salmon-loop 0.3.2 → 0.5.0

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 (227) hide show
  1. package/dist/cli/authorization/non-interactive.js +9 -13
  2. package/dist/cli/authorization/provider.js +2 -10
  3. package/dist/cli/chat.js +12 -6
  4. package/dist/cli/commands/allowlist.js +1 -1
  5. package/dist/cli/commands/chat.js +13 -13
  6. package/dist/cli/commands/config.js +2 -2
  7. package/dist/cli/commands/mode.js +2 -2
  8. package/dist/cli/commands/parallel.js +1 -1
  9. package/dist/cli/commands/run/handler.js +9 -4
  10. package/dist/cli/commands/run/loop-params.js +2 -0
  11. package/dist/cli/commands/run/parse-options.js +14 -26
  12. package/dist/cli/commands/run/runtime-llm.js +15 -12
  13. package/dist/cli/commands/run/runtime-options.js +3 -1
  14. package/dist/cli/config.js +0 -8
  15. package/dist/cli/headless/openai-responses-canonical-applier.js +1 -7
  16. package/dist/cli/locales/en.js +2 -2
  17. package/dist/cli/reporters/standard.js +12 -3
  18. package/dist/cli/reporters/stream-json.js +2 -1
  19. package/dist/cli/slash/runtime.js +2 -2
  20. package/dist/cli/ui/hooks/useLoopEvents.js +1 -1
  21. package/dist/cli/ui/hooks/useLoopState.js +1 -1
  22. package/dist/core/adapters/fs/file-adapter.js +3 -1
  23. package/dist/core/adapters/git/git-adapter.js +6 -3
  24. package/dist/core/adapters/git/git-runner.js +5 -2
  25. package/dist/core/adapters/git/lock-manager.js +7 -4
  26. package/dist/core/ast/parser.js +18 -9
  27. package/dist/core/checkpoint-domain/manifest-store.js +21 -13
  28. package/dist/core/checkpoint-domain/service.js +3 -1
  29. package/dist/core/config/limits.js +1 -1
  30. package/dist/core/config/model-pricing.js +61 -0
  31. package/dist/core/config/schema.js +738 -0
  32. package/dist/core/config/validate.js +11 -922
  33. package/dist/core/context/ast/skeleton-extractor.js +225 -0
  34. package/dist/core/context/ast/source-outline.js +24 -1
  35. package/dist/core/context/budget/dynamic-adjuster.js +20 -5
  36. package/dist/core/context/builder.js +7 -3
  37. package/dist/core/context/cache/store-factory.js +3 -1
  38. package/dist/core/context/dependencies.js +2 -1
  39. package/dist/core/context/effectiveness/persistence.js +50 -0
  40. package/dist/core/context/effectiveness/tracker.js +24 -0
  41. package/dist/core/context/gatherers/architecture-gatherer.js +2 -1
  42. package/dist/core/context/gatherers/artifact-gatherer.js +7 -4
  43. package/dist/core/context/gatherers/ast-gatherer.js +34 -40
  44. package/dist/core/context/gatherers/ghost-dependency-gatherer.js +0 -1
  45. package/dist/core/context/gatherers/git-history-gatherer.js +3 -1
  46. package/dist/core/context/gatherers/knowledge-gatherer.js +21 -2
  47. package/dist/core/context/gatherers/metadata-gatherer.js +12 -7
  48. package/dist/core/context/gatherers/ripgrep-gatherer.js +6 -3
  49. package/dist/core/context/service.js +12 -2
  50. package/dist/core/context/steps/context-gather.js +14 -3
  51. package/dist/core/context/steps/context-targets.js +1 -0
  52. package/dist/core/context/targeting/target-resolver.js +29 -11
  53. package/dist/core/context/token/cache.js +5 -2
  54. package/dist/core/context/token/encoding-registry.js +7 -6
  55. package/dist/core/context/truncation/strategies/json.js +5 -2
  56. package/dist/core/context/truncation/type-detector.js +3 -1
  57. package/dist/core/extensions/index.js +48 -3
  58. package/dist/core/extensions/load.js +3 -2
  59. package/dist/core/extensions/merge.js +5 -1
  60. package/dist/core/extensions/paths.js +8 -2
  61. package/dist/core/extensions/schemas.js +21 -0
  62. package/dist/core/facades/cli-authorization-provider.js +1 -0
  63. package/dist/core/facades/cli-command-chat.js +2 -0
  64. package/dist/core/facades/cli-run-handler.js +1 -0
  65. package/dist/core/facades/cli-utils-serialize.js +2 -0
  66. package/dist/core/feedback/parsers.js +290 -1
  67. package/dist/core/grizzco/dsl/llm-strategy.js +4 -3
  68. package/dist/core/grizzco/engine/observability/loop-telemetry.js +5 -2
  69. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +30 -13
  70. package/dist/core/grizzco/engine/pipeline/pipeline.js +149 -240
  71. package/dist/core/grizzco/engine/transaction/attempt-failure.js +49 -24
  72. package/dist/core/grizzco/engine/transaction/authorization-summary.js +2 -1
  73. package/dist/core/grizzco/engine/transaction/transaction-runner.js +40 -34
  74. package/dist/core/grizzco/execution/RejectionManager.js +7 -5
  75. package/dist/core/grizzco/runtime/apply-back-runtime.js +5 -2
  76. package/dist/core/grizzco/services/implementations/default/GitConfigService.js +2 -1
  77. package/dist/core/grizzco/services/registry.js +18 -0
  78. package/dist/core/grizzco/steps/audit.js +20 -10
  79. package/dist/core/grizzco/steps/autopilot.js +21 -32
  80. package/dist/core/grizzco/steps/display-report.js +4 -11
  81. package/dist/core/grizzco/steps/explore.js +14 -4
  82. package/dist/core/grizzco/steps/generateReview.js +3 -1
  83. package/dist/core/grizzco/steps/patch/prompt-input.js +4 -1
  84. package/dist/core/grizzco/steps/patch.js +1 -0
  85. package/dist/core/grizzco/steps/plan.js +58 -49
  86. package/dist/core/grizzco/steps/research.js +3 -1
  87. package/dist/core/grizzco/steps/tool-runtime.js +3 -0
  88. package/dist/core/grizzco/steps/verify.js +7 -1
  89. package/dist/core/grizzco/validation/AstValidationService.js +3 -1
  90. package/dist/core/grizzco/workers/strata-sync-worker.js +2 -1
  91. package/dist/core/history/input-history.js +3 -1
  92. package/dist/core/intent/chat-intent.js +3 -1
  93. package/dist/core/llm/ai-sdk/message-mapper.js +37 -26
  94. package/dist/core/llm/ai-sdk/request-params.js +2 -6
  95. package/dist/core/llm/ai-sdk/result-mapper.js +14 -8
  96. package/dist/core/llm/ai-sdk/retry-classifier.js +17 -7
  97. package/dist/core/llm/ai-sdk/retry-executor.js +1 -1
  98. package/dist/core/llm/contracts/repair.js +16 -8
  99. package/dist/core/llm/errors.js +18 -14
  100. package/dist/core/llm/output-policy.js +8 -0
  101. package/dist/core/llm/redact.js +1 -3
  102. package/dist/core/llm/retry-utils.js +8 -2
  103. package/dist/core/llm/stream-utils.js +5 -3
  104. package/dist/core/llm/sub-agent-factory.js +51 -0
  105. package/dist/core/llm/tool-calling-stub.js +48 -0
  106. package/dist/core/llm/utils.js +17 -6
  107. package/dist/core/mcp/bridge/prompt-command-provider.js +4 -3
  108. package/dist/core/mcp/bridge/resource-context-provider.js +3 -1
  109. package/dist/core/mcp/bridge/tool-bridge.js +5 -14
  110. package/dist/core/mcp/catalog/discovery.js +3 -1
  111. package/dist/core/mcp/client/connection-manager.js +7 -4
  112. package/dist/core/mcp/client/transport-factory.js +7 -3
  113. package/dist/core/mcp/host/sampling-provider.js +1 -1
  114. package/dist/core/mcp/schema/json-schema-to-zod.js +2 -1
  115. package/dist/core/memory/relevant-retrieval.js +6 -4
  116. package/dist/core/observability/audit-file.js +2 -1
  117. package/dist/core/observability/audit-trail.js +3 -1
  118. package/dist/core/observability/authorization-decisions.js +13 -12
  119. package/dist/core/observability/error-mapping.js +2 -1
  120. package/dist/core/observability/logger.js +2 -1
  121. package/dist/core/observability/monitor.js +24 -0
  122. package/dist/core/observability/run-outcome-reporter.js +1 -0
  123. package/dist/core/observability/token-usage.js +5 -4
  124. package/dist/core/permission-gate/default-gate.js +5 -8
  125. package/dist/core/plan/storage.js +7 -4
  126. package/dist/core/plugin/loader.js +8 -5
  127. package/dist/core/prompts/registry.js +12 -30
  128. package/dist/core/prompts/runtime.js +3 -1
  129. package/dist/core/prompts/templates/system/autopilot_system.hbs +28 -4
  130. package/dist/core/protocols/a2a/sdk/executor.js +3 -1
  131. package/dist/core/protocols/a2a/sdk/server.js +5 -4
  132. package/dist/core/protocols/acp/acp-command-runner.js +7 -6
  133. package/dist/core/protocols/acp/acp-session-persistence.js +13 -10
  134. package/dist/core/protocols/acp/formal-agent.js +13 -6
  135. package/dist/core/protocols/acp/permission-provider.js +3 -2
  136. package/dist/core/protocols/acp/stdio-server.js +6 -6
  137. package/dist/core/reflection/engine.js +114 -14
  138. package/dist/core/runtime/agent-server-runtime.js +3 -2
  139. package/dist/core/runtime/batch-runner.js +81 -0
  140. package/dist/core/runtime/initialize.js +71 -6
  141. package/dist/core/runtime/loop-finalize.js +3 -0
  142. package/dist/core/runtime/loop-session-runner.js +5 -0
  143. package/dist/core/runtime/loop.js +4 -0
  144. package/dist/core/runtime/paths.js +9 -6
  145. package/dist/core/runtime/spawn-interactive.js +5 -4
  146. package/dist/core/security/redaction.js +3 -2
  147. package/dist/core/session/compaction/index.js +4 -3
  148. package/dist/core/session/compression.js +3 -1
  149. package/dist/core/session/manager.js +26 -38
  150. package/dist/core/session/pruning-strategy.js +2 -1
  151. package/dist/core/session/token-tracker.js +27 -9
  152. package/dist/core/skills/parser.js +3 -2
  153. package/dist/core/skills/permissions.js +2 -2
  154. package/dist/core/skills/runtime/MicroTaskRunner.js +1 -1
  155. package/dist/core/skills/runtime/SkillRunner.js +5 -2
  156. package/dist/core/slash/steps/slash-execute.js +7 -5
  157. package/dist/core/slash/strategy.js +1 -1
  158. package/dist/core/strata/checkpoint/manager.js +16 -10
  159. package/dist/core/strata/checkpoint/snapshot-create.js +5 -4
  160. package/dist/core/strata/checkpoint/snapshot-write-tree.js +7 -3
  161. package/dist/core/strata/engine/shadow-merge-engine.js +4 -2
  162. package/dist/core/strata/interaction/file-system-provider.js +2 -1
  163. package/dist/core/strata/layers/file-state-resolver.js +9 -7
  164. package/dist/core/strata/layers/immutable-git-layer.js +3 -1
  165. package/dist/core/strata/layers/shadow-driver/readonly-lock.js +8 -6
  166. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +2 -1
  167. package/dist/core/strata/layers/worktree.js +9 -10
  168. package/dist/core/strata/runtime/environment.js +2 -1
  169. package/dist/core/strata/runtime/synchronizer.js +28 -26
  170. package/dist/core/streaming/canonical/parts-from-llm-stream-chunk.js +1 -11
  171. package/dist/core/structured-output/json-extract.js +3 -1
  172. package/dist/core/structured-output/json-schema-validator.js +1 -13
  173. package/dist/core/sub-agent/artifacts/store.js +2 -1
  174. package/dist/core/sub-agent/context-snapshot.js +12 -6
  175. package/dist/core/sub-agent/controller.js +70 -1
  176. package/dist/core/sub-agent/core/loop.js +25 -3
  177. package/dist/core/sub-agent/core/manager.js +343 -117
  178. package/dist/core/sub-agent/registry-defaults.js +12 -0
  179. package/dist/core/sub-agent/registry.js +8 -0
  180. package/dist/core/sub-agent/summary.js +96 -0
  181. package/dist/core/sub-agent/team.js +98 -0
  182. package/dist/core/sub-agent/tools/task-await.js +109 -0
  183. package/dist/core/sub-agent/tools/task-spawn.js +52 -7
  184. package/dist/core/sub-agent/tools/team.js +92 -0
  185. package/dist/core/sub-agent/types.js +11 -2
  186. package/dist/core/target-runtime/profile.js +3 -1
  187. package/dist/core/tools/audit.js +3 -2
  188. package/dist/core/tools/budget.js +7 -12
  189. package/dist/core/tools/builtin/ast.js +144 -0
  190. package/dist/core/tools/builtin/code-search/backends/powershell.js +3 -1
  191. package/dist/core/tools/builtin/code-search/backends/rg.js +3 -1
  192. package/dist/core/tools/builtin/code-search/executor.js +46 -43
  193. package/dist/core/tools/builtin/code-search/parse/plain-grep.js +3 -1
  194. package/dist/core/tools/builtin/code-search/parse/rg-json.js +3 -1
  195. package/dist/core/tools/builtin/fs.js +90 -7
  196. package/dist/core/tools/builtin/git.js +242 -0
  197. package/dist/core/tools/builtin/glob.js +79 -0
  198. package/dist/core/tools/builtin/index.js +53 -111
  199. package/dist/core/tools/builtin/interaction.js +13 -15
  200. package/dist/core/tools/builtin/knowledge.js +146 -4
  201. package/dist/core/tools/builtin/proposal.js +14 -3
  202. package/dist/core/tools/builtin/verify.js +35 -3
  203. package/dist/core/tools/capability/executor.js +5 -5
  204. package/dist/core/tools/headless-payload.js +1 -3
  205. package/dist/core/tools/mapper.js +8 -42
  206. package/dist/core/tools/parallel/persistence.js +17 -5
  207. package/dist/core/tools/parallel/scheduler.js +23 -21
  208. package/dist/core/tools/permissions/permission-rules.js +69 -115
  209. package/dist/core/tools/plugins/loader.js +4 -3
  210. package/dist/core/tools/router.js +112 -58
  211. package/dist/core/tools/session.js +64 -102
  212. package/dist/core/tools/streaming/ToolCallAccumulator.js +1 -3
  213. package/dist/core/tools/tool-visibility.js +2 -1
  214. package/dist/core/tools/types.js +10 -0
  215. package/dist/core/types/batch.js +2 -0
  216. package/dist/core/utils/error.js +79 -0
  217. package/dist/core/utils/sanitizer.js +5 -2
  218. package/dist/core/utils/serialize.js +66 -0
  219. package/dist/core/utils/zod.js +29 -0
  220. package/dist/core/verification/detect-runner.js +86 -0
  221. package/dist/core/verification/runner.js +76 -0
  222. package/dist/core/version.js +3 -1
  223. package/dist/core/workspace/capabilities.js +3 -2
  224. package/dist/integrations/langfuse/litellm-langfuse-outcome-reporter.js +9 -8
  225. package/dist/languages/python/index.js +154 -0
  226. package/dist/locales/en.js +8 -1
  227. package/package.json +2 -1
