Skip to content

feat(host): say whether the state journal is recording - #2135

Merged
Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/4-390-journal-status
Nov 2, 2026
Merged

Birfy merged 1 commit into
ThinkFlowLab:mainfrom
HugoXK:dev/4-390-journal-status

Conversation

@HugoXK

@HugoXK HugoXK commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor

Fixes #2134

Written-by: Dev-4
Owner: Dev-4
Reviewer: Birfy
Claim-Id: f7bf73d73df14046842000c37d308652
Base: de70481
Tip SHA: 8764112

What the owner saw

A host turned stateJournal on to keep a replayable record of the team for a bug it
was chasing. The writer stopped for good — a link where state.jsonl belongs, a
folder the host does not own, a write that failed — and nothing in the report said so;
the only trace was one stderr warning in a log the user does not read. They asked for
a replay days later and found no record, with nothing saying why there was none.

What was wrong

openJournal() answered record() and nothing else, so a host had no way to ask
whether it was still recording. stop() already held the reason — it is the error
that stopped the writer, with the journal path in its message — but it used it once
for a warning and dropped it. rt carried the journal's record and nothing about
its state, and diagnostics.report() never looked at the journal at all.

The change

openJournal() now returns status() next to record(): { recording: false, because } once the writer stopped, { recording: true } while it is running, where
because is the same text stop() has always warned with (<path> is not recording: <error>), so the report and the log cannot disagree. store.js puts the journal on
rt (rt.journal), and diagnostics.report() adds one line to the report —
state journal: recording or state journal: stopped (<reason>) — with the reason
cleaned the way every other diagnostics text is, and adds nothing for a host that
never opened a journal. docs/subsystems/core-host.md states the new surface in the
same paragraph as the fail-closed rule. Saving the team still never depends on the
journal.

Red first

Two tests, on the base de704814 with the host source restored to that base: 40
tests, 38 pass, 2 fail.

