instar 1.3.504 → 1.3.506

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.
Files changed (43) hide show
  1. package/dist/commands/server.d.ts.map +1 -1
  2. package/dist/commands/server.js +46 -0
  3. package/dist/commands/server.js.map +1 -1
  4. package/dist/core/AutonomousSessions.d.ts +19 -0
  5. package/dist/core/AutonomousSessions.d.ts.map +1 -1
  6. package/dist/core/AutonomousSessions.js +71 -0
  7. package/dist/core/AutonomousSessions.js.map +1 -1
  8. package/dist/core/PostUpdateMigrator.d.ts.map +1 -1
  9. package/dist/core/PostUpdateMigrator.js +34 -1
  10. package/dist/core/PostUpdateMigrator.js.map +1 -1
  11. package/dist/core/TransferByNickname.d.ts +11 -0
  12. package/dist/core/TransferByNickname.d.ts.map +1 -1
  13. package/dist/core/TransferByNickname.js +29 -6
  14. package/dist/core/TransferByNickname.js.map +1 -1
  15. package/dist/core/devGatedFeatures.d.ts.map +1 -1
  16. package/dist/core/devGatedFeatures.js +6 -0
  17. package/dist/core/devGatedFeatures.js.map +1 -1
  18. package/dist/core/time-claim.d.ts +57 -0
  19. package/dist/core/time-claim.d.ts.map +1 -0
  20. package/dist/core/time-claim.js +167 -0
  21. package/dist/core/time-claim.js.map +1 -0
  22. package/dist/messaging/OutboundAdvisory.d.ts +20 -2
  23. package/dist/messaging/OutboundAdvisory.d.ts.map +1 -1
  24. package/dist/messaging/OutboundAdvisory.js +37 -1
  25. package/dist/messaging/OutboundAdvisory.js.map +1 -1
  26. package/dist/monitoring/SessionReaper.d.ts +27 -0
  27. package/dist/monitoring/SessionReaper.d.ts.map +1 -1
  28. package/dist/monitoring/SessionReaper.js +41 -2
  29. package/dist/monitoring/SessionReaper.js.map +1 -1
  30. package/dist/scaffold/templates.d.ts.map +1 -1
  31. package/dist/scaffold/templates.js +4 -2
  32. package/dist/scaffold/templates.js.map +1 -1
  33. package/dist/server/routes.d.ts.map +1 -1
  34. package/dist/server/routes.js +96 -9
  35. package/dist/server/routes.js.map +1 -1
  36. package/package.json +1 -1
  37. package/src/data/builtin-manifest.json +64 -64
  38. package/src/scaffold/templates.ts +4 -2
  39. package/src/templates/scripts/telegram-reply.sh +28 -8
  40. package/upgrades/1.3.505.md +57 -0
  41. package/upgrades/1.3.506.md +63 -0
  42. package/upgrades/side-effects/multi-machine-seamlessness-ws12-ws14.md +197 -0
  43. package/upgrades/side-effects/time-claim-advisory.md +177 -0
@@ -38,8 +38,18 @@
38
38
  # and every error path (server down, timeout, bad JSON) proceeds straight
39
39
  # to the send as if the preflight returned nothing. Script-class senders
40
40
  # (INSTAR_SENDER_CLASS=script) skip the preflight — there is no agent to
41
- # inform. Conversational sessions have none of these env vars and are
42
- # completely unaffected.
41
+ # inform.
42
+ #
43
+ # Every OTHER sender (including a conversational session with no stamps —
44
+ # typically an interactive session running an autonomous job) also runs the
45
+ # preflight, with its real message kind defaulting to "reply". The server
46
+ # applies ONLY the TIME_CLAIM check to non-automated kinds — an
47
+ # elapsed/remaining claim contradicting the topic's live session clock
48
+ # (operator mandate 2026-06-12: accurate time reporting is structural, not
49
+ # willpower). Jargon/path/link detectors never run for conversational
50
+ # sends, and when the topic has no active time-boxed session the preflight
51
+ # returns nothing — conversational sends remain effectively unaffected.
52
+ # Same fail-open contract end-to-end.
43
53
  #
44
54
  # Port resolution (in order):
45
55
  # 1. INSTAR_PORT environment variable (explicit operator override).
@@ -180,7 +190,13 @@ esac
180
190
  JOB_SLUG=$(printf '%s' "${INSTAR_JOB_SLUG:-}" | tr -c 'A-Za-z0-9._-' '_' | head -c 128)
181
191
 
182
192
  ADVISORY_CODES_CSV=""
183
- if [ "$MESSAGE_KIND" = "automated" ] && [ "$SENDER_CLASS" = "llm-session" ]; then
193
+ # Preflight gate: every sender EXCEPT script-class (no agent to inform).
194
+ # automated+llm-session → full detector set server-side (unchanged);
195
+ # anything else (incl. unstamped conversational sessions) → the server
196
+ # applies only the TIME_CLAIM clock check, kind defaulting to "reply".
197
+ # An older server returns no advisories for non-automated kinds — the new
198
+ # gate is a no-op against it (version-skew safe both directions).
199
+ if [ "$SENDER_CLASS" != "script" ]; then
184
200
  # Timeout: config messaging.outboundAdvisory.timeoutMs (default 2000ms),
