omp-conductor 0.18.2 → 0.19.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 (60) hide show
  1. package/README.md +105 -40
  2. package/REFERENCE.md +865 -30
  3. package/package.json +1 -1
  4. package/schema/config.schema.json +26 -0
  5. package/src/admission.ts +212 -26
  6. package/src/ask.ts +288 -1
  7. package/src/briefs/orchestrator.md +6 -5
  8. package/src/cli.ts +5 -1
  9. package/src/command-help.ts +9 -1
  10. package/src/command-manifest.ts +36 -3
  11. package/src/commands/arm.ts +5 -1
  12. package/src/commands/context.ts +2 -0
  13. package/src/commands/message.ts +26 -2
  14. package/src/commands/reconcile-units.ts +104 -0
  15. package/src/commands/release-composition.ts +232 -0
  16. package/src/commands/resume.ts +2 -27
  17. package/src/commands/setup.ts +101 -16
  18. package/src/commands/stats.ts +11 -30
  19. package/src/commands/tail.ts +31 -1
  20. package/src/commands/upgrade.ts +20 -3
  21. package/src/commands/verb.ts +2 -1
  22. package/src/config-schema.ts +19 -0
  23. package/src/config.ts +80 -0
  24. package/src/credential-class.ts +366 -0
  25. package/src/daemon.ts +1218 -288
  26. package/src/dashboard/app.js +504 -2
  27. package/src/dashboard/controls.ts +336 -0
  28. package/src/dashboard/index.html +30 -0
  29. package/src/dashboard/server.ts +271 -30
  30. package/src/dashboard/style.css +116 -0
  31. package/src/dashboard/transcript.ts +173 -0
  32. package/src/doctor.ts +377 -20
  33. package/src/failure-class.ts +59 -0
  34. package/src/fleet.ts +497 -15
  35. package/src/host.ts +6 -130
  36. package/src/omp.ts +29 -0
  37. package/src/orchestrator-tick.ts +343 -88
  38. package/src/pause.ts +233 -0
  39. package/src/settlement.ts +159 -2
  40. package/src/setup-answers.ts +97 -0
  41. package/src/setup-host.ts +321 -1155
  42. package/src/setup-install.ts +204 -27
  43. package/src/setup-wizard.ts +111 -50
  44. package/src/setup.ts +33 -0
  45. package/src/spend-telemetry.ts +117 -0
  46. package/src/stats.ts +35 -0
  47. package/src/status-render.ts +348 -19
  48. package/src/store.ts +1229 -55
  49. package/src/telegram-freshness.ts +269 -0
  50. package/src/to-spec.ts +27 -0
  51. package/src/types.ts +697 -4
  52. package/src/unblock.ts +22 -0
  53. package/src/unit-reconcile.ts +303 -0
  54. package/src/upgrade-verify.ts +8 -1
  55. package/src/upgrade.ts +299 -12
  56. package/src/verbs/actions.ts +124 -10
  57. package/src/verbs/protocol.ts +70 -2
  58. package/src/verbs/server.ts +447 -8
  59. package/src/wake.ts +48 -0
  60. package/src/worker.ts +403 -3
package/src/host.ts CHANGED
@@ -187,21 +187,7 @@ export function rssBytesFromHealthz(body: string | undefined): number | undefine
187
187
  }
188
188
  }
189
189
 
190
- // ------------------------------------------------------- worker host state (#894) --
191
-
192
- /**
193
- * The dedicated worker account earlier boundary slices created on this host.
194
- *
195
- * Worker sessions no longer launch under it (#894 restored the fleet-account
196
- * runtime v0.18.0 used): the constants below survive only so `setup host` can
197
- * keep staging and recognising the units it once installed until #895 retires
198
- * that host lifecycle end to end. Nothing on the session-launch path reads
199
- * them.
200
- */
201
- export const WORKER_ACCOUNT = "omp-worker";
202
-
203
- /** The worker account's system home: its harness config, caches and state. */
204
- export const WORKER_HOME_DIR = "/var/lib/omp-worker";
190
+ // ------------------------------------------------------- harness resolution --
205
191
 
