@agentuity/opencode 1.0.15 → 1.0.16

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 (74) hide show
  1. package/dist/agents/expert-backend.js +1 -1
  2. package/dist/agents/expert-backend.js.map +1 -1
  3. package/dist/agents/expert-frontend.js +1 -1
  4. package/dist/agents/expert-frontend.js.map +1 -1
  5. package/dist/agents/expert-ops.js +1 -1
  6. package/dist/agents/expert-ops.js.map +1 -1
  7. package/dist/agents/expert.js +1 -1
  8. package/dist/agents/expert.js.map +1 -1
  9. package/dist/agents/monitor.d.ts +1 -1
  10. package/dist/agents/monitor.d.ts.map +1 -1
  11. package/dist/agents/monitor.js +22 -33
  12. package/dist/agents/monitor.js.map +1 -1
  13. package/dist/agents/reviewer.js +1 -1
  14. package/dist/agents/reviewer.js.map +1 -1
  15. package/dist/agents/scout.js +1 -1
  16. package/dist/agents/scout.js.map +1 -1
  17. package/dist/background/manager.d.ts +1 -0
  18. package/dist/background/manager.d.ts.map +1 -1
  19. package/dist/background/manager.js +60 -26
  20. package/dist/background/manager.js.map +1 -1
  21. package/dist/plugin/hooks/cadence.d.ts +3 -1
  22. package/dist/plugin/hooks/cadence.d.ts.map +1 -1
  23. package/dist/plugin/hooks/cadence.js +167 -66
  24. package/dist/plugin/hooks/cadence.js.map +1 -1
  25. package/dist/plugin/hooks/compaction-utils.d.ts +48 -0
  26. package/dist/plugin/hooks/compaction-utils.d.ts.map +1 -0
  27. package/dist/plugin/hooks/compaction-utils.js +259 -0
  28. package/dist/plugin/hooks/compaction-utils.js.map +1 -0
  29. package/dist/plugin/hooks/params.d.ts +1 -1
  30. package/dist/plugin/hooks/params.d.ts.map +1 -1
  31. package/dist/plugin/hooks/params.js +5 -1
  32. package/dist/plugin/hooks/params.js.map +1 -1
  33. package/dist/plugin/hooks/session-memory.d.ts +2 -1
  34. package/dist/plugin/hooks/session-memory.d.ts.map +1 -1
  35. package/dist/plugin/hooks/session-memory.js +97 -48
  36. package/dist/plugin/hooks/session-memory.js.map +1 -1
  37. package/dist/plugin/plugin.d.ts.map +1 -1
  38. package/dist/plugin/plugin.js +31 -9
  39. package/dist/plugin/plugin.js.map +1 -1
  40. package/dist/sqlite/index.d.ts +1 -1
  41. package/dist/sqlite/index.d.ts.map +1 -1
  42. package/dist/sqlite/queries.d.ts +1 -0
  43. package/dist/sqlite/queries.d.ts.map +1 -1
  44. package/dist/sqlite/queries.js +4 -0
  45. package/dist/sqlite/queries.js.map +1 -1
  46. package/dist/sqlite/reader.d.ts +11 -1
  47. package/dist/sqlite/reader.d.ts.map +1 -1
  48. package/dist/sqlite/reader.js +62 -0
  49. package/dist/sqlite/reader.js.map +1 -1
  50. package/dist/sqlite/types.d.ts +40 -0
  51. package/dist/sqlite/types.d.ts.map +1 -1
  52. package/dist/types.d.ts +36 -0
  53. package/dist/types.d.ts.map +1 -1
  54. package/dist/types.js +10 -0
  55. package/dist/types.js.map +1 -1
  56. package/package.json +3 -3
  57. package/src/agents/expert-backend.ts +1 -1
  58. package/src/agents/expert-frontend.ts +1 -1
  59. package/src/agents/expert-ops.ts +1 -1
  60. package/src/agents/expert.ts +1 -1
  61. package/src/agents/monitor.ts +22 -33
  62. package/src/agents/reviewer.ts +1 -1
  63. package/src/agents/scout.ts +1 -1
  64. package/src/background/manager.ts +67 -31
  65. package/src/plugin/hooks/cadence.ts +184 -66
  66. package/src/plugin/hooks/compaction-utils.ts +291 -0
  67. package/src/plugin/hooks/params.ts +10 -1
  68. package/src/plugin/hooks/session-memory.ts +109 -47
  69. package/src/plugin/plugin.ts +47 -10
  70. package/src/sqlite/index.ts +4 -0
  71. package/src/sqlite/queries.ts +5 -0
  72. package/src/sqlite/reader.ts +69 -0
  73. package/src/sqlite/types.ts +40 -0
  74. package/src/types.ts +30 -0
