switchroom 0.20.2 → 0.20.4

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 (27) hide show
  1. package/bin/handoff-briefing.sh +41 -1
  2. package/dist/cli/switchroom.js +10259 -1232
  3. package/dist/host-control/main.js +1 -1
  4. package/package.json +3 -3
  5. package/profiles/default/CLAUDE.md.hbs +13 -11
  6. package/telegram-plugin/dist/gateway/gateway.js +429 -98
  7. package/telegram-plugin/edit-flood-fuse.ts +332 -14
  8. package/telegram-plugin/gateway/feed-open-gate.ts +28 -8
  9. package/telegram-plugin/gateway/feed-reopen-gate.ts +88 -6
  10. package/telegram-plugin/gateway/gateway.ts +128 -104
  11. package/telegram-plugin/gateway/narrative-lane.ts +4 -0
  12. package/telegram-plugin/gateway/progress-fallback-cap.ts +195 -0
  13. package/telegram-plugin/gateway/stream-render.ts +67 -12
  14. package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +13 -0
  15. package/telegram-plugin/gateway/subagent-origin-surface.ts +181 -0
  16. package/telegram-plugin/gateway/subagent-progress-inbound-builder.ts +7 -0
  17. package/telegram-plugin/registry/subagents-schema.ts +61 -0
  18. package/telegram-plugin/registry/turns-schema.ts +26 -0
  19. package/telegram-plugin/tests/edit-flood-fuse-cosmetic-fairness.test.ts +229 -0
  20. package/telegram-plugin/tests/feed-open-gate.test.ts +42 -0
  21. package/telegram-plugin/tests/feed-reopen-gate.test.ts +114 -0
  22. package/telegram-plugin/tests/progress-cap.test.ts +182 -0
  23. package/telegram-plugin/tests/progress-fallback-cap.test.ts +91 -0
  24. package/telegram-plugin/tests/progress-update.test.ts +108 -12
  25. package/telegram-plugin/tests/subagent-handback-inbound-builder.test.ts +46 -0
  26. package/telegram-plugin/tests/subagent-progress-inbound-builder.test.ts +26 -0
  27. package/telegram-plugin/tests/worker-origin-gap-dispatch.test.ts +304 -0
