@pasko70/pibo 2.5.2 → 3.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/context/pibo-native-tooling.md +1 -0
  2. package/dist/agent-runtime/contract.js +2 -1
  3. package/dist/agent-runtime/history.js +53 -1
  4. package/dist/agent-runtime/routed-session.js +100 -26
  5. package/dist/agent-runtimes/codex-native/adapter.js +304 -30
  6. package/dist/agent-runtimes/codex-native/client.js +22 -3
  7. package/dist/agent-runtimes/codex-native/first-use.js +294 -0
  8. package/dist/agent-runtimes/codex-native/history.js +26 -7
  9. package/dist/agent-runtimes/codex-native/process.js +14 -0
  10. package/dist/agent-runtimes/codex-native/thread.js +59 -25
  11. package/dist/agent-runtimes/codex-native/turn.js +27 -0
  12. package/dist/agent-runtimes/history-proof.js +8 -0
  13. package/dist/agent-runtimes/omp/adapter.js +61 -14
  14. package/dist/agent-runtimes/omp/history.js +189 -34
  15. package/dist/agent-runtimes/omp/thread.js +82 -36
  16. package/dist/agent-runtimes/pi/adapter.js +30 -3
  17. package/dist/agent-runtimes/pi/history.js +66 -5
  18. package/dist/agent-runtimes/pi/openai-response-observer.js +102 -0
  19. package/dist/agent-runtimes/pi/routed-session.js +344 -37
  20. package/dist/apps/chat/chat-api-routes.js +1 -1
  21. package/dist/apps/chat/chat-files.js +130 -9
  22. package/dist/apps/chat/chat-settings-routes.js +27 -2
  23. package/dist/apps/chat/chat-speech.js +100 -0
  24. package/dist/apps/chat/data/chat-data-mappers.js +25 -5
  25. package/dist/apps/chat/data/timeline-query-service.js +89 -1
  26. package/dist/apps/chat/trace-v2.js +210 -8
  27. package/dist/apps/chat/trace.js +3 -0
  28. package/dist/apps/chat/web-app.js +108 -3
  29. package/dist/apps/chat-ui/assets/{dist-DTWJf37A.js → dist-BBDMeU5-.js} +1 -1
  30. package/dist/apps/chat-ui/assets/{dist-CkYWKnvy.js → dist-BDIATCQt.js} +1 -1
  31. package/dist/apps/chat-ui/assets/{dist-DZ8Wpj-H.js → dist-JnW4v8UE.js} +1 -1
  32. package/dist/apps/chat-ui/assets/{dist-Dmblz15N.js → dist-lRiLP9rr.js} +1 -1
  33. package/dist/apps/chat-ui/assets/{dist-DjcwoyNY.js → dist-mqCviI-c.js} +1 -1
  34. package/dist/apps/chat-ui/assets/index-0XOOVxLP.js +228 -0
  35. package/dist/apps/chat-ui/assets/index-DV7_CgsC.css +1 -0
  36. package/dist/apps/chat-ui/index.html +2 -2
  37. package/dist/apps/chat-vscode-web/assets/index-Dtw2Z7jz.js +43 -0
  38. package/dist/apps/chat-vscode-web/assets/{index-CF1-9PKU.css → index-SluZr_-r.css} +1 -1
  39. package/dist/apps/chat-vscode-web/index.html +2 -2
  40. package/dist/apps/vscode-artifacts/latest.vsix +0 -0
  41. package/dist/apps/vscode-artifacts/pibo-vscode-ext-3.0.1.vsix +0 -0
  42. package/dist/cli.js +49 -5
  43. package/dist/config/config.js +59 -8
  44. package/dist/core/preview-server-settings.js +20 -0
  45. package/dist/core/session-router.js +464 -37
  46. package/dist/core/user-settings.js +10 -0
  47. package/dist/data/payload-store.js +57 -1
  48. package/dist/data/schema.js +18 -1
  49. package/dist/debug/trace.js +5 -0
  50. package/dist/gateway/client.js +368 -65
  51. package/dist/gateway/protocol.js +2 -0
  52. package/dist/gateway/server.js +8 -0
  53. package/dist/gateway/web.js +2 -0
  54. package/dist/index.js +1 -1
  55. package/dist/loops/store.js +6 -4
  56. package/dist/mcp/config.js +89 -17
  57. package/dist/mcp/daemon-client.js +473 -153
  58. package/dist/mcp/daemon.js +401 -37
  59. package/dist/plugins/builtin.js +2 -2
  60. package/dist/plugins/codex-native.js +2 -0
  61. package/dist/plugins/registry.js +259 -1
  62. package/dist/previews/cli.js +345 -0
  63. package/dist/previews/config.js +61 -0
  64. package/dist/previews/manager.js +644 -0
  65. package/dist/previews/network.js +198 -0
  66. package/dist/previews/plugin.js +11 -0
  67. package/dist/previews/process-supervisor.js +38 -0
  68. package/dist/previews/proxy.js +387 -0
  69. package/dist/previews/store.js +750 -0
  70. package/dist/previews/types.js +1 -0
  71. package/dist/previews/web-app.js +446 -0
  72. package/dist/runs/registry.js +22 -0
  73. package/dist/runs/tools.js +46 -7
  74. package/dist/session-ui/terminalRows.js +187 -13
  75. package/dist/sessions/pibo-data-store.js +37 -0
  76. package/dist/sessions/runtime-binding-persistence.js +34 -0
  77. package/dist/sessions/runtime-binding.js +6 -0
  78. package/dist/sessions/sqlite-store.js +37 -0
  79. package/dist/shared/trace-engine.js +12 -64
  80. package/dist/shared/trace-event-projection.js +16 -0
  81. package/dist/shared/trace-history.js +684 -184
  82. package/dist/shared/trace-limits.js +7 -0
  83. package/dist/shared/trace-live-patch.js +61 -0
  84. package/dist/shared/trace-tool-identity.js +43 -0
  85. package/dist/speech/openai-codex-realtime-call-proxy.js +248 -0
  86. package/dist/speech/openai-codex.js +407 -0
  87. package/dist/speech/types.js +8 -0
  88. package/dist/subagents/context.js +5 -3
  89. package/dist/subagents/observations.js +94 -6
  90. package/dist/subagents/tool.js +103 -20
  91. package/dist/tools/codex-image-artifacts.js +24 -0
  92. package/dist/tools/codex-image-generation.js +4 -22
  93. package/dist/web/channel.js +88 -4
  94. package/dist/web/http.js +2 -1
  95. package/npm-shrinkwrap.json +2 -2
  96. package/package.json +1 -1
  97. package/dist/apps/chat-ui/assets/index-0WZI2phJ.css +0 -1
  98. package/dist/apps/chat-ui/assets/index-DsgTm1Aw.js +0 -228
  99. package/dist/apps/chat-vscode-web/assets/index-DvTSSvzN.js +0 -43
  100. package/dist/apps/vscode-artifacts/pibo-vscode-ext-2.5.2.vsix +0 -0
@@ -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 for IPC.
5
- * It maintains the MCP server connection and forwards requests from CLI invocations.
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 { existsSync, mkdirSync, readFileSync, unlinkSync, writeFileSync, } from 'node:fs';
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), { mode: 0o600 });
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 readPidFile(serverName) {
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
- const pidPath = getPidPath(serverName);
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
- if (existsSync(pidPath)) {
54
- unlinkSync(pidPath);
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
- // Ignore errors during cleanup
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
- // Ignore errors during cleanup
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
- * Kill a process by PID
89
- */
90
- export function killProcess(pid) {
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 cleanup = async () => {
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
- // Clean up files
151
- removeSocketFile(serverName);
152
- removePidFile(serverName);
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
- // Remove stale socket if exists
181
- removeSocketFile(serverName);
182
- // Write PID file
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: 'pong' };
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
- const response = await handleRequest(Buffer.isBuffer(data) ? data : Buffer.from(data));
274
- socket.write(`${JSON.stringify(response)}\n`);
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
- // Signal readiness by writing to stdout (parent will read this)
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
- if (!serverName || !configJson) {
312
- console.error('Usage: daemon.ts --daemon <serverName> <configJson>');
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
  });
@@ -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.",