pi-agent-replicate 0.0.0-stage → 1.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 (79) hide show
  1. package/AGENTS.md +27 -0
  2. package/CHANGELOG.md +147 -0
  3. package/CONTEXT.md +63 -0
  4. package/LICENSE +21 -0
  5. package/README.md +239 -2
  6. package/RELEASING.md +130 -0
  7. package/assets/banner.png +0 -0
  8. package/config.json.example +6 -0
  9. package/docs/README.md +20 -0
  10. package/docs/adr/0003-installable-role-packs.md +29 -0
  11. package/docs/adr/0008-adopt-pi-only-subagent-execution.md +22 -0
  12. package/docs/adr/0009-durable-recursive-subagent-ownership.md +23 -0
  13. package/docs/adr/0010-public-subagent-tree-registry.md +16 -0
  14. package/docs/public-subagent-tree-api.md +58 -0
  15. package/docs/settled-lifecycle-contract.md +71 -0
  16. package/docs/worktree-subagents.md +58 -0
  17. package/examples/role-pack/extension.ts +18 -0
  18. package/examples/role-pack/package.json +16 -0
  19. package/examples/role-pack/roles/example-reviewer.md +12 -0
  20. package/index.d.ts +116 -0
  21. package/index.js +1064 -0
  22. package/index.ts +155 -0
  23. package/package.json +70 -4
  24. package/pi-extension/subagents/activity.js +575 -0
  25. package/pi-extension/subagents/activity.ts +980 -0
  26. package/pi-extension/subagents/completion.js +184 -0
  27. package/pi-extension/subagents/completion.ts +229 -0
  28. package/pi-extension/subagents/delivery-drain.js +15 -0
  29. package/pi-extension/subagents/delivery-drain.ts +20 -0
  30. package/pi-extension/subagents/delivery-files.js +86 -0
  31. package/pi-extension/subagents/delivery-files.ts +105 -0
  32. package/pi-extension/subagents/delivery-log.js +51 -0
  33. package/pi-extension/subagents/delivery-log.ts +51 -0
  34. package/pi-extension/subagents/delivery-payload.ts +55 -0
  35. package/pi-extension/subagents/delivery.ts +219 -0
  36. package/pi-extension/subagents/execution-owner.js +30 -0
  37. package/pi-extension/subagents/execution-owner.ts +38 -0
  38. package/pi-extension/subagents/herdr.js +645 -0
  39. package/pi-extension/subagents/herdr.ts +927 -0
  40. package/pi-extension/subagents/index.ts +3400 -0
  41. package/pi-extension/subagents/launch.js +774 -0
  42. package/pi-extension/subagents/launch.ts +1124 -0
  43. package/pi-extension/subagents/lifecycle.js +477 -0
  44. package/pi-extension/subagents/lifecycle.ts +582 -0
  45. package/pi-extension/subagents/lineage.js +806 -0
  46. package/pi-extension/subagents/lineage.ts +841 -0
  47. package/pi-extension/subagents/replica-background.js +32 -0
  48. package/pi-extension/subagents/replica-background.ts +38 -0
  49. package/pi-extension/subagents/replica-branch.ts +45 -0
  50. package/pi-extension/subagents/replica-directory-settings.ts +51 -0
  51. package/pi-extension/subagents/replica-history.js +22 -0
  52. package/pi-extension/subagents/replica-history.ts +21 -0
  53. package/pi-extension/subagents/replica-work.js +326 -0
  54. package/pi-extension/subagents/replica-work.ts +336 -0
  55. package/pi-extension/subagents/replica-worker.js +74 -0
  56. package/pi-extension/subagents/replica-worker.ts +71 -0
  57. package/pi-extension/subagents/replication.ts +99 -0
  58. package/pi-extension/subagents/result-delivery-queue.ts +33 -0
  59. package/pi-extension/subagents/role-catalog.js +123 -0
  60. package/pi-extension/subagents/role-catalog.ts +24 -0
  61. package/pi-extension/subagents/runtime-routing.js +154 -0
  62. package/pi-extension/subagents/runtime-routing.ts +239 -0
  63. package/pi-extension/subagents/session-binding.js +346 -0
  64. package/pi-extension/subagents/session-binding.ts +339 -0
  65. package/pi-extension/subagents/session-work.js +219 -0
  66. package/pi-extension/subagents/session-work.ts +167 -0
  67. package/pi-extension/subagents/session.js +318 -0
  68. package/pi-extension/subagents/session.ts +448 -0
  69. package/pi-extension/subagents/settled-contract.js +47 -0
  70. package/pi-extension/subagents/settled-contract.ts +175 -0
  71. package/pi-extension/subagents/source-state.js +180 -0
  72. package/pi-extension/subagents/source-state.ts +184 -0
  73. package/pi-extension/subagents/status.ts +493 -0
  74. package/pi-extension/subagents/subagent-done.js +805 -0
  75. package/pi-extension/subagents/subagent-done.ts +878 -0
  76. package/pi-extension/subagents/terminal.js +111 -0
  77. package/pi-extension/subagents/terminal.ts +208 -0
  78. package/pi-extension/subagents/watchdog.js +122 -0
  79. package/pi-extension/subagents/watchdog.ts +171 -0
