@agentuity/opencode 1.0.15 → 1.0.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (137) hide show
  1. package/dist/agents/architect.d.ts +1 -1
  2. package/dist/agents/architect.d.ts.map +1 -1
  3. package/dist/agents/architect.js +30 -33
  4. package/dist/agents/architect.js.map +1 -1
  5. package/dist/agents/builder.d.ts +1 -1
  6. package/dist/agents/builder.d.ts.map +1 -1
  7. package/dist/agents/builder.js +53 -60
  8. package/dist/agents/builder.js.map +1 -1
  9. package/dist/agents/expert-backend.d.ts +1 -1
  10. package/dist/agents/expert-backend.d.ts.map +1 -1
  11. package/dist/agents/expert-backend.js +32 -40
  12. package/dist/agents/expert-backend.js.map +1 -1
  13. package/dist/agents/expert-frontend.d.ts +1 -1
  14. package/dist/agents/expert-frontend.d.ts.map +1 -1
  15. package/dist/agents/expert-frontend.js +18 -24
  16. package/dist/agents/expert-frontend.js.map +1 -1
  17. package/dist/agents/expert-ops.d.ts +1 -1
  18. package/dist/agents/expert-ops.d.ts.map +1 -1
  19. package/dist/agents/expert-ops.js +37 -51
  20. package/dist/agents/expert-ops.js.map +1 -1
  21. package/dist/agents/expert.d.ts +1 -1
  22. package/dist/agents/expert.d.ts.map +1 -1
  23. package/dist/agents/expert.js +33 -43
  24. package/dist/agents/expert.js.map +1 -1
  25. package/dist/agents/lead.d.ts +1 -1
  26. package/dist/agents/lead.d.ts.map +1 -1
  27. package/dist/agents/lead.js +179 -222
  28. package/dist/agents/lead.js.map +1 -1
  29. package/dist/agents/memory.d.ts +1 -1
  30. package/dist/agents/memory.d.ts.map +1 -1
  31. package/dist/agents/memory.js +62 -90
  32. package/dist/agents/memory.js.map +1 -1
  33. package/dist/agents/monitor.d.ts +1 -1
  34. package/dist/agents/monitor.d.ts.map +1 -1
  35. package/dist/agents/monitor.js +84 -44
  36. package/dist/agents/monitor.js.map +1 -1
  37. package/dist/agents/product.d.ts +1 -1
  38. package/dist/agents/product.d.ts.map +1 -1
  39. package/dist/agents/product.js +16 -22
  40. package/dist/agents/product.js.map +1 -1
  41. package/dist/agents/reviewer.d.ts +1 -1
  42. package/dist/agents/reviewer.d.ts.map +1 -1
  43. package/dist/agents/reviewer.js +15 -27
  44. package/dist/agents/reviewer.js.map +1 -1
  45. package/dist/agents/runner.d.ts +1 -1
  46. package/dist/agents/runner.d.ts.map +1 -1
  47. package/dist/agents/runner.js +52 -76
  48. package/dist/agents/runner.js.map +1 -1
  49. package/dist/agents/scout.d.ts +1 -1
  50. package/dist/agents/scout.d.ts.map +1 -1
  51. package/dist/agents/scout.js +42 -43
  52. package/dist/agents/scout.js.map +1 -1
  53. package/dist/agents/types.d.ts +8 -0
  54. package/dist/agents/types.d.ts.map +1 -1
  55. package/dist/background/manager.d.ts +18 -0
  56. package/dist/background/manager.d.ts.map +1 -1
  57. package/dist/background/manager.js +201 -33
  58. package/dist/background/manager.js.map +1 -1
  59. package/dist/background/types.d.ts +3 -0
  60. package/dist/background/types.d.ts.map +1 -1
  61. package/dist/config/loader.js +2 -2
  62. package/dist/plugin/hooks/cadence.d.ts +3 -1
  63. package/dist/plugin/hooks/cadence.d.ts.map +1 -1
  64. package/dist/plugin/hooks/cadence.js +167 -70
  65. package/dist/plugin/hooks/cadence.js.map +1 -1
  66. package/dist/plugin/hooks/compaction-utils.d.ts +48 -0
  67. package/dist/plugin/hooks/compaction-utils.d.ts.map +1 -0
  68. package/dist/plugin/hooks/compaction-utils.js +259 -0
  69. package/dist/plugin/hooks/compaction-utils.js.map +1 -0
  70. package/dist/plugin/hooks/completion.d.ts +14 -0
  71. package/dist/plugin/hooks/completion.d.ts.map +1 -0
  72. package/dist/plugin/hooks/completion.js +45 -0
  73. package/dist/plugin/hooks/completion.js.map +1 -0
  74. package/dist/plugin/hooks/params.d.ts +47 -2
  75. package/dist/plugin/hooks/params.d.ts.map +1 -1
  76. package/dist/plugin/hooks/params.js +82 -1
  77. package/dist/plugin/hooks/params.js.map +1 -1
  78. package/dist/plugin/hooks/session-memory.d.ts +2 -1
  79. package/dist/plugin/hooks/session-memory.d.ts.map +1 -1
  80. package/dist/plugin/hooks/session-memory.js +101 -48
  81. package/dist/plugin/hooks/session-memory.js.map +1 -1
  82. package/dist/plugin/hooks/tools.d.ts.map +1 -1
  83. package/dist/plugin/hooks/tools.js +26 -1
  84. package/dist/plugin/hooks/tools.js.map +1 -1
  85. package/dist/plugin/plugin.d.ts.map +1 -1
  86. package/dist/plugin/plugin.js +38 -9
  87. package/dist/plugin/plugin.js.map +1 -1
  88. package/dist/sqlite/index.d.ts +1 -1
  89. package/dist/sqlite/index.d.ts.map +1 -1
  90. package/dist/sqlite/queries.d.ts +1 -0
  91. package/dist/sqlite/queries.d.ts.map +1 -1
  92. package/dist/sqlite/queries.js +4 -0
  93. package/dist/sqlite/queries.js.map +1 -1
  94. package/dist/sqlite/reader.d.ts +11 -1
  95. package/dist/sqlite/reader.d.ts.map +1 -1
  96. package/dist/sqlite/reader.js +62 -0
  97. package/dist/sqlite/reader.js.map +1 -1
  98. package/dist/sqlite/types.d.ts +40 -0
  99. package/dist/sqlite/types.d.ts.map +1 -1
  100. package/dist/tools/background.d.ts.map +1 -1
  101. package/dist/tools/background.js +15 -0
  102. package/dist/tools/background.js.map +1 -1
  103. package/dist/types.d.ts +46 -0
  104. package/dist/types.d.ts.map +1 -1
  105. package/dist/types.js +10 -0
  106. package/dist/types.js.map +1 -1
  107. package/package.json +3 -3
  108. package/src/agents/architect.ts +30 -33
  109. package/src/agents/builder.ts +53 -60
  110. package/src/agents/expert-backend.ts +32 -40
  111. package/src/agents/expert-frontend.ts +18 -24
  112. package/src/agents/expert-ops.ts +37 -51
  113. package/src/agents/expert.ts +33 -43
  114. package/src/agents/lead.ts +179 -222
  115. package/src/agents/memory.ts +62 -90
  116. package/src/agents/monitor.ts +84 -44
  117. package/src/agents/product.ts +16 -22
  118. package/src/agents/reviewer.ts +15 -27
  119. package/src/agents/runner.ts +52 -76
  120. package/src/agents/scout.ts +42 -43
  121. package/src/agents/types.ts +8 -0
  122. package/src/background/manager.ts +227 -38
  123. package/src/background/types.ts +3 -0
  124. package/src/config/loader.ts +2 -2
  125. package/src/plugin/hooks/cadence.ts +188 -74
  126. package/src/plugin/hooks/compaction-utils.ts +291 -0
  127. package/src/plugin/hooks/completion.ts +61 -0
  128. package/src/plugin/hooks/params.ts +107 -2
  129. package/src/plugin/hooks/session-memory.ts +113 -47
  130. package/src/plugin/hooks/tools.ts +32 -1
  131. package/src/plugin/plugin.ts +54 -10
  132. package/src/sqlite/index.ts +4 -0
  133. package/src/sqlite/queries.ts +5 -0
  134. package/src/sqlite/reader.ts +69 -0
  135. package/src/sqlite/types.ts +40 -0
  136. package/src/tools/background.ts +28 -0
  137. package/src/types.ts +40 -0
