@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.
- package/dist/agents/expert-backend.js +1 -1
- package/dist/agents/expert-backend.js.map +1 -1
- package/dist/agents/expert-frontend.js +1 -1
- package/dist/agents/expert-frontend.js.map +1 -1
- package/dist/agents/expert-ops.js +1 -1
- package/dist/agents/expert-ops.js.map +1 -1
- package/dist/agents/expert.js +1 -1
- package/dist/agents/expert.js.map +1 -1
- package/dist/agents/monitor.d.ts +1 -1
- package/dist/agents/monitor.d.ts.map +1 -1
- package/dist/agents/monitor.js +22 -33
- package/dist/agents/monitor.js.map +1 -1
- package/dist/agents/reviewer.js +1 -1
- package/dist/agents/reviewer.js.map +1 -1
- package/dist/agents/scout.js +1 -1
- package/dist/agents/scout.js.map +1 -1
- package/dist/background/manager.d.ts +1 -0
- package/dist/background/manager.d.ts.map +1 -1
- package/dist/background/manager.js +60 -26
- package/dist/background/manager.js.map +1 -1
- package/dist/plugin/hooks/cadence.d.ts +3 -1
- package/dist/plugin/hooks/cadence.d.ts.map +1 -1
- package/dist/plugin/hooks/cadence.js +167 -66
- package/dist/plugin/hooks/cadence.js.map +1 -1
- package/dist/plugin/hooks/compaction-utils.d.ts +48 -0
- package/dist/plugin/hooks/compaction-utils.d.ts.map +1 -0
- package/dist/plugin/hooks/compaction-utils.js +259 -0
- package/dist/plugin/hooks/compaction-utils.js.map +1 -0
- package/dist/plugin/hooks/params.d.ts +1 -1
- package/dist/plugin/hooks/params.d.ts.map +1 -1
- package/dist/plugin/hooks/params.js +5 -1
- package/dist/plugin/hooks/params.js.map +1 -1
- package/dist/plugin/hooks/session-memory.d.ts +2 -1
- package/dist/plugin/hooks/session-memory.d.ts.map +1 -1
- package/dist/plugin/hooks/session-memory.js +97 -48
- package/dist/plugin/hooks/session-memory.js.map +1 -1
- package/dist/plugin/plugin.d.ts.map +1 -1
- package/dist/plugin/plugin.js +31 -9
- package/dist/plugin/plugin.js.map +1 -1
- package/dist/sqlite/index.d.ts +1 -1
- package/dist/sqlite/index.d.ts.map +1 -1
- package/dist/sqlite/queries.d.ts +1 -0
- package/dist/sqlite/queries.d.ts.map +1 -1
- package/dist/sqlite/queries.js +4 -0
- package/dist/sqlite/queries.js.map +1 -1
- package/dist/sqlite/reader.d.ts +11 -1
- package/dist/sqlite/reader.d.ts.map +1 -1
- package/dist/sqlite/reader.js +62 -0
- package/dist/sqlite/reader.js.map +1 -1
- package/dist/sqlite/types.d.ts +40 -0
- package/dist/sqlite/types.d.ts.map +1 -1
- package/dist/types.d.ts +36 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +10 -0
- package/dist/types.js.map +1 -1
- package/package.json +3 -3
- package/src/agents/expert-backend.ts +1 -1
- package/src/agents/expert-frontend.ts +1 -1
- package/src/agents/expert-ops.ts +1 -1
- package/src/agents/expert.ts +1 -1
- package/src/agents/monitor.ts +22 -33
- package/src/agents/reviewer.ts +1 -1
- package/src/agents/scout.ts +1 -1
- package/src/background/manager.ts +67 -31
- package/src/plugin/hooks/cadence.ts +184 -66
- package/src/plugin/hooks/compaction-utils.ts +291 -0
- package/src/plugin/hooks/params.ts +10 -1
- package/src/plugin/hooks/session-memory.ts +109 -47
- package/src/plugin/plugin.ts +47 -10
- package/src/sqlite/index.ts +4 -0
- package/src/sqlite/queries.ts +5 -0
- package/src/sqlite/reader.ts +69 -0
- package/src/sqlite/types.ts +40 -0
- package/src/types.ts +30 -0
package/src/agents/expert-ops.ts
CHANGED
|
@@ -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-
|
|
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
|
package/src/agents/expert.ts
CHANGED
|
@@ -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-
|
|
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
|
package/src/agents/monitor.ts
CHANGED
|
@@ -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
|
|
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
|
|
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
|
-
##
|
|
34
|
+
## Bounded Check Cycles
|
|
28
35
|
|
|
29
|
-
-
|
|
30
|
-
-
|
|
31
|
-
-
|
|
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
|
-
##
|
|
40
|
+
## Check Process
|
|
34
41
|
|
|
35
|
-
For each
|
|
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
|
|
39
|
-
4.
|
|
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
|
|
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 |
|
|
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
|
-
|
|
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 = {
|
package/src/agents/reviewer.ts
CHANGED
|
@@ -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-
|
|
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
|
package/src/agents/scout.ts
CHANGED
|
@@ -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-
|
|
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:
|
|
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
|
-
|
|
796
|
-
|
|
797
|
-
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
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
|
-
|
|
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
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
//
|
|
97
|
-
|
|
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
|
-
//
|
|
302
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
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
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
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
|
-
|
|
371
|
-
if (
|
|
372
|
-
|
|
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
|
|