@arnilo/prism 0.10.0 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (60) hide show
  1. package/CHANGELOG.md +27 -1
  2. package/README.md +18 -16
  3. package/dist/agent-run-lifecycle.d.ts +2 -1
  4. package/dist/agent-run-lifecycle.js +1 -1
  5. package/dist/agent-session/session/assemble.js +9 -7
  6. package/dist/agent-session/session/tool-round.js +30 -20
  7. package/dist/agent-session/session/types.d.ts +1 -0
  8. package/dist/agent-session/session.d.ts +1 -0
  9. package/dist/agent-session/session.js +3 -2
  10. package/dist/checkpoint-restore.d.ts +50 -14
  11. package/dist/checkpoint-restore.js +104 -28
  12. package/dist/contracts-core/session.d.ts +2 -1
  13. package/dist/contracts-run-state.d.ts +12 -4
  14. package/dist/index.d.ts +3 -3
  15. package/dist/index.js +1 -1
  16. package/dist/leases.js +32 -6
  17. package/dist/node/contribution-discovery.d.ts +16 -1
  18. package/dist/node/contribution-discovery.js +47 -0
  19. package/dist/node/session-store-jsonl.js +67 -17
  20. package/dist/run-limits.d.ts +11 -5
  21. package/dist/session-stores.js +61 -12
  22. package/dist/testing/prefix-stability-conformance.d.ts +44 -1
  23. package/dist/testing/prefix-stability-conformance.js +92 -29
  24. package/dist/usage-estimation.d.ts +7 -1
  25. package/dist/usage-estimation.js +16 -10
  26. package/docs/acp.md +2 -2
  27. package/docs/agent-events.md +7 -6
  28. package/docs/agent-session-runtime.md +1 -1
  29. package/docs/coding-agent-tools.md +1 -1
  30. package/docs/coding-tools.md +7 -11
  31. package/docs/context-and-skills.md +6 -7
  32. package/docs/contribution-discovery.md +13 -0
  33. package/docs/durable-runs.md +10 -3
  34. package/docs/embeddings.md +3 -1
  35. package/docs/execution-timeline.md +6 -0
  36. package/docs/extensions.md +1 -2
  37. package/docs/impeccable.md +1 -2
  38. package/docs/index.md +24 -20
  39. package/docs/live-testing.md +1 -2
  40. package/docs/memory-fabric.md +3 -2
  41. package/docs/migrate-to-0.11.md +65 -0
  42. package/docs/migration.md +12 -1
  43. package/docs/node-jsonl-session-store.md +4 -3
  44. package/docs/operations.md +1 -1
  45. package/docs/peer-dependencies.md +3 -5
  46. package/docs/policy-and-audit.md +1 -1
  47. package/docs/prefix-stability-conformance.md +30 -7
  48. package/docs/provider-packages.md +20 -20
  49. package/docs/public-contracts.md +1 -1
  50. package/docs/rag.md +2 -2
  51. package/docs/release-and-install.md +57 -57
  52. package/docs/runs-and-usage.md +6 -4
  53. package/docs/session-stores.md +2 -2
  54. package/docs/supervisors.md +14 -6
  55. package/docs/testing.md +17 -9
  56. package/docs/workflows.md +2 -2
  57. package/package.json +5 -4
  58. package/docs/caveman.md +0 -130
  59. package/docs/graft.md +0 -149
  60. package/docs/ponytail.md +0 -129
@@ -8,6 +8,43 @@ import { createAgent } from "../agent-session/create-agent.js";
8
8
  import { providerDone, providerThinkingDelta, toolCallContent } from "../provider-events.js";
9
9
  import { createLoadSkillTool } from "../skill-load.js";
10
10
  import { createSkillRegistry } from "../skills.js";
