instar 1.3.753 → 1.3.755
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 +238 -19
- package/dist/commands/server.js.map +1 -1
- package/dist/config/ConfigDefaults.d.ts.map +1 -1
- package/dist/config/ConfigDefaults.js +40 -0
- package/dist/config/ConfigDefaults.js.map +1 -1
- package/dist/core/ProfileIntentClassifier.d.ts +167 -0
- package/dist/core/ProfileIntentClassifier.d.ts.map +1 -0
- package/dist/core/ProfileIntentClassifier.js +421 -0
- package/dist/core/ProfileIntentClassifier.js.map +1 -0
- 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/topicProfileIngress.d.ts +14 -2
- package/dist/core/topicProfileIngress.d.ts.map +1 -1
- package/dist/core/topicProfileIngress.js +15 -44
- package/dist/core/topicProfileIngress.js.map +1 -1
- package/dist/data/llmBenchCoverage.d.ts.map +1 -1
- package/dist/data/llmBenchCoverage.js +15 -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 +16 -0
- package/upgrades/1.3.754.md +46 -0
- package/upgrades/1.3.755.md +75 -0
- package/upgrades/side-effects/keyword-intent-conversion-3-hubcommands.md +170 -0
- package/upgrades/side-effects/topicprofile-intent-llm-rebuild.md +87 -0
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
The recognizer that decides whether a mid-conversation message means *"change this topic's
|
|
9
|
+
framework / model / thinking"* — e.g. "use codex here", "switch this topic to gemini", "set high
|
|
10
|
+
thinking on this topic" — was rebuilt from keyword regexes into an LLM classifier
|
|
11
|
+
(`src/core/ProfileIntentClassifier.ts`). The old code (`parseProfileTrigger`'s
|
|
12
|
+
`/^use (codex|claude|gemini|pi…) …$/`, `/^switch this topic to …$/`, `THINKING_WORDS`, and the
|
|
13
|
+
`pin this topic to <id>$/` forms) was offender #1 of the 2026-07-03 keyword-intent audit: a
|
|
14
|
+
keyword/regex list DECIDING what a human meant. Those regexes were tightly anchored (so few false
|
|
15
|
+
positives), but they are brittle in the MISS direction — a valid paraphrase ("let's run this topic
|
|
16
|
+
on claude") had no anchor — and, more fundamentally, they are the exact anti-pattern the
|
|
17
|
+
constitutional standard *"Intelligence Infers, Keywords Only Guard"* forbids. The new classifier
|
|
18
|
+
infers the change intent from the message **and** a bounded window of recent conversation, and
|
|
19
|
+
constrains the value to a closed enum (framework ∈ configured frameworks, model ∈ known ids/tiers,
|
|
20
|
+
thinking ∈ off/low/medium/high/max) via **code-side validation of the model's emitted field** —
|
|
21
|
+
never string-matching the model's prose. It is conversion #1 under that standard, mirroring the
|
|
22
|
+
merged move-intent exemplar (PR #1367).
|
|
23
|
+
|
|
24
|
+
It **fails open**: on any uncertainty — no provider, breaker open, timeout, unparseable/schema-
|
|
25
|
+
violating output, value-not-in-enum, value-not-grounded-in-the-latest-message, or low confidence —
|
|
26
|
+
the message passes straight through to the agent, never actuating a respawn. A **grounding guard**
|
|
27
|
+
(`valueGroundedInLatestMessage`) additionally requires the resolved value to appear in the LATEST
|
|
28
|
+
message, so a value inferred purely from stale prior context ("yeah go with that") never actuates —
|
|
29
|
+
closing a confirm-slot-bypass the context window would otherwise open. The LLM call is `gating:true`
|
|
30
|
+
(swap-provider-before-fail, no silent heuristic drop). It ships **dev-gated dark on the fleet +
|
|
31
|
+
dry-run first on a development agent** (it logs would-actuate vs would-pass to
|
|
32
|
+
`logs/profile-intent.jsonl` — no raw message content — and actuates nothing until a deliberate
|
|
33
|
+
`dryRun:false`). The framework/model/thinking regexes are removed from `parseProfileTrigger`; the
|
|
34
|
+
command kinds (readout / undo / clear / reapply / switch-now / confirm) and the explicit
|
|
35
|
+
`effort` + `escalationOverride` forms remain (structural / explicit-mandate, not framework/model/
|
|
36
|
+
thinking intent). The `TopicProfileWriteSurface` authority + confirm-slot ordering guards are
|
|
37
|
+
unchanged.
|
|
38
|
+
|
|
39
|
+
## What to Tell Your User
|
|
40
|
+
|
|
41
|
+
Nothing user-facing right now — this ships dark on the fleet and dry-run on a development agent, so
|
|
42
|
+
no behavior changes until it's deliberately graduated (and the whole topic-profile WRITE layer is
|
|
43
|
+
itself already dev-gated + dry-run). If asked why the agent might once have missed a phrasing like
|
|
44
|
+
"let's run this topic on claude", or why "should we use codex here?" should never flip the setting:
|
|
45
|
+
the old recognizer was a brittle regex list, now replaced by an LLM that judges intent from the
|
|
46
|
+
message and its conversation context and errs toward *not* changing your topic when unsure.
|
|
47
|
+
|
|
48
|
+
## Summary of New Capabilities
|
|
49
|
+
|
|
50
|
+
- `src/core/ProfileIntentClassifier.ts` — LLM-with-context framework/model/thinking intent
|
|
51
|
+
recognizer (`classifyProfileIntent` + `toProfilePatch`); structured-output enum guardrail +
|
|
52
|
+
latest-message grounding guard validated in code; fail-open on all uncertainty; `gating:true`.
|
|
53
|
+
- Config `topicProfiles.intentClassifier` (`enabled` dev-gated; `dryRun:true`, `minConfidence:0.85`,
|
|
54
|
+
`timeoutMs:4000`, `contextWindowTurns:6`, `modelTier:'fast'`); registered in `DEV_GATED_FEATURES`;
|
|
55
|
+
attributed `gate` in `componentCategories`; row in `docs/LLM-ROUTING-REGISTRY.md`; bench coverage
|
|
56
|
+
queued (`llmBenchCoverage.ts`, wave-3).
|
|
57
|
+
- `logs/profile-intent.jsonl` — machine-local dry-run soak log (LLM-engaged decisions only; enum
|
|
58
|
+
fields + message length, NO raw content).
|
|
59
|
+
- Committed discrimination corpus + opt-in real-model benchmark (`INSTAR_LIVE_PROFILE_INTENT=1`),
|
|
60
|
+
the graduation gate before `dryRun:false` (≥200 decisions, <1% false-actuation, zero
|
|
61
|
+
context-resolved actuations).
|
|
62
|
+
|
|
63
|
+
## Evidence
|
|
64
|
+
|
|
65
|
+
Not reproducible in dev as a live false-positive: the removed regexes were `^…$`-anchored, so
|
|
66
|
+
they rarely MIS-fired today ("should we use codex here?" never matched `^use …$`). This converts
|
|
67
|
+
the banned anti-pattern (a keyword/regex list DECIDING natural-language intent) pre-emptively —
|
|
68
|
+
the sibling offender #2 (`NicknameCommand`) is the one that produced the actual 2026-07-03 hijack;
|
|
69
|
+
offender #1 is the same CLASS, converted before it can misfire. Observed before/after is proven by
|
|
70
|
+
the committed discrimination corpus (`tests/unit/profile-intent-discrimination.test.ts`): the old
|
|
71
|
+
class fires on the words, not the meaning — the corpus asserts, both directions, that "use codex
|
|
72
|
+
here" actuates while "should we use codex here?", "codex here keeps failing", and a stale-context
|
|
73
|
+
"yeah go with that" all pass through, and that a valid paraphrase the anchored regex MISSED
|
|
74
|
+
("let's run this topic on claude") is now recognized. The deterministic harness runs in CI; the
|
|
75
|
+
opt-in `INSTAR_LIVE_PROFILE_INTENT=1` real-model harness is the 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,87 @@
|
|
|
1
|
+
# Side-Effects Review — topicprofile-intent-llm-rebuild
|
|
2
|
+
|
|
3
|
+
**Change:** Replace the FRAMEWORK/MODEL/THINKING keyword regexes in
|
|
4
|
+
`parseProfileTrigger` with an LLM-with-context recognizer (`ProfileIntentClassifier`),
|
|
5
|
+
per the standard "Intelligence Infers, Keywords Only Guard" (keyword-intent audit offender #1).
|
|
6
|
+
Dev-gated dark + dry-run-first; fail-open.
|
|
7
|
+
|
|
8
|
+
**Spec:** `docs/specs/topicprofile-intent-llm-rebuild.md` (converged, approved)
|
|
9
|
+
|
|
10
|
+
## Phase 1 — Principle check (signal vs authority)
|
|
11
|
+
|
|
12
|
+
Yes — this is a decision point (it gates whether an inbound message actuates a topic-profile write
|
|
13
|
+
vs passes through to the agent). It is designed as a **signal producer**: the classifier's only
|
|
14
|
+
power is to move a message from "actuate a write" toward "pass through" (fail-open). The authority —
|
|
15
|
+
`TopicProfileWriteSurface.applyWrite` — is unchanged and independently re-validates every field
|
|
16
|
+
against the closed enums. No brittle check holds blocking authority; the classifier can never
|
|
17
|
+
actuate a value the write surface would reject.
|
|
18
|
+
|
|
19
|
+
## The 8 questions
|
|
20
|
+
|
|
21
|
+
1. **Over-block (rejects legitimate inputs it shouldn't).** The grounding guard requires the
|
|
22
|
+
resolved value to appear in the LATEST message, so a purely context-resolved command ("yeah go
|
|
23
|
+
with that") is intentionally NOT actuated — it passes through to the agent. This is a deliberate
|
|
24
|
+
safety choice (a respawn off stale context is the harm). The cost is cheap: the operator restates
|
|
25
|
+
("switch to gemini") or the agent handles it. Not a defect.
|
|
26
|
+
|
|
27
|
+
2. **Under-block (failure modes it still misses).** A genuinely-intended change the LLM classifies
|
|
28
|
+
as `isChange:false` is a MISS — cheap by design (fail-open; the operator restates). The prefilter
|
|
29
|
+
is drop-only and inclusive, so it does not cause misses beyond the LLM's own judgment. A cross-
|
|
30
|
+
framework model id the classifier passes is refused at the write surface with a named reason (no
|
|
31
|
+
wrong respawn).
|
|
32
|
+
|
|
33
|
+
3. **Level-of-abstraction fit.** Correct layer: the recognizer's DECISION moves from regex to LLM;
|
|
34
|
+
the actuator (write surface, validation, respawn debounce, confirm slots) is untouched. It feeds
|
|
35
|
+
the existing authority rather than paralleling it. The command kinds + effort/escalation stay in
|
|
36
|
+
`parseProfileTrigger` (structural / explicit-mandate forms, not framework/model/thinking intent).
|
|
37
|
+
|
|
38
|
+
4. **Signal vs authority compliance.** Compliant — see Phase 1. Fail-open (never actuates on doubt),
|
|
39
|
+
`gating:true` (swap-provider-before-fail, no silent heuristic drop), independent write-surface
|
|
40
|
+
re-validation.
|
|
41
|
+
|
|
42
|
+
5. **Interactions (shadow / shadowed / double-fire / race).** Runs ONLY when `parseProfileTrigger`
|
|
43
|
+
returns null, so it never shadows or double-fires with the deterministic command kinds (readout /
|
|
44
|
+
undo / clear / reapply / switch-now / confirm) or the retained effort/escalation writes. Bare
|
|
45
|
+
affirmatives ("yes", "do it") are consumed by `parseProfileTrigger`'s `confirm` kind and the
|
|
46
|
+
armed-slot machinery BEFORE the classifier — the classifier never competes with the confirm-slot
|
|
47
|
+
ordering/TTL guards. The grounding guard specifically prevents the classifier from re-introducing
|
|
48
|
+
the confirm-slot-bypass an unconstrained context window would open.
|
|
49
|
+
|
|
50
|
+
6. **External surfaces.** Adds no HTTP route. Writes a dev-agent-only, machine-local soak log
|
|
51
|
+
(`logs/profile-intent.jsonl`) with NO raw message content (enum fields + length only). The LLM
|
|
52
|
+
call is one bounded fast-tier call per candidate message, attributed to `/metrics/features` as
|
|
53
|
+
`ProfileIntentClassifier` (gate). On the fleet it is dark (no external surface changes at all).
|
|
54
|
+
|
|
55
|
+
7. **Multi-machine posture (Cross-Machine Coherence).** Machine-local BY DESIGN
|
|
56
|
+
(`physical-credential-locality`): the recognizer runs on the topic-owning machine's Telegram
|
|
57
|
+
inbound path and gates a session that lives on that machine; there is no shared state to
|
|
58
|
+
replicate. `unified` would be infeasible (nothing to replicate). The one operational note:
|
|
59
|
+
graduation evidence is read from the auto-aggregated `/metrics/features` surface, not by hand-
|
|
60
|
+
unioning per-machine logs. No user-facing notice (one-voice) concern — the only user-facing
|
|
61
|
+
output is the write surface's existing disclosure reply, unchanged. No durable state strands on
|
|
62
|
+
topic transfer (the classifier holds none). No generated URLs.
|
|
63
|
+
|
|
64
|
+
8. **Rollback cost.** Trivial and doubly-inert: `enabled` is omitted (dev-gate dark on the fleet),
|
|
65
|
+
`dryRun:true` default (never actuates), and the whole topic-profile WRITE layer is separately
|
|
66
|
+
dev-gated + dryRun. Flipping `topicProfiles.intentClassifier.enabled:false` (or reverting the
|
|
67
|
+
commit) fully removes it; no data migration, no state repair. On the fleet, framework/model/
|
|
68
|
+
thinking conversational pins simply pass through to the agent, as they would with the feature
|
|
69
|
+
absent.
|
|
70
|
+
|
|
71
|
+
## Phase 5 — Second-pass review (required: inbound-message gate)
|
|
72
|
+
|
|
73
|
+
This change gates inbound messaging, so a dedicated adversarial + integration review was run
|
|
74
|
+
(two independent reviewer passes + two cross-model external passes) during spec convergence. The
|
|
75
|
+
one MATERIAL adversarial finding — context-referential affirmatives bypassing the confirm-slot
|
|
76
|
+
guards to actuate directly off stale context — was found and CLOSED with the deterministic
|
|
77
|
+
`valueGroundedInLatestMessage` grounding guard (the value must be named in the latest message to
|
|
78
|
+
actuate), proven by the `guard-context-only-value` corpus case and unit tests. All minor findings
|
|
79
|
+
(symmetric untrusted-data delimiting, scrubbed log, cleared timeout, context dedupe, `gating:true`
|
|
80
|
+
swap-first) were folded in. The multi-machine reviewer confirmed machine-local-by-design is
|
|
81
|
+
correct.
|
|
82
|
+
|
|
83
|
+
**Concur with the review.** The final design holds no brittle blocking authority, fails open to the
|
|
84
|
+
more-capable agent on every uncertainty, and the write-surface authority + confirm-slot ordering
|
|
85
|
+
guards are unchanged. The single new actuation trigger (a paraphrase the old anchored regex missed)
|
|
86
|
+
is strictly gated by the grounding guard + enum guardrail + confidence gate + dev-gate + dryRun.
|
|
87
|
+
No residual concern.
|