@linkshell/host 0.4.89 → 0.4.91

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.
@@ -95,6 +95,8 @@ export declare class CodexDriver implements AgentDriver {
95
95
  private readonly goalRevision;
96
96
  private skillsRevision;
97
97
  private models?;
98
+ private modelsLoadedAt?;
99
+ private lastModels;
98
100
  private restartTimer?;
99
101
  private restartDelay;
100
102
  private stopped;
@@ -268,12 +270,14 @@ export declare class CodexDriver implements AgentDriver {
268
270
  private sideUpdate;
269
271
  private markFullAccess;
270
272
  private approvePending;
271
- /** From the app-server the driver keeps running: its model list (cached) and the configuration a thread started in `cwd` reads. */
273
+ /** Refresh when a client opens the picker; a long-running host must pick up models added since its first session. */
272
274
  listModels(cwd?: string, model?: string): Promise<import("@linkshell/wire").MethodResult<"agents.models">>;
273
275
  /** The model config.toml (with the project's own layers, for `cwd`) gives a new thread; undefined: Codex's default. */
274
276
  private configuredSettings;
275
277
  private loadModels;
276
278
  private announceConfig;
279
+ private clearModels;
280
+ private publishConfig;
277
281
  private isNews;
278
282
  private loadSkills;
279
283
  /** What `/` offers on a device: the commands above, then the skills this project can use. */
@@ -172,6 +172,8 @@ export class CodexDriver {
172
172
  goalRevision = new Map();
173
173
  skillsRevision = 0;
174
174
  models;
175
+ modelsLoadedAt;
176
+ lastModels = [];
175
177
  restartTimer;
176
178
  restartDelay;
177
179
  stopped = false;
@@ -257,6 +259,7 @@ export class CodexDriver {
257
259
  clearTimeout(timer);
258
260
  this.taskPolls.clear();
259
261
  this.stopped = true;
262
+ this.clearModels();
260
263
  if (this.restartTimer)
261
264
  clearTimeout(this.restartTimer);
262
265
  for (const timer of this.followRetries.values())
@@ -888,6 +891,10 @@ export class CodexDriver {
888
891
  watched(nativeId, open) {
889
892
  if (open) {
890
893
  this.watching.add(nativeId);
894
+ // The hub can keep a session attached after its last viewer leaves.
895
+ // Reopening it must also refresh the choices sent in ls_config.
896
+ if (this.attached.has(nativeId) || this.observed.has(nativeId))
897
+ void this.announceConfig(nativeId);
891
898
  void this.syncTasks(nativeId);
892
899
  this.cancelRelease(nativeId);
893
900
  }
@@ -1601,9 +1608,9 @@ export class CodexDriver {
1601
1608
  this.host?.update(this.id, threadId, { sessionUpdate: "ls_permission_resolved", requestId, optionId });
1602
1609
  }
1603
1610
  }
1604
- /** From the app-server the driver keeps running: its model list (cached) and the configuration a thread started in `cwd` reads. */
1611
+ /** Refresh when a client opens the picker; a long-running host must pick up models added since its first session. */
1605
1612
  async listModels(cwd, model) {
1606
- const [models, configured] = await Promise.all([this.loadModels(true), this.configuredSettings(cwd)]);
1613
+ const [models, configured] = await Promise.all([this.loadModels(true, true), this.configuredSettings(cwd)]);
1607
1614
  const listed = newThreadModels(models, configured.model);
1608
1615
  const selected = model && listed.models.some((entry) => entry.value === model) ? model : undefined;
1609
1616
  return { ...listed, config: configOptions(configured, selected ? { model: selected } : {}, models).filter((option) => option.category === "mode" || option.category === "effort") };
@@ -1620,33 +1627,66 @@ export class CodexDriver {
1620
1627
  return {};
1621
1628
  }
1622
1629
  }
1623
- loadModels(strict = false) {
1624
- this.models ??= (async () => {
1625
- const models = [];
1626
- const cursors = new Set();
1627
- let cursor;
1628
- do {
1629
- const result = await this.rpc("model/list", { includeHidden: false, ...(cursor ? { cursor } : {}) });
1630
- models.push(...result.data);
1631
- cursor = result.nextCursor ?? undefined;
1632
- if (cursor && cursors.has(cursor))
1633
- throw new Error("Codex 返回了重复的模型分页,请重试");
1634
- if (cursor)
1635
- cursors.add(cursor);
1636
- } while (cursor);
1637
- return models;
1638
- })()
1639
- .catch((error) => {
1630
+ loadModels(strict = false, refresh = false) {
1631
+ // Concurrent pickers share the same request. Internal configuration updates can
1632
+ // reuse its result, while the next explicit listing reads the agent again.
1633
+ if (this.modelsLoadedAt !== undefined && (refresh || Date.now() - this.modelsLoadedAt >= 60_000))
1640
1634
  this.models = undefined;
1641
- this.host?.log(`[codex] model/list failed: ${error instanceof Error ? error.message : String(error)}`);
1642
- if (error instanceof RpcError && error.code === RPC_METHOD_NOT_FOUND)
1643
- return [];
1644
- throw error;
1645
- });
1646
- return strict ? this.models : this.models.catch(() => []);
1635
+ if (!this.models) {
1636
+ this.modelsLoadedAt = undefined;
1637
+ const loading = (async () => {
1638
+ const models = [];
1639
+ const cursors = new Set();
1640
+ let cursor;
1641
+ do {
1642
+ const result = await this.rpc("model/list", { includeHidden: false, ...(cursor ? { cursor } : {}) });
1643
+ models.push(...result.data);
1644
+ cursor = result.nextCursor ?? undefined;
1645
+ if (cursor && cursors.has(cursor))
1646
+ throw new Error("Codex 返回了重复的模型分页,请重试");
1647
+ if (cursor)
1648
+ cursors.add(cursor);
1649
+ } while (cursor);
1650
+ return models;
1651
+ })()
1652
+ .then((models) => {
1653
+ if (this.models === loading) {
1654
+ this.modelsLoadedAt = Date.now();
1655
+ this.lastModels = models;
1656
+ // An existing composer reads ls_config, not agents.models. Keep its
1657
+ // current selection while publishing newly discovered choices.
1658
+ for (const nativeId of this.watching) {
1659
+ if (this.attached.has(nativeId) || this.observed.has(nativeId))
1660
+ this.publishConfig(nativeId, models);
1661
+ }
1662
+ }
1663
+ return models;
1664
+ })
1665
+ .catch((error) => {
1666
+ if (this.models === loading)
1667
+ this.models = undefined;
1668
+ this.host?.log(`[codex] model/list failed: ${error instanceof Error ? error.message : String(error)}`);
1669
+ if (error instanceof RpcError && error.code === RPC_METHOD_NOT_FOUND)
1670
+ return [];
1671
+ throw error;
1672
+ });
1673
+ this.models = loading;
1674
+ }
1675
+ return strict ? this.models : this.models.catch(() => this.lastModels);
1647
1676
  }
1648
1677
  async announceConfig(nativeId) {
1678
+ // Permissions and Plan still come from this session when the catalogue
1679
+ // is unavailable; keep its last model choices while publishing new settings.
1649
1680
  const models = await this.loadModels();
1681
+ if (!this.stopped)
1682
+ this.publishConfig(nativeId, models);
1683
+ }
1684
+ clearModels() {
1685
+ this.models = undefined;
1686
+ this.modelsLoadedAt = undefined;
1687
+ this.lastModels = [];
1688
+ }
1689
+ publishConfig(nativeId, models) {
1650
1690
  const options = configOptions(this.settings.get(nativeId) ?? {}, this.overridesOf(nativeId), models);
1651
1691
  if (options.length > 0 && this.isNews(`config:${nativeId}`, options))
1652
1692
  this.host?.update(this.id, nativeId, { sessionUpdate: "ls_config", options });
@@ -1849,7 +1889,7 @@ export class CodexDriver {
1849
1889
  });
1850
1890
  }
1851
1891
  onDown(reason) {
1852
- this.models = undefined;
1892
+ this.clearModels();
1853
1893
  this.skills.clear();
1854
1894
  if (this.stopped)
1855
1895
  return;