antechamber 1.0.0 → 1.0.2

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.
@@ -3,43 +3,42 @@ import { toolMappers } from "./tool-map.js";
3
3
  function record(value) {
4
4
  return value !== null && typeof value === "object" && !Array.isArray(value);
5
5
  }
6
- export function flattenTodos(sessionID, data) {
6
+ function todoStatus(value) {
7
+ switch (value) {
8
+ case "in_progress":
9
+ return "in_progress";
10
+ case "completed":
11
+ return "completed";
12
+ case "abandoned":
13
+ case "dropped":
14
+ case "cancelled":
15
+ return "cancelled";
16
+ default:
17
+ return "pending";
18
+ }
19
+ }
20
+ /** The `senpi.todo-state` phases, keeping only well-formed phases and tasks. */
21
+ export function todoPhases(data) {
7
22
  if (!record(data) || !Array.isArray(data.phases))
8
23
  return [];
9
- const phases = data.phases.filter(record);
10
- return phases.flatMap((phase) => {
24
+ return data.phases.filter(record).flatMap((phase) => {
11
25
  if (typeof phase.name !== "string" || !Array.isArray(phase.tasks))
12
26
  return [];
13
- return phase.tasks.filter(record).flatMap((task) => {
14
- if (typeof task.content !== "string")
15
- return [];
16
- let status;
17
- switch (task.status) {
18
- case "in_progress":
19
- status = "in_progress";
20
- break;
21
- case "completed":
22
- status = "completed";
23
- break;
24
- case "abandoned":
25
- case "dropped":
26
- case "cancelled":
27
- status = "cancelled";
28
- break;
29
- default:
30
- status = "pending";
31
- }
32
- return [
33
- {
34
- id: createHash("md5").update(`${sessionID}|${phase.name}|${task.content}`).digest("hex").slice(0, 16),
35
- content: phases.length === 1 ? task.content : `${phase.name}: ${task.content}`,
36
- status,
37
- priority: "medium",
38
- },
39
- ];
40
- });
27
+ const tasks = phase.tasks
28
+ .filter(record)
29
+ .flatMap((task) => typeof task.content === "string" ? [{ content: task.content, status: todoStatus(task.status) }] : []);
30
+ return [{ name: phase.name, tasks }];
41
31
  });
42
32
  }
33
+ export function flattenTodos(sessionID, data) {
34
+ const phases = todoPhases(data);
35
+ return phases.flatMap((phase) => phase.tasks.map((task) => ({
36
+ id: createHash("md5").update(`${sessionID}|${phase.name}|${task.content}`).digest("hex").slice(0, 16),
37
+ content: phases.length === 1 ? task.content : `${phase.name}: ${task.content}`,
38
+ status: task.status,
39
+ priority: "medium",
40
+ })));
41
+ }
43
42
  toolMappers.todo = (args) => ({
44
43
  tool: "todowrite",
45
44
  input: { todos: Array.isArray(args.todos) ? args.todos : [] },
@@ -49,6 +48,7 @@ toolMappers.todo = (args) => ({
49
48
  export class TodoBridge {
50
49
  sessionID;
51
50
  todos = [];
51
+ phases = [];
52
52
  awaitingState = [];
53
53
  constructor(sessionID) {
54
54
  this.sessionID = sessionID;
@@ -68,6 +68,7 @@ export class TodoBridge {
68
68
  if (!record(entry) || entry.type !== "custom" || entry.customType !== "senpi.todo-state")
69
69
  return undefined;
70
70
  this.todos = flattenTodos(this.sessionID, entry.data);
71
+ this.phases = todoPhases(entry.data);
71
72
  const part = this.awaitingState.shift();
72
73
  if (part !== undefined) {
73
74
  part.state.input = { todos: this.todos };
package/dist/v2.js CHANGED
@@ -282,6 +282,15 @@ export class V2EventTranslator {
282
282
  ? [this.frame(directory, "session.metadata.updated", { sessionID: info.id, metadata: info.metadata })]
283
283
  : []),
284
284
  this.frame(directory, "session.agent.selected", { sessionID: info.id, agent: info.agent ?? getAgentName() }),
285
+ ...(info.cost !== undefined
286
+ ? [
287
+ this.frame(directory, "session.usage.updated", {
288
+ sessionID: info.id,
289
+ cost: info.cost,
290
+ tokens: info.tokens ?? zeroTokens(),
291
+ }),
292
+ ]
293
+ : []),
285
294
  ];
286
295
  }
287
296
  case "session.diff":
@@ -462,7 +471,6 @@ export class V2EventTranslator {
462
471
  return [];
463
472
  st.ended = true;
464
473
  return [
465
- this.frame(directory, "session.usage.updated", { sessionID: st.sessionID, cost: st.cost, tokens: st.tokens }),
466
474
  st.finish === "error"
467
475
  ? this.frame(directory, "session.step.failed", {
468
476
  sessionID: st.sessionID,
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-compat",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "OpenChamber extension: displays OmO tool calls the way OmO means them when OpenChamber runs on antechamber",
5
5
  "private": true,
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "antechamber-omo-graph",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "description": "OpenChamber extension: live, interactive graph of the OmO sub-agents behind the current session",
5
5
  "private": true,
6
6
  "type": "module",
@@ -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.2",
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
+ }