super-dsh 0.1.9 → 0.1.10-e
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-claude/dist/adapter.js +106 -31
- package/agent-claude/dist/agent.js +154 -17
- package/agent-claude/dist/claude-client.js +84 -43
- package/agent-claude/dist/claude-launch.js +101 -0
- package/agent-claude/dist/claude-native-config.js +169 -0
- package/agent-claude/dist/claude-provider-gateway.js +340 -0
- package/agent-claude/dist/claude-providers.js +391 -0
- package/agent-claude/dist/claude-route.js +152 -0
- package/agent-claude/dist/index.js +214 -37
- package/agent-claude/dist/models.js +14 -0
- package/agent-claude/dist/sdk-runtime.js +320 -0
- package/agent-claude/package.json +6 -4
- 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 +1204 -93
- package/cordis.patch.yml +64 -0
- package/package.json +17 -8
- package/superd-hub/dist/client/agent-settings.d.ts +210 -0
- package/superd-hub/dist/client/agent-settings.js +300 -0
- package/superd-hub/dist/client/roster.js +6 -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/lib/client/index.js +1204 -93
- package/agent-codex/dist/models.js +0 -152
- package/agent-omp/dist/sidecar-client.js +0 -195
- /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
|
@@ -15,37 +15,162 @@
|
|
|
15
15
|
* (SDK env is REPLACE semantics — Task 1 spike fact, so the spread is
|
|
16
16
|
* mandatory) and carries NO `CODEX_HOME`: the CLI resolves its own native
|
|
17
17
|
* `~/.codex` (user ruling 2026-09-17 — never redirect the native app home at
|
|
18
|
-
* spawn; tests redirect via `process.env.CODEX_HOME`).
|
|
19
|
-
*
|
|
18
|
+
* spawn; tests redirect via `process.env.CODEX_HOME`).
|
|
19
|
+
*
|
|
20
|
+
* 2026-10-04 user ruling: this package declares NO SDK dependency — the whole
|
|
21
|
+
* runtime is fetched into the world home on the first real prompt
|
|
22
|
+
* (sdk-runtime.ts). The default factory therefore returns a zero-IO
|
|
23
|
+
* {@link DeferredCodex} that materializes the real SDK inside `ensureStarted()`
|
|
24
|
+
* (and, defensively, on the first `runStreamed`): constructing a client — which
|
|
25
|
+
* the provider does at session-create time — touches no disk and no network,
|
|
26
|
+
* and a session that never gets a prompt never fetches anything.
|
|
27
|
+
* {@link setCodexFactory} replaces that construction wholesale, so the unit
|
|
28
|
+
* suite stays green with the SDK absent.
|
|
20
29
|
*
|
|
21
30
|
* Wire events flow through `projectThreadEvent` (codex-events.ts) before
|
|
22
31
|
* reaching listeners, so Task 7's agent.ts switch is unchanged from omp.
|
|
23
32
|
*/
|
|
24
|
-
import {
|
|
25
|
-
import {
|
|
26
|
-
|
|
27
|
-
/**
|
|
33
|
+
import { loadCodexSdk } from "./sdk-runtime.js";
|
|
34
|
+
import { NATIVE_SDK_LAUNCH } from "./codex-sdk-launch.js";
|
|
35
|
+
import { projectThreadEvent } from "./codex-sdk-events.js";
|
|
36
|
+
/**
|
|
37
|
+
* The production handle: a stand-in that constructs the REAL SDK entry on first
|
|
38
|
+
* materialization. Deferring is what keeps session-create at zero IO while
|
|
39
|
+
* still letting the fetch happen exactly once, from `ensureStarted()`.
|
|
40
|
+
*/
|
|
41
|
+
class DeferredCodex {
|
|
42
|
+
#build;
|
|
43
|
+
#real = null;
|
|
44
|
+
constructor(build) {
|
|
45
|
+
this.#build = build;
|
|
46
|
+
}
|
|
47
|
+
/** Fetch (first time) + construct the real instance; resolves when usable. */
|
|
48
|
+
warm() {
|
|
49
|
+
return this.#realCodex().then(() => undefined);
|
|
50
|
+
}
|
|
51
|
+
startThread(options) {
|
|
52
|
+
return new DeferredThread(() => this.#realCodex().then((real) => real.startThread(options)));
|
|
53
|
+
}
|
|
54
|
+
resumeThread(id, options) {
|
|
55
|
+
return new DeferredThread(() => this.#realCodex().then((real) => real.resumeThread(id, options)));
|
|
56
|
+
}
|
|
57
|
+
#realCodex() {
|
|
58
|
+
if (this.#real === null) {
|
|
59
|
+
// A rejected bootstrap must not be cached: the runtime fetch heals on the
|
|
60
|
+
// next call (a failure leaves no marker), so a retried prompt can succeed.
|
|
61
|
+
this.#real = this.#build().catch((error) => {
|
|
62
|
+
this.#real = null;
|
|
63
|
+
throw error;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
return this.#real;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/** The thread half of {@link DeferredCodex}: materializes on first `runStreamed`. */
|
|
70
|
+
class DeferredThread {
|
|
71
|
+
#build;
|
|
72
|
+
#target = null;
|
|
73
|
+
#materialize = null;
|
|
74
|
+
constructor(build) {
|
|
75
|
+
this.#build = build;
|
|
76
|
+
}
|
|
77
|
+
get id() {
|
|
78
|
+
return this.#target?.id ?? null;
|
|
79
|
+
}
|
|
80
|
+
async runStreamed(input, turnOptions) {
|
|
81
|
+
// Local narrowing: the materializer resolves inside a callback, so TS
|
|
82
|
+
// cannot see the field become non-null across the await.
|
|
83
|
+
let target = this.#target;
|
|
84
|
+
if (target === null) {
|
|
85
|
+
this.#materialize ??= this.#build()
|
|
86
|
+
.then((resolved) => {
|
|
87
|
+
this.#target = resolved;
|
|
88
|
+
return resolved;
|
|
89
|
+
})
|
|
90
|
+
.catch((error) => {
|
|
91
|
+
this.#materialize = null;
|
|
92
|
+
throw error;
|
|
93
|
+
});
|
|
94
|
+
target = await this.#materialize;
|
|
95
|
+
}
|
|
96
|
+
return target.runStreamed(input, turnOptions);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
/**
|
|
100
|
+
* The default factory: the deferred handle wired to the durable runtime. A
|
|
101
|
+
* world home is mandatory on this path — deriving one from ambient env inside
|
|
102
|
+
* the client is the prod-home trap the adapter rules forbid (the provider owns
|
|
103
|
+
* the resolution and passes it in).
|
|
104
|
+
*/
|
|
105
|
+
const defaultCodexFactory = (options, worldHome) => {
|
|
106
|
+
if (worldHome === "") {
|
|
107
|
+
throw new Error("codex-sdk: the client needs the world home to fetch its SDK runtime into (the provider passes dshHomePath)");
|
|
108
|
+
}
|
|
109
|
+
return new DeferredCodex(async () => {
|
|
110
|
+
const sdk = await loadCodexSdk(worldHome);
|
|
111
|
+
// `options` already carries the provider's config overrides (when it has
|
|
112
|
+
// any) and the env the caller composed — the SDK is constructed exactly as
|
|
113
|
+
// the pinning launch asked, on first materialization.
|
|
114
|
+
return new sdk.Codex(options);
|
|
115
|
+
});
|
|
116
|
+
};
|
|
117
|
+
let codexFactory = defaultCodexFactory;
|
|
118
|
+
/** Replace the SDK construction (tests inject a fake: zero fetch, zero disk). */
|
|
28
119
|
export function setCodexFactory(factory) {
|
|
29
120
|
codexFactory = factory;
|
|
30
121
|
}
|
|
31
122
|
// ---------------------------------------------------------------------------
|
|
32
|
-
// Shared Codex
|
|
123
|
+
// Shared Codex instances (module-level, refcounted PER PROVIDER — omp sidecar
|
|
124
|
+
// pattern, 2026-10-04 multi-provider rework)
|
|
33
125
|
// ---------------------------------------------------------------------------
|
|
34
|
-
|
|
126
|
+
/**
|
|
127
|
+
* Registry key for "let the native `config.toml` decide". Also the key every
|
|
128
|
+
* client starts on: the route is not known until the first turn resolves it.
|
|
129
|
+
*/
|
|
130
|
+
const NATIVE_KEY = "native";
|
|
131
|
+
/**
|
|
132
|
+
* One entry per PROVIDER, not one for the process: a provider is a property of
|
|
133
|
+
* the config object the SDK is constructed with, so clients on different
|
|
134
|
+
* providers must not share an instance. Clients on the same provider still do —
|
|
135
|
+
* that is what keeps a many-session world at one SDK handle per provider.
|
|
136
|
+
*/
|
|
137
|
+
const sharedCodex = new Map();
|
|
35
138
|
let refcount = 0;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
139
|
+
/**
|
|
140
|
+
* Refcount one provider's handle, constructing it on first use. The construction
|
|
141
|
+
* is cheap by contract: the default factory returns a {@link DeferredCodex}
|
|
142
|
+
* (zero disk, zero network) and a test factory returns a plain fake. The REAL
|
|
143
|
+
* instance — and therefore the runtime fetch — is materialized later, from
|
|
144
|
+
* `ensureStarted()`.
|
|
145
|
+
*
|
|
146
|
+
* The first launch for a key wins. That is sound because the key IS the
|
|
147
|
+
* provider: two launches on one key carry the same provider table, the same
|
|
148
|
+
* catalog path and the same synthetic key name, so the handle they would build
|
|
149
|
+
* is the same handle.
|
|
150
|
+
*/
|
|
151
|
+
function acquire(key, worldHome, launch) {
|
|
152
|
+
let entry = sharedCodex.get(key);
|
|
153
|
+
if (entry === undefined) {
|
|
154
|
+
entry = {
|
|
155
|
+
codex: codexFactory({ env: { ...process.env }, ...(Object.keys(launch.config).length === 0 ? {} : { config: launch.config }) }, worldHome, launch),
|
|
156
|
+
refs: 0,
|
|
157
|
+
};
|
|
158
|
+
sharedCodex.set(key, entry);
|
|
39
159
|
}
|
|
160
|
+
entry.refs += 1;
|
|
40
161
|
refcount += 1;
|
|
41
|
-
return
|
|
162
|
+
return entry.codex;
|
|
42
163
|
}
|
|
43
|
-
function release() {
|
|
164
|
+
function release(key) {
|
|
165
|
+
const entry = sharedCodex.get(key);
|
|
166
|
+
if (entry === undefined)
|
|
167
|
+
return;
|
|
168
|
+
entry.refs -= 1;
|
|
44
169
|
refcount = Math.max(0, refcount - 1);
|
|
45
|
-
if (
|
|
46
|
-
sharedCodex
|
|
170
|
+
if (entry.refs <= 0)
|
|
171
|
+
sharedCodex.delete(key);
|
|
47
172
|
}
|
|
48
|
-
/** Test/observability hook: current
|
|
173
|
+
/** Test/observability hook: current handle refcount across providers (0 = none alive). */
|
|
49
174
|
export function codexClientRefCount() {
|
|
50
175
|
return refcount;
|
|
51
176
|
}
|
|
@@ -84,7 +209,22 @@ function parseSpawnArgs(args) {
|
|
|
84
209
|
}
|
|
85
210
|
// ---------------------------------------------------------------------------
|
|
86
211
|
export class CodexSdkClient {
|
|
212
|
+
/** The handle in force; swapped when the route moves to another provider. */
|
|
87
213
|
#codex;
|
|
214
|
+
/**
|
|
215
|
+
* The world home the SDK runtime is fetched into. Held (not just borrowed at
|
|
216
|
+
* spawn) because a provider switch constructs the NEXT handle from it.
|
|
217
|
+
*/
|
|
218
|
+
#worldHome;
|
|
219
|
+
/**
|
|
220
|
+
* The launch the next turn must run under, and the registry key it maps to.
|
|
221
|
+
* `applyLaunch` is the only writer; both start native, because the route is
|
|
222
|
+
* not known until the first turn resolves it.
|
|
223
|
+
*/
|
|
224
|
+
#launch = NATIVE_SDK_LAUNCH;
|
|
225
|
+
#launchKey = NATIVE_KEY;
|
|
226
|
+
/** The key `#codex` is currently a handle of (what `close()` must release). */
|
|
227
|
+
#appliedProvider = NATIVE_KEY;
|
|
88
228
|
#listeners = new Set();
|
|
89
229
|
#failureListeners = new Set();
|
|
90
230
|
#threadOptions;
|
|
@@ -102,8 +242,11 @@ export class CodexSdkClient {
|
|
|
102
242
|
#lastUsage = null;
|
|
103
243
|
/** Bumped by newSession()/close() to orphan runs started on an earlier thread. */
|
|
104
244
|
#sessionEpoch = 0;
|
|
105
|
-
|
|
245
|
+
/** One-shot latch for `ensureStarted()` — the runtime fetch happens there. */
|
|
246
|
+
#startPromise = null;
|
|
247
|
+
constructor(codex, cwd, approvalMode, resumeThreadId, worldHome = "") {
|
|
106
248
|
this.#codex = codex;
|
|
249
|
+
this.#worldHome = worldHome;
|
|
107
250
|
this.#approvalMode = approvalMode;
|
|
108
251
|
// skipGitRepoCheck: adapters spawn from arbitrary test/worktree cwds (Task 1 spike precedent).
|
|
109
252
|
// Launch-only approval mapping (Task 7): present only when the provider
|
|
@@ -119,17 +262,25 @@ export class CodexSdkClient {
|
|
|
119
262
|
resumeThreadId === undefined ? codex.startThread(this.#threadOptions) : codex.resumeThread(resumeThreadId, this.#threadOptions);
|
|
120
263
|
}
|
|
121
264
|
/**
|
|
122
|
-
*
|
|
265
|
+
* Construct a client on the shared handle. ZERO IO by contract: the default
|
|
266
|
+
* factory hands back a deferred SDK stand-in, so neither the runtime fetch
|
|
267
|
+
* nor the real SDK construction happens here — `ensureStarted()` owns both.
|
|
268
|
+
* A session that is created and never prompted therefore fetches nothing.
|
|
123
269
|
* Mirrors `OmpSdkClient.spawn(args, cwd)`.
|
|
124
270
|
*/
|
|
125
|
-
static async spawn(args = [], cwd) {
|
|
271
|
+
static async spawn(args = [], cwd, worldHome) {
|
|
126
272
|
const { approvalMode, resumeThreadId } = parseSpawnArgs(args);
|
|
127
|
-
|
|
273
|
+
// Born on the native key: the route is not known until the first turn
|
|
274
|
+
// resolves it, and `applyLaunch` moves this client onto the provider it
|
|
275
|
+
// names. The handle is deferred either way, so nothing is constructed,
|
|
276
|
+
// fetched or even decided by this call.
|
|
277
|
+
const home = worldHome ?? "";
|
|
278
|
+
const codex = acquire(NATIVE_KEY, home, NATIVE_SDK_LAUNCH);
|
|
128
279
|
try {
|
|
129
|
-
return new CodexSdkClient(codex, cwd, approvalMode, resumeThreadId);
|
|
280
|
+
return new CodexSdkClient(codex, cwd, approvalMode, resumeThreadId, home);
|
|
130
281
|
}
|
|
131
282
|
catch (error) {
|
|
132
|
-
release();
|
|
283
|
+
release(NATIVE_KEY);
|
|
133
284
|
throw error;
|
|
134
285
|
}
|
|
135
286
|
}
|
|
@@ -273,7 +424,7 @@ export class CodexSdkClient {
|
|
|
273
424
|
async setModel(provider, modelId) {
|
|
274
425
|
this.#ensureUsable();
|
|
275
426
|
void provider;
|
|
276
|
-
this.#pendingModel = modelId;
|
|
427
|
+
this.#pendingModel = this.#modelOverride(modelId);
|
|
277
428
|
return ok("set_model");
|
|
278
429
|
}
|
|
279
430
|
/** Abort the current streaming run (AbortController per run, turn-level signal). */
|
|
@@ -300,9 +451,27 @@ export class CodexSdkClient {
|
|
|
300
451
|
this.#appliedModel = null; // a pending model still applies at the next run
|
|
301
452
|
return ok("new_session");
|
|
302
453
|
}
|
|
303
|
-
/**
|
|
304
|
-
|
|
305
|
-
|
|
454
|
+
/**
|
|
455
|
+
* Materialize the durable SDK runtime (fetch on first call, then free) and
|
|
456
|
+
* the real SDK instance behind it. This is the ONLY place the production path
|
|
457
|
+
* may touch the network — `create()`/`spawn()` stay zero-IO.
|
|
458
|
+
*
|
|
459
|
+
* A failure is NOT memoized: the runtime bootstrap leaves no marker and heals
|
|
460
|
+
* on the next call, so a later prompt in the same session retries instead of
|
|
461
|
+
* being permanently bricked.
|
|
462
|
+
*/
|
|
463
|
+
async ensureStarted() {
|
|
464
|
+
if (this.#startPromise === null) {
|
|
465
|
+
this.#startPromise = (async () => {
|
|
466
|
+
const warm = this.#codex.warm;
|
|
467
|
+
if (typeof warm === "function")
|
|
468
|
+
await warm.call(this.#codex);
|
|
469
|
+
})().catch((error) => {
|
|
470
|
+
this.#startPromise = null;
|
|
471
|
+
throw error;
|
|
472
|
+
});
|
|
473
|
+
}
|
|
474
|
+
return this.#startPromise;
|
|
306
475
|
}
|
|
307
476
|
/**
|
|
308
477
|
* The current Codex thread id, exactly as the SDK thread exposes it (null
|
|
@@ -312,7 +481,12 @@ export class CodexSdkClient {
|
|
|
312
481
|
get threadId() {
|
|
313
482
|
return this.#thread.id;
|
|
314
483
|
}
|
|
315
|
-
/**
|
|
484
|
+
/**
|
|
485
|
+
* Lazy-surface parity: a real client is born live (see OmpAgentRpc.spawned).
|
|
486
|
+
* Kept `true` on the deferred path on purpose: the client's whole surface is
|
|
487
|
+
* usable from construction, and `ensureStarted()` only warms the runtime —
|
|
488
|
+
* this flag was never the "has the fetch happened" bit.
|
|
489
|
+
*/
|
|
316
490
|
get spawned() {
|
|
317
491
|
return true;
|
|
318
492
|
}
|
|
@@ -323,7 +497,7 @@ export class CodexSdkClient {
|
|
|
323
497
|
this.#closed = true;
|
|
324
498
|
this.#sessionEpoch += 1; // a settled run must not drain the queue after close
|
|
325
499
|
this.#currentAbort?.abort();
|
|
326
|
-
release();
|
|
500
|
+
release(this.#appliedProvider);
|
|
327
501
|
this.#fail(new Error("codex sdk client closed"));
|
|
328
502
|
}
|
|
329
503
|
// -------------------------------------------------------------------------
|
|
@@ -361,7 +535,7 @@ export class CodexSdkClient {
|
|
|
361
535
|
const controller = new AbortController();
|
|
362
536
|
this.#currentAbort = controller;
|
|
363
537
|
try {
|
|
364
|
-
this.#
|
|
538
|
+
this.#reconcileRoute();
|
|
365
539
|
const streamed = await this.#thread.runStreamed(message, { signal: controller.signal });
|
|
366
540
|
for await (const raw of streamed.events) {
|
|
367
541
|
const record = asRecord(raw);
|
|
@@ -400,17 +574,64 @@ export class CodexSdkClient {
|
|
|
400
574
|
}
|
|
401
575
|
}
|
|
402
576
|
}
|
|
403
|
-
/**
|
|
404
|
-
|
|
405
|
-
|
|
577
|
+
/**
|
|
578
|
+
* Put the resolved route in force for the next run: swap the handle if the
|
|
579
|
+
* PROVIDER moved, then rebuild the thread object against it.
|
|
580
|
+
*
|
|
581
|
+
* Cheaper than the app-server line's equivalent by construction. There, a
|
|
582
|
+
* provider is an argv-level property of a LONG-LIVED child, so a switch means
|
|
583
|
+
* killing it and re-attaching the thread to a new one (`reattach`). Here the
|
|
584
|
+
* SDK spawns `codex exec` (or `codex exec resume <id>`) PER RUN, so the
|
|
585
|
+
* provider is a property of the handle we are about to ask for a run — the
|
|
586
|
+
* switch is just "use the other handle from now on", and the thread keeps its
|
|
587
|
+
* identity because the id is a rollout on disk that both handles read.
|
|
588
|
+
*
|
|
589
|
+
* That also means a mid-turn switch lands on the next turn for free, exactly
|
|
590
|
+
* as the app-server line promises: the user message already dispatched keeps
|
|
591
|
+
* the handle it started with, and the next `#runTurn` re-reads the route.
|
|
592
|
+
*/
|
|
593
|
+
#reconcileRoute() {
|
|
594
|
+
const key = this.#launchKey;
|
|
595
|
+
const providerMoved = key !== this.#appliedProvider;
|
|
596
|
+
if (!providerMoved && this.#pendingModel === this.#appliedModel)
|
|
406
597
|
return;
|
|
598
|
+
if (providerMoved) {
|
|
599
|
+
this.#codex = acquire(key, this.#worldHome, this.#launch);
|
|
600
|
+
release(this.#appliedProvider);
|
|
601
|
+
this.#appliedProvider = key;
|
|
602
|
+
}
|
|
407
603
|
const model = this.#pendingModel;
|
|
408
604
|
const options = model === null ? { ...this.#threadOptions } : { ...this.#threadOptions, model };
|
|
605
|
+
// Resume by id whenever the thread has one: that is what carries the
|
|
606
|
+
// conversation across BOTH a model change and a provider change.
|
|
409
607
|
const currentId = this.#thread.id;
|
|
410
608
|
this.#thread =
|
|
411
609
|
currentId !== null ? this.#codex.resumeThread(currentId, options) : this.#codex.startThread(options);
|
|
412
610
|
this.#appliedModel = model;
|
|
413
611
|
}
|
|
612
|
+
/**
|
|
613
|
+
* Pin the provider the NEXT run must use (2026-10-04 multi-provider rework).
|
|
614
|
+
*
|
|
615
|
+
* Called by the agent at turn open with the launch it resolved, so the client
|
|
616
|
+
* never has to know what a provider IS — it is handed a config and an env and
|
|
617
|
+
* runs on them. `providerId` is `null` for "let the native `config.toml`
|
|
618
|
+
* decide", which is what a session pinned to the retired single `codex` route
|
|
619
|
+
* resolves to.
|
|
620
|
+
*/
|
|
621
|
+
applyRoute(launch, providerId, model) {
|
|
622
|
+
this.#launch = launch;
|
|
623
|
+
this.#launchKey = providerId ?? NATIVE_KEY;
|
|
624
|
+
this.#pendingModel = this.#modelOverride(model);
|
|
625
|
+
}
|
|
626
|
+
/**
|
|
627
|
+
* An empty model id means "pin no model", not "pin the empty model": the
|
|
628
|
+
* native `config.toml` then decides, which is what a route the adapter cannot
|
|
629
|
+
* resolve falls back to. Letting `model=""` through would emit
|
|
630
|
+
* `--config model=""` and codex would reject every turn.
|
|
631
|
+
*/
|
|
632
|
+
#modelOverride(model) {
|
|
633
|
+
return model === "" ? null : model;
|
|
634
|
+
}
|
|
414
635
|
/** Mirror wire events into the getMessages accumulator (best effort). */
|
|
415
636
|
#mirror(wire) {
|
|
416
637
|
if (wire.type === "message_end") {
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codex-sdk-launch.ts — how a provider reaches the in-process Codex SDK
|
|
3
|
+
* (2026-10-04 multi-provider rework).
|
|
4
|
+
*
|
|
5
|
+
* This is the SDK half of `agent-codex/codex-launch.ts`. That module turns a
|
|
6
|
+
* provider into argv for a `codex app-server` child; this one turns the same
|
|
7
|
+
* provider into the structured object the SDK's `CodexOptions.config` takes —
|
|
8
|
+
* and underneath they are the same mechanism. The SDK's own type docs say it
|
|
9
|
+
* "flatten[s] it into dotted paths and serialize[s] values as TOML literals so
|
|
10
|
+
* they are compatible with the CLI's `--config` parsing", which is exactly the
|
|
11
|
+
* `--config key=value` channel the app-server line drives by hand. A provider
|
|
12
|
+
* therefore costs nothing on disk on this path either, and is scoped to the
|
|
13
|
+
* process that builds the config, which is what a per-session provider choice
|
|
14
|
+
* needs.
|
|
15
|
+
*
|
|
16
|
+
* The one transport difference is env. A spawned app-server child inherits the
|
|
17
|
+
* process environment, so `codex-launch.ts` hands back ADDITIONS and its caller
|
|
18
|
+
* spreads them (`env: { ...process.env, ...launch.env }`). The SDK's `env` is
|
|
19
|
+
* REPLACE semantics — "When provided, the SDK will not inherit variables from
|
|
20
|
+
* `process.env`" — so the caller must spread the same way before constructing
|
|
21
|
+
* `Codex`. This module still hands back additions only: composing the full map
|
|
22
|
+
* belongs to the caller, and keeping the addition set small is what makes the
|
|
23
|
+
* synthetic-key branch legible in tests.
|
|
24
|
+
*
|
|
25
|
+
* The measured constraints `codex-launch.ts` encodes carry over unchanged (see
|
|
26
|
+
* `docs/test-reports/2026-10-03-codex-provider-model-switch-probe.md`):
|
|
27
|
+
*
|
|
28
|
+
* - a HYPHENATED id is fine in a dotted path (`model_providers.zhipu-cn.…`),
|
|
29
|
+
* so native table names survive verbatim as provider ids;
|
|
30
|
+
* - `model_catalog_json` is resolved against the process CWD when it arrives
|
|
31
|
+
* as an override, so the path is always absolute;
|
|
32
|
+
* - `env_key` is the one auth shape that never consults the auth manager, so a
|
|
33
|
+
* stored API key rides the child's environment under a synthetic variable
|
|
34
|
+
* name. That is also what makes a keyed provider usable behind a ChatGPT
|
|
35
|
+
* login, where `requires_openai_auth` is silently shadowed.
|
|
36
|
+
*
|
|
37
|
+
* @module agent-codex-sdk/codex-sdk-launch
|
|
38
|
+
*/
|
|
39
|
+
import { writeProviderCatalog } from "./codex-native-config.js";
|
|
40
|
+
/** The environment variable prefix for a key this adapter injects itself. */
|
|
41
|
+
const SYNTHETIC_KEY_PREFIX = "SUPERD_CODEX_KEY_";
|
|
42
|
+
/** The launch for "no provider injection at all" (the native config applies). */
|
|
43
|
+
export const NATIVE_SDK_LAUNCH = { config: {}, env: {}, providerId: "" };
|
|
44
|
+
/** The environment variable a stored API key is handed to the child as. */
|
|
45
|
+
export function syntheticKeyName(providerId) {
|
|
46
|
+
return `${SYNTHETIC_KEY_PREFIX}${providerId.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The launch that pins one provider for the life of an SDK process.
|
|
50
|
+
*
|
|
51
|
+
* @param worldHome - the world's DSH home (our per-provider catalogs live there).
|
|
52
|
+
* @param codexHome - the native codex home; READ ONLY (catalog template material).
|
|
53
|
+
* @param provider - the provider the SDK must run on.
|
|
54
|
+
* @returns the structured config, the environment additions, and the provider id they pin.
|
|
55
|
+
*/
|
|
56
|
+
export function providerSdkLaunch(worldHome, codexHome, provider) {
|
|
57
|
+
// The SDK serializes these values as TOML literals itself, so unlike the argv
|
|
58
|
+
// line there is no hand-quoting here: a bare object is the whole override.
|
|
59
|
+
const table = {
|
|
60
|
+
name: provider.label,
|
|
61
|
+
base_url: provider.baseUrl,
|
|
62
|
+
wire_api: provider.protocol,
|
|
63
|
+
};
|
|
64
|
+
const env = {};
|
|
65
|
+
if (provider.auth.kind === "env") {
|
|
66
|
+
table["env_key"] = provider.auth.name;
|
|
67
|
+
}
|
|
68
|
+
else if (provider.auth.value !== "") {
|
|
69
|
+
// A key we hold: never touch auth.json, never go through the auth manager.
|
|
70
|
+
const name = syntheticKeyName(provider.id);
|
|
71
|
+
table["env_key"] = name;
|
|
72
|
+
env[name] = provider.auth.value;
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
// The seeded shape — the runtime's own auth.json already holds this key.
|
|
76
|
+
table["requires_openai_auth"] = true;
|
|
77
|
+
}
|
|
78
|
+
// Top-level defaults for this process, so a thread that does not restate the
|
|
79
|
+
// provider still lands on the right one. `model_providers` is nested rather
|
|
80
|
+
// than pre-dotted: the SDK owns the flattening, and a nested table is what
|
|
81
|
+
// its `CodexConfigObject` type describes.
|
|
82
|
+
const config = {
|
|
83
|
+
model_providers: { [provider.id]: table },
|
|
84
|
+
model_provider: provider.id,
|
|
85
|
+
};
|
|
86
|
+
const catalog = writeProviderCatalog(worldHome, codexHome, provider);
|
|
87
|
+
// Absolute on purpose: codex resolves this override against the process cwd
|
|
88
|
+
// (`writeProviderCatalog` returns it under the absolute world home).
|
|
89
|
+
if (catalog !== undefined)
|
|
90
|
+
config["model_catalog_json"] = catalog;
|
|
91
|
+
return { config, env, providerId: provider.id };
|
|
92
|
+
}
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Codex home resolution + the rollout-HEAD metadata reader.
|
|
3
|
+
*
|
|
4
|
+
* The listing/replay scanner that used to live here is GONE (2026-09-16): the
|
|
5
|
+
* DSH session log is now the session authority (upstream `sessionPersistence`
|
|
6
|
+
* owns list/read/replay), so the adapter no longer derives session ids or
|
|
7
|
+
* transcripts from Codex's rollout store. What remains is:
|
|
8
|
+
*
|
|
9
|
+
* - home resolution (native `~/.codex`, never redirected; `CODEX_HOME` is the
|
|
10
|
+
* test/operator override), and
|
|
11
|
+
* - {@link readRolloutHead}: a bounded, memoized reader of ONE rollout's
|
|
12
|
+
* FIRST line (`session_meta`), used only to recover the Codex system
|
|
13
|
+
* prompt (`payload.base_instructions`) and the recorded cwd. It never
|
|
14
|
+
* parses the transcript and never throws.
|
|
15
|
+
*
|
|
16
|
+
* Single-direction valve (spec S7): this module NEVER writes under the codex
|
|
17
|
+
* home; the rollout stays Codex's transcript of record.
|
|
18
|
+
*/
|
|
19
|
+
import { closeSync, openSync, readdirSync, readSync, statSync } from "node:fs";
|
|
20
|
+
import { homedir } from "node:os";
|
|
21
|
+
import { join, resolve } from "node:path";
|
|
22
|
+
/**
|
|
23
|
+
* Resolve the Codex CLI's app-data home — the NATIVE `~/.codex` (user ruling
|
|
24
|
+
* 2026-09-17, pi precedent): the installed app keeps its own home, so the
|
|
25
|
+
* adapter never redirects it when spawning the SDK/CLI and never seeds config
|
|
26
|
+
* into it. Precedence: `CODEX_HOME` (tests / operator override), then the
|
|
27
|
+
* native default.
|
|
28
|
+
*
|
|
29
|
+
* The world's DSH home (`<dshHome>/agents/codex-sdk`) is NOT an input here anymore
|
|
30
|
+
* — it anchors adapter-owned DSH state only (session-map.ts). A home without
|
|
31
|
+
* `config.toml` would make the CLI fall back to OpenAI's own endpoint
|
|
32
|
+
* (region-blocked here), but that is the operator's native home: provisioning
|
|
33
|
+
* it is the operator's business (test helper: scripts/setup-codex-home.mjs).
|
|
34
|
+
*/
|
|
35
|
+
export function resolveCodexHome() {
|
|
36
|
+
if (process.env.CODEX_HOME !== undefined) {
|
|
37
|
+
const env = resolve(process.env.CODEX_HOME);
|
|
38
|
+
return env;
|
|
39
|
+
}
|
|
40
|
+
const native = join(homedir(), ".codex");
|
|
41
|
+
return native;
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Take an explicit codex home as-is (prod-guarded) — for callers that already
|
|
45
|
+
* hold the final path (models.ts, the spike); distinct from
|
|
46
|
+
* {@link resolveCodexHome}, which resolves the native default.
|
|
47
|
+
*/
|
|
48
|
+
export function useCodexHome(path) {
|
|
49
|
+
const resolved = resolve(path);
|
|
50
|
+
return resolved;
|
|
51
|
+
}
|
|
52
|
+
/** Bounded read budget: `session_meta` is the FIRST line, so this is generous. */
|
|
53
|
+
const HEAD_BYTES = 64 * 1024;
|
|
54
|
+
/** Memoized heads by thread id — the head line is immutable once written. */
|
|
55
|
+
const headCache = new Map();
|
|
56
|
+
/**
|
|
57
|
+
* Locate one thread's rollout file under `<codexHome>/sessions/<Y>/<M>/<D>/`.
|
|
58
|
+
* The rollout file name embeds the thread id, so presence is the match test;
|
|
59
|
+
* a miss is retried (a thread's rollout may not exist yet at first call).
|
|
60
|
+
*/
|
|
61
|
+
function findRolloutFile(codexHome, threadId) {
|
|
62
|
+
const root = join(codexHome, "sessions");
|
|
63
|
+
try {
|
|
64
|
+
for (const year of readdirSync(root, { withFileTypes: true })) {
|
|
65
|
+
if (!year.isDirectory())
|
|
66
|
+
continue;
|
|
67
|
+
for (const month of readdirSync(join(root, year.name), { withFileTypes: true })) {
|
|
68
|
+
if (!month.isDirectory())
|
|
69
|
+
continue;
|
|
70
|
+
for (const day of readdirSync(join(root, year.name, month.name), { withFileTypes: true })) {
|
|
71
|
+
if (!day.isDirectory())
|
|
72
|
+
continue;
|
|
73
|
+
const dir = join(root, year.name, month.name, day.name);
|
|
74
|
+
for (const file of readdirSync(dir)) {
|
|
75
|
+
if (file.endsWith(".jsonl") && file.includes(threadId))
|
|
76
|
+
return join(dir, file);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
return undefined;
|
|
84
|
+
}
|
|
85
|
+
return undefined;
|
|
86
|
+
}
|
|
87
|
+
/** Read (at most) the first line of a file; fail-soft. */
|
|
88
|
+
function readFirstLine(path) {
|
|
89
|
+
let fd;
|
|
90
|
+
try {
|
|
91
|
+
fd = openSync(path, "r");
|
|
92
|
+
}
|
|
93
|
+
catch {
|
|
94
|
+
return undefined;
|
|
95
|
+
}
|
|
96
|
+
try {
|
|
97
|
+
const chunk = Buffer.allocUnsafe(HEAD_BYTES);
|
|
98
|
+
const read = readSync(fd, chunk, 0, chunk.length, 0);
|
|
99
|
+
if (read <= 0)
|
|
100
|
+
return undefined;
|
|
101
|
+
const text = chunk.subarray(0, read).toString("utf8");
|
|
102
|
+
const end = text.indexOf("\n");
|
|
103
|
+
return end === -1 ? text : text.slice(0, end);
|
|
104
|
+
}
|
|
105
|
+
catch {
|
|
106
|
+
return undefined;
|
|
107
|
+
}
|
|
108
|
+
finally {
|
|
109
|
+
closeSync(fd);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Read the `session_meta` head of the rollout belonging to `threadId`:
|
|
114
|
+
* the Codex system prompt and the recorded cwd. Memoized per thread id (the
|
|
115
|
+
* head line never changes); fail-soft — an unknown thread, an unwritten
|
|
116
|
+
* rollout, or a malformed head all yield `undefined`.
|
|
117
|
+
*/
|
|
118
|
+
/**
|
|
119
|
+
* Codex writes `payload.base_instructions` either as a plain string or as an
|
|
120
|
+
* object `{ text }` (observed 2026-09-16 on the SDK line: the object form is
|
|
121
|
+
* what the vendored binary emits today). Accept both; anything else is absent.
|
|
122
|
+
*/
|
|
123
|
+
function baseInstructionsText(value) {
|
|
124
|
+
if (typeof value === "string")
|
|
125
|
+
return value.trim() === "" ? undefined : value;
|
|
126
|
+
if (value !== null && typeof value === "object") {
|
|
127
|
+
const text = value["text"];
|
|
128
|
+
if (typeof text === "string" && text.trim() !== "")
|
|
129
|
+
return text;
|
|
130
|
+
}
|
|
131
|
+
return undefined;
|
|
132
|
+
}
|
|
133
|
+
export function readRolloutHead(codexHome, threadId) {
|
|
134
|
+
const cached = headCache.get(threadId);
|
|
135
|
+
if (cached !== undefined)
|
|
136
|
+
return cached;
|
|
137
|
+
const file = findRolloutFile(codexHome, threadId);
|
|
138
|
+
if (file === undefined)
|
|
139
|
+
return undefined;
|
|
140
|
+
// Cheap existence/size gate before paying for a read.
|
|
141
|
+
try {
|
|
142
|
+
if (!statSync(file).isFile())
|
|
143
|
+
return undefined;
|
|
144
|
+
}
|
|
145
|
+
catch {
|
|
146
|
+
return undefined;
|
|
147
|
+
}
|
|
148
|
+
const line = readFirstLine(file);
|
|
149
|
+
if (line === undefined)
|
|
150
|
+
return undefined;
|
|
151
|
+
let record;
|
|
152
|
+
try {
|
|
153
|
+
record = JSON.parse(line);
|
|
154
|
+
}
|
|
155
|
+
catch {
|
|
156
|
+
return undefined;
|
|
157
|
+
}
|
|
158
|
+
if (record === null || typeof record !== "object")
|
|
159
|
+
return undefined;
|
|
160
|
+
const rec = record;
|
|
161
|
+
if (rec["type"] !== "session_meta")
|
|
162
|
+
return undefined;
|
|
163
|
+
const payload = rec["payload"];
|
|
164
|
+
if (payload === null || typeof payload !== "object")
|
|
165
|
+
return undefined;
|
|
166
|
+
const meta = payload;
|
|
167
|
+
const head = {
|
|
168
|
+
...(typeof meta["id"] === "string" && meta["id"] !== "" ? { threadId: meta["id"] } : {}),
|
|
169
|
+
...(typeof meta["cwd"] === "string" && meta["cwd"] !== "" ? { cwd: meta["cwd"] } : {}),
|
|
170
|
+
...(baseInstructionsText(meta["base_instructions"]) === undefined
|
|
171
|
+
? {}
|
|
172
|
+
: { baseInstructions: baseInstructionsText(meta["base_instructions"]) }),
|
|
173
|
+
};
|
|
174
|
+
headCache.set(threadId, head);
|
|
175
|
+
return head;
|
|
176
|
+
}
|
|
177
|
+
/** Test hygiene: drop memoized heads. */
|
|
178
|
+
export function clearRolloutHeadCache() {
|
|
179
|
+
headCache.clear();
|
|
180
|
+
}
|