@sema-agent/core 7.56.0 → 7.57.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 (173) hide show
  1. package/CHANGELOG.md +541 -0
  2. package/LICENSE +1 -1
  3. package/dist/agents/org-delivery-gate.js +3 -4
  4. package/dist/agents/peer-held-queue.d.ts +69 -49
  5. package/dist/agents/peer-held-queue.js +255 -106
  6. package/dist/agents/peer-session-drain.js +63 -43
  7. package/dist/agents/send-message-tool.js +3 -2
  8. package/dist/agents/subagent.js +8 -8
  9. package/dist/brain/anthropic.js +16 -8
  10. package/dist/brain/open-responses.js +10 -8
  11. package/dist/brain/openai.js +11 -9
  12. package/dist/brain/provider-dialect.d.ts +3 -2
  13. package/dist/brain/provider-dialect.js +2 -2
  14. package/dist/brain/request-params.d.ts +6 -0
  15. package/dist/brain/request-params.js +14 -1
  16. package/dist/brain/stream-engine.d.ts +9 -0
  17. package/dist/brain/stream-engine.js +24 -10
  18. package/dist/brain/stream-shared.d.ts +5 -4
  19. package/dist/brain/stream-shared.js +4 -4
  20. package/dist/core/allow-entry.d.ts +3 -10
  21. package/dist/core/allow-entry.js +17 -25
  22. package/dist/core/approval-object.d.ts +21 -5
  23. package/dist/core/approval-object.js +44 -17
  24. package/dist/core/ask-origin.d.ts +2 -2
  25. package/dist/core/auto-compaction.d.ts +71 -6
  26. package/dist/core/auto-compaction.js +42 -26
  27. package/dist/core/background-agent-store.d.ts +6 -2
  28. package/dist/core/background-agent-store.js +1 -1
  29. package/dist/core/checkpoint-requirements.d.ts +7 -1
  30. package/dist/core/checkpoint-requirements.js +2 -0
  31. package/dist/core/checkpoint-store.d.ts +17 -29
  32. package/dist/core/checkpoint-store.js +19 -11
  33. package/dist/core/cross-boundary-taint.js +4 -2
  34. package/dist/core/engine-notice.d.ts +3 -2
  35. package/dist/core/engine-notice.js +6 -0
  36. package/dist/core/file-history-store.d.ts +14 -8
  37. package/dist/core/file-history-store.js +14 -4
  38. package/dist/core/gate-lanes.js +2 -3
  39. package/dist/core/hooks.d.ts +2 -2
  40. package/dist/core/mailbox-store.d.ts +62 -1
  41. package/dist/core/mailbox-store.js +59 -0
  42. package/dist/core/mcp-name-judge.d.ts +46 -0
  43. package/dist/core/mcp-name-judge.js +61 -0
  44. package/dist/core/mcp-probe.d.ts +1 -1
  45. package/dist/core/mcp-probe.js +2 -5
  46. package/dist/core/mcp.js +10 -30
  47. package/dist/core/memory-engine/consolidation-driver.js +4 -3
  48. package/dist/core/memory-engine/distiller.js +26 -17
  49. package/dist/core/memory-engine/layout.js +5 -2
  50. package/dist/core/park-selfcheck.js +2 -0
  51. package/dist/core/permission-rule-consent.d.ts +5 -3
  52. package/dist/core/permission-rule-consent.js +20 -15
  53. package/dist/core/permission-rule-model.d.ts +43 -2
  54. package/dist/core/permission-rule-model.js +52 -8
  55. package/dist/core/permission-rule-org.js +1 -1
  56. package/dist/core/protocol-http-auth.d.ts +16 -2
  57. package/dist/core/protocol-http-auth.js +42 -11
  58. package/dist/core/protocol-naming.d.ts +20 -0
  59. package/dist/core/protocol-naming.js +20 -3
  60. package/dist/core/read-only-shell.d.ts +20 -5
  61. package/dist/core/read-only-shell.js +29 -14
  62. package/dist/core/remote-env.d.ts +37 -1
  63. package/dist/core/remote-env.js +11 -0
  64. package/dist/core/resume-startup-cause.d.ts +24 -2
  65. package/dist/core/resume-startup-cause.js +20 -1
  66. package/dist/core/roles.js +50 -7
  67. package/dist/core/runner/compact-session.d.ts +10 -0
  68. package/dist/core/runner/compact-session.js +202 -0
  69. package/dist/core/runner/compaction-call-options.d.ts +2 -2
  70. package/dist/core/runner/compaction-seams.d.ts +21 -1
  71. package/dist/core/runner/compaction-seams.js +11 -8
  72. package/dist/core/runner/contracts.d.ts +23 -16
  73. package/dist/core/runner/derived-route-fallback.d.ts +13 -0
  74. package/dist/core/runner/derived-route-fallback.js +6 -0
  75. package/dist/core/runner/env-capability-census.d.ts +3 -2
  76. package/dist/core/runner/env-capability-census.js +9 -3
  77. package/dist/core/runner/permission-rule-lanes.d.ts +7 -7
  78. package/dist/core/runner/permission-rule-lanes.js +17 -11
  79. package/dist/core/runner/prepare-acquire-reconcile.d.ts +3 -0
  80. package/dist/core/runner/prepare-acquire-reconcile.js +2 -1
  81. package/dist/core/runner/prepare-config-doors.d.ts +6 -0
  82. package/dist/core/runner/prepare-config-doors.js +4 -3
  83. package/dist/core/runner/prepare-hands-readface.d.ts +3 -1
  84. package/dist/core/runner/prepare-hands-readface.js +2 -0
  85. package/dist/core/runner/prepare-park-ask.d.ts +2 -0
  86. package/dist/core/runner/prepare-park-ask.js +2 -2
  87. package/dist/core/runner/prepare-policy-chain.js +2 -4
  88. package/dist/core/runner/prepare-task.d.ts +4 -1
  89. package/dist/core/runner/prepare-task.js +23 -27
  90. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +13 -0
  91. package/dist/core/runner/prepare-tool-disclosure-mount.js +13 -7
  92. package/dist/core/runner/rewind-files.d.ts +2 -5
  93. package/dist/core/runner/rewind-files.js +9 -51
  94. package/dist/core/runner/run-leg.js +10 -21
  95. package/dist/core/runner/run-notification-lane.d.ts +2 -0
  96. package/dist/core/runner/run-notification-lane.js +2 -2
  97. package/dist/core/runner/run-recovery-lanes.js +3 -4
  98. package/dist/core/runner/run-settle-and-teardown.js +5 -11
  99. package/dist/core/runner/run-terminal-adoption.js +3 -4
  100. package/dist/core/runner/run-turn-boundary.js +3 -5
  101. package/dist/core/runner/runtask.d.ts +8 -1
  102. package/dist/core/runner/runtask.js +28 -34
  103. package/dist/core/runner/send-message-continuation.js +1 -1
  104. package/dist/core/runner/session-verb-doors.d.ts +28 -0
  105. package/dist/core/runner/session-verb-doors.js +48 -0
  106. package/dist/core/runner/session-write-ledger.d.ts +30 -0
  107. package/dist/core/runner/session-write-ledger.js +101 -0
  108. package/dist/core/runner/stream-settle-backstop.js +11 -14
  109. package/dist/core/runner/sub-agent-deny-lists.d.ts +25 -3
  110. package/dist/core/runner/sub-agent-deny-lists.js +21 -10
  111. package/dist/core/runner/teardown-bounded.d.ts +1 -0
  112. package/dist/core/runner/teardown-bounded.js +1 -0
  113. package/dist/core/runner-deps.d.ts +2 -2
  114. package/dist/core/secret-env.d.ts +4 -0
  115. package/dist/core/secret-env.js +3 -0
  116. package/dist/core/session-policy-store.d.ts +7 -2
  117. package/dist/core/session-policy-store.js +1 -1
  118. package/dist/core/shell-wrapper-projection.d.ts +1 -1
  119. package/dist/core/store-contracts/background-agent-store-contract.js +1 -0
  120. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -1
  121. package/dist/core/store-contracts/file-history-store-contract.js +9 -0
  122. package/dist/core/store-contracts/mailbox-store-contract.js +49 -1
  123. package/dist/core/task-registry-agent.d.ts +8 -4
  124. package/dist/core/task-registry-agent.js +14 -8
  125. package/dist/core/task-registry-shared.d.ts +35 -6
  126. package/dist/core/task-registry-shared.js +11 -0
  127. package/dist/core/task-registry.d.ts +1 -1
  128. package/dist/core/task-registry.js +1 -1
  129. package/dist/core/tool-catalog-entries.d.ts +6 -0
  130. package/dist/core/tool-catalog-entries.js +1 -0
  131. package/dist/core/tool-name-aliases.d.ts +32 -2
  132. package/dist/core/tool-name-aliases.js +23 -0
  133. package/dist/core/tool-name-readings.d.ts +4 -0
  134. package/dist/core/tool-name-readings.js +1 -0
  135. package/dist/core/tool-policy.d.ts +10 -6
  136. package/dist/core/tool-policy.js +4 -7
  137. package/dist/core/trace.d.ts +4 -1
  138. package/dist/core/trace.js +2 -1
  139. package/dist/core/untrusted-egress.d.ts +20 -0
  140. package/dist/core/untrusted-egress.js +77 -3
  141. package/dist/engine/harness/agent-harness.d.ts +7 -5
  142. package/dist/engine/harness/agent-harness.js +11 -2
  143. package/dist/engine/loop/agent-loop.d.ts +1 -4
  144. package/dist/engine/loop/agent-loop.js +26 -4
  145. package/dist/index.d.ts +12 -7
  146. package/dist/index.js +7 -4
  147. package/dist/internal/harness.d.ts +2 -1
  148. package/dist/internal/harness.js +1 -0
  149. package/dist/primitives/credential-ref.d.ts +89 -0
  150. package/dist/primitives/credential-ref.js +125 -0
  151. package/dist/primitives/index.d.ts +2 -0
  152. package/dist/primitives/index.js +1 -0
  153. package/dist/stores/file/checkpoint-store.d.ts +0 -4
  154. package/dist/stores/file/checkpoint-store.js +0 -1
  155. package/dist/stores/file/file-history-store.js +12 -4
  156. package/dist/stores/file/mailbox-store.d.ts +2 -1
  157. package/dist/stores/file/mailbox-store.js +26 -1
  158. package/dist/tools/fs/bash-readonly-classifier.d.ts +15 -19
  159. package/dist/tools/fs/bash-readonly-classifier.js +5 -10
  160. package/dist/tools/fs/fs-bash.js +10 -3
  161. package/dist/tools/fs/pattern-kill-guard.d.ts +3 -2
  162. package/dist/tools/fs/pattern-kill-guard.js +68 -36
  163. package/dist/tools/fs/shell-read-check.d.ts +10 -0
  164. package/dist/tools/fs/shell-read-check.js +7 -1
  165. package/dist/tools/fs/shell-read-identity.d.ts +12 -9
  166. package/dist/tools/fs/shell-read-identity.js +9 -4
  167. package/dist/tools/web.js +9 -7
  168. package/package.json +1 -1
  169. package/test/export-surface.snapshot.json +79 -3
  170. package/wire-changes/core-stores-sql@7.57.0.json +29 -0
  171. package/wire-changes/core@7.57.0.json +1487 -0
  172. package/wire-changes/mods-kernel@7.57.0.json +29 -0
  173. package/wire-changes/primitives@7.57.0.json +245 -0
