@agentcomms/whatsapp 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.
package/dist/cli.mjs CHANGED
@@ -1,5 +1,5 @@
1
1
  #!/usr/bin/env node
2
- import { $ as gatedChangeAtTerminal, A as requireAccount, B as PATH_OPTIONS, Ct as CALLER, F as expandSource, G as approveChangeAtTerminal, H as absoluteSearchPath, J as cliHandoffs, K as canPrompt, L as CHANNEL_SERVERS, M as storeIdentity, N as checkStorePath, O as PLATFORM, P as defaultStorePath, Q as exemptFromUpdateNotice, R as CommsError, S as chatRefOf, St as writeResult, Tt as VERSION, U as agentMarker, W as approveAndWaitSentence, X as commandPathOf, Y as colorEnabled, Z as escapeForDisplay, _ as WhatsAppContext, _t as truncateDisplay, a as CHAT_KINDS, at as nameAvailable, b as requireSupportedNode, bt as windowsSystemProgram, c as searchMessages, ct as paint, dt as renderPrune, et as handoffSentence, f as draftMessage, ft as resolvePaths, ht as serverPruneChange, i as probeStore, it as lookupName, j as requireNamedConfig, k as newWhatsAppAccount, l as Presenter, lt as pathOverridesFromCliOptions, m as listKey, mt as serverInstallChange, nt as installExitStatus, o as listChats, ot as nameShapeProblem, p as Visibility, pt as runCommand, q as childEnvironment, r as whatsappStatus, rt as isCommsError, s as readChat, st as openCore, t as syncAccount, tt as handoffText, u as innerText, ut as renderInstall, v as WhatsAppIndex, vt as updateNoticeAtTerminal, xt as withFileLock, z as EXIT_CODES } from "./sync-DYq2CGVj.mjs";
2
+ import { $ as gatedChangeAtTerminal, A as requireAccount, B as PATH_OPTIONS, Ct as CALLER, F as expandSource, G as approveChangeAtTerminal, H as absoluteSearchPath, J as cliHandoffs, K as canPrompt, L as CHANNEL_SERVERS, M as storeIdentity, N as checkStorePath, O as PLATFORM, P as defaultStorePath, Q as exemptFromUpdateNotice, R as CommsError, S as chatRefOf, St as writeResult, Tt as VERSION, U as agentMarker, W as approveAndWaitSentence, X as commandPathOf, Y as colorEnabled, Z as escapeForDisplay, _ as WhatsAppContext, _t as truncateDisplay, a as CHAT_KINDS, at as nameAvailable, b as requireSupportedNode, bt as windowsSystemProgram, c as searchMessages, ct as paint, dt as renderPrune, et as handoffSentence, f as draftMessage, ft as resolvePaths, ht as serverPruneChange, i as probeStore, it as lookupName, j as requireNamedConfig, k as newWhatsAppAccount, l as Presenter, lt as pathOverridesFromCliOptions, m as listKey, mt as serverInstallChange, nt as installExitStatus, o as listChats, ot as nameShapeProblem, p as Visibility, pt as runCommand, q as childEnvironment, r as whatsappStatus, rt as isCommsError, s as readChat, st as openCore, t as syncAccount, tt as handoffText, u as innerText, ut as renderInstall, v as WhatsAppIndex, vt as updateNoticeAtTerminal, xt as withFileLock, z as EXIT_CODES } from "./sync-BwJSBi-M.mjs";
3
3
  import { rm } from "node:fs/promises";
4
4
  import path, { delimiter, join } from "node:path";
5
5
  import { stripVTControlCharacters } from "node:util";
@@ -3569,7 +3569,7 @@ a macOS dialog may be waiting) · 77 permission needed (macOS privacy) · 78 con
3569
3569
  ran = true;
