Skip to content

Fix npm fallback + meeting passive mode - #142

Merged
sonichi merged 2 commits into
mainfrom
fix/startup-npm-meeting-mode
Apr 5, 2026
Merged

sonichi merged 2 commits into
mainfrom
fix/startup-npm-meeting-mode

Conversation

@sonichi

@sonichi sonichi commented Apr 5, 2026

Copy link
Copy Markdown
Owner

startup.sh: npm→pnpm→yarn fallback with nvm instructions. voice-agent: stricter meeting silence.

sonichi and others added 2 commits April 4, 2026 16:12
Voice agent writes to /tmp/sutando-live-transcript-voice.txt in real-time.
Phone calls write to /tmp/sutando-live-transcript-{callSid}.txt with symlink
to /tmp/sutando-live-transcript.txt. Both append timestamped entries as
turns complete. Enables live note-taking during meetings.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
startup.sh: try npm → pnpm → yarn, print nvm instructions if all fail.
Fixes setup for users with restricted npm global context.

voice-agent.ts: meeting mode now requires COMPLETE silence — no speaking,
no work calls, no task creation. Only responds to "Sutando" + direct command.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@sonichi
sonichi merged commit c22c5e9 into main Apr 5, 2026
@sonichi
sonichi deleted the fix/startup-npm-meeting-mode branch April 5, 2026 00:18
Wenjix added a commit to Wenjix/sutando that referenced this pull request Apr 5, 2026
…ssive meeting mode

Resolved conflict in src/voice-agent.ts by keeping our Cartesia infrastructure
(CartesiaSTTProvider, generateSpeech, injectText, env-driven models) while
integrating upstream's new features (live transcript to /tmp, insight file
greeting, stricter passive meeting mode).