@@ -133,7 +133,10 @@ export const generatePlan = async (ctx) => {
133
133
  flowMode: ctx.mode,
134
134
  runtime: toolVisibility,
135
135
  });
136
- const systemPrompt = await getPlanSystemPrompt(promptVisibleTools, toolVisibility);
136
+ const baseSystemPrompt = await getPlanSystemPrompt(promptVisibleTools, toolVisibility);
137
+ const systemPrompt = ctx.options.subAgentSystemPrompt
138
+ ? [baseSystemPrompt, ctx.options.subAgentSystemPrompt]
139
+ : baseSystemPrompt;
137
140
  const requestEnvelope = await buildPhaseRequestEnvelope({
138
141
  phase: Phase.PLAN,
139
142
  defaultNamespace: 'plan',
@@ -192,62 +195,68 @@ export const generatePlan = async (ctx) => {
192
195
  });
193
196
  const content = response.content;
194
197
  let finalContent = content || '';
198
+ // Codex-style self-healing: if plan JSON parsing fails, send the error back to the LLM
199
+ // and let it self-correct. Try up to MAX_REPAIR_ATTEMPTS times.
200
+ const MAX_REPAIR_ATTEMPTS = 2;
195
201
  let plan;
196
- try {
197
- if (!finalContent) {
198
- throw new Error(text.llm.planEmpty);
199
- }
200
- plan = parsePlanFromLLMContent(finalContent);
201
- }
202
- catch (e) {
203
- recordPlanRepairAttempt({
204
- reason: sanitizeError(e),
205
- badContentLength: finalContent.length,
206
- });
207
- let repaired;
208
- try {
209
- repaired = await repairToJsonObject({
210
- llm: ctx.options.llm,
211
- baseMessages,
212
- chatOptions: { signal: ctx.options.signal },
213
- badContent: finalContent,
214
- reason: sanitizeError(e),
215
- });
216
- }
217
- catch (repairError) {
218
- recordPlanRepairResult({
219
- ok: false,
220
- contentLength: 0,
221
- error: sanitizeError(repairError).slice(0, 400),
222
- });
223
- throw new Error(text.llm.planParseFailed(finalContent, sanitizeError(repairError)));
224
- }
225
- finalContent = repaired.content || '';
226
- emitLlmOutput({
227
- emit: ctx.emit,
228
- policy: ctx.options.llmOutput,
229
- kind: 'plan',
230
- step: 'PLAN',
231
- content: finalContent,
232
- });
202
+ for (let attempt = 0; attempt <= MAX_REPAIR_ATTEMPTS; attempt++) {
233
203
  try {
234
- if (!finalContent)
204
+ if (!finalContent) {
235
205
  throw new Error(text.llm.planEmpty);
206
+ }
236
207
  plan = parsePlanFromLLMContent(finalContent);
208
+ break; // Success
237
209
  }
238
- catch (e2) {
239
- recordPlanRepairResult({
240
- ok: false,
241
- contentLength: finalContent.length,
242
- error: sanitizeError(e2).slice(0, 400),
210
+ catch (parseError) {
211
+ // On last attempt, throw
212
+ if (attempt >= MAX_REPAIR_ATTEMPTS) {
213
+ recordPlanRepairResult({
214
+ ok: false,
215
+ contentLength: finalContent.length,
216
+ error: sanitizeError(parseError).slice(0, 400),
217
+ });
218
+ throw new Error(text.llm.planParseFailed(finalContent, sanitizeError(parseError)));
219
+ }
220
+ // Repair attempt: send error feedback to LLM and let it self-correct
221
+ recordPlanRepairAttempt({
222
+ reason: sanitizeError(parseError),
223
+ badContentLength: finalContent.length,
224
+ });
225
+ let repaired;
226
+ try {
227
+ repaired = await repairToJsonObject({
228
+ llm: ctx.options.llm,
229
+ baseMessages,
230
+ chatOptions: { signal: ctx.options.signal },
231
+ badContent: finalContent,
232
+ reason: sanitizeError(parseError),
233
+ });
234
+ }
235
+ catch (repairError) {
236
+ recordPlanRepairResult({
237
+ ok: false,
238
+ contentLength: 0,
239
+ error: sanitizeError(repairError).slice(0, 400),
240
+ });
241
+ throw new Error(text.llm.planParseFailed(finalContent, sanitizeError(repairError)));
242
+ }
243
+ finalContent = repaired.content || '';
244
+ emitLlmOutput({
245
+ emit: ctx.emit,
246
+ policy: ctx.options.llmOutput,
247
+ kind: 'plan',
248
+ step: 'PLAN',
249
+ content: finalContent,
243
250
  });
244
- throw new Error(text.llm.planParseFailed(finalContent, sanitizeError(e2)));
245
251
  }
246
- recordPlanRepairResult({
247
- ok: true,
248
- contentLength: finalContent.length,
249
- });
250
252
  }
253
+ if (!plan) {
254
+ throw new Error(text.llm.planParseFailed(finalContent, 'Plan was not parsed successfully'));
255
+ }
256
+ recordPlanRepairResult({
257
+ ok: true,
258
+ contentLength: finalContent.length,
259
+ });
251
260
  ctx.emit({
252
261
  type: 'log',
253
262
  level: 'debug',
@@ -2,6 +2,7 @@ import { text } from '../../../locales/index.js';
2
2
  import { supportsLlmStreaming } from '../../llm/capabilities.js';
3
3
  import { emitLlmOutput } from '../../llm/output-policy.js';
4
4
  import { recordAuditEvent } from '../../observability/audit-trail.js';
5
+ import { getLogger } from '../../observability/logger.js';
5
6
  import { getResearchPrompt, getResearchSystemPrompt } from '../../prompts/runtime.js';
6
7
  import { SessionReplacementPreviewProvider } from '../../session/replacement-preview-provider.js';
7
8
  import { chatWithTools, chatWithToolsStreaming } from '../../tools/session.js';
@@ -50,7 +51,8 @@ function parseResearchResponse(content, fallbackSources) {
50
51
  try {
51
52
  parsed = JSON.parse(content);
52
53
  }
53
- catch {
54
+ catch (error) {
55
+ getLogger().debug(`[Research] Failed to parse research response as JSON: ${error instanceof Error ? error.message : String(error)}`);
54
56
  parsed = undefined;
55
57
  }
56
58
  const researchText = String(parsed?.researchText ?? content ?? text.grizzco.research.empty ?? '');
@@ -14,6 +14,9 @@ export function buildPhaseToolRuntimeContext(ctx, phase, cacheSurface) {
14
14
  agentKind: ctx.options.agentKind ?? 'primary',
15
15
  languagePlugins: ctx.options.languagePlugins,
16
16
  subAgentController: ctx.options.subAgentController,
17
+ llmFactory: ctx.options.llmFactory,
18
+ onSubAgentComplete: ctx.options.onSubAgentComplete,
19
+ agentId: ctx.options.agentId,
17
20
  phase,
18
21
  contextSnapshot: {
19
22
  conversationContext: ctx.options.conversationContext,
@@ -1,5 +1,6 @@
1
1
  import { text } from '../../../locales/index.js';
2
2
  import { collectBudgetMetrics, evaluateBudgetAlert, getGlobalAdjuster, recordBudgetAlert, } from '../../context/budget/integration.js';
3
+ import { getEffectivenessTracker } from '../../context/effectiveness/tracker.js';
3
4
  import { recordAuditEvent } from '../../observability/audit-trail.js';
4
5
  import { executeVerifyForWorkspace } from './verify-shared.js';
5
6
  function extractCommandProgram(command) {
@@ -32,7 +33,12 @@ export const runVerify = async (ctx) => {
32
33
  scope: 'session',
33
34
  phase: 'VERIFY',
34
35
  });
35
- // Collect budget metrics after verification
36
+ // Collect budget metrics and effectiveness data after verification
37
+ const effectivenessTracker = getEffectivenessTracker();
38
+ effectivenessTracker.recordExecution(verifyResult.ok, 0);
39
+ if (!verifyResult.ok) {
40
+ effectivenessTracker.recordFailure('missing_context', `Verification failed: ${verifyResult.output?.slice(0, 200) ?? 'unknown'}`);
41
+ }
36
42
  if (ctx.contextResult) {
37
43
  const metrics = collectBudgetMetrics({
38
44
  contextResult: ctx.contextResult,
@@ -4,6 +4,7 @@ import path from 'path';
4
4
  import { readFile, rm } from '../../adapters/fs/node-fs.js';
5
5
  import { GitAdapter } from '../../adapters/git/git-adapter.js';
6
6
  import { AstParser } from '../../ast/index.js';
7
+ import { getLogger } from '../../observability/logger.js';
7
8
  import { convertDiffToShadowOperations } from '../../patch/diff.js';
8
9
  import { tryGetPluginRegistry } from '../../plugin/registry.js';
9
10
  import { OpType } from '../domain/grizzco-types.js';
@@ -70,7 +71,8 @@ async function defaultBuildProposedSource(workPath, operation) {
70
71
  return null;
71
72
  return showResult.stdout.toString('utf8');
72
73
  }
73
- catch {
74
+ catch (error) {
75
+ getLogger().debug(`[AstValidationService] Failed to build proposed source for "${operation.path}": ${error instanceof Error ? error.message : String(error)}`);
74
76
  return null;
75
77
  }
76
78
  finally {
@@ -44,7 +44,8 @@ export class StrataSyncWorker {
44
44
  const aiContent = op.content;
45
45
  // Invoke legacy merge logic
46
46
  // private method: async mergeFileContents(repoPath, base, user, ai, options?)
47
- const result = await engine.mergeFileContents(this.git.repoPath, baseContent, userContent, aiContent);
47
+ const engineMethods = engine;
48
+ const result = await engineMethods.mergeFileContents(this.git.repoPath, baseContent, userContent, aiContent);
48
49
  return {
49
50
  path: state.path,
50
51
  success: !result.conflict,
@@ -1,5 +1,6 @@
1
1
  import { join } from 'path';
2
2
  import { FileAdapter } from '../adapters/fs/index.js';
3
+ import { getLogger } from '../observability/logger.js';
3
4
  /**
4
5
  * Manages persistence of user input history isolated by Session.
5
6
  * Storage path: .salmonloop/ui-history/{sessionId}.json
@@ -26,7 +27,8 @@ export class InputHistoryManager {
26
27
  const data = await this.fileAdapter.readFile(filePath);
27
28
  return JSON.parse(data);
28
29
  }
29
- catch {
30
+ catch (error) {
31
+ getLogger().debug(`[InputHistory] Failed to load history for session "${sessionId}": ${error instanceof Error ? error.message : String(error)}`);
30
32
  return [];
31
33
  }
32
34
  }
@@ -1,4 +1,5 @@
1
1
  import { z } from 'zod';
2
+ import { getLogger } from '../observability/logger.js';
2
3
  const LlmDecisionSchema = z
3
4
  .object({
4
5
  intent: z.enum(['answer', 'review', 'patch', 'debug', 'research']),
@@ -165,8 +166,9 @@ async function routeByLlm(input, options) {
165
166
  try {
166
167
  return JSON.parse(candidate);
167
168
  }
168
- catch {
169
+ catch (error) {
169
170
  // Best-effort: extract the first JSON object from mixed output.
171
+ getLogger().debug(`[ChatIntent] Primary JSON parse failed, attempting extraction: ${error instanceof Error ? error.message : String(error)}`);
170
172
  const start = candidate.indexOf('{');
171
173
  const end = candidate.lastIndexOf('}');
172
174
  if (start >= 0 && end > start) {
@@ -1,12 +1,14 @@
1
1
  import { jsonSchema, tool } from 'ai';
2
2
  import { z } from 'zod';
3
3
  import { zodToJsonSchema } from 'zod-to-json-schema';
4
+ import { getLogger } from '../../observability/logger.js';
4
5
  import { toolToOpenAI } from '../../tools/mapper.js';
6
+ import { isRecord } from '../../utils/serialize.js';
5
7
  function formatOutputSchema(schema) {
6
8
  if (!schema)
7
9
  return 'any (dynamic)';
8
- const def = schema._def;
9
- if (def?.description) {
10
+ const def = schema.def;
11
+ if (typeof def?.description === 'string') {
10
12
  return def.description;
11
13
  }
12
14
  try {
@@ -19,8 +21,9 @@ function formatOutputSchema(schema) {
19
21
  return JSON.stringify(cleanSchema);
20
22
  }
21
23
  }
22
- catch {
24
+ catch (error) {
23
25
  // Fallback to generic description for invalid/unsupported schema.
26
+ getLogger().debug(`[MessageMapper] Failed to format output schema to JSON: ${error instanceof Error ? error.message : String(error)}`);
24
27
  }
25
28
  return 'complex object';
26
29
  }
@@ -30,8 +33,8 @@ function safeParseJsonObject(textValue) {
30
33
  if (parsed && typeof parsed === 'object' && !Array.isArray(parsed))
31
34
  return parsed;
32
35
  }
33
- catch {
34
- // ignored
36
+ catch (error) {
37
+ getLogger().debug(`[MessageMapper] Failed to parse JSON object: ${error instanceof Error ? error.message : String(error)}`);
35
38
  }
36
39
  return {};
37
40
  }
@@ -42,13 +45,12 @@ function deepCloneJson(value, fallback) {
42
45
  return fallback;
43
46
  return JSON.parse(serialized);
44
47
  }
45
- catch {
48
+ catch (error) {
49
+ getLogger().debug(`[MessageMapper] deepCloneJson failed: ${error instanceof Error ? error.message : String(error)}`);
46
50
  return fallback;
47
51
  }
48
52
  }
49
- function isObjectRecord(value) {
50
- return typeof value === 'object' && value !== null && !Array.isArray(value);
51
- }
53
+ const isObjectRecord = isRecord;
52
54
  export function extractUsageFromAiSdkResult(result) {
53
55
  if (!isObjectRecord(result))
54
56
  return null;
@@ -93,7 +95,9 @@ function toAiSdkToolResultOutput(value) {
93
95
  };
94
96
  }
95
97
  export function toAiSdkMessages(messages) {
96
- return messages.map((m) => {
98
+ // Each branch returns a structurally valid ModelMessage; the union is too
99
+ // complex for TS to verify inline, so we assert the array at the end.
100
+ const result = messages.map((m) => {
97
101
  if (m.role === 'tool') {
98
102
  const toolCallId = m.tool_call_id || 'unknown';
99
103
  const toolName = m.name || 'unknown';
@@ -101,7 +105,8 @@ export function toAiSdkMessages(messages) {
101
105
  try {
102
106
  parsedContent = JSON.parse(m.content);
103
107
  }
104
- catch {
108
+ catch (error) {
109
+ getLogger().debug(`[MessageMapper] Failed to parse tool message content as JSON: ${error instanceof Error ? error.message : String(error)}`);
105
110
  parsedContent = m.content;
106
111
  }
107
112
  if (isToolApprovalResponse(parsedContent)) {
@@ -143,7 +148,7 @@ export function toAiSdkMessages(messages) {
143
148
  content = JSON.stringify(content);
144
149
  }
145
150
  return {
146
- role: m.role,
151
+ role: 'assistant',
147
152
  content: content,
148
153
  };
149
154
  }
@@ -191,6 +196,7 @@ export function toAiSdkMessages(messages) {
191
196
  content: content,
192
197
  };
193
198
  });
199
+ return result;
194
200
  }
195
201
  export function toAiSdkToolSet(openAiTools, toolSpecs) {
196
202
  const tools = {};
@@ -199,12 +205,14 @@ export function toAiSdkToolSet(openAiTools, toolSpecs) {
199
205
  const outputDesc = formatOutputSchema(spec.outputSchema);
200
206
  const description = `${spec.description}\n\nReturns: ${outputDesc}`;
201
207
  const openAiDef = toolToOpenAI(spec);
202
- const parameters = jsonSchema(openAiDef.function?.parameters || {});
203
- tools[spec.name] = tool({
204
- description,
205
- parameters,
206
- });
207
- tools[spec.name].outputSchema = spec.outputSchema || z.any();
208
+ const parameters = jsonSchema(openAiDef.function?.parameters ?? {});
209
+ tools[spec.name] = {
210
+ ...tool({
211
+ description,
212
+ inputSchema: parameters,
213
+ }),
214
+ outputSchema: spec.outputSchema ?? z.any(),
215
+ };
208
216
  }
209
217
  }
210
218
  if (Array.isArray(openAiTools)) {
@@ -215,11 +223,13 @@ export function toAiSdkToolSet(openAiTools, toolSpecs) {
215
223
  continue;
216
224
  const rawDesc = typeof fn?.description === 'string' ? fn.description : '';
217
225
  const description = `${rawDesc}\n\nReturns: any (dynamic)`.trim();
218
- tools[name] = tool({
219
- description,
220
- parameters: jsonSchema(fn?.parameters || { type: 'object', properties: {} }),
221
- });
222
- tools[name].outputSchema = z.any();
226
+ tools[name] = {
227
+ ...tool({
228
+ description,
229
+ inputSchema: jsonSchema(fn?.parameters ?? { type: 'object', properties: {} }),
230
+ }),
231
+ outputSchema: z.any(),
232
+ };
223
233
  }
224
234
  }
225
235
  return Object.keys(tools).length > 0 ? tools : undefined;
@@ -241,14 +251,15 @@ export function toOpenAiToolCalls(toolCalls) {
241
251
  try {
242
252
  parsed = JSON.parse(nested);
243
253
  }
244
- catch {
245
- // ignored
254
+ catch (error) {
255
+ getLogger().debug(`[MessageMapper] Failed to parse nested JSON string: ${error instanceof Error ? error.message : String(error)}`);
246
256
  }
247
257
  }
248
258
  }
249
259
  return parsed;
250
260
  }
251
- catch {
261
+ catch (error) {
262
+ getLogger().debug(`[MessageMapper] Failed to normalize tool input JSON: ${error instanceof Error ? error.message : String(error)}`);
252
263
  return raw;
253
264
  }
254
265
  };
@@ -1,6 +1,4 @@
1
- function isRecord(value) {
2
- return typeof value === 'object' && value !== null && !Array.isArray(value);
3
- }
1
+ import { isRecord } from '../../utils/serialize.js';
4
2
  function isJsonValue(value) {
5
3
  if (value === null ||
6
4
  typeof value === 'string' ||
@@ -115,9 +113,7 @@ export function buildAiSdkRequestParams(params) {
115
113
  responseFormat: resolveResponseFormat(params.options),
116
114
  toolChoice: (params.options.toolChoice === 'none'
117
115
  ? 'none'
118
- : params.tools
119
- ? 'auto'
120
- : undefined),
116
+ : (params.options.toolChoice ?? (params.tools ? 'auto' : undefined))),
121
117
  providerOptions: mergeProviderOptions({
122
118
  providerOptions: params.options.providerOptions,
123
119
  providerHints: params.options.providerHints,
@@ -1,32 +1,38 @@
1
+ import { isRecord } from '../../utils/serialize.js';
1
2
  import { mapAiSdkStreamPartToChunk } from '../stream-utils.js';
2
3
  import { toOpenAiToolCalls } from './message-mapper.js';
3
4
  function extractReasoningContent(result) {
4
- if (typeof result?.reasoningText === 'string' && result.reasoningText.length > 0) {
5
+ if (!isRecord(result))
6
+ return undefined;
7
+ if (typeof result.reasoningText === 'string' && result.reasoningText.length > 0) {
5
8
  return result.reasoningText;
6
9
  }
7
- const reasoningParts = Array.isArray(result?.reasoning)
10
+ const reasoningParts = Array.isArray(result.reasoning)
8
11
  ? result.reasoning
9
- : Array.isArray(result?.content)
10
- ? result.content.filter((part) => part?.type === 'reasoning')
12
+ : Array.isArray(result.content)
13
+ ? result.content.filter((part) => isRecord(part) && part.type === 'reasoning')
11
14
  : [];
12
15
  const text = reasoningParts
13
- .map((part) => (typeof part?.text === 'string' ? part.text : ''))
16
+ .map((part) => (typeof part.text === 'string' ? part.text : ''))
14
17
  .join('');
15
18
  return text.length > 0 ? text : undefined;
16
19
  }
17
20
  export function mapAiSdkGenerateResultToMessage(result) {
18
21
  const reasoningContent = extractReasoningContent(result);
22
+ const r = isRecord(result) ? result : {};
19
23
  return {
20
24
  role: 'assistant',
21
- content: result?.text || '',
25
+ content: typeof r.text === 'string' ? r.text : '',
22
26
  ...(reasoningContent ? { reasoning_content: reasoningContent } : {}),
23
- tool_calls: toOpenAiToolCalls(result?.toolCalls),
27
+ tool_calls: toOpenAiToolCalls(Array.isArray(r.toolCalls)
28
+ ? r.toolCalls
29
+ : undefined),
24
30
  };
25
31
  }
26
32
  export async function* mapAiSdkStreamResultToChunks(fullStream) {
27
33
  let doneEmitted = false;
28
34
  for await (const part of fullStream) {
29
- if (!part)
35
+ if (!part || !isRecord(part))
30
36
  continue;
31
37
  if (part.type === 'error')
32
38
  throw part.error;
@@ -1,3 +1,4 @@
1
+ import { isRecord } from '../../utils/serialize.js';
1
2
  function unwrapRetryError(err) {
2
3
  if (!err || typeof err !== 'object')
3
4
  return err;
@@ -15,7 +16,7 @@ function findStatusCode(err) {
15
16
  if (typeof direct === 'number' && Number.isFinite(direct))
16
17
  return direct;
17
18
  const response = obj.response;
18
- if (response && typeof response === 'object') {
19
+ if (isRecord(response)) {
19
20
  const status = response.status;
20
21
  if (typeof status === 'number' && Number.isFinite(status))
21
22
  return status;
@@ -31,23 +32,25 @@ function findNetworkCode(err) {
31
32
  if (typeof code === 'string')
32
33
  return code;
33
34
  const cause = obj.cause;
34
- if (cause && typeof cause === 'object' && typeof cause.code === 'string') {
35
+ if (isRecord(cause) && typeof cause.code === 'string') {
35
36
  return cause.code;
36
37
  }
37
38
  return undefined;
38
39
  }
39
- function isAbortLikeError(err) {
40
+ function isUserAbortError(err) {
40
41
  const unwrapped = unwrapRetryError(err);
41
42
  const name = unwrapped instanceof Error ? unwrapped.name : '';
42
- const msg = String(unwrapped?.message ?? unwrapped).toLowerCase();
43
- return name === 'AbortError' || msg.includes('aborted');
43
+ // Only treat as user abort if the error name is AbortError (from AbortController).
44
+ // Provider-side "aborted" messages are transient and should be retried.
45
+ return name === 'AbortError';
44
46
  }
45
47
  export function classifyRetryableApiError(err) {
46
- if (isAbortLikeError(err))
48
+ if (isUserAbortError(err))
47
49
  return { retryable: false, reason: 'aborted' };
48
50
  const statusCode = findStatusCode(err);
49
51
  const networkCode = findNetworkCode(err);
50
- const msg = String(unwrapRetryError(err)?.message ?? err).toLowerCase();
52
+ const unwrapped = unwrapRetryError(err);
53
+ const msg = String((isRecord(unwrapped) ? unwrapped.message : undefined) ?? err).toLowerCase();
51
54
  if (statusCode === 408)
52
55
  return { retryable: true, reason: 'timeout', statusCode, networkCode };
53
56
  if (statusCode === 429)
@@ -67,6 +70,13 @@ export function classifyRetryableApiError(err) {
67
70
  if (msg.includes('overloaded')) {
68
71
  return { retryable: true, reason: 'overloaded', statusCode, networkCode };
69
72
  }
73
+ // Provider-side aborts and unexpected errors are transient — retry them.
74
+ if (msg.includes('aborted')) {
75
+ return { retryable: true, reason: 'provider_abort', statusCode, networkCode };
76
+ }
77
+ if (msg.includes('unexpected error')) {
78
+ return { retryable: true, reason: 'unexpected', statusCode, networkCode };
79
+ }
70
80
  if (typeof networkCode === 'string') {
71
81
  const normalized = networkCode.toUpperCase();
72
82
  if (normalized === 'ECONNRESET' ||
@@ -2,7 +2,7 @@ import { toLlmError } from '../errors.js';
2
2
  import { withRetry, withStreamRetry } from '../retry-utils.js';
3
3
  import { createAiSdkRetryLogger, isRetryableAiSdkError } from './request-runtime.js';
4
4
  const DEFAULT_AI_SDK_RETRY_OPTIONS = {
5
- maxRetries: 2,
5
+ maxRetries: 3,
6
6
  jitterRatio: 0.2,
7
7
  };
8
8
  export async function executeWithAiSdkRetry(params) {
@@ -10,18 +10,26 @@ function truncateForPrompt(text, maxChars) {
10
10
  export async function repairToJsonObject(args) {
11
11
  const { llm, baseMessages, chatOptions, badContent, reason } = args;
12
12
  const prompt = [
13
- 'Your previous response did not satisfy the contract.',
14
- `Reason: ${reason}`,
13
+ 'Your previous response failed to parse as a valid plan JSON object.',
15
14
  '',
16
- 'Return exactly one JSON object and nothing else.',
17
- 'The first non-whitespace character must be {.',
18
- 'The last non-whitespace character must be }.',
15
+ `Specific error: ${reason}`,
19
16
  '',
20
- 'Forbidden: Markdown fences, commentary, labels, multiple objects, or any leading/trailing text.',
17
+ 'To fix this, return EXACTLY one JSON object with these keys:',
18
+ ' - goal: string (what this plan accomplishes)',
19
+ ' - files: string[] (list of file paths to modify)',
20
+ ' - changes: string[] (list of change descriptions)',
21
+ ' - verify: string (command to verify the changes)',
21
22
  '',
22
- 'The JSON object MUST include keys: goal, files, changes, verify.',
23
+ 'Example of a valid response:',
24
+ '{"goal":"Add error handling","files":["src/index.ts"],"changes":["Add try-catch to main"],"verify":"npm test"}',
23
25
  '',
24
- 'Previous response (truncated):',
26
+ 'Rules:',
27
+ '- The first non-whitespace character must be {',
28
+ '- The last non-whitespace character must be }',
29
+ '- No markdown fences, commentary, or leading/trailing text',
30
+ '- All four keys (goal, files, changes, verify) are REQUIRED',
31
+ '',
32
+ 'Your previous invalid response (truncated):',
25
33
  truncateForPrompt(badContent, Math.min(1200, Math.max(400, LIMITS.maxContextChars / 100))),
26
34
  ].join('\n');
27
35
  return llm.chat([
@@ -1,5 +1,7 @@
1
+ import { getLogger } from '../observability/logger.js';
1
2
  import { SalmonError } from '../types/errors.js';
2
3
  import { sanitizeErrorMessage } from '../utils/sanitizer.js';
4
+ import { isRecord } from '../utils/serialize.js';
3
5
  export class LlmError extends SalmonError {
4
6
  llmCode;
5
7
  meta;
@@ -44,8 +46,8 @@ function extractProviderDetails(err) {
44
46
  details.providerMessage = sanitizeError(parsed.message);
45
47
  }
46
48
  }
47
- catch {
48
- // ignore
49
+ catch (error) {
50
+ getLogger().debug(`[LlmErrors] Failed to parse responseBody as JSON: ${error instanceof Error ? error.message : String(error)}`);
49
51
  }
50
52
  }
51
53
  else if (candidate.responseBody) {
@@ -72,8 +74,8 @@ function extractProviderDetails(err) {
72
74
  details.providerMessage = sanitizeError(parsed.error.message);
73
75
  }
74
76
  }
75
- catch {
76
- // ignore
77
+ catch (error) {
78
+ getLogger().debug(`[LlmErrors] Failed to parse embedded JSON in message: ${error instanceof Error ? error.message : String(error)}`);
77
79
  }
78
80
  }
79
81
  }
@@ -88,9 +90,8 @@ function extractNetworkCode(err) {
88
90
  if (typeof direct === 'string' && direct.trim())
89
91
  return direct;
90
92
  const cause = candidate.cause;
91
- if (cause && typeof cause === 'object' && typeof cause.code === 'string') {
92
- const code = String(cause.code);
93
- return code.trim() ? code : undefined;
93
+ if (isRecord(cause) && typeof cause.code === 'string') {
94
+ return cause.code.trim() || undefined;
94
95
  }
95
96
  return undefined;
96
97
  }
@@ -124,19 +125,20 @@ export function sanitizeError(err) {
124
125
  return sanitizeErrorMessage(err);
125
126
  }
126
127
  export function toLlmError(err, provider) {
128
+ const errObj = isRecord(err) ? err : null;
127
129
  let name = err instanceof Error
128
130
  ? err.name
129
- : typeof err?.name === 'string'
130
- ? String(err.name)
131
+ : typeof errObj?.name === 'string'
132
+ ? errObj.name
131
133
  : 'UnknownError';
132
134
  let message = err instanceof Error
133
135
  ? err.message
134
- : typeof err?.message === 'string'
135
- ? String(err.message)
136
+ : typeof errObj?.message === 'string'
137
+ ? errObj.message
136
138
  : String(err);
137
139
  // Unwrap RetryError to get the last error's message if available
138
- if (name === 'AI_RetryError' || err?.lastError) {
139
- const lastError = err.lastError;
140
+ if (name === 'AI_RetryError' || errObj?.lastError) {
141
+ const lastError = errObj?.lastError;
140
142
  // Update the error reference so subsequent checks work on the actual cause
141
143
  err = lastError;
142
144
  if (lastError instanceof Error) {
@@ -149,7 +151,9 @@ export function toLlmError(err, provider) {
149
151
  name === 'ZodError' ||
150
152
  name.includes('TypeValidationError') ||
151
153
  message.includes('TypeValidationError') ||
152
- err?.[Symbol.for('vercel.ai.error.AI_TypeValidationError')]) {
154
+ (err != null &&
155
+ typeof err === 'object' &&
156
+ Symbol.for('vercel.ai.error.AI_TypeValidationError') in err)) {
153
157
  return new LlmError('LLM validation failed', 'LLM_VALIDATION_FAILED', {
154
158
  provider,
155
159
  causeName: name,