blun-king-cli 9.1.589 → 9.1.595

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 (46) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/README.md +12 -0
  3. package/agent-spine-plugin/CHANGELOG.md +1 -0
  4. package/agent-spine-plugin/docs/host-integration.md +15 -1
  5. package/agent-spine-plugin/docs/preflight-recall.md +1 -1
  6. package/agent-spine-plugin/scripts/check-install-hook.js +6 -5
  7. package/agent-spine-plugin/scripts/check-install-king.js +37 -0
  8. package/agent-spine-plugin/scripts/check-install.js +6 -1
  9. package/agent-spine-plugin/scripts/release-check.js +3 -2
  10. package/agent-spine-plugin/src/cli-agent.js +20 -1
  11. package/agent-spine-plugin/src/cli.js +1 -0
  12. package/agent-spine-plugin/src/index.js +1 -1
  13. package/agent-spine-plugin/src/lib/delivery-command-actions.js +16 -7
  14. package/agent-spine-plugin/src/lib/gateway-control.js +69 -1
  15. package/agent-spine-plugin/src/lib/gateway-host-fencing.js +92 -0
  16. package/agent-spine-plugin/src/lib/gateway-host-lifecycle.js +9 -1
  17. package/agent-spine-plugin/src/lib/gateway-prepared-host.js +28 -0
  18. package/agent-spine-plugin/src/lib/gateway-runs.js +46 -10
  19. package/agent-spine-plugin/src/lib/gateway-runtime.js +1 -1
  20. package/agent-spine-plugin/src/lib/gateway-state.js +3 -1
  21. package/agent-spine-plugin/src/lib/hook-context.js +8 -3
  22. package/agent-spine-plugin/src/lib/hook-output.js +2 -3
  23. package/agent-spine-plugin/src/lib/hook-process-advisory.js +1 -2
  24. package/agent-spine-plugin/src/lib/host-instruction-budget.js +17 -0
  25. package/agent-spine-plugin/src/lib/preflight.js +5 -19
  26. package/agent-spine-plugin/src/lib/source-roots.js +3 -2
  27. package/agent-spine-plugin/src/worker.js +30 -12
  28. package/bin/agentspine-king-goal-inbox.mjs +127 -0
  29. package/bin/agentspine-king-goal-intake.mjs +106 -0
  30. package/bin/agentspine-king-host-runner.mjs +109 -0
  31. package/bin/agentspine-king-snapshot-policy.cjs +80 -0
  32. package/bin/agentspine-king-status-policy.mjs +226 -0
  33. package/bin/agentspine-king-worker-host.mjs +160 -0
  34. package/bin/core-bootstrap.js +20 -3
  35. package/bin/launcher-mode.js +38 -1
  36. package/bin/launcher-restart-policy.cjs +131 -0
  37. package/bin/launcher-runtime.js +48 -9
  38. package/bin/runtime-exit-ledger.cjs +144 -0
  39. package/bin/runtime-exit-ledger.d.cts +23 -0
  40. package/bin/windows-node-crash-dump.cjs +289 -0
  41. package/blun.mjs +83241 -74234
  42. package/bundled-agent-sources.json +109 -34
  43. package/codebase-index/codebase_index.py +470 -0
  44. package/package.json +6 -1
  45. package/telegram-plugin/dist/bridge.mjs +390 -4
  46. package/worker-host.mjs +348023 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,41 @@
1
1
  # BLUN King Changelog
2
2
 
3
+ ## 9.1.594
4
+
5
+ - Preserve private Telegram replies while group messages wait, and retain slash-command routing.
6
+ - Honor long Retry-After delays and restore remaining cooldowns after resuming a session.
7
+ - Continue one settled tool step after rejected narration without publishing that narration or weakening final-answer review.
8
+ - Restore bounded write-continuation recovery, shell timeouts, task controls and session recovery.
9
+ - Keep input, quota rows, activity, task expansion and source-labelled tool output visible during streaming and history navigation.
10
+ - Require complete console regression evidence before packaging and verify matching public update metadata.
11
+
12
+ ## 9.1.593
13
+
14
+ - Keep the input and both status rows visible after delayed startup messages, without clearing terminal scrollback.
15
+
16
+ ## 9.1.592
17
+
18
+ - Preserve explicit startup permissions and remember deliberate permission selections for new sessions.
19
+ - Prevent overlapping session selections from closing the session being resumed.
20
+ - Restore rotating activity words, cumulative usage, step number and local date/time alongside live timing and token estimates.
21
+ - Allow protected-source reads redirected to literal null sinks without removing protected-write checks.
22
+ - Include the bounded startup and reload corrections prepared in 9.1.591.
23
+
24
+ ## 9.1.591
25
+
26
+ - Finish the managed model refresh before opening a session, with a bounded catalog request.
27
+ - Reopen continued sessions through one reload so an overdue loop cannot race a second reload.
28
+ - Preserve additional workspace directories through the reload path.
29
+ - Keep the existing input, footer, quota rows, loop countdown and compaction display unchanged.
30
+
31
+ ## 9.1.590
32
+
33
+ - Retain complete bounded instructions without the extra 8 KiB AgentSpine rejection.
34
+ - Keep routine AgentSpine preparation out of the visible transcript while retaining source and receipt checks.
35
+ - Preserve the active King profile when unrelated host settings are inherited.
36
+ - Restore the loop countdown and replace the previous recurring loop when scheduling a new one.
37
+ - Retain the approved compaction display and queued reload behavior.
38
+
3
39
  ## 9.1.589
4
40
 
5
41
  - Bundle reviewed AgentSpine and Translate Native sources with verified update manifests.
package/README.md CHANGED
@@ -60,3 +60,15 @@ Sie stehen unter `blun` und `king` identisch zur Verfügung.
60
60
  denselben abgesicherten Updater. Er bleibt im installierten Stable- oder
61
61
  Next-Kanal, verhindert Rückstufungen und übergibt npm ausschließlich eine zuvor
62
62
  aufgelöste exakte Paketversion.
