switchroom 0.12.14 → 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.
@@ -11788,6 +11788,15 @@ var ApprovalRecordRequestSchema = exports_external.object({
11788
11788
  granted_by_user_id: exports_external.number().int(),
11789
11789
  ttl_ms: exports_external.number().int().positive().nullable().optional()
11790
11790
  });
11791
+ var ApprovalConsumeRecordRequestSchema = exports_external.object({
11792
+ v: exports_external.literal(1),
11793
+ op: exports_external.literal("approval_consume_record"),
11794
+ request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
11795
+ decision: ApprovalDecisionModeSchema,
11796
+ approver_set: exports_external.array(exports_external.string()),
11797
+ granted_by_user_id: exports_external.number().int(),
11798
+ ttl_ms: exports_external.number().int().positive().nullable().optional()
11799
+ });
11791
11800
  var RequestSchema = exports_external.discriminatedUnion("op", [
11792
11801
  GetRequestSchema,
11793
11802
  PutRequestSchema,
@@ -11803,7 +11812,8 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
11803
11812
  ApprovalConsumeRequestSchema,
11804
11813
  ApprovalRevokeRequestSchema,
11805
11814
  ApprovalListRequestSchema,
11806
- ApprovalRecordRequestSchema
11815
+ ApprovalRecordRequestSchema,
11816
+ ApprovalConsumeRecordRequestSchema
11807
11817
  ]);
