instar 1.3.768 → 1.3.770
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.map +1 -1
- package/dist/commands/server.js +18 -0
- package/dist/commands/server.js.map +1 -1
- package/dist/core/slackRefreshBinding.d.ts +20 -0
- package/dist/core/slackRefreshBinding.d.ts.map +1 -1
- package/dist/core/slackRefreshBinding.js +31 -0
- package/dist/core/slackRefreshBinding.js.map +1 -1
- package/package.json +1 -1
- package/scripts/lint-machine-local-justification.js +273 -0
- package/scripts/lint-self-heal-fields.js +259 -0
- package/src/data/builtin-manifest.json +2 -2
- package/upgrades/1.3.769.md +62 -0
- package/upgrades/1.3.770.md +40 -0
- package/upgrades/eli16/act1174-lint-floors.eli16.md +75 -0
- package/upgrades/side-effects/act1174-lint-floors.md +124 -0
- package/upgrades/side-effects/slack-respawn-bind-token.eli16.md +46 -0
- package/upgrades/side-effects/slack-respawn-bind-token.md +121 -0
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Adds two enforcement lints — deterministic, no-LLM parser scripts — that back the two
|
|
9
|
+
already-ratified constitutional standards A ("An Instar Agent Is Always a Multi-Machine
|
|
10
|
+
Entity") and B ("Self-Heal Before Notify") with a cheap structural signal, per
|
|
11
|
+
`docs/specs/three-standards-enforcement.md` (§178-202 for A, §256-289 / §343-361 for B).
|
|
12
|
+
|
|
13
|
+
- `scripts/lint-machine-local-justification.js` grades a spec's `machine-local-justification: <taxonomy-key>`
|
|
14
|
+
marker: an undefended machine-local surface is a finding, and — bidirectionally — a
|
|
15
|
+
spurious/out-of-taxonomy marker, or an `operator-ratified-exception` citing no
|
|
16
|
+
machine-verifiable ref, is a finding too.
|
|
17
|
+
- `scripts/lint-self-heal-fields.js` grades a spec's self-heal declaration (anchored on
|
|
18
|
+
the `remediation-actions` field): it requires the full P19 brake set, a non-empty
|
|
19
|
+
`remediation-actions` list (the anti-no-op floor), a units-carrying
|
|
20
|
+
`max-notification-latency`, and a well-formed severity class.
|
|
21
|
+
|
|
22
|
+
Both ship REPORT-FIRST — they print findings and exit 0 by default (a non-blocking
|
|
23
|
+
signal); a `--strict` flag is the FAIL capability used by their tests and available for a
|
|
24
|
+
later CI graduation. Each is registered in `docs/STANDARDS-REGISTRY.md` so the
|
|
25
|
+
Standards Enforcement-Coverage auditor now grades Standards A and B as enforced by a
|
|
26
|
+
deterministic lint (verified: two more standards move from unenforced to lint-enforced).
|
|
27
|
+
No constitutional text is minted or re-ratified; no runtime `src/` code changes.
|
|
28
|
+
|
|
29
|
+
## What to Tell Your User
|
|
30
|
+
|
|
31
|
+
Nothing proactive — this is instar-developing-agent tooling and nothing changes for a
|
|
32
|
+
user. If a user asks how the constitution's "always multi-machine" and "self-heal before
|
|
33
|
+
telling me" rules are actually enforced now: two small automatic checkers read a design
|
|
34
|
+
document's plain text and flag when it forgets to say which machine a piece of state
|
|
35
|
+
lives on, or writes a self-repair plan that would do nothing or never stop trying. For now
|
|
36
|
+
these run in report-only mode — they point out the problem but do not block anything — and
|
|
37
|
+
they sit underneath the smart reviewer that still makes the real judgment call, rather than
|
|
38
|
+
replacing it.
|
|
39
|
+
|
|
40
|
+
## Summary of New Capabilities
|
|
41
|
+
|
|
42
|
+
- **Standard A marker floor** — a no-AI checker that flags an undefended machine-local
|
|
43
|
+
state surface in a spec, and also flags the reverse: a made-up justification key or an
|
|
44
|
+
operator-ratified claim with no verifiable proof.
|
|
45
|
+
- **Standard B self-heal floor** — a no-AI checker that flags a watchdog self-heal plan
|
|
46
|
+
missing its brakes, listing no real repair actions, or writing a notification deadline
|
|
47
|
+
with no time units.
|
|
48
|
+
- **Report-first rollout** — both checkers signal without blocking by default; a strict
|
|
49
|
+
mode exists for their tests and a future graduation.
|
|
50
|
+
- **Constitution coverage** — Standards A and B are now graded as lint-enforced by the
|
|
51
|
+
Standards Enforcement-Coverage auditor, not merely documented.
|
|
52
|
+
|
|
53
|
+
## Evidence
|
|
54
|
+
|
|
55
|
+
- `tests/unit/lint-machine-local-justification.test.ts` + `tests/unit/lint-self-heal-fields.test.ts`
|
|
56
|
+
— 13 unit tests (7 A + 6 B): positive/defended cases pass; undefended (A1),
|
|
57
|
+
spurious-key (A2), and no-ref (A2) fail under strict; missing-brakes (B1), no-op +
|
|
58
|
+
unitless + unknown-class (B2/B3/B4) fail under strict; out-of-scope is clean; and report
|
|
59
|
+
mode exits 0 on every bad fixture.
|
|
60
|
+
- `node scripts/standards-coverage.mjs` reports the lint-enforced standard count rising
|
|
61
|
+
from 0 to 2 with Standards A and B removed from the unenforced-gaps list and zero
|
|
62
|
+
dangling references; the coverage ratchet check passes.
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
# Upgrade Guide — vNEXT
|
|
2
|
+
|
|
3
|
+
<!-- assembled-by: assemble-next-md -->
|
|
4
|
+
<!-- bump: patch -->
|
|
5
|
+
|
|
6
|
+
## What Changed
|
|
7
|
+
|
|
8
|
+
Fixed the final live-proven S7 gap: a Slack session that gets **respawned** (via `/sessions/refresh`,
|
|
9
|
+
a quota-swap, a restart, or restart-all — all funnel through `SessionRefresh` → `slackRespawner`) now
|
|
10
|
+
re-mints its conversation bind token. A **fresh** Slack spawn passes `bootstrapConversationIds` so the
|
|
11
|
+
session gets `INSTAR_BIND_TOKEN` + `INSTAR_CONVERSATION_ID` and can open durable state (a commitment)
|
|
12
|
+
bound to its minted conversation id; the respawn path **omitted** it, so a refreshed/quota-swapped Slack
|
|
13
|
+
session came up token-less, its durable binds were refused (fail-closed), and the follow-through fell
|
|
14
|
+
back to a fragile session-local timer that dies on the next restart. The respawn now resolves the
|
|
15
|
+
conversation id from the routing key (idempotent get-or-create) and passes it, restoring parity with the
|
|
16
|
+
fresh spawn. Telegram was unaffected (it has a `telegramTopicId` fallback; Slack had none).
|
|
17
|
+
|
|
18
|
+
## What to Tell Your User
|
|
19
|
+
|
|
20
|
+
Nothing proactive — this is an internal robustness fix. If a user ever asks why a Slack promise didn't
|
|
21
|
+
survive a session swap or restart, the answer is that a restarted Slack session used to lose the small
|
|
22
|
+
security key it needs to save a durable, restart-proof promise, so it fell back to a flimsy timer; now
|
|
23
|
+
a restarted Slack session keeps that key, so the durable follow-through survives restarts.
|
|
24
|
+
|
|
25
|
+
## Summary of New Capabilities
|
|
26
|
+
|
|
27
|
+
- A refreshed / quota-swapped / restarted Slack session now registers durable, restart-surviving
|
|
28
|
+
follow-through (a commitment bound to the Slack conversation), matching a fresh spawn.
|
|
29
|
+
- Fail-open: if the conversation-id lookup errors, the respawn proceeds (prior token-less behavior),
|
|
30
|
+
never blocked.
|
|
31
|
+
- No config, no persistent state, no Telegram change.
|
|
32
|
+
|
|
33
|
+
## Evidence
|
|
34
|
+
|
|
35
|
+
- `tests/unit/slack-respawn-bootstrap-ids.test.ts` — 4 cases (id → `[id]`, full `channel:thread` key,
|
|
36
|
+
`null` → `undefined`, throwing registry → `undefined` / no rethrow).
|
|
37
|
+
- Existing `tests/unit/sessionRefresh-slack.test.ts` (22) stay green; `tsc` clean.
|
|
38
|
+
- Live proof it closes: `docs/investigations/s7-slack-delivery-repro-2026-07-04.md` §9 — Round-1
|
|
39
|
+
(already-running session) durable bind refused; Round-2 (fresh session) durable `CMT-1922` bound to
|
|
40
|
+
the minted id. This fix makes the respawn path behave like Round-2.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
# ELI16 — Two deterministic lint floors for Standards A and B
|
|
2
|
+
|
|
3
|
+
## The one-sentence version
|
|
4
|
+
|
|
5
|
+
Two tiny no-AI checkers now read a design spec's plain text and flag when it forgets
|
|
6
|
+
the "which machine does this live on?" tag (Standard A) or writes a broken
|
|
7
|
+
"try to fix it yourself before bugging the operator" plan (Standard B) — cheap
|
|
8
|
+
signals that back up the smart reviewer instead of replacing it.
|
|
9
|
+
|
|
10
|
+
## Why this exists
|
|
11
|
+
|
|
12
|
+
Instar's constitution has two rules that used to live only in prose and reviewer
|
|
13
|
+
habit:
|
|
14
|
+
|
|
15
|
+
- **Standard A — "always multi-machine":** every piece of state a feature adds should
|
|
16
|
+
be SHARED across all the agent's machines by default. Keeping it on one machine is
|
|
17
|
+
only allowed for a short list of concrete reasons (a login/key that physically sits
|
|
18
|
+
on one disk, a piece of hardware, or something the operator explicitly signed off on).
|
|
19
|
+
This rule was ratified *because* a spec once wrongly kept its memory on one machine
|
|
20
|
+
and it survived SEVEN review rounds — only the operator caught it.
|
|
21
|
+
- **Standard B — "self-heal before notify":** a watchdog should try a bounded, logged
|
|
22
|
+
self-repair FIRST and only ping the operator when that repair genuinely runs out of
|
|
23
|
+
road. A repair plan that does nothing, or that can retry forever, is exactly the trap
|
|
24
|
+
the rule forbids.
|
|
25
|
+
|
|
26
|
+
The smart `/spec-converge` reviewer already reads for these. But "a reviewer will
|
|
27
|
+
notice" is willpower, and Instar's root rule is **structure beats willpower**. So each
|
|
28
|
+
rule gets a cheap deterministic floor that runs first and never needs to remember.
|
|
29
|
+
|
|
30
|
+
## What actually shipped
|
|
31
|
+
|
|
32
|
+
Two small parser scripts (no AI, just text rules) plus their tests:
|
|
33
|
+
|
|
34
|
+
- **`scripts/lint-machine-local-justification.js` (Standard A).** It looks in a spec's
|
|
35
|
+
`## Multi-machine posture` section. If the spec says a surface is "machine-local" but
|
|
36
|
+
carries no `machine-local-justification: <reason>` tag, that's a finding. It also
|
|
37
|
+
checks the OTHER direction: a tag with a made-up reason (not one of the three allowed),
|
|
38
|
+
or an "operator ratified it" claim that cites no checkable proof (a commit SHA, a URL,
|
|
39
|
+
or a registry key), is also a finding.
|
|
40
|
+
- **`scripts/lint-self-heal-fields.js` (Standard B).** When a spec declares a watchdog's
|
|
41
|
+
self-heal, it must list all its brakes (max attempts, time limit, backoff, dedupe key,
|
|
42
|
+
a breaker, a notification deadline with real units like "300s", an audit location, and
|
|
43
|
+
the concrete repair actions), plus the severity class. An empty repair-actions list
|
|
44
|
+
(the fake heal that does nothing) or a deadline written as a bare "300" with no units
|
|
45
|
+
is a finding.
|
|
46
|
+
|
|
47
|
+
Both are registered in `docs/STANDARDS-REGISTRY.md` so Instar's own conformance auditor
|
|
48
|
+
now grades Standards A and B as enforced by a real deterministic `lint` on disk, not just
|
|
49
|
+
"documented" (the auditor confirmed: 0 → 2 lint-enforced standards).
|
|
50
|
+
|
|
51
|
+
## The important nuance: report-first
|
|
52
|
+
|
|
53
|
+
These lints ship in **report mode** — they PRINT their findings but exit 0 (they do not
|
|
54
|
+
block anything yet). A `--strict` flag makes them exit non-zero; that is the FAIL
|
|
55
|
+
capability the tests exercise and the hook a future graduation can flip on. This is
|
|
56
|
+
deliberate: the marker convention is brand new, existing specs predate it, and the spec's
|
|
57
|
+
own honesty clause says the deterministic floor's grade is "inert until the registry
|
|
58
|
+
ship." Shipping a brand-new blocking gate over the whole spec corpus would be a wall of
|
|
59
|
+
false failures. Report-first is the graduated-rollout path Instar always takes with a new
|
|
60
|
+
enforcement.
|
|
61
|
+
|
|
62
|
+
## Signal vs. authority
|
|
63
|
+
|
|
64
|
+
Neither lint decides the hard question. Standard A's lint cannot tell whether a
|
|
65
|
+
justification is TRUE; Standard B's lint cannot tell whether a repair is genuinely
|
|
66
|
+
SUBSTANTIVE or a severity label is HONEST. Those calls stay with the smart `/spec-converge`
|
|
67
|
+
reviewer. The lint is the cheap body; the reviewer is the mind. Together they enforce the
|
|
68
|
+
rule; neither alone does.
|
|
69
|
+
|
|
70
|
+
## What did NOT change
|
|
71
|
+
|
|
72
|
+
No constitutional text was written or re-ratified — Standards A and B were already
|
|
73
|
+
ratified by the operator on 2026-07-03. This change is purely the enforcement machinery
|
|
74
|
+
(two lints + tests + the registry rows that register them as guards). It touches no
|
|
75
|
+
runtime server code and changes nothing for a user.
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
# Side-Effects Review — ACT-1174: two deterministic lint floors (Standards A + B)
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `act1174-lint-floors`
|
|
4
|
+
**Date:** `2026-07-04`
|
|
5
|
+
**Author:** `echo (build hand)`
|
|
6
|
+
**Second-pass reviewer:** `not required (Tier 1)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
Ships the two DEFERRED deterministic lint floors named in
|
|
11
|
+
`docs/specs/three-standards-enforcement.md` (§178-202 for A, §256-289/§343-361 for B) —
|
|
12
|
+
the "registry ship" the spec hard-sequences against each standard's registered guard.
|
|
13
|
+
Two no-LLM parser scripts + their unit tests + fixtures, plus the STANDARDS-REGISTRY
|
|
14
|
+
guard-registration rows that let the conformance auditor grade Standards A and B as
|
|
15
|
+
enforced by a deterministic `lint`.
|
|
16
|
+
|
|
17
|
+
Files touched:
|
|
18
|
+
- `scripts/lint-machine-local-justification.js` — Standard A marker floor (no-LLM).
|
|
19
|
+
- `scripts/lint-self-heal-fields.js` — Standard B self-heal field-schema floor (no-LLM).
|
|
20
|
+
- `tests/unit/lint-machine-local-justification.test.ts`, `tests/unit/lint-self-heal-fields.test.ts` — self-tests.
|
|
21
|
+
- `tests/fixtures/spec-lint/*.md` — positive / negative / bidirectional / out-of-scope fixtures.
|
|
22
|
+
- `docs/STANDARDS-REGISTRY.md` — the two `**Applied through.**` rows now cite the lints (enforcement registration, NOT new ratification prose).
|
|
23
|
+
- `upgrades/eli16/act1174-lint-floors.eli16.md`, `upgrades/side-effects/act1174-lint-floors.md`, `upgrades/next/act1174-lint-floors.md` — gate artifacts.
|
|
24
|
+
|
|
25
|
+
No constitutional text is minted or re-ratified (the standard TEXTS were ratified by the operator 2026-07-03). No runtime `src/` code changes.
|
|
26
|
+
|
|
27
|
+
## Decision-point inventory
|
|
28
|
+
|
|
29
|
+
- `Standards Enforcement-Coverage auditor classification (StandardsEnforcementAuditor)` — pass-through — the audit reads the registry and now classifies A + B as `lint` because the two rows cite `scripts/lint-*.js` (verified: baseline lint 0 → 2, A/B out of the gaps list, dangling still 0).
|
|
30
|
+
- `/spec-converge integration reviewer` — pass-through — the semantic authority is unchanged; these lints are the deterministic SIGNAL beneath it.
|
|
31
|
+
|
|
32
|
+
---
|
|
33
|
+
|
|
34
|
+
## 1. Over-block
|
|
35
|
+
|
|
36
|
+
The lints DO have a block/allow surface, but ONLY under `--strict`. In report mode (the shipped default) they never block. Under `--strict` the plausible over-block is: a spec that legitimately mentions "machine-local" inside its `## Multi-machine posture` section as prose discussion (not a real surface) would trip A1. This is mitigated by scoping A1's trigger to the posture section AND skipping `<placeholder>` marker values, and by the report-first default — a false positive is a printed line, never a blocked commit. `--strict` is opt-in (tests + a future graduation), so no live over-block exists today.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## 2. Under-block
|
|
41
|
+
|
|
42
|
+
The deterministic floor is PRESENCE + well-formedness only, by design (Signal vs. Authority). It misses: a marker whose key is valid but substantively WRONG (A), and a self-heal whose fields are all present but whose remediation is a plausible-but-ineffective no-op or whose severity class is dishonestly `recoverable` (B). Those are exactly the semantic calls left to the `/spec-converge` reviewer — the lint is not meant to catch them. A spec that omits a posture section entirely is also not flagged (that is the reviewer's §168 call).
|
|
43
|
+
|
|
44
|
+
---
|
|
45
|
+
|
|
46
|
+
## 3. Level-of-abstraction fit
|
|
47
|
+
|
|
48
|
+
Correct layer: a cheap, brittle DETECTOR that produces a signal, explicitly paired with the existing high-context `/spec-converge` reviewer that owns the semantic authority. This is the constitutional Signal-vs-Authority / Body-and-Mind split the spec mandates (§87-95). The lint does NOT hold blocking authority over a merge in its shipped (report) mode.
|
|
49
|
+
|
|
50
|
+
---
|
|
51
|
+
|
|
52
|
+
## 4. Signal vs authority compliance
|
|
53
|
+
|
|
54
|
+
**Required reference:** docs/signal-vs-authority.md
|
|
55
|
+
|
|
56
|
+
- [x] No — this change produces a signal consumed by an existing smart gate (the `/spec-converge` integration reviewer), and in its shipped mode it does not block at all.
|
|
57
|
+
|
|
58
|
+
The lints are deterministic signals. Their `--strict` FAIL capability exists for tests and a future, separately-decided CI graduation; it is not wired into the blocking `npm run lint` chain. The reviewer holds semantic authority.
|
|
59
|
+
|
|
60
|
+
---
|
|
61
|
+
|
|
62
|
+
## 5. Interactions
|
|
63
|
+
|
|
64
|
+
- **Shadowing:** none. The lints are standalone scripts, not wired into the blocking lint `&&`-chain, so they cannot shadow an existing lint or be shadowed.
|
|
65
|
+
- **Double-fire:** none. No runtime path invokes them; they are dev/CI tooling run explicitly or by their tests.
|
|
66
|
+
- **Races:** none. Pure fs-read parsers, no shared state.
|
|
67
|
+
- **Feedback loops:** none.
|
|
68
|
+
|
|
69
|
+
The one real interaction is the Standards Enforcement-Coverage auditor: the registry rows now resolve to on-disk `lint` guards, which RAISES the enforced ratio (0.4429 → 0.4714). That is the intended effect, not a regression; the coverage ratchet (`standards-coverage.mjs --check`) passes (floors: ratio ≥ floor, dangling ≤ 0).
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
|
+
## 6. External surfaces
|
|
74
|
+
|
|
75
|
+
- Other agents on the same machine? No.
|
|
76
|
+
- Install base? These are instar-repo dev/CI tooling; they do not ship into agent homes as runtime behavior.
|
|
77
|
+
- External systems? No.
|
|
78
|
+
- Persistent state? No (the coverage script's `.instar/standards-coverage.json` output is untracked runtime state, never a committed baseline).
|
|
79
|
+
- Operator surface (Mobile-Complete): No operator-facing actions — dev/CI tooling only.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
## 6b. Operator-surface quality
|
|
84
|
+
|
|
85
|
+
No operator surface — not applicable.
|
|
86
|
+
|
|
87
|
+
---
|
|
88
|
+
|
|
89
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
90
|
+
|
|
91
|
+
**machine-local BY DESIGN — pure per-machine dev/CI tooling with no durable state.** These lint scripts + tests + fixtures are repo source, replicated to every machine via git like all source (so there is no per-machine divergence to reconcile). They emit no user-facing notices (no one-voice gating needed), hold no durable state (nothing to strand on a topic transfer), and generate no URLs. The registry rows they add are likewise git-replicated source. There is no runtime state surface introduced by this change, so the multi-machine posture question is satisfied structurally: source is uniform across machines by construction.
|
|
92
|
+
|
|
93
|
+
---
|
|
94
|
+
|
|
95
|
+
## 8. Rollback cost
|
|
96
|
+
|
|
97
|
+
Pure additive dev/CI change — revert the commit and ship as the next patch. No persistent state, no data migration, no agent-state repair, no user-visible regression. The registry rows revert cleanly (the conformance auditor simply re-grades A/B back to their prior kind). Because the lints ship report-first and are not in the blocking chain, a rollback cannot un-block anything that was blocking.
|
|
98
|
+
|
|
99
|
+
## Conclusion
|
|
100
|
+
|
|
101
|
+
The review produced no design changes. The two lints are correctly scoped as deterministic signals beneath the existing reviewer authority, ship report-first per the spec's honesty / hard-sequencing clause, and are registered so the conformance auditor grades Standards A and B as `lint`-enforced. Clear to ship as a Tier-1 change.
|
|
102
|
+
|
|
103
|
+
---
|
|
104
|
+
|
|
105
|
+
## Second-pass review (if required)
|
|
106
|
+
|
|
107
|
+
**Reviewer:** not required (Tier 1)
|
|
108
|
+
**Independent read of the artifact: [concur]**
|
|
109
|
+
|
|
110
|
+
Tier-1 low-risk additive tooling; no independent reviewer required per the tier policy.
|
|
111
|
+
|
|
112
|
+
---
|
|
113
|
+
|
|
114
|
+
## Evidence pointers
|
|
115
|
+
|
|
116
|
+
- `npx vitest run tests/unit/lint-machine-local-justification.test.ts tests/unit/lint-self-heal-fields.test.ts` → 13 passed (7 A + 6 B).
|
|
117
|
+
- `node scripts/standards-coverage.mjs` → `lint 2` (baseline 0), A/B absent from the gaps list, dangling 0, enforced-ratio 0.4429 → 0.4714.
|
|
118
|
+
- Manual fixture runs: A good-defended/good-ratified pass `--strict`; A-bad-undefended (A1), A-bad-spurious-key (A2), A-bad-ratified-noref (A2) fail `--strict`; B-good-complete + B-out-of-scope pass; B-bad-missing-fields (B1), B-bad-noop-and-unitless (B2/B3/B4) fail `--strict`; report mode exits 0 on all bad fixtures.
|
|
119
|
+
|
|
120
|
+
---
|
|
121
|
+
|
|
122
|
+
## Class-Closure Declaration (display-only mirror)
|
|
123
|
+
|
|
124
|
+
No agent-authored-artifact defect and no self-triggered controller — not applicable. This change adds enforcement lints; it does not fix a defect in an LLM prompt/hook/config/skill/standards-text, and it introduces no loop/monitor/sentinel/reaper/scheduler/recovery path.
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
# ELI16 — when a Slack session restarts, it now keeps its "durable-memory key"
|
|
2
|
+
|
|
3
|
+
## The story
|
|
4
|
+
|
|
5
|
+
There's a safety feature: when you ask the agent (over Slack) to do something later — "post a note in
|
|
6
|
+
5 minutes" — it writes that promise down as a **durable commitment** so it survives even if the agent's
|
|
7
|
+
session restarts. To write it down, the session needs a small **security key** (a bind token) that
|
|
8
|
+
proves it's allowed to save state for *that specific Slack conversation*. The key is handed to the
|
|
9
|
+
session the moment it starts up.
|
|
10
|
+
|
|
11
|
+
The bug: a Slack session gets restarted fairly often — when the agent swaps to a different account to
|
|
12
|
+
avoid a rate limit, when it's refreshed, or after a crash. Every time a *fresh* session starts for a
|
|
13
|
+
Slack channel, it correctly gets the key. But every time a session was *restarted*, the code forgot to
|
|
14
|
+
hand it the key again. So a restarted Slack session came up **without** its key, couldn't write down the
|
|
15
|
+
durable promise (the save was refused for safety), and fell back to a flimsy in-memory timer instead —
|
|
16
|
+
one that vanishes if the session restarts again. That's exactly the failure we've been chasing: a Slack
|
|
17
|
+
promise that quietly loses its restart-proof backup after the session gets swapped.
|
|
18
|
+
|
|
19
|
+
We proved this live earlier today, as a real Slack user: a message to an *already-restarted* session →
|
|
20
|
+
the durable save was refused → timer fallback ("if my session had died, this note would not have been
|
|
21
|
+
delivered"); a message to a *fresh* session → durable save worked. Same code, one path missing the key.
|
|
22
|
+
|
|
23
|
+
## What this change does
|
|
24
|
+
|
|
25
|
+
It makes the restart path hand the session its key again — the same way the fresh-start path already
|
|
26
|
+
does. When a Slack session restarts, the code now looks up which Slack conversation it belongs to (a
|
|
27
|
+
cheap, repeatable lookup that returns the same id every time) and gives the new session a key scoped to
|
|
28
|
+
that conversation. So a restarted Slack session can write down durable, restart-proof promises again,
|
|
29
|
+
just like a fresh one.
|
|
30
|
+
|
|
31
|
+
## Why it's safe
|
|
32
|
+
|
|
33
|
+
- It only *restores* a capability that was accidentally dropped; it adds no new powers. The security
|
|
34
|
+
gate that checks the key is unchanged — this just makes sure the rightful session actually gets its
|
|
35
|
+
own key.
|
|
36
|
+
- If the lookup ever fails, it quietly falls back to the old (keyless) behavior instead of crashing —
|
|
37
|
+
a restart must never be blocked.
|
|
38
|
+
- Telegram was never affected (it had a different fallback); this only fixes the Slack restart path.
|
|
39
|
+
- It's a plain code change with no saved data, so rolling it back is trivial.
|
|
40
|
+
|
|
41
|
+
## How we know it works
|
|
42
|
+
|
|
43
|
+
Four small tests check the key-lookup helper: it returns the right conversation id, it passes the full
|
|
44
|
+
channel/thread key through, it returns "no key" when there's no id, and — importantly — it never throws
|
|
45
|
+
even if the lookup blows up (so a refresh can't be blocked). The 22 existing Slack-refresh tests all
|
|
46
|
+
still pass.
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
# Side-Effects Review — Slack session respawn re-mints the conversation bind token
|
|
2
|
+
|
|
3
|
+
**Version / slug:** `slack-respawn-bind-token`
|
|
4
|
+
**Date:** `2026-07-04`
|
|
5
|
+
**Author:** `Echo`
|
|
6
|
+
**Second-pass reviewer:** `not required (Tier-1)`
|
|
7
|
+
|
|
8
|
+
## Summary of the change
|
|
9
|
+
|
|
10
|
+
A FRESH Slack channel spawn passes `bootstrapConversationIds: [conversationId]` to
|
|
11
|
+
`spawnInteractiveSession`, so the session mints `INSTAR_BIND_TOKEN` + `INSTAR_CONVERSATION_ID`
|
|
12
|
+
(durable-conversation-identity §7) and can open durable state — a commitment bound to its minted
|
|
13
|
+
(negative) conversation id. The Slack session **respawn** closure (`slackRespawner` in
|
|
14
|
+
`src/commands/server.ts`, used by `/sessions/refresh`, quota-swap, restart, and restart-all — all
|
|
15
|
+
funnel through `SessionRefresh` → `slackRespawner`) **omitted** `bootstrapConversationIds`. So a
|
|
16
|
+
refreshed/quota-swapped Slack session came up **token-less** (and without `INSTAR_CONVERSATION_ID`),
|
|
17
|
+
its durable commitment binds were **refused fail-closed**, and the follow-through fell back to a
|
|
18
|
+
fragile session-local timer that dies on the next restart. This is the live-proven S7 gap (Round-1 of
|
|
19
|
+
the 2026-07-04 test-as-self proof: an already-running/refreshed Slack session couldn't register a
|
|
20
|
+
durable commitment; a fresh spawn — Round-2 — could, registering `CMT-1922` bound to `-1734007126`).
|
|
21
|
+
Telegram's respawn is unaffected because it passes `telegramTopicId`, which the bind-token env
|
|
22
|
+
resolver uses as a fallback; Slack has no such fallback. Fix: the respawn closure resolves the
|
|
23
|
+
conversation id from the routing key (`conversationRegistry.mintForInbound(routingKey).id`, idempotent
|
|
24
|
+
get-or-create → the SAME id the fresh dispatch resolves) via a new unit-tested helper
|
|
25
|
+
`slackRespawnBootstrapIds` and passes it as `bootstrapConversationIds` — restoring parity with the
|
|
26
|
+
fresh spawn. Files: `src/commands/server.ts` (closure + import), `src/core/slackRefreshBinding.ts`
|
|
27
|
+
(helper), + test.
|
|
28
|
+
|
|
29
|
+
## Decision-point inventory
|
|
30
|
+
|
|
31
|
+
- `slackRespawner` closure spawn options (`src/commands/server.ts`) — **modify** — now passes
|
|
32
|
+
`bootstrapConversationIds` resolved from the routing key.
|
|
33
|
+
- `slackRespawnBootstrapIds` (`src/core/slackRefreshBinding.ts`) — **add** — the pure, tested resolver.
|
|
34
|
+
|
|
35
|
+
## 1. Over-block / 2. Under-block
|
|
36
|
+
|
|
37
|
+
No block/allow surface. This RESTORES a capability (durable bind on a respawned Slack session) that was
|
|
38
|
+
mistakenly dropped. Over/under-block not applicable.
|
|
39
|
+
|
|
40
|
+
## 3. Level-of-abstraction fit
|
|
41
|
+
|
|
42
|
+
Right layer — the fix is a one-option addition at the respawn spawn site, mirroring the fresh-spawn
|
|
43
|
+
site exactly; the resolution is a tiny pure helper in the existing `slackRefreshBinding` module. No new
|
|
44
|
+
machinery, no new authority.
|
|
45
|
+
|
|
46
|
+
## 4. Signal vs authority compliance
|
|
47
|
+
|
|
48
|
+
**Required reference:** [docs/signal-vs-authority.md](../../docs/signal-vs-authority.md)
|
|
49
|
+
|
|
50
|
+
- [x] No — no runtime block/allow surface. The bind token it restores is enforced by the EXISTING §7
|
|
51
|
+
`conversationBindGate` (unchanged); this change only ensures the legitimately-authorized session
|
|
52
|
+
actually receives its own token on respawn.
|
|
53
|
+
|
|
54
|
+
## 5. Interactions
|
|
55
|
+
|
|
56
|
+
- **Shadowing:** none — additive spawn option, mirroring the fresh path.
|
|
57
|
+
- **Double-fire:** none. `mintForInbound` is idempotent (get-or-create), so re-resolving on respawn
|
|
58
|
+
returns the same id the fresh dispatch used; no duplicate conversation is minted.
|
|
59
|
+
- **Races:** none new — the respawn already spawned a session; this only adds one option to that call.
|
|
60
|
+
- **Feedback loops:** none.
|
|
61
|
+
- **Fail posture:** `slackRespawnBootstrapIds` FAILS TOWARD RESPAWN — any resolution error → `undefined`
|
|
62
|
+
(the prior token-less behavior), never throws, so a refresh can never be blocked by id resolution.
|
|
63
|
+
|
|
64
|
+
## 6. External surfaces
|
|
65
|
+
|
|
66
|
+
- **Install base / agents:** ships with the server; a refreshed Slack session now carries its bind
|
|
67
|
+
token. No config migration. No behavior change for a session that already had the token (fresh
|
|
68
|
+
spawns) or for Telegram (unaffected — separate fallback).
|
|
69
|
+
- **External systems:** Slack unchanged (same channel session, same delivery). No new API.
|
|
70
|
+
- **Persistent state:** none — `INSTAR_BIND_TOKEN` lives only in the tmux `-e` env at spawn (stateless,
|
|
71
|
+
self-authenticating). This change sets it correctly on respawn; nothing is persisted.
|
|
72
|
+
|
|
73
|
+
## 6b. Operator-surface quality
|
|
74
|
+
|
|
75
|
+
No operator surface — not applicable.
|
|
76
|
+
|
|
77
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
78
|
+
|
|
79
|
+
**machine-local BY DESIGN** — `machine-local-justification: physical-credential-locality`. A Slack
|
|
80
|
+
session is a tmux process on the machine that owns the Slack connection; its bind token is minted into
|
|
81
|
+
that process's env at spawn on that machine (the token is a per-session, per-machine credential — the
|
|
82
|
+
§7 secret is machine-local plaintext, same posture as authToken). The respawn happens on the owning
|
|
83
|
+
machine. There is no cross-machine state, notice, durable record, or URL introduced; the fix only
|
|
84
|
+
corrects a spawn-option omission on the local respawn path.
|
|
85
|
+
|
|
86
|
+
## 8. Rollback cost
|
|
87
|
+
|
|
88
|
+
Pure code change — revert the closure option, the helper, the import, and the test. No persistent
|
|
89
|
+
state; a rollback returns to the (buggy) token-less respawn. Zero-cost back-out.
|
|
90
|
+
|
|
91
|
+
## Conclusion
|
|
92
|
+
|
|
93
|
+
Closes the final live-proven S7 gap: a refreshed/quota-swapped Slack session now re-mints its
|
|
94
|
+
conversation bind token, so durable, restart-surviving follow-through works even after a session
|
|
95
|
+
churn — matching the fresh-spawn path that the 2026-07-04 test-as-self proof showed already works.
|
|
96
|
+
Minimal, parity-restoring, fail-open, unit-tested. Clear to ship.
|
|
97
|
+
|
|
98
|
+
## Second-pass review (if required)
|
|
99
|
+
|
|
100
|
+
**Reviewer:** not required (Tier-1)
|
|
101
|
+
|
|
102
|
+
## Evidence pointers
|
|
103
|
+
|
|
104
|
+
- `tests/unit/slack-respawn-bootstrap-ids.test.ts` — 4 cases: resolves id → `[id]`, passes the full
|
|
105
|
+
`channel:thread` routing key, `null` id → `undefined`, throwing registry → `undefined` (fails toward
|
|
106
|
+
respawn).
|
|
107
|
+
- Existing `tests/unit/sessionRefresh-slack.test.ts` (22) stay green; `tsc` clean.
|
|
108
|
+
- Live proof: `docs/investigations/s7-slack-delivery-repro-2026-07-04.md` §9 (Round-1 refused vs
|
|
109
|
+
Round-2 durable `CMT-1922`).
|
|
110
|
+
|
|
111
|
+
## Class-Closure Declaration (display-only mirror)
|
|
112
|
+
|
|
113
|
+
- **`defectClass`** — `spawn-option-asymmetry` (`novel`; nearestExistingClass: `feature-un-enablable`;
|
|
114
|
+
includes: a spawn/respawn path that omits an option the parallel fresh-spawn path passes, silently
|
|
115
|
+
degrading a capability; excludes: an intentionally-different respawn option). Enters
|
|
116
|
+
`status:"unconfirmed"`, so this fix carries `closure: gap`.
|
|
117
|
+
- **`closure`** — `gap` — a class-level guard (a lint/test asserting respawn spawn-option parity with
|
|
118
|
+
the fresh-spawn site) is out of scope for this fix.
|
|
119
|
+
- **`guardEvidence`** — n/a for `closure: gap`.
|
|
120
|
+
- **`gap`** — tracked follow-up: "respawn/fresh-spawn option-parity guard — assert the Slack (and other
|
|
121
|
+
platform) respawn paths pass the same identity/bind options as their fresh-spawn counterparts."
|