@@ -369,7 +369,7 @@ export const expertOpsAgent: AgentDefinition = {
369
369
  id: 'ag-expert-ops',
370
370
  displayName: 'Agentuity Coder Expert Ops',
371
371
  description: 'Agentuity operations specialist - CLI, cloud services, deployments, sandboxes',
372
- defaultModel: 'anthropic/claude-sonnet-4-5-20250929',
372
+ defaultModel: 'anthropic/claude-sonnet-4-6',
373
373
  systemPrompt: EXPERT_OPS_SYSTEM_PROMPT,
374
374
  mode: 'subagent',
375
375
  hidden: true, // Only invoked by Expert orchestrator
@@ -214,7 +214,7 @@ export const expertAgent: AgentDefinition = {
214
214
  id: 'ag-expert',
215
215
  displayName: 'Agentuity Coder Expert',
216
216
  description: 'Agentuity Coder Agentuity specialist - knows CLI, SDK, cloud services deeply',
217
- defaultModel: 'anthropic/claude-sonnet-4-5-20250929',
217
+ defaultModel: 'anthropic/claude-sonnet-4-6',
218
218
  systemPrompt: EXPERT_SYSTEM_PROMPT,
219
219
  variant: 'high', // Careful thinking for technical guidance
220
220
  temperature: 0.1, // Accurate, consistent technical answers
@@ -4,10 +4,17 @@ export const MONITOR_SYSTEM_PROMPT = `# BackgroundMonitor Agent
4
4
 
5
5
  You are a background task monitor. Your ONLY job is to watch background tasks and report when they complete.
6
6
 
7
+ ## Primary Notification Channel
8
+
9
+ Background tasks automatically notify Lead with messages like:
10
+ \`[BACKGROUND TASK COMPLETED]\`
11
+
12
+ Those event-driven notifications are the primary mechanism. You are a fallback for Lead-of-Leads scenarios where multiple child Leads are running and a summary pass is needed.
13
+
7
14
  ## How You Work
8
15
 
9
16
  1. You receive a list of task IDs to monitor
10
- 2. You poll their status using agentuity_background_output
17
+ 2. You check their status using agentuity_background_output
11
18
  3. When ALL tasks complete (or error), you report back to Lead
12
19
  4. You do NOT interpret results - just report completion status
13
20
 
@@ -20,36 +27,36 @@ When you need deeper insight into a task, use \`agentuity_background_inspect\` w
20
27
  - Cost summary (total cost + tokens)
21
28
  - Child session count (for nested Lead-of-Leads)
22
29
 
23
- Use inspect when a task has been running for many poll cycles without completing — it can reveal what the agent is stuck on.
30
+ Use inspect when a task has been running for many check cycles without completing — it can reveal what the agent is stuck on.
24
31
 
25
32
  For a full session tree with all child sessions, costs, and health summary, use \`agentuity_session_dashboard({ session_id: "..." })\`. This is especially useful when monitoring Lead-of-Leads scenarios with multiple parallel workstreams.
26
33
 
27
- ## Polling Behavior
34
+ ## Bounded Check Cycles
28
35
 
29
- - Poll every 10 seconds (wait between checks)
30
- - Continue until ALL tasks are complete or errored
31
- - No timeout - watch indefinitely
36
+ - Run a short, bounded series of check cycles (e.g., 3–5 passes)
37
+ - If tasks are still pending/running after the final pass, report the current status and highlight which tasks appear stuck
38
+ - If tasks appear stuck, use \`agentuity_background_inspect\` for those tasks before reporting
32
39
 
33
- ## Polling Process
40
+ ## Check Process
34
41
 
35
- For each poll cycle:
42
+ For each check cycle:
36
43
  1. Check each task ID with \`agentuity_background_output({ task_id: "bg_xxx" })\`
37
44
  2. Track the status of each task
38
- 3. If any task is still "pending" or "running", wait 10 seconds and poll again
39
- 4. When all tasks are "completed" or "error", generate the final report
45
+ 3. If all tasks are "completed" or "error", generate the final report
46
+ 4. Otherwise, repeat for the next cycle (bounded)
40
47
 
41
48
  ## Report Format
42
49
 
43
- When all tasks complete, output:
50
+ When all tasks complete (or when you finish the bounded cycles), output:
44
51
 
45
52
  \`\`\`markdown
46
- ## Background Tasks Complete
53
+ ## Background Tasks Status
47
54
 
48
55
  | Task ID | Status | Summary |
49
56
  |---------|--------|---------|
50
57
  | bg_xxx | completed | [first 100 chars of result] |
51
58
  | bg_yyy | error | [error message] |
52
- | bg_zzz | completed | [first 100 chars of result] |
59
+ | bg_zzz | running | [last known status] |
53
60
 
54
61
  ### Detailed Results
55
62
 
@@ -59,7 +66,7 @@ When all tasks complete, output:
59
66
  **bg_yyy (error):**
60
67
  [error message]
61
68
 
62
- All monitored tasks have finished. Lead can now proceed with integration.
69
+ If any tasks are still running/pending after the final pass, list them under a short "Still Running" section and mention that Lead should wait for event-driven notifications or re-check later.
63
70
  \`\`\`
64
71
 
65
72
  ## What You Do NOT Do
@@ -69,27 +76,9 @@ All monitored tasks have finished. Lead can now proceed with integration.
69
76
  - ❌ Interact with the user
70
77
  - ❌ Modify any files
71
78
  - ❌ Call other agents
72
- - ❌ Use tools other than agentuity_background_output
79
+ - ❌ Use tools other than agentuity_background_output, agentuity_background_inspect, and agentuity_session_dashboard
73
80
 
74
81
  You are a simple, focused watcher. Report completions, nothing more.
75
-
76
- ## Example Workflow
77
-
78
- Given task: "Monitor these tasks: bg_abc123, bg_def456"
79
-
80
- 1. Call agentuity_background_output for bg_abc123
81
- 2. Call agentuity_background_output for bg_def456
82
- 3. If any status is "pending" or "running", wait 10 seconds
83
- 4. Repeat steps 1-3 until all complete
84
- 5. Output final report
85
-
86
- ## Waiting Between Polls
87
-
88
- Since you cannot use setTimeout, after checking all tasks and finding some still running, respond with something like:
89
-
90
- "Polling cycle complete. Tasks still running: [list]. Waiting 10 seconds before next poll..."
91
-
92
- Then immediately poll again. The conversation history serves as your "timer" - each response and check adds natural delay.
93
82
  `;
94
83
 
95
84
  export const monitorAgent: AgentDefinition = {
@@ -363,7 +363,7 @@ export const reviewerAgent: AgentDefinition = {
363
363
  id: 'ag-reviewer',
364
364
  displayName: 'Agentuity Coder Reviewer',
365
365
  description: 'Agentuity Coder reviewer - reviews code, catches issues, applies fixes',
366
- defaultModel: 'anthropic/claude-sonnet-4-5-20250929',
366
+ defaultModel: 'anthropic/claude-sonnet-4-6',
367
367
  systemPrompt: REVIEWER_SYSTEM_PROMPT,
368
368
  variant: 'high', // Careful thinking for thorough review
369
369
  temperature: 0.1, // Consistent, deterministic reviews
@@ -316,7 +316,7 @@ export const scoutAgent: AgentDefinition = {
316
316
  displayName: 'Agentuity Coder Scout',
317
317
  description:
318
318
  'Agentuity Coder explorer - analyzes codebases, finds patterns, researches docs (read-only)',
319
- defaultModel: 'anthropic/claude-haiku-4-5-20251001',
319
+ defaultModel: 'anthropic/claude-sonnet-4-6',
320
320
  systemPrompt: SCOUT_SYSTEM_PROMPT,
321
321
  tools: {
322
322
  exclude: ['write', 'edit', 'apply_patch'],
@@ -14,7 +14,7 @@ import { ConcurrencyManager } from './concurrency';
14
14
 
15
15
  const DEFAULT_BACKGROUND_CONFIG: BackgroundTaskConfig = {
16
16
  enabled: true,
17
- defaultConcurrency: 1,
17
+ defaultConcurrency: 5,
18
18
  staleTimeoutMs: 30 * 60 * 1000,
19
19
  };
20
20
 
@@ -56,6 +56,7 @@ export class BackgroundManager {
56
56
  private notifications = new Map<string, Set<string>>();
57
57
  private toolCallIds = new Map<string, Set<string>>();
58
58
  private shuttingDown = false;
59
+ private refreshIntervalId: ReturnType<typeof setInterval> | undefined;
59
60
 
60
61
  constructor(
61
62
  ctx: PluginInput,
@@ -73,6 +74,17 @@ export class BackgroundManager {
73
74
  this.dbReader = dbReader;
74
75
  this.serverUrl = this.resolveServerUrl();
75
76
  this.authHeaders = this.resolveAuthHeaders();
77
+
78
+ // Periodic safety net: refresh task statuses every 30s in case events are missed
79
+ this.refreshIntervalId = setInterval(() => {
80
+ if (this.shuttingDown) return;
81
+ const hasActive = Array.from(this.tasks.values()).some(
82
+ (t) => t.status === 'pending' || t.status === 'running'
83
+ );
84
+ if (hasActive) {
85
+ void this.refreshStatuses();
86
+ }
87
+ }, 30_000);
76
88
  }
77
89
 
78
90
  /**
@@ -135,6 +147,7 @@ export class BackgroundManager {
135
147
  status: 'pending',
136
148
  queuedAt: new Date(),
137
149
  concurrencyGroup: this.getConcurrencyGroup(input.agent),
150
+ notifiedStatuses: new Set(),
138
151
  };
139
152
 
140
153
  this.tasks.set(task.id, task);
@@ -390,6 +403,15 @@ export class BackgroundManager {
390
403
  },
391
404
  };
392
405
 
406
+ // Mark recovered terminal tasks as already notified
407
+ if (
408
+ task.status === 'completed' ||
409
+ task.status === 'error' ||
410
+ task.status === 'cancelled'
411
+ ) {
412
+ task.notifiedStatuses = new Set([task.status]);
413
+ }
414
+
393
415
  this.tasks.set(task.id, task);
394
416
  this.tasksBySession.set(sess.id, task.id);
395
417
 
@@ -466,6 +488,15 @@ export class BackgroundManager {
466
488
  },
467
489
  };
468
490
 
491
+ // Mark recovered terminal tasks as already notified
492
+ if (
493
+ task.status === 'completed' ||
494
+ task.status === 'error' ||
495
+ task.status === 'cancelled'
496
+ ) {
497
+ task.notifiedStatuses = new Set([task.status]);
498
+ }
499
+
469
500
  // Add to our tracking maps
470
501
  this.tasks.set(task.id, task);
471
502
  this.tasksBySession.set(sess.id, task.id);
@@ -583,6 +614,10 @@ export class BackgroundManager {
583
614
 
584
615
  shutdown(): void {
585
616
  this.shuttingDown = true;
617
+ if (this.refreshIntervalId) {
618
+ clearInterval(this.refreshIntervalId);
619
+ this.refreshIntervalId = undefined;
620
+ }
586
621
  this.concurrency.clear();
587
622
  this.notifications.clear();
588
623
  try {
@@ -758,29 +793,15 @@ export class BackgroundManager {
758
793
 
759
794
  private async notifyParent(task: BackgroundTask): Promise<void> {
760
795
  if (!task.parentSessionId) return;
796
+ if (this.shuttingDown) return;
761
797
 
762
798
  // Prevent duplicate notifications for the same task+status combination
763
799
  // This guards against OpenCode firing multiple events for the same status transition
764
800
  const notifiedStatuses = task.notifiedStatuses ?? new Set();
765
801
 
766
- // Self-healing for tasks created before deduplication was added:
767
- // If a task is already in a terminal state but has no notification history,
768
- // assume it was already notified and skip to prevent duplicate notifications.
769
- if (
770
- notifiedStatuses.size === 0 &&
771
- (task.status === 'completed' || task.status === 'error' || task.status === 'cancelled')
772
- ) {
773
- notifiedStatuses.add(task.status);
774
- task.notifiedStatuses = notifiedStatuses;
775
- return;
776
- }
777
-
778
802
  if (notifiedStatuses.has(task.status)) {
779
803
  return; // Already notified for this status, skip duplicate
780
804
  }
781
- // Mark as notified BEFORE sending to prevent race conditions
782
- notifiedStatuses.add(task.status);
783
- task.notifiedStatuses = notifiedStatuses;
784
805
 
785
806
  const statusLine = task.status === 'completed' ? 'completed' : task.status;
786
807
  const message = `[BACKGROUND TASK ${statusLine.toUpperCase()}]
@@ -792,21 +813,36 @@ Task ID: ${task.id}
792
813
 
793
814
  Use the agentuity_background_output tool with task_id "${task.id}" to view the result.`;
794
815
 
795
- try {
796
- await this.ctx.client.session.prompt({
797
- path: { id: task.parentSessionId },
798
- body: {
799
- parts: [{ type: 'text', text: message }],
800
- },
801
- throwOnError: true,
802
- responseStyle: 'data',
803
- ...this.getClientOverrides(),
804
- });
805
- } catch (error) {
806
- console.error(
807
- `[BackgroundManager] Failed to notify parent for task ${task.id}:`,
808
- extractErrorMessage(error, 'notification failed')
809
- );
816
+ const maxRetries = 3;
817
+ for (let attempt = 0; attempt < maxRetries; attempt++) {
818
+ try {
819
+ await this.ctx.client.session.prompt({
820
+ path: { id: task.parentSessionId },
821
+ body: {
822
+ parts: [{ type: 'text', text: message }],
823
+ },
824
+ throwOnError: true,
825
+ responseStyle: 'data',
826
+ ...this.getClientOverrides(),
827
+ });
828
+ // Mark as notified only AFTER confirmed delivery
829
+ notifiedStatuses.add(task.status);
830
+ task.notifiedStatuses = notifiedStatuses;
831
+ return; // Success
832
+ } catch (error) {
833
+ const errorMsg = extractErrorMessage(error, 'notification failed');
834
+ if (attempt < maxRetries - 1) {
835
+ // Exponential backoff: 1s, 2s, 4s
836
+ await new Promise((r) => setTimeout(r, 1000 * Math.pow(2, attempt)));
837
+ if (this.shuttingDown) return;
838
+ } else {
839
+ console.error(
840
+ `[BackgroundManager] Failed to notify parent for task ${task.id} after ${maxRetries} attempts:`,
841
+ errorMsg
842
+ );
843
+ // Don't mark as notified — allow future retry via refreshStatuses or Monitor
844
+ }
845
+ }
810
846
  }
811
847
  }
812
848
 
@@ -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
 
@@ -285,6 +299,8 @@ Continue Cadence iteration ${state.iteration} of ${state.maxIterations}
285
299
  /**
286
300
  * Called during context compaction to inject Cadence state.
287
301
  * This ensures the compaction summary includes critical loop state.
302
+ * Uses output.prompt to REPLACE the default compaction prompt with
303
+ * enriched context (planning state, images, tool calls, diagnostics).
288
304
  */
289
305
  async onCompacting(input: CompactingInput, output: CompactingOutput): Promise<void> {
290
306
  const sessionId = input.sessionID;
@@ -298,12 +314,53 @@ Continue Cadence iteration ${state.iteration} of ${state.maxIterations}
298
314
  log(`Injecting Cadence context during compaction for session ${sessionId}`);
299
315
  showToast(ctx, '💾 Compacting Cadence context...');
300
316
 
301
- // Get current git branch
302
- const branch = await getCurrentBranch();
317
+ // Config flags for compaction behavior
318
+ const compactionCfg = config?.compaction ?? {};
319
+ const useCustomPrompt = compactionCfg.customPrompt !== false;
320
+ const useInlinePlanning = compactionCfg.inlinePlanning !== false;
321
+ const useImageAwareness = compactionCfg.imageAwareness !== false;
322
+ const useSnapshotToKV = compactionCfg.snapshotToKV !== false;
323
+ const maxTokens = compactionCfg.maxContextTokens ?? 4000;
324
+
325
+ // 1. Build custom compaction instructions
326
+ const instructions = useCustomPrompt ? buildCustomCompactionPrompt('cadence') : null;
327
+
328
+ // 2. Gather enrichment data in parallel
329
+ const toolCallLimit = config?.compaction?.toolCallSummaryLimit ?? 5;
330
+ const [branch, planningState, imageDescs, toolSummaries] = await Promise.all([
331
+ getCurrentBranch(),
332
+ useInlinePlanning ? fetchAndFormatPlanningState(sessionId) : Promise.resolve(null),
333
+ useImageAwareness
334
+ ? Promise.resolve(getImageDescriptions(dbReader ?? null, sessionId))
335
+ : Promise.resolve(null),
336
+ Promise.resolve(getRecentToolCallSummaries(dbReader ?? null, sessionId, toolCallLimit)),
337
+ ]);
338
+
339
+ // 3. Build Cadence state section
340
+ const cadenceStateSection = `## CADENCE MODE ACTIVE
341
+
342
+ This session is running in Cadence mode (long-running autonomous loop).
303
343
 
304
- // Get active background tasks for this session
344
+ **Cadence State:**
345
+ - Session ID: ${sessionId}
346
+ - Loop ID: ${state.loopId ?? 'unknown'}
347
+ - Branch: ${branch}
348
+ - Started: ${state.startedAt}
349
+ - Iteration: ${state.iteration} / ${state.maxIterations}
350
+ - Last activity: ${state.lastActivity}
351
+
352
+ **Session Record Location:**
353
+ \`session:${sessionId}\` in agentuity-opencode-memory
354
+
355
+ After compaction:
356
+ 1. Memory will save this summary and update the session record
357
+ 2. Memory should update planning.progress with this compaction
358
+ 3. Lead will continue the loop from iteration ${state.iteration}
359
+ 4. Use 5-Question Reboot to re-orient: Where am I? Where going? Goal? Learned? Done?`;
360
+
361
+ // 4. Build background tasks section
305
362
  const tasks = backgroundManager?.getTasksByParent(sessionId) ?? [];
306
- let backgroundTaskContext = '';
363
+ let backgroundSection: string | null = null;
307
364
 
308
365
  if (tasks.length > 0) {
309
366
  const taskList = tasks
@@ -313,9 +370,7 @@ Continue Cadence iteration ${state.iteration} of ${state.maxIterations}
313
370
  )
314
371
  .join('\n');
315
372
 
316
- backgroundTaskContext = `
317
-
318
- ## Active Background Tasks
373
+ backgroundSection = `## Active Background Tasks
319
374
 
320
375
  This session has ${tasks.length} background task(s) running in separate sessions:
321
376
  ${taskList}
@@ -331,45 +386,70 @@ agentuity_background_task({
331
386
  task: "Monitor these background tasks and report when all complete:\\n${tasks.map((t) => `- ${t.id}`).join('\\n')}",
332
387
  description: "Monitor child tasks"
333
388
  })
334
- \`\`\`
335
- `;
389
+ \`\`\``;
336
390
  }
337
391
 
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
392
+ // 5. Build SQLite dashboard section
393
+ const dashboardSection = buildSqliteDashboardSummary(dbReader, sessionId);
394
+
395
+ // 6. Combine everything into the full prompt
396
+ const sections: string[] = [];
397
+ if (instructions) sections.push(instructions);
398
+ sections.push(cadenceStateSection);
399
+ if (backgroundSection) sections.push(backgroundSection);
400
+ if (planningState) sections.push(planningState);
401
+ if (imageDescs) sections.push(imageDescs);
402
+ if (toolSummaries) sections.push(toolSummaries);
403
+ if (dashboardSection) sections.push(dashboardSection);
404
+
405
+ // 7. Add diagnostics
406
+ const stats: CompactionStats = {
407
+ planningPhasesCount: countListItems(planningState),
408
+ backgroundTasksCount: tasks.length,
409
+ imageDescriptionsCount: countListItems(imageDescs),
410
+ toolCallSummariesCount: countListItems(toolSummaries),
411
+ estimatedTokens: Math.ceil(sections.join('\n\n').length / 4),
412
+ };
413
+ const diagnostics = formatCompactionDiagnostics(stats);
414
+ if (diagnostics) sections.push(diagnostics);
415
+
416
+ // 8. Enforce token budget
417
+ let fullPrompt = sections.join('\n\n');
418
+ const estimatedTokens = Math.ceil(fullPrompt.length / 4);
419
+ if (maxTokens > 0 && estimatedTokens > maxTokens) {
420
+ // Trim least-critical sections first
421
+ const trimOrder = [diagnostics, toolSummaries, imageDescs, planningState].filter(
422
+ Boolean
423
+ );
424
+ let trimmed = [...sections];
425
+ for (const candidate of trimOrder) {
426
+ if (Math.ceil(trimmed.join('\n\n').length / 4) <= maxTokens) break;
427
+ trimmed = trimmed.filter((s) => s !== candidate);
428
+ }
429
+ fullPrompt = trimmed.join('\n\n');
430
+ }
353
431
 
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
- `);
432
+ // 9. Set the full prompt or push to context
433
+ if (useCustomPrompt) {
434
+ output.prompt = fullPrompt;
435
+ } else {
436
+ output.context.push(fullPrompt);
437
+ }
369
438
 
370
- const dashboardSummary = buildSqliteDashboardSummary(dbReader, sessionId);
371
- if (dashboardSummary) {
372
- output.context.push(dashboardSummary);
439
+ // 10. Store pre-compaction snapshot to KV (fire-and-forget)
440
+ if (useSnapshotToKV) {
441
+ storePreCompactionSnapshot(sessionId, {
442
+ timestamp: new Date().toISOString(),
443
+ sessionId,
444
+ planningState: planningState ? { raw: planningState } : undefined,
445
+ backgroundTasks: tasks.map((t) => ({
446
+ id: t.id,
447
+ description: t.description || 'No description',
448
+ status: t.status,
449
+ })),
450
+ cadenceState: state ? { ...state } : undefined,
451
+ branch,
452
+ }).catch(() => {}); // Fire and forget
373
453
  }
374
454
  },
375
455
 
@@ -380,6 +460,44 @@ After compaction:
380
460
  isActiveCadenceSession(sessionId: string): boolean {
381
461
  return activeCadenceSessions.has(sessionId);
382
462
  },
463
+
464
+ /**
465
+ * Lazy restore: check KV for persisted Cadence state and populate in-memory Map.
466
+ * Called before routing decisions to recover state after plugin restarts.
467
+ * Returns true if state was found and restored.
468
+ */
469
+ async tryRestoreFromKV(sessionId: string): Promise<boolean> {
470
+ // Already in memory — nothing to restore
471
+ if (activeCadenceSessions.has(sessionId)) return true;
472
+ // Known non-Cadence session — skip KV lookup
473
+ if (nonCadenceSessions.has(sessionId)) return false;
474
+
475
+ try {
476
+ const kvState = await restoreCadenceStateFromKV(sessionId);
477
+ if (!kvState) {
478
+ if (nonCadenceSessions.size >= NON_CADENCE_CACHE_MAX) {
479
+ nonCadenceSessions.clear();
480
+ }
481
+ nonCadenceSessions.add(sessionId);
482
+ return false;
483
+ }
484
+
485
+ const state: CadenceSessionState = {
486
+ startedAt: (kvState.startedAt as string) ?? new Date().toISOString(),
487
+ loopId: kvState.loopId as string | undefined,
488
+ iteration: (kvState.iteration as number) ?? 1,
489
+ maxIterations: (kvState.maxIterations as number) ?? 50,
490
+ lastActivity: (kvState.lastActivity as string) ?? new Date().toISOString(),
491
+ };
492
+ activeCadenceSessions.set(sessionId, state);
493
+ log(
494
+ `Restored Cadence state from KV for session ${sessionId} (iteration ${state.iteration}/${state.maxIterations})`
495
+ );
496
+ return true;
497
+ } catch {
498
+ return false;
499
+ }
500
+ },
383
501
  };
384
502
  }
385
503