switchroom 0.18.26 → 0.18.27

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 (32) 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 +494 -36
  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 +305 -41
  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/send-gate.test.ts +138 -0
  18. package/telegram-plugin/send-gate.ts +104 -1
  19. package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +14 -4
  20. package/telegram-plugin/tests/effort-command.test.ts +47 -0
  21. package/telegram-plugin/tests/flushed-turn-supersede.test.ts +60 -0
  22. package/telegram-plugin/tests/handback-preturn-adoption-roundtrip.test.ts +211 -0
  23. package/telegram-plugin/tests/handback-preturn-signal.test.ts +346 -0
  24. package/telegram-plugin/tests/model-command.test.ts +112 -0
  25. package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +14 -2
  26. package/telegram-plugin/tests/outbound-send-chunks.test.ts +57 -0
  27. package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +18 -11
  28. package/telegram-plugin/tests/reply-owner-resolve.test.ts +90 -0
  29. package/telegram-plugin/tests/subagent-handback-inbound-builder.test.ts +5 -0
  30. package/telegram-plugin/tests/turn-active-marker.test.ts +29 -0
  31. package/telegram-plugin/tests/worker-activity-feed.test.ts +121 -0
  32. package/telegram-plugin/worker-activity-feed.ts +91 -1
@@ -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,6 +41084,35 @@ 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);
@@ -41125,6 +41162,7 @@ function createWorkerActivityFeed(opts) {
41125
41162
  return;
41126
41163
  }
41127
41164
  const body = renderGroupBody(g, now, opts2.terminalRecap ?? null, opts2.heartbeat ?? false);
