@khorsheed/dsh-local-agent-codex 0.1.0-rc.6

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.
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Codex harness instantiation of the local-agent family. Registers the
3
+ * `codex` harness into the core registry: scoped home under the shared homes
4
+ * root (`CODEX_HOME`), device-code login through `codex login --device-auth`
5
+ * (prompt captured from stdout), and rollout-file session records.
6
+ * Delegation mounts through the `codex-local` provider (one-shot
7
+ * `codex exec` under the scoped home); the bundle patch's
8
+ * `tool-subagent-codex-local` row puts the tool on the profile root, so
9
+ * every agent preset can delegate.
10
+ *
11
+ * @module @khorsheed/dsh-local-agent-codex
12
+ */
13
+ import type { Context } from '@deepseek-ai/cordis';
14
+ import z from '@deepseek-ai/schemastery';
15
+ /** Stable Cordis plugin name; the bundle patch row id. */
16
+ export declare const name = "local-agent-codex";
17
+ /**
18
+ * Boot delay before the model catalog's eager warmup probe fires. The probe
19
+ * spawns a `codex app-server` — cheap, but not free — so it waits out the
20
+ * boot storm instead of competing with it, and the FIRST settings-card open
21
+ * usually hits a warm cache. A card opened before the timer fires simply
22
+ * triggers the probe itself (the read path's lazy kick), so the delay never
23
+ * costs correctness. Kept short: a setting visible within seconds of boot.
24
+ */
25
+ export declare const CATALOG_WARMUP_DELAY_MS = 1500;
26
+ /** Services required before the harness can register. */
27
+ export declare const inject: string[];
28
+ /** Plugin config: the sandbox mode fresh delegations default to, plus the live driver. */
29
+ export interface Config {
30
+ /** Codex sandbox policy for `codex exec` / app-server threads; defaults to workspace-write. */
31
+ sandbox?: 'read-only' | 'workspace-write' | 'danger-full-access';
32
+ /**
33
+ * The model every delegation round starts the CLI with (`codex exec -m
34
+ * <model>`, and `-c model=…` for the resident app-server). Absent — the
35
+ * default — passes NO model flag at all: the scoped `config.toml`'s own
36
+ * `model`, or codex's built-in default with none, decides exactly as it did
37
+ * before this key existed. The settings card writes the same key, so a
38
+ * change applies to the next round without a reload.
39
+ */
40
+ model?: string;
41
+ /**
42
+ * Live driver: keep one resident `codex app-server` process per member and
43
+ * drive turns over the app-server wire (runtime-level interrupt, push-mode
44
+ * mirroring) instead of one `codex exec` process per round. Default off;
45
+ * the exec one-shot stays the fallback whenever the channel cannot come up.
46
+ */
47
+ live?: boolean;
48
+ /** Idle lifetime of an unused resident runtime before reclaim. */
49
+ liveIdleMs?: number;
50
+ /** @deprecated Accepted for old profiles; live output is always incremental. */
51
+ liveMirrorGranularity?: 'event' | 'token';
52
+ /** Model identifiers the settings card saved before (settings field). */
53
+ recentModels?: readonly string[];
54
+ }
55
+ export declare const Config: z;
56
+ /** The sandbox policy a fresh delegation defaults to. */
57
+ export declare const DEFAULT_SANDBOX: NonNullable<Config['sandbox']>;
58
+ /**
59
+ * Settings namespace for the settings-page card. The Cordis config feeds the
60
+ * composition `base` layer, so a field absent from the user layer inherits the
61
+ * YAML value — the card only ever stores deliberate overrides.
62
+ */
63
+ export declare const CODEX_SETTINGS_NAMESPACE = "local-agent-codex";
64
+ /**
65
+ * Register the Codex harness into the local-agent registry.
66
+ * @param ctx - plugin context carrying the registry.
67
+ * @param config - plugin config; `sandbox` selects the `codex exec` policy.
68
+ */
69
+ export declare function apply(ctx: Context, config: Config): void;
70
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,272 @@
1
+ /**
2
+ * Codex harness instantiation of the local-agent family. Registers the
3
+ * `codex` harness into the core registry: scoped home under the shared homes
4
+ * root (`CODEX_HOME`), device-code login through `codex login --device-auth`
5
+ * (prompt captured from stdout), and rollout-file session records.
6
+ * Delegation mounts through the `codex-local` provider (one-shot
7
+ * `codex exec` under the scoped home); the bundle patch's
8
+ * `tool-subagent-codex-local` row puts the tool on the profile root, so
9
+ * every agent preset can delegate.
10
+ *
11
+ * @module @khorsheed/dsh-local-agent-codex
12
+ */
13
+ import z from '@deepseek-ai/schemastery';
14
+ import { settingsFace, vol } from '@khorsheed/dsh-local-agent';
15
+ import { endpointHost } from '@khorsheed/dsh-local-agent/types';
16
+ import { CodexCliProvider, codexCliVersion } from './codex-cli-provider.js';
17
+ import { DEFAULT_LIVE_IDLE_MS } from './live-driver.js';
18
+ import { LiveDriverSwitch } from './live-switch.js';
19
+ import { CodexModelBroker } from './model-broker.js';
20
+ import { CodexModelCatalog } from './model-catalog.js';
21
+ import { codexAuthenticated, listCodexSessions } from './records.js';
22
+ import { codexCredentialStamp, codexLogout, provisionCodexConfig, readCodexBaseUrl, readCodexModel, readCodexReasoningEffort } from './provision.js';
23
+ /** Stable Cordis plugin name; the bundle patch row id. */
24
+ export const name = 'local-agent-codex';
25
+ /**
26
+ * Boot delay before the model catalog's eager warmup probe fires. The probe
27
+ * spawns a `codex app-server` — cheap, but not free — so it waits out the
28
+ * boot storm instead of competing with it, and the FIRST settings-card open
29
+ * usually hits a warm cache. A card opened before the timer fires simply
30
+ * triggers the probe itself (the read path's lazy kick), so the delay never
31
+ * costs correctness. Kept short: a setting visible within seconds of boot.
32
+ */
33
+ export const CATALOG_WARMUP_DELAY_MS = 1_500;
34
+ /** Services required before the harness can register. */
35
+ export const inject = ['localAgent', 'subagents', 'subprocess', 'settings'];
36
+ /**
37
+ * The settings fields (`live`, `liveMirrorGranularity`, `model`,
38
+ * `recentModels`) ride the plugin Config, marked `.volatile()` where the
39
+ * host's schemastery has it (3.18.4+, the rc.1 line — probed, never sniffed):
40
+ * there the SettingsForms writes hot-track the running fiber's Volatile
41
+ * references without a remount, and the official one-shot import moves an old
42
+ * settings.yaml section into this row's config. On 0.1.5 the call is absent,
43
+ * the fields stay plain config keys (a semantic superset — the profile row
44
+ * can now preset them too), and the legacy settings namespace below carries
45
+ * the user layer exactly as before. `model` deliberately carries NO default:
46
+ * an unset key must resolve to undefined, which is what keeps the pre-key
47
+ * behavior byte-identical.
48
+ */
49
+ const SETTINGS_FIELDS = {
50
+ live: z.boolean().default(false),
51
+ liveMirrorGranularity: z.union([z.const('event'), z.const('token')]).default('token'),
52
+ model: z.string(),
53
+ recentModels: z.array(z.string()).default([]),
54
+ };
55
+ /** Mark one schema field volatile when the host's schemastery has the method (rc.1), pass it through plain when not (0.1.5). */
56
+ function volatilize(field) {
57
+ return typeof field.volatile === 'function' ? field.volatile() : field;
58
+ }
59
+ // Bare `z` annotation: `z<Config>` fails schemastery 3.18.4's variance under
60
+ // exactOptionalPropertyTypes (TS2375) and dropping the annotation trips
61
+ // TS2742; the interface stays the apply signature's contract.
62
+ export const Config = z.object({
63
+ sandbox: z.union([
64
+ z.const('read-only'),
65
+ z.const('workspace-write'),
66
+ z.const('danger-full-access'),
67
+ ]),
68
+ model: volatilize(SETTINGS_FIELDS.model),
69
+ live: volatilize(SETTINGS_FIELDS.live),
70
+ liveIdleMs: z.number().default(DEFAULT_LIVE_IDLE_MS),
71
+ liveMirrorGranularity: volatilize(SETTINGS_FIELDS.liveMirrorGranularity),
72
+ recentModels: volatilize(SETTINGS_FIELDS.recentModels),
73
+ });
74
+ /** The sandbox policy a fresh delegation defaults to. */
75
+ export const DEFAULT_SANDBOX = 'workspace-write';
76
+ /**
77
+ * Settings namespace for the settings-page card. The Cordis config feeds the
78
+ * composition `base` layer, so a field absent from the user layer inherits the
79
+ * YAML value — the card only ever stores deliberate overrides.
80
+ */
81
+ export const CODEX_SETTINGS_NAMESPACE = 'local-agent-codex';
82
+ /**
83
+ * The card's 0.1.5 schema; field defaults are the innermost layer below
84
+ * `base`. Shared with the Config schema's settings fields above.
85
+ */
86
+ const CODEX_SETTINGS_SCHEMA = z.object(SETTINGS_FIELDS);
87
+ /**
88
+ * Register the Codex harness into the local-agent registry.
89
+ * @param ctx - plugin context carrying the registry.
90
+ * @param config - plugin config; `sandbox` selects the `codex exec` policy.
91
+ */
92
+ export function apply(ctx, config) {
93
+ ctx.effect(() => {
94
+ const sandbox = config.sandbox ?? DEFAULT_SANDBOX;
95
+ const homeDir = ctx.localAgent.homeDir('codex');
96
+ // Codex's default credential store on macOS is the keychain; pin
97
+ // file-based storage so login lands in the scoped home's auth.json.
98
+ void provisionCodexConfig(homeDir).catch((error) => {
99
+ ctx.logger.warn(`local-agent-codex: config.toml provisioning failed: ${error instanceof Error ? error.message : String(error)}`);
100
+ });
101
+ // The live driver is settings-driven: the settings card's toggle (user
102
+ // layer over the YAML composition base) swaps driver generations without
103
+ // a reload. Toggling OFF drains the retiring generation — new rounds fall
104
+ // back to exec, in-flight rounds finish on their runtime, idle runtimes
105
+ // are reclaimed at once. Legacy granularity settings are accepted but ignored.
106
+ // The settings face hides the two host lines: 0.1.5 serves the legacy
107
+ // namespace scope (register + base); rc.1 reads the row config's volatile
108
+ // fields and re-reads on `settings/document-updated`. Both hot-apply the
109
+ // card's writes without a reload.
110
+ const readSettings = () => {
111
+ const model = vol(config.model);
112
+ const recentModels = vol(config.recentModels);
113
+ return {
114
+ live: vol(config.live) ?? false,
115
+ liveMirrorGranularity: vol(config.liveMirrorGranularity) ?? 'token',
116
+ ...model === undefined ? {} : { model },
117
+ ...recentModels === undefined ? {} : { recentModels },
118
+ };
119
+ };
120
+ const legacyBase = {};
121
+ for (const field of ['live', 'liveMirrorGranularity', 'model']) {
122
+ const value = vol(config[field]);
123
+ if (value !== undefined)
124
+ legacyBase[field] = value;
125
+ }
126
+ const face = settingsFace(ctx, {
127
+ namespace: CODEX_SETTINGS_NAMESPACE,
128
+ legacySchema: CODEX_SETTINGS_SCHEMA,
129
+ legacyBase,
130
+ read: readSettings,
131
+ });
132
+ // The model is read PER ROUND, not captured at apply: the settings card
133
+ // writes the same namespace field, so a change has to reach the next
134
+ // delegation without a plugin reload — exactly like the live toggle.
135
+ // Blank is not a model: a whitespace-only value means "unset", which is
136
+ // the pre-key argv.
137
+ const resolveModel = () => {
138
+ const model = face.get().model?.trim();
139
+ return model === undefined || model === '' ? undefined : model;
140
+ };
141
+ // The member-level model surface: the composer picker's session-level
142
+ // overrides (in-memory, deliberately lost on a host restart), shared by
143
+ // reference with the broker (which writes them), the exec provider (which
144
+ // consults them per round), and the live driver (which binds them at
145
+ // runtime spawn). A switch on a member with a live runtime bound to a
146
+ // different model retires that runtime — the next round respawns onto the
147
+ // new model while the codex thread itself resumes.
148
+ const memberModelOverrides = new Map();
149
+ const modelCatalog = new CodexModelCatalog({
150
+ spawn: spec => ctx.subprocess.spawn(spec),
151
+ warn: message => ctx.logger.warn(message),
152
+ });
153
+ // Eager warmup: kick one background probe of the DEFAULT scoped home
154
+ // shortly after apply completes (never blocking it — read() is the
155
+ // synchronous cache read whose only side effect is the background
156
+ // re-probe), so the first settings-card open usually serves the warm
157
+ // cache instead of reading an empty one and waiting a TTL window. Named
158
+ // scopes keep the lazy path: their first modelInfo read probes on demand.
159
+ // The timer is unref'd (it must not hold the host process open) and
160
+ // cleared on dispose (an unloaded plugin spawns nothing).
161
+ const warmup = setTimeout(() => {
162
+ void modelCatalog.read(homeDir);
163
+ }, CATALOG_WARMUP_DELAY_MS);
164
+ warmup.unref();
165
+ const liveSwitch = new LiveDriverSwitch(ctx, face, {
166
+ sandbox,
167
+ model: childSessionId => memberModelOverrides.get(childSessionId) ?? resolveModel(),
168
+ ...config.liveIdleMs === undefined ? {} : { liveIdleMs: config.liveIdleMs },
169
+ });
170
+ const modelBroker = new CodexModelBroker({
171
+ ctx,
172
+ settingsModel: resolveModel,
173
+ recentModels: () => face.get().recentModels ?? [],
174
+ homeDir: childSessionId => ctx.localAgent.homeDir('codex', childSessionId === undefined ? undefined : (ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId))?.scope),
175
+ cwd: childSessionId => childSessionId === undefined ? undefined : (ctx.localAgent.memberBinding?.(childSessionId) ?? ctx.localAgent.getDelegation(childSessionId))?.cwd,
176
+ directory: (home, cwd) => modelCatalog.directory(home, cwd),
177
+ refreshDirectory: (home, cwd) => modelCatalog.refresh(home, cwd),
178
+ followDirectory: (home, cwd, signal) => modelCatalog.follow(home, signal, cwd),
179
+ live: () => face.get().live,
180
+ overrides: memberModelOverrides,
181
+ liveBoundModel: childSessionId => liveSwitch.boundModel(childSessionId),
182
+ retireRuntime: childSessionId => liveSwitch.retireRuntime(childSessionId),
183
+ // The account catalog probe is LAZY per read: read() serves the cache
184
+ // and kicks a background probe when stale; the apply above warms the
185
+ // default scope's cache so the first card open usually hits it.
186
+ catalog: (scopedHome, cwd) => modelCatalog.read(scopedHome, cwd),
187
+ // The account's built-in default slug (the probe's isDefault entry) —
188
+ // the layer that names the CLI's compiled default, read off the same
189
+ // cache.
190
+ catalogDefault: (scopedHome, cwd) => modelCatalog.readDefault(scopedHome, cwd),
191
+ });
192
+ const disposeProvider = ctx.subagents.registerProvider(new CodexCliProvider(ctx, sandbox, liveSwitch.resolve, resolveModel, childSessionId => memberModelOverrides.get(childSessionId)));
193
+ const disposeHarness = ctx.localAgent.register({
194
+ prepareMember: async ({ binding, childSession, configuration, signal }) => {
195
+ const driver = liveSwitch.resolve(binding.childSessionId);
196
+ if (driver === undefined || driver.disabled)
197
+ throw new Error('Enable the native live harness before preparing a coordinator');
198
+ await driver.prepare({ cwd: binding.cwd, homeDir: ctx.localAgent.homeDir('codex', binding.scope), childSession,
199
+ parentSessionId: binding.parentSessionId, configuration: configuration.resolved, }, signal);
200
+ },
201
+ name: 'codex',
202
+ displayName: 'Codex',
203
+ homeEnvVar: 'CODEX_HOME',
204
+ delegationProvider: 'codex-local',
205
+ modelBroker,
206
+ // A PTY run of plain `codex login` starts a localhost callback server
207
+ // (no device code, no paste) and opens the browser itself — the
208
+ // first-class flow. `--device-auth` remains the documented headless
209
+ // fallback for remote terminals.
210
+ login: { pty: { command: 'codex', args: ['login'] } },
211
+ records: { listSessions: homeDir => listCodexSessions(homeDir) },
212
+ // A NAMED scope's directory is provisioned through this hook when the
213
+ // registry materializes it — the same `config.toml` storage pin the
214
+ // default scope gets from the apply above, so a scope's first login
215
+ // lands in its own `auth.json` instead of the macOS keychain.
216
+ provision: scopedHome => provisionCodexConfig(scopedHome).then(() => { }),
217
+ isAuthenticated: codexAuthenticated,
218
+ credentialStamp: codexCredentialStamp,
219
+ logout: codexLogout,
220
+ // The eval snapshot: the sandbox policy comes from the plugin config
221
+ // (it rides every spawn argv); effort and endpoint are read live from
222
+ // the scoped config, which codex itself reads — a person-edited value
223
+ // is exactly what the rounds run with. The model follows the family's
224
+ // fixed order: the plugin config key (it overrides the file on every
225
+ // argv) before the scoped config's own `model`, absent when neither
226
+ // names one.
227
+ // The directory is a PARAMETER, not the apply-time capture: a status
228
+ // read (or an evaluation snapshot) of a named scope must report the
229
+ // config that scope's rounds would run with, which is the config in
230
+ // that scope's own directory.
231
+ effectiveSettings: async (scopedHome) => {
232
+ const [reasoningEffort, baseUrl, scopedModel, cliVersion] = await Promise.all([
233
+ readCodexReasoningEffort(scopedHome).catch(() => undefined),
234
+ readCodexBaseUrl(scopedHome).catch(() => undefined),
235
+ readCodexModel(scopedHome).catch(() => undefined),
236
+ codexCliVersion(ctx, scopedHome).catch(() => undefined),
237
+ ]);
238
+ const model = resolveModel() ?? scopedModel;
239
+ const baseUrlHost = baseUrl !== undefined ? endpointHost(baseUrl) : undefined;
240
+ return {
241
+ drive: face.get().live ? 'live' : 'exec',
242
+ sandbox,
243
+ ...reasoningEffort !== undefined ? { reasoningEffort } : {},
244
+ baseUrlSet: baseUrl !== undefined,
245
+ ...baseUrlHost !== undefined ? { baseUrlHost } : {},
246
+ ...model !== undefined ? { model } : {},
247
+ ...cliVersion !== undefined ? { cliVersion } : {},
248
+ };
249
+ },
250
+ subcommand: (input, invocation) => {
251
+ const [verb] = input.split(/\s+/);
252
+ if (verb === 'sessions') {
253
+ return listCodexSessions(invocation.agent.ctx.localAgent.homeDir('codex'))
254
+ .then(records => ({
255
+ kind: 'success',
256
+ text: records.length === 0
257
+ ? 'No codex sessions in the scoped home yet.'
258
+ : records.map(r => `${r.id} ${r.workDir}`).join('\n'),
259
+ }));
260
+ }
261
+ return undefined;
262
+ },
263
+ });
264
+ return () => {
265
+ clearTimeout(warmup);
266
+ modelCatalog.dispose();
267
+ disposeProvider();
268
+ disposeHarness();
269
+ liveSwitch.dispose();
270
+ };
271
+ }, 'local-agent-codex: harness');
272
+ }
@@ -0,0 +1,16 @@
1
+ /**
2
+ * Package-owned invariant companion for `@khorsheed/dsh-local-agent-codex`.
3
+ * @module @khorsheed/dsh-local-agent-codex/invariant
4
+ */
5
+ import type { Context } from '@deepseek-ai/cordis';
6
+ /** Cordis companion plugin name. */
7
+ export declare const name = "local-agent-codex-invariant";
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export declare const inject: string[];
10
+ /**
11
+ * Register this package's invariant companion.
12
+ * @param ctx - plugin context carrying the invariant registry.
13
+ * @returns the installed registration's disposer.
14
+ */
15
+ export declare const apply: (ctx: Context) => Promise<() => void>;
16
+ //# sourceMappingURL=invariant.d.ts.map
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Package-owned invariant companion for `@khorsheed/dsh-local-agent-codex`.
3
+ * @module @khorsheed/dsh-local-agent-codex/invariant
4
+ */
5
+ const PACKAGE_NAME = '@khorsheed/dsh-local-agent-codex';
6
+ /** Cordis companion plugin name. */
7
+ export const name = 'local-agent-codex-invariant';
8
+ /** Service required before the companion can reserve package ownership. */
9
+ export const inject = ['invariants'];
10
+ /**
11
+ * The codex harness definition is this bundle's contract with the core
12
+ * registry: whenever a `codex` harness registers, its scoped-home variable
13
+ * must be the one this bundle pins.
14
+ */
15
+ const install = (ctx, fail) => {
16
+ ctx.on('internal/dispatch', (_mode, eventName, args) => {
17
+ if (eventName !== 'localAgent/harness-added')
18
+ return;
19
+ const harness = args[0];
20
+ if (harness.name !== 'codex')
21
+ return;
22
+ if (harness.homeEnvVar !== 'CODEX_HOME') {
23
+ fail('codex harness registered with a home env other than the local-agent-codex contract');
24
+ }
25
+ }, { global: true });
26
+ };
27
+ /**
28
+ * Register this package's invariant companion.
29
+ * @param ctx - plugin context carrying the invariant registry.
30
+ * @returns the installed registration's disposer.
31
+ */
32
+ export const apply = (ctx) => Promise.resolve(ctx.invariants.register(PACKAGE_NAME, install));
33
+ /* jscpd:ignore-end */
@@ -0,0 +1,229 @@
1
+ /**
2
+ * The codex provider's live driver: one resident `codex app-server --stdio`
3
+ * process per member (child session), driven over the vendor's experimental
4
+ * JSON-RPC app-server wire (protocol-probed against codex-cli 0.144.0's
5
+ * `app-server generate-json-schema`; the harness `subagent-codex` package is
6
+ * the reference implementation). A delegation round is a `turn/start` on the
7
+ * member's resident thread instead of a fresh `codex exec` process, which buys
8
+ * what the process boundary could not: runtime-level graceful interrupt
9
+ * (`turn/interrupt`; the process survives and the thread stays continuable)
10
+ * and push-mode mirroring (`item/completed` + delta notifications instead of
11
+ * folding a polled stdout stream).
12
+ *
13
+ * Lifecycle mirrors the dsh live driver's discipline (M1): lazy spawn with an
14
+ * initialize handshake, one in-flight spawn per member, idle-timeout reclaim
15
+ * (stdin EOF → grace → SIGTERM ladder — the app-server wire has no shutdown
16
+ * method), crash re-spawn with `thread/resume` of the recorded thread id, a
17
+ * channel breaker with cooldown, and `disposeAll` on plugin unload. The wire
18
+ * adapter is self-contained (no new dependency): line framing with a
19
+ * StringDecoder for split UTF-8 tails, request correlation, and unattended
20
+ * auto-answers for the server→client approval requests (the proposal's
21
+ * default permission policy: answer exactly as the unattended exec mode does).
22
+ * @module @khorsheed/dsh-local-agent-codex/live-driver
23
+ */
24
+ import type { Context } from '@deepseek-ai/cordis';
25
+ import type { Session } from '@deepseek-ai/dsh-session';
26
+ import { type SubagentRun, type SubagentStartRequest } from '@deepseek-ai/dsh-subagent';
27
+ import type { Config } from './index.ts';
28
+ import type { LocalAgentResolvedConfiguration } from '@khorsheed/dsh-local-agent/types';
29
+ import { type CodexTranscriptLine } from './codex-cli-provider.ts';
30
+ /** Default idle lifetime of an unused resident runtime before reclaim. */
31
+ export declare const DEFAULT_LIVE_IDLE_MS: number;
32
+ /** Default timeout for a wire request acknowledgement. */
33
+ export declare const DEFAULT_LIVE_REQUEST_TIMEOUT_MS = 30000;
34
+ /** Default timeout for the app-server handshake (a cold boot can be slow). */
35
+ export declare const DEFAULT_LIVE_INITIALIZE_TIMEOUT_MS = 60000;
36
+ /** Bounded wait for an interrupted turn to converge while a run disposes. */
37
+ export declare const DEFAULT_LIVE_DISPOSE_CONVERGE_MS = 5000;
38
+ /** How long a tripped channel breaker stays on the exec fallback. */
39
+ export declare const DEFAULT_LIVE_CHANNEL_RETRY_MS: number;
40
+ /** Default minimum interval between one streaming item's snapshot messages. */
41
+ export declare const DEFAULT_SNAPSHOT_MIN_INTERVAL_MS = 50;
42
+ /** Default minimum text growth between one streaming item's snapshot messages. */
43
+ export declare const DEFAULT_SNAPSHOT_MIN_CHARS = 0;
44
+ /**
45
+ * The app-server channel could not come up (spawn failure or handshake
46
+ * timeout/mismatch). The provider catches exactly this and falls back to the
47
+ * exec one-shot; any other error is a real round failure.
48
+ */
49
+ export declare class LiveChannelUnavailableError extends Error {
50
+ constructor(message: string);
51
+ }
52
+ /** Internal timeouts; tests inject small values. */
53
+ export interface CodexLiveDriverTimeouts {
54
+ readonly initializeMs: number;
55
+ readonly requestMs: number;
56
+ readonly convergeMs: number;
57
+ readonly channelRetryMs: number;
58
+ }
59
+ /** How much of the live event stream crosses into the child session. */
60
+ export type CodexLiveMirrorGranularity = 'event' | 'token';
61
+ /** Fully resolved inputs for one live round. */
62
+ export interface CodexLiveRoundSpec {
63
+ /** Parent Session workspace; also the runtime process cwd. */
64
+ readonly cwd: string;
65
+ /** The codex harness's scoped home, injected as the runtime's `CODEX_HOME`. */
66
+ readonly homeDir: string;
67
+ /** dsh subagent session recording this delegation (always live on this path). */
68
+ readonly childSession: Session;
69
+ /** The delegating session (member-channel registration). */
70
+ readonly parentSessionId: string;
71
+ /**
72
+ * Resume round: continue the thread recorded as `cliSessionId`, appending
73
+ * under this turn number. Also the reattach path after a runtime crash.
74
+ */
75
+ readonly resume?: {
76
+ readonly cliSessionId: string;
77
+ readonly turn: number;
78
+ } | undefined;
79
+ /** Fresh round: called with the server-assigned thread id (delegation record). */
80
+ readonly onThreadId?: ((threadId: string) => void) | undefined;
81
+ /**
82
+ * The member's START model — the delegation's own requested model, or the
83
+ * composer's session-level override, as resolved by the provider. When it
84
+ * names one, a resident runtime bound to a DIFFERENT model is retired
85
+ * before the spawn so the round respawns onto it (the same CLI session
86
+ * resumes — the rollout carries over). Absent means the spawn resolver
87
+ * decides (override → recorded → settings).
88
+ */
89
+ readonly startModel?: string | undefined;
90
+ readonly configuration?: LocalAgentResolvedConfiguration;
91
+ }
92
+ type JsonObject = Record<string, unknown>;
93
+ /**
94
+ * Auth-shaped failure signatures in codex's error output: the endpoint
95
+ * rejected the credential. Mirrors the exec path's detector
96
+ * (CODEX_AUTH_FAILURE in codex-cli-provider.ts); the live path matches
97
+ * against turn-failure messages, which is where a mid-run 401 surfaces when
98
+ * the process does not exit.
99
+ */
100
+ export declare const CODEX_LIVE_AUTH_FAILURE: RegExp;
101
+ /**
102
+ * Map one app-server `item/completed` ThreadItem onto the shared transcript
103
+ * line shape — the live transport's entry into the SAME fold the exec NDJSON
104
+ * parser owns (`foldCodexStreamLine`); both produce `CodexTranscriptLine`s
105
+ * and append through `appendCodexTranscriptLine`, so the two drivers' final
106
+ * child transcripts cannot drift.
107
+ */
108
+ export declare function codexAppServerItemToLine(item: JsonObject): CodexTranscriptLine | undefined;
109
+ /**
110
+ * The codex live driver owns every resident app-server runtime of this
111
+ * provider generation. Created only when `live: true`; disposed with the
112
+ * plugin.
113
+ */
114
+ export declare class CodexLiveDriver {
115
+ private readonly ctx;
116
+ private readonly config;
117
+ private readonly timeouts;
118
+ private readonly runtimes;
119
+ /** In-flight spawns by member: concurrent rounds share one, disposeAll waits them out. */
120
+ private readonly ensuring;
121
+ private readonly idleTimers;
122
+ /** Per-member round serialization (the resume lock covers resume-vs-resume only). */
123
+ private readonly roundChains;
124
+ private channelBrokenAt;
125
+ private disposed;
126
+ /**
127
+ * Set by drain() (a settings-driven generation handoff): new rounds are
128
+ * refused so the provider falls back to exec, while in-flight rounds finish
129
+ * on their runtime undisturbed. Unlike `disposed`, the driver still serves
130
+ * what it already accepted.
131
+ */
132
+ private draining;
133
+ private readonly disposeController;
134
+ constructor(ctx: Context, config: Pick<Config, 'sandbox'> & {
135
+ liveIdleMs?: number;
136
+ liveMirrorGranularity?: CodexLiveMirrorGranularity;
137
+ /** Maximum batching wait for incremental streaming messages. */
138
+ snapshotMinIntervalMs?: number;
139
+ /** @deprecated Character growth no longer gates live publication. */
140
+ snapshotMinChars?: number;
141
+ /**
142
+ * Resolver for the member's configured model, read at each RUNTIME SPAWN
143
+ * (the app-server is where a live round's CLI starts). Receives the
144
+ * member's child session id so the session-level override can outrank
145
+ * the settings value; absent, or resolving to nothing, leaves the spawn
146
+ * argv unchanged.
147
+ */
148
+ model?: (childSessionId: string) => string | undefined;
149
+ }, timeouts?: CodexLiveDriverTimeouts);
150
+ /** Whether the channel is in its post-failure cooldown (rounds fall back to exec). */
151
+ get disabled(): boolean;
152
+ /** Count of resident runtimes currently registered (zombie accounting in tests). */
153
+ get liveCount(): number;
154
+ private clearIdleTimer;
155
+ private armIdleTimer;
156
+ private reclaim;
157
+ /** Reclaim every runtime (plugin unload); no process survives the profile. */
158
+ disposeAll(): Promise<void>;
159
+ /**
160
+ * True while the member's runtime exists or is being spawned. The settings
161
+ * controller gates a new generation on this: a fresh driver must not serve
162
+ * a member whose retiring generation still hosts the (same) codex thread.
163
+ */
164
+ hasRuntime(key: string): boolean;
165
+ /** @deprecated Compatibility no-op: live output is always incremental. */
166
+ setLiveMirrorGranularity(_granularity: CodexLiveMirrorGranularity): void;
167
+ /**
168
+ * Drain for a settings-driven generation handoff: refuse new rounds (the
169
+ * provider's catch falls back to exec), let every in-flight round finish on
170
+ * its runtime, then reclaim. Resolves when no runtime or spawn remains.
171
+ * Unlike disposeAll, in-flight work is never interrupted.
172
+ */
173
+ drain(): Promise<void>;
174
+ /**
175
+ * The model the member's live runtime bound at spawn: the identifier, or
176
+ * undefined when the runtime spawned with no model flag. Null when the
177
+ * member has no live runtime in this generation — the broker's switch check
178
+ * distinguishes "no runtime to retire" from "runtime bound to no model".
179
+ */
180
+ boundModelOf(key: string): string | undefined | null;
181
+ /**
182
+ * Retire the member's resident runtime so the NEXT round respawns (the CLI
183
+ * session itself carries over via thread/resume). The composer's model
184
+ * switch calls this after storing the new override; the caller guarantees
185
+ * no round is in flight for the member.
186
+ */
187
+ retireRuntime(key: string): Promise<void>;
188
+ /** The member's runtime, spawning it (once per member at a time) when absent or dead. */
189
+ private ensureRuntime;
190
+ /**
191
+ * Spawn the member's resident app-server and prove the wire with a
192
+ * handshake. Failure trips the channel breaker; a cancellation mid-spawn
193
+ * reclaims the half-started process without touching the breaker.
194
+ */
195
+ private spawnRuntime;
196
+ private markChannelBroken;
197
+ /**
198
+ * Report an auth-shaped round failure to the family registry's auth-failure
199
+ * mark (the exec path's post-exit 401 detection ported to a process that
200
+ * never exits: the match runs against the settled error instead). Degrades
201
+ * silently on a core predating reportAuthFailure.
202
+ */
203
+ private reportAuthIfShaped;
204
+ /**
205
+ * Auto-answer the app-server's approval/elicitation requests, matching the
206
+ * unattended exec behavior (and the harness reference implementation's
207
+ * tables): the live driver never grants interactive approval. This is the
208
+ * proposal's default permission policy; the relay hook for a human
209
+ * approval UI is a later increment.
210
+ */
211
+ private answerServerRequest;
212
+ /**
213
+ * Drive one delegation round on the member's resident app-server. Mirrors
214
+ * the exec run's settlement contract exactly so the facade, the tool, and
215
+ * the projections cannot tell the difference — except `cancel` is a runtime
216
+ * interrupt and the process survives. Cancellation is honored in every
217
+ * window (the M1 acceptance lesson): the abort listener registers before
218
+ * any await, the spawn/handshake races it, and a pre-accept cancel reclaims
219
+ * the fresh runtime instead of letting the turn run unwatched.
220
+ */
221
+ /** Create/resume the native thread without starting a model turn. */
222
+ prepare(spec: CodexLiveRoundSpec, signal: AbortSignal): Promise<void>;
223
+ private ensureThread;
224
+ startRound(request: SubagentStartRequest, spec: CodexLiveRoundSpec): Promise<SubagentRun>;
225
+ /** The serialized round body. */
226
+ private startRoundLocked;
227
+ }
228
+ export {};
229
+ //# sourceMappingURL=live-driver.d.ts.map