agents-can-communicate 0.8.4 → 0.9.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 (142) hide show
  1. package/README.md +11 -7
  2. package/bin/entrypoints/acc-antigravity-relay.mjs +2 -4
  3. package/bin/entrypoints/acc-hook.mjs +33 -19
  4. package/bin/entrypoints/hook-output.mjs +14 -5
  5. package/docs/ADAPTER_AUTHORING.md +32 -0
  6. package/docs/ARCHITECTURE.md +7 -4
  7. package/docs/CAPABILITIES.md +7 -6
  8. package/docs/CLI.md +5 -0
  9. package/docs/CONFIGURATION.md +15 -10
  10. package/docs/DESIGN_DECISIONS.md +0 -2
  11. package/docs/GETTING_STARTED.md +39 -6
  12. package/docs/HOW_IT_WORKS.md +13 -8
  13. package/docs/SECURITY_MODEL.md +26 -5
  14. package/docs/TROUBLESHOOTING.md +74 -12
  15. package/docs/UPGRADING.md +50 -5
  16. package/node_modules/@agents-can-communicate/adapter-antigravity/certification.json +78 -0
  17. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/certification-provenance.json +111 -0
  18. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product-evidence.json +156 -0
  19. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/delivery/antigravity-desktop-2.19.1-product.json +25 -0
  20. package/node_modules/@agents-can-communicate/adapter-antigravity/fixtures/desktop-2.19.1.json +141 -0
  21. package/node_modules/@agents-can-communicate/adapter-antigravity/package.json +4 -1
  22. package/node_modules/@agents-can-communicate/adapter-antigravity/plugin/plugin.json +1 -1
  23. package/node_modules/@agents-can-communicate/adapter-antigravity/src/adapter.mjs +13 -2
  24. package/node_modules/@agents-can-communicate/adapter-antigravity/src/agentapi.mjs +7 -5
  25. package/node_modules/@agents-can-communicate/adapter-antigravity/src/allow-rule.mjs +98 -17
  26. package/node_modules/@agents-can-communicate/adapter-antigravity/src/client-process.mjs +64 -0
  27. package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-allow-rule.mjs +140 -0
  28. package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-delivery.mjs +110 -0
  29. package/node_modules/@agents-can-communicate/adapter-antigravity/src/desktop-endpoint.mjs +203 -0
  30. package/node_modules/@agents-can-communicate/adapter-antigravity/src/install.mjs +61 -30
  31. package/node_modules/@agents-can-communicate/adapter-antigravity/src/native-delivery.mjs +61 -24
  32. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-endpoint.mjs +30 -20
  33. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay-start.mjs +2 -1
  34. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relay.mjs +20 -12
  35. package/node_modules/@agents-can-communicate/adapter-antigravity/src/relays.mjs +18 -4
  36. package/node_modules/@agents-can-communicate/adapter-claude-code/package.json +1 -1
  37. package/node_modules/@agents-can-communicate/adapter-claude-code/src/adapter.mjs +6 -0
  38. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-delivery.mjs +50 -37
  39. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-endpoint.mjs +44 -31
  40. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-registry.mjs +99 -14
  41. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-settings.mjs +3 -7
  42. package/node_modules/@agents-can-communicate/adapter-claude-code/src/inbox-socket-directories.mjs +6 -4
  43. package/node_modules/@agents-can-communicate/adapter-claude-code/src/install.mjs +32 -7
  44. package/node_modules/@agents-can-communicate/adapter-codex/package.json +1 -1
  45. package/node_modules/@agents-can-communicate/adapter-codex/src/app-server-client.mjs +68 -3
  46. package/node_modules/@agents-can-communicate/adapter-codex/src/daemon-layouts.mjs +13 -0
  47. package/node_modules/@agents-can-communicate/adapter-codex/src/embedded-host.mjs +87 -13
  48. package/node_modules/@agents-can-communicate/adapter-codex/src/hooks.mjs +16 -6
  49. package/node_modules/@agents-can-communicate/adapter-codex/src/install.mjs +18 -9
  50. package/node_modules/@agents-can-communicate/adapter-codex/src/maintenance-host.mjs +68 -27
  51. package/node_modules/@agents-can-communicate/adapter-codex/src/native-delivery.mjs +66 -26
  52. package/node_modules/@agents-can-communicate/adapter-codex/src/native-endpoint.mjs +39 -16
  53. package/node_modules/@agents-can-communicate/adapter-codex/src/ws-json-rpc.mjs +4 -2
  54. package/node_modules/@agents-can-communicate/adapter-gemini-cli/extension/gemini-extension.json +1 -1
  55. package/node_modules/@agents-can-communicate/adapter-gemini-cli/package.json +1 -1
  56. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/adapter.mjs +2 -1
  57. package/node_modules/@agents-can-communicate/adapter-gemini-cli/src/install.mjs +15 -9
  58. package/node_modules/@agents-can-communicate/adapter-grok/package.json +1 -1
  59. package/node_modules/@agents-can-communicate/adapter-grok/src/install.mjs +25 -14
  60. package/node_modules/@agents-can-communicate/adapter-kimi/package.json +1 -1
  61. package/node_modules/@agents-can-communicate/adapter-kimi/src/adapter.mjs +2 -1
  62. package/node_modules/@agents-can-communicate/adapter-kimi/src/install.mjs +15 -10
  63. package/node_modules/@agents-can-communicate/adapter-sdk/package.json +1 -1
  64. package/node_modules/@agents-can-communicate/adapter-sdk/src/capabilities.mjs +50 -5
  65. package/node_modules/@agents-can-communicate/adapter-sdk/src/certification.mjs +11 -2
  66. package/node_modules/@agents-can-communicate/adapter-sdk/src/executables.mjs +149 -0
  67. package/node_modules/@agents-can-communicate/adapter-sdk/src/hook-shim.mjs +79 -12
  68. package/node_modules/@agents-can-communicate/adapter-sdk/src/index.mjs +11 -2
  69. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-attempt.mjs +6 -2
  70. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-delivery.mjs +82 -17
  71. package/node_modules/@agents-can-communicate/adapter-sdk/src/native-vocabulary.mjs +9 -2
  72. package/node_modules/@agents-can-communicate/adapter-sdk/src/private-files.mjs +35 -0
  73. package/node_modules/@agents-can-communicate/adapter-sdk/src/process-table.mjs +232 -0
  74. package/node_modules/@agents-can-communicate/adapter-sdk/src/session-binding.mjs +8 -1
  75. package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-command.mjs +69 -0
  76. package/node_modules/@agents-can-communicate/adapter-sdk/src/windows-process-chain.wsf +45 -0
  77. package/node_modules/@agents-can-communicate/cli/package.json +2 -1
  78. package/node_modules/@agents-can-communicate/cli/src/config-command.mjs +2 -1
  79. package/node_modules/@agents-can-communicate/cli/src/doctor-command.mjs +60 -4
  80. package/node_modules/@agents-can-communicate/cli/src/hook-support.mjs +9 -0
  81. package/node_modules/@agents-can-communicate/cli/src/hook-workspace.mjs +12 -3
  82. package/node_modules/@agents-can-communicate/cli/src/main.mjs +2 -1
  83. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/activation.mjs +3 -2
  84. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/download.mjs +12 -3
  85. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/entry.mjs +7 -5
  86. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/generation.mjs +7 -7
  87. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/launchers.mjs +8 -5
  88. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/maintenance.mjs +1 -1
  89. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/mutex.mjs +7 -6
  90. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/portable-fs.mjs +165 -0
  91. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/schedule.mjs +1 -1
  92. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/state.mjs +18 -8
  93. package/node_modules/@agents-can-communicate/cli/src/managed-runtime/worker.mjs +18 -3
  94. package/node_modules/@agents-can-communicate/cli/src/native-session-diagnostics.mjs +15 -4
  95. package/node_modules/@agents-can-communicate/cli/src/platform-paths.mjs +15 -10
  96. package/node_modules/@agents-can-communicate/cli/src/runtime-paths.mjs +3 -2
  97. package/node_modules/@agents-can-communicate/cli/src/update-command.mjs +11 -1
  98. package/node_modules/@agents-can-communicate/cli/src/workspace-discovery.mjs +4 -2
  99. package/node_modules/@agents-can-communicate/core/package.json +1 -1
  100. package/node_modules/@agents-can-communicate/core/src/delivery-bindings.mjs +7 -0
  101. package/node_modules/@agents-can-communicate/core/src/materialisation.mjs +39 -1
  102. package/node_modules/@agents-can-communicate/core/src/session-opening.mjs +11 -0
  103. package/node_modules/@agents-can-communicate/core/src/sessions.mjs +8 -2
  104. package/node_modules/@agents-can-communicate/delivery-router/package.json +1 -1
  105. package/node_modules/@agents-can-communicate/delivery-router/src/native-reason.mjs +6 -2
  106. package/node_modules/@agents-can-communicate/delivery-router/src/router.mjs +9 -5
  107. package/node_modules/@agents-can-communicate/hook-runner/package.json +1 -1
  108. package/node_modules/@agents-can-communicate/hook-runner/src/client-pid.mjs +65 -6
  109. package/node_modules/@agents-can-communicate/hook-runner/src/client-version.mjs +80 -0
  110. package/node_modules/@agents-can-communicate/hook-runner/src/native-attempt.mjs +7 -2
  111. package/node_modules/@agents-can-communicate/hook-runner/src/native-binding.mjs +6 -3
  112. package/node_modules/@agents-can-communicate/hook-runner/src/owner-context.mjs +34 -6
  113. package/node_modules/@agents-can-communicate/hook-runner/src/process-table.mjs +3 -50
  114. package/node_modules/@agents-can-communicate/hook-runner/src/runner.mjs +167 -34
  115. package/node_modules/@agents-can-communicate/installer/package.json +3 -2
  116. package/node_modules/@agents-can-communicate/installer/src/delivery-diagnostics.mjs +11 -2
  117. package/node_modules/@agents-can-communicate/installer/src/detect.mjs +13 -3
  118. package/node_modules/@agents-can-communicate/installer/src/hook-support.mjs +5 -0
  119. package/node_modules/@agents-can-communicate/installer/src/native-activation.mjs +6 -14
  120. package/node_modules/@agents-can-communicate/installer/src/plan.mjs +14 -0
  121. package/node_modules/@agents-can-communicate/mcp-server/package.json +1 -1
  122. package/node_modules/@agents-can-communicate/protocol/package.json +1 -1
  123. package/node_modules/@agents-can-communicate/protocol/src/config.mjs +6 -2
  124. package/node_modules/@agents-can-communicate/protocol/src/resources.mjs +6 -2
  125. package/node_modules/@agents-can-communicate/storage-filesystem/package.json +1 -1
  126. package/node_modules/@agents-can-communicate/storage-filesystem/src/active-journal.mjs +5 -2
  127. package/node_modules/@agents-can-communicate/storage-filesystem/src/atomic-json.mjs +41 -23
  128. package/node_modules/@agents-can-communicate/storage-filesystem/src/doomed-directory.mjs +4 -12
  129. package/node_modules/@agents-can-communicate/storage-filesystem/src/identity.mjs +7 -3
  130. package/node_modules/@agents-can-communicate/storage-filesystem/src/index.mjs +1 -0
  131. package/node_modules/@agents-can-communicate/storage-filesystem/src/journal.mjs +24 -7
  132. package/node_modules/@agents-can-communicate/storage-filesystem/src/portable-fs.mjs +165 -0
  133. package/node_modules/@agents-can-communicate/storage-filesystem/src/publication-path.mjs +8 -0
  134. package/node_modules/@agents-can-communicate/storage-filesystem/src/reclaim.mjs +26 -17
  135. package/node_modules/@agents-can-communicate/storage-filesystem/src/recovery.mjs +7 -5
  136. package/node_modules/@agents-can-communicate/storage-filesystem/src/retention.mjs +7 -16
  137. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-directory.mjs +138 -24
  138. package/node_modules/@agents-can-communicate/storage-filesystem/src/safe-file.mjs +3 -2
  139. package/node_modules/@agents-can-communicate/storage-filesystem/src/stage-sweep.mjs +9 -6
  140. package/node_modules/@agents-can-communicate/storage-filesystem/src/store.mjs +30 -8
  141. package/node_modules/@agents-can-communicate/storage-filesystem/src/writer-mutex.mjs +102 -32
  142. package/package.json +1 -5