Upstream brings 38 commits including:
- Live transcript for voice sessions and phone calls (sonichi#140)
- Multi-screen capture support (sonichi#171, sonichi#172)
- X posting skill (sonichi#165, sonichi#167)
- Proactive friction detector (sonichi#161, sonichi#162)
- Bidirectional memory + notes sync (sonichi#176)
- Migration script for new Mac setup (sonichi#139)
- Stricter passive meeting mode (sonichi#142)
- Daily insight generator (sonichi#157, sonichi#158)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
liususan091219 pushed a commit to liususan091219/sutando-fork that referenced this pull request May 28, 2026
* Add live transcript for voice sessions and phone calls

Voice agent writes to /tmp/sutando-live-transcript-voice.txt in real-time.
Phone calls write to /tmp/sutando-live-transcript-{callSid}.txt with symlink
to /tmp/sutando-live-transcript.txt. Both append timestamped entries as
turns complete. Enables live note-taking during meetings.

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

* Fix npm fallback in startup.sh + strengthen meeting passive mode

startup.sh: try npm → pnpm → yarn, print nvm instructions if all fail.
Fixes setup for users with restricted npm global context.

voice-agent.ts: meeting mode now requires COMPLETE silence — no speaking,
no work calls, no task creation. Only responds to "Sutando" + direct command.

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

---------

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
qingyun-wu added a commit that referenced this pull request Jul 21, 2026
…item 3)

Broker re-serves a task on lease expiry (at-least-once). If the worker already
handled it, _write_task must NOT re-queue it (no second local execution) — the
existing guard drops a [no-send] result so the drain re-acks it upstream. Adds the
test for that re-serve-after-completion branch (both triggers: task archived, and
reply already archived). Mutation-checked: disabling the archived-dedup guard makes
this test fail. Completes freeze-gate item 3 alongside items 5/6 in this PR.
qingyun-wu added a commit that referenced this pull request Jul 21, 2026
…v1 freeze gate) (#2240)

* test(sparrow): atomic publish + in-flight recovery regression tests

Two v1-freeze-gate regression tests for the ag2-sparrow transport client
(gate items 5 & 6 of ag2-space/ag2space-backend#142), locking down the
worker-edge properties the broker's at-least-once contract depends on:

- test_write_task_atomic.py (gate item 5): _write_task publishes a complete
  file (access_tier written last), leaves no .txt.tmp sidecar, never exposes a
  partial file to the watcher on a publish crash (rename failure => dest absent),
  is idempotent under gateway redelivery (a modified-body redelivery must NOT
  overwrite the queued file — proven via a mutation check, not just an
  identical-body no-op), and drops id-less / unsafe-id tasks.
- test_inflight_recovery.py (gate item 6): the persisted in-flight ledger
  survives a restart (save -> reload -> load round-trips the set), so a task
  pulled but not yet POSTed to /v1/results isn't lost across a crash; the write
  is atomic (no .json.tmp sidecar) and the read fails open (corrupt/missing
  ledger loads empty, never raises).

Complements the atomic-write hardening in #2236/#2237 (per-PID staging) — those
are the fix, these pin the behavior so it can't regress. Tests follow the
existing tests/test_gateway_status.py harness (env-injected task/result/state
dirs + importlib.reload); no product code changed.

* test(sparrow): no-double-execution on re-serve after completion (#142 item 3)

Broker re-serves a task on lease expiry (at-least-once). If the worker already
handled it, _write_task must NOT re-queue it (no second local execution) — the
existing guard drops a [no-send] result so the drain re-acks it upstream. Adds the
test for that re-serve-after-completion branch (both triggers: task archived, and
reply already archived). Mutation-checked: disabling the archived-dedup guard makes
this test fail. Completes freeze-gate item 3 alongside items 5/6 in this PR.
qingyun-wu added a commit that referenced this pull request Jul 21, 2026
… the endpoint (#2241)

* fix(sparrow): self-healing retry for task /ack when the gateway gains the endpoint

_post_task_ack POSTs /v1/tasks/<id>/ack on every pulled task so the broker can
surface a "received" state. When the gateway lacks the endpoint it 404s — but the
worker latched `_ack_disabled = True` permanently, so a broker that LATER deploys
the endpoint (e.g. the #142 freeze-gate /ack landing on the relay) is never picked
up until the worker restarts. That's a silent deploy-ordering trap: acks stay off,
and the "received/working" ladder shows nothing, with no error.

Replace the permanent bool latch with a time-gated backoff (`_ack_disabled_until`,
epoch): a 404/405 backs off for REMOTE_ACK_RETRY_COOLDOWN (default 300s) then
retries, and a successful ack clears it. A newly-deployed endpoint is thus picked
up automatically, no restart needed. Heartbeat keeps its own latch (separate
concern, endpoint has always existed).

Test (tests/test_ack_retry.py): success-no-backoff, invalid-tid-no-network,
404-backs-off-within-cooldown, and self-heal-after-cooldown. The self-heal test is
mutation-checked — a permanent latch (cooldown → ∞) fails it, the finite cooldown
passes it — so it genuinely pins the retry, not an identical-body no-op.

* test(sparrow): update src bridge test for _ack_disabled → _ack_disabled_until rename

The canonical package rename (permanent latch → time-gated backoff) left the
src-side mock-gateway test suite (src/remote-gateway-bridge.test.py) asserting on
the old _ack_disabled bool, which no longer exists — the '404 disables ack' check
failed in CI (tsc + tests / clean install). Update it to the new symbol +
semantics (404 arms a retryable backoff, _ack_disabled_until > 0) and clear the
timer after so later calls aren't skipped. Also fix the stale symbol in the shim's
namespace-doc comment. src suite + package ack test both green.

* fix(sparrow): a per-task 404 on /ack must not disable acking globally

_post_task_ack armed a global cooldown on ANY 404. But a 404 can be per-task — the
gateway answers 'not leased to you' for an expired/re-served/foreign lease — not
'endpoint absent'. One such routine 404 would then skip /ack for every other task
until the cooldown expired, recreating the visibility blind spot this change
removes, triggered by normal churn. Distinguish by response body: a 404 carrying
the 'not leased' marker is a single-task negative ack (skip that one, keep acking
the rest); only a bare no-route 404 or a 405 arms the unsupported-endpoint cooldown.
Adds a test (mutation-checked) + a bare-404-still-cools test; existing behavior for
the pre-endpoint bare 404 is unchanged.
john-the-dev pushed a commit that referenced this pull request Jul 21, 2026
* test(sparrow): atomic publish + in-flight recovery regression tests (v1 freeze gate) (#2240)

* test(sparrow): atomic publish + in-flight recovery regression tests

Two v1-freeze-gate regression tests for the ag2-sparrow transport client
(gate items 5 & 6 of ag2-space/ag2space-backend#142), locking down the
worker-edge properties the broker's at-least-once contract depends on:

- test_write_task_atomic.py (gate item 5): _write_task publishes a complete
  file (access_tier written last), leaves no .txt.tmp sidecar, never exposes a
  partial file to the watcher on a publish crash (rename failure => dest absent),
  is idempotent under gateway redelivery (a modified-body redelivery must NOT
  overwrite the queued file — proven via a mutation check, not just an
  identical-body no-op), and drops id-less / unsafe-id tasks.
- test_inflight_recovery.py (gate item 6): the persisted in-flight ledger
  survives a restart (save -> reload -> load round-trips the set), so a task
  pulled but not yet POSTed to /v1/results isn't lost across a crash; the write
  is atomic (no .json.tmp sidecar) and the read fails open (corrupt/missing
  ledger loads empty, never raises).

Complements the atomic-write hardening in #2236/#2237 (per-PID staging) — those
are the fix, these pin the behavior so it can't regress. Tests follow the
existing tests/test_gateway_status.py harness (env-injected task/result/state
dirs + importlib.reload); no product code changed.

* test(sparrow): no-double-execution on re-serve after completion (#142 item 3)

Broker re-serves a task on lease expiry (at-least-once). If the worker already
handled it, _write_task must NOT re-queue it (no second local execution) — the
existing guard drops a [no-send] result so the drain re-acks it upstream. Adds the
test for that re-serve-after-completion branch (both triggers: task archived, and
reply already archived). Mutation-checked: disabling the archived-dedup guard makes
this test fail. Completes freeze-gate item 3 alongside items 5/6 in this PR.

* fix(gateway-bridge): prefer IPv4 for the relay (v6-blackhole ~26s delay) (#2238)

On hosts where IPv6 is black-holed at the network (SYN silently dropped), the
relay host's AAAA records make Python's getaddrinfo return v6 first, so each
fresh urllib connection — one per long-poll AND one per outbound send, no
keep-alive — hangs ~26s on the dead v6 address before falling back to v4. That
timeout is added to EVERY inbound message and EVERY reply, so the owner sees a
~26s delay each way and messages appear dropped (measured live 2026-07-20: stuck
tcp6 SYN_SENT to the relay's 2606:4700:20:: address; v4 connect in 0.25s).

Filter getaddrinfo to A records for the gateway host (`ag2.space`) at import, so
the dead v6 path is never tried. Keep the original result when there is no v4
address (a genuinely v6-only destination still resolves), and scope the filter
to the relay host so nothing else is affected. Opt out with
REMOTE_GATEWAY_ALLOW_IPV6=1 for hosts with working v6.

Verified: filter returns v4-only for chat.ag2.space ([AF_INET,AF_INET6] ->
[AF_INET]); non-relay hosts pass through unchanged; py_compile clean.

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(sparrow): per-PID staging for state-file atomic writes (#2222 follow-up) (#2237)

remote_gateway_bridge.py staged four state files under a shared `.json.tmp`
name. For last-owner-activity.json this is the #2222 cross-process bug directly
— the sparrow bridge is the fourth writer alongside slack/discord/telegram, and
two concurrent writers can truncate/interleave the shared temp so the rename
publishes torn JSON to the proactive loop's presence check. For gateway-status,
task-rooms, and inflight it is latent: safe with one sparrow instance, a
torn-write race the moment two run.

Stage each under a per-PID name (`.json.{os.getpid()}.tmp`) and publish with
os.replace (atomic overwrite) — no two processes ever share a temp path. Also
switches gateway-status from Path.replace to os.replace so all four sites are
uniform (behaviour-identical; both are atomic overwrite).

Completes the sparrow follow-up noted on #2222; the three in-repo bridges are
#2236. The per-PID mechanism is proven under real concurrency by
tests/owner-activity-atomic-write.test.py (#2236, identical pattern), so this
does not re-prove it.

Test (tests/sparrow-per-pid-staging.test.py, main harness):
- source guard: all four sites stage per-PID, none uses the shared `.json.tmp`;
  mutation-checked (reverting any site to the shared name fails it).
- behavioral: imports the bridge, spies os.replace, and confirms the live
  gateway-status write stages a pid-suffixed temp and publishes a valid file.
Existing sparrow tests (test_gateway_status.py) still pass — the Path.replace →
os.replace change is behaviour-preserving.


Claude-Session: https://claude.ai/code/session_015BuRpcYxqsRQsoV44E5Gh1

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(sparrow): self-healing retry for task /ack when the gateway gains the endpoint (#2241)

* fix(sparrow): self-healing retry for task /ack when the gateway gains the endpoint

_post_task_ack POSTs /v1/tasks/<id>/ack on every pulled task so the broker can
surface a "received" state. When the gateway lacks the endpoint it 404s — but the
worker latched `_ack_disabled = True` permanently, so a broker that LATER deploys
the endpoint (e.g. the #142 freeze-gate /ack landing on the relay) is never picked
up until the worker restarts. That's a silent deploy-ordering trap: acks stay off,
and the "received/working" ladder shows nothing, with no error.

Replace the permanent bool latch with a time-gated backoff (`_ack_disabled_until`,
epoch): a 404/405 backs off for REMOTE_ACK_RETRY_COOLDOWN (default 300s) then
retries, and a successful ack clears it. A newly-deployed endpoint is thus picked
up automatically, no restart needed. Heartbeat keeps its own latch (separate
concern, endpoint has always existed).

Test (tests/test_ack_retry.py): success-no-backoff, invalid-tid-no-network,
404-backs-off-within-cooldown, and self-heal-after-cooldown. The self-heal test is
mutation-checked — a permanent latch (cooldown → ∞) fails it, the finite cooldown
passes it — so it genuinely pins the retry, not an identical-body no-op.

* test(sparrow): update src bridge test for _ack_disabled → _ack_disabled_until rename

The canonical package rename (permanent latch → time-gated backoff) left the
src-side mock-gateway test suite (src/remote-gateway-bridge.test.py) asserting on
the old _ack_disabled bool, which no longer exists — the '404 disables ack' check
failed in CI (tsc + tests / clean install). Update it to the new symbol +
semantics (404 arms a retryable backoff, _ack_disabled_until > 0) and clear the
timer after so later calls aren't skipped. Also fix the stale symbol in the shim's
namespace-doc comment. src suite + package ack test both green.

* fix(sparrow): a per-task 404 on /ack must not disable acking globally

_post_task_ack armed a global cooldown on ANY 404. But a 404 can be per-task — the
gateway answers 'not leased to you' for an expired/re-served/foreign lease — not
'endpoint absent'. One such routine 404 would then skip /ack for every other task
until the cooldown expired, recreating the visibility blind spot this change
removes, triggered by normal churn. Distinguish by response body: a 404 carrying
the 'not leased' marker is a single-task negative ack (skip that one, keep acking
the rest); only a bare no-route 404 or a 405 arms the unsupported-endpoint cooldown.
Adds a test (mutation-checked) + a bare-404-still-cools test; existing behavior for
the pre-endpoint bare 404 is unchanged.

* fix(bridges): per-PID staging for last-owner-activity.json (concurrent writes) (#2236)

* fix(bridges): per-PID staging for last-owner-activity.json (concurrent writes)

`write_owner_activity()` in the slack, discord, and telegram bridges staged
its atomic write under a SHARED temp name (`OWNER_ACTIVITY_FILE.with_suffix
(".json.tmp")`). last-owner-activity.json is written by four processes (these
three bridges + the sparrow remote-gateway bridge), so two that fire within the
same window both open and truncate the same temp file; their writes interleave,
and the rename can publish torn JSON. The reader is the proactive loop's
owner-presence check, which then sees a corrupt or partial timestamp.

Stage under a per-PID name (`.json.{os.getpid()}.tmp`) so no two processes ever
share a temp file, and use os.replace (atomic overwrite): last writer wins,
cleanly, with no torn intermediate. Closes #2222.

Scope: the three in-repo bridges. The sparrow package
(packages/ag2-sparrow/…/remote_gateway_bridge.py) shares the same pattern across
several state files (owner-activity, gateway-status, task-rooms, inflight); that
is a separate package boundary and a follow-up, noted on #2222. Fixing these
three already removes the realistic collision — they are the bridges that
co-run on one host, and once they use distinct temp names no two writers share
a path.

Test (tests/owner-activity-atomic-write.test.py, runs under npm run test:py):
- concurrency proof: 12 spawned processes hammer the target with the per-PID
  pattern; a concurrent reader samples throughout and every read parses — 0 torn.
  (A self-check, `--demo-old-pattern-tears`, shows the shared-name pattern DOES
  produce torn mid-race reads, so the proof exercises a real race.)
- source guard: the three call sites use the per-PID form, not the bare
  `.json.tmp`. Mutation-checked: reverting any one to the shared name fails it.

multiprocessing uses the spawn context with module-level targets (macOS-safe;
avoids the #2201 heredoc re-import trap). Bounded by a 5s deadline + join.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015BuRpcYxqsRQsoV44E5Gh1

* fix(sparrow): per-PID staging for last-owner-activity.json (4th writer)

The sparrow remote-gateway bridge's _write_owner_activity() is the fourth
concurrent writer of state/last-owner-activity.json named in this PR's body,
but was staging under the shared ".json.tmp" name the rest of the PR removes —
so a sparrow write racing a discord/slack/telegram write could still publish
torn JSON to the proactive-loop's owner-presence reader.

Apply the identical fix: per-PID temp name + os.replace (atomic overwrite,
last-writer-wins). Extend the source-guard test's writer list to include the
sparrow module so a revert is caught (mutation-verified: reverting this hunk
fails test_source_sites_use_perpid_staging). The other .json.tmp writes in that
file (gateway-status, task-rooms, inflight) are single-writer and correctly
untouched. Completes #2222 across all four writers.

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

* fix(task-bridge): per-PID staging for the 5th owner-activity writer

The per-PID staging fix covered the four Python owner-activity writers but missed
src/task-bridge.ts, which still staged last-owner-activity.json under the shared
'.tmp' name — the identical target path (with_suffix('.json.tmp') and
OWNER_ACTIVITY_FILE + '.tmp' resolve to the same file). Two concurrent task-bridge
writes could therefore still truncate/interleave the shared temp and publish a
torn file, so the race this change removes was still reachable for that writer
class. Move it to a process.pid-suffixed staging name (renameSync stays — atomic
rename(2)) and extend the writer-census guard test to cover the TS site so the
5-writer set stays complete. Mutation-checked. (qingyun-001 blocking finding.)

* fix(bridges): per-invocation (PID+uuid) owner-activity staging — thread-safe

PID-only staging is unique across processes but SHARED across threads in one
process, so a multi-threaded writer (Slack Bolt runs listeners on a
ThreadPoolExecutor) still collides on the same '.json.<pid>.tmp' path — two
listener threads truncate/interleave it and os.replace only makes the final
rename atomic, not the concurrent writes to the shared staging inode. Repro (8
threads): 1,216 torn reads + 5,276 replacement failures. Move the four
owner-activity writers (slack/discord/telegram + sparrow) to a per-INVOCATION
name (PID + uuid4().hex), unique across processes AND threads. Adds a same-process
threaded regression test (per-invocation → 0 torn; PID-only witness → does tear)
and tightens the census guard to require the uuid form. Mutation-checked.
(qingyun-001 blocking finding.)

* test(sparrow): update the OTHER staging guard for the owner-activity uuid form

The per-invocation (PID+uuid) owner-activity staging also has to satisfy the
existing tests/sparrow-per-pid-staging.test.py guard, which still required
OWNER_ACTIVITY_FILE to stage PID-only — so that suite failed. Split it: the three
single-writer state files (gateway-status/task-rooms/inflight) keep the PID-only
requirement; OWNER_ACTIVITY_FILE now requires the per-invocation PID+uuid form and
explicitly rejects the thread-unsafe PID-only one. Both owner-activity guard tests
(this + owner-activity-atomic-write) now pass; grepped tests/ to confirm these are
the only two that assert the staging pattern. (qingyun-001 re-review finding.)

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(sparrow): combined integration E2E — connected → task-once → result-once → reconnect → no-dup recovery

Component tests assert each primitive in isolation; John asked for one
combined live run proving the integration-heavy reconnect path. This harness
drives the REAL bridge module through the full sequence against an in-process
mock gateway (real HTTP contract → genuinely connected; reconnect forced
deterministically by re-serving the in-flight task-id) and prints
gateway/task/result log excerpts. Asserts exactly one local task, one ack,
one result POST across a forced redelivery — no duplicate, no loss.

Addresses the review request on #2245.

* fix(test): drive main()'s loop body in the combined E2E redelivery leg

The committed integration E2E did not exercise the control flow the bridge
actually runs. On the redelivery leg it called _write_task() directly and
jumped to _post_ready_results(), skipping inflight-add, persist, and ack.

main() appends every id returned by _write_task() to pending_ack -- including a
deduped redelivery, since _write_task() returns the id for already-handled work
precisely so the broker gets its ack and stops replaying -- then acks each one
after inflight is durable. The test therefore published acks=1 where the real
loop produces acks=2: a false tally for the runtime path, in the artifact
requested to close the integration review.

Both legs now go through one drive_poll() helper that mirrors main()'s block
line-for-line (write -> inflight -> persist -> ack every pending id -> drain),
and the redelivery is asserted to re-ack upstream.

Tally now matches a main()-shaped run: tasks_served=2, acks=2, real_results=1.

Mutation-checked: removing the ack loop fails both ack assertions.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-authored-by: Qingyun Wu <qingyun@ag2.ai>
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