switchroom 0.19.25 โ 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 +61 -2
- package/dist/auth-broker/index.js +125 -8
- package/dist/cli/notion-write-pretool.mjs +61 -2
- package/dist/cli/switchroom.js +2347 -1104
- package/dist/host-control/main.js +126 -9
- package/dist/vault/approvals/kernel-server.js +124 -8
- package/dist/vault/broker/server.js +124 -8
- package/package.json +6 -2
- package/profiles/_base/cron-session.sh.hbs +14 -0
- package/profiles/_base/start.sh.hbs +145 -4
- package/telegram-plugin/card-layout.ts +328 -0
- package/telegram-plugin/dist/bridge/bridge.js +93 -1
- package/telegram-plugin/dist/gateway/gateway.js +2213 -1204
- 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 +103 -112
- 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/periodic-sweep-guard.ts +86 -0
- package/telegram-plugin/gateway/pinned-message-handler.ts +12 -16
- package/telegram-plugin/gateway/status-pin-retarget.ts +180 -0
- 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 +11 -0
- package/telegram-plugin/status-pin-driver.ts +33 -17
- package/telegram-plugin/status-pin.ts +51 -5
- 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 +291 -0
- 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/periodic-sweep-guard.test.ts +151 -0
- package/telegram-plugin/tests/pinned-card-collapse.test.ts +24 -18
- 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 +244 -0
- package/telegram-plugin/tests/status-pin-service-message-suppression.test.ts +7 -3
- package/telegram-plugin/tests/status-pin-shutdown-wiring.test.ts +94 -0
- package/telegram-plugin/tests/status-pin-store.test.ts +179 -64
- package/telegram-plugin/tests/status-pin.test.ts +184 -7
- 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 +29 -19
- 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/tier-downgrade.ts +3 -2
- package/telegram-plugin/tool-activity-summary.ts +239 -322
- package/telegram-plugin/uat/assertions.ts +33 -3
- package/telegram-plugin/uat/feed-matcher.test.ts +36 -0
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-channel.test.ts +9 -2
- package/telegram-plugin/uat/scenarios/jtbd-liveness-narration-dm.test.ts +9 -2
- package/telegram-plugin/worker-activity-feed.ts +109 -30
- 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/scripts/tests/test_recall_request_timeout.py +241 -0
- package/vendor/hindsight-memory/settings.json +1 -1
|
@@ -23,8 +23,15 @@ import type { Driver, ObservedMessage, ObservedReaction } from "./driver.js";
|
|
|
23
23
|
*
|
|
24
24
|
* Single source of truth; the worker-feed scenario asserts against this,
|
|
25
25
|
* and recall/reply scenarios exclude it via {@link isWorkerFeedMessage}.
|
|
26
|
+
*
|
|
27
|
+
* `Workers?` is load-bearing, not cosmetic: the COMBINED (2+ worker) card's
|
|
28
|
+
* glance line reads `๐ WORKERS ยท N running ยท โฆ`, and `Worker\b` cannot match
|
|
29
|
+
* inside `WORKERS` (no word boundary between `r` and `S`). Without the optional
|
|
30
|
+
* `s` the whole 2+ worker surface was invisible to this predicate, so a
|
|
31
|
+
* recall/reply scenario running with two live workers could latch onto the
|
|
32
|
+
* combined card as the agent's answer. Found during the #3842 card audit.
|
|
26
33
|
*/
|
|
27
|
-
export const WORKER_FEED_RE = /๐ [๏ธ]?\s*
|
|
34
|
+
export const WORKER_FEED_RE = /๐ [๏ธ]?\s*Workers?\b|finished\s*ยท\s*(?:completed|failed)/i;
|
|
28
35
|
|
|
29
36
|
/**
|
|
30
37
|
* True when `m` is a worker-activity-feed message rather than the agent's
|
|
@@ -72,6 +79,29 @@ const ACTIVITY_BODY_LINE_RE = /^[โโโณ]/u;
|
|
|
72
79
|
* Elapsed is `formatFeedElapsed`: `<N>s` under a minute, else `<M>m<SS>s`.
|
|
73
80
|
*/
|
|
74
81
|
const LIVENESS_HEADER_L1_RE = /^(?:๐ค|๐ [๏ธ]?|โ[๏ธ]?)\s+\S/u;
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Leading indent chrome a card line can carry: a `U+2800` run, applied by
|
|
85
|
+
* `WORKER_STEP_INDENT` to the step lines under each worker header on the
|
|
86
|
+
* combined (2+ worker) card so one worker's steps are visibly separated from
|
|
87
|
+
* the next worker's.
|
|
88
|
+
*
|
|
89
|
+
* Every line-shape matcher below must normalise it away first: Telegram
|
|
90
|
+
* delivers it verbatim and `String.trim()` does NOT remove it โ U+2800 is
|
|
91
|
+
* category So, not whitespace (that property is exactly why it survives
|
|
92
|
+
* Telegram's own left-trim; see WORKER_STEP_INDENT). Without this, a worker
|
|
93
|
+
* card's body lines stop matching `ACTIVITY_BODY_LINE_RE`.
|
|
94
|
+
*
|
|
95
|
+
* #3842 removed the whole-card `โโ ` header prefix and the whole-card indent
|
|
96
|
+
* that #3820/#3821 put on worker cards, so this no longer has to strip a `โโ `.
|
|
97
|
+
* The step indent is the only nesting a card emits now.
|
|
98
|
+
*/
|
|
99
|
+
const CARD_INDENT_PREFIX_RE = /^\u2800+/u;
|
|
100
|
+
|
|
101
|
+
/** A card line with its leading step-indent chrome removed, trimmed. */
|
|
102
|
+
export function stripCardNesting(line: string): string {
|
|
103
|
+
return line.replace(CARD_INDENT_PREFIX_RE, "").trim();
|
|
104
|
+
}
|
|
75
105
|
const LIVENESS_ELAPSED = String.raw`(?:\d+m)?\d+s`;
|
|
76
106
|
const LIVENESS_HEADER_L2_RE = new RegExp(
|
|
77
107
|
`^(?:${LIVENESS_ELAPSED}\\s*ยท\\s*\\d+\\s+tools?` +
|
|
@@ -94,7 +124,7 @@ const LIVENESS_HEADER_L2_RE = new RegExp(
|
|
|
94
124
|
export function isLivenessCardMessage(m: ObservedMessage): boolean {
|
|
95
125
|
const lines = m.text
|
|
96
126
|
.split("\n")
|
|
97
|
-
.map((l) => l
|
|
127
|
+
.map((l) => stripCardNesting(l))
|
|
98
128
|
.filter((l) => l.length > 0);
|
|
99
129
|
if (lines.length < 2) return false;
|
|
100
130
|
if (!LIVENESS_HEADER_L1_RE.test(lines[0])) return false;
|
|
@@ -124,7 +154,7 @@ export function isLivenessCardMessage(m: ObservedMessage): boolean {
|
|
|
124
154
|
export function isActivityFeedMessage(m: ObservedMessage): boolean {
|
|
125
155
|
const lines = m.text
|
|
126
156
|
.split("\n")
|
|
127
|
-
.map((l) => l
|
|
157
|
+
.map((l) => stripCardNesting(l))
|
|
128
158
|
.filter((l) => l.length > 0);
|
|
129
159
|
if (lines.length === 0) return false;
|
|
130
160
|
if (lines.every((l) => ACTIVITY_FEED_LINE_RE.test(l))) return true;
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
isFrameworkFallbackText,
|
|
5
5
|
isLivenessCardMessage,
|
|
6
6
|
isWorkerFeedMessage,
|
|
7
|
+
stripCardNesting,
|
|
7
8
|
WORKER_FEED_RE,
|
|
8
9
|
} from "./assertions.js";
|
|
9
10
|
|
|
@@ -50,6 +51,41 @@ describe("isWorkerFeedMessage", () => {
|
|
|
50
51
|
it("exposes the regex for scenarios that assert on the feed directly", () => {
|
|
51
52
|
expect(WORKER_FEED_RE.test("๐ Worker ยท x")).toBe(true);
|
|
52
53
|
});
|
|
54
|
+
|
|
55
|
+
it("matches the FLUSH worker card as Telegram delivers it (#3842)", () => {
|
|
56
|
+
// #3842 removed the whole-card `โโ ` prefix and whole-card indent that
|
|
57
|
+
// #3820/#3821 put on worker cards, so the single-worker card arrives flush.
|
|
58
|
+
const flush = "๐ WORKER ยท crawling changelog\n55s ยท 9 tools ยท opus 5";
|
|
59
|
+
expect(isWorkerFeedMessage(feed(flush))).toBe(true);
|
|
60
|
+
});
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
describe("#3842 the surviving step indent is transparent to the matchers", () => {
|
|
64
|
+
it("classifies a flush agent-shaped worker card as the liveness card, not an answer", () => {
|
|
65
|
+
const flush =
|
|
66
|
+
"๐ WORKER ยท crawl\n12s ยท 3 tools\nโ Reading CLAUDE.md\nโ Searching memory";
|
|
67
|
+
expect(isLivenessCardMessage(feed(flush))).toBe(true);
|
|
68
|
+
expect(isActivityFeedMessage(feed(flush))).toBe(true);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("classifies a combined card whose step lines carry WORKER_STEP_INDENT", () => {
|
|
72
|
+
// The one indent that survives #3842: step lines under a worker row header
|
|
73
|
+
// on the 2+ worker card. `String.trim()` does NOT remove U+2800 (category
|
|
74
|
+
// So, not whitespace), so the matchers must normalise it explicitly or a
|
|
75
|
+
// recall/reply scenario latches onto the combined card as an answer.
|
|
76
|
+
const combined =
|
|
77
|
+
"๐ WORKERS ยท 2 running ยท oldest 55s ยท 13 tools\n" +
|
|
78
|
+
"1. crawl ยท 55s ยท 9 tools\nโ โ โ โ Reading CLAUDE.md\nโ โ โ โ Searching memory";
|
|
79
|
+
expect(isWorkerFeedMessage(feed(combined))).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("strips the step indent and leaves ordinary prose alone", () => {
|
|
83
|
+
expect(stripCardNesting("โ โ โ โ Searching memory")).toBe("โ Searching memory");
|
|
84
|
+
expect(stripCardNesting("๐ WORKER ยท crawl")).toBe("๐ WORKER ยท crawl");
|
|
85
|
+
expect(stripCardNesting("on it, pulling the logs now")).toBe(
|
|
86
|
+
"on it, pulling the logs now",
|
|
87
|
+
);
|
|
88
|
+
});
|
|
53
89
|
});
|
|
54
90
|
|
|
55
91
|
describe("isActivityFeedMessage", () => {
|
|
@@ -11,7 +11,11 @@
|
|
|
11
11
|
|
|
12
12
|
import { describe, expect, it } from "vitest";
|
|
13
13
|
import { spinUp } from "../harness.js";
|
|
14
|
-
import {
|
|
14
|
+
import {
|
|
15
|
+
isActivityFeedMessage,
|
|
16
|
+
isFrameworkFallbackText,
|
|
17
|
+
stripCardNesting,
|
|
18
|
+
} from "../assertions.js";
|
|
15
19
|
import type { ObservedMessage } from "../driver.js";
|
|
16
20
|
|
|
17
21
|
const AGENT = "test-harness";
|
|
@@ -30,7 +34,10 @@ const NARRATED_WORK_PROMPT =
|
|
|
30
34
|
function narratedBodyLines(text: string): string[] {
|
|
31
35
|
return text
|
|
32
36
|
.split("\n")
|
|
33
|
-
|
|
37
|
+
// strip the #3820 subordinate-card nesting (`โโ ` / U+2800 indent) first โ
|
|
38
|
+
// `trim()` removes neither, and a worker card's header/body lines would
|
|
39
|
+
// otherwise read as model narration
|
|
40
|
+
.map((l) => stripCardNesting(l))
|
|
34
41
|
.filter((l) => l.length > 0)
|
|
35
42
|
.map((l) => l.replace(/^[โโโณ]+\s*/u, "").trim())
|
|
36
43
|
.filter(
|
|
@@ -17,7 +17,11 @@
|
|
|
17
17
|
|
|
18
18
|
import { describe, expect, it } from "vitest";
|
|
19
19
|
import { spinUp } from "../harness.js";
|
|
20
|
-
import {
|
|
20
|
+
import {
|
|
21
|
+
isActivityFeedMessage,
|
|
22
|
+
isFrameworkFallbackText,
|
|
23
|
+
stripCardNesting,
|
|
24
|
+
} from "../assertions.js";
|
|
21
25
|
import type { ObservedMessage } from "../driver.js";
|
|
22
26
|
|
|
23
27
|
/**
|
|
@@ -30,7 +34,10 @@ import type { ObservedMessage } from "../driver.js";
|
|
|
30
34
|
function narratedBodyLines(text: string): string[] {
|
|
31
35
|
return text
|
|
32
36
|
.split("\n")
|
|
33
|
-
|
|
37
|
+
// strip the #3820 subordinate-card nesting (`โโ ` / U+2800 indent) first โ
|
|
38
|
+
// `trim()` removes neither, and a worker card's header/body lines would
|
|
39
|
+
// otherwise read as model narration
|
|
40
|
+
.map((l) => stripCardNesting(l))
|
|
34
41
|
.filter((l) => l.length > 0)
|
|
35
42
|
// strip glyphs so a `โ Workingโฆ` placeholder compares as `Workingโฆ`
|
|
36
43
|
.map((l) => l.replace(/^[โโโณ]+\s*/u, "").trim())
|
|
@@ -53,9 +53,9 @@ import {
|
|
|
53
53
|
cleanWorkerResultParagraph,
|
|
54
54
|
stripMarkdown,
|
|
55
55
|
truncate,
|
|
56
|
-
COLLAPSE_SAFE_SEPARATOR,
|
|
57
56
|
} from './card-format.js'
|
|
58
57
|
import { WORKER_HISTORY_MAX } from './status-no-truncate.js'
|
|
58
|
+
import { renderCardTitleLine } from './card-layout.js'
|
|
59
59
|
import {
|
|
60
60
|
renderStatusCard,
|
|
61
61
|
formatStepSuffix,
|
|
@@ -126,6 +126,49 @@ export interface WorkerActivityView {
|
|
|
126
126
|
model?: string
|
|
127
127
|
}
|
|
128
128
|
|
|
129
|
+
/**
|
|
130
|
+
* Out-of-band metadata about ONE feed edit, handed to the transport adapter
|
|
131
|
+
* alongside the Bot API arguments (switchroom#3848).
|
|
132
|
+
*
|
|
133
|
+
* Deliberately a SEPARATE parameter rather than a field on `opts`: `opts` is
|
|
134
|
+
* forwarded verbatim to `editMessageText` and therefore onto the wire, and
|
|
135
|
+
* `outbound-class.ts` exists precisely so a priority signal never has to be
|
|
136
|
+
* smuggled into an outbound Bot API payload.
|
|
137
|
+
*/
|
|
138
|
+
export interface WorkerFeedEditMeta {
|
|
139
|
+
/**
|
|
140
|
+
* True when this edit paints the card's FINAL state โ the last worker's
|
|
141
|
+
* terminal recap, or the "superseded" note on a rotated-out message. False
|
|
142
|
+
* for every intermediate / liveness repaint.
|
|
143
|
+
*/
|
|
144
|
+
terminal: boolean
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* The send-gate priority class a worker-feed edit must carry.
|
|
149
|
+
*
|
|
150
|
+
* Intermediate repaints are `cosmetic`: dropping one costs nothing, because
|
|
151
|
+
* the next render carries full state, and holding them to the fuse's tight
|
|
152
|
+
* cosmetic ceilings (4/message/60s, 6/chat/60s) is what stops the feed
|
|
153
|
+
* earning a per-chat flood ban (#3847).
|
|
154
|
+
*
|
|
155
|
+
* A TERMINAL frame is `useful` (#3848). It is the last frame the operator
|
|
156
|
+
* ever reads for that card and nothing newer is coming to repaint it, so
|
|
157
|
+
* classing it cosmetic made the one frame carrying the finished state
|
|
158
|
+
* compete in โ and get shed from โ the same starved budget as a heartbeat.
|
|
159
|
+
* `useful` rather than `critical` matches the activity-summary finalize in
|
|
160
|
+
* `gateway/narrative-lane.ts`: it may be DEFERRED under pressure, but it is
|
|
161
|
+
* not shed, and it does not spend the per-chat reply reservation that keeps
|
|
162
|
+
* the operator's real answer unstarvable.
|
|
163
|
+
*
|
|
164
|
+
* Exported so the mapping is unit-testable; `gateway.ts` only calls it.
|
|
165
|
+
*/
|
|
166
|
+
export function workerFeedEditPriorityClass(
|
|
167
|
+
meta?: WorkerFeedEditMeta,
|
|
168
|
+
): 'useful' | 'cosmetic' {
|
|
169
|
+
return meta?.terminal === true ? 'useful' : 'cosmetic'
|
|
170
|
+
}
|
|
171
|
+
|
|
129
172
|
export interface BotApiForWorkerFeed {
|
|
130
173
|
sendMessage(
|
|
131
174
|
chatId: string,
|
|
@@ -137,6 +180,12 @@ export interface BotApiForWorkerFeed {
|
|
|
137
180
|
messageId: number,
|
|
138
181
|
text: string,
|
|
139
182
|
opts?: Record<string, unknown>,
|
|
183
|
+
/**
|
|
184
|
+
* Out-of-band edit metadata (NOT part of the Bot API payload). The
|
|
185
|
+
* gateway adapter maps `terminal` to the send-gate priority class via
|
|
186
|
+
* {@link workerFeedEditPriorityClass}.
|
|
187
|
+
*/
|
|
188
|
+
meta?: WorkerFeedEditMeta,
|
|
140
189
|
): Promise<unknown>
|
|
141
190
|
}
|
|
142
191
|
|
|
@@ -164,22 +213,32 @@ export function repeatCountOf(line: string): number {
|
|
|
164
213
|
|
|
165
214
|
/**
|
|
166
215
|
* Thin adapter over the unified `renderStatusCard` primitive (emoji ๐ , label
|
|
167
|
-
* '
|
|
168
|
-
* stripMarkdown โ collapse ws โ clip โ escape per line),
|
|
169
|
-
* the cleaned result paragraph as the `result` block.
|
|
216
|
+
* 'WORKER'): builds the header, passes raw narrative steps
|
|
217
|
+
* (the primitive runs stripMarkdown โ collapse ws โ clip โ escape per line),
|
|
218
|
+
* and on finish passes the cleaned result paragraph as the `result` block.
|
|
170
219
|
*
|
|
171
220
|
* Layout (running):
|
|
172
|
-
* ๐
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
221
|
+
* ๐ **WORKER** ยท _{description}_
|
|
222
|
+
* _{elapsed} ยท {n} tools_
|
|
223
|
+
* ~~_โ {earlier step}_~~
|
|
224
|
+
* **โ {newest step}**
|
|
176
225
|
*
|
|
177
226
|
* Layout (finished): the feed renders all-done, then a rule + cleaned result:
|
|
178
|
-
* ๐
|
|
179
|
-
*
|
|
180
|
-
*
|
|
227
|
+
* ๐ **WORKER** ยท _{description}_
|
|
228
|
+
* _done ยท {n} tools ยท {elapsed}_
|
|
229
|
+
* ~~_โ {step}_~~
|
|
181
230
|
* โโโโโ
|
|
182
|
-
* โ
|
|
231
|
+
* โ
_{cleaned result paragraph}_
|
|
232
|
+
*
|
|
233
|
+
* FLUSH (#3842): this card sits at the left margin. #3820/#3821 indented the
|
|
234
|
+
* whole block and prefixed line 1 with `โโ ` to mark it subordinate to the ๐ค
|
|
235
|
+
* agent card; that was reverted because it burned a level of horizontal phone
|
|
236
|
+
* width and because this card does not always sit below the agent card, so a
|
|
237
|
+
* card-level subordination marker asserts a relationship that is not always
|
|
238
|
+
* true. The remaining type cue is line 1: the ๐ emoji plus the caps `WORKER`
|
|
239
|
+
* label against the agent card's ๐ค `Agent`. Keep that pair โ with identical
|
|
240
|
+
* step text and stats the two cards' BODY lines can now coincide byte for
|
|
241
|
+
* byte, so line 1 is doing all of the distinguishing work.
|
|
183
242
|
*/
|
|
184
243
|
export function renderWorkerActivity(v: WorkerActivityView, liveSuffix = ''): string {
|
|
185
244
|
const desc = truncate(stripMarkdown(v.description).trim() || 'background task', DESC_MAX)
|
|
@@ -201,7 +260,9 @@ export function renderWorkerActivity(v: WorkerActivityView, liveSuffix = ''): st
|
|
|
201
260
|
|
|
202
261
|
const header: Parameters<typeof renderStatusCard>[0]['header'] = {
|
|
203
262
|
emoji: '๐ ',
|
|
204
|
-
|
|
263
|
+
// Caps (#3820, issue option 4): `WORKER` against the agent card's `Agent`
|
|
264
|
+
// is a high-contrast type label, not a same-shaped word one letter apart.
|
|
265
|
+
label: 'WORKER',
|
|
205
266
|
description: desc,
|
|
206
267
|
elapsedMs: v.elapsedMs,
|
|
207
268
|
toolCount: v.toolCount,
|
|
@@ -225,9 +286,6 @@ export function renderWorkerActivity(v: WorkerActivityView, liveSuffix = ''): st
|
|
|
225
286
|
if (text.length > 0) result = { emoji: v.state === 'done' ? 'โ
' : 'โ ๏ธ', text }
|
|
226
287
|
}
|
|
227
288
|
|
|
228
|
-
// `renderStatusCard` always returns content when a header is supplied. When
|
|
229
|
-
// running with no steps it shows just the header โ append a "startingโฆ" line
|
|
230
|
-
// for parity with the prior behaviour.
|
|
231
289
|
const card = renderStatusCard({
|
|
232
290
|
header,
|
|
233
291
|
steps,
|
|
@@ -237,19 +295,18 @@ export function renderWorkerActivity(v: WorkerActivityView, liveSuffix = ''): st
|
|
|
237
295
|
// Lone-worker card: window to the w=1 point of Ken's curve (6) so it shows
|
|
238
296
|
// the full recent trail, not the 5-line agent-card default (#3349).
|
|
239
297
|
historyWindow: workerHistoryDepth(1),
|
|
298
|
+
// A just-dispatched worker has no narrative yet. The placeholder is a normal
|
|
299
|
+
// BODY line of the shared spec now (#3846): it used to be concatenated onto
|
|
300
|
+
// this function's return value, which put a user-visible card line outside
|
|
301
|
+
// the primitive that owns hard-break stacking, the collapse-safe separator
|
|
302
|
+
// and the char budget, and made the seam the one boundary that could mash in
|
|
303
|
+
// Telegram's pinned bar. Only while RUNNING โ a finished worker with no
|
|
304
|
+
// steps must not read as "startingโฆ".
|
|
305
|
+
emptyPlaceholder: finished ? undefined : '_startingโฆ_',
|
|
240
306
|
})
|
|
241
307
|
if (card == null) {
|
|
242
308
|
// Unreachable (header always present) โ defensive.
|
|
243
|
-
return
|
|
244
|
-
}
|
|
245
|
-
if (!finished && steps.length === 0) {
|
|
246
|
-
// Header-only running render โ append the starting placeholder with a GFM
|
|
247
|
-
// hard break (` \n`) so it stacks under the header instead of collapsing
|
|
248
|
-
// onto the header line in the rich-message renderer (matches stackCardLines).
|
|
249
|
-
// The collapse separator is carried here too (#3666) โ this card is pinned,
|
|
250
|
-
// and a hand-rolled seam would be the one boundary that still mashed in
|
|
251
|
-
// Telegram's pinned bar.
|
|
252
|
-
return `${card}${COLLAPSE_SAFE_SEPARATOR} \n_startingโฆ_`
|
|
309
|
+
return renderCardTitleLine('๐ ', 'WORKER', 'startingโฆ')
|
|
253
310
|
}
|
|
254
311
|
return card
|
|
255
312
|
}
|
|
@@ -596,9 +653,21 @@ const COOLDOWN_JITTER_MS = 500
|
|
|
596
653
|
* rows and reads like a stuck worker. A single best-effort edit collapses it to
|
|
597
654
|
* an honest "moved" note โ issued once per rotation (โฅ cap interval), never per
|
|
598
655
|
* tick, so it adds no edit churn / pin storm. Plain voice, no em dash.
|
|
656
|
+
*
|
|
657
|
+
* Exported so the card-preview harness (`scripts/card-previews.ts`) renders the
|
|
658
|
+
* real string rather than a copy that can drift from it.
|
|
659
|
+
*
|
|
660
|
+
* DELIBERATELY still a constant, not a card render (#3846). It shows no live
|
|
661
|
+
* state โ no elapsed, no tool count, no steps โ so there is nothing for a
|
|
662
|
+
* renderer to compose and no rolling window or char budget to enforce; routing
|
|
663
|
+
* it through `renderStatusCard` would mean inventing a state to render. What it
|
|
664
|
+
* DOES share with every other card is its type chrome, so line 1 comes from the
|
|
665
|
+
* one title composer (`renderCardTitleLine`): if the ๐ / **WORKER** convention
|
|
666
|
+
* ever changes, this notice moves with it instead of being the one stale card.
|
|
599
667
|
*/
|
|
600
|
-
const WORKER_CARD_SUPERSEDED_BODY =
|
|
601
|
-
'๐
|
|
668
|
+
export const WORKER_CARD_SUPERSEDED_BODY =
|
|
669
|
+
`${renderCardTitleLine('๐ ', 'WORKER', 'continued')}\n\n` +
|
|
670
|
+
'_Live progress moved to a fresh card to stay pinned._'
|
|
602
671
|
|
|
603
672
|
function extractRetryAfterSecs(err: unknown): number | null {
|
|
604
673
|
if (err == null || typeof err !== 'object') return null
|
|
@@ -1176,7 +1245,11 @@ export function createWorkerActivityFeed(opts: WorkerActivityFeedOpts): WorkerAc
|
|
|
1176
1245
|
}
|
|
1177
1246
|
|
|
1178
1247
|
try {
|
|
1179
|
-
|
|
1248
|
+
// #3848: tell the transport whether this is the card's LAST frame. A
|
|
1249
|
+
// terminal recap must not be shed with the heartbeat repaints.
|
|
1250
|
+
const res = await opts.bot.editMessageText(
|
|
1251
|
+
g.chatId, g.messageId, body, sendOptsFor(g), { terminal: isTerminal },
|
|
1252
|
+
)
|
|
1180
1253
|
// Shed honesty (#3084): a cosmetic edit the gate shed resolves the
|
|
1181
1254
|
// distinguishable SEND_GATE_SHED sentinel (NOT a bare `undefined`, which
|
|
1182
1255
|
// the gate reserves for a benign no-op drop whose payload IS on screen).
|
|
@@ -1465,8 +1538,14 @@ export function createWorkerActivityFeed(opts: WorkerActivityFeedOpts): WorkerAc
|
|
|
1465
1538
|
// doesn't sit frozen showing live-styled rows (mistakable for a stuck
|
|
1466
1539
|
// worker). ONE best-effort edit per rotation (โฅ cap interval), fired
|
|
1467
1540
|
// off-chain and swallowing errors โ never per-tick, never a burst.
|
|
1541
|
+
// #3848: terminal โ this is the retired message's LAST frame, and it
|
|
1542
|
+
// fires at most once per `groupMessageLifetimeCapMs`, so promoting it
|
|
1543
|
+
// out of the cosmetic budget costs nothing and stops the retired card
|
|
1544
|
+
// being left frozen showing live-styled rows.
|
|
1468
1545
|
void opts.bot
|
|
1469
|
-
.editMessageText(
|
|
1546
|
+
.editMessageText(
|
|
1547
|
+
g.chatId, retiredId, WORKER_CARD_SUPERSEDED_BODY, sendOptsFor(g), { terminal: true },
|
|
1548
|
+
)
|
|
1470
1549
|
.catch(() => {})
|
|
1471
1550
|
}
|
|
1472
1551
|
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
# CLAUDE.md โ vendor/hindsight-memory
|
|
2
|
+
|
|
3
|
+
**This directory is a VENDORED SNAPSHOT of the hindsight-memory plugin. It is
|
|
4
|
+
not the specification, and it is not what runs.** Treat it as evidence about
|
|
5
|
+
one commit of the implementation, nothing more.
|
|
6
|
+
|
|
7
|
+
The load-bearing copy of this rule is the repo-root `CLAUDE.md`
|
|
8
|
+
("Third-party docs โ the official site is the spec"); nothing auto-loads a
|
|
9
|
+
subdirectory `CLAUDE.md`, so this file only reaches an agent that opens it.
|
|
10
|
+
|
|
11
|
+
## Where the truth is
|
|
12
|
+
|
|
13
|
+
1. **Official docs site** โ <https://hindsight.vectorize.io/>. Curation and
|
|
14
|
+
the memory-unit endpoints: `/developer/api/memories`. Also
|
|
15
|
+
`/developer/api/retain`, `/developer/api/recall`,
|
|
16
|
+
`/developer/observations`, `/developer/configuration`, index at
|
|
17
|
+
`/api-reference`. (`/developer/` itself has no index page โ it 404s.)
|
|
18
|
+
2. **context7** โ prefer `/websites/hindsight_vectorize_io` (the docs site)
|
|
19
|
+
over `/vectorize-io/hindsight` (the OSS repo).
|
|
20
|
+
3. **This tree** โ last, and only for "what does the code here actually do".
|
|
21
|
+
|
|
22
|
+
Not finding something here is not evidence it doesn't exist. Search the docs
|
|
23
|
+
before you tell anyone a knob, endpoint or config path is unsupported.
|
|
24
|
+
|
|
25
|
+
## Trap: `settings.json` here is NOT what switchroom installs
|
|
26
|
+
|
|
27
|
+
`installHindsightPlugin` (`src/agents/scaffold.ts`) copies this tree into each
|
|
28
|
+
agent's `.claude/plugins/hindsight-memory/` and then **stamps switchroom's own
|
|
29
|
+
overrides over `settings.json`** โ additional banks, the retain cadence knobs,
|
|
30
|
+
and the recall types. Concretely, this file has
|
|
31
|
+
`"recallTypes": ["world", "experience"]` while switchroom writes
|
|
32
|
+
`["world", "experience", "observation"]` (`scaffold.ts:3449` as of writing;
|
|
33
|
+
line anchors drift โ grep `settings.recallTypes`).
|
|
34
|
+
|
|
35
|
+
So reading the vendored `settings.json` to learn live behaviour is actively
|
|
36
|
+
misleading. Read the override site in `scaffold.ts`, or the deployed
|
|
37
|
+
`~/.switchroom/agents/<name>/.claude/plugins/hindsight-memory/settings.json`.
|
|
38
|
+
|
|
39
|
+
## Editing
|
|
40
|
+
|
|
41
|
+
Don't hand-edit vendored upstream files without a reason (repo-root
|
|
42
|
+
`CLAUDE.md` ยง Secrets & safety rails). This file and switchroom's local
|
|
43
|
+
patches are the exceptions; changes here ship to every agent on the next
|
|
44
|
+
`apply`/reconcile, and the Python under `scripts/` is gated by the required
|
|
45
|
+
`python-ok` check (`python3 -m unittest discover` in `scripts`).
|
|
@@ -46,6 +46,20 @@ DEFAULTS = {
|
|
|
46
46
|
# cap of 6 its fleet actually deploys.
|
|
47
47
|
"recallOwnBankMinSlots": 0,
|
|
48
48
|
"recallAdditionalBankMinSlots": 0,
|
|
49
|
+
# Switchroom #3837: absolute floor on a result's engine relevance score
|
|
50
|
+
# (`scores.final`) for it to be injected. 0.0 (default) DISABLES the floor
|
|
51
|
+
# โ nothing is dropped and the injected set is byte-identical to the
|
|
52
|
+
# pre-#3837 behaviour. `recallMinScoreScope` decides which turns a
|
|
53
|
+
# non-zero floor binds on: "degraded" (default) = only turns where the
|
|
54
|
+
# agent's OWN bank timed out or was unreachable, which is the population
|
|
55
|
+
# where a below-floor score actually predicts noise (98.4% of degraded
|
|
56
|
+
# rows have a best injected score under 0.01, against 28.4% of healthy
|
|
57
|
+
# ones); "all" = every turn, which #3761's replay says empties ~28% of
|
|
58
|
+
# HEALTHY recalls at 0.01 and is not recommended as a fleet default. See
|
|
59
|
+
# the design note above `_filter_by_min_score` in recall.py. Env:
|
|
60
|
+
# HINDSIGHT_RECALL_MIN_SCORE / HINDSIGHT_RECALL_MIN_SCORE_SCOPE.
|
|
61
|
+
"recallMinScore": 0.0,
|
|
62
|
+
"recallMinScoreScope": "degraded",
|
|
49
63
|
"recallTypes": ["world", "experience"],
|
|
50
64
|
# Switchroom-local: when True (default; Ken-approved ON) recall biases
|
|
51
65
|
# toward synthesized `observation`-tier facts. Escape hatch: pin off via
|
|
@@ -324,6 +338,12 @@ ENV_OVERRIDES = {
|
|
|
324
338
|
# .additional_bank_min_slots (cascading through defaults). 0 = off.
|
|
325
339
|
"HINDSIGHT_RECALL_OWN_BANK_MIN_SLOTS": ("recallOwnBankMinSlots", int),
|
|
326
340
|
"HINDSIGHT_RECALL_ADDITIONAL_BANK_MIN_SLOTS": ("recallAdditionalBankMinSlots", int),
|
|
341
|
+
# Switchroom #3837: absolute `scores.final` floor + the population it binds
|
|
342
|
+
# on. Set by start.sh from agents.<name>.memory.recall.min_score /
|
|
343
|
+
# .min_score_scope (cascading through defaults), exported only when the
|
|
344
|
+
# operator opted in. 0.0 = off (the default, and the shipped behaviour).
|
|
345
|
+
"HINDSIGHT_RECALL_MIN_SCORE": ("recallMinScore", float),
|
|
346
|
+
"HINDSIGHT_RECALL_MIN_SCORE_SCOPE": ("recallMinScoreScope", str),
|
|
327
347
|
# Switchroom-local: recall fact types (comma-separated). Set by start.sh
|
|
328
348
|
# from agents.<name>.memory.recall.types only when the operator overrode
|
|
329
349
|
# the switchroom default (world,experience,observation) โ i.e. the
|
|
@@ -333,6 +353,19 @@ ENV_OVERRIDES = {
|
|
|
333
353
|
# from agents.<name>.memory.recall.skip_trivial only on override; the
|
|
334
354
|
# switchroom default is on (recall.py falls back to True).
|
|
335
355
|
"HINDSIGHT_RECALL_SKIP_TRIVIAL": ("recallSkipTrivial", bool),
|
|
356
|
+
# Switchroom #3841: the last three recall settings that had a config key but
|
|
357
|
+
# no env channel at all, so switchroom.yaml could not reach them and a
|
|
358
|
+
# hand-edit of the installed plugin did not survive `switchroom apply`. Set
|
|
359
|
+
# by start.sh from agents.<name>.memory.recall.prefer_observations / .roles /
|
|
360
|
+
# .prompt_preamble (cascading through defaults), always exported at their
|
|
361
|
+
# existing effective values, so an operator who sets none of them sees no
|
|
362
|
+
# change. The other #3841 knobs (budget, max_tokens, context_turns,
|
|
363
|
+
# max_query_chars, transcript_tail_bytes, tags, tags_match, tag_groups,
|
|
364
|
+
# tag_weights, additional_bank_filters, transcript_fallback, parallel)
|
|
365
|
+
# already had entries in this table and only needed the yaml surface.
|
|
366
|
+
"HINDSIGHT_RECALL_PREFER_OBSERVATIONS": ("recallPreferObservations", bool),
|
|
367
|
+
"HINDSIGHT_RECALL_ROLES": ("recallRoles", list),
|
|
368
|
+
"HINDSIGHT_RECALL_PROMPT_PREAMBLE": ("recallPromptPreamble", str),
|
|
336
369
|
# Switchroom #2848 Stage B: directive-capture nudge on/off. Set by
|
|
337
370
|
# start.sh from agents.<name>.memory.directive_capture_nudge only when
|
|
338
371
|
# the operator overrode it; the switchroom default is on (settings.json
|