@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.
- package/README.en.md +136 -0
- package/README.i18n.yaml +6 -0
- package/README.md +136 -0
- package/cordis.patch.yml +42 -0
- package/lib/client.js +1066 -0
- package/lib/index.js +3550 -0
- package/lib/invariant.js +31 -0
- package/lib/types/client/SettingsCard.d.ts +76 -0
- package/lib/types/client/SettingsCard.js +200 -0
- package/lib/types/client/icons.d.ts +18 -0
- package/lib/types/client/icons.js +2 -0
- package/lib/types/client/index.d.ts +43 -0
- package/lib/types/client/index.js +88 -0
- package/lib/types/client/locales.d.ts +64 -0
- package/lib/types/client/locales.js +55 -0
- package/lib/types/codex-cli-provider.d.ts +347 -0
- package/lib/types/codex-cli-provider.js +1225 -0
- package/lib/types/index.d.ts +70 -0
- package/lib/types/index.js +272 -0
- package/lib/types/invariant.d.ts +16 -0
- package/lib/types/invariant.js +33 -0
- package/lib/types/live-driver.d.ts +229 -0
- package/lib/types/live-driver.js +1279 -0
- package/lib/types/live-switch.d.ts +70 -0
- package/lib/types/live-switch.js +90 -0
- package/lib/types/model-broker.d.ts +102 -0
- package/lib/types/model-broker.js +187 -0
- package/lib/types/model-catalog.d.ts +44 -0
- package/lib/types/model-catalog.js +220 -0
- package/lib/types/provision.d.ts +80 -0
- package/lib/types/provision.js +213 -0
- package/lib/types/records.d.ts +120 -0
- package/lib/types/records.js +505 -0
- package/locale/en.json +6 -0
- package/locale/zh.json +6 -0
- package/package.json +106 -0
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings-driven live-driver switch: owns the active driver generation
|
|
3
|
+
* and retires old ones on a settings change, without a plugin reload.
|
|
4
|
+
*
|
|
5
|
+
* Toggle OFF (or unload): the retiring generation is DRAINED, not killed —
|
|
6
|
+
* new rounds are refused (the provider's catch falls back to exec), in-flight
|
|
7
|
+
* rounds finish on their runtime, idle runtimes are reclaimed at once.
|
|
8
|
+
* Toggling ON builds the next generation lazily (no process until the first
|
|
9
|
+
* round). Legacy granularity settings are ignored.
|
|
10
|
+
*
|
|
11
|
+
* @module @khorsheed/dsh-local-agent-codex — internal, unit-tested directly.
|
|
12
|
+
*/
|
|
13
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
14
|
+
import type { SettingsFace } from '@khorsheed/dsh-local-agent';
|
|
15
|
+
import type { Config } from './index.ts';
|
|
16
|
+
import { CodexLiveDriver } from './live-driver.ts';
|
|
17
|
+
import type { CodexLiveMirrorGranularity } from './live-driver.ts';
|
|
18
|
+
/** The resolved live settings (schema defaults ← YAML base ← user layer). */
|
|
19
|
+
export interface CodexLiveSettings {
|
|
20
|
+
live: boolean;
|
|
21
|
+
liveMirrorGranularity: CodexLiveMirrorGranularity;
|
|
22
|
+
/** The configured model, when the card or the YAML base names one. */
|
|
23
|
+
model?: string;
|
|
24
|
+
/** Model identifiers the card has saved before (its input's suggestions). */
|
|
25
|
+
recentModels?: readonly string[];
|
|
26
|
+
}
|
|
27
|
+
export declare class LiveDriverSwitch {
|
|
28
|
+
private readonly ctx;
|
|
29
|
+
private readonly options;
|
|
30
|
+
private liveOn;
|
|
31
|
+
private active;
|
|
32
|
+
/** Retiring generations still draining their in-flight rounds. */
|
|
33
|
+
private readonly draining;
|
|
34
|
+
private readonly unwatch;
|
|
35
|
+
constructor(ctx: Context, face: SettingsFace<CodexLiveSettings>, options: {
|
|
36
|
+
sandbox: Config['sandbox'];
|
|
37
|
+
liveIdleMs?: number;
|
|
38
|
+
/**
|
|
39
|
+
* Per-spawn model resolver, handed to every driver generation. Receives
|
|
40
|
+
* the member's child session id so the session-level override can
|
|
41
|
+
* outrank the settings value.
|
|
42
|
+
*/
|
|
43
|
+
model?: (childSessionId: string) => string | undefined;
|
|
44
|
+
});
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the live driver for one round's member (the provider's call
|
|
47
|
+
* site). A retiring generation still hosting the member gates the new one —
|
|
48
|
+
* the round falls back to exec rather than double-loading the same codex
|
|
49
|
+
* thread into two processes.
|
|
50
|
+
*/
|
|
51
|
+
readonly resolve: (childSessionId: string) => CodexLiveDriver | undefined;
|
|
52
|
+
/**
|
|
53
|
+
* The model the member's live runtime bound at spawn (the broker's switch
|
|
54
|
+
* check): the identifier, undefined for "bound to no model", null for "no
|
|
55
|
+
* live runtime". Consults only the ACTIVE generation — a draining one is on
|
|
56
|
+
* its way out anyway.
|
|
57
|
+
*/
|
|
58
|
+
boundModel(childSessionId: string): string | undefined | null;
|
|
59
|
+
/**
|
|
60
|
+
* Retire the member's resident runtime in the active generation so the next
|
|
61
|
+
* round respawns onto the member's new effective model (the composer's
|
|
62
|
+
* model switch path; the CLI session itself carries over via thread/resume).
|
|
63
|
+
*/
|
|
64
|
+
retireRuntime(childSessionId: string): Promise<void>;
|
|
65
|
+
/** Mirror the resolved settings into the driver generation. */
|
|
66
|
+
private apply;
|
|
67
|
+
/** Plugin unload: interrupt whatever survives (disposeAll, not drain). */
|
|
68
|
+
dispose(): void;
|
|
69
|
+
}
|
|
70
|
+
//# sourceMappingURL=live-switch.d.ts.map
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The settings-driven live-driver switch: owns the active driver generation
|
|
3
|
+
* and retires old ones on a settings change, without a plugin reload.
|
|
4
|
+
*
|
|
5
|
+
* Toggle OFF (or unload): the retiring generation is DRAINED, not killed —
|
|
6
|
+
* new rounds are refused (the provider's catch falls back to exec), in-flight
|
|
7
|
+
* rounds finish on their runtime, idle runtimes are reclaimed at once.
|
|
8
|
+
* Toggling ON builds the next generation lazily (no process until the first
|
|
9
|
+
* round). Legacy granularity settings are ignored.
|
|
10
|
+
*
|
|
11
|
+
* @module @khorsheed/dsh-local-agent-codex — internal, unit-tested directly.
|
|
12
|
+
*/
|
|
13
|
+
import { CodexLiveDriver } from './live-driver.js';
|
|
14
|
+
export class LiveDriverSwitch {
|
|
15
|
+
ctx;
|
|
16
|
+
options;
|
|
17
|
+
liveOn = false;
|
|
18
|
+
active;
|
|
19
|
+
/** Retiring generations still draining their in-flight rounds. */
|
|
20
|
+
draining = new Set();
|
|
21
|
+
unwatch;
|
|
22
|
+
constructor(ctx, face, options) {
|
|
23
|
+
this.ctx = ctx;
|
|
24
|
+
this.options = options;
|
|
25
|
+
this.apply(face.get());
|
|
26
|
+
this.unwatch = face.watch((next) => { this.apply(next); });
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Resolve the live driver for one round's member (the provider's call
|
|
30
|
+
* site). A retiring generation still hosting the member gates the new one —
|
|
31
|
+
* the round falls back to exec rather than double-loading the same codex
|
|
32
|
+
* thread into two processes.
|
|
33
|
+
*/
|
|
34
|
+
resolve = (childSessionId) => {
|
|
35
|
+
if (this.active === undefined)
|
|
36
|
+
return undefined;
|
|
37
|
+
for (const retiring of this.draining) {
|
|
38
|
+
if (retiring.hasRuntime(childSessionId))
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
return this.active;
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* The model the member's live runtime bound at spawn (the broker's switch
|
|
45
|
+
* check): the identifier, undefined for "bound to no model", null for "no
|
|
46
|
+
* live runtime". Consults only the ACTIVE generation — a draining one is on
|
|
47
|
+
* its way out anyway.
|
|
48
|
+
*/
|
|
49
|
+
boundModel(childSessionId) {
|
|
50
|
+
return this.active?.boundModelOf(childSessionId) ?? null;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Retire the member's resident runtime in the active generation so the next
|
|
54
|
+
* round respawns onto the member's new effective model (the composer's
|
|
55
|
+
* model switch path; the CLI session itself carries over via thread/resume).
|
|
56
|
+
*/
|
|
57
|
+
async retireRuntime(childSessionId) {
|
|
58
|
+
await this.active?.retireRuntime(childSessionId);
|
|
59
|
+
}
|
|
60
|
+
/** Mirror the resolved settings into the driver generation. */
|
|
61
|
+
apply(next) {
|
|
62
|
+
if (next.live === this.liveOn) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
this.liveOn = next.live;
|
|
66
|
+
const retiring = this.active;
|
|
67
|
+
this.active = next.live
|
|
68
|
+
? new CodexLiveDriver(this.ctx, {
|
|
69
|
+
...this.options.sandbox === undefined ? {} : { sandbox: this.options.sandbox },
|
|
70
|
+
...this.options.liveIdleMs === undefined ? {} : { liveIdleMs: this.options.liveIdleMs },
|
|
71
|
+
...this.options.model === undefined ? {} : { model: this.options.model },
|
|
72
|
+
})
|
|
73
|
+
: undefined;
|
|
74
|
+
if (retiring !== undefined) {
|
|
75
|
+
this.draining.add(retiring);
|
|
76
|
+
void retiring.drain().then(() => { this.draining.delete(retiring); }, (error) => {
|
|
77
|
+
this.draining.delete(retiring);
|
|
78
|
+
this.ctx.logger.warn(`local-agent-codex: live driver drain failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
/** Plugin unload: interrupt whatever survives (disposeAll, not drain). */
|
|
83
|
+
dispose() {
|
|
84
|
+
this.unwatch();
|
|
85
|
+
void this.active?.disposeAll();
|
|
86
|
+
for (const retiring of this.draining)
|
|
87
|
+
void retiring.disposeAll();
|
|
88
|
+
this.draining.clear();
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The codex harness's model broker: the member-aware model surface the core
|
|
3
|
+
* gateway routes to (`harnessModel` for the settings card, `memberModel` /
|
|
4
|
+
* `setMemberModel` for the composer picker). It owns the session-level
|
|
5
|
+
* override map — shared with the exec provider (which consults it per round)
|
|
6
|
+
* and the live driver (which binds it at runtime spawn) — and retires a
|
|
7
|
+
* member's resident runtime when a switch leaves it bound to a different
|
|
8
|
+
* model, so the NEXT round respawns onto the new one while the codex thread
|
|
9
|
+
* itself carries over via thread/resume.
|
|
10
|
+
*
|
|
11
|
+
* Resolution order is the family's fixed one (first match wins): override →
|
|
12
|
+
* the delegation's recorded model → the plugin-config `model` (settings) →
|
|
13
|
+
* the scoped config.toml's own `model` (cliDefault) → the account catalog's
|
|
14
|
+
* `isDefault` slug (catalogDefault) → the CLI's built-in default, which names
|
|
15
|
+
* nothing. The catalogDefault layer is the ONE case source `cli-builtin`
|
|
16
|
+
* carries an `effective` model: the app-server probe marks the account's
|
|
17
|
+
* compiled default with `isDefault`, so "follow the CLI default" finally has
|
|
18
|
+
* a name. The `cliDefault` FIELD stays scoped-config-only — the two layers
|
|
19
|
+
* must stay distinguishable (the file is user-editable, the catalog answer is
|
|
20
|
+
* the account's). The pickable vocabulary is what the instance actually
|
|
21
|
+
* knows — settings + cliDefault + the identifiers the scoped config itself
|
|
22
|
+
* carries (profiles) + the app-server `model/list` account catalog (probed,
|
|
23
|
+
* cached, best-effort — see model-catalog.ts) + the card's recent-model
|
|
24
|
+
* memory — never a hardcoded catalog.
|
|
25
|
+
* @module @khorsheed/dsh-local-agent-codex/model-broker
|
|
26
|
+
*/
|
|
27
|
+
import type { Context } from '@deepseek-ai/cordis';
|
|
28
|
+
import type { MemberConfigurationAdapter } from '@khorsheed/dsh-local-agent';
|
|
29
|
+
import type { LocalAgentModelBroker, LocalAgentModelInfo, LocalAgentModelDirectory, LocalAgentMemberBinding } from '@khorsheed/dsh-local-agent/types';
|
|
30
|
+
/** Everything the broker reads or drives, injected so the unit specs stay small. */
|
|
31
|
+
export interface CodexModelBrokerDeps {
|
|
32
|
+
/** Host context carrying the family registry (in-flight reads, delegation records). */
|
|
33
|
+
readonly ctx: Context;
|
|
34
|
+
/** The plugin-config `model` resolver — the settings layer, blank reads unset. */
|
|
35
|
+
readonly settingsModel: () => string | undefined;
|
|
36
|
+
/** The card's recent-model memory (the suggestion vocabulary's tail). */
|
|
37
|
+
readonly recentModels: () => readonly string[];
|
|
38
|
+
/**
|
|
39
|
+
* The account catalog cache's SYNC read of the scoped home (see
|
|
40
|
+
* model-catalog.ts): the last completed `model/list` probe's slugs, empty
|
|
41
|
+
* on a cold cache, re-probing in the background when stale. Sits ahead of
|
|
42
|
+
* the recent-model memory in the choice vocabulary.
|
|
43
|
+
*/
|
|
44
|
+
readonly catalog: (homeDir: string, cwd?: string) => readonly string[];
|
|
45
|
+
/**
|
|
46
|
+
* The catalog cache's SYNC read of the account's built-in default slug (the
|
|
47
|
+
* `isDefault` entry of the same probe {@link catalog} serves): the layer
|
|
48
|
+
* between the scoped config's `model` and the CLI's unnamed default. When
|
|
49
|
+
* this layer supplies the effective model the source stays `cli-builtin` —
|
|
50
|
+
* it names the CLI's OWN default, not a configured one.
|
|
51
|
+
*/
|
|
52
|
+
readonly catalogDefault: (homeDir: string, cwd?: string) => string | undefined;
|
|
53
|
+
/** The default scope's scoped home; config discovery reads it live. */
|
|
54
|
+
readonly homeDir: (childSessionId?: string) => string;
|
|
55
|
+
readonly cwd?: (childSessionId?: string) => string | undefined;
|
|
56
|
+
readonly directory?: (homeDir: string, cwd?: string) => LocalAgentModelDirectory;
|
|
57
|
+
readonly refreshDirectory?: (homeDir: string, cwd?: string) => Promise<unknown>;
|
|
58
|
+
readonly followDirectory?: (homeDir: string, cwd: string | undefined, signal: AbortSignal) => AsyncIterable<LocalAgentModelDirectory>;
|
|
59
|
+
/** Whether the live driver is on (the member's rounds bind resident runtimes). */
|
|
60
|
+
readonly live: () => boolean;
|
|
61
|
+
/**
|
|
62
|
+
* The session-level overrides this broker owns. Shared by reference with the
|
|
63
|
+
* exec provider's per-round resolution and the live driver's spawn binding,
|
|
64
|
+
* so a switch reaches every path without a reload.
|
|
65
|
+
*/
|
|
66
|
+
readonly overrides: Map<string, string>;
|
|
67
|
+
/**
|
|
68
|
+
* The model the member's live runtime bound at spawn: the identifier,
|
|
69
|
+
* undefined for "bound to no model", null for "no live runtime".
|
|
70
|
+
*/
|
|
71
|
+
readonly liveBoundModel: (childSessionId: string) => string | undefined | null;
|
|
72
|
+
/** Retire the member's live runtime; the next round respawns onto the new model. */
|
|
73
|
+
readonly retireRuntime: (childSessionId: string) => Promise<void>;
|
|
74
|
+
}
|
|
75
|
+
export declare class CodexModelBroker implements LocalAgentModelBroker {
|
|
76
|
+
private readonly deps;
|
|
77
|
+
constructor(deps: CodexModelBrokerDeps);
|
|
78
|
+
configurationAdapter(binding: LocalAgentMemberBinding): MemberConfigurationAdapter;
|
|
79
|
+
modelDirectory(childSessionId?: string, refresh?: boolean): Promise<LocalAgentModelDirectory>;
|
|
80
|
+
followModelDirectory(childSessionId: string | undefined, signal: AbortSignal): AsyncIterable<LocalAgentModelDirectory>;
|
|
81
|
+
/** The member's in-flight rounds (an empty answer on a core that predates the read). */
|
|
82
|
+
private activeDelegations;
|
|
83
|
+
/** The model the member's NEXT spawn binds explicitly: override → recorded → settings. */
|
|
84
|
+
private boundLayer;
|
|
85
|
+
/**
|
|
86
|
+
* Read the model surface. The cliDefault and the discovered identifiers come
|
|
87
|
+
* from the scoped config.toml LIVE (a person-edited file is authoritative);
|
|
88
|
+
* a read failure degrades that layer to absent, never to a guessed value.
|
|
89
|
+
* The account catalog comes from the probe cache SYNCHRONOUSLY (a cold
|
|
90
|
+
* cache reads empty and re-probes in the background — the settings card
|
|
91
|
+
* re-fetches on open, so it never blocks on a CLI boot).
|
|
92
|
+
*/
|
|
93
|
+
modelInfo(childSessionId?: string, delegationModel?: string): Promise<LocalAgentModelInfo>;
|
|
94
|
+
/**
|
|
95
|
+
* Set (or clear) the member's session-level override. Same-model is a
|
|
96
|
+
* no-op; a different model with a live runtime bound elsewhere retires the
|
|
97
|
+
* runtime so the next round respawns (the codex thread resumes). Throws
|
|
98
|
+
* while a round is in flight — retiring mid-round would kill the run.
|
|
99
|
+
*/
|
|
100
|
+
setMemberModel(childSessionId: string, model: string | undefined): Promise<void>;
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=model-broker.d.ts.map
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The codex harness's model broker: the member-aware model surface the core
|
|
3
|
+
* gateway routes to (`harnessModel` for the settings card, `memberModel` /
|
|
4
|
+
* `setMemberModel` for the composer picker). It owns the session-level
|
|
5
|
+
* override map — shared with the exec provider (which consults it per round)
|
|
6
|
+
* and the live driver (which binds it at runtime spawn) — and retires a
|
|
7
|
+
* member's resident runtime when a switch leaves it bound to a different
|
|
8
|
+
* model, so the NEXT round respawns onto the new one while the codex thread
|
|
9
|
+
* itself carries over via thread/resume.
|
|
10
|
+
*
|
|
11
|
+
* Resolution order is the family's fixed one (first match wins): override →
|
|
12
|
+
* the delegation's recorded model → the plugin-config `model` (settings) →
|
|
13
|
+
* the scoped config.toml's own `model` (cliDefault) → the account catalog's
|
|
14
|
+
* `isDefault` slug (catalogDefault) → the CLI's built-in default, which names
|
|
15
|
+
* nothing. The catalogDefault layer is the ONE case source `cli-builtin`
|
|
16
|
+
* carries an `effective` model: the app-server probe marks the account's
|
|
17
|
+
* compiled default with `isDefault`, so "follow the CLI default" finally has
|
|
18
|
+
* a name. The `cliDefault` FIELD stays scoped-config-only — the two layers
|
|
19
|
+
* must stay distinguishable (the file is user-editable, the catalog answer is
|
|
20
|
+
* the account's). The pickable vocabulary is what the instance actually
|
|
21
|
+
* knows — settings + cliDefault + the identifiers the scoped config itself
|
|
22
|
+
* carries (profiles) + the app-server `model/list` account catalog (probed,
|
|
23
|
+
* cached, best-effort — see model-catalog.ts) + the card's recent-model
|
|
24
|
+
* memory — never a hardcoded catalog.
|
|
25
|
+
* @module @khorsheed/dsh-local-agent-codex/model-broker
|
|
26
|
+
*/
|
|
27
|
+
import { extendModelDirectory } from '@khorsheed/dsh-local-agent';
|
|
28
|
+
import { listCodexConfigModels, readCodexModel, readCodexReasoningEffort } from './provision.js';
|
|
29
|
+
/** Dedupe the choice vocabulary, dropping unset layers and blanks. */
|
|
30
|
+
function dedupeChoices(layers) {
|
|
31
|
+
const seen = new Set();
|
|
32
|
+
const choices = [];
|
|
33
|
+
for (const layer of layers) {
|
|
34
|
+
const value = layer?.trim();
|
|
35
|
+
if (value === undefined || value === '' || seen.has(value))
|
|
36
|
+
continue;
|
|
37
|
+
seen.add(value);
|
|
38
|
+
choices.push(value);
|
|
39
|
+
}
|
|
40
|
+
return choices;
|
|
41
|
+
}
|
|
42
|
+
export class CodexModelBroker {
|
|
43
|
+
deps;
|
|
44
|
+
constructor(deps) {
|
|
45
|
+
this.deps = deps;
|
|
46
|
+
}
|
|
47
|
+
configurationAdapter(binding) {
|
|
48
|
+
const home = this.deps.ctx.localAgent.homeDir('codex', binding.scope);
|
|
49
|
+
const resolve = async (selection) => {
|
|
50
|
+
let directory = this.deps.directory?.(home, binding.cwd);
|
|
51
|
+
if (directory?.status === 'loading') {
|
|
52
|
+
await this.deps.refreshDirectory?.(home, binding.cwd);
|
|
53
|
+
directory = this.deps.directory?.(home, binding.cwd);
|
|
54
|
+
}
|
|
55
|
+
const model = selection.model.mode === 'value' ? selection.model.value
|
|
56
|
+
: (selection.model.mode === 'inherit' ? binding.model : undefined)
|
|
57
|
+
?? this.deps.settingsModel() ?? await readCodexModel(home)
|
|
58
|
+
?? this.deps.catalogDefault(home, binding.cwd);
|
|
59
|
+
const entry = directory?.entries.find(entry => entry.value === model);
|
|
60
|
+
const explicitEffort = selection.effort.mode === 'value' ? selection.effort.value
|
|
61
|
+
: selection.effort.mode === 'inherit' ? binding.effort : undefined;
|
|
62
|
+
const effort = explicitEffort ?? await readCodexReasoningEffort(home) ?? entry?.reasoning?.default;
|
|
63
|
+
if (explicitEffort !== undefined && !entry?.reasoning?.options.some(option => option.value === explicitEffort)) {
|
|
64
|
+
throw new Error('Codex has not advertised this reasoning effort for the selected model; refresh its native directory');
|
|
65
|
+
}
|
|
66
|
+
return { ...model === undefined ? {} : { model }, ...effort === undefined ? {} : { effort } };
|
|
67
|
+
};
|
|
68
|
+
return {
|
|
69
|
+
validate: async (selection) => { await resolve(selection); },
|
|
70
|
+
prepare: resolve,
|
|
71
|
+
apply: async (selection) => {
|
|
72
|
+
const resolved = await resolve(selection);
|
|
73
|
+
await this.deps.retireRuntime(binding.childSessionId);
|
|
74
|
+
return resolved;
|
|
75
|
+
},
|
|
76
|
+
reconcile: async (state) => {
|
|
77
|
+
if (this.activeDelegations().includes(binding.childSessionId))
|
|
78
|
+
return { active: true, matches: 'unknown', resolved: {} };
|
|
79
|
+
// No request is replayed: retire any idle process, then arm the
|
|
80
|
+
// persisted current selection for the next thread/resume + turn/start.
|
|
81
|
+
await this.deps.retireRuntime(binding.childSessionId);
|
|
82
|
+
return { active: false, matches: 'current', resolved: await resolve(state.current.selection) };
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
async modelDirectory(childSessionId, refresh = false) {
|
|
87
|
+
const home = this.deps.homeDir(childSessionId);
|
|
88
|
+
if (refresh)
|
|
89
|
+
await this.deps.refreshDirectory?.(home, this.deps.cwd?.(childSessionId));
|
|
90
|
+
const info = await this.modelInfo(childSessionId, childSessionId === undefined ? undefined : this.deps.ctx.localAgent.getDelegation(childSessionId)?.model);
|
|
91
|
+
return info.directory ?? { entries: [], complete: false, customInput: true, status: 'unsupported', refreshing: false, revision: 0 };
|
|
92
|
+
}
|
|
93
|
+
async *followModelDirectory(childSessionId, signal) {
|
|
94
|
+
if (this.deps.followDirectory === undefined) {
|
|
95
|
+
yield await this.modelDirectory(childSessionId);
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
for await (const _snapshot of this.deps.followDirectory(this.deps.homeDir(childSessionId), this.deps.cwd?.(childSessionId), signal)) {
|
|
99
|
+
yield await this.modelDirectory(childSessionId);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
/** The member's in-flight rounds (an empty answer on a core that predates the read). */
|
|
103
|
+
activeDelegations() {
|
|
104
|
+
const registry = this.deps.ctx.localAgent;
|
|
105
|
+
return typeof registry.activeDelegations === 'function' ? registry.activeDelegations() : [];
|
|
106
|
+
}
|
|
107
|
+
/** The model the member's NEXT spawn binds explicitly: override → recorded → settings. */
|
|
108
|
+
boundLayer(childSessionId) {
|
|
109
|
+
const override = this.deps.overrides.get(childSessionId);
|
|
110
|
+
if (override !== undefined)
|
|
111
|
+
return override;
|
|
112
|
+
const record = this.deps.ctx.localAgent.getDelegation(childSessionId);
|
|
113
|
+
const recorded = record?.model?.trim();
|
|
114
|
+
if (recorded !== undefined && recorded !== '')
|
|
115
|
+
return recorded;
|
|
116
|
+
return this.deps.settingsModel();
|
|
117
|
+
}
|
|
118
|
+
/**
|
|
119
|
+
* Read the model surface. The cliDefault and the discovered identifiers come
|
|
120
|
+
* from the scoped config.toml LIVE (a person-edited file is authoritative);
|
|
121
|
+
* a read failure degrades that layer to absent, never to a guessed value.
|
|
122
|
+
* The account catalog comes from the probe cache SYNCHRONOUSLY (a cold
|
|
123
|
+
* cache reads empty and re-probes in the background — the settings card
|
|
124
|
+
* re-fetches on open, so it never blocks on a CLI boot).
|
|
125
|
+
*/
|
|
126
|
+
async modelInfo(childSessionId, delegationModel) {
|
|
127
|
+
const settings = this.deps.settingsModel();
|
|
128
|
+
const homeDir = this.deps.homeDir(childSessionId);
|
|
129
|
+
const [cliDefault, discovered] = await Promise.all([
|
|
130
|
+
readCodexModel(homeDir).catch(() => undefined),
|
|
131
|
+
listCodexConfigModels(homeDir).catch(() => []),
|
|
132
|
+
]);
|
|
133
|
+
const override = childSessionId === undefined ? undefined : this.deps.overrides.get(childSessionId);
|
|
134
|
+
const delegation = delegationModel?.trim() === '' ? undefined : delegationModel;
|
|
135
|
+
const catalogDefaultRaw = this.deps.catalogDefault(homeDir, this.deps.cwd?.(childSessionId))?.trim();
|
|
136
|
+
const catalogDefault = catalogDefaultRaw === undefined || catalogDefaultRaw === '' ? undefined : catalogDefaultRaw;
|
|
137
|
+
const effective = override ?? delegation ?? settings ?? cliDefault ?? catalogDefault;
|
|
138
|
+
// The catalogDefault layer keeps source `cli-builtin`: it names the CLI's
|
|
139
|
+
// OWN compiled default (the account's `isDefault` slug), the one case
|
|
140
|
+
// cli-builtin names a model. `cliDefault` stays scoped-config-only, so
|
|
141
|
+
// the two layers stay distinguishable downstream.
|
|
142
|
+
const source = override !== undefined ? 'override'
|
|
143
|
+
: delegation !== undefined ? 'delegation'
|
|
144
|
+
: settings !== undefined ? 'settings'
|
|
145
|
+
: cliDefault !== undefined ? 'cli-config' : 'cli-builtin';
|
|
146
|
+
const inFlight = childSessionId !== undefined && this.activeDelegations().includes(childSessionId);
|
|
147
|
+
return {
|
|
148
|
+
...effective === undefined ? {} : { effective },
|
|
149
|
+
source,
|
|
150
|
+
...override === undefined ? {} : { override },
|
|
151
|
+
...delegation === undefined ? {} : { delegation },
|
|
152
|
+
...settings === undefined ? {} : { settings },
|
|
153
|
+
...cliDefault === undefined ? {} : { cliDefault },
|
|
154
|
+
...this.deps.directory === undefined ? {} : { directory: extendModelDirectory(this.deps.directory(homeDir, this.deps.cwd?.(childSessionId)), [settings, cliDefault, ...discovered], this.deps.recentModels()) },
|
|
155
|
+
choices: dedupeChoices([settings, cliDefault, ...discovered, ...this.deps.catalog(homeDir, this.deps.cwd?.(childSessionId)), ...this.deps.recentModels()]),
|
|
156
|
+
live: this.deps.live(),
|
|
157
|
+
switchable: !inFlight,
|
|
158
|
+
...inFlight ? { reason: '成员有进行中的委派轮次,等其完成后再切换模型' } : {},
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Set (or clear) the member's session-level override. Same-model is a
|
|
163
|
+
* no-op; a different model with a live runtime bound elsewhere retires the
|
|
164
|
+
* runtime so the next round respawns (the codex thread resumes). Throws
|
|
165
|
+
* while a round is in flight — retiring mid-round would kill the run.
|
|
166
|
+
*/
|
|
167
|
+
async setMemberModel(childSessionId, model) {
|
|
168
|
+
if (typeof this.deps.ctx.localAgent.setMemberModel === 'function') {
|
|
169
|
+
return this.deps.ctx.localAgent.setMemberModel(childSessionId, model);
|
|
170
|
+
}
|
|
171
|
+
if (this.activeDelegations().includes(childSessionId)) {
|
|
172
|
+
throw new Error(`subagent-codex: 成员有进行中的委派轮次,等其完成后再切换模型 (child session ${childSessionId})`);
|
|
173
|
+
}
|
|
174
|
+
const trimmed = model?.trim();
|
|
175
|
+
const next = trimmed === undefined || trimmed === '' ? undefined : trimmed;
|
|
176
|
+
if (this.deps.overrides.get(childSessionId) === next)
|
|
177
|
+
return;
|
|
178
|
+
if (next === undefined)
|
|
179
|
+
this.deps.overrides.delete(childSessionId);
|
|
180
|
+
else
|
|
181
|
+
this.deps.overrides.set(childSessionId, next);
|
|
182
|
+
const bound = this.deps.liveBoundModel(childSessionId);
|
|
183
|
+
if (bound !== null && bound !== this.boundLayer(childSessionId)) {
|
|
184
|
+
await this.deps.retireRuntime(childSessionId);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/** Native app-server model/list discovery. Shared core cache owns refresh,
|
|
2
|
+
* stale data and subscriptions; this provider owns protocol and pagination. */
|
|
3
|
+
import type { SubprocessHandle, SubprocessSpawnSpec } from '@deepseek-ai/dsh-subprocess';
|
|
4
|
+
import type { LocalAgentModelDirectory } from '@khorsheed/dsh-local-agent/types';
|
|
5
|
+
/** Default overall bound for one catalog probe (spawn → model/list answer). */
|
|
6
|
+
export declare const DEFAULT_CATALOG_PROBE_TIMEOUT_MS = 5000;
|
|
7
|
+
/** Default lifetime of a completed probe's cached answer, per scoped home. */
|
|
8
|
+
export declare const DEFAULT_CATALOG_TTL_MS: number;
|
|
9
|
+
/** Everything the catalog needs, injected so the specs drive a fake app-server. */
|
|
10
|
+
export interface CodexModelCatalogDeps {
|
|
11
|
+
/** The host's managed spawn — the same seam the live driver spawns through. */
|
|
12
|
+
readonly spawn: (spec: SubprocessSpawnSpec) => SubprocessHandle;
|
|
13
|
+
/** Diagnostic sink; a degraded probe warns once, never throws. */
|
|
14
|
+
readonly warn?: (message: string) => void;
|
|
15
|
+
/** Overall probe bound; tests inject a small value. */
|
|
16
|
+
readonly timeoutMs?: number;
|
|
17
|
+
/** Completed-probe cache lifetime; tests inject a small value. */
|
|
18
|
+
readonly ttlMs?: number;
|
|
19
|
+
/** Clock, injectable for the TTL assertions. */
|
|
20
|
+
readonly now?: () => number;
|
|
21
|
+
}
|
|
22
|
+
/** Synchronous compatibility reads plus a rich, subscribable directory. */
|
|
23
|
+
export declare class CodexModelCatalog {
|
|
24
|
+
private readonly deps;
|
|
25
|
+
private readonly timeoutMs;
|
|
26
|
+
private readonly cache;
|
|
27
|
+
constructor(deps: CodexModelCatalogDeps);
|
|
28
|
+
private key;
|
|
29
|
+
read(homeDir: string, cwd?: string): readonly string[];
|
|
30
|
+
readDefault(homeDir: string, cwd?: string): string | undefined;
|
|
31
|
+
directory(homeDir: string, cwd?: string): LocalAgentModelDirectory;
|
|
32
|
+
follow(homeDir: string, signal: AbortSignal, cwd?: string): AsyncIterable<LocalAgentModelDirectory>;
|
|
33
|
+
invalidate(homeDir: string, cwd?: string): void;
|
|
34
|
+
dispose(): void;
|
|
35
|
+
refresh(homeDir: string, cwd?: string): Promise<readonly string[]>;
|
|
36
|
+
/**
|
|
37
|
+
* One bounded probe: spawn the app-server against the scoped home, answer
|
|
38
|
+
* the initialize handshake exactly as the live driver shapes it, ask
|
|
39
|
+
* `model/list`, then tear the process down (stdin EOF → grace →
|
|
40
|
+
* terminate). The overall timeout kills the process on expiry. Failures propagate to the shared cache, which preserves prior data.
|
|
41
|
+
*/
|
|
42
|
+
private probe;
|
|
43
|
+
}
|
|
44
|
+
//# sourceMappingURL=model-catalog.d.ts.map
|