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
@@ -8,6 +8,7 @@ import { GitAdapter } from '../../adapters/git/git-adapter.js';
8
8
  import { logIgnoredError } from '../../observability/ignored-error.js';
9
9
  import { getLogger } from '../../observability/logger.js';
10
10
  import { getMonitor } from '../../observability/monitor.js';
11
+ import { errorMessage } from '../../utils/error.js';
11
12
  import { isCanonicalPathWithinDirectory } from '../../utils/path.js';
12
13
  import { detectDependencyPaths } from '../layers/shadow-driver/strategy.js';
13
14
  const SECURITY_BLOCKLIST = [
@@ -56,7 +57,8 @@ export class WorkspaceSynchronizer {
56
57
  try {
57
58
  return await realpath(value);
58
59
  }
59
- catch {
60
+ catch (error) {
61
+ getLogger().debug(`[Synchronizer] realpath failed for ${value}: ${error instanceof Error ? error.message : String(error)}`);
60
62
  return null;
61
63
  }
62
64
  }
@@ -87,7 +89,8 @@ export class WorkspaceSynchronizer {
87
89
  try {
88
90
  entries = (await readdir(tempRoot, { withFileTypes: true }));
89
91
  }
90
- catch {
92
+ catch (error) {
93
+ getLogger().debug(`[Synchronizer] Failed to read tmpdir for backup pruning: ${error instanceof Error ? error.message : String(error)}`);
91
94
  return;
92
95
  }
93
96
  const cutoffTs = Date.now() - retentionMs;
@@ -100,8 +103,8 @@ export class WorkspaceSynchronizer {
100
103
  await rm(backupPath, { recursive: true, force: true });
101
104
  }
102
105
  }
103
- catch {
104
- // Ignore stale cleanup failures; cleanup is best-effort.
106
+ catch (error) {
107
+ getLogger().debug(`[Synchronizer] Failed to prune stale backup ${entry.name}: ${error instanceof Error ? error.message : String(error)}`);
105
108
  }
106
109
  }));
107
110
  }
@@ -135,7 +138,7 @@ export class WorkspaceSynchronizer {
135
138
  detectedDependencyPaths = await detectDependencyPaths(repoPath);
136
139
  }
137
140
  catch (error) {
138
- getLogger().debug(`[checkpoint] Failed to detect dependency paths: ${error instanceof Error ? error.message : String(error)}`);
141
+ getLogger().debug(`[checkpoint] Failed to detect dependency paths: ${errorMessage(error)}`);
139
142
  }
140
143
  const candidates = new Set([
141
144
  ...DEFAULT_DEPENDENCY_ROOT_CANDIDATES,
@@ -159,8 +162,8 @@ export class WorkspaceSynchronizer {
159
162
  symlinkedRoots.add(normalizedCandidate);
160
163
  }
161
164
  }
162
- catch {
163
- // Ignore non-existent dependency roots.
165
+ catch (error) {
166
+ getLogger().debug(`[Synchronizer] Dependency root probe failed for ${normalizedCandidate}: ${error instanceof Error ? error.message : String(error)}`);
164
167
  }
165
168
  }
166
169
  return symlinkedRoots;
@@ -364,10 +367,8 @@ export class WorkspaceSynchronizer {
364
367
  try {
365
368
  oursContent = await readFile(mainAbsPath);
366
369
  }
367
- catch {
368
- // If file missing in main but modify in shadow -> conflict or re-create?
369
- // Since we filtered for 'M', it implies it existed in Base. If missing in Main, User deleted it.
370
- // Merge Modified vs Deleted -> Conflict.
370
+ catch (error) {
371
+ getLogger().debug(`[ExplicitMerge] Ours file missing in main workspace, treating as conflict: ${relativePath}: ${error instanceof Error ? error.message : String(error)}`);
371
372
  conflicts.push(relativePath);
372
373
  continue;
373
374
  }
@@ -445,7 +446,7 @@ export class WorkspaceSynchronizer {
445
446
  });
446
447
  }
447
448
  catch (error) {
448
- throw new Error(`Apply-back completed with conflicts (Atomic Patch). Rejection files (.rej) have been generated. Original error: ${error instanceof Error ? error.message : String(error)}`);
449
+ throw new Error(`Apply-back completed with conflicts (Atomic Patch). Rejection files (.rej) have been generated. Original error: ${errorMessage(error)}`);
449
450
  }
450
451
  }
