@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.
- package/AGENTS.md +59 -28
- package/CHANGELOG.md +234 -0
- package/README.md +16 -12
- package/agents/architect.md +21 -0
- package/agents/balthasar.md +17 -22
- package/agents/casper.md +17 -23
- package/agents/evidence-verifier.md +31 -16
- package/agents/melchior.md +17 -22
- package/agents/operator.md +15 -55
- package/agents/research.md +14 -48
- package/agents/reviewer.md +20 -39
- package/agents/scout.md +14 -15
- package/agents/verifier.md +15 -16
- package/docs/ARCHITECTURE.md +183 -55
- package/docs/EXPERIENCE.md +19 -9
- package/docs/MONITORS.md +62 -3
- package/docs/REFERENCE.md +82 -32
- package/docs/SPINE.md +2 -2
- package/docs/STRATEGIES.md +23 -8
- package/docs/TELEMETRY.md +1 -0
- package/flows/deep-review.flow.json +1 -1
- package/package.json +9 -8
- package/personas/audit.md +28 -17
- package/personas/dev.md +30 -32
- package/personas/elite.md +223 -221
- package/personas/judge.md +25 -19
- package/personas/magi.md +26 -29
- package/personas/planner.md +31 -24
- package/personas/researcher.md +25 -26
- package/personas/swarm.md +35 -25
- package/personas/verify.md +30 -19
- package/src/agents/agent.ts +20 -0
- package/src/bridge.ts +3 -2
- package/src/core/brief.ts +429 -356
- package/src/core/config.ts +15 -10
- package/src/core/model-catalog.ts +171 -0
- package/src/core/model-tier.ts +89 -0
- package/src/core/models.ts +13 -4
- package/src/core/monitor-output.ts +85 -0
- package/src/core/monitor.ts +34 -2
- package/src/core/nudge.ts +176 -23
- package/src/core/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +1 -1
- package/src/core/timer.ts +34 -9
- package/src/engine/adapter.ts +12 -8
- package/src/engine/async.ts +165 -146
- package/src/engine/child.ts +2 -2
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +12 -7
- package/src/engine/stream.ts +4 -20
- package/src/exocom/install.ts +2 -1
- package/src/extension/engine.ts +43 -16
- package/src/extension/hooks.ts +106 -36
- package/src/extension/persona-model-settings.ts +31 -13
- package/src/extension/shared.ts +15 -7
- package/src/extension.ts +505 -241
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +26 -5
- package/src/orchestration/flow-run.ts +38 -13
- package/src/orchestration/flow.ts +9 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/sdk.ts +17 -5
- package/src/orchestration/strategies/compete.ts +5 -1
- package/src/orchestration/strategies/council-rounds.ts +2 -0
- package/src/orchestration/strategies/critic-loop.ts +3 -0
- package/src/orchestration/strategies/debate.ts +2 -0
- package/src/orchestration/strategies/fanout.ts +3 -2
- package/src/orchestration/strategies/judge.ts +5 -1
- package/src/orchestration/strategies/magi.ts +40 -23
- package/src/orchestration/strategies/map.ts +13 -3
- package/src/orchestration/strategies/pair.ts +2 -0
- package/src/orchestration/strategies/pipeline.ts +2 -0
- package/src/orchestration/strategies/synthesize.ts +27 -7
- package/src/orchestration/strategy.ts +31 -0
- package/src/persona/persona.ts +7 -3
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/background-run.ts +151 -0
- package/src/tools/council.ts +77 -52
- package/src/tools/delegate-tool.ts +310 -280
- package/src/tools/delegate.ts +22 -210
- package/src/tools/exocom-work.ts +18 -53
- package/src/tools/exocom.ts +15 -19
- package/src/tools/flow.ts +85 -28
- package/src/tools/intercom-tool.ts +108 -118
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +19 -9
- package/src/tools/monitor.ts +39 -6
- package/src/tools/timer.ts +11 -13
- package/src/ui/agent-overlay.ts +55 -14
- package/src/ui/agent-tree.ts +84 -18
- package/src/ui/compact-widget.ts +25 -0
- package/src/ui/failure-summary.ts +55 -0
- package/src/ui/literal-tool-result.ts +24 -0
- package/src/ui/model-picker.ts +158 -81
- package/src/ui/persona-settings.ts +9 -3
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +189 -0
- package/src/ui/wake-rows.ts +262 -0
- 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
|
|
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,
|
|
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:
|
|
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:
|
|
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
|
-
|
|
107
|
-
|
|
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
|
-
*
|
|
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:
|
|
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
|
|
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 (
|
|
231
|
-
* notifier as a fresh follow-up,
|
|
232
|
-
*
|
|
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
|
-
|
|
147
|
-
"agents/
|
|
148
|
-
|
|
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
|
+
}
|
package/src/core/settings.ts
CHANGED
|
@@ -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
|
|
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 },
|