@corenel/harness 0.2.0 → 0.4.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.
Files changed (163) hide show
  1. package/dist/acp/agentMap.d.ts +34 -0
  2. package/dist/acp/agentMap.d.ts.map +1 -0
  3. package/dist/acp/agentMap.js +110 -0
  4. package/dist/acp/agentMap.js.map +1 -0
  5. package/dist/acp/index.d.ts +4 -0
  6. package/dist/acp/index.d.ts.map +1 -0
  7. package/dist/acp/index.js +6 -0
  8. package/dist/acp/index.js.map +1 -0
  9. package/dist/acp/policyMap.d.ts +55 -0
  10. package/dist/acp/policyMap.d.ts.map +1 -0
  11. package/dist/acp/policyMap.js +121 -0
  12. package/dist/acp/policyMap.js.map +1 -0
  13. package/dist/acp/types.d.ts +41 -0
  14. package/dist/acp/types.d.ts.map +1 -0
  15. package/dist/acp/types.js +14 -0
  16. package/dist/acp/types.js.map +1 -0
  17. package/dist/api/backend.d.ts +39 -0
  18. package/dist/api/backend.d.ts.map +1 -0
  19. package/dist/api/backend.js +126 -0
  20. package/dist/api/backend.js.map +1 -0
  21. package/dist/api/client.d.ts +95 -0
  22. package/dist/api/client.d.ts.map +1 -0
  23. package/dist/api/client.js +212 -0
  24. package/dist/api/client.js.map +1 -0
  25. package/dist/api/fileMirrorSource.d.ts +24 -0
  26. package/dist/api/fileMirrorSource.d.ts.map +1 -0
  27. package/dist/api/fileMirrorSource.js +59 -0
  28. package/dist/api/fileMirrorSource.js.map +1 -0
  29. package/dist/api/mirror.d.ts +102 -0
  30. package/dist/api/mirror.d.ts.map +1 -0
  31. package/dist/api/mirror.js +204 -0
  32. package/dist/api/mirror.js.map +1 -0
  33. package/dist/api/mirrorScheduler.d.ts +56 -0
  34. package/dist/api/mirrorScheduler.d.ts.map +1 -0
  35. package/dist/api/mirrorScheduler.js +167 -0
  36. package/dist/api/mirrorScheduler.js.map +1 -0
  37. package/dist/compression.d.ts +15 -1
  38. package/dist/compression.d.ts.map +1 -1
  39. package/dist/compression.js +32 -1
  40. package/dist/compression.js.map +1 -1
  41. package/dist/core/loop.d.ts +12 -0
  42. package/dist/core/loop.d.ts.map +1 -1
  43. package/dist/core/loop.js +138 -14
  44. package/dist/core/loop.js.map +1 -1
  45. package/dist/core/spawn.d.ts +54 -0
  46. package/dist/core/spawn.d.ts.map +1 -0
  47. package/dist/core/spawn.js +58 -0
  48. package/dist/core/spawn.js.map +1 -0
  49. package/dist/dag/executeNode.js +2 -2
  50. package/dist/fileSchema.js +1 -1
  51. package/dist/guardrail/guardrail.js +1 -1
  52. package/dist/guardrail/index.js +7 -7
  53. package/dist/guardrail/permission-service.js +1 -1
  54. package/dist/guardrail/serialize.js +1 -1
  55. package/dist/guardrail/store.js +2 -2
  56. package/dist/install/lockfile.d.ts +79 -0
  57. package/dist/install/lockfile.d.ts.map +1 -0
  58. package/dist/install/lockfile.js +155 -0
  59. package/dist/install/lockfile.js.map +1 -0
  60. package/dist/install/store.d.ts +13 -0
  61. package/dist/install/store.d.ts.map +1 -0
  62. package/dist/install/store.js +38 -0
  63. package/dist/install/store.js.map +1 -0
  64. package/dist/log.d.ts.map +1 -1
  65. package/dist/log.js +6 -1
  66. package/dist/log.js.map +1 -1
  67. package/dist/prompts/config.js +2 -2
  68. package/dist/prompts/host.d.ts +4 -0
  69. package/dist/prompts/host.d.ts.map +1 -1
  70. package/dist/prompts/host.js.map +1 -1
  71. package/dist/prompts/index.d.ts.map +1 -1
  72. package/dist/prompts/index.js +13 -6
  73. package/dist/prompts/index.js.map +1 -1
  74. package/dist/prompts/library/defaults.generated.js +2 -2
  75. package/dist/prompts/library/defaults.generated.js.map +1 -1
  76. package/dist/prompts/library/index.js +1 -1
  77. package/dist/providers/contextWindow.d.ts +19 -0
  78. package/dist/providers/contextWindow.d.ts.map +1 -0
  79. package/dist/providers/contextWindow.js +68 -0
  80. package/dist/providers/contextWindow.js.map +1 -0
  81. package/dist/providers/gateway.d.ts +3 -0
  82. package/dist/providers/gateway.d.ts.map +1 -1
  83. package/dist/providers/gateway.js +4 -1
  84. package/dist/providers/gateway.js.map +1 -1
  85. package/dist/sessions/apiLeaseStore.d.ts +10 -0
  86. package/dist/sessions/apiLeaseStore.d.ts.map +1 -0
  87. package/dist/sessions/apiLeaseStore.js +41 -0
  88. package/dist/sessions/apiLeaseStore.js.map +1 -0
  89. package/dist/sessions/blobGc.d.ts +62 -0
  90. package/dist/sessions/blobGc.d.ts.map +1 -0
  91. package/dist/sessions/blobGc.js +154 -0
  92. package/dist/sessions/blobGc.js.map +1 -0
  93. package/dist/sessions/fileRecallStore.js +1 -1
  94. package/dist/sessions/fileSessionStore.js +1 -1
  95. package/dist/sessions/head.d.ts +90 -0
  96. package/dist/sessions/head.d.ts.map +1 -0
  97. package/dist/sessions/head.js +82 -0
  98. package/dist/sessions/head.js.map +1 -0
  99. package/dist/sessions/inflight.d.ts +58 -0
  100. package/dist/sessions/inflight.d.ts.map +1 -0
  101. package/dist/sessions/inflight.js +0 -0
  102. package/dist/sessions/inflight.js.map +1 -0
  103. package/dist/sessions/lease.d.ts +59 -0
  104. package/dist/sessions/lease.d.ts.map +1 -0
  105. package/dist/sessions/lease.js +115 -0
  106. package/dist/sessions/lease.js.map +1 -0
  107. package/dist/sessions/leaseStore.d.ts +46 -0
  108. package/dist/sessions/leaseStore.d.ts.map +1 -0
  109. package/dist/sessions/leaseStore.js +58 -0
  110. package/dist/sessions/leaseStore.js.map +1 -0
  111. package/dist/sessions/timeline.d.ts +134 -0
  112. package/dist/sessions/timeline.d.ts.map +1 -0
  113. package/dist/sessions/timeline.js +127 -0
  114. package/dist/sessions/timeline.js.map +1 -0
  115. package/dist/sidecar/host.d.ts +241 -5
  116. package/dist/sidecar/host.d.ts.map +1 -1
  117. package/dist/sidecar/host.js +630 -5
  118. package/dist/sidecar/host.js.map +1 -1
  119. package/dist/sidecar/server.d.ts +124 -2
  120. package/dist/sidecar/server.d.ts.map +1 -1
  121. package/dist/sidecar/server.js +620 -9
  122. package/dist/sidecar/server.js.map +1 -1
  123. package/dist/sidecar/wsChannel.d.ts +4 -0
  124. package/dist/sidecar/wsChannel.d.ts.map +1 -0
  125. package/dist/sidecar/wsChannel.js +62 -0
  126. package/dist/sidecar/wsChannel.js.map +1 -0
  127. package/dist/skills.js +2 -2
  128. package/dist/snapshot/effects.d.ts +69 -0
  129. package/dist/snapshot/effects.d.ts.map +1 -0
  130. package/dist/snapshot/effects.js +67 -0
  131. package/dist/snapshot/effects.js.map +1 -0
  132. package/dist/snapshot/manifest.d.ts +73 -0
  133. package/dist/snapshot/manifest.d.ts.map +1 -0
  134. package/dist/snapshot/manifest.js +141 -0
  135. package/dist/snapshot/manifest.js.map +1 -0
  136. package/dist/snapshot/tree.d.ts +62 -0
  137. package/dist/snapshot/tree.d.ts.map +1 -0
  138. package/dist/snapshot/tree.js +147 -0
  139. package/dist/snapshot/tree.js.map +1 -0
  140. package/dist/tools/builtins.d.ts.map +1 -1
  141. package/dist/tools/builtins.js +67 -8
  142. package/dist/tools/builtins.js.map +1 -1
  143. package/dist/tools/callAgent.d.ts +16 -0
  144. package/dist/tools/callAgent.d.ts.map +1 -0
  145. package/dist/tools/callAgent.js +53 -0
  146. package/dist/tools/callAgent.js.map +1 -0
  147. package/dist/tools/files.d.ts +10 -0
  148. package/dist/tools/files.d.ts.map +1 -1
  149. package/dist/tools/files.js +159 -8
  150. package/dist/tools/files.js.map +1 -1
  151. package/dist/tools/http.js +1 -1
  152. package/dist/tools/namespaces.d.ts +1 -1
  153. package/dist/tools/namespaces.d.ts.map +1 -1
  154. package/dist/tools/namespaces.js +40 -2
  155. package/dist/tools/namespaces.js.map +1 -1
  156. package/dist/tools/procs.d.ts +18 -0
  157. package/dist/tools/procs.d.ts.map +1 -0
  158. package/dist/tools/procs.js +281 -0
  159. package/dist/tools/procs.js.map +1 -0
  160. package/dist/tools/todo.d.ts.map +1 -1
  161. package/dist/tools/todo.js +12 -0
  162. package/dist/tools/todo.js.map +1 -1
  163. package/package.json +18 -7
