The envelope guard added in #311 rejects a workspace passed by name, and the rejection offers that same workspace back as its closest match:
Invalid workspace "Desenvolvedor". Closest match: "Desenvolvedor". Use one of these
exact values — do not infer a workspace name from the user's wording.
Available: "default" (global), "Desenvolvedor", "Dev", "Reflexao", ...
Hit on the first call of a session on 5.16.2, running the same vault as #214 (13,432 notes, 8 workspaces).
Repro
Any useTools envelope carrying a real workspace name, on a vault whose boot-time snapshot was empty:
useTools { workspaceId: "Desenvolvedor", tool: "task list-projects --status active" }
→ Invalid workspace "Desenvolvedor". Closest match: "Desenvolvedor". …
useTools { workspaceId: "a8fbad11-7412-49c8-bce0-5690e2c1d197", tool: "task list-projects --status active" }
→ 5 projects
Same workspace, both forms. The UUID passes, the name does not — which isolates the acceptance path as the only difference.
The two halves of the mandatory pair now contradict each other
getTools is a required first step, and on 5.16.2 it grounds the caller by returning the live list in the RESULT — the whole point of #311:
workspaces: ["default", "Default Workspace", "Desenvolvedor", "Dev", …]
workspacesNote: "These are the only workspaces that exist. Pass one of these exact
names — do not infer a workspace name from the user's wording."
useTools then receives one of those exact names and answers with "do not infer a workspace name from the user's wording". An agent that follows the grounding instruction perfectly is told it invented the name.
Cause
ToolBatchExecutionService.validateWorkspaceId checks the name and the id against different lists:
const byName = this.knownWorkspaces.find(w => w.name.toLowerCase() === workspaceId.toLowerCase());
if (byName) return null; // boot-time snapshot
const workspaces = await workspaceService.listWorkspaces();
const byUuid = workspaces.find(w => w.id === workspaceId);
if (byUuid) return null; // live list — id only
The name is only ever tested against knownWorkspaces. That is the same boot-time snapshot #311 deliberately stopped trusting three lines further down:
Name the alternatives off the LIVE list, not knownWorkspaces — that snapshot is taken at boot and is empty whenever SQLite was not ready then
So the error message reads from the live list and the acceptance does not. On exactly the vaults where the snapshot is empty — the ones #311 was written for — every by-name call is rejected, and matchWorkspaces then finds the real workspace and prints it as the suggestion. The message is self-refuting by construction: it can only name a closest match that came from the live list it never checked for acceptance.
Fix
Recheck the name against the live list alongside the id — 13 lines, one file, no behavior change for identifiers that resolve to nothing.
const byLiveName = workspaces.find(w => w.name.toLowerCase() === workspaceId.toLowerCase());
if (byLiveName) return null;
Coverage
tests/unit/EnvelopeWorkspaceValidation.test.ts, 5 cases, RED verified before the fix — 3 failing, and notably the id case and the legitimate rejection (--workspaceId) already passed, which is what pins the defect to the by-name path rather than to the guard as a whole.
The fifth is a self-contradiction lock rather than another hand-picked case: for every live workspace, assert the error never contains Closest match: "<that same name>". Any future divergence between the list the guard accepts from and the list it suggests from fails there, whichever direction it drifts.
- Suite: 4253 pass / 3 fail — the 3 reproduce identically on clean
5.16.2 (two are toLocaleString under a pt-BR locale, one is LocalCliInstaller).
tsc --noEmit, eslint ., npm run build clean.
Note on severity
validateWorkspaceId fails open, so this is bounded — but it fails closed here, because the plugin and workspaceService do resolve. The guard works; only the acceptance list is wrong. On a vault with named workspaces this makes every by-name envelope unusable until callers are rewritten to pass UUIDs, which is the opposite of the grounding #311 introduced.
Happy to open a PR if you want it; not opening a cold one.
Measured on macOS 15.6, Nexus 5.16.2 stock (main.js 4,288,180 bytes), vault of 13,432 notes.
The envelope guard added in #311 rejects a workspace passed by name, and the rejection offers that same workspace back as its closest match:
Hit on the first call of a session on 5.16.2, running the same vault as #214 (13,432 notes, 8 workspaces).
Repro
Any
useToolsenvelope carrying a real workspace name, on a vault whose boot-time snapshot was empty:Same workspace, both forms. The UUID passes, the name does not — which isolates the acceptance path as the only difference.
The two halves of the mandatory pair now contradict each other
getToolsis a required first step, and on 5.16.2 it grounds the caller by returning the live list in the RESULT — the whole point of #311:useToolsthen receives one of those exact names and answers with "do not infer a workspace name from the user's wording". An agent that follows the grounding instruction perfectly is told it invented the name.Cause
ToolBatchExecutionService.validateWorkspaceIdchecks the name and the id against different lists:The name is only ever tested against
knownWorkspaces. That is the same boot-time snapshot #311 deliberately stopped trusting three lines further down:So the error message reads from the live list and the acceptance does not. On exactly the vaults where the snapshot is empty — the ones #311 was written for — every by-name call is rejected, and
matchWorkspacesthen finds the real workspace and prints it as the suggestion. The message is self-refuting by construction: it can only name a closest match that came from the live list it never checked for acceptance.Fix
Recheck the name against the live list alongside the id — 13 lines, one file, no behavior change for identifiers that resolve to nothing.
5.16.2)Coverage
tests/unit/EnvelopeWorkspaceValidation.test.ts, 5 cases, RED verified before the fix — 3 failing, and notably the id case and the legitimate rejection (--workspaceId) already passed, which is what pins the defect to the by-name path rather than to the guard as a whole.The fifth is a self-contradiction lock rather than another hand-picked case: for every live workspace, assert the error never contains
Closest match: "<that same name>". Any future divergence between the list the guard accepts from and the list it suggests from fails there, whichever direction it drifts.5.16.2(two aretoLocaleStringunder a pt-BR locale, one isLocalCliInstaller).tsc --noEmit,eslint .,npm run buildclean.Note on severity
validateWorkspaceIdfails open, so this is bounded — but it fails closed here, because the plugin andworkspaceServicedo resolve. The guard works; only the acceptance list is wrong. On a vault with named workspaces this makes every by-name envelope unusable until callers are rewritten to pass UUIDs, which is the opposite of the grounding #311 introduced.Happy to open a PR if you want it; not opening a cold one.
Measured on macOS 15.6, Nexus 5.16.2 stock (
main.js4,288,180 bytes), vault of 13,432 notes.