switchroom 0.18.32 → 0.19.0
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.
- package/dist/auth-broker/index.js +17 -1
- package/dist/cli/switchroom.js +847 -729
- package/dist/host-control/main.js +18 -2
- package/dist/vault/approvals/kernel-server.js +17 -1
- package/dist/vault/broker/server.js +44 -2
- package/package.json +2 -2
- package/profiles/_base/start.sh.hbs +105 -18
- package/telegram-plugin/dist/gateway/gateway.js +60612 -56998
- package/telegram-plugin/gateway/agent-button-callback-handler.ts +237 -0
- package/telegram-plugin/gateway/ask-callback-handler.ts +92 -0
- package/telegram-plugin/gateway/attachment-message-handlers.ts +152 -0
- package/telegram-plugin/gateway/boot-card.ts +169 -1
- package/telegram-plugin/gateway/bot-commands-model-effort.ts +209 -0
- package/telegram-plugin/gateway/bot-commands-start-info.ts +108 -0
- package/telegram-plugin/gateway/callback-query-handlers.ts +124 -0
- package/telegram-plugin/gateway/card-approval-keyboards.test.ts +28 -0
- package/telegram-plugin/gateway/card-tool-handlers.ts +639 -0
- package/telegram-plugin/gateway/checklist-message-handler.ts +107 -0
- package/telegram-plugin/gateway/delivery-confirm-wiring.ts +133 -0
- package/telegram-plugin/gateway/gateway.ts +1347 -6758
- package/telegram-plugin/gateway/inbound-interceptors.ts +1133 -0
- package/telegram-plugin/gateway/inbound-router.ts +400 -0
- package/telegram-plugin/gateway/liveness-wiring.ts +440 -0
- package/telegram-plugin/gateway/media-message-handlers.ts +256 -0
- package/telegram-plugin/gateway/mental-model-propose-card.ts +16 -0
- package/telegram-plugin/gateway/model-command.ts +23 -0
- package/telegram-plugin/gateway/narrative-lane.ts +865 -0
- package/telegram-plugin/gateway/obligation-wiring.ts +333 -0
- package/telegram-plugin/gateway/photo-message-handler.ts +80 -0
- package/telegram-plugin/gateway/pinned-message-handler.ts +86 -0
- package/telegram-plugin/gateway/secret-request-card.test.ts +46 -0
- package/telegram-plugin/gateway/secret-request-card.ts +45 -0
- package/telegram-plugin/gateway/stream-render.ts +2166 -0
- package/telegram-plugin/gateway/turn-end.ts +606 -0
- package/telegram-plugin/gateway/turn-start-surfaces.ts +298 -0
- package/telegram-plugin/gateway/vault-request-access-card.ts +16 -0
- package/telegram-plugin/gateway/vault-request-save-card.test.ts +49 -0
- package/telegram-plugin/gateway/vault-request-save-card.ts +52 -0
- package/telegram-plugin/gateway/voice-message-handler.ts +123 -0
- package/telegram-plugin/gateway/voice-ondemand-callback-handler.ts +204 -0
- package/telegram-plugin/gateway/worker-feed-dispatch.ts +40 -0
- package/telegram-plugin/narrative-dedup.ts +24 -1
- package/telegram-plugin/narrative-flush.ts +2 -2
- package/telegram-plugin/render/render.ts +25 -1
- package/telegram-plugin/status-no-truncate.ts +13 -0
- package/telegram-plugin/subagent-watcher.ts +186 -3
- package/telegram-plugin/tests/activity-card-wiring.test.ts +8 -3
- package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +18 -3
- package/telegram-plugin/tests/agent-button-callback-handler.test.ts +149 -0
- package/telegram-plugin/tests/ask-callback-handler.test.ts +118 -0
- package/telegram-plugin/tests/attachment-message-handlers.test.ts +135 -0
- package/telegram-plugin/tests/boot-card-routing.test.ts +139 -0
- package/telegram-plugin/tests/bot-commands-model-effort.test.ts +189 -0
- package/telegram-plugin/tests/bot-commands-start-info.test.ts +240 -0
- package/telegram-plugin/tests/buffer-gate-broadened.test.ts +15 -6
- package/telegram-plugin/tests/busy-ack-wiring.test.ts +6 -1
- package/telegram-plugin/tests/button-tap-turn-gated.test.ts +18 -9
- package/telegram-plugin/tests/callback-query-handlers.test.ts +101 -0
- package/telegram-plugin/tests/card-tool-handlers.test.ts +497 -0
- package/telegram-plugin/tests/catch-all-unhandled-message.test.ts +5 -2
- package/telegram-plugin/tests/checklist-message-handler.test.ts +160 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +47 -10
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +27 -9
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +30 -7
- package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +39 -18
- package/telegram-plugin/tests/gateway-boot-smoke.test.ts +160 -0
- package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +3 -7
- package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +44 -29
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +8 -2
- package/telegram-plugin/tests/gateway-request-secret.test.ts +7 -3
- package/telegram-plugin/tests/gateway-secret-detect.test.ts +20 -10
- package/telegram-plugin/tests/gateway-session-model-relaunch.test.ts +8 -2
- package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +14 -3
- package/telegram-plugin/tests/inbound-message-types.test.ts +52 -16
- package/telegram-plugin/tests/media-message-handlers.test.ts +276 -0
- package/telegram-plugin/tests/mental-model-propose-callback-gate.test.ts +8 -4
- package/telegram-plugin/tests/model-command.test.ts +30 -0
- package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +27 -9
- package/telegram-plugin/tests/narrative-dedup.test.ts +32 -0
- package/telegram-plugin/tests/narrative-flush.test.ts +6 -2
- package/telegram-plugin/tests/narrative-lane-golden.test.ts +458 -0
- package/telegram-plugin/tests/no-reply-bounded-drain.test.ts +14 -3
- package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +16 -7
- package/telegram-plugin/tests/per-topic-current-turn.test.ts +32 -8
- package/telegram-plugin/tests/photo-message-handler.test.ts +114 -0
- package/telegram-plugin/tests/pinned-message-handler.test.ts +108 -0
- package/telegram-plugin/tests/render/render.test.ts +42 -0
- package/telegram-plugin/tests/secret-detect-delete-must-surface-failures.test.ts +8 -4
- package/telegram-plugin/tests/secret-detect-fail-closed.test.ts +38 -28
- package/telegram-plugin/tests/secret-detect-oauth-code.test.ts +28 -18
- package/telegram-plugin/tests/silence-liveness-wiring.test.ts +22 -8
- package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +42 -49
- package/telegram-plugin/tests/stop-command.test.ts +22 -12
- package/telegram-plugin/tests/stream-render-golden.test.ts +424 -0
- package/telegram-plugin/tests/subagent-watcher-boot-skip-dead.test.ts +218 -0
- package/telegram-plugin/tests/subagent-watcher-resume-reregister.test.ts +14 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +35 -3
- package/telegram-plugin/tests/turn-flush-safety.test.ts +183 -5
- package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +9 -4
- package/telegram-plugin/tests/vault-approval-posture.test.ts +8 -2
- package/telegram-plugin/tests/vault-grant-union.test.ts +4 -1
- package/telegram-plugin/tests/vault-key-regex-allows-slash.test.ts +16 -5
- package/telegram-plugin/tests/vault-request-access-tool.test.ts +10 -5
- package/telegram-plugin/tests/vault-request-access-unlock-resume.test.ts +4 -1
- package/telegram-plugin/tests/vault-subcommands.test.ts +6 -1
- package/telegram-plugin/tests/voice-message-handler.test.ts +111 -0
- package/telegram-plugin/tests/voice-ondemand-callback-handler.test.ts +140 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +86 -19
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +110 -20
- package/telegram-plugin/tests/worker-feed-resume-guard.test.ts +86 -0
- package/telegram-plugin/tool-activity-summary.ts +83 -35
- package/telegram-plugin/turn-flush-safety.ts +80 -14
- package/telegram-plugin/uat/restart-capability.ts +76 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +14 -4
- package/telegram-plugin/uat/scenarios/bridge-flap-resilience-dm.test.ts +11 -1
- package/telegram-plugin/uat/scenarios/cross-turn-pending-progress-dm.test.ts +19 -2
- package/telegram-plugin/uat/scenarios/jtbd-always-on-after-restart-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-deliberate-restart-resumes-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-interrupted-turn-resumes-dm.test.ts +6 -12
- package/telegram-plugin/uat/scenarios/jtbd-multipart-render-dm.test.ts +47 -13
- package/telegram-plugin/worker-activity-feed.ts +10 -4
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `checklist_tasks_done` / `checklist_tasks_added` service-message forwarding
|
|
3
|
+
* (switchroom#2996 P6 cluster F).
|
|
4
|
+
*
|
|
5
|
+
* Telegram emits these service messages when users tick or add tasks in a
|
|
6
|
+
* native checklist. They arrive as part of the `message` update type (no extra
|
|
7
|
+
* `allowed_updates` config needed). We route them to the agent as a new channel
|
|
8
|
+
* event with kind="checklist_task_changed" so the agent can react to user
|
|
9
|
+
* actions on a checklist it sent.
|
|
10
|
+
*
|
|
11
|
+
* Extracted verbatim from gateway.ts (Amendment 9 — no module-global reads).
|
|
12
|
+
* The two `bot.on('message:checklist_tasks_*')` registration lines stay in
|
|
13
|
+
* gateway.ts (order is a load-bearing invariant) and delegate here.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { Context } from 'grammy'
|
|
17
|
+
import type { InboundMessage } from './ipc-protocol.js'
|
|
18
|
+
|
|
19
|
+
type ChecklistTaskUpdate = {
|
|
20
|
+
message_checklist?: {
|
|
21
|
+
title?: string
|
|
22
|
+
tasks?: Array<{ id?: number; text?: string; is_completed?: boolean }>
|
|
23
|
+
}
|
|
24
|
+
checklist_tasks_done?: Array<{ id?: number; user?: { id?: number; username?: string }; done?: boolean }>
|
|
25
|
+
checklist_tasks_added?: Array<{ id?: number; text?: string; user?: { id?: number; username?: string } }>
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface ChecklistHandlerDeps {
|
|
29
|
+
/** Access config — only `allowFrom` is consulted (same gate as inbound). */
|
|
30
|
+
loadAccess: () => { allowFrom: string[] }
|
|
31
|
+
/** Broadcast a channel event to connected bridges. */
|
|
32
|
+
broadcast: (msg: InboundMessage) => void
|
|
33
|
+
/** stderr log sink. */
|
|
34
|
+
log: (line: string) => void
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function handleChecklistUpdate(
|
|
38
|
+
ctx: Context,
|
|
39
|
+
kind: 'checklist_tasks_done' | 'checklist_tasks_added',
|
|
40
|
+
deps: ChecklistHandlerDeps,
|
|
41
|
+
): void {
|
|
42
|
+
try {
|
|
43
|
+
const msg = ctx.message as (typeof ctx.message & ChecklistTaskUpdate) | undefined
|
|
44
|
+
if (!msg) return
|
|
45
|
+
|
|
46
|
+
const chat = ctx.chat
|
|
47
|
+
if (!chat) return
|
|
48
|
+
|
|
49
|
+
const chat_id = String(chat.id)
|
|
50
|
+
const access = deps.loadAccess()
|
|
51
|
+
|
|
52
|
+
// Only notify if this chat is allowlisted — same guard as inbound user messages.
|
|
53
|
+
// Closes #472 finding #13. Pre-fix the `&& access.allowFrom.length > 0`
|
|
54
|
+
// tail made this fail-OPEN when the allowlist was empty: every chat's
|
|
55
|
+
// checklist tasks would forward to the agent. Sibling guards (the
|
|
56
|
+
// inbound-message gate at line ~588 and the operator-event broadcast
|
|
57
|
+
// at line ~1221) are both fail-closed for empty allowlists. The
|
|
58
|
+
// empty-allowlist case is the most likely state for a misconfiguration
|
|
59
|
+
// (e.g. /unpair just ran), so fail-OPEN is the worst default.
|
|
60
|
+
if (!access.allowFrom.includes(chat_id)) return
|
|
61
|
+
|
|
62
|
+
const message_id = String(msg.message_id)
|
|
63
|
+
const ts = msg.date ?? Math.floor(Date.now() / 1000)
|
|
64
|
+
|
|
65
|
+
// Extract task updates depending on service message type
|
|
66
|
+
const tasksDone = msg.checklist_tasks_done ?? []
|
|
67
|
+
const tasksAdded = msg.checklist_tasks_added ?? []
|
|
68
|
+
const allTasks = kind === 'checklist_tasks_done' ? tasksDone : tasksAdded
|
|
69
|
+
|
|
70
|
+
// Build per-task channel events and broadcast each to connected bridges.
|
|
71
|
+
for (const task of allTasks) {
|
|
72
|
+
const taskId = task.id != null ? String(task.id) : '?'
|
|
73
|
+
const user = (task.user as { username?: string; id?: number } | undefined)
|
|
74
|
+
const userName = user?.username ?? (user?.id != null ? String(user.id) : 'unknown')
|
|
75
|
+
const state = kind === 'checklist_tasks_done'
|
|
76
|
+
? ((task as { done?: boolean }).done === false ? 'undone' : 'done')
|
|
77
|
+
: 'added'
|
|
78
|
+
|
|
79
|
+
const inboundMsg: InboundMessage = {
|
|
80
|
+
type: 'inbound',
|
|
81
|
+
chatId: chat_id,
|
|
82
|
+
messageId: Number(message_id),
|
|
83
|
+
user: userName,
|
|
84
|
+
userId: user?.id ?? 0,
|
|
85
|
+
ts,
|
|
86
|
+
text: `(checklist task ${state}: id=${taskId})`,
|
|
87
|
+
meta: {
|
|
88
|
+
chat_id,
|
|
89
|
+
message_id,
|
|
90
|
+
kind: 'checklist_task_changed',
|
|
91
|
+
task_id: taskId,
|
|
92
|
+
state,
|
|
93
|
+
user: userName,
|
|
94
|
+
user_id: user?.id != null ? String(user.id) : '0',
|
|
95
|
+
ts: new Date(ts * 1000).toISOString(),
|
|
96
|
+
},
|
|
97
|
+
}
|
|
98
|
+
// allow-broadcast: informational checklist task notification, not turn-driving
|
|
99
|
+
deps.broadcast(inboundMsg)
|
|
100
|
+
deps.log(
|
|
101
|
+
`telegram gateway: checklist ${kind}: chat_id=${chat_id} message_id=${message_id} task_id=${taskId} state=${state} user=${userName}\n`,
|
|
102
|
+
)
|
|
103
|
+
}
|
|
104
|
+
} catch (err) {
|
|
105
|
+
deps.log(`telegram gateway: checklist handler error (${kind}): ${err}\n`)
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* delivery-confirm-wiring.ts — the deliver-until-acked SWEEP wiring
|
|
3
|
+
* (#2996 P8 PR-C1).
|
|
4
|
+
*
|
|
5
|
+
* Extracted VERBATIM from gateway.ts: the 5 s confirm-sweep tick body
|
|
6
|
+
* (`runDeliveryConfirmSweep`), the strand re-delivery (`redeliverStrandedInbound`)
|
|
7
|
+
* and the #2787 Mechanism A card-suspension scoper (`sweepSuspendedTargets`).
|
|
8
|
+
* The gateway keeps thin same-name wrappers and OWNS the `setInterval`
|
|
9
|
+
* (the P0c rule — a test import must stay side-effect-free).
|
|
10
|
+
*
|
|
11
|
+
* DI contract: state stays in gateway.ts (`deliveryQueue`, the pending-card
|
|
12
|
+
* maps, `pendingInboundBuffer`); volatile handles cross as getters. The idle
|
|
13
|
+
* gate deliberately reads `getCurrentTurnNull()` — the turn ATOM, not the
|
|
14
|
+
* machine's eager in-turn state (the documented trap: `turnInFlightForGate`
|
|
15
|
+
* stays set through a strand until the TTL tick, so it is NOT a usable idle
|
|
16
|
+
* signal here). Design §3 PR-C1 risk row; harness case 4 pins mid-turn
|
|
17
|
+
* suppression.
|
|
18
|
+
*
|
|
19
|
+
* `trackDelivery` enrolment callsites stay in gateway.ts unchanged — the two
|
|
20
|
+
* twin-body sites live inside the P7-owned handleInbound window and are P7's
|
|
21
|
+
* to delete (design §2 PR-C1 note).
|
|
22
|
+
*/
|
|
23
|
+
import type { InboundMessage } from './ipc-protocol.js'
|
|
24
|
+
import type { DeliveryConfirmWiringDeps } from './gateway.js'
|
|
25
|
+
import {
|
|
26
|
+
trackDelivery,
|
|
27
|
+
sweep as sweepDeliveryQueue,
|
|
28
|
+
forgetDelivery,
|
|
29
|
+
isRedeliverySuspended,
|
|
30
|
+
type PendingDelivery,
|
|
31
|
+
} from './inbound-delivery-confirm.js'
|
|
32
|
+
import { chatKey } from './chat-key.js'
|
|
33
|
+
|
|
34
|
+
export function createDeliveryConfirmWiring(deps: DeliveryConfirmWiringDeps) {
|
|
35
|
+
const {
|
|
36
|
+
DELIVERY_CONFIRM_ENABLED,
|
|
37
|
+
DELIVERY_CONFIRM_TIMEOUT_MS,
|
|
38
|
+
getCurrentTurnNull,
|
|
39
|
+
sendToAgent,
|
|
40
|
+
deliveryQueue,
|
|
41
|
+
pendingInboundBuffer,
|
|
42
|
+
pendingPermissions,
|
|
43
|
+
pendingAskUser,
|
|
44
|
+
} = deps
|
|
45
|
+
|
|
46
|
+
// Re-deliver stranded inbounds until claude acks (the marko drop-wedge).
|
|
47
|
+
// Every few seconds, re-send any inbound that was handed to claude but never
|
|
48
|
+
// acked by an `enqueue` — it stranded unsubmitted in the composer. Re-clear
|
|
49
|
+
// the composer so the re-sent notification lands on a clean line, then
|
|
50
|
+
// re-send. Reuses the same delivery primitives; the message is never dropped.
|
|
51
|
+
// (Refs ipcServer / pendingInboundBuffer declared below — resolved at fire
|
|
52
|
+
// time, after module init.) unref so the interval never holds the process.
|
|
53
|
+
async function redeliverStrandedInbound(p: PendingDelivery<InboundMessage>): Promise<void> {
|
|
54
|
+
const selfAgent = process.env.SWITCHROOM_AGENT_NAME ?? ''
|
|
55
|
+
process.stderr.write(
|
|
56
|
+
`telegram gateway: inbound strand (no enqueue ack) key=${p.key} — re-clearing composer + re-delivering\n`,
|
|
57
|
+
)
|
|
58
|
+
try {
|
|
59
|
+
const { clearAgentComposer } = await import('../../src/agents/tmux.js')
|
|
60
|
+
if (selfAgent) clearAgentComposer({ agentName: selfAgent })
|
|
61
|
+
} catch { /* best-effort; re-deliver regardless */ }
|
|
62
|
+
const ok = sendToAgent(selfAgent, p.inbound)
|
|
63
|
+
if (ok) {
|
|
64
|
+
// Survive an ack that raced the `await import` above: only `enqueue`
|
|
65
|
+
// clears tracking, so if a concurrent ack removed the entry, re-affirm
|
|
66
|
+
// it — never drop. Idempotent.
|
|
67
|
+
if (!deliveryQueue.pending.has(p.key)) {
|
|
68
|
+
trackDelivery(deliveryQueue, p.key, p.inbound, Date.now(), p.messageId)
|
|
69
|
+
}
|
|
70
|
+
} else {
|
|
71
|
+
// Bridge offline between attempts — hand off to the offline buffer
|
|
72
|
+
// (bridgeUp drains it) and stop tracking here; the spool owns it now.
|
|
73
|
+
pendingInboundBuffer.push(selfAgent, p.inbound)
|
|
74
|
+
forgetDelivery(deliveryQueue, p.key)
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
// #2787 Mechanism A — which chats/topics currently hold a live permission or
|
|
78
|
+
// ask_user card. A pending card is a live interaction for ITS OWN chat, so the
|
|
79
|
+
// confirm sweep must not re-clear the composer + re-send there. But the OLD guard
|
|
80
|
+
// (`pendingPermissions.size > 0 || pendingAskUser.size > 0 → return`) suspended
|
|
81
|
+
// the sweep GLOBALLY: one card parked in a single topic (or in the operator DM,
|
|
82
|
+
// a different chatId entirely) froze re-delivery of every stranded inbound across
|
|
83
|
+
// EVERY topic until that one card resolved. This scopes the suspension to the
|
|
84
|
+
// card's own target. Returns per-topic chatKeys where the topic is known and bare
|
|
85
|
+
// chatIds where it isn't (a card fanned to operator DMs records chatId only, and
|
|
86
|
+
// a whole-chat suspension is the safe conservative fallback there); the callsite
|
|
87
|
+
// tests a delivery entry against both renderings.
|
|
88
|
+
function sweepSuspendedTargets(): { keys: Set<string>; chats: Set<string> } {
|
|
89
|
+
const keys = new Set<string>()
|
|
90
|
+
const chats = new Set<string>()
|
|
91
|
+
for (const p of pendingPermissions.values()) {
|
|
92
|
+
for (const c of p.cards) {
|
|
93
|
+
if (c.threadId != null) keys.add(chatKey(c.chatId, c.threadId))
|
|
94
|
+
else chats.add(c.chatId)
|
|
95
|
+
}
|
|
96
|
+
// A permission whose card send hasn't resolved yet (cards still empty) has
|
|
97
|
+
// no recorded target — suspend nothing for it; the next sweep sees it once
|
|
98
|
+
// the send resolves, and the currentTurn guard already covers the in-turn
|
|
99
|
+
// window a fresh permission request lives in.
|
|
100
|
+
}
|
|
101
|
+
for (const a of pendingAskUser.values()) {
|
|
102
|
+
if (a.threadId != null) keys.add(chatKey(a.chatId, a.threadId))
|
|
103
|
+
else chats.add(a.chatId)
|
|
104
|
+
}
|
|
105
|
+
return { keys, chats }
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// #2996 P0c/P8 PR-C1: the sweep TICK body (named in PR-A so the harness can
|
|
109
|
+
// drive it) — the GATEWAY still owns the `setInterval` (the P0c gate rule:
|
|
110
|
+
// modules export the tick body; the gateway registers the timer).
|
|
111
|
+
function runDeliveryConfirmSweep(): void {
|
|
112
|
+
if (!DELIVERY_CONFIRM_ENABLED) return
|
|
113
|
+
// Re-deliver ONLY when claude is genuinely idle. `currentTurn` is set solely
|
|
114
|
+
// by the enqueue session-event and nulled at turn-end, so `currentTurn != null`
|
|
115
|
+
// means a real turn is in flight — re-clearing the composer + re-sending now
|
|
116
|
+
// would clobber it (the exact mid-turn wedge this queue exists to prevent).
|
|
117
|
+
// NB: the machine's in-turn state (turnInFlightForGate) is advanced EAGERLY
|
|
118
|
+
// at delivery and stays set through a strand until the TTL tick, so it is
|
|
119
|
+
// NOT a usable "idle" signal here.
|
|
120
|
+
if (!getCurrentTurnNull()) return
|
|
121
|
+
// #2787 Mechanism A: a pending permission / ask_user card suspends re-delivery
|
|
122
|
+
// ONLY for its own chat/topic — never globally. Skip just the stranded entries
|
|
123
|
+
// whose target holds a live card; sweep the rest so unrelated topics keep
|
|
124
|
+
// getting re-delivered.
|
|
125
|
+
const suspended = sweepSuspendedTargets()
|
|
126
|
+
for (const p of sweepDeliveryQueue(deliveryQueue, Date.now(), DELIVERY_CONFIRM_TIMEOUT_MS)) {
|
|
127
|
+
if (isRedeliverySuspended(p.key, suspended)) continue
|
|
128
|
+
void redeliverStrandedInbound(p)
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
return { runDeliveryConfirmSweep, redeliverStrandedInbound, sweepSuspendedTargets }
|
|
133
|
+
}
|