Skip to content

feat: add daily brain journal export timer - #20

Merged
Mauryanx merged 2 commits into
mainfrom
fm/journal-export-schedule
Oct 9, 2026
Merged

Mauryanx merged 2 commits into
mainfrom
fm/journal-export-schedule

Conversation

@Mauryanx

@Mauryanx Mauryanx commented Oct 9, 2026

Copy link
Copy Markdown
Owner

Intent

Phase 0 item LR3 of the live rollout plan (~/dev/firstmate/data/live-rollout-plan/report.md), part of the hardening build: smallest durable best-practice fix, never a today-only patch, never over-engineered. Under the five rooms, the nightly brain journal (04:05 UTC) reads only /srv/brain/journal-input, which Firstmate fills with FM_BRAIN_DESK_ENABLED=1 bin/fm-brain-desk.py journal-export (merged in #15). Nothing schedules that export today.

(Substance of the referenced items: the rollout plan's LR3 says something must run FM_BRAIN_DESK_ENABLED=1 bin/fm-brain-desk.py journal-export daily before the 04:05 UTC brain journal, because under the brain rooms the journal reads only /srv/brain/journal-input and nothing schedules the export today; until it ships it is run by hand. PR 15 added bin/fm-brain-desk.py, whose journal-export operation copies the home's backlog, done archive, captain, learnings and task reports into that journal-input directory and is off unless FM_BRAIN_DESK_ENABLED=1.)

What Changed

  • Add an installer for systemd user units that run journal-export daily at 03:45 UTC with explicit FM_HOME and brain-desk opt-in, catching up missed runs before the 04:05 UTC journal.
  • Document installation, activation, lingering requirements, monitoring, reinstallation, and rollback.
  • Add coverage for rendered and installed units, schedule settings, export execution, and invalid paths, with systemd syntax and calendar checks when available.

Risk Assessment

✅ Low: The bounded timer schedules the existing exporter before 04:05 UTC with explicit home and opt-in settings; no material correctness, privacy, or intent-conformance issues were found.

Testing

The focused suite and manual CLI checks passed, with transcripts showing installed units, UTC calendar evaluation, exported records, refreshes and refusals. Isolated systemd startup was blocked by cgroup permissions, leaving automatic firing, catch-up and failed-unit reporting untested. Temporary files were removed and the worktree is clean.

  • Live validation: ⚠️ inconclusive - 5 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Operator installs both units for a selected home; installation writes mode-0644 files without starting an export. ✅ pass live journal-schedule-cli.log:33; installed units and modes shown above it
Operator evaluates the installed calendar in a non-UTC timezone; systemd reports consecutive daily runs at 03:45 UTC. ✅ pass live journal-schedule-cli.log:41,44,47,50
Operator runs the generated service command; it exports the selected home's journal records and refreshes changed records on subsequent runs. ✅ pass live journal-schedule-cli.log:52-67; brain-desk-tests.log:10
Operator supplies relative, missing or unescaped home paths and invalid render arguments; the installer refuses without writing units. ✅ pass live journal-schedule-cli.log:68-99
Operator runs the service command with an unavailable journal destination; export exits nonzero without creating that destination. ✅ pass live journal-schedule-cli.log:100-103
Operator enables the timer; systemd automatically runs the export before the 04:05 UTC journal. ⏸️ untested no Tried user/PID isolation with unshare, then downloaded and extracted systemd inside the worktree and launched it through bubblewrap with isolated writable directories. Unshare could not write uid_map;…
Operator restarts the user manager after a missed daily run; Persistent scheduling catches up the export. ⏸️ untested no The workspace-local systemd manager could not start because cgroup.procs was inaccessible, so no isolated manager could be stopped and restarted. Supply a disposable systemd host/container with writab…
A scheduled export refuses; systemd exposes the service as failed without retrying. ⏸️ untested no The export refusal was driven directly, but the isolated systemd manager exited on cgroup.procs permission denial before loading the service. Supply a disposable systemd host/container with writable d…
Evidence: Focused product test transcript

Source: Focused product test transcript

FM_TEST_BEGIN 2026-10-09T11:36:01Z tests/fm-brain-desk.test.sh family=standalone expected_gate_skip=none
........
----------------------------------------------------------------------
Ran 8 tests in 11.063s

OK
ok - desk launcher uses fixed argv and a 0640 prompt published once per task
ok - brainctl verbs use fixed argv, pass text and status through, and refuse non-JSON
ok - opt-in off does no I/O and enabled missing endpoints refuse
ok - journal export replaces only journal records atomically as 0640 with source mtimes
ok - malformed results, unsafe citations, bad input and room failure expose nothing
ok - ordinary punctuation passes and an unsafe fact is withheld without discarding the rest
ok - a symlinked request ancestor refuses without privilege calls or writes
ok - an expired desk call relays SIGTERM through sudo so no room child survives
ok - journal export schedule: valid daily timer before 04:05 UTC runs this export for its home
FM_TEST_END 2026-10-09T11:36:13Z tests/fm-brain-desk.test.sh exit=0 duration_ms=12169 gate_skip=false
FM_TEST_SUMMARY total=1 failed=0 skipped_gate=0 duration_ms=12375
FM_TEST_SUMMARY_FAMILY family=standalone count=1 duration_ms=12169 failed=0
FM_TEST_SLOWEST rank=1 script=tests/fm-brain-desk.test.sh duration_ms=12169
Evidence: Installed units, evaluated calendar, exported records and refusals

Source: Installed units, evaluated calendar, exported records and refusals

$ ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/bin/fm-brain-journal-export-service.sh install
~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/.test-journal-export/manual/config/systemd/user/fm-brain-journal-export.service
~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/.test-journal-export/manual/config/systemd/user/fm-brain-journal-export.timer
exit=0
fm-brain-journal-export.service (mode 0644):
# Generated by ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/bin/fm-brain-journal-export-service.sh; re-run its install rather than editing.
[Unit]
Description=Firstmate brain journal-input export for ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/.test-journal-export/manual/home
Documentation=file://~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/docs/configuration.md

[Service]
Type=oneshot
Environment=FM_HOME=~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/.test-journal-export/manual/home
Environment=FM_BRAIN_DESK_ENABLED=1
WorkingDirectory=~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/.test-journal-export/manual/home
ExecStart=~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/bin/fm-brain-desk.py journal-export
NoNewPrivileges=yes

fm-brain-journal-export.timer (mode 0644):
# Generated by ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/bin/fm-brain-journal-export-service.sh; re-run its install rather than editing.
[Unit]
Description=Daily Firstmate brain journal-input export before the 04:05 UTC journal
Documentation=file://~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/docs/configuration.md

[Timer]
OnCalendar=*-*-* 03:45:00 UTC
Persistent=true
Unit=fm-brain-journal-export.service

[Install]
WantedBy=timers.target

PASS: installing writes only both units and does not start an export
$ systemd-analyze verify ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/.test-journal-export/manual/config/systemd/user/fm-brain-journal-export.service ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/.test-journal-export/manual/config/systemd/user/fm-brain-journal-export.timer
/usr/lib/systemd/system/xfs_scrub_all.service:26: Support for option CPUAccounting= has been removed and it is ignored
/usr/lib/systemd/system/system-xfs_scrub.slice:15: Support for option CPUAccounting= has been removed and it is ignored
exit=0
$ systemd-analyze calendar '--base-time=2026-10-09 00:00:00 UTC' --iterations=3 '*-*-* 03:45:00 UTC'
Normalized form: *-*-* 03:45:00 UTC
    Next elapse: Fri 2026-10-09 09:15:00 IST
       (in UTC): Fri 2026-10-09 03:45:00 UTC
       From now: 7h ago
   Iteration #2: Sat 2026-10-10 09:15:00 IST
       (in UTC): Sat 2026-10-10 03:45:00 UTC
       From now: 16h left
   Iteration #3: Sun 2026-10-11 09:15:00 IST
       (in UTC): Sun 2026-10-11 03:45:00 UTC
       From now: 1 day 16h left
exit=0
PASS: real systemd calendar consumer evaluates the installed schedule in a non-UTC timezone
$ ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/bin/fm-brain-desk.py journal-export
{"copied":6,"files":6}
exit=0
EXPORTED data/backlog.md: mode=0640 mtime=1700000000 content='# Backlog\n- [ ] scheduled item\n'
EXPORTED data/captain.md: mode=0640 mtime=1700000000 content='Daily export validation\n'
EXPORTED data/done-archive.md: mode=0640 mtime=1700000000 content='# Done\n- archived item\n'
EXPORTED data/learnings.md: mode=0640 mtime=1700000000 content='Durable finding\n'
EXPORTED data/remote-secondmates/mate/data/task-b/report.md: mode=0640 mtime=1700000000 content='# Report B\n'
EXPORTED data/task-a/report.md: mode=0640 mtime=1700000000 content='# Report A\n'
PASS: generated service command opts in and exports all six allowed records from the selected home
$ ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/bin/fm-brain-desk.py journal-export
{"copied":0,"files":6}
exit=0
$ ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/bin/fm-brain-desk.py journal-export
{"copied":1,"files":6}
exit=0
PASS: repeated fresh command updates changed backlog and skips unchanged records
$ ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/bin/fm-brain-journal-export-service.sh install
fm-brain-journal-export-service.sh: FM_HOME must be an absolute path: relative/home
exit=1
$ ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/bin/fm-brain-journal-export-service.sh install
fm-brain-journal-export-service.sh: FM_HOME is not a directory: ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/.test-journal-export/manual/missing
exit=1
$ ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/bin/fm-brain-journal-export-service.sh install
fm-brain-journal-export-service.sh: FM_HOME has characters this unit does not escape: ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/.test-journal-export/manual/has space
exit=1
$ ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/bin/fm-brain-journal-export-service.sh render
Usage:
  fm-brain-journal-export-service.sh render service|timer
      print that unit on stdout
  fm-brain-journal-export-service.sh install
      write both units to ${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/
      (mode 0644, atomic replace) and print their paths, service first
FM_HOME (default: this checkout) must be an existing absolute directory.
Paths with characters outside [A-Za-z0-9._/@+_-] are refused rather than escaped for systemd.
Exit 0 on success, 1 on a refused path or failed write, 2 on usage.
exit=2
$ ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/bin/fm-brain-journal-export-service.sh render bogus
Usage:
  fm-brain-journal-export-service.sh render service|timer
      print that unit on stdout
  fm-brain-journal-export-service.sh install
      write both units to ${XDG_CONFIG_HOME:-$HOME/.config}/systemd/user/
      (mode 0644, atomic replace) and print their paths, service first
FM_HOME (default: this checkout) must be an existing absolute directory.
Paths with characters outside [A-Za-z0-9._/@+_-] are refused rather than escaped for systemd.
Exit 0 on success, 1 on a refused path or failed write, 2 on usage.
exit=2
PASS: invalid paths and render requests refuse before writing units
$ ~/.no-mistakes/worktrees/8352e211db7e/01M4G70PN2WFAM31G3D35CJAED/bin/fm-brain-desk.py journal-export
brain desk refused request or result
exit=1
PASS: unavailable export destination returns nonzero without creating it
Evidence: Isolated systemd startup blocker

Source: Isolated systemd startup blocker

Isolated scheduler validation attempts
1. unshare --user --map-root-user --pid --fork true
Result: unshare: write failed /proc/self/uid_map: Operation not permitted
2. Downloaded systemd with apt-get download systemd and extracted it with dpkg-deb -x inside .test-journal-export/tools (no system installation).
3. Ran that systemd --user --unit=basic.target --log-target=console --log-level=debug through bwrap --unshare-all.
Host filesystem was read-only, and writable /run, /tmp, HOME and XDG directories were bound to disposable worktree directories.
Result: isolated manager exit=1.
4. Repeated through strace -e trace=file,write to locate the startup blocker:
statfs("/sys/fs/cgroup/", {f_type=CGROUP2_SUPER_MAGIC, f_bsize=4096, f_blocks=0, f_bfree=0, f_bavail=0, f_files=0, f_ffree=0, f_fsid={val=[0x6ef8a532, 0xedac724f]}, f_namelen=255, f_frsize=4096, f_flags=ST_VALID|ST_RDONLY|ST_NOSUID|ST_NODEV|ST_NOEXEC|ST_RELATIME}) = 0
openat(AT_FDCWD, "/sys/fs/cgroup/memory.max", O_RDONLY|O_CLOEXEC) = -1 ENOENT (No such file or directory)
openat(AT_FDCWD, "/sys/fs/cgroup", O_RDONLY|O_CLOEXEC|O_PATH|O_DIRECTORY) = 5
newfstatat(AT_FDCWD, "/sys/fs/cgroup", {st_mode=S_IFDIR|0555, st_size=0, ...}, 0) = 0
mkdir("/sys/fs/cgroup/init.scope", 0755) = -1 EEXIST (File exists)
openat(AT_FDCWD, "/sys/fs/cgroup/init.scope/cgroup.procs", O_WRONLY|O_NOCTTY|O_CLOEXEC) = -1 EACCES (Permission denied)
+++ exited with 1 +++
No timer was activated in this run. Starting it in the operator user manager would modify state outside the authorized worktree.
To validate activation, calendar-triggered export, Persistent catch-up and failed-unit visibility, supply a disposable Linux systemd host/container with its own writable delegated cgroup subtree, and authorize its systemd state writes.
  • Outcome: ⚠️ 2 warnings across 1 run (5m21s)

Pipeline

Updates from git push no-mistakes

✅ **Intent** - passed

✅ No issues found.

✅ **Rebase** - passed

✅ No issues found.

✅ **Review** - passed

✅ No issues found.

⚠️ **Test** - 2 warnings
  • ⚠️ bin/fm-brain-journal-export-service.sh - The core automatic scheduling requirement remains unproven. A workspace-local systemd manager launched through bubblewrap exited because it could not write cgroup.procs. Supply a disposable Linux systemd host or container with writable delegated cgroups and authorization for its isolated systemd state writes to validate timer firing, missed-run catch-up and failed-unit visibility.
  • ⚠️ live validation verdict: inconclusive (5 of 8 scenarios were driven live against the product); untested: Operator enables the timer; systemd automatically runs the export before the 04:05 UTC journal., Operator restarts the user manager after a missed daily run; Persistent scheduling catches up the export., A scheduled export refuses; systemd exposes the service as failed without retrying.
  • Live validation: ⚠️ inconclusive - 5 of 8 scenarios driven live against the product
Scenario Result Live Evidence
Operator installs both units for a selected home; installation writes mode-0644 files without starting an export. ✅ pass live journal-schedule-cli.log:33; installed units and modes shown above it
Operator evaluates the installed calendar in a non-UTC timezone; systemd reports consecutive daily runs at 03:45 UTC. ✅ pass live journal-schedule-cli.log:41,44,47,50
Operator runs the generated service command; it exports the selected home's journal records and refreshes changed records on subsequent runs. ✅ pass live journal-schedule-cli.log:52-67; brain-desk-tests.log:10
Operator supplies relative, missing or unescaped home paths and invalid render arguments; the installer refuses without writing units. ✅ pass live journal-schedule-cli.log:68-99
Operator runs the service command with an unavailable journal destination; export exits nonzero without creating that destination. ✅ pass live journal-schedule-cli.log:100-103
Operator enables the timer; systemd automatically runs the export before the 04:05 UTC journal. ⏸️ untested no Tried user/PID isolation with unshare, then downloaded and extracted systemd inside the worktree and launched it through bubblewrap with isolated writable directories. Unshare could not write uid_map;…
Operator restarts the user manager after a missed daily run; Persistent scheduling catches up the export. ⏸️ untested no The workspace-local systemd manager could not start because cgroup.procs was inaccessible, so no isolated manager could be stopped and restarted. Supply a disposable systemd host/container with writab…
A scheduled export refuses; systemd exposes the service as failed without retrying. ⏸️ untested no The export refusal was driven directly, but the isolated systemd manager exited on cgroup.procs permission denial before loading the service. Supply a disposable systemd host/container with writable d…
  • TMPDIR="$PWD/.test-journal-export/tmp" bin/fm-test-run.sh tests/fm-brain-desk.test.sh
  • PYTHONDONTWRITEBYTECODE=1 python3 .test-journal-export/manual.py drove installation, generated-command exports, refreshes and adversarial refusals.
  • systemd-analyze verify <installed-service> <installed-timer>
  • TZ=Asia/Kolkata systemd-analyze calendar --base-time='2026-10-09 00:00:00 UTC' --iterations=3 '*-*-* 03:45:00 UTC'
  • unshare --user --map-root-user --pid --fork true
  • Downloaded systemd with apt-get download systemd, extracted it locally with dpkg-deb -x, and attempted isolated systemd --user --unit=basic.target through bwrap --unshare-all; repeated with strace -e trace=file,write to identify the blocker.
✅ **Document** - passed

✅ No issues found.

✅ **Lint** - passed

✅ No issues found.

✅ **Push** - passed

✅ No issues found.

@Mauryanx
Mauryanx merged commit b5efb38 into main Oct 9, 2026
19 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant