salmon-loop 0.2.3 → 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 (234) 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 +161 -24
  4. package/dist/cli/commands/chat.js +30 -24
  5. package/dist/cli/commands/context.js +15 -3
  6. package/dist/cli/commands/flow-mode.js +63 -0
  7. package/dist/cli/commands/help-format.js +12 -0
  8. package/dist/cli/commands/registry.js +6 -7
  9. package/dist/cli/commands/run/benchmark-artifacts.js +41 -0
  10. package/dist/cli/commands/run/config-resolution.js +30 -24
  11. package/dist/cli/commands/run/early-errors.js +23 -0
  12. package/dist/cli/commands/run/handler.js +131 -44
  13. package/dist/cli/commands/run/headless-error-writer.js +8 -0
  14. package/dist/cli/commands/run/loop-params.js +3 -0
  15. package/dist/cli/commands/run/mode.js +2 -5
  16. package/dist/cli/commands/run/parse-options.js +18 -2
  17. package/dist/cli/commands/run/persist-session.js +10 -1
  18. package/dist/cli/commands/run/preflight.js +10 -0
  19. package/dist/cli/commands/run/reporter-factory.js +4 -0
  20. package/dist/cli/commands/run/runtime-llm.js +38 -11
  21. package/dist/cli/commands/run/runtime-options.js +2 -2
  22. package/dist/cli/commands/run/validate-options.js +0 -5
  23. package/dist/cli/commands/run/verbose.js +2 -7
  24. package/dist/cli/commands/serve.js +117 -90
  25. package/dist/cli/commands/tool-names.js +78 -78
  26. package/dist/cli/headless/anthropic-stream-normalized-encoder.js +6 -1
  27. package/dist/cli/headless/json-protocol.js +37 -0
  28. package/dist/cli/headless/native-stream-normalized-encoder.js +6 -1
  29. package/dist/cli/headless/protocol-metadata.js +22 -0
  30. package/dist/cli/headless/stream-json-protocol.js +34 -1
  31. package/dist/cli/index.js +6 -4
  32. package/dist/cli/locales/en.js +32 -6
  33. package/dist/cli/program-bootstrap.js +14 -4
  34. package/dist/cli/program-commands.js +9 -1
  35. package/dist/cli/program-options.js +1 -0
  36. package/dist/cli/reporters/anthropic-stream.js +7 -1
  37. package/dist/cli/reporters/json.js +4 -0
  38. package/dist/cli/reporters/stream-json.js +17 -2
  39. package/dist/cli/run-cli.js +5 -3
  40. package/dist/cli/slash/runtime.js +30 -15
  41. package/dist/cli/ui/components/CommandInput.js +7 -3
  42. package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
  43. package/dist/cli/utils/command-option-source.js +13 -0
  44. package/dist/cli/utils/output-format.js +6 -0
  45. package/dist/cli/utils/resolve-cli-config.js +98 -0
  46. package/dist/cli/utils/verbose-level.js +8 -0
  47. package/dist/cli/utils/verify-resolver.js +8 -4
  48. package/dist/cli/utils/worktree-prepare-resolver.js +7 -3
  49. package/dist/core/adapters/fs/file-adapter.js +6 -0
  50. package/dist/core/adapters/fs/filesystem.js +2 -1
  51. package/dist/core/adapters/git/git-adapter.js +78 -1
  52. package/dist/core/benchmark/patch-artifact.js +124 -0
  53. package/dist/core/benchmark/swe-bench.js +25 -0
  54. package/dist/core/config/load.js +39 -18
  55. package/dist/core/config/merge.js +27 -0
  56. package/dist/core/config/paths.js +24 -5
  57. package/dist/core/config/resolve-llm.js +12 -0
  58. package/dist/core/config/resolve.js +7 -5
  59. package/dist/core/config/resolvers/server.js +0 -6
  60. package/dist/core/config/validate.js +94 -21
  61. package/dist/core/context/gatherers/metadata-gatherer.js +1 -0
  62. package/dist/core/context/gatherers/ripgrep-gatherer.js +84 -2
  63. package/dist/core/context/keywords.js +18 -4
  64. package/dist/core/context/service-deps.js +2 -2
  65. package/dist/core/context/service.js +8 -0
  66. package/dist/core/context/steps/context-gather.js +38 -0
  67. package/dist/core/context/summarization/summarizer.js +55 -12
  68. package/dist/core/context/targeting/target-resolver.js +4 -4
  69. package/dist/core/extensions/index.js +23 -5
  70. package/dist/core/extensions/paths.js +31 -0
  71. package/dist/core/extensions/schemas.js +8 -5
  72. package/dist/core/facades/cli-chat.js +6 -2
  73. package/dist/core/facades/cli-command-chat.js +2 -1
  74. package/dist/core/facades/cli-command-tool-names.js +2 -0
  75. package/dist/core/facades/cli-context.js +1 -0
  76. package/dist/core/facades/cli-observability.js +1 -1
  77. package/dist/core/facades/cli-run-handler.js +4 -2
  78. package/dist/core/facades/cli-run-persist-session.js +1 -0
  79. package/dist/core/facades/cli-serve.js +2 -4
  80. package/dist/core/facades/cli-utils-worktree.js +1 -1
  81. package/dist/core/failure/diagnostics.js +53 -1
  82. package/dist/core/grizzco/dsl/llm-strategy.js +4 -1
  83. package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +67 -9
  84. package/dist/core/grizzco/engine/pipeline/pipeline.js +6 -2
  85. package/dist/core/grizzco/engine/transaction/attempt-failure.js +90 -15
  86. package/dist/core/grizzco/engine/transaction/report-mapper.js +17 -3
  87. package/dist/core/grizzco/engine/transaction/transaction-runner.js +173 -7
  88. package/dist/core/grizzco/flows/AutopilotFlow.js +18 -0
  89. package/dist/core/grizzco/flows/flow-dispatch.js +11 -0
  90. package/dist/core/grizzco/steps/answer.js +13 -14
  91. package/dist/core/grizzco/steps/autopilot.js +396 -0
  92. package/dist/core/grizzco/steps/cache-sharing.js +29 -0
  93. package/dist/core/grizzco/steps/explore.js +37 -21
  94. package/dist/core/grizzco/steps/generateReview.js +2 -5
  95. package/dist/core/grizzco/steps/patch/apply-check.js +10 -0
  96. package/dist/core/grizzco/steps/patch/diff-normalization.js +70 -0
  97. package/dist/core/grizzco/steps/patch/diff-salvage.js +46 -0
  98. package/dist/core/grizzco/steps/patch/prompt-input.js +42 -0
  99. package/dist/core/grizzco/steps/patch.js +105 -146
  100. package/dist/core/grizzco/steps/plan.js +101 -25
  101. package/dist/core/grizzco/steps/preflight.js +5 -3
  102. package/dist/core/grizzco/steps/request-assembly.js +78 -0
  103. package/dist/core/grizzco/steps/research.js +39 -36
  104. package/dist/core/grizzco/steps/tool-runtime.js +47 -0
  105. package/dist/core/grizzco/steps/verify-shared.js +23 -0
  106. package/dist/core/grizzco/steps/verify.js +13 -21
  107. package/dist/core/intent/chat-intent.js +0 -4
  108. package/dist/core/llm/ai-sdk/chat-executor.js +2 -0
  109. package/dist/core/llm/ai-sdk/high-level-phase-specs.js +63 -0
  110. package/dist/core/llm/ai-sdk/message-mapper.js +40 -10
  111. package/dist/core/llm/ai-sdk/provider-factory.js +14 -0
  112. package/dist/core/llm/ai-sdk/request-params.js +74 -1
  113. package/dist/core/llm/ai-sdk/result-mapper.js +16 -0
  114. package/dist/core/llm/ai-sdk.js +112 -27
  115. package/dist/core/llm/capabilities.js +12 -0
  116. package/dist/core/llm/contracts/repair.js +36 -30
  117. package/dist/core/llm/errors.js +83 -2
  118. package/dist/core/llm/message-composition.js +7 -22
  119. package/dist/core/llm/phase-router.js +29 -10
  120. package/dist/core/llm/redact.js +28 -3
  121. package/dist/core/llm/registry.js +2 -0
  122. package/dist/core/llm/request-augmentation.js +55 -0
  123. package/dist/core/llm/request-envelope.js +334 -0
  124. package/dist/core/llm/shared-request-assembly.js +35 -0
  125. package/dist/core/llm/stream-utils.js +13 -4
  126. package/dist/core/llm/utils.js +18 -29
  127. package/dist/core/memory/relevant-retrieval.js +144 -0
  128. package/dist/core/observability/logger.js +11 -2
  129. package/dist/core/patch/diff.js +1 -0
  130. package/dist/core/prompts/registry.js +39 -2
  131. package/dist/core/prompts/runtime.js +50 -12
  132. package/dist/core/prompts/templates/phases/patch_user.hbs +2 -5
  133. package/dist/core/prompts/templates/phases/research_user.hbs +11 -0
  134. package/dist/core/prompts/templates/phases/review_user.hbs +3 -0
  135. package/dist/core/prompts/templates/system/answer_system.hbs +5 -0
  136. package/dist/core/prompts/templates/system/autopilot_system.hbs +11 -0
  137. package/dist/core/prompts/templates/system/explore_system.hbs +14 -23
  138. package/dist/core/prompts/templates/system/main_system.hbs +4 -16
  139. package/dist/core/prompts/templates/system/patch_system.hbs +39 -8
  140. package/dist/core/prompts/templates/system/plan_system.hbs +86 -1
  141. package/dist/core/prompts/templates/system/research_system.hbs +2 -0
  142. package/dist/core/protocols/a2a/agent-card.js +3 -2
  143. package/dist/core/protocols/a2a/sdk/executor.js +8 -6
  144. package/dist/core/protocols/a2a/sdk/server.js +0 -1
  145. package/dist/core/protocols/acp/formal-agent.js +221 -55
  146. package/dist/core/protocols/acp/handlers.js +5 -1
  147. package/dist/core/protocols/acp/permission-provider.js +21 -1
  148. package/dist/core/protocols/shared/execution-request.js +24 -0
  149. package/dist/core/protocols/shared/flow-mode-mapping.js +23 -0
  150. package/dist/core/public-capabilities/flow-mode-metadata.js +39 -0
  151. package/dist/core/public-capabilities/projections.js +29 -0
  152. package/dist/core/public-capabilities/registry.js +26 -0
  153. package/dist/core/public-capabilities/types.js +2 -0
  154. package/dist/core/runtime/agent-server-runtime.js +47 -43
  155. package/dist/core/runtime/execution-profile.js +67 -0
  156. package/dist/core/session/artifact-state.js +160 -0
  157. package/dist/core/session/compaction/index.js +183 -0
  158. package/dist/core/session/compaction/microcompact.js +78 -0
  159. package/dist/core/session/compaction/tracking.js +48 -0
  160. package/dist/core/session/compaction/types.js +11 -0
  161. package/dist/core/session/compression.js +12 -4
  162. package/dist/core/session/manager.js +247 -10
  163. package/dist/core/session/pruning-strategy.js +55 -9
  164. package/dist/core/session/replacement-preview-provider.js +24 -0
  165. package/dist/core/session/replacement-state.js +131 -0
  166. package/dist/core/session/resume-repair/pipeline.js +79 -0
  167. package/dist/core/session/resume-repair/stages/load-raw-archive-state.js +40 -0
  168. package/dist/core/session/resume-repair/stages/reattach-runtime-state.js +8 -0
  169. package/dist/core/session/resume-repair/stages/recover-orphaned-branches.js +10 -0
  170. package/dist/core/session/resume-repair/stages/relink-boundary-and-tail.js +36 -0
  171. package/dist/core/session/resume-repair/stages/replay-startup-hooks.js +23 -0
  172. package/dist/core/session/resume-repair/stages/rescue-stale-metadata.js +17 -0
  173. package/dist/core/session/resume-repair/types.js +2 -0
  174. package/dist/core/session/summary-sync.js +164 -13
  175. package/dist/core/session/token-tracker.js +6 -0
  176. package/dist/core/skills/audit.js +34 -0
  177. package/dist/core/skills/bridge.js +84 -7
  178. package/dist/core/skills/discovery.js +94 -0
  179. package/dist/core/skills/feature-flags.js +52 -0
  180. package/dist/core/skills/index.js +1 -1
  181. package/dist/core/skills/loader.js +195 -20
  182. package/dist/core/skills/parser.js +296 -24
  183. package/dist/core/skills/permissions.js +117 -0
  184. package/dist/core/skills/runtime/MicroTaskRunner.js +10 -4
  185. package/dist/core/skills/runtime/SkillRunner.js +240 -61
  186. package/dist/core/strata/layers/shadow-driver/shadow-driver.js +37 -7
  187. package/dist/core/strata/layers/worktree.js +70 -13
  188. package/dist/core/strata/runtime/synchronizer.js +29 -2
  189. package/dist/core/streaming/stream-assembler.js +75 -31
  190. package/dist/core/sub-agent/context-snapshot.js +156 -0
  191. package/dist/core/sub-agent/core/loop.js +1 -1
  192. package/dist/core/sub-agent/core/manager.js +119 -20
  193. package/dist/core/sub-agent/dispatch-policy.js +29 -0
  194. package/dist/core/sub-agent/prefix-consistency.js +48 -0
  195. package/dist/core/sub-agent/registry-defaults.js +4 -0
  196. package/dist/core/sub-agent/tools/task-spawn.js +79 -2
  197. package/dist/core/sub-agent/types.js +134 -5
  198. package/dist/core/tools/audit.js +13 -4
  199. package/dist/core/tools/builtin/ast-grep.js +1 -1
  200. package/dist/core/tools/builtin/ast.js +1 -1
  201. package/dist/core/tools/builtin/benchmark.js +360 -0
  202. package/dist/core/tools/builtin/code-search/backends/rg.js +2 -1
  203. package/dist/core/tools/builtin/code-search/executor.js +6 -1
  204. package/dist/core/tools/builtin/code-search/spec.js +26 -2
  205. package/dist/core/tools/builtin/fs.js +256 -23
  206. package/dist/core/tools/builtin/git.js +2 -2
  207. package/dist/core/tools/builtin/index.js +51 -2
  208. package/dist/core/tools/builtin/interaction.js +8 -1
  209. package/dist/core/tools/builtin/plan.js +37 -15
  210. package/dist/core/tools/builtin/shell.js +1 -1
  211. package/dist/core/tools/loader.js +39 -16
  212. package/dist/core/tools/mapper.js +17 -3
  213. package/dist/core/tools/parallel/scheduler.js +35 -4
  214. package/dist/core/tools/permissions/permission-rules.js +5 -10
  215. package/dist/core/tools/policy.js +6 -1
  216. package/dist/core/tools/recoverable-tool-errors.js +10 -0
  217. package/dist/core/tools/router.js +24 -6
  218. package/dist/core/tools/session.js +458 -48
  219. package/dist/core/tools/tool-visibility.js +62 -0
  220. package/dist/core/tools/types.js +9 -1
  221. package/dist/core/types/execution.js +4 -0
  222. package/dist/core/types/flow-mode.js +8 -0
  223. package/dist/core/utils/path.js +52 -0
  224. package/dist/core/verification/runner.js +4 -1
  225. package/dist/interfaces/cli/task-runner.js +4 -3
  226. package/dist/languages/typescript/index.js +4 -1
  227. package/dist/locales/en.js +87 -2
  228. package/dist/utils/eol.js +1 -1
  229. package/package.json +15 -8
  230. package/scripts/fix-es-abstract-compat.js +77 -0
  231. package/dist/core/runtime/fastify-server-bundle.js +0 -26
  232. package/dist/core/runtime/sidecar-fastify-plugin.js +0 -35
  233. package/dist/core/runtime/sidecar-paths.js +0 -47
  234. package/dist/core/runtime/sidecar-route-catalog.js +0 -103
