@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
@@ -1,10 +1,12 @@
1
1
  import { posix } from "node:path";
2
2
  import { lexedWordOf, readShellCommand } from "./shell-run-candidates.js";
3
+ import { unlistedReaderSentence } from "../tools/fs/bash-readonly-classifier.js";
3
4
  import { descriptorsOfPlan, newLineDescriptors, standardDeviceUnrebound } from "../tools/fs/shell-read-device.js";
4
5
  import { planShellReads } from "../tools/fs/shell-read-plan.js";
5
6
  import { writesOnlyStandardDevices } from "../tools/fs/shell-read-write-targets.js";
6
7
  import { FIND_ACTION_PRIMARIES, FIND_NEWER_PRIMARY, FIND_VALUE_PRIMARIES, FLAG_VALUE_ACCEPTS, READ_ONLY_BARE_ONLY, READ_ONLY_BARE_PROGRAMS, READ_ONLY_COMMAND_TABLE, READ_ONLY_ENV_NAMES, READ_ONLY_EXACT_FORMS, READ_ONLY_GLOB_PROGRAMS, XARGS_READ_ONLY_TARGETS, dockerRetargetDanger, } from "./read-only-shell-table.js";
7
8
  const NOT = (reason) => ({ readOnly: false, reason });
9
+ const READ_ONLY = { readOnly: true };
8
10
  export const CD_GIT_COMPOUND_SENTENCE = "This command changes directory before running a version-control command, which can pick up untrusted hooks or repository configuration from the target directory. Approve only if you trust it.";
