antechamber 1.0.13 → 1.0.15
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 +16 -0
- package/dist/lifecycle.js +1 -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 +2 -0
- package/dist/sessions.js +31 -6
- package/dist/work-status.d.ts +2 -0
- package/dist/work-status.js +4 -0
- 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,22 @@ and this project uses [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.15] - 2026-10-23
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- Stop reaping a session while detached eval cells are still running: idle eviction killed the agent process and
|
|
15
|
+
every cell in its kernels once the session had been quiet for `ANTECHAMBER_IDLE_TIMEOUT_MS`. A session now stays
|
|
16
|
+
live while senpi-codemode reports detached cells, and its idle window restarts when the last one settles.
|
|
17
|
+
|
|
18
|
+
## [1.0.14] - 2026-10-22
|
|
19
|
+
|
|
20
|
+
### Fixed
|
|
21
|
+
|
|
22
|
+
- Preselect OmO's real effort in OpenChamber 2.x instead of "Default": `/api/agent` now nests the agent's
|
|
23
|
+
default model and level as `model: {id, providerID, variant}`, and a reopened session reports the model and
|
|
24
|
+
level OmO will resume it with, read from its session file, instead of `variant: "default"`.
|
|
25
|
+
|
|
10
26
|
## [1.0.13] - 2026-10-22
|
|
11
27
|
|
|
12
28
|
### Fixed
|
package/dist/lifecycle.js
CHANGED
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
|
@@ -73,6 +73,8 @@ export interface ManagedSession extends ThinkingState {
|
|
|
73
73
|
hasRunningChildren: () => boolean;
|
|
74
74
|
/** Live OmO background work (monitors) that keeps the agent process from idle eviction. */
|
|
75
75
|
hasBackgroundWork?: () => boolean;
|
|
76
|
+
/** Live wake sources OmO publishes over RPC (detached eval cells), which also block idle eviction. */
|
|
77
|
+
hasWakeSources?: () => boolean;
|
|
76
78
|
}
|
|
77
79
|
type EventSink = (directory: string, event: OCEvent | TodoUpdated) => void;
|
|
78
80
|
export declare class SessionManager {
|
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;
|
|
@@ -134,6 +149,7 @@ export class SessionManager {
|
|
|
134
149
|
published = true;
|
|
135
150
|
this.subagents.wire(managed);
|
|
136
151
|
this.monitors.wire(managed);
|
|
152
|
+
this.workStatus.wire(managed);
|
|
137
153
|
this.lifecycle.touch(managed);
|
|
138
154
|
this.wirePi(managed, pi);
|
|
139
155
|
if (options.model !== undefined)
|
|
@@ -215,9 +231,15 @@ export class SessionManager {
|
|
|
215
231
|
refreshNativeTitle(s) {
|
|
216
232
|
if (s.pi !== undefined || s.restored === undefined || !existsSync(s.restored.piPath))
|
|
217
233
|
return;
|
|
218
|
-
const metadata =
|
|
234
|
+
const metadata = readSessionFileMeta(s.restored.piPath, s.restored.piSessionId);
|
|
219
235
|
if (metadata.titleSource === "native")
|
|
220
236
|
this.applyNativeTitle(s, metadata.title);
|
|
237
|
+
// Not revived here yet, so nothing cached overrides what OmO will resume with, which another client may change.
|
|
238
|
+
const resumed = s.thinkingLevel === undefined ? dormantModel(metadata, undefined) : undefined;
|
|
239
|
+
if (resumed !== undefined) {
|
|
240
|
+
s.model = resumed.model;
|
|
241
|
+
s.info.model = { id: resumed.model.modelID, providerID: resumed.model.providerID, variant: resumed.variant };
|
|
242
|
+
}
|
|
221
243
|
}
|
|
222
244
|
wirePi(s, pi) {
|
|
223
245
|
s.unwirePi?.();
|
|
@@ -1218,6 +1240,7 @@ export class SessionManager {
|
|
|
1218
1240
|
s.piPath = restored.piPath;
|
|
1219
1241
|
s.piSessionId = restored.piSessionId;
|
|
1220
1242
|
this.applyPiModel(s, state.data.model?.provider, state.data.model?.id);
|
|
1243
|
+
this.syncRecordVariant(s);
|
|
1221
1244
|
this.refreshMessageCache(s);
|
|
1222
1245
|
this.persistMeta(s);
|
|
1223
1246
|
delete s.restored;
|
|
@@ -1346,6 +1369,7 @@ export class SessionManager {
|
|
|
1346
1369
|
const titleSource = reg?.titleSource ??
|
|
1347
1370
|
(reg?.title && reg.title !== "New session" && reg.title !== sc.fallbackTitle ? "user" : "fallback");
|
|
1348
1371
|
const useRegistry = titleSource === "user" || (sc.titleSource !== "native" && titleSource === "generated");
|
|
1372
|
+
const resumed = dormantModel(sc, reg?.model);
|
|
1349
1373
|
const info = {
|
|
1350
1374
|
id: ocId,
|
|
1351
1375
|
projectID: sc.directory,
|
|
@@ -1359,9 +1383,9 @@ export class SessionManager {
|
|
|
1359
1383
|
...(reg?.metadata !== undefined ? { metadata: reg.metadata } : {}),
|
|
1360
1384
|
...(reg?.parentID !== undefined ? { parentID: reg.parentID } : {}),
|
|
1361
1385
|
...(reg?.parentMessageID !== undefined ? { parentMessageID: reg.parentMessageID } : {}),
|
|
1362
|
-
...(
|
|
1386
|
+
...(resumed !== undefined
|
|
1363
1387
|
? {
|
|
1364
|
-
model: { id:
|
|
1388
|
+
model: { id: resumed.model.modelID, providerID: resumed.model.providerID, variant: resumed.variant },
|
|
1365
1389
|
}
|
|
1366
1390
|
: {}),
|
|
1367
1391
|
...(reg?.tree?.revert ? { revert: { messageID: reg.tree.revert.messageID } } : {}),
|
|
@@ -1379,7 +1403,7 @@ export class SessionManager {
|
|
|
1379
1403
|
pi: undefined,
|
|
1380
1404
|
messages: undefined,
|
|
1381
1405
|
status: { type: "idle" },
|
|
1382
|
-
model:
|
|
1406
|
+
model: resumed?.model,
|
|
1383
1407
|
streaming: undefined,
|
|
1384
1408
|
toolByCallId: new Map(),
|
|
1385
1409
|
restored: { piSessionId: sc.piSessionId, piPath: sc.piPath },
|
|
@@ -1391,6 +1415,7 @@ export class SessionManager {
|
|
|
1391
1415
|
this.createdSessions.set(sc.piSessionId, sc.directory);
|
|
1392
1416
|
this.subagents.wire(managed);
|
|
1393
1417
|
this.monitors.wire(managed);
|
|
1418
|
+
this.workStatus.wire(managed);
|
|
1394
1419
|
return managed;
|
|
1395
1420
|
}
|
|
1396
1421
|
/** Resolve a bare session id (e.g. deep link after restart) via known directories. */
|
|
@@ -1403,7 +1428,7 @@ export class SessionManager {
|
|
|
1403
1428
|
piSessionId: reg.piSessionId,
|
|
1404
1429
|
piPath: reg.piPath,
|
|
1405
1430
|
directory: reg.directory,
|
|
1406
|
-
...
|
|
1431
|
+
...readSessionFileMeta(reg.piPath, reg.piSessionId),
|
|
1407
1432
|
created: reg.created,
|
|
1408
1433
|
updated: reg.updated,
|
|
1409
1434
|
});
|
package/dist/work-status.d.ts
CHANGED
|
@@ -70,6 +70,8 @@ export declare class WorkStatusBridge {
|
|
|
70
70
|
private readonly work;
|
|
71
71
|
constructor(monitors: MonitorSessions);
|
|
72
72
|
private state;
|
|
73
|
+
/** Evicting the agent process would kill its detached eval cells, so a live wake source keeps it. */
|
|
74
|
+
wire(s: ManagedSession): void;
|
|
73
75
|
observe(s: ManagedSession, ev: Record<string, unknown>): void;
|
|
74
76
|
/** The agent process exited: its in-process wake sources and timers died with it. */
|
|
75
77
|
stopParent(sessionID: string): void;
|
package/dist/work-status.js
CHANGED
|
@@ -138,6 +138,10 @@ export class WorkStatusBridge {
|
|
|
138
138
|
}
|
|
139
139
|
return work;
|
|
140
140
|
}
|
|
141
|
+
/** Evicting the agent process would kill its detached eval cells, so a live wake source keeps it. */
|
|
142
|
+
wire(s) {
|
|
143
|
+
s.hasWakeSources = () => (this.work.get(s.info.id)?.wakeSources.size ?? 0) > 0;
|
|
144
|
+
}
|
|
141
145
|
observe(s, ev) {
|
|
142
146
|
if (ev.name === WAKE_SOURCE_STATE_EVENT) {
|
|
143
147
|
const source = parseWakeSource(ev.data);
|