@@ -0,0 +1,78 @@
1
+ import { getLogger } from '../../observability/logger.js';
2
+ import { DEFAULT_MICROCOMPACT_CONFIG } from './types.js';
3
+ /**
4
+ * Microcompact is a rule-based context reduction utility.
5
+ * It's idempotent, zero-LLM-cost, and operates on the "View" layer.
6
+ *
7
+ * Rules:
8
+ * - Only affects messages with role === 'assistant' that contain tool results.
9
+ * - Preserves the most recent `keepRecentTurns` rounds.
10
+ * - EXCLUDES "stateful" tools (e.g. cd, export) to avoid environment desync.
11
+ * - Preserves the assistant's thought process (the text part) before tools.
12
+ */
13
+ export function microcompact(messages, config = {}) {
14
+ const mergedConfig = {
15
+ ...DEFAULT_MICROCOMPACT_CONFIG,
16
+ ...config,
17
+ };
18
+ const { keepRecentTurns, placeholder, statefulTools } = mergedConfig;
19
+ // 1. Identify cutoff turn (1 turn = user + assistant pair, usually)
20
+ // We'll keep the last N assistant messages as "recent"
21
+ let assistantCount = 0;
22
+ const cutoffIndex = [...messages].reverse().findIndex((msg) => {
23
+ if (msg.role === 'assistant') {
24
+ assistantCount++;
25
+ }
26
+ return assistantCount > keepRecentTurns;
27
+ });
28
+ // Calculate the absolute index in the original array
29
+ const absCutoffIndex = cutoffIndex === -1 ? -1 : messages.length - 1 - cutoffIndex;
30
+ let totalClearedCount = 0;
31
+ const result = messages.map((msg, index) => {
32
+ // Only process assistant messages BEFORE the cutoff
33
+ if (index > absCutoffIndex || msg.role !== 'assistant' || !msg.content) {
34
+ return msg;
35
+ }
36
+ const { content } = msg;
37
+ // Pattern to match tool results while capturing tool name and content
38
+ // Improved regex to handle attributes more robustly
39
+ const toolResultRegex = /<tool_result\b[^>]*?name="([^"]+)"[^>]*?>([\s\S]*?)<\/tool_result>/g;
40
+ let hasMatched = false;
41
+ const newContent = content.replace(toolResultRegex, (match, toolName, toolOutput) => {
42
+ // Rule: Skip stateful tools
43
+ if (statefulTools.includes(toolName)) {
44
+ return match;
45
+ }
46
+ // Rule: Skip if already cleared
47
+ if (toolOutput.trim() === placeholder) {
48
+ return match;
49
+ }
50
+ hasMatched = true;
51
+ totalClearedCount++;
52
+ // Extract original tag prefix (including attributes) to preserve them
53
+ const tagMatch = match.match(/<tool_result\b[^>]*?>/);
54
+ const tagPrefix = tagMatch ? tagMatch[0] : `<tool_result name="${toolName}">`;
55
+ return `${tagPrefix}${placeholder}</tool_result>`;
56
+ });
57
+ if (!hasMatched) {
58
+ return msg;
59
+ }
60
+ return {
61
+ ...msg,
62
+ content: newContent,
63
+ };
64
+ });
65
+ if (totalClearedCount > 0) {
66
+ getLogger().audit('COMPACTION_MICROCOMPACT', {
67
+ clearedCount: totalClearedCount,
68
+ keepRecentTurns,
69
+ }, {
70
+ source: 'session',
71
+ severity: 'low',
72
+ scope: 'session',
73
+ phase: 'COMPACTION',
74
+ });
75
+ }
76
+ return result;
77
+ }
78
+ //# sourceMappingURL=microcompact.js.map
@@ -0,0 +1,48 @@
1
+ import { randomUUID } from 'crypto';
2
+ /**
3
+ * Initial compaction tracking state
4
+ */
5
+ export function createInitialTracking() {
6
+ return {
7
+ compacted: false,
8
+ turnCounter: 0,
9
+ consecutiveFailures: 0,
10
+ };
11
+ }
12
+ /**
13
+ * Update state on successful compaction
14
+ */
15
+ export function onCompactionSuccess(_prev) {
16
+ return {
17
+ compacted: true,
18
+ compactId: randomUUID(),
19
+ turnCounter: 0,
20
+ consecutiveFailures: 0,
21
+ lastCompactedAt: Date.now(),
22
+ };
23
+ }
24
+ /**
25
+ * Update state on compaction failure (for circuit breaker)
26
+ */
27
+ export function onCompactionFailure(prev) {
28
+ return {
29
+ ...prev,
30
+ consecutiveFailures: prev.consecutiveFailures + 1,
31
+ };
32
+ }
33
+ /**
34
+ * Increment turn counter after successful execution cycle
35
+ */
36
+ export function onNormalTurnComplete(prev) {
37
+ return {
38
+ ...prev,
39
+ turnCounter: prev.turnCounter + 1,
40
+ };
41
+ }
42
+ /**
43
+ * Check if circuit breaker is tripped
44
+ */
45
+ export function isCircuitBreakerTripped(tracking, maxFailures) {
46
+ return tracking.consecutiveFailures >= maxFailures;
47
+ }
48
+ //# sourceMappingURL=tracking.js.map
@@ -0,0 +1,11 @@
1
+ export const DEFAULT_MICROCOMPACT_CONFIG = {
2
+ keepRecentTurns: 3, // Keep last 3 rounds (approx 6 messages)
3
+ placeholder: '[Previous tool output cleared for context efficiency]',
4
+ statefulTools: ['cd', 'export', 'env_set', 'enter_worktree', 'exit_worktree'],
5
+ };
6
+ export const DEFAULT_AUTOCOMPACT_CONFIG = {
7
+ tokenThreshold: 8000,
8
+ maxFailures: 3,
9
+ keepRecentMessages: 10,
10
+ };
11
+ //# sourceMappingURL=types.js.map
@@ -1,6 +1,8 @@
1
1
  import { promisify } from 'util';