206
192
  /**
207
193
  * The harness package omp-conductor loads a session from. Held here, beside
@@ -214,36 +200,14 @@ export const OMP_HARNESS_PACKAGE = "@oh-my-pi/pi-coding-agent";
214
200
  export const OMP_NATIVES_PACKAGE = "@oh-my-pi/pi-natives";
215
201
 
216
202
  /**
217
- * Where `setup host` binds the operator's install so a worker session can
218
- * resolve it (#828).
219
- *
220
- * Bun's node_modules resolution needs **read** permission not merely search
221
- * on the directory that holds a `node_modules`: it enumerates the directory
222
- * to decide whether the child is there. The fleet account's home is granted to
223
- * the worker search-only by design (#798), so a bare `@oh-my-pi/pi-coding-agent`
224
- * import from `<fleet home>/node_modules/omp-conductor` did not find the
225
- * operator's install at all and fell through to Bun's auto-install, which
226
- * downloaded a *different* harness version into the worker's own cache whose
227
- * native addon then failed to load — every dispatch on the host stopped before
228
- * session start.
229
- *
230
- * The fix is a read-only bind of the operator's `node_modules` at a path whose
231
- * every ancestor is world-readable. Worker children are launched from it, so
232
- * the entry module, the peer import and every transitive import resolve inside
233
- * one tree the worker can enumerate — and, because a bind shares inodes with
234
- * its source, it is the operator's exact build rather than a copy that can
235
- * drift.
236
- *
237
- * Deliberately **not** under {@link WORKER_HOME_DIR}: the worker owns its home
238
- * between setups, and a symlink planted where a root-run mount point goes would
239
- * redirect that mount to an arbitrary target (#816).
203
+ * The mount point releases 0.18.1-era bound the operator's install at, kept
204
+ * only as the retirement target `setup host` and `reconcile-units` remove
205
+ * (#895). Nothing mounts it any more: worker sessions launch under the fleet
206
+ * account again (#894), so the harness resolves through ordinary Node/Bun
207
+ * resolution from this package's own install root.
240
208
  */
241
209
  export const WORKER_HARNESS_DIR = "/var/lib/omp-worker-harness";
242
210
 
