@cabane/companion 0.6.106 → 0.6.107

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
@@ -93,7 +93,15 @@ var prepareHookSchema = z.object({
93
93
  //
94
94
  // Default OFF, and deliberately: an operator hook that provisions
95
95
  // unconditionally must not start running on every turn because it upgraded.
96
- validateCached: z.boolean().optional()
96
+ validateCached: z.boolean().optional(),
97
+ // Independent operator-owned reporter. Receives sanitized failure metadata
98
+ // on stdin; never re-enters this hook and cannot change the turn outcome.
99
+ failureReporter: z.object({
100
+ command: z.string().min(1),
101
+ args: z.array(z.string()).optional(),
102
+ env: z.record(z.string(), z.string()).optional(),
103
+ timeoutMs: z.number().int().positive().max(3e4).optional()
104
+ }).strict().optional()
97
105
  }).strict();
98
106
  var DEFAULT_TIMEOUT_MS = 10 * 6e4;
99
107
  var prepareResultSchema = z.object({
@@ -1769,8 +1777,8 @@ function requestEnrollmentCode(baseUrl, opts = {}) {
1769
1777
  ...opts.label ? { label: opts.label } : {}
1770
1778
  });
1771
1779
  }
1772
- function deviceLabelFromHostname(hostname4) {
1773
- const trimmed = hostname4.trim().replace(/\.local$/i, "");
1780
+ function deviceLabelFromHostname(hostname5) {
1781
+ const trimmed = hostname5.trim().replace(/\.local$/i, "");
1774
1782
  if (trimmed.length === 0) return void 0;
1775
1783
  return trimmed.slice(0, 120);
1776
1784
  }
@@ -2097,7 +2105,7 @@ async function logs(opts = {}) {
2097
2105
  }
2098
2106
 
2099
2107
  // src/commands/start.ts
2100
- import { hostname as hostname3 } from "os";
2108
+ import { hostname as hostname4 } from "os";
2101
2109
 
2102
2110
  // src/runtime.ts
2103
2111
  import { randomUUID as randomUUID5 } from "crypto";
@@ -2662,7 +2670,7 @@ async function warnAboutHarnessReadiness(cfg, deps = {}) {
2662
2670
  }
2663
2671
 
2664
2672
  // src/supervisor.ts
2665
- import { hostname as hostname2 } from "os";
2673
+ import { hostname as hostname3 } from "os";
2666
2674
 
2667
2675
  // packages/agent-runtime/src/failure.ts
2668
2676
  import { z as z4 } from "zod";
@@ -8604,6 +8612,68 @@ function readOutboxFloor(read, turnId, log) {
8604
8612
  }
8605
8613
  }
8606
8614
 
