switchroom 0.18.26 → 0.18.28
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/README.md +6 -2
- package/dist/cli/ms-365-write-pretool.mjs +4953 -14
- package/dist/cli/switchroom.js +1 -1
- package/dist/host-control/main.js +1 -1
- package/package.json +1 -1
- package/profiles/_base/start.sh.hbs +16 -0
- package/telegram-plugin/dist/gateway/gateway.js +571 -43
- package/telegram-plugin/flushed-turn-supersede.ts +58 -0
- package/telegram-plugin/gateway/derive-turn-id.ts +32 -0
- package/telegram-plugin/gateway/gateway.ts +358 -53
- package/telegram-plugin/gateway/handback-preturn-signal.ts +442 -0
- package/telegram-plugin/gateway/model-command.ts +68 -0
- package/telegram-plugin/gateway/ms365-write-approval.test.ts +101 -0
- package/telegram-plugin/gateway/ms365-write-approval.ts +65 -3
- package/telegram-plugin/gateway/subagent-handback-inbound-builder.ts +12 -0
- package/telegram-plugin/gateway/turn-active-marker.ts +35 -0
- package/telegram-plugin/gateway/worker-pin-reaper.ts +54 -0
- package/telegram-plugin/send-gate.test.ts +138 -0
- package/telegram-plugin/send-gate.ts +104 -1
- package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +14 -4
- package/telegram-plugin/tests/effort-command.test.ts +47 -0
- package/telegram-plugin/tests/flushed-turn-supersede.test.ts +60 -0
- package/telegram-plugin/tests/handback-preturn-adoption-roundtrip.test.ts +211 -0
- package/telegram-plugin/tests/handback-preturn-signal.test.ts +346 -0
- package/telegram-plugin/tests/model-command.test.ts +112 -0
- package/telegram-plugin/tests/multitopic-routing-wiring.test.ts +14 -2
- package/telegram-plugin/tests/outbound-send-chunks.test.ts +57 -0
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +18 -11
- package/telegram-plugin/tests/reply-owner-resolve.test.ts +90 -0
- package/telegram-plugin/tests/subagent-handback-inbound-builder.test.ts +5 -0
- package/telegram-plugin/tests/turn-active-marker.test.ts +29 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +121 -0
- package/telegram-plugin/tests/worker-feed-migration-eviction.test.ts +140 -0
- package/telegram-plugin/tests/worker-pin-reaper.test.ts +78 -0
- package/telegram-plugin/worker-activity-feed.ts +169 -6
|
@@ -113,6 +113,64 @@ export function decideSupersede(
|
|
|
113
113
|
return { supersede: true, deleteMessageIds: [...record.messageIds], reason: 'supersede' }
|
|
114
114
|
}
|
|
115
115
|
|
|
116
|
+
/**
|
|
117
|
+
* How the gateway should CORRECT a flushed message once its canonical `reply`
|
|
118
|
+
* lands. Historically the supersede always did `deleteMessage(A)` + fresh send
|
|
119
|
+
* of the canonical reply B — visible to the user as a delete+replace flicker
|
|
120
|
+
* plus a second device ping. When the reply fits a single message and matches
|
|
121
|
+
* the flushed message's type (both plain text, the normal case), we can instead
|
|
122
|
+
* edit message A in place with B's content: no delete, no flicker, no re-ping.
|
|
123
|
+
*
|
|
124
|
+
* - `edit-in-place` → the gateway feeds `editMessageId` into the existing
|
|
125
|
+
* single-message edit path (the `previewMessageId` edit lane in
|
|
126
|
+
* `sendReplyChunks`), which renders the canonical reply with the SAME rich
|
|
127
|
+
* path a fresh reply uses and, on any edit 400 (message too old / can't be
|
|
128
|
+
* edited / not found), falls back to delete+resend — so correctness never
|
|
129
|
+
* regresses. `deleteMessageIds` is empty (message A becomes message B).
|
|
130
|
+
* - `delete-resend` → the legacy behaviour: delete every flushed message id,
|
|
131
|
+
* then send the canonical reply fresh. Chosen whenever edit-in-place isn't
|
|
132
|
+
* safe (multi-part reply, >1 flushed message, a file/voice-only reply, or a
|
|
133
|
+
* draft-stream preview already owns the single-message edit lane).
|
|
134
|
+
*/
|
|
135
|
+
export type SupersedeCorrection =
|
|
136
|
+
| { mode: 'edit-in-place'; editMessageId: number; deleteMessageIds: number[] }
|
|
137
|
+
| { mode: 'delete-resend'; deleteMessageIds: number[] }
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Decide how to correct a superseded flush. Pure so the gateway runs the exact
|
|
141
|
+
* branch the regression tests exercise. Edit-in-place is chosen IFF the flush
|
|
142
|
+
* posted exactly ONE message AND the canonical reply is a single plain-text
|
|
143
|
+
* message with no competing edit target:
|
|
144
|
+
* - `flushMessageIds.length === 1` — a multi-message flush has no single
|
|
145
|
+
* edit target; delete all and resend.
|
|
146
|
+
* - `chunkCount === 1` — a multi-part reply can't collapse into one edit.
|
|
147
|
+
* - `!hasFiles` — a file/album reply is not a plain-text edit.
|
|
148
|
+
* - `!suppressText` — a voice-only reply sends no text body to edit into.
|
|
149
|
+
* - `!hasOpenPreview` — a live draft-stream preview already owns the
|
|
150
|
+
* single-message edit lane; deleting the flush and letting the preview edit
|
|
151
|
+
* keeps exactly one message.
|
|
152
|
+
* `literalText` (`format:'text'`) stays eligible — it is still a text message,
|
|
153
|
+
* and the edit lane renders literal vs rich identically to a fresh send.
|
|
154
|
+
*/
|
|
155
|
+
export function decideSupersedeCorrection(input: {
|
|
156
|
+
flushMessageIds: number[]
|
|
157
|
+
chunkCount: number
|
|
158
|
+
hasFiles: boolean
|
|
159
|
+
suppressText: boolean
|
|
160
|
+
hasOpenPreview: boolean
|
|
161
|
+
}): SupersedeCorrection {
|
|
162
|
+
const eligible =
|
|
163
|
+
input.flushMessageIds.length === 1 &&
|
|
164
|
+
input.chunkCount === 1 &&
|
|
165
|
+
!input.hasFiles &&
|
|
166
|
+
!input.suppressText &&
|
|
167
|
+
!input.hasOpenPreview
|
|
168
|
+
if (eligible) {
|
|
169
|
+
return { mode: 'edit-in-place', editMessageId: input.flushMessageIds[0]!, deleteMessageIds: [] }
|
|
170
|
+
}
|
|
171
|
+
return { mode: 'delete-resend', deleteMessageIds: [...input.flushMessageIds] }
|
|
172
|
+
}
|
|
173
|
+
|
|
116
174
|
/** Sentinel key for records whose flush carried no turnId nonce. */
|
|
117
175
|
const NULL_TURN_KEY = '<<null-turn>>'
|
|
118
176
|
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* derive-turn-id.ts — the stable per-turn identity, extracted from the gateway
|
|
3
|
+
* monolith so BOTH the enqueue seam and out-of-gateway tests use the SAME
|
|
4
|
+
* function (no mirror-drift between production and the round-trip test).
|
|
5
|
+
*
|
|
6
|
+
* Component 3 — derive the stable per-turn identity from the chat, thread, and
|
|
7
|
+
* originating message id. Stamped into the inbound meta at build time
|
|
8
|
+
* (`origin_turn_id`) AND reconstructed at enqueue time from the same three
|
|
9
|
+
* values, so the id stamped on the message the model reads matches the id on
|
|
10
|
+
* the turn the gateway started for it. Using the message id (not the
|
|
11
|
+
* not-yet-known startedAt) is what lets the two sites agree. Returns null when
|
|
12
|
+
* there is no message id (synthetic / cron turns with no originating inbound —
|
|
13
|
+
* they never need origin routing, the live turn IS the origin).
|
|
14
|
+
*
|
|
15
|
+
* NOTE (#3268): a subagent-handback IS a synthetic inbound, but it MUST still
|
|
16
|
+
* derive a stable non-null id so the dead-air pre-turn card can be adopted by
|
|
17
|
+
* identity at enqueue. The handback inbound builder therefore rounds-trips its
|
|
18
|
+
* fabricated `ts` through `meta.message_id` (mirroring resume-inbound-builder),
|
|
19
|
+
* so `ev.messageId` is populated at enqueue and this function yields the SAME
|
|
20
|
+
* `chatKey#ts` the pre-turn seam recorded at release.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { chatKey } from './chat-key.js'
|
|
24
|
+
|
|
25
|
+
export function deriveTurnId(
|
|
26
|
+
chatId: string,
|
|
27
|
+
threadId: number | null | undefined,
|
|
28
|
+
messageId: string | number | null | undefined,
|
|
29
|
+
): string | null {
|
|
30
|
+
if (messageId == null || messageId === '' || String(messageId) === '0') return null
|
|
31
|
+
return `${chatKey(chatId, threadId ?? null)}#${messageId}`
|
|
32
|
+
}
|