41165
+ const substanceKey = groupSubstanceKey(g, opts2.terminalRecap ?? null);
41128
41166
  if (body == null) {
41129
41167
  if (isTerminal)
41130
41168
  clearStaged();
@@ -41148,6 +41186,7 @@ function createWorkerActivityFeed(opts) {
41148
41186
  g.messageId = sent.message_id;
41149
41187
  g.messageCreatedAtMs = now;
41150
41188
  g.lastBody = body;
41189
+ g.lastSubstanceKey = substanceKey;
41151
41190
  g.lastEditAt = now;
41152
41191
  g.terminalPainted = false;
41153
41192
  syncPin(g);
@@ -41163,8 +41202,13 @@ function createWorkerActivityFeed(opts) {
41163
41202
  clearStaged();
41164
41203
  return;
41165
41204
  }
41166
- if (!opts2.force && now - g.lastEditAt < minEditInterval)
41167
- return;
41205
+ if (!opts2.force && !isTerminal) {
41206
+ if (now - g.lastEditAt < minEditInterval)
41207
+ return;
41208
+ const substanceChanged = substanceKey !== g.lastSubstanceKey;
41209
+ if (!substanceChanged && now - g.lastEditAt < elapsedRefreshMs)
41210
+ return;
41211
+ }
41168
41212
  try {
41169
41213
  const res = await opts.bot.editMessageText(g.chatId, g.messageId, body, sendOptsFor(g));
41170
41214
  if (isSendGateShed(res)) {
@@ -41172,6 +41216,7 @@ function createWorkerActivityFeed(opts) {
41172
41216
  return;
41173
41217
  }
41174
41218
  g.lastBody = body;
41219
+ g.lastSubstanceKey = substanceKey;
41175
41220
  g.lastEditAt = now;
41176
41221
  if (isTerminal) {
41177
41222
  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 +41234,7 @@ function createWorkerActivityFeed(opts) {
41189
41234
  }
41190
41235
  if (outcome === "not_modified") {
41191
41236
  g.lastBody = body;
41237
+ g.lastSubstanceKey = substanceKey;
41192
41238
  g.lastEditAt = now;
41193
41239
  if (isTerminal)
41194
41240
  clearStaged();
@@ -41198,6 +41244,7 @@ function createWorkerActivityFeed(opts) {
41198
41244
  g.messageId = null;
41199
41245
  g.messageCreatedAtMs = 0;
41200
41246
  g.lastBody = null;
41247
+ g.lastSubstanceKey = null;
41201
41248
  if (isTerminal)
41202
41249
  clearStaged();
41203
41250
  else
@@ -41307,6 +41354,7 @@ function createWorkerActivityFeed(opts) {
41307
41354
  g.messageId = null;
41308
41355
  g.messageCreatedAtMs = 0;
41309
41356
  g.lastBody = null;
41357
+ g.lastSubstanceKey = null;
41310
41358
  syncPin(g);
41311
41359
  opts.bot.editMessageText(g.chatId, retiredId, WORKER_CARD_SUPERSEDED_BODY, sendOptsFor(g)).catch(() => {});
41312
41360
  }
@@ -41366,6 +41414,7 @@ function createWorkerActivityFeed(opts) {
41366
41414
  messageId: null,
41367
41415
  messageCreatedAtMs: 0,
41368
41416
  lastBody: null,
41417
+ lastSubstanceKey: null,
41369
41418
  lastEditAt: 0,
41370
41419
  cooldownUntil: 0,
41371
41420
  chain: Promise.resolve(),
@@ -41379,6 +41428,7 @@ function createWorkerActivityFeed(opts) {
41379
41428
  g.messageId = null;
41380
41429
  g.messageCreatedAtMs = 0;
41381
41430
  g.lastBody = null;
41431
+ g.lastSubstanceKey = null;
41382
41432
  g.pendingFinalize.clear();
41383
41433
  g.terminalPainted = false;
41384
41434
  syncPin(g);
@@ -46193,6 +46243,207 @@ function createTurnTypingLoop(deps) {
46193
46243
  };
46194
46244
  }
46195
46245
 
46246
+ // gateway/handback-preturn-signal.ts
46247
+ var PRETURN_TURNKEY_PREFIX = "preturn:";
46248
+ function isHandbackInbound(msg) {
46249
+ return msg.type === "inbound" && msg.meta?.source === "subagent_handback";
46250
+ }
46251
+ function createHandbackPreturnSignal(deps) {
46252
+ const now = deps.now ?? (() => Date.now());
46253
+ const debounceMs = deps.debounceMs ?? 700;
46254
+ const adoptTimeoutMs = deps.adoptTimeoutMs ?? 30000;
46255
+ const setTimer = deps.setTimer ?? ((fn, ms) => {
46256
+ const t = setTimeout(fn, ms);
46257
+ t.unref?.();
46258
+ return t;
46259
+ });
46260
+ const clearTimer = deps.clearTimer ?? ((h) => clearTimeout(h));
46261
+ const log = deps.log ?? ((l) => process.stderr.write(l));
46262
+ const byKey = new Map;
46263
+ const bySyntheticKey = new Map;
46264
+ function clearTimers(entry) {
46265
+ if (entry.debounceTimer != null) {
46266
+ clearTimer(entry.debounceTimer);
46267
+ entry.debounceTimer = null;
46268
+ }
46269
+ if (entry.reapTimer != null) {
46270
+ clearTimer(entry.reapTimer);
46271
+ entry.reapTimer = null;
46272
+ }
46273
+ }
46274
+ function dropEntry(entry) {
46275
+ clearTimers(entry);
46276
+ byKey.delete(entry.statusKey);
46277
+ bySyntheticKey.delete(entry.syntheticTurnKey);
46278
+ }
46279
+ function emit(entry) {
46280
+ entry.debounceTimer = null;
46281
+ if (entry.consumed)
46282
+ return;
46283
+ if (deps.isTurnSettled?.(entry.statusKey)) {
46284
+ dropEntry(entry);
46285
+ return;
46286
+ }
46287
+ deps.startTypingLoop(entry.chatId, entry.threadId);
46288
+ entry.emitted = true;
46289
+ entry.reapTimer = setTimer(() => reap(entry), adoptTimeoutMs);
46290
+ Promise.resolve().then(() => deps.openCard(entry.chatId, entry.threadId)).then((messageId) => {
46291
+ if (messageId == null)
46292
+ return;
46293
+ if (entry.consumed) {
46294
+ deps.finalizeCard({
46295
+ turnKey: entry.syntheticTurnKey,
46296
+ chatId: entry.chatId,
46297
+ threadId: entry.threadId,
46298
+ activityMessageId: messageId,
46299
+ startedAt: entry.startedAt,
46300
+ pinned: entry.pinned
46301
+ });
46302
+ return;
46303
+ }
46304
+ entry.activityMessageId = messageId;
46305
+ const record = {
46306
+ turnKey: entry.syntheticTurnKey,
46307
+ chatId: entry.chatId,
46308
+ threadId: entry.threadId,
46309
+ activityMessageId: messageId,
46310
+ startedAt: entry.startedAt,
46311
+ pinned: entry.pinned
46312
+ };
46313
+ deps.writeCardRecord(record);
46314
+ }).catch((err) => {
46315
+ log(`handback-preturn-signal: openCard failed key=${entry.statusKey}: ` + `${err instanceof Error ? err.message : String(err)}
46316
+ `);
46317
+ });
46318
+ }
46319
+ function reap(entry) {
46320
+ entry.reapTimer = null;
46321
+ if (entry.consumed)
46322
+ return;
46323
+ entry.consumed = true;
46324
+ deps.stopTypingLoop(entry.chatId, entry.threadId);
46325
+ if (entry.activityMessageId != null) {
46326
+ const record = {
46327
+ turnKey: entry.syntheticTurnKey,
46328
+ chatId: entry.chatId,
46329
+ threadId: entry.threadId,
46330
+ activityMessageId: entry.activityMessageId,
46331
+ startedAt: entry.startedAt,
46332
+ pinned: entry.pinned
46333
+ };
46334
+ deps.clearCardRecord(entry.syntheticTurnKey, entry.activityMessageId);
46335
+ Promise.resolve(deps.finalizeCard(record)).catch((err) => {
46336
+ log(`handback-preturn-signal: orphan finalize failed key=${entry.statusKey}: ` + `${err instanceof Error ? err.message : String(err)}
46337
+ `);
46338
+ });
46339
+ }
46340
+ dropEntry(entry);
46341
+ }
46342
+ return {
46343
+ noteHandbackRelease(inbound) {
46344
+ if (!isHandbackInbound(inbound))
46345
+ return;
46346
+ const chatId = inbound.chatId;
46347
+ if (chatId == null || chatId === "")
46348
+ return;
46349
+ const threadId = inbound.threadId ?? null;
46350
+ const adoptTurnId = deps.deriveTurnId(chatId, threadId, inbound.messageId);
46351
+ if (adoptTurnId == null)
46352
+ return;
46353
+ const statusKey = deps.chatKey(chatId, threadId);
46354
+ if (byKey.has(statusKey))
46355
+ return;
46356
+ const startedAt = now();
46357
+ const syntheticTurnKey = `${PRETURN_TURNKEY_PREFIX}${statusKey}:${startedAt}`;
46358
+ const entry = {
46359
+ statusKey,
46360
+ chatId,
46361
+ threadId,
46362
+ adoptTurnId,
46363
+ syntheticTurnKey,
46364
+ startedAt,
46365
+ pinned: false,
46366
+ debounceTimer: null,
46367
+ reapTimer: null,
46368
+ activityMessageId: null,
46369
+ emitted: false,
46370
+ consumed: false
46371
+ };
46372
+ byKey.set(statusKey, entry);
46373
+ bySyntheticKey.set(syntheticTurnKey, statusKey);
46374
+ entry.debounceTimer = setTimer(() => emit(entry), debounceMs);
46375
+ },
46376
+ tryAdopt(turnId) {
46377
+ let entry;
46378
+ for (const e of byKey.values()) {
46379
+ if (e.adoptTurnId === turnId && !e.consumed) {
46380
+ entry = e;
46381
+ break;
46382
+ }
46383
+ }
46384
+ if (entry == null)
46385
+ return null;
46386
+ entry.consumed = true;
46387
+ clearTimers(entry);
46388
+ const adoption = {
46389
+ statusKey: entry.statusKey,
46390
+ chatId: entry.chatId,
46391
+ threadId: entry.threadId,
46392
+ activityMessageId: entry.activityMessageId,
46393
+ startedAt: entry.startedAt,
46394
+ pinned: entry.pinned
46395
+ };
46396
+ if (entry.activityMessageId != null) {
46397
+ deps.clearCardRecord(entry.syntheticTurnKey, entry.activityMessageId);
46398
+ deps.writeCardRecord({
46399
+ turnKey: entry.statusKey,
46400
+ chatId: entry.chatId,
46401
+ threadId: entry.threadId,
46402
+ activityMessageId: entry.activityMessageId,
46403
+ startedAt: entry.startedAt,
46404
+ pinned: entry.pinned
46405
+ });
46406
+ }
46407
+ dropEntry(entry);
46408
+ return adoption;
46409
+ },
46410
+ isPreTurnRecord(turnKey2) {
46411
+ return turnKey2.startsWith(PRETURN_TURNKEY_PREFIX);
46412
+ },
46413
+ handleReaped(turnKey2) {
46414
+ const statusKey = bySyntheticKey.get(turnKey2);
46415
+ if (statusKey == null)
46416
+ return;
46417
+ const entry = byKey.get(statusKey);
46418
+ if (entry == null)
46419
+ return;
46420
+ entry.consumed = true;
46421
+ deps.stopTypingLoop(entry.chatId, entry.threadId);
46422
+ dropEntry(entry);
46423
+ },
46424
+ pendingCount() {
46425
+ let n = 0;
46426
+ for (const e of byKey.values())
46427
+ if (!e.consumed)
46428
+ n++;
46429
+ return n;
46430
+ },
46431
+ stopAll() {
46432
+ for (const e of [...byKey.values()])
46433
+ clearTimers(e);
46434
+ byKey.clear();
46435
+ bySyntheticKey.clear();
46436
+ }
46437
+ };
46438
+ }
46439
+
46440
+ // gateway/derive-turn-id.ts
46441
+ function deriveTurnId(chatId, threadId, messageId) {
46442
+ if (messageId == null || messageId === "" || String(messageId) === "0")
46443
+ return null;
46444
+ return `${chatKey(chatId, threadId ?? null)}#${messageId}`;
46445
+ }
46446
+
46196
46447
  // typing-emitter.ts
46197
46448
  var TYPING_REFRESH_MS = 4000;
46198
46449
  var TYPING_FLOOR_MS = 3500;
@@ -56388,7 +56639,9 @@ var SEND_GATE_DEFAULTS = {
56388
56639
  perChatBurst: 3,
56389
56640
  perGroupPerMin: 18,
56390
56641
  perGroupBurst: 2,
56391
- editFloorMs: 1500
56642
+ editFloorMs: 1500,
56643
+ perMessageEditWindowMs: 300000,
56644
+ perMessageEditMaxPerWindow: 150
56392
56645
  };
56393
56646
  function createSendGate(config) {
56394
56647
  const enabled2 = config.enabled;
@@ -56400,6 +56653,8 @@ function createSendGate(config) {
56400
56653
  const perGroupPerMin = config.perGroupPerMin ?? SEND_GATE_DEFAULTS.perGroupPerMin;
56401
56654
  const perGroupBurst = config.perGroupBurst ?? SEND_GATE_DEFAULTS.perGroupBurst;
56402
56655
  const editFloorMs = config.editFloorMs ?? SEND_GATE_DEFAULTS.editFloorMs;
56656
+ const perMessageEditWindowMs = Math.max(1, Math.floor(config.perMessageEditWindowMs ?? SEND_GATE_DEFAULTS.perMessageEditWindowMs));
56657
+ const perMessageEditMaxPerWindow = Math.max(0, Math.floor(config.perMessageEditMaxPerWindow ?? SEND_GATE_DEFAULTS.perMessageEditMaxPerWindow));
56403
56658
  const messageStateTtlMs = config.messageStateTtlMs ?? 60000;
56404
56659
  const maxMessageStates = config.maxMessageStates ?? 5000;
56405
56660
  const usefulTtlMs = config.usefulTtlMs ?? 120000;
@@ -56415,7 +56670,8 @@ function createSendGate(config) {
56415
56670
  dropped: 0,
56416
56671
  shed: 0,
56417
56672
  expired: 0,
56418
- failedFast: 0
56673
+ failedFast: 0,
56674
+ budgetDeferred: 0
56419
56675
  };
56420
56676
  const bootStart = clock.now();
56421
56677
  const globalRamp = config.bootRamp ? {
@@ -56454,7 +56710,8 @@ function createSendGate(config) {
56454
56710
  lastHash: undefined,
56455
56711
  pending: null,
56456
56712
  running: false,
56457
- suppressedUntilMs: 0
56713
+ suppressedUntilMs: 0,
56714
+ editWindowTs: []
56458
56715
  };
56459
56716
  perMessage.set(key, state);
56460
56717
  }
@@ -56641,7 +56898,20 @@ function createSendGate(config) {
56641
56898
  try {
56642
56899
  while (state.pending) {
56643
56900
  const now = clock.now();
56644
- const readyAt = Math.max(state.lastSentMs + editFloorMs, state.suppressedUntilMs);
56901
+ let readyAt = Math.max(state.lastSentMs + editFloorMs, state.suppressedUntilMs);
56902
+ if (perMessageEditMaxPerWindow > 0 && state.pending.priorityClass === "cosmetic") {
56903
+ const windowStart = now - perMessageEditWindowMs;
56904
+ while (state.editWindowTs.length > 0 && state.editWindowTs[0] <= windowStart) {
56905
+ state.editWindowTs.shift();
56906
+ }
56907
+ if (state.editWindowTs.length >= perMessageEditMaxPerWindow) {
56908
+ const budgetReadyAt = state.editWindowTs[0] + perMessageEditWindowMs;
56909
+ if (budgetReadyAt > readyAt) {
56910
+ readyAt = budgetReadyAt;
56911
+ counters.budgetDeferred++;
56912
+ }
56913
+ }
56914
+ }
56645
56915
  const waitMs = readyAt - now;
56646
56916
  if (waitMs > 0) {
56647
56917
  await clock.sleep(waitMs);
@@ -56667,6 +56937,12 @@ function createSendGate(config) {
56667
56937
  await admit(bucketsFor(opts));
56668
56938
  }
56669
56939
  state.lastSentMs = clock.now();
56940
+ if (perMessageEditMaxPerWindow > 0 && p.priorityClass === "cosmetic") {
56941
+ state.editWindowTs.push(state.lastSentMs);
56942
+ const overflow = state.editWindowTs.length - (perMessageEditMaxPerWindow + 1);
56943
+ if (overflow > 0)
56944
+ state.editWindowTs.splice(0, overflow);
56945
+ }
56670
56946
  try {
56671
56947
  const res = await p.fn();
56672
56948
  state.lastHash = p.hash;
@@ -56854,6 +57130,12 @@ function sendGateConfigFromEnv(env = process.env) {
56854
57130
  const editFloorMs = parseNonNegativeInt(env.SWITCHROOM_TG_SEND_GATE_EDIT_FLOOR_MS);
56855
57131
  if (editFloorMs !== undefined)
56856
57132
  out.editFloorMs = editFloorMs;
57133
+ const perMsgWindowMs = parsePositiveInt(env.SWITCHROOM_TG_SEND_GATE_PER_MSG_EDIT_WINDOW_MS);
57134
+ if (perMsgWindowMs !== undefined)
57135
+ out.perMessageEditWindowMs = perMsgWindowMs;
57136
+ const perMsgMax = parseNonNegativeInt(env.SWITCHROOM_TG_SEND_GATE_PER_MSG_EDIT_MAX);
57137
+ if (perMsgMax !== undefined)
57138
+ out.perMessageEditMaxPerWindow = perMsgMax;
56857
57139
  const conservativeGlobal = parseBoolFlag(env.SWITCHROOM_TG_SEND_GATE_CONSERVATIVE_GLOBAL);
56858
57140
  if (conservativeGlobal !== undefined)
56859
57141
  out.conservativeGlobalFloodScope = conservativeGlobal;
@@ -70901,6 +71183,15 @@ function parseModelCommand(text4) {
70901
71183
  function isModelCommandBusy(ctx) {
70902
71184
  return ctx.currentTurnActive || ctx.turnInFlight;
70903
71185
  }
71186
+ function resolveStaleAwareBusy(input) {
71187
+ const turnStale = input.currentTurnActive && input.turnAgeMs !== null && input.turnAgeMs > input.hardTtlMs;
71188
+ const approvalLive = input.oldestPendingApprovalAgeMs !== null && input.oldestPendingApprovalAgeMs <= input.hardTtlMs;
71189
+ return {
71190
+ currentTurnActive: input.currentTurnActive && !turnStale,
71191
+ turnInFlight: input.machineInTurn || approvalLive,
71192
+ clearStaleTurn: turnStale
71193
+ };
71194
+ }
70904
71195
  function planModelCommand(parsed, ctx) {
70905
71196
  if (parsed.kind === "show" && ctx.menuEnabled)
70906
71197
  return { kind: "menu" };
@@ -75523,10 +75814,34 @@ function validateMs365Preview(input) {
75523
75814
  out.sizeBytesBefore = o.sizeBytesBefore;
75524
75815
  if (typeof o.sizeBytesAfter === "number")
75525
75816
  out.sizeBytesAfter = o.sizeBytesAfter;
75817
+ if (typeof o.eventWhen === "string")
75818
+ out.eventWhen = o.eventWhen;
75819
+ const changes = sanitizeChanges(o.changes);
75820
+ if (changes)
75821
+ out.changes = changes;
75526
75822
  if (typeof o.agentRationale === "string")
75527
75823
  out.agentRationale = o.agentRationale;
75528
75824
  return out;
75529
75825
  }
75826
+ function sanitizeChanges(input) {
75827
+ if (!Array.isArray(input))
75828
+ return;
75829
+ const out = [];
75830
+ for (const raw of input) {
75831
+ if (!raw || typeof raw !== "object")
75832
+ continue;
75833
+ const c = raw;
75834
+ if (typeof c.field !== "string" || c.field.length === 0)
75835
+ continue;
75836
+ const entry = { field: c.field };
75837
+ if (typeof c.before === "string")
75838
+ entry.before = c.before;
75839
+ if (typeof c.after === "string")
75840
+ entry.after = c.after;
75841
+ out.push(entry);
75842
+ }
75843
+ return out.length > 0 ? out : undefined;
75844
+ }
75530
75845
  var DEFAULT_TTL_MS2 = 5 * 60 * 1000;
75531
75846
  var MAX_TTL_MS2 = 30 * 60 * 1000;
75532
75847
  var MIN_TTL_MS2 = 30 * 1000;
@@ -75551,6 +75866,9 @@ function buildMs365CardText(p) {
75551
75866
  lines.push(`ID: ${truncate3(p.itemId, 96)}`);
75552
75867
  }
75553
75868
  lines.push(`Account: ${truncate3(p.accountEmail, 96)}`);
75869
+ if (p.eventWhen) {
75870
+ lines.push(`When: ${truncate3(p.eventWhen, 96)}`);
75871
+ }
75554
75872
  if (typeof p.sizeBytesBefore === "number" || typeof p.sizeBytesAfter === "number") {
75555
75873
  const before = p.sizeBytesBefore ?? 0;
75556
75874
  const after = p.sizeBytesAfter ?? 0;
@@ -75561,19 +75879,29 @@ function buildMs365CardText(p) {
75561
75879
  if (p.deepLink) {
75562
75880
  lines.push(`Link: ${truncate3(p.deepLink, 256)}`);
75563
75881
  }
75882
+ if (p.changes && p.changes.length > 0) {
75883
+ lines.push("");
75884
+ lines.push("Changes:");
75885
+ for (const c of p.changes.slice(0, 8)) {
75886
+ const before = c.before !== undefined ? truncate3(c.before, 96) : "(none)";
75887
+ const after = c.after !== undefined ? truncate3(c.after, 96) : "(cleared)";
75888
+ lines.push(`\u2022 ${c.field}: ${before} \u2192 ${after}`);
75889
+ }
75890
+ }
75564
75891
  if (p.agentRationale) {
75565
75892
  lines.push("");
75566
75893
  lines.push(`\uD83D\uDCAC ${truncate3(p.agentRationale, 512)}`);
75567
75894
  }
75568
75895
  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.");
75896
+ 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
75897
  return hardenCardBreaks(lines.join(`
75571
75898
  `));
75572
75899
  }
75573
75900
  function truncate3(s, n) {
75574
- if (s.length <= n)
75575
- return s;
75576
- return s.slice(0, n - 1) + "\u2026";
75901
+ const oneLine = s.replace(/[\r\n\t]+/g, " ");
75902
+ if (oneLine.length <= n)
75903
+ return oneLine;
75904
+ return oneLine.slice(0, n - 1) + "\u2026";
75577
75905
  }
75578
75906
  function humanBytes(bytes) {
75579
75907
  const abs = Math.abs(bytes);
@@ -78164,6 +78492,7 @@ ${result}
78164
78492
  meta: {
78165
78493
  source: "subagent_handback",
78166
78494
  outcome: opts.ctx.outcome,
78495
+ message_id: String(ts),
78167
78496
  ...opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {},
78168
78497
  ...opts.ctx.jsonlAgentId ? { subagent_jsonl_id: opts.ctx.jsonlAgentId } : {}
78169
78498
  }
@@ -80002,6 +80331,7 @@ import {
80002
80331
  } from "node:fs";
80003
80332
  import { join as join39 } from "node:path";
80004
80333
  var TURN_ACTIVE_MARKER_FILE = "turn-active.json";
80334
+ var TURN_ACTIVE_HARD_TTL_MS = 10 * 60000;
80005
80335
  function touchTurnActiveMarker(stateDir) {
80006
80336
  const path2 = join39(stateDir, TURN_ACTIVE_MARKER_FILE);
80007
80337
  if (!existsSync34(path2))
@@ -83728,6 +84058,8 @@ import {
83728
84058
  } from "node:fs";
83729
84059
  import { join as join52 } from "node:path";
83730
84060
  var TURN_ACTIVE_MARKER_FILE2 = "turn-active.json";
84061
+ var TURN_ACTIVE_HARD_TTL_MS2 = 10 * 60000;
84062
+ var TURN_ACTIVE_IDLE_SWEEP_MS = 60000;
83731
84063
  function writeTurnActiveMarker(stateDir, marker) {
83732
84064
  try {
83733
84065
  mkdirSync37(stateDir, { recursive: true });
@@ -83794,12 +84126,15 @@ function readTurnActiveMarkerAgeMs(stateDir, now) {
83794
84126
  return null;
83795
84127
  }
83796
84128
  }
84129
+ function effectiveTurnAgeMs(markerAgeMs, turnStartedAt, now) {
84130
+ return markerAgeMs ?? now - turnStartedAt;
84131
+ }
83797
84132
 
83798
84133
  // ../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;
84134
+ var VERSION = "0.18.27";
84135
+ var COMMIT_SHA = "93871829";
84136
+ var COMMIT_DATE = "2026-07-16T01:31:20Z";
84137
+ var LATEST_PR = 3273;
83803
84138
  var COMMITS_AHEAD_OF_TAG = 0;
83804
84139
 
83805
84140
  // gateway/boot-version.ts
@@ -86479,9 +86814,43 @@ var FEED_LIVENESS_OPEN_MS = (() => {
86479
86814
  var POST_ANSWER_LIVENESS_STALE_MS = parsePostAnswerLivenessMs(process.env.SWITCHROOM_POST_ANSWER_LIVENESS_STALE_MS) || 30000;
86480
86815
  function turnInFlightForGate() {
86481
86816
  const hasPendingApproval = pendingPermissions.size > 0;
86817
+ return turnInFlightMachineOnly() || hasPendingApproval;
86818
+ }
86819
+ function turnInFlightMachineOnly() {
86482
86820
  if (!isDeliveryCutoverEnabled())
86483
- return claudeBusyKeys.size > 0 || hasPendingApproval;
86484
- return probeGateParity(isMachineInTurn(), claudeBusyKeys.size) || hasPendingApproval;
86821
+ return claudeBusyKeys.size > 0;
86822
+ return probeGateParity(isMachineInTurn(), claudeBusyKeys.size);
86823
+ }
86824
+ function liveTurnAgeMs(now) {
86825
+ if (currentTurn === null)
86826
+ return null;
86827
+ return effectiveTurnAgeMs(readTurnActiveMarkerAgeMs(STATE_DIR, now), currentTurn.startedAt, now);
86828
+ }
86829
+ function oldestPendingApprovalAgeMs(now) {
86830
+ let oldest = null;
86831
+ for (const p of pendingPermissions.values()) {
86832
+ const age = now - p.startedAt;
86833
+ if (oldest === null || age > oldest)
86834
+ oldest = age;
86835
+ }
86836
+ return oldest;
86837
+ }
86838
+ function resolveModelEffortBusy(now = Date.now()) {
86839
+ const turnAgeMs = liveTurnAgeMs(now);
86840
+ const resolved = resolveStaleAwareBusy({
86841
+ currentTurnActive: currentTurn !== null,
86842
+ turnAgeMs,
86843
+ machineInTurn: turnInFlightMachineOnly(),
86844
+ oldestPendingApprovalAgeMs: oldestPendingApprovalAgeMs(now),
86845
+ hardTtlMs: TURN_ACTIVE_HARD_TTL_MS2
86846
+ });
86847
+ if (resolved.clearStaleTurn && currentTurn !== null) {
86848
+ const ageSec = Math.round((turnAgeMs ?? 0) / 1000);
86849
+ 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()}
86850
+ `);
86851
+ clearAllCurrentTurns();
86852
+ }
86853
+ return { currentTurnActive: resolved.currentTurnActive, turnInFlight: resolved.turnInFlight };
86485
86854
  }
86486
86855
  function deliverResumeSyntheticOrBuffer(agent, inbound) {
86487
86856
  const decision = decideInboundDelivery({
@@ -86649,11 +87018,6 @@ function findTurnByQuotedMessageId(chatId, replyTo) {
86649
87018
  return null;
86650
87019
  return turn;
86651
87020
  }
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
87021
  function findTurnByOriginId(originTurnId) {
86658
87022
  if (originTurnId == null || originTurnId === "")
86659
87023
  return null;
@@ -88479,8 +88843,8 @@ var pendingStateReaper = setInterval(() => {
88479
88843
  try {
88480
88844
  sweepStaleTurnActiveMarker(STATE_DIR, {
88481
88845
  turnInFlight: currentTurn?.registryKey != null,
88482
- idleSweepMs: 60000,
88483
- hardTtlMs: 600000,
88846
+ idleSweepMs: TURN_ACTIVE_IDLE_SWEEP_MS,
88847
+ hardTtlMs: TURN_ACTIVE_HARD_TTL_MS2,
88484
88848
  now,
88485
88849
  onRemove: ({ ageMs, reason, payload }) => {
88486
88850
  const agent = getMyAgentName();
@@ -88927,11 +89291,16 @@ async function runMidSessionCardReaper() {
88927
89291
  isLive: (record2) => topicKeys.has(record2.turnKey),
88928
89292
  ttlMs: MID_SESSION_CARD_REAPER_TTL_MS,
88929
89293
  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
- }),
89294
+ finalizeCard: (record2) => {
89295
+ if (handbackPreturnSignal.isPreTurnRecord(record2.turnKey)) {
89296
+ handbackPreturnSignal.handleReaped(record2.turnKey);
89297
+ }
89298
+ return robustApiCall(() => lockedBot.api.editMessageText(record2.chatId, record2.activityMessageId, richMessage2(restartOrphanCardFinalizeText(record2.startedAt)), {}), {
89299
+ chat_id: record2.chatId,
89300
+ ...record2.threadId != null ? { threadId: record2.threadId } : {},
89301
+ verb: "activity-card.mid-session-reap-finalize"
89302
+ });
89303
+ },
88935
89304
  unpinCard: async (record2) => {
88936
89305
  const pinKey = `fg:${record2.turnKey}`;
88937
89306
  if (statusPinState.has(pinKey)) {
@@ -89411,6 +89780,8 @@ var _deliveryMachineTick = setInterval(() => {
89411
89780
  }, DELIVERY_MACHINE_TICK_MS);
89412
89781
  _deliveryMachineTick.unref?.();
89413
89782
  function trackRedeliveredInbound(merged) {
89783
+ if (HANDBACK_PRETURN_ENABLED)
89784
+ handbackPreturnSignal.noteHandbackRelease(merged);
89414
89785
  if (!DELIVERY_CONFIRM_ENABLED)
89415
89786
  return;
89416
89787
  const isTrackableResume = isTrackableResumeSynthetic(merged.meta);
@@ -91090,6 +91461,7 @@ async function executeReply(args) {
91090
91461
  return { content: [{ type: "text", text: "sent (deduped \u2014 same content sent via earlier path)" }] };
91091
91462
  }
91092
91463
  }
91464
+ let supersedeFlushIds = [];
91093
91465
  {
91094
91466
  const replyThreadId = args.message_thread_id != null ? Number(args.message_thread_id) : undefined;
91095
91467
  const ownerTurn = resolveReplyOwnerTurn(turn, chat_id, args);
@@ -91098,9 +91470,9 @@ async function executeReply(args) {
91098
91470
  if (decision.supersede) {
91099
91471
  process.stderr.write(`telegram gateway: reply: superseding flushed turn message(s) chatId=${chat_id} ids=${JSON.stringify(decision.deleteMessageIds)}
91100
91472
  `);
91101
- for (const id of decision.deleteMessageIds) {
91102
- await swallowingApiCall(() => lockedBot.api.deleteMessage(chat_id, id), { chat_id, verb: "reply.supersedeFlushed" });
91103
- }
91473
+ supersedeFlushIds = decision.deleteMessageIds;
91474
+ if (ownerTurn != null)
91475
+ ownerTurn.answerDelivered = true;
91104
91476
  } else {
91105
91477
  const replySubstantive = isSubstantiveFinalReply({
91106
91478
  text: rawText,
@@ -91328,6 +91700,25 @@ ${url}`;
91328
91700
  });
91329
91701
  }
91330
91702
  }
91703
+ if (supersedeFlushIds.length > 0) {
91704
+ const correction = decideSupersedeCorrection({
91705
+ flushMessageIds: supersedeFlushIds,
91706
+ chunkCount: chunks.length,
91707
+ hasFiles: files.length > 0,
91708
+ suppressText,
91709
+ hasOpenPreview: previewMessageId != null
91710
+ });
91711
+ if (correction.mode === "edit-in-place") {
91712
+ previewMessageId = correction.editMessageId;
91713
+ reply_to = undefined;
91714
+ process.stderr.write(`telegram gateway: reply: superseding flushed message via edit-in-place chatId=${chat_id} id=${correction.editMessageId}
91715
+ `);
91716
+ } else {
91717
+ for (const id of correction.deleteMessageIds) {
91718
+ await swallowingApiCall(() => lockedBot.api.deleteMessage(chat_id, id), { chat_id, verb: "reply.supersedeFlushed" });
91719
+ }
91720
+ }
91721
+ }
91331
91722
  if (previewMessageId != null && reply_to != null && replyMode !== "off") {
91332
91723
  await deleteStalePreview(previewMessageId);
91333
91724
  previewMessageId = null;
@@ -91336,7 +91727,7 @@ ${url}`;
91336
91727
  let silentAnchorEditDone = false;
91337
91728
  {
91338
91729
  const turn2 = currentTurn;
91339
- if (turn2 != null && chunks.length === 1) {
91730
+ if (turn2 != null && chunks.length === 1 && supersedeFlushIds.length === 0) {
91340
91731
  const decision = decideSilentReplyAnchor({
91341
91732
  effectivelySilent: disableNotification,
91342
91733
  anchorMessageId: turn2.silentAnchorMessageId,
@@ -93151,6 +93542,61 @@ function clearActivitySummary(turn, finalHtmlOverride) {
93151
93542
  }
93152
93543
  });
93153
93544
  }
93545
+ var HANDBACK_PRETURN_ENABLED = !STATIC && process.env.SWITCHROOM_HANDBACK_PRETURN !== "0";
93546
+ var HANDBACK_PRETURN_HTML = "\uD83E\uDD1D Reading the worker\u2019s results\u2026";
93547
+ var HANDBACK_PRETURN_ORPHAN_HTML = "\uD83E\uDD1D A background worker finished, but the handback never started \u2014 it may need a nudge.";
93548
+ async function openHandbackPreTurnCard(chatId, threadId) {
93549
+ if (STATIC)
93550
+ return null;
93551
+ try {
93552
+ const sent = await robustApiCall(() => bot.api.sendRichMessage(chatId, richMessage2(HANDBACK_PRETURN_HTML), {
93553
+ ...threadId != null ? { message_thread_id: threadId } : {},
93554
+ disable_notification: true
93555
+ }), {
93556
+ chat_id: chatId,
93557
+ ...threadId != null ? { threadId } : {},
93558
+ verb: "handback-preturn.send"
93559
+ });
93560
+ return sent?.message_id ?? null;
93561
+ } catch (err) {
93562
+ process.stderr.write(`telegram gateway: handback pre-turn card send failed: ${err.message}
93563
+ `);
93564
+ return null;
93565
+ }
93566
+ }
93567
+ function finalizeHandbackPreTurnCard(record2) {
93568
+ return robustApiCall(() => bot.api.editMessageText(record2.chatId, record2.activityMessageId, richMessage2(HANDBACK_PRETURN_ORPHAN_HTML), {}), {
93569
+ chat_id: record2.chatId,
93570
+ ...record2.threadId != null ? { threadId: record2.threadId } : {},
93571
+ verb: "handback-preturn.orphan-finalize"
93572
+ }).then(() => {
93573
+ return;
93574
+ }).catch(() => {
93575
+ return;
93576
+ });
93577
+ }
93578
+ var handbackPreturnSignal = createHandbackPreturnSignal({
93579
+ chatKey: (chatId, threadId) => chatKey2(chatId, threadId),
93580
+ deriveTurnId: (chatId, threadId, messageId) => deriveTurnId(chatId, threadId, messageId),
93581
+ startTypingLoop: (chatId, threadId) => startTurnTypingLoop(chatId, threadId),
93582
+ stopTypingLoop: (chatId, threadId) => stopTurnTypingLoop(chatId, threadId),
93583
+ openCard: openHandbackPreTurnCard,
93584
+ finalizeCard: finalizeHandbackPreTurnCard,
93585
+ writeCardRecord: (record2) => {
93586
+ if (!activityCardPersistEnabled)
93587
+ return;
93588
+ writeActivityCardRecord(ACTIVITY_CARD_STORE_PATH, activityCardStoreFs, record2);
93589
+ },
93590
+ clearCardRecord: (turnKey2, activityMessageId) => {
93591
+ if (!activityCardPersistEnabled)
93592
+ return;
93593
+ clearActivityCardRecord(ACTIVITY_CARD_STORE_PATH, activityCardStoreFs, turnKey2, activityMessageId);
93594
+ },
93595
+ isTurnSettled: (key) => {
93596
+ const live = currentTurnMap.get(key);
93597
+ return live != null && (live.finalAnswerDelivered || live.endedAt != null);
93598
+ }
93599
+ });
93154
93600
  var memoryLegibilityStager = new MemoryLegibilityStager;
93155
93601
  function sendMemoryLegibilityLine(event, chatId, threadId) {
93156
93602
  const line = renderMemoryLegibilityLine(event);
@@ -93304,6 +93750,16 @@ function handleSessionEvent(ev) {
93304
93750
  emissionAuthority: new EmissionAuthority(statusKey(ev.chatId, enqThreadIdNum))
93305
93751
  };
93306
93752
  next.narrativeGate = makeNarrativeGate(next);
93753
+ if (HANDBACK_PRETURN_ENABLED) {
93754
+ const handbackAdoption = handbackPreturnSignal.tryAdopt(turnId);
93755
+ if (handbackAdoption != null) {
93756
+ if (handbackAdoption.activityMessageId != null) {
93757
+ next.activityMessageId = handbackAdoption.activityMessageId;
93758
+ next.activityEverOpened = true;
93759
+ }
93760
+ startTurnTypingLoop(ev.chatId, enqThreadIdNum ?? null);
93761
+ }
93762
+ }
93307
93763
  setCurrentTurn(next, statusKey(ev.chatId, enqThreadIdNum));
93308
93764
  scheduleEarlyLivenessOpen(next);
93309
93765
  process.stderr.write(`telegram gateway: ${formatTurnLifecycle("set", "enqueue", next, startedAt)}
@@ -96350,7 +96806,8 @@ bot.command("model", async (ctx) => {
96350
96806
  const parsed = parseModelCommand(text5) ?? { kind: "show" };
96351
96807
  const chatId = String(ctx.chat.id);
96352
96808
  const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id);
96353
- const busyNow = currentTurn !== null || turnInFlightForGate();
96809
+ const modelBusy = resolveModelEffortBusy();
96810
+ const busyNow = modelBusy.currentTurnActive || modelBusy.turnInFlight;
96354
96811
  process.stderr.write(modelCommandReceiptLine(getMyAgentName(), parsed, busyNow) + `
96355
96812
  `);
96356
96813
  if (HISTORY_ENABLED && ctx.message?.message_id != null) {
@@ -96371,8 +96828,8 @@ bot.command("model", async (ctx) => {
96371
96828
  }
96372
96829
  const deps = buildModelDeps({ chatId, threadId });
96373
96830
  const disposition = planModelCommand(parsed, {
96374
- currentTurnActive: currentTurn !== null,
96375
- turnInFlight: turnInFlightForGate(),
96831
+ currentTurnActive: modelBusy.currentTurnActive,
96832
+ turnInFlight: modelBusy.turnInFlight,
96376
96833
  menuEnabled: process.env.SWITCHROOM_MODEL_MENU !== "0"
96377
96834
  });
96378
96835
  if (disposition.kind === "menu") {
@@ -96443,7 +96900,8 @@ bot.command("effort", async (ctx) => {
96443
96900
  await switchroomReply(ctx, menu.text, { html: true, reply_markup: effortMenuReplyMarkup(menu) });
96444
96901
  return;
96445
96902
  }
96446
- if ((parsed.kind === "set" || parsed.kind === "default") && currentTurn !== null) {
96903
+ const effortBusy = resolveModelEffortBusy();
96904
+ if ((parsed.kind === "set" || parsed.kind === "default") && effortBusy.currentTurnActive) {
96447
96905
  const requestedLevel = parsed.kind === "set" ? parsed.level : "default";
96448
96906
  const chatId = String(ctx.chat.id);
96449
96907
  const threadId = resolveThreadId(chatId, ctx.message?.message_thread_id);