Skip to content

Fix watcher restart loop - #48

Merged
sonichi merged 1 commit into
mainfrom
fix-watcher-loop
Mar 30, 2026
Merged

sonichi merged 1 commit into
mainfrom
fix-watcher-loop

Conversation

@sonichi

@sonichi sonichi commented Mar 30, 2026

Copy link
Copy Markdown
Owner

Fixes infinite restart loop caused by early-exit-on-existing-files logic. Reverted to clean fswatch -1 with event filters.

🤖 Generated with Claude Code

Previous fix to check existing files caused an infinite restart loop
(exit immediately → notification → restart → exit immediately...).

Reverted to simple fswatch -1 with --event Created --event Updated
filters. Improved dedup to avoid killing own PID/PPID.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sonichi
sonichi merged commit 9e1cdfc into main Mar 30, 2026
@sonichi
sonichi deleted the fix-watcher-loop branch March 30, 2026 16:32
liususan091219 pushed a commit to liususan091219/sutando-fork that referenced this pull request May 28, 2026
…onichi#48)

Previous fix to check existing files caused an infinite restart loop
(exit immediately → notification → restart → exit immediately...).

Reverted to simple fswatch -1 with --event Created --event Updated
filters. Improved dedup to avoid killing own PID/PPID.

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
liususan091219 added a commit that referenced this pull request Oct 10, 2026
… agents through a lost upstream

