stop building cwds out of an email address

host found a live regression from 95951fb, on the owner's own paths.

resolveBaseCwd used to take an email and resolve its own root. 95951fb made the
first parameter the home itself, and three callers outside that diff kept
passing an email: pipeline-executor twice and agent-handoff once. Both
parameters are string, so tsgo had nothing to say. Every pipeline step and
handoff with a relative cwd, a `~`, or no cwd was building a path out of an
address, resolving it against the platform's own working directory — the
checkout. Absolute paths kept working, which is what would have made it look
intermittent.

All three are owner-only, so they now pass getOwnerHomeDir(email) explicitly,
the way agent-runner does. The definition of resolveBaseCwd carries the warning:
an absolute path, NOT an email, with the reason.

Not done: the branded type this argues for. Two strings meaning "identity" and
"filesystem path" sat adjacent through a refactor and the compiler could not
help, which is a real gap — but it reaches every path function in the server,
and doing it at 01:00 on the back of a bug caused by a hasty refactor would be
the joke telling itself.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-12 00:51:09 +00:00
co-authored by Claude Opus 5
parent 2bb8128619
commit 1575df3f78
4 changed files with 50 additions and 4 deletions
+2 -1
View File
@@ -2,6 +2,7 @@ import { getUserById, markAgentPanelIntroduced, type AgentPanel } from 'officerd
import * as sidecar from '@@/sidecar-registry';
import { resolveBaseCwd } from '../chat/websocket';
import { logger } from '../chat/logger';
import { getOwnerHomeDir } from '@@/data-path';
/**
* Push a turn into an agent panel's session — with or without a browser attached.
@@ -33,7 +34,7 @@ export async function deliverToAgentPanel(target: AgentPanel, prompt: string): P
username: user.name ?? user.email,
prompt,
sessionKey: target.sessionKey,
cwd: resolveBaseCwd(user.email, target.cwd ?? undefined),
cwd: resolveBaseCwd(getOwnerHomeDir(user.email), target.cwd ?? undefined),
// Deliberately no `model`: a live session ignores it anyway, and a resumed one keeps whatever the
// panel started with. Passing one here would only look like it worked.
durable: true,
+8
View File
@@ -101,6 +101,14 @@ const resolveCwd = (home: string, cwd?: string) => {
return join(home, cwd);
};
/**
* @param home an absolute filesystem path — NOT an email.
*
* It took an email until `95951fb`, resolved its own root, and both parameters are `string`, so the change of
* meaning was invisible to the compiler and to every caller outside that diff. Three of them kept passing an
* email and silently began building relative paths out of an address. If a fourth caller ever appears, this
* line is the warning it gets.
*/
export const resolveBaseCwd = (home: string, cwd?: string) => resolveCwd(home, cwd);
// The email chat runs from the selected account's storage dir:
+3 -3
View File
@@ -5,7 +5,7 @@ import { join } from 'node:path';
import { tmpdir } from 'node:os';
import { getUserSettings } from 'officerdb';
import { getTaskByDirName } from './task-files';
import { getHomeDir } from '../../data-path';
import { getHomeDir, getOwnerHomeDir } from '../../data-path';
import { resolveBaseCwd } from '../chat/websocket';
import { sendClaudeCodeStreaming } from '../../channels/send-claude-code';
import type { TurnMessage, MessageCost } from '../chat/types';
@@ -496,7 +496,7 @@ async function runForeach({
}
const cwdRelative = cwd ? `${cwd}/${subdir}` : subdir;
const resolvedCwd = resolveBaseCwd(email, cwdRelative);
const resolvedCwd = resolveBaseCwd(getOwnerHomeDir(email), cwdRelative);
const targetDir = resolvedCwd;
const prompt = buildStepPrompt(stepTask.body!, iterInputs, targetDir);
@@ -588,7 +588,7 @@ export async function executePipeline({
return;
}
const baseCwd = resolveBaseCwd(email, cwd);
const baseCwd = resolveBaseCwd(getOwnerHomeDir(email), cwd);
let model = modelOverride || (await resolveModel(userId));
// Claude-only: coerce any legacy non-Claude task-model preference to the Claude default.
if (!model.startsWith('claude-code')) model = DEFAULT_MODEL;