2
2
  import { gzip, gunzip } from 'zlib';
3
3
  import { FileAdapter } from '../adapters/fs/index.js';
4
+ import { normalizeSessionArtifactState } from './artifact-state.js';
5
+ import { normalizeToolResultReplacementState, } from './replacement-state.js';
4
6
  export const DEFAULT_COMPRESSION_CONFIG = {
5
7
  maxKeyMessages: 20,
6
8
  maxKeyIterations: 10,
@@ -53,6 +55,9 @@ export class SessionCompressor {
53
55
  originalSize,
54
56
  compressedSize: 0, // Will be updated after serialization
55
57
  compressionRatio: 0, // Will be calculated after serialization
58
+ chatState: session.meta.chatState,
59
+ artifactState: normalizeSessionArtifactState(session.meta.artifactState),
60
+ replacementState: normalizeToolResultReplacementState(session.meta.replacementState),
56
61
  },
57
62
  compressed: {
58
63
  summary: summary.text,
@@ -106,6 +111,9 @@ export class SessionCompressor {
106
111
  successfulIterations: compressed.compressed.stats.successfulIterations,
107
112
  totalTokens: compressed.compressed.stats.totalTokens,
108
113
  snapshots: [], // Will be restored from full data
114
+ chatState: compressed.meta.chatState,
115
+ artifactState: normalizeSessionArtifactState(compressed.meta.artifactState),
116
+ replacementState: normalizeToolResultReplacementState(compressed.meta.replacementState),
109
117
  },
110
118
  messages: compressed.compressed.keyMessages.map((msg) => ({
111
119
  role: msg.role,
@@ -178,7 +186,7 @@ export class SessionCompressor {
178
186
  return stats;
179
187
  }
180
188
  determineOutcome(iteration) {
181
- // 简化的结果判断逻辑
189
+ // Simplified result determination logic
182
190
  if (iteration.result?.success === true)
183
191
  return 'success';
184
192
  if (iteration.result?.success === false)
@@ -186,7 +194,7 @@ export class SessionCompressor {
186
194
  return 'partial';
187
195
  }
188
196
  generateIterationSummary(iteration) {
189
- // 生成迭代的简短摘要
197
+ // Generate brief iteration summary
190
198
  const result = iteration.result;
191
199
  if (!result)
192
200
  return 'No result data';
@@ -198,7 +206,7 @@ export class SessionCompressor {
198
206
  }
199
207
  }
200
208
  countErrors(iteration) {
201
- // 计算迭代中的错误数量
209
+ // Count errors in iteration
202
210
  const result = iteration.result;
203
211
  if (!result)
204
212
  return 0;
@@ -313,7 +321,7 @@ export class CompressedSessionStore {
313
321
  }
314
322
  async readFile(path) {
315
323
  const data = await this.fileAdapter.readFile(path);
316
- // FileAdapter返回base64字符串,需要解码为Uint8Array
324
+ // FileAdapter returns base64 string, need to decode to Uint8Array
317
325
  if (typeof data === 'string') {
318
326
  return new Uint8Array(Buffer.from(data, 'base64'));
319
327
  }
@@ -1,14 +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';
5
+ import { getLogger } from '../observability/logger.js';
6
+ import { parseFlowMode } from '../types/flow-mode.js';
7
+ import { mergeReplacementStateFromArtifactHints, mergeSessionArtifactState, normalizeSessionArtifactState, } from './artifact-state.js';
4
8
  import { SessionCompressor, CompressedSessionStore } from './compression.js';
5
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
+ }
6
42
  /**
7
43
  * Manages chat session persistence and lifecycle.
8
44
  * Storage: .salmonloop/chat-sessions/<id>.json
9
45
  * Features: Auto-pruning, compression, intelligent cleanup
10
46
  */
11
47
  export class ChatSessionManager {
48
+ repoPath;
12
49
  storageDir;
13
50
  currentSession = null;
14
51
  fileAdapter = new FileAdapter();
@@ -16,6 +53,7 @@ export class ChatSessionManager {
16
53
  compressor;
17
54
  compressedStore;
18
55
  constructor(repoPath, pruningStrategy) {
56
+ this.repoPath = repoPath;
19
57
  this.storageDir = join(repoPath, '.salmonloop', 'chat-sessions');
20
58
  this.pruningEngine = new SessionPruningEngine(pruningStrategy);
21
59
  this.compressor = new SessionCompressor();
@@ -86,7 +124,11 @@ export class ChatSessionManager {
86
124
  const filePath = join(this.storageDir, `${targetId}.json`);
87
125
  try {
88
126
  const data = await this.fileAdapter.readFile(filePath);
89
- 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;
90
132
  return this.currentSession;
91
133
  }
92
134
  catch {
@@ -170,6 +212,9 @@ export class ChatSessionManager {
170
212
  getSummaryState() {
171
213
  return this.currentSession?.meta.summaryState;
172
214
  }
215
+ getArtifactState() {
216
+ return normalizeSessionArtifactState(this.currentSession?.meta.artifactState);
217
+ }
173
218
  /**
174
219
  * Update summary state after summarization.
175
220
  */
@@ -178,6 +223,38 @@ export class ChatSessionManager {
178
223
  throw new Error('No active session');
179
224
  this.currentSession.meta.summaryState = state;
180
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
+ }
181
258
  /**
182
259
  * Clear summary state (e.g., on session reset).
183
260
  */
@@ -254,11 +331,14 @@ export class ChatSessionManager {
254
331
  const filePath = join(this.storageDir, file);
255
332
  const data = await this.fileAdapter.readFile(filePath);
256
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);
257
337
  sessions.push(session);
258
338
  }
259
339
  catch (error) {
260
340
  // Skip corrupted session files
261
- console.warn(`Failed to load session file ${file}:`, error);
341
+ getLogger().warn(`Failed to load session file ${file}: ${error}`);
262
342
  }
263
343
  }
264
344
  return sessions;
@@ -272,7 +352,7 @@ export class ChatSessionManager {
272
352
  await this.fileAdapter.deleteFile(filePath);
273
353
  }
274
354
  catch (error) {
275
- console.warn(`Failed to delete session ${sessionId}:`, error);
355
+ getLogger().warn(`Failed to delete session ${sessionId}: ${error}`);
276
356
  }
277
357
  }
278
358
  /**
@@ -297,17 +377,174 @@ export class ChatSessionManager {
297
377
  * List archived sessions
298
378
  */
299
379
  async listArchivedSessions() {
300
- // Implement archived session list functionality
301
- // This needs to access the compressed storage
302
- 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);
303
402
  }
304
403
  /**
305
404
  * Restore session from archive
306
405
  */
307
- async restoreFromArchive(_archiveId) {
308
- // Implement session restoration from archive functionality
309
- // This needs to access the compressed storage and decompress
310
- 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;
311
548
  }
312
549
  }
313
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