@cjhyy/code-shell-core 0.5.0-rc.2 → 0.6.0-rc.2
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/README.md +13 -10
- package/dist/agent/agent-definition-registry.d.ts +3 -0
- package/dist/agent/agent-definition-registry.js +3 -1
- package/dist/agent/agent-definition.d.ts +15 -0
- package/dist/agent/agent-definition.js +27 -3
- package/dist/arena/context/context-tools.js +47 -4
- package/dist/arena/ledger.js +9 -1
- package/dist/arena/strategies/utils.d.ts +1 -4
- package/dist/arena/strategies/utils.js +7 -84
- package/dist/automation/cron-expr.d.ts +10 -0
- package/dist/automation/cron-expr.js +23 -5
- package/dist/automation/runner.d.ts +21 -4
- package/dist/automation/runner.js +48 -12
- package/dist/automation/scheduler.d.ts +38 -0
- package/dist/automation/scheduler.js +89 -12
- package/dist/automation/store.js +5 -2
- package/dist/capability-control/disabled-lists.d.ts +25 -0
- package/dist/capability-control/disabled-lists.js +57 -0
- package/dist/capability-control/overlay.d.ts +15 -0
- package/dist/capability-control/overlay.js +27 -0
- package/dist/cc-orchestrator/agent-adapter.d.ts +50 -0
- package/dist/cc-orchestrator/agent-adapter.js +133 -0
- package/dist/cc-orchestrator/cc-capability.d.ts +19 -0
- package/dist/cc-orchestrator/cc-capability.js +53 -0
- package/dist/cc-orchestrator/codex-session-discovery.d.ts +24 -0
- package/dist/cc-orchestrator/codex-session-discovery.js +191 -0
- package/dist/cc-orchestrator/codex-session-history.d.ts +25 -0
- package/dist/cc-orchestrator/codex-session-history.js +187 -0
- package/dist/cc-orchestrator/external-agent-changes.d.ts +19 -0
- package/dist/cc-orchestrator/external-agent-changes.js +214 -0
- package/dist/cc-orchestrator/external-agent-driver.d.ts +18 -0
- package/dist/cc-orchestrator/external-agent-driver.js +69 -0
- package/dist/cc-orchestrator/index.d.ts +8 -0
- package/dist/cc-orchestrator/index.js +8 -0
- package/dist/cc-orchestrator/relevance-judge.d.ts +15 -0
- package/dist/cc-orchestrator/relevance-judge.js +29 -0
- package/dist/cc-orchestrator/session-discovery.d.ts +46 -0
- package/dist/cc-orchestrator/session-discovery.js +125 -0
- package/dist/cc-orchestrator/session-history.d.ts +19 -0
- package/dist/cc-orchestrator/session-history.js +67 -0
- package/dist/cli/agent-server-stdio.d.ts +15 -1
- package/dist/cli/agent-server-stdio.js +115 -17
- package/dist/cli/agent-server-tcp.js +40 -26
- package/dist/context/compaction.d.ts +56 -0
- package/dist/context/compaction.js +186 -0
- package/dist/context/manager.js +25 -4
- package/dist/cost-tracker.js +5 -61
- package/dist/credentials/cipher.d.ts +49 -0
- package/dist/credentials/cipher.js +45 -0
- package/dist/credentials/cookie-jar.d.ts +24 -0
- package/dist/credentials/cookie-jar.js +40 -0
- package/dist/credentials/index.d.ts +7 -0
- package/dist/credentials/index.js +5 -0
- package/dist/credentials/inject-credential-tool.d.ts +20 -0
- package/dist/credentials/inject-credential-tool.js +130 -0
- package/dist/credentials/store.d.ts +72 -0
- package/dist/credentials/store.js +184 -0
- package/dist/credentials/types.d.ts +56 -0
- package/dist/credentials/use-credential-tool.d.ts +29 -0
- package/dist/credentials/use-credential-tool.js +205 -0
- package/dist/credentials/use-gate.d.ts +56 -0
- package/dist/credentials/use-gate.js +52 -0
- package/dist/data/model-metadata.d.ts +77 -0
- package/dist/data/model-metadata.js +56 -0
- package/dist/data/model-metadata.json +216 -0
- package/dist/data/openrouter-models.d.ts +18 -7
- package/dist/data/openrouter-models.js +35 -8
- package/dist/engine/aux-key.d.ts +10 -0
- package/dist/engine/aux-key.js +11 -0
- package/dist/engine/dynamic-tool-defs.d.ts +19 -0
- package/dist/engine/dynamic-tool-defs.js +36 -0
- package/dist/engine/engine.d.ts +267 -138
- package/dist/engine/engine.js +957 -191
- package/dist/engine/friendly-error.d.ts +18 -0
- package/dist/engine/friendly-error.js +63 -0
- package/dist/engine/goal.d.ts +145 -0
- package/dist/engine/goal.js +144 -0
- package/dist/engine/image-policy.d.ts +13 -0
- package/dist/engine/image-policy.js +24 -0
- package/dist/engine/model-connections-pool.d.ts +17 -0
- package/dist/engine/model-connections-pool.js +66 -0
- package/dist/engine/model-facade.d.ts +10 -0
- package/dist/engine/model-facade.js +25 -0
- package/dist/engine/patch-orphaned-tools.js +3 -0
- package/dist/engine/resolve-llm-config.d.ts +16 -0
- package/dist/engine/resolve-llm-config.js +44 -0
- package/dist/engine/runtime.js +2 -1
- package/dist/engine/sandbox-cache-key.d.ts +10 -0
- package/dist/engine/sandbox-cache-key.js +9 -0
- package/dist/engine/sandbox-config.d.ts +31 -0
- package/dist/engine/sandbox-config.js +38 -0
- package/dist/engine/session-usage.d.ts +19 -0
- package/dist/engine/session-usage.js +25 -0
- package/dist/engine/steer-queue.d.ts +32 -0
- package/dist/engine/steer-queue.js +26 -0
- package/dist/engine/streaming-tool-queue.d.ts +12 -0
- package/dist/engine/streaming-tool-queue.js +50 -10
- package/dist/engine/turn-loop.d.ts +67 -3
- package/dist/engine/turn-loop.js +292 -41
- package/dist/engine/types.d.ts +175 -0
- package/dist/engine/types.js +13 -0
- package/dist/external-agents/config.d.ts +2 -0
- package/dist/external-agents/config.js +15 -0
- package/dist/external-agents/types.d.ts +31 -0
- package/dist/external-agents/types.js +1 -0
- package/dist/git/utils.js +11 -6
- package/dist/git/worktree.d.ts +48 -0
- package/dist/git/worktree.js +86 -10
- package/dist/hooks/goal-stop-hook.d.ts +28 -0
- package/dist/hooks/goal-stop-hook.js +186 -9
- package/dist/hooks/registry.d.ts +8 -0
- package/dist/hooks/registry.js +19 -0
- package/dist/hooks/shell-runner.js +10 -21
- package/dist/index.d.ts +49 -6
- package/dist/index.js +60 -4
- package/dist/llm/capabilities/param-specs.d.ts +14 -0
- package/dist/llm/capabilities/param-specs.js +62 -0
- package/dist/llm/capabilities/rules.js +4 -0
- package/dist/llm/capabilities/types.d.ts +10 -0
- package/dist/llm/client-base.d.ts +28 -1
- package/dist/llm/client-base.js +119 -13
- package/dist/llm/model-cache.js +4 -2
- package/dist/llm/model-pool.d.ts +14 -0
- package/dist/llm/model-pool.js +13 -0
- package/dist/llm/provider-auth.d.ts +41 -0
- package/dist/llm/provider-auth.js +76 -0
- package/dist/llm/provider-catalog.d.ts +4 -0
- package/dist/llm/providers/anthropic.js +64 -14
- package/dist/llm/providers/openai.d.ts +36 -0
- package/dist/llm/providers/openai.js +257 -51
- package/dist/llm/reasoning-setting.d.ts +3 -3
- package/dist/llm/reasoning-setting.js +9 -1
- package/dist/llm/stream-watchdog.js +5 -1
- package/dist/llm/token-counter.js +9 -2
- package/dist/llm/types.d.ts +4 -0
- package/dist/logging/sanitize-messages.js +22 -0
- package/dist/lsp/manager.d.ts +1 -1
- package/dist/lsp/manager.js +40 -10
- package/dist/model-catalog/builtin.d.ts +12 -0
- package/dist/model-catalog/builtin.js +407 -0
- package/dist/model-catalog/gen-connections.d.ts +20 -0
- package/dist/model-catalog/gen-connections.js +28 -0
- package/dist/model-catalog/index.d.ts +41 -0
- package/dist/model-catalog/index.js +90 -0
- package/dist/model-catalog/params.d.ts +20 -0
- package/dist/model-catalog/params.js +45 -0
- package/dist/model-catalog/resolve.d.ts +48 -0
- package/dist/model-catalog/resolve.js +33 -0
- package/dist/model-catalog/save-entry.d.ts +32 -0
- package/dist/model-catalog/save-entry.js +104 -0
- package/dist/model-catalog/types.d.ts +561 -0
- package/dist/model-catalog/types.js +93 -0
- package/dist/model-catalog/upsert.d.ts +9 -0
- package/dist/model-catalog/upsert.js +8 -0
- package/dist/onboarding.d.ts +12 -82
- package/dist/onboarding.js +61 -326
- package/dist/plugins/gitOps.d.ts +19 -0
- package/dist/plugins/gitOps.js +73 -4
- package/dist/plugins/installer/checkUpdate.d.ts +16 -0
- package/dist/plugins/installer/checkUpdate.js +32 -0
- package/dist/plugins/installer/codex/convertCommands.d.ts +19 -0
- package/dist/plugins/installer/codex/convertCommands.js +46 -0
- package/dist/plugins/installer/codex/convertMcp.d.ts +5 -2
- package/dist/plugins/installer/codex/convertMcp.js +45 -5
- package/dist/plugins/installer/install.js +25 -1
- package/dist/plugins/installer/installFromArchive.d.ts +43 -0
- package/dist/plugins/installer/installFromArchive.js +134 -0
- package/dist/plugins/installer/installFromSource.js +8 -2
- package/dist/plugins/installer/loadPluginAgents.js +6 -2
- package/dist/plugins/installer/loadPluginMcp.d.ts +9 -2
- package/dist/plugins/installer/loadPluginMcp.js +35 -2
- package/dist/plugins/installer/pruneDisabled.d.ts +24 -0
- package/dist/plugins/installer/pruneDisabled.js +73 -0
- package/dist/plugins/installer/types.d.ts +5 -2
- package/dist/plugins/installer/types.js +1 -0
- package/dist/plugins/installer/uninstall.js +4 -0
- package/dist/plugins/installer/unzip.d.ts +14 -0
- package/dist/plugins/installer/unzip.js +82 -0
- package/dist/plugins/installer/update.d.ts +14 -0
- package/dist/plugins/installer/update.js +53 -21
- package/dist/plugins/loadPluginHooks.d.ts +47 -1
- package/dist/plugins/loadPluginHooks.js +73 -1
- package/dist/plugins/marketplaceManager.d.ts +7 -0
- package/dist/plugins/marketplaceManager.js +20 -0
- package/dist/plugins/pluginCommandHook.js +4 -18
- package/dist/plugins/pluginContent.d.ts +30 -0
- package/dist/plugins/pluginContent.js +83 -0
- package/dist/plugins/pluginInstaller.d.ts +13 -0
- package/dist/plugins/pluginInstaller.js +42 -8
- package/dist/plugins/schemas.js +1 -0
- package/dist/plugins/types.d.ts +6 -0
- package/dist/preset/index.d.ts +7 -1
- package/dist/preset/index.js +96 -6
- package/dist/product/types.d.ts +1 -1
- package/dist/prompt/composer.d.ts +27 -0
- package/dist/prompt/composer.js +67 -21
- package/dist/prompt/instruction-scanner.js +5 -3
- package/dist/prompt/section-loader.js +1 -0
- package/dist/prompt/sections/base.md +1 -0
- package/dist/prompt/sections/browser.md +10 -0
- package/dist/prompt/sections/coding.md +4 -0
- package/dist/protocol/chat-session-manager.d.ts +8 -2
- package/dist/protocol/chat-session-manager.js +36 -0
- package/dist/protocol/chat-session.d.ts +65 -0
- package/dist/protocol/chat-session.js +94 -2
- package/dist/protocol/client.d.ts +27 -0
- package/dist/protocol/client.js +38 -0
- package/dist/protocol/server.d.ts +112 -0
- package/dist/protocol/server.js +586 -16
- package/dist/protocol/transport.js +3 -2
- package/dist/protocol/types.d.ts +46 -0
- package/dist/protocol/types.js +16 -0
- package/dist/quota/credentials.d.ts +3 -0
- package/dist/quota/credentials.js +80 -0
- package/dist/quota/index.d.ts +36 -0
- package/dist/quota/index.js +155 -0
- package/dist/quota/types.d.ts +48 -0
- package/dist/quota/types.js +13 -0
- package/dist/review/review-prompt.d.ts +28 -0
- package/dist/review/review-prompt.js +81 -0
- package/dist/run/FileRunStore.js +8 -3
- package/dist/run/RunApprovalBackend.js +25 -5
- package/dist/run/RunManager.d.ts +12 -0
- package/dist/run/RunManager.js +35 -0
- package/dist/run/factory.d.ts +1 -1
- package/dist/runtime/background-shell.d.ts +139 -0
- package/dist/runtime/background-shell.js +495 -0
- package/dist/runtime/output-clean.d.ts +24 -0
- package/dist/runtime/output-clean.js +41 -0
- package/dist/runtime/ring-file.d.ts +64 -0
- package/dist/runtime/ring-file.js +174 -0
- package/dist/runtime/safe-spawn.js +15 -34
- package/dist/runtime/spawn-common.d.ts +148 -0
- package/dist/runtime/spawn-common.js +303 -0
- package/dist/runtime/truncate-output.d.ts +22 -0
- package/dist/runtime/truncate-output.js +49 -0
- package/dist/runtime/utf8-cut.d.ts +11 -0
- package/dist/runtime/utf8-cut.js +23 -0
- package/dist/services/auto-dream.d.ts +4 -0
- package/dist/services/auto-dream.js +26 -26
- package/dist/services/diagnostics.d.ts +1 -2
- package/dist/services/diagnostics.js +12 -7
- package/dist/services/extract-memories.d.ts +14 -1
- package/dist/services/extract-memories.js +45 -6
- package/dist/services/memory-orchestrator.d.ts +21 -0
- package/dist/services/memory-orchestrator.js +125 -50
- package/dist/services/session-memory.js +24 -12
- package/dist/session/file-history.d.ts +124 -1
- package/dist/session/file-history.js +222 -6
- package/dist/session/memory.d.ts +116 -2
- package/dist/session/memory.js +250 -28
- package/dist/session/session-manager.d.ts +40 -0
- package/dist/session/session-manager.js +113 -3
- package/dist/session/simple-diff.d.ts +23 -0
- package/dist/session/simple-diff.js +84 -0
- package/dist/session/transcript.d.ts +25 -1
- package/dist/session/transcript.js +35 -2
- package/dist/session/undo-target.d.ts +67 -0
- package/dist/session/undo-target.js +144 -0
- package/dist/settings/disk-defaults.d.ts +9 -2
- package/dist/settings/disk-defaults.js +11 -2
- package/dist/settings/feature-flags.d.ts +64 -0
- package/dist/settings/feature-flags.js +61 -0
- package/dist/settings/manager.d.ts +68 -1
- package/dist/settings/manager.js +266 -29
- package/dist/settings/migrate-config.d.ts +45 -0
- package/dist/settings/migrate-config.js +125 -0
- package/dist/settings/schema-export.d.ts +25 -0
- package/dist/settings/schema-export.js +38 -0
- package/dist/settings/schema.d.ts +1186 -771
- package/dist/settings/schema.js +268 -97
- package/dist/skills/scanner.d.ts +9 -0
- package/dist/skills/scanner.js +30 -2
- package/dist/stt/resolve-transcribe.d.ts +31 -0
- package/dist/stt/resolve-transcribe.js +108 -0
- package/dist/stt/transcribe.d.ts +51 -0
- package/dist/stt/transcribe.js +65 -0
- package/dist/tool-system/browser-bridge.d.ts +226 -0
- package/dist/tool-system/browser-bridge.js +163 -0
- package/dist/tool-system/builtin/agent-heartbeat.d.ts +49 -0
- package/dist/tool-system/builtin/agent-heartbeat.js +89 -0
- package/dist/tool-system/builtin/agent-notifications.d.ts +12 -3
- package/dist/tool-system/builtin/agent-notifications.js +9 -3
- package/dist/tool-system/builtin/agent-output-file.d.ts +38 -0
- package/dist/tool-system/builtin/agent-output-file.js +72 -0
- package/dist/tool-system/builtin/agent-registry.d.ts +12 -0
- package/dist/tool-system/builtin/agent-registry.js +8 -0
- package/dist/tool-system/builtin/agent.d.ts +21 -1
- package/dist/tool-system/builtin/agent.js +489 -42
- package/dist/tool-system/builtin/apply-patch/applier.js +66 -8
- package/dist/tool-system/builtin/apply-patch/backup-targets.d.ts +10 -0
- package/dist/tool-system/builtin/apply-patch/backup-targets.js +30 -0
- package/dist/tool-system/builtin/apply-patch/index.js +0 -15
- package/dist/tool-system/builtin/background-jobs.d.ts +76 -0
- package/dist/tool-system/builtin/background-jobs.js +124 -0
- package/dist/tool-system/builtin/background-shell-tools.d.ts +20 -0
- package/dist/tool-system/builtin/background-shell-tools.js +108 -0
- package/dist/tool-system/builtin/background-work.d.ts +67 -0
- package/dist/tool-system/builtin/background-work.js +86 -0
- package/dist/tool-system/builtin/bash-output-style.d.ts +32 -0
- package/dist/tool-system/builtin/bash-output-style.js +40 -0
- package/dist/tool-system/builtin/bash.d.ts +5 -2
- package/dist/tool-system/builtin/bash.js +98 -62
- package/dist/tool-system/builtin/browser-tools.d.ts +33 -0
- package/dist/tool-system/builtin/browser-tools.js +312 -0
- package/dist/tool-system/builtin/cancel-goal.d.ts +31 -0
- package/dist/tool-system/builtin/cancel-goal.js +64 -0
- package/dist/tool-system/builtin/check-quota.d.ts +15 -0
- package/dist/tool-system/builtin/check-quota.js +34 -0
- package/dist/tool-system/builtin/config.js +7 -0
- package/dist/tool-system/builtin/cron.d.ts +7 -0
- package/dist/tool-system/builtin/cron.js +55 -3
- package/dist/tool-system/builtin/drive-claude-code.d.ts +30 -0
- package/dist/tool-system/builtin/drive-claude-code.js +157 -0
- package/dist/tool-system/builtin/edit-model-catalog.d.ts +3 -0
- package/dist/tool-system/builtin/edit-model-catalog.js +94 -0
- package/dist/tool-system/builtin/edit.js +20 -16
- package/dist/tool-system/builtin/eol.d.ts +29 -0
- package/dist/tool-system/builtin/eol.js +37 -0
- package/dist/tool-system/builtin/file-cache.d.ts +6 -0
- package/dist/tool-system/builtin/file-cache.js +8 -0
- package/dist/tool-system/builtin/generate-image.d.ts +35 -0
- package/dist/tool-system/builtin/generate-image.js +278 -50
- package/dist/tool-system/builtin/generate-video.d.ts +55 -0
- package/dist/tool-system/builtin/generate-video.js +361 -0
- package/dist/tool-system/builtin/glob.js +0 -7
- package/dist/tool-system/builtin/grep.js +6 -8
- package/dist/tool-system/builtin/image-providers.d.ts +86 -0
- package/dist/tool-system/builtin/image-providers.js +190 -0
- package/dist/tool-system/builtin/image-uploader.d.ts +33 -0
- package/dist/tool-system/builtin/image-uploader.js +74 -0
- package/dist/tool-system/builtin/index.d.ts +7 -1
- package/dist/tool-system/builtin/index.js +249 -25
- package/dist/tool-system/builtin/mcp-tools.js +23 -3
- package/dist/tool-system/builtin/memory.js +45 -7
- package/dist/tool-system/builtin/notebook-edit.js +0 -7
- package/dist/tool-system/builtin/powershell.js +4 -1
- package/dist/tool-system/builtin/read.js +10 -10
- package/dist/tool-system/builtin/repl.js +4 -1
- package/dist/tool-system/builtin/skill.js +9 -0
- package/dist/tool-system/builtin/sleep.js +3 -2
- package/dist/tool-system/builtin/tool-search.js +25 -7
- package/dist/tool-system/builtin/video-providers.d.ts +154 -0
- package/dist/tool-system/builtin/video-providers.js +235 -0
- package/dist/tool-system/builtin/web-fetch.js +12 -2
- package/dist/tool-system/builtin/web-search.js +21 -7
- package/dist/tool-system/builtin/worktree.d.ts +2 -1
- package/dist/tool-system/builtin/worktree.js +31 -4
- package/dist/tool-system/builtin/write.js +0 -6
- package/dist/tool-system/context.d.ts +154 -6
- package/dist/tool-system/executor.d.ts +3 -1
- package/dist/tool-system/executor.js +172 -88
- package/dist/tool-system/investigation-guard.js +1 -1
- package/dist/tool-system/mcp-manager.d.ts +42 -4
- package/dist/tool-system/mcp-manager.js +186 -20
- package/dist/tool-system/path-policy.d.ts +5 -0
- package/dist/tool-system/path-policy.js +307 -8
- package/dist/tool-system/permission.d.ts +36 -1
- package/dist/tool-system/permission.js +281 -69
- package/dist/tool-system/plan-mode-allowlist.d.ts +13 -2
- package/dist/tool-system/plan-mode-allowlist.js +24 -2
- package/dist/tool-system/registry.d.ts +1 -0
- package/dist/tool-system/registry.js +16 -2
- package/dist/tool-system/sandbox/index.d.ts +8 -0
- package/dist/tool-system/sandbox/index.js +7 -2
- package/dist/tool-system/validate-tool-metadata.d.ts +36 -0
- package/dist/tool-system/validate-tool-metadata.js +63 -0
- package/dist/types.d.ts +217 -6
- package/dist/updater.js +20 -9
- package/dist/utils/envUtils.d.ts +0 -9
- package/dist/utils/envUtils.js +3 -28
- package/dist/utils/exec.d.ts +40 -0
- package/dist/utils/exec.js +144 -0
- package/dist/utils/json.d.ts +12 -0
- package/dist/utils/json.js +92 -0
- package/dist/utils/theme.d.ts +1 -1
- package/dist/utils/theme.js +1 -1
- package/dist/utils/toolDisplay.js +0 -1
- package/package.json +13 -7
- package/dist/agent/coordinator.d.ts +0 -49
- package/dist/agent/coordinator.js +0 -77
- package/dist/settings/manager.test.js +0 -73
- package/dist/tool-system/builtin/remote-trigger.d.ts +0 -6
- package/dist/tool-system/builtin/remote-trigger.js +0 -54
- package/dist/tool-system/builtin/send-message.d.ts +0 -6
- package/dist/tool-system/builtin/send-message.js +0 -47
- /package/dist/{settings/manager.test.d.ts → credentials/types.js} +0 -0
package/dist/session/memory.js
CHANGED
|
@@ -22,13 +22,32 @@
|
|
|
22
22
|
import { mkdirSync, existsSync, readFileSync, writeFileSync, readdirSync, renameSync, statSync, } from "node:fs";
|
|
23
23
|
import { join } from "node:path";
|
|
24
24
|
import { homedir } from "node:os";
|
|
25
|
+
/**
|
|
26
|
+
* Drop memories older than `maxAgeDays` (by file mtime) for context injection
|
|
27
|
+
* (TODO 8.1). A non-positive/undefined maxAge means no filtering. Entries with
|
|
28
|
+
* an unknown mtime (updatedAt 0/undefined) are KEPT — we never hide a memory
|
|
29
|
+
* just because we couldn't read its timestamp. Pure + testable.
|
|
30
|
+
*/
|
|
31
|
+
export function filterByAge(entries, maxAgeDays, now = Date.now()) {
|
|
32
|
+
if (!maxAgeDays || maxAgeDays <= 0)
|
|
33
|
+
return entries;
|
|
34
|
+
const cutoff = now - maxAgeDays * 24 * 60 * 60 * 1000;
|
|
35
|
+
// Pinned memories never age out of injection — that's the point of the pin.
|
|
36
|
+
return entries.filter((e) => e.pinned || !e.updatedAt || e.updatedAt >= cutoff);
|
|
37
|
+
}
|
|
25
38
|
export function resolveMemoryBaseDir(override) {
|
|
26
39
|
if (override)
|
|
27
40
|
return override;
|
|
28
41
|
const fromEnv = process.env.CODE_SHELL_HOME;
|
|
29
42
|
if (fromEnv && fromEnv.length > 0)
|
|
30
43
|
return fromEnv;
|
|
31
|
-
|
|
44
|
+
// `process.env.HOME ?? homedir()` (NOT raw homedir()): bun caches homedir()
|
|
45
|
+
// at process start and never re-reads it, so a test's HOME override — and any
|
|
46
|
+
// code that relies on $HOME — would be ignored, and memory would read the
|
|
47
|
+
// REAL ~/.code-shell (leaking global memories into isolated tests). See the
|
|
48
|
+
// homedir()-cache trap; mirrors settings/manager.ts userHome().
|
|
49
|
+
const home = process.env.HOME ?? homedir();
|
|
50
|
+
return join(home, ".code-shell");
|
|
32
51
|
}
|
|
33
52
|
export class MemoryManager {
|
|
34
53
|
baseDir;
|
|
@@ -72,10 +91,29 @@ export class MemoryManager {
|
|
|
72
91
|
save(entry) {
|
|
73
92
|
const fileName = this.slugify(entry.name) + ".md";
|
|
74
93
|
const filePath = join(this.memoryDir, fileName);
|
|
94
|
+
const nowIso = new Date().toISOString();
|
|
95
|
+
// Overwrite = UPDATE: preserve the original `created` and accumulated
|
|
96
|
+
// `usageCount` of the file being replaced (only `created` is load-bearing
|
|
97
|
+
// for TTL; carrying usageCount avoids resetting a popular memory's score).
|
|
98
|
+
// explicit entry fields win over the existing file (lets callers force a value).
|
|
99
|
+
const existing = existsSync(filePath) ? this.loadFile(fileName) : null;
|
|
100
|
+
const created = entry.created ?? existing?.created ?? nowIso;
|
|
101
|
+
const usageCount = entry.usageCount ?? existing?.usageCount ?? 0;
|
|
102
|
+
const lastUsed = entry.lastUsed ?? existing?.lastUsed ?? created;
|
|
103
|
+
const originProject = entry.originProject ?? existing?.originProject;
|
|
104
|
+
// pinned/origin are written only when meaningful so legacy-shaped files
|
|
105
|
+
// stay byte-identical for unpinned manual saves. Lifecycle fields are
|
|
106
|
+
// always written now (created/lastUsed/usageCount) — they're how TTL works.
|
|
75
107
|
const content = `---\n` +
|
|
76
108
|
`name: ${entry.name}\n` +
|
|
77
109
|
`description: ${entry.description}\n` +
|
|
78
110
|
`type: ${entry.type}\n` +
|
|
111
|
+
(entry.pinned ? `pinned: true\n` : "") +
|
|
112
|
+
(entry.origin ? `origin: ${entry.origin}\n` : "") +
|
|
113
|
+
(originProject ? `originProject: ${originProject}\n` : "") +
|
|
114
|
+
`created: ${created}\n` +
|
|
115
|
+
`lastUsed: ${lastUsed}\n` +
|
|
116
|
+
`usageCount: ${usageCount}\n` +
|
|
79
117
|
`---\n\n` +
|
|
80
118
|
`${entry.content}\n`;
|
|
81
119
|
writeFileSync(filePath, content, "utf-8");
|
|
@@ -107,6 +145,17 @@ export class MemoryManager {
|
|
|
107
145
|
*/
|
|
108
146
|
loadFile(fileName) {
|
|
109
147
|
const filePath = join(this.memoryDir, fileName);
|
|
148
|
+
if (!existsSync(filePath))
|
|
149
|
+
return null;
|
|
150
|
+
return this.loadFileFromDir(this.memoryDir, fileName, this.scope);
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Parse a memory file's frontmatter + body into a MemoryEntry. Shared by both
|
|
154
|
+
* loadFile (own scope) and loadScope (cross-scope) so the parsing of every
|
|
155
|
+
* field — including the lifecycle fields — lives in exactly one place.
|
|
156
|
+
* Returns null on missing file / unparseable frontmatter.
|
|
157
|
+
*/
|
|
158
|
+
parseMemoryFile(filePath, fileName, scope) {
|
|
110
159
|
if (!existsSync(filePath))
|
|
111
160
|
return null;
|
|
112
161
|
try {
|
|
@@ -119,7 +168,30 @@ export class MemoryManager {
|
|
|
119
168
|
const name = frontmatter.match(/name:\s*(.+)/)?.[1]?.trim() ?? fileName;
|
|
120
169
|
const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim() ?? "";
|
|
121
170
|
const type = (frontmatter.match(/type:\s*(.+)/)?.[1]?.trim() ?? "project");
|
|
122
|
-
|
|
171
|
+
const pinned = frontmatter.match(/pinned:\s*(.+)/)?.[1]?.trim() === "true";
|
|
172
|
+
// Anchor with ^…/m so `origin:` doesn't accidentally match the
|
|
173
|
+
// `originProject:` line (and vice-versa).
|
|
174
|
+
const originRaw = frontmatter.match(/^origin:\s*(.+)/m)?.[1]?.trim();
|
|
175
|
+
const origin = originRaw === "auto" || originRaw === "manual" ? originRaw : undefined;
|
|
176
|
+
const originProject = frontmatter.match(/^originProject:\s*(.+)/m)?.[1]?.trim() || undefined;
|
|
177
|
+
let updatedAt = 0;
|
|
178
|
+
try {
|
|
179
|
+
updatedAt = statSync(filePath).mtimeMs;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// mtime best-effort; 0 = unknown (never filtered out by maxAge).
|
|
183
|
+
}
|
|
184
|
+
// Lifecycle fields. Legacy files lack them → fall back to mtime so a TTL
|
|
185
|
+
// sweep treats them as "last used = file age" rather than "never used".
|
|
186
|
+
const mtimeIso = updatedAt > 0 ? new Date(updatedAt).toISOString() : undefined;
|
|
187
|
+
const created = frontmatter.match(/created:\s*(.+)/)?.[1]?.trim() || mtimeIso;
|
|
188
|
+
const lastUsed = frontmatter.match(/lastUsed:\s*(.+)/)?.[1]?.trim() || created;
|
|
189
|
+
const usageRaw = frontmatter.match(/usageCount:\s*(\d+)/)?.[1];
|
|
190
|
+
const usageCount = usageRaw ? parseInt(usageRaw, 10) : 0;
|
|
191
|
+
return {
|
|
192
|
+
name, description, type, content, fileName, scope,
|
|
193
|
+
updatedAt, pinned, origin, usageCount, lastUsed, created, originProject,
|
|
194
|
+
};
|
|
123
195
|
}
|
|
124
196
|
catch {
|
|
125
197
|
return null;
|
|
@@ -149,6 +221,129 @@ export class MemoryManager {
|
|
|
149
221
|
return false;
|
|
150
222
|
}
|
|
151
223
|
}
|
|
224
|
+
/**
|
|
225
|
+
* Record a recall: a MemoryRead hit this entry. Bumps usageCount and sets
|
|
226
|
+
* lastUsed=now, rewriting only the frontmatter (content untouched). This is
|
|
227
|
+
* the signal that drives recall-based TTL — a memory that keeps getting read
|
|
228
|
+
* never ages out. Idempotent-safe: missing entry → false, no throw.
|
|
229
|
+
*/
|
|
230
|
+
recordRecall(nameOrFile, now = new Date()) {
|
|
231
|
+
const entry = this.loadAll().find((e) => e.name === nameOrFile || e.fileName === nameOrFile);
|
|
232
|
+
if (!entry)
|
|
233
|
+
return false;
|
|
234
|
+
try {
|
|
235
|
+
this.save({
|
|
236
|
+
name: entry.name,
|
|
237
|
+
description: entry.description,
|
|
238
|
+
type: entry.type,
|
|
239
|
+
content: entry.content,
|
|
240
|
+
pinned: entry.pinned,
|
|
241
|
+
origin: entry.origin,
|
|
242
|
+
created: entry.created,
|
|
243
|
+
usageCount: (entry.usageCount ?? 0) + 1,
|
|
244
|
+
lastUsed: now.toISOString(),
|
|
245
|
+
});
|
|
246
|
+
return true;
|
|
247
|
+
}
|
|
248
|
+
catch {
|
|
249
|
+
return false;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
/**
|
|
253
|
+
* Recall-based TTL sweep. Soft-deletes `project`-type memories not read for
|
|
254
|
+
* more than `ttlDays` (by lastUsed). Stable types (user/feedback/reference)
|
|
255
|
+
* and pinned entries are NEVER pruned — only ephemeral project events age out.
|
|
256
|
+
* Returns the names pruned. ttlDays<=0 disables the sweep.
|
|
257
|
+
*/
|
|
258
|
+
pruneByRecall(ttlDays, now = new Date()) {
|
|
259
|
+
if (!ttlDays || ttlDays <= 0)
|
|
260
|
+
return [];
|
|
261
|
+
const cutoff = now.getTime() - ttlDays * 24 * 60 * 60 * 1000;
|
|
262
|
+
const pruned = [];
|
|
263
|
+
for (const e of this.loadAll()) {
|
|
264
|
+
if (e.type !== "project")
|
|
265
|
+
continue; // stable types are exempt
|
|
266
|
+
if (e.pinned)
|
|
267
|
+
continue; // pinned exempt
|
|
268
|
+
const lastUsedMs = e.lastUsed ? new Date(e.lastUsed).getTime() : NaN;
|
|
269
|
+
// Unknown lastUsed (unparseable) → keep, never prune on missing data.
|
|
270
|
+
if (!Number.isFinite(lastUsedMs))
|
|
271
|
+
continue;
|
|
272
|
+
if (lastUsedMs < cutoff) {
|
|
273
|
+
if (this.delete(e.name))
|
|
274
|
+
pruned.push(e.name);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
return pruned;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Approve a pending memory: move it from the pending scope into the user scope
|
|
281
|
+
* of the SAME memory root (pending only ever exists at the global root — the
|
|
282
|
+
* approval门 only gates global writes). Must be called on a pending-scope
|
|
283
|
+
* manager. Returns the new user filename, or null if not found / wrong scope.
|
|
284
|
+
*
|
|
285
|
+
* 审批门 (用户拍板): the only path that moves an auto-extracted memory into the
|
|
286
|
+
* curated, injected global user store.
|
|
287
|
+
*/
|
|
288
|
+
approvePending(nameOrFile) {
|
|
289
|
+
return this.movePending(nameOrFile, "global");
|
|
290
|
+
}
|
|
291
|
+
/**
|
|
292
|
+
* Demote a pending memory: 不批准升全局,但它仍是有用记忆 → 落回它来源项目的
|
|
293
|
+
* user store (originProject)。无 originProject 时落全局 user(兜底,不丢)。
|
|
294
|
+
*/
|
|
295
|
+
demotePending(nameOrFile) {
|
|
296
|
+
return this.movePending(nameOrFile, "project");
|
|
297
|
+
}
|
|
298
|
+
/** Shared move for approve(→global user)/demote(→origin-project user). */
|
|
299
|
+
movePending(nameOrFile, dest) {
|
|
300
|
+
if (this.scope !== "pending")
|
|
301
|
+
return null;
|
|
302
|
+
const entry = this.loadAll().find((e) => e.name === nameOrFile || e.fileName === nameOrFile);
|
|
303
|
+
if (!entry)
|
|
304
|
+
return null;
|
|
305
|
+
const targetMgr = dest === "project" && entry.originProject
|
|
306
|
+
? new MemoryManager({ baseDir: this.baseDir, projectDir: entry.originProject, scope: "user" })
|
|
307
|
+
: new MemoryManager({ baseDir: this.baseDir, scope: "user" }); // global, or fallback
|
|
308
|
+
const fileName = targetMgr.save({
|
|
309
|
+
name: entry.name,
|
|
310
|
+
description: entry.description,
|
|
311
|
+
type: entry.type,
|
|
312
|
+
content: entry.content,
|
|
313
|
+
origin: entry.origin ?? "auto",
|
|
314
|
+
created: entry.created,
|
|
315
|
+
lastUsed: entry.lastUsed,
|
|
316
|
+
usageCount: entry.usageCount,
|
|
317
|
+
// originProject is no longer needed once it has landed in a real store.
|
|
318
|
+
});
|
|
319
|
+
this.delete(entry.name); // soft-delete the pending copy
|
|
320
|
+
return fileName;
|
|
321
|
+
}
|
|
322
|
+
/**
|
|
323
|
+
* Promote a project-level user memory to the GLOBAL user store (用户手动点
|
|
324
|
+
* "提升到全局")。Must be called on a project-scoped user manager. Copies the
|
|
325
|
+
* entry into global user/ and soft-deletes the project copy. Returns the new
|
|
326
|
+
* global filename, or null if not found.
|
|
327
|
+
*/
|
|
328
|
+
promoteToGlobal(nameOrFile) {
|
|
329
|
+
const entry = this.loadAll().find((e) => e.name === nameOrFile || e.fileName === nameOrFile);
|
|
330
|
+
if (!entry)
|
|
331
|
+
return null;
|
|
332
|
+
const globalMgr = new MemoryManager({ baseDir: this.baseDir, scope: "user" });
|
|
333
|
+
const fileName = globalMgr.save({
|
|
334
|
+
name: entry.name,
|
|
335
|
+
description: entry.description,
|
|
336
|
+
type: entry.type,
|
|
337
|
+
content: entry.content,
|
|
338
|
+
origin: entry.origin,
|
|
339
|
+
pinned: entry.pinned,
|
|
340
|
+
created: entry.created,
|
|
341
|
+
lastUsed: entry.lastUsed,
|
|
342
|
+
usageCount: entry.usageCount,
|
|
343
|
+
});
|
|
344
|
+
this.delete(entry.name); // remove from the project store
|
|
345
|
+
return fileName;
|
|
346
|
+
}
|
|
152
347
|
/**
|
|
153
348
|
* Get the MEMORY.md index content for injection into prompts.
|
|
154
349
|
*/
|
|
@@ -164,20 +359,20 @@ export class MemoryManager {
|
|
|
164
359
|
* tagged with their origin so the model can tell user-owned from
|
|
165
360
|
* dream-generated.
|
|
166
361
|
*/
|
|
167
|
-
buildMemoryContext() {
|
|
168
|
-
const userEntries = this.scope === "user"
|
|
169
|
-
|
|
170
|
-
: this.loadScope("user");
|
|
171
|
-
const dreamEntries = this.scope === "dream"
|
|
172
|
-
? this.loadAll()
|
|
173
|
-
: this.loadScope("dream");
|
|
362
|
+
buildMemoryContext(opts) {
|
|
363
|
+
const userEntries = filterByAge(this.scope === "user" ? this.loadAll() : this.loadScope("user"), opts?.maxAgeDays, opts?.now);
|
|
364
|
+
const dreamEntries = filterByAge(this.scope === "dream" ? this.loadAll() : this.loadScope("dream"), opts?.maxAgeDays, opts?.now);
|
|
174
365
|
if (userEntries.length === 0 && dreamEntries.length === 0)
|
|
175
366
|
return "";
|
|
367
|
+
// Pinned memories lead the list so the user's hand-picked context is what
|
|
368
|
+
// the model reads first (sort is stable — unpinned keep their order).
|
|
369
|
+
const pinnedFirst = (a, b) => Number(b.pinned ?? false) - Number(a.pinned ?? false);
|
|
370
|
+
userEntries.sort(pinnedFirst);
|
|
176
371
|
const lines = [];
|
|
177
372
|
if (userEntries.length > 0) {
|
|
178
373
|
lines.push("## User memories (you own these — needs permission to modify)");
|
|
179
374
|
for (const e of userEntries) {
|
|
180
|
-
lines.push(`- [${e.type}] ${e.name}: ${e.description}`);
|
|
375
|
+
lines.push(`- ${e.pinned ? "[pinned] " : ""}[${e.type}] ${e.name}: ${e.description}`);
|
|
181
376
|
}
|
|
182
377
|
}
|
|
183
378
|
if (dreamEntries.length > 0) {
|
|
@@ -193,6 +388,50 @@ export class MemoryManager {
|
|
|
193
388
|
lines.join("\n") +
|
|
194
389
|
`\n\nTo read a specific memory, look under ${this.memoryRoot}/{user,dream}/`);
|
|
195
390
|
}
|
|
391
|
+
/**
|
|
392
|
+
* Two-layer injection index (用户拍板). Merges GLOBAL (cross-project
|
|
393
|
+
* experience) + PROJECT (this repo's facts) memories into a compact index —
|
|
394
|
+
* one line per entry, name + description only, NO body. The model reads a
|
|
395
|
+
* specific entry's full content on demand via MemoryRead (which records a
|
|
396
|
+
* recall → drives TTL + UI visibility).
|
|
397
|
+
*
|
|
398
|
+
* This is what fixes "global memory never shows up": global memories are now
|
|
399
|
+
* injected alongside project ones, every session, regardless of cwd.
|
|
400
|
+
*
|
|
401
|
+
* Static because it must construct two managers (global = no projectDir,
|
|
402
|
+
* project = with projectDir). `projectDir` undefined → only global is shown.
|
|
403
|
+
*/
|
|
404
|
+
static buildInjectionIndex(opts) {
|
|
405
|
+
const global = new MemoryManager({ baseDir: opts.baseDir });
|
|
406
|
+
const project = opts.projectDir
|
|
407
|
+
? new MemoryManager({ baseDir: opts.baseDir, projectDir: opts.projectDir })
|
|
408
|
+
: null;
|
|
409
|
+
const pinnedFirst = (a, b) => Number(b.pinned ?? false) - Number(a.pinned ?? false);
|
|
410
|
+
const collect = (mm) => filterByAge([...mm.loadScope("user"), ...mm.loadScope("dream")], opts.maxAgeDays, opts.now).sort(pinnedFirst);
|
|
411
|
+
const globalEntries = collect(global);
|
|
412
|
+
const projectEntries = project ? collect(project) : [];
|
|
413
|
+
if (globalEntries.length === 0 && projectEntries.length === 0)
|
|
414
|
+
return "";
|
|
415
|
+
const fmt = (e) => `- ${e.pinned ? "[pinned] " : ""}[${e.type}] ${e.name}: ${e.description}`;
|
|
416
|
+
const lines = [];
|
|
417
|
+
if (globalEntries.length > 0) {
|
|
418
|
+
lines.push("## Global memories (apply across all projects)");
|
|
419
|
+
for (const e of globalEntries)
|
|
420
|
+
lines.push(fmt(e));
|
|
421
|
+
}
|
|
422
|
+
if (projectEntries.length > 0) {
|
|
423
|
+
if (lines.length > 0)
|
|
424
|
+
lines.push("");
|
|
425
|
+
lines.push("## Project memories (this repo)");
|
|
426
|
+
for (const e of projectEntries)
|
|
427
|
+
lines.push(fmt(e));
|
|
428
|
+
}
|
|
429
|
+
return (`# Persistent Memory (index)\n\n` +
|
|
430
|
+
`These are summaries of memories from previous sessions. Only the summary is shown here.\n` +
|
|
431
|
+
`When a memory looks relevant to the current task, read its full content with the ` +
|
|
432
|
+
`MemoryRead tool (scope = user or dream; location = global or project) before relying on it.\n\n` +
|
|
433
|
+
lines.join("\n"));
|
|
434
|
+
}
|
|
196
435
|
/**
|
|
197
436
|
* Load every entry belonging to the given scope, without changing the
|
|
198
437
|
* manager's own scope. Used by buildMemoryContext to merge user + dream
|
|
@@ -212,24 +451,7 @@ export class MemoryManager {
|
|
|
212
451
|
return entries;
|
|
213
452
|
}
|
|
214
453
|
loadFileFromDir(dir, fileName, scope) {
|
|
215
|
-
|
|
216
|
-
if (!existsSync(filePath))
|
|
217
|
-
return null;
|
|
218
|
-
try {
|
|
219
|
-
const raw = readFileSync(filePath, "utf-8");
|
|
220
|
-
const frontmatterMatch = raw.match(/^---\n([\s\S]*?)\n---\n\n?([\s\S]*)$/);
|
|
221
|
-
if (!frontmatterMatch)
|
|
222
|
-
return null;
|
|
223
|
-
const frontmatter = frontmatterMatch[1];
|
|
224
|
-
const content = frontmatterMatch[2].trim();
|
|
225
|
-
const name = frontmatter.match(/name:\s*(.+)/)?.[1]?.trim() ?? fileName;
|
|
226
|
-
const description = frontmatter.match(/description:\s*(.+)/)?.[1]?.trim() ?? "";
|
|
227
|
-
const type = (frontmatter.match(/type:\s*(.+)/)?.[1]?.trim() ?? "project");
|
|
228
|
-
return { name, description, type, content, fileName, scope };
|
|
229
|
-
}
|
|
230
|
-
catch {
|
|
231
|
-
return null;
|
|
232
|
-
}
|
|
454
|
+
return this.parseMemoryFile(join(dir, fileName), fileName, scope);
|
|
233
455
|
}
|
|
234
456
|
/**
|
|
235
457
|
* Move pre-scope flat-layout entries (entries that lived directly under
|
|
@@ -46,6 +46,46 @@ export declare class SessionManager {
|
|
|
46
46
|
* without catching SessionError.
|
|
47
47
|
*/
|
|
48
48
|
exists(sessionId: string): boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Cheap "what cwd is this session bound to?" probe — reads only state.json,
|
|
51
|
+
* NOT the transcript (unlike resume()). engine.run uses this to recover a
|
|
52
|
+
* resumed session's project directory when the caller omits options.cwd
|
|
53
|
+
* (e.g. a desktop host whose sidebar repo selection has drifted to null),
|
|
54
|
+
* so a project-bound session keeps loading its own agents/settings/memory
|
|
55
|
+
* instead of falling back to process.cwd(). Returns undefined for an
|
|
56
|
+
* unknown, malformed, or traversal-shaped id rather than throwing — the
|
|
57
|
+
* caller treats "no recoverable cwd" the same as "not given".
|
|
58
|
+
*/
|
|
59
|
+
readCwd(sessionId: string): string | undefined;
|
|
60
|
+
/**
|
|
61
|
+
* Cheap "does this session have a persisted goal?" probe — reads only
|
|
62
|
+
* state.json, NOT the transcript (like readCwd). A persistent goal lives ONLY
|
|
63
|
+
* in state.activeGoal; it is never appended to the transcript as an event, so
|
|
64
|
+
* a session rebuilt from disk (e.g. localStorage wiped) can't recover the
|
|
65
|
+
* goal from its messages. The desktop host calls this on session load to
|
|
66
|
+
* re-surface the active-goal block + its Cancel button, which would otherwise
|
|
67
|
+
* be invisible (and thus uncancellable) after a reload of an aborted goal
|
|
68
|
+
* run. Returns undefined for an unknown / malformed / traversal-shaped id, or
|
|
69
|
+
* a session with no active goal — never throws.
|
|
70
|
+
*/
|
|
71
|
+
readActiveGoal(sessionId: string): import("../engine/goal.js").GoalConfig | undefined;
|
|
72
|
+
/**
|
|
73
|
+
* Disk-only "wipe this session's persistent goal" — the counterpart to
|
|
74
|
+
* readActiveGoal. Reads state.json, removes state.activeGoal, and rewrites it
|
|
75
|
+
* atomically (via saveState). Returns true only if a goal was actually
|
|
76
|
+
* present (idempotent — clearing a session with no goal is a no-op returning
|
|
77
|
+
* false). Never throws on an unknown / malformed / traversal-shaped id
|
|
78
|
+
* (returns false), matching readActiveGoal's tolerance.
|
|
79
|
+
*
|
|
80
|
+
* Why it lives here and not only on Engine: a persistent goal can strand a
|
|
81
|
+
* session whose worker is NOT live (aborted/reloaded) — Engine.clearGoal
|
|
82
|
+
* needs a resumed in-RAM session and a matching in-flight hook, neither of
|
|
83
|
+
* which exists once the worker has exited. Hosts (the desktop bridge) call
|
|
84
|
+
* THIS to clear such a goal off disk without spinning up a full Engine.
|
|
85
|
+
* Engine.clearGoal also delegates its disk write here so the wipe logic lives
|
|
86
|
+
* in exactly one place.
|
|
87
|
+
*/
|
|
88
|
+
clearActiveGoal(sessionId: string): boolean;
|
|
49
89
|
resume(sessionId: string): SessionBundle;
|
|
50
90
|
saveState(state: SessionState): void;
|
|
51
91
|
/**
|
|
@@ -93,7 +93,12 @@ export class SessionManager {
|
|
|
93
93
|
parentSessionId: parentSessionId ?? null,
|
|
94
94
|
...(origin ? { origin } : {}),
|
|
95
95
|
};
|
|
96
|
-
|
|
96
|
+
// Atomic write (tmp+rename) like saveState, so a crash during this one-time
|
|
97
|
+
// create can't leave a torn state.json that resume() then fails to parse.
|
|
98
|
+
const stateTarget = join(sessionDir, "state.json");
|
|
99
|
+
const stateTmp = `${stateTarget}.${process.pid}.${Date.now()}.create.tmp`;
|
|
100
|
+
writeFileSync(stateTmp, JSON.stringify(state, null, 2), "utf-8");
|
|
101
|
+
renameSync(stateTmp, stateTarget);
|
|
97
102
|
const transcript = new Transcript(join(sessionDir, "transcript.jsonl"));
|
|
98
103
|
transcript.append("session_meta", {
|
|
99
104
|
sessionId,
|
|
@@ -121,6 +126,102 @@ export class SessionManager {
|
|
|
121
126
|
}
|
|
122
127
|
return existsSync(join(this.sessionsDir, sessionId));
|
|
123
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Cheap "what cwd is this session bound to?" probe — reads only state.json,
|
|
131
|
+
* NOT the transcript (unlike resume()). engine.run uses this to recover a
|
|
132
|
+
* resumed session's project directory when the caller omits options.cwd
|
|
133
|
+
* (e.g. a desktop host whose sidebar repo selection has drifted to null),
|
|
134
|
+
* so a project-bound session keeps loading its own agents/settings/memory
|
|
135
|
+
* instead of falling back to process.cwd(). Returns undefined for an
|
|
136
|
+
* unknown, malformed, or traversal-shaped id rather than throwing — the
|
|
137
|
+
* caller treats "no recoverable cwd" the same as "not given".
|
|
138
|
+
*/
|
|
139
|
+
readCwd(sessionId) {
|
|
140
|
+
try {
|
|
141
|
+
assertSafeSessionId(sessionId);
|
|
142
|
+
}
|
|
143
|
+
catch {
|
|
144
|
+
return undefined;
|
|
145
|
+
}
|
|
146
|
+
const stateFile = join(this.sessionsDir, sessionId, "state.json");
|
|
147
|
+
if (!existsSync(stateFile))
|
|
148
|
+
return undefined;
|
|
149
|
+
try {
|
|
150
|
+
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
151
|
+
return typeof state.cwd === "string" && state.cwd.length > 0 ? state.cwd : undefined;
|
|
152
|
+
}
|
|
153
|
+
catch {
|
|
154
|
+
return undefined;
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Cheap "does this session have a persisted goal?" probe — reads only
|
|
159
|
+
* state.json, NOT the transcript (like readCwd). A persistent goal lives ONLY
|
|
160
|
+
* in state.activeGoal; it is never appended to the transcript as an event, so
|
|
161
|
+
* a session rebuilt from disk (e.g. localStorage wiped) can't recover the
|
|
162
|
+
* goal from its messages. The desktop host calls this on session load to
|
|
163
|
+
* re-surface the active-goal block + its Cancel button, which would otherwise
|
|
164
|
+
* be invisible (and thus uncancellable) after a reload of an aborted goal
|
|
165
|
+
* run. Returns undefined for an unknown / malformed / traversal-shaped id, or
|
|
166
|
+
* a session with no active goal — never throws.
|
|
167
|
+
*/
|
|
168
|
+
readActiveGoal(sessionId) {
|
|
169
|
+
try {
|
|
170
|
+
assertSafeSessionId(sessionId);
|
|
171
|
+
}
|
|
172
|
+
catch {
|
|
173
|
+
return undefined;
|
|
174
|
+
}
|
|
175
|
+
const stateFile = join(this.sessionsDir, sessionId, "state.json");
|
|
176
|
+
if (!existsSync(stateFile))
|
|
177
|
+
return undefined;
|
|
178
|
+
try {
|
|
179
|
+
const state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
180
|
+
return state.activeGoal;
|
|
181
|
+
}
|
|
182
|
+
catch {
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
/**
|
|
187
|
+
* Disk-only "wipe this session's persistent goal" — the counterpart to
|
|
188
|
+
* readActiveGoal. Reads state.json, removes state.activeGoal, and rewrites it
|
|
189
|
+
* atomically (via saveState). Returns true only if a goal was actually
|
|
190
|
+
* present (idempotent — clearing a session with no goal is a no-op returning
|
|
191
|
+
* false). Never throws on an unknown / malformed / traversal-shaped id
|
|
192
|
+
* (returns false), matching readActiveGoal's tolerance.
|
|
193
|
+
*
|
|
194
|
+
* Why it lives here and not only on Engine: a persistent goal can strand a
|
|
195
|
+
* session whose worker is NOT live (aborted/reloaded) — Engine.clearGoal
|
|
196
|
+
* needs a resumed in-RAM session and a matching in-flight hook, neither of
|
|
197
|
+
* which exists once the worker has exited. Hosts (the desktop bridge) call
|
|
198
|
+
* THIS to clear such a goal off disk without spinning up a full Engine.
|
|
199
|
+
* Engine.clearGoal also delegates its disk write here so the wipe logic lives
|
|
200
|
+
* in exactly one place.
|
|
201
|
+
*/
|
|
202
|
+
clearActiveGoal(sessionId) {
|
|
203
|
+
try {
|
|
204
|
+
assertSafeSessionId(sessionId);
|
|
205
|
+
}
|
|
206
|
+
catch {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
const stateFile = join(this.sessionsDir, sessionId, "state.json");
|
|
210
|
+
if (!existsSync(stateFile))
|
|
211
|
+
return false;
|
|
212
|
+
let state;
|
|
213
|
+
try {
|
|
214
|
+
state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
return false;
|
|
218
|
+
}
|
|
219
|
+
if (state.activeGoal === undefined)
|
|
220
|
+
return false;
|
|
221
|
+
state.activeGoal = undefined;
|
|
222
|
+
this.saveState(state);
|
|
223
|
+
return true;
|
|
224
|
+
}
|
|
124
225
|
resume(sessionId) {
|
|
125
226
|
assertSafeSessionId(sessionId);
|
|
126
227
|
const sessionDir = join(this.sessionsDir, sessionId);
|
|
@@ -131,7 +232,16 @@ export class SessionManager {
|
|
|
131
232
|
if (!existsSync(stateFile)) {
|
|
132
233
|
throw new SessionError(`Session state file not found: ${sessionId}`);
|
|
133
234
|
}
|
|
134
|
-
|
|
235
|
+
let state;
|
|
236
|
+
try {
|
|
237
|
+
state = JSON.parse(readFileSync(stateFile, "utf-8"));
|
|
238
|
+
}
|
|
239
|
+
catch (err) {
|
|
240
|
+
// A corrupt state.json (external tampering, disk corruption, or a crash
|
|
241
|
+
// during the one-time create() write) must surface as a clean SessionError
|
|
242
|
+
// — not a raw SyntaxError that escapes callers expecting SessionError.
|
|
243
|
+
throw new SessionError(`Session state is corrupt for ${sessionId}: ${err instanceof Error ? err.message : String(err)}`);
|
|
244
|
+
}
|
|
135
245
|
const transcriptFile = join(sessionDir, "transcript.jsonl");
|
|
136
246
|
const transcript = Transcript.loadFromFile(transcriptFile);
|
|
137
247
|
state.status = "active";
|
|
@@ -165,7 +275,7 @@ export class SessionManager {
|
|
|
165
275
|
newBundle.state.parentSessionId = sourceSessionId;
|
|
166
276
|
// Copy events up to the fork point
|
|
167
277
|
for (const event of events) {
|
|
168
|
-
if (event.type === "turn_boundary" && event.data.turnNumber > forkTurn) {
|
|
278
|
+
if (event.type === "turn_boundary" && (event.data.turnNumber ?? -1) > forkTurn) {
|
|
169
279
|
break;
|
|
170
280
|
}
|
|
171
281
|
newBundle.transcript.append(event.type, event.data);
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal, dependency-free unified-diff for human-readable previews (e.g. the
|
|
3
|
+
* /undo confirmation). NOT a patch generator — it's for showing a user "here's
|
|
4
|
+
* what restoring will change" before they confirm. Uses a standard LCS so the
|
|
5
|
+
* output reads like a normal diff (unchanged context, -removed, +added).
|
|
6
|
+
*
|
|
7
|
+
* `from` is the content currently on disk; `to` is the snapshot we'd restore.
|
|
8
|
+
* So the preview answers "if I undo, these lines change."
|
|
9
|
+
*/
|
|
10
|
+
/** A single diff line with its marker. */
|
|
11
|
+
export interface DiffLine {
|
|
12
|
+
marker: " " | "-" | "+";
|
|
13
|
+
text: string;
|
|
14
|
+
}
|
|
15
|
+
/** Longest-common-subsequence table → minimal -/+ line diff (Myers-equivalent
|
|
16
|
+
* result via classic LCS; fine for preview-sized files). */
|
|
17
|
+
export declare function diffLines(from: string, to: string): DiffLine[];
|
|
18
|
+
/**
|
|
19
|
+
* Render a compact unified-diff string for a preview. Collapses long runs of
|
|
20
|
+
* unchanged context to at most `context` lines around each change so a small
|
|
21
|
+
* edit in a big file doesn't print the whole file. Returns "" when identical.
|
|
22
|
+
*/
|
|
23
|
+
export declare function renderDiffPreview(from: string, to: string, context?: number): string;
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Minimal, dependency-free unified-diff for human-readable previews (e.g. the
|
|
3
|
+
* /undo confirmation). NOT a patch generator — it's for showing a user "here's
|
|
4
|
+
* what restoring will change" before they confirm. Uses a standard LCS so the
|
|
5
|
+
* output reads like a normal diff (unchanged context, -removed, +added).
|
|
6
|
+
*
|
|
7
|
+
* `from` is the content currently on disk; `to` is the snapshot we'd restore.
|
|
8
|
+
* So the preview answers "if I undo, these lines change."
|
|
9
|
+
*/
|
|
10
|
+
/** Longest-common-subsequence table → minimal -/+ line diff (Myers-equivalent
|
|
11
|
+
* result via classic LCS; fine for preview-sized files). */
|
|
12
|
+
export function diffLines(from, to) {
|
|
13
|
+
// Normalize CRLF → LF before splitting so a Windows (CRLF) snapshot vs. an
|
|
14
|
+
// LF edit doesn't render as every-line-changed in the /undo preview.
|
|
15
|
+
const a = from.replace(/\r\n/g, "\n").split("\n");
|
|
16
|
+
const b = to.replace(/\r\n/g, "\n").split("\n");
|
|
17
|
+
const n = a.length;
|
|
18
|
+
const m = b.length;
|
|
19
|
+
// LCS length table.
|
|
20
|
+
const lcs = Array.from({ length: n + 1 }, () => new Array(m + 1).fill(0));
|
|
21
|
+
for (let i = n - 1; i >= 0; i--) {
|
|
22
|
+
for (let j = m - 1; j >= 0; j--) {
|
|
23
|
+
lcs[i][j] = a[i] === b[j] ? lcs[i + 1][j + 1] + 1 : Math.max(lcs[i + 1][j], lcs[i][j + 1]);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
const out = [];
|
|
27
|
+
let i = 0;
|
|
28
|
+
let j = 0;
|
|
29
|
+
while (i < n && j < m) {
|
|
30
|
+
if (a[i] === b[j]) {
|
|
31
|
+
out.push({ marker: " ", text: a[i] });
|
|
32
|
+
i++;
|
|
33
|
+
j++;
|
|
34
|
+
}
|
|
35
|
+
else if (lcs[i + 1][j] >= lcs[i][j + 1]) {
|
|
36
|
+
out.push({ marker: "-", text: a[i] });
|
|
37
|
+
i++;
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
out.push({ marker: "+", text: b[j] });
|
|
41
|
+
j++;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
while (i < n)
|
|
45
|
+
out.push({ marker: "-", text: a[i++] });
|
|
46
|
+
while (j < m)
|
|
47
|
+
out.push({ marker: "+", text: b[j++] });
|
|
48
|
+
return out;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Render a compact unified-diff string for a preview. Collapses long runs of
|
|
52
|
+
* unchanged context to at most `context` lines around each change so a small
|
|
53
|
+
* edit in a big file doesn't print the whole file. Returns "" when identical.
|
|
54
|
+
*/
|
|
55
|
+
export function renderDiffPreview(from, to, context = 3) {
|
|
56
|
+
const lines = diffLines(from, to);
|
|
57
|
+
if (lines.every((l) => l.marker === " "))
|
|
58
|
+
return "";
|
|
59
|
+
// Mark which lines are within `context` of a change; drop the rest, inserting
|
|
60
|
+
// a "⋯" gap marker where we elide.
|
|
61
|
+
const keep = new Array(lines.length).fill(false);
|
|
62
|
+
for (let k = 0; k < lines.length; k++) {
|
|
63
|
+
if (lines[k].marker !== " ") {
|
|
64
|
+
for (let d = -context; d <= context; d++) {
|
|
65
|
+
const idx = k + d;
|
|
66
|
+
if (idx >= 0 && idx < lines.length)
|
|
67
|
+
keep[idx] = true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
const rows = [];
|
|
72
|
+
let elided = false;
|
|
73
|
+
for (let k = 0; k < lines.length; k++) {
|
|
74
|
+
if (keep[k]) {
|
|
75
|
+
rows.push(`${lines[k].marker}${lines[k].text}`);
|
|
76
|
+
elided = false;
|
|
77
|
+
}
|
|
78
|
+
else if (!elided) {
|
|
79
|
+
rows.push("⋯");
|
|
80
|
+
elided = true;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return rows.join("\n");
|
|
84
|
+
}
|