salmon-loop 0.2.13 → 0.2.16

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 (218) hide show
  1. package/dist/cli/argv/headless-detection.js +27 -0
  2. package/dist/cli/chat-flow.js +11 -0
  3. package/dist/cli/chat.js +160 -24
  4. package/dist/cli/commands/chat.js +14 -7
  5. package/dist/cli/commands/flow-mode.js +63 -0
  6. package/dist/cli/commands/registry.js +2 -0
  7. package/dist/cli/commands/run/benchmark-artifacts.js +41 -0
  8. package/dist/cli/commands/run/early-errors.js +23 -0
  9. package/dist/cli/commands/run/handler.js +115 -27
  10. package/dist/cli/commands/run/headless-error-writer.js +8 -0
  11. package/dist/cli/commands/run/loop-params.js +2 -0
  12. package/dist/cli/commands/run/mode.js +2 -5
  13. package/dist/cli/commands/run/parse-options.js +16 -0
  14. package/dist/cli/commands/run/persist-session.js +10 -1
  15. package/dist/cli/commands/run/preflight.js +10 -0
  16. package/dist/cli/commands/run/reporter-factory.js +4 -0
  17. package/dist/cli/commands/run/runtime-llm.js +38 -11
  18. package/dist/cli/commands/run/runtime-options.js +2 -2
  19. package/dist/cli/commands/serve.js +91 -71
  20. package/dist/cli/commands/tool-names.js +78 -78
  21. package/dist/cli/headless/anthropic-stream-normalized-encoder.js +6 -1
  22. package/dist/cli/headless/json-protocol.js +37 -0
  23. package/dist/cli/headless/native-stream-normalized-encoder.js +6 -1
  24. package/dist/cli/headless/protocol-metadata.js +22 -0
  25. package/dist/cli/headless/stream-json-protocol.js +34 -1
  26. package/dist/cli/index.js +6 -4
  27. package/dist/cli/locales/en.js +30 -6
  28. package/dist/cli/program-bootstrap.js +8 -3
  29. package/dist/cli/program-commands.js +5 -1
  30. package/dist/cli/reporters/anthropic-stream.js +7 -1
  31. package/dist/cli/reporters/json.js +4 -0
  32. package/dist/cli/reporters/stream-json.js +17 -2
  33. package/dist/cli/run-cli.js +5 -3
  34. package/dist/cli/slash/runtime.js +27 -12
  35. package/dist/cli/ui/components/CommandInput.js +7 -3
  36. package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
  37. package/dist/cli/utils/command-option-source.js +13 -0
  38. package/dist/cli/utils/verify-resolver.js +8 -4
  39. package/dist/cli/utils/worktree-prepare-resolver.js +7 -3
  40. package/dist/core/adapters/fs/file-adapter.js +6 -0
  41. package/dist/core/adapters/fs/filesystem.js +2 -1
  42. package/dist/core/adapters/git/git-adapter.js +78 -1
  43. package/dist/core/benchmark/patch-artifact.js +124 -0
  44. package/dist/core/benchmark/swe-bench.js +25 -0
  45. package/dist/core/config/load.js +18 -11
  46. package/dist/core/config/resolve-llm.js +12 -0
  47. package/dist/core/config/resolvers/server.js +0 -6
  48. package/dist/core/config/validate.js +73 -21
  49. package/dist/core/context/gatherers/metadata-gatherer.js +1 -0
  50. package/dist/core/context/gatherers/ripgrep-gatherer.js +84 -2
  51. package/dist/core/context/keywords.js +18 -4
  52. package/dist/core/context/service-deps.js +2 -2
  53. package/dist/core/context/service.js +8 -0
  54. package/dist/core/context/steps/context-gather.js +38 -0
  55. package/dist/core/context/summarization/summarizer.js +55 -12
  56. package/dist/core/context/targeting/target-resolver.js +4 -4
  57. package/dist/core/extensions/index.js +23 -5
  58. package/dist/core/extensions/paths.js +31 -0
  59. package/dist/core/extensions/schemas.js +8 -5
  60. package/dist/core/facades/cli-chat.js +6 -2
  61. package/dist/core/facades/cli-command-chat.js +1 -0
  62. package/dist/core/facades/cli-command-tool-names.js +2 -0
  63. package/dist/core/facades/cli-observability.js +1 -1
  64. package/dist/core/facades/cli-run-handler.js +4 -2
  65. package/dist/core/facades/cli-run-persist-session.js +1 -0
  66. package/dist/core/facades/cli-serve.js +2 -4
  67. package/dist/core/facades/cli-utils-worktree.js +1 -1
  68. package/dist/core/failure/diagnostics.js +53 -1
  69. package/dist/core/grizzco/dsl/llm-strategy.js +4 -1
  70. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +67 -9
  71. package/dist/core/grizzco/engine/pipeline/pipeline.js +6 -2
  72. package/dist/core/grizzco/engine/transaction/attempt-failure.js +90 -15
  73. package/dist/core/grizzco/engine/transaction/report-mapper.js +17 -3
  74. package/dist/core/grizzco/engine/transaction/transaction-runner.js +165 -7
  75. package/dist/core/grizzco/flows/AutopilotFlow.js +18 -0
  76. package/dist/core/grizzco/flows/flow-dispatch.js +11 -0
  77. package/dist/core/grizzco/steps/answer.js +13 -14
  78. package/dist/core/grizzco/steps/autopilot.js +396 -0
  79. package/dist/core/grizzco/steps/cache-sharing.js +29 -0
  80. package/dist/core/grizzco/steps/explore.js +37 -21
  81. package/dist/core/grizzco/steps/generateReview.js +2 -5
  82. package/dist/core/grizzco/steps/patch/apply-check.js +10 -0
  83. package/dist/core/grizzco/steps/patch/diff-normalization.js +70 -0
  84. package/dist/core/grizzco/steps/patch/diff-salvage.js +46 -0
  85. package/dist/core/grizzco/steps/patch/prompt-input.js +42 -0
  86. package/dist/core/grizzco/steps/patch.js +105 -146
  87. package/dist/core/grizzco/steps/plan.js +101 -25
  88. package/dist/core/grizzco/steps/preflight.js +5 -6
  89. package/dist/core/grizzco/steps/request-assembly.js +78 -0
  90. package/dist/core/grizzco/steps/research.js +39 -36
  91. package/dist/core/grizzco/steps/tool-runtime.js +47 -0
  92. package/dist/core/grizzco/steps/verify-shared.js +23 -0
  93. package/dist/core/grizzco/steps/verify.js +13 -21
  94. package/dist/core/llm/ai-sdk/chat-executor.js +2 -0
  95. package/dist/core/llm/ai-sdk/high-level-phase-specs.js +63 -0
  96. package/dist/core/llm/ai-sdk/message-mapper.js +40 -10
  97. package/dist/core/llm/ai-sdk/provider-factory.js +14 -0
  98. package/dist/core/llm/ai-sdk/request-params.js +73 -0
  99. package/dist/core/llm/ai-sdk/result-mapper.js +16 -0
  100. package/dist/core/llm/ai-sdk.js +112 -27
  101. package/dist/core/llm/capabilities.js +12 -0
  102. package/dist/core/llm/contracts/repair.js +36 -30
  103. package/dist/core/llm/errors.js +83 -2
  104. package/dist/core/llm/message-composition.js +7 -22
  105. package/dist/core/llm/phase-router.js +29 -10
  106. package/dist/core/llm/redact.js +28 -3
  107. package/dist/core/llm/registry.js +2 -0
  108. package/dist/core/llm/request-augmentation.js +55 -0
  109. package/dist/core/llm/request-envelope.js +334 -0
  110. package/dist/core/llm/shared-request-assembly.js +35 -0
  111. package/dist/core/llm/stream-utils.js +13 -4
  112. package/dist/core/llm/utils.js +18 -29
  113. package/dist/core/memory/relevant-retrieval.js +144 -0
  114. package/dist/core/observability/logger.js +11 -2
  115. package/dist/core/patch/diff.js +1 -0
  116. package/dist/core/prompts/registry.js +39 -2
  117. package/dist/core/prompts/runtime.js +50 -12
  118. package/dist/core/prompts/templates/phases/patch_user.hbs +2 -5
  119. package/dist/core/prompts/templates/phases/research_user.hbs +11 -0
  120. package/dist/core/prompts/templates/phases/review_user.hbs +3 -0
  121. package/dist/core/prompts/templates/system/answer_system.hbs +5 -0
  122. package/dist/core/prompts/templates/system/autopilot_system.hbs +11 -0
  123. package/dist/core/prompts/templates/system/explore_system.hbs +14 -23
  124. package/dist/core/prompts/templates/system/main_system.hbs +4 -16
  125. package/dist/core/prompts/templates/system/patch_system.hbs +39 -8
  126. package/dist/core/prompts/templates/system/plan_system.hbs +86 -1
  127. package/dist/core/prompts/templates/system/research_system.hbs +2 -0
  128. package/dist/core/protocols/a2a/agent-card.js +3 -2
  129. package/dist/core/protocols/a2a/sdk/executor.js +2 -1
  130. package/dist/core/protocols/a2a/sdk/server.js +0 -1
  131. package/dist/core/protocols/acp/formal-agent.js +74 -51
  132. package/dist/core/protocols/acp/handlers.js +5 -1
  133. package/dist/core/protocols/acp/permission-provider.js +1 -1
  134. package/dist/core/protocols/shared/flow-mode-mapping.js +23 -0
  135. package/dist/core/public-capabilities/flow-mode-metadata.js +39 -0
  136. package/dist/core/public-capabilities/projections.js +29 -0
  137. package/dist/core/public-capabilities/registry.js +26 -0
  138. package/dist/core/public-capabilities/types.js +2 -0
  139. package/dist/core/runtime/agent-server-runtime.js +47 -43
  140. package/dist/core/runtime/execution-profile.js +67 -0
  141. package/dist/core/session/artifact-state.js +160 -0
  142. package/dist/core/session/compaction/index.js +183 -0
  143. package/dist/core/session/compaction/microcompact.js +78 -0
  144. package/dist/core/session/compaction/tracking.js +48 -0
  145. package/dist/core/session/compaction/types.js +11 -0
  146. package/dist/core/session/compression.js +8 -0
  147. package/dist/core/session/manager.js +244 -8
  148. package/dist/core/session/pruning-strategy.js +55 -9
  149. package/dist/core/session/replacement-preview-provider.js +24 -0
  150. package/dist/core/session/replacement-state.js +131 -0
  151. package/dist/core/session/resume-repair/pipeline.js +79 -0
  152. package/dist/core/session/resume-repair/stages/load-raw-archive-state.js +40 -0
  153. package/dist/core/session/resume-repair/stages/reattach-runtime-state.js +8 -0
  154. package/dist/core/session/resume-repair/stages/recover-orphaned-branches.js +10 -0
  155. package/dist/core/session/resume-repair/stages/relink-boundary-and-tail.js +36 -0
  156. package/dist/core/session/resume-repair/stages/replay-startup-hooks.js +23 -0
  157. package/dist/core/session/resume-repair/stages/rescue-stale-metadata.js +17 -0
  158. package/dist/core/session/resume-repair/types.js +2 -0
  159. package/dist/core/session/summary-sync.js +164 -13
  160. package/dist/core/session/token-tracker.js +6 -0
  161. package/dist/core/skills/audit.js +34 -0
  162. package/dist/core/skills/bridge.js +84 -7
  163. package/dist/core/skills/discovery.js +94 -0
  164. package/dist/core/skills/feature-flags.js +52 -0
  165. package/dist/core/skills/index.js +1 -1
  166. package/dist/core/skills/loader.js +195 -20
  167. package/dist/core/skills/parser.js +296 -24
  168. package/dist/core/skills/permissions.js +117 -0
  169. package/dist/core/skills/runtime/MicroTaskRunner.js +10 -4
  170. package/dist/core/skills/runtime/SkillRunner.js +240 -61
  171. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +37 -7
  172. package/dist/core/strata/layers/worktree.js +67 -10
  173. package/dist/core/strata/runtime/synchronizer.js +29 -2
  174. package/dist/core/streaming/stream-assembler.js +75 -31
  175. package/dist/core/sub-agent/context-snapshot.js +156 -0
  176. package/dist/core/sub-agent/core/loop.js +1 -1
  177. package/dist/core/sub-agent/core/manager.js +119 -20
  178. package/dist/core/sub-agent/dispatch-policy.js +29 -0
  179. package/dist/core/sub-agent/prefix-consistency.js +48 -0
  180. package/dist/core/sub-agent/registry-defaults.js +4 -0
  181. package/dist/core/sub-agent/tools/task-spawn.js +79 -2
  182. package/dist/core/sub-agent/types.js +134 -5
  183. package/dist/core/tools/audit.js +13 -4
  184. package/dist/core/tools/builtin/ast-grep.js +1 -1
  185. package/dist/core/tools/builtin/ast.js +1 -1
  186. package/dist/core/tools/builtin/benchmark.js +360 -0
  187. package/dist/core/tools/builtin/code-search/backends/rg.js +2 -1
  188. package/dist/core/tools/builtin/code-search/executor.js +6 -1
  189. package/dist/core/tools/builtin/code-search/spec.js +26 -2
  190. package/dist/core/tools/builtin/fs.js +256 -23
  191. package/dist/core/tools/builtin/git.js +2 -2
  192. package/dist/core/tools/builtin/index.js +51 -2
  193. package/dist/core/tools/builtin/interaction.js +8 -1
  194. package/dist/core/tools/builtin/plan.js +37 -15
  195. package/dist/core/tools/builtin/shell.js +1 -1
  196. package/dist/core/tools/loader.js +39 -16
  197. package/dist/core/tools/mapper.js +17 -3
  198. package/dist/core/tools/parallel/scheduler.js +35 -4
  199. package/dist/core/tools/permissions/permission-rules.js +5 -10
  200. package/dist/core/tools/policy.js +6 -1
  201. package/dist/core/tools/recoverable-tool-errors.js +10 -0
  202. package/dist/core/tools/router.js +24 -6
  203. package/dist/core/tools/session.js +458 -48
  204. package/dist/core/tools/tool-visibility.js +62 -0
  205. package/dist/core/tools/types.js +9 -1
  206. package/dist/core/types/execution.js +4 -0
  207. package/dist/core/types/flow-mode.js +8 -0
  208. package/dist/core/utils/path.js +52 -0
  209. package/dist/core/verification/runner.js +4 -1
  210. package/dist/languages/typescript/index.js +4 -1
  211. package/dist/locales/en.js +35 -2
  212. package/dist/utils/eol.js +1 -1
  213. package/package.json +13 -6
  214. package/scripts/fix-es-abstract-compat.js +77 -0
  215. package/dist/core/runtime/fastify-server-bundle.js +0 -26
  216. package/dist/core/runtime/sidecar-fastify-plugin.js +0 -35
  217. package/dist/core/runtime/sidecar-paths.js +0 -47
  218. package/dist/core/runtime/sidecar-route-catalog.js +0 -103
