antechamber 1.0.12 → 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 +17 -0
- package/dist/routes-v2.js +5 -1
- package/dist/session-store.d.ts +14 -4
- package/dist/session-store.js +96 -6
- package/dist/sessions.d.ts +5 -0
- package/dist/sessions.js +39 -6
- package/dist/v2.d.ts +2 -0
- package/dist/v2.js +2 -1
- package/extensions/omo-compat/package.json +1 -1
- package/extensions/omo-dag/package.json +1 -1
- package/extensions/omo-work/package.json +1 -1
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,23 @@ 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
|
+
|
|
18
|
+
## [1.0.13] - 2026-10-22
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Show the effort (thinking level) picker again in OpenChamber 2.x: `/api/model` and `/api/model/default`
|
|
23
|
+
now list each model's thinking levels in `variants` instead of an empty array.
|
|
24
|
+
- Keep a session's effort when OpenChamber 2.x reopens it on another device: the v2 session record now reports
|
|
25
|
+
the thinking level actually applied, instead of a stale one that the next prompt sent back.
|
|
26
|
+
|
|
10
27
|
## [1.0.12] - 2026-10-21
|
|
11
28
|
|
|
12
29
|
### 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,
|
package/dist/session-store.d.ts
CHANGED
|
@@ -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
|
-
|
|
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 {};
|
package/dist/session-store.js
CHANGED
|
@@ -84,16 +84,97 @@ function titleFromHead(head, piSessionId) {
|
|
|
84
84
|
}
|
|
85
85
|
return `pi session ${piSessionId.slice(0, 8)}`;
|
|
86
86
|
}
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
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 =
|
|
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
|
-
|
|
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
|
-
...
|
|
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.d.ts
CHANGED
|
@@ -176,6 +176,11 @@ export declare class SessionManager {
|
|
|
176
176
|
*/
|
|
177
177
|
setModel(sessionId: string, ref: ModelSelection): Promise<OCModelRef>;
|
|
178
178
|
private setModelLive;
|
|
179
|
+
/**
|
|
180
|
+
* On a device with no saved pick, OpenChamber 2.x restores a reopened session's effort from this
|
|
181
|
+
* record and resends it with the next prompt, so a stale level silently resets the user's choice.
|
|
182
|
+
*/
|
|
183
|
+
private syncRecordVariant;
|
|
179
184
|
/**
|
|
180
185
|
* Single funnel for "pi is now running this model": adopt it, persist it for
|
|
181
186
|
* the next cold start, and tell the UI. Returns whether anything changed.
|
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,
|
|
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 =
|
|
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?.();
|
|
@@ -837,6 +858,7 @@ export class SessionManager {
|
|
|
837
858
|
// not cost a round-trip nor emit a phantom "model switched" marker.
|
|
838
859
|
if (s.model !== undefined && s.model.providerID === ref.providerID && s.model.modelID === ref.modelID) {
|
|
839
860
|
await setThinkingLevel(s, pi, ref.variant);
|
|
861
|
+
this.syncRecordVariant(s);
|
|
840
862
|
return s.model;
|
|
841
863
|
}
|
|
842
864
|
const res = await pi.command({
|
|
@@ -868,8 +890,17 @@ export class SessionManager {
|
|
|
868
890
|
};
|
|
869
891
|
this.applyPiModel(s, resolved.providerID, resolved.modelID);
|
|
870
892
|
await setThinkingLevel(s, pi, ref.variant);
|
|
893
|
+
this.syncRecordVariant(s);
|
|
871
894
|
return resolved;
|
|
872
895
|
}
|
|
896
|
+
/**
|
|
897
|
+
* On a device with no saved pick, OpenChamber 2.x restores a reopened session's effort from this
|
|
898
|
+
* record and resends it with the next prompt, so a stale level silently resets the user's choice.
|
|
899
|
+
*/
|
|
900
|
+
syncRecordVariant(s) {
|
|
901
|
+
if (s.info.model !== undefined)
|
|
902
|
+
s.info.model = { ...s.info.model, variant: s.thinkingLevel ?? "default" };
|
|
903
|
+
}
|
|
873
904
|
/**
|
|
874
905
|
* Single funnel for "pi is now running this model": adopt it, persist it for
|
|
875
906
|
* the next cold start, and tell the UI. Returns whether anything changed.
|
|
@@ -1208,6 +1239,7 @@ export class SessionManager {
|
|
|
1208
1239
|
s.piPath = restored.piPath;
|
|
1209
1240
|
s.piSessionId = restored.piSessionId;
|
|
1210
1241
|
this.applyPiModel(s, state.data.model?.provider, state.data.model?.id);
|
|
1242
|
+
this.syncRecordVariant(s);
|
|
1211
1243
|
this.refreshMessageCache(s);
|
|
1212
1244
|
this.persistMeta(s);
|
|
1213
1245
|
delete s.restored;
|
|
@@ -1336,6 +1368,7 @@ export class SessionManager {
|
|
|
1336
1368
|
const titleSource = reg?.titleSource ??
|
|
1337
1369
|
(reg?.title && reg.title !== "New session" && reg.title !== sc.fallbackTitle ? "user" : "fallback");
|
|
1338
1370
|
const useRegistry = titleSource === "user" || (sc.titleSource !== "native" && titleSource === "generated");
|
|
1371
|
+
const resumed = dormantModel(sc, reg?.model);
|
|
1339
1372
|
const info = {
|
|
1340
1373
|
id: ocId,
|
|
1341
1374
|
projectID: sc.directory,
|
|
@@ -1349,9 +1382,9 @@ export class SessionManager {
|
|
|
1349
1382
|
...(reg?.metadata !== undefined ? { metadata: reg.metadata } : {}),
|
|
1350
1383
|
...(reg?.parentID !== undefined ? { parentID: reg.parentID } : {}),
|
|
1351
1384
|
...(reg?.parentMessageID !== undefined ? { parentMessageID: reg.parentMessageID } : {}),
|
|
1352
|
-
...(
|
|
1385
|
+
...(resumed !== undefined
|
|
1353
1386
|
? {
|
|
1354
|
-
model: { id:
|
|
1387
|
+
model: { id: resumed.model.modelID, providerID: resumed.model.providerID, variant: resumed.variant },
|
|
1355
1388
|
}
|
|
1356
1389
|
: {}),
|
|
1357
1390
|
...(reg?.tree?.revert ? { revert: { messageID: reg.tree.revert.messageID } } : {}),
|
|
@@ -1369,7 +1402,7 @@ export class SessionManager {
|
|
|
1369
1402
|
pi: undefined,
|
|
1370
1403
|
messages: undefined,
|
|
1371
1404
|
status: { type: "idle" },
|
|
1372
|
-
model:
|
|
1405
|
+
model: resumed?.model,
|
|
1373
1406
|
streaming: undefined,
|
|
1374
1407
|
toolByCallId: new Map(),
|
|
1375
1408
|
restored: { piSessionId: sc.piSessionId, piPath: sc.piPath },
|
|
@@ -1393,7 +1426,7 @@ export class SessionManager {
|
|
|
1393
1426
|
piSessionId: reg.piSessionId,
|
|
1394
1427
|
piPath: reg.piPath,
|
|
1395
1428
|
directory: reg.directory,
|
|
1396
|
-
...
|
|
1429
|
+
...readSessionFileMeta(reg.piPath, reg.piSessionId),
|
|
1397
1430
|
created: reg.created,
|
|
1398
1431
|
updated: reg.updated,
|
|
1399
1432
|
});
|
package/dist/v2.d.ts
CHANGED
package/dist/v2.js
CHANGED
|
@@ -44,7 +44,8 @@ function toV2Model(m) {
|
|
|
44
44
|
providerID: m.providerID,
|
|
45
45
|
name: m.name,
|
|
46
46
|
capabilities: { tools: true, input: inputs.length > 0 ? inputs : ["text"], output: ["text"] },
|
|
47
|
-
|
|
47
|
+
// OpenChamber 2.x reads a model's efforts only from this array; empty hides the picker.
|
|
48
|
+
variants: Object.entries(m.variants ?? {}).map(([id, settings]) => ({ id, settings })),
|
|
48
49
|
time: { released: 0 },
|
|
49
50
|
cost: [
|
|
50
51
|
{ input: m.cost.input, output: m.cost.output, cache: { read: m.cost.cache.read, write: m.cost.cache.write } },
|