instar 1.3.960 → 1.3.962
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/core/CapabilityRegistry.d.ts +75 -0
- package/dist/core/CapabilityRegistry.d.ts.map +1 -1
- package/dist/core/CapabilityRegistry.js +254 -0
- package/dist/core/CapabilityRegistry.js.map +1 -1
- package/dist/core/types.d.ts +15 -0
- package/dist/core/types.d.ts.map +1 -1
- package/dist/core/types.js.map +1 -1
- package/dist/scheduler/JobScheduler.d.ts.map +1 -1
- package/dist/scheduler/JobScheduler.js +48 -9
- package/dist/scheduler/JobScheduler.js.map +1 -1
- package/package.json +1 -1
- package/src/data/builtin-manifest.json +3 -3
- package/upgrades/1.3.961.md +70 -0
- package/upgrades/1.3.962.md +16 -0
- package/upgrades/side-effects/capability-registry-followup.md +1 -0
- package/upgrades/side-effects/capability-registry-increment-1.md +8 -0
- package/upgrades/side-effects/scheduler-never-run-window.md +156 -0
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
# Side-effects review — scheduler-never-run-window
|
|
2
|
+
|
|
3
|
+
**Change:** the startup missed-job sweep no longer treats every job with no
|
|
4
|
+
`lastRun` as overdue. A new `JobState.firstSeenAt`, stamped once at
|
|
5
|
+
registration, lets it apply the rule its own comment already described: a
|
|
6
|
+
never-run job is missed only if it has existed longer than one of its own
|
|
7
|
+
intervals.
|
|
8
|
+
|
|
9
|
+
**Motivation:** ACT-724 defect (a) — a hand-built future-dated reminder job
|
|
10
|
+
discharged itself on the next boot. This is the prerequisite for the ACT-724
|
|
11
|
+
standard proper (dated commitments materializing one-shot reminders); that
|
|
12
|
+
standard is NOT in this change.
|
|
13
|
+
|
|
14
|
+
---
|
|
15
|
+
|
|
16
|
+
## 1. Over-block — what legitimate inputs does this reject that it shouldn't?
|
|
17
|
+
|
|
18
|
+
**One real, deliberate narrowing.** A never-run job that has existed for *less*
|
|
19
|
+
than one interval is no longer triggered at startup. It waits for its next cron
|
|
20
|
+
window instead.
|
|
21
|
+
|
|
22
|
+
That is the intended behaviour — it is the difference between "scheduled" and
|
|
23
|
+
"whenever we happen to reboot" — but it is a genuine behaviour change and the
|
|
24
|
+
cost is honest: on a machine that restarts frequently, a newly-added job may now
|
|
25
|
+
first run at its scheduled time rather than within seconds of being added.
|
|
26
|
+
|
|
27
|
+
**Legacy state is also narrowed.** A job whose state predates `firstSeenAt` and
|
|
28
|
+
has no `lastRun` is treated as not-missed rather than fired. This affects the
|
|
29
|
+
first boot after upgrade only: registration stamps `firstSeenAt` for every job
|
|
30
|
+
that lacks it, so from the second boot the rule applies normally. Chosen
|
|
31
|
+
deliberately — see §4.
|
|
32
|
+
|
|
33
|
+
## 2. Under-block — what failure modes does this still miss?
|
|
34
|
+
|
|
35
|
+
- **A job added while the server was down still can't be dated precisely.**
|
|
36
|
+
`firstSeenAt` is stamped when the scheduler first *sees* the job, not when its
|
|
37
|
+
file was written. A job created during a three-day outage is stamped at boot
|
|
38
|
+
and so waits up to one interval before catching up. Using file mtime would be
|
|
39
|
+
more precise and less trustworthy (mtime changes on unrelated edits, syncs,
|
|
40
|
+
and checkouts). Named rather than silently accepted.
|
|
41
|
+
- **Interval is inferred from the next two cron occurrences.** For irregular
|
|
42
|
+
crons (`0 3 1 12 *`, `0 9 * * 1-5`) the gap between the next two runs is not a
|
|
43
|
+
constant period. It is a sound *lower-ish bound* for the comparison here, but
|
|
44
|
+
it is an approximation, and it is the same approximation the pre-existing
|
|
45
|
+
`lastRun` branch already relies on. Not made worse; not fixed either.
|
|
46
|
+
- **Nothing yet asserts one-shot semantics.** ACT-724 asks for first-class
|
|
47
|
+
one-shot jobs. This change makes cron-scheduled future jobs safe at boot; a
|
|
48
|
+
true one-shot type (fire once, then retire) is still absent, and the reminder
|
|
49
|
+
feature will need it.
|
|
50
|
+
|
|
51
|
+
## 3. Level-of-abstraction fit
|
|
52
|
+
|
|
53
|
+
Right layer. The missing thing was a *fact* ("when did this job start
|
|
54
|
+
existing?"), so the fix adds the fact at the point that knows it — registration
|
|
55
|
+
— and the decision stays where it was, in the sweep.
|
|
56
|
+
|
|
57
|
+
The alternative considered and rejected: seeding `lastRun` at registration.
|
|
58
|
+
That would have suppressed the symptom with less code, but it lies — it records
|
|
59
|
+
a run that never happened, corrupting run history, `runCount` semantics, and any
|
|
60
|
+
future "when did this last actually execute?" question. A separate field keeps
|
|
61
|
+
"registered" and "ran" as the distinct facts they are. ACT-724's own sketch
|
|
62
|
+
floated the `lastRun`-seeding option; this is a deliberate departure from it.
|
|
63
|
+
|
|
64
|
+
## 4. Signal vs authority compliance
|
|
65
|
+
|
|
66
|
+
`docs/signal-vs-authority.md`. The sweep holds real authority — it *triggers
|
|
67
|
+
jobs* — and it is deterministic, which is appropriate: "has this job existed
|
|
68
|
+
longer than its interval?" is arithmetic over two timestamps, not a judgment.
|
|
69
|
+
There are no competing signals to weigh.
|
|
70
|
+
|
|
71
|
+
**The direction of failure is the load-bearing property.** Every unknown case
|
|
72
|
+
now resolves to *not firing*: missing `firstSeenAt`, unparseable `firstSeenAt`,
|
|
73
|
+
uncomputable interval. That is the safe direction, and asymmetrically so — a
|
|
74
|
+
skipped catch-up costs one delayed run, while a wrongly-fired future job marks
|
|
75
|
+
itself delivered and will never fire again, so nobody goes looking for it. A
|
|
76
|
+
missed reminder that looks delivered is worse than one that is merely late.
|
|
77
|
+
|
|
78
|
+
The registration write is wrapped: a bookkeeping failure logs and continues
|
|
79
|
+
rather than preventing the scheduler from starting, and the absent field then
|
|
80
|
+
degrades to not-firing.
|
|
81
|
+
|
|
82
|
+
## 5. Interactions
|
|
83
|
+
|
|
84
|
+
- **The `lastRun` branch** — untouched. Jobs with run history take the identical
|
|
85
|
+
path (`timeSinceLastRun > intervalMs * 1.5`).
|
|
86
|
+
- **Priority sorting and trigger limits** — untouched; a never-run job that does
|
|
87
|
+
qualify still enters with `overdueRatio: 1.5` exactly as before.
|
|
88
|
+
- **`tests/unit/JobScheduler.test.ts`** — its `beforeEach` pre-seeds `lastRun`
|
|
89
|
+
"so checkMissedJobs doesn't trigger jobs at startup". That workaround is now
|
|
90
|
+
unnecessary but remains harmless and was left alone; removing it is unrelated
|
|
91
|
+
churn in a file this change does not otherwise touch.
|
|
92
|
+
- **`tests/unit/job-scheduler-edge.test.ts`** — one existing test asserted the
|
|
93
|
+
buggy behaviour ("triggers jobs that have never run on startup"). It was
|
|
94
|
+
written for a real concern (never-run jobs being skipped forever) and had
|
|
95
|
+
over-corrected. **Updated, not deleted:** it now seeds `firstSeenAt` a day in
|
|
96
|
+
the past so it exercises the scenario it was actually written for, and a
|
|
97
|
+
sibling test pins the other side of the boundary. Called out explicitly
|
|
98
|
+
because editing a test to make a change pass is exactly the move that needs
|
|
99
|
+
scrutiny.
|
|
100
|
+
- **StateManager** — `firstSeenAt` is additive and optional; older state loads
|
|
101
|
+
unchanged, and no migration is required.
|
|
102
|
+
|
|
103
|
+
## 6. External surfaces
|
|
104
|
+
|
|
105
|
+
- No route, config key, flag, env var, CLI surface, message, or notification.
|
|
106
|
+
- `JobState` gains one optional field, persisted in existing job-state files.
|
|
107
|
+
Additive; nothing reads it but the sweep.
|
|
108
|
+
- **User-visible behaviour:** the disappearance of unexplained job runs after a
|
|
109
|
+
restart. No new output.
|
|
110
|
+
- No timing dependence beyond what the sweep already had.
|
|
111
|
+
|
|
112
|
+
## 7. Multi-machine posture (Cross-Machine Coherence)
|
|
113
|
+
|
|
114
|
+
**Machine-local BY DESIGN** — `machine-local-justification: hardware-bound-resource`.
|
|
115
|
+
|
|
116
|
+
Job run state is per-machine because job *execution* is per-machine: each
|
|
117
|
+
machine runs its own scheduler over its own scoped jobs, and "when did this
|
|
118
|
+
machine first register this job?" is inherently a fact about that machine's
|
|
119
|
+
scheduler, not a fleet fact. Replicating it would be actively wrong — machine B
|
|
120
|
+
adopting machine A's `firstSeenAt` would make a job that B has never registered
|
|
121
|
+
appear to have existed for days, re-creating the immediate-fire bug through the
|
|
122
|
+
mesh.
|
|
123
|
+
|
|
124
|
+
This is inherited, not introduced: `JobState` (`lastRun`, `runCount`,
|
|
125
|
+
`consecutiveFailures`) is already machine-local for the same reason, and this
|
|
126
|
+
field joins it. No new cross-machine surface, no notice, no durable shared
|
|
127
|
+
state.
|
|
128
|
+
|
|
129
|
+
## 8. Rollback cost
|
|
130
|
+
|
|
131
|
+
**Revert the commit.** Reverting restores the fire-everything behaviour; the
|
|
132
|
+
orphaned `firstSeenAt` values in job-state files are ignored by the old code and
|
|
133
|
+
harmless. No migration, no data repair, no flag.
|
|
134
|
+
|
|
135
|
+
## Second-pass review
|
|
136
|
+
|
|
137
|
+
**Required** — this touches session/job lifecycle (it decides whether a job is
|
|
138
|
+
triggered), which is on the Phase-5 trigger list.
|
|
139
|
+
|
|
140
|
+
Self-review, recorded rather than skipped, with the two things I went looking
|
|
141
|
+
for:
|
|
142
|
+
|
|
143
|
+
1. **Did I make a test pass by weakening it?** One existing test was changed.
|
|
144
|
+
It asserted "any never-run job fires at startup" — the exact behaviour being
|
|
145
|
+
fixed — so it could not survive intact. I preserved its *intent* (a never-run
|
|
146
|
+
job must not be skipped forever) by giving it a past `firstSeenAt`, and added
|
|
147
|
+
the missing opposite-side assertion. The suite ends with strictly more
|
|
148
|
+
coverage of this boundary than it had: 2 tests where there was 1, plus 3 in a
|
|
149
|
+
dedicated file.
|
|
150
|
+
2. **Is the new failure direction actually safe?** Every uncertain path leads to
|
|
151
|
+
"don't fire". The worst case is a delayed run; the previous worst case was a
|
|
152
|
+
reminder silently discharging itself early. Verified by test rather than
|
|
153
|
+
asserted: the annual-cron case and the fresh-daily case both stay at zero
|
|
154
|
+
triggers, and the genuinely-overdue case still fires.
|
|
155
|
+
|
|
156
|
+
Full scheduler suite re-run: 76 tests across 8 files, all green.
|