antechamber 0.1.1 → 0.2.0
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 +28 -0
- package/README.md +17 -14
- package/bin/opencode-antechamber +10 -1
- package/bin/opencode-pi +11 -1
- package/dist/approvals.d.ts +4 -1
- package/dist/approvals.js +51 -33
- package/dist/catalog.d.ts +1 -1
- package/dist/catalog.js +1 -1
- package/dist/commands/context.d.ts +0 -1
- package/dist/commands/context.js +1 -1
- package/dist/commands/doctor.d.ts +3 -2
- package/dist/commands/serve.d.ts +2 -1
- package/dist/commands/setup.d.ts +2 -3
- package/dist/commands/setup.js +16 -4
- package/dist/http.d.ts +8 -1
- package/dist/http.js +42 -19
- package/dist/pi-event-translation.d.ts +9 -5
- package/dist/pi-event-translation.js +233 -79
- package/dist/pi-rpc.d.ts +20 -2
- package/dist/pi-rpc.js +96 -29
- package/dist/prompt-parts.d.ts +40 -0
- package/dist/prompt-parts.js +157 -0
- package/dist/route-services.d.ts +45 -2
- package/dist/route-services.js +70 -3
- package/dist/routes-v1.js +110 -65
- package/dist/routes-v2.js +376 -73
- package/dist/server.d.ts +9 -1
- package/dist/server.js +109 -9
- package/dist/session-commands.d.ts +12 -15
- package/dist/session-commands.js +52 -11
- package/dist/session-diff.d.ts +1 -1
- package/dist/session-diff.js +10 -2
- package/dist/session-history.js +108 -42
- package/dist/session-hosts.js +2 -2
- package/dist/session-store.d.ts +8 -4
- package/dist/session-store.js +183 -33
- package/dist/session-tree.d.ts +9 -1
- package/dist/session-tree.js +51 -26
- package/dist/sessions.d.ts +32 -5
- package/dist/sessions.js +330 -119
- package/dist/sse.d.ts +16 -6
- package/dist/sse.js +77 -21
- package/dist/subagents.d.ts +5 -0
- package/dist/subagents.js +84 -71
- package/dist/tool-map.d.ts +3 -2
- package/dist/types.d.ts +152 -1
- package/dist/v2.d.ts +18 -4
- package/dist/v2.js +128 -44
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/extensions/omo-dag/README.md +2 -0
- package/extensions/omo-dag/docs/DESIGN.md +10 -1
- package/extensions/omo-dag/panel/index.html +1 -1
- package/extensions/omo-dag/panel/main.js +60 -60
- package/extensions/omo-dag/service/main.js +72 -8
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
/** HTTP/SSE composition root for the OpenCode API backed by pi. */
|
|
2
|
+
import { createHash } from "node:crypto";
|
|
2
3
|
import { createServer } from "node:http";
|
|
3
4
|
import { debugLog } from "./misc-routes.js";
|
|
4
5
|
import { SessionManager } from "./sessions.js";
|
|
@@ -14,17 +15,22 @@ export class AdapterServer {
|
|
|
14
15
|
sessions;
|
|
15
16
|
sse;
|
|
16
17
|
sseClients = new Map();
|
|
18
|
+
webSocketClients = new Set();
|
|
19
|
+
webSocketEvents = [];
|
|
20
|
+
nextWebSocketEventID = 0;
|
|
17
21
|
catalogPromise;
|
|
18
22
|
actualPort;
|
|
19
|
-
http = new Http();
|
|
23
|
+
http = new Http(() => this.url);
|
|
20
24
|
constructor(options) {
|
|
21
25
|
this.options = options;
|
|
22
26
|
this.sse = new SSE(options, this.sseClients);
|
|
23
27
|
this.sessions = new SessionManager((directory, event) => this.broadcast(directory, event), undefined, options.defaultDirectory);
|
|
24
28
|
this.server = createServer((req, res) => void this.route(req, res));
|
|
29
|
+
this.server.on("upgrade", (req, socket, head) => this.handleUpgrade(req, socket, head));
|
|
25
30
|
}
|
|
26
31
|
catalog() {
|
|
27
32
|
this.catalogPromise ??= loadCatalog(this.options.defaultDirectory).catch((err) => {
|
|
33
|
+
this.catalogPromise = undefined;
|
|
28
34
|
console.error("[catalog] failed to load models from pi:", err);
|
|
29
35
|
return { providers: [], defaults: {} };
|
|
30
36
|
});
|
|
@@ -50,6 +56,9 @@ export class AdapterServer {
|
|
|
50
56
|
// session RPC groups and private task hosts are still being disposed.
|
|
51
57
|
const closed = new Promise((resolve) => this.server.close(() => resolve()));
|
|
52
58
|
this.sse.close();
|
|
59
|
+
for (const socket of this.webSocketClients)
|
|
60
|
+
socket.destroy();
|
|
61
|
+
this.webSocketClients.clear();
|
|
53
62
|
try {
|
|
54
63
|
await this.sessions.disposeAll();
|
|
55
64
|
}
|
|
@@ -64,12 +73,91 @@ export class AdapterServer {
|
|
|
64
73
|
// ---------------------------------------------------------------
|
|
65
74
|
broadcast(directory, event) {
|
|
66
75
|
this.sse.broadcast(directory, event);
|
|
76
|
+
this.broadcastWebSocket(directory, event);
|
|
67
77
|
}
|
|
68
78
|
writeSSE(res, frame) {
|
|
69
79
|
return this.sse.writeSSE(res, frame);
|
|
70
80
|
}
|
|
71
|
-
handleSSE(res, flavor) {
|
|
72
|
-
|
|
81
|
+
handleSSE(req, res, flavor, directory, global) {
|
|
82
|
+
const lastEventID = req.headers["last-event-id"];
|
|
83
|
+
this.sse.handleSSE(res, { flavor, global, ...(global ? {} : { directory }) }, Array.isArray(lastEventID) ? lastEventID[0] : lastEventID, this.writeSSE.bind(this));
|
|
84
|
+
}
|
|
85
|
+
handleUpgrade(req, socket, _head) {
|
|
86
|
+
const url = new URL(req.url ?? "/", this.url);
|
|
87
|
+
if (url.pathname !== "/api/global/event/ws") {
|
|
88
|
+
this.rejectUpgrade(socket, 404, "Not Found");
|
|
89
|
+
return;
|
|
90
|
+
}
|
|
91
|
+
if (!this.http.allowsUpgrade(req)) {
|
|
92
|
+
this.rejectUpgrade(socket, 403, "Forbidden");
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
const keyHeader = req.headers["sec-websocket-key"];
|
|
96
|
+
const key = Array.isArray(keyHeader) ? keyHeader[0] : keyHeader;
|
|
97
|
+
if (req.headers["sec-websocket-version"] !== "13" || typeof key !== "string") {
|
|
98
|
+
this.rejectUpgrade(socket, 400, "Bad Request");
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const accept = createHash("sha1").update(`${key}258EAFA5-E914-47DA-95CA-C5AB0DC85B11`).digest("base64");
|
|
102
|
+
socket.write([
|
|
103
|
+
"HTTP/1.1 101 Switching Protocols",
|
|
104
|
+
"Upgrade: websocket",
|
|
105
|
+
"Connection: Upgrade",
|
|
106
|
+
`Sec-WebSocket-Accept: ${accept}`,
|
|
107
|
+
"",
|
|
108
|
+
"",
|
|
109
|
+
].join("\r\n"));
|
|
110
|
+
this.webSocketClients.add(socket);
|
|
111
|
+
socket.once("close", () => this.webSocketClients.delete(socket));
|
|
112
|
+
socket.once("error", () => this.webSocketClients.delete(socket));
|
|
113
|
+
const cursor = Number(url.searchParams.get("lastEventId"));
|
|
114
|
+
const replayReset = Number.isSafeInteger(cursor) && cursor >= 0 && cursor < this.nextWebSocketEventID - this.webSocketEvents.length;
|
|
115
|
+
this.writeWebSocket(socket, { type: "ready", ...(replayReset ? { replayReset: true } : {}) });
|
|
116
|
+
if (!Number.isSafeInteger(cursor) || cursor < 0)
|
|
117
|
+
return;
|
|
118
|
+
for (const event of this.webSocketEvents) {
|
|
119
|
+
if (event.id > cursor)
|
|
120
|
+
this.writeWebSocketEvent(socket, event);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
rejectUpgrade(socket, status, message) {
|
|
124
|
+
socket.end(`HTTP/1.1 ${status} ${message}\r\nConnection: close\r\n\r\n`);
|
|
125
|
+
}
|
|
126
|
+
broadcastWebSocket(directory, payload) {
|
|
127
|
+
const event = { id: ++this.nextWebSocketEventID, directory, payload };
|
|
128
|
+
this.webSocketEvents.push(event);
|
|
129
|
+
if (this.webSocketEvents.length > 1024)
|
|
130
|
+
this.webSocketEvents.shift();
|
|
131
|
+
for (const socket of this.webSocketClients)
|
|
132
|
+
this.writeWebSocketEvent(socket, event);
|
|
133
|
+
}
|
|
134
|
+
writeWebSocketEvent(socket, event) {
|
|
135
|
+
this.writeWebSocket(socket, {
|
|
136
|
+
type: "event",
|
|
137
|
+
eventId: String(event.id),
|
|
138
|
+
directory: event.directory,
|
|
139
|
+
scope: "global",
|
|
140
|
+
payload: event.payload,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
writeWebSocket(socket, frame) {
|
|
144
|
+
if (socket.destroyed || !socket.writable) {
|
|
145
|
+
this.webSocketClients.delete(socket);
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
const payload = Buffer.from(JSON.stringify(frame));
|
|
149
|
+
const header = payload.length < 126
|
|
150
|
+
? Buffer.from([0x81, payload.length])
|
|
151
|
+
: payload.length <= 0xffff
|
|
152
|
+
? Buffer.from([0x81, 126, payload.length >> 8, payload.length & 0xff])
|
|
153
|
+
: (() => {
|
|
154
|
+
const value = Buffer.alloc(10);
|
|
155
|
+
value[0] = 0x81;
|
|
156
|
+
value[1] = 127;
|
|
157
|
+
value.writeBigUInt64BE(BigInt(payload.length), 2);
|
|
158
|
+
return value;
|
|
159
|
+
})();
|
|
160
|
+
socket.write(Buffer.concat([header, payload]));
|
|
73
161
|
}
|
|
74
162
|
// ---------------------------------------------------------------
|
|
75
163
|
// Router
|
|
@@ -82,18 +170,30 @@ export class AdapterServer {
|
|
|
82
170
|
const url = new URL(req.url ?? "/", this.url);
|
|
83
171
|
const path = url.pathname.replace(/\/+$/, "") || "/";
|
|
84
172
|
const method = req.method ?? "GET";
|
|
85
|
-
const
|
|
173
|
+
const directoryQuery = url.searchParams.get("directory") ??
|
|
86
174
|
url.searchParams.get("location[directory]") ??
|
|
87
|
-
url.searchParams.get("location")
|
|
88
|
-
|
|
89
|
-
|
|
175
|
+
url.searchParams.get("location");
|
|
176
|
+
const directoryHeader = req.headers["x-opencode-directory"];
|
|
177
|
+
const encodedDirectory = Array.isArray(directoryHeader) ? directoryHeader[0] : directoryHeader;
|
|
178
|
+
const directoryEncodingHeader = req.headers["x-opencode-directory-encoding"];
|
|
179
|
+
const directoryEncoding = Array.isArray(directoryEncodingHeader)
|
|
180
|
+
? directoryEncodingHeader[0]
|
|
181
|
+
: directoryEncodingHeader;
|
|
182
|
+
let headerDirectory = encodedDirectory;
|
|
183
|
+
if (encodedDirectory !== undefined &&
|
|
184
|
+
(directoryEncoding?.toLowerCase() === "uri" || /^(?:%|[a-z]%3a)/i.test(encodedDirectory))) {
|
|
185
|
+
headerDirectory = decodeURIComponent(encodedDirectory);
|
|
186
|
+
}
|
|
187
|
+
const directory = directoryQuery ?? headerDirectory ?? this.options.defaultDirectory;
|
|
90
188
|
// SSE stream (v2 clients subscribe at /api/event; v1 at /event)
|
|
91
189
|
if (method === "GET" && (path === "/event" || path === "/global/event")) {
|
|
92
|
-
this.handleSSE(res, "v1");
|
|
190
|
+
this.handleSSE(req, res, "v1", directory, path === "/global/event");
|
|
93
191
|
return;
|
|
94
192
|
}
|
|
95
193
|
if (method === "GET" && path === "/api/event") {
|
|
96
|
-
|
|
194
|
+
// OpenChamber 2.3.2 opens one unscoped /api/event as its global hub (captured);
|
|
195
|
+
// only an explicit directory selector scopes the stream.
|
|
196
|
+
this.handleSSE(req, res, "v2", directory, directoryQuery === null && headerDirectory === undefined);
|
|
97
197
|
return;
|
|
98
198
|
}
|
|
99
199
|
const services = {
|
|
@@ -1,16 +1,18 @@
|
|
|
1
|
+
import type { OCPromptPart } from "./types.js";
|
|
1
2
|
import type { ManagedSession, SessionManager } from "./sessions.js";
|
|
2
3
|
import type { OCAssistantMessage, OCEvent, OCMessageWithParts, OCToolPart, OCTextPart, OCUserMessage } from "./types.js";
|
|
3
|
-
|
|
4
|
+
type CommandDescriptor = {
|
|
4
5
|
name: string;
|
|
5
6
|
description: string;
|
|
6
7
|
template: "";
|
|
7
8
|
};
|
|
8
|
-
|
|
9
|
+
type SlashCommandInput = {
|
|
9
10
|
command: string;
|
|
10
11
|
arguments: string;
|
|
11
12
|
messageID?: string;
|
|
13
|
+
parts?: OCPromptPart[];
|
|
12
14
|
};
|
|
13
|
-
|
|
15
|
+
type ShellResult = {
|
|
14
16
|
output: string;
|
|
15
17
|
exitCode: number | null;
|
|
16
18
|
cancelled: boolean;
|
|
@@ -20,40 +22,36 @@ export type ShellMessage = OCMessageWithParts & {
|
|
|
20
22
|
info: OCAssistantMessage;
|
|
21
23
|
parts: [OCToolPart];
|
|
22
24
|
};
|
|
23
|
-
|
|
25
|
+
type ShellUserMessage = OCMessageWithParts & {
|
|
24
26
|
info: OCUserMessage;
|
|
25
27
|
parts: [OCTextPart];
|
|
26
28
|
};
|
|
27
|
-
|
|
28
|
-
info: OCAssistantMessage;
|
|
29
|
-
parts: [OCTextPart];
|
|
30
|
-
};
|
|
31
|
-
export type ShellExecution = {
|
|
29
|
+
type ShellExecution = {
|
|
32
30
|
kind: "completed";
|
|
33
31
|
message: ShellMessage;
|
|
34
32
|
} | {
|
|
35
33
|
kind: "error";
|
|
36
34
|
message: string;
|
|
37
35
|
};
|
|
38
|
-
|
|
36
|
+
interface ShellCommandContext {
|
|
39
37
|
sessions: SessionManager;
|
|
40
38
|
session: ManagedSession;
|
|
41
39
|
command: string;
|
|
42
40
|
emit: (directory: string, event: OCEvent) => void;
|
|
43
41
|
}
|
|
44
|
-
|
|
42
|
+
interface ShareNoticeContext {
|
|
45
43
|
sessions: SessionManager;
|
|
46
44
|
session: ManagedSession;
|
|
47
45
|
emit: (directory: string, event: OCEvent) => void;
|
|
48
46
|
}
|
|
49
|
-
|
|
47
|
+
type CompactResult = {
|
|
50
48
|
kind: "compacted";
|
|
51
49
|
} | {
|
|
52
50
|
kind: "error";
|
|
53
51
|
message: string;
|
|
54
52
|
};
|
|
55
53
|
export declare const unsupportedShare: {
|
|
56
|
-
readonly name: "
|
|
54
|
+
readonly name: "UnsupportedError";
|
|
57
55
|
readonly data: {
|
|
58
56
|
readonly message: "Sharing is not available with OmO";
|
|
59
57
|
};
|
|
@@ -69,10 +67,8 @@ export declare function parseSlashCommand(value: unknown): SlashCommandInput | u
|
|
|
69
67
|
export declare function slashTitlePrompt(text: string): {
|
|
70
68
|
sessionTitlePrompt?: string;
|
|
71
69
|
};
|
|
72
|
-
export declare function slashCommandPrompt(input: SlashCommandInput): string;
|
|
73
70
|
export declare function runSlashCommand(sessions: SessionManager, sessionId: string, input: SlashCommandInput): Promise<OCMessageWithParts | undefined>;
|
|
74
71
|
export declare function parseShellCommand(value: unknown): string | undefined;
|
|
75
|
-
export declare function parseShellResult(value: unknown): ShellResult;
|
|
76
72
|
export declare function createShellMessage(session: ManagedSession, command: string): ShellMessage;
|
|
77
73
|
export declare function shellUserMessage(message: ShellMessage): ShellUserMessage;
|
|
78
74
|
export declare function announceSessionNotice(context: ShareNoticeContext, messageText: string, error?: OCAssistantMessage["error"]): void;
|
|
@@ -81,3 +77,4 @@ export declare function completeShellMessage(message: ShellMessage, command: str
|
|
|
81
77
|
export declare function failShellMessage(message: ShellMessage, command: string, error: string, metadata?: Record<string, unknown>): void;
|
|
82
78
|
export declare function compactSession(sessions: SessionManager, sessionId: string): Promise<CompactResult>;
|
|
83
79
|
export declare function runShellCommand(context: ShellCommandContext): Promise<ShellExecution>;
|
|
80
|
+
export {};
|
package/dist/session-commands.js
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
|
-
import { PiRpcClient } from "./pi-rpc.js";
|
|
2
|
+
import { PiRpcClient, UpstreamProtocolError } from "./pi-rpc.js";
|
|
3
3
|
import { getAgentName } from "./agent-name.js";
|
|
4
|
+
import { nativePromptInput, parsePromptParts } from "./prompt-parts.js";
|
|
4
5
|
// OpenChamber drops rejected Share actions, so emit a visible session notice too.
|
|
5
6
|
export const unsupportedShare = {
|
|
6
|
-
name: "
|
|
7
|
+
name: "UnsupportedError",
|
|
7
8
|
data: { message: "Sharing is not available with OmO" },
|
|
8
9
|
};
|
|
9
10
|
function isRecord(value) {
|
|
@@ -12,8 +13,10 @@ function isRecord(value) {
|
|
|
12
13
|
function id(prefix) {
|
|
13
14
|
return `${prefix}_${randomUUID().replaceAll("-", "")}`;
|
|
14
15
|
}
|
|
16
|
+
const SHELL_TITLE_MAX_CHARS = 60;
|
|
17
|
+
const SHELL_TITLE_TRUNCATED_CHARS = 57;
|
|
15
18
|
export function shellTitle(command) {
|
|
16
|
-
const short = command.length >
|
|
19
|
+
const short = command.length > SHELL_TITLE_MAX_CHARS ? `${command.slice(0, SHELL_TITLE_TRUNCATED_CHARS)}…` : command;
|
|
17
20
|
return `bash: ${short}`;
|
|
18
21
|
}
|
|
19
22
|
function assistantMessage(session) {
|
|
@@ -112,7 +115,14 @@ export function parseSlashCommand(value) {
|
|
|
112
115
|
return undefined;
|
|
113
116
|
// OpenChamber inserts the command's user message optimistically under this id.
|
|
114
117
|
const messageID = typeof value.messageID === "string" && value.messageID !== "" ? value.messageID : undefined;
|
|
115
|
-
|
|
118
|
+
const parts = parsePromptParts(value.parts);
|
|
119
|
+
nativePromptInput("", parts);
|
|
120
|
+
return {
|
|
121
|
+
command: value.command,
|
|
122
|
+
arguments: value.arguments,
|
|
123
|
+
...(messageID ? { messageID } : {}),
|
|
124
|
+
...(parts.length > 0 ? { parts } : {}),
|
|
125
|
+
};
|
|
116
126
|
}
|
|
117
127
|
/**
|
|
118
128
|
* OmO never auto-titles a first prompt that starts with "/", so a session opened
|
|
@@ -123,11 +133,12 @@ export function slashTitlePrompt(text) {
|
|
|
123
133
|
const match = /^\s*\/\S+\s+([\s\S]*\S)/.exec(text);
|
|
124
134
|
return match?.[1] !== undefined && !match[1].startsWith("/") ? { sessionTitlePrompt: match[1] } : {};
|
|
125
135
|
}
|
|
126
|
-
|
|
136
|
+
function slashCommandPrompt(input) {
|
|
127
137
|
return input.arguments === "" ? `/${input.command}` : `/${input.command} ${input.arguments}`;
|
|
128
138
|
}
|
|
129
139
|
export async function runSlashCommand(sessions, sessionId, input) {
|
|
130
|
-
|
|
140
|
+
const text = slashCommandPrompt(input);
|
|
141
|
+
await sessions.prompt(sessionId, text, undefined, false, input.messageID, "steer", input.parts ? [{ type: "text", text }, ...input.parts] : undefined);
|
|
131
142
|
await sessions.reloadMessages(sessionId);
|
|
132
143
|
const messages = sessions.messages(sessionId) ?? [];
|
|
133
144
|
return [...messages].reverse().find((message) => message.info.role === "assistant") ?? messages[messages.length - 1];
|
|
@@ -137,7 +148,7 @@ export function parseShellCommand(value) {
|
|
|
137
148
|
return undefined;
|
|
138
149
|
return value.command;
|
|
139
150
|
}
|
|
140
|
-
|
|
151
|
+
function parseShellResult(value) {
|
|
141
152
|
if (!isRecord(value) ||
|
|
142
153
|
typeof value.output !== "string" ||
|
|
143
154
|
(typeof value.exitCode !== "number" &&
|
|
@@ -145,7 +156,7 @@ export function parseShellResult(value) {
|
|
|
145
156
|
!(value.cancelled === true && value.exitCode === undefined)) ||
|
|
146
157
|
typeof value.cancelled !== "boolean" ||
|
|
147
158
|
typeof value.truncated !== "boolean") {
|
|
148
|
-
throw new
|
|
159
|
+
throw new UpstreamProtocolError("OmO bash returned an invalid result");
|
|
149
160
|
}
|
|
150
161
|
return {
|
|
151
162
|
output: value.output,
|
|
@@ -265,6 +276,15 @@ export async function runShellCommand(context) {
|
|
|
265
276
|
sessions.messages(session.info.id);
|
|
266
277
|
const message = createShellMessage(session, command);
|
|
267
278
|
const user = shellUserMessage(message);
|
|
279
|
+
const shell = {
|
|
280
|
+
id: message.parts[0].callID,
|
|
281
|
+
command,
|
|
282
|
+
cwd: session.info.directory,
|
|
283
|
+
shell: "bash",
|
|
284
|
+
file: "",
|
|
285
|
+
metadata: {},
|
|
286
|
+
time: { started: message.info.time.created },
|
|
287
|
+
};
|
|
268
288
|
const dispatch = (session.tree.shellDispatch ??= {});
|
|
269
289
|
dispatch[message.info.id] = false;
|
|
270
290
|
const afterEntryId = session.tree.leafId ?? null;
|
|
@@ -285,8 +305,12 @@ export async function runShellCommand(context) {
|
|
|
285
305
|
shellEvent(context, { type: "message.updated", properties: { info: user.info } });
|
|
286
306
|
shellEvent(context, { type: "message.part.updated", properties: { part: user.parts[0] } });
|
|
287
307
|
emitShellMessage(context, message);
|
|
308
|
+
shellEvent(context, {
|
|
309
|
+
type: "session.shell.started",
|
|
310
|
+
properties: { sessionID: session.info.id, shell: { ...shell, status: "running" } },
|
|
311
|
+
});
|
|
288
312
|
try {
|
|
289
|
-
sessions.update(session.info.id, {});
|
|
313
|
+
await sessions.update(session.info.id, {});
|
|
290
314
|
const response = await sessions.sessionRpc(session.info.id, { type: "bash", command }, controller.signal, () => {
|
|
291
315
|
dispatch[message.info.id] = true;
|
|
292
316
|
// Only actual dispatch admissions are durable; queued attempts stay in the live cache.
|
|
@@ -294,7 +318,7 @@ export async function runShellCommand(context) {
|
|
|
294
318
|
// A native result can land beyond the last synchronized leaf.
|
|
295
319
|
if (!session.tree.revert)
|
|
296
320
|
delete session.tree.leafId;
|
|
297
|
-
sessions.update(session.info.id, {});
|
|
321
|
+
void sessions.update(session.info.id, {}).catch((error) => controller.abort(error));
|
|
298
322
|
});
|
|
299
323
|
if (controller.signal.aborted)
|
|
300
324
|
throw controller.signal.reason;
|
|
@@ -326,7 +350,24 @@ export async function runShellCommand(context) {
|
|
|
326
350
|
}
|
|
327
351
|
finally {
|
|
328
352
|
delete session.cancelShell;
|
|
353
|
+
const state = message.parts[0].state;
|
|
354
|
+
const metadata = state.status === "pending" ? undefined : state.metadata;
|
|
355
|
+
const output = state.status === "completed" ? state.output : state.status === "error" ? state.error : "";
|
|
356
|
+
const size = Buffer.byteLength(output);
|
|
357
|
+
shellEvent(context, {
|
|
358
|
+
type: "session.shell.ended",
|
|
359
|
+
properties: {
|
|
360
|
+
sessionID: session.info.id,
|
|
361
|
+
shell: {
|
|
362
|
+
...shell,
|
|
363
|
+
status: state.status === "completed" ? "exited" : "killed",
|
|
364
|
+
...(typeof metadata?.exitCode === "number" ? { exit: metadata.exitCode } : {}),
|
|
365
|
+
time: { ...shell.time, completed: message.info.time.completed ?? Date.now() },
|
|
366
|
+
},
|
|
367
|
+
output: { output, cursor: size, size, truncated: metadata?.truncated === true },
|
|
368
|
+
},
|
|
369
|
+
});
|
|
329
370
|
setShellStatus(context, "idle");
|
|
330
|
-
sessions.update(session.info.id, {});
|
|
371
|
+
await sessions.update(session.info.id, {});
|
|
331
372
|
}
|
|
332
373
|
}
|
package/dist/session-diff.d.ts
CHANGED
|
@@ -11,4 +11,4 @@ export interface SessionFileDiff {
|
|
|
11
11
|
status: "added" | "modified" | "deleted";
|
|
12
12
|
}
|
|
13
13
|
export declare function touchedFiles(parts: readonly OCPart[]): string[];
|
|
14
|
-
export declare function sessionDiff(directory: string, messages: readonly OCMessageWithParts[]): Promise<SessionFileDiff[]>;
|
|
14
|
+
export declare function sessionDiff(directory: string, messages: readonly OCMessageWithParts[], context?: number): Promise<SessionFileDiff[]>;
|
package/dist/session-diff.js
CHANGED
|
@@ -3,6 +3,8 @@ import { readFile, realpath, lstat } from "node:fs/promises";
|
|
|
3
3
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
4
4
|
import { promisify } from "node:util";
|
|
5
5
|
const exec = promisify(execFile);
|
|
6
|
+
const GIT_EXEC_TIMEOUT_MS = 5_000;
|
|
7
|
+
const GIT_EXEC_MAX_BUFFER_BYTES = 16 * 1024 * 1024;
|
|
6
8
|
export function touchedFiles(parts) {
|
|
7
9
|
return [
|
|
8
10
|
...new Set(parts.flatMap((part) => part.type === "tool" &&
|
|
@@ -14,7 +16,11 @@ export function touchedFiles(parts) {
|
|
|
14
16
|
];
|
|
15
17
|
}
|
|
16
18
|
async function git(directory, args) {
|
|
17
|
-
const result = await exec("git", args, {
|
|
19
|
+
const result = await exec("git", args, {
|
|
20
|
+
cwd: directory,
|
|
21
|
+
timeout: GIT_EXEC_TIMEOUT_MS,
|
|
22
|
+
maxBuffer: GIT_EXEC_MAX_BUFFER_BYTES,
|
|
23
|
+
});
|
|
18
24
|
return result.stdout;
|
|
19
25
|
}
|
|
20
26
|
/** A removed file may be absent, but neither traversal nor symlinks may escape. */
|
|
@@ -49,7 +55,7 @@ async function confinedFile(directory, path) {
|
|
|
49
55
|
}
|
|
50
56
|
return absolute;
|
|
51
57
|
}
|
|
52
|
-
export async function sessionDiff(directory, messages) {
|
|
58
|
+
export async function sessionDiff(directory, messages, context = 3) {
|
|
53
59
|
let gitRoot;
|
|
54
60
|
try {
|
|
55
61
|
gitRoot = (await git(directory, ["rev-parse", "--show-toplevel"])).trim();
|
|
@@ -104,6 +110,7 @@ export async function sessionDiff(directory, messages) {
|
|
|
104
110
|
"HEAD",
|
|
105
111
|
"--no-ext-diff",
|
|
106
112
|
"--no-color",
|
|
113
|
+
`--unified=${Math.floor(context)}`,
|
|
107
114
|
"--src-prefix=a/",
|
|
108
115
|
"--dst-prefix=b/",
|
|
109
116
|
"--",
|
|
@@ -118,6 +125,7 @@ export async function sessionDiff(directory, messages) {
|
|
|
118
125
|
"--no-index",
|
|
119
126
|
"--no-ext-diff",
|
|
120
127
|
"--no-color",
|
|
128
|
+
`--unified=${Math.floor(context)}`,
|
|
121
129
|
"--src-prefix=a/",
|
|
122
130
|
"--dst-prefix=b/",
|
|
123
131
|
"--",
|
package/dist/session-history.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
import { readFileSync } from "node:fs";
|
|
3
3
|
import { applyProviderToolResults, finishProviderToolParts, mapTool, providerToolParts, resultMetadata, resultText, } from "./tool-map.js";
|
|
4
4
|
import { TodoBridge } from "./todo-bridge.js";
|
|
5
|
+
import { contextMessageID } from "./prompt-parts.js";
|
|
5
6
|
import { bindMessageEntries } from "./session-tree.js";
|
|
6
7
|
import { taskParts, updateTaskParts, taskCompletions } from "./subagents.js";
|
|
7
8
|
import { shellTitle, shellUserMessage } from "./session-commands.js";
|
|
@@ -38,9 +39,7 @@ export function parsePiSessionFile(s, agentName) {
|
|
|
38
39
|
}
|
|
39
40
|
return parsePiSessionText(s, agentName, raw);
|
|
40
41
|
}
|
|
41
|
-
|
|
42
|
-
const sessionID = s.info.id;
|
|
43
|
-
const directory = s.info.directory;
|
|
42
|
+
function parseEntries(raw) {
|
|
44
43
|
const entries = [];
|
|
45
44
|
for (const line of raw.split("\n")) {
|
|
46
45
|
if (line.length === 0)
|
|
@@ -54,12 +53,43 @@ export function parsePiSessionText(s, agentName, raw) {
|
|
|
54
53
|
/* torn trailing write — ignore */
|
|
55
54
|
}
|
|
56
55
|
}
|
|
57
|
-
|
|
58
|
-
|
|
56
|
+
return entries;
|
|
57
|
+
}
|
|
58
|
+
function nativeTree(entries, leaf) {
|
|
59
|
+
return {
|
|
60
|
+
leafId: leaf?.id ?? null,
|
|
61
|
+
entries: entries.flatMap((entry) => {
|
|
62
|
+
if (entry.id === undefined || entry.type === undefined)
|
|
63
|
+
return [];
|
|
64
|
+
const message = entry.message;
|
|
65
|
+
return [
|
|
66
|
+
{
|
|
67
|
+
type: entry.type,
|
|
68
|
+
id: entry.id,
|
|
69
|
+
parentId: entry.parentId ?? null,
|
|
70
|
+
...(message?.role === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: {
|
|
73
|
+
message: {
|
|
74
|
+
role: message.role,
|
|
75
|
+
...(message.clientMessageId === undefined ? {} : { clientMessageId: message.clientMessageId }),
|
|
76
|
+
...(message.role === "user" ? { content: [{ type: "text", text: textOf(message.content) }] } : {}),
|
|
77
|
+
...(message.command === undefined ? {} : { command: message.command }),
|
|
78
|
+
...(message.output === undefined ? {} : { output: message.output }),
|
|
79
|
+
...(message.exitCode === undefined ? {} : { exitCode: message.exitCode }),
|
|
80
|
+
...(message.cancelled === undefined ? {} : { cancelled: message.cancelled }),
|
|
81
|
+
},
|
|
82
|
+
}),
|
|
83
|
+
},
|
|
84
|
+
];
|
|
85
|
+
}),
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
function activeChain(s, entries) {
|
|
59
89
|
const byId = new Map();
|
|
60
|
-
for (const
|
|
61
|
-
if (typeof
|
|
62
|
-
byId.set(
|
|
90
|
+
for (const entry of entries)
|
|
91
|
+
if (typeof entry.id === "string")
|
|
92
|
+
byId.set(entry.id, entry);
|
|
63
93
|
let leaf = [...entries].reverse().find((entry) => entry.type !== "session" && typeof entry.id === "string");
|
|
64
94
|
let selectedLeaf = s.tree.revert ? s.tree.revert.previousLeafId : s.tree.leafId;
|
|
65
95
|
if (!s.tree.revert && s.tree.localShellTurns?.length) {
|
|
@@ -77,40 +107,21 @@ export function parsePiSessionText(s, agentName, raw) {
|
|
|
77
107
|
leaf = selectedLeaf === null ? undefined : byId.get(selectedLeaf);
|
|
78
108
|
const chain = [];
|
|
79
109
|
const seen = new Set();
|
|
80
|
-
for (let
|
|
81
|
-
seen.add(
|
|
82
|
-
chain.push(
|
|
83
|
-
|
|
110
|
+
for (let entry = leaf; entry !== undefined && !seen.has(entry);) {
|
|
111
|
+
seen.add(entry);
|
|
112
|
+
chain.push(entry);
|
|
113
|
+
entry = typeof entry.parentId === "string" ? byId.get(entry.parentId) : undefined;
|
|
84
114
|
}
|
|
85
115
|
chain.reverse();
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
id: entry.id,
|
|
96
|
-
parentId: entry.parentId ?? null,
|
|
97
|
-
...(m?.role === undefined
|
|
98
|
-
? {}
|
|
99
|
-
: {
|
|
100
|
-
message: {
|
|
101
|
-
role: m.role,
|
|
102
|
-
...(m.clientMessageId === undefined ? {} : { clientMessageId: m.clientMessageId }),
|
|
103
|
-
...(m.role === "user" ? { content: [{ type: "text", text: textOf(m.content) }] } : {}),
|
|
104
|
-
...(m.command === undefined ? {} : { command: m.command }),
|
|
105
|
-
...(m.output === undefined ? {} : { output: m.output }),
|
|
106
|
-
...(m.exitCode === undefined ? {} : { exitCode: m.exitCode }),
|
|
107
|
-
...(m.cancelled === undefined ? {} : { cancelled: m.cancelled }),
|
|
108
|
-
},
|
|
109
|
-
}),
|
|
110
|
-
},
|
|
111
|
-
];
|
|
112
|
-
}),
|
|
113
|
-
};
|
|
116
|
+
return { chain, nativeTree: nativeTree(entries, leaf) };
|
|
117
|
+
}
|
|
118
|
+
export function parsePiSessionText(s, agentName, raw) {
|
|
119
|
+
const sessionID = s.info.id;
|
|
120
|
+
const directory = s.info.directory;
|
|
121
|
+
const entries = parseEntries(raw);
|
|
122
|
+
const indexOf = new Map();
|
|
123
|
+
entries.forEach((e, i) => indexOf.set(e, i));
|
|
124
|
+
const { chain, nativeTree } = activeChain(s, entries);
|
|
114
125
|
if (s.tree.localShellTurns?.length)
|
|
115
126
|
bindMessageEntries(s, nativeTree);
|
|
116
127
|
const out = [];
|
|
@@ -133,7 +144,41 @@ export function parsePiSessionText(s, agentName, raw) {
|
|
|
133
144
|
}
|
|
134
145
|
if (e.type === "compaction" && typeof e.summary === "string" && typeof e.firstKeptEntryId === "string") {
|
|
135
146
|
// Not a run boundary: an automatic compaction can sit between steps of one turn.
|
|
136
|
-
out.push(...compactionMessages(s, e.
|
|
147
|
+
out.push(...compactionMessages(s, e.id ?? `r${indexOf.get(e) ?? 0}`, e.summary, entryTime(e), currentModel));
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
const contextID = contextMessageID(e);
|
|
151
|
+
if (contextID !== undefined) {
|
|
152
|
+
endRun();
|
|
153
|
+
const content = Array.isArray(e.content)
|
|
154
|
+
? e.content
|
|
155
|
+
: typeof e.content === "string"
|
|
156
|
+
? [{ type: "text", text: e.content }]
|
|
157
|
+
: [];
|
|
158
|
+
const time = entryTime(e);
|
|
159
|
+
if (e.id)
|
|
160
|
+
s.tree.messageEntries[contextID] = e.id;
|
|
161
|
+
out.push({
|
|
162
|
+
info: {
|
|
163
|
+
id: contextID,
|
|
164
|
+
sessionID,
|
|
165
|
+
role: "user",
|
|
166
|
+
time: { created: time },
|
|
167
|
+
agent: agentName,
|
|
168
|
+
model: currentModel,
|
|
169
|
+
synthetic: true,
|
|
170
|
+
},
|
|
171
|
+
parts: [
|
|
172
|
+
{
|
|
173
|
+
id: `prt_${contextID}_context`,
|
|
174
|
+
sessionID,
|
|
175
|
+
messageID: contextID,
|
|
176
|
+
type: "text",
|
|
177
|
+
text: textOf(content),
|
|
178
|
+
time: { start: time, end: time },
|
|
179
|
+
},
|
|
180
|
+
],
|
|
181
|
+
});
|
|
137
182
|
continue;
|
|
138
183
|
}
|
|
139
184
|
if (e.type !== "message")
|
|
@@ -372,7 +417,9 @@ export function parsePiSessionText(s, agentName, raw) {
|
|
|
372
417
|
finishProviderToolParts([...toolParts.values()].flat(), entryTime(lastAssistant), stopReason === "aborted" ? "Cancelled" : "No result");
|
|
373
418
|
}
|
|
374
419
|
s.todoBridge = new TodoBridge(sessionID);
|
|
375
|
-
|
|
420
|
+
// Tree-less snapshots predate branch selection and have no competing ancestor chain.
|
|
421
|
+
const todoEntries = chain.length === 0 && s.tree.leafId === undefined && !s.tree.revert ? entries : chain;
|
|
422
|
+
s.todoBridge.restore(todoEntries, new Map([...toolParts].flatMap(([callID, parts]) => (parts[0] ? [[callID, parts[0]]] : []))));
|
|
376
423
|
// Completions can be appended after the newest message, outside its ancestor chain.
|
|
377
424
|
for (const entry of entries) {
|
|
378
425
|
if (entry.customType !== "omo-senpi:wake")
|
|
@@ -417,5 +464,24 @@ export function parsePiSessionText(s, agentName, raw) {
|
|
|
417
464
|
});
|
|
418
465
|
(s.inputs ??= new Map()).set(messageID, { text, delivery, accepted: true, delivered: false });
|
|
419
466
|
}
|
|
467
|
+
for (const message of out) {
|
|
468
|
+
const metadata = s.tree.promptMetadata?.[message.info.id];
|
|
469
|
+
if (message.info.role === "user" && metadata !== undefined)
|
|
470
|
+
message.info.metadata = metadata;
|
|
471
|
+
const inputParts = s.tree.promptParts?.[message.info.id];
|
|
472
|
+
if (message.info.role === "user" && inputParts !== undefined)
|
|
473
|
+
message.parts = inputParts.map((part, index) => ({
|
|
474
|
+
...part,
|
|
475
|
+
id: part.id ?? `prt_${message.info.id}_${index}`,
|
|
476
|
+
sessionID,
|
|
477
|
+
messageID: message.info.id,
|
|
478
|
+
}));
|
|
479
|
+
const identities = s.tree.messageParts?.[message.info.id];
|
|
480
|
+
for (const [index, part] of message.parts.entries()) {
|
|
481
|
+
const identity = identities?.[index];
|
|
482
|
+
if (identity !== undefined)
|
|
483
|
+
part.id = identity;
|
|
484
|
+
}
|
|
485
|
+
}
|
|
420
486
|
return out;
|
|
421
487
|
}
|
package/dist/session-hosts.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { createHash } from "node:crypto";
|
|
3
3
|
import { once } from "node:events";
|
|
4
|
-
import { closeSync, constants, openSync, readFileSync, readdirSync, readlinkSync } from "node:fs";
|
|
4
|
+
import { closeSync, constants, openSync, readFileSync, readdirSync, readlinkSync, realpathSync } from "node:fs";
|
|
5
5
|
import { basename } from "node:path";
|
|
6
6
|
import { findPidfdPython } from "./pidfd.js";
|
|
7
7
|
/** Capability and warning state are captured once, before any shutdown work. */
|
|
@@ -144,7 +144,7 @@ async function stopSupportedSessionHosts(created, python) {
|
|
|
144
144
|
try {
|
|
145
145
|
// The original leader must still exist after the process-table scan:
|
|
146
146
|
// a recycled group number cannot confer its ownership on new members.
|
|
147
|
-
return readlinkSync(`/proc/self/fd/${info.fd}/cwd`) === directory;
|
|
147
|
+
return readlinkSync(`/proc/self/fd/${info.fd}/cwd`) === realpathSync(directory);
|
|
148
148
|
}
|
|
149
149
|
catch (error) {
|
|
150
150
|
if (error instanceof Error && "code" in error && ["ENOENT", "ESRCH"].includes(String(error.code)))
|