Skip to content

Commit fff7d36

Browse files
Merge pull request #117 from Gentleman-Programming/fix/opencode-session-inflation
fix(plugin): skip sub-agent session registration in OpenCode plugin
2 parents 0fd1787 + 43870ea commit fff7d36

File tree

3 files changed

+132
-6
lines changed

3 files changed

+132
-6
lines changed

internal/setup/plugins/opencode/engram.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,22 @@ export const Engram: Plugin = async (ctx) => {
206206
// Track which sessions we've already ensured exist in engram
207207
const knownSessions = new Set<string>()
208208

209+
// Track sub-agent session IDs so we can suppress their tool-hook registrations.
210+
// Sub-agents (Task() calls) have a parentID or a title ending in " subagent)".
211+
// We must not register them as top-level Engram sessions — they cause session
212+
// inflation (e.g. 170 sessions for 1 real conversation, issue #116).
213+
const subAgentSessions = new Set<string>()
214+
209215
/**
210216
* Ensure a session exists in engram. Idempotent — calls POST /sessions
211217
* which uses INSERT OR IGNORE. Safe to call multiple times.
218+
*
219+
* Silently skips sub-agent sessions (tracked in `subAgentSessions`).
212220
*/
213221
async function ensureSession(sessionId: string): Promise<void> {
214222
if (!sessionId || knownSessions.has(sessionId)) return
223+
// Do not register sub-agent sessions in Engram (issue #116).
224+
if (subAgentSessions.has(sessionId)) return
215225
knownSessions.add(sessionId)
216226
await engramFetch("/sessions", {
217227
method: "POST",
@@ -272,18 +282,40 @@ export const Engram: Plugin = async (ctx) => {
272282
event: async ({ event }) => {
273283
// --- Session Created ---
274284
if (event.type === "session.created") {
275-
const sessionId = (event.properties as any)?.id
276-
if (sessionId) {
285+
// Bug fix (#116): session data is nested under event.properties.info,
286+
// not event.properties directly.
287+
const info = (event.properties as any)?.info
288+
const sessionId = info?.id
289+
const parentID = info?.parentID
290+
const title: string = info?.title ?? ""
291+
292+
// Sub-agent sessions (created via Task()) must NOT be registered as
293+
// top-level Engram sessions. They cause massive session inflation
294+
// (e.g. 170 sessions for 1 real conversation).
295+
//
296+
// Detection heuristics:
297+
// - parentID is set on all Task() sub-agent sessions
298+
// - title ends with " subagent)" as a secondary signal
299+
const isSubAgent = !!parentID || title.endsWith(" subagent)")
300+
301+
if (sessionId && !isSubAgent) {
277302
await ensureSession(sessionId)
303+
} else if (sessionId && isSubAgent) {
304+
// Remember this as a sub-agent session so tool-hook calls
305+
// to ensureSession() are also suppressed for it.
306+
subAgentSessions.add(sessionId)
278307
}
279308
}
280309

281310
// --- Session Deleted ---
282311
if (event.type === "session.deleted") {
283-
const sessionId = (event.properties as any)?.id
312+
// Same properties.info path as session.created.
313+
const info = (event.properties as any)?.info
314+
const sessionId = info?.id
284315
if (sessionId) {
285316
toolCounts.delete(sessionId)
286317
knownSessions.delete(sessionId)
318+
subAgentSessions.delete(sessionId)
287319
}
288320
}
289321

internal/setup/setup_test.go

Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2407,3 +2407,65 @@ func TestInstallOpenCodeBakesENGRAMBIN(t *testing.T) {
24072407
}
24082408
})
24092409
}
2410+
2411+
// ─── Issue #116: Sub-agent session inflation fix ─────────────────────────────
2412+
2413+
// TestPluginSubAgentFiltering verifies that the installed plugin source
2414+
// contains the necessary logic to:
2415+
//
2416+
// a) read session data from event.properties.info (not event.properties)
2417+
// b) suppress Task() sub-agent sessions via parentID or title suffix check
2418+
// c) track sub-agent IDs in subAgentSessions for cross-hook suppression
2419+
func TestPluginSubAgentFiltering(t *testing.T) {
2420+
resetSetupSeams(t)
2421+
home := useTestHome(t)
2422+
runtimeGOOS = "linux"
2423+
osExecutable = func() (string, error) { return "/usr/local/bin/engram", nil }
2424+
t.Setenv("XDG_CONFIG_HOME", filepath.Join(home, "xdg"))
2425+
2426+
if _, err := installOpenCode(); err != nil {
2427+
t.Fatalf("installOpenCode failed: %v", err)
2428+
}
2429+
2430+
pluginPath := filepath.Join(home, "xdg", "opencode", "plugins", "engram.ts")
2431+
raw, err := os.ReadFile(pluginPath)
2432+
if err != nil {
2433+
t.Fatalf("read installed plugin: %v", err)
2434+
}
2435+
content := string(raw)
2436+
2437+
// a) Session data must be read from event.properties.info
2438+
if !strings.Contains(content, `event.properties as any)?.info`) {
2439+
t.Fatalf("plugin must read session data from event.properties.info, got:\n%s", content)
2440+
}
2441+
2442+
// b) parentID check: sub-agents with a parentID must not register sessions
2443+
if !strings.Contains(content, `parentID`) {
2444+
t.Fatalf("plugin must check parentID to detect sub-agent sessions")
2445+
}
2446+
2447+
// b) title suffix check: secondary signal for sub-agent detection
2448+
if !strings.Contains(content, `subagent)`) {
2449+
t.Fatalf("plugin must check title suffix ' subagent)' as secondary sub-agent signal")
2450+
}
2451+
2452+
// b) isSubAgent gate: must guard ensureSession() call
2453+
if !strings.Contains(content, `isSubAgent`) {
2454+
t.Fatalf("plugin must use isSubAgent flag to gate ensureSession()")
2455+
}
2456+
2457+
// c) subAgentSessions set must exist for cross-hook suppression
2458+
if !strings.Contains(content, `subAgentSessions`) {
2459+
t.Fatalf("plugin must define subAgentSessions set for cross-hook suppression")
2460+
}
2461+
2462+
// Verify ensureSession itself guards against sub-agent sessions
2463+
if !strings.Contains(content, `subAgentSessions.has(sessionId)`) {
2464+
t.Fatalf("ensureSession must check subAgentSessions before registering")
2465+
}
2466+
2467+
// session.deleted must clean up subAgentSessions too
2468+
if !strings.Contains(content, `subAgentSessions.delete(sessionId)`) {
2469+
t.Fatalf("session.deleted handler must clean up subAgentSessions set")
2470+
}
2471+
}

plugin/opencode/engram.ts

Lines changed: 35 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -206,12 +206,22 @@ export const Engram: Plugin = async (ctx) => {
206206
// Track which sessions we've already ensured exist in engram
207207
const knownSessions = new Set<string>()
208208

209+
// Track sub-agent session IDs so we can suppress their tool-hook registrations.
210+
// Sub-agents (Task() calls) have a parentID or a title ending in " subagent)".
211+
// We must not register them as top-level Engram sessions — they cause session
212+
// inflation (e.g. 170 sessions for 1 real conversation, issue #116).
213+
const subAgentSessions = new Set<string>()
214+
209215
/**
210216
* Ensure a session exists in engram. Idempotent — calls POST /sessions
211217
* which uses INSERT OR IGNORE. Safe to call multiple times.
218+
*
219+
* Silently skips sub-agent sessions (tracked in `subAgentSessions`).
212220
*/
213221
async function ensureSession(sessionId: string): Promise<void> {
214222
if (!sessionId || knownSessions.has(sessionId)) return
223+
// Do not register sub-agent sessions in Engram (issue #116).
224+
if (subAgentSessions.has(sessionId)) return
215225
knownSessions.add(sessionId)
216226
await engramFetch("/sessions", {
217227
method: "POST",
@@ -272,18 +282,40 @@ export const Engram: Plugin = async (ctx) => {
272282
event: async ({ event }) => {
273283
// --- Session Created ---
274284
if (event.type === "session.created") {
275-
const sessionId = (event.properties as any)?.id
276-
if (sessionId) {
285+
// Bug fix (#116): session data is nested under event.properties.info,
286+
// not event.properties directly.
287+
const info = (event.properties as any)?.info
288+
const sessionId = info?.id
289+
const parentID = info?.parentID
290+
const title: string = info?.title ?? ""
291+
292+
// Sub-agent sessions (created via Task()) must NOT be registered as
293+
// top-level Engram sessions. They cause massive session inflation
294+
// (e.g. 170 sessions for 1 real conversation).
295+
//
296+
// Detection heuristics:
297+
// - parentID is set on all Task() sub-agent sessions
298+
// - title ends with " subagent)" as a secondary signal
299+
const isSubAgent = !!parentID || title.endsWith(" subagent)")
300+
301+
if (sessionId && !isSubAgent) {
277302
await ensureSession(sessionId)
303+
} else if (sessionId && isSubAgent) {
304+
// Remember this as a sub-agent session so tool-hook calls
305+
// to ensureSession() are also suppressed for it.
306+
subAgentSessions.add(sessionId)
278307
}
279308
}
280309

281310
// --- Session Deleted ---
282311
if (event.type === "session.deleted") {
283-
const sessionId = (event.properties as any)?.id
312+
// Same properties.info path as session.created.
313+
const info = (event.properties as any)?.info
314+
const sessionId = info?.id
284315
if (sessionId) {
285316
toolCounts.delete(sessionId)
286317
knownSessions.delete(sessionId)
318+
subAgentSessions.delete(sessionId)
287319
}
288320
}
289321

0 commit comments

Comments
 (0)