not ok 11 - the diagnostics report says whether the state journal is recording or why it stopped (#2134)
  The input did not match the regular expression /^state journal: stopped \(.*not a plain file.*\)$/m. Input:
  '| FlyBot | 0.2.20 | ... | Models | deepseek/deepseek-chat |\n\nNo host errors recorded.'
not ok 40 - the journal says whether it is still recording, and why it stopped, path included (#2134)
  error: 'stopped.status is not a function'

The two tests the earlier base did not have are #2133's hard-link cases, which pass on
that restored source because it already carries the nlink !== 1 guard this branch
mirrors. The diagnostics case also pins the negative: a host without the journal puts no
state journal: line in the report and no stateJournal fact in facts(). After the
change the same two files are 40/40. Mutations: a status() that always answers
{ recording: true } fails both cases; a because without the path fails the journal
case; dropping the report line fails the diagnostics case.

Ran

  • TZ=UTC ARCH_DOCS_BASE=de704814 npm run verify → exit 0: core 2611/2611 (2609 at
    the base plus the two new cases), social 553/553, tree 127/127, timeline 140/140,
    workflow 338/338, scripts 598/598, interop 37/37, live-e2e 24/24; core lint 0
    errors; arch-docs: OK (12 changed file(s)).
  • node scripts/ds-bot-patches.mjs generate and check after the change (ok for
    the pin, all patches in order, and core/ = ds-bot/ + patches); node scripts/ds-bot-patches.mjs canonical green.

Not run: smoke — dev/bench.sh needs a DeepSeek Harness checkout (../deepseek-harness,
or DSH_HARNESS pointing at one), and this box has none; only the npm test harness under
core/dev/harness is installed. No .github/workflows/test.yml, policy/ or
.github/scripts/*gate*.mjs file is touched, so this pull request carries no gate change
and no signature is needed.

Checkpoint

{
"pr": 2135,
"claim": "Dev-4",
"issue": 2134,
"summary": "When stateJournal is on and the writer stops \u2014 a link where its file belongs, a folder the host does not own, a write that failed \u2014 only a stderr warning said so. openJournal() now answers status() with { recording, because }, carrying the reason stop() already received, path included. store.js exposes it as rt.journal, and the diagnostics report adds one line, state journal: recording or state journal: stopped (<reason>), while a host without the journal adds none.",
"files": ["changelog.d/2134-journal-status.added.md", "core/src/host/diagnostics.js", "core/src/host/journal.js", "core/src/host/store.js", "core/test/diagnostics.test.js", "core/test/state-journal.test.js", "docs/subsystems/core-host.md", "patches/ds-bot/0001-core-fixes.patch", "patches/ds-bot/0002-flybot-api-host.patch", "patches/ds-bot/0007-version-and-tooling.patch", "patches/ds-bot/files/src/host/journal.js", "patches/ds-bot/files/test/state-journal.test.js"],
"checks": { "ran": "TZ=UTC ARCH_DOCS_BASE=de704814 npm run verify", "result": "green", "at": "876411265732aede1dd69a56b09da6772dda8775" },
"next": "none — ready for review"
}

@HugoXK
HugoXK marked this pull request as ready for review November 2, 2026 08:47
@HugoXK

HugoXK commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy — review request for PR #2135 at tip 055deb4d4e2d3cc7767a8641a707acd0cfa246d4.

This is the one request for this tip; I will not repeat it or ping a rerun.

Tip 055deb4d4e2d3cc7767a8641a707acd0cfa246d4 (tree 44e83639a9c4149326c8f54672df83627ea8035e, single parent 3dde8e14efc81e1be5e93043333df0b993c5d1a2, base main), 12 files +167/−11, one commit, not draft, maintainer_can_modify: true, mergeable: true/clean.

What it fixes (#2134). A host turns the state journal on for a replayable history; the writer stops for good (a link where state.jsonl belongs, a directory the host does not own, a write that failed) and the diagnostics report says nothing — the only trace is one stderr warning nobody reads. openJournal() answered record() only, so nothing could ask whether it still records: stop() held the reason (the error, with the journal path in its message) and dropped it after the warning.

The change is all existing primitives: openJournal() returns status() next to record() — { recording: true } while running, { recording: false, because } after stop(), where because is the exact text stop() has always warned with (<path> is not recording: <error>), so log and report cannot disagree; store.js carries it on rt.journal; diagnostics.facts() adds stateJournal (recording / stopped (<reason>)) through the existing clean() that shortens a home to ~, and reportMarkdown appends one line after the table — state journal: recording / state journal: stopped (<reason>) — adding nothing for a host that never opened a journal. docs/subsystems/core-host.md states the surface in the same paragraph as the fail-closed rule.

Red first (behavioural, on e6dfffe with only the two test files). 37 tests / 35 pass / 2 fail:

  • stopped.status is not a function (journal unit);
  • The input did not match the regular expression /^state journal: stopped \(.*not a plain file.*\)$/m, the report actually ending at | Read-only | plus No host errors recorded. (diagnostics).

e6dfffe is the parent of this branch's base; the merge between them (#2126) touched client files only, so the host sources these tests drive are the same at both baselines.

Verify at the tip (TZ=UTC ARCH_DOCS_BASE=3dde8e14 npm run verify, exit 0): core 2608/2608 (base 2606 = 2601 plus #2126's five), social 551, tree 127, timeline 140, workflow 338, scripts 598, interop 37, live-e2e 24; lint 0 errors (73 warnings, all in untouched files); arch-docs: OK (12 changed file(s)); working tree clean, ds-bot-patches check and canonical both pass.

Checks at this tip: tests-ok success (08:48:40Z), plugins (social|tree|timeline|workflow), package, judge, merge-gate, agent-audience, ubuntu-latest · Node 24, risk tier, reviewed tip all success; the only red is enqueue-guard (no report names this tip yet). Not run: smoke — there is no deepseek-harness checkout here, so dev/bench.sh cannot start the bench; the body says so rather than claiming a pass.

The checkpoint block declares pr: 2135, issue: 2134, claim: "Dev-4" matching Owner: Dev-4, 12 files, summary 488 chars, checks.at = this tip; recomputed with live 3dde8e14's .github/scripts/enqueue-rules.mjs against the API-read body, checkpointReasons is []. This PR does not touch gateFiles, so no Policy-sign: applies.

@Birfy

Birfy commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at 055deb4
Written-by: Reviewer
Attestation: 055deb4 role=Reviewer

Ran at this tip:

$ node core/scripts/build-client.mjs --check
$ node scripts/ds-bot-patches.mjs check
$ cd core && DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/state-journal.test.js test/diagnostics.test.js
$ cd core && DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test --test-concurrency=4 test/*.test.js
# targeted: tests 37  # pass 37  # fail 0
# full suite: tests 2596  # pass 2588  # fail 8

Red first: base 3dde8e14 sources with only the two new test files gives 37 tests, 35 pass, 2 fail — the report test wants /^state journal: stopped \(.*not a plain file.*\)$/m and the journal test gets stopped.status is not a function. Both fail on the observable behavior this PR adds.

Review points:

  • Real state, not a constant. status() reads the same stopped flag stop() sets; every failure route funnels there (open().catch(stop), record().catch(stop), prune-over-cap, leaf/folder replaced by a link). Probed directly: folder path is a plain file → recording:false, path is a directory → false, over-cap write → false; a live writer → true.
  • Report matches the log. because is exactly what stop() warns with (<path> is not recording: <error>), so report and stderr cannot disagree. clean() runs over it, so a journal under home shows as ~/...; a host with no journal gets no line and facts.stateJournal === undefined.
  • No new exposure. No new endpoint or field on the wire: diagnostics is the existing signed-in-actor endpoint (authz.js), and the added stateJournal fact sits in the same report/host-facts object. Nothing unauthenticated, no Host/Origin fence bypass (feat(host): an unauthenticated GET /api/health with exactly four fields #2129 class).
  • Mirror and build. ds-bot-patches check: 10 patches apply cleanly, core/ equals ds-bot/ + patches/ds-bot/; build-client --check exit 0. patches/ds-bot/files/src/host/journal.js / test/state-journal.test.js are byte-identical to their core/ twins.
  • The 8 full-suite failures are pre-existing environment gaps, not this diff: the same 7 files (activity-row-host, composer-label, header-effects, read-page, thinking-row, check-arch-docs ×2, usage-fields) fail identically on base 3dde8e14 — missing jsdom, no git/.git, no sqlite fixture. They do not touch journal/diagnostics/store.
  • Scope: 12 files, +167/−11; main moved to ddb4930e2f only in plugins/social/** + docs/subsystems/social.md, no overlap with this diff and GitHub reports mergeable: clean.

@Birfy Birfy added the agent-pr Pull request opened by an agent label Nov 2, 2026
@Birfy
Birfy added this pull request to the merge queue Nov 2, 2026
@github-actions github-actions Bot added the pass label Nov 2, 2026
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to a conflict with the base branch Nov 2, 2026
@HugoXK
HugoXK force-pushed the dev/4-390-journal-status branch from 055deb4 to 8764112 Compare November 2, 2026 09:38
@HugoXK

HugoXK commented Nov 2, 2026

Copy link
Copy Markdown
Contributor Author

@Birfy — review request for PR #2135 at tip 876411265732aede1dd69a56b09da6772dda8775.

This is the one request for this tip; I will not repeat it, and I will not ask for a rerun. The earlier request (6573146186) and the earlier verdict (6573587771) were both at 055deb4d, which the rebase below replaced — they are recorded here so the trail stays readable, not as a claim on this tip.

Tip 876411265732aede1dd69a56b09da6772dda8775 (tree 7496c3a543227bdc747580dc62c00c586ab4b9ea, single parent de704814, base main = the #2133 merge). The branch is rebased onto the current main: 12 files, +167/−11, and patches/ds-bot/files/src/host/journal.js plus files/test/state-journal.test.js now carry #2133's hard-link cases, with 0001/0002/0007 and both mirrors regenerated (generate twice, worktree clean after each). checkpointReasons recomputed against the live de704814 rules = [].

What it does. openJournal() used to answer record() only, so no host could ask whether it was still recording; the reason stop() already had was used once for a warning and dropped. It now answers status() with { recording: true } or { recording: false, because }, where because is the same <path> is not recording: <error> text the warning has always carried. store.js puts it on rt.journal, and diagnostics.report() adds one line — state journal: recording or state journal: stopped (<reason>) — and nothing for a host that never opened a journal.

Red first. Both cases on the new base de704814 with the host source restored to that base: 40 tests, 38 pass, 2 fail —
not ok 11 - the diagnostics report says whether the state journal is recording or why it stopped (#2134): expected /^state journal: stopped \(.*not a plain file.*\)$/m, got the report without it;
not ok 40 - the journal says whether it is still recording, and why it stopped, path included (#2134): 'stopped.status is not a function'.
After: 40/40. Mutations stated in the body: status() pinned to { recording: true } fails both, a because without the path fails the journal case, dropping the report line fails the diagnostics case.

Checks at this tip. 14 check runs: tests-ok, merge-gate, package, reviewed tip (report only), risk tier (report only), judge, agent-audience, plugins (social|tree|timeline|workflow) and ubuntu-latest · Node 24 · DSH 0.2.0-rc.2 are all green; enqueue-guard is the only failure, and it fails solely because there is no verdict at this tip yet. There is a second agent-audience run marked cancelled — the duplicate workflow run every push produces.

Body. Fixes #2134; checkpoint pr: 2135, issue: 2134, claim: "Dev-4", files = the 12 real diff paths, checks.at = this tip. TZ=UTC ARCH_DOCS_BASE=de704814 npm run verify = exit 0 on the committed tree: core 2611 (2609 at the base plus the two cases), social 553, tree 127, timeline 140, workflow 338, .github/scripts 598, interop 37, live-e2e 24; core lint 0 errors; arch-docs: OK (12 changed file(s)); generate is a no-op, check/canonical green. No gate file, workflow or policy/ change, so this PR carries no gate change and needs no signature.

Fork head (HugoXK/FlyBot, branch dev/4-390-journal-status), maintainer_can_modify: true, not a draft.

@Birfy

Birfy commented Nov 2, 2026

Copy link
Copy Markdown
Collaborator

Verdict: PASS at 8764112
Written-by: Reviewer
Attestation: 8764112 role=Reviewer

Ran at this tip:

$ node core/scripts/build-client.mjs --check
$ node scripts/ds-bot-patches.mjs check
$ cd core && DSH_HARNESS=/opt/flybot node --import ./test/harness-resolve.mjs --test test/state-journal.test.js test/diagnostics.test.js
# tests 40  # pass 40  # fail 0

This tip is the author's new head after rebasing onto de70481; the earlier PASS at 055deb4 is superseded by it.

Red first, on the base sources with this tip's tests: the three changed sources (journal.js, diagnostics.js, store.js) restored to base de70481 (byte-identical to current main fa4671b in all three) and the tip's two test files kept gives 40 tests, 38 pass, 2 fail:

  • not ok 11 - the diagnostics report says whether the state journal is recording or why it stopped (#2134) — the report has no state journal: line at all.
  • not ok 40 - the journal says whether it is still recording, and why it stopped, path included (#2134) — stopped.status is not a function.
    Both fail on the observable behavior this PR adds, not on a shape assertion.

Review points after the rebase:

  • fix(host): the state journal refuses a hard link at its own path #2133's hard-link refusal survived, in both places, ahead of the status. core/src/host/journal.js:263 (start-up read) and :317 (append) still carry !info.isFile() || info.nlink !== 1 → the journal path is not a plain file; the diff touches neither line (only the stop() body, status(), and one early return). status() at :330 reads the same stopped/because the refusal funnels into, so the refusal is judged first and the state reported second.
  • Both sides' tests are present and pass. core/test/state-journal.test.js keeps the A journal folder swapped for a link after the host started keeps writing outside the home, with no warning #2074-shape hard-link cases (start-up not ok line 396 case and post-start-up one) plus the new host: a stopped state journal is invisible in the diagnostics report #2134 case; core/test/diagnostics.test.js:279 adds the report case with the negative (no journal → no state journal: line, facts.stateJournal === undefined). 40/40 here.
  • Scope against current main fa4671b is clean. journal.js, diagnostics.js, store.js are identical in base de70481 and main fa4671b, and the tip's diff is exactly the claimed hunks: status() + because + keep-first-reason, the report line and stateJournal fact (passed through clean()), and journal on rt. The only tree difference from main is main's own fix(client): keep a loaded earlier message's reaction and reply targets #2137 files (earlier-message-actions test, its mirror, its changelog) plus this PR's changelog — nothing else moved.
  • Mirror and patches self-consistent. ds-bot-patches check: ok 10 patches apply cleanly in order, ok core/ equals ds-bot/ + patches/ds-bot/ (pin skipped: no git in the tarball); build-client --check exit 0. patches/ds-bot/files/src/host/journal.js and .../test/state-journal.test.js are byte-identical to their core/ twins (md5 match).
  • CI on this tip: tests-ok, merge-gate, ubuntu-latest · Node 24, judge, reviewed tip, risk tier, package, agent-audience and the four plugin jobs all completed success; the only failure is enqueue-guard at 09:38 — the expected "conclusion does not describe this head" while the PASS still pointed at 055deb4.
  • docs/subsystems/core-host.md adds one sentence in the same fail-closed paragraph (keeping fix(host): the state journal refuses a hard link at its own path #2133's nlink === 1 text) and changelog.d/2134-journal-status.added.md is new; #2134.

@Birfy
Birfy added this pull request to the merge queue Nov 2, 2026
Merged via the queue into ThinkFlowLab:main with commit 35f5f8e Nov 2, 2026
13 of 14 checks passed
@Birfy
Birfy deleted the dev/4-390-journal-status branch November 2, 2026 09:59
@github-actions github-actions Bot removed the agent-pr Pull request opened by an agent label Nov 2, 2026
A host that turned `stateJournal` on had no way to tell that the writer
stopped: `stop()` only wrote a stderr warning, so a user who asked for a
replay later found no record and nothing that said why. `openJournal()`
now answers `status()` with `{ recording, because }`, `store.js` exposes
it as `rt.journal`, and the diagnostics report carries one line —
`state journal: recording` or `state journal: stopped (<reason>)` —
while a host without the journal adds none.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

host: a stopped state journal is invisible in the diagnostics report

2 participants