@cabane/companion 0.6.212 → 0.6.213

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/dist/cli.js CHANGED
@@ -9292,6 +9292,7 @@ var init_turn_execution = __esm({
9292
9292
  this.supervisor = supervisor;
9293
9293
  this.payload = payload;
9294
9294
  this.resumed = handleOpts.resumed === true;
9295
+ this.dispatchTiming = handleOpts.dispatchTiming ?? (() => void 0);
9295
9296
  this.dispatchId = payload.messageId;
9296
9297
  this.workspaceId = opts.workspaceId;
9297
9298
  this.turnLog = opts.log.child({
@@ -9323,6 +9324,8 @@ var init_turn_execution = __esm({
9323
9324
  // is set on every dispatch. `resumedFromSeq` is what it seeded from.
9324
9325
  resumed;
9325
9326
  resumedFromSeq;
9327
+ // The dispatch's time on the device before this process started, as the launcher said it.
9328
+ dispatchTiming;
9326
9329
  // Whether the span this resumed run continues already has first words —
9327
9330
  // from the server (`turnSpoke`) or from speech still queued in the outbox.
9328
9331
  resumedSpoke = false;
@@ -9774,6 +9777,16 @@ var init_turn_execution = __esm({
9774
9777
  this.hookEnv = hookEnv;
9775
9778
  this.turnEnv = turnEnv;
9776
9779
  }
9780
+ leaseTiming() {
9781
+ const timing = this.dispatchTiming();
9782
+ if (this.turnContext.dispatchTimingVersion !== 1 || !timing) return void 0;
9783
+ return {
9784
+ dispatchTiming: {
9785
+ queueMs: Math.round(timing.queueMs),
9786
+ setupMs: Math.round(timing.beforeRunnerMs + performance.now())
9787
+ }
9788
+ };
9789
+ }
9777
9790
  async acquireLease() {
9778
9791
  const { payload, workspaceId, turnId, turnLog } = this;
9779
9792
  const abortController = this.abortController;
@@ -9785,6 +9798,11 @@ var init_turn_execution = __esm({
9785
9798
  responsivenessVersion: 1,
9786
9799
  responsivenessSessionMode: this.request.session ? "resumed" : "fresh"
9787
9800
  } : {},
9801
+ // The dispatch's time on this device before this request: its queue, and its setup,
9802
+ // which is what passed before this process started plus this process's uptime
9803
+ // (`performance.now()` counts from the process's start). Only to an API that takes the
9804
+ // field, and only once the launcher has said; never a guess in its place.
9805
+ ...this.leaseTiming() ?? {},
9788
9806
  // A runner leases under its launcher's boot id, the one the device
9789
9807
  // heartbeats, or the lost-run sweep reads it as a restart.
9790
9808
  bootId: this.opts.inRunner?.bootId ?? bootId,
@@ -10631,6 +10649,7 @@ function openLauncherChannel(stdin, emit, onLauncherGone) {
10631
10649
  let gotFirst = false;
10632
10650
  let stopRequested = false;
10633
10651
  let gzipBodies;
10652
+ let dispatchTiming;
10634
10653
  let gone = false;
10635
10654
  const causeWaiters = /* @__PURE__ */ new Set();
10636
10655
  const first = new Promise((resolve2) => resolveFirst = resolve2);
@@ -10645,6 +10664,9 @@ function openLauncherChannel(stdin, emit, onLauncherGone) {
10645
10664
  if (message2?.cabaneLauncher === "granted") waiting.get(message2.id)?.();
10646
10665
  if (message2?.cabaneLauncher === "stop") stopRequested = true;
10647
10666
  if (message2?.cabaneLauncher === "gzip_bodies") gzipBodies = message2.on;
10667
+ if (message2?.cabaneLauncher === "dispatch_timing") {
10668
+ dispatchTiming = { queueMs: message2.queueMs, beforeRunnerMs: message2.beforeRunnerMs };
10669
+ }
10648
10670
  if (message2?.cabaneLauncher === "cause") {
10649
10671
  for (const resolve2 of causeWaiters) resolve2(message2.cause);
10650
10672
  }
@@ -10663,6 +10685,9 @@ function openLauncherChannel(stdin, emit, onLauncherGone) {
10663
10685
  get gzipBodies() {
10664
10686
  return gzipBodies;
10665
10687
  },
10688
+ get dispatchTiming() {
10689
+ return dispatchTiming;
10690
+ },
10666
10691
  terminationCause(timeoutMs = CAUSE_TIMEOUT_MS) {
10667
10692
  if (gone) return Promise.resolve(null);
10668
10693
  return new Promise((resolve2) => {
@@ -10874,7 +10899,7 @@ async function runTurn(env, deps) {
10874
10899
  for (const controller of controllers) controller.abort(RUNNER_SIGTERM);
10875
10900
  };
10876
10901
  deps.signals.on("SIGTERM", onTerm);
10877
- emit({ cabaneRunner: "ready" });
10902
+ emit({ cabaneRunner: "ready", uptimeMs: performance.now() });
10878
10903
  try {
10879
10904
  const input = parseRunnerInput(env, await channel.first);
10880
10905
  for (const name of Object.values(RUNNER_ENV)) delete env[name];
@@ -10913,7 +10938,8 @@ async function runTurn(env, deps) {
10913
10938
  });
10914
10939
  const result = await new TurnExecution(opts, supervisor, input.dispatch, {
10915
10940
  turnId: input.turnId,
10916
- resumed: input.resumed
10941
+ resumed: input.resumed,
10942
+ dispatchTiming: () => channel.dispatchTiming
10917
10943
  }).run().finally(() => shims.dispose());
10918
10944
  if (outbox) await drainBeforeExit(outbox.outbox, outbox.api, deps.outboxGraceMs, log2);
10919
10945
  emit({ cabaneRunner: "result", result });
@@ -13244,6 +13270,16 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
13244
13270
  case "ready":
13245
13271
  ready = true;
13246
13272
  if (controller.signal.aborted) stopRunner.abort();
13273
+ if (handleOpts.clock && event.uptimeMs !== void 0 && running.stdin.writable) {
13274
+ const elapsed = performance.now() - handleOpts.clock.begunAt;
13275
+ running.stdin.write(
13276
+ encodeLauncherMessage({
13277
+ cabaneLauncher: "dispatch_timing",
13278
+ queueMs: handleOpts.clock.queueMs,
13279
+ beforeRunnerMs: Math.max(0, elapsed - event.uptimeMs)
13280
+ })
13281
+ );
13282
+ }
13247
13283
  return;
13248
13284
  case "start":
13249
13285
  duties.notifyStart({
@@ -13424,7 +13460,11 @@ var Dispatcher = class {
13424
13460
  notifyEnd: (info) => this.notifyEnd(info)
13425
13461
  },
13426
13462
  payload,
13427
- { turnId: opts.turnId ?? randomUUID5(), resumed: opts.resumed === true }
13463
+ {
13464
+ turnId: opts.turnId ?? randomUUID5(),
13465
+ resumed: opts.resumed === true,
13466
+ ...opts.clock ? { clock: opts.clock } : {}
13467
+ }
13428
13468
  );
13429
13469
  }
13430
13470
  // Cancel the named turn. An older server omits turnId and cancels the pair.
@@ -14642,7 +14682,9 @@ var CompanionSupervisor = class {
14642
14682
  this.detectTimer = setInterval(() => void this.detect(), DETECT_INTERVAL_MS);
14643
14683
  this.detectTimer.unref?.();
14644
14684
  if (!this.deviceApi || !this.config.deviceToken) {
14645
- this.log.warn("This device isn't paired. Get a setup line from a workspace's Settings \u2192 Devices.");
14685
+ this.log.warn(
14686
+ "This device isn't paired. Get a setup line from a workspace's Settings \u2192 Devices."
14687
+ );
14646
14688
  return;
14647
14689
  }
14648
14690
  this.deviceSub = new SseSubscriber({
@@ -15236,6 +15278,7 @@ var CompanionSupervisor = class {
15236
15278
  // it replays what is still offered on every connect instead of resuming from
15237
15279
  // a `Last-Event-ID`.
15238
15280
  async handleDeviceMessage(ev) {
15281
+ const receivedAt = performance.now();
15239
15282
  const wire = this.parseFrame(ev);
15240
15283
  if (!wire) return;
15241
15284
  let wr = this.workspaces.get(wire.workspaceId);
@@ -15257,7 +15300,7 @@ var CompanionSupervisor = class {
15257
15300
  }
15258
15301
  }
15259
15302
  this.hub.recordEvent(wr.workspaceId);
15260
- await this.deliverFrame(wr, wire);
15303
+ await this.deliverFrame(wr, wire, receivedAt);
15261
15304
  }
15262
15305
  parseFrame(ev) {
15263
15306
  try {
@@ -15276,19 +15319,19 @@ var CompanionSupervisor = class {
15276
15319
  //
15277
15320
  // A cancel needs no gate: `dispatcher.cancel` on an already-aborted run is a
15278
15321
  // no-op that reports `false`, so a second copy is inert.
15279
- async deliverFrame(wr, wire) {
15322
+ async deliverFrame(wr, wire, receivedAt) {
15280
15323
  const claim = wire.type === "device:dispatch_requested" && wire.id ? this.claims.claim(wire.id) : null;
15281
15324
  if (claim && !claim.first) {
15282
15325
  await claim.done;
15283
15326
  return;
15284
15327
  }
15285
15328
  try {
15286
- await this.routeFrame(wr, wire);
15329
+ await this.routeFrame(wr, wire, receivedAt);
15287
15330
  } finally {
15288
15331
  if (claim) this.claims.release(wire.id);
15289
15332
  }
15290
15333
  }
15291
- async routeFrame(wr, wire) {
15334
+ async routeFrame(wr, wire, receivedAt) {
15292
15335
  if (wire.type === "device:cancel_requested") {
15293
15336
  const payload2 = {
15294
15337
  type: "device:cancel_requested",
@@ -15342,7 +15385,7 @@ var CompanionSupervisor = class {
15342
15385
  }
15343
15386
  const chainKey = `${payload.conversationId}|${payload.agentId}`;
15344
15387
  const prev = wr.chains.get(chainKey) ?? Promise.resolve();
15345
- const tail = prev.then(() => this.runDispatch(wr, wire.id, payload, agent)).catch((err) => {
15388
+ const tail = prev.then(() => this.runDispatch(wr, wire.id, payload, agent, void 0, receivedAt)).catch((err) => {
15346
15389
  this.log.warn(
15347
15390
  {
15348
15391
  workspaceId: wr.workspaceId,
@@ -15539,7 +15582,7 @@ var CompanionSupervisor = class {
15539
15582
  }
15540
15583
  }
15541
15584
  // One turn: the skip-vs-resume dedupe, the dispatch, then the console summary.
15542
- async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId) {
15585
+ async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId, receivedAt) {
15543
15586
  await this.initialHarnessReady;
15544
15587
  if (this.stopped) return;
15545
15588
  const workspaceId = wr.workspaceId;
@@ -15602,9 +15645,11 @@ var CompanionSupervisor = class {
15602
15645
  workspaceId
15603
15646
  };
15604
15647
  this.log.info(turnBindings, "Turn started");
15648
+ const begunAt = performance.now();
15605
15649
  const result = await agent.dispatcher.handle(payload, {
15606
15650
  turnId,
15607
- resumed: resumedTurnId !== null
15651
+ resumed: resumedTurnId !== null,
15652
+ ...receivedAt !== void 0 ? { clock: { queueMs: begunAt - receivedAt, begunAt } } : {}
15608
15653
  });
15609
15654
  if (dispatchId) markCompleted(workspaceId, dispatchId);
15610
15655
  const durationS = (result.durationMs / 1e3).toFixed(1);
package/dist/runtime.js CHANGED
@@ -8101,6 +8101,16 @@ async function runTurnInRunner(opts, launch, duties, payload, handleOpts) {
8101
8101
  case "ready":
8102
8102
  ready = true;
8103
8103
  if (controller.signal.aborted) stopRunner.abort();
8104
+ if (handleOpts.clock && event.uptimeMs !== void 0 && running.stdin.writable) {
8105
+ const elapsed = performance.now() - handleOpts.clock.begunAt;
8106
+ running.stdin.write(
8107
+ encodeLauncherMessage({
8108
+ cabaneLauncher: "dispatch_timing",
8109
+ queueMs: handleOpts.clock.queueMs,
8110
+ beforeRunnerMs: Math.max(0, elapsed - event.uptimeMs)
8111
+ })
8112
+ );
8113
+ }
8104
8114
  return;
8105
8115
  case "start":
8106
8116
  duties.notifyStart({
@@ -8591,7 +8601,11 @@ var Dispatcher = class {
8591
8601
  notifyEnd: (info) => this.notifyEnd(info)
8592
8602
  },
8593
8603
  payload,
8594
- { turnId: opts.turnId ?? randomUUID5(), resumed: opts.resumed === true }
8604
+ {
8605
+ turnId: opts.turnId ?? randomUUID5(),
8606
+ resumed: opts.resumed === true,
8607
+ ...opts.clock ? { clock: opts.clock } : {}
8608
+ }
8595
8609
  );
8596
8610
  }
8597
8611
  // Cancel the named turn. An older server omits turnId and cancels the pair.
@@ -10187,7 +10201,9 @@ var CompanionSupervisor = class {
10187
10201
  this.detectTimer = setInterval(() => void this.detect(), DETECT_INTERVAL_MS);
10188
10202
  this.detectTimer.unref?.();
10189
10203
  if (!this.deviceApi || !this.config.deviceToken) {
10190
- this.log.warn("This device isn't paired. Get a setup line from a workspace's Settings \u2192 Devices.");
10204
+ this.log.warn(
10205
+ "This device isn't paired. Get a setup line from a workspace's Settings \u2192 Devices."
10206
+ );
10191
10207
  return;
10192
10208
  }
10193
10209
  this.deviceSub = new SseSubscriber({
@@ -10781,6 +10797,7 @@ var CompanionSupervisor = class {
10781
10797
  // it replays what is still offered on every connect instead of resuming from
10782
10798
  // a `Last-Event-ID`.
10783
10799
  async handleDeviceMessage(ev) {
10800
+ const receivedAt = performance.now();
10784
10801
  const wire = this.parseFrame(ev);
10785
10802
  if (!wire) return;
10786
10803
  let wr = this.workspaces.get(wire.workspaceId);
@@ -10802,7 +10819,7 @@ var CompanionSupervisor = class {
10802
10819
  }
10803
10820
  }
10804
10821
  this.hub.recordEvent(wr.workspaceId);
10805
- await this.deliverFrame(wr, wire);
10822
+ await this.deliverFrame(wr, wire, receivedAt);
10806
10823
  }
10807
10824
  parseFrame(ev) {
10808
10825
  try {
@@ -10821,19 +10838,19 @@ var CompanionSupervisor = class {
10821
10838
  //
10822
10839
  // A cancel needs no gate: `dispatcher.cancel` on an already-aborted run is a
10823
10840
  // no-op that reports `false`, so a second copy is inert.
10824
- async deliverFrame(wr, wire) {
10841
+ async deliverFrame(wr, wire, receivedAt) {
10825
10842
  const claim = wire.type === "device:dispatch_requested" && wire.id ? this.claims.claim(wire.id) : null;
10826
10843
  if (claim && !claim.first) {
10827
10844
  await claim.done;
10828
10845
  return;
10829
10846
  }
10830
10847
  try {
10831
- await this.routeFrame(wr, wire);
10848
+ await this.routeFrame(wr, wire, receivedAt);
10832
10849
  } finally {
10833
10850
  if (claim) this.claims.release(wire.id);
10834
10851
  }
10835
10852
  }
10836
- async routeFrame(wr, wire) {
10853
+ async routeFrame(wr, wire, receivedAt) {
10837
10854
  if (wire.type === "device:cancel_requested") {
10838
10855
  const payload2 = {
10839
10856
  type: "device:cancel_requested",
@@ -10887,7 +10904,7 @@ var CompanionSupervisor = class {
10887
10904
  }
10888
10905
  const chainKey = `${payload.conversationId}|${payload.agentId}`;
10889
10906
  const prev = wr.chains.get(chainKey) ?? Promise.resolve();
10890
- const tail = prev.then(() => this.runDispatch(wr, wire.id, payload, agent)).catch((err) => {
10907
+ const tail = prev.then(() => this.runDispatch(wr, wire.id, payload, agent, void 0, receivedAt)).catch((err) => {
10891
10908
  this.log.warn(
10892
10909
  {
10893
10910
  workspaceId: wr.workspaceId,
@@ -11084,7 +11101,7 @@ var CompanionSupervisor = class {
11084
11101
  }
11085
11102
  }
11086
11103
  // One turn: the skip-vs-resume dedupe, the dispatch, then the console summary.
11087
- async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId) {
11104
+ async runDispatch(wr, dispatchId, payload, agent, recoveredTurnId, receivedAt) {
11088
11105
  await this.initialHarnessReady;
11089
11106
  if (this.stopped) return;
11090
11107
  const workspaceId = wr.workspaceId;
@@ -11147,9 +11164,11 @@ var CompanionSupervisor = class {
11147
11164
  workspaceId
11148
11165
  };
11149
11166
  this.log.info(turnBindings, "Turn started");
11167
+ const begunAt = performance.now();
11150
11168
  const result = await agent.dispatcher.handle(payload, {
11151
11169
  turnId,
11152
- resumed: resumedTurnId !== null
11170
+ resumed: resumedTurnId !== null,
11171
+ ...receivedAt !== void 0 ? { clock: { queueMs: begunAt - receivedAt, begunAt } } : {}
11153
11172
  });
11154
11173
  if (dispatchId) markCompleted(workspaceId, dispatchId);
11155
11174
  const durationS = (result.durationMs / 1e3).toFixed(1);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.212",
3
+ "version": "0.6.213",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect an agent on your machine to your Cabane workspace as a responder — drive work against your own codebase, files, and MCP servers without putting any of it in Cabane.",
6
6
  "license": "UNLICENSED",