a commander runs where the owner chose, and Command HQ stops scanning one folder
Two of the three things the identity change unblocked. THE WORKING DIRECTORY. `resolveChatCwd` forced `getCommandersDir(email)` and ignored `msg.cwd` entirely — every commander shared one empty directory whose only purpose was to BE a cwd, because the cwd was how a commander was recognised. It honours the choice now, resolved through the same `resolveCwd(home, …)` containment the ordinary chat branch uses, so "client-chosen" still cannot mean "anywhere outside the account's tree". No fallback to the old shared folder: the UI will require a directory, and an absent one falls back to the account's home, which is the honest last resort rather than a silent special case. THE LIST. Command HQ found its commanders by scanning that one directory, which would now find only the ones that happen to still live there and silently hide every other. It reads its rows and loads each transcript by id — `loadClaudeSessionSummaryById`, added here — a handful of lookups rather than a directory walk. A row whose transcript is nowhere is treated as absent exactly as a missing file was. `POST /purge` had the same scan and it was the dangerous one: unfiling every commander that chose a different directory, from a housekeeping route, when the rows are what identity now depends on. Three smaller corrections that came with it: - rename and delete passed the commanders directory as their cwd argument. It is documented as a HINT that falls back to locating the transcript anywhere, so they worked — but passing a path we know is usually wrong reads as intent later. They pass `who.home` now. - `POST /command-hq/commanders/new` returned `cwd: getCommandersDir(...)` regardless of where the commander actually ran, and now accepts `cwd` and `name` like the UI will, and reports what it was given. - the router's "Where commanders live" section said every commander shares one directory and that listing is therefore a scan. Both sentences were the design being replaced. `loadClaudeSessionSummaryById` takes its mtime from `listTranscriptsAs` rather than a bare stat: the file may belong to a member, and reading a member's tree needs the as-user path. That is one scoped directory listing per commander — the same call the old whole-folder scan made, once per row instead of once for all, which is the price of letting each choose where it lives. Verified on live data: the existing commander still resolves, with its title and its real updatedAt. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { basename, dirname, join, resolve } from 'node:path';
|
||||
import { DATA_PATH, getCommandersDir } from '../../data-path';
|
||||
import {
|
||||
appendTextAs,
|
||||
@@ -1033,6 +1033,36 @@ export function isCommandersDir(email: string, cwd: string): boolean {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* One session's list-shaped summary, found by id wherever it lives.
|
||||
*
|
||||
* For callers that hold a set of ids rather than a directory — Command HQ, which reads its commanders from
|
||||
* `command_hq_sessions` and cannot scan for them any more now that each chooses its own working directory.
|
||||
*
|
||||
* Returns null when no transcript can be found, which is the same signal a directory scan gave by omission:
|
||||
* a filed row whose conversation has been deleted. `purgeCommandHqSessions` is what tidies those.
|
||||
*
|
||||
* Deliberately NOT chain-aware in the way `listClaudeSessions` is. It reports the transcript addressed by
|
||||
* this id; a `/clear` chain's head is what gets filed and what every surface addresses, so the tail is not
|
||||
* something a caller here holds an id for.
|
||||
*/
|
||||
export function loadClaudeSessionSummaryById(who: ChatIdentity, sessionId: string): ClaudeSessionSummary | null {
|
||||
const filePath = locateTranscript(who, sessionId);
|
||||
if (!filePath) return null;
|
||||
|
||||
// `mtimeMs` becomes `updatedAt`, which is what every caller sorts on — so it has to be real, not zero.
|
||||
// Taken from `listTranscriptsAs` over the transcript's OWN directory rather than a bare `statSync`: the
|
||||
// file may belong to a member, and reading a member's tree needs the as-user path. One directory listing
|
||||
// per commander, scoped to one group — the same call the old whole-directory scan made, once per row
|
||||
// instead of once for all of them, which is the cost of letting each choose where it lives.
|
||||
const group = basename(dirname(filePath));
|
||||
const entry = listTranscriptsAs(who.osUser, claudeProjectsDir(who.home), group).find((f) => f.id === sessionId);
|
||||
if (!entry) return null;
|
||||
|
||||
const summary = summarizeTranscript(who.osUser, filePath, sessionId, entry.mtimeMs);
|
||||
return summary ? publish(summary) : null;
|
||||
}
|
||||
|
||||
/**
|
||||
* Every conversation in every directory, newest first.
|
||||
*
|
||||
|
||||
Reference in New Issue
Block a user