@@ -13,7 +13,7 @@ import { announceReadDenySwap, screenSwappableDeps } from "../swappable-deps.js"
13
13
  import { runSideQuery } from "../side-query.js";
14
14
  import { PushQueue } from "../push-queue.js";
15
15
  import { TtlSessionStore } from "../session-store.js";
16
- import { errorCodeOf } from "./terminal-account.js";
16
+ import { compensateUnstarted, unstartedRowSentence } from "../resume-startup-cause.js";
17
17
  import { effectiveDelegationFacts, prepareTask } from "./prepare-task.js";
18
18
  import { SessionReadFileStates } from "./session-read-file-states.js";
19
19
  import { SessionModeSeats } from "./session-mode-seats.js";
@@ -24,6 +24,8 @@ import { PendingSessionNotifications } from "../task-notification.js";
24
24
  import { ToolDetachHub } from "../tool-detach.js";
25
25
  import { assertContextSettingsValue, assertContextSettingsWords, contextOverlayFor } from "../context-settings.js";
26
26
  import { createRunState } from "./initial-run-state.js";
27
+ import { SessionWriteLedger } from "./session-write-ledger.js";
28
+ import { teardownLegBudget } from "./teardown-bounded.js";
27
29
  import { emitSettled, reconciledEnd } from "./tool-end-body.js";
28
30
  import { answerFaceForRedeemedCall } from "./decide-continuation.js";
29
31
  import { createTurnBoundary } from "./run-turn-boundary.js";
@@ -63,11 +65,14 @@ import { runRecoveryLanes } from "./run-recovery-lanes.js";
63
65
  import { runGitLane } from "./run-git-lane.js";
64
66
  import { deploymentCanRerootChild } from "./child-tree.js";
65
67
  import { rewindSessionFiles } from "./rewind-files.js";