3570
3570
  exitCode = await runCommand(output(), async () => {
3571
3571
  requireSupportedNode();
3572
- await (deps.startMcp ?? (await import("./stdio-entry-pwRwRiwa.mjs")).startWhatsAppStdioServer)({
3572
+ await (deps.startMcp ?? (await import("./stdio-entry-DQ1RgYGk.mjs")).startWhatsAppStdioServer)({
3573
3573
  env,
3574
3574
  platform,
3575
3575
  ...deps,
package/dist/index.d.mts CHANGED
@@ -4277,7 +4277,7 @@ declare namespace external_d_exports {
4277
4277
  export { $brand, $input, $output, BRAND, CompileOptions, GlobalMeta, INVALID, Infer$1 as Infer, IssueData, JSONType, NEVER, $RefinementCtx as RefinementCtx, SafeExtendShape, Schema, TimePrecision, TypeOf, ZodAny, ZodArray, ZodBase64, ZodBase64URL, ZodBigInt, ZodBigIntFormat, ZodBoolean, ZodCIDRv4, ZodCIDRv6, ZodCUID, ZodCUID2, ZodCatch, ZodCodec, ZodCoercedBigInt, ZodCoercedBoolean, ZodCoercedDate, ZodCoercedNumber, ZodCoercedString, ZodCompileAsyncError, ZodCompileUnsupportedError, ZodCreditCard, ZodCustom, ZodCustomStringFormat, ZodDate, ZodDefault, ZodDiscriminatedUnion, ZodE164, ZodEmail, ZodEmoji, ZodEnum, ZodError, ZodErrorMap, ZodExactOptional, ZodFile, ZodFirstPartySchemaTypes, ZodFirstPartyTypeKind, ZodFlattenedError, ZodFloat32, ZodFloat64, ZodFormattedError, ZodFunction, ZodGUID, ZodIBAN, ZodIPv4, ZodIPv6, ZodISODate, ZodISODateTime, ZodISODuration, ZodISOTime, ZodInstanceOf, ZodInt, ZodInt32, ZodInt64, ZodIntersection, ZodIssue, ZodIssueCode, ZodJSONSchema, ZodJSONSchemaInternals, ZodJWT, ZodKSUID, ZodLazy, ZodLiteral, ZodMAC, ZodMap, ZodNaN, ZodNanoID, ZodNever, ZodNonOptional, ZodNull, ZodNullable, ZodNumber, ZodNumberFormat, ZodObject, ZodOptional, ZodPipe, ZodPrefault, ZodPreprocess, ZodPromise, ZodRawShape$1 as ZodRawShape, ZodReadonly, ZodRealError, ZodRecord, ZodSafeParseError, ZodSafeParseResult, ZodSafeParseSuccess, ZodSchema, ZodSet, ZodStandardSchemaWithJSON, ZodString, ZodStringFormat, ZodSuccess, ZodSymbol, ZodTemplateLiteral, ZodTransform, ZodTuple, ZodType, ZodTypeAny, ZodUInt32, ZodUInt64, ZodULID, ZodURL, ZodUUID, ZodUndefined, ZodUnion, ZodUnknown, ZodVoid, ZodXID, ZodXor, _ZodBigInt, _ZodBoolean, _ZodDate, _ZodNumber, _ZodString, _ZodType, _default, _function, any, array, base64, base64url, bigint$1 as bigint, boolean$1 as boolean, _catch as catch, check, cidrv4, cidrv6, clone, codec, coerce_d_exports as coerce, compile, config, index_d_exports$1 as core, creditCard, cuid, cuid2, currencyCode, custom, date$2 as date, decode, decodeAsync, deepPartial, describe, discriminatedUnion, e164, email, emoji, encode, encodeAsync, _endsWith as endsWith, _enum as enum, exactOptional, file, flattenError, float32, float64, formatError, fromJSONSchema, _function as function, getDiscriminatedOption, getErrorMap, globalRegistry, _gt as gt, _gte as gte, guid, hash, hex, hostname, httpUrl, iban, _includes as includes, output$1 as infer, inferFlattenedErrors, inferFormattedError, input, _instanceof as instanceof, int, int32, int64, intersection, invertCodec, ipv4, ipv6, iso_d_exports as iso, json, jwt, keyof, ksuid, lazy, _length as length, literal, index_d_exports$2 as locales, looseObject, looseRecord, _lowercase as lowercase, _lt as lt, _lte as lte, mac, map, _maxLength as maxLength, _maxSize as maxSize, memoizer, meta, _mime as mime, _minLength as minLength, _minSize as minSize, _multipleOf as multipleOf, nan, nanoid, nativeEnum, _negative as negative, never, _nonnegative as nonnegative, nonoptional, _nonpositive as nonpositive, _normalize as normalize, _null as null, nullable, nullish, number$1 as number, object, optional, output, _overwrite as overwrite, parse, parseAsync, partialRecord, pipe, _positive as positive, prefault, preprocess, prettifyError, promise, _properties as properties, _property as property, readonly, record, refine, _regex as regex, regexes_d_exports as regexes, registry, safeDecode, safeDecodeAsync, safeEncode, safeEncodeAsync, safeParse, safeParseAsync, set, setErrorMap, _size as size, _slugify as slugify, _startsWith as startsWith, strictObject, string$1 as string, stringFormat, stringbool, success, superRefine, symbol, templateLiteral, toJSONSchema, _toLowerCase as toLowerCase, _toUpperCase as toUpperCase, toZod, transform, treeifyError, _trim as trim, tuple, uint32, uint64, ulid, _undefined as undefined, union, unknown, _uppercase as uppercase, url, util_d_exports as util, uuid, uuidv4, uuidv6, uuidv7, validate, validateAsync, _void as void, withParser, xid, xor };
4278
4278
  }
4279
4279
  //#endregion
4280
- //#region ../core/dist/launcher-B8pYW7eG.d.mts
4280
+ //#region ../core/dist/launcher-PMDWJUz9.d.mts
4281
4281
  interface RegisteredServer {
4282
4282
  client: string;
4283
4283
  path: string;
package/dist/index.mjs CHANGED
@@ -1,5 +1,5 @@
1
- import { C as chatKindOf, D as text, E as numeric, R as CommsError, T as kindOf, Tt as VERSION, _ as WhatsAppContext, d as composeDraft, g as visibilityUnitKey, h as statusAuthor, m as listKey, n as withCheckedCopy, p as Visibility, w as coreDataToIso, wt as PACKAGE_NAME, x as inspectSchema, y as rebuildIndex } from "./sync-DYq2CGVj.mjs";
2
- import { t as createWhatsAppMcpServer } from "./server-Btcjiqz2.mjs";
1
+ import { C as chatKindOf, D as text, E as numeric, R as CommsError, T as kindOf, Tt as VERSION, _ as WhatsAppContext, d as composeDraft, g as visibilityUnitKey, h as statusAuthor, m as listKey, n as withCheckedCopy, p as Visibility, w as coreDataToIso, wt as PACKAGE_NAME, x as inspectSchema, y as rebuildIndex } from "./sync-BwJSBi-M.mjs";
2
+ import { t as createWhatsAppMcpServer } from "./server-ubl3VinK.mjs";
3
3
  //#region src/source/event-reader.ts
4
4
  function rawFromMe(value) {
5
5
  const number = numeric(value);
@@ -1,4 +1,4 @@
1
- import { I as strictToolArguments, Tt as VERSION, V as UNTRUSTED_NOTICE, _ as WhatsAppContext, a as CHAT_KINDS, c as searchMessages, f as draftMessage, gt as toCommsError, o as listChats, r as whatsappStatus, s as readChat, t as syncAccount, yt as updateToolNotice } from "./sync-DYq2CGVj.mjs";
1
+ import { I as strictToolArguments, Tt as VERSION, V as UNTRUSTED_NOTICE, _ as WhatsAppContext, a as CHAT_KINDS, c as searchMessages, f as draftMessage, gt as toCommsError, o as listChats, r as whatsappStatus, s as readChat, t as syncAccount, yt as updateToolNotice } from "./sync-BwJSBi-M.mjs";
2
2
  import { c as boolean, h as number, x as string } from "./iso-USuVU_sk.mjs";
3
3
  import { t as McpServer } from "./mcp-DIH4cS6P-CIWWZQbz.mjs";
4
4
  //#region src/mcp/server.ts
@@ -1,4 +1,4 @@
1
- import { t as createWhatsAppMcpServer } from "./server-Btcjiqz2.mjs";
1
+ import { t as createWhatsAppMcpServer } from "./server-ubl3VinK.mjs";
2
2
  //#region src/mcp/stdio-entry.ts
3
3
  /**
4
4
  * Starting the stdio server. On stdio, stdout is the protocol, so the console is redirected to stderr first — the
@@ -11,7 +11,7 @@ import { execFile, spawn } from "node:child_process";
11
11
  //#endregion
12
12
  //#region src/version.ts
13
13
  /** The package version, read from package.json at build time. */
14
- const VERSION$2 = "0.24.0";
14
+ const VERSION$2 = "0.26.0";
15
15
  //#endregion
16
16
  //#region src/caller.ts
17
17
  /** This package's name, as its manifest has it. */
@@ -28,7 +28,7 @@ const CALLER = Object.freeze({
28
28
  packageName: PACKAGE_NAME
29
29
  });
30
30
  //#endregion
31
- //#region ../core/dist/update-check-BY5xNUZa.mjs
31
+ //#region ../core/dist/update-check-C3j1VxDg.mjs
32
32
  /**
33
33
  * Every failure a user or an agent can act on carries a specific, stable code from this registry, a message, an
34
34
  * optional one-line hint and structured `details`. The code decides the process exit status; agents and skills branch
@@ -7157,6 +7157,24 @@ function parseConfig(text, source = "config.json") {
7157
7157
  if (!parsed.success) throw new CommsError("CONFIG", `${source} is invalid: ${describeIssues(parsed.error, naming === 1 ? 1 : 2)}`);
7158
7158
  return parsed.data;
7159
7159
  }
7160
+ /**
7161
+ * How a config write waits for the config lock: five seconds for each holder, thirty seconds in all.
7162
+ *
7163
+ * The lock is a queue. Every send prepare, claim and approval on every channel runs `ensureSendEpochConfig` first, and
7164
+ * while a legacy drain is open that is a config write or two per call, so an agent's parallel calls across several MCP
7165
+ * servers arrive at this lock together, each holding it for one read, check and fsynced rename (and the drain's, for a
7166
+ * list of the approvals too). Counted from when each waiter began, the lock's five seconds were a budget for the whole
7167
+ * line ahead: on the Windows runner a line of twelve writes gave up at the ninth, "another agent-communications process
7168
+ * is holding", while the lock was being handed on exactly as it should be. A holder there took about half a second, and
7169
+ * on Windows a rename alone may retry for one. Counted per holder, as Resend's throttle and Gmail's history caches count
7170
+ * theirs, a write gives up only on a holder that keeps the lock for the whole five seconds: the stuck lock the timeout is
7171
+ * for. The lock is polled rather than queued, so thirty seconds in all bounds the rest, and leaves room inside the minute
7172
+ * an MCP client waits for a tool call.
7173
+ */
7174
+ const CONFIG_WRITE_LOCK = {
7175
+ timeoutPerHolder: true,
7176
+ maxWaitMs: 3e4
7177
+ };
7160
7178
  var ConfigStore = class {
7161
7179
  path;
7162
7180
  #lockPath;
@@ -7177,6 +7195,10 @@ var ConfigStore = class {
7177
7195
  #namesMigrateAgain(why) {
7178
7196
  return handoffSentence(requiredHandoffs(this.#handoffs).core(["names", "migrate"]), (command) => `Run ${command} again ${why}.`, { instead: `Call comms_names_migrate again ${why}.` });
7179
7197
  }
7198
+ /** Runs `fn` holding the config lock, waited for as a write waits for it (`CONFIG_WRITE_LOCK`). */
7199
+ #withWriteLock(fn) {
7200
+ return withFileLock(this.#lockPath, fn, CONFIG_WRITE_LOCK);
7201
+ }
7180
7202
  /** The current config; an empty one when the file does not exist yet. */
7181
7203
  async load() {
7182
7204
  let key;
@@ -7203,7 +7225,7 @@ var ConfigStore = class {
7203
7225
  let committed;
7204
7226
  let result;
7205
7227
  try {
7206
- result = await withFileLock(this.#lockPath, async () => {
7228
+ result = await this.#withWriteLock(async () => {
7207
7229
  options.signal?.throwIfAborted();
7208
7230
  this.#cache = null;
7209
7231
  const current = structuredClone(await this.load());
@@ -12004,7 +12026,7 @@ function folding(platform) {
12004
12026
  return platform === "win32" ? (text) => text.toLowerCase() : (text) => text;
12005
12027
  }
12006
12028
  /** The package version, read from package.json at build time. */
12007
- const VERSION = "0.24.0";
12029
+ const VERSION = "0.26.0";
12008
12030
  /**
12009
12031
  * 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).
12010
12032
  *
@@ -13298,7 +13320,7 @@ const PER_WORKSPACE = {
13298
13320
  "agentLabel.slackStyles": "app",
13299
13321
  "agentLabel.slackIcons": "your Slack photo"
13300
13322
  };
13301
- function recordOf$1(value) {
13323
+ function recordOf$2(value) {
13302
13324
  return typeof value === "object" && value !== null && !Array.isArray(value) ? value : void 0;
13303
13325
  }
13304
13326
  /**
@@ -13308,7 +13330,7 @@ function recordOf$1(value) {
13308
13330
  */
13309
13331
  function perWorkspace(field, before, after) {
13310
13332
  const none = PER_WORKSPACE[field];
13311
- const [was, now] = [recordOf$1(before ?? {}), recordOf$1(after ?? {})];
13333
+ const [was, now] = [recordOf$2(before ?? {}), recordOf$2(after ?? {})];
13312
13334
  if (none === void 0 || was === void 0 || now === void 0) return void 0;
13313
13335
  const value = (record, name) => Object.hasOwn(record, name) ? String(record[name]) : none;
13314
13336
  const moved = [.../* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(now)])].filter((name) => value(was, name) !== value(now, name)).sort();
@@ -13321,7 +13343,7 @@ function perWorkspace(field, before, after) {
13321
13343
  * commas. A level one side does not list allows everything there. Undefined for a value of another shape.
13322
13344
  */
13323
13345
  function perLevel(field, before, after) {
13324
- const [was, now] = [recordOf$1(before ?? {}), recordOf$1(after ?? {})];
13346
+ const [was, now] = [recordOf$2(before ?? {}), recordOf$2(after ?? {})];
13325
13347
  if (field !== "behaviour.rules" || was === void 0 || now === void 0) return void 0;
13326
13348
  const value = (record, level) => Object.hasOwn(record, level) ? String(record[level]) : "everything";
13327
13349
  const levels = [.../* @__PURE__ */ new Set([...Object.keys(was), ...Object.keys(now)])].sort();
@@ -13896,7 +13918,7 @@ const OUTCOMES = [
13896
13918
  "unknown"
13897
13919
  ];
13898
13920
  const isString = (value) => typeof value === "string";
13899
- const isTime$2 = (value) => isString(value) && ISO.test(value) && !Number.isNaN(Date.parse(value));
13921
+ const isTime$3 = (value) => isString(value) && ISO.test(value) && !Number.isNaN(Date.parse(value));
13900
13922
  /** One line as an entry, or null when it is not one this release wrote. */
13901
13923
  function entryOf$2(line) {
13902
13924
  let raw;
@@ -13907,7 +13929,7 @@ function entryOf$2(line) {
13907
13929
  }
13908
13930
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) return null;
13909
13931
  const entry = raw;
13910
- if (!isTime$2(entry.at)) return null;
13932
+ if (!isTime$3(entry.at)) return null;
13911
13933
  if (entry.kind === "use") {
13912
13934
  if (![
13913
13935
  "standingId",
@@ -13939,7 +13961,7 @@ function entryOf$2(line) {
13939
13961
  if (entry.kind === "outcome") {
13940
13962
  if (!isString(entry.childId) || !isString(entry.outcome) || !OUTCOMES.includes(entry.outcome)) return null;
13941
13963
  if (!isString(entry.notice) || entry.link !== void 0 && !isString(entry.link)) return null;
13942
- if (entry.actAt !== void 0 && !isTime$2(entry.actAt)) return null;
13964
+ if (entry.actAt !== void 0 && !isTime$3(entry.actAt)) return null;
13943
13965
  return {
13944
13966
  kind: "outcome",
13945
13967
  at: entry.at,
@@ -14384,7 +14406,7 @@ function updateJobSupported(platform) {
14384
14406
  function updateJobMarkerPath(stateDir) {
14385
14407
  return join(stateDir, "update-job.json");
14386
14408
  }
14387
- const isTime$1 = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
14409
+ const isTime$2 = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
14388
14410
  /** The record, or null when there is none — or none this release can read, which is treated as none. */
14389
14411
  async function readUpdateJobMarker(stateDir) {
14390
14412
  let parsed;
@@ -14399,11 +14421,11 @@ async function readUpdateJobMarker(stateDir) {
14399
14421
  platform: record.platform,
14400
14422
  installedAt: record.installedAt,
14401
14423
  label: record.label,
14402
- ...isTime$1(record.lastRun) ? { lastRun: record.lastRun } : {}
14424
+ ...isTime$2(record.lastRun) ? { lastRun: record.lastRun } : {}
14403
14425
  };
14404
14426
  }
14405
14427
  function within$1(at, now, intervalMs) {
14406
- if (at === void 0 || !isTime$1(at)) return false;
14428
+ if (at === void 0 || !isTime$2(at)) return false;
14407
14429
  const since = now.getTime() - Date.parse(at);
14408
14430
  return since >= -3e5 && since < intervalMs;
14409
14431
  }
@@ -14484,7 +14506,7 @@ const NOTICE_OUTCOMES = [
14484
14506
  function readBackgroundNotice(value) {
14485
14507
  if (typeof value !== "object" || value === null) return null;
14486
14508
  const { at, outcome, version, text } = value;
14487
- if (!isTime(at) || !NOTICE_OUTCOMES.includes(outcome) || typeof text !== "string") return null;
14509
+ if (!isTime$1(at) || !NOTICE_OUTCOMES.includes(outcome) || typeof text !== "string") return null;
14488
14510
  return {
14489
14511
  at,
14490
14512
  outcome,
@@ -14513,7 +14535,7 @@ const EMPTY_UPDATE_CHECK = Object.freeze({
14513
14535
  function updateCheckPath(stateDir) {
14514
14536
  return join(stateDir, UPDATE_CHECK_FILE);
14515
14537
  }
14516
- const isTime = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
14538
+ const isTime$1 = (value) => typeof value === "string" && Number.isFinite(Date.parse(value));
14517
14539
  const isWords = (value) => Array.isArray(value) && value.every((word) => typeof word === "string");
14518
14540
  function readRelease(value) {
14519
14541
  if (typeof value !== "object" || value === null) return null;
@@ -14546,17 +14568,17 @@ async function readUpdateCheck(stateDir) {
14546
14568
  }
14547
14569
  const held = typeof parsed === "object" && parsed !== null ? parsed : {};
14548
14570
  return {
14549
- lastChecked: isTime(held.lastChecked) ? held.lastChecked : null,
14571
+ lastChecked: isTime$1(held.lastChecked) ? held.lastChecked : null,
14550
14572
  latest: isPlainVersion(held.latest) ? held.latest : null,
14551
14573
  behind: typeof held.behind === "boolean" ? held.behind : null,
14552
14574
  current: readCurrent(held.current),
14553
14575
  lastError: typeof held.lastError === "string" ? held.lastError.slice(0, 300) : null,
14554
- snoozedUntil: isTime(held.snoozedUntil) ? held.snoozedUntil : null,
14555
- checking: isTime(held.checking) ? held.checking : null,
14556
- commandNoticeAt: isTime(held.commandNoticeAt) ? held.commandNoticeAt : null,
14557
- noticeAt: isTime(held.noticeAt) ? held.noticeAt : null,
14558
- pausedAt: isTime(held.pausedAt) ? held.pausedAt : null,
14559
- pausedUntil: isTime(held.pausedUntil) ? held.pausedUntil : null,
14576
+ snoozedUntil: isTime$1(held.snoozedUntil) ? held.snoozedUntil : null,
14577
+ checking: isTime$1(held.checking) ? held.checking : null,
14578
+ commandNoticeAt: isTime$1(held.commandNoticeAt) ? held.commandNoticeAt : null,
14579
+ noticeAt: isTime$1(held.noticeAt) ? held.noticeAt : null,
14580
+ pausedAt: isTime$1(held.pausedAt) ? held.pausedAt : null,
14581
+ pausedUntil: isTime$1(held.pausedUntil) ? held.pausedUntil : null,
14560
14582
  release: readRelease(held.release),
14561
14583
  securityAsked: isPlainVersion(held.securityAsked) ? held.securityAsked : null,
14562
14584
  securityShown: readSecurityShown(held.securityShown),
@@ -14565,11 +14587,41 @@ async function readUpdateCheck(stateDir) {
14565
14587
  };
14566
14588
  }
14567
14589
  /**
14590
+ * 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).
14591
+ *
14592
+ * Every server's tool call takes this lock for its refresh, so on a busy machine a writer often waits behind several
14593
+ * holders in turn, each holding it for a moment. Counted from the start of the wait, 2 s was a budget for that whole
14594
+ * queue, and a writer at the back of it gave up while the lock was being handed on exactly as it should be: a
14595
+ * person's update was installed and its record dropped. Counted per holder, a writer gives up only on one holder that
14596
+ * kept the lock for 2 s, which is a stuck lock; 15 s bounds a queue that never empties, as config writes are bounded.
14597
+ */
14598
+ const UPDATE_CHECK_LOCK = Object.freeze({
14599
+ timeoutPerHolder: true,
14600
+ timeoutMs: 2e3,
14601
+ maxWaitMs: 15e3
14602
+ });
14603
+ /**
14604
+ * How a notice takes the lock: 2 s in all, counted from the start of the wait, however often it changes hands meanwhile.
14605
+ *
14606
+ * A notice is best effort. Deciding a tool call's or a command's line, the security line in a preview, and recording
14607
+ * the person's answer to that line take the lock only for what is said, and each runs while a person or an agent
14608
+ * waits. Waited out holder by holder, as `UPDATE_CHECK_LOCK` waits, a busy machine's queue of refreshes held them for
14609
+ * up to 15 s. Busy past 2 s, a notice decides nothing this time, and a later call says it.
14610
+ *
14611
+ * 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
14612
+ * review B). The claim's function is not named here: this comment ships in WhatsApp's bundle, which carries none of the
14613
+ * checker, not even its name (`packages/whatsapp/test/update-gate.test.ts`).
14614
+ */
14615
+ const UPDATE_NOTICE_LOCK = Object.freeze({ timeoutMs: 2e3 });
14616
+ /**
14568
14617
  * Reads the record, changes it and writes it back, under a lock beside it, atomically — so a server recording a check
14569
14618
  * and a person's "not now" arriving at the same moment each keep what the other wrote. Returns what `change` returned
14570
14619
  * alongside the record written; `change` returning `null` writes nothing.
14620
+ *
14621
+ * `lock` is how long to wait for the lock: `UPDATE_CHECK_LOCK`, for a record that matters, unless a notice or the
14622
+ * day's claim passes `UPDATE_NOTICE_LOCK`. Busy past it, this throws LOCK_TIMEOUT, and nothing is written.
14571
14623
  */
14572
- async function changeUpdateCheck(stateDir, change) {
14624
+ async function changeUpdateCheck(stateDir, change, lock = UPDATE_CHECK_LOCK) {
14573
14625
  return withFileLock(join(stateDir, ".update-check.lock"), async () => {
14574
14626
  const current = await readUpdateCheck(stateDir);
14575
14627
  const next = change(current);
@@ -14584,7 +14636,7 @@ async function changeUpdateCheck(stateDir, change) {
14584
14636
  result: next.result,
14585
14637
  written: true
14586
14638
  };
14587
- }, { timeoutMs: 2e3 });
14639
+ }, lock);
14588
14640
  }
14589
14641
  /**
14590
14642
  * Whether the check is due: never asked, asked a day or more ago, or — a clock that went back — asked more than a few
@@ -14823,7 +14875,7 @@ async function securityAnswered(stateDir, approvalId) {
14823
14875
  securityAsked: record.securityShown.version,
14824
14876
  securityShown: null
14825
14877
  } };
14826
- });
14878
+ }, UPDATE_NOTICE_LOCK);
14827
14879
  } catch {}
14828
14880
  }
14829
14881
  /** A local time as a person reads it in a preview: `2026-09-29 00:00`. */
@@ -15488,6 +15540,7 @@ function serverInstallChange(core, env, request, own, deps = {}) {
15488
15540
  });
15489
15541
  const unverifiedOwn = fetches && deps.verifyRelease === void 0 && running !== void 0 && running.packageName === product.packageName && running.version === version;
15490
15542
  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;
15543
+ if (request.print !== true && launcher === "npx" && target.writes && deps.verifyRelease !== void 0) await verifyReleaseSet(from, /* @__PURE__ */ new Map([[facts.npxPackage, version]]), "install");
15491
15544
  planned = {
15492
15545
  product,
15493
15546
  install: plannedInstall(preflight),
@@ -15667,7 +15720,7 @@ function serverPruneChange(core, env, request, own) {
15667
15720
  };
15668
15721
  const { packageName, version } = ownProduct(request.channel, own) ?? {
15669
15722
  packageName: serverFacts(request.channel).packageName,
15670
- version: "0.24.0"
15723
+ version: "0.26.0"
15671
15724
  };
15672
15725
  const product = {
15673
15726
  packageName,
@@ -15949,7 +16002,7 @@ async function decideNotice(options, where) {
15949
16002
  };
15950
16003
  };
15951
16004
  if (due(await readUpdateCheck(options.core.paths.stateDir), now()) === null) return null;
15952
- return (await changeUpdateCheck(options.core.paths.stateDir, (record) => {
16005
+ const decide = (record) => {
15953
16006
  const at = now();
15954
16007
  const decision = due(record, at);
15955
16008
  if (decision === null) return null;
@@ -15973,7 +16026,8 @@ async function decideNotice(options, where) {
15973
16026
  }
15974
16027
  }
15975
16028
  };
15976
- })).result ?? null;
16029
+ };
16030
+ return (await changeUpdateCheck(options.core.paths.stateDir, decide, UPDATE_NOTICE_LOCK)).result ?? null;
15977
16031
  }
15978
16032
  /**
15979
16033
  * What a background run left, when it is this process's to say: an install only to a process running a release before
@@ -16206,7 +16260,7 @@ function commandDecisionLine(core, decision, channel, platform) {
16206
16260
  }
16207
16261
  channelServer("core").packageName;
16208
16262
  //#endregion
16209
- //#region ../core/dist/tool-arguments-5t8Xd2eK.mjs
16263
+ //#region ../core/dist/tool-arguments-C8Oy4uOx.mjs
16210
16264
  const APPLIED = Symbol.for("agentcomms.strictToolArguments");
16211
16265
  const TARGET = "draft-2020-12";
16212
16266
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agentcomms/whatsapp",
3
- "version": "0.24.0",
3
+ "version": "0.26.0",
4
4
  "description": "WhatsApp for coding agents, read-only: list, read and search the chats WhatsApp for Mac keeps on this Mac, and draft replies as links the person sends. No network client; it never sends.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -34,7 +34,7 @@
34
34
  "access": "public"
35
35
  },
36
36
  "dependencies": {
37
- "@agentcomms/core": "0.24.0"
37
+ "@agentcomms/core": "0.26.0"
38
38
  },
39
39
  "devDependencies": {
40
40
  "@modelcontextprotocol/client": "2.3.1",
@@ -98,5 +98,5 @@
98
98
  "typecheck": "tsc -p tsconfig.json",
99
99
  "test": "node --experimental-strip-types --disable-warning=ExperimentalWarning --test --test-timeout=600000 \"test/**/*.test.ts\""
100
100
  },
101
- "gitHead": "f239501ba696ae239586f22f1dae43f181bbe817"
101
+ "gitHead": "4a096cb13ea1444f1301137f9f91d07b8401aaa2"
102
102
  }