opencode-forge 0.1.5
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.
Potentially problematic release.
This version of opencode-forge might be problematic. Click here for more details.
- package/LICENSE +21 -0
- package/README.md +534 -0
- package/config.jsonc +47 -0
- package/dist/agents/architect.d.ts +3 -0
- package/dist/agents/architect.d.ts.map +1 -0
- package/dist/agents/architect.js +152 -0
- package/dist/agents/architect.js.map +1 -0
- package/dist/agents/auditor.d.ts +3 -0
- package/dist/agents/auditor.d.ts.map +1 -0
- package/dist/agents/auditor.js +168 -0
- package/dist/agents/auditor.js.map +1 -0
- package/dist/agents/code.d.ts +3 -0
- package/dist/agents/code.d.ts.map +1 -0
- package/dist/agents/code.js +67 -0
- package/dist/agents/code.js.map +1 -0
- package/dist/agents/index.d.ts +4 -0
- package/dist/agents/index.d.ts.map +1 -0
- package/dist/agents/index.js +9 -0
- package/dist/agents/index.js.map +1 -0
- package/dist/agents/prompts.d.ts +1 -0
- package/dist/agents/prompts.d.ts.map +1 -0
- package/dist/agents/prompts.js +4 -0
- package/dist/agents/prompts.js.map +1 -0
- package/dist/agents/types.d.ts +34 -0
- package/dist/agents/types.d.ts.map +1 -0
- package/dist/agents/types.js +2 -0
- package/dist/agents/types.js.map +1 -0
- package/dist/cache/index.d.ts +4 -0
- package/dist/cache/index.d.ts.map +1 -0
- package/dist/cache/index.js +5 -0
- package/dist/cache/index.js.map +1 -0
- package/dist/cache/memory-cache.d.ts +14 -0
- package/dist/cache/memory-cache.d.ts.map +1 -0
- package/dist/cache/memory-cache.js +51 -0
- package/dist/cache/memory-cache.js.map +1 -0
- package/dist/cache/types.d.ts +8 -0
- package/dist/cache/types.d.ts.map +1 -0
- package/dist/cache/types.js +2 -0
- package/dist/cache/types.js.map +1 -0
- package/dist/cli/commands/cancel.d.ts +15 -0
- package/dist/cli/commands/cancel.d.ts.map +1 -0
- package/dist/cli/commands/cancel.js +194 -0
- package/dist/cli/commands/cancel.js.map +1 -0
- package/dist/cli/commands/graph.d.ts +16 -0
- package/dist/cli/commands/graph.d.ts.map +1 -0
- package/dist/cli/commands/graph.js +208 -0
- package/dist/cli/commands/graph.js.map +1 -0
- package/dist/cli/commands/restart.d.ts +15 -0
- package/dist/cli/commands/restart.d.ts.map +1 -0
- package/dist/cli/commands/restart.js +268 -0
- package/dist/cli/commands/restart.js.map +1 -0
- package/dist/cli/commands/status.d.ts +17 -0
- package/dist/cli/commands/status.d.ts.map +1 -0
- package/dist/cli/commands/status.js +356 -0
- package/dist/cli/commands/status.js.map +1 -0
- package/dist/cli/commands/upgrade.d.ts +3 -0
- package/dist/cli/commands/upgrade.d.ts.map +1 -0
- package/dist/cli/commands/upgrade.js +40 -0
- package/dist/cli/commands/upgrade.js.map +1 -0
- package/dist/cli/index.d.ts +3 -0
- package/dist/cli/index.d.ts.map +1 -0
- package/dist/cli/index.js +224 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/utils.d.ts +36 -0
- package/dist/cli/utils.d.ts.map +1 -0
- package/dist/cli/utils.js +163 -0
- package/dist/cli/utils.js.map +1 -0
- package/dist/command/template/review.txt +101 -0
- package/dist/config.d.ts +5 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +186 -0
- package/dist/config.js.map +1 -0
- package/dist/constants/loop.d.ts +10 -0
- package/dist/constants/loop.d.ts.map +1 -0
- package/dist/constants/loop.js +6 -0
- package/dist/constants/loop.js.map +1 -0
- package/dist/graph/cache.d.ts +17 -0
- package/dist/graph/cache.d.ts.map +1 -0
- package/dist/graph/cache.js +50 -0
- package/dist/graph/cache.js.map +1 -0
- package/dist/graph/client.d.ts +51 -0
- package/dist/graph/client.d.ts.map +1 -0
- package/dist/graph/client.js +152 -0
- package/dist/graph/client.js.map +1 -0
- package/dist/graph/clone-detection.d.ts +9 -0
- package/dist/graph/clone-detection.d.ts.map +1 -0
- package/dist/graph/clone-detection.js +148 -0
- package/dist/graph/clone-detection.js.map +1 -0
- package/dist/graph/constants.d.ts +18 -0
- package/dist/graph/constants.d.ts.map +1 -0
- package/dist/graph/constants.js +532 -0
- package/dist/graph/constants.js.map +1 -0
- package/dist/graph/database.d.ts +11 -0
- package/dist/graph/database.d.ts.map +1 -0
- package/dist/graph/database.js +250 -0
- package/dist/graph/database.js.map +1 -0
- package/dist/graph/index.d.ts +14 -0
- package/dist/graph/index.d.ts.map +1 -0
- package/dist/graph/index.js +13 -0
- package/dist/graph/index.js.map +1 -0
- package/dist/graph/repo-map.d.ts +59 -0
- package/dist/graph/repo-map.d.ts.map +1 -0
- package/dist/graph/repo-map.js +948 -0
- package/dist/graph/repo-map.js.map +1 -0
- package/dist/graph/rpc.d.ts +34 -0
- package/dist/graph/rpc.d.ts.map +1 -0
- package/dist/graph/rpc.js +139 -0
- package/dist/graph/rpc.js.map +1 -0
- package/dist/graph/service.d.ts +46 -0
- package/dist/graph/service.d.ts.map +1 -0
- package/dist/graph/service.js +329 -0
- package/dist/graph/service.js.map +1 -0
- package/dist/graph/tree-sitter.d.ts +40 -0
- package/dist/graph/tree-sitter.d.ts.map +1 -0
- package/dist/graph/tree-sitter.js +799 -0
- package/dist/graph/tree-sitter.js.map +1 -0
- package/dist/graph/types.d.ts +175 -0
- package/dist/graph/types.d.ts.map +1 -0
- package/dist/graph/types.js +105 -0
- package/dist/graph/types.js.map +1 -0
- package/dist/graph/utils.d.ts +64 -0
- package/dist/graph/utils.d.ts.map +1 -0
- package/dist/graph/utils.js +406 -0
- package/dist/graph/utils.js.map +1 -0
- package/dist/graph/worker.d.ts +2 -0
- package/dist/graph/worker.d.ts.map +1 -0
- package/dist/graph/worker.js +6043 -0
- package/dist/graph/worker.js.map +1 -0
- package/dist/hooks/compaction-utils.d.ts +21 -0
- package/dist/hooks/compaction-utils.d.ts.map +1 -0
- package/dist/hooks/compaction-utils.js +82 -0
- package/dist/hooks/compaction-utils.js.map +1 -0
- package/dist/hooks/graph-command.d.ts +27 -0
- package/dist/hooks/graph-command.d.ts.map +1 -0
- package/dist/hooks/graph-command.js +57 -0
- package/dist/hooks/graph-command.js.map +1 -0
- package/dist/hooks/graph-tools.d.ts +11 -0
- package/dist/hooks/graph-tools.d.ts.map +1 -0
- package/dist/hooks/graph-tools.js +125 -0
- package/dist/hooks/graph-tools.js.map +1 -0
- package/dist/hooks/index.d.ts +5 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/index.js +5 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/loop.d.ts +23 -0
- package/dist/hooks/loop.d.ts.map +1 -0
- package/dist/hooks/loop.js +667 -0
- package/dist/hooks/loop.js.map +1 -0
- package/dist/hooks/sandbox-tools.d.ts +13 -0
- package/dist/hooks/sandbox-tools.d.ts.map +1 -0
- package/dist/hooks/sandbox-tools.js +105 -0
- package/dist/hooks/sandbox-tools.js.map +1 -0
- package/dist/hooks/session.d.ts +19 -0
- package/dist/hooks/session.d.ts.map +1 -0
- package/dist/hooks/session.js +56 -0
- package/dist/hooks/session.js.map +1 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +298 -0
- package/dist/index.js.map +1 -0
- package/dist/sandbox/context.d.ts +27 -0
- package/dist/sandbox/context.d.ts.map +1 -0
- package/dist/sandbox/context.js +18 -0
- package/dist/sandbox/context.js.map +1 -0
- package/dist/sandbox/docker.d.ts +29 -0
- package/dist/sandbox/docker.d.ts.map +1 -0
- package/dist/sandbox/docker.js +213 -0
- package/dist/sandbox/docker.js.map +1 -0
- package/dist/sandbox/manager.d.ts +23 -0
- package/dist/sandbox/manager.d.ts.map +1 -0
- package/dist/sandbox/manager.js +131 -0
- package/dist/sandbox/manager.js.map +1 -0
- package/dist/sandbox/path.d.ts +4 -0
- package/dist/sandbox/path.d.ts.map +1 -0
- package/dist/sandbox/path.js +27 -0
- package/dist/sandbox/path.js.map +1 -0
- package/dist/services/kv.d.ts +17 -0
- package/dist/services/kv.d.ts.map +1 -0
- package/dist/services/kv.js +62 -0
- package/dist/services/kv.js.map +1 -0
- package/dist/services/loop.d.ts +96 -0
- package/dist/services/loop.d.ts.map +1 -0
- package/dist/services/loop.js +315 -0
- package/dist/services/loop.js.map +1 -0
- package/dist/setup.d.ts +4 -0
- package/dist/setup.d.ts.map +1 -0
- package/dist/setup.js +118 -0
- package/dist/setup.js.map +1 -0
- package/dist/storage/database.d.ts +6 -0
- package/dist/storage/database.d.ts.map +1 -0
- package/dist/storage/database.js +90 -0
- package/dist/storage/database.js.map +1 -0
- package/dist/storage/graph-projects.d.ts +80 -0
- package/dist/storage/graph-projects.d.ts.map +1 -0
- package/dist/storage/graph-projects.js +154 -0
- package/dist/storage/graph-projects.js.map +1 -0
- package/dist/storage/index.d.ts +5 -0
- package/dist/storage/index.d.ts.map +1 -0
- package/dist/storage/index.js +3 -0
- package/dist/storage/index.js.map +1 -0
- package/dist/storage/kv-queries.d.ts +18 -0
- package/dist/storage/kv-queries.d.ts.map +1 -0
- package/dist/storage/kv-queries.js +70 -0
- package/dist/storage/kv-queries.js.map +1 -0
- package/dist/tools/graph.d.ts +9 -0
- package/dist/tools/graph.d.ts.map +1 -0
- package/dist/tools/graph.js +272 -0
- package/dist/tools/graph.js.map +1 -0
- package/dist/tools/index.d.ts +6 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +16 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/loop.d.ts +21 -0
- package/dist/tools/loop.d.ts.map +1 -0
- package/dist/tools/loop.js +570 -0
- package/dist/tools/loop.js.map +1 -0
- package/dist/tools/plan-approval.d.ts +15 -0
- package/dist/tools/plan-approval.d.ts.map +1 -0
- package/dist/tools/plan-approval.js +203 -0
- package/dist/tools/plan-approval.js.map +1 -0
- package/dist/tools/plan-execute.d.ts +4 -0
- package/dist/tools/plan-execute.d.ts.map +1 -0
- package/dist/tools/plan-execute.js +85 -0
- package/dist/tools/plan-execute.js.map +1 -0
- package/dist/tools/plan-kv.d.ts +4 -0
- package/dist/tools/plan-kv.d.ts.map +1 -0
- package/dist/tools/plan-kv.js +107 -0
- package/dist/tools/plan-kv.js.map +1 -0
- package/dist/tools/review.d.ts +4 -0
- package/dist/tools/review.d.ts.map +1 -0
- package/dist/tools/review.js +90 -0
- package/dist/tools/review.js.map +1 -0
- package/dist/tools/sandbox-fs.d.ts +22 -0
- package/dist/tools/sandbox-fs.d.ts.map +1 -0
- package/dist/tools/sandbox-fs.js +83 -0
- package/dist/tools/sandbox-fs.js.map +1 -0
- package/dist/tools/types.d.ts +26 -0
- package/dist/tools/types.d.ts.map +1 -0
- package/dist/tools/types.js +2 -0
- package/dist/tools/types.js.map +1 -0
- package/dist/tui.d.ts +3 -0
- package/dist/tui.js +2061 -0
- package/dist/types.d.ts +124 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/git-branch.d.ts +11 -0
- package/dist/utils/git-branch.d.ts.map +1 -0
- package/dist/utils/git-branch.js +35 -0
- package/dist/utils/git-branch.js.map +1 -0
- package/dist/utils/graph-status-store.d.ts +72 -0
- package/dist/utils/graph-status-store.d.ts.map +1 -0
- package/dist/utils/graph-status-store.js +62 -0
- package/dist/utils/graph-status-store.js.map +1 -0
- package/dist/utils/logger.d.ts +8 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +89 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/loop-format.d.ts +5 -0
- package/dist/utils/loop-format.d.ts.map +1 -0
- package/dist/utils/loop-format.js +29 -0
- package/dist/utils/loop-format.js.map +1 -0
- package/dist/utils/loop-helpers.d.ts +9 -0
- package/dist/utils/loop-helpers.d.ts.map +1 -0
- package/dist/utils/loop-helpers.js +20 -0
- package/dist/utils/loop-helpers.js.map +1 -0
- package/dist/utils/loop-launch.d.ts +32 -0
- package/dist/utils/loop-launch.d.ts.map +1 -0
- package/dist/utils/loop-launch.js +162 -0
- package/dist/utils/loop-launch.js.map +1 -0
- package/dist/utils/model-fallback.d.ts +27 -0
- package/dist/utils/model-fallback.d.ts.map +1 -0
- package/dist/utils/model-fallback.js +33 -0
- package/dist/utils/model-fallback.js.map +1 -0
- package/dist/utils/partial-match.d.ts +7 -0
- package/dist/utils/partial-match.d.ts.map +1 -0
- package/dist/utils/partial-match.js +56 -0
- package/dist/utils/partial-match.js.map +1 -0
- package/dist/utils/plan-execution.d.ts +65 -0
- package/dist/utils/plan-execution.d.ts.map +1 -0
- package/dist/utils/plan-execution.js +107 -0
- package/dist/utils/plan-execution.js.map +1 -0
- package/dist/utils/session-stats.d.ts +36 -0
- package/dist/utils/session-stats.d.ts.map +1 -0
- package/dist/utils/session-stats.js +145 -0
- package/dist/utils/session-stats.js.map +1 -0
- package/dist/utils/tui-graph-status.d.ts +38 -0
- package/dist/utils/tui-graph-status.d.ts.map +1 -0
- package/dist/utils/tui-graph-status.js +95 -0
- package/dist/utils/tui-graph-status.js.map +1 -0
- package/dist/utils/tui-plan-store.d.ts +54 -0
- package/dist/utils/tui-plan-store.d.ts.map +1 -0
- package/dist/utils/tui-plan-store.js +168 -0
- package/dist/utils/tui-plan-store.js.map +1 -0
- package/dist/utils/tui-refresh-helpers.d.ts +44 -0
- package/dist/utils/tui-refresh-helpers.d.ts.map +1 -0
- package/dist/utils/tui-refresh-helpers.js +120 -0
- package/dist/utils/tui-refresh-helpers.js.map +1 -0
- package/dist/utils/upgrade.d.ts +23 -0
- package/dist/utils/upgrade.d.ts.map +1 -0
- package/dist/utils/upgrade.js +111 -0
- package/dist/utils/upgrade.js.map +1 -0
- package/dist/version.d.ts +2 -0
- package/dist/version.d.ts.map +1 -0
- package/dist/version.js +2 -0
- package/dist/version.js.map +1 -0
- package/package.json +92 -0
- package/scripts/build.ts +67 -0
- package/src/command/template/review.txt +101 -0
|
@@ -0,0 +1,667 @@
|
|
|
1
|
+
import { MAX_RETRIES, MAX_CONSECUTIVE_STALLS, LOOP_PERMISSION_RULESET } from '../services/loop';
|
|
2
|
+
import { retryWithModelFallback } from '../utils/model-fallback';
|
|
3
|
+
import { resolveLoopModel } from '../utils/loop-helpers';
|
|
4
|
+
import { execSync, spawnSync } from 'child_process';
|
|
5
|
+
import { resolve } from 'path';
|
|
6
|
+
export function createLoopEventHandler(loopService, _client, v2Client, logger, getConfig, sandboxManager) {
|
|
7
|
+
const minAudits = loopService.getMinAudits();
|
|
8
|
+
const retryTimeouts = new Map();
|
|
9
|
+
const lastActivityTime = new Map();
|
|
10
|
+
const stallWatchdogs = new Map();
|
|
11
|
+
const consecutiveStalls = new Map();
|
|
12
|
+
const watchdogRunning = new Map();
|
|
13
|
+
const stateLocks = new Map();
|
|
14
|
+
function withStateLock(worktreeName, fn) {
|
|
15
|
+
const prev = stateLocks.get(worktreeName) ?? Promise.resolve();
|
|
16
|
+
const next = prev.then(fn, fn).finally(() => {
|
|
17
|
+
if (stateLocks.get(worktreeName) === next) {
|
|
18
|
+
stateLocks.delete(worktreeName);
|
|
19
|
+
}
|
|
20
|
+
});
|
|
21
|
+
stateLocks.set(worktreeName, next);
|
|
22
|
+
return next;
|
|
23
|
+
}
|
|
24
|
+
async function commitAndCleanupWorktree(state) {
|
|
25
|
+
if (!state.worktree) {
|
|
26
|
+
logger.log(`Loop: in-place mode, skipping commit and cleanup`);
|
|
27
|
+
return { committed: false, cleaned: false };
|
|
28
|
+
}
|
|
29
|
+
let committed = false;
|
|
30
|
+
let cleaned = false;
|
|
31
|
+
try {
|
|
32
|
+
const addResult = spawnSync('git', ['add', '-A'], { cwd: state.worktreeDir, encoding: 'utf-8' });
|
|
33
|
+
if (addResult.status !== 0) {
|
|
34
|
+
throw new Error(addResult.stderr || 'git add failed');
|
|
35
|
+
}
|
|
36
|
+
const statusResult = spawnSync('git', ['status', '--porcelain'], { cwd: state.worktreeDir, encoding: 'utf-8' });
|
|
37
|
+
if (statusResult.status !== 0) {
|
|
38
|
+
throw new Error(statusResult.stderr || 'git status failed');
|
|
39
|
+
}
|
|
40
|
+
const status = statusResult.stdout.trim();
|
|
41
|
+
if (status) {
|
|
42
|
+
const message = `loop: ${state.worktreeName} completed after ${state.iteration} iterations`;
|
|
43
|
+
const commitResult = spawnSync('git', ['commit', '-m', message], { cwd: state.worktreeDir, encoding: 'utf-8' });
|
|
44
|
+
if (commitResult.status !== 0) {
|
|
45
|
+
throw new Error(commitResult.stderr || 'git commit failed');
|
|
46
|
+
}
|
|
47
|
+
committed = true;
|
|
48
|
+
logger.log(`Loop: committed changes on branch ${state.worktreeBranch}`);
|
|
49
|
+
}
|
|
50
|
+
else {
|
|
51
|
+
logger.log(`Loop: no uncommitted changes to commit on branch ${state.worktreeBranch}`);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
logger.error(`Loop: failed to commit changes in worktree ${state.worktreeDir}`, err);
|
|
56
|
+
}
|
|
57
|
+
if (state.worktreeDir && state.worktreeBranch) {
|
|
58
|
+
try {
|
|
59
|
+
const gitCommonDir = execSync('git rev-parse --git-common-dir', { cwd: state.worktreeDir, encoding: 'utf-8' }).trim();
|
|
60
|
+
const gitRoot = resolve(state.worktreeDir, gitCommonDir, '..');
|
|
61
|
+
const removeResult = spawnSync('git', ['worktree', 'remove', '-f', state.worktreeDir], { cwd: gitRoot, encoding: 'utf-8' });
|
|
62
|
+
if (removeResult.status !== 0) {
|
|
63
|
+
throw new Error(removeResult.stderr || 'git worktree remove failed');
|
|
64
|
+
}
|
|
65
|
+
cleaned = true;
|
|
66
|
+
logger.log(`Loop: removed worktree ${state.worktreeDir}, branch ${state.worktreeBranch} preserved`);
|
|
67
|
+
}
|
|
68
|
+
catch (err) {
|
|
69
|
+
logger.error(`Loop: failed to remove worktree ${state.worktreeDir}`, err);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return { committed, cleaned };
|
|
73
|
+
}
|
|
74
|
+
function stopWatchdog(worktreeName) {
|
|
75
|
+
const interval = stallWatchdogs.get(worktreeName);
|
|
76
|
+
if (interval) {
|
|
77
|
+
clearInterval(interval);
|
|
78
|
+
stallWatchdogs.delete(worktreeName);
|
|
79
|
+
}
|
|
80
|
+
lastActivityTime.delete(worktreeName);
|
|
81
|
+
consecutiveStalls.delete(worktreeName);
|
|
82
|
+
watchdogRunning.delete(worktreeName);
|
|
83
|
+
}
|
|
84
|
+
function startWatchdog(worktreeName) {
|
|
85
|
+
stopWatchdog(worktreeName);
|
|
86
|
+
lastActivityTime.set(worktreeName, Date.now());
|
|
87
|
+
consecutiveStalls.set(worktreeName, 0);
|
|
88
|
+
const stallTimeout = loopService.getStallTimeoutMs();
|
|
89
|
+
const interval = setInterval(async () => {
|
|
90
|
+
if (watchdogRunning.get(worktreeName))
|
|
91
|
+
return;
|
|
92
|
+
watchdogRunning.set(worktreeName, true);
|
|
93
|
+
try {
|
|
94
|
+
const lastActivity = lastActivityTime.get(worktreeName);
|
|
95
|
+
if (!lastActivity)
|
|
96
|
+
return;
|
|
97
|
+
const elapsed = Date.now() - lastActivity;
|
|
98
|
+
if (elapsed < stallTimeout)
|
|
99
|
+
return;
|
|
100
|
+
const state = loopService.getActiveState(worktreeName);
|
|
101
|
+
if (!state?.active) {
|
|
102
|
+
stopWatchdog(worktreeName);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const sessionId = state.sessionId;
|
|
106
|
+
let statusCheckFailed = false;
|
|
107
|
+
try {
|
|
108
|
+
const statusResult = await v2Client.session.status({ directory: state.worktreeDir });
|
|
109
|
+
const statuses = (statusResult.data ?? {});
|
|
110
|
+
const status = statuses[sessionId]?.type;
|
|
111
|
+
const hasActiveWork = status === 'busy' || status === 'retry';
|
|
112
|
+
if (hasActiveWork) {
|
|
113
|
+
lastActivityTime.set(worktreeName, Date.now());
|
|
114
|
+
logger.log(`Loop watchdog: worktree ${worktreeName} has active work (${status}), resetting timer`);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
logger.error(`Loop watchdog: failed to check session status, treating as stall`, err);
|
|
120
|
+
statusCheckFailed = true;
|
|
121
|
+
}
|
|
122
|
+
const stallCount = (consecutiveStalls.get(worktreeName) ?? 0) + 1;
|
|
123
|
+
consecutiveStalls.set(worktreeName, stallCount);
|
|
124
|
+
lastActivityTime.set(worktreeName, Date.now());
|
|
125
|
+
if (stallCount >= MAX_CONSECUTIVE_STALLS) {
|
|
126
|
+
logger.error(`Loop watchdog: worktree ${worktreeName} exceeded max consecutive stalls (${MAX_CONSECUTIVE_STALLS}), terminating`);
|
|
127
|
+
await terminateLoop(worktreeName, state, 'stall_timeout');
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
logger.log(`Loop watchdog: stall #${stallCount}/${MAX_CONSECUTIVE_STALLS} for ${worktreeName} (phase=${state.phase}, elapsed=${elapsed}ms, statusCheckFailed=${statusCheckFailed}), re-triggering`);
|
|
131
|
+
await withStateLock(worktreeName, async () => {
|
|
132
|
+
const freshState = loopService.getActiveState(worktreeName);
|
|
133
|
+
if (!freshState?.active)
|
|
134
|
+
return;
|
|
135
|
+
try {
|
|
136
|
+
if (freshState.phase === 'auditing') {
|
|
137
|
+
await handleAuditingPhase(worktreeName, freshState);
|
|
138
|
+
}
|
|
139
|
+
else {
|
|
140
|
+
await handleCodingPhase(worktreeName, freshState);
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
catch (err) {
|
|
144
|
+
await handlePromptError(worktreeName, freshState, `watchdog recovery in ${freshState.phase} phase`, err);
|
|
145
|
+
}
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
finally {
|
|
149
|
+
watchdogRunning.set(worktreeName, false);
|
|
150
|
+
}
|
|
151
|
+
}, stallTimeout);
|
|
152
|
+
stallWatchdogs.set(worktreeName, interval);
|
|
153
|
+
logger.log(`Loop watchdog: started for worktree ${worktreeName} (timeout: ${stallTimeout}ms)`);
|
|
154
|
+
}
|
|
155
|
+
function getStallInfo(worktreeName) {
|
|
156
|
+
const lastActivity = lastActivityTime.get(worktreeName);
|
|
157
|
+
if (lastActivity === undefined)
|
|
158
|
+
return null;
|
|
159
|
+
return {
|
|
160
|
+
consecutiveStalls: consecutiveStalls.get(worktreeName) ?? 0,
|
|
161
|
+
lastActivityTime: lastActivity,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
async function terminateLoop(worktreeName, state, reason) {
|
|
165
|
+
const sessionId = state.sessionId;
|
|
166
|
+
stopWatchdog(worktreeName);
|
|
167
|
+
const retryTimeout = retryTimeouts.get(worktreeName);
|
|
168
|
+
if (retryTimeout) {
|
|
169
|
+
clearTimeout(retryTimeout);
|
|
170
|
+
retryTimeouts.delete(worktreeName);
|
|
171
|
+
}
|
|
172
|
+
loopService.unregisterSession(sessionId);
|
|
173
|
+
loopService.setState(worktreeName, {
|
|
174
|
+
...state,
|
|
175
|
+
active: false,
|
|
176
|
+
completedAt: new Date().toISOString(),
|
|
177
|
+
terminationReason: reason,
|
|
178
|
+
});
|
|
179
|
+
try {
|
|
180
|
+
await v2Client.session.abort({ sessionID: sessionId });
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
// Session may already be idle
|
|
184
|
+
}
|
|
185
|
+
logger.log(`Loop terminated: reason="${reason}", worktree="${state.worktreeName}", iteration=${state.iteration}`);
|
|
186
|
+
if (v2Client.tui) {
|
|
187
|
+
const toastVariant = reason === 'completed' ? 'success'
|
|
188
|
+
: reason === 'cancelled' || reason === 'user_aborted' ? 'info'
|
|
189
|
+
: reason === 'max_iterations' ? 'warning'
|
|
190
|
+
: 'error';
|
|
191
|
+
const toastMessage = reason === 'completed' ? `Completed after ${state.iteration} iteration${state.iteration !== 1 ? 's' : ''}`
|
|
192
|
+
: reason === 'cancelled' ? 'Loop cancelled'
|
|
193
|
+
: reason === 'max_iterations' ? `Reached max iterations (${state.maxIterations})`
|
|
194
|
+
: reason === 'stall_timeout' ? `Stalled after ${state.iteration} iteration${state.iteration !== 1 ? 's' : ''}`
|
|
195
|
+
: reason === 'user_aborted' ? 'Loop aborted by user'
|
|
196
|
+
: `Loop ended: ${reason}`;
|
|
197
|
+
v2Client.tui.publish({
|
|
198
|
+
directory: state.worktreeDir,
|
|
199
|
+
body: {
|
|
200
|
+
type: 'tui.toast.show',
|
|
201
|
+
properties: {
|
|
202
|
+
title: state.worktreeName,
|
|
203
|
+
message: toastMessage,
|
|
204
|
+
variant: toastVariant,
|
|
205
|
+
duration: reason === 'completed' ? 5000 : 3000,
|
|
206
|
+
},
|
|
207
|
+
},
|
|
208
|
+
}).catch((err) => {
|
|
209
|
+
logger.error('Loop: failed to publish toast notification', err);
|
|
210
|
+
});
|
|
211
|
+
}
|
|
212
|
+
if (reason === 'completed' || reason === 'cancelled') {
|
|
213
|
+
await commitAndCleanupWorktree(state);
|
|
214
|
+
}
|
|
215
|
+
if (state.sandbox && state.sandboxContainerName && sandboxManager) {
|
|
216
|
+
try {
|
|
217
|
+
await sandboxManager.stop(state.worktreeName);
|
|
218
|
+
logger.log(`Loop: stopped sandbox container for ${state.worktreeName}`);
|
|
219
|
+
}
|
|
220
|
+
catch (err) {
|
|
221
|
+
logger.error(`Loop: failed to stop sandbox container`, err);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
async function handlePromptError(worktreeName, _state, context, err, retryFn) {
|
|
226
|
+
const currentState = loopService.getActiveState(worktreeName);
|
|
227
|
+
if (!currentState?.active) {
|
|
228
|
+
logger.log(`Loop: loop ${worktreeName} already terminated, ignoring error: ${context}`);
|
|
229
|
+
return;
|
|
230
|
+
}
|
|
231
|
+
const nextErrorCount = (currentState.errorCount ?? 0) + 1;
|
|
232
|
+
if (nextErrorCount < MAX_RETRIES) {
|
|
233
|
+
logger.error(`Loop: ${context} (attempt ${nextErrorCount}/${MAX_RETRIES}), will retry`, err);
|
|
234
|
+
loopService.setState(worktreeName, { ...currentState, errorCount: nextErrorCount });
|
|
235
|
+
if (retryFn) {
|
|
236
|
+
const retryTimeout = setTimeout(async () => {
|
|
237
|
+
const freshState = loopService.getActiveState(worktreeName);
|
|
238
|
+
if (!freshState?.active) {
|
|
239
|
+
logger.log(`Loop: loop cancelled, skipping retry`);
|
|
240
|
+
retryTimeouts.delete(worktreeName);
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
try {
|
|
244
|
+
await retryFn();
|
|
245
|
+
}
|
|
246
|
+
catch (retryErr) {
|
|
247
|
+
await handlePromptError(worktreeName, freshState, context, retryErr, retryFn);
|
|
248
|
+
}
|
|
249
|
+
}, 2000);
|
|
250
|
+
retryTimeouts.set(worktreeName, retryTimeout);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
else {
|
|
254
|
+
logger.error(`Loop: ${context} (attempt ${nextErrorCount}/${MAX_RETRIES}), giving up`, err);
|
|
255
|
+
await terminateLoop(worktreeName, currentState, `error_max_retries: ${context}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
async function getLastAssistantInfo(sessionId, worktreeDir) {
|
|
259
|
+
try {
|
|
260
|
+
const messagesResult = await v2Client.session.messages({
|
|
261
|
+
sessionID: sessionId,
|
|
262
|
+
directory: worktreeDir,
|
|
263
|
+
limit: 4,
|
|
264
|
+
});
|
|
265
|
+
const messages = (messagesResult.data ?? []);
|
|
266
|
+
const lastMessage = messages.length > 0 ? messages[messages.length - 1] : null;
|
|
267
|
+
const lastAssistant = [...messages].reverse().find((m) => m.info.role === 'assistant');
|
|
268
|
+
if (!lastAssistant) {
|
|
269
|
+
const role = lastMessage?.info.role ?? 'none';
|
|
270
|
+
logger.log(`Loop: no assistant message found in session ${sessionId}, last message role: ${role}`);
|
|
271
|
+
return { text: null, error: null, lastMessageRole: role };
|
|
272
|
+
}
|
|
273
|
+
const text = lastAssistant.parts
|
|
274
|
+
.filter((p) => p.type === 'text' && typeof p.text === 'string')
|
|
275
|
+
.map((p) => p.text)
|
|
276
|
+
.join('\n') || null;
|
|
277
|
+
const error = lastAssistant.info.error?.data?.message ?? lastAssistant.info.error?.name ?? null;
|
|
278
|
+
return { text, error, lastMessageRole: 'assistant' };
|
|
279
|
+
}
|
|
280
|
+
catch (err) {
|
|
281
|
+
logger.error(`Loop: could not read session messages`, err);
|
|
282
|
+
return { text: null, error: null, lastMessageRole: 'error' };
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
async function rotateSession(worktreeName, state) {
|
|
286
|
+
const oldSessionId = state.sessionId;
|
|
287
|
+
const createParams = {
|
|
288
|
+
title: state.worktreeName,
|
|
289
|
+
directory: state.worktreeDir,
|
|
290
|
+
permission: LOOP_PERMISSION_RULESET,
|
|
291
|
+
};
|
|
292
|
+
const createResult = await v2Client.session.create(createParams);
|
|
293
|
+
if (createResult.error || !createResult.data) {
|
|
294
|
+
throw new Error(`Failed to create new session: ${createResult.error}`);
|
|
295
|
+
}
|
|
296
|
+
const newSessionId = createResult.data.id;
|
|
297
|
+
const oldRetryTimeout = retryTimeouts.get(worktreeName);
|
|
298
|
+
if (oldRetryTimeout) {
|
|
299
|
+
clearTimeout(oldRetryTimeout);
|
|
300
|
+
retryTimeouts.delete(worktreeName);
|
|
301
|
+
}
|
|
302
|
+
loopService.unregisterSession(oldSessionId);
|
|
303
|
+
loopService.registerSession(newSessionId, worktreeName);
|
|
304
|
+
stopWatchdog(worktreeName);
|
|
305
|
+
startWatchdog(worktreeName);
|
|
306
|
+
v2Client.session.delete({ sessionID: oldSessionId, directory: state.worktreeDir }).catch((err) => {
|
|
307
|
+
logger.error(`Loop: failed to delete old session ${oldSessionId}`, err);
|
|
308
|
+
});
|
|
309
|
+
logger.log(`Loop: rotated session ${oldSessionId} → ${newSessionId}`);
|
|
310
|
+
if (!state.worktree && v2Client.tui) {
|
|
311
|
+
v2Client.tui.selectSession({ sessionID: newSessionId }).catch((err) => {
|
|
312
|
+
logger.error(`Loop: failed to navigate TUI to rotated session`, err);
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
return newSessionId;
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Shared: handle assistant error detection and model failure.
|
|
319
|
+
* Returns null if the loop was terminated (caller should return).
|
|
320
|
+
* Returns updated { assistantErrorDetected, currentState }.
|
|
321
|
+
*/
|
|
322
|
+
async function detectAndHandleAssistantError(worktreeName, currentState, assistantError, phase) {
|
|
323
|
+
if (!assistantError) {
|
|
324
|
+
return { assistantErrorDetected: false, currentState };
|
|
325
|
+
}
|
|
326
|
+
logger.error(`Loop: assistant error detected in ${phase} phase: ${assistantError}`);
|
|
327
|
+
const isModelError = /provider|auth|model|api\s*error/i.test(assistantError);
|
|
328
|
+
if (isModelError) {
|
|
329
|
+
const nextErrorCount = (currentState.errorCount ?? 0) + 1;
|
|
330
|
+
if (nextErrorCount >= MAX_RETRIES) {
|
|
331
|
+
await terminateLoop(worktreeName, currentState, `error_max_retries: assistant error: ${assistantError}`);
|
|
332
|
+
return null;
|
|
333
|
+
}
|
|
334
|
+
loopService.setState(worktreeName, { ...currentState, modelFailed: true, errorCount: nextErrorCount });
|
|
335
|
+
logger.log(`Loop: marking model as failed, will fall back to default model (error ${nextErrorCount}/${MAX_RETRIES})`);
|
|
336
|
+
return { assistantErrorDetected: true, currentState: loopService.getActiveState(worktreeName) };
|
|
337
|
+
}
|
|
338
|
+
return { assistantErrorDetected: true, currentState };
|
|
339
|
+
}
|
|
340
|
+
/**
|
|
341
|
+
* Shared: check completion signal and terminate if ready.
|
|
342
|
+
* Returns true if the loop was terminated (caller should return).
|
|
343
|
+
*/
|
|
344
|
+
async function checkCompletionAndTerminate(worktreeName, currentState, textContent, auditCount) {
|
|
345
|
+
if (!currentState.completionSignal || !textContent)
|
|
346
|
+
return false;
|
|
347
|
+
if (!loopService.checkCompletionSignal(textContent, currentState.completionSignal))
|
|
348
|
+
return false;
|
|
349
|
+
if (!currentState.audit || auditCount >= minAudits) {
|
|
350
|
+
if (loopService.hasOutstandingFindings(currentState.worktreeBranch)) {
|
|
351
|
+
logger.log(`Loop: completion promise detected but outstanding review findings remain, continuing`);
|
|
352
|
+
return false;
|
|
353
|
+
}
|
|
354
|
+
await terminateLoop(worktreeName, currentState, 'completed');
|
|
355
|
+
logger.log(`Loop completed: detected ${currentState.completionSignal} at iteration ${currentState.iteration} (${auditCount}/${minAudits} audits)`);
|
|
356
|
+
return true;
|
|
357
|
+
}
|
|
358
|
+
logger.log(`Loop: completion promise detected but only ${auditCount}/${minAudits} audits performed, continuing`);
|
|
359
|
+
return false;
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Shared: reset error count after a successful (non-error) iteration.
|
|
363
|
+
*/
|
|
364
|
+
function resetErrorCountIfNeeded(worktreeName, currentState, assistantErrorDetected, phase) {
|
|
365
|
+
if (!assistantErrorDetected && currentState.errorCount && currentState.errorCount > 0) {
|
|
366
|
+
loopService.setState(worktreeName, { ...currentState, errorCount: 0, modelFailed: false });
|
|
367
|
+
logger.log(`Loop: resetting error count after successful retry in ${phase} phase`);
|
|
368
|
+
return loopService.getActiveState(worktreeName);
|
|
369
|
+
}
|
|
370
|
+
return currentState;
|
|
371
|
+
}
|
|
372
|
+
/**
|
|
373
|
+
* Shared: rotate session and send continuation prompt with model fallback.
|
|
374
|
+
*/
|
|
375
|
+
async function rotateAndSendContinuation(worktreeName, currentState, stateUpdates, continuationPrompt, assistantErrorDetected, errorContext) {
|
|
376
|
+
let activeSessionId = currentState.sessionId;
|
|
377
|
+
try {
|
|
378
|
+
activeSessionId = await rotateSession(worktreeName, currentState);
|
|
379
|
+
}
|
|
380
|
+
catch (err) {
|
|
381
|
+
logger.error(`Loop: session rotation failed, continuing with existing session`, err);
|
|
382
|
+
}
|
|
383
|
+
loopService.setState(worktreeName, {
|
|
384
|
+
...currentState,
|
|
385
|
+
sessionId: activeSessionId,
|
|
386
|
+
errorCount: assistantErrorDetected ? currentState.errorCount : 0,
|
|
387
|
+
modelFailed: assistantErrorDetected ? currentState.modelFailed : false,
|
|
388
|
+
...stateUpdates,
|
|
389
|
+
});
|
|
390
|
+
const nextIteration = stateUpdates.iteration ?? currentState.iteration;
|
|
391
|
+
logger.log(`Loop iteration ${nextIteration} for session ${activeSessionId}`);
|
|
392
|
+
const currentConfig = getConfig();
|
|
393
|
+
const loopModel = resolveLoopModel(currentConfig, loopService, worktreeName);
|
|
394
|
+
if (!loopModel) {
|
|
395
|
+
logger.log(`Loop: configured model previously failed, using default model`);
|
|
396
|
+
}
|
|
397
|
+
const sendWithModel = async () => {
|
|
398
|
+
const freshState = loopService.getActiveState(worktreeName);
|
|
399
|
+
if (!freshState?.active) {
|
|
400
|
+
throw new Error('loop_cancelled');
|
|
401
|
+
}
|
|
402
|
+
const result = await v2Client.session.promptAsync({
|
|
403
|
+
sessionID: activeSessionId,
|
|
404
|
+
directory: freshState.worktreeDir,
|
|
405
|
+
parts: [{ type: 'text', text: continuationPrompt }],
|
|
406
|
+
model: loopModel,
|
|
407
|
+
});
|
|
408
|
+
return { data: result.data, error: result.error };
|
|
409
|
+
};
|
|
410
|
+
const sendWithoutModel = async () => {
|
|
411
|
+
const freshState = loopService.getActiveState(worktreeName);
|
|
412
|
+
if (!freshState?.active) {
|
|
413
|
+
throw new Error('loop_cancelled');
|
|
414
|
+
}
|
|
415
|
+
const result = await v2Client.session.promptAsync({
|
|
416
|
+
sessionID: activeSessionId,
|
|
417
|
+
directory: freshState.worktreeDir,
|
|
418
|
+
parts: [{ type: 'text', text: continuationPrompt }],
|
|
419
|
+
});
|
|
420
|
+
return { data: result.data, error: result.error };
|
|
421
|
+
};
|
|
422
|
+
const { result: promptResult, usedModel: actualModel } = await retryWithModelFallback(sendWithModel, sendWithoutModel, loopModel, logger);
|
|
423
|
+
if (promptResult.error) {
|
|
424
|
+
const retryFn = async () => {
|
|
425
|
+
const freshState = loopService.getActiveState(worktreeName);
|
|
426
|
+
if (!freshState?.active) {
|
|
427
|
+
throw new Error('loop_cancelled');
|
|
428
|
+
}
|
|
429
|
+
const result = await sendWithoutModel();
|
|
430
|
+
if (result.error) {
|
|
431
|
+
await handlePromptError(worktreeName, currentState, `retry failed ${errorContext}`, result.error);
|
|
432
|
+
return;
|
|
433
|
+
}
|
|
434
|
+
};
|
|
435
|
+
await handlePromptError(worktreeName, currentState, `failed to send continuation prompt ${errorContext}`, promptResult.error, retryFn);
|
|
436
|
+
return;
|
|
437
|
+
}
|
|
438
|
+
if (actualModel) {
|
|
439
|
+
logger.log(`${errorContext} using model: ${actualModel.providerID}/${actualModel.modelID}`);
|
|
440
|
+
}
|
|
441
|
+
else {
|
|
442
|
+
logger.log(`${errorContext} using default model (fallback)`);
|
|
443
|
+
}
|
|
444
|
+
consecutiveStalls.set(worktreeName, 0);
|
|
445
|
+
}
|
|
446
|
+
async function handleCodingPhase(worktreeName, _state) {
|
|
447
|
+
let currentState = loopService.getActiveState(worktreeName);
|
|
448
|
+
if (!currentState?.active) {
|
|
449
|
+
logger.log(`Loop: loop ${worktreeName} no longer active, skipping coding phase`);
|
|
450
|
+
return;
|
|
451
|
+
}
|
|
452
|
+
if (!currentState.worktreeDir) {
|
|
453
|
+
logger.error(`Loop: loop ${worktreeName} missing worktreeDir in coding phase, terminating`);
|
|
454
|
+
await terminateLoop(worktreeName, currentState, 'missing_worktree_dir');
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
let assistantErrorDetected = false;
|
|
458
|
+
if (currentState.completionSignal) {
|
|
459
|
+
const { text: textContent, error: assistantError, lastMessageRole } = await getLastAssistantInfo(currentState.sessionId, currentState.worktreeDir);
|
|
460
|
+
if (lastMessageRole !== 'assistant') {
|
|
461
|
+
logger.error(`Loop: assistant message not found in coding phase (last message: ${lastMessageRole}), session may not have responded yet`);
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
const errorResult = await detectAndHandleAssistantError(worktreeName, currentState, assistantError, 'coding');
|
|
465
|
+
if (!errorResult)
|
|
466
|
+
return;
|
|
467
|
+
assistantErrorDetected = errorResult.assistantErrorDetected;
|
|
468
|
+
currentState = errorResult.currentState;
|
|
469
|
+
if (await checkCompletionAndTerminate(worktreeName, currentState, textContent, currentState.auditCount ?? 0))
|
|
470
|
+
return;
|
|
471
|
+
}
|
|
472
|
+
currentState = resetErrorCountIfNeeded(worktreeName, currentState, assistantErrorDetected, 'coding');
|
|
473
|
+
if ((currentState.maxIterations ?? 0) > 0 && (currentState.iteration ?? 0) >= (currentState.maxIterations ?? 0)) {
|
|
474
|
+
await terminateLoop(worktreeName, currentState, 'max_iterations');
|
|
475
|
+
return;
|
|
476
|
+
}
|
|
477
|
+
if (currentState.audit) {
|
|
478
|
+
loopService.setState(worktreeName, { ...currentState, phase: 'auditing', errorCount: 0 });
|
|
479
|
+
logger.log(`Loop iteration ${currentState.iteration ?? 0} complete, running auditor for session ${currentState.sessionId}`);
|
|
480
|
+
const auditPrompt = {
|
|
481
|
+
sessionID: currentState.sessionId,
|
|
482
|
+
directory: currentState.worktreeDir,
|
|
483
|
+
parts: [{
|
|
484
|
+
type: 'subtask',
|
|
485
|
+
agent: 'auditor',
|
|
486
|
+
description: `Post-iteration ${currentState.iteration} code review`,
|
|
487
|
+
prompt: loopService.buildAuditPrompt(currentState),
|
|
488
|
+
}],
|
|
489
|
+
};
|
|
490
|
+
const promptResult = await v2Client.session.promptAsync(auditPrompt);
|
|
491
|
+
if (promptResult.error) {
|
|
492
|
+
const retryFn = async () => {
|
|
493
|
+
const result = await v2Client.session.promptAsync(auditPrompt);
|
|
494
|
+
if (result.error) {
|
|
495
|
+
throw result.error;
|
|
496
|
+
}
|
|
497
|
+
};
|
|
498
|
+
await handlePromptError(worktreeName, { ...currentState, phase: 'coding' }, 'failed to send audit prompt', promptResult.error, retryFn);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
const currentConfig = getConfig();
|
|
502
|
+
const configuredModel = currentConfig.auditorModel ?? currentConfig.loop?.model ?? currentConfig.executionModel;
|
|
503
|
+
logger.log(`auditor using agent-configured model: ${configuredModel ?? 'default'}`);
|
|
504
|
+
consecutiveStalls.set(worktreeName, 0);
|
|
505
|
+
return;
|
|
506
|
+
}
|
|
507
|
+
const nextIteration = (currentState.iteration ?? 0) + 1;
|
|
508
|
+
const continuationPrompt = loopService.buildContinuationPrompt({ ...currentState, iteration: nextIteration });
|
|
509
|
+
await rotateAndSendContinuation(worktreeName, currentState, { iteration: nextIteration }, continuationPrompt, assistantErrorDetected, 'coding phase');
|
|
510
|
+
}
|
|
511
|
+
async function handleAuditingPhase(worktreeName, _state) {
|
|
512
|
+
let currentState = loopService.getActiveState(worktreeName);
|
|
513
|
+
if (!currentState?.active) {
|
|
514
|
+
logger.log(`Loop: loop ${worktreeName} no longer active, skipping auditing phase`);
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (!currentState.worktreeDir) {
|
|
518
|
+
logger.error(`Loop: loop ${worktreeName} missing worktreeDir in auditing phase, terminating`);
|
|
519
|
+
await terminateLoop(worktreeName, currentState, 'missing_worktree_dir');
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
const { text: auditText, error: assistantError, lastMessageRole } = await getLastAssistantInfo(currentState.sessionId, currentState.worktreeDir);
|
|
523
|
+
if (lastMessageRole !== 'assistant') {
|
|
524
|
+
logger.error(`Loop: assistant message not found in auditing phase (last message: ${lastMessageRole}), session may not have responded yet`);
|
|
525
|
+
return;
|
|
526
|
+
}
|
|
527
|
+
const errorResult = await detectAndHandleAssistantError(worktreeName, currentState, assistantError, 'auditing');
|
|
528
|
+
if (!errorResult)
|
|
529
|
+
return;
|
|
530
|
+
const assistantErrorDetected = errorResult.assistantErrorDetected;
|
|
531
|
+
currentState = errorResult.currentState;
|
|
532
|
+
currentState = resetErrorCountIfNeeded(worktreeName, currentState, assistantErrorDetected, 'auditing');
|
|
533
|
+
const nextIteration = (currentState.iteration ?? 0) + 1;
|
|
534
|
+
const newAuditCount = (currentState.auditCount ?? 0) + 1;
|
|
535
|
+
logger.log(`Loop audit ${newAuditCount} at iteration ${currentState.iteration ?? 0}`);
|
|
536
|
+
const auditFindings = auditText ?? undefined;
|
|
537
|
+
if (await checkCompletionAndTerminate(worktreeName, currentState, auditText, newAuditCount))
|
|
538
|
+
return;
|
|
539
|
+
if ((currentState.maxIterations ?? 0) > 0 && nextIteration > (currentState.maxIterations ?? 0)) {
|
|
540
|
+
await terminateLoop(worktreeName, currentState, 'max_iterations');
|
|
541
|
+
return;
|
|
542
|
+
}
|
|
543
|
+
const continuationPrompt = loopService.buildContinuationPrompt({ ...currentState, iteration: nextIteration }, auditFindings);
|
|
544
|
+
await rotateAndSendContinuation(worktreeName, currentState, {
|
|
545
|
+
iteration: nextIteration,
|
|
546
|
+
phase: 'coding',
|
|
547
|
+
lastAuditResult: auditFindings,
|
|
548
|
+
auditCount: newAuditCount,
|
|
549
|
+
}, continuationPrompt, assistantErrorDetected, 'coding continuation');
|
|
550
|
+
}
|
|
551
|
+
async function onEvent(input) {
|
|
552
|
+
const { event } = input;
|
|
553
|
+
if (event.type === 'worktree.failed') {
|
|
554
|
+
const message = event.properties?.message;
|
|
555
|
+
const directory = event.properties?.directory;
|
|
556
|
+
logger.error(`Loop: worktree failed: ${message}`);
|
|
557
|
+
if (directory) {
|
|
558
|
+
const activeLoops = loopService.listActive();
|
|
559
|
+
const affectedLoop = activeLoops.find((s) => s.worktreeDir === directory);
|
|
560
|
+
if (affectedLoop) {
|
|
561
|
+
await terminateLoop(affectedLoop.worktreeName, affectedLoop, `worktree_failed: ${message}`);
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
return;
|
|
565
|
+
}
|
|
566
|
+
if (event.type === 'session.error') {
|
|
567
|
+
const errorProps = event.properties;
|
|
568
|
+
const eventSessionId = errorProps?.sessionID;
|
|
569
|
+
const errorName = errorProps?.error?.name;
|
|
570
|
+
const isAbort = errorName === 'MessageAbortedError' || errorName === 'AbortError';
|
|
571
|
+
if (!eventSessionId)
|
|
572
|
+
return;
|
|
573
|
+
if (isAbort) {
|
|
574
|
+
const worktreeName = loopService.resolveWorktreeName(eventSessionId);
|
|
575
|
+
if (!worktreeName)
|
|
576
|
+
return;
|
|
577
|
+
const state = loopService.getActiveState(worktreeName);
|
|
578
|
+
if (state?.active) {
|
|
579
|
+
logger.log(`Loop: session ${eventSessionId} aborted, terminating loop`);
|
|
580
|
+
await terminateLoop(worktreeName, state, 'user_aborted');
|
|
581
|
+
}
|
|
582
|
+
return;
|
|
583
|
+
}
|
|
584
|
+
const worktreeName = loopService.resolveWorktreeName(eventSessionId);
|
|
585
|
+
if (!worktreeName)
|
|
586
|
+
return;
|
|
587
|
+
const state = loopService.getActiveState(worktreeName);
|
|
588
|
+
if (state?.active) {
|
|
589
|
+
const errorMessage = errorProps?.error?.data?.message ?? errorName ?? 'unknown error';
|
|
590
|
+
logger.error(`Loop: session error for ${eventSessionId}: ${errorMessage}`);
|
|
591
|
+
const isModelError = /provider|auth|model|api\s*error/i.test(errorMessage);
|
|
592
|
+
if (isModelError && !state.modelFailed) {
|
|
593
|
+
logger.log(`Loop: marking model as failed, will fall back to default on next iteration`);
|
|
594
|
+
loopService.setState(worktreeName, { ...state, modelFailed: true });
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
return;
|
|
598
|
+
}
|
|
599
|
+
if (event.type !== 'session.idle')
|
|
600
|
+
return;
|
|
601
|
+
const sessionId = event.properties?.sessionID;
|
|
602
|
+
if (!sessionId)
|
|
603
|
+
return;
|
|
604
|
+
const worktreeName = loopService.resolveWorktreeName(sessionId);
|
|
605
|
+
if (!worktreeName)
|
|
606
|
+
return;
|
|
607
|
+
await withStateLock(worktreeName, async () => {
|
|
608
|
+
const state = loopService.getActiveState(worktreeName);
|
|
609
|
+
if (!state || !state.active)
|
|
610
|
+
return;
|
|
611
|
+
if (state.sessionId !== sessionId) {
|
|
612
|
+
logger.log(`Loop: ignoring stale idle event for session ${sessionId} (current: ${state.sessionId})`);
|
|
613
|
+
return;
|
|
614
|
+
}
|
|
615
|
+
try {
|
|
616
|
+
startWatchdog(worktreeName);
|
|
617
|
+
if (state.phase === 'auditing') {
|
|
618
|
+
await handleAuditingPhase(worktreeName, state);
|
|
619
|
+
}
|
|
620
|
+
else {
|
|
621
|
+
await handleCodingPhase(worktreeName, state);
|
|
622
|
+
}
|
|
623
|
+
}
|
|
624
|
+
catch (err) {
|
|
625
|
+
const freshState = loopService.getActiveState(worktreeName);
|
|
626
|
+
await handlePromptError(worktreeName, freshState ?? state, `unhandled error in ${(freshState ?? state).phase} phase`, err);
|
|
627
|
+
}
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
function terminateAll() {
|
|
631
|
+
loopService.terminateAll();
|
|
632
|
+
}
|
|
633
|
+
function clearAllRetryTimeouts() {
|
|
634
|
+
for (const [worktreeName, timeout] of retryTimeouts.entries()) {
|
|
635
|
+
clearTimeout(timeout);
|
|
636
|
+
retryTimeouts.delete(worktreeName);
|
|
637
|
+
}
|
|
638
|
+
for (const [worktreeName, interval] of stallWatchdogs.entries()) {
|
|
639
|
+
clearInterval(interval);
|
|
640
|
+
stallWatchdogs.delete(worktreeName);
|
|
641
|
+
}
|
|
642
|
+
lastActivityTime.clear();
|
|
643
|
+
consecutiveStalls.clear();
|
|
644
|
+
watchdogRunning.clear();
|
|
645
|
+
stateLocks.clear();
|
|
646
|
+
logger.log('Loop: cleared all retry timeouts');
|
|
647
|
+
}
|
|
648
|
+
async function cancelBySessionId(sessionId) {
|
|
649
|
+
const worktreeName = loopService.resolveWorktreeName(sessionId);
|
|
650
|
+
if (!worktreeName)
|
|
651
|
+
return false;
|
|
652
|
+
const state = loopService.getActiveState(worktreeName);
|
|
653
|
+
if (!state?.active)
|
|
654
|
+
return false;
|
|
655
|
+
await terminateLoop(worktreeName, state, 'cancelled');
|
|
656
|
+
return true;
|
|
657
|
+
}
|
|
658
|
+
return {
|
|
659
|
+
onEvent,
|
|
660
|
+
terminateAll,
|
|
661
|
+
clearAllRetryTimeouts,
|
|
662
|
+
startWatchdog,
|
|
663
|
+
getStallInfo,
|
|
664
|
+
cancelBySessionId,
|
|
665
|
+
};
|
|
666
|
+
}
|
|
667
|
+
//# sourceMappingURL=loop.js.map
|