salmon-loop 0.3.1 → 0.4.1
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/cli/authorization/non-interactive.js +9 -13
- package/dist/cli/chat.js +12 -6
- package/dist/cli/commands/allowlist.js +1 -1
- package/dist/cli/commands/chat.js +13 -13
- package/dist/cli/commands/parallel.js +1 -1
- package/dist/cli/commands/run/handler.js +6 -3
- package/dist/cli/commands/run/loop-params.js +1 -0
- package/dist/cli/commands/run/parse-options.js +14 -26
- package/dist/cli/commands/run/runtime-llm.js +15 -12
- package/dist/cli/commands/serve.js +14 -1
- package/dist/cli/headless/openai-responses-canonical-applier.js +1 -7
- package/dist/cli/reporters/standard.js +2 -3
- package/dist/cli/reporters/stream-json.js +2 -1
- package/dist/cli/slash/runtime.js +2 -2
- package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
- package/dist/cli/ui/hooks/useLoopEvents.js +1 -1
- package/dist/cli/ui/hooks/useLoopState.js +1 -1
- package/dist/core/ast/parser.js +18 -9
- package/dist/core/config/schema.js +738 -0
- package/dist/core/config/validate.js +11 -922
- package/dist/core/context/gatherers/ast-gatherer.js +4 -12
- package/dist/core/context/gatherers/ghost-dependency-gatherer.js +0 -1
- package/dist/core/context/gatherers/knowledge-gatherer.js +3 -0
- package/dist/core/context/service.js +39 -8
- package/dist/core/context/token/encoding-registry.js +7 -6
- package/dist/core/extensions/index.js +48 -3
- package/dist/core/extensions/load.js +3 -2
- package/dist/core/extensions/merge.js +5 -1
- package/dist/core/extensions/paths.js +6 -0
- package/dist/core/extensions/schemas.js +21 -0
- package/dist/core/facades/cli-command-chat.js +2 -0
- package/dist/core/facades/cli-run-handler.js +1 -0
- package/dist/core/facades/cli-utils-serialize.js +2 -0
- package/dist/core/grizzco/dsl/llm-strategy.js +3 -2
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +15 -10
- package/dist/core/grizzco/engine/pipeline/pipeline.js +149 -240
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +5 -4
- package/dist/core/grizzco/engine/transaction/authorization-summary.js +2 -1
- package/dist/core/grizzco/runtime/apply-back-runtime.js +2 -1
- package/dist/core/grizzco/services/registry.js +18 -0
- package/dist/core/grizzco/steps/audit.js +20 -10
- package/dist/core/grizzco/steps/display-report.js +4 -11
- package/dist/core/grizzco/steps/explore.js +9 -2
- package/dist/core/grizzco/steps/patch/prompt-input.js +4 -1
- package/dist/core/grizzco/steps/patch.js +1 -0
- package/dist/core/grizzco/steps/plan.js +58 -49
- package/dist/core/grizzco/steps/tool-runtime.js +3 -0
- package/dist/core/grizzco/workers/strata-sync-worker.js +2 -1
- package/dist/core/llm/ai-sdk/message-mapper.js +24 -18
- package/dist/core/llm/ai-sdk/request-params.js +1 -3
- package/dist/core/llm/ai-sdk/result-mapper.js +14 -8
- package/dist/core/llm/ai-sdk/retry-classifier.js +6 -4
- package/dist/core/llm/contracts/repair.js +16 -8
- package/dist/core/llm/errors.js +13 -10
- package/dist/core/llm/output-policy.js +8 -0
- package/dist/core/llm/redact.js +1 -3
- package/dist/core/llm/sub-agent-factory.js +48 -0
- package/dist/core/llm/tool-calling-stub.js +48 -0
- package/dist/core/llm/utils.js +17 -6
- package/dist/core/mcp/bridge/prompt-command-provider.js +4 -3
- package/dist/core/mcp/bridge/tool-bridge.js +5 -14
- package/dist/core/mcp/client/connection-manager.js +3 -2
- package/dist/core/mcp/host/sampling-provider.js +1 -1
- package/dist/core/mcp/schema/json-schema-to-zod.js +2 -1
- package/dist/core/memory/relevant-retrieval.js +6 -4
- package/dist/core/observability/authorization-decisions.js +13 -12
- package/dist/core/observability/error-mapping.js +2 -1
- package/dist/core/observability/token-usage.js +5 -4
- package/dist/core/plugin/loader.js +5 -4
- package/dist/core/prompts/registry.js +11 -29
- package/dist/core/protocols/a2a/sdk/server.js +2 -3
- package/dist/core/protocols/acp/formal-agent.js +10 -4
- package/dist/core/protocols/acp/stdio-server.js +6 -6
- package/dist/core/runtime/agent-server-runtime.js +3 -2
- package/dist/core/runtime/initialize.js +70 -6
- package/dist/core/session/compaction/index.js +4 -3
- package/dist/core/session/manager.js +41 -47
- package/dist/core/session/token-tracker.js +18 -7
- package/dist/core/skills/parser.js +3 -2
- package/dist/core/skills/runtime/MicroTaskRunner.js +1 -1
- package/dist/core/skills/runtime/SkillRunner.js +5 -2
- package/dist/core/slash/steps/slash-execute.js +7 -5
- package/dist/core/slash/strategy.js +1 -1
- package/dist/core/strata/layers/worktree.js +7 -9
- package/dist/core/strata/runtime/synchronizer.js +10 -9
- package/dist/core/streaming/canonical/parts-from-llm-stream-chunk.js +1 -11
- package/dist/core/structured-output/json-schema-validator.js +1 -13
- package/dist/core/sub-agent/context-snapshot.js +12 -6
- package/dist/core/sub-agent/controller.js +70 -1
- package/dist/core/sub-agent/core/loop.js +25 -3
- package/dist/core/sub-agent/core/manager.js +319 -116
- package/dist/core/sub-agent/registry-defaults.js +12 -0
- package/dist/core/sub-agent/registry.js +8 -0
- package/dist/core/sub-agent/team.js +98 -0
- package/dist/core/sub-agent/tools/task-await.js +109 -0
- package/dist/core/sub-agent/tools/task-spawn.js +49 -7
- package/dist/core/sub-agent/tools/team.js +92 -0
- package/dist/core/sub-agent/types.js +11 -2
- package/dist/core/tools/budget.js +4 -11
- package/dist/core/tools/builtin/code-search/executor.js +46 -43
- package/dist/core/tools/builtin/fs.js +14 -6
- package/dist/core/tools/builtin/index.js +41 -107
- package/dist/core/tools/builtin/interaction.js +13 -15
- package/dist/core/tools/builtin/proposal.js +11 -2
- package/dist/core/tools/capability/executor.js +5 -5
- package/dist/core/tools/headless-payload.js +1 -3
- package/dist/core/tools/mapper.js +8 -42
- package/dist/core/tools/parallel/persistence.js +17 -5
- package/dist/core/tools/parallel/scheduler.js +23 -21
- package/dist/core/tools/permissions/permission-rules.js +66 -114
- package/dist/core/tools/plugins/loader.js +4 -3
- package/dist/core/tools/router.js +24 -53
- package/dist/core/tools/session.js +54 -97
- package/dist/core/tools/streaming/ToolCallAccumulator.js +1 -3
- package/dist/core/tools/tool-visibility.js +2 -1
- package/dist/core/tools/types.js +10 -0
- package/dist/core/utils/error.js +79 -0
- package/dist/core/utils/serialize.js +63 -0
- package/dist/core/utils/zod.js +29 -0
- package/dist/core/workspace/capabilities.js +3 -2
- package/dist/integrations/langfuse/litellm-langfuse-outcome-reporter.js +9 -8
- package/dist/locales/en.js +2 -1
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@ import { recordAuditEvent, setAuditContext } from '../../../observability/audit-
|
|
|
2
2
|
import { getLogger } from '../../../observability/logger.js';
|
|
3
3
|
import { appendPlanNote } from '../../../plan/index.js';
|
|
4
4
|
import { EXECUTION_PHASES, } from '../../../types/runtime.js';
|
|
5
|
+
import { isRecord } from '../../../utils/serialize.js';
|
|
5
6
|
/**
|
|
6
7
|
* Typed Async Pipeline Container
|
|
7
8
|
*/
|
|
@@ -28,271 +29,179 @@ export class Pipeline {
|
|
|
28
29
|
* Add a step to the pipeline
|
|
29
30
|
*/
|
|
30
31
|
step(name, action) {
|
|
31
|
-
const nextPromise = this.
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
const persistenceRoot = ctx?.workspace?.baseRepoPath || ctx?.workspace?.workPath;
|
|
41
|
-
const attempt = ctx?.attempt ?? 1;
|
|
42
|
-
const tryAppendPlanNote = async (note) => {
|
|
43
|
-
if (!planRuntime || !persistenceRoot)
|
|
44
|
-
return;
|
|
45
|
-
try {
|
|
46
|
-
await appendPlanNote({
|
|
47
|
-
persistenceRoot,
|
|
48
|
-
sessionId: planRuntime.sessionId,
|
|
49
|
-
note,
|
|
50
|
-
});
|
|
51
|
-
recordAuditEvent('plan.runtime.note.append', { note, ok: true }, { source: 'plan', severity: 'low', scope: 'session', phase: name });
|
|
52
|
-
return true;
|
|
53
|
-
}
|
|
54
|
-
catch (e) {
|
|
55
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
56
|
-
recordAuditEvent('plan.runtime.note.append.failed', { note, error: msg }, { source: 'plan', severity: 'low', scope: 'session', phase: name });
|
|
57
|
-
getLogger().debug(`[PlanRuntime] Failed to append note: ${msg}`);
|
|
58
|
-
return false;
|
|
59
|
-
}
|
|
60
|
-
};
|
|
61
|
-
try {
|
|
62
|
-
this.ctxRef.current = ctx;
|
|
63
|
-
const signal = ctx?.options?.signal;
|
|
64
|
-
const strategy = ctx?.workspace?.strategy ?? ctx?.options?.strategy;
|
|
65
|
-
if (signal?.aborted && strategy === 'worktree') {
|
|
66
|
-
throw new Error('Operation cancelled by user');
|
|
67
|
-
}
|
|
68
|
-
setAuditContext({ phase: name });
|
|
69
|
-
if (emit && isPhase(name)) {
|
|
70
|
-
emit({ type: 'phase.start', phase: name, timestamp: new Date() });
|
|
71
|
-
phaseStarted = true;
|
|
72
|
-
const ok = await tryAppendPlanNote(`Attempt ${attempt}: phase.start ${name}`);
|
|
73
|
-
if (planRuntime && ok !== undefined) {
|
|
74
|
-
emit({
|
|
75
|
-
type: 'plan.runtime.journal',
|
|
76
|
-
sessionId: planRuntime.sessionId,
|
|
77
|
-
phase: name,
|
|
78
|
-
kind: 'start',
|
|
79
|
-
attempt,
|
|
80
|
-
ok,
|
|
81
|
-
timestamp: new Date(),
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
result = await action(ctx);
|
|
86
|
-
this.ctxRef.current = result;
|
|
87
|
-
// APPLY_BACK failure is represented as a structured result (not an exception) to preserve context.
|
|
88
|
-
// The pipeline must still treat it as a phase failure for progress reporting and plan journaling.
|
|
89
|
-
if (name === 'APPLY_BACK' &&
|
|
90
|
-
result &&
|
|
91
|
-
typeof result === 'object' &&
|
|
92
|
-
result.applyBackResult &&
|
|
93
|
-
typeof result.applyBackResult === 'object') {
|
|
94
|
-
const applyBackResult = result.applyBackResult;
|
|
95
|
-
if (applyBackResult.success === false && !applyBackResult.skipped) {
|
|
96
|
-
errorStr = applyBackResult.safeMessage || applyBackResult.error || 'Apply-back failed';
|
|
97
|
-
errorMeta = {
|
|
32
|
+
const nextPromise = this.executeStep(name, action, async () => {
|
|
33
|
+
// No recovery for plain steps — check for APPLY_BACK structured failure
|
|
34
|
+
const result = this.ctxRef.current;
|
|
35
|
+
if (name === 'APPLY_BACK' && isRecord(result) && isRecord(result.applyBackResult)) {
|
|
36
|
+
const applyBackResult = result.applyBackResult;
|
|
37
|
+
if (applyBackResult.success === false && !applyBackResult.skipped) {
|
|
38
|
+
return {
|
|
39
|
+
errorStr: applyBackResult.safeMessage || applyBackResult.error || 'Apply-back failed',
|
|
40
|
+
errorMeta: {
|
|
98
41
|
name: 'ApplyBackFailure',
|
|
99
42
|
code: applyBackResult.errorCode || 'APPLY_BACK_FAILED',
|
|
100
|
-
}
|
|
101
|
-
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
102
45
|
}
|
|
103
|
-
return result;
|
|
104
46
|
}
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
47
|
+
return null;
|
|
48
|
+
});
|
|
49
|
+
return new Pipeline(nextPromise, this.startTime, name, this.traces, this.ctxRef);
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* Add a step with error recovery
|
|
53
|
+
*/
|
|
54
|
+
stepWithRecovery(name, action, recovery) {
|
|
55
|
+
const nextPromise = this.executeStep(name, action, async (ctx, errorStr) => {
|
|
56
|
+
// Only run recovery when the step actually failed
|
|
57
|
+
if (!errorStr)
|
|
58
|
+
return null;
|
|
59
|
+
const recStart = Date.now();
|
|
60
|
+
try {
|
|
61
|
+
await recovery(ctx);
|
|
62
|
+
this.traces.push({
|
|
63
|
+
name: `${name}:recovery`,
|
|
64
|
+
start: recStart,
|
|
65
|
+
end: Date.now(),
|
|
66
|
+
duration: Date.now() - recStart,
|
|
67
|
+
metadata: { success: true },
|
|
68
|
+
});
|
|
116
69
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
type: 'phase.end',
|
|
121
|
-
phase: name,
|
|
122
|
-
success: !errorStr,
|
|
123
|
-
timestamp: new Date(),
|
|
124
|
-
});
|
|
125
|
-
const ok = await tryAppendPlanNote(`Attempt ${attempt}: phase.end ${name} (success=${String(!errorStr)})`);
|
|
126
|
-
if (planRuntime && ok !== undefined) {
|
|
127
|
-
emit({
|
|
128
|
-
type: 'plan.runtime.journal',
|
|
129
|
-
sessionId: planRuntime.sessionId,
|
|
130
|
-
phase: name,
|
|
131
|
-
kind: 'end',
|
|
132
|
-
attempt,
|
|
133
|
-
ok,
|
|
134
|
-
timestamp: new Date(),
|
|
135
|
-
});
|
|
136
|
-
}
|
|
137
|
-
}
|
|
138
|
-
setAuditContext({ phase: undefined });
|
|
139
|
-
const end = Date.now();
|
|
70
|
+
catch (recError) {
|
|
71
|
+
const recEnd = Date.now();
|
|
72
|
+
const errorDetail = recError instanceof Error ? recError.message : String(recError);
|
|
140
73
|
this.traces.push({
|
|
141
|
-
name
|
|
142
|
-
start,
|
|
143
|
-
end,
|
|
144
|
-
duration:
|
|
145
|
-
error:
|
|
146
|
-
metadata:
|
|
74
|
+
name: `${name}:recovery`,
|
|
75
|
+
start: recStart,
|
|
76
|
+
end: recEnd,
|
|
77
|
+
duration: recEnd - recStart,
|
|
78
|
+
error: errorDetail,
|
|
79
|
+
metadata: { success: false, phase: 'RECOVERY_FAILURE' },
|
|
147
80
|
});
|
|
81
|
+
getLogger().audit('PIPELINE_RECOVERY_FAILED', { step: name, originalError: errorStr, recoveryError: errorDetail }, { source: 'system', severity: 'high', scope: 'session' });
|
|
148
82
|
}
|
|
83
|
+
return null; // Always re-throw original error
|
|
149
84
|
});
|
|
150
85
|
return new Pipeline(nextPromise, this.startTime, name, this.traces, this.ctxRef);
|
|
151
86
|
}
|
|
152
87
|
/**
|
|
153
|
-
*
|
|
88
|
+
* Core step execution shared by step() and stepWithRecovery().
|
|
89
|
+
* Handles: abort check, phase events, plan journaling, tracing.
|
|
90
|
+
* The onError callback runs recovery/post-processing; return non-null to inject error metadata.
|
|
154
91
|
*/
|
|
155
|
-
|
|
156
|
-
const
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
persistenceRoot,
|
|
174
|
-
sessionId: planRuntime.sessionId,
|
|
175
|
-
note,
|
|
176
|
-
});
|
|
177
|
-
recordAuditEvent('plan.runtime.note.append', { note, ok: true }, { source: 'plan', severity: 'low', scope: 'session', phase: name });
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
|
-
catch (e) {
|
|
181
|
-
const msg = e instanceof Error ? e.message : String(e);
|
|
182
|
-
recordAuditEvent('plan.runtime.note.append.failed', { note, error: msg }, { source: 'plan', severity: 'low', scope: 'session', phase: name });
|
|
183
|
-
getLogger().debug(`[PlanRuntime] Failed to append note: ${msg}`);
|
|
184
|
-
return false;
|
|
185
|
-
}
|
|
186
|
-
};
|
|
92
|
+
async executeStep(name, action, onError) {
|
|
93
|
+
const start = Date.now();
|
|
94
|
+
let phaseStarted = false;
|
|
95
|
+
let errorStr;
|
|
96
|
+
let errorMeta;
|
|
97
|
+
let result;
|
|
98
|
+
const ctx = await this.promise;
|
|
99
|
+
const emit = ctx.emit;
|
|
100
|
+
const isPhase = (value) => EXECUTION_PHASES.includes(value);
|
|
101
|
+
const ctxObj = isRecord(ctx) ? ctx : null;
|
|
102
|
+
const planRuntime = ctxObj?.planRuntime;
|
|
103
|
+
const workspace = isRecord(ctxObj?.workspace) ? ctxObj.workspace : null;
|
|
104
|
+
const persistenceRoot = (typeof workspace?.baseRepoPath === 'string' ? workspace.baseRepoPath : undefined) ||
|
|
105
|
+
(typeof workspace?.workPath === 'string' ? workspace.workPath : undefined);
|
|
106
|
+
const attempt = typeof ctxObj?.attempt === 'number' ? ctxObj.attempt : 1;
|
|
107
|
+
const tryAppendPlanNote = async (note) => {
|
|
108
|
+
if (!planRuntime || !persistenceRoot)
|
|
109
|
+
return;
|
|
187
110
|
try {
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
if (signal?.aborted && strategy === 'worktree') {
|
|
192
|
-
abortedBeforeAction = true;
|
|
193
|
-
throw new Error('Operation cancelled by user');
|
|
194
|
-
}
|
|
195
|
-
setAuditContext({ phase: name });
|
|
196
|
-
if (emit && isPhase(name)) {
|
|
197
|
-
emit({ type: 'phase.start', phase: name, timestamp: new Date() });
|
|
198
|
-
phaseStarted = true;
|
|
199
|
-
const ok = await tryAppendPlanNote(`Attempt ${attempt}: phase.start ${name}`);
|
|
200
|
-
if (planRuntime && ok !== undefined) {
|
|
201
|
-
emit({
|
|
202
|
-
type: 'plan.runtime.journal',
|
|
203
|
-
sessionId: planRuntime.sessionId,
|
|
204
|
-
phase: name,
|
|
205
|
-
kind: 'start',
|
|
206
|
-
attempt,
|
|
207
|
-
ok,
|
|
208
|
-
timestamp: new Date(),
|
|
209
|
-
});
|
|
210
|
-
}
|
|
211
|
-
}
|
|
212
|
-
result = await action(ctx);
|
|
213
|
-
this.ctxRef.current = result;
|
|
214
|
-
return result;
|
|
111
|
+
await appendPlanNote({ persistenceRoot, sessionId: planRuntime.sessionId, note });
|
|
112
|
+
recordAuditEvent('plan.runtime.note.append', { note, ok: true }, { source: 'plan', severity: 'low', scope: 'session', phase: name });
|
|
113
|
+
return true;
|
|
215
114
|
}
|
|
216
|
-
catch (
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
115
|
+
catch (e) {
|
|
116
|
+
const msg = e instanceof Error ? e.message : String(e);
|
|
117
|
+
recordAuditEvent('plan.runtime.note.append.failed', { note, error: msg }, { source: 'plan', severity: 'low', scope: 'session', phase: name });
|
|
118
|
+
getLogger().debug(`[PlanRuntime] Failed to append note: ${msg}`);
|
|
119
|
+
return false;
|
|
120
|
+
}
|
|
121
|
+
};
|
|
122
|
+
try {
|
|
123
|
+
this.ctxRef.current = ctx;
|
|
124
|
+
const options = isRecord(ctxObj?.options) ? ctxObj.options : null;
|
|
125
|
+
const signal = options?.signal;
|
|
126
|
+
const strategy = (workspace?.strategy ?? options?.strategy);
|
|
127
|
+
if (signal?.aborted && strategy === 'worktree') {
|
|
128
|
+
throw new Error('Operation cancelled by user');
|
|
129
|
+
}
|
|
130
|
+
setAuditContext({ phase: name });
|
|
131
|
+
if (emit && isPhase(name)) {
|
|
132
|
+
emit({ type: 'phase.start', phase: name, timestamp: new Date() });
|
|
133
|
+
phaseStarted = true;
|
|
134
|
+
const ok = await tryAppendPlanNote(`Attempt ${attempt}: phase.start ${name}`);
|
|
135
|
+
if (planRuntime && ok !== undefined) {
|
|
136
|
+
emit({
|
|
137
|
+
type: 'plan.runtime.journal',
|
|
138
|
+
sessionId: planRuntime.sessionId,
|
|
139
|
+
phase: name,
|
|
140
|
+
kind: 'start',
|
|
141
|
+
attempt,
|
|
142
|
+
ok,
|
|
143
|
+
timestamp: new Date(),
|
|
239
144
|
});
|
|
240
145
|
}
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
146
|
+
}
|
|
147
|
+
result = await action(ctx);
|
|
148
|
+
this.ctxRef.current = result;
|
|
149
|
+
// Check for structured failures (e.g., APPLY_BACK)
|
|
150
|
+
const postResult = await onError(ctx, '');
|
|
151
|
+
if (postResult) {
|
|
152
|
+
errorStr = postResult.errorStr;
|
|
153
|
+
errorMeta = postResult.errorMeta;
|
|
154
|
+
}
|
|
155
|
+
return result;
|
|
156
|
+
}
|
|
157
|
+
catch (error) {
|
|
158
|
+
errorStr = error instanceof Error ? error.message : String(error);
|
|
159
|
+
errorMeta =
|
|
160
|
+
typeof error === 'object' && error !== null
|
|
161
|
+
? {
|
|
162
|
+
name: error.name,
|
|
163
|
+
code: error.code,
|
|
164
|
+
llmCode: error.llmCode,
|
|
165
|
+
}
|
|
166
|
+
: undefined;
|
|
167
|
+
// Skip recovery on abort - the operation was cancelled, not failed
|
|
168
|
+
if (errorStr !== 'Operation cancelled by user') {
|
|
169
|
+
// Run recovery/post-processing
|
|
170
|
+
const postResult = await onError(ctx, errorStr);
|
|
171
|
+
if (postResult?.errorStr) {
|
|
172
|
+
errorStr = postResult.errorStr;
|
|
173
|
+
errorMeta = postResult.errorMeta;
|
|
259
174
|
}
|
|
260
|
-
throw error; // Propagate original error
|
|
261
175
|
}
|
|
262
|
-
|
|
263
|
-
|
|
176
|
+
throw error;
|
|
177
|
+
}
|
|
178
|
+
finally {
|
|
179
|
+
if (emit && isPhase(name) && phaseStarted) {
|
|
180
|
+
emit({ type: 'phase.end', phase: name, success: !errorStr, timestamp: new Date() });
|
|
181
|
+
const ok = await tryAppendPlanNote(`Attempt ${attempt}: phase.end ${name} (success=${String(!errorStr)})`);
|
|
182
|
+
if (planRuntime && ok !== undefined) {
|
|
264
183
|
emit({
|
|
265
|
-
type: '
|
|
184
|
+
type: 'plan.runtime.journal',
|
|
185
|
+
sessionId: planRuntime.sessionId,
|
|
266
186
|
phase: name,
|
|
267
|
-
|
|
187
|
+
kind: 'end',
|
|
188
|
+
attempt,
|
|
189
|
+
ok,
|
|
268
190
|
timestamp: new Date(),
|
|
269
191
|
});
|
|
270
|
-
const ok = await tryAppendPlanNote(`Attempt ${attempt}: phase.end ${name} (success=${String(!errorStr)})`);
|
|
271
|
-
if (planRuntime && ok !== undefined) {
|
|
272
|
-
emit({
|
|
273
|
-
type: 'plan.runtime.journal',
|
|
274
|
-
sessionId: planRuntime.sessionId,
|
|
275
|
-
phase: name,
|
|
276
|
-
kind: 'end',
|
|
277
|
-
attempt,
|
|
278
|
-
ok,
|
|
279
|
-
timestamp: new Date(),
|
|
280
|
-
});
|
|
281
|
-
}
|
|
282
192
|
}
|
|
283
|
-
setAuditContext({ phase: undefined });
|
|
284
|
-
const end = Date.now();
|
|
285
|
-
this.traces.push({
|
|
286
|
-
name,
|
|
287
|
-
start,
|
|
288
|
-
end,
|
|
289
|
-
duration: end - start,
|
|
290
|
-
error: errorStr,
|
|
291
|
-
metadata: errorMeta,
|
|
292
|
-
});
|
|
293
193
|
}
|
|
294
|
-
|
|
295
|
-
|
|
194
|
+
setAuditContext({ phase: undefined });
|
|
195
|
+
const end = Date.now();
|
|
196
|
+
this.traces.push({
|
|
197
|
+
name,
|
|
198
|
+
start,
|
|
199
|
+
end,
|
|
200
|
+
duration: end - start,
|
|
201
|
+
error: errorStr,
|
|
202
|
+
metadata: errorMeta,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
296
205
|
}
|
|
297
206
|
/**
|
|
298
207
|
* Execute the pipeline and get the final result
|
|
@@ -5,6 +5,7 @@ import { mapErrorForDisplay } from '../../../observability/error-mapping.js';
|
|
|
5
5
|
import { resolveExecutionProfile } from '../../../runtime/execution-profile.js';
|
|
6
6
|
import { isRecoverableToolInputErrorCode } from '../../../tools/recoverable-tool-errors.js';
|
|
7
7
|
import { EXECUTION_PHASES } from '../../../types/runtime.js';
|
|
8
|
+
import { isRecord } from '../../../utils/serialize.js';
|
|
8
9
|
import { classifyError, isRetryable } from '../../../verification/runner.js';
|
|
9
10
|
const RETRYABLE_PHASES = new Set([
|
|
10
11
|
'CONTEXT',
|
|
@@ -49,20 +50,20 @@ function sanitizeReason(value) {
|
|
|
49
50
|
return mapErrorForDisplay({ message: sanitized }).message;
|
|
50
51
|
}
|
|
51
52
|
function extractInputRequired(error) {
|
|
52
|
-
if (!error
|
|
53
|
+
if (!isRecord(error))
|
|
53
54
|
return undefined;
|
|
54
55
|
const value = 'inputRequired' in error ? error.inputRequired : error;
|
|
55
|
-
if (!value
|
|
56
|
+
if (!isRecord(value))
|
|
56
57
|
return undefined;
|
|
57
58
|
if (typeof value.prompt !== 'string' || typeof value.type !== 'string')
|
|
58
59
|
return undefined;
|
|
59
60
|
return value;
|
|
60
61
|
}
|
|
61
62
|
function extractInterrupt(error) {
|
|
62
|
-
if (!error
|
|
63
|
+
if (!isRecord(error))
|
|
63
64
|
return undefined;
|
|
64
65
|
const value = error.interrupt;
|
|
65
|
-
if (!value
|
|
66
|
+
if (!isRecord(value))
|
|
66
67
|
return undefined;
|
|
67
68
|
if (typeof value.type !== 'string')
|
|
68
69
|
return undefined;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isRecord } from '../../../utils/serialize.js';
|
|
1
2
|
export function buildAuthorizationSummary(logs) {
|
|
2
3
|
if (!logs || logs.length === 0)
|
|
3
4
|
return null;
|
|
@@ -11,7 +12,7 @@ export function buildAuthorizationSummary(logs) {
|
|
|
11
12
|
};
|
|
12
13
|
let hasEntries = false;
|
|
13
14
|
for (const entry of logs) {
|
|
14
|
-
if (!entry || entry.eventType !== 'authorization')
|
|
15
|
+
if (!isRecord(entry) || entry.eventType !== 'authorization')
|
|
15
16
|
continue;
|
|
16
17
|
const source = entry.authSource;
|
|
17
18
|
if (source === 'auto') {
|
|
@@ -2,6 +2,7 @@ import { text } from '../../../locales/index.js';
|
|
|
2
2
|
import { recordAuditEvent } from '../../observability/audit-trail.js';
|
|
3
3
|
import { writeDebugArtifact } from '../../observability/debug-artifacts.js';
|
|
4
4
|
import { buildErrorEnvelope, toSafeErrorSummary } from '../../observability/error-envelope.js';
|
|
5
|
+
import { isRecord } from '../../utils/serialize.js';
|
|
5
6
|
import { collectSidecarPaths } from './apply-back-utils.js';
|
|
6
7
|
export async function runApplyBackPhase(params) {
|
|
7
8
|
const { checkpointRef, initialSnapshotHash, options, synchronizer, activeRepoPath, shadowTaskId, attempt, emit, } = params;
|
|
@@ -89,7 +90,7 @@ export async function runApplyBackPhase(params) {
|
|
|
89
90
|
`safeTelemetry=${JSON.stringify(toSafeTelemetry(applyBackTelemetry), null, 2)}`,
|
|
90
91
|
'',
|
|
91
92
|
`errorType=${error instanceof Error ? error.name : typeof error}`,
|
|
92
|
-
`errorCode=${error
|
|
93
|
+
`errorCode=${isRecord(error) ? (error.code ?? error.llmCode ?? '') : ''}`,
|
|
93
94
|
`errorMessage=${error instanceof Error ? error.message : String(error)}`,
|
|
94
95
|
].join('\n'),
|
|
95
96
|
});
|
|
@@ -26,5 +26,23 @@ export const registry = {
|
|
|
26
26
|
has(id) {
|
|
27
27
|
return services.has(id);
|
|
28
28
|
},
|
|
29
|
+
/**
|
|
30
|
+
* Unregister a data service by its identifier
|
|
31
|
+
*/
|
|
32
|
+
unregister(id) {
|
|
33
|
+
return services.delete(id);
|
|
34
|
+
},
|
|
35
|
+
/**
|
|
36
|
+
* Clear all registered services (for testing or shutdown)
|
|
37
|
+
*/
|
|
38
|
+
clear() {
|
|
39
|
+
services.clear();
|
|
40
|
+
},
|
|
41
|
+
/**
|
|
42
|
+
* List all registered service IDs
|
|
43
|
+
*/
|
|
44
|
+
list() {
|
|
45
|
+
return Array.from(services.keys());
|
|
46
|
+
},
|
|
29
47
|
};
|
|
30
48
|
//# sourceMappingURL=registry.js.map
|
|
@@ -8,6 +8,8 @@ import { mapErrorForDisplay } from '../../observability/error-mapping.js';
|
|
|
8
8
|
import { getLogger } from '../../observability/logger.js';
|
|
9
9
|
import { getAuditDir } from '../../runtime/paths.js';
|
|
10
10
|
import { SalmonError } from '../../types/errors.js';
|
|
11
|
+
import { errorMessage } from '../../utils/error.js';
|
|
12
|
+
import { isRecord } from '../../utils/serialize.js';
|
|
11
13
|
function replaceRedactedTokens(value) {
|
|
12
14
|
if (value === null || value === undefined)
|
|
13
15
|
return value;
|
|
@@ -43,7 +45,7 @@ export async function saveAudit(report, _options) {
|
|
|
43
45
|
});
|
|
44
46
|
}
|
|
45
47
|
catch (error) {
|
|
46
|
-
const msg =
|
|
48
|
+
const msg = errorMessage(error);
|
|
47
49
|
getLogger().warn(`[Audit] Failed to externalize verify output: ${msg}`);
|
|
48
50
|
recordAuditEvent('audit.blob.externalize.failed', { target: 'verifyResult.output', error: msg.slice(0, 500) }, { source: 'saveAudit', severity: 'low', scope: 'session', phase: 'AUDIT' });
|
|
49
51
|
}
|
|
@@ -55,7 +57,7 @@ export async function saveAudit(report, _options) {
|
|
|
55
57
|
});
|
|
56
58
|
}
|
|
57
59
|
catch (error) {
|
|
58
|
-
const msg =
|
|
60
|
+
const msg = errorMessage(error);
|
|
59
61
|
getLogger().warn(`[Audit] Failed to externalize tool audit summaries: ${msg}`);
|
|
60
62
|
recordAuditEvent('audit.blob.externalize.failed', { target: 'toolAuditLogs.*', error: msg.slice(0, 500) }, { source: 'saveAudit', severity: 'low', scope: 'session', phase: 'AUDIT' });
|
|
61
63
|
}
|
|
@@ -70,7 +72,12 @@ export async function saveAudit(report, _options) {
|
|
|
70
72
|
: errorInfo?.code || errorInfo?.llmCode,
|
|
71
73
|
}
|
|
72
74
|
: report.error
|
|
73
|
-
? {
|
|
75
|
+
? {
|
|
76
|
+
name: 'UnknownError',
|
|
77
|
+
message: String(report.error),
|
|
78
|
+
stack: undefined,
|
|
79
|
+
code: undefined,
|
|
80
|
+
}
|
|
74
81
|
: undefined;
|
|
75
82
|
const mappedErrorMeta = replaceRedactedTokens(errorMeta);
|
|
76
83
|
const errorDisplay = mapErrorForDisplay({
|
|
@@ -130,7 +137,7 @@ export async function saveAudit(report, _options) {
|
|
|
130
137
|
return `${auditDir}/${filename}`;
|
|
131
138
|
}
|
|
132
139
|
catch (error) {
|
|
133
|
-
const msg =
|
|
140
|
+
const msg = errorMessage(error);
|
|
134
141
|
getLogger().error(`[Audit] Failed to save audit log: ${msg}`);
|
|
135
142
|
return undefined;
|
|
136
143
|
}
|
|
@@ -162,7 +169,7 @@ async function writeBlobBestEffort(args) {
|
|
|
162
169
|
return { path: path.join('blobs', blobName), sha256, chars: content.length };
|
|
163
170
|
}
|
|
164
171
|
catch (error) {
|
|
165
|
-
const msg =
|
|
172
|
+
const msg = errorMessage(error);
|
|
166
173
|
getLogger().warn(`[Audit] Failed to write blob ${blobName}: ${msg}`);
|
|
167
174
|
recordAuditEvent('audit.blob.write.failed', {
|
|
168
175
|
target: auditTarget,
|
|
@@ -179,7 +186,7 @@ async function externalizeVerifyOutput(args) {
|
|
|
179
186
|
if (!sanitizedContext)
|
|
180
187
|
return;
|
|
181
188
|
const verifyResult = sanitizedContext.verifyResult;
|
|
182
|
-
if (!verifyResult
|
|
189
|
+
if (!isRecord(verifyResult))
|
|
183
190
|
return;
|
|
184
191
|
const output = verifyResult.output;
|
|
185
192
|
if (typeof output !== 'string')
|
|
@@ -292,15 +299,18 @@ function sanitizeContext(ctx) {
|
|
|
292
299
|
if (typed.applyBackResult)
|
|
293
300
|
safe.applyBackResult = typed.applyBackResult;
|
|
294
301
|
if (typed.toolCallingAudit && Array.isArray(typed.toolCallingAudit)) {
|
|
302
|
+
const KEYS_TO_STRIP = new Set([
|
|
303
|
+
'rawArgsPreview',
|
|
304
|
+
'parsedArgsPreview',
|
|
305
|
+
'toolResultErrorMessage',
|
|
306
|
+
]);
|
|
295
307
|
safe.toolCallingAudit = typed.toolCallingAudit.map((entry) => {
|
|
296
308
|
if (!entry || typeof entry !== 'object')
|
|
297
309
|
return entry;
|
|
298
|
-
const
|
|
299
|
-
const keepArgsPreview = typedEntry.toolResultErrorCode === 'INVALID_INPUT';
|
|
310
|
+
const keepArgsPreview = entry.toolResultErrorCode === 'INVALID_INPUT';
|
|
300
311
|
if (keepArgsPreview)
|
|
301
312
|
return entry;
|
|
302
|
-
|
|
303
|
-
return rest;
|
|
313
|
+
return Object.fromEntries(Object.entries(entry).filter(([k]) => !KEYS_TO_STRIP.has(k)));
|
|
304
314
|
});
|
|
305
315
|
}
|
|
306
316
|
if (typed.toolAuditLogger?.getLogs) {
|
|
@@ -1,18 +1,11 @@
|
|
|
1
1
|
import { text } from '../../../locales/index.js';
|
|
2
2
|
import { SalmonError } from '../../types/errors.js';
|
|
3
|
+
import { safeStringify } from '../../utils/serialize.js';
|
|
3
4
|
export class ReportContextMissingError extends SalmonError {
|
|
4
5
|
constructor(message) {
|
|
5
6
|
super(message, 'REPORT_CONTEXT_MISSING');
|
|
6
7
|
}
|
|
7
8
|
}
|
|
8
|
-
function safeStringify(value) {
|
|
9
|
-
try {
|
|
10
|
-
return JSON.stringify(value, null, 2);
|
|
11
|
-
}
|
|
12
|
-
catch {
|
|
13
|
-
return String(value);
|
|
14
|
-
}
|
|
15
|
-
}
|
|
16
9
|
function removeLeadingSpaces(content) {
|
|
17
10
|
const lines = content.split('\n');
|
|
18
11
|
const minIndent = lines
|
|
@@ -40,7 +33,7 @@ function normalizeSuggestions(input) {
|
|
|
40
33
|
}
|
|
41
34
|
if (isReviewSuggestion(item)) {
|
|
42
35
|
const type = typeof item.type === 'string' ? item.type : 'note';
|
|
43
|
-
const content = typeof item.content === 'string' ? item.content : safeStringify(item);
|
|
36
|
+
const content = typeof item.content === 'string' ? item.content : safeStringify(item, { indent: 2 });
|
|
44
37
|
return { type, content };
|
|
45
38
|
}
|
|
46
39
|
return { type: 'note', content: String(item) };
|
|
@@ -51,10 +44,10 @@ function normalizeSuggestions(input) {
|
|
|
51
44
|
}
|
|
52
45
|
if (isReviewSuggestion(input)) {
|
|
53
46
|
const type = typeof input.type === 'string' ? input.type : 'note';
|
|
54
|
-
const content = typeof input.content === 'string' ? input.content : safeStringify(input);
|
|
47
|
+
const content = typeof input.content === 'string' ? input.content : safeStringify(input, { indent: 2 });
|
|
55
48
|
return [{ type, content }];
|
|
56
49
|
}
|
|
57
|
-
return [{ type: 'note', content: safeStringify(input) }];
|
|
50
|
+
return [{ type: 'note', content: safeStringify(input, { indent: 2 }) }];
|
|
58
51
|
}
|
|
59
52
|
export async function displayReport(ctx) {
|
|
60
53
|
if (!ctx.report || !ctx.report.kind) {
|