@flame0510/project-aether 1.6.0 → 1.6.2
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/app/api/update-check/route.ts +4 -1
- package/app/components/VersionBanner.tsx +80 -52
- package/docs/dev/API-REFERENCE.md +4 -0
- package/docs/dev/GATEWAY.md +23 -0
- package/models.config.json +937 -45
- package/package.json +4 -2
- package/scripts/model-info-verify.mjs +194 -0
|
@@ -59,7 +59,10 @@ export async function GET(request: Request): Promise<NextResponse> {
|
|
|
59
59
|
const now = Date.now();
|
|
60
60
|
if (cache && now - cache.checkedAt < CACHE_TTL_MS) {
|
|
61
61
|
latest = cache.latest;
|
|
62
|
-
|
|
62
|
+
// Recomputed against the version reading us now: `installed` is not cached, and
|
|
63
|
+
// reusing the cached flag made the banner offer an update that had just been
|
|
64
|
+
// installed, for up to the cache's fifteen minutes (seen right after an update).
|
|
65
|
+
updateAvailable = !!latest && latest !== installed;
|
|
63
66
|
} else {
|
|
64
67
|
try {
|
|
65
68
|
const pkgName = pkg.name || FALLBACK_PKG_NAME;
|
|
@@ -4,19 +4,22 @@
|
|
|
4
4
|
* "Update available" banner for Rev4a itself.
|
|
5
5
|
*
|
|
6
6
|
* The update is spawned by `POST /api/update-check` and restarts the server, so this
|
|
7
|
-
* component cannot be told the outcome by the response. It records what it asked for
|
|
8
|
-
*
|
|
7
|
+
* component cannot be told the outcome by the response. It records what it asked for and
|
|
8
|
+
* follows the restart from the browser.
|
|
9
9
|
*
|
|
10
|
-
*
|
|
10
|
+
* Three rules learned from the 1.6.0 update (2026-09-23):
|
|
11
11
|
* - a **success** is only a version that is *readable* and different. During the update
|
|
12
|
-
* npm rewrites `package.json`, and a read that
|
|
13
|
-
* as "the version moved" — the banner
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
12
|
+
* npm rewrites `package.json`, and a read that failed used to answer `0.0.0`, which read
|
|
13
|
+
* as "the version moved" — the banner said "Updated to v0.0.0" and reloaded mid-install.
|
|
14
|
+
* - while the restart is in flight the banner shows an **updating** state, never the
|
|
15
|
+
* "Update now" button again: nobody should think the update failed and start it twice.
|
|
16
|
+
* A page loaded in that window resumes the wait by itself and reloads when the new
|
|
17
|
+
* version answers.
|
|
18
|
+
* - the banner never reloads **blindly**: if the version has not moved by the deadline it
|
|
19
|
+
* says the update has not finished (it may still be running, or it failed), points at
|
|
20
|
+
* `update.log` and offers Retry and Reload.
|
|
18
21
|
*/
|
|
19
|
-
import { useEffect, useState } from 'react';
|
|
22
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
20
23
|
import { Button } from '../components/ui';
|
|
21
24
|
|
|
22
25
|
interface VersionInfo {
|
|
@@ -27,20 +30,23 @@ interface VersionInfo {
|
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
const DISMISS_KEY = 'rev4a-update-dismissed';
|
|
30
|
-
/** What we asked for and from which version, so
|
|
33
|
+
/** What we asked for and from which version, so a reload can resume the wait. */
|
|
31
34
|
const PENDING_KEY = 'rev4a-update-pending';
|
|
32
35
|
|
|
33
36
|
/** How long to wait for the restart before saying the update has not finished. */
|
|
34
|
-
const WAIT_MS =
|
|
37
|
+
const WAIT_MS = 150_000;
|
|
38
|
+
/** How long a recorded request stays believable across reloads. */
|
|
39
|
+
const PENDING_TTL_MS = 15 * 60 * 1000;
|
|
35
40
|
|
|
36
|
-
interface Pending { target: string; from: string }
|
|
41
|
+
interface Pending { target: string; from: string; at: number }
|
|
37
42
|
|
|
38
43
|
function readPending(): Pending | null {
|
|
39
44
|
try {
|
|
40
45
|
const raw = sessionStorage.getItem(PENDING_KEY);
|
|
41
46
|
if (!raw) return null;
|
|
42
47
|
const parsed = JSON.parse(raw) as Pending;
|
|
43
|
-
|
|
48
|
+
if (!parsed || typeof parsed.target !== 'string' || typeof parsed.from !== 'string') return null;
|
|
49
|
+
return parsed;
|
|
44
50
|
} catch {
|
|
45
51
|
return null;
|
|
46
52
|
}
|
|
@@ -57,6 +63,33 @@ export default function VersionBanner() {
|
|
|
57
63
|
const [updating, setUpdating] = useState(false);
|
|
58
64
|
const [updateError, setUpdateError] = useState<string | null>(null);
|
|
59
65
|
const [updateOk, setUpdateOk] = useState<string | null>(null);
|
|
66
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Follow the restart until the server answers with a different, readable version, then
|
|
70
|
+
* reload. `from` is the version we started from, so a resumed wait knows what to expect.
|
|
71
|
+
*/
|
|
72
|
+
const followRestart = useCallback((from: string) => {
|
|
73
|
+
const deadline = Date.now() + WAIT_MS;
|
|
74
|
+
const poll = async () => {
|
|
75
|
+
try {
|
|
76
|
+
const res = await fetch('/api/update-check', { cache: 'no-store' });
|
|
77
|
+
const d = (await res.json()) as VersionInfo;
|
|
78
|
+
if (usableVersion(d.installed) && d.installed !== from) {
|
|
79
|
+
sessionStorage.removeItem(PENDING_KEY);
|
|
80
|
+
window.location.reload();
|
|
81
|
+
return;
|
|
82
|
+
}
|
|
83
|
+
} catch { /* the server is down for the restart: expected */ }
|
|
84
|
+
if (Date.now() > deadline) {
|
|
85
|
+
setUpdating(false);
|
|
86
|
+
setUpdateError('The update has not finished after a couple of minutes. It may still be running — see update.log, then reload.');
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
timerRef.current = setTimeout(poll, 3000);
|
|
90
|
+
};
|
|
91
|
+
timerRef.current = setTimeout(poll, 3000);
|
|
92
|
+
}, []);
|
|
60
93
|
|
|
61
94
|
useEffect(() => {
|
|
62
95
|
let active = true;
|
|
@@ -72,13 +105,22 @@ export default function VersionBanner() {
|
|
|
72
105
|
setTimeout(() => { if (active) window.location.reload(); }, 2500);
|
|
73
106
|
return;
|
|
74
107
|
}
|
|
108
|
+
if (pending && Date.now() - (pending.at ?? 0) < PENDING_TTL_MS) {
|
|
109
|
+
// The restart is probably in flight (a page loaded inside that window, or a
|
|
110
|
+
// reload before the new version answered): resume the wait, show no action.
|
|
111
|
+
setInfo(d);
|
|
112
|
+
setDismissed(false);
|
|
113
|
+
setUpdating(true);
|
|
114
|
+
followRestart(pending.from);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
75
117
|
if (pending) {
|
|
76
|
-
|
|
118
|
+
sessionStorage.removeItem(PENDING_KEY);
|
|
77
119
|
setInfo(d);
|
|
78
120
|
setDismissed(false);
|
|
79
121
|
setUpdateError(
|
|
80
122
|
`The update to v${pending.target} has not finished: ${usableVersion(d.installed) ? `Rev4a is still on v${d.installed}` : 'Rev4a is not answering with a version'}.`
|
|
81
|
-
+ `
|
|
123
|
+
+ ` See ${d.log ?? 'update.log'} in the Rev4a data directory, then reload.`,
|
|
82
124
|
);
|
|
83
125
|
return;
|
|
84
126
|
}
|
|
@@ -89,7 +131,10 @@ export default function VersionBanner() {
|
|
|
89
131
|
})
|
|
90
132
|
.catch(() => {});
|
|
91
133
|
return () => { active = false; };
|
|
92
|
-
}, []);
|
|
134
|
+
}, [followRestart]);
|
|
135
|
+
|
|
136
|
+
// Clear a timer the component no longer owns (unmount or a full reload).
|
|
137
|
+
useEffect(() => () => { if (timerRef.current) clearTimeout(timerRef.current); }, []);
|
|
93
138
|
|
|
94
139
|
function handleDismiss() {
|
|
95
140
|
if (info?.latest) localStorage.setItem(DISMISS_KEY, info.latest);
|
|
@@ -113,37 +158,16 @@ export default function VersionBanner() {
|
|
|
113
158
|
setUpdating(false);
|
|
114
159
|
return;
|
|
115
160
|
}
|
|
116
|
-
sessionStorage.setItem(PENDING_KEY, JSON.stringify({ target: info.latest ?? '', from: info.installed }));
|
|
161
|
+
sessionStorage.setItem(PENDING_KEY, JSON.stringify({ target: info.latest ?? '', from: info.installed, at: Date.now() }));
|
|
117
162
|
localStorage.removeItem(DISMISS_KEY);
|
|
118
|
-
|
|
119
|
-
// The server goes down and comes back on the new version: reload once it answers
|
|
120
|
-
// with a different, readable version. If it never does, say so rather than reloading
|
|
121
|
-
// into a state the operator cannot interpret.
|
|
122
|
-
const deadline = Date.now() + WAIT_MS;
|
|
123
|
-
const poll = async () => {
|
|
124
|
-
try {
|
|
125
|
-
const r = await fetch('/api/update-check', { cache: 'no-store' });
|
|
126
|
-
const d = (await r.json()) as VersionInfo;
|
|
127
|
-
if (usableVersion(d.installed) && d.installed !== info.installed) { window.location.reload(); return; }
|
|
128
|
-
} catch { /* the server is down for the update: expected */ }
|
|
129
|
-
if (Date.now() > deadline) {
|
|
130
|
-
setUpdating(false);
|
|
131
|
-
setUpdateOk(null);
|
|
132
|
-
setUpdateError('The update has not finished after two minutes. It may still be running — reload in a moment, or check update.log.');
|
|
133
|
-
return;
|
|
134
|
-
}
|
|
135
|
-
setTimeout(poll, 3000);
|
|
136
|
-
};
|
|
137
|
-
setTimeout(poll, 3000);
|
|
163
|
+
followRestart(info.installed);
|
|
138
164
|
} catch (err) {
|
|
139
165
|
setUpdateError(err instanceof Error ? err.message : 'Network error');
|
|
140
166
|
setUpdating(false);
|
|
141
167
|
}
|
|
142
168
|
}
|
|
143
169
|
|
|
144
|
-
|
|
145
|
-
// succeeded); the "available" state needs the version info and no dismissal.
|
|
146
|
-
const show = Boolean(updateOk) || Boolean(updateError) || Boolean(info?.updateAvailable && !dismissed);
|
|
170
|
+
const show = Boolean(updateOk) || Boolean(updateError) || Boolean(updating) || Boolean(info?.updateAvailable && !dismissed);
|
|
147
171
|
if (!show) return null;
|
|
148
172
|
|
|
149
173
|
return (
|
|
@@ -154,6 +178,8 @@ export default function VersionBanner() {
|
|
|
154
178
|
<span className="update-banner__ok">{updateOk}</span>
|
|
155
179
|
) : updateError ? (
|
|
156
180
|
<span className="update-banner__err">{updateError}</span>
|
|
181
|
+
) : updating ? (
|
|
182
|
+
<span className="update-banner__ok">Update started. Rev4a is restarting — this page reloads on its own when the new version answers.</span>
|
|
157
183
|
) : (
|
|
158
184
|
<>
|
|
159
185
|
<span className="update-banner__label">Update available</span>
|
|
@@ -166,19 +192,21 @@ export default function VersionBanner() {
|
|
|
166
192
|
|
|
167
193
|
{!updateOk && (
|
|
168
194
|
<div className="update-banner__actions">
|
|
169
|
-
{
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
195
|
+
{updating ? (
|
|
196
|
+
// No action while the restart runs: the only honest one is to wait.
|
|
197
|
+
<Button variant="ghost" size="sm" onClick={() => window.location.reload()}>Reload</Button>
|
|
198
|
+
) : updateError ? (
|
|
199
|
+
<>
|
|
200
|
+
<Button variant="primary" size="sm" onClick={handleUpdate}>Retry</Button>
|
|
201
|
+
<Button variant="secondary" size="sm" onClick={() => window.location.reload()}>Reload</Button>
|
|
202
|
+
<Button variant="ghost" size="sm" onClick={handleDismiss}>Dismiss</Button>
|
|
203
|
+
</>
|
|
173
204
|
) : (
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
{updateError && (
|
|
179
|
-
<Button variant="secondary" size="sm" onClick={() => window.location.reload()}>Reload</Button>
|
|
205
|
+
<>
|
|
206
|
+
<Button variant="primary" size="sm" onClick={handleUpdate}>Update now</Button>
|
|
207
|
+
<Button variant="ghost" size="sm" onClick={handleDismiss}>Dismiss</Button>
|
|
208
|
+
</>
|
|
180
209
|
)}
|
|
181
|
-
<Button variant="ghost" size="sm" onClick={handleDismiss}>Dismiss</Button>
|
|
182
210
|
</div>
|
|
183
211
|
)}
|
|
184
212
|
</div>
|
|
@@ -2596,6 +2596,10 @@ from local state only.
|
|
|
2596
2596
|
|
|
2597
2597
|
**Response:** `{ "installed": "1.4.2", "latest": "1.5.0", "updateAvailable": true, "package": "@flame0510/project-aether", "log": "update.log" }`
|
|
2598
2598
|
|
|
2599
|
+
`updateAvailable` is recomputed against the version reading the request, even when the
|
|
2600
|
+
registry answer comes from the fifteen-minute cache: reusing the cached flag made the
|
|
2601
|
+
banner offer an update that had just been installed, right after an update.
|
|
2602
|
+
|
|
2599
2603
|
`installed` is `null` when `package.json` cannot be read — during an update npm rewrites
|
|
2600
2604
|
it, and a transient failure must not look like a version. With `installed` unknown there
|
|
2601
2605
|
is nothing to compare, so `latest` is `null` and `updateAvailable` false. `installed` is
|
package/docs/dev/GATEWAY.md
CHANGED
|
@@ -206,6 +206,29 @@ block, and every number there carries its source:
|
|
|
206
206
|
No generated field ever replaces a curated one silently: prices are separate files, and the
|
|
207
207
|
modal shows the generated part's date (`asOf`).
|
|
208
208
|
|
|
209
|
+
### Curating an `info` block
|
|
210
|
+
|
|
211
|
+
Nothing here is estimated, and every number carries its source and date. The workflow has
|
|
212
|
+
three steps, one command each:
|
|
213
|
+
|
|
214
|
+
```bash
|
|
215
|
+
npm run info:suggest -- <model id> … # candidate facts from the model card (read-only)
|
|
216
|
+
# the maintainer, or a worker agent asked to follow the same contract, proposes the block
|
|
217
|
+
npm run info:verify -- proposals.json # the gate: refuses what the card does not support
|
|
218
|
+
```
|
|
219
|
+
|
|
220
|
+
- `scripts/model-info-suggest.mjs` prints the card's size sentences, its benchmark tables
|
|
221
|
+
(headers included) and its doc links, so the reading is a lookup rather than a hunt.
|
|
222
|
+
- `scripts/model-info-verify.mjs` checks a proposal against the card: `evidence` present,
|
|
223
|
+
every number in `params` in the card text, every benchmark value in a row named after it
|
|
224
|
+
**under the column that names this model** (never a sibling variant), and `docUrl` when
|
|
225
|
+
asked with `--check-links`. It exits non-zero on a rejected proposal.
|
|
226
|
+
- The proposal contract is what makes this safe: one JSON object per model with an
|
|
227
|
+
`evidence` entry per field, the verbatim line, the column used and the card's own date.
|
|
228
|
+
A worker that skips the evidence — or takes a value from a variant's column — is caught
|
|
229
|
+
here, not in the catalogue. Benchmarks that a card states only for a sibling
|
|
230
|
+
(`-Max`, `-Base`, `-HighSpeed`) are dropped.
|
|
231
|
+
|
|
209
232
|
---
|
|
210
233
|
|
|
211
234
|
## Per-agent model assignment
|