11
+ /**
12
+ * Score an already-captured request list. One pass, no session, no provider call.
13
+ * A wrong `tailSegments` map yields a wrong number, not a throw.
14
+ */
15
+ export function scorePrefixStability(requests, options) {
16
+ const first = requests[0];
17
+ if (requests.length < 2 || first === undefined) {
18
+ throw new Error(`scorePrefixStability needs at least 2 captured requests, got ${requests.length}`);
19
+ }
20
+ const minContinuity = options?.minContinuity ?? 0.95;
21
+ const assertOn = options?.assertOn ?? "providerPrefix";
22
+ const isTail = tailClassifier(options?.tailSegments ?? new Map());
23
+ let observed = 1;
24
+ let cacheableObserved = 1;
25
+ const resetDetails = [];
26
+ let previous = measureRequest(first, isTail);
27
+ for (let index = 1; index < requests.length; index += 1) {
28
+ const current = requests[index];
29
+ if (current === undefined)
30
+ continue;
31
+ const next = measureRequest(current, isTail);
32
+ const fraction = sharedPrefixFraction(previous.providerPrefix, next.providerPrefix);
33
+ const cacheableFraction = sharedPrefixFraction(previous.cacheablePrefix, next.cacheablePrefix);
34
+ observed = Math.min(observed, fraction);
35
+ cacheableObserved = Math.min(cacheableObserved, cacheableFraction);
36
+ const measured = assertOn === "cacheablePrefix" ? cacheableFraction : fraction;
37
+ if (measured < minContinuity)
38
+ resetDetails.push(projectResetDetail(index + 1, fraction, cacheableFraction, assertOn));
39
+ previous = next;
40
+ }
41
+ return {
42
+ minContinuity: observed,
43
+ cacheableContinuity: cacheableObserved,
44
+ resets: resetDetails.map((gap) => gap.request),
45
+ resetDetails,
46
+ };
47
+ }
11
48
  /**
12
49
  * Drive a real session through two staggered skill loads and assert that each
13
50
  * provider request keeps a byte-identical leading prefix (messages **and** tool
@@ -32,13 +69,19 @@ export async function runPrefixStabilityConformance(options) {
32
69
  bodies.push(instructions);
33
70
  }
34
71
  const requests = [];
72
+ const foldableBytes = options.foldableToolResultBytes;
73
+ assert.ok(foldableBytes === undefined || (Number.isSafeInteger(foldableBytes) && foldableBytes > 0), "prefix stability conformance foldableToolResultBytes must be a positive safe integer");
35
74
  const registry = createSkillRegistry([...skills]);
36
75
  const hostTools = host.tools && "list" in host.tools ? host.tools.list() : (host.tools ?? []);
37
76
  const agent = createAgent({
38
77
  ...host,
39
78
  skills: registry,
40
- tools: [...hostTools, createLoadSkillTool({ registry })],
41
- provider: fixtureProvider(requests, [first.name, second.name], host.attentionCompiler === true || typeof host.attentionCompiler === "object"),
79
+ tools: [
80
+ ...hostTools,
81
+ createLoadSkillTool({ registry }),
82
+ ...(foldableBytes === undefined ? [] : [createFoldableToolResultTool(foldableBytes)]),
83
+ ],
84
+ provider: fixtureProvider(requests, [first.name, second.name], host.attentionCompiler === true || typeof host.attentionCompiler === "object", foldableBytes !== undefined),
42
85
  });
43
86
  const session = agent.createSession();
44
87
  const [firstInput, secondInput] = options.inputs ?? ["Prefix stability turn one", "Prefix stability turn two"];
@@ -49,40 +92,26 @@ export async function runPrefixStabilityConformance(options) {
49
92
  // The session's own map holds the exact `Message` objects `appendTailSegment` allocated, so the
50
93
  // classification is exact rather than a heuristic over host-authored content. (`tailSegments` is
51
94
  // runtime-session state, not part of the public `AgentSession` contract, hence the narrow above.)
52
- const isTail = tailClassifier(session.tailSegments);
53
- const captured = requests.map((request) => measureRequest(request, isTail));
95
+ const assertOn = options.assertOn ?? "providerPrefix";
96
+ // One measurement. The assertion below reads the sample; it does not serialize again.
97
+ const sample = scorePrefixStability(requests, { tailSegments: session.tailSegments, minContinuity, assertOn });
54
98
  // Guard against a vacuous pass: both bodies must have been disclosed by the end.
55
- const last = captured.at(-1)?.providerPrefix ?? "";
99
+ const last = (requests.at(-1)?.messages ?? []).map((message) => JSON.stringify(message)).join("\n");
56
100
  for (const [index, skill] of skills.entries()) {
57
101
  assert.ok(last.includes(bodies[index] ?? ""), `prefix stability conformance: skill ${skill.name} body never reached the provider request — progressive disclosure did not expand it`);
58
102
  }
59
- const assertOn = options.assertOn ?? "providerPrefix";
60
103
  const allowedResets = options.allowedResets ?? 0;
61
104
  assert.ok(Number.isSafeInteger(allowedResets) && allowedResets >= 0, "prefix stability conformance allowedResets must be a non-negative safe integer");
62
- let observed = 1;
63
- let cacheableObserved = 1;
64
- let previous = captured.at(0) ?? { providerPrefix: "", cacheablePrefix: "" };
65
- // Collect every gap first: an allowed reset must not be hidden by a later assert, and the
66
- // vacuity check needs the whole list to prove the fixture folded exactly as declared.
67
- const gaps = [];
68
- for (let index = 1; index < captured.length; index += 1) {
69
- const next = captured[index] ?? previous;
70
- const fraction = sharedPrefixFraction(previous.providerPrefix, next.providerPrefix);
71
- const cacheableFraction = sharedPrefixFraction(previous.cacheablePrefix, next.cacheablePrefix);
72
- observed = Math.min(observed, fraction);
73
- cacheableObserved = Math.min(cacheableObserved, cacheableFraction);
74
- const measured = assertOn === "cacheablePrefix" ? cacheableFraction : fraction;
75
- if (measured < minContinuity)
76
- gaps.push({ request: index + 1, fraction, cacheableFraction });
77
- previous = next;
78
- }
79
- const resets = gaps.map((gap) => gap.request);
105
+ const observed = sample.minContinuity;
106
+ const cacheableObserved = sample.cacheableContinuity;
107
+ const gaps = sample.resetDetails;
108
+ const resets = sample.resets;
80
109
  const measuredLabel = assertOn === "cacheablePrefix" ? "previous cacheable prefix (tail segments excluded)" : "previous provider prefix";
81
110
  const minimum = (minContinuity * 100).toFixed(1);
82
- const observedResets = `resets ${formatResets(resets)} of ${captured.length - 1} request pairs`;
111
+ const observedResets = `resets ${formatResets(resets)} of ${requests.length - 1} request pairs`;
83
112
  const firstGap = gaps[0];
84
113
  if (firstGap !== undefined && gaps.length > allowedResets) {
85
- const measured = assertOn === "cacheablePrefix" ? firstGap.cacheableFraction : firstGap.fraction;
114
+ const measured = firstGap.fraction;
86
115
  assert.fail(`prefix stability conformance: request ${firstGap.request - 1} → ${firstGap.request} kept ${(measured * 100).toFixed(1)}% of the ${measuredLabel} ` +
87
116
  `(minimum ${minimum}%), and ${gaps.length} pair(s) broke below it (${observedResets}, allowedResets ${allowedResets}). ` +
88
117
  "Late skill bodies must append after the stable prefix; recomposed context, an in-place skill-catalog rewrite, or any leading-message mutation invalidates it. " +
@@ -92,7 +121,21 @@ export async function runPrefixStabilityConformance(options) {
92
121
  assert.fail(`prefix stability conformance: allowedResets is ${allowedResets} but only ${gaps.length} pair(s) broke below the minimum (${minimum}% of the ${measuredLabel}); ${observedResets}. ` +
93
122
  "The fixture was supposed to invalidate the prefix at those boundaries — drop allowedResets for an append-only assembly, or check the fold trigger or eviction condition actually fired.");
94
123
  }
95
- return { requests: captured.length, minContinuity: observed, cacheableContinuity: cacheableObserved, resets };
124
+ return {
125
+ requests: requests.length,
126
+ minContinuity: observed,
127
+ cacheableContinuity: cacheableObserved,
128
+ resets,
129
+ resetDetails: sample.resetDetails,
130
+ };
131
+ }
132
+ /** One gap row. `fraction` is the metric the caller selected; `cacheableFraction` stays the tail-excluded pair. */
133
+ function projectResetDetail(request, providerFraction, cacheableFraction, assertOn) {
134
+ return {
135
+ request,
136
+ fraction: assertOn === "cacheablePrefix" ? cacheableFraction : providerFraction,
137
+ cacheableFraction,
138
+ };
96
139
  }
97
140
  /**
98
141
  * Deterministic reasoning block the fixture provider emits before each skill load when the host
@@ -101,12 +144,26 @@ export async function runPrefixStabilityConformance(options) {
101
144
  * in the measured prefix.
102
145
  */
103
146
  const FIXTURE_THINKING = "Prefix-stability fixture reasoning: the harness measures a byte-shared provider prefix, so this block exists only to give the attention-compiler thinking stage deterministic content to strip. ".repeat(17);
147
+ /** Runner-owned fixture tool (plan 110 Task 4): the tool-result stage needs a row worth stubbing. */
148
+ const PREFIX_STABILITY_BULK_TOOL_NAME = "prefix_stability_bulk";
149
+ function createFoldableToolResultTool(bytes) {
150
+ return {
151
+ name: PREFIX_STABILITY_BULK_TOOL_NAME,
152
+ description: "Deterministic bulk payload for the tool-result fold fixture.",
153
+ parameters: { type: "object", properties: {} },
154
+ execute(_args, context) {
155
+ const text = "x".repeat(bytes);
156
+ return { toolCallId: context.toolCallId, name: PREFIX_STABILITY_BULK_TOOL_NAME, value: text, content: [{ type: "text", text }] };
157
+ },
158
+ };
159
+ }
104
160
  /**
105
161
  * Fixture provider: turn 1 loads `skillNames[0]`, turn 2 loads `skillNames[1]`, everything else
106
162
  * completes. With `reasoning` (the host runs an attention compiler) each skill-load round also
107
- * carries a thinking block, so the compiler's thinking stage has real content to fold.
163
+ * carries a thinking block, so the compiler's thinking stage has real content to fold. With `bulk`
164
+ * the same round also calls the runner-owned bulk tool, so the tool-result stage has a foldable row.
108
165
  */
