@gajae-code/coding-agent 0.14.2 → 0.15.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +120 -1
- package/dist/types/async/job-manager.d.ts +4 -1
- package/dist/types/autoresearch/dashboard.d.ts +29 -4
- package/dist/types/autoresearch/data-context.d.ts +25 -0
- package/dist/types/autoresearch/git.d.ts +65 -7
- package/dist/types/autoresearch/harness.d.ts +58 -0
- package/dist/types/autoresearch/prompts.d.ts +51 -0
- package/dist/types/autoresearch/runs.d.ts +173 -0
- package/dist/types/autoresearch/types.d.ts +9 -139
- package/dist/types/capability/types.d.ts +20 -0
- package/dist/types/cli/accounts-cli.d.ts +1 -0
- package/dist/types/commands/{rlm.d.ts → autoresearch.d.ts} +2 -4
- package/dist/types/config/autorouting-contract.d.ts +146 -0
- package/dist/types/config/autorouting-generator.d.ts +40 -0
- package/dist/types/config/autorouting-tier-map.d.ts +16844 -0
- package/dist/types/config/autorouting.d.ts +35 -0
- package/dist/types/config/model-discovery-manager.d.ts +8 -1
- package/dist/types/config/model-registry.d.ts +30 -1
- package/dist/types/config/provider-selection-policy.d.ts +23 -0
- package/dist/types/config/settings-schema.d.ts +61 -3
- package/dist/types/config/settings.d.ts +3 -0
- package/dist/types/coordinator/contract.d.ts +1 -1
- package/dist/types/coordinator-mcp/durability.d.ts +29 -0
- package/dist/types/coordinator-mcp/event-webhook.d.ts +50 -0
- package/dist/types/coordinator-mcp/question-state.d.ts +1 -1
- package/dist/types/coordinator-mcp/server.d.ts +14 -0
- package/dist/types/crash/index-store.d.ts +3 -0
- package/dist/types/crash/record-loader.d.ts +2 -0
- package/dist/types/crash/upstream/relay.d.ts +5 -3
- package/dist/types/cursor.d.ts +12 -0
- package/dist/types/defaults/gjc-defaults.d.ts +35 -1
- package/dist/types/defaults/gjc-skills.generated.d.ts +1 -1
- package/dist/types/discovery/helpers.d.ts +1 -1
- package/dist/types/edit/index.d.ts +2 -1
- package/dist/types/eval/py/executor.d.ts +2 -0
- package/dist/types/extensibility/extensions/loader.d.ts +32 -0
- package/dist/types/extensibility/gjc-plugins/types.d.ts +1 -1
- package/dist/types/extensibility/skills.d.ts +2 -0
- package/dist/types/gjc-runtime/autoresearch-runtime.d.ts +194 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +1 -1
- package/dist/types/gjc-runtime/psmux-detect.d.ts +1 -1
- package/dist/types/gjc-runtime/python-transcript.d.ts +19 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +2 -8
- package/dist/types/gjc-runtime/session-layout.d.ts +6 -3
- package/dist/types/gjc-runtime/state-renderer.d.ts +0 -22
- package/dist/types/gjc-runtime/state-schema.d.ts +7 -7
- package/dist/types/gjc-runtime/tmux-common.d.ts +2 -2
- package/dist/types/gjc-runtime/ultragoal-change-set.d.ts +38 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +2 -24
- package/dist/types/gjc-runtime/ultragoal-validation-policy.d.ts +73 -0
- package/dist/types/gjc-runtime/workflow-recovery-projection.d.ts +100 -0
- package/dist/types/hooks/skill-keywords.d.ts +1 -1
- package/dist/types/main.d.ts +0 -10
- package/dist/types/modes/acp/acp-agent.d.ts +13 -6
- package/dist/types/modes/components/model-selector.d.ts +14 -0
- package/dist/types/modes/components/skill-hud/render.d.ts +6 -1
- package/dist/types/modes/components/smart-routing-panel.d.ts +82 -0
- package/dist/types/modes/controllers/selector-controller.d.ts +14 -0
- package/dist/types/modes/interactive-mode.d.ts +3 -0
- package/dist/types/modes/types.d.ts +9 -0
- package/dist/types/rlm/artifacts.d.ts +4 -4
- package/dist/types/runtime/optional-runtime-services.d.ts +2 -1
- package/dist/types/runtime/workspace-tree-service.d.ts +5 -1
- package/dist/types/runtime-mcp/config.d.ts +6 -0
- package/dist/types/runtime-mcp/manager.d.ts +10 -0
- package/dist/types/sdk/acp/adapter.d.ts +0 -4
- package/dist/types/sdk/broker/broker.d.ts +1 -1
- package/dist/types/sdk/broker/lifecycle.d.ts +57 -2
- package/dist/types/sdk/broker/session-index.d.ts +15 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +6 -0
- package/dist/types/sdk/bus/chat-daemon-runtime.d.ts +4 -0
- package/dist/types/sdk/bus/index.d.ts +8 -0
- package/dist/types/sdk/bus/kind-aware-reconciliation.d.ts +2 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +12 -2
- package/dist/types/sdk/client/client.d.ts +39 -4
- package/dist/types/sdk/host/host.d.ts +1 -0
- package/dist/types/sdk/host/internal-autorouting-state.d.ts +3 -0
- package/dist/types/sdk/host/model-pin.d.ts +29 -0
- package/dist/types/sdk/host/session-runtime.d.ts +2 -1
- package/dist/types/sdk/lifecycle-session.d.ts +13 -0
- package/dist/types/sdk/router/session-router.d.ts +27 -1
- package/dist/types/sdk/session.d.ts +11 -1
- package/dist/types/session/agent-session.d.ts +36 -6
- package/dist/types/session/artifacts.d.ts +16 -1
- package/dist/types/session/internal/managed-session-storage.d.ts +7 -0
- package/dist/types/session/session-manager.d.ts +84 -2
- package/dist/types/session/session-staging-paths.d.ts +3 -0
- package/dist/types/skill-state/canonical-skills.d.ts +1 -1
- package/dist/types/skill-state/workflow-hud.d.ts +12 -21
- package/dist/types/skill-state/workflow-mutation-guard.d.ts +1 -0
- package/dist/types/slash-commands/helpers/autorouting-status.d.ts +8 -0
- package/dist/types/system-prompt.d.ts +23 -0
- package/dist/types/task/executor.d.ts +35 -4
- package/dist/types/task/index.d.ts +17 -3
- package/dist/types/task/receipt.d.ts +3 -1
- package/dist/types/task/types.d.ts +144 -0
- package/dist/types/tools/atomic-file-write.d.ts +72 -0
- package/dist/types/tools/descriptors.d.ts +1 -0
- package/dist/types/tools/index.d.ts +36 -1
- package/dist/types/tools/move-session.d.ts +40 -0
- package/dist/types/tools/python.d.ts +12 -0
- package/dist/types/tools/resource-gc.d.ts +0 -3
- package/dist/types/tools/search.d.ts +8 -1
- package/dist/types/tools/skill.d.ts +9 -4
- package/dist/types/tools/subagent.d.ts +33 -3
- package/dist/types/utils/xml-escape.d.ts +2 -0
- package/dist/types/workflow/workflow-intent-diff.d.ts +2 -2
- package/dist/types/workflow/workflow-intent-report-consumer.d.ts +1 -1
- package/package.json +11 -12
- package/scripts/build-sdk-package-smoke.ts +7 -5
- package/scripts/check-autorouting-tier-map.ts +183 -0
- package/scripts/generate-sdk-operation-inventory.ts +12 -0
- package/scripts/generate-tool-catalog.ts +40 -5
- package/src/async/job-manager.ts +5 -2
- package/src/autoresearch/dashboard.ts +134 -314
- package/src/autoresearch/data-context.ts +35 -0
- package/src/autoresearch/git.ts +186 -26
- package/src/autoresearch/{helpers.ts → harness.ts} +65 -40
- package/src/autoresearch/{prompt-setup.md → prompts/prompt-setup.md} +7 -7
- package/src/autoresearch/prompts/prompt.md +103 -0
- package/src/autoresearch/prompts.ts +126 -0
- package/src/autoresearch/runs.ts +708 -0
- package/src/autoresearch/types.ts +9 -150
- package/src/capability/index.ts +4 -4
- package/src/capability/types.ts +20 -0
- package/src/cli/accounts-cli.ts +21 -5
- package/src/cli/config-cli.ts +25 -1
- package/src/cli/fast-help.ts +1 -2
- package/src/cli/list-models.ts +5 -8
- package/src/cli/setup-cli.ts +8 -0
- package/src/cli.ts +2 -21
- package/src/commands/autoresearch.ts +33 -0
- package/src/commands/coordinator.ts +15 -0
- package/src/commands/launch.ts +2 -4
- package/src/commands/quick-lane.ts +1 -1
- package/src/commands/sdk.ts +24 -0
- package/src/commands/state.ts +1 -1
- package/src/config/autorouting-contract.ts +474 -0
- package/src/config/autorouting-generator.ts +238 -0
- package/src/config/autorouting-tier-map.ts +6108 -0
- package/src/config/autorouting.ts +120 -0
- package/src/config/model-discovery-manager.ts +42 -11
- package/src/config/model-registry.ts +348 -68
- package/src/config/provider-selection-policy.ts +60 -9
- package/src/config/settings-schema.ts +195 -6
- package/src/config/settings.ts +62 -1
- package/src/config.ts +2 -3
- package/src/coordinator/contract.ts +0 -1
- package/src/coordinator-mcp/codex-handoff.ts +68 -42
- package/src/coordinator-mcp/durability.ts +200 -0
- package/src/coordinator-mcp/event-webhook.ts +278 -0
- package/src/coordinator-mcp/question-state.ts +109 -46
- package/src/coordinator-mcp/server.ts +932 -504
- package/src/crash/index-store.ts +126 -22
- package/src/crash/record-loader.ts +7 -0
- package/src/crash/upstream/relay.ts +82 -34
- package/src/cursor.ts +84 -1
- package/src/debug/index.ts +26 -0
- package/src/defaults/gjc/skills/autoresearch/SKILL.md +116 -0
- package/src/defaults/gjc/skills/autoresearch/auto-critic.md +46 -0
- package/src/defaults/gjc/skills/autoresearch/auto-iterate.md +42 -0
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +17 -37
- package/src/defaults/gjc/skills/ralplan/SKILL.md +21 -18
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +6 -17
- package/src/defaults/gjc/skills/ultragoal/ai-slop-cleaner.md +2 -2
- package/src/defaults/gjc-defaults.ts +89 -2
- package/src/defaults/gjc-skills.generated.ts +24 -17
- package/src/discovery/builtin.ts +18 -5
- package/src/discovery/helpers.ts +6 -6
- package/src/edit/index.ts +5 -2
- package/src/eval/py/executor.ts +8 -0
- package/src/extensibility/extensions/loader.ts +135 -13
- package/src/extensibility/extensions/wrapper.ts +1 -2
- package/src/extensibility/gjc-plugins/schema.ts +1 -1
- package/src/extensibility/runtime-skill-discovery.ts +2 -2
- package/src/extensibility/skill-management.ts +2 -3
- package/src/extensibility/skills.ts +16 -8
- package/src/gjc-runtime/autoresearch-runtime.ts +1509 -0
- package/src/gjc-runtime/gc-render.ts +0 -1
- package/src/gjc-runtime/gc-runtime.ts +1 -11
- package/src/gjc-runtime/launch-tmux.ts +3 -3
- package/src/gjc-runtime/psmux-detect.ts +2 -2
- package/src/gjc-runtime/python-transcript.ts +58 -0
- package/src/gjc-runtime/ralplan-runtime.ts +6 -18
- package/src/gjc-runtime/session-layout.ts +18 -7
- package/src/gjc-runtime/session-state-sidecar.ts +58 -9
- package/src/gjc-runtime/state-renderer.ts +0 -38
- package/src/gjc-runtime/state-runtime.ts +43 -22
- package/src/gjc-runtime/state-schema.ts +1 -1
- package/src/gjc-runtime/tmux-common.ts +7 -7
- package/src/gjc-runtime/ultragoal-change-set.ts +232 -11
- package/src/gjc-runtime/ultragoal-runtime.ts +309 -114
- package/src/gjc-runtime/ultragoal-validation-policy.ts +247 -0
- package/src/gjc-runtime/workflow-command-ref.ts +15 -11
- package/src/gjc-runtime/workflow-manifest.generated.json +413 -532
- package/src/gjc-runtime/workflow-manifest.ts +43 -83
- package/src/gjc-runtime/workflow-recovery-projection.ts +616 -0
- package/src/hooks/skill-keywords.ts +3 -3
- package/src/hooks/skill-state.ts +2 -2
- package/src/internal-urls/docs-index.generated.ts +26 -24
- package/src/lsp/index.ts +25 -5
- package/src/main.ts +2 -27
- package/src/modes/acp/acp-agent.ts +200 -75
- package/src/modes/components/model-selector.ts +216 -13
- package/src/modes/components/oauth-selector.ts +5 -5
- package/src/modes/components/skill-hud/render.ts +173 -35
- package/src/modes/components/smart-routing-panel.ts +531 -0
- package/src/modes/components/tool-status-header.ts +6 -3
- package/src/modes/controllers/command-controller.ts +17 -7
- package/src/modes/controllers/input-controller.ts +44 -9
- package/src/modes/controllers/runtime-mcp-command-controller.ts +4 -0
- package/src/modes/controllers/selector-controller.ts +298 -65
- package/src/modes/interactive-mode.ts +8 -1
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +1 -1
- package/src/modes/types.ts +9 -1
- package/src/prompts/agents/planner.md +1 -1
- package/src/prompts/system/system-prompt.md +1 -1
- package/src/prompts/system/volatile-project-context.md +2 -1
- package/src/prompts/tools/move-session.md +15 -0
- package/src/prompts/tools/python.md +12 -0
- package/src/prompts/tools/skill-discovery.md +1 -1
- package/src/prompts/tools/skill.md +2 -2
- package/src/prompts/tools/task-summary.md +1 -0
- package/src/prompts/tools/task.md +6 -0
- package/src/rlm/artifacts.ts +12 -10
- package/src/runtime/optional-runtime-services.ts +3 -2
- package/src/runtime/workspace-tree-service.ts +11 -3
- package/src/runtime-mcp/config.ts +8 -1
- package/src/runtime-mcp/manager.ts +33 -0
- package/src/sdk/acp/adapter.ts +48 -1
- package/src/sdk/broker/broker.ts +30 -2
- package/src/sdk/broker/discovery.ts +217 -2
- package/src/sdk/broker/lifecycle.ts +385 -59
- package/src/sdk/broker/session-index.ts +243 -48
- package/src/sdk/broker/transport.ts +1 -0
- package/src/sdk/bus/chat-daemon-control.ts +8 -2
- package/src/sdk/bus/chat-daemon-runtime.ts +5 -0
- package/src/sdk/bus/index.ts +208 -11
- package/src/sdk/bus/kind-aware-reconciliation.ts +16 -0
- package/src/sdk/bus/reconciliation-store.ts +27 -5
- package/src/sdk/bus/telegram-daemon-contract.ts +12 -2
- package/src/sdk/client/client.ts +204 -22
- package/src/sdk/host/host.ts +16 -0
- package/src/sdk/host/internal-autorouting-state.ts +13 -0
- package/src/sdk/host/model-pin.ts +101 -0
- package/src/sdk/host/reverse-leases.ts +2 -1
- package/src/sdk/host/session-runtime.test.ts +224 -1
- package/src/sdk/host/session-runtime.ts +125 -37
- package/src/sdk/lifecycle/service.ts +3 -1
- package/src/sdk/lifecycle-session.ts +42 -1
- package/src/sdk/protocol/operation-inventory.generated.json +66 -0
- package/src/sdk/router/session-router.ts +375 -36
- package/src/sdk/session.ts +547 -47
- package/src/session/account-inventory.ts +1 -1
- package/src/session/agent-session.ts +765 -189
- package/src/session/artifacts.ts +234 -4
- package/src/session/internal/managed-session-storage.ts +91 -4
- package/src/session/provider-safety-stop-hint.ts +3 -2
- package/src/session/session-manager.ts +775 -16
- package/src/session/session-staging-paths.ts +8 -0
- package/src/setup/hermes/templates/operator-instructions.v1.md +0 -1
- package/src/setup/host-plugin-setup.ts +2 -2
- package/src/skill-state/active-state.ts +2 -2
- package/src/skill-state/canonical-skills.ts +1 -1
- package/src/skill-state/initial-phase.ts +1 -1
- package/src/skill-state/workflow-hud.ts +41 -41
- package/src/skill-state/workflow-mutation-guard.ts +26 -7
- package/src/slash-commands/builtin-registry.ts +77 -0
- package/src/slash-commands/helpers/autorouting-status.ts +55 -0
- package/src/system-prompt.ts +102 -5
- package/src/task/discovery.ts +2 -3
- package/src/task/executor.ts +617 -43
- package/src/task/index.ts +262 -15
- package/src/task/receipt.ts +30 -0
- package/src/task/render.ts +11 -1
- package/src/task/types.ts +316 -0
- package/src/tools/atomic-file-write.ts +594 -0
- package/src/tools/bisect.ts +3 -2
- package/src/tools/descriptors.test.ts +3 -0
- package/src/tools/descriptors.ts +24 -0
- package/src/tools/image-gen.ts +5 -1
- package/src/tools/index.ts +62 -5
- package/src/tools/move-session.ts +107 -0
- package/src/tools/python.ts +205 -0
- package/src/tools/read.ts +88 -15
- package/src/tools/renderers.ts +2 -0
- package/src/tools/resource-gc.ts +8 -177
- package/src/tools/search.ts +30 -6
- package/src/tools/skill-discovery.ts +1 -0
- package/src/tools/skill.ts +37 -7
- package/src/tools/subagent-render.ts +119 -13
- package/src/tools/subagent.ts +91 -74
- package/src/tools/tool-catalog.generated.ts +64 -2
- package/src/tools/write.ts +16 -12
- package/src/utils/xml-escape.ts +14 -0
- package/src/web/search/providers/codex.ts +2 -2
- package/src/workflow/workflow-intent-diff.ts +13 -12
- package/src/workflow/workflow-intent-report-consumer.ts +6 -6
- package/dist/types/autoresearch/helpers.d.ts +0 -24
- package/dist/types/autoresearch/index.d.ts +0 -2
- package/dist/types/autoresearch/state.d.ts +0 -17
- package/dist/types/autoresearch/storage.d.ts +0 -142
- package/dist/types/autoresearch/tools/init-experiment.d.ts +0 -31
- package/dist/types/autoresearch/tools/log-experiment.d.ts +0 -23
- package/dist/types/autoresearch/tools/run-experiment.d.ts +0 -8
- package/dist/types/autoresearch/tools/update-notes.d.ts +0 -12
- package/dist/types/commands/team.d.ts +0 -24
- package/dist/types/gjc-runtime/team-gc.d.ts +0 -7
- package/dist/types/gjc-runtime/team-launch.d.ts +0 -61
- package/dist/types/gjc-runtime/team-notify.d.ts +0 -45
- package/dist/types/gjc-runtime/team-runtime.d.ts +0 -336
- package/dist/types/gjc-runtime/team-store.d.ts +0 -168
- package/dist/types/gjc-runtime/team-worker-heartbeat.d.ts +0 -43
- package/dist/types/gjc-runtime/team-worker-memory-guard.d.ts +0 -92
- package/dist/types/gjc-runtime/team-workers.d.ts +0 -83
- package/dist/types/rlm/index.d.ts +0 -47
- package/dist/types/rlm/preset.d.ts +0 -23
- package/dist/types/rlm/python-tool.d.ts +0 -16
- package/src/autoresearch/command-resume.md +0 -14
- package/src/autoresearch/index.ts +0 -536
- package/src/autoresearch/prompt.md +0 -103
- package/src/autoresearch/resume-message.md +0 -10
- package/src/autoresearch/state.ts +0 -273
- package/src/autoresearch/storage.ts +0 -699
- package/src/autoresearch/tools/init-experiment.ts +0 -272
- package/src/autoresearch/tools/log-experiment.ts +0 -524
- package/src/autoresearch/tools/run-experiment.ts +0 -475
- package/src/autoresearch/tools/update-notes.ts +0 -109
- package/src/commands/rlm.ts +0 -19
- package/src/commands/team.ts +0 -238
- package/src/defaults/gjc/skills/deep-interview/auto-research-greenfield.md +0 -42
- package/src/defaults/gjc/skills/team/SKILL.md +0 -467
- package/src/gjc-runtime/team-gc.ts +0 -49
- package/src/gjc-runtime/team-launch.ts +0 -404
- package/src/gjc-runtime/team-notify.ts +0 -326
- package/src/gjc-runtime/team-runtime.ts +0 -5923
- package/src/gjc-runtime/team-store.ts +0 -878
- package/src/gjc-runtime/team-worker-heartbeat.ts +0 -149
- package/src/gjc-runtime/team-worker-memory-guard.ts +0 -363
- package/src/gjc-runtime/team-workers.ts +0 -1123
- package/src/prompts/system/rlm-report-command.md +0 -1
- package/src/prompts/system/rlm-research.md +0 -23
- package/src/rlm/index.ts +0 -365
- package/src/rlm/preset.ts +0 -76
- package/src/rlm/python-tool.ts +0 -68
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { AsyncLocalStorage } from "node:async_hooks";
|
|
1
2
|
import * as crypto from "node:crypto";
|
|
2
3
|
import * as fs from "node:fs";
|
|
3
4
|
|
|
@@ -21,6 +22,13 @@ import type * as native from "@gajae-code/natives";
|
|
|
21
22
|
function nativeSessionManager(): typeof import("@gajae-code/natives") {
|
|
22
23
|
return require("@gajae-code/natives") as typeof import("@gajae-code/natives");
|
|
23
24
|
}
|
|
25
|
+
const cwdTransitionAls = new AsyncLocalStorage<symbol>();
|
|
26
|
+
type CwdReadLeaseContext = { active: boolean; owner: symbol };
|
|
27
|
+
const cwdReadLeaseAls = new AsyncLocalStorage<CwdReadLeaseContext>();
|
|
28
|
+
const CWD_NOFOLLOW_OPEN_FLAGS =
|
|
29
|
+
fs.constants.O_RDONLY |
|
|
30
|
+
(typeof fs.constants.O_DIRECTORY === "number" ? fs.constants.O_DIRECTORY : 0) |
|
|
31
|
+
(process.platform === "win32" ? 0 : (fs.constants.O_NOFOLLOW ?? 0));
|
|
24
32
|
|
|
25
33
|
import { getTerminalId } from "@gajae-code/tui";
|
|
26
34
|
import {
|
|
@@ -194,6 +202,7 @@ import {
|
|
|
194
202
|
stripInternalDetailsFields,
|
|
195
203
|
} from "./messages";
|
|
196
204
|
import { type SessionManagerReadAccess, sessionManagerReadCapability } from "./session-manager-internal";
|
|
205
|
+
import { isStagedSessionPath, SESSION_STAGING_DIRNAME } from "./session-staging-paths";
|
|
197
206
|
import type {
|
|
198
207
|
ManagedSessionSecurityContext,
|
|
199
208
|
SessionStorage,
|
|
@@ -281,6 +290,103 @@ interface PreparedNewSessionState extends PreparedNewSession {
|
|
|
281
290
|
type ResidentTransitionFailurePolicy = "install-staged" | "memory-fallback" | "retain-and-throw" | "memory-only";
|
|
282
291
|
type ResidentBlobMissingPolicy = "throw" | "placeholder";
|
|
283
292
|
|
|
293
|
+
const MAX_STAGED_ATTEMPT_ID_LENGTH = 128;
|
|
294
|
+
|
|
295
|
+
function assertSafeStagedAttemptId(attemptId: string): void {
|
|
296
|
+
if (!/^[A-Za-z0-9_-]{1,128}$/.test(attemptId) || attemptId.length > MAX_STAGED_ATTEMPT_ID_LENGTH)
|
|
297
|
+
throw new Error("Unsafe artifact attempt id");
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const ATTEMPT_REMAP_STRUCTURAL_KEYS = new Set(["id", "parentId", "timestamp"]);
|
|
301
|
+
const ARTIFACT_REFERENCE_KEYS = new Set([
|
|
302
|
+
"artifactId",
|
|
303
|
+
"artifactIds",
|
|
304
|
+
"artifactRef",
|
|
305
|
+
"artifactRefs",
|
|
306
|
+
"agentId",
|
|
307
|
+
"agentIds",
|
|
308
|
+
"agentRef",
|
|
309
|
+
"agentRefs",
|
|
310
|
+
]);
|
|
311
|
+
/**
|
|
312
|
+
* Trailing-selector grammar mirrored from `splitPathAndSel` / `splitInternalUrlSel` in
|
|
313
|
+
* `src/tools/path-utils.ts`. It is duplicated rather than imported because `path-utils` pulls in
|
|
314
|
+
* `internal-urls`, which reaches back into the session layer and would create an import cycle.
|
|
315
|
+
* The boundary red-team suite cross-checks these against the real parser so drift fails a test.
|
|
316
|
+
*/
|
|
317
|
+
const SELECTOR_RANGE_RE = /^L?\d+(?:[-+]L?\d+|-)?(?:,L?\d+(?:[-+]L?\d+|-)?)*$/i;
|
|
318
|
+
const SELECTOR_TAIL_RE = /^(?:L?\d+(?:[-+]L?\d+|-)?(?:,L?\d+(?:[-+]L?\d+|-)?)*|raw|conflicts)$/i;
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Decide whether a `:` directly after `<scheme>://<id>` terminates the id (so the id is a remappable
|
|
322
|
+
* reference and the tail is an opaque selector) or is part of an opaque authority (so the id must be
|
|
323
|
+
* left alone). `artifact://` splits unconditionally at the first colon; every other scheme requires a
|
|
324
|
+
* strict selector tail, so `agent://3:bogus` keeps `3:bogus` as the authority and must NOT be remapped.
|
|
325
|
+
*/
|
|
326
|
+
function colonTerminatesUriId(scheme: string, tail: string): boolean {
|
|
327
|
+
if (scheme.toLowerCase() === "artifact") return true;
|
|
328
|
+
if (SELECTOR_TAIL_RE.test(tail)) return true;
|
|
329
|
+
const innerColon = tail.lastIndexOf(":");
|
|
330
|
+
if (innerColon <= 0) return false;
|
|
331
|
+
const head = tail.slice(0, innerColon);
|
|
332
|
+
const last = tail.slice(innerColon + 1);
|
|
333
|
+
const headIsRaw = /^raw$/i.test(head);
|
|
334
|
+
const lastIsRaw = /^raw$/i.test(last);
|
|
335
|
+
return (headIsRaw && SELECTOR_RANGE_RE.test(last)) || (SELECTOR_RANGE_RE.test(head) && lastIsRaw);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
function remapArtifactReferenceString(value: string, idMap: ReadonlyMap<string, string>, exactId = false): string {
|
|
339
|
+
const exact = exactId ? idMap.get(value) : undefined;
|
|
340
|
+
if (exact !== undefined) return exact;
|
|
341
|
+
// Only re-key when the ENTIRE value is a single URI reference token. A string carrying prose or
|
|
342
|
+
// multiple tokens is opaque content we do not own, and rewriting inside it caused real corruption
|
|
343
|
+
// in earlier revisions. Everything after the id (selector, query, fragment) is likewise opaque and
|
|
344
|
+
// is preserved verbatim, so nested ids inside those payloads are never re-keyed.
|
|
345
|
+
if (/[\s"'<>]/.test(value)) return value;
|
|
346
|
+
const head = value.match(/^(artifact|agent):\/\/([0-9]+)/i);
|
|
347
|
+
if (!head) return value;
|
|
348
|
+
const protocol = head[1];
|
|
349
|
+
const mapped = idMap.get(head[2]);
|
|
350
|
+
if (mapped === undefined) return value;
|
|
351
|
+
const rest = value.slice(head[0].length);
|
|
352
|
+
if (rest !== "" && !/^[:/?#]/.test(rest)) return value;
|
|
353
|
+
if (rest.startsWith(":") && !colonTerminatesUriId(protocol, rest.slice(1))) return value;
|
|
354
|
+
return `${protocol}://${mapped}${rest}`;
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
function remapAttemptReferencesInEntries(entries: readonly FileEntry[], idMap: ReadonlyMap<string, string>): void {
|
|
358
|
+
if (idMap.size === 0) return;
|
|
359
|
+
const seen = new WeakSet<object>();
|
|
360
|
+
const visit = (value: unknown, key?: string): unknown => {
|
|
361
|
+
if (
|
|
362
|
+
typeof value === "number" &&
|
|
363
|
+
key !== undefined &&
|
|
364
|
+
ARTIFACT_REFERENCE_KEYS.has(key) &&
|
|
365
|
+
Number.isSafeInteger(value)
|
|
366
|
+
) {
|
|
367
|
+
const mapped = idMap.get(String(value));
|
|
368
|
+
return mapped === undefined ? value : Number(mapped);
|
|
369
|
+
}
|
|
370
|
+
if (typeof value === "string") {
|
|
371
|
+
return remapArtifactReferenceString(value, idMap, key !== undefined && ARTIFACT_REFERENCE_KEYS.has(key));
|
|
372
|
+
}
|
|
373
|
+
if (!value || typeof value !== "object" || seen.has(value)) return value;
|
|
374
|
+
seen.add(value);
|
|
375
|
+
if (Array.isArray(value)) {
|
|
376
|
+
for (let index = 0; index < value.length; index++) value[index] = visit(value[index], key);
|
|
377
|
+
return value;
|
|
378
|
+
}
|
|
379
|
+
const record = value as Record<string, unknown>;
|
|
380
|
+
for (const [childKey, child] of Object.entries(record)) {
|
|
381
|
+
if (ATTEMPT_REMAP_STRUCTURAL_KEYS.has(childKey)) continue;
|
|
382
|
+
const next = visit(child, childKey);
|
|
383
|
+
if (next !== child) record[childKey] = next;
|
|
384
|
+
}
|
|
385
|
+
return value;
|
|
386
|
+
};
|
|
387
|
+
for (const entry of entries) visit(entry);
|
|
388
|
+
}
|
|
389
|
+
|
|
284
390
|
type ResidentTransitionSource =
|
|
285
391
|
| {
|
|
286
392
|
mode: "materialize";
|
|
@@ -3395,6 +3501,7 @@ async function readTerminalBreadcrumb(cwd: string): Promise<string | null> {
|
|
|
3395
3501
|
return null;
|
|
3396
3502
|
}
|
|
3397
3503
|
|
|
3504
|
+
if (isStagedSessionPath(sessionFile)) return null;
|
|
3398
3505
|
const inspected = inspectResumeSessionFile(sessionFile, new FileSessionStorage());
|
|
3399
3506
|
if ("kind" in inspected) {
|
|
3400
3507
|
if (inspected.reason !== "missing") return null;
|
|
@@ -3408,7 +3515,8 @@ async function readTerminalBreadcrumb(cwd: string): Promise<string | null> {
|
|
|
3408
3515
|
const listing = listManagedCandidates(resolved.scope);
|
|
3409
3516
|
if (listing.kind !== "complete") return null;
|
|
3410
3517
|
const migrated = listing.owned.filter(
|
|
3411
|
-
candidate =>
|
|
3518
|
+
candidate =>
|
|
3519
|
+
!isStagedSessionPath(candidate.path) && path.basename(candidate.path) === path.basename(sessionFile),
|
|
3412
3520
|
);
|
|
3413
3521
|
return migrated.length === 1 ? migrated[0]!.path : null;
|
|
3414
3522
|
}
|
|
@@ -3423,9 +3531,14 @@ async function readTerminalBreadcrumb(cwd: string): Promise<string | null> {
|
|
|
3423
3531
|
if (resolved.kind !== "resolved") return null;
|
|
3424
3532
|
const listing = listManagedCandidates(resolved.scope);
|
|
3425
3533
|
if (listing.kind !== "complete") return null;
|
|
3426
|
-
const exact = listing.owned.find(
|
|
3534
|
+
const exact = listing.owned.find(
|
|
3535
|
+
candidate =>
|
|
3536
|
+
!isStagedSessionPath(candidate.path) && path.resolve(candidate.path) === path.resolve(sessionFile),
|
|
3537
|
+
);
|
|
3427
3538
|
if (exact) return exact.path;
|
|
3428
|
-
const byIdentity = listing.owned.find(
|
|
3539
|
+
const byIdentity = listing.owned.find(
|
|
3540
|
+
candidate => !isStagedSessionPath(candidate.path) && candidate.sessionId === header.id,
|
|
3541
|
+
);
|
|
3429
3542
|
if (byIdentity) return byIdentity.path;
|
|
3430
3543
|
return pathIsWithin(sessionsRoot, path.resolve(sessionFile)) ? null : path.resolve(sessionFile);
|
|
3431
3544
|
} catch (err) {
|
|
@@ -6237,9 +6350,11 @@ const PROJECT_SESSION_SCAN_MAX_DIRECTORIES = 4096;
|
|
|
6237
6350
|
const PROJECT_SESSION_SCAN_MAX_FILES = 1000;
|
|
6238
6351
|
|
|
6239
6352
|
function isProjectSessionTranscriptPath(projectGjcDir: string, filePath: string): boolean {
|
|
6353
|
+
if (isStagedSessionPath(filePath)) return false;
|
|
6240
6354
|
const relative = path.relative(projectGjcDir, filePath);
|
|
6241
6355
|
if (relative.startsWith("..") || path.isAbsolute(relative)) return false;
|
|
6242
6356
|
const segments = relative.split(path.sep);
|
|
6357
|
+
if (segments.includes(SESSION_STAGING_DIRNAME)) return false;
|
|
6243
6358
|
if (segments.length === 1) return true;
|
|
6244
6359
|
const parent = segments.at(-2);
|
|
6245
6360
|
return parent === "agent-session" || segments.includes("sessions");
|
|
@@ -6250,7 +6365,7 @@ function isProjectSessionTranscriptPath(projectGjcDir: string, filePath: string)
|
|
|
6250
6365
|
* Runtime token/audit JSONL files are excluded by requiring a known transcript
|
|
6251
6366
|
* container (`agent-session` or `sessions`).
|
|
6252
6367
|
*/
|
|
6253
|
-
function listProjectSessionTranscriptFiles(cwd: string): string[] {
|
|
6368
|
+
export function listProjectSessionTranscriptFiles(cwd: string): string[] {
|
|
6254
6369
|
const projectGjcDir = path.join(path.resolve(cwd), ".gjc");
|
|
6255
6370
|
let rootStat: fs.Stats;
|
|
6256
6371
|
try {
|
|
@@ -6276,6 +6391,7 @@ function listProjectSessionTranscriptFiles(cwd: string): string[] {
|
|
|
6276
6391
|
if (entry.isSymbolicLink()) continue;
|
|
6277
6392
|
const entryPath = path.join(directory, entry.name);
|
|
6278
6393
|
if (entry.isDirectory()) {
|
|
6394
|
+
if (entry.name === SESSION_STAGING_DIRNAME) continue;
|
|
6279
6395
|
directories.push(entryPath);
|
|
6280
6396
|
continue;
|
|
6281
6397
|
}
|
|
@@ -7180,11 +7296,48 @@ export class SessionManager {
|
|
|
7180
7296
|
/** Defense-in-depth (#4443): one-shot warn for adjacent private thinking blocks in persisted assistant transcripts. */
|
|
7181
7297
|
#warnedAdjacentThinkingPersist = false;
|
|
7182
7298
|
#closeRetryPending = false;
|
|
7299
|
+
/** Serializes model, SDK, and ACP cwd transitions; dispose joins this tail. */
|
|
7300
|
+
#cwdTransitionTail: Promise<void> = Promise.resolve();
|
|
7301
|
+
#cwdTransitionOwner: symbol | undefined;
|
|
7302
|
+
#cwdReadLeaseOwner = Symbol("cwd-read-lease-owner");
|
|
7303
|
+
#cwdGeneration = 0;
|
|
7304
|
+
/** Number of tool executions currently holding a shared read lease on `cwd`. */
|
|
7305
|
+
#cwdReaderCount = 0;
|
|
7306
|
+
/** Resolved when the last outstanding read lease is released. */
|
|
7307
|
+
#cwdReadersDrained: (() => void) | undefined;
|
|
7308
|
+
#cwdReadersIdle: Promise<void> = Promise.resolve();
|
|
7309
|
+
/** Set while a writer is queued or running so new readers do not starve it. */
|
|
7310
|
+
#cwdWriterPending = 0;
|
|
7183
7311
|
/** Depth of the non-yielding same-session persistence fence (reentrancy counter). */
|
|
7184
7312
|
#persistenceFenceDepth = 0;
|
|
7185
7313
|
/** Publication fence counter carried by the mutable `.spill.commit` marker. */
|
|
7186
7314
|
#commitGen = 0;
|
|
7187
7315
|
/** Failed staged persistence retains its exact writer and temporary pathname for retryable cleanup. */
|
|
7316
|
+
/** Candidate-owned session publication state; set only by staged factories. */
|
|
7317
|
+
#stagedPublication:
|
|
7318
|
+
| {
|
|
7319
|
+
finalSessionFile: string;
|
|
7320
|
+
stagedSessionFile: string;
|
|
7321
|
+
finalDestination: SessionDestination;
|
|
7322
|
+
managedParentStore?: ManagedSessionDescendantStore;
|
|
7323
|
+
/** Subtree store bound to the staging directory; basename reads stay authority-safe. */
|
|
7324
|
+
managedStagingStore?: ManagedSessionDescendantStore;
|
|
7325
|
+
attemptId: string;
|
|
7326
|
+
committed: boolean;
|
|
7327
|
+
discarded: boolean;
|
|
7328
|
+
publishedFinalSnapshot?: ManagedFileSnapshot;
|
|
7329
|
+
publishedFinalBytes?: Buffer;
|
|
7330
|
+
deferArtifactFinalize?: boolean;
|
|
7331
|
+
/**
|
|
7332
|
+
* A publish that native code could neither prove nor disprove. The transcript may
|
|
7333
|
+
* already be visible at the destination, so no later cleanup may reclaim the
|
|
7334
|
+
* staging or artifacts it references.
|
|
7335
|
+
*/
|
|
7336
|
+
preservedUnproven?: boolean;
|
|
7337
|
+
}
|
|
7338
|
+
| undefined;
|
|
7339
|
+
#stagedArtifactParent: ArtifactManager | null = null;
|
|
7340
|
+
#stagedCommitArtifactParent: ArtifactManager | null = null;
|
|
7188
7341
|
#preparedNewSessionCleanupInProgress = false;
|
|
7189
7342
|
/** Active cold-sidecar runtime (retirement + lazy resolution). Undefined when disabled. */
|
|
7190
7343
|
#sidecarRuntime: SessionMemorySidecarRuntime | undefined = undefined;
|
|
@@ -9141,7 +9294,7 @@ export class SessionManager {
|
|
|
9141
9294
|
this.#effectiveSessionMemoryMode(transcriptSize) === "enabled" &&
|
|
9142
9295
|
this.#storage.existsSync(`${sidecarRoot}/.session-memory.spill.commit`);
|
|
9143
9296
|
if (boundedTranscriptAdmitted && (await this.#tryInitSessionFileFromSidecar(resolvedSessionFile))) {
|
|
9144
|
-
writeTerminalBreadcrumb(
|
|
9297
|
+
this.#writeTerminalBreadcrumb(resolvedSessionFile);
|
|
9145
9298
|
revalidateStrictResume();
|
|
9146
9299
|
return;
|
|
9147
9300
|
}
|
|
@@ -9151,7 +9304,7 @@ export class SessionManager {
|
|
|
9151
9304
|
this.#sessionMemoryMode = "shadow";
|
|
9152
9305
|
this.#sessionMemoryAutoDisabledReason = "sidecar_reload_failures";
|
|
9153
9306
|
}
|
|
9154
|
-
writeTerminalBreadcrumb(
|
|
9307
|
+
this.#writeTerminalBreadcrumb(resolvedSessionFile);
|
|
9155
9308
|
revalidateStrictResume();
|
|
9156
9309
|
return;
|
|
9157
9310
|
}
|
|
@@ -9167,7 +9320,7 @@ export class SessionManager {
|
|
|
9167
9320
|
this.#applyFreshSessionMetadata(fresh);
|
|
9168
9321
|
this.#commitResidentTextStoreTransition(prepared);
|
|
9169
9322
|
this.#retireEphemeralArtifacts();
|
|
9170
|
-
writeTerminalBreadcrumb(
|
|
9323
|
+
this.#writeTerminalBreadcrumb(resolvedSessionFile);
|
|
9171
9324
|
await this.#rewriteFile();
|
|
9172
9325
|
this.#flushed = true;
|
|
9173
9326
|
this.#ensuredOnDisk = true;
|
|
@@ -9195,7 +9348,7 @@ export class SessionManager {
|
|
|
9195
9348
|
this.#applyFreshSessionMetadata(fresh);
|
|
9196
9349
|
this.#commitResidentTextStoreTransition(prepared);
|
|
9197
9350
|
this.#retireEphemeralArtifacts();
|
|
9198
|
-
writeTerminalBreadcrumb(
|
|
9351
|
+
this.#writeTerminalBreadcrumb(resolvedSessionFile);
|
|
9199
9352
|
await this.#rewriteFile();
|
|
9200
9353
|
this.#flushed = true;
|
|
9201
9354
|
this.#ensuredOnDisk = true;
|
|
@@ -9228,7 +9381,7 @@ export class SessionManager {
|
|
|
9228
9381
|
this.#titleSource = header?.titleSource;
|
|
9229
9382
|
this.#needsFullRewriteOnNextPersist = migrationApplied;
|
|
9230
9383
|
this.#commitResidentTextStoreTransition(prepared);
|
|
9231
|
-
writeTerminalBreadcrumb(
|
|
9384
|
+
this.#writeTerminalBreadcrumb(resolvedSessionFile);
|
|
9232
9385
|
this.#flushed = true;
|
|
9233
9386
|
this.#ensuredOnDisk = true;
|
|
9234
9387
|
this.#adoptManagedPersistIdentity(resolvedSessionFile);
|
|
@@ -9240,6 +9393,11 @@ export class SessionManager {
|
|
|
9240
9393
|
}
|
|
9241
9394
|
}
|
|
9242
9395
|
|
|
9396
|
+
#writeTerminalBreadcrumb(sessionFile: string): void {
|
|
9397
|
+
if (this.#stagedPublication && !this.#stagedPublication.committed) return;
|
|
9398
|
+
writeTerminalBreadcrumb(this.cwd, sessionFile);
|
|
9399
|
+
}
|
|
9400
|
+
|
|
9243
9401
|
async #hydrateExistingSession(
|
|
9244
9402
|
sessionFile: string,
|
|
9245
9403
|
entries: FileEntry[],
|
|
@@ -10449,13 +10607,209 @@ export class SessionManager {
|
|
|
10449
10607
|
}
|
|
10450
10608
|
}
|
|
10451
10609
|
|
|
10610
|
+
async #assertCwdTargetIdentity(
|
|
10611
|
+
resolvedCwd: string,
|
|
10612
|
+
options: {
|
|
10613
|
+
expectedIdentity?: { dev: bigint; ino: bigint };
|
|
10614
|
+
targetHandle?: { stat: (opts: { bigint: true }) => Promise<fs.BigIntStats> };
|
|
10615
|
+
},
|
|
10616
|
+
): Promise<void> {
|
|
10617
|
+
let opened: fs.BigIntStats | undefined;
|
|
10618
|
+
if (options.targetHandle) {
|
|
10619
|
+
opened = await options.targetHandle.stat({ bigint: true });
|
|
10620
|
+
if (!opened.isDirectory()) {
|
|
10621
|
+
throw new Error(
|
|
10622
|
+
`Refusing to move through a replaced path: ${resolvedCwd} is no longer the validated directory.`,
|
|
10623
|
+
);
|
|
10624
|
+
}
|
|
10625
|
+
}
|
|
10626
|
+
let observed: fs.BigIntStats;
|
|
10627
|
+
try {
|
|
10628
|
+
observed = await fs.promises.lstat(resolvedCwd, { bigint: true });
|
|
10629
|
+
} catch {
|
|
10630
|
+
throw new Error(`Directory identity unavailable at state-changing boundary: ${resolvedCwd}`);
|
|
10631
|
+
}
|
|
10632
|
+
if (observed.isSymbolicLink() || !observed.isDirectory()) {
|
|
10633
|
+
throw new Error(
|
|
10634
|
+
`Refusing to move through a replaced path: ${resolvedCwd} is no longer the validated directory.`,
|
|
10635
|
+
);
|
|
10636
|
+
}
|
|
10637
|
+
if (opened && (observed.dev !== opened.dev || observed.ino !== opened.ino)) {
|
|
10638
|
+
throw new Error(`Refusing to move: target identity changed at ${resolvedCwd}.`);
|
|
10639
|
+
}
|
|
10640
|
+
const expected = options.expectedIdentity;
|
|
10641
|
+
const pinned = opened ?? observed;
|
|
10642
|
+
if (expected && (pinned.dev !== expected.dev || pinned.ino !== expected.ino)) {
|
|
10643
|
+
throw new Error(`Refusing to move: target identity changed at ${resolvedCwd}.`);
|
|
10644
|
+
}
|
|
10645
|
+
}
|
|
10646
|
+
|
|
10647
|
+
/**
|
|
10648
|
+
* Serialize every cwd transition (model, TUI, SDK/ACP). Re-entry is allowed
|
|
10649
|
+
* only for the async context that already owns the lock — unrelated callers
|
|
10650
|
+
* queue on the tail instead of skipping it.
|
|
10651
|
+
*/
|
|
10652
|
+
async runExclusiveCwdTransition<T>(fn: () => Promise<T>): Promise<T> {
|
|
10653
|
+
const owner = this.#cwdTransitionOwner;
|
|
10654
|
+
if (owner !== undefined && cwdTransitionAls.getStore() === owner) {
|
|
10655
|
+
return fn();
|
|
10656
|
+
}
|
|
10657
|
+
const previous = this.#cwdTransitionTail;
|
|
10658
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
10659
|
+
this.#cwdTransitionTail = previous.then(
|
|
10660
|
+
() => promise,
|
|
10661
|
+
() => promise,
|
|
10662
|
+
);
|
|
10663
|
+
// Announce the writer BEFORE awaiting the queue so readers arriving during
|
|
10664
|
+
// the wait queue behind it rather than starving it indefinitely.
|
|
10665
|
+
this.#cwdWriterPending += 1;
|
|
10666
|
+
const token = Symbol("cwd-transition");
|
|
10667
|
+
try {
|
|
10668
|
+
await previous.catch(() => {});
|
|
10669
|
+
// A reader that entered before this writer was announced still holds the
|
|
10670
|
+
// old cwd; the transition may not commit until every such lease is
|
|
10671
|
+
// released, otherwise an in-flight tool resolves paths across the move.
|
|
10672
|
+
while (this.#cwdReaderCount > 0) await this.#cwdReadersIdle;
|
|
10673
|
+
this.#cwdTransitionOwner = token;
|
|
10674
|
+
return await cwdTransitionAls.run(token, fn);
|
|
10675
|
+
} finally {
|
|
10676
|
+
if (this.#cwdTransitionOwner === token) this.#cwdTransitionOwner = undefined;
|
|
10677
|
+
this.#cwdWriterPending -= 1;
|
|
10678
|
+
resolve();
|
|
10679
|
+
}
|
|
10680
|
+
}
|
|
10681
|
+
|
|
10682
|
+
/**
|
|
10683
|
+
* Run `fn` under a shared read lease on `cwd`.
|
|
10684
|
+
*
|
|
10685
|
+
* Tools that resolve relative paths against the session cwd must hold this
|
|
10686
|
+
* lease across their WHOLE execution, not merely re-check a generation before
|
|
10687
|
+
* they start: the check-then-yield shape lets a move commit inside the tool's
|
|
10688
|
+
* first `await`, so a command admitted for root A would execute in root B.
|
|
10689
|
+
* Writers wait for outstanding leases to drain, so the cwd observed at lease
|
|
10690
|
+
* acquisition stays authoritative until the lease is released.
|
|
10691
|
+
*/
|
|
10692
|
+
async runWithCwdReadLease<T>(fn: () => Promise<T>): Promise<T> {
|
|
10693
|
+
const activeReadLease = cwdReadLeaseAls.getStore();
|
|
10694
|
+
// Nested tool dispatch (for example eval -> tool bridge) inherits the
|
|
10695
|
+
// outer lease's async context. Re-entering that lease must not wait behind
|
|
10696
|
+
// a writer that is already queued: the writer is waiting for the outer
|
|
10697
|
+
// lease, and waiting here would deadlock the session permanently.
|
|
10698
|
+
if (activeReadLease?.active && activeReadLease.owner === this.#cwdReadLeaseOwner) return fn();
|
|
10699
|
+
const owner = this.#cwdTransitionOwner;
|
|
10700
|
+
// The writer's own async context already holds exclusive access; taking a
|
|
10701
|
+
// read lease there would wait on itself.
|
|
10702
|
+
if (owner !== undefined && cwdTransitionAls.getStore() === owner) return fn();
|
|
10703
|
+
while (this.#cwdWriterPending > 0) await this.#cwdTransitionTail.catch(() => {});
|
|
10704
|
+
if (this.#cwdReaderCount === 0) {
|
|
10705
|
+
const { promise, resolve } = Promise.withResolvers<void>();
|
|
10706
|
+
this.#cwdReadersIdle = promise;
|
|
10707
|
+
this.#cwdReadersDrained = resolve;
|
|
10708
|
+
}
|
|
10709
|
+
this.#cwdReaderCount += 1;
|
|
10710
|
+
const readLease: CwdReadLeaseContext = { active: true, owner: this.#cwdReadLeaseOwner };
|
|
10711
|
+
return cwdReadLeaseAls.run(readLease, async () => {
|
|
10712
|
+
try {
|
|
10713
|
+
return await fn();
|
|
10714
|
+
} finally {
|
|
10715
|
+
readLease.active = false;
|
|
10716
|
+
this.#cwdReaderCount -= 1;
|
|
10717
|
+
if (this.#cwdReaderCount === 0) {
|
|
10718
|
+
const drained = this.#cwdReadersDrained;
|
|
10719
|
+
this.#cwdReadersDrained = undefined;
|
|
10720
|
+
drained?.();
|
|
10721
|
+
}
|
|
10722
|
+
}
|
|
10723
|
+
});
|
|
10724
|
+
}
|
|
10725
|
+
|
|
10726
|
+
/** Wait for any in-flight exclusive cwd transition to settle. */
|
|
10727
|
+
async joinCwdTransition(): Promise<void> {
|
|
10728
|
+
await this.#cwdTransitionTail;
|
|
10729
|
+
}
|
|
10730
|
+
getCwdGeneration(): number {
|
|
10731
|
+
return this.#cwdGeneration;
|
|
10732
|
+
}
|
|
10733
|
+
|
|
10734
|
+
#ownsCwdTransition(): boolean {
|
|
10735
|
+
const owner = this.#cwdTransitionOwner;
|
|
10736
|
+
return owner !== undefined && cwdTransitionAls.getStore() === owner;
|
|
10737
|
+
}
|
|
10738
|
+
static async openNoFollowDirectory(dir: string): Promise<fs.promises.FileHandle> {
|
|
10739
|
+
return fs.promises.open(dir, CWD_NOFOLLOW_OPEN_FLAGS);
|
|
10740
|
+
}
|
|
10741
|
+
|
|
10742
|
+
/**
|
|
10743
|
+
* Owner of this process's cwd, or undefined when no session has claimed it.
|
|
10744
|
+
*
|
|
10745
|
+
* `process.chdir` and the caches keyed off it are process-global, so only one
|
|
10746
|
+
* session may drive them. Ownership is an explicit claim rather than an
|
|
10747
|
+
* inference from `process.cwd() === session.cwd`: two sessions launched at the
|
|
10748
|
+
* same root both satisfy that comparison, and letting either one act on it
|
|
10749
|
+
* lets a move in one session chdir the process under its sibling.
|
|
10750
|
+
*/
|
|
10751
|
+
static #processCwdOwner: WeakRef<SessionManager> | undefined;
|
|
10752
|
+
|
|
10753
|
+
/**
|
|
10754
|
+
* Claim process-cwd authority for `manager` when it is unowned or the prior
|
|
10755
|
+
* owner has been collected. Returns whether `manager` holds the claim.
|
|
10756
|
+
*/
|
|
10757
|
+
static claimProcessCwdOwnership(manager: SessionManager): boolean {
|
|
10758
|
+
const current = SessionManager.#processCwdOwner?.deref();
|
|
10759
|
+
if (current === manager) return true;
|
|
10760
|
+
if (current !== undefined) return false;
|
|
10761
|
+
SessionManager.#processCwdOwner = new WeakRef(manager);
|
|
10762
|
+
return true;
|
|
10763
|
+
}
|
|
10764
|
+
|
|
10765
|
+
static isProcessCwdOwner(manager: SessionManager): boolean {
|
|
10766
|
+
return SessionManager.#processCwdOwner?.deref() === manager;
|
|
10767
|
+
}
|
|
10768
|
+
|
|
10769
|
+
static releaseProcessCwdOwnership(manager: SessionManager): void {
|
|
10770
|
+
if (SessionManager.#processCwdOwner?.deref() === manager) SessionManager.#processCwdOwner = undefined;
|
|
10771
|
+
}
|
|
10772
|
+
|
|
10773
|
+
/**
|
|
10774
|
+
* Verify that `process.cwd()` is the directory pinned by `expectedIdentity`.
|
|
10775
|
+
*
|
|
10776
|
+
* `process.chdir` resolves a NAME, so a path replaced after the last
|
|
10777
|
+
* name-based comparison lands the process outside the validated directory —
|
|
10778
|
+
* the exact confinement `move_session` exists to enforce. Node exposes no
|
|
10779
|
+
* `fchdir`, so the handle cannot be the chdir authority directly; comparing
|
|
10780
|
+
* the resulting cwd's identity to the pinned handle closes the same gap.
|
|
10781
|
+
*/
|
|
10782
|
+
static async assertProcessCwdIdentity(expectedIdentity: { dev: bigint; ino: bigint }): Promise<void> {
|
|
10783
|
+
const observed = await fs.promises.stat(process.cwd(), { bigint: true });
|
|
10784
|
+
if (observed.dev !== expectedIdentity.dev || observed.ino !== expectedIdentity.ino) {
|
|
10785
|
+
throw new Error(
|
|
10786
|
+
`Refusing to rescope: process cwd ${process.cwd()} is not the validated target directory (identity changed).`,
|
|
10787
|
+
);
|
|
10788
|
+
}
|
|
10789
|
+
}
|
|
10790
|
+
|
|
10452
10791
|
/**
|
|
10453
10792
|
* Move the session to a new working directory.
|
|
10454
10793
|
* Moves session files and artifacts on disk, updates all internal references,
|
|
10455
10794
|
* and rewrites the session header with the new cwd.
|
|
10795
|
+
*
|
|
10796
|
+
* All callers (model `move_session`, TUI `/move`, SDK/ACP `session.cwd.move`)
|
|
10797
|
+
* share this exclusive transition so concurrent moves cannot interleave.
|
|
10456
10798
|
*/
|
|
10457
|
-
async moveTo(
|
|
10799
|
+
async moveTo(
|
|
10800
|
+
newCwd: string,
|
|
10801
|
+
options?: {
|
|
10802
|
+
expectedIdentity?: { dev: bigint; ino: bigint };
|
|
10803
|
+
targetHandle?: { stat: (opts: { bigint: true }) => Promise<fs.BigIntStats> };
|
|
10804
|
+
},
|
|
10805
|
+
): Promise<void> {
|
|
10806
|
+
if (!this.#ownsCwdTransition()) {
|
|
10807
|
+
return this.runExclusiveCwdTransition(() => this.moveTo(newCwd, options));
|
|
10808
|
+
}
|
|
10458
10809
|
const resolvedCwd = path.resolve(newCwd);
|
|
10810
|
+
if (options?.expectedIdentity || options?.targetHandle) {
|
|
10811
|
+
await this.#assertCwdTargetIdentity(resolvedCwd, options);
|
|
10812
|
+
}
|
|
10459
10813
|
if (resolvedCwd === this.cwd) return;
|
|
10460
10814
|
const previousCwd = this.cwd;
|
|
10461
10815
|
const previousSessionDir = this.sessionDir;
|
|
@@ -10700,6 +11054,10 @@ export class SessionManager {
|
|
|
10700
11054
|
|
|
10701
11055
|
// Update cwd and sessionDir after physical publication succeeds. Metadata failures restore the source
|
|
10702
11056
|
// authority but deliberately retain any destination publication evidence rather than deleting it.
|
|
11057
|
+
if (options?.expectedIdentity || options?.targetHandle) {
|
|
11058
|
+
await this.#assertCwdTargetIdentity(resolvedCwd, options);
|
|
11059
|
+
}
|
|
11060
|
+
this.#cwdGeneration += 1;
|
|
10703
11061
|
this.cwd = resolvedCwd;
|
|
10704
11062
|
this.sessionDir = newSessionDir;
|
|
10705
11063
|
this.destination = nextDestination;
|
|
@@ -15216,6 +15574,8 @@ export class SessionManager {
|
|
|
15216
15574
|
|
|
15217
15575
|
/** Close the persistent writer after flushing all pending data. */
|
|
15218
15576
|
async close(): Promise<void> {
|
|
15577
|
+
await this.joinCwdTransition();
|
|
15578
|
+
SessionManager.releaseProcessCwdOwnership(this);
|
|
15219
15579
|
// Drain any uncommitted prepared successors before releasing resources so
|
|
15220
15580
|
// dispose/shutdown retains exact cleanup authority (#3138).
|
|
15221
15581
|
try {
|
|
@@ -15814,8 +16174,9 @@ export class SessionManager {
|
|
|
15814
16174
|
* Adopt an externally-owned ArtifactManager. Used by subagents to share
|
|
15815
16175
|
* the parent session's artifact directory and ID counter.
|
|
15816
16176
|
*/
|
|
15817
|
-
adoptArtifactManager(manager: ArtifactManager): void {
|
|
16177
|
+
adoptArtifactManager(manager: ArtifactManager, parent?: ArtifactManager): void {
|
|
15818
16178
|
this.#adoptedArtifactManager = manager;
|
|
16179
|
+
if (parent) this.#stagedArtifactParent = parent;
|
|
15819
16180
|
}
|
|
15820
16181
|
|
|
15821
16182
|
/** Release only the matching externally adopted manager. */
|
|
@@ -17019,6 +17380,15 @@ export class SessionManager {
|
|
|
17019
17380
|
}
|
|
17020
17381
|
}
|
|
17021
17382
|
|
|
17383
|
+
/** Remap artifact references in an unpublished candidate before its publication fence. */
|
|
17384
|
+
async remapStagedArtifactReferences(idMap: ReadonlyMap<string, string>): Promise<void> {
|
|
17385
|
+
const staged = this.#stagedPublication;
|
|
17386
|
+
if (!staged || staged.committed || staged.discarded) throw new Error("Staged session is unavailable");
|
|
17387
|
+
remapAttemptReferencesInEntries(this.#fileEntries, idMap);
|
|
17388
|
+
this.#needsFullRewriteOnNextPersist = true;
|
|
17389
|
+
await this.#rewriteFileContents();
|
|
17390
|
+
}
|
|
17391
|
+
|
|
17022
17392
|
/**
|
|
17023
17393
|
* Append a custom message entry (for extensions) that participates in LLM context.
|
|
17024
17394
|
* @param customType Hook identifier for filtering on reload
|
|
@@ -17878,7 +18248,7 @@ export class SessionManager {
|
|
|
17878
18248
|
|
|
17879
18249
|
async #sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist(): Promise<boolean> {
|
|
17880
18250
|
const patches = this.#sanitizeLoadedOpenAIResponsesReplayMetadata();
|
|
17881
|
-
await this.#persistPatches(patches);
|
|
18251
|
+
if (!this.isManagedDestination()) await this.#persistPatches(patches);
|
|
17882
18252
|
return patches.length > 0;
|
|
17883
18253
|
}
|
|
17884
18254
|
|
|
@@ -18951,6 +19321,383 @@ export class SessionManager {
|
|
|
18951
19321
|
* @param path Path to session file
|
|
18952
19322
|
* @param sessionDir Optional session directory for /new or /branch. If omitted, derives from file's parent.
|
|
18953
19323
|
*/
|
|
19324
|
+
/** Open an unpublished candidate transcript below the reserved staging directory. */
|
|
19325
|
+
static async openStaged(
|
|
19326
|
+
finalSessionFile: string,
|
|
19327
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
19328
|
+
attemptId: string = crypto.randomUUID(),
|
|
19329
|
+
): Promise<SessionManager> {
|
|
19330
|
+
assertSafeStagedAttemptId(attemptId);
|
|
19331
|
+
|
|
19332
|
+
if (isStagedSessionPath(finalSessionFile)) throw new Error("Final session path cannot be staged");
|
|
19333
|
+
const finalPath =
|
|
19334
|
+
storage instanceof FileSessionStorage ? canonicalizeTrustedPath(finalSessionFile) : finalSessionFile;
|
|
19335
|
+
const finalDestination = explicitDestination(path.dirname(finalPath));
|
|
19336
|
+
const stagingDir = path.join(path.dirname(finalPath), SESSION_STAGING_DIRNAME);
|
|
19337
|
+
await fs.promises.mkdir(stagingDir, { recursive: true, mode: 0o700 });
|
|
19338
|
+
const stagedSessionFile = path.join(stagingDir, `${attemptId}.jsonl`);
|
|
19339
|
+
if (path.resolve(stagedSessionFile) === path.resolve(finalPath))
|
|
19340
|
+
throw new Error("Staged session path collides with final transcript");
|
|
19341
|
+
|
|
19342
|
+
const manager = new SessionManager(getProjectDir(), stagingDir, true, storage, finalDestination);
|
|
19343
|
+
manager.#stagedPublication = {
|
|
19344
|
+
finalSessionFile: finalPath,
|
|
19345
|
+
stagedSessionFile,
|
|
19346
|
+
finalDestination,
|
|
19347
|
+
attemptId,
|
|
19348
|
+
committed: false,
|
|
19349
|
+
discarded: false,
|
|
19350
|
+
};
|
|
19351
|
+
if (fs.existsSync(stagedSessionFile)) throw new Error("Staged session attempt already exists");
|
|
19352
|
+
try {
|
|
19353
|
+
await manager.#initSessionFile(stagedSessionFile, true);
|
|
19354
|
+
const parentArtifacts = new ArtifactManager(finalPath.endsWith(".jsonl") ? finalPath.slice(0, -6) : finalPath);
|
|
19355
|
+
manager.adoptArtifactManager(parentArtifacts.createAttemptStaging(attemptId), parentArtifacts);
|
|
19356
|
+
return manager;
|
|
19357
|
+
} catch (error) {
|
|
19358
|
+
try {
|
|
19359
|
+
await manager.discardStaged();
|
|
19360
|
+
} catch (cleanupError) {
|
|
19361
|
+
throw new AggregateError(
|
|
19362
|
+
[toError(error), toError(cleanupError)],
|
|
19363
|
+
"Staged session open and cleanup both failed.",
|
|
19364
|
+
);
|
|
19365
|
+
}
|
|
19366
|
+
throw error;
|
|
19367
|
+
}
|
|
19368
|
+
}
|
|
19369
|
+
|
|
19370
|
+
/** Managed-authority variant of {@link openStaged}. */
|
|
19371
|
+
static async stagedNestedManaged(
|
|
19372
|
+
finalSessionFile: string,
|
|
19373
|
+
destination: SessionDestination,
|
|
19374
|
+
store: ManagedSessionDescendantStore,
|
|
19375
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
19376
|
+
attemptId: string = crypto.randomUUID(),
|
|
19377
|
+
): Promise<SessionManager> {
|
|
19378
|
+
assertSafeStagedAttemptId(attemptId);
|
|
19379
|
+
|
|
19380
|
+
if (destination.kind !== "managed" || !trustedSessionDestinations.has(destination))
|
|
19381
|
+
throw new Error("Nested managed session authority is unavailable");
|
|
19382
|
+
if (isStagedSessionPath(finalSessionFile)) throw new Error("Final session path cannot be staged");
|
|
19383
|
+
store.assertBound();
|
|
19384
|
+
const finalPath = path.resolve(finalSessionFile);
|
|
19385
|
+
if (path.dirname(finalPath) !== path.resolve(destination.directory))
|
|
19386
|
+
throw new Error("Nested managed session escaped retained authority");
|
|
19387
|
+
const stagingStore = store.deriveSubtree(SESSION_STAGING_DIRNAME);
|
|
19388
|
+
const stagedDir = stagingStore.dir;
|
|
19389
|
+
const stagedSessionFile = path.join(stagedDir, `${attemptId}.jsonl`);
|
|
19390
|
+
if (path.resolve(stagedSessionFile) === path.resolve(finalPath))
|
|
19391
|
+
throw new Error("Staged session path collides with final transcript");
|
|
19392
|
+
const stagedDestination = SessionManager.nestedManagedDestination(stagingStore, stagedDir);
|
|
19393
|
+
const manager = new SessionManager(getProjectDir(), stagedDir, true, storage, stagedDestination);
|
|
19394
|
+
manager.#stagedPublication = {
|
|
19395
|
+
finalSessionFile: finalPath,
|
|
19396
|
+
stagedSessionFile,
|
|
19397
|
+
finalDestination: destination,
|
|
19398
|
+
managedParentStore: store,
|
|
19399
|
+
managedStagingStore: stagingStore,
|
|
19400
|
+
attemptId,
|
|
19401
|
+
committed: false,
|
|
19402
|
+
discarded: false,
|
|
19403
|
+
};
|
|
19404
|
+
if (fs.existsSync(stagedSessionFile)) throw new Error("Staged session attempt already exists");
|
|
19405
|
+
try {
|
|
19406
|
+
await manager.#initSessionFile(stagedSessionFile, true);
|
|
19407
|
+
const parentArtifacts = new ArtifactManager(store);
|
|
19408
|
+
manager.adoptArtifactManager(parentArtifacts.createAttemptStaging(attemptId), parentArtifacts);
|
|
19409
|
+
store.assertBound();
|
|
19410
|
+
return manager;
|
|
19411
|
+
} catch (error) {
|
|
19412
|
+
try {
|
|
19413
|
+
await manager.discardStaged();
|
|
19414
|
+
} catch (cleanupError) {
|
|
19415
|
+
throw new AggregateError(
|
|
19416
|
+
[toError(error), toError(cleanupError)],
|
|
19417
|
+
"Staged session open and cleanup both failed.",
|
|
19418
|
+
);
|
|
19419
|
+
}
|
|
19420
|
+
throw error;
|
|
19421
|
+
}
|
|
19422
|
+
}
|
|
19423
|
+
/** Publish the candidate-owned staged transcript and artifacts at the real accept fence. */
|
|
19424
|
+
static async openStagedNestedManaged(
|
|
19425
|
+
finalSessionFile: string,
|
|
19426
|
+
destination: SessionDestination,
|
|
19427
|
+
store: ManagedSessionDescendantStore,
|
|
19428
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
19429
|
+
attemptId: string = crypto.randomUUID(),
|
|
19430
|
+
): Promise<SessionManager> {
|
|
19431
|
+
return SessionManager.stagedNestedManaged(finalSessionFile, destination, store, storage, attemptId);
|
|
19432
|
+
}
|
|
19433
|
+
|
|
19434
|
+
async commitStaged(options?: { deferArtifactFinalize?: boolean }): Promise<void> {
|
|
19435
|
+
const staged = this.#stagedPublication;
|
|
19436
|
+
if (!staged || staged.discarded) throw new Error("Staged session is unavailable");
|
|
19437
|
+
if (staged.committed) return;
|
|
19438
|
+
staged.deferArtifactFinalize = options?.deferArtifactFinalize === true;
|
|
19439
|
+
await this.flush();
|
|
19440
|
+
await this.#closePersistWriter();
|
|
19441
|
+
const stagedManager = this.#adoptedArtifactManager ?? this.#artifactManager;
|
|
19442
|
+
// Lifecycle invariant: while a staged publication is uncommitted, the session must
|
|
19443
|
+
// carry exactly the attempt-rooted staging manager it opened with. openStaged,
|
|
19444
|
+
// stagedNestedManaged, and openStagedSession all pre-adopt an attempt-rooted
|
|
19445
|
+
// manager, so an adopted manager that is absent or foreign means a second root
|
|
19446
|
+
// replaced the first — orphaning the original from commit/discard cleanup.
|
|
19447
|
+
if (this.#adoptedArtifactManager?.getAttemptId() !== staged.attemptId)
|
|
19448
|
+
throw new Error("Staged session artifact root does not match the staged attempt.");
|
|
19449
|
+
const parentArtifacts =
|
|
19450
|
+
this.#stagedArtifactParent ??
|
|
19451
|
+
new ArtifactManager(
|
|
19452
|
+
staged.finalSessionFile.endsWith(".jsonl") ? staged.finalSessionFile.slice(0, -6) : staged.finalSessionFile,
|
|
19453
|
+
);
|
|
19454
|
+
this.#stagedCommitArtifactParent = parentArtifacts;
|
|
19455
|
+
let published = false;
|
|
19456
|
+
try {
|
|
19457
|
+
if (stagedManager?.getAttemptId() === staged.attemptId) {
|
|
19458
|
+
const stagedArtifactFiles = await stagedManager.listFiles();
|
|
19459
|
+
if (stagedArtifactFiles.length > 0 || stagedManager.getAllocatedIds().length > 0) {
|
|
19460
|
+
await parentArtifacts.commitAttemptStaging(stagedManager, staged.attemptId, {
|
|
19461
|
+
beforePublish: idMap => this.remapStagedArtifactReferences(idMap),
|
|
19462
|
+
});
|
|
19463
|
+
} else await stagedManager.discardAttemptStaging();
|
|
19464
|
+
}
|
|
19465
|
+
if (staged.managedParentStore && staged.managedStagingStore) {
|
|
19466
|
+
const stagedName = path.basename(staged.stagedSessionFile);
|
|
19467
|
+
const relative = path.posix.join(SESSION_STAGING_DIRNAME, stagedName);
|
|
19468
|
+
const expected = staged.managedStagingStore.readExpected(stagedName);
|
|
19469
|
+
if (!expected) throw new Error("staged_session_missing");
|
|
19470
|
+
staged.managedParentStore.moveFileNoReplace(relative, path.basename(staged.finalSessionFile), expected, {
|
|
19471
|
+
sourceStore: staged.managedStagingStore,
|
|
19472
|
+
sourceStoreRelativePath: stagedName,
|
|
19473
|
+
});
|
|
19474
|
+
} else {
|
|
19475
|
+
const outcome = classifyNativePublishOutcome(
|
|
19476
|
+
nativeSessionManager().renameNoReplacePath(staged.stagedSessionFile, staged.finalSessionFile),
|
|
19477
|
+
);
|
|
19478
|
+
if (!outcome.ok) throw new Error(outcome.code ?? "staged_session_publish_failed");
|
|
19479
|
+
}
|
|
19480
|
+
published = true;
|
|
19481
|
+
if (staged.managedParentStore) {
|
|
19482
|
+
staged.publishedFinalSnapshot =
|
|
19483
|
+
staged.managedParentStore.readExpected(path.basename(staged.finalSessionFile)) ?? undefined;
|
|
19484
|
+
if (!staged.publishedFinalSnapshot) throw new Error("staged_session_publish_missing");
|
|
19485
|
+
} else staged.publishedFinalBytes = await fs.promises.readFile(staged.finalSessionFile);
|
|
19486
|
+
|
|
19487
|
+
const finalStat = fs.lstatSync(staged.finalSessionFile, { bigint: true });
|
|
19488
|
+
if (!finalStat.isFile() || finalStat.isSymbolicLink()) throw new Error("staged_session_identity_changed");
|
|
19489
|
+
this.sessionDir = path.dirname(staged.finalSessionFile);
|
|
19490
|
+
this.destination = staged.finalDestination;
|
|
19491
|
+
this.#managedTranscriptStoreCache = staged.managedParentStore
|
|
19492
|
+
? { directory: path.dirname(staged.finalSessionFile), store: staged.managedParentStore }
|
|
19493
|
+
: null;
|
|
19494
|
+
this.#sessionFile = staged.finalSessionFile;
|
|
19495
|
+
this.#artifactManager = parentArtifacts;
|
|
19496
|
+
this.#artifactManagerSessionFile = staged.finalSessionFile;
|
|
19497
|
+
this.#adoptedArtifactManager = parentArtifacts;
|
|
19498
|
+
staged.committed = true;
|
|
19499
|
+
if (!staged.deferArtifactFinalize) {
|
|
19500
|
+
writeTerminalBreadcrumb(this.cwd, staged.finalSessionFile);
|
|
19501
|
+
parentArtifacts.finalizeLastAttemptCommit(staged.attemptId);
|
|
19502
|
+
}
|
|
19503
|
+
} catch (error) {
|
|
19504
|
+
const cleanupErrors: Error[] = [];
|
|
19505
|
+
// A managed move can commit and still report failure when native code cannot
|
|
19506
|
+
// prove durability or terminal identity. Treating that as unpublished would
|
|
19507
|
+
// roll the artifacts back underneath a transcript that is already visible at
|
|
19508
|
+
// the destination, so probe the destination first and preserve every owned
|
|
19509
|
+
// artifact when the mutation may have landed.
|
|
19510
|
+
if (!published && staged.managedParentStore && !mayCleanManagedTreeStaging(error)) {
|
|
19511
|
+
const probeErrors: Error[] = [];
|
|
19512
|
+
let destination: ManagedFileSnapshot | null = null;
|
|
19513
|
+
try {
|
|
19514
|
+
destination = staged.managedParentStore.readExpected(path.basename(staged.finalSessionFile));
|
|
19515
|
+
} catch (probeError) {
|
|
19516
|
+
probeErrors.push(toError(probeError));
|
|
19517
|
+
}
|
|
19518
|
+
// Absence must be proven; an unreadable destination stays fail-closed.
|
|
19519
|
+
if (destination || probeErrors.length > 0) {
|
|
19520
|
+
staged.publishedFinalSnapshot = destination ?? undefined;
|
|
19521
|
+
// Latch the uncertainty so no later compensation reclaims what a possibly
|
|
19522
|
+
// published transcript references.
|
|
19523
|
+
staged.preservedUnproven = true;
|
|
19524
|
+
throw new AggregateError(
|
|
19525
|
+
[toError(error), ...probeErrors],
|
|
19526
|
+
"Staged publication may have committed without proof; artifacts and staging were preserved for recovery.",
|
|
19527
|
+
);
|
|
19528
|
+
}
|
|
19529
|
+
}
|
|
19530
|
+
if (published) {
|
|
19531
|
+
try {
|
|
19532
|
+
if (staged.managedParentStore) {
|
|
19533
|
+
const finalSnapshot = staged.managedParentStore.readExpected(path.basename(staged.finalSessionFile));
|
|
19534
|
+
if (finalSnapshot)
|
|
19535
|
+
staged.managedParentStore.removeExpected(path.basename(staged.finalSessionFile), finalSnapshot);
|
|
19536
|
+
} else await fs.promises.rm(staged.finalSessionFile, { force: true });
|
|
19537
|
+
} catch (cleanupError) {
|
|
19538
|
+
cleanupErrors.push(toError(cleanupError));
|
|
19539
|
+
}
|
|
19540
|
+
}
|
|
19541
|
+
try {
|
|
19542
|
+
await parentArtifacts.rollbackLastAttemptCommit(staged.attemptId);
|
|
19543
|
+
} catch (cleanupError) {
|
|
19544
|
+
cleanupErrors.push(toError(cleanupError));
|
|
19545
|
+
}
|
|
19546
|
+
try {
|
|
19547
|
+
await this.discardStaged();
|
|
19548
|
+
} catch (cleanupError) {
|
|
19549
|
+
cleanupErrors.push(toError(cleanupError));
|
|
19550
|
+
}
|
|
19551
|
+
if (cleanupErrors.length > 0)
|
|
19552
|
+
throw new AggregateError([toError(error), ...cleanupErrors], "Staged publication and cleanup both failed.");
|
|
19553
|
+
throw error;
|
|
19554
|
+
}
|
|
19555
|
+
}
|
|
19556
|
+
|
|
19557
|
+
/** Finalize a staged publication whose post-fence publisher completed successfully. */
|
|
19558
|
+
finalizeStagedCommit(): void {
|
|
19559
|
+
const staged = this.#stagedPublication;
|
|
19560
|
+
if (!staged?.committed || !staged.deferArtifactFinalize) return;
|
|
19561
|
+
this.#stagedCommitArtifactParent?.finalizeLastAttemptCommit(staged.attemptId);
|
|
19562
|
+
writeTerminalBreadcrumb(this.cwd, staged.finalSessionFile);
|
|
19563
|
+
staged.deferArtifactFinalize = false;
|
|
19564
|
+
}
|
|
19565
|
+
|
|
19566
|
+
/** Roll back a staged publication when post-fence visibility setup fails. */
|
|
19567
|
+
async rollbackCommittedStaged(): Promise<void> {
|
|
19568
|
+
const staged = this.#stagedPublication;
|
|
19569
|
+
if (!staged?.committed) return;
|
|
19570
|
+
if (staged.managedParentStore) {
|
|
19571
|
+
const current = staged.managedParentStore.readExpected(path.basename(staged.finalSessionFile));
|
|
19572
|
+
if (
|
|
19573
|
+
current &&
|
|
19574
|
+
staged.publishedFinalSnapshot &&
|
|
19575
|
+
current.identity.dev === staged.publishedFinalSnapshot.identity.dev &&
|
|
19576
|
+
current.identity.ino === staged.publishedFinalSnapshot.identity.ino
|
|
19577
|
+
) {
|
|
19578
|
+
if (this.#stagedCommitArtifactParent) {
|
|
19579
|
+
const removed = await this.#stagedCommitArtifactParent.removeNamedBestEffort(
|
|
19580
|
+
path.basename(staged.finalSessionFile),
|
|
19581
|
+
);
|
|
19582
|
+
if (!removed) throw new Error("staged_final_cleanup_failed");
|
|
19583
|
+
} else staged.managedParentStore.removeExpected(path.basename(staged.finalSessionFile), current);
|
|
19584
|
+
}
|
|
19585
|
+
} else if (staged.publishedFinalBytes) {
|
|
19586
|
+
const current = await fs.promises.readFile(staged.finalSessionFile).catch(() => undefined);
|
|
19587
|
+
if (current?.equals(staged.publishedFinalBytes))
|
|
19588
|
+
await fs.promises.rm(staged.finalSessionFile, { force: true });
|
|
19589
|
+
}
|
|
19590
|
+
await this.#stagedCommitArtifactParent?.rollbackLastAttemptCommit(staged.attemptId);
|
|
19591
|
+
staged.committed = false;
|
|
19592
|
+
staged.discarded = true;
|
|
19593
|
+
staged.deferArtifactFinalize = false;
|
|
19594
|
+
}
|
|
19595
|
+
|
|
19596
|
+
/** Refresh the owned final snapshot after post-fence session metadata is appended. */
|
|
19597
|
+
async refreshStagedCommitSnapshot(): Promise<void> {
|
|
19598
|
+
const staged = this.#stagedPublication;
|
|
19599
|
+
if (!staged?.committed) return;
|
|
19600
|
+
if (staged.managedParentStore) {
|
|
19601
|
+
staged.publishedFinalSnapshot =
|
|
19602
|
+
staged.managedParentStore.readExpected(path.basename(staged.finalSessionFile)) ?? undefined;
|
|
19603
|
+
if (!staged.publishedFinalSnapshot) throw new Error("staged_session_publish_missing");
|
|
19604
|
+
} else staged.publishedFinalBytes = await fs.promises.readFile(staged.finalSessionFile);
|
|
19605
|
+
}
|
|
19606
|
+
|
|
19607
|
+
/** Idempotently remove an unpublished staged transcript and its owned artifacts. */
|
|
19608
|
+
async discardStaged(): Promise<void> {
|
|
19609
|
+
const staged = this.#stagedPublication;
|
|
19610
|
+
if (!staged || staged.committed || staged.discarded) return;
|
|
19611
|
+
// An unproven publish may already be visible at the destination; reclaiming its
|
|
19612
|
+
// staging or artifacts would strand that transcript with dangling references.
|
|
19613
|
+
if (staged.preservedUnproven) return;
|
|
19614
|
+
const cleanupErrors: Error[] = [];
|
|
19615
|
+
// Same strict single-root invariant as commitStaged: an absent or foreign adopted
|
|
19616
|
+
// manager means the publication's own attempt root was replaced or released.
|
|
19617
|
+
if (this.#adoptedArtifactManager?.getAttemptId() !== staged.attemptId) {
|
|
19618
|
+
cleanupErrors.push(new Error("Staged session artifact root does not match the staged attempt."));
|
|
19619
|
+
}
|
|
19620
|
+
const captureCleanupError = (error: unknown): void => {
|
|
19621
|
+
const normalized = toError(error);
|
|
19622
|
+
if (normalized.message !== "not_found" && !isAuthorizedPendingCleanup(normalized))
|
|
19623
|
+
cleanupErrors.push(normalized);
|
|
19624
|
+
};
|
|
19625
|
+
try {
|
|
19626
|
+
await this.#closePersistWriter();
|
|
19627
|
+
} catch (error) {
|
|
19628
|
+
captureCleanupError(error);
|
|
19629
|
+
}
|
|
19630
|
+
let managedStagingBefore: ReturnType<ManagedSessionDescendantStore["captureTree"]> | undefined;
|
|
19631
|
+
if (staged.managedParentStore) {
|
|
19632
|
+
try {
|
|
19633
|
+
managedStagingBefore = staged.managedParentStore.captureTree(SESSION_STAGING_DIRNAME);
|
|
19634
|
+
} catch (error) {
|
|
19635
|
+
captureCleanupError(error);
|
|
19636
|
+
}
|
|
19637
|
+
}
|
|
19638
|
+
|
|
19639
|
+
const stagedManager = this.#adoptedArtifactManager ?? this.#artifactManager;
|
|
19640
|
+
if (stagedManager?.getAttemptId() === staged.attemptId) {
|
|
19641
|
+
try {
|
|
19642
|
+
await stagedManager.discardAttemptStaging();
|
|
19643
|
+
} catch (error) {
|
|
19644
|
+
captureCleanupError(error);
|
|
19645
|
+
}
|
|
19646
|
+
}
|
|
19647
|
+
if (staged.managedParentStore) {
|
|
19648
|
+
const stagedName = path.basename(staged.stagedSessionFile);
|
|
19649
|
+
try {
|
|
19650
|
+
const stagingStore = staged.managedStagingStore;
|
|
19651
|
+
if (stagingStore) {
|
|
19652
|
+
const expected = stagingStore.readExpected(stagedName);
|
|
19653
|
+
if (expected) stagingStore.removeExpected(stagedName, expected);
|
|
19654
|
+
}
|
|
19655
|
+
} catch (error) {
|
|
19656
|
+
captureCleanupError(error);
|
|
19657
|
+
}
|
|
19658
|
+
if (managedStagingBefore) {
|
|
19659
|
+
try {
|
|
19660
|
+
const after = staged.managedParentStore.captureTree(SESSION_STAGING_DIRNAME);
|
|
19661
|
+
const beforePaths = new Set(managedStagingBefore.entries.map(entry => entry.relativePath));
|
|
19662
|
+
for (const entry of after.entries) {
|
|
19663
|
+
if (
|
|
19664
|
+
entry.relativePath.length === 0 ||
|
|
19665
|
+
beforePaths.has(entry.relativePath) ||
|
|
19666
|
+
(!/^\.gjc-/u.test(path.posix.basename(entry.relativePath)) &&
|
|
19667
|
+
!/\.removing$/u.test(path.posix.basename(entry.relativePath)))
|
|
19668
|
+
)
|
|
19669
|
+
continue;
|
|
19670
|
+
try {
|
|
19671
|
+
await fs.promises.rm(
|
|
19672
|
+
path.join(staged.managedParentStore.dir, SESSION_STAGING_DIRNAME, entry.relativePath),
|
|
19673
|
+
{ recursive: entry.kind === "directory", force: true },
|
|
19674
|
+
);
|
|
19675
|
+
} catch (error) {
|
|
19676
|
+
captureCleanupError(error);
|
|
19677
|
+
}
|
|
19678
|
+
}
|
|
19679
|
+
} catch (error) {
|
|
19680
|
+
captureCleanupError(error);
|
|
19681
|
+
}
|
|
19682
|
+
}
|
|
19683
|
+
} else {
|
|
19684
|
+
try {
|
|
19685
|
+
await fs.promises.rm(staged.stagedSessionFile, { force: true });
|
|
19686
|
+
} catch (error) {
|
|
19687
|
+
captureCleanupError(error);
|
|
19688
|
+
}
|
|
19689
|
+
}
|
|
19690
|
+
if (cleanupErrors.length > 0) throw new AggregateError(cleanupErrors, "Staged session cleanup failed.");
|
|
19691
|
+
staged.discarded = true;
|
|
19692
|
+
}
|
|
19693
|
+
async commitStagedNestedManaged(): Promise<void> {
|
|
19694
|
+
return this.commitStaged();
|
|
19695
|
+
}
|
|
19696
|
+
|
|
19697
|
+
async discardStagedNestedManaged(): Promise<void> {
|
|
19698
|
+
return this.discardStaged();
|
|
19699
|
+
}
|
|
19700
|
+
|
|
18954
19701
|
static async open(
|
|
18955
19702
|
filePath: string,
|
|
18956
19703
|
destinationInput?: SessionDestinationInput,
|
|
@@ -18958,6 +19705,7 @@ export class SessionManager {
|
|
|
18958
19705
|
migrationPolicy: SessionDirectoryMigrationPolicy = "copy-retain",
|
|
18959
19706
|
sessionMemoryMode: SessionMemoryMode = "shadow",
|
|
18960
19707
|
): Promise<SessionManager> {
|
|
19708
|
+
if (isStagedSessionPath(filePath)) throw new Error("Staged session paths are not resumable");
|
|
18961
19709
|
const destination =
|
|
18962
19710
|
destinationInput === undefined
|
|
18963
19711
|
? explicitDestination(path.dirname(filePath))
|
|
@@ -19446,7 +20194,7 @@ export class SessionManager {
|
|
|
19446
20194
|
const listing = listManagedCandidates(resolved.scope);
|
|
19447
20195
|
if (listing.kind === "error") return [];
|
|
19448
20196
|
const managed = await collectSessionsFromFiles(
|
|
19449
|
-
listing.owned.map(candidate => candidate.path),
|
|
20197
|
+
listing.owned.filter(candidate => !isStagedSessionPath(candidate.path)).map(candidate => candidate.path),
|
|
19450
20198
|
storage,
|
|
19451
20199
|
);
|
|
19452
20200
|
return mergeSessionInventories(managed, await collectProjectSessions(cwd, storage));
|
|
@@ -19464,7 +20212,10 @@ export class SessionManager {
|
|
|
19464
20212
|
): Promise<SessionInfo[]> {
|
|
19465
20213
|
if (!sessionDir) return await SessionManager.listManagedForResumePickerReadOnly(cwd, undefined, storage);
|
|
19466
20214
|
try {
|
|
19467
|
-
return await collectSessionsFromFiles(
|
|
20215
|
+
return await collectSessionsFromFiles(
|
|
20216
|
+
storage.listFilesSync(sessionDir, "*.jsonl").filter(file => !isStagedSessionPath(file)),
|
|
20217
|
+
storage,
|
|
20218
|
+
);
|
|
19468
20219
|
} catch {
|
|
19469
20220
|
return [];
|
|
19470
20221
|
}
|
|
@@ -20184,7 +20935,13 @@ export class SessionManager {
|
|
|
20184
20935
|
await manager.close();
|
|
20185
20936
|
return ownershipInspection;
|
|
20186
20937
|
}
|
|
20187
|
-
|
|
20938
|
+
try {
|
|
20939
|
+
await manager.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
|
|
20940
|
+
writeTerminalBreadcrumb(manager.cwd, sessionPath);
|
|
20941
|
+
} catch (error) {
|
|
20942
|
+
await manager.close();
|
|
20943
|
+
throw error;
|
|
20944
|
+
}
|
|
20188
20945
|
return { kind: "opened", manager };
|
|
20189
20946
|
}
|
|
20190
20947
|
|
|
@@ -20368,7 +21125,8 @@ export class SessionManager {
|
|
|
20368
21125
|
logger.warn("Ignored invalid managed session candidates during global listing", {
|
|
20369
21126
|
count: listing.invalid.length,
|
|
20370
21127
|
});
|
|
20371
|
-
for (const candidate of listing.owned)
|
|
21128
|
+
for (const candidate of listing.owned)
|
|
21129
|
+
if (!isStagedSessionPath(candidate.path)) logicalFiles.add(candidate.path);
|
|
20372
21130
|
}
|
|
20373
21131
|
return await collectSessionsFromFiles([...logicalFiles], storage);
|
|
20374
21132
|
} catch {
|
|
@@ -20427,6 +21185,7 @@ export class SessionManager {
|
|
|
20427
21185
|
const failures: StrictInventoryFailure[] = [];
|
|
20428
21186
|
const candidates: StrictInventoryCandidate[] = [];
|
|
20429
21187
|
for (const managedCandidate of listing.owned) {
|
|
21188
|
+
if (isStagedSessionPath(managedCandidate.path)) continue;
|
|
20430
21189
|
const candidate = inventoryReadCandidate(
|
|
20431
21190
|
storage,
|
|
20432
21191
|
managedCandidate.path,
|