8615
+ // src/prepare-failure.ts
8616
+ import { spawn as spawn4 } from "child_process";
8617
+ import { hostname as hostname2 } from "os";
8618
+ function prepareFailureCause(error) {
8619
+ const message = error instanceof Error ? error.message : String(error);
8620
+ if (/timed out/i.test(message)) return "prepare hook timed out";
8621
+ if (/MODULE_NOT_FOUND|Cannot find (?:module|package)/.test(message))
8622
+ return "prepare hook module missing; check the configured command and its installation for this device";
8623
+ if (/not valid JSON|JSON must carry|produced no output/.test(message))
8624
+ return "prepare hook returned malformed output; expected a cwd path or JSON with a non-empty cwd";
8625
+ if (/ENOENT/.test(message)) return "prepare hook executable missing (ENOENT)";
8626
+ if (/EACCES/.test(message)) return "prepare hook executable is not accessible (EACCES)";
8627
+ const exit = message.match(/exited with code (\d+)/)?.[1];
8628
+ return exit ? `prepare hook exited with code ${exit}` : "prepare hook failed; inspect the device-local hook log";
8629
+ }
8630
+ async function runPrepareWithReporting(runner, hook, input, context, reportError) {
8631
+ try {
8632
+ return await runner(hook, input);
8633
+ } catch (error) {
8634
+ if (hook.failureReporter) {
8635
+ try {
8636
+ await reportFailure(hook.failureReporter, {
8637
+ workspaceId: input.workspaceId,
8638
+ conversationId: input.conversationId,
8639
+ agentId: input.agentId,
8640
+ agentUsername: input.agentUsername,
8641
+ turnId: context.turnId,
8642
+ deviceId: context.deviceId ?? hostname2(),
8643
+ cause: prepareFailureCause(error)
8644
+ });
8645
+ } catch {
8646
+ reportError();
8647
+ }
8648
+ }
8649
+ throw error;
8650
+ }
8651
+ }
8652
+ function reportFailure(config, body) {
8653
+ return new Promise((resolve2, reject) => {
8654
+ const child = spawn4(config.command, config.args ?? [], {
8655
+ stdio: ["pipe", "ignore", "ignore"],
8656
+ env: { ...process.env, ...config.env }
8657
+ });
8658
+ const timer = setTimeout(() => {
8659
+ child.kill("SIGKILL");
8660
+ reject(new Error("reporter timeout"));
8661
+ }, config.timeoutMs ?? 15e3);
8662
+ child.stdin.on("error", () => {
8663
+ });
8664
+ child.once("error", () => {
8665
+ clearTimeout(timer);
8666
+ reject(new Error("reporter failed to start"));
8667
+ });
8668
+ child.once("close", (code) => {
8669
+ clearTimeout(timer);
8670
+ if (code === 0) resolve2();
8671
+ else reject(new Error("reporter failed"));
8672
+ });
8673
+ child.stdin.end(JSON.stringify(body));
8674
+ });
8675
+ }
8676
+
8607
8677
  // src/prepared.ts
8608
8678
  import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
8609
8679
  import { join as join12 } from "path";
@@ -8834,7 +8904,7 @@ function pruneOld(dir2, retain) {
8834
8904
  }
8835
8905
 
8836
8906
  // src/turn-containment.ts
8837
- import { execFile, execFileSync, spawn as spawn4 } from "child_process";
8907
+ import { execFile, execFileSync, spawn as spawn5 } from "child_process";
8838
8908
  import { readFileSync as readFileSync8 } from "fs";
8839
8909
  import { promisify } from "util";
8840
8910
  var execFileAsync = promisify(execFile);
@@ -8916,7 +8986,7 @@ function createTurnContainment(turnId, deps = {}) {
8916
8986
  command: options.command
8917
8987
  });
