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.
Files changed (123) hide show
  1. package/dist/cli/authorization/non-interactive.js +9 -13
  2. package/dist/cli/chat.js +12 -6
  3. package/dist/cli/commands/allowlist.js +1 -1
  4. package/dist/cli/commands/chat.js +13 -13
  5. package/dist/cli/commands/parallel.js +1 -1
  6. package/dist/cli/commands/run/handler.js +6 -3
  7. package/dist/cli/commands/run/loop-params.js +1 -0
  8. package/dist/cli/commands/run/parse-options.js +14 -26
  9. package/dist/cli/commands/run/runtime-llm.js +15 -12
  10. package/dist/cli/commands/serve.js +14 -1
  11. package/dist/cli/headless/openai-responses-canonical-applier.js +1 -7
  12. package/dist/cli/reporters/standard.js +2 -3
  13. package/dist/cli/reporters/stream-json.js +2 -1
  14. package/dist/cli/slash/runtime.js +2 -2
  15. package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
  16. package/dist/cli/ui/hooks/useLoopEvents.js +1 -1
  17. package/dist/cli/ui/hooks/useLoopState.js +1 -1
  18. package/dist/core/ast/parser.js +18 -9
  19. package/dist/core/config/schema.js +738 -0
  20. package/dist/core/config/validate.js +11 -922
  21. package/dist/core/context/gatherers/ast-gatherer.js +4 -12
  22. package/dist/core/context/gatherers/ghost-dependency-gatherer.js +0 -1
  23. package/dist/core/context/gatherers/knowledge-gatherer.js +3 -0
  24. package/dist/core/context/service.js +39 -8
  25. package/dist/core/context/token/encoding-registry.js +7 -6
  26. package/dist/core/extensions/index.js +48 -3
  27. package/dist/core/extensions/load.js +3 -2
  28. package/dist/core/extensions/merge.js +5 -1
  29. package/dist/core/extensions/paths.js +6 -0
  30. package/dist/core/extensions/schemas.js +21 -0
  31. package/dist/core/facades/cli-command-chat.js +2 -0
  32. package/dist/core/facades/cli-run-handler.js +1 -0
  33. package/dist/core/facades/cli-utils-serialize.js +2 -0
  34. package/dist/core/grizzco/dsl/llm-strategy.js +3 -2
  35. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +15 -10
  36. package/dist/core/grizzco/engine/pipeline/pipeline.js +149 -240
  37. package/dist/core/grizzco/engine/transaction/attempt-failure.js +5 -4
  38. package/dist/core/grizzco/engine/transaction/authorization-summary.js +2 -1
  39. package/dist/core/grizzco/runtime/apply-back-runtime.js +2 -1
  40. package/dist/core/grizzco/services/registry.js +18 -0
  41. package/dist/core/grizzco/steps/audit.js +20 -10
  42. package/dist/core/grizzco/steps/display-report.js +4 -11
  43. package/dist/core/grizzco/steps/explore.js +9 -2
  44. package/dist/core/grizzco/steps/patch/prompt-input.js +4 -1
  45. package/dist/core/grizzco/steps/patch.js +1 -0
  46. package/dist/core/grizzco/steps/plan.js +58 -49
  47. package/dist/core/grizzco/steps/tool-runtime.js +3 -0
  48. package/dist/core/grizzco/workers/strata-sync-worker.js +2 -1
  49. package/dist/core/llm/ai-sdk/message-mapper.js +24 -18
  50. package/dist/core/llm/ai-sdk/request-params.js +1 -3
  51. package/dist/core/llm/ai-sdk/result-mapper.js +14 -8
  52. package/dist/core/llm/ai-sdk/retry-classifier.js +6 -4
  53. package/dist/core/llm/contracts/repair.js +16 -8
  54. package/dist/core/llm/errors.js +13 -10
  55. package/dist/core/llm/output-policy.js +8 -0
  56. package/dist/core/llm/redact.js +1 -3
  57. package/dist/core/llm/sub-agent-factory.js +48 -0
  58. package/dist/core/llm/tool-calling-stub.js +48 -0
  59. package/dist/core/llm/utils.js +17 -6
  60. package/dist/core/mcp/bridge/prompt-command-provider.js +4 -3
  61. package/dist/core/mcp/bridge/tool-bridge.js +5 -14
  62. package/dist/core/mcp/client/connection-manager.js +3 -2
  63. package/dist/core/mcp/host/sampling-provider.js +1 -1
  64. package/dist/core/mcp/schema/json-schema-to-zod.js +2 -1
  65. package/dist/core/memory/relevant-retrieval.js +6 -4
  66. package/dist/core/observability/authorization-decisions.js +13 -12
  67. package/dist/core/observability/error-mapping.js +2 -1
  68. package/dist/core/observability/token-usage.js +5 -4
  69. package/dist/core/plugin/loader.js +5 -4
  70. package/dist/core/prompts/registry.js +11 -29
  71. package/dist/core/protocols/a2a/sdk/server.js +2 -3
  72. package/dist/core/protocols/acp/formal-agent.js +10 -4
  73. package/dist/core/protocols/acp/stdio-server.js +6 -6
  74. package/dist/core/runtime/agent-server-runtime.js +3 -2
  75. package/dist/core/runtime/initialize.js +70 -6
  76. package/dist/core/session/compaction/index.js +4 -3
  77. package/dist/core/session/manager.js +41 -47
  78. package/dist/core/session/token-tracker.js +18 -7
  79. package/dist/core/skills/parser.js +3 -2
  80. package/dist/core/skills/runtime/MicroTaskRunner.js +1 -1
  81. package/dist/core/skills/runtime/SkillRunner.js +5 -2
  82. package/dist/core/slash/steps/slash-execute.js +7 -5
  83. package/dist/core/slash/strategy.js +1 -1
  84. package/dist/core/strata/layers/worktree.js +7 -9
  85. package/dist/core/strata/runtime/synchronizer.js +10 -9
  86. package/dist/core/streaming/canonical/parts-from-llm-stream-chunk.js +1 -11
  87. package/dist/core/structured-output/json-schema-validator.js +1 -13
  88. package/dist/core/sub-agent/context-snapshot.js +12 -6
  89. package/dist/core/sub-agent/controller.js +70 -1
  90. package/dist/core/sub-agent/core/loop.js +25 -3
  91. package/dist/core/sub-agent/core/manager.js +319 -116
  92. package/dist/core/sub-agent/registry-defaults.js +12 -0
  93. package/dist/core/sub-agent/registry.js +8 -0
  94. package/dist/core/sub-agent/team.js +98 -0
  95. package/dist/core/sub-agent/tools/task-await.js +109 -0
  96. package/dist/core/sub-agent/tools/task-spawn.js +49 -7
  97. package/dist/core/sub-agent/tools/team.js +92 -0
  98. package/dist/core/sub-agent/types.js +11 -2
  99. package/dist/core/tools/budget.js +4 -11
  100. package/dist/core/tools/builtin/code-search/executor.js +46 -43
  101. package/dist/core/tools/builtin/fs.js +14 -6
  102. package/dist/core/tools/builtin/index.js +41 -107
  103. package/dist/core/tools/builtin/interaction.js +13 -15
  104. package/dist/core/tools/builtin/proposal.js +11 -2
  105. package/dist/core/tools/capability/executor.js +5 -5
  106. package/dist/core/tools/headless-payload.js +1 -3
  107. package/dist/core/tools/mapper.js +8 -42
  108. package/dist/core/tools/parallel/persistence.js +17 -5
  109. package/dist/core/tools/parallel/scheduler.js +23 -21
  110. package/dist/core/tools/permissions/permission-rules.js +66 -114
  111. package/dist/core/tools/plugins/loader.js +4 -3
  112. package/dist/core/tools/router.js +24 -53
  113. package/dist/core/tools/session.js +54 -97
  114. package/dist/core/tools/streaming/ToolCallAccumulator.js +1 -3
  115. package/dist/core/tools/tool-visibility.js +2 -1
  116. package/dist/core/tools/types.js +10 -0
  117. package/dist/core/utils/error.js +79 -0
  118. package/dist/core/utils/serialize.js +63 -0
  119. package/dist/core/utils/zod.js +29 -0
  120. package/dist/core/workspace/capabilities.js +3 -2
  121. package/dist/integrations/langfuse/litellm-langfuse-outcome-reporter.js +9 -8
  122. package/dist/locales/en.js +2 -1
  123. 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.promise.then(async (ctx) => {
32
- const start = Date.now();
33
- let phaseStarted = false;
34
- let errorStr;
35
- let errorMeta;
36
- let result;
37
- const emit = ctx.emit;
38
- const isPhase = (value) => EXECUTION_PHASES.includes(value);
39
- const planRuntime = ctx?.planRuntime;
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
- catch (error) {
106
- errorStr = error instanceof Error ? error.message : String(error);
107
- errorMeta =
108
- typeof error === 'object' && error !== null
109
- ? {
110
- name: error.name,
111
- code: error.code,
112
- llmCode: error.llmCode,
113
- }
114
- : undefined;
115
- throw error;
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
- finally {
118
- if (emit && isPhase(name) && phaseStarted) {
119
- emit({
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: end - start,
145
- error: errorStr,
146
- metadata: errorMeta,
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
- * Add a step with error recovery
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
- stepWithRecovery(name, action, recovery) {
156
- const nextPromise = this.promise.then(async (ctx) => {
157
- const start = Date.now();
158
- let phaseStarted = false;
159
- let abortedBeforeAction = false;
160
- let errorStr;
161
- let errorMeta;
162
- let result;
163
- const emit = ctx.emit;
164
- const isPhase = (value) => EXECUTION_PHASES.includes(value);
165
- const planRuntime = ctx?.planRuntime;
166
- const persistenceRoot = ctx?.workspace?.baseRepoPath || ctx?.workspace?.workPath;
167
- const attempt = ctx?.attempt ?? 1;
168
- const tryAppendPlanNote = async (note) => {
169
- if (!planRuntime || !persistenceRoot)
170
- return;
171
- try {
172
- await appendPlanNote({
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
- this.ctxRef.current = ctx;
189
- const signal = ctx?.options?.signal;
190
- const strategy = ctx?.workspace?.strategy ?? ctx?.options?.strategy;
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 (error) {
217
- errorStr = error instanceof Error ? error.message : String(error);
218
- errorMeta =
219
- typeof error === 'object' && error !== null
220
- ? {
221
- name: error.name,
222
- code: error.code,
223
- llmCode: error.llmCode,
224
- }
225
- : undefined;
226
- if (abortedBeforeAction) {
227
- throw error;
228
- }
229
- // Trigger Recovery
230
- const recStart = Date.now();
231
- try {
232
- await recovery(ctx);
233
- this.traces.push({
234
- name: `${name}:recovery`,
235
- start: recStart,
236
- end: Date.now(),
237
- duration: Date.now() - recStart,
238
- metadata: { success: true },
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
- catch (recError) {
242
- const recEnd = Date.now();
243
- const errorDetail = recError instanceof Error ? recError.message : String(recError);
244
- // 1. Record recovery failure to internal traces
245
- this.traces.push({
246
- name: `${name}:recovery`,
247
- start: recStart,
248
- end: recEnd,
249
- duration: recEnd - recStart,
250
- error: errorDetail,
251
- metadata: { success: false, phase: 'RECOVERY_FAILURE' },
252
- });
253
- // 2. Force audit log to disk (persistent storage)
254
- getLogger().audit('PIPELINE_RECOVERY_FAILED', {
255
- step: name,
256
- originalError: errorStr,
257
- recoveryError: errorDetail,
258
- }, { source: 'system', severity: 'high', scope: 'session' });
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
- finally {
263
- if (emit && isPhase(name) && phaseStarted) {
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: 'phase.end',
184
+ type: 'plan.runtime.journal',
185
+ sessionId: planRuntime.sessionId,
266
186
  phase: name,
267
- success: !errorStr,
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
- return new Pipeline(nextPromise, this.startTime, name, this.traces, this.ctxRef);
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 || typeof error !== 'object')
53
+ if (!isRecord(error))
53
54
  return undefined;
54
55
  const value = 'inputRequired' in error ? error.inputRequired : error;
55
- if (!value || typeof value !== 'object')
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 || typeof error !== 'object')
63
+ if (!isRecord(error))
63
64
  return undefined;
64
65
  const value = error.interrupt;
65
- if (!value || typeof value !== 'object')
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?.code ?? error?.llmCode ?? ''}`,
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 = error instanceof Error ? error.message : String(error);
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 = error instanceof Error ? error.message : String(error);
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
- ? { name: 'UnknownError', message: String(report.error), stack: undefined }
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 = error instanceof Error ? error.message : String(error);
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 = error instanceof Error ? error.message : String(error);
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 || typeof verifyResult !== 'object')
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 typedEntry = entry;
299
- const keepArgsPreview = typedEntry.toolResultErrorCode === 'INVALID_INPUT';
310
+ const keepArgsPreview = entry.toolResultErrorCode === 'INVALID_INPUT';
300
311
  if (keepArgsPreview)
301
312
  return entry;
302
- const { rawArgsPreview: _rawArgsPreview, parsedArgsPreview: _parsedArgsPreview, toolResultErrorMessage: _toolResultErrorMessage, ...rest } = typedEntry;
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) {