@@ -8,20 +8,22 @@ import { capabilityEvidence, clearNativeAttempt, clearSessionBinding, effectiveC
8
8
  loadSessionBinding, normalizedEvent, storeSessionBinding }
9
9
  from "@agents-can-communicate/adapter-sdk";
10
10
  import { createCoordinationService } from "@agents-can-communicate/core";
11
- import { loadOwnership } from "@agents-can-communicate/installer";
11
+ import { loadOwnership } from "@agents-can-communicate/installer/hook-support";
12
12
  import { AccError, createId } from "@agents-can-communicate/protocol";
13
13
  import { openFilesystemStore } from "@agents-can-communicate/storage-filesystem";
14
14
  import { clearPin, createGitProbe, resolveHookWorkspace, platformDataHome, runtimePaths,
15
15
  writePin as writeRuntimePin }
16
- from "@agents-can-communicate/cli";
16
+ from "@agents-can-communicate/cli/hook-support";
17
17
 
18
- import { resolveClientPid } from "./client-pid.mjs";
19
- import { probeClientVersion as defaultProbeClientVersion } from "./client-version.mjs";
18
+ import { resolveClient } from "./client-pid.mjs";
19
+ import { ownExecutable, probeClientVersion as defaultProbeClientVersion, probeExecutableVersion }
20
+ from "./client-version.mjs";
20
21
  import { bindNative, callRelease, nativeActivationHintFor, nativeDiagnosticDeadline }
