@agwab/pi-workflow 0.2.1 → 0.4.0

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.
Files changed (119) hide show
  1. package/README.md +3 -1
  2. package/dist/artifact-graph-runtime.d.ts +1 -1
  3. package/dist/artifact-graph-runtime.js +10 -5
  4. package/dist/artifact-graph-schema.js +127 -5
  5. package/dist/compiler.js +52 -19
  6. package/dist/dynamic-generated-task-runtime.js +3 -1
  7. package/dist/dynamic-profiles.d.ts +1 -1
  8. package/dist/engine-run-graph.d.ts +3 -0
  9. package/dist/engine-run-graph.js +194 -4
  10. package/dist/engine.d.ts +5 -0
  11. package/dist/engine.js +389 -41
  12. package/dist/extension.d.ts +2 -1
  13. package/dist/extension.js +30 -8
  14. package/dist/index.d.ts +11 -3
  15. package/dist/index.js +6 -1
  16. package/dist/prompt-json.d.ts +7 -0
  17. package/dist/prompt-json.js +13 -0
  18. package/dist/roles.d.ts +1 -1
  19. package/dist/roles.js +5 -8
  20. package/dist/store.d.ts +20 -1
  21. package/dist/store.js +139 -35
  22. package/dist/strings.d.ts +11 -0
  23. package/dist/strings.js +24 -0
  24. package/dist/subagent-backend.js +710 -40
  25. package/dist/types.d.ts +107 -1
  26. package/dist/verification-ontology.d.ts +31 -0
  27. package/dist/verification-ontology.js +66 -0
  28. package/dist/workflow-artifact-tool.js +5 -6
  29. package/dist/workflow-artifacts.d.ts +7 -0
  30. package/dist/workflow-artifacts.js +55 -4
  31. package/dist/workflow-fetch-cache-extension.d.ts +1 -0
  32. package/dist/workflow-fetch-cache-extension.js +57 -9
  33. package/dist/workflow-metrics.d.ts +113 -0
  34. package/dist/workflow-metrics.js +272 -0
  35. package/dist/workflow-output-artifacts.js +5 -3
  36. package/dist/workflow-partial-output.d.ts +45 -0
  37. package/dist/workflow-partial-output.js +205 -0
  38. package/dist/workflow-progress-health.js +42 -10
  39. package/dist/workflow-runtime.js +10 -1
  40. package/dist/workflow-view.js +3 -1
  41. package/dist/workflow-web-source-extension.js +194 -52
  42. package/dist/workflow-web-source.d.ts +2 -1
  43. package/dist/workflow-web-source.js +109 -30
  44. package/docs/usage.md +76 -29
  45. package/node_modules/@agwab/pi-subagent/README.md +3 -3
  46. package/node_modules/@agwab/pi-subagent/api.mjs +1 -0
  47. package/node_modules/@agwab/pi-subagent/docs/usage.md +63 -12
  48. package/node_modules/@agwab/pi-subagent/package.json +2 -2
  49. package/node_modules/@agwab/pi-subagent/src/api.ts +54 -1
  50. package/node_modules/@agwab/pi-subagent/src/artifacts/registry.ts +9 -4
  51. package/node_modules/@agwab/pi-subagent/src/artifacts/result.ts +8 -0
  52. package/node_modules/@agwab/pi-subagent/src/core/constants.ts +9 -0
  53. package/node_modules/@agwab/pi-subagent/src/core/validation.ts +21 -0
  54. package/node_modules/@agwab/pi-subagent/src/index.ts +1046 -576
  55. package/node_modules/@agwab/pi-subagent/src/orchestrate/async.ts +279 -156
  56. package/node_modules/@agwab/pi-subagent/src/orchestrate/interrupt.ts +165 -89
  57. package/node_modules/@agwab/pi-subagent/src/orchestrate/reconcile.ts +111 -65
  58. package/node_modules/@agwab/pi-subagent/src/orchestrate/run-ref.ts +219 -0
  59. package/node_modules/@agwab/pi-subagent/src/orchestrate/run.ts +88 -8
  60. package/node_modules/@agwab/pi-subagent/src/orchestrate/status.ts +614 -298
  61. package/node_modules/@agwab/pi-subagent/src/panel.ts +1356 -560
  62. package/node_modules/@agwab/pi-subagent/src/runners/headless-model.ts +53 -5
  63. package/node_modules/@agwab/pi-subagent/src/runners/tmux.ts +13 -6
  64. package/package.json +2 -2
  65. package/skills/workflow-guide/SKILL.md +1 -0
  66. package/src/artifact-graph-runtime.ts +19 -13
  67. package/src/artifact-graph-schema.ts +143 -3
  68. package/src/cli.mjs +52 -0
  69. package/src/compiler.ts +63 -18
  70. package/src/dynamic-generated-task-runtime.ts +3 -1
  71. package/src/dynamic-profiles.ts +1 -1
  72. package/src/engine-run-graph.ts +246 -4
  73. package/src/engine.ts +545 -38
  74. package/src/extension.ts +36 -6
  75. package/src/index.ts +52 -1
  76. package/src/prompt-json.ts +13 -0
  77. package/src/roles.ts +6 -9
  78. package/src/store.ts +194 -42
  79. package/src/strings.ts +38 -0
  80. package/src/subagent-backend.ts +921 -62
  81. package/src/types.ts +116 -2
  82. package/src/verification-ontology.ts +88 -0
  83. package/src/workflow-artifact-tool.ts +5 -7
  84. package/src/workflow-artifacts.ts +83 -3
  85. package/src/workflow-fetch-cache-extension.ts +78 -13
  86. package/src/workflow-metrics.ts +478 -0
  87. package/src/workflow-output-artifacts.ts +5 -3
  88. package/src/workflow-partial-output.ts +299 -0
  89. package/src/workflow-progress-health.ts +47 -15
  90. package/src/workflow-runtime.ts +18 -2
  91. package/src/workflow-view.ts +2 -1
  92. package/src/workflow-web-source-extension.ts +654 -232
  93. package/src/workflow-web-source.ts +153 -39
  94. package/workflows/README.md +7 -25
  95. package/workflows/deep-research/batched-verification.spec.json +253 -0
  96. package/workflows/deep-research/helpers/batch-verification-candidates.mjs +136 -0
  97. package/workflows/deep-research/helpers/claim-evidence-gate.mjs +229 -36
  98. package/workflows/deep-research/helpers/final-audit-packet.mjs +1 -4
  99. package/workflows/deep-research/helpers/normalize-input-packet.mjs +81 -2
  100. package/workflows/deep-research/helpers/render-executive.mjs +40 -26
  101. package/workflows/deep-research/helpers/sanitize-verification-candidates.mjs +89 -15
  102. package/workflows/deep-research/helpers/shadow-select-verification.mjs +229 -0
  103. package/workflows/deep-research/helpers/verification-ontology.mjs +77 -0
  104. package/workflows/deep-research/schemas/deep-research-executive-render-control.schema.json +3 -3
  105. package/workflows/deep-research/schemas/deep-research-research-questions-control.schema.json +38 -0
  106. package/workflows/deep-research/schemas/deep-research-sanitize-claims-control.schema.json +63 -0
  107. package/workflows/deep-research/schemas/deep-research-verify-claims-batch-control.schema.json +47 -0
  108. package/workflows/deep-research/schemas/deep-research-verify-claims-control.schema.json +13 -3
  109. package/workflows/deep-research/spec.json +32 -12
  110. package/workflows/impact-review/spec.json +3 -3
  111. package/workflows/spec-review/helpers/spec-review-pipeline.mjs +1 -8
  112. package/dist/dynamic-loader.d.ts +0 -25
  113. package/dist/dynamic-loader.js +0 -13
  114. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stderr +0 -0
  115. package/skills/workflow-guide/scaffolds/dag-required-reads/spec.json.validate.stdout +0 -13
  116. package/src/dynamic-loader.ts +0 -49
  117. package/workflows/impact-review/schemas/docs-release-impact-control.schema.json +0 -42
  118. package/workflows/impact-review/schemas/security-performance-impact-control.schema.json +0 -42
  119. package/workflows/impact-review/schemas/state-data-impact-control.schema.json +0 -42