@@ -57,6 +57,18 @@ export class SidecarHost {
57
57
  writable: true,
58
58
  value: new Map()
59
59
  });
60
+ Object.defineProperty(this, "acpSeq", {
61
+ enumerable: true,
62
+ configurable: true,
63
+ writable: true,
64
+ value: 0
65
+ });
66
+ Object.defineProperty(this, "acpPending", {
67
+ enumerable: true,
68
+ configurable: true,
69
+ writable: true,
70
+ value: new Map()
71
+ });
60
72
  Object.defineProperty(this, "syncSeq", {
61
73
  enumerable: true,
62
74
  configurable: true,
@@ -69,6 +81,59 @@ export class SidecarHost {
69
81
  writable: true,
70
82
  value: new Map()
71
83
  });
84
+ Object.defineProperty(this, "rootsSeq", {
85
+ enumerable: true,
86
+ configurable: true,
87
+ writable: true,
88
+ value: 0
89
+ });
90
+ Object.defineProperty(this, "rootsPending", {
91
+ enumerable: true,
92
+ configurable: true,
93
+ writable: true,
94
+ value: new Map()
95
+ });
96
+ Object.defineProperty(this, "procSeq", {
97
+ enumerable: true,
98
+ configurable: true,
99
+ writable: true,
100
+ value: 0
101
+ });
102
+ Object.defineProperty(this, "procPending", {
103
+ enumerable: true,
104
+ configurable: true,
105
+ writable: true,
106
+ value: new Map()
107
+ });
108
+ /** Watchers by procId, NOT by request id: a process outlives the request that
109
+ * spawned it, and the same process can be watched by a spawn and later by a
110
+ * reattach. Keyed by the only durable identity there is. */
111
+ Object.defineProperty(this, "procWatchers", {
112
+ enumerable: true,
113
+ configurable: true,
114
+ writable: true,
115
+ value: new Map()
116
+ });
117
+ Object.defineProperty(this, "sessionSeq", {
118
+ enumerable: true,
119
+ configurable: true,
120
+ writable: true,
121
+ value: 0
122
+ });
123
+ Object.defineProperty(this, "sessionPending", {
124
+ enumerable: true,
125
+ configurable: true,
126
+ writable: true,
127
+ value: new Map()
128
+ });
129
+ /** Per-session stream handlers, keyed by session because a session outlives
130
+ * the request that started watching it. */
131
+ Object.defineProperty(this, "sessionWatchers", {
132
+ enumerable: true,
133
+ configurable: true,
134
+ writable: true,
135
+ value: new Map()
136
+ });
72
137
  Object.defineProperty(this, "features", {
73
138
  enumerable: true,
74
139
  configurable: true,
@@ -98,10 +163,30 @@ export class SidecarHost {
98
163
  this.ch.onMessage((m) => this.onMessage(m));
99
164
  }
100
165
  /** Authenticate, then import the sidecar's tools. Resolves with their names,
101
- * or rejects if the sidecar denies the token / protocol. */
102
- connect() {
166
+ * or rejects if the sidecar denies the token / protocol.
167
+ *
168
+ * `timeoutMs`, when given, rejects the connection if no `welcome`/`denied`
169
+ * ever arrives -- a handshake that silently never completes (a peer that
170
+ * accepts the socket but never answers `hello`, e.g. something other than a
171
+ * SidecarServer on the other end) would otherwise hang this promise
172
+ * forever, with nothing to debug. Omit it to keep the old unbounded
173
+ * behavior (every existing caller relies on that default). */
174
+ connect(timeoutMs) {
103
175
  return new Promise((resolve, reject) => {
104
- this.welcome = { resolve, reject };
176
+ let timer;
177
+ const clear = () => { if (timer)
178
+ clearTimeout(timer); };
179
+ this.welcome = {
180
+ resolve: (names) => { clear(); resolve(names); },
181
+ reject: (e) => { clear(); reject(e); },
182
+ };
183
+ if (timeoutMs !== undefined) {
184
+ timer = setTimeout(() => {
185
+ const pending = this.welcome;
186
+ this.welcome = null;
187
+ pending?.reject(new Error(`sidecar handshake timed out after ${timeoutMs}ms waiting for welcome`));
188
+ }, timeoutMs);
189
+ }
105
190
  this.ch.send({
106
191
  t: 'hello', protocol: SIDECAR_PROTOCOL, token: this.opts.token,
107
192
  ...(this.opts.stateDir ? { stateDir: this.opts.stateDir } : {}),
@@ -124,9 +209,29 @@ export class SidecarHost {
124
209
  for (const p of this.runPending.values())
125
210
  p.reject(new Error(reason));
126
211
  this.runPending.clear();
212
+ for (const p of this.acpPending.values())
213
+ p.reject(new Error(reason));
214
+ this.acpPending.clear();
127
215
  for (const p of this.syncPending.values())
128
216
  p.reject(new Error(reason));
129
217
  this.syncPending.clear();
218
+ for (const p of this.rootsPending.values())
219
+ p.reject(new Error(reason));
220
+ this.rootsPending.clear();
221
+ /* Session and process calls settle too, and this was MISSING.
222
+ *
223
+ * Every other pending map was swept here and these two were not, so a
224
+ * `sessionPause` or `procStart` in flight when the daemon dropped never
225
+ * settled at all — not resolved, not rejected. A surface awaiting one sits
226
+ * on a disabled button reading "Starting…" until the page is reloaded,
227
+ * because the failure it is waiting to show never arrives. A dropped
228
+ * connection is an outcome; silence is not. */
229
+ for (const p of this.sessionPending.values())
230
+ p.reject(new Error(reason));
231
+ this.sessionPending.clear();
232
+ for (const p of this.procPending.values())
233
+ p.reject(new Error(reason));
234
+ this.procPending.clear();
130
235
  this.welcome?.reject(new Error(reason));
131
236
  this.welcome = null;
132
237
  this.ch.close();
@@ -161,6 +266,32 @@ export class SidecarHost {
161
266
  p.reject(new Error(m.error || 'sidecar tool failed'));
162
267
  return;
163
268
  }
269
+ case 'roots':
270
+ // Unsolicited: the set changed on the daemon (possibly via ANOTHER client).
271
+ this.opts.onRoots?.({ root: m.root, roots: m.roots });
272
+ return;
273
+ case 'roots-browse-result': {
274
+ const p = this.rootsPending.get(m.id);
275
+ if (!p)
276
+ return;
277
+ this.rootsPending.delete(m.id);
278
+ if (m.ok)
279
+ p.resolve(m.dirs ?? []);
280
+ else
281
+ p.reject(new Error(m.error || 'could not browse'));
282
+ return;
283
+ }
284
+ case 'roots-add-result': {
285
+ const p = this.rootsPending.get(m.id);
286
+ if (!p)
287
+ return;
288
+ this.rootsPending.delete(m.id);
289
+ if (m.ok)
290
+ p.resolve([]);
291
+ else
292
+ p.reject(new Error(m.error || 'could not add that working directory'));
293
+ return;
294
+ }
164
295
  case 'host-call':
165
296
  void this.serveHostCall(m);
166
297
  return;
@@ -180,6 +311,39 @@ export class SidecarHost {
180
311
  this.httpPending.get(m.id)?.onErr(m.error);
181
312
  return;
182
313
  }
314
+ case 'acp-started': {
315
+ const p = this.acpPending.get(m.id);
316
+ // The start call resolves, but the entry STAYS so later acp-update
317
+ // frames on this id still reach the caller's stream handler.
318
+ p?.resolve({
319
+ runId: m.runId, sessionId: m.sessionId, agent: m.agent,
320
+ ...(m.modes ? { modes: m.modes } : {}),
321
+ ...(m.currentModeId ? { currentModeId: m.currentModeId } : {}),
322
+ ...(m.resumed ? { resumed: true } : {}),
323
+ });
324
+ return;
325
+ }
326
+ case 'acp-mode-set': {
327
+ const p = this.acpPending.get(m.id);
328
+ this.acpPending.delete(m.id);
329
+ p?.resolve({ modeId: m.modeId });
330
+ return;
331
+ }
332
+ case 'acp-update':
333
+ this.acpPending.get(m.id)?.onUpdate?.(m.update);
334
+ return;
335
+ case 'acp-done': {
336
+ const p = this.acpPending.get(m.id);
337
+ this.acpPending.delete(m.id);
338
+ p?.resolve({ stopReason: m.stopReason });
339
+ return;
340
+ }
341
+ case 'acp-error': {
342
+ const p = this.acpPending.get(m.id);
343
+ this.acpPending.delete(m.id);
344
+ p?.reject(new Error(m.error));
345
+ return;
346
+ }
183
347
  case 'run-accepted': {
184
348
  const p = this.runPending.get(m.id);
185
349
  if (p)
@@ -226,16 +390,401 @@ export class SidecarHost {
226
390
  this.opts.onPermissionResolved?.({ id: m.id, decision: m.decision, by: m.by });
227
391
  return;
228
392
  }
393
+ case 'run-activity': {
394
+ this.opts.onRunActivity?.(m.activity);
395
+ return;
396
+ }
397
+ case 'proc-started': {
398
+ const p = this.procPending.get(m.id);
399
+ if (!p)
400
+ return;
401
+ this.procPending.delete(m.id);
402
+ p.resolve(m.proc);
403
+ return;
404
+ }
405
+ case 'proc-error': {
406
+ const p = this.procPending.get(m.id);
407
+ if (!p)
408
+ return;
409
+ this.procPending.delete(m.id);
410
+ p.reject(new Error(m.error));
411
+ return;
412
+ }
413
+ case 'proc-attach-result':
414
+ case 'proc-kill-result':
415
+ case 'proc-input-result': {
416
+ const p = this.procPending.get(m.id);
417
+ if (!p)
418
+ return;
419
+ this.procPending.delete(m.id);
420
+ if (m.ok)
421
+ p.resolve(m);
422
+ else
423
+ p.reject(new Error(m.error || 'process request failed'));
424
+ return;
425
+ }
426
+ case 'proc-list-result': {
427
+ const p = this.procPending.get(m.id);
428
+ if (!p)
429
+ return;
430
+ this.procPending.delete(m.id);
431
+ p.resolve(m.procs);
432
+ return;
433
+ }
434
+ case 'proc-out': {
435
+ for (const w of this.procWatchers.get(m.procId) ?? [])
436
+ w.onOut?.({ seq: m.seq, stream: m.stream, text: m.text });
437
+ return;
438
+ }
439
+ case 'proc-exit': {
440
+ const watchers = this.procWatchers.get(m.procId);
441
+ if (!watchers)
442
+ return;
443
+ // Delete BEFORE notifying: an onExit that re-spawns and re-watches must
444
+ // not have its new registration wiped by this cleanup.
445
+ this.procWatchers.delete(m.procId);
446
+ for (const w of watchers)
447
+ w.onExit?.({ code: m.code, ...(m.signal ? { signal: m.signal } : {}) });
448
+ return;
449
+ }
450
+ case 'session-list-result': {
451
+ const p = this.sessionPending.get(m.id);
452
+ if (!p)
453
+ return;
454
+ this.sessionPending.delete(m.id);
455
+ p.resolve(m.sessions);
456
+ return;
457
+ }
458
+ case 'session-created': {
459
+ const p = this.sessionPending.get(m.id);
460
+ if (!p)
461
+ return;
462
+ this.sessionPending.delete(m.id);
463
+ p.resolve(m.session);
464
+ return;
465
+ }
466
+ case 'session-paused': {
467
+ const p = this.sessionPending.get(m.id);
468
+ if (!p)
469
+ return;
470
+ this.sessionPending.delete(m.id);
471
+ p.resolve({ drained: m.drained, remaining: m.remaining });
472
+ return;
473
+ }
474
+ case 'session-committed': {
475
+ const p = this.sessionPending.get(m.id);
476
+ if (!p)
477
+ return;
478
+ this.sessionPending.delete(m.id);
479
+ p.resolve(m.snapshot);
480
+ return;
481
+ }
482
+ case 'run-transcript-result': {
483
+ const p = this.sessionPending.get(m.id);
484
+ if (!p)
485
+ return;
486
+ this.sessionPending.delete(m.id);
487
+ p.resolve(m.lines);
488
+ return;
489
+ }
490
+ case 'run-history-result': {
491
+ const p = this.sessionPending.get(m.id);
492
+ if (!p)
493
+ return;
494
+ this.sessionPending.delete(m.id);
495
+ p.resolve(m.runs);
496
+ return;
497
+ }
498
+ case 'session-snapshots-result': {
499
+ const p = this.sessionPending.get(m.id);
500
+ if (!p)
501
+ return;
502
+ this.sessionPending.delete(m.id);
503
+ p.resolve(m.snapshots);
504
+ return;
505
+ }
506
+ case 'session-snapshot-usage-result': {
507
+ const p = this.sessionPending.get(m.id);
508
+ if (!p)
509
+ return;
510
+ this.sessionPending.delete(m.id);
511
+ p.resolve(m.usage);
512
+ return;
513
+ }
514
+ case 'session-ok': {
515
+ const p = this.sessionPending.get(m.id);
516
+ if (!p)
517
+ return;
518
+ this.sessionPending.delete(m.id);
519
+ p.resolve(undefined);
520
+ return;
521
+ }
522
+ case 'session-error': {
523
+ const p = this.sessionPending.get(m.id);
524
+ if (!p)
525
+ return;
526
+ this.sessionPending.delete(m.id);
527
+ p.reject(new Error(m.error));
528
+ return;
529
+ }
530
+ case 'session-metrics': {
531
+ this.sessionWatchers.get(m.sessionId)?.onMetrics?.(m.sample);
532
+ return;
533
+ }
534
+ case 'session-log': {
535
+ this.sessionWatchers.get(m.sessionId)?.onLog?.(m.line);
536
+ return;
537
+ }
538
+ case 'registrations': {
539
+ this.opts.onRegistrations?.(m.registrations);
540
+ return;
541
+ }
229
542
  default:
230
543
  return; // hello / invoke / host-call-result are handled on the sidecar side
231
544
  }
232
545
  }
233
546
  /** Whether the connected sidecar advertised the loopback HTTP proxy. */
234
547
  supportsHttpProxy() { return this.features.includes('http-proxy'); }
548
+ /** Everything the sidecar advertised. A copy — the internal list must not be
549
+ * mutable from outside. For surfaces that SHOW capabilities; code that DEPENDS
550
+ * on one should use the specific supportsX() so the feature name lives here. */
551
+ advertisedFeatures() { return [...this.features]; }
235
552
  /** Whether the connected sidecar advertised a wired crew runner — i.e. it can
236
553
  * actually fire daemon-tier triggers (schedule/continuous/self-directed) on its
237
554
  * own clock, not just serve tools. */
238
555
  supportsRunner() { return this.features.includes('runner'); }
556
+ /** True when the daemon was started with --acp and can proxy an external
557
+ * agent. A client must check this before offering the surface: a daemon
558
+ * without it answers acp-start with an error. */
559
+ supportsAcp() { return this.features.includes('acp'); }
560
+ /** True when the daemon can run long-lived processes - i.e. the operator
561
+ * allowed shell. A client MUST check this before offering the surface: a
562
+ * daemon without it answers every proc message with an error. */
563
+ supportsProcs() { return this.features.includes('procs'); }
564
+ /** Start a long-running command on the sidecar and stream its output.
565
+ *
566
+ * This is the counterpart to the one-shot `sidecar_shell_exec` tool, not a
567
+ * replacement: use the tool when you want the result of something that
568
+ * finishes, and this when you want to WATCH something that may not.
569
+ *
570
+ * The returned promise settles on the spawn, not on the exit - the process is
571
+ * still running when it resolves. `watch` keeps receiving output until
572
+ * `onExit`, which fires exactly once.
573
+ */
574
+ procSpawn(command, watch = {}, opts) {
575
+ const id = ++this.procSeq;
576
+ return new Promise((resolve, reject) => {
577
+ this.procPending.set(id, {
578
+ resolve: (v) => {
579
+ const info = v;
580
+ this.addProcWatcher(info.procId, watch);
581
+ resolve(info);
582
+ },
583
+ reject,
584
+ });
585
+ this.ch.send({
586
+ t: 'proc-spawn', id, command,
587
+ ...(opts?.cwd ? { cwd: opts.cwd } : {}),
588
+ ...(opts?.root ? { root: opts.root } : {}),
589
+ ...(opts?.label ? { label: opts.label } : {}),
590
+ });
591
+ });
592
+ }
593
+ /** Re-attach to a process the daemon is already running - after a reload, or
594
+ * from a second tab. `sinceSeq` is the last chunk this client already has, so
595
+ * the replay is exactly the rest; omit it to replay the whole buffered tail.
596
+ *
597
+ * `truncated` in the result means the daemon's tail no longer reaches back to
598
+ * where you were: there is a GAP, and a UI that renders the replay as if it
599
+ * were continuous would be lying about what the process printed. */
600
+ procAttach(procId, watch = {}, sinceSeq) {
601
+ const id = ++this.procSeq;
602
+ return new Promise((resolve, reject) => {
603
+ this.procPending.set(id, {
604
+ // Register BEFORE resolving so output the daemon replays immediately
605
+ // after the result cannot arrive ahead of the watcher.
606
+ resolve: (v) => {
607
+ const r = v;
608
+ if (!r.proc) {
609
+ reject(new Error('attach returned no process'));
610
+ return;
611
+ }
612
+ this.addProcWatcher(procId, watch);
613
+ resolve({ proc: r.proc, truncated: r.truncated === true });
614
+ },
615
+ reject,
616
+ });
617
+ this.ch.send({ t: 'proc-attach', id, procId, ...(sinceSeq === undefined ? {} : { sinceSeq }) });
618
+ });
619
+ }
620
+ /** Everything the daemon is running, or recently ran. Called on attach: a
621
+ * process nobody can see is worse than one that died. */
622
+ procList() {
623
+ const id = ++this.procSeq;
624
+ return new Promise((resolve, reject) => {
625
+ this.procPending.set(id, { resolve: (v) => resolve(v), reject });
626
+ this.ch.send({ t: 'proc-list', id });
627
+ });
628
+ }
629
+ /** Write to a running process's stdin. */
630
+ procInput(procId, text) {
631
+ const id = ++this.procSeq;
632
+ return new Promise((resolve, reject) => {
633
+ this.procPending.set(id, { resolve: () => resolve(), reject });
634
+ this.ch.send({ t: 'proc-input', id, procId, text });
635
+ });
636
+ }
637
+ /** Stop a process. Idempotent on the daemon side. */
638
+ procKill(procId) {
639
+ const id = ++this.procSeq;
640
+ return new Promise((resolve, reject) => {
641
+ this.procPending.set(id, { resolve: () => resolve(), reject });
642
+ this.ch.send({ t: 'proc-kill', id, procId });
643
+ });
644
+ }
645
+ /** Stop receiving a process's output without stopping the process. */
646
+ procUnwatch(procId, watch) {
647
+ const set = this.procWatchers.get(procId);
648
+ if (!set)
649
+ return;
650
+ set.delete(watch);
651
+ if (!set.size)
652
+ this.procWatchers.delete(procId);
653
+ }
654
+ addProcWatcher(procId, watch) {
655
+ const set = this.procWatchers.get(procId) ?? new Set();
656
+ set.add(watch);
657
+ this.procWatchers.set(procId, set);
658
+ }
659
+ /** True when the daemon runs sessions at all. A client MUST check this before
660
+ * offering the surface: a daemon without it answers every session message
661
+ * with an error. */
662
+ supportsSessions() { return this.features.includes('sessions'); }
663
+ /**
664
+ * Whether the OPERATOR allowed run transcripts to be kept at all.
665
+ *
666
+ * False means no run on this daemon can be recorded however it asks, so a
667
+ * surface must not offer the choice — a control that cannot be honoured is
668
+ * worse than an absent one. True does NOT mean recording is on: each run
669
+ * still opts in (`run(..., { keepTranscript: true })`).
670
+ */
671
+ supportsKeepTranscripts() { return this.features.includes('keep-transcripts'); }
672
+ /** Every session the daemon knows about. */
673
+ sessionList() {
674
+ return this.sessionCall((id) => ({ t: 'session-list', id }));
675
+ }
676
+ /**
677
+ * Create a session on a directory, or FROM a repository.
678
+ *
679
+ * Exactly one of `workdir` and `repo`. With `repo` the daemon clones on its
680
+ * own host, using its own git credentials, and mounts the result — which is
681
+ * why the caller cannot name the directory: it does not exist yet. The
682
+ * returned SessionInfo carries `source` with the resolved commit.
683
+ */
684
+ sessionCreate(spec) {
685
+ return this.sessionCall((id) => ({
686
+ t: 'session-create', id,
687
+ ...(spec.workdir ? { workdir: spec.workdir } : {}),
688
+ ...(spec.repo ? { repo: spec.repo } : {}),
689
+ ...(spec.owner ? { owner: spec.owner } : {}),
690
+ ...(spec.fromSnapshot ? { fromSnapshot: spec.fromSnapshot } : {}),
691
+ }));
692
+ }
693
+ /**
694
+ * Whether this daemon can create a session FROM a repository.
695
+ *
696
+ * False means the operator declared no `--clone-under` directory, so there is
697
+ * nowhere a checkout could be written and the option must not be offered.
698
+ */
699
+ supportsSessionClone() { return this.features.includes('session-clone'); }
700
+ /**
701
+ * Pause a session, and learn whether it actually drained.
702
+ *
703
+ * `drained: false` means work was STILL RUNNING when the wait gave up. A
704
+ * caller about to capture needs that: it is the difference between a snapshot
705
+ * and a torn one.
706
+ */
707
+ sessionPause(sessionId, drainMs) {
708
+ return this.sessionCall((id) => ({
709
+ t: 'session-pause', id, sessionId, ...(drainMs === undefined ? {} : { drainMs }),
710
+ }));
711
+ }
712
+ sessionResume(sessionId) {
713
+ return this.sessionCall((id) => ({ t: 'session-resume', id, sessionId }));
714
+ }
715
+ sessionDestroy(sessionId) {
716
+ return this.sessionCall((id) => ({ t: 'session-destroy', id, sessionId }));
717
+ }
718
+ sessionCommit(sessionId, name) {
719
+ return this.sessionCall((id) => ({
720
+ t: 'session-commit', id, sessionId, ...(name ? { name } : {}),
721
+ }));
722
+ }
723
+ /**
724
+ * One agent's finished runs, newest first, as the DAEMON recorded them.
725
+ *
726
+ * The durable counterpart to the live `run-activity` broadcasts: those say
727
+ * what is happening while this browser is attached and are lost on
728
+ * disconnect, this says what happened at all. Scoped to this client by the
729
+ * daemon, from the identity the connection authenticated with.
730
+ *
731
+ * An empty list means no record, which a caller renders rather than treats
732
+ * as an error — a daemon that keeps none is a normal daemon.
733
+ */
734
+ runHistory(agent, limit) {
735
+ return this.sessionCall((id) => ({
736
+ t: 'run-history', id, agent, ...(limit === undefined ? {} : { limit }),
737
+ }));
738
+ }
739
+ /**
740
+ * One run's kept events, as the daemon stored them.
741
+ *
742
+ * Empty means the words were never kept — the default, since transcripts are
743
+ * opt-in. A caller decides what to show from `RunRecordInfo.transcript`,
744
+ * which says whether there was ever anything to fetch.
745
+ */
746
+ runTranscript(agent, runId) {
747
+ return this.sessionCall((id) => ({ t: 'run-transcript', id, agent, runId }));
748
+ }
749
+ /** Every capture this daemon could restore from, newest first.
750
+ *
751
+ * An empty list is a real answer — a daemon whose backend cannot capture has
752
+ * nothing to restore from — so a caller uses the length to decide whether to
753
+ * offer the choice, not to detect a failure. */
754
+ sessionSnapshots() {
755
+ return this.sessionCall((id) => ({ t: 'session-snapshots', id }));
756
+ }
757
+ /** Usage against the snapshot refuse-at-budget ceiling, BEFORE a capture
758
+ * ever hits it. `budgetBytes: 0` means the daemon's backend reports no
759
+ * budget at all (no capture capability) — a caller renders that as
760
+ * "not applicable", not as zero headroom. */
761
+ sessionSnapshotUsage() {
762
+ return this.sessionCall((id) => ({ t: 'session-snapshot-usage', id }));
763
+ }
764
+ /** Start receiving a session's metrics and logs. */
765
+ async sessionWatch(sessionId, watch) {
766
+ // Registered BEFORE the request, so a sample that arrives immediately after
767
+ // the daemon starts following is not dropped on the floor.
768
+ this.sessionWatchers.set(sessionId, watch);
769
+ try {
770
+ await this.sessionCall((id) => ({ t: 'session-watch', id, sessionId }));
771
+ }
772
+ catch (e) {
773
+ this.sessionWatchers.delete(sessionId);
774
+ throw e;
775
+ }
776
+ }
777
+ async sessionUnwatch(sessionId) {
778
+ this.sessionWatchers.delete(sessionId);
779
+ await this.sessionCall((id) => ({ t: 'session-unwatch', id, sessionId }));
780
+ }
781
+ sessionCall(build) {
782
+ const id = ++this.sessionSeq;
783
+ return new Promise((resolve, reject) => {
784
+ this.sessionPending.set(id, { resolve, reject });
785
+ this.ch.send(build(id));
786
+ });
787
+ }
239
788
  /** The sidecar's advertised self-description (root/platform), or {} if it sent
240
789
  * none. Valid only after connect() resolves. */
241
790
  sidecarInfo() { return this.info; }
@@ -276,13 +825,65 @@ export class SidecarHost {
276
825
  * `onEvent` as they arrive. Resolves once the run completes (`run-done`) with
277
826
  * the daemon-assigned runId, final text, and stop reason; rejects on
278
827
  * `run-error` or if the connection drops while the run is in flight. */
279
- run(agent, input, onEvent) {
828
+ run(agent, input, onEvent, opts) {
280
829
  const id = ++this.runSeq;
281
830
  return new Promise((resolve, reject) => {
282
831
  this.runPending.set(id, { resolve, reject, onEvent });
283
- this.ch.send({ t: 'run', id, agent, input });
832
+ // `sessionId` asks for the run to happen INSIDE that session. A daemon
833
+ // that cannot honour it answers run-error rather than running it on the
834
+ // host, so the caller learns rather than being quietly downgraded.
835
+ // Sent only when asked for, so an unset option is indistinguishable on the
836
+ // wire from an older client that never had one — both mean keep nothing.
837
+ this.ch.send({
838
+ t: 'run', id, agent, input,
839
+ ...(opts?.sessionId ? { sessionId: opts.sessionId } : {}),
840
+ ...(opts?.keepTranscript ? { keepTranscript: true } : {}),
841
+ });
842
+ });
843
+ }
844
+ /** Start a proxied external ACP agent on the sidecar. The returned promise
845
+ * settles on the handshake; `onUpdate` keeps receiving the agent's stream for
846
+ * the life of the session. Permission asks do NOT come through here — they use
847
+ * the existing permission-ask broadcast, the same path a crew run uses. */
848
+ acpStart(target, onUpdate, opts) {
849
+ const id = ++this.acpSeq;
850
+ return new Promise((resolve, reject) => {
851
+ this.acpPending.set(id, { resolve: resolve, reject, onUpdate });
852
+ // `root` is the working directory the USER has active. The daemon checks it
853
+ // against the operator's approved set and refuses anything else, so naming
854
+ // one here is a request, not a grant.
855
+ // `modeId` is a mode the user picked BEFORE a session existed - modes are
856
+ // only known after session/new, so an early choice has to be carried into
857
+ // the start rather than applied after it. `resume` asks the agent to
858
+ // reattach to a session it may or may not still hold.
859
+ this.ch.send({
860
+ t: 'acp-start', id, target,
861
+ ...(opts?.root ? { root: opts.root } : {}),
862
+ ...(opts?.modeId ? { modeId: opts.modeId } : {}),
863
+ ...(opts?.resume ? { resume: opts.resume } : {}),
864
+ });
865
+ });
866
+ }
867
+ /** Send one turn to the proxied agent. */
868
+ acpPrompt(text, content) {
869
+ const id = ++this.acpSeq;
870
+ return new Promise((resolve, reject) => {
871
+ this.acpPending.set(id, { resolve: resolve, reject });
872
+ this.ch.send({ t: 'acp-prompt', id, text, ...(content?.length ? { content: [...content] } : {}) });
873
+ });
874
+ }
875
+ /** Switch the proxied agent's mode. Rejects when it does not offer that mode. */
876
+ acpSetMode(modeId) {
877
+ const id = ++this.acpSeq;
878
+ return new Promise((resolve, reject) => {
879
+ this.acpPending.set(id, { resolve: resolve, reject });
880
+ this.ch.send({ t: 'acp-set-mode', id, modeId });
284
881
  });
285
882
  }
883
+ /** Interrupt the current turn. Fire-and-forget, like cancel in ACP itself. */
884
+ acpCancel() {
885
+ this.ch.send({ t: 'acp-cancel', id: ++this.acpSeq });
886
+ }
286
887
  /** Answer an outstanding permission ask. Fire-and-forget over the wire — the
287
888
  * actual drop of the prompt is driven by the server's `permission-resolved`
288
889
  * broadcast (`onPermissionResolved`), not by this call returning. */
@@ -311,6 +912,30 @@ export class SidecarHost {
311
912
  this.ch.send({ t: 'host-sync-delete', id, path });
312
913
  });
313
914
  }
915
+ /** True when the operator started the sidecar with --allow-add-under, so a client
916
+ * may add a working directory this session. Feature-detected from the welcome --
917
+ * an older sidecar simply never advertises it. */
918
+ canAddRoots() { return this.features.includes('add-roots'); }
919
+ /** Directory NAMES under `path`, or the declared areas themselves when `path` is
920
+ * omitted. Names only: never files, contents or stats. Rejects with the sidecar's
921
+ * message (e.g. outside every allowed area). */
922
+ browseRoots(path) {
923
+ const id = ++this.rootsSeq;
924
+ return new Promise((resolve, reject) => {
925
+ this.rootsPending.set(id, { resolve, reject });
926
+ this.ch.send(path === undefined ? { t: 'roots-browse', id } : { t: 'roots-browse', id, path });
927
+ });
928
+ }
929
+ /** Promote `path` to a working directory for the rest of the session. Resolves on
930
+ * acceptance -- the resulting set arrives separately via `onRoots`, broadcast to
931
+ * every connected client -- and rejects with the sidecar's refusal message. */
932
+ async addRoot(path) {
933
+ const id = ++this.rootsSeq;
934
+ await new Promise((resolve, reject) => {
935
+ this.rootsPending.set(id, { resolve, reject });
936
+ this.ch.send({ t: 'roots-add', id, path });
937
+ });
938
+ }
314
939
  invoke(name, args) {
315
940
  const id = ++this.invokeSeq;
316
941
  return new Promise((resolve, reject) => {