summaryrefslogtreecommitdiffhomepage
path: root/packages/session-orchestrator/src
diff options
context:
space:
mode:
authorAdam Malczewski <[email protected]>2026-06-25 21:45:58 +0900
committerAdam Malczewski <[email protected]>2026-06-25 21:45:58 +0900
commit2cc9ddfb590dc60557bba3ed76a6c4639df5f596 (patch)
treeb0ced1ecb5f899e6a2b835d41603c4040a49bbce /packages/session-orchestrator/src
parent087ce142247637bb10351ab7815144b720836153 (diff)
downloaddispatch-2cc9ddfb590dc60557bba3ed76a6c4639df5f596.tar.gz
dispatch-2cc9ddfb590dc60557bba3ed76a6c4639df5f596.zip
feat(ssh): discover computers from ~/.ssh/known_hosts + remote system-prompt
Two improvements to the SSH support feature: 1. KNOWN_HOSTS DISCOVERY (packages/ssh): Computers are now auto-discovered from ~/.ssh/known_hosts (every hostname you've ever connected to) in ADDITION to ~/.ssh/config (explicit Host aliases). Config entries take precedence (full params); known_hosts entries get defaulted params (User=defaultUser, IdentityFile=null→pool probes default keys, Port from [host]:port or 22, knownHost=true). Zero-config — no ~/.ssh/config file needed; hosts just appear. Reject list: dispatch.toml [ssh].reject = [...] (glob patterns like github.com, *.ts.net) filters noise from the catalog. Read from both the global ~/.config/dispatch/dispatch.toml and the project dispatch.toml. Parsed with Bun.TOML.parse (zero deps). Only filters discovery (catalog); specific lookups (getComputer/getStatus/test/connect) ignore the reject list (it's a visibility filter, not access control). New pure functions: parseKnownHosts(), isRejected(), globMatch(). +26 tests. tsc EXIT 0, biome clean, 1756 tests pass. 2. REMOTE SYSTEM-PROMPT AWARENESS (packages/system-prompt): When a conversation has a computerId set (remote turn), the system prompt now resolves system:os, system:hostname, git:branch/git:status, and file: reads against the REMOTE machine — not the local host. Previously the prompt always said 'Arch Linux (WSL)' + local hostname even when the agent was connected to a remote Artix Linux machine. The ResolverAdapters' hostname()/platform() are now async (so a remote adapter can run 'hostname'/'uname -s' over SSH). The system-prompt extension builds remote adapters from the ExecBackend (readFile→SFTP, spawn→SSH exec). Cache invalidation now checks computerId (switching computers rebuilds the prompt). The compaction path also threads computerId. @dispatch/system-prompt now depends on @dispatch/exec-backend.
Diffstat (limited to 'packages/session-orchestrator/src')
-rw-r--r--packages/session-orchestrator/src/orchestrator.test.ts19
-rw-r--r--packages/session-orchestrator/src/orchestrator.ts13
2 files changed, 22 insertions, 10 deletions
diff --git a/packages/session-orchestrator/src/orchestrator.test.ts b/packages/session-orchestrator/src/orchestrator.test.ts
index 8ff3f5e..654c0c3 100644
--- a/packages/session-orchestrator/src/orchestrator.test.ts
+++ b/packages/session-orchestrator/src/orchestrator.test.ts
@@ -3611,12 +3611,13 @@ function createFakeSystemPromptService(
constructImpl: (
conversationId: string,
cwd: string,
- context?: { readonly model?: string },
+ context?: { readonly model?: string; readonly computerId?: string },
) => Promise<string>,
- getWithMetaImpl: (
- conversationId: string,
- ) => Promise<{ readonly prompt: string | null; readonly cwd: string | null }> = () =>
- Promise.resolve({ prompt: null, cwd: null }),
+ getWithMetaImpl: (conversationId: string) => Promise<{
+ readonly prompt: string | null;
+ readonly cwd: string | null;
+ readonly computerId: string | null;
+ }> = () => Promise.resolve({ prompt: null, cwd: null, computerId: null }),
): SystemPromptService {
return {
construct: constructImpl,
@@ -3663,7 +3664,7 @@ describe("system prompt: regular turn flow", () => {
},
async (conversationId) => {
getCalls.push(conversationId);
- return null;
+ return { prompt: null, cwd: null, computerId: null };
},
),
});
@@ -3714,7 +3715,7 @@ describe("system prompt: regular turn flow", () => {
},
async (conversationId) => {
getWithMetaCalls.push(conversationId);
- return { prompt: "PERSISTED_PROMPT", cwd: "/work/dir" };
+ return { prompt: "PERSISTED_PROMPT", cwd: "/work/dir", computerId: null };
},
),
});
@@ -3762,7 +3763,7 @@ describe("system prompt: regular turn flow", () => {
constructCalls.push({ conversationId, cwd, model: context?.model });
return "RECONSTRUCTED_PROMPT";
},
- async () => ({ prompt: null, cwd: null }),
+ async () => ({ prompt: null, cwd: null, computerId: null }),
),
});
@@ -3815,7 +3816,7 @@ describe("system prompt: regular turn flow", () => {
async (conversationId) => {
getWithMetaCalls.push(conversationId);
// Stored prompt was built against an OLD cwd.
- return { prompt: "STALE_PROMPT", cwd: "/old/dir" };
+ return { prompt: "STALE_PROMPT", cwd: "/old/dir", computerId: null };
},
),
});
diff --git a/packages/session-orchestrator/src/orchestrator.ts b/packages/session-orchestrator/src/orchestrator.ts
index a1401d6..4aa77f7 100644
--- a/packages/session-orchestrator/src/orchestrator.ts
+++ b/packages/session-orchestrator/src/orchestrator.ts
@@ -619,17 +619,26 @@ export function createSessionOrchestrator(
{
...(effectiveModelName !== undefined ? { model: effectiveModelName } : {}),
...(workspaceId !== undefined ? { workspaceId } : {}),
+ ...(effectiveComputerId !== undefined ? { computerId: effectiveComputerId } : {}),
},
);
} else {
const meta = await systemPromptService.getWithMeta(conversationId);
const currentCwd = effectiveCwd ?? process.cwd();
- if (meta.prompt !== null && meta.cwd === currentCwd) {
+ const currentComputerId = effectiveComputerId ?? null;
+ // Invalidate when cwd OR computerId changed (switching computers
+ // must rebuild the prompt against the remote OS/hostname).
+ if (
+ meta.prompt !== null &&
+ meta.cwd === currentCwd &&
+ meta.computerId === currentComputerId
+ ) {
systemPrompt = meta.prompt;
} else {
systemPrompt = await systemPromptService.construct(conversationId, currentCwd, {
...(effectiveModelName !== undefined ? { model: effectiveModelName } : {}),
...(workspaceId !== undefined ? { workspaceId } : {}),
+ ...(effectiveComputerId !== undefined ? { computerId: effectiveComputerId } : {}),
});
}
}
@@ -1122,9 +1131,11 @@ export function createCompactionService(
if (systemPromptService !== undefined) {
const cwd = (await deps.conversationStore.getEffectiveCwd(conversationId)) ?? process.cwd();
const workspaceId = await deps.conversationStore.getWorkspaceId(conversationId);
+ const computerId = await deps.conversationStore.getEffectiveComputer(conversationId);
const constructed = await systemPromptService.construct(conversationId, cwd, {
...(opts?.modelName !== undefined ? { model: opts.modelName } : {}),
workspaceId,
+ ...(computerId !== null ? { computerId } : {}),
});
compactionSystemPrompt = `${constructed}\n\n${COMPACTION_SYSTEM_PROMPT}`;
} else {