@@ -1,3 +1,4 @@
1
+ import { createHash } from "node:crypto";
1
2
  import { existsSync } from "node:fs";
2
3
  import { copyFile, mkdir, readFile, readdir, rm, writeFile, } from "node:fs/promises";
3
4
  import { delimiter, dirname, extname, isAbsolute, join, relative, resolve, sep, } from "node:path";
@@ -10,14 +11,24 @@ import { writeWorkflowFetchCacheExtensionWrapper } from "./workflow-fetch-cache-
10
11
  import { writeWorkflowWebSourceExtensionWrapper } from "./workflow-web-source-extension.js";
11
12
  import { isWorkflowWebSourceTool } from "./workflow-web-source.js";
12
13
  import { buildWorkflowOutputRetryInstructions, parseWorkflowOutputForBundle, writeWorkflowTaskArtifactBundle, } from "./workflow-output-artifacts.js";
14
+ import { writeWorkflowPartialOutputLedgerFromFile } from "./workflow-partial-output.js";
13
15
  const DEFAULT_SUBAGENT_RUNS_ROOT = ".pi/workflow-subagents";
16
+ const MAX_SUBAGENT_SESSION_ID_LENGTH = 64;
14
17
  const EXTRA_SUBAGENT_EXTENSIONS_ENV = "PI_WORKFLOW_SUBAGENT_EXTRA_EXTENSIONS";
15
18
  const FETCH_CONTENT_CACHE_ENV = "PI_WORKFLOW_FETCH_CONTENT_CACHE";
16
19
  const LEGACY_FETCH_CACHE_ENV = "PI_WORKFLOW_FETCH_CACHE";
20
+ const FETCH_CONTENT_INLINE_CHARS_ENV = "PI_WORKFLOW_FETCH_CONTENT_INLINE_CHARS";
21
+ const DEFAULT_WORKFLOW_FETCH_CONTENT_INLINE_CHARS = 12_000;
17
22
  const DEFAULT_TRANSIENT_MODEL_FAILURE_RETRIES = 5;
18
23
  const DEFAULT_ARTIFACT_OUTPUT_RETRIES = 2;
19
24
  const MAX_CONCURRENT_LAUNCHES_ENV = "PI_WORKFLOW_MAX_CONCURRENT_LAUNCHES";
25
+ const LAUNCH_SLOT_RELEASE_DELAY_MS_ENV = "PI_WORKFLOW_LAUNCH_SLOT_RELEASE_DELAY_MS";
26
+ const PARENT_SUBAGENT_CWD_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_CWD";
27
+ const PARENT_SUBAGENT_RUNS_DIR_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUNS_DIR";
28
+ const PARENT_SUBAGENT_RUN_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_RUN_ID";
29
+ const PARENT_SUBAGENT_ATTEMPT_ID_ENV = "PI_WORKFLOW_PARENT_SUBAGENT_ATTEMPT_ID";
20
30
  const DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS = 3_000;
31
+ const STALE_LAUNCH_CLAIM_GRACE_MS = 30_000;
21
32
  const MIN_TRANSIENT_RETRY_JITTER_MS = 1_000;
22
33
  const MAX_TRANSIENT_RETRY_JITTER_MS = 5_000;
23
34
  const MODULE_PATH = fileURLToPath(import.meta.url);
@@ -39,6 +50,12 @@ function bundledNodeModulePath(packageName, ...parts) {
39
50
  ];
40
51
  return (candidates.find((candidate) => existsSync(candidate)) ?? candidates[0]);
41
52
  }
53
+ const GENERIC_TASK_STATUS_DETAILS = new Set([
54
+ "completed",
55
+ "failed",
56
+ "interrupted",
57
+ "running",
58
+ ]);
42
59
  const subagentApiSpecifier = "@agwab/pi-subagent/api";
43
60
  let cachedSubagentApi;
44
61
  let injectedSubagentApi;
@@ -52,7 +69,68 @@ async function loadSubagentApi() {
52
69
  cachedSubagentApi ??= import(subagentApiSpecifier).then((mod) => mod);
53
70
  return cachedSubagentApi;
54
71
  }
55
- let launchSlotReleaseDelayMs = DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS;
72
+ function nonEmptyEnv(env, key) {
73
+ const value = env[key]?.trim();
74
+ return value ? value : undefined;
75
+ }
76
+ function parentSubagentRefFromEnv(env = process.env) {
77
+ const cwd = nonEmptyEnv(env, PARENT_SUBAGENT_CWD_ENV);
78
+ const runsDir = nonEmptyEnv(env, PARENT_SUBAGENT_RUNS_DIR_ENV);
79
+ const runId = nonEmptyEnv(env, PARENT_SUBAGENT_RUN_ID_ENV);
80
+ if (!cwd || !runsDir || !runId)
81
+ return undefined;
82
+ const attemptId = nonEmptyEnv(env, PARENT_SUBAGENT_ATTEMPT_ID_ENV);
83
+ return { cwd, runsDir, runId, ...(attemptId ? { attemptId } : {}) };
84
+ }
85
+ function terminalChildEventForTaskStatus(status) {
86
+ if (status === "completed")
87
+ return "completed";
88
+ if (status === "failed")
89
+ return "failed";
90
+ if (status === "interrupted")
91
+ return "cancelled";
92
+ return undefined;
93
+ }
94
+ async function recordParentSubagentChildEvent(options) {
95
+ const parent = parentSubagentRefFromEnv();
96
+ if (!parent)
97
+ return;
98
+ const api = await loadSubagentApi().catch(() => undefined);
99
+ if (!api?.recordSubagentChildEvent)
100
+ return;
101
+ await api
102
+ .recordSubagentChildEvent({
103
+ ...parent,
104
+ event: options.event,
105
+ childRunId: options.childRunId,
106
+ workflowRunId: options.run.runId,
107
+ childTaskId: options.task.taskId,
108
+ ...(options.failureKind === undefined
109
+ ? {}
110
+ : { failureKind: options.failureKind }),
111
+ ...(options.message === undefined ? {} : { message: options.message }),
112
+ })
113
+ .catch(() => undefined);
114
+ }
115
+ async function recordTerminalParentSubagentChildEvent(run, task, snapshot) {
116
+ const event = terminalChildEventForTaskStatus(task.status);
117
+ if (!event)
118
+ return;
119
+ const taskFailureKind = task.statusDetail && !GENERIC_TASK_STATUS_DETAILS.has(task.statusDetail)
120
+ ? task.statusDetail
121
+ : undefined;
122
+ await recordParentSubagentChildEvent({
123
+ event,
124
+ childRunId: snapshot.runId,
125
+ run,
126
+ task,
127
+ failureKind: event === "completed"
128
+ ? undefined
129
+ : (snapshot.failureKind ?? taskFailureKind ?? task.statusDetail),
130
+ message: task.lastMessage,
131
+ });
132
+ }
133
+ let launchSlotReleaseDelayMsForTests;
56
134
  let transientRetryJitterForTests;