@@ -0,0 +1,582 @@
1
+ import type { ActivityReadResult, SubagentActivityScope } from "./activity.ts";
2
+ import type { CompletionResult } from "./completion.ts";
3
+ import {
4
+ settledDeliveryKey,
5
+ type SettledDeliveryGate,
6
+ type SettledDeliveryIdentity,
7
+ } from "./settled-contract.ts";
8
+
9
+ export type HerdrAgentStatus =
10
+ | "idle"
11
+ | "working"
12
+ | "blocked"
13
+ | "done"
14
+ | "unknown";
15
+
16
+ export type PaneInspection =
17
+ | {
18
+ kind: "present";
19
+ agent?: string;
20
+ agentStatus: HerdrAgentStatus;
21
+ observedAt: number;
22
+ }
23
+ | { kind: "missing"; error?: string }
24
+ | { kind: "unavailable"; error?: string };
25
+
26
+ export type ProcessState =
27
+ | { kind: "starting"; startedAt: number }
28
+ | { kind: "running"; startedAt: number; confirmedAt: number }
29
+ | { kind: "finalizing"; startedAt: number; detectedAt: number; completion: CompletionResult }
30
+ | { kind: "completed"; startedAt: number; detectedAt: number; completedAt: number; completion: CompletionResult }
31
+ | { kind: "failed"; startedAt: number; detectedAt: number; completedAt: number; error: string; exitCode?: number };
32
+
33
+ export type ActivityDetail =
34
+ | { kind: "none"; observedAt: number }
35
+ | { kind: "scope"; scope: SubagentActivityScope; label?: string; since: number; observedAt: number; sequence: number };
36
+
37
+ export type TurnState =
38
+ | { kind: "unknown" }
39
+ | { kind: "starting"; observedAt: number }
40
+ | { kind: "active"; startedAt: number; source: "activity" | "herdr" | "fallback"; activity?: ActivityDetail }
41
+ | { kind: "blocked"; startedAt: number }
42
+ | { kind: "waiting"; startedAt: number }
43
+ | { kind: "interrupted"; requestedAt: number; previousActivitySequence: number | null };
44
+
45
+ export type ActivityHealth =
46
+ | { kind: "unseen" }
47
+ | { kind: "healthy"; observedAt: number }
48
+ | { kind: "problem"; reason: "missing" | "invalid" | "wrong-id"; since: number; error?: string };
49
+
50
+ export type PaneObservation =
51
+ | { kind: "unknown" }
52
+ | { kind: "present"; observedAt: number; agentStatus: HerdrAgentStatus }
53
+ | { kind: "read-error"; firstFailedAt: number; lastFailedAt: number; consecutiveFailures: number; error?: string }
54
+ | { kind: "missing"; detectedAt: number; error?: string };
55
+
56
+ export type CompletionDelivery = "pending" | "delivered" | "suppressed";
57
+
58
+ export interface SubagentLifecycle {
59
+ process: ProcessState;
60
+ turn: TurnState;
61
+ activityHealth: ActivityHealth;
62
+ /** Latest optional Pi detail, independent of Herdr coarse turn state. */
63
+ activityDetail: ActivityDetail | null;
64
+ pane: PaneObservation;
65
+ /** Durable across unavailable/missing observations. */
66
+ hasWorked: boolean;
67
+ lastActivitySequence: number | null;
68
+ delivery: CompletionDelivery;
69
+ /** Repeated settled-turn delivery state, independent from terminal delivery. */
70
+ settledDelivery?: SettledDeliveryGate;
71
+ /** Sequence of the settled boundary that consumed the latest interrupt intent. */
72
+ interruptSettledSequence?: number;
73
+ }
74
+
75
+ export interface LifecycleProjection {
76
+ kind: "starting" | "running" | "active" | "blocked" | "waiting" | "interrupted" | "stalled" | "finalizing" | "completed" | "failed";
77
+ label?: string;
78
+ runtimeEndedAt?: number;
79
+ stateDurationSince?: number;
80
+ }
81
+
82
+ export function createLifecycle(startedAt: number): SubagentLifecycle {
83
+ return {
84
+ process: { kind: "starting", startedAt },
85
+ turn: { kind: "unknown" },
86
+ activityHealth: { kind: "unseen" },
87
+ activityDetail: null,
88
+ pane: { kind: "unknown" },
89
+ hasWorked: false,
90
+ lastActivitySequence: null,
91
+ delivery: "pending",
92
+ settledDelivery: {
93
+ lastActivitySequence: null,
94
+ delivered: new Set(),
95
+ },
96
+ };
97
+ }
98
+
99
+ function isTerminal(process: ProcessState): boolean {
100
+ return process.kind === "completed" || process.kind === "failed";
101
+ }
102
+
103
+ function startedAt(process: ProcessState): number {
104
+ return process.startedAt;
105
+ }
106
+
107
+ export function observePaneInspection(
108
+ lifecycle: SubagentLifecycle,
109
+ inspection: PaneInspection,
110
+ observedAt: number,
111
+ ): SubagentLifecycle {
112
+ if (isTerminal(lifecycle.process)) return lifecycle;
113
+ if (lifecycle.process.kind === "finalizing") return lifecycle;
114
+
115
+ if (inspection.kind === "unavailable") {
116
+ const previous = lifecycle.pane.kind === "read-error" ? lifecycle.pane : null;
117
+ return {
118
+ ...lifecycle,
119
+ pane: {
120
+ kind: "read-error",
121
+ firstFailedAt: previous?.firstFailedAt ?? observedAt,
122
+ lastFailedAt: observedAt,
123
+ consecutiveFailures: (previous?.consecutiveFailures ?? 0) + 1,
124
+ error: inspection.error,
125
+ },
126
+ };
127
+ }
128
+
129
+ if (inspection.kind === "missing") {
130
+ return {
131
+ ...lifecycle,
132
+ pane: { kind: "missing", detectedAt: observedAt, ...(inspection.error ? { error: inspection.error } : {}) },
133
+ };
134
+ }
135
+
136
+ const agentStatus = inspection.agentStatus;
137
+ const hasWorked =
138
+ lifecycle.hasWorked ||
139
+ agentStatus === "working" ||
140
+ agentStatus === "blocked" ||
141
+ agentStatus === "done";
142
+
143
+ const pane: PaneObservation = {
144
+ kind: "present",
145
+ observedAt,
146
+ agentStatus,
147
+ };
148
+
149
+ const process: ProcessState = lifecycle.process.kind === "starting"
150
+ ? { kind: "running", startedAt: lifecycle.process.startedAt, confirmedAt: observedAt }
151
+ : lifecycle.process;
152
+
153
+ // A local interrupt has higher precedence than coarse Herdr status. Herdr
154
+ // can lag behind Escape; only newer Pi activity or completion clears it.
155
+ if (lifecycle.turn.kind === "interrupted") {
156
+ return { ...lifecycle, process, pane, hasWorked };
157
+ }
158
+
159
+ let turn: TurnState = lifecycle.turn;
160
+ if (agentStatus === "blocked") {
161
+ turn = hasWorked
162
+ ? {
163
+ kind: "blocked",
164
+ startedAt: lifecycle.turn.kind === "blocked" ? lifecycle.turn.startedAt : observedAt,
165
+ }
166
+ : {
167
+ kind: "starting",
168
+ observedAt: lifecycle.turn.kind === "starting" ? lifecycle.turn.observedAt : observedAt,
169
+ };
170
+ } else if (agentStatus === "working") {
171
+ turn = {
172
+ kind: "active",
173
+ startedAt: lifecycle.turn.kind === "active" ? lifecycle.turn.startedAt : observedAt,
174
+ source: "herdr",
175
+ ...(lifecycle.activityDetail ? { activity: lifecycle.activityDetail } : {}),
176
+ };
177
+ } else if (agentStatus === "done" || agentStatus === "idle") {
178
+ turn = hasWorked
179
+ ? {
180
+ kind: "waiting",
181
+ startedAt: lifecycle.turn.kind === "waiting" ? lifecycle.turn.startedAt : observedAt,
182
+ }
183
+ : {
184
+ kind: "starting",
185
+ observedAt: lifecycle.turn.kind === "starting" ? lifecycle.turn.observedAt : observedAt,
186
+ };
187
+ } else if (agentStatus === "unknown") {
188
+ // Keep existing process/turn; only record observation.
189
+ return { ...lifecycle, process, pane };
190
+ }
191
+
192
+ return {
193
+ ...lifecycle,
194
+ process,
195
+ turn,
196
+ pane,
197
+ hasWorked,
198
+ };
199
+ }
200
+
201
+ export function observeActivity(
202
+ lifecycle: SubagentLifecycle,
203
+ read: ActivityReadResult,
204
+ observedAt: number,
205
+ ): SubagentLifecycle {
206
+ if (lifecycle.process.kind === "finalizing" || isTerminal(lifecycle.process)) return lifecycle;
207
+
208
+ const detail: ActivityDetail | null = (() => {
209
+ if (!read.ok) return null;
210
+ const activity = read.activity;
211
+ if (lifecycle.lastActivitySequence != null && activity.sequence < lifecycle.lastActivitySequence) {
212
+ return null;
213
+ }
214
+ if (activity.phase !== "active") return null;
215
+ if (activity.activeScope === "compaction") {
216
+ const label = activity.compactionReason
217
+ ? `compacting (${activity.compactionReason})`
218
+ : "compacting";
219
+ return {
220
+ kind: "scope",
221
+ scope: "compaction",
222
+ since: activity.activeSince ?? activity.updatedAt,
223
+ observedAt: activity.updatedAt,
224
+ sequence: activity.sequence,
225
+ label,
226
+ };
227
+ }
228
+ if (activity.activeScope === "tool") {
229
+ return {
230
+ kind: "scope",
231
+ scope: "tool",
232
+ since: activity.toolStartedAt ?? activity.activeSince ?? activity.updatedAt,
233
+ observedAt: activity.updatedAt,
234
+ sequence: activity.sequence,
235
+ ...(activity.toolName ? { label: activity.toolName } : {}),
236
+ };
237
+ }
238
+ if (activity.activeScope === "provider") {
239
+ return { kind: "scope", scope: "provider", since: activity.activeSince ?? activity.updatedAt, observedAt: activity.updatedAt, sequence: activity.sequence, label: "provider" };
240
+ }
241
+ if (activity.activeScope === "streaming") {
242
+ return { kind: "scope", scope: "streaming", since: activity.activeSince ?? activity.updatedAt, observedAt: activity.updatedAt, sequence: activity.sequence, label: "streaming" };
243
+ }
244
+ if (activity.activeScope === "agent" || activity.activeScope === "turn") {
245
+ return { kind: "scope", scope: activity.activeScope, since: activity.activeSince ?? activity.updatedAt, observedAt: activity.updatedAt, sequence: activity.sequence };
246
+ }
247
+ return null;
248
+ })();
249
+
250
+ if (!read.ok) {
251
+ const since = lifecycle.activityHealth.kind === "problem"
252
+ ? lifecycle.activityHealth.since
253
+ : observedAt;
254
+ return {
255
+ ...lifecycle,
256
+ activityHealth: { kind: "problem", reason: read.reason, since, ...(read.error ? { error: read.error } : {}) },
257
+ };
258
+ }
259
+
260
+ if (!detail) {
261
+ // Reading succeeded but no enrichable detail; clear any stale label.
262
+ return {
263
+ ...lifecycle,
264
+ activityDetail: null,
265
+ activityHealth: { kind: "healthy", observedAt },
266
+ lastActivitySequence: Math.max(lifecycle.lastActivitySequence ?? -1, read.activity.sequence),
267
+ };
268
+ }
269
+
270
+ let resumesInterruptedTurn = false;
271
+ if (lifecycle.turn.kind === "interrupted") {
272
+ const staleInterruptSnapshot = detail.observedAt < lifecycle.turn.requestedAt ||
273
+ (detail.observedAt === lifecycle.turn.requestedAt &&
274
+ lifecycle.turn.previousActivitySequence != null &&
275
+ detail.sequence <= lifecycle.turn.previousActivitySequence);
276
+ if (staleInterruptSnapshot) return lifecycle;
277
+ resumesInterruptedTurn = true;
278
+ }
279
+
280
+ const process: ProcessState = lifecycle.process.kind === "starting"
281
+ ? { kind: "running", startedAt: lifecycle.process.startedAt, confirmedAt: observedAt }
282
+ : lifecycle.process;
283
+
284
+ // Herdr owns coarse turn state. Pi detail may enrich an authoritative
285
+ // Herdr-working turn, or provide a fallback only while pane status is unknown.
286
+ let turn = lifecycle.turn;
287
+ const sameDetail = lifecycle.activityDetail?.kind === "scope" &&
288
+ lifecycle.activityDetail.scope === detail.scope &&
289
+ lifecycle.activityDetail.label === detail.label;
290
+ const detailStartedAt = sameDetail && lifecycle.turn.kind === "active"
291
+ ? lifecycle.turn.startedAt
292
+ : detail.since;
293
+
294
+ if (resumesInterruptedTurn) {
295
+ turn = {
296
+ kind: "active",
297
+ startedAt: detailStartedAt,
298
+ source: "activity",
299
+ activity: detail,
300
+ };
301
+ } else if (lifecycle.turn.kind !== "interrupted") {
302
+ if (lifecycle.pane.kind === "present" && lifecycle.pane.agentStatus === "working") {
303
+ turn = {
304
+ kind: "active",
305
+ startedAt: detailStartedAt,
306
+ source: "activity",
307
+ activity: detail,
308
+ };
309
+ } else if (lifecycle.pane.kind === "unknown" || lifecycle.pane.kind === "read-error") {
310
+ turn = {
311
+ kind: "active",
312
+ startedAt: detailStartedAt,
313
+ source: "fallback",
314
+ activity: detail,
315
+ };
316
+ }
317
+ }
318
+
319
+ return {
320
+ ...lifecycle,
321
+ process,
322
+ turn,
323
+ activityDetail: detail,
324
+ activityHealth: { kind: "healthy", observedAt },
325
+ lastActivitySequence: detail.sequence,
326
+ };
327
+ }
328
+
329
+ export function markProcessRunning(
330
+ lifecycle: SubagentLifecycle,
331
+ confirmedAt: number,
332
+ ): SubagentLifecycle {
333
+ if (lifecycle.process.kind !== "starting") return lifecycle;
334
+ return {
335
+ ...lifecycle,
336
+ process: { kind: "running", startedAt: lifecycle.process.startedAt, confirmedAt },
337
+ };
338
+ }
339
+
340
+ export function markInterruptRequested(
341
+ lifecycle: SubagentLifecycle,
342
+ requestedAt: number,
343
+ ): SubagentLifecycle {
344
+ if (lifecycle.process.kind === "finalizing" || isTerminal(lifecycle.process)) return lifecycle;
345
+ return {
346
+ ...lifecycle,
347
+ turn: {
348
+ kind: "interrupted",
349
+ requestedAt,
350
+ previousActivitySequence: lifecycle.lastActivitySequence,
351
+ },
352
+ };
353
+ }
354
+
355
+ /** Consume one interrupt intent at its first eligible settled boundary. */
356
+ export function consumeInterruptBoundary(
357
+ lifecycle: SubagentLifecycle,
358
+ sequence: number,
359
+ ): SubagentLifecycle {
360
+ if (lifecycle.turn.kind !== "interrupted") return lifecycle;
361
+ return {
362
+ ...lifecycle,
363
+ interruptSettledSequence: sequence,
364
+ turn: { kind: "waiting", startedAt: lifecycle.turn.requestedAt },
365
+ };
366
+ }
367
+
368
+ export function markCompletionDetected(
369
+ lifecycle: SubagentLifecycle,
370
+ completion: CompletionResult,
371
+ detectedAt: number,
372
+ ): SubagentLifecycle {
373
+ if (lifecycle.process.kind === "finalizing" || isTerminal(lifecycle.process)) return lifecycle;
374
+ return {
375
+ ...lifecycle,
376
+ process: {
377
+ kind: "finalizing",
378
+ startedAt: startedAt(lifecycle.process),
379
+ detectedAt: Math.max(startedAt(lifecycle.process), detectedAt),
380
+ completion,
381
+ },
382
+ };
383
+ }
384
+
385
+ export function markCompleted(lifecycle: SubagentLifecycle, completedAt: number): SubagentLifecycle {
386
+ if (isTerminal(lifecycle.process)) return lifecycle;
387
+ if (lifecycle.process.kind !== "finalizing") return lifecycle;
388
+ return {
389
+ ...lifecycle,
390
+ process: {
391
+ kind: "completed",
392
+ startedAt: lifecycle.process.startedAt,
393
+ detectedAt: lifecycle.process.detectedAt,
394
+ completedAt: Math.max(lifecycle.process.detectedAt, completedAt),
395
+ completion: lifecycle.process.completion,
396
+ },
397
+ };
398
+ }
399
+
400
+ export function markFailed(
401
+ lifecycle: SubagentLifecycle,
402
+ error: string,
403
+ detectedAt: number,
404
+ exitCode?: number,
405
+ ): SubagentLifecycle {
406
+ if (isTerminal(lifecycle.process)) return lifecycle;
407
+ const start = startedAt(lifecycle.process);
408
+ const detected = lifecycle.process.kind === "finalizing"
409
+ ? lifecycle.process.detectedAt
410
+ : Math.max(start, detectedAt);
411
+ return {
412
+ ...lifecycle,
413
+ process: {
414
+ kind: "failed",
415
+ startedAt: start,
416
+ detectedAt: detected,
417
+ completedAt: Math.max(detected, detectedAt),
418
+ error,
419
+ ...(exitCode == null ? {} : { exitCode }),
420
+ },
421
+ };
422
+ }
423
+
424
+ export function markDelivery(lifecycle: SubagentLifecycle, delivery: CompletionDelivery): SubagentLifecycle {
425
+ if (lifecycle.delivery !== "pending") return lifecycle;
426
+ return { ...lifecycle, delivery };
427
+ }
428
+ function settledGate(lifecycle: SubagentLifecycle): SettledDeliveryGate {
429
+ return lifecycle.settledDelivery ?? { lastActivitySequence: null, delivered: new Set() };
430
+ }
431
+
432
+ /** Accept only a newer settled activity sequence for this child. */
433
+ export function observeSettledActivity(
434
+ lifecycle: SubagentLifecycle,
435
+ activitySequence: number,
436
+ ): SubagentLifecycle {
437
+ const gate = settledGate(lifecycle);
438
+ if (
439
+ gate.lastActivitySequence != null &&
440
+ activitySequence <= gate.lastActivitySequence
441
+ ) {
442
+ return lifecycle;
443
+ }
444
+ return {
445
+ ...lifecycle,
446
+ settledDelivery: { ...gate, lastActivitySequence: activitySequence },
447
+ };
448
+ }
449
+
450
+ /** Return whether terminal content was already delivered as a settled turn. */
451
+ export function hasSettledAssistant(
452
+ lifecycle: SubagentLifecycle,
453
+ identity: SettledDeliveryIdentity,
454
+ ): boolean {
455
+ return settledGate(lifecycle).delivered.has(settledDeliveryKey(identity));
456
+ }
457
+
458
+ /** Record one settled assistant after its parent enqueue succeeds. */
459
+ export function markSettledAssistantDelivered(
460
+ lifecycle: SubagentLifecycle,
461
+ identity: SettledDeliveryIdentity,
462
+ activitySequence: number,
463
+ ): SubagentLifecycle {
464
+ const gate = settledGate(lifecycle);
465
+ const key = settledDeliveryKey(identity);
466
+ if (gate.delivered.has(key)) return lifecycle;
467
+ if (
468
+ gate.lastActivitySequence != null &&
469
+ activitySequence < gate.lastActivitySequence
470
+ ) {
471
+ return lifecycle;
472
+ }
473
+ const delivered = new Set(gate.delivered);
474
+ delivered.add(key);
475
+ return {
476
+ ...lifecycle,
477
+ settledDelivery: {
478
+ lastActivitySequence:
479
+ gate.lastActivitySequence == null
480
+ ? activitySequence
481
+ : Math.max(gate.lastActivitySequence, activitySequence),
482
+ delivered,
483
+ },
484
+ };
485
+ }
486
+
487
+ export function projectLifecycle(lifecycle: SubagentLifecycle, now: number): LifecycleProjection {
488
+ const process = lifecycle.process;
489
+ if (process.kind === "finalizing") return { kind: "finalizing", runtimeEndedAt: process.detectedAt };
490
+ if (process.kind === "completed") {
491
+ return lifecycle.delivery === "delivered" || lifecycle.delivery === "suppressed"
492
+ ? { kind: "completed", runtimeEndedAt: process.completedAt }
493
+ : { kind: "finalizing", label: "delivery-pending", runtimeEndedAt: process.completedAt };
494
+ }
495
+ if (process.kind === "failed") {
496
+ return lifecycle.delivery === "delivered" || lifecycle.delivery === "suppressed"
497
+ ? { kind: "failed", label: process.error, runtimeEndedAt: process.completedAt }
498
+ : { kind: "finalizing", label: "delivery-pending", runtimeEndedAt: process.completedAt };
499
+ }
500
+
501
+ // Pi activity is optional enrichment. Only authoritative Herdr inspection
502
+ // unavailability may produce a stalled projection.
503
+ if (
504
+ lifecycle.pane.kind === "read-error" &&
505
+ now - lifecycle.pane.firstFailedAt >= 60_000
506
+ ) {
507
+ return { kind: "stalled", stateDurationSince: lifecycle.pane.firstFailedAt };
508
+ }
509
+
510
+ const turn = lifecycle.turn;
511
+ switch (turn.kind) {
512
+ case "interrupted":
513
+ return { kind: "interrupted", stateDurationSince: turn.requestedAt };
514
+ case "active": {
515
+ if (turn.activity?.kind === "scope") {
516
+ const label = turn.activity.label ?? turn.activity.scope;
517
+ return { kind: "active", label, stateDurationSince: turn.startedAt };
518
+ }
519
+ return { kind: "active", label: turn.source === "herdr" ? "agent working" : "agent active", stateDurationSince: turn.startedAt };
520
+ }
521
+ case "blocked":
522
+ return { kind: "blocked", stateDurationSince: turn.startedAt };
523
+ case "waiting":
524
+ return { kind: "waiting", stateDurationSince: turn.startedAt };
525
+ case "starting":
526
+ return { kind: "starting", stateDurationSince: turn.observedAt };
527
+ case "unknown":
528
+ return process.kind === "running" ? { kind: "running" } : { kind: "starting" };
529
+ }
530
+ }
531
+
532
+ export type LifecycleTransition = "stalled" | "recovered" | null;
533
+
534
+ export function lifecycleTransition(
535
+ previous: LifecycleProjection["kind"] | undefined,
536
+ next: LifecycleProjection["kind"],
537
+ ): LifecycleTransition {
538
+ if (previous !== "stalled" && next === "stalled") return "stalled";
539
+ if (
540
+ previous === "stalled" &&
541
+ (next === "active" ||
542
+ next === "blocked" ||
543
+ next === "waiting" ||
544
+ next === "interrupted" ||
545
+ next === "running" ||
546
+ next === "starting")
547
+ ) {
548
+ return "recovered";
549
+ }
550
+ return null;
551
+ }
552
+
553
+ export function formatLifecycleTransitionLine(
554
+ name: string,
555
+ projection: LifecycleProjection,
556
+ transition: Exclude<LifecycleTransition, null>,
557
+ now: number,
558
+ startedAt: number,
559
+ formatElapsed: (ms: number) => string,
560
+ ): string {
561
+ const runtime = formatElapsed(Math.max(0, now - startedAt));
562
+ const duration = projection.stateDurationSince == null
563
+ ? ""
564
+ : ` ${formatElapsed(now - projection.stateDurationSince)}`;
565
+ if (transition === "stalled") {
566
+ return `${name} running ${runtime}, stalled${duration}.`;
567
+ }
568
+ if (projection.kind === "waiting") {
569
+ return `${name} running ${runtime}, recovered; waiting${duration}.`;
570
+ }
571
+ if (projection.kind === "active") {
572
+ const detail = projection.label ? ` (${projection.label}${duration})` : duration;
573
+ return `${name} running ${runtime}, recovered; active${detail}.`;
574
+ }
575
+ if (projection.kind === "blocked") {
576
+ return `${name} running ${runtime}, recovered; blocked${duration}.`;
577
+ }
578
+ if (projection.kind === "interrupted") {
579
+ return `${name} running ${runtime}, recovered; interrupted${duration}.`;
580
+ }
581
+ return `${name} running ${runtime}, recovered; running.`;
582
+ }