63
+
64
+ ## Absturzdiagnose unter Windows
65
+
66
+ Startfehler und unbehandelte Ausnahmen werden im begrenzten, bereinigten Protokoll `BLUN_HOME/diagnostics/runtime-exits.jsonl` erfasst. Die Diagnose startet keinen neuen Prozess und ändert beim Start keine Windows-Registrywerte.
67
+
68
+ Ausführliche Node-Berichte sind optional. Nur `BLUN_NODE_CRASH_REPORTS=1` aktiviert sie für den gestarteten Prozess. Die Berichte liegen unter `BLUN_HOME/diagnostics/crash-dumps`. Umgebungsvariablen und Netzwerkinformationen werden ausgeschlossen; Programmargumente, Dateipfade und weitere sensible Prozessdaten können trotzdem enthalten sein. Berichte vor einer Weitergabe prüfen.
69
+
70
+ Drei gemeinsam reservierte Plätze begrenzen die Anzahl dieser Node-Berichte, nicht deren Dateigröße. Ein ordentlich beendeter Prozess gibt seine Reservierung frei; ein späterer Prozess kann den Bericht dieses Platzes ersetzen. Aktive oder unklar verwaiste Reservierungen werden nicht automatisch entfernt. Sind alle Plätze belegt oder liegen alte Berichte mit Zufallskennung vor, bleiben neue Detailberichte aus, bis der Betreiber diese Dateien ausdrücklich geprüft und bereinigt hat. Der normale Start und das kleine Fehlerprotokoll bleiben davon unabhängig. Sitzungen und fremde Dateien werden nicht entfernt.
71
+
72
+ Native Windows-Dumps sind davon getrennt. Der mitgelieferte Helfer `bin/windows-node-crash-dump.cjs` bietet `--query-wer` für eine reine Abfrage. Eine Änderung erfordert ausdrücklich `--configure-wer --confirm-system-wide`, ein gesetztes `BLUN_HOME` und ausreichende Windows-Rechte. Sie betrifft alle Prozesse namens `node.exe`, nicht nur BLUN: drei Minidumps im angegebenen Diagnoseordner. Bei einem Teilfehler nennt die JSON-Ausgabe die bereits gesetzten Werte. Der normale Start ruft diesen Befehl niemals auf.
73
+
74
+ Node-Berichte erfassen unterstützte Node-Fehler; sie sind kein Beleg dafür, dass jeder native Windows-Abbruch erfasst wird. Die Funktion lässt den ursprünglichen Fehler und den vorgesehenen Prozessabbruch bestehen.
@@ -58,6 +58,7 @@ All notable changes to AgentSpine will be documented here. The project follows [
58
58
 
59
59
  ### Fixed
60
60
 
61
+ - King keeps complete instructions within the existing source-reader resource bounds, resolves its isolated profile first, and accounts for JSON escaping without adding an instruction-size gate. Routine hooks, lifecycle receipts and process advisories use quiet `additionalContext`; actual denials and exact source/receipt checks remain unchanged. Synthetic fresh/upgrade probes cover large and combined sources, escaping and byte preservation. Package allowance grows by 4 KiB unpacked for the helper and install checks; packed and file-count limits stay fixed. Live King/macOS acceptance remains separate.
61
62
  - The portal/thread lifecycle probe that starts a fresh process now runs outside the shared Windows I/O pool. Its existing assertions and process deadlines are unchanged.
62
63
  - The hermetic runner isolates the real host-origin source-attestation probe from concurrent Windows filesystem pressure, preserving its existing fail-closed result and timing boundaries without increasing a timeout.
63
64
  - Reuse host-verified, assignment-bound briefing observations in MCP delivery preparation without a second content fetch; keep missing proof advisory and document provider-specific history limits.
@@ -10,7 +10,7 @@ Provider evidence is not interchangeable. Each adapter separately binds its life
10
10
  |---|---|---|---|
11
11
  | Project instructions | `CLAUDE.md` hierarchy | `AGENTS.md` hierarchy | `AGENTS.md` through the isolated BLUN profile |
12
12
  | Hook package | Native `hooks/hooks.json` | Explicit `hooks/codex.json` | Manifest lifecycle hooks |
13
- | Verified hook briefing → delivery knowledge | Native `additionalContext` handoff receipt | Native `additionalContext` handoff receipt | Native bounded `message` handoff receipt; live acceptance open |
13
+ | Verified hook briefing → delivery knowledge | Native `additionalContext` handoff receipt | Native `additionalContext` handoff receipt | Bounded model-only `additionalContext` handoff receipt; live acceptance open |
14
14
  | Historical transcript enrollment | Bounded registered Claude source | Bounded Codex `sessions` source with verified headers | Protected King agent-wire mapping; live mapping open |
15
15
  | Cross-provider evidence handoff | Explicit opt-in; Claude provenance retained | Explicit opt-in; Codex provenance retained | Same contract; live launcher unverified |
16
16
  | Native permissions/trust | Host-owned | Host-owned | Host-owned; enforcement of returned block decisions remains externally unverified |
@@ -19,6 +19,20 @@ Provider evidence is not interchangeable. Each adapter separately binds its life
19
19
 
20
20
  Adapter acceptance uses bounded synthetic A/B sessions and covers restart, source mutation, foreign scope, private exclusion and service failure. Cross-provider recall still needs local opt-in plus `includePriorProviders: true` with `includePriorSessions: true`. Report repository, installed-host and model observations separately.
21
21
 
22
+ King preparation, lifecycle receipts and process advisories do not emit visible
23
+ `message` prompts. Required instructions remain byte-exact; `UserPromptSubmit`
24
+ uses model-only `additionalContext`. Other events also return that quiet field;
25
+ whether the host consumes it is a separate lifecycle contract. Actual deny/block
26
+ decisions retain their original error schema.
27
+ King uses the existing source-reader safety bounds of 4 MiB per document and
28
+ 8 MiB for the source set, not Codex's 32 KiB instruction cap. When both King
29
+ environment bindings are present, `BLUN_HOME` owns the profile even if an unrelated
30
+ `CODEX_HOME` is inherited. These are resource limits, not recommended file sizes.
31
+ Codex/King internal hook envelopes account for the serialized verified briefing,
32
+ including JSON escaping. King's emitted runtime context remains at most 1200
33
+ bytes; its native instruction loader separately supplies full instructions.
34
+ Claude's existing instruction and context limits remain unchanged.
35
+
22
36
  ## Claude Code
23
37
 
24
38
  | Component | Path | Purpose |
@@ -6,7 +6,7 @@ The receipt binds agent and optional persona, user, tenant, host, instruction ho
6
6
 
7
7
  ## Host instructions
8
8
 
9
- Claude Code uses its resolved user and project `CLAUDE.md` hierarchy. Codex uses the corresponding `AGENTS.override.md`/`AGENTS.md` hierarchy. A generic host must explicitly bind `instruction_host` to `claude` or `codex`; AgentSpine does not guess. The mandatory preflight section contains the complete bytes of every active instruction document. Its standard hard budget is 8 KiB. Claude instructions may use one explicit aggregate overflow up to 16 KiB; the mode, used bytes, overflow and hard limit are bound into the signed exact-turn receipt and revalidated before consumption. Codex and generic instruction hosts remain capped at 8 KiB. An unreadable, replaced, deleted, oversized, out-of-scope, or symlinked mandatory file blocks immediately with a visible bounded diagnostic instead of degrading to a descriptor or waiting silently.
9
+ Claude Code uses its resolved user and project `CLAUDE.md` hierarchy. Codex uses the corresponding `AGENTS.override.md`/`AGENTS.md` hierarchy. A generic host must explicitly bind `instruction_host` to `claude` or `codex`; AgentSpine does not guess. The mandatory preflight section contains the complete bytes of every active instruction document. The standard budget is 8 KiB, with aggregate overflow up to 16 KiB for Claude or 32 KiB for Codex. King uses its own profile and the existing source-reader resource limits: 4 MiB per source and 8 MiB total, not a recommended instruction size (see [host integration](host-integration.md)). The mode, used bytes, overflow and hard limit are bound into the signed exact-turn receipt and revalidated before consumption. An unreadable, replaced, deleted, resource-oversized, out-of-scope, or symlinked mandatory file blocks immediately with a visible bounded diagnostic instead of degrading to a descriptor or waiting silently.
10
10
 
11
11
  Claude Code's `InstructionsLoaded` lifecycle event is registered as an additional observability signal, while `UserPromptSubmit` remains the blocking and injection boundary. Codex uses its own manifest-selected hook set without that unsupported Claude-only event. The preflight does not rely on the model remembering to read a file or call a tool.
12
12
 
@@ -101,6 +101,7 @@ export async function invokeInstalledHook(pluginRoot, projectRoot, stateRoot, ho
101
101
  decision: protocol.decision || null,
102
102
  reason: protocol.reason || null,
103
103
  message: protocol.hookSpecificOutput?.message || null,
104
+ additionalContext: protocol.hookSpecificOutput?.additionalContext || null,
104
105
  ...protocolShape(protocol)
105
106
  });