0.4.5 (randombet/bodhi_realtime_agent#52) fixes the #48 review items the relay
agent depends on: under upstreamLossPolicy 'hold' a park or provider close is
not a session end in actor mode (no agent stop, one redialer), upstreamRecovery
requires the recovery primitives, and active silence hears recognized input.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
liususan091219 added a commit that referenced this pull request Oct 10, 2026
… agents through a lost upstream

0.4.5 (randombet/bodhi_realtime_agent#52) fixes the #48 review items the relay
agent depends on: under upstreamLossPolicy 'hold' a park or provider close is
not a session end in actor mode (no agent stop, one redialer), upstreamRecovery
requires the recovery primitives, and active silence hears recognized input.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
liususan091219 added a commit that referenced this pull request Oct 10, 2026
…r own results (#5310)

* Revert "feat(voice): relay agent — a task table reconciled with the core until the user has heard every outcome (#5290)"

This reverts commit e841a92. The relay agent is rebuilt as a bodhi
PersistentSubagentInstance in actor mode in the next PR.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* feat(voice): relay agent as a bodhi subagent — work calls return their own results

The relay agent is rebuilt as the bodhi PersistentSubagentInstance behind the
work tool, in actor mode:
- work is a background tool (pending message: the old "working on it" text);
  RelayAgent.invoke submits the task through the tool's own submission and
  resolves with that task's core result, which bodhi hands to the model when
  it is idle (and holds through a meeting). A queue position or an offline
  core is said at submission.
- A result whose call is gone (cancelled, session closed) takes the ordinary
  delivery path; a voice task that cannot be delivered leaves one DM copy.
- Kept from #5290: the voice task table, planReconcile (an unheard result is
  handed over again at most twice; a task not picked up after 3 min is
  reported once), and the status answers (get_task_status, the cancel list,
  the count ahead) read from the table.
- Not kept: #5290's own result queue and pacing; bodhi's delivery does that.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* chore(deps): bodhi-realtime-agent 0.4.5 — actor mode under hold keeps agents through a lost upstream

0.4.5 (randombet/bodhi_realtime_agent#52) fixes the #48 review items the relay
agent depends on: under upstreamLossPolicy 'hold' a park or provider close is
not a session end in actor mode (no agent stop, one redialer), upstreamRecovery
requires the recovery primitives, and active silence hears recognized input.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(voice): drop the unused readdirSync import

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(voice): relay agent owns the result queue — heard only when spoken, DM when undeliverable, no pacing

The previous head marked a result heard the moment it was handed to bodhi and
had no fallback when the session could not take it (a result for a closed page
waited in memory, lost on restart). The relay agent now owns #5290's result
queue again, minus pacing:
- invoke returns the submission status at once (queued, the queue position, a
  duplicate, a rejection) as the tool result; no pendingMessage.
- RelayAgent.attachDelivery binds the queue to the session. Results are handed
  over through session.tryPublishSystemNotification; a refusal is retried, then
  the result goes to the DM, one copy per result.
- A result is recorded spoken only when the turn ends, handed over again once
  if the user cuts it off, names the request it answers, and is held through a
  meeting. Phone-call results go through the same queue.
- Not kept: conversation pacing and the 2 s gather window. bodhi delivers
  when the model is idle; the pacer delayed a result 49 s on room noise.
- Tests: the queue, refusal -> DM, the subagent, and the two single-copy
  guards (an offline copy still in results/, a result already sent to the DM),
  each failing when its guard is removed.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(voice): the task table records which result answered a task — a deduped request is never left unheard

Live: three PR status requests; the core answered all three in the first
task's result and wrote [deduped: <first>] for the other two. The table did
not know that, so the first result was handed over naming only its own
request (the model spoke only that part) and the other two rows, holding a
skip marker, were owed nothing.

- A row records answeredBy when its result is [deduped: <task>] (local and
  relay-mode result paths; dedupTarget in skip_marker_ownership).
- A result is handed over naming every request it answers (its own and each
  unheard row answered by it) and records all of their deliveries together.
- Reconcile judges an answeredBy row by its answering task's result: if that
  was handed over without it (the dedup landed later), it is handed over
  again, named for this row's request.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(voice): work is a Gemini async call that returns its result; the task table covers what the call cannot

Delivery goes back to the work call itself, now declared NON_BLOCKING:
invoke waits for its task's result and returns it as the function response,
so Gemini decides when to say it and which call it answers. The relay's own
result queue is removed.

The task table covers what a pending call cannot carry:
- a fresh provider connection (session.reconnectBoundary): responses to calls
  made on the old one would be fenced off, so those calls end quietly and
  their results go out as a system notice naming the request;
- a cancelled call or a restart: same notice path; one DM copy when the
  session cannot speak; held through a meeting;
- a result answering several tasks (answeredBy) ends their waiting calls and
  records them all; a late dedup is offered to the task's own waiting call
  by reconcile first.
A queue position or an offline core is said at submission.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(voice): work keeps its pending message — the model answers at once while the call waits for the result

Live at 5b046c3: with work declared NON_BLOCKING and no pending message the
model said nothing after calling it (an empty turn) until the user spoke
again; it waited for the function response, which comes only when the core
finishes. The pending message is back: bodhi answers the call at once with
"still in progress" and hands the result over as a system notification when
invoke returns, which a fresh provider connection does not drop, so the
reconnect-boundary detach is gone too. invoke still waits for its task's
result; the task table and reconcile cover a call that is gone.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(voice): a work call always ends — on cancel, on abort, and when its result goes to the DM

- Cancel: the core never answers a cancelled task, so its waiting call hung
  until the session ended. noteVoiceTaskCancelled now tells the relay agent,
  which ends the call quietly.
- Abort: the relay rejected with "aborted", and because work has a pending
  message bodhi published "Background task failed… aborted. Tell the user"
  while the task kept running. The call now ends normally with "still
  running; the result comes separately", and the result goes the usual way.
- No client connected: the result watcher sends a voice task's result to the
  DM (it never offers it to the relay), so the waiting call also hung. It now
  ends that call and the calls of the tasks answered in the same result.

No call ends with a rejection: bodhi reads a rejection out as a failure.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

* fix(voice): a queued voice task is cancelled at once — no instruction whose reply contradicts the table

Live on the test Mac: the user cancelled a task still in the queue. cancel_task
deleted its file and also wrote a CANCEL_INSTRUCTION, then told the model to
wait for the core. The core found no file and replied "Nothing to cancel —
task was never queued"; that reply was read out as "the cancellation couldn't
be processed".

A queued task whose file is deleted never runs, so it is cancelled: the user is
told so at once and no instruction is written. If the core took the task in
that instant, its result already comes with CANCELLED_BUT_FINISHED_NOTE. The
cancel_requested state ("waiting for the core to confirm") is gone with it.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
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