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
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,25 @@ and this project uses [Semantic Versioning](https://semver.org/).
|
|
|
7
7
|
|
|
8
8
|
## [Unreleased]
|
|
9
9
|
|
|
10
|
+
## [1.0.1] - 2026-10-20
|
|
11
|
+
|
|
12
|
+
### Added
|
|
13
|
+
|
|
14
|
+
- OmO Work, an OpenChamber 2.x Work Status section (`extensions/omo-work`). For the open session it
|
|
15
|
+
shows OmO's todo list by phase, terminal monitors with their matched lines, detached eval cells
|
|
16
|
+
(output peek and Stop), background terminal sessions and the goal continuation timer (Fire now).
|
|
17
|
+
Its service reaches antechamber through new `/antechamber/work-status` routes, authenticated by a
|
|
18
|
+
token `serve` writes into its run record.
|
|
19
|
+
|
|
20
|
+
### Changed
|
|
21
|
+
|
|
22
|
+
- On OpenChamber 2.x, OmO monitors are no longer listed as subagent sessions; the OmO Work section
|
|
23
|
+
shows them. OpenChamber 1.24 still sees them as child sessions.
|
|
24
|
+
|
|
25
|
+
## [1.0.0] - 2026-10-20
|
|
26
|
+
|
|
27
|
+
- First stable release. No functional changes since 0.2.37.
|
|
28
|
+
|
|
10
29
|
## [0.2.37] - 2026-10-20
|
|
11
30
|
|
|
12
31
|
- fix(questions): drop optional comment field that OpenChamber 2.4 makes mandatory (#45)
|
package/README.md
CHANGED
|
@@ -40,6 +40,9 @@ an **external OpenCode server** and sessions run on OmO.
|
|
|
40
40
|
- An optional OpenChamber extension, **OmO Graph**, that draws the OmO
|
|
41
41
|
sub-agents behind the open session as a live, clickable graph (see
|
|
42
42
|
[OpenChamber extension](#openchamber-extension-omo-graph)).
|
|
43
|
+
- An optional OpenChamber 2.x Work Status section, **OmO Work**, for the
|
|
44
|
+
session's todo phases, monitors, eval cells and background work (see
|
|
45
|
+
[OmO Work](#openchamber-extension-omo-work)).
|
|
43
46
|
|
|
44
47
|
## Supported backends
|
|
45
48
|
|
|
@@ -234,6 +237,24 @@ OpenChamber clones the repository root, which is antechamber itself, not the
|
|
|
234
237
|
extension, and refuses it as an invalid manifest. Details:
|
|
235
238
|
[extensions/omo-dag/README.md](extensions/omo-dag/README.md).
|
|
236
239
|
|
|
240
|
+
## OpenChamber extension: OmO Work
|
|
241
|
+
|
|
242
|
+
`extensions/omo-work` adds an **OmO Work** section to OpenChamber 2.x's Work
|
|
243
|
+
Status panel (OpenChamber 2.3.2 or newer; 1.x has no extension sections). For
|
|
244
|
+
the open session it shows the OmO todo list grouped by phase, terminal
|
|
245
|
+
monitors with their matched lines, detached eval cells with their output and a
|
|
246
|
+
Stop button, background terminal sessions, and the goal continuation timer
|
|
247
|
+
with a Fire now button. On 2.x, monitors appear here instead of as subagent
|
|
248
|
+
sessions.
|
|
249
|
+
|
|
250
|
+
Add it like OmO Graph: the folder `antechamber setup` prints
|
|
251
|
+
(`.../extensions/omo-work`, built by `npm run build`), or
|
|
252
|
+
`https://github.com/eliottness/antechamber/releases/latest/download/antechamber-omo-work.zip`.
|
|
253
|
+
Approve its local service. It needs an antechamber version that writes a
|
|
254
|
+
work-status token into its run record (this release or newer) and an
|
|
255
|
+
OpenChamber restart after upgrading. Details:
|
|
256
|
+
[extensions/omo-work/README.md](extensions/omo-work/README.md).
|
|
257
|
+
|
|
237
258
|
## OpenChamber extension: OmO Compatibility
|
|
238
259
|
|
|
239
260
|
`extensions/omo-compat` is a second, code-free extension for OpenChamber 1.24
|
package/dist/commands/serve.js
CHANGED
|
@@ -28,10 +28,16 @@ export async function serve(options) {
|
|
|
28
28
|
process.on("SIGTERM", shutdown);
|
|
29
29
|
await server.start();
|
|
30
30
|
try {
|
|
31
|
-
forget = recordRunningServer(envSetting("STATE_DIR") ?? defaultStateDir(homedir(), fs.existsSync), {
|
|
31
|
+
forget = recordRunningServer(envSetting("STATE_DIR") ?? defaultStateDir(homedir(), fs.existsSync), {
|
|
32
|
+
pid: process.pid,
|
|
33
|
+
version: ADAPTER_VERSION,
|
|
34
|
+
url: server.url,
|
|
35
|
+
started: new Date().toISOString(),
|
|
36
|
+
token: server.extensionToken,
|
|
37
|
+
}, fs);
|
|
32
38
|
}
|
|
33
39
|
catch (error) {
|
|
34
|
-
//
|
|
40
|
+
// Doctor and the OmO Work extension read the record; serving must not depend on it.
|
|
35
41
|
console.error(`antechamber: cannot record the running version: ${error instanceof Error ? error.message : String(error)}`);
|
|
36
42
|
}
|
|
37
43
|
if (compatible)
|
package/dist/commands/setup.js
CHANGED
|
@@ -18,6 +18,12 @@ function omoGraphExtensionDir(ctx) {
|
|
|
18
18
|
const built = ["package.json", "panel/main.js", "service/main.js"].every((file) => ctx.fs.existsSync(join(dir, file)));
|
|
19
19
|
return built ? dir : undefined;
|
|
20
20
|
}
|
|
21
|
+
/** The bundled OmO Work status-section extension folder, when this installation has its built files. */
|
|
22
|
+
function omoWorkExtensionDir(ctx) {
|
|
23
|
+
const dir = resolve(dirname(ctx.cliPath), "..", "extensions", "omo-work");
|
|
24
|
+
const built = ["package.json", "status/main.js", "service/main.js"].every((file) => ctx.fs.existsSync(join(dir, file)));
|
|
25
|
+
return built ? dir : undefined;
|
|
26
|
+
}
|
|
21
27
|
/** The bundled OmO Compatibility extension folder; it is a manifest only, so it needs no build. */
|
|
22
28
|
function omoCompatExtensionDir(ctx) {
|
|
23
29
|
const dir = resolve(dirname(ctx.cliPath), "..", "extensions", "omo-compat");
|
|
@@ -179,6 +185,10 @@ export function setup(options, ctx = createContext()) {
|
|
|
179
185
|
if (extension !== undefined) {
|
|
180
186
|
report(`Optional OmO Graph extension: in OpenChamber, Settings \u2192 Extensions \u2192 Add \u2192 Folder: ${extension}`);
|
|
181
187
|
}
|
|
188
|
+
const work = omoWorkExtensionDir(ctx);
|
|
189
|
+
if (work !== undefined) {
|
|
190
|
+
report(`Optional OmO Work section (todo phases, monitors, eval cells; OpenChamber 2.x): Settings \u2192 Extensions \u2192 Add \u2192 Folder: ${work}`);
|
|
191
|
+
}
|
|
182
192
|
const compat = omoCompatExtensionDir(ctx);
|
|
183
193
|
if (compat !== undefined) {
|
|
184
194
|
report(`Optional OmO Compatibility extension (eval card headers): Settings \u2192 Extensions \u2192 Add \u2192 Folder: ${compat}`);
|
package/dist/http.d.ts
CHANGED
|
@@ -13,11 +13,14 @@ export declare function errorBody(name: string, message: string): {
|
|
|
13
13
|
};
|
|
14
14
|
};
|
|
15
15
|
export declare function json(res: ServerResponse, status: number, body: unknown): void;
|
|
16
|
+
/** Routes the OmO Work extension service calls with the run-record token instead of the server password. */
|
|
17
|
+
export declare const EXTENSION_ROUTE_PREFIX = "/antechamber/work-status/";
|
|
16
18
|
export declare class Http {
|
|
17
19
|
private serverOrigin?;
|
|
18
20
|
private readonly credentials;
|
|
19
21
|
private readonly maxBodyBytes;
|
|
20
|
-
|
|
22
|
+
private readonly extensionToken;
|
|
23
|
+
constructor(serverOrigin?: (() => string) | undefined, extensionToken?: string);
|
|
21
24
|
allowsUpgrade(req: IncomingMessage): boolean;
|
|
22
25
|
route(req: IncomingMessage, res: ServerResponse, handle: () => Promise<void>): Promise<void>;
|
|
23
26
|
body(req: IncomingMessage): Promise<unknown>;
|
package/dist/http.js
CHANGED
|
@@ -45,12 +45,17 @@ export function json(res, status, body) {
|
|
|
45
45
|
const payload = JSON.stringify(body);
|
|
46
46
|
res.writeHead(status, { "Content-Type": "application/json" }).end(payload);
|
|
47
47
|
}
|
|
48
|
+
/** Routes the OmO Work extension service calls with the run-record token instead of the server password. */
|
|
49
|
+
export const EXTENSION_ROUTE_PREFIX = "/antechamber/work-status/";
|
|
48
50
|
export class Http {
|
|
49
51
|
serverOrigin;
|
|
50
52
|
credentials;
|
|
51
53
|
maxBodyBytes;
|
|
52
|
-
|
|
54
|
+
extensionToken;
|
|
55
|
+
constructor(serverOrigin, extensionToken) {
|
|
53
56
|
this.serverOrigin = serverOrigin;
|
|
57
|
+
this.extensionToken =
|
|
58
|
+
extensionToken === undefined ? undefined : createHash("sha256").update(`Bearer ${extensionToken}`).digest();
|
|
54
59
|
const maxBodyBytes = Number(envSetting("MAX_BODY_BYTES"));
|
|
55
60
|
this.maxBodyBytes = Number.isSafeInteger(maxBodyBytes) && maxBodyBytes > 0 ? maxBodyBytes : DEFAULT_MAX_BODY_BYTES;
|
|
56
61
|
const password = serverPassword();
|
|
@@ -182,6 +187,14 @@ export class Http {
|
|
|
182
187
|
authorized(req) {
|
|
183
188
|
if (this.credentials === undefined)
|
|
184
189
|
return true;
|
|
190
|
+
if (this.extensionToken !== undefined && // The router resolves dot segments, %2e and backslashes, so the check must see the same path.
|
|
191
|
+
new URL(req.url ?? "/", "http://localhost").pathname.startsWith(EXTENSION_ROUTE_PREFIX)) {
|
|
192
|
+
const supplied = createHash("sha256")
|
|
193
|
+
.update(req.headers.authorization ?? "")
|
|
194
|
+
.digest();
|
|
195
|
+
if (timingSafeEqual(this.extensionToken, supplied))
|
|
196
|
+
return true;
|
|
197
|
+
}
|
|
185
198
|
const match = req.headers.authorization?.match(/^Basic\s+([A-Za-z0-9+/]+=*)$/i);
|
|
186
199
|
const supplied = createHash("sha256")
|
|
187
200
|
.update(Buffer.from(match?.[1] ?? "", "base64"))
|
|
@@ -17,11 +17,19 @@ export interface MonitorSnapshot {
|
|
|
17
17
|
fireCount: number;
|
|
18
18
|
lastFiredAtMs: number | null;
|
|
19
19
|
}
|
|
20
|
-
interface MonitorEnding {
|
|
20
|
+
export interface MonitorEnding {
|
|
21
21
|
reason: string;
|
|
22
22
|
exitCode: number | null;
|
|
23
23
|
endedAtMs: number;
|
|
24
24
|
}
|
|
25
|
+
/** One watch of a parent, for the Work Status section. */
|
|
26
|
+
export interface MonitorView {
|
|
27
|
+
monitor: MonitorSnapshot;
|
|
28
|
+
ending?: MonitorEnding & {
|
|
29
|
+
label: string;
|
|
30
|
+
};
|
|
31
|
+
lines: string[];
|
|
32
|
+
}
|
|
25
33
|
export declare function parseMonitor(value: unknown): MonitorSnapshot | undefined;
|
|
26
34
|
export declare function monitorSummary(monitor: MonitorSnapshot, ending?: MonitorEnding): string;
|
|
27
35
|
export declare class MonitorSessions {
|
|
@@ -33,6 +41,8 @@ export declare class MonitorSessions {
|
|
|
33
41
|
constructor(sessions: Map<string, ManagedSession>, emit: (directory: string, event: OCEvent) => void);
|
|
34
42
|
wire(parent: ManagedSession): void;
|
|
35
43
|
owns(id: string): boolean;
|
|
44
|
+
/** A parent's watches, live first then the newest ended ones, with their latest matched lines. */
|
|
45
|
+
list(parentID: string, maxEnded: number, maxLines: number): MonitorView[];
|
|
36
46
|
messages(id: string): OCMessageWithParts[];
|
|
37
47
|
observe(parent: ManagedSession, ev: Record<string, unknown>): void;
|
|
38
48
|
/** A hidden `senpi-monitor:notification` custom message: append its lines to each watch it names. */
|
|
@@ -55,4 +65,3 @@ export declare class MonitorSessions {
|
|
|
55
65
|
private create;
|
|
56
66
|
private publish;
|
|
57
67
|
}
|
|
58
|
-
export {};
|
package/dist/monitor-sessions.js
CHANGED
|
@@ -88,6 +88,20 @@ export class MonitorSessions {
|
|
|
88
88
|
owns(id) {
|
|
89
89
|
return this.watches.has(id);
|
|
90
90
|
}
|
|
91
|
+
/** A parent's watches, live first then the newest ended ones, with their latest matched lines. */
|
|
92
|
+
list(parentID, maxEnded, maxLines) {
|
|
93
|
+
const watches = [...this.watches.values()].filter((watch) => watch.session.info.parentID === parentID);
|
|
94
|
+
const live = watches.filter((watch) => watch.ending === undefined);
|
|
95
|
+
const ended = watches
|
|
96
|
+
.filter((watch) => watch.ending !== undefined)
|
|
97
|
+
.sort((a, b) => (b.ending?.endedAtMs ?? 0) - (a.ending?.endedAtMs ?? 0))
|
|
98
|
+
.slice(0, maxEnded);
|
|
99
|
+
return [...live.sort((a, b) => b.monitor.startedAtMs - a.monitor.startedAtMs), ...ended].map(({ monitor, ending, lines }) => ({
|
|
100
|
+
monitor: { ...monitor },
|
|
101
|
+
...(ending ? { ending: { ...ending, label: endingLabel(ending) } } : {}),
|
|
102
|
+
lines: lines.slice(-maxLines),
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
91
105
|
messages(id) {
|
|
92
106
|
return this.watches.get(id)?.session.messages ?? [];
|
|
93
107
|
}
|
|
@@ -151,12 +165,13 @@ export class MonitorSessions {
|
|
|
151
165
|
for (const [childID, watch] of this.watches) {
|
|
152
166
|
if (childID !== id && watch.session.info.parentID !== id)
|
|
153
167
|
continue;
|
|
168
|
+
// Emitted while the watch is still owned, so the v2 stream keeps hiding it.
|
|
169
|
+
if (this.sessions.get(childID) === watch.session) {
|
|
170
|
+
this.emit(watch.session.info.directory, { type: "session.deleted", properties: { info: watch.session.info } });
|
|
171
|
+
this.sessions.delete(childID);
|
|
172
|
+
}
|
|
154
173
|
this.watches.delete(childID);
|
|
155
174
|
this.dismissed.add(childID);
|
|
156
|
-
if (this.sessions.get(childID) !== watch.session)
|
|
157
|
-
continue;
|
|
158
|
-
this.sessions.delete(childID);
|
|
159
|
-
this.emit(watch.session.info.directory, { type: "session.deleted", properties: { info: watch.session.info } });
|
|
160
175
|
}
|
|
161
176
|
}
|
|
162
177
|
running(parentID) {
|
|
@@ -4,6 +4,7 @@ import type { ManagedSession } from "./sessions.js";
|
|
|
4
4
|
import type { SessionStore } from "./session-store.js";
|
|
5
5
|
import { type SubagentBridge } from "./subagents.js";
|
|
6
6
|
import { type MonitorSessions } from "./monitor-sessions.js";
|
|
7
|
+
import type { WorkStatusBridge } from "./work-status.js";
|
|
7
8
|
import { type TodoUpdated } from "./todo-bridge.js";
|
|
8
9
|
export interface PiUsage {
|
|
9
10
|
input?: number;
|
|
@@ -40,6 +41,7 @@ interface PiEventServices {
|
|
|
40
41
|
readonly emit: (directory: string, event: OCEvent | TodoUpdated) => void;
|
|
41
42
|
readonly subagents: SubagentBridge;
|
|
42
43
|
readonly monitors: MonitorSessions;
|
|
44
|
+
readonly workStatus: Pick<WorkStatusBridge, "observe">;
|
|
43
45
|
readonly store: Pick<SessionStore, "update">;
|
|
44
46
|
readonly dirtyDiffs: Set<string>;
|
|
45
47
|
readonly ensureMessages: (s: ManagedSession) => OCMessageWithParts[];
|
|
@@ -311,6 +311,7 @@ export class PiEventTranslator {
|
|
|
311
311
|
case "extension_event":
|
|
312
312
|
this.services.monitors.observe(s, ev);
|
|
313
313
|
this.services.subagents.observe(s, ev);
|
|
314
|
+
this.services.workStatus.observe(s, ev);
|
|
314
315
|
break;
|
|
315
316
|
default:
|
|
316
317
|
break; // turn_start, queue_update — future milestones
|
package/dist/routes-v2.js
CHANGED
|
@@ -233,7 +233,7 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
233
233
|
}
|
|
234
234
|
// Read-only inventories OpenChamber loads at bootstrap. OmO has no OpenCode-style shells or
|
|
235
235
|
// web-search providers, so each is an honest empty list (mutations stay 400);
|
|
236
|
-
// OmO monitors appear
|
|
236
|
+
// OmO monitors appear in the OmO Work status section instead (work-status.ts).
|
|
237
237
|
if ((path === "/api/shell" || path === "/api/websearch/provider") && method === "GET") {
|
|
238
238
|
json(res, 200, withLocation([]));
|
|
239
239
|
return;
|
|
@@ -328,7 +328,9 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
328
328
|
const parentID = url.searchParams.get("parentID");
|
|
329
329
|
const order = url.searchParams.get("order") === "asc" ? "asc" : "desc";
|
|
330
330
|
const limit = numberParam(url.searchParams.get("limit"));
|
|
331
|
-
let infos = dir !== null && dir !== "" ? services.sessions.list(dir) : services.sessions.listAll()
|
|
331
|
+
let infos = (dir !== null && dir !== "" ? services.sessions.list(dir) : services.sessions.listAll()).filter(
|
|
332
|
+
// OmO monitors are a child session only for v1 clients; 2.x shows them in the OmO Work section.
|
|
333
|
+
(session) => !services.sessions.monitors.owns(session.id));
|
|
332
334
|
if (search)
|
|
333
335
|
infos = infos.filter((s) => s.title.toLowerCase().includes(search.toLowerCase()));
|
|
334
336
|
if (parentID !== null)
|
|
@@ -373,7 +375,7 @@ export async function handleV2(services, req, res, url, path, method, directory)
|
|
|
373
375
|
if (path === "/api/session/active" && method === "GET") {
|
|
374
376
|
const data = {};
|
|
375
377
|
for (const [id, status] of Object.entries(services.sessions.statuses())) {
|
|
376
|
-
if (status.type !== "idle")
|
|
378
|
+
if (status.type !== "idle" && !services.sessions.monitors.owns(id))
|
|
377
379
|
data[id] = { type: "running" };
|
|
378
380
|
}
|
|
379
381
|
json(res, 200, { data });
|
|
@@ -9,6 +9,8 @@ export interface RunningServer {
|
|
|
9
9
|
readonly version: string;
|
|
10
10
|
readonly url: string;
|
|
11
11
|
readonly started: string;
|
|
12
|
+
/** Bearer token for the `/antechamber/work-status` routes; the file is 0600 in a 0700 directory. */
|
|
13
|
+
readonly token?: string;
|
|
12
14
|
}
|
|
13
15
|
type Fs = Pick<typeof fs, "existsSync" | "mkdirSync" | "readdirSync" | "readFileSync" | "rmSync" | "writeFileSync">;
|
|
14
16
|
/** Writes the record and returns its remover; a crash leaves a stale record that readers skip by PID. */
|
package/dist/server.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ export declare class AdapterServer {
|
|
|
20
20
|
private nextWebSocketEventID;
|
|
21
21
|
private catalogPromise;
|
|
22
22
|
private actualPort;
|
|
23
|
+
/** Bearer token for the `/antechamber/work-status` routes, published in the run record for the OmO Work extension. */
|
|
24
|
+
readonly extensionToken: string;
|
|
23
25
|
private readonly http;
|
|
24
26
|
constructor(options: AdapterServerOptions);
|
|
25
27
|
private catalog;
|
package/dist/server.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/** HTTP/SSE composition root for the OpenCode API backed by pi. */
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
2
|
+
import { createHash, randomBytes } from "node:crypto";
|
|
3
3
|
import { createServer } from "node:http";
|
|
4
4
|
import { debugLog } from "./misc-routes.js";
|
|
5
5
|
import { SessionManager } from "./sessions.js";
|
|
@@ -9,6 +9,7 @@ import { SSE } from "./sse.js";
|
|
|
9
9
|
import { handleV1 } from "./routes-v1.js";
|
|
10
10
|
import { handleV2 } from "./routes-v2.js";
|
|
11
11
|
import { selfUpdate } from "./self-update.js";
|
|
12
|
+
import { handleWorkStatus } from "./work-status.js";
|
|
12
13
|
export { isLoopbackHost, serverPassword } from "./http.js";
|
|
13
14
|
export class AdapterServer {
|
|
14
15
|
options;
|
|
@@ -21,10 +22,12 @@ export class AdapterServer {
|
|
|
21
22
|
nextWebSocketEventID = 0;
|
|
22
23
|
catalogPromise;
|
|
23
24
|
actualPort;
|
|
24
|
-
|
|
25
|
+
/** Bearer token for the `/antechamber/work-status` routes, published in the run record for the OmO Work extension. */
|
|
26
|
+
extensionToken = randomBytes(32).toString("base64url");
|
|
27
|
+
http = new Http(() => this.url, this.extensionToken);
|
|
25
28
|
constructor(options) {
|
|
26
29
|
this.options = options;
|
|
27
|
-
this.sse = new SSE(options, this.sseClients);
|
|
30
|
+
this.sse = new SSE(options, this.sseClients, (id) => this.sessions.monitors.owns(id));
|
|
28
31
|
this.sessions = new SessionManager((directory, event) => this.broadcast(directory, event), undefined, options.defaultDirectory);
|
|
29
32
|
this.server = createServer((req, res) => void this.route(req, res));
|
|
30
33
|
this.server.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head));
|
|
@@ -197,6 +200,10 @@ export class AdapterServer {
|
|
|
197
200
|
this.handleSSE(req, res, "v2", directory, directoryQuery === null && headerDirectory === undefined);
|
|
198
201
|
return;
|
|
199
202
|
}
|
|
203
|
+
if (path.startsWith("/antechamber/work-status/")) {
|
|
204
|
+
await handleWorkStatus(this.sessions, req, res, url, method);
|
|
205
|
+
return;
|
|
206
|
+
}
|
|
200
207
|
const services = {
|
|
201
208
|
sessions: this.sessions,
|
|
202
209
|
defaultDirectory: this.options.defaultDirectory,
|
package/dist/sessions.d.ts
CHANGED
|
@@ -10,6 +10,7 @@ import type { TodoBridge, Todo, TodoUpdated } from "./todo-bridge.js";
|
|
|
10
10
|
import { type SessionTreeState } from "./session-tree.js";
|
|
11
11
|
import { SubagentBridge } from "./subagents.js";
|
|
12
12
|
import { MonitorSessions } from "./monitor-sessions.js";
|
|
13
|
+
import { WorkStatusBridge } from "./work-status.js";
|
|
13
14
|
import { SessionStore, type TitleSource } from "./session-store.js";
|
|
14
15
|
import type { OCAssistantMessage, OCEvent, OCMessageWithParts, OCModelRef, OCPart, OCPromptPart, OCSession, OCSessionStatus, OCToolPart } from "./types.js";
|
|
15
16
|
export { ADAPTER_VERSION } from "./version.js";
|
|
@@ -90,6 +91,7 @@ export declare class SessionManager {
|
|
|
90
91
|
readonly lifecycle: SessionLifecycle;
|
|
91
92
|
readonly subagents: SubagentBridge;
|
|
92
93
|
readonly monitors: MonitorSessions;
|
|
94
|
+
readonly workStatus: WorkStatusBridge;
|
|
93
95
|
/** directory → last scan freshness (pi dir mtime + wall clock). */
|
|
94
96
|
private scanCache;
|
|
95
97
|
/** freshness of the all-directories scan (see listAll). */
|
package/dist/sessions.js
CHANGED
|
@@ -17,6 +17,7 @@ import { RequestBodyError } from "./http.js";
|
|
|
17
17
|
import { bindMessageEntries, durableShellTree, followExternalAppends, forkConversation, getEntries, navigate, recordTurn, readSessionTree, revertConversation, SessionTreeError, unrevertConversation, } from "./session-tree.js";
|
|
18
18
|
import { SubagentBridge } from "./subagents.js";
|
|
19
19
|
import { MonitorSessions } from "./monitor-sessions.js";
|
|
20
|
+
import { WorkStatusBridge } from "./work-status.js";
|
|
20
21
|
import { sessionDiff } from "./session-diff.js";
|
|
21
22
|
import { announceSessionNotice, skillCommandText, slashTitlePrompt } from "./session-commands.js";
|
|
22
23
|
import { ocIdFor, piSessionDir, piSessionsRootMtime, scanAllPiSessions, scanPiSessions, SessionStore, readSessionTitle, } from "./session-store.js";
|
|
@@ -45,6 +46,7 @@ export class SessionManager {
|
|
|
45
46
|
lifecycle;
|
|
46
47
|
subagents;
|
|
47
48
|
monitors;
|
|
49
|
+
workStatus;
|
|
48
50
|
/** directory → last scan freshness (pi dir mtime + wall clock). */
|
|
49
51
|
scanCache = new Map();
|
|
50
52
|
/** freshness of the all-directories scan (see listAll). */
|
|
@@ -56,10 +58,12 @@ export class SessionManager {
|
|
|
56
58
|
this.lifecycle = new SessionLifecycle(() => this.sessions.values(), lifecycleOptions);
|
|
57
59
|
this.subagents = new SubagentBridge(this.store, this.sessions, this.emit, (session, raw) => parsePiSessionText(session, getAgentName(), raw));
|
|
58
60
|
this.monitors = new MonitorSessions(this.sessions, this.emit);
|
|
61
|
+
this.workStatus = new WorkStatusBridge(this.monitors);
|
|
59
62
|
this.events = new PiEventTranslator({
|
|
60
63
|
emit: this.emit,
|
|
61
64
|
subagents: this.subagents,
|
|
62
65
|
monitors: this.monitors,
|
|
66
|
+
workStatus: this.workStatus,
|
|
63
67
|
store: this.store,
|
|
64
68
|
dirtyDiffs: this.dirtyDiffs,
|
|
65
69
|
ensureMessages: (s) => this.ensureMessages(s),
|
|
@@ -254,6 +258,7 @@ export class SessionManager {
|
|
|
254
258
|
// Its task cards end with the run; the tasks themselves may still run in OmO's shared host.
|
|
255
259
|
this.subagents.finishCalls(s.info.id, true);
|
|
256
260
|
this.monitors.stopParent(s.info.id);
|
|
261
|
+
this.workStatus.stopParent(s.info.id);
|
|
257
262
|
void this.lifecycle
|
|
258
263
|
.dormant(s)
|
|
259
264
|
.catch((error) => console.error("[lifecycle] exit cleanup failed:", error));
|
|
@@ -348,6 +353,7 @@ export class SessionManager {
|
|
|
348
353
|
const piPath = this.subagents.owns(sessionId) || this.monitors.owns(sessionId) ? undefined : (s.piPath ?? s.restored?.piPath);
|
|
349
354
|
const child = this.subagents.remove(sessionId);
|
|
350
355
|
this.monitors.remove(sessionId);
|
|
356
|
+
this.workStatus.stopParent(sessionId);
|
|
351
357
|
if (piPath !== undefined) {
|
|
352
358
|
try {
|
|
353
359
|
unlinkSync(piPath);
|
package/dist/sse.d.ts
CHANGED
|
@@ -7,10 +7,14 @@ export type SSESubscriber = {
|
|
|
7
7
|
directory?: string;
|
|
8
8
|
global: boolean;
|
|
9
9
|
};
|
|
10
|
+
/** The session an event belongs to, when it names one. */
|
|
11
|
+
export declare function eventSessionID(event: OCEvent | TodoUpdated): string | undefined;
|
|
10
12
|
export declare class SSE {
|
|
11
13
|
private options;
|
|
12
14
|
/** SSE subscribers tagged by protocol flavor and explicit directory scope. */
|
|
13
15
|
readonly clients: Map<ServerResponse, SSESubscriber>;
|
|
16
|
+
/** Sessions v2 clients must not see (OmO monitor children, shown by the OmO Work section). */
|
|
17
|
+
private hiddenFromV2;
|
|
14
18
|
private v2events;
|
|
15
19
|
private nextEventID;
|
|
16
20
|
private replay;
|
|
@@ -18,7 +22,9 @@ export declare class SSE {
|
|
|
18
22
|
defaultDirectory: string;
|
|
19
23
|
},
|
|
20
24
|
/** SSE subscribers tagged by protocol flavor and explicit directory scope. */
|
|
21
|
-
clients: Map<ServerResponse, SSESubscriber
|
|
25
|
+
clients: Map<ServerResponse, SSESubscriber>,
|
|
26
|
+
/** Sessions v2 clients must not see (OmO monitor children, shown by the OmO Work section). */
|
|
27
|
+
hiddenFromV2?: (sessionID: string) => boolean);
|
|
22
28
|
close(): void;
|
|
23
29
|
broadcast(directory: string, event: OCEvent | TodoUpdated): void;
|
|
24
30
|
writeSSE(res: ServerResponse, frame: string): boolean;
|
package/dist/sse.js
CHANGED
|
@@ -5,17 +5,35 @@ import { V2EventTranslator } from "./v2.js";
|
|
|
5
5
|
const MAX_SSE_SUBSCRIBERS = 64;
|
|
6
6
|
const MAX_SSE_PENDING_BYTES = 8 * 1024 * 1024;
|
|
7
7
|
const MAX_SSE_REPLAY_RECORDS = 1024;
|
|
8
|
+
/** The session an event belongs to, when it names one. */
|
|
9
|
+
export function eventSessionID(event) {
|
|
10
|
+
const properties = event.properties;
|
|
11
|
+
const nested = (key) => {
|
|
12
|
+
const value = properties[key];
|
|
13
|
+
return value !== null && typeof value === "object" ? value : undefined;
|
|
14
|
+
};
|
|
15
|
+
if (event.type === "session.created" || event.type === "session.updated" || event.type === "session.deleted")
|
|
16
|
+
return event.properties.info.id;
|
|
17
|
+
const info = nested("info");
|
|
18
|
+
const part = nested("part");
|
|
19
|
+
const id = properties.sessionID ?? info?.sessionID ?? part?.sessionID;
|
|
20
|
+
return typeof id === "string" ? id : undefined;
|
|
21
|
+
}
|
|
8
22
|
export class SSE {
|
|
9
23
|
options;
|
|
10
24
|
clients;
|
|
25
|
+
hiddenFromV2;
|
|
11
26
|
v2events = new V2EventTranslator();
|
|
12
27
|
nextEventID = 0;
|
|
13
28
|
replay = [];
|
|
14
29
|
constructor(options,
|
|
15
30
|
/** SSE subscribers tagged by protocol flavor and explicit directory scope. */
|
|
16
|
-
clients
|
|
31
|
+
clients,
|
|
32
|
+
/** Sessions v2 clients must not see (OmO monitor children, shown by the OmO Work section). */
|
|
33
|
+
hiddenFromV2 = () => false) {
|
|
17
34
|
this.options = options;
|
|
18
35
|
this.clients = clients;
|
|
36
|
+
this.hiddenFromV2 = hiddenFromV2;
|
|
19
37
|
}
|
|
20
38
|
close() {
|
|
21
39
|
for (const res of this.clients.keys()) {
|
|
@@ -24,9 +42,12 @@ export class SSE {
|
|
|
24
42
|
}
|
|
25
43
|
}
|
|
26
44
|
broadcast(directory, event) {
|
|
27
|
-
const
|
|
28
|
-
|
|
29
|
-
|
|
45
|
+
const hidden = this.hiddenFromV2(eventSessionID(event) ?? "");
|
|
46
|
+
const v2Frames = hidden
|
|
47
|
+
? []
|
|
48
|
+
: event.type === "todo.updated"
|
|
49
|
+
? [{ id: `evt_${randomUUID()}`, type: event.type, location: { directory }, data: event.properties }]
|
|
50
|
+
: this.v2events.translate(directory, event);
|
|
30
51
|
const records = v2Frames.length === 0
|
|
31
52
|
? [this.record(directory, { v1: event })]
|
|
32
53
|
: v2Frames.map((v2, index) => this.record(directory, index === 0 ? { v1: event, v2 } : { v2 }));
|
package/dist/todo-bridge.d.ts
CHANGED
|
@@ -5,6 +5,14 @@ export interface Todo {
|
|
|
5
5
|
readonly status: "pending" | "in_progress" | "completed" | "cancelled";
|
|
6
6
|
readonly priority: "medium";
|
|
7
7
|
}
|
|
8
|
+
/** One todo phase as OmO groups it, in its own order (the Work Status section's waves). */
|
|
9
|
+
export interface TodoPhase {
|
|
10
|
+
readonly name: string;
|
|
11
|
+
readonly tasks: ReadonlyArray<{
|
|
12
|
+
readonly content: string;
|
|
13
|
+
readonly status: Todo["status"];
|
|
14
|
+
}>;
|
|
15
|
+
}
|
|
8
16
|
export interface TodoUpdated {
|
|
9
17
|
type: "todo.updated";
|
|
10
18
|
properties: {
|
|
@@ -12,11 +20,14 @@ export interface TodoUpdated {
|
|
|
12
20
|
todos: Todo[];
|
|
13
21
|
};
|
|
14
22
|
}
|
|
23
|
+
/** The `senpi.todo-state` phases, keeping only well-formed phases and tasks. */
|
|
24
|
+
export declare function todoPhases(data: unknown): TodoPhase[];
|
|
15
25
|
export declare function flattenTodos(sessionID: string, data: unknown): Todo[];
|
|
16
26
|
/** Per-session snapshots; each card retains the state produced by its own call. */
|
|
17
27
|
export declare class TodoBridge {
|
|
18
28
|
private readonly sessionID;
|
|
19
29
|
todos: Todo[];
|
|
30
|
+
phases: TodoPhase[];
|
|
20
31
|
private awaitingState;
|
|
21
32
|
constructor(sessionID: string);
|
|
22
33
|
track(part: OCToolPart, args: unknown): void;
|
package/dist/todo-bridge.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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 };
|