@adhdev/daemon-core 0.9.82-rc.21 → 0.9.82-rc.211
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 +1 -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 +178 -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 +29 -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 +32 -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 +46 -11
- package/dist/index.js +35173 -15756
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +34880 -15556
- 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 +151 -0
- package/dist/mesh/mesh-events-pending.d.ts +33 -0
- package/dist/mesh/mesh-events-stale.d.ts +40 -0
- package/dist/mesh/mesh-events-utils.d.ts +14 -0
- package/dist/mesh/mesh-events.d.ts +5 -40
- 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-status.d.ts +26 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-runtime-store.d.ts +324 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-work-queue.d.ts +137 -6
- 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 +39 -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 +12 -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 +101 -0
- package/dist/providers/spec/driver.d.ts +225 -0
- package/dist/providers/spec/evaluator.d.ts +54 -0
- package/dist/providers/spec/loader.d.ts +15 -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/schema.gen.d.ts +2025 -0
- package/dist/providers/spec/types.d.ts +302 -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 +109 -1
- package/dist/sessions/registry.d.ts +3 -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 +22 -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 +1138 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +673 -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 +59 -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 +1787 -60
- package/src/commands/cli-manager.ts +283 -14
- package/src/commands/handler.ts +809 -2
- package/src/commands/mesh-coordinator.ts +332 -122
- package/src/commands/router.ts +4198 -557
- package/src/config/chat-history.ts +95 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +280 -2
- 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 +126 -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 +218 -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 +1404 -0
- package/src/mesh/mesh-events-pending.ts +371 -0
- package/src/mesh/mesh-events-stale.ts +283 -0
- package/src/mesh/mesh-events-utils.ts +161 -0
- package/src/mesh/mesh-events.ts +24 -908
- 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 +587 -102
- package/src/mesh/mesh-missions.ts +151 -0
- package/src/mesh/mesh-refine-status.ts +144 -0
- package/src/mesh/mesh-review-inbox.ts +307 -0
- package/src/mesh/mesh-runtime-store.ts +1305 -0
- package/src/mesh/mesh-task-stats.ts +154 -0
- package/src/mesh/mesh-work-queue.ts +490 -166
- 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 +833 -64
- 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 +419 -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 +582 -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 +138 -0
- package/src/providers/spec/cli-adapter.ts +626 -0
- package/src/providers/spec/driver.ts +922 -0
- package/src/providers/spec/evaluator.ts +597 -0
- package/src/providers/spec/loader.ts +324 -0
- package/src/providers/spec/native-history-executor.ts +939 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/schema.gen.ts +691 -0
- package/src/providers/spec/schema.json +341 -0
- package/src/providers/spec/types.ts +356 -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 +119 -1
- package/src/sessions/registry.ts +3 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.ts +45 -1
- package/src/status/builders.ts +24 -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,371 @@
|
|
|
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, 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
|
+
const sessionId = resolveEventSessionId(metadata);
|
|
64
|
+
const providerSessionId = readNonEmptyString(metadata.providerSessionId);
|
|
65
|
+
const taskId = readNonEmptyString(metadata.taskId) || readNonEmptyString(readRecord(metadata.payload)?.taskId);
|
|
66
|
+
const jobId = readRefineJobId(event);
|
|
67
|
+
const timestamp = metadata.timestamp !== undefined && metadata.timestamp !== null ? String(metadata.timestamp) : '';
|
|
68
|
+
return [
|
|
69
|
+
event.meshId,
|
|
70
|
+
event.event,
|
|
71
|
+
event.nodeId || '',
|
|
72
|
+
sessionId || '',
|
|
73
|
+
providerSessionId || '',
|
|
74
|
+
taskId || '',
|
|
75
|
+
jobId || '',
|
|
76
|
+
timestamp || '',
|
|
77
|
+
].join('::');
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
|
|
81
|
+
const fingerprint = buildPendingEventFingerprint(event);
|
|
82
|
+
if (!fingerprint.trim()) return false;
|
|
83
|
+
// Check SQLite inbox first (G3 primary path)
|
|
84
|
+
try {
|
|
85
|
+
if (MeshRuntimeStore.getInstance().hasPendingEventFingerprint(event.meshId, fingerprint)) return true;
|
|
86
|
+
} catch { /* fall through to JSONL check */ }
|
|
87
|
+
return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) => buildPendingEventFingerprint(pending) === fingerprint);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function getPendingEventsPath(meshId: string, coordinatorDaemonId?: string): string {
|
|
91
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
92
|
+
if (coordinatorDaemonId) {
|
|
93
|
+
const safeDaemon = coordinatorDaemonId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
94
|
+
return join(getLedgerDir(), `${safe}-${safeDaemon}.pending-events.jsonl`);
|
|
95
|
+
}
|
|
96
|
+
return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function readPendingMeshCoordinatorEventsFromDisk(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
|
|
100
|
+
if (!meshId) return [];
|
|
101
|
+
// Read coordinator-scoped file first; fall back to legacy shared file.
|
|
102
|
+
const paths = coordinatorDaemonId
|
|
103
|
+
? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
|
|
104
|
+
: [getPendingEventsPath(meshId)];
|
|
105
|
+
const events: PendingMeshCoordinatorEvent[] = [];
|
|
106
|
+
for (const path of paths) {
|
|
107
|
+
if (!existsSync(path)) continue;
|
|
108
|
+
try {
|
|
109
|
+
const raw = readFileSync(path, 'utf-8');
|
|
110
|
+
const parsed = raw.split('\n').filter(Boolean).flatMap(line => {
|
|
111
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
112
|
+
});
|
|
113
|
+
// If reading the shared file, filter to events that target this coordinator or are unscoped.
|
|
114
|
+
const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
|
|
115
|
+
? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
|
|
116
|
+
: parsed;
|
|
117
|
+
events.push(...filtered);
|
|
118
|
+
} catch { /* skip unreadable files */ }
|
|
119
|
+
}
|
|
120
|
+
return events;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function refineTerminalEventFromLedger(meshId: string, pending: readonly PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
|
|
124
|
+
const acceptedJobIds = new Set(
|
|
125
|
+
pending
|
|
126
|
+
.filter(event => event.event === 'refine:accepted')
|
|
127
|
+
.map(event => readRefineJobId(event))
|
|
128
|
+
.filter(Boolean),
|
|
129
|
+
);
|
|
130
|
+
if (acceptedJobIds.size === 0) return [];
|
|
131
|
+
const existingTerminalJobIds = new Set(
|
|
132
|
+
pending
|
|
133
|
+
.filter(event => REFINE_TERMINAL_EVENTS.has(event.event))
|
|
134
|
+
.map(event => `${event.event}:${readRefineJobId(event)}`)
|
|
135
|
+
.filter(value => !value.endsWith(':')),
|
|
136
|
+
);
|
|
137
|
+
const backfilled: PendingMeshCoordinatorEvent[] = [];
|
|
138
|
+
const entries = readLedgerEntries(meshId, { tail: 200 });
|
|
139
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
140
|
+
const entry = entries[i];
|
|
141
|
+
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed') continue;
|
|
142
|
+
const payload = readRecord(entry.payload);
|
|
143
|
+
if (payload?.source !== 'refine_mesh_node_async_job') continue;
|
|
144
|
+
const refineJob = readRecord(payload.refineJob);
|
|
145
|
+
const jobId = readNonEmptyString(refineJob?.jobId);
|
|
146
|
+
if (!jobId || !acceptedJobIds.has(jobId)) continue;
|
|
147
|
+
const eventName = entry.kind === 'task_completed' ? 'refine:completed' : 'refine:failed';
|
|
148
|
+
if (existingTerminalJobIds.has(`${eventName}:${jobId}`)) continue;
|
|
149
|
+
existingTerminalJobIds.add(`${eventName}:${jobId}`);
|
|
150
|
+
const result = readRecord(payload.result);
|
|
151
|
+
const metadataEvent = {
|
|
152
|
+
source: 'refine_mesh_node_async_job',
|
|
153
|
+
jobId,
|
|
154
|
+
interactionId: readNonEmptyString(refineJob?.interactionId),
|
|
155
|
+
meshId,
|
|
156
|
+
nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
|
|
157
|
+
targetDaemonId: readNonEmptyString(refineJob?.targetDaemonId),
|
|
158
|
+
workspace: readNonEmptyString(refineJob?.workspace),
|
|
159
|
+
status: eventName === 'refine:completed' ? 'completed' : 'failed',
|
|
160
|
+
startedAt: readNonEmptyString(refineJob?.startedAt),
|
|
161
|
+
completedAt: readNonEmptyString(refineJob?.completedAt) || entry.timestamp,
|
|
162
|
+
retryOfJobId: readNonEmptyString(refineJob?.retryOfJobId) || readNonEmptyString(payload.retryOfJobId),
|
|
163
|
+
...(result ? { result } : {}),
|
|
164
|
+
};
|
|
165
|
+
const nodeLabel = readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job';
|
|
166
|
+
backfilled.push({
|
|
167
|
+
event: eventName,
|
|
168
|
+
meshId,
|
|
169
|
+
nodeLabel,
|
|
170
|
+
nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
|
|
171
|
+
workspace: readNonEmptyString(refineJob?.workspace),
|
|
172
|
+
metadataEvent,
|
|
173
|
+
coordinatorMessage: buildMeshSystemMessage({ event: eventName, nodeLabel, metadataEvent }),
|
|
174
|
+
queuedAt: Date.now(),
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
return backfilled.reverse();
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function reconcilePendingMeshCoordinatorEvents(meshId: string, events: PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
|
|
181
|
+
const backfilled = refineTerminalEventFromLedger(meshId, events);
|
|
182
|
+
if (backfilled.length === 0) return events;
|
|
183
|
+
const terminalJobIds = new Set(backfilled.map(event => readRefineJobId(event)).filter(Boolean));
|
|
184
|
+
return [
|
|
185
|
+
...events.filter(event => !(event.event === 'refine:accepted' && terminalJobIds.has(readRefineJobId(event)))),
|
|
186
|
+
...backfilled,
|
|
187
|
+
];
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const MAX_PENDING_EVENTS_BYTES = 100 * 1024; // 100 KB — keep the pending file small
|
|
191
|
+
const MAX_PENDING_EVENTS_KEEP = 50; // keep the last 50 events when trimming
|
|
192
|
+
|
|
193
|
+
function trimPendingEventsIfNeeded(path: string): void {
|
|
194
|
+
try {
|
|
195
|
+
if (!existsSync(path)) return;
|
|
196
|
+
if (statSync(path).size <= MAX_PENDING_EVENTS_BYTES) return;
|
|
197
|
+
const lines = readFileSync(path, 'utf-8').split('\n').filter(Boolean);
|
|
198
|
+
if (lines.length <= MAX_PENDING_EVENTS_KEEP) return;
|
|
199
|
+
writeFileSync(path, lines.slice(-MAX_PENDING_EVENTS_KEEP).join('\n') + '\n', 'utf-8');
|
|
200
|
+
} catch { /* best-effort; if trim fails, append still proceeds */ }
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
|
|
204
|
+
try {
|
|
205
|
+
if (hasPendingRefineTerminalEventDuplicate(event)) {
|
|
206
|
+
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for refine job ${readRefineJobId(event)}`);
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
if (hasPendingCoordinatorEventDuplicate(event)) {
|
|
210
|
+
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for mesh ${event.meshId}`);
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const fingerprint = buildPendingEventFingerprint(event);
|
|
215
|
+
|
|
216
|
+
// G3: Write to SQLite inbox (primary path going forward)
|
|
217
|
+
try {
|
|
218
|
+
MeshRuntimeStore.getInstance().insertPendingEvent({
|
|
219
|
+
id: randomUUID(),
|
|
220
|
+
meshId: event.meshId,
|
|
221
|
+
coordinatorDaemonId: event.targetCoordinatorDaemonId ?? null,
|
|
222
|
+
event: event.event,
|
|
223
|
+
payload: event,
|
|
224
|
+
fingerprint: fingerprint || null,
|
|
225
|
+
queuedAt: event.queuedAt,
|
|
226
|
+
});
|
|
227
|
+
} catch {
|
|
228
|
+
// SQLite write failure is non-fatal; JSONL fallback below still works.
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// Also write to JSONL (retained as legacy/export artifact)
|
|
232
|
+
const path = getPendingEventsPath(event.meshId, event.targetCoordinatorDaemonId);
|
|
233
|
+
trimPendingEventsIfNeeded(path);
|
|
234
|
+
appendFileSync(path, JSON.stringify(event) + '\n', 'utf-8');
|
|
235
|
+
return true;
|
|
236
|
+
} catch (e: any) {
|
|
237
|
+
LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
|
|
238
|
+
return false;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Atomically rename the file before reading so concurrent drains can't both consume
|
|
243
|
+
// the same events. renameSync is atomic on POSIX (same filesystem); only one caller
|
|
244
|
+
// wins the rename — the other gets ENOENT and returns null, preventing duplicate delivery.
|
|
245
|
+
function atomicDrainFile(path: string): string | null {
|
|
246
|
+
const tmpPath = `${path}.draining`;
|
|
247
|
+
try {
|
|
248
|
+
renameSync(path, tmpPath);
|
|
249
|
+
} catch {
|
|
250
|
+
return null; // another drain already renamed it, or file doesn't exist
|
|
251
|
+
}
|
|
252
|
+
try {
|
|
253
|
+
const content = readFileSync(tmpPath, 'utf-8');
|
|
254
|
+
try { unlinkSync(tmpPath); } catch { /* already cleaned up */ }
|
|
255
|
+
return content;
|
|
256
|
+
} catch {
|
|
257
|
+
try { unlinkSync(tmpPath); } catch { /* best-effort cleanup */ }
|
|
258
|
+
return null;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
/** Drain and return all pending coordinator events for meshId, removing them from disk. */
|
|
263
|
+
export function drainPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
|
|
264
|
+
if (!meshId) return [];
|
|
265
|
+
|
|
266
|
+
// Dual-write means SQLite and JSONL hold the same events. Both stores must be
|
|
267
|
+
// emptied in one drain call — draining only one leaves the other to re-deliver
|
|
268
|
+
// the same events on the next call. Merge with fingerprint dedup.
|
|
269
|
+
const merged: PendingMeshCoordinatorEvent[] = [];
|
|
270
|
+
const seenFingerprints = new Set<string>();
|
|
271
|
+
const pushUnique = (event: PendingMeshCoordinatorEvent) => {
|
|
272
|
+
const fingerprint = buildPendingEventFingerprint(event);
|
|
273
|
+
if (fingerprint.trim()) {
|
|
274
|
+
if (seenFingerprints.has(fingerprint)) return;
|
|
275
|
+
seenFingerprints.add(fingerprint);
|
|
276
|
+
}
|
|
277
|
+
merged.push(event);
|
|
278
|
+
};
|
|
279
|
+
|
|
280
|
+
// G3: SQLite inbox
|
|
281
|
+
try {
|
|
282
|
+
const store = MeshRuntimeStore.getInstance();
|
|
283
|
+
if (store.pendingEventCount(meshId) > 0) {
|
|
284
|
+
for (const row of store.drainPendingEvents(meshId, coordinatorDaemonId)) {
|
|
285
|
+
const event = row.payload as PendingMeshCoordinatorEvent;
|
|
286
|
+
if (event) pushUnique(event);
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
} catch {
|
|
290
|
+
// SQLite drain failed — JSONL below still drains
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
// JSONL (legacy / migration path) — always drained alongside SQLite
|
|
294
|
+
const paths = coordinatorDaemonId
|
|
295
|
+
? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
|
|
296
|
+
: [getPendingEventsPath(meshId)];
|
|
297
|
+
for (const path of paths) {
|
|
298
|
+
const content = atomicDrainFile(path);
|
|
299
|
+
if (!content) continue;
|
|
300
|
+
const parsed = content.split('\n').filter(Boolean).flatMap(line => {
|
|
301
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
302
|
+
});
|
|
303
|
+
// If reading the shared file, filter to events that target this coordinator or are unscoped.
|
|
304
|
+
const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
|
|
305
|
+
? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
|
|
306
|
+
: parsed;
|
|
307
|
+
for (const event of filtered) pushUnique(event);
|
|
308
|
+
}
|
|
309
|
+
if (merged.length === 0) return [];
|
|
310
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, merged);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
314
|
+
export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): readonly PendingMeshCoordinatorEvent[] {
|
|
315
|
+
if (!meshId) return [];
|
|
316
|
+
|
|
317
|
+
// Merge SQLite (primary) + JSONL (legacy) with fingerprint dedup.
|
|
318
|
+
const merged: PendingMeshCoordinatorEvent[] = [];
|
|
319
|
+
const seenFingerprints = new Set<string>();
|
|
320
|
+
const pushUnique = (event: PendingMeshCoordinatorEvent) => {
|
|
321
|
+
const fingerprint = buildPendingEventFingerprint(event);
|
|
322
|
+
if (fingerprint.trim()) {
|
|
323
|
+
if (seenFingerprints.has(fingerprint)) return;
|
|
324
|
+
seenFingerprints.add(fingerprint);
|
|
325
|
+
}
|
|
326
|
+
merged.push(event);
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
// G3: SQLite inbox (non-destructive peek at undrained rows)
|
|
330
|
+
try {
|
|
331
|
+
const store = MeshRuntimeStore.getInstance();
|
|
332
|
+
if (store.pendingEventCount(meshId) > 0) {
|
|
333
|
+
for (const row of store.peekPendingEvents(meshId, coordinatorDaemonId)) {
|
|
334
|
+
const event = row.payload as PendingMeshCoordinatorEvent;
|
|
335
|
+
if (event) pushUnique(event);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
} catch { /* SQLite unavailable — JSONL fallback below */ }
|
|
339
|
+
|
|
340
|
+
// JSONL (legacy)
|
|
341
|
+
for (const event of readPendingMeshCoordinatorEventsFromDisk(meshId, coordinatorDaemonId)) {
|
|
342
|
+
pushUnique(event);
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, merged);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/**
|
|
349
|
+
* Test helper: purge all pending-event state for a mesh — SQLite rows
|
|
350
|
+
* (including drained fingerprint history) and JSONL files.
|
|
351
|
+
*/
|
|
352
|
+
export function __clearMeshPendingEventsForTests(meshId: string): void {
|
|
353
|
+
try {
|
|
354
|
+
MeshRuntimeStore.getInstance().clearPendingEventsForMesh(meshId);
|
|
355
|
+
} catch { /* store unavailable — nothing to clear */ }
|
|
356
|
+
clearPendingMeshCoordinatorEvents(meshId);
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
/** Explicitly clear all pending coordinator events for a mesh (and coordinator if scoped). */
|
|
360
|
+
export function clearPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): void {
|
|
361
|
+
if (!meshId) return;
|
|
362
|
+
// Clear SQLite rows
|
|
363
|
+
try { MeshRuntimeStore.getInstance().clearPendingEventsForMesh(meshId); } catch { /* store unavailable */ }
|
|
364
|
+
// Clear JSONL files
|
|
365
|
+
const paths = coordinatorDaemonId
|
|
366
|
+
? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
|
|
367
|
+
: [getPendingEventsPath(meshId)];
|
|
368
|
+
for (const path of paths) {
|
|
369
|
+
if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
|
|
370
|
+
}
|
|
371
|
+
}
|
|
@@ -0,0 +1,283 @@
|
|
|
1
|
+
import { appendLedgerEntry, buildTaskCompletionEvidence, readLedgerEntries } from './mesh-ledger.js';
|
|
2
|
+
import type { MeshLedgerKind } from './mesh-ledger.js';
|
|
3
|
+
import { updateDirectDispatchStatus, cleanupTerminalDirectDispatches } from './mesh-work-queue.js';
|
|
4
|
+
import { markSessionDeliveriesTerminal } from './mesh-delivery-policy.js';
|
|
5
|
+
import { queuePendingMeshCoordinatorEvent } from './mesh-events-pending.js';
|
|
6
|
+
import { readNonEmptyString, readRecord, resolveEventSessionId, readWorkerResultMetadata } from './mesh-events-utils.js';
|
|
7
|
+
|
|
8
|
+
// ---------------------------------------------------------------------------
|
|
9
|
+
// Stale direct-dispatch detection & transcript reconciliation
|
|
10
|
+
// ---------------------------------------------------------------------------
|
|
11
|
+
|
|
12
|
+
export function findRecentTerminalLedgerEvidence(args: {
|
|
13
|
+
meshId: string;
|
|
14
|
+
sessionId?: string;
|
|
15
|
+
nodeId?: string;
|
|
16
|
+
}): { id: string; kind: MeshLedgerKind; payload: Record<string, unknown>; timestamp: string } | null {
|
|
17
|
+
if (!args.sessionId && !args.nodeId) return null;
|
|
18
|
+
// Tail-limit: 200 entries gives a wide enough window to catch terminal events for active
|
|
19
|
+
// sessions while avoiding a full O(n) scan. If a terminal is older than 200 entries,
|
|
20
|
+
// the MeshRuntimeStore fingerprint dedup will still block duplicate processing downstream.
|
|
21
|
+
const entries = readLedgerEntries(args.meshId, { tail: 200 });
|
|
22
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
23
|
+
const entry = entries[i];
|
|
24
|
+
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
|
|
25
|
+
if (args.sessionId && entry.sessionId === args.sessionId) {
|
|
26
|
+
return { id: entry.id, kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
|
|
27
|
+
}
|
|
28
|
+
if (!args.sessionId && args.nodeId && entry.nodeId === args.nodeId) {
|
|
29
|
+
return { id: entry.id, kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Returns true when a task_dispatched entry for the given session appears AFTER the terminal
|
|
36
|
+
// entry (identified by terminalId) in ledger order. Positional (append) order is used rather
|
|
37
|
+
// than timestamp comparison because both entries may share the same millisecond.
|
|
38
|
+
export function hasDispatchAfterTerminal(meshId: string, sessionId: string, terminalId: string): boolean {
|
|
39
|
+
// 200-entry window matches findRecentTerminalLedgerEvidence for consistency.
|
|
40
|
+
const entries = readLedgerEntries(meshId, { tail: 200 });
|
|
41
|
+
let pastTerminal = false;
|
|
42
|
+
for (const entry of entries) {
|
|
43
|
+
if (!pastTerminal) {
|
|
44
|
+
if (entry.id === terminalId) pastTerminal = true;
|
|
45
|
+
continue;
|
|
46
|
+
}
|
|
47
|
+
if (entry.kind === 'task_dispatched' && entry.sessionId === sessionId) return true;
|
|
48
|
+
}
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function hasUnterminalDirectDispatchLedgerEntry(meshId: string, sessionId: string): boolean {
|
|
53
|
+
// Some dispatch paths can persist task_dispatched before the direct-dispatch DB row is
|
|
54
|
+
// available. Recover routing from ledger order so coordinator self-targets still emit
|
|
55
|
+
// task_completed and pendingCoordinatorEvents.
|
|
56
|
+
const entries = readLedgerEntries(meshId, { tail: 200 });
|
|
57
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
58
|
+
const entry = entries[i];
|
|
59
|
+
if (entry.sessionId !== sessionId) continue;
|
|
60
|
+
if (entry.kind === 'task_completed' || entry.kind === 'task_failed' || entry.kind === 'task_stalled') {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
if (entry.kind === 'task_dispatched' && entry.payload?.source === 'direct') {
|
|
64
|
+
return true;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
return false;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function findDirectDispatchLedgerEntry(args: {
|
|
71
|
+
meshId: string;
|
|
72
|
+
taskId: string;
|
|
73
|
+
sessionId?: string;
|
|
74
|
+
}): { id: string; timestamp: string; nodeId?: string; sessionId?: string; providerType?: string; payload: Record<string, unknown> } | null {
|
|
75
|
+
const entries = readLedgerEntries(args.meshId, { tail: 500 });
|
|
76
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
77
|
+
const entry = entries[i];
|
|
78
|
+
if (entry.kind !== 'task_dispatched') continue;
|
|
79
|
+
const payloadTaskId = readNonEmptyString(entry.payload?.taskId);
|
|
80
|
+
if (payloadTaskId !== args.taskId) continue;
|
|
81
|
+
if (args.sessionId && entry.sessionId && entry.sessionId !== args.sessionId) continue;
|
|
82
|
+
return {
|
|
83
|
+
id: entry.id,
|
|
84
|
+
timestamp: entry.timestamp,
|
|
85
|
+
nodeId: entry.nodeId,
|
|
86
|
+
sessionId: entry.sessionId,
|
|
87
|
+
providerType: entry.providerType,
|
|
88
|
+
payload: entry.payload || {},
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
return null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function hasTerminalLedgerAfterDispatch(args: {
|
|
95
|
+
meshId: string;
|
|
96
|
+
taskId: string;
|
|
97
|
+
sessionId?: string;
|
|
98
|
+
dispatchEntryId?: string;
|
|
99
|
+
dispatchTimestamp?: string;
|
|
100
|
+
}): boolean {
|
|
101
|
+
const entries = readLedgerEntries(args.meshId, { tail: 500 });
|
|
102
|
+
let afterDispatch = !args.dispatchEntryId && !args.dispatchTimestamp;
|
|
103
|
+
const dispatchTime = args.dispatchTimestamp ? new Date(args.dispatchTimestamp).getTime() : Number.NaN;
|
|
104
|
+
for (const entry of entries) {
|
|
105
|
+
if (!afterDispatch) {
|
|
106
|
+
if (args.dispatchEntryId && entry.id === args.dispatchEntryId) {
|
|
107
|
+
afterDispatch = true;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (!args.dispatchEntryId && Number.isFinite(dispatchTime)) {
|
|
111
|
+
const entryTime = new Date(entry.timestamp).getTime();
|
|
112
|
+
if (Number.isFinite(entryTime) && entryTime >= dispatchTime) afterDispatch = true;
|
|
113
|
+
}
|
|
114
|
+
if (!afterDispatch) continue;
|
|
115
|
+
}
|
|
116
|
+
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
|
|
117
|
+
const terminalTaskId = readNonEmptyString(entry.payload?.taskId);
|
|
118
|
+
if (terminalTaskId && terminalTaskId === args.taskId) return true;
|
|
119
|
+
if (terminalTaskId && terminalTaskId !== args.taskId) continue;
|
|
120
|
+
if (args.sessionId && entry.sessionId === args.sessionId) return true;
|
|
121
|
+
}
|
|
122
|
+
return false;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
126
|
+
meshId: string;
|
|
127
|
+
nodeId?: string;
|
|
128
|
+
sessionId: string;
|
|
129
|
+
providerType?: string;
|
|
130
|
+
providerSessionId?: string;
|
|
131
|
+
taskId: string;
|
|
132
|
+
finalSummary: string;
|
|
133
|
+
transcriptMessageAt?: string;
|
|
134
|
+
completedAt?: string;
|
|
135
|
+
targetCoordinatorDaemonId?: string;
|
|
136
|
+
source?: string;
|
|
137
|
+
}): { reconciled: boolean; kind?: MeshLedgerKind; alreadyTerminal?: boolean; workerResult?: unknown; ledgerEntryId?: string; reason?: string } {
|
|
138
|
+
const finalSummary = readNonEmptyString(args.finalSummary);
|
|
139
|
+
if (!args.meshId || !args.taskId || !args.sessionId || !finalSummary) {
|
|
140
|
+
return { reconciled: false, reason: 'missing_required_completion_evidence' };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
const dispatch = findDirectDispatchLedgerEntry({
|
|
144
|
+
meshId: args.meshId,
|
|
145
|
+
taskId: args.taskId,
|
|
146
|
+
sessionId: args.sessionId,
|
|
147
|
+
});
|
|
148
|
+
if (hasTerminalLedgerAfterDispatch({
|
|
149
|
+
meshId: args.meshId,
|
|
150
|
+
taskId: args.taskId,
|
|
151
|
+
sessionId: args.sessionId,
|
|
152
|
+
dispatchEntryId: dispatch?.id,
|
|
153
|
+
dispatchTimestamp: dispatch?.timestamp,
|
|
154
|
+
})) {
|
|
155
|
+
return { reconciled: false, alreadyTerminal: true, reason: 'terminal_ledger_entry_exists' };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const nodeId = readNonEmptyString(args.nodeId) || dispatch?.nodeId;
|
|
159
|
+
const providerType = readNonEmptyString(args.providerType) || dispatch?.providerType || readNonEmptyString(dispatch?.payload.providerType);
|
|
160
|
+
const completedAt = args.completedAt || new Date().toISOString();
|
|
161
|
+
const evidence = buildTaskCompletionEvidence({
|
|
162
|
+
event: 'agent:generating_completed',
|
|
163
|
+
nodeId: nodeId || 'unknown',
|
|
164
|
+
sessionId: args.sessionId,
|
|
165
|
+
providerType,
|
|
166
|
+
providerSessionId: readNonEmptyString(args.providerSessionId),
|
|
167
|
+
finalSummary,
|
|
168
|
+
completedAt,
|
|
169
|
+
});
|
|
170
|
+
const workerResult = evidence.workerResult;
|
|
171
|
+
const dispatchTime = dispatch?.timestamp ? new Date(dispatch.timestamp).getTime() : Number.NaN;
|
|
172
|
+
const transcriptTime = args.transcriptMessageAt ? new Date(args.transcriptMessageAt).getTime() : Number.NaN;
|
|
173
|
+
const transcriptAfterDispatch = Number.isFinite(dispatchTime) && Number.isFinite(transcriptTime) && transcriptTime >= dispatchTime;
|
|
174
|
+
if (workerResult.source !== 'final_summary_json' && !transcriptAfterDispatch) {
|
|
175
|
+
return { reconciled: false, reason: 'transcript_not_proven_after_dispatch' };
|
|
176
|
+
}
|
|
177
|
+
const workerFailed = workerResult.status === 'failed' || (workerResult.status !== 'completed' && workerResult.errors.length > 0);
|
|
178
|
+
const kind: MeshLedgerKind = workerFailed ? 'task_failed' : 'task_completed';
|
|
179
|
+
|
|
180
|
+
const entry = appendLedgerEntry(args.meshId, {
|
|
181
|
+
kind,
|
|
182
|
+
nodeId: nodeId || undefined,
|
|
183
|
+
sessionId: args.sessionId,
|
|
184
|
+
providerType: providerType || undefined,
|
|
185
|
+
payload: {
|
|
186
|
+
event: 'agent:generating_completed',
|
|
187
|
+
source: args.source || 'direct_task_transcript_reconciliation',
|
|
188
|
+
taskId: args.taskId,
|
|
189
|
+
providerSessionId: readNonEmptyString(args.providerSessionId),
|
|
190
|
+
finalSummary,
|
|
191
|
+
workerResult,
|
|
192
|
+
completionDiagnostic: {
|
|
193
|
+
reason: 'direct_task_transcript_reconciliation',
|
|
194
|
+
dispatchEntryId: dispatch?.id,
|
|
195
|
+
dispatchTimestamp: dispatch?.timestamp,
|
|
196
|
+
transcriptMessageAt: readNonEmptyString(args.transcriptMessageAt),
|
|
197
|
+
transcriptFinalAssistantPresent: true,
|
|
198
|
+
},
|
|
199
|
+
evidence,
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
updateDirectDispatchStatus(args.meshId, args.sessionId, kind === 'task_completed' ? 'completed' : 'failed');
|
|
203
|
+
markSessionDeliveriesTerminal(args.meshId, args.sessionId, kind === 'task_completed' ? 'completed' : 'failed');
|
|
204
|
+
setImmediate(() => cleanupTerminalDirectDispatches());
|
|
205
|
+
queuePendingMeshCoordinatorEvent({
|
|
206
|
+
event: kind === 'task_completed' ? 'agent:generating_completed' : 'agent:stopped',
|
|
207
|
+
meshId: args.meshId,
|
|
208
|
+
nodeLabel: nodeId ? `Node '${nodeId}'` : 'Remote agent',
|
|
209
|
+
nodeId: nodeId || undefined,
|
|
210
|
+
metadataEvent: {
|
|
211
|
+
targetSessionId: args.sessionId,
|
|
212
|
+
providerType: providerType || undefined,
|
|
213
|
+
providerSessionId: readNonEmptyString(args.providerSessionId),
|
|
214
|
+
finalSummary,
|
|
215
|
+
taskId: args.taskId,
|
|
216
|
+
workerResult,
|
|
217
|
+
completionDiagnostic: {
|
|
218
|
+
reason: 'direct_task_transcript_reconciliation',
|
|
219
|
+
terminalLedgerKind: kind,
|
|
220
|
+
terminalLedgerId: entry.id,
|
|
221
|
+
},
|
|
222
|
+
},
|
|
223
|
+
coordinatorMessage: undefined,
|
|
224
|
+
queuedAt: Date.now(),
|
|
225
|
+
...(readNonEmptyString(args.targetCoordinatorDaemonId) ? { targetCoordinatorDaemonId: readNonEmptyString(args.targetCoordinatorDaemonId) } : {}),
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
return { reconciled: true, kind, workerResult, ledgerEntryId: entry.id };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function buildLongGeneratingCompletionReconciliation(args: {
|
|
232
|
+
meshId: string;
|
|
233
|
+
nodeId?: string;
|
|
234
|
+
nodeLabel: string;
|
|
235
|
+
metadataEvent: Record<string, unknown>;
|
|
236
|
+
sourceInstanceId?: string;
|
|
237
|
+
}): Record<string, unknown> | null {
|
|
238
|
+
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
239
|
+
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
240
|
+
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
241
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
|
|
242
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
243
|
+
const completionDiagnostic = readRecord(args.metadataEvent.completionDiagnostic);
|
|
244
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
|
|
245
|
+
const status = readNonEmptyString(args.metadataEvent.status).toLowerCase();
|
|
246
|
+
const explicitCompletionEvidence = Boolean(
|
|
247
|
+
finalSummary
|
|
248
|
+
|| workerResult
|
|
249
|
+
|| completionDiagnostic?.finalAssistantPresent === true
|
|
250
|
+
|| status === 'idle'
|
|
251
|
+
|| status === 'ready'
|
|
252
|
+
|| status === 'completed',
|
|
253
|
+
);
|
|
254
|
+
if (explicitCompletionEvidence) {
|
|
255
|
+
return {
|
|
256
|
+
...args.metadataEvent,
|
|
257
|
+
targetSessionId: sessionId,
|
|
258
|
+
providerType,
|
|
259
|
+
providerSessionId,
|
|
260
|
+
finalSummary,
|
|
261
|
+
source: 'long_generating_reconciliation',
|
|
262
|
+
reconciledFromEvent: 'monitor:long_generating',
|
|
263
|
+
timestamp: args.metadataEvent.timestamp ?? Date.now(),
|
|
264
|
+
completionDiagnostic: {
|
|
265
|
+
...(completionDiagnostic || {}),
|
|
266
|
+
reconciliationReason: 'provider_completion_evidence',
|
|
267
|
+
},
|
|
268
|
+
};
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const terminal = findRecentTerminalLedgerEvidence({
|
|
272
|
+
meshId: args.meshId,
|
|
273
|
+
sessionId: sessionId || undefined,
|
|
274
|
+
nodeId: nodeId || undefined,
|
|
275
|
+
});
|
|
276
|
+
if (!terminal) return null;
|
|
277
|
+
return {
|
|
278
|
+
...args.metadataEvent,
|
|
279
|
+
source: 'long_generating_terminal_ledger_suppression',
|
|
280
|
+
terminalLedgerKind: terminal.kind,
|
|
281
|
+
terminalLedgerAt: terminal.timestamp,
|
|
282
|
+
};
|
|
283
|
+
}
|