@aeondave/pi-persona 1.17.1 → 1.17.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.
Files changed (103) hide show
  1. package/AGENTS.md +59 -28
  2. package/CHANGELOG.md +234 -0
  3. package/README.md +16 -12
  4. package/agents/architect.md +21 -0
  5. package/agents/balthasar.md +17 -22
  6. package/agents/casper.md +17 -23
  7. package/agents/evidence-verifier.md +31 -16
  8. package/agents/melchior.md +17 -22
  9. package/agents/operator.md +15 -55
  10. package/agents/research.md +14 -48
  11. package/agents/reviewer.md +20 -39
  12. package/agents/scout.md +14 -15
  13. package/agents/verifier.md +15 -16
  14. package/docs/ARCHITECTURE.md +183 -55
  15. package/docs/EXPERIENCE.md +19 -9
  16. package/docs/MONITORS.md +62 -3
  17. package/docs/REFERENCE.md +82 -32
  18. package/docs/SPINE.md +2 -2
  19. package/docs/STRATEGIES.md +23 -8
  20. package/docs/TELEMETRY.md +1 -0
  21. package/flows/deep-review.flow.json +1 -1
  22. package/package.json +9 -8
  23. package/personas/audit.md +28 -17
  24. package/personas/dev.md +30 -32
  25. package/personas/elite.md +223 -221
  26. package/personas/judge.md +25 -19
  27. package/personas/magi.md +26 -29
  28. package/personas/planner.md +31 -24
  29. package/personas/researcher.md +25 -26
  30. package/personas/swarm.md +35 -25
  31. package/personas/verify.md +30 -19
  32. package/src/agents/agent.ts +20 -0
  33. package/src/bridge.ts +3 -2
  34. package/src/core/brief.ts +429 -356
  35. package/src/core/config.ts +15 -10
  36. package/src/core/model-catalog.ts +171 -0
  37. package/src/core/model-tier.ts +89 -0
  38. package/src/core/models.ts +13 -4
  39. package/src/core/monitor-output.ts +85 -0
  40. package/src/core/monitor.ts +34 -2
  41. package/src/core/nudge.ts +176 -23
  42. package/src/core/seed.ts +99 -6
  43. package/src/core/session-runs.ts +87 -0
  44. package/src/core/settings.ts +1 -1
  45. package/src/core/timer.ts +34 -9
  46. package/src/engine/adapter.ts +12 -8
  47. package/src/engine/async.ts +165 -146
  48. package/src/engine/child.ts +2 -2
  49. package/src/engine/host-module.ts +12 -0
  50. package/src/engine/inproc.ts +12 -7
  51. package/src/engine/stream.ts +4 -20
  52. package/src/exocom/install.ts +2 -1
  53. package/src/extension/engine.ts +43 -16
  54. package/src/extension/hooks.ts +106 -36
  55. package/src/extension/persona-model-settings.ts +31 -13
  56. package/src/extension/shared.ts +15 -7
  57. package/src/extension.ts +505 -241
  58. package/src/monitor/archive.ts +235 -0
  59. package/src/monitor/session.ts +21 -1
  60. package/src/orchestration/flow-journal.ts +26 -5
  61. package/src/orchestration/flow-run.ts +38 -13
  62. package/src/orchestration/flow.ts +9 -2
  63. package/src/orchestration/reducers.ts +9 -5
  64. package/src/orchestration/render.ts +1 -1
  65. package/src/orchestration/sdk.ts +17 -5
  66. package/src/orchestration/strategies/compete.ts +5 -1
  67. package/src/orchestration/strategies/council-rounds.ts +2 -0
  68. package/src/orchestration/strategies/critic-loop.ts +3 -0
  69. package/src/orchestration/strategies/debate.ts +2 -0
  70. package/src/orchestration/strategies/fanout.ts +3 -2
  71. package/src/orchestration/strategies/judge.ts +5 -1
  72. package/src/orchestration/strategies/magi.ts +40 -23
  73. package/src/orchestration/strategies/map.ts +13 -3
  74. package/src/orchestration/strategies/pair.ts +2 -0
  75. package/src/orchestration/strategies/pipeline.ts +2 -0
  76. package/src/orchestration/strategies/synthesize.ts +27 -7
  77. package/src/orchestration/strategy.ts +31 -0
  78. package/src/persona/persona.ts +7 -3
  79. package/src/telemetry/contract.ts +1 -1
  80. package/src/tools/background-run.ts +151 -0
  81. package/src/tools/council.ts +77 -52
  82. package/src/tools/delegate-tool.ts +310 -280
  83. package/src/tools/delegate.ts +22 -210
  84. package/src/tools/exocom-work.ts +18 -53
  85. package/src/tools/exocom.ts +15 -19
  86. package/src/tools/flow.ts +85 -28
  87. package/src/tools/intercom-tool.ts +108 -118
  88. package/src/tools/intercom.ts +1 -1
  89. package/src/tools/models.ts +19 -9
  90. package/src/tools/monitor.ts +39 -6
  91. package/src/tools/timer.ts +11 -13
  92. package/src/ui/agent-overlay.ts +55 -14
  93. package/src/ui/agent-tree.ts +84 -18
  94. package/src/ui/compact-widget.ts +25 -0
  95. package/src/ui/failure-summary.ts +55 -0
  96. package/src/ui/literal-tool-result.ts +24 -0
  97. package/src/ui/model-picker.ts +158 -81
  98. package/src/ui/persona-settings.ts +9 -3
  99. package/src/ui/presentation.ts +24 -0
  100. package/src/ui/report-body.ts +63 -0
  101. package/src/ui/session-cards.ts +189 -0
  102. package/src/ui/wake-rows.ts +262 -0
  103. package/teams.yaml +5 -1
package/src/core/nudge.ts CHANGED
@@ -15,7 +15,7 @@
15
15
  * (echo/cd/one-line check) is orchestration glue and does not advance the run; a run reaching the
16
16
  * threshold is a by-hand sweep. Tool identity is not required (a real sweep mixes read/grep/curl). A
17
17
  * high single-command threshold stays as a SECONDARY trigger for a genuinely enormous one-shot dump
18
- * (a sign it should have been detached/quarantined). A hand-off (delegate/council) resets the run;
18
+ * (a sign it should have been detached/quarantined). A hand-off (delegate/council/flow) resets the run;
19
19
  * nothing else does.
20
20
  *
21
21
  * Backoff: repeating the identical reminder is noise once the operator has seen it and kept going —
@@ -23,15 +23,19 @@
23
23
  * sub-agent can't inherit; from the supervisor side that loop is indistinguishable from a delegable
24
24
  * sweep, so no signal can separate them). Each un-actioned nudge WIDENS the next run window (n× the
25
25
  * base): the reminder lands early, then de-escalates. Pure — no I/O, fully unit-testable.
