@adhdev/daemon-core 0.9.82-rc.360 → 0.9.82-rc.361

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.
@@ -28,6 +28,8 @@
28
28
  * types and leaves runtime behaviour unchanged.
29
29
  */
30
30
 
31
+ import { daemonIdsEquivalent, machineCoreFromDaemonId } from '@adhdev/mesh-shared';
32
+
31
33
  /** Provider type identifier (e.g. 'claude-cli', 'codex-cli', 'roo-code').
32
34
  * Free-form string; no shared enum exists in daemon-core yet. */
33
35
  type ProviderType = string;
@@ -76,15 +78,22 @@ export interface CoordinatorIdentity {
76
78
  }
77
79
 
78
80
  export function coordinatorIdentityEquals(a: CoordinatorIdentity, b: CoordinatorIdentity): boolean {
79
- return a.daemonId === b.daemonId
81
+ // daemonId compares by machine core: the same daemon answers to three
82
+ // interchangeable id forms (mach_X / daemon_mach_X / standalone_mach_X), so a
83
+ // raw `===` would treat one coordinator addressed under two forms as distinct.
84
+ return daemonIdsEquivalent(a.daemonId, b.daemonId)
80
85
  && a.coordinatorRunId === b.coordinatorRunId
81
86
  && (a.sessionId ?? '') === (b.sessionId ?? '');
82
87
  }
83
88
 
84
89
  export function coordinatorIdentityKey(identity: CoordinatorIdentity): string {
85
90
  // Stable string form for map keys and ledger payloads. Avoids the
86
- // {a:b, c:d} JSON.stringify ordering trap by enforcing field order.
87
- return `${identity.daemonId}|${identity.coordinatorRunId}|${identity.sessionId ?? ''}`;
91
+ // {a:b, c:d} JSON.stringify ordering trap by enforcing field order. The
92
+ // daemonId is normalized to its machine core so the key stays consistent with
93
+ // coordinatorIdentityEquals — two equal identities addressed under different
94
+ // daemon-id forms must produce the SAME key (else equal-but-distinct-key bug).
95
+ const daemonCore = machineCoreFromDaemonId(identity.daemonId) ?? identity.daemonId;
96
+ return `${daemonCore}|${identity.coordinatorRunId}|${identity.sessionId ?? ''}`;
88
97
  }
89
98
 
90
99
  // ─── Session handle ──────────────────────────────────────────────────────
@@ -2444,7 +2444,7 @@ function ackUnresolvedDelegateForwardByFingerprint(
2444
2444
  payload: Record<string, unknown>,
2445
2445
  ): void {
2446
2446
  const match = peekUnresolvedDelegateForwards().find(entry =>
2447
- entry.coordinatorDaemonId === coordinatorDaemonId
2447
+ daemonIdsEquivalent(entry.coordinatorDaemonId, coordinatorDaemonId)
2448
2448
  && readNonEmptyString(entry.payload.event) === eventName
2449
2449
  && readNonEmptyString(entry.payload.targetSessionId || entry.payload.sessionId || entry.payload.instanceId)
2450
2450
  === readNonEmptyString(payload.targetSessionId || payload.sessionId || payload.instanceId)
@@ -10,26 +10,6 @@ export function readRecord(value: unknown): Record<string, unknown> | undefined
10
10
  : undefined;
11
11
  }
12
12
 
13
- // A single daemon is identified three ways across the system: the raw machineId
14
- // (`mach_X`, what loadConfig().machineId returns and what the core forwarder uses as
15
- // localDaemonId), the cloud daemon id (`daemon_mach_X`), and the standalone daemon id
16
- // (`standalone_mach_X`, the runtime instanceId the MCP coordinator reports as
17
- // ctx.localDaemonId). Worker envelope stamps (meshCoordinatorDaemonId) and coordinator
18
- // drains use the prefixed forms, while the forwarder compares against the raw id — so a
19
- // naive `===` treats the same daemon as remote and breaks both coordinator matching and
20
- // the R3 direct-delivered dedup. Canonicalize to the raw id before comparing.
21
- export function canonicalDaemonId(value: unknown): string {
22
- const id = readNonEmptyString(value);
23
- if (!id) return '';
24
- return id.replace(/^(?:daemon|standalone)_/, '');
25
- }
26
-
27
- export function sameDaemonId(a: unknown, b: unknown): boolean {
28
- const ca = canonicalDaemonId(a);
29
- const cb = canonicalDaemonId(b);
30
- return ca !== '' && ca === cb;
31
- }
32
-
33
13
  /**
34
14
  * The relay-safety metadata a worker session must carry so that its completion /
35
15
  * generating events route back to the coordinator proactively (without waiting for
@@ -910,7 +910,13 @@ export class CliProviderInstance implements ProviderInstance {
910
910
  this.settings = {
911
911
  ...this.settings,
912
912
  meshNodeFor: assignment.meshId,
913
- ...(assignment.nodeId ? { meshNodeId: assignment.nodeId } : {}),
913
+ // WTCLAIM (A): track the bound node id under BOTH the active marker
914
+ // (meshNodeId, cleared on detach) and a sticky marker (meshLastNodeId,
915
+ // preserved across detach). The sticky marker lets a detached but still
916
+ // coordinator-owned session be re-picked ONLY for the SAME node it served
917
+ // — never auto-adopted for a sibling node (e.g. a cloned worktree) that
918
+ // shares this daemon. See isMeshOwnedDelegateSession's post-detach gate.
919
+ ...(assignment.nodeId ? { meshNodeId: assignment.nodeId, meshLastNodeId: assignment.nodeId } : {}),
914
920
  ...(assignment.taskId ? { meshActiveTaskId: assignment.taskId } : {}),
915
921
  ...(assignment.coordinatorDaemonId ? { meshCoordinatorDaemonId: assignment.coordinatorDaemonId } : {}),
916
922
  // Session-level routing anchor: the originating coordinator session, so this
@@ -929,8 +935,15 @@ export class CliProviderInstance implements ProviderInstance {
929
935
  detachMeshAssignment(): void {
930
936
  if (!this.settings.meshNodeFor && !this.settings.meshActiveTaskId && !this.settings.meshNodeId) return;
931
937
  const { meshNodeFor, meshNodeId, meshActiveTaskId, ...rest } = this.settings;
932
- void meshNodeFor; void meshNodeId; void meshActiveTaskId;
933
- this.settings = rest;
938
+ void meshNodeFor; void meshActiveTaskId;
939
+ // WTCLAIM (A): clear the active binding but PRESERVE the last bound node id
940
+ // (meshLastNodeId) so a later sessionless dispatch can re-adopt this idle
941
+ // session ONLY for the node it last served. Carry the id being cleared, or
942
+ // keep an already-present sticky marker if meshNodeId was absent.
943
+ const lastNodeId = (typeof meshNodeId === 'string' && meshNodeId.trim())
944
+ ? meshNodeId.trim()
945
+ : (typeof rest.meshLastNodeId === 'string' && rest.meshLastNodeId.trim() ? rest.meshLastNodeId.trim() : undefined);
946
+ this.settings = lastNodeId ? { ...rest, meshLastNodeId: lastNodeId } : rest;
934
947
  this.adapter.updateRuntimeSettings?.(this.settings);
935
948
  }
936
949