@bermudi/pi-delegate 0.1.3 → 0.1.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/README.md +27 -1
- package/config.ts +1 -1
- package/delegate.ts +3 -0
- package/format.ts +5 -1
- package/host-compat.ts +1 -0
- package/lifecycle.ts +190 -12
- package/package.json +1 -1
- package/schema.ts +15 -1
- package/task-resolution.ts +6 -0
- package/telemetry.ts +8 -3
- package/types.ts +5 -0
- package/workspace.ts +908 -0
package/README.md
CHANGED
|
@@ -38,6 +38,31 @@ Parent extension/MCP tools are not copied, and project instructions are rebuilt
|
|
|
38
38
|
for the task's `cwd`. Omit `agent` when you want an ad-hoc task using delegate's
|
|
39
39
|
normal inline defaults instead.
|
|
40
40
|
|
|
41
|
+
### Disposable scratch workspace
|
|
42
|
+
|
|
43
|
+
For review, tests, or other commands whose project changes should be thrown
|
|
44
|
+
away, run a one-shot task in a CoW copy:
|
|
45
|
+
|
|
46
|
+
```ts
|
|
47
|
+
delegate({
|
|
48
|
+
tasks: [
|
|
49
|
+
{ prompt: "Review this change and run its tests", workspace: "scratch" },
|
|
50
|
+
],
|
|
51
|
+
});
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
Delegate reflink-copies the containing Git repository beside the original, runs
|
|
55
|
+
the subagent in the corresponding copied directory, then deletes the copy. It
|
|
56
|
+
requires Linux with `/proc/self/fd`, GNU `cp`, and a reflink-capable filesystem
|
|
57
|
+
such as Btrfs; it never falls back to an expensive full copy. Scratch mode
|
|
58
|
+
cannot use `sessionId`,
|
|
59
|
+
`resumeFrom`, session actions, linked Git worktrees, or project symlinks that
|
|
60
|
+
point outside the copied tree.
|
|
61
|
+
|
|
62
|
+
This protects the real project from ordinary relative writes. It is not a
|
|
63
|
+
security sandbox: unrestricted commands and absolute paths can still reach the
|
|
64
|
+
host filesystem.
|
|
65
|
+
|
|
41
66
|
### Token accounting
|
|
42
67
|
|
|
43
68
|
Sync delegate calls report aggregate subagent `Usage` on the tool result, so Pi
|
|
@@ -98,7 +123,8 @@ over an installed extension.
|
|
|
98
123
|
|
|
99
124
|
- **Delegate task** — One item in `delegate({ tasks: [...] })`. This is the
|
|
100
125
|
core unit of work: a prompt plus optional overrides such as `agent`, `tools`,
|
|
101
|
-
`systemPrompt`, `thinking`, `cwd`, `context`, `sessionId`, or
|
|
126
|
+
`systemPrompt`, `thinking`, `cwd`, `context`, `workspace`, `sessionId`, or
|
|
127
|
+
`resumeFrom`.
|
|
102
128
|
`model` is also accepted but should be rare — subagents inherit the parent
|
|
103
129
|
model by default.
|
|
104
130
|
- **Default subagent** — The reserved built-in `agent: "default"` profile. It
|
package/config.ts
CHANGED
|
@@ -131,7 +131,7 @@ function normalizeProviderExtensions(
|
|
|
131
131
|
// `providerExtensions` replaces a provider's entries (it does not append); an
|
|
132
132
|
// empty array is ignored so the default persists.
|
|
133
133
|
const DEFAULT_PROVIDER_EXTENSIONS: Record<string, readonly string[]> = {
|
|
134
|
-
"openai-codex": ["
|
|
134
|
+
"openai-codex": ["git:github.com/bermudi/manaflow-pi-codex"],
|
|
135
135
|
};
|
|
136
136
|
|
|
137
137
|
function resolveProviderExtensions(
|
package/delegate.ts
CHANGED
|
@@ -3,6 +3,7 @@ export { default } from "./extension.ts";
|
|
|
3
3
|
export type {
|
|
4
4
|
AgentConfig,
|
|
5
5
|
SessionAction,
|
|
6
|
+
WorkspaceMode,
|
|
6
7
|
TicketAction,
|
|
7
8
|
DelegateAction,
|
|
8
9
|
DelegateArguments,
|
|
@@ -77,6 +78,8 @@ export {
|
|
|
77
78
|
isCrossLeafTicket,
|
|
78
79
|
} from "./leaf.ts";
|
|
79
80
|
export { runAgentSession } from "./runner.ts";
|
|
81
|
+
export { createScratchWorkspace } from "./workspace.ts";
|
|
82
|
+
export type { ScratchWorkspace } from "./workspace.ts";
|
|
80
83
|
export {
|
|
81
84
|
activeTicketSummary,
|
|
82
85
|
buildStatusText,
|
package/format.ts
CHANGED
|
@@ -7,6 +7,7 @@ import type {
|
|
|
7
7
|
TaskProgress,
|
|
8
8
|
TaskResult,
|
|
9
9
|
ToolActivity,
|
|
10
|
+
WorkspaceMode,
|
|
10
11
|
} from "./types.ts";
|
|
11
12
|
|
|
12
13
|
const SPINNER = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
|
|
@@ -517,7 +518,10 @@ export function relativeTouchedSummary(
|
|
|
517
518
|
* in the same repository do not fabricate false conflicts from shared
|
|
518
519
|
* repository-wide git snapshots. */
|
|
519
520
|
export function findTouchedOverlaps(
|
|
520
|
-
results: readonly {
|
|
521
|
+
results: readonly {
|
|
522
|
+
attributedFiles?: string[];
|
|
523
|
+
workspace?: WorkspaceMode;
|
|
524
|
+
}[],
|
|
521
525
|
): string[] {
|
|
522
526
|
const counts = new Map<string, number>();
|
|
523
527
|
for (const r of results) {
|
package/host-compat.ts
CHANGED
|
@@ -23,6 +23,7 @@ const REQUIRED_EXPORTS: ExportCheck[] = [
|
|
|
23
23
|
{ name: "SettingsManager", requiredMember: "create" },
|
|
24
24
|
{ name: "SessionManager", requiredMember: "create" },
|
|
25
25
|
{ name: "SessionManager", requiredMember: "open" },
|
|
26
|
+
{ name: "SessionManager", requiredMember: "inMemory" },
|
|
26
27
|
{ name: "DefaultResourceLoader" },
|
|
27
28
|
{ name: "DefaultPackageManager" },
|
|
28
29
|
{ name: "createAgentSession" },
|
package/lifecycle.ts
CHANGED
|
@@ -28,6 +28,7 @@ import { getWholeTaskMaxRetries, getWholeTaskBaseDelayMs } from "./config.ts";
|
|
|
28
28
|
import { addUsage, emptyUsage } from "./usage.ts";
|
|
29
29
|
import { scheduleDeadline } from "./timer.ts";
|
|
30
30
|
import { recordTask } from "./telemetry.ts";
|
|
31
|
+
import { createScratchWorkspace, ScratchDeadlineError } from "./workspace.ts";
|
|
31
32
|
|
|
32
33
|
/** Internal seam for lifecycle-level tests without replacing session ownership. */
|
|
33
34
|
type RunAgentSession = typeof runAgentSession;
|
|
@@ -181,6 +182,11 @@ function updateProgressFromResult(p: TaskProgress, r: TaskResult): void {
|
|
|
181
182
|
p.failureKind = r.failureKind;
|
|
182
183
|
}
|
|
183
184
|
|
|
185
|
+
const taskRetryCounts = new WeakMap<TaskResult, number>();
|
|
186
|
+
/** The SQLite task id is part of the logical task, not the transient result
|
|
187
|
+
* object. Scratch wrapping clones results, so keep this alongside retry data. */
|
|
188
|
+
const taskTelemetryIds = new WeakMap<TaskResult, string>();
|
|
189
|
+
|
|
184
190
|
/** Apply a TaskResult to progress and notify the env (sync fires onUpdate).
|
|
185
191
|
* Used at every return point in runResolvedTask — mirrors the old fire() pattern
|
|
186
192
|
* that the duplicated sync/async bodies used after every early-return. */
|
|
@@ -192,8 +198,9 @@ function finishTask(
|
|
|
192
198
|
retries = 0,
|
|
193
199
|
): TaskResult {
|
|
194
200
|
updateProgressFromResult(p, r);
|
|
201
|
+
taskRetryCounts.set(r, retries);
|
|
195
202
|
if (env.telemetryCallId) {
|
|
196
|
-
recordTask({
|
|
203
|
+
const id = recordTask({
|
|
197
204
|
callId: env.telemetryCallId,
|
|
198
205
|
generation: env.telemetryGeneration,
|
|
199
206
|
async: env.async ?? false,
|
|
@@ -203,6 +210,7 @@ function finishTask(
|
|
|
203
210
|
result: r,
|
|
204
211
|
retries,
|
|
205
212
|
});
|
|
213
|
+
if (id) taskTelemetryIds.set(r, id);
|
|
206
214
|
}
|
|
207
215
|
env.onStatusChange?.();
|
|
208
216
|
return r;
|
|
@@ -494,15 +502,24 @@ async function acquireAgentSession(
|
|
|
494
502
|
}
|
|
495
503
|
|
|
496
504
|
// ── Fresh session (no resume) ────────────────────────────────────────────
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
505
|
+
if (task.workspace === "scratch") {
|
|
506
|
+
// A discarded filesystem must not advertise a resumable conversation: a
|
|
507
|
+
// later resume would run against the source cwd and silently lose scratch
|
|
508
|
+
// isolation. Keep scratch transcripts in memory only.
|
|
509
|
+
sessionManager = SessionManager.inMemory(task.cwd);
|
|
510
|
+
} else {
|
|
511
|
+
const fresh = createSubagentSessionManager(
|
|
512
|
+
env.parentSessionManager,
|
|
513
|
+
task.cwd,
|
|
514
|
+
);
|
|
515
|
+
if (!fresh) {
|
|
516
|
+
return {
|
|
517
|
+
error: failTask(task, "Internal: could not create session file"),
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
sessionManager = fresh.manager;
|
|
521
|
+
sessionFile = fresh.file;
|
|
503
522
|
}
|
|
504
|
-
sessionManager = fresh.manager;
|
|
505
|
-
sessionFile = fresh.file;
|
|
506
523
|
|
|
507
524
|
const session = await buildDelegateSession(
|
|
508
525
|
task,
|
|
@@ -564,6 +581,166 @@ async function runResolvedTaskUnlocked(
|
|
|
564
581
|
task: ResolvedTask,
|
|
565
582
|
p: TaskProgress,
|
|
566
583
|
taskIndex: number,
|
|
584
|
+
): Promise<TaskResult> {
|
|
585
|
+
if (task.workspace !== "scratch") {
|
|
586
|
+
return runResolvedTaskCore(env, task, p, taskIndex);
|
|
587
|
+
}
|
|
588
|
+
if (task.sessionId || task.resumeFrom || task.sessionAction) {
|
|
589
|
+
return finishTask(
|
|
590
|
+
env,
|
|
591
|
+
p,
|
|
592
|
+
failTask(
|
|
593
|
+
task,
|
|
594
|
+
"workspace 'scratch' is one-shot and cannot be combined with sessionId, resumeFrom, or sessionAction.",
|
|
595
|
+
),
|
|
596
|
+
task,
|
|
597
|
+
);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
const startedAt = Date.now();
|
|
601
|
+
const deadlineAt =
|
|
602
|
+
task.deadlineMs && task.deadlineMs > 0
|
|
603
|
+
? startedAt + task.deadlineMs
|
|
604
|
+
: undefined;
|
|
605
|
+
let workspace: Awaited<ReturnType<typeof createScratchWorkspace>>;
|
|
606
|
+
try {
|
|
607
|
+
workspace = await createScratchWorkspace(task.cwd, env.signal, deadlineAt);
|
|
608
|
+
} catch (error) {
|
|
609
|
+
const setupError = error instanceof Error ? error.message : String(error);
|
|
610
|
+
const deadlineExceeded =
|
|
611
|
+
!env.signal?.aborted && error instanceof ScratchDeadlineError;
|
|
612
|
+
return finishTask(
|
|
613
|
+
env,
|
|
614
|
+
p,
|
|
615
|
+
{
|
|
616
|
+
...failTask(
|
|
617
|
+
task,
|
|
618
|
+
env.signal?.aborted
|
|
619
|
+
? "Aborted"
|
|
620
|
+
: deadlineExceeded
|
|
621
|
+
? formatDeadlineExceededError(task.deadlineMs ?? 0)
|
|
622
|
+
: setupError,
|
|
623
|
+
),
|
|
624
|
+
failureKind: deadlineExceeded ? "deadline_exceeded" : undefined,
|
|
625
|
+
durationMs: Date.now() - startedAt,
|
|
626
|
+
},
|
|
627
|
+
task,
|
|
628
|
+
);
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
const executionTask: ResolvedTask = {
|
|
632
|
+
...task,
|
|
633
|
+
cwd: workspace.cwd,
|
|
634
|
+
};
|
|
635
|
+
let result: TaskResult | undefined;
|
|
636
|
+
let cleanupError: string | undefined;
|
|
637
|
+
let needsCorrection = false;
|
|
638
|
+
let telemetryTaskId: string | undefined;
|
|
639
|
+
let retries = 0;
|
|
640
|
+
try {
|
|
641
|
+
const preMappingResult = await runResolvedTaskCore(
|
|
642
|
+
env,
|
|
643
|
+
executionTask,
|
|
644
|
+
p,
|
|
645
|
+
taskIndex,
|
|
646
|
+
{
|
|
647
|
+
taskStartedAt: startedAt,
|
|
648
|
+
deadlineAt,
|
|
649
|
+
},
|
|
650
|
+
);
|
|
651
|
+
result = preMappingResult;
|
|
652
|
+
// Capture metadata before scratch wrapping creates a new result object.
|
|
653
|
+
// Both values belong to the logical task and must survive that clone.
|
|
654
|
+
telemetryTaskId = taskTelemetryIds.get(result);
|
|
655
|
+
retries = taskRetryCounts.get(result) ?? 0;
|
|
656
|
+
result = {
|
|
657
|
+
...result,
|
|
658
|
+
workspace: "scratch",
|
|
659
|
+
sessionFile: undefined,
|
|
660
|
+
touchedFiles: await Promise.all(
|
|
661
|
+
result.touchedFiles.map((file) => workspace.resolveReportedPath(file)),
|
|
662
|
+
),
|
|
663
|
+
// Writes inside scratch are discarded and cannot conflict. Explicit
|
|
664
|
+
// writes outside scratch (for example an absolute host path) persist and
|
|
665
|
+
// must remain attributable for overlap warnings. Resolve those paths
|
|
666
|
+
// physically so aliases to the same host file compare equally.
|
|
667
|
+
attributedFiles: (
|
|
668
|
+
await Promise.all(
|
|
669
|
+
(result.attributedFiles ?? []).map((file) =>
|
|
670
|
+
workspace.resolveAttributedPath(file),
|
|
671
|
+
),
|
|
672
|
+
)
|
|
673
|
+
).filter((file): file is string => file !== undefined),
|
|
674
|
+
};
|
|
675
|
+
} catch (error) {
|
|
676
|
+
result = {
|
|
677
|
+
...failTask(task, error instanceof Error ? error.message : String(error)),
|
|
678
|
+
...(result
|
|
679
|
+
? {
|
|
680
|
+
tokens: result.tokens,
|
|
681
|
+
usage: result.usage,
|
|
682
|
+
}
|
|
683
|
+
: {}),
|
|
684
|
+
workspace: "scratch",
|
|
685
|
+
durationMs: Date.now() - startedAt,
|
|
686
|
+
};
|
|
687
|
+
// runResolvedTaskCore may already have recorded/notified a successful
|
|
688
|
+
// result before path mapping failed. Correct those observable outcomes.
|
|
689
|
+
needsCorrection = true;
|
|
690
|
+
} finally {
|
|
691
|
+
try {
|
|
692
|
+
await workspace.cleanup();
|
|
693
|
+
} catch (error) {
|
|
694
|
+
cleanupError = `Scratch workspace cleanup failed: ${error instanceof Error ? error.message : String(error)}`;
|
|
695
|
+
console.error("[delegate] scratch workspace cleanup failed", error);
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (!result) {
|
|
700
|
+
result = {
|
|
701
|
+
...failTask(task, "Scratch task failed before producing a result."),
|
|
702
|
+
workspace: "scratch",
|
|
703
|
+
durationMs: Date.now() - startedAt,
|
|
704
|
+
};
|
|
705
|
+
}
|
|
706
|
+
if (cleanupError || needsCorrection) {
|
|
707
|
+
result = {
|
|
708
|
+
...result,
|
|
709
|
+
error: cleanupError
|
|
710
|
+
? result.error
|
|
711
|
+
? `${result.error}\n${cleanupError}`
|
|
712
|
+
: cleanupError
|
|
713
|
+
: result.error,
|
|
714
|
+
durationMs: Math.max(result.durationMs, Date.now() - startedAt),
|
|
715
|
+
};
|
|
716
|
+
updateProgressFromResult(p, result);
|
|
717
|
+
if (env.telemetryCallId) {
|
|
718
|
+
// runResolvedTaskCore already recorded the pre-cleanup result. Telemetry
|
|
719
|
+
// rows are upserts, so replace it with the actual returned outcome while
|
|
720
|
+
// preserving the retry count captured by finishTask.
|
|
721
|
+
recordTask({
|
|
722
|
+
id: telemetryTaskId,
|
|
723
|
+
callId: env.telemetryCallId,
|
|
724
|
+
generation: env.telemetryGeneration,
|
|
725
|
+
async: env.async ?? false,
|
|
726
|
+
taskIndex: p.index,
|
|
727
|
+
task,
|
|
728
|
+
progress: p,
|
|
729
|
+
result,
|
|
730
|
+
retries,
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
env.onStatusChange?.();
|
|
734
|
+
}
|
|
735
|
+
return result;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
async function runResolvedTaskCore(
|
|
739
|
+
env: TaskRunEnv,
|
|
740
|
+
task: ResolvedTask,
|
|
741
|
+
p: TaskProgress,
|
|
742
|
+
taskIndex: number,
|
|
743
|
+
timing?: { taskStartedAt: number; deadlineAt: number | undefined },
|
|
567
744
|
): Promise<TaskResult> {
|
|
568
745
|
try {
|
|
569
746
|
// ── Aborted before we started? ───────────────────────────────────
|
|
@@ -629,11 +806,12 @@ async function runResolvedTaskUnlocked(
|
|
|
629
806
|
let hasBashExecution = false;
|
|
630
807
|
let cumulativeTokens = 0;
|
|
631
808
|
let cumulativeToolUses = 0;
|
|
632
|
-
const taskStartedAt = Date.now();
|
|
809
|
+
const taskStartedAt = timing?.taskStartedAt ?? Date.now();
|
|
633
810
|
const deadlineAt =
|
|
634
|
-
|
|
811
|
+
timing?.deadlineAt ??
|
|
812
|
+
(task.deadlineMs && task.deadlineMs > 0
|
|
635
813
|
? taskStartedAt + task.deadlineMs
|
|
636
|
-
: undefined;
|
|
814
|
+
: undefined);
|
|
637
815
|
let accumulatedUsage = emptyUsage();
|
|
638
816
|
|
|
639
817
|
const onAttemptProgress = (u: AgentProgressUpdate): void => {
|
package/package.json
CHANGED
package/schema.ts
CHANGED
|
@@ -103,6 +103,13 @@ export const delegateTaskSchema = Type.Object({
|
|
|
103
103
|
"Wall-clock budget (ms) from run start after queueing. Cooperative abort; side effects remain. Omit disables.",
|
|
104
104
|
}),
|
|
105
105
|
),
|
|
106
|
+
workspace: Type.Optional(
|
|
107
|
+
StringEnum(["shared", "scratch"], {
|
|
108
|
+
description:
|
|
109
|
+
"scratch=disposable CoW project copy; relative edits are discarded. Not security isolation. Default=shared.",
|
|
110
|
+
default: "shared",
|
|
111
|
+
}),
|
|
112
|
+
),
|
|
106
113
|
});
|
|
107
114
|
|
|
108
115
|
// Single source of truth for registration and generated help. The exported
|
|
@@ -146,7 +153,7 @@ export const delegateArgumentsSchema = Type.Object({
|
|
|
146
153
|
Type.Array(delegateTaskSchema, {
|
|
147
154
|
minItems: 0,
|
|
148
155
|
description:
|
|
149
|
-
"Tasks
|
|
156
|
+
"Tasks run concurrently; shared workspaces share files. scratch uses a disposable CoW copy. []=full manual.",
|
|
150
157
|
}),
|
|
151
158
|
),
|
|
152
159
|
});
|
|
@@ -167,6 +174,7 @@ const TASK_FIELD_NAMES = [
|
|
|
167
174
|
"sessionAction",
|
|
168
175
|
"resumeFrom",
|
|
169
176
|
"deadlineMs",
|
|
177
|
+
"workspace",
|
|
170
178
|
] as const;
|
|
171
179
|
|
|
172
180
|
/** Every field a task entry may carry. Anything else is a model mistake —
|
|
@@ -317,6 +325,12 @@ export function validateDelegateOperation(
|
|
|
317
325
|
if (typeof rawTask.deadlineMs === "number" && !(rawTask.deadlineMs > 0)) {
|
|
318
326
|
return `task ${index + 1}: deadlineMs must be a positive number of milliseconds.`;
|
|
319
327
|
}
|
|
328
|
+
if (
|
|
329
|
+
task.workspace === "scratch" &&
|
|
330
|
+
(task.sessionId || task.resumeFrom || sessionAction !== undefined)
|
|
331
|
+
) {
|
|
332
|
+
return `task ${index + 1}: workspace 'scratch' is one-shot and cannot be combined with sessionId, resumeFrom, or sessionAction.`;
|
|
333
|
+
}
|
|
320
334
|
if (sessionAction === "close") {
|
|
321
335
|
if (!task.sessionId) {
|
|
322
336
|
return `task ${index + 1}: sessionAction 'close' requires sessionId.`;
|
package/task-resolution.ts
CHANGED
|
@@ -208,6 +208,11 @@ export function resolveTasks(
|
|
|
208
208
|
);
|
|
209
209
|
let tools: string[] = [];
|
|
210
210
|
const warnings: string[] = [];
|
|
211
|
+
if (t.workspace === "scratch") {
|
|
212
|
+
warnings.push(
|
|
213
|
+
"Scratch workspace: relative file changes run in a disposable CoW copy and are discarded.",
|
|
214
|
+
);
|
|
215
|
+
}
|
|
211
216
|
|
|
212
217
|
// Prompt is required for fresh tasks. ResumeFrom provides context already.
|
|
213
218
|
if (
|
|
@@ -418,6 +423,7 @@ export function resolveTasks(
|
|
|
418
423
|
...t,
|
|
419
424
|
id: t.id,
|
|
420
425
|
cwd,
|
|
426
|
+
workspace: t.workspace ?? "shared",
|
|
421
427
|
systemPrompt,
|
|
422
428
|
model: model!,
|
|
423
429
|
tools,
|
package/telemetry.ts
CHANGED
|
@@ -604,6 +604,8 @@ export function recordCall(record: CallRecord, generation?: number): void {
|
|
|
604
604
|
}
|
|
605
605
|
|
|
606
606
|
export interface TaskSpanInput {
|
|
607
|
+
/** Stable identity for correction writes of the same logical task row. */
|
|
608
|
+
id?: string;
|
|
607
609
|
callId: string;
|
|
608
610
|
/** Runtime generation captured by the dispatch that owns this task. */
|
|
609
611
|
generation?: number;
|
|
@@ -622,13 +624,15 @@ function outcomeFromResult(result: TaskResult): string {
|
|
|
622
624
|
return "success";
|
|
623
625
|
}
|
|
624
626
|
|
|
625
|
-
export function recordTask(input: TaskSpanInput):
|
|
627
|
+
export function recordTask(input: TaskSpanInput): string | undefined {
|
|
626
628
|
const b = getBackend(input.generation);
|
|
627
|
-
if (!b) return;
|
|
629
|
+
if (!b) return undefined;
|
|
628
630
|
|
|
629
631
|
const { callId, async, taskIndex, task, progress, result, retries } = input;
|
|
630
632
|
const record: TaskRecord = {
|
|
631
|
-
|
|
633
|
+
// Correction writes must reuse the provisional row's primary key. A fresh
|
|
634
|
+
// UUID here would make INSERT OR REPLACE append a second task row.
|
|
635
|
+
id: input.id ?? crypto.randomUUID(),
|
|
632
636
|
call_id: callId,
|
|
633
637
|
ts: Date.now(),
|
|
634
638
|
version: getDelegateVersion(),
|
|
@@ -651,6 +655,7 @@ export function recordTask(input: TaskSpanInput): void {
|
|
|
651
655
|
async: async ? 1 : 0,
|
|
652
656
|
};
|
|
653
657
|
b.recordTask(record);
|
|
658
|
+
return record.id;
|
|
654
659
|
}
|
|
655
660
|
|
|
656
661
|
/** Prevent this runtime's late workers from writing after a bounded shutdown
|
package/types.ts
CHANGED
|
@@ -43,6 +43,8 @@ export type TicketAction = NonNullable<
|
|
|
43
43
|
>;
|
|
44
44
|
/** Per-task session action: "prompt" | "close" | "list". */
|
|
45
45
|
export type SessionAction = NonNullable<CanonicalTaskDef["sessionAction"]>;
|
|
46
|
+
/** Filesystem mode: shared source tree or an ephemeral CoW scratch copy. */
|
|
47
|
+
export type WorkspaceMode = NonNullable<CanonicalTaskDef["workspace"]>;
|
|
46
48
|
|
|
47
49
|
export type TaskDef = CanonicalTaskDef & {
|
|
48
50
|
/** @deprecated Use `sessionAction` instead. Runtime normalization still accepts this alias. */
|
|
@@ -123,6 +125,7 @@ export interface ResolvedTask {
|
|
|
123
125
|
thinking: ThinkingLevel;
|
|
124
126
|
systemPrompt: string;
|
|
125
127
|
cwd: string;
|
|
128
|
+
workspace?: WorkspaceMode;
|
|
126
129
|
context?: "fresh" | "with-parent-transcript";
|
|
127
130
|
sessionId?: string;
|
|
128
131
|
sessionAction?: SessionAction;
|
|
@@ -211,6 +214,8 @@ export interface TaskResult {
|
|
|
211
214
|
* stay 0 because `getSessionStats()` exposes only the aggregate cost — and
|
|
212
215
|
* Pi sums `cost.total` for nested usage anyway. */
|
|
213
216
|
usage: Usage;
|
|
217
|
+
/** Scratch results are excluded from shared-file conflict detection and never resumable. */
|
|
218
|
+
workspace?: WorkspaceMode;
|
|
214
219
|
sessionFile?: string;
|
|
215
220
|
/** All files the subagent is known to have touched, including bash mutations
|
|
216
221
|
* captured via git diff and other tasks' concurrent git changes. This is a
|