@pasko70/pibo 2.5.1 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/context/pibo-native-tooling.md +1 -0
- package/dist/agent-runtime/contract.js +2 -1
- package/dist/agent-runtime/history.js +53 -1
- package/dist/agent-runtime/routed-session.js +100 -26
- package/dist/agent-runtimes/codex-native/adapter.js +307 -31
- package/dist/agent-runtimes/codex-native/client.js +22 -3
- package/dist/agent-runtimes/codex-native/first-use.js +294 -0
- package/dist/agent-runtimes/codex-native/history.js +26 -7
- package/dist/agent-runtimes/codex-native/models.js +48 -1
- package/dist/agent-runtimes/codex-native/process.js +15 -1
- package/dist/agent-runtimes/codex-native/thread.js +75 -31
- package/dist/agent-runtimes/codex-native/turn.js +30 -0
- package/dist/agent-runtimes/history-proof.js +8 -0
- package/dist/agent-runtimes/omp/adapter.js +61 -14
- package/dist/agent-runtimes/omp/history.js +189 -34
- package/dist/agent-runtimes/omp/thread.js +82 -36
- package/dist/agent-runtimes/pi/adapter.js +30 -3
- package/dist/agent-runtimes/pi/history.js +66 -5
- package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
- package/dist/agent-runtimes/pi/routed-session.js +344 -37
- package/dist/apps/chat/agent-store.js +3 -0
- package/dist/apps/chat/chat-api-routes.js +1 -1
- package/dist/apps/chat/chat-files.js +130 -9
- package/dist/apps/chat/chat-request-normalizers.js +6 -0
- package/dist/apps/chat/chat-settings-routes.js +27 -2
- package/dist/apps/chat/chat-speech.js +100 -0
- package/dist/apps/chat/data/chat-data-mappers.js +25 -5
- package/dist/apps/chat/data/timeline-query-service.js +89 -1
- package/dist/apps/chat/trace-v2.js +210 -8
- package/dist/apps/chat/trace.js +3 -0
- package/dist/apps/chat/web-app.js +108 -3
- package/dist/apps/chat-ui/assets/{dist-GS8xUdfg.js → dist-BBDMeU5-.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-BpVSf6ZV.js → dist-BDIATCQt.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CRv7fzi3.js → dist-JnW4v8UE.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-CbBXbGVi.js → dist-lRiLP9rr.js} +1 -1
- package/dist/apps/chat-ui/assets/{dist-HC0xQ92C.js → dist-mqCviI-c.js} +1 -1
- package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
- package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
- package/dist/apps/chat-ui/index.html +2 -2
- package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
- package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
- package/dist/apps/chat-vscode-web/index.html +2 -2
- package/dist/apps/vscode-artifacts/latest.vsix +0 -0
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.0.vsix +0 -0
- package/dist/cli.js +49 -5
- package/dist/config/config.js +59 -8
- package/dist/core/preview-server-settings.js +20 -0
- package/dist/core/profiles.js +1 -0
- package/dist/core/session-router.js +476 -44
- package/dist/core/user-settings.js +10 -0
- package/dist/data/payload-store.js +57 -1
- package/dist/data/schema.js +18 -1
- package/dist/debug/trace.js +5 -0
- package/dist/gateway/client.js +368 -65
- package/dist/gateway/protocol.js +2 -0
- package/dist/gateway/server.js +8 -0
- package/dist/gateway/web.js +2 -0
- package/dist/index.js +1 -1
- package/dist/loops/store.js +6 -4
- package/dist/mcp/config.js +89 -17
- package/dist/mcp/daemon-client.js +473 -153
- package/dist/mcp/daemon.js +401 -37
- package/dist/plugins/builtin.js +2 -2
- package/dist/plugins/codex-native.js +2 -0
- package/dist/plugins/registry.js +260 -1
- package/dist/previews/cli.js +345 -0
- package/dist/previews/config.js +61 -0
- package/dist/previews/manager.js +644 -0
- package/dist/previews/network.js +198 -0
- package/dist/previews/plugin.js +11 -0
- package/dist/previews/process-supervisor.js +38 -0
- package/dist/previews/proxy.js +387 -0
- package/dist/previews/store.js +750 -0
- package/dist/previews/types.js +1 -0
- package/dist/previews/web-app.js +446 -0
- package/dist/runs/registry.js +22 -0
- package/dist/runs/tools.js +46 -7
- package/dist/session-ui/terminalRows.js +187 -13
- package/dist/sessions/pibo-data-store.js +37 -0
- package/dist/sessions/runtime-binding-persistence.js +34 -0
- package/dist/sessions/runtime-binding.js +6 -0
- package/dist/sessions/sqlite-store.js +37 -0
- package/dist/shared/trace-engine.js +12 -64
- package/dist/shared/trace-event-projection.js +16 -0
- package/dist/shared/trace-history.js +684 -184
- package/dist/shared/trace-limits.js +7 -0
- package/dist/shared/trace-live-patch.js +61 -0
- package/dist/shared/trace-tool-identity.js +43 -0
- package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
- package/dist/speech/openai-codex.js +407 -0
- package/dist/speech/types.js +8 -0
- package/dist/subagents/context.js +5 -3
- package/dist/subagents/observations.js +94 -6
- package/dist/subagents/tool.js +103 -20
- package/dist/tools/codex-image-artifacts.js +24 -0
- package/dist/tools/codex-image-generation.js +4 -22
- package/dist/web/channel.js +88 -4
- package/dist/web/http.js +2 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
- package/dist/apps/chat-ui/assets/index-DT5jCQBP.js +0 -228
- package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
- package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.1.vsix +0 -0
package/dist/mcp/daemon.js
CHANGED
|
@@ -1,21 +1,102 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* MCP-CLI Daemon - Background worker that maintains persistent MCP connections
|
|
3
3
|
*
|
|
4
|
-
* This is spawned as a detached process and manages a Unix socket
|
|
5
|
-
* It maintains the MCP server connection and forwards
|
|
4
|
+
* This is spawned as a detached process and manages a Unix socket on POSIX or
|
|
5
|
+
* a named pipe on Windows. It maintains the MCP server connection and forwards
|
|
6
|
+
* requests from CLI invocations.
|
|
6
7
|
*/
|
|
7
|
-
import {
|
|
8
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
9
|
+
import { closeSync, existsSync, fstatSync, futimesSync, linkSync, mkdirSync, openSync, readFileSync, readlinkSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
|
|
8
10
|
import { createServer } from 'node:net';
|
|
9
11
|
import { dirname } from 'node:path';
|
|
10
12
|
import { callTool, connectToServer, listTools, } from './client.js';
|
|
11
|
-
import { debug, getConfigHash, getDaemonTimeoutMs, getPidPath, getSocketDir, getSocketPath, } from './config.js';
|
|
13
|
+
import { debug, getConfigHash, getDaemonClaimPath, getDaemonTimeoutMs, getPidPath, getSocketDir, getSocketPath, usesFilesystemSocket, } from './config.js';
|
|
14
|
+
function hashBuffer(value) {
|
|
15
|
+
return createHash('sha256').update(value).digest('hex');
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Read an OS process creation identity that changes when a PID is reused.
|
|
19
|
+
* Linux exposes the kernel start tick, executable, and argv through procfs.
|
|
20
|
+
* Other platforms fail closed: callers may request graceful endpoint shutdown,
|
|
21
|
+
* but they must not authorize a PID signal from unverifiable metadata.
|
|
22
|
+
*/
|
|
23
|
+
export function readProcessIdentity(pid) {
|
|
24
|
+
if (process.platform !== 'linux' || !Number.isInteger(pid) || pid <= 0) {
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
try {
|
|
28
|
+
const stat = readFileSync(`/proc/${pid}/stat`, 'utf8');
|
|
29
|
+
const commandEnd = stat.lastIndexOf(') ');
|
|
30
|
+
if (commandEnd === -1)
|
|
31
|
+
return null;
|
|
32
|
+
const fieldsAfterCommand = stat.slice(commandEnd + 2).trim().split(/\s+/);
|
|
33
|
+
const startToken = fieldsAfterCommand[19];
|
|
34
|
+
if (!startToken)
|
|
35
|
+
return null;
|
|
36
|
+
return {
|
|
37
|
+
platform: 'linux',
|
|
38
|
+
startToken,
|
|
39
|
+
executable: readlinkSync(`/proc/${pid}/exe`),
|
|
40
|
+
commandHash: hashBuffer(readFileSync(`/proc/${pid}/cmdline`)),
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
export function processIdentityMatches(actual, expected) {
|
|
48
|
+
return Boolean(actual &&
|
|
49
|
+
expected &&
|
|
50
|
+
actual.platform === expected.platform &&
|
|
51
|
+
actual.startToken === expected.startToken &&
|
|
52
|
+
actual.executable === expected.executable &&
|
|
53
|
+
actual.commandHash === expected.commandHash);
|
|
54
|
+
}
|
|
55
|
+
export function daemonProcessIdentityMatches(expected) {
|
|
56
|
+
return processIdentityMatches(readProcessIdentity(expected.pid), expected.processIdentity);
|
|
57
|
+
}
|
|
58
|
+
function getFileIdentity(path) {
|
|
59
|
+
try {
|
|
60
|
+
const stat = statSync(path);
|
|
61
|
+
return {
|
|
62
|
+
device: stat.dev,
|
|
63
|
+
inode: stat.ino,
|
|
64
|
+
size: stat.size,
|
|
65
|
+
modifiedAt: stat.mtimeMs,
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
catch {
|
|
69
|
+
return null;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
function fileIdentityMatches(actual, expected) {
|
|
73
|
+
return Boolean(actual &&
|
|
74
|
+
expected &&
|
|
75
|
+
actual.device === expected.device &&
|
|
76
|
+
actual.inode === expected.inode &&
|
|
77
|
+
actual.size === expected.size &&
|
|
78
|
+
actual.modifiedAt === expected.modifiedAt);
|
|
79
|
+
}
|
|
80
|
+
function getQuarantinePath(path) {
|
|
81
|
+
return `${path}.delete-${process.pid}-${Date.now()}-${randomUUID()}`;
|
|
82
|
+
}
|
|
83
|
+
function restoreQuarantinedFile(path, quarantinePath) {
|
|
84
|
+
try {
|
|
85
|
+
if (existsSync(quarantinePath) && !existsSync(path)) {
|
|
86
|
+
renameSync(quarantinePath, path);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch {
|
|
90
|
+
// A newer owner may already occupy the canonical path.
|
|
91
|
+
}
|
|
92
|
+
}
|
|
12
93
|
// ============================================================================
|
|
13
94
|
// PID File Management
|
|
14
95
|
// ============================================================================
|
|
15
96
|
/**
|
|
16
97
|
* Write PID file with config hash for stale detection
|
|
17
98
|
*/
|
|
18
|
-
export function writePidFile(serverName, configHash) {
|
|
99
|
+
export function writePidFile(serverName, configHash, generation) {
|
|
19
100
|
const pidPath = getPidPath(serverName);
|
|
20
101
|
const dir = dirname(pidPath);
|
|
21
102
|
if (!existsSync(dir)) {
|
|
@@ -24,15 +105,21 @@ export function writePidFile(serverName, configHash) {
|
|
|
24
105
|
const content = {
|
|
25
106
|
pid: process.pid,
|
|
26
107
|
configHash,
|
|
108
|
+
generation,
|
|
27
109
|
startedAt: new Date().toISOString(),
|
|
110
|
+
serverName,
|
|
111
|
+
processIdentity: readProcessIdentity(process.pid) ?? undefined,
|
|
28
112
|
};
|
|
29
|
-
writeFileSync(pidPath, JSON.stringify(content), {
|
|
113
|
+
writeFileSync(pidPath, JSON.stringify(content), {
|
|
114
|
+
flag: 'wx',
|
|
115
|
+
mode: 0o600,
|
|
116
|
+
});
|
|
117
|
+
return content;
|
|
30
118
|
}
|
|
31
119
|
/**
|
|
32
120
|
* Read PID file content
|
|
33
121
|
*/
|
|
34
|
-
export function
|
|
35
|
-
const pidPath = getPidPath(serverName);
|
|
122
|
+
export function readPidFilePath(pidPath) {
|
|
36
123
|
if (!existsSync(pidPath)) {
|
|
37
124
|
return null;
|
|
38
125
|
}
|
|
@@ -44,34 +131,261 @@ export function readPidFile(serverName) {
|
|
|
44
131
|
return null;
|
|
45
132
|
}
|
|
46
133
|
}
|
|
134
|
+
export function readPidFile(serverName) {
|
|
135
|
+
return readPidFilePath(getPidPath(serverName));
|
|
136
|
+
}
|
|
137
|
+
export function daemonIdentityMatches(actual, expected) {
|
|
138
|
+
return Boolean(actual &&
|
|
139
|
+
expected &&
|
|
140
|
+
actual.pid === expected.pid &&
|
|
141
|
+
actual.configHash === expected.configHash &&
|
|
142
|
+
actual.generation === expected.generation);
|
|
143
|
+
}
|
|
144
|
+
export function pidFileIdentityMatches(actual, expected) {
|
|
145
|
+
return Boolean(daemonIdentityMatches(actual, expected) &&
|
|
146
|
+
actual?.startedAt === expected?.startedAt &&
|
|
147
|
+
actual?.serverName === expected?.serverName &&
|
|
148
|
+
((actual?.processIdentity === undefined &&
|
|
149
|
+
expected?.processIdentity === undefined) ||
|
|
150
|
+
processIdentityMatches(actual?.processIdentity, expected?.processIdentity)));
|
|
151
|
+
}
|
|
152
|
+
function quarantinePidFilePath(pidPath, expected) {
|
|
153
|
+
const quarantinePath = getQuarantinePath(pidPath);
|
|
154
|
+
try {
|
|
155
|
+
const current = readPidFilePath(pidPath);
|
|
156
|
+
if (!current || (expected && !pidFileIdentityMatches(current, expected))) {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
renameSync(pidPath, quarantinePath);
|
|
160
|
+
const moved = readPidFilePath(quarantinePath);
|
|
161
|
+
if (moved && (!expected || pidFileIdentityMatches(moved, expected))) {
|
|
162
|
+
return quarantinePath;
|
|
163
|
+
}
|
|
164
|
+
restoreQuarantinedFile(pidPath, quarantinePath);
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
catch {
|
|
168
|
+
restoreQuarantinedFile(pidPath, quarantinePath);
|
|
169
|
+
return null;
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
export function removePidFilePath(pidPath, expected) {
|
|
173
|
+
const quarantinePath = quarantinePidFilePath(pidPath, expected);
|
|
174
|
+
if (!quarantinePath)
|
|
175
|
+
return false;
|
|
176
|
+
try {
|
|
177
|
+
unlinkSync(quarantinePath);
|
|
178
|
+
return true;
|
|
179
|
+
}
|
|
180
|
+
catch {
|
|
181
|
+
restoreQuarantinedFile(pidPath, quarantinePath);
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
47
185
|
/**
|
|
48
186
|
* Remove PID file
|
|
49
187
|
*/
|
|
50
|
-
export function removePidFile(serverName) {
|
|
51
|
-
|
|
188
|
+
export function removePidFile(serverName, expected) {
|
|
189
|
+
return removePidFilePath(getPidPath(serverName), expected);
|
|
190
|
+
}
|
|
191
|
+
export function writeOwnershipFileExclusive(path, content) {
|
|
192
|
+
const dir = dirname(path);
|
|
193
|
+
if (!existsSync(dir)) {
|
|
194
|
+
mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
195
|
+
}
|
|
196
|
+
const publicationPath = `${path}.publish-${process.pid}-${randomUUID()}`;
|
|
52
197
|
try {
|
|
53
|
-
|
|
54
|
-
|
|
198
|
+
writeFileSync(publicationPath, JSON.stringify(content), {
|
|
199
|
+
flag: 'wx',
|
|
200
|
+
mode: 0o600,
|
|
201
|
+
});
|
|
202
|
+
linkSync(publicationPath, path);
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
catch (error) {
|
|
206
|
+
if (error.code === 'EEXIST') {
|
|
207
|
+
return false;
|
|
208
|
+
}
|
|
209
|
+
throw error;
|
|
210
|
+
}
|
|
211
|
+
finally {
|
|
212
|
+
try {
|
|
213
|
+
unlinkSync(publicationPath);
|
|
214
|
+
}
|
|
215
|
+
catch {
|
|
216
|
+
// The temporary publication name may never have been created.
|
|
55
217
|
}
|
|
56
218
|
}
|
|
219
|
+
}
|
|
220
|
+
export function readOwnershipFilePath(path) {
|
|
221
|
+
try {
|
|
222
|
+
return JSON.parse(readFileSync(path, 'utf8'));
|
|
223
|
+
}
|
|
57
224
|
catch {
|
|
58
|
-
|
|
225
|
+
return null;
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
export function getOwnershipFileAgeMs(path) {
|
|
229
|
+
try {
|
|
230
|
+
return Math.max(0, Date.now() - statSync(path).mtimeMs);
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
return null;
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Refresh one ownership generation through an opened inode. A concurrent path
|
|
238
|
+
* replacement is never refreshed because the descriptor remains bound to the
|
|
239
|
+
* original file and the canonical inode is checked before reporting success.
|
|
240
|
+
*/
|
|
241
|
+
export function refreshOwnershipFile(path, expectedGeneration, expectedOwnerNonce) {
|
|
242
|
+
let handle;
|
|
243
|
+
try {
|
|
244
|
+
handle = openSync(path, 'r');
|
|
245
|
+
const opened = fstatSync(handle);
|
|
246
|
+
const current = JSON.parse(readFileSync(handle, { encoding: 'utf8' }));
|
|
247
|
+
if (current.generation !== expectedGeneration ||
|
|
248
|
+
(expectedOwnerNonce !== undefined &&
|
|
249
|
+
current.ownerNonce !== expectedOwnerNonce)) {
|
|
250
|
+
return false;
|
|
251
|
+
}
|
|
252
|
+
const now = new Date();
|
|
253
|
+
futimesSync(handle, now, now);
|
|
254
|
+
const canonical = statSync(path);
|
|
255
|
+
return canonical.dev === opened.dev && canonical.ino === opened.ino;
|
|
256
|
+
}
|
|
257
|
+
catch {
|
|
258
|
+
return false;
|
|
259
|
+
}
|
|
260
|
+
finally {
|
|
261
|
+
if (handle !== undefined) {
|
|
262
|
+
try {
|
|
263
|
+
closeSync(handle);
|
|
264
|
+
}
|
|
265
|
+
catch {
|
|
266
|
+
// A failed close cannot make the ownership refresh authoritative.
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
export function removeOwnershipFile(path, expectedGeneration) {
|
|
272
|
+
const quarantinePath = getQuarantinePath(path);
|
|
273
|
+
try {
|
|
274
|
+
const current = readOwnershipFilePath(path);
|
|
275
|
+
if (!current || current.generation !== expectedGeneration) {
|
|
276
|
+
return false;
|
|
277
|
+
}
|
|
278
|
+
// Rename is the atomic compare-and-delete boundary. If another process
|
|
279
|
+
// replaced the path after our first read, inspect the moved file and put it
|
|
280
|
+
// back instead of unlinking that process's ownership record.
|
|
281
|
+
renameSync(path, quarantinePath);
|
|
282
|
+
const moved = readOwnershipFilePath(quarantinePath);
|
|
283
|
+
if (moved?.generation === expectedGeneration) {
|
|
284
|
+
unlinkSync(quarantinePath);
|
|
285
|
+
return true;
|
|
286
|
+
}
|
|
287
|
+
if (!existsSync(path))
|
|
288
|
+
renameSync(quarantinePath, path);
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
catch {
|
|
292
|
+
restoreQuarantinedFile(path, quarantinePath);
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
/** Remove an old malformed ownership file without racing a replacement. */
|
|
297
|
+
export function removeMalformedOwnershipFile(path, minimumAgeMs) {
|
|
298
|
+
const expectedFileIdentity = getFileIdentity(path);
|
|
299
|
+
const ageMs = getOwnershipFileAgeMs(path);
|
|
300
|
+
if (!expectedFileIdentity ||
|
|
301
|
+
ageMs === null ||
|
|
302
|
+
ageMs < minimumAgeMs ||
|
|
303
|
+
readOwnershipFilePath(path) !== null) {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
const quarantinePath = getQuarantinePath(path);
|
|
307
|
+
try {
|
|
308
|
+
renameSync(path, quarantinePath);
|
|
309
|
+
if (readOwnershipFilePath(quarantinePath) === null &&
|
|
310
|
+
fileIdentityMatches(getFileIdentity(quarantinePath), expectedFileIdentity)) {
|
|
311
|
+
unlinkSync(quarantinePath);
|
|
312
|
+
return true;
|
|
313
|
+
}
|
|
314
|
+
restoreQuarantinedFile(path, quarantinePath);
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
catch {
|
|
318
|
+
restoreQuarantinedFile(path, quarantinePath);
|
|
319
|
+
return false;
|
|
59
320
|
}
|
|
60
321
|
}
|
|
61
322
|
/**
|
|
62
323
|
* Remove socket file
|
|
63
324
|
*/
|
|
64
325
|
export function removeSocketFile(serverName) {
|
|
326
|
+
if (!usesFilesystemSocket()) {
|
|
327
|
+
return true;
|
|
328
|
+
}
|
|
65
329
|
const socketPath = getSocketPath(serverName);
|
|
66
330
|
try {
|
|
67
331
|
if (existsSync(socketPath)) {
|
|
68
332
|
unlinkSync(socketPath);
|
|
69
333
|
}
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
catch {
|
|
337
|
+
return false;
|
|
338
|
+
}
|
|
339
|
+
}
|
|
340
|
+
export function daemonStateOwnershipMatches(ownership) {
|
|
341
|
+
return (readOwnershipFilePath(ownership.path)?.generation === ownership.generation);
|
|
342
|
+
}
|
|
343
|
+
/**
|
|
344
|
+
* Atomically acquire one matching PID generation before deleting its endpoint.
|
|
345
|
+
* The caller must hold the per-server ownership claim for the whole operation.
|
|
346
|
+
*/
|
|
347
|
+
export function removeDaemonState(serverName, expected, ownership) {
|
|
348
|
+
if (!daemonStateOwnershipMatches(ownership))
|
|
349
|
+
return false;
|
|
350
|
+
const pidPath = getPidPath(serverName);
|
|
351
|
+
const quarantinePath = quarantinePidFilePath(pidPath, expected);
|
|
352
|
+
if (!quarantinePath)
|
|
353
|
+
return false;
|
|
354
|
+
if (!daemonStateOwnershipMatches(ownership) || existsSync(pidPath)) {
|
|
355
|
+
if (existsSync(pidPath)) {
|
|
356
|
+
try {
|
|
357
|
+
unlinkSync(quarantinePath);
|
|
358
|
+
}
|
|
359
|
+
catch {
|
|
360
|
+
// A later cleanup can remove the quarantined old generation.
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
else {
|
|
364
|
+
restoreQuarantinedFile(pidPath, quarantinePath);
|
|
365
|
+
}
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
if (!removeSocketFile(serverName)) {
|
|
369
|
+
restoreQuarantinedFile(pidPath, quarantinePath);
|
|
370
|
+
return false;
|
|
371
|
+
}
|
|
372
|
+
try {
|
|
373
|
+
unlinkSync(quarantinePath);
|
|
374
|
+
return true;
|
|
70
375
|
}
|
|
71
376
|
catch {
|
|
72
|
-
//
|
|
377
|
+
// The endpoint is already gone. Keep the old PID generation unavailable
|
|
378
|
+
// rather than republishing metadata for a stopped daemon.
|
|
379
|
+
return false;
|
|
73
380
|
}
|
|
74
381
|
}
|
|
382
|
+
export function removeUntrackedEndpoint(serverName, ownership) {
|
|
383
|
+
if (!daemonStateOwnershipMatches(ownership) ||
|
|
384
|
+
existsSync(getPidPath(serverName))) {
|
|
385
|
+
return false;
|
|
386
|
+
}
|
|
387
|
+
return removeSocketFile(serverName);
|
|
388
|
+
}
|
|
75
389
|
/**
|
|
76
390
|
* Check if a process is running
|
|
77
391
|
*/
|
|
@@ -84,12 +398,12 @@ export function isProcessRunning(pid) {
|
|
|
84
398
|
return false;
|
|
85
399
|
}
|
|
86
400
|
}
|
|
87
|
-
/**
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
401
|
+
/** Signal only the process creation identity recorded by the daemon itself. */
|
|
402
|
+
export function terminateDaemonProcess(expected) {
|
|
403
|
+
if (!daemonProcessIdentityMatches(expected))
|
|
404
|
+
return false;
|
|
91
405
|
try {
|
|
92
|
-
process.kill(pid, 'SIGTERM');
|
|
406
|
+
process.kill(expected.pid, 'SIGTERM');
|
|
93
407
|
return true;
|
|
94
408
|
}
|
|
95
409
|
catch {
|
|
@@ -102,7 +416,7 @@ export function killProcess(pid) {
|
|
|
102
416
|
/**
|
|
103
417
|
* Main daemon entry point - run as detached background process
|
|
104
418
|
*/
|
|
105
|
-
export async function runDaemon(serverName, config) {
|
|
419
|
+
export async function runDaemon(serverName, config, generation) {
|
|
106
420
|
const socketPath = getSocketPath(serverName);
|
|
107
421
|
const configHash = getConfigHash(config);
|
|
108
422
|
const timeoutMs = getDaemonTimeoutMs();
|
|
@@ -110,8 +424,10 @@ export async function runDaemon(serverName, config) {
|
|
|
110
424
|
let mcpClient = null;
|
|
111
425
|
let server = null;
|
|
112
426
|
const activeConnections = new Set();
|
|
427
|
+
let identity = null;
|
|
428
|
+
let cleanupPromise = null;
|
|
113
429
|
// Cleanup function
|
|
114
|
-
const
|
|
430
|
+
const performCleanup = async () => {
|
|
115
431
|
debug(`[daemon:${serverName}] Shutting down...`);
|
|
116
432
|
if (idleTimer) {
|
|
117
433
|
clearTimeout(idleTimer);
|
|
@@ -147,11 +463,35 @@ export async function runDaemon(serverName, config) {
|
|
|
147
463
|
}
|
|
148
464
|
server = null;
|
|
149
465
|
}
|
|
150
|
-
//
|
|
151
|
-
|
|
152
|
-
|
|
466
|
+
// Self-cleanup uses the same per-server ownership boundary as elected
|
|
467
|
+
// clients. If another owner is replacing this daemon, leave cleanup to it.
|
|
468
|
+
if (identity) {
|
|
469
|
+
const claimPath = getDaemonClaimPath(serverName);
|
|
470
|
+
const cleanupClaim = {
|
|
471
|
+
ownerPid: process.pid,
|
|
472
|
+
generation: randomUUID(),
|
|
473
|
+
configHash: identity.configHash,
|
|
474
|
+
startedAt: new Date().toISOString(),
|
|
475
|
+
serverName,
|
|
476
|
+
};
|
|
477
|
+
if (writeOwnershipFileExclusive(claimPath, cleanupClaim)) {
|
|
478
|
+
try {
|
|
479
|
+
removeDaemonState(serverName, identity, {
|
|
480
|
+
path: claimPath,
|
|
481
|
+
generation: cleanupClaim.generation,
|
|
482
|
+
});
|
|
483
|
+
}
|
|
484
|
+
finally {
|
|
485
|
+
removeOwnershipFile(claimPath, cleanupClaim.generation);
|
|
486
|
+
}
|
|
487
|
+
}
|
|
488
|
+
}
|
|
153
489
|
debug(`[daemon:${serverName}] Cleanup complete`);
|
|
154
490
|
};
|
|
491
|
+
const cleanup = () => {
|
|
492
|
+
cleanupPromise ??= performCleanup();
|
|
493
|
+
return cleanupPromise;
|
|
494
|
+
};
|
|
155
495
|
// Reset idle timer
|
|
156
496
|
const resetIdleTimer = () => {
|
|
157
497
|
if (idleTimer) {
|
|
@@ -177,10 +517,9 @@ export async function runDaemon(serverName, config) {
|
|
|
177
517
|
if (!existsSync(socketDir)) {
|
|
178
518
|
mkdirSync(socketDir, { recursive: true, mode: 0o700 });
|
|
179
519
|
}
|
|
180
|
-
//
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
writePidFile(serverName, configHash);
|
|
520
|
+
// The spawning client owns the startup claim and removes stale state before
|
|
521
|
+
// launch. Exclusive PID creation makes an ownership bug fail closed.
|
|
522
|
+
identity = writePidFile(serverName, configHash, generation);
|
|
184
523
|
// Connect to MCP server
|
|
185
524
|
try {
|
|
186
525
|
debug(`[daemon:${serverName}] Connecting to MCP server...`);
|
|
@@ -207,6 +546,17 @@ export async function runDaemon(serverName, config) {
|
|
|
207
546
|
};
|
|
208
547
|
}
|
|
209
548
|
debug(`[daemon:${serverName}] Request: ${request.type} (${request.id})`);
|
|
549
|
+
if (request.type !== 'ping' &&
|
|
550
|
+
request.generation !== identity?.generation) {
|
|
551
|
+
return {
|
|
552
|
+
id: request.id,
|
|
553
|
+
success: false,
|
|
554
|
+
error: {
|
|
555
|
+
code: 'STALE_DAEMON_GENERATION',
|
|
556
|
+
message: 'Daemon generation changed; reconnect before retrying',
|
|
557
|
+
},
|
|
558
|
+
};
|
|
559
|
+
}
|
|
210
560
|
if (!mcpClient) {
|
|
211
561
|
return {
|
|
212
562
|
id: request.id,
|
|
@@ -217,7 +567,7 @@ export async function runDaemon(serverName, config) {
|
|
|
217
567
|
try {
|
|
218
568
|
switch (request.type) {
|
|
219
569
|
case 'ping':
|
|
220
|
-
return { id: request.id, success: true, data:
|
|
570
|
+
return { id: request.id, success: true, data: identity };
|
|
221
571
|
case 'listTools': {
|
|
222
572
|
const tools = await listTools(mcpClient.client);
|
|
223
573
|
return { id: request.id, success: true, data: tools };
|
|
@@ -264,14 +614,28 @@ export async function runDaemon(serverName, config) {
|
|
|
264
614
|
};
|
|
265
615
|
}
|
|
266
616
|
};
|
|
267
|
-
// Start Unix socket server
|
|
617
|
+
// Start the Unix domain socket or Windows named-pipe server
|
|
268
618
|
try {
|
|
269
619
|
server = createServer((socket) => {
|
|
270
620
|
activeConnections.add(socket);
|
|
271
621
|
debug(`[daemon:${serverName}] Client connected`);
|
|
622
|
+
let requestBuffer = '';
|
|
623
|
+
socket.setEncoding('utf8');
|
|
272
624
|
socket.on('data', async (data) => {
|
|
273
|
-
|
|
274
|
-
|
|
625
|
+
requestBuffer += data;
|
|
626
|
+
if (requestBuffer.length > 1024 * 1024) {
|
|
627
|
+
socket.destroy(new Error('Daemon request exceeds 1 MiB'));
|
|
628
|
+
return;
|
|
629
|
+
}
|
|
630
|
+
while (requestBuffer.includes('\n')) {
|
|
631
|
+
const newlineIndex = requestBuffer.indexOf('\n');
|
|
632
|
+
const requestText = requestBuffer.slice(0, newlineIndex);
|
|
633
|
+
requestBuffer = requestBuffer.slice(newlineIndex + 1);
|
|
634
|
+
if (!requestText.trim())
|
|
635
|
+
continue;
|
|
636
|
+
const response = await handleRequest(Buffer.from(requestText));
|
|
637
|
+
socket.write(`${JSON.stringify(response)}\n`);
|
|
638
|
+
}
|
|
275
639
|
});
|
|
276
640
|
socket.on('close', () => {
|
|
277
641
|
activeConnections.delete(socket);
|
|
@@ -292,8 +656,7 @@ export async function runDaemon(serverName, config) {
|
|
|
292
656
|
debug(`[daemon:${serverName}] Listening on ${socketPath}`);
|
|
293
657
|
// Start idle timer
|
|
294
658
|
resetIdleTimer();
|
|
295
|
-
//
|
|
296
|
-
console.log('DAEMON_READY');
|
|
659
|
+
// The client detects readiness by pinging the IPC endpoint.
|
|
297
660
|
}
|
|
298
661
|
catch (error) {
|
|
299
662
|
console.error(`[daemon:${serverName}] Failed to start socket server:`, error.message);
|
|
@@ -308,8 +671,9 @@ export async function runDaemon(serverName, config) {
|
|
|
308
671
|
if (process.argv[2] === '--daemon') {
|
|
309
672
|
const serverName = process.argv[3];
|
|
310
673
|
const configJson = process.argv[4];
|
|
311
|
-
|
|
312
|
-
|
|
674
|
+
const generation = process.argv[5];
|
|
675
|
+
if (!serverName || !configJson || !generation) {
|
|
676
|
+
console.error('Usage: daemon.ts --daemon <serverName> <configJson> <generation>');
|
|
313
677
|
process.exit(1);
|
|
314
678
|
}
|
|
315
679
|
let config;
|
|
@@ -320,7 +684,7 @@ if (process.argv[2] === '--daemon') {
|
|
|
320
684
|
console.error('Invalid config JSON');
|
|
321
685
|
process.exit(1);
|
|
322
686
|
}
|
|
323
|
-
runDaemon(serverName, config).catch((error) => {
|
|
687
|
+
runDaemon(serverName, config, generation).catch((error) => {
|
|
324
688
|
console.error('Daemon failed:', error);
|
|
325
689
|
process.exit(1);
|
|
326
690
|
});
|
package/dist/plugins/builtin.js
CHANGED
|
@@ -367,8 +367,8 @@ export const piboCorePlugin = definePiboPlugin({
|
|
|
367
367
|
name: "session.fork_candidates",
|
|
368
368
|
description: "Return user messages that can be used as fork targets.",
|
|
369
369
|
slashCommands: ["fork-candidates"],
|
|
370
|
-
execute(context) {
|
|
371
|
-
return { messages: context.getForkCandidates() };
|
|
370
|
+
async execute(context) {
|
|
371
|
+
return { messages: await context.getForkCandidates() };
|
|
372
372
|
},
|
|
373
373
|
});
|
|
374
374
|
api.registerGatewayAction({
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { CODEX_NATIVE_ADAPTER_ID, CODEX_NATIVE_AGENT_RUNTIME_DRIVER, } from "../agent-runtimes/codex-native/adapter.js";
|
|
2
2
|
import { InitialSessionContextBuilder } from "../core/profiles.js";
|
|
3
|
+
import { createOpenAiCodexSpeechProvider } from "../speech/openai-codex.js";
|
|
3
4
|
import { definePiboPlugin } from "./registry.js";
|
|
4
5
|
export const CODEX_NATIVE_RUNTIME_INSTANCE_ID = "codex-native";
|
|
5
6
|
export const CODEX_NATIVE_PROFILE_NAME = "codex-native";
|
|
@@ -13,6 +14,7 @@ export const piboCodexNativePlugin = definePiboPlugin({
|
|
|
13
14
|
adapterId: CODEX_NATIVE_ADAPTER_ID,
|
|
14
15
|
displayName: "Native Codex App Server",
|
|
15
16
|
});
|
|
17
|
+
api.registerSpeechProvider(createOpenAiCodexSpeechProvider());
|
|
16
18
|
api.registerProfile({
|
|
17
19
|
name: CODEX_NATIVE_PROFILE_NAME,
|
|
18
20
|
description: "Native Codex App Server profile. Distinct from the Pi-backed Codex compatibility profile.",
|