memorix 1.1.7 → 1.1.8
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/CHANGELOG.md +13 -0
- package/CLAUDE.md +6 -1
- package/README.md +21 -0
- package/README.zh-CN.md +21 -0
- package/TEAM.md +86 -86
- package/dist/cli/index.js +730 -150
- package/dist/cli/index.js.map +1 -1
- package/dist/dashboard/static/index.html +201 -201
- package/dist/dashboard/static/style.css +3584 -3584
- package/dist/index.js +15 -8
- package/dist/index.js.map +1 -1
- package/dist/memcode-runtime/CHANGELOG.md +13 -0
- package/dist/memcode-runtime/package.json +4 -4
- package/dist/sdk.js +15 -8
- package/dist/sdk.js.map +1 -1
- package/docs/AGENT_OPERATOR_PLAYBOOK.md +18 -0
- package/docs/DESIGN_DECISIONS.md +357 -357
- package/docs/SETUP.md +10 -0
- package/docs/dev-log/progress.txt +35 -26
- package/package.json +1 -1
- package/src/audit/index.ts +156 -156
- package/src/cli/commands/agent-integrations.ts +623 -0
- package/src/cli/commands/audit-list.ts +89 -89
- package/src/cli/commands/background.ts +659 -659
- package/src/cli/commands/cleanup.ts +255 -255
- package/src/cli/commands/doctor.ts +23 -0
- package/src/cli/commands/formation.ts +48 -48
- package/src/cli/commands/git-hook-install.ts +111 -111
- package/src/cli/commands/handoff.ts +66 -66
- package/src/cli/commands/hooks-status.ts +63 -63
- package/src/cli/commands/ingest-commit.ts +153 -153
- package/src/cli/commands/ingest-image.ts +73 -73
- package/src/cli/commands/ingest-log.ts +180 -180
- package/src/cli/commands/ingest.ts +44 -44
- package/src/cli/commands/integrate-shared.ts +15 -15
- package/src/cli/commands/lock.ts +96 -96
- package/src/cli/commands/message.ts +121 -121
- package/src/cli/commands/poll.ts +70 -70
- package/src/cli/commands/purge-all-memory.ts +85 -85
- package/src/cli/commands/purge-project-memory.ts +83 -83
- package/src/cli/commands/reasoning.ts +132 -132
- package/src/cli/commands/repair.ts +60 -0
- package/src/cli/commands/retention.ts +108 -108
- package/src/cli/commands/serve-shared.ts +118 -118
- package/src/cli/commands/setup.ts +3 -3
- package/src/cli/commands/skills.ts +123 -123
- package/src/cli/commands/task.ts +192 -192
- package/src/cli/commands/transfer.ts +73 -73
- package/src/cli/commands/uninstall-project-artifacts.ts +85 -85
- package/src/cli/index.ts +3 -1
- package/src/cli/tui/ChatView.tsx +234 -234
- package/src/cli/tui/CommandBar.tsx +312 -312
- package/src/cli/tui/ContextRail.tsx +118 -118
- package/src/cli/tui/HeaderBar.tsx +72 -72
- package/src/cli/tui/LogoBanner.tsx +51 -51
- package/src/cli/tui/Panels.tsx +632 -632
- package/src/cli/tui/Sidebar.tsx +179 -179
- package/src/cli/tui/chat-service.ts +742 -742
- package/src/cli/tui/data.ts +547 -547
- package/src/cli/tui/index.ts +41 -41
- package/src/cli/tui/markdown-render.tsx +371 -371
- package/src/cli/tui/theme.ts +178 -178
- package/src/cli/tui/use-mouse.ts +157 -157
- package/src/cli/tui/useNavigation.ts +56 -56
- package/src/cli/update-checker.ts +211 -211
- package/src/cli/version.ts +7 -7
- package/src/cli/workbench.ts +1 -1
- package/src/codegraph/context-pack.ts +5 -1
- package/src/codegraph/lite-provider.ts +5 -1
- package/src/codegraph/project-context.ts +5 -1
- package/src/compact/token-budget.ts +74 -74
- package/src/config/behavior.ts +59 -59
- package/src/dashboard/project-classification.ts +64 -64
- package/src/dashboard/static/index.html +201 -201
- package/src/dashboard/static/style.css +3584 -3584
- package/src/embedding/fastembed-provider.ts +142 -142
- package/src/embedding/transformers-provider.ts +111 -111
- package/src/git/extractor.ts +209 -209
- package/src/git/hooks-path.ts +85 -85
- package/src/git/noise-filter.ts +210 -210
- package/src/hooks/installers/index.ts +4 -4
- package/src/hooks/official-skills.ts +1 -1
- package/src/hooks/pattern-detector.ts +173 -173
- package/src/hooks/rules/memorix-agent-rules.md +2 -2
- package/src/hooks/significance-filter.ts +250 -250
- package/src/llm/memory-manager.ts +328 -328
- package/src/llm/provider.ts +885 -885
- package/src/llm/quality.ts +248 -248
- package/src/memory/attribution-guard.ts +249 -249
- package/src/memory/auto-relations.ts +107 -107
- package/src/memory/consolidation.ts +302 -302
- package/src/memory/disclosure-policy.ts +141 -141
- package/src/memory/entity-extractor.ts +197 -197
- package/src/memory/formation/evaluate.ts +217 -217
- package/src/memory/formation/extract.ts +361 -361
- package/src/memory/formation/index.ts +417 -417
- package/src/memory/formation/resolve.ts +344 -344
- package/src/memory/formation/types.ts +315 -315
- package/src/memory/freshness.ts +122 -122
- package/src/memory/graph.ts +197 -197
- package/src/memory/refs.ts +94 -94
- package/src/memory/retention.ts +433 -433
- package/src/memory/secret-filter.ts +79 -79
- package/src/memory/session.ts +523 -523
- package/src/multimodal/image-loader.ts +143 -143
- package/src/orchestrate/adapters/claude-stream.ts +192 -192
- package/src/orchestrate/adapters/claude.ts +111 -111
- package/src/orchestrate/adapters/codex-stream.ts +134 -134
- package/src/orchestrate/adapters/codex.ts +41 -41
- package/src/orchestrate/adapters/gemini-stream.ts +166 -166
- package/src/orchestrate/adapters/gemini.ts +42 -42
- package/src/orchestrate/adapters/index.ts +73 -73
- package/src/orchestrate/adapters/opencode-stream.ts +143 -143
- package/src/orchestrate/adapters/opencode.ts +47 -47
- package/src/orchestrate/adapters/spawn-helper.ts +286 -286
- package/src/orchestrate/adapters/types.ts +77 -77
- package/src/orchestrate/capability-router.ts +284 -284
- package/src/orchestrate/context-compact.ts +188 -188
- package/src/orchestrate/cost-tracker.ts +219 -219
- package/src/orchestrate/error-recovery.ts +191 -191
- package/src/orchestrate/evidence.ts +140 -140
- package/src/orchestrate/ledger.ts +110 -110
- package/src/orchestrate/memorix-bridge.ts +380 -380
- package/src/orchestrate/output-budget.ts +80 -80
- package/src/orchestrate/permission.ts +152 -152
- package/src/orchestrate/pipeline-trace.ts +131 -131
- package/src/orchestrate/prompt-builder.ts +155 -155
- package/src/orchestrate/ring-buffer.ts +37 -37
- package/src/orchestrate/task-graph.ts +389 -389
- package/src/orchestrate/verify-gate.ts +219 -219
- package/src/orchestrate/worktree.ts +232 -232
- package/src/project/aliases.ts +374 -374
- package/src/project/detector.ts +268 -268
- package/src/rules/adapters/claude-code.ts +99 -99
- package/src/rules/adapters/codex.ts +97 -97
- package/src/rules/adapters/copilot.ts +124 -124
- package/src/rules/adapters/cursor.ts +114 -114
- package/src/rules/adapters/kiro.ts +126 -126
- package/src/rules/adapters/trae.ts +56 -56
- package/src/rules/adapters/windsurf.ts +83 -83
- package/src/rules/syncer.ts +235 -235
- package/src/sdk.ts +327 -327
- package/src/search/intent-detector.ts +289 -289
- package/src/search/query-expansion.ts +52 -52
- package/src/server/formation-timeout.ts +27 -27
- package/src/skills/mini-skills.ts +386 -386
- package/src/store/chat-store.ts +119 -119
- package/src/store/file-lock.ts +100 -100
- package/src/store/graph-store.ts +249 -249
- package/src/store/mini-skill-store.ts +349 -349
- package/src/store/obs-store.ts +255 -255
- package/src/store/persistence-json.ts +212 -212
- package/src/store/persistence.ts +291 -291
- package/src/store/project-affinity.ts +195 -195
- package/src/store/session-store.ts +259 -259
- package/src/store/sqlite-store.ts +339 -339
- package/src/team/event-bus.ts +76 -76
- package/src/team/file-locks.ts +173 -173
- package/src/team/handoff.ts +167 -167
- package/src/team/messages.ts +203 -203
- package/src/team/poll.ts +132 -132
- package/src/team/tasks.ts +211 -211
- package/src/wiki/generator.ts +237 -237
- package/src/wiki/knowledge-graph.ts +334 -334
- package/src/wiki/types.ts +85 -85
- package/src/workspace/mcp-adapters/codex.ts +191 -191
- package/src/workspace/mcp-adapters/copilot.ts +105 -105
- package/src/workspace/mcp-adapters/cursor.ts +53 -53
- package/src/workspace/mcp-adapters/kiro.ts +64 -64
- package/src/workspace/mcp-adapters/opencode.ts +123 -123
- package/src/workspace/mcp-adapters/trae.ts +134 -134
- package/src/workspace/mcp-adapters/windsurf.ts +91 -91
- package/src/workspace/sanitizer.ts +60 -60
- package/src/workspace/workflow-sync.ts +131 -131
|
@@ -1,191 +1,191 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Error Recovery — Phase 7, Step 3: Layered error classification and recovery.
|
|
3
|
-
*
|
|
4
|
-
* Detects three failure modes from agent output/exit and recommends a
|
|
5
|
-
* recovery strategy. The coordinator uses this to decide between:
|
|
6
|
-
* - continuation (truncated output)
|
|
7
|
-
* - context compaction + re-dispatch (context overflow)
|
|
8
|
-
* - exponential backoff + retry (transient errors)
|
|
9
|
-
* - normal retry (unknown / unrecoverable)
|
|
10
|
-
*
|
|
11
|
-
* Design principle: detection is heuristic-based pattern matching on
|
|
12
|
-
* stderr/tailOutput. No false positive should crash the pipeline —
|
|
13
|
-
* the worst case of a wrong classification is a wasted retry attempt.
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
// ── Types ──────────────────────────────────────────────────────────
|
|
17
|
-
|
|
18
|
-
export type ErrorCategory = 'truncated' | 'context_overflow' | 'transient' | 'unknown';
|
|
19
|
-
|
|
20
|
-
export interface RecoveryAction {
|
|
21
|
-
category: ErrorCategory;
|
|
22
|
-
/** Recommended recovery strategy */
|
|
23
|
-
strategy: 'continue' | 'compact_and_retry' | 'backoff_and_retry' | 'normal_retry';
|
|
24
|
-
/** Suggested delay before retry in ms (0 = immediate) */
|
|
25
|
-
delayMs: number;
|
|
26
|
-
/** Human-readable explanation of the detection */
|
|
27
|
-
reason: string;
|
|
28
|
-
/** Suggested continuation prompt (for 'continue' strategy) */
|
|
29
|
-
continuationPrompt?: string;
|
|
30
|
-
/** Backoff attempt number (for 'backoff_and_retry') */
|
|
31
|
-
backoffAttempt?: number;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
export interface ErrorContext {
|
|
35
|
-
exitCode: number | null;
|
|
36
|
-
killed: boolean;
|
|
37
|
-
tailOutput: string;
|
|
38
|
-
/** Whether the agent stream had an end_turn signal */
|
|
39
|
-
hasEndTurn?: boolean;
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
// ── Pattern Registry ───────────────────────────────────────────────
|
|
43
|
-
|
|
44
|
-
const TRUNCATED_PATTERNS = [
|
|
45
|
-
/max.?output/i,
|
|
46
|
-
/output.?limit/i,
|
|
47
|
-
/max.?tokens/i,
|
|
48
|
-
/response.?truncat/i,
|
|
49
|
-
/generation.?limit/i,
|
|
50
|
-
];
|
|
51
|
-
|
|
52
|
-
const CONTEXT_OVERFLOW_PATTERNS = [
|
|
53
|
-
/overlong.?prompt/i,
|
|
54
|
-
/context.?length.?exceed/i,
|
|
55
|
-
/prompt.?too.?long/i,
|
|
56
|
-
/maximum.?context/i,
|
|
57
|
-
/token.?limit.?exceed/i,
|
|
58
|
-
/input.?too.?long/i,
|
|
59
|
-
/request.?too.?large/i,
|
|
60
|
-
/max.?input.?tokens/i,
|
|
61
|
-
/content.?too.?long/i,
|
|
62
|
-
/exceeds.?(?:the\s+)?(?:model'?s?\s+)?(?:maximum|max).?(?:context|token)/i,
|
|
63
|
-
];
|
|
64
|
-
|
|
65
|
-
const TRANSIENT_PATTERNS = [
|
|
66
|
-
/rate.?limit/i,
|
|
67
|
-
/429/,
|
|
68
|
-
/too.?many.?requests/i,
|
|
69
|
-
/server.?error/i,
|
|
70
|
-
/502|503|504/,
|
|
71
|
-
/bad.?gateway/i,
|
|
72
|
-
/service.?unavailable/i,
|
|
73
|
-
/gateway.?timeout/i,
|
|
74
|
-
/ECONNRESET/i,
|
|
75
|
-
/ECONNREFUSED/i,
|
|
76
|
-
/ETIMEDOUT/i,
|
|
77
|
-
/socket.?hang.?up/i,
|
|
78
|
-
/network.?error/i,
|
|
79
|
-
/temporary.?failure/i,
|
|
80
|
-
/overloaded/i,
|
|
81
|
-
/capacity/i,
|
|
82
|
-
];
|
|
83
|
-
|
|
84
|
-
// ── Backoff State ──────────────────────────────────────────────────
|
|
85
|
-
|
|
86
|
-
const backoffState = new Map<string, number>(); // taskId → consecutive transient failures
|
|
87
|
-
|
|
88
|
-
/** Reset backoff state for a task (call on success or non-transient failure) */
|
|
89
|
-
export function resetBackoff(taskId: string): void {
|
|
90
|
-
backoffState.delete(taskId);
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
/** Get current backoff attempt count for a task */
|
|
94
|
-
export function getBackoffAttempt(taskId: string): number {
|
|
95
|
-
return backoffState.get(taskId) ?? 0;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
// ── Core Classification ────────────────────────────────────────────
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Classify an agent failure and recommend a recovery action.
|
|
102
|
-
*
|
|
103
|
-
* Priority order: truncated > context_overflow > transient > unknown.
|
|
104
|
-
* This ensures the most specific recovery strategy is chosen.
|
|
105
|
-
*/
|
|
106
|
-
export function classifyError(ctx: ErrorContext, taskId?: string): RecoveryAction {
|
|
107
|
-
const { exitCode, killed, tailOutput, hasEndTurn } = ctx;
|
|
108
|
-
const text = tailOutput.slice(-2000); // Only check last 2KB
|
|
109
|
-
|
|
110
|
-
// 1. Truncated output detection
|
|
111
|
-
// Agent was cut off mid-response (no end_turn + non-zero exit)
|
|
112
|
-
if (!killed && exitCode !== 0 && hasEndTurn === false) {
|
|
113
|
-
return {
|
|
114
|
-
category: 'truncated',
|
|
115
|
-
strategy: 'continue',
|
|
116
|
-
delayMs: 0,
|
|
117
|
-
reason: 'Agent output appears truncated (no end_turn signal with non-zero exit)',
|
|
118
|
-
continuationPrompt: 'Your previous output was truncated due to output limits. Continue from where you stopped. Do not repeat what you already wrote.',
|
|
119
|
-
};
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Also detect explicit truncation error messages
|
|
123
|
-
if (matchesAny(text, TRUNCATED_PATTERNS)) {
|
|
124
|
-
return {
|
|
125
|
-
category: 'truncated',
|
|
126
|
-
strategy: 'continue',
|
|
127
|
-
delayMs: 0,
|
|
128
|
-
reason: `Truncation pattern detected in output`,
|
|
129
|
-
continuationPrompt: 'Your previous output was truncated due to output limits. Continue from where you stopped. Do not repeat what you already wrote.',
|
|
130
|
-
};
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
// 2. Context overflow detection
|
|
134
|
-
if (matchesAny(text, CONTEXT_OVERFLOW_PATTERNS)) {
|
|
135
|
-
return {
|
|
136
|
-
category: 'context_overflow',
|
|
137
|
-
strategy: 'compact_and_retry',
|
|
138
|
-
delayMs: 0,
|
|
139
|
-
reason: 'Context overflow detected — prompt is too large for the model',
|
|
140
|
-
};
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
// 3. Transient error detection
|
|
144
|
-
if (matchesAny(text, TRANSIENT_PATTERNS)) {
|
|
145
|
-
const attempt = taskId ? (backoffState.get(taskId) ?? 0) + 1 : 1;
|
|
146
|
-
if (taskId) backoffState.set(taskId, attempt);
|
|
147
|
-
|
|
148
|
-
// Exponential backoff: 2s, 4s, 8s, capped at 30s
|
|
149
|
-
const baseDelay = 2000;
|
|
150
|
-
const maxDelay = 30_000;
|
|
151
|
-
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
152
|
-
// Add jitter: ±25%
|
|
153
|
-
const jitter = delay * 0.25 * (Math.random() * 2 - 1);
|
|
154
|
-
|
|
155
|
-
return {
|
|
156
|
-
category: 'transient',
|
|
157
|
-
strategy: 'backoff_and_retry',
|
|
158
|
-
delayMs: Math.round(delay + jitter),
|
|
159
|
-
reason: `Transient error detected (attempt ${attempt})`,
|
|
160
|
-
backoffAttempt: attempt,
|
|
161
|
-
};
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
// 4. Unknown — fall back to normal retry
|
|
165
|
-
if (taskId) resetBackoff(taskId); // Non-transient failure resets backoff
|
|
166
|
-
|
|
167
|
-
return {
|
|
168
|
-
category: 'unknown',
|
|
169
|
-
strategy: 'normal_retry',
|
|
170
|
-
delayMs: 0,
|
|
171
|
-
reason: killed
|
|
172
|
-
? 'Agent killed by timeout'
|
|
173
|
-
: `Agent exited with code ${exitCode}`,
|
|
174
|
-
};
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
/**
|
|
178
|
-
* Check if the error is recoverable (not a permanent failure).
|
|
179
|
-
* Truncated and transient errors are always recoverable.
|
|
180
|
-
* Context overflow is recoverable if compaction is available.
|
|
181
|
-
* Unknown errors may be recoverable via normal retry.
|
|
182
|
-
*/
|
|
183
|
-
export function isRecoverable(action: RecoveryAction): boolean {
|
|
184
|
-
return action.strategy !== 'normal_retry' || action.category === 'unknown';
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
// ── Helpers ────────────────────────────────────────────────────────
|
|
188
|
-
|
|
189
|
-
function matchesAny(text: string, patterns: RegExp[]): boolean {
|
|
190
|
-
return patterns.some(p => p.test(text));
|
|
191
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Error Recovery — Phase 7, Step 3: Layered error classification and recovery.
|
|
3
|
+
*
|
|
4
|
+
* Detects three failure modes from agent output/exit and recommends a
|
|
5
|
+
* recovery strategy. The coordinator uses this to decide between:
|
|
6
|
+
* - continuation (truncated output)
|
|
7
|
+
* - context compaction + re-dispatch (context overflow)
|
|
8
|
+
* - exponential backoff + retry (transient errors)
|
|
9
|
+
* - normal retry (unknown / unrecoverable)
|
|
10
|
+
*
|
|
11
|
+
* Design principle: detection is heuristic-based pattern matching on
|
|
12
|
+
* stderr/tailOutput. No false positive should crash the pipeline —
|
|
13
|
+
* the worst case of a wrong classification is a wasted retry attempt.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
17
|
+
|
|
18
|
+
export type ErrorCategory = 'truncated' | 'context_overflow' | 'transient' | 'unknown';
|
|
19
|
+
|
|
20
|
+
export interface RecoveryAction {
|
|
21
|
+
category: ErrorCategory;
|
|
22
|
+
/** Recommended recovery strategy */
|
|
23
|
+
strategy: 'continue' | 'compact_and_retry' | 'backoff_and_retry' | 'normal_retry';
|
|
24
|
+
/** Suggested delay before retry in ms (0 = immediate) */
|
|
25
|
+
delayMs: number;
|
|
26
|
+
/** Human-readable explanation of the detection */
|
|
27
|
+
reason: string;
|
|
28
|
+
/** Suggested continuation prompt (for 'continue' strategy) */
|
|
29
|
+
continuationPrompt?: string;
|
|
30
|
+
/** Backoff attempt number (for 'backoff_and_retry') */
|
|
31
|
+
backoffAttempt?: number;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface ErrorContext {
|
|
35
|
+
exitCode: number | null;
|
|
36
|
+
killed: boolean;
|
|
37
|
+
tailOutput: string;
|
|
38
|
+
/** Whether the agent stream had an end_turn signal */
|
|
39
|
+
hasEndTurn?: boolean;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// ── Pattern Registry ───────────────────────────────────────────────
|
|
43
|
+
|
|
44
|
+
const TRUNCATED_PATTERNS = [
|
|
45
|
+
/max.?output/i,
|
|
46
|
+
/output.?limit/i,
|
|
47
|
+
/max.?tokens/i,
|
|
48
|
+
/response.?truncat/i,
|
|
49
|
+
/generation.?limit/i,
|
|
50
|
+
];
|
|
51
|
+
|
|
52
|
+
const CONTEXT_OVERFLOW_PATTERNS = [
|
|
53
|
+
/overlong.?prompt/i,
|
|
54
|
+
/context.?length.?exceed/i,
|
|
55
|
+
/prompt.?too.?long/i,
|
|
56
|
+
/maximum.?context/i,
|
|
57
|
+
/token.?limit.?exceed/i,
|
|
58
|
+
/input.?too.?long/i,
|
|
59
|
+
/request.?too.?large/i,
|
|
60
|
+
/max.?input.?tokens/i,
|
|
61
|
+
/content.?too.?long/i,
|
|
62
|
+
/exceeds.?(?:the\s+)?(?:model'?s?\s+)?(?:maximum|max).?(?:context|token)/i,
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const TRANSIENT_PATTERNS = [
|
|
66
|
+
/rate.?limit/i,
|
|
67
|
+
/429/,
|
|
68
|
+
/too.?many.?requests/i,
|
|
69
|
+
/server.?error/i,
|
|
70
|
+
/502|503|504/,
|
|
71
|
+
/bad.?gateway/i,
|
|
72
|
+
/service.?unavailable/i,
|
|
73
|
+
/gateway.?timeout/i,
|
|
74
|
+
/ECONNRESET/i,
|
|
75
|
+
/ECONNREFUSED/i,
|
|
76
|
+
/ETIMEDOUT/i,
|
|
77
|
+
/socket.?hang.?up/i,
|
|
78
|
+
/network.?error/i,
|
|
79
|
+
/temporary.?failure/i,
|
|
80
|
+
/overloaded/i,
|
|
81
|
+
/capacity/i,
|
|
82
|
+
];
|
|
83
|
+
|
|
84
|
+
// ── Backoff State ──────────────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
const backoffState = new Map<string, number>(); // taskId → consecutive transient failures
|
|
87
|
+
|
|
88
|
+
/** Reset backoff state for a task (call on success or non-transient failure) */
|
|
89
|
+
export function resetBackoff(taskId: string): void {
|
|
90
|
+
backoffState.delete(taskId);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Get current backoff attempt count for a task */
|
|
94
|
+
export function getBackoffAttempt(taskId: string): number {
|
|
95
|
+
return backoffState.get(taskId) ?? 0;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// ── Core Classification ────────────────────────────────────────────
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Classify an agent failure and recommend a recovery action.
|
|
102
|
+
*
|
|
103
|
+
* Priority order: truncated > context_overflow > transient > unknown.
|
|
104
|
+
* This ensures the most specific recovery strategy is chosen.
|
|
105
|
+
*/
|
|
106
|
+
export function classifyError(ctx: ErrorContext, taskId?: string): RecoveryAction {
|
|
107
|
+
const { exitCode, killed, tailOutput, hasEndTurn } = ctx;
|
|
108
|
+
const text = tailOutput.slice(-2000); // Only check last 2KB
|
|
109
|
+
|
|
110
|
+
// 1. Truncated output detection
|
|
111
|
+
// Agent was cut off mid-response (no end_turn + non-zero exit)
|
|
112
|
+
if (!killed && exitCode !== 0 && hasEndTurn === false) {
|
|
113
|
+
return {
|
|
114
|
+
category: 'truncated',
|
|
115
|
+
strategy: 'continue',
|
|
116
|
+
delayMs: 0,
|
|
117
|
+
reason: 'Agent output appears truncated (no end_turn signal with non-zero exit)',
|
|
118
|
+
continuationPrompt: 'Your previous output was truncated due to output limits. Continue from where you stopped. Do not repeat what you already wrote.',
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Also detect explicit truncation error messages
|
|
123
|
+
if (matchesAny(text, TRUNCATED_PATTERNS)) {
|
|
124
|
+
return {
|
|
125
|
+
category: 'truncated',
|
|
126
|
+
strategy: 'continue',
|
|
127
|
+
delayMs: 0,
|
|
128
|
+
reason: `Truncation pattern detected in output`,
|
|
129
|
+
continuationPrompt: 'Your previous output was truncated due to output limits. Continue from where you stopped. Do not repeat what you already wrote.',
|
|
130
|
+
};
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// 2. Context overflow detection
|
|
134
|
+
if (matchesAny(text, CONTEXT_OVERFLOW_PATTERNS)) {
|
|
135
|
+
return {
|
|
136
|
+
category: 'context_overflow',
|
|
137
|
+
strategy: 'compact_and_retry',
|
|
138
|
+
delayMs: 0,
|
|
139
|
+
reason: 'Context overflow detected — prompt is too large for the model',
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// 3. Transient error detection
|
|
144
|
+
if (matchesAny(text, TRANSIENT_PATTERNS)) {
|
|
145
|
+
const attempt = taskId ? (backoffState.get(taskId) ?? 0) + 1 : 1;
|
|
146
|
+
if (taskId) backoffState.set(taskId, attempt);
|
|
147
|
+
|
|
148
|
+
// Exponential backoff: 2s, 4s, 8s, capped at 30s
|
|
149
|
+
const baseDelay = 2000;
|
|
150
|
+
const maxDelay = 30_000;
|
|
151
|
+
const delay = Math.min(baseDelay * Math.pow(2, attempt - 1), maxDelay);
|
|
152
|
+
// Add jitter: ±25%
|
|
153
|
+
const jitter = delay * 0.25 * (Math.random() * 2 - 1);
|
|
154
|
+
|
|
155
|
+
return {
|
|
156
|
+
category: 'transient',
|
|
157
|
+
strategy: 'backoff_and_retry',
|
|
158
|
+
delayMs: Math.round(delay + jitter),
|
|
159
|
+
reason: `Transient error detected (attempt ${attempt})`,
|
|
160
|
+
backoffAttempt: attempt,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// 4. Unknown — fall back to normal retry
|
|
165
|
+
if (taskId) resetBackoff(taskId); // Non-transient failure resets backoff
|
|
166
|
+
|
|
167
|
+
return {
|
|
168
|
+
category: 'unknown',
|
|
169
|
+
strategy: 'normal_retry',
|
|
170
|
+
delayMs: 0,
|
|
171
|
+
reason: killed
|
|
172
|
+
? 'Agent killed by timeout'
|
|
173
|
+
: `Agent exited with code ${exitCode}`,
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Check if the error is recoverable (not a permanent failure).
|
|
179
|
+
* Truncated and transient errors are always recoverable.
|
|
180
|
+
* Context overflow is recoverable if compaction is available.
|
|
181
|
+
* Unknown errors may be recoverable via normal retry.
|
|
182
|
+
*/
|
|
183
|
+
export function isRecoverable(action: RecoveryAction): boolean {
|
|
184
|
+
return action.strategy !== 'normal_retry' || action.category === 'unknown';
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
188
|
+
|
|
189
|
+
function matchesAny(text: string, patterns: RegExp[]): boolean {
|
|
190
|
+
return patterns.some(p => p.test(text));
|
|
191
|
+
}
|
|
@@ -1,140 +1,140 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Evidence Directory — Phase 7, Step 9: Per-task evidence collection.
|
|
3
|
-
*
|
|
4
|
-
* Creates a structured evidence directory for each pipeline run:
|
|
5
|
-
* .pipeline/<pipelineId>/evidence/<taskId>/
|
|
6
|
-
* prompt.md, output.txt, compile.txt, test.txt, result.json
|
|
7
|
-
*
|
|
8
|
-
* Design principle: evidence collection is best-effort. Disk write failures
|
|
9
|
-
* are logged and ignored — never crash the pipeline for evidence.
|
|
10
|
-
*/
|
|
11
|
-
|
|
12
|
-
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
13
|
-
import { join } from 'node:path';
|
|
14
|
-
import type { GateResult } from './verify-gate.js';
|
|
15
|
-
import type { TokenUsage } from './adapters/types.js';
|
|
16
|
-
|
|
17
|
-
// ── Types ──────────────────────────────────────────────────────────
|
|
18
|
-
|
|
19
|
-
export interface TaskEvidence {
|
|
20
|
-
taskId: string;
|
|
21
|
-
taskDescription: string;
|
|
22
|
-
agentName: string;
|
|
23
|
-
status: 'completed' | 'failed';
|
|
24
|
-
durationMs: number;
|
|
25
|
-
prompt?: string;
|
|
26
|
-
tailOutput?: string;
|
|
27
|
-
gateResults?: GateResult[];
|
|
28
|
-
tokenUsage?: Record<string, TokenUsage>;
|
|
29
|
-
costUSD?: number | null;
|
|
30
|
-
fixAttempts?: number;
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
export interface PipelineSummary {
|
|
34
|
-
pipelineId: string;
|
|
35
|
-
goal: string;
|
|
36
|
-
totalTasks: number;
|
|
37
|
-
completed: number;
|
|
38
|
-
failed: number;
|
|
39
|
-
elapsedMs: number;
|
|
40
|
-
tokenUsage?: Record<string, TokenUsage>;
|
|
41
|
-
costUSD?: number | null;
|
|
42
|
-
tasks: TaskEvidence[];
|
|
43
|
-
/** A4: Idle agents and why they weren't dispatched */
|
|
44
|
-
idleAgents?: Array<{ name: string; reason: string }>;
|
|
45
|
-
/** A2: Routing decisions for explainability */
|
|
46
|
-
routingDecisions?: Array<{ role: string; selected: string; reason: string; available: string[] }>;
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
// ── Core ───────────────────────────────────────────────────────────
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* Write evidence for a single task. Best-effort — never throws.
|
|
53
|
-
*/
|
|
54
|
-
export function writeTaskEvidence(
|
|
55
|
-
projectDir: string,
|
|
56
|
-
pipelineId: string,
|
|
57
|
-
evidence: TaskEvidence,
|
|
58
|
-
): string | null {
|
|
59
|
-
try {
|
|
60
|
-
const dir = join(projectDir, '.pipeline', pipelineId, 'evidence', evidence.taskId.slice(0, 12));
|
|
61
|
-
mkdirSync(dir, { recursive: true });
|
|
62
|
-
|
|
63
|
-
if (evidence.prompt) {
|
|
64
|
-
safeWrite(join(dir, 'prompt.md'), evidence.prompt);
|
|
65
|
-
}
|
|
66
|
-
if (evidence.tailOutput) {
|
|
67
|
-
safeWrite(join(dir, 'output.txt'), evidence.tailOutput);
|
|
68
|
-
}
|
|
69
|
-
if (evidence.gateResults) {
|
|
70
|
-
for (const g of evidence.gateResults) {
|
|
71
|
-
const content = `Command: ${g.command}\nStatus: ${g.passed ? 'PASS' : 'FAIL'}\nDuration: ${(g.durationMs / 1000).toFixed(1)}s\n\n${g.output}`;
|
|
72
|
-
safeWrite(join(dir, `${g.gate}.txt`), content);
|
|
73
|
-
}
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const resultJson = {
|
|
77
|
-
taskId: evidence.taskId,
|
|
78
|
-
description: evidence.taskDescription,
|
|
79
|
-
agent: evidence.agentName,
|
|
80
|
-
status: evidence.status,
|
|
81
|
-
durationMs: evidence.durationMs,
|
|
82
|
-
fixAttempts: evidence.fixAttempts ?? 0,
|
|
83
|
-
gates: evidence.gateResults?.map(g => ({ gate: g.gate, passed: g.passed, durationMs: g.durationMs, command: g.command })),
|
|
84
|
-
tokenUsage: evidence.tokenUsage,
|
|
85
|
-
costUSD: evidence.costUSD,
|
|
86
|
-
};
|
|
87
|
-
safeWrite(join(dir, 'result.json'), JSON.stringify(resultJson, null, 2));
|
|
88
|
-
return dir;
|
|
89
|
-
} catch {
|
|
90
|
-
return null;
|
|
91
|
-
}
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Write pipeline summary markdown. Best-effort — never throws.
|
|
96
|
-
*/
|
|
97
|
-
export function writePipelineSummary(
|
|
98
|
-
projectDir: string,
|
|
99
|
-
summary: PipelineSummary,
|
|
100
|
-
): string | null {
|
|
101
|
-
try {
|
|
102
|
-
const dir = join(projectDir, '.pipeline', summary.pipelineId);
|
|
103
|
-
mkdirSync(dir, { recursive: true });
|
|
104
|
-
|
|
105
|
-
const elapsed = (summary.elapsedMs / 1000).toFixed(0);
|
|
106
|
-
const lines = [
|
|
107
|
-
`# Pipeline: ${summary.goal.slice(0, 100)}`,
|
|
108
|
-
'',
|
|
109
|
-
`- **ID**: ${summary.pipelineId}`,
|
|
110
|
-
`- **Tasks**: ${summary.completed}/${summary.totalTasks} completed, ${summary.failed} failed`,
|
|
111
|
-
`- **Elapsed**: ${elapsed}s`,
|
|
112
|
-
];
|
|
113
|
-
|
|
114
|
-
if (summary.costUSD != null) {
|
|
115
|
-
lines.push(`- **Cost**: $${summary.costUSD.toFixed(4)}`);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
lines.push('', '## Tasks', '');
|
|
119
|
-
for (const t of summary.tasks) {
|
|
120
|
-
const status = t.status === 'completed' ? 'PASS' : 'FAIL';
|
|
121
|
-
const dur = (t.durationMs / 1000).toFixed(1);
|
|
122
|
-
const fixes = t.fixAttempts ? ` (${t.fixAttempts} fix attempts)` : '';
|
|
123
|
-
lines.push(`- [${status}] ${t.taskDescription.slice(0, 80)} — ${t.agentName}, ${dur}s${fixes}`);
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const content = lines.join('\n') + '\n';
|
|
127
|
-
safeWrite(join(dir, 'summary.md'), content);
|
|
128
|
-
return join(dir, 'summary.md');
|
|
129
|
-
} catch {
|
|
130
|
-
return null;
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// ── Helpers ────────────────────────────────────────────────────────
|
|
135
|
-
|
|
136
|
-
function safeWrite(path: string, content: string): void {
|
|
137
|
-
try {
|
|
138
|
-
writeFileSync(path, content, 'utf-8');
|
|
139
|
-
} catch { /* best-effort */ }
|
|
140
|
-
}
|
|
1
|
+
/**
|
|
2
|
+
* Evidence Directory — Phase 7, Step 9: Per-task evidence collection.
|
|
3
|
+
*
|
|
4
|
+
* Creates a structured evidence directory for each pipeline run:
|
|
5
|
+
* .pipeline/<pipelineId>/evidence/<taskId>/
|
|
6
|
+
* prompt.md, output.txt, compile.txt, test.txt, result.json
|
|
7
|
+
*
|
|
8
|
+
* Design principle: evidence collection is best-effort. Disk write failures
|
|
9
|
+
* are logged and ignored — never crash the pipeline for evidence.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { mkdirSync, writeFileSync } from 'node:fs';
|
|
13
|
+
import { join } from 'node:path';
|
|
14
|
+
import type { GateResult } from './verify-gate.js';
|
|
15
|
+
import type { TokenUsage } from './adapters/types.js';
|
|
16
|
+
|
|
17
|
+
// ── Types ──────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
export interface TaskEvidence {
|
|
20
|
+
taskId: string;
|
|
21
|
+
taskDescription: string;
|
|
22
|
+
agentName: string;
|
|
23
|
+
status: 'completed' | 'failed';
|
|
24
|
+
durationMs: number;
|
|
25
|
+
prompt?: string;
|
|
26
|
+
tailOutput?: string;
|
|
27
|
+
gateResults?: GateResult[];
|
|
28
|
+
tokenUsage?: Record<string, TokenUsage>;
|
|
29
|
+
costUSD?: number | null;
|
|
30
|
+
fixAttempts?: number;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PipelineSummary {
|
|
34
|
+
pipelineId: string;
|
|
35
|
+
goal: string;
|
|
36
|
+
totalTasks: number;
|
|
37
|
+
completed: number;
|
|
38
|
+
failed: number;
|
|
39
|
+
elapsedMs: number;
|
|
40
|
+
tokenUsage?: Record<string, TokenUsage>;
|
|
41
|
+
costUSD?: number | null;
|
|
42
|
+
tasks: TaskEvidence[];
|
|
43
|
+
/** A4: Idle agents and why they weren't dispatched */
|
|
44
|
+
idleAgents?: Array<{ name: string; reason: string }>;
|
|
45
|
+
/** A2: Routing decisions for explainability */
|
|
46
|
+
routingDecisions?: Array<{ role: string; selected: string; reason: string; available: string[] }>;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// ── Core ───────────────────────────────────────────────────────────
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Write evidence for a single task. Best-effort — never throws.
|
|
53
|
+
*/
|
|
54
|
+
export function writeTaskEvidence(
|
|
55
|
+
projectDir: string,
|
|
56
|
+
pipelineId: string,
|
|
57
|
+
evidence: TaskEvidence,
|
|
58
|
+
): string | null {
|
|
59
|
+
try {
|
|
60
|
+
const dir = join(projectDir, '.pipeline', pipelineId, 'evidence', evidence.taskId.slice(0, 12));
|
|
61
|
+
mkdirSync(dir, { recursive: true });
|
|
62
|
+
|
|
63
|
+
if (evidence.prompt) {
|
|
64
|
+
safeWrite(join(dir, 'prompt.md'), evidence.prompt);
|
|
65
|
+
}
|
|
66
|
+
if (evidence.tailOutput) {
|
|
67
|
+
safeWrite(join(dir, 'output.txt'), evidence.tailOutput);
|
|
68
|
+
}
|
|
69
|
+
if (evidence.gateResults) {
|
|
70
|
+
for (const g of evidence.gateResults) {
|
|
71
|
+
const content = `Command: ${g.command}\nStatus: ${g.passed ? 'PASS' : 'FAIL'}\nDuration: ${(g.durationMs / 1000).toFixed(1)}s\n\n${g.output}`;
|
|
72
|
+
safeWrite(join(dir, `${g.gate}.txt`), content);
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const resultJson = {
|
|
77
|
+
taskId: evidence.taskId,
|
|
78
|
+
description: evidence.taskDescription,
|
|
79
|
+
agent: evidence.agentName,
|
|
80
|
+
status: evidence.status,
|
|
81
|
+
durationMs: evidence.durationMs,
|
|
82
|
+
fixAttempts: evidence.fixAttempts ?? 0,
|
|
83
|
+
gates: evidence.gateResults?.map(g => ({ gate: g.gate, passed: g.passed, durationMs: g.durationMs, command: g.command })),
|
|
84
|
+
tokenUsage: evidence.tokenUsage,
|
|
85
|
+
costUSD: evidence.costUSD,
|
|
86
|
+
};
|
|
87
|
+
safeWrite(join(dir, 'result.json'), JSON.stringify(resultJson, null, 2));
|
|
88
|
+
return dir;
|
|
89
|
+
} catch {
|
|
90
|
+
return null;
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Write pipeline summary markdown. Best-effort — never throws.
|
|
96
|
+
*/
|
|
97
|
+
export function writePipelineSummary(
|
|
98
|
+
projectDir: string,
|
|
99
|
+
summary: PipelineSummary,
|
|
100
|
+
): string | null {
|
|
101
|
+
try {
|
|
102
|
+
const dir = join(projectDir, '.pipeline', summary.pipelineId);
|
|
103
|
+
mkdirSync(dir, { recursive: true });
|
|
104
|
+
|
|
105
|
+
const elapsed = (summary.elapsedMs / 1000).toFixed(0);
|
|
106
|
+
const lines = [
|
|
107
|
+
`# Pipeline: ${summary.goal.slice(0, 100)}`,
|
|
108
|
+
'',
|
|
109
|
+
`- **ID**: ${summary.pipelineId}`,
|
|
110
|
+
`- **Tasks**: ${summary.completed}/${summary.totalTasks} completed, ${summary.failed} failed`,
|
|
111
|
+
`- **Elapsed**: ${elapsed}s`,
|
|
112
|
+
];
|
|
113
|
+
|
|
114
|
+
if (summary.costUSD != null) {
|
|
115
|
+
lines.push(`- **Cost**: $${summary.costUSD.toFixed(4)}`);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
lines.push('', '## Tasks', '');
|
|
119
|
+
for (const t of summary.tasks) {
|
|
120
|
+
const status = t.status === 'completed' ? 'PASS' : 'FAIL';
|
|
121
|
+
const dur = (t.durationMs / 1000).toFixed(1);
|
|
122
|
+
const fixes = t.fixAttempts ? ` (${t.fixAttempts} fix attempts)` : '';
|
|
123
|
+
lines.push(`- [${status}] ${t.taskDescription.slice(0, 80)} — ${t.agentName}, ${dur}s${fixes}`);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
const content = lines.join('\n') + '\n';
|
|
127
|
+
safeWrite(join(dir, 'summary.md'), content);
|
|
128
|
+
return join(dir, 'summary.md');
|
|
129
|
+
} catch {
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
// ── Helpers ────────────────────────────────────────────────────────
|
|
135
|
+
|
|
136
|
+
function safeWrite(path: string, content: string): void {
|
|
137
|
+
try {
|
|
138
|
+
writeFileSync(path, content, 'utf-8');
|
|
139
|
+
} catch { /* best-effort */ }
|
|
140
|
+
}
|