switchroom 0.19.25 → 0.19.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/dist/agent-scheduler/index.js +6 -2
  2. package/dist/auth-broker/index.js +9 -2
  3. package/dist/cli/notion-write-pretool.mjs +6 -2
  4. package/dist/cli/switchroom.js +908 -527
  5. package/dist/host-control/main.js +10 -3
  6. package/dist/vault/approvals/kernel-server.js +10 -2
  7. package/dist/vault/broker/server.js +10 -2
  8. package/package.json +1 -1
  9. package/profiles/_base/cron-session.sh.hbs +6 -0
  10. package/profiles/_base/start.sh.hbs +40 -4
  11. package/telegram-plugin/dist/gateway/gateway.js +275 -109
  12. package/telegram-plugin/gateway/gateway.ts +53 -52
  13. package/telegram-plugin/gateway/periodic-sweep-guard.ts +86 -0
  14. package/telegram-plugin/gateway/status-pin-retarget.ts +144 -0
  15. package/telegram-plugin/status-no-truncate.ts +49 -0
  16. package/telegram-plugin/status-pin-driver.ts +28 -0
  17. package/telegram-plugin/status-pin.ts +33 -4
  18. package/telegram-plugin/tests/card-type-distinguishability.test.ts +268 -0
  19. package/telegram-plugin/tests/periodic-sweep-guard.test.ts +151 -0
  20. package/telegram-plugin/tests/pinned-card-collapse.test.ts +29 -18
  21. package/telegram-plugin/tests/status-pin-retarget.test.ts +216 -0
  22. package/telegram-plugin/tests/status-pin-shutdown-wiring.test.ts +94 -0
  23. package/telegram-plugin/tests/status-pin-store.test.ts +87 -21
  24. package/telegram-plugin/tests/status-pin.test.ts +128 -2
  25. package/telegram-plugin/tests/worker-activity-feed.test.ts +10 -10
  26. package/telegram-plugin/tests/worker-feed-coalesce.test.ts +37 -21
  27. package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +1 -1
  28. package/telegram-plugin/tier-downgrade.ts +3 -2
  29. package/telegram-plugin/tool-activity-summary.ts +61 -18
  30. package/telegram-plugin/uat/assertions.ts +21 -2
  31. package/telegram-plugin/uat/feed-matcher.test.ts +29 -0
  32. package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +9 -2
  33. package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +9 -2
  34. package/telegram-plugin/worker-activity-feed.ts +38 -17
  35. package/vendor/hindsight-memory/scripts/tests/test_recall_request_timeout.py +241 -0
