@gajae-code/coding-agent 0.11.3 → 0.11.5
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 +47 -0
- package/dist/types/cli/notify-cli.d.ts +5 -1
- package/dist/types/cli/worktree-cli.d.ts +8 -25
- package/dist/types/commands/gc.d.ts +4 -0
- package/dist/types/commands/worktree.d.ts +7 -32
- package/dist/types/config/settings-schema.d.ts +20 -0
- package/dist/types/export/html/template.generated.d.ts +1 -1
- package/dist/types/gjc-runtime/deep-interview-state.d.ts +28 -0
- package/dist/types/gjc-runtime/gc-runtime.d.ts +9 -0
- package/dist/types/gjc-runtime/launch-tmux.d.ts +3 -0
- package/dist/types/gjc-runtime/managed-owner-admission.d.ts +42 -0
- package/dist/types/gjc-runtime/managed-owner-supervisor.d.ts +41 -0
- package/dist/types/gjc-runtime/state-argv.d.ts +38 -0
- package/dist/types/gjc-runtime/tmux-owner-isolation.d.ts +9 -0
- package/dist/types/gjc-runtime/ultragoal-owner-loss-recovery.d.ts +94 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +1 -0
- package/dist/types/harness-control-plane/owner.d.ts +3 -1
- package/dist/types/internal-urls/local-protocol.d.ts +18 -0
- package/dist/types/main.d.ts +2 -2
- package/dist/types/modes/components/ansi-display-validator.d.ts +2 -0
- package/dist/types/modes/components/custom-editor.d.ts +8 -3
- package/dist/types/modes/components/notifications-settings-editor.d.ts +4 -0
- package/dist/types/modes/components/session-observer-overlay.d.ts +3 -1
- package/dist/types/modes/components/settings-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-transcript-format.d.ts +77 -0
- package/dist/types/modes/components/transcript-viewer-overlay.d.ts +3 -0
- package/dist/types/modes/controllers/goal-mode-controller.d.ts +8 -1
- package/dist/types/modes/controllers/input-controller.d.ts +8 -2
- package/dist/types/modes/controllers/selector-controller.d.ts +15 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-broker.d.ts +3 -0
- package/dist/types/modes/shared/agent-wire/workflow-gate-types.d.ts +16 -0
- package/dist/types/plan-mode/approved-plan.d.ts +1 -0
- package/dist/types/runtime-mcp/manager.d.ts +2 -0
- package/dist/types/runtime-mcp/oauth-discovery.d.ts +12 -1
- package/dist/types/sdk/broker/broker.d.ts +1 -0
- package/dist/types/sdk/broker/discovery.d.ts +10 -0
- package/dist/types/sdk/broker/ensure.d.ts +24 -1
- package/dist/types/sdk/broker/internal.d.ts +3 -0
- package/dist/types/sdk/broker/session-index.d.ts +12 -0
- package/dist/types/sdk/broker/state-version.d.ts +2 -1
- package/dist/types/sdk/bus/chat-daemon-cli.d.ts +3 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +39 -4
- package/dist/types/sdk/bus/config-commands.d.ts +8 -0
- package/dist/types/sdk/bus/config.d.ts +22 -0
- package/dist/types/sdk/bus/index.d.ts +162 -2
- package/dist/types/sdk/bus/notification-service.d.ts +71 -8
- package/dist/types/sdk/bus/session-control.d.ts +11 -8
- package/dist/types/sdk/bus/telegram-daemon-cli.d.ts +5 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +6 -3
- package/dist/types/sdk/bus/telegram-daemon-control.d.ts +23 -4
- package/dist/types/sdk/bus/telegram-daemon.d.ts +214 -20
- package/dist/types/sdk/session.d.ts +16 -1
- package/dist/types/secrets/index.d.ts +4 -2
- package/dist/types/secrets/obfuscator.d.ts +1 -1
- package/dist/types/session/agent-session.d.ts +31 -2
- package/dist/types/session/artifacts.d.ts +30 -8
- package/dist/types/session/internal/managed-session-scope.d.ts +7 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +63 -6
- package/dist/types/session/session-manager.d.ts +88 -18
- package/dist/types/session/session-storage.d.ts +27 -0
- package/dist/types/session/streaming-output.d.ts +4 -0
- package/dist/types/task/executor.d.ts +9 -0
- package/dist/types/tool-discovery/tool-index.d.ts +2 -0
- package/dist/types/tools/ask.d.ts +87 -12
- package/dist/types/tools/bash-skill-urls.d.ts +2 -2
- package/dist/types/tools/browser/screenshot-format.d.ts +8 -0
- package/dist/types/tools/browser/tab-protocol.d.ts +11 -0
- package/dist/types/tools/browser/tab-supervisor.d.ts +1 -1
- package/dist/types/tools/browser/tab-worker-path-resolver.d.ts +2 -0
- package/dist/types/tools/browser/tab-worker-smoke.d.ts +16 -0
- package/dist/types/tools/index.d.ts +4 -0
- package/dist/types/tools/path-utils.d.ts +9 -14
- package/dist/types/tools/render-utils.d.ts +1 -8
- package/dist/types/tools/resource-gc.d.ts +14 -0
- package/dist/types/utils/image-loading.d.ts +23 -0
- package/dist/types/utils/image-resize.d.ts +2 -0
- package/dist/types/utils/pasted-image-loading.d.ts +39 -0
- package/dist/types/utils/pasted-image-path.d.ts +21 -19
- package/dist/types/web/scrapers/html-to-markdown.d.ts +5 -0
- package/dist/types/web/scrapers/types.d.ts +1 -5
- package/package.json +25 -9
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-threat-model-check.ts +60 -0
- package/src/cli/notify-cli.ts +40 -18
- package/src/cli/worktree-cli.ts +9 -287
- package/src/cli.ts +10 -9
- package/src/commands/gc.ts +11 -1
- package/src/commands/sdk.ts +10 -4
- package/src/commands/worktree.ts +10 -53
- package/src/config/model-profiles.ts +15 -15
- package/src/config/settings-schema.ts +17 -0
- package/src/daemon/runtime.ts +2 -2
- package/src/defaults/gjc/skills/deep-interview/SKILL.md +70 -49
- package/src/defaults/gjc/skills/ralplan/SKILL.md +0 -6
- package/src/defaults/gjc/skills/team/SKILL.md +7 -9
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +0 -2
- package/src/eval/js/shared/runtime.ts +1 -1
- package/src/eval/py/index.ts +2 -2
- package/src/export/html/template.generated.ts +1 -1
- package/src/extensibility/plugins/marketplace/manager.ts +94 -10
- package/src/gjc-runtime/deep-interview-recorder.ts +7 -0
- package/src/gjc-runtime/deep-interview-runtime.ts +10 -0
- package/src/gjc-runtime/deep-interview-state.ts +127 -3
- package/src/gjc-runtime/gc-render.ts +19 -1
- package/src/gjc-runtime/gc-runtime.ts +68 -3
- package/src/gjc-runtime/launch-tmux.ts +61 -6
- package/src/gjc-runtime/managed-owner-admission.ts +352 -0
- package/src/gjc-runtime/managed-owner-supervisor.ts +162 -0
- package/src/gjc-runtime/state-argv.ts +292 -0
- package/src/gjc-runtime/state-runtime.ts +80 -222
- package/src/gjc-runtime/team-runtime.ts +1 -1
- package/src/gjc-runtime/tmux-owner-isolation.ts +117 -0
- package/src/gjc-runtime/ultragoal-owner-loss-recovery.ts +328 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +14 -0
- package/src/harness-control-plane/owner.ts +59 -3
- package/src/hindsight/client.ts +65 -2
- package/src/internal-urls/agent-protocol.ts +76 -5
- package/src/internal-urls/docs-index.generated.ts +7 -6
- package/src/internal-urls/local-protocol.ts +416 -18
- package/src/lsp/index.ts +4 -88
- package/src/main.ts +37 -13
- package/src/modes/acp/acp-agent.ts +8 -0
- package/src/modes/components/ansi-display-validator.ts +54 -0
- package/src/modes/components/custom-editor.ts +103 -32
- package/src/modes/components/custom-provider-wizard.ts +26 -1
- package/src/modes/components/notifications-settings-editor.ts +26 -2
- package/src/modes/components/session-observer-overlay.ts +40 -37
- package/src/modes/components/settings-selector.ts +3 -1
- package/src/modes/components/tool-transcript-format.ts +393 -0
- package/src/modes/components/transcript-viewer-overlay.ts +63 -1
- package/src/modes/controllers/goal-mode-controller.ts +20 -1
- package/src/modes/controllers/input-controller.ts +98 -37
- package/src/modes/controllers/plan-mode-controller.ts +2 -0
- package/src/modes/controllers/runtime-mcp-command-controller.ts +3 -0
- package/src/modes/controllers/selector-controller.ts +71 -9
- package/src/modes/interactive-mode.ts +14 -2
- package/src/modes/shared/agent-wire/deep-interview-gate.ts +31 -1
- package/src/modes/shared/agent-wire/workflow-gate-broker.ts +17 -1
- package/src/modes/shared/agent-wire/workflow-gate-schema.ts +4 -2
- package/src/modes/shared/agent-wire/workflow-gate-types.ts +120 -3
- package/src/plan-mode/approved-plan.ts +3 -1
- package/src/prompts/agents/architect.md +1 -0
- package/src/prompts/agents/critic.md +1 -0
- package/src/prompts/system/custom-system-prompt.md +1 -1
- package/src/prompts/system/system-prompt.md +2 -1
- package/src/prompts/tools/lsp.md +3 -3
- package/src/runtime-mcp/manager.ts +16 -1
- package/src/runtime-mcp/oauth-discovery.ts +116 -26
- package/src/sdk/broker/broker.ts +139 -32
- package/src/sdk/broker/discovery.ts +125 -0
- package/src/sdk/broker/ensure.ts +103 -19
- package/src/sdk/broker/internal.ts +10 -0
- package/src/sdk/broker/session-index.ts +309 -38
- package/src/sdk/broker/state-version.ts +5 -2
- package/src/sdk/bus/chat-daemon-cli.ts +72 -10
- package/src/sdk/bus/chat-daemon-control.ts +526 -130
- package/src/sdk/bus/chat-daemon-runtime.ts +1 -0
- package/src/sdk/bus/config-commands.ts +22 -0
- package/src/sdk/bus/config.ts +37 -0
- package/src/sdk/bus/index.ts +384 -92
- package/src/sdk/bus/lifecycle-control-runtime.ts +53 -3
- package/src/sdk/bus/notification-service.ts +284 -62
- package/src/sdk/bus/recent-activity.ts +14 -5
- package/src/sdk/bus/session-control.ts +150 -44
- package/src/sdk/bus/telegram-daemon-cli.ts +20 -14
- package/src/sdk/bus/telegram-daemon-contract.ts +6 -3
- package/src/sdk/bus/telegram-daemon-control.ts +294 -76
- package/src/sdk/bus/telegram-daemon.ts +1842 -281
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +155 -27
- package/src/secrets/index.ts +53 -10
- package/src/secrets/obfuscator.ts +21 -18
- package/src/secrets/regex.ts +2 -0
- package/src/session/agent-session.ts +495 -253
- package/src/session/artifacts.ts +188 -23
- package/src/session/context-estimation.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +215 -39
- package/src/session/internal/managed-session-storage.ts +1152 -46
- package/src/session/session-manager.ts +1055 -148
- package/src/session/session-storage.ts +109 -11
- package/src/session/streaming-output.ts +6 -0
- package/src/task/executor.ts +71 -26
- package/src/task/index.ts +63 -20
- package/src/tool-discovery/tool-index.ts +18 -0
- package/src/tools/ask.ts +329 -57
- package/src/tools/bash-allowed-prefixes.ts +79 -29
- package/src/tools/bash-skill-urls.ts +14 -3
- package/src/tools/bash.ts +2 -2
- package/src/tools/browser/launch.ts +3 -1
- package/src/tools/browser/readable.ts +1 -1
- package/src/tools/browser/screenshot-format.ts +34 -0
- package/src/tools/browser/tab-protocol.ts +3 -0
- package/src/tools/browser/tab-supervisor.ts +125 -122
- package/src/tools/browser/tab-worker-entry.ts +33 -14
- package/src/tools/browser/tab-worker-path-resolver.ts +67 -0
- package/src/tools/browser/tab-worker-smoke.ts +87 -0
- package/src/tools/browser/tab-worker.ts +7 -5
- package/src/tools/index.ts +4 -0
- package/src/tools/path-utils.ts +42 -35
- package/src/tools/plan-mode-guard.ts +1 -0
- package/src/tools/read.ts +7 -1
- package/src/tools/render-utils.ts +1 -27
- package/src/tools/resource-gc.ts +123 -34
- package/src/tools/sqlite-reader.ts +3 -2
- package/src/tools/subagent.ts +2 -0
- package/src/utils/image-loading.ts +85 -34
- package/src/utils/image-resize.ts +16 -9
- package/src/utils/pasted-image-loading.ts +514 -0
- package/src/utils/pasted-image-path.ts +223 -98
- package/src/web/scrapers/html-to-markdown.ts +68 -0
- package/src/web/scrapers/readthedocs.ts +1 -1
- package/src/web/scrapers/types.ts +1 -68
- package/src/web/search/providers/openai-compatible.ts +76 -23
|
@@ -14,13 +14,16 @@ import {
|
|
|
14
14
|
import {
|
|
15
15
|
acquireManagedLock,
|
|
16
16
|
captureManagedFileNoFollow,
|
|
17
|
+
captureManagedFilePrefixNoFollow,
|
|
17
18
|
copyManagedFileNoReplace,
|
|
18
19
|
ensureManagedDirectory,
|
|
19
20
|
fsyncManagedArtifactTree,
|
|
21
|
+
ManagedSessionDescendantStore,
|
|
20
22
|
type ManagedStorageLock,
|
|
23
|
+
managedDirectoryRoot,
|
|
21
24
|
publishManagedFileNoReplace,
|
|
22
|
-
publishManagedFileNoReplaceSync,
|
|
23
25
|
publishManagedTombstone,
|
|
26
|
+
retainManagedDirectoryAuthority,
|
|
24
27
|
validateManagedArtifactTree,
|
|
25
28
|
} from "./managed-session-storage";
|
|
26
29
|
|
|
@@ -41,6 +44,49 @@ export interface ManagedScope {
|
|
|
41
44
|
platform: "posix" | "win32";
|
|
42
45
|
}
|
|
43
46
|
|
|
47
|
+
const managedRoots = new WeakMap<ManagedScope, ReturnType<typeof managedDirectoryRoot>>();
|
|
48
|
+
const managedDirectoryIdentities = new WeakMap<ManagedScope, { dev: bigint; ino: bigint }>();
|
|
49
|
+
const managedDirectoryAuthorities = new WeakMap<ManagedScope, native.RecoveryFsRoot | undefined>();
|
|
50
|
+
|
|
51
|
+
export function managedDirectoryAuthorityForScope(scope: ManagedScope): native.RecoveryFsRoot | undefined {
|
|
52
|
+
if (!managedDirectoryAuthorities.has(scope)) throw new Error("Managed session directory authority was not prepared");
|
|
53
|
+
return managedDirectoryAuthorities.get(scope);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function managedDirectoryIdentityForScope(scope: ManagedScope): { dev: bigint; ino: bigint } {
|
|
57
|
+
const identity = managedDirectoryIdentities.get(scope);
|
|
58
|
+
if (!identity) throw new Error("Managed session directory identity was not prepared");
|
|
59
|
+
return identity;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function configuredRootPath(scope: ManagedScope): string {
|
|
63
|
+
let candidate = pathIsWithin(scope.agentDir, scope.sessionsRoot) ? scope.agentDir : path.dirname(scope.sessionsRoot);
|
|
64
|
+
for (;;) {
|
|
65
|
+
try {
|
|
66
|
+
const stat = fs.lstatSync(candidate);
|
|
67
|
+
if (!stat.isDirectory() || stat.isSymbolicLink()) throw new Error(`Unsafe configured root: ${candidate}`);
|
|
68
|
+
return fs.realpathSync.native(candidate);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") throw error;
|
|
71
|
+
const parent = path.dirname(candidate);
|
|
72
|
+
if (parent === candidate) throw new Error("Configured managed root is unavailable.");
|
|
73
|
+
candidate = parent;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function scopeRoot(scope: ManagedScope) {
|
|
79
|
+
const retained = managedRoots.get(scope);
|
|
80
|
+
if (retained) return retained;
|
|
81
|
+
const root = managedDirectoryRoot(configuredRootPath(scope));
|
|
82
|
+
managedRoots.set(scope, root);
|
|
83
|
+
return root;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
export function managedRootForScope(scope: ManagedScope) {
|
|
87
|
+
return scopeRoot(scope);
|
|
88
|
+
}
|
|
89
|
+
|
|
44
90
|
export type ManagedMigrationPolicy = "copy-retain" | "disabled";
|
|
45
91
|
|
|
46
92
|
export type ManagedScopeErrorCode =
|
|
@@ -99,6 +145,7 @@ export type ManagedDeleteCandidateResult =
|
|
|
99
145
|
type NativeIdentity =
|
|
100
146
|
| { ok: true; platform: "posix" | "win32"; canonicalPath: string }
|
|
101
147
|
| { ok: false; code: NativeIdentityFailureCode };
|
|
148
|
+
type CanonicalNativeIdentity = Extract<NativeIdentity, { ok: true }>;
|
|
102
149
|
|
|
103
150
|
type NativeIdentityFailureCode =
|
|
104
151
|
| "not_found"
|
|
@@ -149,6 +196,18 @@ function identityFor(cwd: string): NativeIdentity {
|
|
|
149
196
|
return canonicalExistingDirectoryIdentity(cwd) as NativeIdentity;
|
|
150
197
|
}
|
|
151
198
|
|
|
199
|
+
function canonicalExistingPathForIo(base: string, identity: CanonicalNativeIdentity): string {
|
|
200
|
+
if (identity.platform !== "win32") return identity.canonicalPath;
|
|
201
|
+
try {
|
|
202
|
+
// Native identity uses a stable Volume GUID path on Windows. Bun 1.3.14
|
|
203
|
+
// cannot reliably create/read files through that path, so retain the
|
|
204
|
+
// symlink-resolved DOS path for JavaScript filesystem I/O.
|
|
205
|
+
return fs.realpathSync.native(base);
|
|
206
|
+
} catch {
|
|
207
|
+
return path.resolve(base);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
152
211
|
/**
|
|
153
212
|
* Resolve benign symlinks in the deepest existing ancestor of a trusted storage
|
|
154
213
|
* root (e.g. macOS `/var -> /private/var`, or a symlinked `$HOME`) while keeping
|
|
@@ -164,7 +223,8 @@ export function canonicalizeTrustedPath(target: string): string {
|
|
|
164
223
|
for (;;) {
|
|
165
224
|
const identity = canonicalExistingDirectoryIdentity(base) as NativeIdentity;
|
|
166
225
|
if (identity.ok) {
|
|
167
|
-
|
|
226
|
+
const canonicalBase = canonicalExistingPathForIo(base, identity);
|
|
227
|
+
return suffix.length === 0 ? canonicalBase : path.join(canonicalBase, ...suffix);
|
|
168
228
|
}
|
|
169
229
|
if (identity.code !== "not_found" && identity.code !== "not_directory") return path.resolve(target);
|
|
170
230
|
const parent = path.dirname(base);
|
|
@@ -214,15 +274,11 @@ function isBinding(value: unknown): value is Binding {
|
|
|
214
274
|
);
|
|
215
275
|
}
|
|
216
276
|
|
|
217
|
-
function
|
|
218
|
-
const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
|
|
219
|
-
let raw: string;
|
|
277
|
+
function validateBindingRaw(scope: ManagedScope, raw: string): ManagedScopeResolution | undefined {
|
|
220
278
|
let parsed: unknown;
|
|
221
279
|
try {
|
|
222
|
-
raw = captureManagedFileNoFollow(bindingPath).bytes.toString("utf8");
|
|
223
280
|
parsed = JSON.parse(raw);
|
|
224
|
-
} catch
|
|
225
|
-
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
281
|
+
} catch {
|
|
226
282
|
return { kind: "error", code: "binding_invalid", message: "The managed scope binding is invalid JSON." };
|
|
227
283
|
}
|
|
228
284
|
if (!isBinding(parsed))
|
|
@@ -249,6 +305,18 @@ function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution |
|
|
|
249
305
|
return undefined;
|
|
250
306
|
}
|
|
251
307
|
|
|
308
|
+
function validateExistingBinding(scope: ManagedScope): ManagedScopeResolution | undefined {
|
|
309
|
+
const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
|
|
310
|
+
let raw: string;
|
|
311
|
+
try {
|
|
312
|
+
raw = captureManagedFileNoFollow(bindingPath).bytes.toString("utf8");
|
|
313
|
+
} catch (error) {
|
|
314
|
+
if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
|
|
315
|
+
return { kind: "error", code: "binding_invalid", message: "The managed scope binding is invalid JSON." };
|
|
316
|
+
}
|
|
317
|
+
return validateBindingRaw(scope, raw);
|
|
318
|
+
}
|
|
319
|
+
|
|
252
320
|
export function resolveManagedScope(input: {
|
|
253
321
|
cwd: string;
|
|
254
322
|
agentDir: string;
|
|
@@ -256,6 +324,23 @@ export function resolveManagedScope(input: {
|
|
|
256
324
|
}): ManagedScopeResolution {
|
|
257
325
|
const identity = identityFor(input.cwd);
|
|
258
326
|
if (!identity.ok) return nativeFailure(identity.code);
|
|
327
|
+
try {
|
|
328
|
+
if (fs.lstatSync(input.sessionsRoot).isSymbolicLink()) {
|
|
329
|
+
return {
|
|
330
|
+
kind: "error",
|
|
331
|
+
code: "sessions_root_unavailable",
|
|
332
|
+
message: "The sessions root is not a safe directory.",
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
} catch (error) {
|
|
336
|
+
if ((error as NodeJS.ErrnoException).code !== "ENOENT") {
|
|
337
|
+
return {
|
|
338
|
+
kind: "error",
|
|
339
|
+
code: "sessions_root_unavailable",
|
|
340
|
+
message: "The sessions root could not be inspected.",
|
|
341
|
+
};
|
|
342
|
+
}
|
|
343
|
+
}
|
|
259
344
|
const sessionsRoot = canonicalizeTrustedPath(input.sessionsRoot);
|
|
260
345
|
const agentDir = canonicalizeTrustedPath(input.agentDir);
|
|
261
346
|
const digest = scopeDigest(identity.platform, identity.canonicalPath);
|
|
@@ -376,6 +461,49 @@ function fsyncManagedParent(pathname: string): void {
|
|
|
376
461
|
}
|
|
377
462
|
}
|
|
378
463
|
|
|
464
|
+
type CandidatePreflight =
|
|
465
|
+
| { kind: "capture"; identity: { dev: bigint; ino: bigint; size: number; mtimeNs: bigint } }
|
|
466
|
+
| {
|
|
467
|
+
kind: "foreign";
|
|
468
|
+
}
|
|
469
|
+
| {
|
|
470
|
+
kind: "invalid";
|
|
471
|
+
code: string;
|
|
472
|
+
};
|
|
473
|
+
|
|
474
|
+
function preflightCandidate(filePath: string, scope: ManagedScope): CandidatePreflight {
|
|
475
|
+
try {
|
|
476
|
+
const snapshot = captureManagedFilePrefixNoFollow(filePath, HEADER_MAX_BYTES);
|
|
477
|
+
const lineEnd = snapshot.bytes.indexOf(0x0a);
|
|
478
|
+
if (lineEnd < 0) return { kind: "invalid", code: "invalid_header" };
|
|
479
|
+
const value: unknown = JSON.parse(snapshot.bytes.subarray(0, lineEnd).toString("utf8"));
|
|
480
|
+
if (!value || typeof value !== "object" || Array.isArray(value))
|
|
481
|
+
return { kind: "invalid", code: "invalid_header" };
|
|
482
|
+
const header = value as Record<string, unknown>;
|
|
483
|
+
if (header.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string")
|
|
484
|
+
return { kind: "invalid", code: "invalid_header" };
|
|
485
|
+
const candidateIdentity = identityFor(header.cwd);
|
|
486
|
+
if (
|
|
487
|
+
candidateIdentity.ok &&
|
|
488
|
+
(candidateIdentity.platform !== scope.platform || candidateIdentity.canonicalPath !== scope.canonicalCwd)
|
|
489
|
+
)
|
|
490
|
+
return { kind: "foreign" };
|
|
491
|
+
return { kind: "capture", identity: snapshot.identity };
|
|
492
|
+
} catch {
|
|
493
|
+
return { kind: "invalid", code: "unreadable_candidate" };
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function matchesPreflightIdentity(candidate: ManagedCandidate, preflight: CandidatePreflight): boolean {
|
|
498
|
+
return (
|
|
499
|
+
preflight.kind === "capture" &&
|
|
500
|
+
candidate.identity.dev === preflight.identity.dev &&
|
|
501
|
+
candidate.identity.ino === preflight.identity.ino &&
|
|
502
|
+
candidate.identity.size === preflight.identity.size &&
|
|
503
|
+
candidate.identity.mtimeNs === preflight.identity.mtimeNs
|
|
504
|
+
);
|
|
505
|
+
}
|
|
506
|
+
|
|
379
507
|
function inspectCandidate(filePath: string, provenance: "v2" | "legacy"): ManagedCandidate | { code: string } {
|
|
380
508
|
try {
|
|
381
509
|
const snapshot = captureManagedFileNoFollow(filePath);
|
|
@@ -386,7 +514,8 @@ function inspectCandidate(filePath: string, provenance: "v2" | "legacy"): Manage
|
|
|
386
514
|
const header = value as Record<string, unknown>;
|
|
387
515
|
if (header.type !== "session" || typeof header.id !== "string" || typeof header.cwd !== "string")
|
|
388
516
|
return { code: "invalid_header" };
|
|
389
|
-
|
|
517
|
+
const cwdIdentity = identityFor(header.cwd);
|
|
518
|
+
if (!cwdIdentity.ok) return { code: `cwd_${cwdIdentity.code}` };
|
|
390
519
|
const named = fs.lstatSync(filePath, { bigint: true });
|
|
391
520
|
if (
|
|
392
521
|
!named.isFile() ||
|
|
@@ -436,10 +565,13 @@ function discoveredLegacyDirectoryNames(scope: ManagedScope): readonly string[]
|
|
|
436
565
|
return [...known, ...discovered];
|
|
437
566
|
}
|
|
438
567
|
|
|
568
|
+
type CandidateInspection = ManagedCandidate | { code: string } | { foreign: true };
|
|
569
|
+
|
|
439
570
|
function listDirectoryCandidates(
|
|
440
571
|
directory: string,
|
|
441
572
|
provenance: "v2" | "legacy",
|
|
442
|
-
|
|
573
|
+
scope: ManagedScope,
|
|
574
|
+
): readonly CandidateInspection[] {
|
|
443
575
|
let directoryStat: fs.Stats;
|
|
444
576
|
try {
|
|
445
577
|
directoryStat = fs.lstatSync(directory);
|
|
@@ -451,17 +583,26 @@ function listDirectoryCandidates(
|
|
|
451
583
|
return fs
|
|
452
584
|
.readdirSync(directory, { withFileTypes: true })
|
|
453
585
|
.filter(entry => entry.name.endsWith(".jsonl"))
|
|
454
|
-
.map(entry =>
|
|
586
|
+
.map(entry => {
|
|
587
|
+
const filePath = path.join(directory, entry.name);
|
|
588
|
+
const preflight = preflightCandidate(filePath, scope);
|
|
589
|
+
if (preflight.kind === "invalid") return { code: preflight.code };
|
|
590
|
+
if (preflight.kind === "foreign") return { foreign: true };
|
|
591
|
+
const candidate = inspectCandidate(filePath, provenance);
|
|
592
|
+
if ("code" in candidate) return candidate;
|
|
593
|
+
return matchesPreflightIdentity(candidate, preflight) ? candidate : { code: "source_changed" };
|
|
594
|
+
});
|
|
455
595
|
}
|
|
456
596
|
|
|
457
597
|
export async function ensureManagedScope(scope: ManagedScope): Promise<ManagedScopeResolution> {
|
|
458
598
|
try {
|
|
459
|
-
|
|
460
|
-
ensureManagedDirectory(scope.
|
|
599
|
+
const root = scopeRoot(scope);
|
|
600
|
+
ensureManagedDirectory(scope.sessionsRoot, root);
|
|
601
|
+
ensureManagedDirectory(scope.directoryPath, root);
|
|
461
602
|
const bindingPath = path.join(scope.directoryPath, MANAGED_SESSION_BINDING_FILE);
|
|
462
603
|
const binding = `${JSON.stringify(bindingFor(scope))}\n`;
|
|
463
604
|
try {
|
|
464
|
-
await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding));
|
|
605
|
+
await publishManagedFileNoReplace(bindingPath, new TextEncoder().encode(binding), undefined, root);
|
|
465
606
|
} catch (error) {
|
|
466
607
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
467
608
|
}
|
|
@@ -478,24 +619,46 @@ export async function ensureManagedScope(scope: ManagedScope): Promise<ManagedSc
|
|
|
478
619
|
/** Synchronously create and validate the v2 binding before a default session writer exists. */
|
|
479
620
|
export function prepareManagedSessionScopeForWriteSync(scope: ManagedScope): ManagedScopeResolution {
|
|
480
621
|
try {
|
|
481
|
-
|
|
482
|
-
ensureManagedDirectory(scope.
|
|
483
|
-
|
|
622
|
+
const root = scopeRoot(scope);
|
|
623
|
+
ensureManagedDirectory(scope.sessionsRoot, root);
|
|
624
|
+
ensureManagedDirectory(scope.directoryPath, root);
|
|
625
|
+
const preparedDirectory = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
626
|
+
if (!preparedDirectory.isDirectory() || preparedDirectory.isSymbolicLink())
|
|
627
|
+
throw new Error("Managed session directory changed");
|
|
628
|
+
const retainedAuthority = retainManagedDirectoryAuthority(root, scope.directoryPath, {
|
|
629
|
+
dev: preparedDirectory.dev,
|
|
630
|
+
ino: preparedDirectory.ino,
|
|
631
|
+
});
|
|
632
|
+
const store = new ManagedSessionDescendantStore(
|
|
633
|
+
root,
|
|
634
|
+
scope.directoryPath,
|
|
635
|
+
retainedAuthority ? { authority: retainedAuthority, authorityBaseDir: scope.directoryPath } : undefined,
|
|
636
|
+
);
|
|
637
|
+
const binding = new TextEncoder().encode(`${JSON.stringify(bindingFor(scope))}\n`);
|
|
484
638
|
try {
|
|
485
|
-
|
|
486
|
-
bindingPath,
|
|
487
|
-
new TextEncoder().encode(`${JSON.stringify(bindingFor(scope))}\n`),
|
|
488
|
-
);
|
|
639
|
+
store.publishNoReplaceSync(MANAGED_SESSION_BINDING_FILE, binding);
|
|
489
640
|
} catch (error) {
|
|
490
641
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
491
642
|
}
|
|
492
|
-
const
|
|
643
|
+
const capturedBinding = store.readExpected(MANAGED_SESSION_BINDING_FILE);
|
|
644
|
+
if (!capturedBinding) throw new Error("Managed scope binding is unavailable");
|
|
645
|
+
const validated = validateBindingRaw(scope, capturedBinding.bytes.toString("utf8"));
|
|
646
|
+
managedDirectoryAuthorities.set(scope, retainedAuthority);
|
|
647
|
+
const directoryStat = fs.lstatSync(scope.directoryPath, { bigint: true });
|
|
648
|
+
if (
|
|
649
|
+
!directoryStat.isDirectory() ||
|
|
650
|
+
directoryStat.isSymbolicLink() ||
|
|
651
|
+
directoryStat.dev !== preparedDirectory.dev ||
|
|
652
|
+
directoryStat.ino !== preparedDirectory.ino
|
|
653
|
+
)
|
|
654
|
+
throw new Error("Managed session directory changed");
|
|
655
|
+
managedDirectoryIdentities.set(scope, { dev: preparedDirectory.dev, ino: preparedDirectory.ino });
|
|
493
656
|
if (validated) return validated;
|
|
494
657
|
const internal = managedInternalDirectory(scope);
|
|
495
|
-
ensureManagedDirectory(internal);
|
|
496
|
-
ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY));
|
|
497
|
-
ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY));
|
|
498
|
-
ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY));
|
|
658
|
+
ensureManagedDirectory(internal, root);
|
|
659
|
+
ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root);
|
|
660
|
+
ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root);
|
|
661
|
+
ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root);
|
|
499
662
|
return { kind: "resolved", scope };
|
|
500
663
|
} catch (error) {
|
|
501
664
|
return {
|
|
@@ -530,14 +693,18 @@ export function listManagedCandidates(scope: ManagedScope): ManagedCandidateList
|
|
|
530
693
|
];
|
|
531
694
|
const seen = new Set<string>();
|
|
532
695
|
for (const directory of directories) {
|
|
533
|
-
for (const candidate of listDirectoryCandidates(directory.path, directory.provenance)) {
|
|
696
|
+
for (const candidate of listDirectoryCandidates(directory.path, directory.provenance, scope)) {
|
|
534
697
|
if ("code" in candidate) {
|
|
535
698
|
invalid.push({ code: candidate.code });
|
|
536
699
|
continue;
|
|
537
700
|
}
|
|
701
|
+
if ("foreign" in candidate) {
|
|
702
|
+
foreignCount++;
|
|
703
|
+
continue;
|
|
704
|
+
}
|
|
538
705
|
const candidateIdentity = identityFor(candidate.cwd);
|
|
539
706
|
if (!candidateIdentity.ok) {
|
|
540
|
-
invalid.push({ code:
|
|
707
|
+
invalid.push({ code: `cwd_${candidateIdentity.code}` });
|
|
541
708
|
continue;
|
|
542
709
|
}
|
|
543
710
|
if (
|
|
@@ -1074,6 +1241,7 @@ function artifactTreeSnapshot(value: unknown): NativeDirectoryTreeSnapshot | und
|
|
|
1074
1241
|
typeof item.ino === "string" &&
|
|
1075
1242
|
typeof item.size === "string" &&
|
|
1076
1243
|
typeof item.mtimeNs === "string" &&
|
|
1244
|
+
typeof item.ctimeNs === "string" &&
|
|
1077
1245
|
(item.sha256 === undefined || typeof item.sha256 === "string")
|
|
1078
1246
|
);
|
|
1079
1247
|
})
|
|
@@ -1612,12 +1780,14 @@ function restoreDetachedArtifactRoot(detached: DetachedArtifactRoot): void {
|
|
|
1612
1780
|
}
|
|
1613
1781
|
|
|
1614
1782
|
async function copyArtifacts(
|
|
1783
|
+
scope: ManagedScope,
|
|
1615
1784
|
sourceTranscript: string,
|
|
1616
1785
|
destinationTranscript: string,
|
|
1617
1786
|
manifest: readonly ArtifactManifestEntry[],
|
|
1618
1787
|
lock: ManagedStorageLock,
|
|
1619
1788
|
sourceRootOverride?: string,
|
|
1620
1789
|
): Promise<void> {
|
|
1790
|
+
const root = scopeRoot(scope);
|
|
1621
1791
|
if (manifest.length === 0) return;
|
|
1622
1792
|
const sourceRoot = sourceRootOverride ?? sourceTranscript.slice(0, -6);
|
|
1623
1793
|
if (!manifestMatches(sourceTranscript, manifest, sourceRoot)) throw new Error("source_changed");
|
|
@@ -1627,8 +1797,8 @@ async function copyArtifacts(
|
|
|
1627
1797
|
const source = path.join(sourceRoot, entry.path);
|
|
1628
1798
|
const destination = path.join(destinationRoot, entry.path);
|
|
1629
1799
|
if (entry.kind === "directory") {
|
|
1630
|
-
if (entry.path === "") ensureManagedDirectory(destinationRoot);
|
|
1631
|
-
else ensureManagedDirectory(destination);
|
|
1800
|
+
if (entry.path === "") ensureManagedDirectory(destinationRoot, root);
|
|
1801
|
+
else ensureManagedDirectory(destination, root);
|
|
1632
1802
|
continue;
|
|
1633
1803
|
}
|
|
1634
1804
|
const snapshot = captureManagedFileNoFollow(source);
|
|
@@ -1638,7 +1808,7 @@ async function copyArtifacts(
|
|
|
1638
1808
|
)
|
|
1639
1809
|
throw new Error("source_changed");
|
|
1640
1810
|
try {
|
|
1641
|
-
await copyManagedFileNoReplace(source, destination, snapshot);
|
|
1811
|
+
await copyManagedFileNoReplace(source, destination, snapshot, root);
|
|
1642
1812
|
} catch (error) {
|
|
1643
1813
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
1644
1814
|
}
|
|
@@ -1755,6 +1925,7 @@ export async function reconcileManagedTombstones(scope: ManagedScope): Promise<v
|
|
|
1755
1925
|
lock = await acquireManagedLock(
|
|
1756
1926
|
path.join(managedInternalDirectory(scope), MANAGED_LOCKS_DIRECTORY),
|
|
1757
1927
|
path.basename(tombstone, ".json"),
|
|
1928
|
+
scopeRoot(scope),
|
|
1758
1929
|
);
|
|
1759
1930
|
const lockedTargets = retiredTargets(scope, tombstone);
|
|
1760
1931
|
if (!lockedTargets) continue;
|
|
@@ -1866,10 +2037,11 @@ export async function prepareManagedSessionScopeForWrite(scope: ManagedScope): P
|
|
|
1866
2037
|
if (prepared.kind === "error") return prepared;
|
|
1867
2038
|
try {
|
|
1868
2039
|
const internal = managedInternalDirectory(scope);
|
|
1869
|
-
|
|
1870
|
-
ensureManagedDirectory(
|
|
1871
|
-
ensureManagedDirectory(path.join(internal,
|
|
1872
|
-
ensureManagedDirectory(path.join(internal,
|
|
2040
|
+
const root = scopeRoot(scope);
|
|
2041
|
+
ensureManagedDirectory(internal, root);
|
|
2042
|
+
ensureManagedDirectory(path.join(internal, MANAGED_LOCKS_DIRECTORY), root);
|
|
2043
|
+
ensureManagedDirectory(path.join(internal, MANAGED_RECEIPTS_DIRECTORY), root);
|
|
2044
|
+
ensureManagedDirectory(path.join(internal, MANAGED_TOMBSTONES_DIRECTORY), root);
|
|
1873
2045
|
await reconcileManagedTombstones(scope);
|
|
1874
2046
|
return { kind: "resolved", scope };
|
|
1875
2047
|
} catch (error) {
|
|
@@ -1920,7 +2092,7 @@ export async function openManagedCandidateForWrite(
|
|
|
1920
2092
|
let lock: ManagedStorageLock | undefined;
|
|
1921
2093
|
let detachedArtifacts: DetachedArtifactRoot | undefined;
|
|
1922
2094
|
try {
|
|
1923
|
-
lock = await acquireManagedLock(path.join(internal, MANAGED_LOCKS_DIRECTORY), operation);
|
|
2095
|
+
lock = await acquireManagedLock(path.join(internal, MANAGED_LOCKS_DIRECTORY), operation, scopeRoot(scope));
|
|
1924
2096
|
const afterLock = validateCandidateForScope(scope, current);
|
|
1925
2097
|
if (!afterLock)
|
|
1926
2098
|
return { kind: "error", code: "source_changed", message: "The legacy candidate changed during migration." };
|
|
@@ -1986,11 +2158,11 @@ export async function openManagedCandidateForWrite(
|
|
|
1986
2158
|
if (artifactPlan && fs.existsSync(artifactPlan.detachedPath)) throw new Error("destination_conflict");
|
|
1987
2159
|
detachedArtifacts = artifactPlan ? detachArtifactRootForMigration(artifactPlan) : undefined;
|
|
1988
2160
|
manifest = detachedArtifacts ? artifactManifestFromSnapshot(detachedArtifacts.tree) : [];
|
|
1989
|
-
await copyArtifacts(afterLock.path, destination, manifest, lock, detachedArtifacts?.detachedPath);
|
|
2161
|
+
await copyArtifacts(scope, afterLock.path, destination, manifest, lock, detachedArtifacts?.detachedPath);
|
|
1990
2162
|
if (!existing) {
|
|
1991
2163
|
try {
|
|
1992
2164
|
lock.assertOwned();
|
|
1993
|
-
await copyManagedFileNoReplace(afterLock.path, destination, sourceSnapshot);
|
|
2165
|
+
await copyManagedFileNoReplace(afterLock.path, destination, sourceSnapshot, scopeRoot(scope));
|
|
1994
2166
|
} catch (error) {
|
|
1995
2167
|
if ((error as Error).message !== "destination_conflict") throw error;
|
|
1996
2168
|
}
|
|
@@ -2087,7 +2259,11 @@ export async function deleteManagedSessionCandidate(
|
|
|
2087
2259
|
const operation = path.basename(tombstone, ".json");
|
|
2088
2260
|
let lock: ManagedStorageLock | undefined;
|
|
2089
2261
|
try {
|
|
2090
|
-
lock = await acquireManagedLock(
|
|
2262
|
+
lock = await acquireManagedLock(
|
|
2263
|
+
path.join(managedInternalDirectory(scope), MANAGED_LOCKS_DIRECTORY),
|
|
2264
|
+
operation,
|
|
2265
|
+
scopeRoot(scope),
|
|
2266
|
+
);
|
|
2091
2267
|
let targets = retiredTargets(scope, tombstone);
|
|
2092
2268
|
if (!targets) {
|
|
2093
2269
|
if (!current) throw new Error("source_changed");
|