26
+ *
27
+ * The module also hosts the two call-time wake guards, {@link PollGuard} and {@link longWaitSeconds}.
26
28
  */
29
+ import { recommendModels, shortlistModels } from "./model-catalog.ts";
30
+ import { modelCostTiers, type ModelCostMetadata } from "./model-tier.ts";
27
31
 
28
32
  /** Tools that mean "the operator handed work off" — they end (reset) a by-hand run. */
29
- export const HANDOFF_TOOL_NAMES: ReadonlySet<string> = new Set(["delegate", "council"]);
30
- /** Supervision glue: collecting a delegated result (`intercom wait`/`result`) or arming a
31
- * `timer` is neither hands-on work nor a hand-off. Their payloads are large by nature, so
33
+ export const HANDOFF_TOOL_NAMES: ReadonlySet<string> = new Set(["delegate", "council", "flow"]);
34
+ /** Supervision glue: collecting a delegated result (`intercom result`) or arming a
35
+ * `timer`/`monitor` is neither hands-on work nor a hand-off. Their payloads are large by nature, so
32
36
  * counting them would punish the exact behavior the nudge exists to encourage. They neither
33
37
  * advance nor reset the run, and never nudge. */
34
- const GLUE_TOOLS = new Set(["intercom", "timer"]);
38
+ const GLUE_TOOLS = new Set(["intercom", "timer", "monitor"]);
35
39
  /** Mutation tools often return only a terse acknowledgement. Their output size is not a useful
36
40
  * proxy for the amount of direct coding work, so every call counts as substantive. */
37
41
  const MUTATING_TOOLS = new Set(["edit", "write", "apply_patch"]);
@@ -58,7 +62,7 @@ export interface NudgeThresholds {
58
62
  * check), not a sweep step: it does not advance the run counter. */
59
63
  minStepChars: number;
60
64
  /** Minimum cumulative burn (chars since last hand-off) before a sweep run can fire. Without
61
- * this, 5 tiny git/grep commands (200 chars each = 1000 total ≈ 0k tokens) trip the nudge
65
+ * this, a few tiny git/grep commands (200 chars each = 600 total ≈ 0k tokens) trip the nudge
62
66
  * and renderNudge prints "~0k tokens" — advice to "delegate what burns context" when nothing
63
67
  * burned. This floor gates the sweep so it only fires when the burn is substantive.
64
68
  * Defaults to `DEFAULT_NUDGE_THRESHOLDS.minSweepBurnChars` when omitted. */
