switchroom 0.12.13 → 0.12.15

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.
@@ -24664,7 +24664,7 @@ function decodeResponse2(line) {
24664
24664
  const obj = JSON.parse(line);
24665
24665
  return ResponseSchema2.parse(obj);
24666
24666
  }
24667
- var MAX_FRAME_BYTES2, GetRequestSchema, PutRequestSchema, ListRequestSchema, MintGrantRequestSchema, ListGrantsRequestSchema, RevokeGrantRequestSchema, StatusRequestSchema, LockRequestSchema, ApprovalRequestRequestSchema, ApprovalLookupRequestSchema, ApprovalConsumeRequestSchema, ApprovalRevokeRequestSchema, ApprovalListRequestSchema, ApprovalDecisionModeSchema, ApprovalRecordRequestSchema, RequestSchema2, VaultEntrySchema, ErrorCode, OkEntryResponseSchema, OkKeysResponseSchema, BrokerStatus, OkStatusResponseSchema, OkLockResponseSchema, OkPutResponseSchema, OkMintGrantResponseSchema, GrantMetaSchema, OkListGrantsResponseSchema, OkRevokeGrantResponseSchema, OkApprovalRequestResponseSchema, ApprovalDecisionMetaSchema, OkApprovalLookupResponseSchema, OkApprovalConsumeResponseSchema, OkApprovalRevokeResponseSchema, OkApprovalListResponseSchema, OkApprovalRecordResponseSchema, ErrorResponseSchema2, ResponseSchema2;
24667
+ var MAX_FRAME_BYTES2, GetRequestSchema, PutRequestSchema, ListRequestSchema, MintGrantRequestSchema, ListGrantsRequestSchema, RevokeGrantRequestSchema, StatusRequestSchema, LockRequestSchema, PreflightAccessRequestSchema, OkPreflightAccessResponseSchema, ApprovalRequestRequestSchema, ApprovalLookupRequestSchema, ApprovalConsumeRequestSchema, ApprovalRevokeRequestSchema, ApprovalListRequestSchema, ApprovalDecisionModeSchema, ApprovalRecordRequestSchema, ApprovalConsumeRecordRequestSchema, RequestSchema2, VaultEntrySchema, ErrorCode, OkEntryResponseSchema, OkKeysResponseSchema, BrokerStatus, OkStatusResponseSchema, OkLockResponseSchema, OkPutResponseSchema, OkMintGrantResponseSchema, GrantMetaSchema, OkListGrantsResponseSchema, OkRevokeGrantResponseSchema, OkApprovalRequestResponseSchema, ApprovalDecisionMetaSchema, OkApprovalLookupResponseSchema, OkApprovalConsumeResponseSchema, OkApprovalRevokeResponseSchema, OkApprovalListResponseSchema, OkApprovalRecordResponseSchema, OkApprovalConsumeRecordResponseSchema, ErrorResponseSchema2, ResponseSchema2;
24668
24668
  var init_protocol2 = __esm(() => {
24669
24669
  init_zod();
24670
24670
  MAX_FRAME_BYTES2 = 64 * 1024;
@@ -24723,6 +24723,24 @@ var init_protocol2 = __esm(() => {
24723
24723
  v: exports_external.literal(1),
24724
24724
  op: exports_external.literal("lock")
24725
24725
  });
24726
+ PreflightAccessRequestSchema = exports_external.object({
24727
+ v: exports_external.literal(1),
24728
+ op: exports_external.literal("preflight_access"),
24729
+ agent: exports_external.string().min(1),
24730
+ keys: exports_external.array(exports_external.string().min(1)).min(1).max(128)
24731
+ });
24732
+ OkPreflightAccessResponseSchema = exports_external.object({
24733
+ ok: exports_external.literal(true),
24734
+ op: exports_external.literal("preflight_access"),
24735
+ results: exports_external.array(exports_external.object({
24736
+ key: exports_external.string(),
24737
+ exists: exports_external.boolean(),
24738
+ acl_ok: exports_external.boolean(),
24739
+ acl_reason: exports_external.string().optional(),
24740
+ scope_ok: exports_external.boolean(),
24741
+ scope_reason: exports_external.string().optional()
24742
+ }))
24743
+ });
24726
24744
  ApprovalRequestRequestSchema = exports_external.object({
24727
24745
  v: exports_external.literal(1),
24728
24746
  op: exports_external.literal("approval_request"),
@@ -24774,12 +24792,22 @@ var init_protocol2 = __esm(() => {
24774
24792
  granted_by_user_id: exports_external.number().int(),
24775
24793
  ttl_ms: exports_external.number().int().positive().nullable().optional()
24776
24794
  });
24795
+ ApprovalConsumeRecordRequestSchema = exports_external.object({
24796
+ v: exports_external.literal(1),
24797
+ op: exports_external.literal("approval_consume_record"),
24798
+ request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
24799
+ decision: ApprovalDecisionModeSchema,
24800
+ approver_set: exports_external.array(exports_external.string()),
24801
+ granted_by_user_id: exports_external.number().int(),
24802
+ ttl_ms: exports_external.number().int().positive().nullable().optional()
24803
+ });
24777
24804
  RequestSchema2 = exports_external.discriminatedUnion("op", [
24778
24805
  GetRequestSchema,
24779
24806
  PutRequestSchema,
24780
24807
  ListRequestSchema,
24781
24808
  StatusRequestSchema,
24782
24809
  LockRequestSchema,
24810
+ PreflightAccessRequestSchema,
24783
24811
  MintGrantRequestSchema,
24784
24812
  ListGrantsRequestSchema,
24785
24813
  RevokeGrantRequestSchema,
@@ -24788,7 +24816,8 @@ var init_protocol2 = __esm(() => {
24788
24816
  ApprovalConsumeRequestSchema,
24789
24817
  ApprovalRevokeRequestSchema,
24790
24818
  ApprovalListRequestSchema,
24791
- ApprovalRecordRequestSchema
24819
+ ApprovalRecordRequestSchema,
24820
+ ApprovalConsumeRecordRequestSchema
24792
24821
  ]);
24793
24822
  VaultEntrySchema = exports_external.union([
24794
24823
  exports_external.object({ kind: exports_external.literal("string"), value: exports_external.string() }),
@@ -24910,6 +24939,15 @@ var init_protocol2 = __esm(() => {
24910
24939
  ok: exports_external.literal(true),
24911
24940
  decision_id: exports_external.string()
24912
24941
  });
24942
+ OkApprovalConsumeRecordResponseSchema = exports_external.object({
24943
+ ok: exports_external.literal(true),
24944
+ consumed: exports_external.boolean(),
24945
+ decision_id: exports_external.string().optional(),
24946
+ agent_unit: exports_external.string().optional(),
24947
+ scope: exports_external.string().optional(),
24948
+ action: exports_external.string().optional(),
24949
+ why: exports_external.string().nullable().optional()
24950
+ });
24913
24951
  ErrorResponseSchema2 = exports_external.object({
24914
24952
  ok: exports_external.literal(false),
24915
24953
  code: ErrorCode,
@@ -24920,6 +24958,7 @@ var init_protocol2 = __esm(() => {
24920
24958
  OkKeysResponseSchema,
24921
24959
  OkStatusResponseSchema,
24922
24960
  OkLockResponseSchema,
24961
+ OkPreflightAccessResponseSchema,
24923
24962
  OkPutResponseSchema,
24924
24963
  OkMintGrantResponseSchema,
24925
24964
  OkListGrantsResponseSchema,
@@ -24930,6 +24969,7 @@ var init_protocol2 = __esm(() => {
24930
24969
  OkApprovalRevokeResponseSchema,
24931
24970
  OkApprovalListResponseSchema,
24932
24971
  OkApprovalRecordResponseSchema,
24972
+ OkApprovalConsumeRecordResponseSchema,
24933
24973
  ErrorResponseSchema2
24934
24974
  ]);
24935
24975
  });
@@ -29112,20 +29152,6 @@ function withKernelOpts2(opts) {
29112
29152
  return opts;
29113
29153
  return { ...opts ?? {}, socket: sock };
29114
29154
  }
29115
- async function approvalConsume2(request_id, opts) {
29116
- const r = await rpcRaw({ v: 1, op: "approval_consume", request_id }, withKernelOpts2(opts));
29117
- if (r.kind !== "response" || !r.resp.ok)
29118
- return null;
29119
- if (!("consumed" in r.resp))
29120
- return null;
29121
- return {
29122
- consumed: r.resp.consumed,
29123
- agent_unit: r.resp.agent_unit,
29124
- scope: r.resp.scope,
29125
- action: r.resp.action,
29126
- why: r.resp.why ?? null
29127
- };
29128
- }
29129
29155
  async function approvalRevoke(decision_id, actor, reason, opts) {
29130
29156
  const r = await rpcRaw({ v: 1, op: "approval_revoke", decision_id, actor, reason }, withKernelOpts2(opts));
29131
29157
  if (r.kind !== "response" || !r.resp.ok)
@@ -29134,10 +29160,10 @@ async function approvalRevoke(decision_id, actor, reason, opts) {
29134
29160
  return null;
29135
29161
  return r.resp.revoked;
29136
29162
  }
29137
- async function approvalRecord2(args, opts) {
29163
+ async function approvalConsumeRecord(args, opts) {
29138
29164
  const r = await rpcRaw({
29139
29165
  v: 1,
29140
- op: "approval_record",
29166
+ op: "approval_consume_record",
29141
29167
  request_id: args.request_id,
29142
29168
  decision: args.decision,
29143
29169
  approver_set: args.approver_set,
@@ -29146,9 +29172,17 @@ async function approvalRecord2(args, opts) {
29146
29172
  }, withKernelOpts2(opts));
29147
29173
  if (r.kind !== "response" || !r.resp.ok)
29148
29174
  return null;
29149
- if (!("decision_id" in r.resp))
29175
+ if (!("consumed" in r.resp))
29150
29176
  return null;
29151
- return r.resp.decision_id;
29177
+ const resp = r.resp;
29178
+ return {
29179
+ consumed: resp.consumed,
29180
+ decision_id: resp.decision_id,
29181
+ agent_unit: resp.agent_unit,
29182
+ scope: resp.scope,
29183
+ action: resp.action,
29184
+ why: resp.why ?? null
29185
+ };
29152
29186
  }
29153
29187
  async function approvalList(agent_unit, opts) {
29154
29188
  const r = await rpcRaw({ v: 1, op: "approval_list", agent_unit }, withKernelOpts2(opts));
@@ -29595,9 +29629,26 @@ var init_approval_card = __esm(() => {
29595
29629
  // gateway/approval-callback.ts
29596
29630
  var exports_approval_callback = {};
29597
29631
  __export(exports_approval_callback, {
29632
+ resolveApprovalDecision: () => resolveApprovalDecision,
29598
29633
  handleApprovalCallback: () => handleApprovalCallback,
29599
29634
  buildGrantedKeyboard: () => buildGrantedKeyboard
29600
29635
  });
29636
+ function resolveApprovalDecision(choice) {
29637
+ switch (choice.kind) {
29638
+ case "deny":
29639
+ return { ok: true, decision: "deny", granted: false, ttl_ms: null, displayMode: "denied" };
29640
+ case "once":
29641
+ return { ok: true, decision: "allow_once", granted: true, ttl_ms: null, displayMode: "granted once" };
29642
+ case "always":
29643
+ return { ok: true, decision: "allow_always", granted: true, ttl_ms: null, displayMode: "granted always" };
29644
+ case "ttl": {
29645
+ const ms = ttlMsFromToken(choice.param);
29646
+ if (ms === null)
29647
+ return { ok: false, error: "bad ttl token" };
29648
+ return { ok: true, decision: "allow_ttl", granted: true, ttl_ms: ms, displayMode: `granted for ${choice.param}` };
29649
+ }
29650
+ }
29651
+ }
29601
29652
  function buildGrantedKeyboard(scope) {
29602
29653
  const btn = scopeToOpenInDriveButton(scope);
29603
29654
  if (btn === null)
@@ -29610,64 +29661,37 @@ async function handleApprovalCallback(ctx, data) {
29610
29661
  await ctx.answerCallbackQuery({ text: "malformed approval callback" });
29611
29662
  return;
29612
29663
  }
29613
- const consumed = await approvalConsume2(parsed.request_id);
29614
- if (consumed === null) {
29615
- await ctx.answerCallbackQuery({ text: "approval kernel unreachable" });
29616
- return;
29617
- }
29618
- if (!consumed.consumed) {
29619
- await ctx.answerCallbackQuery({ text: "this prompt expired" });
29664
+ const resolved = resolveApprovalDecision(parsed.choice);
29665
+ if (!resolved.ok) {
29666
+ await ctx.answerCallbackQuery({ text: resolved.error });
29620
29667
  return;
29621
29668
  }
29622
- let decision;
29623
- let granted;
29624
- let ttl_ms = null;
29625
- let displayMode;
29626
- switch (parsed.choice.kind) {
29627
- case "deny":
29628
- decision = "deny";
29629
- granted = false;
29630
- displayMode = "denied";
29631
- break;
29632
- case "once":
29633
- decision = "allow_once";
29634
- granted = true;
29635
- displayMode = "granted once";
29636
- break;
29637
- case "always":
29638
- decision = "allow_always";
29639
- granted = true;
29640
- displayMode = "granted always";
29641
- break;
29642
- case "ttl": {
29643
- decision = "allow_ttl";
29644
- granted = true;
29645
- const ms = ttlMsFromToken(parsed.choice.param);
29646
- if (ms === null) {
29647
- await ctx.answerCallbackQuery({ text: "bad ttl token" });
29648
- return;
29649
- }
29650
- ttl_ms = ms;
29651
- displayMode = `granted for ${parsed.choice.param}`;
29652
- break;
29653
- }
29654
- }
29669
+ const { decision, granted, ttl_ms, displayMode } = resolved;
29655
29670
  const granted_by_user_id = ctx.from?.id ?? 0;
29656
29671
  const approver_set = [String(granted_by_user_id)];
29657
- const decision_id = await approvalRecord2({
29672
+ const result = await approvalConsumeRecord({
29658
29673
  request_id: parsed.request_id,
29659
29674
  decision,
29660
29675
  approver_set,
29661
29676
  granted_by_user_id,
29662
29677
  ttl_ms
29663
29678
  });
29664
- if (decision_id === null) {
29679
+ if (result === null) {
29680
+ await ctx.answerCallbackQuery({ text: "approval kernel unreachable" });
29681
+ return;
29682
+ }
29683
+ if (!result.consumed) {
29684
+ await ctx.answerCallbackQuery({ text: "this prompt expired" });
29685
+ return;
29686
+ }
29687
+ if (!result.decision_id) {
29665
29688
  await ctx.answerCallbackQuery({ text: "kernel record failed" });
29666
29689
  return;
29667
29690
  }
29691
+ const decision_id = result.decision_id;
29668
29692
  const icon = granted ? "\u2705" : "\uD83D\uDEAB";
29669
29693
  const newBody = `${icon} ${displayMode}` + (granted ? ` \u00b7 /approvals revoke <code>${decision_id}</code>` : "");
29670
- const postTapKeyboard = granted && consumed.scope ? buildGrantedKeyboard(consumed.scope) : undefined;
29694
+ const postTapKeyboard = granted && result.scope ? buildGrantedKeyboard(result.scope) : undefined;
29671
29695
  try {
29672
29696
  await ctx.editMessageText(newBody, {
29673
29697
  parse_mode: "HTML",
@@ -41280,7 +41304,7 @@ async function approvalRecord(args, opts) {
41280
41304
  return null;
41281
41305
  if (!("decision_id" in r.resp))
41282
41306
  return null;
41283
- return r.resp.decision_id;
41307
+ return r.resp.decision_id ?? null;
41284
41308
  }
41285
41309
 
41286
41310
  // quota-check.ts
@@ -43322,6 +43346,26 @@ function escapeHtml7(s) {
43322
43346
 
43323
43347
  // gateway/pending-inbound-buffer.ts
43324
43348
  var DEFAULT_PENDING_INBOUND_CAP = 32;
43349
+ function redeliverBufferedInbound(buffer, agent, send) {
43350
+ const pending = buffer.drain(agent);
43351
+ let redelivered = 0;
43352
+ let rebuffered = 0;
43353
+ for (const msg of pending) {
43354
+ let delivered = false;
43355
+ try {
43356
+ delivered = send(msg);
43357
+ } catch {
43358
+ delivered = false;
43359
+ }
43360
+ if (delivered) {
43361
+ redelivered++;
43362
+ } else {
43363
+ buffer.push(agent, msg);
43364
+ rebuffered++;
43365
+ }
43366
+ }
43367
+ return { drained: pending.length, redelivered, rebuffered };
43368
+ }
43325
43369
  function createPendingInboundBuffer(opts = {}) {
43326
43370
  const cap = opts.capPerAgent ?? DEFAULT_PENDING_INBOUND_CAP;
43327
43371
  const log = opts.log ?? ((line) => process.stderr.write(line));
@@ -43366,6 +43410,52 @@ function createPendingInboundBuffer(opts = {}) {
43366
43410
  };
43367
43411
  }
43368
43412
 
43413
+ // gateway/pending-permission-decisions.ts
43414
+ var DEFAULT_PENDING_PERMISSION_CAP = 32;
43415
+ function createPendingPermissionBuffer(opts = {}) {
43416
+ const cap = opts.capPerAgent ?? DEFAULT_PENDING_PERMISSION_CAP;
43417
+ const log = opts.log ?? ((line) => process.stderr.write(line));
43418
+ const queues = new Map;
43419
+ return {
43420
+ push(agent, ev) {
43421
+ let q = queues.get(agent);
43422
+ if (q == null) {
43423
+ q = [];
43424
+ queues.set(agent, q);
43425
+ }
43426
+ let evicted = false;
43427
+ if (q.length >= cap) {
43428
+ const dropped = q.shift();
43429
+ evicted = true;
43430
+ log(`pending-permission-buffer: agent=${agent} cap=${cap} reached \u2014 ` + `dropped oldest verdict request=${dropped?.requestId ?? "-"} ` + `behavior=${dropped?.behavior ?? "-"}
43431
+ `);
43432
+ }
43433
+ q.push(ev);
43434
+ log(`pending-permission-buffer: agent=${agent} buffered request=${ev.requestId} ` + `behavior=${ev.behavior} depth_after=${q.length} evicted=${evicted}
43435
+ `);
43436
+ return !evicted;
43437
+ },
43438
+ drain(agent) {
43439
+ const q = queues.get(agent);
43440
+ if (q == null || q.length === 0)
43441
+ return [];
43442
+ queues.delete(agent);
43443
+ log(`pending-permission-buffer: drained agent=${agent} count=${q.length} ` + `requests=[${q.map((e) => e.requestId).join(",")}]
43444
+ `);
43445
+ return q;
43446
+ },
43447
+ depth(agent) {
43448
+ return queues.get(agent)?.length ?? 0;
43449
+ },
43450
+ totalDepth() {
43451
+ let n = 0;
43452
+ for (const q of queues.values())
43453
+ n += q.length;
43454
+ return n;
43455
+ }
43456
+ };
43457
+ }
43458
+
43369
43459
  // gateway/vault-grant-inbound-builders.ts
43370
43460
  function buildVaultGrantApprovedInbound(opts) {
43371
43461
  const ts = opts.nowMs ?? Date.now();
@@ -43409,6 +43499,63 @@ function buildVaultGrantDeniedInbound(opts) {
43409
43499
  }
43410
43500
  };
43411
43501
  }
43502
+ function buildVaultSaveCompletedInbound(opts) {
43503
+ const ts = opts.nowMs ?? Date.now();
43504
+ return {
43505
+ type: "inbound",
43506
+ chatId: opts.ctx.chat_id,
43507
+ messageId: ts,
43508
+ user: "vault-broker",
43509
+ userId: 0,
43510
+ ts,
43511
+ text: `\u2705 Operator saved your secret as \`vault:${opts.ctx.key}\`. ` + `Please resume the task that was waiting on it \u2014 reference the ` + `value via the usual \`vault:${opts.ctx.key}\` path.`,
43512
+ meta: {
43513
+ source: "vault_save_completed",
43514
+ agent: opts.ctx.agent,
43515
+ key: opts.ctx.key,
43516
+ stage_id: opts.stageId,
43517
+ operator_id: opts.operatorId
43518
+ }
43519
+ };
43520
+ }
43521
+ function buildVaultSaveFailedInbound(opts) {
43522
+ const ts = opts.nowMs ?? Date.now();
43523
+ return {
43524
+ type: "inbound",
43525
+ chatId: opts.ctx.chat_id,
43526
+ messageId: ts,
43527
+ user: "vault-broker",
43528
+ userId: 0,
43529
+ ts,
43530
+ text: `\u26a0\ufe0f The operator tapped Save but the vault write for ` + `\`vault:${opts.ctx.key}\` FAILED (${opts.reason}). The secret was ` + `NOT stored \u2014 do NOT assume \`vault:${opts.ctx.key}\` resolves. ` + `Either retry the save (the operator may need to fix the underlying ` + `issue first) or pick a fallback for the original task.`,
43531
+ meta: {
43532
+ source: "vault_save_failed",
43533
+ agent: opts.ctx.agent,
43534
+ key: opts.ctx.key,
43535
+ stage_id: opts.stageId,
43536
+ operator_id: opts.operatorId
43537
+ }
43538
+ };
43539
+ }
43540
+ function buildVaultSaveDiscardedInbound(opts) {
43541
+ const ts = opts.nowMs ?? Date.now();
43542
+ return {
43543
+ type: "inbound",
43544
+ chatId: opts.ctx.chat_id,
43545
+ messageId: ts,
43546
+ user: "vault-broker",
43547
+ userId: 0,
43548
+ ts,
43549
+ text: `\uD83D\uDEAB Operator discarded your \`vault_request_save\` for ` + `\`${opts.ctx.key}\` \u2014 the secret was NOT stored and ` + `\`vault:${opts.ctx.key}\` will not resolve. Pick a fallback for ` + `the original task (ask the user, try another approach, or skip ` + `the feature). Do NOT re-request the save without asking the user.`,
43550
+ meta: {
43551
+ source: "vault_save_discarded",
43552
+ agent: opts.ctx.agent,
43553
+ key: opts.ctx.key,
43554
+ stage_id: opts.stageId,
43555
+ operator_id: opts.operatorId
43556
+ }
43557
+ };
43558
+ }
43412
43559
 
43413
43560
  // gateway/poll-health.ts
43414
43561
  var DEFAULT_LOG = (msg) => {
@@ -46621,11 +46768,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
46621
46768
  }
46622
46769
 
46623
46770
  // ../src/build-info.ts
46624
- var VERSION = "0.12.13";
46625
- var COMMIT_SHA = "fc96015d";
46626
- var COMMIT_DATE = "2026-05-19T02:10:56Z";
46627
- var LATEST_PR = 1532;
46628
- var COMMITS_AHEAD_OF_TAG = 8;
46771
+ var VERSION = "0.12.15";
46772
+ var COMMIT_SHA = "dc508a92";
46773
+ var COMMIT_DATE = "2026-05-19T07:24:41Z";
46774
+ var LATEST_PR = 1547;
46775
+ var COMMITS_AHEAD_OF_TAG = 22;
46629
46776
 
46630
46777
  // gateway/boot-version.ts
46631
46778
  function formatRelativeAgo(iso) {
@@ -47995,8 +48142,12 @@ var pendingStateReaper = setInterval(() => {
47995
48142
  pendingVaultOps.delete(k);
47996
48143
  }
47997
48144
  for (const [k, v] of pendingPermissions) {
47998
- if (now - v.startedAt > PERMISSION_TTL_MS)
48145
+ if (now - v.startedAt > PERMISSION_TTL_MS) {
48146
+ dispatchPermissionVerdict({ type: "permission", requestId: k, behavior: "deny" });
48147
+ process.stderr.write(`telegram gateway: permission TTL expired \u2014 auto-deny request=${k} tool=${v.tool_name} (no operator response in ${Math.round(PERMISSION_TTL_MS / 60000)}m)
48148
+ `);
47999
48149
  pendingPermissions.delete(k);
48150
+ }
48000
48151
  }
48001
48152
  for (const [k, v] of vaultPassphraseCache) {
48002
48153
  if (now > v.expiresAt)
@@ -48311,11 +48462,23 @@ startTimer({
48311
48462
  try {
48312
48463
  clearSilentEndState(fbKey);
48313
48464
  } catch {}
48314
- process.stderr.write(`telegram gateway: silence-poke framework-fallback ended wedged turn chat=${fbChatId} thread=${ctx.threadId ?? "-"} silence_ms=${ctx.silenceMs} currentTurn_nulled=${turnMatchesFallback}
48465
+ const fbSelfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
48466
+ const fbRedeliver = redeliverBufferedInbound(pendingInboundBuffer, fbSelfAgent, (m) => ipcServer.sendToAgent(fbSelfAgent, m));
48467
+ process.stderr.write(`telegram gateway: silence-poke framework-fallback ended wedged turn chat=${fbChatId} thread=${ctx.threadId ?? "-"} silence_ms=${ctx.silenceMs} currentTurn_nulled=${turnMatchesFallback} drained_buffered=${fbRedeliver.redelivered}/${fbRedeliver.drained}${fbRedeliver.rebuffered > 0 ? ` rebuffered=${fbRedeliver.rebuffered}` : ""}
48315
48468
  `);
48316
48469
  }
48317
48470
  });
48318
48471
  var pendingInboundBuffer = createPendingInboundBuffer();
48472
+ var pendingPermissionBuffer = createPendingPermissionBuffer();
48473
+ function dispatchPermissionVerdict(ev) {
48474
+ const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
48475
+ const delivered = ipcServer.sendToAgent(selfAgent, ev);
48476
+ if (!delivered) {
48477
+ pendingPermissionBuffer.push(selfAgent, ev);
48478
+ process.stderr.write(`telegram gateway: permission verdict buffered (bridge offline) request=${ev.requestId} behavior=${ev.behavior}
48479
+ `);
48480
+ }
48481
+ }
48319
48482
  var ipcServer = createIpcServer({
48320
48483
  socketPath: SOCKET_PATH,
48321
48484
  onClientRegistered(client3) {
@@ -48329,6 +48492,15 @@ var ipcServer = createIpcServer({
48329
48492
  client3.send(msg);
48330
48493
  } catch (err) {
48331
48494
  process.stderr.write(`telegram gateway: pending-inbound drain failed agent=${client3.agentName} source=${msg.meta?.source ?? "-"}: ${err.message}
48495
+ `);
48496
+ }
48497
+ }
48498
+ const pendingVerdicts = pendingPermissionBuffer.drain(client3.agentName);
48499
+ for (const ev of pendingVerdicts) {
48500
+ try {
48501
+ client3.send(ev);
48502
+ } catch (err) {
48503
+ process.stderr.write(`telegram gateway: pending-permission drain failed agent=${client3.agentName} request=${ev.requestId} behavior=${ev.behavior}: ${err.message}
48332
48504
  `);
48333
48505
  }
48334
48506
  }
@@ -50646,7 +50818,7 @@ async function handleInbound(ctx, text, downloadImage, attachment) {
50646
50818
  if (permMatch) {
50647
50819
  const behavior = permMatch[1].toLowerCase().startsWith("y") ? "allow" : "deny";
50648
50820
  const request_id = permMatch[2].toLowerCase();
50649
- ipcServer.broadcast({
50821
+ dispatchPermissionVerdict({
50650
50822
  type: "permission",
50651
50823
  requestId: request_id,
50652
50824
  behavior
@@ -51121,11 +51293,12 @@ ${preBlock(write.output)}`;
51121
51293
  } : {}
51122
51294
  }
51123
51295
  };
51124
- ipcServer.broadcast(inboundMsg);
51125
- const delivered = ipcServer.clientCount() > 0;
51296
+ const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? "";
51297
+ const delivered = ipcServer.sendToAgent(selfAgent, inboundMsg);
51126
51298
  if (!delivered) {
51299
+ pendingInboundBuffer.push(selfAgent, inboundMsg);
51127
51300
  const threadOpts = messageThreadId != null ? { message_thread_id: messageThreadId } : {};
51128
- swallowingApiCall(() => bot.api.sendMessage(chat_id, "\u23F3 Agent is restarting, please wait\u2026", { ...threadOpts }), {
51301
+ swallowingApiCall(() => bot.api.sendMessage(chat_id, "\u23F3 Agent is restarting \u2014 your message is queued and will be processed when it reconnects.", { ...threadOpts }), {
51129
51302
  chat_id,
51130
51303
  verb: "agent-restarting-notice",
51131
51304
  ...messageThreadId != null ? { threadId: messageThreadId } : {}
@@ -52298,7 +52471,7 @@ async function handlePermissionSlash(ctx, behavior) {
52298
52471
  await switchroomReply(ctx, `No pending permission for id <code>${escapeHtmlForTg(request_id)}</code>. It may have already been answered or timed out.`, { html: true });
52299
52472
  return;
52300
52473
  }
52301
- ipcServer.broadcast({ type: "permission", requestId: request_id, behavior });
52474
+ dispatchPermissionVerdict({ type: "permission", requestId: request_id, behavior });
52302
52475
  pendingPermissions.delete(request_id);
52303
52476
  process.stderr.write(`[telegram gateway] slash-${behavior} request_id=${request_id} tool=${details.tool_name} by=${senderId}
52304
52477
  `);
@@ -52893,6 +53066,16 @@ async function handleVaultRequestSaveCallback(ctx, data) {
52893
53066
  if (pending.card_message_id != null) {
52894
53067
  await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, `\uD83D\uDEAB <i>Discarded. The secret was not written to the vault.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
52895
53068
  }
53069
+ const discardInbound = buildVaultSaveDiscardedInbound({
53070
+ ctx: { agent: pending.agent, key: pending.key, chat_id: pending.chat_id },
53071
+ stageId,
53072
+ operatorId: senderId
53073
+ });
53074
+ const dDelivered = ipcServer.sendToAgent(pending.agent, discardInbound);
53075
+ process.stderr.write(`telegram gateway: vault_save_discarded injection agent=${pending.agent} key=${pending.key} stage=${stageId} delivered=${dDelivered}
53076
+ `);
53077
+ if (!dDelivered)
53078
+ pendingInboundBuffer.push(pending.agent, discardInbound);
52896
53079
  return;
52897
53080
  }
52898
53081
  if (action === "rename") {
@@ -52935,6 +53118,19 @@ async function handleVaultRequestSaveCallback(ctx, data) {
52935
53118
  <i>Tap a fresh card after fixing the underlying issue.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
52936
53119
  }
52937
53120
  pendingVaultRequestSaves.delete(stageId);
53121
+ const failReason = (write.output || "vault write error").split(`
53122
+ `)[0].slice(0, 200);
53123
+ const failInbound = buildVaultSaveFailedInbound({
53124
+ ctx: { agent: pending.agent, key: pending.key, chat_id: pending.chat_id },
53125
+ stageId,
53126
+ operatorId: senderId,
53127
+ reason: failReason
53128
+ });
53129
+ const fDelivered = ipcServer.sendToAgent(pending.agent, failInbound);
53130
+ process.stderr.write(`telegram gateway: vault_save_failed injection agent=${pending.agent} key=${pending.key} stage=${stageId} delivered=${fDelivered}
53131
+ `);
53132
+ if (!fDelivered)
53133
+ pendingInboundBuffer.push(pending.agent, failInbound);
52938
53134
  return;
52939
53135
  }
52940
53136
  pendingVaultRequestSaves.delete(stageId);
@@ -52942,6 +53138,16 @@ async function handleVaultRequestSaveCallback(ctx, data) {
52942
53138
  await ctx.api.editMessageText(pending.chat_id, pending.card_message_id, `\u2705 saved as <code>vault:${escapeHtmlForTg(pending.key)}</code> (masked: <code>${escapeHtmlForTg(maskToken2(pending.value))}</code>)
52943
53139
  <i>The agent can now reference this as <code>vault:${escapeHtmlForTg(pending.key)}</code>.</i>`, { parse_mode: "HTML", reply_markup: { inline_keyboard: [] } }).catch(() => {});
52944
53140
  }
53141
+ const okInbound = buildVaultSaveCompletedInbound({
53142
+ ctx: { agent: pending.agent, key: pending.key, chat_id: pending.chat_id },
53143
+ stageId,
53144
+ operatorId: senderId
53145
+ });
53146
+ const okDelivered = ipcServer.sendToAgent(pending.agent, okInbound);
53147
+ process.stderr.write(`telegram gateway: vault_save_completed injection agent=${pending.agent} key=${pending.key} stage=${stageId} delivered=${okDelivered}
53148
+ `);
53149
+ if (!okDelivered)
53150
+ pendingInboundBuffer.push(pending.agent, okInbound);
52945
53151
  return;
52946
53152
  }
52947
53153
  await ctx.answerCallbackQuery({ text: "Unknown action" }).catch(() => {});
@@ -54348,9 +54554,11 @@ ${preBlock(formatSwitchroomOutput(err.message ?? "unknown error"))}`, { html: tr
54348
54554
  };
54349
54555
  process.stderr.write(`telegram gateway: button_callback chatId=${cbChatId} user=${ctx.from.id} data=${JSON.stringify(agentCb.raw)} btnText=${JSON.stringify(buttonText ?? null)}
54350
54556
  `);
54351
- ipcServer.broadcast(inboundMsg);
54352
- if (ipcServer.clientCount() === 0) {
54353
- swallowingApiCall(() => bot.api.sendMessage(cbChatId, "\u23F3 Agent is restarting \u2014 your button tap was queued but won't be processed until it comes back.", cbThreadId != null ? { message_thread_id: cbThreadId } : {}), {
54557
+ const selfAgentBtn = process.env.SWITCHROOM_AGENT_NAME ?? "";
54558
+ const btnDelivered = ipcServer.sendToAgent(selfAgentBtn, inboundMsg);
54559
+ if (!btnDelivered) {
54560
+ pendingInboundBuffer.push(selfAgentBtn, inboundMsg);
54561
+ swallowingApiCall(() => bot.api.sendMessage(cbChatId, "\u23F3 Agent is restarting \u2014 your button tap is queued and will be processed when it comes back.", cbThreadId != null ? { message_thread_id: cbThreadId } : {}), {
54354
54562
  chat_id: cbChatId,
54355
54563
  verb: "button-tap-restarting-notice",
54356
54564
  ...cbThreadId != null ? { threadId: cbThreadId } : {}
@@ -54445,7 +54653,7 @@ ${prettyInput}`;
54445
54653
  ${editLabel}` : editLabel,
54446
54654
  parseMode: "HTML",
54447
54655
  synthInbound: () => {
54448
- ipcServer.broadcast({
54656
+ dispatchPermissionVerdict({
54449
54657
  type: "permission",
54450
54658
  requestId: request_id,
54451
54659
  behavior: "allow",
@@ -54466,7 +54674,7 @@ ${editLabel}` : editLabel,
54466
54674
  ${label}` : label,
54467
54675
  parseMode: "HTML",
54468
54676
  synthInbound: () => {
54469
- ipcServer.broadcast({
54677
+ dispatchPermissionVerdict({
54470
54678
  type: "permission",
54471
54679
  requestId: request_id,
54472
54680
  behavior
@@ -12,7 +12,10 @@
12
12
 
13
13
  import { describe, expect, it } from "vitest";
14
14
  import { InlineKeyboard } from "grammy";
15
- import { buildGrantedKeyboard } from "./approval-callback.js";
15
+ import {
16
+ buildGrantedKeyboard,
17
+ resolveApprovalDecision,
18
+ } from "./approval-callback.js";
16
19
 
17
20
  /**
18
21
  * Helper — pull the `[{text, url}]` rows out of a grammy InlineKeyboard
@@ -102,3 +105,48 @@ describe("buildGrantedKeyboard — no button cases", () => {
102
105
  expect(buildGrantedKeyboard("doc:gdrive:write:abc?evil=1")).toBeUndefined();
103
106
  });
104
107
  });
108
+
109
+ describe("resolveApprovalDecision — pure decision resolution (PR-5)", () => {
110
+ it("deny → deny / not granted", () => {
111
+ expect(resolveApprovalDecision({ kind: "deny" })).toEqual({
112
+ ok: true, decision: "deny", granted: false, ttl_ms: null, displayMode: "denied",
113
+ });
114
+ });
115
+
116
+ it("once → allow_once, no ttl", () => {
117
+ expect(resolveApprovalDecision({ kind: "once" })).toEqual({
118
+ ok: true, decision: "allow_once", granted: true, ttl_ms: null, displayMode: "granted once",
119
+ });
120
+ });
121
+
122
+ it("always → allow_always, no ttl", () => {
123
+ expect(resolveApprovalDecision({ kind: "always" })).toEqual({
124
+ ok: true, decision: "allow_always", granted: true, ttl_ms: null, displayMode: "granted always",
125
+ });
126
+ });
127
+
128
+ it("ttl with a valid token → allow_ttl with computed ms", () => {
129
+ expect(resolveApprovalDecision({ kind: "ttl", param: "24h" })).toEqual({
130
+ ok: true,
131
+ decision: "allow_ttl",
132
+ granted: true,
133
+ ttl_ms: 24 * 60 * 60 * 1000,
134
+ displayMode: "granted for 24h",
135
+ });
136
+ expect(resolveApprovalDecision({ kind: "ttl", param: "7d" })).toMatchObject({
137
+ ok: true, decision: "allow_ttl", ttl_ms: 7 * 24 * 60 * 60 * 1000,
138
+ });
139
+ });
140
+
141
+ it("ttl with a bad token → { ok: false } so the caller does NOT consume the nonce", () => {
142
+ // This is the load-bearing case: pre-PR-4 this branch ran after
143
+ // approvalConsume() burned the single-use nonce, wedging the agent.
144
+ // It must report failure WITHOUT side effects (pure fn → caller
145
+ // returns before approvalConsume).
146
+ for (const param of ["bogus", "0h", "1w", "", "h", "12"]) {
147
+ expect(resolveApprovalDecision({ kind: "ttl", param })).toEqual({
148
+ ok: false, error: "bad ttl token",
149
+ });
150
+ }
151
+ });
152
+ });