@@ -21496,13 +21496,15 @@ var init_schema = __esm(() => {
21496
21496
  recall: exports_external.object({
21497
21497
  max_memories: exports_external.number().int().min(0).optional().describe("Cap on the number of memories injected into the prompt by " + "auto-recall, regardless of token budget. Plugin default is 12. " + "0 disables the cap (all memories Hindsight returns are injected)."),
21498
21498
  cache_ttl_secs: exports_external.number().int().min(0).optional().describe("Per-session recall cache TTL in seconds. When > 0, identical " + "(prompt, bank) within the same session reuse the cached recall " + "result instead of round-tripping to Hindsight. 0 disables. " + "Default is 600 (10 min) for switchroom-managed agents."),
21499
+ hook_timeout_seconds: exports_external.number().int().min(1).optional().describe("Ceiling (seconds) Claude Code gives the UserPromptSubmit recall " + "hook before killing it. Stamped into the installed plugin's " + "hooks/hooks.json, so it survives `switchroom apply` reinstalling " + "the plugin. Default 12. Raising it lets slow banks finish at the " + "cost of pre-turn dead air; `parallel_deadline_seconds` and " + "`request_timeout_seconds` are both kept under it. A value below " + "3s is raised to 3s and reported: the fan-out deadline must be at " + "least 1s AND still leave 2s of post-deadline headroom, so a " + "lower ceiling admits no usable envelope at all."),
21500
+ parallel_deadline_seconds: exports_external.number().int().min(1).optional().describe("Shared deadline (seconds) for the whole parallel multi-bank " + "recall fan-out. Slots unfinished when it elapses are abandoned " + "and reported as timed out. Defaults to `hook_timeout_seconds` " + "minus 2s of headroom for block formatting, cache write and " + "stdout flush, so a straggler bank can never push the hook past " + "its ceiling. Set explicitly to override that derivation; a value " + "that would leave less than 2s under the hook ceiling \u2014 including " + "one set EQUAL to it \u2014 is clamped back to `hook_timeout_seconds` " + "minus 2, and the clamp is reported. Equality is not allowed: at " + "zero headroom the hook is killed mid-write and the turn loses " + "both the memories and the recall_log row explaining why."),
21499
21501
  query_max_tokens: exports_external.number().int().min(0).optional().describe("Cap on the number of DISTINCT BM25 terms the recall hook may " + "put on the wire. `recallMaxQueryChars` bounds characters, which " + "is not the cost driver: Hindsight OR-joins every query token " + "into one tsquery and Postgres native FTS ranks the entire " + "matched set before the top-60 heapsort, so cost tracks TERMS. " + "An 800-char composed query is ~96 distinct terms and matched " + "119,510 of 135,565 rows on the `overlord` bank (14.0s for the " + "3-arm " + "BM25 UNION), past the per-bank timeout \u2014 96.8% of that agent's " + "own-bank recalls returned nothing. Plugin default is 24 " + "(measured 48,433 rows / 2.7s on the same bank). Terms are " + "chosen recency-first (the latest turn beats prior context), " + "then by selectivity. 0 disables shaping (rollback lever)."),
21500
21502
  query_stop_terms: exports_external.array(exports_external.string().min(1).regex(/^[\w./-]+$/)).optional().describe("Extra terms dropped from the BM25 recall query, on top of the " + "built-in English stopword list. For BANK-SPECIFIC " + "high-document-frequency words a generic stoplist cannot know " + "about: on `overlord`, `switchroom` matches 20% of the bank and " + "`agent` another 20%, purely because that is what the corpus is " + "about, and each such term drags tens of thousands of rows into " + "the ranking. Defaults to []."),
21501
- request_timeout_seconds: exports_external.number().int().min(1).optional().describe("Per-bank HTTP read timeout, in seconds, for one recall " + "request. Plugin default is 12, matching the UserPromptSubmit " + "hook ceiling; the shared `recallParallelDeadlineSeconds` (10) " + "is the tighter outer guard in the default configuration, so " + "this is a per-request safety net. Was a hardcoded 8 in the " + "plugin before #3757."),
21503
+ request_timeout_seconds: exports_external.number().int().min(1).optional().describe("Per-bank HTTP read timeout, in seconds, for one recall " + "request. Even parallelised, each bank carries its own deadline " + "so ONE hung bank returns empty instead of consuming the shared " + "deadline and starving its siblings. The plugin default is 12 " + "(raised from a hardcoded 8 in #3757, which fired on 96.8% of " + "one agent's own-bank recalls). Switchroom defaults it to the " + "effective `parallel_deadline_seconds` instead \u2014 10 at the " + "shipped ceiling \u2014 because the shared fan-out deadline is " + "already the tighter outer guard, so a per-bank value above it " + "can never bind. An explicitly configured value above the " + "effective deadline is clamped down to it, and the clamp is " + "reported."),
21502
21504
  own_bank_min_slots: exports_external.number().int().min(0).optional().describe("Slots inside `max_memories` reserved as a FLOOR for the agent's " + "own bank when recall fans out to more than one bank. The merged " + "set is sorted globally by relevance and head-sliced, which is " + "winner-take-all across banks: when both banks return more " + "candidates than the cap, one bank's score distribution can fill " + "every slot and the agent gets a dossier about its operator with " + "none of its own session memory. A floor, not a quota: at most " + "this many slots, only if the own bank returned that many, and " + "only up to HALF the cap shared with `additional_bank_min_slots` " + "\u2014 the rest is always won on pure relevance, so composition still " + "moves with the scores. Fixes score-based crowd-out only; a " + "timed-out bank returns no candidates and reservation is a no-op " + "there. 0 disables (default). Switchroom-managed agents use 2 " + "against the fleet-deployed cap of 6."),
21503
21505
  additional_bank_min_slots: exports_external.number().int().min(0).optional().describe("Slots inside `max_memories` reserved as a FLOOR for the " + "additional (profile / shared / sender) banks. Symmetric with " + "`own_bank_min_slots` \u2014 same floor-not-quota semantics, and the " + "two share the same half-of-cap reservation budget. When they sum " + "above that budget the own-bank floor is honoured first. 0 " + "disables (default). Switchroom-managed agents use 1 against the " + "fleet-deployed cap of 6. Observe `injected_own_bank_count` / " + "`injected_additional_bank_count` via " + "`switchroom memory recall-log`."),
21504
21506
  types: exports_external.array(exports_external.string()).optional().describe("Hindsight fact types to recall. Switchroom default is " + '["world", "experience", "observation"] \u2014 the synthesized ' + "`observation` tier is on by default. Set to " + '["world", "experience"] to opt out of observation-backed ' + "recall for this agent (or fleet-wide under defaults)."),
21505
- additional_banks: exports_external.array(exports_external.string()).optional().describe("Extra Hindsight banks to recall from on every turn, merged into " + "the agent's own bank results \u2014 e.g. a shared operator/household " + "profile bank authored via `switchroom memory profile`. Each is " + "recalled with an 8s timeout and is non-fatal on failure. Stays " + "within the single tenant: all banks are the operator's data, in " + "the operator's Hindsight instance (see the `single-tenant` " + "invariant). Defaults to [] (no extra banks)."),
21507
+ additional_banks: exports_external.array(exports_external.string()).optional().describe("Extra Hindsight banks to recall from on every turn, merged into " + "the agent's own bank results \u2014 e.g. a shared operator/household " + "profile bank authored via `switchroom memory profile`. Each is " + "recalled with the `request_timeout_seconds` per-bank timeout " + "(defaults to the effective `parallel_deadline_seconds`, 10s at " + "the shipped ceiling) and is non-fatal on failure. Stays " + "within the single tenant: all banks are the operator's data, in " + "the operator's Hindsight instance (see the `single-tenant` " + "invariant). Defaults to [] (no extra banks)."),
21506
21508
  sender_banks: exports_external.record(exports_external.string(), exports_external.string()).optional().describe("Per-speaker recall routing: a map of Telegram sender \u2192 extra " + "recall bank. When a message arrives, the agent also recalls the " + "speaker's bank (matched by Telegram username \u2014 a leading @ is " + "optional \u2014 or numeric user_id), merged " + "into its own results \u2014 so each trusted user gets their own " + "profile context. Additive recall scoping within the single " + "tenant: never an access boundary (who may drive an agent stays " + "the per-agent user assignment in `access.allowFrom`). Author the " + "banks via `switchroom memory profile`."),
21507
21509
  skip_trivial: exports_external.boolean().optional().describe("Skip recall on plausibly-stateless trivial turns (time/date/" + "greeting). Switchroom default true \u2014 saves the recall arm + " + "injected tokens on turns that never need memory, guarded so it " + "never skips a turn that references user/project/session state. " + "Set false to always run recall."),
21508
21510
  topic_filter_mode: exports_external.enum(["soft-preamble", "hard-filter"]).optional().describe("Supergroup-mode cross-topic memory behaviour. Default " + "(unset) \u2192 soft-preamble: recall returns memories from all " + "topics, and a 'Current topic: \u2026' preamble tells the model " + "to self-scope. hard-filter: drop any recalled memory whose " + "metadata.thread_id differs from the active inbound's topic. " + "Flip to hard-filter when the recall_log shows binding " + "failures (model surfacing the right memory but applying " + "it to the wrong topic).")
@@ -21861,6 +21863,8 @@ var init_schema = __esm(() => {
21861
21863
  recall: exports_external.object({
21862
21864
  max_memories: exports_external.number().int().min(0).optional(),
21863
21865
  cache_ttl_secs: exports_external.number().int().min(0).optional(),
21866
+ hook_timeout_seconds: exports_external.number().int().min(1).optional(),
21867
+ parallel_deadline_seconds: exports_external.number().int().min(1).optional(),
21864
21868
  query_max_tokens: exports_external.number().int().min(0).optional(),
21865
21869
  query_stop_terms: exports_external.array(exports_external.string().min(1).regex(/^[\w./-]+$/)).optional(),
21866
21870
  request_timeout_seconds: exports_external.number().int().min(1).optional(),
@@ -44389,6 +44393,45 @@ async function handleVoiceMessage(ctx, deps) {
44389
44393
  }
44390
44394
 
44391
44395
  // gateway/status-pin-store.ts
44396
+ function isPinRow(x) {
44397
+ if (x == null || typeof x !== "object")
44398
+ return false;
44399
+ const o = x;
44400
+ return typeof o.pinKey === "string" && o.pinKey.length > 0 && typeof o.chatId === "string" && o.chatId.length > 0 && typeof o.messageId === "number" && (o.pending === undefined || typeof o.pending === "boolean") && (o.expiresAt === undefined || typeof o.expiresAt === "number") && (o.attempts === undefined || typeof o.attempts === "number");
44401
+ }
44402
+ function loadStatusPins(path, fs2) {
44403
+ if (!fs2.existsSync(path))
44404
+ return [];
44405
+ let raw = "";
44406
+ try {
44407
+ raw = fs2.readFileSync(path);
44408
+ } catch {
44409
+ return [];
44410
+ }
44411
+ let parsed;
44412
+ try {
44413
+ parsed = JSON.parse(raw);
44414
+ } catch {
44415
+ return [];
44416
+ }
44417
+ if (parsed == null || typeof parsed !== "object")
44418
+ return [];
44419
+ const env = parsed;
44420
+ if (env.v !== 1 && env.v !== 2 || !Array.isArray(env.pins))
44421
+ return [];
44422
+ return env.pins.filter(isPinRow);
44423
+ }
44424
+ function persistStatusPins(path, fs2, snapshot, log = (l) => process.stderr.write(l)) {
44425
+ const env = { v: 2, pins: [...snapshot] };
44426
+ const tmp = path + ".tmp";
44427
+ try {
44428
+ fs2.writeFileSync(tmp, JSON.stringify(env));
44429
+ fs2.renameSync(tmp, path);
44430
+ } catch (err) {
44431
+ log(`status-pin-store: persist FAILED path=${path}: ${err.message} \u2014 ` + `durability degraded to in-memory
44432
+ `);
44433
+ }
44434
+ }
44392
44435
  function pinnedMessageIsOurs(tracked, chatId, pinnedMessageId) {
44393
44436
  for (const t of tracked) {
44394
44437
  if (t.messageId === pinnedMessageId && t.chatId === chatId)
@@ -44397,7 +44440,46 @@ function pinnedMessageIsOurs(tracked, chatId, pinnedMessageId) {
44397
44440
  return false;
44398
44441
  }
44399
44442
  var storeLockTails = new Map;
44443
+ function withStoreLock(path, fn) {
44444
+ const prev = storeLockTails.get(path) ?? Promise.resolve();
44445
+ const run2 = prev.then(fn, fn);
44446
+ storeLockTails.set(path, run2.then(() => {
44447
+ return;
44448
+ }, () => {
44449
+ return;
44450
+ }));
44451
+ return run2;
44452
+ }
44400
44453
  var pinReconcileTails = new Map;
44454
+ function applyStatusPinRow(path, fs2, pinKey, row, log) {
44455
+ const current = loadStatusPins(path, fs2);
44456
+ const others = current.filter((p) => p.pinKey !== pinKey);
44457
+ const next = row == null ? others : [...others, row];
44458
+ persistStatusPins(path, fs2, next, log);
44459
+ }
44460
+ function reconcileAndPersistStatusPin(args) {
44461
+ const { path, fs: fs2, pinKey, chatId, op } = args;
44462
+ const log = args.log ?? ((l) => process.stderr.write(l));
44463
+ return withStoreLock(path, async () => {
44464
+ if (op.kind === "pin") {
44465
+ applyStatusPinRow(path, fs2, pinKey, { pinKey, chatId, messageId: op.messageId, pending: true }, log);
44466
+ const next2 = await args.applyPin();
44467
+ if (next2 == null) {
44468
+ applyStatusPinRow(path, fs2, pinKey, null, log);
44469
+ return null;
44470
+ }
44471
+ applyStatusPinRow(path, fs2, pinKey, { pinKey, chatId, messageId: next2.messageId }, log);
44472
+ return next2;
44473
+ }
44474
+ const next = await args.applyPin();
44475
+ if (next == null) {
44476
+ applyStatusPinRow(path, fs2, pinKey, null, log);
44477
+ } else {
44478
+ applyStatusPinRow(path, fs2, pinKey, { pinKey, chatId, messageId: next.messageId }, log);
44479
+ }
44480
+ return next;
44481
+ });
44482
+ }
44401
44483
 
44402
44484
  // gateway/pinned-message-handler.ts
44403
44485
  async function handlePinnedMessage(ctx, deps) {
@@ -46504,6 +46586,11 @@ var STATUS_LINE_MAX = 200;
46504
46586
  var STATUS_CARD_CHAR_BUDGET = RICH_MESSAGE_MAX_CHARS;
46505
46587
  var NESTED_PREFIX = " \u21b3 ";
46506
46588
  var WORKER_STEP_INDENT = "\u2800\u2800\u2800";
46589
+ var SUBORDINATE_HEADER_PREFIX = "\u2514\u2500 ";
46590
+ var SUBORDINATE_LINE_INDENT = WORKER_STEP_INDENT;
46591
+ function nestSubordinateCardLines(lines) {
46592
+ return lines.map((line, i) => i === 0 ? `${SUBORDINATE_HEADER_PREFIX}${line}` : `${SUBORDINATE_LINE_INDENT}${line}`);
46593
+ }
46507
46594
 
46508
46595
  // hooks/tool-label-pretool.mjs
46509
46596
  import { readFileSync as readFileSync9, mkdirSync as mkdirSync10, appendFileSync as appendFileSync2, existsSync as existsSync9 } from "node:fs";
@@ -46976,7 +47063,9 @@ function renderStatusCard(opts) {
46976
47063
  }
46977
47064
  if (out.length === 0)
46978
47065
  return null;
46979
- const joined = stackCardLines(out, { collapseSafe: true });
47066
+ const joined = stackCardLines(opts.subordinate === true ? nestSubordinateCardLines(out) : out, {
47067
+ collapseSafe: true
47068
+ });
46980
47069
  if (joined.length <= STATUS_CARD_CHAR_BUDGET)
46981
47070
  return joined;
46982
47071
  return fitCardToBudget(opts, headerLines);
@@ -46988,6 +47077,9 @@ function fitCardToBudget(opts, headerLines) {
46988
47077
  const rawSteps = opts.steps.filter((s) => s != null);
46989
47078
  const rawChildren = (opts.childSteps ?? []).map((s) => s.trim()).filter((s) => s.length > 0);
46990
47079
  const hasChildren = rawChildren.length > 0;
47080
+ const subordinate = opts.subordinate === true;
47081
+ const nestCost = subordinate ? SUBORDINATE_LINE_INDENT.length : 0;
47082
+ const stack = (lines2) => stackCardLines(subordinate ? nestSubordinateCardLines(lines2) : lines2, { collapseSafe: true });
46991
47083
  const footerLines = [];
46992
47084
  if (final && stepCount != null && stepCount > 0)
46993
47085
  footerLines.push(`_\u2713 ${stepCount} steps_`);
@@ -46996,7 +47088,7 @@ function fitCardToBudget(opts, headerLines) {
46996
47088
  footerLines.push(`${result.emoji} _${escapeMarkdown(truncate(result.text, WORKER_RESULT_MAX))}_`);
46997
47089
  }
46998
47090
  const fixedCost = [...headerLines, ...footerLines].join(`
46999
- `).length;
47091
+ `).length + (headerLines.length + footerLines.length) * nestCost;
47000
47092
  const body = hasChildren ? rawChildren : rawSteps;
47001
47093
  const escapedBody = body.map(escapeStepLine);
47002
47094
  const prefix = hasChildren ? NESTED_PREFIX : "";
@@ -47011,13 +47103,13 @@ function fitCardToBudget(opts, headerLines) {
47011
47103
  const lastIdx = shown.length - 1;
47012
47104
  shown.forEach((esc, i) => lines2.push(buildBullet(esc, i === lastIdx)));
47013
47105
  lines2.push(...footerLines);
47014
- const candidate = stackCardLines(lines2, { collapseSafe: true });
47106
+ const candidate = stack(lines2);
47015
47107
  if (candidate.length <= STATUS_CARD_CHAR_BUDGET)
47016
47108
  return candidate;
47017
47109
  }
47018
47110
  const rawNewest = body.length > 0 ? stripMarkdown(body[body.length - 1]).replace(/\s+/g, " ").trim() : "";
47019
- const wrapperOverhead = final ? (prefix + "_\u2713 _").length : (prefix + "**\u2192 **").length + liveSuffix.length;
47020
- const headerFooterCost = fixedCost + (fixedCost > 0 ? 1 : 0) + (parentMarker != null ? parentMarker.length + 1 : 0);
47111
+ const wrapperOverhead = (final ? (prefix + "_\u2713 _").length : (prefix + "**\u2192 **").length + liveSuffix.length) + nestCost;
47112
+ const headerFooterCost = fixedCost + (fixedCost > 0 ? 1 : 0) + (parentMarker != null ? parentMarker.length + nestCost + 1 : 0);
47021
47113
  const budget = STATUS_CARD_CHAR_BUDGET - headerFooterCost - wrapperOverhead;
47022
47114
  let raw = rawNewest.slice(0, Math.max(0, budget));
47023
47115
  let newest = escapeMarkdown(raw);
@@ -47032,7 +47124,7 @@ function fitCardToBudget(opts, headerLines) {
47032
47124
  lines.push(parentMarker);
47033
47125
  lines.push(newestLine);
47034
47126
  lines.push(...footerLines);
47035
- return stackCardLines(lines, { collapseSafe: true });
47127
+ return stack(lines);
47036
47128
  }
47037
47129
  function renderActivityFeed(lines, final = false, liveSuffix = "", stepCount, header) {
47038
47130
  if (lines.length === 0 && header == null)
@@ -47090,7 +47182,7 @@ function glanceLine(rows) {
47090
47182
  const tools = rows.reduce((n, r) => n + r.toolCount, 0);
47091
47183
  const tok = rows.reduce((n, r) => n + (r.totalTokens ?? 0), 0);
47092
47184
  const toolWord = tools === 1 ? "tool" : "tools";
47093
- return `\uD83D\uDEE0 **Workers** \u00b7 _${rows.length} running \u00b7 oldest ${formatFeedElapsed(oldestMs)}` + ` \u00b7 ${tools} ${toolWord}${tokenSegment(tok)}_`;
47185
+ return `\uD83D\uDEE0 **WORKERS** \u00b7 _${rows.length} running \u00b7 oldest ${formatFeedElapsed(oldestMs)}` + ` \u00b7 ${tools} ${toolWord}${tokenSegment(tok)}_`;
47094
47186
  }
47095
47187
  function renderCombinedWorkerFeed(rows, opts) {
47096
47188
  if (rows.length === 0)
@@ -47129,7 +47221,10 @@ function renderCombinedWorkerFeed(rows, opts) {
47129
47221
  const out = [...chrome, ...bodyOut];
47130
47222
  if (hidden > 0)
47131
47223
  out.push(`_+${hidden} more working\u2026_`);
47132
- return { body: stackCardLines(out, { collapseSafe: true }), bodyLines: bodyOut.length };
47224
+ return {
47225
+ body: stackCardLines(nestSubordinateCardLines(out), { collapseSafe: true }),
47226
+ bodyLines: bodyOut.length
47227
+ };
47133
47228
  };
47134
47229
  let visible = Math.min(rows.length, maxRows);
47135
47230
  let { body, bodyLines } = compose(visible);
@@ -47185,7 +47280,7 @@ function renderWorkerActivity(v, liveSuffix = "") {
47185
47280
  }
47186
47281
  const header = {
47187
47282
  emoji: "\uD83D\uDEE0",
47188
- label: "Worker",
47283
+ label: "WORKER",
47189
47284
  description: desc,
47190
47285
  elapsedMs: v.elapsedMs,
47191
47286
  toolCount: v.toolCount,
@@ -47205,21 +47300,22 @@ function renderWorkerActivity(v, liveSuffix = "") {
47205
47300
  final: finished,
47206
47301
  liveSuffix: finished ? "" : liveSuffix,
47207
47302
  result,
47208
- historyWindow: workerHistoryDepth(1)
47303
+ historyWindow: workerHistoryDepth(1),
47304
+ subordinate: true
47209
47305
  });
47210
47306
  if (card == null) {
47211
- return `\uD83D\uDEE0 **Worker** \u00b7 _starting\u2026_`;
47307
+ return `${SUBORDINATE_HEADER_PREFIX}\uD83D\uDEE0 **WORKER** \u00b7 _starting\u2026_`;
47212
47308
  }
47213
47309
  if (!finished && steps.length === 0) {
47214
47310
  return `${card}${COLLAPSE_SAFE_SEPARATOR}
47215
- _starting\u2026_`;
47311
+ ${SUBORDINATE_LINE_INDENT}_starting\u2026_`;
47216
47312
  }
47217
47313
  return card;
47218
47314
  }
47219
47315
  var COOLDOWN_JITTER_MS = 500;
47220
- var WORKER_CARD_SUPERSEDED_BODY = `\uD83D\uDEE0 **Worker** \u00b7 _continued_
47316
+ var WORKER_CARD_SUPERSEDED_BODY = `${SUBORDINATE_HEADER_PREFIX}\uD83D\uDEE0 **WORKER** \u00b7 _continued_
47221
47317
 
47222
- _Live progress moved to a fresh card to stay pinned._`;
47318
+ ` + `${SUBORDINATE_LINE_INDENT}_Live progress moved to a fresh card to stay pinned._`;
47223
47319
  function extractRetryAfterSecs(err) {
47224
47320
  if (err == null || typeof err !== "object")
47225
47321
  return null;
@@ -48095,7 +48191,11 @@ function decidePinAction(prev, desired) {
48095
48191
  if (prev.messageId === desired.messageId) {
48096
48192
  return { kind: "noop", reason: "already pinned this message" };
48097
48193
  }
48098
- return { kind: "unpin", messageId: prev.messageId };
48194
+ return {
48195
+ kind: "repin",
48196
+ unpinMessageId: prev.messageId,
48197
+ pinMessageId: desired.messageId
48198
+ };
48099
48199
  }
48100
48200
  function errorDescription(err) {
48101
48201
  if (err != null && typeof err === "object") {
@@ -48144,6 +48244,19 @@ async function reconcilePin(args) {
48144
48244
  const action = decidePinAction(args.prevState, args.desired);
48145
48245
  if (action.kind === "noop")
48146
48246
  return args.prevState;
48247
+ if (action.kind === "repin") {
48248
+ const afterUnpin = await reconcilePin({
48249
+ ...args,
48250
+ desired: { pinned: false }
48251
+ });
48252
+ if (afterUnpin != null)
48253
+ return afterUnpin;
48254
+ return reconcilePin({
48255
+ ...args,
48256
+ prevState: null,
48257
+ desired: { pinned: true, messageId: action.pinMessageId }
48258
+ });
48259
+ }
48147
48260
  if (action.kind === "unpin") {
48148
48261
  if (args.rightsCache?.isBlocked(args.chatId))
48149
48262
  return null;
@@ -48184,19 +48297,6 @@ async function reconcilePin(args) {
48184
48297
  }
48185
48298
 
48186
48299
  // status-pin.ts
48187
- function decidePinAction2(prev, desired) {
48188
- if (!desired.pinned) {
48189
- if (prev)
48190
- return { kind: "unpin", messageId: prev.messageId };
48191
- return { kind: "noop", reason: "nothing pinned, nothing wanted" };
48192
- }
48193
- if (prev == null)
48194
- return { kind: "pin", messageId: desired.messageId };
48195
- if (prev.messageId === desired.messageId) {
48196
- return { kind: "noop", reason: "already pinned this message" };
48197
- }
48198
- return { kind: "unpin", messageId: prev.messageId };
48199
- }
48200
48300
  class PinRightsCache2 {
48201
48301
  blocked = new Set;
48202
48302
  isBlocked(chatId) {
@@ -83028,6 +83128,12 @@ init_peercred();
83028
83128
 
83029
83129
  // ../src/agents/scaffold.ts
83030
83130
  init_generation_stamp();
83131
+
83132
+ // ../src/setup/hindsight-recall-tunables.ts
83133
+ var RECALL_DEADLINE_HEADROOM_SECONDS = 2;
83134
+ var MIN_RECALL_HOOK_TIMEOUT_SECONDS = RECALL_DEADLINE_HEADROOM_SECONDS + 1;
83135
+
83136
+ // ../src/agents/scaffold.ts
83031
83137
  init_schema();
83032
83138
 
83033
83139
  // ../src/config/users.ts
@@ -84974,6 +85080,97 @@ async function runBootPinSweepSteps(deps) {
84974
85080
  await step(`dm-sweep:${id}`, () => deps.sweepDm(id), log);
84975
85081
  }
84976
85082
 
85083
+ // gateway/status-pin-retarget.ts
85084
+ async function runStatusPinReconcile(args) {
85085
+ const { pinKey, chatId, prev, desired, persist, runPin, registries } = args;
85086
+ const commit = (next) => {
85087
+ if (next == null) {
85088
+ registries.state.delete(pinKey);
85089
+ registries.chatIds.delete(pinKey);
85090
+ registries.pinnedAt.delete(pinKey);
85091
+ return;
85092
+ }
85093
+ registries.state.set(pinKey, next);
85094
+ registries.chatIds.set(pinKey, chatId);
85095
+ if (!registries.pinnedAt.has(pinKey))
85096
+ registries.pinnedAt.set(pinKey, Date.now());
85097
+ };
85098
+ const runLeg = (from, want) => {
85099
+ if (persist == null)
85100
+ return runPin(from, want);
85101
+ const legAction = decidePinAction(from, want);
85102
+ const op = legAction.kind === "pin" ? { kind: "pin", messageId: legAction.messageId } : { kind: "clear" };
85103
+ return reconcileAndPersistStatusPin({
85104
+ path: persist.path,
85105
+ fs: persist.fs,
85106
+ pinKey,
85107
+ chatId,
85108
+ op,
85109
+ applyPin: () => runPin(from, want)
85110
+ });
85111
+ };
85112
+ const action = decidePinAction(prev, desired);
85113
+ if (action.kind === "repin" && desired.pinned) {
85114
+ const afterUnpin = await runLeg(prev, { pinned: false });
85115
+ commit(afterUnpin);
85116
+ if (afterUnpin != null)
85117
+ return;
85118
+ commit(await runLeg(null, desired));
85119
+ return;
85120
+ }
85121
+ commit(await runLeg(prev, desired));
85122
+ }
85123
+
85124
+ // gateway/periodic-sweep-guard.ts
85125
+ function createPeriodicSweepGuard(args) {
85126
+ let running = false;
85127
+ let skippedCount = 0;
85128
+ const notify = (fn) => {
85129
+ if (fn == null)
85130
+ return;
85131
+ try {
85132
+ fn();
85133
+ } catch {}
85134
+ };
85135
+ return {
85136
+ async tick() {
85137
+ if (running) {
85138
+ skippedCount += 1;
85139
+ notify(args.onSkip);
85140
+ return;
85141
+ }
85142
+ running = true;
85143
+ try {
85144
+ await args.run();
85145
+ } catch (err) {
85146
+ notify(args.onError == null ? undefined : () => args.onError?.(err));
85147
+ } finally {
85148
+ running = false;
85149
+ }
85150
+ },
85151
+ isRunning() {
85152
+ return running;
85153
+ },
85154
+ skipped() {
85155
+ return skippedCount;
85156
+ }
85157
+ };
85158
+ }
85159
+
85160
+ // gateway/with-deadline.ts
85161
+ function withDeadline(p, ms, timeoutMessage) {
85162
+ p.catch(() => {});
85163
+ let timer3;
85164
+ const deadline = new Promise((_resolve, reject) => {
85165
+ timer3 = setTimeout(() => reject(new Error(timeoutMessage)), ms);
85166
+ timer3.unref?.();
85167
+ });
85168
+ return Promise.race([p, deadline]).finally(() => {
85169
+ if (timer3 !== undefined)
85170
+ clearTimeout(timer3);
85171
+ });
85172
+ }
85173
+
84977
85174
  // gateway/status-pin-api.ts
84978
85175
  class BotNotReadyError extends Error {
84979
85176
  constructor(what) {
@@ -87879,13 +88076,13 @@ function createCapturedResumeDispatcher(ports) {
87879
88076
 
87880
88077
  // gateway/status-pin-store.ts
87881
88078
  var BOOT_UNPIN_MAX_ATTEMPTS = 5;
87882
- function isPinRow(x) {
88079
+ function isPinRow2(x) {
87883
88080
  if (x == null || typeof x !== "object")
87884
88081
  return false;
87885
88082
  const o = x;
87886
88083
  return typeof o.pinKey === "string" && o.pinKey.length > 0 && typeof o.chatId === "string" && o.chatId.length > 0 && typeof o.messageId === "number" && (o.pending === undefined || typeof o.pending === "boolean") && (o.expiresAt === undefined || typeof o.expiresAt === "number") && (o.attempts === undefined || typeof o.attempts === "number");
87887
88084
  }
87888
- function loadStatusPins(path3, fs3) {
88085
+ function loadStatusPins2(path3, fs3) {
87889
88086
  if (!fs3.existsSync(path3))
87890
88087
  return [];
87891
88088
  let raw = "";
@@ -87905,9 +88102,9 @@ function loadStatusPins(path3, fs3) {
87905
88102
  const env = parsed;
87906
88103
  if (env.v !== 1 && env.v !== 2 || !Array.isArray(env.pins))
87907
88104
  return [];
87908
- return env.pins.filter(isPinRow);
88105
+ return env.pins.filter(isPinRow2);
87909
88106
  }
87910
- function persistStatusPins(path3, fs3, snapshot, log = (l) => process.stderr.write(l)) {
88107
+ function persistStatusPins2(path3, fs3, snapshot, log = (l) => process.stderr.write(l)) {
87911
88108
  const env = { v: 2, pins: [...snapshot] };
87912
88109
  const tmp = path3 + ".tmp";
87913
88110
  try {
@@ -87921,7 +88118,7 @@ function persistStatusPins(path3, fs3, snapshot, log = (l) => process.stderr.wri
87921
88118
  async function runStatusPinBootCleanup(args) {
87922
88119
  const log = args.log ?? ((l) => process.stderr.write(l));
87923
88120
  const now = args.now ?? Date.now();
87924
- const persisted = loadStatusPins(args.path, args.fs);
88121
+ const persisted = loadStatusPins2(args.path, args.fs);
87925
88122
  if (persisted.length === 0)
87926
88123
  return { cleared: 0, retained: 0, kept: 0, total: 0 };
87927
88124
  let cleared = 0;
@@ -87950,11 +88147,11 @@ async function runStatusPinBootCleanup(args) {
87950
88147
  }
87951
88148
  }
87952
88149
  }
87953
- persistStatusPins(args.path, args.fs, next, log);
88150
+ persistStatusPins2(args.path, args.fs, next, log);
87954
88151
  return { cleared, retained, kept, total: persisted.length };
87955
88152
  }
87956
88153
  var storeLockTails2 = new Map;
87957
- function withStoreLock(path3, fn) {
88154
+ function withStoreLock2(path3, fn) {
87958
88155
  const prev = storeLockTails2.get(path3) ?? Promise.resolve();
87959
88156
  const run3 = prev.then(fn, fn);
87960
88157
  storeLockTails2.set(path3, run3.then(() => {
@@ -87975,38 +88172,15 @@ function withPinReconcileLock(pinKey, fn) {
87975
88172
  }));
87976
88173
  return run3;
87977
88174
  }
87978
- function applyStatusPinRow(path3, fs3, pinKey, row, log) {
87979
- const current = loadStatusPins(path3, fs3);
88175
+ function applyStatusPinRow2(path3, fs3, pinKey, row, log) {
88176
+ const current = loadStatusPins2(path3, fs3);
87980
88177
  const others = current.filter((p) => p.pinKey !== pinKey);
87981
88178
  const next = row == null ? others : [...others, row];
87982
- persistStatusPins(path3, fs3, next, log);
88179
+ persistStatusPins2(path3, fs3, next, log);
87983
88180
  }
87984
88181
  function mutateStatusPinRow(path3, fs3, pinKey, row, log = (l) => process.stderr.write(l)) {
87985
- return withStoreLock(path3, async () => {
87986
- applyStatusPinRow(path3, fs3, pinKey, row, log);
87987
- });
87988
- }
87989
- function reconcileAndPersistStatusPin(args) {
87990
- const { path: path3, fs: fs3, pinKey, chatId, op } = args;
87991
- const log = args.log ?? ((l) => process.stderr.write(l));
87992
- return withStoreLock(path3, async () => {
87993
- if (op.kind === "pin") {
87994
- applyStatusPinRow(path3, fs3, pinKey, { pinKey, chatId, messageId: op.messageId, pending: true }, log);
87995
- const next2 = await args.applyPin();
87996
- if (next2 == null) {
87997
- applyStatusPinRow(path3, fs3, pinKey, null, log);
87998
- return null;
87999
- }
88000
- applyStatusPinRow(path3, fs3, pinKey, { pinKey, chatId, messageId: next2.messageId }, log);
88001
- return next2;
88002
- }
88003
- const next = await args.applyPin();
88004
- if (next == null) {
88005
- applyStatusPinRow(path3, fs3, pinKey, null, log);
88006
- } else {
88007
- applyStatusPinRow(path3, fs3, pinKey, { pinKey, chatId, messageId: next.messageId }, log);
88008
- }
88009
- return next;
88182
+ return withStoreLock2(path3, async () => {
88183
+ applyStatusPinRow2(path3, fs3, pinKey, row, log);
88010
88184
  });
88011
88185
  }
88012
88186
 
@@ -90059,7 +90233,7 @@ function buildObligationRepresentInbound(o, now) {
90059
90233
  }
90060
90234
 
90061
90235
  // gateway/with-deadline.ts
90062
- function withDeadline(p, ms, timeoutMessage) {
90236
+ function withDeadline2(p, ms, timeoutMessage) {
90063
90237
  p.catch(() => {});
90064
90238
  let timer3;
90065
90239
  const deadline = new Promise((_resolve, reject) => {
@@ -90076,7 +90250,7 @@ function withDeadline(p, ms, timeoutMessage) {
90076
90250
  function driveEscalation(args) {
90077
90251
  const { escId, inFlight, ledger, send, maxAttempts, deadlineMs } = args;
90078
90252
  const log = args.log ?? ((l) => process.stderr.write(l));
90079
- const wd = args.withDeadlineFn ?? withDeadline;
90253
+ const wd = args.withDeadlineFn ?? withDeadline2;
90080
90254
  if (inFlight.has(escId))
90081
90255
  return;
90082
90256
  const attempt = ledger.markEscalateAttempt(escId);
@@ -96617,10 +96791,10 @@ function startOutboxSweep(deps) {
96617
96791
  }
96618
96792
 
96619
96793
  // ../src/build-info.ts
96620
- var VERSION2 = "0.19.25";
96621
- var COMMIT_SHA = "d6ed6bdd";
96622
- var COMMIT_DATE = "2026-07-27T09:38:45Z";
96623
- var LATEST_PR = 3808;
96794
+ var VERSION2 = "0.19.26";
96795
+ var COMMIT_SHA = "f9314b89";
96796
+ var COMMIT_DATE = "2026-07-27T13:46:37Z";
96797
+ var LATEST_PR = 3829;
96624
96798
  var COMMITS_AHEAD_OF_TAG = 0;
96625
96799
 
96626
96800
  // gateway/boot-version.ts
@@ -101873,7 +102047,7 @@ async function runMidSessionCardReaper() {
101873
102047
  pinnedAt: statusPinPinnedAt.get(k) ?? now
101874
102048
  }));
101875
102049
  const storeOnlyCandidates = statusPinPersistEnabled ? storeOnlyWorkerPinCandidates({
101876
- rows: loadStatusPins(STATUS_PIN_STORE_PATH, statusPinStoreFs),
102050
+ rows: loadStatusPins2(STATUS_PIN_STORE_PATH, statusPinStoreFs),
101877
102051
  inMemoryPinKeys: inMemoryKeys,
101878
102052
  now
101879
102053
  }) : [];
@@ -101926,8 +102100,15 @@ async function runMidSessionCardReaper() {
101926
102100
  }
101927
102101
  }
101928
102102
  }
102103
+ var midSessionReaperGuard = createPeriodicSweepGuard({
102104
+ run: runMidSessionCardReaper,
102105
+ onSkip: () => process.stderr.write(`telegram gateway: mid-session reaper tick skipped \u2014 previous pass still running
102106
+ `),
102107
+ onError: (err) => process.stderr.write(`telegram gateway: mid-session reaper pass threw: ${err.message}
102108
+ `)
102109
+ });
101929
102110
  var midSessionCardReaper = isGatewayMain ? setInterval(() => {
101930
- runMidSessionCardReaper();
102111
+ midSessionReaperGuard.tick();
101931
102112
  }, MID_SESSION_CARD_REAPER_INTERVAL_MS) : undefined;
101932
102113
  midSessionCardReaper?.unref();
101933
102114
  async function reconcileStatusPin(pinKey, chatId, desired) {
@@ -101945,11 +102126,11 @@ async function reconcileStatusPinInner(pinKey, chatId, desired) {
101945
102126
  if (chatId.length === 0)
101946
102127
  return;
101947
102128
  const prev = statusPinState.get(pinKey) ?? null;
101948
- const runReconcile = () => reconcilePin({
102129
+ const runReconcileFrom = (from, want) => reconcilePin({
101949
102130
  api: statusPinApi(),
101950
102131
  chatId,
101951
- prevState: prev,
101952
- desired,
102132
+ prevState: from,
102133
+ desired: want,
101953
102134
  rightsCache: statusPinRightsCache,
101954
102135
  onPinRightsDisabled: (chat) => {
101955
102136
  process.stderr.write(`telegram gateway: status-pin disabled for chat ${chat}: bot lacks pin rights; grant 'Pin messages' admin right to re-enable after restart
@@ -101961,40 +102142,19 @@ async function reconcileStatusPinInner(pinKey, chatId, desired) {
101961
102142
  `);
101962
102143
  }
101963
102144
  });
101964
- const action = decidePinAction2(prev, desired);
101965
- const op = action.kind === "pin" ? { kind: "pin", messageId: action.messageId } : { kind: "clear" };
101966
- if (!statusPinPersistEnabled) {
101967
- const next2 = await runReconcile();
101968
- if (next2 == null) {
101969
- statusPinState.delete(pinKey);
101970
- statusPinChatIds.delete(pinKey);
101971
- statusPinPinnedAt.delete(pinKey);
101972
- } else {
101973
- statusPinState.set(pinKey, next2);
101974
- statusPinChatIds.set(pinKey, chatId);
101975
- if (!statusPinPinnedAt.has(pinKey))
101976
- statusPinPinnedAt.set(pinKey, Date.now());
101977
- }
101978
- return;
101979
- }
101980
- const next = await reconcileAndPersistStatusPin({
101981
- path: STATUS_PIN_STORE_PATH,
101982
- fs: statusPinStoreFs,
102145
+ await runStatusPinReconcile({
101983
102146
  pinKey,
101984
102147
  chatId,
101985
- op,
101986
- applyPin: runReconcile
102148
+ prev,
102149
+ desired,
102150
+ persist: statusPinPersistEnabled ? { path: STATUS_PIN_STORE_PATH, fs: statusPinStoreFs } : null,
102151
+ runPin: runReconcileFrom,
102152
+ registries: {
102153
+ state: statusPinState,
102154
+ chatIds: statusPinChatIds,
102155
+ pinnedAt: statusPinPinnedAt
102156
+ }
101987
102157
  });
101988
- if (next == null) {
101989
- statusPinState.delete(pinKey);
101990
- statusPinChatIds.delete(pinKey);
101991
- statusPinPinnedAt.delete(pinKey);
101992
- } else {
101993
- statusPinState.set(pinKey, next);
101994
- statusPinChatIds.set(pinKey, chatId);
101995
- if (!statusPinPinnedAt.has(pinKey))
101996
- statusPinPinnedAt.set(pinKey, Date.now());
101997
- }
101998
102158
  }
101999
102159
  async function unpinAllStatusPins() {
102000
102160
  const keys = [...statusPinState.keys()];
@@ -102028,7 +102188,7 @@ var dmPinSweeper = createDmPinSweeper({
102028
102188
  }
102029
102189
  if (statusPinPersistEnabled || bannerPinPersistEnabled || toolPinPersistEnabled) {
102030
102190
  try {
102031
- ids.push(...unexpiredStoreRepinIds(loadStatusPins(STATUS_PIN_STORE_PATH, statusPinStoreFs), chatId, Date.now()));
102191
+ ids.push(...unexpiredStoreRepinIds(loadStatusPins2(STATUS_PIN_STORE_PATH, statusPinStoreFs), chatId, Date.now()));
102032
102192
  } catch (err) {
102033
102193
  process.stderr.write(`telegram gateway: dm-pin-sweep: store repin scan failed (chat=${chatId}): ${err.message}
102034
102194
  `);
@@ -102042,7 +102202,7 @@ var dmPinSweeper = createDmPinSweeper({
102042
102202
  function runBootPinCleanupAndDmSweep() {
102043
102203
  return runBootPinSweepSteps({
102044
102204
  scanDmChatIds: () => collectDmChatIdsFromStores({
102045
- statusPins: statusPinPersistEnabled || bannerPinPersistEnabled || toolPinPersistEnabled ? loadStatusPins(STATUS_PIN_STORE_PATH, statusPinStoreFs) : [],
102205
+ statusPins: statusPinPersistEnabled || bannerPinPersistEnabled || toolPinPersistEnabled ? loadStatusPins2(STATUS_PIN_STORE_PATH, statusPinStoreFs) : [],
102046
102206
  activityCards: activityCardPersistEnabled ? loadActivityCards2(ACTIVITY_CARD_STORE_PATH, activityCardStoreFs) : [],
102047
102207
  queuedCards: queuedCardPersistEnabled ? loadQueuedCards(QUEUED_CARD_STORE_PATH, queuedCardStoreFs) : []
102048
102208
  }),
@@ -109375,6 +109535,12 @@ async function shutdown(signal) {
109375
109535
  budgetMs: SHUTDOWN_DRAIN_BUDGET_MS,
109376
109536
  agentName: agentName3
109377
109537
  });
109538
+ try {
109539
+ await withDeadline(unpinAllStatusPins(), 5000, "status-pin shutdown sweep timed out");
109540
+ } catch (err) {
109541
+ process.stderr.write(`telegram gateway: shutdown status-pin sweep incomplete: ${err.message}
109542
+ `);
109543
+ }
109378
109544
  inboundCoalescer.reset();
109379
109545
  pendingReauthFlows.clear();
109380
109546
  for (const [, v] of pendingLoopbackFlows2)