@bermudi/pi-delegate 0.1.2 → 0.1.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/README.md +27 -1
- package/concurrency.ts +20 -3
- package/config.ts +62 -1
- package/delegate.ts +4 -0
- package/dispatch.ts +94 -15
- package/extension.ts +292 -61
- package/format.ts +5 -1
- package/host-compat.ts +1 -0
- package/lifecycle.ts +235 -25
- package/manual.ts +3 -2
- package/package.json +1 -1
- package/schema.ts +16 -2
- package/task-resolution.ts +6 -0
- package/telemetry.ts +743 -0
- package/tickets.ts +34 -10
- package/types.ts +22 -0
- package/usage.ts +19 -0
- package/workspace.ts +672 -0
package/lifecycle.ts
CHANGED
|
@@ -27,6 +27,8 @@ import { resolveCwd, validateResumeFromPath } from "./utils.ts";
|
|
|
27
27
|
import { getWholeTaskMaxRetries, getWholeTaskBaseDelayMs } from "./config.ts";
|
|
28
28
|
import { addUsage, emptyUsage } from "./usage.ts";
|
|
29
29
|
import { scheduleDeadline } from "./timer.ts";
|
|
30
|
+
import { recordTask } from "./telemetry.ts";
|
|
31
|
+
import { createScratchWorkspace, ScratchDeadlineError } from "./workspace.ts";
|
|
30
32
|
|
|
31
33
|
/** Internal seam for lifecycle-level tests without replacing session ownership. */
|
|
32
34
|
type RunAgentSession = typeof runAgentSession;
|
|
@@ -180,6 +182,11 @@ function updateProgressFromResult(p: TaskProgress, r: TaskResult): void {
|
|
|
180
182
|
p.failureKind = r.failureKind;
|
|
181
183
|
}
|
|
182
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
|
+
|
|
183
190
|
/** Apply a TaskResult to progress and notify the env (sync fires onUpdate).
|
|
184
191
|
* Used at every return point in runResolvedTask — mirrors the old fire() pattern
|
|
185
192
|
* that the duplicated sync/async bodies used after every early-return. */
|
|
@@ -187,8 +194,24 @@ function finishTask(
|
|
|
187
194
|
env: TaskRunEnv,
|
|
188
195
|
p: TaskProgress,
|
|
189
196
|
r: TaskResult,
|
|
197
|
+
task: ResolvedTask,
|
|
198
|
+
retries = 0,
|
|
190
199
|
): TaskResult {
|
|
191
200
|
updateProgressFromResult(p, r);
|
|
201
|
+
taskRetryCounts.set(r, retries);
|
|
202
|
+
if (env.telemetryCallId) {
|
|
203
|
+
const id = recordTask({
|
|
204
|
+
callId: env.telemetryCallId,
|
|
205
|
+
generation: env.telemetryGeneration,
|
|
206
|
+
async: env.async ?? false,
|
|
207
|
+
taskIndex: p.index,
|
|
208
|
+
task,
|
|
209
|
+
progress: p,
|
|
210
|
+
result: r,
|
|
211
|
+
retries,
|
|
212
|
+
});
|
|
213
|
+
if (id) taskTelemetryIds.set(r, id);
|
|
214
|
+
}
|
|
192
215
|
env.onStatusChange?.();
|
|
193
216
|
return r;
|
|
194
217
|
}
|
|
@@ -479,15 +502,24 @@ async function acquireAgentSession(
|
|
|
479
502
|
}
|
|
480
503
|
|
|
481
504
|
// ── Fresh session (no resume) ────────────────────────────────────────────
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
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;
|
|
488
522
|
}
|
|
489
|
-
sessionManager = fresh.manager;
|
|
490
|
-
sessionFile = fresh.file;
|
|
491
523
|
|
|
492
524
|
const session = await buildDelegateSession(
|
|
493
525
|
task,
|
|
@@ -549,11 +581,171 @@ async function runResolvedTaskUnlocked(
|
|
|
549
581
|
task: ResolvedTask,
|
|
550
582
|
p: TaskProgress,
|
|
551
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 },
|
|
552
744
|
): Promise<TaskResult> {
|
|
553
745
|
try {
|
|
554
746
|
// ── Aborted before we started? ───────────────────────────────────
|
|
555
747
|
if (env.signal?.aborted) {
|
|
556
|
-
return finishTask(env, p, failTask(task, "Aborted"));
|
|
748
|
+
return finishTask(env, p, failTask(task, "Aborted"), task);
|
|
557
749
|
}
|
|
558
750
|
|
|
559
751
|
// ── Session busy guard (defense-in-depth) ────────────────────────
|
|
@@ -563,7 +755,7 @@ async function runResolvedTaskUnlocked(
|
|
|
563
755
|
const busyTicketId = isSessionBusy(task.sessionId);
|
|
564
756
|
if (busyTicketId && busyTicketId !== env.ticketId) {
|
|
565
757
|
const msg = `Session '${task.sessionId}' is already in use by ticket ${busyTicketId}. Each session can only handle one task at a time.`;
|
|
566
|
-
return finishTask(env, p, failTask(task, msg));
|
|
758
|
+
return finishTask(env, p, failTask(task, msg), task);
|
|
567
759
|
}
|
|
568
760
|
}
|
|
569
761
|
|
|
@@ -577,6 +769,7 @@ async function runResolvedTaskUnlocked(
|
|
|
577
769
|
env,
|
|
578
770
|
p,
|
|
579
771
|
failTask(task, "sessionAction='close' requires sessionId."),
|
|
772
|
+
task,
|
|
580
773
|
);
|
|
581
774
|
}
|
|
582
775
|
// The per-session lock for action-based operations is already held by the
|
|
@@ -593,6 +786,7 @@ async function runResolvedTaskUnlocked(
|
|
|
593
786
|
: `Session '${task.sessionId}' not found.`,
|
|
594
787
|
Date.now() - env.delegateStartedAt,
|
|
595
788
|
),
|
|
789
|
+
task,
|
|
596
790
|
);
|
|
597
791
|
}
|
|
598
792
|
|
|
@@ -605,17 +799,19 @@ async function runResolvedTaskUnlocked(
|
|
|
605
799
|
`Active sessions:\n${pool.listPooledAgents().join("\n")}`,
|
|
606
800
|
Date.now() - env.delegateStartedAt,
|
|
607
801
|
),
|
|
802
|
+
task,
|
|
608
803
|
);
|
|
609
804
|
}
|
|
610
805
|
|
|
611
806
|
let hasBashExecution = false;
|
|
612
807
|
let cumulativeTokens = 0;
|
|
613
808
|
let cumulativeToolUses = 0;
|
|
614
|
-
const taskStartedAt = Date.now();
|
|
809
|
+
const taskStartedAt = timing?.taskStartedAt ?? Date.now();
|
|
615
810
|
const deadlineAt =
|
|
616
|
-
|
|
811
|
+
timing?.deadlineAt ??
|
|
812
|
+
(task.deadlineMs && task.deadlineMs > 0
|
|
617
813
|
? taskStartedAt + task.deadlineMs
|
|
618
|
-
: undefined;
|
|
814
|
+
: undefined);
|
|
619
815
|
let accumulatedUsage = emptyUsage();
|
|
620
816
|
|
|
621
817
|
const onAttemptProgress = (u: AgentProgressUpdate): void => {
|
|
@@ -821,16 +1017,22 @@ async function runResolvedTaskUnlocked(
|
|
|
821
1017
|
task,
|
|
822
1018
|
err instanceof Error ? err.message : String(err),
|
|
823
1019
|
);
|
|
824
|
-
return finishTask(
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
1020
|
+
return finishTask(
|
|
1021
|
+
env,
|
|
1022
|
+
p,
|
|
1023
|
+
{
|
|
1024
|
+
...failure,
|
|
1025
|
+
durationMs: Math.max(failure.durationMs, Date.now() - taskStartedAt),
|
|
1026
|
+
tokens: accumulatedUsage.totalTokens,
|
|
1027
|
+
usage: accumulatedUsage,
|
|
1028
|
+
},
|
|
1029
|
+
task,
|
|
1030
|
+
);
|
|
830
1031
|
}
|
|
831
1032
|
|
|
832
1033
|
const maxRetries = resolvedWholeTaskMaxRetries();
|
|
833
1034
|
const baseDelayMs = resolvedWholeTaskBaseDelayMs();
|
|
1035
|
+
let retriesExecuted = 0;
|
|
834
1036
|
for (
|
|
835
1037
|
let retry = 0;
|
|
836
1038
|
retry < maxRetries && canRetryWholeTask(task, result, hasBashExecution);
|
|
@@ -864,6 +1066,7 @@ async function runResolvedTaskUnlocked(
|
|
|
864
1066
|
p.error = undefined;
|
|
865
1067
|
p.failureKind = undefined;
|
|
866
1068
|
env.onStatusChange?.();
|
|
1069
|
+
retriesExecuted++;
|
|
867
1070
|
try {
|
|
868
1071
|
result = await runAttempt();
|
|
869
1072
|
} catch (err) {
|
|
@@ -881,12 +1084,18 @@ async function runResolvedTaskUnlocked(
|
|
|
881
1084
|
}
|
|
882
1085
|
}
|
|
883
1086
|
|
|
884
|
-
return finishTask(
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
1087
|
+
return finishTask(
|
|
1088
|
+
env,
|
|
1089
|
+
p,
|
|
1090
|
+
{
|
|
1091
|
+
...result,
|
|
1092
|
+
durationMs: Math.max(result.durationMs, Date.now() - taskStartedAt),
|
|
1093
|
+
tokens: Math.max(cumulativeTokens, accumulatedUsage.totalTokens),
|
|
1094
|
+
usage: accumulatedUsage,
|
|
1095
|
+
},
|
|
1096
|
+
task,
|
|
1097
|
+
retriesExecuted,
|
|
1098
|
+
);
|
|
890
1099
|
} catch (err) {
|
|
891
1100
|
// Any acquired session is released by runAttempt's finally before an
|
|
892
1101
|
// exception reaches this boundary. This outer catch handles unexpected
|
|
@@ -896,6 +1105,7 @@ async function runResolvedTaskUnlocked(
|
|
|
896
1105
|
env,
|
|
897
1106
|
p,
|
|
898
1107
|
failTask(task, err instanceof Error ? err.message : String(err)),
|
|
1108
|
+
task,
|
|
899
1109
|
);
|
|
900
1110
|
}
|
|
901
1111
|
}
|
package/manual.ts
CHANGED
|
@@ -185,8 +185,9 @@ export function getSubagentManualMarkdown(
|
|
|
185
185
|
"```",
|
|
186
186
|
"",
|
|
187
187
|
'- `delegate({ ticketAction: "poll" })` \u2014 list all tickets',
|
|
188
|
-
'- `delegate({ ticketAction: "poll", ticket: "abc123" })` \u2014
|
|
189
|
-
'- `delegate({ ticketAction: "wait", ticket: "abc123"
|
|
188
|
+
'- `delegate({ ticketAction: "poll", ticket: "abc123" })` \u2014 take one progress snapshot',
|
|
189
|
+
'- `delegate({ ticketAction: "wait", ticket: "abc123" })` \u2014 block until finished; omit `timeoutMs` when the result is needed this turn',
|
|
190
|
+
'- `delegate({ ticketAction: "wait", ticket: "abc123", timeoutMs: 600000 })` \u2014 bounded wait; timeout includes the latest snapshot, so do not poll afterward',
|
|
190
191
|
'- `delegate({ ticketAction: "cancel", ticket: "abc123" })` \u2014 preview activity and partial effects before cancelling',
|
|
191
192
|
'- `delegate({ ticketAction: "cancel", ticket: "abc123", force: true })` \u2014 abort after review',
|
|
192
193
|
"",
|
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
|
|
@@ -139,14 +146,14 @@ export const delegateArgumentsSchema = Type.Object({
|
|
|
139
146
|
Type.Number({
|
|
140
147
|
minimum: 0,
|
|
141
148
|
description:
|
|
142
|
-
"Bounds wait (ms); omit to block until settled. Timeout
|
|
149
|
+
"Bounds wait (ms); omit to block until settled. Timeout returns a snapshot; do not poll afterward.",
|
|
143
150
|
}),
|
|
144
151
|
),
|
|
145
152
|
tasks: Type.Optional(
|
|
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,
|