@bridge4dev/runner 0.51.0 → 0.52.0

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.
@@ -284,6 +284,12 @@ class CodexSession {
284
284
  sessionId: this.spec.sessionId,
285
285
  });
286
286
  }
287
+ // Before the thread, not after: a model this CLI no longer offers is
288
+ // refused by `thread/start` itself, and the pin is stored — so the same
289
+ // session would fail again on every relaunch (#374).
290
+ await this.dropModelTheCliNoLongerHas();
291
+ if (this.stopped)
292
+ return;
287
293
  const thread = await this.openThread();
288
294
  if (this.stopped)
289
295
  return;
@@ -1915,6 +1921,93 @@ class CodexSession {
1915
1921
  };
1916
1922
  this.emit({ type: 'capabilities', capabilities });
1917
1923
  }
1924
+ /**
1925
+ * A pinned model the installed CLI does not have is dropped before it can
1926
+ * fail the session (#374).
1927
+ *
1928
+ * The pin is chosen from a catalogue that was measured the last time an agent
1929
+ * ran on this machine, and the CLI moves underneath it: codex 0.153.4 gained
1930
+ * `gpt-6-astra` and lost `gpt-5.3-codex-spark`, so a machine that had not run
1931
+ * a session since the update was offering — and storing — a model that no
1932
+ * longer exists. `turn/start` does not warn about that, it refuses, and since
1933
+ * the pin is persisted the refusal repeats on every relaunch.
1934
+ *
1935
+ * Asked of `model/list` rather than recognised in an error message: matching
1936
+ * on text is what the retry rules were explicitly moved away from in 0.44.1,
1937
+ * because the wording is the vendor's to change.
1938
+ *
1939
+ * A probe that itself fails changes nothing. Diagnosing the start must not be
1940
+ * able to prevent it — an unreachable `model/list` on a CLI that would have
1941
+ * accepted the model would otherwise cost the user their choice.
1942
+ */
1943
+ async dropModelTheCliNoLongerHas() {
1944
+ const pinned = this.model;
1945
+ if (!pinned)
1946
+ return;
1947
+ let known;
1948
+ try {
1949
+ known = await this.probeModelIds();
1950
+ }
1951
+ catch (error) {
1952
+ log.warn('codex: could not check the pinned model — leaving it alone', {
1953
+ sessionId: this.spec.sessionId,
1954
+ error: describe(error),
1955
+ });
1956
+ return;
1957
+ }
1958
+ // Only a COMPLETE list can convict a model. An empty answer means this build
1959
+ // does not really implement `model/list`; a truncated one means the answer
1960
+ // continues on a page we did not ask for. Both must read as «no verdict»,
1961
+ // or the check invents an absence and takes the user's choice away.
1962
+ if (!known.complete || known.ids.size === 0)
1963
+ return;
1964
+ if (known.ids.has(pinned))
1965
+ return;
1966
+ delete this.model;
1967
+ // Said out loud, because the session is about to run on a different model
1968
+ // than the one that was picked — silently substituting it would be worse
1969
+ // than the failure this replaces.
1970
+ this.notice('warn', `This server's codex no longer offers ${pinned} — starting on the agent's own default instead.`);
1971
+ // Cleared everywhere, not just here: the API stores the pin and hands it
1972
+ // back in the next relaunch descriptor, so a pin dropped only in memory
1973
+ // comes back on the next restart (the same trap as QA-100 MAJOR-5).
1974
+ //
1975
+ // What the column ends up holding is «nothing pinned» only until the thread
1976
+ // opens: the capabilities frame right after it reports `currentModel`, and
1977
+ // with no pin that is the thread's own model. Either way the invariant the
1978
+ // clearing exists for holds — what is stored is a model this CLI has.
1979
+ this.emit({ type: 'settings', model: null });
1980
+ if (this.effort) {
1981
+ // The level was chosen for a model that is not there. Keeping it would
1982
+ // apply «Ultra» to whatever the CLI defaults to, which may not have it.
1983
+ delete this.effort;
1984
+ this.emit({ type: 'settings', effort: null });
1985
+ }
1986
+ }
1987
+ /**
1988
+ * Every model id this CLI knows, for the pin check only — including the ones
1989
+ * `listModels()` filters out of the PICKER.
1990
+ *
1991
+ * `hidden` means «do not offer this», not «this does not exist»: codex ships
1992
+ * internal entries like `codex-auto-review` that way, and a pin is not an
1993
+ * offer. Judging a stored pin by the picker's list would delete a model the
1994
+ * CLI still accepts.
1995
+ *
1996
+ * `complete` is false when the answer is paged, and a paged answer proves
1997
+ * nothing about what is missing.
1998
+ */
1999
+ async probeModelIds() {
2000
+ const result = asRecord(await this.client.request('model/list', { limit: 50 }, 15_000));
2001
+ const data = Array.isArray(result['data']) ? result['data'] : [];
2002
+ const ids = new Set();
2003
+ for (const entry of data) {
2004
+ const row = asRecord(entry);
2005
+ const id = str(row['id']) ?? str(row['model']);
2006
+ if (id)
2007
+ ids.add(id);
2008
+ }
2009
+ return { ids, complete: !str(result['nextCursor']) };
2010
+ }
1918
2011
  async listModels() {
1919
2012
  const result = asRecord(await this.client.request('model/list', { limit: 50 }, 15_000));
1920
2013
  const data = Array.isArray(result['data']) ? result['data'] : [];
@@ -406,9 +406,16 @@ export type AgentEvent = {
406
406
  } | {
407
407
  type: 'capabilities';
408
408
  capabilities: AgentCapabilities;
409
- } | {
409
+ }
410
+ /**
411
+ * `model: null` — the pin is GONE, not merely unchanged: the installed CLI
412
+ * stopped offering it (#374). Spelled like `effort: null` and read the same
413
+ * way by the API, which clears the stored column instead of handing the dead
414
+ * pin back in the next descriptor.
415
+ */
416
+ | {
410
417
  type: 'settings';
411
- model?: string;
418
+ model?: string | null;
412
419
  mode?: AgentMode;
413
420
  effort?: string | null;
414
421
  } | {
@@ -2456,7 +2456,13 @@ export class Supervisor {
2456
2456
  case 'settings':
2457
2457
  if (event.mode)
2458
2458
  running.mode = event.mode;
2459
- if (event.model)
2459
+ // null = the agent dropped the pinned MODEL, because the installed CLI
2460
+ // stopped offering it (#374). Dropped from the local record too: this
2461
+ // object is what a relaunch inside the same runner process starts from,
2462
+ // so keeping it would re-pin the model the adapter just refused.
2463
+ if (event.model === null)
2464
+ delete running.model;
2465
+ else if (event.model)
2460
2466
  running.model = event.model;
2461
2467
  // null = the agent dropped the pinned level (model without it) — the
2462
2468
  // API must clear its column, so the value is forwarded as-is.
@@ -2465,7 +2471,7 @@ export class Supervisor {
2465
2471
  else if (event.effort)
2466
2472
  running.effort = event.effort;
2467
2473
  this.sendEvent(running, 'settings', {
2468
- model: event.model,
2474
+ ...(event.model === undefined ? {} : { model: event.model }),
2469
2475
  mode: event.mode,
2470
2476
  ...(event.effort === undefined ? {} : { effort: event.effort }),
2471
2477
  });
package/dist/version.d.ts CHANGED
@@ -1,2 +1,2 @@
1
- export declare const RUNNER_VERSION = "0.51.0";
1
+ export declare const RUNNER_VERSION = "0.52.0";
2
2
  //# sourceMappingURL=version.d.ts.map
package/dist/version.js CHANGED
@@ -1,3 +1,3 @@
1
1
  // Kept in sync with package.json by the release script (manual for now).
2
- export const RUNNER_VERSION = '0.51.0';
2
+ export const RUNNER_VERSION = '0.52.0';
3
3
  //# sourceMappingURL=version.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@bridge4dev/runner",
3
- "version": "0.51.0",
3
+ "version": "0.52.0",
4
4
  "description": "DevBridge dev runner — connects a dev server to DevBridge and runs agent sessions (Claude Code / Codex)",
5
5
  "homepage": "https://bridge4.dev",
6
6
  "license": "MIT",