switchroom 0.18.26 → 0.18.28

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/README.md +6 -2
  2. package/dist/cli/ms-365-write-pretool.mjs +4953 -14
  3. package/dist/cli/switchroom.js +1 -1
  4. package/dist/host-control/main.js +1 -1
  5. package/package.json +1 -1
  6. package/profiles/_base/start.sh.hbs +16 -0
  7. package/telegram-plugin/dist/gateway/gateway.js +571 -43
  8. package/telegram-plugin/flushed-turn-supersede.ts +58 -0
  9. package/telegram-plugin/gateway/derive-turn-id.ts +32 -0
  10. package/telegram-plugin/gateway/gateway.ts +358 -53
  11. package/telegram-plugin/gateway/handback-preturn-signal.ts +442 -0
  12. package/telegram-plugin/gateway/model-command.ts +68 -0
  13. package/telegram-plugin/gateway/ms365-write-approval.test.ts +101 -0
  14. package/telegram-plugin/gateway/ms365-write-approval.ts +65 -3
  15. package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +12 -0
  16. package/telegram-plugin/gateway/turn-active-marker.ts +35 -0
  17. package/telegram-plugin/gateway/worker-pin-reaper.ts +54 -0
  18. package/telegram-plugin/send-gate.test.ts +138 -0
  19. package/telegram-plugin/send-gate.ts +104 -1
  20. package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +14 -4
  21. package/telegram-plugin/tests/effort-command.test.ts +47 -0
  22. package/telegram-plugin/tests/flushed-turn-supersede.test.ts +60 -0
  23. package/telegram-plugin/tests/handback-preturn-adoption-roundtrip.test.ts +211 -0
  24. package/telegram-plugin/tests/handback-preturn-signal.test.ts +346 -0
  25. package/telegram-plugin/tests/model-command.test.ts +112 -0
  26. package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +14 -2
  27. package/telegram-plugin/tests/outbound-send-chunks.test.ts +57 -0
  28. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +18 -11
  29. package/telegram-plugin/tests/reply-owner-resolve.test.ts +90 -0
  30. package/telegram-plugin/tests/subagent-handback-inbound-builder.test.ts +5 -0
  31. package/telegram-plugin/tests/turn-active-marker.test.ts +29 -0
  32. package/telegram-plugin/tests/worker-activity-feed.test.ts +121 -0
  33. package/telegram-plugin/tests/worker-feed-migration-eviction.test.ts +140 -0
  34. package/telegram-plugin/tests/worker-pin-reaper.test.ts +78 -0
  35. package/telegram-plugin/worker-activity-feed.ts +169 -6
@@ -39670,6 +39670,13 @@ function decideSupersede(record, args) {
39670
39670
  }
39671
39671
  return { supersede: true, deleteMessageIds: [...record.messageIds], reason: "supersede" };
39672
39672
  }
39673
+ function decideSupersedeCorrection(input) {
39674
+ const eligible = input.flushMessageIds.length === 1 && input.chunkCount === 1 && !input.hasFiles && !input.suppressText && !input.hasOpenPreview;
39675
+ if (eligible) {
39676
+ return { mode: "edit-in-place", editMessageId: input.flushMessageIds[0], deleteMessageIds: [] };
39677
+ }
39678
+ return { mode: "delete-resend", deleteMessageIds: [...input.flushMessageIds] };
39679
+ }
39673
39680
  var NULL_TURN_KEY = "<<null-turn>>";
39674
39681
  function turnKey(turnId) {
39675
39682
  return turnId == null ? NULL_TURN_KEY : turnId;
@@ -40962,6 +40969,7 @@ function createWorkerActivityFeed(opts) {
40962
40969
  const nowFn = opts.now ?? Date.now;
40963
40970
  const floodWaitRemainingMs = opts.floodWaitRemainingMs ?? (() => 0);
40964
40971
  const minEditInterval = opts.minEditIntervalMs ?? 2500;
40972
+ const elapsedRefreshMs = Math.max(minEditInterval, Math.floor(opts.elapsedRefreshMs ?? 15000));
40965
40973
  const firstPaintMin = opts.firstPaintMinMs ?? 8000;
40966
40974
  const heartbeatTickMs = opts.heartbeatTickMs ?? 6000;
40967
40975
  const maxRows = Math.max(1, Math.floor(opts.maxRows ?? 8));
@@ -41076,11 +41084,46 @@ function createWorkerActivityFeed(opts) {
41076
41084
  });
41077
41085
  return renderCombinedWorkerFeed(rows, { maxRows });
41078
41086
  }
41087
+ function groupSubstanceKey(g, terminalRecap) {
41088
+ const FS = "\x00";
41089
+ const RS = "\x1E";
41090
+ if (terminalRecap != null) {
41091
+ return [
41092
+ "T",
41093
+ terminalRecap.state,
41094
+ terminalRecap.description,
41095
+ terminalRecap.toolCount,
41096
+ terminalRecap.totalTokens ?? "",
41097
+ terminalRecap.latestSummary,
41098
+ ...terminalRecap.narrativeLines ?? []
41099
+ ].join(FS);
41100
+ }
41101
+ const running = runningRows(g);
41102
+ if (running.length === 0)
41103
+ return "EMPTY";
41104
+ return running.map((r) => {
41105
+ const v = r.lastView;
41106
+ return [
41107
+ r.agentId,
41108
+ v.state,
41109
+ v.description,
41110
+ v.toolCount,
41111
+ v.totalTokens ?? "",
41112
+ ...r.narrative
41113
+ ].join(FS);
41114
+ }).join(RS);
41115
+ }
41079
41116
  function removeWorker(g, agentId) {
41080
41117
  g.workers.delete(agentId);
41081
41118
  agentIndex.delete(agentId);
41082
41119
  maybeDeleteGroup(g);
41083
41120
  }
