@adhdev/daemon-core 0.9.82-rc.26 → 0.9.82-rc.261
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 +4 -0
- package/dist/boot/process-hardening.d.ts +50 -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 +23 -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 +106 -63
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -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/terminal-backends/ghostty-vt-backend.d.ts +3 -3
- package/dist/cli-adapters/terminal-backends/types.d.ts +1 -2
- package/dist/cli-adapters/terminal-screen.d.ts +2 -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 +44 -1
- 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/git/git-commands.d.ts +9 -1
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-types.d.ts +2 -0
- package/dist/index.d.ts +47 -11
- package/dist/index.js +31114 -11255
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +31615 -11852
- 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/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 +90 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +152 -0
- package/dist/mesh/mesh-events-pending.d.ts +40 -0
- package/dist/mesh/mesh-events-stale.d.ts +40 -0
- package/dist/mesh/mesh-events-utils.d.ts +16 -0
- package/dist/mesh/mesh-events.d.ts +5 -49
- package/dist/mesh/mesh-fast-forward.d.ts +41 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -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 +58 -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 +327 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-work-queue.d.ts +160 -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 +37 -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 +52 -0
- package/dist/providers/spec/cli-adapter.d.ts +140 -0
- package/dist/providers/spec/evaluator.d.ts +23 -0
- package/dist/providers/spec/fsm-driver.d.ts +273 -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 +91 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +129 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +48 -0
- package/dist/repo-mesh-types.d.ts +126 -1
- 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 +6 -3
- package/src/agent-stream/poller.ts +2 -3
- package/src/boot/daemon-lifecycle.ts +30 -10
- package/src/boot/process-hardening.ts +89 -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 +22 -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 +674 -1139
- 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 +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +68 -11
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +35 -29
- package/src/cli-adapters/terminal-backends/types.ts +1 -3
- package/src/cli-adapters/terminal-screen.ts +11 -81
- package/src/commands/chat-commands.ts +1806 -60
- 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 +4890 -489
- package/src/config/chat-history.ts +95 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +303 -3
- 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/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +42 -12
- package/src/git/git-diff.ts +53 -0
- package/src/git/git-status.ts +35 -6
- package/src/git/git-types.ts +2 -0
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +127 -10
- 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/logger.ts +2 -1
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +219 -31
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +437 -0
- package/src/mesh/mesh-delivery-policy.ts +315 -0
- package/src/mesh/mesh-events-coordinator.ts +1625 -0
- package/src/mesh/mesh-events-pending.ts +439 -0
- package/src/mesh/mesh-events-stale.ts +283 -0
- package/src/mesh/mesh-events-utils.ts +195 -0
- package/src/mesh/mesh-events.ts +26 -1035
- package/src/mesh/mesh-fast-forward.ts +438 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
- package/src/mesh/mesh-ledger.ts +588 -102
- package/src/mesh/mesh-missions.ts +151 -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 +272 -0
- package/src/mesh/mesh-runtime-store.ts +1342 -0
- package/src/mesh/mesh-task-stats.ts +154 -0
- package/src/mesh/mesh-work-queue.ts +573 -145
- 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 +15 -1
- 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 +806 -72
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +141 -6
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +19 -5
- 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 +584 -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 +145 -0
- package/src/providers/spec/cli-adapter.ts +730 -0
- package/src/providers/spec/evaluator.ts +321 -0
- package/src/providers/spec/fsm-driver.ts +811 -0
- package/src/providers/spec/fsm-evaluator.ts +255 -0
- package/src/providers/spec/fsm-loader.ts +102 -0
- package/src/providers/spec/fsm-types.ts +184 -0
- package/src/providers/spec/native-history-executor.ts +943 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/types.ts +173 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +425 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +138 -1
- 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,439 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
3
|
+
import { randomUUID } from 'crypto';
|
|
4
|
+
import { LOG } from '../logging/logger.js';
|
|
5
|
+
import { getLedgerDir, readLedgerEntries } from './mesh-ledger.js';
|
|
6
|
+
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
7
|
+
import { buildMeshSystemMessage, canonicalDaemonId, readNonEmptyString, readRecord, resolveEventSessionId } from './mesh-events-utils.js';
|
|
8
|
+
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
// MCP coordinator pending-event queue — FILE-BASED PERSISTENCE
|
|
11
|
+
// ---------------------------------------------------------------------------
|
|
12
|
+
// When a mesh event fires but no CLI coordinator session is registered (e.g.
|
|
13
|
+
// the coordinator is Claude Code running via MCP), we persist the event to a
|
|
14
|
+
// per-mesh JSONL file so it survives daemon restarts. The 50-entry hard cap
|
|
15
|
+
// is removed; the file is drained atomically on each get_pending_mesh_events
|
|
16
|
+
// call and limited to 100 KB to prevent runaway growth.
|
|
17
|
+
//
|
|
18
|
+
// File: <ledgerDir>/<meshId>.pending-events.jsonl
|
|
19
|
+
// ---------------------------------------------------------------------------
|
|
20
|
+
|
|
21
|
+
export interface PendingMeshCoordinatorEvent {
|
|
22
|
+
event: string;
|
|
23
|
+
meshId: string;
|
|
24
|
+
nodeLabel: string;
|
|
25
|
+
nodeId?: string;
|
|
26
|
+
workspace?: string;
|
|
27
|
+
metadataEvent: Record<string, unknown>;
|
|
28
|
+
coordinatorMessage?: string;
|
|
29
|
+
queuedAt: number;
|
|
30
|
+
/**
|
|
31
|
+
* When set, this event is intended for a specific coordinator daemon.
|
|
32
|
+
* Coordinators on other daemons should ignore it during drain.
|
|
33
|
+
* Absent on legacy events — treated as broadcast to any coordinator.
|
|
34
|
+
*/
|
|
35
|
+
targetCoordinatorDaemonId?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const REFINE_TERMINAL_EVENTS = new Set(['refine:completed', 'refine:failed']);
|
|
39
|
+
|
|
40
|
+
export function readRefineJobId(event: { metadataEvent?: Record<string, unknown> } | Record<string, unknown>): string {
|
|
41
|
+
const metadata = readRecord((event as any).metadataEvent) || event as Record<string, unknown>;
|
|
42
|
+
const result = readRecord(metadata.result);
|
|
43
|
+
const refineJob = readRecord(result?.refineJob);
|
|
44
|
+
return readNonEmptyString(metadata.jobId) || readNonEmptyString(refineJob?.jobId);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildRefineTerminalEventFingerprint(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): string {
|
|
48
|
+
const jobId = readRefineJobId({ metadataEvent });
|
|
49
|
+
return jobId && REFINE_TERMINAL_EVENTS.has(eventName) ? `${meshId}::${eventName}::${jobId}` : '';
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function hasPendingRefineTerminalEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
|
|
53
|
+
if (!REFINE_TERMINAL_EVENTS.has(event.event)) return false;
|
|
54
|
+
const jobId = readRefineJobId(event);
|
|
55
|
+
if (!jobId) return false;
|
|
56
|
+
return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) =>
|
|
57
|
+
pending.event === event.event && readRefineJobId(pending) === jobId,
|
|
58
|
+
);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent): string {
|
|
62
|
+
const metadata = readRecord(event.metadataEvent) || {};
|
|
63
|
+
// Bootstrap events are node-scoped: dedup by meshId+event+nodeId only.
|
|
64
|
+
// They carry no sessionId/taskId/timestamp — using those fields would produce
|
|
65
|
+
// an empty fingerprint that defeats dedup entirely.
|
|
66
|
+
if (event.event === 'worktree_bootstrap_complete' || event.event === 'worktree_bootstrap_failed') {
|
|
67
|
+
return [event.meshId, event.event, event.nodeId || ''].join('::');
|
|
68
|
+
}
|
|
69
|
+
const sessionId = resolveEventSessionId(metadata);
|
|
70
|
+
const providerSessionId = readNonEmptyString(metadata.providerSessionId);
|
|
71
|
+
const taskId = readNonEmptyString(metadata.taskId) || readNonEmptyString(readRecord(metadata.payload)?.taskId);
|
|
72
|
+
const jobId = readRefineJobId(event);
|
|
73
|
+
const timestamp = metadata.timestamp !== undefined && metadata.timestamp !== null ? String(metadata.timestamp) : '';
|
|
74
|
+
return [
|
|
75
|
+
event.meshId,
|
|
76
|
+
event.event,
|
|
77
|
+
event.nodeId || '',
|
|
78
|
+
sessionId || '',
|
|
79
|
+
providerSessionId || '',
|
|
80
|
+
taskId || '',
|
|
81
|
+
jobId || '',
|
|
82
|
+
timestamp || '',
|
|
83
|
+
].join('::');
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// R3: TTL for the direct-delivered marker. A coordinator polls get_pending_mesh_events
|
|
87
|
+
// well within this window after a terminal event; after it expires the marker is swept and
|
|
88
|
+
// a late/duplicate drain would (harmlessly) re-surface — but by then the event is long gone
|
|
89
|
+
// from the queue too. 10 minutes mirrors the completion-fingerprint TTL.
|
|
90
|
+
const DIRECT_DELIVERED_TTL_MS = 10 * 60 * 1000;
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* R3: record that an event was direct-injected into a live coordinator on `coordinatorDaemonId`.
|
|
94
|
+
* That coordinator's own drain (get_pending_mesh_events with the same coordinatorDaemonId) will
|
|
95
|
+
* skip the queued copy, so it receives the event exactly once instead of twice (PTY + poll).
|
|
96
|
+
* Other consumers (unscoped drainers, other daemons) are unaffected — they did not get the inject.
|
|
97
|
+
*/
|
|
98
|
+
export function markMeshCoordinatorEventDirectDelivered(
|
|
99
|
+
coordinatorDaemonId: string,
|
|
100
|
+
event: PendingMeshCoordinatorEvent,
|
|
101
|
+
): void {
|
|
102
|
+
// Canonicalize so the raw machineId used at inject time and the prefixed instanceId the
|
|
103
|
+
// coordinator drains with resolve to the same key.
|
|
104
|
+
const canonical = canonicalDaemonId(coordinatorDaemonId);
|
|
105
|
+
if (!canonical) return;
|
|
106
|
+
const fingerprint = buildPendingEventFingerprint(event);
|
|
107
|
+
if (!fingerprint.trim()) return;
|
|
108
|
+
try {
|
|
109
|
+
const store = MeshRuntimeStore.getInstance();
|
|
110
|
+
store.recordDirectDelivered(canonical, fingerprint, DIRECT_DELIVERED_TTL_MS);
|
|
111
|
+
store.sweepExpiredDirectDelivered();
|
|
112
|
+
} catch { /* best-effort — a duplicate is preferable to a crash */ }
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function wasDirectDeliveredToCoordinator(coordinatorDaemonId: string, event: PendingMeshCoordinatorEvent): boolean {
|
|
116
|
+
const canonical = canonicalDaemonId(coordinatorDaemonId);
|
|
117
|
+
if (!canonical) return false;
|
|
118
|
+
const fingerprint = buildPendingEventFingerprint(event);
|
|
119
|
+
if (!fingerprint.trim()) return false;
|
|
120
|
+
try {
|
|
121
|
+
return MeshRuntimeStore.getInstance().wasDirectDelivered(canonical, fingerprint);
|
|
122
|
+
} catch {
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
|
|
128
|
+
const fingerprint = buildPendingEventFingerprint(event);
|
|
129
|
+
if (!fingerprint.trim()) return false;
|
|
130
|
+
// Check SQLite inbox first (G3 primary path)
|
|
131
|
+
try {
|
|
132
|
+
if (MeshRuntimeStore.getInstance().hasPendingEventFingerprint(event.meshId, fingerprint)) return true;
|
|
133
|
+
} catch { /* fall through to JSONL check */ }
|
|
134
|
+
return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) => buildPendingEventFingerprint(pending) === fingerprint);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getPendingEventsPath(meshId: string, coordinatorDaemonId?: string): string {
|
|
138
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
139
|
+
if (coordinatorDaemonId) {
|
|
140
|
+
const safeDaemon = coordinatorDaemonId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
141
|
+
return join(getLedgerDir(), `${safe}-${safeDaemon}.pending-events.jsonl`);
|
|
142
|
+
}
|
|
143
|
+
return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function readPendingMeshCoordinatorEventsFromDisk(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
|
|
147
|
+
if (!meshId) return [];
|
|
148
|
+
// Read coordinator-scoped file first; fall back to legacy shared file.
|
|
149
|
+
const paths = coordinatorDaemonId
|
|
150
|
+
? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
|
|
151
|
+
: [getPendingEventsPath(meshId)];
|
|
152
|
+
const events: PendingMeshCoordinatorEvent[] = [];
|
|
153
|
+
for (const path of paths) {
|
|
154
|
+
if (!existsSync(path)) continue;
|
|
155
|
+
try {
|
|
156
|
+
const raw = readFileSync(path, 'utf-8');
|
|
157
|
+
const parsed = raw.split('\n').filter(Boolean).flatMap(line => {
|
|
158
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
159
|
+
});
|
|
160
|
+
// If reading the shared file, filter to events that target this coordinator or are unscoped.
|
|
161
|
+
const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
|
|
162
|
+
? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
|
|
163
|
+
: parsed;
|
|
164
|
+
events.push(...filtered);
|
|
165
|
+
} catch { /* skip unreadable files */ }
|
|
166
|
+
}
|
|
167
|
+
return events;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
function refineTerminalEventFromLedger(meshId: string, pending: readonly PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
|
|
171
|
+
const acceptedJobIds = new Set(
|
|
172
|
+
pending
|
|
173
|
+
.filter(event => event.event === 'refine:accepted')
|
|
174
|
+
.map(event => readRefineJobId(event))
|
|
175
|
+
.filter(Boolean),
|
|
176
|
+
);
|
|
177
|
+
if (acceptedJobIds.size === 0) return [];
|
|
178
|
+
const existingTerminalJobIds = new Set(
|
|
179
|
+
pending
|
|
180
|
+
.filter(event => REFINE_TERMINAL_EVENTS.has(event.event))
|
|
181
|
+
.map(event => `${event.event}:${readRefineJobId(event)}`)
|
|
182
|
+
.filter(value => !value.endsWith(':')),
|
|
183
|
+
);
|
|
184
|
+
const backfilled: PendingMeshCoordinatorEvent[] = [];
|
|
185
|
+
const entries = readLedgerEntries(meshId, { tail: 200 });
|
|
186
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
187
|
+
const entry = entries[i];
|
|
188
|
+
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed') continue;
|
|
189
|
+
const payload = readRecord(entry.payload);
|
|
190
|
+
if (payload?.source !== 'refine_mesh_node_async_job') continue;
|
|
191
|
+
const refineJob = readRecord(payload.refineJob);
|
|
192
|
+
const jobId = readNonEmptyString(refineJob?.jobId);
|
|
193
|
+
if (!jobId || !acceptedJobIds.has(jobId)) continue;
|
|
194
|
+
const eventName = entry.kind === 'task_completed' ? 'refine:completed' : 'refine:failed';
|
|
195
|
+
if (existingTerminalJobIds.has(`${eventName}:${jobId}`)) continue;
|
|
196
|
+
existingTerminalJobIds.add(`${eventName}:${jobId}`);
|
|
197
|
+
const result = readRecord(payload.result);
|
|
198
|
+
const metadataEvent = {
|
|
199
|
+
source: 'refine_mesh_node_async_job',
|
|
200
|
+
jobId,
|
|
201
|
+
interactionId: readNonEmptyString(refineJob?.interactionId),
|
|
202
|
+
meshId,
|
|
203
|
+
nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
|
|
204
|
+
targetDaemonId: readNonEmptyString(refineJob?.targetDaemonId),
|
|
205
|
+
workspace: readNonEmptyString(refineJob?.workspace),
|
|
206
|
+
status: eventName === 'refine:completed' ? 'completed' : 'failed',
|
|
207
|
+
startedAt: readNonEmptyString(refineJob?.startedAt),
|
|
208
|
+
completedAt: readNonEmptyString(refineJob?.completedAt) || entry.timestamp,
|
|
209
|
+
retryOfJobId: readNonEmptyString(refineJob?.retryOfJobId) || readNonEmptyString(payload.retryOfJobId),
|
|
210
|
+
...(result ? { result } : {}),
|
|
211
|
+
};
|
|
212
|
+
const nodeLabel = readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job';
|
|
213
|
+
backfilled.push({
|
|
214
|
+
event: eventName,
|
|
215
|
+
meshId,
|
|
216
|
+
nodeLabel,
|
|
217
|
+
nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
|
|
218
|
+
workspace: readNonEmptyString(refineJob?.workspace),
|
|
219
|
+
metadataEvent,
|
|
220
|
+
coordinatorMessage: buildMeshSystemMessage({ event: eventName, nodeLabel, metadataEvent }),
|
|
221
|
+
queuedAt: Date.now(),
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
return backfilled.reverse();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function reconcilePendingMeshCoordinatorEvents(meshId: string, events: PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
|
|
228
|
+
const backfilled = refineTerminalEventFromLedger(meshId, events);
|
|
229
|
+
// A refine:accepted event is a provisional "job accepted, result to follow" signal.
|
|
230
|
+
// Once its terminal (completed/failed) counterpart for the same jobId exists — whether
|
|
231
|
+
// already direct-queued into the pending store OR backfilled from the ledger here — the
|
|
232
|
+
// accepted is superseded and is dropped so the coordinator isn't shown stale duplicate
|
|
233
|
+
// noise alongside the terminal outcome.
|
|
234
|
+
const terminalJobIds = new Set(
|
|
235
|
+
[...events.filter(event => REFINE_TERMINAL_EVENTS.has(event.event)), ...backfilled]
|
|
236
|
+
.map(event => readRefineJobId(event))
|
|
237
|
+
.filter(Boolean),
|
|
238
|
+
);
|
|
239
|
+
const reconciled = terminalJobIds.size === 0
|
|
240
|
+
? events
|
|
241
|
+
: events.filter(event => !(event.event === 'refine:accepted' && terminalJobIds.has(readRefineJobId(event))));
|
|
242
|
+
return backfilled.length === 0 ? reconciled : [...reconciled, ...backfilled];
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const MAX_PENDING_EVENTS_BYTES = 100 * 1024; // 100 KB — keep the pending file small
|
|
246
|
+
const MAX_PENDING_EVENTS_KEEP = 50; // keep the last 50 events when trimming
|
|
247
|
+
|
|
248
|
+
function trimPendingEventsIfNeeded(path: string): void {
|
|
249
|
+
try {
|
|
250
|
+
if (!existsSync(path)) return;
|
|
251
|
+
if (statSync(path).size <= MAX_PENDING_EVENTS_BYTES) return;
|
|
252
|
+
const lines = readFileSync(path, 'utf-8').split('\n').filter(Boolean);
|
|
253
|
+
if (lines.length <= MAX_PENDING_EVENTS_KEEP) return;
|
|
254
|
+
writeFileSync(path, lines.slice(-MAX_PENDING_EVENTS_KEEP).join('\n') + '\n', 'utf-8');
|
|
255
|
+
} catch { /* best-effort; if trim fails, append still proceeds */ }
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
|
|
259
|
+
try {
|
|
260
|
+
if (hasPendingRefineTerminalEventDuplicate(event)) {
|
|
261
|
+
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for refine job ${readRefineJobId(event)}`);
|
|
262
|
+
return true;
|
|
263
|
+
}
|
|
264
|
+
if (hasPendingCoordinatorEventDuplicate(event)) {
|
|
265
|
+
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for mesh ${event.meshId}`);
|
|
266
|
+
return true;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const fingerprint = buildPendingEventFingerprint(event);
|
|
270
|
+
|
|
271
|
+
// G3: Write to SQLite inbox (primary path going forward)
|
|
272
|
+
try {
|
|
273
|
+
MeshRuntimeStore.getInstance().insertPendingEvent({
|
|
274
|
+
id: randomUUID(),
|
|
275
|
+
meshId: event.meshId,
|
|
276
|
+
coordinatorDaemonId: event.targetCoordinatorDaemonId ?? null,
|
|
277
|
+
event: event.event,
|
|
278
|
+
payload: event,
|
|
279
|
+
fingerprint: fingerprint || null,
|
|
280
|
+
queuedAt: event.queuedAt,
|
|
281
|
+
});
|
|
282
|
+
} catch {
|
|
283
|
+
// SQLite write failure is non-fatal; JSONL fallback below still works.
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Also write to JSONL (retained as legacy/export artifact)
|
|
287
|
+
const path = getPendingEventsPath(event.meshId, event.targetCoordinatorDaemonId);
|
|
288
|
+
trimPendingEventsIfNeeded(path);
|
|
289
|
+
appendFileSync(path, JSON.stringify(event) + '\n', 'utf-8');
|
|
290
|
+
return true;
|
|
291
|
+
} catch (e: any) {
|
|
292
|
+
LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
|
|
293
|
+
return false;
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
// Atomically rename the file before reading so concurrent drains can't both consume
|
|
298
|
+
// the same events. renameSync is atomic on POSIX (same filesystem); only one caller
|
|
299
|
+
// wins the rename — the other gets ENOENT and returns null, preventing duplicate delivery.
|
|
300
|
+
function atomicDrainFile(path: string): string | null {
|
|
301
|
+
const tmpPath = `${path}.draining`;
|
|
302
|
+
try {
|
|
303
|
+
renameSync(path, tmpPath);
|
|
304
|
+
} catch {
|
|
305
|
+
return null; // another drain already renamed it, or file doesn't exist
|
|
306
|
+
}
|
|
307
|
+
try {
|
|
308
|
+
const content = readFileSync(tmpPath, 'utf-8');
|
|
309
|
+
try { unlinkSync(tmpPath); } catch { /* already cleaned up */ }
|
|
310
|
+
return content;
|
|
311
|
+
} catch {
|
|
312
|
+
try { unlinkSync(tmpPath); } catch { /* best-effort cleanup */ }
|
|
313
|
+
return null;
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
/** Drain and return all pending coordinator events for meshId, removing them from disk. */
|
|
318
|
+
export function drainPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
|
|
319
|
+
if (!meshId) return [];
|
|
320
|
+
|
|
321
|
+
// Dual-write means SQLite and JSONL hold the same events. Both stores must be
|
|
322
|
+
// emptied in one drain call — draining only one leaves the other to re-deliver
|
|
323
|
+
// the same events on the next call. Merge with fingerprint dedup.
|
|
324
|
+
const merged: PendingMeshCoordinatorEvent[] = [];
|
|
325
|
+
const seenFingerprints = new Set<string>();
|
|
326
|
+
const pushUnique = (event: PendingMeshCoordinatorEvent) => {
|
|
327
|
+
const fingerprint = buildPendingEventFingerprint(event);
|
|
328
|
+
if (fingerprint.trim()) {
|
|
329
|
+
if (seenFingerprints.has(fingerprint)) return;
|
|
330
|
+
seenFingerprints.add(fingerprint);
|
|
331
|
+
}
|
|
332
|
+
merged.push(event);
|
|
333
|
+
};
|
|
334
|
+
|
|
335
|
+
// G3: SQLite inbox
|
|
336
|
+
try {
|
|
337
|
+
const store = MeshRuntimeStore.getInstance();
|
|
338
|
+
if (store.pendingEventCount(meshId) > 0) {
|
|
339
|
+
for (const row of store.drainPendingEvents(meshId, coordinatorDaemonId)) {
|
|
340
|
+
const event = row.payload as PendingMeshCoordinatorEvent;
|
|
341
|
+
if (event) pushUnique(event);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
} catch {
|
|
345
|
+
// SQLite drain failed — JSONL below still drains
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
// JSONL (legacy / migration path) — always drained alongside SQLite
|
|
349
|
+
const paths = coordinatorDaemonId
|
|
350
|
+
? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
|
|
351
|
+
: [getPendingEventsPath(meshId)];
|
|
352
|
+
for (const path of paths) {
|
|
353
|
+
const content = atomicDrainFile(path);
|
|
354
|
+
if (!content) continue;
|
|
355
|
+
const parsed = content.split('\n').filter(Boolean).flatMap(line => {
|
|
356
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
357
|
+
});
|
|
358
|
+
// If reading the shared file, filter to events that target this coordinator or are unscoped.
|
|
359
|
+
const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
|
|
360
|
+
? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
|
|
361
|
+
: parsed;
|
|
362
|
+
for (const event of filtered) pushUnique(event);
|
|
363
|
+
}
|
|
364
|
+
if (merged.length === 0) return [];
|
|
365
|
+
// R3: when this drain is scoped to a coordinator daemon, exclude events that were already
|
|
366
|
+
// direct-injected into that coordinator's live CLI session. Unscoped drains (no daemon id)
|
|
367
|
+
// keep everything — they belong to consumers that never received the direct inject.
|
|
368
|
+
const deliverable = coordinatorDaemonId
|
|
369
|
+
? merged.filter(event => !wasDirectDeliveredToCoordinator(coordinatorDaemonId, event))
|
|
370
|
+
: merged;
|
|
371
|
+
if (deliverable.length === 0) return [];
|
|
372
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, deliverable);
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
376
|
+
export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): readonly PendingMeshCoordinatorEvent[] {
|
|
377
|
+
if (!meshId) return [];
|
|
378
|
+
|
|
379
|
+
// Merge SQLite (primary) + JSONL (legacy) with fingerprint dedup.
|
|
380
|
+
const merged: PendingMeshCoordinatorEvent[] = [];
|
|
381
|
+
const seenFingerprints = new Set<string>();
|
|
382
|
+
const pushUnique = (event: PendingMeshCoordinatorEvent) => {
|
|
383
|
+
const fingerprint = buildPendingEventFingerprint(event);
|
|
384
|
+
if (fingerprint.trim()) {
|
|
385
|
+
if (seenFingerprints.has(fingerprint)) return;
|
|
386
|
+
seenFingerprints.add(fingerprint);
|
|
387
|
+
}
|
|
388
|
+
merged.push(event);
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
// G3: SQLite inbox (non-destructive peek at undrained rows)
|
|
392
|
+
try {
|
|
393
|
+
const store = MeshRuntimeStore.getInstance();
|
|
394
|
+
if (store.pendingEventCount(meshId) > 0) {
|
|
395
|
+
for (const row of store.peekPendingEvents(meshId, coordinatorDaemonId)) {
|
|
396
|
+
const event = row.payload as PendingMeshCoordinatorEvent;
|
|
397
|
+
if (event) pushUnique(event);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
} catch { /* SQLite unavailable — JSONL fallback below */ }
|
|
401
|
+
|
|
402
|
+
// JSONL (legacy)
|
|
403
|
+
for (const event of readPendingMeshCoordinatorEventsFromDisk(meshId, coordinatorDaemonId)) {
|
|
404
|
+
pushUnique(event);
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
// R3: hide events already direct-delivered to this coordinator from its status peek, so
|
|
408
|
+
// mesh_status doesn't report a "pending" event the coordinator has in fact already received.
|
|
409
|
+
const deliverable = coordinatorDaemonId
|
|
410
|
+
? merged.filter(event => !wasDirectDeliveredToCoordinator(coordinatorDaemonId, event))
|
|
411
|
+
: merged;
|
|
412
|
+
|
|
413
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, deliverable);
|
|
414
|
+
}
|
|
415
|
+
|
|
416
|
+
/**
|
|
417
|
+
* Test helper: purge all pending-event state for a mesh — SQLite rows
|
|
418
|
+
* (including drained fingerprint history) and JSONL files.
|
|
419
|
+
*/
|
|
420
|
+
export function __clearMeshPendingEventsForTests(meshId: string): void {
|
|
421
|
+
try {
|
|
422
|
+
MeshRuntimeStore.getInstance().clearPendingEventsForMesh(meshId);
|
|
423
|
+
} catch { /* store unavailable — nothing to clear */ }
|
|
424
|
+
clearPendingMeshCoordinatorEvents(meshId);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/** Explicitly clear all pending coordinator events for a mesh (and coordinator if scoped). */
|
|
428
|
+
export function clearPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): void {
|
|
429
|
+
if (!meshId) return;
|
|
430
|
+
// Clear SQLite rows
|
|
431
|
+
try { MeshRuntimeStore.getInstance().clearPendingEventsForMesh(meshId); } catch { /* store unavailable */ }
|
|
432
|
+
// Clear JSONL files
|
|
433
|
+
const paths = coordinatorDaemonId
|
|
434
|
+
? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
|
|
435
|
+
: [getPendingEventsPath(meshId)];
|
|
436
|
+
for (const path of paths) {
|
|
437
|
+
if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
|
|
438
|
+
}
|
|
439
|
+
}
|