9
11
  export function changesDirectoryBeforeGit(shape, cwd) {
10
12
  const programs = shape.segments.map((s) => peelBuiltinPrefix(s.argv.slice(leadingAssignmentCount(s.argv)).map((w) => w.text))[0] ?? "");
@@ -20,7 +22,6 @@ export function changesDirectoryBeforeGit(shape, cwd) {
20
22
  const trim = (p) => p.replace(/\/+$/, "") || "/";
21
23
  return trim(posix.resolve(cwd, target.text)) !== trim(cwd);
22
24
  }
23
- const READ_ONLY = { readOnly: true };
24
25
  const BARE_PROGRAMS = new Set(READ_ONLY_BARE_PROGRAMS.filter((p) => !p.includes(" ")));
25
26
  const BARE_MULTIWORD = READ_ONLY_BARE_PROGRAMS.filter((p) => p.includes(" ")).map((p) => p.split(" "));
26
27
  const GLOB_PROGRAMS = new Set(READ_ONLY_GLOB_PROGRAMS);
@@ -41,7 +42,11 @@ const PRINTF_ESCAPE = new RegExp(`%[^%a-zA-Z]*${PRINTF_LENGTH}\\\\[0-7xX]`);
41
42
  const PRINTF_NUMERIC = new RegExp(`%[-+ 0#']*[0-9.*]*${PRINTF_LENGTH}[diouxXeEfFgGaAn]`);
42
43
  const JQ_DANGER_FLAG = /^['"]?-[a-zA-Z]*[fL]/;
43
44
  const JQ_DANGER_TEXT = /--from-file|--rawfile|--slurpfile|--run-tests|--library-path|\benv\b|\$ENV\b|\binclude\b|\bimport\b/;
44
- export function readOnlyShellVerdict(command, shape = readShellCommand(command), cwd, bound) {
45
+ export function readOnlyShellVerdict(command, opts = {}) {
46
+ const { cwd, bound, readers } = opts;
47
+ if (readers !== undefined && typeof readers.has !== "function")
48
+ throw new TypeError("readOnlyShellVerdict: `readers` must be a Set of program names");
49
+ const shape = opts.shape ?? readShellCommand(command);
45
50
  const line = lineStructureObjection(shape);
46
51
  if (line !== undefined)
47
52
  return NOT(line);
@@ -66,12 +71,15 @@ export function readOnlyShellVerdict(command, shape = readShellCommand(command),
66
71
  let boundOfText;
67
72
  const bindings = () => (boundOfText ??= (typeof bound === "function" ? bound() : bound) ?? lineDescriptorsOfText(command));
68
73
  for (const seg of shape.segments) {
69
- const verdict = segmentVerdict(seg, descriptors, bindings);
74
+ const verdict = segmentVerdict(seg, descriptors, bindings, readers);
70
75
  if (!verdict.readOnly)
71
76
  return verdict;
72
77
  }
73
78
  return READ_ONLY;
74
79
  }
80
+ function unlistedObjection(program, readers) {
81
+ return readers !== undefined && !readers.has(program) ? NOT(unlistedReaderSentence(program)) : undefined;
82
+ }
75
83
  const CD_GIT_REASON = "a compound that changes directory and runs git";
76
84
  function lineStructureObjection(shape) {
77
85
  if (shape.grouped)
@@ -144,16 +152,11 @@ function peelBuiltinPrefix(words) {
144
152
  return t;
145
153
  t = t.slice(r);
146
154
  }
147
- else if (t[0] === "noglob") {
148
- if (t.length <= 1)
149
- return t;
150
- t = t.slice(1);
151
- }
152
155
  else
153
156
  return t;
154
157
  }
155
158
  }
156
- function segmentVerdict(seg, line, bindings) {
159
+ function segmentVerdict(seg, line, bindings, readers) {
157
160
  for (const r of seg.redirections) {
158
161
  const target = r.target.text;
159
162
  if (!READING_REDIRECTS.has(r.op) && !standardDeviceUnrebound(target, line, []) && !(r.op === ">&" && /^\d+$/.test(target)))
@@ -174,6 +177,9 @@ function segmentVerdict(seg, line, bindings) {
174
177
  return NOT("a bare assignment runs no program");
175
178
  const words = peelBuiltinPrefix(run.map((w) => w.text));
176
179
  const program = words[0];
180
+ const unlisted = unlistedObjection(program, readers);
181
+ if (unlisted !== undefined)
182
+ return unlisted;
177
183
  if (run.some((w) => expansionKind(w) === "glob"))
178
184
  return GLOB_PROGRAMS.has(program) ? READ_ONLY : NOT(`\`${program}\` with a glob argument — the glob may name anything`);
179
185
  if (DEVICE_WRITERS.has(program) && writesOnlyStandardDevices(program, words.slice(1), bindings()))
@@ -181,7 +187,7 @@ function segmentVerdict(seg, line, bindings) {
181
187
  const structural = structuralReading(words);
182
188
  if (structural !== null)
183
189
  return structural ? READ_ONLY : NOT(`\`${program}\` with these arguments is not a read-only form`);
184
- return tableReading(words, run);
190
+ return tableReading(words, run, readers);
185
191
  }
186
192
  export function lineDescriptorsOfText(command) {
187
193
  return descriptorsOfPlan(planShellReads(command, { readers: new Set(), cwd: "/", roots: [] }));
@@ -248,7 +254,7 @@ function printfReading(words) {
248
254
  }
249
255
  return true;
250
256
  }
251
- function tableReading(words, run) {
257
+ function tableReading(words, run, readers) {
252
258
  const program = words[0];
253
259
  const hit = TABLE_ROWS.find((r) => words.length >= r.words.length && r.words.every((w, i) => words[i] === w));
254
260
  if (hit !== undefined) {
@@ -259,7 +265,8 @@ function tableReading(words, run) {
259
265
  if (a.includes("{") && (a.includes(",") || a.includes("..")))
260
266
  return NOT(`\`${program}\`: a brace pattern argument`);
261
267
  }
262
- if (!flagWalk(args, hit.row, program))
268
+ const named = [];
269
+ if (!flagWalk(args, hit.row, program, named))
263
270
  return NOT(`\`${program}\`: an option outside its read-only set (or a malformed option value)`);
264
271
  if (hit.row.wordsShape !== undefined ? !hit.row.wordsShape(args) : run.some((w) => w.raw.includes("`")))
265
272
  return NOT(`\`${program}\`: arguments outside its read-only form`);
@@ -267,6 +274,11 @@ function tableReading(words, run) {
267
274
  return NOT(`\`${program}\`: a line break inside an argument`);
268
275
  if (hit.row.dangerous?.(args) === true)
269
276
  return NOT(`\`${program}\`: an argument form upstream marks as writing, executing or resolving`);
277
+ for (const target of named) {
278
+ const unlisted = unlistedObjection(target, readers);
279
+ if (unlisted !== undefined)
280
+ return unlisted;
281
+ }
270
282
  return READ_ONLY;
271
283
  }
272
284
  if (program === "uniq" && words.slice(1).every((w, i, all) => /^-[a-zA-Z]+$/.test(w) || /^--[a-zA-Z-]+(=\S+)?$/.test(w) || (i > 0 && /^-[fsw]$/.test(all[i - 1]) && /^\d+$/.test(w))))
@@ -296,7 +308,7 @@ function jqReading(args) {
296
308
  }
297
309
  return true;
298
310
  }
299
- function flagWalk(args, row, program) {
311
+ function flagWalk(args, row, program, named) {
300
312
  let i = 0;
301
313
  while (i < args.length) {
302
314
  let word = args[i];
@@ -309,7 +321,10 @@ function flagWalk(args, row, program) {
309
321
  i++;
310
322
  word = args[i];
311
323
  }
312
- return XARGS_TARGETS.includes(word);
324
+ if (!XARGS_TARGETS.includes(word))
325
+ return false;
326
+ named.push(word);
327
+ return true;
313
328
  }
314
329
  if (word === "--") {
315
330
  if (row.respectsDoubleDash !== false)
@@ -323,7 +323,7 @@ export interface RemoteExecutionEnv extends ExecutionEnv {
323
323
  * `{isolation:true, suspendable:true}` adapter); **partial-capability** adapters (SSH/ADB: `{false, false}`
324
324
  * — a real machine/device, not snapshotable and not isolated) declare it here. **Required + explicit** so
325
325
  * the orchestrator never has to guess: there is no safe default (assuming isolation when there is none is a
326
- * security hole; assuming suspendable crashes on `suspendVM`). Use {@link isSuspendable}/{@link isIsolated}
326
+ * security hole; assuming suspendable crashes on `suspendVM`). Use {@link isSuspendable}/{@link isIsolated}/{@link processTableOf}
327
327
  * rather than the structural {@link isRemoteExecutionEnv} (method presence ≠ semantics — a stub `suspendVM`
328
328
  * that returns `"unsupported"` still satisfies the structural check). Extensible: add fields as new
329
329
  * partial-capability classes appear.
@@ -355,6 +355,29 @@ export interface RemoteExecutionEnv extends ExecutionEnv {
355
355
  * Read STRICTLY through {@link canonicalPathIsAuthoritative}, like every other capability whose
356
356
  * `true` changes what a containment leg does; a wrong-SHAPE value reads absent and is announced. */
357
357
  canonicalPathAuthoritative?: boolean;
358
+ /** True iff the commands this env runs are processes on a table that holds THIS process — the table the
359
+ * engine itself and the processes above it are on, so a kill the model writes there can reach them.
360
+ * `true` for an adapter that spawns the commands as this process's own descendants (a host lane), and
361
+ * for one whose commands run in a PID namespace that contains this process's (the host of the container
362
+ * the engine runs in); `false` only for one whose commands run where this process is not: another
363
+ * machine (SSH host, device, cloud sandbox) or a PID namespace this process is not in (a sandbox
364
+ * container, a pod). Not {@link isolation}: a non-isolated SSH host is a real machine and still another
365
+ * table. The self-preservation guard of the Bash tool asks its question exactly when this is `true`
366
+ * (and for every env that is not remote at all), and does not ask where it is `false`.
367
+ *
368
+ * What a `true` buys is the guard's question exactly as a NodeExecutionEnv gets it, limits included: on this
369
+ * process's own table, of the platform's `pgrep`, with the kill's words as written, from this process's own
370
+ * working directory. It is put as for this process's descendants, so for commands on the same table that
371
+ * are not (SSH to `localhost`) BSD's `pgrep` leaves its OWN caller's ancestors out where the real kill would
372
+ * not, and from an enclosing namespace the question sees only this process's namespace, not the ancestors
373
+ * above it.
374
+ *
375
+ * REQUIRED, with no default: either reading of an absent value is wrong somewhere (assuming `true`
376
+ * refuses an ordinary `pkill` in a sandbox because a word matched this engine here; assuming `false`
377
+ * lets a host lane's `pkill` end the session that issued it). So an absent or non-boolean value is
378
+ * announced once per run at the env's admission point and the guard refuses every pattern kill that needs the question on
379
+ * that env (`pkill -P $$` runs), naming this declaration, until the adapter states it. Read through {@link processTableOf}. */
380
+ sharesProcessTable: boolean;
358
381
  };
359
382
  /** Identity of the connected workspace (design/48 §5 gap 1). Synchronous: it is data the env already holds. */
360
383
  workspaceHandle(): WorkspaceHandle;
@@ -634,3 +657,16 @@ export declare function boundaryIsTheSandbox(env: ExecutionEnv): boolean;
634
657
  * has no case to fire on.
635
658
  */
636
659
  export declare function canonicalPathIsAuthoritative(env: ExecutionEnv): boolean;
660
+ /**
661
+ * Which process table this env's commands run on, as its adapter declares it
662
+ * ({@link RemoteExecutionEnv.capabilities}.sharesProcessTable):
663
+ * · `"this"` — a table that holds this process: a remote env that declares `true`, and every env that is not
664
+ * remote. The second is the base contract's in-process env (its commands are this process's children); an
665
+ * adapter that runs commands elsewhere without the remote seam has no block to declare in and is read the
666
+ * same way, which errs toward asking (a refusal of a kill that could not have reached this process);
667
+ * · `"other"` — a remote env that declares `false`;
668
+ * · `"undeclared"` — a remote env whose declaration is absent, not a boolean, or cannot be read (a throwing
669
+ * getter or Proxy trap). There is no default to fold it to, so the caller refuses what it cannot answer.
670
+ * Total: it never throws. It does not announce — the admission census does, once per run.
671
+ */
672
+ export declare function processTableOf(env: ExecutionEnv): "this" | "other" | "undeclared";
@@ -67,3 +67,14 @@ export function boundaryIsTheSandbox(env) {
67
67
  export function canonicalPathIsAuthoritative(env) {
68
68
  return isRemoteExecutionEnv(env) ? env.capabilities.canonicalPathAuthoritative === true : false;
69
69
  }
70
+ export function processTableOf(env) {
71
+ try {
72
+ if (!isRemoteExecutionEnv(env))
73
+ return "this";
74
+ const declared = env.capabilities.sharesProcessTable;
75
+ return declared === true ? "this" : declared === false ? "other" : "undeclared";
76
+ }
77
+ catch {
78
+ return "undeclared";
79
+ }
80
+ }
@@ -84,6 +84,28 @@ export type ReopenReasonTableCoversEveryCause = AssertAllKeysHandled<Exclude<Res
84
84
  * decision is untouched, and the retry replays it.
85
85
  */
86
86
  export declare function reopenReasonOf(cause: ResumeStartupCause | undefined): ReopenReason;
87
- /** What the result of a returned resume leg says after its checkpoint was reopened, per reason. */
88
- export declare function reopenedUnstartedSentence(reason: ReopenReason): string;
87
+ /**
88
+ * What happened to a claimed row whose approved action never started — the one closed outcome every exit of such a leg reports
89
+ * (the thrown-prepare backstop, the returned-leg settle, the post-claim abort): `reopened` under a reason (the row is pending again),
90
+ * `declined` (the store's `reopen` answered `false`: the row is no longer the resolved row this leg claimed — missing, expired, its
91
+ * decided action already disposed, or pending again — which the boolean does not say), `unknown` (the reopen threw: the write may have
92
+ * committed and only its acknowledgement been lost, so the row's state cannot be proven from here).
93
+ */
94
+ export type UnstartedRow = {
95
+ readonly kind: "reopened";
96
+ readonly reason: ReopenReason;
97
+ } | {
98
+ readonly kind: "declined";
99
+ } | {
100
+ readonly kind: "unknown";
101
+ readonly thrown: unknown;
102
+ };
103
+ /**
104
+ * THE compensation of a claimed leg whose approved action never started: the claim's reversal (`reopen`, bound to the row) asked once,
105
+ * under `reason`, and read into {@link UnstartedRow} — its answer, never a code sniffed off whatever it threw (a store's own error may
106
+ * carry any code, and a throw after a committed write is not a refusal). Every exit calls it; none re-reads the outcome another way.
107
+ */
108
+ export declare function compensateUnstarted(reopen: (reason: ReopenReason) => Promise<"reopened" | "declined">, reason: ReopenReason): Promise<UnstartedRow>;
109
+ /** The sentence for {@link UnstartedRow} — one per outcome, the same on every exit; the exit puts its own lead before it. */
110
+ export declare function unstartedRowSentence(row: UnstartedRow): string;
89
111
  export {};
@@ -1,3 +1,4 @@
1
+ import { egressThrownText } from "./untrusted-egress.js";
1
2
  export const REOPEN_REASONS = Object.freeze(["env_failed", "tool_unavailable", "constraint_rejected", "config_refused"]);
2
3
  export const RESUME_STARTUP_CAUSES = Object.freeze(["environment", "session_gone", "transcript_corrupt", "tool_unavailable", "configuration", "decision_rejected"]);
3
4
  const CAUSE_OF_CODE = new Map([
@@ -44,7 +45,25 @@ const REOPEN_REASON_OF_CAUSE = {
44
45
  export function reopenReasonOf(cause) {
45
46
  return cause === undefined ? "env_failed" : REOPEN_REASON_OF_CAUSE[cause];
46
47
  }
47
- export function reopenedUnstartedSentence(reason) {
48
+ export async function compensateUnstarted(reopen, reason) {
49
+ try {
50
+ return (await reopen(reason)) === "reopened" ? { kind: "reopened", reason } : { kind: "declined" };
51
+ }
52
+ catch (thrown) {
53
+ return { kind: "unknown", thrown };
54
+ }
55
+ }
56
+ export function unstartedRowSentence(row) {
57
+ switch (row.kind) {
58
+ case "reopened":
59
+ return reopenedUnstartedSentence(row.reason);
60
+ case "declined":
61
+ return "the approved action never started, and the store DECLINED to reopen the checkpoint — it is no longer the resolved row this leg claimed (missing, expired, its decided action already disposed, or pending again): read the row before deciding; if it is resolved or gone, a retry needs a fresh approval";
62
+ case "unknown":
63
+ return `the approved action never started, and reopening the checkpoint FAILED IN FLIGHT (${egressThrownText("same-principal", row.thrown)}) — its state UNKNOWN, unprovable from here: re-read it before deciding; do NOT issue a fresh approval on the assumption the old one is dead`;
64
+ }
65
+ }
66
+ function reopenedUnstartedSentence(reason) {
48
67
  switch (reason) {
49
68
  case "env_failed":
50
69
  return "the approved action never started, so the checkpoint was REOPENED (pending): the same approval can be resumed again, do not ask for a fresh one";
@@ -1,6 +1,7 @@
1
1
  import { selectModel } from "./select-model.js";
2
2
  import { namesRoleBase } from "../prompt-assembly/role-base.js";
3
3
  import { readPromptRole } from "../prompt-assembly/select.js";
4
+ import { readMember } from "./thrown-value.js";
4
5
  import { MODEL_ROLES } from "./types.js";
5
6
  export { MODEL_ROLES } from "./types.js";
6
7
  function catalogModel(ref, models) {
@@ -13,13 +14,36 @@ function modelBindingRefusal(field, reason, message) {
13
14
  e.detail = { field, reason };
14
15
  return e;
15
16
  }
17
+ const ROLE_RECORD_KEYS = new Set(["model", "select", "thinking", "promptRole", "systemPrompt"]);
18
+ function isModelShaped(s) {
19
+ try {
20
+ if (typeof s === "function" || isList(s)) {
21
+ Reflect.ownKeys(s);
22
+ return false;
23
+ }
24
+ if (Reflect.ownKeys(s).some((k) => typeof k !== "string" || !ROLE_RECORD_KEYS.has(k)))
25
+ return true;
26
+ return readMember(s, "id") !== undefined || readMember(s, "api") !== undefined;
27
+ }
28
+ catch {
29
+ return true;
30
+ }
31
+ }
16
32
  function isBindingObject(value) {
17
33
  return (typeof value === "object" && value !== null) || typeof value === "function";
18
34
  }
19
35
  function bindingShown(value) {
20
36
  if (typeof value === "string")
21
37
  return JSON.stringify(value.length > 80 ? `${value.slice(0, 80)}…` : value);
22
- return value === null ? "null" : Array.isArray(value) ? "a list" : `a ${typeof value}`;
38
+ return value === null ? "null" : isList(value) ? "a list" : `a ${typeof value}`;
39
+ }
40
+ function isList(value) {
41
+ try {
42
+ return Array.isArray(value);
43
+ }
44
+ catch {
45
+ return false;
46
+ }
23
47
  }
24
48
  function unknownModelRefRefusal(ref, seat) {
25
49
  return modelBindingRefusal(seat, "unknown_model_ref", `Unknown model ref ${bindingShown(ref)} in ${seat}. Provide it in RunnerDeps.models or pass a Model object.`);
@@ -30,6 +54,7 @@ function modelOfBinding(value, models, seat, binds) {
30
54
  const held = catalogModel(value, models);
31
55
  if (held === undefined)
32
56
  throw unknownModelRefRefusal(value, seat);
57
+ assertModelObject(held, seat, `${bindingShown(value)} (a catalog entry)`);
33
58
  return held;
34
59
  }
35
60
  return value;
@@ -37,9 +62,19 @@ function modelOfBinding(value, models, seat, binds) {
37
62
  function assertModelBindingShape(value, seat, binds) {
38
63
  if (typeof value === "string")
39
64
  return;
40
- if (typeof value !== "object" || value === null || Array.isArray(value)) {
65
+ if (typeof value !== "object" || value === null || isList(value)) {
41
66
  throw modelBindingRefusal(seat, "not_a_model_ref", `${seat} is ${bindingShown(value)}, not a model reference — ${binds}.`);
42
67
  }
68
+ assertModelObject(value, seat, "a Model object", binds === ROLE_BINDS ? " At a role seat an object with a key beyond model / select / thinking / promptRole / systemPrompt is read as a Model, not as a role record." : "");
69
+ }
70
+ function assertModelObject(value, seat, what, note = "") {
71
+ const id = readMember(value, "id");
72
+ const api = readMember(value, "api");
73
+ const [member, got] = typeof id !== "string" || id === "" ? ["id", id] : typeof api !== "string" ? ["api", api] : [undefined, undefined];
74
+ if (member === undefined)
75
+ return;
76
+ const shown = got === undefined ? "absent" : bindingShown(got);
77
+ throw modelBindingRefusal(seat, "not_a_model_ref", `${seat} is ${what} whose \`${member}\` is ${shown} — a Model carries a non-empty string \`id\` and a string \`api\`.${note}`);
43
78
  }
44
79
  const ROLE_BINDS = "a role binds a catalog model name or a Model object";
45
80
  const roleModelSeat = (named, seat) => (typeof named === "string" ? seat : `${seat}.model`);
@@ -158,7 +193,10 @@ function roleTierModel(role, models) {
158
193
  if (!models || !models[TIERS_ACTIVE])
159
194
  return undefined;
160
195
  const tier = ROLE_TIER_DEFAULTS[role];
161
- return tier !== undefined ? models[tier] : undefined;
196
+ const m = tier !== undefined ? models[tier] : undefined;
197
+ if (m)
198
+ assertModelObject(m, `tiers.${String(tier)}`, "the catalog entry the tier word names");
199
+ return m;
162
200
  }
163
201
  const FALLBACK = {
164
202
  default: [],
@@ -178,7 +216,7 @@ function specParts(s, seat = "a role seat") {
178
216
  if (!isBindingObject(s)) {
179
217
  throw modelBindingRefusal(seat, "not_a_model_ref", `${seat} is ${bindingShown(s)}, not a model reference — a role binds a catalog model name, a Model object or a { model, select, thinking, systemPrompt } record.`);
180
218
  }
181
- if ("id" in s && "api" in s) {
219
+ if (isModelShaped(s)) {
182
220
  return { model: s };
183
221
  }
184
222
  const cfg = s;
@@ -192,7 +230,7 @@ function toConfig(s) {
192
230
  if (s === undefined) {
193
231
  return undefined;
194
232
  }
195
- if (typeof s === "string" || ("id" in s && "api" in s)) {
233
+ if (typeof s === "string" || isModelShaped(s)) {
196
234
  return { model: s };
197
235
  }
198
236
  return s;
@@ -248,8 +286,13 @@ function partsModel(parts, models, seat = "a role seat") {
248
286
  if (needs != null && !Array.isArray(needs)) {
249
287
  throw modelBindingRefusal(`${seat}.select`, "not_a_model_ref", `${seat}.select.needs is ${bindingShown(needs)}, not a list of capabilities (vision / reasoning).`);
250
288
  }
251
- const held = models != null && typeof models === "object" ? Object.values(models).filter(Boolean) : [];
252
- return selectModel(held, parts.select);
289
+ const held = models != null && typeof models === "object" ? Object.entries(models).filter(([, m]) => Boolean(m)) : [];
290
+ for (const [key, m] of held)
291
+ assertModelObject(m, `${seat}.select`, `the catalog entry ${bindingShown(key)} the criteria read`);
292
+ const picked = selectModel(held.map(([, m]) => m), parts.select);
293
+ if (picked !== undefined)
294
+ assertModelObject(picked, `${seat}.select`, "the catalog entry the criteria pick");
295
+ return picked;
253
296
  }
254
297
  return undefined;
255
298
  }
@@ -0,0 +1,10 @@
1
+ import { type CompactSessionRequest, type CompactSessionResult } from "../auto-compaction.js";
2
+ import { type SessionStore } from "../session.js";
3
+ import type { RunnerDeps } from "../types.js";
4
+ import { type SessionLockSeats } from "./session-verb-doors.js";
5
+ /** What the Runner lends the compaction: its current deps, its session store, and its lock. */
6
+ export interface CompactSessionSeats extends SessionLockSeats {
7
+ deps: RunnerDeps;
8
+ sessions: SessionStore;
9
+ }
10
+ export declare function compactIdleSession(seats: CompactSessionSeats, request: CompactSessionRequest): Promise<CompactSessionResult>;
@@ -0,0 +1,202 @@
1
+ import { convertToLlm, uuidv7 } from "../../internal/harness.js";
2
+ import { compactionFailureReason, compactionPassReport, DEFAULT_COMPACTION_INSTRUCTIONS, isCompactionManualCancel, manualCompactOutcomeOf, maybeCompact, mintCompactedEvent } from "../auto-compaction.js";
3
+ import { contextOverlayFor } from "../context-settings.js";
4
+ import { admitLegMods, legFacadeSeats } from "../mods/stations.js";
5
+ import { malformedPricingField } from "../pricing.js";
6
+ import { resolveTaskModel } from "../roles.js";
7
+ import { StoredSession } from "../session.js";
8
+ import { emitTrace } from "../trace.js";
9
+ import { egressThrownText } from "../untrusted-egress.js";
10
+ import { emitInputTruncated, legCompactionSettings } from "./compaction-call-options.js";
11
+ import { compactionHookOptions, summaryProviderOptions } from "./compaction-seams.js";
12
+ import { compactionSummaryFallback, compactionSummaryIsDerived } from "./derived-route-fallback.js";
13
+ import { resolveCompactionModel } from "./prepare-config-doors.js";
14
+ import { compactionActivationSnapshot } from "./prepare-tool-disclosure-mount.js";
15
+ import { derivedLegBrain, sideCallBookOf } from "./side-call-account.js";
16
+ import { acquireExistingSession, claimIdleSession, readPlainRequest, shownValue } from "./session-verb-doors.js";
17
+ const VERB = "Runner.compactSession";
18
+ const REQUEST_KEYS = new Set(["sessionId", "model", "instructions", "compaction", "compactionModel", "getApiKeyAndHeaders", "signal"]);
19
+ const SETTINGS_KEYS = new Set(["enabled", "reserveTokens", "keepRecentTokens", "instructions", "clampTolerance"]);
20
+ export async function compactIdleSession(seats, request) {
21
+ const req = screenCompactSessionRequest(request);
22
+ const { deps } = seats;
23
+ const resolved = resolveTaskModel({ model: req.model }, deps);
24
+ const named = resolveCompactionModel(req.compactionModel !== undefined ? { compactionModel: req.compactionModel } : {}, deps, "compactSession.compactionModel");
25
+ if (req.signal?.aborted === true)
26
+ return { outcome: "mooted", events: [] };
27
+ const release = await claimIdleSession(seats, req.sessionId, () => sessionBusyRefusal(req.sessionId));
28
+ try {
29
+ const acquired = await acquireExistingSession(seats.sessions, req.sessionId, VERB, "nothing was compacted");
30
+ const legMods = await admitLegMods({ spec: {}, deps, sessionId: req.sessionId });
31
+ return await compactOnSession(req, deps, new StoredSession(acquired.session.getStorage()), resolved, named, legMods);
32
+ }
33
+ finally {
34
+ release();
35
+ }
36
+ }
37
+ async function compactOnSession(req, deps, session, resolved, named, legMods) {
38
+ const { sessionId } = req;
39
+ const events = [];
40
+ const ident = () => ({ eventId: uuidv7() });
41
+ const passReport = compactionPassReport({ push: (e) => events.push(e) }, ident);
42
+ const stats = { turns: 0, tokens: 0, toolCalls: 0, promptTokens: 0, totalInputTokens: 0, cachedTokens: 0, cacheWriteTokens: 0, cacheWriteTokensLong: 0, outputTokens: 0, costMicroUsd: 0 };
43
+ const telemetry = { unpricedSpend: false, tracer: deps.tracer, taskId: sessionId };
44
+ const accountBook = sideCallBookOf(stats, { telemetry, pricing: () => deps.pricing, noteUnevaluablePriceTable: (p) => void (malformedPricingField(p) !== undefined && (telemetry.unpricedSpend = true)), catalog: deps.models });
45
+ let calls = 0;
46
+ const book = (lane, model, reply) => {
47
+ calls += 1;
48
+ accountBook(lane, model, reply);
49
+ };
50
+ const answer = (outcome) => ({
51
+ outcome,
52
+ events,
53
+ ...(calls > 0
54
+ ? {
55
+ usage: {
56
+ tokens: stats.tokens,
57
+ totalInputTokens: stats.totalInputTokens,
58
+ promptTokens: stats.promptTokens,
59
+ cachedTokens: stats.cachedTokens,
60
+ cacheWriteTokens: stats.cacheWriteTokens,
61
+ outputTokens: stats.outputTokens,
62
+ costMicroUsd: stats.costMicroUsd,
63
+ ...(stats.usageMissing === true ? { usageMissing: true } : {}),
64
+ ...(telemetry.unpricedSpend ? { unpriced: true } : {}),
65
+ },
66
+ }
67
+ : {}),
68
+ });
69
+ const trace = deps.tracer;
70
+ try {
71
+ const compactionModel = compactionSummaryIsDerived(req.compactionModel !== undefined, named) ? await compactionSummaryFallback({ derived: named, primary: resolved.model, brain: deps.brain, getApiKeyAndHeaders: req.getApiKeyAndHeaders, onNotice: deps.onNotice, sessionId }) : named;
72
+ const branch = await session.getBranch();
73
+ const contextMessages = convertToLlm((await session.buildContext()).messages);
74
+ bindSessionFacade(legMods, { deps, req, model: resolved.model, messages: contextMessages, book });
75
+ const overlay = contextOverlayFor(deps.contextSettings, resolved.modelRef, deps);
76
+ const activeTools = compactionActivationSnapshot(branch);
77
+ const thinking = resolved.thinking ?? resolved.model.defaultThinking;
78
+ const comp = await maybeCompact({
79
+ session,
80
+ model: resolved.model,
81
+ ...(overlay !== undefined ? { contextOverlay: overlay } : {}),
82
+ ...(compactionModel !== undefined ? { compactionModel } : {}),
83
+ brain: derivedLegBrain(() => deps.brain, "compaction", book),
84
+ ...(req.getApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: req.getApiKeyAndHeaders } : {}),
85
+ ...(thinking !== undefined ? { thinking } : {}),
86
+ settings: legCompactionSettings(req.compaction !== undefined ? { compaction: req.compaction } : {}, overlay),
87
+ customInstructions: req.instructions ?? req.compaction?.instructions ?? DEFAULT_COMPACTION_INSTRUCTIONS,
88
+ ...(req.signal !== undefined ? { signal: req.signal, manualCancelSignal: req.signal } : {}),
89
+ minTokens: 0,
90
+ force: true,
91
+ onInputTruncated: emitInputTruncated(trace, sessionId),
92
+ ...(activeTools.length > 0 ? { activeTools } : {}),
93
+ ...summaryProviderOptions({ deps }, 0),
94
+ ...compactionHookOptions({ deps }, { modBus: legMods.bus, hookIdentity: undefined, sessionId }, "manual", req.signal),
95
+ onStarted: passReport.onStarted,
96
+ });
97
+ const outcome = manualCompactOutcomeOf(comp);
98
+ if (comp.compacted) {
99
+ events.push(mintCompactedEvent(comp, "manual", ident()));
100
+ return answer(outcome);
101
+ }
102
+ if (comp.blocked)
103
+ emitTrace(trace, () => ({ kind: "compaction.blocked", version: 1, taskId: sessionId, trigger: "manual", ts: Date.now() }));
104
+ else if (comp.disabled)
105
+ emitTrace(trace, () => ({ kind: "compaction.disabled", version: 1, taskId: sessionId, ts: Date.now() }));
106
+ else
107
+ emitTrace(trace, () => ({ kind: "compaction.noop", version: 1, taskId: sessionId, ts: Date.now() }));
108
+ events.push({ type: "compaction_outcome", outcome: outcome, trigger: "manual", ...ident() });
109
+ return answer(outcome);
110
+ }
111
+ catch (err) {
112
+ if (isCompactionManualCancel(err)) {
113
+ emitTrace(trace, () => ({ kind: "compaction.mooted", version: 1, taskId: sessionId, reason: "cancelled", ts: Date.now() }));
114
+ events.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason: "cancelled", ...passReport.end, ...ident() });
115
+ return answer("mooted");
116
+ }
117
+ deps.onError?.(asError(err), { phase: "compaction", sessionId });
118
+ const reason = compactionFailureReason(err);
119
+ emitTrace(trace, () => ({ kind: "compaction.failed", version: 1, taskId: sessionId, trigger: "manual", reason, ts: Date.now() }));
120
+ events.push({ type: "compaction_outcome", outcome: "failed", trigger: "manual", reason, ...passReport.end, ...ident() });
121
+ return answer("failed");
122
+ }
123
+ }
124
+ function bindSessionFacade(legMods, input) {
125
+ const unavailable = (method) => {
126
+ throw new Error(`${method}: not available on a session-level compaction (${VERB}) — there is no run, so no execution environment, working directory, tool roster or agent listing`);
127
+ };
128
+ const seats = legFacadeSeats({
129
+ deps: input.deps,
130
+ spec: input.req.getApiKeyAndHeaders !== undefined ? { getApiKeyAndHeaders: input.req.getApiKeyAndHeaders } : {},
131
+ sessionId: input.req.sessionId,
132
+ model: input.model,
133
+ executionEnv: { exec: () => unavailable("process.run") },
134
+ taskRoot: "",
135
+ cwdRef: undefined,
136
+ lastBrainContext: () => ({ messages: input.messages }),
137
+ activeTools: new Set(),
138
+ agentListing: undefined,
139
+ book: (answeredBy, reply) => input.book("mod", answeredBy, reply),
140
+ toolCall: () => unavailable("tool.call"),
141
+ });
142
+ legMods.bindFacade({
143
+ ...seats,
144
+ cwd: () => unavailable("session.cwd"),
145
+ root: () => unavailable("session.root"),
146
+ tools: () => unavailable("tool.list"),
147
+ agents: () => unavailable("agent.list"),
148
+ });
149
+ }
150
+ function asError(err) {
151
+ return err instanceof Error ? err : new Error(egressThrownText("same-principal", err));
152
+ }
153
+ function screenCompactSessionRequest(request) {
154
+ const bad = (what) => {
155
+ const e = new Error(`${VERB}: ${what} — nothing was compacted. The request is { sessionId, model, instructions?, compaction?, compactionModel?, getApiKeyAndHeaders?, signal? }.`);
156
+ e.code = "compact.invalid_request";
157
+ throw e;
158
+ };
159
+ const { sessionId, model, instructions, compaction, compactionModel, getApiKeyAndHeaders, signal } = readPlainRequest(request, REQUEST_KEYS, bad);
160
+ if (typeof sessionId !== "string" || sessionId === "")
161
+ return bad(`sessionId must be a non-empty string, got ${shownValue(sessionId)}`);
162
+ if (model === undefined)
163
+ return bad("model is required — the model the session's next turn runs on, whose window bounds the pass");
164
+ if (instructions !== undefined && typeof instructions !== "string")
165
+ return bad(`instructions must be a string when given, got ${shownValue(instructions)}`);
166
+ if (getApiKeyAndHeaders !== undefined && typeof getApiKeyAndHeaders !== "function")
167
+ return bad(`getApiKeyAndHeaders must be a function when given, got ${shownValue(getApiKeyAndHeaders)}`);
168
+ if (signal !== undefined && !(signal instanceof AbortSignal))
169
+ return bad(`signal must be an AbortSignal when given, got ${shownValue(signal)}`);
170
+ return {
171
+ sessionId,
172
+ model: model,
173
+ instructions,
174
+ compaction: compaction === undefined ? undefined : screenSettings(compaction, bad),
175
+ compactionModel: compactionModel,
176
+ getApiKeyAndHeaders: getApiKeyAndHeaders,
177
+ signal,
178
+ };
179
+ }
180
+ function screenSettings(raw, bad) {
181
+ const { enabled, reserveTokens, keepRecentTokens, instructions, clampTolerance } = readPlainRequest(raw, SETTINGS_KEYS, (what) => bad(`compaction: ${what}`));
182
+ if (enabled !== undefined && typeof enabled !== "boolean")
183
+ return bad(`compaction.enabled must be a boolean when given, got ${shownValue(enabled)}`);
184
+ for (const [name, v] of [["reserveTokens", reserveTokens], ["keepRecentTokens", keepRecentTokens], ["clampTolerance", clampTolerance]]) {
185
+ if (v !== undefined && typeof v !== "number")
186
+ return bad(`compaction.${name} must be a number when given, got ${shownValue(v)}`);
187
+ }
188
+ if (instructions !== undefined && typeof instructions !== "string")
189
+ return bad(`compaction.instructions must be a string when given, got ${shownValue(instructions)}`);
190
+ return {
191
+ ...(enabled !== undefined ? { enabled: enabled } : {}),
192
+ ...(reserveTokens !== undefined ? { reserveTokens: reserveTokens } : {}),
193
+ ...(keepRecentTokens !== undefined ? { keepRecentTokens: keepRecentTokens } : {}),
194
+ ...(instructions !== undefined ? { instructions: instructions } : {}),
195
+ ...(clampTolerance !== undefined ? { clampTolerance: clampTolerance } : {}),
196
+ };
197
+ }
198
+ function sessionBusyRefusal(sessionId) {
199
+ const e = new Error(`${VERB}: a task is running or queued on session "${sessionId}" in this Runner, or a write one of its tasks issued has not settled — compacting it now would rewrite the transcript under that work, so nothing was compacted; a running task takes the request through its own stream (TaskStream.compact), otherwise retry once it has ended and its writes have settled`);
200
+ e.code = "compact.session_busy";
201
+ return e;
202
+ }
@@ -156,7 +156,7 @@ export declare function emitInputTruncated(tracer: TracerHook | undefined, taskI
156
156
  */
157
157
  export declare function legContextOverlayOption(prepared: Prepared, runner: RunnerDepsSeat): Pick<MaybeCompactOptions, "contextOverlay">;
158
158
  /**
159
- * The compaction SETTINGS a pass runs under — the assembly all three lanes share.
159
+ * The compaction SETTINGS a pass runs under — the assembly all three lanes and the out-of-run `Runner.compactSession` share.
160
160
  *
161
161
  * `{ ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction }` was hand-copied at each lane, and it is where the
162
162
  * deployment's live `enabled` declaration belongs: `maybeCompact` has exactly one enablement gate (it reads
@@ -165,4 +165,4 @@ export declare function legContextOverlayOption(prepared: Prepared, runner: Runn
165
165
  * compaction runs" a single expression ({@link compactionEnabled}: the request authored it > the deployment
166
166
  * declared it > on).
167
167
  */
168
- export declare function legCompactionSettings(spec: TaskSpec, overlay: ContextWindowOverlay | undefined): CompactionSettings;
168
+ export declare function legCompactionSettings(spec: Pick<TaskSpec, "compaction">, overlay: ContextWindowOverlay | undefined): CompactionSettings;