109
- function fixtureProvider(requests, skillNames, reasoning) {
166
+ function fixtureProvider(requests, skillNames, reasoning, bulk) {
110
167
  let call = 0;
111
168
  return {
112
169
  id: "prefix-stability-fixture",
@@ -119,6 +176,12 @@ function fixtureProvider(requests, skillNames, reasoning) {
119
176
  if (reasoning)
120
177
  yield providerThinkingDelta(FIXTURE_THINKING);
121
178
  yield { type: "tool_call", call: toolCallContent(`prefix-stability-${index}`, "load_skill", { name: skillName }) };
179
+ if (bulk) {
180
+ yield {
181
+ type: "tool_call",
182
+ call: toolCallContent(`prefix-stability-bulk-${index}`, PREFIX_STABILITY_BULK_TOOL_NAME, {}),
183
+ };
184
+ }
122
185
  return;
123
186
  }
124
187
  yield providerDone();
@@ -19,7 +19,13 @@ export interface ModelFamilyTokens {
19
19
  readonly perMessageOverhead: number;
20
20
  readonly confidence: TokenEstimateConfidence;
21
21
  }
22
- /** Per-family chars/token tables (plan 091 Task 1). Estimates only, never billing. */
22
+ /** Per-family chars/token tables (plan 091 Task 1). Estimates only, never billing.
23
+ *
24
+ * Deep-frozen — each row, then the table. `Readonly<Record<…>>` is compile-time only, and a
25
+ * runtime write to a nested row silently changes token accounting: an under-counted input
26
+ * estimate is what `maxInputTokens`/`maxCost` are checked against, and a replaced row makes the
27
+ * estimate `NaN`, which compares false. Recalibration is a source change plus the live calibration
28
+ * leg (`scripts/usage-calibration-live.test.mjs`), never a runtime override. */
23
29
  export declare const MODEL_FAMILY_TOKENS: Readonly<Record<ModelFamily, ModelFamilyTokens>>;
24
30
  /** Resolve a model id (e.g. `"claude-sonnet-4.5"`), provider id, or family name
25
31
  * to a table key. Unmatched input is `"unknown"` — never a throw. */
@@ -2,16 +2,22 @@
2
2
  const CJK_CHARS_PER_TOKEN = 1.5;
3
3
  /** Fenced code tokenizes worse than prose: code ratio = prose ratio * this factor. */
4
4
  const CODE_RATIO_FACTOR = 0.88;
5
- /** Per-family chars/token tables (plan 091 Task 1). Estimates only, never billing. */
6
- export const MODEL_FAMILY_TOKENS = {
7
- anthropic: { charsPerToken: 3.7, perMessageOverhead: 4, confidence: "medium" },
8
- openai: { charsPerToken: 5.0, perMessageOverhead: 3, confidence: "medium" },
9
- google: { charsPerToken: 3.9, perMessageOverhead: 4, confidence: "medium" },
10
- deepseek: { charsPerToken: 3.8, perMessageOverhead: 4, confidence: "medium" },
11
- "openrouter-generic": { charsPerToken: 4.4, perMessageOverhead: 4, confidence: "medium" },
12
- mistral: { charsPerToken: 3.9, perMessageOverhead: 3, confidence: "medium" },
13
- unknown: { charsPerToken: 3.5, perMessageOverhead: 6, confidence: "low" },
14
- };
5
+ /** Per-family chars/token tables (plan 091 Task 1). Estimates only, never billing.
6
+ *
7
+ * Deep-frozen — each row, then the table. `Readonly<Record<…>>` is compile-time only, and a
8
+ * runtime write to a nested row silently changes token accounting: an under-counted input
9
+ * estimate is what `maxInputTokens`/`maxCost` are checked against, and a replaced row makes the
10
+ * estimate `NaN`, which compares false. Recalibration is a source change plus the live calibration
11
+ * leg (`scripts/usage-calibration-live.test.mjs`), never a runtime override. */
12
+ export const MODEL_FAMILY_TOKENS = Object.freeze({
13
+ anthropic: Object.freeze({ charsPerToken: 3.7, perMessageOverhead: 4, confidence: "medium" }),
14
+ openai: Object.freeze({ charsPerToken: 5.0, perMessageOverhead: 3, confidence: "medium" }),
15
+ google: Object.freeze({ charsPerToken: 3.9, perMessageOverhead: 4, confidence: "medium" }),
16
+ deepseek: Object.freeze({ charsPerToken: 3.8, perMessageOverhead: 4, confidence: "medium" }),
17
+ "openrouter-generic": Object.freeze({ charsPerToken: 4.4, perMessageOverhead: 4, confidence: "medium" }),
18
+ mistral: Object.freeze({ charsPerToken: 3.9, perMessageOverhead: 3, confidence: "medium" }),
19
+ unknown: Object.freeze({ charsPerToken: 3.5, perMessageOverhead: 6, confidence: "low" }),
20
+ });
15
21
  /** Model-id patterns per family. Family names themselves also resolve (see `resolveModelFamily`). */
16
22
  const FAMILY_PATTERNS = [
17
23
  ["anthropic", /claude|anthropic/i],
package/docs/acp.md CHANGED
@@ -57,7 +57,7 @@ In-stream `SessionUpdate`s:
57
57
  | `permission_denied` lifecycle | `tool_call_update` status `failed` (never raw args; synthesized id `prism:denied:<approvalId>` when no `toolCallId`) |
58
58
  | `configuration_changed` lifecycle | `config_option_update` with the full current set, per streaming session |
59
59
  | Plan lifecycle (F5, UNSTABLE-gated) | `plan_changed` → `plan_update` with `plan: { type: "items", planId = planPath, entries: [{ content, priority: "medium", status }] }` — the complete entry list per update (client replaces its plan wholesale); `plan_removed` → `plan_removed` with `planId = planPath`. Emitted only when the client advertised `ClientCapabilities.plan`; mapper stays capability-agnostic (gate in the agent wiring). Entries come from `writeCodingPlanFile`'s `onEvent` (parsed via `parseCodingPlanTodos`) or host-emitted through their `CodingLifecycleEmitter`; text passes the shared redactor and byte caps. |
60
- | Subagent lifecycle | `subagent_started` / `subagent_stopped` → `agent_message_chunk` with only redacted child/delegation ids and terminal status. Wire `observeSupervisorLifecycle()` to emit them; child inputs, outputs, paths, and error text stay absent. |
60
+ | Subagent lifecycle | `subagent_started` / `subagent_stopped` → `agent_message_chunk` with only redacted child/delegation ids and terminal status. Wire `observeSupervisorLifecycle()` to emit them; child inputs, outputs, paths, and error text stay absent. The coding event's opt-in `failure` / `recovery` fields are host-side only — this mapping reads ids, depth, and status. |
61
61
  | Session title (F6) | `sessions.title({ sessionId, prompt, signal })` resolves on `session/prompt`; a defined value differing from the last emitted title produces `session_info_update` with `{ sessionUpdate: "session_info_update", title }`. Best-effort: `undefined` or a throw means no title and no update (requests never fail on titles); the host owns title storage. Titles pass the shared redactor and are truncated at `maxTextBytes`/`maxEventBytes`. |
62
62
  | Slash commands (F9) | `commands.list({ sessionId, signal })` on `session/new`/`load`/`resume` produces `available_commands_update` with `{ name, description, input?: { hint } }` (SDK `AvailableCommand`; description is required). Names/descriptions/hints pass the shared redactor and `maxTextBytes`; the list is sliced at `acpCommandsPerUpdate`. Best-effort: a throw or non-array omits the update (session start never fails on commands). |
63
63
  | Session mode/config switch | `current_mode_update` / `config_option_update` |
@@ -121,7 +121,7 @@ const agent = createPrismAcpAgent({
121
121
 
122
122
  ### Persistence and ownership
123
123
 
124
- - **Active-run recovery (0.2.6, plan 026 Task 5).** When `sessionStore` and the `recovery` seam (checkpoints + leases + ownerId, all three together) are wired, the agent records a bounded `activeRun` reference on `PersistedAcpSession` while a durable run is live (first run event → `running`, suspension → `suspended` + version, finish/deny/error → `terminal`; frozen 512-byte cap; advisory only — the authoritative status is re-queried from `AgentRunLifecycle.status`). After a restart, `restore` re-attaches the ref to the live session and hosts re-resolve it with `createAcpRunRecovery` (exported from `@arnilo/prism-ag-ui/acp`): suspended runs report their pending approval ids and durable version, terminal runs report terminal, and unprovable in-flight streams report `unknown` — the prompt is never restarted automatically. Durable cancellation (`recovery.cancel`) is ownership/version/fence checked, terminal/idempotent, aborts no unrelated run, and never replays a pending/dispatched tool: a cancelled run reports `cancelled` and must not be resumed. `session/cancel` on a live agent aborts the controller (0.2.5 parity) and, for restored runs, writes the durable marker under the session's ownership. Cancel markers live in `prism.coding-agent.cancel.v1` (schemaVersion 1, CAS + lease fenced). A host-side terminal whose managed process is unattestable after restart reports `unknown` (exitCode null); the agent never fabricates an exit or replays input (`terminal-client`).
124
+ - **Active-run recovery (0.2.6, plan 026 Task 5).** When `sessionStore` and the `recovery` seam (checkpoints + leases + ownerId, all three together) are wired, the agent records a bounded `activeRun` reference on `PersistedAcpSession` while a durable run is live (first run event → `running`, suspension → `suspended` + version, finish/deny/error → `terminal`; frozen 512-byte cap; advisory only — the authoritative status is re-queried from `AgentRunLifecycle.status`). After a restart, `restore` re-attaches the ref to the live session and hosts re-resolve it with `createAcpRunRecovery` (exported from `@arnilo/prism-ag-ui/acp`): suspended runs report their pending approval ids and durable version, terminal runs report terminal, and unprovable in-flight streams report `unknown` — the prompt is never restarted automatically. Durable cancellation (`recovery.cancel`) is ownership/version/fence checked, terminal/idempotent, aborts no unrelated run, and never replays a pending/dispatched tool: a cancelled run reports `cancelled` and must not be resumed. `session/cancel` on a live agent aborts the controller (0.2.5 parity) and, for restored runs, writes the durable marker under the session's ownership; that write is not tied to the connection, so it lands even when the client disconnects immediately after `session/cancel`; `session/close` likewise does not abort a durable cancel write already in flight — it stays bounded by the cancel-lease TTL. Cancel markers live in `prism.coding-agent.cancel.v1` (schemaVersion 1, CAS + lease fenced). A host-side terminal whose managed process is unattestable after restart reports `unknown` (exitCode null); the agent never fabricates an exit or replays input (`terminal-client`).
125
125
 
126
126
  - **Without the durability seam the agent never persists `modeId`/`configValues`.** Defaults are recomputed per session from the `modes`/`configOptions` seams — a fresh `session/new`, `load`, or `resume` always starts from `defaultModeId` / option `defaultValue`, and the agent's per-session registry is in-memory only. Persisting mode/config across sessions is a **host** decision, and host-side persistence MUST be ownership-scoped.
127
127
  - **Host persistence MUST key by `sessions.ownership`.** `authorize` binds transport identity to ownership; a host store that persists `modeId`/`configValues` must refuse any restore whose stored ownership differs from the current session's ownership — a `sessionId` alone is never a sufficient key (session ids may collide across tenants). A cross-tenant restore rejects with `ERR_PRISM_ACP_INPUT` and never returns the other tenant's mode/config.
@@ -110,9 +110,9 @@ Agent / turn / message events:
110
110
 
111
111
  Adapters should call `createDelegatedAgentStep({ sessionId, runId, adapterId, externalConversationId, stepIndex, state, kind, usage })` rather than forwarding external JSON. The constructor allow-lists fields and fails closed on malformed or oversized identifiers/counters.
112
112
 
113
- Coding hosts call `observeSupervisorLifecycle(supervisor, { onEvent, delegatedAgentStep })` to turn supervisor milestones into `subagent_started` / `subagent_stopped` coding lifecycle events. Both carry only redacted `childId`, `delegationId`, and `depth`; stopped events add terminal `AgentRunStatus`. Supplying `delegatedAgentStep` emits the bounded `delegated_agent_step` records AG-UI already maps. Child inputs, outputs, paths, and delegation error text never cross either bridge.
113
+ Coding hosts call `observeSupervisorLifecycle(supervisor, { onEvent, delegatedAgentStep })` to turn supervisor milestones into `subagent_started` / `subagent_stopped` coding lifecycle events. Both carry only redacted `childId`, `delegationId`, and `depth`; stopped events add terminal `AgentRunStatus`. Two independent opt-ins extend the stopped event without changing the default: `includeFailure: true` attaches `failure: { reason, limit?, stopReason? }` to the stop a `child_failed` produced — the supervisor-redacted reason truncated to `DEFAULT_LIFECYCLE_MAX_REASON_BYTES`, the fired `RunLimitName`, and the child's stop reason (`delegation_finished` and `delegation_rejected` never carry it) — and `includeRecovery: true` attaches the child's `summary()` row as `recovery: { attempts, retries, failures, failureRadius, outcome }`, omitted when the source exposes no `summary()`. Supplying `delegatedAgentStep` emits the bounded `delegated_agent_step` records AG-UI already maps. Child inputs, outputs, paths, and delegation error text never cross either bridge: with both options off the stopped event is byte-identical to before, and the opt-in failure fields are counts, enums, and the already-redacted reason.
114
114
 
115
- Supervisor child reporting is opt-in per child (`SupervisorChild.policy.report` ceiling; a request can only lower it). With `report: "milestones"` the supervisor publishes `child_milestone` (`childId`, `delegationId`, `depth`, `turn`, redacted `childEvent`) at the configured `milestone.everyTurns` cadence or host predicate; with `report: "stream"` it publishes `delegation_child_event` for every per-turn provider/tool/turn child event (never per-token `message_delta`). Both are redacted, count/byte-capped, and rate-coalesced (`delegation_child_events_coalesced` reports dropped events); the cap marker is `delegation_child_events_capped`. `child_failed` carries failure attribution for any child that died on an error or a limit: the redacted `reason`, the terminal `status`/`stopReason`, and the plan-086/087 `RunLimitBreach` in `limit` when a configured ceiling fired. Host cancels, policy denials, and hook rejections are not failures and never emit it. Hosts that want recovery counters rather than events read `supervisor.summary()` (`attempts`, `retries`, `failures`, `failureRadius`, `outcome` per child). Child events stay on the supervisor stream unless the host passes `childEventSink`, which receives the identical payload tagged with `child: { childId, delegationId, depth }` (`ChildEventOrigin`) for routing onto a parent session stream; they are not native `AgentEvent`s of the parent session, and hosts that surface them there re-attach the parent `sessionId`/`runId` themselves if needed.
115
+ Supervisor child reporting is opt-in per child (`SupervisorChild.policy.report` ceiling; a request can only lower it). With `report: "milestones"` the supervisor publishes `child_milestone` (`childId`, `delegationId`, `depth`, `turn`, redacted `childEvent`) at the configured `milestone.everyTurns` cadence or host predicate; with `report: "stream"` it publishes `delegation_child_event` for every per-turn provider/tool/turn child event (never per-token `message_delta`). Both are redacted, count/byte-capped, and rate-coalesced (`delegation_child_events_coalesced` reports dropped events); the cap marker is `delegation_child_events_capped`. `child_failed` carries failure attribution for any child that died on an error or a limit: the redacted `reason`, the terminal `status`/`stopReason`, and the plan-086/087 `RunLimitBreach` in `limit` when a configured ceiling fired, plus that death's exhaustion attribution (`consumed`, `closestOtherAxes`, `recentToolCalls`, the payload the child's own `budget_exhausted` event and `AgentRunResult.attribution` carry). Host cancels, policy denials, and hook rejections are not failures and never emit it. Hosts that want recovery counters rather than events read `supervisor.summary()` (`attempts`, `retries`, `failures`, `failureRadius`, `outcome` per child), or pass `includeRecovery` to the lifecycle bridge, which attaches that row to each stopped event. Child events stay on the supervisor stream unless the host passes `childEventSink`, which receives the identical payload tagged with `child: { childId, delegationId, depth }` (`ChildEventOrigin`) for routing onto a parent session stream; they are not native `AgentEvent`s of the parent session, and hosts that surface them there re-attach the parent `sessionId`/`runId` themselves if needed.
116
116
 
117
117
  `message_delta.content.type === "tool_call_delta"` carries `{ index, id?, name?, argumentsText? }`. Treat it as a streaming fragment. The runtime reconstructs and persists a final `tool_call` before executing tools. Deltas missing `id`/`name` at stream end fail the provider turn with `ErrorInfo.code: "incomplete_delta"` (typed `ProviderTransportError`); they never throw a bare `Error`. Malformed JSON with id+name present recovers as a blocked tool result (`invalid_json_arguments`) instead.
118
118
 
@@ -198,10 +198,11 @@ value when the adapter saw a native reason.
198
198
 
199
199
  `provider_turn_finished.metadata.budgets` is an O(1) snapshot from the run limit tracker:
200
200
  `{ inputTokens?, inputTokensSource?, inputCap?, runInputBudget?, runInputUsed, turns, maxTurns }` —
201
- current-turn charged input tokens (provider-reported, or the labeled fallback estimate when the
202
- provider reported none) against the resolved per-request input cap, cumulative run input against
203
- `limits.maxInputTokens`, and provider turns against `limits.maxTurns` (`null` when disabled).
204
- `inputTokensSource` is `"reported"` or `"estimated"` and is absent together with `inputTokens`.
201
+ current-turn charged input tokens against the resolved per-request input cap, cumulative run input
202
+ against `limits.maxInputTokens`, and provider turns against `limits.maxTurns` (`null` when disabled).
203
+ `inputTokensSource` is `"reported"` or `"estimated"` and is absent together with `inputTokens`; what
204
+ produces each is documented in
205
+ [Runs and usage § Automatic fallback](runs-and-usage.md#automatic-fallback-agentconfigusageestimation).
205
206
  Optional fields are absent when the provider reported no usage or no input cap can be derived; hosts
206
207
  that ignore the fields are unaffected.
207
208
 
@@ -61,7 +61,7 @@ string | Message | readonly Message[]
61
61
 
62
62
  `session.close()` is the session teardown seam: it dispatches `session_shutdown` middleware exactly once (idempotent — a second `close()` dispatches nothing) and then closes every subscriber, run-scoped and `acrossRuns` alike. It does not abort an active run, so call it after the run settles. `session_start` middleware, the mirror dispatch, runs once at the session's first run start (the two hooks are the only per-session middleware calls — every other hook is per turn or per boundary). See [Middleware hooks](middleware-hooks.md).
63
63
 
64
- `session.run()` / `session.prompt()` resolve to an `AgentRunResult` with `sessionId`, `runId`, `status`, `text`, `content`, optional `message`/`usage`/`leafId`, and terminal `error`/`abortReason` when applicable. Callers may ignore the return value. Failed and aborted runs still emit their terminal events, then reject with `AgentRunError` whose `.result` carries the same shape.
64
+ `session.run()` / `session.prompt()` resolve to an `AgentRunResult` with `sessionId`, `runId`, `status`, `text`, `content`, optional `message`/`usage`/`leafId`, `limit`/`attribution` when the run died on a configured ceiling, and terminal `error`/`abortReason` when applicable. Callers may ignore the return value. Failed and aborted runs still emit their terminal events, then reject with `AgentRunError` whose `.result` carries the same shape.
65
65
 
66
66
  `session.stream(input, options?)` subscribes first, starts exactly one run, yields only that run's events, and terminates when the run succeeds, fails, or aborts. The subscription belongs to `stream()`: it closes it when the owned run settles, so even a run that fails before its first event (a pre-flight validation rejection returns before run-end cleanup) ends the consumer instead of parking it, and no run-end close is required for `stream()` to be correct. Early consumer return aborts the owned run and releases the session. `SubscribeOptions.maxQueuedEvents` / `overflow` may be passed alongside `RunOptions`.
67
67
 
@@ -614,5 +614,5 @@ Every configurable value is a positive safe integer (context may be zero); Prism
614
614
  - [Public contracts](public-contracts.md): `ToolDefinition`, `ToolResult`, `ToolExecutionContext`, `ContentBlock`, and `JsonObject` shapes.
615
615
  - [Host security guide](host-security.md): fail-closed checklist for permission policies, tool validation, and trust boundaries that must gate these tools.
616
616
  - [Tool conformance](tool-conformance.md): assertions for the tool-dispatch blocked-reason matrix these tools participate in.
617
- - [ACP coding-host interop](acp.md): host editors drive these tools through stable ACP v1 — client fs/terminal adapters, `CodingLifecycleEvent` emission (`file_changed` etc. via the `onEvent` options; `plan_changed` also fires from `writeCodingPlanFile`'s `onEvent`, F5), redacted supervisor `subagent_started` / `subagent_stopped` via `observeSupervisorLifecycle`, and permission/elicitation through the shared four-outcome decision model.
617
+ - [ACP coding-host interop](acp.md): host editors drive these tools through stable ACP v1 — client fs/terminal adapters, `CodingLifecycleEvent` emission (`file_changed` etc. via the `onEvent` options; `plan_changed` also fires from `writeCodingPlanFile`'s `onEvent`, F5), redacted supervisor `subagent_started` / `subagent_stopped` via `observeSupervisorLifecycle` (the opt-in `includeFailure` / `includeRecovery` options add the redacted reason/limit/stop reason and the child's `summary()` counters to stopped events), and permission/elicitation through the shared four-outcome decision model.
618
618
  - [LLM compaction package](compaction-llm.md): optional `createCodingCompactionStrategy()` retains bounded paths, patch intent, checks, plan/todo state, blockers, and next verification—not complete diffs or raw command output.
@@ -8,14 +8,11 @@ The `@arnilo/prism-coding-tools` family package unifies Prism's coding agent too
8
8
  npm install @arnilo/prism @arnilo/prism-coding-tools
9
9
  ```
10
10
 
11
- For document reading or specialized persona integrations, install the optional peer dependencies as needed:
11
+ For document reading or specialized integrations, install the optional peer dependencies as needed:
12
12
 
13
13
  ```bash
14
14
  # PDF and DOCX document extraction
15
15
  npm install pdf-parse mammoth
16
-
17
- # Ponytail upstream integration
18
- npm install @dietrichgebert/ponytail
19
16
  ```
20
17
 
21
18
  ## Subpaths Map
@@ -28,8 +25,6 @@ npm install @dietrichgebert/ponytail
28
25
  | `@arnilo/prism-coding-tools/computer-use-linux` | Linux desktop observation and targeting tool bridge | — |
29
26
  | `@arnilo/prism-coding-tools/dev` | Loopback-only developer inspector, event timeline visualizer, and local replay server | — |
30
27
  | `@arnilo/prism-coding-tools/dev/cli` | Command-line entrypoint for `prism dev` | — |
31
- | `@arnilo/prism-coding-tools/caveman` | Caveman ultra-terse engineering persona extension | — |
32
- | `@arnilo/prism-coding-tools/ponytail` | Ponytail multi-agent planning and delegation persona extension | `@dietrichgebert/ponytail` |
33
28
  | `@arnilo/prism-coding-tools/impeccable` | Impeccable high-precision frontend engineering persona extension | — |
34
29
 
35
30
  ## CLI
@@ -65,17 +60,18 @@ const composition = createSandboxCodingComposition({
65
60
 
66
61
  ### Persona Extensions
67
62
  ```ts
68
- import { createCavemanExtension } from "@arnilo/prism-coding-tools/caveman";
69
- import { createPonytailExtension } from "@arnilo/prism-coding-tools/ponytail";
70
63
  import { createImpeccableExtension } from "@arnilo/prism-coding-tools/impeccable";
71
64
 
72
- const caveman = createCavemanExtension();
73
- const ponytail = createPonytailExtension();
74
65
  const impeccable = createImpeccableExtension();
75
66
  ```
76
67
 
68
+ Host-owned personas (any upstream `SKILL.md` tree) need no package subpath: load it with
69
+ `loadSkillDirectory` from `@arnilo/prism/node/contribution-discovery`, register the skills and an
70
+ instruction injector from a host extension, and persist the active mode in session entries — see
71
+ [`examples/caveman-ponytail.ts`](../examples/caveman-ponytail.ts).
72
+
77
73
  ## Security & Import Isolation
78
74
 
79
75
  - Importing `@arnilo/prism-coding-tools/agent` never loads Docker sandbox adapters, desktop MCP bridges, document parser peers, or Dev inspector modules.
80
- - Document parser peers (`pdf-parse`, `mammoth`) and Ponytail optional peer fail closed when absent.
76
+ - Document parser peers (`pdf-parse`, `mammoth`) fail closed when absent.
81
77
  - Persona extensions are pure prompt and behavior modifiers and never gain implicit host privileges.
@@ -198,16 +198,15 @@ await agent.createSession().run("…", { activeSkills: ["ponytail"] });
198
198
  // Turn 1: catalog only. After load_skill({ name: "ponytail" }), later turns include instructions.
199
199
  ```
200
200
 
201
- ### Third-party behavior packages (Caveman, Ponytail, Impeccable)
201
+ ### Third-party behavior packages (Impeccable, host-owned personas)
202
202
 
203
- `@arnilo/prism-coding-tools/caveman` and `@arnilo/prism-coding-tools/ponytail` register upstream skills into the extension kernel skill registry. Hosts should:
203
+ `@arnilo/prism-coding-tools/impeccable` registers its upstream skill into the extension kernel skill registry from a host-supplied `upstreamPath`. For any other upstream persona, the host keeps the same shape with public APIs only:
204
204
 
205
- 1. `kernel.load([createCavemanExtension(...), createPonytailExtension(...)])` with session `appendEntry` / `getEntries` callbacks.
206
- 2. Build `createSkillRegistry(kernel.registries.skills.list())` and pass `activeSkills` / `resolveActiveSkills` names.
207
- 3. Keep `skillsDisclosure: "progressive"` and register `createLoadSkillTool` — full `SKILL.md` bodies stay catalog-only until `load_skill`.
208
- 4. Select `instructionInjectors: ["caveman-mode", "ponytail-mode"]` (or subset) for mode/level slices **without** forcing `skillsDisclosure: "eager"`.
205
+ 1. Load the `SKILL.md` tree with `loadSkillDirectory` from `@arnilo/prism/node/contribution-discovery` (bounded reads, symlink-contained), and register each skill from an extension `setup()`; log the skills registry with `createSkillRegistry(kernel.registries.skills.list())` and pass `activeSkills` / `resolveActiveSkills` names.
206
+ 2. Keep `skillsDisclosure: "progressive"` and register `createLoadSkillTool` — full `SKILL.md` bodies stay catalog-only until `load_skill`.
207
+ 3. Register an instruction injector (`api.registerInstructionInjector`) for the mode/level slice and a command that persists the mode as a session `custom` entry, restoring it on the next load — **without** forcing `skillsDisclosure: "eager"`.
209
208
 
210
- Mode slices and skill bodies are independent: the injector can add `PONYTAIL MODE ACTIVE` while `ponytail-audit` remains catalog-only until loaded. See [Caveman](caveman.md), [Ponytail](ponytail.md), [Impeccable](impeccable.md), and `examples/caveman-ponytail.ts`.
209
+ Mode slices and skill bodies are independent: the injector can add `PONYTAIL MODE ACTIVE` while `ponytail-audit` remains catalog-only until loaded. See [Impeccable](impeccable.md), [Contribution discovery](contribution-discovery.md), and `examples/caveman-ponytail.ts`.
211
210
 
212
211
  Pure validation without the tool: `resolveSkillLoad({ registry, name, tools, loaded, activeSkillNames })`.
213
212
 
@@ -64,6 +64,18 @@ The markdown body below the front fence becomes `Skill.instructions`. Unknown fr
64
64
  | `--discover-kinds <csv>` | Kinds to scan. Defaults to `skill`. Accepts `skill,tool,context,instructions`. |
65
65
  | `--no-discovery` | Hard-disable discovery even if `--discover` is set. |
66
66
 
67
+ ### `loadSkillDirectory` (third-party skill trees)
68
+
69
+ `discoverContributions` scans only `<workspaceRoot>/.agents/<kind>s/<name>/`. To load a tree you already own — an upstream persona checkout, a provider's compiled skills, a vendored `SKILL.md` set — use `loadSkillDirectory(directory, options?)` from `@arnilo/prism/node/contribution-discovery`:
70
+
71
+ ```ts
72
+ import { loadSkillDirectory } from "@arnilo/prism/node/contribution-discovery";
73
+
74
+ const skills = await loadSkillDirectory("/opt/upstream/skills", { maxSkillBytes: 64 * 1024 });
75
+ ```
76
+
77
+ It reads `<directory>/<name>/SKILL.md` and returns inert `Skill[]` sorted by directory name. Subdirectories without `SKILL.md`, plain files, and symlinks that escape `directory` are skipped; an unreadable `directory` or a file over `options.maxSkillBytes` (default `HARD_MAX_SKILL_INSTRUCTION_BYTES`, 262 144) throws. No trust policy, no permission callback, and no `import()`: the host naming the directory is the authority, exactly as with the deleted persona subpaths (see `examples/caveman-ponytail.ts`).
78
+
67
79
  ## Outputs / response / events
68
80
 
69
81
  `discoverContributions()` returns `readonly DiscoveredContribution[]`. Each envelope has `kind`, `name`, `origin` (`"global"` | `"workspace"`), `path`, and either `skill` (for the `skill` kind) or `declaration` (a `ManifestContributionDeclaration` for other kinds), plus optional `metadata`. The envelope is inert: it contains no executable code, no credential, and no resolved provider/model/tool reference.
@@ -131,6 +143,7 @@ A complete runnable example lives at `examples/discover-skills.ts`.
131
143
  - **Workspace gating**: workspace roots are checked through `createPathTrustPolicy` + `isPathInsideReal`, which resolve symlinks and fail closed (return false) if either root or target cannot be resolved. Untrusted workspace roots are skipped silently, never thrown over. Permission is asserted per directory read via `assertPermission`.
132
144
  - **Symlink handling**: symlinked entries that escape the kind root after realpath resolution are excluded. `SKILL.md` and `manifest.json` are also realpath-checked against their contribution directory before read, so an entry-file symlink cannot escape to another path.
133
145
  - **Opt-in**: discovery is opt-in — it runs only when the host passes `--discover` or calls `discoverContributions()` explicitly. Default runs perform no filesystem I/O.
146
+ - **Bounded third-party trees**: `loadSkillDirectory` reads one level (`<dir>/<name>/SKILL.md`), realpath-checks each directory and entry file against the supplied root, and fails closed on a missing directory or an over-cap file; the host names the root, so no workspace trust decision is implied.
134
147
  - **No auto-execute**: discovery reads text. It does not `import()`, `require()`, or run contribution code. `registerDiscoveredContributions` registers descriptor stubs whose execution methods throw — the host lifts them into live tools/providers itself.
135
148
  - **No auto-activate**: discovery registers skills; it does not select them. Activation requires explicit `RunOptions.activeSkills`, and `toolNames` is still validated against the resolved tool set. Discovery grants no tools, permissions, or provider slots.
136
149
  - **No provider scanning**: provider/model discovery stays config/package-driven (Phase 24). See [Provider packages](provider-packages.md).
@@ -49,20 +49,26 @@ head = "commit-2"; // the next checkpoint records the new commit
49
49
  ```ts
50
50
  await lifecycle.resume(ref, { decision: "approve", expectedVersion }, {
51
51
  restoreHooks: [
52
+ {
53
+ id: "docs",
54
+ restore: (cp) => docs.restoreVersion(cp.metadata?.docVersion),
55
+ compensate: () => docs.restoreVersion(previousVersion),
56
+ },
52
57
  async function restoreGit(cp) {
53
58
  await git.reset(cp.metadata?.gitCommit);
54
59
  },
55
- async function restoreDocs(cp) {
56
- await docs.restoreVersion(cp.metadata?.docVersion);
57
- },
58
60
  ],
59
61
  restoreHookTimeoutMs: 10_000, // default, per hook
60
62
  });
61
63
  ```
62
64
 
65
+ A bare function is the plan 094 form: the audit names it by `fn.name`, and it has no undo. The object
66
+ form `{ id?, restore, compensate? }` adds a stable audit name and the layer's own undo handler.
67
+
63
68
  All-or-nothing:
64
69
 
65
70
  - The first hook that throws or overruns `restoreHookTimeoutMs` (default 10 s, `DEFAULT_CHECKPOINT_RESTORE_TIMEOUT_MS`) aborts the resume with `CheckpointRestoreError` — `code: "ERR_PRISM_CHECKPOINT_RESTORE"`, `hook` naming the layer, `cause` the original error. Later hooks do not run.
71
+ - A failing restore compensates the applied layers in reverse order, starting with the failing hook itself — a half-applied layer is put back by its own `compensate` — each under the same `restoreHookTimeoutMs`. The error carries `compensation: { ran: [<hook names, most recent first>], failed?: { hook, reason } }`; the first compensation failure is recorded (reason redacted and capped at 1 KiB) and never replaces the original `cause`, and the pass continues with the remaining layers. A caller abort stops the pass and rethrows the abort. Compensation is best-effort: the checkpoint stays unclaimed and resumable either way, so a host fixes the failing layer and retries the whole resume.
66
72
  - The claim write and the conversation replay happen only after every hook succeeds, so a failed restore leaves the checkpoint byte-for-byte as it was — still resumable — instead of claiming a half-restored world. The server maps the failure to `409`/`ERR_PRISM_CHECKPOINT_RESTORE`.
67
73
  - Hooks run on claiming resumes only; `deny` and resuspend paths never call them.
68
74
  - The claim's `agent_resumed` event carries the audit: `restore: { hooks: [{ hook, durationMs }], durationMs }`.
@@ -125,6 +131,7 @@ The complete network-free demo — one tool execution across the crash, resumed
125
131
 
126
132
  - `"continue"` is a host-API action only. Prism's AG-UI interrupt resolution accepts `approve`/`deny` only, channel adapters resume with `deny`, and there is no server route that forwards an untrusted `continue`; adding one would create an approval-bypass path.
127
133
  - Restore hooks are trusted host code running outside the sandbox: they see the checkpoint's (already redacted) sidecar map and are bounded only by their timeout. Because they run before the claim write, a timeout cannot leave a claimed checkpoint pointing at un-restored external state.
134
+ - Compensation reports only hook names and `compensation.failed.reason`: the failing handler's message, redacted by the configured agent/lifecycle redactor and capped at 1 KiB. Hook arguments and the sidecar map are never copied into the report; the original error stays in `cause` and crosses the server boundary as before.
128
135
  - Every gate that protects a suspension protects a continue resume: exact ownership, fencing token, fingerprint, revision, CAS version, and the absence of unresolved work. A running checkpoint is a recovery point, never an authorization.
129
136
  - Cost is one bounded checkpoint write per provider turn (same redaction and `maxStateBytes` ceiling as suspension writes). A 40-turn investigation under `"every-turn"` therefore writes 40 checkpoint rows plus the terminal save, while the default `"decision"` policy writes at most one row per approval or suspension. Each row carries the run frontier, counters, run limits, and loop snapshot — not the message history, which stays in the session store and is pointed at by `leafId` — so the store grows with turns, not with turns × transcript; a state that would exceed `maxStateBytes` (default 256 KiB, `DEFAULT_MAX_AGENT_RUN_STATE_BYTES`) fails closed rather than truncating. Pick `"every-turn"` when a worker restart must cost at most one turn of thinking, and leave the default for runs with many cheap turns.
130
137
  - Checkpoints never contain provider objects, callbacks, signals, credentials, or raw secrets; the payload is bounded and redacted like any other durable state.
@@ -95,7 +95,9 @@ await runEmbeddingsConformance({
95
95
  and the reranker through the same runtime should point them at one weight cache:
96
96
  one `cacheDir` per host (e.g. `~/.cache/prism/models`), one subdirectory per
97
97
  model id, so each model is downloaded once and shared by every process on that
98
- host; a cache miss downloads into that directory and later runs stay on disk.
98
+ host; a cache miss downloads into that directory and later runs stay on disk. The live reranker
99
+ leg runs exactly that pair — a semantic embedder and the cross-encoder into one cache dir — and
100
+ records both subdirectories ([semantic reranker evidence](_evidence/phase111-reranker-semantic-recall.md)).
99
101
  - Adapters never auto-chunk: a batch over the provider cap rejects with
100
102
  `batch_too_large`, so `embedBatched`-style callers own batching and preserve
101
103
  per-item error attribution.
@@ -70,6 +70,8 @@ interface ExecutionTimeline {
70
70
  readonly workflowRevision?: string;
71
71
  /** Workflow checkpoint sidecar metadata (`WorkflowCheckpointValue.metadata`); present only when projected with a checkpoint. */
72
72
  readonly workflowMetadata?: Readonly<Record<string, unknown>>;
73
+ /** Restore-hook audit from the claiming `agent_resumed` / `workflow_resumed` event (plan 094 Task 3); hook names and durations only, absent when the run never resumed or resumed without hooks. */
74
+ readonly restore?: CheckpointRestoreAudit;
73
75
  readonly traceId?: string;
74
76
  readonly status: string;
75
77
  readonly stopReason?: AgentFinishReason;
@@ -88,6 +90,10 @@ interface ExecutionTimeline {
88
90
  }
89
91
  ```
90
92
 
93
+ `restore` is the same `{ hooks: [{ hook, durationMs }], durationMs }` the claiming resume event published, in
94
+ run order — a metadata-only audit (hook names are host-chosen identifiers), so it projects under every
95
+ content policy and never carries prompts, tool arguments, or node payloads.
96
+
91
97
  ### `ExecutionStep`
92
98
 
93
99
  ```ts
@@ -169,9 +169,8 @@ const stop = forwardAgentEvents(session.subscribe(), kernel.events, { onError: (
169
169
  - [Compaction and retry policies](compaction-and-retry.md): compaction strategy/retry policy contributions and `compaction`/`retry` middleware runtime behavior.
170
170
  - [LLM compaction package](compaction-llm.md): optional extension helper that registers a provider-backed compaction strategy.
171
171
  - [Observational memory compaction package](compaction-observational-memory.md): optional extension helper that registers an inert fast memory compaction strategy.
172
- - [Caveman behavior integration](caveman.md): optional `@arnilo/prism-coding-tools/caveman` upstream Caveman skills, commands, level injector, and session `caveman-level` persistence.
173
- - [Ponytail behavior integration](ponytail.md): optional `@arnilo/prism-coding-tools/ponytail` upstream Ponytail skills, commands, mode injector, and session `ponytail-mode` persistence.
174
172
  - [Impeccable behavior integration](impeccable.md): optional `@arnilo/prism-coding-tools/impeccable` upstream Impeccable skill and `load_skill` command.
173
+ - [Contribution discovery](contribution-discovery.md): `loadSkillDirectory` loads a host-supplied `<dir>/<name>/SKILL.md` tree for host-owned personas (`examples/caveman-ponytail.ts`).
175
174
  - [Public contracts](public-contracts.md): `Extension`, `ExtensionAPI`, and contribution contract types.
176
175
  - [Credentials and redaction](credentials-and-redaction.md): secret-redaction behavior used for extension errors.
177
176
 
@@ -98,7 +98,6 @@ Keep `skillsDisclosure: "progressive"` so the full `SKILL.md` stays catalog-only
98
98
 
99
99
  ## Related APIs
100
100
 
101
- - [Caveman behavior integration](caveman.md)
102
- - [Ponytail behavior integration](ponytail.md)
103
101
  - [Extension kernel and event bus](extensions.md)
104
102
  - [Context and skills](context-and-skills.md)
103
+ - [Contribution discovery](contribution-discovery.md): `loadSkillDirectory` for host-owned upstream skill trees (the removed Caveman/Ponytail pattern).