8918
8988
  return asContained(
8919
- spawn4(
8989
+ spawn5(
8920
8990
  "systemd-run",
8921
8991
  [
8922
8992
  "--user",
@@ -8943,7 +9013,7 @@ function createTurnContainment(turnId, deps = {}) {
8943
9013
  );
8944
9014
  }
8945
9015
  function spawnInGroup(options) {
8946
- const child = spawn4(options.command, options.args, {
9016
+ const child = spawn5(options.command, options.args, {
8947
9017
  ...options.cwd ? { cwd: options.cwd } : {},
8948
9018
  env: options.env,
8949
9019
  stdio: ["pipe", "pipe", "pipe"],
@@ -9645,18 +9715,28 @@ var TurnExecution = class {
9645
9715
  if (prepareHook.validateCached) {
9646
9716
  const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
9647
9717
  try {
9648
- await runHook(prepareHook, {
9649
- workspaceId,
9650
- conversationId: payload.conversationId,
9651
- agentId: payload.agentId,
9652
- agentUsername: this.opts.agentUsername,
9653
- runtime: this.turnContext.runtime,
9654
- hostAccess: this.turnContext.policy.hostFs,
9655
- triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
9656
- title: this.turnContext.conversation.title,
9657
- messageBody: this.turnContext.message.body,
9658
- prepared: cached2
9659
- });
9718
+ await runPrepareWithReporting(
9719
+ runHook,
9720
+ prepareHook,
9721
+ {
9722
+ workspaceId,
9723
+ conversationId: payload.conversationId,
9724
+ agentId: payload.agentId,
9725
+ agentUsername: this.opts.agentUsername,
9726
+ runtime: this.turnContext.runtime,
9727
+ hostAccess: this.turnContext.policy.hostFs,
9728
+ triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
9729
+ title: this.turnContext.conversation.title,
9730
+ messageBody: this.turnContext.message.body,
9731
+ prepared: cached2
9732
+ },
9733
+ { turnId, deviceId: this.opts.deviceId },
9734
+ () => {
9735
+ turnLog.error(
9736
+ "dispatcher: prepare failure reporter failed; original failure preserved"
9737
+ );
9738
+ }
9739
+ );
9660
9740
  } catch (err) {
9661
9741
  const reason = err instanceof Error ? err.message : String(err);
9662
9742
  turnLog.debug({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
@@ -9692,24 +9772,34 @@ var TurnExecution = class {
9692
9772
  preparingTimer.unref?.();
9693
9773
  const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
9694
9774
  try {
9695
- const result = await runHook(prepareHook, {
9696
- workspaceId,
9697
- conversationId: payload.conversationId,
9698
- agentId: payload.agentId,
9699
- agentUsername: this.opts.agentUsername,
9700
- runtime: this.turnContext.runtime,
9701
- hostAccess: this.turnContext.policy.hostFs,
9702
- // CT317/CT319: the trigger message's referenced-entry paths — what the
9703
- // tasker prepare hook keys its per-task env off. Defaults to `[]` for
9704
- // an older API. The conversation anchor is gone (CT319).
9705
- triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
9706
- title: this.turnContext.conversation.title,
9707
- // CT943: the dispatching message's text where an `env:` directive
9708
- // rides. The server has always sent the trigger body on the turn
9709
- // context (for the live feed); this is the first thing to read it as
9710
- // an INPUT, so a dispatch can ask for its environment in words.
9711
- messageBody: this.turnContext.message.body
9712
- });
9775
+ const result = await runPrepareWithReporting(
9776
+ runHook,
9777
+ prepareHook,
9778
+ {
9779
+ workspaceId,
9780
+ conversationId: payload.conversationId,
9781
+ agentId: payload.agentId,
9782
+ agentUsername: this.opts.agentUsername,
9783
+ runtime: this.turnContext.runtime,
9784
+ hostAccess: this.turnContext.policy.hostFs,
9785
+ // CT317/CT319: the trigger message's referenced-entry paths — what the
9786
+ // tasker prepare hook keys its per-task env off. Defaults to `[]` for
9787
+ // an older API. The conversation anchor is gone (CT319).
9788
+ triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
9789
+ title: this.turnContext.conversation.title,
9790
+ // CT943: the dispatching message's text where an `env:` directive
9791
+ // rides. The server has always sent the trigger body on the turn
9792
+ // context (for the live feed); this is the first thing to read it as
9793
+ // an INPUT, so a dispatch can ask for its environment in words.
9794
+ messageBody: this.turnContext.message.body
9795
+ },
9796
+ { turnId, deviceId: this.opts.deviceId },
9797
+ () => {
9798
+ turnLog.error(
9799
+ "dispatcher: prepare failure reporter failed; original failure preserved"
9800
+ );
9801
+ }
9802
+ );
9713
9803
  clearTimeout(preparingTimer);
9714
9804
  if (preparingStarted) reportPreparing("done");
9715
9805
  writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
@@ -10870,7 +10960,7 @@ var CompanionSupervisor = class {
10870
10960
  async start() {
10871
10961
  this.log.info(
10872
10962
  { protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
10873
- `Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname2()}`
10963
+ `Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname3()}`
10874
10964
  );
10875
10965
  const startupHarnessRefresh = this.refreshHarnessStatuses();
10876
10966
  this.trackHeartbeat(startupHarnessRefresh);
@@ -10889,7 +10979,7 @@ var CompanionSupervisor = class {
10889
10979
  token: this.config.deviceToken,
10890
10980
  log: this.log,
10891
10981
  lastEventId: null,
10892
- onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname2()}`),
10982
+ onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname3()}`),
10893
10983
  onMessage: async (ev) => {
10894
10984
  if (ev.event === "assignments_changed") {
10895
10985
  if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
@@ -11305,6 +11395,7 @@ var CompanionSupervisor = class {
11305
11395
  });
11306
11396
  if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx, local });
11307
11397
  return new Dispatcher({
11398
+ deviceId: this.deviceId ?? void 0,
11308
11399
  api: ctx.api,
11309
11400
  baseUrl: ctx.baseUrl,
11310
11401
  workspaceId: ctx.workspaceId,
@@ -11952,9 +12043,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
11952
12043
  }
11953
12044
  function defaultReexec() {
11954
12045
  clearRuntimeState();
11955
- void import("child_process").then(({ spawn: spawn6 }) => {
12046
+ void import("child_process").then(({ spawn: spawn7 }) => {
11956
12047
  try {
11957
- const child = spawn6(process.execPath, process.argv.slice(1), {
12048
+ const child = spawn7(process.execPath, process.argv.slice(1), {
11958
12049
  stdio: "inherit",
11959
12050
  detached: false
11960
12051
  });
@@ -12209,7 +12300,7 @@ async function closeSurfaces(control, dashboard) {
12209
12300
  }
12210
12301
 
12211
12302
  // src/commands/daemon.ts
12212
- import { spawn as spawn5 } from "child_process";
12303
+ import { spawn as spawn6 } from "child_process";
12213
12304
  import { closeSync as closeSync3, mkdirSync as mkdirSync12, openSync as openSync3 } from "fs";
12214
12305
 
12215
12306
  // src/cli-entry.ts
@@ -12321,7 +12412,7 @@ function defaultSpawnDetached(args) {
12321
12412
  mkdirSync12(cabaneDir(), { recursive: true });
12322
12413
  const logFd = openSync3(companionLogPath(), "a");
12323
12414
  try {
12324
- return spawn5(process.execPath, [cliPath, ...args], {
12415
+ return spawn6(process.execPath, [cliPath, ...args], {
12325
12416
  detached: true,
12326
12417
  stdio: ["ignore", logFd, logFd],
12327
12418
  env: { ...process.env, CABANE_COMPANION_DAEMON: "1" }
@@ -12489,7 +12580,7 @@ function prePairFoundPhrase(harness) {
12489
12580
  }
12490
12581
  async function pairHere(opts, interactive) {
12491
12582
  const baseUrl = resolvePairBaseUrl(opts.server);
12492
- const label = deviceLabelFromHostname(hostname3());
12583
+ const label = deviceLabelFromHostname(hostname4());
12493
12584
  const aborter = new AbortController();
12494
12585
  const onSigint = () => aborter.abort();
12495
12586
  process.on("SIGINT", onSigint);
@@ -76,7 +76,15 @@ var prepareHookSchema = z.object({
76
76
  //
77
77
  // Default OFF, and deliberately: an operator hook that provisions
78
78
  // unconditionally must not start running on every turn because it upgraded.
79
- validateCached: z.boolean().optional()
79
+ validateCached: z.boolean().optional(),
80
+ // Independent operator-owned reporter. Receives sanitized failure metadata
81
+ // on stdin; never re-enters this hook and cannot change the turn outcome.
82
+ failureReporter: z.object({
83
+ command: z.string().min(1),
84
+ args: z.array(z.string()).optional(),
85
+ env: z.record(z.string(), z.string()).optional(),
86
+ timeoutMs: z.number().int().positive().max(3e4).optional()
87
+ }).strict().optional()
80
88
  }).strict();
81
89
  var DEFAULT_TIMEOUT_MS = 10 * 6e4;
82
90
  var prepareResultSchema = z.object({
package/dist/runtime.js CHANGED
@@ -85,7 +85,15 @@ var prepareHookSchema = z.object({
85
85
  //
86
86
  // Default OFF, and deliberately: an operator hook that provisions
87
87
  // unconditionally must not start running on every turn because it upgraded.
88
- validateCached: z.boolean().optional()
88
+ validateCached: z.boolean().optional(),
89
+ // Independent operator-owned reporter. Receives sanitized failure metadata
90
+ // on stdin; never re-enters this hook and cannot change the turn outcome.
91
+ failureReporter: z.object({
92
+ command: z.string().min(1),
93
+ args: z.array(z.string()).optional(),
94
+ env: z.record(z.string(), z.string()).optional(),
95
+ timeoutMs: z.number().int().positive().max(3e4).optional()
96
+ }).strict().optional()
89
97
  }).strict();
90
98
  var DEFAULT_TIMEOUT_MS = 10 * 6e4;
91
99
  var prepareResultSchema = z.object({
@@ -1996,7 +2004,7 @@ async function verifyRuntime(state, requestImpl = controlRequest) {
1996
2004
  }
1997
2005
 
1998
2006
  // src/supervisor.ts
1999
- import { hostname } from "os";
2007
+ import { hostname as hostname2 } from "os";
2000
2008
 
2001
2009
  // packages/agent-runtime/src/failure.ts
2002
2010
  import { z as z3 } from "zod";
@@ -8027,6 +8035,68 @@ function readOutboxFloor(read, turnId, log) {
8027
8035
  }
8028
8036
  }
8029
8037
 
8038
+ // src/prepare-failure.ts
8039
+ import { spawn as spawn4 } from "child_process";
8040
+ import { hostname } from "os";
8041
+ function prepareFailureCause(error) {
8042
+ const message = error instanceof Error ? error.message : String(error);
8043
+ if (/timed out/i.test(message)) return "prepare hook timed out";
8044
+ if (/MODULE_NOT_FOUND|Cannot find (?:module|package)/.test(message))
8045
+ return "prepare hook module missing; check the configured command and its installation for this device";
8046
+ if (/not valid JSON|JSON must carry|produced no output/.test(message))
8047
+ return "prepare hook returned malformed output; expected a cwd path or JSON with a non-empty cwd";
8048
+ if (/ENOENT/.test(message)) return "prepare hook executable missing (ENOENT)";
8049
+ if (/EACCES/.test(message)) return "prepare hook executable is not accessible (EACCES)";
8050
+ const exit = message.match(/exited with code (\d+)/)?.[1];
8051
+ return exit ? `prepare hook exited with code ${exit}` : "prepare hook failed; inspect the device-local hook log";
8052
+ }
8053
+ async function runPrepareWithReporting(runner, hook, input, context, reportError) {
8054
+ try {
8055
+ return await runner(hook, input);
8056
+ } catch (error) {
8057
+ if (hook.failureReporter) {
8058
+ try {
8059
+ await reportFailure(hook.failureReporter, {
8060
+ workspaceId: input.workspaceId,
8061
+ conversationId: input.conversationId,
8062
+ agentId: input.agentId,
8063
+ agentUsername: input.agentUsername,
8064
+ turnId: context.turnId,
8065
+ deviceId: context.deviceId ?? hostname(),
8066
+ cause: prepareFailureCause(error)
8067
+ });
8068
+ } catch {
8069
+ reportError();
8070
+ }
8071
+ }
8072
+ throw error;
8073
+ }
8074
+ }
8075
+ function reportFailure(config, body) {
8076
+ return new Promise((resolve2, reject) => {
8077
+ const child = spawn4(config.command, config.args ?? [], {
8078
+ stdio: ["pipe", "ignore", "ignore"],
8079
+ env: { ...process.env, ...config.env }
8080
+ });
8081
+ const timer = setTimeout(() => {
8082
+ child.kill("SIGKILL");
8083
+ reject(new Error("reporter timeout"));
8084
+ }, config.timeoutMs ?? 15e3);
8085
+ child.stdin.on("error", () => {
8086
+ });
8087
+ child.once("error", () => {
8088
+ clearTimeout(timer);
8089
+ reject(new Error("reporter failed to start"));
8090
+ });
8091
+ child.once("close", (code) => {
8092
+ clearTimeout(timer);
8093
+ if (code === 0) resolve2();
8094
+ else reject(new Error("reporter failed"));
8095
+ });
8096
+ child.stdin.end(JSON.stringify(body));
8097
+ });
8098
+ }
8099
+
8030
8100
  // src/prepared.ts
8031
8101
  import { mkdirSync as mkdirSync8, readFileSync as readFileSync6, rmSync as rmSync5, writeFileSync as writeFileSync6, existsSync as existsSync8 } from "fs";
8032
8102
  import { join as join12 } from "path";
@@ -8257,7 +8327,7 @@ function pruneOld(dir2, retain) {
8257
8327
  }
8258
8328
 
8259
8329
  // src/turn-containment.ts
8260
- import { execFile, execFileSync, spawn as spawn4 } from "child_process";
8330
+ import { execFile, execFileSync, spawn as spawn5 } from "child_process";
8261
8331
  import { readFileSync as readFileSync8 } from "fs";
8262
8332
  import { promisify } from "util";
8263
8333
  var execFileAsync = promisify(execFile);
@@ -8339,7 +8409,7 @@ function createTurnContainment(turnId, deps = {}) {
8339
8409
  command: options.command
8340
8410
  });
8341
8411
  return asContained(
8342
- spawn4(
8412
+ spawn5(
8343
8413
  "systemd-run",
8344
8414
  [
8345
8415
  "--user",
@@ -8366,7 +8436,7 @@ function createTurnContainment(turnId, deps = {}) {
8366
8436
  );
8367
8437
  }
8368
8438
  function spawnInGroup(options) {
8369
- const child = spawn4(options.command, options.args, {
8439
+ const child = spawn5(options.command, options.args, {
8370
8440
  ...options.cwd ? { cwd: options.cwd } : {},
8371
8441
  env: options.env,
8372
8442
  stdio: ["pipe", "pipe", "pipe"],
@@ -9068,18 +9138,28 @@ var TurnExecution = class {
9068
9138
  if (prepareHook.validateCached) {
9069
9139
  const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
9070
9140
  try {
9071
- await runHook(prepareHook, {
9072
- workspaceId,
9073
- conversationId: payload.conversationId,
9074
- agentId: payload.agentId,
9075
- agentUsername: this.opts.agentUsername,
9076
- runtime: this.turnContext.runtime,
9077
- hostAccess: this.turnContext.policy.hostFs,
9078
- triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
9079
- title: this.turnContext.conversation.title,
9080
- messageBody: this.turnContext.message.body,
9081
- prepared: cached2
9082
- });
9141
+ await runPrepareWithReporting(
9142
+ runHook,
9143
+ prepareHook,
9144
+ {
9145
+ workspaceId,
9146
+ conversationId: payload.conversationId,
9147
+ agentId: payload.agentId,
9148
+ agentUsername: this.opts.agentUsername,
9149
+ runtime: this.turnContext.runtime,
9150
+ hostAccess: this.turnContext.policy.hostFs,
9151
+ triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
9152
+ title: this.turnContext.conversation.title,
9153
+ messageBody: this.turnContext.message.body,
9154
+ prepared: cached2
9155
+ },
9156
+ { turnId, deviceId: this.opts.deviceId },
9157
+ () => {
9158
+ turnLog.error(
9159
+ "dispatcher: prepare failure reporter failed; original failure preserved"
9160
+ );
9161
+ }
9162
+ );
9083
9163
  } catch (err) {
9084
9164
  const reason = err instanceof Error ? err.message : String(err);
9085
9165
  turnLog.debug({ err: reason }, "dispatcher: prepare hook rejected a prepared turn");
@@ -9115,24 +9195,34 @@ var TurnExecution = class {
9115
9195
  preparingTimer.unref?.();
9116
9196
  const runHook = this.opts.prepareHookRunner ?? runPrepareHook;
9117
9197
  try {
9118
- const result = await runHook(prepareHook, {
9119
- workspaceId,
9120
- conversationId: payload.conversationId,
9121
- agentId: payload.agentId,
9122
- agentUsername: this.opts.agentUsername,
9123
- runtime: this.turnContext.runtime,
9124
- hostAccess: this.turnContext.policy.hostFs,
9125
- // CT317/CT319: the trigger message's referenced-entry paths — what the
9126
- // tasker prepare hook keys its per-task env off. Defaults to `[]` for
9127
- // an older API. The conversation anchor is gone (CT319).
9128
- triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
9129
- title: this.turnContext.conversation.title,
9130
- // CT943: the dispatching message's text where an `env:` directive
9131
- // rides. The server has always sent the trigger body on the turn
9132
- // context (for the live feed); this is the first thing to read it as
9133
- // an INPUT, so a dispatch can ask for its environment in words.
9134
- messageBody: this.turnContext.message.body
9135
- });
9198
+ const result = await runPrepareWithReporting(
9199
+ runHook,
9200
+ prepareHook,
9201
+ {
9202
+ workspaceId,
9203
+ conversationId: payload.conversationId,
9204
+ agentId: payload.agentId,
9205
+ agentUsername: this.opts.agentUsername,
9206
+ runtime: this.turnContext.runtime,
9207
+ hostAccess: this.turnContext.policy.hostFs,
9208
+ // CT317/CT319: the trigger message's referenced-entry paths — what the
9209
+ // tasker prepare hook keys its per-task env off. Defaults to `[]` for
9210
+ // an older API. The conversation anchor is gone (CT319).
9211
+ triggerEntryPaths: this.turnContext.conversation.triggerEntryPaths ?? [],
9212
+ title: this.turnContext.conversation.title,
9213
+ // CT943: the dispatching message's text where an `env:` directive
9214
+ // rides. The server has always sent the trigger body on the turn
9215
+ // context (for the live feed); this is the first thing to read it as
9216
+ // an INPUT, so a dispatch can ask for its environment in words.
9217
+ messageBody: this.turnContext.message.body
9218
+ },
9219
+ { turnId, deviceId: this.opts.deviceId },
9220
+ () => {
9221
+ turnLog.error(
9222
+ "dispatcher: prepare failure reporter failed; original failure preserved"
9223
+ );
9224
+ }
9225
+ );
9136
9226
  clearTimeout(preparingTimer);
9137
9227
  if (preparingStarted) reportPreparing("done");
9138
9228
  writePrepared(workspaceId, payload.conversationId, payload.agentId, result);
@@ -10293,7 +10383,7 @@ var CompanionSupervisor = class {
10293
10383
  async start() {
10294
10384
  this.log.info(
10295
10385
  { protocolVersion: TURN_PROTOCOL_VERSION, version: COMPANION_VERSION },
10296
- `Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname()}`
10386
+ `Companion ${COMPANION_VERSION} starting on ${this.config.deviceLabel ?? hostname2()}`
10297
10387
  );
10298
10388
  const startupHarnessRefresh = this.refreshHarnessStatuses();
10299
10389
  this.trackHeartbeat(startupHarnessRefresh);
@@ -10312,7 +10402,7 @@ var CompanionSupervisor = class {
10312
10402
  token: this.config.deviceToken,
10313
10403
  log: this.log,
10314
10404
  lastEventId: null,
10315
- onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname()}`),
10405
+ onOpen: () => this.log.info(`Connected to Cabane as ${this.hub.statusJson().device_label ?? hostname2()}`),
10316
10406
  onMessage: async (ev) => {
10317
10407
  if (ev.event === "assignments_changed") {
10318
10408
  if (this.refreshing && this.inFlightRefresh) await this.inFlightRefresh;
@@ -10728,6 +10818,7 @@ var CompanionSupervisor = class {
10728
10818
  });
10729
10819
  if (this.dispatcherFactory) return this.dispatcherFactory({ ...ctx, local });
10730
10820
  return new Dispatcher({
10821
+ deviceId: this.deviceId ?? void 0,
10731
10822
  api: ctx.api,
10732
10823
  baseUrl: ctx.baseUrl,
10733
10824
  workspaceId: ctx.workspaceId,
@@ -11375,9 +11466,9 @@ async function waitForBoundedHeartbeat(heartbeat) {
11375
11466
  }
11376
11467
  function defaultReexec() {
11377
11468
  clearRuntimeState();
11378
- void import("child_process").then(({ spawn: spawn5 }) => {
11469
+ void import("child_process").then(({ spawn: spawn6 }) => {
11379
11470
  try {
11380
- const child = spawn5(process.execPath, process.argv.slice(1), {
11471
+ const child = spawn6(process.execPath, process.argv.slice(1), {
11381
11472
  stdio: "inherit",
11382
11473
  detached: false
11383
11474
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cabane/companion",
3
- "version": "0.6.106",
3
+ "version": "0.6.107",
4
4
  "type": "module",
5
5
  "description": "The Cabane Companion (headless): connect a coding 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",