@@ -2,6 +2,19 @@ import type { PluginInput } from '@opencode-ai/plugin';
2
2
  import type { CoderConfig } from '../../types';
3
3
  import type { BackgroundManager } from '../../background';
4
4
  import type { OpenCodeDBReader, SessionTreeNode } from '../../sqlite';
5
+ import type { CompactionStats } from '../../sqlite/types';
6
+ import {
7
+ getCurrentBranch,
8
+ buildCustomCompactionPrompt,
9
+ fetchAndFormatPlanningState,
10
+ getImageDescriptions,
11
+ getRecentToolCallSummaries,
12
+ storePreCompactionSnapshot,
13
+ persistCadenceStateToKV,
14
+ restoreCadenceStateFromKV,
15
+ formatCompactionDiagnostics,
16
+ countListItems,
17
+ } from './compaction-utils';
5
18
 
6
19
  /** Compacting hook input/output types */
7
20
  type CompactingInput = { sessionID: string };
@@ -13,27 +26,12 @@ export interface CadenceHooks {
13
26
  onCompacting: (input: CompactingInput, output: CompactingOutput) => Promise<void>;
14
27
  /** Check if a session is currently in Cadence mode */
15
28
  isActiveCadenceSession: (sessionId: string) => boolean;
29
+ /** Lazy restore: check KV for persisted Cadence state and populate in-memory Map */
30
+ tryRestoreFromKV: (sessionId: string) => Promise<boolean>;
16
31
  }