21
22
  from "./native-attempt.mjs";
22
23
  import { readProcessTable as defaultReadProcessTable } from "./process-table.mjs";
23
24
  import { withSessionLifecycle } from "./session-lifecycle.mjs";
24
- import { appendStartOwner, appendToolOwner, ownerHeader, ownerOnlyOutcome } from "./owner-context.mjs";
25
+ import { appendStartOwner, appendToolOwner, assertOwnerQuotable, ownerHeader, ownerOnlyOutcome }
26
+ from "./owner-context.mjs";
25
27
 
26
28
  // Kept cohesive above 300 lines because every handler shares one fail-open
27
29
  // hook boundary, binding lifecycle, and client-specific outcome contract.
@@ -31,6 +33,16 @@ import { appendStartOwner, appendToolOwner, ownerHeader, ownerOnlyOutcome } from
31
33
  // A hook runs in front of the user's turn, so it gets a hard ceiling. Better to
32
34
  // let a call through than to make someone's session sit waiting on us.
33
35
  const DEFAULT_BUDGET_MS = 5_000;
36
+ // Failures the hook output explains with advice of its own; any other error
37
+ // text stays out of what the client shows.
38
+ const KNOWN_FAILURES = new Set(["workspace_contains_runtime", "workspace_path_unquotable"]);
39
+ // Windows reads the table through WMI in Script Host, 145 ms on a
40
+ // windows-latest runner, and through PowerShell where Script Host is off, whose
41
+ // first start on a machine took 3.8 s. It gets more of the budget there, and
42
+ // leaves the rest for the write that follows.
43
+ const WINDOWS = process.platform === "win32";
44
+ const PROCESS_TABLE_MS = WINDOWS ? 3_000 : 1_000;
45
+ const TABLE_RESERVE_MS = WINDOWS ? 500 : 0;
34
46
 
