salmon-loop 0.2.13 → 0.3.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.
- package/dist/cli/argv/headless-detection.js +27 -0
- package/dist/cli/chat-flow.js +11 -0
- package/dist/cli/chat.js +160 -24
- package/dist/cli/commands/chat.js +14 -7
- package/dist/cli/commands/flow-mode.js +63 -0
- package/dist/cli/commands/registry.js +2 -0
- package/dist/cli/commands/run/benchmark-artifacts.js +41 -0
- package/dist/cli/commands/run/early-errors.js +23 -0
- package/dist/cli/commands/run/handler.js +115 -27
- package/dist/cli/commands/run/headless-error-writer.js +8 -0
- package/dist/cli/commands/run/loop-params.js +2 -0
- package/dist/cli/commands/run/mode.js +2 -5
- package/dist/cli/commands/run/parse-options.js +16 -0
- package/dist/cli/commands/run/persist-session.js +10 -1
- package/dist/cli/commands/run/preflight.js +10 -0
- package/dist/cli/commands/run/reporter-factory.js +4 -0
- package/dist/cli/commands/run/runtime-llm.js +38 -11
- package/dist/cli/commands/run/runtime-options.js +2 -2
- package/dist/cli/commands/serve.js +97 -77
- package/dist/cli/commands/tool-names.js +78 -78
- package/dist/cli/headless/anthropic-stream-normalized-encoder.js +6 -1
- package/dist/cli/headless/json-protocol.js +37 -0
- package/dist/cli/headless/native-stream-normalized-encoder.js +6 -1
- package/dist/cli/headless/protocol-metadata.js +22 -0
- package/dist/cli/headless/stream-json-protocol.js +34 -1
- package/dist/cli/index.js +6 -4
- package/dist/cli/locales/en.js +30 -6
- package/dist/cli/program-bootstrap.js +10 -5
- package/dist/cli/program-commands.js +5 -1
- package/dist/cli/reporters/anthropic-stream.js +7 -1
- package/dist/cli/reporters/json.js +4 -0
- package/dist/cli/reporters/stream-json.js +17 -2
- package/dist/cli/run-cli.js +5 -3
- package/dist/cli/slash/runtime.js +27 -12
- package/dist/cli/ui/components/CommandInput.js +7 -3
- package/dist/cli/ui/components/CommandSuggestionList.js +1 -1
- package/dist/cli/utils/command-option-source.js +13 -0
- package/dist/cli/utils/verify-resolver.js +8 -4
- package/dist/cli/utils/worktree-prepare-resolver.js +7 -3
- package/dist/core/adapters/fs/file-adapter.js +6 -0
- package/dist/core/adapters/fs/filesystem.js +2 -1
- package/dist/core/adapters/git/git-adapter.js +78 -1
- package/dist/core/backends/salmon-loop/task-executor.js +1 -0
- package/dist/core/benchmark/patch-artifact.js +124 -0
- package/dist/core/benchmark/swe-bench.js +25 -0
- package/dist/core/config/load.js +18 -11
- package/dist/core/config/resolve-llm.js +12 -0
- package/dist/core/config/resolvers/server.js +0 -6
- package/dist/core/config/validate.js +73 -21
- package/dist/core/context/gatherers/metadata-gatherer.js +1 -0
- package/dist/core/context/gatherers/ripgrep-gatherer.js +84 -2
- package/dist/core/context/keywords.js +18 -4
- package/dist/core/context/service-deps.js +2 -2
- package/dist/core/context/service.js +8 -0
- package/dist/core/context/steps/context-gather.js +38 -0
- package/dist/core/context/summarization/summarizer.js +55 -12
- package/dist/core/context/targeting/target-resolver.js +4 -4
- package/dist/core/extensions/index.js +23 -5
- package/dist/core/extensions/merge.js +14 -0
- package/dist/core/extensions/paths.js +31 -0
- package/dist/core/extensions/schemas.js +8 -5
- package/dist/core/facades/cli-chat.js +6 -2
- package/dist/core/facades/cli-command-chat.js +1 -0
- package/dist/core/facades/cli-command-tool-names.js +2 -0
- package/dist/core/facades/cli-observability.js +1 -1
- package/dist/core/facades/cli-program-bootstrap.js +1 -0
- package/dist/core/facades/cli-run-handler.js +4 -2
- package/dist/core/facades/cli-run-persist-session.js +1 -0
- package/dist/core/facades/cli-serve.js +4 -4
- package/dist/core/facades/cli-utils-worktree.js +1 -1
- package/dist/core/failure/diagnostics.js +53 -1
- package/dist/core/grizzco/dsl/llm-strategy.js +4 -1
- package/dist/core/grizzco/engine/outcome/loop-result-mapper.js +67 -9
- package/dist/core/grizzco/engine/pipeline/pipeline.js +6 -2
- package/dist/core/grizzco/engine/transaction/attempt-failure.js +90 -15
- package/dist/core/grizzco/engine/transaction/report-mapper.js +17 -3
- package/dist/core/grizzco/engine/transaction/transaction-runner.js +165 -7
- package/dist/core/grizzco/flows/AutopilotFlow.js +18 -0
- package/dist/core/grizzco/flows/flow-dispatch.js +11 -0
- package/dist/core/grizzco/steps/answer.js +13 -14
- package/dist/core/grizzco/steps/autopilot.js +396 -0
- package/dist/core/grizzco/steps/cache-sharing.js +29 -0
- package/dist/core/grizzco/steps/explore.js +37 -21
- package/dist/core/grizzco/steps/generateReview.js +2 -5
- package/dist/core/grizzco/steps/patch/apply-check.js +10 -0
- package/dist/core/grizzco/steps/patch/diff-normalization.js +70 -0
- package/dist/core/grizzco/steps/patch/diff-salvage.js +46 -0
- package/dist/core/grizzco/steps/patch/prompt-input.js +42 -0
- package/dist/core/grizzco/steps/patch.js +105 -146
- package/dist/core/grizzco/steps/plan.js +101 -25
- package/dist/core/grizzco/steps/preflight.js +5 -6
- package/dist/core/grizzco/steps/request-assembly.js +78 -0
- package/dist/core/grizzco/steps/research.js +39 -36
- package/dist/core/grizzco/steps/tool-runtime.js +47 -0
- package/dist/core/grizzco/steps/verify-shared.js +23 -0
- package/dist/core/grizzco/steps/verify.js +13 -21
- package/dist/core/interaction/orchestration/facade.js +1 -1
- package/dist/core/llm/ai-sdk/chat-executor.js +2 -0
- package/dist/core/llm/ai-sdk/high-level-phase-specs.js +63 -0
- package/dist/core/llm/ai-sdk/message-mapper.js +40 -10
- package/dist/core/llm/ai-sdk/provider-factory.js +14 -0
- package/dist/core/llm/ai-sdk/request-params.js +113 -1
- package/dist/core/llm/ai-sdk/result-mapper.js +16 -0
- package/dist/core/llm/ai-sdk.js +112 -27
- package/dist/core/llm/capabilities.js +12 -0
- package/dist/core/llm/contracts/repair.js +36 -30
- package/dist/core/llm/errors.js +83 -2
- package/dist/core/llm/message-composition.js +7 -22
- package/dist/core/llm/phase-router.js +29 -10
- package/dist/core/llm/redact.js +28 -3
- package/dist/core/llm/registry.js +2 -0
- package/dist/core/llm/request-augmentation.js +55 -0
- package/dist/core/llm/request-envelope.js +334 -0
- package/dist/core/llm/shared-request-assembly.js +35 -0
- package/dist/core/llm/stream-utils.js +13 -4
- package/dist/core/llm/utils.js +18 -29
- package/dist/core/memory/relevant-retrieval.js +144 -0
- package/dist/core/observability/logger.js +11 -2
- package/dist/core/patch/diff.js +1 -0
- package/dist/core/prompts/registry.js +39 -2
- package/dist/core/prompts/runtime.js +50 -12
- package/dist/core/prompts/templates/phases/patch_user.hbs +2 -5
- package/dist/core/prompts/templates/phases/research_user.hbs +11 -0
- package/dist/core/prompts/templates/phases/review_user.hbs +3 -0
- package/dist/core/prompts/templates/system/answer_system.hbs +5 -0
- package/dist/core/prompts/templates/system/autopilot_system.hbs +11 -0
- package/dist/core/prompts/templates/system/explore_system.hbs +14 -23
- package/dist/core/prompts/templates/system/main_system.hbs +4 -16
- package/dist/core/prompts/templates/system/patch_system.hbs +39 -8
- package/dist/core/prompts/templates/system/plan_system.hbs +86 -1
- package/dist/core/prompts/templates/system/research_system.hbs +2 -0
- package/dist/core/protocols/a2a/agent-card.js +5 -3
- package/dist/core/protocols/a2a/sdk/executor.js +2 -1
- package/dist/core/protocols/a2a/sdk/server.js +0 -1
- package/dist/core/protocols/acp/formal-agent.js +300 -58
- package/dist/core/protocols/acp/handlers.js +5 -1
- package/dist/core/protocols/acp/permission-provider.js +1 -1
- package/dist/core/protocols/shared/flow-mode-mapping.js +23 -0
- package/dist/core/public-capabilities/flow-mode-metadata.js +39 -0
- package/dist/core/public-capabilities/projections.js +29 -0
- package/dist/core/public-capabilities/registry.js +26 -0
- package/dist/core/public-capabilities/types.js +2 -0
- package/dist/core/runtime/agent-server-runtime.js +47 -43
- package/dist/core/runtime/execution-profile.js +67 -0
- package/dist/core/session/artifact-state.js +160 -0
- package/dist/core/session/compaction/index.js +183 -0
- package/dist/core/session/compaction/microcompact.js +78 -0
- package/dist/core/session/compaction/tracking.js +48 -0
- package/dist/core/session/compaction/types.js +11 -0
- package/dist/core/session/compression.js +8 -0
- package/dist/core/session/manager.js +244 -8
- package/dist/core/session/pruning-strategy.js +55 -9
- package/dist/core/session/replacement-preview-provider.js +24 -0
- package/dist/core/session/replacement-state.js +131 -0
- package/dist/core/session/resume-repair/pipeline.js +79 -0
- package/dist/core/session/resume-repair/stages/load-raw-archive-state.js +40 -0
- package/dist/core/session/resume-repair/stages/reattach-runtime-state.js +8 -0
- package/dist/core/session/resume-repair/stages/recover-orphaned-branches.js +10 -0
- package/dist/core/session/resume-repair/stages/relink-boundary-and-tail.js +36 -0
- package/dist/core/session/resume-repair/stages/replay-startup-hooks.js +23 -0
- package/dist/core/session/resume-repair/stages/rescue-stale-metadata.js +17 -0
- package/dist/core/session/resume-repair/types.js +2 -0
- package/dist/core/session/summary-sync.js +164 -13
- package/dist/core/session/token-tracker.js +6 -0
- package/dist/core/skills/audit.js +34 -0
- package/dist/core/skills/bridge.js +84 -7
- package/dist/core/skills/discovery.js +94 -0
- package/dist/core/skills/feature-flags.js +52 -0
- package/dist/core/skills/index.js +1 -1
- package/dist/core/skills/loader.js +195 -20
- package/dist/core/skills/parser.js +296 -24
- package/dist/core/skills/permissions.js +117 -0
- package/dist/core/skills/runtime/MicroTaskRunner.js +10 -4
- package/dist/core/skills/runtime/SkillRunner.js +240 -61
- package/dist/core/strata/layers/shadow-driver/shadow-driver.js +37 -7
- package/dist/core/strata/layers/worktree.js +67 -10
- package/dist/core/strata/runtime/synchronizer.js +29 -2
- package/dist/core/streaming/stream-assembler.js +75 -31
- package/dist/core/sub-agent/context-snapshot.js +156 -0
- package/dist/core/sub-agent/core/loop.js +1 -1
- package/dist/core/sub-agent/core/manager.js +119 -20
- package/dist/core/sub-agent/dispatch-policy.js +29 -0
- package/dist/core/sub-agent/prefix-consistency.js +48 -0
- package/dist/core/sub-agent/registry-defaults.js +4 -0
- package/dist/core/sub-agent/tools/task-spawn.js +79 -2
- package/dist/core/sub-agent/types.js +134 -5
- package/dist/core/tools/audit.js +13 -4
- package/dist/core/tools/builtin/ast-grep.js +1 -1
- package/dist/core/tools/builtin/ast.js +1 -1
- package/dist/core/tools/builtin/benchmark.js +360 -0
- package/dist/core/tools/builtin/code-search/backends/rg.js +2 -1
- package/dist/core/tools/builtin/code-search/executor.js +6 -1
- package/dist/core/tools/builtin/code-search/spec.js +26 -2
- package/dist/core/tools/builtin/fs.js +256 -23
- package/dist/core/tools/builtin/git.js +2 -2
- package/dist/core/tools/builtin/index.js +51 -2
- package/dist/core/tools/builtin/interaction.js +8 -1
- package/dist/core/tools/builtin/plan.js +37 -15
- package/dist/core/tools/builtin/shell.js +1 -1
- package/dist/core/tools/loader.js +39 -16
- package/dist/core/tools/mapper.js +17 -3
- package/dist/core/tools/mcp/client.js +2 -1
- package/dist/core/tools/parallel/scheduler.js +35 -4
- package/dist/core/tools/permissions/permission-rules.js +5 -10
- package/dist/core/tools/policy.js +6 -1
- package/dist/core/tools/recoverable-tool-errors.js +10 -0
- package/dist/core/tools/router.js +24 -6
- package/dist/core/tools/session.js +458 -48
- package/dist/core/tools/tool-visibility.js +62 -0
- package/dist/core/tools/types.js +9 -1
- package/dist/core/types/execution.js +4 -0
- package/dist/core/types/flow-mode.js +8 -0
- package/dist/core/utils/path.js +52 -0
- package/dist/core/verification/runner.js +4 -1
- package/dist/core/version.js +17 -0
- package/dist/languages/typescript/index.js +4 -1
- package/dist/locales/en.js +35 -2
- package/dist/utils/eol.js +1 -1
- package/package.json +14 -7
- package/scripts/fix-es-abstract-compat.js +77 -0
- package/dist/core/runtime/fastify-server-bundle.js +0 -26
- package/dist/core/runtime/sidecar-fastify-plugin.js +0 -35
- package/dist/core/runtime/sidecar-paths.js +0 -47
- 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,
|
|
@@ -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
|
-
|
|
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
|
-
|
|
302
|
-
|
|
303
|
-
|
|
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(
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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(
|
|
144
|
+
async restoreFromArchive(archiveId) {
|
|
135
145
|
try {
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
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
|
-
|
|
146
|
-
// The SessionArchiver is currently a placeholder for future implementation
|
|
186
|
+
await this.fileAdapter.mkdir(this.archiveDir);
|
|
147
187
|
}
|
|
148
|
-
async writeCompressedData(
|
|
149
|
-
|
|
150
|
-
|
|
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
|