185
201
  # converted ms→SECONDS for curl --max-time with ceil division, clamped to
186
202
  # [1, 10]. A raw `--max-time 2000` would be a ~33-minute fail-HANG and
@@ -194,11 +210,11 @@ if [ "$MESSAGE_KIND" = "automated" ] && [ "$SENDER_CLASS" = "llm-session" ]; the
194
210
 
195
211
  # Preflight body via python3 (already a hard dependency of this script). If
196
212
  # python3 is unavailable the preflight is skipped entirely — fail-open.
197
- PREFLIGHT_BODY=$(PF_TOPIC="$TOPIC_ID" PF_SLUG="$JOB_SLUG" PF_KIND="$MESSAGE_KIND" python3 -c '
213
+ PREFLIGHT_BODY=$(PF_TOPIC="$TOPIC_ID" PF_SLUG="$JOB_SLUG" PF_KIND="${MESSAGE_KIND:-reply}" python3 -c '
198
214
  import sys, json, os
199
215
  print(json.dumps({
200
216
  "text": sys.stdin.read(),
201
- "messageKind": os.environ.get("PF_KIND", "automated"),
217
+ "messageKind": os.environ.get("PF_KIND") or "reply",
202
218
  "topicId": int(os.environ.get("PF_TOPIC") or 0),
203
219
  "jobSlug": os.environ.get("PF_SLUG", ""),
204
220
  }))
@@ -252,7 +268,7 @@ for a in advisories:
252
268
  # was deliberately not yet sent and the next move belongs to the agent.
253
269
  echo "NOT SENT — advisory (fix and re-run, or re-run with --ack-advisory to send unchanged)"
254
270
  echo ""
255
- echo "The outbound advisory flagged this automated message BEFORE delivery:"
271
+ echo "The outbound advisory flagged this outbound message BEFORE delivery:"
256
272
  printf '%s\n' "$ADVISORY_DETAIL"
257
273
  echo ""
258
274
  echo "Next move (yours — the advisory layer never blocks):"
@@ -269,7 +285,7 @@ fi
269
285
  # component is enum-validated or charset-clamped above, so this fragment is
270
286
  # safe to interpolate into JSON and (parameterized) SQL contexts.
271
287
  METADATA_JSON=""
272
- if [ -n "$MESSAGE_KIND" ] || [ -n "$SENDER_CLASS" ] || [ -n "$JOB_SLUG" ]; then
288
+ if [ -n "$MESSAGE_KIND" ] || [ -n "$SENDER_CLASS" ] || [ -n "$JOB_SLUG" ] || { [ "$ACK_ADVISORY" = "1" ] && [ "$SENDER_CLASS" != "script" ]; }; then
273
289
  META_PARTS=""
274
290
  [ -n "$MESSAGE_KIND" ] && META_PARTS="\"messageKind\":\"${MESSAGE_KIND}\""
275
291
  if [ -n "$SENDER_CLASS" ]; then
@@ -280,7 +296,11 @@ if [ -n "$MESSAGE_KIND" ] || [ -n "$SENDER_CLASS" ] || [ -n "$JOB_SLUG" ]; then
280
296
  [ -n "$META_PARTS" ] && META_PARTS="${META_PARTS},"
281
297
  META_PARTS="${META_PARTS}\"jobSlug\":\"${JOB_SLUG}\""
282
298
  fi
283
- if [ "$ACK_ADVISORY" = "1" ] && [ "$MESSAGE_KIND" = "automated" ] && [ "$SENDER_CLASS" = "llm-session" ]; then
299
+ # Ack annotation rides for EVERY non-script sender (not just automated):
300
+ # an unstamped interactive session's --ack-advisory must record 'acked'
301
+ # server-side, or its advised episodes never resolve and the escalation
302
+ # false-fires on messages that actually delivered (second-pass concern 2).
303
+ if [ "$ACK_ADVISORY" = "1" ] && [ "$SENDER_CLASS" != "script" ]; then
284
304
  # REQUIRED annotation (§2.4(4)) — how the server audits "acked" as the
285
305
  # single writer. Carries the overridden codes, including [] for a
286
306
  # preemptive ack on a clean message (itself a signal).
@@ -0,0 +1,57 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ - **Post-transfer closeout P19 breaker (SessionReaper):** the closeout's veto-retry
9
+ loop is now bounded — after `monitoring.sessionReaper.topicMovedVetoBreakerAttempts`
10
+ (default 5) consecutive vetoed close attempts on the same session, retries stop for
11
+ the episode, a `closeout-breaker-open` row lands in sentinel-events.jsonl, and ONE
12
+ deduped attention item surfaces. The session still closes via the normal idle path
13
+ when its work finishes. Episode resets on success / topic-home / pin-conflict hold.
14
+ - **Autonomous-run transfer consent gate (WS1.4):** `POST /pool/transfer` and the
15
+ natural-language "move this to <nickname>" now answer `needsConfirmation` when the
16
+ topic has a LIVE autonomous run on this machine — evaluated first among the consent
17
+ gates. A confirmed move (`"confirm":true`) suspends the run at its next turn
18
+ boundary via the new `suspendAutonomousTopicForMove`: `active: false` +
19
+ `moved_to`/`move_suspended_at` markers, ATOMIC rewrite (temp + fsync + rename), the
20
+ state file SURVIVES to ride the working-set carrier, and the journal `stopped` emit
21
+ re-fires the receiver's pull. Response reports `autonomousRunSuspended`.
22
+ - CLAUDE.md template + PostUpdateMigrator migration so new AND deployed agents learn
23
+ the consent gate (Agent Awareness + Migration Parity).
24
+
25
+ ## What to Tell Your User
26
+
27
+ Two multi-machine rough edges are gone. First: when a conversation moves between
28
+ your machines, the old machine's cleanup can no longer get stuck hammering a session
29
+ that's still finishing real work — it now backs off after a few refused attempts and
30
+ sends you one clear notice instead of retrying silently for hours. Second: moving a
31
+ topic that has an autonomous job running now asks you first ("there's a run in
32
+ flight, ~N minutes remaining — move anyway?"), and if you say yes, the job is paused
33
+ at a clean stopping point and its progress travels with the conversation to the new
34
+ machine — nothing is lost mid-thought.
35
+
36
+ ## Summary of New Capabilities
37
+
38
+ - The post-transfer closeout is bounded by a circuit breaker with one honest
39
+ operator notice per episode (tunable via
40
+ `monitoring.sessionReaper.topicMovedVetoBreakerAttempts`).
41
+ - Topic transfers are autonomous-run-aware: consent before interrupting a run, clean
42
+ turn-boundary suspend on confirmation, and the run's state rides the working-set
43
+ carrier with atomic-write + hash-verified transport.
44
+
45
+ ## Evidence
46
+
47
+ - `tests/unit/session-reaper-topic-moved.test.ts` +5 (14 total): the spec's required
48
+ sustained-failure test (permanently-vetoing session → exactly N attempts + ONE
49
+ escalation with stable dedupe id), episode reset on topic-home, success-clears-
50
+ counter, pin-conflict clears counter, absent-dep audit-only.
51
+ - `tests/unit/TransferByNickname.test.ts` +4 (17 total): veto prompt content,
52
+ precedence over offline/mid-reply, idempotency-noop stays ahead, backward compat.
53
+ - `tests/unit/AutonomousSessions.test.ts` +4 (17 total): file survives with markers,
54
+ idempotent re-suspend, no-run no-op, journal stopped emit with artifact path.
55
+ - PostUpdateMigrator suite 416 green; `tsc --noEmit` clean; build green.
56
+ - Independent second-pass review (reaper kill loop + transfer dispatch surface) —
57
+ verdict appended to `upgrades/side-effects/multi-machine-seamlessness-ws12-ws14.md`.
@@ -0,0 +1,63 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ - **New `TIME_CLAIM` advisory code in the outbound preflight** (`POST /messaging/preflight`):
9
+ when the sending topic has an ACTIVE time-boxed (autonomous) session, anchored
10
+ elapsed/remaining/percent claims in the candidate text ("~7h elapsed", "2h 40m left",
11
+ "8% through") are verified against the live session clock (`readSessionClocks`).
12
+ A claim contradicting the clock beyond tolerance (max(15 min, 20%) for durations,
13
+ 15 points for percent) returns a TIME_CLAIM advisory → the relay script's standard
14
+ NOT-SENT loop (fix and re-run, or `--ack-advisory`). Pure deterministic detector at
15
+ `src/core/time-claim.ts`; quoted claims (a correction citing the wrong number) and
16
+ unanchored durations never match.
17
+ - **The relay preflight now covers unstamped senders** (`telegram-reply.sh` template):
18
+ every non-script sender runs the preflight — an interactive session running an
19
+ autonomous run (exactly the founding mis-report path) defaults to kind `reply`,
20
+ where the server applies ONLY the clock check (jargon/path/link detectors still
21
+ never run for conversational sends; no active clock → no advisories). Fail-open
22
+ end-to-end, version-skew safe in both directions. Prior shipped template SHA added
23
+ to `TELEGRAM_REPLY_PRIOR_SHIPPED_SHAS` so deployed stock scripts auto-upgrade.
24
+ - **Ships dark behind the development-agent gate** at
25
+ `messaging.outboundAdvisory.timeClaim.enabled` (registered in `DEV_GATED_FEATURES`);
26
+ master advisory off-switch (`messaging.outboundAdvisory.enabled`) still wins.
27
+ - CLAUDE.md template + PostUpdateMigrator bullet-insert migration so new AND deployed
28
+ agents learn the TIME_CLAIM rule (Agent Awareness + Migration Parity).
29
+
30
+ ## What to Tell Your User
31
+
32
+ When I'm running a long autonomous job for you, my progress reports state how long
33
+ I've been running and how much time is left. Previously I could (and once did) guess
34
+ those numbers wrong — saying "7 hours in" when the run was 1.5 hours old. Now there's
35
+ a structural check: before a report reaches you, any time claim in it is compared
36
+ against the run's real clock, and a wrong number stops the message until I correct it.
37
+ You'll simply notice that the times in my reports are right.
38
+
39
+ ## Summary of New Capabilities
40
+
41
+ - Time claims in outbound messages are verified against the live session clock
42
+ before delivery (TIME_CLAIM advisory; dark by default, dev-agents live, fleet flip
43
+ via `messaging.outboundAdvisory.timeClaim.enabled: true`).
44
+ - The outbound advisory preflight now also covers interactive sessions running
45
+ autonomous jobs — the clock check only; conversational text gets no new friction.
46
+
47
+ ## Evidence
48
+
49
+ - `tests/unit/time-claim.test.ts` (15 new): extraction decision table (anchored,
50
+ composites, minutes-only, percent), both sides of every tolerance boundary, the
51
+ founding incident shape fires, quoted-claim and future-tense "in 2 hours" never
52
+ match, unbounded-run skip, lenient multi-clock.
53
+ - `tests/unit/outbound-advisory-routes.test.ts` +8: automated + reply kinds fire,
54
+ reply kind runs ONLY the clock check, accurate report passes, inactive run never
55
+ fires, dev-gate dark-on-fleet / explicit-enable / force-dark, audit row keyed
56
+ `interactive-session`.
57
+ - `tests/unit/telegram-reply-advisory-script.test.ts` +1/±2: unstamped sender NOT-SENT
58
+ loop, preflight kind degrades to `reply` under an injection-shaped kind env,
59
+ script-class senders still skip.
60
+ - `tests/e2e/outbound-advisory-alive.test.ts` +1: TIME_CLAIM alive on the production
61
+ init path (explicit-enable fires; dark by default on a non-dev config).
62
+ - `tests/unit/devGatedFeatures-wiring.test.ts` auto-covers the new registry entry
63
+ (live-on-dev / dark-on-fleet, both sides). `tsc --noEmit` clean.
@@ -0,0 +1,197 @@
1
+ # Side-Effects Review — WS1.2 closeout P19 breaker + WS1.4 autonomous-run transfer guard
2
+
3
+ **Spec:** docs/specs/MULTI-MACHINE-SEAMLESSNESS-SPEC.md §WS1.2 (closeout coupling + P19
4
+ foundation patch) + §WS1.4 (autonomous runs survive or veto topic moves)
5
+ **Build order position:** step 5's first slice. This change ships the two pieces that
6
+ are complete WITHOUT the drain mesh verb; the drain verb + barrier + terminal
7
+ semantics are the next item in this same workstream's build order (spec §WS1.2 lines
8
+ 141–172) and extend — never rework — what ships here.
9
+
10
+ What ships:
11
+
12
+ 1. **Closeout P19 breaker (SessionReaper):** the post-transfer closeout's veto-retry
13
+ loop was unbounded — the 2026-06-12 incident was this exact loop attacking a
14
+ working session every 2 minutes for hours. Now: after
15
+ `topicMovedVetoBreakerAttempts` (default 5) CONSECUTIVE vetoed terminate attempts
16
+ on the same session, retries STOP for the episode, one `closeout-breaker-open`
17
+ audit row lands in sentinel-events.jsonl, and ONE deduped attention item surfaces
18
+ ("topic moved to X but the old session won't close — held by Y"). The session is
19
+ NOT stranded: the idle pipeline still evaluates it every tick and reaps it when
20
+ its work actually finishes. Episode resets on success / topic-home /
21
+ pin-conflict hold / session end.
22
+ 2. **WS1.4 transfer-time veto (planTransferByNickname):** a LIVE local autonomous run
23
+ on the topic → `confirm-required` (`detail: 'autonomous-run-in-flight'`),
24
+ evaluated FIRST among the consent gates (spec precedence: the veto fires at
25
+ transfer-request time, before any move mechanics). Wired into BOTH callers:
26
+ POST /pool/transfer (confirm:true consents) and the NL "move this to <nick>" arm
27
+ (prompts conversationally; the confirmed move arrives via the API).
28
+ 3. **WS1.4 confirmed move → turn-boundary suspend (suspendAutonomousTopicForMove):**
29
+ distinct from stop in exactly one way — the state file SURVIVES to ride the
30
+ working-set carrier. `active: false` releases the stop hook at the run's next
31
+ turn boundary; `moved_to` + `move_suspended_at` markers are the honest
32
+ breadcrumb; the rewrite is ATOMIC (same-dir temp + fsync + rename) so the carrier
33
+ can never ship a half-rewritten file; the journal `stopped` emit is what re-fires
34
+ the receiving machine's working-set pull (WorkingSetManifest §3.4). Stated
35
+ outright: NOTHING auto-resumes the run on the receiving machine — the carried
36
+ file lands `active: false` with its markers, and resuming is a deliberate act
37
+ (the operator's or a future workstream's). The spec's receiving-side schema
38
+ gate applies when auto-resume exists; transport integrity (hash-verified
39
+ assembly, torn-chunk refusal, never-clobber) is already enforced by the
40
+ carrier today.
41
+
42
+ **Second-pass fixes folded in (2026-06-13, before merge):** the independent review
43
+ below caught two real defects in the WS1.4 half as first built — (1) a stacked-gate
44
+ consent collapse: `confirm:true` after the autonomous-run prompt would silently
45
+ consent to an offline target the user was never shown (the veto's higher precedence
46
+ suppressed the offline prompt). Fixed at the planner: ALL live consent conditions
47
+ now stack into ONE prompt with a '+'-joined `detail`, the route no longer suppresses
48
+ the gate on confirm (the full chain re-evaluates), and the 409 echoes `detail`.
49
+ (2) a silent false-success: the suspend's `active: true` flip regex was stricter
50
+ than the reader's quote-tolerant parse, so a quoted-active file would be reported
51
+ `suspended: true` while the run stayed live. Fixed: reader-aligned tolerant flip +
52
+ a post-flip verification gate that returns `suspended: false` (and emits nothing)
53
+ when the flip did not land; an inactive file with no move markers is an honest
54
+ no-op. Both fixes carry dedicated tests.
55
+
56
+ ## 1. Over-block
57
+ The veto requires confirmation for EVERY move off a topic with a live run — including
58
+ a move the operator urgently wants. Bounded: one extra round-trip (`confirm:true`),
59
+ and the prompt carries goal + remaining time so the consent is informed
60
+ (context-before-consent, P8). The breaker stops closeout retries while a session
61
+ still SHOULD close — bounded by the idle pipeline still owning the session (it reaps
62
+ on actual completion) and by the attention item handing the operator the manual
63
+ lever. The idempotency no-op stays AHEAD of the veto, so "already there" never asks.
64
+
65
+ ## 2. Under-block
66
+ The veto and the suspend read THIS machine's run registry (the state-file directory)
67
+ — a run living on a REMOTE owner (holder ≠ owner under the active-active pool) is
68
+ not seen, because the holder can neither see nor suspend a remote machine's run
69
+ without a drain signal; that is precisely the WS1.2 drain mesh verb's job (next item
70
+ in this workstream, spec §WS1.2 "drain authorization"), and today's production
71
+ topology (one-awake-machine; the run lives where the transfer executes) is fully
72
+ covered. The breaker counts only REAL vetoed terminate calls — budget-exhausted
73
+ ticks don't increment, so a busy reap hour cannot open the breaker spuriously.
74
+
75
+ ## 3. Level-of-abstraction fit
76
+ The breaker lives inside the loop it bounds (P19's "in-component" rule). The veto is
77
+ a planner consent gate exactly beside the two existing ones (offline / mid-reply) —
78
+ no parallel mechanism. The suspend is a sibling of stopAutonomousTopic in the run
79
+ registry's own module, not route-inline logic. The carrier integrity story reuses
80
+ the EXISTING working-set machinery (manifest sha256, served-bytes assembly hash,
81
+ fstat tear detection, never-clobber landing, liveSource re-fire on `stopped`) —
82
+ nothing re-implemented.
83
+
84
+ ## 4. Signal vs authority compliance
85
+ The breaker REMOVES authority (stops retrying a kill) on a deterministic counter and
86
+ escalates to the operator — the safe direction. The veto converts an implicit move
87
+ into explicit operator consent; it never blocks (confirm proceeds). The suspend acts
88
+ only on the operator's confirmed instruction, and the run's own stop hook remains
89
+ the enforcement authority (`active: false` is the hook's existing contract, not a
90
+ new kill path). No message content is inspected anywhere.
91
+
92
+ ## 5. Interactions
93
+ - Breaker × pin-conflict hold (WS1.3): the hold withdraws the closeout intent and
94
+ CLEARS the veto counter — a post-reconcile genuine move starts a fresh episode.
95
+ - Breaker × idle pipeline: a breaker-open session falls through to the idle
96
+ evaluation the same tick (verified: only a successful terminate `continue`s).
97
+ - Breaker × attention queue: keyed `closeout-breaker:<session>` — the store dedupes
98
+ on id (P17), which makes the dedupe per SESSION LIFETIME, not per episode: a
99
+ second breaker episode for the same still-running session re-raises the same id
100
+ and is swallowed even if the first item was resolved. The per-episode audit row
101
+ still lands; acceptable, named honestly (reviewer correction).
102
+ - Veto × confirm flow: a consented call cannot loop on its own prompt — the route
103
+ proceeds past `confirm-required` when `confirm:true` — and because the planner
104
+ stacks every live condition into the one prompt the caller saw, a confirm can
105
+ never consent to an unseen condition (second-pass fix).
106
+ - Suspend × journal scanner: emitStopped runs BEFORE the rewrite so the runId keys
107
+ on the live startedAt — op-key dedupe with the scanner's observed-stopped holds.
108
+ - Suspend × stop-all/emergency-stop: untouched — those still DELETE state (a stop is
109
+ not a move).
110
+ - Suspend × pin/release ordering: suspend runs BEFORE pin+release so the re-fired
111
+ carrier pull reads the final, consistent file.
112
+
113
+ ## 6. External surfaces
114
+ POST /pool/transfer gains a response field (`autonomousRunSuspended`) and a new
115
+ confirm prompt — additive. The NL arm gains one conversational prompt. CLAUDE.md
116
+ template + PostUpdateMigrator migration ship in this change (Agent Awareness +
117
+ Migration Parity): new installs and deployed agents both learn the consent gate.
118
+ No new mesh verbs, no protocol bump (that arrives with the drain verb).
119
+
120
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
121
+ The breaker is per-machine local state over local terminate attempts — no
122
+ replication, no cross-machine reads on the reaper tick (spec hot-path rule). The
123
+ veto/suspend read only the local run registry; the cross-machine arm rides the
124
+ already-replicated coherence journal (`stopped` emit) and the existing carrier.
125
+ Phase C: nothing here assumes 2 machines, a LAN, or interactivity — the breaker
126
+ scales per-session O(1); the veto's registry read is one directory listing
127
+ regardless of pool size; headless VMs run the identical path.
128
+
129
+ ## 8. Rollback cost
130
+ Breaker: `topicMovedVetoBreakerAttempts` is plain config; setting it very high
131
+ restores the old retry-forever behavior (and `topicMovedCloseout:false` disables the
132
+ rule wholesale, as before). Veto/suspend: the planner dep is optional — reverting
133
+ the two wiring sites restores pre-WS1.4 behavior byte-for-byte; a suspended state
134
+ file is manually resumable (`active: true`, delete the markers). No data migration,
135
+ no state repair. Revert-and-release covers all of it.
136
+
137
+ ## Second-pass review
138
+ REQUIRED (session lifecycle: reaper kill loop + transfer dispatch). Independent
139
+ reviewer response appended below.
140
+
141
+ <!-- second-pass reviewer response appended below by the independent reviewer -->
142
+
143
+ ### Independent second-pass review — round 1 (2026-06-12)
144
+
145
+ **Concern raised: confirm:true on the autonomous-run prompt silently consents to the
146
+ offline-target gate the user never saw, and `suspendAutonomousTopicForMove` can
147
+ report `suspended: true` while having suspended nothing.**
148
+
149
+ Summary of the round-1 verdict (full notes in the review record): the WS1.2 breaker
150
+ half was confirmed correct and well-tested without reservation (only-real-vetoes
151
+ counting, exactly-once open/escalation, no-strand fall-through, all four episode
152
+ resets, throw-proof attention wiring). The WS1.4 half had two real defects — (1) the
153
+ stacked-gate consent collapse: the autonomous veto's early return suppressed the
154
+ offline prompt, and the route's confirm:true suppression let a confirm consent to a
155
+ condition the caller was never shown; (2) a latent silent false-success: the
156
+ suspend's `active: true` flip regex was stricter than the quote-tolerant
157
+ `readField`, so a quoted-active file would be reported suspended while the run
158
+ stayed live. Plus two wording corrections: the breaker attention dedupe is per
159
+ session lifetime (not per episode), and the artifact should state outright that
160
+ nothing auto-resumes a carried run on the receiving machine. Both defects were
161
+ fixed and both corrections folded into the artifact before commit (see "Second-pass
162
+ fixes folded in" above).
163
+
164
+ ### Independent second-pass re-verification — round 2 (2026-06-12)
165
+
166
+ **Concur — both fixes verified.**
167
+
168
+ 1. **Consent collapse FIXED.** All three consent conditions evaluate unconditionally
169
+ into one `consents` array with no early return between them
170
+ (TransferByNickname.ts:112-131); `detail` is the '+'-joined list and the prompt
171
+ joins every condition's sentence (:135-136); the triple-stack is test-pinned
172
+ (TransferByNickname.test.ts:139-151). The route passes `autonomousRunActive`
173
+ unconditionally (routes.ts:10609-10627), the only 409 branch requires
174
+ `confirm !== true` (:10638) so confirm:true falls through to execute (no loop),
175
+ the 409 echoes `detail` (:10641), and the holder-proxy forwards `confirm`
176
+ (:10564). The NL arm relays the stacked prompt for free (server.ts:14373-14375)
177
+ and never executes a confirm itself. Residual (non-blocking): the generic
178
+ prompt→confirm TOCTOU (a condition going live between the 409 and the retry) —
179
+ the `detail` echo is the natural hook for an `expectedDetail` handshake if that
180
+ window ever matters.
181
+ 2. **Suspend false-success FIXED — structurally, not regex-patched.** The post-flip
182
+ verification gate re-reads with the same `readField` the veto uses and returns
183
+ `{suspended:false}` with no emit and no write when the flip did not land
184
+ (AutonomousSessions.ts:213-217). Adversarial counterexample search (quoted,
185
+ unspaced, trailing-whitespace, CRLF, malformed-quote forms): every shape
186
+ `readField` accepts as 'true' is either flipped or caught by the gate; the only
187
+ constructed escape (duplicate active keys) is one where all readers already
188
+ treat the run as inactive — reader and suspend agree, no system-visible false
189
+ success. Idempotent re-suspend refreshes markers without emitting (:206-228,
190
+ :232); honest no-op and missing-file paths verified byte-identical/false; the
191
+ atomic same-dir temp + fsync + rename path is unchanged (:234-242).
192
+ 3. **Suite green, artifact accurate.** 51/51 across the three test files;
193
+ `tsc --noEmit` clean. The artifact records both fixes, the no-auto-resume
194
+ property, and the per-session-lifetime dedupe wording. The dedicated tests added
195
+ for both fixes would each have failed against the originally-reviewed code.
196
+
197
+ No regressions found.
@@ -0,0 +1,177 @@
1
+ # Side-Effects Review — TIME_CLAIM outbound advisory (accurate time reporting)
2
+
3
+ **Version / slug:** `time-claim-advisory`
4
+ **Date:** `2026-06-12`
5
+ **Author:** `Instar Agent (echo)`
6
+ **Second-pass reviewer:** `independent reviewer subagent (verdict appended below)`
7
+
8
+ ## Summary of the change
9
+
10
+ Adds a `TIME_CLAIM` advisory code to the inform-only outbound preflight: when the
11
+ sending topic has an ACTIVE time-boxed (autonomous) session, anchored
12
+ elapsed/remaining/percent claims in the candidate text are verified against the
13
+ live session clock; a gross contradiction returns an advisory and the relay
14
+ script's existing NOT-SENT loop holds the message until the sender fixes the
15
+ number or acks. Files: `src/core/time-claim.ts` (new pure detector),
16
+ `src/messaging/OutboundAdvisory.ts` (code + guidance + `composeTimeClaimAdvisories`),
17
+ `src/server/routes.ts` (preflight route resolves active clocks; reply-kind sends get
18
+ the clock check ONLY), `src/templates/scripts/telegram-reply.sh` (preflight gate
19
+ widened from automated+llm-session to every non-script sender),
20
+ `src/core/devGatedFeatures.ts` (dev-gate registry), `src/core/PostUpdateMigrator.ts`
21
+ (prior-shipped template SHA + CLAUDE.md bullet migration), `src/scaffold/templates.ts`
22
+ (Agent Awareness). Operator mandate: Justin, 2026-06-12 17:55 PDT, topic 13481.
23
+
24
+ ## Decision-point inventory
25
+
26
+ - `POST /messaging/preflight` advisory composition — **modify** — adds the TIME_CLAIM
27
+ detector for automated kinds and makes it the ONLY detector for non-automated kinds.
28
+ Inform-only: the route returns advisories; it never blocks a send.
29
+ - `telegram-reply.sh` preflight gate — **modify** — widens which senders consult the
30
+ preflight (every non-script sender). The script's existing NOT-SENT/ack loop is
31
+ unchanged; sender keeps final authority.
32
+ - `recordPreflight` audit keying — **modify (pass-through shape)** — reply-kind rows
33
+ with no job slug are keyed `interactive-session` instead of empty string.
34
+
35
+ ---
36
+
37
+ ## 1. Over-block
38
+
39
+ **What legitimate inputs does this reject that it shouldn't?**
40
+
41
+ The advisory never rejects — worst case is a held message the sender must re-send or
42
+ ack. The realistic false-hold shapes, and their mitigations:
43
+
44
+ - A message quoting a WRONG time to correct it ("my '~7h elapsed' line was wrong"):
45
+ quoted-claim skip (chars `"'""''` + backtick immediately before the claim) — unit-tested.
46
+ - Honest rounding ("about 2h" at 1h54m): tolerance is max(15 min, 20%) — unit-tested
47
+ on both sides.
48
+ - Talk about OTHER durations ("the regression took 3h", "in 2 hours I'll check"):
49
+ extraction requires an explicit elapsed/remaining/percent anchor; future-tense
50
+ "in X hours" and "Xh in CI" shapes are excluded by the boundary lookahead — unit-tested.
51
+ - A topic with NO active timed run: detector is a structural no-op (no clocks → no
52
+ advisory) — conversational sends remain effectively friction-free.
53
+
54
+ Residual: a sender quoting a wrong time WITHOUT quote marks in a correction message
55
+ could be held once; the advisory text tells it the real numbers, and `--ack-advisory`
56
+ delivers unchanged. Accepted — this is the exact message class the mandate targets.
57
+
58
+ ## 2. Under-block
59
+
60
+ **What failure modes does this still miss?**
61
+
62
+ - Prose-only claims with no anchored number ("we're most of the way through") — out
63
+ of scope for a deterministic detector; accepted.
64
+ - Wrong ABSOLUTE times ("started at 4pm", "ends at 16:07 tomorrow") — not parsed in
65
+ v1; the elapsed/remaining/percent family covers the founding incident class.
66
+ Tracked for a follow-up detector revision if dogfooding shows absolute-time
67
+ guesses recur. <!-- tracked: 13481 -->
68
+ - Slack/WhatsApp/iMessage relay scripts have no preflight at all today (pre-existing);
69
+ the Telegram template is where autonomous reports go. Same tracking item.
70
+ <!-- tracked: 13481 -->
71
+ - Time claims in CONVERSATIONAL sessions on topics with no active run are unchecked
72
+ by design (no clock = no truth to compare against).
73
+
74
+ ## 3. Level-of-abstraction fit
75
+
76
+ Right layer. The deterministic detector lives beside the other text detectors
77
+ (`JargonDetector`, `raw-file-path`, `localhost-link`) in `src/core/`; the clock READ
78
+ stays in the route (the module remains pure); the hold-and-fix loop reuses the
79
+ existing advisory mechanism rather than inventing a parallel one. The smarter
80
+ outbound authority (MessagingToneGate) is not the right owner: this is an objective
81
+ numeric contradiction, not a tone/judgment call, and the mandate asks for a
82
+ deterministic guarantee.
83
+
84
+ ## 4. Signal vs authority compliance
85
+
86
+ Compliant (docs/signal-vs-authority.md). The detector is a brittle/cheap
87
+ signal-producer feeding the existing inform-only advisory surface. It holds NO
88
+ blocking authority: the server route only returns advisories; the script-side loop
89
+ gives the sender the fix-or-ack decision; every error path (clock read failure,
90
+ detector throw, server down, timeout, malformed JSON) fails OPEN to delivery.
91
+
92
+ ## 5. Interactions
93
+
94
+ - The localhost-link server guard, tone gate, dedup, and delivery pipeline are
95
+ untouched — the TIME_CLAIM advisory composes alongside the other codes for
96
+ automated sends and rides the same audit/escalation machinery (signature includes
97
+ the code set, so per-signature escalation just works).
98
+ - Double-fire: for automated sends the detector runs once inside
99
+ `composeAdvisories`; the reply path calls `composeTimeClaimAdvisories` directly —
100
+ one detector run per preflight, never two.
101
+ - The reply-kind preflight writes audit rows that previously only automated sends
102
+ wrote; rows are keyed `interactive-session`, so per-slug aggregate escalation for
103
+ real jobs is not polluted.
104
+ - Race with a run finishing between preflight and send: harmless — worst case a
105
+ passing claim was checked against a clock that then expired; the advisory is
106
+ point-in-time by design.
107
+
108
+ ## 6. External surfaces
109
+
110
+ - `POST /messaging/preflight` response may now include a `TIME_CLAIM` advisory; for
111
+ non-automated kinds the response can be non-empty for the first time. OLD scripts
112
+ never call the preflight for those kinds → no deployed-behavior change until the
113
+ script template migrates (SHA-gated, stock copies only; customized copies get a
114
+ `.new` candidate + degradation event — the established mechanism).
115
+ - NEW script + OLD server: server returns `[]` for non-automated kinds → no-op.
116
+ Version-skew safe both directions (tested at the route level for the kinds matrix).
117
+ - Timing dependence: yes, inherently — the detector compares against the live clock
118
+ at preflight time; tolerances absorb request latency (seconds vs a 15-minute floor).
119
+
120
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
121
+
122
+ **machine-local BY DESIGN.** The session clock reads `<stateDir>/autonomous/*.local.md`
123
+ — per-machine state for runs executing ON this machine — and the preflight runs on
124
+ the same machine as the sending session, so the clock consulted is the right one by
125
+ construction. A topic transferred mid-run carries its autonomous state file via the
126
+ working-set carrier (WS1.4, PR #1092); after a transfer the new machine's preflight
127
+ reads the carried file once the run resumes there. No replication needed; no URL or
128
+ notice surface is generated; one-voice gating is unaffected (this layer holds a
129
+ message BEFORE send, it never speaks).
130
+
131
+ ## 8. Rollback cost
132
+
133
+ Cheap, three independent levers, no data migration:
134
+ - Live-config: `messaging.outboundAdvisory.timeClaim.enabled: false` (or the master
135
+ `messaging.outboundAdvisory.enabled: false`) — read live, no restart.
136
+ - The feature ships DARK on the fleet (dev-agent gate); never enabling it is the
137
+ default state.
138
+ - Full revert: the detector is additive; reverting the PR restores the prior
139
+ template SHA path (the SHA set keeps old entries valid as migration sources).
140
+ State written: only advisory audit JSONL rows (observe-only).
141
+
142
+ ---
143
+
144
+ ## Second-pass review
145
+
146
+ **Round 1 (independent reviewer): CONCERN RAISED — two genuine defects; all safety
147
+ claims (fail-open at every layer, dev-gate resolution, version-skew no-op both
148
+ directions, ReDoS-bounded regexes ≤1.3ms on adversarial 64KB inputs, migration
149
+ idempotency, template-SHA correctness) verified clean.**
150
+
151
+ - **Concern 1 — percent anchors captured TASK progress.** `done`/`complete`/bare-`in`
152
+ ("the migration is 90% done", "passes 100% in CI") parsed as TIME claims; task
153
+ progress diverges from wall-clock percent in the NORMAL state, so honest reports
154
+ would be held routinely — and the guidance would have made them WRONG.
155
+ **Fixed:** percent anchors now require an explicit time noun
156
+ (`elapsed | (through|of|into) the (run|session|clock)`); 5 new negative tests on
157
+ the reviewer's exact counterexamples.
158
+ - **Concern 2 — a non-automated `--ack-advisory` never recorded `acked`.** The
159
+ script's ack annotation was gated to automated+llm-session, and the route's ack
160
+ writer used the raw (empty) jobSlug — so an interactive session's advised
161
+ episodes could never resolve, and the ignore-escalation would false-page on
162
+ messages that actually DELIVERED (compounded by every interactive session
163
+ sharing the `interactive-session` signature). **Fixed:** the script annotates
164
+ the ack for every non-script sender, and the route's ack writer applies the same
165
+ `interactive-session` slug fallback as the preflight writer; covered by a new
166
+ script test (unstamped ack carries advisoryAck + codes) and a new route test
167
+ (kindless ack lands `acked` under the SAME signature key).
168
+
169
+ **Round 2: CONCUR.** The reviewer executed the literal percent regex against all
170
+ round-1 counterexamples (zero matches) and the three time-noun shapes (all match);
171
+ traced the script's ack-first METADATA_JSON assembly for the all-env-empty case
172
+ (valid JSON, no leading-comma defect, confirmed empirically by the unstamped-ack
173
+ test JSON-parsing the delivered body); and verified the preflight writer and the
174
+ ack writer resolve to the IDENTICAL signature key for every kind, so a reply-kind
175
+ advised episode is genuinely resolved by its acked row (recordAck clears
176
+ `unresolved` + `escalatedIgnore` on the `slug|topic|codes` signature). 55/55 tests
177
+ green in the three touched files.