switchroom 0.18.31 → 0.18.33
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/agent-scheduler/index.js +4 -2
- package/dist/auth-broker/index.js +21 -3
- package/dist/cli/notion-write-pretool.mjs +4 -2
- package/dist/cli/switchroom.js +1410 -852
- package/dist/host-control/main.js +22 -4
- package/dist/vault/approvals/kernel-server.js +21 -3
- package/dist/vault/broker/server.js +48 -4
- package/package.json +4 -3
- package/profiles/_base/start.sh.hbs +148 -23
- package/telegram-plugin/dist/gateway/gateway.js +62726 -58282
- 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/backstop-delivery.ts +223 -23
- 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/captured-answer-resume.ts +259 -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/disconnect-flush.ts +6 -44
- package/telegram-plugin/gateway/gateway-import-clean.test.ts +188 -0
- package/telegram-plugin/gateway/gateway.ts +6403 -13456
- package/telegram-plugin/gateway/inbound-delivery-machine-dispatch.ts +7 -15
- package/telegram-plugin/gateway/inbound-delivery-machine-shadow.ts +35 -68
- 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-ledger.ts +42 -0
- package/telegram-plugin/gateway/obligation-store.ts +37 -1
- package/telegram-plugin/gateway/obligation-wiring.ts +333 -0
- package/telegram-plugin/gateway/outbound-send-path.ts +2012 -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/pending-user-notice.ts +59 -13
- package/telegram-plugin/render/render.ts +25 -1
- package/telegram-plugin/status-no-truncate.ts +13 -0
- package/telegram-plugin/subagent-watcher.ts +297 -31
- 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/backstop-delivery.test.ts +167 -0
- package/telegram-plugin/tests/backstop-readback-probe.test.ts +144 -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 +28 -9
- package/telegram-plugin/tests/busy-ack-wiring.test.ts +6 -1
- package/telegram-plugin/tests/button-tap-turn-gated.test.ts +21 -12
- package/telegram-plugin/tests/callback-query-handlers.test.ts +101 -0
- package/telegram-plugin/tests/captured-answer-resume.test.ts +358 -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 +76 -29
- package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +4 -1
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +45 -16
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +30 -7
- package/telegram-plugin/tests/gateway-boot-side-effect-gating.test.ts +270 -0
- package/telegram-plugin/tests/gateway-boot-smoke.test.ts +150 -0
- package/telegram-plugin/tests/gateway-bot-construction-deferral.test.ts +251 -0
- package/telegram-plugin/tests/gateway-disconnect-flush.test.ts +5 -128
- package/telegram-plugin/tests/gateway-handler-registration-wiring.test.ts +299 -0
- package/telegram-plugin/tests/gateway-loopback-paste-redact.test.ts +44 -29
- package/telegram-plugin/tests/gateway-outbound-redact.test.ts +18 -5
- 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-delivery-cutover-flip.test.ts +54 -150
- package/telegram-plugin/tests/inbound-delivery-cutover-gate.test.ts +10 -14
- package/telegram-plugin/tests/inbound-delivery-dispatch-equivalence.test.ts +6 -7
- package/telegram-plugin/tests/inbound-delivery-machine-dispatch.test.ts +0 -16
- package/telegram-plugin/tests/inbound-emit-after-intercepts.test.ts +18 -7
- package/telegram-plugin/tests/inbound-message-types.test.ts +52 -16
- package/telegram-plugin/tests/litellm-proxy-auth-misconfig.test.ts +69 -14
- 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 +36 -12
- 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/obligation-ledger.test.ts +40 -0
- package/telegram-plugin/tests/obligation-store.test.ts +43 -0
- package/telegram-plugin/tests/pending-card-durability-wiring.test.ts +18 -8
- package/telegram-plugin/tests/per-topic-current-turn.test.ts +32 -8
- package/telegram-plugin/tests/permission-no-repeat-wiring.test.ts +9 -6
- package/telegram-plugin/tests/photo-message-handler.test.ts +114 -0
- package/telegram-plugin/tests/photo-reroute-wiring.test.ts +5 -2
- 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/reply-terminal-reaction.test.ts +6 -2
- 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 +31 -20
- package/telegram-plugin/tests/send-reply-golden.test.ts +571 -0
- 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 +305 -0
- package/telegram-plugin/tests/subagent-watcher-resurrection.test.ts +32 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +35 -3
- package/telegram-plugin/tests/turn-end-gate-backstop.test.ts +8 -12
- package/telegram-plugin/tests/turn-flush-safety.test.ts +191 -11
- package/telegram-plugin/tests/turn-flush-suppression-wiring.test.ts +117 -0
- package/telegram-plugin/tests/vault-approval-posture.test.ts +8 -2
- package/telegram-plugin/tests/vault-grant-inbound-builders.test.ts +2 -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 +236 -20
- package/telegram-plugin/tests/worker-feed-resume-guard.test.ts +86 -0
- package/telegram-plugin/tool-activity-summary.ts +110 -38
- 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 +34 -4
- package/telegram-plugin/gateway/busy-key-reaper.ts +0 -113
- package/telegram-plugin/gateway/gate-parity-probe.ts +0 -102
- package/telegram-plugin/tests/busy-key-reaper.test.ts +0 -192
- package/telegram-plugin/tests/fixtures/cutover-killswitch-probe.ts +0 -75
- package/telegram-plugin/tests/gate-parity-probe.test.ts +0 -171
- package/telegram-plugin/tests/parallel-turns-deadlock-fix.test.ts +0 -217
|
@@ -48,6 +48,39 @@ const gatewaySrc = readFileSync(
|
|
|
48
48
|
resolve(__dirname, '..', 'gateway', 'gateway.ts'),
|
|
49
49
|
'utf-8',
|
|
50
50
|
)
|
|
51
|
+
// #2996 P2: executeReply's body (the over-ping block + the lever-2 finalize)
|
|
52
|
+
// moved VERBATIM to outbound-send-path.ts `sendReply()`. The façade call-site
|
|
53
|
+
// assertions that used to grep the executeReply window grep the sendReply
|
|
54
|
+
// window there instead — the wiring contract they pin is unchanged.
|
|
55
|
+
const sendPathSrc = readFileSync(
|
|
56
|
+
resolve(__dirname, '..', 'gateway', 'outbound-send-path.ts'),
|
|
57
|
+
'utf-8',
|
|
58
|
+
)
|
|
59
|
+
// #2996 P4-A: `handleSessionEvent`'s body (the tool_label / sub-agent drains
|
|
60
|
+
// and their `ea.mayDrain(turn)` single-flight guards) moved VERBATIM to
|
|
61
|
+
// stream-render.ts. Drain-site assertions that grep for the full population of
|
|
62
|
+
// façade call sites therefore scan gateway.ts + stream-render.ts combined —
|
|
63
|
+
// the wiring contract they pin is unchanged, only its file home split.
|
|
64
|
+
const streamRenderSrc = readFileSync(
|
|
65
|
+
resolve(__dirname, '..', 'gateway', 'stream-render.ts'),
|
|
66
|
+
'utf-8',
|
|
67
|
+
)
|
|
68
|
+
// #2996 P4-B: the narrative/activity lane (showNarrativeStep,
|
|
69
|
+
// openLivenessFeedIfDue, feedHeartbeatTick, drainActivitySummary,
|
|
70
|
+
// clearActivitySummary + the cardDrainGate-wrapped drains inside them) moved
|
|
71
|
+
// VERBATIM into narrative-lane.ts (factory scope, bodies indented +2).
|
|
72
|
+
// gateway.ts keeps thin same-name wrappers, so lane-body assertions must read
|
|
73
|
+
// the MODULE source, not the wrapper.
|
|
74
|
+
const laneSrc = readFileSync(
|
|
75
|
+
resolve(__dirname, '..', 'gateway', 'narrative-lane.ts'),
|
|
76
|
+
'utf-8',
|
|
77
|
+
)
|
|
78
|
+
const gatewayAndStreamSrc = gatewaySrc + '\n' + streamRenderSrc + '\n' + laneSrc
|
|
79
|
+
/** The sendReply window: from the function decl to the next export. */
|
|
80
|
+
function sendReplyWindow(): string {
|
|
81
|
+
const after = sendPathSrc.split('export async function sendReply(')[1] ?? ''
|
|
82
|
+
return after.split('\nexport ')[0] ?? after
|
|
83
|
+
}
|
|
51
84
|
|
|
52
85
|
/** Source of a named gateway function up to the next top-level definition. */
|
|
53
86
|
function fnSrc(name: string): string {
|
|
@@ -55,6 +88,13 @@ function fnSrc(name: string): string {
|
|
|
55
88
|
return after.split('\nasync function ')[0]?.split('\nfunction ')[0] ?? after
|
|
56
89
|
}
|
|
57
90
|
|
|
91
|
+
/** Source of a named narrative-lane function (factory scope, indented +2) up
|
|
92
|
+
* to the next lane function definition (#2996 P4-B). */
|
|
93
|
+
function laneFnSrc(name: string): string {
|
|
94
|
+
const after = laneSrc.split(`function ${name}(`)[1] ?? ''
|
|
95
|
+
return after.split('\n async function ')[0]?.split('\n function ')[0] ?? after
|
|
96
|
+
}
|
|
97
|
+
|
|
58
98
|
describe('SWITCHROOM_EMISSION_AUTHORITY kill-switch (default OFF)', () => {
|
|
59
99
|
it('parses with `=== "1"` semantic so an unset env var is OFF (default off)', () => {
|
|
60
100
|
expect(facadeSrc).toMatch(
|
|
@@ -243,15 +283,15 @@ describe('claimOrDowngradePing — PR-4c over-ping decision moves into the faça
|
|
|
243
283
|
// The relocation must NOT split the #2562 pair across the façade boundary:
|
|
244
284
|
// the façade decides; the call-site thunk sets firstPingAt AND
|
|
245
285
|
// firstPingWasSubstantive on two adjacent lines, no await between.
|
|
246
|
-
expect(
|
|
286
|
+
expect(sendPathSrc).toMatch(
|
|
247
287
|
/turn\.firstPingAt = now\s*\n\s*turn\.firstPingWasSubstantive = replySubstantive/,
|
|
248
288
|
)
|
|
249
289
|
// No await anywhere in the executeReply over-ping block (the decide→apply→
|
|
250
290
|
// pair-set synchronous chain). Bound to the block, BEFORE the Telegraph
|
|
251
291
|
// block below (which legitimately awaits).
|
|
252
|
-
const blockStart =
|
|
253
|
-
const telegraphIdx =
|
|
254
|
-
const block =
|
|
292
|
+
const blockStart = sendPathSrc.indexOf('const applyOverPingDecision')
|
|
293
|
+
const telegraphIdx = sendPathSrc.indexOf('// Telegraph publish (#579)', blockStart)
|
|
294
|
+
const block = sendPathSrc.slice(blockStart, telegraphIdx)
|
|
255
295
|
const blockCode = block
|
|
256
296
|
.split('\n')
|
|
257
297
|
.filter((l) => {
|
|
@@ -267,17 +307,14 @@ describe('claimOrDowngradePing — PR-4c over-ping decision moves into the faça
|
|
|
267
307
|
// The over-ping net exists ONLY in executeReply. The retired stream_reply
|
|
268
308
|
// tool (executeStreamReply) had no decideOverPing / firstPingAt /
|
|
269
309
|
// wasOverPingSuppressed and never called claimOrDowngradePing.
|
|
270
|
-
|
|
310
|
+
// #2996 P2: the single call now lives in the extracted sendReply body;
|
|
311
|
+
// gateway.ts must have ZERO (the wrapper delegates).
|
|
312
|
+
const gwCalls = [...gatewaySrc.matchAll(/\.claimOrDowngradePing\(/g)]
|
|
313
|
+
expect(gwCalls).toHaveLength(0)
|
|
314
|
+
const calls = [...sendPathSrc.matchAll(/\.claimOrDowngradePing\(/g)]
|
|
271
315
|
expect(calls).toHaveLength(1)
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
// Upper bound: the next top-level function after executeReply.
|
|
275
|
-
const nextFnIdx = gatewaySrc.indexOf('\nasync function ', execReplyIdx + 1)
|
|
276
|
-
expect(execReplyIdx).toBeGreaterThan(-1)
|
|
277
|
-
expect(nextFnIdx).toBeGreaterThan(execReplyIdx)
|
|
278
|
-
// The single call is inside executeReply.
|
|
279
|
-
expect(callIdx).toBeGreaterThan(execReplyIdx)
|
|
280
|
-
expect(callIdx).toBeLessThan(nextFnIdx)
|
|
316
|
+
// The single call is inside the sendReply window.
|
|
317
|
+
expect(sendReplyWindow()).toMatch(/\.claimOrDowngradePing\(/)
|
|
281
318
|
})
|
|
282
319
|
})
|
|
283
320
|
|
|
@@ -299,7 +336,8 @@ describe('façade delegates to the existing emission primitives (call-site liter
|
|
|
299
336
|
it('finalizeCard wraps clearActivitySummary at the call sites (not CALLED inside the façade)', () => {
|
|
300
337
|
expect(facadeSrc).not.toMatch(/import .*clearActivitySummary/)
|
|
301
338
|
expect(facadeCode).not.toMatch(/clearActivitySummary\(/)
|
|
302
|
-
|
|
339
|
+
// #2996 P2: the lever-2 call site moved (verbatim) into sendReply.
|
|
340
|
+
expect(sendPathSrc).toMatch(/finalizeCard\(\(\) => \{\s*\n\s*clearActivitySummary\(/)
|
|
303
341
|
})
|
|
304
342
|
|
|
305
343
|
it('claimOrDowngradePing — the call-site disabled thunk STILL contains a literal decideOverPing( (disabled-path proof)', () => {
|
|
@@ -308,16 +346,16 @@ describe('façade delegates to the existing emission primitives (call-site liter
|
|
|
308
346
|
// its OWN literal `decideOverPing(` call inside the call-site thunk, VERBATIM
|
|
309
347
|
// from PR-4b-base — so the disabled path is provably byte-identical (never
|
|
310
348
|
// depends on the façade for the decision).
|
|
311
|
-
const pingIdx =
|
|
349
|
+
const pingIdx = sendPathSrc.indexOf('claimOrDowngradePing(')
|
|
312
350
|
expect(pingIdx).toBeGreaterThan(-1)
|
|
313
|
-
const window =
|
|
351
|
+
const window = sendPathSrc.slice(pingIdx, pingIdx + 1600)
|
|
314
352
|
expect(window).toMatch(/decideOverPing\(/)
|
|
315
353
|
})
|
|
316
354
|
})
|
|
317
355
|
|
|
318
356
|
describe('the drain sites route through the façade with producers preserved verbatim', () => {
|
|
319
357
|
it('the narrative SHOW site routes via openOrEditCard("narrative") + the producer-"narrative" drain', () => {
|
|
320
|
-
const body =
|
|
358
|
+
const body = laneFnSrc('showNarrativeStep')
|
|
321
359
|
expect(body).toMatch(/openOrEditCard\('narrative'/)
|
|
322
360
|
// The drain literal (producer arg verbatim) is preserved in the delegate.
|
|
323
361
|
expect(body).toMatch(/drainActivitySummary\(turn,\s*'narrative'\)/)
|
|
@@ -337,8 +375,8 @@ describe('the drain sites route through the façade with producers preserved ver
|
|
|
337
375
|
// - feedHeartbeatTick: the 0-label CLIMB of an already-open card
|
|
338
376
|
// (Phase 1) + the labelled-feed stale-step maintain.
|
|
339
377
|
// All must still route via the façade with the producer arg verbatim.
|
|
340
|
-
const earlyOpen =
|
|
341
|
-
const heartbeat =
|
|
378
|
+
const earlyOpen = laneFnSrc('openLivenessFeedIfDue')
|
|
379
|
+
const heartbeat = laneFnSrc('feedHeartbeatTick')
|
|
342
380
|
const opens = [
|
|
343
381
|
...earlyOpen.matchAll(/openOrEditCard\('liveness'/g),
|
|
344
382
|
...heartbeat.matchAll(/openOrEditCard\('liveness'/g),
|
|
@@ -361,15 +399,20 @@ describe('the drain sites route through the façade with producers preserved ver
|
|
|
361
399
|
it('both foreground sub-agent drains route via openOrEditCard("tool") with producer made EXPLICIT', () => {
|
|
362
400
|
// Previously these called drainActivitySummary(turn) with the implicit
|
|
363
401
|
// 'tool' default; PR-4a makes 'tool' explicit at both, per the plan.
|
|
364
|
-
|
|
402
|
+
// #2996 P4-A: these sites now live in stream-render.ts (tool_label + the
|
|
403
|
+
// sub-agent drains moved with handleSessionEvent), so scan the combined src.
|
|
404
|
+
const opens = [...gatewayAndStreamSrc.matchAll(/ea\.openOrEditCard\('tool', \(\) => \{\s*\n\s*turn\.activityInFlight = drainActivitySummary\(turn, 'tool'\)/g)]
|
|
365
405
|
// tool_label site (1) + the two sub-agent drains (2) = 3 'tool' openers.
|
|
366
406
|
expect(opens.length).toBeGreaterThanOrEqual(3)
|
|
367
407
|
// No foreground drain still uses the bare implicit-producer form.
|
|
368
|
-
expect(
|
|
408
|
+
expect(gatewayAndStreamSrc).not.toMatch(/turn\.activityInFlight = drainActivitySummary\(turn\)\n/)
|
|
369
409
|
})
|
|
370
410
|
|
|
371
411
|
it('every routed drain site guards the single-flight via ea.mayDrain(turn), not a bare activityInFlight read', () => {
|
|
372
|
-
|
|
412
|
+
// #2996 P4-A: the tool / sub-agent / post-answer bg-liveness guards moved
|
|
413
|
+
// to stream-render.ts with handleSessionEvent; the narrative + liveness
|
|
414
|
+
// guards stay in gateway.ts. The full population (8) spans both files.
|
|
415
|
+
const mayDrainGuards = [...gatewayAndStreamSrc.matchAll(/if \(ea\.mayDrain\(turn\)\)/g)]
|
|
373
416
|
// narrative SHOW + narrative RETRACT (timer-flush early-paint retract,
|
|
374
417
|
// narrative-flush.ts) + 2 liveness + tool + 2 sub-agent + 1 post-answer
|
|
375
418
|
// bg-liveness (Fix 2) = 8. The Phase-1 0-label climb
|
|
@@ -379,14 +422,13 @@ describe('the drain sites route through the façade with producers preserved ver
|
|
|
379
422
|
// gateway call site — pinned by feed-heartbeat-liveness-open.test.ts +
|
|
380
423
|
// silent-turn-climb-transport.test.ts.
|
|
381
424
|
expect(mayDrainGuards).toHaveLength(8)
|
|
382
|
-
expect(
|
|
425
|
+
expect(gatewayAndStreamSrc).toMatch(/mayDrain:\s*\(\)\s*=>\s*ea\.mayDrain\(turn\)/)
|
|
383
426
|
})
|
|
384
427
|
})
|
|
385
428
|
|
|
386
429
|
describe('the lever-2 finalize block routes through the façade', () => {
|
|
387
430
|
it('executeReply finalize routes via markSubstantiveFinalDelivered + finalizeCard (latch + clear preserved)', () => {
|
|
388
|
-
const
|
|
389
|
-
const body = after.split('\nasync function ')[0]?.split('\nfunction ')[0] ?? after
|
|
431
|
+
const body = sendReplyWindow()
|
|
390
432
|
expect(body).toMatch(/markSubstantiveFinalDelivered\(\(\) => \{\s*\n\s*finalizeTurn\.finalAnswerEverDelivered = true/)
|
|
391
433
|
expect(body).toMatch(/finalizeCard\(\(\) => \{\s*\n\s*clearActivitySummary\(finalizeTurn\)/)
|
|
392
434
|
})
|
|
@@ -396,7 +438,9 @@ describe('per-turn construction — one façade per turn, explicit chat/thread k
|
|
|
396
438
|
it('the turn ctor constructs a fresh EmissionAuthority with the explicit statusKey', () => {
|
|
397
439
|
// Per-turn: born in the CurrentTurn object literal so it is discarded with
|
|
398
440
|
// the turn and never persists across turns.
|
|
399
|
-
|
|
441
|
+
// #2996 P4-A: the turn ctor lives in handleSessionEvent's `enqueue` branch,
|
|
442
|
+
// which moved to stream-render.ts.
|
|
443
|
+
expect(gatewayAndStreamSrc).toMatch(/emissionAuthority: new EmissionAuthority\(\s*\n\s*statusKey\(/)
|
|
400
444
|
})
|
|
401
445
|
|
|
402
446
|
it('the façade constructor takes the chat/thread key EXPLICITLY (the PR-4e map seam)', () => {
|
|
@@ -504,12 +548,15 @@ describe('mayDrainCardNow — PR-4d card-drain gate (pure read; gateway holds th
|
|
|
504
548
|
// thunks stay byte-identical, wrapped by the centralized helper.
|
|
505
549
|
// 6 original + 1 post-answer background-agent liveness drain (Fix 2) + 1
|
|
506
550
|
// narrative-retract drain (timer-flush early-paint retract, narrative-flush.ts).
|
|
507
|
-
|
|
551
|
+
// #2996 P4-A: one of these guarded drains (the post-answer bg-liveness
|
|
552
|
+
// drain) moved to stream-render.ts with handleSessionEvent; the population
|
|
553
|
+
// of 8 now spans gateway.ts + stream-render.ts.
|
|
554
|
+
const wraps = [...gatewayAndStreamSrc.matchAll(/cardDrainGate\(turn, ea, \(\) => \{/g)]
|
|
508
555
|
expect(wraps).toHaveLength(8)
|
|
509
556
|
// The Phase-1 0-label climb (deterministic-turn-liveness.md) routes through
|
|
510
557
|
// the SAME helper via an injected thunk — its call lives in the extracted
|
|
511
558
|
// tick body (feed-heartbeat-climb.ts), wired at the gateway call site as
|
|
512
559
|
// `cardDrainGate: (run) => cardDrainGate(turn, ea, run)`.
|
|
513
|
-
expect(
|
|
560
|
+
expect(gatewayAndStreamSrc).toMatch(/cardDrainGate:\s*\(run\)\s*=>\s*cardDrainGate\(turn, ea, run\)/)
|
|
514
561
|
})
|
|
515
562
|
})
|
|
@@ -354,8 +354,11 @@ describe('claimOrDowngradePing — no await in the synchronous decide→apply→
|
|
|
354
354
|
resolve(__dirname, '..', 'gateway', 'emission-authority.ts'),
|
|
355
355
|
'utf-8',
|
|
356
356
|
)
|
|
357
|
+
// #2996 P2: the over-ping call site moved VERBATIM (with executeReply's
|
|
358
|
+
// body) into outbound-send-path.ts `sendReply()`; the await-free window
|
|
359
|
+
// assertion reads there. Contract unchanged.
|
|
357
360
|
const gatewaySrc = readFileSync(
|
|
358
|
-
resolve(__dirname, '..', 'gateway', '
|
|
361
|
+
resolve(__dirname, '..', 'gateway', 'outbound-send-path.ts'),
|
|
359
362
|
'utf-8',
|
|
360
363
|
)
|
|
361
364
|
|
|
@@ -17,25 +17,50 @@ const gatewaySrc = readFileSync(
|
|
|
17
17
|
resolve(__dirname, '..', 'gateway', 'gateway.ts'),
|
|
18
18
|
'utf-8',
|
|
19
19
|
)
|
|
20
|
+
// #2996 P2: executeReply's body (incl. the lever-2 finalize + the latch sets)
|
|
21
|
+
// moved VERBATIM to outbound-send-path.ts `sendReply()`; the assertions that
|
|
22
|
+
// pinned that body now read it there. The wiring contract is unchanged.
|
|
23
|
+
const sendPathSrc = readFileSync(
|
|
24
|
+
resolve(__dirname, '..', 'gateway', 'outbound-send-path.ts'),
|
|
25
|
+
'utf-8',
|
|
26
|
+
)
|
|
27
|
+
// #2996 P4-A: the per-turn CurrentTurn object literal (which initialises the
|
|
28
|
+
// sticky `finalAnswerEverDelivered: false` latch) is constructed in
|
|
29
|
+
// handleSessionEvent's `enqueue` branch, which moved VERBATIM to
|
|
30
|
+
// stream-render.ts. The init-latch assertions scan gateway.ts + that module.
|
|
31
|
+
const streamSrc = readFileSync(
|
|
32
|
+
resolve(__dirname, '..', 'gateway', 'stream-render.ts'),
|
|
33
|
+
'utf-8',
|
|
34
|
+
)
|
|
35
|
+
// #2996 P4-B: the narrative/activity lane (drainActivitySummary,
|
|
36
|
+
// showNarrativeStep, feedHeartbeatTick, clearActivitySummary) moved VERBATIM
|
|
37
|
+
// into narrative-lane.ts (factory scope, bodies indented +2); gateway keeps
|
|
38
|
+
// thin same-name wrappers. Lane-body windows read the module source.
|
|
39
|
+
const laneSrc = readFileSync(
|
|
40
|
+
resolve(__dirname, '..', 'gateway', 'narrative-lane.ts'),
|
|
41
|
+
'utf-8',
|
|
42
|
+
)
|
|
43
|
+
const gatewayAndStreamSrc = gatewaySrc + '\n' + streamSrc + '\n' + laneSrc
|
|
20
44
|
|
|
21
45
|
/** Source of `drainActivitySummary` up to the next top-level function. */
|
|
22
46
|
function drainSrc(): string {
|
|
23
|
-
|
|
24
|
-
|
|
47
|
+
// #2996 P4-B: reads the lane module (factory scope — next fn at 2-space indent).
|
|
48
|
+
const after = laneSrc.split('async function drainActivitySummary(')[1] ?? ''
|
|
49
|
+
return after.split('\n async function ')[0]?.split('\n function ')[0] ?? after
|
|
25
50
|
}
|
|
26
51
|
|
|
27
52
|
describe('sticky finalAnswerEverDelivered latch (lever 1 precondition / R0)', () => {
|
|
28
53
|
it('is initialised false in the turn object literal (per-turn reset at turn start)', () => {
|
|
29
|
-
expect(
|
|
54
|
+
expect(gatewayAndStreamSrc).toMatch(/finalAnswerEverDelivered:\s*false/)
|
|
30
55
|
})
|
|
31
56
|
|
|
32
57
|
it('is reset to false in exactly ONE place — the turn initialiser (never cleared by reopen)', () => {
|
|
33
58
|
// Mirrors activityEverOpened's sticky-true contract: the only `false` is the
|
|
34
59
|
// per-turn init. A standalone `= false` reassignment would let reopen clear
|
|
35
60
|
// the latch and reintroduce the reorder (the R0 correction).
|
|
36
|
-
const initFalse = [...
|
|
61
|
+
const initFalse = [...gatewayAndStreamSrc.matchAll(/finalAnswerEverDelivered:\s*false/g)]
|
|
37
62
|
expect(initFalse).toHaveLength(1)
|
|
38
|
-
const resetFalse = [...
|
|
63
|
+
const resetFalse = [...gatewayAndStreamSrc.matchAll(/finalAnswerEverDelivered\s*=\s*false/g)]
|
|
39
64
|
expect(resetFalse).toHaveLength(0)
|
|
40
65
|
})
|
|
41
66
|
|
|
@@ -51,20 +76,23 @@ describe('sticky finalAnswerEverDelivered latch (lever 1 precondition / R0)', ()
|
|
|
51
76
|
it('every site that sets the sticky latch true gates on finalAnswerSubstantive', () => {
|
|
52
77
|
// The latch is set true only at the points that set finalAnswerDelivered=true,
|
|
53
78
|
// and only when the reply was substantive — so an ack never latches it.
|
|
54
|
-
|
|
55
|
-
|
|
79
|
+
// #2996 P2: all three latch-set sites live in the extracted sendReply body.
|
|
80
|
+
const setTrue = [...sendPathSrc.matchAll(/finalAnswerEverDelivered\s*=\s*true/g)]
|
|
81
|
+
// sendReply post-send, silent-anchor merge, + the lever-2 finalize block
|
|
56
82
|
// (which is itself substantive-gated).
|
|
57
83
|
expect(setTrue.length).toBeGreaterThanOrEqual(3)
|
|
84
|
+
// And gateway.ts has none left (the wrapper delegates).
|
|
85
|
+
expect(gatewaySrc).not.toMatch(/finalAnswerEverDelivered\s*=\s*true/)
|
|
58
86
|
// Each `finalAnswerEverDelivered = true` must sit in a substantive context:
|
|
59
87
|
// either guarded by `if (turn.finalAnswerSubstantive)` or inside an
|
|
60
88
|
// `isSubstantiveFinalReply(...)` branch. Assert the substantive-gating
|
|
61
89
|
// token co-occurs (no bare unconditional latch set).
|
|
62
90
|
const bareUnconditional = [
|
|
63
|
-
...
|
|
91
|
+
...sendPathSrc.matchAll(/\n\s*(?:turn|finalizeTurn)\.finalAnswerEverDelivered\s*=\s*true/g),
|
|
64
92
|
]
|
|
65
93
|
for (const m of bareUnconditional) {
|
|
66
94
|
const idx = m.index ?? 0
|
|
67
|
-
const window =
|
|
95
|
+
const window = sendPathSrc.slice(Math.max(0, idx - 600), idx)
|
|
68
96
|
expect(
|
|
69
97
|
/finalAnswerSubstantive/.test(window) || /isSubstantiveFinalReply/.test(window),
|
|
70
98
|
).toBe(true)
|
|
@@ -108,14 +136,14 @@ describe('drainActivitySummary OPEN gate (levers 1 + 5, heartbeat-covered)', ()
|
|
|
108
136
|
|
|
109
137
|
describe('drain producers — narrative may not OPEN, liveness + tool may', () => {
|
|
110
138
|
it('showNarrativeStep drains with producer "narrative" (lever 5 base case)', () => {
|
|
111
|
-
const after =
|
|
112
|
-
const body = after.split('\
|
|
139
|
+
const after = laneSrc.split('function showNarrativeStep(')[1] ?? ''
|
|
140
|
+
const body = after.split('\n function ')[0] ?? after
|
|
113
141
|
expect(body).toMatch(/drainActivitySummary\(turn,\s*'narrative'\)/)
|
|
114
142
|
})
|
|
115
143
|
|
|
116
144
|
it('the liveness-open path drains with producer "liveness" (producer C preserved)', () => {
|
|
117
|
-
const after =
|
|
118
|
-
const body = after.split('\
|
|
145
|
+
const after = laneSrc.split('function feedHeartbeatTick(')[1] ?? ''
|
|
146
|
+
const body = after.split('\n function ')[0] ?? after
|
|
119
147
|
expect(body).toMatch(/drainActivitySummary\(turn,\s*'liveness'\)/)
|
|
120
148
|
})
|
|
121
149
|
|
|
@@ -125,10 +153,11 @@ describe('drain producers — narrative may not OPEN, liveness + tool may', () =
|
|
|
125
153
|
})
|
|
126
154
|
|
|
127
155
|
describe('lever 2 — finalize the card BEFORE a substantive reply send', () => {
|
|
128
|
-
/**
|
|
156
|
+
/** The extracted reply-orchestration body (#2996 P2): the sendReply window
|
|
157
|
+
* in outbound-send-path.ts, up to the next export. */
|
|
129
158
|
function executeReplySrc(): string {
|
|
130
|
-
const after =
|
|
131
|
-
return after.split('\
|
|
159
|
+
const after = sendPathSrc.split('export async function sendReply(')[1] ?? ''
|
|
160
|
+
return after.split('\nexport ')[0] ?? after
|
|
132
161
|
}
|
|
133
162
|
|
|
134
163
|
// #2996 step 1: the chunk send loop was relocated verbatim from executeReply
|
|
@@ -43,18 +43,41 @@ const gatewaySrc = readFileSync(
|
|
|
43
43
|
resolve(__dirname, '..', 'gateway', 'gateway.ts'),
|
|
44
44
|
'utf-8',
|
|
45
45
|
)
|
|
46
|
+
// #2996 P4-A: the enqueue-time `scheduleEarlyLivenessOpen(next)` moved with
|
|
47
|
+
// handleSessionEvent into stream-render.ts; the turn-end teardown stays in
|
|
48
|
+
// gateway.ts. The timer-lifecycle assertion spans both files.
|
|
49
|
+
const streamSrc = readFileSync(
|
|
50
|
+
resolve(__dirname, '..', 'gateway', 'stream-render.ts'),
|
|
51
|
+
'utf-8',
|
|
52
|
+
)
|
|
53
|
+
// #2996 P4-B: feedHeartbeatTick / openLivenessFeedIfDue (and the whole
|
|
54
|
+
// activity lane) moved VERBATIM into narrative-lane.ts (factory scope, bodies
|
|
55
|
+
// indented +2); gateway keeps thin same-name wrappers, so the body helpers
|
|
56
|
+
// below read the module source.
|
|
57
|
+
const laneSrc = readFileSync(
|
|
58
|
+
resolve(__dirname, '..', 'gateway', 'narrative-lane.ts'),
|
|
59
|
+
'utf-8',
|
|
60
|
+
)
|
|
61
|
+
// #2996 P8 PR-B: the turn-end funnel (incl. the `stopEarlyLivenessOpen(key as
|
|
62
|
+
// string)` teardown inside purgeReactionTracking) moved verbatim to turn-end.ts.
|
|
63
|
+
const turnEndSrc = readFileSync(
|
|
64
|
+
resolve(__dirname, '..', 'gateway', 'turn-end.ts'),
|
|
65
|
+
'utf-8',
|
|
66
|
+
)
|
|
67
|
+
const gatewayAndStreamSrc = gatewaySrc + '\n' + streamSrc + '\n' + laneSrc + '\n' + turnEndSrc
|
|
46
68
|
|
|
47
69
|
/** Return the source text of `feedHeartbeatTick` (everything up to the next top-level function). */
|
|
48
70
|
function feedHeartbeatTickSrc(): string {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
return after.split('\
|
|
71
|
+
// #2996 P4-B: reads narrative-lane.ts; next lane fn sits at 2-space indent.
|
|
72
|
+
const after = laneSrc.split('function feedHeartbeatTick(): void {')[1] ?? ''
|
|
73
|
+
return after.split('\n async function ')[0]?.split('\n function ')[0] ?? after
|
|
52
74
|
}
|
|
53
75
|
|
|
54
76
|
/** Return the source text of `openLivenessFeedIfDue` (the shared open helper). */
|
|
55
77
|
function openLivenessFeedIfDueSrc(): string {
|
|
56
|
-
|
|
57
|
-
|
|
78
|
+
// #2996 P4-B: reads narrative-lane.ts; next lane fn sits at 2-space indent.
|
|
79
|
+
const after = laneSrc.split('function openLivenessFeedIfDue(turn: CurrentTurn): void {')[1] ?? ''
|
|
80
|
+
return after.split('\n async function ')[0]?.split('\n function ')[0] ?? after
|
|
58
81
|
}
|
|
59
82
|
|
|
60
83
|
describe('H-1: feedHeartbeatTick liveness-open threshold', () => {
|
|
@@ -121,8 +144,8 @@ describe('H-1: feedHeartbeatTick liveness-open threshold', () => {
|
|
|
121
144
|
// The enqueue-time early-open (the dead-air fix): a one-shot timer fires the
|
|
122
145
|
// shared open helper at turn start, and it is cancelled at the canonical
|
|
123
146
|
// turn-end so a leaked timer can never fire against a successor turn.
|
|
124
|
-
expect(
|
|
125
|
-
expect(
|
|
147
|
+
expect(gatewayAndStreamSrc).toMatch(/scheduleEarlyLivenessOpen\(next\)/)
|
|
148
|
+
expect(gatewayAndStreamSrc).toMatch(/stopEarlyLivenessOpen\(key as string\)/)
|
|
126
149
|
})
|
|
127
150
|
})
|
|
128
151
|
|