Repository navigation
fix(workspace): honor SUTANDO_WORKSPACE in 5 TS sites — round 3 audit, first TS slice (issue #839) - #842
Merged
Conversation
…ols sites (issue #839) Round-3 audit followup to #836/#833/#838 — first slice of the TS audit flagged in #839. Five bug sites across two files, all hitting the same class: code paths resolving workspace data against `process.env.WORKSPACE_DIR` (wrong env var name) or `process.cwd()` instead of `resolveWorkspace()`. **Sites fixed:** - `src/cartesia-tts.ts:29` (`getWorkspace`) — TTS audio output dir (`results/audio/<category>/<label>-<ts>.wav`). Pre-fix: `WORKSPACE_DIR` env (legacy name, not set) → `process.cwd()` fallback → audio written to whatever cwd the launcher had. Now resolves to `$SUTANDO_WORKSPACE`. - `src/inline-tools.ts:471, 472` (`cancel_task` tool) — `tasksDir` and `resultsDir` for the cancel-task lookup. The tool listed tasks from `<cwd>/tasks/` and wrote cancel results to `<cwd>/results/`. On hosts with `SUTANDO_WORKSPACE` set, the bridge writes tasks to the workspace but cancel_task was looking in the repo (or whatever cwd) — so voice- initiated cancels would never find an in-flight task and would silently write a stranded CANCEL_INSTRUCTION file. - `src/inline-tools.ts:775` (`show_view` tool) — `dynamic-content.json` for the menu-bar app's view toggle. Pre-fix the inline tool wrote to `<cwd>/dynamic-content.json` while Sutando.app (post-#837) reads from `$SUTANDO_WORKSPACE/dynamic-content.json`. Split-brain: voice toggle fired but the app never saw it on hosts with `SUTANDO_WORKSPACE`. - `src/inline-tools.ts:873` (`recent_context` tool) — voice-agent state context. Pre-fix the read path looked at `<cwd>/state/voice-session-context.json` but the writer (core via CLAUDE.md "Voice session context" section) writes to `$SUTANDO_WORKSPACE/state/`. Voice-agent's deictic-pronoun recovery ("what was the post?", "what's pending?") would return `{note: "no context recorded yet"}` even when the writer had populated the file. - `src/inline-tools.ts:772` (`NOTES_DIR`) — `sharedPersonalPath('notes', process.cwd())` passes cwd as the workspace argument to the helper. When `SUTANDO_PRIVATE_DIR` isn't set, the helper falls through to `join(workspace, 'notes')` which becomes `<cwd>/notes/` instead of `$SUTANDO_WORKSPACE/notes/`. read_note / search_notes silently look in the wrong place. **Pattern (matches #821):** ```ts import { resolveWorkspace } from './workspace_default.js'; const WORKSPACE_DIR = resolveWorkspace(); ``` Module-init resolution mirrors the Python `WORKSPACE_DIR = resolve_workspace()` canonical pattern. Single source of truth per module. **Not addressed in this PR (still in #839's backlog):** - `src/voice-agent.ts:97, 103` — `WORKSPACE_DIR`/`CALL_RESULTS_DIR`. The highest-traffic sites; defer to a focused review. - `src/util_paths.ts:26, 54` — the fallback inside the helper itself. Cleaner to fix every caller first, then either delete the fallback or switch it to `resolveWorkspace()` so the helper has a sensible default. - `src/inline-tools.ts:957, 1021` — `join(process.cwd(), 'skills')`. These are CODE-adjacent paths (skills ship with the code), so they need REPO_ROOT semantics, not WORKSPACE_DIR. Latent today because the launcher always cwd's to the repo, but worth a separate PR. - `src/mentra-bridge.ts:88` — file is currently untracked (WIP). **Verified:** - `npx tsc --noEmit` ✓ clean across the whole tree. - Diff is targeted: 2 files, 22+/-8.
sonichi
reviewed
May 18, 2026
sonichi
left a comment
Owner
There was a problem hiding this comment.
LGTM — round-3 TS audit, clean.
5 migrations checked:
src/cartesia-tts.tsgetWorkspace()— drops theWORKSPACE_DIRenv var alias +process.cwd()fallback. resolveWorkspace() consolidates. ✓src/inline-tools.tscancelTaskTooltasksDir/resultsDir — wasprocess.cwd()/tasks, nowWORKSPACE_DIR/tasks. ✓ Same drift class as PR #821 callsites.src/inline-tools.tsNOTES_DIR —sharedPersonalPath('notes', process.cwd())→sharedPersonalPath('notes', WORKSPACE_DIR). Note the docstring update too — "else cwd/notes" → "else /notes". Good attention to comment-rot.src/inline-tools.tsdynamic-content.json— was repo-root drop. Now workspace. ✓src/inline-tools.tsVOICE_SESSION_CONTEXT_PATH — same shape. ✓
Bug-citation comment is the right shape: explains why process.cwd() "happened to match the workspace" pre-fix only when voice-agent was launched from the repo with SUTANDO_WORKSPACE unset. That's the silent-divergence story.
Non-blocking nit: the docstring says "first TS slice (issue #839)" — if there are more TS sites worth migrating, worth listing them in #839 explicitly so the round-N work is enumerable. Up to you.
Both checks green (tsc+tests SUCCESS, license/cla SUCCESS — second is empty in rollup but PR view shows it). MERGEABLE. Ship.
This was referenced May 18, 2026
qingyun-wu
added a commit
that referenced
this pull request
May 18, 2026
…owup) Migrates voice-agent.ts's `WORKSPACE_DIR` + `CALL_RESULTS_DIR` constants to `resolveWorkspace()` (the canonical TS helper introduced in #821). Two const definitions cover ~15 call sites — all of which use the path for per-user runtime state (tasks/, results/, state/, conversation.log, briefings, audio, dynamic-content, PIDFILE, etc.). **Why this matters:** On hosts with `SUTANDO_WORKSPACE` set (e.g. this machine, where `SUTANDO_WORKSPACE=/Users/qingyunwu/Documents/github/sutando-workspace-001`), the pre-fix behavior was: 1. `WORKSPACE_DIR = process.env.WORKSPACE_DIR || <repo-path-via-import.meta.url>` - `WORKSPACE_DIR` env var was the *legacy* name (now `SUTANDO_WORKSPACE`). - Fallback resolved to the repo checkout, so voice-agent wrote tasks, results, conversation.log, briefings, the PIDFILE, audio dirs, etc. to the **repo**, while Sutando.app (post-#837), Python bridges (post-#762), and #842 inline-tools (just landed) all wrote/read from the **workspace**. 2. `CALL_RESULTS_DIR = <repo>/results/calls` — phone-call result records landed under the repo too. Net effect: split-brain on `SUTANDO_WORKSPACE`-enabled hosts. Phone-call `latest-result.json` lived under the repo; new tasks the voice-agent generated landed there too; bridges/Sutando.app couldn't see them. **Pattern (matches #821 / #836 / #842):** ```ts import { resolveWorkspace } from './workspace_default.js'; const WORKSPACE_DIR = resolveWorkspace(); ``` Module-init resolution, single source of truth. CALL_RESULTS_DIR becomes `join(WORKSPACE_DIR, 'results', 'calls')` — same workspace, just the subpath. **Doc-comment cleanup:** - Renamed env-doc at line 19 from `WORKSPACE_DIR` (legacy) to `SUTANDO_WORKSPACE` (canonical) with a pointer to workspace_default.ts. - Removed the stale "default to sutando/ so Claude Code subprocess picks up CLAUDE.md automatically" comment at line 96. Voice-agent doesn't spawn Claude Code anymore (`grep -n claude src/voice-agent.ts` is empty) — task-bridge handles that via the file pipeline. The dual-use rationale that conflated repo + workspace into one var is obsolete. **Verified:** - `npx tsc --noEmit` ✓ clean. - Diff is targeted: 1 file, 15+/-4. - All 15 existing call sites (lines 98, 142, 334, 366, 515, 547, 551, 553, 799, 985, 1110, 1111, 1113, 1299, 1302, 1387) keep their `WORKSPACE_DIR` reference — they automatically pick up the new resolution. No site-by-site edits required. **Operational note:** This change takes effect on the next voice-agent restart. Currently-running voice-agent processes keep their old paths until restart. No mid-flight disruption.
qingyun-wu
added a commit
that referenced
this pull request
May 18, 2026
… cwd (#839) Round-3 audit followup — `personalPath()` and `sharedPersonalPath()` both defaulted the optional `workspace` parameter to `process.cwd()` when the caller didn't pass one. That's the same cwd-as-workspace antipattern the rest of the audit family is fixing: it produces the right path only when the caller's cwd happens to be the workspace dir. This PR changes the default to `resolveWorkspace()` (the canonical helper introduced in #821). Callers that DO pass a workspace are unchanged. **Why this is the last util_paths.ts piece of the audit:** Most callers in src/ already pass `WORKSPACE_DIR` explicitly after the #842 / #843 migration: - src/voice-agent.ts:345 — passes WORKSPACE_DIR (resolveWorkspace post-#843) - src/inline-tools.ts:764 (post-#842) — passes WORKSPACE_DIR (resolveWorkspace) What this fix catches is the still-non-passing callers: - src/voice-agent.ts:556, 583 — `personalPath('stand-identity.json')` with no workspace arg. Falls through to the default. Plus any future caller that forgets to pass workspace — the default is now sensible. **Verified:** - `npx tsc --noEmit` ✓ clean. - Diff is targeted: 1 file, 8+/-3.
qingyun-wu
added a commit
that referenced
this pull request
May 18, 2026
…owup) Migrates voice-agent.ts's `WORKSPACE_DIR` + `CALL_RESULTS_DIR` constants to `resolveWorkspace()` (the canonical TS helper introduced in #821). Two const definitions cover ~15 call sites — all of which use the path for per-user runtime state (tasks/, results/, state/, conversation.log, briefings, audio, dynamic-content, PIDFILE, etc.). **Why this matters:** On hosts with `SUTANDO_WORKSPACE` set (e.g. this machine, where `SUTANDO_WORKSPACE=/Users/qingyunwu/Documents/github/sutando-workspace-001`), the pre-fix behavior was: 1. `WORKSPACE_DIR = process.env.WORKSPACE_DIR || <repo-path-via-import.meta.url>` - `WORKSPACE_DIR` env var was the *legacy* name (now `SUTANDO_WORKSPACE`). - Fallback resolved to the repo checkout, so voice-agent wrote tasks, results, conversation.log, briefings, the PIDFILE, audio dirs, etc. to the **repo**, while Sutando.app (post-#837), Python bridges (post-#762), and #842 inline-tools (just landed) all wrote/read from the **workspace**. 2. `CALL_RESULTS_DIR = <repo>/results/calls` — phone-call result records landed under the repo too. Net effect: split-brain on `SUTANDO_WORKSPACE`-enabled hosts. Phone-call `latest-result.json` lived under the repo; new tasks the voice-agent generated landed there too; bridges/Sutando.app couldn't see them. **Pattern (matches #821 / #836 / #842):** ```ts import { resolveWorkspace } from './workspace_default.js'; const WORKSPACE_DIR = resolveWorkspace(); ``` Module-init resolution, single source of truth. CALL_RESULTS_DIR becomes `join(WORKSPACE_DIR, 'results', 'calls')` — same workspace, just the subpath. **Doc-comment cleanup:** - Renamed env-doc at line 19 from `WORKSPACE_DIR` (legacy) to `SUTANDO_WORKSPACE` (canonical) with a pointer to workspace_default.ts. - Removed the stale "default to sutando/ so Claude Code subprocess picks up CLAUDE.md automatically" comment at line 96. Voice-agent doesn't spawn Claude Code anymore (`grep -n claude src/voice-agent.ts` is empty) — task-bridge handles that via the file pipeline. The dual-use rationale that conflated repo + workspace into one var is obsolete. **Verified:** - `npx tsc --noEmit` ✓ clean. - Diff is targeted: 1 file, 15+/-4. - All 15 existing call sites (lines 98, 142, 334, 366, 515, 547, 551, 553, 799, 985, 1110, 1111, 1113, 1299, 1302, 1387) keep their `WORKSPACE_DIR` reference — they automatically pick up the new resolution. No site-by-site edits required. **Operational note:** This change takes effect on the next voice-agent restart. Currently-running voice-agent processes keep their old paths until restart. No mid-flight disruption.
qingyun-wu
added a commit
that referenced
this pull request
May 18, 2026
… cwd (#839) Round-3 audit followup — `personalPath()` and `sharedPersonalPath()` both defaulted the optional `workspace` parameter to `process.cwd()` when the caller didn't pass one. That's the same cwd-as-workspace antipattern the rest of the audit family is fixing: it produces the right path only when the caller's cwd happens to be the workspace dir. This PR changes the default to `resolveWorkspace()` (the canonical helper introduced in #821). Callers that DO pass a workspace are unchanged. **Why this is the last util_paths.ts piece of the audit:** Most callers in src/ already pass `WORKSPACE_DIR` explicitly after the #842 / #843 migration: - src/voice-agent.ts:345 — passes WORKSPACE_DIR (resolveWorkspace post-#843) - src/inline-tools.ts:764 (post-#842) — passes WORKSPACE_DIR (resolveWorkspace) What this fix catches is the still-non-passing callers: - src/voice-agent.ts:556, 583 — `personalPath('stand-identity.json')` with no workspace arg. Falls through to the default. Plus any future caller that forgets to pass workspace — the default is now sensible. **Verified:** - `npx tsc --noEmit` ✓ clean. - Diff is targeted: 1 file, 8+/-3.
sonichi
pushed a commit
that referenced
this pull request
May 18, 2026
…owup) (#843) Migrates voice-agent.ts's `WORKSPACE_DIR` + `CALL_RESULTS_DIR` constants to `resolveWorkspace()` (the canonical TS helper introduced in #821). Two const definitions cover ~15 call sites — all of which use the path for per-user runtime state (tasks/, results/, state/, conversation.log, briefings, audio, dynamic-content, PIDFILE, etc.). **Why this matters:** On hosts with `SUTANDO_WORKSPACE` set (e.g. this machine, where `SUTANDO_WORKSPACE=/Users/qingyunwu/Documents/github/sutando-workspace-001`), the pre-fix behavior was: 1. `WORKSPACE_DIR = process.env.WORKSPACE_DIR || <repo-path-via-import.meta.url>` - `WORKSPACE_DIR` env var was the *legacy* name (now `SUTANDO_WORKSPACE`). - Fallback resolved to the repo checkout, so voice-agent wrote tasks, results, conversation.log, briefings, the PIDFILE, audio dirs, etc. to the **repo**, while Sutando.app (post-#837), Python bridges (post-#762), and #842 inline-tools (just landed) all wrote/read from the **workspace**. 2. `CALL_RESULTS_DIR = <repo>/results/calls` — phone-call result records landed under the repo too. Net effect: split-brain on `SUTANDO_WORKSPACE`-enabled hosts. Phone-call `latest-result.json` lived under the repo; new tasks the voice-agent generated landed there too; bridges/Sutando.app couldn't see them. **Pattern (matches #821 / #836 / #842):** ```ts import { resolveWorkspace } from './workspace_default.js'; const WORKSPACE_DIR = resolveWorkspace(); ``` Module-init resolution, single source of truth. CALL_RESULTS_DIR becomes `join(WORKSPACE_DIR, 'results', 'calls')` — same workspace, just the subpath. **Doc-comment cleanup:** - Renamed env-doc at line 19 from `WORKSPACE_DIR` (legacy) to `SUTANDO_WORKSPACE` (canonical) with a pointer to workspace_default.ts. - Removed the stale "default to sutando/ so Claude Code subprocess picks up CLAUDE.md automatically" comment at line 96. Voice-agent doesn't spawn Claude Code anymore (`grep -n claude src/voice-agent.ts` is empty) — task-bridge handles that via the file pipeline. The dual-use rationale that conflated repo + workspace into one var is obsolete. **Verified:** - `npx tsc --noEmit` ✓ clean. - Diff is targeted: 1 file, 15+/-4. - All 15 existing call sites (lines 98, 142, 334, 366, 515, 547, 551, 553, 799, 985, 1110, 1111, 1113, 1299, 1302, 1387) keep their `WORKSPACE_DIR` reference — they automatically pick up the new resolution. No site-by-site edits required. **Operational note:** This change takes effect on the next voice-agent restart. Currently-running voice-agent processes keep their old paths until restart. No mid-flight disruption.
sonichi
pushed a commit
that referenced
this pull request
May 18, 2026
… cwd (#839) (#844) Round-3 audit followup — `personalPath()` and `sharedPersonalPath()` both defaulted the optional `workspace` parameter to `process.cwd()` when the caller didn't pass one. That's the same cwd-as-workspace antipattern the rest of the audit family is fixing: it produces the right path only when the caller's cwd happens to be the workspace dir. This PR changes the default to `resolveWorkspace()` (the canonical helper introduced in #821). Callers that DO pass a workspace are unchanged. **Why this is the last util_paths.ts piece of the audit:** Most callers in src/ already pass `WORKSPACE_DIR` explicitly after the #842 / #843 migration: - src/voice-agent.ts:345 — passes WORKSPACE_DIR (resolveWorkspace post-#843) - src/inline-tools.ts:764 (post-#842) — passes WORKSPACE_DIR (resolveWorkspace) What this fix catches is the still-non-passing callers: - src/voice-agent.ts:556, 583 — `personalPath('stand-identity.json')` with no workspace arg. Falls through to the default. Plus any future caller that forgets to pass workspace — the default is now sensible. **Verified:** - `npx tsc --noEmit` ✓ clean. - Diff is targeted: 1 file, 8+/-3.
This was referenced May 18, 2026
Merged
4 tasks done
liususan091219
pushed a commit
that referenced
this pull request
May 18, 2026
…loses #840 properly) Two stacked issues fixed in one shot. **Production bug:** Python writers migrated core-status.json to $SUTANDO_WORKSPACE in #836 (per CLAUDE.md workspace contract), but TS readers in `src/web-client.ts:readCoreStatus` and `src/inline-tools.ts: getCoreStatusTool` still read it from REPO_ROOT via `import.meta.url`- relative paths. Same silent-divergence shape as #821/#842/#843 fixed for other paths — voice-agent reading "core idle" while Python actually wrote "core working" elsewhere. Fix: both TS readers now `join(WORKSPACE_DIR, 'core-status.json')`. **Test flake (#840):** `tests/agent-state-endpoint.test.ts` and `tests/ get-core-status-tool.test.ts` both wrote to `<REPO_ROOT>/core-status.json` in their fixtures. node:test runs files in parallel by default; the get-core-status-tool test writes `status:'running'` as one of its cases right when agent-state-endpoint is asserting `state:'listening'`. Race manifested as `expected listening, got working` on whichever subtest happened to call effectiveAgentState() during the foreign 'running' write. Real fix is test isolation: each test process creates its own `mkdtempSync` workspace dir and exports `SUTANDO_WORKSPACE=$TEMP_DIR`. The spawned web-client inherits the env; the in-process tool import reads the same temp dir. No more shared state, no more race. PR #841's `beforeEach` reset stayed (still useful for tool-track sequencing within the single describe block), but #840 wasn't fully closed by that — this PR closes it for real. Verified: - agent-state-endpoint.test.ts: 9/9 pass - get-core-status-tool.test.ts: 6/6 pass - Running both together (the failure mode): 15/15 pass - Full TS suite: 173/173 pass Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
5 tasks
sonichi
added a commit
that referenced
this pull request
May 18, 2026
…loses #840 properly) (#849) Two stacked issues fixed in one shot. **Production bug:** Python writers migrated core-status.json to $SUTANDO_WORKSPACE in #836 (per CLAUDE.md workspace contract), but TS readers in `src/web-client.ts:readCoreStatus` and `src/inline-tools.ts: getCoreStatusTool` still read it from REPO_ROOT via `import.meta.url`- relative paths. Same silent-divergence shape as #821/#842/#843 fixed for other paths — voice-agent reading "core idle" while Python actually wrote "core working" elsewhere. Fix: both TS readers now `join(WORKSPACE_DIR, 'core-status.json')`. **Test flake (#840):** `tests/agent-state-endpoint.test.ts` and `tests/ get-core-status-tool.test.ts` both wrote to `<REPO_ROOT>/core-status.json` in their fixtures. node:test runs files in parallel by default; the get-core-status-tool test writes `status:'running'` as one of its cases right when agent-state-endpoint is asserting `state:'listening'`. Race manifested as `expected listening, got working` on whichever subtest happened to call effectiveAgentState() during the foreign 'running' write. Real fix is test isolation: each test process creates its own `mkdtempSync` workspace dir and exports `SUTANDO_WORKSPACE=$TEMP_DIR`. The spawned web-client inherits the env; the in-process tool import reads the same temp dir. No more shared state, no more race. PR #841's `beforeEach` reset stayed (still useful for tool-track sequencing within the single describe block), but #840 wasn't fully closed by that — this PR closes it for real. Verified: - agent-state-endpoint.test.ts: 9/9 pass - get-core-status-tool.test.ts: 6/6 pass - Running both together (the failure mode): 15/15 pass - Full TS suite: 173/173 pass Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 18, 2026
qingyun-wu
added a commit
that referenced
this pull request
May 18, 2026
Followup to #849 — same workspace-contract fix for `voice-state.json` that #849 just landed for `core-status.json`. Issue #853 filed explicitly to track this. **Sites migrated:** - `src/voice-agent.ts:853` (writer) — was `writeFileSync('voice-state.json', ...)` (cwd-relative; effectively REPO_ROOT when launched from there). Now `writeFileSync(join(WORKSPACE_DIR, 'voice-state.json'), ...)`. `WORKSPACE_DIR` is already in scope post-#843. - `src/web-client.ts:3137` (reader) — was `new URL('../voice-state.json', import.meta.url)` (REPO_ROOT-relative). Now `join(WORKSPACE_DIR, 'voice-state.json')`. `WORKSPACE_DIR` is already in scope post-#821. **Test isolation update (mirrors #849's pattern):** - `tests/agent-state-endpoint.test.ts` — `VOICE_STATE_PATH` was joined against REPO_ROOT (`dirname(fileURLToPath(import.meta.url))`). Now joined against the per-process `TEMP_WORKSPACE` introduced in #849. The 3 subtests at lines 192/211/230 that write/unlink the file pick up the new path automatically. - Dropped the `savedVoiceState` stash/restore — TEMP_WORKSPACE is fresh per test run + `rmSync`'d on teardown, so no prod-file leak is possible. - Removed now-unused imports: `readFileSync`, `existsSync`, `dirname`, `fileURLToPath`. Net: 32+/-23 in the test file. **Why this bug bites:** Pre-fix, voice-agent's writer used `writeFileSync('voice-state.json', ...)` — a cwd-relative path. On hosts where the bridge / voice-agent was launched from the repo, cwd ≈ REPO_ROOT and both sides aligned. But: - On `$SUTANDO_WORKSPACE`-set hosts, the writer kept writing to repo while the reader (post-#849, post-#842) read from $SUTANDO_WORKSPACE. - On any host where the launcher's cwd drifted from REPO_ROOT (IDE spawn, `npx tsx` from elsewhere), the writer landed in an unexpected location. `dm-result.py`'s voiceConnected query goes through web-client's `/sse-status` → `readVoiceState()` → now uniformly resolves through WORKSPACE_DIR. Same path for the writer. Split-brain closed. **Verified:** - `npx tsc --noEmit` ✓ clean. - Diff is targeted: 3 files, 29 insertions / 23 deletions. **Audit family — TS side**: this is the last known core-state file left at REPO_ROOT. After this lands, all workspace runtime data flows through `$SUTANDO_WORKSPACE` consistently across Python + TS + Swift writers and readers.
sonichi
added a commit
that referenced
this pull request
May 18, 2026
…) (#855) Followup to #849 — same workspace-contract fix for `voice-state.json` that #849 just landed for `core-status.json`. Issue #853 filed explicitly to track this. **Sites migrated:** - `src/voice-agent.ts:853` (writer) — was `writeFileSync('voice-state.json', ...)` (cwd-relative; effectively REPO_ROOT when launched from there). Now `writeFileSync(join(WORKSPACE_DIR, 'voice-state.json'), ...)`. `WORKSPACE_DIR` is already in scope post-#843. - `src/web-client.ts:3137` (reader) — was `new URL('../voice-state.json', import.meta.url)` (REPO_ROOT-relative). Now `join(WORKSPACE_DIR, 'voice-state.json')`. `WORKSPACE_DIR` is already in scope post-#821. **Test isolation update (mirrors #849's pattern):** - `tests/agent-state-endpoint.test.ts` — `VOICE_STATE_PATH` was joined against REPO_ROOT (`dirname(fileURLToPath(import.meta.url))`). Now joined against the per-process `TEMP_WORKSPACE` introduced in #849. The 3 subtests at lines 192/211/230 that write/unlink the file pick up the new path automatically. - Dropped the `savedVoiceState` stash/restore — TEMP_WORKSPACE is fresh per test run + `rmSync`'d on teardown, so no prod-file leak is possible. - Removed now-unused imports: `readFileSync`, `existsSync`, `dirname`, `fileURLToPath`. Net: 32+/-23 in the test file. **Why this bug bites:** Pre-fix, voice-agent's writer used `writeFileSync('voice-state.json', ...)` — a cwd-relative path. On hosts where the bridge / voice-agent was launched from the repo, cwd ≈ REPO_ROOT and both sides aligned. But: - On `$SUTANDO_WORKSPACE`-set hosts, the writer kept writing to repo while the reader (post-#849, post-#842) read from $SUTANDO_WORKSPACE. - On any host where the launcher's cwd drifted from REPO_ROOT (IDE spawn, `npx tsx` from elsewhere), the writer landed in an unexpected location. `dm-result.py`'s voiceConnected query goes through web-client's `/sse-status` → `readVoiceState()` → now uniformly resolves through WORKSPACE_DIR. Same path for the writer. Split-brain closed. **Verified:** - `npx tsc --noEmit` ✓ clean. - Diff is targeted: 3 files, 29 insertions / 23 deletions. **Audit family — TS side**: this is the last known core-state file left at REPO_ROOT. After this lands, all workspace runtime data flows through `$SUTANDO_WORKSPACE` consistently across Python + TS + Swift writers and readers. Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com>
qingyun-wu
pushed a commit
that referenced
this pull request
May 21, 2026
save_note resolved NOTES_DIR against the workspace (correct since #842), but never ensured the notes/ subdir exists. On a fresh install — or any workspace without a prior note — the first save_note failed with ENOENT and surfaced to the user as a generic "issue saving the file". Add mkdirSync(NOTES_DIR, { recursive: true }) before writeFileSync so the first save always succeeds. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219
pushed a commit
to liususan091219/sutando-fork
that referenced
this pull request
May 28, 2026
…ols sites (issue sonichi#839) (sonichi#842) Round-3 audit followup to sonichi#836/sonichi#833/sonichi#838 — first slice of the TS audit flagged in sonichi#839. Five bug sites across two files, all hitting the same class: code paths resolving workspace data against `process.env.WORKSPACE_DIR` (wrong env var name) or `process.cwd()` instead of `resolveWorkspace()`. **Sites fixed:** - `src/cartesia-tts.ts:29` (`getWorkspace`) — TTS audio output dir (`results/audio/<category>/<label>-<ts>.wav`). Pre-fix: `WORKSPACE_DIR` env (legacy name, not set) → `process.cwd()` fallback → audio written to whatever cwd the launcher had. Now resolves to `$SUTANDO_WORKSPACE`. - `src/inline-tools.ts:471, 472` (`cancel_task` tool) — `tasksDir` and `resultsDir` for the cancel-task lookup. The tool listed tasks from `<cwd>/tasks/` and wrote cancel results to `<cwd>/results/`. On hosts with `SUTANDO_WORKSPACE` set, the bridge writes tasks to the workspace but cancel_task was looking in the repo (or whatever cwd) — so voice- initiated cancels would never find an in-flight task and would silently write a stranded CANCEL_INSTRUCTION file. - `src/inline-tools.ts:775` (`show_view` tool) — `dynamic-content.json` for the menu-bar app's view toggle. Pre-fix the inline tool wrote to `<cwd>/dynamic-content.json` while Sutando.app (post-sonichi#837) reads from `$SUTANDO_WORKSPACE/dynamic-content.json`. Split-brain: voice toggle fired but the app never saw it on hosts with `SUTANDO_WORKSPACE`. - `src/inline-tools.ts:873` (`recent_context` tool) — voice-agent state context. Pre-fix the read path looked at `<cwd>/state/voice-session-context.json` but the writer (core via CLAUDE.md "Voice session context" section) writes to `$SUTANDO_WORKSPACE/state/`. Voice-agent's deictic-pronoun recovery ("what was the post?", "what's pending?") would return `{note: "no context recorded yet"}` even when the writer had populated the file. - `src/inline-tools.ts:772` (`NOTES_DIR`) — `sharedPersonalPath('notes', process.cwd())` passes cwd as the workspace argument to the helper. When `SUTANDO_PRIVATE_DIR` isn't set, the helper falls through to `join(workspace, 'notes')` which becomes `<cwd>/notes/` instead of `$SUTANDO_WORKSPACE/notes/`. read_note / search_notes silently look in the wrong place. **Pattern (matches sonichi#821):** ```ts import { resolveWorkspace } from './workspace_default.js'; const WORKSPACE_DIR = resolveWorkspace(); ``` Module-init resolution mirrors the Python `WORKSPACE_DIR = resolve_workspace()` canonical pattern. Single source of truth per module. **Not addressed in this PR (still in sonichi#839's backlog):** - `src/voice-agent.ts:97, 103` — `WORKSPACE_DIR`/`CALL_RESULTS_DIR`. The highest-traffic sites; defer to a focused review. - `src/util_paths.ts:26, 54` — the fallback inside the helper itself. Cleaner to fix every caller first, then either delete the fallback or switch it to `resolveWorkspace()` so the helper has a sensible default. - `src/inline-tools.ts:957, 1021` — `join(process.cwd(), 'skills')`. These are CODE-adjacent paths (skills ship with the code), so they need REPO_ROOT semantics, not WORKSPACE_DIR. Latent today because the launcher always cwd's to the repo, but worth a separate PR. - `src/mentra-bridge.ts:88` — file is currently untracked (WIP). **Verified:** - `npx tsc --noEmit` ✓ clean across the whole tree. - Diff is targeted: 2 files, 22+/-8. Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com>
liususan091219
pushed a commit
to liususan091219/sutando-fork
that referenced
this pull request
May 28, 2026
… followup) (sonichi#843) Migrates voice-agent.ts's `WORKSPACE_DIR` + `CALL_RESULTS_DIR` constants to `resolveWorkspace()` (the canonical TS helper introduced in sonichi#821). Two const definitions cover ~15 call sites — all of which use the path for per-user runtime state (tasks/, results/, state/, conversation.log, briefings, audio, dynamic-content, PIDFILE, etc.). **Why this matters:** On hosts with `SUTANDO_WORKSPACE` set (e.g. this machine, where `SUTANDO_WORKSPACE=/Users/qingyunwu/Documents/github/sutando-workspace-001`), the pre-fix behavior was: 1. `WORKSPACE_DIR = process.env.WORKSPACE_DIR || <repo-path-via-import.meta.url>` - `WORKSPACE_DIR` env var was the *legacy* name (now `SUTANDO_WORKSPACE`). - Fallback resolved to the repo checkout, so voice-agent wrote tasks, results, conversation.log, briefings, the PIDFILE, audio dirs, etc. to the **repo**, while Sutando.app (post-sonichi#837), Python bridges (post-sonichi#762), and sonichi#842 inline-tools (just landed) all wrote/read from the **workspace**. 2. `CALL_RESULTS_DIR = <repo>/results/calls` — phone-call result records landed under the repo too. Net effect: split-brain on `SUTANDO_WORKSPACE`-enabled hosts. Phone-call `latest-result.json` lived under the repo; new tasks the voice-agent generated landed there too; bridges/Sutando.app couldn't see them. **Pattern (matches sonichi#821 / sonichi#836 / sonichi#842):** ```ts import { resolveWorkspace } from './workspace_default.js'; const WORKSPACE_DIR = resolveWorkspace(); ``` Module-init resolution, single source of truth. CALL_RESULTS_DIR becomes `join(WORKSPACE_DIR, 'results', 'calls')` — same workspace, just the subpath. **Doc-comment cleanup:** - Renamed env-doc at line 19 from `WORKSPACE_DIR` (legacy) to `SUTANDO_WORKSPACE` (canonical) with a pointer to workspace_default.ts. - Removed the stale "default to sutando/ so Claude Code subprocess picks up CLAUDE.md automatically" comment at line 96. Voice-agent doesn't spawn Claude Code anymore (`grep -n claude src/voice-agent.ts` is empty) — task-bridge handles that via the file pipeline. The dual-use rationale that conflated repo + workspace into one var is obsolete. **Verified:** - `npx tsc --noEmit` ✓ clean. - Diff is targeted: 1 file, 15+/-4. - All 15 existing call sites (lines 98, 142, 334, 366, 515, 547, 551, 553, 799, 985, 1110, 1111, 1113, 1299, 1302, 1387) keep their `WORKSPACE_DIR` reference — they automatically pick up the new resolution. No site-by-site edits required. **Operational note:** This change takes effect on the next voice-agent restart. Currently-running voice-agent processes keep their old paths until restart. No mid-flight disruption.
liususan091219
pushed a commit
to liususan091219/sutando-fork
that referenced
this pull request
May 28, 2026
… cwd (sonichi#839) (sonichi#844) Round-3 audit followup — `personalPath()` and `sharedPersonalPath()` both defaulted the optional `workspace` parameter to `process.cwd()` when the caller didn't pass one. That's the same cwd-as-workspace antipattern the rest of the audit family is fixing: it produces the right path only when the caller's cwd happens to be the workspace dir. This PR changes the default to `resolveWorkspace()` (the canonical helper introduced in sonichi#821). Callers that DO pass a workspace are unchanged. **Why this is the last util_paths.ts piece of the audit:** Most callers in src/ already pass `WORKSPACE_DIR` explicitly after the sonichi#842 / sonichi#843 migration: - src/voice-agent.ts:345 — passes WORKSPACE_DIR (resolveWorkspace post-sonichi#843) - src/inline-tools.ts:764 (post-sonichi#842) — passes WORKSPACE_DIR (resolveWorkspace) What this fix catches is the still-non-passing callers: - src/voice-agent.ts:556, 583 — `personalPath('stand-identity.json')` with no workspace arg. Falls through to the default. Plus any future caller that forgets to pass workspace — the default is now sensible. **Verified:** - `npx tsc --noEmit` ✓ clean. - Diff is targeted: 1 file, 8+/-3.
liususan091219
pushed a commit
to liususan091219/sutando-fork
that referenced
this pull request
May 28, 2026
…loses sonichi#840 properly) (sonichi#849) Two stacked issues fixed in one shot. **Production bug:** Python writers migrated core-status.json to $SUTANDO_WORKSPACE in sonichi#836 (per CLAUDE.md workspace contract), but TS readers in `src/web-client.ts:readCoreStatus` and `src/inline-tools.ts: getCoreStatusTool` still read it from REPO_ROOT via `import.meta.url`- relative paths. Same silent-divergence shape as sonichi#821/sonichi#842/sonichi#843 fixed for other paths — voice-agent reading "core idle" while Python actually wrote "core working" elsewhere. Fix: both TS readers now `join(WORKSPACE_DIR, 'core-status.json')`. **Test flake (sonichi#840):** `tests/agent-state-endpoint.test.ts` and `tests/ get-core-status-tool.test.ts` both wrote to `<REPO_ROOT>/core-status.json` in their fixtures. node:test runs files in parallel by default; the get-core-status-tool test writes `status:'running'` as one of its cases right when agent-state-endpoint is asserting `state:'listening'`. Race manifested as `expected listening, got working` on whichever subtest happened to call effectiveAgentState() during the foreign 'running' write. Real fix is test isolation: each test process creates its own `mkdtempSync` workspace dir and exports `SUTANDO_WORKSPACE=$TEMP_DIR`. The spawned web-client inherits the env; the in-process tool import reads the same temp dir. No more shared state, no more race. PR sonichi#841's `beforeEach` reset stayed (still useful for tool-track sequencing within the single describe block), but sonichi#840 wasn't fully closed by that — this PR closes it for real. Verified: - agent-state-endpoint.test.ts: 9/9 pass - get-core-status-tool.test.ts: 6/6 pass - Running both together (the failure mode): 15/15 pass - Full TS suite: 173/173 pass Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
liususan091219
pushed a commit
to liususan091219/sutando-fork
that referenced
this pull request
May 28, 2026
…nichi#853) (sonichi#855) Followup to sonichi#849 — same workspace-contract fix for `voice-state.json` that sonichi#849 just landed for `core-status.json`. Issue sonichi#853 filed explicitly to track this. **Sites migrated:** - `src/voice-agent.ts:853` (writer) — was `writeFileSync('voice-state.json', ...)` (cwd-relative; effectively REPO_ROOT when launched from there). Now `writeFileSync(join(WORKSPACE_DIR, 'voice-state.json'), ...)`. `WORKSPACE_DIR` is already in scope post-sonichi#843. - `src/web-client.ts:3137` (reader) — was `new URL('../voice-state.json', import.meta.url)` (REPO_ROOT-relative). Now `join(WORKSPACE_DIR, 'voice-state.json')`. `WORKSPACE_DIR` is already in scope post-sonichi#821. **Test isolation update (mirrors sonichi#849's pattern):** - `tests/agent-state-endpoint.test.ts` — `VOICE_STATE_PATH` was joined against REPO_ROOT (`dirname(fileURLToPath(import.meta.url))`). Now joined against the per-process `TEMP_WORKSPACE` introduced in sonichi#849. The 3 subtests at lines 192/211/230 that write/unlink the file pick up the new path automatically. - Dropped the `savedVoiceState` stash/restore — TEMP_WORKSPACE is fresh per test run + `rmSync`'d on teardown, so no prod-file leak is possible. - Removed now-unused imports: `readFileSync`, `existsSync`, `dirname`, `fileURLToPath`. Net: 32+/-23 in the test file. **Why this bug bites:** Pre-fix, voice-agent's writer used `writeFileSync('voice-state.json', ...)` — a cwd-relative path. On hosts where the bridge / voice-agent was launched from the repo, cwd ≈ REPO_ROOT and both sides aligned. But: - On `$SUTANDO_WORKSPACE`-set hosts, the writer kept writing to repo while the reader (post-sonichi#849, post-sonichi#842) read from $SUTANDO_WORKSPACE. - On any host where the launcher's cwd drifted from REPO_ROOT (IDE spawn, `npx tsx` from elsewhere), the writer landed in an unexpected location. `dm-result.py`'s voiceConnected query goes through web-client's `/sse-status` → `readVoiceState()` → now uniformly resolves through WORKSPACE_DIR. Same path for the writer. Split-brain closed. **Verified:** - `npx tsc --noEmit` ✓ clean. - Diff is targeted: 3 files, 29 insertions / 23 deletions. **Audit family — TS side**: this is the last known core-state file left at REPO_ROOT. After this lands, all workspace runtime data flows through `$SUTANDO_WORKSPACE` consistently across Python + TS + Swift writers and readers. Co-authored-by: Chi Wang <4250911+sonichi@users.noreply.github.com>
liususan091219
pushed a commit
to liususan091219/sutando-fork
that referenced
this pull request
May 28, 2026
save_note resolved NOTES_DIR against the workspace (correct since sonichi#842), but never ensured the notes/ subdir exists. On a fresh install — or any workspace without a prior note — the first save_note failed with ENOENT and surfaced to the user as a generic "issue saving the file". Add mkdirSync(NOTES_DIR, { recursive: true }) before writeFileSync so the first save always succeeds. Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
First TS slice of the workspace audit followup filed as #839. 5 bug sites across 2 files, same class as #836 / #833 / #838 (Python side, all merged today): code paths resolving runtime data against
process.env.WORKSPACE_DIR(wrong env var name — should beSUTANDO_WORKSPACE) orprocess.cwd()instead of the canonicalresolveWorkspace()helper introduced in #821.Sites fixed
src/cartesia-tts.ts:29getWorkspaceWORKSPACE_DIRenv (legacy, not set) →process.cwd()fallback. TTS audio files (results/audio/<category>/<label>-<ts>.wav) written to whatever cwd the launcher had.src/inline-tools.ts:471, 472cancel_tasktasksDir/resultsDirlooked in<cwd>/tasks/. Voice-initiated cancels silently failed to find in-flight tasks on hosts withSUTANDO_WORKSPACEset — bridge writes to workspace, cancel_task was reading repo.src/inline-tools.ts:775show_viewdynamic-content.jsonwritten to<cwd>/. Sutando.app (post-#837) reads from\$SUTANDO_WORKSPACE/. Voice toggle fired but the app never saw it.src/inline-tools.ts:873recent_contextstate/voice-session-context.jsonlooked in<cwd>/state/. Core writer (CLAUDE.md "Voice session context") writes to\$SUTANDO_WORKSPACE/state/. Voice-agent's deictic-pronoun recovery returned "no context recorded yet" even when populated.src/inline-tools.ts:772NOTES_DIRsharedPersonalPath('notes', process.cwd())passed cwd as the workspace arg to the helper, so the fallback inside the helper resolved to<cwd>/notes/instead of\$SUTANDO_WORKSPACE/notes/. read_note / search_notes looked in the wrong place.Pattern (matches #821 + #836)
Module-init resolution. Single source of truth per module. Mirrors the Python
WORKSPACE_DIR = resolve_workspace()pattern from the Python side of the audit.Not addressed in this PR (still in #839's backlog)
src/voice-agent.ts:97, 103—WORKSPACE_DIR(the canonical voice-agent reference) +CALL_RESULTS_DIR. Highest-traffic sites, used across ~10 call sites in voice-agent.ts (PIDFILE, tasks/, results/, briefings, conversation.log). Worth a focused review — saved for the next PR.src/util_paths.ts:26, 54— theworkspace ?? process.cwd()fallback inside the helper. Cleaner to fix every caller first (this PR is one step toward that) and then either delete the fallback or switch it toresolveWorkspace()so the helper has a sensible default.src/inline-tools.ts:957, 1021—join(process.cwd(), 'skills'). These are CODE-adjacent paths (skills ship with the code), so they need REPO_ROOT semantics, not WORKSPACE_DIR. Latent today (the launcher always cwd's to the repo) but worth fixing.src/mentra-bridge.ts:88— file is currently untracked (WIP per the commit history).Test plan
npx tsc --noEmit— clean.Followups
After this lands, 3 of the 4 followup buckets in #839 remain (voice-agent core, util_paths fallback, skills-path REPO_ROOT semantics). Once all are addressed, #839 can close.
Audit running total: 18 bug sites across 10 files (#815 + #836 + #833 + #838 + this PR), in the workspace-vs-repo class.