switchroom 0.19.26 → 0.19.27
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/bin/git-agent-attribution-hook.sh +144 -0
- package/dist/agent-scheduler/index.js +55 -0
- package/dist/auth-broker/index.js +116 -6
- package/dist/cli/notion-write-pretool.mjs +55 -0
- package/dist/cli/switchroom.js +2055 -1193
- package/dist/host-control/main.js +117 -7
- package/dist/vault/approvals/kernel-server.js +114 -6
- package/dist/vault/broker/server.js +114 -6
- package/package.json +6 -2
- package/profiles/_base/cron-session.sh.hbs +8 -0
- package/profiles/_base/start.sh.hbs +105 -0
- package/telegram-plugin/card-layout.ts +328 -0
- package/telegram-plugin/dist/bridge/bridge.js +93 -1
- package/telegram-plugin/dist/gateway/gateway.js +2009 -1166
- package/telegram-plugin/dist/server.js +96 -1
- package/telegram-plugin/edit-flood-fuse.ts +637 -56
- package/telegram-plugin/flood-429-ledger.ts +526 -0
- package/telegram-plugin/flood-circuit-breaker.ts +18 -0
- package/telegram-plugin/gateway/flood-reply-queue.ts +168 -0
- package/telegram-plugin/gateway/gateway.ts +58 -68
- package/telegram-plugin/gateway/narrative-lane.ts +14 -0
- package/telegram-plugin/gateway/outbound-send-path.ts +36 -0
- package/telegram-plugin/gateway/outbox-sweep.ts +183 -6
- package/telegram-plugin/gateway/pinned-message-handler.ts +12 -16
- package/telegram-plugin/gateway/status-pin-retarget.ts +72 -36
- package/telegram-plugin/gateway/status-pin-store.ts +58 -9
- package/telegram-plugin/gateway/worker-pin-reaper.ts +56 -7
- package/telegram-plugin/llm-error-present.ts +61 -2
- package/telegram-plugin/model-unavailable.ts +8 -0
- package/telegram-plugin/operator-events.ts +72 -5
- package/telegram-plugin/outbound-class.ts +81 -0
- package/telegram-plugin/provider-credit.ts +237 -0
- package/telegram-plugin/scripts/bun-test-ci.sh +36 -6
- package/telegram-plugin/send-gate.ts +24 -2
- package/telegram-plugin/status-no-truncate.ts +10 -48
- package/telegram-plugin/status-pin-driver.ts +33 -45
- package/telegram-plugin/status-pin.ts +18 -1
- package/telegram-plugin/tests/card-golden.test.ts +69 -0
- package/telegram-plugin/tests/card-lifecycle-render.test.ts +362 -0
- package/telegram-plugin/tests/card-type-distinguishability.test.ts +187 -164
- package/telegram-plugin/tests/card-variants.golden.txt +211 -0
- package/telegram-plugin/tests/card-variants.ts +366 -0
- package/telegram-plugin/tests/edit-flood-fuse-ban-awareness.test.ts +316 -0
- package/telegram-plugin/tests/edit-flood-fuse-default-deny.test.ts +319 -0
- package/telegram-plugin/tests/edit-flood-fuse.test.ts +11 -2
- package/telegram-plugin/tests/feed-edit-rate-ceiling.test.ts +462 -0
- package/telegram-plugin/tests/fixtures/real-429-stream.ts +220 -0
- package/telegram-plugin/tests/flood-429-ledger.test.ts +278 -0
- package/telegram-plugin/tests/flood-429-recorder-wiring.test.ts +128 -0
- package/telegram-plugin/tests/flood-reply-queue.test.ts +418 -0
- package/telegram-plugin/tests/outbox-sweep-flood-breaker.test.ts +221 -0
- package/telegram-plugin/tests/pinned-card-collapse.test.ts +19 -24
- package/telegram-plugin/tests/pinned-message-handler.test.ts +15 -15
- package/telegram-plugin/tests/provider-credit-402.test.ts +243 -0
- package/telegram-plugin/tests/status-pin-api.test.ts +11 -11
- package/telegram-plugin/tests/status-pin-boot-recovery.test.ts +36 -37
- package/telegram-plugin/tests/status-pin-lifecycle.test.ts +602 -0
- package/telegram-plugin/tests/status-pin-retarget.test.ts +90 -62
- package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +7 -3
- package/telegram-plugin/tests/status-pin-store.test.ts +109 -60
- package/telegram-plugin/tests/status-pin.test.ts +56 -5
- package/telegram-plugin/tests/test-runner-coverage.test.ts +133 -0
- package/telegram-plugin/tests/worker-activity-feed.test.ts +12 -10
- package/telegram-plugin/tests/worker-feed-coalesce.test.ts +23 -29
- package/telegram-plugin/tests/worker-feed-pin-persistence.test.ts +56 -59
- package/telegram-plugin/tests/worker-feed-terminal-edit-class.test.ts +335 -0
- package/telegram-plugin/tests/worker-visibility-prose-silent-harness.test.ts +1 -1
- package/telegram-plugin/tool-activity-summary.ts +239 -365
- package/telegram-plugin/uat/assertions.ts +22 -11
- package/telegram-plugin/uat/feed-matcher.test.ts +24 -17
- package/telegram-plugin/worker-activity-feed.ts +105 -47
- package/vendor/hindsight-memory/CLAUDE.md +45 -0
- package/vendor/hindsight-memory/scripts/lib/config.py +33 -0
- package/vendor/hindsight-memory/scripts/recall.py +176 -7
- package/vendor/hindsight-memory/scripts/tests/test_config_recall_passthrough_env.py +170 -0
- package/vendor/hindsight-memory/scripts/tests/test_recall_min_score.py +464 -0
- package/vendor/hindsight-memory/settings.json +1 -1
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, it, expect } from "vitest";
|
|
2
|
-
import {
|
|
3
|
-
|
|
2
|
+
import {
|
|
3
|
+
runStatusPinReconcile,
|
|
4
|
+
type StatusPinClaim,
|
|
5
|
+
} from "../gateway/status-pin-retarget.js";
|
|
6
|
+
import type { PinLegAction, PinState } from "../status-pin.js";
|
|
4
7
|
|
|
5
8
|
/**
|
|
6
9
|
* The orchestrator the gateway's `reconcileStatusPinInner` delegates to.
|
|
@@ -14,16 +17,19 @@ import type { DesiredPin, PinState } from "../status-pin.js";
|
|
|
14
17
|
* turn AND deleted the durable row, putting it out of reach of both the
|
|
15
18
|
* mid-session reaper and the next boot's sweep.
|
|
16
19
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
20
|
+
* #3831: the expansion ALSO existed inside `status-pin-driver.ts`, decided from
|
|
21
|
+
* the driver's own `decidePinAction` call and lacking the per-leg persistence.
|
|
22
|
+
* That copy is gone — the driver now takes a `PinLegAction`, which cannot
|
|
23
|
+
* express a repin — so this module is the only decider. These tests assert the
|
|
24
|
+
* LEG SEQUENCE the driver now receives, plus the claim bookkeeping.
|
|
19
25
|
*/
|
|
20
26
|
|
|
21
27
|
/** Records every leg and returns a scripted outcome for it. */
|
|
22
28
|
function legRecorder(outcomes: Array<PinState | null>) {
|
|
23
|
-
const legs: Array<{ from: PinState | null;
|
|
29
|
+
const legs: Array<{ from: PinState | null; action: PinLegAction }> = [];
|
|
24
30
|
let i = 0;
|
|
25
|
-
const runPin = async (from: PinState | null
|
|
26
|
-
legs.push({ from,
|
|
31
|
+
const runPin = async (action: PinLegAction, from: PinState | null) => {
|
|
32
|
+
legs.push({ from, action });
|
|
27
33
|
const out = outcomes[i];
|
|
28
34
|
i += 1;
|
|
29
35
|
return out ?? null;
|
|
@@ -31,18 +37,10 @@ function legRecorder(outcomes: Array<PinState | null>) {
|
|
|
31
37
|
return { legs, runPin };
|
|
32
38
|
}
|
|
33
39
|
|
|
34
|
-
function
|
|
35
|
-
const
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
pinnedAt: new Map<string, number>(),
|
|
39
|
-
};
|
|
40
|
-
if (seed) {
|
|
41
|
-
r.state.set(seed.key, seed.state);
|
|
42
|
-
r.chatIds.set(seed.key, seed.chatId);
|
|
43
|
-
r.pinnedAt.set(seed.key, seed.at);
|
|
44
|
-
}
|
|
45
|
-
return r;
|
|
40
|
+
function claimsOf(seed?: { key: string; claim: StatusPinClaim }) {
|
|
41
|
+
const claims = new Map<string, StatusPinClaim>();
|
|
42
|
+
if (seed) claims.set(seed.key, seed.claim);
|
|
43
|
+
return claims;
|
|
46
44
|
}
|
|
47
45
|
|
|
48
46
|
const KEY = "fg:c:7";
|
|
@@ -51,7 +49,10 @@ const CHAT = "-100123";
|
|
|
51
49
|
describe("runStatusPinReconcile — RETARGET", () => {
|
|
52
50
|
it("runs BOTH legs: unpins the stale claim, then pins the new message", async () => {
|
|
53
51
|
const { legs, runPin } = legRecorder([null, { messageId: 901 }]);
|
|
54
|
-
const
|
|
52
|
+
const claims = claimsOf({
|
|
53
|
+
key: KEY,
|
|
54
|
+
claim: { messageId: 900, chatId: CHAT, pinnedAt: 1000 },
|
|
55
|
+
});
|
|
55
56
|
|
|
56
57
|
await runStatusPinReconcile({
|
|
57
58
|
pinKey: KEY,
|
|
@@ -60,19 +61,19 @@ describe("runStatusPinReconcile — RETARGET", () => {
|
|
|
60
61
|
desired: { pinned: true, messageId: 901 },
|
|
61
62
|
persist: null,
|
|
62
63
|
runPin,
|
|
63
|
-
|
|
64
|
+
claims,
|
|
64
65
|
});
|
|
65
66
|
|
|
66
67
|
// The pre-fix behaviour was legs === [unpin] and nothing pinned.
|
|
67
68
|
expect(legs).toEqual([
|
|
68
|
-
{ from: { messageId: 900 },
|
|
69
|
-
{ from: null,
|
|
69
|
+
{ from: { messageId: 900 }, action: { kind: "unpin", messageId: 900 } },
|
|
70
|
+
{ from: null, action: { kind: "pin", messageId: 901 } },
|
|
70
71
|
]);
|
|
71
|
-
expect(
|
|
72
|
-
expect(
|
|
72
|
+
expect(claims.get(KEY)?.messageId).toBe(901);
|
|
73
|
+
expect(claims.get(KEY)?.chatId).toBe(CHAT);
|
|
73
74
|
});
|
|
74
75
|
|
|
75
|
-
it("leg 2 starts from a NULL claim, so
|
|
76
|
+
it("leg 2 starts from a NULL claim, so the driver pins rather than no-opping", async () => {
|
|
76
77
|
const { legs, runPin } = legRecorder([null, { messageId: 901 }]);
|
|
77
78
|
await runStatusPinReconcile({
|
|
78
79
|
pinKey: KEY,
|
|
@@ -81,14 +82,18 @@ describe("runStatusPinReconcile — RETARGET", () => {
|
|
|
81
82
|
desired: { pinned: true, messageId: 901 },
|
|
82
83
|
persist: null,
|
|
83
84
|
runPin,
|
|
84
|
-
|
|
85
|
+
claims: claimsOf(),
|
|
85
86
|
});
|
|
86
87
|
expect(legs[1].from).toBeNull();
|
|
88
|
+
expect(legs[1].action).toEqual({ kind: "pin", messageId: 901 });
|
|
87
89
|
});
|
|
88
90
|
|
|
89
91
|
it("re-stamps pinnedAt for the NEW card, so the reaper ages the live message", async () => {
|
|
90
92
|
const { runPin } = legRecorder([null, { messageId: 901 }]);
|
|
91
|
-
const
|
|
93
|
+
const claims = claimsOf({
|
|
94
|
+
key: KEY,
|
|
95
|
+
claim: { messageId: 900, chatId: CHAT, pinnedAt: 1000 },
|
|
96
|
+
});
|
|
92
97
|
await runStatusPinReconcile({
|
|
93
98
|
pinKey: KEY,
|
|
94
99
|
chatId: CHAT,
|
|
@@ -96,18 +101,22 @@ describe("runStatusPinReconcile — RETARGET", () => {
|
|
|
96
101
|
desired: { pinned: true, messageId: 901 },
|
|
97
102
|
persist: null,
|
|
98
103
|
runPin,
|
|
99
|
-
|
|
104
|
+
claims,
|
|
105
|
+
now: () => 5000,
|
|
100
106
|
});
|
|
101
107
|
// Leg 1 cleared the claim (dropping the old stamp), leg 2 took a fresh one.
|
|
102
|
-
expect(
|
|
108
|
+
expect(claims.get(KEY)?.pinnedAt).toBe(5000);
|
|
103
109
|
});
|
|
104
110
|
|
|
105
111
|
it("NEVER-CONFIRMED unpin (#3664 Defect B): SKIPS the pin leg and retains the OLD claim", async () => {
|
|
106
|
-
// A non-null leg-1 result means
|
|
112
|
+
// A non-null leg-1 result means executePinLeg deliberately kept the claim —
|
|
107
113
|
// the old message is provably still pinned. Pinning the new one now would
|
|
108
114
|
// leave TWO pins in the chat with a durable record of one.
|
|
109
115
|
const { legs, runPin } = legRecorder([{ messageId: 900 }]);
|
|
110
|
-
const
|
|
116
|
+
const claims = claimsOf({
|
|
117
|
+
key: KEY,
|
|
118
|
+
claim: { messageId: 900, chatId: CHAT, pinnedAt: 1000 },
|
|
119
|
+
});
|
|
111
120
|
|
|
112
121
|
await runStatusPinReconcile({
|
|
113
122
|
pinKey: KEY,
|
|
@@ -116,19 +125,24 @@ describe("runStatusPinReconcile — RETARGET", () => {
|
|
|
116
125
|
desired: { pinned: true, messageId: 901 },
|
|
117
126
|
persist: null,
|
|
118
127
|
runPin,
|
|
119
|
-
|
|
128
|
+
claims,
|
|
120
129
|
});
|
|
121
130
|
|
|
122
131
|
expect(legs).toHaveLength(1);
|
|
123
|
-
expect(
|
|
124
|
-
|
|
125
|
-
|
|
132
|
+
expect(claims.get(KEY)).toEqual({
|
|
133
|
+
messageId: 900,
|
|
134
|
+
chatId: CHAT,
|
|
135
|
+
pinnedAt: 1000, // original age preserved for the TTL gate
|
|
136
|
+
});
|
|
126
137
|
});
|
|
127
138
|
|
|
128
139
|
it("a FAILED pin leg drops the claim entirely (nothing is pinned, nothing is tracked)", async () => {
|
|
129
|
-
//
|
|
140
|
+
// executePinLeg returns its `from` on a failed pin — here `null`.
|
|
130
141
|
const { runPin } = legRecorder([null, null]);
|
|
131
|
-
const
|
|
142
|
+
const claims = claimsOf({
|
|
143
|
+
key: KEY,
|
|
144
|
+
claim: { messageId: 900, chatId: CHAT, pinnedAt: 1000 },
|
|
145
|
+
});
|
|
132
146
|
await runStatusPinReconcile({
|
|
133
147
|
pinKey: KEY,
|
|
134
148
|
chatId: CHAT,
|
|
@@ -136,18 +150,16 @@ describe("runStatusPinReconcile — RETARGET", () => {
|
|
|
136
150
|
desired: { pinned: true, messageId: 901 },
|
|
137
151
|
persist: null,
|
|
138
152
|
runPin,
|
|
139
|
-
|
|
153
|
+
claims,
|
|
140
154
|
});
|
|
141
|
-
expect(
|
|
142
|
-
expect(reg.chatIds.has(KEY)).toBe(false);
|
|
143
|
-
expect(reg.pinnedAt.has(KEY)).toBe(false);
|
|
155
|
+
expect(claims.has(KEY)).toBe(false);
|
|
144
156
|
});
|
|
145
157
|
});
|
|
146
158
|
|
|
147
159
|
describe("runStatusPinReconcile — single-leg actions are unchanged", () => {
|
|
148
|
-
it("first pin: one leg, claim + chat + age recorded", async () => {
|
|
160
|
+
it("first pin: one leg, claim + chat + age recorded together", async () => {
|
|
149
161
|
const { legs, runPin } = legRecorder([{ messageId: 900 }]);
|
|
150
|
-
const
|
|
162
|
+
const claims = claimsOf();
|
|
151
163
|
await runStatusPinReconcile({
|
|
152
164
|
pinKey: KEY,
|
|
153
165
|
chatId: CHAT,
|
|
@@ -155,16 +167,26 @@ describe("runStatusPinReconcile — single-leg actions are unchanged", () => {
|
|
|
155
167
|
desired: { pinned: true, messageId: 900 },
|
|
156
168
|
persist: null,
|
|
157
169
|
runPin,
|
|
158
|
-
|
|
170
|
+
claims,
|
|
171
|
+
now: () => 4242,
|
|
172
|
+
});
|
|
173
|
+
expect(legs).toEqual([
|
|
174
|
+
{ from: null, action: { kind: "pin", messageId: 900 } },
|
|
175
|
+
]);
|
|
176
|
+
// #3809: the claim is ONE record — a chat-less claim cannot be represented.
|
|
177
|
+
expect(claims.get(KEY)).toEqual({
|
|
178
|
+
messageId: 900,
|
|
179
|
+
chatId: CHAT,
|
|
180
|
+
pinnedAt: 4242,
|
|
159
181
|
});
|
|
160
|
-
expect(legs).toEqual([{ from: null, want: { pinned: true, messageId: 900 } }]);
|
|
161
|
-
expect(reg.state.get(KEY)).toEqual({ messageId: 900 });
|
|
162
|
-
expect(reg.pinnedAt.has(KEY)).toBe(true);
|
|
163
182
|
});
|
|
164
183
|
|
|
165
|
-
it("turn-end unpin: one leg,
|
|
184
|
+
it("turn-end unpin: one leg, the whole claim is cleared", async () => {
|
|
166
185
|
const { legs, runPin } = legRecorder([null]);
|
|
167
|
-
const
|
|
186
|
+
const claims = claimsOf({
|
|
187
|
+
key: KEY,
|
|
188
|
+
claim: { messageId: 900, chatId: CHAT, pinnedAt: 1000 },
|
|
189
|
+
});
|
|
168
190
|
await runStatusPinReconcile({
|
|
169
191
|
pinKey: KEY,
|
|
170
192
|
chatId: CHAT,
|
|
@@ -172,19 +194,22 @@ describe("runStatusPinReconcile — single-leg actions are unchanged", () => {
|
|
|
172
194
|
desired: { pinned: false },
|
|
173
195
|
persist: null,
|
|
174
196
|
runPin,
|
|
175
|
-
|
|
197
|
+
claims,
|
|
176
198
|
});
|
|
177
|
-
expect(legs).toEqual([
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
expect(
|
|
199
|
+
expect(legs).toEqual([
|
|
200
|
+
{ from: { messageId: 900 }, action: { kind: "unpin", messageId: 900 } },
|
|
201
|
+
]);
|
|
202
|
+
expect(claims.has(KEY)).toBe(false);
|
|
181
203
|
});
|
|
182
204
|
|
|
183
205
|
it("steady-state re-pin of the SAME id: still one leg, and the ORIGINAL age is kept", async () => {
|
|
184
206
|
// 20 edits/turn each re-drive this; the reaper's TTL must measure from the
|
|
185
207
|
// first pin, not be reset by every edit.
|
|
186
208
|
const { legs, runPin } = legRecorder([{ messageId: 900 }]);
|
|
187
|
-
const
|
|
209
|
+
const claims = claimsOf({
|
|
210
|
+
key: KEY,
|
|
211
|
+
claim: { messageId: 900, chatId: CHAT, pinnedAt: 1000 },
|
|
212
|
+
});
|
|
188
213
|
await runStatusPinReconcile({
|
|
189
214
|
pinKey: KEY,
|
|
190
215
|
chatId: CHAT,
|
|
@@ -192,15 +217,17 @@ describe("runStatusPinReconcile — single-leg actions are unchanged", () => {
|
|
|
192
217
|
desired: { pinned: true, messageId: 900 },
|
|
193
218
|
persist: null,
|
|
194
219
|
runPin,
|
|
195
|
-
|
|
220
|
+
claims,
|
|
221
|
+
now: () => 9_999_999,
|
|
196
222
|
});
|
|
197
223
|
expect(legs).toHaveLength(1);
|
|
198
|
-
expect(
|
|
224
|
+
expect(legs[0].action.kind).toBe("noop");
|
|
225
|
+
expect(claims.get(KEY)?.pinnedAt).toBe(1000);
|
|
199
226
|
});
|
|
200
227
|
|
|
201
|
-
it("unpin with no claim: one no-op leg,
|
|
228
|
+
it("unpin with no claim: one no-op leg, the registry stays empty", async () => {
|
|
202
229
|
const { legs, runPin } = legRecorder([null]);
|
|
203
|
-
const
|
|
230
|
+
const claims = claimsOf();
|
|
204
231
|
await runStatusPinReconcile({
|
|
205
232
|
pinKey: KEY,
|
|
206
233
|
chatId: CHAT,
|
|
@@ -208,9 +235,10 @@ describe("runStatusPinReconcile — single-leg actions are unchanged", () => {
|
|
|
208
235
|
desired: { pinned: false },
|
|
209
236
|
persist: null,
|
|
210
237
|
runPin,
|
|
211
|
-
|
|
238
|
+
claims,
|
|
212
239
|
});
|
|
213
240
|
expect(legs).toHaveLength(1);
|
|
214
|
-
expect(
|
|
241
|
+
expect(legs[0].action.kind).toBe("noop");
|
|
242
|
+
expect(claims.size).toBe(0);
|
|
215
243
|
});
|
|
216
244
|
});
|
|
@@ -43,13 +43,17 @@ describe('status-pin service-message suppression', () => {
|
|
|
43
43
|
// message (which would nuke manual/operator pins too) — AND it must be
|
|
44
44
|
// chat-scoped (pinnedMessageIsOurs requires chatId + messageId), never a
|
|
45
45
|
// messageId-only match.
|
|
46
|
-
expect(MODULE_SRC).toContain('deps.
|
|
46
|
+
expect(MODULE_SRC).toContain('deps.statusPinClaims')
|
|
47
47
|
expect(MODULE_SRC).toMatch(/pinned_message\?\.message_id/)
|
|
48
48
|
expect(MODULE_SRC).toContain('pinnedMessageIsOurs(')
|
|
49
49
|
// The chatId computed from the update must be fed to the guard.
|
|
50
50
|
expect(MODULE_SRC).toMatch(/pinnedMessageIsOurs\(trackedPins\(\), chatId, pinnedId\)/)
|
|
51
|
-
// And the tracked entries must carry their chat association.
|
|
52
|
-
|
|
51
|
+
// And the tracked entries must carry their chat association. Since #3809
|
|
52
|
+
// that association is STRUCTURAL — one claim record per key carrying both
|
|
53
|
+
// messageId and chatId — so the guard reads it off the claim itself and a
|
|
54
|
+
// chat-less tracked pin can no longer be constructed.
|
|
55
|
+
expect(MODULE_SRC).toMatch(/chatId: c\.chatId/)
|
|
56
|
+
expect(MODULE_SRC).toMatch(/messageId: c\.messageId/)
|
|
53
57
|
})
|
|
54
58
|
|
|
55
59
|
it('bails out when the pinned message is not one of ours', () => {
|