@haven_ai/connect 0.0.0-dev.202609231741.b88d644 → 0.0.0-dev.202609241737.2d2b91b

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/README.md CHANGED
@@ -175,7 +175,12 @@ minutes. Two rules follow:
175
175
  and a refusal is `{"tombstoned": false, "error": {"code", "next_action"}}`
176
176
  with exit 1 — so check the result rather than assuming silence means success
177
177
  (#2175). The `message` field is present only for connector-authored refusals;
178
- an unexpected filesystem error keeps its raw text on stderr alone.
178
+ an unexpected filesystem error keeps its raw text on stderr alone. If only the
179
+ surviving copy fails — the directory IS tombstoned — the run still succeeds
180
+ with `"recordPath": null` and `"mirrorError"` set to the errno code (or
181
+ `mirror_write_failed` when there is none), and `--unwire --json` carries the
182
+ same as `mirror_error`; `--replace` still removes the old directory's key
183
+ files and names the failure in `retirement_mirror_errors` (#3259).
179
184
 
180
185
  ### Unwiring an agent (`--unwire`, #2169)
181
186
 
@@ -352,7 +357,8 @@ read-only verification tools, `hosted_mcp_url`, `superseded_agent_ids`,
352
357
  `existing_agents_before_write` (#3122 — the other live-keyed directories named
353
358
  before the first write, each with the account it spends from), and — on a run
354
359
  that replaced existing wiring — `superseded_agents_retired_locally` with
355
- `retired_agent_ids`; `server_name_rebound_from` (#3122) appears only when the
360
+ `retired_agent_ids`, plus `retirement_mirror_errors` (#3259) only when a retired
361
+ directory's surviving ledger record could not be written; `server_name_rebound_from` (#3122) appears only when the
356
362
  run took a server name over from another directory's local binding record. It
357
363
  contains no API key, private key, credential
358
364
  contents, full credential paths, or full delegate address. The same redacted
package/dist/cli.cjs CHANGED
@@ -619,9 +619,9 @@ var init_runtime_manifest = __esm({
619
619
  mcpPackage: "@haven_ai/mcp",
620
620
  mcpVersion: mcp.MCP_VERSION,
621
621
  sdkPackage: "@haven_ai/sdk",
622
- sdkVersion: "0.0.0-dev.202609231741.b88d644",
622
+ sdkVersion: "0.0.0-dev.202609241737.2d2b91b",
623
623
  signerPackage: "@haven_ai/signer",
624
- signerVersion: "0.0.0-dev.202609231741.b88d644",
624
+ signerVersion: "0.0.0-dev.202609241737.2d2b91b",
625
625
  // Sourced from the SDK, never a literal (#1161). This field read '20.0.0'
626
626
  // while every package's `engines` said `>=24` and the docs said `>=24.0.0`,
627
627
  // so the guard that was supposed to enforce the floor waved Node v23 through
@@ -3098,9 +3098,14 @@ async function writeAgentTombstone(input) {
3098
3098
  const record = JSON.stringify(info, null, 2) + "\n";
3099
3099
  await promises.writeFile(path.join(input.directory, TOMBSTONE_FILENAME), record, "utf8");
3100
3100
  const root = input.tombstonesDir ?? defaultTombstonesDir();
3101
- await promises.mkdir(root, { recursive: true, mode: 448 });
3102
3101
  const recordPath = path.join(root, `${info.agent_id}.json`);
3103
- await promises.writeFile(recordPath, record, { mode: MIRROR_MODE });
3102
+ try {
3103
+ await promises.mkdir(root, { recursive: true, mode: 448 });
3104
+ await promises.writeFile(recordPath, record, { mode: MIRROR_MODE });
3105
+ } catch (err) {
3106
+ const code = err?.code;
3107
+ return { ...info, recordPath: null, mirrorError: typeof code === "string" && /^E[A-Z]+$/.test(code) ? code : "mirror_write_failed" };
3108
+ }
3104
3109
  return { ...info, recordPath };
3105
3110
  }
3106
3111
  async function readAgentTombstone(directory) {
@@ -3169,7 +3174,7 @@ async function unwireAgent(input) {
3169
3174
  const slug = input.slug ?? sidecar?.server_name;
3170
3175
  const names = serverNamesFor(slug);
3171
3176
  const runtimes = [];
3172
- const tombstoned = await tombstoneDirectoryIfAbsent({
3177
+ const { written: tombstoned, mirrorError } = await tombstoneDirectoryIfAbsent({
3173
3178
  directory: input.directory,
3174
3179
  agentId,
3175
3180
  reason: input.reason ?? "unwired via --unwire",
@@ -3263,7 +3268,16 @@ async function unwireAgent(input) {
3263
3268
  const bindingReleased = await clearMcpServerBinding(input.directory);
3264
3269
  const teardown = await decideTeardown(identity, input);
3265
3270
  if (teardown.status !== "retained") await teardownLocalKeyMaterial(input.directory, identity);
3266
- return { directory: input.directory, agentId, slug, tombstoned, runtimes, teardown, bindingReleased };
3271
+ return {
3272
+ directory: input.directory,
3273
+ agentId,
3274
+ slug,
3275
+ tombstoned,
3276
+ runtimes,
3277
+ teardown,
3278
+ bindingReleased,
3279
+ ...mirrorError === void 0 ? {} : { mirrorError }
3280
+ };
3267
3281
  }
3268
3282
  async function decideTeardown(identity, input) {
3269
3283
  const ended = "Local recovery of a stranded delegate balance ends with it: the sweep-recovery routes accept only this agent's API key and delegate signature, and neither exists on this machine any more.";
@@ -3308,9 +3322,9 @@ async function decideTeardown(identity, input) {
3308
3322
  }
3309
3323
  }
3310
3324
  async function tombstoneDirectoryIfAbsent(input) {
3311
- if (await readOptionalText(path.join(input.directory, TOMBSTONE_FILENAME)) !== null) return false;
3312
- await writeAgentTombstone(input);
3313
- return true;
3325
+ if (await readOptionalText(path.join(input.directory, TOMBSTONE_FILENAME)) !== null) return { written: false };
3326
+ const { mirrorError } = await writeAgentTombstone(input);
3327
+ return mirrorError === void 0 ? { written: true } : { written: true, mirrorError };
3314
3328
  }
3315
3329
  async function teardownLocalKeyMaterial(directory, identity) {
3316
3330
  await Promise.all([
@@ -4845,7 +4859,7 @@ init_unwire();
4845
4859
  init_tombstone();
4846
4860
  init_local_mcp_runtime();
4847
4861
  init_runtime_manifest();
4848
- var CONNECTOR_VERSION = "0.0.0-dev.202609231741.b88d644";
4862
+ var CONNECTOR_VERSION = "0.0.0-dev.202609241737.2d2b91b";
4849
4863
  var CONNECT_OUTCOME_SCHEMA_VERSION = 1;
4850
4864
  var failureOutcomesByError = /* @__PURE__ */ new WeakMap();
4851
4865
  function failureOutcomeFor(runtimeHint, error) {
@@ -5133,6 +5147,7 @@ async function executeConnect(options, deps, trace) {
5133
5147
  }
5134
5148
  let supersededAgentsRetiredLocally;
5135
5149
  const retiredAgentIds = [];
5150
+ const retirementMirrorErrors = {};
5136
5151
  if (replacing) {
5137
5152
  if (runtimeInstall.errorCode) {
5138
5153
  supersededAgentsRetiredLocally = false;
@@ -5143,7 +5158,7 @@ async function executeConnect(options, deps, trace) {
5143
5158
  supersededAgentsRetiredLocally = true;
5144
5159
  for (const entry of replacing.superseded) {
5145
5160
  try {
5146
- await tombstoneDirectoryIfAbsent({
5161
+ const { mirrorError } = await tombstoneDirectoryIfAbsent({
5147
5162
  directory: entry.directory,
5148
5163
  agentId: entry.agentId,
5149
5164
  reason: "replaced by a new setup (--replace)",
@@ -5152,6 +5167,12 @@ async function executeConnect(options, deps, trace) {
5152
5167
  // root → ~/.haven/tombstones, as before; any other → <root>/.tombstones).
5153
5168
  tombstonesDir: tombstonesDirForCredentialRoot(options.credentialsDir)
5154
5169
  });
5170
+ if (mirrorError !== void 0) {
5171
+ retirementMirrorErrors[entry.agentId] = mirrorError;
5172
+ log(
5173
+ `Warning: the surviving tombstone record for ${entry.agentId} could not be written to ${tombstonesDirForCredentialRoot(options.credentialsDir)} (${mirrorError}). The in-place TOMBSTONE.json stands; --doctor will not list this retirement once the directory is deleted.`
5174
+ );
5175
+ }
5155
5176
  await teardownLocalKeyMaterial(entry.directory, await readIdentityFile(entry.directory));
5156
5177
  await clearMcpServerBinding(entry.directory);
5157
5178
  retiredAgentIds.push(entry.agentId);
@@ -5223,6 +5244,7 @@ async function executeConnect(options, deps, trace) {
5223
5244
  supersededAgentIds,
5224
5245
  supersededAgentsRetiredLocally,
5225
5246
  ...replacing ? { retiredAgentIds } : {},
5247
+ ...Object.keys(retirementMirrorErrors).length > 0 ? { retirementMirrorErrors } : {},
5226
5248
  existingAgentsBeforeWrite: existingAgents.map((a) => ({ agent_id: a.agentId, account_address: a.accountAddress })),
5227
5249
  ...reboundFrom ? { serverNameReboundFrom: reboundFrom } : {},
5228
5250
  setupChallengeExpiresAt: setup.challenge.expires_at,
@@ -5289,6 +5311,7 @@ function completionOutcome(input) {
5289
5311
  superseded_agent_ids: input.supersededAgentIds ?? [],
5290
5312
  ...input.supersededAgentsRetiredLocally !== void 0 ? { superseded_agents_retired_locally: input.supersededAgentsRetiredLocally } : {},
5291
5313
  ...input.retiredAgentIds ? { retired_agent_ids: input.retiredAgentIds } : {},
5314
+ ...input.retirementMirrorErrors ? { retirement_mirror_errors: input.retirementMirrorErrors } : {},
5292
5315
  // #3122: always emitted on a completed run (empty list included), for the
5293
5316
  // same reason as superseded_agent_ids above.
5294
5317
  existing_agents_before_write: input.existingAgentsBeforeWrite ?? [],
@@ -5962,6 +5985,7 @@ async function runCli(argv, io = {
5962
5985
  agentId = identity.agent_id ?? "unknown";
5963
5986
  } catch {
5964
5987
  }
5988
+ const ledgerDir = tombstonesDirForAgentDirectory2(parsed.tombstone.directory);
5965
5989
  const info = await writeAgentTombstone2({
5966
5990
  directory: parsed.tombstone.directory,
5967
5991
  agentId,
@@ -5970,7 +5994,7 @@ async function runCli(argv, io = {
5970
5994
  // #3251: the ledger of the root that holds the retired directory —
5971
5995
  // ~/.haven/agents/<x> → ~/.haven/tombstones, exactly as before; a
5972
5996
  // directory under any other root → <root>/.tombstones.
5973
- tombstonesDir: tombstonesDirForAgentDirectory2(parsed.tombstone.directory)
5997
+ tombstonesDir: ledgerDir
5974
5998
  });
5975
5999
  if (parsed.json) {
5976
6000
  io.stdout(`${redactSecrets(JSON.stringify({ tombstoned: true, ...info }))}
@@ -5982,11 +6006,18 @@ async function runCli(argv, io = {
5982
6006
  "Key files were NOT touched and nothing was revoked \u2014 revoke the agent on the Haven agent page if you have not already.\n"
5983
6007
  );
5984
6008
  io.stdout(
5985
- `A surviving tombstone record was mirrored to ${redactSecrets(info.recordPath)}
6009
+ info.recordPath !== null ? `A surviving tombstone record was mirrored to ${redactSecrets(info.recordPath)}
6010
+ ` : (
6011
+ // #3259: the directory IS tombstoned; only the surviving copy failed.
6012
+ redactSecrets(
6013
+ `! The surviving tombstone record could NOT be written to ${ledgerDir} (${info.mirrorError}). The in-place TOMBSTONE.json stands, but --doctor will not list this retirement once the directory is deleted.
5986
6014
  `
6015
+ )
6016
+ )
5987
6017
  );
5988
6018
  io.stdout(
5989
- "Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot from its own start time, and the tombstone only speaks when a stale host next probes the old path. The mirrored record keeps this retirement observable even after the agent directory itself is deleted.\n"
6019
+ "Restart EVERY long-lived MCP host (gateway, TUI workers, editors): each holds the wiring snapshot from its own start time, and the tombstone only speaks when a stale host next probes the old path." + // #3259 review: never claim a copy that the line above says was not written.
6020
+ (info.recordPath !== null ? " The mirrored record keeps this retirement observable even after the agent directory itself is deleted." : "") + "\n"
5990
6021
  );
5991
6022
  }
5992
6023
  return 0;
@@ -6005,6 +6036,7 @@ async function runCli(argv, io = {
6005
6036
  const homeDir = homedir11();
6006
6037
  const root = parsed.options.credentialsDir ?? join13(homeDir, ".haven", "agents");
6007
6038
  const directory = parsed.unwireDir ?? (parsed.options.serverName ? join13(root, parsed.options.serverName) : root);
6039
+ const ledgerDir = tombstonesDirForAgentDirectory2(directory, homeDir);
6008
6040
  try {
6009
6041
  const result = await unwireAgent2({
6010
6042
  directory,
@@ -6019,7 +6051,7 @@ async function runCli(argv, io = {
6019
6051
  // directory itself, not a root. ~/.haven/agents/<slug> →
6020
6052
  // ~/.haven/tombstones, exactly as before; any other root →
6021
6053
  // <root>/.tombstones.
6022
- tombstonesDir: tombstonesDirForAgentDirectory2(directory, homeDir)
6054
+ tombstonesDir: ledgerDir
6023
6055
  });
6024
6056
  const failures = result.runtimes.filter((r) => r.status === "refused" || r.status === "unreadable");
6025
6057
  const retained = result.teardown.status === "retained";
@@ -6032,6 +6064,8 @@ async function runCli(argv, io = {
6032
6064
  slug: result.slug ?? null,
6033
6065
  directory: result.directory,
6034
6066
  tombstoned: result.tombstoned,
6067
+ // #3259: additive — present only when the surviving ledger record failed.
6068
+ ...result.mirrorError !== void 0 ? { mirror_error: result.mirrorError } : {},
6035
6069
  runtimes: result.runtimes.map((r) => ({
6036
6070
  runtime: r.runtime,
6037
6071
  label: r.label,
@@ -6057,6 +6091,12 @@ async function runCli(argv, io = {
6057
6091
  io.stdout(
6058
6092
  result.tombstoned ? " \xB7 Tombstoned first: any long-lived host still resolving the old wrapper gets the HAVEN-TOMBSTONE diagnosis.\n" : " \xB7 Directory was already tombstoned.\n"
6059
6093
  );
6094
+ if (result.mirrorError !== void 0) {
6095
+ io.stdout(redactSecrets(
6096
+ ` ! Surviving tombstone record: NOT written to ${ledgerDir} (${result.mirrorError}) \u2014 the in-place tombstone stands; --doctor will not list this retirement once the directory is deleted.
6097
+ `
6098
+ ));
6099
+ }
6060
6100
  for (const r of result.runtimes) {
6061
6101
  const mark = r.status === "removed" ? "\u2713" : r.status === "clean" ? "\u2013" : "\u2717";
6062
6102
  io.stdout(redactSecrets(` ${mark} ${r.label}: ${r.status}${r.detail ? ` \u2014 ${r.detail}` : ""}