@gajae-code/coding-agent 0.12.1 → 0.12.4
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 +39 -0
- package/dist/types/config/settings-schema.d.ts +2 -2
- package/dist/types/gjc-runtime/ralplan-review-conflicts.d.ts +114 -0
- package/dist/types/gjc-runtime/ralplan-runtime.d.ts +5 -3
- package/dist/types/gjc-runtime/ultragoal-evidence.d.ts +5 -1
- package/dist/types/gjc-runtime/ultragoal-runtime.d.ts +8 -2
- package/dist/types/modes/components/irc-sidebar.d.ts +16 -0
- package/dist/types/modes/controllers/command-controller.d.ts +1 -1
- package/dist/types/modes/controllers/event-controller.d.ts +1 -1
- package/dist/types/modes/controllers/extension-ui-controller.d.ts +2 -1
- package/dist/types/modes/controllers/selector-controller.d.ts +1 -1
- package/dist/types/modes/interactive-mode.d.ts +9 -0
- package/dist/types/modes/theme/theme.d.ts +3 -1
- package/dist/types/modes/types.d.ts +32 -0
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +2 -1
- package/dist/types/sdk/bus/telegram-daemon-contract.d.ts +3 -2
- package/dist/types/session/agent-session.d.ts +2 -0
- package/dist/types/session/internal/managed-session-storage.d.ts +22 -3
- package/dist/types/task/index.d.ts +4 -0
- package/dist/types/task/output-manager.d.ts +1 -0
- package/dist/types/task/receipt.d.ts +2 -0
- package/dist/types/tools/index.d.ts +2 -0
- package/package.json +8 -8
- package/scripts/dogfood-ralplan-review-conflicts.ts +272 -0
- package/scripts/generate-sdk-operation-inventory.ts +2 -0
- package/scripts/run-acp-conformance.ts +5 -0
- package/src/async/job-manager.ts +22 -20
- package/src/config/provider-ranking.ts +1 -0
- package/src/config/settings-schema.ts +2 -2
- package/src/debug/index.ts +42 -17
- package/src/defaults/gjc/skills/ralplan/SKILL.md +9 -7
- package/src/defaults/gjc/skills/ultragoal/SKILL.md +22 -5
- package/src/defaults/gjc/skills/ultragoal/validation-batch-contracts.md +2 -1
- package/src/gjc-runtime/launch-worktree.ts +103 -1
- package/src/gjc-runtime/ledger-event-renderer.ts +1 -0
- package/src/gjc-runtime/ralplan-review-conflicts.ts +472 -0
- package/src/gjc-runtime/ralplan-runtime.ts +80 -10
- package/src/gjc-runtime/ultragoal-evidence.ts +110 -49
- package/src/gjc-runtime/ultragoal-runtime.ts +467 -22
- package/src/gjc-runtime/workflow-manifest.generated.json +29 -0
- package/src/gjc-runtime/workflow-manifest.ts +17 -2
- package/src/internal-urls/docs-index.generated.ts +2 -2
- package/src/modes/DESIGN.md +53 -0
- package/src/modes/components/irc-sidebar.ts +30 -1
- package/src/modes/components/tool-status-header.ts +0 -1
- package/src/modes/controllers/command-controller.ts +38 -34
- package/src/modes/controllers/event-controller.ts +27 -16
- package/src/modes/controllers/extension-ui-controller.ts +186 -34
- package/src/modes/controllers/selector-controller.ts +115 -22
- package/src/modes/interactive-mode.ts +189 -62
- package/src/modes/jobs-observer.ts +1 -1
- package/src/modes/theme/theme.ts +11 -9
- package/src/modes/types.ts +88 -0
- package/src/sdk/broker/lifecycle.ts +18 -13
- package/src/sdk/bus/chat-daemon-control.ts +4 -3
- package/src/sdk/bus/telegram-daemon-contract.ts +3 -2
- package/src/sdk/protocol/operation-inventory.generated.json +11 -0
- package/src/sdk/session.ts +1 -0
- package/src/session/agent-session.ts +22 -1
- package/src/session/internal/managed-session-scope.ts +158 -107
- package/src/session/internal/managed-session-storage.ts +445 -57
- package/src/session/session-storage.ts +29 -44
- package/src/task/index.ts +257 -31
- package/src/task/output-manager.ts +24 -14
- package/src/task/receipt.ts +11 -1
- package/src/tools/index.ts +2 -0
|
@@ -454,7 +454,7 @@ function secureOwnerOnlyFileDescriptor(
|
|
|
454
454
|
): void {
|
|
455
455
|
if (securityContext && !isValidManagedSecurityContext(securityContext))
|
|
456
456
|
throw new Error("Invalid managed session security context");
|
|
457
|
-
if (process.platform !== "linux"
|
|
457
|
+
if (process.platform !== "linux" && process.platform !== "win32") {
|
|
458
458
|
if (operation === "apply") {
|
|
459
459
|
const applied = validateNativeSecurityResult(
|
|
460
460
|
native.applyOwnerOnlyPathSecurity(pathname, "file"),
|
|
@@ -471,7 +471,7 @@ function secureOwnerOnlyFileDescriptor(
|
|
|
471
471
|
if (!verified.ok) throw new Error(`Owner-only security rejected ${pathname}: ${verified.code}`);
|
|
472
472
|
return;
|
|
473
473
|
}
|
|
474
|
-
if (!pathIsWithin(securityContext.sessionDir, pathname))
|
|
474
|
+
if (securityContext && !pathIsWithin(securityContext.sessionDir, pathname))
|
|
475
475
|
throw new Error(`Managed writer escaped its session directory: ${pathname}`);
|
|
476
476
|
const result = validateNativeSecurityResult(
|
|
477
477
|
operation === "apply"
|
|
@@ -908,6 +908,28 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
908
908
|
}
|
|
909
909
|
|
|
910
910
|
const parentIdentity = this.#directoryIdentity(path.dirname(transcriptPath));
|
|
911
|
+
const durablyRecordArtifactPhase = (): void => {
|
|
912
|
+
if (process.platform === "win32") return;
|
|
913
|
+
let descriptor: number | undefined;
|
|
914
|
+
try {
|
|
915
|
+
descriptor = fs.openSync(
|
|
916
|
+
path.dirname(transcriptPath),
|
|
917
|
+
fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
|
|
918
|
+
);
|
|
919
|
+
const durableParent = fs.fstatSync(descriptor, { bigint: true });
|
|
920
|
+
if (
|
|
921
|
+
!durableParent.isDirectory() ||
|
|
922
|
+
durableParent.dev !== parentIdentity.dev ||
|
|
923
|
+
durableParent.ino !== parentIdentity.ino
|
|
924
|
+
)
|
|
925
|
+
throw new Error("parent_changed");
|
|
926
|
+
fs.fsyncSync(descriptor);
|
|
927
|
+
} catch (error) {
|
|
928
|
+
throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
|
|
929
|
+
} finally {
|
|
930
|
+
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
931
|
+
}
|
|
932
|
+
};
|
|
911
933
|
if (detachedArtifactsPath) {
|
|
912
934
|
if (
|
|
913
935
|
!expectedArtifactsIdentity ||
|
|
@@ -938,6 +960,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
938
960
|
"artifacts",
|
|
939
961
|
"Native artifact removal returned an unauthorized root",
|
|
940
962
|
);
|
|
963
|
+
durablyRecordArtifactPhase();
|
|
941
964
|
return {
|
|
942
965
|
kind: "cleanup_pending",
|
|
943
966
|
phase: "artifacts",
|
|
@@ -978,6 +1001,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
978
1001
|
"artifacts",
|
|
979
1002
|
"Authorized artifact removal root remains after restart",
|
|
980
1003
|
);
|
|
1004
|
+
durablyRecordArtifactPhase();
|
|
981
1005
|
return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
|
|
982
1006
|
}
|
|
983
1007
|
|
|
@@ -1022,27 +1046,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1022
1046
|
`Exact artifact detach rejected: ${detach.ok ? "missing_path" : detach.code}`,
|
|
1023
1047
|
);
|
|
1024
1048
|
}
|
|
1025
|
-
if (!detach.ok
|
|
1026
|
-
let descriptor: number | undefined;
|
|
1027
|
-
try {
|
|
1028
|
-
descriptor = fs.openSync(
|
|
1029
|
-
path.dirname(transcriptPath),
|
|
1030
|
-
fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
|
|
1031
|
-
);
|
|
1032
|
-
const durableParent = fs.fstatSync(descriptor, { bigint: true });
|
|
1033
|
-
if (
|
|
1034
|
-
!durableParent.isDirectory() ||
|
|
1035
|
-
durableParent.dev !== parentIdentity.dev ||
|
|
1036
|
-
durableParent.ino !== parentIdentity.ino
|
|
1037
|
-
)
|
|
1038
|
-
throw new Error("parent_changed");
|
|
1039
|
-
fs.fsyncSync(descriptor);
|
|
1040
|
-
} catch (error) {
|
|
1041
|
-
throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
|
|
1042
|
-
} finally {
|
|
1043
|
-
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
1044
|
-
}
|
|
1045
|
-
}
|
|
1049
|
+
if (!detach.ok) durablyRecordArtifactPhase();
|
|
1046
1050
|
if (!detach.ok) {
|
|
1047
1051
|
return {
|
|
1048
1052
|
kind: "cleanup_pending",
|
|
@@ -1066,6 +1070,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1066
1070
|
"artifacts",
|
|
1067
1071
|
"Native artifact removal returned an unauthorized root",
|
|
1068
1072
|
);
|
|
1073
|
+
durablyRecordArtifactPhase();
|
|
1069
1074
|
return {
|
|
1070
1075
|
kind: "cleanup_pending",
|
|
1071
1076
|
phase: "artifacts",
|
|
@@ -1090,27 +1095,7 @@ export class FileSessionStorage implements SessionStorage {
|
|
|
1090
1095
|
}
|
|
1091
1096
|
}
|
|
1092
1097
|
if (!artifactsRemoved) {
|
|
1093
|
-
|
|
1094
|
-
let descriptor: number | undefined;
|
|
1095
|
-
try {
|
|
1096
|
-
descriptor = fs.openSync(
|
|
1097
|
-
path.dirname(transcriptPath),
|
|
1098
|
-
fs.constants.O_RDONLY | fs.constants.O_DIRECTORY | fs.constants.O_NOFOLLOW,
|
|
1099
|
-
);
|
|
1100
|
-
const durableParent = fs.fstatSync(descriptor, { bigint: true });
|
|
1101
|
-
if (
|
|
1102
|
-
!durableParent.isDirectory() ||
|
|
1103
|
-
durableParent.dev !== parentIdentity.dev ||
|
|
1104
|
-
durableParent.ino !== parentIdentity.ino
|
|
1105
|
-
)
|
|
1106
|
-
throw new Error("parent_changed");
|
|
1107
|
-
fs.fsyncSync(descriptor);
|
|
1108
|
-
} catch (error) {
|
|
1109
|
-
throw new SessionDeleteVerificationError("artifacts", "durability_failed", { cause: toError(error) });
|
|
1110
|
-
} finally {
|
|
1111
|
-
if (descriptor !== undefined) fs.closeSync(descriptor);
|
|
1112
|
-
}
|
|
1113
|
-
}
|
|
1098
|
+
durablyRecordArtifactPhase();
|
|
1114
1099
|
return { kind: "artifacts_removed", phase: "artifacts", transcriptIdentity };
|
|
1115
1100
|
}
|
|
1116
1101
|
if (hasDetachedTranscript) {
|
package/src/task/index.ts
CHANGED
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
* - Progress tracking via JSON events
|
|
13
13
|
* - Session artifacts for debugging
|
|
14
14
|
*/
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
15
16
|
import * as fs from "node:fs/promises";
|
|
16
17
|
import * as os from "node:os";
|
|
17
18
|
import path from "node:path";
|
|
@@ -53,6 +54,7 @@ import {
|
|
|
53
54
|
resolveTaskRepositoryBinding,
|
|
54
55
|
} from "../gjc-runtime/repository-binding";
|
|
55
56
|
import { initializeLocalRoot, type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
57
|
+
import { ArtifactManager } from "../session/artifacts";
|
|
56
58
|
import { generateCommitMessage } from "../utils/commit-message-generator";
|
|
57
59
|
import * as git from "../utils/git";
|
|
58
60
|
import { discoverAgents, filterVisibleAgents, getAgent } from "./discovery";
|
|
@@ -88,6 +90,7 @@ interface TaskResumeDescriptor {
|
|
|
88
90
|
params: TaskParams;
|
|
89
91
|
task: TaskItem & { id: string };
|
|
90
92
|
sessionFile: string | null;
|
|
93
|
+
durableOutputAllowed?: boolean;
|
|
91
94
|
forkContextSeed?: ForkContextSeed;
|
|
92
95
|
agentSource: AgentDefinition["source"];
|
|
93
96
|
}
|
|
@@ -420,6 +423,37 @@ export function resolveForkContextMaxTokens(configured: number, model: Model | u
|
|
|
420
423
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
421
424
|
// Tool Class
|
|
422
425
|
// ═══════════════════════════════════════════════════════════════════════════
|
|
426
|
+
interface SessionLifetimeArtifactsState {
|
|
427
|
+
dir?: string;
|
|
428
|
+
manager?: ArtifactManager;
|
|
429
|
+
ensurePromise?: Promise<string | null>;
|
|
430
|
+
outputPrefix: string;
|
|
431
|
+
authorized: boolean;
|
|
432
|
+
cleanupRegistered: boolean;
|
|
433
|
+
originalGetArtifactsDir?: ToolSession["getArtifactsDir"];
|
|
434
|
+
originalGetAuthorizedArtifactsDirs?: ToolSession["getAuthorizedArtifactsDirs"];
|
|
435
|
+
originalGetArtifactManager?: ToolSession["getArtifactManager"];
|
|
436
|
+
originalAgentOutputManager?: AgentOutputManager;
|
|
437
|
+
installedGetArtifactsDir?: ToolSession["getArtifactsDir"];
|
|
438
|
+
installedGetAuthorizedArtifactsDirs?: ToolSession["getAuthorizedArtifactsDirs"];
|
|
439
|
+
installedGetArtifactManager?: ToolSession["getArtifactManager"];
|
|
440
|
+
installedAgentOutputManager?: AgentOutputManager;
|
|
441
|
+
}
|
|
442
|
+
|
|
443
|
+
const sessionLifetimeArtifacts = new WeakMap<ToolSession, SessionLifetimeArtifactsState>();
|
|
444
|
+
|
|
445
|
+
function sessionLifetimeArtifactsState(session: ToolSession): SessionLifetimeArtifactsState {
|
|
446
|
+
let state = sessionLifetimeArtifacts.get(session);
|
|
447
|
+
if (!state) {
|
|
448
|
+
state = {
|
|
449
|
+
authorized: false,
|
|
450
|
+
cleanupRegistered: false,
|
|
451
|
+
outputPrefix: `0-Session${randomUUID().replaceAll("-", "")}`,
|
|
452
|
+
};
|
|
453
|
+
sessionLifetimeArtifacts.set(session, state);
|
|
454
|
+
}
|
|
455
|
+
return state;
|
|
456
|
+
}
|
|
423
457
|
|
|
424
458
|
/**
|
|
425
459
|
* Task tool - Delegate tasks to specialized agents.
|
|
@@ -436,6 +470,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
436
470
|
readonly renderResult = renderResult;
|
|
437
471
|
readonly #discoveredAgents: AgentDefinition[];
|
|
438
472
|
readonly #blockedAgent: string | undefined;
|
|
473
|
+
/**
|
|
474
|
+
* Session-lifetime durable artifact state is shared atomically by every
|
|
475
|
+
* TaskTool created for the same parent ToolSession.
|
|
476
|
+
*/
|
|
439
477
|
|
|
440
478
|
get parameters(): TaskToolSchemaInstance {
|
|
441
479
|
const isolationEnabled = this.session.settings.get("task.isolation.mode") !== "none";
|
|
@@ -478,6 +516,172 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
478
516
|
return this.session.settings.get("task.simple");
|
|
479
517
|
}
|
|
480
518
|
|
|
519
|
+
/**
|
|
520
|
+
* Ensure a session-lifetime artifact directory when the parent has no session file.
|
|
521
|
+
* Returns null only when durable allocation fails (mkdir error) — callers must not
|
|
522
|
+
* advertise agent:// URIs without a successful allocation.
|
|
523
|
+
*/
|
|
524
|
+
async #ensureSessionLifetimeArtifacts(): Promise<{ dir: string; manager: ArtifactManager } | null> {
|
|
525
|
+
const state = sessionLifetimeArtifactsState(this.session);
|
|
526
|
+
if (state.authorized && state.dir && state.manager) return { dir: state.dir, manager: state.manager };
|
|
527
|
+
|
|
528
|
+
const sessionArtifactsDir = this.session.getArtifactsDir?.() ?? null;
|
|
529
|
+
if (sessionArtifactsDir) {
|
|
530
|
+
const existingManager = this.session.getArtifactManager?.() ?? null;
|
|
531
|
+
if (existingManager && path.resolve(existingManager.dir) !== path.resolve(sessionArtifactsDir)) return null;
|
|
532
|
+
const manager = existingManager ?? new ArtifactManager(sessionArtifactsDir);
|
|
533
|
+
state.dir = sessionArtifactsDir;
|
|
534
|
+
state.manager = manager;
|
|
535
|
+
if (!(await this.#authorizeSessionLifetimeArtifacts(state, sessionArtifactsDir, manager, false))) return null;
|
|
536
|
+
return { dir: sessionArtifactsDir, manager };
|
|
537
|
+
}
|
|
538
|
+
if (!this.session.registerSessionCleanup) return null;
|
|
539
|
+
|
|
540
|
+
state.ensurePromise ??= this.#allocateSessionLifetimeArtifacts(state);
|
|
541
|
+
const dir = await state.ensurePromise;
|
|
542
|
+
if (!dir || !state.manager) {
|
|
543
|
+
state.ensurePromise = undefined;
|
|
544
|
+
return null;
|
|
545
|
+
}
|
|
546
|
+
return { dir, manager: state.manager };
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
async #allocateSessionLifetimeArtifacts(state: SessionLifetimeArtifactsState): Promise<string | null> {
|
|
550
|
+
let dir: string;
|
|
551
|
+
try {
|
|
552
|
+
dir = await fs.mkdtemp(path.join(os.tmpdir(), "gjc-task-session-"));
|
|
553
|
+
} catch {
|
|
554
|
+
state.dir = undefined;
|
|
555
|
+
state.manager = undefined;
|
|
556
|
+
return null;
|
|
557
|
+
}
|
|
558
|
+
state.dir = dir;
|
|
559
|
+
state.manager = new ArtifactManager(dir);
|
|
560
|
+
if (!(await this.#authorizeSessionLifetimeArtifacts(state, dir, state.manager, true))) return null;
|
|
561
|
+
return dir;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Expose the durable root on the parent ToolSession so subsequent parent-side
|
|
566
|
+
* tools and same-session descendants can resolve agent:// without registry-wide
|
|
567
|
+
* enumeration (#3302 scoped authorization).
|
|
568
|
+
*/
|
|
569
|
+
async #authorizeSessionLifetimeArtifacts(
|
|
570
|
+
state: SessionLifetimeArtifactsState,
|
|
571
|
+
dir: string,
|
|
572
|
+
manager: ArtifactManager,
|
|
573
|
+
owned: boolean,
|
|
574
|
+
): Promise<boolean> {
|
|
575
|
+
if (state.authorized) return true;
|
|
576
|
+
state.originalGetArtifactsDir = this.session.getArtifactsDir;
|
|
577
|
+
state.originalGetAuthorizedArtifactsDirs = this.session.getAuthorizedArtifactsDirs;
|
|
578
|
+
state.originalGetArtifactManager = this.session.getArtifactManager;
|
|
579
|
+
state.originalAgentOutputManager = this.session.agentOutputManager;
|
|
580
|
+
|
|
581
|
+
state.installedGetArtifactsDir = () => state.originalGetArtifactsDir?.() ?? dir;
|
|
582
|
+
state.installedGetAuthorizedArtifactsDirs = () => {
|
|
583
|
+
const dirs: string[] = [];
|
|
584
|
+
const add = (candidate: string | null | undefined) => {
|
|
585
|
+
if (!candidate) return;
|
|
586
|
+
const normalized = path.resolve(candidate);
|
|
587
|
+
if (!dirs.includes(normalized)) dirs.push(normalized);
|
|
588
|
+
};
|
|
589
|
+
for (const authorized of state.originalGetAuthorizedArtifactsDirs?.() ?? []) add(authorized);
|
|
590
|
+
add(dir);
|
|
591
|
+
return dirs;
|
|
592
|
+
};
|
|
593
|
+
state.installedGetArtifactManager = () => manager;
|
|
594
|
+
if (owned || !this.session.agentOutputManager) {
|
|
595
|
+
state.installedAgentOutputManager = new AgentOutputManager(() => this.session.getArtifactsDir?.() ?? null, {
|
|
596
|
+
parentPrefix: owned ? state.outputPrefix : undefined,
|
|
597
|
+
getAuthorizedArtifactsDirs: () => this.session.getAuthorizedArtifactsDirs?.() ?? [],
|
|
598
|
+
});
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
let cleanupRan = false;
|
|
602
|
+
const cleanup = async () => {
|
|
603
|
+
cleanupRan = true;
|
|
604
|
+
try {
|
|
605
|
+
if (owned) await fs.rm(dir, { recursive: true, force: true });
|
|
606
|
+
} finally {
|
|
607
|
+
if (this.session.getArtifactsDir === state.installedGetArtifactsDir)
|
|
608
|
+
this.session.getArtifactsDir = state.originalGetArtifactsDir;
|
|
609
|
+
if (this.session.getAuthorizedArtifactsDirs === state.installedGetAuthorizedArtifactsDirs)
|
|
610
|
+
this.session.getAuthorizedArtifactsDirs = state.originalGetAuthorizedArtifactsDirs;
|
|
611
|
+
if (this.session.getArtifactManager === state.installedGetArtifactManager)
|
|
612
|
+
this.session.getArtifactManager = state.originalGetArtifactManager;
|
|
613
|
+
if (this.session.agentOutputManager === state.installedAgentOutputManager)
|
|
614
|
+
this.session.agentOutputManager = state.originalAgentOutputManager;
|
|
615
|
+
sessionLifetimeArtifacts.delete(this.session);
|
|
616
|
+
}
|
|
617
|
+
};
|
|
618
|
+
if (owned) {
|
|
619
|
+
const registerCleanup = this.session.registerSessionCleanup;
|
|
620
|
+
if (!registerCleanup) {
|
|
621
|
+
state.dir = undefined;
|
|
622
|
+
state.manager = undefined;
|
|
623
|
+
state.ensurePromise = undefined;
|
|
624
|
+
sessionLifetimeArtifacts.delete(this.session);
|
|
625
|
+
await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
626
|
+
return false;
|
|
627
|
+
}
|
|
628
|
+
try {
|
|
629
|
+
registerCleanup(cleanup);
|
|
630
|
+
} catch {
|
|
631
|
+
state.dir = undefined;
|
|
632
|
+
state.manager = undefined;
|
|
633
|
+
state.ensurePromise = undefined;
|
|
634
|
+
sessionLifetimeArtifacts.delete(this.session);
|
|
635
|
+
await fs.rm(dir, { recursive: true, force: true }).catch(() => {});
|
|
636
|
+
return false;
|
|
637
|
+
}
|
|
638
|
+
if (cleanupRan) return false;
|
|
639
|
+
state.cleanupRegistered = true;
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
state.authorized = true;
|
|
643
|
+
this.session.getArtifactsDir = state.installedGetArtifactsDir;
|
|
644
|
+
this.session.getAuthorizedArtifactsDirs = state.installedGetAuthorizedArtifactsDirs;
|
|
645
|
+
this.session.getArtifactManager = state.installedGetArtifactManager;
|
|
646
|
+
if (state.installedAgentOutputManager) this.session.agentOutputManager = state.installedAgentOutputManager;
|
|
647
|
+
return true;
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
/**
|
|
651
|
+
* Resolve the effective artifacts directory for this task batch.
|
|
652
|
+
* File-backed sessions use the session artifacts path; in-memory parents use
|
|
653
|
+
* the session-lifetime durable temp root when allocation succeeds.
|
|
654
|
+
*/
|
|
655
|
+
async #resolveEffectiveArtifactsDir(): Promise<{
|
|
656
|
+
sessionArtifactsDir: string | null;
|
|
657
|
+
durableArtifactsDir: string | null;
|
|
658
|
+
effectiveArtifactsDir: string | undefined;
|
|
659
|
+
parentArtifactManager: ArtifactManager | undefined;
|
|
660
|
+
}> {
|
|
661
|
+
const sessionFile = this.session.getSessionFile();
|
|
662
|
+
const sessionArtifactsDir = sessionFile ? sessionFile.slice(0, -6) : null;
|
|
663
|
+
if (sessionArtifactsDir) {
|
|
664
|
+
const candidateManager = this.session.getArtifactManager?.() ?? undefined;
|
|
665
|
+
const parentArtifactManager =
|
|
666
|
+
candidateManager && path.resolve(candidateManager.dir) === path.resolve(sessionArtifactsDir)
|
|
667
|
+
? candidateManager
|
|
668
|
+
: undefined;
|
|
669
|
+
return {
|
|
670
|
+
sessionArtifactsDir,
|
|
671
|
+
durableArtifactsDir: null,
|
|
672
|
+
effectiveArtifactsDir: sessionArtifactsDir,
|
|
673
|
+
parentArtifactManager,
|
|
674
|
+
};
|
|
675
|
+
}
|
|
676
|
+
const durable = await this.#ensureSessionLifetimeArtifacts();
|
|
677
|
+
return {
|
|
678
|
+
sessionArtifactsDir: null,
|
|
679
|
+
durableArtifactsDir: durable?.dir ?? null,
|
|
680
|
+
effectiveArtifactsDir: durable?.dir,
|
|
681
|
+
parentArtifactManager: durable?.manager,
|
|
682
|
+
};
|
|
683
|
+
}
|
|
684
|
+
|
|
481
685
|
/**
|
|
482
686
|
* Create a TaskTool instance.
|
|
483
687
|
* Repository authority is captured from session cwd *before* agent discovery so
|
|
@@ -589,6 +793,25 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
589
793
|
};
|
|
590
794
|
}
|
|
591
795
|
|
|
796
|
+
// Prefer file-backed session artifacts; otherwise allocate a session-lifetime
|
|
797
|
+
// durable root so detached outputs remain readable for the parent session.
|
|
798
|
+
// Resolve before ID allocation so agentOutputManager can scan the durable root.
|
|
799
|
+
const { effectiveArtifactsDir: batchArtifactsDir, parentArtifactManager: asyncParentArtifactManager } =
|
|
800
|
+
await this.#resolveEffectiveArtifactsDir();
|
|
801
|
+
let externalTaskSessionsDir: string | undefined;
|
|
802
|
+
if (!batchArtifactsDir) {
|
|
803
|
+
// Durable allocation failed: keep child session jsonl under local:// only.
|
|
804
|
+
// Do not advertise agent:// URIs without durable output backing.
|
|
805
|
+
const asyncLocalOptions: LocalProtocolOptions = {
|
|
806
|
+
getArtifactsDir: this.session.getArtifactsDir ?? (() => null),
|
|
807
|
+
isManagedDestination: this.session.isManagedSessionDestination,
|
|
808
|
+
getSessionId: this.session.getSessionId ?? (() => null),
|
|
809
|
+
};
|
|
810
|
+
await initializeLocalRoot(asyncLocalOptions);
|
|
811
|
+
externalTaskSessionsDir = resolveLocalUrlToPath("local://subagents/sessions", asyncLocalOptions);
|
|
812
|
+
await fs.mkdir(externalTaskSessionsDir, { recursive: true, mode: 0o700 });
|
|
813
|
+
}
|
|
814
|
+
|
|
592
815
|
const outputManager =
|
|
593
816
|
this.session.agentOutputManager ?? new AgentOutputManager(this.session.getArtifactsDir ?? (() => null));
|
|
594
817
|
const uniqueIds = await outputManager.allocateBatch(taskItems.map(t => t.id));
|
|
@@ -668,6 +891,14 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
668
891
|
resumeMessage: message,
|
|
669
892
|
sessionFiles: new Map([[descriptor.task.id, descriptor.sessionFile]]),
|
|
670
893
|
suppressRoiReconciliation: true,
|
|
894
|
+
...(descriptor.durableOutputAllowed === true
|
|
895
|
+
? {}
|
|
896
|
+
: {
|
|
897
|
+
persistence: {
|
|
898
|
+
effectiveArtifactsDir: undefined,
|
|
899
|
+
parentArtifactManager: undefined,
|
|
900
|
+
},
|
|
901
|
+
}),
|
|
671
902
|
},
|
|
672
903
|
);
|
|
673
904
|
const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
|
|
@@ -714,20 +945,6 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
714
945
|
});
|
|
715
946
|
};
|
|
716
947
|
const frozenForkSeeds = new Map<string, ForkContextSeed>();
|
|
717
|
-
const asyncParentArtifactManager = this.session.getArtifactManager?.() ?? undefined;
|
|
718
|
-
const parentSessionFileForBatch = this.session.getSessionFile();
|
|
719
|
-
const batchArtifactsDir = parentSessionFileForBatch ? parentSessionFileForBatch.slice(0, -6) : null;
|
|
720
|
-
let externalTaskSessionsDir: string | undefined;
|
|
721
|
-
if (!batchArtifactsDir) {
|
|
722
|
-
const asyncLocalOptions: LocalProtocolOptions = {
|
|
723
|
-
getArtifactsDir: this.session.getArtifactsDir ?? (() => null),
|
|
724
|
-
isManagedDestination: this.session.isManagedSessionDestination,
|
|
725
|
-
getSessionId: this.session.getSessionId ?? (() => null),
|
|
726
|
-
};
|
|
727
|
-
await initializeLocalRoot(asyncLocalOptions);
|
|
728
|
-
externalTaskSessionsDir = resolveLocalUrlToPath("local://subagents/sessions", asyncLocalOptions);
|
|
729
|
-
await fs.mkdir(externalTaskSessionsDir, { recursive: true, mode: 0o700 });
|
|
730
|
-
}
|
|
731
948
|
|
|
732
949
|
for (let i = 0; i < taskItems.length; i++) {
|
|
733
950
|
const taskItem = taskItems[i];
|
|
@@ -793,6 +1010,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
793
1010
|
{
|
|
794
1011
|
sessionFiles: new Map([[uniqueId, subtaskSessionFile]]),
|
|
795
1012
|
suppressRoiReconciliation: true,
|
|
1013
|
+
persistence: {
|
|
1014
|
+
effectiveArtifactsDir: batchArtifactsDir,
|
|
1015
|
+
parentArtifactManager: asyncParentArtifactManager,
|
|
1016
|
+
},
|
|
796
1017
|
},
|
|
797
1018
|
);
|
|
798
1019
|
const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
|
|
@@ -902,6 +1123,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
902
1123
|
params,
|
|
903
1124
|
task: { ...taskItem, id: uniqueId },
|
|
904
1125
|
sessionFile: subtaskSessionFile,
|
|
1126
|
+
durableOutputAllowed: Boolean(batchArtifactsDir),
|
|
905
1127
|
forkContextSeed: frozenForkSeed,
|
|
906
1128
|
agentSource: fallbackAgentSource,
|
|
907
1129
|
} satisfies TaskResumeDescriptor,
|
|
@@ -998,6 +1220,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
998
1220
|
runMode?: "initial" | "resume" | "message";
|
|
999
1221
|
resumeMessage?: string;
|
|
1000
1222
|
sessionFiles?: ReadonlyMap<string, string | null>;
|
|
1223
|
+
persistence?: {
|
|
1224
|
+
effectiveArtifactsDir: string | undefined;
|
|
1225
|
+
parentArtifactManager: ArtifactManager | undefined;
|
|
1226
|
+
};
|
|
1001
1227
|
/**
|
|
1002
1228
|
* Set for per-child async runs: the spawnPlan is carried for gate
|
|
1003
1229
|
* consistency, but batch-level ROI reconciliation must not be computed
|
|
@@ -1247,11 +1473,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1247
1473
|
|
|
1248
1474
|
const preferredIsolationBackend = parseIsolationMode(isolationMode);
|
|
1249
1475
|
|
|
1250
|
-
//
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
const
|
|
1254
|
-
|
|
1476
|
+
// File-backed session artifacts, or a session-lifetime durable temp root for
|
|
1477
|
+
// in-memory parents. Never use a per-task temp dir that is deleted on return —
|
|
1478
|
+
// advertised agent:// URIs must remain readable for the parent session lifetime.
|
|
1479
|
+
const { effectiveArtifactsDir, parentArtifactManager } =
|
|
1480
|
+
executionOverrides?.persistence ?? (await this.#resolveEffectiveArtifactsDir());
|
|
1481
|
+
const hasDurableOutputBacking = Boolean(effectiveArtifactsDir);
|
|
1255
1482
|
|
|
1256
1483
|
// Share the parent session's local:// root with subagents so they read/write the same scratch space
|
|
1257
1484
|
const localProtocolOptions: LocalProtocolOptions = {
|
|
@@ -1260,9 +1487,9 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1260
1487
|
getSessionId: this.session.getSessionId ?? (() => null),
|
|
1261
1488
|
};
|
|
1262
1489
|
|
|
1263
|
-
// Subagents adopt the parent's ArtifactManager
|
|
1264
|
-
//
|
|
1265
|
-
|
|
1490
|
+
// Subagents adopt the parent's ArtifactManager (including session-lifetime
|
|
1491
|
+
// durable managers) so artifact IDs are unique and agent:// resolves under
|
|
1492
|
+
// scoped authorization for the whole tree.
|
|
1266
1493
|
|
|
1267
1494
|
// Initialize progress tracking
|
|
1268
1495
|
const progressMap = new Map<number, AgentProgress>();
|
|
@@ -1468,7 +1695,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1468
1695
|
thinkingLevel: thinkingLevelOverride,
|
|
1469
1696
|
outputSchema: effectiveOutputSchema,
|
|
1470
1697
|
sessionFile: taskSessionFile,
|
|
1471
|
-
persistArtifacts:
|
|
1698
|
+
persistArtifacts: hasDurableOutputBacking,
|
|
1472
1699
|
artifactsDir: effectiveArtifactsDir,
|
|
1473
1700
|
managedPersistence,
|
|
1474
1701
|
contextFile: contextFilePath,
|
|
@@ -1542,7 +1769,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1542
1769
|
thinkingLevel: thinkingLevelOverride,
|
|
1543
1770
|
outputSchema: effectiveOutputSchema,
|
|
1544
1771
|
sessionFile: taskSessionFile,
|
|
1545
|
-
persistArtifacts:
|
|
1772
|
+
persistArtifacts: hasDurableOutputBacking,
|
|
1546
1773
|
artifactsDir: effectiveArtifactsDir,
|
|
1547
1774
|
managedPersistence,
|
|
1548
1775
|
contextFile: contextFilePath,
|
|
@@ -1703,7 +1930,10 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1703
1930
|
abortReason: "Cancelled before start",
|
|
1704
1931
|
};
|
|
1705
1932
|
});
|
|
1706
|
-
|
|
1933
|
+
// Only advertise agent:// when outputs land on durable backing (file-backed
|
|
1934
|
+
// session artifacts or the session-lifetime durable temp root). Ephemeral
|
|
1935
|
+
// or failed allocation must not emit dead URIs (#3471 / #295).
|
|
1936
|
+
if (!hasDurableOutputBacking) {
|
|
1707
1937
|
for (const result of results) {
|
|
1708
1938
|
delete result.outputMeta;
|
|
1709
1939
|
delete result.outputPath;
|
|
@@ -1913,12 +2143,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1913
2143
|
mergeSummary,
|
|
1914
2144
|
});
|
|
1915
2145
|
|
|
1916
|
-
//
|
|
1917
|
-
|
|
1918
|
-
tempArtifactsDir && (!isIsolated || changesApplied === true || changesApplied === null);
|
|
1919
|
-
if (shouldCleanupTempArtifacts) {
|
|
1920
|
-
await fs.rm(tempArtifactsDir, { recursive: true, force: true });
|
|
1921
|
-
}
|
|
2146
|
+
// Session-lifetime durable dirs must outlive this return so parent +
|
|
2147
|
+
// same-session descendants can resolve agent://. Do not rm them here.
|
|
1922
2148
|
|
|
1923
2149
|
const details: TaskToolDetails = {
|
|
1924
2150
|
projectAgentsDir,
|
|
@@ -29,10 +29,15 @@ export class AgentOutputManager {
|
|
|
29
29
|
#initPromise: Promise<void> | undefined;
|
|
30
30
|
readonly #getArtifactsDir: () => string | null;
|
|
31
31
|
readonly #parentPrefix: string | undefined;
|
|
32
|
+
readonly #getAuthorizedArtifactsDirs: (() => readonly string[]) | undefined;
|
|
32
33
|
|
|
33
|
-
constructor(
|
|
34
|
+
constructor(
|
|
35
|
+
getArtifactsDir: () => string | null,
|
|
36
|
+
options?: { parentPrefix?: string; getAuthorizedArtifactsDirs?: () => readonly string[] },
|
|
37
|
+
) {
|
|
34
38
|
this.#getArtifactsDir = getArtifactsDir;
|
|
35
39
|
this.#parentPrefix = options?.parentPrefix;
|
|
40
|
+
this.#getAuthorizedArtifactsDirs = options?.getAuthorizedArtifactsDirs;
|
|
36
41
|
}
|
|
37
42
|
|
|
38
43
|
/**
|
|
@@ -53,24 +58,29 @@ export class AgentOutputManager {
|
|
|
53
58
|
* This ensures we don't overwrite outputs when resuming a session.
|
|
54
59
|
*/
|
|
55
60
|
async #scanExistingOutputs(): Promise<void> {
|
|
56
|
-
const
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
}
|
|
61
|
+
const dirs: string[] = [];
|
|
62
|
+
const add = (dir: string | null | undefined) => {
|
|
63
|
+
if (!dir || dirs.includes(dir)) return;
|
|
64
|
+
dirs.push(dir);
|
|
65
|
+
};
|
|
66
|
+
add(this.#getArtifactsDir());
|
|
67
|
+
for (const dir of this.#getAuthorizedArtifactsDirs?.() ?? []) add(dir);
|
|
68
|
+
if (dirs.length === 0) return;
|
|
65
69
|
|
|
66
70
|
const pattern = this.#parentPrefix
|
|
67
71
|
? new RegExp(`^${escapeRegExp(this.#parentPrefix)}\\.(\\d+)-.*\\.${RESERVED_OUTPUT_EXTENSIONS_PATTERN}$`)
|
|
68
72
|
: new RegExp(`^(\\d+)-.*\\.${RESERVED_OUTPUT_EXTENSIONS_PATTERN}$`);
|
|
69
|
-
|
|
70
73
|
let maxId = -1;
|
|
71
|
-
for (const
|
|
72
|
-
|
|
73
|
-
|
|
74
|
+
for (const dir of dirs) {
|
|
75
|
+
let files: string[];
|
|
76
|
+
try {
|
|
77
|
+
files = await fs.readdir(dir);
|
|
78
|
+
} catch {
|
|
79
|
+
continue;
|
|
80
|
+
}
|
|
81
|
+
for (const file of files) {
|
|
82
|
+
const match = file.match(pattern);
|
|
83
|
+
if (!match) continue;
|
|
74
84
|
const id = Number.parseInt(match[1], 10);
|
|
75
85
|
if (id > maxId) maxId = id;
|
|
76
86
|
}
|
package/src/task/receipt.ts
CHANGED
|
@@ -45,7 +45,14 @@ export interface TaskResultReceipt {
|
|
|
45
45
|
abortSummary?: string;
|
|
46
46
|
preview: string;
|
|
47
47
|
previewTruncated: boolean;
|
|
48
|
-
outputRef?: {
|
|
48
|
+
outputRef?: {
|
|
49
|
+
uri: string;
|
|
50
|
+
sizeBytes: number;
|
|
51
|
+
lineCount: number;
|
|
52
|
+
sha256?: string;
|
|
53
|
+
/** Output remains readable for the parent session lifetime (and same-session descendants). */
|
|
54
|
+
durability?: "session";
|
|
55
|
+
};
|
|
49
56
|
outputUnavailable?: boolean;
|
|
50
57
|
review?: {
|
|
51
58
|
overallCorrectness?: string;
|
|
@@ -224,12 +231,15 @@ export function buildTaskRoiSummary(receipts: readonly TaskResultReceipt[]): Tas
|
|
|
224
231
|
}
|
|
225
232
|
|
|
226
233
|
export function buildTaskReceipt(raw: SingleResult): TaskResultReceipt {
|
|
234
|
+
// Receipts only include outputRef when production code kept outputMeta after a
|
|
235
|
+
// durable write (file-backed session dir or session-lifetime durable root).
|
|
227
236
|
const outputRef = raw.outputMeta
|
|
228
237
|
? {
|
|
229
238
|
uri: `agent://${raw.id}`,
|
|
230
239
|
sizeBytes: raw.outputMeta.byteSize ?? Buffer.byteLength(raw.output, "utf8"),
|
|
231
240
|
lineCount: raw.outputMeta.lineCount,
|
|
232
241
|
sha256: raw.outputMeta.sha256,
|
|
242
|
+
durability: "session" as const,
|
|
233
243
|
}
|
|
234
244
|
: undefined;
|
|
235
245
|
const preview = buildSafeSynopsis(raw, outputRef);
|
package/src/tools/index.ts
CHANGED
|
@@ -296,6 +296,8 @@ export interface ToolSession {
|
|
|
296
296
|
getAuthorizedArtifactsDirs?: () => readonly string[];
|
|
297
297
|
/** Get the ArtifactManager backing this session (shared across parent + subagents). */
|
|
298
298
|
getArtifactManager?: () => ArtifactManager | null;
|
|
299
|
+
/** Register teardown work owned by the current logical session. */
|
|
300
|
+
registerSessionCleanup?: (cleanup: () => Promise<void> | void) => () => void;
|
|
299
301
|
/** Allocate a new artifact path and ID for session-scoped truncated output. */
|
|
300
302
|
allocateOutputArtifact?: (toolType: string) => Promise<{ id?: string; path?: string }>;
|
|
301
303
|
/** Get session spawns */
|