pi-legion 3.0.0 → 3.0.3
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/CHANGELOG.md +163 -2
- package/CONTRIBUTING.md +38 -25
- package/README.md +23 -18
- package/agents/worker.md +6 -6
- package/docs/contracts/vexillatio-nonblocking-launch-v1.md +161 -0
- package/docs/design.md +39 -9
- package/docs/inprocess-supervisor.md +67 -5
- package/docs/legion-v2-contract.md +6 -3
- package/docs/subagents-teams.md +8 -4
- package/docs/vexillatio-contract.md +2 -1
- package/index.ts +149 -58
- package/package.json +4 -7
- package/src/agents/agents.ts +5 -5
- package/src/api.ts +6 -1
- package/src/extension/auto-doctor.ts +21 -3
- package/src/extension/join.ts +7 -18
- package/src/extension/legion-doctor.ts +125 -38
- package/src/extension/legion-tools.ts +7 -1
- package/src/extension/playbook.ts +1 -1
- package/src/extension/runtime.ts +206 -8
- package/src/extension/spawn-hints.ts +25 -4
- package/src/runs/orchestrator.ts +78 -8
- package/src/runs/worktree.ts +5 -4
- package/src/shared/session.ts +83 -7
- package/src/shared/types.ts +17 -2
- package/src/supervisor/inprocess-supervisor.ts +449 -16
- package/src/supervisor/routing-supervisor.ts +1 -1
- package/src/supervisor/spawn-mode.ts +25 -0
package/src/shared/session.ts
CHANGED
|
@@ -37,7 +37,17 @@ function emptyUsage(): Usage {
|
|
|
37
37
|
|
|
38
38
|
/** Mutable clock used while folding events; not part of ParsedSession. */
|
|
39
39
|
export interface GenerationClock {
|
|
40
|
+
/** Nested Unix-ms timestamp of the message that opened this generation window. */
|
|
40
41
|
awaitingSince?: number;
|
|
42
|
+
/**
|
|
43
|
+
* Same window edge expressed as the enclosing entry's write instant.
|
|
44
|
+
*
|
|
45
|
+
* Sessions may carry only the nested timestamp, only the entry ISO timestamp,
|
|
46
|
+
* or both. Measuring an end against a start from a different axis yields a
|
|
47
|
+
* nonsense span, so each window records both edges and `applyAssistant`
|
|
48
|
+
* pairs like with like.
|
|
49
|
+
*/
|
|
50
|
+
awaitingWall?: number;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
export function emptyParsedSession(): ParsedSession {
|
|
@@ -126,10 +136,13 @@ function accumulateUsage(
|
|
|
126
136
|
}
|
|
127
137
|
|
|
128
138
|
/**
|
|
129
|
-
* Turn-boundary
|
|
139
|
+
* Turn-boundary timestamp for the current-turn elapsed display.
|
|
130
140
|
*
|
|
131
|
-
*
|
|
132
|
-
* the
|
|
141
|
+
* Prefers the nested message `timestamp` (Unix ms, session-format.md) — for the
|
|
142
|
+
* elapsed display the moment generation began is exactly what we want — and
|
|
143
|
+
* falls back to the ISO 8601 entry timestamp for entries written without it.
|
|
144
|
+
* Generation *duration* is measured elsewhere (`applyAssistant`): the nested
|
|
145
|
+
* value marks a start, not a completion, and `durationMs` carries the length.
|
|
133
146
|
*/
|
|
134
147
|
function messageTimestamp(
|
|
135
148
|
event: Record<string, unknown>,
|
|
@@ -145,6 +158,19 @@ function messageTimestamp(
|
|
|
145
158
|
return undefined;
|
|
146
159
|
}
|
|
147
160
|
|
|
161
|
+
/**
|
|
162
|
+
* The instant a message was written, on the same Unix-ms axis as
|
|
163
|
+
* `message.timestamp`. Used for the generation clock so start and end share a
|
|
164
|
+
* basis; a session that only carries the entry ISO timestamp would otherwise
|
|
165
|
+
* mix a wall-clock instant into a Unix-ms subtraction.
|
|
166
|
+
*/
|
|
167
|
+
function entryWrittenAt(event: Record<string, unknown>): number | undefined {
|
|
168
|
+
const outer = event.timestamp;
|
|
169
|
+
if (typeof outer !== "string") return undefined;
|
|
170
|
+
const ms = Date.parse(outer);
|
|
171
|
+
return Number.isFinite(ms) ? ms : undefined;
|
|
172
|
+
}
|
|
173
|
+
|
|
148
174
|
/** Find (or create) the turn an assistant/toolResult message belongs to. */
|
|
149
175
|
function currentTurn(parsed: ParsedSession): TurnRecord {
|
|
150
176
|
const existing = parsed.turns.at(-1);
|
|
@@ -163,6 +189,7 @@ function applyAssistant(
|
|
|
163
189
|
parsed: ParsedSession,
|
|
164
190
|
message: Record<string, unknown>,
|
|
165
191
|
timestamp: number | undefined,
|
|
192
|
+
wall: number | undefined,
|
|
166
193
|
clock: GenerationClock,
|
|
167
194
|
): void {
|
|
168
195
|
const turn = currentTurn(parsed);
|
|
@@ -181,7 +208,21 @@ function applyAssistant(
|
|
|
181
208
|
});
|
|
182
209
|
if (timestamp !== undefined) turn.endedAt = timestamp;
|
|
183
210
|
|
|
184
|
-
|
|
211
|
+
// Generation time spans the wait for the model plus the model call itself. Two
|
|
212
|
+
// strictly paired sources, never mixed:
|
|
213
|
+
// 1. `durationMs` — an exact length reported by the provider, axis-free.
|
|
214
|
+
// 2. entry-to-entry (`wall` − `awaitingWall`) — both edges are write instants,
|
|
215
|
+
// so a session that omits nested timestamps still measures correctly.
|
|
216
|
+
// Subtracting the nested `message.timestamp` is NOT the completion instant: it
|
|
217
|
+
// marks when generation began, so it captures only prompt→first-token latency
|
|
218
|
+
// (2-7ms on real sessions). It survives as a last resort for synthetic or
|
|
219
|
+
// legacy sessions that carry no entry timestamps at all.
|
|
220
|
+
const measured = message.durationMs;
|
|
221
|
+
if (typeof measured === "number" && Number.isFinite(measured) && measured >= 0) {
|
|
222
|
+
parsed.generationMs += measured;
|
|
223
|
+
} else if (wall !== undefined && clock.awaitingWall !== undefined && wall >= clock.awaitingWall) {
|
|
224
|
+
parsed.generationMs += wall - clock.awaitingWall;
|
|
225
|
+
} else if (
|
|
185
226
|
timestamp !== undefined &&
|
|
186
227
|
clock.awaitingSince !== undefined &&
|
|
187
228
|
timestamp >= clock.awaitingSince
|
|
@@ -189,6 +230,7 @@ function applyAssistant(
|
|
|
189
230
|
parsed.generationMs += timestamp - clock.awaitingSince;
|
|
190
231
|
}
|
|
191
232
|
clock.awaitingSince = undefined;
|
|
233
|
+
clock.awaitingWall = undefined;
|
|
192
234
|
|
|
193
235
|
accumulateUsage(parsed.usage, message);
|
|
194
236
|
if (typeof message.model === "string") parsed.model = message.model;
|
|
@@ -201,6 +243,7 @@ function applyToolResult(
|
|
|
201
243
|
parsed: ParsedSession,
|
|
202
244
|
message: Record<string, unknown>,
|
|
203
245
|
timestamp: number | undefined,
|
|
246
|
+
wall: number | undefined,
|
|
204
247
|
clock: GenerationClock,
|
|
205
248
|
): void {
|
|
206
249
|
const turn = parsed.turns.at(-1);
|
|
@@ -212,6 +255,7 @@ function applyToolResult(
|
|
|
212
255
|
}
|
|
213
256
|
// Tool completion starts the next generation window (model resumes).
|
|
214
257
|
if (timestamp !== undefined) clock.awaitingSince = timestamp;
|
|
258
|
+
if (wall !== undefined) clock.awaitingWall = wall;
|
|
215
259
|
}
|
|
216
260
|
|
|
217
261
|
/**
|
|
@@ -288,6 +332,9 @@ function applyEvent(
|
|
|
288
332
|
const message = event.message as Record<string, unknown> | undefined;
|
|
289
333
|
if (!message || typeof message !== "object") return;
|
|
290
334
|
const ts = messageTimestamp(event, message);
|
|
335
|
+
// The entry write instant, on the same Unix-ms axis as `message.timestamp`.
|
|
336
|
+
// Generation timing uses it so both window edges share one basis.
|
|
337
|
+
const wall = entryWrittenAt(event);
|
|
291
338
|
|
|
292
339
|
switch (message.role) {
|
|
293
340
|
case "user":
|
|
@@ -300,13 +347,14 @@ function applyEvent(
|
|
|
300
347
|
});
|
|
301
348
|
// User prompt opens a generation window.
|
|
302
349
|
if (ts !== undefined) clock.awaitingSince = ts;
|
|
350
|
+
if (wall !== undefined) clock.awaitingWall = wall;
|
|
303
351
|
break;
|
|
304
352
|
case "assistant":
|
|
305
|
-
applyAssistant(parsed, message, ts, clock);
|
|
353
|
+
applyAssistant(parsed, message, ts, wall, clock);
|
|
306
354
|
break;
|
|
307
355
|
case "toolResult":
|
|
308
356
|
case "tool":
|
|
309
|
-
applyToolResult(parsed, message, ts, clock);
|
|
357
|
+
applyToolResult(parsed, message, ts, wall, clock);
|
|
310
358
|
break;
|
|
311
359
|
default:
|
|
312
360
|
break;
|
|
@@ -529,6 +577,25 @@ export const NO_ASSISTANT_ABORT_REASON = "no assistant message for the last prom
|
|
|
529
577
|
export const PARENT_SESSION_SHUTDOWN_ABORT_REASON =
|
|
530
578
|
"parent session shutdown retired the child before it produced an assistant message";
|
|
531
579
|
|
|
580
|
+
/**
|
|
581
|
+
* The parent turn ended and the abort signal carried no reason (#227 D3b).
|
|
582
|
+
*
|
|
583
|
+
* Upstream writes a reason-less `AbortController` cancellation as
|
|
584
|
+
* `stopReason:"error"` with a bare `"This operation was aborted"` message, which
|
|
585
|
+
* used to be indistinguishable from a real provider failure. The status stays
|
|
586
|
+
* `aborted`; only the reason becomes self-describing.
|
|
587
|
+
*/
|
|
588
|
+
export const ABORT_WITHOUT_REASON =
|
|
589
|
+
"parent turn ended (abort signal without a reason)";
|
|
590
|
+
|
|
591
|
+
/**
|
|
592
|
+
* The exact messages `AbortSignal`/`AbortController` produce with no reason.
|
|
593
|
+
* Anchored to the whole message so a longer, meaningful error (a provider
|
|
594
|
+
* message that merely mentions aborting) does not get rewritten.
|
|
595
|
+
*/
|
|
596
|
+
const REASONLESS_ABORT_MESSAGE =
|
|
597
|
+
/^(this operation was aborted|the operation was aborted|operation aborted)$/i;
|
|
598
|
+
|
|
532
599
|
/** True when the last turn (or the whole file) has no assistant message. */
|
|
533
600
|
export function lastTurnHasNoAssistant(parsed: ParsedSession): boolean {
|
|
534
601
|
const last = parsed.turns.at(-1);
|
|
@@ -631,11 +698,20 @@ function outcomeFromFinalMessage(
|
|
|
631
698
|
// F29: a graceful interrupt surfaces as `error` with an abort-ish message.
|
|
632
699
|
const msg = final.errorMessage ?? "";
|
|
633
700
|
if (/abort/i.test(msg)) {
|
|
701
|
+
// #227 D3b: a reason-less AbortController cancellation reaches us as
|
|
702
|
+
// the bare engine message. Name it as the parent's abort instead of
|
|
703
|
+
// echoing text that reads like a provider failure. Anything carrying
|
|
704
|
+
// more (notably a `timeout`, or a longer provider sentence) keeps the
|
|
705
|
+
// original message — status is `aborted` either way.
|
|
706
|
+
const reason =
|
|
707
|
+
REASONLESS_ABORT_MESSAGE.test(msg.trim()) && !/timeout/i.test(msg)
|
|
708
|
+
? ABORT_WITHOUT_REASON
|
|
709
|
+
: msg;
|
|
634
710
|
return {
|
|
635
711
|
status: "aborted",
|
|
636
712
|
stopReason: "error",
|
|
637
713
|
errorMessage: msg,
|
|
638
|
-
reason
|
|
714
|
+
reason,
|
|
639
715
|
...base,
|
|
640
716
|
};
|
|
641
717
|
}
|
package/src/shared/types.ts
CHANGED
|
@@ -364,6 +364,13 @@ export interface ChildRecord {
|
|
|
364
364
|
thinking?: string | false;
|
|
365
365
|
/** Models that were requested but cannot be represented by the RPC child. */
|
|
366
366
|
modelDropped?: string[];
|
|
367
|
+
/**
|
|
368
|
+
* Backend this child was actually spawned on, as arbitrated by
|
|
369
|
+
* `Orchestrator.launch` (#209). Optional because run.json records written
|
|
370
|
+
* before the field existed have no value — readers must render those as
|
|
371
|
+
* unknown, never guess a backend.
|
|
372
|
+
*/
|
|
373
|
+
spawnMode?: SpawnMode;
|
|
367
374
|
}
|
|
368
375
|
|
|
369
376
|
export interface RunRecord {
|
|
@@ -373,8 +380,11 @@ export interface RunRecord {
|
|
|
373
380
|
cwd: string;
|
|
374
381
|
|
|
375
382
|
herdr: {
|
|
376
|
-
/**
|
|
377
|
-
|
|
383
|
+
/**
|
|
384
|
+
* Transport marker under the historic run.json key: the arbitrated backend
|
|
385
|
+
* of the run's own process (#209). Absent on legacy records.
|
|
386
|
+
*/
|
|
387
|
+
supervisor?: SpawnMode;
|
|
378
388
|
/** Reserved for M2 tree identity. */
|
|
379
389
|
node_id?: string;
|
|
380
390
|
};
|
|
@@ -482,6 +492,11 @@ export interface Handle {
|
|
|
482
492
|
* silently defeated the ownership audit.
|
|
483
493
|
*/
|
|
484
494
|
child: ChildRecord;
|
|
495
|
+
/**
|
|
496
|
+
* Backend the orchestrator actually chose for this launch (arbitration
|
|
497
|
+
* result, #209). Callers render this instead of assuming `rpc`.
|
|
498
|
+
*/
|
|
499
|
+
spawnMode: SpawnMode;
|
|
485
500
|
}
|
|
486
501
|
|
|
487
502
|
// =============================================================================
|