451
452
  parseStatusEntries(statusPorcelainZ) {
@@ -545,7 +546,8 @@ export class WorkspaceSynchronizer {
545
546
  const content = await readFile(path.join(mainRepoPath, ...file.split('/')));
546
547
  entries.push(`${file}:${hashContent(content)}`);
547
548
  }
548
- catch {
549
+ catch (error) {
550
+ getLogger().debug(`[Synchronizer] Fingerprint read failed for untracked file ${file}: ${error instanceof Error ? error.message : String(error)}`);
549
551
  entries.push(`${file}:missing`);
550
552
  }
551
553
  }
@@ -594,8 +596,8 @@ export class WorkspaceSynchronizer {
594
596
  try {
595
597
  await copyFile(src, dst);
596
598
  }
597
- catch {
598
- // Ignore backup failure for deleted files
599
+ catch (error) {
600
+ getLogger().debug(`[Synchronizer] Failed to backup dirty file ${file}: ${error instanceof Error ? error.message : String(error)}`);
599
601
  }
600
602
  }
601
603
  }
@@ -624,7 +626,7 @@ export class WorkspaceSynchronizer {
624
626
  stagedPatchPath,
625
627
  };
626
628
  };
627
- dirtyBackup = (await createDirtyBackup());
629
+ dirtyBackup = await createDirtyBackup();
628
630
  getLogger().info(text.loop.applyBackCheckpointCreated());
629
631
  getLogger().info(text.loop.applyBackCheckpointLocation(dirtyBackup?.dir || ''));
630
632
  if (telemetry) {
@@ -673,7 +675,7 @@ export class WorkspaceSynchronizer {
673
675
  }
674
676
  }
675
677
  catch (error) {
676
- const err = error instanceof Error ? error : new Error(String(error));
678
+ const err = error instanceof Error ? error : new Error(errorMessage(error));
677
679
  if (telemetry) {
678
680
  telemetry.error = err.message;
679
681
  }
@@ -693,8 +695,8 @@ export class WorkspaceSynchronizer {
693
695
  current.working !== originalFingerprint.working ||
694
696
  current.untracked !== originalFingerprint.untracked;
695
697
  }
696
- catch {
697
- // If fingerprinting fails, assume changed to be safe.
698
+ catch (error) {
699
+ getLogger().debug(`[Synchronizer] Workspace fingerprint comparison failed, assuming changed: ${error instanceof Error ? error.message : String(error)}`);
698
700
  workspaceChanged = true;
699
701
  }
700
702
  }
@@ -737,7 +739,7 @@ export class WorkspaceSynchronizer {
737
739
  await copyFile(path.join(trackedDir, ...file.split('/')), path.join(mainRepoPath, ...file.split('/')));
738
740
  }
739
741
  catch (e) {
740
- getLogger().error(`[applyBack] Failed to restore tracked file ${file}: ${e instanceof Error ? e.message : String(e)}`);
742
+ getLogger().error(`[applyBack] Failed to restore tracked file ${file}: ${errorMessage(e)}`);
741
743
  }
742
744
  }
743
745
  }
@@ -751,8 +753,8 @@ export class WorkspaceSynchronizer {
751
753
  });
752
754
  await copyFile(path.join(untrackedDir, ...file.split('/')), path.join(mainRepoPath, ...file.split('/')));
753
755
  }
754
- catch {
755
- // Ignore restore errors for untracked files
756
+ catch (error) {
757
+ getLogger().debug(`[Synchronizer] Failed to restore untracked file ${file}: ${error instanceof Error ? error.message : String(error)}`);
756
758
  }
757
759
  }
758
760
  }
@@ -769,7 +771,7 @@ export class WorkspaceSynchronizer {
769
771
  }
770
772
  }