@@ -1,15 +1,51 @@
1
1
  import { randomBytes } from 'crypto';
2
2
  import { join } from 'path';
3
3
  import { FileAdapter } from '../adapters/fs/index.js';
4
+ import { recordAuditEvent } from '../observability/audit-trail.js';
4
5
  import { getLogger } from '../observability/logger.js';
6
+ import { parseFlowMode } from '../types/flow-mode.js';
7
+ import { mergeReplacementStateFromArtifactHints, mergeSessionArtifactState, normalizeSessionArtifactState, } from './artifact-state.js';
5
8
  import { SessionCompressor, CompressedSessionStore } from './compression.js';
6
9
  import { SessionPruningEngine } from './pruning-strategy.js';
10
+ import { freezeToolResultReplacementDecision, normalizeToolResultReplacementState, } from './replacement-state.js';
11
+ import { createResumeRepairPipeline } from './resume-repair/pipeline.js';
12
+ const RESUME_REPAIR_V1_FLAG = 'SALMONLOOP_RESUME_REPAIR_V1';
13
+ function resolveResumeRepairV1Enabled() {
14
+ const raw = process.env[RESUME_REPAIR_V1_FLAG];
15
+ if (!raw || !raw.trim())
16
+ return true;
17
+ const normalized = raw.trim().toLowerCase();
18
+ if (normalized === '0' || normalized === 'false' || normalized === 'off' || normalized === 'no') {
19
+ return false;
20
+ }
21
+ return true;
22
+ }
23
+ function recordResumeRepairMetrics(details) {
24
+ recordAuditEvent('session.resume_repair.completed', {
25
+ mode: details.mode,
26
+ success: details.success,
27
+ metric: 'repair_violation_rate',
28
+ repairViolationCount: details.repairViolationCount,
29
+ replacementReuseMetric: 'replacement_reuse_hit_rate',
30
+ replacementReuseHitCount: details.replacementReuseHitCount,
31
+ contractViolationCodes: details.contractViolationCodes ?? [],
32
+ }, {
33
+ source: 'session',
34
+ severity: details.success ? 'low' : 'medium',
35
+ scope: 'session',
36
+ });
37
+ }
38
+ function normalizeChatState(chatState) {
39
+ const flowMode = parseFlowMode(chatState?.flowMode);
40
+ return flowMode ? { flowMode } : undefined;
41
+ }
7
42
  /**
8
43
  * Manages chat session persistence and lifecycle.
9
44
  * Storage: .salmonloop/chat-sessions/<id>.json
10
45
  * Features: Auto-pruning, compression, intelligent cleanup
11
46
  */
