antechamber 0.2.37 → 1.0.1
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 +19 -0
- package/README.md +21 -0
- package/dist/commands/serve.js +8 -2
- package/dist/commands/setup.js +10 -0
- package/dist/http.d.ts +4 -1
- package/dist/http.js +14 -1
- package/dist/monitor-sessions.d.ts +11 -2
- package/dist/monitor-sessions.js +19 -4
- package/dist/pi-event-translation.d.ts +2 -0
- package/dist/pi-event-translation.js +1 -0
- package/dist/routes-v2.js +5 -3
- package/dist/running-servers.d.ts +2 -0
- package/dist/server.d.ts +2 -0
- package/dist/server.js +10 -3
- package/dist/sessions.d.ts +2 -0
- package/dist/sessions.js +6 -0
- package/dist/sse.d.ts +7 -1
- package/dist/sse.js +25 -4
- package/dist/todo-bridge.d.ts +11 -0
- package/dist/todo-bridge.js +32 -31
- package/dist/work-status.d.ts +87 -0
- package/dist/work-status.js +308 -0
- package/extensions/omo-compat/package.json +1 -1
- package/extensions/omo-dag/package.json +1 -1
- package/extensions/omo-work/README.md +91 -0
- package/extensions/omo-work/package.json +30 -0
- package/extensions/omo-work/service/main.js +159 -0
- package/extensions/omo-work/status/index.html +78 -0
- package/extensions/omo-work/status/main.js +262 -0
- package/package.json +10 -3
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
2
|
+
import type { MonitorSessions, MonitorView } from "./monitor-sessions.js";
|
|
3
|
+
import type { ManagedSession, SessionManager } from "./sessions.js";
|
|
4
|
+
import type { TodoPhase } from "./todo-bridge.js";
|
|
5
|
+
export declare const WAKE_SOURCE_STATE_EVENT = "wake_source_state";
|
|
6
|
+
export declare const GOAL_TIMER_STATE_EVENT = "goal_continuation_timer_state";
|
|
7
|
+
export declare const GOAL_SCHEDULED_EVENT = "goal_continuation_scheduled";
|
|
8
|
+
export declare const GOAL_FIRE_NOW_REQUEST = "goal_continuation_fire_now";
|
|
9
|
+
export declare const CODEMODE_WAKE_SOURCE = "senpi-codemode";
|
|
10
|
+
export interface WakeSourceItem {
|
|
11
|
+
id: string;
|
|
12
|
+
description: string;
|
|
13
|
+
startedAtMs: number;
|
|
14
|
+
}
|
|
15
|
+
export interface WakeSource {
|
|
16
|
+
source: string;
|
|
17
|
+
activeCount: number;
|
|
18
|
+
items: WakeSourceItem[];
|
|
19
|
+
}
|
|
20
|
+
export interface EvalCellView {
|
|
21
|
+
cellId: string;
|
|
22
|
+
language?: string;
|
|
23
|
+
state: string;
|
|
24
|
+
startedAtMs?: number;
|
|
25
|
+
summary?: string;
|
|
26
|
+
queuedBehind?: string[];
|
|
27
|
+
}
|
|
28
|
+
export interface BackgroundSessionView {
|
|
29
|
+
id: string;
|
|
30
|
+
command: string;
|
|
31
|
+
startedAtMs: number;
|
|
32
|
+
pid?: number;
|
|
33
|
+
}
|
|
34
|
+
export interface GoalTimerView {
|
|
35
|
+
armed: boolean;
|
|
36
|
+
kind?: string;
|
|
37
|
+
dueAtMs?: number;
|
|
38
|
+
/** Live wake-source counts the goal waits on, by source name (`goal_continuation_scheduled`). */
|
|
39
|
+
wakeSources?: Record<string, number>;
|
|
40
|
+
}
|
|
41
|
+
export interface WorkStatus {
|
|
42
|
+
version: 1;
|
|
43
|
+
sessionID: string;
|
|
44
|
+
/** The agent process is running; eval cells and stop actions need it. */
|
|
45
|
+
live: boolean;
|
|
46
|
+
generatedAt: number;
|
|
47
|
+
todo: TodoPhase[];
|
|
48
|
+
monitors: MonitorView[];
|
|
49
|
+
evalCells: EvalCellView[];
|
|
50
|
+
/** False when the cell list could not be read (agent not running, or codemode absent). */
|
|
51
|
+
evalCellsListed: boolean;
|
|
52
|
+
background: BackgroundSessionView[];
|
|
53
|
+
goal: GoalTimerView | null;
|
|
54
|
+
/** Other published wake sources, for work this adapter has no dedicated view for. */
|
|
55
|
+
wakeSources: WakeSource[];
|
|
56
|
+
}
|
|
57
|
+
export declare class WorkStatusError extends Error {
|
|
58
|
+
readonly status: 400 | 404 | 409 | 502;
|
|
59
|
+
constructor(status: 400 | 404 | 409 | 502, message: string);
|
|
60
|
+
}
|
|
61
|
+
export declare function parseWakeSource(data: unknown): WakeSource | undefined;
|
|
62
|
+
export declare function parseEvalCell(value: unknown): EvalCellView | undefined;
|
|
63
|
+
/**
|
|
64
|
+
* Background sessions from the terminal manifest whose process is still alive. OmO writes the
|
|
65
|
+
* file on start and exit, but a crashed agent leaves its last entries behind.
|
|
66
|
+
*/
|
|
67
|
+
export declare function readBackgroundSessions(sessionFile: string, piSessionId: string, isAlive?: (pid: number) => boolean): BackgroundSessionView[];
|
|
68
|
+
export declare class WorkStatusBridge {
|
|
69
|
+
private readonly monitors;
|
|
70
|
+
private readonly work;
|
|
71
|
+
constructor(monitors: MonitorSessions);
|
|
72
|
+
private state;
|
|
73
|
+
observe(s: ManagedSession, ev: Record<string, unknown>): void;
|
|
74
|
+
/** The agent process exited: its in-process wake sources and timers died with it. */
|
|
75
|
+
stopParent(sessionID: string): void;
|
|
76
|
+
snapshot(s: ManagedSession): Promise<WorkStatus>;
|
|
77
|
+
private listEvalCells;
|
|
78
|
+
peekEvalCell(s: ManagedSession, cellId: string, tailBytes: number): Promise<Record<string, unknown>>;
|
|
79
|
+
stopEvalCell(s: ManagedSession, cellId: string): Promise<Record<string, unknown>>;
|
|
80
|
+
fireGoalContinuation(s: ManagedSession): Promise<void>;
|
|
81
|
+
private evalCommand;
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* `/antechamber/work-status/<session>` and its actions. These are antechamber's own routes, not
|
|
85
|
+
* OpenCode's: the OmO Work extension service authenticates with the run-record token.
|
|
86
|
+
*/
|
|
87
|
+
export declare function handleWorkStatus(sessions: Pick<SessionManager, "get" | "workStatus">, req: IncomingMessage, res: ServerResponse, url: URL, method: string): Promise<void>;
|
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Background work of one session for the OmO Work status section (extensions/omo-work):
|
|
3
|
+
* todo phases, terminal monitors, detached eval cells, background terminal sessions and the
|
|
4
|
+
* goal continuation timer. OpenChamber 2.x has no native surface for any of these.
|
|
5
|
+
*
|
|
6
|
+
* Sources, all from installed omo-ai 5.1.33 / senpi 2026.10.13:
|
|
7
|
+
* - `wake_source_state` extension events: senpi-codemode publishes its detached cells over RPC
|
|
8
|
+
* (senpi-codemode/src/index.ts `emitWakeSourceState`); the terminal extension publishes
|
|
9
|
+
* its sources on the in-process bus only (core/extensions/builtin/terminal/extension-state.js).
|
|
10
|
+
* - `eval_cells.list/peek/stop` RPC commands (senpi-codemode/src/extension/eval-cells-rpc.ts).
|
|
11
|
+
* - `goal_continuation_timer_state` / `goal_continuation_scheduled` extension events
|
|
12
|
+
* (core/extensions/builtin/goal/monitor-continuation.js), and the `goal_continuation_fire_now`
|
|
13
|
+
* extension request (goal/rpc-wait-surface.js).
|
|
14
|
+
* - the terminal manifest `<session dir>/extensions/terminal/<session id>.json`, written on every
|
|
15
|
+
* background session start and exit (terminal/terminal-manifest.js), for background sessions.
|
|
16
|
+
*/
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
import { errorBody, json } from "./http.js";
|
|
20
|
+
export const WAKE_SOURCE_STATE_EVENT = "wake_source_state";
|
|
21
|
+
export const GOAL_TIMER_STATE_EVENT = "goal_continuation_timer_state";
|
|
22
|
+
export const GOAL_SCHEDULED_EVENT = "goal_continuation_scheduled";
|
|
23
|
+
export const GOAL_FIRE_NOW_REQUEST = "goal_continuation_fire_now";
|
|
24
|
+
export const CODEMODE_WAKE_SOURCE = "senpi-codemode";
|
|
25
|
+
/** Ended watches and matched lines kept per watch in a snapshot. */
|
|
26
|
+
const MAX_ENDED_MONITORS = 5;
|
|
27
|
+
const MAX_MONITOR_LINES = 20;
|
|
28
|
+
/** `eval_cells.list` answers are reused this long, so several open sections cost one RPC. */
|
|
29
|
+
const EVAL_LIST_TTL_MS = 1000;
|
|
30
|
+
const EVAL_RPC_TIMEOUT_MS = 10_000;
|
|
31
|
+
const MAX_PEEK_BYTES = 64 * 1024;
|
|
32
|
+
export class WorkStatusError extends Error {
|
|
33
|
+
status;
|
|
34
|
+
constructor(status, message) {
|
|
35
|
+
super(message);
|
|
36
|
+
this.status = status;
|
|
37
|
+
this.name = "WorkStatusError";
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
function record(value) {
|
|
41
|
+
return value !== null && typeof value === "object" && !Array.isArray(value);
|
|
42
|
+
}
|
|
43
|
+
function finite(value) {
|
|
44
|
+
return typeof value === "number" && Number.isFinite(value) ? value : undefined;
|
|
45
|
+
}
|
|
46
|
+
function nonEmpty(value) {
|
|
47
|
+
return typeof value === "string" && value !== "" ? value : undefined;
|
|
48
|
+
}
|
|
49
|
+
export function parseWakeSource(data) {
|
|
50
|
+
if (!record(data) || typeof data.source !== "string" || data.source === "")
|
|
51
|
+
return undefined;
|
|
52
|
+
const activeCount = finite(data.activeCount);
|
|
53
|
+
if (activeCount === undefined)
|
|
54
|
+
return undefined;
|
|
55
|
+
const rawItems = Array.isArray(data.items) ? data.items : Array.isArray(data.monitors) ? data.monitors : [];
|
|
56
|
+
const items = rawItems.flatMap((item) => {
|
|
57
|
+
if (!record(item) || typeof item.id !== "string")
|
|
58
|
+
return [];
|
|
59
|
+
const startedAtMs = finite(item.startedAtMs);
|
|
60
|
+
if (startedAtMs === undefined)
|
|
61
|
+
return [];
|
|
62
|
+
return [{ id: item.id, description: nonEmpty(item.description) ?? item.id, startedAtMs }];
|
|
63
|
+
});
|
|
64
|
+
return { source: data.source, activeCount, items };
|
|
65
|
+
}
|
|
66
|
+
export function parseEvalCell(value) {
|
|
67
|
+
if (!record(value) || typeof value.cellId !== "string" || typeof value.state !== "string")
|
|
68
|
+
return undefined;
|
|
69
|
+
const language = nonEmpty(value.language);
|
|
70
|
+
const startedAtMs = finite(value.startedAtMs);
|
|
71
|
+
const summary = nonEmpty(value.summary);
|
|
72
|
+
const queuedBehind = Array.isArray(value.queuedBehind)
|
|
73
|
+
? value.queuedBehind.filter((id) => typeof id === "string")
|
|
74
|
+
: undefined;
|
|
75
|
+
return {
|
|
76
|
+
cellId: value.cellId,
|
|
77
|
+
state: value.state,
|
|
78
|
+
...(language !== undefined ? { language } : {}),
|
|
79
|
+
...(startedAtMs !== undefined ? { startedAtMs } : {}),
|
|
80
|
+
...(summary !== undefined ? { summary } : {}),
|
|
81
|
+
...(queuedBehind !== undefined && queuedBehind.length > 0 ? { queuedBehind } : {}),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
function alive(pid) {
|
|
85
|
+
try {
|
|
86
|
+
process.kill(pid, 0);
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
catch (error) {
|
|
90
|
+
return error instanceof Error && "code" in error && error.code === "EPERM";
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Background sessions from the terminal manifest whose process is still alive. OmO writes the
|
|
95
|
+
* file on start and exit, but a crashed agent leaves its last entries behind.
|
|
96
|
+
*/
|
|
97
|
+
export function readBackgroundSessions(sessionFile, piSessionId, isAlive = alive) {
|
|
98
|
+
let manifest;
|
|
99
|
+
try {
|
|
100
|
+
const path = join(dirname(sessionFile), "extensions", "terminal", `${encodeURIComponent(piSessionId)}.json`);
|
|
101
|
+
manifest = JSON.parse(readFileSync(path, "utf8"));
|
|
102
|
+
}
|
|
103
|
+
catch {
|
|
104
|
+
return []; // Missing (no background session yet), or mid-write: the next poll reads it.
|
|
105
|
+
}
|
|
106
|
+
if (!record(manifest) || !Array.isArray(manifest.backgroundSessions))
|
|
107
|
+
return [];
|
|
108
|
+
return manifest.backgroundSessions.flatMap((entry) => {
|
|
109
|
+
if (!record(entry) || typeof entry.id !== "string")
|
|
110
|
+
return [];
|
|
111
|
+
const startedAtMs = finite(entry.startedAtMs);
|
|
112
|
+
if (startedAtMs === undefined)
|
|
113
|
+
return [];
|
|
114
|
+
const pid = record(entry.runtime) ? finite(entry.runtime.pid) : undefined;
|
|
115
|
+
if (pid !== undefined && !isAlive(pid))
|
|
116
|
+
return [];
|
|
117
|
+
return [
|
|
118
|
+
{
|
|
119
|
+
id: entry.id,
|
|
120
|
+
command: nonEmpty(entry.command) ?? entry.id,
|
|
121
|
+
startedAtMs,
|
|
122
|
+
...(pid !== undefined ? { pid } : {}),
|
|
123
|
+
},
|
|
124
|
+
];
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
export class WorkStatusBridge {
|
|
128
|
+
monitors;
|
|
129
|
+
work = new Map();
|
|
130
|
+
constructor(monitors) {
|
|
131
|
+
this.monitors = monitors;
|
|
132
|
+
}
|
|
133
|
+
state(sessionID) {
|
|
134
|
+
let work = this.work.get(sessionID);
|
|
135
|
+
if (work === undefined) {
|
|
136
|
+
work = { wakeSources: new Map(), goal: null };
|
|
137
|
+
this.work.set(sessionID, work);
|
|
138
|
+
}
|
|
139
|
+
return work;
|
|
140
|
+
}
|
|
141
|
+
observe(s, ev) {
|
|
142
|
+
if (ev.name === WAKE_SOURCE_STATE_EVENT) {
|
|
143
|
+
const source = parseWakeSource(ev.data);
|
|
144
|
+
if (source === undefined)
|
|
145
|
+
return;
|
|
146
|
+
const work = this.state(s.info.id);
|
|
147
|
+
if (source.activeCount > 0)
|
|
148
|
+
work.wakeSources.set(source.source, source);
|
|
149
|
+
else
|
|
150
|
+
work.wakeSources.delete(source.source);
|
|
151
|
+
if (source.source === CODEMODE_WAKE_SOURCE)
|
|
152
|
+
delete work.evalList;
|
|
153
|
+
return;
|
|
154
|
+
}
|
|
155
|
+
if (!record(ev.data))
|
|
156
|
+
return;
|
|
157
|
+
if (ev.name === GOAL_TIMER_STATE_EVENT) {
|
|
158
|
+
const work = this.state(s.info.id);
|
|
159
|
+
const kind = nonEmpty(ev.data.kind);
|
|
160
|
+
const dueAtMs = finite(ev.data.dueAtMs);
|
|
161
|
+
work.goal =
|
|
162
|
+
ev.data.armed === true
|
|
163
|
+
? {
|
|
164
|
+
armed: true,
|
|
165
|
+
...(kind !== undefined ? { kind } : {}),
|
|
166
|
+
...(dueAtMs !== undefined ? { dueAtMs } : {}),
|
|
167
|
+
...(work.goal?.wakeSources !== undefined ? { wakeSources: work.goal.wakeSources } : {}),
|
|
168
|
+
}
|
|
169
|
+
: null;
|
|
170
|
+
}
|
|
171
|
+
else if (ev.name === GOAL_SCHEDULED_EVENT && record(ev.data.wakeSources)) {
|
|
172
|
+
const counts = Object.fromEntries(Object.entries(ev.data.wakeSources).filter((entry) => finite(entry[1]) !== undefined));
|
|
173
|
+
const work = this.state(s.info.id);
|
|
174
|
+
work.goal = { ...(work.goal ?? { armed: false }), wakeSources: counts };
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
/** The agent process exited: its in-process wake sources and timers died with it. */
|
|
178
|
+
stopParent(sessionID) {
|
|
179
|
+
this.work.delete(sessionID);
|
|
180
|
+
}
|
|
181
|
+
async snapshot(s) {
|
|
182
|
+
const work = this.work.get(s.info.id);
|
|
183
|
+
const live = s.pi !== undefined;
|
|
184
|
+
const cells = live ? await this.listEvalCells(s) : undefined;
|
|
185
|
+
const sessionFile = s.piPath ?? s.restored?.piPath;
|
|
186
|
+
const piSessionId = s.piSessionId ?? s.restored?.piSessionId;
|
|
187
|
+
const codemode = work?.wakeSources.get(CODEMODE_WAKE_SOURCE);
|
|
188
|
+
return {
|
|
189
|
+
version: 1,
|
|
190
|
+
sessionID: s.info.id,
|
|
191
|
+
live,
|
|
192
|
+
generatedAt: Date.now(),
|
|
193
|
+
todo: s.todoBridge?.phases ?? [],
|
|
194
|
+
monitors: this.monitors.list(s.info.id, MAX_ENDED_MONITORS, MAX_MONITOR_LINES),
|
|
195
|
+
// Without the list, the live cells the wake source names are still worth showing.
|
|
196
|
+
evalCells: cells ??
|
|
197
|
+
(codemode?.items ?? []).map((item) => ({
|
|
198
|
+
cellId: item.id,
|
|
199
|
+
state: "running",
|
|
200
|
+
startedAtMs: item.startedAtMs,
|
|
201
|
+
summary: item.description,
|
|
202
|
+
})),
|
|
203
|
+
evalCellsListed: cells !== undefined,
|
|
204
|
+
background: sessionFile && piSessionId ? readBackgroundSessions(sessionFile, piSessionId) : [],
|
|
205
|
+
goal: work?.goal ?? null,
|
|
206
|
+
wakeSources: [...(work?.wakeSources.values() ?? [])].filter((source) => source.source !== CODEMODE_WAKE_SOURCE && source.source !== "terminal-monitors"),
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
listEvalCells(s) {
|
|
210
|
+
const work = this.state(s.info.id);
|
|
211
|
+
const now = Date.now();
|
|
212
|
+
if (work.evalList !== undefined && now - work.evalList.at < EVAL_LIST_TTL_MS)
|
|
213
|
+
return work.evalList.cells;
|
|
214
|
+
const cells = this.evalCommand(s, { type: "eval_cells.list" }).then((data) => (Array.isArray(data) ? data.flatMap((cell) => parseEvalCell(cell) ?? []) : undefined), () => undefined);
|
|
215
|
+
work.evalList = { at: now, cells };
|
|
216
|
+
return cells;
|
|
217
|
+
}
|
|
218
|
+
async peekEvalCell(s, cellId, tailBytes) {
|
|
219
|
+
const bytes = Math.max(0, Math.min(MAX_PEEK_BYTES, Math.trunc(tailBytes)));
|
|
220
|
+
const data = await this.evalCommand(s, { type: "eval_cells.peek", cellId, tailBytes: bytes });
|
|
221
|
+
if (!record(data))
|
|
222
|
+
throw new WorkStatusError(502, "OmO answered eval_cells.peek without a cell");
|
|
223
|
+
return data;
|
|
224
|
+
}
|
|
225
|
+
async stopEvalCell(s, cellId) {
|
|
226
|
+
const data = await this.evalCommand(s, { type: "eval_cells.stop", cellId });
|
|
227
|
+
const work = this.work.get(s.info.id);
|
|
228
|
+
if (work !== undefined)
|
|
229
|
+
delete work.evalList;
|
|
230
|
+
if (!record(data))
|
|
231
|
+
throw new WorkStatusError(502, "OmO answered eval_cells.stop without a cell");
|
|
232
|
+
return data;
|
|
233
|
+
}
|
|
234
|
+
async fireGoalContinuation(s) {
|
|
235
|
+
const pi = s.pi;
|
|
236
|
+
if (pi === undefined)
|
|
237
|
+
throw new WorkStatusError(409, "The agent is not running");
|
|
238
|
+
const reply = await pi.command({ type: "extension_request", name: GOAL_FIRE_NOW_REQUEST, data: {} }, EVAL_RPC_TIMEOUT_MS);
|
|
239
|
+
if (!reply.success)
|
|
240
|
+
throw new WorkStatusError(409, reply.error ?? "OmO refused to fire the goal continuation");
|
|
241
|
+
}
|
|
242
|
+
async evalCommand(s, command) {
|
|
243
|
+
const pi = s.pi;
|
|
244
|
+
if (pi === undefined)
|
|
245
|
+
throw new WorkStatusError(409, "The agent is not running");
|
|
246
|
+
const reply = await pi.command(command, EVAL_RPC_TIMEOUT_MS);
|
|
247
|
+
if (reply.success)
|
|
248
|
+
return reply.data;
|
|
249
|
+
const code = reply.errorCode;
|
|
250
|
+
if (code === "eval_cell_not_found")
|
|
251
|
+
throw new WorkStatusError(404, reply.error ?? "Unknown eval cell");
|
|
252
|
+
if (code === "invalid_eval_cell_request")
|
|
253
|
+
throw new WorkStatusError(400, reply.error ?? "Invalid eval cell request");
|
|
254
|
+
throw new WorkStatusError(409, reply.error ?? `OmO refused ${command.type}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
const SESSION_ID = /^ses_[0-9A-Za-z]+$/;
|
|
258
|
+
const CELL_ID = /^[\w.:-]{1,128}$/;
|
|
259
|
+
const DEFAULT_PEEK_BYTES = 8 * 1024;
|
|
260
|
+
/**
|
|
261
|
+
* `/antechamber/work-status/<session>` and its actions. These are antechamber's own routes, not
|
|
262
|
+
* OpenCode's: the OmO Work extension service authenticates with the run-record token.
|
|
263
|
+
*/
|
|
264
|
+
export async function handleWorkStatus(sessions, req, res, url, method) {
|
|
265
|
+
const [sessionID, kind, target, action] = url.pathname
|
|
266
|
+
.slice("/antechamber/work-status/".length)
|
|
267
|
+
.split("/")
|
|
268
|
+
.map((part) => decodeURIComponent(part));
|
|
269
|
+
if (sessionID === undefined || !SESSION_ID.test(sessionID)) {
|
|
270
|
+
json(res, 400, errorBody("BadRequestError", "Expected /antechamber/work-status/<session id>"));
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
const s = sessions.get(sessionID);
|
|
274
|
+
if (s === undefined) {
|
|
275
|
+
json(res, 404, errorBody("NotFoundError", `Session not found: ${sessionID}`));
|
|
276
|
+
return;
|
|
277
|
+
}
|
|
278
|
+
try {
|
|
279
|
+
if (kind === undefined && method === "GET") {
|
|
280
|
+
json(res, 200, await sessions.workStatus.snapshot(s));
|
|
281
|
+
return;
|
|
282
|
+
}
|
|
283
|
+
if (kind === "eval" && target !== undefined && CELL_ID.test(target)) {
|
|
284
|
+
if (action === undefined && method === "GET") {
|
|
285
|
+
const tail = Number(url.searchParams.get("tailBytes") ?? DEFAULT_PEEK_BYTES);
|
|
286
|
+
json(res, 200, await sessions.workStatus.peekEvalCell(s, target, Number.isFinite(tail) ? tail : DEFAULT_PEEK_BYTES));
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
if (action === "stop" && method === "POST") {
|
|
290
|
+
req.resume();
|
|
291
|
+
json(res, 200, await sessions.workStatus.stopEvalCell(s, target));
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (kind === "goal" && target === "fire" && action === undefined && method === "POST") {
|
|
296
|
+
req.resume();
|
|
297
|
+
await sessions.workStatus.fireGoalContinuation(s);
|
|
298
|
+
res.writeHead(204).end();
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
json(res, 404, errorBody("NotFoundError", `Unknown work-status route: ${method} ${url.pathname}`));
|
|
302
|
+
}
|
|
303
|
+
catch (error) {
|
|
304
|
+
if (!(error instanceof WorkStatusError))
|
|
305
|
+
throw error;
|
|
306
|
+
json(res, error.status, errorBody(error.name, error.message));
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# OmO Work: OpenChamber extension
|
|
2
|
+
|
|
3
|
+
An **OmO Work** section in OpenChamber's Work Status panel, for the background
|
|
4
|
+
work of the session you are looking at. It ships with
|
|
5
|
+
[antechamber](../../README.md) and needs OpenChamber **2.3.2 or newer** (web or
|
|
6
|
+
desktop). OpenChamber 1.x has no extension sections, and VS Code and mobile
|
|
7
|
+
load no extensions.
|
|
8
|
+
|
|
9
|
+
For the open session it shows:
|
|
10
|
+
|
|
11
|
+
- **Todo**: OmO's todo list grouped by phase, with progress per phase. The
|
|
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
|
+
- **Goal**: the goal continuation timer, what it waits on, and **Fire now**.
|
|
21
|
+
- **Other activity**: any other wake source OmO reports.
|
|
22
|
+
|
|
23
|
+
The section sizes itself to its content, up to OpenChamber's 320 px limit, and
|
|
24
|
+
scrolls inside beyond that.
|
|
25
|
+
|
|
26
|
+
## Install
|
|
27
|
+
|
|
28
|
+
In OpenChamber, open **Settings → Extensions → Add**:
|
|
29
|
+
|
|
30
|
+
- **Folder**, when OpenChamber runs on the machine where antechamber is
|
|
31
|
+
installed: the path `antechamber setup` prints,
|
|
32
|
+
`$(npm root -g)/antechamber/extensions/omo-work` for npm installs or
|
|
33
|
+
`<checkout>/extensions/omo-work` after `npm run build`.
|
|
34
|
+
- **URL**:
|
|
35
|
+
`https://github.com/eliottness/antechamber/releases/latest/download/antechamber-omo-work.zip`.
|
|
36
|
+
- **ZIP file**: the same archive, or
|
|
37
|
+
`npm run build && npm run pack:work-extension -- antechamber-omo-work.zip`.
|
|
38
|
+
|
|
39
|
+
Approve its **local service**. The section then appears in the Work Status
|
|
40
|
+
panel; the panel's sections dialog can hide or reorder it.
|
|
41
|
+
|
|
42
|
+
## Requirements
|
|
43
|
+
|
|
44
|
+
- antechamber with the `/antechamber/work-status` routes (the release that
|
|
45
|
+
added this extension or newer), running as OpenChamber's OpenCode server.
|
|
46
|
+
After upgrading antechamber, restart OpenChamber so it runs the new adapter;
|
|
47
|
+
`antechamber doctor` reports the running version.
|
|
48
|
+
- The extension service runs on the OpenChamber host and reads antechamber's
|
|
49
|
+
run records, so antechamber must run on the same machine.
|
|
50
|
+
|
|
51
|
+
## How it works
|
|
52
|
+
|
|
53
|
+
```
|
|
54
|
+
section (sandboxed iframe) --serviceRequest /status?session=ses_...--> OpenChamber host
|
|
55
|
+
| loopback + host token
|
|
56
|
+
v
|
|
57
|
+
service/main.js --Bearer <run token>--> antechamber
|
|
58
|
+
reads <state dir>/run/<pid>.json /antechamber/work-status/<session>
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`antechamber serve` writes `<state dir>/run/<pid>.json` (mode 0600) with its
|
|
62
|
+
URL and a random token that opens only the `/antechamber/work-status` routes. The service
|
|
63
|
+
picks the running server whose agent holds the session and relays the snapshot
|
|
64
|
+
and the Stop and Fire now actions. It keeps no state. See
|
|
65
|
+
[docs/architecture.md](../../docs/architecture.md#omo-work-status-section) for
|
|
66
|
+
where each piece of data comes from in OmO.
|
|
67
|
+
|
|
68
|
+
The state directory defaults to `~/.config/antechamber`, then
|
|
69
|
+
`~/.config/openchamber-pi-connector`. OpenChamber starts extension services
|
|
70
|
+
with a minimal environment, so if your deployment sets `ANTECHAMBER_STATE_DIR`,
|
|
71
|
+
enter the same path under **Data path** in the section header. It is saved in
|
|
72
|
+
the extension's OpenChamber storage.
|
|
73
|
+
|
|
74
|
+
## Limitations
|
|
75
|
+
|
|
76
|
+
OmO does not expose these over RPC, so the section cannot offer them:
|
|
77
|
+
|
|
78
|
+
- stopping a monitor or a background terminal session;
|
|
79
|
+
- continuation holds (the loop guard publishes them in-process only).
|
|
80
|
+
|
|
81
|
+
Eval cells need OmO's codemode. Without it, or while the agent process is not
|
|
82
|
+
running, the section lists the cells OmO last reported, without output or Stop.
|
|
83
|
+
|
|
84
|
+
## Development
|
|
85
|
+
|
|
86
|
+
From the repository root: `npm run build:extension` bundles `src/status.ts`
|
|
87
|
+
into `status/main.js` (browser IIFE) and `src/service-main.ts` into
|
|
88
|
+
`service/main.js` (Node ESM); `npm test`, `npm run typecheck`, `npm run lint`
|
|
89
|
+
and `npm run format:check` cover it. `src/model.ts` imports the snapshot types
|
|
90
|
+
from the adapter (`src/work-status.ts`), so a shape change on either side fails
|
|
91
|
+
the typecheck.
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "antechamber-omo-work",
|
|
3
|
+
"version": "1.0.1",
|
|
4
|
+
"description": "OpenChamber 2.x Work Status section: OmO todo phases, monitors, eval cells and background work of the current session",
|
|
5
|
+
"private": true,
|
|
6
|
+
"type": "module",
|
|
7
|
+
"license": "MIT",
|
|
8
|
+
"openchamber": {
|
|
9
|
+
"apiVersion": 1,
|
|
10
|
+
"engines": {
|
|
11
|
+
"openchamber": ">=2.3.2"
|
|
12
|
+
},
|
|
13
|
+
"contributes": {
|
|
14
|
+
"panel": {
|
|
15
|
+
"id": "omo-work",
|
|
16
|
+
"name": "OmO Work",
|
|
17
|
+
"icon": "list-check-3"
|
|
18
|
+
},
|
|
19
|
+
"statusSection": {
|
|
20
|
+
"entry": "status/index.html",
|
|
21
|
+
"title": "OmO Work",
|
|
22
|
+
"height": 120
|
|
23
|
+
},
|
|
24
|
+
"service": {
|
|
25
|
+
"entry": "service/main.js",
|
|
26
|
+
"runtime": "host"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
}
|