@adhdev/daemon-core 0.9.82-rc.32 → 0.9.82-rc.321
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/boot/daemon-lifecycle.d.ts +8 -0
- package/dist/boot/process-hardening.d.ts +50 -0
- package/dist/build-info.d.ts +37 -0
- package/dist/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +46 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +115 -64
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +38 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/cli-adapters/resolve-executable.d.ts +14 -0
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
- package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
- package/dist/cli-adapters/terminal-screen.d.ts +4 -11
- package/dist/commands/cli-manager.d.ts +2 -1
- package/dist/commands/handler.d.ts +110 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router.d.ts +229 -1
- package/dist/commands/upgrade-helper.d.ts +12 -0
- package/dist/config/chat-history.d.ts +9 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +72 -1
- package/dist/detection/ide-detector.d.ts +13 -0
- package/dist/detection/win32-ide-version.d.ts +37 -0
- package/dist/git/git-commands.d.ts +9 -1
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-executor.d.ts +11 -0
- package/dist/git/git-status.d.ts +9 -0
- package/dist/git/git-types.d.ts +2 -50
- package/dist/index.d.ts +51 -12
- package/dist/index.js +35378 -11873
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +38854 -15461
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/logging/log-redactor.d.ts +24 -0
- package/dist/logging/log-tail-reader.d.ts +46 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +36 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +108 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +169 -0
- package/dist/mesh/mesh-events-pending.d.ts +45 -0
- package/dist/mesh/mesh-events-stale.d.ts +40 -0
- package/dist/mesh/mesh-events-utils.d.ts +47 -0
- package/dist/mesh/mesh-events.d.ts +6 -49
- package/dist/mesh/mesh-fast-forward.d.ts +81 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-init.d.ts +86 -0
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
- package/dist/mesh/mesh-ledger.d.ts +77 -1
- package/dist/mesh/mesh-missions.d.ts +116 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
- package/dist/mesh/mesh-refine-batch.d.ts +68 -0
- package/dist/mesh/mesh-refine-status.d.ts +62 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-routing.d.ts +70 -0
- package/dist/mesh/mesh-runtime-store.d.ts +401 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
- package/dist/mesh/mesh-work-queue.d.ts +204 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +215 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
- package/dist/providers/acp-provider-instance.d.ts +2 -0
- package/dist/providers/approval-utils.d.ts +13 -0
- package/dist/providers/cli-provider-instance.d.ts +61 -3
- package/dist/providers/contracts.d.ts +130 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/dispatcher.d.ts +27 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +13 -0
- package/dist/providers/provider-instance-manager.d.ts +13 -0
- package/dist/providers/provider-instance.d.ts +13 -1
- package/dist/providers/provider-loader.d.ts +26 -4
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +69 -0
- package/dist/providers/spec/cli-adapter.d.ts +217 -0
- package/dist/providers/spec/evaluator.d.ts +23 -0
- package/dist/providers/spec/fsm-driver.d.ts +278 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
- package/dist/providers/spec/fsm-loader.d.ts +14 -0
- package/dist/providers/spec/fsm-types.d.ts +128 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +180 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +84 -0
- package/dist/providers/working-dir.d.ts +17 -0
- package/dist/repo-mesh-types.d.ts +277 -13
- package/dist/sessions/registry.d.ts +6 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +42 -1
- package/dist/status/normalize.d.ts +1 -1
- package/dist/status/normalize.js +1 -0
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +1 -0
- package/dist/status/normalize.mjs.map +1 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +7 -3
- package/src/agent-stream/poller.ts +2 -3
- package/src/boot/daemon-lifecycle.ts +55 -12
- package/src/boot/process-hardening.ts +89 -0
- package/src/build-info.ts +73 -0
- package/src/chat/source-machine.ts +534 -0
- package/src/chat/source-resolver.ts +0 -0
- package/src/chat/subscription-updates.ts +20 -1
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +47 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1215 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +710 -1140
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +13 -0
- package/src/cli-adapters/provider-cli-runtime.ts +56 -7
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +85 -11
- package/src/cli-adapters/pty-transport.ts +2 -1
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/resolve-executable.ts +90 -0
- package/src/cli-adapters/session-host-transport.ts +2 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
- package/src/cli-adapters/terminal-backends/types.ts +10 -3
- package/src/cli-adapters/terminal-screen.ts +16 -81
- package/src/commands/chat-commands.ts +1822 -61
- package/src/commands/cli-manager.ts +286 -14
- package/src/commands/handler.ts +809 -2
- package/src/commands/mesh-coordinator.ts +332 -122
- package/src/commands/router.ts +6369 -704
- package/src/commands/stream-commands.ts +8 -0
- package/src/commands/upgrade-helper.ts +81 -30
- package/src/config/chat-history.ts +483 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +320 -4
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-auto-implement.ts +3 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/cli-detector.ts +28 -9
- package/src/detection/ide-detector.ts +55 -16
- package/src/detection/win32-ide-version.ts +106 -0
- package/src/git/git-commands.ts +42 -12
- package/src/git/git-diff.ts +53 -0
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +408 -43
- package/src/git/git-types.ts +14 -62
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +145 -11
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/log-redactor.ts +100 -0
- package/src/logging/log-tail-reader.ts +220 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +234 -31
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +469 -0
- package/src/mesh/mesh-delivery-policy.ts +315 -0
- package/src/mesh/mesh-events-coordinator.ts +2102 -0
- package/src/mesh/mesh-events-pending.ts +508 -0
- package/src/mesh/mesh-events-stale.ts +290 -0
- package/src/mesh/mesh-events-utils.ts +254 -0
- package/src/mesh/mesh-events.ts +30 -1035
- package/src/mesh/mesh-fast-forward.ts +839 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-init.ts +260 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
- package/src/mesh/mesh-ledger.ts +589 -102
- package/src/mesh/mesh-missions.ts +248 -0
- package/src/mesh/mesh-reconcile-loop.ts +496 -0
- package/src/mesh/mesh-refine-batch.ts +197 -0
- package/src/mesh/mesh-refine-status.ts +231 -0
- package/src/mesh/mesh-review-inbox.ts +307 -0
- package/src/mesh/mesh-routing.ts +291 -0
- package/src/mesh/mesh-runtime-store.ts +1578 -0
- package/src/mesh/mesh-task-stats.ts +154 -0
- package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
- package/src/mesh/mesh-work-queue.ts +663 -142
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +403 -0
- package/src/mesh/worktree-bootstrap-config.ts +326 -0
- package/src/providers/acp-provider-instance.ts +19 -4
- package/src/providers/approval-utils.d.ts +4 -0
- package/src/providers/approval-utils.ts +47 -5
- package/src/providers/chat-message-normalization.ts +4 -11
- package/src/providers/cli-provider-instance.ts +958 -91
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +141 -6
- package/src/providers/extension-provider-instance.ts +5 -1
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +25 -6
- package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +479 -0
- package/src/providers/native-history/dispatcher.ts +340 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +30 -0
- package/src/providers/provider-instance.ts +10 -1
- package/src/providers/provider-loader.ts +662 -50
- package/src/providers/provider-schema.ts +87 -14
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +229 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +168 -0
- package/src/providers/spec/cli-adapter.ts +986 -0
- package/src/providers/spec/evaluator.ts +345 -0
- package/src/providers/spec/fsm-driver.ts +909 -0
- package/src/providers/spec/fsm-evaluator.ts +255 -0
- package/src/providers/spec/fsm-loader.ts +112 -0
- package/src/providers/spec/fsm-types.ts +222 -0
- package/src/providers/spec/native-history-executor.ts +1136 -0
- package/src/providers/spec/pre-launch-trust.ts +104 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/types.ts +226 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +536 -0
- package/src/providers/version-archive.ts +64 -24
- package/src/providers/working-dir.ts +23 -0
- package/src/repo-mesh-types.ts +356 -13
- package/src/sessions/registry.ts +6 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.ts +45 -1
- package/src/status/builders.ts +26 -6
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +15 -0
- package/src/status/snapshot.ts +84 -25
- package/src/system/host-memory.ts +29 -12
- package/src/types.ts +5 -0
- package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
- package/dist/mesh/mesh-sync.d.ts +0 -53
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
- package/src/mesh/mesh-sync.ts +0 -111
|
@@ -0,0 +1,496 @@
|
|
|
1
|
+
// ---------------------------------------------------------------------------
|
|
2
|
+
// mesh-reconcile-loop — periodic queue → live coordinator reconciliation
|
|
3
|
+
// ---------------------------------------------------------------------------
|
|
4
|
+
// Single-model replacement for the old event-based "spontaneous forward" paths
|
|
5
|
+
// (remote P2P mesh_forward_event dispatch + live-CLI PTY fire-and-forget inject).
|
|
6
|
+
// Those pushed events at the moment a worker transitioned state, and silently
|
|
7
|
+
// dropped on the network (P2P) or when the coordinator was generating.
|
|
8
|
+
//
|
|
9
|
+
// The reliable backbone has always been the pending-events queue (SQLite +
|
|
10
|
+
// JSONL): every mesh coordinator event is persisted there before anything else
|
|
11
|
+
// (see injectMeshSystemMessage). What was missing was an *active* drainer that
|
|
12
|
+
// runs on a schedule rather than only when the coordinator (an LLM) happens to
|
|
13
|
+
// call a mesh tool.
|
|
14
|
+
//
|
|
15
|
+
// This loop is that drainer. On a fixed interval it:
|
|
16
|
+
// 1. Finds live CLI coordinator sessions on THIS daemon (meshCoordinatorFor
|
|
17
|
+
// stamp). For each mesh, drains the local queue scoped to this daemon and
|
|
18
|
+
// injects pending events into the coordinator. When a coordinator is idle it
|
|
19
|
+
// receives every queued event. When ONLY generating coordinators exist (the
|
|
20
|
+
// common case while the coordinator is blocked awaiting a worker result), the
|
|
21
|
+
// loop force-drains ONLY the force-inject events (completion / approval / stop /
|
|
22
|
+
// refine·bootstrap terminal) and force-writes them into the generating PTY —
|
|
23
|
+
// the same busy-bypass send-guard escape the live-CLI inject used to use.
|
|
24
|
+
// Non-force progress events stay queued for the next idle tick (injecting them
|
|
25
|
+
// mid-generation would be noise). This is what makes a coordinator parked in
|
|
26
|
+
// `generating` while awaiting a worker's completion actually receive it.
|
|
27
|
+
// 2. In cloud mode (dispatchMeshCommand present), pulls each remote worker
|
|
28
|
+
// node daemon's queue over P2P (get_pending_mesh_events) and re-injects via
|
|
29
|
+
// handleMeshForwardEvent — the same pull the MCP drainCoordinatorPendingEvents
|
|
30
|
+
// already does, now driven by the daemon timer instead of an LLM tool call.
|
|
31
|
+
//
|
|
32
|
+
// IMPORTANT — limits of this loop:
|
|
33
|
+
// - It only delivers to *live CLI coordinator instances* on this daemon. A
|
|
34
|
+
// pure stdio MCP coordinator (an LLM with no live CLI session to inject
|
|
35
|
+
// into) has no inject target here; that case stays pull-driven — the LLM
|
|
36
|
+
// drains the queue when it calls mesh_status / mesh_read_chat. We do NOT try
|
|
37
|
+
// to "wake" an LLM from the daemon; that is structurally impossible over a
|
|
38
|
+
// stdio request/response transport. See docs/refactoring/2026-06-15-mesh-event-to-queue-polling.md §4.7.
|
|
39
|
+
// - Queue persistence (queuePendingMeshCoordinatorEvent) and the SQLite
|
|
40
|
+
// drained=1 idempotency are the trust backbone and are untouched by this loop.
|
|
41
|
+
// ---------------------------------------------------------------------------
|
|
42
|
+
|
|
43
|
+
import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
|
|
44
|
+
import type { LocalMeshEntry } from '../repo-mesh-types.js';
|
|
45
|
+
import { loadConfig } from '../config/config.js';
|
|
46
|
+
import { listMeshes } from '../config/mesh-config.js';
|
|
47
|
+
import { LOG } from '../logging/logger.js';
|
|
48
|
+
import { drainPendingMeshCoordinatorEvents } from './mesh-events-pending.js';
|
|
49
|
+
import type { PendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
50
|
+
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
51
|
+
import { handleMeshForwardEvent, shouldForceInjectMeshEvent, MESH_FORCE_INJECT_EVENTS, triggerMeshQueue } from './mesh-events-coordinator.js';
|
|
52
|
+
import {
|
|
53
|
+
peekUnresolvedDelegateForwards,
|
|
54
|
+
ackUnresolvedDelegateForward,
|
|
55
|
+
expireStaleUnresolvedDelegateForwards,
|
|
56
|
+
} from './mesh-unresolved-forward-outbox.js';
|
|
57
|
+
import { readNonEmptyString } from './mesh-events-utils.js';
|
|
58
|
+
|
|
59
|
+
// Default reconcile cadence. approval/completion notifications to a live CLI
|
|
60
|
+
// coordinator land within at most one interval. Overridable via env for tuning.
|
|
61
|
+
const DEFAULT_RECONCILE_INTERVAL_MS = 4_000;
|
|
62
|
+
|
|
63
|
+
function resolveReconcileIntervalMs(): number {
|
|
64
|
+
const raw = readNonEmptyString(process.env.MESH_RECONCILE_INTERVAL_MS);
|
|
65
|
+
if (raw) {
|
|
66
|
+
const parsed = Number.parseInt(raw, 10);
|
|
67
|
+
if (Number.isFinite(parsed) && parsed >= 1_000 && parsed <= 60_000) return parsed;
|
|
68
|
+
}
|
|
69
|
+
return DEFAULT_RECONCILE_INTERVAL_MS;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
interface LiveCoordinator {
|
|
73
|
+
meshId: string;
|
|
74
|
+
instance: ReturnType<DaemonComponents['instanceManager']['getInstance']>;
|
|
75
|
+
idle: boolean;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// The set of coordinator-daemon ids THIS daemon answers to when draining the
|
|
79
|
+
// pending-events queue. A unicast completion event is stamped with the worker's
|
|
80
|
+
// meshCoordinatorDaemonId, which can be either:
|
|
81
|
+
// - the daemon's canonical status id (`standalone_<machineId>` / `daemon_<machineId>`),
|
|
82
|
+
// stamped by the MCP layer via ctx.localDaemonId (= getStatus().status.instanceId), or
|
|
83
|
+
// - the bare machineId, stamped by the local queue-assignment path (loadConfig().machineId).
|
|
84
|
+
// Draining with only one of these silently misses events stamped with the other —
|
|
85
|
+
// the exact reason a generating coordinator never self-received local completions.
|
|
86
|
+
// We accept BOTH so the drain matches regardless of which path stamped the event.
|
|
87
|
+
function resolveCoordinatorDaemonIds(components: DaemonComponents): string[] {
|
|
88
|
+
const ids = new Set<string>();
|
|
89
|
+
const statusInstanceId = readNonEmptyString((components as { statusInstanceId?: string }).statusInstanceId);
|
|
90
|
+
if (statusInstanceId) ids.add(statusInstanceId);
|
|
91
|
+
const machineId = readNonEmptyString(loadConfig().machineId);
|
|
92
|
+
if (machineId) ids.add(machineId);
|
|
93
|
+
return [...ids];
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// Whether THIS daemon is the coordinator/host for a mesh — i.e. the daemon that
|
|
97
|
+
// owns coordinator ownership and must collect every worker node's completion
|
|
98
|
+
// events into its local queue. This is true regardless of whether a *live CLI*
|
|
99
|
+
// coordinator session currently exists: the coordinator is frequently a pure
|
|
100
|
+
// stdio MCP LLM (no live CLI session to inject into), and that LLM only sees the
|
|
101
|
+
// queue when it next calls a mesh tool. For it to see remote worker completions
|
|
102
|
+
// at all, the daemon must have already pulled them into the local queue on the
|
|
103
|
+
// timer — which is exactly what this predicate gates.
|
|
104
|
+
//
|
|
105
|
+
// Rule: this daemon hosts the mesh when meshHost.role is 'host' (the default for
|
|
106
|
+
// standalone-compat meshes with no host metadata) AND, when a hostDaemonId is
|
|
107
|
+
// pinned, it resolves to one of this daemon's ids. Member-only daemons return
|
|
108
|
+
// false — their own queue is pulled BY the host, not the other way around.
|
|
109
|
+
//
|
|
110
|
+
// `daemonIds` here is the EXPANDED self-identity set (runtime drain ids ∪ this
|
|
111
|
+
// daemon's mesh-config node id forms) — see resolveCoordinatorSelfIds. The
|
|
112
|
+
// pinned hostDaemonId is itself a config-form id and frequently does NOT equal a
|
|
113
|
+
// runtime id (bare machineId / status id), so gating on the runtime ids alone
|
|
114
|
+
// would wrongly classify the real host as a non-host and skip the remote pull
|
|
115
|
+
// entirely.
|
|
116
|
+
function daemonHostsMesh(mesh: LocalMeshEntry, daemonIds: string[]): boolean {
|
|
117
|
+
const host = mesh.meshHost;
|
|
118
|
+
// No metadata → default host (standalone compatibility, see createDefaultMeshHostMetadata).
|
|
119
|
+
if (!host) return true;
|
|
120
|
+
if (host.role && host.role !== 'host') return false;
|
|
121
|
+
const hostDaemonId = readNonEmptyString(host.hostDaemonId);
|
|
122
|
+
// Host role but no pinned hostDaemonId → treat as host (single-daemon / legacy).
|
|
123
|
+
if (!hostDaemonId) return true;
|
|
124
|
+
return daemonIds.includes(hostDaemonId);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Resolve EVERY id-form this daemon answers to FOR A GIVEN MESH: the runtime drain
|
|
128
|
+
// ids (status id + bare machineId) unioned with this daemon's mesh-config identity
|
|
129
|
+
// forms — the self node's daemonId/machineId (the node whose daemonId/machineId
|
|
130
|
+
// matches a runtime id) and the pinned meshHost.hostDaemonId WHEN it is provably
|
|
131
|
+
// ours. This is the single source of truth for "is this id me?" across both the
|
|
132
|
+
// host gate and the remote pull filter; the worker's meshCoordinatorDaemonId stamp
|
|
133
|
+
// is guaranteed to be one of these forms (it comes from resolveCoordinatorDaemonId,
|
|
134
|
+
// which prefers the coordinator node's config-form daemonId over the runtime status id).
|
|
135
|
+
function resolveCoordinatorSelfIds(mesh: LocalMeshEntry, drainDaemonIds: string[]): string[] {
|
|
136
|
+
const ids = new Set<string>(drainDaemonIds);
|
|
137
|
+
// Expand with the config-form id(s) of the self node — the mesh node whose
|
|
138
|
+
// daemonId/machineId matches a runtime id. Its config-form daemonId is exactly
|
|
139
|
+
// what resolveCoordinatorNode()→resolveCoordinatorDaemonId() stamps onto a worker.
|
|
140
|
+
for (const node of mesh.nodes) {
|
|
141
|
+
const nodeDaemonId = readNonEmptyString(node.daemonId);
|
|
142
|
+
const nodeMachineId = readNonEmptyString(node.machineId);
|
|
143
|
+
const isSelf = (nodeDaemonId && drainDaemonIds.includes(nodeDaemonId))
|
|
144
|
+
|| (nodeMachineId && drainDaemonIds.includes(nodeMachineId));
|
|
145
|
+
if (!isSelf) continue;
|
|
146
|
+
if (nodeDaemonId) ids.add(nodeDaemonId);
|
|
147
|
+
if (nodeMachineId) ids.add(nodeMachineId);
|
|
148
|
+
}
|
|
149
|
+
// The pinned host id is included ONLY when it is provably one of THIS daemon's ids
|
|
150
|
+
// (it already matches a runtime id or a resolved self-node id). A hostDaemonId that
|
|
151
|
+
// names a DIFFERENT daemon must NOT be claimed — that would make a member-only
|
|
152
|
+
// daemon believe it is the host and pull queues it does not own. Having a node on
|
|
153
|
+
// this daemon does not make this daemon the host; daemonHostsMesh still honours a
|
|
154
|
+
// foreign hostDaemonId and rejects ownership.
|
|
155
|
+
const hostDaemonId = readNonEmptyString(mesh.meshHost?.hostDaemonId);
|
|
156
|
+
if (hostDaemonId && ids.has(hostDaemonId)) ids.add(hostDaemonId);
|
|
157
|
+
return [...ids];
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Find live CLI coordinator instances on THIS daemon, keyed by mesh.
|
|
161
|
+
function findLiveCoordinators(components: DaemonComponents): LiveCoordinator[] {
|
|
162
|
+
const out: LiveCoordinator[] = [];
|
|
163
|
+
for (const inst of components.instanceManager.getByCategory('cli')) {
|
|
164
|
+
const state = inst.getState();
|
|
165
|
+
const settings = state.settings && typeof state.settings === 'object'
|
|
166
|
+
? state.settings as Record<string, unknown>
|
|
167
|
+
: {};
|
|
168
|
+
const meshId = readNonEmptyString(settings.meshCoordinatorFor);
|
|
169
|
+
if (!meshId) continue;
|
|
170
|
+
const status = readNonEmptyString(state.status).toLowerCase();
|
|
171
|
+
out.push({ meshId, instance: inst, idle: status === 'idle' });
|
|
172
|
+
}
|
|
173
|
+
return out;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Inject a drained pending event into a live coordinator session. Force-inject
|
|
177
|
+
// events carry force:true so they bypass the busy send-guard and land in the PTY
|
|
178
|
+
// even while the coordinator is generating (see shouldForceInjectMeshEvent).
|
|
179
|
+
function injectPendingIntoCoordinator(
|
|
180
|
+
coordinator: LiveCoordinator['instance'],
|
|
181
|
+
pending: PendingMeshCoordinatorEvent,
|
|
182
|
+
): void {
|
|
183
|
+
if (!coordinator || !pending.coordinatorMessage) return;
|
|
184
|
+
const force = shouldForceInjectMeshEvent(pending.event);
|
|
185
|
+
coordinator.onEvent('send_message', {
|
|
186
|
+
input: { text: pending.coordinatorMessage, textFallback: pending.coordinatorMessage },
|
|
187
|
+
...(force ? { force: true } : {}),
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// One reconcile tick. Two independent phases:
|
|
192
|
+
//
|
|
193
|
+
// PHASE 1 — Remote queue pull (the fix for remote worktree completions never
|
|
194
|
+
// reaching an MCP/LLM coordinator). For EVERY mesh this daemon hosts/
|
|
195
|
+
// coordinates, pull each remote worker node's pending-events queue over P2P
|
|
196
|
+
// into THIS daemon's local queue. This runs *regardless of whether a live CLI
|
|
197
|
+
// coordinator exists* — the coordinator is usually a pure stdio MCP LLM with
|
|
198
|
+
// no live CLI session, and it can only observe a remote worker's completion
|
|
199
|
+
// once that event has been pulled into the local queue (which it then drains
|
|
200
|
+
// on its next mesh tool call). Previously this pull was gated behind a live
|
|
201
|
+
// CLI coordinator and so never ran for MCP/LLM coordinators — remote
|
|
202
|
+
// completions sat on the remote node's queue until the LLM happened to call
|
|
203
|
+
// mesh_read_chat, which triggered the MCP-side pull. The daemon now does it
|
|
204
|
+
// autonomously on the timer. Standalone (no dispatchMeshCommand) skips this
|
|
205
|
+
// phase entirely — there are no remote nodes to pull from.
|
|
206
|
+
//
|
|
207
|
+
// PHASE 2 — Live CLI inject. For each mesh that has a live CLI coordinator on
|
|
208
|
+
// THIS daemon, drain the local queue and inject pending events into the PTY.
|
|
209
|
+
// Unchanged from before.
|
|
210
|
+
export async function runMeshReconcileTick(components: DaemonComponents): Promise<void> {
|
|
211
|
+
const localDaemonId = readNonEmptyString(loadConfig().machineId) || undefined;
|
|
212
|
+
// The id-set used to scope the local queue drain (status id + machineId). See
|
|
213
|
+
// resolveCoordinatorDaemonIds — the status id is what the MCP layer stamps and
|
|
214
|
+
// is mandatory here for a generating CLI coordinator to self-receive completions.
|
|
215
|
+
const drainDaemonIds = resolveCoordinatorDaemonIds(components);
|
|
216
|
+
const dispatchMeshCommand = components.dispatchMeshCommand;
|
|
217
|
+
const store = (() => {
|
|
218
|
+
try { return MeshRuntimeStore.getInstance(); } catch { return undefined; }
|
|
219
|
+
})();
|
|
220
|
+
|
|
221
|
+
// ── PHASE 0: retry the worker-side unresolved-delegate forward outbox ──────
|
|
222
|
+
// Cloud-only (needs dispatchMeshCommand). A worker that is NOT a member of the
|
|
223
|
+
// coordinator's mesh cannot be reached by the coordinator's PHASE 1 pull (it is
|
|
224
|
+
// in no mesh.node), so its completion must be PUSHED to the coordinator. This
|
|
225
|
+
// drains the durable outbox enqueued by forwardUnresolvedDelegateEvent and retries
|
|
226
|
+
// any push that has not yet been acked. See mesh-unresolved-forward-outbox.ts.
|
|
227
|
+
if (dispatchMeshCommand) {
|
|
228
|
+
try {
|
|
229
|
+
await retryUnresolvedDelegateForwards(components);
|
|
230
|
+
} catch (e: any) {
|
|
231
|
+
LOG.warn('MeshReconcile', `Unresolved-delegate forward retry failed: ${e?.message || e}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// ── PHASE 1: pull remote node queues for every mesh this daemon hosts ──────
|
|
236
|
+
// Cloud-only (dispatchMeshCommand present). Runs whether or not a live CLI
|
|
237
|
+
// coordinator exists — this is what lets an MCP/LLM coordinator ever see a
|
|
238
|
+
// remote worker's completion.
|
|
239
|
+
if (dispatchMeshCommand) {
|
|
240
|
+
for (const mesh of listMeshes()) {
|
|
241
|
+
// Expand to every id-form this daemon answers to for this mesh (runtime
|
|
242
|
+
// drain ids ∪ config-form node/host ids) and use it for BOTH the host gate
|
|
243
|
+
// and the remote pull filter, so a worker stamp in any form is recovered.
|
|
244
|
+
const selfIds = resolveCoordinatorSelfIds(mesh, drainDaemonIds);
|
|
245
|
+
if (!daemonHostsMesh(mesh, selfIds)) continue;
|
|
246
|
+
try {
|
|
247
|
+
await pullRemoteNodeQueues(components, mesh, localDaemonId, selfIds);
|
|
248
|
+
} catch (e: any) {
|
|
249
|
+
LOG.warn('MeshReconcile', `Remote node pull failed for mesh ${mesh.id}: ${e?.message || e}`);
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// ── PHASE 3: recover pending queue claims for newly-idle sessions ──────────
|
|
255
|
+
// The event-driven claim paths (agent:ready / agent:generating_completed in
|
|
256
|
+
// mesh-events-coordinator) re-claim the queue the moment a session goes idle,
|
|
257
|
+
// but that depends on a single event being emitted AND (for a remote node)
|
|
258
|
+
// successfully forwarded to this coordinator. If that event is missed/dropped,
|
|
259
|
+
// a pending task targeting a now-idle session would sit unclaimed forever —
|
|
260
|
+
// there was no periodic safety net. This phase is that net: for every mesh this
|
|
261
|
+
// daemon hosts that has at least one pending task, run one triggerMeshQueue so a
|
|
262
|
+
// session that became idle without a delivered ready-event still gets its work.
|
|
263
|
+
//
|
|
264
|
+
// O(1) guard: skip the (relatively expensive) full idle-session + remote-idle
|
|
265
|
+
// scan entirely when the queue has no pending tasks — a COUNT(*) over the
|
|
266
|
+
// indexed status column, so an idle mesh costs one cheap query per tick.
|
|
267
|
+
// claimNextQueueTask is atomic, so racing the event-driven path can only have
|
|
268
|
+
// one winner; double-claiming is impossible.
|
|
269
|
+
for (const mesh of listMeshes()) {
|
|
270
|
+
const selfIds = resolveCoordinatorSelfIds(mesh, drainDaemonIds);
|
|
271
|
+
if (!daemonHostsMesh(mesh, selfIds)) continue;
|
|
272
|
+
if (store) {
|
|
273
|
+
try {
|
|
274
|
+
if (store.pendingQueueTaskCount(mesh.id) === 0) continue;
|
|
275
|
+
} catch { /* fall through and let triggerMeshQueue decide */ }
|
|
276
|
+
}
|
|
277
|
+
try {
|
|
278
|
+
await triggerMeshQueue(components, mesh.id);
|
|
279
|
+
} catch (e: any) {
|
|
280
|
+
LOG.warn('MeshReconcile', `Pending-claim recovery trigger failed for mesh ${mesh.id}: ${e?.message || e}`);
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
// ── PHASE 2: inject into live CLI coordinators on this daemon ──────────────
|
|
285
|
+
const coordinators = findLiveCoordinators(components);
|
|
286
|
+
if (coordinators.length === 0) {
|
|
287
|
+
// No live CLI coordinator on this daemon — nothing to inject into.
|
|
288
|
+
// (MCP-only LLM coordinators drain the local queue via their own tool
|
|
289
|
+
// calls; PHASE 1 above has already populated it from remote nodes.)
|
|
290
|
+
return;
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// Group coordinators by mesh; multiple coordinator instances for one mesh is
|
|
294
|
+
// unusual but supported (each gets the same drained events).
|
|
295
|
+
const byMesh = new Map<string, LiveCoordinator[]>();
|
|
296
|
+
for (const c of coordinators) {
|
|
297
|
+
const list = byMesh.get(c.meshId);
|
|
298
|
+
if (list) list.push(c);
|
|
299
|
+
else byMesh.set(c.meshId, [c]);
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
for (const [meshId, meshCoordinators] of byMesh) {
|
|
303
|
+
// Drain the local queue scoped to this coordinator daemon and inject.
|
|
304
|
+
// - If an idle coordinator exists, FULL-drain and deliver every event to it
|
|
305
|
+
// (it can receive non-force progress events without deadlocking).
|
|
306
|
+
// - If only GENERATING coordinators exist, force-drain ONLY the force-inject
|
|
307
|
+
// events (completion/approval/stop/refine·bootstrap terminal) and force-inject
|
|
308
|
+
// them so a coordinator parked in `generating` while awaiting that very event
|
|
309
|
+
// is not deadlocked. Non-force progress events stay queued for the next idle
|
|
310
|
+
// tick — injecting them would be noise mid-generation. Both drains mark the
|
|
311
|
+
// consumed rows drained=1 atomically, so the pull path can't re-deliver.
|
|
312
|
+
const idleCoordinators = meshCoordinators.filter(c => c.idle);
|
|
313
|
+
const generatingCoordinators = meshCoordinators.filter(c => !c.idle);
|
|
314
|
+
const targetCoordinators = idleCoordinators.length > 0 ? idleCoordinators : generatingCoordinators;
|
|
315
|
+
const forceOnly = idleCoordinators.length === 0;
|
|
316
|
+
|
|
317
|
+
// O(1) guard: skip the drain entirely when the queue is empty.
|
|
318
|
+
if (store) {
|
|
319
|
+
try {
|
|
320
|
+
if (store.pendingEventCount(meshId) === 0) continue;
|
|
321
|
+
} catch { /* fall through to drain */ }
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
let pendingEvents: PendingMeshCoordinatorEvent[] = [];
|
|
325
|
+
try {
|
|
326
|
+
pendingEvents = drainPendingMeshCoordinatorEvents(
|
|
327
|
+
meshId,
|
|
328
|
+
drainDaemonIds.length > 0 ? drainDaemonIds : localDaemonId,
|
|
329
|
+
forceOnly ? { onlyEvents: MESH_FORCE_INJECT_EVENTS } : undefined,
|
|
330
|
+
);
|
|
331
|
+
} catch (e: any) {
|
|
332
|
+
LOG.warn('MeshReconcile', `Drain failed for mesh ${meshId}: ${e?.message || e}`);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
if (pendingEvents.length === 0) continue;
|
|
336
|
+
|
|
337
|
+
const mode = forceOnly ? 'force-drain → generating' : 'inject → idle';
|
|
338
|
+
LOG.info('MeshReconcile', `Reconcile ${mode}: ${pendingEvents.length} pending event(s) → ${targetCoordinators.length} coordinator(s) for mesh ${meshId}`);
|
|
339
|
+
for (const pending of pendingEvents) {
|
|
340
|
+
for (const c of targetCoordinators) {
|
|
341
|
+
injectPendingIntoCoordinator(c.instance, pending);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Cloud-only: retry the worker-side unresolved-delegate forward outbox. For each
|
|
348
|
+
// durably-queued entry, push it to its coordinator daemon over P2P (mesh_forward_event)
|
|
349
|
+
// and ack (mark drained) ONLY on a successful, non-rejected response. A failed or
|
|
350
|
+
// rejected push leaves the entry queued for the next tick — at-least-once delivery.
|
|
351
|
+
// Stale entries (coordinator unreachable past the max age) are expired first so the
|
|
352
|
+
// outbox can't grow without bound. The coordinator dedups duplicate deliveries on its
|
|
353
|
+
// own fingerprint, so a retry that races the original immediate push is harmless.
|
|
354
|
+
async function retryUnresolvedDelegateForwards(components: DaemonComponents): Promise<void> {
|
|
355
|
+
const dispatchMeshCommand = components.dispatchMeshCommand;
|
|
356
|
+
if (!dispatchMeshCommand) return;
|
|
357
|
+
|
|
358
|
+
// Drop entries that have exhausted their retry budget (fail-loud inside).
|
|
359
|
+
expireStaleUnresolvedDelegateForwards();
|
|
360
|
+
|
|
361
|
+
const entries = peekUnresolvedDelegateForwards();
|
|
362
|
+
if (entries.length === 0) return;
|
|
363
|
+
|
|
364
|
+
for (const entry of entries) {
|
|
365
|
+
let result: any;
|
|
366
|
+
try {
|
|
367
|
+
result = await dispatchMeshCommand(entry.coordinatorDaemonId, 'mesh_forward_event', entry.payload);
|
|
368
|
+
} catch (e: any) {
|
|
369
|
+
// Coordinator unreachable — keep the entry queued and try again next tick.
|
|
370
|
+
LOG.warn('MeshReconcile', `Retry forward to coordinator ${entry.coordinatorDaemonId} failed: ${e?.message || e} — left queued`);
|
|
371
|
+
continue;
|
|
372
|
+
}
|
|
373
|
+
if (result && result.success === false) {
|
|
374
|
+
LOG.warn('MeshReconcile', `Retry forward to coordinator ${entry.coordinatorDaemonId} rejected (${readNonEmptyString(result.error) || 'no reason'}) — left queued`);
|
|
375
|
+
continue;
|
|
376
|
+
}
|
|
377
|
+
// Acked — mark the durable copy delivered.
|
|
378
|
+
ackUnresolvedDelegateForward(entry.id);
|
|
379
|
+
LOG.info('MeshReconcile', `Retried+delivered unresolved-delegate ${readNonEmptyString(entry.payload.event)} to coordinator ${entry.coordinatorDaemonId}`);
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
// Cloud-only: poll each remote worker node daemon for pending coordinator events
|
|
384
|
+
// and re-inject them locally via handleMeshForwardEvent (which re-queues +
|
|
385
|
+
// surfaces to the live coordinator on the next tick / immediately if idle).
|
|
386
|
+
//
|
|
387
|
+
// Scoping: the remote handler (get_pending_mesh_events) drains its queue filtered
|
|
388
|
+
// by coordinatorDaemonId — returning events targeted at that id OR unscoped, and
|
|
389
|
+
// leaving events targeted at a *different* coordinator. A remote worker stamps the
|
|
390
|
+
// coordinator id in one of SEVERAL forms (the canonical status id `standalone_`/
|
|
391
|
+
// `daemon_<machineId>` stamped by the MCP layer, the bare machineId stamped by the
|
|
392
|
+
// local queue path, OR — most commonly for remote launches — the coordinator mesh
|
|
393
|
+
// node's config-form `daemonId`, which resolveCoordinatorDaemonId prefers and which
|
|
394
|
+
// is NOT canonicalised). `candidateDaemonIds` is the already-expanded self-identity
|
|
395
|
+
// set (resolveCoordinatorSelfIds: runtime drain ids ∪ this daemon's mesh-config node/
|
|
396
|
+
// host id forms), so we pull ONCE PER candidate id and a completion stamped with any
|
|
397
|
+
// of them is recovered. The remote drain is atomic (drained=1), so issuing multiple
|
|
398
|
+
// pulls cannot double-deliver — the first pull that matches consumes the event; the
|
|
399
|
+
// rest see nothing. When no ids resolve we fall back to a single unscoped pull.
|
|
400
|
+
async function pullRemoteNodeQueues(
|
|
401
|
+
components: DaemonComponents,
|
|
402
|
+
mesh: LocalMeshEntry,
|
|
403
|
+
localDaemonId: string | undefined,
|
|
404
|
+
candidateDaemonIds: string[],
|
|
405
|
+
): Promise<void> {
|
|
406
|
+
const dispatchMeshCommand = components.dispatchMeshCommand;
|
|
407
|
+
if (!dispatchMeshCommand) return;
|
|
408
|
+
const meshId = mesh.id;
|
|
409
|
+
|
|
410
|
+
// One args object per candidate coordinator-id form, or a single unscoped pull
|
|
411
|
+
// when none resolve.
|
|
412
|
+
const pulls: Array<Record<string, unknown>> = candidateDaemonIds.length > 0
|
|
413
|
+
? candidateDaemonIds.map(id => ({ meshId, coordinatorDaemonId: id }))
|
|
414
|
+
: [{ meshId }];
|
|
415
|
+
|
|
416
|
+
for (const node of mesh.nodes) {
|
|
417
|
+
const nodeDaemonId = readNonEmptyString(node.daemonId);
|
|
418
|
+
// Skip nodes without a daemon, and nodes on THIS daemon (their events are
|
|
419
|
+
// already in the local queue drained in PHASE 2). "This daemon" is matched
|
|
420
|
+
// against the full self-identity set (candidateDaemonIds), not just the bare
|
|
421
|
+
// localDaemonId — a self node can be registered under the config-form daemonId
|
|
422
|
+
// (`daemon_<machineId>`) which would NOT equal bare localDaemonId, and pulling
|
|
423
|
+
// from ourselves over P2P is both wasteful and a self-dispatch hazard.
|
|
424
|
+
if (!nodeDaemonId) continue;
|
|
425
|
+
if (localDaemonId && nodeDaemonId === localDaemonId) continue;
|
|
426
|
+
if (candidateDaemonIds.includes(nodeDaemonId)) continue;
|
|
427
|
+
|
|
428
|
+
for (const pendingEventArgs of pulls) {
|
|
429
|
+
let events: unknown;
|
|
430
|
+
try {
|
|
431
|
+
events = await dispatchMeshCommand(nodeDaemonId, 'get_pending_mesh_events', pendingEventArgs);
|
|
432
|
+
} catch {
|
|
433
|
+
// Remote pull is best-effort; the node may be offline. Retry next tick.
|
|
434
|
+
break; // node unreachable — don't bother with the other id form this tick.
|
|
435
|
+
}
|
|
436
|
+
const list = extractPendingEvents(events).filter(e => readNonEmptyString(e?.meshId) === meshId);
|
|
437
|
+
for (const event of list) {
|
|
438
|
+
const payload = buildForwardPayloadFromPending(event);
|
|
439
|
+
if (!payload.event || !payload.meshId) continue;
|
|
440
|
+
try {
|
|
441
|
+
handleMeshForwardEvent(components, payload);
|
|
442
|
+
} catch { /* best-effort re-inject */ }
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
function extractPendingEvents(raw: unknown): any[] {
|
|
449
|
+
if (Array.isArray(raw)) return raw;
|
|
450
|
+
if (raw && typeof raw === 'object') {
|
|
451
|
+
const events = (raw as Record<string, unknown>).events;
|
|
452
|
+
if (Array.isArray(events)) return events;
|
|
453
|
+
}
|
|
454
|
+
return [];
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
// Flatten a queued PendingMeshCoordinatorEvent into the flat payload shape
|
|
458
|
+
// handleMeshForwardEvent expects (mirrors the MCP buildMeshForwardPayloadFromPendingEvent).
|
|
459
|
+
function buildForwardPayloadFromPending(event: any): Record<string, unknown> {
|
|
460
|
+
const metadata = event?.metadataEvent && typeof event.metadataEvent === 'object'
|
|
461
|
+
? event.metadataEvent as Record<string, unknown>
|
|
462
|
+
: {};
|
|
463
|
+
return {
|
|
464
|
+
event: readNonEmptyString(event?.event),
|
|
465
|
+
meshId: readNonEmptyString(event?.meshId),
|
|
466
|
+
nodeId: readNonEmptyString(event?.nodeId) || readNonEmptyString(metadata.meshNodeId),
|
|
467
|
+
workspace: readNonEmptyString(event?.workspace) || readNonEmptyString(metadata.workspace),
|
|
468
|
+
...metadata,
|
|
469
|
+
};
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
interface ReconcileLoopHandle {
|
|
473
|
+
stop(): void;
|
|
474
|
+
}
|
|
475
|
+
|
|
476
|
+
// Start the periodic reconcile loop. Returns a handle with stop() for shutdown.
|
|
477
|
+
export function setupMeshReconcileLoop(components: DaemonComponents): ReconcileLoopHandle {
|
|
478
|
+
const intervalMs = resolveReconcileIntervalMs();
|
|
479
|
+
let running = false;
|
|
480
|
+
const timer = setInterval(() => {
|
|
481
|
+
if (running) return; // never overlap ticks
|
|
482
|
+
running = true;
|
|
483
|
+
void runMeshReconcileTick(components)
|
|
484
|
+
.catch((e: any) => LOG.warn('MeshReconcile', `Reconcile tick error: ${e?.message || e}`))
|
|
485
|
+
.finally(() => { running = false; });
|
|
486
|
+
}, intervalMs);
|
|
487
|
+
// Don't keep the process alive solely for this timer.
|
|
488
|
+
if (typeof timer.unref === 'function') timer.unref();
|
|
489
|
+
LOG.info('MeshReconcile', `Mesh reconcile loop started (interval ${intervalMs}ms)`);
|
|
490
|
+
return {
|
|
491
|
+
stop() {
|
|
492
|
+
clearInterval(timer);
|
|
493
|
+
LOG.info('MeshReconcile', 'Mesh reconcile loop stopped');
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
}
|