@gajae-code/coding-agent 0.11.4 → 0.11.6
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 +23 -0
- package/dist/types/cli/notify-cli.d.ts +5 -1
- package/dist/types/config/settings-schema.d.ts +31 -1
- 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/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/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/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 +58 -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/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/bus/chat-daemon-cli.d.ts +3 -1
- package/dist/types/sdk/bus/chat-daemon-control.d.ts +42 -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 +11 -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 +98 -18
- package/dist/types/session/session-storage.d.ts +27 -0
- package/dist/types/session/streaming-output.d.ts +4 -0
- package/dist/types/session/yield-queue.d.ts +8 -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 +48 -0
- package/dist/types/tools/bash-skill-urls.d.ts +2 -2
- package/dist/types/tools/index.d.ts +4 -0
- package/package.json +9 -9
- package/scripts/generate-sdk-operation-inventory.ts +3 -0
- package/src/cli/notify-cli.ts +40 -18
- package/src/commands/sdk.ts +7 -2
- package/src/config/settings-schema.ts +31 -1
- 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/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/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-runtime.ts +38 -12
- 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 +8 -7
- package/src/internal-urls/local-protocol.ts +416 -18
- package/src/lsp/index.ts +4 -88
- package/src/main.ts +36 -12
- package/src/modes/acp/acp-agent.ts +8 -0
- package/src/modes/components/ansi-display-validator.ts +54 -0
- package/src/modes/components/notifications-settings-editor.ts +26 -2
- package/src/modes/components/session-observer-overlay.ts +40 -24
- package/src/modes/components/settings-selector.ts +3 -1
- package/src/modes/components/tool-transcript-format.ts +321 -0
- package/src/modes/components/transcript-viewer-overlay.ts +39 -17
- package/src/modes/controllers/command-controller.ts +15 -0
- package/src/modes/controllers/extension-ui-controller.ts +14 -2
- package/src/modes/controllers/goal-mode-controller.ts +20 -1
- 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 +70 -8
- package/src/modes/interactive-mode.ts +1 -0
- package/src/modes/runtime-init.ts +16 -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/session-index.ts +122 -64
- package/src/sdk/bus/chat-daemon-cli.ts +72 -10
- package/src/sdk/bus/chat-daemon-control.ts +538 -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 +388 -94
- 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 +11 -3
- package/src/sdk/bus/telegram-daemon-control.ts +312 -76
- package/src/sdk/bus/telegram-daemon.ts +1916 -285
- package/src/sdk/host/control/dispatch.ts +14 -5
- package/src/sdk/protocol/operation-inventory.generated.json +29 -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 +1447 -852
- package/src/session/artifacts.ts +188 -23
- package/src/session/context-estimation.ts +7 -2
- package/src/session/internal/managed-session-scope.ts +200 -38
- package/src/session/internal/managed-session-storage.ts +1152 -46
- package/src/session/session-manager.ts +1121 -148
- package/src/session/session-storage.ts +109 -11
- package/src/session/streaming-output.ts +6 -0
- package/src/session/yield-queue.ts +20 -0
- package/src/slash-commands/builtin-registry.ts +36 -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-answer-registry.ts +15 -1
- package/src/tools/ask.ts +129 -32
- package/src/tools/bash-skill-urls.ts +14 -3
- package/src/tools/bash.ts +2 -2
- package/src/tools/index.ts +4 -0
- package/src/tools/plan-mode-guard.ts +1 -0
- package/src/tools/sqlite-reader.ts +3 -2
- package/src/tools/subagent.ts +2 -0
|
@@ -5,6 +5,11 @@ import * as path from "node:path";
|
|
|
5
5
|
import * as native from "@gajae-code/natives";
|
|
6
6
|
|
|
7
7
|
import { isEnoent, pathIsWithin, peekFile, toError } from "@gajae-code/utils";
|
|
8
|
+
import {
|
|
9
|
+
assertManagedDirectoryRoot,
|
|
10
|
+
type ManagedDirectoryRoot,
|
|
11
|
+
validateNativeSecurityResult,
|
|
12
|
+
} from "./internal/managed-session-storage";
|
|
8
13
|
|
|
9
14
|
const utf8Decoder = new TextDecoder("utf-8");
|
|
10
15
|
function canonicalPathSync(value: string): string {
|
|
@@ -89,8 +94,41 @@ export interface SessionStorageWriterOpenOptions {
|
|
|
89
94
|
onError?: (err: Error) => void;
|
|
90
95
|
/** Injectable OS-close dispatcher; defaults to `fs.closeSync`. */
|
|
91
96
|
closeAdapter?: SessionStorageWriterCloseAdapter;
|
|
97
|
+
/** Opaque authority for default-computed managed destinations only. */
|
|
98
|
+
securityContext?: SessionStorageSecurityContext;
|
|
92
99
|
}
|
|
93
100
|
|
|
101
|
+
/**
|
|
102
|
+
* Immutable authority attached only to a computed managed session destination.
|
|
103
|
+
* A caller-supplied pathname never receives this capability, even when it
|
|
104
|
+
* happens to equal the current default session directory.
|
|
105
|
+
*/
|
|
106
|
+
export interface ManagedSessionSecurityContext {
|
|
107
|
+
readonly kind: "managed";
|
|
108
|
+
readonly agentDir: string;
|
|
109
|
+
readonly sessionsRoot: string;
|
|
110
|
+
readonly sessionDir: string;
|
|
111
|
+
readonly rootAuthority: ManagedDirectoryRoot;
|
|
112
|
+
readonly retainedAuthority?: native.RecoveryFsRoot;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const managedSecurityContexts = new WeakSet<ManagedSessionSecurityContext>();
|
|
116
|
+
|
|
117
|
+
/** @internal Create the only accepted managed writer authority object. */
|
|
118
|
+
export function createManagedSessionSecurityContext(input: {
|
|
119
|
+
agentDir: string;
|
|
120
|
+
sessionsRoot: string;
|
|
121
|
+
sessionDir: string;
|
|
122
|
+
rootAuthority: ManagedDirectoryRoot;
|
|
123
|
+
retainedAuthority?: native.RecoveryFsRoot;
|
|
124
|
+
}): ManagedSessionSecurityContext {
|
|
125
|
+
const context = Object.freeze({ kind: "managed" as const, ...input });
|
|
126
|
+
managedSecurityContexts.add(context);
|
|
127
|
+
return context;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
export type SessionStorageSecurityContext = ManagedSessionSecurityContext | undefined;
|
|
131
|
+
|
|
94
132
|
export interface SessionStorageWriter {
|
|
95
133
|
writeLine(line: string): Promise<void>;
|
|
96
134
|
/**
|
|
@@ -268,8 +306,6 @@ const defaultCloseAdapter: SessionStorageWriterCloseAdapter = {
|
|
|
268
306
|
},
|
|
269
307
|
};
|
|
270
308
|
|
|
271
|
-
type NativeSecurity = { ok: true } | { ok: false; code: string };
|
|
272
|
-
|
|
273
309
|
type NativeExactUnlinkResult = { ok: true; detachedPath?: string } | { ok: false; code: string; detachedPath?: string };
|
|
274
310
|
type NativeExactUnlink = (
|
|
275
311
|
path: string,
|
|
@@ -309,6 +345,7 @@ type NativeDirectoryTreeEntry = {
|
|
|
309
345
|
ino: string;
|
|
310
346
|
size: string;
|
|
311
347
|
mtimeNs: string;
|
|
348
|
+
ctimeNs: string;
|
|
312
349
|
sha256?: string;
|
|
313
350
|
};
|
|
314
351
|
export type NativeDirectoryTreeSnapshot = {
|
|
@@ -349,11 +386,57 @@ function exactUnlinkFailure(result: NativeExactUnlinkResult): SessionDeleteVerif
|
|
|
349
386
|
: "stat";
|
|
350
387
|
return new SessionDeleteVerificationError(kind, `Exact transcript deletion rejected: ${result.code}`);
|
|
351
388
|
}
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
if (
|
|
355
|
-
|
|
356
|
-
|
|
389
|
+
|
|
390
|
+
function isValidManagedSecurityContext(value: SessionStorageSecurityContext): value is ManagedSessionSecurityContext {
|
|
391
|
+
if (
|
|
392
|
+
value?.kind !== "managed" ||
|
|
393
|
+
!Object.isFrozen(value) ||
|
|
394
|
+
!managedSecurityContexts.has(value) ||
|
|
395
|
+
!pathIsWithin(value.agentDir, value.sessionsRoot) ||
|
|
396
|
+
!pathIsWithin(value.sessionsRoot, value.sessionDir)
|
|
397
|
+
) {
|
|
398
|
+
return false;
|
|
399
|
+
}
|
|
400
|
+
assertManagedDirectoryRoot(value.rootAuthority);
|
|
401
|
+
if (!pathIsWithin(value.rootAuthority.canonicalPath, value.agentDir)) return false;
|
|
402
|
+
return true;
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function secureOwnerOnlyFileDescriptor(
|
|
406
|
+
pathname: string,
|
|
407
|
+
fd: number,
|
|
408
|
+
operation: "apply" | "verify",
|
|
409
|
+
securityContext: SessionStorageSecurityContext,
|
|
410
|
+
): void {
|
|
411
|
+
if (securityContext && !isValidManagedSecurityContext(securityContext))
|
|
412
|
+
throw new Error("Invalid managed session security context");
|
|
413
|
+
if (process.platform !== "linux" || !securityContext) {
|
|
414
|
+
if (operation === "apply") {
|
|
415
|
+
const applied = validateNativeSecurityResult(
|
|
416
|
+
native.applyOwnerOnlyPathSecurity(pathname, "file"),
|
|
417
|
+
"apply",
|
|
418
|
+
"file",
|
|
419
|
+
);
|
|
420
|
+
if (!applied.ok) throw new Error(`Owner-only security rejected ${pathname}: ${applied.code}`);
|
|
421
|
+
}
|
|
422
|
+
const verified = validateNativeSecurityResult(
|
|
423
|
+
native.verifyOwnerOnlyPathSecurity(pathname, "file"),
|
|
424
|
+
"verify",
|
|
425
|
+
"file",
|
|
426
|
+
);
|
|
427
|
+
if (!verified.ok) throw new Error(`Owner-only security rejected ${pathname}: ${verified.code}`);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
if (!pathIsWithin(securityContext.sessionDir, pathname))
|
|
431
|
+
throw new Error(`Managed writer escaped its session directory: ${pathname}`);
|
|
432
|
+
const result = validateNativeSecurityResult(
|
|
433
|
+
operation === "apply"
|
|
434
|
+
? native.applyOwnerOnlyFdSecurity(pathname, "file", fd)
|
|
435
|
+
: native.verifyOwnerOnlyFdSecurity(pathname, "file", fd),
|
|
436
|
+
operation,
|
|
437
|
+
"file",
|
|
438
|
+
);
|
|
439
|
+
if (!result.ok) throw new Error(`Owner-only security rejected ${pathname}: ${result.code}`);
|
|
357
440
|
}
|
|
358
441
|
|
|
359
442
|
/** Reject a symlink/junction/reparse component before a storage path is created or opened. */
|
|
@@ -383,35 +466,41 @@ const writerRegistry = new FinalizationRegistry<number>(fd => {
|
|
|
383
466
|
|
|
384
467
|
class FileSessionStorageWriter implements SessionStorageWriter {
|
|
385
468
|
#fd: number;
|
|
469
|
+
#path: string;
|
|
470
|
+
|
|
386
471
|
#closeState: SessionStorageWriterCloseState = "open";
|
|
387
472
|
#closeError: Error | undefined;
|
|
388
473
|
#error: Error | undefined;
|
|
389
474
|
#onError: ((err: Error) => void) | undefined;
|
|
390
475
|
#closeAdapter: SessionStorageWriterCloseAdapter;
|
|
476
|
+
#securityContext: SessionStorageSecurityContext;
|
|
391
477
|
|
|
392
478
|
constructor(fpath: string, options?: SessionStorageWriterOpenOptions) {
|
|
393
479
|
this.#onError = options?.onError;
|
|
394
480
|
this.#closeAdapter = options?.closeAdapter ?? defaultCloseAdapter;
|
|
481
|
+
this.#securityContext = options?.securityContext;
|
|
395
482
|
const flags = options?.flags ?? "a";
|
|
396
483
|
const dir = path.dirname(fpath);
|
|
397
484
|
assertNoReparsePath(dir);
|
|
398
485
|
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
399
486
|
assertNoReparsePath(dir);
|
|
400
487
|
assertNoReparsePath(fpath);
|
|
401
|
-
//
|
|
488
|
+
// Never truncate before the descriptor and its terminal pathname have passed native security.
|
|
402
489
|
const openFlags =
|
|
403
490
|
(flags === "w"
|
|
404
|
-
? fs.constants.O_WRONLY | fs.constants.O_CREAT
|
|
491
|
+
? fs.constants.O_WRONLY | fs.constants.O_CREAT
|
|
405
492
|
: fs.constants.O_WRONLY | fs.constants.O_CREAT | fs.constants.O_APPEND) | (fs.constants.O_NOFOLLOW ?? 0);
|
|
406
493
|
const fd = fs.openSync(fpath, openFlags, 0o600);
|
|
407
494
|
try {
|
|
408
|
-
|
|
409
|
-
|
|
495
|
+
secureOwnerOnlyFileDescriptor(fpath, fd, "apply", this.#securityContext);
|
|
496
|
+
if (flags === "w") fs.ftruncateSync(fd, 0);
|
|
410
497
|
} catch (error) {
|
|
411
498
|
fs.closeSync(fd);
|
|
412
499
|
throw error;
|
|
413
500
|
}
|
|
414
501
|
this.#fd = fd;
|
|
502
|
+
this.#path = fpath;
|
|
503
|
+
|
|
415
504
|
writerRegistry.register(this, this.#fd, this);
|
|
416
505
|
}
|
|
417
506
|
|
|
@@ -469,6 +558,7 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
469
558
|
if (this.#error) throw this.#error;
|
|
470
559
|
try {
|
|
471
560
|
fs.fsyncSync(this.#fd);
|
|
561
|
+
secureOwnerOnlyFileDescriptor(this.#path, this.#fd, "verify", this.#securityContext);
|
|
472
562
|
} catch (err) {
|
|
473
563
|
throw this.#recordError(err);
|
|
474
564
|
}
|
|
@@ -481,6 +571,14 @@ class FileSessionStorageWriter implements SessionStorageWriter {
|
|
|
481
571
|
// for this numeric fd again; surface the stored non-quiescent error.
|
|
482
572
|
if (this.#closeState === "close_unknown") throw this.#closeError!;
|
|
483
573
|
// State is "open" or "close_failed_retryable": a close may be dispatched.
|
|
574
|
+
try {
|
|
575
|
+
secureOwnerOnlyFileDescriptor(this.#path, this.#fd, "verify", this.#securityContext);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
// Verification happens before the OS-close dispatch, so descriptor ownership remains proven.
|
|
578
|
+
this.#closeState = "close_failed_retryable";
|
|
579
|
+
this.#closeError = toError(err);
|
|
580
|
+
throw this.#closeError;
|
|
581
|
+
}
|
|
484
582
|
try {
|
|
485
583
|
this.#closeAdapter.close(this.#fd);
|
|
486
584
|
} catch (err) {
|
|
@@ -54,6 +54,10 @@ export interface OutputSummary {
|
|
|
54
54
|
}
|
|
55
55
|
|
|
56
56
|
export interface OutputSinkOptions {
|
|
57
|
+
/**
|
|
58
|
+
* Deprecated managed artifact pathname. Bare paths are deliberately ignored:
|
|
59
|
+
* streaming output must be terminally published through ArtifactManager.
|
|
60
|
+
*/
|
|
57
61
|
artifactPath?: string;
|
|
58
62
|
artifactId?: string;
|
|
59
63
|
/** Tail buffer budget (bytes). Default DEFAULT_MAX_BYTES. */
|
|
@@ -736,6 +740,8 @@ export class OutputSink {
|
|
|
736
740
|
artifactMaxBytes = DEFAULT_ARTIFACT_MAX_BYTES,
|
|
737
741
|
coalesceSanitize = process.env.PI_OUTPUT_SANITIZE_COALESCE === "1",
|
|
738
742
|
} = options ?? {};
|
|
743
|
+
// Managed callers omit artifactPath at the allocation boundary; explicit callers
|
|
744
|
+
// retain the existing pathname-backed streaming contract.
|
|
739
745
|
this.#artifactPath = artifactPath;
|
|
740
746
|
this.#artifactId = artifactId;
|
|
741
747
|
this.#spillThreshold = spillThreshold;
|
|
@@ -107,6 +107,26 @@ export class YieldQueue {
|
|
|
107
107
|
this.#idleFlushPending = false;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/** Drop only the queued entries of a single kind, leaving other kinds intact. */
|
|
111
|
+
clearKind(kind: string): void {
|
|
112
|
+
this.#entries.delete(kind);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Re-schedule an idle flush if work remains and the session is idle. Used after
|
|
117
|
+
* a transition (e.g. handoff) releases a delivery fence so entries queued while
|
|
118
|
+
* fenced are not stranded until an unrelated enqueue or agent yield.
|
|
119
|
+
*/
|
|
120
|
+
rearmIdle(): void {
|
|
121
|
+
if (this.#options.isStreaming()) return;
|
|
122
|
+
for (const entries of this.#entries.values()) {
|
|
123
|
+
if (entries.length > 0) {
|
|
124
|
+
this.#scheduleIdleFlush();
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
110
130
|
#scheduleIdleFlush(): void {
|
|
111
131
|
if (this.#idleFlushPending) return;
|
|
112
132
|
this.#idleFlushPending = true;
|
|
@@ -1435,6 +1435,42 @@ const BUILTIN_SLASH_COMMAND_REGISTRY: ReadonlyArray<SlashCommandSpec> = [
|
|
|
1435
1435
|
await runtime.ctx.handleCompactCommand(customInstructions);
|
|
1436
1436
|
},
|
|
1437
1437
|
},
|
|
1438
|
+
{
|
|
1439
|
+
name: "handoff",
|
|
1440
|
+
priority: 71,
|
|
1441
|
+
description: "Generate a handoff and continue in a new session",
|
|
1442
|
+
acpDescription: "Generate a handoff document and start a new session",
|
|
1443
|
+
inlineHint: "[focus instructions]",
|
|
1444
|
+
acpInputHint: "[focus instructions]",
|
|
1445
|
+
allowArgs: true,
|
|
1446
|
+
handle: async (command, runtime) => {
|
|
1447
|
+
let result: Awaited<ReturnType<typeof runtime.session.handoff>>;
|
|
1448
|
+
try {
|
|
1449
|
+
result = await runtime.session.handoff(command.args || undefined);
|
|
1450
|
+
} catch (err) {
|
|
1451
|
+
// Handoff precondition failures (nothing to hand off, streaming),
|
|
1452
|
+
// cancellation, and provider errors propagate as plain Errors; the
|
|
1453
|
+
// switch is non-destructive so the current session is unchanged.
|
|
1454
|
+
return usage(`Handoff failed: ${errorMessage(err)}; current session is unchanged.`, runtime);
|
|
1455
|
+
}
|
|
1456
|
+
if (!result) {
|
|
1457
|
+
return usage(
|
|
1458
|
+
"Handoff not created (cancelled or nothing to hand off); current session is unchanged.",
|
|
1459
|
+
runtime,
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
await runtime.output(
|
|
1463
|
+
result.savedPath
|
|
1464
|
+
? `Handoff created; new session started. Handoff document saved to: ${result.savedPath}`
|
|
1465
|
+
: "Handoff created; new session started with handoff context.",
|
|
1466
|
+
);
|
|
1467
|
+
return commandConsumed();
|
|
1468
|
+
},
|
|
1469
|
+
handleTui: async (command, runtime) => {
|
|
1470
|
+
runtime.ctx.editor.setText("");
|
|
1471
|
+
await runtime.ctx.handleHandoffCommand(command.args || undefined);
|
|
1472
|
+
},
|
|
1473
|
+
},
|
|
1438
1474
|
{
|
|
1439
1475
|
name: "contribute-pr",
|
|
1440
1476
|
aliases: ["contribution-prep"],
|
package/src/task/executor.ts
CHANGED
|
@@ -5,7 +5,6 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { createHash } from "node:crypto";
|
|
8
|
-
import * as fs from "node:fs/promises";
|
|
9
8
|
import path from "node:path";
|
|
10
9
|
import type {
|
|
11
10
|
AgentEvent,
|
|
@@ -50,6 +49,7 @@ import { ToolAbortError } from "../tools/tool-errors";
|
|
|
50
49
|
import type { EventBus } from "../utils/event-bus";
|
|
51
50
|
import { buildNamedToolChoiceResult } from "../utils/tool-choice";
|
|
52
51
|
import type { WorkspaceTree } from "../workspace-tree";
|
|
52
|
+
import { validateAllocatedTaskId } from "./id";
|
|
53
53
|
import { subprocessToolRegistry } from "./subprocess-tool-registry";
|
|
54
54
|
import { persistTaskTokenLog, taskTokenLogFromUsage } from "./token-log";
|
|
55
55
|
import {
|
|
@@ -227,6 +227,7 @@ export interface ExecutorOptions {
|
|
|
227
227
|
* artifacts directory (no per-subagent subdir).
|
|
228
228
|
*/
|
|
229
229
|
parentArtifactManager?: ArtifactManager;
|
|
230
|
+
managedPersistence?: ManagedTaskPersistence;
|
|
230
231
|
parentHindsightSessionState?: HindsightSessionState;
|
|
231
232
|
/**
|
|
232
233
|
* Parent agent's OpenTelemetry configuration. When defined, the subagent's
|
|
@@ -242,6 +243,45 @@ export interface ExecutorOptions {
|
|
|
242
243
|
forkContextSeed?: ForkContextSeed;
|
|
243
244
|
}
|
|
244
245
|
|
|
246
|
+
export class ManagedTaskPersistence {
|
|
247
|
+
readonly #artifacts: ArtifactManager;
|
|
248
|
+
readonly #taskId: string;
|
|
249
|
+
|
|
250
|
+
constructor(artifacts: ArtifactManager, taskId: string) {
|
|
251
|
+
this.#artifacts = artifacts;
|
|
252
|
+
this.#taskId = validateAllocatedTaskId(taskId);
|
|
253
|
+
if (!artifacts.getManagedSubtreeRootAuthority())
|
|
254
|
+
throw new Error("Managed task persistence authority is unavailable");
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async openSession(): Promise<SessionManager> {
|
|
258
|
+
const store = this.#artifacts.getManagedStore();
|
|
259
|
+
if (!store) throw new Error("Managed task persistence authority is unavailable");
|
|
260
|
+
this.#artifacts.assertManagedBinding();
|
|
261
|
+
const sessionFile = path.join(this.#artifacts.dir, `${this.#taskId}.jsonl`);
|
|
262
|
+
const session = await SessionManager.openNestedManaged(
|
|
263
|
+
sessionFile,
|
|
264
|
+
SessionManager.nestedManagedDestination(store, this.#artifacts.dir),
|
|
265
|
+
store,
|
|
266
|
+
);
|
|
267
|
+
this.#artifacts.assertManagedBinding();
|
|
268
|
+
return session;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
async publishOutput(rawOutput: string, metadata: Uint8Array): Promise<void> {
|
|
272
|
+
await this.#artifacts.publishManagedOutputGeneration(
|
|
273
|
+
`${this.#taskId}.md.selector.json`,
|
|
274
|
+
`${this.#taskId}.md`,
|
|
275
|
+
Buffer.from(rawOutput, "utf8"),
|
|
276
|
+
metadata,
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
export function createManagedTaskPersistence(artifacts: ArtifactManager, taskId: string): ManagedTaskPersistence {
|
|
282
|
+
return new ManagedTaskPersistence(artifacts, taskId);
|
|
283
|
+
}
|
|
284
|
+
|
|
245
285
|
export function renderSubagentUserPrompt(assignment: string, independentMode: boolean): string {
|
|
246
286
|
return prompt.render(subagentUserPromptTemplate, { assignment: assignment.trim(), independentMode });
|
|
247
287
|
}
|
|
@@ -888,7 +928,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
888
928
|
task,
|
|
889
929
|
assignment,
|
|
890
930
|
progress: { ...progress },
|
|
891
|
-
sessionFile:
|
|
931
|
+
sessionFile: null,
|
|
892
932
|
});
|
|
893
933
|
}
|
|
894
934
|
lastProgressEmitMs = Date.now();
|
|
@@ -1354,9 +1394,13 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1354
1394
|
: (thinkingLevel ?? resolvedThinkingLevel);
|
|
1355
1395
|
effectiveThinkingLevelForWarning = effectiveThinkingLevel;
|
|
1356
1396
|
|
|
1357
|
-
const sessionManager =
|
|
1358
|
-
? await awaitAbortable(
|
|
1359
|
-
:
|
|
1397
|
+
const sessionManager = options.managedPersistence
|
|
1398
|
+
? await awaitAbortable(options.managedPersistence.openSession())
|
|
1399
|
+
: sessionFile
|
|
1400
|
+
? await awaitAbortable(
|
|
1401
|
+
SessionManager.open(sessionFile, SessionManager.explicitDestination(path.dirname(sessionFile))),
|
|
1402
|
+
)
|
|
1403
|
+
: SessionManager.inMemory(worktree ?? cwd);
|
|
1360
1404
|
if (options.parentArtifactManager) {
|
|
1361
1405
|
sessionManager.adoptArtifactManager(options.parentArtifactManager);
|
|
1362
1406
|
}
|
|
@@ -1556,7 +1600,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1556
1600
|
agentSource: agent.source,
|
|
1557
1601
|
description: options.description,
|
|
1558
1602
|
status: "started",
|
|
1559
|
-
sessionFile:
|
|
1603
|
+
sessionFile: null,
|
|
1560
1604
|
index,
|
|
1561
1605
|
});
|
|
1562
1606
|
}
|
|
@@ -1915,37 +1959,38 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1915
1959
|
// Write output artifact (input and jsonl already written in real-time)
|
|
1916
1960
|
// Compute output metadata for agent:// URL integration
|
|
1917
1961
|
let outputMeta: { lineCount: number; charCount: number; byteSize?: number; sha256?: string } | undefined;
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1962
|
+
// Never overwrite the artifact with empty output: a failed/no-op resume leg
|
|
1963
|
+
// carries no new information and must preserve the previous retained output.
|
|
1964
|
+
if (options.artifactsDir && rawOutput.length > 0) {
|
|
1965
|
+
const managedPersistence = options.managedPersistence;
|
|
1921
1966
|
try {
|
|
1922
|
-
await Bun.write(candidateOutputPath, rawOutput);
|
|
1923
1967
|
const byteSize = Buffer.byteLength(rawOutput, "utf8");
|
|
1924
1968
|
const lineCount = rawOutput.split("\n").length;
|
|
1925
1969
|
const sha256 = createHash("sha256").update(rawOutput).digest("hex");
|
|
1926
1970
|
const createdAt = new Date().toISOString();
|
|
1927
|
-
await Bun.write(
|
|
1928
|
-
|
|
1971
|
+
if (!managedPersistence) await Bun.write(path.join(options.artifactsDir, `${id}.md`), rawOutput);
|
|
1972
|
+
const metadataBytes = Buffer.from(
|
|
1929
1973
|
JSON.stringify({ id, kind: "agent-output", sizeBytes: byteSize, lineCount, sha256, createdAt }, null, 2),
|
|
1974
|
+
"utf8",
|
|
1930
1975
|
);
|
|
1931
|
-
|
|
1976
|
+
if (managedPersistence) await managedPersistence.publishOutput(rawOutput, metadataBytes);
|
|
1977
|
+
else await Bun.write(path.join(options.artifactsDir, `${id}.md.meta.json`), metadataBytes);
|
|
1932
1978
|
outputMeta = {
|
|
1933
1979
|
lineCount,
|
|
1934
1980
|
charCount: rawOutput.length,
|
|
1935
1981
|
byteSize,
|
|
1936
1982
|
sha256,
|
|
1937
1983
|
};
|
|
1938
|
-
} catch {
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
// best-effort cleanup; ignore
|
|
1984
|
+
} catch (error) {
|
|
1985
|
+
if (!managedPersistence) {
|
|
1986
|
+
// A missing sidecar keeps a partial immutable output invisible to agent://.
|
|
1987
|
+
outputMeta = undefined;
|
|
1988
|
+
} else {
|
|
1989
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
1990
|
+
stderr = `${stderr}${stderr ? "\n" : ""}Managed output publication failed: ${message}`;
|
|
1991
|
+
exitCode = 1;
|
|
1992
|
+
paused = false;
|
|
1993
|
+
progress.retryFailure = { attempt: 0, errorMessage: `Managed output publication failed: ${message}` };
|
|
1949
1994
|
}
|
|
1950
1995
|
}
|
|
1951
1996
|
}
|
|
@@ -1978,7 +2023,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
1978
2023
|
agentSource: agent.source,
|
|
1979
2024
|
description: options.description,
|
|
1980
2025
|
status: progress.status as "completed" | "failed" | "aborted" | "paused",
|
|
1981
|
-
sessionFile:
|
|
2026
|
+
sessionFile: null,
|
|
1982
2027
|
index,
|
|
1983
2028
|
});
|
|
1984
2029
|
}
|
|
@@ -2008,7 +2053,7 @@ export async function runSubprocess(options: ExecutorOptions): Promise<SingleRes
|
|
|
2008
2053
|
paused,
|
|
2009
2054
|
usage: hasUsage ? accumulatedUsage : undefined,
|
|
2010
2055
|
usageCostBreakdownComplete: hasUsage && usageCostBreakdownComplete ? true : undefined,
|
|
2011
|
-
outputPath,
|
|
2056
|
+
outputPath: undefined,
|
|
2012
2057
|
extractedToolData: progress.extractedToolData,
|
|
2013
2058
|
retryFailure: progress.retryFailure,
|
|
2014
2059
|
outputMeta,
|
package/src/task/index.ts
CHANGED
|
@@ -43,11 +43,11 @@ import {
|
|
|
43
43
|
|
|
44
44
|
// Import review tools for side effects (registers subagent tool handlers)
|
|
45
45
|
import "../tools/review";
|
|
46
|
-
import type
|
|
46
|
+
import { initializeLocalRoot, type LocalProtocolOptions, resolveLocalUrlToPath } from "../internal-urls";
|
|
47
47
|
import { generateCommitMessage } from "../utils/commit-message-generator";
|
|
48
48
|
import * as git from "../utils/git";
|
|
49
49
|
import { discoverAgents, filterVisibleAgents, getAgent } from "./discovery";
|
|
50
|
-
import { renderSubagentUserPrompt, runSubprocess } from "./executor";
|
|
50
|
+
import { createManagedTaskPersistence, renderSubagentUserPrompt, runSubprocess } from "./executor";
|
|
51
51
|
import { adviseForkContextMode } from "./fork-context-advisory";
|
|
52
52
|
import { FORK_CONTEXT_TOKEN_BUDGET_BY_MODE } from "./fork-context-budget";
|
|
53
53
|
import { getTaskIdValidationError, validateAllocatedTaskId } from "./id";
|
|
@@ -581,7 +581,15 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
581
581
|
);
|
|
582
582
|
const finalText = result.content.find(part => part.type === "text")?.text ?? "(no output)";
|
|
583
583
|
const singleResult = result.details?.results[0];
|
|
584
|
-
|
|
584
|
+
if (singleResult?.paused) return { kind: "paused" };
|
|
585
|
+
// A resumed subprocess that aborted or exited non-zero is a failed
|
|
586
|
+
// resume, not a completed one. Throw the rendered failure summary
|
|
587
|
+
// (finalText) so the leg is reported failed instead of being
|
|
588
|
+
// returned as successful continuation.
|
|
589
|
+
if (singleResult && ((singleResult.aborted ?? false) || singleResult.exitCode !== 0)) {
|
|
590
|
+
throw new Error(finalText);
|
|
591
|
+
}
|
|
592
|
+
return finalText;
|
|
585
593
|
},
|
|
586
594
|
{
|
|
587
595
|
id: `${descriptor.task.id}-resume-${Snowflake.next()}`,
|
|
@@ -623,8 +631,20 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
623
631
|
});
|
|
624
632
|
};
|
|
625
633
|
const frozenForkSeeds = new Map<string, ForkContextSeed>();
|
|
634
|
+
const asyncParentArtifactManager = this.session.getArtifactManager?.() ?? undefined;
|
|
626
635
|
const parentSessionFileForBatch = this.session.getSessionFile();
|
|
627
636
|
const batchArtifactsDir = parentSessionFileForBatch ? parentSessionFileForBatch.slice(0, -6) : null;
|
|
637
|
+
let externalTaskSessionsDir: string | undefined;
|
|
638
|
+
if (!batchArtifactsDir) {
|
|
639
|
+
const asyncLocalOptions: LocalProtocolOptions = {
|
|
640
|
+
getArtifactsDir: this.session.getArtifactsDir ?? (() => null),
|
|
641
|
+
isManagedDestination: this.session.isManagedSessionDestination,
|
|
642
|
+
getSessionId: this.session.getSessionId ?? (() => null),
|
|
643
|
+
};
|
|
644
|
+
await initializeLocalRoot(asyncLocalOptions);
|
|
645
|
+
externalTaskSessionsDir = resolveLocalUrlToPath("local://subagents/sessions", asyncLocalOptions);
|
|
646
|
+
await fs.mkdir(externalTaskSessionsDir, { recursive: true, mode: 0o700 });
|
|
647
|
+
}
|
|
628
648
|
|
|
629
649
|
for (let i = 0; i < taskItems.length; i++) {
|
|
630
650
|
const taskItem = taskItems[i];
|
|
@@ -652,7 +672,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
652
672
|
const singleParams: TaskParams = { ...params, tasks: [taskItem] };
|
|
653
673
|
const label = uniqueId;
|
|
654
674
|
try {
|
|
655
|
-
const
|
|
675
|
+
const managedPersistence = asyncParentArtifactManager?.getManagedStore()
|
|
676
|
+
? createManagedTaskPersistence(asyncParentArtifactManager, uniqueId)
|
|
677
|
+
: undefined;
|
|
678
|
+
const subtaskSessionFile = managedPersistence
|
|
679
|
+
? null
|
|
680
|
+
: path.join(batchArtifactsDir ?? externalTaskSessionsDir!, `${uniqueId}.jsonl`);
|
|
656
681
|
const jobId = manager.register(
|
|
657
682
|
"task",
|
|
658
683
|
label,
|
|
@@ -810,8 +835,8 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
810
835
|
currentJobId: jobId,
|
|
811
836
|
historicalJobIds: [],
|
|
812
837
|
status: manager.getJob(jobId)?.status ?? "running",
|
|
813
|
-
sessionFile:
|
|
814
|
-
resumable:
|
|
838
|
+
sessionFile: null,
|
|
839
|
+
resumable: true,
|
|
815
840
|
});
|
|
816
841
|
}
|
|
817
842
|
} catch (error) {
|
|
@@ -1132,6 +1157,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1132
1157
|
// Share the parent session's local:// root with subagents so they read/write the same scratch space
|
|
1133
1158
|
const localProtocolOptions: LocalProtocolOptions = {
|
|
1134
1159
|
getArtifactsDir: this.session.getArtifactsDir ?? (() => null),
|
|
1160
|
+
isManagedDestination: this.session.isManagedSessionDestination,
|
|
1135
1161
|
getSessionId: this.session.getSessionId ?? (() => null),
|
|
1136
1162
|
};
|
|
1137
1163
|
|
|
@@ -1141,6 +1167,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1141
1167
|
|
|
1142
1168
|
// Initialize progress tracking
|
|
1143
1169
|
const progressMap = new Map<number, AgentProgress>();
|
|
1170
|
+
const isolatedPatchBytes = new Map<string, Buffer>();
|
|
1144
1171
|
|
|
1145
1172
|
// Update callback
|
|
1146
1173
|
const emitProgress = () => {
|
|
@@ -1201,14 +1228,16 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1201
1228
|
};
|
|
1202
1229
|
}
|
|
1203
1230
|
|
|
1204
|
-
//
|
|
1205
|
-
//
|
|
1206
|
-
|
|
1231
|
+
// Place fork-context handoff material in the session-scoped external local
|
|
1232
|
+
// root. Subagent prompts may expose this path to subprocesses, so it must
|
|
1233
|
+
// never carry managed transcript/artifact authority.
|
|
1207
1234
|
const shouldWriteConversationContext = !hasAvailableIrcTool(this.session);
|
|
1208
1235
|
const compactContext = shouldWriteConversationContext ? this.session.getCompactContext?.() : undefined;
|
|
1209
1236
|
let contextFilePath: string | undefined;
|
|
1210
1237
|
if (compactContext) {
|
|
1211
|
-
|
|
1238
|
+
await initializeLocalRoot(localProtocolOptions);
|
|
1239
|
+
contextFilePath = resolveLocalUrlToPath("local://subagents/context.md", localProtocolOptions);
|
|
1240
|
+
await fs.mkdir(path.dirname(contextFilePath), { recursive: true, mode: 0o700 });
|
|
1212
1241
|
await Bun.write(contextFilePath, compactContext);
|
|
1213
1242
|
}
|
|
1214
1243
|
|
|
@@ -1307,7 +1336,12 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1307
1336
|
recommendedMode: advisory.recommendedMode,
|
|
1308
1337
|
reasons: advisory.reasons,
|
|
1309
1338
|
};
|
|
1310
|
-
const
|
|
1339
|
+
const managedPersistence = parentArtifactManager?.getManagedStore()
|
|
1340
|
+
? createManagedTaskPersistence(parentArtifactManager, task.id)
|
|
1341
|
+
: undefined;
|
|
1342
|
+
const taskSessionFile = managedPersistence
|
|
1343
|
+
? null
|
|
1344
|
+
: (overrides?.sessionFile ?? executionOverrides?.sessionFiles?.get(task.id) ?? null);
|
|
1311
1345
|
if (!isIsolated) {
|
|
1312
1346
|
const result = await runSubprocess({
|
|
1313
1347
|
cwd: this.session.cwd,
|
|
@@ -1330,6 +1364,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1330
1364
|
sessionFile: taskSessionFile,
|
|
1331
1365
|
persistArtifacts: !!artifactsDir,
|
|
1332
1366
|
artifactsDir: effectiveArtifactsDir,
|
|
1367
|
+
managedPersistence,
|
|
1333
1368
|
contextFile: contextFilePath,
|
|
1334
1369
|
ircAvailable: hasAvailableIrcTool(this.session),
|
|
1335
1370
|
enableLsp: subagentLspEnabled,
|
|
@@ -1394,6 +1429,7 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1394
1429
|
sessionFile: taskSessionFile,
|
|
1395
1430
|
persistArtifacts: !!artifactsDir,
|
|
1396
1431
|
artifactsDir: effectiveArtifactsDir,
|
|
1432
|
+
managedPersistence,
|
|
1397
1433
|
contextFile: contextFilePath,
|
|
1398
1434
|
ircAvailable: hasAvailableIrcTool(this.session),
|
|
1399
1435
|
enableLsp: subagentLspEnabled,
|
|
@@ -1465,9 +1501,15 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1465
1501
|
if (resultWithForkContext.exitCode === 0) {
|
|
1466
1502
|
try {
|
|
1467
1503
|
const delta = await captureDeltaPatch(isolationDir, taskBaseline);
|
|
1504
|
+
await initializeLocalRoot(localProtocolOptions);
|
|
1468
1505
|
const artifactId = validateAllocatedTaskId(task.id);
|
|
1469
|
-
const patchPath =
|
|
1506
|
+
const patchPath = resolveLocalUrlToPath(
|
|
1507
|
+
`local://subagents/${artifactId}.patch`,
|
|
1508
|
+
localProtocolOptions,
|
|
1509
|
+
);
|
|
1510
|
+
await fs.mkdir(path.dirname(patchPath), { recursive: true, mode: 0o700 });
|
|
1470
1511
|
await Bun.write(patchPath, delta.rootPatch);
|
|
1512
|
+
isolatedPatchBytes.set(patchPath, Buffer.from(delta.rootPatch, "utf8"));
|
|
1471
1513
|
const producedChanges = Boolean(delta.rootPatch.trim() || delta.nestedPatches.length);
|
|
1472
1514
|
return {
|
|
1473
1515
|
...resultWithForkContext,
|
|
@@ -1622,19 +1664,20 @@ export class TaskTool implements AgentTool<TaskToolSchemaInstance, TaskToolDetai
|
|
|
1622
1664
|
changesApplied = false;
|
|
1623
1665
|
hadAnyChanges = false;
|
|
1624
1666
|
} else {
|
|
1625
|
-
const patchStats =
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1667
|
+
const patchStats = patchesInOrder.map(patchPath => ({
|
|
1668
|
+
patchPath,
|
|
1669
|
+
size: isolatedPatchBytes.get(patchPath)?.byteLength ?? -1,
|
|
1670
|
+
}));
|
|
1671
|
+
if (patchStats.some(patch => patch.size < 0)) {
|
|
1672
|
+
throw new Error("Captured isolated patch bytes are unavailable");
|
|
1673
|
+
}
|
|
1631
1674
|
const nonEmptyPatches = patchStats.filter(patch => patch.size > 0).map(patch => patch.patchPath);
|
|
1632
1675
|
if (nonEmptyPatches.length === 0) {
|
|
1633
1676
|
changesApplied = true;
|
|
1634
1677
|
hadAnyChanges = false;
|
|
1635
1678
|
} else {
|
|
1636
|
-
const patchTexts =
|
|
1637
|
-
|
|
1679
|
+
const patchTexts = nonEmptyPatches.map(patchPath =>
|
|
1680
|
+
isolatedPatchBytes.get(patchPath)!.toString("utf8"),
|
|
1638
1681
|
);
|
|
1639
1682
|
const combinedPatch = patchTexts
|
|
1640
1683
|
.map(text => (text.endsWith("\n") ? text : `${text}\n`))
|