@bermudi/pi-delegate 0.1.10 → 0.1.12
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 +57 -4
- package/concurrency.ts +55 -16
- package/config.ts +584 -50
- package/delegate.ts +11 -3
- package/dispatch.ts +436 -65
- package/extension.ts +16 -4
- package/format.ts +52 -6
- package/host-cache.ts +70 -0
- package/host.ts +169 -811
- package/isolated-workspace.ts +857 -0
- package/lifecycle.ts +699 -520
- package/manual.ts +4 -7
- package/package.json +1 -1
- package/pi-package-source.ts +293 -0
- package/pool.ts +23 -1
- package/provider-extensions.ts +537 -0
- package/quiescence.ts +262 -0
- package/render-branches.ts +30 -0
- package/render-result.ts +8 -5
- package/runner.ts +35 -141
- package/schema.ts +133 -202
- package/settings.ts +202 -84
- package/shared-write-safety.ts +273 -0
- package/task-resolution.ts +121 -32
- package/telemetry.ts +135 -68
- package/ticket-format.ts +331 -0
- package/tickets.ts +101 -258
- package/tools.ts +12 -0
- package/trusted-paths.ts +71 -0
- package/types.ts +83 -16
package/schema.ts
CHANGED
|
@@ -7,6 +7,8 @@ import type { DelegateArguments } from "./types.ts";
|
|
|
7
7
|
// `Type.Union([Type.Literal…])` keeps the literals but serializes as `anyOf`,
|
|
8
8
|
// which some providers handle poorly. `Type.Unsafe` gives both: the wire
|
|
9
9
|
// format stays `{ type: "string", enum: [...] }` and the type stays narrow.
|
|
10
|
+
// (TypeBox 0.34's `Type.Enum` targets numeric TS enums, not string arrays, so
|
|
11
|
+
// it is not a drop-in replacement here.)
|
|
10
12
|
function StringEnum<const T extends readonly string[]>(
|
|
11
13
|
values: T,
|
|
12
14
|
options?: SchemaOptions,
|
|
@@ -104,61 +106,63 @@ export const delegateTaskSchema = Type.Object({
|
|
|
104
106
|
}),
|
|
105
107
|
),
|
|
106
108
|
workspace: Type.Optional(
|
|
107
|
-
StringEnum(["shared", "scratch"], {
|
|
109
|
+
StringEnum(["shared", "scratch", "isolated"], {
|
|
108
110
|
description:
|
|
109
|
-
"shared
|
|
111
|
+
"shared; scratch discarded; isolated=sync one-shot Git apply; not security isolation. Reviewer=scratch.",
|
|
110
112
|
}),
|
|
111
113
|
),
|
|
112
114
|
});
|
|
113
115
|
|
|
114
116
|
// Single source of truth for registration and generated help. The exported
|
|
115
|
-
// argument types in types.ts project this canonical schema
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
}
|
|
117
|
+
// argument types in types.ts project this canonical schema; providers see
|
|
118
|
+
// only these fields.
|
|
119
|
+
export const delegateArgumentsSchema = Type.Object(
|
|
120
|
+
{
|
|
121
|
+
ticketAction: Type.Optional(
|
|
122
|
+
StringEnum(["poll", "cancel", "wait"], {
|
|
123
|
+
description:
|
|
124
|
+
"Ticket control: poll=snapshot; wait=block until settled; cancel=abort. Prefer wait; never cancel for time.",
|
|
125
|
+
}),
|
|
126
|
+
),
|
|
127
|
+
async: Type.Optional(
|
|
128
|
+
Type.Boolean({
|
|
129
|
+
description:
|
|
130
|
+
"Detach work, return a ticket; applies to ALL tasks. Results auto-deliver. Wait only if blocked.",
|
|
131
|
+
default: false,
|
|
132
|
+
}),
|
|
133
|
+
),
|
|
134
|
+
ticket: Type.Optional(
|
|
135
|
+
Type.String({
|
|
136
|
+
description: "Ticket ID; omit only when polling all tickets.",
|
|
137
|
+
}),
|
|
138
|
+
),
|
|
139
|
+
force: Type.Optional(
|
|
140
|
+
Type.Boolean({
|
|
141
|
+
description:
|
|
142
|
+
"With cancel: false previews active work; true confirms abort. Completed writes/commands remain.",
|
|
143
|
+
default: false,
|
|
144
|
+
}),
|
|
145
|
+
),
|
|
146
|
+
timeoutMs: Type.Optional(
|
|
147
|
+
Type.Number({
|
|
148
|
+
minimum: 0,
|
|
149
|
+
description:
|
|
150
|
+
"Bounds wait (ms); omit to block until settled. Timeout returns a snapshot; do not poll afterward.",
|
|
151
|
+
}),
|
|
152
|
+
),
|
|
153
|
+
tasks: Type.Optional(
|
|
154
|
+
Type.Array(delegateTaskSchema, {
|
|
155
|
+
minItems: 0,
|
|
156
|
+
description:
|
|
157
|
+
"Tasks run concurrently; shared workspaces share files. scratch uses a disposable CoW copy. []=full manual.",
|
|
158
|
+
}),
|
|
159
|
+
),
|
|
160
|
+
},
|
|
161
|
+
{ additionalProperties: false },
|
|
162
|
+
);
|
|
159
163
|
|
|
160
|
-
/** Fields that belong to a task entry. Models sometimes place these at the
|
|
161
|
-
*
|
|
164
|
+
/** Fields that belong to a task entry. Models sometimes place these at the top
|
|
165
|
+
* level of the arguments; the shim folds them back into a single task. */
|
|
162
166
|
const TASK_FIELD_NAMES = [
|
|
163
167
|
"id",
|
|
164
168
|
"prompt",
|
|
@@ -181,52 +185,26 @@ const TASK_FIELD_NAMES = [
|
|
|
181
185
|
* corrective message instead of being silently ignored (observed in the
|
|
182
186
|
* wild: a task-level `async: true` the caller believed had backgrounded
|
|
183
187
|
* the work while the call in fact ran synchronously). */
|
|
184
|
-
const VALID_TASK_KEYS = new Set<string>(
|
|
185
|
-
|
|
186
|
-
/** Top-level ticket actions the legacy `action` field may map to. */
|
|
187
|
-
const TICKET_ACTIONS = new Set(["poll", "cancel", "wait"]);
|
|
188
|
-
|
|
189
|
-
/** Session actions that are valid at the task level. A flat `action` at the
|
|
190
|
-
* top level may also fold into a wrapped task's `sessionAction`. */
|
|
191
|
-
const TASK_ACTIONS = new Set(["prompt", "close", "list"]);
|
|
192
|
-
|
|
193
|
-
/** Every value the legacy `action` field can carry before it is normalized to
|
|
194
|
-
* `ticketAction` or `sessionAction`. */
|
|
195
|
-
const LEGACY_ACTIONS = new Set([...TICKET_ACTIONS, ...TASK_ACTIONS]);
|
|
188
|
+
const VALID_TASK_KEYS = new Set<string>(TASK_FIELD_NAMES);
|
|
196
189
|
|
|
197
190
|
/** Validate the three operation modes after compatibility reshaping. */
|
|
198
191
|
export function validateDelegateOperation(
|
|
199
192
|
params: DelegateArguments,
|
|
200
193
|
): string | undefined {
|
|
201
194
|
const rawParams = params as Record<string, unknown>;
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
if (hasLegacyAction) {
|
|
208
|
-
if (!LEGACY_ACTIONS.has(rawParams.action as string)) {
|
|
209
|
-
return `unknown action '${rawParams.action}'; valid ticket actions are poll/cancel/wait, valid session actions are prompt/close/list.`;
|
|
210
|
-
}
|
|
211
|
-
if (hasTicketAction) {
|
|
212
|
-
return "ambiguous: supply only ticketAction (or only legacy action), not both.";
|
|
213
|
-
}
|
|
214
|
-
if (TASK_ACTIONS.has(rawParams.action as string) && tasks.length > 0) {
|
|
215
|
-
return `legacy top-level action '${rawParams.action}' cannot be combined with an explicit tasks array; move it into the task's sessionAction or remove tasks.`;
|
|
216
|
-
}
|
|
195
|
+
if ("action" in rawParams) {
|
|
196
|
+
return (
|
|
197
|
+
"unsupported field 'action'; use 'ticketAction' for poll/cancel/wait " +
|
|
198
|
+
"or 'sessionAction' for prompt/close/list."
|
|
199
|
+
);
|
|
217
200
|
}
|
|
201
|
+
const tasks = params.tasks ?? [];
|
|
218
202
|
|
|
219
|
-
const ticketAction
|
|
220
|
-
params.ticketAction ??
|
|
221
|
-
(hasLegacyAction && TICKET_ACTIONS.has(rawParams.action as string)
|
|
222
|
-
? (rawParams.action as string)
|
|
223
|
-
: undefined);
|
|
224
|
-
|
|
203
|
+
const ticketAction = params.ticketAction;
|
|
225
204
|
const isTicketControl = ticketAction !== undefined;
|
|
226
205
|
|
|
227
206
|
if (isTicketControl) {
|
|
228
|
-
const
|
|
229
|
-
const taskIntentFields = topLevelTaskIntentFields.filter(
|
|
207
|
+
const taskIntentFields = ([...TASK_FIELD_NAMES, "tasks"] as const).filter(
|
|
230
208
|
(field) => rawParams[field] !== undefined,
|
|
231
209
|
);
|
|
232
210
|
if (taskIntentFields.length) {
|
|
@@ -263,14 +241,18 @@ export function validateDelegateOperation(
|
|
|
263
241
|
: undefined; // Intentional help request.
|
|
264
242
|
}
|
|
265
243
|
|
|
244
|
+
if (params.async && tasks.some((task) => task.workspace === "isolated")) {
|
|
245
|
+
return 'workspace "isolated" is synchronous; remove async.';
|
|
246
|
+
}
|
|
247
|
+
|
|
266
248
|
// Reject mixed shapes: flat task fields at the top level alongside a
|
|
267
249
|
// nonempty tasks array. The normalize shim only wraps flat fields when
|
|
268
250
|
// there is no tasks array, so a mixed call silently lets tasks win —
|
|
269
251
|
// a model mistake that should fail loudly.
|
|
270
252
|
if (tasks.length > 0) {
|
|
271
|
-
const flatTaskFields =
|
|
272
|
-
|
|
273
|
-
|
|
253
|
+
const flatTaskFields = TASK_FIELD_NAMES.filter(
|
|
254
|
+
(field) => rawParams[field] !== undefined,
|
|
255
|
+
);
|
|
274
256
|
if (flatTaskFields.length) {
|
|
275
257
|
return `cannot mix top-level task field(s) ${flatTaskFields
|
|
276
258
|
.map((field) => `'${field}'`)
|
|
@@ -282,37 +264,20 @@ export function validateDelegateOperation(
|
|
|
282
264
|
|
|
283
265
|
for (const [index, task] of tasks.entries()) {
|
|
284
266
|
const rawTask = task as Record<string, unknown>;
|
|
285
|
-
const
|
|
286
|
-
const hasSessionAction = task.sessionAction !== undefined;
|
|
267
|
+
const sessionAction = task.sessionAction;
|
|
287
268
|
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
}
|
|
292
|
-
if (hasSessionAction) {
|
|
293
|
-
return `task ${index + 1}: ambiguous: supply only sessionAction (or only legacy action), not both.`;
|
|
294
|
-
}
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
const sessionAction: string | undefined =
|
|
298
|
-
task.sessionAction ??
|
|
299
|
-
(hasLegacyTaskAction && TASK_ACTIONS.has(rawTask.action as string)
|
|
300
|
-
? (rawTask.action as string)
|
|
301
|
-
: undefined);
|
|
302
|
-
|
|
303
|
-
const unknownKeys = Object.keys(rawTask).filter((key) => {
|
|
304
|
-
if (VALID_TASK_KEYS.has(key)) return false;
|
|
305
|
-
if (key === "action" && sessionAction !== undefined) return false;
|
|
306
|
-
return true;
|
|
307
|
-
});
|
|
269
|
+
const unknownKeys = Object.keys(rawTask).filter(
|
|
270
|
+
(key) => !VALID_TASK_KEYS.has(key),
|
|
271
|
+
);
|
|
308
272
|
if (unknownKeys.length) {
|
|
309
|
-
const
|
|
310
|
-
|
|
273
|
+
const misplacedTopLevel = unknownKeys.filter((key) => key === "async");
|
|
274
|
+
const topLevelHint = misplacedTopLevel.length
|
|
275
|
+
? ` ${misplacedTopLevel.map((key) => `'${key}'`).join(" and ")} ${misplacedTopLevel.length === 1 ? "is a" : "are"} top-level flag${misplacedTopLevel.length === 1 ? "" : "s"}; move ${misplacedTopLevel.length === 1 ? "it" : "them"} out of the task entry.`
|
|
311
276
|
: "";
|
|
312
277
|
return (
|
|
313
278
|
`task ${index + 1}: unknown field(s) ${unknownKeys
|
|
314
279
|
.map((key) => `'${key}'`)
|
|
315
|
-
.join(", ")}.${
|
|
280
|
+
.join(", ")}.${topLevelHint} ` +
|
|
316
281
|
`Valid task fields: ${[...VALID_TASK_KEYS].join(", ")}.`
|
|
317
282
|
);
|
|
318
283
|
}
|
|
@@ -325,22 +290,17 @@ export function validateDelegateOperation(
|
|
|
325
290
|
return `task ${index + 1}: deadlineMs must be a positive number of milliseconds.`;
|
|
326
291
|
}
|
|
327
292
|
if (
|
|
328
|
-
task.workspace === "scratch" &&
|
|
329
|
-
!task.agent &&
|
|
293
|
+
(task.workspace === "scratch" || task.workspace === "isolated") &&
|
|
330
294
|
(task.sessionId || task.resumeFrom || sessionAction !== undefined)
|
|
331
295
|
) {
|
|
332
|
-
return `task ${index + 1}: workspace '
|
|
296
|
+
return `task ${index + 1}: workspace '${task.workspace}' is one-shot and cannot be combined with sessionId, resumeFrom, or sessionAction. Set workspace: "shared" to use a persistent agent.`;
|
|
333
297
|
}
|
|
334
298
|
if (sessionAction === "close") {
|
|
335
299
|
if (!task.sessionId) {
|
|
336
300
|
return `task ${index + 1}: sessionAction 'close' requires sessionId.`;
|
|
337
301
|
}
|
|
338
302
|
const extras = Object.keys(rawTask).filter(
|
|
339
|
-
(key) =>
|
|
340
|
-
key !== "sessionAction" &&
|
|
341
|
-
key !== "sessionId" &&
|
|
342
|
-
key !== "action" &&
|
|
343
|
-
key !== "id",
|
|
303
|
+
(key) => key !== "sessionAction" && key !== "sessionId" && key !== "id",
|
|
344
304
|
);
|
|
345
305
|
if (extras.length) {
|
|
346
306
|
return `task ${index + 1}: sessionAction 'close' accepts only sessionAction and sessionId.`;
|
|
@@ -348,7 +308,7 @@ export function validateDelegateOperation(
|
|
|
348
308
|
}
|
|
349
309
|
if (sessionAction === "list") {
|
|
350
310
|
const extras = Object.keys(rawTask).filter(
|
|
351
|
-
(key) => key !== "sessionAction" && key !== "
|
|
311
|
+
(key) => key !== "sessionAction" && key !== "id",
|
|
352
312
|
);
|
|
353
313
|
if (extras.length) {
|
|
354
314
|
return `task ${index + 1}: sessionAction 'list' accepts only sessionAction.`;
|
|
@@ -378,6 +338,52 @@ function normalizeToolsField(value: string): unknown {
|
|
|
378
338
|
return trimmed && !/[\s,]/.test(trimmed) ? [trimmed] : value;
|
|
379
339
|
}
|
|
380
340
|
|
|
341
|
+
/** True when `record` carries a top-level ticket-control intent that makes a
|
|
342
|
+
* flat task-field wrap illegitimate: an explicit `ticketAction`, or a bare
|
|
343
|
+
* `ticket` id (which only makes sense with poll/cancel/wait). */
|
|
344
|
+
function hasTicketControlIntent(record: Record<string, unknown>): boolean {
|
|
345
|
+
return (
|
|
346
|
+
record.ticketAction === "poll" ||
|
|
347
|
+
record.ticketAction === "cancel" ||
|
|
348
|
+
record.ticketAction === "wait" ||
|
|
349
|
+
record.ticket !== undefined
|
|
350
|
+
);
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Fold top-level task fields into a single `tasks` entry. Only fires when
|
|
354
|
+
* there is no usable tasks array and no ticket-control intent — those calls
|
|
355
|
+
* are legitimately taskless. `sessionAction` is part of `TASK_FIELD_NAMES`,
|
|
356
|
+
* so a top-level `sessionAction` rides along into the wrapped task. */
|
|
357
|
+
function wrapFlatTaskFields(record: Record<string, unknown>): void {
|
|
358
|
+
const hasTasks = Array.isArray(record.tasks) && record.tasks.length > 0;
|
|
359
|
+
if (hasTasks || hasTicketControlIntent(record)) return;
|
|
360
|
+
const task: Record<string, unknown> = {};
|
|
361
|
+
for (const key of TASK_FIELD_NAMES) {
|
|
362
|
+
if (record[key] !== undefined) {
|
|
363
|
+
task[key] = record[key];
|
|
364
|
+
delete record[key];
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
if (Object.keys(task).length > 0) record.tasks = [task];
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/** Per-entry recovery for one task: stringified (or bare-token) `tools` → a
|
|
371
|
+
* real array, and `agent: ""` → omitted (ad-hoc). Other malformed input is
|
|
372
|
+
* left for schema validation to reject loudly. */
|
|
373
|
+
function normalizeTaskEntry(entry: unknown): unknown {
|
|
374
|
+
if (!entry || typeof entry !== "object") return entry;
|
|
375
|
+
const e = entry as Record<string, unknown>;
|
|
376
|
+
const rawTools = e.tools;
|
|
377
|
+
const fixAgent = e.agent === "";
|
|
378
|
+
if (typeof rawTools !== "string" && !fixAgent) return entry;
|
|
379
|
+
const out = { ...e };
|
|
380
|
+
if (typeof rawTools === "string") {
|
|
381
|
+
out.tools = normalizeToolsField(rawTools);
|
|
382
|
+
}
|
|
383
|
+
if (fixAgent) delete out.agent;
|
|
384
|
+
return out;
|
|
385
|
+
}
|
|
386
|
+
|
|
381
387
|
/** Compatibility shim run by pi before schema validation. Recovers the
|
|
382
388
|
* malformed shapes weaker models emit, instead of letting them silently
|
|
383
389
|
* degrade to the help response (an empty `tasks` returns the manual, which
|
|
@@ -386,13 +392,9 @@ function normalizeToolsField(value: string): unknown {
|
|
|
386
392
|
* - task fields (`prompt`, `systemPrompt`, `tools`, ...) placed at the top
|
|
387
393
|
* level instead of inside a `tasks` entry — wrapped into a single task;
|
|
388
394
|
* - `tools` as a JSON string (or bare token) inside a task entry;
|
|
389
|
-
* - `agent: ""` inside a task entry — treated as omitted (ad-hoc)
|
|
390
|
-
*
|
|
391
|
-
*
|
|
392
|
-
* Skipped when a ticket action is in play. Conflicts between the legacy
|
|
393
|
-
* `action` field and its canonical replacement are left for
|
|
394
|
-
* `validateDelegateOperation` to report. All other invalid input is left for
|
|
395
|
-
* normal schema validation to reject loudly.
|
|
395
|
+
* - `agent: ""` inside a task entry — treated as omitted (ad-hoc).
|
|
396
|
+
* All other invalid input is left for normal schema validation to reject
|
|
397
|
+
* loudly.
|
|
396
398
|
*
|
|
397
399
|
* Silent by design: these rewrites are lossless re-shaping, so unlike the
|
|
398
400
|
* model-suffix warning in task-resolution (which fires because thinking
|
|
@@ -409,83 +411,12 @@ export function normalizeDelegateArguments(args: unknown): DelegateArguments {
|
|
|
409
411
|
if (parsed) record.tasks = parsed;
|
|
410
412
|
}
|
|
411
413
|
|
|
412
|
-
//
|
|
413
|
-
|
|
414
|
-
if (
|
|
415
|
-
typeof record.action === "string" &&
|
|
416
|
-
["poll", "cancel", "wait"].includes(record.action)
|
|
417
|
-
) {
|
|
418
|
-
if (record.ticketAction === undefined) {
|
|
419
|
-
record.ticketAction = record.action;
|
|
420
|
-
delete record.action;
|
|
421
|
-
}
|
|
422
|
-
}
|
|
414
|
+
// Flat task fields at the top level → wrap into a single task.
|
|
415
|
+
wrapFlatTaskFields(record);
|
|
423
416
|
|
|
424
|
-
//
|
|
425
|
-
// when there is no usable tasks array and no ticket action (`ticket`,
|
|
426
|
-
// poll/cancel/wait) — those calls are legitimately taskless.
|
|
427
|
-
const hasTasks = Array.isArray(record.tasks) && record.tasks.length > 0;
|
|
428
|
-
const isTicketAction =
|
|
429
|
-
record.ticketAction === "poll" ||
|
|
430
|
-
record.ticketAction === "cancel" ||
|
|
431
|
-
record.ticketAction === "wait" ||
|
|
432
|
-
record.action === "poll" ||
|
|
433
|
-
record.action === "cancel" ||
|
|
434
|
-
record.action === "wait";
|
|
435
|
-
if (!hasTasks && !isTicketAction && record.ticket === undefined) {
|
|
436
|
-
const task: Record<string, unknown> = {};
|
|
437
|
-
for (const key of TASK_FIELD_NAMES) {
|
|
438
|
-
if (record[key] !== undefined) {
|
|
439
|
-
task[key] = record[key];
|
|
440
|
-
delete record[key];
|
|
441
|
-
}
|
|
442
|
-
}
|
|
443
|
-
// Canonical `sessionAction` at the top level folds into the wrapped task.
|
|
444
|
-
if (typeof record.sessionAction === "string") {
|
|
445
|
-
if (task.sessionAction === undefined) {
|
|
446
|
-
task.sessionAction = record.sessionAction;
|
|
447
|
-
}
|
|
448
|
-
delete record.sessionAction;
|
|
449
|
-
}
|
|
450
|
-
// Legacy top-level session `action` folds into the wrapped task's
|
|
451
|
-
// `sessionAction`. A conflict with an explicit `sessionAction` is left
|
|
452
|
-
// for validateDelegateOperation to report.
|
|
453
|
-
if (typeof record.action === "string" && TASK_ACTIONS.has(record.action)) {
|
|
454
|
-
if (task.sessionAction === undefined) {
|
|
455
|
-
task.sessionAction = record.action;
|
|
456
|
-
} else {
|
|
457
|
-
task.action = record.action;
|
|
458
|
-
}
|
|
459
|
-
delete record.action;
|
|
460
|
-
}
|
|
461
|
-
if (Object.keys(task).length > 0) record.tasks = [task];
|
|
462
|
-
}
|
|
463
|
-
|
|
464
|
-
// Per-entry recovery: stringified (or bare-token) `tools` → real arrays,
|
|
465
|
-
// `agent: ""` → omitted, and legacy `action` → `sessionAction`.
|
|
417
|
+
// Per-entry recovery: stringified/bare-token `tools` and `agent: ""`.
|
|
466
418
|
if (Array.isArray(record.tasks)) {
|
|
467
|
-
record.tasks = record.tasks.map(
|
|
468
|
-
if (!entry || typeof entry !== "object") return entry;
|
|
469
|
-
const e = entry as Record<string, unknown>;
|
|
470
|
-
const rawTools = e.tools;
|
|
471
|
-
const fixAgent = e.agent === "";
|
|
472
|
-
const needsActionNorm =
|
|
473
|
-
typeof e.action === "string" &&
|
|
474
|
-
TASK_ACTIONS.has(e.action) &&
|
|
475
|
-
e.sessionAction === undefined;
|
|
476
|
-
if (typeof rawTools !== "string" && !fixAgent && !needsActionNorm)
|
|
477
|
-
return entry;
|
|
478
|
-
const out = { ...e };
|
|
479
|
-
if (typeof rawTools === "string") {
|
|
480
|
-
out.tools = normalizeToolsField(rawTools);
|
|
481
|
-
}
|
|
482
|
-
if (fixAgent) delete out.agent;
|
|
483
|
-
if (needsActionNorm) {
|
|
484
|
-
out.sessionAction = out.action;
|
|
485
|
-
delete out.action;
|
|
486
|
-
}
|
|
487
|
-
return out;
|
|
488
|
-
});
|
|
419
|
+
record.tasks = record.tasks.map(normalizeTaskEntry);
|
|
489
420
|
}
|
|
490
421
|
|
|
491
422
|
return record as DelegateArguments;
|