super-dsh 0.1.6 → 0.1.10
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/README.md +25 -3
- package/agent-agy/cordis.patch.yml +7 -4
- package/agent-agy/dist/agent.d.ts +12 -11
- package/agent-agy/dist/agy-auth-gateway.d.ts +124 -0
- package/agent-agy/dist/agy-auth-gateway.js +233 -0
- package/agent-agy/dist/agy-auth.d.ts +153 -0
- package/agent-agy/dist/agy-auth.js +179 -0
- package/agent-agy/dist/agy-cli-client.d.ts +39 -9
- package/agent-agy/dist/agy-cli-client.js +82 -18
- package/agent-agy/dist/agy-store.d.ts +8 -0
- package/agent-agy/dist/index.d.ts +46 -16
- package/agent-agy/dist/index.js +82 -56
- package/agent-agy/dist/models.d.ts +70 -0
- package/agent-agy/dist/models.js +102 -3
- package/agent-agy/package.json +7 -5
- package/agent-agy-sdk/cordis.patch.yml +58 -0
- package/agent-agy-sdk/dist/adapter.d.ts +15 -0
- package/agent-agy-sdk/dist/adapter.js +42 -0
- package/agent-agy-sdk/dist/agent-preset-agy.d.ts +27 -0
- package/agent-agy-sdk/dist/agent-preset-agy.js +32 -0
- package/agent-agy-sdk/dist/agent.d.ts +138 -0
- package/agent-agy-sdk/dist/agent.js +494 -0
- package/{agent-agy → agent-agy-sdk}/dist/agy-client.d.ts +26 -6
- package/{agent-agy → agent-agy-sdk}/dist/agy-client.js +43 -25
- package/agent-agy-sdk/dist/agy-sessions.d.ts +25 -0
- package/agent-agy-sdk/dist/agy-sessions.js +107 -0
- package/agent-agy-sdk/dist/agy-store.d.ts +39 -0
- package/agent-agy-sdk/dist/agy-store.js +83 -0
- package/agent-agy-sdk/dist/inbox.d.ts +26 -0
- package/agent-agy-sdk/dist/inbox.js +53 -0
- package/agent-agy-sdk/dist/index.d.ts +93 -0
- package/agent-agy-sdk/dist/index.js +476 -0
- package/agent-agy-sdk/dist/models.d.ts +41 -0
- package/agent-agy-sdk/dist/models.js +74 -0
- package/agent-agy-sdk/dist/sdk-venv.d.ts +76 -0
- package/agent-agy-sdk/dist/sdk-venv.js +173 -0
- package/agent-agy-sdk/dist/world-plugin.d.ts +10 -0
- package/agent-agy-sdk/dist/world-plugin.js +82 -0
- package/agent-agy-sdk/package.json +64 -0
- package/agent-codex/README.md +141 -36
- package/agent-codex/cordis.patch.yml +14 -10
- package/agent-codex/dist/adapter.js +64 -28
- package/agent-codex/dist/agent.js +255 -81
- package/agent-codex/dist/app-server-approvals.js +165 -0
- package/agent-codex/dist/app-server-client.js +723 -0
- package/agent-codex/dist/app-server-events.js +432 -0
- package/agent-codex/dist/app-server-protocol.js +95 -0
- package/agent-codex/dist/app-server-wire.js +299 -0
- package/agent-codex/dist/codex-launch.js +78 -0
- package/agent-codex/dist/codex-native-config.js +392 -0
- package/agent-codex/dist/codex-provider-gateway.js +402 -0
- package/agent-codex/dist/codex-providers.js +376 -0
- package/agent-codex/dist/codex-route.js +86 -0
- package/agent-codex/dist/index.js +256 -115
- package/agent-codex/dist/permission.js +75 -1
- package/agent-codex/package.json +1 -2
- package/agent-codex-sdk/README.md +79 -0
- package/agent-codex-sdk/cordis.patch.yml +81 -0
- package/agent-codex-sdk/dist/adapter.js +91 -0
- package/agent-codex-sdk/dist/agent-preset-codex.js +32 -0
- package/agent-codex-sdk/dist/agent.js +1085 -0
- package/agent-codex-sdk/dist/codex-native-config.js +392 -0
- package/agent-codex-sdk/dist/codex-provider-gateway.js +402 -0
- package/agent-codex-sdk/dist/codex-providers.js +376 -0
- package/agent-codex-sdk/dist/codex-route.js +86 -0
- package/{agent-codex/dist/codex-client.js → agent-codex-sdk/dist/codex-sdk-client.js} +253 -32
- package/agent-codex-sdk/dist/codex-sdk-launch.js +92 -0
- package/agent-codex-sdk/dist/codex-store.js +180 -0
- package/agent-codex-sdk/dist/inbox.js +53 -0
- package/agent-codex-sdk/dist/index.js +739 -0
- package/agent-codex-sdk/dist/permission.js +71 -0
- package/agent-codex-sdk/dist/sdk-runtime.js +307 -0
- package/agent-codex-sdk/dist/session-map.js +167 -0
- package/agent-codex-sdk/dist/world-plugin.js +93 -0
- package/agent-codex-sdk/package.json +79 -0
- package/agent-codex-sdk/scripts/fetch-sdk-runtime.mjs +57 -0
- package/agent-omp/README.md +72 -55
- package/agent-omp/dist/agent.js +36 -11
- package/agent-omp/dist/index.js +92 -52
- package/agent-omp/dist/lazy-rpc.js +11 -4
- package/agent-omp/dist/omp-cli.js +114 -97
- package/agent-omp/dist/omp-store.js +12 -4
- package/agent-omp/dist/permission.js +70 -5
- package/agent-omp/dist/rpc-client.js +409 -0
- package/agent-omp/dist/world-plugin.js +36 -3
- package/agent-omp/package.json +2 -6
- package/agent-omp-sdk/README.md +103 -0
- package/agent-omp-sdk/cordis.patch.yml +108 -0
- package/agent-omp-sdk/dist/adapter.d.ts +21 -0
- package/agent-omp-sdk/dist/adapter.js +36 -0
- package/agent-omp-sdk/dist/agent-preset-omp.d.ts +31 -0
- package/agent-omp-sdk/dist/agent-preset-omp.js +32 -0
- package/agent-omp-sdk/dist/agent.d.ts +64 -0
- package/agent-omp-sdk/dist/agent.js +1178 -0
- package/agent-omp-sdk/dist/discovery.d.ts +57 -0
- package/agent-omp-sdk/dist/discovery.js +116 -0
- package/agent-omp-sdk/dist/inbox.d.ts +28 -0
- package/agent-omp-sdk/dist/inbox.js +53 -0
- package/agent-omp-sdk/dist/index.d.ts +60 -0
- package/agent-omp-sdk/dist/index.js +632 -0
- package/agent-omp-sdk/dist/knobs.d.ts +34 -0
- package/agent-omp-sdk/dist/knobs.js +50 -0
- package/agent-omp-sdk/dist/lazy-rpc.d.ts +98 -0
- package/agent-omp-sdk/dist/lazy-rpc.js +178 -0
- package/agent-omp-sdk/dist/mobile/zoom-guard.d.ts +187 -0
- package/agent-omp-sdk/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp-sdk/dist/mobile-boot.d.ts +49 -0
- package/agent-omp-sdk/dist/mobile-boot.js +38 -0
- package/agent-omp-sdk/dist/models.d.ts +39 -0
- package/agent-omp-sdk/dist/models.js +299 -0
- package/agent-omp-sdk/dist/omp-cli.d.ts +67 -0
- package/agent-omp-sdk/dist/omp-cli.js +164 -0
- package/agent-omp-sdk/dist/omp-disk-discovery.d.ts +27 -0
- package/agent-omp-sdk/dist/omp-disk-discovery.js +178 -0
- package/agent-omp-sdk/dist/omp-executable.d.ts +2 -0
- package/agent-omp-sdk/dist/omp-executable.js +30 -0
- package/agent-omp-sdk/dist/omp-store.d.ts +134 -0
- package/agent-omp-sdk/dist/omp-store.js +526 -0
- package/agent-omp-sdk/dist/pairing.d.ts +7 -0
- package/agent-omp-sdk/dist/pairing.js +45 -0
- package/agent-omp-sdk/dist/permission.d.ts +138 -0
- package/agent-omp-sdk/dist/permission.js +161 -0
- package/agent-omp-sdk/dist/protocol.d.ts +189 -0
- package/agent-omp-sdk/dist/rpc-types.d.ts +77 -0
- package/agent-omp-sdk/dist/rpc-types.js +1 -0
- package/agent-omp-sdk/dist/sdk-client.d.ts +160 -0
- package/{agent-omp → agent-omp-sdk}/dist/sdk-client.js +108 -56
- package/agent-omp-sdk/dist/sdk-runtime.d.ts +211 -0
- package/agent-omp-sdk/dist/sdk-runtime.js +418 -0
- package/agent-omp-sdk/dist/sidecar-client.d.ts +87 -0
- package/agent-omp-sdk/dist/sidecar-client.js +197 -0
- package/agent-omp-sdk/dist/store/adapter.d.ts +17 -0
- package/agent-omp-sdk/dist/store/adapter.js +47 -0
- package/agent-omp-sdk/dist/store/db.d.ts +70 -0
- package/agent-omp-sdk/dist/store/db.js +210 -0
- package/agent-omp-sdk/dist/store/index.d.ts +23 -0
- package/agent-omp-sdk/dist/store/index.js +59 -0
- package/agent-omp-sdk/dist/store/reconcile.d.ts +15 -0
- package/agent-omp-sdk/dist/store/reconcile.js +51 -0
- package/agent-omp-sdk/dist/store/schema.d.ts +8 -0
- package/agent-omp-sdk/dist/store/schema.js +40 -0
- package/agent-omp-sdk/dist/transport.d.ts +49 -0
- package/agent-omp-sdk/dist/transport.js +35 -0
- package/agent-omp-sdk/dist/world-home.d.ts +4 -0
- package/agent-omp-sdk/dist/world-home.js +23 -0
- package/agent-omp-sdk/dist/world-plugin.d.ts +10 -0
- package/agent-omp-sdk/dist/world-plugin.js +137 -0
- package/agent-omp-sdk/package.json +87 -0
- package/client/index.js +1794 -287
- package/cordis.patch.yml +80 -0
- package/package.json +23 -8
- package/superd-hub/dist/a2a-delivery.d.ts +189 -0
- package/superd-hub/dist/a2a-delivery.js +240 -0
- package/superd-hub/dist/a2a-host.d.ts +76 -0
- package/superd-hub/dist/a2a-host.js +145 -0
- package/superd-hub/dist/a2a-ingest.d.ts +31 -0
- package/superd-hub/dist/a2a-ingest.js +636 -0
- package/superd-hub/dist/a2a-log.d.ts +17 -0
- package/superd-hub/dist/a2a-log.js +42 -0
- package/superd-hub/dist/a2a-row.d.ts +65 -0
- package/superd-hub/dist/a2a-row.js +162 -0
- package/superd-hub/dist/a2a-service.d.ts +357 -0
- package/superd-hub/dist/a2a-service.js +980 -0
- package/superd-hub/dist/a2a-wire.d.ts +220 -0
- package/superd-hub/dist/a2a-wire.js +305 -0
- package/superd-hub/dist/agent-ids.d.ts +10 -0
- package/superd-hub/dist/agent-ids.js +68 -0
- package/superd-hub/dist/agent-roster.d.ts +29 -18
- package/superd-hub/dist/agent-roster.js +54 -15
- package/superd-hub/dist/agent-status.d.ts +119 -0
- package/superd-hub/dist/agent-status.js +386 -0
- package/superd-hub/dist/client/agent-settings.d.ts +156 -0
- package/superd-hub/dist/client/agent-settings.js +229 -0
- package/superd-hub/dist/client/mesh-api.d.ts +134 -0
- package/superd-hub/dist/client/mesh-api.js +219 -0
- package/superd-hub/dist/client/mesh-groups.d.ts +63 -0
- package/superd-hub/dist/client/mesh-groups.js +66 -0
- package/superd-hub/dist/client/roster.d.ts +48 -0
- package/superd-hub/dist/client/roster.js +102 -0
- package/superd-hub/dist/client/runtime-settings-api.d.ts +76 -0
- package/superd-hub/dist/client/runtime-settings-api.js +137 -0
- package/superd-hub/dist/client/switch.d.ts +165 -0
- package/superd-hub/dist/client/switch.js +295 -0
- package/superd-hub/dist/exec-pty-channel.d.ts +58 -0
- package/superd-hub/dist/exec-pty-channel.js +162 -0
- package/superd-hub/dist/exec-pty-framing.d.ts +32 -0
- package/superd-hub/dist/exec-pty-framing.js +81 -0
- package/superd-hub/dist/exec-pty-rpc.d.ts +21 -0
- package/superd-hub/dist/exec-pty-rpc.js +128 -0
- package/superd-hub/dist/exec-pty-session.d.ts +76 -0
- package/superd-hub/dist/exec-pty-session.js +234 -0
- package/superd-hub/dist/exec.d.ts +56 -0
- package/superd-hub/dist/exec.js +225 -0
- package/superd-hub/dist/gateway.d.ts +39 -2
- package/superd-hub/dist/gateway.js +62 -3
- package/superd-hub/dist/hub-tool.d.ts +174 -0
- package/superd-hub/dist/hub-tool.js +464 -0
- package/superd-hub/dist/index.d.ts +15 -1
- package/superd-hub/dist/index.js +16 -0
- package/superd-hub/dist/machines.d.ts +45 -0
- package/superd-hub/dist/machines.js +125 -0
- package/superd-hub/dist/mesh-federate.d.ts +138 -0
- package/superd-hub/dist/mesh-federate.js +223 -0
- package/superd-hub/dist/mesh-gateway.d.ts +67 -0
- package/superd-hub/dist/mesh-gateway.js +408 -0
- package/superd-hub/dist/mesh-link.d.ts +274 -0
- package/superd-hub/dist/mesh-link.js +441 -0
- package/superd-hub/dist/mesh-rpc.d.ts +123 -0
- package/superd-hub/dist/mesh-rpc.js +250 -0
- package/superd-hub/dist/mux-client.d.ts +71 -0
- package/superd-hub/dist/mux-client.js +281 -0
- package/superd-hub/dist/pack-version.d.ts +2 -0
- package/superd-hub/dist/pack-version.js +30 -0
- package/superd-hub/dist/roster.d.ts +6 -0
- package/superd-hub/dist/rpc.d.ts +18 -2
- package/superd-hub/dist/rpc.js +34 -4
- package/superd-hub/dist/spawn-world.js +3 -1
- package/superd-hub/dist/switch-core.d.ts +62 -25
- package/superd-hub/dist/switch-core.js +53 -30
- package/superd-hub/dist/system-head.d.ts +22 -12
- package/superd-hub/dist/system-head.js +100 -84
- package/superd-hub/dist/wire-endpoints.d.ts +264 -0
- package/superd-hub/dist/wire-endpoints.js +180 -0
- package/superd-hub/lib/client/index.js +1794 -287
- package/superd-hub/package.json +11 -2
- package/agent-agy/dist/agent-preset-projection.d.ts +0 -31
- package/agent-agy/dist/agent-preset-projection.js +0 -33
- package/agent-claude/dist/agent-preset-projection.js +0 -30
- package/agent-codex/dist/agent-preset-projection.js +0 -37
- package/agent-codex/dist/app-home.js +0 -65
- package/agent-codex/dist/models.js +0 -152
- package/agent-codex/dist/spike.js +0 -46
- package/agent-hermes/dist/agent-preset-projection.js +0 -34
- package/agent-omp/dist/agent-preset-projection.js +0 -36
- package/agent-omp/dist/app-home.js +0 -86
- package/agent-omp/dist/sidecar-client.js +0 -195
- package/agent-pi/dist/agent-preset-projection.js +0 -29
- package/superd-hub/dist/client-shim.d.ts +0 -28
- package/superd-hub/dist/client-shim.js +0 -251
- package/superd-hub/dist/index-pass.d.ts +0 -22
- package/superd-hub/dist/index-pass.js +0 -47
- package/superd-hub/dist/labels.d.ts +0 -45
- package/superd-hub/dist/labels.js +0 -83
- /package/{agent-agy → agent-agy-sdk}/bridge/agy_bridge.py +0 -0
- /package/{agent-agy → agent-agy-sdk}/bridge/gemini_openai_server.mjs +0 -0
- /package/{agent-codex/dist/codex-events.js → agent-codex-sdk/dist/codex-sdk-events.js} +0 -0
- /package/{agent-omp → agent-omp-sdk}/dist/protocol.js +0 -0
- /package/{agent-omp → agent-omp-sdk}/sidecar/main.ts +0 -0
|
@@ -0,0 +1,739 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-codex provider — Codex provider plugin for DeepSeek Harness
|
|
3
|
+
* (Super D, SDK line).
|
|
4
|
+
*
|
|
5
|
+
* Replaces the built-in agent loop with an `AgentFactory` that drives
|
|
6
|
+
* `@openai/codex-sdk` threads (one `CodexSdkClient` per session) and bridges
|
|
7
|
+
* the projected wire stream into the Dash Agent/Session contracts. Mirrors
|
|
8
|
+
* `@deepseek-ai/dsh-agent-loop`'s creation transaction (prepare → owned
|
|
9
|
+
* storage handle → setup → publish) so the session + agent publish as one
|
|
10
|
+
* ordered lifecycle AND the DSH session log is written through the upstream
|
|
11
|
+
* `sessionPersistence` service — list/read/replay/workspace grouping are then
|
|
12
|
+
* ordinary DSH services over that log.
|
|
13
|
+
*
|
|
14
|
+
* Session identity (2026-09-16, "mapping only"): the DSH session id is the
|
|
15
|
+
* authority — supplied by the harness at create (`options.sessionId`) and
|
|
16
|
+
* resume (`options.resumeSessionId`). Codex's thread id only materializes
|
|
17
|
+
* after the first turn, so the pairing is persisted in the adapter's app home
|
|
18
|
+
* (`<codexHome>/dsh-sessions.json`, see session-map.ts). Resume resolves that
|
|
19
|
+
* record and fails closed when the thread is unknown. The rollout scan, the
|
|
20
|
+
* adapter-owned session list and the transcript replay are gone.
|
|
21
|
+
*/
|
|
22
|
+
import { existsSync, realpathSync, statSync } from "node:fs";
|
|
23
|
+
import { join } from "node:path";
|
|
24
|
+
import { Service } from "@deepseek-ai/cordis";
|
|
25
|
+
import { installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
26
|
+
import { SessionLogOffset, SessionPreparation, interruptedTurnClosers, } from "@deepseek-ai/dsh-session";
|
|
27
|
+
import { CodexSdkClient } from "./codex-sdk-client.js";
|
|
28
|
+
import { CodexAgent } from "./agent.js";
|
|
29
|
+
import { CodexLlmAdapter, routeIds } from "./adapter.js";
|
|
30
|
+
import { registerCodexPreset } from "./agent-preset-codex.js";
|
|
31
|
+
import { readRolloutHead, resolveCodexHome } from "./codex-store.js";
|
|
32
|
+
import { CodexResumeError, sessionRecord, upsertSession } from "./session-map.js";
|
|
33
|
+
import { readProviderStore } from "./codex-providers.js";
|
|
34
|
+
import { readNativeSelection } from "./codex-native-config.js";
|
|
35
|
+
import { CodexProvidersGateway, syncFromNative } from "./codex-provider-gateway.js";
|
|
36
|
+
import { seedRoute } from "./codex-route.js";
|
|
37
|
+
import { providerSdkLaunch } from "./codex-sdk-launch.js";
|
|
38
|
+
import { codexApprovalMode, defaultPermissionPreset, envApprovalMode, presetFromEvents } from "./permission.js";
|
|
39
|
+
/** Realpath of a recorded cwd, accepted only when it names an existing directory. */
|
|
40
|
+
function validatedCwd(cwd) {
|
|
41
|
+
if (cwd === undefined)
|
|
42
|
+
return undefined;
|
|
43
|
+
try {
|
|
44
|
+
const canonical = realpathSync(cwd);
|
|
45
|
+
return statSync(canonical).isDirectory() ? canonical : undefined;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
return undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
/** Diagnostic trace (CODEX_TRACE=1 on the dsh process enables stderr tracing). */
|
|
52
|
+
const trace = (...parts) => {
|
|
53
|
+
if (process.env.CODEX_TRACE === "1")
|
|
54
|
+
process.stderr.write(`[codex-provider ${Date.now() % 1_000_000}] ${parts.join(" ")}\n`);
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* How often the provider store is reconciled with the user's `config.toml`
|
|
58
|
+
* (2026-10-04 user ruling). cc-switch writes that file behind our back, so a
|
|
59
|
+
* one-shot read at boot would let the panel drift from the file the user edits;
|
|
60
|
+
* a minute-scale tick is cheap (one small file parse) and keeps the two honest.
|
|
61
|
+
*/
|
|
62
|
+
const SYNC_INTERVAL_MS = 600_000;
|
|
63
|
+
/** Open a read handle, pull the complete stored log, close. */
|
|
64
|
+
async function readStoredEvents(persistence, id, signal) {
|
|
65
|
+
const handle = await persistence.open(id, "read", signal === undefined ? {} : { signal });
|
|
66
|
+
try {
|
|
67
|
+
return (await handle.read(0, undefined, signal === undefined ? {} : { signal })).events;
|
|
68
|
+
}
|
|
69
|
+
finally {
|
|
70
|
+
await handle.close();
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
export class CodexProvider extends Service {
|
|
74
|
+
/**
|
|
75
|
+
* `agentDefaultModel` and `settings` are load-bearing, not decorative (OMP
|
|
76
|
+
* twin, same boot-order trap): the harness owns the "default for new sessions"
|
|
77
|
+
* selection there (`@deepseek-ai/dsh-agent-default-model`, mounted by dsh-base
|
|
78
|
+
* with a hard-coded `deepseek-official/deepseek-flash`), and this bridge is the
|
|
79
|
+
* only thing that feeds it the codex config's real default. Without the inject
|
|
80
|
+
* the context cannot resolve the service at apply time and the push in
|
|
81
|
+
* #registerDefaultModel never fires — the picker keeps advertising a model no
|
|
82
|
+
* adapter in this world serves.
|
|
83
|
+
*/
|
|
84
|
+
static inject = ["agents", "sessions", "llm", "agentDefaultModel", "settings"];
|
|
85
|
+
/** Plain holder — prevents Cordis re-tracing the factory's ctx through a caller shadow. */
|
|
86
|
+
runtime;
|
|
87
|
+
/**
|
|
88
|
+
* The world's DSH home — captured once from the world's own `dshHomePath`
|
|
89
|
+
* (never ambient env inside plugins). It anchors adapter-owned DSH state
|
|
90
|
+
* (the session map) only; the codex app home is the CLI's native `~/.codex`
|
|
91
|
+
* (2026-09-17 ruling), resolved on demand via resolveCodexHome().
|
|
92
|
+
*/
|
|
93
|
+
home;
|
|
94
|
+
/** Echo/staleness guard for the app-wide default-model push. */
|
|
95
|
+
defaultModelKey = "";
|
|
96
|
+
/**
|
|
97
|
+
* The live `ctx.llm` registration for the codex routes, when one exists.
|
|
98
|
+
* Held so a later store change can re-point it atomically: re-registering
|
|
99
|
+
* from scratch would race the picker and briefly lose every provider.
|
|
100
|
+
*/
|
|
101
|
+
routes;
|
|
102
|
+
/** Epoch ms of the last reconcile with the user's `config.toml`. */
|
|
103
|
+
syncedAt;
|
|
104
|
+
constructor(ctx) {
|
|
105
|
+
super(ctx, "codexProvider");
|
|
106
|
+
this.runtime = { ctx };
|
|
107
|
+
// Native-home ruling (2026-09-17): the Codex CLI keeps its own native home
|
|
108
|
+
// (~/.codex; $CODEX_HOME overrides in tests) — the adapter never redirects
|
|
109
|
+
// it and never seeds config into it. this.home is only the world's DSH
|
|
110
|
+
// home, anchoring adapter-owned DSH state (the session map).
|
|
111
|
+
this.home = this.#resolveHome();
|
|
112
|
+
const codexHome = resolveCodexHome();
|
|
113
|
+
trace(`home: worldHome=${this.home} codexHome=${codexHome} configExists=${existsSync(join(codexHome, "config.toml"))}`);
|
|
114
|
+
ctx.effect(() => ctx.agents.setFactory(this), "codexProvider.setFactory()");
|
|
115
|
+
// The store is canonical and the native file is a source (2026-10-04).
|
|
116
|
+
// Register the routes the store already implies FIRST, so a malformed
|
|
117
|
+
// config.toml costs the world a reconcile, never its provider list; then
|
|
118
|
+
// reconcile, then keep reconciling on a timer.
|
|
119
|
+
this.#registerModelCatalog();
|
|
120
|
+
this.syncNow();
|
|
121
|
+
const timer = setInterval(() => this.syncNow(), SYNC_INTERVAL_MS);
|
|
122
|
+
timer.unref?.();
|
|
123
|
+
ctx.effect(() => () => clearInterval(timer), "codexProvider.syncTimer()");
|
|
124
|
+
this.#registerDefaultModel();
|
|
125
|
+
// The roster must be visible to the API gateway's root-level remote
|
|
126
|
+
// enumeration (dsh-host-apiproxy read it from the root service table);
|
|
127
|
+
// nesting it under a child fiber via ctx.plugin hides its @Remote routes
|
|
128
|
+
// from the gateway and every agentPresets/* call 404s. Register it on
|
|
129
|
+
// this plugin's own (top-level) fiber instead — its Service constructor
|
|
130
|
+
// ties teardown to this fiber via reflect.provide.
|
|
131
|
+
// 0.1.7: roster/selection/document RPC + the `agentPreset` session
|
|
132
|
+
// projection all live in the host AgentPresetRegistry (mounted by the
|
|
133
|
+
// dsh-web-app bundle's `agent-preset-registry` row; the adapter patch
|
|
134
|
+
// pins its `default` to the Codex preset). The adapter only registers
|
|
135
|
+
// its one fixed preset into the registry.
|
|
136
|
+
registerCodexPreset(ctx);
|
|
137
|
+
// The Hub settings panel's data plane (2026-10-04 multi-provider ruling on
|
|
138
|
+
// the SDK line): an adapter-owned Typert Remote (`codexProviders/get|save|remove`)
|
|
139
|
+
// on this world's context, exactly as the app-server line mounts it. The
|
|
140
|
+
// hub's carrier forwards `envelope.method` verbatim, so the panel reaches it
|
|
141
|
+
// through the existing `/hub/api?agent=codex-sdk` family — no new hub route,
|
|
142
|
+
// and no provider knowledge leaks out of the adapter (dev-rules §12).
|
|
143
|
+
//
|
|
144
|
+
// `resolveCodexHome` is passed as a THUNK on purpose: the native home is
|
|
145
|
+
// re-resolved per call (so a test's `CODEX_HOME` and an operator's override
|
|
146
|
+
// are both honored) and construction stays read-free — the panel's first
|
|
147
|
+
// `get` is what touches disk.
|
|
148
|
+
new CodexProvidersGateway(ctx, this.home, () => resolveCodexHome(), () => this.refreshRoutes());
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Reconcile the canonical provider store with the user's `config.toml` and
|
|
152
|
+
* re-register the `ctx.llm` routes if either moved.
|
|
153
|
+
*
|
|
154
|
+
* Called once at startup and every {@link SYNC_INTERVAL_MS} (2026-10-04 user
|
|
155
|
+
* ruling), plus after every panel mutation through the gateway's `onChange`.
|
|
156
|
+
* Never throws: a provider list is not worth a boot.
|
|
157
|
+
*/
|
|
158
|
+
syncNow() {
|
|
159
|
+
try {
|
|
160
|
+
const { store, added, updated } = syncFromNative(this.home, resolveCodexHome());
|
|
161
|
+
if (added.length > 0 || updated.length > 0) {
|
|
162
|
+
this.runtime.ctx.logger.info(`codex-provider: config.toml sync — added ${added.join(", ") || "none"}, refreshed ${updated.join(", ") || "none"}`);
|
|
163
|
+
}
|
|
164
|
+
this.syncedAt = Date.now();
|
|
165
|
+
this.#applyRoutes(store.providers);
|
|
166
|
+
}
|
|
167
|
+
catch (error) {
|
|
168
|
+
this.runtime.ctx.logger.warn(`codex-provider: provider sync failed: ${String(error)}`);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/** Re-register the `ctx.llm` routes from the current store (panel mutation path). */
|
|
172
|
+
refreshRoutes() {
|
|
173
|
+
try {
|
|
174
|
+
this.#applyRoutes(readProviderStore(this.home).providers);
|
|
175
|
+
}
|
|
176
|
+
catch (error) {
|
|
177
|
+
this.runtime.ctx.logger.warn(`codex-provider: route refresh failed: ${String(error)}`);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The provider id the user's own `config.toml` names, mapped into the store.
|
|
182
|
+
* It is the app-wide default's provider and the fallback for a session whose
|
|
183
|
+
* logged selection predates the multi-provider rework. `undefined` when the
|
|
184
|
+
* native file names nothing this world can serve.
|
|
185
|
+
*/
|
|
186
|
+
nativeProviderId() {
|
|
187
|
+
const native = readNativeSelection(resolveCodexHome());
|
|
188
|
+
const wanted = native.providerId;
|
|
189
|
+
if (wanted === undefined)
|
|
190
|
+
return undefined;
|
|
191
|
+
const providers = readProviderStore(this.home).providers;
|
|
192
|
+
return providers.find((entry) => entry.id === wanted)?.id ?? undefined;
|
|
193
|
+
}
|
|
194
|
+
/** The store entry a DSH route id names. */
|
|
195
|
+
providerEntry(providerId) {
|
|
196
|
+
return readProviderStore(this.home).providers.find((entry) => entry.id === providerId);
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* The factory's session-level route fallback — always ONE coherent pair.
|
|
200
|
+
*
|
|
201
|
+
* The ambient `agentDefaultModel` selection is not authoritative here (user
|
|
202
|
+
* directive 2026-09-15), and neither is a global "active" provider any more:
|
|
203
|
+
* the session's OWN `model/selection` decides, resolved by the agent at each
|
|
204
|
+
* turn. These options are what a session with no selection of its own starts
|
|
205
|
+
* from.
|
|
206
|
+
*
|
|
207
|
+
* The one trap this method exists to close: a cached default can outlive the
|
|
208
|
+
* model list it named, because the panel may have removed that model since
|
|
209
|
+
* (upstream persists the last pick as the deployment default). Honouring a pair
|
|
210
|
+
* the provider no longer serves would seed a route nobody can run, so a request
|
|
211
|
+
* wins only when BOTH halves are still servable; otherwise the user's own
|
|
212
|
+
* `config.toml` pair takes over, which is authoritative for exactly the provider
|
|
213
|
+
* it is in force for.
|
|
214
|
+
*/
|
|
215
|
+
sessionAgentOptions(requested) {
|
|
216
|
+
const native = readNativeSelection(resolveCodexHome());
|
|
217
|
+
const wanted = requested?.provider !== undefined && typeof requested.model === "string" ? requested : undefined;
|
|
218
|
+
const serves = wanted !== undefined
|
|
219
|
+
&& this.providerEntry(wanted.provider)?.models.some((model) => model.id === wanted.model) === true;
|
|
220
|
+
if (wanted !== undefined && serves)
|
|
221
|
+
return { ...wanted, provider: wanted.provider, model: wanted.model };
|
|
222
|
+
const provider = this.nativeProviderId();
|
|
223
|
+
const model = native.model !== undefined && native.model !== "" ? native.model : undefined;
|
|
224
|
+
return {
|
|
225
|
+
...(requested ?? {}),
|
|
226
|
+
...(provider === undefined ? {} : { provider }),
|
|
227
|
+
...(model === undefined ? {} : { model }),
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
/**
|
|
231
|
+
* The write path is mandatory now (2026-09-16): this factory owns the DSH
|
|
232
|
+
* session log through the upstream persistence service — a session created
|
|
233
|
+
* or resumed without a backend would persist nothing.
|
|
234
|
+
*/
|
|
235
|
+
requirePersistence(op) {
|
|
236
|
+
const persistence = this.runtime.ctx.get("sessionPersistence");
|
|
237
|
+
if (persistence === undefined) {
|
|
238
|
+
throw new Error(`cannot ${op} a Codex session: session persistence is not configured`);
|
|
239
|
+
}
|
|
240
|
+
return persistence;
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Take a fresh session's write ownership. Nothing is appended here: the
|
|
244
|
+
* constructor seed (which never re-emits through `session/event`) is stored
|
|
245
|
+
* by `appendUnstoredSuffix` at the publication commit point, so a failed or
|
|
246
|
+
* cancelled setup closes an unmaterialized handle and leaves no residue.
|
|
247
|
+
*/
|
|
248
|
+
async createStoredSession(persistence, session, signal) {
|
|
249
|
+
const handle = await persistence.create(session.header, {
|
|
250
|
+
inheritedEventCount: session.inheritedEventCount,
|
|
251
|
+
...(signal === undefined ? {} : { signal }),
|
|
252
|
+
});
|
|
253
|
+
return { handle, storedCount: 0 };
|
|
254
|
+
}
|
|
255
|
+
/**
|
|
256
|
+
* Runtime facts the agent stamps into the log and needs to pin a provider:
|
|
257
|
+
* Codex's own system prompt (rollout HEAD `base_instructions`, readable once
|
|
258
|
+
* the thread exists), the launch that pins a provider, and the provider's own
|
|
259
|
+
* context window for one of its models.
|
|
260
|
+
*/
|
|
261
|
+
agentRuntime(client, id) {
|
|
262
|
+
const codexHome = resolveCodexHome();
|
|
263
|
+
return {
|
|
264
|
+
systemPrompt: () => {
|
|
265
|
+
const threadId = client.threadId;
|
|
266
|
+
if (threadId === null)
|
|
267
|
+
return undefined;
|
|
268
|
+
return readRolloutHead(codexHome, threadId)?.baseInstructions;
|
|
269
|
+
},
|
|
270
|
+
// The whole on-the-fly mechanism in one call: the agent asks for the
|
|
271
|
+
// config/env that pin a provider, and gets them without a single write to
|
|
272
|
+
// the user's home (the per-provider model catalog is ours).
|
|
273
|
+
launchFor: (providerId) => {
|
|
274
|
+
const entry = this.providerEntry(providerId);
|
|
275
|
+
return entry === undefined ? undefined : providerSdkLaunch(this.home, codexHome, entry);
|
|
276
|
+
},
|
|
277
|
+
contextWindowFor: (providerId, model) => {
|
|
278
|
+
const entry = this.providerEntry(providerId);
|
|
279
|
+
const found = entry?.models.find((candidate) => candidate.id === model);
|
|
280
|
+
return found?.contextWindow;
|
|
281
|
+
},
|
|
282
|
+
// RC-4: turn-time native rejections mark the map so the next resume
|
|
283
|
+
// fails fast (CodexResumeError NATIVE_REJECTED) instead of burning
|
|
284
|
+
// another turn on a dead thread.
|
|
285
|
+
markNotResumable: () => {
|
|
286
|
+
try {
|
|
287
|
+
upsertSession(this.home, String(id), { resumable: false });
|
|
288
|
+
this.runtime.ctx.logger.warn(`codex-provider: session ${String(id)} marked not resumable after a native rejection`);
|
|
289
|
+
}
|
|
290
|
+
catch (error) {
|
|
291
|
+
this.runtime.ctx.logger.warn(`codex-provider: failed to mark ${String(id)} not resumable: ${String(error)}`);
|
|
292
|
+
}
|
|
293
|
+
},
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
/** The world's DSH home: boot-provided dshHomePath first, env last. */
|
|
297
|
+
#resolveHome() {
|
|
298
|
+
const fromBoot = this.runtime.ctx.get("dshHomePath");
|
|
299
|
+
if (typeof fromBoot === "string" && fromBoot !== "")
|
|
300
|
+
return fromBoot;
|
|
301
|
+
if (typeof fromBoot === "function") {
|
|
302
|
+
const resolved = fromBoot();
|
|
303
|
+
if (typeof resolved === "string" && resolved !== "")
|
|
304
|
+
return resolved;
|
|
305
|
+
}
|
|
306
|
+
return process.env.DSH_HOME ?? join(process.cwd(), ".tests", "superd");
|
|
307
|
+
}
|
|
308
|
+
/**
|
|
309
|
+
* App-wide wiring the adapter owns (user 2026-09-15): the SDK has no
|
|
310
|
+
* default-model concept, so the bridge publishes the codex config.toml
|
|
311
|
+
* default into the world's `agentDefaultModel`. One-directional on purpose —
|
|
312
|
+
* the config is read-only for us (S7 single-direction valve), which makes the
|
|
313
|
+
* codex world's default sticky. Echo-guarded by the saved key.
|
|
314
|
+
*
|
|
315
|
+
* Why the write goes through `settings.replace` and not through
|
|
316
|
+
* `agentDefaultModel.saveSelection`: the harness service installs its section
|
|
317
|
+
* from an `inject(["settings"], …)` callback but then writes through
|
|
318
|
+
* `this.ctx.get("settings")?.replace(…)` on its UNSCOPED context, where
|
|
319
|
+
* `settings` is not injected — the optional chain swallows the lookup and the
|
|
320
|
+
* call is a silent no-op (the settings file is never touched, and the picker
|
|
321
|
+
* keeps showing the base default). Same defect and same fix as the OMP twin
|
|
322
|
+
* (`#saveDefaultSelection`): write the installed section directly through our
|
|
323
|
+
* own injected `settings` service, which the harness folds back via
|
|
324
|
+
* `installSection`, so every other consumer sees the new default.
|
|
325
|
+
*/
|
|
326
|
+
#registerDefaultModel() {
|
|
327
|
+
this.runtime.ctx.inject(["agentDefaultModel", "settings"], (mCtx) => {
|
|
328
|
+
const service = mCtx.get("agentDefaultModel");
|
|
329
|
+
if (service === undefined)
|
|
330
|
+
return;
|
|
331
|
+
// The default is whatever the user's own codex config runs, expressed in
|
|
332
|
+
// the store's own route id so the harness can resolve it. A config naming
|
|
333
|
+
// a provider this world has no route for is not a default we can push.
|
|
334
|
+
const native = readNativeSelection(resolveCodexHome());
|
|
335
|
+
const nativeProvider = this.nativeProviderId();
|
|
336
|
+
const nativeModel = native.model;
|
|
337
|
+
if (nativeProvider === undefined || nativeModel === undefined || nativeModel === "") {
|
|
338
|
+
trace("default model push skipped: config.toml names no servable provider/model");
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const target = { provider: nativeProvider, model: nativeModel };
|
|
342
|
+
const targetKey = `${target.provider}/${target.model}`;
|
|
343
|
+
const current = service.currentSelection?.();
|
|
344
|
+
const currentKey = current === undefined ? undefined : `${current.provider}/${current.model}`;
|
|
345
|
+
if (currentKey === targetKey || currentKey === this.defaultModelKey)
|
|
346
|
+
return;
|
|
347
|
+
trace(`default model: ${currentKey ?? "none"} → ${targetKey}`);
|
|
348
|
+
const settings = mCtx.get("settings");
|
|
349
|
+
void Promise.resolve(settings?.replace?.("agent-default-model", target))
|
|
350
|
+
// Best-effort second path: harmless when upstream's no-op swallows it,
|
|
351
|
+
// and it keeps the in-memory selection fresh on builds where it works.
|
|
352
|
+
.then(() => service.saveSelection?.(target))
|
|
353
|
+
.then(() => {
|
|
354
|
+
this.defaultModelKey = targetKey;
|
|
355
|
+
this.runtime.ctx.logger.info(`codex-provider: default model ← codex config: ${targetKey}`);
|
|
356
|
+
})
|
|
357
|
+
.catch((error) => {
|
|
358
|
+
this.runtime.ctx.logger.warn(`codex-provider: default model save failed: ${String(error)}`);
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
}
|
|
362
|
+
/**
|
|
363
|
+
* Register the codex routes on `ctx.llm` — ONE ROUTE PER PROVIDER with
|
|
364
|
+
* models (2026-10-04 rework).
|
|
365
|
+
*
|
|
366
|
+
* The harness's model selector is provider-grouped, and its per-session
|
|
367
|
+
* choice is durable (`model/selection` + the `modelSelection` projection), so
|
|
368
|
+
* expressing the Codex provider list as real routes is what makes the whole
|
|
369
|
+
* feature DSH-native: nothing in Super D has to model "the active provider" or
|
|
370
|
+
* remember which route a session last used.
|
|
371
|
+
*
|
|
372
|
+
* A provider with no models yet registers no route — codex itself cannot run
|
|
373
|
+
* it until the user says which model to ask for, and an empty group in the
|
|
374
|
+
* picker would be noise.
|
|
375
|
+
*/
|
|
376
|
+
#registerModelCatalog() {
|
|
377
|
+
this.#applyRoutes(readProviderStore(this.home).providers);
|
|
378
|
+
}
|
|
379
|
+
/** Register or atomically re-point the codex routes at the given providers. */
|
|
380
|
+
#applyRoutes(providers) {
|
|
381
|
+
const llm = this.runtime.ctx.get("llm");
|
|
382
|
+
if (llm === undefined)
|
|
383
|
+
return;
|
|
384
|
+
const ids = routeIds({ version: 1, providers });
|
|
385
|
+
if (this.routes === undefined) {
|
|
386
|
+
if (ids.length === 0) {
|
|
387
|
+
// `registerAdapter` refuses an empty initial set, and there is nothing
|
|
388
|
+
// to serve yet; the next sync or panel save will register the routes.
|
|
389
|
+
trace("route registration deferred: no provider with models yet");
|
|
390
|
+
return;
|
|
391
|
+
}
|
|
392
|
+
this.routes = llm.registerAdapter([...ids], new CodexLlmAdapter(this.home));
|
|
393
|
+
trace(`routes registered: ${ids.join(", ")}`);
|
|
394
|
+
return;
|
|
395
|
+
}
|
|
396
|
+
// `replace` validates the whole candidate set before swapping, so a bad id
|
|
397
|
+
// cannot leave the registry half-updated — but a throw here must not take
|
|
398
|
+
// the caller (a timer, or a panel save) down with it.
|
|
399
|
+
this.routes.replace([...ids]);
|
|
400
|
+
trace(`routes replaced: ${ids.join(", ") || "none"}`);
|
|
401
|
+
}
|
|
402
|
+
async createAgent(ownerCtx, options) {
|
|
403
|
+
const loopCtx = this.runtime.ctx;
|
|
404
|
+
const id = options.sessionId;
|
|
405
|
+
const meta = options.meta ?? {};
|
|
406
|
+
const cwd = meta.cwd;
|
|
407
|
+
// Fork (the apiproxy's `session.fork`) seeds a copied-turn prefix but
|
|
408
|
+
// Codex has no native fork: fail the fork cleanly at the factory boundary.
|
|
409
|
+
// `parentSession` in create metadata is set only by the fork path
|
|
410
|
+
// (subagents go through `ctx.subagents` instead).
|
|
411
|
+
if (meta.parentSession !== undefined) {
|
|
412
|
+
throw new Error(`cannot fork session "${meta.parentSession}" onto the Codex provider: Codex has no native session fork`);
|
|
413
|
+
}
|
|
414
|
+
// The DSH session log is this factory's write path (2026-09-16): without
|
|
415
|
+
// the upstream persistence service a created session would persist
|
|
416
|
+
// nothing, so require it up front.
|
|
417
|
+
const persistence = this.requirePersistence("create");
|
|
418
|
+
// Approval policy is pinned at Codex thread construction and the Dash
|
|
419
|
+
// session does not exist yet at this point (it is prepared + announced —
|
|
420
|
+
// which is what stamps `permission/preset` via pinInitialPermission —
|
|
421
|
+
// only inside setupAndPublish, AFTER this spawn), so the effective
|
|
422
|
+
// preset cannot come from session events: the env override
|
|
423
|
+
// (CODEX_APPROVAL_MODE, headless runs) wins, else the permission
|
|
424
|
+
// service's default, mapped onto the launch policy.
|
|
425
|
+
const preset = envApprovalMode() ?? defaultPermissionPreset(loopCtx);
|
|
426
|
+
const approvalPolicy = codexApprovalMode(preset);
|
|
427
|
+
const spawnCwd = validatedCwd(cwd) ?? process.cwd();
|
|
428
|
+
trace(`create id=${id} preset=${preset ?? "none"} approval=${approvalPolicy} spawnCwd=${spawnCwd}${envApprovalMode() === undefined ? "" : " (env override)"}`);
|
|
429
|
+
// Backend takes ZERO part in the UI's browser-local new-session draft
|
|
430
|
+
// beyond the (zero-IO) thread construction: no rollout, no session
|
|
431
|
+
// events — the session announces blank. The thread materializes on the
|
|
432
|
+
// first prompt.
|
|
433
|
+
// The world home is threaded through: it is where the durable SDK runtime
|
|
434
|
+
// is fetched on the first real prompt (sdk-runtime.ts).
|
|
435
|
+
const client = await CodexSdkClient.spawn(["--approval-mode", approvalPolicy], spawnCwd, this.home);
|
|
436
|
+
// No eager `setModel` here any more (2026-10-04): the agent resolves the
|
|
437
|
+
// whole route — provider included — at the top of every turn and hands it to
|
|
438
|
+
// the client, so the seeding below is the only thing `create` says about
|
|
439
|
+
// where the first turn runs.
|
|
440
|
+
const preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
|
|
441
|
+
...(options.seed === undefined ? {} : { seed: options.seed }),
|
|
442
|
+
...(meta === undefined ? {} : { meta }),
|
|
443
|
+
...(options.inheritedEventCount === undefined ? {} : { inheritedEventCount: options.inheritedEventCount }),
|
|
444
|
+
}));
|
|
445
|
+
let stored;
|
|
446
|
+
let handedOff = false;
|
|
447
|
+
try {
|
|
448
|
+
stored = await this.createStoredSession(persistence, preparation.session, options.signal);
|
|
449
|
+
// Identity record BEFORE publication: the DSH id → Codex thread pairing
|
|
450
|
+
// (threadId null until the first turn) is what resume resolves.
|
|
451
|
+
upsertSession(this.home, String(id), {
|
|
452
|
+
threadId: client.threadId,
|
|
453
|
+
cwd: spawnCwd,
|
|
454
|
+
createdAt: Date.now(),
|
|
455
|
+
preset: preset ?? null,
|
|
456
|
+
});
|
|
457
|
+
this.followThreadId(id, client);
|
|
458
|
+
handedOff = true;
|
|
459
|
+
return await setupAndPublish(loopCtx, ownerCtx, id, this.sessionAgentOptions(options.agentOptions), options.setup, preparation.session, client, options.parentAgent, "startup", { enterSession: true, preparation }, stored, this.agentRuntime(client, id));
|
|
460
|
+
}
|
|
461
|
+
catch (error) {
|
|
462
|
+
trace(`create ${id} THREW: ${String(error)}`);
|
|
463
|
+
client.close();
|
|
464
|
+
if (!handedOff)
|
|
465
|
+
await stored?.handle.close().catch(() => { });
|
|
466
|
+
throw error;
|
|
467
|
+
}
|
|
468
|
+
finally {
|
|
469
|
+
if (!handedOff)
|
|
470
|
+
preparation[Symbol.dispose]();
|
|
471
|
+
}
|
|
472
|
+
}
|
|
473
|
+
async resume(ownerCtx, options) {
|
|
474
|
+
const loopCtx = this.runtime.ctx;
|
|
475
|
+
const id = options.resumeSessionId;
|
|
476
|
+
// Identity (2026-09-16 "mapping only", reaffirmed 2026-09-18): the DSH
|
|
477
|
+
// session id resolves through the adapter-owned map — the thread id is
|
|
478
|
+
// recorded when Codex materializes it. The adapter NEVER scans the native
|
|
479
|
+
// ~/.codex session storage to recover pairings (2026-09-18 user ruling:
|
|
480
|
+
// the discovery tier violated the design — DSH log is the transcript
|
|
481
|
+
// authority; the native home contributes config/settings only). Every
|
|
482
|
+
// miss fails closed with a stable code (CodexResumeError).
|
|
483
|
+
const persistence = this.requirePersistence("resume");
|
|
484
|
+
const record = sessionRecord(this.home, String(id));
|
|
485
|
+
trace(`resume id=${id} record=${record === undefined ? "MISSING" : record.threadId ?? "NO-THREAD"}${record?.resumable === false ? " (marked not resumable)" : ""}`);
|
|
486
|
+
if (record === undefined) {
|
|
487
|
+
throw new CodexResumeError("SESSION_MAP_MISS", `cannot resume session "${id}": no Codex thread is recorded for this Dash session id`);
|
|
488
|
+
}
|
|
489
|
+
if (record.threadId === null) {
|
|
490
|
+
// C2: never auto-start a fresh thread — the session's history is in the
|
|
491
|
+
// (missing) native thread, not in a blank one.
|
|
492
|
+
throw new CodexResumeError("NATIVE_THREAD_NEVER_STARTED", `cannot resume session "${id}": its Codex thread was never started (no thread id recorded)`);
|
|
493
|
+
}
|
|
494
|
+
if (record.resumable === false) {
|
|
495
|
+
// A previous native resume was rejected at turn time (plan RC-4): fail
|
|
496
|
+
// fast with the clear refusal instead of burning another turn.
|
|
497
|
+
throw new CodexResumeError("NATIVE_REJECTED", `cannot resume session "${id}": its Codex thread ${record.threadId} was rejected by the native runtime in an earlier attempt`);
|
|
498
|
+
}
|
|
499
|
+
// Pre-validation (plan RC-4): the rollout must exist in the CURRENT codex
|
|
500
|
+
// home BEFORE spawning — covers rollouts stranded under the abandoned
|
|
501
|
+
// `<worldHome>/.codex` home (never migrated) and pruned rollouts alike.
|
|
502
|
+
// Without this gate the failure would surface only at the first turn
|
|
503
|
+
// (resumeThread is zero-IO).
|
|
504
|
+
if (readRolloutHead(resolveCodexHome(), record.threadId) === undefined) {
|
|
505
|
+
throw new CodexResumeError("ROLLOUT_MISSING", `cannot resume session "${id}": no rollout for Codex thread ${record.threadId} exists in the current Codex home (${resolveCodexHome()})`);
|
|
506
|
+
}
|
|
507
|
+
// Spawn cwd comes from the recorded pairing — re-realpath and re-verify so
|
|
508
|
+
// a moved/removed directory cannot aim a resume outside a live path.
|
|
509
|
+
const spawnCwd = validatedCwd(record.cwd);
|
|
510
|
+
if (spawnCwd === undefined) {
|
|
511
|
+
throw new Error(`cannot resume session "${id}": its recorded working directory no longer exists`);
|
|
512
|
+
}
|
|
513
|
+
// Approval policy for the re-attached thread: launch-only, so it is
|
|
514
|
+
// decided BEFORE the spawn from the recorded preset, else the persisted
|
|
515
|
+
// session log, else the permission service's default. CODEX_APPROVAL_MODE
|
|
516
|
+
// (headless runs) overrides; no preset → never.
|
|
517
|
+
const envMode = envApprovalMode();
|
|
518
|
+
const preset = envMode !== undefined
|
|
519
|
+
? envMode
|
|
520
|
+
: record.preset ??
|
|
521
|
+
presetFromEvents(await readStoredEvents(persistence, id, options.signal)) ??
|
|
522
|
+
defaultPermissionPreset(loopCtx);
|
|
523
|
+
const approvalPolicy = codexApprovalMode(preset);
|
|
524
|
+
trace(`resume id=${id} spawnCwd=${spawnCwd} preset=${preset ?? "none"} approval=${approvalPolicy}`);
|
|
525
|
+
// Re-attach to the Codex thread by its id (Codex owns the live agent
|
|
526
|
+
// context and keeps generating it from here on; the model context is
|
|
527
|
+
// restored by Codex itself from its rollout).
|
|
528
|
+
const client = await CodexSdkClient.spawn(["--approval-mode", approvalPolicy, "--resume-thread", record.threadId], spawnCwd, this.home);
|
|
529
|
+
let handle;
|
|
530
|
+
let stored;
|
|
531
|
+
let preparation;
|
|
532
|
+
let handedOff = false;
|
|
533
|
+
try {
|
|
534
|
+
// Taking write ownership FIRST excludes a concurrent resume of the same
|
|
535
|
+
// id (a live agent's handle holds the claim in this process).
|
|
536
|
+
handle = await persistence.open(id, "write", options.signal === undefined ? {} : { signal: options.signal });
|
|
537
|
+
// Crash repair belongs to the agent layer: an interrupted final turn
|
|
538
|
+
// receives synthetic closers appended through the same handle.
|
|
539
|
+
const cold = await handle.read(0, undefined, options.signal === undefined ? {} : { signal: options.signal });
|
|
540
|
+
const closers = interruptedTurnClosers(cold.events);
|
|
541
|
+
if (closers.length > 0)
|
|
542
|
+
await handle.append(closers);
|
|
543
|
+
preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
|
|
544
|
+
seed: [...cold.events, ...closers],
|
|
545
|
+
meta: structuredClone(handle.header),
|
|
546
|
+
inheritedEventCount: handle.inheritedEventCount,
|
|
547
|
+
eventState: cold.eventState,
|
|
548
|
+
}));
|
|
549
|
+
stored = { handle, storedCount: cold.events.length + closers.length };
|
|
550
|
+
trace(`resume id=${id} seeded ${cold.events.length} events (+${closers.length} closers)`);
|
|
551
|
+
handle = undefined; // ownership passes to setupAndPublish
|
|
552
|
+
this.followThreadId(id, client);
|
|
553
|
+
handedOff = true;
|
|
554
|
+
return await setupAndPublish(loopCtx, ownerCtx, id, this.sessionAgentOptions(options.agentOptions), options.setup, preparation.session, client, options.parentAgent, "resume", { enterSession: true, preparation }, stored, this.agentRuntime(client, id));
|
|
555
|
+
}
|
|
556
|
+
catch (error) {
|
|
557
|
+
trace(`resume ${id} THREW: ${String(error)} | stack=${error instanceof Error ? error.stack?.slice(0, 400) : "n/a"}`);
|
|
558
|
+
client.close();
|
|
559
|
+
if (!handedOff)
|
|
560
|
+
await handle?.close().catch(() => { });
|
|
561
|
+
throw error;
|
|
562
|
+
}
|
|
563
|
+
finally {
|
|
564
|
+
if (!handedOff)
|
|
565
|
+
preparation?.[Symbol.dispose]();
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
/**
|
|
569
|
+
* Watch the client for its first materialized thread id and persist the
|
|
570
|
+
* Dash↔thread pairing once (the record is the resume authority; the
|
|
571
|
+
* create-time entry starts with `threadId: null`).
|
|
572
|
+
* Arrow FIELD (not a `#` method and not a true-private field): the provider
|
|
573
|
+
* is exposed through a Cordis tracing proxy, and hard-private members fail
|
|
574
|
+
* their brand check on the proxy receiver (the same reason setupAndPublish
|
|
575
|
+
* is module-level). A persistence failure here is logged, never thrown into
|
|
576
|
+
* the client's event stream.
|
|
577
|
+
*/
|
|
578
|
+
followThreadId = (id, client) => {
|
|
579
|
+
// RE-ARMING observer (plan C6): newSession() swaps threads mid-session, so
|
|
580
|
+
// the pairing must be re-upserted whenever the client's thread id CHANGES,
|
|
581
|
+
// not just once. Unsubscribing after the first capture would leave the map
|
|
582
|
+
// pointing at the discarded thread, and the next resume would re-attach to
|
|
583
|
+
// a thread the session no longer runs on.
|
|
584
|
+
let lastRecorded = client.threadId;
|
|
585
|
+
const observe = (threadId) => {
|
|
586
|
+
lastRecorded = threadId;
|
|
587
|
+
try {
|
|
588
|
+
upsertSession(this.home, String(id), { threadId });
|
|
589
|
+
trace(`thread id recorded for ${String(id)}: ${threadId}`);
|
|
590
|
+
}
|
|
591
|
+
catch (error) {
|
|
592
|
+
this.runtime.ctx.logger.warn(`codex-provider: failed to persist the Codex thread id for ${String(id)}: ${String(error)}`);
|
|
593
|
+
}
|
|
594
|
+
};
|
|
595
|
+
if (lastRecorded !== null)
|
|
596
|
+
observe(lastRecorded);
|
|
597
|
+
client.on(() => {
|
|
598
|
+
const observed = client.threadId;
|
|
599
|
+
if (observed === null || observed === lastRecorded)
|
|
600
|
+
return;
|
|
601
|
+
observe(observed);
|
|
602
|
+
});
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
/**
|
|
606
|
+
* Durable flush of the session's pre-publication suffix. Constructor seed
|
|
607
|
+
* markers and setup-window events never re-emit through `session/event`, so
|
|
608
|
+
* publication must push them through the handle before live events start
|
|
609
|
+
* routing into it. Advances by what was stored (an event appended during the
|
|
610
|
+
* await stays unstored for the next flush).
|
|
611
|
+
*/
|
|
612
|
+
async function appendUnstoredSuffix(stored, session) {
|
|
613
|
+
const suffix = session.snapshotEvents(SessionLogOffset(stored.storedCount));
|
|
614
|
+
if (suffix.length > 0)
|
|
615
|
+
await stored.handle.append(suffix);
|
|
616
|
+
stored.storedCount += suffix.length;
|
|
617
|
+
}
|
|
618
|
+
/**
|
|
619
|
+
* Shared creation/resume transaction: build the agent over the prepared
|
|
620
|
+
* session, run unpublished setup, flush the unstored suffix, and publish both
|
|
621
|
+
* in order; the owned storage handle is drained (closed) on teardown. Kept as
|
|
622
|
+
* a module-level function (not a private method) because Cordis exposes the
|
|
623
|
+
* provider through a tracing proxy, which breaks hard-private (`#`) receivers.
|
|
624
|
+
*/
|
|
625
|
+
async function setupAndPublish(loopCtx, ownerCtx, id, agentOptions, setup, session, client, parentAgent, source, opts, stored, runtimeInfo) {
|
|
626
|
+
let detachSession;
|
|
627
|
+
let detachAgent;
|
|
628
|
+
let agent;
|
|
629
|
+
// Late-bound teardown the agent triggers itself after its idle window
|
|
630
|
+
// expires (see CodexAgent's idle exit) — defined only after publication.
|
|
631
|
+
let idleExit;
|
|
632
|
+
try {
|
|
633
|
+
// Build the agent shim over the session and the live client.
|
|
634
|
+
agent = new CodexAgent(loopCtx, id, agentOptions, session, client, () => idleExit?.(), runtimeInfo);
|
|
635
|
+
// Pin THIS session to the codex route. The global default-model setting is
|
|
636
|
+
// a best-effort push (the settings service may flush late, and a shared
|
|
637
|
+
// test home can hold another line's default), so the factory must not let
|
|
638
|
+
// the first prompt depend on it: `selectForNextRequest` is the same
|
|
639
|
+
// per-session mechanism `session/selectModel` uses (appends the log-only
|
|
640
|
+
// `model/selection` and caches the next request's route).
|
|
641
|
+
try {
|
|
642
|
+
// The seed comes from ONE source — the factory's reconciled options —
|
|
643
|
+
// because that pair is already coherent. Building it from the native
|
|
644
|
+
// provider id and the deployment default model independently is what once
|
|
645
|
+
// produced a route no provider served (`zhipu-cn/deepseek-flash`); taking
|
|
646
|
+
// one object makes that mistake unrepresentable (`seedRoute`).
|
|
647
|
+
const route = seedRoute(agentOptions);
|
|
648
|
+
if (route !== undefined) {
|
|
649
|
+
const selection = { provider: route.provider, model: route.model };
|
|
650
|
+
// Mirrors the upstream session-controller's `selectForNextRequest`:
|
|
651
|
+
// the durable log-only `model/selection` plus an agent-scoped
|
|
652
|
+
// `ModelSelectionRef` (same shape the controller installs) that the
|
|
653
|
+
// next prompt's route check reads. Synchronous on purpose — the first
|
|
654
|
+
// prompt must not depend on the global default's late flush.
|
|
655
|
+
let picked = selection;
|
|
656
|
+
installModelSelection(agent.ctx, {
|
|
657
|
+
get current() { return picked; },
|
|
658
|
+
set current(next) { picked = next; },
|
|
659
|
+
assembled: undefined,
|
|
660
|
+
});
|
|
661
|
+
// `model/selection` is contributed to `SessionEventMap` by the upstream
|
|
662
|
+
// session-controller package (declaration merging); this adapter does
|
|
663
|
+
// not depend on it, so the append goes through a narrow local cast.
|
|
664
|
+
session.append("model/selection", selection);
|
|
665
|
+
trace(`session route pinned: ${route.provider}/${route.model}`);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
catch (error) {
|
|
669
|
+
// Fail-soft: the boot-time default-model push remains the fallback.
|
|
670
|
+
trace(`session route pin failed: ${String(error)}`);
|
|
671
|
+
}
|
|
672
|
+
// Session identity (the preset stamp) and the Codex system prompt commit
|
|
673
|
+
// on the first turn via CodexAgent's #bootstrapSessionIdentity /
|
|
674
|
+
// #emitSystemMessage: a freshly created session announces completely
|
|
675
|
+
// blank, so the backend takes zero part in the UI's new-session draft.
|
|
676
|
+
// Composition-only setup on the unpublished agent scope.
|
|
677
|
+
const commit = await setup?.(agent.ctx, agent);
|
|
678
|
+
// Publish: flush the unstored suffix (seed + setup-window events) through
|
|
679
|
+
// the owned handle, then enter both session and agent, announce in order,
|
|
680
|
+
// and signal session-start. The commit runs immediately before
|
|
681
|
+
// publication.
|
|
682
|
+
commit?.commit();
|
|
683
|
+
await appendUnstoredSuffix(stored, session);
|
|
684
|
+
if (opts.enterSession) {
|
|
685
|
+
detachSession = agent.ctx.sessions.enter(session);
|
|
686
|
+
agent.ctx.sessions.announce(session);
|
|
687
|
+
}
|
|
688
|
+
detachAgent = loopCtx.agents.enter(agent, parentAgent);
|
|
689
|
+
// 0.1.6: announce(agent, source) is required, async, and emits agent/created
|
|
690
|
+
// itself — the manual agent/session-start emission is gone upstream.
|
|
691
|
+
await loopCtx.agents.announce(agent, source);
|
|
692
|
+
let disposed = false;
|
|
693
|
+
let unfollowOwner;
|
|
694
|
+
const dispose = async () => {
|
|
695
|
+
if (disposed)
|
|
696
|
+
return;
|
|
697
|
+
disposed = true;
|
|
698
|
+
trace(`dispose() called for agent ${id} (source=${source})`);
|
|
699
|
+
unfollowOwner?.();
|
|
700
|
+
await agent?.dispose();
|
|
701
|
+
detachAgent?.();
|
|
702
|
+
detachSession?.();
|
|
703
|
+
// Drain queued appends; a durability failure surfaces to the caller
|
|
704
|
+
// (the owner-unload and idle-exit paths log it).
|
|
705
|
+
await stored.handle.close();
|
|
706
|
+
};
|
|
707
|
+
idleExit = () => {
|
|
708
|
+
void dispose().catch((error) => {
|
|
709
|
+
loopCtx.logger.warn(`codex-provider: teardown of ${id} failed: ${String(error)}`);
|
|
710
|
+
});
|
|
711
|
+
};
|
|
712
|
+
// Follow the owner: a caller-fiber unload tears this agent down.
|
|
713
|
+
unfollowOwner = ownerCtx.effect(() => () => {
|
|
714
|
+
void dispose().catch((error) => {
|
|
715
|
+
loopCtx.logger.warn(`codex-provider: owner-unload teardown of ${id} failed: ${String(error)}`);
|
|
716
|
+
});
|
|
717
|
+
}, `codexProvider.lifecycle(${id})`);
|
|
718
|
+
return { agent, dispose };
|
|
719
|
+
}
|
|
720
|
+
catch (error) {
|
|
721
|
+
// Unwind the half-published transaction. A failure between `enter` and
|
|
722
|
+
// the announcements (e.g. a persistence listener rejecting the session)
|
|
723
|
+
// must not leave a live-but-dead entry the API resolver would serve.
|
|
724
|
+
trace(`setupAndPublish ${id} FAILED: ${String(error)}`);
|
|
725
|
+
detachAgent?.();
|
|
726
|
+
detachSession?.();
|
|
727
|
+
void agent?.dispose().catch(() => { });
|
|
728
|
+
client.close();
|
|
729
|
+
throw error;
|
|
730
|
+
}
|
|
731
|
+
finally {
|
|
732
|
+
// Release the preparation's per-id reservation on every path (mirrors
|
|
733
|
+
// the reference loop's unconditional dispose): the session it seeded is
|
|
734
|
+
// already published above, so a same-process re-resume of this id can
|
|
735
|
+
// prepare again instead of colliding with a leaked reservation.
|
|
736
|
+
opts.preparation?.[Symbol.dispose]();
|
|
737
|
+
}
|
|
738
|
+
}
|
|
739
|
+
export default CodexProvider;
|