antechamber 1.0.13 → 1.0.14

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/CHANGELOG.md CHANGED
@@ -7,6 +7,14 @@ and this project uses [Semantic Versioning](https://semver.org/).
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [1.0.14] - 2026-10-22
11
+
12
+ ### Fixed
13
+
14
+ - Preselect OmO's real effort in OpenChamber 2.x instead of "Default": `/api/agent` now nests the agent's
15
+ default model and level as `model: {id, providerID, variant}`, and a reopened session reports the model and
16
+ level OmO will resume it with, read from its session file, instead of `variant: "default"`.
17
+
10
18
  ## [1.0.13] - 2026-10-22
11
19
 
12
20
  ### Fixed
package/dist/routes-v2.js CHANGED
@@ -16,9 +16,13 @@ import { announceSessionError, compactSession, getCommands, unsupportedShare } f
16
16
  import { defaultV2Model, flattenModels, toV2Message, toV2Session } from "./v2.js";
17
17
  import { agentDescriptors, modelRefFromBody, numberParam, OPENCODE_COMPAT_VERSION, promptAsync, runSessionShell, runSessionSlash, sessionDirectory, sessionMetadata, } from "./route-services.js";
18
18
  async function v2Agents(services) {
19
- return (await agentDescriptors(services)).map((agent) => ({
19
+ return (await agentDescriptors(services)).map(({ model, variant, ...agent }) => ({
20
20
  id: agent.name,
21
21
  ...agent,
22
+ // v2 nests the effort in the agent's ModelRef; OpenChamber 2.x preselects only `model.id` + `model.variant`.
23
+ ...(model !== undefined
24
+ ? { model: { id: model.modelID, providerID: model.providerID, ...(variant !== undefined ? { variant } : {}) } }
25
+ : {}),
22
26
  request: { settings: {}, headers: {}, body: {} },
23
27
  permissions: agent.permission.map((rule) => ({
24
28
  action: rule.permission,
@@ -19,8 +19,16 @@ import type { ChildTask } from "./subagents.js";
19
19
  export declare function piSessionDir(cwd: string): string;
20
20
  /** Deterministic OpenChamber session id for a pi session id. */
21
21
  export declare function ocIdFor(piSessionId: string): string;
22
+ /** The model and thinking level OmO resumes a session file with. */
23
+ export interface SessionSelection {
24
+ model?: {
25
+ providerID: string;
26
+ modelID: string;
27
+ };
28
+ thinkingLevel?: string;
29
+ }
22
30
  /** One *.jsonl discovered on disk, not (yet) backed by a live pi process. */
23
- export interface ScannedSession {
31
+ export interface ScannedSession extends SessionSelection {
24
32
  piSessionId: string;
25
33
  piPath: string;
26
34
  directory: string;
@@ -66,12 +74,13 @@ export interface SessionRegistryEntry {
66
74
  export declare class SessionPersistenceError extends Error {
67
75
  constructor(file: string, cause: unknown);
68
76
  }
69
- /** Native renames append anywhere in the transcript; directory mtimes do not change. */
70
- export declare function readSessionTitle(piPath: string, piSessionId: string): {
77
+ interface SessionFileMeta extends SessionSelection {
71
78
  title: string;
72
79
  fallbackTitle: string;
73
80
  titleSource: TitleSource;
74
- };
81
+ }
82
+ /** Native renames and model/thinking changes append anywhere in the transcript; directory mtimes do not change. */
83
+ export declare function readSessionFileMeta(piPath: string, piSessionId: string): SessionFileMeta;
75
84
  /**
76
85
  * Newest mtime across the sessions root and its per-cwd subdirectories. Cache
77
86
  * key for {@link scanAllPiSessions}: a new session file bumps its own cwd
@@ -114,3 +123,4 @@ export declare class SessionStore {
114
123
  /** Every directory the registry knows about — used to resolve bare session ids. */
115
124
  directories(): string[];
116
125
  }
126
+ export {};
@@ -84,16 +84,97 @@ function titleFromHead(head, piSessionId) {
84
84
  }
85
85
  return `pi session ${piSessionId.slice(0, 8)}`;
86
86
  }
87
- const titleCache = new Map();
88
- /** Native renames append anywhere in the transcript; directory mtimes do not change. */
89
- export function readSessionTitle(piPath, piSessionId) {
87
+ function modelRef(provider, modelID) {
88
+ return typeof provider === "string" && provider !== "" && typeof modelID === "string" && modelID !== ""
89
+ ? { providerID: provider, modelID }
90
+ : undefined;
91
+ }
92
+ function selectionStep(entry) {
93
+ if (entry.type === "thinking_level_change")
94
+ return { kind: "thinking", level: typeof entry.thinkingLevel === "string" ? entry.thinkingLevel : undefined };
95
+ if (entry.type === "model_change")
96
+ return {
97
+ kind: "model",
98
+ reason: entry.reason,
99
+ model: modelRef(entry.provider, entry.modelId),
100
+ original: modelRef(entry.originalProvider, entry.originalModelId),
101
+ };
102
+ const message = entry.message;
103
+ // A failed routing attempt names the virtual model; no model answered it.
104
+ if (entry.type !== "message" || message?.role !== "assistant" || message.api === "pi-virtual")
105
+ return undefined;
106
+ const model = modelRef(message.provider, message.model);
107
+ return model === undefined ? undefined : { kind: "reply", model };
108
+ }
109
+ /**
110
+ * OmO resumes at the file's last entry and resolves the model and level along its path, restoring the
111
+ * pre-fallback model and level across a fallback window. Mirrors senpi's getSessionContextSettings
112
+ * (dist/core/session-manager.js), except that legacy provider ids are not normalized.
113
+ */
114
+ function resumeSelection(byId, leafId) {
115
+ const path = [];
116
+ const seen = new Set();
117
+ for (let id = leafId; id !== undefined && !seen.has(id);) {
118
+ const entry = byId.get(id);
119
+ if (entry === undefined)
120
+ break;
121
+ seen.add(id);
122
+ if (entry.step !== undefined)
123
+ path.push(entry.step);
124
+ id = entry.parentId ?? undefined;
125
+ }
126
+ let model;
127
+ let explicit = false;
128
+ let inFallback = false;
129
+ let level;
130
+ let preFallbackLevel;
131
+ for (const step of path.reverse()) {
132
+ if (step.kind === "thinking")
133
+ level = step.level;
134
+ else if (step.kind === "model" && step.reason === "fallback") {
135
+ if (!inFallback) {
136
+ preFallbackLevel = level;
137
+ if (step.original !== undefined) {
138
+ model = step.original;
139
+ explicit = true;
140
+ }
141
+ }
142
+ inFallback = true;
143
+ }
144
+ else if (step.kind === "model" && step.reason === "fallback-revert") {
145
+ if (inFallback)
146
+ level = preFallbackLevel;
147
+ inFallback = false;
148
+ }
149
+ else if (step.kind === "model") {
150
+ inFallback = false;
151
+ if (step.model !== undefined) {
152
+ model = step.model;
153
+ explicit = true;
154
+ }
155
+ }
156
+ else if (!inFallback && !(explicit && model?.providerID === step.model.providerID)) {
157
+ // A reply echoes the wire id; only a reply from another provider overrides an explicit pick.
158
+ model = step.model;
159
+ explicit = false;
160
+ }
161
+ }
162
+ if (inFallback)
163
+ level = preFallbackLevel;
164
+ return { ...(model !== undefined ? { model } : {}), ...(level !== undefined ? { thinkingLevel: level } : {}) };
165
+ }
166
+ const fileMetaCache = new Map();
167
+ /** Native renames and model/thinking changes append anywhere in the transcript; directory mtimes do not change. */
168
+ export function readSessionFileMeta(piPath, piSessionId) {
90
169
  const stat = statSync(piPath);
91
- const cached = titleCache.get(piPath);
170
+ const cached = fileMetaCache.get(piPath);
92
171
  if (cached?.mtimeMs === stat.mtimeMs && cached.size === stat.size)
93
172
  return cached;
94
173
  const raw = readFileSync(piPath, "utf8");
95
174
  const fallbackTitle = titleFromHead(raw, piSessionId);
96
175
  let native;
176
+ const byId = new Map();
177
+ let leafId;
97
178
  for (const line of raw.split("\n")) {
98
179
  try {
99
180
  const entry = JSON.parse(line);
@@ -102,6 +183,14 @@ export function readSessionTitle(piPath, piSessionId) {
102
183
  if (name !== "")
103
184
  native = name;
104
185
  }
186
+ if (entry?.type !== "session" && typeof entry?.id === "string") {
187
+ const step = selectionStep(entry);
188
+ byId.set(entry.id, {
189
+ parentId: typeof entry.parentId === "string" ? entry.parentId : null,
190
+ ...(step !== undefined ? { step } : {}),
191
+ });
192
+ leafId = entry.id;
193
+ }
105
194
  }
106
195
  catch {
107
196
  // Partial trailing writes do not erase the last valid title.
@@ -113,8 +202,9 @@ export function readSessionTitle(piPath, piSessionId) {
113
202
  title: native ?? fallbackTitle,
114
203
  fallbackTitle,
115
204
  titleSource: native === undefined ? "fallback" : "native",
205
+ ...resumeSelection(byId, leafId),
116
206
  };
117
- titleCache.set(piPath, metadata);
207
+ fileMetaCache.set(piPath, metadata);
118
208
  return metadata;
119
209
  }
120
210
  function parseSessionHeader(head) {
@@ -146,7 +236,7 @@ function buildScanned(piPath, directory, header) {
146
236
  piSessionId,
147
237
  piPath,
148
238
  directory,
149
- ...readSessionTitle(piPath, piSessionId),
239
+ ...readSessionFileMeta(piPath, piSessionId),
150
240
  created: Number.isNaN(created) ? Math.floor(updated) : created,
151
241
  updated: Math.floor(updated),
152
242
  };
package/dist/sessions.js CHANGED
@@ -20,7 +20,7 @@ import { MonitorSessions } from "./monitor-sessions.js";
20
20
  import { WorkStatusBridge } from "./work-status.js";
21
21
  import { sessionDiff } from "./session-diff.js";
22
22
  import { announceSessionNotice, skillCommandText, slashTitlePrompt } from "./session-commands.js";
23
- import { ocIdFor, piSessionDir, piSessionsRootMtime, scanAllPiSessions, scanPiSessions, SessionStore, readSessionTitle, } from "./session-store.js";
23
+ import { ocIdFor, piSessionDir, piSessionsRootMtime, scanAllPiSessions, scanPiSessions, SessionStore, readSessionFileMeta, } from "./session-store.js";
24
24
  import { ADAPTER_VERSION } from "./version.js";
25
25
  export { ADAPTER_VERSION } from "./version.js";
26
26
  const SESSION_SCAN_CACHE_TTL_MS = 5_000;
@@ -30,6 +30,21 @@ const SESSION_TITLE_MAX_CHARS = 60;
30
30
  /** OmO's reply when a compaction is aborted (`CompactionCancelledError`). */
31
31
  const OMO_COMPACTION_CANCELLED = "Compaction cancelled";
32
32
  const COMPACT_CANCEL_RETRIES = 2;
33
+ /**
34
+ * A dormant session's record as OmO will resume it. OpenChamber 2.x restores a reopened session's effort
35
+ * from this record and resends it, so the model and level come together from the file; the registry only
36
+ * fills in when the file records no model.
37
+ */
38
+ function dormantModel(file, registry) {
39
+ if (file.model !== undefined)
40
+ return { model: file.model, variant: file.thinkingLevel ?? "default" };
41
+ if (registry === undefined)
42
+ return undefined;
43
+ return {
44
+ model: { providerID: registry.providerID, modelID: registry.modelID },
45
+ variant: registry.variant ?? "default",
46
+ };
47
+ }
33
48
  export class SessionManager {
34
49
  emit;
35
50
  defaultDirectory;
@@ -215,9 +230,15 @@ export class SessionManager {
215
230
  refreshNativeTitle(s) {
216
231
  if (s.pi !== undefined || s.restored === undefined || !existsSync(s.restored.piPath))
217
232
  return;
218
- const metadata = readSessionTitle(s.restored.piPath, s.restored.piSessionId);
233
+ const metadata = readSessionFileMeta(s.restored.piPath, s.restored.piSessionId);
219
234
  if (metadata.titleSource === "native")
220
235
  this.applyNativeTitle(s, metadata.title);
236
+ // Not revived here yet, so nothing cached overrides what OmO will resume with, which another client may change.
237
+ const resumed = s.thinkingLevel === undefined ? dormantModel(metadata, undefined) : undefined;
238
+ if (resumed !== undefined) {
239
+ s.model = resumed.model;
240
+ s.info.model = { id: resumed.model.modelID, providerID: resumed.model.providerID, variant: resumed.variant };
241
+ }
221
242
  }
222
243
  wirePi(s, pi) {
223
244
  s.unwirePi?.();
@@ -1218,6 +1239,7 @@ export class SessionManager {
1218
1239
  s.piPath = restored.piPath;
1219
1240
  s.piSessionId = restored.piSessionId;
1220
1241
  this.applyPiModel(s, state.data.model?.provider, state.data.model?.id);
1242
+ this.syncRecordVariant(s);
1221
1243
  this.refreshMessageCache(s);
1222
1244
  this.persistMeta(s);
1223
1245
  delete s.restored;
@@ -1346,6 +1368,7 @@ export class SessionManager {
1346
1368
  const titleSource = reg?.titleSource ??
1347
1369
  (reg?.title && reg.title !== "New session" && reg.title !== sc.fallbackTitle ? "user" : "fallback");
1348
1370
  const useRegistry = titleSource === "user" || (sc.titleSource !== "native" && titleSource === "generated");
1371
+ const resumed = dormantModel(sc, reg?.model);
1349
1372
  const info = {
1350
1373
  id: ocId,
1351
1374
  projectID: sc.directory,
@@ -1359,9 +1382,9 @@ export class SessionManager {
1359
1382
  ...(reg?.metadata !== undefined ? { metadata: reg.metadata } : {}),
1360
1383
  ...(reg?.parentID !== undefined ? { parentID: reg.parentID } : {}),
1361
1384
  ...(reg?.parentMessageID !== undefined ? { parentMessageID: reg.parentMessageID } : {}),
1362
- ...(reg?.model !== undefined
1385
+ ...(resumed !== undefined
1363
1386
  ? {
1364
- model: { id: reg.model.modelID, providerID: reg.model.providerID, variant: reg.model.variant ?? "default" },
1387
+ model: { id: resumed.model.modelID, providerID: resumed.model.providerID, variant: resumed.variant },
1365
1388
  }
1366
1389
  : {}),
1367
1390
  ...(reg?.tree?.revert ? { revert: { messageID: reg.tree.revert.messageID } } : {}),
@@ -1379,7 +1402,7 @@ export class SessionManager {
1379
1402
  pi: undefined,
1380
1403
  messages: undefined,
1381
1404
  status: { type: "idle" },
1382
- model: reg?.model !== undefined ? { providerID: reg.model.providerID, modelID: reg.model.modelID } : undefined,
1405
+ model: resumed?.model,
1383
1406
  streaming: undefined,
1384
1407
  toolByCallId: new Map(),
1385
1408
  restored: { piSessionId: sc.piSessionId, piPath: sc.piPath },
@@ -1403,7 +1426,7 @@ export class SessionManager {
1403
1426
  piSessionId: reg.piSessionId,
1404
1427
  piPath: reg.piPath,
1405
1428
  directory: reg.directory,
1406
- ...readSessionTitle(reg.piPath, reg.piSessionId),
1429
+ ...readSessionFileMeta(reg.piPath, reg.piSessionId),
1407
1430
  created: reg.created,
1408
1431
  updated: reg.updated,
1409
1432
  });
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-compat",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "OpenChamber extension: displays OmO tool calls the way OmO means them when OpenChamber runs on antechamber",
5
5
  "private": true,
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-graph",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "OpenChamber extension: live, interactive graph of the OmO sub-agents behind the current session",
5
5
  "private": true,
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-work",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "OpenChamber 2.x Work Status section: OmO todo phases, monitors, eval cells and background work of the current session",
5
5
  "private": true,
6
6
  "type": "module",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber",
3
- "version": "1.0.13",
3
+ "version": "1.0.14",
4
4
  "description": "OpenCode-compatible HTTP/SSE server that lets OpenChamber drive the pi coding agent (and OmO) over RPC",
5
5
  "type": "module",
6
6
  "license": "MIT",