106
107
  } catch (error) {
@@ -122,7 +123,7 @@ export async function invokeInstalledBlunPostWriteDigest(pluginRoot, projectRoot
122
123
  };
123
124
  const prompted = await invokeInstalledHook(pluginRoot, projectRoot, stateRoot, "blun", shared,
124
125
  { requireBriefing: false });
125
- const requirementId = prompted.message?.match(
126
+ const requirementId = prompted.additionalContext?.match(
126
127
  /Requirement: (premortem-requirement:[a-f0-9]{64}:[a-f0-9]{64})/
127
128
  )?.[1];
128
129
  if (!requirementId) throw new Error("installed BLUN prompt did not expose its premortem requirement");
@@ -163,12 +164,12 @@ export async function invokeInstalledBlunPostWriteDigest(pluginRoot, projectRoot
163
164
  const post = await invokeInstalledHook(pluginRoot, projectRoot, stateRoot, "blun", {
164
165
  ...shared, ...tool, hook_event_name: "PostToolUse", success: true, tool_response: { ok: true }
165
166
  }, { requireBriefing: false });
166
- const digest = post.message?.match(/Premortem latest write sha256 ([a-f0-9]{64})/)?.[1];
167
+ const digest = post.additionalContext?.match(/Premortem latest write sha256 ([a-f0-9]{64})/)?.[1];
167
168
  if (!digest || !exactKeys(post.protocolKeys, ["hookSpecificOutput"])
168
- || !exactKeys(post.hookSpecificKeys, ["hookEventName", "message"])) {
169
- throw new Error("installed BLUN PostToolUse did not return its latest-write digest through message");
169
+ || !exactKeys(post.hookSpecificKeys, ["additionalContext", "hookEventName"])) {
170
+ throw new Error("installed BLUN PostToolUse did not return its latest-write digest through model context");
170
171
  }
171
- return { contextField: "message", latestWriteDigest: digest };
172
+ return { contextField: "additionalContext", latestWriteDigest: digest };
172
173
  }
173
174
 
174
175
  function exactKeys(value, expected) {
@@ -0,0 +1,37 @@
1
+ import { mkdir, readFile, writeFile } from "node:fs/promises";
2
+ import { join } from "node:path";
3
+ import { invokeInstalledHook } from "./check-install-hook.js";
4
+
5
+ export async function invokeInstalledKingInstructions(pluginRoot, workspace) {
6
+ const results = [];
7
+ for (const [name, projectBytes, userBytes, fill] of [
8
+ ["large", 44000, 0, "x"], ["combined", 25000, 19000, "x"], ["escaped", 32768, 0, '"']
9
+ ]) {
10
+ const root = join(workspace, name, "project"), home = join(workspace, name, "home");
11
+ await mkdir(join(root, ".git"), { recursive: true });
12
+ await mkdir(home, { recursive: true });
13
+ const sources = new Map();
14
+ for (const [path, bytes] of [[join(root, "AGENTS.md"), projectBytes], [join(home, "AGENTS.md"), userBytes]]) {
15
+ if (!bytes) continue;
16
+ const header = "# Synthetic installed King instructions\n\n";
17
+ const content = Buffer.from(header + fill.repeat(bytes - Buffer.byteLength(header)));
18
+ await writeFile(path, content); sources.set(path, content);
19
+ }
20
+ const output = await invokeInstalledHook(pluginRoot, root, join(workspace, name, "state"), "blun", {
21
+ hook_event_name: "UserPromptSubmit", host: "codex", cwd: root,
22
+ session_id: `session:king-${name}`, event_id: `turn:king-${name}`,
23
+ prompt: "Inspect the complete synthetic instructions."
24
+ }, { requireBriefing: false, extraEnv: { BLUN_HOME: home, CODEX_HOME: home } });
25
+ const bytes = Buffer.byteLength(output.additionalContext || "");
26
+ if (output.decision || output.message || !bytes || bytes > 1200
27
+ || JSON.stringify(output.hookSpecificKeys) !== JSON.stringify(["additionalContext", "hookEventName"])) {
28
+ throw new Error(`installed King ${name} must accept full instructions through quiet bounded context: ${output.reason}`);
29
+ }
30
+ for (const [path, original] of sources) {
31
+ if (!(await readFile(path)).equals(original)) throw new Error("installed King changed source instructions");
32
+ }
33
+ results.push({ name, instructionBytes: projectBytes + userBytes, contextBytes: bytes,
34
+ contextField: "additionalContext", sourcePreserved: true });
35
+ }
36
+ return results;
37
+ }
@@ -12,6 +12,7 @@ import {
12
12
  invokeInstalledBlockingProtocols, invokeInstalledBlunPostWriteDigest, invokeInstalledHook
13
13
  } from "./check-install-hook.js";
14
14
  import { invokeInstalledSelfstarter as runInstalledSelfstarter } from "./check-install-selfstarter.js";
15
+ import { invokeInstalledKingInstructions } from "./check-install-king.js";
15
16
 
16
17
  function hash(value) {
17
18
  return createHash("sha256").update(value).digest("hex");
@@ -364,13 +365,15 @@ export async function checkInstall(root = process.cwd()) {
364
365
  await mkdir(userProject, { recursive: true });
365
366
  await writeFile(source, "# Existing soul\n\nNever modify me.\n", "utf8");
366
367
  await writeFile(join(userProject, "CLAUDE.md"), "# Installed Claude rules\n\nLoad this before every answer.\n", "utf8");
367
- await writeFile(join(userProject, "AGENTS.md"), "# Installed Codex rules\n\nLoad this before every answer.\n", "utf8");
368
+ const largeCodexRules = `# Installed Codex rules\n\n${"x".repeat(17_590 - 25)}`;
369
+ await writeFile(join(userProject, "AGENTS.md"), largeCodexRules, "utf8");
368
370
  const protectedInstallSources = [source, join(userProject, "CLAUDE.md"), join(userProject, "AGENTS.md")];
369
371
  const sourceHashes = new Map(await Promise.all(protectedInstallSources.map(async (path) => [path, hash(await readFile(path))])));
370
372
 
371
373
  const fresh = join(workspace, "fresh", "agent-spine");
372
374
  await copyBundle(root, fresh);
373
375
  const freshResult = await checkHosts(fresh);
376
+ const freshKing = await invokeInstalledKingInstructions(fresh, join(workspace, "king-fresh"));
374
377
  const aliasRoot = join(workspace, "fresh-alias");
375
378
  await symlink(join(workspace, "fresh"), aliasRoot, process.platform === "win32" ? "junction" : "dir");
376
379
  const aliasResult = await checkHosts(join(aliasRoot, "agent-spine"));
@@ -410,6 +413,7 @@ export async function checkInstall(root = process.cwd()) {
410
413
  await removeTree(installed);
411
414
  await rename(staging, installed);
412
415
  const upgraded = await checkHosts(installed);
416
+ const upgradedKing = await invokeInstalledKingInstructions(installed, join(workspace, "king-upgrade"));
413
417
  const upgradeState = join(workspace, "state-upgrade");
414
418
  const upgradedHook = await invokeInstalledHook(installed, userProject, upgradeState, "codex");
415
419
  const upgradedBlockingProtocols = await invokeInstalledBlockingProtocols(
@@ -439,6 +443,7 @@ export async function checkInstall(root = process.cwd()) {
439
443
  automaticBriefing: { fresh: freshHook, upgrade: upgradedHook },
440
444
  blockingProtocols: { fresh: freshBlockingProtocols, upgrade: upgradedBlockingProtocols },
441
445
  blunPostWriteDigest: { fresh: freshBlunPost, upgrade: upgradedBlunPost },
446
+ kingInstructions: { fresh: freshKing, upgrade: upgradedKing },
442
447
  automaticAttention: { fresh: freshAttention, upgrade: upgradedAttention },
443
448
  automaticSelfstarter: { fresh: freshSelfstarter, upgrade: upgradedSelfstarter },
444
449
  automaticChannelWake: { fresh: freshChannelWake, upgrade: upgradedChannelWake },
@@ -8,7 +8,8 @@ const SEMVER_RE = /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)(?:-[0-9A-Za-z.-]+)?
8
8
  // Includes the bounded user-feedback candidate path; this is not semantic acceptance.
9
9
  // Measured code growth before documentation: 1874 packed / 7830 unpacked bytes.
10
10
  const MAX_PACKED_BYTES = 544 * 1024;
11
- const MAX_UNPACKED_BYTES = 2430 * 1024;
11
+ // King instruction/handoff fix and copied-install probes add about 4 KiB unpacked.
12
+ const MAX_UNPACKED_BYTES = 2434 * 1024;
12
13
  const REQUIRED_PACKAGE_FILES = [
13
14
  "blun.plugin.json", ".claude-plugin/plugin.json", ".codex-plugin/plugin.json", ".mcp.json",
14
15
  "CHANGELOG.md", "LICENSE", "README.md", "bin/agentspine.js", "bin/agentspine-mcp.js",
@@ -78,7 +79,7 @@ function validatePackageReport(report, version) {
78
79
  assert(typeof item.integrity === "string" && item.integrity.startsWith("sha512-"), "npm pack did not report SHA-512 integrity");
79
80
  assert(Number.isInteger(item.entryCount) && item.entryCount > 0 && item.entryCount <= 500, "npm package file count is outside the release limit");
80
81
  assert(Number.isInteger(item.size) && item.size > 0 && item.size <= MAX_PACKED_BYTES, "npm package exceeds the 544 KiB packed release limit");
81
- assert(Number.isInteger(item.unpackedSize) && item.unpackedSize > 0 && item.unpackedSize <= MAX_UNPACKED_BYTES, "npm package exceeds the 2430 KiB unpacked release limit");
82
+ assert(Number.isInteger(item.unpackedSize) && item.unpackedSize > 0 && item.unpackedSize <= MAX_UNPACKED_BYTES, "npm package exceeds the 2434 KiB unpacked release limit");
82
83
  const paths = (item.files || []).map((file) => file.path);
83
84
  assert(paths.length === new Set(paths).size, "npm package contains duplicate paths");
84
85
  for (const path of REQUIRED_PACKAGE_FILES) assert(paths.includes(path), `npm package is missing required file: ${path}`);
@@ -2,7 +2,7 @@ import { checkDelegation, createTask, deleteTask, grantDelegation, loadDelegatio
2
2
  import { cancelJob, deleteJob, grantExecution, loadExecutionPolicy, registerJob, revokeExecution, selfstarterContext } from "./lib/selfstarter.js";
3
3
  import { channelRuntimeContext, grantChannelBinding, loadChannelPolicy, revokeChannelBinding } from "./lib/channel-runtime.js";
4
4
  import { personaContext, syncPersonaRosterFromEnvironment } from "./lib/persona-runtime.js";
5
- import { assignGoal, gatewayContext, resolveGoalKnowledgeGap, setGatewayControl } from "./lib/gateway-runtime.js";
5
+ import { assignGoal, gatewayContext, resolveGoalKnowledgeGap, reviewLegacyGatewayHold, setGatewayControl } from "./lib/gateway-runtime.js";
6
6
  import {
7
7
  currentHostTranscriptReceipt, enrollPrivateSessionTimeline, LOCAL_TIMELINE_ENROLLMENT_CONFIRMATION,
8
8
  LOCAL_TIMELINE_ENROLLMENT_RECOVERY_CONFIRMATION, resetPrivateSessionTimelineEnrollment
@@ -35,6 +35,7 @@ export const agentCommands = new Set([
35
35
  "goal-assign",
36
36
  "goal-clarify",
37
37
  "gateway-control",
38
+ "gateway-review-legacy-hold",
38
39
  "gateway-status",
39
40
  "timeline-receipt",
40
41
  "timeline-enroll",
@@ -293,4 +294,22 @@ export async function runAgentCommand({ command, flags, positional, json }) {
293
294
  if (command === "gateway-status") {
294
295
  return output(await gatewayContext({ root: positional[0] || process.cwd(), agentId: flags.agent || null }), json);
295
296
  }
297
+
298
+ if (command === "gateway-review-legacy-hold") {
299
+ const required = ["root", "goal", "owner", "project", "worker", "claimed-at", "attempt", "review-note"];
300
+ const allowed = new Set([...required, "group", "host-stopped", "effects-reviewed", "confirm-local-review", "json"]);
301
+ if (positional.length !== 1 || Object.keys(flags).some((name) => !allowed.has(name))) {
302
+ throw new Error("legacy hold review accepts one queue ID and only its documented exact review flags");
303
+ }
304
+ if (required.some((name) => typeof flags[name] !== "string" || !flags[name])) {
305
+ throw new Error("legacy hold review requires explicit root, goal, owner, project, worker, claimed-at, attempt and review-note");
306
+ }
307
+ return output(await reviewLegacyGatewayHold({
308
+ root: flags.root, queueId: positional[0], goalId: flags.goal, ownerSubjectId: flags.owner,
309
+ projectId: flags.project, groupId: flags.group ?? null, workerId: flags.worker,
310
+ claimedAt: flags["claimed-at"], attempt: Number(flags.attempt), reviewNote: flags["review-note"],
311
+ hostStopped: booleanFlag(flags["host-stopped"]), effectsReviewed: booleanFlag(flags["effects-reviewed"]),
312
+ confirmation: booleanFlag(flags["confirm-local-review"]) ? "local-owner-confirmed" : null
313
+ }), json);
314
+ }
296
315
  }
@@ -115,6 +115,7 @@ Usage:
115
115
  agentspine goal-clarify <goal-id> --gap id --answer text [--source owner-input|objective-observation] [--source-digest sha256] --confirm-local-goal
116
116
  agentspine gateway-control [root] [--enabled true|false] [--kill-switch true|false] --confirm-local-gateway
117
117
  agentspine gateway-status [root] [--agent id]
118
+ agentspine gateway-review-legacy-hold <queue-id> --root path --goal id --owner id --project id [--group id] --worker id --claimed-at ISO --attempt n --review-note text --host-stopped --effects-reviewed --confirm-local-review
118
119
  agentspine share-keygen <signer-id> [--public-out signer.json] [--rotate] [--confirm-local-share]
119
120
  agentspine share-signers [root]
120
121
  agentspine share-trust <signer.json> [--root path] [--confirm-local-share]
@@ -48,7 +48,7 @@ export {
48
48
  export {
49
49
  EXECUTION_ATTEMPT_SCHEMA, GATEWAY_EVENT_SCHEMA, GATEWAY_POLICY_SCHEMA, GATEWAY_RUNTIME_SCHEMA,
50
50
  GOAL_PLAN_SCHEMA,
51
- assignGoal, claimGatewayWork, completeGatewayRun, deliverPrepared, failGatewayRun, markGatewayHostStarted,
51
+ assignGoal, claimGatewayWork, completeGatewayRun, deliverPrepared, failGatewayRun, markGatewayHostStarted, reviewLegacyGatewayHold,
52
52
  enqueueGatewayWake, executionAttemptForStep, gatewayContext, gatewayRuntimeFindings,
53
53
  inspectGatewayRuntime, loadGatewayRuntime, reconcileGateway, resolveGoalKnowledgeGap, setGatewayControl
54
54
  } from "./lib/gateway-runtime.js";
@@ -298,7 +298,7 @@ function segmentWrites(segment) {
298
298
  if (executable === "dd") return tokens.slice(1).some((token) => /^of=/.test(token.value));
299
299
  if (new Set(["bash", "sh", "zsh", "pwsh", "powershell"]).has(executable)
300
300
  && new Set(["-c", "-command"]).has(tokens[1]?.value.toLowerCase()) && tokens[2]) {
301
- return commandWriteActions(tokens[2].value).length > 0;
301
+ return commandWriteActions(tokens[2].value, 0, executable === "pwsh" || executable === "powershell").length > 0;
302
302
  }
303
303
  if (executable === "eval" && tokens[1]) {
304
304
  return commandWriteActions(tokens.slice(1).map((token) => token.value).join(" ")).length > 0;
@@ -378,7 +378,7 @@ function structuredComparisonIndexes(command) {
378
378
  return indexes;
379
379
  }
380
380
 
381
- function redirectionActions(command) {
381
+ function redirectionActions(command, powershell = false) {
382
382
  const actions = [];
383
383
  const comparisons = structuredComparisonIndexes(command);
384
384
  let quote = null;
@@ -405,26 +405,32 @@ function redirectionActions(command) {
405
405
  if (next === ">") index += 1;
406
406
  let target = index + 1;
407
407
  while (/\s/.test(command[target] || "")) target += 1;
408
+ const tail = shellSegments(command.slice(target))[0]?.text || "";
409
+ const sink = shellTokens(tail)[0];
410
+ const nullDevice = ((!powershell || process.platform !== "win32") && sink?.value === "/dev/null")
411
+ || (process.platform === "win32" && /^nul$/i.test(sink?.value || ""));
412
+ const nullVariable = powershell && sink?.plain && /^\$null$/i.test(sink.value);
413
+ if (nullDevice || nullVariable) continue;
408
414
  if (!["&", "="].includes(command[target])) actions.push({ kind: "write", family: null, index });
409
415
  }
410
416
  return actions;
411
417
  }
412
418
 
413
- function commandWriteActions(command, depth = 0) {
419
+ function commandWriteActions(command, depth = 0, powershell = false) {
414
420
  const heredocs = maskHeredocBodies(command);
415
421
  const analyzable = heredocs.command;
416
422
  const actions = shellSegments(analyzable).filter(segmentWrites)
417
423
  .map((segment) => ({ kind: "write", family: null, index: segment.index }));
418
- actions.push(...redirectionActions(analyzable));
424
+ actions.push(...redirectionActions(analyzable, powershell));
419
425
  if (depth < MAX_SUBSTITUTION_DEPTH) {
420
426
  for (const heredoc of heredocs.bodies) {
421
427
  const executable = shellExecutableName(executableTokens(heredoc.opener)[0]?.value);
422
428
  if (!SHELL_STDIN_EXECUTORS.has(executable)) continue;
423
- actions.push(...commandWriteActions(heredoc.text, depth + 1)
429
+ actions.push(...commandWriteActions(heredoc.text, depth + 1, false)
424
430
  .map((action) => ({ ...action, index: heredoc.index + action.index })));
425
431
  }
426
432
  for (const substitution of shellSubstitutions(analyzable, depth)) {
427
- actions.push(...commandWriteActions(substitution.text, depth + 1)
433
+ actions.push(...commandWriteActions(substitution.text, depth + 1, powershell)
428
434
  .map((action) => ({ ...action, index: substitution.index + action.index })));
429
435
  }
430
436
  }
@@ -446,7 +452,10 @@ export function deliveryToolActions(input) {
446
452
  if (!SHELL_TOOLS.test(toolName)) return [];
447
453
  const command = commandFromInput(input);
448
454
  if (command.length > MAX_COMMAND_CHARS) return [];
449
- const actions = commandWriteActions(command);
455
+ const toolInput = input?.tool_input ?? input?.tool_args;
456
+ const powershell = /(^|__)(powershell|pwsh)(_|$)/i.test(toolName)
457
+ || /(?:^|[\\/])(?:powershell|pwsh)(?:\.exe)?$/i.test(toolInput?.shell || "");
458
+ const actions = commandWriteActions(command, 0, powershell);
450
459
  const test = trailingTestAction(command);
451
460
  if (test) actions.push(test);
452
461
  return actions.sort((left, right) => left.index - right.index || left.kind.localeCompare(right.kind));
@@ -2,6 +2,7 @@ import { attentionFindings, loadAttention } from "./attention.js";
2
2
  import { channelRuntimeFindings, loadChannelPolicy, loadChannelRuntime } from "./channel-runtime.js";
3
3
  import { loadGraph } from "./graph.js";
4
4
  import { expireGatewayLane } from "./gateway-host-lifecycle.js";
5
+ import { heldExecution } from "./gateway-host-fencing.js";
5
6
  import * as goalPremortem from "./gateway-premortem.js";
6
7
  import { resolveKnowledgeGapCandidate, sameKnowledgeGapResolution } from "./knowledge-evidence.js";
7
8
  import { loadPersonaRuntime, personaRuntimeFindings } from "./persona-runtime.js";
@@ -33,6 +34,71 @@ export async function setGatewayControl({ root = process.cwd(), enabled, killSwi
33
34
  });
34
35
  }
35
36
 
37
+ export async function reviewLegacyGatewayHold({ root = process.cwd(), queueId, goalId, ownerSubjectId,
38
+ projectId, groupId = null, workerId, claimedAt, attempt, hostStopped, effectsReviewed, reviewNote,
39
+ confirmation, now = new Date() }) {
40
+ if (confirmation !== CONFIRMATION || hostStopped !== true || effectsReviewed !== true) {
41
+ throw new Error("legacy hold review requires explicit local owner confirmation of host stop and reviewed effects");
42
+ }
43
+ queueId = exactId(queueId, "queueId"); goalId = exactId(goalId, "goalId");
44
+ ownerSubjectId = exactId(ownerSubjectId, "ownerSubjectId"); workerId = exactId(workerId, "workerId");
45
+ projectId = exactId(projectId, "projectId"); groupId = exactId(groupId, "groupId", true);
46
+ if (typeof claimedAt !== "string" || timestamp(claimedAt) !== claimedAt
47
+ || !Number.isInteger(attempt) || attempt < 1 || attempt > 20) {
48
+ throw new Error("legacy hold review requires the exact claim timestamp and integer attempt");
49
+ }
50
+ if (typeof reviewNote !== "string" || reviewNote.length > 500) throw new Error("legacy review note must contain 1-500 characters");
51
+ reviewNote = safeText(reviewNote, "reviewNote", 500);
52
+ const current = timestamp(now);
53
+ const paths = await pathsFor(root);
54
+ return withLock(paths, async () => {
55
+ const policy = await readJson(paths.gatewayPolicyPath, paths.catalog.root, normalizePolicy, emptyPolicy);
56
+ const runtime = await readJson(paths.gatewayRuntimePath, paths.catalog.root, normalizeRuntime, emptyRuntime);
57
+ const item = runtime.queue.find((entry) => entry.queueId === queueId);
58
+ const goal = policy.goals.find((entry) => entry.goalId === goalId);
59
+ const step = goal?.plan?.steps.find((entry) => entry.stepId === item?.goalStepId);
60
+ if (!item || !goal || item.goalId !== goalId || goal.ownerSubjectId !== ownerSubjectId
61
+ || item.projectId !== projectId || goal.projectId !== projectId
62
+ || item.groupId !== groupId || goal.groupId !== groupId
63
+ || (item.goalStepId && !step) || item.agentId !== planStepAgentId(goal, step)) {
64
+ throw new Error("legacy hold review requires the exact goal owner and queue scope");
65
+ }
66
+ const hold = item.executionHold;
67
+ if (!hold || hold.workerId !== workerId || hold.claimedAt !== claimedAt || hold.attempt !== attempt) {
68
+ throw new Error("legacy hold review requires the exact held generation");
69
+ }
70
+ if (hold.hostSessionId !== null || runtime.history.some((entry) => entry.kind === "queue"
71
+ && entry.objectId === queueId && entry.value.attempts === attempt
72
+ && entry.value.executionHold?.hostSessionId)) {
73
+ throw new Error("a known host session requires exact-session quiescence, not legacy owner review");
74
+ }
75
+ if (item.lease !== null || !["blocked", "cancelled"].includes(item.status)) {
76
+ throw new Error("legacy owner review requires an unleased blocked or cancelled queue item");
77
+ }
78
+ const details = { goalId, ownerSubjectId, projectId, groupId, workerId, claimedAt, attempt,
79
+ hostStopped: true, effectsReviewed: true, reviewNote, reviewBasis: "local-owner-attestation" };
80
+ if (hold.status === "settled") {
81
+ const receipt = runtime.receipts.find((entry) => entry.kind === "legacy-host-owner-reviewed"
82
+ && entry.objectId === queueId && entry.details.workerId === workerId
83
+ && entry.details.claimedAt === claimedAt && entry.details.attempt === attempt);
84
+ if (!receipt || JSON.stringify(receipt.details) !== JSON.stringify(details)) {
85
+ throw new Error("legacy hold already has a different settlement or owner review");
86
+ }
87
+ return { item: structuredClone(item), receipt: structuredClone(receipt), duplicate: true };
88
+ }
89
+ if (current < item.updatedAt || current < claimedAt || (hold.hostStartedAt && current < hold.hostStartedAt)) {
90
+ throw new Error("legacy review timestamp precedes the held generation");
91
+ }
92
+ // This attestation is not native quiescence, goal completion, or authority to resume work.
93
+ preserve(runtime, "queue", item, "legacy-host-owner-reviewed", current);
94
+ hold.status = "settled"; hold.settledAt = current;
95
+ item.updatedAt = current; runtime.revision += 1;
96
+ const receipt = appendReceipt(runtime, "legacy-host-owner-reviewed", queueId, current, details);
97
+ await writeJson(paths.gatewayRuntimePath, runtime);
98
+ return { item: structuredClone(item), receipt: structuredClone(receipt), duplicate: false };
99
+ });
100
+ }
101
+
36
102
  export async function assignGoal({ root = process.cwd(), goalId, agentId, ownerSubjectId, projectId, groupId = null,
37
103
  priority = 70, successCriterion, nextSafeStep = null, steps = null, deadline = null, confirmation, now = new Date() }) {
38
104
  if (confirmation !== CONFIRMATION) throw new Error("goal assignment requires explicit local owner confirmation");
@@ -76,6 +142,9 @@ export async function assignGoal({ root = process.cwd(), goalId, agentId, ownerS
76
142
  if (previous?.plan && previous.plan.definitionsDigest !== goal.plan?.definitionsDigest) {
77
143
  throw new Error("goal plan definitions are immutable; assign a new goal ID");
78
144
  }
145
+ if (previous && runtime.queue.some((item) => item.goalId === goalId && heldExecution(item))) {
146
+ return { goal: structuredClone(previous), gatewayPolicyPath: paths.gatewayPolicyPath, duplicate: true, held: true };
147
+ }
79
148
  if (previous?.plan && previous.status === "blocked") {
80
149
  const blockedStep = currentPlanStep(previous);
81
150
  if (!blockedStep || blockedStep.status !== "blocked") throw new Error("blocked goal plan has no bound blocked step");
@@ -299,4 +368,3 @@ export async function reconcileGateway({ root = process.cwd(), now = new Date()
299
368
  return { policy, runtime, recovered: true };
300
369
  });
301
370
  }
302
-
@@ -0,0 +1,92 @@
1
+ import { ID_RE, safeCheckpoint, sha256 } from "./gateway-common.js";
2
+
3
+ const SCHEMA = "agentspine.host-execution-hold/v1";
4
+ const validTime = (value) => typeof value === "string" && Number.isFinite(new Date(value).getTime());
5
+
6
+ export function heldExecution(item) {
7
+ return item?.executionHold?.status === "held" ? item.executionHold : null;
8
+ }
9
+
10
+ export function newExecutionHold(item, resources = null) {
11
+ return { schema: SCHEMA, status: "held", workerId: item.lease.workerId,
12
+ claimedAt: item.lease.claimedAt, attempt: item.attempts,
13
+ hostSessionId: null, hostStartedAt: item.lease.hostStartedAt ?? null,
14
+ resources, settledAt: null, checkpointDigest: null, authority: "execution-state-only" };
15
+ }
16
+
17
+ export function validExecutionHold(item) {
18
+ const hold = item.executionHold;
19
+ if (hold === undefined) return true;
20
+ return hold && hold.schema === SCHEMA && ["held", "settled"].includes(hold.status)
21
+ && ID_RE.test(hold.workerId || "") && validTime(hold.claimedAt)
22
+ && Number.isInteger(hold.attempt) && hold.attempt > 0 && hold.attempt === item.attempts
23
+ && (hold.hostSessionId === null || ID_RE.test(hold.hostSessionId || ""))
24
+ && (hold.hostStartedAt === null || validTime(hold.hostStartedAt))
25
+ && (hold.resources === null || (Array.isArray(hold.resources) && hold.resources.length <= 16
26
+ && new Set(hold.resources).size === hold.resources.length && hold.resources.every((resource) => ID_RE.test(resource || ""))))
27
+ && (hold.status === "held" ? hold.settledAt === null && hold.checkpointDigest === null
28
+ : validTime(hold.settledAt) && (hold.checkpointDigest === null || /^[a-f0-9]{64}$/.test(hold.checkpointDigest)))
29
+ && (!item.lease || (hold.status === "held" && hold.workerId === item.lease.workerId
30
+ && hold.claimedAt === item.lease.claimedAt))
31
+ && hold.authority === "execution-state-only";
32
+ }
33
+
34
+ // Old ambiguous records have no resource snapshot. Conservatively retain the whole scope.
35
+ export function retainLegacyExecutionHolds(runtime) {
36
+ for (const item of runtime.queue) {
37
+ if (item.executionHold !== undefined) continue;
38
+ let original = item.lease ? item : null;
39
+ if (!original && ["blocked", "cancelled"].includes(item.status)) {
40
+ original = runtime.history.findLast((entry) => entry.kind === "queue"
41
+ && entry.objectId === item.queueId && entry.value.attempts === item.attempts
42
+ && entry.value.lease && entry.value.lease.executionMode !== "read-only")?.value;
43
+ }
44
+ if (original?.lease && original.lease.executionMode !== "read-only") {
45
+ item.executionHold = newExecutionHold(original);
46
+ }
47
+ }
48
+ }
49
+
50
+ export function executionBlocks(heldItem, candidate, resources) {
51
+ const hold = heldExecution(heldItem);
52
+ if (!hold || heldItem.queueId === candidate.queueId) return false;
53
+ if (heldItem.agentId === candidate.agentId) return true;
54
+ return heldItem.projectId === candidate.projectId && heldItem.groupId === candidate.groupId
55
+ && (hold.resources === null || resources.some((resource) => hold.resources.includes(resource)));
56
+ }
57
+
58
+ export function exactHeldGeneration(item, { workerId, claimedAt, attempt }) {
59
+ const hold = heldExecution(item);
60
+ return Boolean(hold && hold.workerId === workerId && hold.claimedAt === claimedAt && hold.attempt === attempt);
61
+ }
62
+
63
+ export function requireHostQuiescence(item, receipt, result, current) {
64
+ const hold = heldExecution(item);
65
+ if (!hold?.hostSessionId) return null;
66
+ const checkedAt = receipt?.checkedAt;
67
+ const age = new Date(current).getTime() - checkedAt;
68
+ if (!receipt || receipt.sessionId !== hold.hostSessionId || receipt.admissionClosed !== true
69
+ || receipt.status !== "quiescent" || !Array.isArray(receipt.remaining) || receipt.remaining.length !== 0
70
+ || !Number.isFinite(checkedAt) || age < 0 || age > 30000
71
+ || checkedAt < new Date(hold.hostStartedAt).getTime()) {
72
+ throw new Error("host settlement requires fresh exact-session quiescence with no remaining work");
73
+ }
74
+ if (item.goalId && (result?.checkpoint === undefined || result.checkpoint === null)) {
75
+ throw new Error("host settlement requires a non-null goal checkpoint");
76
+ }
77
+ return { sessionId: hold.hostSessionId, checkedAt,
78
+ checkpointDigest: item.goalId ? sha256(JSON.stringify(safeCheckpoint(result.checkpoint))) : null };
79
+ }
80
+
81
+ export function settleHostExecution(runtime, item, proof, current, appendReceipt) {
82
+ const hold = heldExecution(item);
83
+ if (!hold) return null;
84
+ hold.status = "settled";
85
+ hold.settledAt = current;
86
+ hold.checkpointDigest = proof?.checkpointDigest ?? null;
87
+ return appendReceipt(runtime, "host-execution-settled", item.queueId, current, {
88
+ workerId: hold.workerId, claimedAt: hold.claimedAt, attempt: hold.attempt,
89
+ sessionId: hold.hostSessionId, checkedAt: proof?.checkedAt ?? null,
90
+ checkpointDigest: hold.checkpointDigest, legacy: hold.hostSessionId === null
91
+ });
92
+ }
@@ -1,3 +1,5 @@
1
+ import { exactHeldGeneration, heldExecution } from "./gateway-host-fencing.js";
2
+
1
3
  export const AMBIGUOUS_HOST_OUTCOME =
2
4
  "Host execution may have produced an effect before restart; manual owner review is required.";
3
5
 
@@ -45,7 +47,13 @@ export function exactGatewayLeaseGeneration(claimedAt, attempt, action) {
45
47
  return new Date(claimedAt).toISOString();
46
48
  }
47
49
 
48
- export function requireExactGatewayLease(runtime, item, { workerId, claimedAt, attempt, current, action }) {
50
+ export function requireExactGatewayLease(runtime, item, { workerId, claimedAt, attempt, current, action, allowHeld = false }) {
51
+ if (allowHeld && heldExecution(item)?.hostSessionId
52
+ && exactHeldGeneration(item, { workerId, claimedAt, attempt })) {
53
+ const lane = runtime.lanes.find((entry) => entry.queueId === item.queueId && entry.workerId === workerId
54
+ && entry.claimedAt === claimedAt && ["leased", "expired"].includes(entry.status));
55
+ if (lane) return lane;
56
+ }
49
57
  if (!item || item.status !== "leased" || item.attempts !== attempt
50
58
  || item.lease?.workerId !== workerId || item.lease.claimedAt !== claimedAt
51
59
  || new Date(item.lease.expiresAt) <= new Date(current)) {
@@ -0,0 +1,28 @@
1
+ import { markGatewayHostStarted } from "./gateway-runs.js";
2
+
3
+ export async function executePreparedHost({ root, item, hostFactory, env, clock }) {
4
+ const host = await hostFactory(item, { env });
5
+ if (!host || typeof host.sessionId !== "string" || typeof host.run !== "function"
6
+ || typeof host.quiesce !== "function") throw new Error("prepared host requires an exact session, run and quiesce");
7
+ try {
8
+ await markGatewayHostStarted({ root, queueId: item.queueId, workerId: item.lease.workerId,
9
+ claimedAt: item.lease.claimedAt, attempt: item.attempts, hostSessionId: host.sessionId, now: clock() });
10
+ } catch (error) {
11
+ await host.quiesce().catch(() => {});
12
+ throw error;
13
+ }
14
+ let result;
15
+ try { result = await host.run(); }
16
+ catch {
17
+ result = { text: "Host step failed; the last known checkpoint is retained.", blocked: true,
18
+ blocker: "Host step failed; inspect the saved session before continuing.",
19
+ checkpoint: item.goal?.checkpoint ?? { hostStepFailed: true, sessionId: host.sessionId } };
20
+ }
21
+ // This is a host-returned observation, separate from model-controlled result JSON.
22
+ const quiescence = await host.quiesce();
23
+ if (quiescence?.sessionId !== host.sessionId || quiescence.status !== "quiescent"
24
+ || quiescence.admissionClosed !== true || !Array.isArray(quiescence.remaining) || quiescence.remaining.length) {
25
+ throw new Error("Host execution termination remains unconfirmed");
26
+ }
27
+ return { result, quiescence };
28
+ }