771
773
  catch (e) {
772
- const patchError = e instanceof Error ? e.message : String(e);
774
+ const patchError = errorMessage(e);
773
775
  getLogger().error(`[applyBack] Failed to restore staged state from patch. ${patchError}. ` +
774
776
  `Falling back to read-tree restore.`);
775
777
  try {
@@ -780,7 +782,7 @@ export class WorkspaceSynchronizer {
780
782
  }
781
783
  }
782
784
  catch (fallbackError) {
783
- const fallbackMessage = fallbackError instanceof Error ? fallbackError.message : String(fallbackError);
785
+ const fallbackMessage = errorMessage(fallbackError);
784
786
  if (telemetry) {
785
787
  telemetry.stagedRestoreSucceeded = false;
786
788
  telemetry.stagedRestoreError = `${patchError}; fallback read-tree failed: ${fallbackMessage}`;
@@ -808,7 +810,7 @@ export class WorkspaceSynchronizer {
808
810
  catch (snapshotRestoreError) {
809
811
  getLogger().error(`[applyBack] Snapshot restore failed during clean rollback. ` +
810
812
  `baseRef=${checkpointRef.baseRef}; ` +
811
- `error=${snapshotRestoreError instanceof Error ? snapshotRestoreError.message : String(snapshotRestoreError)}. ` +
813
+ `error=${errorMessage(snapshotRestoreError)}. ` +
812
814
  `Falling back to clean reset.`);
813
815
  }
814
816
  if (!restoredFromSnapshot) {
@@ -830,7 +832,7 @@ export class WorkspaceSynchronizer {
830
832
  await rm(dirtyBackup.dir, { recursive: true, force: true });
831
833
  }
832
834
  catch (cleanupError) {
833
- getLogger().debug(`[applyBack] Failed to cleanup dirty backup ${dirtyBackup.dir}: ${cleanupError instanceof Error ? cleanupError.message : String(cleanupError)}`);
835
+ getLogger().debug(`[applyBack] Failed to cleanup dirty backup ${dirtyBackup.dir}: ${errorMessage(cleanupError)}`);
834
836
  }
835
837
  }
836
838
  if (telemetry) {
@@ -1,14 +1,4 @@
1
- function isRecord(value) {
2
- return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
3
- }
4
- function getString(record, key) {
5
- const value = record[key];
6
- return typeof value === 'string' ? value : null;
7
- }
8
- function getRecord(record, key) {
9
- const value = record[key];
10
- return isRecord(value) ? value : null;
11
- }
1
+ import { isRecord, getString, getRecord } from '../../utils/serialize.js';
12
2
  /**
13
3
  * Best-effort conversion from our provider-agnostic `LLMStreamChunk` into
14
4
  * provider-agnostic canonical stream parts.
@@ -1,3 +1,4 @@
1
+ import { getLogger } from '../observability/logger.js';
1
2
  function findFirstJsonStart(text) {
2
3
  const obj = text.indexOf('{');
3
4
  const arr = text.indexOf('[');
@@ -63,7 +64,8 @@ export function extractFirstJsonValueFromText(text) {
63
64
  try {
64
65
  return JSON.parse(slice);
65
66
  }
66
- catch {
67
+ catch (error) {
68
+ getLogger().debug(`[StructuredOutput] Failed to parse extracted JSON: ${error instanceof Error ? error.message : String(error)}`);
67
69
  return null;
68
70
  }
69
71
  }
@@ -1,17 +1,5 @@
1
1
  import { Ajv } from 'ajv';
2
- function safeStringify(value) {
3
- try {
4
- return JSON.stringify(value);
5
- }
6
- catch {
7
- try {
8
- return String(value);
9
- }
10
- catch {
11
- return '[Unserializable]';
12
- }
13
- }
14
- }
2
+ import { safeStringify } from '../utils/serialize.js';
15
3
  function toSchemaKey(schema) {
16
4
  if (!schema || typeof schema !== 'object')
17
5
  return `non_object:${typeof schema}`;
@@ -125,8 +125,9 @@ export class ArtifactStore {
125
125
  getLogger().debug(`[ArtifactStore] GC removed ${result.removedFiles} files (${result.removedBytes} bytes)`);
126
126
  }
127
127
  }
128
- catch {
128
+ catch (error) {
129
129
  // Best-effort only; never fail the caller.
130
+ getLogger().debug(`[ArtifactStore] GC failed: ${error instanceof Error ? error.message : String(error)}`);
130
131
  }
131
132
  }
132
133
  static ensureGcLoop() {
@@ -1,5 +1,9 @@
1
1
  import { normalizeToolResultReplacementState, } from '../session/replacement-state.js';
2
2
  import { SUB_AGENT_CONTEXT_SNAPSHOT_VERSION, SUB_AGENT_CONTEXT_SNAPSHOT_FIELD_SEMANTICS, } from './types.js';
3
+ const SUPPORTED_SNAPSHOT_FIELDS = new Set([
4
+ 'version',
5
+ ...Object.keys(SUB_AGENT_CONTEXT_SNAPSHOT_FIELD_SEMANTICS),
6
+ ]);
3
7
  function deepClone(value) {
4
8
  if (typeof structuredClone === 'function') {
5
9
  return structuredClone(value);
@@ -39,7 +43,13 @@ function cloneConversationContext(messages) {
39
43
  function cloneToolCallingAudit(entries) {
40
44
  if (!Array.isArray(entries) || entries.length === 0)
41
45
  return undefined;
42
- return entries.map((entry) => deepClone(entry));
46
+ return entries.map((entry) => ({
47
+ ...entry,
48
+ toolResultPatchArtifact: cloneArtifactHandle(entry.toolResultPatchArtifact),
49
+ toolResultAuditArtifact: cloneArtifactHandle(entry.toolResultAuditArtifact),
50
+ toolResultReadArtifact: cloneArtifactHandle(entry.toolResultReadArtifact),
51
+ toolResultPreviewArtifact: cloneArtifactHandle(entry.toolResultPreviewArtifact),
52
+ }));
43
53
  }
44
54
  function cloneArtifactHints(hints) {
45
55
  if (!hints)
@@ -106,11 +116,7 @@ function normalizeSnapshotVersion(snapshot) {
106
116
  return version;
107
117
  }
108
118
  function assertSupportedSnapshotFields(snapshot) {
109
- const supportedFields = new Set([
110
- 'version',
111
- ...Object.keys(SUB_AGENT_CONTEXT_SNAPSHOT_FIELD_SEMANTICS),
112
- ]);
113
- const unknownFields = Object.keys(snapshot).filter((key) => !supportedFields.has(key));
119
+ const unknownFields = Object.keys(snapshot).filter((key) => !SUPPORTED_SNAPSHOT_FIELDS.has(key));
114
120
  if (unknownFields.length > 0) {
115
121
  throw new Error(`Unsupported sub-agent context snapshot fields: ${unknownFields.sort().join(', ')}`);
116
122
  }
@@ -1,6 +1,9 @@
1
- const LOG_HISTORY_LIMIT = 50;
1
+ const LOG_HISTORY_LIMIT = 200;
2
2
  export class InMemorySubAgentController {
3
3
  agents = new Map();
4
+ toolCallListeners = new Set();
5
+ results = new Map();
6
+ waiters = new Map();
4
7
  registerAgent(id, profile, status) {
5
8
  const existing = this.agents.get(id);
6
9
  if (existing) {
@@ -16,6 +19,8 @@ export class InMemorySubAgentController {
16
19
  updatedAt: new Date(),
17
20
  stopRequested: false,
18
21
  logs: [],
22
+ tokenUsage: 0,
23
+ toolCallCount: 0,
19
24
  });
20
25
  }
21
26
  updateStatus(id, status, summary) {
@@ -37,6 +42,35 @@ export class InMemorySubAgentController {
37
42
  agent.logs.splice(0, agent.logs.length - LOG_HISTORY_LIMIT);
38
43
  }
39
44
  }
45
+ addTokenUsage(id, tokens) {
46
+ const agent = this.agents.get(id);
47
+ if (!agent)
48
+ return;
49
+ agent.tokenUsage += tokens;
50
+ }
51
+ recordToolCall(id, toolName, durationMs, success) {
52
+ const agent = this.agents.get(id);
53
+ if (!agent)
54
+ return;
55
+ agent.toolCallCount++;
56
+ const event = {
57
+ type: 'tool.call.end',
58
+ agentId: id,
59
+ toolName,
60
+ timestamp: Date.now(),
61
+ durationMs,
62
+ success,
63
+ };
64
+ for (const listener of this.toolCallListeners) {
65
+ listener(event);
66
+ }
67
+ }
68
+ onToolCall(listener) {
69
+ this.toolCallListeners.add(listener);
70
+ return () => {
71
+ this.toolCallListeners.delete(listener);
72
+ };
73
+ }
40
74
  listAgents() {
41
75
  return Array.from(this.agents.values());
42
76
  }
@@ -62,6 +96,41 @@ export class InMemorySubAgentController {
62
96
  isStopRequested(id) {
63
97
  return this.agents.get(id)?.stopRequested ?? false;
64
98
  }
99
+ setResult(id, result) {
100
+ this.results.set(id, result);
101
+ const waiters = this.waiters.get(id);
102
+ if (waiters) {
103
+ for (const resolve of waiters) {
104
+ resolve(result);
105
+ }
106
+ this.waiters.delete(id);
107
+ }
108
+ }
109
+ async awaitResult(id, timeoutMs = 300_000) {
110
+ // Check if result is already available
111
+ const existing = this.results.get(id);
112
+ if (existing)
113
+ return existing;
114
+ // Wait for the result with timeout
115
+ return new Promise((resolve) => {
116
+ const timer = setTimeout(() => {
117
+ // Remove this waiter on timeout
118
+ const waiters = this.waiters.get(id);
119
+ if (waiters) {
120
+ const idx = waiters.indexOf(resolve);
121
+ if (idx >= 0)
122
+ waiters.splice(idx, 1);
123
+ }
124
+ resolve(undefined);
125
+ }, timeoutMs);
126
+ const waiters = this.waiters.get(id) ?? [];
127
+ waiters.push((result) => {
128
+ clearTimeout(timer);
129
+ resolve(result);
130
+ });
131
+ this.waiters.set(id, waiters);
132
+ });
133
+ }
65
134
  }
66
135
  export function createSubAgentController() {
67
136
  return new InMemorySubAgentController();
@@ -27,12 +27,34 @@ export class SmallfryLoop {
27
27
  async execute(initCtx) {
28
28
  getLogger().debug(`[SmallfryLoop] ${text.smallfry.status.working} (${this.profile.name})`);
29
29
  let pipeline = Pipeline.of(initCtx);
30
+ let turnCount = 0;
31
+ const maxTurns = this.profile.maxTurns;
30
32
  // Dynamic Phase Injection based on Stratagem
33
+ // PREFLIGHT is deterministic (no LLM call), so it doesn't count as a turn
31
34
  pipeline = pipeline.step('PREFLIGHT', runPreflight);
32
- pipeline = pipeline.step('CONTEXT', buildContext);
33
- pipeline = pipeline.step('PLAN', generatePlan);
35
+ // Each subsequent step makes at least one LLM call
36
+ if (maxTurns !== undefined && turnCount >= maxTurns) {
37
+ getLogger().warn(`[SmallfryLoop] maxTurns (${maxTurns}) reached before CONTEXT — stopping early`);
38
+ }
39
+ else {
40
+ pipeline = pipeline.step('CONTEXT', buildContext);
41
+ turnCount++;
42
+ }
43
+ if (maxTurns !== undefined && turnCount >= maxTurns) {
44
+ getLogger().warn(`[SmallfryLoop] maxTurns (${maxTurns}) reached before PLAN — stopping early`);
45
+ }
46
+ else {
47
+ pipeline = pipeline.step('PLAN', generatePlan);
48
+ turnCount++;
49
+ }
34
50
  if (this.profile.stratagem === 'surgeon') {
35
- pipeline = pipeline.step('PATCH', generatePatch);
51
+ if (maxTurns !== undefined && turnCount >= maxTurns) {
52
+ getLogger().warn(`[SmallfryLoop] maxTurns (${maxTurns}) reached before PATCH — stopping early`);
53
+ }
54
+ else {
55
+ pipeline = pipeline.step('PATCH', generatePatch);
56
+ turnCount++;
57
+ }
36
58
  }
37
59
  const report = await pipeline.execute();
38
60
  report.auditPath = await saveAudit(report, initCtx.options);