switchroom 0.15.45 → 0.16.4
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 +122 -88
- package/dist/auth-broker/index.js +463 -177
- package/dist/cli/autoaccept-poll.js +4842 -35
- package/dist/cli/drive-write-pretool.mjs +17 -14
- package/dist/cli/notion-write-pretool.mjs +117 -86
- package/dist/cli/self-improve-apply-guard-pretool.mjs +626 -0
- package/dist/cli/self-improve-stop.mjs +428 -0
- package/dist/cli/skill-validate-pretool.mjs +72 -72
- package/dist/cli/switchroom.js +3158 -1178
- package/dist/host-control/main.js +2833 -355
- package/dist/vault/approvals/kernel-server.js +7479 -7439
- package/dist/vault/broker/server.js +11312 -11272
- package/examples/minimal.yaml +1 -0
- package/examples/switchroom.yaml +1 -0
- package/package.json +3 -3
- package/profiles/_base/start.sh.hbs +88 -1
- package/profiles/_shared/execution-discipline.md.hbs +18 -0
- package/profiles/default/CLAUDE.md.hbs +0 -19
- package/telegram-plugin/.claude-plugin/plugin.json +2 -2
- package/telegram-plugin/answer-stream-flag.ts +12 -49
- package/telegram-plugin/answer-stream.ts +5 -150
- package/telegram-plugin/auth-snapshot-format.ts +280 -48
- package/telegram-plugin/auto-fallback-fleet.ts +44 -1
- package/telegram-plugin/context-exhaustion.ts +12 -0
- package/telegram-plugin/demo-mask.ts +154 -0
- package/telegram-plugin/dist/bridge/bridge.js +167 -124
- package/telegram-plugin/dist/gateway/gateway.js +3039 -1159
- package/telegram-plugin/dist/server.js +215 -172
- package/telegram-plugin/docs/waiting-ux-spec.md +2 -2
- package/telegram-plugin/draft-stream.ts +47 -410
- package/telegram-plugin/final-answer-detect.ts +17 -12
- package/telegram-plugin/fleet-fallback-resume.ts +131 -0
- package/telegram-plugin/format.ts +56 -19
- package/telegram-plugin/gateway/auth-add-flow.ts +332 -127
- package/telegram-plugin/gateway/auth-broker-client.ts +2 -2
- package/telegram-plugin/gateway/auth-command.ts +70 -14
- package/telegram-plugin/gateway/clean-shutdown-marker.ts +44 -0
- package/telegram-plugin/gateway/config-approval-handler.test.ts +91 -4
- package/telegram-plugin/gateway/config-approval-handler.ts +94 -13
- package/telegram-plugin/gateway/current-turn-map.ts +188 -0
- package/telegram-plugin/gateway/disconnect-flush.ts +3 -1
- package/telegram-plugin/gateway/effort-command.ts +8 -3
- package/telegram-plugin/gateway/emission-authority.ts +369 -0
- package/telegram-plugin/gateway/feed-open-gate.ts +292 -0
- package/telegram-plugin/gateway/gateway.ts +1837 -291
- package/telegram-plugin/gateway/inject-handler.test.ts +2 -1
- package/telegram-plugin/gateway/ms365-write-approval.test.ts +4 -4
- package/telegram-plugin/gateway/represent-guard.ts +72 -0
- package/telegram-plugin/gateway/status-surface-log.test.ts +5 -4
- package/telegram-plugin/gateway/status-surface-log.ts +14 -3
- package/telegram-plugin/history.ts +33 -11
- package/telegram-plugin/hooks/repo-context-pretool.mjs +26 -0
- package/telegram-plugin/hooks/subagent-tracker-posttool.mjs +5 -0
- package/telegram-plugin/hooks/subagent-tracker-pretool.mjs +8 -0
- package/telegram-plugin/hooks/tool-label-pretool.mjs +39 -15
- package/telegram-plugin/issues-card.ts +4 -0
- package/telegram-plugin/model-unavailable.ts +124 -0
- package/telegram-plugin/narrative-dedup.ts +69 -0
- package/telegram-plugin/over-ping-safety-net.ts +70 -4
- package/telegram-plugin/package.json +3 -3
- package/telegram-plugin/pending-work-progress.ts +12 -0
- package/telegram-plugin/permission-rule.ts +32 -5
- package/telegram-plugin/permission-title.ts +152 -9
- package/telegram-plugin/quota-check.ts +13 -0
- package/telegram-plugin/quota-watch.ts +135 -7
- package/telegram-plugin/registry/turns-schema.test.ts +24 -0
- package/telegram-plugin/registry/turns-schema.ts +9 -0
- package/telegram-plugin/runtime-metrics.ts +13 -0
- package/telegram-plugin/session-tail.ts +96 -11
- package/telegram-plugin/silence-poke.ts +170 -24
- package/telegram-plugin/slot-banner-driver.ts +3 -0
- package/telegram-plugin/status-no-truncate.ts +44 -0
- package/telegram-plugin/status-reactions.ts +20 -3
- package/telegram-plugin/stream-controller.ts +4 -23
- package/telegram-plugin/stream-reply-handler.ts +6 -24
- package/telegram-plugin/streaming-metrics.ts +91 -0
- package/telegram-plugin/subagent-watcher.ts +212 -66
- package/telegram-plugin/tests/activity-ever-opened-sticky.test.ts +47 -0
- package/telegram-plugin/tests/answer-stream-dedup.test.ts +9 -26
- package/telegram-plugin/tests/answer-stream-flag.test.ts +25 -58
- package/telegram-plugin/tests/answer-stream-silent-markers.test.ts +41 -51
- package/telegram-plugin/tests/answer-stream.test.ts +2 -411
- package/telegram-plugin/tests/auth-add-flow.test.ts +488 -253
- package/telegram-plugin/tests/auth-command-format2.test.ts +71 -1
- package/telegram-plugin/tests/auth-snapshot-format.test.ts +376 -6
- package/telegram-plugin/tests/auto-fallback-fleet.test.ts +120 -0
- package/telegram-plugin/tests/cross-turn-card-gate.test.ts +424 -0
- package/telegram-plugin/tests/demo-mask.test.ts +127 -0
- package/telegram-plugin/tests/draft-stream.test.ts +0 -827
- package/telegram-plugin/tests/emission-authority-card-drain-gate.test.ts +236 -0
- package/telegram-plugin/tests/emission-authority-facade.test.ts +488 -0
- package/telegram-plugin/tests/emission-authority-open-gate.test.ts +179 -0
- package/telegram-plugin/tests/emission-authority-ping-gate.test.ts +395 -0
- package/telegram-plugin/tests/emission-determinism-wiring.test.ts +177 -0
- package/telegram-plugin/tests/feed-heartbeat-liveness-open.test.ts +146 -0
- package/telegram-plugin/tests/feed-open-gate.test.ts +259 -0
- package/telegram-plugin/tests/feed-survival.test.ts +526 -0
- package/telegram-plugin/tests/fleet-fallback-resume.test.ts +197 -0
- package/telegram-plugin/tests/gateway-clean-shutdown-marker.test.ts +117 -0
- package/telegram-plugin/tests/gateway-no-reply-single-emit.test.ts +4 -11
- package/telegram-plugin/tests/history.test.ts +60 -0
- package/telegram-plugin/tests/model-unavailable.test.ts +118 -0
- package/telegram-plugin/tests/narrative-dedup.test.ts +118 -0
- package/telegram-plugin/tests/orphaned-reply-rearm.test.ts +285 -0
- package/telegram-plugin/tests/over-ping-final-answer-decoupling.test.ts +194 -0
- package/telegram-plugin/tests/over-ping-safety-net.test.ts +2 -2
- package/telegram-plugin/tests/per-topic-current-turn.test.ts +373 -0
- package/telegram-plugin/tests/permission-card-origin-kill-switch.test.ts +42 -0
- package/telegram-plugin/tests/permission-rule.test.ts +17 -0
- package/telegram-plugin/tests/permission-title.test.ts +206 -17
- package/telegram-plugin/tests/quota-watch.test.ts +252 -9
- package/telegram-plugin/tests/reply-terminal-reaction.test.ts +6 -1
- package/telegram-plugin/tests/repo-context-pretool.test.ts +62 -0
- package/telegram-plugin/tests/represent-guard.test.ts +162 -0
- package/telegram-plugin/tests/session-tail.test.ts +147 -3
- package/telegram-plugin/tests/silence-liveness-wiring.test.ts +18 -0
- package/telegram-plugin/tests/status-card-budget-parity.test.ts +72 -0
- package/telegram-plugin/tests/status-surface-log.test.ts +146 -0
- package/telegram-plugin/tests/subagent-watcher-clip-narrative.test.ts +58 -0
- package/telegram-plugin/tests/subagent-watcher-parent-turn-key.test.ts +102 -0
- package/telegram-plugin/tests/subagent-watcher-workflow-visibility.test.ts +225 -0
- package/telegram-plugin/tests/subagent-watcher.test.ts +147 -0
- package/telegram-plugin/tests/telegram-activity-visibility-integration.test.ts +597 -0
- package/telegram-plugin/tests/telegram-format.test.ts +101 -6
- package/telegram-plugin/tests/tool-activity-summary.test.ts +550 -15
- package/telegram-plugin/tests/tool-label-pretool.test.ts +73 -0
- package/telegram-plugin/tests/tool-label-sidecar.test.ts +44 -0
- package/telegram-plugin/tests/tool-labels.test.ts +67 -0
- package/telegram-plugin/tests/turn-liveness-floor.test.ts +196 -0
- package/telegram-plugin/tests/turn-liveness-invariant.test.ts +340 -0
- package/telegram-plugin/tests/welcome-text.test.ts +32 -3
- package/telegram-plugin/tests/worker-activity-feed.test.ts +470 -22
- package/telegram-plugin/tool-activity-summary.ts +375 -58
- package/telegram-plugin/turn-liveness-floor.ts +240 -0
- package/telegram-plugin/uat/assertions.ts +115 -0
- package/telegram-plugin/uat/driver.ts +68 -0
- package/telegram-plugin/uat/scenarios/bg-sub-agent-dispatch-dm.test.ts +119 -133
- package/telegram-plugin/uat/scenarios/jtbd-answer-pings.test.ts +94 -0
- package/telegram-plugin/uat/scenarios/jtbd-cross-turn-card-dm.test.ts +109 -0
- package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-thinkgap-dm.test.ts +478 -0
- package/telegram-plugin/uat/scenarios/jtbd-foreground-feed-visibility-dm.test.ts +396 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-feed-open-dm.test.ts +202 -0
- package/telegram-plugin/uat/scenarios/jtbd-reply-is-last-dm.test.ts +202 -0
- package/telegram-plugin/uat/scenarios/reactions-dm.test.ts +93 -87
- package/telegram-plugin/welcome-text.ts +13 -1
- package/telegram-plugin/worker-activity-feed.ts +157 -82
- package/telegram-plugin/draft-transport.ts +0 -122
- package/telegram-plugin/tests/draft-retirement-wiring.test.ts +0 -82
- package/telegram-plugin/tests/draft-transport.test.ts +0 -211
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Feed-OPEN gate (deterministic activity-card OPEN gating).
|
|
3
|
+
*
|
|
4
|
+
* Pure decision: the activity-card drain (`drainActivitySummary`) is about to
|
|
5
|
+
* OPEN a brand-new card (`activityMessageId == null` → a fresh `sendMessage`).
|
|
6
|
+
* Should it be allowed to? This module encodes the two OPEN-suppression levers
|
|
7
|
+
* from `docs/message-emission-determinism.md` §9, so the gateway has ONE place
|
|
8
|
+
* to reason about WHEN a card may first appear on screen.
|
|
9
|
+
*
|
|
10
|
+
* Only the OPEN (first `sendMessage`) is gated. An EDIT of an already-open card
|
|
11
|
+
* (`activityMessageId != null`) is never blocked — once a card exists it must
|
|
12
|
+
* keep rendering, and an edit never reorders on screen.
|
|
13
|
+
*
|
|
14
|
+
* ## Lever 1 — no card OPEN after a substantive final (§9 lever 1, race A/B/E)
|
|
15
|
+
*
|
|
16
|
+
* Once a *substantive* final answer has been delivered this turn, no new card
|
|
17
|
+
* may open — it would land below the reply (higher message_id) and break the
|
|
18
|
+
* scoped "reply is last" invariant (§6). This keys on the STICKY
|
|
19
|
+
* `finalAnswerEverDelivered` latch, NOT the mutable `finalAnswerDelivered`
|
|
20
|
+
* (which the ack-reopen path clears mid-turn, `feed-reopen-gate.ts:157`). Keying
|
|
21
|
+
* on the mutable flag would be a no-op on exactly the ack-first turn where the
|
|
22
|
+
* reorder originates (design §9 preamble / R0). The sticky latch is set once a
|
|
23
|
+
* substantive final lands and is never cleared by reopen — so an "On it…" ack
|
|
24
|
+
* (non-substantive) does NOT trip it and the #2141 ack-then-work feed still
|
|
25
|
+
* opens.
|
|
26
|
+
*
|
|
27
|
+
* ## Lever 5 — INERT (pre-answer narrative OPEN now permitted, #2588)
|
|
28
|
+
*
|
|
29
|
+
* Lever 5 was added in #2557 to prevent a "triplication" reorder: a 0-tool
|
|
30
|
+
* conversational turn would open a card (message_id N), then the reply would
|
|
31
|
+
* send (message_id N+1) ABOVE the card, and then a Stop-hook re-prompt caused
|
|
32
|
+
* a second card below. The fix over-suppressed: it killed the card entirely
|
|
33
|
+
* rather than fixing the G2/G3 half of the triplication.
|
|
34
|
+
*
|
|
35
|
+
* **Lever 2 (`clearActivitySummary`) already owns reply-is-last ordering:** it
|
|
36
|
+
* edits the narrative card IN-PLACE before the reply chunks send, keeping its
|
|
37
|
+
* lower message_id and guaranteeing the reply lands above it — no reorder.
|
|
38
|
+
* Lever 5's open-suppression is therefore redundant and harms visibility:
|
|
39
|
+
* a pre-answer narrative on a 0-tool turn (the agent thinking aloud before
|
|
40
|
+
* dispatching tools) is exactly the kind of step operators want to see.
|
|
41
|
+
*
|
|
42
|
+
* Lever 5 is now INERT for the pre-answer case. Post-answer is already blocked
|
|
43
|
+
* by Lever 1 (`finalAnswerEverDelivered`), so Lever 5 was unreachable there
|
|
44
|
+
* anyway. The `labeledToolCount` field remains in `FeedOpenInput` — it is still
|
|
45
|
+
* used by R4 (a turn that starts conversational then dispatches a tool opens on
|
|
46
|
+
* the first label) and by tests.
|
|
47
|
+
*
|
|
48
|
+
* ## Lever 4 — no card OPEN below an EARLIER turn's answer (§9 lever 4, race C/D)
|
|
49
|
+
*
|
|
50
|
+
* The CROSS-turn case. Lever 1's `finalAnswerEverDelivered` latch is PER-TURN —
|
|
51
|
+
* it is reset to `false` at every turn ctor (mirroring `activityEverOpened`). So
|
|
52
|
+
* a SYNTHETIC turn born from a cross-turn surface — the `obligation_represent`
|
|
53
|
+
* re-delivery (`obligationSweep`, race D) or the liveness/heartbeat timer firing
|
|
54
|
+
* on it — starts with a cleared latch EVEN WHEN a substantive answer already
|
|
55
|
+
* reached the user in an EARLIER turn of this exchange. Its first drain would
|
|
56
|
+
* then OPEN a "thinking…" card BELOW that already-delivered reply (higher
|
|
57
|
+
* message_id → reorders, breaking the scoped "reply is last" invariant §6 across
|
|
58
|
+
* the turn boundary). This is the cousin of the §5 represent-duplicate bug, at
|
|
59
|
+
* the card layer instead of the message layer.
|
|
60
|
+
*
|
|
61
|
+
* Lever 4 closes it with a cross-turn signal the CALLER computes and passes in:
|
|
62
|
+
* `crossTurnAnswerDelivered` is true iff (a) this is a cross-turn synthetic
|
|
63
|
+
* surface (a represent / owed-reply turn — NOT the foreground turn's own card)
|
|
64
|
+
* AND (b) a SUBSTANTIVE (≥`FINAL_ANSWER_MIN_CHARS`) outbound reply has already
|
|
65
|
+
* been delivered to this chat SINCE THE OBLIGATION/TIMER WAS RAISED — checked via
|
|
66
|
+
* the SAME `hasOutboundDeliveredSince` history predicate the represent guard uses
|
|
67
|
+
* (`represent-guard.ts`), not a parallel mechanism. When true, NO producer may
|
|
68
|
+
* OPEN a card; edits of an already-open id are still allowed. Checked FIRST.
|
|
69
|
+
*
|
|
70
|
+
* Three deliberate scoping properties keep it from over-firing:
|
|
71
|
+
* - SCOPED to synthetic cross-turn surfaces by the caller (a normal foreground
|
|
72
|
+
* turn passes `false`), so it can never suppress the foreground turn's card.
|
|
73
|
+
* - Keys on SUBSTANTIVE delivery (the 200-char proxy), never on an ack — so it
|
|
74
|
+
* does NOT regress #2141: an ack-then-work turn has no substantive row and
|
|
75
|
+
* its feed still opens.
|
|
76
|
+
* - Does NOT touch the represent SEND itself — only the decorative card. A
|
|
77
|
+
* genuinely-unanswered obligation is STILL re-asked (the represent guard, not
|
|
78
|
+
* this gate, owns suppressing an already-satisfied represent). This gate only
|
|
79
|
+
* suppresses the card that would otherwise narrate beneath an answer the user
|
|
80
|
+
* already received.
|
|
81
|
+
*/
|
|
82
|
+
|
|
83
|
+
/** Which producer triggered this drain — determines lever-5 OPEN eligibility. */
|
|
84
|
+
export type FeedOpenProducer =
|
|
85
|
+
/** Narrative SHOW (producer A): plain assistant text, no tool, no time
|
|
86
|
+
* threshold. May only EDIT, never OPEN, while the turn has 0 tool labels. */
|
|
87
|
+
| 'narrative'
|
|
88
|
+
/** Tool label (producer B): the model dispatched a tool. OPEN-eligible unless a
|
|
89
|
+
* substantive final already landed (lever 1).
|
|
90
|
+
* Foreground sub-agent renders (a Task tool) are tool work too → 'tool'. */
|
|
91
|
+
| 'tool'
|
|
92
|
+
/** Liveness timer (producer C): a genuine ≥12s thinking-gap open, or the
|
|
93
|
+
* labelled-feed heartbeat maintaining an open card. OPEN-eligible unless a
|
|
94
|
+
* substantive final already landed (lever 1). */
|
|
95
|
+
| 'liveness'
|
|
96
|
+
|
|
97
|
+
export interface FeedOpenInput {
|
|
98
|
+
/** Which producer triggered the drain (see FeedOpenProducer). */
|
|
99
|
+
producer: FeedOpenProducer
|
|
100
|
+
/** Sticky latch: has a *substantive* final answer ever been delivered this
|
|
101
|
+
* turn? Set once and never cleared by reopen (lever 1 / R0). */
|
|
102
|
+
finalAnswerEverDelivered: boolean
|
|
103
|
+
/** Count of surfaced tool steps this turn (`turn.labeledToolCount`). 0 means
|
|
104
|
+
* no tool has ever produced a label — pure conversation / thinking. */
|
|
105
|
+
labeledToolCount: number
|
|
106
|
+
/** Lever 4 (cross-turn / race C/D). True iff this is a cross-turn SYNTHETIC
|
|
107
|
+
* surface (an `obligation_represent` / owed-reply turn, or the liveness timer
|
|
108
|
+
* firing on it) AND a SUBSTANTIVE reply has already been delivered to this
|
|
109
|
+
* chat since the obligation/timer was raised — computed by the caller via the
|
|
110
|
+
* reused `hasOutboundDeliveredSince` history predicate with the obligation's
|
|
111
|
+
* `openedAt` cutoff. When true, no producer may OPEN a card (it would land
|
|
112
|
+
* below the earlier reply). A normal foreground turn passes `false` (or omits
|
|
113
|
+
* it), so lever 4 is inert there. Defaults to `false`. */
|
|
114
|
+
crossTurnAnswerDelivered?: boolean
|
|
115
|
+
/**
|
|
116
|
+
* Fix 2 (post-answer background-agent liveness, #2587 supersede): true when
|
|
117
|
+
* the sub-agent/workflow watcher has produced a NEW activity step AFTER the
|
|
118
|
+
* substantive final answer was delivered. This signal is updated by the watcher
|
|
119
|
+
* callback INDEPENDENTLY of the tool_label path, so the drop-guard
|
|
120
|
+
* (`shouldReopenFeedAfterAck` / `turn.finalAnswerSubstantive`) does NOT gate it.
|
|
121
|
+
*
|
|
122
|
+
* When true AND `producer === 'tool'`, Lever 1's blanket post-answer block is
|
|
123
|
+
* lifted: a liveness card surfaces below the reply to show "background agent
|
|
124
|
+
* still working". Idle producers (`liveness`, `narrative`) remain blocked after
|
|
125
|
+
* the final answer — the reply-is-last invariant is preserved for idle gaps.
|
|
126
|
+
* Only the `feedHeartbeatTick` post-answer branch sets this; it reads
|
|
127
|
+
* `turn.subagentActivityAt > (turn.finalAnswerDeliveredAt ?? 0)` (a tool-label
|
|
128
|
+
* rendered genuinely after the answer) rather than the frozen `lastToolLabelAt`.
|
|
129
|
+
*
|
|
130
|
+
* Defaults to `false` (Lever 1 stays fully active) — callers that don't pass
|
|
131
|
+
* this see no behaviour change.
|
|
132
|
+
*/
|
|
133
|
+
postAnswerSubagentActivity?: boolean
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Pure. Given the drain is about to OPEN a fresh card, returns true when the
|
|
138
|
+
* OPEN is allowed. An EDIT (caller has a non-null `activityMessageId`) does NOT
|
|
139
|
+
* consult this — only the OPEN branch does.
|
|
140
|
+
*
|
|
141
|
+
* - crossTurnAnswerDelivered → false: lever 4. A cross-turn synthetic surface
|
|
142
|
+
* whose exchange already delivered a substantive answer in an EARLIER turn;
|
|
143
|
+
* no card may open below it (any producer). Checked FIRST.
|
|
144
|
+
* - finalAnswerEverDelivered && !postAnswerSubagentActivity → false: lever 1.
|
|
145
|
+
* A substantive final already landed THIS turn; no card may open below it.
|
|
146
|
+
* Exception: when `postAnswerSubagentActivity === true` AND `producer ===
|
|
147
|
+
* 'tool'`, Lever 1 is lifted so the background-agent liveness heartbeat can
|
|
148
|
+
* surface a card below the reply showing the watcher's real new activity.
|
|
149
|
+
* Idle producers ('liveness', 'narrative') stay blocked — no card opens from
|
|
150
|
+
* wall-clock alone after the final answer.
|
|
151
|
+
* - producer 'narrative': always allowed when pre-answer (lever 5 is INERT —
|
|
152
|
+
* Lever 2 / clearActivitySummary guarantees reply-is-last ordering instead).
|
|
153
|
+
* - producer 'tool' or 'liveness' → true (unless lever 1/4).
|
|
154
|
+
*/
|
|
155
|
+
export function mayOpenActivityCard(input: FeedOpenInput): boolean {
|
|
156
|
+
// Lever 4 — cross-turn: nothing opens on a synthetic represent/owed-reply
|
|
157
|
+
// surface whose exchange already delivered a substantive answer in an earlier
|
|
158
|
+
// turn (race C/D). Checked FIRST, above lever 1, and scoped to cross-turn
|
|
159
|
+
// synthetic surfaces by the caller so it can never fire on a foreground turn.
|
|
160
|
+
if (input.crossTurnAnswerDelivered) return false
|
|
161
|
+
// Lever 1 — sticky: nothing opens after a substantive final answer, EXCEPT
|
|
162
|
+
// when genuine post-answer sub-agent/watcher activity warrants a liveness card
|
|
163
|
+
// (Fix 2 / #2587 supersede). Only 'tool' is exempted so idle liveness and
|
|
164
|
+
// narrative producers remain blocked after the final answer.
|
|
165
|
+
if (input.finalAnswerEverDelivered) {
|
|
166
|
+
if (input.postAnswerSubagentActivity && input.producer === 'tool') return true
|
|
167
|
+
return false
|
|
168
|
+
}
|
|
169
|
+
// Lever 5 — INERT (see module comment above). Pre-answer narrative may now
|
|
170
|
+
// open a card; Lever 2 (clearActivitySummary) handles reply-is-last ordering.
|
|
171
|
+
return true
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Injected dependencies for the lever-4 cross-turn history check (PR-4b).
|
|
176
|
+
*
|
|
177
|
+
* Passed in EXPLICITLY so `feed-open-gate.ts` stays sqlite-free — it does NOT
|
|
178
|
+
* import `history.js`, so the module (and its vitest suite) never transitively
|
|
179
|
+
* pulls in `bun:sqlite`. The gateway centralizes the wiring (the real
|
|
180
|
+
* `hasOutboundDeliveredSince` predicate + `HISTORY_ENABLED` + the substantive
|
|
181
|
+
* `FINAL_ANSWER_MIN_CHARS` floor) in one place (`emissionAuthorityFor`); tests
|
|
182
|
+
* inject the real history harness (`cross-turn-card-gate.test.ts`) or a stub.
|
|
183
|
+
*/
|
|
184
|
+
export interface FeedOpenGateDeps {
|
|
185
|
+
/** The represent-guard's delivered-since predicate (`history.ts`). Returns
|
|
186
|
+
* true iff a ≥`minChars` outbound landed in `chatId`/`threadId` since
|
|
187
|
+
* `sinceMs`. Injected so this module never imports `history.js`. */
|
|
188
|
+
hasOutboundDeliveredSince: (
|
|
189
|
+
chatId: string,
|
|
190
|
+
sinceMs: number,
|
|
191
|
+
threadId: number | null | undefined,
|
|
192
|
+
minChars: number,
|
|
193
|
+
) => boolean
|
|
194
|
+
/** Whether history is enabled (the gateway's `HISTORY_ENABLED`). When false,
|
|
195
|
+
* the cross-turn check short-circuits to false — exactly as the drain does. */
|
|
196
|
+
historyEnabled: boolean
|
|
197
|
+
/** The substantive 200-char floor (`FINAL_ANSWER_MIN_CHARS`) — an ack never
|
|
198
|
+
* trips lever 4 (keeps #2141 green). */
|
|
199
|
+
finalAnswerMinChars: number
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The per-turn surface the OPEN verdict reads. A structural subset of
|
|
204
|
+
* `CurrentTurn` — kept SEPARATE from the façade's minimal `EmissionTurnView`
|
|
205
|
+
* (which is scoped to the single-flight `activityInFlight` read) so neither
|
|
206
|
+
* leaks the other's concern. Mirrors the exact fields the drain's inline
|
|
207
|
+
* cross-turn computation + `mayOpenActivityCard` consult.
|
|
208
|
+
*/
|
|
209
|
+
export interface FeedOpenGateView {
|
|
210
|
+
/** Single in-place card transport id. `null` ⇒ the next drain would OPEN a
|
|
211
|
+
* fresh card; non-null ⇒ an EDIT (never gated). */
|
|
212
|
+
activityMessageId: number | null
|
|
213
|
+
/** Sticky lever-1 latch (per-turn). */
|
|
214
|
+
finalAnswerEverDelivered: boolean
|
|
215
|
+
/** Surfaced tool steps this turn (lever-5 base case). */
|
|
216
|
+
labeledToolCount: number
|
|
217
|
+
/** Present ONLY on a cross-turn synthetic surface (represent / owed-reply).
|
|
218
|
+
* Carries the obligation's `openedAt` as the delivered-since cutoff. A
|
|
219
|
+
* foreground turn omits it, so lever 4 is inert there. */
|
|
220
|
+
crossTurnGate?: { sinceMs: number }
|
|
221
|
+
/** Chat the card targets (null ⇒ no history query). */
|
|
222
|
+
sessionChatId: string | null
|
|
223
|
+
/** Forum-topic thread, if any (scopes the delivered-since check). */
|
|
224
|
+
sessionThreadId?: number | null
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Lever-4 cross-turn predicate — PURE, lifted VERBATIM from the inline
|
|
229
|
+
* computation `drainActivitySummary` ran in main (the OPEN branch). True iff
|
|
230
|
+
* this drain is about to OPEN a fresh card (`activityMessageId == null`) on a
|
|
231
|
+
* cross-turn synthetic surface (`crossTurnGate != null`) whose exchange already
|
|
232
|
+
* delivered a SUBSTANTIVE reply since the obligation was raised. Identical
|
|
233
|
+
* inputs ⇒ identical result to the drain's own (now-redundant) gate, so flag-ON
|
|
234
|
+
* and flag-OFF agree by construction.
|
|
235
|
+
*/
|
|
236
|
+
export function computeCrossTurnAnswerDelivered(
|
|
237
|
+
view: FeedOpenGateView,
|
|
238
|
+
deps: FeedOpenGateDeps,
|
|
239
|
+
): boolean {
|
|
240
|
+
return (
|
|
241
|
+
view.activityMessageId == null
|
|
242
|
+
&& view.crossTurnGate != null
|
|
243
|
+
&& view.sessionChatId != null
|
|
244
|
+
&& deps.historyEnabled
|
|
245
|
+
&& deps.hasOutboundDeliveredSince(
|
|
246
|
+
view.sessionChatId,
|
|
247
|
+
view.crossTurnGate.sinceMs,
|
|
248
|
+
view.sessionThreadId,
|
|
249
|
+
deps.finalAnswerMinChars,
|
|
250
|
+
)
|
|
251
|
+
)
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/** The OPEN-gate verdict for a turn view + producer (PR-4b). */
|
|
255
|
+
export interface FeedOpenVerdict {
|
|
256
|
+
/** A card is already open (`activityMessageId != null`) ⇒ this drain EDITs,
|
|
257
|
+
* which is NEVER gated. The façade applies unconditionally when `isOpen`. */
|
|
258
|
+
isOpen: boolean
|
|
259
|
+
/** Whether an OPEN would be allowed — the EXACT `mayOpenActivityCard(...)`
|
|
260
|
+
* result the drain computes (lever 1 + lever 4 + lever 5). When `!isOpen`
|
|
261
|
+
* and `!mayOpen`, the drain `break`s (refuses the OPEN); the façade skips
|
|
262
|
+
* `apply()`. When `isOpen`, this is consulted only by the equivalence test. */
|
|
263
|
+
mayOpen: boolean
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
/**
|
|
267
|
+
* Compute the OPEN-gate verdict for a turn view + producer (PR-4b). PURE: wraps
|
|
268
|
+
* `mayOpenActivityCard` over `computeCrossTurnAnswerDelivered`, with history
|
|
269
|
+
* deps injected. Returns BOTH `isOpen` (is a card already open → EDIT, never
|
|
270
|
+
* gated) and `mayOpen` (the raw `mayOpenActivityCard` verdict).
|
|
271
|
+
*
|
|
272
|
+
* The façade relocates main's drain decision with this: main refuses (and
|
|
273
|
+
* `break`s) iff `activityMessageId == null && !mayOpenActivityCard(...)`, i.e.
|
|
274
|
+
* `!isOpen && !mayOpen`. So the façade calls `apply()` iff `isOpen || mayOpen`
|
|
275
|
+
* — exactly the cases main did NOT `break`. Same pure inputs ⇒ same verdict on
|
|
276
|
+
* flag-ON and flag-OFF, so no emitted message differs in either flag state.
|
|
277
|
+
*/
|
|
278
|
+
export function computeFeedOpenVerdict(
|
|
279
|
+
view: FeedOpenGateView,
|
|
280
|
+
producer: FeedOpenProducer,
|
|
281
|
+
deps: FeedOpenGateDeps,
|
|
282
|
+
): FeedOpenVerdict {
|
|
283
|
+
const isOpen = view.activityMessageId != null
|
|
284
|
+
const crossTurnAnswerDelivered = computeCrossTurnAnswerDelivered(view, deps)
|
|
285
|
+
const mayOpen = mayOpenActivityCard({
|
|
286
|
+
producer,
|
|
287
|
+
finalAnswerEverDelivered: view.finalAnswerEverDelivered,
|
|
288
|
+
labeledToolCount: view.labeledToolCount,
|
|
289
|
+
crossTurnAnswerDelivered,
|
|
290
|
+
})
|
|
291
|
+
return { isOpen, mayOpen }
|
|
292
|
+
}
|