243
- /** The bound `node_modules` itself. The leaf name is load-bearing: it is what
244
- * Node/Bun resolution looks for walking up from the entry module. */
245
- export const WORKER_HARNESS_NODE_MODULES = join(WORKER_HARNESS_DIR, "node_modules");
246
-
247
211
  /**
248
212
  * The install root a module path sits in — its nearest ancestor named
249
213
  * `node_modules` — or `undefined` when it has none, which is this package
@@ -256,91 +220,3 @@ export function packageNodeModulesRoot(modulePath: string): string | undefined {
256
220
  const idx = parts.lastIndexOf("node_modules");
257
221
  return idx < 0 ? undefined : `/${parts.slice(0, idx + 1).join("/")}`;
258
222
  }
259
-
260
- /**
261
- * `path` as a worker session sees it through the harness binding, or
262
- * `undefined` when it is not inside `packageRoot` — a test seam pointing at a
263
- * file elsewhere, or a source checkout with no install root at all.
264
- *
265
- * Production uses {@link WORKER_HARNESS_NODE_MODULES}; an explicit binding
266
- * root lets the Linux regression build the same inode-sharing tree under its
267
- * private temporary directory instead of touching the live host mount.
268
- */
269
- export function workerHarnessPath(
270
- path: string,
271
- packageRoot: string | undefined,
272
- bindingRoot: string = WORKER_HARNESS_NODE_MODULES,
273
- ): string | undefined {
274
- if (packageRoot === undefined) return undefined;
275
- const absolute = resolve(path);
276
- const prefix = `${packageRoot}/`;
277
- if (!absolute.startsWith(prefix)) return undefined;
278
- return join(bindingRoot, absolute.slice(prefix.length));
279
- }
280
-
281
- /** One path's filesystem identity. Identity rather than bytes, because that is
282
- * exactly what distinguishes a live bind of the operator's install (same
283
- * device and inode) from an empty mount point or a copy that has drifted. */
284
- function pathIdentity(path: string): string | undefined {
285
- try {
286
- const st = statSync(path);
287
- return `${st.dev}:${st.ino}`;
288
- } catch {
289
- return undefined;
290
- }
291
- }
292
-
293
- /** Read-only facts {@link harnessBindingProblem} decides on; injected by tests. */
294
- export interface HarnessBindingDeps {
295
- /** This module's own directory — the install root is derived from it. */
296
- moduleDir?: string;
297
- /** `dev:ino` of one path, or `undefined` when it cannot be stat'ed. */
298
- identity?: (path: string) => string | undefined;
299
- /** Alternate binding root for the isolated Linux currentness regression. */
300
- bindingRoot?: string;
301
- }
302
-
303
- /**
304
- * Why a worker session could not load the operator's harness through the
305
- * binding, or `undefined` when it can.
306
- *
307
- * Both halves of the launch are checked, by filesystem identity: this package's
308
- * own directory (the child's entry module comes from it) and the harness
309
- * package directory (its peer import resolves to it). A mount point that is
310
- * empty, stale, or bound to some other tree fails on the identity comparison
311
- * rather than being taken on faith — which is the whole point, since the
312
- * symptom this replaces was a *successful* import of the wrong build.
313
- */
314
- export function harnessBindingProblem(deps: HarnessBindingDeps = {}): string | undefined {
315
- const moduleDir = deps.moduleDir ?? import.meta.dir;
316
- const identity = deps.identity ?? pathIdentity;
317
- const bindingRoot = deps.bindingRoot ?? WORKER_HARNESS_NODE_MODULES;
318
- const packageRoot = packageNodeModulesRoot(moduleDir);
319
- if (packageRoot === undefined) {
320
- return (
321
- `omp-conductor is running from ${moduleDir}, which is not inside a node_modules install root — ` +
322
- "a worker session resolves its harness through a read-only bind of that root, so worker dispatch " +
323
- "needs the installed package (install omp-conductor with its harness peer, then re-run `omp-conductor setup host`)"
324
- );
325
- }
326
- for (const dir of [moduleDir, join(packageRoot, OMP_HARNESS_PACKAGE)]) {
327
- const source = identity(dir);
328
- if (source === undefined) {
329
- return (
330
- `${dir} is missing or unreadable — ${OMP_HARNESS_PACKAGE} must be installed alongside omp-conductor ` +
331
- "for a worker session to load the same harness build the operator runs"
332
- );
333
- }
334
- // Non-null by construction: `dir` is inside `packageRoot`.
335
- const bound = workerHarnessPath(dir, packageRoot, bindingRoot) ?? dir;
336
- if (identity(bound) !== source) {
337
- return (
338
- `${bound} does not resolve to ${dir} — the worker harness binding of ${packageRoot} at ` +
339
- `${bindingRoot} is missing or stale, so a worker session would resolve a different ` +
340
- "harness build (or none). Run `omp-conductor setup host` to establish it"
341
- );
342
- }
343
- }
344
- return undefined;
345
- }
346
-
package/src/omp.ts CHANGED
@@ -641,6 +641,24 @@ export interface CreateSessionOptions {
641
641
  * its verb channel to this pid, and a channel accepts nothing until it is bound.
642
642
  */
643
643
  onSpawn?: (pid: number) => void;
644
+ /**
645
+ * The session's Herdr representation (#840), opened from the exact pid the
646
+ * spawn below reports and released when the child is gone.
647
+ *
648
+ * Injected rather than imported so this module keeps knowing nothing about
649
+ * Herdr: `createSession` owns the process, and whoever launches a *worker*
650
+ * owns whether that process should be visible in an operator's workspace. A
651
+ * caller that passes nothing gets today's behaviour exactly.
652
+ *
653
+ * `open` is called with the same pid, at the same instant, as
654
+ * {@link CreateSessionOptions.onSpawn} — the representation is of the
655
+ * authoritative child or of nothing. It must never throw and never block the
656
+ * launch: visibility is not a precondition for work.
657
+ */
658
+ pane?: {
659
+ open(pid: number): void;
660
+ release(): void;
661
+ };
644
662
  /**
645
663
  * Pre-spawn admission gate (#374). Consulted once, immediately after the
646
664
  * socket bind await and immediately before `Bun.spawn` — the last window a
@@ -805,6 +823,11 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
805
823
  // Synchronously, before a single await: the child cannot have connected yet, so
806
824
  // the caller's channel is bound before it can be reached.
807
825
  if (child.pid !== undefined) opts.onSpawn?.(child.pid);
826
+ // The workspace representation is of THIS pid or of nothing (#840). After the
827
+ // verb-channel bind above, because a pane is a convenience and the bind is a
828
+ // correctness boundary — and never before the child exists, which is what
829
+ // makes "bound to the exact session-host pid" true rather than nominal.
830
+ if (child.pid !== undefined) opts.pane?.open(child.pid);
808
831
 
809
832
  let stderrTail = "";
810
833
  const drain = async (stream: ReadableStream<Uint8Array> | undefined, prefix: string): Promise<void> => {
@@ -965,6 +988,12 @@ export async function createSession(opts: CreateSessionOptions): Promise<AgentSe
965
988
  onExit();
966
989
  const exitCode = typeof code === "number" ? code : null;
967
990
  emitSessionExit(exitCode);
991
+ // The pid this pane spoke for is gone, so conductor stops speaking for it
992
+ // (#840) — on every exit path, crash and clean dispose alike, because a
993
+ // representation that outlives its process is a worker the workspace still
994
+ // shows as live. What happens to the pane itself is #841's policy; this only
995
+ // gives back the authority.
996
+ opts.pane?.release();
968
997
  // A child that cannot start writes `start-error` over the socket, not the
969
998
  // pipes — and its exit can be dispatched before the accept of a connection
970
999
  // that already completed in the kernel. Let the socket settle before the