11808
11818
  var VaultEntrySchema = exports_external.union([
11809
11819
  exports_external.object({ kind: exports_external.literal("string"), value: exports_external.string() }),
@@ -11925,6 +11935,15 @@ var OkApprovalRecordResponseSchema = exports_external.object({
11925
11935
  ok: exports_external.literal(true),
11926
11936
  decision_id: exports_external.string()
11927
11937
  });
11938
+ var OkApprovalConsumeRecordResponseSchema = exports_external.object({
11939
+ ok: exports_external.literal(true),
11940
+ consumed: exports_external.boolean(),
11941
+ decision_id: exports_external.string().optional(),
11942
+ agent_unit: exports_external.string().optional(),
11943
+ scope: exports_external.string().optional(),
11944
+ action: exports_external.string().optional(),
11945
+ why: exports_external.string().nullable().optional()
11946
+ });
11928
11947
  var ErrorResponseSchema = exports_external.object({
11929
11948
  ok: exports_external.literal(false),
11930
11949
  code: ErrorCode,
@@ -11946,6 +11965,7 @@ var ResponseSchema = exports_external.union([
11946
11965
  OkApprovalRevokeResponseSchema,
11947
11966
  OkApprovalListResponseSchema,
11948
11967
  OkApprovalRecordResponseSchema,
11968
+ OkApprovalConsumeRecordResponseSchema,
11949
11969
  ErrorResponseSchema
11950
11970
  ]);
11951
11971
  function decodeRequest(line) {
@@ -12264,6 +12284,22 @@ function recordDecision(db, input, now = Date.now()) {
12264
12284
  });
12265
12285
  return id;
12266
12286
  }
12287
+ function consumeAndRecord(db, input, now = Date.now()) {
12288
+ const run = db.transaction(() => {
12289
+ const nonce = consumeNonce(db, input.request_id, now);
12290
+ if (nonce === null)
12291
+ return { consumed: false };
12292
+ const decision_id = recordDecision(db, {
12293
+ nonce,
12294
+ decision: input.decision,
12295
+ approver_set: input.approver_set,
12296
+ granted_by_user_id: input.granted_by_user_id,
12297
+ ttl_ms: input.ttl_ms
12298
+ }, now);
12299
+ return { consumed: true, decision_id, nonce };
12300
+ });
12301
+ return run();
12302
+ }
12267
12303
  function revokeDecision(db, decision_id, actor, reason, now = Date.now()) {
12268
12304
  const row = db.query(`SELECT * FROM approval_decisions WHERE id = ?`).get(decision_id);
12269
12305
  if (!row)
@@ -12973,6 +13009,37 @@ function handleRequest(socket, req, agent, db, peerUid, isOperator = false) {
12973
13009
  socket.write(encodeResponse({ ok: true, decision_id }));
12974
13010
  return;
12975
13011
  }
13012
+ if (req.op === "approval_consume_record") {
13013
+ const peek = getNonce(db, req.request_id);
13014
+ if (peek !== null) {
13015
+ const acl = checkApprovalAclByAgent(agent, peek.agent_unit);
13016
+ if (!acl.allow) {
13017
+ socket.write(encodeResponse(errorResponse("DENIED", "approval_consume_record denied: nonce does not belong to the calling agent")));
13018
+ return;
13019
+ }
13020
+ }
13021
+ const res = consumeAndRecord(db, {
13022
+ request_id: req.request_id,
13023
+ decision: req.decision,
13024
+ approver_set: req.approver_set,
13025
+ granted_by_user_id: req.granted_by_user_id,
13026
+ ttl_ms: req.ttl_ms ?? undefined
13027
+ });
13028
+ if (!res.consumed) {
13029
+ socket.write(encodeResponse({ ok: true, consumed: false }));
13030
+ return;
13031
+ }
13032
+ socket.write(encodeResponse({
13033
+ ok: true,
13034
+ consumed: true,
13035
+ decision_id: res.decision_id,
13036
+ agent_unit: res.nonce.agent_unit,
13037
+ scope: res.nonce.scope,
13038
+ action: res.nonce.action,
13039
+ why: res.nonce.why
13040
+ }));
13041
+ return;
13042
+ }
12976
13043
  if (req.op === "approval_list") {
12977
13044
  const decisions = listDecisions(db, { agent_unit: req.agent_unit });
12978
13045
  const meta = decisions.map((d) => ({
@@ -13216,6 +13216,15 @@ var ApprovalRecordRequestSchema = exports_external.object({
13216
13216
  granted_by_user_id: exports_external.number().int(),
13217
13217
  ttl_ms: exports_external.number().int().positive().nullable().optional()
13218
13218
  });
13219
+ var ApprovalConsumeRecordRequestSchema = exports_external.object({
13220
+ v: exports_external.literal(1),
13221
+ op: exports_external.literal("approval_consume_record"),
13222
+ request_id: exports_external.string().regex(/^[0-9a-f]{32}$/),
13223
+ decision: ApprovalDecisionModeSchema,
13224
+ approver_set: exports_external.array(exports_external.string()),
13225
+ granted_by_user_id: exports_external.number().int(),
13226
+ ttl_ms: exports_external.number().int().positive().nullable().optional()
13227
+ });
13219
13228
  var RequestSchema = exports_external.discriminatedUnion("op", [
13220
13229
  GetRequestSchema,
13221
13230
  PutRequestSchema,
@@ -13231,7 +13240,8 @@ var RequestSchema = exports_external.discriminatedUnion("op", [
13231
13240
  ApprovalConsumeRequestSchema,
13232
13241
  ApprovalRevokeRequestSchema,
13233
13242
  ApprovalListRequestSchema,
13234
- ApprovalRecordRequestSchema
13243
+ ApprovalRecordRequestSchema,
13244
+ ApprovalConsumeRecordRequestSchema
13235
13245
  ]);
13236
13246
  var VaultEntrySchema = exports_external.union([
13237
13247
  exports_external.object({ kind: exports_external.literal("string"), value: exports_external.string() }),
@@ -13353,6 +13363,15 @@ var OkApprovalRecordResponseSchema = exports_external.object({
13353
13363
  ok: exports_external.literal(true),
13354
13364
  decision_id: exports_external.string()
13355
13365
  });
13366
+ var OkApprovalConsumeRecordResponseSchema = exports_external.object({
13367
+ ok: exports_external.literal(true),
13368
+ consumed: exports_external.boolean(),
13369
+ decision_id: exports_external.string().optional(),
13370
+ agent_unit: exports_external.string().optional(),
13371
+ scope: exports_external.string().optional(),
13372
+ action: exports_external.string().optional(),
13373
+ why: exports_external.string().nullable().optional()
13374
+ });
13356
13375
  var ErrorResponseSchema = exports_external.object({
13357
13376
  ok: exports_external.literal(false),
13358
13377
  code: ErrorCode,
@@ -13374,6 +13393,7 @@ var ResponseSchema = exports_external.union([
13374
13393
  OkApprovalRevokeResponseSchema,
13375
13394
  OkApprovalListResponseSchema,
13376
13395
  OkApprovalRecordResponseSchema,
13396
+ OkApprovalConsumeRecordResponseSchema,
13377
13397
  ErrorResponseSchema
13378
13398
  ]);
13379
13399
  function decodeRequest(line) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "switchroom",
3
- "version": "0.12.14",
3
+ "version": "0.12.15",
4
4
  "description": "Run Claude Code 24/7 on your Claude Pro/Max subscription over Telegram. Open-source alternative to OpenClaw and NanoClaw — no API keys.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -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, PreflightAccessRequestSchema, OkPreflightAccessResponseSchema, 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;
@@ -24792,6 +24792,15 @@ var init_protocol2 = __esm(() => {
24792
24792
  granted_by_user_id: exports_external.number().int(),
24793
24793
  ttl_ms: exports_external.number().int().positive().nullable().optional()
24794
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
+ });
24795
24804
  RequestSchema2 = exports_external.discriminatedUnion("op", [
24796
24805
  GetRequestSchema,
24797
24806
  PutRequestSchema,
@@ -24807,7 +24816,8 @@ var init_protocol2 = __esm(() => {
24807
24816
  ApprovalConsumeRequestSchema,
24808
24817
  ApprovalRevokeRequestSchema,
24809
24818
  ApprovalListRequestSchema,
24810
- ApprovalRecordRequestSchema
24819
+ ApprovalRecordRequestSchema,
24820
+ ApprovalConsumeRecordRequestSchema
24811
24821
  ]);
24812
24822
  VaultEntrySchema = exports_external.union([
24813
24823
  exports_external.object({ kind: exports_external.literal("string"), value: exports_external.string() }),
@@ -24929,6 +24939,15 @@ var init_protocol2 = __esm(() => {
24929
24939
  ok: exports_external.literal(true),
24930
24940
  decision_id: exports_external.string()
24931
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
+ });
24932
24951
  ErrorResponseSchema2 = exports_external.object({
24933
24952
  ok: exports_external.literal(false),
24934
24953
  code: ErrorCode,
@@ -24950,6 +24969,7 @@ var init_protocol2 = __esm(() => {
24950
24969
  OkApprovalRevokeResponseSchema,
24951
24970
  OkApprovalListResponseSchema,
24952
24971
  OkApprovalRecordResponseSchema,
24972
+ OkApprovalConsumeRecordResponseSchema,
24953
24973
  ErrorResponseSchema2
24954
24974
  ]);
24955
24975
  });
@@ -29132,20 +29152,6 @@ function withKernelOpts2(opts) {
29132
29152
  return opts;
29133
29153
  return { ...opts ?? {}, socket: sock };
29134
29154
  }
29135
- async function approvalConsume2(request_id, opts) {
29136
- const r = await rpcRaw({ v: 1, op: "approval_consume", request_id }, withKernelOpts2(opts));
29137
- if (r.kind !== "response" || !r.resp.ok)
29138
- return null;
29139
- if (!("consumed" in r.resp))
29140
- return null;
29141
- return {
29142
- consumed: r.resp.consumed,
29143
- agent_unit: r.resp.agent_unit,
29144
- scope: r.resp.scope,
29145
- action: r.resp.action,
29146
- why: r.resp.why ?? null
29147
- };
29148
- }
29149
29155
  async function approvalRevoke(decision_id, actor, reason, opts) {
29150
29156
  const r = await rpcRaw({ v: 1, op: "approval_revoke", decision_id, actor, reason }, withKernelOpts2(opts));
29151
29157
  if (r.kind !== "response" || !r.resp.ok)
@@ -29154,10 +29160,10 @@ async function approvalRevoke(decision_id, actor, reason, opts) {
29154
29160
  return null;
29155
29161
  return r.resp.revoked;
29156
29162
  }
29157
- async function approvalRecord2(args, opts) {
29163
+ async function approvalConsumeRecord(args, opts) {
29158
29164
  const r = await rpcRaw({
29159
29165
  v: 1,
29160
- op: "approval_record",
29166
+ op: "approval_consume_record",
29161
29167
  request_id: args.request_id,
29162
29168
  decision: args.decision,
29163
29169
  approver_set: args.approver_set,
@@ -29166,9 +29172,17 @@ async function approvalRecord2(args, opts) {
29166
29172
  }, withKernelOpts2(opts));
29167
29173
  if (r.kind !== "response" || !r.resp.ok)
29168
29174
  return null;
29169
- if (!("decision_id" in r.resp))
29175
+ if (!("consumed" in r.resp))
29170
29176
  return null;
29171
- 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
+ };
29172
29186
  }
29173
29187
  async function approvalList(agent_unit, opts) {
29174
29188
  const r = await rpcRaw({ v: 1, op: "approval_list", agent_unit }, withKernelOpts2(opts));
@@ -29615,9 +29629,26 @@ var init_approval_card = __esm(() => {
29615
29629
  // gateway/approval-callback.ts
29616
29630
  var exports_approval_callback = {};
29617
29631
  __export(exports_approval_callback, {
29632
+ resolveApprovalDecision: () => resolveApprovalDecision,
29618
29633
  handleApprovalCallback: () => handleApprovalCallback,
29619
29634
  buildGrantedKeyboard: () => buildGrantedKeyboard
29620
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
+ }
29621
29652
  function buildGrantedKeyboard(scope) {
29622
29653
  const btn = scopeToOpenInDriveButton(scope);
29623
29654
  if (btn === null)
@@ -29630,64 +29661,37 @@ async function handleApprovalCallback(ctx, data) {
29630
29661
  await ctx.answerCallbackQuery({ text: "malformed approval callback" });
29631
29662
  return;
29632
29663
  }
29633
- let decision;
29634
- let granted;
29635
- let ttl_ms = null;
29636
- let displayMode;
29637
- switch (parsed.choice.kind) {
29638
- case "deny":
29639
- decision = "deny";
29640
- granted = false;
29641
- displayMode = "denied";
29642
- break;
29643
- case "once":
29644
- decision = "allow_once";
29645
- granted = true;
29646
- displayMode = "granted once";
29647
- break;
29648
- case "always":
29649
- decision = "allow_always";
29650
- granted = true;
29651
- displayMode = "granted always";
29652
- break;
29653
- case "ttl": {
29654
- decision = "allow_ttl";
29655
- granted = true;
29656
- const ms = ttlMsFromToken(parsed.choice.param);
29657
- if (ms === null) {
29658
- await ctx.answerCallbackQuery({ text: "bad ttl token" });
29659
- return;
29660
- }
29661
- ttl_ms = ms;
29662
- displayMode = `granted for ${parsed.choice.param}`;
29663
- break;
29664
- }
29665
- }
29666
- const consumed = await approvalConsume2(parsed.request_id);
29667
- if (consumed === null) {
29668
- await ctx.answerCallbackQuery({ text: "approval kernel unreachable" });
29669
- return;
29670
- }
29671
- if (!consumed.consumed) {
29672
- await ctx.answerCallbackQuery({ text: "this prompt expired" });
29664
+ const resolved = resolveApprovalDecision(parsed.choice);
29665
+ if (!resolved.ok) {
29666
+ await ctx.answerCallbackQuery({ text: resolved.error });
29673
29667
  return;
29674
29668
  }
29669
+ const { decision, granted, ttl_ms, displayMode } = resolved;
29675
29670
  const granted_by_user_id = ctx.from?.id ?? 0;
29676
29671
  const approver_set = [String(granted_by_user_id)];
29677
- const decision_id = await approvalRecord2({
29672
+ const result = await approvalConsumeRecord({
29678
29673
  request_id: parsed.request_id,
29679
29674
  decision,
29680
29675
  approver_set,
29681
29676
  granted_by_user_id,
29682
29677
  ttl_ms
29683
29678
  });
29684
- 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) {
29685
29688
  await ctx.answerCallbackQuery({ text: "kernel record failed" });
29686
29689
  return;
29687
29690
  }
29691
+ const decision_id = result.decision_id;
29688
29692
  const icon = granted ? "\u2705" : "\uD83D\uDEAB";
29689
29693
  const newBody = `${icon} ${displayMode}` + (granted ? ` \u00b7 /approvals revoke <code>${decision_id}</code>` : "");
29690
- const postTapKeyboard = granted && consumed.scope ? buildGrantedKeyboard(consumed.scope) : undefined;
29694
+ const postTapKeyboard = granted && result.scope ? buildGrantedKeyboard(result.scope) : undefined;
29691
29695
  try {
29692
29696
  await ctx.editMessageText(newBody, {
29693
29697
  parse_mode: "HTML",
@@ -41300,7 +41304,7 @@ async function approvalRecord(args, opts) {
41300
41304
  return null;
41301
41305
  if (!("decision_id" in r.resp))
41302
41306
  return null;
41303
- return r.resp.decision_id;
41307
+ return r.resp.decision_id ?? null;
41304
41308
  }
41305
41309
 
41306
41310
  // quota-check.ts
@@ -43342,6 +43346,26 @@ function escapeHtml7(s) {
43342
43346
 
43343
43347
  // gateway/pending-inbound-buffer.ts
43344
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
+ }
43345
43369
  function createPendingInboundBuffer(opts = {}) {
43346
43370
  const cap = opts.capPerAgent ?? DEFAULT_PENDING_INBOUND_CAP;
43347
43371
  const log = opts.log ?? ((line) => process.stderr.write(line));
@@ -46744,11 +46768,11 @@ function sweepStaleTurnActiveMarker(stateDir, opts) {
46744
46768
  }
46745
46769
 
46746
46770
  // ../src/build-info.ts
46747
- var VERSION = "0.12.14";
46748
- var COMMIT_SHA = "db6d87d6";
46749
- var COMMIT_DATE = "2026-05-19T05:51:09Z";
46750
- var LATEST_PR = 1542;
46751
- var COMMITS_AHEAD_OF_TAG = 17;
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;
46752
46776
 
46753
46777
  // gateway/boot-version.ts
46754
46778
  function formatRelativeAgo(iso) {
@@ -48438,7 +48462,9 @@ startTimer({
48438
48462
  try {
48439
48463
  clearSilentEndState(fbKey);
48440
48464
  } catch {}
48441
- 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}` : ""}
48442
48468
  `);
48443
48469
  }
48444
48470
  });
@@ -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
+ });