@adhdev/daemon-core 0.9.82-rc.390 → 0.9.82-rc.392

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.390",
3
+ "version": "0.9.82-rc.392",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -46,7 +46,7 @@
46
46
  "author": "vilmire",
47
47
  "license": "AGPL-3.0-or-later",
48
48
  "dependencies": {
49
- "@adhdev/mesh-shared": "0.9.82-rc.390",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.392",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -927,21 +927,49 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
927
927
  }
928
928
 
929
929
  if (sessionId && nodeId && providerType) {
930
+ // WORKTREE-BOOTSTRAP-DISPATCH-RACE: a freshly cloned worktree session emits
931
+ // agent:ready as soon as the CLI process reaches its idle prompt — which happens
932
+ // BEFORE the worktree bootstrap (npm install + native-addon repair: node-datachannel,
933
+ // better-sqlite3, ghostty-vt-node) has finished. Claiming a queued task on that early
934
+ // ready dispatches work into a session whose runtime is half-built: the child daemon
935
+ // dies loading the absent native addon → the worker session boots empty (no task ever
936
+ // injected), the queue row reports assigned-but-dead, and the work silently leaks to /
937
+ // gets re-routed onto the base node. Live evidence (the OPSRULES dispatch): node
938
+ // node_6df455dd emitted agent:ready at 10:36:28 but worktree_bootstrap_complete only at
939
+ // 10:36:56 — a 28s window in which a claim produced an empty session.
940
+ //
941
+ // Gate the claim on bootstrap state: if this node is a worktree whose bootstrap is still
942
+ // 'running', register the idle session (so it stays a claim candidate) but DEFER the
943
+ // claim. The claim re-fires on the next agent:ready / reconcile drain tick once bootstrap
944
+ // reaches a terminal state. 'failed' is left to flow through unchanged — a failed
945
+ // bootstrap surfaces its own coordinator event and a dispatch there fails loudly rather
946
+ // than silently, which is the correct, visible behavior (deferring forever would hide it).
947
+ let worktreeBootstrapPending = false;
948
+ try {
949
+ const mesh = getMeshWithCache(components, args.meshId);
950
+ const node = mesh?.nodes?.find((n: any) => meshNodeIdMatches(n, nodeId)) as { worktreeBootstrap?: { status?: string } } | undefined;
951
+ worktreeBootstrapPending = node?.worktreeBootstrap?.status === 'running';
952
+ } catch { /* best-effort: unknown bootstrap state → do not defer (prior behavior) */ }
953
+
930
954
  sweepExpiredRemoteIdleSessions();
931
955
  try {
932
956
  MeshRuntimeStore.getInstance().setRemoteIdleSession(args.meshId, nodeId, sessionId, providerType, Date.now() + REMOTE_IDLE_SESSION_TTL_MS);
933
957
  } catch { /* best-effort */ }
934
- setImmediate(() => {
935
- maybeAutoFastForwardIdleNode(components, { meshId: args.meshId, nodeId, sessionId, providerType })
936
- .finally(() => {
937
- try {
938
- const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
939
- if (assigned) MeshRuntimeStore.getInstance().deleteRemoteIdleSession(args.meshId, nodeId, sessionId);
940
- } catch (e: any) {
941
- LOG.warn('MeshQueue', `Failed to assign idle queue task after maintenance for ${nodeId}: ${e?.message || e}`);
942
- }
943
- });
944
- });
958
+ if (worktreeBootstrapPending) {
959
+ LOG.info('MeshQueue', `Deferring queue claim for worktree node ${nodeId} (${sessionId}): worktree bootstrap still running — early agent:ready precedes bootstrap completion; the idle session is registered and the claim will re-fire once bootstrap finishes (guards against dispatching into a half-built worktree → empty session / work leaking to the base node)`);
960
+ } else {
961
+ setImmediate(() => {
962
+ maybeAutoFastForwardIdleNode(components, { meshId: args.meshId, nodeId, sessionId, providerType })
963
+ .finally(() => {
964
+ try {
965
+ const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
966
+ if (assigned) MeshRuntimeStore.getInstance().deleteRemoteIdleSession(args.meshId, nodeId, sessionId);
967
+ } catch (e: any) {
968
+ LOG.warn('MeshQueue', `Failed to assign idle queue task after maintenance for ${nodeId}: ${e?.message || e}`);
969
+ }
970
+ });
971
+ });
972
+ }
945
973
  }
946
974
  } else if (args.event === 'agent:generating_started') {
947
975
  const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);