41121
+ function evictRowFromGroup(g, agentId) {
41122
+ g.workers.delete(agentId);
41123
+ if (agentIndex.get(agentId) === g.feedKey)
41124
+ agentIndex.delete(agentId);
41125
+ maybeDeleteGroup(g);
41126
+ }
41084
41127
  function maybeDeleteGroup(g) {
41085
41128
  if (g.workers.size === 0 && g.pendingFinalize.size === 0)
41086
41129
  groups.delete(g.feedKey);
@@ -41125,6 +41168,7 @@ function createWorkerActivityFeed(opts) {
41125
41168
  return;
41126
41169
  }
41127
41170
  const body = renderGroupBody(g, now, opts2.terminalRecap ?? null, opts2.heartbeat ?? false);
41171
+ const substanceKey = groupSubstanceKey(g, opts2.terminalRecap ?? null);
41128
41172
  if (body == null) {
41129
41173
  if (isTerminal)
41130
41174
  clearStaged();
@@ -41148,6 +41192,7 @@ function createWorkerActivityFeed(opts) {
41148
41192
  g.messageId = sent.message_id;
41149
41193
  g.messageCreatedAtMs = now;
41150
41194
  g.lastBody = body;
41195
+ g.lastSubstanceKey = substanceKey;
41151
41196
  g.lastEditAt = now;
41152
41197
  g.terminalPainted = false;
41153
41198
  syncPin(g);
@@ -41163,8 +41208,13 @@ function createWorkerActivityFeed(opts) {
41163
41208
  clearStaged();
41164
41209
  return;
41165
41210
  }
41166
- if (!opts2.force && now - g.lastEditAt < minEditInterval)
41167
- return;
41211
+ if (!opts2.force && !isTerminal) {
41212
+ if (now - g.lastEditAt < minEditInterval)
41213
+ return;
41214
+ const substanceChanged = substanceKey !== g.lastSubstanceKey;
41215
+ if (!substanceChanged && now - g.lastEditAt < elapsedRefreshMs)
41216
+ return;
41217
+ }
41168
41218
  try {
41169
41219
  const res = await opts.bot.editMessageText(g.chatId, g.messageId, body, sendOptsFor(g));
41170
41220
  if (isSendGateShed(res)) {
@@ -41172,6 +41222,7 @@ function createWorkerActivityFeed(opts) {
41172
41222
  return;
41173
41223
  }
41174
41224
  g.lastBody = body;
41225
+ g.lastSubstanceKey = substanceKey;
41175
41226
  g.lastEditAt = now;
41176
41227
  if (isTerminal) {
41177
41228
  log(`worker-feed: finish feed=${g.feedKey} chat=${g.chatId} thread=${g.threadId ?? "-"} ` + `msgId=${g.messageId} agent=${finishingAgentId ?? "-"} ` + `state=${opts2.terminalRecap?.state ?? "done"} bytes=${body.length}`);
@@ -41189,6 +41240,7 @@ function createWorkerActivityFeed(opts) {
41189
41240
  }
41190
41241
  if (outcome === "not_modified") {
41191
41242
  g.lastBody = body;
41243
+ g.lastSubstanceKey = substanceKey;
41192
41244
  g.lastEditAt = now;
41193
41245
  if (isTerminal)
41194
41246
  clearStaged();
@@ -41198,6 +41250,7 @@ function createWorkerActivityFeed(opts) {
41198
41250
  g.messageId = null;
41199
41251
  g.messageCreatedAtMs = 0;
41200
41252
  g.lastBody = null;
41253
+ g.lastSubstanceKey = null;
41201
41254
  if (isTerminal)
41202
41255
  clearStaged();
41203
41256
  else
@@ -41262,7 +41315,7 @@ function createWorkerActivityFeed(opts) {
41262
41315
  if (row.finished)
41263
41316
  staleFinished.push({ g, agentId: row.agentId, reason });
41264
41317
  else
41265
- staleAgentIds.push({ agentId: row.agentId, reason });
41318
+ staleAgentIds.push({ g, agentId: row.agentId, reason });
41266
41319
  }
41267
41320
  }
41268
41321
  for (const { g, agentId, reason } of staleFinished) {
@@ -41276,15 +41329,22 @@ function createWorkerActivityFeed(opts) {
41276
41329
  removeWorker(g, agentId);
41277
41330
  syncPin(g);
41278
41331
  }
41279
- for (const { agentId, reason } of staleAgentIds) {
41280
- const row = groupOfAgent(agentId)?.workers.get(agentId);
41332
+ for (const { g, agentId, reason } of staleAgentIds) {
41333
+ const row = g.workers.get(agentId);
41281
41334
  if (reason === "absolute") {
41282
41335
  const age = Math.floor((now - (row?.createdAtMs ?? now)) / 1000);
41283
41336
  log(`worker-feed: ABSOLUTE cap reap agent=${agentId} \u2014 row age ${age}s (>= ${Math.floor(absoluteRowLifetimeCapMs / 1000)}s); force-terminating immortal row (survives lastUpdateAt reset)`);
41284
41337
  } else {
41285
41338
  log(`worker-feed: TTL reap agent=${agentId} \u2014 no update in ${Math.floor((now - (row?.lastUpdateAt ?? now)) / 1000)}s (>= ${Math.floor(staleWorkerTtlMs / 1000)}s); force-terminating leaked row`);
41286
41339
  }
41287
- terminateWorker(agentId);
41340
+ if (groupOfAgent(agentId) === g) {
41341
+ terminateWorker(agentId);
41342
+ } else {
41343
+ log(`worker-feed: force-evicting leaked row agent=${agentId} feed=${g.feedKey} \u2014 agentIndex desynced (points elsewhere); removing directly`);
41344
+ markFinalized(agentId);
41345
+ evictRowFromGroup(g, agentId);
41346
+ syncPin(g);
41347
+ }
41288
41348
  }
41289
41349
  for (const g of [...groups.values()]) {
41290
41350
  if (g.pendingFinalize.size > 0 && now >= g.cooldownUntil) {
@@ -41307,6 +41367,7 @@ function createWorkerActivityFeed(opts) {
41307
41367
  g.messageId = null;
41308
41368
  g.messageCreatedAtMs = 0;
41309
41369
  g.lastBody = null;
41370
+ g.lastSubstanceKey = null;
41310
41371
  syncPin(g);
41311
41372
  opts.bot.editMessageText(g.chatId, retiredId, WORKER_CARD_SUPERSEDED_BODY, sendOptsFor(g)).catch(() => {});
41312
41373
  }
@@ -41330,6 +41391,14 @@ function createWorkerActivityFeed(opts) {
41330
41391
  }
41331
41392
  }
41332
41393
  heartbeatTimer = setIntervalFn(heartbeatTick, heartbeatTickMs);
41394
+ opts.exposeTestControls?.({
41395
+ repointAgentIndex: (agentId, feedKey) => {
41396
+ if (feedKey == null)
41397
+ agentIndex.delete(agentId);
41398
+ else
41399
+ agentIndex.set(agentId, feedKey);
41400
+ }
41401
+ });
41333
41402
  return {
41334
41403
  has(agentId) {
41335
41404
  const g = groupOfAgent(agentId);
@@ -41357,6 +41426,14 @@ function createWorkerActivityFeed(opts) {
41357
41426
  if (existingRow?.finished === true)
41358
41427
  return Promise.resolve();
41359
41428
  const feedKey = feedKeyOf(chatId, threadId);
41429
+ const priorFeedKey = agentIndex.get(agentId);
41430
+ if (priorFeedKey != null && priorFeedKey !== feedKey) {
41431
+ const priorGroup = groups.get(priorFeedKey);
41432
+ if (priorGroup != null) {
41433
+ evictRowFromGroup(priorGroup, agentId);
41434
+ syncPin(priorGroup);
41435
+ }
41436
+ }
41360
41437
  let g = groups.get(feedKey);
41361
41438
  if (g == null) {
41362
41439
  g = {
@@ -41366,6 +41443,7 @@ function createWorkerActivityFeed(opts) {
41366
41443
  messageId: null,
41367
41444
  messageCreatedAtMs: 0,
41368
41445
  lastBody: null,
41446
+ lastSubstanceKey: null,
41369
41447
  lastEditAt: 0,
41370
41448
  cooldownUntil: 0,
41371
41449
  chain: Promise.resolve(),
@@ -41379,6 +41457,7 @@ function createWorkerActivityFeed(opts) {
41379
41457
  g.messageId = null;
41380
41458
  g.messageCreatedAtMs = 0;
41381
41459
  g.lastBody = null;
41460
+ g.lastSubstanceKey = null;
41382
41461
  g.pendingFinalize.clear();
41383
41462
  g.terminalPainted = false;
41384
41463
  syncPin(g);
@@ -46193,6 +46272,207 @@ function createTurnTypingLoop(deps) {
46193
46272
  };
46194
46273
  }
46195
46274
 
46275
+ // gateway/handback-preturn-signal.ts
46276
+ var PRETURN_TURNKEY_PREFIX = "preturn:";
46277
+ function isHandbackInbound(msg) {
46278
+ return msg.type === "inbound" && msg.meta?.source === "subagent_handback";
46279
+ }
46280
+ function createHandbackPreturnSignal(deps) {
46281
+ const now = deps.now ?? (() => Date.now());
46282
+ const debounceMs = deps.debounceMs ?? 700;
46283
+ const adoptTimeoutMs = deps.adoptTimeoutMs ?? 30000;
46284
+ const setTimer = deps.setTimer ?? ((fn, ms) => {
46285
+ const t = setTimeout(fn, ms);
46286
+ t.unref?.();
46287
+ return t;
46288
+ });
46289
+ const clearTimer = deps.clearTimer ?? ((h) => clearTimeout(h));
46290
+ const log = deps.log ?? ((l) => process.stderr.write(l));
46291
+ const byKey = new Map;
46292
+ const bySyntheticKey = new Map;
46293
+ function clearTimers(entry) {
46294
+ if (entry.debounceTimer != null) {
46295
+ clearTimer(entry.debounceTimer);
46296
+ entry.debounceTimer = null;
46297
+ }
46298
+ if (entry.reapTimer != null) {
46299
+ clearTimer(entry.reapTimer);
46300
+ entry.reapTimer = null;
46301
+ }
46302
+ }
46303
+ function dropEntry(entry) {
46304
+ clearTimers(entry);
46305
+ byKey.delete(entry.statusKey);
46306
+ bySyntheticKey.delete(entry.syntheticTurnKey);
46307
+ }
46308
+ function emit(entry) {
46309
+ entry.debounceTimer = null;
46310
+ if (entry.consumed)
46311
+ return;
46312
+ if (deps.isTurnSettled?.(entry.statusKey)) {
46313
+ dropEntry(entry);
46314
+ return;
46315
+ }
46316
+ deps.startTypingLoop(entry.chatId, entry.threadId);
46317
+ entry.emitted = true;
46318
+ entry.reapTimer = setTimer(() => reap(entry), adoptTimeoutMs);
46319
+ Promise.resolve().then(() => deps.openCard(entry.chatId, entry.threadId)).then((messageId) => {
46320
+ if (messageId == null)
46321
+ return;
46322
+ if (entry.consumed) {
46323
+ deps.finalizeCard({
46324
+ turnKey: entry.syntheticTurnKey,
46325
+ chatId: entry.chatId,
46326
+ threadId: entry.threadId,
46327
+ activityMessageId: messageId,
46328
+ startedAt: entry.startedAt,
46329
+ pinned: entry.pinned
46330
+ });
46331
+ return;
46332
+ }
46333
+ entry.activityMessageId = messageId;
46334
+ const record = {
46335
+ turnKey: entry.syntheticTurnKey,
46336
+ chatId: entry.chatId,
46337
+ threadId: entry.threadId,
46338
+ activityMessageId: messageId,
46339
+ startedAt: entry.startedAt,
46340
+ pinned: entry.pinned
46341
+ };
46342
+ deps.writeCardRecord(record);
46343
+ }).catch((err) => {
46344
+ log(`handback-preturn-signal: openCard failed key=${entry.statusKey}: ` + `${err instanceof Error ? err.message : String(err)}
46345
+ `);
46346
+ });
46347
+ }
46348
+ function reap(entry) {
46349
+ entry.reapTimer = null;
46350
+ if (entry.consumed)
46351
+ return;
46352
+ entry.consumed = true;
46353
+ deps.stopTypingLoop(entry.chatId, entry.threadId);
46354
+ if (entry.activityMessageId != null) {
46355
+ const record = {
46356
+ turnKey: entry.syntheticTurnKey,
46357
+ chatId: entry.chatId,
46358
+ threadId: entry.threadId,
46359
+ activityMessageId: entry.activityMessageId,
46360
+ startedAt: entry.startedAt,
46361
+ pinned: entry.pinned
46362
+ };
46363
+ deps.clearCardRecord(entry.syntheticTurnKey, entry.activityMessageId);
46364
+ Promise.resolve(deps.finalizeCard(record)).catch((err) => {
46365
+ log(`handback-preturn-signal: orphan finalize failed key=${entry.statusKey}: ` + `${err instanceof Error ? err.message : String(err)}
46366
+ `);
46367
+ });
46368
+ }
46369
+ dropEntry(entry);
46370
+ }
46371
+ return {
46372
+ noteHandbackRelease(inbound) {
46373
+ if (!isHandbackInbound(inbound))
46374
+ return;
46375
+ const chatId = inbound.chatId;
46376
+ if (chatId == null || chatId === "")
46377
+ return;
46378
+ const threadId = inbound.threadId ?? null;
46379
+ const adoptTurnId = deps.deriveTurnId(chatId, threadId, inbound.messageId);
46380
+ if (adoptTurnId == null)
46381
+ return;
46382
+ const statusKey = deps.chatKey(chatId, threadId);
46383
+ if (byKey.has(statusKey))
46384
+ return;
46385
+ const startedAt = now();
46386
+ const syntheticTurnKey = `${PRETURN_TURNKEY_PREFIX}${statusKey}:${startedAt}`;
46387
+ const entry = {
46388
+ statusKey,
46389
+ chatId,
46390
+ threadId,
46391
+ adoptTurnId,
46392
+ syntheticTurnKey,
46393
+ startedAt,
46394
+ pinned: false,
46395
+ debounceTimer: null,
46396
+ reapTimer: null,
46397
+ activityMessageId: null,
46398
+ emitted: false,
46399
+ consumed: false
46400
+ };
46401
+ byKey.set(statusKey, entry);
46402
+ bySyntheticKey.set(syntheticTurnKey, statusKey);
46403
+ entry.debounceTimer = setTimer(() => emit(entry), debounceMs);
46404
+ },
46405
+ tryAdopt(turnId) {
46406
+ let entry;
46407
+ for (const e of byKey.values()) {
46408
+ if (e.adoptTurnId === turnId && !e.consumed) {
46409
+ entry = e;
46410
+ break;
46411
+ }
46412
+ }
46413
+ if (entry == null)
46414
+ return null;
46415
+ entry.consumed = true;
46416
+ clearTimers(entry);
46417
+ const adoption = {
46418
+ statusKey: entry.statusKey,
46419
+ chatId: entry.chatId,
46420
+ threadId: entry.threadId,
46421
+ activityMessageId: entry.activityMessageId,
46422
+ startedAt: entry.startedAt,
46423
+ pinned: entry.pinned
46424
+ };
46425
+ if (entry.activityMessageId != null) {
46426
+ deps.clearCardRecord(entry.syntheticTurnKey, entry.activityMessageId);
46427
+ deps.writeCardRecord({
46428
+ turnKey: entry.statusKey,
46429
+ chatId: entry.chatId,
46430
+ threadId: entry.threadId,
46431
+ activityMessageId: entry.activityMessageId,
46432
+ startedAt: entry.startedAt,
46433
+ pinned: entry.pinned
46434
+ });
46435
+ }
46436
+ dropEntry(entry);
46437
+ return adoption;
46438
+ },
46439
+ isPreTurnRecord(turnKey2) {
46440
+ return turnKey2.startsWith(PRETURN_TURNKEY_PREFIX);
46441
+ },
46442
+ handleReaped(turnKey2) {
46443
+ const statusKey = bySyntheticKey.get(turnKey2);
46444
+ if (statusKey == null)
46445
+ return;
46446
+ const entry = byKey.get(statusKey);
46447
+ if (entry == null)
46448
+ return;
46449
+ entry.consumed = true;
46450
+ deps.stopTypingLoop(entry.chatId, entry.threadId);
46451
+ dropEntry(entry);
46452
+ },
46453
+ pendingCount() {
46454
+ let n = 0;
46455
+ for (const e of byKey.values())
46456
+ if (!e.consumed)
46457
+ n++;
46458
+ return n;
46459
+ },
46460
+ stopAll() {
46461
+ for (const e of [...byKey.values()])
46462
+ clearTimers(e);
46463
+ byKey.clear();
46464
+ bySyntheticKey.clear();
46465
+ }
46466
+ };
46467
+ }
46468
+
46469
+ // gateway/derive-turn-id.ts
46470
+ function deriveTurnId(chatId, threadId, messageId) {
46471
+ if (messageId == null || messageId === "" || String(messageId) === "0")
46472
+ return null;
46473
+ return `${chatKey(chatId, threadId ?? null)}#${messageId}`;
46474
+ }
46475
+
46196
46476
  // typing-emitter.ts
46197
46477
  var TYPING_REFRESH_MS = 4000;
46198
46478
  var TYPING_FLOOR_MS = 3500;
@@ -56388,7 +56668,9 @@ var SEND_GATE_DEFAULTS = {
56388
56668
  perChatBurst: 3,
56389
56669
  perGroupPerMin: 18,
56390
56670
  perGroupBurst: 2,
56391
- editFloorMs: 1500
56671
+ editFloorMs: 1500,
56672
+ perMessageEditWindowMs: 300000,
56673
+ perMessageEditMaxPerWindow: 150
56392
56674
  };
56393
56675
  function createSendGate(config) {
56394
56676
  const enabled2 = config.enabled;
@@ -56400,6 +56682,8 @@ function createSendGate(config) {
56400
56682
  const perGroupPerMin = config.perGroupPerMin ?? SEND_GATE_DEFAULTS.perGroupPerMin;
56401
56683
  const perGroupBurst = config.perGroupBurst ?? SEND_GATE_DEFAULTS.perGroupBurst;
56402
56684
  const editFloorMs = config.editFloorMs ?? SEND_GATE_DEFAULTS.editFloorMs;
56685
+ const perMessageEditWindowMs = Math.max(1, Math.floor(config.perMessageEditWindowMs ?? SEND_GATE_DEFAULTS.perMessageEditWindowMs));
56686
+ const perMessageEditMaxPerWindow = Math.max(0, Math.floor(config.perMessageEditMaxPerWindow ?? SEND_GATE_DEFAULTS.perMessageEditMaxPerWindow));
56403
56687
  const messageStateTtlMs = config.messageStateTtlMs ?? 60000;
56404
56688
  const maxMessageStates = config.maxMessageStates ?? 5000;
56405
56689
  const usefulTtlMs = config.usefulTtlMs ?? 120000;
@@ -56415,7 +56699,8 @@ function createSendGate(config) {
56415
56699
  dropped: 0,
56416
56700
  shed: 0,
56417
56701
  expired: 0,
56418
- failedFast: 0
56702
+ failedFast: 0,
56703
+ budgetDeferred: 0
56419
56704
  };
56420
56705
  const bootStart = clock.now();
56421
56706
  const globalRamp = config.bootRamp ? {
@@ -56454,7 +56739,8 @@ function createSendGate(config) {
56454
56739
  lastHash: undefined,
56455
56740
  pending: null,
56456
56741
  running: false,
56457
- suppressedUntilMs: 0
56742
+ suppressedUntilMs: 0,
56743
+ editWindowTs: []
56458
56744
  };
56459
56745
  perMessage.set(key, state);
56460
56746
  }
@@ -56641,7 +56927,20 @@ function createSendGate(config) {
56641
56927
  try {
56642
56928
  while (state.pending) {
56643
56929
  const now = clock.now();
56644
- const readyAt = Math.max(state.lastSentMs + editFloorMs, state.suppressedUntilMs);
56930
+ let readyAt = Math.max(state.lastSentMs + editFloorMs, state.suppressedUntilMs);
56931
+ if (perMessageEditMaxPerWindow > 0 && state.pending.priorityClass === "cosmetic") {
56932
+ const windowStart = now - perMessageEditWindowMs;
56933
+ while (state.editWindowTs.length > 0 && state.editWindowTs[0] <= windowStart) {
56934
+ state.editWindowTs.shift();
56935
+ }
56936
+ if (state.editWindowTs.length >= perMessageEditMaxPerWindow) {
56937
+ const budgetReadyAt = state.editWindowTs[0] + perMessageEditWindowMs;
56938
+ if (budgetReadyAt > readyAt) {
56939
+ readyAt = budgetReadyAt;
56940
+ counters.budgetDeferred++;
56941
+ }
56942
+ }
56943
+ }
56645
56944
  const waitMs = readyAt - now;
56646
56945
  if (waitMs > 0) {
56647
56946
  await clock.sleep(waitMs);
@@ -56667,6 +56966,12 @@ function createSendGate(config) {
56667
56966
  await admit(bucketsFor(opts));
56668
56967
  }
56669
56968
  state.lastSentMs = clock.now();
56969
+ if (perMessageEditMaxPerWindow > 0 && p.priorityClass === "cosmetic") {
56970
+ state.editWindowTs.push(state.lastSentMs);
56971
+ const overflow = state.editWindowTs.length - (perMessageEditMaxPerWindow + 1);
56972
+ if (overflow > 0)
56973
+ state.editWindowTs.splice(0, overflow);
56974
+ }
56670
56975
  try {
56671
56976
  const res = await p.fn();
56672
56977
  state.lastHash = p.hash;
@@ -56854,6 +57159,12 @@ function sendGateConfigFromEnv(env = process.env) {
56854
57159
  const editFloorMs = parseNonNegativeInt(env.SWITCHROOM_TG_SEND_GATE_EDIT_FLOOR_MS);
56855
57160
  if (editFloorMs !== undefined)
56856
57161
  out.editFloorMs = editFloorMs;
57162
+ const perMsgWindowMs = parsePositiveInt(env.SWITCHROOM_TG_SEND_GATE_PER_MSG_EDIT_WINDOW_MS);
57163
+ if (perMsgWindowMs !== undefined)
57164
+ out.perMessageEditWindowMs = perMsgWindowMs;
57165
+ const perMsgMax = parseNonNegativeInt(env.SWITCHROOM_TG_SEND_GATE_PER_MSG_EDIT_MAX);
57166
+ if (perMsgMax !== undefined)
57167
+ out.perMessageEditMaxPerWindow = perMsgMax;
56857
57168
  const conservativeGlobal = parseBoolFlag(env.SWITCHROOM_TG_SEND_GATE_CONSERVATIVE_GLOBAL);
56858
57169
  if (conservativeGlobal !== undefined)
56859
57170
  out.conservativeGlobalFloodScope = conservativeGlobal;
@@ -70901,6 +71212,15 @@ function parseModelCommand(text4) {
70901
71212
  function isModelCommandBusy(ctx) {
70902
71213
  return ctx.currentTurnActive || ctx.turnInFlight;
70903
71214
  }
71215
+ function resolveStaleAwareBusy(input) {
71216
+ const turnStale = input.currentTurnActive && input.turnAgeMs !== null && input.turnAgeMs > input.hardTtlMs;
71217
+ const approvalLive = input.oldestPendingApprovalAgeMs !== null && input.oldestPendingApprovalAgeMs <= input.hardTtlMs;
71218
+ return {
71219
+ currentTurnActive: input.currentTurnActive && !turnStale,
71220
+ turnInFlight: input.machineInTurn || approvalLive,
71221
+ clearStaleTurn: turnStale
71222
+ };
71223
+ }
70904
71224
  function planModelCommand(parsed, ctx) {
70905
71225
  if (parsed.kind === "show" && ctx.menuEnabled)
70906
71226
  return { kind: "menu" };
@@ -75523,10 +75843,34 @@ function validateMs365Preview(input) {
75523
75843
  out.sizeBytesBefore = o.sizeBytesBefore;
75524
75844
  if (typeof o.sizeBytesAfter === "number")
75525
75845
  out.sizeBytesAfter = o.sizeBytesAfter;
75846
+ if (typeof o.eventWhen === "string")
75847
+ out.eventWhen = o.eventWhen;
75848
+ const changes = sanitizeChanges(o.changes);
75849
+ if (changes)
75850
+ out.changes = changes;
75526
75851
  if (typeof o.agentRationale === "string")
75527
75852
  out.agentRationale = o.agentRationale;
75528
75853
  return out;
75529
75854
  }
75855
+ function sanitizeChanges(input) {
75856
+ if (!Array.isArray(input))
75857
+ return;
75858
+ const out = [];
75859
+ for (const raw of input) {
75860
+ if (!raw || typeof raw !== "object")
75861
+ continue;
75862
+ const c = raw;
75863
+ if (typeof c.field !== "string" || c.field.length === 0)
75864
+ continue;
75865
+ const entry = { field: c.field };
75866
+ if (typeof c.before === "string")
75867
+ entry.before = c.before;
75868
+ if (typeof c.after === "string")
75869
+ entry.after = c.after;
75870
+ out.push(entry);
75871
+ }
75872
+ return out.length > 0 ? out : undefined;
75873
+ }
75530
75874
  var DEFAULT_TTL_MS2 = 5 * 60 * 1000;
75531
75875
  var MAX_TTL_MS2 = 30 * 60 * 1000;
75532
75876
  var MIN_TTL_MS2 = 30 * 1000;
@@ -75551,6 +75895,9 @@ function buildMs365CardText(p) {
75551
75895
  lines.push(`ID: ${truncate3(p.itemId, 96)}`);
75552
75896
  }
75553
75897
  lines.push(`Account: ${truncate3(p.accountEmail, 96)}`);
75898
+ if (p.eventWhen) {
75899
+ lines.push(`When: ${truncate3(p.eventWhen, 96)}`);
75900
+ }
75554
75901
  if (typeof p.sizeBytesBefore === "number" || typeof p.sizeBytesAfter === "number") {
75555
75902
  const before = p.sizeBytesBefore ?? 0;
75556
75903
  const after = p.sizeBytesAfter ?? 0;
@@ -75561,19 +75908,29 @@ function buildMs365CardText(p) {
75561
75908
  if (p.deepLink) {
75562
75909
  lines.push(`Link: ${truncate3(p.deepLink, 256)}`);
75563
75910
  }
75911
+ if (p.changes && p.changes.length > 0) {
75912
+ lines.push("");
75913
+ lines.push("Changes:");
75914
+ for (const c of p.changes.slice(0, 8)) {
75915
+ const before = c.before !== undefined ? truncate3(c.before, 96) : "(none)";
75916
+ const after = c.after !== undefined ? truncate3(c.after, 96) : "(cleared)";
75917
+ lines.push(`\u2022 ${c.field}: ${before} \u2192 ${after}`);
75918
+ }
75919
+ }
75564
75920
  if (p.agentRationale) {
75565
75921
  lines.push("");
75566
75922
  lines.push(`\uD83D\uDCAC ${truncate3(p.agentRationale, 512)}`);
75567
75923
  }
75568
75924
  lines.push("");
75569
- lines.push("\u26a0\ufe0f Weak attestation (RFC \u00a78 v1): operator should click through to verify the actual change before approving. Structural diff coming v1.5.");
75925
+ lines.push(p.changes && p.changes.length > 0 ? "\u26a0\ufe0f Attestation (RFC \u00a78 v1.5): the diff above is derived from live Graph state + the mutation payload. Verify before approving." : "\u26a0\ufe0f Weak attestation (RFC \u00a78 v1): operator should click through to verify the actual change before approving. Structural diff coming v1.5.");
75570
75926
  return hardenCardBreaks(lines.join(`
75571
75927
  `));
75572
75928
  }
75573
75929
  function truncate3(s, n) {
75574
- if (s.length <= n)
75575
- return s;
75576
- return s.slice(0, n - 1) + "\u2026";
75930
+ const oneLine = s.replace(/[\r\n\t]+/g, " ");
75931
+ if (oneLine.length <= n)
75932
+ return oneLine;
75933
+ return oneLine.slice(0, n - 1) + "\u2026";
75577
75934
  }
75578
75935
  function humanBytes(bytes) {
75579
75936
  const abs = Math.abs(bytes);
@@ -76431,6 +76788,28 @@ function workerAgentIdOfPinKey(pinKey) {
76431
76788
  const agentId = pinKey.slice(WORKER_PIN_KEY_PREFIX.length);
76432
76789
  return agentId.length > 0 ? agentId : null;
76433
76790
  }
76791
+ function storeOnlyWorkerPinCandidates(args) {
76792
+ const out = [];
76793
+ for (const r of args.rows) {
76794
+ if (workerAgentIdOfPinKey(r.pinKey) == null)
76795
+ continue;
76796
+ if (r.pending)
76797
+ continue;
76798
+ if (r.expiresAt != null)
76799
+ continue;
76800
+ if (r.chatId.length === 0)
76801
+ continue;
76802
+ if (args.inMemoryPinKeys.has(r.pinKey))
76803
+ continue;
76804
+ out.push({
76805
+ pinKey: r.pinKey,
76806
+ chatId: r.chatId,
76807
+ pinnedAt: args.now,
76808
+ messageId: r.messageId
76809
+ });
76810
+ }
76811
+ return out;
76812
+ }
76434
76813
  function decideWorkerPinReaps(args) {
76435
76814
  const reaps = [];
76436
76815
  for (const pin of args.pins) {
@@ -78164,6 +78543,7 @@ ${result}
78164
78543
  meta: {
78165
78544
  source: "subagent_handback",
78166
78545
  outcome: opts.ctx.outcome,
78546
+ message_id: String(ts),
78167
78547
  ...opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {},
78168
78548
  ...opts.ctx.jsonlAgentId ? { subagent_jsonl_id: opts.ctx.jsonlAgentId } : {}
78169
78549
  }
@@ -80002,6 +80382,7 @@ import {
80002
80382
  } from "node:fs";
80003
80383
  import { join as join39 } from "node:path";
80004
80384
  var TURN_ACTIVE_MARKER_FILE = "turn-active.json";
80385
+ var TURN_ACTIVE_HARD_TTL_MS = 10 * 60000;
80005
80386
  function touchTurnActiveMarker(stateDir) {
80006
80387
  const path2 = join39(stateDir, TURN_ACTIVE_MARKER_FILE);
80007
80388
  if (!existsSync34(path2))
@@ -83728,6 +84109,8 @@ import {
83728
84109
  } from "node:fs";
83729
84110
  import { join as join52 } from "node:path";
83730
84111
  var TURN_ACTIVE_MARKER_FILE2 = "turn-active.json";
84112
+ var TURN_ACTIVE_HARD_TTL_MS2 = 10 * 60000;
84113
+ var TURN_ACTIVE_IDLE_SWEEP_MS = 60000;
83731
84114
  function writeTurnActiveMarker(stateDir, marker) {
83732
84115
  try {
83733
84116
  mkdirSync37(stateDir, { recursive: true });
@@ -83794,12 +84177,15 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
83794
84177
  return null;
83795
84178
  }
83796
84179
  }
84180
+ function effectiveTurnAgeMs(markerAgeMs, turnStartedAt, now) {
84181
+ return markerAgeMs ?? now - turnStartedAt;
84182
+ }
83797
84183
 
83798
84184
  // ../src/build-info.ts
83799
- var VERSION = "0.18.26";
83800
- var COMMIT_SHA = "ceb7d1a9";
83801
- var COMMIT_DATE = "2026-07-15T11:05:52Z";
83802
- var LATEST_PR = 3258;
84185
+ var VERSION = "0.18.28";
84186
+ var COMMIT_SHA = "ef0be9a2";
84187
+ var COMMIT_DATE = "2026-07-16T14:42:35+10:00";
84188
+ var LATEST_PR = 3274;
83803
84189
  var COMMITS_AHEAD_OF_TAG = 0;
83804
84190
 
83805
84191
  // gateway/boot-version.ts
@@ -86479,9 +86865,43 @@ var FEED_LIVENESS_OPEN_MS = (() => {
86479
86865
  var POST_ANSWER_LIVENESS_STALE_MS = parsePostAnswerLivenessMs(process.env.SWITCHROOM_POST_ANSWER_LIVENESS_STALE_MS) || 30000;
86480
86866
  function turnInFlightForGate() {
86481
86867
  const hasPendingApproval = pendingPermissions.size > 0;
86868
+ return turnInFlightMachineOnly() || hasPendingApproval;
86869
+ }
86870
+ function turnInFlightMachineOnly() {
86482
86871
  if (!isDeliveryCutoverEnabled())
86483
- return claudeBusyKeys.size > 0 || hasPendingApproval;
86484
- return probeGateParity(isMachineInTurn(), claudeBusyKeys.size) || hasPendingApproval;
86872
+ return claudeBusyKeys.size > 0;
86873
+ return probeGateParity(isMachineInTurn(), claudeBusyKeys.size);
86874
+ }
86875
+ function liveTurnAgeMs(now) {
86876
+ if (currentTurn === null)
86877
+ return null;
86878
+ return effectiveTurnAgeMs(readTurnActiveMarkerAgeMs(STATE_DIR, now), currentTurn.startedAt, now);
86879
+ }
86880
+ function oldestPendingApprovalAgeMs(now) {
86881
+ let oldest = null;
86882
+ for (const p of pendingPermissions.values()) {
86883
+ const age = now - p.startedAt;
86884
+ if (oldest === null || age > oldest)
86885
+ oldest = age;
86886
+ }
86887
+ return oldest;
86888
+ }
86889
+ function resolveModelEffortBusy(now = Date.now()) {
86890
+ const turnAgeMs = liveTurnAgeMs(now);
86891
+ const resolved = resolveStaleAwareBusy({
86892
+ currentTurnActive: currentTurn !== null,
86893
+ turnAgeMs,
86894
+ machineInTurn: turnInFlightMachineOnly(),
86895
+ oldestPendingApprovalAgeMs: oldestPendingApprovalAgeMs(now),
86896
+ hardTtlMs: TURN_ACTIVE_HARD_TTL_MS2
86897
+ });
86898
+ if (resolved.clearStaleTurn && currentTurn !== null) {
86899
+ const ageSec = Math.round((turnAgeMs ?? 0) / 1000);
86900
+ process.stderr.write(`telegram gateway: [phantomturn] cleared stale currentTurn atom age=${ageSec}s ttl=${Math.round(TURN_ACTIVE_HARD_TTL_MS2 / 1000)}s for /model|/effort busy-check agent=${getMyAgentName()}
86901
+ `);
86902
+ clearAllCurrentTurns();
86903
+ }
86904
+ return { currentTurnActive: resolved.currentTurnActive, turnInFlight: resolved.turnInFlight };
86485
86905
  }
86486
86906
  function deliverResumeSyntheticOrBuffer(agent, inbound) {
86487
86907
  const decision = decideInboundDelivery({
@@ -86649,11 +87069,6 @@ function findTurnByQuotedMessageId(chatId, replyTo) {
86649
87069
  return null;
86650
87070
  return turn;
86651
87071
  }
86652
- function deriveTurnId(chatId, threadId, messageId) {
86653
- if (messageId == null || messageId === "" || String(messageId) === "0")
86654
- return null;
86655
- return `${chatKey2(chatId, threadId ?? null)}#${messageId}`;
86656
- }
86657
87072
  function findTurnByOriginId(originTurnId) {
86658
87073
  if (originTurnId == null || originTurnId === "")
86659
87074
  return null;
@@ -88479,8 +88894,8 @@ var pendingStateReaper = setInterval(() => {
88479
88894
  try {
88480
88895
  sweepStaleTurnActiveMarker(STATE_DIR, {
88481
88896
  turnInFlight: currentTurn?.registryKey != null,
88482
- idleSweepMs: 60000,
88483
- hardTtlMs: 600000,
88897
+ idleSweepMs: TURN_ACTIVE_IDLE_SWEEP_MS,
88898
+ hardTtlMs: TURN_ACTIVE_HARD_TTL_MS2,
88484
88899
  now,
88485
88900
  onRemove: ({ ageMs, reason, payload }) => {
88486
88901
  const agent = getMyAgentName();
@@ -88927,11 +89342,16 @@ async function runMidSessionCardReaper() {
88927
89342
  isLive: (record2) => topicKeys.has(record2.turnKey),
88928
89343
  ttlMs: MID_SESSION_CARD_REAPER_TTL_MS,
88929
89344
  now,
88930
- finalizeCard: (record2) => robustApiCall(() => lockedBot.api.editMessageText(record2.chatId, record2.activityMessageId, richMessage2(restartOrphanCardFinalizeText(record2.startedAt)), {}), {
88931
- chat_id: record2.chatId,
88932
- ...record2.threadId != null ? { threadId: record2.threadId } : {},
88933
- verb: "activity-card.mid-session-reap-finalize"
88934
- }),
89345
+ finalizeCard: (record2) => {
89346
+ if (handbackPreturnSignal.isPreTurnRecord(record2.turnKey)) {
89347
+ handbackPreturnSignal.handleReaped(record2.turnKey);
89348
+ }
89349
+ return robustApiCall(() => lockedBot.api.editMessageText(record2.chatId, record2.activityMessageId, richMessage2(restartOrphanCardFinalizeText(record2.startedAt)), {}), {
89350
+ chat_id: record2.chatId,
89351
+ ...record2.threadId != null ? { threadId: record2.threadId } : {},
89352
+ verb: "activity-card.mid-session-reap-finalize"
89353
+ });
89354
+ },
88935
89355
  unpinCard: async (record2) => {
88936
89356
  const pinKey = `fg:${record2.turnKey}`;
88937
89357
  if (statusPinState.has(pinKey)) {
@@ -88956,11 +89376,19 @@ async function runMidSessionCardReaper() {
88956
89376
  }
88957
89377
  if (WORKER_PIN_REAPER_ENABLED && PIN_STATUS_WHILE_WORKING) {
88958
89378
  try {
88959
- const candidates = [...statusPinState.keys()].filter((k) => k.startsWith("wk:")).map((k) => ({
89379
+ const inMemoryKeys = new Set([...statusPinState.keys()].filter((k) => k.startsWith("wk:")));
89380
+ const inMemoryCandidates = [...inMemoryKeys].map((k) => ({
88960
89381
  pinKey: k,
88961
89382
  chatId: statusPinChatIds.get(k) ?? "",
88962
89383
  pinnedAt: statusPinPinnedAt.get(k) ?? now
88963
89384
  }));
89385
+ const storeOnlyCandidates = statusPinPersistEnabled ? storeOnlyWorkerPinCandidates({
89386
+ rows: loadStatusPins(STATUS_PIN_STORE_PATH, statusPinStoreFs),
89387
+ inMemoryPinKeys: inMemoryKeys,
89388
+ now
89389
+ }) : [];
89390
+ const storeOnlyKeys = new Set(storeOnlyCandidates.map((c) => c.pinKey));
89391
+ const candidates = [...inMemoryCandidates, ...storeOnlyCandidates];
88964
89392
  const reaps = decideWorkerPinReaps({
88965
89393
  pins: candidates,
88966
89394
  statusOf: (agentId) => {
@@ -88987,9 +89415,20 @@ async function runMidSessionCardReaper() {
88987
89415
  now
88988
89416
  });
88989
89417
  for (const reap of reaps) {
88990
- process.stderr.write(`telegram gateway: worker-pin reaper unpinning ${reap.pinKey} (chat=${reap.chatId} reason=${reap.reason})
89418
+ const storeOnly = storeOnlyKeys.has(reap.pinKey);
89419
+ process.stderr.write(`telegram gateway: worker-pin reaper unpinning ${reap.pinKey} (chat=${reap.chatId} reason=${reap.reason}${storeOnly ? " source=store-orphan" : ""})
88991
89420
  `);
88992
- await reconcileStatusPin(reap.pinKey, reap.chatId, { pinned: false });
89421
+ if (storeOnly && reap.messageId != null) {
89422
+ try {
89423
+ await statusPinApi().unpinChatMessage(reap.chatId, reap.messageId);
89424
+ } catch (err) {
89425
+ process.stderr.write(`telegram gateway: worker-pin reaper store-orphan unpin failed (${reap.pinKey} chat=${reap.chatId} msg=${reap.messageId}): ${err.message}
89426
+ `);
89427
+ }
89428
+ await mutateStatusPinRow(STATUS_PIN_STORE_PATH, statusPinStoreFs, reap.pinKey, null);
89429
+ } else {
89430
+ await reconcileStatusPin(reap.pinKey, reap.chatId, { pinned: false });
89431
+ }
88993
89432
  }
88994
89433
  } catch (err) {
88995
89434
  process.stderr.write(`telegram gateway: worker-pin reaper error: ${err.message}
@@ -89411,6 +89850,8 @@ var _deliveryMachineTick = setInterval(() => {
89411
89850
  }, DELIVERY_MACHINE_TICK_MS);
89412
89851
  _deliveryMachineTick.unref?.();
89413
89852
  function trackRedeliveredInbound(merged) {
89853
+ if (HANDBACK_PRETURN_ENABLED)
89854
+ handbackPreturnSignal.noteHandbackRelease(merged);
89414
89855
  if (!DELIVERY_CONFIRM_ENABLED)
89415
89856
  return;
89416
89857
  const isTrackableResume = isTrackableResumeSynthetic(merged.meta);
@@ -91090,6 +91531,7 @@ async function executeReply(args) {
91090
91531
  return { content: [{ type: "text", text: "sent (deduped \u2014 same content sent via earlier path)" }] };
91091
91532
  }
91092
91533
  }
91534
+ let supersedeFlushIds = [];
91093
91535
  {
91094
91536
  const replyThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
91095
91537
  const ownerTurn = resolveReplyOwnerTurn(turn, chat_id, args);
@@ -91098,9 +91540,9 @@ async function executeReply(args) {
91098
91540
  if (decision.supersede) {
91099
91541
  process.stderr.write(`telegram gateway: reply: superseding flushed turn message(s) chatId=${chat_id} ids=${JSON.stringify(decision.deleteMessageIds)}
91100
91542
  `);
91101
- for (const id of decision.deleteMessageIds) {
91102
- await swallowingApiCall(() => lockedBot.api.deleteMessage(chat_id, id), { chat_id, verb: "reply.supersedeFlushed" });
91103
- }
91543
+ supersedeFlushIds = decision.deleteMessageIds;
91544
+ if (ownerTurn != null)
91545
+ ownerTurn.answerDelivered = true;
91104
91546
  } else {
91105
91547
  const replySubstantive = isSubstantiveFinalReply({
91106
91548
  text: rawText,
@@ -91328,6 +91770,25 @@ ${url}`;
91328
91770
  });
91329
91771
  }
91330
91772
  }
91773
+ if (supersedeFlushIds.length > 0) {
91774
+ const correction = decideSupersedeCorrection({
91775
+ flushMessageIds: supersedeFlushIds,
91776
+ chunkCount: chunks.length,
91777
+ hasFiles: files.length > 0,
91778
+ suppressText,
91779
+ hasOpenPreview: previewMessageId != null
91780
+ });
91781
+ if (correction.mode === "edit-in-place") {
91782
+ previewMessageId = correction.editMessageId;
91783
+ reply_to = undefined;
91784
+ process.stderr.write(`telegram gateway: reply: superseding flushed message via edit-in-place chatId=${chat_id} id=${correction.editMessageId}
91785
+ `);
91786
+ } else {
91787
+ for (const id of correction.deleteMessageIds) {
91788
+ await swallowingApiCall(() => lockedBot.api.deleteMessage(chat_id, id), { chat_id, verb: "reply.supersedeFlushed" });
91789
+ }
91790
+ }
91791
+ }
91331
91792
  if (previewMessageId != null && reply_to != null && replyMode !== "off") {
91332
91793
  await deleteStalePreview(previewMessageId);
91333
91794
  previewMessageId = null;
@@ -91336,7 +91797,7 @@ ${url}`;
91336
91797
  let silentAnchorEditDone = false;
91337
91798
  {
91338
91799
  const turn2 = currentTurn;
91339
- if (turn2 != null && chunks.length === 1) {
91800
+ if (turn2 != null && chunks.length === 1 && supersedeFlushIds.length === 0) {
91340
91801
  const decision = decideSilentReplyAnchor({
91341
91802
  effectivelySilent: disableNotification,
91342
91803
  anchorMessageId: turn2.silentAnchorMessageId,
@@ -93151,6 +93612,61 @@ function clearActivitySummary(turn, finalHtmlOverride) {
93151
93612
  }
93152
93613
  });
93153
93614
  }
93615
+ var HANDBACK_PRETURN_ENABLED = !STATIC && process.env.SWITCHROOM_HANDBACK_PRETURN !== "0";
93616
+ var HANDBACK_PRETURN_HTML = "\uD83E\uDD1D Reading the worker\u2019s results\u2026";
93617
+ var HANDBACK_PRETURN_ORPHAN_HTML = "\uD83E\uDD1D A background worker finished, but the handback never started \u2014 it may need a nudge.";
93618
+ async function openHandbackPreTurnCard(chatId, threadId) {
93619
+ if (STATIC)
93620
+ return null;
93621
+ try {
93622
+ const sent = await robustApiCall(() => bot.api.sendRichMessage(chatId, richMessage2(HANDBACK_PRETURN_HTML), {
93623
+ ...threadId != null ? { message_thread_id: threadId } : {},
93624
+ disable_notification: true
93625
+ }), {
93626
+ chat_id: chatId,
93627
+ ...threadId != null ? { threadId } : {},
93628
+ verb: "handback-preturn.send"
93629
+ });
93630
+ return sent?.message_id ?? null;
93631
+ } catch (err) {
93632
+ process.stderr.write(`telegram gateway: handback pre-turn card send failed: ${err.message}
93633
+ `);
93634
+ return null;
93635
+ }
93636
+ }
93637
+ function finalizeHandbackPreTurnCard(record2) {
93638
+ return robustApiCall(() => bot.api.editMessageText(record2.chatId, record2.activityMessageId, richMessage2(HANDBACK_PRETURN_ORPHAN_HTML), {}), {
93639
+ chat_id: record2.chatId,
93640
+ ...record2.threadId != null ? { threadId: record2.threadId } : {},
93641
+ verb: "handback-preturn.orphan-finalize"
93642
+ }).then(() => {
93643
+ return;
93644
+ }).catch(() => {
93645
+ return;
93646
+ });
93647
+ }
93648
+ var handbackPreturnSignal = createHandbackPreturnSignal({
93649
+ chatKey: (chatId, threadId) => chatKey2(chatId, threadId),
93650
+ deriveTurnId: (chatId, threadId, messageId) => deriveTurnId(chatId, threadId, messageId),
93651
+ startTypingLoop: (chatId, threadId) => startTurnTypingLoop(chatId, threadId),
93652
+ stopTypingLoop: (chatId, threadId) => stopTurnTypingLoop(chatId, threadId),
93653
+ openCard: openHandbackPreTurnCard,
93654
+ finalizeCard: finalizeHandbackPreTurnCard,
93655
+ writeCardRecord: (record2) => {
93656
+ if (!activityCardPersistEnabled)
93657
+ return;
93658
+ writeActivityCardRecord(ACTIVITY_CARD_STORE_PATH, activityCardStoreFs, record2);
93659
+ },
93660
+ clearCardRecord: (turnKey2, activityMessageId) => {
93661
+ if (!activityCardPersistEnabled)
93662
+ return;
93663
+ clearActivityCardRecord(ACTIVITY_CARD_STORE_PATH, activityCardStoreFs, turnKey2, activityMessageId);
93664
+ },
93665
+ isTurnSettled: (key) => {
93666
+ const live = currentTurnMap.get(key);
93667
+ return live != null && (live.finalAnswerDelivered || live.endedAt != null);
93668
+ }
93669
+ });
93154
93670
  var memoryLegibilityStager = new MemoryLegibilityStager;
93155
93671
  function sendMemoryLegibilityLine(event, chatId, threadId) {
93156
93672
  const line = renderMemoryLegibilityLine(event);
@@ -93304,6 +93820,16 @@ function handleSessionEvent(ev) {
93304
93820
  emissionAuthority: new EmissionAuthority(statusKey(ev.chatId, enqThreadIdNum))
93305
93821
  };
93306
93822
  next.narrativeGate = makeNarrativeGate(next);
93823
+ if (HANDBACK_PRETURN_ENABLED) {
93824
+ const handbackAdoption = handbackPreturnSignal.tryAdopt(turnId);
93825
+ if (handbackAdoption != null) {
93826
+ if (handbackAdoption.activityMessageId != null) {
93827
+ next.activityMessageId = handbackAdoption.activityMessageId;
93828
+ next.activityEverOpened = true;
93829
+ }
93830
+ startTurnTypingLoop(ev.chatId, enqThreadIdNum ?? null);
93831
+ }
93832
+ }
93307
93833
  setCurrentTurn(next, statusKey(ev.chatId, enqThreadIdNum));
93308
93834
  scheduleEarlyLivenessOpen(next);
93309
93835
  process.stderr.write(`telegram gateway: ${formatTurnLifecycle("set", "enqueue", next, startedAt)}
@@ -96350,7 +96876,8 @@ bot.command("model", async (ctx) => {
96350
96876
  const parsed = parseModelCommand(text5) ?? { kind: "show" };
96351
96877
  const chatId = String(ctx.chat.id);
96352
96878
  const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
96353
- const busyNow = currentTurn !== null || turnInFlightForGate();
96879
+ const modelBusy = resolveModelEffortBusy();
96880
+ const busyNow = modelBusy.currentTurnActive || modelBusy.turnInFlight;
96354
96881
  process.stderr.write(modelCommandReceiptLine(getMyAgentName(), parsed, busyNow) + `
96355
96882
  `);
96356
96883
  if (HISTORY_ENABLED && ctx.message?.message_id != null) {
@@ -96371,8 +96898,8 @@ bot.command("model", async (ctx) => {
96371
96898
  }
96372
96899
  const deps = buildModelDeps({ chatId, threadId });
96373
96900
  const disposition = planModelCommand(parsed, {
96374
- currentTurnActive: currentTurn !== null,
96375
- turnInFlight: turnInFlightForGate(),
96901
+ currentTurnActive: modelBusy.currentTurnActive,
96902
+ turnInFlight: modelBusy.turnInFlight,
96376
96903
  menuEnabled: process.env.SWITCHROOM_MODEL_MENU !== "0"
96377
96904
  });
96378
96905
  if (disposition.kind === "menu") {
@@ -96443,7 +96970,8 @@ bot.command("effort", async (ctx) => {
96443
96970
  await switchroomReply(ctx, menu.text, { html: true, reply_markup: effortMenuReplyMarkup(menu) });
96444
96971
  return;
96445
96972
  }
96446
- if ((parsed.kind === "set" || parsed.kind === "default") && currentTurn !== null) {
96973
+ const effortBusy = resolveModelEffortBusy();
96974
+ if ((parsed.kind === "set" || parsed.kind === "default") && effortBusy.currentTurnActive) {
96447
96975
  const requestedLevel = parsed.kind === "set" ? parsed.level : "default";
96448
96976
  const chatId = String(ctx.chat.id);
96449
96977
  const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id);