@openacp/cli 2026.331.1 → 2026.331.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.
- package/README.md +2 -1
- package/dist/cli.js +24932 -270
- package/dist/cli.js.map +1 -1
- package/dist/data/registry-snapshot.json +1 -1
- package/dist/index.js +17626 -409
- package/dist/index.js.map +1 -1
- package/package.json +2 -2
- package/dist/adapter-ELG3VRZ3.js +0 -14
- package/dist/adapter-ELG3VRZ3.js.map +0 -1
- package/dist/agent-catalog-UYD26QDK.js +0 -10
- package/dist/agent-catalog-UYD26QDK.js.map +0 -1
- package/dist/agent-dependencies-ED2ZTUHG.js +0 -23
- package/dist/agent-dependencies-ED2ZTUHG.js.map +0 -1
- package/dist/agent-registry-YOGP656W.js +0 -8
- package/dist/agent-registry-YOGP656W.js.map +0 -1
- package/dist/agent-store-5UHZH2XI.js +0 -8
- package/dist/agent-store-5UHZH2XI.js.map +0 -1
- package/dist/api-client-PEMHYL5U.js +0 -13
- package/dist/api-client-PEMHYL5U.js.map +0 -1
- package/dist/api-server-DATG2KBR.js +0 -10
- package/dist/api-server-DATG2KBR.js.map +0 -1
- package/dist/api-server-L5Z7XACW.js +0 -7
- package/dist/api-server-L5Z7XACW.js.map +0 -1
- package/dist/autostart-CUPZMKKC.js +0 -22
- package/dist/autostart-CUPZMKKC.js.map +0 -1
- package/dist/chunk-23SRIVG4.js +0 -50
- package/dist/chunk-23SRIVG4.js.map +0 -1
- package/dist/chunk-2KT6TROD.js +0 -129
- package/dist/chunk-2KT6TROD.js.map +0 -1
- package/dist/chunk-2R5XM3ES.js +0 -154
- package/dist/chunk-2R5XM3ES.js.map +0 -1
- package/dist/chunk-3EWTPOF7.js +0 -51
- package/dist/chunk-3EWTPOF7.js.map +0 -1
- package/dist/chunk-566W6INH.js +0 -83
- package/dist/chunk-566W6INH.js.map +0 -1
- package/dist/chunk-5WGVYX3C.js +0 -55
- package/dist/chunk-5WGVYX3C.js.map +0 -1
- package/dist/chunk-7GXEMMEV.js +0 -44
- package/dist/chunk-7GXEMMEV.js.map +0 -1
- package/dist/chunk-7U6IZIJP.js +0 -186
- package/dist/chunk-7U6IZIJP.js.map +0 -1
- package/dist/chunk-7YIKTRSM.js +0 -105
- package/dist/chunk-7YIKTRSM.js.map +0 -1
- package/dist/chunk-7ZCQF6QM.js +0 -27
- package/dist/chunk-7ZCQF6QM.js.map +0 -1
- package/dist/chunk-AFKX424Q.js +0 -92
- package/dist/chunk-AFKX424Q.js.map +0 -1
- package/dist/chunk-BYCJQPMN.js +0 -543
- package/dist/chunk-BYCJQPMN.js.map +0 -1
- package/dist/chunk-CDAUYTVP.js +0 -41
- package/dist/chunk-CDAUYTVP.js.map +0 -1
- package/dist/chunk-EWVXSTQK.js +0 -6544
- package/dist/chunk-EWVXSTQK.js.map +0 -1
- package/dist/chunk-FNRSWA2K.js +0 -1
- package/dist/chunk-FNRSWA2K.js.map +0 -1
- package/dist/chunk-FPKQYCQS.js +0 -776
- package/dist/chunk-FPKQYCQS.js.map +0 -1
- package/dist/chunk-IZ5UEZF7.js +0 -138
- package/dist/chunk-IZ5UEZF7.js.map +0 -1
- package/dist/chunk-K6UY5M75.js +0 -653
- package/dist/chunk-K6UY5M75.js.map +0 -1
- package/dist/chunk-KGAQW6F4.js +0 -106
- package/dist/chunk-KGAQW6F4.js.map +0 -1
- package/dist/chunk-LGFWH3AE.js +0 -26
- package/dist/chunk-LGFWH3AE.js.map +0 -1
- package/dist/chunk-LRV56K2M.js +0 -4106
- package/dist/chunk-LRV56K2M.js.map +0 -1
- package/dist/chunk-MDJHCCFS.js +0 -485
- package/dist/chunk-MDJHCCFS.js.map +0 -1
- package/dist/chunk-MLF4W5R6.js +0 -101
- package/dist/chunk-MLF4W5R6.js.map +0 -1
- package/dist/chunk-NHD5XDD2.js +0 -686
- package/dist/chunk-NHD5XDD2.js.map +0 -1
- package/dist/chunk-NJX75BLK.js +0 -259
- package/dist/chunk-NJX75BLK.js.map +0 -1
- package/dist/chunk-NOEAJNTK.js +0 -156
- package/dist/chunk-NOEAJNTK.js.map +0 -1
- package/dist/chunk-ON7HB5O7.js +0 -58
- package/dist/chunk-ON7HB5O7.js.map +0 -1
- package/dist/chunk-OSBZXY2W.js +0 -126
- package/dist/chunk-OSBZXY2W.js.map +0 -1
- package/dist/chunk-OYSAN7UX.js +0 -15
- package/dist/chunk-OYSAN7UX.js.map +0 -1
- package/dist/chunk-P3HHJANC.js +0 -209
- package/dist/chunk-P3HHJANC.js.map +0 -1
- package/dist/chunk-R2YLDQLI.js +0 -1115
- package/dist/chunk-R2YLDQLI.js.map +0 -1
- package/dist/chunk-R6KZYF7D.js +0 -231
- package/dist/chunk-R6KZYF7D.js.map +0 -1
- package/dist/chunk-S64CB6J3.js +0 -98
- package/dist/chunk-S64CB6J3.js.map +0 -1
- package/dist/chunk-SSLVNCEA.js +0 -236
- package/dist/chunk-SSLVNCEA.js.map +0 -1
- package/dist/chunk-TGP34LQN.js +0 -681
- package/dist/chunk-TGP34LQN.js.map +0 -1
- package/dist/chunk-VUSCVRJL.js +0 -229
- package/dist/chunk-VUSCVRJL.js.map +0 -1
- package/dist/chunk-W26AUH5B.js +0 -61
- package/dist/chunk-W26AUH5B.js.map +0 -1
- package/dist/chunk-WQCJTU2C.js +0 -84
- package/dist/chunk-WQCJTU2C.js.map +0 -1
- package/dist/chunk-XRJUS6FE.js +0 -53
- package/dist/chunk-XRJUS6FE.js.map +0 -1
- package/dist/chunk-YZCKSNRN.js +0 -453
- package/dist/chunk-YZCKSNRN.js.map +0 -1
- package/dist/chunk-ZIRH6QWW.js +0 -69
- package/dist/chunk-ZIRH6QWW.js.map +0 -1
- package/dist/chunk-ZSLHHQPQ.js +0 -282
- package/dist/chunk-ZSLHHQPQ.js.map +0 -1
- package/dist/config-X4UP7H6R.js +0 -13
- package/dist/config-X4UP7H6R.js.map +0 -1
- package/dist/config-editor-7BENRVG5.js +0 -11
- package/dist/config-editor-7BENRVG5.js.map +0 -1
- package/dist/config-registry-M3FFWEVM.js +0 -18
- package/dist/config-registry-M3FFWEVM.js.map +0 -1
- package/dist/context-FVGCU5TI.js +0 -9
- package/dist/context-FVGCU5TI.js.map +0 -1
- package/dist/core-plugins-JSY2I44L.js +0 -25
- package/dist/core-plugins-JSY2I44L.js.map +0 -1
- package/dist/daemon-UOSRDEXW.js +0 -34
- package/dist/daemon-UOSRDEXW.js.map +0 -1
- package/dist/dev-loader-7P3HZCIA.js +0 -37
- package/dist/dev-loader-7P3HZCIA.js.map +0 -1
- package/dist/doctor-6DLACBR4.js +0 -10
- package/dist/doctor-6DLACBR4.js.map +0 -1
- package/dist/file-service-FQQYME7M.js +0 -8
- package/dist/file-service-FQQYME7M.js.map +0 -1
- package/dist/install-cloudflared-LNS5L5FR.js +0 -33
- package/dist/install-cloudflared-LNS5L5FR.js.map +0 -1
- package/dist/install-context-KZO5FR4D.js +0 -78
- package/dist/install-context-KZO5FR4D.js.map +0 -1
- package/dist/install-jq-SN4IA5K4.js +0 -31
- package/dist/install-jq-SN4IA5K4.js.map +0 -1
- package/dist/instance-context-FLCE7VZ4.js +0 -13
- package/dist/instance-context-FLCE7VZ4.js.map +0 -1
- package/dist/instance-registry-SW5FWKHO.js +0 -7
- package/dist/instance-registry-SW5FWKHO.js.map +0 -1
- package/dist/integrate-JIEZYDOR.js +0 -371
- package/dist/integrate-JIEZYDOR.js.map +0 -1
- package/dist/log-YZ243M5G.js +0 -25
- package/dist/log-YZ243M5G.js.map +0 -1
- package/dist/main-D7M2AKRM.js +0 -697
- package/dist/main-D7M2AKRM.js.map +0 -1
- package/dist/menu-ALFN37IR.js +0 -15
- package/dist/menu-ALFN37IR.js.map +0 -1
- package/dist/notifications-MO23S7S3.js +0 -8
- package/dist/notifications-MO23S7S3.js.map +0 -1
- package/dist/plugin-create-HFKS23JY.js +0 -968
- package/dist/plugin-create-HFKS23JY.js.map +0 -1
- package/dist/plugin-installer-VSTYZSXC.js +0 -9
- package/dist/plugin-installer-VSTYZSXC.js.map +0 -1
- package/dist/plugin-registry-6J3YSFHF.js +0 -7
- package/dist/plugin-registry-6J3YSFHF.js.map +0 -1
- package/dist/plugin-search-MGKAL5JM.js +0 -39
- package/dist/plugin-search-MGKAL5JM.js.map +0 -1
- package/dist/post-upgrade-F4YPMTUT.js +0 -79
- package/dist/post-upgrade-F4YPMTUT.js.map +0 -1
- package/dist/read-text-file-DJBTITIB.js +0 -7
- package/dist/read-text-file-DJBTITIB.js.map +0 -1
- package/dist/registry-client-GTBWLXYU.js +0 -7
- package/dist/registry-client-GTBWLXYU.js.map +0 -1
- package/dist/security-O4XGN2CM.js +0 -8
- package/dist/security-O4XGN2CM.js.map +0 -1
- package/dist/settings-manager-B4UN2LAC.js +0 -7
- package/dist/settings-manager-B4UN2LAC.js.map +0 -1
- package/dist/setup-44WLBIOT.js +0 -989
- package/dist/setup-44WLBIOT.js.map +0 -1
- package/dist/speech-GHTSWDAN.js +0 -9
- package/dist/speech-GHTSWDAN.js.map +0 -1
- package/dist/suggest-RST5VOHB.js +0 -36
- package/dist/suggest-RST5VOHB.js.map +0 -1
- package/dist/telegram-D7ASLVEB.js +0 -7
- package/dist/telegram-D7ASLVEB.js.map +0 -1
- package/dist/tunnel-ALJDPFDQ.js +0 -10
- package/dist/tunnel-ALJDPFDQ.js.map +0 -1
- package/dist/tunnel-service-TBAHDXMF.js +0 -755
- package/dist/tunnel-service-TBAHDXMF.js.map +0 -1
- package/dist/validators-GITLOFXC.js +0 -11
- package/dist/validators-GITLOFXC.js.map +0 -1
- package/dist/version-AXXV6IV2.js +0 -15
- package/dist/version-AXXV6IV2.js.map +0 -1
package/dist/chunk-LRV56K2M.js
DELETED
|
@@ -1,4106 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
AgentCatalog
|
|
3
|
-
} from "./chunk-MDJHCCFS.js";
|
|
4
|
-
import {
|
|
5
|
-
readTextFileWithRange
|
|
6
|
-
} from "./chunk-OYSAN7UX.js";
|
|
7
|
-
import {
|
|
8
|
-
closeSessionLogger,
|
|
9
|
-
createChildLogger,
|
|
10
|
-
createSessionLogger
|
|
11
|
-
} from "./chunk-R6KZYF7D.js";
|
|
12
|
-
import {
|
|
13
|
-
getAgentCapabilities
|
|
14
|
-
} from "./chunk-ZSLHHQPQ.js";
|
|
15
|
-
|
|
16
|
-
// src/core/utils/streams.ts
|
|
17
|
-
function nodeToWebWritable(nodeStream) {
|
|
18
|
-
return new WritableStream({
|
|
19
|
-
write(chunk) {
|
|
20
|
-
return new Promise((resolve, reject) => {
|
|
21
|
-
const ok = nodeStream.write(chunk);
|
|
22
|
-
if (ok) {
|
|
23
|
-
resolve();
|
|
24
|
-
return;
|
|
25
|
-
}
|
|
26
|
-
nodeStream.once("drain", resolve);
|
|
27
|
-
nodeStream.once("error", reject);
|
|
28
|
-
});
|
|
29
|
-
},
|
|
30
|
-
close() {
|
|
31
|
-
nodeStream.end();
|
|
32
|
-
},
|
|
33
|
-
abort(reason) {
|
|
34
|
-
nodeStream.destroy(reason instanceof Error ? reason : new Error(String(reason)));
|
|
35
|
-
}
|
|
36
|
-
});
|
|
37
|
-
}
|
|
38
|
-
function nodeToWebReadable(nodeStream) {
|
|
39
|
-
return new ReadableStream({
|
|
40
|
-
start(controller) {
|
|
41
|
-
nodeStream.on("data", (chunk) => controller.enqueue(new Uint8Array(chunk)));
|
|
42
|
-
nodeStream.on("end", () => controller.close());
|
|
43
|
-
nodeStream.on("error", (err) => controller.error(err));
|
|
44
|
-
},
|
|
45
|
-
cancel() {
|
|
46
|
-
nodeStream.destroy();
|
|
47
|
-
}
|
|
48
|
-
});
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
// src/core/utils/stderr-capture.ts
|
|
52
|
-
var StderrCapture = class {
|
|
53
|
-
constructor(maxLines = 50) {
|
|
54
|
-
this.maxLines = maxLines;
|
|
55
|
-
}
|
|
56
|
-
lines = [];
|
|
57
|
-
append(chunk) {
|
|
58
|
-
this.lines.push(...chunk.split("\n").filter(Boolean));
|
|
59
|
-
if (this.lines.length > this.maxLines) {
|
|
60
|
-
this.lines = this.lines.slice(-this.maxLines);
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
getLastLines() {
|
|
64
|
-
return this.lines.join("\n");
|
|
65
|
-
}
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
// src/core/utils/typed-emitter.ts
|
|
69
|
-
var TypedEmitter = class {
|
|
70
|
-
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
71
|
-
listeners = /* @__PURE__ */ new Map();
|
|
72
|
-
paused = false;
|
|
73
|
-
buffer = [];
|
|
74
|
-
on(event, listener) {
|
|
75
|
-
let set = this.listeners.get(event);
|
|
76
|
-
if (!set) {
|
|
77
|
-
set = /* @__PURE__ */ new Set();
|
|
78
|
-
this.listeners.set(event, set);
|
|
79
|
-
}
|
|
80
|
-
set.add(listener);
|
|
81
|
-
return this;
|
|
82
|
-
}
|
|
83
|
-
off(event, listener) {
|
|
84
|
-
this.listeners.get(event)?.delete(listener);
|
|
85
|
-
return this;
|
|
86
|
-
}
|
|
87
|
-
emit(event, ...args) {
|
|
88
|
-
if (this.paused) {
|
|
89
|
-
if (this.passthroughFn?.(event, args)) {
|
|
90
|
-
this.deliver(event, args);
|
|
91
|
-
} else {
|
|
92
|
-
this.buffer.push({ event, args });
|
|
93
|
-
}
|
|
94
|
-
return;
|
|
95
|
-
}
|
|
96
|
-
this.deliver(event, args);
|
|
97
|
-
}
|
|
98
|
-
/**
|
|
99
|
-
* Pause event delivery. Events emitted while paused are buffered.
|
|
100
|
-
* Optionally pass a filter to allow specific events through even while paused.
|
|
101
|
-
*/
|
|
102
|
-
pause(passthrough) {
|
|
103
|
-
this.paused = true;
|
|
104
|
-
this.passthroughFn = passthrough;
|
|
105
|
-
}
|
|
106
|
-
passthroughFn;
|
|
107
|
-
/** Resume event delivery and replay buffered events in order. */
|
|
108
|
-
resume() {
|
|
109
|
-
this.paused = false;
|
|
110
|
-
this.passthroughFn = void 0;
|
|
111
|
-
const buffered = this.buffer.splice(0);
|
|
112
|
-
for (const { event, args } of buffered) {
|
|
113
|
-
this.deliver(event, args);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
/** Discard all buffered events without delivering them. */
|
|
117
|
-
clearBuffer() {
|
|
118
|
-
this.buffer.length = 0;
|
|
119
|
-
}
|
|
120
|
-
get isPaused() {
|
|
121
|
-
return this.paused;
|
|
122
|
-
}
|
|
123
|
-
get bufferSize() {
|
|
124
|
-
return this.buffer.length;
|
|
125
|
-
}
|
|
126
|
-
removeAllListeners(event) {
|
|
127
|
-
if (event) {
|
|
128
|
-
this.listeners.delete(event);
|
|
129
|
-
} else {
|
|
130
|
-
this.listeners.clear();
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
deliver(event, args) {
|
|
134
|
-
const set = this.listeners.get(event);
|
|
135
|
-
if (!set) return;
|
|
136
|
-
for (const listener of set) {
|
|
137
|
-
listener(...args);
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
};
|
|
141
|
-
|
|
142
|
-
// src/core/agents/agent-instance.ts
|
|
143
|
-
import { spawn as spawn2, execFileSync } from "child_process";
|
|
144
|
-
import { Transform } from "stream";
|
|
145
|
-
import fs2 from "fs";
|
|
146
|
-
import path2 from "path";
|
|
147
|
-
import { ClientSideConnection, ndJsonStream } from "@agentclientprotocol/sdk";
|
|
148
|
-
import { PROTOCOL_VERSION } from "@agentclientprotocol/sdk";
|
|
149
|
-
|
|
150
|
-
// src/core/sessions/terminal-manager.ts
|
|
151
|
-
import { spawn } from "child_process";
|
|
152
|
-
import { randomUUID } from "crypto";
|
|
153
|
-
var TerminalManager = class {
|
|
154
|
-
terminals = /* @__PURE__ */ new Map();
|
|
155
|
-
maxOutputBytes;
|
|
156
|
-
constructor(maxOutputBytes = 1024 * 1024) {
|
|
157
|
-
this.maxOutputBytes = maxOutputBytes;
|
|
158
|
-
}
|
|
159
|
-
async createTerminal(sessionId, params, middlewareChain) {
|
|
160
|
-
let termCommand = params.command;
|
|
161
|
-
let termArgs = params.args ?? [];
|
|
162
|
-
let termEnvArr = params.env ?? [];
|
|
163
|
-
let termCwd = params.cwd ?? void 0;
|
|
164
|
-
if (middlewareChain) {
|
|
165
|
-
const envRecord = {};
|
|
166
|
-
for (const ev of termEnvArr) {
|
|
167
|
-
envRecord[ev.name] = ev.value;
|
|
168
|
-
}
|
|
169
|
-
const result = await middlewareChain.execute("terminal:beforeCreate", {
|
|
170
|
-
sessionId,
|
|
171
|
-
command: termCommand,
|
|
172
|
-
args: termArgs,
|
|
173
|
-
env: envRecord,
|
|
174
|
-
cwd: termCwd
|
|
175
|
-
}, async (p) => p);
|
|
176
|
-
if (!result) return { terminalId: "" };
|
|
177
|
-
termCommand = result.command;
|
|
178
|
-
termArgs = result.args ?? termArgs;
|
|
179
|
-
termCwd = result.cwd ?? termCwd;
|
|
180
|
-
if (result.env) {
|
|
181
|
-
termEnvArr = Object.entries(result.env).map(([name, value]) => ({ name, value }));
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
const terminalId = randomUUID();
|
|
185
|
-
const args = termArgs;
|
|
186
|
-
const env = {};
|
|
187
|
-
for (const ev of termEnvArr) {
|
|
188
|
-
env[ev.name] = ev.value;
|
|
189
|
-
}
|
|
190
|
-
const childProcess = spawn(termCommand, args, {
|
|
191
|
-
cwd: termCwd,
|
|
192
|
-
env: { ...process.env, ...env },
|
|
193
|
-
shell: false
|
|
194
|
-
});
|
|
195
|
-
const state = {
|
|
196
|
-
process: childProcess,
|
|
197
|
-
output: "",
|
|
198
|
-
exitStatus: null,
|
|
199
|
-
command: termCommand,
|
|
200
|
-
startTime: Date.now()
|
|
201
|
-
};
|
|
202
|
-
this.terminals.set(terminalId, state);
|
|
203
|
-
const outputByteLimit = params.outputByteLimit ?? this.maxOutputBytes;
|
|
204
|
-
const appendOutput = (chunk) => {
|
|
205
|
-
state.output += chunk;
|
|
206
|
-
const bytes = Buffer.byteLength(state.output, "utf-8");
|
|
207
|
-
if (bytes > outputByteLimit) {
|
|
208
|
-
const excess = bytes - outputByteLimit;
|
|
209
|
-
state.output = state.output.slice(excess);
|
|
210
|
-
}
|
|
211
|
-
};
|
|
212
|
-
childProcess.stdout?.on(
|
|
213
|
-
"data",
|
|
214
|
-
(chunk) => appendOutput(chunk.toString())
|
|
215
|
-
);
|
|
216
|
-
childProcess.stderr?.on(
|
|
217
|
-
"data",
|
|
218
|
-
(chunk) => appendOutput(chunk.toString())
|
|
219
|
-
);
|
|
220
|
-
childProcess.on("exit", (code, signal) => {
|
|
221
|
-
state.exitStatus = { exitCode: code, signal };
|
|
222
|
-
if (middlewareChain) {
|
|
223
|
-
middlewareChain.execute("terminal:afterExit", {
|
|
224
|
-
sessionId,
|
|
225
|
-
terminalId,
|
|
226
|
-
command: state.command,
|
|
227
|
-
exitCode: code ?? -1,
|
|
228
|
-
durationMs: Date.now() - state.startTime
|
|
229
|
-
}, async (p) => p).catch(() => {
|
|
230
|
-
});
|
|
231
|
-
}
|
|
232
|
-
});
|
|
233
|
-
return { terminalId };
|
|
234
|
-
}
|
|
235
|
-
getOutput(terminalId) {
|
|
236
|
-
const state = this.terminals.get(terminalId);
|
|
237
|
-
if (!state) {
|
|
238
|
-
throw new Error(`Terminal not found: ${terminalId}`);
|
|
239
|
-
}
|
|
240
|
-
return {
|
|
241
|
-
output: state.output,
|
|
242
|
-
truncated: false,
|
|
243
|
-
exitStatus: state.exitStatus ? {
|
|
244
|
-
exitCode: state.exitStatus.exitCode,
|
|
245
|
-
signal: state.exitStatus.signal
|
|
246
|
-
} : void 0
|
|
247
|
-
};
|
|
248
|
-
}
|
|
249
|
-
async waitForExit(terminalId) {
|
|
250
|
-
const state = this.terminals.get(terminalId);
|
|
251
|
-
if (!state) {
|
|
252
|
-
throw new Error(`Terminal not found: ${terminalId}`);
|
|
253
|
-
}
|
|
254
|
-
if (state.exitStatus !== null) {
|
|
255
|
-
return {
|
|
256
|
-
exitCode: state.exitStatus.exitCode,
|
|
257
|
-
signal: state.exitStatus.signal
|
|
258
|
-
};
|
|
259
|
-
}
|
|
260
|
-
return new Promise((resolve) => {
|
|
261
|
-
state.process.on("exit", (code, signal) => {
|
|
262
|
-
resolve({ exitCode: code, signal });
|
|
263
|
-
});
|
|
264
|
-
});
|
|
265
|
-
}
|
|
266
|
-
kill(terminalId) {
|
|
267
|
-
const state = this.terminals.get(terminalId);
|
|
268
|
-
if (!state) {
|
|
269
|
-
throw new Error(`Terminal not found: ${terminalId}`);
|
|
270
|
-
}
|
|
271
|
-
state.process.kill("SIGTERM");
|
|
272
|
-
}
|
|
273
|
-
release(terminalId) {
|
|
274
|
-
const state = this.terminals.get(terminalId);
|
|
275
|
-
if (!state) {
|
|
276
|
-
return;
|
|
277
|
-
}
|
|
278
|
-
state.process.kill("SIGKILL");
|
|
279
|
-
this.terminals.delete(terminalId);
|
|
280
|
-
}
|
|
281
|
-
destroyAll() {
|
|
282
|
-
for (const [, t] of this.terminals) {
|
|
283
|
-
t.process.kill("SIGKILL");
|
|
284
|
-
}
|
|
285
|
-
this.terminals.clear();
|
|
286
|
-
}
|
|
287
|
-
};
|
|
288
|
-
|
|
289
|
-
// src/core/agents/mcp-manager.ts
|
|
290
|
-
var McpManager = class {
|
|
291
|
-
/**
|
|
292
|
-
* Resolve the MCP server config to pass to ACP session methods.
|
|
293
|
-
* Returns the provided config array or an empty array if none given.
|
|
294
|
-
*/
|
|
295
|
-
resolve(sessionConfig) {
|
|
296
|
-
return sessionConfig ?? [];
|
|
297
|
-
}
|
|
298
|
-
};
|
|
299
|
-
|
|
300
|
-
// src/core/utils/debug-tracer.ts
|
|
301
|
-
import fs from "fs";
|
|
302
|
-
import path from "path";
|
|
303
|
-
var DEBUG_ENABLED = process.env.OPENACP_DEBUG === "true" || process.env.OPENACP_DEBUG === "1";
|
|
304
|
-
var DebugTracer = class {
|
|
305
|
-
constructor(sessionId, workingDirectory) {
|
|
306
|
-
this.sessionId = sessionId;
|
|
307
|
-
this.workingDirectory = workingDirectory;
|
|
308
|
-
this.logDir = path.join(workingDirectory, ".log");
|
|
309
|
-
}
|
|
310
|
-
dirCreated = false;
|
|
311
|
-
logDir;
|
|
312
|
-
log(layer, data) {
|
|
313
|
-
try {
|
|
314
|
-
if (!this.dirCreated) {
|
|
315
|
-
fs.mkdirSync(this.logDir, { recursive: true });
|
|
316
|
-
this.dirCreated = true;
|
|
317
|
-
}
|
|
318
|
-
const filePath = path.join(this.logDir, `${this.sessionId}_${layer}.jsonl`);
|
|
319
|
-
const line = JSON.stringify({ ts: Date.now(), ...data }) + "\n";
|
|
320
|
-
fs.appendFileSync(filePath, line);
|
|
321
|
-
} catch {
|
|
322
|
-
}
|
|
323
|
-
}
|
|
324
|
-
/** No-op cleanup — establishes the pattern for future async implementations */
|
|
325
|
-
destroy() {
|
|
326
|
-
}
|
|
327
|
-
};
|
|
328
|
-
function createDebugTracer(sessionId, workingDirectory) {
|
|
329
|
-
if (!DEBUG_ENABLED) return null;
|
|
330
|
-
return new DebugTracer(sessionId, workingDirectory);
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// src/core/agents/agent-instance.ts
|
|
334
|
-
var log = createChildLogger({ module: "agent-instance" });
|
|
335
|
-
function findPackageRoot(startDir) {
|
|
336
|
-
let dir = startDir;
|
|
337
|
-
while (dir !== path2.dirname(dir)) {
|
|
338
|
-
if (fs2.existsSync(path2.join(dir, "package.json"))) {
|
|
339
|
-
return dir;
|
|
340
|
-
}
|
|
341
|
-
dir = path2.dirname(dir);
|
|
342
|
-
}
|
|
343
|
-
return startDir;
|
|
344
|
-
}
|
|
345
|
-
function resolveAgentCommand(cmd) {
|
|
346
|
-
const searchRoots = [process.cwd()];
|
|
347
|
-
const ownDir = findPackageRoot(import.meta.dirname);
|
|
348
|
-
if (ownDir !== process.cwd()) {
|
|
349
|
-
searchRoots.push(ownDir);
|
|
350
|
-
}
|
|
351
|
-
for (const root of searchRoots) {
|
|
352
|
-
const packageDirs = [
|
|
353
|
-
path2.resolve(root, "node_modules", "@zed-industries", cmd, "dist", "index.js"),
|
|
354
|
-
path2.resolve(root, "node_modules", cmd, "dist", "index.js")
|
|
355
|
-
];
|
|
356
|
-
for (const jsPath of packageDirs) {
|
|
357
|
-
if (fs2.existsSync(jsPath)) {
|
|
358
|
-
return { command: process.execPath, args: [jsPath] };
|
|
359
|
-
}
|
|
360
|
-
}
|
|
361
|
-
}
|
|
362
|
-
for (const root of searchRoots) {
|
|
363
|
-
const localBin = path2.resolve(root, "node_modules", ".bin", cmd);
|
|
364
|
-
if (fs2.existsSync(localBin)) {
|
|
365
|
-
const content = fs2.readFileSync(localBin, "utf-8");
|
|
366
|
-
if (content.startsWith("#!/usr/bin/env node")) {
|
|
367
|
-
return { command: process.execPath, args: [localBin] };
|
|
368
|
-
}
|
|
369
|
-
const match = content.match(/"([^"]+\.js)"/);
|
|
370
|
-
if (match) {
|
|
371
|
-
const target = path2.resolve(path2.dirname(localBin), match[1]);
|
|
372
|
-
if (fs2.existsSync(target)) {
|
|
373
|
-
return { command: process.execPath, args: [target] };
|
|
374
|
-
}
|
|
375
|
-
}
|
|
376
|
-
}
|
|
377
|
-
}
|
|
378
|
-
try {
|
|
379
|
-
const fullPath = execFileSync("which", [cmd], { encoding: "utf-8" }).trim();
|
|
380
|
-
if (fullPath) {
|
|
381
|
-
const content = fs2.readFileSync(fullPath, "utf-8");
|
|
382
|
-
if (content.startsWith("#!/usr/bin/env node")) {
|
|
383
|
-
return { command: process.execPath, args: [fullPath] };
|
|
384
|
-
}
|
|
385
|
-
}
|
|
386
|
-
} catch {
|
|
387
|
-
}
|
|
388
|
-
return { command: cmd, args: [] };
|
|
389
|
-
}
|
|
390
|
-
var AgentInstance = class _AgentInstance extends TypedEmitter {
|
|
391
|
-
connection;
|
|
392
|
-
child;
|
|
393
|
-
stderrCapture;
|
|
394
|
-
terminalManager = new TerminalManager();
|
|
395
|
-
static mcpManager = new McpManager();
|
|
396
|
-
_destroying = false;
|
|
397
|
-
sessionId;
|
|
398
|
-
agentName;
|
|
399
|
-
promptCapabilities;
|
|
400
|
-
middlewareChain;
|
|
401
|
-
debugTracer = null;
|
|
402
|
-
// Callback — set by core when wiring events
|
|
403
|
-
onPermissionRequest = async () => "";
|
|
404
|
-
constructor(agentName) {
|
|
405
|
-
super();
|
|
406
|
-
this.agentName = agentName;
|
|
407
|
-
}
|
|
408
|
-
static async spawnSubprocess(agentDef, workingDirectory) {
|
|
409
|
-
const instance = new _AgentInstance(agentDef.name);
|
|
410
|
-
const resolved = resolveAgentCommand(agentDef.command);
|
|
411
|
-
log.debug(
|
|
412
|
-
{
|
|
413
|
-
agentName: agentDef.name,
|
|
414
|
-
command: resolved.command,
|
|
415
|
-
args: resolved.args
|
|
416
|
-
},
|
|
417
|
-
"Resolved agent command"
|
|
418
|
-
);
|
|
419
|
-
instance.child = spawn2(
|
|
420
|
-
resolved.command,
|
|
421
|
-
[...resolved.args, ...agentDef.args],
|
|
422
|
-
{
|
|
423
|
-
stdio: ["pipe", "pipe", "pipe"],
|
|
424
|
-
cwd: workingDirectory,
|
|
425
|
-
env: { ...process.env, ...agentDef.env }
|
|
426
|
-
}
|
|
427
|
-
);
|
|
428
|
-
await new Promise((resolve, reject) => {
|
|
429
|
-
instance.child.on("error", (err) => {
|
|
430
|
-
reject(
|
|
431
|
-
new Error(
|
|
432
|
-
`Failed to spawn agent "${agentDef.name}": ${err.message}. Is "${agentDef.command}" installed?`
|
|
433
|
-
)
|
|
434
|
-
);
|
|
435
|
-
});
|
|
436
|
-
instance.child.on("spawn", () => resolve());
|
|
437
|
-
});
|
|
438
|
-
instance.stderrCapture = new StderrCapture(50);
|
|
439
|
-
instance.child.stderr.on("data", (chunk) => {
|
|
440
|
-
instance.stderrCapture.append(chunk.toString());
|
|
441
|
-
});
|
|
442
|
-
const stdinLogger = new Transform({
|
|
443
|
-
transform(chunk, _enc, cb) {
|
|
444
|
-
if (instance.debugTracer) {
|
|
445
|
-
const raw = chunk.toString().trimEnd();
|
|
446
|
-
try {
|
|
447
|
-
instance.debugTracer.log("acp", { dir: "send", data: JSON.parse(raw) });
|
|
448
|
-
} catch {
|
|
449
|
-
instance.debugTracer.log("acp", { dir: "send", data: raw });
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
cb(null, chunk);
|
|
453
|
-
}
|
|
454
|
-
});
|
|
455
|
-
stdinLogger.pipe(instance.child.stdin);
|
|
456
|
-
const stdoutLogger = new Transform({
|
|
457
|
-
transform(chunk, _enc, cb) {
|
|
458
|
-
if (instance.debugTracer) {
|
|
459
|
-
const raw = chunk.toString().trimEnd();
|
|
460
|
-
try {
|
|
461
|
-
instance.debugTracer.log("acp", { dir: "recv", data: JSON.parse(raw) });
|
|
462
|
-
} catch {
|
|
463
|
-
instance.debugTracer.log("acp", { dir: "recv", data: raw });
|
|
464
|
-
}
|
|
465
|
-
}
|
|
466
|
-
cb(null, chunk);
|
|
467
|
-
}
|
|
468
|
-
});
|
|
469
|
-
instance.child.stdout.pipe(stdoutLogger);
|
|
470
|
-
const toAgent = nodeToWebWritable(stdinLogger);
|
|
471
|
-
const fromAgent = nodeToWebReadable(stdoutLogger);
|
|
472
|
-
const stream = ndJsonStream(toAgent, fromAgent);
|
|
473
|
-
instance.connection = new ClientSideConnection(
|
|
474
|
-
(_agent) => instance.createClient(_agent),
|
|
475
|
-
stream
|
|
476
|
-
);
|
|
477
|
-
const initResponse = await instance.connection.initialize({
|
|
478
|
-
protocolVersion: PROTOCOL_VERSION,
|
|
479
|
-
clientCapabilities: {
|
|
480
|
-
fs: { readTextFile: true, writeTextFile: true },
|
|
481
|
-
terminal: true
|
|
482
|
-
}
|
|
483
|
-
});
|
|
484
|
-
if (initResponse.protocolVersion !== PROTOCOL_VERSION) {
|
|
485
|
-
log.warn(
|
|
486
|
-
{ expected: PROTOCOL_VERSION, got: initResponse.protocolVersion },
|
|
487
|
-
"ACP protocol version mismatch \u2014 some features may not work correctly"
|
|
488
|
-
);
|
|
489
|
-
}
|
|
490
|
-
instance.promptCapabilities = initResponse.agentCapabilities?.promptCapabilities;
|
|
491
|
-
log.info(
|
|
492
|
-
{ promptCapabilities: instance.promptCapabilities ?? {} },
|
|
493
|
-
"Agent prompt capabilities"
|
|
494
|
-
);
|
|
495
|
-
return instance;
|
|
496
|
-
}
|
|
497
|
-
setupCrashDetection() {
|
|
498
|
-
this.child.on("exit", (code, signal) => {
|
|
499
|
-
if (this._destroying) return;
|
|
500
|
-
log.info(
|
|
501
|
-
{ sessionId: this.sessionId, exitCode: code, signal },
|
|
502
|
-
"Agent process exited"
|
|
503
|
-
);
|
|
504
|
-
if (code !== 0 && code !== null || signal) {
|
|
505
|
-
const stderr = this.stderrCapture.getLastLines();
|
|
506
|
-
this.emit("agent_event", {
|
|
507
|
-
type: "error",
|
|
508
|
-
message: signal ? `Agent killed by signal ${signal}
|
|
509
|
-
${stderr}` : `Agent crashed (exit code ${code})
|
|
510
|
-
${stderr}`
|
|
511
|
-
});
|
|
512
|
-
}
|
|
513
|
-
});
|
|
514
|
-
this.connection.closed.then(() => {
|
|
515
|
-
log.debug({ sessionId: this.sessionId }, "ACP connection closed");
|
|
516
|
-
});
|
|
517
|
-
}
|
|
518
|
-
static async spawn(agentDef, workingDirectory, mcpServers) {
|
|
519
|
-
log.debug(
|
|
520
|
-
{ agentName: agentDef.name, command: agentDef.command },
|
|
521
|
-
"Spawning agent"
|
|
522
|
-
);
|
|
523
|
-
const spawnStart = Date.now();
|
|
524
|
-
const instance = await _AgentInstance.spawnSubprocess(
|
|
525
|
-
agentDef,
|
|
526
|
-
workingDirectory
|
|
527
|
-
);
|
|
528
|
-
const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
|
|
529
|
-
const response = await instance.connection.newSession({
|
|
530
|
-
cwd: workingDirectory,
|
|
531
|
-
mcpServers: resolvedMcp
|
|
532
|
-
});
|
|
533
|
-
instance.sessionId = response.sessionId;
|
|
534
|
-
instance.debugTracer = createDebugTracer(response.sessionId, workingDirectory);
|
|
535
|
-
instance.setupCrashDetection();
|
|
536
|
-
log.info(
|
|
537
|
-
{ sessionId: response.sessionId, durationMs: Date.now() - spawnStart },
|
|
538
|
-
"Agent spawn complete"
|
|
539
|
-
);
|
|
540
|
-
return instance;
|
|
541
|
-
}
|
|
542
|
-
static async resume(agentDef, workingDirectory, agentSessionId, mcpServers) {
|
|
543
|
-
log.debug({ agentName: agentDef.name, agentSessionId }, "Resuming agent");
|
|
544
|
-
const spawnStart = Date.now();
|
|
545
|
-
const instance = await _AgentInstance.spawnSubprocess(
|
|
546
|
-
agentDef,
|
|
547
|
-
workingDirectory
|
|
548
|
-
);
|
|
549
|
-
try {
|
|
550
|
-
const response = await instance.connection.unstable_resumeSession({
|
|
551
|
-
sessionId: agentSessionId,
|
|
552
|
-
cwd: workingDirectory
|
|
553
|
-
});
|
|
554
|
-
instance.sessionId = response.sessionId;
|
|
555
|
-
instance.debugTracer = createDebugTracer(response.sessionId, workingDirectory);
|
|
556
|
-
log.info(
|
|
557
|
-
{ sessionId: response.sessionId, durationMs: Date.now() - spawnStart },
|
|
558
|
-
"Agent resume complete"
|
|
559
|
-
);
|
|
560
|
-
} catch (err) {
|
|
561
|
-
log.warn(
|
|
562
|
-
{ err, agentSessionId },
|
|
563
|
-
"Resume failed, falling back to new session"
|
|
564
|
-
);
|
|
565
|
-
const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
|
|
566
|
-
const response = await instance.connection.newSession({
|
|
567
|
-
cwd: workingDirectory,
|
|
568
|
-
mcpServers: resolvedMcp
|
|
569
|
-
});
|
|
570
|
-
instance.sessionId = response.sessionId;
|
|
571
|
-
instance.debugTracer = createDebugTracer(response.sessionId, workingDirectory);
|
|
572
|
-
log.info(
|
|
573
|
-
{ sessionId: response.sessionId, durationMs: Date.now() - spawnStart },
|
|
574
|
-
"Agent fallback spawn complete"
|
|
575
|
-
);
|
|
576
|
-
}
|
|
577
|
-
instance.setupCrashDetection();
|
|
578
|
-
return instance;
|
|
579
|
-
}
|
|
580
|
-
// createClient — implemented in Task 6b
|
|
581
|
-
createClient(_agent) {
|
|
582
|
-
const self = this;
|
|
583
|
-
const MAX_OUTPUT_BYTES = 1024 * 1024;
|
|
584
|
-
return {
|
|
585
|
-
// ── Session updates ──────────────────────────────────────────────────
|
|
586
|
-
async sessionUpdate(params) {
|
|
587
|
-
const update = params.update;
|
|
588
|
-
let event = null;
|
|
589
|
-
switch (update.sessionUpdate) {
|
|
590
|
-
case "agent_message_chunk":
|
|
591
|
-
if (update.content.type === "text") {
|
|
592
|
-
event = { type: "text", content: update.content.text };
|
|
593
|
-
} else if (update.content.type === "image") {
|
|
594
|
-
const c = update.content;
|
|
595
|
-
event = { type: "image_content", data: c.data, mimeType: c.mimeType };
|
|
596
|
-
} else if (update.content.type === "audio") {
|
|
597
|
-
const c = update.content;
|
|
598
|
-
event = { type: "audio_content", data: c.data, mimeType: c.mimeType };
|
|
599
|
-
} else if (update.content.type === "resource") {
|
|
600
|
-
const c = update.content;
|
|
601
|
-
event = {
|
|
602
|
-
type: "resource_content",
|
|
603
|
-
uri: c.resource.uri,
|
|
604
|
-
name: c.resource.uri,
|
|
605
|
-
text: c.resource.text ?? void 0,
|
|
606
|
-
blob: c.resource.blob ?? void 0,
|
|
607
|
-
mimeType: c.resource.mimeType ?? void 0
|
|
608
|
-
};
|
|
609
|
-
} else if (update.content.type === "resource_link") {
|
|
610
|
-
const c = update.content;
|
|
611
|
-
event = {
|
|
612
|
-
type: "resource_link",
|
|
613
|
-
uri: c.uri,
|
|
614
|
-
name: c.name,
|
|
615
|
-
mimeType: c.mimeType ?? void 0,
|
|
616
|
-
title: c.title ?? void 0,
|
|
617
|
-
description: c.description ?? void 0,
|
|
618
|
-
size: c.size ?? void 0
|
|
619
|
-
};
|
|
620
|
-
}
|
|
621
|
-
break;
|
|
622
|
-
case "agent_thought_chunk":
|
|
623
|
-
if (update.content.type === "text") {
|
|
624
|
-
event = { type: "thought", content: update.content.text };
|
|
625
|
-
}
|
|
626
|
-
break;
|
|
627
|
-
case "tool_call": {
|
|
628
|
-
const tc = update;
|
|
629
|
-
event = {
|
|
630
|
-
type: "tool_call",
|
|
631
|
-
id: update.toolCallId,
|
|
632
|
-
name: update.title,
|
|
633
|
-
kind: update.kind ?? void 0,
|
|
634
|
-
status: update.status ?? "pending",
|
|
635
|
-
content: update.content ?? void 0,
|
|
636
|
-
rawInput: tc.rawInput ?? void 0,
|
|
637
|
-
rawOutput: tc.rawOutput ?? void 0,
|
|
638
|
-
meta: tc._meta ?? void 0
|
|
639
|
-
};
|
|
640
|
-
break;
|
|
641
|
-
}
|
|
642
|
-
case "tool_call_update": {
|
|
643
|
-
const tcu = update;
|
|
644
|
-
event = {
|
|
645
|
-
type: "tool_update",
|
|
646
|
-
id: update.toolCallId,
|
|
647
|
-
name: update.title ?? void 0,
|
|
648
|
-
kind: update.kind ?? void 0,
|
|
649
|
-
status: update.status ?? "pending",
|
|
650
|
-
content: update.content ?? void 0,
|
|
651
|
-
rawInput: tcu.rawInput ?? void 0,
|
|
652
|
-
rawOutput: tcu.rawOutput ?? void 0,
|
|
653
|
-
meta: tcu._meta ?? void 0
|
|
654
|
-
};
|
|
655
|
-
break;
|
|
656
|
-
}
|
|
657
|
-
case "plan":
|
|
658
|
-
event = { type: "plan", entries: update.entries };
|
|
659
|
-
break;
|
|
660
|
-
case "usage_update":
|
|
661
|
-
event = {
|
|
662
|
-
type: "usage",
|
|
663
|
-
tokensUsed: update.used,
|
|
664
|
-
contextSize: update.size,
|
|
665
|
-
cost: update.cost ?? void 0
|
|
666
|
-
};
|
|
667
|
-
break;
|
|
668
|
-
case "available_commands_update":
|
|
669
|
-
event = {
|
|
670
|
-
type: "commands_update",
|
|
671
|
-
commands: update.availableCommands
|
|
672
|
-
};
|
|
673
|
-
break;
|
|
674
|
-
case "session_info_update": {
|
|
675
|
-
const si = update;
|
|
676
|
-
event = {
|
|
677
|
-
type: "session_info_update",
|
|
678
|
-
title: si.title ?? void 0,
|
|
679
|
-
updatedAt: si.updatedAt ?? void 0,
|
|
680
|
-
_meta: si._meta ?? void 0
|
|
681
|
-
};
|
|
682
|
-
break;
|
|
683
|
-
}
|
|
684
|
-
case "current_mode_update": {
|
|
685
|
-
const cm = update;
|
|
686
|
-
event = {
|
|
687
|
-
type: "current_mode_update",
|
|
688
|
-
modeId: cm.currentModeId
|
|
689
|
-
};
|
|
690
|
-
break;
|
|
691
|
-
}
|
|
692
|
-
case "config_option_update": {
|
|
693
|
-
const co = update;
|
|
694
|
-
event = {
|
|
695
|
-
type: "config_option_update",
|
|
696
|
-
options: co.configOptions ?? []
|
|
697
|
-
};
|
|
698
|
-
break;
|
|
699
|
-
}
|
|
700
|
-
case "user_message_chunk": {
|
|
701
|
-
const um = update;
|
|
702
|
-
event = {
|
|
703
|
-
type: "user_message_chunk",
|
|
704
|
-
content: um.content?.text ?? ""
|
|
705
|
-
};
|
|
706
|
-
break;
|
|
707
|
-
}
|
|
708
|
-
// NOTE: model_update is NOT a session update type in the ACP SDK schema.
|
|
709
|
-
// Model changes are applied via the unstable_setSessionModel() method and
|
|
710
|
-
// the response is synchronous — the SDK does not push a model_update
|
|
711
|
-
// notification to the client. Therefore AgentEvent "model_update" cannot
|
|
712
|
-
// originate from sessionUpdate and must be emitted by callers of setModel()
|
|
713
|
-
// if they need to propagate the change downstream.
|
|
714
|
-
default:
|
|
715
|
-
return;
|
|
716
|
-
}
|
|
717
|
-
if (event !== null) {
|
|
718
|
-
self.emit("agent_event", event);
|
|
719
|
-
}
|
|
720
|
-
},
|
|
721
|
-
// ── Permission requests ──────────────────────────────────────────────
|
|
722
|
-
async requestPermission(params) {
|
|
723
|
-
const permissionRequest = {
|
|
724
|
-
id: params.toolCall.toolCallId,
|
|
725
|
-
description: params.toolCall.title ?? params.toolCall.toolCallId,
|
|
726
|
-
options: params.options.map((opt) => ({
|
|
727
|
-
id: opt.optionId,
|
|
728
|
-
label: opt.name,
|
|
729
|
-
isAllow: opt.kind === "allow_once" || opt.kind === "allow_always"
|
|
730
|
-
}))
|
|
731
|
-
};
|
|
732
|
-
const selectedOptionId = await self.onPermissionRequest(permissionRequest);
|
|
733
|
-
return {
|
|
734
|
-
outcome: { outcome: "selected", optionId: selectedOptionId }
|
|
735
|
-
};
|
|
736
|
-
},
|
|
737
|
-
// ── File operations ──────────────────────────────────────────────────
|
|
738
|
-
async readTextFile(params) {
|
|
739
|
-
const p = params;
|
|
740
|
-
if (self.middlewareChain) {
|
|
741
|
-
const result = await self.middlewareChain.execute("fs:beforeRead", { sessionId: self.sessionId, path: p.path, line: p.line, limit: p.limit }, async (r) => r);
|
|
742
|
-
if (!result) return { content: "" };
|
|
743
|
-
p.path = result.path;
|
|
744
|
-
}
|
|
745
|
-
const content = await readTextFileWithRange(p.path, {
|
|
746
|
-
line: p.line ?? void 0,
|
|
747
|
-
limit: p.limit ?? void 0
|
|
748
|
-
});
|
|
749
|
-
return { content };
|
|
750
|
-
},
|
|
751
|
-
async writeTextFile(params) {
|
|
752
|
-
let writePath = params.path;
|
|
753
|
-
let writeContent = params.content;
|
|
754
|
-
if (self.middlewareChain) {
|
|
755
|
-
const result = await self.middlewareChain.execute("fs:beforeWrite", { sessionId: self.sessionId, path: writePath, content: writeContent }, async (r) => r);
|
|
756
|
-
if (!result) return {};
|
|
757
|
-
writePath = result.path;
|
|
758
|
-
writeContent = result.content;
|
|
759
|
-
}
|
|
760
|
-
await fs2.promises.mkdir(path2.dirname(writePath), { recursive: true });
|
|
761
|
-
await fs2.promises.writeFile(writePath, writeContent, "utf-8");
|
|
762
|
-
return {};
|
|
763
|
-
},
|
|
764
|
-
// ── Terminal operations (delegated to TerminalManager) ─────────────
|
|
765
|
-
async createTerminal(params) {
|
|
766
|
-
return self.terminalManager.createTerminal(
|
|
767
|
-
self.sessionId,
|
|
768
|
-
{
|
|
769
|
-
command: params.command,
|
|
770
|
-
args: params.args,
|
|
771
|
-
env: params.env,
|
|
772
|
-
cwd: params.cwd,
|
|
773
|
-
outputByteLimit: params.outputByteLimit ?? MAX_OUTPUT_BYTES
|
|
774
|
-
},
|
|
775
|
-
self.middlewareChain
|
|
776
|
-
);
|
|
777
|
-
},
|
|
778
|
-
async terminalOutput(params) {
|
|
779
|
-
return self.terminalManager.getOutput(params.terminalId);
|
|
780
|
-
},
|
|
781
|
-
async waitForTerminalExit(params) {
|
|
782
|
-
return self.terminalManager.waitForExit(params.terminalId);
|
|
783
|
-
},
|
|
784
|
-
async killTerminal(params) {
|
|
785
|
-
self.terminalManager.kill(params.terminalId);
|
|
786
|
-
return {};
|
|
787
|
-
},
|
|
788
|
-
async releaseTerminal(params) {
|
|
789
|
-
self.terminalManager.release(params.terminalId);
|
|
790
|
-
}
|
|
791
|
-
};
|
|
792
|
-
}
|
|
793
|
-
// ── New ACP methods ──────────────────────────────────────────────────
|
|
794
|
-
async setMode(modeId) {
|
|
795
|
-
await this.connection.setSessionMode({ sessionId: this.sessionId, modeId });
|
|
796
|
-
}
|
|
797
|
-
async setConfigOption(configId, value) {
|
|
798
|
-
return await this.connection.setSessionConfigOption({
|
|
799
|
-
sessionId: this.sessionId,
|
|
800
|
-
configId,
|
|
801
|
-
...value
|
|
802
|
-
});
|
|
803
|
-
}
|
|
804
|
-
async setModel(modelId) {
|
|
805
|
-
await this.connection.unstable_setSessionModel({
|
|
806
|
-
sessionId: this.sessionId,
|
|
807
|
-
modelId
|
|
808
|
-
});
|
|
809
|
-
}
|
|
810
|
-
async listSessions(cwd, cursor) {
|
|
811
|
-
return await this.connection.listSessions({
|
|
812
|
-
cwd: cwd ?? null,
|
|
813
|
-
cursor: cursor ?? null
|
|
814
|
-
});
|
|
815
|
-
}
|
|
816
|
-
async loadSession(sessionId, cwd, mcpServers) {
|
|
817
|
-
const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
|
|
818
|
-
return await this.connection.loadSession({
|
|
819
|
-
sessionId,
|
|
820
|
-
cwd,
|
|
821
|
-
mcpServers: resolvedMcp
|
|
822
|
-
});
|
|
823
|
-
}
|
|
824
|
-
async authenticate(methodId) {
|
|
825
|
-
await this.connection.authenticate({ methodId });
|
|
826
|
-
}
|
|
827
|
-
async forkSession(sessionId, cwd, mcpServers) {
|
|
828
|
-
const resolvedMcp = _AgentInstance.mcpManager.resolve(mcpServers);
|
|
829
|
-
return await this.connection.unstable_forkSession({
|
|
830
|
-
sessionId,
|
|
831
|
-
cwd,
|
|
832
|
-
mcpServers: resolvedMcp
|
|
833
|
-
});
|
|
834
|
-
}
|
|
835
|
-
async closeSession(sessionId) {
|
|
836
|
-
await this.connection.unstable_closeSession({ sessionId });
|
|
837
|
-
}
|
|
838
|
-
// ── Prompt & lifecycle ──────────────────────────────────────────────
|
|
839
|
-
async prompt(text, attachments) {
|
|
840
|
-
const contentBlocks = [{ type: "text", text }];
|
|
841
|
-
const SUPPORTED_IMAGE_MIMES = /* @__PURE__ */ new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]);
|
|
842
|
-
for (const att of attachments ?? []) {
|
|
843
|
-
const tooLarge = att.size > 10 * 1024 * 1024;
|
|
844
|
-
if (att.type === "image" && this.promptCapabilities?.image && !tooLarge && SUPPORTED_IMAGE_MIMES.has(att.mimeType)) {
|
|
845
|
-
const data = await fs2.promises.readFile(att.filePath);
|
|
846
|
-
contentBlocks.push({ type: "image", data: data.toString("base64"), mimeType: att.mimeType });
|
|
847
|
-
} else if (att.type === "audio" && this.promptCapabilities?.audio && !tooLarge) {
|
|
848
|
-
const data = await fs2.promises.readFile(att.filePath);
|
|
849
|
-
contentBlocks.push({ type: "audio", data: data.toString("base64"), mimeType: att.mimeType });
|
|
850
|
-
} else {
|
|
851
|
-
if ((att.type === "image" || att.type === "audio") && !tooLarge) {
|
|
852
|
-
log.debug(
|
|
853
|
-
{ type: att.type, capabilities: this.promptCapabilities ?? {} },
|
|
854
|
-
"Agent does not support %s content, falling back to file path",
|
|
855
|
-
att.type
|
|
856
|
-
);
|
|
857
|
-
}
|
|
858
|
-
contentBlocks[0].text += `
|
|
859
|
-
|
|
860
|
-
[Attached file: ${att.filePath}]`;
|
|
861
|
-
}
|
|
862
|
-
}
|
|
863
|
-
return this.connection.prompt({
|
|
864
|
-
sessionId: this.sessionId,
|
|
865
|
-
prompt: contentBlocks
|
|
866
|
-
});
|
|
867
|
-
}
|
|
868
|
-
async cancel() {
|
|
869
|
-
await this.connection.cancel({ sessionId: this.sessionId });
|
|
870
|
-
}
|
|
871
|
-
async destroy() {
|
|
872
|
-
this._destroying = true;
|
|
873
|
-
this.terminalManager.destroyAll();
|
|
874
|
-
if (this.child.exitCode !== null) return;
|
|
875
|
-
await new Promise((resolve) => {
|
|
876
|
-
this.child.on("exit", () => {
|
|
877
|
-
clearTimeout(forceKillTimer);
|
|
878
|
-
resolve();
|
|
879
|
-
});
|
|
880
|
-
this.child.kill("SIGTERM");
|
|
881
|
-
const forceKillTimer = setTimeout(() => {
|
|
882
|
-
if (this.child.exitCode === null) this.child.kill("SIGKILL");
|
|
883
|
-
resolve();
|
|
884
|
-
}, 1e4);
|
|
885
|
-
if (typeof forceKillTimer === "object" && forceKillTimer !== null && "unref" in forceKillTimer) {
|
|
886
|
-
forceKillTimer.unref();
|
|
887
|
-
}
|
|
888
|
-
});
|
|
889
|
-
}
|
|
890
|
-
};
|
|
891
|
-
|
|
892
|
-
// src/core/agents/agent-manager.ts
|
|
893
|
-
var AgentManager = class {
|
|
894
|
-
constructor(catalog) {
|
|
895
|
-
this.catalog = catalog;
|
|
896
|
-
}
|
|
897
|
-
getAvailableAgents() {
|
|
898
|
-
const installed = this.catalog.getInstalledEntries();
|
|
899
|
-
return Object.entries(installed).map(([key, agent]) => ({
|
|
900
|
-
name: key,
|
|
901
|
-
command: agent.command,
|
|
902
|
-
args: agent.args,
|
|
903
|
-
env: agent.env
|
|
904
|
-
}));
|
|
905
|
-
}
|
|
906
|
-
getAgent(name) {
|
|
907
|
-
return this.catalog.resolve(name);
|
|
908
|
-
}
|
|
909
|
-
async spawn(agentName, workingDirectory) {
|
|
910
|
-
const agentDef = this.getAgent(agentName);
|
|
911
|
-
if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
|
|
912
|
-
return AgentInstance.spawn(agentDef, workingDirectory);
|
|
913
|
-
}
|
|
914
|
-
async resume(agentName, workingDirectory, agentSessionId) {
|
|
915
|
-
const agentDef = this.getAgent(agentName);
|
|
916
|
-
if (!agentDef) throw new Error(`Agent "${agentName}" is not installed. Run "openacp agents install ${agentName}" to add it.`);
|
|
917
|
-
return AgentInstance.resume(agentDef, workingDirectory, agentSessionId);
|
|
918
|
-
}
|
|
919
|
-
};
|
|
920
|
-
|
|
921
|
-
// src/core/sessions/prompt-queue.ts
|
|
922
|
-
var PromptQueue = class {
|
|
923
|
-
constructor(processor, onError) {
|
|
924
|
-
this.processor = processor;
|
|
925
|
-
this.onError = onError;
|
|
926
|
-
}
|
|
927
|
-
queue = [];
|
|
928
|
-
processing = false;
|
|
929
|
-
abortController = null;
|
|
930
|
-
async enqueue(text, attachments) {
|
|
931
|
-
if (this.processing) {
|
|
932
|
-
return new Promise((resolve) => {
|
|
933
|
-
this.queue.push({ text, attachments, resolve });
|
|
934
|
-
});
|
|
935
|
-
}
|
|
936
|
-
await this.process(text, attachments);
|
|
937
|
-
}
|
|
938
|
-
async process(text, attachments) {
|
|
939
|
-
this.processing = true;
|
|
940
|
-
this.abortController = new AbortController();
|
|
941
|
-
const { signal } = this.abortController;
|
|
942
|
-
try {
|
|
943
|
-
await Promise.race([
|
|
944
|
-
this.processor(text, attachments),
|
|
945
|
-
new Promise((_, reject) => {
|
|
946
|
-
signal.addEventListener("abort", () => reject(new Error("Prompt aborted")), { once: true });
|
|
947
|
-
})
|
|
948
|
-
]);
|
|
949
|
-
} catch (err) {
|
|
950
|
-
if (!(err instanceof Error && err.message === "Prompt aborted")) {
|
|
951
|
-
this.onError?.(err);
|
|
952
|
-
}
|
|
953
|
-
} finally {
|
|
954
|
-
this.abortController = null;
|
|
955
|
-
this.processing = false;
|
|
956
|
-
this.drainNext();
|
|
957
|
-
}
|
|
958
|
-
}
|
|
959
|
-
drainNext() {
|
|
960
|
-
const next = this.queue.shift();
|
|
961
|
-
if (next) {
|
|
962
|
-
this.process(next.text, next.attachments).then(next.resolve);
|
|
963
|
-
}
|
|
964
|
-
}
|
|
965
|
-
clear() {
|
|
966
|
-
if (this.abortController) {
|
|
967
|
-
this.abortController.abort();
|
|
968
|
-
}
|
|
969
|
-
for (const item of this.queue) {
|
|
970
|
-
item.resolve();
|
|
971
|
-
}
|
|
972
|
-
this.queue = [];
|
|
973
|
-
}
|
|
974
|
-
get pending() {
|
|
975
|
-
return this.queue.length;
|
|
976
|
-
}
|
|
977
|
-
get isProcessing() {
|
|
978
|
-
return this.processing;
|
|
979
|
-
}
|
|
980
|
-
};
|
|
981
|
-
|
|
982
|
-
// src/core/sessions/permission-gate.ts
|
|
983
|
-
var DEFAULT_TIMEOUT_MS = 10 * 60 * 1e3;
|
|
984
|
-
var PermissionGate = class {
|
|
985
|
-
request;
|
|
986
|
-
resolveFn;
|
|
987
|
-
rejectFn;
|
|
988
|
-
settled = false;
|
|
989
|
-
timeoutTimer;
|
|
990
|
-
timeoutMs;
|
|
991
|
-
constructor(timeoutMs) {
|
|
992
|
-
this.timeoutMs = timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
993
|
-
}
|
|
994
|
-
setPending(request) {
|
|
995
|
-
if (!this.settled && this.rejectFn) {
|
|
996
|
-
this.rejectFn(new Error("Superseded by new permission request"));
|
|
997
|
-
}
|
|
998
|
-
this.request = request;
|
|
999
|
-
this.settled = false;
|
|
1000
|
-
this.clearTimeout();
|
|
1001
|
-
return new Promise((resolve, reject) => {
|
|
1002
|
-
this.resolveFn = resolve;
|
|
1003
|
-
this.rejectFn = reject;
|
|
1004
|
-
this.timeoutTimer = setTimeout(() => {
|
|
1005
|
-
this.reject("Permission request timed out (no response received)");
|
|
1006
|
-
}, this.timeoutMs);
|
|
1007
|
-
if (typeof this.timeoutTimer === "object" && "unref" in this.timeoutTimer) {
|
|
1008
|
-
this.timeoutTimer.unref();
|
|
1009
|
-
}
|
|
1010
|
-
});
|
|
1011
|
-
}
|
|
1012
|
-
resolve(optionId) {
|
|
1013
|
-
if (this.settled || !this.resolveFn) return;
|
|
1014
|
-
this.settled = true;
|
|
1015
|
-
this.clearTimeout();
|
|
1016
|
-
this.resolveFn(optionId);
|
|
1017
|
-
this.cleanup();
|
|
1018
|
-
}
|
|
1019
|
-
reject(reason) {
|
|
1020
|
-
if (this.settled || !this.rejectFn) return;
|
|
1021
|
-
this.settled = true;
|
|
1022
|
-
this.clearTimeout();
|
|
1023
|
-
this.rejectFn(new Error(reason ?? "Permission rejected"));
|
|
1024
|
-
this.cleanup();
|
|
1025
|
-
}
|
|
1026
|
-
get isPending() {
|
|
1027
|
-
return !!this.request && !this.settled;
|
|
1028
|
-
}
|
|
1029
|
-
get currentRequest() {
|
|
1030
|
-
return this.isPending ? this.request : void 0;
|
|
1031
|
-
}
|
|
1032
|
-
/** The request ID of the current pending request, undefined after settlement */
|
|
1033
|
-
get requestId() {
|
|
1034
|
-
return this.request?.id;
|
|
1035
|
-
}
|
|
1036
|
-
clearTimeout() {
|
|
1037
|
-
if (this.timeoutTimer) {
|
|
1038
|
-
clearTimeout(this.timeoutTimer);
|
|
1039
|
-
this.timeoutTimer = void 0;
|
|
1040
|
-
}
|
|
1041
|
-
}
|
|
1042
|
-
cleanup() {
|
|
1043
|
-
this.request = void 0;
|
|
1044
|
-
this.resolveFn = void 0;
|
|
1045
|
-
this.rejectFn = void 0;
|
|
1046
|
-
}
|
|
1047
|
-
};
|
|
1048
|
-
|
|
1049
|
-
// src/core/sessions/session.ts
|
|
1050
|
-
import { nanoid } from "nanoid";
|
|
1051
|
-
import * as fs3 from "fs";
|
|
1052
|
-
var moduleLog = createChildLogger({ module: "session" });
|
|
1053
|
-
var TTS_PROMPT_INSTRUCTION = `
|
|
1054
|
-
|
|
1055
|
-
Additionally, include a [TTS]...[/TTS] block with a spoken-friendly summary of your response. Focus on key information, decisions the user needs to make, or actions required. The agent decides what to say and how long. Respond in the same language the user is using. This instruction applies to this message only.`;
|
|
1056
|
-
var TTS_BLOCK_REGEX = /\[TTS\]([\s\S]*?)\[\/TTS\]/;
|
|
1057
|
-
var TTS_MAX_LENGTH = 5e3;
|
|
1058
|
-
var TTS_TIMEOUT_MS = 3e4;
|
|
1059
|
-
var VALID_TRANSITIONS = {
|
|
1060
|
-
initializing: /* @__PURE__ */ new Set(["active", "error"]),
|
|
1061
|
-
active: /* @__PURE__ */ new Set(["error", "finished", "cancelled"]),
|
|
1062
|
-
error: /* @__PURE__ */ new Set(["active", "cancelled"]),
|
|
1063
|
-
cancelled: /* @__PURE__ */ new Set(["active"]),
|
|
1064
|
-
finished: /* @__PURE__ */ new Set()
|
|
1065
|
-
};
|
|
1066
|
-
var Session = class extends TypedEmitter {
|
|
1067
|
-
id;
|
|
1068
|
-
channelId;
|
|
1069
|
-
threadId = "";
|
|
1070
|
-
agentName;
|
|
1071
|
-
workingDirectory;
|
|
1072
|
-
agentInstance;
|
|
1073
|
-
agentSessionId = "";
|
|
1074
|
-
_status = "initializing";
|
|
1075
|
-
name;
|
|
1076
|
-
createdAt = /* @__PURE__ */ new Date();
|
|
1077
|
-
voiceMode = "off";
|
|
1078
|
-
dangerousMode = false;
|
|
1079
|
-
currentMode;
|
|
1080
|
-
availableModes = [];
|
|
1081
|
-
configOptions = [];
|
|
1082
|
-
currentModel;
|
|
1083
|
-
availableModels = [];
|
|
1084
|
-
agentCapabilities;
|
|
1085
|
-
archiving = false;
|
|
1086
|
-
promptCount = 0;
|
|
1087
|
-
firstAgent;
|
|
1088
|
-
agentSwitchHistory = [];
|
|
1089
|
-
log;
|
|
1090
|
-
middlewareChain;
|
|
1091
|
-
permissionGate = new PermissionGate();
|
|
1092
|
-
queue;
|
|
1093
|
-
speechService;
|
|
1094
|
-
pendingContext = null;
|
|
1095
|
-
constructor(opts) {
|
|
1096
|
-
super();
|
|
1097
|
-
this.id = opts.id || nanoid(12);
|
|
1098
|
-
this.channelId = opts.channelId;
|
|
1099
|
-
this.agentName = opts.agentName;
|
|
1100
|
-
this.firstAgent = opts.agentName;
|
|
1101
|
-
this.workingDirectory = opts.workingDirectory;
|
|
1102
|
-
this.agentInstance = opts.agentInstance;
|
|
1103
|
-
this.speechService = opts.speechService;
|
|
1104
|
-
this.log = createSessionLogger(this.id, moduleLog);
|
|
1105
|
-
this.log.info({ agentName: this.agentName }, "Session created");
|
|
1106
|
-
this.queue = new PromptQueue(
|
|
1107
|
-
(text, attachments) => this.processPrompt(text, attachments),
|
|
1108
|
-
(err) => {
|
|
1109
|
-
this.fail("Prompt execution failed");
|
|
1110
|
-
this.log.error({ err }, "Prompt execution failed");
|
|
1111
|
-
}
|
|
1112
|
-
);
|
|
1113
|
-
}
|
|
1114
|
-
// --- State Machine ---
|
|
1115
|
-
get status() {
|
|
1116
|
-
return this._status;
|
|
1117
|
-
}
|
|
1118
|
-
/** Transition to active — from initializing, error, or cancelled */
|
|
1119
|
-
activate() {
|
|
1120
|
-
this.transition("active");
|
|
1121
|
-
}
|
|
1122
|
-
/** Transition to error — from initializing or active */
|
|
1123
|
-
fail(reason) {
|
|
1124
|
-
this.transition("error");
|
|
1125
|
-
this.emit("error", new Error(reason));
|
|
1126
|
-
}
|
|
1127
|
-
/** Transition to finished — from active only. Emits session_end for backward compat. */
|
|
1128
|
-
finish(reason) {
|
|
1129
|
-
this.transition("finished");
|
|
1130
|
-
this.emit("session_end", reason ?? "completed");
|
|
1131
|
-
}
|
|
1132
|
-
/** Transition to cancelled — from active only (terminal session cancel) */
|
|
1133
|
-
markCancelled() {
|
|
1134
|
-
this.transition("cancelled");
|
|
1135
|
-
}
|
|
1136
|
-
transition(to) {
|
|
1137
|
-
const from = this._status;
|
|
1138
|
-
const allowed = VALID_TRANSITIONS[from];
|
|
1139
|
-
if (!allowed?.has(to)) {
|
|
1140
|
-
throw new Error(
|
|
1141
|
-
`Invalid session transition: ${from} \u2192 ${to}`
|
|
1142
|
-
);
|
|
1143
|
-
}
|
|
1144
|
-
this._status = to;
|
|
1145
|
-
this.log.debug({ from, to }, "Session status transition");
|
|
1146
|
-
this.emit("status_change", from, to);
|
|
1147
|
-
}
|
|
1148
|
-
/** Number of prompts waiting in queue */
|
|
1149
|
-
get queueDepth() {
|
|
1150
|
-
return this.queue.pending;
|
|
1151
|
-
}
|
|
1152
|
-
get promptRunning() {
|
|
1153
|
-
return this.queue.isProcessing;
|
|
1154
|
-
}
|
|
1155
|
-
// --- Context Injection ---
|
|
1156
|
-
setContext(markdown) {
|
|
1157
|
-
this.pendingContext = markdown;
|
|
1158
|
-
}
|
|
1159
|
-
// --- Voice Mode ---
|
|
1160
|
-
setVoiceMode(mode) {
|
|
1161
|
-
this.voiceMode = mode;
|
|
1162
|
-
this.log.info({ voiceMode: mode }, "TTS mode changed");
|
|
1163
|
-
}
|
|
1164
|
-
// --- Public API ---
|
|
1165
|
-
async enqueuePrompt(text, attachments) {
|
|
1166
|
-
if (this.middlewareChain) {
|
|
1167
|
-
const payload = { text, attachments, sessionId: this.id };
|
|
1168
|
-
const result = await this.middlewareChain.execute("agent:beforePrompt", payload, async (p) => p);
|
|
1169
|
-
if (!result) return;
|
|
1170
|
-
text = result.text;
|
|
1171
|
-
attachments = result.attachments;
|
|
1172
|
-
}
|
|
1173
|
-
await this.queue.enqueue(text, attachments);
|
|
1174
|
-
}
|
|
1175
|
-
async processPrompt(text, attachments) {
|
|
1176
|
-
if (text === "\0__warmup__") {
|
|
1177
|
-
await this.runWarmup();
|
|
1178
|
-
return;
|
|
1179
|
-
}
|
|
1180
|
-
if (this._status === "finished") return;
|
|
1181
|
-
this.promptCount++;
|
|
1182
|
-
this.emit("prompt_count_changed", this.promptCount);
|
|
1183
|
-
if (this._status === "initializing" || this._status === "cancelled" || this._status === "error") {
|
|
1184
|
-
this.activate();
|
|
1185
|
-
}
|
|
1186
|
-
const promptStart = Date.now();
|
|
1187
|
-
this.log.debug("Prompt execution started");
|
|
1188
|
-
const contextUsed = this.pendingContext;
|
|
1189
|
-
if (contextUsed) {
|
|
1190
|
-
text = `[CONVERSATION HISTORY - This is context from previous sessions, not current conversation]
|
|
1191
|
-
|
|
1192
|
-
${contextUsed}
|
|
1193
|
-
|
|
1194
|
-
[END CONVERSATION HISTORY]
|
|
1195
|
-
|
|
1196
|
-
${text}`;
|
|
1197
|
-
this.log.debug("Context injected into prompt");
|
|
1198
|
-
}
|
|
1199
|
-
const processed = await this.maybeTranscribeAudio(text, attachments);
|
|
1200
|
-
const ttsActive = this.voiceMode !== "off" && !!this.speechService?.isTTSAvailable();
|
|
1201
|
-
if (ttsActive) {
|
|
1202
|
-
processed.text += TTS_PROMPT_INSTRUCTION;
|
|
1203
|
-
}
|
|
1204
|
-
let accumulatedText = "";
|
|
1205
|
-
const accumulatorListener = ttsActive ? (event) => {
|
|
1206
|
-
if (event.type === "text") {
|
|
1207
|
-
accumulatedText += event.content;
|
|
1208
|
-
}
|
|
1209
|
-
} : null;
|
|
1210
|
-
if (accumulatorListener) {
|
|
1211
|
-
this.on("agent_event", accumulatorListener);
|
|
1212
|
-
}
|
|
1213
|
-
if (this.middlewareChain) {
|
|
1214
|
-
this.middlewareChain.execute("turn:start", { sessionId: this.id, promptText: processed.text, promptNumber: this.promptCount }, async (p) => p).catch(() => {
|
|
1215
|
-
});
|
|
1216
|
-
}
|
|
1217
|
-
let stopReason = "end_turn";
|
|
1218
|
-
try {
|
|
1219
|
-
const response = await this.agentInstance.prompt(processed.text, processed.attachments);
|
|
1220
|
-
if (response && typeof response === "object" && "stopReason" in response) {
|
|
1221
|
-
stopReason = response.stopReason ?? "end_turn";
|
|
1222
|
-
}
|
|
1223
|
-
if (contextUsed) {
|
|
1224
|
-
this.pendingContext = null;
|
|
1225
|
-
}
|
|
1226
|
-
if (ttsActive && this.voiceMode === "next") {
|
|
1227
|
-
this.voiceMode = "off";
|
|
1228
|
-
}
|
|
1229
|
-
} finally {
|
|
1230
|
-
if (accumulatorListener) {
|
|
1231
|
-
this.off("agent_event", accumulatorListener);
|
|
1232
|
-
}
|
|
1233
|
-
}
|
|
1234
|
-
if (this.middlewareChain) {
|
|
1235
|
-
this.middlewareChain.execute("turn:end", { sessionId: this.id, stopReason, durationMs: Date.now() - promptStart }, async (p) => p).catch(() => {
|
|
1236
|
-
});
|
|
1237
|
-
}
|
|
1238
|
-
this.log.info(
|
|
1239
|
-
{ durationMs: Date.now() - promptStart },
|
|
1240
|
-
"Prompt execution completed"
|
|
1241
|
-
);
|
|
1242
|
-
if (ttsActive && accumulatedText) {
|
|
1243
|
-
this.processTTSResponse(accumulatedText).catch((err) => {
|
|
1244
|
-
this.log.warn({ err }, "TTS post-processing failed");
|
|
1245
|
-
});
|
|
1246
|
-
}
|
|
1247
|
-
if (!this.name) {
|
|
1248
|
-
await this.autoName();
|
|
1249
|
-
}
|
|
1250
|
-
}
|
|
1251
|
-
async maybeTranscribeAudio(text, attachments) {
|
|
1252
|
-
if (!attachments?.length || !this.speechService) {
|
|
1253
|
-
return { text, attachments };
|
|
1254
|
-
}
|
|
1255
|
-
const hasAudioCapability = this.agentInstance.promptCapabilities?.audio === true;
|
|
1256
|
-
if (hasAudioCapability) {
|
|
1257
|
-
return { text, attachments };
|
|
1258
|
-
}
|
|
1259
|
-
if (!this.speechService.isSTTAvailable()) {
|
|
1260
|
-
return { text, attachments };
|
|
1261
|
-
}
|
|
1262
|
-
let transcribedText = text;
|
|
1263
|
-
const remainingAttachments = [];
|
|
1264
|
-
for (const att of attachments) {
|
|
1265
|
-
if (att.type !== "audio") {
|
|
1266
|
-
remainingAttachments.push(att);
|
|
1267
|
-
continue;
|
|
1268
|
-
}
|
|
1269
|
-
try {
|
|
1270
|
-
const audioPath = att.originalFilePath || att.filePath;
|
|
1271
|
-
const audioMime = att.originalFilePath ? "audio/ogg" : att.mimeType;
|
|
1272
|
-
const audioBuffer = await fs3.promises.readFile(audioPath);
|
|
1273
|
-
const result = await this.speechService.transcribe(audioBuffer, audioMime);
|
|
1274
|
-
this.log.info({ provider: "stt", duration: result.duration }, "Voice transcribed");
|
|
1275
|
-
this.emit("agent_event", {
|
|
1276
|
-
type: "system_message",
|
|
1277
|
-
message: `\u{1F3A4} You said: ${result.text}`
|
|
1278
|
-
});
|
|
1279
|
-
transcribedText = transcribedText.replace(/\[Audio:\s*[^\]]*\]\s*/g, "").trim();
|
|
1280
|
-
transcribedText = transcribedText ? `${transcribedText}
|
|
1281
|
-
${result.text}` : result.text;
|
|
1282
|
-
} catch (err) {
|
|
1283
|
-
this.log.warn({ err }, "STT transcription failed, keeping audio attachment");
|
|
1284
|
-
this.emit("agent_event", {
|
|
1285
|
-
type: "error",
|
|
1286
|
-
message: `Voice transcription failed: ${err.message}`
|
|
1287
|
-
});
|
|
1288
|
-
remainingAttachments.push(att);
|
|
1289
|
-
}
|
|
1290
|
-
}
|
|
1291
|
-
return {
|
|
1292
|
-
text: transcribedText,
|
|
1293
|
-
attachments: remainingAttachments.length > 0 ? remainingAttachments : void 0
|
|
1294
|
-
};
|
|
1295
|
-
}
|
|
1296
|
-
async processTTSResponse(responseText) {
|
|
1297
|
-
const match = TTS_BLOCK_REGEX.exec(responseText);
|
|
1298
|
-
if (!match?.[1]) {
|
|
1299
|
-
this.log.debug("No [TTS] block found in response, skipping synthesis");
|
|
1300
|
-
return;
|
|
1301
|
-
}
|
|
1302
|
-
let ttsText = match[1].trim();
|
|
1303
|
-
if (!ttsText) return;
|
|
1304
|
-
if (ttsText.length > TTS_MAX_LENGTH) {
|
|
1305
|
-
ttsText = ttsText.slice(0, TTS_MAX_LENGTH);
|
|
1306
|
-
}
|
|
1307
|
-
try {
|
|
1308
|
-
let ttsTimer;
|
|
1309
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
1310
|
-
ttsTimer = setTimeout(() => reject(new Error("TTS synthesis timed out")), TTS_TIMEOUT_MS);
|
|
1311
|
-
});
|
|
1312
|
-
try {
|
|
1313
|
-
const result = await Promise.race([
|
|
1314
|
-
this.speechService.synthesize(ttsText),
|
|
1315
|
-
timeoutPromise
|
|
1316
|
-
]);
|
|
1317
|
-
const base64 = result.audioBuffer.toString("base64");
|
|
1318
|
-
this.emit("agent_event", {
|
|
1319
|
-
type: "audio_content",
|
|
1320
|
-
data: base64,
|
|
1321
|
-
mimeType: result.mimeType
|
|
1322
|
-
});
|
|
1323
|
-
this.emit("agent_event", { type: "tts_strip" });
|
|
1324
|
-
this.log.info("TTS synthesis completed");
|
|
1325
|
-
} finally {
|
|
1326
|
-
clearTimeout(ttsTimer);
|
|
1327
|
-
}
|
|
1328
|
-
} catch (err) {
|
|
1329
|
-
this.log.warn({ err }, "TTS synthesis failed, skipping");
|
|
1330
|
-
}
|
|
1331
|
-
}
|
|
1332
|
-
// NOTE: This injects a summary prompt into the agent's conversation history.
|
|
1333
|
-
async autoName() {
|
|
1334
|
-
let title = "";
|
|
1335
|
-
const captureHandler = (event) => {
|
|
1336
|
-
if (event.type === "text") title += event.content;
|
|
1337
|
-
};
|
|
1338
|
-
this.pause((event) => event !== "agent_event");
|
|
1339
|
-
this.agentInstance.on("agent_event", captureHandler);
|
|
1340
|
-
try {
|
|
1341
|
-
await this.agentInstance.prompt(
|
|
1342
|
-
"Summarize this conversation in max 5 words for a topic title. Reply ONLY with the title, nothing else."
|
|
1343
|
-
);
|
|
1344
|
-
this.name = title.trim().slice(0, 50) || `Session ${this.id.slice(0, 6)}`;
|
|
1345
|
-
this.log.info({ name: this.name }, "Session auto-named");
|
|
1346
|
-
this.emit("named", this.name);
|
|
1347
|
-
} catch {
|
|
1348
|
-
this.name = `Session ${this.id.slice(0, 6)}`;
|
|
1349
|
-
} finally {
|
|
1350
|
-
this.agentInstance.off("agent_event", captureHandler);
|
|
1351
|
-
this.clearBuffer();
|
|
1352
|
-
this.resume();
|
|
1353
|
-
}
|
|
1354
|
-
}
|
|
1355
|
-
/** Fire-and-forget warm-up: primes model cache while user types their first message */
|
|
1356
|
-
async warmup() {
|
|
1357
|
-
await this.queue.enqueue("\0__warmup__");
|
|
1358
|
-
}
|
|
1359
|
-
async runWarmup() {
|
|
1360
|
-
this.pause((_event, args) => {
|
|
1361
|
-
const agentEvent = args[0];
|
|
1362
|
-
return agentEvent?.type === "commands_update";
|
|
1363
|
-
});
|
|
1364
|
-
try {
|
|
1365
|
-
const start = Date.now();
|
|
1366
|
-
await this.agentInstance.prompt('Reply with only "ready".');
|
|
1367
|
-
this.activate();
|
|
1368
|
-
this.log.info({ durationMs: Date.now() - start }, "Warm-up complete");
|
|
1369
|
-
} catch (err) {
|
|
1370
|
-
this.log.error({ err }, "Warm-up failed");
|
|
1371
|
-
} finally {
|
|
1372
|
-
this.clearBuffer();
|
|
1373
|
-
this.resume();
|
|
1374
|
-
}
|
|
1375
|
-
}
|
|
1376
|
-
// --- ACP Mode / Config / Model State ---
|
|
1377
|
-
setInitialAcpState(state) {
|
|
1378
|
-
if (state.modes) {
|
|
1379
|
-
this.currentMode = state.modes.currentModeId;
|
|
1380
|
-
this.availableModes = state.modes.availableModes;
|
|
1381
|
-
}
|
|
1382
|
-
if (state.configOptions) {
|
|
1383
|
-
this.configOptions = state.configOptions;
|
|
1384
|
-
}
|
|
1385
|
-
if (state.models) {
|
|
1386
|
-
this.currentModel = state.models.currentModelId;
|
|
1387
|
-
this.availableModels = state.models.availableModels;
|
|
1388
|
-
}
|
|
1389
|
-
if (state.agentCapabilities) {
|
|
1390
|
-
this.agentCapabilities = state.agentCapabilities;
|
|
1391
|
-
}
|
|
1392
|
-
}
|
|
1393
|
-
/** Set session name explicitly and emit 'named' event */
|
|
1394
|
-
setName(name) {
|
|
1395
|
-
this.name = name;
|
|
1396
|
-
this.emit("named", name);
|
|
1397
|
-
}
|
|
1398
|
-
async updateMode(modeId) {
|
|
1399
|
-
if (this.middlewareChain) {
|
|
1400
|
-
const result = await this.middlewareChain.execute("mode:beforeChange", { sessionId: this.id, fromMode: this.currentMode, toMode: modeId }, async (p) => p);
|
|
1401
|
-
if (!result) return;
|
|
1402
|
-
}
|
|
1403
|
-
this.currentMode = modeId;
|
|
1404
|
-
}
|
|
1405
|
-
async updateConfigOptions(options) {
|
|
1406
|
-
if (this.middlewareChain) {
|
|
1407
|
-
const result = await this.middlewareChain.execute("config:beforeChange", { sessionId: this.id, configId: "options", oldValue: this.configOptions, newValue: options }, async (p) => p);
|
|
1408
|
-
if (!result) return;
|
|
1409
|
-
}
|
|
1410
|
-
this.configOptions = options;
|
|
1411
|
-
}
|
|
1412
|
-
async updateModel(modelId) {
|
|
1413
|
-
if (this.middlewareChain) {
|
|
1414
|
-
const result = await this.middlewareChain.execute("model:beforeChange", { sessionId: this.id, fromModel: this.currentModel, toModel: modelId }, async (p) => p);
|
|
1415
|
-
if (!result) return;
|
|
1416
|
-
}
|
|
1417
|
-
this.currentModel = modelId;
|
|
1418
|
-
}
|
|
1419
|
-
/** Cancel the current prompt and clear the queue. Stays in active state. */
|
|
1420
|
-
async abortPrompt() {
|
|
1421
|
-
if (this.middlewareChain) {
|
|
1422
|
-
const result = await this.middlewareChain.execute("agent:beforeCancel", { sessionId: this.id }, async (p) => p);
|
|
1423
|
-
if (!result) return;
|
|
1424
|
-
}
|
|
1425
|
-
this.queue.clear();
|
|
1426
|
-
this.log.info("Prompt aborted");
|
|
1427
|
-
await this.agentInstance.cancel();
|
|
1428
|
-
}
|
|
1429
|
-
/** Search backward through agentSwitchHistory for the last entry matching agentName */
|
|
1430
|
-
findLastSwitchEntry(agentName) {
|
|
1431
|
-
for (let i = this.agentSwitchHistory.length - 1; i >= 0; i--) {
|
|
1432
|
-
if (this.agentSwitchHistory[i].agentName === agentName) {
|
|
1433
|
-
return this.agentSwitchHistory[i];
|
|
1434
|
-
}
|
|
1435
|
-
}
|
|
1436
|
-
return void 0;
|
|
1437
|
-
}
|
|
1438
|
-
/** Switch the agent instance in-place, preserving session identity */
|
|
1439
|
-
async switchAgent(agentName, createAgent) {
|
|
1440
|
-
if (agentName === this.agentName) {
|
|
1441
|
-
throw new Error(`Already using ${agentName}`);
|
|
1442
|
-
}
|
|
1443
|
-
this.agentSwitchHistory.push({
|
|
1444
|
-
agentName: this.agentName,
|
|
1445
|
-
agentSessionId: this.agentSessionId,
|
|
1446
|
-
switchedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1447
|
-
promptCount: this.promptCount
|
|
1448
|
-
});
|
|
1449
|
-
this.queue.clear();
|
|
1450
|
-
if (this.permissionGate.isPending) {
|
|
1451
|
-
this.permissionGate.reject("Agent switched");
|
|
1452
|
-
}
|
|
1453
|
-
await this.agentInstance.destroy();
|
|
1454
|
-
const newAgent = await createAgent();
|
|
1455
|
-
this.agentInstance = newAgent;
|
|
1456
|
-
this.agentName = agentName;
|
|
1457
|
-
this.agentSessionId = newAgent.sessionId;
|
|
1458
|
-
this.promptCount = 0;
|
|
1459
|
-
this.agentCapabilities = void 0;
|
|
1460
|
-
this.currentMode = void 0;
|
|
1461
|
-
this.availableModes = [];
|
|
1462
|
-
this.currentModel = void 0;
|
|
1463
|
-
this.availableModels = [];
|
|
1464
|
-
this.configOptions = [];
|
|
1465
|
-
this.log.info({ from: this.agentSwitchHistory.at(-1).agentName, to: agentName }, "Agent switched");
|
|
1466
|
-
}
|
|
1467
|
-
async destroy() {
|
|
1468
|
-
this.log.info("Session destroyed");
|
|
1469
|
-
if (this.permissionGate.isPending) {
|
|
1470
|
-
this.permissionGate.reject("Session destroyed");
|
|
1471
|
-
}
|
|
1472
|
-
this.queue.clear();
|
|
1473
|
-
await this.agentInstance.destroy();
|
|
1474
|
-
closeSessionLogger(this.log);
|
|
1475
|
-
}
|
|
1476
|
-
};
|
|
1477
|
-
|
|
1478
|
-
// src/core/sessions/session-manager.ts
|
|
1479
|
-
var SessionManager = class {
|
|
1480
|
-
sessions = /* @__PURE__ */ new Map();
|
|
1481
|
-
store;
|
|
1482
|
-
eventBus;
|
|
1483
|
-
middlewareChain;
|
|
1484
|
-
setEventBus(eventBus) {
|
|
1485
|
-
this.eventBus = eventBus;
|
|
1486
|
-
}
|
|
1487
|
-
constructor(store = null) {
|
|
1488
|
-
this.store = store;
|
|
1489
|
-
}
|
|
1490
|
-
async createSession(channelId, agentName, workingDirectory, agentManager) {
|
|
1491
|
-
const agentInstance = await agentManager.spawn(agentName, workingDirectory);
|
|
1492
|
-
const session = new Session({
|
|
1493
|
-
channelId,
|
|
1494
|
-
agentName,
|
|
1495
|
-
workingDirectory,
|
|
1496
|
-
agentInstance
|
|
1497
|
-
});
|
|
1498
|
-
this.sessions.set(session.id, session);
|
|
1499
|
-
session.agentSessionId = session.agentInstance.sessionId;
|
|
1500
|
-
if (this.store) {
|
|
1501
|
-
await this.store.save({
|
|
1502
|
-
sessionId: session.id,
|
|
1503
|
-
agentSessionId: session.agentInstance.sessionId,
|
|
1504
|
-
agentName: session.agentName,
|
|
1505
|
-
workingDir: session.workingDirectory,
|
|
1506
|
-
channelId,
|
|
1507
|
-
status: session.status,
|
|
1508
|
-
createdAt: session.createdAt.toISOString(),
|
|
1509
|
-
lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
1510
|
-
name: session.name,
|
|
1511
|
-
dangerousMode: false,
|
|
1512
|
-
platform: {}
|
|
1513
|
-
});
|
|
1514
|
-
}
|
|
1515
|
-
return session;
|
|
1516
|
-
}
|
|
1517
|
-
getSession(sessionId) {
|
|
1518
|
-
return this.sessions.get(sessionId);
|
|
1519
|
-
}
|
|
1520
|
-
getSessionByThread(channelId, threadId) {
|
|
1521
|
-
for (const session of this.sessions.values()) {
|
|
1522
|
-
if (session.channelId === channelId && session.threadId === threadId) {
|
|
1523
|
-
return session;
|
|
1524
|
-
}
|
|
1525
|
-
}
|
|
1526
|
-
return void 0;
|
|
1527
|
-
}
|
|
1528
|
-
getSessionByAgentSessionId(agentSessionId) {
|
|
1529
|
-
for (const session of this.sessions.values()) {
|
|
1530
|
-
if (session.agentSessionId === agentSessionId) {
|
|
1531
|
-
return session;
|
|
1532
|
-
}
|
|
1533
|
-
}
|
|
1534
|
-
return void 0;
|
|
1535
|
-
}
|
|
1536
|
-
getRecordByAgentSessionId(agentSessionId) {
|
|
1537
|
-
return this.store?.findByAgentSessionId(agentSessionId);
|
|
1538
|
-
}
|
|
1539
|
-
getRecordByThread(channelId, threadId) {
|
|
1540
|
-
return this.store?.findByPlatform(
|
|
1541
|
-
channelId,
|
|
1542
|
-
(p) => String(p.topicId) === threadId || p.threadId === threadId
|
|
1543
|
-
);
|
|
1544
|
-
}
|
|
1545
|
-
registerSession(session) {
|
|
1546
|
-
this.sessions.set(session.id, session);
|
|
1547
|
-
}
|
|
1548
|
-
async patchRecord(sessionId, patch) {
|
|
1549
|
-
if (!this.store) return;
|
|
1550
|
-
const record = this.store.get(sessionId);
|
|
1551
|
-
if (record) {
|
|
1552
|
-
await this.store.save({ ...record, ...patch });
|
|
1553
|
-
} else if (patch.sessionId) {
|
|
1554
|
-
await this.store.save(patch);
|
|
1555
|
-
}
|
|
1556
|
-
}
|
|
1557
|
-
getSessionRecord(sessionId) {
|
|
1558
|
-
return this.store?.get(sessionId);
|
|
1559
|
-
}
|
|
1560
|
-
async cancelSession(sessionId) {
|
|
1561
|
-
const session = this.sessions.get(sessionId);
|
|
1562
|
-
if (session) {
|
|
1563
|
-
try {
|
|
1564
|
-
await session.abortPrompt();
|
|
1565
|
-
} catch {
|
|
1566
|
-
}
|
|
1567
|
-
session.markCancelled();
|
|
1568
|
-
this.sessions.delete(sessionId);
|
|
1569
|
-
}
|
|
1570
|
-
if (this.store) {
|
|
1571
|
-
const record = this.store.get(sessionId);
|
|
1572
|
-
if (record && record.status !== "cancelled") {
|
|
1573
|
-
await this.store.save({ ...record, status: "cancelled" });
|
|
1574
|
-
}
|
|
1575
|
-
}
|
|
1576
|
-
if (this.middlewareChain) {
|
|
1577
|
-
this.middlewareChain.execute("session:afterDestroy", { sessionId }, async (p) => p).catch(() => {
|
|
1578
|
-
});
|
|
1579
|
-
}
|
|
1580
|
-
}
|
|
1581
|
-
listSessions(channelId) {
|
|
1582
|
-
const all = Array.from(this.sessions.values());
|
|
1583
|
-
if (channelId) return all.filter((s) => s.channelId === channelId);
|
|
1584
|
-
return all;
|
|
1585
|
-
}
|
|
1586
|
-
listRecords(filter) {
|
|
1587
|
-
if (!this.store) return [];
|
|
1588
|
-
let records = this.store.list();
|
|
1589
|
-
if (filter?.statuses?.length) {
|
|
1590
|
-
records = records.filter((r) => filter.statuses.includes(r.status));
|
|
1591
|
-
}
|
|
1592
|
-
return records;
|
|
1593
|
-
}
|
|
1594
|
-
async removeRecord(sessionId) {
|
|
1595
|
-
if (!this.store) return;
|
|
1596
|
-
await this.store.remove(sessionId);
|
|
1597
|
-
this.eventBus?.emit("session:deleted", { sessionId });
|
|
1598
|
-
}
|
|
1599
|
-
/**
|
|
1600
|
-
* Graceful shutdown: persist session state without killing agent subprocesses.
|
|
1601
|
-
* Agent processes will exit naturally when the parent process terminates.
|
|
1602
|
-
*/
|
|
1603
|
-
async shutdownAll() {
|
|
1604
|
-
if (this.store) {
|
|
1605
|
-
for (const session of this.sessions.values()) {
|
|
1606
|
-
const record = this.store.get(session.id);
|
|
1607
|
-
if (record) {
|
|
1608
|
-
await this.store.save({ ...record, status: "finished" });
|
|
1609
|
-
}
|
|
1610
|
-
}
|
|
1611
|
-
}
|
|
1612
|
-
this.sessions.clear();
|
|
1613
|
-
}
|
|
1614
|
-
/**
|
|
1615
|
-
* Forcefully destroy all sessions (kill agent subprocesses).
|
|
1616
|
-
* Use only when sessions must be fully torn down (e.g. archive).
|
|
1617
|
-
*/
|
|
1618
|
-
async destroyAll() {
|
|
1619
|
-
if (this.store) {
|
|
1620
|
-
for (const session of this.sessions.values()) {
|
|
1621
|
-
const record = this.store.get(session.id);
|
|
1622
|
-
if (record) {
|
|
1623
|
-
await this.store.save({ ...record, status: "finished" });
|
|
1624
|
-
}
|
|
1625
|
-
}
|
|
1626
|
-
}
|
|
1627
|
-
const sessionIds = [...this.sessions.keys()];
|
|
1628
|
-
for (const session of this.sessions.values()) {
|
|
1629
|
-
await session.destroy();
|
|
1630
|
-
}
|
|
1631
|
-
this.sessions.clear();
|
|
1632
|
-
if (this.middlewareChain) {
|
|
1633
|
-
for (const sessionId of sessionIds) {
|
|
1634
|
-
this.middlewareChain.execute("session:afterDestroy", { sessionId }, async (p) => p).catch(() => {
|
|
1635
|
-
});
|
|
1636
|
-
}
|
|
1637
|
-
}
|
|
1638
|
-
}
|
|
1639
|
-
};
|
|
1640
|
-
|
|
1641
|
-
// src/core/sessions/session-bridge.ts
|
|
1642
|
-
var log2 = createChildLogger({ module: "session-bridge" });
|
|
1643
|
-
var SessionBridge = class {
|
|
1644
|
-
constructor(session, adapter, deps) {
|
|
1645
|
-
this.session = session;
|
|
1646
|
-
this.adapter = adapter;
|
|
1647
|
-
this.deps = deps;
|
|
1648
|
-
}
|
|
1649
|
-
connected = false;
|
|
1650
|
-
agentEventHandler;
|
|
1651
|
-
sessionEventHandler;
|
|
1652
|
-
statusChangeHandler;
|
|
1653
|
-
namedHandler;
|
|
1654
|
-
promptCountHandler;
|
|
1655
|
-
get tracer() {
|
|
1656
|
-
return this.session.agentInstance.debugTracer ?? null;
|
|
1657
|
-
}
|
|
1658
|
-
/** Send message to adapter, optionally running through message:outgoing middleware */
|
|
1659
|
-
async sendMessage(sessionId, message) {
|
|
1660
|
-
try {
|
|
1661
|
-
const mw = this.deps.middlewareChain;
|
|
1662
|
-
if (mw) {
|
|
1663
|
-
const result = await mw.execute("message:outgoing", { sessionId, message }, async (m) => m);
|
|
1664
|
-
this.tracer?.log("core", { step: "middleware:outgoing", sessionId, hook: "message:outgoing", blocked: !result });
|
|
1665
|
-
if (!result) return;
|
|
1666
|
-
this.tracer?.log("core", { step: "dispatch", sessionId, message: result.message });
|
|
1667
|
-
this.adapter.sendMessage(sessionId, result.message).catch((err) => {
|
|
1668
|
-
log2.error({ err, sessionId }, "Failed to send message to adapter");
|
|
1669
|
-
});
|
|
1670
|
-
} else {
|
|
1671
|
-
this.tracer?.log("core", { step: "dispatch", sessionId, message });
|
|
1672
|
-
this.adapter.sendMessage(sessionId, message).catch((err) => {
|
|
1673
|
-
log2.error({ err, sessionId }, "Failed to send message to adapter");
|
|
1674
|
-
});
|
|
1675
|
-
}
|
|
1676
|
-
} catch (err) {
|
|
1677
|
-
log2.error({ err, sessionId }, "Error in sendMessage middleware");
|
|
1678
|
-
}
|
|
1679
|
-
}
|
|
1680
|
-
connect() {
|
|
1681
|
-
if (this.connected) return;
|
|
1682
|
-
this.connected = true;
|
|
1683
|
-
this.wireAgentToSession();
|
|
1684
|
-
this.wireSessionToAdapter();
|
|
1685
|
-
this.wirePermissions();
|
|
1686
|
-
this.wireLifecycle();
|
|
1687
|
-
}
|
|
1688
|
-
disconnect() {
|
|
1689
|
-
if (!this.connected) return;
|
|
1690
|
-
this.connected = false;
|
|
1691
|
-
if (this.agentEventHandler) {
|
|
1692
|
-
this.session.agentInstance.off("agent_event", this.agentEventHandler);
|
|
1693
|
-
}
|
|
1694
|
-
if (this.sessionEventHandler) {
|
|
1695
|
-
this.session.off("agent_event", this.sessionEventHandler);
|
|
1696
|
-
}
|
|
1697
|
-
if (this.statusChangeHandler) {
|
|
1698
|
-
this.session.off("status_change", this.statusChangeHandler);
|
|
1699
|
-
}
|
|
1700
|
-
if (this.namedHandler) {
|
|
1701
|
-
this.session.off("named", this.namedHandler);
|
|
1702
|
-
}
|
|
1703
|
-
if (this.promptCountHandler) {
|
|
1704
|
-
this.session.off("prompt_count_changed", this.promptCountHandler);
|
|
1705
|
-
}
|
|
1706
|
-
this.session.agentInstance.onPermissionRequest = async () => "";
|
|
1707
|
-
}
|
|
1708
|
-
wireAgentToSession() {
|
|
1709
|
-
this.agentEventHandler = (event) => {
|
|
1710
|
-
this.session.emit("agent_event", event);
|
|
1711
|
-
};
|
|
1712
|
-
this.session.agentInstance.on("agent_event", this.agentEventHandler);
|
|
1713
|
-
}
|
|
1714
|
-
wireSessionToAdapter() {
|
|
1715
|
-
this.sessionEventHandler = (event) => {
|
|
1716
|
-
this.tracer?.log("core", { step: "agent_event", sessionId: this.session.id, event });
|
|
1717
|
-
const mw = this.deps.middlewareChain;
|
|
1718
|
-
if (mw) {
|
|
1719
|
-
mw.execute("agent:beforeEvent", { sessionId: this.session.id, event }, async (e) => e).then((result) => {
|
|
1720
|
-
this.tracer?.log("core", { step: "middleware:before", sessionId: this.session.id, hook: "agent:beforeEvent", blocked: !result });
|
|
1721
|
-
if (!result) return;
|
|
1722
|
-
try {
|
|
1723
|
-
const transformedEvent = result.event;
|
|
1724
|
-
const outgoing = this.handleAgentEvent(transformedEvent);
|
|
1725
|
-
mw.execute("agent:afterEvent", {
|
|
1726
|
-
sessionId: this.session.id,
|
|
1727
|
-
event: transformedEvent,
|
|
1728
|
-
outgoingMessage: outgoing ?? { type: "text", text: "" }
|
|
1729
|
-
}, async (e) => e).catch(() => {
|
|
1730
|
-
});
|
|
1731
|
-
} catch (err) {
|
|
1732
|
-
log2.error({ err, sessionId: this.session.id }, "Error handling agent event after middleware");
|
|
1733
|
-
}
|
|
1734
|
-
}).catch(() => {
|
|
1735
|
-
try {
|
|
1736
|
-
this.handleAgentEvent(event);
|
|
1737
|
-
} catch (err) {
|
|
1738
|
-
log2.error({ err, sessionId: this.session.id }, "Error handling agent event (middleware fallback)");
|
|
1739
|
-
}
|
|
1740
|
-
});
|
|
1741
|
-
} else {
|
|
1742
|
-
try {
|
|
1743
|
-
this.handleAgentEvent(event);
|
|
1744
|
-
} catch (err) {
|
|
1745
|
-
log2.error({ err, sessionId: this.session.id }, "Error handling agent event");
|
|
1746
|
-
}
|
|
1747
|
-
}
|
|
1748
|
-
};
|
|
1749
|
-
this.session.on("agent_event", this.sessionEventHandler);
|
|
1750
|
-
}
|
|
1751
|
-
handleAgentEvent(event) {
|
|
1752
|
-
const session = this.session;
|
|
1753
|
-
const ctx = {
|
|
1754
|
-
get id() {
|
|
1755
|
-
return session.id;
|
|
1756
|
-
},
|
|
1757
|
-
get workingDirectory() {
|
|
1758
|
-
return session.workingDirectory;
|
|
1759
|
-
}
|
|
1760
|
-
};
|
|
1761
|
-
let outgoing;
|
|
1762
|
-
switch (event.type) {
|
|
1763
|
-
case "text":
|
|
1764
|
-
case "thought":
|
|
1765
|
-
case "tool_call":
|
|
1766
|
-
case "tool_update":
|
|
1767
|
-
case "plan":
|
|
1768
|
-
case "usage":
|
|
1769
|
-
outgoing = this.deps.messageTransformer.transform(event, ctx);
|
|
1770
|
-
this.tracer?.log("core", { step: "transform", sessionId: this.session.id, input: event, output: outgoing });
|
|
1771
|
-
this.sendMessage(this.session.id, outgoing);
|
|
1772
|
-
break;
|
|
1773
|
-
case "session_end":
|
|
1774
|
-
this.session.finish(event.reason);
|
|
1775
|
-
this.adapter.cleanupSkillCommands?.(this.session.id);
|
|
1776
|
-
outgoing = this.deps.messageTransformer.transform(event);
|
|
1777
|
-
this.sendMessage(this.session.id, outgoing);
|
|
1778
|
-
this.deps.notificationManager.notify(this.session.channelId, {
|
|
1779
|
-
sessionId: this.session.id,
|
|
1780
|
-
sessionName: this.session.name,
|
|
1781
|
-
type: "completed",
|
|
1782
|
-
summary: `Session "${this.session.name || this.session.id}" completed
|
|
1783
|
-
\u23F1 ${Math.round((Date.now() - this.session.createdAt.getTime()) / 6e4)} min \xB7 \u{1F4AC} ${this.session.promptCount} prompts`
|
|
1784
|
-
});
|
|
1785
|
-
break;
|
|
1786
|
-
case "error":
|
|
1787
|
-
this.session.fail(event.message);
|
|
1788
|
-
this.adapter.cleanupSkillCommands?.(this.session.id);
|
|
1789
|
-
outgoing = this.deps.messageTransformer.transform(event);
|
|
1790
|
-
this.sendMessage(this.session.id, outgoing);
|
|
1791
|
-
this.deps.notificationManager.notify(this.session.channelId, {
|
|
1792
|
-
sessionId: this.session.id,
|
|
1793
|
-
sessionName: this.session.name,
|
|
1794
|
-
type: "error",
|
|
1795
|
-
summary: event.message
|
|
1796
|
-
});
|
|
1797
|
-
break;
|
|
1798
|
-
case "image_content": {
|
|
1799
|
-
if (this.deps.fileService) {
|
|
1800
|
-
const fs6 = this.deps.fileService;
|
|
1801
|
-
const sid = this.session.id;
|
|
1802
|
-
const { data, mimeType } = event;
|
|
1803
|
-
const buffer = Buffer.from(data, "base64");
|
|
1804
|
-
const ext = fs6.extensionFromMime(mimeType);
|
|
1805
|
-
fs6.saveFile(sid, `agent-image${ext}`, buffer, mimeType).then((att) => {
|
|
1806
|
-
this.sendMessage(sid, {
|
|
1807
|
-
type: "attachment",
|
|
1808
|
-
text: "",
|
|
1809
|
-
attachment: att
|
|
1810
|
-
});
|
|
1811
|
-
}).catch((err) => log2.error({ err }, "Failed to save agent image"));
|
|
1812
|
-
}
|
|
1813
|
-
break;
|
|
1814
|
-
}
|
|
1815
|
-
case "audio_content": {
|
|
1816
|
-
if (this.deps.fileService) {
|
|
1817
|
-
const fs6 = this.deps.fileService;
|
|
1818
|
-
const sid = this.session.id;
|
|
1819
|
-
const { data, mimeType } = event;
|
|
1820
|
-
const buffer = Buffer.from(data, "base64");
|
|
1821
|
-
const ext = fs6.extensionFromMime(mimeType);
|
|
1822
|
-
fs6.saveFile(sid, `agent-audio${ext}`, buffer, mimeType).then((att) => {
|
|
1823
|
-
this.sendMessage(sid, {
|
|
1824
|
-
type: "attachment",
|
|
1825
|
-
text: "",
|
|
1826
|
-
attachment: att
|
|
1827
|
-
});
|
|
1828
|
-
}).catch((err) => log2.error({ err }, "Failed to save agent audio"));
|
|
1829
|
-
}
|
|
1830
|
-
break;
|
|
1831
|
-
}
|
|
1832
|
-
case "commands_update":
|
|
1833
|
-
log2.debug({ commands: event.commands }, "Commands available");
|
|
1834
|
-
this.adapter.sendSkillCommands?.(this.session.id, event.commands);
|
|
1835
|
-
break;
|
|
1836
|
-
case "system_message":
|
|
1837
|
-
outgoing = this.deps.messageTransformer.transform(event);
|
|
1838
|
-
this.sendMessage(this.session.id, outgoing);
|
|
1839
|
-
break;
|
|
1840
|
-
case "session_info_update":
|
|
1841
|
-
if (event.title) {
|
|
1842
|
-
this.session.setName(event.title);
|
|
1843
|
-
}
|
|
1844
|
-
outgoing = this.deps.messageTransformer.transform(event);
|
|
1845
|
-
this.sendMessage(this.session.id, outgoing);
|
|
1846
|
-
break;
|
|
1847
|
-
case "current_mode_update":
|
|
1848
|
-
this.session.updateMode(event.modeId);
|
|
1849
|
-
outgoing = this.deps.messageTransformer.transform(event);
|
|
1850
|
-
this.sendMessage(this.session.id, outgoing);
|
|
1851
|
-
break;
|
|
1852
|
-
case "config_option_update":
|
|
1853
|
-
this.session.updateConfigOptions(event.options);
|
|
1854
|
-
outgoing = this.deps.messageTransformer.transform(event);
|
|
1855
|
-
this.sendMessage(this.session.id, outgoing);
|
|
1856
|
-
break;
|
|
1857
|
-
case "model_update":
|
|
1858
|
-
this.session.updateModel(event.modelId);
|
|
1859
|
-
outgoing = this.deps.messageTransformer.transform(event);
|
|
1860
|
-
this.sendMessage(this.session.id, outgoing);
|
|
1861
|
-
break;
|
|
1862
|
-
case "user_message_chunk":
|
|
1863
|
-
outgoing = this.deps.messageTransformer.transform(event);
|
|
1864
|
-
this.sendMessage(this.session.id, outgoing);
|
|
1865
|
-
break;
|
|
1866
|
-
case "resource_content":
|
|
1867
|
-
case "resource_link":
|
|
1868
|
-
outgoing = this.deps.messageTransformer.transform(event);
|
|
1869
|
-
this.sendMessage(this.session.id, outgoing);
|
|
1870
|
-
break;
|
|
1871
|
-
case "tts_strip":
|
|
1872
|
-
this.adapter.stripTTSBlock?.(this.session.id);
|
|
1873
|
-
break;
|
|
1874
|
-
}
|
|
1875
|
-
this.deps.eventBus?.emit("agent:event", {
|
|
1876
|
-
sessionId: this.session.id,
|
|
1877
|
-
event
|
|
1878
|
-
});
|
|
1879
|
-
return outgoing;
|
|
1880
|
-
}
|
|
1881
|
-
wirePermissions() {
|
|
1882
|
-
const mw = this.deps.middlewareChain;
|
|
1883
|
-
this.session.agentInstance.onPermissionRequest = async (request) => {
|
|
1884
|
-
const startTime = Date.now();
|
|
1885
|
-
let permReq = request;
|
|
1886
|
-
if (mw) {
|
|
1887
|
-
const payload = { sessionId: this.session.id, request, autoResolve: void 0 };
|
|
1888
|
-
const result = await mw.execute("permission:beforeRequest", payload, async (r) => r);
|
|
1889
|
-
if (!result) return "";
|
|
1890
|
-
permReq = result.request;
|
|
1891
|
-
if (result.autoResolve) {
|
|
1892
|
-
if (mw) {
|
|
1893
|
-
mw.execute("permission:afterResolve", {
|
|
1894
|
-
sessionId: this.session.id,
|
|
1895
|
-
requestId: permReq.id,
|
|
1896
|
-
decision: result.autoResolve,
|
|
1897
|
-
userId: "middleware",
|
|
1898
|
-
durationMs: Date.now() - startTime
|
|
1899
|
-
}, async (p) => p).catch(() => {
|
|
1900
|
-
});
|
|
1901
|
-
}
|
|
1902
|
-
return result.autoResolve;
|
|
1903
|
-
}
|
|
1904
|
-
}
|
|
1905
|
-
this.session.emit("permission_request", permReq);
|
|
1906
|
-
this.deps.eventBus?.emit("permission:request", {
|
|
1907
|
-
sessionId: this.session.id,
|
|
1908
|
-
permission: permReq
|
|
1909
|
-
});
|
|
1910
|
-
if (permReq.description.toLowerCase().includes("openacp")) {
|
|
1911
|
-
const allowOption = permReq.options.find((o) => o.isAllow);
|
|
1912
|
-
if (allowOption) {
|
|
1913
|
-
log2.info(
|
|
1914
|
-
{ sessionId: this.session.id, requestId: permReq.id },
|
|
1915
|
-
"Auto-approving openacp command"
|
|
1916
|
-
);
|
|
1917
|
-
if (mw) {
|
|
1918
|
-
mw.execute("permission:afterResolve", {
|
|
1919
|
-
sessionId: this.session.id,
|
|
1920
|
-
requestId: permReq.id,
|
|
1921
|
-
decision: allowOption.id,
|
|
1922
|
-
userId: "system",
|
|
1923
|
-
durationMs: Date.now() - startTime
|
|
1924
|
-
}, async (p) => p).catch(() => {
|
|
1925
|
-
});
|
|
1926
|
-
}
|
|
1927
|
-
return allowOption.id;
|
|
1928
|
-
}
|
|
1929
|
-
}
|
|
1930
|
-
if (this.session.dangerousMode) {
|
|
1931
|
-
const allowOption = permReq.options.find((o) => o.isAllow);
|
|
1932
|
-
if (allowOption) {
|
|
1933
|
-
log2.info(
|
|
1934
|
-
{ sessionId: this.session.id, requestId: permReq.id, optionId: allowOption.id },
|
|
1935
|
-
"Dangerous mode: auto-approving permission"
|
|
1936
|
-
);
|
|
1937
|
-
if (mw) {
|
|
1938
|
-
mw.execute("permission:afterResolve", {
|
|
1939
|
-
sessionId: this.session.id,
|
|
1940
|
-
requestId: permReq.id,
|
|
1941
|
-
decision: allowOption.id,
|
|
1942
|
-
userId: "system",
|
|
1943
|
-
durationMs: Date.now() - startTime
|
|
1944
|
-
}, async (p) => p).catch(() => {
|
|
1945
|
-
});
|
|
1946
|
-
}
|
|
1947
|
-
return allowOption.id;
|
|
1948
|
-
}
|
|
1949
|
-
}
|
|
1950
|
-
const promise = this.session.permissionGate.setPending(permReq);
|
|
1951
|
-
await this.adapter.sendPermissionRequest(this.session.id, permReq);
|
|
1952
|
-
const optionId = await promise;
|
|
1953
|
-
if (mw) {
|
|
1954
|
-
mw.execute("permission:afterResolve", {
|
|
1955
|
-
sessionId: this.session.id,
|
|
1956
|
-
requestId: permReq.id,
|
|
1957
|
-
decision: optionId,
|
|
1958
|
-
userId: "user",
|
|
1959
|
-
durationMs: Date.now() - startTime
|
|
1960
|
-
}, async (p) => p).catch(() => {
|
|
1961
|
-
});
|
|
1962
|
-
}
|
|
1963
|
-
return optionId;
|
|
1964
|
-
};
|
|
1965
|
-
}
|
|
1966
|
-
wireLifecycle() {
|
|
1967
|
-
this.statusChangeHandler = (from, to) => {
|
|
1968
|
-
this.deps.sessionManager.patchRecord(this.session.id, {
|
|
1969
|
-
status: to,
|
|
1970
|
-
lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1971
|
-
});
|
|
1972
|
-
this.deps.eventBus?.emit("session:updated", {
|
|
1973
|
-
sessionId: this.session.id,
|
|
1974
|
-
status: to
|
|
1975
|
-
});
|
|
1976
|
-
if (to === "finished") {
|
|
1977
|
-
queueMicrotask(() => this.disconnect());
|
|
1978
|
-
}
|
|
1979
|
-
};
|
|
1980
|
-
this.session.on("status_change", this.statusChangeHandler);
|
|
1981
|
-
this.namedHandler = (name) => {
|
|
1982
|
-
this.deps.sessionManager.patchRecord(this.session.id, { name });
|
|
1983
|
-
this.deps.eventBus?.emit("session:updated", {
|
|
1984
|
-
sessionId: this.session.id,
|
|
1985
|
-
name
|
|
1986
|
-
});
|
|
1987
|
-
this.adapter.renameSessionThread(this.session.id, name);
|
|
1988
|
-
};
|
|
1989
|
-
this.session.on("named", this.namedHandler);
|
|
1990
|
-
this.promptCountHandler = (count) => {
|
|
1991
|
-
this.deps.sessionManager.patchRecord(this.session.id, { currentPromptCount: count });
|
|
1992
|
-
};
|
|
1993
|
-
this.session.on("prompt_count_changed", this.promptCountHandler);
|
|
1994
|
-
}
|
|
1995
|
-
};
|
|
1996
|
-
|
|
1997
|
-
// src/core/utils/extract-file-info.ts
|
|
1998
|
-
function extractFileInfo(name, kind, content, rawInput, meta) {
|
|
1999
|
-
if (kind && !["read", "edit", "write"].includes(kind)) return null;
|
|
2000
|
-
let info = null;
|
|
2001
|
-
if (meta) {
|
|
2002
|
-
const m = meta;
|
|
2003
|
-
const toolResponse = resolveToolResponse(m);
|
|
2004
|
-
if (toolResponse) {
|
|
2005
|
-
const file = toolResponse.file;
|
|
2006
|
-
if (typeof file?.filePath === "string" && typeof file?.content === "string") {
|
|
2007
|
-
info = { filePath: file.filePath, content: file.content };
|
|
2008
|
-
}
|
|
2009
|
-
if (!info && typeof toolResponse.filePath === "string" && typeof toolResponse.originalFile === "string") {
|
|
2010
|
-
const originalFile = toolResponse.originalFile;
|
|
2011
|
-
const oldString = typeof toolResponse.oldString === "string" ? toolResponse.oldString : void 0;
|
|
2012
|
-
const newString = typeof toolResponse.newString === "string" ? toolResponse.newString : void 0;
|
|
2013
|
-
const newContent = oldString && newString ? originalFile.replace(oldString, newString) : originalFile;
|
|
2014
|
-
info = { filePath: toolResponse.filePath, content: newContent, oldContent: originalFile };
|
|
2015
|
-
}
|
|
2016
|
-
if (!info && typeof toolResponse.filePath === "string" && typeof toolResponse.content === "string") {
|
|
2017
|
-
info = { filePath: toolResponse.filePath, content: toolResponse.content };
|
|
2018
|
-
}
|
|
2019
|
-
}
|
|
2020
|
-
}
|
|
2021
|
-
if (!info && rawInput && typeof rawInput === "object") {
|
|
2022
|
-
const ri = rawInput;
|
|
2023
|
-
const filePath = ri?.file_path || ri?.filePath || ri?.path;
|
|
2024
|
-
if (typeof filePath === "string") {
|
|
2025
|
-
const parsed = content ? parseContent(content) : null;
|
|
2026
|
-
const riContent = typeof ri?.content === "string" ? ri.content : void 0;
|
|
2027
|
-
if (kind === "edit") {
|
|
2028
|
-
const oldStr = typeof ri.old_string === "string" ? ri.old_string : typeof ri.oldText === "string" ? ri.oldText : void 0;
|
|
2029
|
-
const newStr = typeof ri.new_string === "string" ? ri.new_string : typeof ri.newText === "string" ? ri.newText : void 0;
|
|
2030
|
-
if (newStr) {
|
|
2031
|
-
info = { filePath, content: newStr, oldContent: oldStr };
|
|
2032
|
-
} else {
|
|
2033
|
-
info = { filePath, content: riContent || parsed?.content, oldContent: parsed?.oldContent };
|
|
2034
|
-
}
|
|
2035
|
-
} else if (kind === "write") {
|
|
2036
|
-
info = { filePath, content: riContent || parsed?.content, oldContent: parsed?.oldContent };
|
|
2037
|
-
} else {
|
|
2038
|
-
info = { filePath, content: parsed?.content || riContent, oldContent: parsed?.oldContent };
|
|
2039
|
-
}
|
|
2040
|
-
}
|
|
2041
|
-
}
|
|
2042
|
-
if (!info && content) {
|
|
2043
|
-
info = parseContent(content);
|
|
2044
|
-
}
|
|
2045
|
-
if (!info) return null;
|
|
2046
|
-
if (!info.filePath) {
|
|
2047
|
-
const pathMatch = name.match(/(?:Read|Edit|Write|View)\s+(.+)/i);
|
|
2048
|
-
if (pathMatch) info.filePath = pathMatch[1].trim();
|
|
2049
|
-
}
|
|
2050
|
-
if (!info.filePath || !info.content) return null;
|
|
2051
|
-
return info;
|
|
2052
|
-
}
|
|
2053
|
-
function resolveToolResponse(meta) {
|
|
2054
|
-
const claudeCode = meta.claudeCode;
|
|
2055
|
-
if (claudeCode?.toolResponse && typeof claudeCode.toolResponse === "object") {
|
|
2056
|
-
return claudeCode.toolResponse;
|
|
2057
|
-
}
|
|
2058
|
-
if (meta.toolResponse && typeof meta.toolResponse === "object") {
|
|
2059
|
-
return meta.toolResponse;
|
|
2060
|
-
}
|
|
2061
|
-
return void 0;
|
|
2062
|
-
}
|
|
2063
|
-
function parseContent(content) {
|
|
2064
|
-
if (typeof content === "string") {
|
|
2065
|
-
return { content };
|
|
2066
|
-
}
|
|
2067
|
-
if (Array.isArray(content)) {
|
|
2068
|
-
for (const block of content) {
|
|
2069
|
-
const result = parseContent(block);
|
|
2070
|
-
if (result?.content || result?.filePath) return result;
|
|
2071
|
-
}
|
|
2072
|
-
return null;
|
|
2073
|
-
}
|
|
2074
|
-
if (typeof content === "object" && content !== null) {
|
|
2075
|
-
const c = content;
|
|
2076
|
-
if (c.type === "diff" && typeof c.path === "string") {
|
|
2077
|
-
const newText = c.newText;
|
|
2078
|
-
const oldText = c.oldText;
|
|
2079
|
-
if (newText) {
|
|
2080
|
-
return {
|
|
2081
|
-
filePath: c.path,
|
|
2082
|
-
content: newText,
|
|
2083
|
-
oldContent: oldText ?? void 0
|
|
2084
|
-
};
|
|
2085
|
-
}
|
|
2086
|
-
}
|
|
2087
|
-
if (c.type === "content" && c.content) {
|
|
2088
|
-
return parseContent(c.content);
|
|
2089
|
-
}
|
|
2090
|
-
if (c.type === "text" && typeof c.text === "string") {
|
|
2091
|
-
return { content: c.text, filePath: c.filePath };
|
|
2092
|
-
}
|
|
2093
|
-
if (typeof c.text === "string") {
|
|
2094
|
-
return { content: c.text, filePath: c.filePath };
|
|
2095
|
-
}
|
|
2096
|
-
if (typeof c.file_path === "string" || typeof c.filePath === "string" || typeof c.path === "string") {
|
|
2097
|
-
const filePath = c.file_path || c.filePath || c.path;
|
|
2098
|
-
const fileContent = c.content || c.text || c.output || c.newText;
|
|
2099
|
-
if (typeof fileContent === "string") {
|
|
2100
|
-
return {
|
|
2101
|
-
filePath,
|
|
2102
|
-
content: fileContent,
|
|
2103
|
-
oldContent: c.old_content || c.oldText
|
|
2104
|
-
};
|
|
2105
|
-
}
|
|
2106
|
-
}
|
|
2107
|
-
if (c.input) {
|
|
2108
|
-
const result = parseContent(c.input);
|
|
2109
|
-
if (result) return result;
|
|
2110
|
-
}
|
|
2111
|
-
if (c.output) {
|
|
2112
|
-
const result = parseContent(c.output);
|
|
2113
|
-
if (result) return result;
|
|
2114
|
-
}
|
|
2115
|
-
}
|
|
2116
|
-
return null;
|
|
2117
|
-
}
|
|
2118
|
-
|
|
2119
|
-
// src/core/message-transformer.ts
|
|
2120
|
-
var log3 = createChildLogger({ module: "message-transformer" });
|
|
2121
|
-
function computeLineDiff(oldStr, newStr) {
|
|
2122
|
-
const oldLines = oldStr ? oldStr.split("\n") : [];
|
|
2123
|
-
const newLines = newStr ? newStr.split("\n") : [];
|
|
2124
|
-
let prefixLen = 0;
|
|
2125
|
-
const minLen = Math.min(oldLines.length, newLines.length);
|
|
2126
|
-
while (prefixLen < minLen && oldLines[prefixLen] === newLines[prefixLen]) {
|
|
2127
|
-
prefixLen++;
|
|
2128
|
-
}
|
|
2129
|
-
let suffixLen = 0;
|
|
2130
|
-
const maxSuffix = minLen - prefixLen;
|
|
2131
|
-
while (suffixLen < maxSuffix && oldLines[oldLines.length - 1 - suffixLen] === newLines[newLines.length - 1 - suffixLen]) {
|
|
2132
|
-
suffixLen++;
|
|
2133
|
-
}
|
|
2134
|
-
return {
|
|
2135
|
-
added: Math.max(0, newLines.length - prefixLen - suffixLen),
|
|
2136
|
-
removed: Math.max(0, oldLines.length - prefixLen - suffixLen)
|
|
2137
|
-
};
|
|
2138
|
-
}
|
|
2139
|
-
var MessageTransformer = class {
|
|
2140
|
-
tunnelService;
|
|
2141
|
-
/** Cache rawInput from tool_call so it's available in tool_update (which often lacks it) */
|
|
2142
|
-
toolRawInputCache = /* @__PURE__ */ new Map();
|
|
2143
|
-
/** Cache viewer links generated from intermediate updates so completion events carry them */
|
|
2144
|
-
toolViewerCache = /* @__PURE__ */ new Map();
|
|
2145
|
-
constructor(tunnelService) {
|
|
2146
|
-
this.tunnelService = tunnelService;
|
|
2147
|
-
}
|
|
2148
|
-
transform(event, sessionContext) {
|
|
2149
|
-
switch (event.type) {
|
|
2150
|
-
case "text":
|
|
2151
|
-
return { type: "text", text: event.content };
|
|
2152
|
-
case "thought":
|
|
2153
|
-
return { type: "thought", text: event.content };
|
|
2154
|
-
case "tool_call": {
|
|
2155
|
-
if (event.id && this.isNonEmptyInput(event.rawInput)) {
|
|
2156
|
-
this.toolRawInputCache.set(event.id, event.rawInput);
|
|
2157
|
-
}
|
|
2158
|
-
const meta = event.meta;
|
|
2159
|
-
const metadata = {
|
|
2160
|
-
id: event.id,
|
|
2161
|
-
name: event.name,
|
|
2162
|
-
kind: event.kind,
|
|
2163
|
-
status: event.status,
|
|
2164
|
-
content: event.content,
|
|
2165
|
-
locations: event.locations,
|
|
2166
|
-
rawInput: event.rawInput,
|
|
2167
|
-
displaySummary: meta?.displaySummary,
|
|
2168
|
-
displayTitle: meta?.displayTitle,
|
|
2169
|
-
displayKind: meta?.displayKind
|
|
2170
|
-
};
|
|
2171
|
-
this.enrichWithViewerLinks(event, metadata, sessionContext);
|
|
2172
|
-
return { type: "tool_call", text: event.name, metadata };
|
|
2173
|
-
}
|
|
2174
|
-
case "tool_update": {
|
|
2175
|
-
if (event.id && this.isNonEmptyInput(event.rawInput)) {
|
|
2176
|
-
this.toolRawInputCache.set(event.id, event.rawInput);
|
|
2177
|
-
}
|
|
2178
|
-
const cachedRawInput = event.id ? this.toolRawInputCache.get(event.id) : void 0;
|
|
2179
|
-
const effectiveRawInput = this.isNonEmptyInput(event.rawInput) ? event.rawInput : cachedRawInput;
|
|
2180
|
-
if (event.id && (event.status === "completed" || event.status === "done" || event.status === "failed" || event.status === "error")) {
|
|
2181
|
-
this.toolRawInputCache.delete(event.id);
|
|
2182
|
-
}
|
|
2183
|
-
const meta = event.meta;
|
|
2184
|
-
const metadata = {
|
|
2185
|
-
id: event.id,
|
|
2186
|
-
name: event.name,
|
|
2187
|
-
kind: event.kind,
|
|
2188
|
-
status: event.status,
|
|
2189
|
-
content: event.content,
|
|
2190
|
-
rawInput: effectiveRawInput,
|
|
2191
|
-
displaySummary: meta?.displaySummary,
|
|
2192
|
-
displayTitle: meta?.displayTitle,
|
|
2193
|
-
displayKind: meta?.displayKind
|
|
2194
|
-
};
|
|
2195
|
-
const enrichEvent = { ...event, rawInput: effectiveRawInput };
|
|
2196
|
-
this.enrichWithViewerLinks(enrichEvent, metadata, sessionContext);
|
|
2197
|
-
if (event.id) {
|
|
2198
|
-
const cached = this.toolViewerCache.get(event.id);
|
|
2199
|
-
if (cached) {
|
|
2200
|
-
metadata.viewerLinks = cached.viewerLinks;
|
|
2201
|
-
metadata.viewerFilePath = cached.viewerFilePath;
|
|
2202
|
-
} else if (metadata.viewerLinks) {
|
|
2203
|
-
this.toolViewerCache.set(event.id, {
|
|
2204
|
-
viewerLinks: metadata.viewerLinks,
|
|
2205
|
-
viewerFilePath: metadata.viewerFilePath
|
|
2206
|
-
});
|
|
2207
|
-
}
|
|
2208
|
-
if (event.status === "completed" || event.status === "done" || event.status === "failed" || event.status === "error") {
|
|
2209
|
-
this.toolViewerCache.delete(event.id);
|
|
2210
|
-
}
|
|
2211
|
-
}
|
|
2212
|
-
return { type: "tool_update", text: "", metadata };
|
|
2213
|
-
}
|
|
2214
|
-
case "plan":
|
|
2215
|
-
return {
|
|
2216
|
-
type: "plan",
|
|
2217
|
-
text: "",
|
|
2218
|
-
metadata: { entries: event.entries }
|
|
2219
|
-
};
|
|
2220
|
-
case "usage":
|
|
2221
|
-
return {
|
|
2222
|
-
type: "usage",
|
|
2223
|
-
text: "",
|
|
2224
|
-
metadata: {
|
|
2225
|
-
tokensUsed: event.tokensUsed,
|
|
2226
|
-
contextSize: event.contextSize,
|
|
2227
|
-
cost: event.cost?.amount
|
|
2228
|
-
}
|
|
2229
|
-
};
|
|
2230
|
-
case "session_end":
|
|
2231
|
-
return { type: "session_end", text: `Done (${event.reason})` };
|
|
2232
|
-
case "error":
|
|
2233
|
-
return { type: "error", text: event.message };
|
|
2234
|
-
case "system_message":
|
|
2235
|
-
return { type: "system_message", text: event.message };
|
|
2236
|
-
case "session_info_update":
|
|
2237
|
-
return {
|
|
2238
|
-
type: "system_message",
|
|
2239
|
-
text: `Session updated: ${event.title ?? ""}`.trim(),
|
|
2240
|
-
metadata: { title: event.title, updatedAt: event.updatedAt }
|
|
2241
|
-
};
|
|
2242
|
-
case "current_mode_update":
|
|
2243
|
-
return {
|
|
2244
|
-
type: "mode_change",
|
|
2245
|
-
text: `Mode: ${event.modeId}`,
|
|
2246
|
-
metadata: { modeId: event.modeId }
|
|
2247
|
-
};
|
|
2248
|
-
case "config_option_update":
|
|
2249
|
-
return {
|
|
2250
|
-
type: "config_update",
|
|
2251
|
-
text: "Config updated",
|
|
2252
|
-
metadata: { options: event.options }
|
|
2253
|
-
};
|
|
2254
|
-
case "model_update":
|
|
2255
|
-
return {
|
|
2256
|
-
type: "model_update",
|
|
2257
|
-
text: `Model: ${event.modelId}`,
|
|
2258
|
-
metadata: { modelId: event.modelId }
|
|
2259
|
-
};
|
|
2260
|
-
case "user_message_chunk":
|
|
2261
|
-
return {
|
|
2262
|
-
type: "user_replay",
|
|
2263
|
-
text: event.content
|
|
2264
|
-
};
|
|
2265
|
-
case "resource_content":
|
|
2266
|
-
return {
|
|
2267
|
-
type: "resource",
|
|
2268
|
-
text: event.name,
|
|
2269
|
-
metadata: { uri: event.uri, text: event.text, blob: event.blob, mimeType: event.mimeType }
|
|
2270
|
-
};
|
|
2271
|
-
case "resource_link":
|
|
2272
|
-
return {
|
|
2273
|
-
type: "resource_link",
|
|
2274
|
-
text: event.name,
|
|
2275
|
-
metadata: { uri: event.uri, mimeType: event.mimeType, title: event.title, description: event.description, size: event.size }
|
|
2276
|
-
};
|
|
2277
|
-
default:
|
|
2278
|
-
return { type: "text", text: "" };
|
|
2279
|
-
}
|
|
2280
|
-
}
|
|
2281
|
-
/** Check if rawInput is a non-empty object (not null, not {}) */
|
|
2282
|
-
isNonEmptyInput(input) {
|
|
2283
|
-
return input !== null && input !== void 0 && typeof input === "object" && !Array.isArray(input) && Object.keys(input).length > 0;
|
|
2284
|
-
}
|
|
2285
|
-
enrichWithViewerLinks(event, metadata, sessionContext) {
|
|
2286
|
-
const kind = "kind" in event ? event.kind : void 0;
|
|
2287
|
-
if (!metadata.diffStats && (kind === "edit" || kind === "write")) {
|
|
2288
|
-
const ri = event.rawInput;
|
|
2289
|
-
if (ri) {
|
|
2290
|
-
const oldStr = typeof ri.old_string === "string" ? ri.old_string : typeof ri.oldText === "string" ? ri.oldText : null;
|
|
2291
|
-
const newStr = typeof ri.new_string === "string" ? ri.new_string : typeof ri.newText === "string" ? ri.newText : typeof ri.content === "string" ? ri.content : null;
|
|
2292
|
-
if (oldStr !== null && newStr !== null) {
|
|
2293
|
-
const stats = computeLineDiff(oldStr, newStr);
|
|
2294
|
-
if (stats.added > 0 || stats.removed > 0) {
|
|
2295
|
-
metadata.diffStats = stats;
|
|
2296
|
-
}
|
|
2297
|
-
} else if (oldStr === null && newStr !== null && kind === "write") {
|
|
2298
|
-
const added = newStr.split("\n").length;
|
|
2299
|
-
if (added > 0) metadata.diffStats = { added, removed: 0 };
|
|
2300
|
-
}
|
|
2301
|
-
}
|
|
2302
|
-
}
|
|
2303
|
-
if (!this.tunnelService || !sessionContext) {
|
|
2304
|
-
log3.debug(
|
|
2305
|
-
{ hasTunnel: !!this.tunnelService, hasCtx: !!sessionContext, kind },
|
|
2306
|
-
"enrichWithViewerLinks: skipping (no tunnel or session context)"
|
|
2307
|
-
);
|
|
2308
|
-
return;
|
|
2309
|
-
}
|
|
2310
|
-
const name = "name" in event ? event.name || "" : "";
|
|
2311
|
-
log3.debug(
|
|
2312
|
-
{ name, kind, status: event.status, hasContent: !!event.content, hasRawInput: !!event.rawInput },
|
|
2313
|
-
"enrichWithViewerLinks: inspecting event"
|
|
2314
|
-
);
|
|
2315
|
-
const fileInfo = extractFileInfo(
|
|
2316
|
-
name,
|
|
2317
|
-
kind,
|
|
2318
|
-
event.content,
|
|
2319
|
-
event.rawInput,
|
|
2320
|
-
event.meta
|
|
2321
|
-
);
|
|
2322
|
-
if (!fileInfo) {
|
|
2323
|
-
log3.debug(
|
|
2324
|
-
{ name, kind, hasContent: !!event.content, hasRawInput: !!event.rawInput, hasMeta: !!event.meta },
|
|
2325
|
-
"enrichWithViewerLinks: extractFileInfo returned null"
|
|
2326
|
-
);
|
|
2327
|
-
return;
|
|
2328
|
-
}
|
|
2329
|
-
const publicUrl = this.tunnelService.getPublicUrl();
|
|
2330
|
-
if (publicUrl.startsWith("http://localhost") || publicUrl.startsWith("http://127.0.0.1")) {
|
|
2331
|
-
log3.debug({ kind, filePath: fileInfo.filePath }, "enrichWithViewerLinks: skipping (no public tunnel URL)");
|
|
2332
|
-
return;
|
|
2333
|
-
}
|
|
2334
|
-
log3.info(
|
|
2335
|
-
{
|
|
2336
|
-
name,
|
|
2337
|
-
kind,
|
|
2338
|
-
filePath: fileInfo.filePath,
|
|
2339
|
-
hasOldContent: !!fileInfo.oldContent
|
|
2340
|
-
},
|
|
2341
|
-
"enrichWithViewerLinks: extracted file info"
|
|
2342
|
-
);
|
|
2343
|
-
const store = this.tunnelService.getStore();
|
|
2344
|
-
const viewerLinks = {};
|
|
2345
|
-
if (fileInfo.oldContent) {
|
|
2346
|
-
const id2 = store.storeDiff(
|
|
2347
|
-
sessionContext.id,
|
|
2348
|
-
fileInfo.filePath,
|
|
2349
|
-
fileInfo.oldContent,
|
|
2350
|
-
fileInfo.content,
|
|
2351
|
-
sessionContext.workingDirectory
|
|
2352
|
-
);
|
|
2353
|
-
if (id2) viewerLinks.diff = this.tunnelService.diffUrl(id2);
|
|
2354
|
-
if (!metadata.diffStats) {
|
|
2355
|
-
const stats = computeLineDiff(fileInfo.oldContent, fileInfo.content);
|
|
2356
|
-
if (stats.added > 0 || stats.removed > 0) {
|
|
2357
|
-
metadata.diffStats = stats;
|
|
2358
|
-
}
|
|
2359
|
-
}
|
|
2360
|
-
}
|
|
2361
|
-
const id = store.storeFile(
|
|
2362
|
-
sessionContext.id,
|
|
2363
|
-
fileInfo.filePath,
|
|
2364
|
-
fileInfo.content,
|
|
2365
|
-
sessionContext.workingDirectory
|
|
2366
|
-
);
|
|
2367
|
-
if (id) viewerLinks.file = this.tunnelService.fileUrl(id);
|
|
2368
|
-
if (Object.keys(viewerLinks).length > 0) {
|
|
2369
|
-
metadata.viewerLinks = viewerLinks;
|
|
2370
|
-
metadata.viewerFilePath = fileInfo.filePath;
|
|
2371
|
-
}
|
|
2372
|
-
}
|
|
2373
|
-
};
|
|
2374
|
-
|
|
2375
|
-
// src/core/sessions/session-factory.ts
|
|
2376
|
-
var log4 = createChildLogger({ module: "session-factory" });
|
|
2377
|
-
var SessionFactory = class {
|
|
2378
|
-
constructor(agentManager, sessionManager, speechServiceAccessor, eventBus) {
|
|
2379
|
-
this.agentManager = agentManager;
|
|
2380
|
-
this.sessionManager = sessionManager;
|
|
2381
|
-
this.speechServiceAccessor = speechServiceAccessor;
|
|
2382
|
-
this.eventBus = eventBus;
|
|
2383
|
-
}
|
|
2384
|
-
middlewareChain;
|
|
2385
|
-
get speechService() {
|
|
2386
|
-
return typeof this.speechServiceAccessor === "function" ? this.speechServiceAccessor() : this.speechServiceAccessor;
|
|
2387
|
-
}
|
|
2388
|
-
async create(params) {
|
|
2389
|
-
let createParams = params;
|
|
2390
|
-
if (this.middlewareChain) {
|
|
2391
|
-
const payload = {
|
|
2392
|
-
agentName: params.agentName,
|
|
2393
|
-
workingDir: params.workingDirectory,
|
|
2394
|
-
userId: "",
|
|
2395
|
-
// userId is not part of SessionCreateParams — resolved upstream
|
|
2396
|
-
channelId: params.channelId,
|
|
2397
|
-
threadId: ""
|
|
2398
|
-
// threadId is assigned after session creation
|
|
2399
|
-
};
|
|
2400
|
-
const result = await this.middlewareChain.execute("session:beforeCreate", payload, async (p) => p);
|
|
2401
|
-
if (!result) throw new Error("Session creation blocked by middleware");
|
|
2402
|
-
createParams = {
|
|
2403
|
-
...params,
|
|
2404
|
-
agentName: result.agentName,
|
|
2405
|
-
workingDirectory: result.workingDir,
|
|
2406
|
-
channelId: result.channelId
|
|
2407
|
-
};
|
|
2408
|
-
}
|
|
2409
|
-
const agentInstance = createParams.resumeAgentSessionId ? await this.agentManager.resume(
|
|
2410
|
-
createParams.agentName,
|
|
2411
|
-
createParams.workingDirectory,
|
|
2412
|
-
createParams.resumeAgentSessionId
|
|
2413
|
-
) : await this.agentManager.spawn(
|
|
2414
|
-
createParams.agentName,
|
|
2415
|
-
createParams.workingDirectory
|
|
2416
|
-
);
|
|
2417
|
-
agentInstance.middlewareChain = this.middlewareChain;
|
|
2418
|
-
const session = new Session({
|
|
2419
|
-
id: createParams.existingSessionId,
|
|
2420
|
-
channelId: createParams.channelId,
|
|
2421
|
-
agentName: createParams.agentName,
|
|
2422
|
-
workingDirectory: createParams.workingDirectory,
|
|
2423
|
-
agentInstance,
|
|
2424
|
-
speechService: this.speechService
|
|
2425
|
-
});
|
|
2426
|
-
session.agentSessionId = agentInstance.sessionId;
|
|
2427
|
-
session.middlewareChain = this.middlewareChain;
|
|
2428
|
-
if (createParams.initialName) {
|
|
2429
|
-
session.name = createParams.initialName;
|
|
2430
|
-
}
|
|
2431
|
-
this.sessionManager.registerSession(session);
|
|
2432
|
-
this.eventBus.emit("session:created", {
|
|
2433
|
-
sessionId: session.id,
|
|
2434
|
-
agent: session.agentName,
|
|
2435
|
-
status: session.status
|
|
2436
|
-
});
|
|
2437
|
-
return session;
|
|
2438
|
-
}
|
|
2439
|
-
wireSideEffects(session, deps) {
|
|
2440
|
-
session.on("agent_event", (event) => {
|
|
2441
|
-
if (event.type !== "usage") return;
|
|
2442
|
-
deps.eventBus.emit("usage:recorded", {
|
|
2443
|
-
sessionId: session.id,
|
|
2444
|
-
agentName: session.agentName,
|
|
2445
|
-
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2446
|
-
tokensUsed: event.tokensUsed ?? 0,
|
|
2447
|
-
contextSize: event.contextSize ?? 0,
|
|
2448
|
-
cost: event.cost
|
|
2449
|
-
});
|
|
2450
|
-
});
|
|
2451
|
-
session.on("status_change", (_from, to) => {
|
|
2452
|
-
if ((to === "finished" || to === "cancelled") && deps.tunnelService) {
|
|
2453
|
-
deps.tunnelService.stopBySession(session.id).then((stopped) => {
|
|
2454
|
-
for (const entry of stopped) {
|
|
2455
|
-
deps.notificationManager.notifyAll({
|
|
2456
|
-
sessionId: session.id,
|
|
2457
|
-
sessionName: session.name,
|
|
2458
|
-
type: "completed",
|
|
2459
|
-
summary: `Tunnel stopped: port ${entry.port}${entry.label ? ` (${entry.label})` : ""} \u2014 session ended`
|
|
2460
|
-
}).catch(() => {
|
|
2461
|
-
});
|
|
2462
|
-
}
|
|
2463
|
-
}).catch(() => {
|
|
2464
|
-
});
|
|
2465
|
-
}
|
|
2466
|
-
});
|
|
2467
|
-
}
|
|
2468
|
-
};
|
|
2469
|
-
|
|
2470
|
-
// src/core/event-bus.ts
|
|
2471
|
-
var EventBus = class extends TypedEmitter {
|
|
2472
|
-
};
|
|
2473
|
-
|
|
2474
|
-
// src/core/core.ts
|
|
2475
|
-
import path6 from "path";
|
|
2476
|
-
import os2 from "os";
|
|
2477
|
-
|
|
2478
|
-
// src/core/sessions/session-store.ts
|
|
2479
|
-
import fs4 from "fs";
|
|
2480
|
-
import path3 from "path";
|
|
2481
|
-
var log5 = createChildLogger({ module: "session-store" });
|
|
2482
|
-
var DEBOUNCE_MS = 2e3;
|
|
2483
|
-
var JsonFileSessionStore = class {
|
|
2484
|
-
records = /* @__PURE__ */ new Map();
|
|
2485
|
-
filePath;
|
|
2486
|
-
ttlDays;
|
|
2487
|
-
debounceTimer = null;
|
|
2488
|
-
cleanupInterval = null;
|
|
2489
|
-
flushHandler = null;
|
|
2490
|
-
constructor(filePath, ttlDays) {
|
|
2491
|
-
this.filePath = filePath;
|
|
2492
|
-
this.ttlDays = ttlDays;
|
|
2493
|
-
this.load();
|
|
2494
|
-
this.cleanup();
|
|
2495
|
-
this.cleanupInterval = setInterval(
|
|
2496
|
-
() => this.cleanup(),
|
|
2497
|
-
24 * 60 * 60 * 1e3
|
|
2498
|
-
);
|
|
2499
|
-
this.flushHandler = () => this.flushSync();
|
|
2500
|
-
process.on("SIGTERM", this.flushHandler);
|
|
2501
|
-
process.on("SIGINT", this.flushHandler);
|
|
2502
|
-
process.on("exit", this.flushHandler);
|
|
2503
|
-
}
|
|
2504
|
-
async save(record) {
|
|
2505
|
-
this.records.set(record.sessionId, { ...record });
|
|
2506
|
-
this.scheduleDiskWrite();
|
|
2507
|
-
}
|
|
2508
|
-
get(sessionId) {
|
|
2509
|
-
return this.records.get(sessionId);
|
|
2510
|
-
}
|
|
2511
|
-
findByPlatform(channelId, predicate) {
|
|
2512
|
-
for (const record of this.records.values()) {
|
|
2513
|
-
if (record.channelId === channelId && predicate(record.platform)) {
|
|
2514
|
-
return record;
|
|
2515
|
-
}
|
|
2516
|
-
}
|
|
2517
|
-
return void 0;
|
|
2518
|
-
}
|
|
2519
|
-
findByAgentSessionId(agentSessionId) {
|
|
2520
|
-
for (const record of this.records.values()) {
|
|
2521
|
-
if (record.agentSessionId === agentSessionId || record.originalAgentSessionId === agentSessionId) {
|
|
2522
|
-
return record;
|
|
2523
|
-
}
|
|
2524
|
-
if (record.agentSwitchHistory?.some((e) => e.agentSessionId === agentSessionId)) {
|
|
2525
|
-
return record;
|
|
2526
|
-
}
|
|
2527
|
-
}
|
|
2528
|
-
return void 0;
|
|
2529
|
-
}
|
|
2530
|
-
list(channelId) {
|
|
2531
|
-
const all = [...this.records.values()];
|
|
2532
|
-
if (channelId) return all.filter((r) => r.channelId === channelId);
|
|
2533
|
-
return all;
|
|
2534
|
-
}
|
|
2535
|
-
async remove(sessionId) {
|
|
2536
|
-
this.records.delete(sessionId);
|
|
2537
|
-
this.scheduleDiskWrite();
|
|
2538
|
-
}
|
|
2539
|
-
flushSync() {
|
|
2540
|
-
if (this.debounceTimer) {
|
|
2541
|
-
clearTimeout(this.debounceTimer);
|
|
2542
|
-
this.debounceTimer = null;
|
|
2543
|
-
}
|
|
2544
|
-
const data = {
|
|
2545
|
-
version: 1,
|
|
2546
|
-
sessions: Object.fromEntries(this.records)
|
|
2547
|
-
};
|
|
2548
|
-
const dir = path3.dirname(this.filePath);
|
|
2549
|
-
if (!fs4.existsSync(dir)) fs4.mkdirSync(dir, { recursive: true });
|
|
2550
|
-
fs4.writeFileSync(this.filePath, JSON.stringify(data, null, 2));
|
|
2551
|
-
}
|
|
2552
|
-
destroy() {
|
|
2553
|
-
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
2554
|
-
if (this.cleanupInterval) clearInterval(this.cleanupInterval);
|
|
2555
|
-
if (this.flushHandler) {
|
|
2556
|
-
process.removeListener("SIGTERM", this.flushHandler);
|
|
2557
|
-
process.removeListener("SIGINT", this.flushHandler);
|
|
2558
|
-
process.removeListener("exit", this.flushHandler);
|
|
2559
|
-
this.flushHandler = null;
|
|
2560
|
-
}
|
|
2561
|
-
}
|
|
2562
|
-
load() {
|
|
2563
|
-
if (!fs4.existsSync(this.filePath)) return;
|
|
2564
|
-
try {
|
|
2565
|
-
const raw = JSON.parse(
|
|
2566
|
-
fs4.readFileSync(this.filePath, "utf-8")
|
|
2567
|
-
);
|
|
2568
|
-
if (raw.version !== 1) {
|
|
2569
|
-
log5.warn(
|
|
2570
|
-
{ version: raw.version },
|
|
2571
|
-
"Unknown session store version, skipping load"
|
|
2572
|
-
);
|
|
2573
|
-
return;
|
|
2574
|
-
}
|
|
2575
|
-
for (const [id, record] of Object.entries(raw.sessions)) {
|
|
2576
|
-
this.records.set(id, record);
|
|
2577
|
-
}
|
|
2578
|
-
log5.debug({ count: this.records.size }, "Loaded session records");
|
|
2579
|
-
} catch (err) {
|
|
2580
|
-
log5.error({ err }, "Failed to load session store, backing up corrupt file");
|
|
2581
|
-
try {
|
|
2582
|
-
fs4.renameSync(this.filePath, `${this.filePath}.bak`);
|
|
2583
|
-
} catch {
|
|
2584
|
-
}
|
|
2585
|
-
}
|
|
2586
|
-
}
|
|
2587
|
-
cleanup() {
|
|
2588
|
-
const cutoff = Date.now() - this.ttlDays * 24 * 60 * 60 * 1e3;
|
|
2589
|
-
let removed = 0;
|
|
2590
|
-
for (const [id, record] of this.records) {
|
|
2591
|
-
if (record.status === "active" || record.status === "initializing")
|
|
2592
|
-
continue;
|
|
2593
|
-
const lastActive = new Date(record.lastActiveAt).getTime();
|
|
2594
|
-
if (lastActive < cutoff) {
|
|
2595
|
-
this.records.delete(id);
|
|
2596
|
-
removed++;
|
|
2597
|
-
}
|
|
2598
|
-
}
|
|
2599
|
-
if (removed > 0) {
|
|
2600
|
-
log5.info({ removed }, "Cleaned up expired session records");
|
|
2601
|
-
this.scheduleDiskWrite();
|
|
2602
|
-
}
|
|
2603
|
-
}
|
|
2604
|
-
scheduleDiskWrite() {
|
|
2605
|
-
if (this.debounceTimer) clearTimeout(this.debounceTimer);
|
|
2606
|
-
this.debounceTimer = setTimeout(() => {
|
|
2607
|
-
this.flushSync();
|
|
2608
|
-
}, DEBOUNCE_MS);
|
|
2609
|
-
}
|
|
2610
|
-
};
|
|
2611
|
-
|
|
2612
|
-
// src/core/plugin/plugin-loader.ts
|
|
2613
|
-
import { createHash } from "crypto";
|
|
2614
|
-
import { readFileSync } from "fs";
|
|
2615
|
-
function resolveLoadOrder(plugins) {
|
|
2616
|
-
const overrideTargets = /* @__PURE__ */ new Set();
|
|
2617
|
-
for (const p of plugins) {
|
|
2618
|
-
if (p.overrides) {
|
|
2619
|
-
overrideTargets.add(p.overrides);
|
|
2620
|
-
}
|
|
2621
|
-
}
|
|
2622
|
-
let remaining = plugins.filter((p) => !overrideTargets.has(p.name));
|
|
2623
|
-
const byName = /* @__PURE__ */ new Map();
|
|
2624
|
-
for (const p of remaining) {
|
|
2625
|
-
byName.set(p.name, p);
|
|
2626
|
-
}
|
|
2627
|
-
const skipped = /* @__PURE__ */ new Set();
|
|
2628
|
-
function cascadeSkip(name) {
|
|
2629
|
-
if (skipped.has(name)) return;
|
|
2630
|
-
skipped.add(name);
|
|
2631
|
-
for (const p of remaining) {
|
|
2632
|
-
if (p.pluginDependencies && name in p.pluginDependencies) {
|
|
2633
|
-
cascadeSkip(p.name);
|
|
2634
|
-
}
|
|
2635
|
-
}
|
|
2636
|
-
}
|
|
2637
|
-
for (const p of remaining) {
|
|
2638
|
-
if (p.pluginDependencies) {
|
|
2639
|
-
for (const dep of Object.keys(p.pluginDependencies)) {
|
|
2640
|
-
if (!byName.has(dep)) {
|
|
2641
|
-
cascadeSkip(p.name);
|
|
2642
|
-
}
|
|
2643
|
-
}
|
|
2644
|
-
}
|
|
2645
|
-
}
|
|
2646
|
-
remaining = remaining.filter((p) => !skipped.has(p.name));
|
|
2647
|
-
byName.clear();
|
|
2648
|
-
for (const p of remaining) {
|
|
2649
|
-
byName.set(p.name, p);
|
|
2650
|
-
}
|
|
2651
|
-
const visited = /* @__PURE__ */ new Set();
|
|
2652
|
-
const inStack = /* @__PURE__ */ new Set();
|
|
2653
|
-
const order = [];
|
|
2654
|
-
function visit(name) {
|
|
2655
|
-
if (visited.has(name)) return;
|
|
2656
|
-
if (inStack.has(name)) {
|
|
2657
|
-
throw new Error(`Circular dependency detected: ${name}`);
|
|
2658
|
-
}
|
|
2659
|
-
inStack.add(name);
|
|
2660
|
-
const plugin = byName.get(name);
|
|
2661
|
-
if (plugin.pluginDependencies) {
|
|
2662
|
-
for (const dep of Object.keys(plugin.pluginDependencies)) {
|
|
2663
|
-
visit(dep);
|
|
2664
|
-
}
|
|
2665
|
-
}
|
|
2666
|
-
inStack.delete(name);
|
|
2667
|
-
visited.add(name);
|
|
2668
|
-
order.push(plugin);
|
|
2669
|
-
}
|
|
2670
|
-
for (const p of remaining) {
|
|
2671
|
-
visit(p.name);
|
|
2672
|
-
}
|
|
2673
|
-
return order;
|
|
2674
|
-
}
|
|
2675
|
-
|
|
2676
|
-
// src/core/plugin/service-registry.ts
|
|
2677
|
-
var ServiceRegistry = class {
|
|
2678
|
-
services = /* @__PURE__ */ new Map();
|
|
2679
|
-
register(name, implementation, pluginName) {
|
|
2680
|
-
if (this.services.has(name)) {
|
|
2681
|
-
const existing = this.services.get(name);
|
|
2682
|
-
throw new Error(`Service '${name}' already registered by ${existing.pluginName}. Plugin ${pluginName} cannot register it without override.`);
|
|
2683
|
-
}
|
|
2684
|
-
this.services.set(name, { implementation, pluginName });
|
|
2685
|
-
}
|
|
2686
|
-
registerOverride(name, implementation, pluginName) {
|
|
2687
|
-
this.services.set(name, { implementation, pluginName });
|
|
2688
|
-
}
|
|
2689
|
-
get(name) {
|
|
2690
|
-
return this.services.get(name)?.implementation;
|
|
2691
|
-
}
|
|
2692
|
-
has(name) {
|
|
2693
|
-
return this.services.has(name);
|
|
2694
|
-
}
|
|
2695
|
-
list() {
|
|
2696
|
-
return [...this.services.entries()].map(([name, { pluginName }]) => ({ name, pluginName }));
|
|
2697
|
-
}
|
|
2698
|
-
unregister(name) {
|
|
2699
|
-
this.services.delete(name);
|
|
2700
|
-
}
|
|
2701
|
-
unregisterByPlugin(pluginName) {
|
|
2702
|
-
for (const [name, entry] of this.services) {
|
|
2703
|
-
if (entry.pluginName === pluginName) {
|
|
2704
|
-
this.services.delete(name);
|
|
2705
|
-
}
|
|
2706
|
-
}
|
|
2707
|
-
}
|
|
2708
|
-
};
|
|
2709
|
-
|
|
2710
|
-
// src/core/plugin/middleware-chain.ts
|
|
2711
|
-
var MIDDLEWARE_TIMEOUT_MS = 5e3;
|
|
2712
|
-
var MiddlewareChain = class {
|
|
2713
|
-
chains = /* @__PURE__ */ new Map();
|
|
2714
|
-
errorHandler;
|
|
2715
|
-
errorTracker;
|
|
2716
|
-
add(hook, pluginName, opts) {
|
|
2717
|
-
const entry = {
|
|
2718
|
-
pluginName,
|
|
2719
|
-
priority: opts.priority ?? 100,
|
|
2720
|
-
handler: opts.handler
|
|
2721
|
-
};
|
|
2722
|
-
const existing = this.chains.get(hook);
|
|
2723
|
-
if (existing) {
|
|
2724
|
-
existing.push(entry);
|
|
2725
|
-
existing.sort((a, b) => a.priority - b.priority);
|
|
2726
|
-
} else {
|
|
2727
|
-
this.chains.set(hook, [entry]);
|
|
2728
|
-
}
|
|
2729
|
-
}
|
|
2730
|
-
async execute(hook, payload, coreHandler) {
|
|
2731
|
-
const handlers = this.chains.get(hook);
|
|
2732
|
-
if (!handlers || handlers.length === 0) {
|
|
2733
|
-
return coreHandler(payload);
|
|
2734
|
-
}
|
|
2735
|
-
const sorted = handlers;
|
|
2736
|
-
let cachedResult = void 0;
|
|
2737
|
-
const buildNext = (index, currentPayload) => {
|
|
2738
|
-
return async () => {
|
|
2739
|
-
if (cachedResult !== void 0) {
|
|
2740
|
-
return cachedResult.value;
|
|
2741
|
-
}
|
|
2742
|
-
if (index >= sorted.length) {
|
|
2743
|
-
const result = await coreHandler(currentPayload);
|
|
2744
|
-
cachedResult = { value: result };
|
|
2745
|
-
return result;
|
|
2746
|
-
}
|
|
2747
|
-
const entry = sorted[index];
|
|
2748
|
-
if (this.errorTracker?.isDisabled(entry.pluginName)) {
|
|
2749
|
-
const skipFn = buildNext(index + 1, currentPayload);
|
|
2750
|
-
return skipFn();
|
|
2751
|
-
}
|
|
2752
|
-
const nextFn = buildNext(index + 1, currentPayload);
|
|
2753
|
-
let nextCalled = false;
|
|
2754
|
-
let nextResult = null;
|
|
2755
|
-
const wrappedNext = async (newPayload) => {
|
|
2756
|
-
if (!nextCalled) {
|
|
2757
|
-
nextCalled = true;
|
|
2758
|
-
const payloadForNext = newPayload !== void 0 ? newPayload : currentPayload;
|
|
2759
|
-
const newNextFn = buildNext(index + 1, payloadForNext);
|
|
2760
|
-
nextResult = await newNextFn();
|
|
2761
|
-
}
|
|
2762
|
-
return nextResult;
|
|
2763
|
-
};
|
|
2764
|
-
let handlerResult;
|
|
2765
|
-
let timeoutTimer;
|
|
2766
|
-
try {
|
|
2767
|
-
const timeoutPromise = new Promise((_, reject) => {
|
|
2768
|
-
timeoutTimer = setTimeout(
|
|
2769
|
-
() => reject(new Error(`Middleware timeout: ${entry.pluginName} on hook ${hook}`)),
|
|
2770
|
-
MIDDLEWARE_TIMEOUT_MS
|
|
2771
|
-
);
|
|
2772
|
-
if (typeof timeoutTimer === "object" && timeoutTimer !== null && "unref" in timeoutTimer) {
|
|
2773
|
-
;
|
|
2774
|
-
timeoutTimer.unref();
|
|
2775
|
-
}
|
|
2776
|
-
});
|
|
2777
|
-
handlerResult = await Promise.race([
|
|
2778
|
-
entry.handler(currentPayload, wrappedNext),
|
|
2779
|
-
timeoutPromise
|
|
2780
|
-
]);
|
|
2781
|
-
} catch (err) {
|
|
2782
|
-
if (this.errorHandler) {
|
|
2783
|
-
this.errorHandler(entry.pluginName, err instanceof Error ? err : new Error(String(err)));
|
|
2784
|
-
}
|
|
2785
|
-
this.errorTracker?.increment(entry.pluginName);
|
|
2786
|
-
return nextFn();
|
|
2787
|
-
} finally {
|
|
2788
|
-
clearTimeout(timeoutTimer);
|
|
2789
|
-
}
|
|
2790
|
-
if (handlerResult === null) {
|
|
2791
|
-
return null;
|
|
2792
|
-
}
|
|
2793
|
-
return handlerResult;
|
|
2794
|
-
};
|
|
2795
|
-
};
|
|
2796
|
-
const start = buildNext(0, payload);
|
|
2797
|
-
return start();
|
|
2798
|
-
}
|
|
2799
|
-
removeAll(pluginName) {
|
|
2800
|
-
for (const [hook, handlers] of this.chains.entries()) {
|
|
2801
|
-
const filtered = handlers.filter((h) => h.pluginName !== pluginName);
|
|
2802
|
-
if (filtered.length === 0) {
|
|
2803
|
-
this.chains.delete(hook);
|
|
2804
|
-
} else {
|
|
2805
|
-
this.chains.set(hook, filtered);
|
|
2806
|
-
}
|
|
2807
|
-
}
|
|
2808
|
-
}
|
|
2809
|
-
setErrorHandler(fn) {
|
|
2810
|
-
this.errorHandler = fn;
|
|
2811
|
-
}
|
|
2812
|
-
setErrorTracker(tracker) {
|
|
2813
|
-
this.errorTracker = tracker;
|
|
2814
|
-
}
|
|
2815
|
-
};
|
|
2816
|
-
|
|
2817
|
-
// src/core/plugin/error-tracker.ts
|
|
2818
|
-
var ErrorTracker = class {
|
|
2819
|
-
errors = /* @__PURE__ */ new Map();
|
|
2820
|
-
disabled = /* @__PURE__ */ new Set();
|
|
2821
|
-
exempt = /* @__PURE__ */ new Set();
|
|
2822
|
-
config;
|
|
2823
|
-
onDisabled;
|
|
2824
|
-
constructor(config) {
|
|
2825
|
-
this.config = { maxErrors: config?.maxErrors ?? 10, windowMs: config?.windowMs ?? 36e5 };
|
|
2826
|
-
}
|
|
2827
|
-
increment(pluginName) {
|
|
2828
|
-
if (this.exempt.has(pluginName)) return;
|
|
2829
|
-
const now = Date.now();
|
|
2830
|
-
const entry = this.errors.get(pluginName);
|
|
2831
|
-
if (!entry || now - entry.windowStart >= this.config.windowMs) {
|
|
2832
|
-
this.errors.set(pluginName, { count: 1, windowStart: now });
|
|
2833
|
-
} else {
|
|
2834
|
-
entry.count += 1;
|
|
2835
|
-
}
|
|
2836
|
-
const current = this.errors.get(pluginName);
|
|
2837
|
-
if (current.count >= this.config.maxErrors && !this.disabled.has(pluginName)) {
|
|
2838
|
-
this.disabled.add(pluginName);
|
|
2839
|
-
const reason = `Error budget exceeded: ${current.count} errors within ${this.config.windowMs}ms window`;
|
|
2840
|
-
this.onDisabled?.(pluginName, reason);
|
|
2841
|
-
}
|
|
2842
|
-
}
|
|
2843
|
-
isDisabled(pluginName) {
|
|
2844
|
-
return this.disabled.has(pluginName);
|
|
2845
|
-
}
|
|
2846
|
-
reset(pluginName) {
|
|
2847
|
-
this.disabled.delete(pluginName);
|
|
2848
|
-
this.errors.delete(pluginName);
|
|
2849
|
-
}
|
|
2850
|
-
setExempt(pluginName) {
|
|
2851
|
-
this.exempt.add(pluginName);
|
|
2852
|
-
}
|
|
2853
|
-
};
|
|
2854
|
-
|
|
2855
|
-
// src/core/plugin/plugin-context.ts
|
|
2856
|
-
import path5 from "path";
|
|
2857
|
-
import os from "os";
|
|
2858
|
-
|
|
2859
|
-
// src/core/plugin/plugin-storage.ts
|
|
2860
|
-
import fs5 from "fs";
|
|
2861
|
-
import path4 from "path";
|
|
2862
|
-
var PluginStorageImpl = class {
|
|
2863
|
-
kvPath;
|
|
2864
|
-
dataDir;
|
|
2865
|
-
writeChain = Promise.resolve();
|
|
2866
|
-
constructor(baseDir) {
|
|
2867
|
-
this.dataDir = path4.join(baseDir, "data");
|
|
2868
|
-
this.kvPath = path4.join(baseDir, "kv.json");
|
|
2869
|
-
fs5.mkdirSync(baseDir, { recursive: true });
|
|
2870
|
-
}
|
|
2871
|
-
readKv() {
|
|
2872
|
-
try {
|
|
2873
|
-
const raw = fs5.readFileSync(this.kvPath, "utf-8");
|
|
2874
|
-
return JSON.parse(raw);
|
|
2875
|
-
} catch {
|
|
2876
|
-
return {};
|
|
2877
|
-
}
|
|
2878
|
-
}
|
|
2879
|
-
writeKv(data) {
|
|
2880
|
-
fs5.writeFileSync(this.kvPath, JSON.stringify(data), "utf-8");
|
|
2881
|
-
}
|
|
2882
|
-
async get(key) {
|
|
2883
|
-
const data = this.readKv();
|
|
2884
|
-
return key in data ? data[key] : void 0;
|
|
2885
|
-
}
|
|
2886
|
-
async set(key, value) {
|
|
2887
|
-
this.writeChain = this.writeChain.then(() => {
|
|
2888
|
-
const data = this.readKv();
|
|
2889
|
-
data[key] = value;
|
|
2890
|
-
this.writeKv(data);
|
|
2891
|
-
});
|
|
2892
|
-
return this.writeChain;
|
|
2893
|
-
}
|
|
2894
|
-
async delete(key) {
|
|
2895
|
-
this.writeChain = this.writeChain.then(() => {
|
|
2896
|
-
const data = this.readKv();
|
|
2897
|
-
delete data[key];
|
|
2898
|
-
this.writeKv(data);
|
|
2899
|
-
});
|
|
2900
|
-
return this.writeChain;
|
|
2901
|
-
}
|
|
2902
|
-
async list() {
|
|
2903
|
-
return Object.keys(this.readKv());
|
|
2904
|
-
}
|
|
2905
|
-
getDataDir() {
|
|
2906
|
-
fs5.mkdirSync(this.dataDir, { recursive: true });
|
|
2907
|
-
return this.dataDir;
|
|
2908
|
-
}
|
|
2909
|
-
};
|
|
2910
|
-
|
|
2911
|
-
// src/core/plugin/plugin-context.ts
|
|
2912
|
-
function requirePermission(permissions, required, action) {
|
|
2913
|
-
if (!permissions.includes(required)) {
|
|
2914
|
-
throw new Error(`Plugin does not have '${required}' permission required for ${action}`);
|
|
2915
|
-
}
|
|
2916
|
-
}
|
|
2917
|
-
function createPluginContext(opts) {
|
|
2918
|
-
const {
|
|
2919
|
-
pluginName,
|
|
2920
|
-
pluginConfig,
|
|
2921
|
-
permissions,
|
|
2922
|
-
serviceRegistry,
|
|
2923
|
-
middlewareChain,
|
|
2924
|
-
eventBus,
|
|
2925
|
-
storagePath,
|
|
2926
|
-
sessions,
|
|
2927
|
-
config,
|
|
2928
|
-
core
|
|
2929
|
-
} = opts;
|
|
2930
|
-
const instanceRoot = opts.instanceRoot ?? path5.join(os.homedir(), ".openacp");
|
|
2931
|
-
const registeredListeners = [];
|
|
2932
|
-
const registeredCommands = [];
|
|
2933
|
-
const noopLog = {
|
|
2934
|
-
trace() {
|
|
2935
|
-
},
|
|
2936
|
-
debug() {
|
|
2937
|
-
},
|
|
2938
|
-
info() {
|
|
2939
|
-
},
|
|
2940
|
-
warn() {
|
|
2941
|
-
},
|
|
2942
|
-
error() {
|
|
2943
|
-
},
|
|
2944
|
-
fatal() {
|
|
2945
|
-
},
|
|
2946
|
-
child() {
|
|
2947
|
-
return noopLog;
|
|
2948
|
-
}
|
|
2949
|
-
};
|
|
2950
|
-
const baseLog = opts.log ?? noopLog;
|
|
2951
|
-
const log7 = typeof baseLog.child === "function" ? baseLog.child({ plugin: pluginName }) : baseLog;
|
|
2952
|
-
const storageImpl = new PluginStorageImpl(storagePath);
|
|
2953
|
-
const storage = {
|
|
2954
|
-
async get(key) {
|
|
2955
|
-
requirePermission(permissions, "storage:read", "storage.get");
|
|
2956
|
-
return storageImpl.get(key);
|
|
2957
|
-
},
|
|
2958
|
-
async set(key, value) {
|
|
2959
|
-
requirePermission(permissions, "storage:write", "storage.set");
|
|
2960
|
-
return storageImpl.set(key, value);
|
|
2961
|
-
},
|
|
2962
|
-
async delete(key) {
|
|
2963
|
-
requirePermission(permissions, "storage:write", "storage.delete");
|
|
2964
|
-
return storageImpl.delete(key);
|
|
2965
|
-
},
|
|
2966
|
-
async list() {
|
|
2967
|
-
requirePermission(permissions, "storage:read", "storage.list");
|
|
2968
|
-
return storageImpl.list();
|
|
2969
|
-
},
|
|
2970
|
-
getDataDir() {
|
|
2971
|
-
requirePermission(permissions, "storage:read", "storage.getDataDir");
|
|
2972
|
-
return storageImpl.getDataDir();
|
|
2973
|
-
}
|
|
2974
|
-
};
|
|
2975
|
-
const ctx = {
|
|
2976
|
-
pluginName,
|
|
2977
|
-
pluginConfig,
|
|
2978
|
-
log: log7,
|
|
2979
|
-
storage,
|
|
2980
|
-
on(event, handler) {
|
|
2981
|
-
requirePermission(permissions, "events:read", "on()");
|
|
2982
|
-
eventBus.on(event, handler);
|
|
2983
|
-
registeredListeners.push({ event, handler });
|
|
2984
|
-
},
|
|
2985
|
-
off(event, handler) {
|
|
2986
|
-
requirePermission(permissions, "events:read", "off()");
|
|
2987
|
-
eventBus.off(event, handler);
|
|
2988
|
-
const idx = registeredListeners.findIndex((l) => l.event === event && l.handler === handler);
|
|
2989
|
-
if (idx >= 0) registeredListeners.splice(idx, 1);
|
|
2990
|
-
},
|
|
2991
|
-
emit(event, payload) {
|
|
2992
|
-
requirePermission(permissions, "events:emit", "emit()");
|
|
2993
|
-
eventBus.emit(event, payload);
|
|
2994
|
-
},
|
|
2995
|
-
registerMiddleware(hook, middlewareOpts) {
|
|
2996
|
-
requirePermission(permissions, "middleware:register", "registerMiddleware()");
|
|
2997
|
-
middlewareChain.add(hook, pluginName, middlewareOpts);
|
|
2998
|
-
},
|
|
2999
|
-
registerService(name, implementation) {
|
|
3000
|
-
requirePermission(permissions, "services:register", "registerService()");
|
|
3001
|
-
serviceRegistry.register(name, implementation, pluginName);
|
|
3002
|
-
},
|
|
3003
|
-
getService(name) {
|
|
3004
|
-
requirePermission(permissions, "services:use", "getService()");
|
|
3005
|
-
return serviceRegistry.get(name);
|
|
3006
|
-
},
|
|
3007
|
-
registerCommand(def) {
|
|
3008
|
-
requirePermission(permissions, "commands:register", "registerCommand()");
|
|
3009
|
-
registeredCommands.push(def);
|
|
3010
|
-
const registry = serviceRegistry.get("command-registry");
|
|
3011
|
-
if (registry && typeof registry.register === "function") {
|
|
3012
|
-
registry.register(def, pluginName);
|
|
3013
|
-
log7.debug(`Command '/${def.name}' registered`);
|
|
3014
|
-
}
|
|
3015
|
-
},
|
|
3016
|
-
async sendMessage(_sessionId, _content) {
|
|
3017
|
-
requirePermission(permissions, "services:use", "sendMessage()");
|
|
3018
|
-
const router = serviceRegistry.get("message-router");
|
|
3019
|
-
if (router) {
|
|
3020
|
-
await router.send(_sessionId, _content);
|
|
3021
|
-
}
|
|
3022
|
-
},
|
|
3023
|
-
get sessions() {
|
|
3024
|
-
requirePermission(permissions, "kernel:access", "sessions");
|
|
3025
|
-
return sessions;
|
|
3026
|
-
},
|
|
3027
|
-
get config() {
|
|
3028
|
-
requirePermission(permissions, "kernel:access", "config");
|
|
3029
|
-
return config;
|
|
3030
|
-
},
|
|
3031
|
-
get eventBus() {
|
|
3032
|
-
requirePermission(permissions, "kernel:access", "eventBus");
|
|
3033
|
-
return eventBus;
|
|
3034
|
-
},
|
|
3035
|
-
get core() {
|
|
3036
|
-
requirePermission(permissions, "kernel:access", "core");
|
|
3037
|
-
return core;
|
|
3038
|
-
},
|
|
3039
|
-
instanceRoot,
|
|
3040
|
-
cleanup() {
|
|
3041
|
-
for (const { event, handler } of registeredListeners) {
|
|
3042
|
-
eventBus.off(event, handler);
|
|
3043
|
-
}
|
|
3044
|
-
registeredListeners.length = 0;
|
|
3045
|
-
middlewareChain.removeAll(pluginName);
|
|
3046
|
-
serviceRegistry.unregisterByPlugin(pluginName);
|
|
3047
|
-
const cmdRegistry = serviceRegistry.get("command-registry");
|
|
3048
|
-
if (cmdRegistry && typeof cmdRegistry.unregisterByPlugin === "function") {
|
|
3049
|
-
cmdRegistry.unregisterByPlugin(pluginName);
|
|
3050
|
-
}
|
|
3051
|
-
registeredCommands.length = 0;
|
|
3052
|
-
}
|
|
3053
|
-
};
|
|
3054
|
-
return ctx;
|
|
3055
|
-
}
|
|
3056
|
-
|
|
3057
|
-
// src/core/plugin/lifecycle-manager.ts
|
|
3058
|
-
var SETUP_TIMEOUT_MS = 3e4;
|
|
3059
|
-
var TEARDOWN_TIMEOUT_MS = 1e4;
|
|
3060
|
-
function withTimeout(promise, ms, label) {
|
|
3061
|
-
return new Promise((resolve, reject) => {
|
|
3062
|
-
const timer = setTimeout(() => reject(new Error(`Timeout: ${label} exceeded ${ms}ms`)), ms);
|
|
3063
|
-
if (typeof timer === "object" && timer !== null && "unref" in timer) {
|
|
3064
|
-
;
|
|
3065
|
-
timer.unref();
|
|
3066
|
-
}
|
|
3067
|
-
promise.then(resolve, reject).finally(() => clearTimeout(timer));
|
|
3068
|
-
});
|
|
3069
|
-
}
|
|
3070
|
-
function resolvePluginConfig(pluginName, configManager) {
|
|
3071
|
-
try {
|
|
3072
|
-
const allConfig = configManager?.get?.() ?? {};
|
|
3073
|
-
const pluginEntry = allConfig.plugins?.builtin?.[pluginName];
|
|
3074
|
-
if (pluginEntry?.config && Object.keys(pluginEntry.config).length > 0) {
|
|
3075
|
-
return pluginEntry.config;
|
|
3076
|
-
}
|
|
3077
|
-
const legacyMap = {
|
|
3078
|
-
"@openacp/security": "security",
|
|
3079
|
-
"@openacp/speech": "speech",
|
|
3080
|
-
"@openacp/tunnel": "tunnel",
|
|
3081
|
-
"@openacp/usage": "usage",
|
|
3082
|
-
"@openacp/file-service": "files",
|
|
3083
|
-
"@openacp/api-server": "api",
|
|
3084
|
-
"@openacp/telegram": "channels.telegram",
|
|
3085
|
-
"@openacp/discord": "channels.discord",
|
|
3086
|
-
"@openacp/adapter-discord": "channels.discord",
|
|
3087
|
-
"@openacp/plugin-discord": "channels.discord",
|
|
3088
|
-
// alias for old name
|
|
3089
|
-
"@openacp/slack": "channels.slack"
|
|
3090
|
-
};
|
|
3091
|
-
const legacyKey = legacyMap[pluginName];
|
|
3092
|
-
if (legacyKey) {
|
|
3093
|
-
const parts = legacyKey.split(".");
|
|
3094
|
-
let obj = allConfig;
|
|
3095
|
-
for (const p of parts) obj = obj?.[p];
|
|
3096
|
-
if (obj && typeof obj === "object") return { ...obj };
|
|
3097
|
-
}
|
|
3098
|
-
} catch {
|
|
3099
|
-
}
|
|
3100
|
-
return {};
|
|
3101
|
-
}
|
|
3102
|
-
var LifecycleManager = class {
|
|
3103
|
-
serviceRegistry;
|
|
3104
|
-
middlewareChain;
|
|
3105
|
-
errorTracker;
|
|
3106
|
-
eventBus;
|
|
3107
|
-
storagePath;
|
|
3108
|
-
sessions;
|
|
3109
|
-
config;
|
|
3110
|
-
core;
|
|
3111
|
-
log;
|
|
3112
|
-
settingsManager;
|
|
3113
|
-
pluginRegistry;
|
|
3114
|
-
instanceRoot;
|
|
3115
|
-
contexts = /* @__PURE__ */ new Map();
|
|
3116
|
-
loadOrder = [];
|
|
3117
|
-
_loaded = /* @__PURE__ */ new Set();
|
|
3118
|
-
_failed = /* @__PURE__ */ new Set();
|
|
3119
|
-
get loadedPlugins() {
|
|
3120
|
-
return [...this._loaded];
|
|
3121
|
-
}
|
|
3122
|
-
get failedPlugins() {
|
|
3123
|
-
return [...this._failed];
|
|
3124
|
-
}
|
|
3125
|
-
get registry() {
|
|
3126
|
-
return this.pluginRegistry;
|
|
3127
|
-
}
|
|
3128
|
-
constructor(opts) {
|
|
3129
|
-
this.serviceRegistry = opts?.serviceRegistry ?? new ServiceRegistry();
|
|
3130
|
-
this.middlewareChain = opts?.middlewareChain ?? new MiddlewareChain();
|
|
3131
|
-
this.errorTracker = opts?.errorTracker ?? new ErrorTracker();
|
|
3132
|
-
this.eventBus = opts?.eventBus ?? {
|
|
3133
|
-
on() {
|
|
3134
|
-
},
|
|
3135
|
-
off() {
|
|
3136
|
-
},
|
|
3137
|
-
emit() {
|
|
3138
|
-
}
|
|
3139
|
-
};
|
|
3140
|
-
this.storagePath = opts?.storagePath ?? "/tmp/openacp-plugins";
|
|
3141
|
-
this.sessions = opts?.sessions ?? {};
|
|
3142
|
-
this.config = opts?.config ?? {};
|
|
3143
|
-
this.core = opts?.core;
|
|
3144
|
-
this.log = opts?.log;
|
|
3145
|
-
this.settingsManager = opts?.settingsManager;
|
|
3146
|
-
this.pluginRegistry = opts?.pluginRegistry;
|
|
3147
|
-
this.instanceRoot = opts?.instanceRoot;
|
|
3148
|
-
}
|
|
3149
|
-
getPluginLogger(pluginName) {
|
|
3150
|
-
if (this.log && typeof this.log.child === "function") {
|
|
3151
|
-
return this.log.child({ plugin: pluginName });
|
|
3152
|
-
}
|
|
3153
|
-
return this.log ?? { trace() {
|
|
3154
|
-
}, debug() {
|
|
3155
|
-
}, info() {
|
|
3156
|
-
}, warn() {
|
|
3157
|
-
}, error() {
|
|
3158
|
-
}, fatal() {
|
|
3159
|
-
}, child() {
|
|
3160
|
-
return this;
|
|
3161
|
-
} };
|
|
3162
|
-
}
|
|
3163
|
-
async boot(plugins) {
|
|
3164
|
-
const newNames = new Set(plugins.map((p) => p.name));
|
|
3165
|
-
const allForResolution = [...this.loadOrder.filter((p) => !newNames.has(p.name)), ...plugins];
|
|
3166
|
-
let sorted;
|
|
3167
|
-
try {
|
|
3168
|
-
sorted = resolveLoadOrder(allForResolution);
|
|
3169
|
-
} catch (err) {
|
|
3170
|
-
const error = err instanceof Error ? err : new Error(String(err));
|
|
3171
|
-
this.log?.error(`Plugin dependency resolution failed: ${error.message}`);
|
|
3172
|
-
for (const p of plugins) {
|
|
3173
|
-
this._failed.add(p.name);
|
|
3174
|
-
}
|
|
3175
|
-
return;
|
|
3176
|
-
}
|
|
3177
|
-
sorted = sorted.filter((p) => newNames.has(p.name));
|
|
3178
|
-
for (const p of sorted) {
|
|
3179
|
-
if (!this.loadOrder.some((existing) => existing.name === p.name)) {
|
|
3180
|
-
this.loadOrder.push(p);
|
|
3181
|
-
} else {
|
|
3182
|
-
const idx = this.loadOrder.findIndex((existing) => existing.name === p.name);
|
|
3183
|
-
this.loadOrder[idx] = p;
|
|
3184
|
-
}
|
|
3185
|
-
}
|
|
3186
|
-
for (const plugin of sorted) {
|
|
3187
|
-
if (plugin.pluginDependencies) {
|
|
3188
|
-
const depFailed = Object.keys(plugin.pluginDependencies).some(
|
|
3189
|
-
(dep) => this._failed.has(dep)
|
|
3190
|
-
);
|
|
3191
|
-
if (depFailed) {
|
|
3192
|
-
this._failed.add(plugin.name);
|
|
3193
|
-
continue;
|
|
3194
|
-
}
|
|
3195
|
-
}
|
|
3196
|
-
const registryEntry = this.pluginRegistry?.get(plugin.name);
|
|
3197
|
-
if (registryEntry && registryEntry.enabled === false) {
|
|
3198
|
-
this.eventBus?.emit("plugin:disabled", { name: plugin.name });
|
|
3199
|
-
continue;
|
|
3200
|
-
}
|
|
3201
|
-
if (registryEntry && plugin.migrate && registryEntry.version !== plugin.version && this.settingsManager) {
|
|
3202
|
-
try {
|
|
3203
|
-
const oldSettings = await this.settingsManager.loadSettings(plugin.name);
|
|
3204
|
-
const pluginLog = this.getPluginLogger(plugin.name);
|
|
3205
|
-
const migrateCtx = {
|
|
3206
|
-
pluginName: plugin.name,
|
|
3207
|
-
settings: this.settingsManager.createAPI(plugin.name),
|
|
3208
|
-
log: pluginLog
|
|
3209
|
-
};
|
|
3210
|
-
const newSettings = await withTimeout(
|
|
3211
|
-
plugin.migrate(migrateCtx, oldSettings, registryEntry.version),
|
|
3212
|
-
SETUP_TIMEOUT_MS,
|
|
3213
|
-
`${plugin.name}.migrate()`
|
|
3214
|
-
);
|
|
3215
|
-
if (newSettings && typeof newSettings === "object") {
|
|
3216
|
-
await migrateCtx.settings.setAll(newSettings);
|
|
3217
|
-
}
|
|
3218
|
-
this.pluginRegistry.updateVersion(plugin.name, plugin.version);
|
|
3219
|
-
await this.pluginRegistry.save();
|
|
3220
|
-
} catch (err) {
|
|
3221
|
-
this.getPluginLogger(plugin.name).warn(`Migration failed, continuing with old settings: ${err}`);
|
|
3222
|
-
}
|
|
3223
|
-
}
|
|
3224
|
-
let pluginConfig;
|
|
3225
|
-
if (this.settingsManager) {
|
|
3226
|
-
pluginConfig = await this.settingsManager.loadSettings(plugin.name);
|
|
3227
|
-
const settingsPath = this.settingsManager.getSettingsPath(plugin.name);
|
|
3228
|
-
this.getPluginLogger(plugin.name).debug(`Settings loaded from ${settingsPath}: ${Object.keys(pluginConfig).length} keys`);
|
|
3229
|
-
if (Object.keys(pluginConfig).length === 0) {
|
|
3230
|
-
pluginConfig = resolvePluginConfig(plugin.name, this.config);
|
|
3231
|
-
}
|
|
3232
|
-
} else {
|
|
3233
|
-
pluginConfig = resolvePluginConfig(plugin.name, this.config);
|
|
3234
|
-
this.getPluginLogger(plugin.name).debug("No settingsManager, using legacy config");
|
|
3235
|
-
}
|
|
3236
|
-
if (plugin.settingsSchema && this.settingsManager) {
|
|
3237
|
-
const validation = this.settingsManager.validateSettings(plugin.name, pluginConfig, plugin.settingsSchema);
|
|
3238
|
-
if (!validation.valid) {
|
|
3239
|
-
this._failed.add(plugin.name);
|
|
3240
|
-
this.getPluginLogger(plugin.name).error(`Settings validation failed: ${validation.errors?.join("; ")}`);
|
|
3241
|
-
this.eventBus?.emit("plugin:failed", { name: plugin.name, error: `Settings validation failed: ${validation.errors?.join("; ")}` });
|
|
3242
|
-
continue;
|
|
3243
|
-
}
|
|
3244
|
-
}
|
|
3245
|
-
const ctx = createPluginContext({
|
|
3246
|
-
pluginName: plugin.name,
|
|
3247
|
-
pluginConfig,
|
|
3248
|
-
permissions: plugin.permissions ?? [],
|
|
3249
|
-
serviceRegistry: this.serviceRegistry,
|
|
3250
|
-
middlewareChain: this.middlewareChain,
|
|
3251
|
-
errorTracker: this.errorTracker,
|
|
3252
|
-
eventBus: this.eventBus,
|
|
3253
|
-
storagePath: `${this.storagePath}/${plugin.name}`,
|
|
3254
|
-
sessions: this.sessions,
|
|
3255
|
-
config: this.config,
|
|
3256
|
-
core: this.core,
|
|
3257
|
-
log: this.log,
|
|
3258
|
-
instanceRoot: this.instanceRoot
|
|
3259
|
-
});
|
|
3260
|
-
try {
|
|
3261
|
-
await withTimeout(plugin.setup(ctx), SETUP_TIMEOUT_MS, `${plugin.name}.setup()`);
|
|
3262
|
-
this.contexts.set(plugin.name, ctx);
|
|
3263
|
-
this._loaded.add(plugin.name);
|
|
3264
|
-
this.eventBus?.emit("plugin:loaded", { name: plugin.name, version: plugin.version });
|
|
3265
|
-
} catch (err) {
|
|
3266
|
-
this._failed.add(plugin.name);
|
|
3267
|
-
ctx.cleanup();
|
|
3268
|
-
this.getPluginLogger(plugin.name).error(`setup() failed: ${err}`);
|
|
3269
|
-
this.eventBus?.emit("plugin:failed", { name: plugin.name, error: String(err) });
|
|
3270
|
-
}
|
|
3271
|
-
}
|
|
3272
|
-
}
|
|
3273
|
-
async unloadPlugin(name) {
|
|
3274
|
-
if (!this._loaded.has(name)) return;
|
|
3275
|
-
const plugin = this.loadOrder.find((p) => p.name === name);
|
|
3276
|
-
if (plugin?.teardown) {
|
|
3277
|
-
try {
|
|
3278
|
-
await withTimeout(plugin.teardown(), TEARDOWN_TIMEOUT_MS, `${name}.teardown()`);
|
|
3279
|
-
} catch {
|
|
3280
|
-
}
|
|
3281
|
-
}
|
|
3282
|
-
const ctx = this.contexts.get(name);
|
|
3283
|
-
if (ctx) {
|
|
3284
|
-
ctx.cleanup();
|
|
3285
|
-
this.contexts.delete(name);
|
|
3286
|
-
}
|
|
3287
|
-
this._loaded.delete(name);
|
|
3288
|
-
this._failed.delete(name);
|
|
3289
|
-
this.loadOrder = this.loadOrder.filter((p) => p.name !== name);
|
|
3290
|
-
this.eventBus?.emit("plugin:unloaded", { name });
|
|
3291
|
-
}
|
|
3292
|
-
async shutdown() {
|
|
3293
|
-
const reversed = [...this.loadOrder].reverse();
|
|
3294
|
-
for (const plugin of reversed) {
|
|
3295
|
-
if (!this._loaded.has(plugin.name)) continue;
|
|
3296
|
-
if (plugin.teardown) {
|
|
3297
|
-
try {
|
|
3298
|
-
await withTimeout(plugin.teardown(), TEARDOWN_TIMEOUT_MS, `${plugin.name}.teardown()`);
|
|
3299
|
-
} catch {
|
|
3300
|
-
}
|
|
3301
|
-
}
|
|
3302
|
-
const ctx = this.contexts.get(plugin.name);
|
|
3303
|
-
if (ctx) {
|
|
3304
|
-
ctx.cleanup();
|
|
3305
|
-
this.contexts.delete(plugin.name);
|
|
3306
|
-
}
|
|
3307
|
-
this.eventBus?.emit("plugin:unloaded", { name: plugin.name });
|
|
3308
|
-
}
|
|
3309
|
-
this._loaded.clear();
|
|
3310
|
-
this.loadOrder = [];
|
|
3311
|
-
}
|
|
3312
|
-
};
|
|
3313
|
-
|
|
3314
|
-
// src/core/core.ts
|
|
3315
|
-
var log6 = createChildLogger({ module: "core" });
|
|
3316
|
-
var OpenACPCore = class {
|
|
3317
|
-
configManager;
|
|
3318
|
-
agentCatalog;
|
|
3319
|
-
agentManager;
|
|
3320
|
-
sessionManager;
|
|
3321
|
-
messageTransformer;
|
|
3322
|
-
adapters = /* @__PURE__ */ new Map();
|
|
3323
|
-
/** sessionId → SessionBridge — tracks active bridges for disconnect/reconnect during agent switch */
|
|
3324
|
-
bridges = /* @__PURE__ */ new Map();
|
|
3325
|
-
/** Set by main.ts — triggers graceful shutdown with restart exit code */
|
|
3326
|
-
requestRestart = null;
|
|
3327
|
-
_tunnelService;
|
|
3328
|
-
sessionStore = null;
|
|
3329
|
-
resumeLocks = /* @__PURE__ */ new Map();
|
|
3330
|
-
switchingLocks = /* @__PURE__ */ new Set();
|
|
3331
|
-
eventBus;
|
|
3332
|
-
sessionFactory;
|
|
3333
|
-
lifecycleManager;
|
|
3334
|
-
instanceContext;
|
|
3335
|
-
// --- Lazy getters: resolve from ServiceRegistry (populated by plugins during boot) ---
|
|
3336
|
-
getService(name) {
|
|
3337
|
-
const svc = this.lifecycleManager.serviceRegistry.get(name);
|
|
3338
|
-
if (!svc) throw new Error(`Service '${name}' not registered \u2014 is the ${name} plugin loaded?`);
|
|
3339
|
-
return svc;
|
|
3340
|
-
}
|
|
3341
|
-
get securityGuard() {
|
|
3342
|
-
return this.getService("security");
|
|
3343
|
-
}
|
|
3344
|
-
get notificationManager() {
|
|
3345
|
-
return this.getService("notifications");
|
|
3346
|
-
}
|
|
3347
|
-
get fileService() {
|
|
3348
|
-
return this.getService("file-service");
|
|
3349
|
-
}
|
|
3350
|
-
get speechService() {
|
|
3351
|
-
return this.getService("speech");
|
|
3352
|
-
}
|
|
3353
|
-
get contextManager() {
|
|
3354
|
-
return this.getService("context");
|
|
3355
|
-
}
|
|
3356
|
-
constructor(configManager, ctx) {
|
|
3357
|
-
this.configManager = configManager;
|
|
3358
|
-
this.instanceContext = ctx;
|
|
3359
|
-
const config = configManager.get();
|
|
3360
|
-
this.agentCatalog = new AgentCatalog();
|
|
3361
|
-
this.agentCatalog.load();
|
|
3362
|
-
this.agentManager = new AgentManager(this.agentCatalog);
|
|
3363
|
-
const storePath = ctx?.paths.sessions ?? path6.join(os2.homedir(), ".openacp", "sessions.json");
|
|
3364
|
-
this.sessionStore = new JsonFileSessionStore(
|
|
3365
|
-
storePath,
|
|
3366
|
-
config.sessionStore.ttlDays
|
|
3367
|
-
);
|
|
3368
|
-
this.sessionManager = new SessionManager(this.sessionStore);
|
|
3369
|
-
this.messageTransformer = new MessageTransformer();
|
|
3370
|
-
this.eventBus = new EventBus();
|
|
3371
|
-
this.sessionManager.setEventBus(this.eventBus);
|
|
3372
|
-
this.sessionFactory = new SessionFactory(
|
|
3373
|
-
this.agentManager,
|
|
3374
|
-
this.sessionManager,
|
|
3375
|
-
() => this.speechService,
|
|
3376
|
-
this.eventBus
|
|
3377
|
-
);
|
|
3378
|
-
this.lifecycleManager = new LifecycleManager({
|
|
3379
|
-
serviceRegistry: new ServiceRegistry(),
|
|
3380
|
-
middlewareChain: new MiddlewareChain(),
|
|
3381
|
-
errorTracker: new ErrorTracker(),
|
|
3382
|
-
eventBus: this.eventBus,
|
|
3383
|
-
sessions: this.sessionManager,
|
|
3384
|
-
config: this.configManager,
|
|
3385
|
-
core: this,
|
|
3386
|
-
storagePath: ctx?.paths.pluginsData ?? path6.join(os2.homedir(), ".openacp", "plugins", "data"),
|
|
3387
|
-
log: createChildLogger({ module: "plugin" })
|
|
3388
|
-
});
|
|
3389
|
-
this.sessionFactory.middlewareChain = this.lifecycleManager.middlewareChain;
|
|
3390
|
-
this.sessionManager.middlewareChain = this.lifecycleManager.middlewareChain;
|
|
3391
|
-
this.configManager.on(
|
|
3392
|
-
"config:changed",
|
|
3393
|
-
async ({ path: configPath, value }) => {
|
|
3394
|
-
if (configPath === "logging.level" && typeof value === "string") {
|
|
3395
|
-
const { setLogLevel } = await import("./log-YZ243M5G.js");
|
|
3396
|
-
setLogLevel(value);
|
|
3397
|
-
log6.info({ level: value }, "Log level changed at runtime");
|
|
3398
|
-
}
|
|
3399
|
-
if (configPath.startsWith("speech.")) {
|
|
3400
|
-
const speechSvc = this.speechService;
|
|
3401
|
-
if (speechSvc) {
|
|
3402
|
-
const newConfig = this.configManager.get();
|
|
3403
|
-
const newSpeechConfig = newConfig.speech ?? {
|
|
3404
|
-
stt: { provider: null, providers: {} },
|
|
3405
|
-
tts: { provider: null, providers: {} }
|
|
3406
|
-
};
|
|
3407
|
-
speechSvc.refreshProviders(newSpeechConfig);
|
|
3408
|
-
log6.info("Speech service config updated at runtime");
|
|
3409
|
-
}
|
|
3410
|
-
}
|
|
3411
|
-
}
|
|
3412
|
-
);
|
|
3413
|
-
}
|
|
3414
|
-
get tunnelService() {
|
|
3415
|
-
return this._tunnelService;
|
|
3416
|
-
}
|
|
3417
|
-
set tunnelService(service) {
|
|
3418
|
-
this._tunnelService = service;
|
|
3419
|
-
this.messageTransformer.tunnelService = service;
|
|
3420
|
-
}
|
|
3421
|
-
registerAdapter(name, adapter) {
|
|
3422
|
-
this.adapters.set(name, adapter);
|
|
3423
|
-
}
|
|
3424
|
-
async start() {
|
|
3425
|
-
this.agentCatalog.refreshRegistryIfStale().catch((err) => {
|
|
3426
|
-
log6.warn({ err }, "Background registry refresh failed");
|
|
3427
|
-
});
|
|
3428
|
-
for (const adapter of this.adapters.values()) {
|
|
3429
|
-
await adapter.start();
|
|
3430
|
-
}
|
|
3431
|
-
}
|
|
3432
|
-
async stop() {
|
|
3433
|
-
try {
|
|
3434
|
-
const nm = this.lifecycleManager.serviceRegistry.get("notifications");
|
|
3435
|
-
if (nm) {
|
|
3436
|
-
await nm.notifyAll({
|
|
3437
|
-
sessionId: "system",
|
|
3438
|
-
type: "error",
|
|
3439
|
-
summary: "OpenACP is shutting down"
|
|
3440
|
-
});
|
|
3441
|
-
}
|
|
3442
|
-
} catch {
|
|
3443
|
-
}
|
|
3444
|
-
await this.sessionManager.shutdownAll();
|
|
3445
|
-
for (const adapter of this.adapters.values()) {
|
|
3446
|
-
await adapter.stop();
|
|
3447
|
-
}
|
|
3448
|
-
}
|
|
3449
|
-
// --- Archive ---
|
|
3450
|
-
async archiveSession(sessionId) {
|
|
3451
|
-
const session = this.sessionManager.getSession(sessionId);
|
|
3452
|
-
if (!session) return { ok: false, error: "Session not found (must be in memory)" };
|
|
3453
|
-
if (session.status !== "active" && session.status !== "cancelled" && session.status !== "error") {
|
|
3454
|
-
return { ok: false, error: `Cannot archive session in '${session.status}' state` };
|
|
3455
|
-
}
|
|
3456
|
-
const adapter = this.adapters.get(session.channelId);
|
|
3457
|
-
if (!adapter) return { ok: false, error: "Adapter not found for session" };
|
|
3458
|
-
if (!adapter.archiveSessionTopic) return { ok: false, error: "Adapter does not support topic archiving" };
|
|
3459
|
-
try {
|
|
3460
|
-
const newThreadId = await adapter.archiveSessionTopic(session.id);
|
|
3461
|
-
session.threadId = newThreadId;
|
|
3462
|
-
try {
|
|
3463
|
-
const platform = {};
|
|
3464
|
-
if (session.channelId === "telegram") {
|
|
3465
|
-
platform.topicId = Number(newThreadId);
|
|
3466
|
-
} else {
|
|
3467
|
-
platform.threadId = newThreadId;
|
|
3468
|
-
}
|
|
3469
|
-
await this.sessionManager.patchRecord(sessionId, { platform });
|
|
3470
|
-
} catch (patchErr) {
|
|
3471
|
-
log6.warn({ err: patchErr, sessionId }, "Failed to update session record after archive \u2014 session will work but may not survive restart");
|
|
3472
|
-
}
|
|
3473
|
-
return { ok: true, newThreadId };
|
|
3474
|
-
} catch (err) {
|
|
3475
|
-
session.archiving = false;
|
|
3476
|
-
return { ok: false, error: err.message };
|
|
3477
|
-
}
|
|
3478
|
-
}
|
|
3479
|
-
// --- Message Routing ---
|
|
3480
|
-
async handleMessage(message) {
|
|
3481
|
-
log6.debug(
|
|
3482
|
-
{
|
|
3483
|
-
channelId: message.channelId,
|
|
3484
|
-
threadId: message.threadId,
|
|
3485
|
-
userId: message.userId
|
|
3486
|
-
},
|
|
3487
|
-
"Incoming message"
|
|
3488
|
-
);
|
|
3489
|
-
if (this.lifecycleManager?.middlewareChain) {
|
|
3490
|
-
const result = await this.lifecycleManager.middlewareChain.execute(
|
|
3491
|
-
"message:incoming",
|
|
3492
|
-
message,
|
|
3493
|
-
async (msg) => msg
|
|
3494
|
-
);
|
|
3495
|
-
if (!result) return;
|
|
3496
|
-
message = result;
|
|
3497
|
-
}
|
|
3498
|
-
const access = this.securityGuard.checkAccess(message);
|
|
3499
|
-
if (!access.allowed) {
|
|
3500
|
-
log6.warn({ userId: message.userId, reason: access.reason }, "Access denied");
|
|
3501
|
-
if (access.reason.includes("Session limit")) {
|
|
3502
|
-
const adapter = this.adapters.get(message.channelId);
|
|
3503
|
-
if (adapter) {
|
|
3504
|
-
await adapter.sendMessage(message.threadId, {
|
|
3505
|
-
type: "error",
|
|
3506
|
-
text: `\u26A0\uFE0F ${access.reason}. Please cancel existing sessions with /cancel before starting new ones.`
|
|
3507
|
-
});
|
|
3508
|
-
}
|
|
3509
|
-
}
|
|
3510
|
-
return;
|
|
3511
|
-
}
|
|
3512
|
-
let session = this.sessionManager.getSessionByThread(
|
|
3513
|
-
message.channelId,
|
|
3514
|
-
message.threadId
|
|
3515
|
-
);
|
|
3516
|
-
if (!session) {
|
|
3517
|
-
session = await this.lazyResume(message) ?? void 0;
|
|
3518
|
-
}
|
|
3519
|
-
if (!session) {
|
|
3520
|
-
log6.warn(
|
|
3521
|
-
{ channelId: message.channelId, threadId: message.threadId },
|
|
3522
|
-
"No session found for thread (in-memory miss + lazy resume returned null)"
|
|
3523
|
-
);
|
|
3524
|
-
return;
|
|
3525
|
-
}
|
|
3526
|
-
this.sessionManager.patchRecord(session.id, {
|
|
3527
|
-
lastActiveAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
3528
|
-
});
|
|
3529
|
-
await session.enqueuePrompt(message.text, message.attachments);
|
|
3530
|
-
}
|
|
3531
|
-
// --- Unified Session Creation Pipeline ---
|
|
3532
|
-
async createSession(params) {
|
|
3533
|
-
const session = await this.sessionFactory.create(params);
|
|
3534
|
-
if (params.threadId) {
|
|
3535
|
-
session.threadId = params.threadId;
|
|
3536
|
-
}
|
|
3537
|
-
const adapter = this.adapters.get(params.channelId);
|
|
3538
|
-
if (params.createThread && adapter) {
|
|
3539
|
-
const threadId = await adapter.createSessionThread(
|
|
3540
|
-
session.id,
|
|
3541
|
-
params.initialName ?? `\u{1F504} ${params.agentName} \u2014 New Session`
|
|
3542
|
-
);
|
|
3543
|
-
session.threadId = threadId;
|
|
3544
|
-
}
|
|
3545
|
-
if (adapter) {
|
|
3546
|
-
const bridge = this.createBridge(session, adapter);
|
|
3547
|
-
bridge.connect();
|
|
3548
|
-
}
|
|
3549
|
-
this.sessionFactory.wireSideEffects(session, {
|
|
3550
|
-
eventBus: this.eventBus,
|
|
3551
|
-
notificationManager: this.notificationManager,
|
|
3552
|
-
tunnelService: this._tunnelService
|
|
3553
|
-
});
|
|
3554
|
-
const existingRecord = this.sessionStore?.get(session.id);
|
|
3555
|
-
const platform = {
|
|
3556
|
-
...existingRecord?.platform ?? {}
|
|
3557
|
-
};
|
|
3558
|
-
if (session.threadId) {
|
|
3559
|
-
if (params.channelId === "telegram") {
|
|
3560
|
-
platform.topicId = Number(session.threadId);
|
|
3561
|
-
} else {
|
|
3562
|
-
platform.threadId = session.threadId;
|
|
3563
|
-
}
|
|
3564
|
-
}
|
|
3565
|
-
await this.sessionManager.patchRecord(session.id, {
|
|
3566
|
-
sessionId: session.id,
|
|
3567
|
-
agentSessionId: session.agentSessionId,
|
|
3568
|
-
agentName: params.agentName,
|
|
3569
|
-
workingDir: params.workingDirectory,
|
|
3570
|
-
channelId: params.channelId,
|
|
3571
|
-
status: session.status,
|
|
3572
|
-
createdAt: session.createdAt.toISOString(),
|
|
3573
|
-
lastActiveAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3574
|
-
name: session.name,
|
|
3575
|
-
platform,
|
|
3576
|
-
firstAgent: session.firstAgent,
|
|
3577
|
-
currentPromptCount: session.promptCount,
|
|
3578
|
-
agentSwitchHistory: session.agentSwitchHistory
|
|
3579
|
-
});
|
|
3580
|
-
log6.info(
|
|
3581
|
-
{ sessionId: session.id, agentName: params.agentName },
|
|
3582
|
-
"Session created via pipeline"
|
|
3583
|
-
);
|
|
3584
|
-
return session;
|
|
3585
|
-
}
|
|
3586
|
-
async handleNewSession(channelId, agentName, workspacePath, options) {
|
|
3587
|
-
const config = this.configManager.get();
|
|
3588
|
-
const resolvedAgent = agentName || config.defaultAgent;
|
|
3589
|
-
log6.info({ channelId, agentName: resolvedAgent }, "New session request");
|
|
3590
|
-
const agentDef = this.agentCatalog.resolve(resolvedAgent);
|
|
3591
|
-
const resolvedWorkspace = this.configManager.resolveWorkspace(
|
|
3592
|
-
workspacePath || agentDef?.workingDirectory
|
|
3593
|
-
);
|
|
3594
|
-
return this.createSession({
|
|
3595
|
-
channelId,
|
|
3596
|
-
agentName: resolvedAgent,
|
|
3597
|
-
workingDirectory: resolvedWorkspace,
|
|
3598
|
-
createThread: options?.createThread
|
|
3599
|
-
});
|
|
3600
|
-
}
|
|
3601
|
-
async adoptSession(agentName, agentSessionId, cwd, channelId) {
|
|
3602
|
-
const caps = getAgentCapabilities(agentName);
|
|
3603
|
-
if (!caps.supportsResume) {
|
|
3604
|
-
return {
|
|
3605
|
-
ok: false,
|
|
3606
|
-
error: "agent_not_supported",
|
|
3607
|
-
message: `Agent '${agentName}' does not support session resume`
|
|
3608
|
-
};
|
|
3609
|
-
}
|
|
3610
|
-
const agentDef = this.agentManager.getAgent(agentName);
|
|
3611
|
-
if (!agentDef) {
|
|
3612
|
-
return {
|
|
3613
|
-
ok: false,
|
|
3614
|
-
error: "agent_not_supported",
|
|
3615
|
-
message: `Agent '${agentName}' not found`
|
|
3616
|
-
};
|
|
3617
|
-
}
|
|
3618
|
-
const { existsSync } = await import("fs");
|
|
3619
|
-
if (!existsSync(cwd)) {
|
|
3620
|
-
return {
|
|
3621
|
-
ok: false,
|
|
3622
|
-
error: "invalid_cwd",
|
|
3623
|
-
message: `Directory does not exist: ${cwd}`
|
|
3624
|
-
};
|
|
3625
|
-
}
|
|
3626
|
-
const maxSessions = this.configManager.get().security.maxConcurrentSessions;
|
|
3627
|
-
if (this.sessionManager.listSessions().length >= maxSessions) {
|
|
3628
|
-
return {
|
|
3629
|
-
ok: false,
|
|
3630
|
-
error: "session_limit",
|
|
3631
|
-
message: "Maximum concurrent sessions reached"
|
|
3632
|
-
};
|
|
3633
|
-
}
|
|
3634
|
-
const existingRecord = this.sessionManager.getRecordByAgentSessionId(agentSessionId);
|
|
3635
|
-
if (existingRecord) {
|
|
3636
|
-
const sameChannel = !channelId || existingRecord.channelId === channelId;
|
|
3637
|
-
const platform = existingRecord.platform;
|
|
3638
|
-
const existingThreadId = platform?.topicId ? String(platform.topicId) : platform?.threadId;
|
|
3639
|
-
if (existingThreadId && sameChannel) {
|
|
3640
|
-
const adapter = this.adapters.get(existingRecord.channelId) ?? this.adapters.values().next().value;
|
|
3641
|
-
if (adapter) {
|
|
3642
|
-
try {
|
|
3643
|
-
await adapter.sendMessage(existingRecord.sessionId, {
|
|
3644
|
-
type: "text",
|
|
3645
|
-
text: "Session resumed from CLI."
|
|
3646
|
-
});
|
|
3647
|
-
} catch {
|
|
3648
|
-
}
|
|
3649
|
-
}
|
|
3650
|
-
return {
|
|
3651
|
-
ok: true,
|
|
3652
|
-
sessionId: existingRecord.sessionId,
|
|
3653
|
-
threadId: existingThreadId,
|
|
3654
|
-
status: "existing"
|
|
3655
|
-
};
|
|
3656
|
-
}
|
|
3657
|
-
}
|
|
3658
|
-
let adapterChannelId;
|
|
3659
|
-
if (channelId) {
|
|
3660
|
-
if (!this.adapters.has(channelId)) {
|
|
3661
|
-
const available = Array.from(this.adapters.keys()).join(", ") || "none";
|
|
3662
|
-
return { ok: false, error: "adapter_not_found", message: `Adapter '${channelId}' is not connected. Available: ${available}` };
|
|
3663
|
-
}
|
|
3664
|
-
adapterChannelId = channelId;
|
|
3665
|
-
} else {
|
|
3666
|
-
const firstEntry = this.adapters.entries().next().value;
|
|
3667
|
-
if (!firstEntry) {
|
|
3668
|
-
return { ok: false, error: "no_adapter", message: "No channel adapter registered" };
|
|
3669
|
-
}
|
|
3670
|
-
adapterChannelId = firstEntry[0];
|
|
3671
|
-
}
|
|
3672
|
-
let session;
|
|
3673
|
-
try {
|
|
3674
|
-
session = await this.createSession({
|
|
3675
|
-
channelId: adapterChannelId,
|
|
3676
|
-
agentName,
|
|
3677
|
-
workingDirectory: cwd,
|
|
3678
|
-
resumeAgentSessionId: agentSessionId,
|
|
3679
|
-
createThread: true,
|
|
3680
|
-
initialName: "Adopted session"
|
|
3681
|
-
});
|
|
3682
|
-
} catch (err) {
|
|
3683
|
-
return {
|
|
3684
|
-
ok: false,
|
|
3685
|
-
error: "resume_failed",
|
|
3686
|
-
message: `Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
|
|
3687
|
-
};
|
|
3688
|
-
}
|
|
3689
|
-
const adoptPlatform = {};
|
|
3690
|
-
if (adapterChannelId === "telegram") {
|
|
3691
|
-
adoptPlatform.topicId = Number(session.threadId);
|
|
3692
|
-
} else {
|
|
3693
|
-
adoptPlatform.threadId = session.threadId;
|
|
3694
|
-
}
|
|
3695
|
-
await this.sessionManager.patchRecord(session.id, {
|
|
3696
|
-
originalAgentSessionId: agentSessionId,
|
|
3697
|
-
platform: adoptPlatform
|
|
3698
|
-
});
|
|
3699
|
-
return {
|
|
3700
|
-
ok: true,
|
|
3701
|
-
sessionId: session.id,
|
|
3702
|
-
threadId: session.threadId,
|
|
3703
|
-
status: "adopted"
|
|
3704
|
-
};
|
|
3705
|
-
}
|
|
3706
|
-
async handleNewChat(channelId, currentThreadId) {
|
|
3707
|
-
const currentSession = this.sessionManager.getSessionByThread(
|
|
3708
|
-
channelId,
|
|
3709
|
-
currentThreadId
|
|
3710
|
-
);
|
|
3711
|
-
if (currentSession) {
|
|
3712
|
-
return this.handleNewSession(
|
|
3713
|
-
channelId,
|
|
3714
|
-
currentSession.agentName,
|
|
3715
|
-
currentSession.workingDirectory
|
|
3716
|
-
);
|
|
3717
|
-
}
|
|
3718
|
-
const record = this.sessionManager.getRecordByThread(
|
|
3719
|
-
channelId,
|
|
3720
|
-
currentThreadId
|
|
3721
|
-
);
|
|
3722
|
-
if (!record || record.status === "cancelled" || record.status === "error")
|
|
3723
|
-
return null;
|
|
3724
|
-
return this.handleNewSession(
|
|
3725
|
-
channelId,
|
|
3726
|
-
record.agentName,
|
|
3727
|
-
record.workingDir
|
|
3728
|
-
);
|
|
3729
|
-
}
|
|
3730
|
-
async createSessionWithContext(params) {
|
|
3731
|
-
let contextResult = null;
|
|
3732
|
-
try {
|
|
3733
|
-
contextResult = await this.contextManager.buildContext(
|
|
3734
|
-
params.contextQuery,
|
|
3735
|
-
params.contextOptions
|
|
3736
|
-
);
|
|
3737
|
-
} catch (err) {
|
|
3738
|
-
log6.warn({ err }, "Context building failed, proceeding without context");
|
|
3739
|
-
}
|
|
3740
|
-
const session = await this.createSession({
|
|
3741
|
-
channelId: params.channelId,
|
|
3742
|
-
agentName: params.agentName,
|
|
3743
|
-
workingDirectory: params.workingDirectory,
|
|
3744
|
-
createThread: params.createThread
|
|
3745
|
-
});
|
|
3746
|
-
if (contextResult) {
|
|
3747
|
-
session.setContext(contextResult.markdown);
|
|
3748
|
-
}
|
|
3749
|
-
return { session, contextResult };
|
|
3750
|
-
}
|
|
3751
|
-
// --- Agent Switch ---
|
|
3752
|
-
async switchSessionAgent(sessionId, toAgent) {
|
|
3753
|
-
if (this.switchingLocks.has(sessionId)) {
|
|
3754
|
-
throw new Error("Switch already in progress");
|
|
3755
|
-
}
|
|
3756
|
-
this.switchingLocks.add(sessionId);
|
|
3757
|
-
try {
|
|
3758
|
-
return await this._doSwitchSessionAgent(sessionId, toAgent);
|
|
3759
|
-
} finally {
|
|
3760
|
-
this.switchingLocks.delete(sessionId);
|
|
3761
|
-
}
|
|
3762
|
-
}
|
|
3763
|
-
async _doSwitchSessionAgent(sessionId, toAgent) {
|
|
3764
|
-
const session = this.sessionManager.getSession(sessionId);
|
|
3765
|
-
if (!session) throw new Error(`Session ${sessionId} not found`);
|
|
3766
|
-
const agentDef = this.agentManager.getAgent(toAgent);
|
|
3767
|
-
if (!agentDef) throw new Error(`Agent "${toAgent}" is not installed`);
|
|
3768
|
-
const fromAgent = session.agentName;
|
|
3769
|
-
const middlewareChain = this.lifecycleManager.middlewareChain;
|
|
3770
|
-
const result = await middlewareChain.execute("agent:beforeSwitch", {
|
|
3771
|
-
sessionId,
|
|
3772
|
-
fromAgent,
|
|
3773
|
-
toAgent
|
|
3774
|
-
}, async (payload) => payload);
|
|
3775
|
-
if (!result) throw new Error("Agent switch blocked by middleware");
|
|
3776
|
-
const lastEntry = session.findLastSwitchEntry(toAgent);
|
|
3777
|
-
const caps = getAgentCapabilities(toAgent);
|
|
3778
|
-
const canResume = !!(lastEntry && caps.supportsResume && lastEntry.promptCount === 0);
|
|
3779
|
-
const resumed = canResume;
|
|
3780
|
-
const bridge = this.bridges.get(sessionId);
|
|
3781
|
-
if (bridge) bridge.disconnect();
|
|
3782
|
-
const switchAdapter = this.adapters.get(session.channelId);
|
|
3783
|
-
if (switchAdapter?.sendSkillCommands) {
|
|
3784
|
-
await switchAdapter.sendSkillCommands(session.id, []);
|
|
3785
|
-
}
|
|
3786
|
-
if (switchAdapter?.cleanupSessionState) {
|
|
3787
|
-
await switchAdapter.cleanupSessionState(session.id);
|
|
3788
|
-
}
|
|
3789
|
-
const fromAgentSessionId = session.agentSessionId;
|
|
3790
|
-
try {
|
|
3791
|
-
await session.switchAgent(toAgent, async () => {
|
|
3792
|
-
if (canResume) {
|
|
3793
|
-
return this.agentManager.resume(toAgent, session.workingDirectory, lastEntry.agentSessionId);
|
|
3794
|
-
} else {
|
|
3795
|
-
const instance = await this.agentManager.spawn(toAgent, session.workingDirectory);
|
|
3796
|
-
try {
|
|
3797
|
-
const contextService = this.lifecycleManager.serviceRegistry.get("context");
|
|
3798
|
-
if (contextService) {
|
|
3799
|
-
const config = this.configManager.get();
|
|
3800
|
-
const labelAgent = config.agentSwitch?.labelHistory ?? true;
|
|
3801
|
-
const contextResult = await contextService.buildContext(
|
|
3802
|
-
{ type: "session", value: sessionId, repoPath: session.workingDirectory },
|
|
3803
|
-
{ labelAgent }
|
|
3804
|
-
);
|
|
3805
|
-
if (contextResult?.markdown) {
|
|
3806
|
-
session.setContext(contextResult.markdown);
|
|
3807
|
-
}
|
|
3808
|
-
}
|
|
3809
|
-
} catch {
|
|
3810
|
-
}
|
|
3811
|
-
return instance;
|
|
3812
|
-
}
|
|
3813
|
-
});
|
|
3814
|
-
} catch (err) {
|
|
3815
|
-
try {
|
|
3816
|
-
let rollbackInstance;
|
|
3817
|
-
try {
|
|
3818
|
-
rollbackInstance = await this.agentManager.resume(fromAgent, session.workingDirectory, fromAgentSessionId);
|
|
3819
|
-
} catch {
|
|
3820
|
-
rollbackInstance = await this.agentManager.spawn(fromAgent, session.workingDirectory);
|
|
3821
|
-
}
|
|
3822
|
-
const oldInstance = rollbackInstance;
|
|
3823
|
-
session.agentSwitchHistory.pop();
|
|
3824
|
-
session.agentInstance = oldInstance;
|
|
3825
|
-
session.agentName = fromAgent;
|
|
3826
|
-
session.agentSessionId = oldInstance.sessionId;
|
|
3827
|
-
const adapter = this.adapters.get(session.channelId);
|
|
3828
|
-
if (adapter) {
|
|
3829
|
-
const rollbackBridge = this.createBridge(session, adapter);
|
|
3830
|
-
rollbackBridge.connect();
|
|
3831
|
-
}
|
|
3832
|
-
log6.warn({ sessionId, fromAgent, toAgent, err }, "Agent switch failed, rolled back to previous agent");
|
|
3833
|
-
} catch (rollbackErr) {
|
|
3834
|
-
session.fail(`Switch failed and rollback failed: ${rollbackErr instanceof Error ? rollbackErr.message : String(rollbackErr)}`);
|
|
3835
|
-
log6.error({ sessionId, fromAgent, toAgent, err, rollbackErr }, "Agent switch failed and rollback also failed");
|
|
3836
|
-
}
|
|
3837
|
-
throw err;
|
|
3838
|
-
}
|
|
3839
|
-
if (bridge) {
|
|
3840
|
-
const adapter = this.adapters.get(session.channelId);
|
|
3841
|
-
if (adapter) {
|
|
3842
|
-
const newBridge = this.createBridge(session, adapter);
|
|
3843
|
-
newBridge.connect();
|
|
3844
|
-
}
|
|
3845
|
-
}
|
|
3846
|
-
await this.sessionManager.patchRecord(sessionId, {
|
|
3847
|
-
agentName: toAgent,
|
|
3848
|
-
agentSessionId: session.agentSessionId,
|
|
3849
|
-
firstAgent: session.firstAgent,
|
|
3850
|
-
currentPromptCount: 0,
|
|
3851
|
-
agentSwitchHistory: session.agentSwitchHistory
|
|
3852
|
-
});
|
|
3853
|
-
middlewareChain.execute("agent:afterSwitch", {
|
|
3854
|
-
sessionId,
|
|
3855
|
-
fromAgent,
|
|
3856
|
-
toAgent,
|
|
3857
|
-
resumed
|
|
3858
|
-
}, async (p) => p).catch(() => {
|
|
3859
|
-
});
|
|
3860
|
-
return { resumed };
|
|
3861
|
-
}
|
|
3862
|
-
// --- Lazy Resume ---
|
|
3863
|
-
/**
|
|
3864
|
-
* Get active session by thread, or attempt lazy resume from store.
|
|
3865
|
-
* Used by adapter command handlers that need a session but don't go through handleMessage().
|
|
3866
|
-
*/
|
|
3867
|
-
async getOrResumeSession(channelId, threadId) {
|
|
3868
|
-
const session = this.sessionManager.getSessionByThread(channelId, threadId);
|
|
3869
|
-
if (session) return session;
|
|
3870
|
-
return this.lazyResume({ channelId, threadId, userId: "", text: "" });
|
|
3871
|
-
}
|
|
3872
|
-
async lazyResume(message) {
|
|
3873
|
-
const store = this.sessionStore;
|
|
3874
|
-
if (!store) return null;
|
|
3875
|
-
const lockKey = `${message.channelId}:${message.threadId}`;
|
|
3876
|
-
const existing = this.resumeLocks.get(lockKey);
|
|
3877
|
-
if (existing) return existing;
|
|
3878
|
-
const record = store.findByPlatform(
|
|
3879
|
-
message.channelId,
|
|
3880
|
-
(p) => String(p.topicId) === message.threadId
|
|
3881
|
-
);
|
|
3882
|
-
if (!record) {
|
|
3883
|
-
log6.debug(
|
|
3884
|
-
{ threadId: message.threadId, channelId: message.channelId },
|
|
3885
|
-
"No session record found for thread"
|
|
3886
|
-
);
|
|
3887
|
-
return null;
|
|
3888
|
-
}
|
|
3889
|
-
if (record.status === "error" || record.status === "cancelled") {
|
|
3890
|
-
log6.debug(
|
|
3891
|
-
{
|
|
3892
|
-
threadId: message.threadId,
|
|
3893
|
-
sessionId: record.sessionId,
|
|
3894
|
-
status: record.status
|
|
3895
|
-
},
|
|
3896
|
-
"Skipping resume of error session"
|
|
3897
|
-
);
|
|
3898
|
-
return null;
|
|
3899
|
-
}
|
|
3900
|
-
log6.info(
|
|
3901
|
-
{
|
|
3902
|
-
threadId: message.threadId,
|
|
3903
|
-
sessionId: record.sessionId,
|
|
3904
|
-
status: record.status
|
|
3905
|
-
},
|
|
3906
|
-
"Lazy resume: found record, attempting resume"
|
|
3907
|
-
);
|
|
3908
|
-
const resumePromise = (async () => {
|
|
3909
|
-
try {
|
|
3910
|
-
const session = await this.createSession({
|
|
3911
|
-
channelId: record.channelId,
|
|
3912
|
-
agentName: record.agentName,
|
|
3913
|
-
workingDirectory: record.workingDir,
|
|
3914
|
-
resumeAgentSessionId: record.agentSessionId,
|
|
3915
|
-
existingSessionId: record.sessionId,
|
|
3916
|
-
initialName: record.name,
|
|
3917
|
-
threadId: message.threadId
|
|
3918
|
-
});
|
|
3919
|
-
session.activate();
|
|
3920
|
-
session.dangerousMode = record.dangerousMode ?? false;
|
|
3921
|
-
if (record.firstAgent) session.firstAgent = record.firstAgent;
|
|
3922
|
-
if (record.agentSwitchHistory) session.agentSwitchHistory = record.agentSwitchHistory;
|
|
3923
|
-
if (record.currentPromptCount != null) session.promptCount = record.currentPromptCount;
|
|
3924
|
-
log6.info(
|
|
3925
|
-
{ sessionId: session.id, threadId: message.threadId },
|
|
3926
|
-
"Lazy resume successful"
|
|
3927
|
-
);
|
|
3928
|
-
return session;
|
|
3929
|
-
} catch (err) {
|
|
3930
|
-
log6.error({ err, record }, "Lazy resume failed");
|
|
3931
|
-
const adapter = this.adapters.get(message.channelId);
|
|
3932
|
-
if (adapter) {
|
|
3933
|
-
try {
|
|
3934
|
-
await adapter.sendMessage(message.threadId, {
|
|
3935
|
-
type: "error",
|
|
3936
|
-
text: `\u26A0\uFE0F Failed to resume session: ${err instanceof Error ? err.message : String(err)}`
|
|
3937
|
-
});
|
|
3938
|
-
} catch {
|
|
3939
|
-
}
|
|
3940
|
-
}
|
|
3941
|
-
return null;
|
|
3942
|
-
} finally {
|
|
3943
|
-
this.resumeLocks.delete(lockKey);
|
|
3944
|
-
}
|
|
3945
|
-
})();
|
|
3946
|
-
this.resumeLocks.set(lockKey, resumePromise);
|
|
3947
|
-
return resumePromise;
|
|
3948
|
-
}
|
|
3949
|
-
// --- Event Wiring ---
|
|
3950
|
-
/** Create a SessionBridge for the given session and adapter */
|
|
3951
|
-
createBridge(session, adapter) {
|
|
3952
|
-
const bridge = new SessionBridge(session, adapter, {
|
|
3953
|
-
messageTransformer: this.messageTransformer,
|
|
3954
|
-
notificationManager: this.notificationManager,
|
|
3955
|
-
sessionManager: this.sessionManager,
|
|
3956
|
-
eventBus: this.eventBus,
|
|
3957
|
-
fileService: this.fileService,
|
|
3958
|
-
middlewareChain: this.lifecycleManager?.middlewareChain
|
|
3959
|
-
});
|
|
3960
|
-
this.bridges.set(session.id, bridge);
|
|
3961
|
-
return bridge;
|
|
3962
|
-
}
|
|
3963
|
-
};
|
|
3964
|
-
|
|
3965
|
-
// src/core/command-registry.ts
|
|
3966
|
-
var CommandRegistry = class _CommandRegistry {
|
|
3967
|
-
/** Main registry: short names + qualified names → RegisteredCommand */
|
|
3968
|
-
commands = /* @__PURE__ */ new Map();
|
|
3969
|
-
/** Adapter-specific overrides: `channelId:commandName` → RegisteredCommand */
|
|
3970
|
-
overrides = /* @__PURE__ */ new Map();
|
|
3971
|
-
static ADAPTER_SCOPES = /* @__PURE__ */ new Set(["telegram", "discord"]);
|
|
3972
|
-
/**
|
|
3973
|
-
* Register a command definition.
|
|
3974
|
-
* @param def - Command definition
|
|
3975
|
-
* @param pluginName - Plugin that owns the command (set automatically by PluginContext)
|
|
3976
|
-
*/
|
|
3977
|
-
register(def, pluginName) {
|
|
3978
|
-
const cmd = {
|
|
3979
|
-
...def,
|
|
3980
|
-
pluginName: pluginName ?? def.pluginName
|
|
3981
|
-
};
|
|
3982
|
-
if (pluginName) {
|
|
3983
|
-
cmd.scope = _CommandRegistry.extractScope(pluginName);
|
|
3984
|
-
}
|
|
3985
|
-
const qualifiedName = cmd.scope ? `${cmd.scope}:${cmd.name}` : void 0;
|
|
3986
|
-
if (cmd.scope && _CommandRegistry.ADAPTER_SCOPES.has(cmd.scope) && this.commands.has(cmd.name)) {
|
|
3987
|
-
this.overrides.set(`${cmd.scope}:${cmd.name}`, cmd);
|
|
3988
|
-
return;
|
|
3989
|
-
}
|
|
3990
|
-
if (qualifiedName) {
|
|
3991
|
-
this.commands.set(qualifiedName, cmd);
|
|
3992
|
-
}
|
|
3993
|
-
if (this.commands.has(cmd.name)) {
|
|
3994
|
-
const existing = this.commands.get(cmd.name);
|
|
3995
|
-
if (existing.category === "system") {
|
|
3996
|
-
return;
|
|
3997
|
-
}
|
|
3998
|
-
return;
|
|
3999
|
-
}
|
|
4000
|
-
this.commands.set(cmd.name, cmd);
|
|
4001
|
-
}
|
|
4002
|
-
/** Retrieve a command by name (short or qualified). */
|
|
4003
|
-
get(name) {
|
|
4004
|
-
return this.commands.get(name);
|
|
4005
|
-
}
|
|
4006
|
-
/** Remove a command by name (short or qualified). Also removes its qualified name entry. */
|
|
4007
|
-
unregister(name) {
|
|
4008
|
-
const cmd = this.commands.get(name);
|
|
4009
|
-
if (!cmd) return;
|
|
4010
|
-
this.commands.delete(name);
|
|
4011
|
-
if (cmd.scope) {
|
|
4012
|
-
this.commands.delete(`${cmd.scope}:${cmd.name}`);
|
|
4013
|
-
this.commands.delete(cmd.name);
|
|
4014
|
-
}
|
|
4015
|
-
}
|
|
4016
|
-
/** Remove all commands registered by a given plugin. */
|
|
4017
|
-
unregisterByPlugin(pluginName) {
|
|
4018
|
-
const toDelete = [];
|
|
4019
|
-
for (const [key, cmd] of this.commands) {
|
|
4020
|
-
if (cmd.pluginName === pluginName) {
|
|
4021
|
-
toDelete.push(key);
|
|
4022
|
-
}
|
|
4023
|
-
}
|
|
4024
|
-
for (const key of toDelete) {
|
|
4025
|
-
this.commands.delete(key);
|
|
4026
|
-
}
|
|
4027
|
-
for (const [key, cmd] of this.overrides) {
|
|
4028
|
-
if (cmd.pluginName === pluginName) {
|
|
4029
|
-
this.overrides.delete(key);
|
|
4030
|
-
}
|
|
4031
|
-
}
|
|
4032
|
-
}
|
|
4033
|
-
/** Return all unique commands (deduplicated — each command appears once). */
|
|
4034
|
-
getAll() {
|
|
4035
|
-
const seen = /* @__PURE__ */ new Set();
|
|
4036
|
-
for (const cmd of this.commands.values()) {
|
|
4037
|
-
seen.add(cmd);
|
|
4038
|
-
}
|
|
4039
|
-
return [...seen];
|
|
4040
|
-
}
|
|
4041
|
-
/** Filter commands by category. */
|
|
4042
|
-
getByCategory(category) {
|
|
4043
|
-
return this.getAll().filter((cmd) => cmd.category === category);
|
|
4044
|
-
}
|
|
4045
|
-
/**
|
|
4046
|
-
* Parse and execute a command string.
|
|
4047
|
-
* @param commandString - Full command string, e.g. "/greet hello world"
|
|
4048
|
-
* @param baseArgs - Base arguments (channelId, userId, etc.)
|
|
4049
|
-
* @returns CommandResponse
|
|
4050
|
-
*/
|
|
4051
|
-
async execute(commandString, baseArgs) {
|
|
4052
|
-
const trimmed = commandString.trim();
|
|
4053
|
-
const spaceIdx = trimmed.indexOf(" ");
|
|
4054
|
-
const rawCmd = spaceIdx === -1 ? trimmed.slice(1) : trimmed.slice(1, spaceIdx);
|
|
4055
|
-
const cmdName = rawCmd.split("@")[0];
|
|
4056
|
-
const rawArgs = spaceIdx === -1 ? "" : trimmed.slice(spaceIdx + 1);
|
|
4057
|
-
const overrideKey = `${baseArgs.channelId}:${cmdName}`;
|
|
4058
|
-
const override = this.overrides.get(overrideKey);
|
|
4059
|
-
const cmd = override ?? this.commands.get(cmdName);
|
|
4060
|
-
if (!cmd) {
|
|
4061
|
-
return { type: "error", message: `Unknown command: /${cmdName}` };
|
|
4062
|
-
}
|
|
4063
|
-
const args = {
|
|
4064
|
-
...baseArgs,
|
|
4065
|
-
raw: rawArgs
|
|
4066
|
-
};
|
|
4067
|
-
try {
|
|
4068
|
-
const result = await cmd.handler(args);
|
|
4069
|
-
if (result === void 0 || result === null) {
|
|
4070
|
-
return { type: "silent" };
|
|
4071
|
-
}
|
|
4072
|
-
return result;
|
|
4073
|
-
} catch (err) {
|
|
4074
|
-
const message = err instanceof Error ? err.message : String(err);
|
|
4075
|
-
return { type: "error", message: `Command /${cmdName} failed: ${message}` };
|
|
4076
|
-
}
|
|
4077
|
-
}
|
|
4078
|
-
/** Extract scope from plugin name: '@openacp/speech' → 'speech', 'my-plugin' → 'my-plugin' */
|
|
4079
|
-
static extractScope(pluginName) {
|
|
4080
|
-
const slashIdx = pluginName.lastIndexOf("/");
|
|
4081
|
-
if (slashIdx !== -1) {
|
|
4082
|
-
return pluginName.slice(slashIdx + 1);
|
|
4083
|
-
}
|
|
4084
|
-
return pluginName;
|
|
4085
|
-
}
|
|
4086
|
-
};
|
|
4087
|
-
|
|
4088
|
-
export {
|
|
4089
|
-
nodeToWebWritable,
|
|
4090
|
-
nodeToWebReadable,
|
|
4091
|
-
StderrCapture,
|
|
4092
|
-
TypedEmitter,
|
|
4093
|
-
AgentInstance,
|
|
4094
|
-
AgentManager,
|
|
4095
|
-
PromptQueue,
|
|
4096
|
-
PermissionGate,
|
|
4097
|
-
Session,
|
|
4098
|
-
SessionManager,
|
|
4099
|
-
SessionBridge,
|
|
4100
|
-
MessageTransformer,
|
|
4101
|
-
SessionFactory,
|
|
4102
|
-
EventBus,
|
|
4103
|
-
OpenACPCore,
|
|
4104
|
-
CommandRegistry
|
|
4105
|
-
};
|
|
4106
|
-
//# sourceMappingURL=chunk-LRV56K2M.js.map
|