35
47
  function assertHookBudget(deadline, message = "hook deadline expired") {
36
48
  if (Date.now() >= deadline) throw new Error(message);
@@ -238,8 +250,8 @@ async function openContext({ event, adapterId, dataHome, runtime, env, deadline
238
250
  // offered. Runs after the heartbeat and the bounded native retry.
239
251
  // `render` is how the projected text reaches this client: before a turn it is
240
252
  // the adapter's context envelope, at the end of one it is a continuation.
241
- async function projectTurn({ binding, context, adapter, adapterId,
242
- render = text => adapter.injectOutcome?.(text), activationHint = null, repeats = true }) {
253
+ async function projectTurn({ binding, context, adapter, adapterId, render = null,
254
+ activationHint = null, activationNotice = null, repeats = true }) {
243
255
  const sync = await context.service.sync({ sessionId: binding.accSessionId,
244
256
  cursor: null, scope: "delta" });
245
257
 
@@ -283,11 +295,17 @@ async function projectTurn({ binding, context, adapter, adapterId,
283
295
  const owner = hintFits ? `${header}\n${activationHint}` : header;
284
296
  const withHint = (outcome, attached) => ({ ...outcome,
285
297
  activationHintIncluded: attached === true && outcome.stdout !== "" });
298
+ // The ask's line for the user travels with its line for the model, or not
299
+ // at all: the adapter shows it where the client shows a hook's message.
300
+ const notice = hintFits && activationNotice !== null ? { userMessage: activationNotice } : null;
301
+ const inject = text => notice === null ? adapter.injectOutcome?.(text)
302
+ : adapter.injectOutcome?.(text, notice);
303
+ const renderText = render ?? inject;
286
304
  // A peer can join after this prompt has begun. The current turn must already
287
305
  // have its own arguments when it needs inbox/reply, without reattaching or
288
306
  // waiting for another user prompt. Solo emits identity, not a peer notice.
289
307
  if (sync.solo && messages.length === 0) {
290
- return withHint(ownerOnlyOutcome(text => adapter.injectOutcome?.(text), owner, totalBudget),
308
+ return withHint(ownerOnlyOutcome(inject, owner, totalBudget),
291
309
  hintFits);
292
310
  }
293
311
 
@@ -336,7 +354,7 @@ async function projectTurn({ binding, context, adapter, adapterId,
336
354
  // Own claims make sync non-solo but produce no peer context. They cannot
337
355
  // remove this turn's identity or consume a nonexistent body separator.
338
356
  if (body === "" && messages.length === 0) {
339
- return withHint(ownerOnlyOutcome(text => adapter.injectOutcome?.(text), owner, totalBudget),
357
+ return withHint(ownerOnlyOutcome(inject, owner, totalBudget),
340
358
  hintFits);
341
359
  }
342
360
  const projected = body === "" ? "" : ownerFits ? `${owner}\n${body}` : body;
@@ -361,7 +379,7 @@ async function projectTurn({ binding, context, adapter, adapterId,
361
379
  // The entry point owns the transport boundary. This handler only prepares
362
380
  // offer inputs; recording them here would claim delivery before stdout's
363
381
  // callback proves that the bytes crossed.
364
- const outcome = { stdout: "", ...render(projected) };
382
+ const outcome = { stdout: "", ...renderText(projected) };
365
383
  const writableOffers = outcome.stdout === "" ? [] : offerInputs;
366
384
  return withHint({ ...outcome,
367
385
  stderr: [outcome.stderr, ownerWarning, degradation].filter(Boolean).join("\n"),
@@ -373,7 +391,8 @@ async function projectTurn({ binding, context, adapter, adapterId,
373
391
  // so the reservation goes with it.
374
392
  async function projectActivation(input, offered) {
375
393
  try {
376
- const turn = await projectTurn({ ...input, activationHint: offered?.line ?? null });
394
+ const turn = await projectTurn({ ...input, activationHint: offered?.line ?? null,
395
+ activationNotice: offered?.userMessage ?? null });
377
396
  const included = turn.activationHintIncluded === true;
378
397
  delete turn.activationHintIncluded;
379
398
  if (!offered) return turn;
@@ -389,13 +408,71 @@ async function projectActivation(input, offered) {
389
408
  }
390
409
  }
391
410
 
411
+ const STABLE_OR_PRERELEASE = /^\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?$/;
412
+ const isVersion = value => typeof value === "string" && STABLE_OR_PRERELEASE.test(value);
413
+
414
+ /**
415
+ * Which of its products the adapter recognised the client as. A variant name
416
+ * the adapter does not declare is ignored together with the version it came
417
+ * with: that version counts on a line ACC has no evidence for.
418
+ */
419
+ function declaredProduct(adapter, client) {
420
+ const name = client?.certificationName;
421
+ if (typeof name !== "string") return { clientName: undefined, version: client?.version };
422
+ const primary = adapter.client?.certificationName ?? adapter.client?.command;
423
+ if (name === primary) return { clientName: undefined, version: client.version };
424
+ const declared = (adapter.client?.variants ?? []).some(variant => variant?.certificationName === name);
425
+ return declared ? { clientName: name, version: client.version }
426
+ : { clientName: undefined, version: undefined };
427
+ }
428
+
429
+ /**
430
+ * The version the session's own client runs, or null to fall back to the PATH
431
+ * probe: what its command line named, then the adapter's reading of the
432
+ * running client (Claude Code's session registry), then the client's own
433
+ * executable when `ps` names it by full path. Each step is bounded by the
434
+ * hook's budget and fails open.
435
+ */
436
+ async function runningClientVersion({ adapter, client, product, env, deadline }) {
437
+ if (client === null || client === undefined) return null;
438
+ if (isVersion(product.version)) return product.version;
439
+ const remaining = cap => Math.max(1, Math.min(cap, deadline - Date.now() - TABLE_RESERVE_MS));
440
+ if (typeof adapter.clientVersionOf === "function" && client.entry !== undefined) {
441
+ let timer;
442
+ const read = await Promise.race([
443
+ Promise.resolve().then(() => adapter.clientVersionOf({ pid: client.pid, entry: client.entry, env }))
444
+ .catch(() => null),
445
+ new Promise(resolve => { timer = setTimeout(() => resolve(null), remaining(500)); }),
446
+ ]).finally(() => clearTimeout(timer));
447
+ if (isVersion(read)) return read;
448
+ }
449
+ const file = ownExecutable(client.entry, adapter.client?.command);
450
+ if (file === null) return null;
451
+ return probeExecutableVersion(file, adapter.client?.versionArgs ?? ["--version"],
452
+ { timeoutMs: remaining(1_000) });
453
+ }
454
+
455
+ // Whether the client process a binding names has exited. Signal 0 delivers
456
+ // nothing; EPERM means the process exists under another user.
457
+ function clientGone(binding) {
458
+ const pid = binding?.clientPid;
459
+ if (!Number.isInteger(pid) || pid <= 0) return false;
460
+ try {
461
+ process.kill(pid, 0);
462
+ return false;
463
+ } catch (error) {
464
+ return error?.code === "ESRCH";
465
+ }
466
+ }
467
+
392
468
  const HANDLERS = {
393
469
  // `knownClientPid` and `pins: false` are registration's (#167): it runs in
394
470
  // a peer's process, so the client's pid is handed over rather than found in
395
471
  // this process's ancestry, and it pins nothing because no hook code of any
396
472
  // generation runs for the session until its first real hook pins its own.
397
473
  async sessionStart({ event, context, adapter, adapterId, binding, paths,
398
- readProcessTable, probeClientVersion, platform, deadline, knownClientPid, pins = true }) {
474
+ readProcessTable, probeClientVersion, platform, deadline, knownClientPid, pins = true,
475
+ tableRead: started = null, mark = () => {}, clientRestarted = false }) {
399
476
  // Where this session's pin lives, resolved once and reused at every write
400
477
  // below. Beside `paths.root`, never inside it: see `managerRootFor`.
401
478
  const pinRoot = managerRootFor(context.dataHome);
@@ -412,23 +489,42 @@ const HANDLERS = {
412
489
  await writePin({ root: pinRoot, harnessSessionId: event.sessionId, runtimeRoot: facts.runtimeRoot,
413
490
  version: facts.version, storeVersion: facts.storeVersion, clientPid: binding.clientPid });
414
491
  }
415
- const clientVersion = await probeClientVersion(adapter,
416
- { timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) });
417
- assertHookBudget(deadline);
418
- const clientFacts = { clientVersion, platform };
419
- const capabilities = effectiveCapabilities(adapter, clientFacts);
420
492
  // Once per session, never per turn. A client that cannot be found yields
421
493
  // null, and the session is then judged by age alone - which is exactly the
422
494
  // behaviour every session had before this existed.
423
495
  const command = adapter.client?.command ?? null;
424
- const pid = Number.isInteger(knownClientPid) && knownClientPid > 0 ? knownClientPid
425
- : command === null ? null
426
- : resolveClientPid({ table: await readProcessTable({
427
- timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) }), from: process.pid, command });
496
+ const known = Number.isInteger(knownClientPid) && knownClientPid > 0;
497
+ // Read while the client reports its version: neither needs the other, and
498
+ // each can start a program. A session start began the read already, when
499
+ // its event was known. Handled here so a probe that fails first leaves no
500
+ // rejection unobserved.
501
+ const tableRead = known || command === null ? null : started ?? Promise.resolve(readProcessTable({
502
+ timeoutMs: Math.max(1, Math.min(PROCESS_TABLE_MS, deadline - Date.now() - TABLE_RESERVE_MS)) }));
503
+ tableRead?.catch(() => {});
504
+ // The binary on PATH answers in parallel, as the fallback: the version the
505
+ // running client names for itself, when it names one, wins below.
506
+ const pathVersion = probeClientVersion(adapter,
507
+ { timeoutMs: Math.max(1, Math.min(1_000, deadline - Date.now())) });
508
+ mark("reading the process table");
509
+ const client = known ? { pid: knownClientPid } : command === null ? null
510
+ : resolveClient({ table: await tableRead, from: process.pid, command,
511
+ clientPackage: adapter.client?.package, identify: adapter.identifyClientProcess });
428
512
  assertHookBudget(deadline);
513
+ mark("probing the client version");
514
+ const product = declaredProduct(adapter, client);
515
+ const clientVersion = await runningClientVersion({ adapter, client, product, env: context.env,
516
+ deadline }) ?? await pathVersion;
517
+ assertHookBudget(deadline);
518
+ const clientFacts = { clientVersion, platform,
519
+ ...(product.clientName === undefined ? {} : { clientName: product.clientName }) };
520
+ const capabilities = effectiveCapabilities(adapter, clientFacts);
521
+ const pid = client?.pid ?? null;
522
+ mark("opening the session");
429
523
  const clientPid = Number.isInteger(pid) && pid > 0 ? pid : undefined;
430
- const native = hookBinding => bindNative({ adapter, event, hookBinding, ...clientFacts,
431
- context, paths, deadline });
524
+ const native = hookBinding => {
525
+ mark("binding live delivery");
526
+ return bindNative({ adapter, event, hookBinding, ...clientFacts, context, paths, deadline });
527
+ };
432
528
  const metadata = {
433
529
  pid,
434
530
  enforcement: capabilities.guards?.beforeWrite === true ? "guarded" : "advisory",
@@ -439,13 +535,18 @@ const HANDLERS = {
439
535
  };
440
536
  if (event.kind === "beforeTurn" && binding !== null) {
441
537
  // A genuine prompt can resume after an ephemeral removal or a durable
442
- // close. Recheck after probes: a CLI replacement can run outside the
443
- // native lifecycle lock. Never adopt that replacement's identity.
538
+ // close, or carry on a session whose client restarted. Recheck after
539
+ // probes: a CLI replacement can run outside the native lifecycle lock.
540
+ // Never adopt that replacement's identity.
541
+ // A session can also close between the turn's check and this one while
542
+ // its client was down; that is the closed-session resumption below,
543
+ // not a replacement.
444
544
  const previous = await context.service.locateSession(binding.accSessionId);
445
- if (previous !== null && (previous.record.state !== "closed"
446
- || previous.record.generation !== binding.generation)) {
447
- throw new Error("the completed hook owner changed during turn registration");
448
- }
545
+ const sameGeneration = previous?.record.generation === binding.generation;
546
+ const expected = previous === null
547
+ || sameGeneration && (previous.record.state === "closed"
548
+ || clientRestarted && previous.record.state === "open");
549
+ if (!expected) throw new Error("the completed hook owner changed during turn registration");
449
550
  }
450
551
  if (binding !== null) {
451
552
  const resumed = await context.service.resumeSession({
@@ -564,6 +665,21 @@ const HANDLERS = {
564
665
  const turn = await projectActivation({ ...input, binding: fresh }, offered);
565
666
  return { ...turn, nativeBinding: started.nativeBinding };
566
667
  }
668
+ if (clientGone(binding)) {
669
+ // The client this session was bound to has exited, and a turn arrived
670
+ // anyway: the client restarted under the same conversation. Antigravity
671
+ // 2.0 reopens a conversation without a second SessionStart, so without
672
+ // this the session stayed bound to a dead process and lost live delivery
673
+ // for good. The start path finds the client running now, keeps this ACC
674
+ // session and generation, and binds again.
675
+ const started = await HANDLERS.sessionStart({ ...input, clientRestarted: true });
676
+ const fresh = await loadSessionBinding({ runtimeDir: paths.root,
677
+ harnessSessionId: event.sessionId });
678
+ const offered = await nativeActivationHintFor({ adapter, event,
679
+ nativeBinding: started.nativeBinding, binding: fresh, context, paths, deadline });
680
+ const turn = await projectActivation({ ...input, binding: fresh }, offered);
681
+ return { ...turn, nativeBinding: started.nativeBinding };
682
+ }
567
683
  // A turn is the clearest sign a session is alive. Never a reason to fail:
568
684
  // this runs in front of somebody's prompt.
569
685
  await context.service.heartbeatSession({ sessionId: binding.accSessionId,
@@ -697,35 +813,51 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
697
813
  const deadline = Date.now() + budgetMs;
698
814
  const fallback = { stdout: "", exitCode: 0, decision: "allow", sessions: [], deadlineAt: deadline,
699
815
  commitOffers: async () => {} };
816
+ // Where the hook is, so a budget that runs out can say which step held it.
817
+ const trail = { phase: "reading the hook event" };
818
+ const mark = phase => { trail.phase = phase; };
700
819
  const execute = async () => {
701
820
  assertHookBudget(deadline);
702
821
  const adapter = adapters?.[adapterId];
703
822
  if (adapter === undefined) throw new Error(`no adapter named ${adapterId}`);
704
823
 
705
824
  const event = await adapter.normalizeHook(payload, { args });
825
+ // A session start reads the process table once, and it depends on nothing
826
+ // below: it starts now, while the store opens. On Windows it is the part
827
+ // of a start that runs another program.
828
+ const tableRead = event.kind !== "sessionStart" || (adapter.client?.command ?? null) === null
829
+ ? null : Promise.resolve(readProcessTable({ timeoutMs: Math.max(1,
830
+ Math.min(PROCESS_TABLE_MS, deadline - Date.now() - TABLE_RESERVE_MS)) }));
831
+ tableRead?.catch(() => {});
832
+ mark("finding the workspace");
706
833
  const context = await openContext({ event, adapterId, dataHome, runtime, env, deadline });
834
+ assertOwnerQuotable(context.workspaceCwd, context.workspaceRef, String(platform).split("-")[0]);
707
835
  const handler = HANDLERS[event.kind];
708
836
  const lifecycle = ["sessionStart", "sessionEnd", "beforeTurn"].includes(event.kind);
709
837
  const invoke = async () => {
710
838
  assertHookBudget(deadline);
711
839
  if (lifecycle) {
840
+ mark("opening the store");
712
841
  // A previous holder may have died after journalling its session while
713
842
  // this process waited. Recover that write before interpreting its binding.
714
843
  const store = await openFilesystemStore({ root: context.paths.root, clock: runtime.clock,
715
844
  ids: runtime.ids, workspaceId: context.descriptor.id, deadlineAt: deadline });
716
845
  context.service = createCoordinationService({ store, clock: runtime.clock, ids: runtime.ids });
717
846
  }
847
+ mark("reading the session binding");
718
848
  const binding = await loadSessionBinding({ runtimeDir: context.paths.root,
719
849
  harnessSessionId: event.sessionId });
720
850
  assertHookBudget(deadline);
851
+ mark("handling the event");
721
852
  // The raw payload goes to a handler only to be handed back to the adapter
722
853
  // that produced it; nothing in core reads it.
723
854
  const result = handler === undefined ? {} : await handler({ event, context, adapter, adapterId,
724
855
  binding, paths: context.paths, payload,
725
- readProcessTable, probeClientVersion, platform, deadline });
856
+ readProcessTable, probeClientVersion, platform, deadline, tableRead, mark });
726
857
  return appendToolOwner(appendStartOwner(result, { event, context, adapter }),
727
858
  { event, binding, context, adapter });
728
859
  };
860
+ if (lifecycle) mark("waiting for the session lock");
729
861
  const work = lifecycle
730
862
  ? withSessionLifecycle({ root: context.paths.root, sessionId: event.sessionId,
731
863
  clock: runtime.clock, deadlineAt: deadline }, invoke)
@@ -748,6 +880,7 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
748
880
  };
749
881
  delete result.offerInputs;
750
882
 
883
+ mark("reading the status");
751
884
  const status = await context.service.collectStatus({
752
885
  workspaceId: context.descriptor.id });
753
886
  assertHookBudget(deadline);
@@ -762,15 +895,15 @@ export async function runHook({ adapterId, payload, adapters, dataHome, env,
762
895
  // final status read. Store/binding deadlines fence undecided writes; an
763
896
  // activated journal still completes under its writer lock after expiry.
764
897
  const budget = new Promise(resolve => {
765
- timer = setTimeout(() => resolve({ ...fallback, timedOut: true }),
898
+ timer = setTimeout(() => resolve({ ...fallback, timedOut: true, phase: trail.phase }),
766
899
  Math.max(0, deadline - Date.now()));
767
900
  });
768
901
  return await Promise.race([execute(), budget]);
769
902
  } catch (error) {
770
903
  return { ...fallback, failed: true, reason: error.message,
771
- ...(error instanceof AccError && error.details?.reasonCode === "workspace_contains_runtime"
772
- ? { failureCode: "workspace_contains_runtime" } : {}),
773
- ...(Date.now() >= deadline ? { timedOut: true } : {}) };
904
+ ...(error instanceof AccError && KNOWN_FAILURES.has(error.details?.reasonCode)
905
+ ? { failureCode: error.details.reasonCode } : {}),
906
+ ...(Date.now() >= deadline ? { timedOut: true, phase: trail.phase } : {}) };
774
907
  } finally {
775
908
  clearTimeout(timer);
776
909
  }
@@ -1,10 +1,11 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/installer",
3
- "version": "0.8.4",
3
+ "version": "0.9.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
7
- ".": "./src/index.mjs"
7
+ ".": "./src/index.mjs",
8
+ "./hook-support": "./src/hook-support.mjs"
8
9
  },
9
10
  "files": [
10
11
  "src/"
@@ -1,7 +1,10 @@
1
+ import { isLaunchOption } from "@agents-can-communicate/adapter-sdk";
2
+
1
3
  // Shared presentation of observed delivery facts. Vendor probes provide closed
2
4
  // reasons; installation and doctor must not turn a version floor into readiness.
3
5
  const REASONS = Object.freeze({
4
- native_delivery_unsupported: "this adapter has no native delivery channel",
6
+ // Said of a client that has no live channel ACC supports.
7
+ native_delivery_unsupported: "this client has no live delivery channel ACC supports here",
5
8
  version_unavailable: "the client version could not be verified",
6
9
  below_minimum_version: "the client is below the native delivery minimum version",
7
10
  known_bad_version: "this client version has a known native delivery failure",
@@ -16,10 +19,16 @@ const REASONS = Object.freeze({
16
19
  + "cannot reach",
17
20
  });
18
21
 
19
- export function describeNativeReason(reason, { clientVersion, minimumVersion } = {}) {
22
+ export function describeNativeReason(reason, { clientVersion, minimumVersion, launchOption } = {}) {
20
23
  if (reason === "below_minimum_version" && clientVersion && minimumVersion) {
21
24
  return `client ${clientVersion} needs version ${minimumVersion} or newer for native delivery`;
22
25
  }
26
+ // A client can run embedded because of how it was started - Codex does for
27
+ // `--search`, `-c`, `--profile` and others while its daemon runs.
28
+ if (reason === "client_session_embedded" && isLaunchOption(launchOption)) {
29
+ return `the client was started with ${launchOption}, which runs this session on its own `
30
+ + "embedded service that peers cannot reach";
31
+ }
23
32
  return REASONS[reason] ?? reason ?? "readiness is unverified";
24
33
  }
25
34
  export const describeDeliveryFallback = entry => entry.capabilities?.delivery?.nextTurn === true
@@ -1,8 +1,8 @@
1
1
  import { execFile } from "node:child_process";
2
2
  import { promisify } from "node:util";
3
3
 
4
- import { capabilityEvidence, effectiveCapabilities, evaluateNativeEligibility,
5
- validateNativeActivationPlan }
4
+ import { capabilityEvidence, effectiveCapabilities, evaluateNativeEligibility, mergeEnv, pathOf,
5
+ resolveExecutable as resolveOnPath, runExecutable, validateNativeActivationPlan }
6
6
  from "@agents-can-communicate/adapter-sdk";
7
7
 
8
8
  import { resolveExecutable, shimDirFor } from "./native-activation.mjs";
@@ -34,6 +34,14 @@ const VERSION = /(?:^|[^0-9A-Za-z])v?(\d+\.\d+\.\d+(?:[-+][0-9A-Za-z.-]+)?)(?:\b
34
34
  // `env` decides where the command is looked up, so detection given an
35
35
  // environment finds the client on that environment's PATH.
36
36
  export const spawnProbe = async (command, args, { env } = {}) => {
37
+ // Windows finds `codex.cmd` or `claude.exe`, never the bare name, and starts a
38
+ // .cmd only through cmd.exe.
39
+ if (process.platform === "win32") {
40
+ const file = await resolveOnPath(command, { pathEnv: pathOf(env ?? process.env) });
41
+ if (file === null) throw Object.assign(new Error(`${command} is not on PATH`), { code: "ENOENT" });
42
+ const { stdout, stderr } = await runExecutable(file, args, env === undefined ? {} : { env });
43
+ return `${stdout}${stderr}`.trim();
44
+ }
37
45
  const { stdout, stderr } = await run(command, args, env === undefined ? {} : { env });
38
46
  return `${stdout}${stderr}`.trim();
39
47
  };
@@ -128,7 +136,7 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
128
136
  // own environment must not find a different client, or none.
129
137
  Promise.resolve(probe(adapter.client.command,
130
138
  adapter.client.versionArgs ?? ["--version"],
131
- { env: context?.env === undefined ? undefined : { ...process.env, ...context.env } })),
139
+ { env: context?.env === undefined ? undefined : mergeEnv(process.env, context.env) })),
132
140
  probeTimeoutMs, `${adapter.id} version probe`);
133
141
  if (typeof output === "string" && output !== "") {
134
142
  entry.present = true;
@@ -167,6 +175,8 @@ export async function detectInstallation({ adapters, context, probe = spawnProbe
167
175
  if (detected.inboundDelivery) entry.inboundDelivery = detected.inboundDelivery;
168
176
  // Whether the client asks before each ACC command, and what would stop it.
169
177
  if (detected.commandApproval) entry.commandApproval = detected.commandApproval;
178
+ // Whether the command that starts live delivery asks first.
179
+ if (detected.relayApproval) entry.relayApproval = detected.relayApproval;
170
180
  if (detected.nativeSetup) entry.nativeSetup = detected.nativeSetup;
171
181
  entry.diagnostics = [...(detected.diagnostics ?? [])];
172
182
  // What a person has to do, as opposed to what is true. Adapters that
@@ -0,0 +1,5 @@
1
+ // What a hook reads from the installation record, without the installer: the
2
+ // package index loads detection, planning and application, which a hook never
3
+ // runs, on every turn.
4
+ export { loadOwnership } from "./ownership.mjs";
5
+ export { readInstalledLivePolicyState } from "./live-policy.mjs";
@@ -1,7 +1,8 @@
1
1
  import { execFile } from "node:child_process";
2
- import { access, constants } from "node:fs/promises";
3
2
  import path from "node:path";
4
3
 
4
+ import { resolveExecutable as resolveOnPath } from "@agents-can-communicate/adapter-sdk";
5
+
5
6
  import { uninstallShellBootstrap } from "./shell-bootstrap.mjs";
6
7
  import { LIVE_POLICIES, livePolicyOf } from "./live-policy.mjs";
7
8
 
@@ -22,19 +23,10 @@ export const shimDirFor = stateRoot => path.join(stateRoot, "bin");
22
23
 
23
24
  // The vendor executable to probe: the first executable on PATH outside the
24
25
  // excluded directories, so a kept 0.7.x shim is never probed as the client.
25
- export async function resolveExecutable(command, { pathEnv = "", exclude = [] } = {}) {
26
- const excluded = exclude.filter(Boolean).map(directory => path.resolve(directory));
27
- for (const directory of String(pathEnv).split(path.delimiter).filter(Boolean)) {
28
- if (excluded.includes(path.resolve(directory))) continue;
29
- const candidate = path.join(directory, command);
30
- try {
31
- await access(candidate, constants.X_OK);
32
- return candidate;
33
- } catch {
34
- // not here; keep walking PATH
35
- }
36
- }
37
- return null;
26
+ // Windows adds an extension from PATHEXT; see the SDK's resolveExecutable.
27
+ export async function resolveExecutable(command, { pathEnv = "", exclude = [], pathExt } = {}) {
28
+ return resolveOnPath(command, { pathEnv, exclude,
29
+ ...(pathExt === undefined ? {} : { pathExt }) });
38
30
  }
39
31
 
40
32
  const defaultExec = (executable, args) => new Promise((resolve, reject) => {
@@ -78,6 +78,20 @@ export function planInstallation({ adapters, detected, context, action = "instal
78
78
  + `if it is, wire it with --adapter ${entry.adapterId}` });
79
79
  continue;
80
80
  }
81
+ // An uninstall takes back what this data home installed. A client that is
82
+ // here but that this home has no record of was wired by another ACC data
83
+ // home - the operator's own, beside an isolated test home - or before this
84
+ // home kept records, and removing it unasked took the operator's wiring
85
+ // with it (#253). Naming the client still removes it.
86
+ if (action === "uninstall" && entry.present && recordedById.get(entry.adapterId) === undefined
87
+ && !askedFor.has(entry.adapterId)) {
88
+ // Also what a second uninstall says: by then this home has no record left.
89
+ skipped.push({ adapterId: entry.adapterId,
90
+ reason: `${entry.displayName ?? entry.adapterId} has no install record in this ACC data home, `
91
+ + "so ACC leaves it as it is; to remove ACC wiring another data home put there, run "
92
+ + `acc uninstall --adapter ${entry.adapterId}` });
93
+ continue;
94
+ }
81
95
  // An adapter can refuse itself for a reason that is nobody's fault and that
82
96
  // no file on disk shows: Antigravity CLI cannot be wired until somebody
83
97
  // picks between a machine-wide and a per-workspace hook registration. It is
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/mcp-server",
3
- "version": "0.8.4",
3
+ "version": "0.9.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/protocol",
3
- "version": "0.8.4",
3
+ "version": "0.9.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -53,8 +53,12 @@ function assertRoot(root, source) {
53
53
  if (typeof root !== "string" || root === "") {
54
54
  data("each workspace root must be a non-empty string", { source, root });
55
55
  }
56
- // Absolute is one machine's layout committed to a shared repository.
57
- if (root.startsWith("/") || /^[A-Za-z]:[\\/]/.test(root)) {
56
+ // Absolute is one machine's layout committed to a shared repository. On
57
+ // Windows a leading separator is rooted on the current drive, `\\host\share`
58
+ // reaches another machine and `D:x` is relative to another drive's current
59
+ // directory, so a drive letter or a leading separator is refused on every
60
+ // platform: a config that is harmless on POSIX travels to Windows unchanged.
61
+ if (/^[\\/]/.test(root) || /^[A-Za-z]:/.test(root)) {
58
62
  data("a workspace root must be relative to the config", { source, root });
59
63
  }
60
64
  // Checked on the segments rather than the string: `packages/../../escape`
@@ -14,6 +14,10 @@
14
14
  */
15
15
  const GLOB = "/**";
16
16
 
17
+ // A Windows agent separates segments with a backslash. Only the path part of a
18
+ // `file:` resource is converted; see normaliseResource.
19
+ const slashes = value => value.replaceAll("\\", "/");
20
+
17
21
  function normalisePath(value) {
18
22
  const segments = [];
19
23
  for (const segment of value.split("/")) {
@@ -44,7 +48,7 @@ function normalisePath(value) {
44
48
  */
45
49
  export function assertMatchableResource(resource, fail) {
46
50
  if (typeof resource !== "string" || !resource.startsWith("file:")) return resource;
47
- const rest = resource.slice("file:".length);
51
+ const rest = slashes(resource.slice("file:".length));
48
52
  if (rest.endsWith("/") && rest !== "/") {
49
53
  fail(`${resource} names a directory; claim ${resource}** to cover what is in it`);
50
54
  }
@@ -61,7 +65,7 @@ export function normaliseResource(resource) {
61
65
  const colon = resource.indexOf(":");
62
66
  if (colon === -1 || resource.slice(0, colon) !== "file") return resource;
63
67
 
64
- const rest = resource.slice(colon + 1);
68
+ const rest = slashes(resource.slice(colon + 1));
65
69
  const glob = rest.endsWith(GLOB);
66
70
  const body = glob ? rest.slice(0, -GLOB.length) : rest;
67
71
  // An absolute path keeps its leading slash: it names a different thing from
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@agents-can-communicate/storage-filesystem",
3
- "version": "0.8.4",
3
+ "version": "0.9.0",
4
4
  "private": true,
5
5
  "type": "module",
6
6
  "exports": {
@@ -4,7 +4,7 @@ import path from "node:path";
4
4
 
5
5
  import { AccError, EXIT, assertPortableId } from "@agents-can-communicate/protocol";
6
6
 
7
- import { encode, publishAtomic } from "./atomic-json.mjs";
7
+ import { encode, nameCommittedLater, publishAtomic } from "./atomic-json.mjs";
8
8
  import { withRegularNoFollow } from "./safe-file.mjs";
9
9
 
10
10
  const ACTIVE_JOURNAL_VERSION = 2;
@@ -181,7 +181,10 @@ export async function initialiseActiveJournal(paths, options) {
181
181
  const current = await readCurrent(paths, options.root);
182
182
  if (current !== null) return current.record;
183
183
  try {
184
- await publishAtomic(activeJournalPath(paths, 0), encodeAuthority(genesis()), options);
184
+ // The first record any write publishes commits this name on Windows; a
185
+ // crash before it leaves no pointer, which the next open initialises.
186
+ await publishAtomic(activeJournalPath(paths, 0), encodeAuthority(genesis()),
187
+ { ...options, durability: nameCommittedLater(options.platform) });
185
188
  } catch (error) {
186
189
  if (error.code !== EXIT.CONFLICT) throw error;
187
190
  }