@runuai/host 0.9.94 → 0.9.95
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/engine-accounts.ts +40 -0
- package/lib/orchestrator.ts +63 -1
- package/package.json +1 -1
package/lib/engine-accounts.ts
CHANGED
|
@@ -345,6 +345,46 @@ export function pickEngineAccount(
|
|
|
345
345
|
return selectAccount(accounts, { exclude: opts.exclude, now: opts.now });
|
|
346
346
|
}
|
|
347
347
|
|
|
348
|
+
/**
|
|
349
|
+
* ADR-076 + codex-cli 0.152.0: two codex agents sharing one account share one
|
|
350
|
+
* CODEX_HOME, and codex's sqlite state runtime cannot be opened by two
|
|
351
|
+
* concurrent processes. These pure helpers give each codex agent its own
|
|
352
|
+
* home (a copy of the account's) — the orchestrator wraps them with the
|
|
353
|
+
* container exec. Only codex config-dir homes under /home/node/.codex are
|
|
354
|
+
* isolated; ambient/env accounts and other kinds are left untouched.
|
|
355
|
+
*/
|
|
356
|
+
export function codexHomeIsolatable(
|
|
357
|
+
kind: string,
|
|
358
|
+
codexHome: string | undefined,
|
|
359
|
+
): codexHome is string {
|
|
360
|
+
return (
|
|
361
|
+
kind === "codex" &&
|
|
362
|
+
typeof codexHome === "string" &&
|
|
363
|
+
codexHome.startsWith("/home/node/.codex")
|
|
364
|
+
);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
/** The per-agent home for a shared account home. Agent id is sanitized so it
|
|
368
|
+
* is always a safe path segment. */
|
|
369
|
+
export function codexPerAgentHome(codexHome: string, agentId: string): string {
|
|
370
|
+
const safe = agentId.replace(/[^A-Za-z0-9_-]/g, "_");
|
|
371
|
+
return `${codexHome}-agent-${safe}`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** The idempotent seed command: copy the account home into the per-agent home
|
|
375
|
+
* ONCE (only when the per-agent home is still empty), node-owned. */
|
|
376
|
+
export function codexHomeSeedCommand(
|
|
377
|
+
codexHome: string,
|
|
378
|
+
perAgentHome: string,
|
|
379
|
+
): string {
|
|
380
|
+
return (
|
|
381
|
+
`set -e; mkdir -p "${perAgentHome}"; ` +
|
|
382
|
+
`if [ -z "$(ls -A "${perAgentHome}" 2>/dev/null)" ]; then ` +
|
|
383
|
+
`cp -a "${codexHome}/." "${perAgentHome}/" 2>/dev/null || true; fi; ` +
|
|
384
|
+
`chown -R node:node "${perAgentHome}"`
|
|
385
|
+
);
|
|
386
|
+
}
|
|
387
|
+
|
|
348
388
|
/** Test hook: clear in-memory rotation state. */
|
|
349
389
|
export function resetRotationState(): void {
|
|
350
390
|
rotationState.clear();
|
package/lib/orchestrator.ts
CHANGED
|
@@ -111,6 +111,9 @@ import {
|
|
|
111
111
|
pickEngineAccount,
|
|
112
112
|
provisionEngineAccounts,
|
|
113
113
|
resolveEngineAccounts,
|
|
114
|
+
codexHomeIsolatable,
|
|
115
|
+
codexPerAgentHome,
|
|
116
|
+
codexHomeSeedCommand,
|
|
114
117
|
type ResolvedEngineAccount,
|
|
115
118
|
} from "./engine-accounts";
|
|
116
119
|
import { clearTaskGatewayAcl, setupMcpTaskConfig } from "./mcp-gateway";
|
|
@@ -1612,15 +1615,74 @@ export class Orchestrator {
|
|
|
1612
1615
|
return operation;
|
|
1613
1616
|
}
|
|
1614
1617
|
|
|
1618
|
+
/**
|
|
1619
|
+
* Per-agent CODEX_HOME (see the call site). Returns `args` unchanged unless
|
|
1620
|
+
* this is a codex agent with a config-dir CODEX_HOME; then it seeds
|
|
1621
|
+
* `<home>-agent-<agentId>` from `<home>` once (in-container cp as root,
|
|
1622
|
+
* node-owned) and points this agent's env there. Best-effort: a seed
|
|
1623
|
+
* failure logs and falls back to the shared home rather than blocking the
|
|
1624
|
+
* session — one shared home is the pre-fix behavior, not a regression.
|
|
1625
|
+
*/
|
|
1626
|
+
private async isolateCodexHome(
|
|
1627
|
+
environment: TaskEnvironmentHandle<unknown>,
|
|
1628
|
+
agentId: string,
|
|
1629
|
+
args: Omit<Parameters<AgentSessionFactory["create"]>[0], "environment">,
|
|
1630
|
+
): Promise<Omit<Parameters<AgentSessionFactory["create"]>[0], "environment">> {
|
|
1631
|
+
const home = args.agentEnv?.CODEX_HOME;
|
|
1632
|
+
if (!codexHomeIsolatable(args.agent?.kind ?? "", home)) return args;
|
|
1633
|
+
const perAgent = codexPerAgentHome(home, agentId);
|
|
1634
|
+
try {
|
|
1635
|
+
const res = await environment.exec({
|
|
1636
|
+
argv: ["/bin/sh", "-c", codexHomeSeedCommand(home, perAgent)],
|
|
1637
|
+
user: "root",
|
|
1638
|
+
env: RUNTIME_AUTHORITY_ENV,
|
|
1639
|
+
timeoutMs: 30_000,
|
|
1640
|
+
maxOutputBytes: 8 * 1024,
|
|
1641
|
+
});
|
|
1642
|
+
if (res.exitCode !== 0) {
|
|
1643
|
+
console.warn(
|
|
1644
|
+
`[codex] ${agentId}: could not isolate CODEX_HOME (exit ${res.exitCode}); using shared home`,
|
|
1645
|
+
);
|
|
1646
|
+
return args;
|
|
1647
|
+
}
|
|
1648
|
+
} catch (err) {
|
|
1649
|
+
console.warn(
|
|
1650
|
+
`[codex] ${agentId}: CODEX_HOME isolation errored (${
|
|
1651
|
+
err instanceof Error ? err.message : String(err)
|
|
1652
|
+
}); using shared home`,
|
|
1653
|
+
);
|
|
1654
|
+
return args;
|
|
1655
|
+
}
|
|
1656
|
+
return {
|
|
1657
|
+
...args,
|
|
1658
|
+
agentEnv: { ...(args.agentEnv ?? {}), CODEX_HOME: perAgent },
|
|
1659
|
+
};
|
|
1660
|
+
}
|
|
1661
|
+
|
|
1615
1662
|
private async createAndBindStableSessionCore(
|
|
1616
1663
|
channel: Channel,
|
|
1617
1664
|
agentId: string,
|
|
1618
1665
|
args: Omit<Parameters<AgentSessionFactory["create"]>[0], "environment">,
|
|
1619
1666
|
): Promise<AgentSession | null> {
|
|
1620
1667
|
const environment = await this.agentEnvironment(channel);
|
|
1668
|
+
// ADR-076 + codex-cli 0.152.0: two codex agents that resolve to the SAME
|
|
1669
|
+
// account share one CODEX_HOME, and codex's new sqlite state runtime
|
|
1670
|
+
// cannot be opened by two concurrent processes — the second agent dies
|
|
1671
|
+
// with "failed to initialize sqlite state runtime under <CODEX_HOME>"
|
|
1672
|
+
// (live 2026-09-04, a task with two codex agents on one account). Give
|
|
1673
|
+
// each codex agent its OWN CODEX_HOME, seeded once from the resolved
|
|
1674
|
+
// account's dir, so their state runtimes never collide. Single-codex
|
|
1675
|
+
// tasks are unaffected beyond a one-time in-container copy.
|
|
1676
|
+
// Only codex agents take the isolation path; everything else uses args
|
|
1677
|
+
// directly, WITHOUT an extra await — a stray microtask here reordered the
|
|
1678
|
+
// Claude re-spawn's prompt re-delivery.
|
|
1679
|
+
const boundArgs =
|
|
1680
|
+
args.agent?.kind === "codex"
|
|
1681
|
+
? await this.isolateCodexHome(environment, agentId, args)
|
|
1682
|
+
: args;
|
|
1621
1683
|
while (this.isActiveChannel(channel)) {
|
|
1622
1684
|
const generation = channel.configGeneration;
|
|
1623
|
-
const session = await this.factory.create({ ...
|
|
1685
|
+
const session = await this.factory.create({ ...boundArgs, environment });
|
|
1624
1686
|
if (!this.isActiveChannel(channel)) {
|
|
1625
1687
|
await session.close().catch(() => {});
|
|
1626
1688
|
return null;
|