instar 1.3.962 → 1.3.963

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.
@@ -0,0 +1,135 @@
1
+ # Upgrade Guide — vNEXT
2
+
3
+ <!-- assembled-by: assemble-next-md -->
4
+ <!-- bump: patch -->
5
+
6
+ ## What Changed
7
+
8
+ A commitment can now carry a **check-in date**, and a background reconciler
9
+ posts one reminder into that commitment's own topic when the date arrives.
10
+
11
+ Before this, "I'll report back by Friday" lived in the session that said it. If
12
+ that session ended, restarted, or compacted, Friday arrived and nothing
13
+ happened. The PromiseBeacon nudges on *rhythm*, not on dates — it has no concept
14
+ of a specific promised moment.
15
+
16
+ **What ships (ACT-724 step 1 of 2):**
17
+
18
+ - `Commitment.checkInAt` — an absolute instant, deliberately separate from the
19
+ beacon's `nextUpdateDueAt` / `softDeadlineAt` so a one-time dated reminder and
20
+ a rolling nudge cadence are not side effects of each other.
21
+ - `CheckInReminderReconciler` — a scan over open commitments, driven by the new
22
+ `commitment-checkin-reminder` built-in job (every 5 minutes, `enabled: false`).
23
+ - `POST /commitments/check-in-reminder/pass` and `GET /commitments/check-in-reminder`.
24
+
25
+ **One recurring scan, not one alarm per commitment.** ACT-724 sketched a
26
+ per-commitment scheduler entry; taken literally that reproduces two of the three
27
+ defects the action itself lists — the two-file job dance (defect b) and
28
+ self-disable by file edit (defect c). A scan has neither: coverage is a property
29
+ of the scan, so there is no registration step that can be skipped, and teardown
30
+ is just the commitment reaching a terminal status.
31
+
32
+ **The ordering was wrong first, and the fix's own justification was unwired.**
33
+ The first design stamped `checkInReminderSentAt` *before* sending — which made
34
+ zero delivery a designed outcome, since a failed send left the commitment marked
35
+ delivered and permanently ineligible. Inverted to send-then-stamp with bounded
36
+ retry (5 attempts, then a loud `checkInReminderFailedAt`). The second review
37
+ round then caught that the duplicate mitigation justifying that inversion —
38
+ "the relay dedups" — was not connected: `sendToTopic` does not dedup, that lives
39
+ in the `/telegram/reply` route. The send is now explicitly routed through the
40
+ same durably-backed `OutboundContentDedup`.
41
+
42
+ **Honest guarantee: at-least-once, deduped at the delivery layer** — not
43
+ exactly-once. Both remaining windows are stated in the spec rather than designed
44
+ away.
45
+
46
+ A newly created scheduled job fired immediately on the next server restart —
47
+ including one scheduled months ahead.
48
+
49
+ At startup the scheduler sweeps for work that was missed while the machine was
50
+ off. To decide, it compares now against the job's last run. For a job that has
51
+ **never** run there is no last run, and the code resolved that case by treating
52
+ it as overdue and triggering it. That is right for a job added while the server
53
+ was down that slept through its window, and wrong for a job created minutes ago
54
+ whose first window is in December — "has never run" describes both identically.
55
+
56
+ The comment directly above that branch stated the correct rule —
57
+
58
+ > *"Jobs that have never run: trigger on startup if their first expected run
59
+ > time has already passed"*
60
+
61
+ — and the code never checked the condition. It couldn't: nothing recorded when a
62
+ job started existing, so there was no fact to reason from.
63
+
64
+ `JobState.firstSeenAt` is now stamped once at registration, and the sweep applies
65
+ the intended rule: a never-run job is missed only if it has existed longer than
66
+ one of its own intervals. A December reminder created today waits; a job that has
67
+ sat unrun through six four-hour windows still catches up.
68
+
69
+ Every unknown resolves to **not firing** — missing or unparseable `firstSeenAt`,
70
+ uncomputable interval. That asymmetry is deliberate: a skipped catch-up costs one
71
+ delayed run, while a future-dated job that fires early marks itself delivered and
72
+ never fires again, so nobody goes looking for it.
73
+
74
+ **Why now:** this is the prerequisite for dated commitments materializing real
75
+ scheduled reminders (ACT-724). A reminder built on a scheduler that fires
76
+ future-dated jobs at boot would discharge itself before its date — worse than
77
+ never having built it. The standard itself is not in this change.
78
+
79
+ ## What to Tell Your User
80
+
81
+ Nothing changes until it's switched on. When it is: on the day the agent said it
82
+ would check in on something, you get one short message in that conversation
83
+ saying the date has arrived.
84
+
85
+ It will not claim the work is done — a reminder that implies completion closes
86
+ the question instead of reopening it.
87
+
88
+ **Not yet true:** ACT-724 asks that a dated commitment without a reminder be
89
+ structurally impossible. This ships dark, and a disabled watcher guarantees
90
+ nothing. What is true today: if the reconciler is running, no dated commitment
91
+ can slip past it individually. The creation-time gate that closes the rest is
92
+ step 2, and it cannot itself ship dark.
93
+
94
+ If you've ever seen a scheduled job run for no obvious reason shortly after a
95
+ restart, this was probably it.
96
+
97
+ The practical effect: reminders and scheduled work now happen when they're
98
+ scheduled, not when the server next happens to reboot. A job that's been waiting
99
+ less than one of its own cycles will now wait for its proper turn rather than
100
+ firing at startup — that delay is the point.
101
+
102
+ ## Summary of New Capabilities
103
+
104
+ - Commitments carry a first-class check-in date that produces a real reminder.
105
+ - A reminder is never recorded as delivered unless it actually was; failures
106
+ retry and then give up visibly.
107
+ - Two read/drive endpoints and a built-in job, all dark by default.
108
+
109
+ - Scheduled jobs with a future first window are no longer discharged at server
110
+ startup.
111
+ - Job state records when a job was first registered, so "brand new" and "long
112
+ overdue" are distinguishable facts rather than a guess.
113
+ - The startup sweep fails toward not-running on every uncertainty.
114
+
115
+ ## Evidence
116
+
117
+ - Tier 1 (41): the full eligibility predicate on both sides of every clause;
118
+ idempotency across repeated passes; **a failed send never reads as sent** (the
119
+ regression test for the round-1 finding); bounded retry to a loud terminal
120
+ state; per-pass cap deferring rather than dropping; the reminder text never
121
+ asserting completion.
122
+ - Tier 2 (10): real Express routes + auth — delivery, HTTP-boundary idempotency,
123
+ dark 503, missing-transport 503, and `dryRun` defaulting ON when config omits it.
124
+ - Tier 3 (4): real `AgentServer` boot with a real injected `CommitmentTracker`.
125
+
126
+ - `tests/unit/job-scheduler-never-run-future-job.test.ts` (3 cases): a never-run
127
+ annual job and a freshly-registered daily job both stay at zero triggers; a job
128
+ registered 6h ago on a 5-minute cron still catches up. Written failing first —
129
+ the two "must not fire" cases failed against the old code, the catch-up case
130
+ already passed.
131
+ - `tests/unit/job-scheduler-edge.test.ts`: the existing test that asserted the
132
+ buggy behaviour was **updated, not deleted** — it now seeds `firstSeenAt` in the
133
+ past so it exercises the scenario it was written for (a job that genuinely
134
+ missed windows), plus a new sibling pinning the other side of the boundary.
135
+ - Full scheduler suite: 76 tests across 8 files, green.
@@ -0,0 +1,240 @@
1
+ # Side-effects review — dated-commitment-reminder
2
+
3
+ **Change:** commitments gain a first-class `checkInAt`; a recurring reconciler
4
+ posts exactly one fixed-template reminder to the commitment's topic when that
5
+ instant arrives; a built-in job drives it. ACT-724 (critical, pinned).
6
+
7
+ **Spec:** `docs/specs/dated-commitment-reminder.md` (2 review rounds; round 1
8
+ returned SERIOUS ISSUES and inverted the send/stamp ordering).
9
+
10
+ **Ships dark:** dev-agent gated + `dryRun` defaulting true + job manifest
11
+ `enabled: false`.
12
+
13
+ ---
14
+
15
+ ## 1. Over-block — what legitimate inputs does this reject that it shouldn't?
16
+
17
+ - **A dated commitment with no `topicId` is never reminded** (`no-topic`). A
18
+ reminder with nowhere to go is not a reminder; it is reported as a skip reason
19
+ rather than silently dropped, and appears in the `pending` list.
20
+ - **An unparseable `checkInAt` fails closed.** Deliberate: coercing it would make
21
+ the commitment look overdue since epoch and fire immediately — the exact
22
+ boot-fire shape fixed in `scheduler-never-run-window` days earlier.
23
+ - **A pass is capped at 25.** An overdue backlog drains across passes rather than
24
+ flooding a topic. The cap logs when it drops work, so a truncated pass is never
25
+ reported as a complete one.
26
+ - **Only `pending` counts as open.** A status added later is treated as not-open
27
+ (no reminder) until classified deliberately. Safe direction, but it does mean a
28
+ future status would silently stop reminders until someone adds it to the set.
29
+
30
+ ## 2. Under-block — what failure modes does this still miss?
31
+
32
+ - **"Structurally impossible" is not yet true.** ACT-724 asks that a dated
33
+ commitment without a reminder be impossible. Shipping dark means the watcher
34
+ can be off, so a dated commitment CAN exist with nothing watching. What IS
35
+ structural today: given the reconciler runs, no individual commitment can be
36
+ missed (coverage is a property of the scan, not of a per-commitment
37
+ registration). The creation-time gate that closes the rest is the declared
38
+ graduation step, and it cannot itself ship dark. Called out because the spec's
39
+ own framing over-claimed until review pushed back.
40
+ - **A duplicate is possible in one narrow window** — crash between send and
41
+ stamp. Accepted, and mitigated by the relay's existing content dedup rather
42
+ than by design novelty. Stated plainly instead of claimed away.
43
+ - **Clock jumps** shift a reminder in time (late on a backwards jump, early on a
44
+ forwards one) but cannot double-send or permanently suppress.
45
+ - **No escalation if a reminder is ignored.** Out of scope: this delivers the
46
+ date, it does not chase. The beacon owns chasing.
47
+
48
+ ## 3. Level-of-abstraction fit
49
+
50
+ The decision (`isCheckInReminderDue`) is pure and lives in
51
+ `checkInReminderCore.ts`; the effects (CAS, send, retry) live in the reconciler;
52
+ the cadence lives in a built-in job. That split is what let both sides of every
53
+ clause be tested without a store, a clock, or a transport.
54
+
55
+ **The alternative I rejected is the more standard one.** A transactional outbox
56
+ (`reminder_deliveries` with pending/sent/failed) is the textbook answer and is
57
+ genuinely better at volume. Rejected here because the commitment record already
58
+ IS durable single-writer state: a second store would add a consistency boundary
59
+ — a new class of divergence bug — to serve tens of commitments. The fields on the
60
+ commitment are a degenerate outbox with exactly one row per commitment. If volume
61
+ ever justifies it, the migration is mechanical.
62
+
63
+ ## 4. Signal vs authority compliance
64
+
65
+ `docs/signal-vs-authority.md`. Both decision points are `invariant`:
66
+
67
+ - **The due predicate** is arithmetic over durable state (open ∧ date arrived ∧
68
+ unstamped ∧ routed ∧ retries remain). No competing signals, nothing to weigh.
69
+ The failure being fixed was an ABSENT mechanism, not a bad judgment.
70
+ - **The reminder text** is a fixed template, deliberately not model-authored.
71
+ Generating it would add a judgment where none is wanted and a failure mode
72
+ (provider down) on a path whose entire purpose is to not fail.
73
+
74
+ **Deliberate gate bypass, argued:** the reminder rides the deterministic delivery
75
+ path, not the LLM tone gate. The tone gate fails closed, and a reminder that can
76
+ be held by a failing gate is the defect this feature exists to fix. This is not
77
+ weakening a safety control — the control is inapplicable, because the message
78
+ carries no agent prose for it to judge. Nothing model-generated reaches the user
79
+ through this path.
80
+
81
+ ## 5. Interactions
82
+
83
+ - **PromiseBeacon** — untouched. `checkInAt` is a NEW field precisely so beacon
84
+ cadence (`nextUpdateDueAt`, `softDeadlineAt`) is not overloaded; a one-time
85
+ dated reminder and a rolling nudge cadence stay independent.
86
+ - **`CommitmentTracker.mutate`** — the existing single-writer CAS is reused, not
87
+ replaced. Two passes racing cannot double-stamp.
88
+ - **The relay's content dedup** (`outboundContentDedup`) — now load-bearing for
89
+ this feature's duplicate story. Documented in both the spec and the reconciler
90
+ so a future change to that window is understood to affect this.
91
+ - **The scheduler** — this is why `scheduler-never-run-window` shipped first. A
92
+ reminder job on the old scheduler would have fired at boot.
93
+ - **`installBuiltinJobs`** — writes both `jobs/instar` and `jobs/schedule` for
94
+ built-ins, which is exactly why ACT-724's defect (b) (the two-file dance) does
95
+ not apply: that dance is only manual for USER jobs.
96
+
97
+ ## 6. External surfaces
98
+
99
+ - **New:** `POST /commitments/check-in-reminder/pass`, `GET
100
+ /commitments/check-in-reminder`. Both Bearer-authed, both 503 when dark.
101
+ - **New built-in job** `commitment-checkin-reminder`, `enabled: false`.
102
+ - **New commitment fields** — additive and optional; existing records load
103
+ unchanged, no migration.
104
+ - **User-visible:** one message per dated commitment, at its date, in its own
105
+ topic. Fixed template. The only user-facing text this change introduces.
106
+ - **A transport is required only to SEND.** A dry run needs none — surfaced by
107
+ the Tier-3 test, which would otherwise have reported the feature dead on an
108
+ agent with no messaging configured.
109
+
110
+ ## 7. Multi-machine posture (Cross-Machine Coherence)
111
+
112
+ **Unified, with a single-executor rule.**
113
+
114
+ - `checkInAt` / `checkInReminderSentAt` / `checkInReminderAttempts` /
115
+ `checkInReminderFailedAt` live on the commitment record and inherit the
116
+ commitment store's existing posture. No new state surface.
117
+ - **The pass must run on ONE machine.** The built-in job is per-machine (each
118
+ machine runs its own scheduler), so on a pool the pass must be lease-gated the
119
+ way the benchmark-divergence analysis pass is. Belt-and-braces: even if two
120
+ ran, the CAS stamp makes the second a no-op — the lease prevents wasted work,
121
+ the CAS prevents the duplicate.
122
+ - **HONEST GAP:** the lease gate is specified in §Multi-machine posture but the
123
+ job ships `enabled: false`, so it is not yet wired in this change. Enabling the
124
+ job on a multi-machine pool BEFORE adding the lease check would risk two
125
+ machines both passing — the CAS still prevents a duplicate reminder, but the
126
+ work would be duplicated. Recorded as a precondition of enabling, not as done.
127
+ - No notice routing, no generated URL, nothing that strands on topic transfer.
128
+
129
+ ## 8. Rollback cost
130
+
131
+ Disable `commitments.checkInReminder` or the job manifest. The reconciler is the
132
+ only reader of these fields; commitments keep them inertly. No migration, no
133
+ durable cleanup, no agent-state repair. Reverting the commit is equally safe.
134
+
135
+ ## Second-pass review
136
+
137
+ **Required** — this sends messages to users (outbound messaging is on the Phase-5
138
+ trigger list).
139
+
140
+ Two rounds of external cross-model review, the first of which returned **SERIOUS
141
+ ISSUES** and changed the design:
142
+
143
+ 1. **Stamp-before-send made zero delivery a designed outcome.** A failed send
144
+ left the commitment marked `checkInReminderSentAt` and permanently
145
+ ineligible. Inverted to send-then-stamp with bounded retry; the duplicate risk
146
+ is covered by the relay's existing dedup. A regression test asserts a failed
147
+ send never reads as sent.
148
+ 2. **"Structurally impossible" over-claimed** for a feature behind a disableable
149
+ flag. Split into what is true now versus at graduation.
150
+ 3. **Time semantics under-specified** — `checkInAt` is now defined as an absolute
151
+ instant with normalization at the creation boundary, and DST/clock-jump
152
+ behaviour is stated.
153
+ 4. **Cadence and scale omitted** — 5-minute cadence, worst-case lateness, the
154
+ in-memory store's actual size, per-pass cap and backlog drain are now stated.
155
+ 5. **Alternatives not compared** — outbox, durable queue, one-shot entries, and
156
+ per-commitment scheduler entries are now compared with reasons.
157
+
158
+ Self-review found one thing the reviewer did not: the Tier-3 test initially
159
+ asserted that `AgentServer` self-constructs the `CommitmentTracker`. It does
160
+ not — `server.ts` injects it — so the routes 503'd on the real boot path while
161
+ Tier 2 passed happily against a hand-assembled context. That is exactly the
162
+ wiring assumption Tier 3 exists to break, and the test now mirrors the
163
+ production entrypoint. It also surfaced the dry-run/transport coupling in §6.
164
+
165
+ Tests: 41 unit + 10 integration + 4 e2e = 55.
166
+
167
+
168
+ ## Class-Closure Declaration
169
+
170
+ **Class:** `unbounded-self-action` · **Closure:** `guard` · **Enforcement:** ratchet
171
+ **Citation:** `tests/unit/self-action-convergence.test.ts`
172
+
173
+ This change adds a self-triggered controller: a cadenced pass that SENDS
174
+ messages to users without a human in the loop. Under the "Capacity Safety — No
175
+ Unbounded Self-Action" standard that has to be proven to CONVERGE under
176
+ sustained pressure, not merely be individually correct.
177
+
178
+ Registered as `check-in-reminder-pass` in `src/testing/selfActionRegistry.ts`.
179
+
180
+ **Steady-state bound.** The emit is gated by a durable per-commitment attempt
181
+ counter (`CHECK_IN_MAX_ATTEMPTS = 5`), persisted through the CAS `mutate()`
182
+ BEFORE the send — so a crash-loop cannot buy fresh attempts, and the registry's
183
+ restart model (which reconstructs from durable state) settles at the same bound.
184
+
185
+ **Settling brake.** Two terminal states, both durable: a successful send writes
186
+ `checkInReminderSentAt`, making the commitment permanently ineligible; repeated
187
+ failure exhausts the counter and writes a loud `checkInReminderFailedAt`.
188
+
189
+ **Modeled at the worst case.** The registry model has every send FAIL, so the
190
+ success stamp never lands and only the attempt counter can stop it. Without that
191
+ counter the model emits once per tick forever and the ratchet fails — which is
192
+ the point: the bound is load-bearing, not decorative.
193
+
194
+ **Instantaneous mass** is separately bounded by the per-pass cap (25), which logs
195
+ when it defers rather than dropping silently.
196
+
197
+
198
+ ---
199
+
200
+ ## Addendum — three CI gates, and a coverage gap they led me to
201
+
202
+ **1. Write-domain classification.** `POST /commitments/check-in-reminder/pass`
203
+ mutates the commitment store and was undeclared. Classified `cluster-shared` in
204
+ `WriteDomainRegistry`, matching the requirement that the pass run on the
205
+ serving-lease holder only — the same single-writer shape as the feedback-factory
206
+ routes. This does NOT weaken the honest gap in section 7: the lease gate is still
207
+ not wired, and the classification records the intended posture rather than
208
+ asserting the wiring exists.
209
+
210
+ **2. Agent-id header.** The job's curl shipped Bearer-only; I dropped the
211
+ AGENT_ID line when adapting the template I copied from. Restored. Worth noting
212
+ as a pattern: adapting a template is where declared requirements silently fall
213
+ out.
214
+
215
+ **3. Docs coverage.** Job coverage fell 85% to 83% because the new built-in job
216
+ was undocumented. Added to the default-jobs reference. That floor caught a real
217
+ Agent-Awareness omission, not a bureaucratic one — an undocumented job is one no
218
+ operator can discover.
219
+
220
+ **And the thing worth more than all three: a coverage gap in the tone-gate suite.**
221
+
222
+ While dogfooding I hit a live advisory I could not override — four attempts,
223
+ byte-identical text, matching rule, sufficient reason, refused every time. That
224
+ is the RELEASING half of the advisory migration failing in production.
225
+
226
+ Investigating: every existing test in the advisory-migration integration file
227
+ pins ONE verdict for the whole run via a fixed mock, so the rule is constant by
228
+ construction and an ack always matches. The live gate RE-REVIEWS on every attempt
229
+ and can return a different rule — mine went B20_INTERNAL_ID_LEAK then
230
+ B11_STYLE_MISMATCH. That variability was not covered at all.
231
+
232
+ Two tests added: (a) re-review returns the SAME acked rule, ack honored, message
233
+ sent; (b) re-review returns a DIFFERENT rule, the stale ack is refused rather
234
+ than silently passing an objection nobody reviewed.
235
+
236
+ BOTH PASS, which exonerates the ack mechanism and kills my working hypothesis. I
237
+ therefore have a reproducible symptom and NO diagnosis, and I am shipping the
238
+ closed coverage gap rather than a guess dressed as a fix. The evidence and the
239
+ candidates I have not ruled out are in the run log; the four tone-gate tracking
240
+ actions stay OPEN because the releasing half is unproven in production.