@@ -69,11 +73,12 @@ export interface NudgeThresholds {
69
73
 
70
74
  export const DEFAULT_NUDGE_THRESHOLDS: NudgeThresholds = {
71
75
  singleHeavyChars: 40_000, // ~10k tokens in one command — a fat one-shot dump
72
- runLength: 5, // 5 substantive hands-on commands in a row without a hand-off = a by-hand sweep.
76
+ runLength: 3, // 3 substantive hands-on commands in a row without a hand-off = a by-hand sweep.
73
77
  // The standing delegation brief (core/brief.ts) carries the default every turn; this is the
74
- // reactive backstop for the supervisor who grinds a sweep through it anyway.
78
+ // reactive backstop for the supervisor who grinds a sweep through it anyway. It lands EARLY on
79
+ // purpose: by the fifth by-hand step the context is already spent, which is the cost it prevents.
75
80
  minStepChars: 200, // below this a command is glue, not a sweep step
76
- minSweepBurnChars: 8_000, // ~2k tokens cumulative — below this the sweep is cheap triage, not a grind
81
+ minSweepBurnChars: 4_000, // ~1k tokens cumulative — below this the sweep is cheap triage, not a grind
77
82
  failedHandoffBackoff: 1, // suppress one identical immediate repair reminder; the failure remains visible
78
83
  };
79
84
 
@@ -96,18 +101,115 @@ function renderVisibleCheckpoint(title: string, lines: readonly string[]): strin
96
101
  * not all burn is delegable: work bound to one interactive session a sub-agent can't inherit
97
102
  * (a live shell, a specific tunnel) stays with you — then the fix is to keep it lean, not to hand off.
98
103
  */
99
- function renderNudge(reason: "dump" | "sweep", run: number, burn: number, size: number): string {
104
+ function renderNudge(reason: "dump" | "sweep", run: number, burn: number, size: number, hints: NudgeHints = {}): string {
100
105
  const signal =
101
106
  reason === "dump"
102
107
  ? [`Trigger: one direct tool result · ~${toK(size)}k estimated output tokens.`, `Scope: ~${toK(burn)}k in the current direct-work streak.`]
103
108
  : [`Trigger: ${run} substantive direct tool calls · ~${toK(burn)}k estimated output tokens.`, "Scope: since the last successful hand-off."];
109
+ // With a roster the action says TO WHOM (and to size the leg's model); without one it stays generic.
110
+ const action = hints.roster
111
+ ? `Action: hand remaining independent breadth to a leg — ${hints.roster} — choose a known compatible model from your provider or inherit yours, then supervise.`
112
+ : "Action: delegate independent work; repair weak hand-offs, then re-dispatch.";
104
113
  return renderVisibleCheckpoint("delegation checkpoint", [
105
114
  ...signal,
106
- "Action: delegate independent work; repair weak hand-offs, then re-dispatch.",
107
- "Keep local: session-bound work; keep it lean and grep-first.",
115
+ action,
116
+ ...(hints.models ? [`Model candidates (your provider; account support may vary): ${hints.models}`] : []),
117
+ "Keep local: quick checks or coupled/session-bound work; keep it lean and grep-first.",
108
118
  ]);
109
119
  }
110
120
 
121
+ /** Longest roster / model list carried on a checkpoint line — one bounded line, never a paragraph. */
122
+ const HINT_CLIP = 320;
123
+
124
+ /** Run a caller-supplied thunk into one bounded, single-line string, or undefined. The text comes from
125
+ * installed agent descriptions and the model registry, so it is collapsed (no newline can forge a
126
+ * card line) and clipped; a throwing or empty thunk is simply no hint — it must never break the hook. */
127
+ function hint(thunk: (() => string | undefined) | undefined): string | undefined {
128
+ if (!thunk) return undefined;
129
+ let raw: string | undefined;
130
+ try {
131
+ raw = thunk();
132
+ } catch {
133
+ return undefined;
134
+ }
135
+ const text = raw?.replace(/\s+/g, " ").trim();
136
+ if (!text) return undefined;
137
+ return text.length > HINT_CLIP ? `${text.slice(0, HINT_CLIP - 1)}…` : text;
138
+ }
139
+
140
+ /** Live context the reminder cites. Thunks, not values: both change under a running session (agents on
141
+ * `/persona reload`, the session model on a switch), so they are read when the reminder fires. */
142
+ export interface NudgeOptions {
143
+ /** Who to hand the breadth to, e.g. `scout (reads) · operator (edits, runs)`. */
144
+ roster?: () => string | undefined;
145
+ /** Catalog candidates for this provider, not account entitlement (the session's own marked). */
146
+ models?: () => string | undefined;
147
+ }
148
+
149
+ interface NudgeHints {
150
+ roster?: string | undefined;
151
+ models?: string | undefined;
152
+ }
153
+
154
+ /** Longest description carried per agent on the roster line. */
155
+ const ROSTER_DESC_CLIP = 40;
156
+
157
+ /** Order delegate targets by the active persona's own prompt: agents it names come first, in order of
158
+ * first mention, then the rest in their given (registry) order. A mention is a whole-word,
159
+ * case-insensitive match of the agent's name, so `scouting` is not `scout` and `evidence-verifier` is
160
+ * not `verifier`. Generic by construction: the persona text is the data, no name is listed here. A
161
+ * prompt that names no agent (or none at all) leaves the order untouched. Pure; returns a new array. */
162
+ export function orderByMention<T extends { name: string }>(agents: readonly T[], personaPrompt: string | undefined): T[] {
163
+ if (!personaPrompt) return [...agents];
164
+ const firstMention = (name: string): number => {
165
+ if (!name) return Number.POSITIVE_INFINITY;
166
+ const literal = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
167
+ const at = personaPrompt.search(new RegExp(`(?<![\\w-])${literal}(?![\\w-])`, "i"));
168
+ return at < 0 ? Number.POSITIVE_INFINITY : at;
169
+ };
170
+ return agents
171
+ .map((agent, index) => ({ agent, index, at: firstMention(agent.name) }))
172
+ .sort((a, b) => (a.at < b.at ? -1 : a.at > b.at ? 1 : a.index - b.index))
173
+ .map((entry) => entry.agent);
174
+ }
175
+
176
+ /** The reminder's roster: `name (clipped description)` for the first `max` delegate targets, joined
177
+ * ` · `. The caller passes the capability-filtered targets, so it never names an agent the persona
178
+ * cannot call. Undefined when there are none. */
179
+ export function renderNudgeRoster(agents: ReadonlyArray<{ name: string; description?: string }>, max = 4): string | undefined {
180
+ const parts = agents.slice(0, max).map((agent) => {
181
+ const description = (agent.description ?? "").replace(/\s+/g, " ").trim();
182
+ if (!description) return agent.name;
183
+ const clipped = description.length > ROSTER_DESC_CLIP ? `${description.slice(0, ROSTER_DESC_CLIP - 1)}…` : description;
184
+ return `${agent.name} (${clipped})`;
185
+ });
186
+ return parts.length > 0 ? parts.join(" · ") : undefined;
187
+ }
188
+
189
+ /** Nearby naming/generation candidates: exact active model first, then provider-relative cost
190
+ * bands and version-ranked representatives. This does not prove account access or quality.
191
+ * Undefined when there is no session model or the provider has no catalog entries. */
192
+ export function renderNudgeModels(
193
+ models: readonly ModelCostMetadata[],
194
+ session: { provider: string; id: string } | undefined,
195
+ max = 10,
196
+ ): string | undefined {
197
+ if (!session?.provider) return undefined;
198
+ const own = models.filter((m) => m.provider === session.provider);
199
+ if (own.length === 0) return undefined;
200
+ const limit = Number.isFinite(max) ? Math.max(0, Math.floor(max)) : 40;
201
+ if (limit === 0) return undefined;
202
+ const recommendations = recommendModels(own, session, Number.MAX_SAFE_INTEGER);
203
+ const selected = recommendations.slice(0, limit);
204
+ if (selected.length === 0) return undefined;
205
+ const tiers = modelCostTiers(own, shortlistModels(own, undefined, Number.MAX_SAFE_INTEGER));
206
+ const shown = selected.map((m) => {
207
+ const ref = `${m.provider}/${m.id}`;
208
+ return `${ref} [cost:${tiers.get(ref) ?? "unknown"}]${m.id === session.id ? " (you)" : ""}`;
209
+ });
210
+ return `${shown.join(", ")}${recommendations.length > shown.length ? " …" : ""}`;
211
+ }
212
+
111
213
  export class DelegationNudge {
112
214
  private run = 0; // consecutive SUBSTANTIVE hands-on calls since the last hand-off
113
215
  private burn = 0; // cumulative output chars since the last hand-off (for the message only)
@@ -116,9 +218,11 @@ export class DelegationNudge {
116
218
  private failedHandoffKey: string | undefined;
117
219
  private failedHandoffRepeats = 0;
118
220
  private readonly t: NudgeThresholds;
221
+ private readonly opts: NudgeOptions;
119
222
 
120
- constructor(thresholds: NudgeThresholds = DEFAULT_NUDGE_THRESHOLDS) {
223
+ constructor(thresholds: NudgeThresholds = DEFAULT_NUDGE_THRESHOLDS, opts: NudgeOptions = {}) {
121
224
  this.t = thresholds;
225
+ this.opts = opts;
122
226
  }
123
227
 
124
228
  /** Clear the run — call on a new session or persona switch. */
@@ -140,6 +244,7 @@ export class DelegationNudge {
140
244
  if (GLUE_TOOLS.has(toolName)) return undefined;
141
245
  if (HANDOFF_TOOL_NAMES.has(toolName)) {
142
246
  if (!handoffSucceeded) {
247
+ if (toolName === "flow") return undefined; // self-describing failure (abort, bad name, a failed phase): never a re-dispatch cue (a rejected checkpoint is not an error: it counts as a landed hand-off)
143
248
  const normalizedSize = Math.max(0, Math.floor(size));
144
249
  const identity = failureText === undefined ? `size:${normalizedSize}` : fingerprintFailure(failureText);
145
250
  const key = `${toolName}:${identity}`;
@@ -183,7 +288,10 @@ export class DelegationNudge {
183
288
  if (!single && !sweep) return undefined;
184
289
  this.lastNudgeRun = this.run;
185
290
  this.nudges += 1;
186
- return renderNudge(sweep ? "sweep" : "dump", this.run, this.burn, size);
291
+ return renderNudge(sweep ? "sweep" : "dump", this.run, this.burn, size, {
292
+ roster: hint(this.opts.roster),
293
+ models: hint(this.opts.models),
294
+ });
187
295
  }
188
296
  }
189
297
 
@@ -193,32 +301,31 @@ export class DelegationNudge {
193
301
  * UP: the child's report carries an explicit surrender/blocked marker. It returns a reminder to
194
302
  * append to that very result — in RECENT context, on the leg that just landed — that a delegating
195
303
  * supervisor must not bank a premature block: confirm it names a genuine missing capability, else
196
- * steer it back with the recovery pass or re-dispatch. This is the runtime half of the operator
304
+ * verify it directly or re-dispatch with a sharper brief. This is the runtime half of the operator
197
305
  * protocol's "reject premature surrender" rule; the top-of-prompt persona directive has decayed by
198
306
  * the time a blocked leg returns.
199
307
  *
200
308
  * Keyed on the ecosystem's OWN explicit protocol tokens (an operator emits `[BLOCKED: need X]`, a CTF
201
309
  * leg emits `FLAG: UNKNOWN`) — never a fuzzy "sounds stuck" guess, so it does not misfire on ordinary
202
- * prose. It inspects only `delegate`/`council` results (a child's report); the supervisor's own tools
310
+ * prose. It inspects only `delegate`/`council`/`flow` results (a child's report); the supervisor's own tools
203
311
  * are DelegationNudge's job, so the two never fire on the same event. Pure — no I/O, unit-testable.
204
312
  */
205
313
 
206
- /** Tools whose result is a delegated child's report (where a surrender can surface). */
207
-
208
314
  /** Explicit surrender/blocked markers the operator + CTF protocols emit (operator.md). */
209
315
  const SURRENDER_MARKERS: readonly RegExp[] = [/\[BLOCKED\b/i, /\bFLAG:\s*UNKNOWN\b/i];
210
316
 
211
317
  const PERSISTENCE_NOTE = renderVisibleCheckpoint("delegated result needs verification", [
212
318
  "Trigger: a delegated leg returned BLOCKED/UNKNOWN.",
213
319
  "Verify: accept only concrete external blockers (access, credentials, or tools).",
214
- "Otherwise: run the recovery pass or re-dispatch with a sharper brief.",
320
+ "Otherwise: verify directly or re-dispatch with a sharper brief.",
215
321
  ]);
216
322
 
217
323
  export class PersistenceNudge {
218
324
  /**
219
325
  * Feed one supervisor tool result: its tool name and the full text of the result. Returns the
220
326
  * reminder to append when a delegated leg's report signals premature surrender, else undefined.
221
- * The SYNC path (a `delegate`/`council` tool_result the supervisor sees inline).
327
+ * The INLINE path: a `delegate`/`council`/`flow` tool_result the supervisor sees in the launching call,
328
+ * which only a headless session produces (an interactive one backgrounds all three).
222
329
  */
223
330
  observe(toolName: string, text: string): string | undefined {
224
331
  if (!HANDOFF_TOOL_NAMES.has(toolName)) return undefined;
@@ -227,11 +334,57 @@ export class PersistenceNudge {
227
334
 
228
335
  /**
229
336
  * Tool-agnostic surrender check on already-known-to-be-a-leg-report text. The ASYNC path uses
230
- * this: a background run (the interactive default) delivers its report through the completion
231
- * notifier as a fresh follow-up, NOT a `delegate` tool_result, so {@link observe}'s tool gate
232
- * would never see it — the caller has already established the text is a settled leg's output.
337
+ * this: a background run (every interactive `delegate`, `council` and `flow`) delivers its report
338
+ * through the completion notifier as a fresh follow-up, or `intercom result` collects it explicitly
339
+ * — NOT a tool_result, so {@link observe}'s tool gate would never see it. The caller has already
340
+ * established the text is a settled leg's output.
233
341
  */
234
342
  scan(text: string): string | undefined {
235
343
  return SURRENDER_MARKERS.some((re) => re.test(text)) ? PERSISTENCE_NOTE : undefined;
236
344
  }
237
345
  }
346
+
347
+ /**
348
+ * PollGuard — the call-time half of "end your turn, a finished run or batch wakes you". A peek (or a
349
+ * `result` on a still-running run) whose signature equals the previous one for the same scope
350
+ * learned nothing; asking again only holds the turn that the idle-gated completion wake needs to be
351
+ * idle. Per scope, the first repeat is refused and the second asks Pi to end the turn (`terminate` is
352
+ * a batch hint Pi honours only when every call in the batch sets it). No clock: a changed signature —
353
+ * a run settled, started, queued, began waiting or newly crossed the stall threshold — is
354
+ * new information and always allowed. Pure.
355
+ */
356
+ export class PollGuard {
357
+ private readonly seen = new Map<string, { sig: string; strikes: number }>();
358
+
359
+ check(scope: string, sig: string): "ok" | "repeat" | "terminate" {
360
+ const entry = this.seen.get(scope);
361
+ if (entry?.sig !== sig) {
362
+ this.seen.set(scope, { sig, strikes: 0 });
363
+ return "ok";
364
+ }
365
+ entry.strikes += 1;
366
+ return entry.strikes === 1 ? "repeat" : "terminate";
367
+ }
368
+
369
+ reset(): void {
370
+ this.seen.clear();
371
+ }
372
+ }
373
+
374
+ /** A leading sleep at/above this many seconds holds the turn that background-child wakes need idle. */
375
+ const LONG_WAIT_SECONDS = 20;
376
+
377
+ const SLEEP_UNIT_SECONDS: Readonly<Record<string, number>> = { s: 1, m: 60, h: 3600, d: 86_400 };
378
+ // Only the leading statement: GNU `sleep N[smhd]` and PowerShell `Start-Sleep [-Seconds|-s] N` /
379
+ // `-Milliseconds|-m N`. Loops, `a; sleep 30`, scripts and `timeout`/`ping` forms are deliberately out.
380
+ const LEADING_SLEEP = /^\s*(?:sleep\s+(?<bash>[\d.]+)(?<unit>[smhd])?|start-sleep(?:\s+-(?:seconds|s)(?=[:\s]))?[:\s]\s*(?<ps>[\d.]+)|start-sleep\s+-(?:milliseconds|m)[:\s]\s*(?<ms>[\d.]+))(?=[\s;&|)]|$)/i;
381
+
382
+ /** Seconds of a leading long sleep in a shell command, else undefined. */
383
+ export function longWaitSeconds(command: string): number | undefined {
384
+ const g = LEADING_SLEEP.exec(command)?.groups;
385
+ if (!g) return undefined;
386
+ const seconds = g.ms !== undefined
387
+ ? Number(g.ms) / 1000
388
+ : Number(g.bash ?? g.ps) * (SLEEP_UNIT_SECONDS[g.unit?.toLowerCase() ?? "s"] ?? 1);
389
+ return seconds >= LONG_WAIT_SECONDS ? seconds : undefined;
390
+ }
package/src/core/seed.ts CHANGED
@@ -93,7 +93,8 @@ export interface SeedResult {
93
93
 
94
94
  /** Exact bytes from earlier releases whose seeded copies are known to be stale. A user file is
95
95
  * eligible only when BOTH size and digest match. Keep this explicit allow-list deliberately
96
- * small: it is a migration policy, not a fuzzy upgrader. */
96
+ * small: it is a migration policy, not a fuzzy upgrader. Before tagging a release, add the
97
+ * previous tag's digest for every changed asset; the guard test in seed.test.ts prints the row. */
97
98
  export interface SeededDefaultSpec {
98
99
  size: number;
99
100
  sha256: string;
@@ -105,9 +106,14 @@ export interface SeededDefaultSpec {
105
106
  export type SeededDefaultEntry = SeededDefaultSpec | readonly SeededDefaultSpec[];
106
107
 
107
108
  const ELITE_REQUIRED_ADDITIONS = ["agents/evidence-verifier.md"] as const;
109
+ /** A migrated planner declares the `design` loop, whose generator is the `architect` agent. */
110
+ const PLANNER_REQUIRED_ADDITIONS = ["agents/architect.md"] as const;
108
111
 
109
112
  export const LEGACY_SEEDED_DEFAULTS: Readonly<Record<string, SeededDefaultEntry>> = {
110
113
  "personas/dev.md": [
114
+ // v1.17.2 pristine seed (LF and CRLF checkout variants).
115
+ { size: 2712, sha256: "2dff44e3c6626d5eb35c5a9d74d77e345c95319bb6d310101ba53f6f330165b4" },
116
+ { size: 2757, sha256: "afdafb105c14a4e4ed960969779f4059491d85bf463739cf01079fe9831ace5b" },
111
117
  // v1.13.4 pristine seed (LF and CRLF checkout variants)
112
118
  { size: 5907, sha256: "1d7abbc482a26c452e2588e405bdcb60af536df1f81aa0b709dcff70554915d2" },
113
119
  { size: 5977, sha256: "a1b46938316636bc90f525d031490eef3ee46363abe3aa199b267449b8bff06a" },
@@ -119,44 +125,113 @@ export const LEGACY_SEEDED_DEFAULTS: Readonly<Record<string, SeededDefaultEntry>
119
125
  { size: 5194, sha256: "0899ee3b10971c2b324e9e9c868fcab833ea80a80b83d94a1f0a95147475e9b5" },
120
126
  // v1.12.1
121
127
  { size: 5195, sha256: "54da40a44a777f8a3b36b6c038248116e19be5a6515707747d070dc3f32ead56" },
128
+ // v1.14.0–v1.17.1 pristine seed (LF and CRLF checkout variants).
129
+ { size: 3292, sha256: "d058a5cf683fa178ffb6c6fc98172328680d0fb19e21cf46a24d42613b5c48de" },
130
+ { size: 3344, sha256: "a11cd85fd22b1c426c5fe148ceab8550f1815aace0702566fea3ad742bc25c13" },
122
131
  ],
123
132
  "personas/planner.md": [
124
133
  // v1.13.4 pristine seed (LF and CRLF checkout variants)
125
- { size: 3350, sha256: "78a1f1c88d4d779dca46414029a247e8c48db85b3b7266be9366c6dc23714d87" },
126
- { size: 3397, sha256: "8055b7440f2cf1e79378196eda675bdeb5d58b8941d450f8901672cf82a9e876" },
134
+ { size: 3350, sha256: "78a1f1c88d4d779dca46414029a247e8c48db85b3b7266be9366c6dc23714d87", requiredAdditions: PLANNER_REQUIRED_ADDITIONS },
135
+ { size: 3397, sha256: "8055b7440f2cf1e79378196eda675bdeb5d58b8941d450f8901672cf82a9e876", requiredAdditions: PLANNER_REQUIRED_ADDITIONS },
136
+ // v1.14.0–v1.17.1 pristine seed (LF and CRLF checkout variants).
137
+ { size: 2456, sha256: "a238aa8dbe01f42edc707585d1eccee3b63401eb1dbe91b607347a49624d27cb", requiredAdditions: PLANNER_REQUIRED_ADDITIONS },
138
+ { size: 2493, sha256: "f8f8a8cdb6110022984556a7102f523411053282f818249c20ec8e0153c810e1", requiredAdditions: PLANNER_REQUIRED_ADDITIONS },
127
139
  ],
128
140
  "personas/researcher.md": [
129
141
  // v1.13.4 pristine seed (LF and CRLF checkout variants)
130
142
  { size: 4441, sha256: "a3819ea76afee2a2f14d50c99198a09d4a4bd858e22ea9e4ceb3d868dcbb78c8" },
131
143
  { size: 4498, sha256: "2bd9dcaf47bd4ef3d139be361d260fbf7c9caa523144548593cdfc7ea4a2246c" },
144
+ // v1.14.0–v1.17.1 pristine seed (LF and CRLF checkout variants).
145
+ { size: 2606, sha256: "68a1e62ddde7b0b0ecfd165e699c76cf6d6f3d533b6d1051dc4d5a64e9968807" },
146
+ { size: 2643, sha256: "717fcd632de176e339928158516f35a6a4d82588e6ca36083726d1dbcfdc0e18" },
132
147
  ],
133
148
  "personas/audit.md": [
134
149
  // v1.13.4 pristine seed (LF and CRLF checkout variants)
135
150
  { size: 2110, sha256: "ea893333fc128a760455bb4eaac3ee2d384163f40950fca2c201b41860bd9d8a" },
136
151
  { size: 2145, sha256: "04f2167437081d69806a704238411275006df98e6ee902bc0a8cb6eb81b0ef14" },
152
+ // v1.14.0–v1.17.1 pristine seed (LF and CRLF checkout variants).
153
+ { size: 1636, sha256: "c35e053ef8578d93b1aa2efa4ecb97138ef84542f9cd7b524b1c7a267c4fdc05" },
154
+ { size: 1665, sha256: "0c8dd4e637570aa82b25070a9027a2f46bda25a25f2195d06df0ccb6d98bb211" },
137
155
  ],
138
156
  "personas/verify.md": [
139
157
  // v1.13.4 pristine seed (LF and CRLF checkout variants)
140
158
  { size: 1927, sha256: "4c670aba28ce15937d60074ec4ac7d50f6db3cccf137b60d9758627d69e82a0f" },
141
159
  { size: 1955, sha256: "9696278c243f83c9e8ee5f128faacebb8a89f85464b2c182f1bb77365a6d5643" },
160
+ // v1.14.0–v1.17.1 pristine seed (LF and CRLF checkout variants).
161
+ { size: 2127, sha256: "d17d2f73f13388f1c92a13045b7a6d1032aa6b710ab4147dfc2eacd27f07a522" },
162
+ { size: 2158, sha256: "7e26e68b77793e7349a2945f7674d5a116e3d1cfc5299e038d7b3a468e977ab2" },
163
+ ],
164
+ // v1.7.0–v1.17.1 pristine seeds (LF and CRLF checkout variants); these two had no row before.
165
+ "personas/judge.md": [
166
+ { size: 1778, sha256: "2f8c987de59d0f2aac6bdb62f556fc5937605d1036b4a42c8ada7a48461d4710" },
167
+ { size: 1810, sha256: "3f0287dd7052f1f0bc65b40dc3af52847c0e5b2295cfcb4001ebe345b9e3fadc" },
168
+ ],
169
+ "personas/magi.md": [
170
+ { size: 2361, sha256: "a608fc26a13070fad79b84f9d1a5f39fd3b760aefb98732f1f95134a89cd0366" },
171
+ { size: 2402, sha256: "0b24f01d93517d5fc56a7318d374ee679fa146e919e86e753f422f9f10a37bda" },
142
172
  ],
143
173
  // The MAGI cores gained `purpose:` (their verticalization) in v1.10.6. Agents are seeded COPIES,
144
174
  // not a live layer read from the package, so without these an existing install keeps three cores
145
175
  // that render as bare names — the label feature would ship inert for everyone who already has them.
146
- "agents/melchior.md": { size: 1503, sha256: "aa7a69296640f1023e00e2f2285b12cfdee158bb89f7b0b3d8c03a6317de9508" },
147
- "agents/balthasar.md": { size: 1527, sha256: "27273ed49ea265f528efad4724e6abec6d051fa60d1f4fa678982e7383364981" },
148
- "agents/casper.md": { size: 1508, sha256: "5074341b7e1ae7e9a44bc71de0405a17ccb404eacfaeb2fcf40f2ac4c23828ea" },
176
+ // Each core lists v1.0.0–v1.10.5, then v1.11.0–v1.14.0 (adds the confidence tie-break paragraph).
177
+ "agents/melchior.md": [
178
+ { size: 1503, sha256: "aa7a69296640f1023e00e2f2285b12cfdee158bb89f7b0b3d8c03a6317de9508" },
179
+ { size: 1523, sha256: "8a78c1ff201d728187fe19f19ea5f62f2d02f67c5cc5c3a9e5255c8776cbe74d" },
180
+ // v1.17.1 pristine seed (LF and CRLF checkout variants).
181
+ { size: 1747, sha256: "43ab5f65ecb89260c09b7e33daef3b68b1bc1c60c81b73b245fdf9984f601044" },
182
+ { size: 1779, sha256: "6b4dcc0cc7416ac9a4543b7330edaeefb29b1006cc5e67b86f15ca94c4eaaf24" },
183
+ ],
184
+ "agents/balthasar.md": [
185
+ { size: 1527, sha256: "27273ed49ea265f528efad4724e6abec6d051fa60d1f4fa678982e7383364981" },
186
+ { size: 1549, sha256: "9e91de81e76dc6fab13ed5a6d0c26319ca89da8da6fb5c71c8d852fcb86b73f0" },
187
+ // v1.17.1 pristine seed (LF and CRLF checkout variants).
188
+ { size: 1773, sha256: "6716f5c68f2eb0bc1e72b33cef3e78b062efa07a01d12edb95e671a482832753" },
189
+ { size: 1805, sha256: "baafecc02b01c962bfc933b3f3f768808d541c85b0e01b29f6ff04a710f6d716" },
190
+ ],
191
+ "agents/casper.md": [
192
+ { size: 1508, sha256: "5074341b7e1ae7e9a44bc71de0405a17ccb404eacfaeb2fcf40f2ac4c23828ea" },
193
+ { size: 1531, sha256: "97f2fd367b7c13bfaec39d9bd61065023ef501a519d95e516e39842c487f9427" },
194
+ // v1.17.1 pristine seed (LF and CRLF checkout variants).
195
+ { size: 1832, sha256: "32effd595231b7bb6ce6af01c87108bb030d489cc8758946206161e148e270e4" },
196
+ { size: 1865, sha256: "9d22349de3252074ba7d1482ee942c36e384181939ec71f1fddfd4c527d3d436" },
197
+ ],
149
198
  // Current pristine seed before the compact-digest/reusable-artifact prompt update (LF and CRLF).
150
199
  "agents/research.md": [
151
200
  { size: 3771, sha256: "6d084d11c84a594ddac0986bcf81aea6e5768b9df18196fedc8715bfd3f1eb16" },
152
201
  { size: 3825, sha256: "768899c4c9a5f60a8ff18602d7ee5a696414fd35a8d53e6ae2cc9dfa41d7a88f" },
202
+ // v1.17.1 pristine seed (LF and CRLF checkout variants).
203
+ { size: 4132, sha256: "bb35ec537ae654bf60e8552721dfd797db34b5dd5f623b23a95adf2cd72c70e0" },
204
+ { size: 4190, sha256: "b579b08e6a9035b8ec9e55166f7bbe3dd11d421edca4e0df6146ceff4fe3d459" },
153
205
  ],
154
206
  // v1.14.1 pristine seed (LF and CRLF checkout variants).
155
207
  "agents/evidence-verifier.md": [
156
208
  { size: 1465, sha256: "428be556abbbd530826631ef255cd7effa3484b00b0c7c35877e8b0c9a786aef" },
157
209
  { size: 1487, sha256: "5e4b2b41700bf7b669769f40dd5e98b41db0d708e1dec75697d06fd6abeaa2a1" },
210
+ // v1.17.1 pristine seed (LF and CRLF checkout variants).
211
+ { size: 1560, sha256: "4fa0af121789e4408bde6ae1d8ced73c7b42adf31e4bf48121621e1047b09861" },
212
+ { size: 1583, sha256: "3156e4748dc32bfd0d4699054a0a8407f7110270ce717af774d54d34d0e98d13" },
213
+ ],
214
+ // v1.17.1 pristine seeds (LF and CRLF checkout variants); these four had no row before.
215
+ "agents/operator.md": [
216
+ { size: 4598, sha256: "be7a18654c6106c8fa3be9694c75ef375d17bf13789bfce9ed1f76012d646d59" },
217
+ { size: 4661, sha256: "9031054af66bca2b726536e5348f6d0223f66bb7b544cc256457b015d55c850f" },
218
+ ],
219
+ "agents/reviewer.md": [
220
+ { size: 3427, sha256: "8570b80e48a2b44dc364f7a41b3cd06741507329d91b0b9d7e1b716e7eaf7ee2" },
221
+ { size: 3476, sha256: "73f404259730719051f8e9aedb17ddd398304ed40068128cd97eae3a884b0912" },
222
+ ],
223
+ "agents/scout.md": [
224
+ { size: 1341, sha256: "ffea9fe3643dbc0e5dea8ffc0722f41050374daed33e7f3a46378de36a630a62" },
225
+ { size: 1364, sha256: "425b8ef2c1141b602b2f67e47224f855a8a11f936affd4429272347678de8b58" },
226
+ ],
227
+ "agents/verifier.md": [
228
+ { size: 1037, sha256: "ce8f161b1879084b336e2f24495a82aba7279f099b450852ba02db8b4850def5" },
229
+ { size: 1058, sha256: "78d7c9e7e6d961b0005d216968196dea28b8ef09a8ccd52b0f7a4de97754a800" },
158
230
  ],
159
231
  "personas/elite.md": [
232
+ // v1.17.2 pristine seed (LF and CRLF checkout variants).
233
+ { size: 17580, sha256: "5a45759f0c94ea090c8a537c59a6549449ed089d0f02fac9a09ba14b01a09ff2", requiredAdditions: ELITE_REQUIRED_ADDITIONS },
234
+ { size: 17835, sha256: "425e0f4fbeb855a788706eb278db4ed4c1026c59bedcca9768c258966ede1654", requiredAdditions: ELITE_REQUIRED_ADDITIONS },
160
235
  {
161
236
  // v1.0.0
162
237
  size: 10489,
@@ -217,6 +292,12 @@ export const LEGACY_SEEDED_DEFAULTS: Readonly<Record<string, SeededDefaultEntry>
217
292
  sha256: "dbe4a4da34338e4210b5abf9a902b4c56bba31eced1f00eb775e30b25ee37b90",
218
293
  requiredAdditions: ELITE_REQUIRED_ADDITIONS,
219
294
  },
295
+ {
296
+ // v1.12.2–v1.14.0
297
+ size: 19312,
298
+ sha256: "a817f6e76164a142a15ec843978942e31ab765ae463bdded3966c0863d010894",
299
+ requiredAdditions: ELITE_REQUIRED_ADDITIONS,
300
+ },
220
301
  {
221
302
  // v1.14.1 pristine seed (LF and CRLF checkout variants).
222
303
  size: 19417,
@@ -228,10 +309,18 @@ export const LEGACY_SEEDED_DEFAULTS: Readonly<Record<string, SeededDefaultEntry>
228
309
  sha256: "69be3a81b6b20ff51e21f8a6730049a5f740dd00c069f21d1a5a2184fda97f24",
229
310
  requiredAdditions: ELITE_REQUIRED_ADDITIONS,
230
311
  },
312
+ // v1.14.2–v1.16.1 and v1.16.2–v1.17.1 pristine seeds (LF and CRLF checkout variants each).
313
+ { size: 19575, sha256: "7a349e63abc07c60c3937eb01005c0567e27327ec13a79550fcfe7deb5f87630", requiredAdditions: ELITE_REQUIRED_ADDITIONS },
314
+ { size: 19831, sha256: "7b078a2b2c2339df1169351fd58c97d5e99e4b33bff00c43229e566f0a737701", requiredAdditions: ELITE_REQUIRED_ADDITIONS },
315
+ { size: 19585, sha256: "23146654476bc477c48ce972b13566a31198c41c1aa051540d25cfb45394ab9d", requiredAdditions: ELITE_REQUIRED_ADDITIONS },
316
+ { size: 19841, sha256: "2751df2d27066c1d7398be3cc4d72abc4ecba01fb757c34b949bf74d64154dcb", requiredAdditions: ELITE_REQUIRED_ADDITIONS },
231
317
  ],
232
318
  // Live-peer collaboration landed in v1.12.2. Without these, a seeded copy from 1.7–1.12.1
233
319
  // (and the two earlier untouched hashes) keeps the pre-collaboration prompt forever.
234
320
  "personas/swarm.md": [
321
+ // v1.17.2 pristine seed (LF and CRLF checkout variants).
322
+ { size: 2694, sha256: "72d17416941edb6f31ca86cff1de6fa6d85c5ba7ae0cd269d104c770c9e42bf2" },
323
+ { size: 2737, sha256: "303ddc6bd44f2f909765d99d8f753136dae9d8a635616d6e65523814e06131a1" },
235
324
  // v1.13.4 pristine seed (LF and CRLF checkout variants)
236
325
  { size: 2729, sha256: "3d5060277ecaec717e87beb8d95b0fe9f3dc70fe5f0192823aa79b55f9e49507" },
237
326
  { size: 2768, sha256: "b81910521e05d62e06abaf0f2a89bd18ad252ba968bb296cc85dea973028e7a5" },
@@ -241,6 +330,10 @@ export const LEGACY_SEEDED_DEFAULTS: Readonly<Record<string, SeededDefaultEntry>
241
330
  { size: 1857, sha256: "77de7f590fdede6ede4848be11b39fb16bfb4733687ac1b1244cc59e626874c6" }, // v1.1.0–v1.4.0
242
331
  { size: 1955, sha256: "b7a7caa38c4d31c8cc27b85eb7facbf2339a024ad8d4cd2ba681a2a885de7646" }, // v1.5.1–v1.6.3
243
332
  { size: 2285, sha256: "985a1c5455b2c81537a55b31daed216d7f67a897b3b2d5bde35d64f3779d6fe4" }, // v1.7.0–v1.12.1
333
+ { size: 2360, sha256: "ae0cfe758d9e1b9a4d2d4bad151999418e10e8291226764e7d155b44ce8ff5ea" }, // v1.14.0–v1.14.2
334
+ // v1.15.0–v1.17.1 pristine seed (LF and CRLF checkout variants).
335
+ { size: 2570, sha256: "3e6537f555d35b396041166d38da5cb397e076d00c258e25992d2e6abb8a37ef" },
336
+ { size: 2608, sha256: "c54acac4f88a1dfbe7b21eb3c4bcea658896f953ea8de5d592bc06d2ad0f312a" },
244
337
  ],
245
338
  };
246
339
 
@@ -0,0 +1,87 @@
1
+ /**
2
+ * What a session restore cannot carry over.
3
+ *
4
+ * Background runs, armed timers and monitors live only in the memory of one extension instance. A
5
+ * `/reload`, an in-session `/resume` or `pi -c` tears that instance down, so the restored supervisor
6
+ * would otherwise keep waiting for wakes that can no longer come, and run ids would restart at
7
+ * `run-1` and collide with the ids already in its history. The shutdown hook records the loss and
8
+ * the id counter in a session entry; the next start reads it back (the session-identity pattern)
9
+ * and tells the supervisor once.
10
+ *
11
+ * Everything read back is untrusted session data, so nothing but run ids matching a strict shape and
12
+ * small counts is ever kept: no free text can reach the model through this entry. Pure — no I/O.
13
+ */
14
+
15
+ /** Custom session-entry type for {@link RunsEntry}; deliberately unrendered. */
16
+ export const RUNS_ENTRY_CUSTOM_TYPE = "pi-persona:runs";
17
+
18
+ const RUN_ID = /^run-[0-9a-z]{1,8}$/;
19
+ /** One cap for every count and the id list (a full 64-leg fan-out fits whole); the entry stays small. */
20
+ const MAX_COUNT = 99;
21
+ const MAX_ISSUED = 1_000_000_000;
22
+ /** Ids named in the notice itself; the rest are counted. */
23
+ const MAX_NAMED_IDS = 8;
24
+
25
+ export interface LostWork {
26
+ runs: string[];
27
+ timers: number;
28
+ monitors: number;
29
+ }
30
+
31
+ export interface RunsEntry {
32
+ version: 1;
33
+ sessionId: string;
34
+ /** Run ids issued so far in this session lineage: the next instance continues from here. */
35
+ issued: number;
36
+ lost: LostWork;
37
+ }
38
+
39
+ const count = (value: unknown): number => (Number.isInteger(value) && (value as number) > 0 ? Math.min(value as number, MAX_COUNT) : 0);
40
+
41
+ function cleanLost(value: { runs?: unknown; timers?: unknown; monitors?: unknown }): LostWork {
42
+ const runs = (Array.isArray(value.runs) ? value.runs : []).filter((id): id is string => typeof id === "string" && RUN_ID.test(id));
43
+ return { runs: [...new Set(runs)].slice(0, MAX_COUNT), timers: count(value.timers), monitors: count(value.monitors) };
44
+ }
45
+
46
+ export function makeRunsEntry(sessionId: string, issued: number, lost: LostWork): RunsEntry {
47
+ return { version: 1, sessionId, issued, lost: cleanLost(lost) };
48
+ }
49
+
50
+ /**
51
+ * The newest runs entry written for THIS session id, or undefined. The newest one wins even when it
52
+ * is malformed (fail closed): skipping it would let an older loss replay after it was acknowledged.
53
+ */
54
+ export function findStoredRuns(entries: readonly unknown[], sessionId: string): RunsEntry | undefined {
55
+ if (!sessionId) return undefined;
56
+ let data: Partial<RunsEntry> | undefined;
57
+ for (const entry of entries) {
58
+ if (!entry || typeof entry !== "object") continue;
59
+ const item = entry as { type?: unknown; customType?: unknown; data?: unknown };
60
+ if (item.type !== "custom" || item.customType !== RUNS_ENTRY_CUSTOM_TYPE) continue;
61
+ if (!item.data || typeof item.data !== "object") continue;
62
+ const candidate = item.data as Partial<RunsEntry>;
63
+ if (candidate.version === 1 && candidate.sessionId === sessionId) data = candidate;
64
+ }
65
+ if (!data) return undefined;
66
+ const { issued, lost } = data;
67
+ if (typeof issued !== "number" || !Number.isInteger(issued) || issued < 0 || issued > MAX_ISSUED) return undefined;
68
+ if (!lost || typeof lost !== "object") return undefined;
69
+ return makeRunsEntry(sessionId, issued, lost);
70
+ }
71
+
72
+ const plural = (n: number, noun: string): string => `${n} ${noun}${n === 1 ? "" : "s"}`;
73
+
74
+ /** The one line the restored supervisor is told, or undefined when nothing was lost. */
75
+ export function restoreNotice(lost: LostWork): string | undefined {
76
+ const parts: string[] = [];
77
+ if (lost.runs.length > 0) {
78
+ const named = lost.runs.slice(0, MAX_NAMED_IDS).join(", ");
79
+ const more = lost.runs.length - MAX_NAMED_IDS;
80
+ parts.push(`run${lost.runs.length === 1 ? "" : "s"} ${named}${more > 0 ? ` +${more} more` : ""} (stopped or never reported)`);
81
+ }
82
+ if (lost.timers > 0) parts.push(plural(lost.timers, "timer"));
83
+ if (lost.monitors > 0) parts.push(plural(lost.monitors, "monitor"));
84
+ if (parts.length === 0) return undefined;
85
+ return `Session restored. Background work from before the restore did not survive (it lives only in memory): ${parts.join("; ")}. `
86
+ + "Earlier run results can no longer be fetched. Do not wait for them; relaunch or re-arm only what you still need.";
87
+ }
@@ -50,7 +50,7 @@ export const PERSONA_SETTINGS: readonly PersonaSetting[] = [
50
50
  { id: "checkInEveryMs", group: "Delegation", label: "Progress check-in interval", description: "Milliseconds between operator-only progress entries; zero disables check-ins.", kind: "number", env: "PI_PERSONA_CHECKIN_MS", applyWhen: APPLY_WHEN, min: 0, max: MAX_TIMER_DELAY_MS },
51
51
  { id: "agentHardTimeoutMs", group: "Delegation", label: "Agent hard timeout", description: "Opt-in wall-clock limit in milliseconds for one agent run; zero means unlimited.", kind: "number", env: "PI_PERSONA_AGENT_MAX_MS", applyWhen: APPLY_WHEN, min: 0, max: MAX_TIMER_DELAY_MS },
52
52
  { id: "agentStartupTimeoutMs", group: "Delegation", label: "Agent startup deadline", description: "Milliseconds allowed without first progress while an agent starts; zero disables the deadline.", kind: "number", env: "PI_PERSONA_AGENT_STARTUP_MS", applyWhen: APPLY_WHEN, min: 0, max: MAX_TIMER_DELAY_MS },
53
- { id: "nudge", group: "Delegation", label: "Delegation nudges", description: "Delegation and persistence checkpoints; disabling silences both.", kind: "boolean", env: "PI_PERSONA_NUDGE", applyWhen: APPLY_WHEN },
53
+ { id: "nudge", group: "Delegation", label: "Delegation nudges", description: "Delegation and persistence checkpoints, plus the repeated peek/result and long-sleep guards; disabling turns all of them off.", kind: "boolean", env: "PI_PERSONA_NUDGE", applyWhen: APPLY_WHEN },
54
54
  { id: "broker", group: "Collaboration", label: "Cross-process communication broker", description: "Connect child-engine agents to supervisor messaging and steering.", kind: "boolean", env: "PI_PERSONA_BROKER", applyWhen: APPLY_WHEN },
55
55
  { id: "exocom", group: "Collaboration", label: "External agent communication", description: "Enable communication between independent top-level Pi instances.", kind: "boolean", env: "PI_PERSONA_EXOCOM", applyWhen: APPLY_WHEN },
56
56
  { id: "asyncRetain", group: "Delegation", label: "Retained async results", description: "Number of settled async runs retained for later collection.", kind: "number", env: "PI_PERSONA_ASYNC_RETAIN", applyWhen: APPLY_WHEN, min: 1, max: MAX_SAFE_INTEGER },