57
135
  const launchWaitQueue = [];
58
136
  let activeLaunchSlots = 0;
@@ -82,16 +160,25 @@ function releaseLaunchSlot() {
82
160
  }
83
161
  activeLaunchSlots = Math.max(0, activeLaunchSlots - 1);
84
162
  }
163
+ function resolveLaunchSlotReleaseDelayMs() {
164
+ if (launchSlotReleaseDelayMsForTests !== undefined) {
165
+ return launchSlotReleaseDelayMsForTests;
166
+ }
167
+ const override = Number.parseInt(process.env[LAUNCH_SLOT_RELEASE_DELAY_MS_ENV] ?? "", 10);
168
+ if (Number.isFinite(override))
169
+ return Math.max(0, Math.floor(override));
170
+ return DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS;
171
+ }
85
172
  function releaseLaunchSlotAfterDelay(delayMs, release) {
86
173
  if (delayMs <= 0) {
87
174
  release();
88
175
  return;
89
176
  }
90
- const timer = setTimeout(release, delayMs);
91
- timer.unref?.();
177
+ setTimeout(release, delayMs);
92
178
  }
93
- async function runWithLaunchSlot(action) {
179
+ async function runWithLaunchSlot(action, onAcquired) {
94
180
  const release = await acquireLaunchSlot();
181
+ onAcquired?.();
95
182
  let holdAfterReturn = false;
96
183
  try {
97
184
  const result = await action();
@@ -99,7 +186,7 @@ async function runWithLaunchSlot(action) {
99
186
  return result;
100
187
  }
101
188
  finally {
102
- releaseLaunchSlotAfterDelay(holdAfterReturn ? launchSlotReleaseDelayMs : 0, release);
189
+ releaseLaunchSlotAfterDelay(holdAfterReturn ? resolveLaunchSlotReleaseDelayMs() : 0, release);
103
190
  }
104
191
  }
105
192
  function transientRetryJitterMs() {
@@ -112,10 +199,459 @@ function transientRetryJitterMs() {
112
199
  function sleep(ms) {
113
200
  return new Promise((resolve) => setTimeout(resolve, ms));
114
201
  }
202
+ const USAGE_METRIC_KEYS = [
203
+ "inputTokens",
204
+ "outputTokens",
205
+ "totalTokens",
206
+ "cachedInputTokens",
207
+ "cacheCreationInputTokens",
208
+ "cacheReadInputTokens",
209
+ "reasoningTokens",
210
+ "costUsd",
211
+ ];
212
+ const USAGE_FIELD_ALIASES = {
213
+ inputTokens: [
214
+ ["inputTokens"],
215
+ ["input_tokens"],
216
+ ["input"],
217
+ ["promptTokens"],
218
+ ["prompt_tokens"],
219
+ ],
220
+ outputTokens: [
221
+ ["outputTokens"],
222
+ ["output_tokens"],
223
+ ["output"],
224
+ ["completionTokens"],
225
+ ["completion_tokens"],
226
+ ],
227
+ totalTokens: [["totalTokens"], ["total_tokens"], ["tokens"], ["total"]],
228
+ cachedInputTokens: [
229
+ ["cachedInputTokens"],
230
+ ["cached_input_tokens"],
231
+ ["prompt_tokens_details", "cached_tokens"],
232
+ ["input_tokens_details", "cached_tokens"],
233
+ ],
234
+ cacheCreationInputTokens: [
235
+ ["cacheCreationInputTokens"],
236
+ ["cacheCreationTokens"],
237
+ ["cacheWriteTokens"],
238
+ ["cache_creation_input_tokens"],
239
+ ["cache_write_input_tokens"],
240
+ ["cacheWrite"],
241
+ ["cache_write"],
242
+ ],
243
+ cacheReadInputTokens: [
244
+ ["cacheReadInputTokens"],
245
+ ["cacheReadTokens"],
246
+ ["cache_read_input_tokens"],
247
+ ["cacheRead"],
248
+ ["cache_read"],
249
+ ],
250
+ reasoningTokens: [
251
+ ["reasoningTokens"],
252
+ ["reasoning_tokens"],
253
+ ["reasoning"],
254
+ ["completion_tokens_details", "reasoning_tokens"],
255
+ ["output_tokens_details", "reasoning_tokens"],
256
+ ],
257
+ costUsd: [
258
+ ["costUsd"],
259
+ ["cost_usd"],
260
+ ["totalCostUsd"],
261
+ ["total_cost_usd"],
262
+ ["estimatedCostUsd"],
263
+ ["estimated_cost_usd"],
264
+ ["cost", "total"],
265
+ ["cost", "totalUsd"],
266
+ ["cost", "total_usd"],
267
+ ],
268
+ };
269
+ const TIMING_AGGREGATE_KEYS = [
270
+ "launchWaitMs",
271
+ "launchDurationMs",
272
+ "executionMs",
273
+ "totalMs",
274
+ ];
275
+ function isPlainRecord(value) {
276
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
277
+ }
278
+ function hasOwnValue(record, key) {
279
+ return Object.hasOwn(record, key);
280
+ }
281
+ function valueAtPath(record, path) {
282
+ let current = record;
283
+ for (const part of path) {
284
+ if (!isPlainRecord(current) || !hasOwnValue(current, part)) {
285
+ return { found: false, value: undefined };
286
+ }
287
+ current = current[part];
288
+ }
289
+ return { found: true, value: current };
290
+ }
291
+ function usageNumberOrNull(value) {
292
+ if (value === null)
293
+ return null;
294
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
295
+ return value;
296
+ }
297
+ return undefined;
298
+ }
299
+ function normalizedUsageValues(raw) {
300
+ const record = isPlainRecord(raw) ? raw : undefined;
301
+ const values = {};
302
+ if (!record)
303
+ return values;
304
+ for (const key of USAGE_METRIC_KEYS) {
305
+ for (const path of USAGE_FIELD_ALIASES[key]) {
306
+ const candidate = valueAtPath(record, path);
307
+ if (!candidate.found)
308
+ continue;
309
+ const value = usageNumberOrNull(candidate.value);
310
+ if (value === undefined)
311
+ continue;
312
+ values[key] = value;
313
+ break;
314
+ }
315
+ }
316
+ return values;
317
+ }
318
+ function firstStringValue(records, keys) {
319
+ for (const record of records) {
320
+ if (!record)
321
+ continue;
322
+ for (const key of keys) {
323
+ const value = record[key];
324
+ if (typeof value === "string" && value.trim())
325
+ return value;
326
+ }
327
+ }
328
+ return undefined;
329
+ }
330
+ function metadataRecord(value) {
331
+ if (!isPlainRecord(value))
332
+ return undefined;
333
+ return isPlainRecord(value.metadata) ? value.metadata : undefined;
334
+ }
335
+ function usageObservation(subagentResult, snapshot) {
336
+ const resultMetadata = metadataRecord(subagentResult);
337
+ if (resultMetadata && hasOwnValue(resultMetadata, "usage")) {
338
+ return {
339
+ source: "subagent-result-metadata",
340
+ raw: resultMetadata.usage,
341
+ present: true,
342
+ };
343
+ }
344
+ const snapshotMetadata = isPlainRecord(snapshot.metadata)
345
+ ? snapshot.metadata
346
+ : undefined;
347
+ if (snapshotMetadata && hasOwnValue(snapshotMetadata, "usage")) {
348
+ return {
349
+ source: "subagent-snapshot-metadata",
350
+ raw: snapshotMetadata.usage,
351
+ present: true,
352
+ };
353
+ }
354
+ if (subagentResult && hasOwnValue(subagentResult, "usage")) {
355
+ return {
356
+ source: "subagent-result",
357
+ raw: subagentResult.usage,
358
+ present: true,
359
+ };
360
+ }
361
+ const snapshotRecord = snapshot;
362
+ if (hasOwnValue(snapshotRecord, "usage")) {
363
+ return {
364
+ source: "subagent-snapshot",
365
+ raw: snapshotRecord.usage,
366
+ present: true,
367
+ };
368
+ }
369
+ return undefined;
370
+ }
371
+ function buildTaskUsageAttempt(options) {
372
+ const resultMetadata = metadataRecord(options.subagentResult);
373
+ const snapshotMetadata = isPlainRecord(options.snapshot.metadata)
374
+ ? options.snapshot.metadata
375
+ : undefined;
376
+ const resultRecord = options.subagentResult;
377
+ const snapshotRecord = options.snapshot;
378
+ const records = [
379
+ resultMetadata,
380
+ snapshotMetadata,
381
+ resultRecord,
382
+ snapshotRecord,
383
+ ];
384
+ const observed = usageObservation(options.subagentResult, options.snapshot);
385
+ const raw = observed?.raw;
386
+ const unavailable = !observed || raw === null || raw === undefined;
387
+ const provider = firstStringValue(records, ["provider"]);
388
+ const model = firstStringValue(records, ["model"]) ?? options.task.runtime.model;
389
+ const thinking = firstStringValue(records, [
390
+ "thinking",
391
+ "thinkingLevel",
392
+ "reasoningLevel",
393
+ ]) ??
394
+ options.task.runtime.thinkingResolution?.resolved ??
395
+ options.task.runtime.thinking;
396
+ return {
397
+ source: observed?.source ?? "subagent-usage-unavailable",
398
+ capturedAt: options.capturedAt,
399
+ backendRunId: options.snapshot.runId,
400
+ backendAttemptId: options.snapshot.attemptId,
401
+ ...(provider === undefined ? {} : { provider }),
402
+ ...(model === undefined ? {} : { model }),
403
+ ...(thinking === undefined ? {} : { thinking }),
404
+ ...(unavailable ? { unavailable: true } : {}),
405
+ ...(observed?.present && raw !== undefined ? { raw } : {}),
406
+ ...normalizedUsageValues(raw),
407
+ };
408
+ }
409
+ function usageAttemptKey(attempt) {
410
+ return `${attempt.backendRunId ?? ""}\0${attempt.backendAttemptId ?? ""}\0${attempt.source}`;
411
+ }
412
+ function upsertUsageAttempt(attempts, attempt) {
413
+ const key = usageAttemptKey(attempt);
414
+ const index = attempts.findIndex((candidate) => usageAttemptKey(candidate) === key);
415
+ if (index < 0)
416
+ return [...attempts, attempt];
417
+ return attempts.map((candidate, candidateIndex) => candidateIndex === index ? attempt : candidate);
418
+ }
419
+ function aggregateUsageAttempts(attempts) {
420
+ const values = {};
421
+ let incomplete = attempts.some((attempt) => attempt.unavailable === true);
422
+ for (const key of USAGE_METRIC_KEYS) {
423
+ const anyPresent = attempts.some((attempt) => hasOwnValue(attempt, key));
424
+ if (!anyPresent)
425
+ continue;
426
+ let total = 0;
427
+ let complete = true;
428
+ for (const attempt of attempts) {
429
+ if (!hasOwnValue(attempt, key)) {
430
+ complete = false;
431
+ break;
432
+ }
433
+ const value = attempt[key];
434
+ if (typeof value !== "number") {
435
+ complete = false;
436
+ break;
437
+ }
438
+ total += value;
439
+ }
440
+ values[key] = complete ? total : null;
441
+ if (!complete)
442
+ incomplete = true;
443
+ }
444
+ return { values, incomplete };
445
+ }
446
+ function latestUsageString(attempts, key) {
447
+ for (let index = attempts.length - 1; index >= 0; index -= 1) {
448
+ const value = attempts[index]?.[key];
449
+ if (typeof value === "string" && value.trim())
450
+ return value;
451
+ }
452
+ return undefined;
453
+ }
454
+ function recordTaskUsageObservation(options) {
455
+ const attempt = buildTaskUsageAttempt(options);
456
+ const attempts = upsertUsageAttempt(options.task.usage?.attempts ?? [], attempt);
457
+ const aggregate = aggregateUsageAttempts(attempts);
458
+ const usage = {
459
+ source: "pi-subagent",
460
+ capturedAt: options.capturedAt,
461
+ ...(latestUsageString(attempts, "provider") === undefined
462
+ ? {}
463
+ : { provider: latestUsageString(attempts, "provider") }),
464
+ ...(latestUsageString(attempts, "model") === undefined
465
+ ? {}
466
+ : { model: latestUsageString(attempts, "model") }),
467
+ ...(latestUsageString(attempts, "thinking") === undefined
468
+ ? {}
469
+ : { thinking: latestUsageString(attempts, "thinking") }),
470
+ ...(aggregate.incomplete ? { incomplete: true } : {}),
471
+ ...aggregate.values,
472
+ aggregate: {
473
+ attempts: attempts.length,
474
+ ...(aggregate.incomplete ? { incomplete: true } : {}),
475
+ ...aggregate.values,
476
+ },
477
+ attempts,
478
+ };
479
+ options.task.usage = usage;
480
+ }
481
+ function isoTimestampMs(timestamp) {
482
+ if (!timestamp)
483
+ return undefined;
484
+ const parsed = Date.parse(timestamp);
485
+ return Number.isFinite(parsed) ? parsed : undefined;
486
+ }
487
+ function durationBetween(startedAt, completedAt) {
488
+ const startedAtMs = isoTimestampMs(startedAt);
489
+ const completedAtMs = isoTimestampMs(completedAt);
490
+ if (startedAtMs === undefined || completedAtMs === undefined)
491
+ return undefined;
492
+ return Math.max(0, completedAtMs - startedAtMs);
493
+ }
494
+ function durationNumber(value) {
495
+ if (value === null)
496
+ return null;
497
+ if (typeof value === "number" && Number.isFinite(value) && value >= 0) {
498
+ return value;
499
+ }
500
+ return undefined;
501
+ }
502
+ function recordTaskLaunchTiming(task, observation) {
503
+ const capturedAt = observation.launchCompletedAt ?? nowIso();
504
+ const launchWaitMs = durationBetween(observation.launchQueuedAt, observation.launchStartedAt);
505
+ const launchDurationMs = durationBetween(observation.launchStartedAt, observation.launchCompletedAt);
506
+ task.timing = {
507
+ source: "pi-workflow",
508
+ capturedAt,
509
+ launchQueuedAt: observation.launchQueuedAt,
510
+ ...(observation.launchStartedAt === undefined
511
+ ? {}
512
+ : { launchStartedAt: observation.launchStartedAt }),
513
+ ...(observation.launchCompletedAt === undefined
514
+ ? {}
515
+ : { launchCompletedAt: observation.launchCompletedAt }),
516
+ ...(launchWaitMs === undefined ? {} : { launchWaitMs }),
517
+ ...(launchDurationMs === undefined ? {} : { launchDurationMs }),
518
+ launchSlotReleaseDelayMs: resolveLaunchSlotReleaseDelayMs(),
519
+ ...(task.timing?.aggregate === undefined
520
+ ? {}
521
+ : { aggregate: task.timing.aggregate }),
522
+ ...(task.timing?.attempts === undefined
523
+ ? {}
524
+ : { attempts: task.timing.attempts }),
525
+ };
526
+ }
527
+ function buildTaskTimingAttempt(options) {
528
+ const resultDuration = options.subagentResult?.durationMs;
529
+ let executionMs = durationNumber(resultDuration === undefined ? options.snapshot.durationMs : resultDuration);
530
+ if (executionMs === undefined || executionMs === null) {
531
+ executionMs =
532
+ durationBetween(options.startedAt, options.completedAt) ?? executionMs;
533
+ }
534
+ const totalMs = durationBetween(options.task.startedAt ?? options.task.timing?.launchQueuedAt, options.completedAt);
535
+ return {
536
+ source: "pi-subagent",
537
+ capturedAt: options.capturedAt,
538
+ backendRunId: options.snapshot.runId,
539
+ backendAttemptId: options.snapshot.attemptId,
540
+ ...(options.task.timing?.launchQueuedAt === undefined
541
+ ? {}
542
+ : { launchQueuedAt: options.task.timing.launchQueuedAt }),
543
+ ...(options.task.timing?.launchStartedAt === undefined
544
+ ? {}
545
+ : { launchStartedAt: options.task.timing.launchStartedAt }),
546
+ ...(options.task.timing?.launchCompletedAt === undefined
547
+ ? {}
548
+ : { launchCompletedAt: options.task.timing.launchCompletedAt }),
549
+ ...(options.task.timing?.launchWaitMs === undefined
550
+ ? {}
551
+ : { launchWaitMs: options.task.timing.launchWaitMs }),
552
+ ...(options.task.timing?.launchDurationMs === undefined
553
+ ? {}
554
+ : { launchDurationMs: options.task.timing.launchDurationMs }),
555
+ ...(options.startedAt === undefined
556
+ ? {}
557
+ : { executionStartedAt: options.startedAt }),
558
+ ...(options.completedAt === undefined
559
+ ? {}
560
+ : { executionCompletedAt: options.completedAt }),
561
+ ...(executionMs === undefined ? {} : { executionMs }),
562
+ ...(totalMs === undefined ? {} : { totalMs }),
563
+ };
564
+ }
565
+ function timingAttemptKey(attempt) {
566
+ return `${attempt.backendRunId ?? ""}\0${attempt.backendAttemptId ?? ""}`;
567
+ }
568
+ function upsertTimingAttempt(attempts, attempt) {
569
+ const key = timingAttemptKey(attempt);
570
+ const index = attempts.findIndex((candidate) => timingAttemptKey(candidate) === key);
571
+ if (index < 0)
572
+ return [...attempts, attempt];
573
+ return attempts.map((candidate, candidateIndex) => candidateIndex === index ? attempt : candidate);
574
+ }
575
+ function aggregateTimingAttempts(attempts) {
576
+ const aggregate = {
577
+ attempts: attempts.length,
578
+ };
579
+ let incomplete = false;
580
+ for (const key of TIMING_AGGREGATE_KEYS) {
581
+ const anyPresent = attempts.some((attempt) => hasOwnValue(attempt, key));
582
+ if (!anyPresent)
583
+ continue;
584
+ let total = 0;
585
+ let complete = true;
586
+ for (const attempt of attempts) {
587
+ if (!hasOwnValue(attempt, key)) {
588
+ complete = false;
589
+ break;
590
+ }
591
+ const value = attempt[key];
592
+ if (typeof value !== "number") {
593
+ complete = false;
594
+ break;
595
+ }
596
+ total += value;
597
+ }
598
+ aggregate[key] = complete ? total : null;
599
+ if (!complete)
600
+ incomplete = true;
601
+ }
602
+ if (incomplete)
603
+ aggregate.incomplete = true;
604
+ return aggregate;
605
+ }
606
+ function recordTaskTerminalTiming(options) {
607
+ const attempt = buildTaskTimingAttempt(options);
608
+ const attempts = upsertTimingAttempt(options.task.timing?.attempts ?? [], attempt);
609
+ options.task.timing = {
610
+ source: "pi-workflow",
611
+ capturedAt: options.capturedAt,
612
+ ...(attempt.launchQueuedAt === undefined
613
+ ? {}
614
+ : { launchQueuedAt: attempt.launchQueuedAt }),
615
+ ...(attempt.launchStartedAt === undefined
616
+ ? {}
617
+ : { launchStartedAt: attempt.launchStartedAt }),
618
+ ...(attempt.launchCompletedAt === undefined
619
+ ? {}
620
+ : { launchCompletedAt: attempt.launchCompletedAt }),
621
+ ...(attempt.launchWaitMs === undefined
622
+ ? {}
623
+ : { launchWaitMs: attempt.launchWaitMs }),
624
+ ...(attempt.launchDurationMs === undefined
625
+ ? {}
626
+ : { launchDurationMs: attempt.launchDurationMs }),
627
+ ...(options.task.timing?.launchSlotReleaseDelayMs === undefined
628
+ ? {}
629
+ : {
630
+ launchSlotReleaseDelayMs: options.task.timing.launchSlotReleaseDelayMs,
631
+ }),
632
+ ...(attempt.executionStartedAt === undefined
633
+ ? {}
634
+ : { executionStartedAt: attempt.executionStartedAt }),
635
+ ...(attempt.executionCompletedAt === undefined
636
+ ? {}
637
+ : { executionCompletedAt: attempt.executionCompletedAt }),
638
+ ...(attempt.executionMs === undefined
639
+ ? {}
640
+ : { executionMs: attempt.executionMs }),
641
+ ...(attempt.totalMs === undefined ? {} : { totalMs: attempt.totalMs }),
642
+ aggregate: aggregateTimingAttempts(attempts),
643
+ attempts,
644
+ };
645
+ }
646
+ function recordTerminalTaskObservability(options) {
647
+ const capturedAt = nowIso();
648
+ recordTaskUsageObservation({ ...options, capturedAt });
649
+ recordTaskTerminalTiming({ ...options, capturedAt });
650
+ }
115
651
  export function setSubagentLaunchControlsForTests(options) {
116
- launchSlotReleaseDelayMs =
652
+ launchSlotReleaseDelayMsForTests =
117
653
  options?.releaseDelayMs === undefined
118
- ? DEFAULT_LAUNCH_SLOT_RELEASE_DELAY_MS
654
+ ? undefined
119
655
  : Math.max(0, Math.floor(options.releaseDelayMs));
120
656
  transientRetryJitterForTests =
121
657
  options?.retryJitterMs === undefined
@@ -129,8 +665,6 @@ export function setSubagentLaunchControlsForTests(options) {
129
665
  }
130
666
  export async function cleanupSubagentRun(_cwd, run) {
131
667
  for (const task of run.tasks) {
132
- if (isTerminalTaskStatus(task.status))
133
- continue;
134
668
  const handle = getSubagentHandle(task);
135
669
  if (!handle)
136
670
  continue;
@@ -214,11 +748,22 @@ export async function launchSubagentTask(cwd, run, task, compiledTask) {
214
748
  subagentOptions.extensions = extensions;
215
749
  if (captureToolCallsEnabled())
216
750
  subagentOptions.captureToolCalls = true;
751
+ const launchQueuedAt = nowIso();
752
+ let launchStartedAt;
753
+ recordTaskLaunchTiming(task, { launchQueuedAt });
217
754
  if (isLaunchGateSaturated()) {
218
755
  task.lastMessage = `waiting for pi-subagent launch slot (${resolveMaxConcurrentLaunches()} max)`;
219
756
  await writeRunRecord(cwd, run).catch(() => undefined);
220
757
  }
221
- launched = await runWithLaunchSlot(() => api.runSubagent(subagentOptions));
758
+ launched = await runWithLaunchSlot(() => api.runSubagent(subagentOptions), () => {
759
+ launchStartedAt = nowIso();
760
+ recordTaskLaunchTiming(task, { launchQueuedAt, launchStartedAt });
761
+ });
762
+ recordTaskLaunchTiming(task, {
763
+ launchQueuedAt,
764
+ launchStartedAt,
765
+ launchCompletedAt: nowIso(),
766
+ });
222
767
  }
223
768
  catch (error) {
224
769
  task.status = "pending";
@@ -240,6 +785,13 @@ export async function launchSubagentTask(cwd, run, task, compiledTask) {
240
785
  task.statusDetail = "running";
241
786
  task.lastMessage = "launched via pi-subagent/headless";
242
787
  await writeRunRecord(cwd, run).catch(() => undefined);
788
+ await recordParentSubagentChildEvent({
789
+ event: "started",
790
+ childRunId: launched.runId,
791
+ run,
792
+ task,
793
+ message: task.lastMessage,
794
+ });
243
795
  return { kind: "launched" };
244
796
  }
245
797
  export async function refreshRunFromSubagentArtifacts(cwd, run) {
@@ -266,8 +818,13 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
266
818
  }
267
819
  }
268
820
  if (!handle) {
821
+ if (isStaleLaunchClaim(task)) {
822
+ resetStaleLaunchClaim(task);
823
+ changed = true;
824
+ continue;
825
+ }
269
826
  if (isTaskTimedOut(task)) {
270
- markTaskTimedOut(task);
827
+ markSubagentTaskTimedOut(task);
271
828
  changed = true;
272
829
  }
273
830
  continue;
@@ -290,38 +847,34 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
290
847
  .catch(() => null);
291
848
  if (snapshot === null) {
292
849
  if (isTaskTimedOut(task)) {
293
- await api
294
- .interruptSubagent({
295
- cwd: handle.cwd,
296
- runsDir: handle.runsDir,
297
- runId: handle.runId,
298
- attemptId: handle.attemptId,
299
- reason: "workflow timeout",
300
- })
301
- .catch(() => undefined);
302
- markTaskTimedOut(task);
850
+ await interruptTimedOutSubagent(api, handle);
851
+ markSubagentTaskTimedOut(task);
303
852
  changed = true;
304
853
  }
305
854
  continue;
306
855
  }
307
856
  const activeAttempt = snapshot.attempts?.find((attempt) => attempt.attemptId === handle.attemptId) ?? snapshot.attempts?.at(-1);
308
- task.pid = activeAttempt?.workerPid ?? activeAttempt?.pid ?? task.pid;
857
+ const nextPid = activeAttempt?.workerPid ?? activeAttempt?.pid ?? task.pid;
858
+ if (task.pid !== nextPid) {
859
+ task.pid = nextPid;
860
+ changed = true;
861
+ }
309
862
  if (snapshot.status === "running" || snapshot.status === "pending") {
310
- task.statusDetail = "running";
311
- task.lastMessage = activeAttempt?.heartbeatAt
863
+ await refreshRunningArtifactGraphPartialOutput(cwd, task, snapshot).catch(() => undefined);
864
+ if (task.statusDetail !== "running") {
865
+ task.statusDetail = "running";
866
+ changed = true;
867
+ }
868
+ const nextLastMessage = activeAttempt?.heartbeatAt
312
869
  ? `pi-subagent heartbeat ${activeAttempt.heartbeatAt}`
313
870
  : "pi-subagent running";
871
+ if (task.lastMessage !== nextLastMessage) {
872
+ task.lastMessage = nextLastMessage;
873
+ changed = true;
874
+ }
314
875
  if (isTaskTimedOut(task)) {
315
- await api
316
- .interruptSubagent({
317
- cwd: handle.cwd,
318
- runsDir: handle.runsDir,
319
- runId: handle.runId,
320
- attemptId: handle.attemptId,
321
- reason: "workflow timeout",
322
- })
323
- .catch(() => undefined);
324
- markTaskTimedOut(task);
876
+ await interruptTimedOutSubagent(api, handle);
877
+ markSubagentTaskTimedOut(task);
325
878
  changed = true;
326
879
  }
327
880
  continue;
@@ -333,6 +886,56 @@ export async function refreshRunFromSubagentArtifacts(cwd, run) {
333
886
  await writeRunRecord(cwd, run);
334
887
  return run;
335
888
  }
889
+ async function refreshRunningArtifactGraphPartialOutput(cwd, task, snapshot) {
890
+ const partial = task.artifactGraph?.output.partial;
891
+ if (!partial || partial.paths.length === 0)
892
+ return;
893
+ const outputRef = findLog(snapshot, "output");
894
+ const outputFile = fromProjectPath(cwd, task.files.output);
895
+ const artifactRoot = task.backendFiles?.runsDir
896
+ ? fromProjectPath(task.cwd, task.backendFiles.runsDir)
897
+ : undefined;
898
+ await copyLogOrEmpty(snapshot, outputRef, outputFile, artifactRoot);
899
+ await writeWorkflowPartialOutputLedgerFromFile({
900
+ taskDir: dirname(fromProjectPath(cwd, task.files.result)),
901
+ outputFile,
902
+ allowedPaths: partial.paths,
903
+ });
904
+ }
905
+ async function interruptTimedOutSubagent(api, handle) {
906
+ await api
907
+ .interruptSubagent({
908
+ cwd: handle.cwd,
909
+ runsDir: handle.runsDir,
910
+ runId: handle.runId,
911
+ attemptId: handle.attemptId,
912
+ reason: "workflow timeout",
913
+ })
914
+ .catch(() => undefined);
915
+ }
916
+ function markSubagentTaskTimedOut(task) {
917
+ markTaskTimedOut(task);
918
+ task.backendHandle = undefined;
919
+ task.backendTaskId = task.taskId;
920
+ task.pid = undefined;
921
+ }
922
+ function isStaleLaunchClaim(task) {
923
+ if (task.statusDetail !== "launching" || !task.startedAt)
924
+ return false;
925
+ const startedAtMs = Date.parse(task.startedAt);
926
+ return (Number.isFinite(startedAtMs) &&
927
+ Date.now() - startedAtMs > STALE_LAUNCH_CLAIM_GRACE_MS);
928
+ }
929
+ function resetStaleLaunchClaim(task) {
930
+ task.status = "pending";
931
+ task.statusDetail = "pending";
932
+ task.startedAt = undefined;
933
+ task.backendHandle = undefined;
934
+ task.backendFiles = undefined;
935
+ task.backendTaskId = task.taskId;
936
+ task.pid = undefined;
937
+ task.lastMessage = "stale pi-subagent launch claim reset";
938
+ }
336
939
  async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
337
940
  const outputRef = findLog(snapshot, "output");
338
941
  const stderrRef = findLog(snapshot, "stderr");
@@ -385,8 +988,15 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
385
988
  : undefined);
386
989
  const contextLengthExceeded = Boolean(subagentResult?.metadata?.contextLengthExceeded ??
387
990
  snapshot.metadata?.contextLengthExceeded);
991
+ recordTerminalTaskObservability({
992
+ task,
993
+ snapshot,
994
+ subagentResult,
995
+ startedAt,
996
+ completedAt,
997
+ });
388
998
  if (task.artifactGraph?.enabled && statusInfo.status === "completed") {
389
- return await materializeTerminalArtifactGraphResult(cwd, run, task, {
999
+ const changed = await materializeTerminalArtifactGraphResult(cwd, run, task, {
390
1000
  outputFile,
391
1001
  stderrFile,
392
1002
  resultFile,
@@ -395,6 +1005,8 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
395
1005
  exitCode,
396
1006
  subagentResult,
397
1007
  });
1008
+ await recordTerminalParentSubagentChildEvent(run, task, snapshot);
1009
+ return changed;
398
1010
  }
399
1011
  if (shouldAttemptArtifactGraphSalvage({
400
1012
  task,
@@ -406,7 +1018,7 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
406
1018
  subagentResult,
407
1019
  snapshot,
408
1020
  })) {
409
- return await materializeTerminalArtifactGraphResult(cwd, run, task, {
1021
+ const changed = await materializeTerminalArtifactGraphResult(cwd, run, task, {
410
1022
  outputFile,
411
1023
  stderrFile,
412
1024
  resultFile,
@@ -420,6 +1032,8 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
420
1032
  subagentFailureKind: snapshot.failureKind,
421
1033
  },
422
1034
  });
1035
+ await recordTerminalParentSubagentChildEvent(run, task, snapshot);
1036
+ return changed;
423
1037
  }
424
1038
  const workflowResult = {
425
1039
  status: statusInfo.status,
@@ -447,10 +1061,12 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
447
1061
  };
448
1062
  if (shouldRetryTransientModelFailure(statusInfo, workflowResult, outputBytes)) {
449
1063
  await writeJson(transientFailureAttemptPath(resultFile, (task.launchRetry?.attempts ?? 0) + 1), workflowResult);
450
- return retryOrFailTransientSubagentFailure(task, {
1064
+ const changed = retryOrFailTransientSubagentFailure(task, {
451
1065
  reason: statusInfo.failureKind ?? "model",
452
1066
  message: errorMessage ?? "pi-subagent run failed before producing output",
453
1067
  });
1068
+ await recordTerminalParentSubagentChildEvent(run, task, snapshot);
1069
+ return changed;
454
1070
  }
455
1071
  await writeJson(resultFile, workflowResult);
456
1072
  const completedAfterTimeout = resultCompletedAfterTimeout(task, completedAt);
@@ -464,6 +1080,7 @@ async function materializeTerminalSubagentResult(cwd, run, task, snapshot) {
464
1080
  delete task.backendHandle;
465
1081
  delete task.backendFiles;
466
1082
  }
1083
+ await recordTerminalParentSubagentChildEvent(run, task, snapshot);
467
1084
  return changed;
468
1085
  }
469
1086
  function artifactGraphRetrySession(run, task, subagentResult, attempt) {
@@ -490,6 +1107,13 @@ function artifactGraphRetrySession(run, task, subagentResult, attempt) {
490
1107
  async function materializeTerminalArtifactGraphResult(cwd, run, task, options) {
491
1108
  const rawOutput = await readFile(options.outputFile, "utf8").catch(() => "");
492
1109
  const artifactOptions = task.artifactGraph?.output;
1110
+ if (artifactOptions?.partial && artifactOptions.partial.paths.length > 0) {
1111
+ await writeWorkflowPartialOutputLedgerFromFile({
1112
+ taskDir: dirname(options.resultFile),
1113
+ outputFile: options.outputFile,
1114
+ allowedPaths: artifactOptions.partial.paths,
1115
+ }).catch(() => undefined);
1116
+ }
493
1117
  let controlJsonSchema;
494
1118
  try {
495
1119
  controlJsonSchema = await readTaskControlJsonSchema(task);
@@ -991,6 +1615,7 @@ async function workflowTaskExtensions(cwd, run, task, compiledTask) {
991
1615
  runId: run.runId,
992
1616
  taskId: task.taskId,
993
1617
  cacheDir: resolve(cwd, ".pi", "workflows", run.runId, "source-cache", "fetch-content"),
1618
+ maxInlineChars: fetchContentInlineCharsEnvValue(),
994
1619
  },
995
1620
  });
996
1621
  extensions = uniqueStrings([
@@ -1056,6 +1681,17 @@ function workflowWebSourceProviderExtensions(tools, toolProviders) {
1056
1681
  function fetchContentCacheEnvValue() {
1057
1682
  return (process.env[FETCH_CONTENT_CACHE_ENV] ?? process.env[LEGACY_FETCH_CACHE_ENV]);
1058
1683
  }
1684
+ function fetchContentInlineCharsEnvValue() {
1685
+ const raw = process.env[FETCH_CONTENT_INLINE_CHARS_ENV];
1686
+ if (raw === undefined || raw.trim() === "")
1687
+ return DEFAULT_WORKFLOW_FETCH_CONTENT_INLINE_CHARS;
1688
+ if (isExplicitlyDisabled(raw))
1689
+ return undefined;
1690
+ const parsed = Number(raw);
1691
+ if (!Number.isFinite(parsed))
1692
+ return DEFAULT_WORKFLOW_FETCH_CONTENT_INLINE_CHARS;
1693
+ return Math.max(1, Math.floor(parsed));
1694
+ }
1059
1695
  function isExplicitlyDisabled(value) {
1060
1696
  return typeof value === "string" && /^(0|false|no|off)$/i.test(value.trim());
1061
1697
  }
@@ -1145,6 +1781,7 @@ async function recoverSubagentHandle(run, task) {
1145
1781
  const runsDir = subagentRunsDir(run, task);
1146
1782
  const absoluteRunsDir = resolve(task.cwd, runsDir);
1147
1783
  const expectedCorrelationId = `${run.runId}:${task.taskId}`;
1784
+ const claimStartedAtMs = timestampMs(task.startedAt);
1148
1785
  const entries = await readdir(absoluteRunsDir, { withFileTypes: true }).catch(() => []);
1149
1786
  const candidates = [];
1150
1787
  for (const entry of entries) {
@@ -1153,6 +1790,8 @@ async function recoverSubagentHandle(run, task) {
1153
1790
  const record = await readJsonLoose(join(absoluteRunsDir, entry.name, "run.json"));
1154
1791
  if (!record || record.correlationId !== expectedCorrelationId)
1155
1792
  continue;
1793
+ if (isPreClaimSubagentRecord(record, claimStartedAtMs))
1794
+ continue;
1156
1795
  const attemptId = record.activeAttemptId ??
1157
1796
  record.latestAttemptId ??
1158
1797
  record.attempts?.at(-1)?.attemptId;
@@ -1170,6 +1809,14 @@ async function recoverSubagentHandle(run, task) {
1170
1809
  candidates.sort((left, right) => right.updatedAtMs - left.updatedAtMs);
1171
1810
  return candidates[0]?.handle;
1172
1811
  }
1812
+ function isPreClaimSubagentRecord(record, claimStartedAtMs) {
1813
+ if (claimStartedAtMs === undefined)
1814
+ return false;
1815
+ const recordStartedAtMs = timestampMs(record.startedAt) ??
1816
+ timestampMs(record.attempts?.[0]?.startedAt) ??
1817
+ timestampMs(record.updatedAt);
1818
+ return (recordStartedAtMs !== undefined && recordStartedAtMs < claimStartedAtMs);
1819
+ }
1173
1820
  function timestampMs(value) {
1174
1821
  if (value === undefined)
1175
1822
  return undefined;
@@ -1213,13 +1860,36 @@ function subagentRunsDir(run, task) {
1213
1860
  function subagentSessionId(run, task) {
1214
1861
  if (!task.artifactGraph?.enabled)
1215
1862
  return undefined;
1216
- return task.outputRetry?.sessionId ?? baseSubagentSessionId(run, task);
1863
+ const baseSessionId = baseSubagentSessionId(run, task);
1864
+ if (task.outputRetry?.sessionId)
1865
+ return task.outputRetry.sessionId;
1866
+ const launchAttempt = task.launchRetry?.attempts ?? 0;
1867
+ if (launchAttempt > 0)
1868
+ return boundedSubagentSessionId(`${baseSessionId}.launch-retry-${launchAttempt}`);
1869
+ const resumeAttempt = task.resumeEvents?.length ?? 0;
1870
+ if (resumeAttempt > 0)
1871
+ return boundedSubagentSessionId(`${baseSessionId}.resume-${resumeAttempt}`);
1872
+ return baseSessionId;
1217
1873
  }
1218
1874
  function baseSubagentSessionId(run, task) {
1219
- return `pi-workflow.${run.runId}.${task.taskId}`.replace(/[^A-Za-z0-9._-]/g, "-");
1875
+ return boundedSubagentSessionId(`pi-workflow.${run.runId}.${task.taskId}`);
1220
1876
  }
1221
1877
  function retrySubagentSessionId(run, task, attempt) {
1222
- return `${baseSubagentSessionId(run, task)}.retry-${attempt}`;
1878
+ return boundedSubagentSessionId(`${baseSubagentSessionId(run, task)}.retry-${attempt}`);
1879
+ }
1880
+ function boundedSubagentSessionId(value) {
1881
+ const sanitized = value.replace(/[^A-Za-z0-9._-]/g, "-");
1882
+ if (sanitized.length <= MAX_SUBAGENT_SESSION_ID_LENGTH)
1883
+ return sanitized;
1884
+ const digest = createHash("sha256")
1885
+ .update(sanitized)
1886
+ .digest("hex")
1887
+ .slice(0, 16);
1888
+ const suffix = sanitized.split(".").at(-1) || "session";
1889
+ const prefix = `piwf.${digest}`;
1890
+ const maxSuffixLength = MAX_SUBAGENT_SESSION_ID_LENGTH - prefix.length - 1;
1891
+ const boundedSuffix = suffix.slice(-Math.max(1, maxSuffixLength));
1892
+ return `${prefix}.${boundedSuffix}`;
1223
1893
  }
1224
1894
  function buildSystemPrompt(task) {
1225
1895
  const workflowMaxDigestChars = task.artifactGraph?.output.maxDigestChars;