instar 1.3.752 → 1.3.754
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/commands/server.d.ts +30 -1
- package/dist/commands/server.d.ts.map +1 -1
- package/dist/commands/server.js +192 -18
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +38 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/MoveIntentClassifier.d.ts +135 -0
- package/dist/core/MoveIntentClassifier.d.ts.map +1 -0
- package/dist/core/MoveIntentClassifier.js +284 -0
- package/dist/core/MoveIntentClassifier.js.map +1 -0
- package/dist/core/NicknameCommand.d.ts +23 -17
- package/dist/core/NicknameCommand.d.ts.map +1 -1
- package/dist/core/NicknameCommand.js +20 -65
- package/dist/core/NicknameCommand.js.map +1 -1
- package/dist/core/componentCategories.d.ts.map +1 -1
- package/dist/core/componentCategories.js +11 -0
- package/dist/core/componentCategories.js.map +1 -1
- package/dist/core/devGatedFeatures.d.ts.map +1 -1
- package/dist/core/devGatedFeatures.js +12 -0
- package/dist/core/devGatedFeatures.js.map +1 -1
- package/dist/core/machineCoherenceManifest.d.ts.map +1 -1
- package/dist/core/machineCoherenceManifest.js +1 -0
- package/dist/core/machineCoherenceManifest.js.map +1 -1
- package/dist/data/llmBenchCoverage.d.ts.map +1 -1
- package/dist/data/llmBenchCoverage.js +12 -0
- package/dist/data/llmBenchCoverage.js.map +1 -1
- package/dist/threadline/HubIntentClassifier.d.ts +150 -0
- package/dist/threadline/HubIntentClassifier.d.ts.map +1 -0
- package/dist/threadline/HubIntentClassifier.js +322 -0
- package/dist/threadline/HubIntentClassifier.js.map +1 -0
- package/dist/threadline/hubCommands.d.ts +16 -12
- package/dist/threadline/hubCommands.d.ts.map +1 -1
- package/dist/threadline/hubCommands.js +16 -30
- package/dist/threadline/hubCommands.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +2 -2
- package/src/data/llmBenchCoverage.ts +14 -0
- package/upgrades/1.3.753.md +44 -0
- package/upgrades/1.3.754.md +46 -0
- package/upgrades/side-effects/keyword-intent-conversion-3-hubcommands.md +170 -0
- package/upgrades/side-effects/nickname-move-intent-llm-rebuild.md +160 -0
|
@@ -48,6 +48,14 @@ export const LLM_BENCH_COVERAGE: Readonly<Record<string, BenchCoverage>> = {
|
|
|
48
48
|
exempt:
|
|
49
49
|
'judges a FIXED known-answer canary probe — the canary is its own benchmark; a bench task would re-test the same constant',
|
|
50
50
|
},
|
|
51
|
+
MoveIntentClassifier: {
|
|
52
|
+
exempt:
|
|
53
|
+
'ships its OWN dedicated discrimination benchmark — tests/unit/move-intent-discrimination.test.ts, a committed command-vs-discussion corpus run deterministically in CI PLUS an opt-in INSTAR_LIVE_MOVE_INTENT=1 real-model accuracy benchmark (the graduation gate before dryRun:false). A generic bench-harness task would re-test the same judgment less precisely (same rationale as InteractivePoolCanaryJudge: the co-located benchmark IS the benchmark). Spec: docs/specs/nickname-move-intent-llm-rebuild.md §Tests.',
|
|
54
|
+
},
|
|
55
|
+
HubIntentClassifier: {
|
|
56
|
+
exempt:
|
|
57
|
+
'ships its OWN dedicated discrimination benchmark — tests/unit/hub-intent-discrimination.test.ts, a committed command-vs-discussion corpus (open/tie vs question/mention + unknown-target guardrail + fail-open) run deterministically in CI PLUS an opt-in INSTAR_LIVE_HUB_INTENT=1 real-model accuracy benchmark (the graduation gate before dryRun:false). Same rationale as MoveIntentClassifier: the co-located benchmark IS the benchmark; a generic harness task would re-test the same judgment less precisely. Spec: docs/specs/keyword-intent-conversions-1-and-3.md §Tests.',
|
|
58
|
+
},
|
|
51
59
|
|
|
52
60
|
// ── Covered by Wave 2 (authored 2026-07-02; tasks-wave2/ in the bench harness) ──
|
|
53
61
|
InputGuard: { task: 'input-guard-coherence' },
|
|
@@ -164,11 +172,13 @@ export const LLM_UNTRUSTED_INPUT: Readonly<Record<string, UntrustedInputFlag>> =
|
|
|
164
172
|
TelegramAdapter: true,
|
|
165
173
|
|
|
166
174
|
// ── Gates judging user/session/operation content → true ──
|
|
175
|
+
HubIntentClassifier: true, // judges an inbound hub message's bind-intent (untrusted user text)
|
|
167
176
|
PromptGate: true,
|
|
168
177
|
ExternalOperationGate: true, // the motivating callsite: credited in-content "user already approved"
|
|
169
178
|
WarrantsReplyGate: true,
|
|
170
179
|
UnjustifiedStopGate: true,
|
|
171
180
|
MessagingToneGate: true, // reviews a draft that routinely quotes untrusted user/tool content
|
|
181
|
+
MoveIntentClassifier: true, // classifies an untrusted inbound user message + recent conversation context
|
|
172
182
|
CoherenceReviewer: true,
|
|
173
183
|
LLMSanitizer: true, // definitionally judges untrusted inbound content
|
|
174
184
|
OverrideDetector: true,
|
|
@@ -331,6 +341,8 @@ export const LLM_JUDGES_CLAIMS: Readonly<Record<string, JudgesClaimsFlag>> = {
|
|
|
331
341
|
IntegrationGate: false, // delegates to JobReflector.reflect(), no own callsite
|
|
332
342
|
ExternalOperationGate: false, // classifies operation mutability/reversibility, not a completion claim
|
|
333
343
|
WarrantsReplyGate: false, // "should I reply?" — not a completion/health claim
|
|
344
|
+
MoveIntentClassifier: false, // classifies a USER's move/pin intent over a message, not an agent/session claim of completion/health/credit
|
|
345
|
+
HubIntentClassifier: false, // classifies a USER's hub bind-intent (open/tie) over a message, not an agent/session claim of completion/health/credit
|
|
334
346
|
CoherenceGate: false, // no own callsite — flows through CoherenceReviewer
|
|
335
347
|
MessagingToneGate: false, // reviews outbound tone/leaks
|
|
336
348
|
CoherenceReviewer: false, // reviews outbound coherence
|
|
@@ -423,6 +435,8 @@ export const LLM_PARSER_CONTRACT: Readonly<Record<string, ParserContractFlag>> =
|
|
|
423
435
|
MessageSentinel: { pending: 'contract-wave-2' }, // closed intent set (pause / emergency / normal)
|
|
424
436
|
LLMSanitizer: { pending: 'contract-wave-2' }, // parses a closed sanitize verdict/decision
|
|
425
437
|
WarrantsReplyGate: { pending: 'contract-wave-2' }, // closed should-reply yes/no verdict
|
|
438
|
+
MoveIntentClassifier: { pending: 'contract-wave-2' }, // parses a closed move-intent verdict (isCommand + intent enum + targetNickname enum + confidence)
|
|
439
|
+
HubIntentClassifier: { pending: 'contract-wave-2' }, // parses a closed hub-intent verdict (intent enum open/tie/null + targetTopicId enum + confidence)
|
|
426
440
|
InputGuard: { pending: 'contract-wave-2' }, // closed input-coherence verdict
|
|
427
441
|
StallTriageNurse: { pending: 'contract-wave-2' }, // closed stall-triage diagnosis label
|
|
428
442
|
CommitmentSentinel: { pending: 'contract-wave-2' }, // closed commitment-detected verdict + structured envelope
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The "move/run/pin this on `<machine-nickname>`" recognizer — the thing that decides whether a
|
|
9
|
+
mid-conversation message is a command to move the session to another machine — was rebuilt from a keyword
|
|
10
|
+
verb-list into an LLM classifier (`src/core/MoveIntentClassifier.ts`). The old code (`TRANSFER_VERBS =
|
|
11
|
+
[move, transfer, run, continue, resume, keep, …]` × a preposition × a known nickname) hijacked the
|
|
12
|
+
operator's plain discussion **"keep the work on the laptop"** on 2026-07-03 — it matched the verb `keep`
|
|
13
|
+
and swallowed the message before the agent ever saw it. The new classifier infers move/pin intent from the
|
|
14
|
+
message **and** a bounded window of recent conversation, and constrains the target machine to a
|
|
15
|
+
known-nickname enum via **code-side validation of the model's emitted field** (never string-matching the
|
|
16
|
+
model's prose). It is the exemplar conversion under the constitutional standard *"Intelligence Infers,
|
|
17
|
+
Keywords Only Guard"* (parent: *"Intelligent Prompts — An LLM Gate Must Not String-Match"*). It **fails
|
|
18
|
+
open**: on any uncertainty — no provider, circuit-breaker open, timeout, unparseable/schema-violating
|
|
19
|
+
output, target-not-in-enum, or low confidence — the message passes straight through to the agent, never
|
|
20
|
+
hijacked. It ships **dev-gated dark on the fleet + dry-run first on a development agent** (it logs
|
|
21
|
+
would-hijack vs would-pass to `logs/move-intent.jsonl` and actuates nothing until a deliberate
|
|
22
|
+
`dryRun:false`). The keyword decision is removed from `NicknameCommand.ts` (only the `NicknameCommand` type,
|
|
23
|
+
consumed by the unchanged `TransferByNickname` planner, remains). Ships with a committed discrimination
|
|
24
|
+
corpus (command vs discussion both ways + guardrail + fail-open) — deterministic in CI plus an opt-in
|
|
25
|
+
`INSTAR_LIVE_MOVE_INTENT=1` real-model benchmark used as the graduation gate before actuation.
|
|
26
|
+
|
|
27
|
+
## What to Tell Your User
|
|
28
|
+
|
|
29
|
+
Nothing user-facing right now — this ships dark on the fleet and dry-run on a development agent, so no
|
|
30
|
+
behavior changes until it's deliberately graduated. If asked why the agent used to grab a message like
|
|
31
|
+
"keep the work on the laptop" as a machine-move command: that was a brittle keyword list, now replaced by
|
|
32
|
+
an LLM that judges intent from the message and its conversation context and errs toward *not* grabbing your
|
|
33
|
+
message when unsure. The move-by-nickname capability itself is a dev-only experimental feature (part of the
|
|
34
|
+
dark multi-machine session pool); only *how it recognizes your intent* changed.
|
|
35
|
+
|
|
36
|
+
## Summary of New Capabilities
|
|
37
|
+
|
|
38
|
+
- `src/core/MoveIntentClassifier.ts` — LLM-with-context move-intent recognizer (`classifyRelocationIntent`
|
|
39
|
+
+ `toNicknameCommand`); structured-output enum guardrail validated in code; fail-open on all uncertainty.
|
|
40
|
+
- Config `multiMachine.sessionPool.moveIntent` (`enabled` dev-gated; `dryRun:true`, `minConfidence:0.85`,
|
|
41
|
+
`timeoutMs:4000`, `contextWindowTurns:6`, `modelTier:'fast'`); registered in `DEV_GATED_FEATURES`.
|
|
42
|
+
- `logs/move-intent.jsonl` — machine-local dry-run soak log (LLM-engaged decisions only; 80-char preview).
|
|
43
|
+
- Committed discrimination corpus + opt-in real-model benchmark (`INSTAR_LIVE_MOVE_INTENT=1`), the
|
|
44
|
+
graduation gate before `dryRun:false`.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The Threadline hub's "open this" / "tie this to `<topic>`" recognizer — the thing that decides whether a
|
|
9
|
+
message in the hub topic is a bind command — was rebuilt from anchored whole-message regexes into an LLM
|
|
10
|
+
classifier (`src/threadline/HubIntentClassifier.ts`). The old regexes
|
|
11
|
+
(`/^open(?:\s+this)?…/`, `/^(?:tie|bind)\s+this\s+to\s+(.+?)…/`) were wired at the `onTopicMessage` seam,
|
|
12
|
+
where a positive match **SWALLOWS the message before the agent ever sees it** and performs a bind — so a
|
|
13
|
+
misread silently EATS a real message (e.g. "should I open this?" or "open this in a new tab"). The new
|
|
14
|
+
classifier infers open/tie intent from the message **and** a bounded window of recent conversation, and
|
|
15
|
+
constrains a `tie` target to a structured enum of the real existing topics via **code-side validation of
|
|
16
|
+
the model's emitted topic-id** (never string-matching the model's prose). It is Conversion #3 under the
|
|
17
|
+
constitutional standard *"Intelligence Infers, Keywords Only Guard"*, following the proven move-intent
|
|
18
|
+
exemplar (PR #1367). It **fails open**: on any uncertainty — no provider, circuit-breaker open, timeout,
|
|
19
|
+
unparseable/schema-violating output, tie-target-not-in-enum, or low confidence — the message passes
|
|
20
|
+
straight through to the agent, never swallowed. It ships **dev-gated dark on the fleet + dry-run first on
|
|
21
|
+
a development agent** (it logs would-swallow vs would-pass to `logs/hub-intent.jsonl` and swallows nothing
|
|
22
|
+
until a deliberate `dryRun:false`). The keyword decision (`parseHubCommand`) is removed from
|
|
23
|
+
`hubCommands.ts`; the `HubCommand` type + the authoritative `bindHubConversation` binder are unchanged.
|
|
24
|
+
Ships with a committed discrimination corpus (command vs discussion both ways + guardrail + fail-open) —
|
|
25
|
+
deterministic in CI plus an opt-in `INSTAR_LIVE_HUB_INTENT=1` real-model benchmark used as the graduation
|
|
26
|
+
gate before actuation.
|
|
27
|
+
|
|
28
|
+
## What to Tell Your User
|
|
29
|
+
|
|
30
|
+
Nothing user-facing right now — this ships dark on the fleet and dry-run on a development agent, so no
|
|
31
|
+
behavior changes until it's deliberately graduated. If asked why the agent used to grab a hub message
|
|
32
|
+
like "should I open this?" as a bind command: that was a brittle regex that ate the message before the
|
|
33
|
+
agent saw it, now replaced by an LLM that judges intent from the message and its conversation context and
|
|
34
|
+
errs toward not grabbing your message when unsure. While dark on the fleet, the hub's automatic
|
|
35
|
+
"open this" grabbing simply does not fire, and everything else about the hub works exactly as before.
|
|
36
|
+
|
|
37
|
+
## Summary of New Capabilities
|
|
38
|
+
|
|
39
|
+
- `src/threadline/HubIntentClassifier.ts` — LLM-with-context hub-intent recognizer (`classifyHubIntent`
|
|
40
|
+
+ `toHubCommand`); structured-output topic-id enum guardrail validated in code; fail-open on all
|
|
41
|
+
uncertainty.
|
|
42
|
+
- Config `threadline.hubIntent` (`enabled` dev-gated; `dryRun:true`, `minConfidence:0.85`,
|
|
43
|
+
`timeoutMs:4000`, `contextWindowTurns:6`, `modelTier:'fast'`); registered in `DEV_GATED_FEATURES`.
|
|
44
|
+
- `logs/hub-intent.jsonl` — machine-local dry-run soak log (LLM-engaged decisions only; 80-char preview).
|
|
45
|
+
- Committed discrimination corpus + opt-in real-model benchmark (`INSTAR_LIVE_HUB_INTENT=1`), the
|
|
46
|
+
graduation gate before `dryRun:false`.
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# Side-Effects Review — Hub-intent recognizer: regex → LLM-with-context (Conversion #3)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `keyword-intent-conversion-3-hubcommands`
|
|
4
|
+
**Date:** `2026-07-03`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `subagent (fresh reviewer) — see appended verdict`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Replaces the keyword/regex DECISION in `src/threadline/hubCommands.ts` `parseHubCommand()` (anchored
|
|
11
|
+
whole-message regexes for "open this" / "tie this to <topic>") with an LLM-with-context classifier,
|
|
12
|
+
`src/threadline/HubIntentClassifier.ts`. The recognizer runs at the `telegram.onTopicMessage` hub
|
|
13
|
+
intercept in `src/commands/server.ts`, where a positive decision SWALLOWS the message before the agent
|
|
14
|
+
sees it and performs a bind — so a misread silently EATS a real message. The new classifier infers
|
|
15
|
+
open/tie intent from the message + a bounded recent-conversation window, constrains a `tie` target to a
|
|
16
|
+
structured enum of real existing topics (validated in code by numeric id membership, never prose-match),
|
|
17
|
+
and FAILS OPEN on every uncertainty (message passes through, never swallowed). The authoritative binder
|
|
18
|
+
`bindHubConversation` is unchanged. Ships dev-gated dark (config `threadline.hubIntent`, `enabled` omitted
|
|
19
|
+
→ `resolveDevAgentGate`) + dry-run first (`logs/hub-intent.jsonl`). Files: `HubIntentClassifier.ts` (new),
|
|
20
|
+
`hubCommands.ts` (regex removed), `server.ts` (wiring + `resolveHubClassifierDeps`), `ConfigDefaults.ts`,
|
|
21
|
+
`devGatedFeatures.ts`, `componentCategories.ts`, `LLM-ROUTING-REGISTRY.md`, three test tiers + updated
|
|
22
|
+
`hubCommands.test.ts` + recomputed dark-gate line-map. Follows the proven `MoveIntentClassifier` exemplar
|
|
23
|
+
(PR #1367).
|
|
24
|
+
|
|
25
|
+
## Decision-point inventory
|
|
26
|
+
|
|
27
|
+
- `hubCommands.parseHubCommand` (`src/threadline/hubCommands.ts`) — **remove** — the anchored-regex "is
|
|
28
|
+
this a hub bind command?" decision is deleted.
|
|
29
|
+
- `HubIntentClassifier.classifyHubIntent` (`src/threadline/HubIntentClassifier.ts`) — **add** — the LLM
|
|
30
|
+
decision that replaces it (open/tie/null + enum-validated target + confidence).
|
|
31
|
+
- `onTopicMessage` hub intercept (`src/commands/server.ts`) — **modify** — swaps regex call for classifier
|
|
32
|
+
+ dev-gate + dry-run gate + audit; still returns (swallows) only on a real, non-dry-run command.
|
|
33
|
+
- `bindHubConversation` (`src/threadline/hubCommands.ts`) — **pass-through** — unchanged binder.
|
|
34
|
+
|
|
35
|
+
---
|
|
36
|
+
|
|
37
|
+
## 1. Over-block
|
|
38
|
+
|
|
39
|
+
Over-block here = wrongly SWALLOWING a legitimate message the user did not mean as a bind command (the
|
|
40
|
+
exact harm the old regex caused, e.g. "should I open this?" / "open this in a new tab"). The new decision
|
|
41
|
+
is LLM-with-context, so these are correctly classified as discussion (covered by the discrimination
|
|
42
|
+
corpus). Structural protections against over-block: (a) FAIL-OPEN — any uncertainty passes through;
|
|
43
|
+
(b) a high confidence bar (0.85) to swallow; (c) a `tie` whose target isn't a real topic id passes
|
|
44
|
+
through; (d) the whole path is DARK on the fleet and DRY-RUN on dev, so nothing is swallowed until a
|
|
45
|
+
deliberate `dryRun:false` gated on a live accuracy benchmark. Net: over-block strictly decreases vs the
|
|
46
|
+
shipped regex.
|
|
47
|
+
|
|
48
|
+
## 2. Under-block
|
|
49
|
+
|
|
50
|
+
Under-block = failing to recognize a genuine bind command (message reaches the agent instead of
|
|
51
|
+
auto-binding). This is the SAFE direction here (a missed auto-bind costs a restate; the agent can still
|
|
52
|
+
bind via the API). Sources: (a) the cheap pre-filter skips messages with no bind-ish stem word, so a
|
|
53
|
+
paraphrase like "put this under the roadmap topic" is not auto-bound; (b) fail-open passes through on
|
|
54
|
+
provider failure; (c) while dark/dry-run nothing is bound. All acceptable — the design deliberately trades
|
|
55
|
+
missed auto-binds for never eating a message.
|
|
56
|
+
|
|
57
|
+
## 3. Level-of-abstraction fit
|
|
58
|
+
|
|
59
|
+
Correct layer: an AUTHORITY (context-rich LLM reasoning), not a brittle detector. It replaces a brittle
|
|
60
|
+
detector (regex) that wrongly held swallow authority. It routes through the shared `IntelligenceProvider`
|
|
61
|
+
(the same smart-gate substrate `CoherenceGate` uses) rather than re-implementing an LLM path, and reuses
|
|
62
|
+
the existing `bindHubConversation` binder rather than duplicating bind logic. The cheap pre-filter is a
|
|
63
|
+
low-level primitive used ONLY to drop toward pass-through (never to decide a positive), exactly as the
|
|
64
|
+
standard permits.
|
|
65
|
+
|
|
66
|
+
## 4. Signal vs authority compliance
|
|
67
|
+
|
|
68
|
+
**Required reference:** docs/signal-vs-authority.md
|
|
69
|
+
|
|
70
|
+
- [x] Yes — the logic is a smart gate with full conversational context (LLM-backed with recent history).
|
|
71
|
+
|
|
72
|
+
The classifier holds the swallow decision, but it is an LLM reasoning over the message + a bounded recent
|
|
73
|
+
conversation window (not brittle logic), and it fails open. The only string-matching in the module is the
|
|
74
|
+
pre-filter, which can never DECIDE a positive command — it only drops obvious non-commands toward
|
|
75
|
+
pass-through. Enum validation of the model's emitted `targetTopicId` is numeric-id membership, not
|
|
76
|
+
prose-matching.
|
|
77
|
+
|
|
78
|
+
## 5. Interactions
|
|
79
|
+
|
|
80
|
+
- **Shadowing:** the hub intercept runs inside `onTopicMessage` before normal session routing, exactly
|
|
81
|
+
where the regex ran. When dark/dry-run OR fail-open OR not-a-command, it returns `handled:false`
|
|
82
|
+
(falls through) so downstream routing (fix-commands, session dispatch) is unaffected — it can only
|
|
83
|
+
shadow routing when it genuinely swallows a real command, which was the old behavior too.
|
|
84
|
+
- **Double-fire:** only one intercept owns hub-command recognition; there is no second recognizer to
|
|
85
|
+
double-fire with. The `POST /threadline/hub/bind` API route is a separate, explicit caller (no text
|
|
86
|
+
decision) and is unchanged.
|
|
87
|
+
- **Races:** none new — the classifier is stateless; the audit append is best-effort and wrapped so it
|
|
88
|
+
never gates the message. `bindHubConversation`'s existing CAS mutate is unchanged.
|
|
89
|
+
- **Feedback loops:** none — the classifier reads recent history but does not write to it.
|
|
90
|
+
|
|
91
|
+
## 6. External surfaces
|
|
92
|
+
|
|
93
|
+
- **Other agents / users:** none while dark on the fleet. On a dev agent (dry-run) the only new surface is
|
|
94
|
+
the machine-local `logs/hub-intent.jsonl` (80-char scrubbed preview, LLM-engaged decisions only).
|
|
95
|
+
- **External systems:** one bounded fast-tier LLM call per candidate hub message through the shared
|
|
96
|
+
provider (spawn-cap + breaker), attributed `HubIntentClassifier`. No new network egress beyond that.
|
|
97
|
+
- **Persistent state:** none beyond the append-only dry-run log.
|
|
98
|
+
- **Operator surface (Mobile-Complete):** no operator-facing actions added — the only control is a config
|
|
99
|
+
flag (`threadline.hubIntent.dryRun/enabled`), same as every dev-gated feature. Not applicable.
|
|
100
|
+
|
|
101
|
+
## 6b. Operator-surface quality
|
|
102
|
+
|
|
103
|
+
No operator surface — not applicable. This change touches no dashboard renderer, approval page, or
|
|
104
|
+
grant/revoke/secret form.
|
|
105
|
+
|
|
106
|
+
## 7. Multi-machine posture
|
|
107
|
+
|
|
108
|
+
**machine-local BY DESIGN.** The hub intercept and its dry-run log are per-machine inbound-message
|
|
109
|
+
processing on whichever machine owns the hub conversation; there is no cross-machine state to replicate
|
|
110
|
+
and no URL generated. It emits NO user-facing notices (it either swallows a command and lets the existing
|
|
111
|
+
binder post the hub confirmation, or passes through), so no one-voice gating is needed. It holds no durable
|
|
112
|
+
state that could strand on topic transfer (the audit log is local observability). The config flag resolves
|
|
113
|
+
per-machine via `resolveDevAgentGate`, consistent with every other dev-gated feature; on the fleet it is
|
|
114
|
+
uniformly dark.
|
|
115
|
+
|
|
116
|
+
## 8. Rollback cost
|
|
117
|
+
|
|
118
|
+
Pure code change — revert and ship a patch. No persistent state needing cleanup (the dry-run log is
|
|
119
|
+
append-only machine-local observability that can be ignored/deleted). No agent-state repair, no
|
|
120
|
+
user-visible regression during the rollback window (the feature is dark on the fleet, so a revert is a
|
|
121
|
+
no-op for fleet agents). The rollback lever short of a revert is `threadline.hubIntent` staying
|
|
122
|
+
dark/dry-run, or `enabled:false` to force-dark even a dev agent.
|
|
123
|
+
|
|
124
|
+
---
|
|
125
|
+
|
|
126
|
+
## Second-pass review (Phase 5)
|
|
127
|
+
|
|
128
|
+
**Reviewer:** fresh subagent (independent audit). **Verdict: Concur with the review.**
|
|
129
|
+
|
|
130
|
+
The reviewer independently traced every `return` in `classifyHubIntent` and confirmed only two paths
|
|
131
|
+
yield `isCommand:true` (a high-confidence `open`, or a high-confidence `tie` with an enum-resolved
|
|
132
|
+
target), both behind the confidence gate; every other path (empty text, no bind signal, no provider,
|
|
133
|
+
throw/timeout, unparseable/schema-violation, intent null, below confidence, tie-target-not-in-enum)
|
|
134
|
+
returns pass-through. The wiring's `willAct = isCommand && !dryRun` gate means the message is swallowed
|
|
135
|
+
ONLY on a real command with a resolved target and `dryRun:false`; the dark gate (`if clsDeps?.enabled`)
|
|
136
|
+
and dry-run default both skip swallowing; the audit write is wrapped so it can never break the path, and
|
|
137
|
+
the outer try/catch fails open to normal routing. Enum validation is numeric-id membership (never
|
|
138
|
+
prose-match); the pre-filter can only drop toward pass-through. The classifier never throws.
|
|
139
|
+
|
|
140
|
+
**One non-blocking observation:** while dark on the fleet, the "open this"/"tie this" structural
|
|
141
|
+
interception no longer fires (no regex fallback), so those messages reach the agent — whereas the fleet
|
|
142
|
+
CLAUDE.md still says the agent will NOT see "open this." This is the intentional dark-rollout tradeoff
|
|
143
|
+
already recorded in §2 (Under-block) / §5 (Shadowing); it is the SAFE direction (pass-through, never
|
|
144
|
+
swallow) and not a safety concern. It is a product-completeness item to close at graduation (flip the
|
|
145
|
+
gate, or update the fleet CLAUDE.md guidance for the dark window) — not a blocker for this change.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
**CI follow-up (2026-07-04):** the new `HubIntentClassifier` gate is registered in
|
|
150
|
+
`COMPONENT_CATEGORY`, which the untrusted-input-classification ratchet requires to carry an explicit
|
|
151
|
+
`LLM_UNTRUSTED_INPUT` classification. It judges an inbound hub message's bind-intent (untrusted user
|
|
152
|
+
text), so it is classified `true` in `src/data/llmBenchCoverage.ts`. Mechanical consequence of the
|
|
153
|
+
component registration; no behavior change.
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## Post-rebase addendum (main now carries #1367)
|
|
158
|
+
|
|
159
|
+
After the move-intent exemplar (PR #1367) merged, `main` gained the keyword-intent classification-ratchet family. Rebasing this branch onto it required registering `HubIntentClassifier` (a new `COMPONENT_CATEGORY` key) across every classification map, exactly mirroring how #1367 registered `MoveIntentClassifier`:
|
|
160
|
+
|
|
161
|
+
- `src/data/llmBenchCoverage.ts`:
|
|
162
|
+
- `LLM_BENCH_COVERAGE`: `{ exempt }` — ships its own discrimination benchmark (`tests/unit/hub-intent-discrimination.test.ts` + opt-in `INSTAR_LIVE_HUB_INTENT=1`), the co-located benchmark IS the benchmark (same argument as MoveIntentClassifier / InteractivePoolCanaryJudge).
|
|
163
|
+
- `LLM_JUDGES_CLAIMS`: bare `false` — classifies a USER's bind-intent, not an agent/session completion/health/credit claim.
|
|
164
|
+
- `LLM_PARSER_CONTRACT`: `{ pending: 'contract-wave-2' }` — parses a closed intent(open/tie/null) + targetTopicId-enum + confidence verdict.
|
|
165
|
+
- `LLM_UNTRUSTED_INPUT`: `true` — judges untrusted inbound hub text (landed in the prior fix commit).
|
|
166
|
+
- Pinned shrink-only baselines updated (each a visible, reviewed act): `EXEMPT_BASELINE` (`tests/unit/llm-bench-coverage-ratchet.test.ts`) and `PENDING_BASELINE` (`tests/unit/parser-contract-classification-ratchet.test.ts`) each gain `HubIntentClassifier`.
|
|
167
|
+
- `tests/unit/keyword-intent-decision-ratchet.test.ts`: `threadline/hubCommands.ts` removed from `EXPECTED_OFFENDERS` and `BASELINE` dropped 5→4 (the converted file no longer keyword-decides intent; the detector confirms exactly 4 remaining offenders). `topicProfileIngress` #1 remains for its own conversion.
|
|
168
|
+
- Merge conflicts in `componentCategories.ts`, `devGatedFeatures.ts`, `LLM-ROUTING-REGISTRY.md`, and `lint-dev-agent-dark-gate.test.ts` resolved keeping BOTH #1367's and this change's registrations; the dark-gate line-map recomputed against the merged `ConfigDefaults` (hubIntent +20 lines and moveIntent +18 lines both present, neither adds an attributed path).
|
|
169
|
+
|
|
170
|
+
No behavior change from this addendum — all registrations are ratchet metadata / test baselines. The classifier, wiring, config, and fail-open contract are unchanged from the reviewed version above.
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# Side-Effects Review — Move-Intent Recognizer: Keyword List → LLM-With-Context
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `nickname-move-intent-llm-rebuild`
|
|
4
|
+
**Date:** `2026-07-04`
|
|
5
|
+
**Author:** `echo`
|
|
6
|
+
**Second-pass reviewer:** `spec-converge round (codex-cli:gpt-5.5 + gemini-cli:gemini-2.5-pro + 4 internal reviewers)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Replaces the keyword verb-list decision in `NicknameCommand.recognizeNicknameCommand`
|
|
11
|
+
(`TRANSFER_VERBS = [move,…,run,continue,resume,keep]`) — which hijacked the operator's discussion message
|
|
12
|
+
"keep the work on the laptop" on 2026-07-03 — with an LLM classifier (`src/core/MoveIntentClassifier.ts`)
|
|
13
|
+
that infers "is this a move/pin command to <known machine>?" over the message + a bounded window of recent
|
|
14
|
+
conversation, guardrailed by code-side enum validation of the model-emitted `targetNickname`. Fail-OPEN on
|
|
15
|
+
any uncertainty (never hijack). Wired at `server.ts` `_tryNicknameRelocation` behind a dev-agent dark gate +
|
|
16
|
+
dry-run. The keyword decision is removed from `NicknameCommand.ts` (only the `NicknameCommand` type, consumed
|
|
17
|
+
by the unchanged `TransferByNickname` planner, remains). Files: `src/core/MoveIntentClassifier.ts` (new),
|
|
18
|
+
`src/core/NicknameCommand.ts`, `src/commands/server.ts`, `src/config/ConfigDefaults.ts`,
|
|
19
|
+
`src/core/devGatedFeatures.ts`, `src/core/componentCategories.ts`, `src/core/machineCoherenceManifest.ts`,
|
|
20
|
+
`docs/LLM-ROUTING-REGISTRY.md`, three test files + the wiring/line-map test updates.
|
|
21
|
+
|
|
22
|
+
## Decision-point inventory
|
|
23
|
+
|
|
24
|
+
- `_tryNicknameRelocation` (server.ts) — **modify** — the "is this a move command?" decision changes from
|
|
25
|
+
keyword-match to LLM classifier; the downstream `planTransferByNickname` actuator is pass-through (unchanged).
|
|
26
|
+
- `NicknameCommand.recognizeNicknameCommand` — **remove** — the keyword verb-list decision is deleted.
|
|
27
|
+
- `MoveIntentClassifier.classifyRelocationIntent` — **add** — the new LLM decision + code-side enum guardrail.
|
|
28
|
+
- `multiMachine.sessionPool.moveIntent` config — **add** — dev-gated dark, dry-run-first knobs.
|
|
29
|
+
|
|
30
|
+
---
|
|
31
|
+
|
|
32
|
+
## 1. Over-block
|
|
33
|
+
|
|
34
|
+
The "block" surface here = hijacking a message (treating it as a move command, swallowing it). Over-block =
|
|
35
|
+
falsely hijacking discussion. This change's entire purpose is to REDUCE over-block: the classifier fails
|
|
36
|
+
open (only a high-confidence command with a resolved enum target hijacks), and ships dry-run so it hijacks
|
|
37
|
+
NOTHING during the soak. The residual over-block risk is a confidently-wrong model returning
|
|
38
|
+
`isCommand:true, confidence≥0.85` on discussion — bounded by the graduation-gate live benchmark (must pass
|
|
39
|
+
before `dryRun:false`) and the conservative threshold. Strictly less over-block than the keyword list, which
|
|
40
|
+
hijacked on any verb×preposition×nickname co-occurrence.
|
|
41
|
+
|
|
42
|
+
## 2. Under-block
|
|
43
|
+
|
|
44
|
+
Under-block = missing a genuine move command (it passes through to the agent). By design this is the SAFE
|
|
45
|
+
direction (fail-open). Known misses: (a) a purely deictic target with no nickname anywhere in the window
|
|
46
|
+
("send it to my desktop") — the prefilter skips → pass-through; (b) any fail-open path (provider down,
|
|
47
|
+
timeout, low confidence, malformed output). All are cheap (the user re-phrases); none cause harm. The old
|
|
48
|
+
keyword list ALSO under-blocked ("let's have the mini take this one" was missed) — this reduces it via
|
|
49
|
+
context-aware inference.
|
|
50
|
+
|
|
51
|
+
## 3. Level-of-abstraction fit
|
|
52
|
+
|
|
53
|
+
Correct layer. It is a smart AUTHORITY (LLM reasoning over message + conversation context), not a brittle
|
|
54
|
+
detector — exactly what the "Intelligence Infers, Keywords Only Guard" standard requires for a
|
|
55
|
+
what-did-they-mean judgment. The cheap deterministic prefilter is a genuine *pre-filter* (drops toward
|
|
56
|
+
pass-through only, never decides a command), matching `TopicIntentCapture`. It USES existing primitives
|
|
57
|
+
(shared `IntelligenceProvider`, `RelocationNicknameSet`, `TransferByNickname`) rather than re-implementing
|
|
58
|
+
them; it FEEDS the unchanged planner rather than running parallel to it.
|
|
59
|
+
|
|
60
|
+
## 4. Signal vs authority compliance
|
|
61
|
+
|
|
62
|
+
**Required reference:** docs/signal-vs-authority.md
|
|
63
|
+
|
|
64
|
+
- [x] Yes — but the logic is a smart gate with full conversational context (LLM-backed with recent history).
|
|
65
|
+
|
|
66
|
+
The decision is an LLM reasoning over the message + a bounded conversation window — a smart gate, not a
|
|
67
|
+
brittle detector holding block authority. It replaces exactly the brittle-detector-with-authority
|
|
68
|
+
anti-pattern the standard forbids. It fails open, so even under provider failure it never asserts authority
|
|
69
|
+
it can't back.
|
|
70
|
+
|
|
71
|
+
## 5. Interactions
|
|
72
|
+
|
|
73
|
+
- **Shadowing:** `_tryNicknameRelocation` runs BEFORE `_sessionRouter.route()` (unchanged ordering) and only
|
|
74
|
+
when `_sessionPoolStage() !== 'dark'`. When it returns `handled:false` (the common case, and ALWAYS during
|
|
75
|
+
dry-run), routing proceeds normally — it cannot shadow the router. Verified by `transfer-activation-wiring.test.ts`.
|
|
76
|
+
- **Double-fire:** no. A message is classified once per inbound; a positive hijack returns early (`return`),
|
|
77
|
+
a pass-through continues to the router exactly once.
|
|
78
|
+
- **Races:** none new. The classifier is stateless; the only write is an append to `logs/move-intent.jsonl`
|
|
79
|
+
(best-effort, wrapped, never throws into the message path).
|
|
80
|
+
- **Feedback loops:** none. The classifier reads recent history but never writes conversation turns.
|
|
81
|
+
|
|
82
|
+
## 6. External surfaces
|
|
83
|
+
|
|
84
|
+
- **Other agents on the machine:** none — internal message-path classifier.
|
|
85
|
+
- **Install base:** dark on the fleet (dev-agent gate + the whole relocation path is already gated behind
|
|
86
|
+
`sessionPool != dark`, which ships dark). No fleet behavior change.
|
|
87
|
+
- **External systems:** one bounded fast-tier LLM call per nickname-mentioning candidate message, via the
|
|
88
|
+
shared `IntelligenceProvider` (spawn-cap + circuit-breaker + attribution). No new network dependency.
|
|
89
|
+
- **Persistent state:** `logs/move-intent.jsonl` — a new machine-local append-only observability log
|
|
90
|
+
(LLM-engaged decisions only; 80-char truncated preview; local, never synced). Consistent with all other
|
|
91
|
+
`logs/*.jsonl`.
|
|
92
|
+
- **Timing:** `timeoutMs` default 4000 bounds the worst-case pass-through delay for a candidate message.
|
|
93
|
+
- **Operator surface (Mobile-Complete):** No operator-facing actions added. The only operator interaction is
|
|
94
|
+
flipping `dryRun:false` / `modelTier` in config (already a standard config edit), plus reading the
|
|
95
|
+
dashboard **LLM Activity** tab (`/metrics/features`, already phone-accessible). N/A.
|
|
96
|
+
|
|
97
|
+
## 6b. Operator-surface quality
|
|
98
|
+
|
|
99
|
+
No operator surface — not applicable. This change touches no dashboard renderer, approval page, or
|
|
100
|
+
grant/revoke/secret-drop form (no `dashboard/*` or form files staged).
|
|
101
|
+
|
|
102
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
103
|
+
|
|
104
|
+
**Posture: machine-local BY DESIGN** — the classifier is stateless request-locality COMPUTE. It runs on the
|
|
105
|
+
machine that holds the topic (the lifeline forwards inbound to the holder), is a pure function of the
|
|
106
|
+
message + the live nickname registry + the provider, and has no durable cross-machine state to replicate.
|
|
107
|
+
The only durable artifact is `logs/move-intent.jsonl`, a machine-local observability log identical in
|
|
108
|
+
posture to `logs/sentinel-events.jsonl` / `logs/reaper-audit.jsonl` (pure per-machine observability).
|
|
109
|
+
|
|
110
|
+
- **User-facing notices / one-voice gating?** No — during dry-run it emits nothing to the user; when live it
|
|
111
|
+
produces the same single "Moving…" reply the existing keyword path did (unchanged), routed by the existing
|
|
112
|
+
single-owner path.
|
|
113
|
+
- **Durable state / strands on topic transfer?** The soak log is per-machine; a topic that transfers mid-soak
|
|
114
|
+
splits its would-hijack/would-pass evidence across machines. This is graduation-decision EVIDENCE, not user
|
|
115
|
+
data — at `dryRun:false` graduation the operator aggregates the per-machine logs (or scopes the soak to a
|
|
116
|
+
non-moving topic). No user data strands.
|
|
117
|
+
- **Generated URLs?** None.
|
|
118
|
+
|
|
119
|
+
Multi-dev note: the dev-agent gate resolves per-machine, so a multi-dev setup should activate
|
|
120
|
+
pool-consistently (documented in the spec Rollout §4) to avoid divergent behavior across topic-holding machines.
|
|
121
|
+
|
|
122
|
+
---
|
|
123
|
+
|
|
124
|
+
## Migration parity
|
|
125
|
+
|
|
126
|
+
Config default `multiMachine.sessionPool.moveIntent` is added under the existing `sessionPool` block in
|
|
127
|
+
`ConfigDefaults.ts`; `applyDefaults`/`getMigrationDefaults` deep-merges it add-missing onto existing agents
|
|
128
|
+
(the documented sessionPool migration-parity path — verified: the whole config/migration test family, 1191
|
|
129
|
+
tests, stays green). `enabled` is OMITTED so `resolveDevAgentGate` decides (dark fleet / live dev);
|
|
130
|
+
registered in `DEV_GATED_FEATURES` (both-sides wiring test green). No hook/CLAUDE.md-template/skill changes
|
|
131
|
+
(the move-by-nickname capability is dev-only experimental behind the dark session pool, so no
|
|
132
|
+
Agent-Awareness template surface is added).
|
|
133
|
+
|
|
134
|
+
## Rollback
|
|
135
|
+
|
|
136
|
+
Pure revert. No schema/data migration to unwind. To disable without reverting: set
|
|
137
|
+
`multiMachine.sessionPool.moveIntent.enabled: false` in config (force-dark even on a dev agent), or leave the
|
|
138
|
+
session pool at `stage: dark` (the default) — either fully inerts the path. `logs/move-intent.jsonl` is
|
|
139
|
+
append-only observability and safe to delete.
|
|
140
|
+
|
|
141
|
+
## Risk
|
|
142
|
+
|
|
143
|
+
Low. Ships dark on the fleet AND dry-run-first on dev (logs would-hijack/would-pass, actuates nothing). The
|
|
144
|
+
change strictly REDUCES risk versus the shipped keyword recognizer (fail-open replaces false-positive
|
|
145
|
+
hijack). The residual risk (a confidently-wrong model once `dryRun:false`) is gated by the graduation-gate
|
|
146
|
+
live benchmark + user-role Live-Channel proof + the soak, all required before actuation. tsc clean; lint
|
|
147
|
+
clean; all three test tiers green including the discrimination corpus.
|
|
148
|
+
|
|
149
|
+
## Post-rebase addendum (2026-07-04)
|
|
150
|
+
|
|
151
|
+
Rebased onto current `main` (clean). The new LLM component `MoveIntentClassifier` was registered across the
|
|
152
|
+
LLM-coverage ratchets that landed on main (#1366 and the bench family): `src/data/llmBenchCoverage.ts` —
|
|
153
|
+
`LLM_UNTRUSTED_INPUT: true` (it reads untrusted user message + context), `LLM_JUDGES_CLAIMS: false` (it
|
|
154
|
+
classifies a USER's move intent, not an agent/session completion/health/credit claim), `LLM_PARSER_CONTRACT:
|
|
155
|
+
{ pending: 'contract-wave-2' }` (it parses a closed verdict), and `LLM_BENCH_COVERAGE: { exempt }` (it ships
|
|
156
|
+
its own discrimination benchmark). The keyword-intent-decision ratchet's baseline was decremented 6→5 and
|
|
157
|
+
`NicknameCommand.ts` removed from its `EXPECTED_OFFENDERS` (offender #2 converted — exactly what that ratchet
|
|
158
|
+
tracks). The `parseMoveIntentResponse` fail-open catch carries an `@silent-fallback-ok` marker (the
|
|
159
|
+
documented fail-open, surfaced via `result.source`/`reason` + the audit log, never a silent swallow). No
|
|
160
|
+
behavior change from these — they are the coverage/observability registrations a new LLM component requires.
|