antechamber 1.0.24 → 1.0.25
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/background-results.d.ts +3 -3
- package/dist/background-results.js +39 -7
- package/dist/lifecycle.js +2 -0
- package/dist/monitor-sessions.d.ts +7 -0
- package/dist/monitor-sessions.js +15 -1
- package/dist/omo-shells.d.ts +80 -0
- package/dist/omo-shells.js +333 -0
- package/dist/pi-event-translation.d.ts +2 -0
- package/dist/pi-event-translation.js +24 -2
- package/dist/routes-v2.js +56 -4
- package/dist/sessions.d.ts +6 -0
- package/dist/sessions.js +16 -0
- package/dist/tool-map.d.ts +2 -0
- package/dist/tool-map.js +3 -0
- package/dist/types.d.ts +30 -0
- package/dist/v2.js +47 -1
- 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/README.md +8 -15
- package/extensions/omo-work/docs/DESIGN.md +24 -28
- package/extensions/omo-work/package.json +1 -1
- package/extensions/omo-work/service/main.js +0 -10
- package/extensions/omo-work/status/main.js +92 -93
- package/package.json +1 -1
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { evalAgentSession } from "./subagents.js";
|
|
4
4
|
import { MONITOR_NOTIFICATION } from "./monitor-sessions.js";
|
|
5
|
+
import { terminalShellId } from "./omo-shells.js";
|
|
5
6
|
import { applyProviderToolResults, finishProviderToolParts, mapTool, nestedToolParts, providerToolParts, resultText, toolFailed, } from "./tool-map.js";
|
|
6
7
|
import { TodoBridge } from "./todo-bridge.js";
|
|
7
8
|
import { readSessionTree, recordTurn } from "./session-tree.js";
|
|
@@ -150,6 +151,18 @@ export function compactionMessages(s, key, summary, created, model, thinkingLeve
|
|
|
150
151
|
},
|
|
151
152
|
];
|
|
152
153
|
}
|
|
154
|
+
/**
|
|
155
|
+
* Events after which OmO's background work may have started or ended: monitor and wake-source snapshots,
|
|
156
|
+
* tool results that launch work, and the custom messages OmO sends when a background session finishes.
|
|
157
|
+
*/
|
|
158
|
+
const SHELL_SYNC_EVENTS = new Set([
|
|
159
|
+
"extension_event",
|
|
160
|
+
"tool_execution_end",
|
|
161
|
+
"turn_end",
|
|
162
|
+
"agent_end",
|
|
163
|
+
"agent_idle",
|
|
164
|
+
"message_end",
|
|
165
|
+
]);
|
|
153
166
|
/** Translates per-session events; registry and lifecycle operations stay with SessionManager. */
|
|
154
167
|
export class PiEventTranslator {
|
|
155
168
|
services;
|
|
@@ -316,6 +329,8 @@ export class PiEventTranslator {
|
|
|
316
329
|
default:
|
|
317
330
|
break; // turn_start, queue_update — future milestones
|
|
318
331
|
}
|
|
332
|
+
if (SHELL_SYNC_EVENTS.has(String(ev.type)))
|
|
333
|
+
this.services.shells.sync(s);
|
|
319
334
|
}
|
|
320
335
|
onTodoEntry(s, ev) {
|
|
321
336
|
s.todoBridge ??= new TodoBridge(s.info.id);
|
|
@@ -335,9 +350,11 @@ export class PiEventTranslator {
|
|
|
335
350
|
const timestamp = "timestamp" in value ? value.timestamp : undefined;
|
|
336
351
|
const parsed = typeof timestamp === "string" ? Date.parse(timestamp) : timestamp;
|
|
337
352
|
const created = typeof parsed === "number" && Number.isFinite(parsed) ? parsed : Date.now();
|
|
338
|
-
const part = completeBackgroundEval(messages, result, created);
|
|
353
|
+
const part = completeBackgroundEval(messages, result, created, s.pi?.startedAtMs);
|
|
339
354
|
const prefix = `msg_background_${result.key}_`;
|
|
340
355
|
const unbound = messages.findLast((message) => message.info.id.startsWith(prefix) && s.tree.messageEntries[message.info.id] === undefined);
|
|
356
|
+
if (source === "message")
|
|
357
|
+
this.services.shells.noteEnding(s, result);
|
|
341
358
|
// A finished shell also ends the wait of the card that launched it (once, from the live message).
|
|
342
359
|
if (result.kind === "terminal" && source === "message")
|
|
343
360
|
for (const waiting of releaseBackground(messages, result.callID, created))
|
|
@@ -885,7 +902,7 @@ export class PiEventTranslator {
|
|
|
885
902
|
continue;
|
|
886
903
|
s.toolByCallId.delete(callID);
|
|
887
904
|
s.todoBridge?.finish(part, toolFailed(tr));
|
|
888
|
-
finishToolPart(part, tr, callID, Date.now());
|
|
905
|
+
finishToolPart(part, tr, callID, Date.now(), s.pi?.startedAtMs);
|
|
889
906
|
if (touchedFiles([part]).length > 0)
|
|
890
907
|
this.services.dirtyDiffs.add(s.info.id);
|
|
891
908
|
this.emitPart(s, part);
|
|
@@ -904,6 +921,11 @@ export class PiEventTranslator {
|
|
|
904
921
|
onNestedTools(s, parent, details) {
|
|
905
922
|
const nested = nestedToolParts(parent, details, {
|
|
906
923
|
monitor: (args) => this.services.monitors.findChild(s.info.id, args),
|
|
924
|
+
monitorShell: (childID) => {
|
|
925
|
+
const bashId = this.services.monitors.terminalId(childID);
|
|
926
|
+
const epoch = s.pi?.startedAtMs;
|
|
927
|
+
return bashId !== undefined && epoch !== undefined ? terminalShellId(epoch, bashId) : undefined;
|
|
928
|
+
},
|
|
907
929
|
agent: (args, claimed, after) => evalAgentSession(s.piSessionId ?? s.restored?.piSessionId, args, claimed, after),
|
|
908
930
|
});
|
|
909
931
|
if (nested.length === 0)
|
package/dist/routes-v2.js
CHANGED
|
@@ -14,6 +14,8 @@ import { getAgentName } from "./agent-name.js";
|
|
|
14
14
|
import { vcsRead, VcsError } from "./vcs.js";
|
|
15
15
|
import { announceSessionError, compactSession, getCommands, unsupportedShare } from "./session-commands.js";
|
|
16
16
|
import { defaultV2Model, flattenModels, toV2Message, toV2Session } from "./v2.js";
|
|
17
|
+
import { SHELL_CANCELLATION_KEY } from "./omo-shells.js";
|
|
18
|
+
import { WorkStatusError } from "./work-status.js";
|
|
17
19
|
import { agentDescriptors, modelRefFromBody, numberParam, OPENCODE_COMPAT_VERSION, promptAsync, runSessionShell, runSessionSlash, sessionDirectory, sessionMetadata, } from "./route-services.js";
|
|
18
20
|
async function v2Agents(services) {
|
|
19
21
|
return (await agentDescriptors(services)).map(({ model, variant, ...agent }) => ({
|
|
@@ -169,6 +171,41 @@ async function handleV2Bootstrap(services, res, path, method, directory) {
|
|
|
169
171
|
}
|
|
170
172
|
return false;
|
|
171
173
|
}
|
|
174
|
+
/** Shell ids named by OpenChamber's stop note (background-shell.ts `shellCancellationNote`). */
|
|
175
|
+
function cancelledShells(metadata) {
|
|
176
|
+
const note = metadata?.[SHELL_CANCELLATION_KEY];
|
|
177
|
+
if (note === null || typeof note !== "object")
|
|
178
|
+
return [];
|
|
179
|
+
const ids = "shellIDs" in note ? note.shellIDs : "shellID" in note ? [note.shellID] : [];
|
|
180
|
+
return Array.isArray(ids) ? ids.filter((id) => typeof id === "string") : [];
|
|
181
|
+
}
|
|
182
|
+
async function handleShell(services, res, id, output, method, url, withLocation) {
|
|
183
|
+
const shells = services.sessions.shells;
|
|
184
|
+
const info = shells.get(id);
|
|
185
|
+
if (info === undefined) {
|
|
186
|
+
json(res, 404, errorBody("NotFoundError", `Shell not found: ${id}`));
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
try {
|
|
190
|
+
if (method === "DELETE") {
|
|
191
|
+
await shells.stop(id);
|
|
192
|
+
res.writeHead(204).end();
|
|
193
|
+
}
|
|
194
|
+
else if (output) {
|
|
195
|
+
const cursor = numberParam(url.searchParams.get("cursor"));
|
|
196
|
+
const limit = numberParam(url.searchParams.get("limit"));
|
|
197
|
+
// @opencode/client returns the body and OpenChamber reads `.data` (lib/opencode/client.ts readShellOutput).
|
|
198
|
+
json(res, 200, withLocation(await shells.output(id, cursor, limit)));
|
|
199
|
+
}
|
|
200
|
+
else
|
|
201
|
+
json(res, 200, withLocation(info));
|
|
202
|
+
}
|
|
203
|
+
catch (error) {
|
|
204
|
+
if (!(error instanceof WorkStatusError))
|
|
205
|
+
throw error;
|
|
206
|
+
json(res, error.status, errorBody(error.status === 404 ? "NotFoundError" : "UnsupportedError", error.message));
|
|
207
|
+
}
|
|
208
|
+
}
|
|
172
209
|
export async function handleV2(services, req, res, url, path, method, directory) {
|
|
173
210
|
const location = { directory };
|
|
174
211
|
const withLocation = (data) => ({ location, data });
|
|
@@ -235,10 +272,18 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
235
272
|
});
|
|
236
273
|
return;
|
|
237
274
|
}
|
|
238
|
-
//
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
275
|
+
// OmO's background work (monitors, background terminal sessions, detached eval cells) as OpenCode shells.
|
|
276
|
+
if (path === "/api/shell" && method === "GET") {
|
|
277
|
+
json(res, 200, withLocation(services.sessions.shells.list(directory)));
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
const shellMatch = path.match(/^\/api\/shell\/([^/]+)(\/output)?$/);
|
|
281
|
+
if (shellMatch && (method === "GET" || (method === "DELETE" && shellMatch[2] === undefined))) {
|
|
282
|
+
await handleShell(services, res, decodeURIComponent(shellMatch[1]), shellMatch[2] !== undefined, method, url, withLocation);
|
|
283
|
+
return;
|
|
284
|
+
}
|
|
285
|
+
// OmO has no web-search providers: an honest empty list.
|
|
286
|
+
if (path === "/api/websearch/provider" && method === "GET") {
|
|
242
287
|
json(res, 200, withLocation([]));
|
|
243
288
|
return;
|
|
244
289
|
}
|
|
@@ -666,6 +711,13 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
666
711
|
return;
|
|
667
712
|
}
|
|
668
713
|
const metadata = sessionMetadata(body.metadata);
|
|
714
|
+
const unstoppable = cancelledShells(metadata).filter((id) => !services.sessions.shells.stoppable(id));
|
|
715
|
+
if (unstoppable.length > 0) {
|
|
716
|
+
// OpenChamber sends this note before it removes the shells and stops nothing when the note fails:
|
|
717
|
+
// refuse it rather than tell the agent about a stop that cannot happen.
|
|
718
|
+
json(res, 400, errorBody("UnsupportedError", `OmO cannot stop ${unstoppable.join(", ")}: only eval cells can be stopped`));
|
|
719
|
+
return;
|
|
720
|
+
}
|
|
669
721
|
const messageID = typeof body.id === "string" && body.id !== "" ? body.id : `msg_${randomUUID().replaceAll("-", "")}`;
|
|
670
722
|
const text = body.text;
|
|
671
723
|
promptAsync(services, managed, text, undefined, true, messageID, body.delivery ?? "steer", undefined, metadata);
|
package/dist/sessions.d.ts
CHANGED
|
@@ -11,6 +11,7 @@ import { type SessionTreeState } from "./session-tree.js";
|
|
|
11
11
|
import { SubagentBridge } from "./subagents.js";
|
|
12
12
|
import { MonitorSessions } from "./monitor-sessions.js";
|
|
13
13
|
import { WorkStatusBridge } from "./work-status.js";
|
|
14
|
+
import { OmoShells } from "./omo-shells.js";
|
|
14
15
|
import { SessionStore, type TitleSource } from "./session-store.js";
|
|
15
16
|
import type { OCAssistantMessage, OCEvent, OCMessageWithParts, OCModelRef, OCPart, OCPromptPart, OCSession, OCSessionStatus, OCToolPart } from "./types.js";
|
|
16
17
|
export { ADAPTER_VERSION } from "./version.js";
|
|
@@ -75,6 +76,8 @@ export interface ManagedSession extends ThinkingState {
|
|
|
75
76
|
hasBackgroundWork?: () => boolean;
|
|
76
77
|
/** Live wake sources OmO publishes over RPC (detached eval cells), which also block idle eviction. */
|
|
77
78
|
hasWakeSources?: () => boolean;
|
|
79
|
+
/** antechamber stopped the agent process itself (lifecycle `dormant`): its in-process work died with it. */
|
|
80
|
+
onDormant?: () => void;
|
|
78
81
|
/** Why the agent last stopped on its own; persisted and replayed as a notice until the next prompt. */
|
|
79
82
|
lastStop?: {
|
|
80
83
|
message: string;
|
|
@@ -101,6 +104,7 @@ export declare class SessionManager {
|
|
|
101
104
|
readonly subagents: SubagentBridge;
|
|
102
105
|
readonly monitors: MonitorSessions;
|
|
103
106
|
readonly workStatus: WorkStatusBridge;
|
|
107
|
+
readonly shells: OmoShells;
|
|
104
108
|
/** directory → last scan freshness (pi dir mtime + wall clock). */
|
|
105
109
|
private scanCache;
|
|
106
110
|
/** freshness of the all-directories scan (see listAll). */
|
|
@@ -229,6 +233,8 @@ export declare class SessionManager {
|
|
|
229
233
|
/** Adopt every restorable pi session on disk, regardless of directory. */
|
|
230
234
|
private ensureAllScanned;
|
|
231
235
|
private adopt;
|
|
236
|
+
/** The same cleanup as an agent exit, for a process antechamber disposed after unwiring its exit handler. */
|
|
237
|
+
private wireDormant;
|
|
232
238
|
/** Resolve a bare session id (e.g. deep link after restart) via known directories. */
|
|
233
239
|
private resolveRestored;
|
|
234
240
|
/** Registry write for adapter-owned metadata (title override, pi path). */
|
package/dist/sessions.js
CHANGED
|
@@ -18,6 +18,7 @@ import { bindMessageEntries, branchEntries, durableShellTree, followExternalAppe
|
|
|
18
18
|
import { SubagentBridge } from "./subagents.js";
|
|
19
19
|
import { MonitorSessions } from "./monitor-sessions.js";
|
|
20
20
|
import { WorkStatusBridge } from "./work-status.js";
|
|
21
|
+
import { OmoShells } from "./omo-shells.js";
|
|
21
22
|
import { sessionDiff } from "./session-diff.js";
|
|
22
23
|
import { announceSessionNotice, noticeMessage, skillCommandText, slashTitlePrompt } from "./session-commands.js";
|
|
23
24
|
import { ocIdFor, piSessionDir, piSessionsRootMtime, scanAllPiSessions, scanPiSessions, SessionStore, readSessionFileMeta, } from "./session-store.js";
|
|
@@ -64,6 +65,7 @@ export class SessionManager {
|
|
|
64
65
|
subagents;
|
|
65
66
|
monitors;
|
|
66
67
|
workStatus;
|
|
68
|
+
shells;
|
|
67
69
|
/** directory → last scan freshness (pi dir mtime + wall clock). */
|
|
68
70
|
scanCache = new Map();
|
|
69
71
|
/** freshness of the all-directories scan (see listAll). */
|
|
@@ -76,11 +78,13 @@ export class SessionManager {
|
|
|
76
78
|
this.subagents = new SubagentBridge(this.store, this.sessions, this.emit, (session, raw) => parsePiSessionText(session, getAgentName(), raw));
|
|
77
79
|
this.monitors = new MonitorSessions(this.sessions, this.emit);
|
|
78
80
|
this.workStatus = new WorkStatusBridge(this.monitors);
|
|
81
|
+
this.shells = new OmoShells(this.sessions, this.monitors, this.workStatus, this.emit);
|
|
79
82
|
this.events = new PiEventTranslator({
|
|
80
83
|
emit: this.emit,
|
|
81
84
|
subagents: this.subagents,
|
|
82
85
|
monitors: this.monitors,
|
|
83
86
|
workStatus: this.workStatus,
|
|
87
|
+
shells: this.shells,
|
|
84
88
|
store: this.store,
|
|
85
89
|
dirtyDiffs: this.dirtyDiffs,
|
|
86
90
|
ensureMessages: (s) => this.ensureMessages(s),
|
|
@@ -152,6 +156,7 @@ export class SessionManager {
|
|
|
152
156
|
this.subagents.wire(managed);
|
|
153
157
|
this.monitors.wire(managed);
|
|
154
158
|
this.workStatus.wire(managed);
|
|
159
|
+
this.wireDormant(managed);
|
|
155
160
|
this.lifecycle.touch(managed);
|
|
156
161
|
this.wirePi(managed, pi);
|
|
157
162
|
if (options.model !== undefined)
|
|
@@ -291,6 +296,7 @@ export class SessionManager {
|
|
|
291
296
|
this.subagents.finishCalls(s.info.id, true);
|
|
292
297
|
this.monitors.stopParent(s.info.id);
|
|
293
298
|
this.workStatus.stopParent(s.info.id);
|
|
299
|
+
this.shells.stopParent(s.info.id);
|
|
294
300
|
void this.lifecycle
|
|
295
301
|
.dormant(s)
|
|
296
302
|
.catch((error) => console.error("[lifecycle] exit cleanup failed:", error));
|
|
@@ -401,6 +407,7 @@ export class SessionManager {
|
|
|
401
407
|
const child = this.subagents.remove(sessionId);
|
|
402
408
|
this.monitors.remove(sessionId);
|
|
403
409
|
this.workStatus.stopParent(sessionId);
|
|
410
|
+
this.shells.stopParent(sessionId);
|
|
404
411
|
if (piPath !== undefined) {
|
|
405
412
|
try {
|
|
406
413
|
unlinkSync(piPath);
|
|
@@ -1484,8 +1491,17 @@ export class SessionManager {
|
|
|
1484
1491
|
this.subagents.wire(managed);
|
|
1485
1492
|
this.monitors.wire(managed);
|
|
1486
1493
|
this.workStatus.wire(managed);
|
|
1494
|
+
this.wireDormant(managed);
|
|
1487
1495
|
return managed;
|
|
1488
1496
|
}
|
|
1497
|
+
/** The same cleanup as an agent exit, for a process antechamber disposed after unwiring its exit handler. */
|
|
1498
|
+
wireDormant(s) {
|
|
1499
|
+
s.onDormant = () => {
|
|
1500
|
+
this.monitors.stopParent(s.info.id);
|
|
1501
|
+
this.workStatus.stopParent(s.info.id);
|
|
1502
|
+
this.shells.stopParent(s.info.id);
|
|
1503
|
+
};
|
|
1504
|
+
}
|
|
1489
1505
|
/** Resolve a bare session id (e.g. deep link after restart) via known directories. */
|
|
1490
1506
|
resolveRestored(sessionId) {
|
|
1491
1507
|
const reg = this.store.get(sessionId);
|
package/dist/tool-map.d.ts
CHANGED
|
@@ -32,6 +32,8 @@ export declare function mapTool(omoName: string, args: unknown, result?: ToolRes
|
|
|
32
32
|
export interface NestedSessions {
|
|
33
33
|
/** The monitor child session for a recorded `monitor` call. */
|
|
34
34
|
monitor?: (args: Record<string, unknown>) => string | undefined;
|
|
35
|
+
/** The OpenCode shell id of a monitor child session's watch while it runs (omo-shells.ts). */
|
|
36
|
+
monitorShell?: (sessionID: string) => string | undefined;
|
|
35
37
|
/** The subagent a recorded `agent()` call started; `claimed` keeps repeated calls apart. */
|
|
36
38
|
agent?: (args: Record<string, unknown>, claimed: Set<string>, after: number) => EvalAgent | null | undefined;
|
|
37
39
|
}
|
package/dist/tool-map.js
CHANGED
|
@@ -200,6 +200,9 @@ export function nestedToolParts(parent, details, sessions = {}) {
|
|
|
200
200
|
const command = typeof args.command === "string" ? args.command : "";
|
|
201
201
|
const label = typeof args.description === "string" && args.description !== "" ? args.description : command;
|
|
202
202
|
mapped = nestedSubagent({ title: "Monitor", subtitle: label }, { description: `Monitor: ${label}`, prompt: command, type: "monitor" }, sessionID);
|
|
203
|
+
const shellID = sessions.monitorShell?.(sessionID);
|
|
204
|
+
if (shellID !== undefined)
|
|
205
|
+
mapped.metadata = { ...mapped.metadata, backgroundShellID: shellID };
|
|
203
206
|
}
|
|
204
207
|
else if (entry.name === "task") {
|
|
205
208
|
const text = (value) => (typeof value === "string" && value !== "" ? value : undefined);
|
package/dist/types.d.ts
CHANGED
|
@@ -231,6 +231,24 @@ export type OCSessionStatus = {
|
|
|
231
231
|
message: string;
|
|
232
232
|
next: number;
|
|
233
233
|
};
|
|
234
|
+
export type OCShellStatus = "running" | "exited" | "timeout" | "killed";
|
|
235
|
+
/** OpenCode 2.x `Shell.Info` (@opencode/schema 2.1.4 shell.js); `metadata.sessionID` ties it to a session. */
|
|
236
|
+
export interface OCShellInfo {
|
|
237
|
+
id: string;
|
|
238
|
+
status: OCShellStatus;
|
|
239
|
+
command: string;
|
|
240
|
+
cwd: string;
|
|
241
|
+
shell: string;
|
|
242
|
+
file: string;
|
|
243
|
+
exit?: number;
|
|
244
|
+
metadata: {
|
|
245
|
+
sessionID: string;
|
|
246
|
+
} & Record<string, unknown>;
|
|
247
|
+
time: {
|
|
248
|
+
started: number;
|
|
249
|
+
completed?: number;
|
|
250
|
+
};
|
|
251
|
+
}
|
|
234
252
|
export type OCEvent = {
|
|
235
253
|
type: "session.inbox.enqueued";
|
|
236
254
|
properties: {
|
|
@@ -356,6 +374,18 @@ export type OCEvent = {
|
|
|
356
374
|
truncated: boolean;
|
|
357
375
|
};
|
|
358
376
|
};
|
|
377
|
+
} | {
|
|
378
|
+
type: "shell.created";
|
|
379
|
+
properties: {
|
|
380
|
+
info: OCShellInfo;
|
|
381
|
+
};
|
|
382
|
+
} | {
|
|
383
|
+
type: "shell.exited";
|
|
384
|
+
properties: {
|
|
385
|
+
id: string;
|
|
386
|
+
status: OCShellStatus;
|
|
387
|
+
exit?: number;
|
|
388
|
+
};
|
|
359
389
|
} | {
|
|
360
390
|
type: "session.deleted";
|
|
361
391
|
properties: {
|
package/dist/v2.js
CHANGED
|
@@ -184,6 +184,25 @@ export function toV2AssistantMessage(info) {
|
|
|
184
184
|
function v2ToolName(tool) {
|
|
185
185
|
return tool === "bash" ? "shell" : tool;
|
|
186
186
|
}
|
|
187
|
+
/**
|
|
188
|
+
* A card whose call left OmO work running (background-results.ts `backgroundShellID`), as OpenCode 2.x settles a
|
|
189
|
+
* call it moved to the background: completed at once with `{status: "running", shellID}` in its metadata, so
|
|
190
|
+
* OpenChamber 2.x draws the row from the shell (background-shell.ts `readBackgroundShellID`) and lists the shell
|
|
191
|
+
* as a background command. A card still waiting on that work is running here, with its result already known.
|
|
192
|
+
*/
|
|
193
|
+
function backgroundShell(part) {
|
|
194
|
+
const st = part.state;
|
|
195
|
+
if (st.status !== "running" && st.status !== "completed")
|
|
196
|
+
return undefined;
|
|
197
|
+
const { backgroundShellID: shellID, backgroundWaits, backgroundFailed, output, ...metadata } = st.metadata ?? {};
|
|
198
|
+
if (typeof shellID !== "string")
|
|
199
|
+
return undefined;
|
|
200
|
+
if (st.status === "completed")
|
|
201
|
+
return { output: st.output, metadata: { ...metadata, status: "running", shellID } };
|
|
202
|
+
if (backgroundFailed !== false || !Array.isArray(backgroundWaits))
|
|
203
|
+
return undefined;
|
|
204
|
+
return { output: typeof output === "string" ? output : "", metadata: { ...metadata, status: "running", shellID } };
|
|
205
|
+
}
|
|
187
206
|
function toolContent(part) {
|
|
188
207
|
const st = part.state;
|
|
189
208
|
const name = v2ToolName(part.tool);
|
|
@@ -202,9 +221,21 @@ function toolContent(part) {
|
|
|
202
221
|
...(end !== undefined ? { completed: end } : {}),
|
|
203
222
|
},
|
|
204
223
|
};
|
|
224
|
+
const background = st.status === "running" ? backgroundShell(part) : undefined;
|
|
205
225
|
if (st.status === "pending") {
|
|
206
226
|
base.state = { status: "streaming", input: st.raw ?? "", metadata: {} };
|
|
207
227
|
}
|
|
228
|
+
else if (background !== undefined) {
|
|
229
|
+
base.time = { created: st.time.start, ran: st.time.start, completed: st.time.start };
|
|
230
|
+
base.state = {
|
|
231
|
+
status: "completed",
|
|
232
|
+
input: st.input,
|
|
233
|
+
content: background.output !== "" ? [{ type: "text", text: background.output }] : [],
|
|
234
|
+
output: background.output,
|
|
235
|
+
title: st.status === "running" ? (st.title ?? part.tool) : part.tool,
|
|
236
|
+
metadata: background.metadata,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
208
239
|
else if (st.status === "running") {
|
|
209
240
|
base.state = {
|
|
210
241
|
status: "running",
|
|
@@ -220,7 +251,7 @@ function toolContent(part) {
|
|
|
220
251
|
content: st.output !== "" ? [{ type: "text", text: st.output }] : [],
|
|
221
252
|
output: st.output,
|
|
222
253
|
title: st.title,
|
|
223
|
-
metadata: st.metadata ?? {},
|
|
254
|
+
metadata: backgroundShell(part)?.metadata ?? st.metadata ?? {},
|
|
224
255
|
};
|
|
225
256
|
}
|
|
226
257
|
else {
|
|
@@ -256,6 +287,8 @@ export class V2EventTranslator {
|
|
|
256
287
|
case "session.compaction.failed":
|
|
257
288
|
case "session.shell.started":
|
|
258
289
|
case "session.shell.ended":
|
|
290
|
+
case "shell.created":
|
|
291
|
+
case "shell.exited":
|
|
259
292
|
return [this.frame(directory, ev.type, ev.properties)];
|
|
260
293
|
case "message.removed":
|
|
261
294
|
return [
|
|
@@ -610,6 +643,19 @@ export class V2EventTranslator {
|
|
|
610
643
|
ts.called = true;
|
|
611
644
|
frames.push(this.frame(directory, "session.tool.called", { ...base, input: state.input, executed: false }));
|
|
612
645
|
}
|
|
646
|
+
const background = backgroundShell(part);
|
|
647
|
+
if (background !== undefined) {
|
|
648
|
+
if (!ts.ended) {
|
|
649
|
+
ts.ended = true;
|
|
650
|
+
frames.push(this.frame(directory, "session.tool.success", {
|
|
651
|
+
...base,
|
|
652
|
+
content: background.output !== "" ? [{ type: "text", text: background.output }] : [],
|
|
653
|
+
metadata: background.metadata,
|
|
654
|
+
executed: false,
|
|
655
|
+
}));
|
|
656
|
+
}
|
|
657
|
+
return frames;
|
|
658
|
+
}
|
|
613
659
|
// `called` carries no metadata (eval card headers, live output); 2.x reads it while running only from progress.
|
|
614
660
|
if (state.status === "running" && state.metadata !== undefined) {
|
|
615
661
|
const metadata = safeJson(state.metadata);
|
package/dist/work-status.d.ts
CHANGED
|
@@ -80,6 +80,8 @@ export declare class WorkStatusBridge {
|
|
|
80
80
|
*/
|
|
81
81
|
wire(s: ManagedSession): void;
|
|
82
82
|
observe(s: ManagedSession, ev: Record<string, unknown>): void;
|
|
83
|
+
/** Detached eval cells codemode reports running or queued. */
|
|
84
|
+
liveCells(sessionID: string): WakeSourceItem[];
|
|
83
85
|
/** The agent process exited: its in-process wake sources and timers died with it. */
|
|
84
86
|
stopParent(sessionID: string): void;
|
|
85
87
|
snapshot(s: ManagedSession): Promise<WorkStatus>;
|
package/dist/work-status.js
CHANGED
|
@@ -194,6 +194,10 @@ export class WorkStatusBridge {
|
|
|
194
194
|
work.goal = { ...(work.goal ?? { armed: false }), wakeSources: counts };
|
|
195
195
|
}
|
|
196
196
|
}
|
|
197
|
+
/** Detached eval cells codemode reports running or queued. */
|
|
198
|
+
liveCells(sessionID) {
|
|
199
|
+
return this.work.get(sessionID)?.wakeSources.get(CODEMODE_WAKE_SOURCE)?.items ?? [];
|
|
200
|
+
}
|
|
197
201
|
/** The agent process exited: its in-process wake sources and timers died with it. */
|
|
198
202
|
stopParent(sessionID) {
|
|
199
203
|
this.work.delete(sessionID);
|
|
@@ -10,16 +10,14 @@ For the open session it shows:
|
|
|
10
10
|
|
|
11
11
|
- **Todo**: OmO's todo list grouped by phase, with progress per phase. The
|
|
12
12
|
phase in progress is open; click any phase to open or close it.
|
|
13
|
-
- **Monitors**: each terminal monitor with its state, age and event count.
|
|
14
|
-
Click one for its command, filter, deadline and latest matched lines. On
|
|
15
|
-
OpenChamber 2.x, monitors appear here instead of as subagent sessions.
|
|
16
|
-
- **Eval cells**: detached eval cells with their language, state and running
|
|
17
|
-
time. Click one to follow its output; **Stop** stops a running or queued
|
|
18
|
-
cell.
|
|
19
|
-
- **Background**: background terminal sessions that are still running.
|
|
20
13
|
- **Goal**: the goal continuation timer, what it waits on, and **Fire now**.
|
|
21
14
|
- **Other activity**: any other wake source OmO reports.
|
|
22
15
|
|
|
16
|
+
Terminal monitors, detached eval cells and background terminal sessions are not
|
|
17
|
+
listed here: antechamber reports them to OpenChamber as background commands,
|
|
18
|
+
so the session reads as working while they run and OpenChamber lists them above
|
|
19
|
+
the composer with their output (and Stop, for eval cells).
|
|
20
|
+
|
|
23
21
|
The section sizes itself to its content, up to OpenChamber's 320 px limit, and
|
|
24
22
|
scrolls inside beyond that.
|
|
25
23
|
|
|
@@ -64,7 +62,7 @@ section (sandboxed iframe) --serviceRequest /status?session=ses_...--> OpenChamb
|
|
|
64
62
|
`antechamber serve` writes `<state dir>/run/<pid>.json` (mode 0600) with its
|
|
65
63
|
URL and a random token that opens only the `/antechamber/work-status` routes. The service
|
|
66
64
|
picks the running server whose agent holds the session and relays the snapshot
|
|
67
|
-
and the
|
|
65
|
+
and the Fire now action. It keeps no state. See [docs/DESIGN.md](docs/DESIGN.md) for the snapshot, refresh,
|
|
68
66
|
server choice and security boundary, and
|
|
69
67
|
[docs/architecture.md](../../docs/architecture.md#omo-work-status-section) for
|
|
70
68
|
where each piece of data comes from in OmO.
|
|
@@ -77,13 +75,8 @@ the extension's OpenChamber storage.
|
|
|
77
75
|
|
|
78
76
|
## Limitations
|
|
79
77
|
|
|
80
|
-
OmO does not
|
|
81
|
-
|
|
82
|
-
- stopping a monitor or a background terminal session;
|
|
83
|
-
- continuation holds (the loop guard publishes them in-process only).
|
|
84
|
-
|
|
85
|
-
Eval cells need OmO's codemode. Without it, or while the agent process is not
|
|
86
|
-
running, the section lists the cells OmO last reported, without output or Stop.
|
|
78
|
+
OmO does not publish continuation holds over RPC (the loop guard keeps them
|
|
79
|
+
in-process), so the section cannot show them.
|
|
87
80
|
|
|
88
81
|
## Development
|
|
89
82
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
1
|
# OmO Work: design
|
|
2
2
|
|
|
3
|
-
OmO Work is an OpenChamber 2.x Work Status section for the
|
|
4
|
-
|
|
3
|
+
OmO Work is an OpenChamber 2.x Work Status section for the open session's work
|
|
4
|
+
that OpenChamber has no surface for: the todo list, the goal continuation timer
|
|
5
|
+
and other wake sources. Monitors, eval cells and background terminal sessions
|
|
6
|
+
are OpenChamber's own background commands, reported by antechamber as OpenCode
|
|
7
|
+
shells ([docs/architecture.md](../../../docs/architecture.md#omo-background-work-as-opencode-shells)),
|
|
8
|
+
so the section does not list them. This document covers its parts, its data, its security
|
|
5
9
|
boundary and its limits. Where each field comes from in OmO is documented in
|
|
6
10
|
[docs/architecture.md](../../../docs/architecture.md#omo-work-status-section).
|
|
7
11
|
|
|
@@ -10,7 +14,7 @@ boundary and its limits. Where each field comes from in OmO is documented in
|
|
|
10
14
|
```
|
|
11
15
|
OpenChamber Work Status panel
|
|
12
16
|
└─ section frame (status/index.html + status/main.js, sandboxed iframe)
|
|
13
|
-
│ host.serviceRequest GET /status,
|
|
17
|
+
│ host.serviceRequest GET /status, POST /goal/fire
|
|
14
18
|
▼
|
|
15
19
|
OpenChamber host ── loopback + OPENCHAMBER_SERVICE_TOKEN ──▶ service/main.js (Node, on the host)
|
|
16
20
|
│ reads <state dir>/run/<pid>.json
|
|
@@ -18,7 +22,7 @@ OpenChamber host ── loopback + OPENCHAMBER_SERVICE_TOKEN ──▶ service/m
|
|
|
18
22
|
▼
|
|
19
23
|
antechamber /antechamber/work-status/<session>[...]
|
|
20
24
|
│ snapshot from its bridges,
|
|
21
|
-
▼
|
|
25
|
+
▼ goal RPC to OmO
|
|
22
26
|
omo --mode rpc
|
|
23
27
|
```
|
|
24
28
|
|
|
@@ -42,7 +46,7 @@ OpenChamber host ── loopback + OPENCHAMBER_SERVICE_TOKEN ──▶ service/m
|
|
|
42
46
|
|
|
43
47
|
| Field | Content |
|
|
44
48
|
|---|---|
|
|
45
|
-
| `live` | the session's agent process is running;
|
|
49
|
+
| `live` | the session's agent process is running; Fire now needs it |
|
|
46
50
|
| `todo` | todo phases in OmO's order, each `{name, tasks: [{content, status}]}`, status `pending`, `in_progress`, `completed` or `cancelled` |
|
|
47
51
|
| `monitors` | live watches (newest first), then the five most recently ended, each with its last 20 matched lines and, once ended, a readable reason |
|
|
48
52
|
| `evalCells` | detached eval cells from `eval_cells.list`, or from the codemode wake source when the list is unavailable (`evalCellsListed: false`) |
|
|
@@ -50,27 +54,27 @@ OpenChamber host ── loopback + OPENCHAMBER_SERVICE_TOKEN ──▶ service/m
|
|
|
50
54
|
| `goal` | the goal continuation timer (`armed`, `kind`, `dueAtMs`) and the wake-source counts it waits on, or `null` |
|
|
51
55
|
| `wakeSources` | other active wake sources without a dedicated view |
|
|
52
56
|
|
|
53
|
-
The frame
|
|
54
|
-
|
|
57
|
+
The frame shows `todo`, `goal` and `wakeSources`. `monitors`, `evalCells` and
|
|
58
|
+
`background` stay in the snapshot, and the adapter keeps its
|
|
59
|
+
`/antechamber/work-status/<session>/eval/<cell>[/stop]` routes, for section
|
|
60
|
+
versions installed before these moved to OpenChamber's background commands
|
|
61
|
+
(ZIP installs never update). The frame checks the envelope (`src/model.ts`) and
|
|
62
|
+
imports these types from the adapter, so a shape change on either side fails
|
|
63
|
+
the typecheck.
|
|
55
64
|
|
|
56
65
|
## Refresh
|
|
57
66
|
|
|
58
67
|
The frame polls `/status` one request at a time:
|
|
59
68
|
|
|
60
|
-
- every 1.5 s while anything can change on its own (
|
|
61
|
-
|
|
69
|
+
- every 1.5 s while anything it shows can change on its own (an armed timer,
|
|
70
|
+
another wake source, a task in progress);
|
|
62
71
|
- every 5 s otherwise, and while the page is hidden;
|
|
63
72
|
- every 10 s after an error, keeping the last snapshot on screen with the
|
|
64
73
|
error above it.
|
|
65
74
|
|
|
66
75
|
A session switch bumps a generation counter, so a late answer for the previous
|
|
67
76
|
session is dropped. Elapsed times and countdowns tick every second from the
|
|
68
|
-
snapshot without refetching.
|
|
69
|
-
poll while the cell runs, and once more when its state changes, so a finished
|
|
70
|
-
cell shows its last lines.
|
|
71
|
-
|
|
72
|
-
The adapter caches `eval_cells.list` for one second per session, so several
|
|
73
|
-
open sections cost one RPC.
|
|
77
|
+
snapshot without refetching.
|
|
74
78
|
|
|
75
79
|
## Choosing the server
|
|
76
80
|
|
|
@@ -97,21 +101,13 @@ loopback.
|
|
|
97
101
|
- **Frame**: the sandboxed iframe holds no token. It reaches the service only
|
|
98
102
|
through the host, which adds its own per-service token. All text from OmO is
|
|
99
103
|
inserted with `textContent`, never as HTML.
|
|
100
|
-
- **Inputs**: session IDs must match `ses_[0-9A-Za-z]
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
(`eval_cells.stop`, `goal_continuation_fire_now`). The extension kills no
|
|
105
|
-
process itself.
|
|
104
|
+
- **Inputs**: session IDs must match `ses_[0-9A-Za-z]+`, in both the service
|
|
105
|
+
and the adapter.
|
|
106
|
+
- **Actions**: Fire now maps to OmO's own RPC request
|
|
107
|
+
(`goal_continuation_fire_now`). The extension kills no process itself.
|
|
106
108
|
|
|
107
109
|
## Limits
|
|
108
110
|
|
|
109
|
-
- No
|
|
110
|
-
holds: OmO exposes neither over RPC.
|
|
111
|
-
- Eval cells need OmO's codemode. Without it, or while the agent process is
|
|
112
|
-
stopped, the section shows the cells OmO last reported, without output or
|
|
113
|
-
Stop.
|
|
114
|
-
- Background sessions come from OmO's terminal manifest, which OmO writes on
|
|
115
|
-
start and exit; their command is shown but not their output.
|
|
111
|
+
- No continuation holds: OmO does not publish them over RPC.
|
|
116
112
|
- The service must run on the machine that runs antechamber, since it reads
|
|
117
113
|
the run records.
|
|
@@ -52,7 +52,6 @@ function discoverServers(stateDirs, isAlive = alive) {
|
|
|
52
52
|
|
|
53
53
|
// extensions/omo-work/src/service.ts
|
|
54
54
|
var SESSION_ID = /^ses_[0-9A-Za-z]+$/;
|
|
55
|
-
var CELL_ID = /^[\w.:-]{1,128}$/;
|
|
56
55
|
var STATUS_TIMEOUT_MS = 5e3;
|
|
57
56
|
var ACTION_TIMEOUT_MS = 15e3;
|
|
58
57
|
async function call(server, method, path, timeoutMs) {
|
|
@@ -131,20 +130,11 @@ function createServer(options) {
|
|
|
131
130
|
if (!SESSION_ID.test(session)) return fail(res, 400, "session query must be an OpenChamber session id");
|
|
132
131
|
const custom = url.searchParams.get("stateDir")?.trim();
|
|
133
132
|
const stateDirs = custom ? [resolve(expandHome(custom))] : defaultDirs();
|
|
134
|
-
const cell = url.searchParams.get("cell") ?? "";
|
|
135
133
|
const method = req.method ?? "GET";
|
|
136
134
|
req.resume();
|
|
137
135
|
if (url.pathname === "/status" && method === "GET") return void relayStatus(res, session, stateDirs);
|
|
138
136
|
if (url.pathname === "/goal/fire" && method === "POST")
|
|
139
137
|
return void relayAction(res, session, stateDirs, "POST", "goal/fire");
|
|
140
|
-
if (url.pathname.startsWith("/eval")) {
|
|
141
|
-
if (!CELL_ID.test(cell)) return fail(res, 400, "cell query must be an eval cell id");
|
|
142
|
-
const tail = Number(url.searchParams.get("tailBytes") ?? 8192);
|
|
143
|
-
if (url.pathname === "/eval" && method === "GET")
|
|
144
|
-
return void relayAction(res, session, stateDirs, "GET", `eval/${encodeURIComponent(cell)}?tailBytes=${tail}`);
|
|
145
|
-
if (url.pathname === "/eval/stop" && method === "POST")
|
|
146
|
-
return void relayAction(res, session, stateDirs, "POST", `eval/${encodeURIComponent(cell)}/stop`);
|
|
147
|
-
}
|
|
148
138
|
return fail(res, 404, "not-found");
|
|
149
139
|
});
|
|
150
140
|
}
|