66
- import { thrownText } from "../thrown-value.js";
68
+ import { compactIdleSession } from "./compact-session.js";
69
+ import { egressThrownText } from "../untrusted-egress.js";
67
70
  export class Runner {
68
71
  deps;
69
72
  sessions;
70
73
  sessionLocks = new Map();
74
+ sessionWrites = new SessionWriteLedger();
75
+ verbLockSeats = { sessionBusy: (id) => this.sessionLocks.has(id) || this.sessionWrites.busy(id), takeSessionLock: (id) => this.acquireSessionLock(id) };
71
76
  sessionReadStates = new SessionReadFileStates();
72
77
  sessionModeSeats = new SessionModeSeats();
73
78
  pendingSessionNotifications = new PendingSessionNotifications();
@@ -96,7 +101,7 @@ export class Runner {
96
101
  version: 1,
97
102
  sessionId: context.sessionId,
98
103
  phase: context.phase,
99
- reason: (thrownText(sinkErr)).slice(0, 256),
104
+ reason: egressThrownText("same-principal", sinkErr).slice(0, 256),
100
105
  ts: Date.now(),
101
106
  }));
102
107
  }
@@ -159,11 +164,14 @@ export class Runner {
159
164
  const chain = prev.then(() => mine);
160
165
  this.sessionLocks.set(sessionId, chain);
161
166
  await prev.catch(() => { });
162
- return () => {
167
+ const releaseHold = () => {
163
168
  release();
164
169
  if (this.sessionLocks.get(sessionId) === chain)
165
170
  this.sessionLocks.delete(sessionId);
166
171
  };
172
+ if (this.sessionWrites.busy(sessionId))
173
+ await this.sessionWrites.settleBeforeHold(sessionId, teardownLegBudget()).catch((err) => { releaseHold(); throw err; });
174
+ return releaseHold;
167
175
  }
168
176
  swapDeps(next) {
169
177
  const screened = screenSwappableDeps(next);
@@ -233,7 +241,8 @@ export class Runner {
233
241
  sideQuery(spec) {
234
242
  return runWithReasoningWireFacts(() => { }, () => runWithCapabilityWithheld(() => { }, () => runSideQuery(spec, { brain: this.deps.brain, models: this.deps.models, roles: this.deps.roles })));
235
243
  }
236
- rewindFiles(request) { return rewindSessionFiles({ deps: this.deps, sessions: this.sessions, readStates: this.sessionReadStates, sessionBusy: (id) => this.sessionLocks.has(id), takeSessionLock: (id) => this.acquireSessionLock(id) }, request); }
244
+ rewindFiles(request) { return rewindSessionFiles({ deps: this.deps, sessions: this.sessions, readStates: this.sessionReadStates, ...this.verbLockSeats }, request); }
245
+ compactSession(request) { return compactIdleSession({ deps: this.deps, sessions: this.sessions, ...this.verbLockSeats }, request); }
237
246
  runTaskStream(spec, resume, internals) {
238
247
  if (resume !== undefined && (typeof resume !== "object" || resume.outcome === undefined)) {
239
248
  throw new TypeError("runTaskStream: `resume` must be a ResumeRun carrying `outcome` — got a value without one. " +
@@ -281,14 +290,16 @@ export class Runner {
281
290
  return held.taken;
282
291
  };
283
292
  const namedSessionId = spec.sessionId;
293
+ const legWrites = this.sessionWrites.openLeg((err, sessionId) => this.deps.onError?.(err, { phase: "degraded", sessionId }));
284
294
  const run = (async () => {
285
- if (namedSessionId)
286
- await holdSessionLock(namedSessionId);
287
295
  try {
296
+ if (namedSessionId)
297
+ await holdSessionLock(namedSessionId);
288
298
  await this.runLocked({
289
299
  spec,
290
300
  queue,
291
301
  holdSessionLock: namedSessionId ? undefined : holdSessionLock,
302
+ legWrites,
292
303
  setResult: (r) => {
293
304
  resultValue = r;
294
305
  const pcs = internals?.inheritedGate?.parentConstraints;
@@ -331,6 +342,7 @@ export class Runner {
331
342
  });
332
343
  }
333
344
  finally {
345
+ legWrites.close();
334
346
  releaseLock?.();
335
347
  publishReady(handle);
336
348
  manualCompactRef.closed = true;
@@ -382,9 +394,9 @@ export class Runner {
382
394
  };
383
395
  }
384
396
  runLocked(input) {
385
- const { spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, holdSessionLock, resume, internals, notifyRef, captureOptOutRef, mcpRedialRef, entryActor, entryTracer } = input;
397
+ const { spec, queue, setResult, onSuggestions, onReady, onSuspend, manualCompactRef, taskIdRef, holdSessionLock, legWrites, resume, internals, notifyRef, captureOptOutRef, mcpRedialRef, entryActor, entryTracer } = input;
386
398
  return runNotificationLane({
387
- spec, queue, resume, internals, taskIdRef, holdSessionLock,
399
+ spec, queue, resume, internals, taskIdRef, holdSessionLock, legWrites,
388
400
  runner: this.depsSeat, self: this, sessions: this.sessions, sessionReadStates: this.sessionReadStates, sessionModeSeats: this.sessionModeSeats, pendingSessionNotifications: this.pendingSessionNotifications,
389
401
  prepareTask,
390
402
  next: ({ prepared, injectTaskNotification, deliveredAtTurnOpen, peerInboundChainRef, modelCatalog, notificationLane, unsubscribeTaskNotifications }) => {
@@ -527,37 +539,19 @@ export class Runner {
527
539
  preemptSignal: config.preemptSignal?.aborted ? undefined : config.preemptSignal,
528
540
  ...(answerFace !== undefined ? { onQuestion: answerFace } : {}),
529
541
  };
530
- const reopenFn = store.reopen?.bind(store);
542
+ const reopenUnstarted = async (reason) => ((await store.reopen(token, cp.scope, reason)) ? "reopened" : "declined");
531
543
  const recordExecutionOutcome = (gate) => store.recordExecutionOutcome(token, cp.scope, gate);
532
- const onEnvRestoreFailed = reopenFn
533
- ? async (reason) => {
534
- const reopened = await reopenFn(token, cp.scope, reason);
535
- if (!reopened) {
536
- throw new CheckpointError("checkpoint.reopen_failed", "checkpoint could not be reopened for a retry (missing, expired, or changed concurrently) — the resume failure is terminal");
537
- }
538
- }
539
- : undefined;
540
544
  if (resumeSignal?.aborted === true && consumeFlipDone) {
541
545
  throw new CheckpointError("checkpoint.resume_aborted", "the resume was aborted after its row was already claimed — the checkpoint stays consumed and the caller's rollback settles the outcome-unknown terminal (reopening here would strand a pending approval no redemption path can reach)");
542
546
  }
543
547
  if (resumeSignal?.aborted === true) {
544
- let reopenOutcome = "refused";
545
- if (onEnvRestoreFailed !== undefined) {
546
- try {
547
- await onEnvRestoreFailed("env_failed");
548
- reopenOutcome = "reopened";
549
- }
550
- catch (err) {
551
- reopenOutcome = errorCodeOf(err) === "checkpoint.reopen_failed" ? "refused" : "unknown";
552
- }
553
- }
554
- throw reopenOutcome === "reopened"
555
- ? new CheckpointError("checkpoint.resume_aborted", "the resume was aborted before the approved action could run — the checkpoint was reopened, so the same approval can be resumed with a live signal")
556
- : reopenOutcome === "refused"
557
- ? new CheckpointError("checkpoint.reopen_failed", "the resume was aborted before the approved action could run AND the store refused to reopen the checkpoint — the approval is terminally consumed and the suspended work was not executed; a retry needs a fresh approval")
558
- : new CheckpointError("checkpoint.reopen_failed", "the resume was aborted before the approved action could run and the reopen attempt FAILED IN FLIGHT — the checkpoint's state is unprovable from here: it may already be pending again. Re-read it before deciding; do NOT issue a fresh approval on the assumption the old one is dead (the approved action did NOT run either way)");
548
+ const row = await compensateUnstarted(reopenUnstarted, "env_failed");
549
+ const said = `the resume was aborted before the approved action could run — ${unstartedRowSentence(row)}`;
550
+ if (row.kind === "reopened")
551
+ throw new CheckpointError("checkpoint.resume_aborted", said);
552
+ throw new CheckpointError("checkpoint.reopen_failed", said);
559
553
  }
560
- return this.runTaskStream(spec, { cp, outcome: outcomeForStore, onEnvRestoreFailed, recordExecutionOutcome, ...(wakeMessage !== undefined ? { wakeMessage } : {}), ...(wakeHookContext !== undefined ? { wakeMessageHookContext: wakeHookContext } : {}), ...(legMods !== undefined ? { legMods } : {}) }, resumeInternals);
554
+ return this.runTaskStream(spec, { cp, outcome: outcomeForStore, reopenUnstarted, recordExecutionOutcome, ...(wakeMessage !== undefined ? { wakeMessage } : {}), ...(wakeHookContext !== undefined ? { wakeMessageHookContext: wakeHookContext } : {}), ...(legMods !== undefined ? { legMods } : {}) }, resumeInternals);
561
555
  }
562
556
  }
563
557
  export function runTask(spec, deps) {
@@ -9,7 +9,7 @@ function buildTier3ReviveSpawn(delegationForRevive, enrichSpecToolCtx) {
9
9
  prompt: req.prompt,
10
10
  run_in_background: true,
11
11
  ...(req.row.agentType !== undefined ? { subagent_type: req.row.agentType } : {}),
12
- ...(req.row.name !== undefined ? { name: req.row.name } : {}),
12
+ name: req.row.name,
13
13
  ...(req.row.model !== undefined ? { model: req.row.model } : {}),
14
14
  }, enrichSpecToolCtx({ toolCallId: `rv-${randomBytes(8).toString("hex")}`, reviveClaim: { row: req.row, rev: req.rev, ...(req.peerSeed !== undefined ? { peerSeed: req.peerSeed } : {}) } }));
15
15
  const o = (typeof out === "string" ? { content: out } : out);
@@ -0,0 +1,28 @@
1
+ import type { AcquiredSession, SessionStore } from "../session.js";
2
+ /** What the Runner lends an out-of-run verb for the session's lock. */
3
+ export interface SessionLockSeats {
4
+ /** Whether a leg of this Runner holds or awaits `sessionId`'s lock, or a write one issued to it has not settled. */
5
+ sessionBusy(sessionId: string): boolean;
6
+ /** Take `sessionId`'s lock; called synchronously after `sessionBusy` answered false, so nothing slips in between. */
7
+ takeSessionLock(sessionId: string): Promise<() => void>;
8
+ }
9
+ /**
10
+ * The request door: the request must be a plain data object — not a Proxy (its traps are the caller's code, and could
11
+ * answer the caller's read of a member differently from this one), its prototype `Object.prototype` or none (so no
12
+ * member is inherited) — judged before any member is read; its members are then taken from ONE snapshot of its own
13
+ * data properties, so no code of the request itself (a trap, a getter) runs while it is read and nothing read can
14
+ * change what is read next (an accessor member is a bad request); own keys inside the verb's closed set. `bad`
15
+ * throws the verb's own refusal; the member types are the verb's to judge on the returned snapshot.
16
+ */
17
+ export declare function readPlainRequest(request: unknown, keys: ReadonlySet<string>, bad: (what: string) => never): Record<string, unknown>;
18
+ /** A value as a refusal names it: a string clipped, a primitive spelled, anything else by its type. */
19
+ export declare function shownValue(v: unknown): string;
20
+ /**
21
+ * The idle claim: refused with `busy()` when a leg of this Runner is running or queued on the session (its state is
22
+ * not moved under it); otherwise the session's lock is claimed in this same synchronous step and held by the verb
23
+ * until it releases it, so a leg that arrives meanwhile waits for the verb.
24
+ */
25
+ export declare function claimIdleSession(seats: SessionLockSeats, sessionId: string, busy: () => Error): Promise<() => void>;
26
+ /** The session must exist — the same door and code a run with `requireExistingSession` meets. `verb` prefixes the
27
+ * message, `nothingDone` says what the verb did not do. */
28
+ export declare function acquireExistingSession(sessions: SessionStore, sessionId: string, verb: string, nothingDone: string): Promise<AcquiredSession>;
@@ -0,0 +1,48 @@
1
+ import { types } from "node:util";
2
+ import { isPlainRecord } from "../mods/kernel/receive.js";
3
+ import { readMember } from "../thrown-value.js";
4
+ export function readPlainRequest(request, keys, bad) {
5
+ if (typeof request !== "object" || request === null)
6
+ return bad(`the request must be a plain object, got ${request === null ? "null" : typeof request}`);
7
+ if (types.isProxy(request) || !isPlainRecord(request))
8
+ return bad("the request must be a plain data object (not a Proxy, and with no prototype that could supply a member)");
9
+ let descriptors;
10
+ try {
11
+ descriptors = Object.getOwnPropertyDescriptors(request);
12
+ }
13
+ catch {
14
+ return bad("the request could not be read");
15
+ }
16
+ const own = Reflect.ownKeys(descriptors);
17
+ const unknown = own.filter((k) => typeof k !== "string" || !keys.has(k));
18
+ if (unknown.length > 0)
19
+ return bad(`unknown key ${unknown.map((k) => JSON.stringify(String(k))).join(", ")}`);
20
+ const read = {};
21
+ for (const k of own) {
22
+ const d = descriptors[k];
23
+ if (!("value" in d))
24
+ return bad(`member ${JSON.stringify(k)} is an accessor, not a value`);
25
+ read[k] = d.value;
26
+ }
27
+ return read;
28
+ }
29
+ export function shownValue(v) {
30
+ return typeof v === "string" ? JSON.stringify(v.slice(0, 40)) : v === null || typeof v === "boolean" || typeof v === "number" ? String(v) : `a ${typeof v}`;
31
+ }
32
+ export function claimIdleSession(seats, sessionId, busy) {
33
+ if (seats.sessionBusy(sessionId))
34
+ throw busy();
35
+ return seats.takeSessionLock(sessionId);
36
+ }
37
+ export async function acquireExistingSession(sessions, sessionId, verb, nothingDone) {
38
+ try {
39
+ return await sessions.acquire(sessionId, { requireExisting: true });
40
+ }
41
+ catch (err) {
42
+ if (readMember(err, "code") !== "not_found")
43
+ throw err;
44
+ const e = new Error(`${verb}: session "${sessionId}" does not exist — ${nothingDone}`);
45
+ e.code = "resume.session_not_found";
46
+ throw e;
47
+ }
48
+ }
@@ -0,0 +1,30 @@
1
+ import type { SessionStorage } from "../session.js";
2
+ import type { LegWrites } from "./contracts.js";
3
+ export type { LegWrites };
4
+ export declare class SessionWriteLedger {
5
+ #private;
6
+ /** A leg's view. `report` receives the incidents (a refused write, a late settlement); a throwing sink is swallowed. */
7
+ openLeg(report: (err: Error, sessionId: string) => void): LegWrites;
8
+ /**
9
+ * The session's next hold starts here: resolves once `sessionId` has no write in flight, and throws `SessionError("conflict")`
10
+ * — the leg about to start has written nothing — when one is still unsettled after `ms`.
11
+ */
12
+ settleBeforeHold(sessionId: string, ms: number): Promise<void>;
13
+ /**
14
+ * Whether `sessionId` has a write in flight — a synchronous read. The session's next holder asks it before {@link settleBeforeHold}
15
+ * and awaits only on `true`: a session with nothing in flight takes no suspension point at its hold. The two reads sit on one tick,
16
+ * and nothing joins the set between the check and the hold's return — the previous holder's view is closed before its lock is
17
+ * released, the lock serialises holders, and a holder that is no leg (the rewind verb) issues no write through a view.
18
+ */
19
+ busy(sessionId: string): boolean;
20
+ /**
21
+ * Resolves `true` once `sessionId` has no write in flight, `false` when `ms` elapse first. The timer is not unref'd: the
22
+ * caller awaits this in foreground control flow (the teardown-bounded discipline), and it is cleared on settle.
23
+ */
24
+ drained(sessionId: string, ms: number): Promise<boolean>;
25
+ }
26
+ /**
27
+ * The acquired storage as a leg writes it: the two write members go through `leg.issue` (named by the entry type, `leaf` for a
28
+ * leaf move), every other member is the storage's own. Per leg and non-mutating (a Proxy), like the conflict watch above it.
29
+ */
30
+ export declare function writesThroughLeg(storage: SessionStorage, leg: LegWrites, sessionId: string): SessionStorage;
@@ -0,0 +1,101 @@
1
+ import { SessionError } from "../session.js";
2
+ import { thrownText } from "../thrown-value.js";
3
+ export class SessionWriteLedger {
4
+ #inFlight = new Map();
5
+ #drainWaiters = new Map();
6
+ openLeg(report) {
7
+ let closedAt;
8
+ const tell = (sessionId, message) => {
9
+ try {
10
+ report(new Error(message), sessionId);
11
+ }
12
+ catch {
13
+ }
14
+ };
15
+ return {
16
+ issue: (sessionId, what, start) => {
17
+ if (closedAt !== undefined) {
18
+ const message = `session "${sessionId}": a ${what} write was issued after its leg ended and was not started`;
19
+ tell(sessionId, message);
20
+ return Promise.reject(new SessionError("conflict", message));
21
+ }
22
+ let write;
23
+ try {
24
+ write = Promise.resolve(start());
25
+ }
26
+ catch (err) {
27
+ return Promise.reject(err);
28
+ }
29
+ this.#add(sessionId, write);
30
+ const settledLate = (outcome) => {
31
+ this.#remove(sessionId, write);
32
+ if (closedAt !== undefined)
33
+ tell(sessionId, `session "${sessionId}": a ${what} write issued by a leg that has since ended settled ${Date.now() - closedAt}ms after the leg ended — ${outcome}`);
34
+ };
35
+ write.then(() => settledLate("it landed"), (err) => settledLate(`it failed: ${thrownText(err)}`));
36
+ return write;
37
+ },
38
+ close: () => {
39
+ closedAt ??= Date.now();
40
+ },
41
+ };
42
+ }
43
+ async settleBeforeHold(sessionId, ms) {
44
+ if (await this.drained(sessionId, ms))
45
+ return;
46
+ throw new SessionError("conflict", `session "${sessionId}": a write an earlier leg of this session issued has not settled after ${ms}ms — this leg wrote nothing; retry once it settles`);
47
+ }
48
+ busy(sessionId) {
49
+ return (this.#inFlight.get(sessionId)?.size ?? 0) > 0;
50
+ }
51
+ drained(sessionId, ms) {
52
+ if (!this.busy(sessionId))
53
+ return Promise.resolve(true);
54
+ return new Promise((resolve) => {
55
+ const waiters = this.#drainWaiters.get(sessionId) ?? new Set();
56
+ this.#drainWaiters.set(sessionId, waiters);
57
+ const done = () => {
58
+ clearTimeout(timer);
59
+ waiters.delete(done);
60
+ if (waiters.size === 0 && this.#drainWaiters.get(sessionId) === waiters)
61
+ this.#drainWaiters.delete(sessionId);
62
+ resolve(true);
63
+ };
64
+ const timer = setTimeout(() => {
65
+ waiters.delete(done);
66
+ if (waiters.size === 0 && this.#drainWaiters.get(sessionId) === waiters)
67
+ this.#drainWaiters.delete(sessionId);
68
+ resolve(false);
69
+ }, ms);
70
+ waiters.add(done);
71
+ });
72
+ }
73
+ #add(sessionId, write) {
74
+ const set = this.#inFlight.get(sessionId) ?? new Set();
75
+ set.add(write);
76
+ this.#inFlight.set(sessionId, set);
77
+ }
78
+ #remove(sessionId, write) {
79
+ const set = this.#inFlight.get(sessionId);
80
+ if (set === undefined)
81
+ return;
82
+ set.delete(write);
83
+ if (set.size > 0)
84
+ return;
85
+ this.#inFlight.delete(sessionId);
86
+ for (const wake of [...(this.#drainWaiters.get(sessionId) ?? [])])
87
+ wake();
88
+ }
89
+ }
90
+ export function writesThroughLeg(storage, leg, sessionId) {
91
+ return new Proxy(storage, {
92
+ get(target, prop, receiver) {
93
+ if (prop === "appendEntry")
94
+ return (...args) => leg.issue(sessionId, args[0].type, () => target.appendEntry(...args));
95
+ if (prop === "setLeafId")
96
+ return (...args) => leg.issue(sessionId, "leaf", () => target.setLeafId(...args));
97
+ const value = Reflect.get(target, prop, receiver);
98
+ return typeof value === "function" ? value.bind(target) : value;
99
+ },
100
+ });
101
+ }
@@ -1,5 +1,5 @@
1
- import { errorCodeOf, thrownFailureOf } from "./terminal-account.js";
2
- import { reopenReasonOf, resumeStartupCauseOf } from "../resume-startup-cause.js";
1
+ import { thrownFailureOf } from "./terminal-account.js";
2
+ import { compensateUnstarted, reopenReasonOf, resumeStartupCauseOf, unstartedRowSentence } from "../resume-startup-cause.js";
3
3
  import { resumeDecisionWasNegative } from "./decide-continuation.js";
4
4
  import { drainForwardedFramesBeforeDone, forwardsSubagentEvents } from "./prepare-run-refs.js";
5
5
  import { hasDestroy } from "../remote-env.js";
@@ -13,23 +13,20 @@ export function streamSettleBackstop(input) {
13
13
  const onRunRejected = async (err) => {
14
14
  let { code, message, detail } = thrownFailureOf(err, thrownText(err));
15
15
  const reopenReason = reopenReasonOf(resumeStartupCauseOf(code));
16
- const reopenable = resume !== undefined && resume.pendingActionStarted !== true && reopenReason !== undefined && !(resumeDecisionWasNegative(resume) && resume.decisionDelivered === true);
16
+ const reopenable = resume !== undefined && live.resultValue === undefined && resume.pendingActionStarted !== true && !(resumeDecisionWasNegative(resume) && resume.decisionDelivered === true);
17
17
  let reopenCommitted = false;
18
18
  let reopenStateUnknown = false;
19
- if (reopenable && resume?.onEnvRestoreFailed) {
20
- try {
21
- await resume.onEnvRestoreFailed(reopenReason);
19
+ if (reopenable && resume !== undefined) {
20
+ const row = await compensateUnstarted(resume.reopenUnstarted, reopenReason);
21
+ if (row.kind === "reopened") {
22
22
  reopenCommitted = true;
23
+ message = `${message} — ${unstartedRowSentence(row)}`;
23
24
  }
24
- catch (reopenErr) {
25
+ else {
25
26
  parentConstraintRegistry.delete(resume.cp.token);
26
27
  const original = asThrownError(err);
27
- const reopenMsg = thrownText(reopenErr);
28
- const definitive = errorCodeOf(reopenErr) === "checkpoint.reopen_failed";
29
- reopenStateUnknown = !definitive;
30
- err = Object.assign(new Error(definitive
31
- ? `${reopenMsg} (original resume failure: ${original.message})`
32
- : `checkpoint reopen threw mid-flight — state UNKNOWN, confirm via the checkpoint store before retrying: ${reopenMsg} (original resume failure: ${original.message})`, { cause: original }), { code: "checkpoint.reopen_failed" });
28
+ reopenStateUnknown = row.kind === "unknown";
29
+ err = Object.assign(new Error(`${original.message} — ${unstartedRowSentence(row)}`, { cause: original }), { code: "checkpoint.reopen_failed" });
33
30
  const replacement = thrownFailureOf(err, thrownText(err));
34
31
  code = replacement.code;
35
32
  message = replacement.message;
@@ -38,7 +35,7 @@ export function streamSettleBackstop(input) {
38
35
  }
39
36
  if (resume)
40
37
  locallyClaimedTokens.delete(resume.cp.token);
41
- if (resume && !reopenCommitted) {
38
+ if (resume && !reopenCommitted && live.resultValue === undefined) {
42
39
  parentConstraintRegistry.delete(resume.cp.token);
43
40
  await settleTeardownLeg(() => sessions.unpin?.(resume.cp.sessionId), "sessions.unpin (resume-failure leg)", (e) => runner.deps.onError?.(e, { phase: "config", sessionId: resume.cp.sessionId }));
44
41
  if (!reopenStateUnknown) {
@@ -36,7 +36,7 @@
36
36
  import type { AgentTool } from "../../internal/harness.js";
37
37
  import { valveLeaves } from "../../agents/tool-filter.js";
38
38
  import { type ProtocolId } from "../protocol-table.js";
39
- import type { ToolLegKind, ToolSelectorLayer } from "../tool-face.js";
39
+ import type { ToolLegKind, ToolSelector, ToolSelectorLayer } from "../tool-face.js";
40
40
  import { type ConstraintChainEntry } from "../tool-policy.js";
41
41
  import type { TaskSpec } from "../types.js";
42
42
  import type { CheckpointErrorReason } from "../checkpoint-store.js";
@@ -87,6 +87,27 @@ export type PersistedToolFaceReason = Extract<CheckpointErrorReason, "not_a_list
87
87
  * leg's roster and a sink; the leg's own policy-name audit is what reads them.
88
88
  */
89
89
  export declare function deniedToolNamesOf(where: string, value: unknown): string[];
90
+ /**
91
+ * Whose lists a run's own layer are, in the words every sentence about them uses: the delegation tool (and agent definition) that wrote
92
+ * them when the run records one (`RunInternals.ownToolLists`), else `undefined` — the run's own `TaskSpec` seats.
93
+ */
94
+ export declare function ownListsWhereOf(ownBy: RunInternals["ownToolLists"]): string | undefined;
95
+ /**
96
+ * THE build of a leg's tool selector: `layers` are the ceiling it was spawned under, then its own lists (the last is its own), and
97
+ * `legs` the kinds of leg it is. Every layer is read by the one layer reader ({@link readSelectorLayer}), once, whoever judged it and
98
+ * whatever evidence the run carries — a live ceiling and the run's own lists were judged by the engine that wrote them, which may have
99
+ * read what this one cannot (a list that is not one, a member that is not a string, an entry no rule syntax reads), so the leg does not
100
+ * start without a layer that says something. The selector is compiled from what was read, never from the layers again: a getter that
101
+ * answers differently the second time is not asked a second time, and the selector's layers (handed to the leg's children as their
102
+ * ceiling) are the frozen product. Built before anything reads it, and before the session is acquired, so a refusal needs nothing given
103
+ * back. `where` is whose lists the own layer is ({@link ownListsWhereOf}). (A ceiling a resumed row restored was read by the same reader
104
+ * before the claim — `persistedToolNamesRefusal`.)
105
+ */
106
+ export declare function readToolSelector(input: {
107
+ readonly layers: readonly unknown[];
108
+ readonly where: string | undefined;
109
+ readonly legs: readonly ToolLegKind[];
110
+ }): ToolSelector;
90
111
  /**
91
112
  * THE read-back of the tool names a checkpoint row carries, before the claim: the ceiling the resumed leg will run under when it is
92
113
  * the row's (`ceiling`: absent when a live seat hands one) — each layer read by {@link readSelectorLayer} as a recorded layer — and
@@ -141,8 +162,9 @@ export interface RunToolListsInput {
141
162
  readonly allow: readonly string[] | undefined;
142
163
  readonly exclude: readonly string[] | undefined;
143
164
  };
144
- /** borrowed-readonly — every layer of the run's selector (the ceiling, then its own) and the kinds of leg it is. */
145
- layers: readonly ToolSelectorLayer[];
165
+ /** borrowed-readonly — the run's selector, as {@link readToolSelector} built it: every layer of it (the ceiling, then its own) already read, and judged here as read. */
166
+ selector: ToolSelector;
167
+ /** The kinds of leg the run is (the ones the selector was built for). */
146
168
  legs: readonly ToolLegKind[];
147
169
  /** True ⇔ the spawning run's prepare judged exactly these own lists where they were mounted, against a pool it could list (as the
148
170
  * child-facing lists of the delegation tool or definition that wrote them) — the evidence digest it handed down (live, or recorded
@@ -150,14 +150,31 @@ function readSelectorLayer(layer, layerWhere, listWhere, recorded) {
150
150
  if (!isToolSelectorLayer(layer)) {
151
151
  throw refused(`${layerWhere} are a value that is ${valueTypeLabel(layer)}, not a pair of tool-name lists (an allowed and a denied list) — the leg does not start. Fix the layer.`, "config.tool_face_control", "not_a_list");
152
152
  }
153
+ const lists = {};
153
154
  for (const side of ["allowed", "denied"]) {
154
155
  const where = listWhere(side);
155
- const entries = readableToolNameList(where, side, side === "allowed" ? layer.allow : layer.deny, "config.tool_face_control");
156
+ const value = side === "allowed" ? layer.allow : layer.deny;
157
+ const entries = readableToolNameList(where, side, value, "config.tool_face_control");
156
158
  const written = recorded ? entries.find((read) => !isCanonicalToolName(read.entry)) : undefined;
157
159
  if (written !== undefined) {
158
160
  throw refused(`${where} declares ${renderListEntry(written.entry)} in its ${LIST_WORD[side]}: a row records each tool name in its bare spelling (\`Read\`, never \`Read()\`, \`Read(*)\` or \`Read(…)\`), and this entry was recorded by an engine that read the list otherwise — it is read here neither as that engine read it nor as this one would`, "config.tool_face_control", "unsupported_pattern");
159
161
  }
162
+ if (value !== undefined && value !== null)
163
+ lists[side] = Object.freeze(entries.map((read) => read.entry));
160
164
  }
165
+ return Object.freeze({ ...(lists.allowed !== undefined ? { allow: lists.allowed } : {}), deny: lists.denied ?? Object.freeze([]) });
166
+ }
167
+ export function ownListsWhereOf(ownBy) {
168
+ return ownBy !== undefined ? delegationListWhere(ownBy.toolName, ownBy.definition) : undefined;
169
+ }
170
+ export function readToolSelector(input) {
171
+ const layers = [...input.layers];
172
+ const ceilings = layers.length - 1;
173
+ const read = layers.map((layer, i) => {
174
+ const inherited = i === ceilings ? undefined : inheritedListsWhere(ceilings, i);
175
+ return readSelectorLayer(layer, inherited ?? "this run's own tool lists", (side) => inherited ?? input.where ?? (side === "allowed" ? "TaskSpec.allowTools" : "TaskSpec.excludeTools"), false);
176
+ });
177
+ return compileToolSelector(read, input.legs);
161
178
  }
162
179
  function ownToolListsFlaw(value) {
163
180
  try {
@@ -255,14 +272,9 @@ export function judgeRunToolLists(input) {
255
272
  catch {
256
273
  }
257
274
  };
258
- const ownWhere = input.ownBy !== undefined ? delegationListWhere(input.ownBy.toolName, input.ownBy.definition) : undefined;
259
- const ceilings = input.layers.length - 1;
260
- input.layers.forEach((layer, i) => {
261
- const own = i === ceilings;
262
- const inherited = own ? undefined : inheritedListsWhere(ceilings, i);
263
- readSelectorLayer(layer, inherited ?? "this run's own tool lists", (side) => inherited ?? ownWhere ?? (side === "allowed" ? "TaskSpec.allowTools" : "TaskSpec.excludeTools"), false);
264
- });
265
- const denied = deniedProtocolTools(input.layers, input.legs);
275
+ const ownWhere = ownListsWhereOf(input.ownBy);
276
+ const selector = input.selector;
277
+ const denied = deniedProtocolTools(selector.layers, input.legs);
266
278
  if (denied.length > 0) {
267
279
  const names = [...new Set(denied.map((h) => `"${h.name}"`))];
268
280
  const one = names.length === 1;
@@ -273,7 +285,6 @@ export function judgeRunToolLists(input) {
273
285
  const by = [...deniedBy(true, ownWhere ?? "this run's own excludeTools"), ...deniedBy(false, "an inherited list (the spawning run's excludeTools, or one it inherits)")];
274
286
  throw refused(`This kind of leg (${input.legs.join(", ")}) relies on ${names.join(", ")}, but ${by.join(" and ")} — the leg cannot work without ${one ? "it" : "them"}. Remove the entry from the list that names it.`, "config.tool_face_control", "protocol_denied");
275
287
  }
276
- const selector = compileToolSelector(input.layers, input.legs);
277
288
  const pool = delegationPoolReading(spec.tools ?? [], valveLeaves(spec, selector));
278
289
  const mounted = mountedToolNames([...tools, ...(spec.tools ?? [])]);
279
290
  if (!input.judgedUpstream) {
@@ -27,6 +27,7 @@
27
27
  export declare const TEARDOWN_LEG_MAX_MS = 15000;
28
28
  /** Test hook (fs-atomic TxnLockTimings precedent): shrink the budget so wedge pins need no real 15s. */
29
29
  export declare function setTeardownLegBudgetForTests(ms: number | undefined): void;
30
+ export declare const teardownLegBudget: () => number;
30
31
  /** The typed timeout incident — `leg` names the site so a leak is attributable from the report alone. */
31
32
  export declare class TeardownLegTimeoutError extends Error {
32
33
  readonly leg: string;
@@ -4,6 +4,7 @@ let teardownLegBudgetMs = TEARDOWN_LEG_MAX_MS;
4
4
  export function setTeardownLegBudgetForTests(ms) {
5
5
  teardownLegBudgetMs = ms ?? TEARDOWN_LEG_MAX_MS;
6
6
  }
7
+ export const teardownLegBudget = () => teardownLegBudgetMs;
7
8
  export class TeardownLegTimeoutError extends Error {
8
9
  leg;
9
10
  constructor(leg, budgetMs) {
@@ -968,8 +968,8 @@ export interface RunnerDeps {
968
968
  * organization's delivery gate appends cross-principal letters; the mailbox contract's fourth chain).
969
969
  * Absent = no consumer, no lane (byte for byte as before); a deployment that mounts a gate DECLARES
970
970
  * `{ assistant: "<agent handle>" }` (the scope's assistant is the only consumer; no terminal leg touches the
971
- * box). `"terminals"` (each terminal drains the box as a second lane) is refused until held decisions are
972
- * shared across terminals (`detail.reason: "terminals_unsupported"`). Letters are judged on the principal's
971
+ * box) or `"terminals"` (each terminal drains the box as a second lane; a held letter's decision is the
972
+ * letter's own field in the box, so every terminal reads the same one). Letters are judged on the principal's
973
973
  * `crossSessionInbound` layers (managed, user), held when unset. ANY other value fails the prepare of a leg
974
974
  * that drains a session box with `config.principal_inbox_consumer_invalid` (`detail.reason: "malformed"`).
975
975
  */
@@ -61,6 +61,10 @@ export interface WithheldEnvDeclaration {
61
61
  * a flag or placeholder word, one character repeated, or fewer than eight characters. */
62
62
  readonly values: readonly string[];
63
63
  }
64
+ /** {@link notACredentialReason}'s answer as a yes: the same guess for the second place that pays for withholding a string — every
65
+ * occurrence of a request's key taken out of a remote's words (`remoteWords`): a key that names no secret (`EMPTY`, a local gateway's
66
+ * one-letter key) is left in the words, as it is left to every child. */
67
+ export declare function namesASecret(value: string): boolean;
64
68
  /**
65
69
  * Declare this deployment's OWN credentials in this process's environment — the engine's or its host's: an approval
66
70
  * bearer, a service token — by name and by value. From then on no child spawned on this host inherits one, whatever the
@@ -209,6 +209,9 @@ function notACredentialReason(value) {
209
209
  return "one character repeated, which names no secret";
210
210
  return undefined;
211
211
  }
212
+ export function namesASecret(value) {
213
+ return notACredentialReason(value) === undefined;
214
+ }
212
215
  function credentialValueProblem(v) {
213
216
  if (typeof v !== "string")
214
217
  return kindOf(v);
@@ -76,7 +76,12 @@ export type SessionRuleEntryField = {
76
76
  * name starting with `*` as no rule; in `toolAllow` it grants nothing by itself and the entries beside it keep their reading,
77
77
  * where grammars 1–4 voided a list holding it. An engine of grammar 4 given a row written in grammar 5 refuses it rather than
78
78
  * read a `*` deny as nothing. In the same revision a host pattern with an empty label (`WebFetch(domain:.evil.test)`) or a `*`
79
- * the URL parser folds (`*.`) is no rule, where grammar 4 read the exact host it spelled; an earlier row's such entry reads as no rule.
79
+ * the URL parser folds (`*.`) is no rule, where grammar 4 read the exact host it spelled; an earlier row's such entry reads as no rule;
80
+ * - 6, a persisted rule with no content (`SendUserFile`, `Bash`, `Bash(*)`) is the WHOLE-TOOL rule (`match: "tool"`), where grammar 5's
81
+ * persisted grammar read every bare name as no rule. A session's entries mean what they meant (the entry reader already read a bare
82
+ * name as the whole tool), so no earlier entry reads differently; the stamp moves for the persisted store's buckets and the sync
83
+ * wire's states, so an engine of grammar 5 given a bucket written in grammar 6 refuses it whole rather than drop a whole-tool deny
84
+ * as damage. No persisted row of an earlier stamp can hold a bare name (every entrance refused one), so none is read anew.
80
85
  * A row an earlier grammar wrote keeps the meaning that grammar gave its entries, and an entry that grammar could not read takes
81
86
  * this one's ({@link earlierGrammarReads}, over the revisions above as one table); an entry this grammar reads as no rule reads as
82
87
  * `earlier_grammar` ({@link compileSessionRuleBucket}).
@@ -89,7 +94,7 @@ export type SessionRuleEntryField = {
89
94
  * what an existing persisted rule text means owes that store a reading of its earlier stamps, as {@link earlierGrammarReads}
90
95
  * is for entries. Every stamp is judged by {@link readRuleGrammarStamp}.
91
96
  */
92
- export declare const SESSION_RULES_GRAMMAR = 5;
97
+ export declare const SESSION_RULES_GRAMMAR = 6;
93
98
  /** {@link SessionPermissionRules} plus the monotonic `rev` the store stamps (the OCC key). */
94
99
  export interface StoredSessionRules extends SessionPermissionRules {
95
100
  rev: number;
@@ -12,7 +12,7 @@ const BUCKETS = Object.keys(SESSION_RULE_BUCKETS);
12
12
  const ENTRY_FIELDS = BUCKETS.filter((f) => SESSION_RULE_BUCKETS[f] !== "dirs");
13
13
  const STORE_STAMPS = new Set(["rev", "gen", "grammar"]);
14
14
  const OPERATOR_PART = "operatorRules";
15
- export const SESSION_RULES_GRAMMAR = 5;
15
+ export const SESSION_RULES_GRAMMAR = 6;
16
16
  export class SessionPolicyError extends Error {
17
17
  code;
18
18
  detail;
@@ -1,6 +1,6 @@
1
1
  import type { ShellWrapperRow } from "./shell-wrapper-table.js";
2
2
  /**
3
- * The PROJECTION {@link import("./shell-wrapper-table.js").SHELL_WRAPPER_TABLE} is: the second lexer's view of the program read model's rows (`program-read-model.ts`
3
+ * The PROJECTION {@link import("./shell-wrapper-table.js").SHELL_WRAPPER_TABLE} is: the tightening reader's peel view of the program read model's rows (`program-read-model.ts`
4
4
  * — one table of program facts, read through the binary gate). A launcher row's options by arity (a value word, a
5
5
  * switch or an attached-only value, a command string), its required operand (a duration, or the spelling it must have),
6
6
  * its bare-dash switch and its `NAME=value` operands; a row that runs its operands, or a code body, as shell input