@agentcomms/resend 0.24.0 → 0.26.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.
@@ -9,10 +9,11 @@ import { styleText } from "node:util";
9
9
  import { homedir, platform, userInfo } from "node:os";
10
10
  import { accessSync, constants as constants$1, lstatSync, readFileSync, readlinkSync, realpathSync, statSync } from "node:fs";
11
11
  import { execFile, spawn } from "node:child_process";
12
+ import { gunzipSync } from "node:zlib";
12
13
  //#endregion
13
14
  //#region src/version.ts
14
15
  /** The package version, read from package.json at build time. */
15
- const VERSION$2 = "0.24.0";
16
+ const VERSION$2 = "0.26.0";
16
17
  //#endregion
17
18
  //#region src/api/routes.ts
18
19
  /**
@@ -201,7 +202,7 @@ const RESEND_CALLER = Object.freeze({
201
202
  packageName: PACKAGE_NAME
202
203
  });
203
204
  //#endregion
204
- //#region ../core/dist/update-check-BY5xNUZa.mjs
205
+ //#region ../core/dist/update-check-C3j1VxDg.mjs
205
206
  /**
206
207
  * Every failure a user or an agent can act on carries a specific, stable code from this registry, a message, an
207
208
  * optional one-line hint and structured `details`. The code decides the process exit status; agents and skills branch
@@ -4447,9 +4448,6 @@ function isBehind(version, latest) {
4447
4448
  function isPlainVersion(value) {
4448
4449
  return typeof value === "string" && /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.test(value);
4449
4450
  }
4450
- function isPrerelease(version) {
4451
- return VERSION_PATTERN.exec(version)?.[4] !== void 0;
4452
- }
4453
4451
  /**
4454
4452
  * Whether `version` is within a package's `engines` range: true or false, or null when the range is not one this reads.
4455
4453
  *
@@ -7815,6 +7813,24 @@ function parseConfig(text, source = "config.json") {
7815
7813
  if (!parsed.success) throw new CommsError("CONFIG", `${source} is invalid: ${describeIssues(parsed.error, naming === 1 ? 1 : 2)}`);
7816
7814
  return parsed.data;
7817
7815
  }
7816
+ /**
7817
+ * How a config write waits for the config lock: five seconds for each holder, thirty seconds in all.
7818
+ *
7819
+ * The lock is a queue. Every send prepare, claim and approval on every channel runs `ensureSendEpochConfig` first, and
7820
+ * while a legacy drain is open that is a config write or two per call, so an agent's parallel calls across several MCP
7821
+ * servers arrive at this lock together, each holding it for one read, check and fsynced rename (and the drain's, for a
7822
+ * list of the approvals too). Counted from when each waiter began, the lock's five seconds were a budget for the whole
7823
+ * line ahead: on the Windows runner a line of twelve writes gave up at the ninth, "another agent-communications process
7824
+ * is holding", while the lock was being handed on exactly as it should be. A holder there took about half a second, and
7825
+ * on Windows a rename alone may retry for one. Counted per holder, as Resend's throttle and Gmail's history caches count
7826
+ * theirs, a write gives up only on a holder that keeps the lock for the whole five seconds: the stuck lock the timeout is
7827
+ * for. The lock is polled rather than queued, so thirty seconds in all bounds the rest, and leaves room inside the minute
7828
+ * an MCP client waits for a tool call.
7829
+ */
7830
+ const CONFIG_WRITE_LOCK = {
7831
+ timeoutPerHolder: true,
7832
+ maxWaitMs: 3e4
7833
+ };
7818
7834
  var ConfigStore = class {
7819
7835
  path;
7820
7836
  #lockPath;
@@ -7835,6 +7851,10 @@ var ConfigStore = class {
7835
7851
  #namesMigrateAgain(why) {
7836
7852
  return handoffSentence(requiredHandoffs(this.#handoffs).core(["names", "migrate"]), (command) => `Run ${command} again ${why}.`, { instead: `Call comms_names_migrate again ${why}.` });
7837
7853
  }
7854
+ /** Runs `fn` holding the config lock, waited for as a write waits for it (`CONFIG_WRITE_LOCK`). */
7855
+ #withWriteLock(fn) {
7856
+ return withFileLock(this.#lockPath, fn, CONFIG_WRITE_LOCK);
7857
+ }
7838
7858
  /** The current config; an empty one when the file does not exist yet. */
7839
7859
  async load() {
7840
7860
  let key;
@@ -7861,7 +7881,7 @@ var ConfigStore = class {
7861
7881
  let committed;
7862
7882
  let result;
7863
7883
  try {
7864
- result = await withFileLock(this.#lockPath, async () => {
7884
+ result = await this.#withWriteLock(async () => {
7865
7885
  options.signal?.throwIfAborted();
7866
7886
  this.#cache = null;
7867
7887
  const current = structuredClone(await this.load());
@@ -12976,7 +12996,7 @@ function folding(platform) {
12976
12996
  return platform === "win32" ? (text) => text.toLowerCase() : (text) => text;
12977
12997
  }
12978
12998
  /** The package version, read from package.json at build time. */
12979
- const VERSION = "0.24.0";
12999
+ const VERSION = "0.26.0";
12980
13000
  /**
12981
13001
  * The command a person is told to run for one of this suite's own CLIs (design 2026-10-04, D1 and D5; CUE-403).
12982
13002
  *
@@ -14349,7 +14369,7 @@ const PER_WORKSPACE = {
14349
14369
  "agentLabel.slackStyles": "app",
14350
14370
  "agentLabel.slackIcons": "your Slack photo"
14351
14371
  };
14352
- function recordOf$1(value) {
14372
+ function recordOf$2(value) {
14353
14373
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
14354
14374
  }
14355
14375
  /**
@@ -14359,7 +14379,7 @@ function recordOf$1(value) {
14359
14379
  */
14360
14380
  function perWorkspace(field, before, after) {
14361
14381
  const none = PER_WORKSPACE[field];
14362
- const [was, now] = [recordOf$1(before ?? {}), recordOf$1(after ?? {})];
14382
+ const [was, now] = [recordOf$2(before ?? {}), recordOf$2(after ?? {})];
14363
14383
  if (none === void 0 || was === void 0 || now === void 0) return void 0;
14364
14384
  const value = (record, name) => Object.hasOwn(record, name) ? String(record[name]) : none;
14365
14385
  const moved = [.../* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(now)])].filter((name) => value(was, name) !== value(now, name)).sort();
@@ -14372,7 +14392,7 @@ function perWorkspace(field, before, after) {
14372
14392
  * commas. A level one side does not list allows everything there. Undefined for a value of another shape.
14373
14393
  */
14374
14394
  function perLevel(field, before, after) {
14375
- const [was, now] = [recordOf$1(before ?? {}), recordOf$1(after ?? {})];
14395
+ const [was, now] = [recordOf$2(before ?? {}), recordOf$2(after ?? {})];
14376
14396
  if (field !== "behaviour.rules" || was === void 0 || now === void 0) return void 0;
14377
14397
  const value = (record, level) => Object.hasOwn(record, level) ? String(record[level]) : "everything";
14378
14398
  const levels = [.../* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(now)])].sort();
@@ -14990,7 +15010,7 @@ const OUTCOMES = [
14990
15010
  "unknown"
14991
15011
  ];
14992
15012
  const isString = (value) => typeof value === "string";
14993
- const isTime$2 = (value) => isString(value) && ISO.test(value) && !Number.isNaN(Date.parse(value));
15013
+ const isTime$3 = (value) => isString(value) && ISO.test(value) && !Number.isNaN(Date.parse(value));
14994
15014
  /** One line as an entry, or null when it is not one this release wrote. */
14995
15015
  function entryOf(line) {
14996
15016
  let raw;
@@ -15001,7 +15021,7 @@ function entryOf(line) {
15001
15021
  }
15002
15022
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
15003
15023
  const entry = raw;
15004
- if (!isTime$2(entry.at)) return null;
15024
+ if (!isTime$3(entry.at)) return null;
15005
15025
  if (entry.kind === "use") {
15006
15026
  if (![
15007
15027
  "standingId",
@@ -15033,7 +15053,7 @@ function entryOf(line) {
15033
15053
  if (entry.kind === "outcome") {
15034
15054
  if (!isString(entry.childId) || !isString(entry.outcome) || !OUTCOMES.includes(entry.outcome)) return null;
15035
15055
  if (!isString(entry.notice) || entry.link !== void 0 && !isString(entry.link)) return null;
15036
- if (entry.actAt !== void 0 && !isTime$2(entry.actAt)) return null;
15056
+ if (entry.actAt !== void 0 && !isTime$3(entry.actAt)) return null;
15037
15057
  return {
15038
15058
  kind: "outcome",
15039
15059
  at: entry.at,
@@ -15478,7 +15498,7 @@ function updateJobSupported(platform) {
15478
15498
  function updateJobMarkerPath(stateDir) {
15479
15499
  return join(stateDir, "update-job.json");
15480
15500
  }
15481
- const isTime$1 = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
15501
+ const isTime$2 = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
15482
15502
  /** The record, or null when there is none — or none this release can read, which is treated as none. */
15483
15503
  async function readUpdateJobMarker(stateDir) {
15484
15504
  let parsed;
@@ -15493,11 +15513,11 @@ async function readUpdateJobMarker(stateDir) {
15493
15513
  platform: record.platform,
15494
15514
  installedAt: record.installedAt,
15495
15515
  label: record.label,
15496
- ...isTime$1(record.lastRun) ? { lastRun: record.lastRun } : {}
15516
+ ...isTime$2(record.lastRun) ? { lastRun: record.lastRun } : {}
15497
15517
  };
15498
15518
  }
15499
15519
  function within$1(at, now, intervalMs) {
15500
- if (at === void 0 || !isTime$1(at)) return false;
15520
+ if (at === void 0 || !isTime$2(at)) return false;
15501
15521
  const since = now.getTime() - Date.parse(at);
15502
15522
  return since >= -3e5 && since < intervalMs;
15503
15523
  }
@@ -15578,7 +15598,7 @@ const NOTICE_OUTCOMES = [
15578
15598
  function readBackgroundNotice(value) {
15579
15599
  if (typeof value !== "object" || value === null) return null;
15580
15600
  const { at, outcome, version, text } = value;
15581
- if (!isTime(at) || !NOTICE_OUTCOMES.includes(outcome) || typeof text !== "string") return null;
15601
+ if (!isTime$1(at) || !NOTICE_OUTCOMES.includes(outcome) || typeof text !== "string") return null;
15582
15602
  return {
15583
15603
  at,
15584
15604
  outcome,
@@ -15607,7 +15627,7 @@ const EMPTY_UPDATE_CHECK = Object.freeze({
15607
15627
  function updateCheckPath(stateDir) {
15608
15628
  return join(stateDir, UPDATE_CHECK_FILE);
15609
15629
  }
15610
- const isTime = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
15630
+ const isTime$1 = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
15611
15631
  const isWords = (value) => Array.isArray(value) && value.every((word) => typeof word === "string");
15612
15632
  function readRelease(value) {
15613
15633
  if (typeof value !== "object" || value === null) return null;
@@ -15640,17 +15660,17 @@ async function readUpdateCheck(stateDir) {
15640
15660
  }
15641
15661
  const held = typeof parsed === "object" && parsed !== null ? parsed : {};
15642
15662
  return {
15643
- lastChecked: isTime(held.lastChecked) ? held.lastChecked : null,
15663
+ lastChecked: isTime$1(held.lastChecked) ? held.lastChecked : null,
15644
15664
  latest: isPlainVersion(held.latest) ? held.latest : null,
15645
15665
  behind: typeof held.behind === "boolean" ? held.behind : null,
15646
15666
  current: readCurrent(held.current),
15647
15667
  lastError: typeof held.lastError === "string" ? held.lastError.slice(0, 300) : null,
15648
- snoozedUntil: isTime(held.snoozedUntil) ? held.snoozedUntil : null,
15649
- checking: isTime(held.checking) ? held.checking : null,
15650
- commandNoticeAt: isTime(held.commandNoticeAt) ? held.commandNoticeAt : null,
15651
- noticeAt: isTime(held.noticeAt) ? held.noticeAt : null,
15652
- pausedAt: isTime(held.pausedAt) ? held.pausedAt : null,
15653
- pausedUntil: isTime(held.pausedUntil) ? held.pausedUntil : null,
15668
+ snoozedUntil: isTime$1(held.snoozedUntil) ? held.snoozedUntil : null,
15669
+ checking: isTime$1(held.checking) ? held.checking : null,
15670
+ commandNoticeAt: isTime$1(held.commandNoticeAt) ? held.commandNoticeAt : null,
15671
+ noticeAt: isTime$1(held.noticeAt) ? held.noticeAt : null,
15672
+ pausedAt: isTime$1(held.pausedAt) ? held.pausedAt : null,
15673
+ pausedUntil: isTime$1(held.pausedUntil) ? held.pausedUntil : null,
15654
15674
  release: readRelease(held.release),
15655
15675
  securityAsked: isPlainVersion(held.securityAsked) ? held.securityAsked : null,
15656
15676
  securityShown: readSecurityShown(held.securityShown),
@@ -15659,11 +15679,41 @@ async function readUpdateCheck(stateDir) {
15659
15679
  };
15660
15680
  }
15661
15681
  /**
15682
+ * How every writer of the record takes the lock beside it: 2 s for each holder, and 15 s in all (design 2026-10-22, R2).
15683
+ *
15684
+ * Every server's tool call takes this lock for its refresh, so on a busy machine a writer often waits behind several
15685
+ * holders in turn, each holding it for a moment. Counted from the start of the wait, 2 s was a budget for that whole
15686
+ * queue, and a writer at the back of it gave up while the lock was being handed on exactly as it should be: a
15687
+ * person's update was installed and its record dropped. Counted per holder, a writer gives up only on one holder that
15688
+ * kept the lock for 2 s, which is a stuck lock; 15 s bounds a queue that never empties, as config writes are bounded.
15689
+ */
15690
+ const UPDATE_CHECK_LOCK = Object.freeze({
15691
+ timeoutPerHolder: true,
15692
+ timeoutMs: 2e3,
15693
+ maxWaitMs: 15e3
15694
+ });
15695
+ /**
15696
+ * How a notice takes the lock: 2 s in all, counted from the start of the wait, however often it changes hands meanwhile.
15697
+ *
15698
+ * A notice is best effort. Deciding a tool call's or a command's line, the security line in a preview, and recording
15699
+ * the person's answer to that line take the lock only for what is said, and each runs while a person or an agent
15700
+ * waits. Waited out holder by holder, as `UPDATE_CHECK_LOCK` waits, a busy machine's queue of refreshes held them for
15701
+ * up to 15 s. Busy past 2 s, a notice decides nothing this time, and a later call says it.
15702
+ *
15703
+ * Claiming the day's check is best effort too: busy past 2 s, nothing is claimed, and a later process claims it (0.26.0
15704
+ * review B). The claim's function is not named here: this comment ships in WhatsApp's bundle, which carries none of the
15705
+ * checker, not even its name (`packages/whatsapp/test/update-gate.test.ts`).
15706
+ */
15707
+ const UPDATE_NOTICE_LOCK = Object.freeze({ timeoutMs: 2e3 });
15708
+ /**
15662
15709
  * Reads the record, changes it and writes it back, under a lock beside it, atomically — so a server recording a check
15663
15710
  * and a person's "not now" arriving at the same moment each keep what the other wrote. Returns what `change` returned
15664
15711
  * alongside the record written; `change` returning `null` writes nothing.
15712
+ *
15713
+ * `lock` is how long to wait for the lock: `UPDATE_CHECK_LOCK`, for a record that matters, unless a notice or the
15714
+ * day's claim passes `UPDATE_NOTICE_LOCK`. Busy past it, this throws LOCK_TIMEOUT, and nothing is written.
15665
15715
  */
15666
- async function changeUpdateCheck(stateDir, change) {
15716
+ async function changeUpdateCheck(stateDir, change, lock = UPDATE_CHECK_LOCK) {
15667
15717
  return withFileLock(join(stateDir, ".update-check.lock"), async () => {
15668
15718
  const current = await readUpdateCheck(stateDir);
15669
15719
  const next = change(current);
@@ -15678,7 +15728,7 @@ async function changeUpdateCheck(stateDir, change) {
15678
15728
  result: next.result,
15679
15729
  written: true
15680
15730
  };
15681
- }, { timeoutMs: 2e3 });
15731
+ }, lock);
15682
15732
  }
15683
15733
  /**
15684
15734
  * Whether the check is due: never asked, asked a day or more ago, or — a clock that went back — asked more than a few
@@ -15938,7 +15988,7 @@ async function securityAnswered(stateDir, approvalId) {
15938
15988
  securityAsked: record.securityShown.version,
15939
15989
  securityShown: null
15940
15990
  } };
15941
- });
15991
+ }, UPDATE_NOTICE_LOCK);
15942
15992
  } catch {}
15943
15993
  }
15944
15994
  /** A local time as a person reads it in a preview: `2026-09-29 00:00`. */
@@ -16500,34 +16550,47 @@ function releaseIntegrity(packument, version) {
16500
16550
  return typeof integrity === "string" && integrity.trim() !== "" ? integrity : null;
16501
16551
  }
16502
16552
  /**
16503
- * What a release's core manifest declares under `agentcommsRelease` (U5): a security fix, and a decision it needs from
16504
- * the person — as flags only. The decision's own words are never read out: they are the registry's, and an update says
16505
- * that one is needed in its own words. A declaration this release cannot read waits for the person: it may be the
16506
- * decision it cannot see.
16553
+ * What a release's core manifest declares under `agentcommsRelease` (U5, design 2026-10-22 §8.1): a security fix, a
16554
+ * decision it needs from the person, and the newest release up to it that needed one — as flags and a plain version
16555
+ * only. The manifest is the one in the release's verified tarball (`release-declarations.ts`, design 2026-10-22 P1),
16556
+ * never the registry's copy. The decision's own words are never read out: an update says that one is needed in its own
16557
+ * words. A declaration this release cannot read waits for the person, as it may be the decision it cannot see: a field
16558
+ * that is not an object, a `needsDecision` that is not false, or a `needsDecisionSince` that is not a plain version.
16507
16559
  */
16508
16560
  function releaseDeclaration(manifest) {
16509
16561
  const declared = manifest?.agentcommsRelease;
16510
16562
  if (declared === void 0 || declared === null) return {
16511
16563
  security: false,
16512
- needsDecision: false
16564
+ needsDecision: false,
16565
+ needsDecisionSince: null
16513
16566
  };
16514
16567
  if (typeof declared !== "object" || Array.isArray(declared)) return {
16515
16568
  security: false,
16516
- needsDecision: true
16569
+ needsDecision: true,
16570
+ needsDecisionSince: null
16517
16571
  };
16518
- const { security, needsDecision } = declared;
16572
+ const { security, needsDecision, needsDecisionSince } = declared;
16573
+ const since = isPlainVersion(needsDecisionSince) ? needsDecisionSince : null;
16519
16574
  return {
16520
16575
  security: security === true,
16521
- needsDecision: !(needsDecision === void 0 || needsDecision === null || needsDecision === false)
16576
+ needsDecision: needsDecisionSince !== void 0 && since === null || !(needsDecision === void 0 || needsDecision === null || needsDecision === false),
16577
+ needsDecisionSince: since
16522
16578
  };
16523
16579
  }
16524
16580
  /**
16525
- * Every release the registry lists after `from`, up to and including `to`, oldest first: what an update takes this
16526
- * machine past. Prereleases are left out — no update installs one — and so is anything that is not a version, or that
16527
- * carries build metadata, which no published version does.
16581
+ * The release a machine running `from` must decide on before it runs `declared` (design 2026-10-22 §8.1), or null when
16582
+ * there is none: `declared` itself when it declares `needsDecision`; otherwise the release its `needsDecisionSince`
16583
+ * names, when that is after `from` — a decision this machine has not taken. One at or before `from` was taken when
16584
+ * that release was installed. Nothing else counts: no release the registry lists, hidden or added, decides anything.
16585
+ * A `from` that is not known, or not a version, cannot be shown to be past it, so it waits; a `since` after
16586
+ * `declared`, which no release check lets through, is said as `declared`.
16528
16587
  */
16529
- function releasesBetween(packument, from, to) {
16530
- return Object.keys(packument.versions ?? {}).filter((version) => isVersion(version) && !isPrerelease(version) && !version.includes("+") && (compareVersions(version, from) ?? 0) > 0 && (compareVersions(version, to) ?? 1) <= 0).sort((a, b) => compareVersions(a, b) ?? 0);
16588
+ function decisionNeeded(declared, from) {
16589
+ if (declared.needsDecision) return declared.version;
16590
+ const since = declared.needsDecisionSince;
16591
+ if (since === null) return null;
16592
+ if (from !== null && (compareVersions(since, from) ?? 1) <= 0) return null;
16593
+ return (compareVersions(since, declared.version) ?? 1) > 0 ? declared.version : since;
16531
16594
  }
16532
16595
  /** The oldest of `versions` that are versions, or null. */
16533
16596
  function oldestVersion(versions) {
@@ -16631,42 +16694,6 @@ async function verifyReleaseSet(sources, wanted, act = "update", noVerifierHint)
16631
16694
  return verified;
16632
16695
  }
16633
16696
  /**
16634
- * What every release after `from`, up to `latest`, declares (design 2026-10-16 U5, U7; security review of 0.20.0,
16635
- * severity 6): a decision any of them needs — a later release that does not say so again does not end it — and whether
16636
- * `latest` is a security fix. Each declaration acted on is verified first, with `verify`; one that does not verify, or
16637
- * where nothing here can verify (`verify` null: a channel's process, which carries no Sigstore), counts for nothing.
16638
- * `from` null reads `latest` alone. Throws when the registry's document cannot be read.
16639
- */
16640
- async function declaredSince(registry, verify, packageName, from, latest) {
16641
- const { packument } = await registry(packageName);
16642
- const declared = (version) => releaseDeclaration(packument.versions?.[version]);
16643
- const range = from === null ? [] : releasesBetween(packument, from, latest);
16644
- const deciding = [.../* @__PURE__ */ new Set([...range, latest])].filter((version) => declared(version).needsDecision);
16645
- const verified = /* @__PURE__ */ new Map();
16646
- const check = async (version) => {
16647
- const known = verified.get(version);
16648
- if (known !== void 0) return known;
16649
- const integrity = releaseIntegrity(packument, version);
16650
- const ok = verify !== null && integrity !== null && await verify({
16651
- packageName,
16652
- version,
16653
- integrity
16654
- }).then((v) => v.ok);
16655
- verified.set(version, ok);
16656
- return ok;
16657
- };
16658
- const decisions = [];
16659
- for (const version of deciding) decisions.push({
16660
- version,
16661
- verified: await check(version)
16662
- });
16663
- return {
16664
- security: declared(latest).security && await check(latest),
16665
- needsDecision: decisions.some((decision) => decision.verified),
16666
- deciding: decisions
16667
- };
16668
- }
16669
- /**
16670
16697
  * Registering, pruning and listing every declared channel or service MCP server, from the core.
16671
16698
  *
16672
16699
  * `agentcomms mcp install` registers core or a declared service; `comms_server_install` registers any declared
@@ -16984,6 +17011,7 @@ function serverInstallChange(core, env, request, own, deps = {}) {
16984
17011
  });
16985
17012
  const unverifiedOwn = fetches && deps.verifyRelease === void 0 && running !== void 0 && running.packageName === product.packageName && running.version === version;
16986
17013
  const verified = fetches && !unverifiedOwn || checksDisk ? (await verifyReleaseSet(from, /* @__PURE__ */ new Map([[product.packageName, version]]), "install", from.registry === null || from.verify === null ? verifyInCore() : void 0)).get(product.packageName) : void 0;
17014
+ if (request.print !== true && launcher === "npx" && target.writes && deps.verifyRelease !== void 0) await verifyReleaseSet(from, /* @__PURE__ */ new Map([[facts.npxPackage, version]]), "install");
16987
17015
  planned = {
16988
17016
  product,
16989
17017
  install: plannedInstall(preflight),
@@ -17163,7 +17191,7 @@ function serverPruneChange(core, env, request, own) {
17163
17191
  };
17164
17192
  const { packageName, version } = ownProduct(request.channel, own) ?? {
17165
17193
  packageName: serverFacts(request.channel).packageName,
17166
- version: "0.24.0"
17194
+ version: "0.26.0"
17167
17195
  };
17168
17196
  const product = {
17169
17197
  packageName,
@@ -17317,6 +17345,599 @@ Object.freeze([
17317
17345
  "off"
17318
17346
  ]);
17319
17347
  Object.keys(STANDING_LIMITS);
17348
+ const UNPACKED_MAX_BYTES = 134217728;
17349
+ const NPM_REGISTRY = "https://registry.npmjs.org/";
17350
+ const REDIRECTS = /* @__PURE__ */ new Set([
17351
+ 301,
17352
+ 302,
17353
+ 303,
17354
+ 307,
17355
+ 308
17356
+ ]);
17357
+ const BLOCK = 512;
17358
+ /** Where npm's own registry serves a release's tarball: `https://registry.npmjs.org/<name>/-/<file>-<version>.tgz`. */
17359
+ function canonicalTarballUrl(packageName, version) {
17360
+ return `${NPM_REGISTRY}${packageName}/-/${packageName.split("/").at(-1) ?? packageName}-${version}.tgz`;
17361
+ }
17362
+ /** `value` as an absolute `http:` or `https:` address (against `base` when given), without a user or password. */
17363
+ function httpAddress(value, base) {
17364
+ if (typeof value !== "string" || value.trim() === "") return null;
17365
+ let url;
17366
+ try {
17367
+ url = base === void 0 ? new URL(value) : new URL(value, base);
17368
+ } catch {
17369
+ return null;
17370
+ }
17371
+ if (url.protocol !== "http:" && url.protocol !== "https:") return null;
17372
+ url.username = "";
17373
+ url.password = "";
17374
+ return url.href;
17375
+ }
17376
+ /**
17377
+ * `dist.tarball` of `version` when it is an absolute http(s) URL; otherwise null. A user and password in it are left
17378
+ * out: no credential is sent with a tarball request, so a mirror that needs one falls back to npm's address.
17379
+ */
17380
+ function tarballLocation(packument, version) {
17381
+ return httpAddress(packument.versions?.[version]?.dist?.tarball);
17382
+ }
17383
+ /**
17384
+ * The fetch a tarball is asked with, unless a caller hands in another: Node's own, asking for the bytes as they are
17385
+ * stored (`accept-encoding: identity` — otherwise it asks for a compressed transfer and decodes it), with no
17386
+ * credential and no cookie.
17387
+ */
17388
+ const fetchTarball = (url, init) => fetch(url, {
17389
+ ...init,
17390
+ headers: { "accept-encoding": "identity" },
17391
+ credentials: "omit"
17392
+ });
17393
+ function discard(response) {
17394
+ response.body?.cancel().catch(() => void 0);
17395
+ }
17396
+ /**
17397
+ * One location: its redirects followed by hand, at most five, each to http(s); its body counted as it arrives; ended
17398
+ * by its own time, or by `deadline` — the caller's — whichever is first.
17399
+ */
17400
+ async function fetchOne(ask, url, timeoutMs, deadline) {
17401
+ const own = AbortSignal.timeout(timeoutMs);
17402
+ const signal = deadline === void 0 ? own : AbortSignal.any([own, deadline]);
17403
+ let current = url;
17404
+ try {
17405
+ for (let redirects = 0;; redirects += 1) {
17406
+ const response = await ask(current, {
17407
+ redirect: "manual",
17408
+ signal
17409
+ });
17410
+ if (REDIRECTS.has(response.status)) {
17411
+ discard(response);
17412
+ const next = httpAddress(response.headers.get("location"), current);
17413
+ if (next === null || redirects >= 5) return {
17414
+ ok: false,
17415
+ problem: "download"
17416
+ };
17417
+ current = next;
17418
+ continue;
17419
+ }
17420
+ if (!response.ok) {
17421
+ discard(response);
17422
+ return {
17423
+ ok: false,
17424
+ problem: "download"
17425
+ };
17426
+ }
17427
+ const declared = Number(response.headers.get("content-length") ?? NaN);
17428
+ if (Number.isFinite(declared) && declared > 33554432) {
17429
+ discard(response);
17430
+ return {
17431
+ ok: false,
17432
+ problem: "size"
17433
+ };
17434
+ }
17435
+ if (response.body === null) return {
17436
+ ok: true,
17437
+ bytes: Buffer.alloc(0)
17438
+ };
17439
+ const reader = response.body.getReader();
17440
+ const chunks = [];
17441
+ let total = 0;
17442
+ for (;;) {
17443
+ const { done, value } = await reader.read();
17444
+ if (done) break;
17445
+ total += value.byteLength;
17446
+ if (total > 33554432) {
17447
+ reader.cancel().catch(() => void 0);
17448
+ return {
17449
+ ok: false,
17450
+ problem: "size"
17451
+ };
17452
+ }
17453
+ chunks.push(Buffer.from(value.buffer, value.byteOffset, value.byteLength));
17454
+ }
17455
+ return {
17456
+ ok: true,
17457
+ bytes: Buffer.concat(chunks, total)
17458
+ };
17459
+ }
17460
+ } catch {
17461
+ return {
17462
+ ok: false,
17463
+ problem: "download"
17464
+ };
17465
+ }
17466
+ }
17467
+ /** Of two failed downloads, the one that says more: bytes that came and were not the release's, then too many. */
17468
+ const RANK = {
17469
+ download: 0,
17470
+ size: 1,
17471
+ digest: 2
17472
+ };
17473
+ /**
17474
+ * The release's tarball: from `location`, then — if that fails in any way — once from npm's canonical address, which
17475
+ * is never asked twice. Kept only when its sha512 is the verified one; otherwise the problem that says the most of the
17476
+ * two tries. A caller's `signal` ends it: what is being fetched is cut off, and nothing more is asked.
17477
+ */
17478
+ async function downloadVerifiedTarball(release, location, options = {}) {
17479
+ const ask = options.fetch ?? fetchTarball;
17480
+ const timeoutMs = options.timeoutMs ?? 6e4;
17481
+ const want = release.sha512.toLowerCase();
17482
+ const canonical = canonicalTarballUrl(release.packageName, release.version);
17483
+ const first = location === null ? null : httpAddress(location);
17484
+ let problem = "download";
17485
+ for (const place of first === null || first === canonical ? [canonical] : [first, canonical]) {
17486
+ if (options.signal?.aborted) break;
17487
+ const fetched = await fetchOne(ask, place, timeoutMs, options.signal);
17488
+ let failed;
17489
+ if (fetched.ok) {
17490
+ if (createHash("sha512").update(fetched.bytes).digest("hex") === want) return fetched;
17491
+ failed = "digest";
17492
+ } else failed = fetched.problem;
17493
+ if (RANK[failed] > RANK[problem]) problem = failed;
17494
+ }
17495
+ return {
17496
+ ok: false,
17497
+ problem
17498
+ };
17499
+ }
17500
+ /** A header field's text: its bytes up to the first NUL. */
17501
+ function fieldText(header, at, width) {
17502
+ const raw = header.subarray(at, at + width);
17503
+ const end = raw.indexOf(0);
17504
+ return raw.subarray(0, end === -1 ? width : end).toString("utf8");
17505
+ }
17506
+ /** A header's octal number, spaces around it allowed; null for base-256 (the high bit set) or anything not octal. */
17507
+ function octalField(header, at, width) {
17508
+ if ((header[at] & 128) !== 0) return null;
17509
+ const digits = fieldText(header, at, width).trim();
17510
+ if (digits === "") return 0;
17511
+ if (!/^[0-7]+$/.test(digits)) return null;
17512
+ const value = Number.parseInt(digits, 8);
17513
+ return Number.isSafeInteger(value) ? value : null;
17514
+ }
17515
+ /** The checksum ustar stores at 148: the header's byte sum, its own eight bytes read as spaces. */
17516
+ function checksumHolds(header) {
17517
+ const stored = octalField(header, 148, 8);
17518
+ if (stored === null) return false;
17519
+ let sum = 0;
17520
+ for (let at = 0; at < BLOCK; at += 1) sum += at >= 148 && at < 156 ? 32 : header[at];
17521
+ return sum === stored;
17522
+ }
17523
+ /** A pax extended header's records, `<length> <key>=<value>\n` each; null when one is not that. */
17524
+ function paxRecords(data) {
17525
+ const records = /* @__PURE__ */ new Map();
17526
+ let at = 0;
17527
+ while (at < data.length) {
17528
+ if (data.subarray(at).every((byte) => byte === 0)) break;
17529
+ const space = data.indexOf(32, at);
17530
+ if (space === -1) return null;
17531
+ const length = data.subarray(at, space).toString("latin1");
17532
+ if (!/^[1-9]\d*$/.test(length)) return null;
17533
+ const end = at + Number(length);
17534
+ if (end > data.length || end <= space + 1 || data[end - 1] !== 10) return null;
17535
+ const record = data.subarray(space + 1, end - 1).toString("utf8");
17536
+ const equals = record.indexOf("=");
17537
+ if (equals <= 0) return null;
17538
+ records.set(record.slice(0, equals), record.slice(equals + 1));
17539
+ at = end;
17540
+ }
17541
+ return records;
17542
+ }
17543
+ /**
17544
+ * An entry's path below `package/`, POSIX, with any leading `./` gone and empty or `.` segments dropped: `''` for the
17545
+ * folder itself. Null for anything outside `package/`, a `..` segment, or a backslash, which no npm pack writes and
17546
+ * which some file systems read as a separator.
17547
+ */
17548
+ function belowPackage(name) {
17549
+ let rest = name;
17550
+ while (rest.startsWith("./")) rest = rest.slice(2);
17551
+ if (rest.includes("\\")) return null;
17552
+ const parts = rest.split("/");
17553
+ if (parts[0] !== "package" || parts.includes("..")) return null;
17554
+ return parts.slice(1).filter((part) => part !== "" && part !== ".").join("/");
17555
+ }
17556
+ function entryType(flag) {
17557
+ if (flag === "0" || flag === "\0") return "file";
17558
+ if (flag === "1" || flag === "2") return "link";
17559
+ if (flag === "5") return "directory";
17560
+ return "other";
17561
+ }
17562
+ /**
17563
+ * Every entry of a release's archive, in order: gunzipped with `node:zlib`, stopping past 128 MiB, then walked as
17564
+ * ustar — 512-byte headers, each checksum checked, to the first all-zero block. A pax `x` header's `path` and `size`,
17565
+ * and a GNU `L` long name, apply to the next entry. `package/` itself is not listed.
17566
+ */
17567
+ function tarballEntries(gz) {
17568
+ let tar;
17569
+ try {
17570
+ tar = gunzipSync(gz, { maxOutputLength: UNPACKED_MAX_BYTES });
17571
+ } catch {
17572
+ return {
17573
+ ok: false,
17574
+ problem: "gzip"
17575
+ };
17576
+ }
17577
+ const broken = {
17578
+ ok: false,
17579
+ problem: "archive"
17580
+ };
17581
+ const entries = [];
17582
+ let pax = {};
17583
+ let longName;
17584
+ for (let offset = 0; offset + BLOCK <= tar.length;) {
17585
+ const header = tar.subarray(offset, offset + BLOCK);
17586
+ if (header.every((byte) => byte === 0)) break;
17587
+ if (!checksumHolds(header)) return broken;
17588
+ const ownSize = octalField(header, 124, 12);
17589
+ if (ownSize === null) return broken;
17590
+ const flag = String.fromCharCode(header[156]);
17591
+ const size = flag === "x" || flag === "L" || flag === "K" || flag === "g" ? ownSize : pax.size ?? ownSize;
17592
+ const start = offset + BLOCK;
17593
+ if (start + size > tar.length) return broken;
17594
+ const data = tar.subarray(start, start + size);
17595
+ offset = start + Math.ceil(size / BLOCK) * BLOCK;
17596
+ if (flag === "g") return broken;
17597
+ if (flag === "x") {
17598
+ const records = paxRecords(data);
17599
+ if (records === null) return broken;
17600
+ const path = records.get("path");
17601
+ const paxSize = records.get("size");
17602
+ if (paxSize !== void 0 && !/^\d+$/.test(paxSize)) return broken;
17603
+ pax = {
17604
+ ...pax,
17605
+ ...path !== void 0 ? { path } : {},
17606
+ ...paxSize !== void 0 ? { size: Number(paxSize) } : {}
17607
+ };
17608
+ if (pax.size !== void 0 && !Number.isSafeInteger(pax.size)) return broken;
17609
+ continue;
17610
+ }
17611
+ if (flag === "L") {
17612
+ const end = data.indexOf(0);
17613
+ longName = data.subarray(0, end === -1 ? data.length : end).toString("utf8");
17614
+ continue;
17615
+ }
17616
+ if (flag === "K") continue;
17617
+ const prefix = fieldText(header, 257, 6) === "ustar" ? fieldText(header, 345, 155) : "";
17618
+ const own = fieldText(header, 0, 100);
17619
+ const name = pax.path ?? longName ?? (prefix === "" ? own : `${prefix}/${own}`);
17620
+ pax = {};
17621
+ longName = void 0;
17622
+ const path = belowPackage(name);
17623
+ const type = entryType(flag);
17624
+ if (path === null) return broken;
17625
+ if (path === "") {
17626
+ if (type === "directory") continue;
17627
+ return broken;
17628
+ }
17629
+ entries.push({
17630
+ path,
17631
+ type,
17632
+ data
17633
+ });
17634
+ }
17635
+ return {
17636
+ ok: true,
17637
+ entries
17638
+ };
17639
+ }
17640
+ /**
17641
+ * The release's `package/package.json`, as an object: exactly one entry by that name (in any case), a regular file of
17642
+ * at most 1 MiB, holding a JSON object. Anything else is `manifest`; an archive that cannot be walked, `archive`.
17643
+ */
17644
+ function tarballManifest(gz) {
17645
+ const read = tarballEntries(gz);
17646
+ if (!read.ok) return read;
17647
+ const none = {
17648
+ ok: false,
17649
+ problem: "manifest"
17650
+ };
17651
+ const named = read.entries.filter((entry) => entry.path.toLowerCase() === "package.json");
17652
+ const [entry] = named;
17653
+ if (named.length !== 1 || entry === void 0) return none;
17654
+ if (entry.path !== "package.json" || entry.type !== "file" || entry.data.length > 1048576) return none;
17655
+ let manifest;
17656
+ try {
17657
+ manifest = JSON.parse(entry.data.toString("utf8").replace(/^/, ""));
17658
+ } catch {
17659
+ return none;
17660
+ }
17661
+ if (manifest === null || typeof manifest !== "object" || Array.isArray(manifest)) return none;
17662
+ return {
17663
+ ok: true,
17664
+ manifest
17665
+ };
17666
+ }
17667
+ const REMOVED_GLOBALS_FILE = "removed-globals.json";
17668
+ function isTime(value) {
17669
+ if (typeof value !== "string") return false;
17670
+ const at = new Date(value);
17671
+ return !Number.isNaN(at.getTime()) && at.toISOString() === value;
17672
+ }
17673
+ /**
17674
+ * The globals recorded for the next update to reinstall, each once: none when there is no record or it cannot be
17675
+ * read. Only a name of this suite and a plain release are read from it.
17676
+ */
17677
+ async function readRemovedGlobals(stateDir) {
17678
+ let parsed;
17679
+ try {
17680
+ parsed = JSON.parse(await readFile(join(stateDir, REMOVED_GLOBALS_FILE), "utf8"));
17681
+ } catch {
17682
+ return [];
17683
+ }
17684
+ const listed = typeof parsed === "object" && parsed !== null ? parsed.removed : void 0;
17685
+ if (!Array.isArray(listed)) return [];
17686
+ const removed = [];
17687
+ for (const item of listed) {
17688
+ if (typeof item !== "object" || item === null) continue;
17689
+ const { package: name, version, removedAt } = item;
17690
+ if (typeof name !== "string" || !SUITE_NAME.test(name) || typeof version !== "string") continue;
17691
+ if (!isPlainVersion(version) || !isTime(removedAt)) continue;
17692
+ if (removed.some((one) => one.package === name)) continue;
17693
+ removed.push({
17694
+ package: name,
17695
+ version,
17696
+ removedAt
17697
+ });
17698
+ }
17699
+ return removed;
17700
+ }
17701
+ /** A name of this suite, as npm writes one: only then is it said in a detail. */
17702
+ const SUITE_NAME = /^@agentcomms\/[a-z0-9][a-z0-9._-]{0,213}$/;
17703
+ /**
17704
+ * What a release declares — a security fix, a decision it needs from the person — read from its verified tarball, and
17705
+ * never from the registry (design 2026-10-22 P1). The registry's copy of a manifest is the registry's word: a mirror
17706
+ * could add or remove either flag on a genuine release. The tarball whose sha512 is the verified provenance digest is
17707
+ * the release's own.
17708
+ *
17709
+ * What was read is kept per integrity, content-free — the version, the integrity, the two flags and the release a
17710
+ * needed decision is carried from (design 2026-10-22 §8.1), never a word of the manifest — in
17711
+ * `<state>/release-declarations.json`, so neither the daily check nor the background run downloads a release it read.
17712
+ * A declaration that cannot be read is unknown: it is never cached, so the next check or run tries it again, and the
17713
+ * caller fails closed. The cache saves downloads, not verification: the caller verifies the release first, every time.
17714
+ */
17715
+ const DECLARATIONS_FILE = "release-declarations.json";
17716
+ const LOCK_FILE = ".release-declarations.lock";
17717
+ const INTEGRITY = /^sha512-[A-Za-z0-9+/]{86}==$/;
17718
+ function declarationsPath(stateDir) {
17719
+ return join(stateDir, DECLARATIONS_FILE);
17720
+ }
17721
+ /**
17722
+ * A record as the cache keeps it, or null for anything else: only these five keys, each of its type. One without
17723
+ * `needsDecisionSince`, as a build before it was carried wrote, is no record: it costs a download, never a decision.
17724
+ */
17725
+ function recordOf(value) {
17726
+ if (value === null || typeof value !== "object" || Array.isArray(value)) return null;
17727
+ const { version, integrity, security, needsDecision, needsDecisionSince } = value;
17728
+ if (!isVersion(version) || typeof integrity !== "string" || !INTEGRITY.test(integrity)) return null;
17729
+ if (typeof security !== "boolean" || typeof needsDecision !== "boolean") return null;
17730
+ if (needsDecisionSince !== null && !isPlainVersion(needsDecisionSince)) return null;
17731
+ return {
17732
+ version,
17733
+ integrity,
17734
+ security,
17735
+ needsDecision,
17736
+ needsDecisionSince
17737
+ };
17738
+ }
17739
+ /** The cache's records; a file that is missing or cannot be read counts as empty. */
17740
+ async function readCache(stateDir) {
17741
+ try {
17742
+ const listed = JSON.parse(await readFile(declarationsPath(stateDir), "utf8"))?.declarations;
17743
+ if (!Array.isArray(listed)) return [];
17744
+ return listed.map(recordOf).filter((record) => record !== null);
17745
+ } catch {
17746
+ return [];
17747
+ }
17748
+ }
17749
+ /**
17750
+ * Adds one record, under the cache's lock, to the file as it is then — so two processes reading at once each keep
17751
+ * the other's — keeping the newest 64. Best effort: a write that fails changes no answer, and costs a download later.
17752
+ */
17753
+ async function remember(stateDir, record) {
17754
+ try {
17755
+ await withFileLock(join(stateDir, LOCK_FILE), async () => {
17756
+ const declarations = [...(await readCache(stateDir)).filter((one) => one.integrity !== record.integrity || one.version !== record.version), record].slice(-64);
17757
+ await writeFileAtomic(declarationsPath(stateDir), `${JSON.stringify({ declarations }, null, 2)}\n`);
17758
+ });
17759
+ } catch {}
17760
+ }
17761
+ /**
17762
+ * What a verified release declares: from the cache, by its integrity and version, or else from its tarball — the bytes
17763
+ * whose sha512 is `provenance.sha512`, from `location` or npm's canonical address — whose `package/package.json` must
17764
+ * name the release itself. Unknown, with the problem, when it cannot be read, or when `signal` ends the read first. A
17765
+ * known declaration is cached unless `writeCache` is false (a dry run); an unknown one never is.
17766
+ */
17767
+ async function readVerifiedDeclaration(provenance, location, options) {
17768
+ const { version } = provenance;
17769
+ const integrity = `sha512-${Buffer.from(provenance.sha512, "hex").toString("base64")}`;
17770
+ const hit = (await readCache(options.stateDir)).find((record) => record.integrity === integrity && record.version === version);
17771
+ if (hit !== void 0) {
17772
+ const { security, needsDecision, needsDecisionSince } = hit;
17773
+ return {
17774
+ known: true,
17775
+ version,
17776
+ security,
17777
+ needsDecision,
17778
+ needsDecisionSince,
17779
+ from: "cache"
17780
+ };
17781
+ }
17782
+ const download = await downloadVerifiedTarball({
17783
+ packageName: provenance.package,
17784
+ version,
17785
+ sha512: provenance.sha512
17786
+ }, location, {
17787
+ ...options.fetch !== void 0 ? { fetch: options.fetch } : {},
17788
+ ...options.timeoutMs !== void 0 ? { timeoutMs: options.timeoutMs } : {},
17789
+ ...options.signal !== void 0 ? { signal: options.signal } : {}
17790
+ });
17791
+ if (!download.ok) return {
17792
+ known: false,
17793
+ version,
17794
+ problem: download.problem
17795
+ };
17796
+ const read = tarballManifest(download.bytes);
17797
+ if (!read.ok) return {
17798
+ known: false,
17799
+ version,
17800
+ problem: read.problem
17801
+ };
17802
+ if (read.manifest.name !== provenance.package || read.manifest.version !== version) return {
17803
+ known: false,
17804
+ version,
17805
+ problem: "identity"
17806
+ };
17807
+ const { security, needsDecision, needsDecisionSince } = releaseDeclaration(read.manifest);
17808
+ if (options.writeCache !== false) await remember(options.stateDir, {
17809
+ version,
17810
+ integrity,
17811
+ security,
17812
+ needsDecision,
17813
+ needsDecisionSince
17814
+ });
17815
+ return {
17816
+ known: true,
17817
+ version,
17818
+ security,
17819
+ needsDecision,
17820
+ needsDecisionSince,
17821
+ from: "tarball"
17822
+ };
17823
+ }
17824
+ /**
17825
+ * What the release an update goes to declares, believed only from its verified tarball (design 2026-10-22 P1, P2,
17826
+ * §8.1): for the daily check's record, `update --check`, and the by-hand update's preview.
17827
+ *
17828
+ * Only that one release is read. A needed decision is carried forward from release to release in core's
17829
+ * `agentcommsRelease.needsDecisionSince`, which the release check holds to the changelog, so the release an update goes
17830
+ * to says itself whether a release on the way needed one: which versions the registry lists decides nothing. A registry
17831
+ * or mirror that leaves out the release needing a decision, adds an unverifiable one, or lists five hundred, changes no
17832
+ * fact and costs no request.
17833
+ *
17834
+ * The registry's document is read once, for that release's integrity and where its tarball is — never for what it
17835
+ * declares: a registry or a mirror can add or remove either flag on a genuine release. The release is verified, and
17836
+ * then its declaration read (`readVerifiedDeclaration`) from the bytes whose sha512 its verified provenance signs. One
17837
+ * that does not verify, that the registry gives no integrity for, whose tarball cannot be read, or whose read is cut off
17838
+ * by its deadline is *unknown*, and counts for nothing either way: neither a security fix nor its absence, neither a
17839
+ * decision nor none.
17840
+ *
17841
+ * Only core reads these, since only core carries the verifier: a channel's process reads none (`releaseFactsReader`).
17842
+ * No Sigstore here, and nothing runs when this module is loaded: WhatsApp's bundle, which reaches the module that
17843
+ * imports this one, drops it.
17844
+ */
17845
+ /**
17846
+ * How long a check gives its target's declaration — its document, its verification and its tarball, from both places —
17847
+ * before it counts it unknown (review B of 0.26.0, finding 2). A check runs under a claim of `UPDATE_CHECK_LEASE_MS`,
17848
+ * two minutes, which also holds the registry's ten seconds for `latest`, `npm ls`'s minute and up to 15 seconds for
17849
+ * the record's lock: 30 seconds beside them still records inside the lease, where a tarball's two tries of a minute
17850
+ * each did not, and the check's finding was dropped as superseded.
17851
+ */
17852
+ const DECLARATION_DEADLINE_MS = 3e4;
17853
+ /**
17854
+ * What `version` declares, read from its verified tarball, and nothing of any other release. Unknown when the
17855
+ * registry's document cannot be read or gives no integrity for it, when it does not verify, when its tarball cannot be
17856
+ * read, or when `signal` ends the read first. Never throws.
17857
+ */
17858
+ async function targetDeclaration(sources, packageName, version, options = {}) {
17859
+ const unknown = {
17860
+ known: false,
17861
+ version
17862
+ };
17863
+ try {
17864
+ const { packument } = await sources.registry(packageName);
17865
+ const integrity = releaseIntegrity(packument, version);
17866
+ if (integrity === null || options.signal?.aborted) return unknown;
17867
+ const verdict = await sources.verify({
17868
+ packageName,
17869
+ version,
17870
+ integrity
17871
+ });
17872
+ if (!verdict.ok || options.signal?.aborted) return unknown;
17873
+ const read = await readVerifiedDeclaration(verdict.provenance, tarballLocation(packument, version), {
17874
+ stateDir: sources.stateDir,
17875
+ ...sources.tarballs !== void 0 ? { fetch: sources.tarballs } : {},
17876
+ ...options.signal !== void 0 ? { signal: options.signal } : {}
17877
+ });
17878
+ if (!read.known) return unknown;
17879
+ const { security, needsDecision, needsDecisionSince } = read;
17880
+ return {
17881
+ known: true,
17882
+ version,
17883
+ security,
17884
+ needsDecision,
17885
+ needsDecisionSince
17886
+ };
17887
+ } catch {
17888
+ return unknown;
17889
+ }
17890
+ }
17891
+ /**
17892
+ * `read`, given `deadlineMs` at most: unknown once that has passed, and the read cut off then — never a guess of
17893
+ * `false`. The timer is cleared as soon as the read is done.
17894
+ */
17895
+ async function withinDeadline(read, version, deadlineMs = DECLARATION_DEADLINE_MS) {
17896
+ const controller = new AbortController();
17897
+ let timer;
17898
+ const passed = new Promise((resolve) => {
17899
+ timer = setTimeout(() => {
17900
+ controller.abort();
17901
+ resolve({
17902
+ known: false,
17903
+ version
17904
+ });
17905
+ }, deadlineMs);
17906
+ });
17907
+ try {
17908
+ return await Promise.race([read(controller.signal), passed]);
17909
+ } finally {
17910
+ clearTimeout(timer);
17911
+ }
17912
+ }
17913
+ /**
17914
+ * What the release declares for a machine whose oldest release is `from` (design 2026-10-22 §8.1): its own security
17915
+ * flag, and a decision when it declares one or carries one after `from` (`decisionNeeded`).
17916
+ */
17917
+ function declaredFor(declared, from) {
17918
+ if (!declared.known) return {
17919
+ security: null,
17920
+ needsDecision: null,
17921
+ deciding: null,
17922
+ unread: true
17923
+ };
17924
+ const deciding = decisionNeeded(declared, from);
17925
+ return {
17926
+ security: declared.security,
17927
+ needsDecision: deciding !== null,
17928
+ deciding,
17929
+ unread: false
17930
+ };
17931
+ }
17932
+ /** The record's facts for `version`: null unless its declaration was read. */
17933
+ function factsOf(version, declared) {
17934
+ const { security, needsDecision } = declared;
17935
+ return security === null || needsDecision === null ? null : {
17936
+ version,
17937
+ security,
17938
+ needsDecision
17939
+ };
17940
+ }
17320
17941
  /**
17321
17942
  * Bringing a machine to the latest release, from a terminal (`agentcomms update`) or a chat (`comms_update`).
17322
17943
  *
@@ -17485,6 +18106,7 @@ async function inspect(core, env, deps) {
17485
18106
  package: server.args.some((arg) => arg.startsWith(`${facts.npxPackage}@`)) ? facts.npxPackage : facts.packageName
17486
18107
  };
17487
18108
  });
18109
+ const removedGlobals = (await readRemovedGlobals(core.paths.stateDir)).filter((one) => PUBLISHED_NAMES.includes(one.package));
17488
18110
  let installed = {};
17489
18111
  try {
17490
18112
  const listed = await (deps.globalPackages ?? (() => npmGlobalPackages(env, PUBLISHED_NAMES)))();
@@ -17502,6 +18124,7 @@ async function inspect(core, env, deps) {
17502
18124
  wanted.add(entry.package);
17503
18125
  }
17504
18126
  for (const name of Object.keys(installed)) wanted.add(name);
18127
+ for (const removed of removedGlobals) wanted.add(removed.package);
17505
18128
  const latest = await latestReleases([...wanted].sort(), deps.latestVersion ?? ((name) => npmLatestVersion(name, { env })));
17506
18129
  let config = null;
17507
18130
  try {
@@ -17603,16 +18226,19 @@ async function inspect(core, env, deps) {
17603
18226
  }
17604
18227
  const globals = [];
17605
18228
  for (const name of PUBLISHED_NAMES) {
17606
- const version = installed[name];
18229
+ const removed = removedGlobals.find((one) => one.package === name);
18230
+ const version = installed[name] ?? removed?.version;
17607
18231
  const newestRelease = latest[name];
17608
18232
  if (version === void 0 || newestRelease === void 0) continue;
18233
+ const reinstall = removed !== void 0 && !isBehind(version, newestRelease);
17609
18234
  const item = {
17610
18235
  kind: "global",
17611
18236
  package: name,
17612
18237
  version,
17613
- latest: newestRelease
18238
+ latest: newestRelease,
18239
+ ...reinstall ? { reinstall: true } : {}
17614
18240
  };
17615
- if (isBehind(version, newestRelease)) {
18241
+ if (reinstall || isBehind(version, newestRelease)) {
17616
18242
  behind.push(item);
17617
18243
  globals.push(item);
17618
18244
  } else upToDate.push(item);
@@ -17647,54 +18273,87 @@ async function inspect(core, env, deps) {
17647
18273
  */
17648
18274
  async function updateCheck(core, env, deps = {}, claim) {
17649
18275
  const { report } = await inspect(core, env, deps);
17650
- await recordFound(core, env, report, deps, null, claim, await releaseOf(report, env, deps));
18276
+ await recordFound(core, env, report, deps, null, claim, await releaseOf(core, report, env, deps));
17651
18277
  return report;
17652
18278
  }
17653
18279
  /**
17654
- * What the latest release's core manifest declares, for the daily check's file — or null, where it is not to be read
17655
- * or could not be: never a reason for the check to fail. Read only for a release newer than nothing — a counted one.
18280
+ * What the latest release's core manifest declares, for the daily check's file, as its verified tarball says (design
18281
+ * 2026-10-22 P1, §8.1): the facts when it was read; null when this check read it and it could not be read; and
18282
+ * undefined where this process reads none — a channel's, with no verifier, or a test's stand-in registry (P2). For
18283
+ * null and undefined alike the record keeps what core's verified check found of the same version, and otherwise makes
18284
+ * no claim. Never a reason for the check to fail.
17656
18285
  */
17657
- async function releaseOf(report, env, deps) {
18286
+ async function releaseOf(core, report, env, deps) {
17658
18287
  const latest = report.latest[CORE_PACKAGE$1];
17659
- if (latest === void 0 || !isPlainVersion(latest)) return null;
17660
- const read = deps.releaseFacts !== void 0 ? deps.releaseFacts : releaseFactsReader(env, deps);
17661
- if (read === null) return null;
18288
+ if (latest === void 0 || !isPlainVersion(latest)) return void 0;
18289
+ const read = deps.releaseFacts !== void 0 ? deps.releaseFacts : releaseFactsReader(env, deps, core.paths.stateDir);
18290
+ if (read === null) return void 0;
17662
18291
  try {
17663
- return {
18292
+ const facts = await read(CORE_PACKAGE$1, latest, oldestHere(report));
18293
+ return facts === null ? null : {
17664
18294
  version: latest,
17665
- ...await read(CORE_PACKAGE$1, latest, oldestHere(report))
18295
+ security: facts.security,
18296
+ needsDecision: facts.needsDecision
17666
18297
  };
17667
18298
  } catch {
17668
18299
  return null;
17669
18300
  }
17670
18301
  }
17671
18302
  /**
17672
- * The oldest release this machine runs, as the report saw it — what every release after it is read for: the core
17673
- * answering, and every registration, runtime and global package behind.
18303
+ * The oldest release this machine runs, as the report saw it — what a decision the latest carries is counted from: the
18304
+ * core answering, and every registration, runtime and global package behind.
17674
18305
  */
17675
18306
  function oldestHere(report) {
17676
18307
  return oldestVersion([report.core, ...report.behind.map((item) => item.version)]);
17677
18308
  }
17678
18309
  /**
17679
- * The daily check's and `update --check`'s reader of what releases declare (`declaredSince`): the registry handed in,
17680
- * or the one npm reads — but only beside the registry's own `latest`: a test with a stand-in `latestVersion` hands in
17681
- * `releaseFacts` itself, or none is read. With the verifier handed in, and none where none was: a channel's process
17682
- * carries no Sigstore, so what it reads counts for nothing. Each package's document is read once, however often asked.
17683
- */
17684
- function releaseFactsReader(env, deps) {
17685
- if (deps.latestVersion !== void 0) return null;
17686
- const read = deps.registry ?? npmRegistry(env);
17687
- const documents = /* @__PURE__ */ new Map();
17688
- const registry = (packageName) => {
17689
- const known = documents.get(packageName) ?? read(packageName);
17690
- documents.set(packageName, known);
17691
- return known;
18310
+ * The daily check's and `update --check`'s reader of what a release declares (design 2026-10-22 P1, §8.1): the release
18311
+ * verified, then its declaration read from its tarball and cached in `stateDir`; its facts for each machine's `from`,
18312
+ * or null when it could not be read (`factsOf`). Nothing of any other release is read: a needed decision is carried in
18313
+ * the release itself.
18314
+ *
18315
+ * Each release is read once however often it is asked — the check asks once for the core answering, and again for the
18316
+ * oldest release it finds here — and known or unknown, the answer stands for the reader's life: an unreadable tarball
18317
+ * is asked for at most twice in a check, at its location and at npm's own address (review B of 0.26.0, finding 2).
18318
+ * And the read is given `deadlineMs` at most (`DECLARATION_DEADLINE_MS` unless a test gives another), and stops at
18319
+ * `deadline` — a time, by `Date.now()` — when that comes first: the day's check passes its claim's time plus
18320
+ * `DECLARATION_DEADLINE_MS`, well inside the claim, however late its ask began. After it the read is unknown — never
18321
+ * false — and cut off, and one not started by then is not started at all, so the check records what it found while
18322
+ * its claim is still its own.
18323
+ *
18324
+ * The registry handed in, or the one npm reads — but only beside the registry's own `latest`: a test with a stand-in
18325
+ * `latestVersion` hands in `releaseFacts` itself, or none is read. And only with a verifier (P2): a channel's process
18326
+ * carries no Sigstore, so it reads nothing a release declares — no document, no tarball — and its check keeps what
18327
+ * core's verified check recorded.
18328
+ */
18329
+ function releaseFactsReader(env, deps, stateDir, options = {}) {
18330
+ const verify = deps.verifyRelease;
18331
+ if (deps.latestVersion !== void 0 || verify === void 0) return null;
18332
+ const sources = {
18333
+ registry: deps.registry ?? npmRegistry(env),
18334
+ verify,
18335
+ stateDir,
18336
+ tarballs: deps.tarballs
18337
+ };
18338
+ const declarations = /* @__PURE__ */ new Map();
18339
+ const declarationOf = (packageName, version) => {
18340
+ const key = `${packageName}@${version}`;
18341
+ let read = declarations.get(key);
18342
+ if (read === void 0) {
18343
+ const left = Math.min(options.deadlineMs ?? 3e4, options.deadline === void 0 ? Number.POSITIVE_INFINITY : options.deadline - Date.now());
18344
+ read = left > 0 ? withinDeadline((signal) => targetDeclaration(sources, packageName, version, { signal }), version, left) : Promise.resolve({
18345
+ known: false,
18346
+ version
18347
+ });
18348
+ declarations.set(key, read);
18349
+ }
18350
+ return read;
17692
18351
  };
17693
18352
  return async (packageName, version, from = null) => {
17694
- const { security, needsDecision } = await declaredSince(registry, deps.verifyRelease ?? null, packageName, from, version);
17695
- return {
17696
- security,
17697
- needsDecision
18353
+ const facts = factsOf(version, declaredFor(await declarationOf(packageName, version), from));
18354
+ return facts === null ? null : {
18355
+ security: facts.security,
18356
+ needsDecision: facts.needsDecision
17698
18357
  };
17699
18358
  };
17700
18359
  }
@@ -17730,25 +18389,34 @@ const registrationKey = (entry) => [
17730
18389
  * when this is that background check's own finding, is the claim it was made under: the finding is dropped when the
17731
18390
  * file no longer holds it, because something newer — a check a person asked for, an update — was written meanwhile.
17732
18391
  * The machine it describes was read before that, and put back it would say "update" where "restart" is right.
18392
+ *
18393
+ * Says how it went: `written`; `busy`, when the file's lock stayed held past its bound, so an update can try once
18394
+ * more (design 2026-10-22, R2); and `unchanged` for everything else — switched off, a claim given up, or a write that
18395
+ * failed for any other reason, each left as it always was.
17733
18396
  */
17734
18397
  async function recordFound(core, env, report, deps, after = null, claim, release) {
17735
18398
  const latest = report.latest[CORE_PACKAGE$1];
17736
- if (updateCheckSwitchedOff(env) !== null || latest === void 0 || !isVersion(latest)) return;
18399
+ if (updateCheckSwitchedOff(env) !== null || latest === void 0 || !isVersion(latest)) return "unchanged";
17737
18400
  const at = (deps.now ?? (() => /* @__PURE__ */ new Date()))().toISOString();
17738
18401
  const { behind, current } = updateCheckFindings(report, latest, after);
17739
18402
  const moves = movesLeft(report, after);
17740
- const facts = (record) => release !== void 0 ? release : record.release?.version === latest ? record.release : null;
17741
- await changeUpdateCheck(core.paths.stateDir, (record) => claim !== void 0 && record.checking !== claim ? null : { record: {
17742
- ...record,
17743
- lastChecked: at,
17744
- latest,
17745
- behind,
17746
- current,
17747
- lastError: null,
17748
- checking: null,
17749
- release: facts(record),
17750
- moves
17751
- } }).catch(() => void 0);
18403
+ const facts = (record) => release ?? (record.release?.version === latest ? record.release : null);
18404
+ try {
18405
+ const { written } = await changeUpdateCheck(core.paths.stateDir, (record) => claim !== void 0 && record.checking !== claim ? null : { record: {
18406
+ ...record,
18407
+ lastChecked: at,
18408
+ latest,
18409
+ behind,
18410
+ current,
18411
+ lastError: null,
18412
+ checking: null,
18413
+ release: facts(record),
18414
+ moves
18415
+ } });
18416
+ return written ? "written" : "unchanged";
18417
+ } catch (error) {
18418
+ return toCommsError(error).code === "LOCK_TIMEOUT" ? "busy" : "unchanged";
18419
+ }
17752
18420
  }
17753
18421
  /**
17754
18422
  * How many managed registrations still name a versioned runtime once `after` is done, for the notice (design
@@ -17798,7 +18466,7 @@ function updateCheckFindings(report, latest, after = null) {
17798
18466
  const globals = items.filter((item) => item.kind === "global");
17799
18467
  const global = CHANNELS.filter((channel) => {
17800
18468
  const name = serverFacts(channel).packageName;
17801
- return globals.some((item) => item.package === name && atLatest(installed.has(name) ? item.latest : item.version));
18469
+ return globals.some((item) => item.package === name && atLatest(installed.has(name) ? item.latest : item.reinstall ? null : item.version));
17802
18470
  });
17803
18471
  const stillBehind = after === null ? report.behind.length > 0 : !(after.ok && after.manual.length === 0 && after.status !== "manual");
17804
18472
  const unknown = report.unreadable.length > 0 || report.unpinned.length > 0;
@@ -17909,7 +18577,7 @@ async function decideNotice(options, where) {
17909
18577
  };
17910
18578
  };
17911
18579
  if (due(await readUpdateCheck(options.core.paths.stateDir), now()) === null) return null;
17912
- return (await changeUpdateCheck(options.core.paths.stateDir, (record) => {
18580
+ const decide = (record) => {
17913
18581
  const at = now();
17914
18582
  const decision = due(record, at);
17915
18583
  if (decision === null) return null;
@@ -17933,7 +18601,8 @@ async function decideNotice(options, where) {
17933
18601
  }
17934
18602
  }
17935
18603
  };
17936
- })).result ?? null;
18604
+ };
18605
+ return (await changeUpdateCheck(options.core.paths.stateDir, decide, UPDATE_NOTICE_LOCK)).result ?? null;
17937
18606
  }
17938
18607
  /**
17939
18608
  * What a background run left, when it is this process's to say: an install only to a process running a release before
@@ -18101,7 +18770,7 @@ async function securityNotice(options) {
18101
18770
  record: withSecurityShown(record, now, approvalId),
18102
18771
  result: now
18103
18772
  };
18104
- });
18773
+ }, UPDATE_NOTICE_LOCK);
18105
18774
  if (decided.result === void 0) return null;
18106
18775
  shown = decided.result;
18107
18776
  }
@@ -18257,7 +18926,7 @@ async function checkForUpdates(core, env, options = {}) {
18257
18926
  asked: false,
18258
18927
  record: await readUpdateCheck(stateDir)
18259
18928
  };
18260
- await askUnderClaim(core, env, claimedAt, options);
18929
+ if (!(options.handOff !== void 0 && options.deps === void 0 && await handToCore(core, env, claimedAt, options.handOff, Date.now() + 12e4))) await askUnderClaim(core, env, claimedAt, options);
18261
18930
  return {
18262
18931
  asked: true,
18263
18932
  record: await readUpdateCheck(stateDir)
@@ -18278,6 +18947,11 @@ async function checkForUpdates(core, env, options = {}) {
18278
18947
  * claim written before anyone asks, so of two processes that both found it a day old, one asks. The time is read under
18279
18948
  * the lock too: read before it, the second process's time could be earlier than the first's claim, which reads as a
18280
18949
  * clock gone back.
18950
+ *
18951
+ * A claim is best effort, as a notice is (`UPDATE_NOTICE_LOCK`): 2 s at most for the lock, however often it changes
18952
+ * hands meanwhile, then null, and a later process claims. Every command's and every tool call's check claims here,
18953
+ * before the person's output is done; waited out holder by holder, as a record that matters waits, a busy machine's
18954
+ * refreshes kept a finished command's process and its stdout open for up to 15 s (0.26.0 review B).
18281
18955
  */
18282
18956
  async function claimUpdateCheck(core, options = {}) {
18283
18957
  const now = options.now ?? (() => /* @__PURE__ */ new Date());
@@ -18294,7 +18968,7 @@ async function claimUpdateCheck(core, options = {}) {
18294
18968
  },
18295
18969
  result: checking
18296
18970
  };
18297
- }).catch(() => null))?.result ?? null;
18971
+ }, UPDATE_NOTICE_LOCK).catch(() => null))?.result ?? null;
18298
18972
  }
18299
18973
  /**
18300
18974
  * The ask, under a claim already taken (`claimUpdateCheck`): the registry, then `comms_update`'s own check. What it
@@ -18319,7 +18993,7 @@ async function ask(core, env, options) {
18319
18993
  */
18320
18994
  const settle = (found) => changeUpdateCheck(core.paths.stateDir, (record) => record.checking === options.claimedAt ? { record: {
18321
18995
  ...record,
18322
- ...found,
18996
+ ...typeof found === "function" ? found(record) : found,
18323
18997
  lastChecked: options.claimedAt,
18324
18998
  checking: null
18325
18999
  } } : null).catch(() => void 0);
@@ -18341,12 +19015,12 @@ async function ask(core, env, options) {
18341
19015
  });
18342
19016
  return;
18343
19017
  }
18344
- const read = deps.releaseFacts !== void 0 ? deps.releaseFacts : releaseFactsReader(env, deps);
18345
- const release = read === null ? null : await read(CORE_PACKAGE, latest, VERSION).then((facts) => ({
19018
+ const read = deps.releaseFacts !== void 0 ? deps.releaseFacts : releaseFactsReader(env, deps, core.paths.stateDir, { deadline: Date.parse(options.claimedAt) + DECLARATION_DEADLINE_MS });
19019
+ const release = read === null ? void 0 : await read(CORE_PACKAGE, latest, VERSION).then((facts) => facts === null ? null : {
18346
19020
  version: latest,
18347
19021
  security: facts.security,
18348
19022
  needsDecision: facts.needsDecision
18349
- }), () => null);
19023
+ }, () => null);
18350
19024
  try {
18351
19025
  await updateCheck(core, env, {
18352
19026
  ...deps,
@@ -18355,13 +19029,13 @@ async function ask(core, env, options) {
18355
19029
  now: options.now
18356
19030
  }, options.claimedAt);
18357
19031
  } catch (error) {
18358
- await settle({
19032
+ await settle((record) => ({
18359
19033
  latest,
18360
19034
  behind: null,
18361
19035
  current: null,
18362
19036
  lastError: reason(error),
18363
- release
18364
- });
19037
+ release: release ?? (record.release?.version === latest ? record.release : null)
19038
+ }));
18365
19039
  }
18366
19040
  }
18367
19041
  /**
@@ -18480,8 +19154,13 @@ function updateCheckChildEnvironment(env, platform = process.platform) {
18480
19154
  * message — or whatever reads that output would wait for the child as it waited for the check. Detached, so a
18481
19155
  * Ctrl-C at the terminal does not cut the ask short, and hidden, so Windows opens no console for it.
18482
19156
  *
18483
- * When the child cannot be started — no entry to start, a program not there or not allowed — the check is asked in
18484
- * this process instead, as it was before. It never throws.
19157
+ * A channel's command hands the ask to core's CLI first (design 2026-10-22, P2): core's command, located at the
19158
+ * channel's own version, with the same hidden command, the same claim and the same environment. Core's child verifies
19159
+ * what a release declares, which the channel's own could not.
19160
+ *
19161
+ * When the child cannot be started — no core, no entry to start, a program not there or not allowed, a `.cmd` that
19162
+ * would need a shell, a launcher whose Node was removed, which starts and ends at once — the next is tried: the
19163
+ * channel's own child, then the check in this process, as it was before. It never throws.
18485
19164
  */
18486
19165
  async function handUpdateCheckToChild(core, env, options) {
18487
19166
  const deadline = Date.now() + TERMINAL_CHECK_WAIT_MS;
@@ -18493,59 +19172,127 @@ async function handUpdateCheckToChild(core, env, options) {
18493
19172
  intervalMs: enabled.intervalMs
18494
19173
  });
18495
19174
  if (claimedAt === null) return;
19175
+ if (options.handOff !== void 0 && await handToCore(core, env, claimedAt, options.handOff, deadline)) return;
18496
19176
  const entry = options.entry === void 0 ? updateCheckChildEntry() : options.entry;
18497
- if (!(entry !== null && await startUpdateCheckChild(entry, claimedAt, core.paths, env, deadline))) await askUnderClaim(core, env, claimedAt, {
19177
+ if (!(entry !== null && await startedSafely(options.handOff?.start ?? startCheckChild, ownChildWords(entry, claimedAt, core.paths), updateCheckChildEnvironment(env), deadline))) await askUnderClaim(core, env, claimedAt, {
18498
19178
  now: options.now,
18499
19179
  ...options.verifyRelease ? { deps: { verifyRelease: options.verifyRelease } } : {}
18500
19180
  });
18501
19181
  } catch {}
18502
19182
  }
19183
+ /** This CLI again, as its child: the entry, every folder pinned, the hidden command and the claim. */
19184
+ function ownChildWords(entry, claimedAt, paths) {
19185
+ const pathArgs = PATH_OPTIONS.flatMap(({ key, flag }) => [flag, paths[key]]);
19186
+ return [
19187
+ entry.command,
19188
+ ...entry.args,
19189
+ ...pathArgs,
19190
+ UPDATE_CHECK_CHILD_COMMAND,
19191
+ claimedAt
19192
+ ];
19193
+ }
19194
+ /**
19195
+ * Core's own command for `update-check-child <claimedAt>`, as the words to start: `core.handoffs.core(…)` with every
19196
+ * folder pinned, located as the channel finds core — its installed dependency, at exactly the channel's own version,
19197
+ * through the stable way in a managed runtime keeps (`locateCliCommand`). Locating checks files and runs nothing.
19198
+ *
19199
+ * Located for this process's own system, whatever shell its printed commands are quoted for: these words are started
19200
+ * here, and a managed runtime's stable launcher is started one way on Windows and another elsewhere.
19201
+ *
19202
+ * Null for core's own process, which asks itself and is never handed its own check; for a core opened without its
19203
+ * caller, which can locate nothing; and where no such core is found.
19204
+ */
19205
+ function coreUpdateCheckWords(core, claimedAt) {
19206
+ if (core.handoffs === void 0 || core.handoffs.caller.packageName === CORE_PACKAGE) return null;
19207
+ try {
19208
+ const handoff = requireHandoffs(core, process.platform).core([UPDATE_CHECK_CHILD_COMMAND, claimedAt], { downloads: true });
19209
+ return isCommand(handoff) ? handoff.words : null;
19210
+ } catch {
19211
+ return null;
19212
+ }
19213
+ }
18503
19214
  /**
18504
- * Starts the child and waits, until `deadline` at most, for it to say the ask is over, or to end. False when it could
18505
- * not be started at all; then nothing was asked, and the claim is still this process's to ask under.
19215
+ * The ask handed to core's CLI (design 2026-10-22, P2): its command located (`handOff.locate`), started with the
19216
+ * claim and this command's child environment (`handOff.start`), and waited for until `deadline`. False when there is
19217
+ * no such command, or it did not start: then nothing was asked, and the claim is still this process's to ask under. It
19218
+ * never throws.
18506
19219
  */
18507
- async function startUpdateCheckChild(entry, claimedAt, paths, env, deadline) {
19220
+ async function handToCore(core, env, claimedAt, handOff, deadline) {
19221
+ let words;
19222
+ try {
19223
+ words = (handOff.locate ?? coreUpdateCheckWords)(core, claimedAt);
19224
+ } catch {
19225
+ return false;
19226
+ }
19227
+ if (words === null || words.length === 0) return false;
19228
+ return startedSafely(handOff.start ?? startCheckChild, words, updateCheckChildEnvironment(env), deadline);
19229
+ }
19230
+ /** A start's answer, with one that throws counted as not started: the fallback after it is all that is left. */
19231
+ async function startedSafely(start, words, env, deadline) {
19232
+ try {
19233
+ return await start(words, env, deadline) === true;
19234
+ } catch {
19235
+ return false;
19236
+ }
19237
+ }
19238
+ /**
19239
+ * Starts `words` — a program, then its arguments — detached and hidden, with no shell and `env` as given, and waits,
19240
+ * until `deadline` at most, for it to say the ask is over, or to end. False when it could not be started at all, or
19241
+ * when it ended with a code other than 0, or by a signal, before it said the ask is over — a launcher whose Node was
19242
+ * removed starts, and ends at once with 127 (0.26.0 review B). Either way nothing was asked, and the claim is still
19243
+ * this process's to ask under.
19244
+ *
19245
+ * Never through a shell: each word is one argument, as located, and nothing in them is read as a shell would read it.
19246
+ * A program only a shell could start — a Windows `.cmd` or `.bat`, which Node refuses without one (EINVAL) — is not
19247
+ * started, and the caller falls back.
19248
+ */
19249
+ async function startCheckChild(words, env, deadline) {
19250
+ const [command, ...args] = words;
19251
+ if (command === void 0) return false;
18508
19252
  let child;
18509
19253
  try {
18510
- const pathArgs = PATH_OPTIONS.flatMap(({ key, flag }) => [flag, paths[key]]);
18511
- child = spawn(entry.command, [
18512
- ...entry.args,
18513
- ...pathArgs,
18514
- UPDATE_CHECK_CHILD_COMMAND,
18515
- claimedAt
18516
- ], {
19254
+ child = spawn(command, args, {
18517
19255
  detached: true,
18518
19256
  windowsHide: true,
19257
+ shell: false,
18519
19258
  stdio: [
18520
19259
  "ignore",
18521
19260
  "ignore",
18522
19261
  "ignore",
18523
19262
  "ipc"
18524
19263
  ],
18525
- env: updateCheckChildEnvironment(env)
19264
+ env
18526
19265
  });
18527
19266
  } catch {
18528
19267
  return false;
18529
19268
  }
18530
- let failed = false;
18531
- child.on("error", () => {
18532
- if (child.pid === void 0) failed = true;
18533
- });
18534
- await new Promise((resolve) => {
18535
- const timer = setTimeout(resolve, Math.max(0, deadline - Date.now()));
18536
- const done = () => {
19269
+ child.on("error", () => void 0);
19270
+ const started = await new Promise((resolve) => {
19271
+ const timer = setTimeout(() => resolve(true), Math.max(0, deadline - Date.now()));
19272
+ const done = (started) => {
18537
19273
  clearTimeout(timer);
18538
- resolve();
19274
+ resolve(started);
18539
19275
  };
18540
- child.once("error", done);
18541
- child.once("exit", done);
19276
+ child.once("error", () => done(child.pid !== void 0));
18542
19277
  child.on("message", (message) => {
18543
- if (message?.type === "settled") done();
19278
+ if (message?.type === "settled") done(true);
19279
+ });
19280
+ let ended = null;
19281
+ const over = () => {
19282
+ if (ended !== null && !child.connected) done(ended.code === 0 && ended.signal === null);
19283
+ };
19284
+ child.once("exit", (code, signal) => {
19285
+ ended = {
19286
+ code,
19287
+ signal
19288
+ };
19289
+ over();
18544
19290
  });
19291
+ child.once("disconnect", over);
18545
19292
  });
18546
19293
  if (child.connected) child.disconnect();
18547
19294
  child.unref();
18548
- return !failed;
19295
+ return started;
18549
19296
  }
18550
19297
  /**
18551
19298
  * The hidden command's work, in the child a command started (`UPDATE_CHECK_CHILD_COMMAND`): the ask, under the claim
@@ -18557,7 +19304,7 @@ async function startUpdateCheckChild(entry, claimedAt, paths, env, deadline) {
18557
19304
  async function runUpdateCheckChild(core, env, claimedAt, options = {}) {
18558
19305
  if (claimedAt === void 0 || !Number.isFinite(Date.parse(claimedAt))) throw new CommsError("USAGE", `${UPDATE_CHECK_CHILD_COMMAND} takes the time of the claim it asks under`, { hint: handoffSentence(requireHandoffs(core).core(["update", "--check"]), (command) => `Nothing runs this but a command finishing the day’s update check; ${command} asks now.`) });
18559
19306
  try {
18560
- if ((await readUpdateCheck(core.paths.stateDir)).checking === claimedAt) await askUnderClaim(core, env, claimedAt, options.verifyRelease ? { deps: { verifyRelease: options.verifyRelease } } : {});
19307
+ if ((await readUpdateCheck(core.paths.stateDir)).checking === claimedAt) await askUnderClaim(core, env, claimedAt, options.deps !== void 0 ? { deps: options.deps } : {});
18561
19308
  } finally {
18562
19309
  settled();
18563
19310
  }
@@ -18586,12 +19333,13 @@ function terminalUpdateHooks(core, env, options = {}) {
18586
19333
  await handUpdateCheckToChild(core, env, {
18587
19334
  now: options.now,
18588
19335
  entry: options.childEntry,
18589
- verifyRelease: options.verifyRelease
19336
+ verifyRelease: options.verifyRelease,
19337
+ handOff: options.handOff
18590
19338
  });
18591
19339
  } };
18592
19340
  }
18593
19341
  //#endregion
18594
- //#region ../core/dist/tool-arguments-5t8Xd2eK.mjs
19342
+ //#region ../core/dist/tool-arguments-C8Oy4uOx.mjs
18595
19343
  const APPLIED = Symbol.for("agentcomms.strictToolArguments");
18596
19344
  const TARGET = "draft-2020-12";
18597
19345
  /**