@gajae-code/coding-agent 0.11.5 → 0.11.7
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 +37 -0
- package/dist/types/capability/index.d.ts +10 -20
- package/dist/types/capability/types.d.ts +3 -0
- package/dist/types/commands/daemon.d.ts +2 -2
- package/dist/types/commands/sdk.d.ts +20 -2
- package/dist/types/config/keybindings.d.ts +1 -1
- package/dist/types/config/settings-schema.d.ts +67 -7
- package/dist/types/eval/py/runner-artifact.d.ts +8 -0
- package/dist/types/gjc-runtime/ultragoal-guard.d.ts +5 -5
- package/dist/types/gjc-runtime/ultragoal-receipt-freshness.d.ts +20 -0
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +19 -5
- package/dist/types/main.d.ts +24 -1
- package/dist/types/modes/components/btw-panel.d.ts +1 -0
- package/dist/types/modes/components/model-selector.d.ts +2 -0
- package/dist/types/modes/components/tool-status-header.d.ts +1 -0
- package/dist/types/modes/controllers/btw-controller.d.ts +5 -0
- package/dist/types/modes/interactive-mode.d.ts +2 -0
- package/dist/types/modes/types.d.ts +1 -0
- package/dist/types/runtime-mcp/content-limits.d.ts +7 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +4 -0
- package/dist/types/sdk/bus/index.d.ts +1 -1
- package/dist/types/sdk/bus/lifecycle-commands.d.ts +5 -4
- package/dist/types/sdk/bus/notification-service.d.ts +34 -0
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +13 -3
- package/dist/types/sdk/bus/telegram-daemon.d.ts +61 -3
- package/dist/types/sdk/bus/topic-registry.d.ts +88 -2
- package/dist/types/sdk/client/discovery.d.ts +2 -0
- package/dist/types/sdk/client/liveness.d.ts +7 -0
- package/dist/types/sdk/transport/auth-preface.d.ts +9 -0
- package/dist/types/sdk/transport/index.d.ts +12 -0
- package/dist/types/sdk/transport/relay.d.ts +43 -0
- package/dist/types/sdk/transport/serve-cli.d.ts +4 -0
- package/dist/types/sdk/transport/socket.d.ts +7 -0
- package/dist/types/sdk/transport/stdio.d.ts +3 -0
- package/dist/types/session/agent-session.d.ts +28 -5
- package/dist/types/session/btw-contract.d.ts +16 -0
- package/dist/types/session/internal/managed-session-scope.d.ts +21 -7
- package/dist/types/session/internal/managed-session-storage.d.ts +10 -8
- package/dist/types/session/session-manager.d.ts +35 -4
- package/dist/types/session/yield-queue.d.ts +8 -0
- package/dist/types/setup/credential-import.d.ts +5 -1
- package/dist/types/setup/provider-onboarding.d.ts +2 -0
- package/dist/types/ssh/utils.d.ts +1 -0
- package/dist/types/task/discovery.d.ts +3 -1
- package/dist/types/tools/fetch.d.ts +5 -8
- package/dist/types/tools/image-gen.d.ts +16 -0
- package/dist/types/tools/skill-discovery.d.ts +7 -0
- package/dist/types/tools/sqlite-reader.d.ts +7 -0
- package/dist/types/utils/shell-snapshot.d.ts +12 -0
- package/dist/types/web/insane/bridge.d.ts +1 -0
- package/dist/types/web/insane/url-guard.d.ts +17 -0
- package/package.json +8 -8
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/src/capability/index.ts +50 -42
- package/src/capability/types.ts +3 -0
- package/src/cli/setup-cli.ts +10 -4
- package/src/cli.ts +34 -9
- package/src/commands/daemon.ts +22 -5
- package/src/commands/sdk.ts +42 -9
- package/src/config/keybindings.ts +1 -1
- package/src/config/model-profiles.ts +18 -0
- package/src/config/settings-schema.ts +71 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +42 -1
- package/src/discovery/ssh.ts +12 -1
- package/src/eval/py/kernel.ts +1 -21
- package/src/eval/py/runner-artifact.ts +84 -0
- package/src/extensibility/plugins/marketplace/fetcher.ts +191 -7
- package/src/gjc-runtime/ultragoal-guard.ts +88 -10
- package/src/gjc-runtime/ultragoal-receipt-freshness.ts +103 -0
- package/src/gjc-runtime/ultragoal-runtime.ts +275 -12
- package/src/gjc-runtime/workflow-manifest.generated.json +58 -0
- package/src/gjc-runtime/workflow-manifest.ts +23 -0
- package/src/internal-urls/docs-index.generated.ts +9 -8
- package/src/internal-urls/local-protocol.ts +32 -6
- package/src/main.ts +87 -28
- package/src/modes/action-registry.ts +1 -1
- package/src/modes/components/btw-panel.ts +82 -16
- package/src/modes/components/model-selector.ts +31 -3
- package/src/modes/components/tool-status-header.ts +6 -1
- package/src/modes/controllers/btw-controller.ts +119 -36
- package/src/modes/controllers/command-controller.ts +15 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -2
- package/src/modes/controllers/input-controller.ts +31 -0
- package/src/modes/controllers/selector-controller.ts +106 -12
- package/src/modes/interactive-mode.ts +16 -7
- package/src/modes/runtime-init.ts +16 -2
- package/src/modes/types.ts +1 -0
- package/src/prompts/system/btw-user.md +3 -8
- package/src/prompts/tools/skill-discovery.md +1 -0
- package/src/runtime-mcp/client.ts +41 -55
- package/src/runtime-mcp/content-limits.ts +71 -0
- package/src/runtime-mcp/json-rpc.ts +23 -9
- package/src/runtime-mcp/transports/http.ts +50 -16
- package/src/sdk/bus/chat-daemon-control.ts +20 -7
- package/src/sdk/bus/index.ts +8 -7
- package/src/sdk/bus/lifecycle-commands.ts +16 -7
- package/src/sdk/bus/notification-service.ts +41 -18
- package/src/sdk/bus/telegram-daemon-contract.ts +13 -3
- package/src/sdk/bus/telegram-daemon-control.ts +26 -8
- package/src/sdk/bus/telegram-daemon.ts +1736 -338
- package/src/sdk/bus/topic-registry.ts +451 -12
- package/src/sdk/client/discovery.ts +19 -3
- package/src/sdk/client/liveness.ts +49 -0
- package/src/sdk/host/control/dispatch.ts +14 -5
- package/src/sdk/protocol/operation-inventory.generated.json +29 -0
- package/src/sdk/session.ts +29 -5
- package/src/sdk/transport/auth-preface.ts +82 -0
- package/src/sdk/transport/index.ts +18 -0
- package/src/sdk/transport/relay.ts +252 -0
- package/src/sdk/transport/serve-cli.ts +105 -0
- package/src/sdk/transport/socket.ts +197 -0
- package/src/sdk/transport/stdio.ts +17 -0
- package/src/session/agent-session.ts +1212 -768
- package/src/session/blob-store.ts +19 -6
- package/src/session/btw-contract.ts +59 -0
- package/src/session/internal/managed-session-scope.ts +313 -57
- package/src/session/internal/managed-session-storage.ts +146 -35
- package/src/session/session-manager.ts +385 -85
- package/src/session/yield-queue.ts +20 -0
- package/src/setup/credential-import.ts +28 -3
- package/src/setup/provider-onboarding.ts +30 -1
- package/src/setup/provider-presets.json +2 -1
- package/src/slash-commands/builtin-registry.ts +37 -1
- package/src/ssh/utils.ts +14 -0
- package/src/task/discovery.ts +8 -2
- package/src/tools/ask-answer-registry.ts +15 -1
- package/src/tools/ask.ts +1 -1
- package/src/tools/fetch.ts +13 -131
- package/src/tools/image-gen.ts +301 -20
- package/src/tools/read.ts +2 -0
- package/src/tools/skill-discovery.ts +39 -3
- package/src/tools/sqlite-reader.ts +63 -5
- package/src/utils/pasted-image-path.ts +15 -6
- package/src/utils/shell-snapshot.ts +173 -63
- package/src/web/insane/bridge.ts +5 -1
- package/src/web/insane/url-guard.ts +138 -27
- package/src/web/scrapers/types.ts +19 -49
- package/src/web/scrapers/utils.ts +12 -26
|
@@ -18,7 +18,6 @@ import * as native from "@gajae-code/natives";
|
|
|
18
18
|
import { getTerminalId } from "@gajae-code/tui";
|
|
19
19
|
import {
|
|
20
20
|
getBlobsDir,
|
|
21
|
-
getAgentDir as getDefaultAgentDir,
|
|
22
21
|
getProjectDir,
|
|
23
22
|
getSessionsDir,
|
|
24
23
|
getTerminalSessionsDir,
|
|
@@ -58,6 +57,7 @@ import {
|
|
|
58
57
|
canonicalizeTrustedPath,
|
|
59
58
|
deleteManagedSessionCandidate,
|
|
60
59
|
listManagedCandidates,
|
|
60
|
+
type ManagedCandidateWriteAuthority,
|
|
61
61
|
type ManagedMigrationPolicy,
|
|
62
62
|
type ManagedScope,
|
|
63
63
|
managedDirectoryAuthorityForScope,
|
|
@@ -65,11 +65,14 @@ import {
|
|
|
65
65
|
openManagedCandidateForWrite,
|
|
66
66
|
prepareManagedSessionScopeForWriteSync,
|
|
67
67
|
resolveManagedScope,
|
|
68
|
+
resolveManagedScopeForWrite,
|
|
68
69
|
} from "./internal/managed-session-scope";
|
|
69
70
|
import {
|
|
71
|
+
assertManagedDirectoryRoot,
|
|
70
72
|
type ManagedDirectoryRoot,
|
|
71
73
|
type ManagedFileSnapshot,
|
|
72
74
|
ManagedSessionDescendantStore,
|
|
75
|
+
type ManagedSessionSecurityPolicy,
|
|
73
76
|
retainManagedDirectoryAuthority,
|
|
74
77
|
} from "./internal/managed-session-storage";
|
|
75
78
|
|
|
@@ -853,6 +856,32 @@ function resolveManagedSessionRoot(sessionDir: string, cwd: string): string | un
|
|
|
853
856
|
return path.resolve(sessionDir) === path.resolve(resolved.scope.directoryPath) ? sessionsRoot : undefined;
|
|
854
857
|
}
|
|
855
858
|
|
|
859
|
+
/**
|
|
860
|
+
* Resolve `target` through the real path of its nearest existing ancestor,
|
|
861
|
+
* re-appending any not-yet-created trailing segments. Symlink/reparse components
|
|
862
|
+
* in the existing prefix are canonicalized (so aliases cannot escape a containment
|
|
863
|
+
* check or alias another file), while a missing leaf (e.g. an uncommitted session
|
|
864
|
+
* transcript) is still resolved deterministically. Non-ENOENT errors (ELOOP,
|
|
865
|
+
* EACCES, ...) surface rather than silently falling back to the lexical path.
|
|
866
|
+
*/
|
|
867
|
+
function canonicalizeThroughExistingAncestor(target: string): string {
|
|
868
|
+
const resolved = path.resolve(target);
|
|
869
|
+
const tail: string[] = [];
|
|
870
|
+
let current = resolved;
|
|
871
|
+
while (true) {
|
|
872
|
+
try {
|
|
873
|
+
const real = fs.realpathSync(current);
|
|
874
|
+
return tail.length === 0 ? real : path.join(real, ...tail.reverse());
|
|
875
|
+
} catch (err) {
|
|
876
|
+
if (!isEnoent(err)) throw err;
|
|
877
|
+
const parent = path.dirname(current);
|
|
878
|
+
if (parent === current) return resolved;
|
|
879
|
+
tail.push(path.basename(current));
|
|
880
|
+
current = parent;
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
|
|
856
885
|
function isSupportedSessionVersion(version: unknown): boolean {
|
|
857
886
|
return (
|
|
858
887
|
version === undefined ||
|
|
@@ -1298,9 +1327,12 @@ function computeDefaultSessionDir(
|
|
|
1298
1327
|
sessionsRoot: string = getSessionsDir(),
|
|
1299
1328
|
): string {
|
|
1300
1329
|
if (!(storage instanceof FileSessionStorage)) throw new SessionManagedStorageError();
|
|
1301
|
-
const resolved =
|
|
1330
|
+
const resolved = resolveManagedScopeForWrite({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
|
|
1302
1331
|
if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
|
|
1303
|
-
const prepared = prepareManagedSessionScopeForWriteSync(
|
|
1332
|
+
const prepared = prepareManagedSessionScopeForWriteSync(
|
|
1333
|
+
resolved.scope,
|
|
1334
|
+
process.platform === "win32" ? "windows-existing-verify-first" : "default",
|
|
1335
|
+
);
|
|
1304
1336
|
if (prepared.kind === "error") throw new Error(`Could not prepare managed session scope: ${prepared.message}`);
|
|
1305
1337
|
return prepared.scope.directoryPath;
|
|
1306
1338
|
}
|
|
@@ -1382,6 +1414,13 @@ function managedFileSnapshotEquals(left: ManagedFileSnapshot | null, right: Mana
|
|
|
1382
1414
|
);
|
|
1383
1415
|
}
|
|
1384
1416
|
const trustedSessionDestinations = new WeakSet<SessionDestination>();
|
|
1417
|
+
const managedSecurityPolicies = new WeakMap<ManagedSessionSecurityContext, ManagedSessionSecurityPolicy>();
|
|
1418
|
+
|
|
1419
|
+
function managedSecurityPolicyForContext(securityContext: ManagedSessionSecurityContext): ManagedSessionSecurityPolicy {
|
|
1420
|
+
const policy = managedSecurityPolicies.get(securityContext);
|
|
1421
|
+
if (!policy) throw new Error("Managed session security policy unavailable");
|
|
1422
|
+
return policy;
|
|
1423
|
+
}
|
|
1385
1424
|
|
|
1386
1425
|
function managedStoreFromContext(
|
|
1387
1426
|
securityContext: ManagedSessionSecurityContext,
|
|
@@ -1393,6 +1432,7 @@ function managedStoreFromContext(
|
|
|
1393
1432
|
securityContext.retainedAuthority
|
|
1394
1433
|
? { authority: securityContext.retainedAuthority, authorityBaseDir: directory }
|
|
1395
1434
|
: undefined,
|
|
1435
|
+
managedSecurityPolicyForContext(securityContext),
|
|
1396
1436
|
);
|
|
1397
1437
|
}
|
|
1398
1438
|
|
|
@@ -1405,6 +1445,10 @@ function freezeManagedDestination(scope: ManagedScope): SessionDestination {
|
|
|
1405
1445
|
rootAuthority,
|
|
1406
1446
|
retainedAuthority: managedDirectoryAuthorityForScope(scope),
|
|
1407
1447
|
});
|
|
1448
|
+
managedSecurityPolicies.set(
|
|
1449
|
+
securityContext,
|
|
1450
|
+
scope.platform === "win32" ? "windows-existing-verify-first" : "default",
|
|
1451
|
+
);
|
|
1408
1452
|
const destination = Object.freeze({ kind: "managed" as const, directory: scope.directoryPath, securityContext });
|
|
1409
1453
|
trustedSessionDestinations.add(destination);
|
|
1410
1454
|
return destination;
|
|
@@ -1419,9 +1463,16 @@ function explicitDestination(directory: string): SessionDestination {
|
|
|
1419
1463
|
function managedDestination(cwd: string, storage: SessionStorage, agentDir?: string): SessionDestination {
|
|
1420
1464
|
if (!(storage instanceof FileSessionStorage)) throw new SessionManagedStorageError();
|
|
1421
1465
|
const sessionsRoot = getSessionsDir(agentDir);
|
|
1422
|
-
const resolved =
|
|
1466
|
+
const resolved = resolveManagedScopeForWrite({
|
|
1467
|
+
cwd,
|
|
1468
|
+
agentDir: agentDir ?? path.resolve(sessionsRoot, ".."),
|
|
1469
|
+
sessionsRoot,
|
|
1470
|
+
});
|
|
1423
1471
|
if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
|
|
1424
|
-
const prepared = prepareManagedSessionScopeForWriteSync(
|
|
1472
|
+
const prepared = prepareManagedSessionScopeForWriteSync(
|
|
1473
|
+
resolved.scope,
|
|
1474
|
+
process.platform === "win32" ? "windows-existing-verify-first" : "default",
|
|
1475
|
+
);
|
|
1425
1476
|
if (prepared.kind === "error") throw new Error(`Could not prepare managed session scope: ${prepared.message}`);
|
|
1426
1477
|
return freezeManagedDestination(prepared.scope);
|
|
1427
1478
|
}
|
|
@@ -1788,12 +1839,46 @@ function inspectResumeSessionFile(
|
|
|
1788
1839
|
mtimeNs: snapshot.mtimeNs,
|
|
1789
1840
|
sha256: crypto.createHash("sha256").update(bytes).digest("hex"),
|
|
1790
1841
|
};
|
|
1791
|
-
return { identity, content:
|
|
1842
|
+
return { identity, content: bytes, entries, context, migrationApplied };
|
|
1792
1843
|
} catch {
|
|
1793
1844
|
return { kind: "error", reason: "malformed" };
|
|
1794
1845
|
}
|
|
1795
1846
|
}
|
|
1796
1847
|
|
|
1848
|
+
/** Revalidate previously parsed authority without another decode, parse, migration, or context build. */
|
|
1849
|
+
function revalidateResumeSessionIdentity(
|
|
1850
|
+
filePath: string,
|
|
1851
|
+
storage: SessionStorage,
|
|
1852
|
+
expected: ResumeSessionIdentity,
|
|
1853
|
+
): StrictSessionOpenFailure | { kind: "valid" } {
|
|
1854
|
+
const canonicalPath = resolveEquivalentPath(path.resolve(filePath));
|
|
1855
|
+
let before: SessionStorageStat;
|
|
1856
|
+
let snapshot: SessionStorageSnapshot;
|
|
1857
|
+
let after: SessionStorageStat;
|
|
1858
|
+
try {
|
|
1859
|
+
before = storage.statSync(canonicalPath);
|
|
1860
|
+
if (!before.isFile || !storage.readSnapshotSync) return { kind: "error", reason: "read-failed" };
|
|
1861
|
+
snapshot = storage.readSnapshotSync(canonicalPath);
|
|
1862
|
+
after = storage.statSync(canonicalPath);
|
|
1863
|
+
} catch (error) {
|
|
1864
|
+
return resumeReadFailure(error, storage, canonicalPath);
|
|
1865
|
+
}
|
|
1866
|
+
if (!sameResumeStat(before, snapshot.stat) || !sameResumeStat(snapshot.stat, after)) {
|
|
1867
|
+
return { kind: "error", reason: "unstable" };
|
|
1868
|
+
}
|
|
1869
|
+
const observed: ResumeSessionIdentity = {
|
|
1870
|
+
canonicalPath,
|
|
1871
|
+
sessionId: expected.sessionId,
|
|
1872
|
+
dev: snapshot.stat.dev,
|
|
1873
|
+
ino: snapshot.stat.ino,
|
|
1874
|
+
size: snapshot.stat.size,
|
|
1875
|
+
mtimeMs: snapshot.stat.mtimeMs,
|
|
1876
|
+
mtimeNs: snapshot.stat.mtimeNs,
|
|
1877
|
+
sha256: crypto.createHash("sha256").update(snapshot.bytes).digest("hex"),
|
|
1878
|
+
};
|
|
1879
|
+
return sameResumeIdentity(expected, observed) ? { kind: "valid" } : { kind: "error", reason: "identity-mismatch" };
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1797
1882
|
/**
|
|
1798
1883
|
* Convert legacy persisted blob references in loaded entries into resident sentinels.
|
|
1799
1884
|
* Images then materialize lazily at provider/display/export chokepoints instead of
|
|
@@ -3841,9 +3926,11 @@ export async function resolveResumableSession(
|
|
|
3841
3926
|
cwd: string,
|
|
3842
3927
|
sessionDir?: string,
|
|
3843
3928
|
storage: SessionStorage = new FileSessionStorage(),
|
|
3929
|
+
managedAgentDir?: string,
|
|
3844
3930
|
): Promise<ResolvedSessionMatch | undefined> {
|
|
3845
|
-
const
|
|
3846
|
-
|
|
3931
|
+
const localSessions = sessionDir
|
|
3932
|
+
? await SessionManager.list(cwd, sessionDir, storage)
|
|
3933
|
+
: await SessionManager.listManagedForResumePickerReadOnly(cwd, managedAgentDir, storage);
|
|
3847
3934
|
const localMatch = localSessions.find(session => sessionMatchesResumeArg(session, sessionArg));
|
|
3848
3935
|
if (localMatch) {
|
|
3849
3936
|
return { session: localMatch, scope: "local" };
|
|
@@ -3853,7 +3940,7 @@ export async function resolveResumableSession(
|
|
|
3853
3940
|
return undefined;
|
|
3854
3941
|
}
|
|
3855
3942
|
|
|
3856
|
-
const globalSessions = await SessionManager.listAll(storage);
|
|
3943
|
+
const globalSessions = await SessionManager.listAll(storage, managedAgentDir);
|
|
3857
3944
|
const globalMatch = globalSessions.find(session => sessionMatchesResumeArg(session, sessionArg));
|
|
3858
3945
|
if (!globalMatch) {
|
|
3859
3946
|
return undefined;
|
|
@@ -3885,6 +3972,7 @@ export class SessionManager {
|
|
|
3885
3972
|
#ensuredOnDisk: boolean = false;
|
|
3886
3973
|
#recoveryHydrationContext: RecoveryHydrationContext | undefined;
|
|
3887
3974
|
#fileEntries: FileEntry[] = [];
|
|
3975
|
+
#pendingStrictAdoption: { canonicalPath: string; identity: ResumeSessionIdentity } | undefined;
|
|
3888
3976
|
#byId: Map<string, SessionEntry> = new Map();
|
|
3889
3977
|
#labelsById: Map<string, string> = new Map();
|
|
3890
3978
|
#leafId: string | null = null;
|
|
@@ -4131,15 +4219,16 @@ export class SessionManager {
|
|
|
4131
4219
|
}
|
|
4132
4220
|
|
|
4133
4221
|
async #hydrateExistingSession(sessionFile: string, entries: FileEntry[], migrationApplied: boolean): Promise<void> {
|
|
4134
|
-
|
|
4135
|
-
|
|
4222
|
+
// Strict inspection creates these entries solely for this hydration path. Adopt that
|
|
4223
|
+
// final, validated representation instead of cloning the complete transcript again.
|
|
4224
|
+
const header = entries[0] as SessionHeader;
|
|
4136
4225
|
this.#sessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
|
|
4137
4226
|
this.#sessionId = header.id;
|
|
4138
4227
|
this.#sessionName = header.title;
|
|
4139
4228
|
this.#titleSource = header.titleSource;
|
|
4140
4229
|
this.#needsFullRewriteOnNextPersist = migrationApplied;
|
|
4141
|
-
await resolveBlobRefsInEntries(
|
|
4142
|
-
this.#fileEntries =
|
|
4230
|
+
await resolveBlobRefsInEntries(entries, this.#blobStore);
|
|
4231
|
+
this.#fileEntries = entries;
|
|
4143
4232
|
this.#resetResidentTextBlobStore();
|
|
4144
4233
|
this.#fileEntries = this.#fileEntries.map(entry =>
|
|
4145
4234
|
prepareEntryForResidentSync(entry, this.#residentBlobStores()),
|
|
@@ -4158,12 +4247,25 @@ export class SessionManager {
|
|
|
4158
4247
|
|
|
4159
4248
|
/** Switch to a different session file (used for resume and branching) */
|
|
4160
4249
|
async setSessionFile(sessionFile: string): Promise<void> {
|
|
4250
|
+
const resolvedSessionFile = this.storage instanceof FileSessionStorage ? path.resolve(sessionFile) : sessionFile;
|
|
4251
|
+
const strictAdoption = this.#pendingStrictAdoption;
|
|
4252
|
+
this.#pendingStrictAdoption = undefined;
|
|
4253
|
+
if (strictAdoption && resolvedSessionFile === strictAdoption.canonicalPath) {
|
|
4254
|
+
const inspected = inspectResumeSessionFile(resolvedSessionFile, this.storage);
|
|
4255
|
+
if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity))
|
|
4256
|
+
throw new Error("Prepared managed session changed before strict adoption.");
|
|
4257
|
+
}
|
|
4161
4258
|
await this.#closePersistWriter();
|
|
4162
4259
|
this.#persistError = undefined;
|
|
4163
4260
|
this.#persistErrorReported = false;
|
|
4164
|
-
this.#sessionFile =
|
|
4261
|
+
this.#sessionFile = resolvedSessionFile;
|
|
4165
4262
|
writeTerminalBreadcrumb(this.cwd, this.#sessionFile);
|
|
4166
4263
|
this.#fileEntries = await loadEntriesFromFile(this.#sessionFile, this.storage);
|
|
4264
|
+
if (strictAdoption) {
|
|
4265
|
+
const inspected = inspectResumeSessionFile(this.#sessionFile, this.storage);
|
|
4266
|
+
if ("kind" in inspected || !sameResumeIdentity(strictAdoption.identity, inspected.identity))
|
|
4267
|
+
throw new Error("Prepared managed session changed during strict adoption.");
|
|
4268
|
+
}
|
|
4167
4269
|
if (this.#fileEntries.length > 0) {
|
|
4168
4270
|
const header = this.#fileEntries.find(e => e.type === "session") as SessionHeader | undefined;
|
|
4169
4271
|
this.#sessionId = header?.id ?? createSessionId();
|
|
@@ -4239,6 +4341,46 @@ export class SessionManager {
|
|
|
4239
4341
|
}
|
|
4240
4342
|
}
|
|
4241
4343
|
|
|
4344
|
+
/**
|
|
4345
|
+
* Exact-delete a session transcript and its artifacts by path WITHOUT requiring
|
|
4346
|
+
* managed logical authorization. This is only for discarding an UNCOMMITTED
|
|
4347
|
+
* successor that a transaction (e.g. handoff) created via `newSession()` but
|
|
4348
|
+
* never durably authorized — such a session has no managed candidate listing
|
|
4349
|
+
* yet, so `dropSession` would refuse it and leak its resident-cache/artifact
|
|
4350
|
+
* root. Bounded to this manager's configured session root and refuses to touch
|
|
4351
|
+
* the active session; tolerates missing files.
|
|
4352
|
+
*/
|
|
4353
|
+
async discardUncommittedSession(sessionPath: string): Promise<void> {
|
|
4354
|
+
// Close the writer FIRST so the canonical checks and the delete run with no
|
|
4355
|
+
// intervening await, eliminating the check/use window in which a parent
|
|
4356
|
+
// directory could be swapped for a symlink between authorization and deletion.
|
|
4357
|
+
await this.#closePersistWriter();
|
|
4358
|
+
// Canonicalize the candidate (and the active file / configured dir) through
|
|
4359
|
+
// their nearest existing ancestor so symlink/reparse components cannot alias
|
|
4360
|
+
// the active session or escape the configured session directory, even when
|
|
4361
|
+
// the successor transcript itself does not yet exist.
|
|
4362
|
+
const canonicalCandidate = canonicalizeThroughExistingAncestor(path.resolve(sessionPath));
|
|
4363
|
+
if (this.#sessionFile) {
|
|
4364
|
+
const canonicalActive = canonicalizeThroughExistingAncestor(path.resolve(this.#sessionFile));
|
|
4365
|
+
if (canonicalActive === canonicalCandidate) {
|
|
4366
|
+
throw new Error("Refusing to discard the active session as uncommitted.");
|
|
4367
|
+
}
|
|
4368
|
+
}
|
|
4369
|
+
// Least-authority containment: only within this manager's configured session
|
|
4370
|
+
// directory (canonical), where newSession() allocates the successor. The
|
|
4371
|
+
// broader managed root (parent of sessionDir) is deliberately NOT accepted.
|
|
4372
|
+
const canonicalSessionDir = canonicalizeThroughExistingAncestor(path.resolve(this.sessionDir));
|
|
4373
|
+
if (!pathIsWithin(canonicalSessionDir, canonicalCandidate)) {
|
|
4374
|
+
throw new Error("Uncommitted session discard is limited to this manager's configured session directory.");
|
|
4375
|
+
}
|
|
4376
|
+
try {
|
|
4377
|
+
await this.storage.deleteSessionWithArtifacts(canonicalCandidate);
|
|
4378
|
+
} catch (err) {
|
|
4379
|
+
if (isEnoent(err)) return;
|
|
4380
|
+
throw err;
|
|
4381
|
+
}
|
|
4382
|
+
}
|
|
4383
|
+
|
|
4242
4384
|
/**
|
|
4243
4385
|
* Fork the current session, creating a new session file with the same entries.
|
|
4244
4386
|
* Returns both the old and new session file paths for artifact copying.
|
|
@@ -4467,17 +4609,14 @@ export class SessionManager {
|
|
|
4467
4609
|
const previousDestination = this.destination;
|
|
4468
4610
|
const previousArtifactDir = previousSessionFile?.slice(0, -6);
|
|
4469
4611
|
|
|
4470
|
-
const nextDestination =
|
|
4471
|
-
|
|
4472
|
-
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
: computeDefaultSessionDir(resolvedCwd, this.storage);
|
|
4479
|
-
return explicitDestination(directory);
|
|
4480
|
-
})();
|
|
4612
|
+
const nextDestination =
|
|
4613
|
+
this.storage instanceof FileSessionStorage
|
|
4614
|
+
? managedDestination(
|
|
4615
|
+
resolvedCwd,
|
|
4616
|
+
this.storage,
|
|
4617
|
+
this.destination.kind === "managed" ? this.destination.securityContext.agentDir : undefined,
|
|
4618
|
+
)
|
|
4619
|
+
: this.destination;
|
|
4481
4620
|
const newSessionDir = nextDestination.directory;
|
|
4482
4621
|
let hadSessionFile = false;
|
|
4483
4622
|
const managedMove = this.destination.kind === "managed" && nextDestination.kind === "managed";
|
|
@@ -5457,6 +5596,17 @@ export class SessionManager {
|
|
|
5457
5596
|
return this.sessionDir;
|
|
5458
5597
|
}
|
|
5459
5598
|
|
|
5599
|
+
/** Lists picker candidates within this manager's captured destination authority. */
|
|
5600
|
+
async listForResumePickerReadOnly(): Promise<SessionInfo[]> {
|
|
5601
|
+
return this.destination.kind === "managed"
|
|
5602
|
+
? await SessionManager.listManagedForResumePickerReadOnly(
|
|
5603
|
+
this.cwd,
|
|
5604
|
+
this.destination.securityContext.agentDir,
|
|
5605
|
+
this.storage,
|
|
5606
|
+
)
|
|
5607
|
+
: await SessionManager.listForResumePickerReadOnly(this.cwd, this.destination.directory, this.storage);
|
|
5608
|
+
}
|
|
5609
|
+
|
|
5460
5610
|
getSessionId(): string {
|
|
5461
5611
|
return this.#sessionId;
|
|
5462
5612
|
}
|
|
@@ -5486,13 +5636,22 @@ export class SessionManager {
|
|
|
5486
5636
|
getManagedLegacyLocalMigrationSource(): ManagedLegacyLocalMigrationSource | null {
|
|
5487
5637
|
if (this.destination.kind !== "managed" || !this.#sessionFile || this.#adoptedArtifactManager) return null;
|
|
5488
5638
|
const store = this.#managedTranscriptStore();
|
|
5639
|
+
const legacyArtifactsRoot = path.basename(this.#sessionFile.slice(0, -6));
|
|
5640
|
+
const legacyLocalRoot = path.posix.join(legacyArtifactsRoot, "local");
|
|
5489
5641
|
return {
|
|
5490
5642
|
capture: async () => {
|
|
5491
5643
|
let snapshot: native.NativeDirectoryTreeSnapshot;
|
|
5492
5644
|
try {
|
|
5493
|
-
snapshot = store.captureTree(
|
|
5645
|
+
snapshot = store.captureTree(legacyLocalRoot);
|
|
5494
5646
|
} catch (error) {
|
|
5495
5647
|
if (error instanceof Error && error.message === "not_found") return null;
|
|
5648
|
+
if (error instanceof Error && error.message === "reparse_point") {
|
|
5649
|
+
try {
|
|
5650
|
+
store.captureTree(legacyArtifactsRoot);
|
|
5651
|
+
} catch (artifactsError) {
|
|
5652
|
+
if (artifactsError instanceof Error && artifactsError.message === "not_found") return null;
|
|
5653
|
+
}
|
|
5654
|
+
}
|
|
5496
5655
|
throw error;
|
|
5497
5656
|
}
|
|
5498
5657
|
let totalBytes = 0;
|
|
@@ -5500,7 +5659,7 @@ export class SessionManager {
|
|
|
5500
5659
|
if (totalBytes > 64 * 1024 * 1024) throw new Error("Legacy local:// migration exceeds the safe size limit");
|
|
5501
5660
|
const entries = snapshot.entries.map(entry => {
|
|
5502
5661
|
if (entry.kind === "directory") return { relativePath: entry.relativePath, kind: "directory" as const };
|
|
5503
|
-
const captured = store.readExpected(path.posix.join(
|
|
5662
|
+
const captured = store.readExpected(path.posix.join(legacyLocalRoot, entry.relativePath));
|
|
5504
5663
|
if (
|
|
5505
5664
|
!captured ||
|
|
5506
5665
|
captured.identity.dev.toString() !== entry.dev ||
|
|
@@ -5516,12 +5675,12 @@ export class SessionManager {
|
|
|
5516
5675
|
sha256: entry.sha256,
|
|
5517
5676
|
};
|
|
5518
5677
|
});
|
|
5519
|
-
const verified = store.captureTree(
|
|
5678
|
+
const verified = store.captureTree(legacyLocalRoot);
|
|
5520
5679
|
if (JSON.stringify(verified) !== JSON.stringify(snapshot))
|
|
5521
5680
|
throw new Error("Legacy local:// migration source changed during capture");
|
|
5522
5681
|
return { snapshot, entries };
|
|
5523
5682
|
},
|
|
5524
|
-
retire: snapshot => store.removeTreeExpected(
|
|
5683
|
+
retire: snapshot => store.removeTreeExpected(legacyLocalRoot, snapshot),
|
|
5525
5684
|
};
|
|
5526
5685
|
}
|
|
5527
5686
|
|
|
@@ -7014,6 +7173,12 @@ export class SessionManager {
|
|
|
7014
7173
|
authority instanceof ManagedSessionDescendantStore
|
|
7015
7174
|
? authority.retainAuthority()
|
|
7016
7175
|
: retainManagedDirectoryAuthority(rootAuthority, directory);
|
|
7176
|
+
const policy =
|
|
7177
|
+
authority instanceof ManagedSessionDescendantStore
|
|
7178
|
+
? authority.securityPolicy
|
|
7179
|
+
: process.platform === "win32"
|
|
7180
|
+
? "windows-existing-verify-first"
|
|
7181
|
+
: "default";
|
|
7017
7182
|
const securityContext = createManagedSessionSecurityContext({
|
|
7018
7183
|
agentDir: rootAuthority.canonicalPath,
|
|
7019
7184
|
sessionsRoot: rootAuthority.canonicalPath,
|
|
@@ -7021,6 +7186,7 @@ export class SessionManager {
|
|
|
7021
7186
|
rootAuthority,
|
|
7022
7187
|
retainedAuthority,
|
|
7023
7188
|
});
|
|
7189
|
+
managedSecurityPolicies.set(securityContext, policy);
|
|
7024
7190
|
const destination = Object.freeze({ kind: "managed" as const, directory, securityContext });
|
|
7025
7191
|
trustedSessionDestinations.add(destination);
|
|
7026
7192
|
return destination;
|
|
@@ -7049,20 +7215,59 @@ export class SessionManager {
|
|
|
7049
7215
|
return manager;
|
|
7050
7216
|
}
|
|
7051
7217
|
|
|
7218
|
+
/** Prepare a selected candidate using this manager's captured managed destination authority. */
|
|
7219
|
+
async prepareManagedCandidateForWrite(
|
|
7220
|
+
filePath: string,
|
|
7221
|
+
migrationPolicy: SessionDirectoryMigrationPolicy,
|
|
7222
|
+
expectedIdentity?: ResumeSessionIdentity,
|
|
7223
|
+
): Promise<string> {
|
|
7224
|
+
return SessionManager.prepareManagedCandidateForWrite(
|
|
7225
|
+
filePath,
|
|
7226
|
+
migrationPolicy,
|
|
7227
|
+
this.destination,
|
|
7228
|
+
expectedIdentity,
|
|
7229
|
+
);
|
|
7230
|
+
}
|
|
7231
|
+
|
|
7232
|
+
/** Prepare a managed candidate and retain its exact post-preparation identity for the next adoption. */
|
|
7233
|
+
async prepareManagedCandidateForStrictAdoption(
|
|
7234
|
+
filePath: string,
|
|
7235
|
+
migrationPolicy: SessionDirectoryMigrationPolicy,
|
|
7236
|
+
expectedIdentity: ResumeSessionIdentity,
|
|
7237
|
+
): Promise<string> {
|
|
7238
|
+
const preparedPath = await this.prepareManagedCandidateForWrite(filePath, migrationPolicy, expectedIdentity);
|
|
7239
|
+
const inspected = inspectResumeSessionFile(preparedPath, this.storage);
|
|
7240
|
+
if ("kind" in inspected) throw new Error(`Could not inspect prepared managed session: ${inspected.reason}`);
|
|
7241
|
+
this.#pendingStrictAdoption = { canonicalPath: inspected.identity.canonicalPath, identity: inspected.identity };
|
|
7242
|
+
return preparedPath;
|
|
7243
|
+
}
|
|
7244
|
+
|
|
7052
7245
|
/** Resolve a default-managed candidate through binding validation and copy-retain migration before mutation. */
|
|
7053
7246
|
static async prepareManagedCandidateForWrite(
|
|
7054
7247
|
filePath: string,
|
|
7055
7248
|
migrationPolicy: SessionDirectoryMigrationPolicy,
|
|
7249
|
+
destination: SessionDestination,
|
|
7250
|
+
expectedIdentity?: ResumeSessionIdentity,
|
|
7056
7251
|
): Promise<string> {
|
|
7252
|
+
if (!trustedSessionDestinations.has(destination) || destination.kind !== "managed")
|
|
7253
|
+
throw new Error("Managed candidate preparation requires a trusted managed destination authority");
|
|
7057
7254
|
const storage = new FileSessionStorage();
|
|
7255
|
+
const managedDestinationStore = managedStoreFromContext(destination.securityContext, destination.directory);
|
|
7256
|
+
const assertManagedDestinationBound = (): void => {
|
|
7257
|
+
assertManagedDirectoryRoot(destination.securityContext.rootAuthority);
|
|
7258
|
+
managedDestinationStore.assertBound();
|
|
7259
|
+
};
|
|
7260
|
+
assertManagedDestinationBound();
|
|
7058
7261
|
const inspected = inspectResumeSessionFile(filePath, storage);
|
|
7059
7262
|
if ("kind" in inspected) {
|
|
7060
7263
|
if (inspected.reason === "missing") return filePath;
|
|
7061
7264
|
throw new Error(`Could not inspect managed session: ${inspected.reason}`);
|
|
7062
7265
|
}
|
|
7266
|
+
if (expectedIdentity && !sameResumeIdentity(expectedIdentity, inspected.identity))
|
|
7267
|
+
throw new Error("Managed session changed before migration authority was adopted.");
|
|
7063
7268
|
const header = inspected.entries.find(entry => entry.type === "session") as SessionHeader | undefined;
|
|
7064
7269
|
if (!header?.cwd) return filePath;
|
|
7065
|
-
const sessionsRoot =
|
|
7270
|
+
const sessionsRoot = destination.securityContext.sessionsRoot;
|
|
7066
7271
|
const resolvedPath = path.resolve(filePath);
|
|
7067
7272
|
const relativeToManagedRoot = path.relative(sessionsRoot, resolvedPath);
|
|
7068
7273
|
const isManagedPath =
|
|
@@ -7070,14 +7275,17 @@ export class SessionManager {
|
|
|
7070
7275
|
(!relativeToManagedRoot.startsWith("..") && !path.isAbsolute(relativeToManagedRoot));
|
|
7071
7276
|
|
|
7072
7277
|
if (!isManagedPath) return filePath;
|
|
7278
|
+
assertManagedDestinationBound();
|
|
7073
7279
|
const resolved = resolveManagedScope({
|
|
7074
7280
|
cwd: header.cwd,
|
|
7075
|
-
agentDir:
|
|
7281
|
+
agentDir: destination.securityContext.agentDir,
|
|
7076
7282
|
sessionsRoot,
|
|
7077
7283
|
});
|
|
7078
7284
|
if (resolved.kind === "error") throw new Error(`Could not resolve managed session scope: ${resolved.message}`);
|
|
7079
7285
|
|
|
7286
|
+
assertManagedDestinationBound();
|
|
7080
7287
|
const listing = listManagedCandidates(resolved.scope);
|
|
7288
|
+
assertManagedDestinationBound();
|
|
7081
7289
|
if (listing.kind !== "complete") throw new Error(`Managed session scan failed: ${listing.message}`);
|
|
7082
7290
|
|
|
7083
7291
|
const candidate = listing.owned.find(item => path.resolve(item.path) === resolvedPath);
|
|
@@ -7094,7 +7302,24 @@ export class SessionManager {
|
|
|
7094
7302
|
candidate.identity.sha256 !== inspected.identity.sha256
|
|
7095
7303
|
)
|
|
7096
7304
|
throw new Error("Managed session changed before migration authority was adopted.");
|
|
7097
|
-
|
|
7305
|
+
assertManagedDestinationBound();
|
|
7306
|
+
const authority: ManagedCandidateWriteAuthority = {
|
|
7307
|
+
rootAuthority: destination.securityContext.rootAuthority,
|
|
7308
|
+
...(path.resolve(destination.directory) === path.resolve(resolved.scope.directoryPath)
|
|
7309
|
+
? {
|
|
7310
|
+
retainedAuthority: destination.securityContext.retainedAuthority,
|
|
7311
|
+
retainedDirectory: destination.directory,
|
|
7312
|
+
}
|
|
7313
|
+
: {}),
|
|
7314
|
+
};
|
|
7315
|
+
const opened = await openManagedCandidateForWrite(
|
|
7316
|
+
resolved.scope,
|
|
7317
|
+
candidate,
|
|
7318
|
+
expectedIdentity ?? inspected.identity,
|
|
7319
|
+
migrationPolicy,
|
|
7320
|
+
authority,
|
|
7321
|
+
);
|
|
7322
|
+
assertManagedDestinationBound();
|
|
7098
7323
|
if (opened.kind === "error") {
|
|
7099
7324
|
if (opened.code === "legacy_migration_disabled") throw new SessionMigrationPolicyError();
|
|
7100
7325
|
throw new Error(`Could not open managed session: ${opened.message}`);
|
|
@@ -7116,7 +7341,7 @@ export class SessionManager {
|
|
|
7116
7341
|
const destination = destinationFor(cwd, destinationInput, storage);
|
|
7117
7342
|
const managedSourcePath =
|
|
7118
7343
|
destination.kind === "managed" && storage instanceof FileSessionStorage
|
|
7119
|
-
? await SessionManager.prepareManagedCandidateForWrite(sourcePath, migrationPolicy)
|
|
7344
|
+
? await SessionManager.prepareManagedCandidateForWrite(sourcePath, migrationPolicy, destination)
|
|
7120
7345
|
: sourcePath;
|
|
7121
7346
|
const dir = destination.directory;
|
|
7122
7347
|
const manager = new SessionManager(cwd, dir, true, storage, destination);
|
|
@@ -7180,10 +7405,9 @@ export class SessionManager {
|
|
|
7180
7405
|
return manager;
|
|
7181
7406
|
}
|
|
7182
7407
|
|
|
7183
|
-
const
|
|
7184
|
-
const inspected = inspectResumeSessionFile(managedPath, storage);
|
|
7408
|
+
const inspected = inspectResumeSessionFile(filePath, storage);
|
|
7185
7409
|
if ("kind" in inspected) {
|
|
7186
|
-
if (inspected.reason === "missing"
|
|
7410
|
+
if (inspected.reason === "missing") {
|
|
7187
7411
|
const manager = new SessionManager(getProjectDir(), destination.directory, true, storage, destination);
|
|
7188
7412
|
await manager.#initSessionFile(filePath);
|
|
7189
7413
|
return manager;
|
|
@@ -7251,46 +7475,47 @@ export class SessionManager {
|
|
|
7251
7475
|
return manager;
|
|
7252
7476
|
}
|
|
7253
7477
|
/**
|
|
7254
|
-
* List sessions for the resume picker without recovery or other
|
|
7478
|
+
* List default-managed sessions for the resume picker without recovery or other
|
|
7479
|
+
* maintenance writes. This is the only picker inventory that includes legacy
|
|
7480
|
+
* sibling directories.
|
|
7481
|
+
*/
|
|
7482
|
+
static async listManagedForResumePickerReadOnly(
|
|
7483
|
+
cwd: string,
|
|
7484
|
+
managedAgentDir?: string,
|
|
7485
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
7486
|
+
): Promise<SessionInfo[]> {
|
|
7487
|
+
if (!(storage instanceof FileSessionStorage)) return [];
|
|
7488
|
+
const sessionsRoot = getSessionsDir(managedAgentDir);
|
|
7489
|
+
const resolved = resolveManagedScope({
|
|
7490
|
+
cwd,
|
|
7491
|
+
agentDir: managedAgentDir ?? path.resolve(sessionsRoot, ".."),
|
|
7492
|
+
sessionsRoot,
|
|
7493
|
+
});
|
|
7494
|
+
if (resolved.kind === "error") return [];
|
|
7495
|
+
const listing = listManagedCandidates(resolved.scope);
|
|
7496
|
+
if (listing.kind === "error") return [];
|
|
7497
|
+
return await collectSessionsFromFiles(
|
|
7498
|
+
listing.owned.map(candidate => candidate.path),
|
|
7499
|
+
storage,
|
|
7500
|
+
);
|
|
7501
|
+
}
|
|
7502
|
+
|
|
7503
|
+
/**
|
|
7504
|
+
* List sessions from an explicitly supplied picker directory without recovery
|
|
7505
|
+
* or other maintenance writes. Unlike managed inventory, this never scans
|
|
7506
|
+
* legacy sibling directories.
|
|
7255
7507
|
*/
|
|
7256
7508
|
static async listForResumePickerReadOnly(
|
|
7257
7509
|
cwd: string,
|
|
7258
7510
|
sessionDir?: string,
|
|
7259
7511
|
storage: SessionStorage = new FileSessionStorage(),
|
|
7260
7512
|
): Promise<SessionInfo[]> {
|
|
7261
|
-
if (!sessionDir
|
|
7262
|
-
if (sessionDir && !resolveManagedSessionRoot(sessionDir, cwd)) {
|
|
7263
|
-
try {
|
|
7264
|
-
return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
|
|
7265
|
-
} catch {
|
|
7266
|
-
return [];
|
|
7267
|
-
}
|
|
7268
|
-
}
|
|
7269
|
-
const sessionsRoot = sessionDir
|
|
7270
|
-
? (resolveManagedSessionRoot(sessionDir, cwd) ?? getSessionsDir())
|
|
7271
|
-
: getSessionsDir();
|
|
7272
|
-
const resolved = resolveManagedScope({ cwd, agentDir: path.resolve(sessionsRoot, ".."), sessionsRoot });
|
|
7273
|
-
if (resolved.kind === "error") return [];
|
|
7274
|
-
if (storage instanceof FileSessionStorage) {
|
|
7275
|
-
const listing = listManagedCandidates(resolved.scope);
|
|
7276
|
-
if (listing.kind === "error") return [];
|
|
7277
|
-
return await collectSessionsFromFiles(
|
|
7278
|
-
listing.owned.map(candidate => candidate.path),
|
|
7279
|
-
storage,
|
|
7280
|
-
);
|
|
7281
|
-
}
|
|
7282
|
-
const legacyDirectory = `--${resolved.scope.canonicalCwd.replace(/^[/\\]/, "").replace(/[/\\:]/g, "-")}--`;
|
|
7283
|
-
const files = new Set<string>();
|
|
7513
|
+
if (!sessionDir) return await SessionManager.listManagedForResumePickerReadOnly(cwd, undefined, storage);
|
|
7284
7514
|
try {
|
|
7285
|
-
|
|
7286
|
-
for (const file of storage.listFilesSync(directory, "*.jsonl")) files.add(file);
|
|
7287
|
-
}
|
|
7515
|
+
return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
|
|
7288
7516
|
} catch {
|
|
7289
7517
|
return [];
|
|
7290
7518
|
}
|
|
7291
|
-
return (await collectSessionsFromFiles([...files], storage)).filter(
|
|
7292
|
-
session => resolveEquivalentPath(session.cwd) === resolved.scope.canonicalCwd,
|
|
7293
|
-
);
|
|
7294
7519
|
}
|
|
7295
7520
|
|
|
7296
7521
|
/** Tombstone and exact-delete a default-managed candidate. */
|
|
@@ -7592,36 +7817,39 @@ export class SessionManager {
|
|
|
7592
7817
|
let sessionPath = identity.canonicalPath;
|
|
7593
7818
|
if (destination.kind === "managed" && storage instanceof FileSessionStorage) {
|
|
7594
7819
|
try {
|
|
7595
|
-
sessionPath = await SessionManager.prepareManagedCandidateForWrite(
|
|
7820
|
+
sessionPath = await SessionManager.prepareManagedCandidateForWrite(
|
|
7821
|
+
sessionPath,
|
|
7822
|
+
migrationPolicy,
|
|
7823
|
+
destination,
|
|
7824
|
+
identity,
|
|
7825
|
+
);
|
|
7596
7826
|
} catch (error) {
|
|
7597
7827
|
if (error instanceof SessionMigrationPolicyError)
|
|
7598
7828
|
return { kind: "error", reason: "legacy_migration_disabled" };
|
|
7829
|
+
if (
|
|
7830
|
+
error instanceof Error &&
|
|
7831
|
+
(error.message.includes("source_changed") || error.message.includes("changed before migration"))
|
|
7832
|
+
)
|
|
7833
|
+
return { kind: "error", reason: "identity-mismatch" };
|
|
7599
7834
|
throw error;
|
|
7600
7835
|
}
|
|
7601
7836
|
}
|
|
7602
7837
|
const inspected = inspectResumeSessionFile(sessionPath, storage);
|
|
7603
7838
|
if ("kind" in inspected) return inspected;
|
|
7604
|
-
if (
|
|
7605
|
-
(sessionPath === identity.canonicalPath && !sameResumeIdentity(identity, inspected.identity)) ||
|
|
7606
|
-
(sessionPath !== identity.canonicalPath &&
|
|
7607
|
-
(identity.sessionId !== inspected.identity.sessionId || identity.sha256 !== inspected.identity.sha256))
|
|
7608
|
-
) {
|
|
7839
|
+
if (sessionPath === identity.canonicalPath && !sameResumeIdentity(identity, inspected.identity)) {
|
|
7609
7840
|
return { kind: "error", reason: "identity-mismatch" };
|
|
7610
7841
|
}
|
|
7842
|
+
|
|
7611
7843
|
const entries = inspected.entries;
|
|
7612
7844
|
const header = entries[0] as SessionHeader;
|
|
7613
7845
|
const dir = destination.directory;
|
|
7614
7846
|
const manager = new SessionManager(header.cwd || getProjectDir(), dir, true, storage, destination);
|
|
7615
7847
|
await manager.#hydrateExistingSession(sessionPath, entries, inspected.migrationApplied);
|
|
7616
|
-
const ownershipInspection =
|
|
7617
|
-
if (
|
|
7848
|
+
const ownershipInspection = revalidateResumeSessionIdentity(sessionPath, storage, inspected.identity);
|
|
7849
|
+
if (ownershipInspection.kind === "error") {
|
|
7618
7850
|
await manager.close();
|
|
7619
7851
|
return ownershipInspection;
|
|
7620
7852
|
}
|
|
7621
|
-
if (!sameResumeIdentity(inspected.identity, ownershipInspection.identity)) {
|
|
7622
|
-
await manager.close();
|
|
7623
|
-
return { kind: "error", reason: "identity-mismatch" };
|
|
7624
|
-
}
|
|
7625
7853
|
try {
|
|
7626
7854
|
await manager.#sanitizeLoadedOpenAIResponsesReplayMetadataAndPersist();
|
|
7627
7855
|
writeTerminalBreadcrumb(manager.cwd, sessionPath);
|
|
@@ -7665,9 +7893,20 @@ export class SessionManager {
|
|
|
7665
7893
|
const managedCandidates =
|
|
7666
7894
|
destination.kind === "explicit"
|
|
7667
7895
|
? []
|
|
7668
|
-
: await SessionManager.
|
|
7896
|
+
: await SessionManager.listManagedForResumePickerReadOnly(
|
|
7897
|
+
cwd,
|
|
7898
|
+
destination.securityContext.agentDir,
|
|
7899
|
+
storage,
|
|
7900
|
+
);
|
|
7901
|
+
|
|
7669
7902
|
const terminalSession = await readTerminalBreadcrumb(cwd);
|
|
7670
|
-
|
|
7903
|
+
const terminalSessionIsInExplicitRoot = (() => {
|
|
7904
|
+
if (destination.kind === "managed" || !terminalSession) return destination.kind === "managed";
|
|
7905
|
+
const canonicalRoot = canonicalizeTrustedPath(destination.directory);
|
|
7906
|
+
const canonicalSessionPath = canonicalizeTrustedPath(terminalSession);
|
|
7907
|
+
return pathIsWithin(canonicalRoot, canonicalSessionPath);
|
|
7908
|
+
})();
|
|
7909
|
+
if (terminalSession && terminalSessionIsInExplicitRoot) {
|
|
7671
7910
|
const opened = await openSelectedStrictly(terminalSession);
|
|
7672
7911
|
if (opened) return opened;
|
|
7673
7912
|
}
|
|
@@ -7705,7 +7944,7 @@ export class SessionManager {
|
|
|
7705
7944
|
sessionDir?: string,
|
|
7706
7945
|
storage: SessionStorage = new FileSessionStorage(),
|
|
7707
7946
|
): Promise<SessionInfo[]> {
|
|
7708
|
-
if (!sessionDir) return await SessionManager.
|
|
7947
|
+
if (!sessionDir) return await SessionManager.listManagedForResumePickerReadOnly(cwd, undefined, storage);
|
|
7709
7948
|
try {
|
|
7710
7949
|
await recoverOrphanedBackups(sessionDir, storage);
|
|
7711
7950
|
return await collectSessionsFromFiles(storage.listFilesSync(sessionDir, "*.jsonl"), storage);
|
|
@@ -7717,9 +7956,12 @@ export class SessionManager {
|
|
|
7717
7956
|
/**
|
|
7718
7957
|
* List all sessions across all project directories.
|
|
7719
7958
|
*/
|
|
7720
|
-
static async listAll(
|
|
7959
|
+
static async listAll(
|
|
7960
|
+
storage: SessionStorage = new FileSessionStorage(),
|
|
7961
|
+
managedAgentDir?: string,
|
|
7962
|
+
): Promise<SessionInfo[]> {
|
|
7721
7963
|
if (!(storage instanceof FileSessionStorage)) return [];
|
|
7722
|
-
const sessionsRoot =
|
|
7964
|
+
const sessionsRoot = getSessionsDir(managedAgentDir);
|
|
7723
7965
|
try {
|
|
7724
7966
|
const directories = await fs.promises.readdir(sessionsRoot, { withFileTypes: true });
|
|
7725
7967
|
const seedFiles = directories
|
|
@@ -7755,6 +7997,19 @@ export class SessionManager {
|
|
|
7755
7997
|
return [];
|
|
7756
7998
|
}
|
|
7757
7999
|
}
|
|
8000
|
+
/**
|
|
8001
|
+
* Strict inventory bound to this manager's captured session authority. Managed
|
|
8002
|
+
* managers include authorized legacy and current candidates; explicit managers
|
|
8003
|
+
* authorize only their exact directory.
|
|
8004
|
+
*/
|
|
8005
|
+
inventorySessionsStrict(): StrictInventoryResult {
|
|
8006
|
+
return SessionManager.inventorySessionsStrict(this.cwd, {
|
|
8007
|
+
sessionDir: this.destination.kind === "explicit" ? this.destination.directory : undefined,
|
|
8008
|
+
storage: this.storage,
|
|
8009
|
+
destination: this.destination,
|
|
8010
|
+
});
|
|
8011
|
+
}
|
|
8012
|
+
|
|
7758
8013
|
/**
|
|
7759
8014
|
* Strict raw scoped inventory for ACP authorization. Enumerates the scoped
|
|
7760
8015
|
* session directory without suppressing any root/scan/lstat/read/parse/stat/
|
|
@@ -7764,10 +8019,55 @@ export class SessionManager {
|
|
|
7764
8019
|
*/
|
|
7765
8020
|
static inventorySessionsStrict(
|
|
7766
8021
|
cwd: string,
|
|
7767
|
-
options?: { sessionDir?: string; storage?: SessionStorage },
|
|
8022
|
+
options?: { sessionDir?: string; storage?: SessionStorage; destination?: SessionDestination },
|
|
7768
8023
|
): StrictInventoryResult {
|
|
7769
8024
|
const storage = options?.storage ?? new FileSessionStorage();
|
|
7770
|
-
const
|
|
8025
|
+
const destination = options?.destination;
|
|
8026
|
+
const dir =
|
|
8027
|
+
options?.sessionDir ?? destination?.directory ?? SessionManager.getDefaultSessionDir(cwd, undefined, storage);
|
|
8028
|
+
if (destination?.kind === "managed") {
|
|
8029
|
+
if (!trustedSessionDestinations.has(destination) || !(storage instanceof FileSessionStorage)) {
|
|
8030
|
+
return {
|
|
8031
|
+
kind: "failure",
|
|
8032
|
+
failures: [{ kind: "scan", message: "Managed strict inventory authority is unavailable", path: dir }],
|
|
8033
|
+
};
|
|
8034
|
+
}
|
|
8035
|
+
try {
|
|
8036
|
+
const store = managedStoreFromContext(destination.securityContext, destination.directory);
|
|
8037
|
+
assertManagedDirectoryRoot(destination.securityContext.rootAuthority);
|
|
8038
|
+
store.assertBound();
|
|
8039
|
+
const resolved = resolveManagedScope({
|
|
8040
|
+
cwd,
|
|
8041
|
+
agentDir: destination.securityContext.agentDir,
|
|
8042
|
+
sessionsRoot: destination.securityContext.sessionsRoot,
|
|
8043
|
+
});
|
|
8044
|
+
if (resolved.kind === "error")
|
|
8045
|
+
return { kind: "failure", failures: [{ kind: "root", message: resolved.message, path: dir }] };
|
|
8046
|
+
const listing = listManagedCandidates(resolved.scope);
|
|
8047
|
+
if (listing.kind === "error")
|
|
8048
|
+
return { kind: "failure", failures: [{ kind: "scan", message: listing.message, path: dir }] };
|
|
8049
|
+
const failures: StrictInventoryFailure[] = [];
|
|
8050
|
+
const candidates: StrictInventoryCandidate[] = [];
|
|
8051
|
+
for (const managedCandidate of listing.owned) {
|
|
8052
|
+
const candidate = inventoryReadCandidate(
|
|
8053
|
+
storage,
|
|
8054
|
+
managedCandidate.path,
|
|
8055
|
+
cwd,
|
|
8056
|
+
path.dirname(managedCandidate.path),
|
|
8057
|
+
);
|
|
8058
|
+
if ("failures" in candidate) failures.push(...candidate.failures);
|
|
8059
|
+
else candidates.push(candidate.candidate);
|
|
8060
|
+
}
|
|
8061
|
+
assertManagedDirectoryRoot(destination.securityContext.rootAuthority);
|
|
8062
|
+
store.assertBound();
|
|
8063
|
+
return failures.length > 0 ? { kind: "failure", failures } : { kind: "complete", candidates };
|
|
8064
|
+
} catch {
|
|
8065
|
+
return {
|
|
8066
|
+
kind: "failure",
|
|
8067
|
+
failures: [{ kind: "root", message: "Managed strict inventory authority changed", path: dir }],
|
|
8068
|
+
};
|
|
8069
|
+
}
|
|
8070
|
+
}
|
|
7771
8071
|
if (!storage.listFilesStrictSync) {
|
|
7772
8072
|
return {
|
|
7773
8073
|
kind: "failure",
|