17
32
 
18
33
  const COMPLETION_PATTERN = /<promise>\s*DONE\s*<\/promise>/i;
19
34
 
20
- /**
21
- * Get the current git branch name.
22
- */
23
- async function getCurrentBranch(): Promise<string> {
24
- try {
25
- const proc = Bun.spawn(['git', 'branch', '--show-current'], {
26
- stdout: 'pipe',
27
- stderr: 'pipe',
28
- });
29
- const stdout = await new Response(proc.stdout).text();
30
- await proc.exited;
31
- return stdout.trim() || 'unknown';
32
- } catch {
33
- return 'unknown';
34
- }
35
- }
36
-
37
35
  // Ultrawork trigger keywords - case insensitive matching
38
36
  const ULTRAWORK_TRIGGERS = [
39
37
  'ultrawork',
@@ -69,11 +67,14 @@ interface CadenceSessionState {
69
67
  */
70
68
  export function createCadenceHooks(
71
69
  ctx: PluginInput,
72
- _config: CoderConfig,
70
+ config: CoderConfig,
73
71
  backgroundManager?: BackgroundManager,
74
- dbReader?: OpenCodeDBReader
72
+ dbReader?: OpenCodeDBReader,
73
+ lastUserMessages?: Map<string, string>
75
74
  ): CadenceHooks {
76
75
  const activeCadenceSessions = new Map<string, CadenceSessionState>();
76
+ const nonCadenceSessions = new Set<string>();
77
+ const NON_CADENCE_CACHE_MAX = 500;
77
78
 
78
79
  const log = (msg: string) => {
79
80
  ctx.client.app.log({
@@ -90,11 +91,14 @@ export function createCadenceHooks(
90
91
  const sessionId = extractSessionId(input);
91
92
  if (!sessionId) return;
92
93
 
93
- const messageText = extractMessageText(output);
94
- if (!messageText) return;
95
-
96
- // Check if this is a Cadence start command or ultrawork trigger
97
- const cadenceType = getCadenceTriggerType(messageText);
94
+ // Use the USER's message (from chat.params) for trigger detection,
95
+ // not the model's output — avoids false positives when the model
96
+ // uses phrases like "go deep" or "be thorough" in its response.
97
+ // Delete after read entries are transient (set in chat.params,
98
+ // consumed here in chat.message) so no unbounded Map growth.
99
+ const userText = lastUserMessages?.get(sessionId) ?? '';
100
+ lastUserMessages?.delete(sessionId);
101
+ const cadenceType = getCadenceTriggerType(userText);
98
102
  if (cadenceType && !activeCadenceSessions.has(sessionId)) {
99
103
  log(`Cadence started for session ${sessionId} via ${cadenceType}`);
100
104
  const now = new Date().toISOString();
@@ -105,6 +109,8 @@ export function createCadenceHooks(
105
109
  lastActivity: now,
106
110
  };
107
111
  activeCadenceSessions.set(sessionId, state);
112
+ nonCadenceSessions.delete(sessionId);
113
+ persistCadenceStateToKV(sessionId, { ...state }).catch(() => {});
108
114
 
109
115
  // If triggered by ultrawork keywords, inject [CADENCE MODE] tag
110
116
  if (cadenceType === 'ultrawork') {
@@ -115,6 +121,12 @@ export function createCadenceHooks(
115
121
  return;
116
122
  }
117
123
 
124
+ // Everything below parses the MODEL's output for structured tags
125
+ // (CADENCE_STATUS, iteration counts, completion signals) that the
126
+ // model intentionally emits — these are NOT false-positive-prone.
127
+ const messageText = extractMessageText(output);
128
+ if (!messageText) return;
129
+
118
130
  // Check if this session is in Cadence mode
119
131
  const state = activeCadenceSessions.get(sessionId);
120
132
  if (!state) {
@@ -146,6 +158,7 @@ export function createCadenceHooks(
146
158
  if (changed) {
147
159
  const loopInfo = state.loopId ? ` · ${state.loopId}` : '';
148
160
  showToast(ctx, `⚡ Cadence · ${state.iteration}/${state.maxIterations}${loopInfo}`);
161
+ persistCadenceStateToKV(sessionId, { ...state }).catch(() => {});
149
162
  }
150
163
  return;
151
164
  }
@@ -158,6 +171,7 @@ export function createCadenceHooks(
158
171
  state.iteration = newIteration;
159
172
  const loopInfo = state.loopId ? ` · ${state.loopId}` : '';
160
173
  showToast(ctx, `⚡ Cadence · ${state.iteration}/${state.maxIterations}${loopInfo}`);
174
+ persistCadenceStateToKV(sessionId, { ...state }).catch(() => {});
161
175
  }
162
176
  }
163
177
 
@@ -183,7 +197,10 @@ export function createCadenceHooks(
183
197
 
184
198
  log(`Event received: ${event.type}`);
185
199
 
186
- // Handle session.compacted - save compaction AND continue loop
200
+ // Handle session.compacted - save compaction AND continue loop.
201
+ // Note: Compaction continues in the SAME session (via session.prompt with
202
+ // the existing sessionId), so permissions configured in the config hook
203
+ // (plugin.ts) are automatically inherited — no re-application needed.
187
204
  if (event.type === 'session.compacted') {
188
205
  const sessionId = event.sessionId;
189
206
  if (!sessionId) return;
@@ -285,6 +302,8 @@ Continue Cadence iteration ${state.iteration} of ${state.maxIterations}
285
302
  /**
286
303
  * Called during context compaction to inject Cadence state.
287
304
  * This ensures the compaction summary includes critical loop state.
305
+ * Uses output.prompt to REPLACE the default compaction prompt with
306
+ * enriched context (planning state, images, tool calls, diagnostics).
288
307
  */
289
308
  async onCompacting(input: CompactingInput, output: CompactingOutput): Promise<void> {
290
309
  const sessionId = input.sessionID;
@@ -298,12 +317,53 @@ Continue Cadence iteration ${state.iteration} of ${state.maxIterations}
298
317
  log(`Injecting Cadence context during compaction for session ${sessionId}`);
299
318
  showToast(ctx, '💾 Compacting Cadence context...');
300
319
 
301
- // Get current git branch
302
- const branch = await getCurrentBranch();
320
+ // Config flags for compaction behavior
321
+ const compactionCfg = config?.compaction ?? {};
322
+ const useCustomPrompt = compactionCfg.customPrompt !== false;
323
+ const useInlinePlanning = compactionCfg.inlinePlanning !== false;
324
+ const useImageAwareness = compactionCfg.imageAwareness !== false;
325
+ const useSnapshotToKV = compactionCfg.snapshotToKV !== false;
326
+ const maxTokens = compactionCfg.maxContextTokens ?? 4000;
327
+
328
+ // 1. Build custom compaction instructions
329
+ const instructions = useCustomPrompt ? buildCustomCompactionPrompt('cadence') : null;
330
+
331
+ // 2. Gather enrichment data in parallel
332
+ const toolCallLimit = config?.compaction?.toolCallSummaryLimit ?? 5;
333
+ const [branch, planningState, imageDescs, toolSummaries] = await Promise.all([
334
+ getCurrentBranch(),
335
+ useInlinePlanning ? fetchAndFormatPlanningState(sessionId) : Promise.resolve(null),
336
+ useImageAwareness
337
+ ? Promise.resolve(getImageDescriptions(dbReader ?? null, sessionId))
338
+ : Promise.resolve(null),
339
+ Promise.resolve(getRecentToolCallSummaries(dbReader ?? null, sessionId, toolCallLimit)),
340
+ ]);
341
+
342
+ // 3. Build Cadence state section
343
+ const cadenceStateSection = `## CADENCE MODE ACTIVE
344
+
345
+ This session is running in Cadence mode (long-running autonomous loop).
303
346
 
304
- // Get active background tasks for this session
347
+ **Cadence State:**
348
+ - Session ID: ${sessionId}
349
+ - Loop ID: ${state.loopId ?? 'unknown'}
350
+ - Branch: ${branch}
351
+ - Started: ${state.startedAt}
352
+ - Iteration: ${state.iteration} / ${state.maxIterations}
353
+ - Last activity: ${state.lastActivity}
354
+
355
+ **Session Record Location:**
356
+ \`session:${sessionId}\` in agentuity-opencode-memory
357
+
358
+ After compaction:
359
+ 1. Memory will save this summary and update the session record
360
+ 2. Memory should update planning.progress with this compaction
361
+ 3. Lead will continue the loop from iteration ${state.iteration}
362
+ 4. Use 5-Question Reboot to re-orient: Where am I? Where going? Goal? Learned? Done?`;
363
+
364
+ // 4. Build background tasks section
305
365
  const tasks = backgroundManager?.getTasksByParent(sessionId) ?? [];
306
- let backgroundTaskContext = '';
366
+ let backgroundSection: string | null = null;
307
367
 
308
368
  if (tasks.length > 0) {
309
369
  const taskList = tasks
@@ -313,9 +373,7 @@ Continue Cadence iteration ${state.iteration} of ${state.maxIterations}
313
373
  )
314
374
  .join('\n');
315
375
 
316
- backgroundTaskContext = `
317
-
318
- ## Active Background Tasks
376
+ backgroundSection = `## Active Background Tasks
319
377
 
320
378
  This session has ${tasks.length} background task(s) running in separate sessions:
321
379
  ${taskList}
@@ -324,52 +382,70 @@ ${taskList}
324
382
  Use \`agentuity_background_output({ task_id: "..." })\` to check their status.
325
383
  Use \`agentuity_session_dashboard({ session_id: "..." })\` to get a full session tree with status, costs, and health summary for Lead-of-Leads monitoring.
326
384
 
327
- **Tip:** If you spawned child Leads for parallel work, delegate monitoring to BackgroundMonitor:
328
- \`\`\`typescript
329
- agentuity_background_task({
330
- agent: "monitor",
331
- task: "Monitor these background tasks and report when all complete:\\n${tasks.map((t) => `- ${t.id}`).join('\\n')}",
332
- description: "Monitor child tasks"
333
- })
334
- \`\`\`
335
- `;
385
+ **Tip:** A Monitor agent is auto-launched to watch these tasks. You will receive \`[BACKGROUND TASK COMPLETED]\` notifications as each task finishes, and \`[ALL BACKGROUND TASKS COMPLETE]\` when all are done. Use \`agentuity_session_dashboard\` for a unified progress view.`;
336
386
  }
337
387
 
338
- output.context.push(`
339
- ## CADENCE MODE ACTIVE
340
-
341
- This session is running in Cadence mode (long-running autonomous loop).
342
-
343
- **Cadence State:**
344
- - Session ID: ${sessionId}
345
- - Loop ID: ${state.loopId ?? 'unknown'}
346
- - Branch: ${branch}
347
- - Started: ${state.startedAt}
348
- - Iteration: ${state.iteration} / ${state.maxIterations}
349
- - Last activity: ${state.lastActivity}
350
-
351
- **Session Record Location:**
352
- \`session:${sessionId}\` in agentuity-opencode-memory
388
+ // 5. Build SQLite dashboard section
389
+ const dashboardSection = buildSqliteDashboardSummary(dbReader, sessionId);
390
+
391
+ // 6. Combine everything into the full prompt
392
+ const sections: string[] = [];
393
+ if (instructions) sections.push(instructions);
394
+ sections.push(cadenceStateSection);
395
+ if (backgroundSection) sections.push(backgroundSection);
396
+ if (planningState) sections.push(planningState);
397
+ if (imageDescs) sections.push(imageDescs);
398
+ if (toolSummaries) sections.push(toolSummaries);
399
+ if (dashboardSection) sections.push(dashboardSection);
400
+
401
+ // 7. Add diagnostics
402
+ const stats: CompactionStats = {
403
+ planningPhasesCount: countListItems(planningState),
404
+ backgroundTasksCount: tasks.length,
405
+ imageDescriptionsCount: countListItems(imageDescs),
406
+ toolCallSummariesCount: countListItems(toolSummaries),
407
+ estimatedTokens: Math.ceil(sections.join('\n\n').length / 4),
408
+ };
409
+ const diagnostics = formatCompactionDiagnostics(stats);
410
+ if (diagnostics) sections.push(diagnostics);
411
+
412
+ // 8. Enforce token budget
413
+ let fullPrompt = sections.join('\n\n');
414
+ const estimatedTokens = Math.ceil(fullPrompt.length / 4);
415
+ if (maxTokens > 0 && estimatedTokens > maxTokens) {
416
+ // Trim least-critical sections first
417
+ const trimOrder = [diagnostics, toolSummaries, imageDescs, planningState].filter(
418
+ Boolean
419
+ );
420
+ let trimmed = [...sections];
421
+ for (const candidate of trimOrder) {
422
+ if (Math.ceil(trimmed.join('\n\n').length / 4) <= maxTokens) break;
423
+ trimmed = trimmed.filter((s) => s !== candidate);
424
+ }
425
+ fullPrompt = trimmed.join('\n\n');
426
+ }
353
427
 
354
- **Planning State:**
355
- If this session has planning active, the session record contains:
356
- - \`planning.prdKey\` - Link to the PRD being executed
357
- - \`planning.objective\` - What we're trying to accomplish
358
- - \`planning.phases\` - Current phases with status and notes
359
- - \`planning.current\` - Current phase
360
- - \`planning.findings\` - Discoveries made during work
361
- - \`planning.errors\` - Failures to avoid repeating
362
- ${backgroundTaskContext}
363
- After compaction:
364
- 1. Memory will save this summary and update the session record
365
- 2. Memory should update planning.progress with this compaction
366
- 3. Lead will continue the loop from iteration ${state.iteration}
367
- 4. Use 5-Question Reboot to re-orient: Where am I? Where going? Goal? Learned? Done?
368
- `);
428
+ // 9. Set the full prompt or push to context
429
+ if (useCustomPrompt) {
430
+ output.prompt = fullPrompt;
431
+ } else {
432
+ output.context.push(fullPrompt);
433
+ }
369
434
 
370
- const dashboardSummary = buildSqliteDashboardSummary(dbReader, sessionId);
371
- if (dashboardSummary) {
372
- output.context.push(dashboardSummary);
435
+ // 10. Store pre-compaction snapshot to KV (fire-and-forget)
436
+ if (useSnapshotToKV) {
437
+ storePreCompactionSnapshot(sessionId, {
438
+ timestamp: new Date().toISOString(),
439
+ sessionId,
440
+ planningState: planningState ? { raw: planningState } : undefined,
441
+ backgroundTasks: tasks.map((t) => ({
442
+ id: t.id,
443
+ description: t.description || 'No description',
444
+ status: t.status,
445
+ })),
446
+ cadenceState: state ? { ...state } : undefined,
447
+ branch,
448
+ }).catch(() => {}); // Fire and forget
373
449
  }
374
450
  },
375
451
 
@@ -380,6 +456,44 @@ After compaction:
380
456
  isActiveCadenceSession(sessionId: string): boolean {
381
457
  return activeCadenceSessions.has(sessionId);
382
458
  },
459
+
460
+ /**
461
+ * Lazy restore: check KV for persisted Cadence state and populate in-memory Map.
462
+ * Called before routing decisions to recover state after plugin restarts.
463
+ * Returns true if state was found and restored.
464
+ */
465
+ async tryRestoreFromKV(sessionId: string): Promise<boolean> {
466
+ // Already in memory — nothing to restore
467
+ if (activeCadenceSessions.has(sessionId)) return true;
468
+ // Known non-Cadence session — skip KV lookup
469
+ if (nonCadenceSessions.has(sessionId)) return false;
470
+
471
+ try {
472
+ const kvState = await restoreCadenceStateFromKV(sessionId);
473
+ if (!kvState) {
474
+ if (nonCadenceSessions.size >= NON_CADENCE_CACHE_MAX) {
475
+ nonCadenceSessions.clear();
476
+ }
477
+ nonCadenceSessions.add(sessionId);
478
+ return false;
479
+ }
480
+
481
+ const state: CadenceSessionState = {
482
+ startedAt: (kvState.startedAt as string) ?? new Date().toISOString(),
483
+ loopId: kvState.loopId as string | undefined,
484
+ iteration: (kvState.iteration as number) ?? 1,
485
+ maxIterations: (kvState.maxIterations as number) ?? 50,
486
+ lastActivity: (kvState.lastActivity as string) ?? new Date().toISOString(),
487
+ };
488
+ activeCadenceSessions.set(sessionId, state);
489
+ log(
490
+ `Restored Cadence state from KV for session ${sessionId} (iteration ${state.iteration}/${state.maxIterations})`
491
+ );
492
+ return true;
493
+ } catch {
494
+ return false;
495
+ }
496
+ },
383
497
  };
384
498
  }
385
499
 
@@ -0,0 +1,291 @@
1
+ import type { OpenCodeDBReader } from '../../sqlite/reader';
2
+ import type { CompactionStats, DBNonTextPart, PreCompactionSnapshot } from '../../sqlite/types';
3
+
4
+ /**
5
+ * Get the current git branch name.
6
+ * Moved here from cadence.ts and session-memory.ts to deduplicate.
7
+ */
8
+ export async function getCurrentBranch(): Promise<string> {
9
+ try {
10
+ const proc = Bun.spawn(['git', 'branch', '--show-current'], {
11
+ stdout: 'pipe',
12
+ stderr: 'pipe',
13
+ });
14
+ const stdout = await new Response(proc.stdout).text();
15
+ await proc.exited;
16
+ return stdout.trim() || 'unknown';
17
+ } catch {
18
+ return 'unknown';
19
+ }
20
+ }
21
+
22
+ /**
23
+ * Access Agentuity KV storage via CLI.
24
+ * All calls are wrapped in try/catch — returns null on failure.
25
+ */
26
+ async function kvGet(namespace: string, key: string): Promise<unknown | null> {
27
+ try {
28
+ const proc = Bun.spawn(['agentuity', 'cloud', 'kv', 'get', namespace, key, '--json'], {
29
+ stdout: 'pipe',
30
+ stderr: 'pipe',
31
+ });
32
+ const output = await new Response(proc.stdout).text();
33
+ const exitCode = await proc.exited;
34
+ if (exitCode !== 0) return null;
35
+ return JSON.parse(output);
36
+ } catch {
37
+ return null;
38
+ }
39
+ }
40
+
41
+ async function kvSet(namespace: string, key: string, value: unknown): Promise<boolean> {
42
+ try {
43
+ const proc = Bun.spawn(
44
+ ['agentuity', 'cloud', 'kv', 'set', namespace, key, JSON.stringify(value)],
45
+ { stdout: 'pipe', stderr: 'pipe' }
46
+ );
47
+ const exitCode = await proc.exited;
48
+ return exitCode === 0;
49
+ } catch {
50
+ return false;
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Build the custom compaction prompt for our agent system.
56
+ * This REPLACES the default OpenCode compaction prompt via output.prompt.
57
+ */
58
+ export function buildCustomCompactionPrompt(mode: 'cadence' | 'regular'): string {
59
+ const cadenceSection =
60
+ mode === 'cadence'
61
+ ? `
62
+
63
+ ## Cadence Loop State
64
+ - Loop ID, iteration number, max iterations
65
+ - Current phase and what's in progress
66
+ - Whether this is a Lead-of-Leads session with child tasks`
67
+ : '';
68
+
69
+ return `You are generating a continuation context for a multi-agent coding system (Agentuity Coder). Your summary will be the ONLY context the orchestrating Lead agent has after this compaction. Preserve everything needed for seamless continuation.
70
+
71
+ ## CRITICAL — Preserve These Verbatim
72
+ 1. The current task/objective (quote the user's original request exactly)
73
+ 2. All background task IDs (bg_xxx) with status, purpose, and session IDs
74
+ 3. Active planning state: current phase, completed phases, next steps, blockers
75
+ 4. ALL file paths being actively worked on (with role: created/modified/read)
76
+ 5. Key decisions made and their rationale
77
+ 6. Any corrections or gotchas discovered during the session
78
+ 7. Todo list state (what's done, in progress, pending)
79
+ 8. Descriptions of any images or attachments that appeared in conversation${cadenceSection}
80
+
81
+ ## Structure Your Summary As:
82
+
83
+ ### Active Task
84
+ [Verbatim objective + what the agent was doing when compaction fired]
85
+
86
+ ### Planning State
87
+ [Phases with status. Include phase notes, not just titles.]
88
+
89
+ ### Background Tasks
90
+ [bg_xxx: description → status (running/completed/errored). Include session IDs.]
91
+
92
+ ### Key Context
93
+ [Decisions, constraints, user preferences, corrections discovered]
94
+
95
+ ### Active Files
96
+ [filepath → role (creating/modifying/reading) + what's being done to it]
97
+
98
+ ### Images & Attachments
99
+ [Describe any images/screenshots: what they showed, when they appeared, why they mattered]
100
+
101
+ ### Next Steps
102
+ [What should happen immediately after compaction resumes]
103
+
104
+ ## Rules
105
+ - Use specific file paths, task IDs, phase names — NOT vague references.
106
+ - State what tools returned, not just that they were called.
107
+ - NEVER drop background task references — the agent MUST know what's still running.
108
+ - Prefer completeness over brevity — this is the agent's entire working memory.`;
109
+ }
110
+
111
+ /**
112
+ * Fetch planning state from KV and format as markdown.
113
+ * Returns null if KV is unavailable or no planning state exists.
114
+ */
115
+ export async function fetchAndFormatPlanningState(sessionId: string): Promise<string | null> {
116
+ try {
117
+ const record = await kvGet('agentuity-opencode-memory', `session:${sessionId}`);
118
+ if (!record || typeof record !== 'object') return null;
119
+
120
+ const data = (record as Record<string, unknown>).data ?? record;
121
+ const planning = (data as Record<string, unknown>).planning as
122
+ | Record<string, unknown>
123
+ | undefined;
124
+ if (!planning) return null;
125
+
126
+ const lines: string[] = ['## Planning State (from KV)'];
127
+ if (planning.objective) lines.push(`**Objective:** ${planning.objective}`);
128
+ if (planning.current) lines.push(`**Current:** ${planning.current}`);
129
+ if (planning.next) lines.push(`**Next:** ${planning.next}`);
130
+
131
+ const phases = planning.phases as Array<Record<string, unknown>> | undefined;
132
+ if (phases?.length) {
133
+ lines.push('', '### Phases:');
134
+ for (const p of phases) {
135
+ const status = p.status ?? 'unknown';
136
+ const title = p.title ?? p.content ?? 'untitled';
137
+ const notes = p.notes ? ` — ${String(p.notes).slice(0, 100)}` : '';
138
+ lines.push(`- [${status}] ${title}${notes}`);
139
+ }
140
+ }
141
+
142
+ const findings = planning.findings as string[] | undefined;
143
+ if (findings?.length) {
144
+ lines.push('', '### Key Findings:');
145
+ for (const f of findings.slice(0, 5)) {
146
+ lines.push(`- ${String(f).slice(0, 150)}`);
147
+ }
148
+ }
149
+
150
+ const errors = planning.errors as string[] | undefined;
151
+ if (errors?.length) {
152
+ lines.push('', '### Errors to Avoid:');
153
+ for (const e of errors.slice(0, 3)) {
154
+ lines.push(`- ${String(e).slice(0, 150)}`);
155
+ }
156
+ }
157
+
158
+ return lines.join('\n');
159
+ } catch {
160
+ return null;
161
+ }
162
+ }
163
+
164
+ /**
165
+ * Get image/attachment descriptions from SQLite for compaction context.
166
+ * Returns brief metadata about non-text parts in the conversation.
167
+ */
168
+ export function getImageDescriptions(
169
+ dbReader: OpenCodeDBReader | null,
170
+ sessionId: string
171
+ ): string | null {
172
+ if (!dbReader?.isAvailable()) return null;
173
+
174
+ try {
175
+ const parts = dbReader.getNonTextParts(sessionId);
176
+ if (!parts.length) return null;
177
+
178
+ // Filter to image-like parts (not tool calls — those are separate)
179
+ const imageParts = parts.filter(
180
+ (p: DBNonTextPart) => !['tool-invocation', 'tool-result', 'text'].includes(p.type)
181
+ );
182
+ if (!imageParts.length) return null;
183
+
184
+ const lines: string[] = ['## Images & Attachments'];
185
+ for (const part of imageParts.slice(0, 10)) {
186
+ const when = part.timestamp ? ` at ${part.timestamp}` : '';
187
+ lines.push(`- [${part.type}]${when}: message ${part.messageId}`);
188
+ }
189
+ return lines.join('\n');
190
+ } catch {
191
+ return null;
192
+ }
193
+ }
194
+
195
+ /**
196
+ * Get recent tool call summaries for compaction context.
197
+ * CONCISE — capped at limit calls, brief descriptions only.
198
+ */
199
+ export function getRecentToolCallSummaries(
200
+ dbReader: OpenCodeDBReader | null,
201
+ sessionId: string,
202
+ limit: number = 5
203
+ ): string | null {
204
+ if (!dbReader?.isAvailable() || limit <= 0) return null;
205
+
206
+ try {
207
+ const calls = dbReader.getRecentToolCalls(sessionId, limit);
208
+ if (!calls.length) return null;
209
+
210
+ const lines: string[] = ['## Recent Tool Activity'];
211
+ for (const call of calls) {
212
+ const inputBrief = call.input ? ` — ${String(call.input).slice(0, 80)}` : '';
213
+ const outputBrief = call.output ? ` → ${String(call.output).slice(0, 80)}` : '';
214
+ lines.push(`- ${call.toolName}${inputBrief}${outputBrief}`);
215
+ }
216
+ return lines.join('\n');
217
+ } catch {
218
+ return null;
219
+ }
220
+ }
221
+
222
+ /**
223
+ * Store a pre-compaction snapshot to KV as a recovery mechanism.
224
+ */
225
+ export async function storePreCompactionSnapshot(
226
+ sessionId: string,
227
+ snapshot: PreCompactionSnapshot
228
+ ): Promise<void> {
229
+ try {
230
+ await kvSet('agentuity-opencode-memory', `compaction:snapshot:${sessionId}`, snapshot);
231
+ } catch {
232
+ // Silently fail — this is a best-effort recovery mechanism
233
+ }
234
+ }
235
+
236
+ /**
237
+ * Persist Cadence session state to KV for recovery after plugin restart.
238
+ */
239
+ export async function persistCadenceStateToKV(
240
+ sessionId: string,
241
+ state: Record<string, unknown>
242
+ ): Promise<void> {
243
+ try {
244
+ await kvSet('agentuity-opencode-memory', `cadence:active:${sessionId}`, state);
245
+ } catch {
246
+ // Silently fail
247
+ }
248
+ }
249
+
250
+ /**
251
+ * Restore Cadence session state from KV.
252
+ */
253
+ export async function restoreCadenceStateFromKV(
254
+ sessionId: string
255
+ ): Promise<Record<string, unknown> | null> {
256
+ try {
257
+ const state = await kvGet('agentuity-opencode-memory', `cadence:active:${sessionId}`);
258
+ return state as Record<string, unknown> | null;
259
+ } catch {
260
+ return null;
261
+ }
262
+ }
263
+
264
+ /**
265
+ * Format compaction diagnostics — brief summary of what was preserved.
266
+ */
267
+ export function formatCompactionDiagnostics(stats: CompactionStats): string {
268
+ const parts: string[] = [];
269
+ if (stats.planningPhasesCount > 0) parts.push(`${stats.planningPhasesCount} planning phases`);
270
+ if (stats.backgroundTasksCount > 0) parts.push(`${stats.backgroundTasksCount} background tasks`);
271
+ if (stats.imageDescriptionsCount > 0) parts.push(`${stats.imageDescriptionsCount} image refs`);
272
+ if (stats.toolCallSummariesCount > 0) parts.push(`${stats.toolCallSummariesCount} tool calls`);
273
+
274
+ if (!parts.length) return '';
275
+ return `> **Compaction preserved:** ${parts.join(', ')} (~${stats.estimatedTokens} tokens injected)`;
276
+ }
277
+
278
+ /** Count markdown list items in a string */
279
+ export function countListItems(s: string | null): number {
280
+ if (!s) return 0;
281
+ return (s.match(/^- /gm) ?? []).length;
282
+ }
283
+
284
+ // Re-export types used by consumers of this module
285
+ export type { CompactionConfig } from '../../types';
286
+ export type {
287
+ CompactionStats,
288
+ DBNonTextPart,
289
+ DBToolCallSummary,
290
+ PreCompactionSnapshot,
291
+ } from '../../sqlite/types';