12
47
  export class ChatSessionManager {
48
+ repoPath;
13
49
  storageDir;
14
50
  currentSession = null;
15
51
  fileAdapter = new FileAdapter();
@@ -17,6 +53,7 @@ export class ChatSessionManager {
17
53
  compressor;
18
54
  compressedStore;
19
55
  constructor(repoPath, pruningStrategy) {
56
+ this.repoPath = repoPath;
20
57
  this.storageDir = join(repoPath, '.salmonloop', 'chat-sessions');
21
58
  this.pruningEngine = new SessionPruningEngine(pruningStrategy);
22
59
  this.compressor = new SessionCompressor();
@@ -87,7 +124,11 @@ export class ChatSessionManager {
87
124
  const filePath = join(this.storageDir, `${targetId}.json`);
88
125
  try {
89
126
  const data = await this.fileAdapter.readFile(filePath);
90
- this.currentSession = JSON.parse(data);
127
+ const parsed = JSON.parse(data);
128
+ parsed.meta.chatState = normalizeChatState(parsed.meta.chatState);
129
+ parsed.meta.artifactState = normalizeSessionArtifactState(parsed.meta.artifactState);
130
+ parsed.meta.replacementState = normalizeToolResultReplacementState(parsed.meta.replacementState);
131
+ this.currentSession = parsed;
91
132
  return this.currentSession;
92
133
  }
93
134
  catch {
@@ -171,6 +212,9 @@ export class ChatSessionManager {
171
212
  getSummaryState() {
172
213
  return this.currentSession?.meta.summaryState;
173
214
  }
215
+ getArtifactState() {
216
+ return normalizeSessionArtifactState(this.currentSession?.meta.artifactState);
217
+ }
174
218
  /**
175
219
  * Update summary state after summarization.
176
220
  */
@@ -179,6 +223,38 @@ export class ChatSessionManager {
179
223
  throw new Error('No active session');
180
224
  this.currentSession.meta.summaryState = state;
181
225
  }
226
+ updateArtifactState(state) {
227
+ if (!this.currentSession)
228
+ throw new Error('No active session');
229
+ this.currentSession.meta.artifactState = normalizeSessionArtifactState(state);
230
+ }
231
+ mergeArtifactState(state) {
232
+ if (!this.currentSession)
233
+ throw new Error('No active session');
234
+ this.currentSession.meta.artifactState = mergeSessionArtifactState(this.currentSession.meta.artifactState, state);
235
+ this.currentSession.meta.replacementState = mergeReplacementStateFromArtifactHints(this.currentSession.meta.replacementState, state);
236
+ }
237
+ getReplacementState() {
238
+ return normalizeToolResultReplacementState(this.currentSession?.meta.replacementState);
239
+ }
240
+ updateReplacementState(state) {
241
+ if (!this.currentSession)
242
+ throw new Error('No active session');
243
+ this.currentSession.meta.replacementState = normalizeToolResultReplacementState(state);
244
+ }
245
+ getChatFlowMode() {
246
+ return this.currentSession?.meta.chatState?.flowMode;
247
+ }
248
+ updateChatFlowMode(mode) {
249
+ if (!this.currentSession)
250
+ throw new Error('No active session');
251
+ this.currentSession.meta.chatState = normalizeChatState(mode === undefined ? undefined : { flowMode: mode });
252
+ }
253
+ freezeReplacementDecision(entry, options) {
254
+ if (!this.currentSession)
255
+ throw new Error('No active session');
256
+ this.currentSession.meta.replacementState = freezeToolResultReplacementDecision(this.currentSession.meta.replacementState, entry, options);
257
+ }
182
258
  /**
183
259
  * Clear summary state (e.g., on session reset).
184
260
  */
@@ -255,6 +331,9 @@ export class ChatSessionManager {
255
331
  const filePath = join(this.storageDir, file);
256
332
  const data = await this.fileAdapter.readFile(filePath);
257
333
  const session = JSON.parse(data);
334
+ session.meta.chatState = normalizeChatState(session.meta.chatState);
335
+ session.meta.artifactState = normalizeSessionArtifactState(session.meta.artifactState);
336
+ session.meta.replacementState = normalizeToolResultReplacementState(session.meta.replacementState);
258
337
  sessions.push(session);
259
338
  }
260
339
  catch (error) {
@@ -298,17 +377,174 @@ export class ChatSessionManager {
298
377
  * List archived sessions
299
378
  */
300
379
  async listArchivedSessions() {
301
- // Implement archived session list functionality
302
- // This needs to access the compressed storage
303
- return [];
380
+ const archiveDir = this.getArchiveStorageDir();
381
+ const files = await this.fileAdapter.readdir(archiveDir).catch(() => []);
382
+ const archived = [];
383
+ for (const file of files) {
384
+ if (!file.endsWith('.mpack.gz'))
385
+ continue;
386
+ try {
387
+ const compressed = await this.compressedStore.loadCompressed(file);
388
+ if (!compressed)
389
+ continue;
390
+ const stats = await this.fileAdapter.stat(join(archiveDir, file));
391
+ archived.push({
392
+ id: compressed.meta.id,
393
+ name: compressed.meta.name,
394
+ archivedAt: stats.mtime.getTime(),
395
+ });
396
+ }
397
+ catch (error) {
398
+ getLogger().warn(`Failed to load archived session ${file}: ${error}`);
399
+ }
400
+ }
401
+ return archived.sort((a, b) => b.archivedAt - a.archivedAt);
304
402
  }
305
403
  /**
306
404
  * Restore session from archive
307
405
  */
308
- async restoreFromArchive(_archiveId) {
309
- // Implement session restoration from archive functionality
310
- // This needs to access the compressed storage and decompress
311
- return null;
406
+ async restoreFromArchive(archiveId) {
407
+ const filename = await this.resolveArchiveFilename(archiveId);
408
+ if (!filename)
409
+ return null;
410
+ const resumeRepairV1Enabled = resolveResumeRepairV1Enabled();
411
+ try {
412
+ if (!resumeRepairV1Enabled) {
413
+ const restored = await this.restoreFromArchiveLegacy(filename);
414
+ if (!restored) {
415
+ recordResumeRepairMetrics({
416
+ mode: 'legacy',
417
+ success: false,
418
+ repairViolationCount: 1,
419
+ replacementReuseHitCount: 0,
420
+ contractViolationCodes: ['LEGACY_RESTORE_FAILED'],
421
+ });
422
+ return null;
423
+ }
424
+ recordResumeRepairMetrics({
425
+ mode: 'legacy',
426
+ success: true,
427
+ repairViolationCount: 0,
428
+ replacementReuseHitCount: Object.keys(restored.meta.replacementState?.entries ?? {})
429
+ .length,
430
+ });
431
+ this.currentSession = restored;
432
+ await this.save();
433
+ return restored;
434
+ }
435
+ const pipeline = createResumeRepairPipeline({
436
+ compressedStore: this.compressedStore,
437
+ compressor: this.compressor,
438
+ repoPath: this.repoPath,
439
+ });
440
+ const repaired = await pipeline.run({ archiveId, filename });
441
+ if (!repaired.session) {
442
+ recordResumeRepairMetrics({
443
+ mode: 'repair_v1',
444
+ success: false,
445
+ repairViolationCount: repaired.contractViolations.length,
446
+ replacementReuseHitCount: 0,
447
+ contractViolationCodes: repaired.contractViolations.map((entry) => entry.code),
448
+ });
449
+ const violationText = repaired.contractViolations.map((entry) => entry.message).join('; ');
450
+ getLogger().warn(`Failed to restore archived session ${archiveId}: ${violationText || 'repair pipeline rejected archive'}`);
451
+ return null;
452
+ }
453
+ repaired.session.meta.resumeRepairState = {
454
+ schemaVersion: 1,
455
+ lastRunAt: Date.now(),
456
+ warnings: repaired.warnings.map((entry) => `${entry.code}: ${entry.message}`),
457
+ repairActions: repaired.repairActions.map((entry) => `${entry.code}: ${entry.detail}`),
458
+ contractViolations: repaired.contractViolations.map((entry) => `${entry.code}: ${entry.message}`),
459
+ };
460
+ repaired.session.meta.replacementState = normalizeToolResultReplacementState(repaired.replacementState);
461
+ recordResumeRepairMetrics({
462
+ mode: 'repair_v1',
463
+ success: true,
464
+ repairViolationCount: repaired.contractViolations.length,
465
+ replacementReuseHitCount: Object.keys(repaired.replacementState?.entries ?? {}).length,
466
+ contractViolationCodes: repaired.contractViolations.map((entry) => entry.code),
467
+ });
468
+ this.currentSession = repaired.session;
469
+ await this.save();
470
+ return repaired.session;
471
+ }
472
+ catch (error) {
473
+ recordResumeRepairMetrics({
474
+ mode: resumeRepairV1Enabled ? 'repair_v1' : 'legacy',
475
+ success: false,
476
+ repairViolationCount: 1,
477
+ replacementReuseHitCount: 0,
478
+ contractViolationCodes: ['RESTORE_EXCEPTION'],
479
+ });
480
+ getLogger().warn(`Failed to restore archived session ${archiveId}: ${error}`);
481
+ return null;
482
+ }
483
+ }
484
+ async restoreFromArchiveLegacy(filename) {
485
+ const compressed = await this.compressedStore.loadCompressed(filename);
486
+ if (!compressed)
487
+ return null;
488
+ const partial = await this.compressor.decompressToSession(compressed);
489
+ if (!partial?.meta?.id || !partial?.meta?.name)
490
+ return null;
491
+ return {
492
+ meta: {
493
+ id: partial.meta.id,
494
+ name: partial.meta.name,
495
+ repoPath: this.repoPath,
496
+ createdAt: partial.meta.createdAt,
497
+ updatedAt: Date.now(),
498
+ totalIterations: partial.meta.totalIterations ?? partial.iterations.length,
499
+ successfulIterations: partial.meta.successfulIterations ?? 0,
500
+ totalTokens: partial.meta.totalTokens ?? { input: 0, output: 0 },
501
+ snapshots: [],
502
+ chatState: normalizeChatState(partial.meta.chatState),
503
+ artifactState: normalizeSessionArtifactState(partial.meta.artifactState),
504
+ replacementState: normalizeToolResultReplacementState(partial.meta.replacementState),
505
+ },
506
+ messages: partial.messages.map((message, index) => ({
507
+ id: `restored-msg-${index}`,
508
+ role: message.role,
509
+ content: message.content,
510
+ timestamp: message.timestamp,
511
+ })),
512
+ iterations: partial.iterations.map((iteration, index) => ({
513
+ id: iteration.id || `restored-iter-${index + 1}`,
514
+ attempt: index + 1,
515
+ plan: null,
516
+ patch: null,
517
+ error: iteration.outcome === 'failure' ? iteration.summary : undefined,
518
+ contextSummary: iteration.summary,
519
+ })),
520
+ };
521
+ }
522
+ getArchiveStorageDir() {
523
+ return join(this.repoPath, '.salmonloop', 'compressed-sessions');
524
+ }
525
+ async resolveArchiveFilename(archiveId) {
526
+ const archiveDir = this.getArchiveStorageDir();
527
+ const files = (await this.fileAdapter.readdir(archiveDir).catch(() => [])).filter((file) => file.endsWith('.mpack.gz'));
528
+ if (files.length === 0)
529
+ return null;
530
+ if (archiveId.endsWith('.mpack.gz') && files.includes(archiveId)) {
531
+ return archiveId;
532
+ }
533
+ const exactFilename = `${archiveId}.mpack.gz`;
534
+ if (files.includes(exactFilename)) {
535
+ return exactFilename;
536
+ }
537
+ const prefixMatches = files.filter((file) => file.startsWith(archiveId));
538
+ if (prefixMatches.length === 0)
539
+ return null;
540
+ if (prefixMatches.length === 1)
541
+ return prefixMatches[0];
542
+ const withMtime = await Promise.all(prefixMatches.map(async (file) => {
543
+ const stats = await this.fileAdapter.stat(join(archiveDir, file));
544
+ return { file, mtime: stats.mtime.getTime() };
545
+ }));
546
+ withMtime.sort((a, b) => b.mtime - a.mtime);
547
+ return withMtime[0]?.file ?? null;
312
548
  }
313
549
  }
314
550
  //# sourceMappingURL=manager.js.map
@@ -1,3 +1,7 @@
1
+ import { FileAdapter } from '../adapters/fs/index.js';
2
+ import { getLogger } from '../observability/logger.js';
3
+ import { normalizeSessionArtifactState } from './artifact-state.js';
4
+ import { SessionCompressor } from './compression.js';
1
5
  /**
2
6
  * Default memory pruning strategy configuration
3
7
  */
@@ -113,8 +117,14 @@ export class SessionPruningEngine {
113
117
  */
114
118
  export class SessionArchiver {
115
119
  archiveDir;
120
+ baseDir;
121
+ fileAdapter;
122
+ compressor;
116
123
  constructor(baseDir) {
124
+ this.baseDir = baseDir;
117
125
  this.archiveDir = `${baseDir}/.salmonloop/chat-archives`;
126
+ this.fileAdapter = new FileAdapter();
127
+ this.compressor = new SessionCompressor();
118
128
  }
119
129
  /**
120
130
  * Create session archive
@@ -131,23 +141,59 @@ export class SessionArchiver {
131
141
  /**
132
142
  * Restore session from archive
133
143
  */
134
- async restoreFromArchive(_archiveId) {
144
+ async restoreFromArchive(archiveId) {
135
145
  try {
136
- // Need to implement decompression and deserialization logic
137
- // Return null for now, implement later
138
- return null;
146
+ const archiveFile = archiveId.endsWith('.mpack.gz') ? archiveId : `${archiveId}.mpack.gz`;
147
+ const archivePath = `${this.archiveDir}/${archiveFile}`;
148
+ const encoded = await this.fileAdapter.readFile(archivePath);
149
+ const binary = new Uint8Array(Buffer.from(encoded, 'base64'));
150
+ const compressed = await this.compressor.decompressFromBinary(binary);
151
+ const partial = await this.compressor.decompressToSession(compressed);
152
+ return {
153
+ meta: {
154
+ id: partial.meta.id,
155
+ name: partial.meta.name,
156
+ repoPath: this.baseDir,
157
+ createdAt: partial.meta.createdAt,
158
+ updatedAt: Date.now(),
159
+ totalIterations: partial.meta.totalIterations ?? partial.iterations.length,
160
+ successfulIterations: partial.meta.successfulIterations ?? 0,
161
+ totalTokens: partial.meta.totalTokens ?? { input: 0, output: 0 },
162
+ snapshots: [],
163
+ artifactState: normalizeSessionArtifactState(partial.meta.artifactState),
164
+ },
165
+ messages: partial.messages.map((message, index) => ({
166
+ id: `archived-msg-${index}`,
167
+ role: message.role,
168
+ content: message.content,
169
+ timestamp: message.timestamp,
170
+ })),
171
+ iterations: partial.iterations.map((iter, index) => ({
172
+ id: iter.id || `archived-iter-${index + 1}`,
173
+ attempt: index + 1,
174
+ plan: null,
175
+ patch: null,
176
+ error: iter.outcome === 'failure' ? iter.summary : undefined,
177
+ contextSummary: iter.summary,
178
+ })),
179
+ };
139
180
  }
140
181
  catch {
141
182
  return null;
142
183
  }
143
184
  }
144
185
  async ensureArchiveDir() {
145
- // This method is not implemented as it requires file system adapter
146
- // The SessionArchiver is currently a placeholder for future implementation
186
+ await this.fileAdapter.mkdir(this.archiveDir);
147
187
  }
148
- async writeCompressedData(_archivePath, _compressedData) {
149
- // This method is not implemented as it requires file system adapter
150
- // The SessionArchiver is currently a placeholder for future implementation
188
+ async writeCompressedData(archivePath, compressedData) {
189
+ const encoded = Buffer.from(compressedData).toString('base64');
190
+ try {
191
+ await this.fileAdapter.writeFile(archivePath, encoded);
192
+ }
193
+ catch (error) {
194
+ getLogger().warn(`Failed to write session archive ${archivePath}: ${error}`);
195
+ throw error;
196
+ }
151
197
  }
152
198
  }
153
199
  //# sourceMappingURL=pruning-strategy.js.map
@@ -0,0 +1,24 @@
1
+ export class SessionReplacementPreviewProvider {
2
+ state;
3
+ constructor(state) {
4
+ this.state = state;
5
+ }
6
+ getPreviewHints() {
7
+ if (!this.state)
8
+ return undefined;
9
+ const out = Object.values(this.state.entries)
10
+ .filter((entry) => entry.decision === 'replaced' && entry.sourceArtifactHandle)
11
+ .sort((a, b) => a.frozenAt - b.frozenAt)
12
+ .map((entry) => ({
13
+ label: `Tool result preview: ${entry.toolResultId}`,
14
+ artifact: {
15
+ handle: entry.sourceArtifactHandle,
16
+ mimeType: 'application/json',
17
+ sha256: entry.toolResultId,
18
+ size: entry.preview.length,
19
+ },
20
+ }));
21
+ return out.length > 0 ? out : undefined;
22
+ }
23
+ }
24
+ //# sourceMappingURL=replacement-preview-provider.js.map
@@ -0,0 +1,131 @@
1
+ import { createHash } from 'crypto';
2
+ export const TOOL_RESULT_REPLACEMENT_IDENTITY_VERSION = 'v1';
3
+ export const TOOL_RESULT_REPLACEMENT_HASH_ALGORITHM = 'sha256';
4
+ export const TOOL_RESULT_REPLACEMENT_STATE_SCHEMA_VERSION = 1;
5
+ const DEFAULT_MAX_ENTRIES = 256;
6
+ function normalizeNewlines(value) {
7
+ return value.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
8
+ }
9
+ function canonicalize(value) {
10
+ if (value === null)
11
+ return 'null';
12
+ if (typeof value === 'string') {
13
+ return JSON.stringify(normalizeNewlines(value));
14
+ }
15
+ if (typeof value === 'number') {
16
+ if (!Number.isFinite(value))
17
+ return JSON.stringify(String(value));
18
+ return JSON.stringify(Number(value));
19
+ }
20
+ if (typeof value === 'boolean')
21
+ return value ? 'true' : 'false';
22
+ if (Array.isArray(value)) {
23
+ return `[${value.map((item) => canonicalize(item)).join(',')}]`;
24
+ }
25
+ if (typeof value === 'object') {
26
+ const record = value;
27
+ const keys = Object.keys(record).sort();
28
+ const pairs = keys.map((key) => `${JSON.stringify(key)}:${canonicalize(record[key])}`);
29
+ return `{${pairs.join(',')}}`;
30
+ }
31
+ return JSON.stringify(String(value));
32
+ }
33
+ export function createToolResultIdentity(params) {
34
+ const payloadBytes = canonicalize(params.payload);
35
+ const hash = createHash(TOOL_RESULT_REPLACEMENT_HASH_ALGORITHM);
36
+ hash.update(TOOL_RESULT_REPLACEMENT_IDENTITY_VERSION);
37
+ hash.update('\n');
38
+ hash.update(normalizeNewlines(params.canonicalToolCallIdentity).trim());
39
+ hash.update('\n');
40
+ hash.update(payloadBytes);
41
+ return hash.digest('hex');
42
+ }
43
+ function isValidEntry(value) {
44
+ if (!value || typeof value !== 'object')
45
+ return false;
46
+ const entry = value;
47
+ if (!entry.toolResultId || typeof entry.toolResultId !== 'string')
48
+ return false;
49
+ if (entry.decision !== 'kept' && entry.decision !== 'replaced')
50
+ return false;
51
+ if (typeof entry.preview !== 'string')
52
+ return false;
53
+ if (typeof entry.frozenAt !== 'number' || !Number.isFinite(entry.frozenAt))
54
+ return false;
55
+ if (entry.sourceArtifactHandle !== undefined && typeof entry.sourceArtifactHandle !== 'string') {
56
+ return false;
57
+ }
58
+ if (entry.identityVersion !== TOOL_RESULT_REPLACEMENT_IDENTITY_VERSION)
59
+ return false;
60
+ if (entry.hashAlgorithm !== TOOL_RESULT_REPLACEMENT_HASH_ALGORITHM)
61
+ return false;
62
+ return true;
63
+ }
64
+ export function normalizeToolResultReplacementState(state) {
65
+ if (!state || typeof state !== 'object')
66
+ return undefined;
67
+ if (state.schemaVersion !== TOOL_RESULT_REPLACEMENT_STATE_SCHEMA_VERSION)
68
+ return undefined;
69
+ if (!state.entries || typeof state.entries !== 'object')
70
+ return undefined;
71
+ const normalizedEntries = {};
72
+ for (const [key, value] of Object.entries(state.entries)) {
73
+ if (!isValidEntry(value))
74
+ continue;
75
+ if (value.toolResultId !== key)
76
+ continue;
77
+ normalizedEntries[key] = {
78
+ toolResultId: value.toolResultId,
79
+ decision: value.decision,
80
+ preview: value.preview,
81
+ frozenAt: value.frozenAt,
82
+ sourceArtifactHandle: value.sourceArtifactHandle,
83
+ identityVersion: value.identityVersion,
84
+ hashAlgorithm: value.hashAlgorithm,
85
+ };
86
+ }
87
+ if (Object.keys(normalizedEntries).length === 0)
88
+ return undefined;
89
+ return {
90
+ schemaVersion: TOOL_RESULT_REPLACEMENT_STATE_SCHEMA_VERSION,
91
+ entries: normalizedEntries,
92
+ };
93
+ }
94
+ export function createEmptyToolResultReplacementState() {
95
+ return {
96
+ schemaVersion: TOOL_RESULT_REPLACEMENT_STATE_SCHEMA_VERSION,
97
+ entries: {},
98
+ };
99
+ }
100
+ export function freezeToolResultReplacementDecision(state, entry, options) {
101
+ const base = normalizeToolResultReplacementState(state) ?? createEmptyToolResultReplacementState();
102
+ const existing = base.entries[entry.toolResultId];
103
+ if (existing) {
104
+ return base;
105
+ }
106
+ const nextEntries = {
107
+ ...base.entries,
108
+ [entry.toolResultId]: {
109
+ toolResultId: entry.toolResultId,
110
+ decision: entry.decision,
111
+ preview: entry.preview,
112
+ frozenAt: entry.frozenAt ?? Date.now(),
113
+ sourceArtifactHandle: entry.sourceArtifactHandle,
114
+ identityVersion: TOOL_RESULT_REPLACEMENT_IDENTITY_VERSION,
115
+ hashAlgorithm: TOOL_RESULT_REPLACEMENT_HASH_ALGORITHM,
116
+ },
117
+ };
118
+ const maxEntries = Math.max(1, options?.maxEntries ?? DEFAULT_MAX_ENTRIES);
119
+ const keys = Object.keys(nextEntries);
120
+ if (keys.length > maxEntries) {
121
+ const sorted = keys.sort((a, b) => nextEntries[a].frozenAt - nextEntries[b].frozenAt);
122
+ for (const evict of sorted.slice(0, keys.length - maxEntries)) {
123
+ delete nextEntries[evict];
124
+ }
125
+ }
126
+ return {
127
+ schemaVersion: TOOL_RESULT_REPLACEMENT_STATE_SCHEMA_VERSION,
128
+ entries: nextEntries,
129
+ };
130
+ }
131
+ //# sourceMappingURL=replacement-state.js.map
@@ -0,0 +1,79 @@
1
+ import { randomBytes } from 'crypto';
2
+ import { loadRawArchiveStateStage } from './stages/load-raw-archive-state.js';
3
+ import { reattachRuntimeStateStage } from './stages/reattach-runtime-state.js';
4
+ import { recoverOrphanedBranchesStage } from './stages/recover-orphaned-branches.js';
5
+ import { relinkBoundaryAndTailStage } from './stages/relink-boundary-and-tail.js';
6
+ import { replayStartupHooksStage } from './stages/replay-startup-hooks.js';
7
+ import { rescueStaleMetadataStage } from './stages/rescue-stale-metadata.js';
8
+ export function createResumeRepairPipeline(params) {
9
+ const context = {
10
+ repoPath: params.repoPath,
11
+ now: params.now ?? (() => Date.now()),
12
+ nextId: params.nextId ?? (() => randomBytes(6).toString('hex')),
13
+ startupHooks: params.startupHooks ?? [],
14
+ };
15
+ return {
16
+ async run(input) {
17
+ const compressed = await params.compressedStore.loadCompressed(input.filename);
18
+ if (!compressed) {
19
+ return {
20
+ warnings: [],
21
+ repairActions: [],
22
+ contractViolations: [
23
+ {
24
+ code: 'ARCHIVE_CORRUPTED',
25
+ message: `Archive "${input.archiveId}" cannot be loaded.`,
26
+ },
27
+ ],
28
+ };
29
+ }
30
+ const partial = await params.compressor.decompressToSession(compressed);
31
+ const state = {
32
+ archiveId: input.archiveId,
33
+ filename: input.filename,
34
+ compressed,
35
+ partial,
36
+ session: {
37
+ meta: {
38
+ id: '',
39
+ name: '',
40
+ repoPath: context.repoPath,
41
+ createdAt: 0,
42
+ updatedAt: 0,
43
+ totalIterations: 0,
44
+ successfulIterations: 0,
45
+ totalTokens: { input: 0, output: 0 },
46
+ snapshots: [],
47
+ },
48
+ messages: [],
49
+ iterations: [],
50
+ },
51
+ warnings: [],
52
+ repairActions: [],
53
+ contractViolations: [],
54
+ };
55
+ const stages = [
56
+ loadRawArchiveStateStage,
57
+ rescueStaleMetadataStage,
58
+ relinkBoundaryAndTailStage,
59
+ recoverOrphanedBranchesStage,
60
+ reattachRuntimeStateStage,
61
+ replayStartupHooksStage,
62
+ ];
63
+ for (const stage of stages) {
64
+ await stage(state, context);
65
+ if (state.contractViolations.length > 0) {
66
+ break;
67
+ }
68
+ }
69
+ return {
70
+ session: state.contractViolations.length > 0 ? undefined : state.session,
71
+ replacementState: state.replacementState,
72
+ warnings: state.warnings,
73
+ repairActions: state.repairActions,
74
+ contractViolations: state.contractViolations,
75
+ };
76
+ },
77
+ };
78
+ }
79
+ //# sourceMappingURL=pipeline.js.map
@@ -0,0 +1,40 @@
1
+ import { parseFlowMode } from '../../../types/flow-mode.js';
2
+ import { normalizeSessionArtifactState } from '../../artifact-state.js';
3
+ import { normalizeToolResultReplacementState } from '../../replacement-state.js';
4
+ export const loadRawArchiveStateStage = async (state, context) => {
5
+ const partial = state.partial;
6
+ const flowMode = parseFlowMode(partial.meta.chatState?.flowMode);
7
+ const reconstructed = {
8
+ meta: {
9
+ id: partial.meta.id,
10
+ name: partial.meta.name,
11
+ repoPath: context.repoPath,
12
+ createdAt: partial.meta.createdAt,
13
+ updatedAt: context.now(),
14
+ totalIterations: partial.meta.totalIterations ?? partial.iterations.length,
15
+ successfulIterations: partial.meta.successfulIterations ?? 0,
16
+ totalTokens: partial.meta.totalTokens ?? { input: 0, output: 0 },
17
+ snapshots: [],
18
+ chatState: flowMode ? { flowMode } : undefined,
19
+ artifactState: normalizeSessionArtifactState(partial.meta.artifactState),
20
+ replacementState: normalizeToolResultReplacementState(partial.meta.replacementState),
21
+ },
22
+ messages: partial.messages.map((message, index) => ({
23
+ id: `restored-msg-${index}`,
24
+ role: message.role,
25
+ content: message.content,
26
+ timestamp: message.timestamp,
27
+ })),
28
+ iterations: partial.iterations.map((iteration, index) => ({
29
+ id: iteration.id || `restored-iter-${index + 1}`,
30
+ attempt: index + 1,
31
+ plan: null,
32
+ patch: null,
33
+ error: iteration.outcome === 'failure' ? iteration.summary : undefined,
34
+ contextSummary: iteration.summary,
35
+ })),
36
+ };
37
+ state.session = reconstructed;
38
+ state.replacementState = reconstructed.meta.replacementState;
39
+ };
40
+ //# sourceMappingURL=load-raw-archive-state.js.map