From 23816cf285c952ddc47393130f8396a1993d7d99 Mon Sep 17 00:00:00 2001 From: Jason Woltje Date: Thu, 22 Oct 2026 11:50:19 -0500 Subject: [PATCH] row224: pace failed inline sync attempts (unrun) --- agents/della/work/queue-224/BUILD.md | 33 +++++++++++++++++-- packages/bus/src/runtime.mjs | 6 +++- packages/bus/tests/library-lookups.test.mjs | 15 +++++++-- packages/harness/tests/library-tools.test.mjs | 2 ++ 4 files changed, 50 insertions(+), 6 deletions(-) diff --git a/agents/della/work/queue-224/BUILD.md b/agents/della/work/queue-224/BUILD.md index 8c7faf6f3..4e3368350 100644 --- a/agents/della/work/queue-224/BUILD.md +++ b/agents/della/work/queue-224/BUILD.md @@ -43,7 +43,7 @@ explicitly. After 253 and 216 land, rebase under a new branch name. ## 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/ 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, 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. -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 command, outcome, elapsed time and timeout before acceptance. No focused skip 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 log-free candidate. Saved as local branch della/row224-draft-with-logs before 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. diff --git a/packages/bus/src/runtime.mjs b/packages/bus/src/runtime.mjs index a16ab2ac6..859d0a630 100644 --- a/packages/bus/src/runtime.mjs +++ b/packages/bus/src/runtime.mjs @@ -95,7 +95,11 @@ export function createLibraryLookup({ dataRoot, sync, now = () => Date.now() }) const source = { ...context, dataRoot }; let state = await libraryModule.libraryStatus(source); 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) { try { await sync(source); } catch (error) { diff --git a/packages/bus/tests/library-lookups.test.mjs b/packages/bus/tests/library-lookups.test.mjs index d01d0e9ef..a612af4a7 100644 --- a/packages/bus/tests/library-lookups.test.mjs +++ b/packages/bus/tests/library-lookups.test.mjs @@ -159,16 +159,25 @@ test('freshness boundary, unknown last fetch and failed fetch still use immutabl f.setState({ lastFetch: null }); await f.call('library.universe', { universe: 'harbor' }); assert.equal(f.fetches(), 2); - let attempted = 0; - const failing = createLibraryLookup({ dataRoot: f.dataRoot, now: () => NOW, sync: async () => { + let attempted = 0, clock = NOW; + const failing = createLibraryLookup({ dataRoot: f.dataRoot, now: () => clock, sync: async () => { 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' }; } }); f.setState({ lastFetch: null }); const result = await failing(f.context, { method: 'get', args: GET, authorize() {} }); assert.equal(attempted, 1); 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 => { diff --git a/packages/harness/tests/library-tools.test.mjs b/packages/harness/tests/library-tools.test.mjs index 771b53956..9208a208f 100644 --- a/packages/harness/tests/library-tools.test.mjs +++ b/packages/harness/tests/library-tools.test.mjs @@ -5,6 +5,7 @@ import { mkdirSync, readFileSync } from 'node:fs'; import { join } from 'node:path'; import { typedTools } from '../src/tools.mjs'; import { buildBundle } from '../src/bundle.mjs'; +import { TYPES } from '../../library/src/mapping.mjs'; import { HARNESS, REPO_SKILLS, resolvedFor, scratch } from './helpers.mjs'; 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])); 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_get.parameters.properties.from.minimum, 1); assert.equal(byName.library_get.parameters.properties.links_from.minimum, 0);