super-dsh 0.1.0 → 0.1.2
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/agent-agy/bridge/agy_bridge.py +250 -0
- package/agent-agy/bridge/gemini_openai_server.mjs +98 -0
- package/agent-agy/cordis.patch.yml +49 -0
- package/agent-agy/dist/adapter.d.ts +15 -0
- package/agent-agy/dist/adapter.js +42 -0
- package/agent-agy/dist/agent-preset-agy.d.ts +47 -0
- package/agent-agy/dist/agent-preset-agy.js +169 -0
- package/agent-agy/dist/agent-preset-projection.d.ts +31 -0
- package/agent-agy/dist/agent-preset-projection.js +33 -0
- package/agent-agy/dist/agent.d.ts +137 -0
- package/agent-agy/dist/agent.js +493 -0
- package/agent-agy/dist/agy-cli-client.d.ts +137 -0
- package/agent-agy/dist/agy-cli-client.js +301 -0
- package/agent-agy/dist/agy-client.d.ts +160 -0
- package/agent-agy/dist/agy-client.js +252 -0
- package/agent-agy/dist/agy-sessions.d.ts +24 -0
- package/agent-agy/dist/agy-sessions.js +116 -0
- package/agent-agy/dist/agy-store.d.ts +39 -0
- package/agent-agy/dist/agy-store.js +83 -0
- package/agent-agy/dist/inbox.d.ts +26 -0
- package/agent-agy/dist/inbox.js +53 -0
- package/agent-agy/dist/index.d.ts +80 -0
- package/agent-agy/dist/index.js +505 -0
- package/agent-agy/dist/models.d.ts +41 -0
- package/agent-agy/dist/models.js +74 -0
- package/agent-agy/dist/world-plugin.d.ts +9 -0
- package/agent-agy/dist/world-plugin.js +97 -0
- package/agent-agy/package.json +64 -0
- package/agent-claude/cordis.patch.yml +42 -0
- package/agent-claude/dist/adapter.js +62 -0
- package/agent-claude/dist/agent-preset-claude.js +169 -0
- package/agent-claude/dist/agent-preset-projection.js +30 -0
- package/agent-claude/dist/agent.js +1271 -0
- package/agent-claude/dist/claude-client.js +357 -0
- package/agent-claude/dist/claude-events.js +223 -0
- package/agent-claude/dist/claude-home.js +29 -0
- package/agent-claude/dist/commands.js +182 -0
- package/agent-claude/dist/content.js +134 -0
- package/agent-claude/dist/inbox.js +53 -0
- package/agent-claude/dist/index.js +555 -0
- package/agent-claude/dist/input-queue.js +64 -0
- package/agent-claude/dist/interaction.js +271 -0
- package/agent-claude/dist/models.js +158 -0
- package/agent-claude/dist/permission.js +120 -0
- package/agent-claude/dist/session-id.js +20 -0
- package/agent-claude/dist/session-map.js +165 -0
- package/agent-claude/dist/world-plugin.js +14 -0
- package/agent-claude/package.json +74 -0
- package/agent-codex/README.md +74 -0
- package/agent-codex/cordis.patch.yml +77 -0
- package/agent-codex/dist/adapter.js +55 -0
- package/agent-codex/dist/agent-preset-codex.js +171 -0
- package/agent-codex/dist/agent-preset-projection.js +37 -0
- package/agent-codex/dist/agent.js +1056 -0
- package/agent-codex/dist/app-home.js +65 -0
- package/agent-codex/dist/codex-client.js +469 -0
- package/agent-codex/dist/codex-events.js +297 -0
- package/agent-codex/dist/codex-store.js +190 -0
- package/agent-codex/dist/inbox.js +53 -0
- package/agent-codex/dist/index.js +605 -0
- package/agent-codex/dist/models.js +152 -0
- package/agent-codex/dist/permission.js +71 -0
- package/agent-codex/dist/session-map.js +170 -0
- package/agent-codex/dist/spike.js +46 -0
- package/agent-codex/dist/world-plugin.js +96 -0
- package/agent-codex/package.json +78 -0
- package/agent-hermes/README.md +144 -0
- package/agent-hermes/cordis.patch.yml +83 -0
- package/agent-hermes/dist/adapter.js +83 -0
- package/agent-hermes/dist/agent-preset-hermes.js +171 -0
- package/agent-hermes/dist/agent-preset-projection.js +34 -0
- package/agent-hermes/dist/agent.js +1068 -0
- package/agent-hermes/dist/hermes-client.js +953 -0
- package/agent-hermes/dist/hermes-events.js +289 -0
- package/agent-hermes/dist/hermes-store.js +158 -0
- package/agent-hermes/dist/inbox.js +53 -0
- package/agent-hermes/dist/index.js +541 -0
- package/agent-hermes/dist/models.js +324 -0
- package/agent-hermes/dist/permission.js +60 -0
- package/agent-hermes/dist/world-plugin.js +101 -0
- package/agent-hermes/package.json +76 -0
- package/agent-hub/cordis.patch.yml +12 -0
- package/agent-hub/dist/agent-roster.d.ts +34 -0
- package/agent-hub/dist/agent-roster.js +27 -0
- package/agent-hub/dist/carrier.d.ts +43 -0
- package/agent-hub/dist/carrier.js +241 -0
- package/agent-hub/dist/client-shim.d.ts +28 -0
- package/agent-hub/dist/client-shim.js +251 -0
- package/agent-hub/dist/envelope.d.ts +54 -0
- package/agent-hub/dist/envelope.js +92 -0
- package/agent-hub/dist/gateway.d.ts +28 -0
- package/agent-hub/dist/gateway.js +18 -0
- package/agent-hub/dist/index-pass.d.ts +22 -0
- package/agent-hub/dist/index-pass.js +47 -0
- package/agent-hub/dist/index.d.ts +29 -0
- package/agent-hub/dist/index.js +29 -0
- package/agent-hub/dist/labels.d.ts +45 -0
- package/agent-hub/dist/labels.js +83 -0
- package/agent-hub/dist/ownership.d.ts +37 -0
- package/agent-hub/dist/ownership.js +54 -0
- package/agent-hub/dist/roster.d.ts +28 -0
- package/agent-hub/dist/roster.js +38 -0
- package/agent-hub/dist/rpc.d.ts +38 -0
- package/agent-hub/dist/rpc.js +52 -0
- package/agent-hub/dist/spawn-world.d.ts +46 -0
- package/agent-hub/dist/spawn-world.js +72 -0
- package/agent-hub/dist/targets.d.ts +33 -0
- package/agent-hub/dist/targets.js +30 -0
- package/agent-hub/dist/world-entry.d.ts +31 -0
- package/agent-hub/dist/world-entry.js +51 -0
- package/agent-hub/dist/world-host.d.ts +69 -0
- package/agent-hub/dist/world-host.js +44 -0
- package/agent-hub/dist/world-join.d.ts +16 -0
- package/agent-hub/dist/world-join.js +122 -0
- package/agent-hub/dist/world-mount.d.ts +52 -0
- package/agent-hub/dist/world-mount.js +52 -0
- package/agent-hub/dist/world-mux.d.ts +72 -0
- package/agent-hub/dist/world-mux.js +224 -0
- package/agent-hub/dist/world-provision.d.ts +20 -0
- package/agent-hub/dist/world-provision.js +122 -0
- package/agent-hub/dist/world-web-server.d.ts +162 -0
- package/agent-hub/dist/world-web-server.js +248 -0
- package/agent-hub/lib/client/index.js +242 -0
- package/agent-hub/package.json +65 -0
- package/agent-omp/README.md +77 -0
- package/agent-omp/cordis.patch.yml +92 -0
- package/agent-omp/dist/adapter.js +36 -0
- package/agent-omp/dist/agent-preset-omp.js +171 -0
- package/agent-omp/dist/agent-preset-projection.js +36 -0
- package/agent-omp/dist/agent.js +1145 -0
- package/agent-omp/dist/app-home.js +86 -0
- package/agent-omp/dist/discovery.js +116 -0
- package/agent-omp/dist/inbox.js +53 -0
- package/agent-omp/dist/index.js +574 -0
- package/agent-omp/dist/knobs.js +50 -0
- package/agent-omp/dist/lazy-rpc.js +161 -0
- package/agent-omp/dist/mobile/gesture.js +148 -0
- package/agent-omp/dist/mobile/zoom-guard.js +294 -0
- package/agent-omp/dist/mobile-boot.js +38 -0
- package/agent-omp/dist/models.js +299 -0
- package/agent-omp/dist/omp-cli.js +147 -0
- package/agent-omp/dist/omp-disk-discovery.js +178 -0
- package/agent-omp/dist/omp-store.js +518 -0
- package/agent-omp/dist/pairing.js +45 -0
- package/agent-omp/dist/permission.js +96 -0
- package/agent-omp/dist/protocol.js +13 -0
- package/agent-omp/dist/rpc-types.js +1 -0
- package/agent-omp/dist/sdk-client.js +378 -0
- package/agent-omp/dist/sidecar-client.js +195 -0
- package/agent-omp/dist/store/adapter.js +47 -0
- package/agent-omp/dist/store/db.js +210 -0
- package/agent-omp/dist/store/index.js +59 -0
- package/agent-omp/dist/store/reconcile.js +51 -0
- package/agent-omp/dist/store/schema.js +40 -0
- package/agent-omp/dist/world-plugin.js +96 -0
- package/agent-omp/package.json +97 -0
- package/agent-omp/sidecar/main.ts +481 -0
- package/agent-pi/README.md +67 -0
- package/agent-pi/cordis.patch.yml +80 -0
- package/agent-pi/dist/adapter.js +53 -0
- package/agent-pi/dist/agent-preset-pi.js +169 -0
- package/agent-pi/dist/agent-preset-projection.js +29 -0
- package/agent-pi/dist/agent.js +949 -0
- package/agent-pi/dist/inbox.js +53 -0
- package/agent-pi/dist/index.js +595 -0
- package/agent-pi/dist/knobs.js +24 -0
- package/agent-pi/dist/models.js +200 -0
- package/agent-pi/dist/permission.js +66 -0
- package/agent-pi/dist/pi-client.js +224 -0
- package/agent-pi/dist/pi-events.js +155 -0
- package/agent-pi/dist/pi-home.js +55 -0
- package/agent-pi/dist/safe-json.js +25 -0
- package/agent-pi/dist/session-map.js +119 -0
- package/agent-pi/dist/world-plugin.js +97 -0
- package/agent-pi/package.json +77 -0
- package/cordis.patch.yml +22 -19
- package/package.json +88 -25
- package/client.js +0 -4
|
@@ -0,0 +1,605 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* agent-codex provider — Codex provider plugin for DeepSeek Harness
|
|
3
|
+
* (Agent Worlds AW-E, 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 { emitAgentEvent, installModelSelection } from "@deepseek-ai/dsh-agent";
|
|
26
|
+
import { SessionLogOffset, SessionPreparation, interruptedTurnClosers, } from "@deepseek-ai/dsh-session";
|
|
27
|
+
import { CodexSdkClient } from "./codex-client.js";
|
|
28
|
+
import { CodexAgent } from "./agent.js";
|
|
29
|
+
import { CodexLlmAdapter, CODEX_PROVIDER_ID } from "./adapter.js";
|
|
30
|
+
import { SingleCodexPresetRoster } from "./agent-preset-codex.js";
|
|
31
|
+
import { codexAgentPresetProjection } from "./agent-preset-projection.js";
|
|
32
|
+
import { readRolloutHead, resolveCodexHome } from "./codex-store.js";
|
|
33
|
+
import { CodexResumeError, sessionRecord, upsertSession } from "./session-map.js";
|
|
34
|
+
import { readCodexModelCatalog, CODEX_DEFAULT_MODEL_PLACEHOLDER } from "./models.js";
|
|
35
|
+
import { codexApprovalMode, defaultPermissionPreset, envApprovalMode, presetFromEvents } from "./permission.js";
|
|
36
|
+
/** Realpath of a recorded cwd, accepted only when it names an existing directory. */
|
|
37
|
+
function validatedCwd(cwd) {
|
|
38
|
+
if (cwd === undefined)
|
|
39
|
+
return undefined;
|
|
40
|
+
try {
|
|
41
|
+
const canonical = realpathSync(cwd);
|
|
42
|
+
return statSync(canonical).isDirectory() ? canonical : undefined;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
/** Diagnostic trace (CODEX_TRACE=1 on the dsh process enables stderr tracing). */
|
|
49
|
+
const trace = (...parts) => {
|
|
50
|
+
if (process.env.CODEX_TRACE === "1")
|
|
51
|
+
process.stderr.write(`[codex-provider ${Date.now() % 1_000_000}] ${parts.join(" ")}\n`);
|
|
52
|
+
};
|
|
53
|
+
/** Open a read handle, pull the complete stored log, close. */
|
|
54
|
+
async function readStoredEvents(persistence, id, signal) {
|
|
55
|
+
const handle = await persistence.open(id, "read", signal === undefined ? {} : { signal });
|
|
56
|
+
try {
|
|
57
|
+
return (await handle.read(0, undefined, signal === undefined ? {} : { signal })).events;
|
|
58
|
+
}
|
|
59
|
+
finally {
|
|
60
|
+
await handle.close();
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
export class CodexProvider extends Service {
|
|
64
|
+
/**
|
|
65
|
+
* `agentDefaultModel` and `settings` are load-bearing, not decorative (OMP
|
|
66
|
+
* twin, same boot-order trap): the harness owns the "default for new sessions"
|
|
67
|
+
* selection there (`@deepseek-ai/dsh-agent-default-model`, mounted by dsh-base
|
|
68
|
+
* with a hard-coded `deepseek-official/deepseek-flash`), and this bridge is the
|
|
69
|
+
* only thing that feeds it the codex config's real default. Without the inject
|
|
70
|
+
* the context cannot resolve the service at apply time and the push in
|
|
71
|
+
* #registerDefaultModel never fires — the picker keeps advertising a model no
|
|
72
|
+
* adapter in this world serves.
|
|
73
|
+
*/
|
|
74
|
+
static inject = ["agents", "sessions", "llm", "agentDefaultModel", "settings"];
|
|
75
|
+
/** Plain holder — prevents Cordis re-tracing the factory's ctx through a caller shadow. */
|
|
76
|
+
runtime;
|
|
77
|
+
/**
|
|
78
|
+
* The world's DSH home — captured once from the world's own `dshHomePath`
|
|
79
|
+
* (never ambient env inside plugins). It anchors adapter-owned DSH state
|
|
80
|
+
* (the session map) only; the codex app home is the CLI's native `~/.codex`
|
|
81
|
+
* (2026-09-17 ruling), resolved on demand via resolveCodexHome().
|
|
82
|
+
*/
|
|
83
|
+
home;
|
|
84
|
+
/** Echo/staleness guard for the app-wide default-model push. */
|
|
85
|
+
defaultModelKey = "";
|
|
86
|
+
constructor(ctx) {
|
|
87
|
+
super(ctx, "codexProvider");
|
|
88
|
+
this.runtime = { ctx };
|
|
89
|
+
// Native-home ruling (2026-09-17): the Codex CLI keeps its own native home
|
|
90
|
+
// (~/.codex; $CODEX_HOME overrides in tests) — the adapter never redirects
|
|
91
|
+
// it and never seeds config into it. this.home is only the world's DSH
|
|
92
|
+
// home, anchoring adapter-owned DSH state (the session map).
|
|
93
|
+
this.home = this.#resolveHome();
|
|
94
|
+
const codexHome = resolveCodexHome();
|
|
95
|
+
trace(`home: worldHome=${this.home} codexHome=${codexHome} configExists=${existsSync(join(codexHome, "config.toml"))}`);
|
|
96
|
+
ctx.effect(() => ctx.agents.setFactory(this), "codexProvider.setFactory()");
|
|
97
|
+
this.#registerModelCatalog();
|
|
98
|
+
this.#registerDefaultModel();
|
|
99
|
+
// The roster must be visible to the API gateway's root-level remote
|
|
100
|
+
// enumeration (dsh-host-apiproxy read it from the root service table);
|
|
101
|
+
// nesting it under a child fiber via ctx.plugin hides its @Remote routes
|
|
102
|
+
// from the gateway and every agentPresets/* call 404s. Register it on
|
|
103
|
+
// this plugin's own (top-level) fiber instead — its Service constructor
|
|
104
|
+
// ties teardown to this fiber via reflect.provide.
|
|
105
|
+
new SingleCodexPresetRoster(ctx);
|
|
106
|
+
// Drive the `agentPreset` session projection ourselves: the upstream
|
|
107
|
+
// registrant lives in the (disabled) dsh-agent-presets package, but the
|
|
108
|
+
// Web UI gates the preset chip and header label on
|
|
109
|
+
// `projectionValues.agentPreset`. Deferred via inject so the registry's
|
|
110
|
+
// service contract is honored (registers only once it exists).
|
|
111
|
+
ctx.inject(["sessionProjections"], (scoped) => {
|
|
112
|
+
scoped.sessionProjections.register(codexAgentPresetProjection);
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* The factory owns its sessions' route (user directive 2026-09-15): the
|
|
117
|
+
* ambient `agentDefaultModel` selection is NOT authoritative here — a
|
|
118
|
+
* session created by this factory always runs on the codex route, with the
|
|
119
|
+
* codex config's real default model unless the caller asked for a served
|
|
120
|
+
* codex model. Without an explicit choice the model rides the thread
|
|
121
|
+
* options only when the catalog is readable (the placeholder is omitted so
|
|
122
|
+
* codex's own config default applies).
|
|
123
|
+
*/
|
|
124
|
+
sessionAgentOptions(requested) {
|
|
125
|
+
const catalog = readCodexModelCatalog(resolveCodexHome());
|
|
126
|
+
const explicit = requested?.provider === CODEX_PROVIDER_ID && typeof requested.model === "string" && requested.model !== CODEX_DEFAULT_MODEL_PLACEHOLDER
|
|
127
|
+
? requested.model
|
|
128
|
+
: undefined;
|
|
129
|
+
const model = explicit ?? (catalog.defaultModel === CODEX_DEFAULT_MODEL_PLACEHOLDER ? undefined : catalog.defaultModel);
|
|
130
|
+
return { ...(requested ?? {}), provider: CODEX_PROVIDER_ID, ...(model === undefined ? {} : { model }) };
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* The write path is mandatory now (2026-09-16): this factory owns the DSH
|
|
134
|
+
* session log through the upstream persistence service — a session created
|
|
135
|
+
* or resumed without a backend would persist nothing.
|
|
136
|
+
*/
|
|
137
|
+
requirePersistence(op) {
|
|
138
|
+
const persistence = this.runtime.ctx.get("sessionPersistence");
|
|
139
|
+
if (persistence === undefined) {
|
|
140
|
+
throw new Error(`cannot ${op} a Codex session: session persistence is not configured`);
|
|
141
|
+
}
|
|
142
|
+
return persistence;
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Take a fresh session's write ownership. Nothing is appended here: the
|
|
146
|
+
* constructor seed (which never re-emits through `session/event`) is stored
|
|
147
|
+
* by `appendUnstoredSuffix` at the publication commit point, so a failed or
|
|
148
|
+
* cancelled setup closes an unmaterialized handle and leaves no residue.
|
|
149
|
+
*/
|
|
150
|
+
async createStoredSession(persistence, session, signal) {
|
|
151
|
+
const handle = await persistence.create(session.header, {
|
|
152
|
+
inheritedEventCount: session.inheritedEventCount,
|
|
153
|
+
...(signal === undefined ? {} : { signal }),
|
|
154
|
+
});
|
|
155
|
+
return { handle, storedCount: 0 };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* Runtime facts the agent stamps into the log: Codex's own system prompt
|
|
159
|
+
* (rollout HEAD `base_instructions`, readable once the thread exists) and
|
|
160
|
+
* the effective route metadata (selected codex model, else the catalog
|
|
161
|
+
* default, plus the catalog's context window).
|
|
162
|
+
*/
|
|
163
|
+
agentRuntime(client, id) {
|
|
164
|
+
const codexHome = resolveCodexHome();
|
|
165
|
+
return {
|
|
166
|
+
systemPrompt: () => {
|
|
167
|
+
const threadId = client.threadId;
|
|
168
|
+
if (threadId === null)
|
|
169
|
+
return undefined;
|
|
170
|
+
return readRolloutHead(codexHome, threadId)?.baseInstructions;
|
|
171
|
+
},
|
|
172
|
+
routeContext: (preferredModel) => {
|
|
173
|
+
const catalog = readCodexModelCatalog(codexHome);
|
|
174
|
+
const model = preferredModel !== undefined && preferredModel !== "" && preferredModel !== CODEX_DEFAULT_MODEL_PLACEHOLDER
|
|
175
|
+
? preferredModel
|
|
176
|
+
: catalog.defaultModel === CODEX_DEFAULT_MODEL_PLACEHOLDER
|
|
177
|
+
? undefined
|
|
178
|
+
: catalog.defaultModel;
|
|
179
|
+
if (model === undefined)
|
|
180
|
+
return undefined;
|
|
181
|
+
const entry = catalog.models.find((candidate) => candidate.id === model);
|
|
182
|
+
return {
|
|
183
|
+
provider: CODEX_PROVIDER_ID,
|
|
184
|
+
model,
|
|
185
|
+
...(entry?.contextWindow !== undefined && entry.contextWindow > 0 ? { contextWindow: entry.contextWindow } : {}),
|
|
186
|
+
};
|
|
187
|
+
},
|
|
188
|
+
// RC-4: turn-time native rejections mark the map so the next resume
|
|
189
|
+
// fails fast (CodexResumeError NATIVE_REJECTED) instead of burning
|
|
190
|
+
// another turn on a dead thread.
|
|
191
|
+
markNotResumable: () => {
|
|
192
|
+
try {
|
|
193
|
+
upsertSession(this.home, String(id), { resumable: false });
|
|
194
|
+
this.runtime.ctx.logger.warn(`codex-provider: session ${String(id)} marked not resumable after a native rejection`);
|
|
195
|
+
}
|
|
196
|
+
catch (error) {
|
|
197
|
+
this.runtime.ctx.logger.warn(`codex-provider: failed to mark ${String(id)} not resumable: ${String(error)}`);
|
|
198
|
+
}
|
|
199
|
+
},
|
|
200
|
+
};
|
|
201
|
+
}
|
|
202
|
+
/** The world's DSH home: boot-provided dshHomePath first, env last. */
|
|
203
|
+
#resolveHome() {
|
|
204
|
+
const fromBoot = this.runtime.ctx.get("dshHomePath");
|
|
205
|
+
if (typeof fromBoot === "string" && fromBoot !== "")
|
|
206
|
+
return fromBoot;
|
|
207
|
+
if (typeof fromBoot === "function") {
|
|
208
|
+
const resolved = fromBoot();
|
|
209
|
+
if (typeof resolved === "string" && resolved !== "")
|
|
210
|
+
return resolved;
|
|
211
|
+
}
|
|
212
|
+
return process.env.DSH_HOME ?? join(process.cwd(), ".tests", "aw");
|
|
213
|
+
}
|
|
214
|
+
/**
|
|
215
|
+
* App-wide wiring the adapter owns (user 2026-09-15): the SDK has no
|
|
216
|
+
* default-model concept, so the bridge publishes the codex config.toml
|
|
217
|
+
* default into the world's `agentDefaultModel`. One-directional on purpose —
|
|
218
|
+
* the config is read-only for us (S7 single-direction valve), which makes the
|
|
219
|
+
* codex world's default sticky. Echo-guarded by the saved key.
|
|
220
|
+
*
|
|
221
|
+
* Why the write goes through `settings.replace` and not through
|
|
222
|
+
* `agentDefaultModel.saveSelection`: the harness service installs its section
|
|
223
|
+
* from an `inject(["settings"], …)` callback but then writes through
|
|
224
|
+
* `this.ctx.get("settings")?.replace(…)` on its UNSCOPED context, where
|
|
225
|
+
* `settings` is not injected — the optional chain swallows the lookup and the
|
|
226
|
+
* call is a silent no-op (the settings file is never touched, and the picker
|
|
227
|
+
* keeps showing the base default). Same defect and same fix as the OMP twin
|
|
228
|
+
* (`#saveDefaultSelection`): write the installed section directly through our
|
|
229
|
+
* own injected `settings` service, which the harness folds back via
|
|
230
|
+
* `installSection`, so every other consumer sees the new default.
|
|
231
|
+
*/
|
|
232
|
+
#registerDefaultModel() {
|
|
233
|
+
this.runtime.ctx.inject(["agentDefaultModel", "settings"], (mCtx) => {
|
|
234
|
+
const service = mCtx.get("agentDefaultModel");
|
|
235
|
+
if (service === undefined)
|
|
236
|
+
return;
|
|
237
|
+
const catalog = readCodexModelCatalog(resolveCodexHome());
|
|
238
|
+
if (catalog.defaultModel === CODEX_DEFAULT_MODEL_PLACEHOLDER) {
|
|
239
|
+
trace("default model push skipped: catalog unreadable (placeholder)");
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
const target = { provider: CODEX_PROVIDER_ID, model: catalog.defaultModel };
|
|
243
|
+
const targetKey = `${target.provider}/${target.model}`;
|
|
244
|
+
const current = service.currentSelection?.();
|
|
245
|
+
const currentKey = current === undefined ? undefined : `${current.provider}/${current.model}`;
|
|
246
|
+
if (currentKey === targetKey || currentKey === this.defaultModelKey)
|
|
247
|
+
return;
|
|
248
|
+
trace(`default model: ${currentKey ?? "none"} → ${targetKey}`);
|
|
249
|
+
const settings = mCtx.get("settings");
|
|
250
|
+
void Promise.resolve(settings?.replace?.("agent-default-model", target))
|
|
251
|
+
// Best-effort second path: harmless when upstream's no-op swallows it,
|
|
252
|
+
// and it keeps the in-memory selection fresh on builds where it works.
|
|
253
|
+
.then(() => service.saveSelection?.(target))
|
|
254
|
+
.then(() => {
|
|
255
|
+
this.defaultModelKey = targetKey;
|
|
256
|
+
this.runtime.ctx.logger.info(`codex-provider: default model ← codex config: ${targetKey}`);
|
|
257
|
+
})
|
|
258
|
+
.catch((error) => {
|
|
259
|
+
this.runtime.ctx.logger.warn(`codex-provider: default model save failed: ${String(error)}`);
|
|
260
|
+
});
|
|
261
|
+
});
|
|
262
|
+
}
|
|
263
|
+
#registerModelCatalog() {
|
|
264
|
+
const llm = this.runtime.ctx.get("llm");
|
|
265
|
+
if (llm === undefined)
|
|
266
|
+
return;
|
|
267
|
+
// The catalog is fail-soft (a `codex-default` placeholder when the home
|
|
268
|
+
// is unreadable), so the route always registers and the selector is
|
|
269
|
+
// never empty.
|
|
270
|
+
llm.registerAdapter([CODEX_PROVIDER_ID], new CodexLlmAdapter(resolveCodexHome()));
|
|
271
|
+
}
|
|
272
|
+
async createAgent(ownerCtx, options) {
|
|
273
|
+
const loopCtx = this.runtime.ctx;
|
|
274
|
+
const id = options.sessionId;
|
|
275
|
+
const meta = options.meta ?? {};
|
|
276
|
+
const cwd = meta.cwd;
|
|
277
|
+
// Fork (the apiproxy's `session.fork`) seeds a copied-turn prefix but
|
|
278
|
+
// Codex has no native fork: fail the fork cleanly at the factory boundary.
|
|
279
|
+
// `parentSession` in create metadata is set only by the fork path
|
|
280
|
+
// (subagents go through `ctx.subagents` instead).
|
|
281
|
+
if (meta.parentSession !== undefined) {
|
|
282
|
+
throw new Error(`cannot fork session "${meta.parentSession}" onto the Codex provider: Codex has no native session fork`);
|
|
283
|
+
}
|
|
284
|
+
// The DSH session log is this factory's write path (2026-09-16): without
|
|
285
|
+
// the upstream persistence service a created session would persist
|
|
286
|
+
// nothing, so require it up front.
|
|
287
|
+
const persistence = this.requirePersistence("create");
|
|
288
|
+
// Approval policy is pinned at Codex thread construction and the Dash
|
|
289
|
+
// session does not exist yet at this point (it is prepared + announced —
|
|
290
|
+
// which is what stamps `permission/preset` via pinInitialPermission —
|
|
291
|
+
// only inside setupAndPublish, AFTER this spawn), so the effective
|
|
292
|
+
// preset cannot come from session events: the env override
|
|
293
|
+
// (CODEX_APPROVAL_MODE, headless runs) wins, else the permission
|
|
294
|
+
// service's default, mapped onto the launch policy.
|
|
295
|
+
const preset = envApprovalMode() ?? defaultPermissionPreset(loopCtx);
|
|
296
|
+
const approvalPolicy = codexApprovalMode(preset);
|
|
297
|
+
const spawnCwd = validatedCwd(cwd) ?? process.cwd();
|
|
298
|
+
trace(`create id=${id} preset=${preset ?? "none"} approval=${approvalPolicy} spawnCwd=${spawnCwd}${envApprovalMode() === undefined ? "" : " (env override)"}`);
|
|
299
|
+
// Backend takes ZERO part in the UI's browser-local new-session draft
|
|
300
|
+
// beyond the (zero-IO) thread construction: no rollout, no session
|
|
301
|
+
// events — the session announces blank. The thread materializes on the
|
|
302
|
+
// first prompt.
|
|
303
|
+
const client = await CodexSdkClient.spawn(["--approval-mode", approvalPolicy], spawnCwd);
|
|
304
|
+
// Deterministic per-session route (user wiring directive 2026-09-15): the
|
|
305
|
+
// factory owns the model — the catalog's real default rides the first
|
|
306
|
+
// turn; the placeholder (unreadable catalog) is OMITTED so the codex
|
|
307
|
+
// config default applies.
|
|
308
|
+
const createCatalog = readCodexModelCatalog(resolveCodexHome());
|
|
309
|
+
if (createCatalog.defaultModel !== CODEX_DEFAULT_MODEL_PLACEHOLDER) {
|
|
310
|
+
void client.setModel(CODEX_PROVIDER_ID, createCatalog.defaultModel).catch(() => { });
|
|
311
|
+
}
|
|
312
|
+
const preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
|
|
313
|
+
...(options.seed === undefined ? {} : { seed: options.seed }),
|
|
314
|
+
...(meta === undefined ? {} : { meta }),
|
|
315
|
+
...(options.inheritedEventCount === undefined ? {} : { inheritedEventCount: options.inheritedEventCount }),
|
|
316
|
+
}));
|
|
317
|
+
let stored;
|
|
318
|
+
let handedOff = false;
|
|
319
|
+
try {
|
|
320
|
+
stored = await this.createStoredSession(persistence, preparation.session, options.signal);
|
|
321
|
+
// Identity record BEFORE publication: the DSH id → Codex thread pairing
|
|
322
|
+
// (threadId null until the first turn) is what resume resolves.
|
|
323
|
+
upsertSession(this.home, String(id), {
|
|
324
|
+
threadId: client.threadId,
|
|
325
|
+
cwd: spawnCwd,
|
|
326
|
+
createdAt: Date.now(),
|
|
327
|
+
preset: preset ?? null,
|
|
328
|
+
});
|
|
329
|
+
this.followThreadId(id, client);
|
|
330
|
+
handedOff = true;
|
|
331
|
+
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));
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
trace(`create ${id} THREW: ${String(error)}`);
|
|
335
|
+
client.close();
|
|
336
|
+
if (!handedOff)
|
|
337
|
+
await stored?.handle.close().catch(() => { });
|
|
338
|
+
throw error;
|
|
339
|
+
}
|
|
340
|
+
finally {
|
|
341
|
+
if (!handedOff)
|
|
342
|
+
preparation[Symbol.dispose]();
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
async resume(ownerCtx, options) {
|
|
346
|
+
const loopCtx = this.runtime.ctx;
|
|
347
|
+
const id = options.resumeSessionId;
|
|
348
|
+
// Identity (2026-09-16 "mapping only", reaffirmed 2026-09-18): the DSH
|
|
349
|
+
// session id resolves through the adapter-owned map — the thread id is
|
|
350
|
+
// recorded when Codex materializes it. The adapter NEVER scans the native
|
|
351
|
+
// ~/.codex session storage to recover pairings (2026-09-18 user ruling:
|
|
352
|
+
// the discovery tier violated the design — DSH log is the transcript
|
|
353
|
+
// authority; the native home contributes config/settings only). Every
|
|
354
|
+
// miss fails closed with a stable code (CodexResumeError).
|
|
355
|
+
const persistence = this.requirePersistence("resume");
|
|
356
|
+
const record = sessionRecord(this.home, String(id));
|
|
357
|
+
trace(`resume id=${id} record=${record === undefined ? "MISSING" : record.threadId ?? "NO-THREAD"}${record?.resumable === false ? " (marked not resumable)" : ""}`);
|
|
358
|
+
if (record === undefined) {
|
|
359
|
+
throw new CodexResumeError("SESSION_MAP_MISS", `cannot resume session "${id}": no Codex thread is recorded for this Dash session id`);
|
|
360
|
+
}
|
|
361
|
+
if (record.threadId === null) {
|
|
362
|
+
// C2: never auto-start a fresh thread — the session's history is in the
|
|
363
|
+
// (missing) native thread, not in a blank one.
|
|
364
|
+
throw new CodexResumeError("NATIVE_THREAD_NEVER_STARTED", `cannot resume session "${id}": its Codex thread was never started (no thread id recorded)`);
|
|
365
|
+
}
|
|
366
|
+
if (record.resumable === false) {
|
|
367
|
+
// A previous native resume was rejected at turn time (plan RC-4): fail
|
|
368
|
+
// fast with the clear refusal instead of burning another turn.
|
|
369
|
+
throw new CodexResumeError("NATIVE_REJECTED", `cannot resume session "${id}": its Codex thread ${record.threadId} was rejected by the native runtime in an earlier attempt`);
|
|
370
|
+
}
|
|
371
|
+
// Pre-validation (plan RC-4): the rollout must exist in the CURRENT codex
|
|
372
|
+
// home BEFORE spawning — covers rollouts stranded under the abandoned
|
|
373
|
+
// `<worldHome>/.codex` home (never migrated) and pruned rollouts alike.
|
|
374
|
+
// Without this gate the failure would surface only at the first turn
|
|
375
|
+
// (resumeThread is zero-IO).
|
|
376
|
+
if (readRolloutHead(resolveCodexHome(), record.threadId) === undefined) {
|
|
377
|
+
throw new CodexResumeError("ROLLOUT_MISSING", `cannot resume session "${id}": no rollout for Codex thread ${record.threadId} exists in the current Codex home (${resolveCodexHome()})`);
|
|
378
|
+
}
|
|
379
|
+
// Spawn cwd comes from the recorded pairing — re-realpath and re-verify so
|
|
380
|
+
// a moved/removed directory cannot aim a resume outside a live path.
|
|
381
|
+
const spawnCwd = validatedCwd(record.cwd);
|
|
382
|
+
if (spawnCwd === undefined) {
|
|
383
|
+
throw new Error(`cannot resume session "${id}": its recorded working directory no longer exists`);
|
|
384
|
+
}
|
|
385
|
+
// Approval policy for the re-attached thread: launch-only, so it is
|
|
386
|
+
// decided BEFORE the spawn from the recorded preset, else the persisted
|
|
387
|
+
// session log, else the permission service's default. CODEX_APPROVAL_MODE
|
|
388
|
+
// (headless runs) overrides; no preset → never.
|
|
389
|
+
const envMode = envApprovalMode();
|
|
390
|
+
const preset = envMode !== undefined
|
|
391
|
+
? envMode
|
|
392
|
+
: record.preset ??
|
|
393
|
+
presetFromEvents(await readStoredEvents(persistence, id, options.signal)) ??
|
|
394
|
+
defaultPermissionPreset(loopCtx);
|
|
395
|
+
const approvalPolicy = codexApprovalMode(preset);
|
|
396
|
+
trace(`resume id=${id} spawnCwd=${spawnCwd} preset=${preset ?? "none"} approval=${approvalPolicy}`);
|
|
397
|
+
// Re-attach to the Codex thread by its id (Codex owns the live agent
|
|
398
|
+
// context and keeps generating it from here on; the model context is
|
|
399
|
+
// restored by Codex itself from its rollout).
|
|
400
|
+
const client = await CodexSdkClient.spawn(["--approval-mode", approvalPolicy, "--resume-thread", record.threadId], spawnCwd);
|
|
401
|
+
let handle;
|
|
402
|
+
let stored;
|
|
403
|
+
let preparation;
|
|
404
|
+
let handedOff = false;
|
|
405
|
+
try {
|
|
406
|
+
// Taking write ownership FIRST excludes a concurrent resume of the same
|
|
407
|
+
// id (a live agent's handle holds the claim in this process).
|
|
408
|
+
handle = await persistence.open(id, "write", options.signal === undefined ? {} : { signal: options.signal });
|
|
409
|
+
// Crash repair belongs to the agent layer: an interrupted final turn
|
|
410
|
+
// receives synthetic closers appended through the same handle.
|
|
411
|
+
const cold = await handle.read(0, undefined, options.signal === undefined ? {} : { signal: options.signal });
|
|
412
|
+
const closers = interruptedTurnClosers(cold.events);
|
|
413
|
+
if (closers.length > 0)
|
|
414
|
+
await handle.append(closers);
|
|
415
|
+
preparation = SessionPreparation.create(loopCtx.sessions.prepare(id, {
|
|
416
|
+
seed: [...cold.events, ...closers],
|
|
417
|
+
meta: structuredClone(handle.header),
|
|
418
|
+
inheritedEventCount: handle.inheritedEventCount,
|
|
419
|
+
eventState: cold.eventState,
|
|
420
|
+
}));
|
|
421
|
+
stored = { handle, storedCount: cold.events.length + closers.length };
|
|
422
|
+
trace(`resume id=${id} seeded ${cold.events.length} events (+${closers.length} closers)`);
|
|
423
|
+
handle = undefined; // ownership passes to setupAndPublish
|
|
424
|
+
this.followThreadId(id, client);
|
|
425
|
+
handedOff = true;
|
|
426
|
+
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));
|
|
427
|
+
}
|
|
428
|
+
catch (error) {
|
|
429
|
+
trace(`resume ${id} THREW: ${String(error)} | stack=${error instanceof Error ? error.stack?.slice(0, 400) : "n/a"}`);
|
|
430
|
+
client.close();
|
|
431
|
+
if (!handedOff)
|
|
432
|
+
await handle?.close().catch(() => { });
|
|
433
|
+
throw error;
|
|
434
|
+
}
|
|
435
|
+
finally {
|
|
436
|
+
if (!handedOff)
|
|
437
|
+
preparation?.[Symbol.dispose]();
|
|
438
|
+
}
|
|
439
|
+
}
|
|
440
|
+
/**
|
|
441
|
+
* Watch the client for its first materialized thread id and persist the
|
|
442
|
+
* Dash↔thread pairing once (the record is the resume authority; the
|
|
443
|
+
* create-time entry starts with `threadId: null`).
|
|
444
|
+
* Arrow FIELD (not a `#` method and not a true-private field): the provider
|
|
445
|
+
* is exposed through a Cordis tracing proxy, and hard-private members fail
|
|
446
|
+
* their brand check on the proxy receiver (the same reason setupAndPublish
|
|
447
|
+
* is module-level). A persistence failure here is logged, never thrown into
|
|
448
|
+
* the client's event stream.
|
|
449
|
+
*/
|
|
450
|
+
followThreadId = (id, client) => {
|
|
451
|
+
// RE-ARMING observer (plan C6): newSession() swaps threads mid-session, so
|
|
452
|
+
// the pairing must be re-upserted whenever the client's thread id CHANGES,
|
|
453
|
+
// not just once. Unsubscribing after the first capture would leave the map
|
|
454
|
+
// pointing at the discarded thread, and the next resume would re-attach to
|
|
455
|
+
// a thread the session no longer runs on.
|
|
456
|
+
let lastRecorded = client.threadId;
|
|
457
|
+
const observe = (threadId) => {
|
|
458
|
+
lastRecorded = threadId;
|
|
459
|
+
try {
|
|
460
|
+
upsertSession(this.home, String(id), { threadId });
|
|
461
|
+
trace(`thread id recorded for ${String(id)}: ${threadId}`);
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
this.runtime.ctx.logger.warn(`codex-provider: failed to persist the Codex thread id for ${String(id)}: ${String(error)}`);
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
if (lastRecorded !== null)
|
|
468
|
+
observe(lastRecorded);
|
|
469
|
+
client.on(() => {
|
|
470
|
+
const observed = client.threadId;
|
|
471
|
+
if (observed === null || observed === lastRecorded)
|
|
472
|
+
return;
|
|
473
|
+
observe(observed);
|
|
474
|
+
});
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
/**
|
|
478
|
+
* Durable flush of the session's pre-publication suffix. Constructor seed
|
|
479
|
+
* markers and setup-window events never re-emit through `session/event`, so
|
|
480
|
+
* publication must push them through the handle before live events start
|
|
481
|
+
* routing into it. Advances by what was stored (an event appended during the
|
|
482
|
+
* await stays unstored for the next flush).
|
|
483
|
+
*/
|
|
484
|
+
async function appendUnstoredSuffix(stored, session) {
|
|
485
|
+
const suffix = session.snapshotEvents(SessionLogOffset(stored.storedCount));
|
|
486
|
+
if (suffix.length > 0)
|
|
487
|
+
await stored.handle.append(suffix);
|
|
488
|
+
stored.storedCount += suffix.length;
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Shared creation/resume transaction: build the agent over the prepared
|
|
492
|
+
* session, run unpublished setup, flush the unstored suffix, and publish both
|
|
493
|
+
* in order; the owned storage handle is drained (closed) on teardown. Kept as
|
|
494
|
+
* a module-level function (not a private method) because Cordis exposes the
|
|
495
|
+
* provider through a tracing proxy, which breaks hard-private (`#`) receivers.
|
|
496
|
+
*/
|
|
497
|
+
async function setupAndPublish(loopCtx, ownerCtx, id, agentOptions, setup, session, client, parentAgent, source, opts, stored, runtimeInfo) {
|
|
498
|
+
let detachSession;
|
|
499
|
+
let detachAgent;
|
|
500
|
+
let agent;
|
|
501
|
+
// Late-bound teardown the agent triggers itself after its idle window
|
|
502
|
+
// expires (see CodexAgent's idle exit) — defined only after publication.
|
|
503
|
+
let idleExit;
|
|
504
|
+
try {
|
|
505
|
+
// Build the agent shim over the session and the live client.
|
|
506
|
+
agent = new CodexAgent(loopCtx, id, agentOptions, session, client, () => idleExit?.(), runtimeInfo);
|
|
507
|
+
// Pin THIS session to the codex route. The global default-model setting is
|
|
508
|
+
// a best-effort push (the settings service may flush late, and a shared
|
|
509
|
+
// test home can hold another line's default), so the factory must not let
|
|
510
|
+
// the first prompt depend on it: `selectForNextRequest` is the same
|
|
511
|
+
// per-session mechanism `session/selectModel` uses (appends the log-only
|
|
512
|
+
// `model/selection` and caches the next request's route).
|
|
513
|
+
try {
|
|
514
|
+
const route = runtimeInfo?.routeContext(agentOptions.model);
|
|
515
|
+
if (route !== undefined) {
|
|
516
|
+
const selection = { provider: route.provider, model: route.model };
|
|
517
|
+
// Mirrors the upstream session-controller's `selectForNextRequest`:
|
|
518
|
+
// the durable log-only `model/selection` plus an agent-scoped
|
|
519
|
+
// `ModelSelectionRef` (same shape the controller installs) that the
|
|
520
|
+
// next prompt's route check reads. Synchronous on purpose — the first
|
|
521
|
+
// prompt must not depend on the global default's late flush.
|
|
522
|
+
let picked = selection;
|
|
523
|
+
installModelSelection(agent.ctx, {
|
|
524
|
+
get current() { return picked; },
|
|
525
|
+
set current(next) { picked = next; },
|
|
526
|
+
assembled: undefined,
|
|
527
|
+
});
|
|
528
|
+
// `model/selection` is contributed to `SessionEventMap` by the upstream
|
|
529
|
+
// session-controller package (declaration merging); this adapter does
|
|
530
|
+
// not depend on it, so the append goes through a narrow local cast.
|
|
531
|
+
session.append("model/selection", selection);
|
|
532
|
+
trace(`session route pinned: ${route.provider}/${route.model}`);
|
|
533
|
+
}
|
|
534
|
+
}
|
|
535
|
+
catch (error) {
|
|
536
|
+
// Fail-soft: the boot-time default-model push remains the fallback.
|
|
537
|
+
trace(`session route pin failed: ${String(error)}`);
|
|
538
|
+
}
|
|
539
|
+
// Session identity (the preset stamp) and the Codex system prompt commit
|
|
540
|
+
// on the first turn via CodexAgent's #bootstrapSessionIdentity /
|
|
541
|
+
// #emitSystemMessage: a freshly created session announces completely
|
|
542
|
+
// blank, so the backend takes zero part in the UI's new-session draft.
|
|
543
|
+
// Composition-only setup on the unpublished agent scope.
|
|
544
|
+
const commit = await setup?.(agent.ctx, agent);
|
|
545
|
+
// Publish: flush the unstored suffix (seed + setup-window events) through
|
|
546
|
+
// the owned handle, then enter both session and agent, announce in order,
|
|
547
|
+
// and signal session-start. The commit runs immediately before
|
|
548
|
+
// publication.
|
|
549
|
+
commit?.commit();
|
|
550
|
+
await appendUnstoredSuffix(stored, session);
|
|
551
|
+
if (opts.enterSession) {
|
|
552
|
+
detachSession = agent.ctx.sessions.enter(session);
|
|
553
|
+
agent.ctx.sessions.announce(session);
|
|
554
|
+
}
|
|
555
|
+
detachAgent = loopCtx.agents.enter(agent, parentAgent);
|
|
556
|
+
loopCtx.agents.announce(agent);
|
|
557
|
+
emitAgentEvent(loopCtx, agent, "agent/session-start", { source });
|
|
558
|
+
let disposed = false;
|
|
559
|
+
let unfollowOwner;
|
|
560
|
+
const dispose = async () => {
|
|
561
|
+
if (disposed)
|
|
562
|
+
return;
|
|
563
|
+
disposed = true;
|
|
564
|
+
trace(`dispose() called for agent ${id} (source=${source})`);
|
|
565
|
+
unfollowOwner?.();
|
|
566
|
+
await agent?.dispose();
|
|
567
|
+
detachAgent?.();
|
|
568
|
+
detachSession?.();
|
|
569
|
+
// Drain queued appends; a durability failure surfaces to the caller
|
|
570
|
+
// (the owner-unload and idle-exit paths log it).
|
|
571
|
+
await stored.handle.close();
|
|
572
|
+
};
|
|
573
|
+
idleExit = () => {
|
|
574
|
+
void dispose().catch((error) => {
|
|
575
|
+
loopCtx.logger.warn(`codex-provider: teardown of ${id} failed: ${String(error)}`);
|
|
576
|
+
});
|
|
577
|
+
};
|
|
578
|
+
// Follow the owner: a caller-fiber unload tears this agent down.
|
|
579
|
+
unfollowOwner = ownerCtx.effect(() => () => {
|
|
580
|
+
void dispose().catch((error) => {
|
|
581
|
+
loopCtx.logger.warn(`codex-provider: owner-unload teardown of ${id} failed: ${String(error)}`);
|
|
582
|
+
});
|
|
583
|
+
}, `codexProvider.lifecycle(${id})`);
|
|
584
|
+
return { agent, dispose };
|
|
585
|
+
}
|
|
586
|
+
catch (error) {
|
|
587
|
+
// Unwind the half-published transaction. A failure between `enter` and
|
|
588
|
+
// the announcements (e.g. a persistence listener rejecting the session)
|
|
589
|
+
// must not leave a live-but-dead entry the API resolver would serve.
|
|
590
|
+
trace(`setupAndPublish ${id} FAILED: ${String(error)}`);
|
|
591
|
+
detachAgent?.();
|
|
592
|
+
detachSession?.();
|
|
593
|
+
void agent?.dispose().catch(() => { });
|
|
594
|
+
client.close();
|
|
595
|
+
throw error;
|
|
596
|
+
}
|
|
597
|
+
finally {
|
|
598
|
+
// Release the preparation's per-id reservation on every path (mirrors
|
|
599
|
+
// the reference loop's unconditional dispose): the session it seeded is
|
|
600
|
+
// already published above, so a same-process re-resume of this id can
|
|
601
|
+
// prepare again instead of colliding with a leaked reservation.
|
|
602
|
+
opts.preparation?.[Symbol.dispose]();
|
|
603
|
+
}
|
|
604
|
+
}
|
|
605
|
+
export default CodexProvider;
|