@@ -65060,6 +65060,9 @@ var EDIT_FLOOD_FUSE_DEFAULTS = {
65060
65060
  perChatSendMaxPerWindow: 25,
65061
65061
  cosmeticPerMessageMaxPerWindow: 4,
65062
65062
  cosmeticPerChatMaxPerWindow: 6,
65063
+ cosmeticFloorPerWindow: 2,
65064
+ cosmeticFloorSlots: 2,
65065
+ throttleNoticeMs: 45000,
65063
65066
  perChatTotalMaxPerWindow: 20,
65064
65067
  perChatReplyReserve: 8,
65065
65068
  perChatCriticalMinPerWindow: 3,
@@ -65094,6 +65097,11 @@ function editFloodFuseConfigFromEnv(env) {
65094
65097
  };
65095
65098
  assign("cosmeticPerMessageMaxPerWindow", envInt(env.SWITCHROOM_FEED_EDIT_MAX_PER_MSG_PER_MIN));
65096
65099
  assign("cosmeticPerChatMaxPerWindow", envInt(env.SWITCHROOM_FEED_EDIT_MAX_PER_CHAT_PER_MIN));
65100
+ if (env.SWITCHROOM_FEED_FAIR_SHARE === "0")
65101
+ cfg.cosmeticFairShareEnabled = false;
65102
+ assign("cosmeticFloorPerWindow", envInt(env.SWITCHROOM_FEED_EDIT_FLOOR_PER_MSG_PER_MIN));
65103
+ assign("cosmeticFloorSlots", envInt(env.SWITCHROOM_FEED_EDIT_FLOOR_SLOTS));
65104
+ assign("throttleNoticeMs", envInt(env.SWITCHROOM_FEED_THROTTLE_NOTICE_MS));
65097
65105
  assign("perChatTotalMaxPerWindow", envInt(env.SWITCHROOM_CHAT_TOTAL_MAX_PER_MIN));
65098
65106
  assign("perChatReplyReserve", envInt(env.SWITCHROOM_CHAT_REPLY_RESERVE));
65099
65107
  assign("perChatCriticalMinPerWindow", envInt(env.SWITCHROOM_CHAT_CRITICAL_MIN_PER_MIN));
@@ -65118,6 +65126,11 @@ function createEditFloodFuse(config = {}) {
65118
65126
  const perChatSendMax = config.perChatSendMaxPerWindow ?? D.perChatSendMaxPerWindow;
65119
65127
  const cosmeticPerMessageMax = Math.min(perMessageMax, config.cosmeticPerMessageMaxPerWindow ?? D.cosmeticPerMessageMaxPerWindow);
65120
65128
  const cosmeticPerChatMax = Math.min(perChatEditMax, config.cosmeticPerChatMaxPerWindow ?? D.cosmeticPerChatMaxPerWindow);
65129
+ const cosmeticFairShareEnabled = config.cosmeticFairShareEnabled ?? true;
65130
+ const cosmeticFloorPerWindow = Math.min(cosmeticPerMessageMax, Math.max(0, config.cosmeticFloorPerWindow ?? D.cosmeticFloorPerWindow));
65131
+ const cosmeticFloorAggMax = Math.min(cosmeticPerChatMax, cosmeticFloorPerWindow * Math.max(0, config.cosmeticFloorSlots ?? D.cosmeticFloorSlots));
65132
+ const cosmeticRemainderMax = Math.max(0, cosmeticPerChatMax - cosmeticFloorAggMax);
65133
+ const throttleNoticeMs = Math.max(0, config.throttleNoticeMs ?? D.throttleNoticeMs);
65121
65134
  const perChatTotalMax = config.perChatTotalMaxPerWindow ?? D.perChatTotalMaxPerWindow;
65122
65135
  const perChatWindowMs = config.perChatWindowMs ?? D.perChatWindowMs;
65123
65136
  const perChatReplyReserve = Math.min(Math.max(0, config.perChatReplyReserve ?? D.perChatReplyReserve), Math.max(0, perChatTotalMax - 1));
@@ -65142,7 +65155,8 @@ function createEditFloodFuse(config = {}) {
65142
65155
  superseded: 0,
65143
65156
  floodObserved: 0,
65144
65157
  meteredByDefault: 0,
65145
- chatless: 0
65158
+ chatless: 0,
65159
+ throttled: 0
65146
65160
  };
65147
65161
  let tightenLevel = 0;
65148
65162
  let tightenedUntil = 0;
@@ -65189,6 +65203,12 @@ function createEditFloodFuse(config = {}) {
65189
65203
  return eff;
65190
65204
  return Math.max(eff, Math.min(base, perChatCriticalMin));
65191
65205
  }
65206
+ function cosmeticMessageCeiling(now) {
65207
+ const eff = ceiling(cosmeticPerMessageMax, now);
65208
+ if (!cosmeticFairShareEnabled)
65209
+ return eff;
65210
+ return Math.max(eff, Math.min(cosmeticPerMessageMax, cosmeticFloorPerWindow));
65211
+ }
65192
65212
  function cosmeticTotalMax(now) {
65193
65213
  const eff = ceiling(perChatTotalMax, now);
65194
65214
  if (replyReserveFraction <= 0)
@@ -65281,9 +65301,87 @@ function createEditFloodFuse(config = {}) {
65281
65301
  if (i >= 0)
65282
65302
  w.ts.splice(i, 1);
65283
65303
  }
65304
+ function makeThrottleNotice(method, key, cls) {
65305
+ if (!cosmeticFairShareEnabled || cls !== "cosmetic" || throttleNoticeMs <= 0) {
65306
+ return { tick: () => {}, sleepCap: () => Number.POSITIVE_INFINITY };
65307
+ }
65308
+ let firstAt = Number.NaN;
65309
+ let fired = false;
65310
+ return {
65311
+ tick: (now) => {
65312
+ if (Number.isNaN(firstAt))
65313
+ firstAt = now;
65314
+ if (fired || now - firstAt < throttleNoticeMs)
65315
+ return;
65316
+ fired = true;
65317
+ counters.throttled++;
65318
+ onTrip?.({ method, key, action: "throttled", cls });
65319
+ },
65320
+ sleepCap: (now) => {
65321
+ if (fired || Number.isNaN(firstAt))
65322
+ return Number.POSITIVE_INFINITY;
65323
+ return Math.max(1, firstAt + throttleNoticeMs - now);
65324
+ }
65325
+ };
65326
+ }
65327
+ async function awaitCosmeticChatFair(chat, msg, method, cls, dropGuard, deadline, lateReleaseKey) {
65328
+ const fMsgKey = `cmf:${chat}:${msg}`;
65329
+ const fAggKey = `cfa:${chat}`;
65330
+ const remKey = `cer:${chat}`;
65331
+ const fw = win(fMsgKey);
65332
+ const aw = win(fAggKey);
65333
+ const rw = win(remKey);
65334
+ let counted = false;
65335
+ const throttle = makeThrottleNotice(method, remKey, cls);
65336
+ for (;; ) {
65337
+ const now = clock.now();
65338
+ const wFloor = Math.max(waitFor(fw, now, perChatWindowMs, cosmeticFloorPerWindow), waitFor(aw, now, perChatWindowMs, cosmeticFloorAggMax));
65339
+ const remCap = cosmeticRemainderMax <= 0 ? 0 : ceiling(cosmeticRemainderMax, now);
65340
+ const wRem = waitFor(rw, now, perChatWindowMs, remCap);
65341
+ if (wFloor === 0) {
65342
+ fw.ts.push(now);
65343
+ aw.ts.push(now);
65344
+ return [[fMsgKey, now], [fAggKey, now]];
65345
+ }
65346
+ if (remCap > 0 && wRem === 0) {
65347
+ rw.ts.push(now);
65348
+ return [[remKey, now]];
65349
+ }
65350
+ if (now >= deadline) {
65351
+ if (dropGuard()) {
65352
+ counters.dropped++;
65353
+ onTrip?.({ method, key: remKey, action: "dropped", cls });
65354
+ return null;
65355
+ }
65356
+ const lw = win(lateReleaseKey);
65357
+ prune(lw, now, perChatWindowMs);
65358
+ if (lw.ts.length >= ceiling(lateReleaseMax, now)) {
65359
+ counters.dropped++;
65360
+ onTrip?.({ method, key: remKey, action: "dropped", cls });
65361
+ return null;
65362
+ }
65363
+ lw.ts.push(now);
65364
+ if (cosmeticRemainderMax > 0) {
65365
+ rw.ts.push(now);
65366
+ return [[remKey, now]];
65367
+ }
65368
+ fw.ts.push(now);
65369
+ aw.ts.push(now);
65370
+ return [[fMsgKey, now], [fAggKey, now]];
65371
+ }
65372
+ if (!counted) {
65373
+ counters.deferred++;
65374
+ counted = true;
65375
+ onTrip?.({ method, key: remKey, action: "deferred", cls });
65376
+ }
65377
+ throttle.tick(now);
65378
+ await clock.sleep(Math.min(Math.min(wFloor, wRem), deadline - now, throttle.sleepCap(now)));
65379
+ }
65380
+ }
65284
65381
  async function awaitRoom(key, windowMs, maxFor, method, mode, cls, dropGuard, deadline, lateReleaseKey) {
65285
65382
  const w = win(key);
65286
65383
  let counted = false;
65384
+ const throttle = makeThrottleNotice(method, key, cls);
65287
65385
  for (;; ) {
65288
65386
  const now = clock.now();
65289
65387
  const wait = waitFor(w, now, windowMs, maxFor(now));
@@ -65315,6 +65413,8 @@ function createEditFloodFuse(config = {}) {
65315
65413
  counted = true;
65316
65414
  onTrip?.({ method, key, action: "deferred", cls });
65317
65415
  }
65416
+ throttle.tick(now);
65417
+ const nap = Math.min(wait, deadline - now, throttle.sleepCap(now));
65318
65418
  let killed = false;
65319
65419
  if (mode === "supersede") {
65320
65420
  w.waiter?.kill();
@@ -65324,7 +65424,7 @@ function createEditFloodFuse(config = {}) {
65324
65424
  resolve6();
65325
65425
  } };
65326
65426
  });
65327
- await Promise.race([clock.sleep(Math.min(wait, deadline - now)), superseded]);
65427
+ await Promise.race([clock.sleep(nap), superseded]);
65328
65428
  if (killed) {
65329
65429
  counters.superseded++;
65330
65430
  onTrip?.({ method, key, action: "superseded", cls });
@@ -65332,7 +65432,7 @@ function createEditFloodFuse(config = {}) {
65332
65432
  }
65333
65433
  w.waiter = null;
65334
65434
  } else {
65335
- await clock.sleep(Math.min(wait, deadline - now));
65435
+ await clock.sleep(nap);
65336
65436
  }
65337
65437
  }
65338
65438
  }
@@ -65368,7 +65468,7 @@ function createEditFloodFuse(config = {}) {
65368
65468
  const dropGuard = () => mw.inflight > 1;
65369
65469
  const lateKey = cls === "cosmetic" ? `lr:${chat}` : undefined;
65370
65470
  try {
65371
- const msgSlot = await awaitRoom(msgKey, perMessageWindowMs, cls === "cosmetic" ? (t) => ceiling(cosmeticPerMessageMax, t) : (t) => classCeiling(perMessageMax, cls, t), method, "supersede", cls, dropGuard, deadline, lateKey);
65471
+ const msgSlot = await awaitRoom(msgKey, perMessageWindowMs, cls === "cosmetic" ? (t) => cosmeticMessageCeiling(t) : (t) => classCeiling(perMessageMax, cls, t), method, "supersede", cls, dropGuard, deadline, lateKey);
65372
65472
  if (msgSlot === null)
65373
65473
  return DROPPED_RESULT;
65374
65474
  const reserved = [[msgKey, msgSlot]];
@@ -65376,13 +65476,22 @@ function createEditFloodFuse(config = {}) {
65376
65476
  for (const [k, at] of reserved)
65377
65477
  unreserve(k, at);
65378
65478
  };
65379
- const chatKey2 = `ce:${chat}`;
65380
- const chatSlot = await awaitRoom(chatKey2, perChatWindowMs, cls === "cosmetic" ? (t) => ceiling(cosmeticPerChatMax, t) : (t) => classCeiling(perChatEditMax, cls, t), method, "drop", cls, dropGuard, deadline, lateKey);
65381
- if (chatSlot === null) {
65382
- giveBack();
65383
- return DROPPED_RESULT;
65479
+ if (cls === "cosmetic" && cosmeticFairShareEnabled) {
65480
+ const fairSlots = await awaitCosmeticChatFair(chat, msg, method, cls, dropGuard, deadline, lateKey);
65481
+ if (fairSlots === null) {
65482
+ giveBack();
65483
+ return DROPPED_RESULT;
65484
+ }
65485
+ reserved.push(...fairSlots);
65486
+ } else {
65487
+ const chatKey2 = `ce:${chat}`;
65488
+ const chatSlot = await awaitRoom(chatKey2, perChatWindowMs, cls === "cosmetic" ? (t) => ceiling(cosmeticPerChatMax, t) : (t) => classCeiling(perChatEditMax, cls, t), method, "drop", cls, dropGuard, deadline, lateKey);
65489
+ if (chatSlot === null) {
65490
+ giveBack();
65491
+ return DROPPED_RESULT;
65492
+ }
65493
+ reserved.push([chatKey2, chatSlot]);
65384
65494
  }
65385
- reserved.push([chatKey2, chatSlot]);
65386
65495
  const totalSlot = await awaitRoom(totalKey, perChatWindowMs, chatTotalMax, method, cls === "cosmetic" ? "drop" : "release", cls, dropGuard, deadline, lateKey);
65387
65496
  if (totalSlot === null) {
65388
65497
  giveBack();
@@ -65431,6 +65540,8 @@ function createEditFloodFuse(config = {}) {
65431
65540
  deferred: counters.deferred,
65432
65541
  dropped: counters.dropped,
65433
65542
  superseded: counters.superseded,
65543
+ throttled: counters.throttled,
65544
+ cosmeticFairShareEnabled,
65434
65545
  floodObserved: counters.floodObserved,
65435
65546
  tightened: isTightened(now),
65436
65547
  tightenLevel: levelAt(now),
@@ -78753,6 +78864,28 @@ init_rich_send();
78753
78864
  init_format();
78754
78865
 
78755
78866
  // registry/subagents-schema.ts
78867
+ function mapSubagentRow(row) {
78868
+ return {
78869
+ id: row.id,
78870
+ parent_session_id: row.parent_session_id,
78871
+ parent_turn_key: row.parent_turn_key,
78872
+ agent_type: row.agent_type,
78873
+ description: row.description,
78874
+ background: row.background !== 0,
78875
+ started_at: row.started_at,
78876
+ last_activity_at: row.last_activity_at,
78877
+ ended_at: row.ended_at,
78878
+ status: row.status,
78879
+ result_summary: row.result_summary,
78880
+ jsonl_agent_id: row.jsonl_agent_id,
78881
+ parent_agent_id: row.parent_agent_id ?? null,
78882
+ model: row.model ?? null
78883
+ };
78884
+ }
78885
+ function getSubagentByJsonlId(db2, jsonlAgentId) {
78886
+ const row = db2.prepare("SELECT * FROM subagents WHERE jsonl_agent_id = ?").get(jsonlAgentId);
78887
+ return row ? mapSubagentRow(row) : null;
78888
+ }
78756
78889
  function countRunningBackgroundSubagents(db2) {
78757
78890
  const row = db2.prepare("SELECT count(*) AS n FROM subagents WHERE background = 1 AND status = 'running'").get();
78758
78891
  return row?.n ?? 0;
@@ -78847,6 +78980,19 @@ function recordNestedSubagentDispatch(db2, args) {
78847
78980
  WHERE id = ?
78848
78981
  `).run(args.parentJsonlAgentId, args.parentJsonlAgentId, args.toolUseId);
78849
78982
  }
78983
+ function stampSubagentDispatchTurn(db2, args) {
78984
+ db2.prepare(`
78985
+ INSERT OR IGNORE INTO subagents
78986
+ (id, parent_session_id, parent_turn_key, agent_type, description,
78987
+ background, started_at, last_activity_at, status)
78988
+ VALUES (?, NULL, ?, ?, ?, ?, ?, ?, 'running')
78989
+ `).run(args.toolUseId, args.parentTurnKey, args.agentType ?? null, args.description ?? null, args.background ? 1 : 0, args.now, args.now);
78990
+ db2.prepare(`
78991
+ UPDATE subagents
78992
+ SET parent_turn_key = COALESCE(parent_turn_key, ?)
78993
+ WHERE id = ?
78994
+ `).run(args.parentTurnKey, args.toolUseId);
78995
+ }
78850
78996
  function resolveSubagentOriginTurnKey(db2, jsonlAgentId, maxHops = 5) {
78851
78997
  const seen = new Set;
78852
78998
  let currentJsonlId = jsonlAgentId;
@@ -81177,6 +81323,28 @@ function isDraftOfReply(textBlock, replyText) {
81177
81323
  }
81178
81324
 
81179
81325
  // registry/turns-schema.ts
81326
+ function mapRow(row) {
81327
+ return {
81328
+ turn_key: row.turn_key,
81329
+ chat_id: row.chat_id,
81330
+ thread_id: row.thread_id,
81331
+ started_at: row.started_at,
81332
+ ended_at: row.ended_at,
81333
+ ended_via: row.ended_via ?? null,
81334
+ last_assistant_msg_id: row.last_assistant_msg_id,
81335
+ last_assistant_done: row.last_assistant_done === null ? null : row.last_assistant_done !== 0,
81336
+ last_user_msg_id: row.last_user_msg_id,
81337
+ user_prompt_preview: row.user_prompt_preview,
81338
+ assistant_reply_preview: row.assistant_reply_preview,
81339
+ tool_call_count: row.tool_call_count,
81340
+ interrupt_reason: row.interrupt_reason,
81341
+ resumed_at: row.resumed_at,
81342
+ session_id: row.session_id ?? null,
81343
+ answer_redelivered_at: row.answer_redelivered_at ?? null,
81344
+ created_at: row.created_at,
81345
+ updated_at: row.updated_at
81346
+ };
81347
+ }
81180
81348
  function recordTurnStart(db3, args) {
81181
81349
  const now = Date.now();
81182
81350
  db3.prepare(`
@@ -81201,6 +81369,19 @@ function recordTurnEnd(db3, args) {
81201
81369
  WHERE turn_key = ?
81202
81370
  `).run(now, args.endedVia, args.lastAssistantMsgId ?? null, args.lastAssistantDone !== undefined ? args.lastAssistantDone ? 1 : 0 : null, args.assistantReplyPreview ?? null, args.toolCallCount !== undefined ? args.toolCallCount : null, now, args.turnKey);
81203
81371
  }
81372
+ function getTurnByKey(db3, turnKey3) {
81373
+ const row = db3.prepare(`SELECT * FROM turns WHERE turn_key = ?`).get(turnKey3);
81374
+ return row ? mapRow(row) : null;
81375
+ }
81376
+ function findMostRecentTurn(db3, beforeOrAtMs) {
81377
+ const row = db3.prepare(`
81378
+ SELECT * FROM turns
81379
+ WHERE started_at <= ?
81380
+ ORDER BY started_at DESC
81381
+ LIMIT 1
81382
+ `).get(beforeOrAtMs);
81383
+ return row ? mapRow(row) : null;
81384
+ }
81204
81385
  var INTERRUPTED_VIA = new Set([
81205
81386
  "restart",
81206
81387
  "reaped_stale",
@@ -81316,7 +81497,7 @@ function mayOpenActivityCard(input) {
81316
81497
  if (input.crossTurnAnswerDelivered)
81317
81498
  return false;
81318
81499
  if (input.finalAnswerEverDelivered) {
81319
- if (input.postAnswerSubagentActivity && input.producer === "tool")
81500
+ if ((input.postAnswerSubagentActivity || input.postAnswerMainActivity) && input.producer === "tool")
81320
81501
  return true;
81321
81502
  return false;
81322
81503
  }
@@ -81429,17 +81610,24 @@ class EmissionAuthority {
81429
81610
  }
81430
81611
 
81431
81612
  // gateway/feed-reopen-gate.ts
81613
+ var SUBSTANTIVE_REOPEN_MIN_LABELS = 2;
81432
81614
  function shouldReopenFeedAfterAck(input) {
81433
81615
  if (!input.finalAnswerDelivered)
81434
81616
  return false;
81435
- if (input.finalAnswerSubstantive)
81436
- return false;
81617
+ if (input.finalAnswerSubstantive) {
81618
+ if (input.reopenAfterSubstantiveEnabled !== true)
81619
+ return false;
81620
+ return (input.postSubstantiveToolLabelCount ?? 0) >= SUBSTANTIVE_REOPEN_MIN_LABELS;
81621
+ }
81437
81622
  return input.enabled === true;
81438
81623
  }
81439
81624
  function decideFeedReopen(input) {
81440
81625
  if (!shouldReopenFeedAfterAck(input)) {
81441
81626
  return { dropLabel: true };
81442
81627
  }
81628
+ if (input.finalAnswerSubstantive) {
81629
+ return { dropLabel: false, liftLeverOne: true };
81630
+ }
81443
81631
  return {
81444
81632
  dropLabel: false,
81445
81633
  reset: {
@@ -82165,6 +82353,8 @@ function beginTurn(deps, ev) {
82165
82353
  replyCalled: false,
82166
82354
  finalAnswerDelivered: false,
82167
82355
  finalAnswerSubstantive: false,
82356
+ postSubstantiveToolLabelCount: 0,
82357
+ postAnswerMainActivity: false,
82168
82358
  finalAnswerEverDelivered: false,
82169
82359
  answerDelivered: false,
82170
82360
  flushedAnswerText: null,
@@ -82278,6 +82468,7 @@ function handleSessionEvent(deps, ev) {
82278
82468
  CONTEXT_EXHAUSTION_COOLDOWN_MS,
82279
82469
  DELIVERY_CONFIRM_ENABLED,
82280
82470
  FEED_REOPEN_AFTER_ACK_ENABLED,
82471
+ FEED_REOPEN_AFTER_SUBSTANTIVE_ENABLED,
82281
82472
  HANDBACK_PRETURN_ENABLED,
82282
82473
  HISTORY_ENABLED,
82283
82474
  LIVENESS_TERMINAL_HONESTY,
@@ -82472,6 +82663,21 @@ function handleSessionEvent(deps, ev) {
82472
82663
  resolvePendingNarrativeOnTool(turn, ev.toolName, ev.input);
82473
82664
  turn.toolCallCount++;
82474
82665
  touchTurnActiveMarker(STATE_DIR);
82666
+ if ((ev.toolName === "Agent" || ev.toolName === "Task") && ev.toolUseId != null && ev.toolUseId.length > 0 && turn.registryKey != null && turnsDb != null) {
82667
+ try {
82668
+ stampSubagentDispatchTurn(turnsDb, {
82669
+ toolUseId: ev.toolUseId,
82670
+ parentTurnKey: turn.registryKey,
82671
+ agentType: typeof ev.input?.subagent_type === "string" ? ev.input.subagent_type : null,
82672
+ description: typeof ev.input?.description === "string" ? ev.input.description : null,
82673
+ background: ev.input?.run_in_background === true,
82674
+ now: Date.now()
82675
+ });
82676
+ } catch (err) {
82677
+ process.stderr.write(`telegram gateway: dispatch-time parent_turn_key stamp failed toolUseId=${ev.toolUseId}: ${err.message}
82678
+ `);
82679
+ }
82680
+ }
82475
82681
  preambleSuppressor.onTool({ isReplyTool: isTelegramSurfaceTool(ev.toolName) });
82476
82682
  surfaceMemoryLegibility(turn, ev.toolName, ev.toolUseId, ev.input);
82477
82683
  const ctrl = activeStatusReactions.get(statusKey(turn.sessionChatId, turn.sessionThreadId));
@@ -82510,16 +82716,24 @@ function handleSessionEvent(deps, ev) {
82510
82716
  if (isTelegramSurfaceTool(ev.toolName))
82511
82717
  return;
82512
82718
  if (turn.finalAnswerDelivered) {
82719
+ if (turn.finalAnswerSubstantive)
82720
+ turn.postSubstantiveToolLabelCount++;
82513
82721
  const reopen = decideFeedReopen({
82514
82722
  finalAnswerDelivered: turn.finalAnswerDelivered,
82515
82723
  finalAnswerSubstantive: turn.finalAnswerSubstantive,
82516
- enabled: FEED_REOPEN_AFTER_ACK_ENABLED
82724
+ enabled: FEED_REOPEN_AFTER_ACK_ENABLED,
82725
+ reopenAfterSubstantiveEnabled: FEED_REOPEN_AFTER_SUBSTANTIVE_ENABLED,
82726
+ postSubstantiveToolLabelCount: turn.postSubstantiveToolLabelCount
82517
82727
  });
82518
82728
  if (reopen.dropLabel)
82519
82729
  return;
82520
- turn.finalAnswerDelivered = reopen.reset.finalAnswerDelivered;
82521
- turn.activityMessageId = reopen.reset.activityMessageId;
82522
- turn.activityLastSentRender = reopen.reset.activityLastSentRender;
82730
+ if (reopen.reset != null) {
82731
+ turn.finalAnswerDelivered = reopen.reset.finalAnswerDelivered;
82732
+ turn.activityMessageId = reopen.reset.activityMessageId;
82733
+ turn.activityLastSentRender = reopen.reset.activityLastSentRender;
82734
+ }
82735
+ if (reopen.liftLeverOne)
82736
+ turn.postAnswerMainActivity = true;
82523
82737
  }
82524
82738
  const rendered = appendActivityLabel(turn.mirrorLines, ev.label);
82525
82739
  if (rendered != null) {
@@ -83450,7 +83664,8 @@ function createNarrativeLane(deps) {
83450
83664
  finalAnswerEverDelivered: turn.finalAnswerEverDelivered,
83451
83665
  labeledToolCount: turn.labeledToolCount,
83452
83666
  crossTurnAnswerDelivered,
83453
- postAnswerSubagentActivity: openFlags?.postAnswerSubagentActivity
83667
+ postAnswerSubagentActivity: openFlags?.postAnswerSubagentActivity,
83668
+ postAnswerMainActivity: turn.postAnswerMainActivity
83454
83669
  })) {
83455
83670
  break;
83456
83671
  }
@@ -94966,6 +95181,77 @@ function createTurnStartSurfaces(deps) {
94966
95181
  return { armTurnStartSurfaces };
94967
95182
  }
94968
95183
 
95184
+ // gateway/progress-fallback-cap.ts
95185
+ var recentSends = new Map;
95186
+ var PROGRESS_FALLBACK_WINDOW_MS = 15 * 60000;
95187
+ var PROGRESS_FALLBACK_MAX = 5;
95188
+ function prune(key, now) {
95189
+ const cutoff = now - PROGRESS_FALLBACK_WINDOW_MS;
95190
+ const recent = (recentSends.get(key) ?? []).filter((ts) => ts > cutoff);
95191
+ if (recent.length === 0)
95192
+ recentSends.delete(key);
95193
+ else
95194
+ recentSends.set(key, recent);
95195
+ return recent;
95196
+ }
95197
+ function reserveProgressFallbackSlot(key, now) {
95198
+ const recent = prune(key, now);
95199
+ if (recent.length >= PROGRESS_FALLBACK_MAX)
95200
+ return null;
95201
+ recent.push(now);
95202
+ recentSends.set(key, recent);
95203
+ let released = false;
95204
+ return {
95205
+ release: () => {
95206
+ if (released)
95207
+ return;
95208
+ released = true;
95209
+ const arr = recentSends.get(key);
95210
+ if (!arr)
95211
+ return;
95212
+ const idx = arr.indexOf(now);
95213
+ if (idx >= 0)
95214
+ arr.splice(idx, 1);
95215
+ if (arr.length === 0)
95216
+ recentSends.delete(key);
95217
+ else
95218
+ recentSends.set(key, arr);
95219
+ }
95220
+ };
95221
+ }
95222
+ var PROGRESS_TURN_MAX = 5;
95223
+ function reserveProgressSlot(deps) {
95224
+ const { key, now, turnStart, turnCount } = deps;
95225
+ if (turnStart != null) {
95226
+ const current = turnCount.get(key) ?? 0;
95227
+ if (current >= PROGRESS_TURN_MAX)
95228
+ return null;
95229
+ turnCount.set(key, current + 1);
95230
+ let released = false;
95231
+ return {
95232
+ release: () => {
95233
+ if (released)
95234
+ return;
95235
+ released = true;
95236
+ turnCount.set(key, Math.max(0, (turnCount.get(key) ?? 0) - 1));
95237
+ }
95238
+ };
95239
+ }
95240
+ return reserveProgressFallbackSlot(key, now);
95241
+ }
95242
+ async function sendWithProgressCap(deps, send) {
95243
+ const reservation = reserveProgressSlot(deps);
95244
+ if (reservation === null)
95245
+ return { capped: true };
95246
+ try {
95247
+ const result = await send();
95248
+ return { capped: false, result };
95249
+ } catch (err) {
95250
+ reservation.release();
95251
+ throw err;
95252
+ }
95253
+ }
95254
+
94969
95255
  // gateway/delivery-confirm-wiring.ts
94970
95256
  function createDeliveryConfirmWiring(deps) {
94971
95257
  const {
@@ -96432,6 +96718,7 @@ ${result}
96432
96718
  meta: {
96433
96719
  source: "subagent_handback",
96434
96720
  outcome: opts.ctx.outcome,
96721
+ chat_id: opts.ctx.chatId,
96435
96722
  message_id: String(ts),
96436
96723
  ...opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {},
96437
96724
  ...opts.ctx.jsonlAgentId ? { subagent_jsonl_id: opts.ctx.jsonlAgentId } : {}
@@ -96511,6 +96798,7 @@ ${summary}
96511
96798
  text: text4,
96512
96799
  meta: {
96513
96800
  source: "subagent_progress",
96801
+ chat_id: opts.ctx.chatId,
96514
96802
  ...opts.ctx.threadId != null ? { message_thread_id: String(opts.ctx.threadId) } : {},
96515
96803
  subagent_jsonl_id: opts.ctx.subagentJsonlId,
96516
96804
  bucket_idx: String(opts.ctx.bucketIdx),
@@ -102121,10 +102409,10 @@ function startOutboxSweep(deps) {
102121
102409
  }
102122
102410
 
102123
102411
  // ../src/build-info.ts
102124
- var VERSION2 = "0.20.2";
102125
- var COMMIT_SHA = "d3170381";
102126
- var COMMIT_DATE = "2026-08-03T11:17:19Z";
102127
- var LATEST_PR = 4313;
102412
+ var VERSION2 = "0.20.4";
102413
+ var COMMIT_SHA = "7f669382";
102414
+ var COMMIT_DATE = "2026-08-04T01:43:38Z";
102415
+ var LATEST_PR = 4331;
102128
102416
  var COMMITS_AHEAD_OF_TAG = 0;
102129
102417
 
102130
102418
  // gateway/boot-version.ts
@@ -103176,7 +103464,7 @@ function openTurnsDb(agentDir) {
103176
103464
  } catch {}
103177
103465
  return db3;
103178
103466
  }
103179
- function mapRow(row) {
103467
+ function mapRow2(row) {
103180
103468
  return {
103181
103469
  turn_key: row.turn_key,
103182
103470
  chat_id: row.chat_id,
@@ -103213,10 +103501,6 @@ function recordTurnEnd2(db3, args) {
103213
103501
  WHERE turn_key = ?
103214
103502
  `).run(now, args.endedVia, args.lastAssistantMsgId ?? null, args.lastAssistantDone !== undefined ? args.lastAssistantDone ? 1 : 0 : null, args.assistantReplyPreview ?? null, args.toolCallCount !== undefined ? args.toolCallCount : null, now, args.turnKey);
103215
103503
  }
103216
- function getTurnByKey(db3, turnKey3) {
103217
- const row = db3.prepare(`SELECT * FROM turns WHERE turn_key = ?`).get(turnKey3);
103218
- return row ? mapRow(row) : null;
103219
- }
103220
103504
  function markOrphanedWithTimeoutClassification(db3, opts) {
103221
103505
  const now = opts.now ?? Date.now();
103222
103506
  const isHang = opts.markerAgeMs != null && opts.markerAgeMs >= opts.hangThresholdMs && opts.markerTurnKey != null && opts.markerTurnKey.length > 0;
@@ -103307,7 +103591,7 @@ function findLatestTurnIfInterrupted(db3) {
103307
103591
  `).get();
103308
103592
  if (!row)
103309
103593
  return null;
103310
- const turn = mapRow(row);
103594
+ const turn = mapRow2(row);
103311
103595
  if (turn.resumed_at != null)
103312
103596
  return null;
103313
103597
  if (turn.ended_at == null)
@@ -104296,7 +104580,7 @@ function applySubagentsSchema(db3) {
104296
104580
  db3.exec("CREATE INDEX IF NOT EXISTS subagents_jsonl_id ON subagents(jsonl_agent_id)");
104297
104581
  db3.exec("CREATE INDEX IF NOT EXISTS subagents_parent_agent ON subagents(parent_agent_id)");
104298
104582
  }
104299
- function mapSubagentRow(row) {
104583
+ function mapSubagentRow2(row) {
104300
104584
  return {
104301
104585
  id: row.id,
104302
104586
  parent_session_id: row.parent_session_id,
@@ -104314,9 +104598,9 @@ function mapSubagentRow(row) {
104314
104598
  model: row.model ?? null
104315
104599
  };
104316
104600
  }
104317
- function getSubagentByJsonlId(db3, jsonlAgentId) {
104601
+ function getSubagentByJsonlId2(db3, jsonlAgentId) {
104318
104602
  const row = db3.prepare("SELECT * FROM subagents WHERE jsonl_agent_id = ?").get(jsonlAgentId);
104319
- return row ? mapSubagentRow(row) : null;
104603
+ return row ? mapSubagentRow2(row) : null;
104320
104604
  }
104321
104605
  function listNonTerminalSubagentsForTurn(db3, parentTurnKey) {
104322
104606
  const rows = db3.prepare(`
@@ -104325,23 +104609,74 @@ function listNonTerminalSubagentsForTurn(db3, parentTurnKey) {
104325
104609
  AND status NOT IN ('completed', 'failed')
104326
104610
  ORDER BY started_at ASC
104327
104611
  `).all(parentTurnKey);
104328
- return rows.map(mapSubagentRow);
104612
+ return rows.map(mapSubagentRow2);
104329
104613
  }
104330
- function resolveSubagentOriginTurnKey2(db3, jsonlAgentId, maxHops = 5) {
104331
- const seen = new Set;
104332
- let currentJsonlId = jsonlAgentId;
104333
- for (let hop = 0;hop <= maxHops && currentJsonlId != null; hop++) {
104334
- if (seen.has(currentJsonlId))
104614
+
104615
+ // gateway/subagent-origin-surface.ts
104616
+ function turnToSurfaceChat(turn) {
104617
+ if (turn == null || turn.chat_id.length === 0)
104618
+ return null;
104619
+ const threadNum = turn.thread_id != null && turn.thread_id.length > 0 ? Number(turn.thread_id) : NaN;
104620
+ return {
104621
+ chatId: turn.chat_id,
104622
+ ...Number.isFinite(threadNum) ? { threadId: threadNum } : {}
104623
+ };
104624
+ }
104625
+ function resolveSubagentOriginChatDb(db3, jsonlAgentId) {
104626
+ try {
104627
+ const originKey = resolveSubagentOriginTurnKey(db3, jsonlAgentId);
104628
+ if (originKey == null)
104335
104629
  return null;
104336
- seen.add(currentJsonlId);
104337
- const row = db3.prepare("SELECT parent_turn_key, parent_agent_id FROM subagents WHERE jsonl_agent_id = ? LIMIT 1").get(currentJsonlId);
104338
- if (row == null)
104630
+ return turnToSurfaceChat(getTurnByKey(db3, originKey));
104631
+ } catch {
104632
+ return null;
104633
+ }
104634
+ }
104635
+ function resolveRecentTurnFallbackChat(db3, jsonlAgentId) {
104636
+ try {
104637
+ const worker = getSubagentByJsonlId(db3, jsonlAgentId);
104638
+ if (worker == null)
104339
104639
  return null;
104340
- if (row.parent_turn_key != null && row.parent_turn_key.length > 0)
104341
- return row.parent_turn_key;
104342
- currentJsonlId = row.parent_agent_id ?? null;
104640
+ return turnToSurfaceChat(findMostRecentTurn(db3, worker.started_at));
104641
+ } catch {
104642
+ return null;
104343
104643
  }
104344
- return null;
104644
+ }
104645
+ var RECENT_TURN_FLOOR_LOG_CAP = 256;
104646
+ var recentTurnFloorLogged = new Set;
104647
+ function noteWorkerRecentTurnFloor(agentId, dest, log = (line) => process.stderr.write(line)) {
104648
+ if (recentTurnFloorLogged.has(agentId))
104649
+ return;
104650
+ recentTurnFloorLogged.add(agentId);
104651
+ if (recentTurnFloorLogged.size > RECENT_TURN_FLOOR_LOG_CAP) {
104652
+ const oldest = recentTurnFloorLogged.values().next().value;
104653
+ if (oldest != null)
104654
+ recentTurnFloorLogged.delete(oldest);
104655
+ }
104656
+ log(`telegram gateway: worker origin unresolved agent=${agentId} \u2014 flooring to pre-dispatch recent turn chat=${dest.chatId}${dest.threadId != null ? ` thread=${dest.threadId}` : ""}
104657
+ `);
104658
+ }
104659
+ function resolveWorkerSurfaceForDecider(db3, jsonlAgentId, opts) {
104660
+ const dest = resolveWorkerSurfaceChat(db3, jsonlAgentId, opts);
104661
+ if (dest.via === "recent-turn")
104662
+ noteWorkerRecentTurnFloor(jsonlAgentId, dest);
104663
+ return {
104664
+ fleetChatId: dest.via === "owner-dm" || dest.via === "none" ? "" : dest.chatId,
104665
+ ...dest.threadId != null ? { originThreadId: dest.threadId } : {}
104666
+ };
104667
+ }
104668
+ function resolveWorkerSurfaceChat(db3, jsonlAgentId, opts) {
104669
+ const origin = db3 != null ? resolveSubagentOriginChatDb(db3, jsonlAgentId) : null;
104670
+ if (origin != null && origin.chatId.length > 0)
104671
+ return { ...origin, via: "origin" };
104672
+ if (opts.fleetChatId.length > 0)
104673
+ return { chatId: opts.fleetChatId, via: "fleet" };
104674
+ const recent = db3 != null ? resolveRecentTurnFallbackChat(db3, jsonlAgentId) : null;
104675
+ if (recent != null)
104676
+ return { ...recent, via: "recent-turn" };
104677
+ if (opts.ownerDm.length > 0)
104678
+ return { chatId: opts.ownerDm, via: "owner-dm" };
104679
+ return { chatId: "", via: "none" };
104345
104680
  }
104346
104681
 
104347
104682
  // gateway/worker-feed-dispatch.ts
@@ -104887,21 +105222,12 @@ if (isGatewayMain)
104887
105222
  function resolveSubagentOriginChat(agentId) {
104888
105223
  if (turnsDb == null)
104889
105224
  return null;
104890
- try {
104891
- const originKey = resolveSubagentOriginTurnKey2(turnsDb, agentId);
104892
- if (originKey == null)
104893
- return null;
104894
- const turn = getTurnByKey(turnsDb, originKey);
104895
- if (turn == null || turn.chat_id.length === 0)
104896
- return null;
104897
- const threadNum = turn.thread_id != null && turn.thread_id.length > 0 ? Number(turn.thread_id) : NaN;
104898
- return {
104899
- chatId: turn.chat_id,
104900
- threadId: Number.isFinite(threadNum) ? threadNum : undefined
104901
- };
104902
- } catch {
105225
+ return resolveSubagentOriginChatDb(turnsDb, agentId);
105226
+ }
105227
+ function recentTurnFallbackChat(agentId) {
105228
+ if (turnsDb == null)
104903
105229
  return null;
104904
- }
105230
+ return resolveRecentTurnFallbackChat(turnsDb, agentId);
104905
105231
  }
104906
105232
  var WORKER_FEED_FALLBACK_LOG_CAP = 256;
104907
105233
  var WORKER_FEED_STALE_TTL_MARGIN_MS = 5 * 60000;
@@ -104925,17 +105251,16 @@ function noteWorkerFeedOwnerDmFallback(agentId) {
104925
105251
  }
104926
105252
  var workerFeedOriginDeferrals = new Map;
104927
105253
  var WORKER_FEED_ORIGIN_DEFER_MAX = 10;
104928
- function resolveWorkerFeedChat(agentId, fleetChatId, fallbackThreadId) {
104929
- const origin = resolveSubagentOriginChat(agentId);
104930
- if (origin != null && origin.chatId.length > 0)
104931
- return origin;
104932
- if (fleetChatId.length > 0)
104933
- return { chatId: fleetChatId, threadId: fallbackThreadId };
104934
- const ownerDm = loadAccess().allowFrom[0] ?? "";
104935
- if (origin == null && fleetChatId.length === 0 && ownerDm.length > 0) {
105254
+ function resolveWorkerFeedChat(agentId, fleetChatId) {
105255
+ const dest = resolveWorkerSurfaceChat(turnsDb, agentId, {
105256
+ fleetChatId,
105257
+ ownerDm: loadAccess().allowFrom[0] ?? ""
105258
+ });
105259
+ if (dest.via === "recent-turn")
105260
+ noteWorkerRecentTurnFloor(agentId, dest);
105261
+ if (dest.via === "owner-dm")
104936
105262
  noteWorkerFeedOwnerDmFallback(agentId);
104937
- }
104938
- return { chatId: ownerDm, threadId: origin?.threadId ?? fallbackThreadId };
105263
+ return { chatId: dest.chatId, ...dest.threadId != null ? { threadId: dest.threadId } : {} };
104939
105264
  }
104940
105265
  var REGISTRY_REAPER_INTERVAL_MS = 6 * 60 * 60 * 1000;
104941
105266
  function runHistoryReaperNow(reason) {
@@ -105197,6 +105522,7 @@ var TOPIC_FRAMING_ENABLED = process.env.SWITCHROOM_TOPIC_FRAMING !== "0";
105197
105522
  var QUEUED_STATUS_UX_ENABLED = process.env.SWITCHROOM_QUEUED_STATUS_UX !== "0";
105198
105523
  var MIDFLIGHT_BUSY_ACK_ENABLED = process.env.SWITCHROOM_MIDFLIGHT_BUSY_ACK !== "0";
105199
105524
  var FEED_REOPEN_AFTER_ACK_ENABLED = process.env.SWITCHROOM_FEED_REOPEN_AFTER_ACK !== "0";
105525
+ var FEED_REOPEN_AFTER_SUBSTANTIVE_ENABLED = process.env.SWITCHROOM_FEED_REOPEN_AFTER_SUBSTANTIVE !== "0";
105200
105526
  var FEED_HEARTBEAT_ENABLED = process.env.SWITCHROOM_FEED_HEARTBEAT !== "0";
105201
105527
  var FEED_HEARTBEAT_TICK_MS = 6000;
105202
105528
  var FEED_HEARTBEAT_MIN_STALE_MS = 6000;
@@ -107697,7 +108023,7 @@ async function runMidSessionCardReaper() {
107697
108023
  if (turnsDb == null)
107698
108024
  return "unknown";
107699
108025
  try {
107700
- const row = getSubagentByJsonlId(turnsDb, agentId);
108026
+ const row = getSubagentByJsonlId2(turnsDb, agentId);
107701
108027
  if (row == null)
107702
108028
  return "unknown";
107703
108029
  if (row.status === "completed" || row.status === "failed")
@@ -109571,26 +109897,23 @@ async function executeProgressUpdate(args) {
109571
109897
  }
109572
109898
  }
109573
109899
  const turnStart = activeTurnStartedAt.get(key);
109574
- if (turnStart != null) {
109575
- const currentCount = progressUpdateTurnCount.get(key) ?? 0;
109576
- if (currentCount >= 5) {
109577
- return {
109578
- content: [
109579
- {
109580
- type: "text",
109581
- text: JSON.stringify({ ok: false, reason: "turn_limit" })
109582
- }
109583
- ]
109584
- };
109585
- }
109586
- progressUpdateTurnCount.set(key, currentCount + 1);
109587
- }
109588
109900
  const access = loadAccess();
109589
109901
  const literalText = (access.parseMode ?? "html") === "text";
109590
109902
  const sendOpts = {
109591
109903
  ...threadId != null ? { message_thread_id: threadId } : {}
109592
109904
  };
109593
- const sent = await robustApiCall(() => literalText ? lockedBot.api.sendMessage(chat_id, text5, sendOpts) : lockedBot.api.sendRichMessage(chat_id, richMessage2(text5), sendOpts), { verb: "sendMessage", chat_id, threadId });
109905
+ const capped = await sendWithProgressCap({ key, now, turnStart, turnCount: progressUpdateTurnCount }, () => robustApiCall(() => literalText ? lockedBot.api.sendMessage(chat_id, text5, sendOpts) : lockedBot.api.sendRichMessage(chat_id, richMessage2(text5), sendOpts), { verb: "sendMessage", chat_id, threadId }));
109906
+ if (capped.capped) {
109907
+ return {
109908
+ content: [
109909
+ {
109910
+ type: "text",
109911
+ text: JSON.stringify({ ok: false, reason: "turn_limit" })
109912
+ }
109913
+ ]
109914
+ };
109915
+ }
109916
+ const sent = capped.result;
109594
109917
  if (HISTORY_ENABLED) {
109595
109918
  recordOutbound({
109596
109919
  chat_id,
@@ -110421,6 +110744,7 @@ function gatewayStreamRenderDeps() {
110421
110744
  CONTEXT_EXHAUSTION_COOLDOWN_MS,
110422
110745
  DELIVERY_CONFIRM_ENABLED,
110423
110746
  FEED_REOPEN_AFTER_ACK_ENABLED,
110747
+ FEED_REOPEN_AFTER_SUBSTANTIVE_ENABLED,
110424
110748
  HANDBACK_PRETURN_ENABLED,
110425
110749
  HISTORY_ENABLED,
110426
110750
  LIVENESS_TERMINAL_HONESTY,
@@ -116006,7 +116330,7 @@ async function startGateway() {
116006
116330
  let dispatch = resolveWorkerFeedDispatch(null, description2, entryBackground);
116007
116331
  if (turnsDb != null) {
116008
116332
  try {
116009
- dispatch = resolveWorkerFeedDispatch(getSubagentByJsonlId(turnsDb, agentId), description2, entryBackground);
116333
+ dispatch = resolveWorkerFeedDispatch(getSubagentByJsonlId2(turnsDb, agentId), description2, entryBackground);
116010
116334
  } catch {}
116011
116335
  }
116012
116336
  let isBackground = dispatch.isBackground;
@@ -116088,14 +116412,13 @@ async function startGateway() {
116088
116412
  model: dispatch.feedModel ?? undefined
116089
116413
  });
116090
116414
  }
116091
- const handbackOrigin = resolveSubagentOriginChat(agentId);
116415
+ const hbOwnerDm = loadAccess().allowFrom[0] ?? "";
116092
116416
  const decision = decideSubagentHandback({
116093
116417
  handbackEnvValue: process.env.SWITCHROOM_SUBAGENT_HANDBACK,
116094
116418
  outcome,
116095
116419
  isBackground,
116096
- fleetChatId: handbackOrigin?.chatId || fleetChatId,
116097
- ...handbackOrigin?.threadId != null ? { originThreadId: handbackOrigin.threadId } : {},
116098
- ownerChatId: loadAccess().allowFrom[0] ?? "",
116420
+ ...resolveWorkerSurfaceForDecider(turnsDb, agentId, { fleetChatId, ownerDm: hbOwnerDm }),
116421
+ ownerChatId: hbOwnerDm,
116099
116422
  taskDescription: description2,
116100
116423
  resultText,
116101
116424
  jsonlAgentId: agentId
@@ -116136,7 +116459,7 @@ async function startGateway() {
116136
116459
  let dispatch = resolveWorkerFeedDispatch(null, description2);
116137
116460
  if (turnsDb != null) {
116138
116461
  try {
116139
- dispatch = resolveWorkerFeedDispatch(getSubagentByJsonlId(turnsDb, agentId), description2);
116462
+ dispatch = resolveWorkerFeedDispatch(getSubagentByJsonlId2(turnsDb, agentId), description2);
116140
116463
  } catch {}
116141
116464
  }
116142
116465
  const isBackground = dispatch.isBackground || dispatch.isNested;
@@ -116212,14 +116535,16 @@ async function startGateway() {
116212
116535
  stampTurn.subagentActivityAt = Date.now();
116213
116536
  }
116214
116537
  if (workerFeedEnabled) {
116538
+ const wfOrigin = resolveSubagentOriginChat(agentId);
116539
+ const wfStamp = stampTurn != null ? { chatId: stampTurn.sessionChatId, threadId: stampTurn.sessionThreadId } : wfOrigin == null ? recentTurnFallbackChat(agentId) : null;
116215
116540
  const dest = decideWorkerFeedDestination({
116216
- origin: resolveSubagentOriginChat(agentId),
116541
+ origin: wfOrigin,
116217
116542
  cardExists: workerActivityFeed?.has(agentId) === true,
116218
116543
  priorDeferrals: workerFeedOriginDeferrals.get(agentId) ?? 0,
116219
116544
  maxDeferrals: WORKER_FEED_ORIGIN_DEFER_MAX,
116220
116545
  fleetChatId,
116221
- stampChatId: stampTurn?.sessionChatId,
116222
- stampThreadId: stampTurn?.sessionThreadId,
116546
+ stampChatId: wfStamp?.chatId,
116547
+ stampThreadId: wfStamp?.threadId,
116223
116548
  ownerDm: loadAccess().allowFrom[0] ?? ""
116224
116549
  });
116225
116550
  if (dest.action === "defer") {
@@ -116233,6 +116558,9 @@ async function startGateway() {
116233
116558
  }
116234
116559
  if (dest.ownerDmFallback)
116235
116560
  noteWorkerFeedOwnerDmFallback(agentId);
116561
+ if (stampTurn == null && wfOrigin == null && wfStamp != null && dest.chatId === wfStamp.chatId) {
116562
+ noteWorkerRecentTurnFloor(agentId, wfStamp);
116563
+ }
116236
116564
  workerActivityFeed?.update(agentId, dest.chatId, {
116237
116565
  description: dispatch.feedDescription,
116238
116566
  lastTool,
@@ -116245,14 +116573,17 @@ async function startGateway() {
116245
116573
  }, dest.threadId);
116246
116574
  return;
116247
116575
  }
116248
- const progressOrigin = resolveSubagentOriginChat(agentId);
116576
+ const pgOwnerDm = loadAccess().allowFrom[0] ?? "";
116577
+ const progressSurface = resolveWorkerSurfaceForDecider(turnsDb, agentId, {
116578
+ fleetChatId,
116579
+ ownerDm: pgOwnerDm
116580
+ });
116249
116581
  const decision = decideSubagentProgress({
116250
116582
  skeleton: skeleton === true,
116251
116583
  disableEnvValue: process.env.SWITCHROOM_DISABLE_SUBAGENT_PROGRESS,
116252
116584
  isBackground,
116253
- fleetChatId: progressOrigin?.chatId || fleetChatId,
116254
- ...progressOrigin?.threadId != null ? { originThreadId: progressOrigin.threadId } : {},
116255
- ownerChatId: loadAccess().allowFrom[0] ?? "",
116585
+ ...progressSurface,
116586
+ ownerChatId: pgOwnerDm,
116256
116587
  subagentJsonlId: agentId,
116257
116588
  taskDescription: description2,
116258
116589
  latestSummary,
@@ -116264,7 +116595,7 @@ async function startGateway() {
116264
116595
  return;
116265
116596
  setBucketIdx(decision.bucketIdx);
116266
116597
  pendingInboundBuffer.push(process.env.SWITCHROOM_AGENT_NAME ?? "", decision.inbound);
116267
- clearPending(statusKey(decision.chatId, progressOrigin?.threadId), "progress");
116598
+ clearPending(statusKey(decision.chatId, progressSurface.originThreadId), "progress");
116268
116599
  process.stderr.write(`telegram gateway: subagent-progress queued agent=${agentId} bucket=${decision.bucketIdx} elapsed_ms=${elapsedMs} chat=${decision.chatId}
116269
116600
  `);
116270
116601
  }