Skip to content

fix: resolve three code-review-filed bugs (#502, #504, #507) - #517

Draft
TYRMars wants to merge 2 commits into
mainfrom
claude/vibrant-dijkstra-xkz1t9
Draft

fix: resolve three code-review-filed bugs (#502, #504, #507)#517
TYRMars wants to merge 2 commits into
mainfrom
claude/vibrant-dijkstra-xkz1t9

Conversation

@TYRMars

@TYRMars TYRMars commented Jul 22, 2026

Copy link
Copy Markdown
Owner

Resolves three independent bugs filed by the scheduled cr code-review task. Two are behavioural fixes with regression tests; the third is a documentation-alignment resolution for a deferred, design-level gap (see the caveat).

Closes #504 — completed once automations get re-armed on PATCH

recomputeNextRun re-anchored the schedule with previousRun = undefined. For an interval schedule that is correct, but for a once schedule previousRun (last_run_at) is the only "already fired" signal — passing undefined wrote the original, now-past run_at back into next_run_at, making a finished one-shot immediately due again.

The single caller fires on any PATCH /v1/automations/:id carrying a schedule key — including a UI save that echoes the unchanged schedule back — so editing an unrelated field (rename, model change) silently re-executed the task's prompt (backup / deploy / send report) that the user scheduled to run exactly once.

Fix: thread the task's own last_run_at through (scheduleNextAfter(task.schedule, task.last_run_at, now)). Interval re-anchoring for a never-run task is unchanged; a completed once stays completed.

Closes #502atomicWrite shared .tmp loses concurrent writes

atomicWrite derived its staging file solely from the target path, so all concurrent writers of one id shared a single <path>.tmp. Writer B rewrote A's staging file; whoever renamed first consumed it, and the loser's rename hit ENOENT — a silently-dropped write (the REST message routes catch-and-ignore the failure).

Fix: per-write staging name (<path>.<pid>.<uuid>.tmp) so the outcome is plain last-writer-wins, and remove the staging file on failure so a crashed write leaves no litter. Every list() scanner gates on endsWith(".json"), so the new tmp names stay invisible to directory scans. This helper backs every JSON-file store plus @jarvis/observability and @jarvis/learning.

Addresses #507 — store→WS fanout has zero subscribers (docs-alignment; wiring deferred)

The store-side Fanout has no subscriber in the running server, so todo_upserted / requirement_upserted frames are never emitted — yet the store doc comments, the SPA frame handlers, and their unit tests all read as though the bridge is live.

Actually wiring the /v1/chat/ws bridge (subscribe to state.todos + the project/requirement fanout, forward workspace-filtered frames, unsubscribe on close) spans multiple stores and must match the SPA's exact frame-decode contracts — a design-level change a maintainer should sign off on (which frames, which filtering semantics). Emitting subtly-wrong frames would be worse than a documented gap, so this run takes the issue's own alternative resolution: align the docs to reality so the tested-but-dead handlers stop reading as wired, and flag the full bridge as a deferred, maintainer-owned task. No runtime behaviour changes.

  • packages/todo/src/store.ts, packages/server/src/todos-routes.ts — mark the fanout a ready seam, not a live feature.
  • apps/jarvis-web/src/services/{todos,requirements}.ts — the frame handlers exercise the contract, not a live path.

Maintainer decision needed: whether to wire the WS bridge in a follow-up or keep it deferred. If you want it wired, I can take that on as a separate, focused PR.

Tests

  • recomputeNextRun keeps a completed 'once' task completed (#504) and an interval re-anchor pin.
  • atomicWrite: concurrent writes to one target don't ENOENT — last-writer-wins — three overlapping writes all settle, the survivor is exactly one writer's full payload, no .tmp litter remains.

@jarvis/automation (38/38), the json-file portion of @jarvis/store (20/20), and @jarvis/todo + @jarvis/server typecheck are green; eslint clean. CI build passed. The sqlite test failures seen locally are an uncompiled better-sqlite3 native binding (build scripts skipped in this environment), unrelated to this change.

🤖 Generated with Claude Code

https://claude.ai/code/session_01TpB8YUSXH3RpNwYBhsHxkC

claude added 2 commits July 22, 2026 01:28
…omicWrite concurrent-safe

Resolves two independent, deterministic bugs surfaced by the code-review task.

#504 — recomputeNextRun re-anchored with previousRun=undefined, which for a
`once` schedule discards the only "already fired" signal. PATCHing any field of
a completed one-shot (a UI save echoing the unchanged schedule back) wrote the
past run_at into next_run_at and re-executed the prompt. Thread the task's own
last_run_at through instead: interval re-anchoring for a never-run task is
unchanged, and a finished `once` stays finished.

#502 — atomicWrite derived its staging file solely from the target path, so
concurrent writers of one id shared a single `.tmp`. The rename race dropped the
loser's write with ENOENT (silently swallowed by the REST message routes). Use a
per-write staging name (`<path>.<pid>.<uuid>.tmp`) so the outcome is plain
last-writer-wins, and clean up the staging file on failure. All list() scanners
gate on `.json`, so the new tmp names remain invisible to them. Benefits every
JSON-file store plus @jarvis/observability and @jarvis/learning.

Adds regression tests for both.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpB8YUSXH3RpNwYBhsHxkC
Issue #507: the store-side Fanout has zero subscribers in the running
server, so todo_upserted / requirement_upserted frames are never sent —
yet the store doc comments, the SPA frame handlers, and their unit tests
all read as though the bridge is live, so the next reader takes a
tested-but-dead path at face value.

Actually wiring the /v1/chat/ws bridge (subscribe to state.todos + the
project/requirement fanout, forward workspace-filtered frames, unsubscribe
on socket close) spans multiple stores and must match the SPA's exact
frame-decode contracts — a design-level change that needs a maintainer's
call on which frames/filtering to ship, not an autonomous guess. Emitting
subtly-wrong frames would be worse than a documented gap.

So, per the issue's own alternative resolution, align the docs to reality
without changing runtime behaviour:
- packages/todo/src/store.ts — the fanout contract is a ready seam, not a
  live feature; subscribe() has no non-test caller.
- packages/server/src/todos-routes.ts — strengthen the "when wired" hedge
  into an explicit not-wired-yet note.
- apps/jarvis-web/src/services/{todos,requirements}.ts — the frame handlers
  exercise the contract, not a live path; no live cross-tab updates today.

No code paths change; the tested-but-dead handlers now read honestly and
the full bridge is flagged as a deferred, maintainer-owned task.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TpB8YUSXH3RpNwYBhsHxkC
@TYRMars TYRMars changed the title fix(automation,store): stop resurrecting completed one-shots; make atomicWrite concurrent-safe fix: resolve three code-review-filed bugs (#502, #504, #507) Jul 22, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment