@oh-my-pi/pi-coding-agent 17.0.9 → 17.1.1
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/CHANGELOG.md +78 -0
- package/README.md +1 -1
- package/dist/cli.js +4685 -4614
- package/dist/types/async/job-manager.d.ts +37 -1
- package/dist/types/cli/args.d.ts +2 -0
- package/dist/types/cli/bench-cli.d.ts +41 -0
- package/dist/types/commands/bench.d.ts +15 -3
- package/dist/types/commands/launch.d.ts +4 -0
- package/dist/types/config/model-registry.d.ts +3 -1
- package/dist/types/config/models-config-schema.d.ts +18 -6
- package/dist/types/config/models-config.d.ts +14 -5
- package/dist/types/config/provider-globals.d.ts +2 -2
- package/dist/types/config/service-tier.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +374 -47
- package/dist/types/config/settings.d.ts +7 -0
- package/dist/types/dap/client.d.ts +20 -0
- package/dist/types/exec/bash-executor.d.ts +38 -0
- package/dist/types/exec/direnv.d.ts +33 -0
- package/dist/types/export/html/args.d.ts +15 -0
- package/dist/types/export/html/index.d.ts +11 -26
- package/dist/types/export/html/web-palette.d.ts +55 -116
- package/dist/types/extensibility/extensions/loader.d.ts +3 -0
- package/dist/types/extensibility/extensions/types.d.ts +17 -2
- package/dist/types/extensibility/legacy-pi-coding-agent-shim.d.ts +3 -0
- package/dist/types/extensibility/plugins/legacy-pi-compat.d.ts +13 -0
- package/dist/types/live/attestation.d.ts +2 -0
- package/dist/types/live/controller.d.ts +48 -0
- package/dist/types/live/protocol.d.ts +85 -0
- package/dist/types/live/protocol.test.d.ts +1 -0
- package/dist/types/live/transport.d.ts +35 -0
- package/dist/types/live/visualizer.d.ts +30 -0
- package/dist/types/lsp/client.d.ts +16 -1
- package/dist/types/mcp/config-writer.test.d.ts +1 -0
- package/dist/types/mcp/oauth-discovery.d.ts +2 -0
- package/dist/types/mcp/smithery-auth.d.ts +1 -1
- package/dist/types/mcp/smithery-auth.test.d.ts +1 -0
- package/dist/types/memory-backend/tool-names.d.ts +2 -0
- package/dist/types/modes/components/assistant-message.d.ts +1 -0
- package/dist/types/modes/components/custom-message.d.ts +1 -1
- package/dist/types/modes/components/login-dialog.d.ts +2 -2
- package/dist/types/modes/components/message-frame.d.ts +8 -4
- package/dist/types/modes/components/plan-review-overlay.d.ts +26 -0
- package/dist/types/modes/components/session-account-selector.d.ts +11 -0
- package/dist/types/modes/components/settings-defs.d.ts +7 -1
- package/dist/types/modes/components/status-line/component.jj-cache.test.d.ts +1 -0
- package/dist/types/modes/components/tool-execution.d.ts +9 -1
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +6 -1
- package/dist/types/modes/controllers/live-command-controller.d.ts +14 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -0
- package/dist/types/modes/interactive-mode.d.ts +6 -0
- package/dist/types/modes/theme/theme.d.ts +1 -1
- package/dist/types/modes/types.d.ts +4 -1
- package/dist/types/modes/utils/context-usage.d.ts +12 -2
- package/dist/types/plan-mode/plan-files.d.ts +10 -0
- package/dist/types/registry/agent-lifecycle.d.ts +21 -9
- package/dist/types/registry/agent-registry.d.ts +11 -4
- package/dist/types/sdk.d.ts +15 -1
- package/dist/types/secrets/index.d.ts +15 -2
- package/dist/types/secrets/obfuscator.d.ts +113 -15
- package/dist/types/session/acp-permission-gate.d.ts +19 -0
- package/dist/types/session/agent-session-events.d.ts +84 -0
- package/dist/types/session/agent-session-types.d.ts +321 -0
- package/dist/types/session/agent-session.d.ts +135 -753
- package/dist/types/session/async-job-delivery.d.ts +28 -0
- package/dist/types/session/auth-storage.d.ts +1 -1
- package/dist/types/session/bash-runner.d.ts +75 -0
- package/dist/types/session/blob-store.d.ts +11 -1
- package/dist/types/session/blob-store.test.d.ts +1 -0
- package/dist/types/session/checkpoint-entries.d.ts +28 -0
- package/dist/types/session/eval-runner.d.ts +51 -0
- package/dist/types/session/irc-bridge.d.ts +46 -0
- package/dist/types/session/messages.d.ts +24 -0
- package/dist/types/session/model-controls.d.ts +180 -0
- package/dist/types/session/prewalk.d.ts +46 -0
- package/dist/types/session/queued-messages.d.ts +22 -0
- package/dist/types/session/redis-session-storage.d.ts +1 -0
- package/dist/types/session/retry-fallback-chains.d.ts +74 -0
- package/dist/types/session/role-models.d.ts +15 -0
- package/dist/types/session/session-advisors.d.ts +228 -0
- package/dist/types/session/session-entries.d.ts +8 -0
- package/dist/types/session/session-handoff.d.ts +70 -0
- package/dist/types/session/session-history-format.d.ts +6 -1
- package/dist/types/session/session-maintenance.d.ts +246 -0
- package/dist/types/session/session-manager.d.ts +41 -0
- package/dist/types/session/session-memory.d.ts +56 -0
- package/dist/types/session/session-provider-boundary.d.ts +62 -0
- package/dist/types/session/session-stats.d.ts +48 -0
- package/dist/types/session/session-tools.d.ts +158 -0
- package/dist/types/session/session-workspace.d.ts +32 -0
- package/dist/types/session/stream-guards.d.ts +44 -0
- package/dist/types/session/streaming-output.d.ts +8 -0
- package/dist/types/session/todo-tracker.d.ts +56 -0
- package/dist/types/session/ttsr-coordinator.d.ts +51 -0
- package/dist/types/session/turn-recovery.d.ts +225 -0
- package/dist/types/slash-commands/helpers/session-pin.d.ts +9 -0
- package/dist/types/stt/index.d.ts +0 -2
- package/dist/types/stt/stt-controller.d.ts +7 -0
- package/dist/types/system-prompt.d.ts +2 -0
- package/dist/types/task/executor.d.ts +4 -1
- package/dist/types/task/output-manager.d.ts +3 -1
- package/dist/types/tiny/title-client.d.ts +10 -0
- package/dist/types/tools/approval.d.ts +1 -0
- package/dist/types/tools/browser/launch.d.ts +5 -0
- package/dist/types/tools/builtin-names.d.ts +1 -1
- package/dist/types/tools/computer/protocol.d.ts +43 -0
- package/dist/types/tools/computer/supervisor.d.ts +32 -0
- package/dist/types/tools/computer/worker-entry.d.ts +1 -0
- package/dist/types/tools/computer/worker.d.ts +15 -0
- package/dist/types/tools/computer-renderer.d.ts +22 -0
- package/dist/types/tools/computer.d.ts +71 -0
- package/dist/types/tools/context.d.ts +2 -0
- package/dist/types/tools/default-renderer.d.ts +21 -0
- package/dist/types/tools/image-gen.d.ts +4 -4
- package/dist/types/tools/image-providers.d.ts +38 -0
- package/dist/types/tools/index.d.ts +7 -0
- package/dist/types/tools/path-utils.d.ts +8 -0
- package/dist/types/tools/todo.d.ts +10 -4
- package/dist/types/tools/xdev.d.ts +5 -1
- package/dist/types/tts/streaming-player.d.ts +10 -29
- package/dist/types/tts/vocalizer.d.ts +5 -0
- package/dist/types/utils/jj.d.ts +29 -0
- package/dist/types/utils/lang-from-path.d.ts +1 -0
- package/dist/types/utils/title-generator.d.ts +30 -0
- package/dist/types/utils/tools-manager.d.ts +1 -2
- package/dist/types/vibe/runtime.d.ts +36 -8
- package/dist/types/web/search/index.d.ts +1 -1
- package/dist/types/web/search/provider.d.ts +15 -5
- package/dist/types/web/search/types.d.ts +94 -0
- package/package.json +12 -12
- package/scripts/generate-share-viewer.ts +4 -6
- package/src/advisor/__tests__/advisor.test.ts +643 -0
- package/src/advisor/runtime.ts +144 -43
- package/src/async/job-manager.ts +90 -2
- package/src/cli/args.ts +3 -0
- package/src/cli/auth-gateway-cli.ts +18 -3
- package/src/cli/bench-cli.ts +395 -29
- package/src/cli/flag-tables.ts +3 -0
- package/src/cli/gc-cli.ts +2 -2
- package/src/cli/setup-cli.ts +2 -14
- package/src/cli/usage-cli.ts +10 -1
- package/src/cli.ts +8 -0
- package/src/collab/host.ts +3 -2
- package/src/commands/bench.ts +18 -4
- package/src/commands/launch.ts +4 -0
- package/src/config/model-registry.ts +16 -2
- package/src/config/models-config-schema.ts +15 -4
- package/src/config/provider-globals.ts +9 -9
- package/src/config/service-tier.ts +26 -1
- package/src/config/settings-schema.ts +270 -51
- package/src/config/settings.ts +81 -2
- package/src/dap/client.ts +37 -4
- package/src/edit/index.ts +49 -12
- package/src/eval/executor-base.ts +1 -0
- package/src/eval/js/executor.ts +2 -0
- package/src/eval/py/executor.ts +103 -33
- package/src/exec/bash-executor.ts +90 -6
- package/src/exec/direnv.ts +145 -0
- package/src/export/html/args.ts +20 -0
- package/src/export/html/index.ts +45 -47
- package/src/export/html/template.css +19 -1
- package/src/export/html/template.html +6 -0
- package/src/export/html/template.js +21 -0
- package/src/export/html/tool-views.generated.js +31 -31
- package/src/export/html/web-palette.ts +116 -132
- package/src/export/share.ts +249 -49
- package/src/extensibility/extensions/loader.ts +29 -1
- package/src/extensibility/extensions/runner.ts +6 -0
- package/src/extensibility/extensions/types.ts +33 -2
- package/src/extensibility/extensions/wrapper.ts +68 -12
- package/src/extensibility/legacy-pi-coding-agent-shim.ts +7 -1
- package/src/extensibility/plugins/legacy-pi-compat.ts +19 -1
- package/src/hindsight/state.ts +64 -6
- package/src/internal-urls/local-protocol.ts +2 -1
- package/src/internal-urls/memory-protocol.ts +2 -2
- package/src/internal-urls/skill-protocol.ts +2 -2
- package/src/internal-urls/ssh-protocol.ts +2 -1
- package/src/internal-urls/vault-protocol.ts +2 -1
- package/src/live/attestation.ts +91 -0
- package/src/live/controller.ts +517 -0
- package/src/live/prompts/agent-final-message.md +3 -0
- package/src/live/prompts/live-instructions.md +23 -0
- package/src/live/protocol.test.ts +140 -0
- package/src/live/protocol.ts +233 -0
- package/src/live/transport.ts +422 -0
- package/src/live/visualizer.ts +214 -0
- package/src/lsp/client.ts +29 -9
- package/src/lsp/index.ts +19 -4
- package/src/main.ts +8 -1
- package/src/mcp/config-writer.test.ts +43 -0
- package/src/mcp/config-writer.ts +109 -82
- package/src/mcp/oauth-discovery.ts +10 -2
- package/src/mcp/smithery-auth.test.ts +29 -0
- package/src/mcp/smithery-auth.ts +3 -2
- package/src/memories/index.ts +40 -20
- package/src/memory-backend/tool-names.ts +2 -0
- package/src/mnemopi/backend.ts +24 -7
- package/src/modes/acp/acp-agent.ts +56 -10
- package/src/modes/acp/acp-event-mapper.ts +8 -1
- package/src/modes/components/agent-hub.ts +1 -1
- package/src/modes/components/assistant-message.ts +7 -2
- package/src/modes/components/custom-message.ts +4 -1
- package/src/modes/components/login-dialog.ts +14 -4
- package/src/modes/components/message-frame.ts +14 -8
- package/src/modes/components/plan-review-overlay.ts +350 -35
- package/src/modes/components/session-account-selector.ts +62 -0
- package/src/modes/components/settings-defs.ts +23 -1
- package/src/modes/components/settings-selector.ts +169 -1
- package/src/modes/components/status-line/component.jj-cache.test.ts +229 -0
- package/src/modes/components/status-line/component.ts +123 -2
- package/src/modes/components/tool-execution.ts +28 -112
- package/src/modes/controllers/command-controller.ts +56 -15
- package/src/modes/controllers/event-controller.ts +111 -16
- package/src/modes/controllers/extension-ui-controller.test.ts +1 -0
- package/src/modes/controllers/extension-ui-controller.ts +20 -4
- package/src/modes/controllers/input-controller.ts +47 -39
- package/src/modes/controllers/live-command-controller.ts +255 -0
- package/src/modes/controllers/mcp-command-controller.ts +10 -1
- package/src/modes/controllers/omfg-controller.ts +44 -40
- package/src/modes/controllers/selector-controller.ts +80 -10
- package/src/modes/controllers/session-focus-controller.ts +6 -1
- package/src/modes/interactive-mode.ts +176 -38
- package/src/modes/runtime-init.ts +2 -0
- package/src/modes/setup-wizard/scenes/web-search.ts +17 -9
- package/src/modes/theme/theme.ts +1 -1
- package/src/modes/types.ts +4 -1
- package/src/modes/utils/context-usage.ts +23 -7
- package/src/modes/utils/ui-helpers.ts +10 -5
- package/src/plan-mode/plan-files.ts +40 -0
- package/src/prompts/bench/cache-prefix-chunk.md +1 -0
- package/src/prompts/bench/cache-prefix.md +3 -0
- package/src/prompts/bench/cache-suffix.md +1 -0
- package/src/prompts/system/computer-safety.md +14 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/project-prompt.md +8 -1
- package/src/prompts/system/subagent-async-pending.md +6 -0
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/workflow-notice.md +48 -40
- package/src/prompts/system/xdev-mount-notice.md +4 -0
- package/src/prompts/tools/computer.md +26 -0
- package/src/prompts/tools/todo.md +3 -1
- package/src/registry/agent-lifecycle.ts +116 -46
- package/src/registry/agent-registry.ts +35 -8
- package/src/registry/persisted-agents.ts +26 -2
- package/src/sdk.ts +215 -136
- package/src/secrets/index.ts +127 -4
- package/src/secrets/obfuscator.ts +2328 -105
- package/src/session/acp-permission-gate.ts +165 -0
- package/src/session/agent-session-error-log.test.ts +1 -1
- package/src/session/agent-session-events.ts +66 -0
- package/src/session/agent-session-types.ts +343 -0
- package/src/session/agent-session.ts +1940 -12218
- package/src/session/artifacts.ts +6 -5
- package/src/session/async-job-delivery.ts +74 -0
- package/src/session/auth-storage.ts +1 -0
- package/src/session/bash-runner.ts +326 -0
- package/src/session/blob-store.test.ts +56 -0
- package/src/session/blob-store.ts +18 -2
- package/src/session/checkpoint-entries.ts +81 -0
- package/src/session/eval-runner.ts +217 -0
- package/src/session/indexed-session-storage.ts +9 -2
- package/src/session/irc-bridge.ts +203 -0
- package/src/session/messages.ts +266 -1
- package/src/session/model-controls.ts +714 -0
- package/src/session/prewalk.ts +252 -0
- package/src/session/queued-messages.ts +93 -0
- package/src/session/redis-session-storage.ts +51 -11
- package/src/session/retry-fallback-chains.ts +455 -0
- package/src/session/role-models.ts +85 -0
- package/src/session/session-advisors.ts +1679 -0
- package/src/session/session-context.ts +6 -3
- package/src/session/session-entries.ts +8 -0
- package/src/session/session-handoff.ts +308 -0
- package/src/session/session-history-format.ts +20 -9
- package/src/session/session-loader.ts +0 -46
- package/src/session/session-maintenance.ts +2983 -0
- package/src/session/session-manager.ts +390 -30
- package/src/session/session-memory.ts +222 -0
- package/src/session/session-provider-boundary.ts +307 -0
- package/src/session/session-stats.ts +293 -0
- package/src/session/session-tools.ts +980 -0
- package/src/session/session-workspace.ts +53 -0
- package/src/session/stream-guards.ts +417 -0
- package/src/session/streaming-output.ts +52 -5
- package/src/session/todo-tracker.ts +380 -0
- package/src/session/ttsr-coordinator.ts +496 -0
- package/src/session/turn-recovery.ts +1629 -0
- package/src/slash-commands/builtin-registry.ts +257 -20
- package/src/slash-commands/helpers/session-pin.ts +44 -0
- package/src/slash-commands/helpers/usage-report.ts +22 -3
- package/src/stt/downloader.ts +0 -2
- package/src/stt/index.ts +0 -2
- package/src/stt/stt-controller.ts +57 -146
- package/src/system-prompt.ts +45 -17
- package/src/task/executor.ts +285 -70
- package/src/task/index.ts +98 -105
- package/src/task/output-manager.ts +25 -3
- package/src/task/persisted-revive.ts +4 -3
- package/src/task/structured-subagent.ts +1 -0
- package/src/tiny/title-client.ts +22 -0
- package/src/tools/approval.ts +55 -10
- package/src/tools/bash-interactive.ts +116 -86
- package/src/tools/bash-skill-urls.ts +28 -2
- package/src/tools/bash.ts +339 -123
- package/src/tools/browser/launch.ts +9 -1
- package/src/tools/browser/tab-supervisor.ts +8 -5
- package/src/tools/browser.ts +94 -47
- package/src/tools/builtin-names.ts +1 -0
- package/src/tools/computer/protocol.ts +28 -0
- package/src/tools/computer/supervisor.ts +258 -0
- package/src/tools/computer/worker-entry.ts +25 -0
- package/src/tools/computer/worker.ts +135 -0
- package/src/tools/computer-renderer.ts +108 -0
- package/src/tools/computer.ts +433 -0
- package/src/tools/context.ts +2 -0
- package/src/tools/default-renderer.ts +139 -0
- package/src/tools/essential-tools.ts +1 -0
- package/src/tools/image-gen.ts +20 -28
- package/src/tools/image-providers.ts +50 -0
- package/src/tools/index.ts +14 -0
- package/src/tools/path-utils.ts +49 -2
- package/src/tools/read.ts +156 -88
- package/src/tools/renderers.ts +9 -1
- package/src/tools/todo.ts +101 -11
- package/src/tools/vibe.ts +1 -2
- package/src/tools/write.ts +51 -1
- package/src/tools/xdev.ts +121 -29
- package/src/tts/streaming-player.ts +82 -283
- package/src/tts/vocalizer.ts +18 -1
- package/src/utils/clipboard.ts +1 -30
- package/src/utils/jj.ts +125 -4
- package/src/utils/lang-from-path.ts +7 -0
- package/src/utils/mac-file-urls.applescript +37 -0
- package/src/utils/title-generator.ts +137 -1
- package/src/utils/tool-choice.ts +14 -0
- package/src/utils/tools-manager.ts +1 -19
- package/src/vibe/runtime.ts +852 -72
- package/src/web/search/index.ts +3 -6
- package/src/web/search/provider.ts +31 -16
- package/src/web/search/providers/xai.ts +7 -1
- package/src/web/search/types.ts +3 -0
- package/dist/types/stt/recorder.d.ts +0 -30
- package/dist/types/stt/transcriber.d.ts +0 -14
- package/dist/types/stt/wav.d.ts +0 -29
- package/dist/types/tts/player.d.ts +0 -32
- package/src/stt/recorder.ts +0 -551
- package/src/stt/transcriber.ts +0 -60
- package/src/stt/wav.ts +0 -173
- package/src/tts/player.ts +0 -137
|
@@ -73,11 +73,14 @@ import {
|
|
|
73
73
|
type SessionStorageWriter,
|
|
74
74
|
} from "./session-storage";
|
|
75
75
|
import { type SessionTitleUpdate, serializeTitleSlot } from "./session-title-slot";
|
|
76
|
+
import {
|
|
77
|
+
additionalWorkspaceDirectories,
|
|
78
|
+
normalizeSessionWorkspace,
|
|
79
|
+
normalizeWorkspaceDirectory,
|
|
80
|
+
} from "./session-workspace";
|
|
76
81
|
|
|
77
82
|
const JSONL_SUFFIX_LENGTH = ".jsonl".length;
|
|
78
83
|
const DRAFT_ONLY_SESSION_MARKER = ".draft-only-session";
|
|
79
|
-
const SUPERSEDED_COMPACTION_SUMMARY = "[Superseded compaction summary elided after a newer compaction]";
|
|
80
|
-
const SUPERSEDED_COMPACTION_SHORT_SUMMARY = "Superseded compaction elided";
|
|
81
84
|
|
|
82
85
|
function mintSessionId(): string {
|
|
83
86
|
return Bun.randomUUIDv7();
|
|
@@ -365,6 +368,34 @@ interface DiskQueueOptions {
|
|
|
365
368
|
epoch?: number;
|
|
366
369
|
}
|
|
367
370
|
|
|
371
|
+
interface AtomicEntryBatch {
|
|
372
|
+
collecting: boolean;
|
|
373
|
+
entryIds: Set<string>;
|
|
374
|
+
deferredNotifications: SessionEntry[];
|
|
375
|
+
preBatchLeafId: string | null;
|
|
376
|
+
externalLeafChanged: boolean;
|
|
377
|
+
externalLeafId: string | null;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
/**
|
|
381
|
+
* The storage may have published a write that rejected, and an authoritative
|
|
382
|
+
* repair could not be proven durable. Callers must fail closed until recovery.
|
|
383
|
+
*/
|
|
384
|
+
export class SessionPersistenceIndeterminateError extends AggregateError {
|
|
385
|
+
readonly operationError: Error;
|
|
386
|
+
readonly recoveryErrors: readonly Error[];
|
|
387
|
+
|
|
388
|
+
constructor(operationError: Error, recoveryErrors: readonly Error[]) {
|
|
389
|
+
super(
|
|
390
|
+
[operationError, ...recoveryErrors],
|
|
391
|
+
`Session persistence is indeterminate after "${operationError.message}" and authoritative repair failed.`,
|
|
392
|
+
);
|
|
393
|
+
this.name = "SessionPersistenceIndeterminateError";
|
|
394
|
+
this.operationError = operationError;
|
|
395
|
+
this.recoveryErrors = [...recoveryErrors];
|
|
396
|
+
}
|
|
397
|
+
}
|
|
398
|
+
|
|
368
399
|
/**
|
|
369
400
|
* Stores and navigates an append-only conversation journal.
|
|
370
401
|
*
|
|
@@ -380,6 +411,8 @@ interface DiskQueueOptions {
|
|
|
380
411
|
*/
|
|
381
412
|
export class SessionManager {
|
|
382
413
|
#cwd: string;
|
|
414
|
+
/** Additional workspace directories beyond cwd (multi-root). Normalized absolute, deduped, excludes cwd. */
|
|
415
|
+
#additionalDirectories: string[] = [];
|
|
383
416
|
#sessionDir: string;
|
|
384
417
|
readonly #persist: boolean;
|
|
385
418
|
readonly #storage: SessionStorage;
|
|
@@ -425,6 +458,10 @@ export class SessionManager {
|
|
|
425
458
|
#diskTail: Promise<void> = Promise.resolve();
|
|
426
459
|
#diskFailure: Error | undefined;
|
|
427
460
|
#diskFailureLogged = false;
|
|
461
|
+
/** FIFO reservation for atomic batches and authoritative recovery. */
|
|
462
|
+
#atomicPersistenceTail: Promise<void> = Promise.resolve();
|
|
463
|
+
/** Observer notifications withheld until their entries are proven durable. */
|
|
464
|
+
#pendingDurabilityNotifications: SessionEntry[] = [];
|
|
428
465
|
/** Bumped on every sync rewrite / chain reset so stale queued tasks become no-ops. */
|
|
429
466
|
#diskEpoch = 0;
|
|
430
467
|
/**
|
|
@@ -438,6 +475,8 @@ export class SessionManager {
|
|
|
438
475
|
#atomicRewriteFenceEpoch: number | null = null;
|
|
439
476
|
/** Set by synchronous appends that land while an atomic replacement is active. */
|
|
440
477
|
#atomicRewriteDirty = false;
|
|
478
|
+
/** Atomic entry batch currently staged for a full-file commit. */
|
|
479
|
+
#atomicEntryBatch: AtomicEntryBatch | undefined;
|
|
441
480
|
|
|
442
481
|
#artifactManager: ArtifactManager | null = null;
|
|
443
482
|
#artifactManagerSessionFile: string | null = null;
|
|
@@ -517,6 +556,18 @@ export class SessionManager {
|
|
|
517
556
|
return reported;
|
|
518
557
|
}
|
|
519
558
|
|
|
559
|
+
async #withAtomicPersistenceLock<T>(operation: () => Promise<T>): Promise<T> {
|
|
560
|
+
const predecessor = this.#atomicPersistenceTail;
|
|
561
|
+
const turn = Promise.withResolvers<void>();
|
|
562
|
+
this.#atomicPersistenceTail = predecessor.catch(() => undefined).then(() => turn.promise);
|
|
563
|
+
await predecessor.catch(() => undefined);
|
|
564
|
+
try {
|
|
565
|
+
return await operation();
|
|
566
|
+
} finally {
|
|
567
|
+
turn.resolve();
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
520
571
|
async #drainAndCloseWriter(): Promise<void> {
|
|
521
572
|
try {
|
|
522
573
|
await this.#scheduleDiskWork(
|
|
@@ -544,6 +595,117 @@ export class SessionManager {
|
|
|
544
595
|
await writer.close();
|
|
545
596
|
}
|
|
546
597
|
|
|
598
|
+
#latchIndeterminate(operationError: Error, recoveryErrors: readonly Error[]): SessionPersistenceIndeterminateError {
|
|
599
|
+
const error = new SessionPersistenceIndeterminateError(operationError, recoveryErrors);
|
|
600
|
+
this.#diskFailure = error;
|
|
601
|
+
if (!this.#diskFailureLogged) {
|
|
602
|
+
this.#diskFailureLogged = true;
|
|
603
|
+
logger.error("Session persistence became indeterminate.", {
|
|
604
|
+
sessionFile: this.#sessionFile,
|
|
605
|
+
error: error.message,
|
|
606
|
+
});
|
|
607
|
+
}
|
|
608
|
+
return error;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
#notifyDurableEntries(entries: readonly SessionEntry[] = []): void {
|
|
612
|
+
const notifications = [...this.#pendingDurabilityNotifications, ...entries];
|
|
613
|
+
this.#pendingDurabilityNotifications = [];
|
|
614
|
+
const seen = new Set<string>();
|
|
615
|
+
for (const entry of notifications) {
|
|
616
|
+
if (seen.has(entry.id)) continue;
|
|
617
|
+
seen.add(entry.id);
|
|
618
|
+
this.#notifyEntryAppended(entry);
|
|
619
|
+
}
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
async #authoritativelyRewriteCurrentStateLocked(operationError: Error): Promise<void> {
|
|
623
|
+
if (!this.#persist || !this.#sessionFile) return;
|
|
624
|
+
const previousDiskTail = this.#diskTail;
|
|
625
|
+
const writer = this.#writer;
|
|
626
|
+
this.#diskEpoch++;
|
|
627
|
+
const epoch = this.#diskEpoch;
|
|
628
|
+
this.#writer = undefined;
|
|
629
|
+
this.#diskTail = Promise.resolve();
|
|
630
|
+
this.#forceFileCreation = true;
|
|
631
|
+
this.#fileIsCurrent = false;
|
|
632
|
+
this.#rewriteRequired = true;
|
|
633
|
+
this.#atomicRewriteFenceEpoch = epoch;
|
|
634
|
+
if (!this.#diskFailure) this.#diskFailure = operationError;
|
|
635
|
+
try {
|
|
636
|
+
await previousDiskTail.catch(() => undefined);
|
|
637
|
+
let closeError: Error | undefined;
|
|
638
|
+
if (writer) {
|
|
639
|
+
try {
|
|
640
|
+
await writer.close();
|
|
641
|
+
} catch (error) {
|
|
642
|
+
closeError = toError(error);
|
|
643
|
+
}
|
|
644
|
+
}
|
|
645
|
+
let drainError: Error | undefined;
|
|
646
|
+
try {
|
|
647
|
+
await this.#storage.drain();
|
|
648
|
+
} catch (error) {
|
|
649
|
+
drainError = toError(error);
|
|
650
|
+
}
|
|
651
|
+
if (writer?.isOpen()) {
|
|
652
|
+
throw this.#latchIndeterminate(operationError, [
|
|
653
|
+
closeError ?? new Error("Failed to close session writer before authoritative repair."),
|
|
654
|
+
...(drainError ? [drainError] : []),
|
|
655
|
+
]);
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
do {
|
|
659
|
+
this.#atomicRewriteDirty = false;
|
|
660
|
+
const sessionFile = this.#sessionFile;
|
|
661
|
+
if (!sessionFile) {
|
|
662
|
+
throw this.#latchIndeterminate(operationError, [
|
|
663
|
+
new Error("Session file disappeared during authoritative repair."),
|
|
664
|
+
]);
|
|
665
|
+
}
|
|
666
|
+
const body = this.#fileBody();
|
|
667
|
+
try {
|
|
668
|
+
await this.#storage.writeTextAtomic(sessionFile, body, {
|
|
669
|
+
commitGuard: () => this.#diskEpoch === epoch,
|
|
670
|
+
});
|
|
671
|
+
} catch (error) {
|
|
672
|
+
const recoveryErrors = [toError(error)];
|
|
673
|
+
try {
|
|
674
|
+
await this.#storage.drain();
|
|
675
|
+
} catch (drainFailure) {
|
|
676
|
+
recoveryErrors.push(toError(drainFailure));
|
|
677
|
+
}
|
|
678
|
+
let actual: string;
|
|
679
|
+
try {
|
|
680
|
+
actual = await this.#storage.readText(sessionFile);
|
|
681
|
+
} catch (readFailure) {
|
|
682
|
+
recoveryErrors.push(toError(readFailure));
|
|
683
|
+
throw this.#latchIndeterminate(operationError, recoveryErrors);
|
|
684
|
+
}
|
|
685
|
+
if (actual !== body) {
|
|
686
|
+
recoveryErrors.push(new Error("Authoritative session repair did not match durable storage."));
|
|
687
|
+
throw this.#latchIndeterminate(operationError, recoveryErrors);
|
|
688
|
+
}
|
|
689
|
+
}
|
|
690
|
+
if (this.#diskEpoch !== epoch) {
|
|
691
|
+
throw this.#latchIndeterminate(operationError, [
|
|
692
|
+
new Error("Authoritative session repair was superseded before verification."),
|
|
693
|
+
]);
|
|
694
|
+
}
|
|
695
|
+
} while (this.#atomicRewriteDirty);
|
|
696
|
+
|
|
697
|
+
this.#fileIsCurrent = true;
|
|
698
|
+
this.#rewriteRequired = false;
|
|
699
|
+
this.#hasTitleSlot = true;
|
|
700
|
+
this.#clearDiskError();
|
|
701
|
+
} catch (error) {
|
|
702
|
+
if (error instanceof SessionPersistenceIndeterminateError) throw error;
|
|
703
|
+
throw this.#latchIndeterminate(operationError, [toError(error)]);
|
|
704
|
+
} finally {
|
|
705
|
+
if (this.#atomicRewriteFenceEpoch === epoch) this.#atomicRewriteFenceEpoch = null;
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
|
|
547
709
|
#appendWriter(): SessionStorageWriter {
|
|
548
710
|
if (!this.#sessionFile) throw new Error("Cannot open a session writer before a session file exists");
|
|
549
711
|
|
|
@@ -583,26 +745,6 @@ export class SessionManager {
|
|
|
583
745
|
return this.#forceFileCreation || this.#fileIsCurrent || this.#historyContainsAssistantMessage();
|
|
584
746
|
}
|
|
585
747
|
|
|
586
|
-
#elideSupersededCompactionsOnBranch(leafId: string | null): boolean {
|
|
587
|
-
if (!leafId) return false;
|
|
588
|
-
let changed = false;
|
|
589
|
-
for (const entry of this.#index.pathTo(leafId)) {
|
|
590
|
-
if (entry.type !== "compaction") continue;
|
|
591
|
-
if (
|
|
592
|
-
entry.summary === SUPERSEDED_COMPACTION_SUMMARY &&
|
|
593
|
-
entry.shortSummary === SUPERSEDED_COMPACTION_SHORT_SUMMARY &&
|
|
594
|
-
entry.preserveData === undefined
|
|
595
|
-
) {
|
|
596
|
-
continue;
|
|
597
|
-
}
|
|
598
|
-
entry.summary = SUPERSEDED_COMPACTION_SUMMARY;
|
|
599
|
-
entry.shortSummary = SUPERSEDED_COMPACTION_SHORT_SUMMARY;
|
|
600
|
-
entry.preserveData = undefined;
|
|
601
|
-
changed = true;
|
|
602
|
-
}
|
|
603
|
-
return changed;
|
|
604
|
-
}
|
|
605
|
-
|
|
606
748
|
/**
|
|
607
749
|
* Synchronously rewrite the whole file (header + entries) and keep no open
|
|
608
750
|
* writer; the next append re-opens one. `writeTextSync` returns with the
|
|
@@ -690,6 +832,12 @@ export class SessionManager {
|
|
|
690
832
|
|
|
691
833
|
#appendToSessionFile(entry: SessionEntry): void {
|
|
692
834
|
if (!this.#persist || !this.#sessionFile) return;
|
|
835
|
+
if (this.#atomicEntryBatch) {
|
|
836
|
+
this.#fileIsCurrent = false;
|
|
837
|
+
this.#rewriteRequired = true;
|
|
838
|
+
this.#atomicRewriteDirty = true;
|
|
839
|
+
return;
|
|
840
|
+
}
|
|
693
841
|
if (this.#diskFailure) throw this.#diskFailure;
|
|
694
842
|
|
|
695
843
|
// Lazy gate: a brand-new session is not written until it has an assistant
|
|
@@ -805,6 +953,14 @@ export class SessionManager {
|
|
|
805
953
|
parentSession: options?.parentSession,
|
|
806
954
|
providerPromptCacheKey: options?.providerPromptCacheKey,
|
|
807
955
|
};
|
|
956
|
+
const workspace = normalizeSessionWorkspace({
|
|
957
|
+
cwd: this.#cwd,
|
|
958
|
+
directories: options?.additionalDirectories ?? [],
|
|
959
|
+
});
|
|
960
|
+
this.#additionalDirectories = additionalWorkspaceDirectories(workspace);
|
|
961
|
+
if (this.#additionalDirectories.length > 0) {
|
|
962
|
+
this.#header.additionalDirectories = [...this.#additionalDirectories];
|
|
963
|
+
}
|
|
808
964
|
this.#titleUpdatedAt = timestamp;
|
|
809
965
|
|
|
810
966
|
this.#entries = [];
|
|
@@ -853,11 +1009,44 @@ export class SessionManager {
|
|
|
853
1009
|
};
|
|
854
1010
|
}
|
|
855
1011
|
|
|
1012
|
+
#setLeaf(id: string | null): void {
|
|
1013
|
+
this.#index.setLeaf(id);
|
|
1014
|
+
const batch = this.#atomicEntryBatch;
|
|
1015
|
+
if (batch && !batch.collecting) {
|
|
1016
|
+
batch.externalLeafChanged = true;
|
|
1017
|
+
batch.externalLeafId = id;
|
|
1018
|
+
}
|
|
1019
|
+
}
|
|
1020
|
+
|
|
856
1021
|
#recordEntry(entry: SessionEntry): void {
|
|
857
1022
|
this.#entries.push(entry);
|
|
858
1023
|
this.#index.insert(entry);
|
|
1024
|
+
const batch = this.#atomicEntryBatch;
|
|
1025
|
+
if (batch?.collecting) batch.entryIds.add(entry.id);
|
|
1026
|
+
if (batch && !batch.collecting) {
|
|
1027
|
+
batch.externalLeafChanged = true;
|
|
1028
|
+
batch.externalLeafId = entry.id;
|
|
1029
|
+
}
|
|
859
1030
|
this.#appendToSessionFile(entry);
|
|
860
|
-
|
|
1031
|
+
if (batch) batch.deferredNotifications.push(entry);
|
|
1032
|
+
else this.#notifyEntryAppended(entry);
|
|
1033
|
+
}
|
|
1034
|
+
|
|
1035
|
+
#rollbackAtomicEntryBatch(batch: AtomicEntryBatch): void {
|
|
1036
|
+
const retainedAncestor = (id: string | null): string | null => {
|
|
1037
|
+
const seen = new Set<string>();
|
|
1038
|
+
while (id && batch.entryIds.has(id) && !seen.has(id)) {
|
|
1039
|
+
seen.add(id);
|
|
1040
|
+
id = this.#index.get(id)?.parentId ?? null;
|
|
1041
|
+
}
|
|
1042
|
+
return id;
|
|
1043
|
+
};
|
|
1044
|
+
const retained = this.#entries.filter(entry => !batch.entryIds.has(entry.id));
|
|
1045
|
+
for (const entry of retained) entry.parentId = retainedAncestor(entry.parentId);
|
|
1046
|
+
const restoredLeaf = retainedAncestor(batch.externalLeafChanged ? batch.externalLeafId : batch.preBatchLeafId);
|
|
1047
|
+
this.#entries = retained;
|
|
1048
|
+
this.#index.rebuild(retained);
|
|
1049
|
+
this.#index.setLeaf(restoredLeaf && this.#index.has(restoredLeaf) ? restoredLeaf : null);
|
|
861
1050
|
}
|
|
862
1051
|
|
|
863
1052
|
#draftPath(): string | null {
|
|
@@ -988,6 +1177,7 @@ export class SessionManager {
|
|
|
988
1177
|
this.#forceFileCreation = snapshot.onDisk;
|
|
989
1178
|
this.#draftOnlySessionCleanupArmed = snapshot.draftOnlySessionCleanupArmed;
|
|
990
1179
|
this.#applyEntries(snapshot.header, [...snapshot.entries]);
|
|
1180
|
+
this.#additionalDirectories = snapshot.header.additionalDirectories ?? [];
|
|
991
1181
|
this.#sessionName = snapshot.sessionName;
|
|
992
1182
|
this.#titleSource = snapshot.titleSource;
|
|
993
1183
|
this.#titleUpdatedAt = snapshot.titleUpdatedAt;
|
|
@@ -1040,6 +1230,7 @@ export class SessionManager {
|
|
|
1040
1230
|
}
|
|
1041
1231
|
|
|
1042
1232
|
this.#applyEntries(header, fileEntries.slice(1) as SessionEntry[]);
|
|
1233
|
+
this.#additionalDirectories = header.additionalDirectories ?? [];
|
|
1043
1234
|
this.#titleUpdatedAt = titleSlot?.updatedAt ?? header.timestamp;
|
|
1044
1235
|
this.#hasTitleSlot = titleSlot !== undefined;
|
|
1045
1236
|
this.#fileIsCurrent = true;
|
|
@@ -1090,6 +1281,7 @@ export class SessionManager {
|
|
|
1090
1281
|
titleSource: this.#header.titleSource ?? this.#titleSource,
|
|
1091
1282
|
timestamp,
|
|
1092
1283
|
cwd: this.#cwd,
|
|
1284
|
+
additionalDirectories: this.#additionalDirectories.length > 0 ? [...this.#additionalDirectories] : undefined,
|
|
1093
1285
|
parentSession: parentSessionId,
|
|
1094
1286
|
providerPromptCacheKey: this.#header.providerPromptCacheKey ?? parentSessionId,
|
|
1095
1287
|
};
|
|
@@ -1197,6 +1389,14 @@ export class SessionManager {
|
|
|
1197
1389
|
this.#cwd = resolvedCwd;
|
|
1198
1390
|
this.#sessionDir = nextSessionDir;
|
|
1199
1391
|
this.#header.cwd = resolvedCwd;
|
|
1392
|
+
// Re-filter additional roots: the new cwd may have been an additional root,
|
|
1393
|
+
// or it may now contain/subsume one. Re-normalize to keep the invariant
|
|
1394
|
+
// that cwd is never also listed as an additional directory.
|
|
1395
|
+
if (this.#additionalDirectories.length > 0) {
|
|
1396
|
+
this.#additionalDirectories = this.#additionalDirectories.filter(d => d !== resolvedCwd);
|
|
1397
|
+
this.#header.additionalDirectories =
|
|
1398
|
+
this.#additionalDirectories.length > 0 ? this.#additionalDirectories : undefined;
|
|
1399
|
+
}
|
|
1200
1400
|
|
|
1201
1401
|
// Rewrite at the new location when the file already existed (update cwd) or
|
|
1202
1402
|
// there is in-memory output worth materializing; otherwise stay lazy.
|
|
@@ -1220,6 +1420,92 @@ export class SessionManager {
|
|
|
1220
1420
|
await this.#rewriteAtomically();
|
|
1221
1421
|
}
|
|
1222
1422
|
|
|
1423
|
+
/**
|
|
1424
|
+
* Stage a synchronous group of entry appends and publish the resulting full
|
|
1425
|
+
* journal with one atomic replace. A failed publish removes only the staged
|
|
1426
|
+
* entries, preserves/reparents entries appended concurrently, restores the
|
|
1427
|
+
* prior durable file view, and clears the failed writer latch for retry.
|
|
1428
|
+
*
|
|
1429
|
+
* The callback MUST be synchronous.
|
|
1430
|
+
*/
|
|
1431
|
+
appendEntriesAtomically<T>(append: () => T): Promise<T> {
|
|
1432
|
+
return this.#withAtomicPersistenceLock(() => this.#appendEntriesAtomicallyLocked(append));
|
|
1433
|
+
}
|
|
1434
|
+
|
|
1435
|
+
async #appendEntriesAtomicallyLocked<T>(append: () => T): Promise<T> {
|
|
1436
|
+
if (!this.#persist || !this.#sessionFile) return append();
|
|
1437
|
+
if (this.#atomicEntryBatch) throw new Error("Atomic persistence lock ownership was violated.");
|
|
1438
|
+
try {
|
|
1439
|
+
await this.ensureOnDisk();
|
|
1440
|
+
await this.flush();
|
|
1441
|
+
} catch (error) {
|
|
1442
|
+
const operationError = toError(error);
|
|
1443
|
+
await this.#authoritativelyRewriteCurrentStateLocked(operationError);
|
|
1444
|
+
this.#notifyDurableEntries();
|
|
1445
|
+
throw error;
|
|
1446
|
+
}
|
|
1447
|
+
|
|
1448
|
+
const batch: AtomicEntryBatch = {
|
|
1449
|
+
collecting: true,
|
|
1450
|
+
entryIds: new Set(),
|
|
1451
|
+
deferredNotifications: [],
|
|
1452
|
+
preBatchLeafId: this.#index.leafId(),
|
|
1453
|
+
externalLeafChanged: false,
|
|
1454
|
+
externalLeafId: null,
|
|
1455
|
+
};
|
|
1456
|
+
this.#atomicEntryBatch = batch;
|
|
1457
|
+
let result!: T;
|
|
1458
|
+
try {
|
|
1459
|
+
try {
|
|
1460
|
+
result = append();
|
|
1461
|
+
} finally {
|
|
1462
|
+
batch.collecting = false;
|
|
1463
|
+
}
|
|
1464
|
+
await this.#rewriteAtomically();
|
|
1465
|
+
if (!this.#fileIsCurrent || this.#rewriteRequired) {
|
|
1466
|
+
throw new Error("Atomic session batch was superseded before commit.");
|
|
1467
|
+
}
|
|
1468
|
+
this.#atomicEntryBatch = undefined;
|
|
1469
|
+
this.#notifyDurableEntries(batch.deferredNotifications);
|
|
1470
|
+
return result;
|
|
1471
|
+
} catch (error) {
|
|
1472
|
+
batch.collecting = false;
|
|
1473
|
+
const operationError = toError(error);
|
|
1474
|
+
this.#rollbackAtomicEntryBatch(batch);
|
|
1475
|
+
try {
|
|
1476
|
+
await this.#authoritativelyRewriteCurrentStateLocked(operationError);
|
|
1477
|
+
} catch (repairError) {
|
|
1478
|
+
const retainedNotifications = batch.deferredNotifications.filter(entry => !batch.entryIds.has(entry.id));
|
|
1479
|
+
this.#pendingDurabilityNotifications.push(...retainedNotifications);
|
|
1480
|
+
this.#atomicEntryBatch = undefined;
|
|
1481
|
+
this.#fileIsCurrent = false;
|
|
1482
|
+
this.#rewriteRequired = true;
|
|
1483
|
+
if (repairError instanceof SessionPersistenceIndeterminateError) throw repairError;
|
|
1484
|
+
throw this.#latchIndeterminate(operationError, [toError(repairError)]);
|
|
1485
|
+
}
|
|
1486
|
+
const retainedNotifications = batch.deferredNotifications.filter(entry => !batch.entryIds.has(entry.id));
|
|
1487
|
+
this.#atomicEntryBatch = undefined;
|
|
1488
|
+
this.#notifyDurableEntries(retainedNotifications);
|
|
1489
|
+
throw error;
|
|
1490
|
+
}
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
/**
|
|
1494
|
+
* Replace an uncertain append tail with the authoritative in-memory journal.
|
|
1495
|
+
* Callers must only use this for monotonic recovery where every retained
|
|
1496
|
+
* entry remains intended (for example, an explicit terminal tombstone).
|
|
1497
|
+
*/
|
|
1498
|
+
recoverPersistenceFromCurrentState(): Promise<void> {
|
|
1499
|
+
return this.#withAtomicPersistenceLock(async () => {
|
|
1500
|
+
if (!this.#persist || !this.#sessionFile) return;
|
|
1501
|
+
if (this.#atomicEntryBatch) throw new Error("Atomic persistence lock ownership was violated.");
|
|
1502
|
+
const operationError =
|
|
1503
|
+
this.#diskFailure ?? new Error("Authoritative session persistence recovery was requested.");
|
|
1504
|
+
await this.#authoritativelyRewriteCurrentStateLocked(operationError);
|
|
1505
|
+
this.#notifyDurableEntries();
|
|
1506
|
+
});
|
|
1507
|
+
}
|
|
1508
|
+
|
|
1223
1509
|
/** Flush pending writes. Call before switching sessions or on shutdown. */
|
|
1224
1510
|
async flush(): Promise<void> {
|
|
1225
1511
|
if (!this.#persist || !this.#sessionFile) return;
|
|
@@ -1240,6 +1526,7 @@ export class SessionManager {
|
|
|
1240
1526
|
*/
|
|
1241
1527
|
flushSync(): void {
|
|
1242
1528
|
if (!this.#persist || !this.#sessionFile) return;
|
|
1529
|
+
if (this.#atomicEntryBatch) throw new Error("Cannot synchronously flush during an atomic session batch.");
|
|
1243
1530
|
if (this.#diskFailure) throw this.#diskFailure;
|
|
1244
1531
|
if (this.#fileIsCurrent && !this.#rewriteRequired) {
|
|
1245
1532
|
const writerError = this.#writer?.getError();
|
|
@@ -1303,6 +1590,79 @@ export class SessionManager {
|
|
|
1303
1590
|
return this.#cwd;
|
|
1304
1591
|
}
|
|
1305
1592
|
|
|
1593
|
+
/** Additional workspace directories beyond cwd (multi-root), absolute and normalized. */
|
|
1594
|
+
getAdditionalDirectories(): string[] {
|
|
1595
|
+
return [...this.#additionalDirectories];
|
|
1596
|
+
}
|
|
1597
|
+
|
|
1598
|
+
/**
|
|
1599
|
+
* Persist a workspace-directory change to the session header. Respects the
|
|
1600
|
+
* lazy-persistence gate: a session with no durable output yet keeps the
|
|
1601
|
+
* change in memory (the header lands with the first real write), so seeding
|
|
1602
|
+
* roots at launch never materializes an empty resumable session file.
|
|
1603
|
+
*/
|
|
1604
|
+
async #persistWorkspaceDirectoriesChange(): Promise<void> {
|
|
1605
|
+
if (!this.#persist || !this.#sessionFile || !this.#shouldHaveSessionFile()) return;
|
|
1606
|
+
this.#rewriteRequired = true;
|
|
1607
|
+
await this.#rewriteAtomically();
|
|
1608
|
+
}
|
|
1609
|
+
|
|
1610
|
+
/**
|
|
1611
|
+
* Add a workspace directory. Normalizes (relative to cwd), dedupes, rejects
|
|
1612
|
+
* the cwd itself, persists to the session header, and triggers an atomic
|
|
1613
|
+
* rewrite so the change survives a crash. Returns the resolved absolute
|
|
1614
|
+
* path or `null` when the directory was already present (no-op).
|
|
1615
|
+
*/
|
|
1616
|
+
async addWorkspaceDirectory(directory: string): Promise<string | null> {
|
|
1617
|
+
const resolved = normalizeWorkspaceDirectory(directory, this.#cwd);
|
|
1618
|
+
if (resolved === path.resolve(this.#cwd)) {
|
|
1619
|
+
throw new Error("The current working directory is already the primary workspace root.");
|
|
1620
|
+
}
|
|
1621
|
+
if (this.#additionalDirectories.includes(resolved)) return null;
|
|
1622
|
+
this.#additionalDirectories = [...this.#additionalDirectories, resolved];
|
|
1623
|
+
this.#header.additionalDirectories = this.#additionalDirectories;
|
|
1624
|
+
await this.#persistWorkspaceDirectoriesChange();
|
|
1625
|
+
return resolved;
|
|
1626
|
+
}
|
|
1627
|
+
|
|
1628
|
+
/**
|
|
1629
|
+
* Remove a workspace directory by absolute or cwd-relative path. Persists
|
|
1630
|
+
* the trimmed header. Returns the resolved path that was removed, or
|
|
1631
|
+
* `null` when the directory was not an additional root (no-op).
|
|
1632
|
+
*/
|
|
1633
|
+
async removeWorkspaceDirectory(directory: string): Promise<string | null> {
|
|
1634
|
+
const resolved = normalizeWorkspaceDirectory(directory, this.#cwd);
|
|
1635
|
+
const idx = this.#additionalDirectories.findIndex(p => path.resolve(p) === resolved);
|
|
1636
|
+
if (idx === -1) return null;
|
|
1637
|
+
this.#additionalDirectories = this.#additionalDirectories.filter((_, i) => i !== idx);
|
|
1638
|
+
if (this.#additionalDirectories.length === 0) {
|
|
1639
|
+
this.#header.additionalDirectories = undefined;
|
|
1640
|
+
} else {
|
|
1641
|
+
this.#header.additionalDirectories = this.#additionalDirectories;
|
|
1642
|
+
}
|
|
1643
|
+
await this.#persistWorkspaceDirectoriesChange();
|
|
1644
|
+
return resolved;
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
/** Seed additional directories from settings or a passed list. Also called on resumed sessions with --add-dir; persists the updated header when the session file is already durable. No-op when the normalized list is unchanged (avoids rewriting large session files on every startup). */
|
|
1648
|
+
async setAdditionalDirectories(directories: string[]): Promise<void> {
|
|
1649
|
+
const workspace = normalizeSessionWorkspace({ cwd: this.#cwd, directories });
|
|
1650
|
+
const next = additionalWorkspaceDirectories(workspace);
|
|
1651
|
+
if (
|
|
1652
|
+
next.length === this.#additionalDirectories.length &&
|
|
1653
|
+
next.every((d, i) => d === this.#additionalDirectories[i])
|
|
1654
|
+
) {
|
|
1655
|
+
return;
|
|
1656
|
+
}
|
|
1657
|
+
this.#additionalDirectories = next;
|
|
1658
|
+
if (this.#additionalDirectories.length > 0) {
|
|
1659
|
+
this.#header.additionalDirectories = this.#additionalDirectories;
|
|
1660
|
+
} else {
|
|
1661
|
+
this.#header.additionalDirectories = undefined;
|
|
1662
|
+
}
|
|
1663
|
+
await this.#persistWorkspaceDirectoriesChange();
|
|
1664
|
+
}
|
|
1665
|
+
|
|
1306
1666
|
getUsageStatistics(): UsageStatistics {
|
|
1307
1667
|
return this.#index.usageSnapshot();
|
|
1308
1668
|
}
|
|
@@ -1597,7 +1957,6 @@ export class SessionManager {
|
|
|
1597
1957
|
fromExtension?: boolean,
|
|
1598
1958
|
preserveData?: Record<string, unknown>,
|
|
1599
1959
|
): string {
|
|
1600
|
-
const elidedSupersededCompactions = this.#elideSupersededCompactionsOnBranch(this.#index.leafId());
|
|
1601
1960
|
const entry: CompactionEntry<T> = {
|
|
1602
1961
|
type: "compaction",
|
|
1603
1962
|
...this.#freshEntryFields(),
|
|
@@ -1610,9 +1969,6 @@ export class SessionManager {
|
|
|
1610
1969
|
preserveData,
|
|
1611
1970
|
};
|
|
1612
1971
|
this.#recordEntry(entry);
|
|
1613
|
-
if (elidedSupersededCompactions) {
|
|
1614
|
-
void this.#rewriteAtomically().catch(err => this.#noteDiskFailure(err));
|
|
1615
|
-
}
|
|
1616
1972
|
return entry.id;
|
|
1617
1973
|
}
|
|
1618
1974
|
|
|
@@ -1782,19 +2138,19 @@ export class SessionManager {
|
|
|
1782
2138
|
*/
|
|
1783
2139
|
branch(branchFromId: string): void {
|
|
1784
2140
|
if (!this.#index.has(branchFromId)) throw new Error(`Entry ${branchFromId} not found`);
|
|
1785
|
-
this.#
|
|
2141
|
+
this.#setLeaf(branchFromId);
|
|
1786
2142
|
}
|
|
1787
2143
|
|
|
1788
2144
|
/** Reset the leaf to null so the next append creates a new root entry. */
|
|
1789
2145
|
resetLeaf(): void {
|
|
1790
|
-
this.#
|
|
2146
|
+
this.#setLeaf(null);
|
|
1791
2147
|
}
|
|
1792
2148
|
|
|
1793
2149
|
/** Like branch(), but also records a branch_summary of the abandoned path. */
|
|
1794
2150
|
branchWithSummary(branchFromId: string | null, summary: string, details?: unknown, fromExtension?: boolean): string {
|
|
1795
2151
|
if (branchFromId !== null && !this.#index.has(branchFromId)) throw new Error(`Entry ${branchFromId} not found`);
|
|
1796
2152
|
|
|
1797
|
-
this.#
|
|
2153
|
+
this.#setLeaf(branchFromId);
|
|
1798
2154
|
const entry: BranchSummaryEntry = {
|
|
1799
2155
|
type: "branch_summary",
|
|
1800
2156
|
id: generateId(this.#index),
|
|
@@ -1836,6 +2192,7 @@ export class SessionManager {
|
|
|
1836
2192
|
timestamp,
|
|
1837
2193
|
cwd: this.#cwd,
|
|
1838
2194
|
parentSession: this.#persist ? sourceSessionFile : undefined,
|
|
2195
|
+
additionalDirectories: this.#additionalDirectories.length > 0 ? [...this.#additionalDirectories] : undefined,
|
|
1839
2196
|
};
|
|
1840
2197
|
|
|
1841
2198
|
const labels: LabelEntry[] = [];
|
|
@@ -1956,6 +2313,9 @@ export class SessionManager {
|
|
|
1956
2313
|
);
|
|
1957
2314
|
manager.#header.title = sourceHeader?.title;
|
|
1958
2315
|
manager.#header.titleSource = sourceHeader?.titleSource;
|
|
2316
|
+
manager.#additionalDirectories = (sourceHeader?.additionalDirectories ?? []).filter(d => d !== path.resolve(cwd));
|
|
2317
|
+
manager.#header.additionalDirectories =
|
|
2318
|
+
manager.#additionalDirectories.length > 0 ? manager.#additionalDirectories : undefined;
|
|
1959
2319
|
manager.#sessionName = manager.#header.title;
|
|
1960
2320
|
manager.#titleSource = manager.#header.titleSource;
|
|
1961
2321
|
manager.#titleUpdatedAt = nowIso();
|