row224: pace failed inline sync attempts (unrun)

This commit is contained in:
jason.woltje committed 2026-10-24 13:02:00 -05:00
1 parent 212aef6161
commit 82d6cc62c6
4 files changed
+50 -6

No files matched your search

+31 -2
View File
@@ -43,7 +43,7 @@ explicitly. After 253 and 216 land, rebase under a new branch name.
## Verification ledger ## Verification ledger
No test or syntax-check run yet. Acceptance requires full bus/harness suites No test or syntax-check run yet. Acceptance requires full bus/cli/harness suites
and every scripts/test-*.sh on Sage's gate; focused files and affected CLI/ and every scripts/test-*.sh on Sage's gate; focused files and affected CLI/
seat import-closure tests precede that only in an authorized CLEAR. seat import-closure tests precede that only in an authorized CLEAR.
@@ -70,7 +70,7 @@ Library sources remain in the existing whole-directory release copy.
Set TMPDIR=/mnt/storage/scratch/tmp. Focused new bus/harness files first, Set TMPDIR=/mnt/storage/scratch/tmp. Focused new bus/harness files first,
then existing library.test.mjs, library-sync.test.mjs, project-runtime.test.mjs, then existing library.test.mjs, library-sync.test.mjs, project-runtime.test.mjs,
harness creative/tools/spec-tools/bundle files and affected CLI launch tests. harness creative/tools/spec-tools/bundle files and affected CLI launch tests.
Follow with the required whole bus/harness suites and Sage's full shell gate. Follow with the required whole bus/cli/harness suites and Sage's full shell gate.
No timing claim yet: use the repository/Sage per-file limits and record every No timing claim yet: use the repository/Sage per-file limits and record every
command, outcome, elapsed time and timeout before acceptance. No focused skip command, outcome, elapsed time and timeout before acceptance. No focused skip
or pattern run substitutes for whole-file gate evidence. or pattern run substitutes for whole-file gate evidence.
@@ -139,3 +139,32 @@ For `docs/SESSIONS.md`:
Local-only draft pins 19c0a7adf/7cf39c390 are superseded by the Local-only draft pins 19c0a7adf/7cf39c390 are superseded by the
log-free candidate. Saved as local branch della/row224-draft-with-logs before log-free candidate. Saved as local branch della/row224-draft-with-logs before
rebuilding the unpublished candidate commit; no remote history changed. rebuilding the unpublished candidate commit; no remote history changed.
## Dewey dewey-0852 and Sage sage-2819 — focused corrections
B1: inline freshness uses max(lastFetch, lastError.at), not lastFetch alone.
An error within 60 seconds counts as a recent attempt, so later lookups serve
the old served pin without another fetch. Keep inline sync: the brief requires
it and businesses without timed libraries have no sync timer. The extended
failed-fetch test makes reads at +1, +59,999 and +60,000 ms (one attempt total),
then +60,001 ms (second attempt). All reads must return the old commit.
Required mutant M-B1: remove lastError.at from recentAttempt. The repeated
reads must fail the attempted-count assertion. This mutant has NOT been run.
Residual (Sage will defer): the first inline fetch in each outage window can
still exceed the tool timeout (60 seconds, or the runner broker client’s
30 seconds). Bounding that fetch is out of scope for 224. Subsequent reads
are paced by persisted lastError.at. No claim of first-read outage latency.
N1: whole bus, cli and harness suites are required, plus the shell gate.
If the new thread-launch integration test skips due to sandboxProblem(),
name the skip; a skip is not launcher integration proof.
N2: cli/tests/thread-launch.test.mjs now depends on library/tests/helpers.mjs
mappingEntry() and library/tests/fixtures/mapping.json. Changes to that fixture
must rerun the CLI launch file.
N3: harness/tests/library-tools.test.mjs compares the type enum to mapping.mjs
TYPES, so adding a mapped type cannot silently leave the typed tool behind.
These edits are local code/docs only. No Node, RED/GREEN run, mutant, claim,
HTTPS Git or push. New local pin goes to Dewey/Mos/Sage for focused review.
+5 -1
View File
@@ -98,7 +98,11 @@ export function createLibraryLookup({ dataRoot, sync, now = () => Date.now() })
const source = { ...context, dataRoot }; const source = { ...context, dataRoot };
let state = await libraryModule.libraryStatus(source); let state = await libraryModule.libraryStatus(source);
authorize(); authorize();
const age = state.lastFetch === null ? Infinity : now() - Date.parse(state.lastFetch); // sage-2819 / dewey-0852: failed attempts also pace inline retries. Otherwise an
// outage makes every lookup refetch while lastFetch remains old or null.
const recentAttempt = Math.max(state.lastFetch === null ? -Infinity : Date.parse(state.lastFetch),
state.lastError === null ? -Infinity : Date.parse(state.lastError.at));
const age = now() - recentAttempt;
if (age > libraryModule.SYNC_INTERVAL_MS || age < 0) { if (age > libraryModule.SYNC_INTERVAL_MS || age < 0) {
try { await sync(source); } try { await sync(source); }
catch (error) { catch (error) {
+12 -3
View File
@@ -159,16 +159,25 @@ test('freshness boundary, unknown last fetch and failed fetch still use immutabl
f.setState({ lastFetch: null }); f.setState({ lastFetch: null });
await f.call('library.universe', { universe: 'harbor' }); await f.call('library.universe', { universe: 'harbor' });
assert.equal(f.fetches(), 2); assert.equal(f.fetches(), 2);
let attempted = 0; let attempted = 0, clock = NOW;
const failing = createLibraryLookup({ dataRoot: f.dataRoot, now: () => NOW, sync: async () => { const failing = createLibraryLookup({ dataRoot: f.dataRoot, now: () => clock, sync: async () => {
attempted++; attempted++;
f.setState({ lastError: { code: 'library-fetch-failed', at: new Date(NOW).toISOString() } }); f.setState({ lastError: { code: 'library-fetch-failed', at: new Date(clock).toISOString() } });
return { code: 'library-fetch-failed' }; return { code: 'library-fetch-failed' };
} }); } });
f.setState({ lastFetch: null }); f.setState({ lastFetch: null });
const result = await failing(f.context, { method: 'get', args: GET, authorize() {} }); const result = await failing(f.context, { method: 'get', args: GET, authorize() {} });
assert.equal(attempted, 1); assert.equal(attempted, 1);
assert.equal(result.commit, f.commit); assert.equal(result.commit, f.commit);
for (const elapsed of [1, 59_999, 60_000]) {
clock = NOW + elapsed;
const retry = await failing(f.context, { method: 'get', args: GET, authorize() {} });
assert.equal(retry.commit, f.commit, 'serve the old pin during the failure cooldown');
assert.equal(attempted, 1, 'recent lastError prevents another sync even though lastFetch is null');
}
clock = NOW + 60_001;
assert.equal((await failing(f.context, { method: 'get', args: GET, authorize() {} })).commit, f.commit);
assert.equal(attempted, 2, 'retry only after the failed-attempt interval expires');
}); });
test('an unreadable sync state fails closed; a busy sync keeps serving the existing pin', async t => { test('an unreadable sync state fails closed; a busy sync keeps serving the existing pin', async t => {
@@ -5,6 +5,7 @@ import { mkdirSync, readFileSync } from 'node:fs';
import { join } from 'node:path'; import { join } from 'node:path';
import { typedTools } from '../src/tools.mjs'; import { typedTools } from '../src/tools.mjs';
import { buildBundle } from '../src/bundle.mjs'; import { buildBundle } from '../src/bundle.mjs';
import { TYPES } from '../../library/src/mapping.mjs';
import { HARNESS, REPO_SKILLS, resolvedFor, scratch } from './helpers.mjs'; import { HARNESS, REPO_SKILLS, resolvedFor, scratch } from './helpers.mjs';
const LOOKUPS = ['library_find', 'library_get', 'library_search', 'library_manuscript', 'library_universe']; const LOOKUPS = ['library_find', 'library_get', 'library_search', 'library_manuscript', 'library_universe'];
@@ -46,6 +47,7 @@ test('lookup schemas require a universe even for get, include paging, and cannot
} }
const byName = Object.fromEntries(tools.map(t => [t.name, t])); const byName = Object.fromEntries(tools.map(t => [t.name, t]));
assert.deepEqual(byName.library_find.parameters.properties.scope.enum, ['canon', 'playground']); assert.deepEqual(byName.library_find.parameters.properties.scope.enum, ['canon', 'playground']);
assert.deepEqual(byName.library_find.parameters.properties.type.enum, [...TYPES]);
assert.equal(byName.library_find.parameters.properties.offset.minimum, 0); assert.equal(byName.library_find.parameters.properties.offset.minimum, 0);
assert.equal(byName.library_get.parameters.properties.from.minimum, 1); assert.equal(byName.library_get.parameters.properties.from.minimum, 1);
assert.equal(byName.library_get.parameters.properties.links_from.minimum, 0); assert.equal(byName.library_get.parameters.properties.links_from.minimum, 0);