@adhdev/daemon-core 0.9.82-rc.19 → 0.9.82-rc.190
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 +22 -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/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 +26 -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 +7 -1
- package/dist/git/git-types.d.ts +2 -0
- package/dist/index.d.ts +37 -10
- package/dist/index.js +32814 -16395
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +32529 -16180
- 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/beads-db.d.ts +73 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +30 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +90 -0
- package/dist/mesh/mesh-events.d.ts +143 -6
- 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.d.ts +58 -1
- package/dist/mesh/mesh-refine-status.d.ts +26 -0
- package/dist/mesh/mesh-work-queue.d.ts +55 -6
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +193 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +113 -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 +12 -0
- 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 +60 -0
- package/dist/providers/spec/cli-adapter.d.ts +98 -0
- package/dist/providers/spec/driver.d.ts +168 -0
- package/dist/providers/spec/evaluator.d.ts +55 -0
- package/dist/providers/spec/loader.d.ts +14 -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 +549 -0
- package/dist/providers/spec/types.d.ts +237 -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 +108 -1
- package/dist/sessions/registry.d.ts +3 -0
- package/dist/shared-types.d.ts +42 -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 -2
- package/src/agent-stream/poller.ts +2 -3
- package/src/boot/daemon-lifecycle.ts +19 -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 +21 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1086 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +670 -1137
- 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 +17 -1
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +8 -1
- 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 +331 -122
- package/src/commands/router.ts +3600 -510
- 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-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +37 -7
- 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 +115 -9
- 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/beads-db.ts +490 -0
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +204 -30
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +437 -0
- package/src/mesh/mesh-events.ts +1234 -71
- package/src/mesh/mesh-fast-forward.ts +438 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +457 -104
- package/src/mesh/mesh-refine-status.ts +144 -0
- package/src/mesh/mesh-work-queue.ts +267 -159
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +366 -0
- package/src/mesh/worktree-bootstrap-config.ts +247 -0
- 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 +763 -60
- 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 +9 -0
- 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 +176 -0
- package/src/providers/spec/cli-adapter.ts +534 -0
- package/src/providers/spec/driver.ts +612 -0
- package/src/providers/spec/evaluator.ts +344 -0
- package/src/providers/spec/loader.ts +130 -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 +538 -0
- package/src/providers/spec/schema.json +235 -0
- package/src/providers/spec/types.ts +256 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +408 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +118 -1
- package/src/sessions/registry.ts +3 -0
- package/src/shared-types.ts +45 -1
- package/src/status/builders.ts +24 -6
- 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/mesh/mesh-sync.d.ts +0 -53
- package/src/mesh/mesh-sync.ts +0 -111
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -1,31 +1,76 @@
|
|
|
1
|
+
import { appendFileSync, existsSync, readFileSync, renameSync, statSync, unlinkSync, writeFileSync } from 'fs';
|
|
2
|
+
import { join } from 'path';
|
|
1
3
|
import type { DaemonComponents } from '../boot/daemon-lifecycle.js';
|
|
2
4
|
import { loadConfig } from '../config/config.js';
|
|
3
5
|
import { getMesh, getMeshByRepo } from '../config/mesh-config.js';
|
|
4
6
|
import { detectCLI } from '../detection/cli-detector.js';
|
|
5
7
|
import { LOG } from '../logging/logger.js';
|
|
6
|
-
import { appendLedgerEntry, buildTaskCompletionEvidence, getSessionRecoveryContext, isIntentionalCleanupStopEntry, readLedgerEntries } from './mesh-ledger.js';
|
|
8
|
+
import { appendLedgerEntry, buildTaskCompletionEvidence, getLedgerDir, getSessionRecoveryContext, isIntentionalCleanupStopEntry, readLedgerEntries } from './mesh-ledger.js';
|
|
7
9
|
import type { MeshLedgerKind, SessionRecoveryContext } from './mesh-ledger.js';
|
|
8
|
-
import { claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus, getQueue, recordTaskAutoLaunch } from './mesh-work-queue.js';
|
|
10
|
+
import { buildMeshNodeCapabilityTags, claimNextTask, updateSessionTaskStatus, enqueueTask, updateTaskStatus, getQueue, recordTaskAutoLaunch, updateDirectDispatchStatus, cleanupTerminalDirectDispatches, getActiveDirectDispatches } from './mesh-work-queue.js';
|
|
11
|
+
import { BeadsDB } from './beads-db.js';
|
|
12
|
+
import { fastForwardMeshNode } from './mesh-fast-forward.js';
|
|
9
13
|
|
|
10
14
|
// ---------------------------------------------------------------------------
|
|
11
15
|
// Remote Node Idle Session Tracking
|
|
12
16
|
// ---------------------------------------------------------------------------
|
|
13
|
-
// Tracks remote sessions that emitted 'agent:ready' so triggerMeshQueue
|
|
14
|
-
// can assign tasks to them.
|
|
17
|
+
// Tracks remote sessions that emitted 'agent:ready' so triggerMeshQueue
|
|
18
|
+
// can assign tasks to them. Each entry carries an expiresAt timestamp;
|
|
19
|
+
// entries are swept on insertion to prevent unbounded growth.
|
|
15
20
|
// ---------------------------------------------------------------------------
|
|
16
21
|
interface RemoteIdleSession {
|
|
17
22
|
nodeId: string;
|
|
18
23
|
sessionId: string;
|
|
19
24
|
providerType: string;
|
|
25
|
+
expiresAt: number;
|
|
20
26
|
}
|
|
27
|
+
const REMOTE_IDLE_SESSION_TTL_MS = 5 * 60 * 1000; // 5 minutes
|
|
21
28
|
const remoteIdleSessions = new Map<string, RemoteIdleSession>(); // key: `${nodeId}:${sessionId}`
|
|
22
29
|
|
|
23
30
|
// ---------------------------------------------------------------------------
|
|
24
|
-
//
|
|
31
|
+
// Workspace-to-mesh lookup cache
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// getMeshByRepo is called on every coordinator event for sessions without
|
|
34
|
+
// meshNodeFor. Cache results for 5 seconds to avoid repeated config reads.
|
|
35
|
+
const meshByWorkspaceCache = new Map<string, { mesh: any; cachedAt: number }>();
|
|
36
|
+
const MESH_WORKSPACE_CACHE_TTL_MS = 5_000;
|
|
37
|
+
const IDLE_AUTO_FAST_FORWARD_THROTTLE_MS = 30 * 60 * 1000;
|
|
38
|
+
const idleAutoFastForwardLastAttempt = new Map<string, number>();
|
|
39
|
+
|
|
40
|
+
function getCachedMeshByWorkspace(workspace: string): any {
|
|
41
|
+
const now = Date.now();
|
|
42
|
+
const cached = meshByWorkspaceCache.get(workspace);
|
|
43
|
+
if (cached && now - cached.cachedAt < MESH_WORKSPACE_CACHE_TTL_MS) return cached.mesh;
|
|
44
|
+
const mesh = getMeshByRepo(workspace);
|
|
45
|
+
meshByWorkspaceCache.set(workspace, { mesh, cachedAt: now });
|
|
46
|
+
return mesh;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function readWorkerResultMetadata(event: Record<string, unknown>): Record<string, unknown> | undefined {
|
|
50
|
+
return readRecord(event.workerResult) || readRecord(event.meshWorkerResult) || readRecord(event.structuredResult);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export function __resetIdleAutoFastForwardForTests(): void {
|
|
54
|
+
idleAutoFastForwardLastAttempt.clear();
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function sweepExpiredRemoteIdleSessions(): void {
|
|
58
|
+
const now = Date.now();
|
|
59
|
+
for (const [key, session] of remoteIdleSessions) {
|
|
60
|
+
if (session.expiresAt <= now) remoteIdleSessions.delete(key);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// MCP coordinator pending-event queue — FILE-BASED PERSISTENCE
|
|
25
66
|
// ---------------------------------------------------------------------------
|
|
26
67
|
// When a mesh event fires but no CLI coordinator session is registered (e.g.
|
|
27
|
-
// the coordinator is Claude Code running via MCP), we
|
|
28
|
-
//
|
|
68
|
+
// the coordinator is Claude Code running via MCP), we persist the event to a
|
|
69
|
+
// per-mesh JSONL file so it survives daemon restarts. The 50-entry hard cap
|
|
70
|
+
// is removed; the file is drained atomically on each get_pending_mesh_events
|
|
71
|
+
// call and limited to 100 KB to prevent runaway growth.
|
|
72
|
+
//
|
|
73
|
+
// File: <ledgerDir>/<meshId>.pending-events.jsonl
|
|
29
74
|
// ---------------------------------------------------------------------------
|
|
30
75
|
|
|
31
76
|
export interface PendingMeshCoordinatorEvent {
|
|
@@ -35,39 +80,272 @@ export interface PendingMeshCoordinatorEvent {
|
|
|
35
80
|
nodeId?: string;
|
|
36
81
|
workspace?: string;
|
|
37
82
|
metadataEvent: Record<string, unknown>;
|
|
83
|
+
coordinatorMessage?: string;
|
|
38
84
|
queuedAt: number;
|
|
85
|
+
/**
|
|
86
|
+
* When set, this event is intended for a specific coordinator daemon.
|
|
87
|
+
* Coordinators on other daemons should ignore it during drain.
|
|
88
|
+
* Absent on legacy events — treated as broadcast to any coordinator.
|
|
89
|
+
*/
|
|
90
|
+
targetCoordinatorDaemonId?: string;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
const REFINE_TERMINAL_EVENTS = new Set(['refine:completed', 'refine:failed']);
|
|
94
|
+
|
|
95
|
+
function readRefineJobId(event: { metadataEvent?: Record<string, unknown> } | Record<string, unknown>): string {
|
|
96
|
+
const metadata = readRecord((event as any).metadataEvent) || event as Record<string, unknown>;
|
|
97
|
+
const result = readRecord(metadata.result);
|
|
98
|
+
const refineJob = readRecord(result?.refineJob);
|
|
99
|
+
return readNonEmptyString(metadata.jobId) || readNonEmptyString(refineJob?.jobId);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function buildRefineTerminalEventFingerprint(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): string {
|
|
103
|
+
const jobId = readRefineJobId({ metadataEvent });
|
|
104
|
+
return jobId && REFINE_TERMINAL_EVENTS.has(eventName) ? `${meshId}::${eventName}::${jobId}` : '';
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function hasPendingRefineTerminalEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
|
|
108
|
+
if (!REFINE_TERMINAL_EVENTS.has(event.event)) return false;
|
|
109
|
+
const jobId = readRefineJobId(event);
|
|
110
|
+
if (!jobId) return false;
|
|
111
|
+
return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) =>
|
|
112
|
+
pending.event === event.event && readRefineJobId(pending) === jobId,
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function buildPendingEventFingerprint(event: PendingMeshCoordinatorEvent): string {
|
|
117
|
+
const metadata = readRecord(event.metadataEvent) || {};
|
|
118
|
+
const sessionId = resolveEventSessionId(metadata);
|
|
119
|
+
const providerSessionId = readNonEmptyString(metadata.providerSessionId);
|
|
120
|
+
const taskId = readNonEmptyString(metadata.taskId) || readNonEmptyString(readRecord(metadata.payload)?.taskId);
|
|
121
|
+
const jobId = readRefineJobId(event);
|
|
122
|
+
const timestamp = metadata.timestamp !== undefined && metadata.timestamp !== null ? String(metadata.timestamp) : '';
|
|
123
|
+
return [
|
|
124
|
+
event.meshId,
|
|
125
|
+
event.event,
|
|
126
|
+
event.nodeId || '',
|
|
127
|
+
sessionId || '',
|
|
128
|
+
providerSessionId || '',
|
|
129
|
+
taskId || '',
|
|
130
|
+
jobId || '',
|
|
131
|
+
timestamp || '',
|
|
132
|
+
].join('::');
|
|
39
133
|
}
|
|
40
134
|
|
|
41
|
-
|
|
42
|
-
const
|
|
135
|
+
function hasPendingCoordinatorEventDuplicate(event: PendingMeshCoordinatorEvent): boolean {
|
|
136
|
+
const fingerprint = buildPendingEventFingerprint(event);
|
|
137
|
+
if (!fingerprint.trim()) return false;
|
|
138
|
+
return readPendingMeshCoordinatorEventsFromDisk(event.meshId).some((pending) => buildPendingEventFingerprint(pending) === fingerprint);
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function getPendingEventsPath(meshId: string, coordinatorDaemonId?: string): string {
|
|
142
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
143
|
+
if (coordinatorDaemonId) {
|
|
144
|
+
const safeDaemon = coordinatorDaemonId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
145
|
+
return join(getLedgerDir(), `${safe}-${safeDaemon}.pending-events.jsonl`);
|
|
146
|
+
}
|
|
147
|
+
return join(getLedgerDir(), `${safe}.pending-events.jsonl`);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function readPendingMeshCoordinatorEventsFromDisk(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
|
|
151
|
+
if (!meshId) return [];
|
|
152
|
+
// Read coordinator-scoped file first; fall back to legacy shared file.
|
|
153
|
+
const paths = coordinatorDaemonId
|
|
154
|
+
? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
|
|
155
|
+
: [getPendingEventsPath(meshId)];
|
|
156
|
+
const events: PendingMeshCoordinatorEvent[] = [];
|
|
157
|
+
for (const path of paths) {
|
|
158
|
+
if (!existsSync(path)) continue;
|
|
159
|
+
try {
|
|
160
|
+
const raw = readFileSync(path, 'utf-8');
|
|
161
|
+
const parsed = raw.split('\n').filter(Boolean).flatMap(line => {
|
|
162
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
163
|
+
});
|
|
164
|
+
// If reading the shared file, filter to events that target this coordinator or are unscoped.
|
|
165
|
+
const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
|
|
166
|
+
? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
|
|
167
|
+
: parsed;
|
|
168
|
+
events.push(...filtered);
|
|
169
|
+
} catch { /* skip unreadable files */ }
|
|
170
|
+
}
|
|
171
|
+
return events;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function refineTerminalEventFromLedger(meshId: string, pending: readonly PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
|
|
175
|
+
const acceptedJobIds = new Set(
|
|
176
|
+
pending
|
|
177
|
+
.filter(event => event.event === 'refine:accepted')
|
|
178
|
+
.map(event => readRefineJobId(event))
|
|
179
|
+
.filter(Boolean),
|
|
180
|
+
);
|
|
181
|
+
if (acceptedJobIds.size === 0) return [];
|
|
182
|
+
const existingTerminalJobIds = new Set(
|
|
183
|
+
pending
|
|
184
|
+
.filter(event => REFINE_TERMINAL_EVENTS.has(event.event))
|
|
185
|
+
.map(event => `${event.event}:${readRefineJobId(event)}`)
|
|
186
|
+
.filter(value => !value.endsWith(':')),
|
|
187
|
+
);
|
|
188
|
+
const backfilled: PendingMeshCoordinatorEvent[] = [];
|
|
189
|
+
const entries = readLedgerEntries(meshId, { tail: 200 });
|
|
190
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
191
|
+
const entry = entries[i];
|
|
192
|
+
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed') continue;
|
|
193
|
+
const payload = readRecord(entry.payload);
|
|
194
|
+
if (payload?.source !== 'refine_mesh_node_async_job') continue;
|
|
195
|
+
const refineJob = readRecord(payload.refineJob);
|
|
196
|
+
const jobId = readNonEmptyString(refineJob?.jobId);
|
|
197
|
+
if (!jobId || !acceptedJobIds.has(jobId)) continue;
|
|
198
|
+
const eventName = entry.kind === 'task_completed' ? 'refine:completed' : 'refine:failed';
|
|
199
|
+
if (existingTerminalJobIds.has(`${eventName}:${jobId}`)) continue;
|
|
200
|
+
existingTerminalJobIds.add(`${eventName}:${jobId}`);
|
|
201
|
+
const result = readRecord(payload.result);
|
|
202
|
+
const metadataEvent = {
|
|
203
|
+
source: 'refine_mesh_node_async_job',
|
|
204
|
+
jobId,
|
|
205
|
+
interactionId: readNonEmptyString(refineJob?.interactionId),
|
|
206
|
+
meshId,
|
|
207
|
+
nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
|
|
208
|
+
targetDaemonId: readNonEmptyString(refineJob?.targetDaemonId),
|
|
209
|
+
workspace: readNonEmptyString(refineJob?.workspace),
|
|
210
|
+
status: eventName === 'refine:completed' ? 'completed' : 'failed',
|
|
211
|
+
startedAt: readNonEmptyString(refineJob?.startedAt),
|
|
212
|
+
completedAt: readNonEmptyString(refineJob?.completedAt) || entry.timestamp,
|
|
213
|
+
retryOfJobId: readNonEmptyString(refineJob?.retryOfJobId) || readNonEmptyString(payload.retryOfJobId),
|
|
214
|
+
...(result ? { result } : {}),
|
|
215
|
+
};
|
|
216
|
+
backfilled.push({
|
|
217
|
+
event: eventName,
|
|
218
|
+
meshId,
|
|
219
|
+
nodeLabel: readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job',
|
|
220
|
+
nodeId: readNonEmptyString(refineJob?.nodeId) || entry.nodeId,
|
|
221
|
+
workspace: readNonEmptyString(refineJob?.workspace),
|
|
222
|
+
metadataEvent,
|
|
223
|
+
coordinatorMessage: buildMeshSystemMessage({
|
|
224
|
+
event: eventName,
|
|
225
|
+
nodeLabel: readNonEmptyString(refineJob?.nodeId) || entry.nodeId || 'refine job',
|
|
226
|
+
metadataEvent,
|
|
227
|
+
}),
|
|
228
|
+
queuedAt: Date.now(),
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
return backfilled.reverse();
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function reconcilePendingMeshCoordinatorEvents(meshId: string, events: PendingMeshCoordinatorEvent[]): PendingMeshCoordinatorEvent[] {
|
|
235
|
+
const backfilled = refineTerminalEventFromLedger(meshId, events);
|
|
236
|
+
if (backfilled.length === 0) return events;
|
|
237
|
+
const terminalJobIds = new Set(backfilled.map(event => readRefineJobId(event)).filter(Boolean));
|
|
238
|
+
return [
|
|
239
|
+
...events.filter(event => !(event.event === 'refine:accepted' && terminalJobIds.has(readRefineJobId(event)))),
|
|
240
|
+
...backfilled,
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const MAX_PENDING_EVENTS_BYTES = 100 * 1024; // 100 KB — keep the pending file small
|
|
245
|
+
const MAX_PENDING_EVENTS_KEEP = 50; // keep the last 50 events when trimming
|
|
246
|
+
|
|
247
|
+
function trimPendingEventsIfNeeded(path: string): void {
|
|
248
|
+
try {
|
|
249
|
+
if (!existsSync(path)) return;
|
|
250
|
+
if (statSync(path).size <= MAX_PENDING_EVENTS_BYTES) return;
|
|
251
|
+
const lines = readFileSync(path, 'utf-8').split('\n').filter(Boolean);
|
|
252
|
+
if (lines.length <= MAX_PENDING_EVENTS_KEEP) return;
|
|
253
|
+
writeFileSync(path, lines.slice(-MAX_PENDING_EVENTS_KEEP).join('\n') + '\n', 'utf-8');
|
|
254
|
+
} catch { /* best-effort; if trim fails, append still proceeds */ }
|
|
255
|
+
}
|
|
43
256
|
|
|
44
257
|
export function queuePendingMeshCoordinatorEvent(event: PendingMeshCoordinatorEvent): boolean {
|
|
45
|
-
|
|
258
|
+
try {
|
|
259
|
+
if (hasPendingRefineTerminalEventDuplicate(event)) {
|
|
260
|
+
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for refine job ${readRefineJobId(event)}`);
|
|
261
|
+
return true;
|
|
262
|
+
}
|
|
263
|
+
if (hasPendingCoordinatorEventDuplicate(event)) {
|
|
264
|
+
LOG.info('MeshEvents', `Suppressed duplicate pending ${event.event} for mesh ${event.meshId}`);
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
267
|
+
// Write to the coordinator-scoped file when the target coordinator is known;
|
|
268
|
+
// fall back to the shared file for legacy/unscoped events.
|
|
269
|
+
const path = getPendingEventsPath(event.meshId, event.targetCoordinatorDaemonId);
|
|
270
|
+
trimPendingEventsIfNeeded(path);
|
|
271
|
+
appendFileSync(path, JSON.stringify(event) + '\n', 'utf-8');
|
|
272
|
+
return true;
|
|
273
|
+
} catch (e: any) {
|
|
274
|
+
LOG.warn('MeshEvents', `Failed to persist pending coordinator event: ${e?.message || e}`);
|
|
46
275
|
return false;
|
|
47
276
|
}
|
|
48
|
-
pendingMeshCoordinatorEvents.push(event);
|
|
49
|
-
return true;
|
|
50
277
|
}
|
|
51
278
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
279
|
+
// Atomically rename the file before reading so concurrent drains can't both consume
|
|
280
|
+
// the same events. renameSync is atomic on POSIX (same filesystem); only one caller
|
|
281
|
+
// wins the rename — the other gets ENOENT and returns null, preventing duplicate delivery.
|
|
282
|
+
function atomicDrainFile(path: string): string | null {
|
|
283
|
+
const tmpPath = `${path}.draining`;
|
|
284
|
+
try {
|
|
285
|
+
renameSync(path, tmpPath);
|
|
286
|
+
} catch {
|
|
287
|
+
return null; // another drain already renamed it, or file doesn't exist
|
|
288
|
+
}
|
|
289
|
+
try {
|
|
290
|
+
const content = readFileSync(tmpPath, 'utf-8');
|
|
291
|
+
try { unlinkSync(tmpPath); } catch { /* already cleaned up */ }
|
|
292
|
+
return content;
|
|
293
|
+
} catch {
|
|
294
|
+
try { unlinkSync(tmpPath); } catch { /* best-effort cleanup */ }
|
|
295
|
+
return null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/** Drain and return all pending coordinator events for meshId, removing them from disk. */
|
|
300
|
+
export function drainPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): PendingMeshCoordinatorEvent[] {
|
|
301
|
+
if (!meshId) return [];
|
|
302
|
+
const paths = coordinatorDaemonId
|
|
303
|
+
? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
|
|
304
|
+
: [getPendingEventsPath(meshId)];
|
|
305
|
+
const all: PendingMeshCoordinatorEvent[] = [];
|
|
306
|
+
for (const path of paths) {
|
|
307
|
+
const content = atomicDrainFile(path);
|
|
308
|
+
if (!content) continue;
|
|
309
|
+
const parsed = content.split('\n').filter(Boolean).flatMap(line => {
|
|
310
|
+
try { return [JSON.parse(line) as PendingMeshCoordinatorEvent]; } catch { return []; }
|
|
311
|
+
});
|
|
312
|
+
// If reading the shared file, filter to events that target this coordinator or are unscoped.
|
|
313
|
+
const filtered = (coordinatorDaemonId && path === getPendingEventsPath(meshId))
|
|
314
|
+
? parsed.filter(e => !e.targetCoordinatorDaemonId || e.targetCoordinatorDaemonId === coordinatorDaemonId)
|
|
315
|
+
: parsed;
|
|
316
|
+
all.push(...filtered);
|
|
317
|
+
}
|
|
318
|
+
if (all.length === 0) return [];
|
|
319
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, all);
|
|
55
320
|
}
|
|
56
321
|
|
|
57
322
|
/** Peek at pending coordinator events without draining (non-destructive). */
|
|
58
|
-
export function getPendingMeshCoordinatorEvents(): readonly PendingMeshCoordinatorEvent[] {
|
|
59
|
-
|
|
323
|
+
export function getPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): readonly PendingMeshCoordinatorEvent[] {
|
|
324
|
+
if (!meshId) return [];
|
|
325
|
+
return reconcilePendingMeshCoordinatorEvents(meshId, readPendingMeshCoordinatorEventsFromDisk(meshId, coordinatorDaemonId));
|
|
60
326
|
}
|
|
61
327
|
|
|
62
|
-
/** Explicitly clear all pending coordinator events. */
|
|
63
|
-
export function clearPendingMeshCoordinatorEvents(): void {
|
|
64
|
-
|
|
328
|
+
/** Explicitly clear all pending coordinator events for a mesh (and coordinator if scoped). */
|
|
329
|
+
export function clearPendingMeshCoordinatorEvents(meshId?: string, coordinatorDaemonId?: string): void {
|
|
330
|
+
if (!meshId) return;
|
|
331
|
+
const paths = coordinatorDaemonId
|
|
332
|
+
? [getPendingEventsPath(meshId, coordinatorDaemonId), getPendingEventsPath(meshId)]
|
|
333
|
+
: [getPendingEventsPath(meshId)];
|
|
334
|
+
for (const path of paths) {
|
|
335
|
+
if (existsSync(path)) try { unlinkSync(path); } catch { /* already removed */ }
|
|
336
|
+
}
|
|
65
337
|
}
|
|
66
338
|
|
|
67
339
|
function readNonEmptyString(value: unknown): string {
|
|
68
340
|
return typeof value === 'string' && value.trim() ? value.trim() : '';
|
|
69
341
|
}
|
|
70
342
|
|
|
343
|
+
function readRecord(value: unknown): Record<string, unknown> | undefined {
|
|
344
|
+
return value && typeof value === 'object' && !Array.isArray(value)
|
|
345
|
+
? value as Record<string, unknown>
|
|
346
|
+
: undefined;
|
|
347
|
+
}
|
|
348
|
+
|
|
71
349
|
function resolveEventSessionId(event: Record<string, unknown>, fallback?: unknown): string {
|
|
72
350
|
return readNonEmptyString(event.targetSessionId)
|
|
73
351
|
|| readNonEmptyString(event.sessionId)
|
|
@@ -82,6 +360,9 @@ const MESH_COORDINATOR_EVENTS = new Set([
|
|
|
82
360
|
'agent:stopped',
|
|
83
361
|
'agent:ready',
|
|
84
362
|
'monitor:long_generating',
|
|
363
|
+
'refine:accepted',
|
|
364
|
+
'refine:completed',
|
|
365
|
+
'refine:failed',
|
|
85
366
|
]);
|
|
86
367
|
|
|
87
368
|
const EVENT_TO_LEDGER_KIND: Record<string, MeshLedgerKind> = {
|
|
@@ -96,10 +377,21 @@ function isMeshCoordinatorEvent(eventName: unknown): eventName is string {
|
|
|
96
377
|
}
|
|
97
378
|
|
|
98
379
|
function formatCompletionMetadata(event: Record<string, unknown>): string {
|
|
380
|
+
const completionDiagnostic = event.completionDiagnostic && typeof event.completionDiagnostic === 'object'
|
|
381
|
+
? event.completionDiagnostic as Record<string, unknown>
|
|
382
|
+
: null;
|
|
383
|
+
const diagnosticReason = completionDiagnostic
|
|
384
|
+
? readNonEmptyString(completionDiagnostic.blockReason) || 'present'
|
|
385
|
+
: '';
|
|
386
|
+
const finalAssistantPresent = typeof completionDiagnostic?.finalAssistantPresent === 'boolean'
|
|
387
|
+
? String(completionDiagnostic.finalAssistantPresent)
|
|
388
|
+
: '';
|
|
99
389
|
const parts = [
|
|
100
390
|
readNonEmptyString(event.targetSessionId) ? `session_id=${readNonEmptyString(event.targetSessionId)}` : '',
|
|
101
391
|
readNonEmptyString(event.providerType) ? `provider=${readNonEmptyString(event.providerType)}` : '',
|
|
102
392
|
readNonEmptyString(event.providerSessionId) ? `provider_session_id=${readNonEmptyString(event.providerSessionId)}` : '',
|
|
393
|
+
diagnosticReason ? `completion_diagnostic=${diagnosticReason}` : '',
|
|
394
|
+
finalAssistantPresent ? `final_assistant=${finalAssistantPresent}` : '',
|
|
103
395
|
].filter(Boolean);
|
|
104
396
|
return parts.length > 0 ? ` (${parts.join('; ')})` : '';
|
|
105
397
|
}
|
|
@@ -126,7 +418,7 @@ function isIntentionalCleanupStopMetadata(event: Record<string, unknown>): boole
|
|
|
126
418
|
function hasRecentIntentionalCleanupStop(meshId: string, sessionId?: string, nodeId?: string): boolean {
|
|
127
419
|
if (!sessionId && !nodeId) return false;
|
|
128
420
|
const cutoff = Date.now() - INTENTIONAL_CLEANUP_STOP_SUPPRESSION_MS;
|
|
129
|
-
const entries = readLedgerEntries(meshId);
|
|
421
|
+
const entries = readLedgerEntries(meshId, { tail: 200 });
|
|
130
422
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
131
423
|
const entry = entries[i];
|
|
132
424
|
const timestamp = new Date(entry.timestamp).getTime();
|
|
@@ -150,6 +442,386 @@ function shouldSuppressIntentionalCleanupStop(args: {
|
|
|
150
442
|
return hasRecentIntentionalCleanupStop(args.meshId, args.sessionId, args.nodeId);
|
|
151
443
|
}
|
|
152
444
|
|
|
445
|
+
const RECENT_COMPLETION_FINGERPRINT_TTL_MS = 10 * 60 * 1000;
|
|
446
|
+
|
|
447
|
+
function hasFingerprintSeen(fingerprint: string): boolean {
|
|
448
|
+
try {
|
|
449
|
+
return BeadsDB.getInstance().hasCompletionFingerprint(fingerprint);
|
|
450
|
+
} catch {
|
|
451
|
+
return false;
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
function recordFingerprintSeen(fingerprint: string): void {
|
|
456
|
+
try {
|
|
457
|
+
const db = BeadsDB.getInstance();
|
|
458
|
+
db.recordCompletionFingerprint(fingerprint, RECENT_COMPLETION_FINGERPRINT_TTL_MS);
|
|
459
|
+
db.sweepExpiredFingerprints();
|
|
460
|
+
} catch { /* best-effort; duplicate events are preferable to a crash */ }
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function readEventTimestamp(value: unknown): number | null {
|
|
464
|
+
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
|
465
|
+
if (typeof value === 'string' && value.trim()) {
|
|
466
|
+
const numeric = Number(value);
|
|
467
|
+
if (Number.isFinite(numeric)) return numeric;
|
|
468
|
+
const parsed = Date.parse(value);
|
|
469
|
+
if (Number.isFinite(parsed)) return parsed;
|
|
470
|
+
}
|
|
471
|
+
return null;
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
function buildMeshCompletionFingerprint(args: {
|
|
475
|
+
meshId: string;
|
|
476
|
+
event: string;
|
|
477
|
+
sessionId: string;
|
|
478
|
+
providerType?: string;
|
|
479
|
+
providerSessionId?: string;
|
|
480
|
+
timestamp?: number | null;
|
|
481
|
+
finalSummary?: string;
|
|
482
|
+
/** When set, scopes the fingerprint to a specific coordinator daemon so
|
|
483
|
+
* two coordinators processing events from their respective workers don't
|
|
484
|
+
* suppress each other's completion events. */
|
|
485
|
+
coordinatorDaemonId?: string;
|
|
486
|
+
}): string {
|
|
487
|
+
const timestampPart = Number.isFinite(args.timestamp)
|
|
488
|
+
? String(args.timestamp)
|
|
489
|
+
: readNonEmptyString(args.finalSummary).slice(0, 200);
|
|
490
|
+
return [
|
|
491
|
+
args.meshId,
|
|
492
|
+
args.event,
|
|
493
|
+
args.sessionId,
|
|
494
|
+
args.providerType || '',
|
|
495
|
+
args.providerSessionId || '',
|
|
496
|
+
timestampPart,
|
|
497
|
+
args.coordinatorDaemonId || '',
|
|
498
|
+
].join('::');
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
function isDuplicateMeshCompletionEvent(args: {
|
|
502
|
+
meshId: string;
|
|
503
|
+
event: string;
|
|
504
|
+
sessionId: string;
|
|
505
|
+
providerType?: string;
|
|
506
|
+
providerSessionId?: string;
|
|
507
|
+
timestamp?: number | null;
|
|
508
|
+
finalSummary?: string;
|
|
509
|
+
coordinatorDaemonId?: string;
|
|
510
|
+
}): boolean {
|
|
511
|
+
const fingerprint = buildMeshCompletionFingerprint(args);
|
|
512
|
+
if (!fingerprint) return false;
|
|
513
|
+
if (hasFingerprintSeen(fingerprint)) return true;
|
|
514
|
+
recordFingerprintSeen(fingerprint);
|
|
515
|
+
return false;
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
function isDuplicateMeshApprovalEvent(args: {
|
|
519
|
+
meshId: string;
|
|
520
|
+
sessionId: string;
|
|
521
|
+
providerType?: string;
|
|
522
|
+
timestamp?: number | null;
|
|
523
|
+
modalMessage?: string;
|
|
524
|
+
modalButtons?: unknown;
|
|
525
|
+
}): boolean {
|
|
526
|
+
const modalButtons = Array.isArray(args.modalButtons)
|
|
527
|
+
? args.modalButtons.map(button => String(button).trim()).filter(Boolean)
|
|
528
|
+
: [];
|
|
529
|
+
const approvalIdentity = Number.isFinite(args.timestamp)
|
|
530
|
+
? String(args.timestamp)
|
|
531
|
+
: JSON.stringify({ message: args.modalMessage || '', buttons: modalButtons });
|
|
532
|
+
if (!approvalIdentity || approvalIdentity === '{"message":"","buttons":[]}') return false;
|
|
533
|
+
const fingerprint = [
|
|
534
|
+
args.meshId,
|
|
535
|
+
'agent:waiting_approval',
|
|
536
|
+
args.sessionId,
|
|
537
|
+
args.providerType || '',
|
|
538
|
+
approvalIdentity,
|
|
539
|
+
].join('::');
|
|
540
|
+
if (hasFingerprintSeen(fingerprint)) return true;
|
|
541
|
+
recordFingerprintSeen(fingerprint);
|
|
542
|
+
return false;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function isDuplicateRefineTerminalEvent(meshId: string, eventName: string, metadataEvent: Record<string, unknown>): boolean {
|
|
546
|
+
const fingerprint = buildRefineTerminalEventFingerprint(meshId, eventName, metadataEvent);
|
|
547
|
+
if (!fingerprint) return false;
|
|
548
|
+
if (hasFingerprintSeen(fingerprint)) return true;
|
|
549
|
+
recordFingerprintSeen(fingerprint);
|
|
550
|
+
return false;
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
function findRecentTerminalLedgerEvidence(args: {
|
|
554
|
+
meshId: string;
|
|
555
|
+
sessionId?: string;
|
|
556
|
+
nodeId?: string;
|
|
557
|
+
}): { id: string; kind: MeshLedgerKind; payload: Record<string, unknown>; timestamp: string } | null {
|
|
558
|
+
if (!args.sessionId && !args.nodeId) return null;
|
|
559
|
+
// Tail-limit: 200 entries gives a wide enough window to catch terminal events for active
|
|
560
|
+
// sessions while avoiding a full O(n) scan. If a terminal is older than 200 entries,
|
|
561
|
+
// the BeadsDB fingerprint dedup will still block duplicate processing downstream.
|
|
562
|
+
const entries = readLedgerEntries(args.meshId, { tail: 200 });
|
|
563
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
564
|
+
const entry = entries[i];
|
|
565
|
+
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
|
|
566
|
+
if (args.sessionId && entry.sessionId === args.sessionId) {
|
|
567
|
+
return { id: entry.id, kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
|
|
568
|
+
}
|
|
569
|
+
if (!args.sessionId && args.nodeId && entry.nodeId === args.nodeId) {
|
|
570
|
+
return { id: entry.id, kind: entry.kind, payload: entry.payload || {}, timestamp: entry.timestamp };
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
return null;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
// Returns true when a task_dispatched entry for the given session appears AFTER the terminal
|
|
577
|
+
// entry (identified by terminalId) in ledger order. Positional (append) order is used rather
|
|
578
|
+
// than timestamp comparison because both entries may share the same millisecond.
|
|
579
|
+
function hasDispatchAfterTerminal(meshId: string, sessionId: string, terminalId: string): boolean {
|
|
580
|
+
// 200-entry window matches findRecentTerminalLedgerEvidence for consistency.
|
|
581
|
+
const entries = readLedgerEntries(meshId, { tail: 200 });
|
|
582
|
+
let pastTerminal = false;
|
|
583
|
+
for (const entry of entries) {
|
|
584
|
+
if (!pastTerminal) {
|
|
585
|
+
if (entry.id === terminalId) pastTerminal = true;
|
|
586
|
+
continue;
|
|
587
|
+
}
|
|
588
|
+
if (entry.kind === 'task_dispatched' && entry.sessionId === sessionId) return true;
|
|
589
|
+
}
|
|
590
|
+
return false;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
function hasUnterminalDirectDispatchLedgerEntry(meshId: string, sessionId: string): boolean {
|
|
594
|
+
// Some dispatch paths can persist task_dispatched before the direct-dispatch DB row is
|
|
595
|
+
// available. Recover routing from ledger order so coordinator self-targets still emit
|
|
596
|
+
// task_completed and pendingCoordinatorEvents.
|
|
597
|
+
const entries = readLedgerEntries(meshId, { tail: 200 });
|
|
598
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
599
|
+
const entry = entries[i];
|
|
600
|
+
if (entry.sessionId !== sessionId) continue;
|
|
601
|
+
if (entry.kind === 'task_completed' || entry.kind === 'task_failed' || entry.kind === 'task_stalled') {
|
|
602
|
+
return false;
|
|
603
|
+
}
|
|
604
|
+
if (entry.kind === 'task_dispatched' && entry.payload?.source === 'direct') {
|
|
605
|
+
return true;
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
return false;
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
function findDirectDispatchLedgerEntry(args: {
|
|
612
|
+
meshId: string;
|
|
613
|
+
taskId: string;
|
|
614
|
+
sessionId?: string;
|
|
615
|
+
}): { id: string; timestamp: string; nodeId?: string; sessionId?: string; providerType?: string; payload: Record<string, unknown> } | null {
|
|
616
|
+
const entries = readLedgerEntries(args.meshId, { tail: 500 });
|
|
617
|
+
for (let i = entries.length - 1; i >= 0; i--) {
|
|
618
|
+
const entry = entries[i];
|
|
619
|
+
if (entry.kind !== 'task_dispatched') continue;
|
|
620
|
+
const payloadTaskId = readNonEmptyString(entry.payload?.taskId);
|
|
621
|
+
if (payloadTaskId !== args.taskId) continue;
|
|
622
|
+
if (args.sessionId && entry.sessionId && entry.sessionId !== args.sessionId) continue;
|
|
623
|
+
return {
|
|
624
|
+
id: entry.id,
|
|
625
|
+
timestamp: entry.timestamp,
|
|
626
|
+
nodeId: entry.nodeId,
|
|
627
|
+
sessionId: entry.sessionId,
|
|
628
|
+
providerType: entry.providerType,
|
|
629
|
+
payload: entry.payload || {},
|
|
630
|
+
};
|
|
631
|
+
}
|
|
632
|
+
return null;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
function hasTerminalLedgerAfterDispatch(args: {
|
|
636
|
+
meshId: string;
|
|
637
|
+
taskId: string;
|
|
638
|
+
sessionId?: string;
|
|
639
|
+
dispatchEntryId?: string;
|
|
640
|
+
dispatchTimestamp?: string;
|
|
641
|
+
}): boolean {
|
|
642
|
+
const entries = readLedgerEntries(args.meshId, { tail: 500 });
|
|
643
|
+
let afterDispatch = !args.dispatchEntryId && !args.dispatchTimestamp;
|
|
644
|
+
const dispatchTime = args.dispatchTimestamp ? new Date(args.dispatchTimestamp).getTime() : Number.NaN;
|
|
645
|
+
for (const entry of entries) {
|
|
646
|
+
if (!afterDispatch) {
|
|
647
|
+
if (args.dispatchEntryId && entry.id === args.dispatchEntryId) {
|
|
648
|
+
afterDispatch = true;
|
|
649
|
+
continue;
|
|
650
|
+
}
|
|
651
|
+
if (!args.dispatchEntryId && Number.isFinite(dispatchTime)) {
|
|
652
|
+
const entryTime = new Date(entry.timestamp).getTime();
|
|
653
|
+
if (Number.isFinite(entryTime) && entryTime >= dispatchTime) afterDispatch = true;
|
|
654
|
+
}
|
|
655
|
+
if (!afterDispatch) continue;
|
|
656
|
+
}
|
|
657
|
+
if (entry.kind !== 'task_completed' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') continue;
|
|
658
|
+
const terminalTaskId = readNonEmptyString(entry.payload?.taskId);
|
|
659
|
+
if (terminalTaskId && terminalTaskId === args.taskId) return true;
|
|
660
|
+
if (terminalTaskId && terminalTaskId !== args.taskId) continue;
|
|
661
|
+
if (args.sessionId && entry.sessionId === args.sessionId) return true;
|
|
662
|
+
}
|
|
663
|
+
return false;
|
|
664
|
+
}
|
|
665
|
+
|
|
666
|
+
export function reconcileDirectDispatchCompletionFromTranscript(args: {
|
|
667
|
+
meshId: string;
|
|
668
|
+
nodeId?: string;
|
|
669
|
+
sessionId: string;
|
|
670
|
+
providerType?: string;
|
|
671
|
+
providerSessionId?: string;
|
|
672
|
+
taskId: string;
|
|
673
|
+
finalSummary: string;
|
|
674
|
+
transcriptMessageAt?: string;
|
|
675
|
+
completedAt?: string;
|
|
676
|
+
targetCoordinatorDaemonId?: string;
|
|
677
|
+
source?: string;
|
|
678
|
+
}): { reconciled: boolean; kind?: MeshLedgerKind; alreadyTerminal?: boolean; workerResult?: unknown; ledgerEntryId?: string; reason?: string } {
|
|
679
|
+
const finalSummary = readNonEmptyString(args.finalSummary);
|
|
680
|
+
if (!args.meshId || !args.taskId || !args.sessionId || !finalSummary) {
|
|
681
|
+
return { reconciled: false, reason: 'missing_required_completion_evidence' };
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
const dispatch = findDirectDispatchLedgerEntry({
|
|
685
|
+
meshId: args.meshId,
|
|
686
|
+
taskId: args.taskId,
|
|
687
|
+
sessionId: args.sessionId,
|
|
688
|
+
});
|
|
689
|
+
if (hasTerminalLedgerAfterDispatch({
|
|
690
|
+
meshId: args.meshId,
|
|
691
|
+
taskId: args.taskId,
|
|
692
|
+
sessionId: args.sessionId,
|
|
693
|
+
dispatchEntryId: dispatch?.id,
|
|
694
|
+
dispatchTimestamp: dispatch?.timestamp,
|
|
695
|
+
})) {
|
|
696
|
+
return { reconciled: false, alreadyTerminal: true, reason: 'terminal_ledger_entry_exists' };
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
const nodeId = readNonEmptyString(args.nodeId) || dispatch?.nodeId;
|
|
700
|
+
const providerType = readNonEmptyString(args.providerType) || dispatch?.providerType || readNonEmptyString(dispatch?.payload.providerType);
|
|
701
|
+
const completedAt = args.completedAt || new Date().toISOString();
|
|
702
|
+
const evidence = buildTaskCompletionEvidence({
|
|
703
|
+
event: 'agent:generating_completed',
|
|
704
|
+
nodeId: nodeId || 'unknown',
|
|
705
|
+
sessionId: args.sessionId,
|
|
706
|
+
providerType,
|
|
707
|
+
providerSessionId: readNonEmptyString(args.providerSessionId),
|
|
708
|
+
finalSummary,
|
|
709
|
+
completedAt,
|
|
710
|
+
});
|
|
711
|
+
const workerResult = evidence.workerResult;
|
|
712
|
+
const dispatchTime = dispatch?.timestamp ? new Date(dispatch.timestamp).getTime() : Number.NaN;
|
|
713
|
+
const transcriptTime = args.transcriptMessageAt ? new Date(args.transcriptMessageAt).getTime() : Number.NaN;
|
|
714
|
+
const transcriptAfterDispatch = Number.isFinite(dispatchTime) && Number.isFinite(transcriptTime) && transcriptTime >= dispatchTime;
|
|
715
|
+
if (workerResult.source !== 'final_summary_json' && !transcriptAfterDispatch) {
|
|
716
|
+
return { reconciled: false, reason: 'transcript_not_proven_after_dispatch' };
|
|
717
|
+
}
|
|
718
|
+
const workerFailed = workerResult.status === 'failed' || (workerResult.status !== 'completed' && workerResult.errors.length > 0);
|
|
719
|
+
const kind: MeshLedgerKind = workerFailed ? 'task_failed' : 'task_completed';
|
|
720
|
+
|
|
721
|
+
const entry = appendLedgerEntry(args.meshId, {
|
|
722
|
+
kind,
|
|
723
|
+
nodeId: nodeId || undefined,
|
|
724
|
+
sessionId: args.sessionId,
|
|
725
|
+
providerType: providerType || undefined,
|
|
726
|
+
payload: {
|
|
727
|
+
event: 'agent:generating_completed',
|
|
728
|
+
source: args.source || 'direct_task_transcript_reconciliation',
|
|
729
|
+
taskId: args.taskId,
|
|
730
|
+
providerSessionId: readNonEmptyString(args.providerSessionId),
|
|
731
|
+
finalSummary,
|
|
732
|
+
workerResult,
|
|
733
|
+
completionDiagnostic: {
|
|
734
|
+
reason: 'direct_task_transcript_reconciliation',
|
|
735
|
+
dispatchEntryId: dispatch?.id,
|
|
736
|
+
dispatchTimestamp: dispatch?.timestamp,
|
|
737
|
+
transcriptMessageAt: readNonEmptyString(args.transcriptMessageAt),
|
|
738
|
+
transcriptFinalAssistantPresent: true,
|
|
739
|
+
},
|
|
740
|
+
evidence,
|
|
741
|
+
},
|
|
742
|
+
});
|
|
743
|
+
updateDirectDispatchStatus(args.meshId, args.sessionId, kind === 'task_completed' ? 'completed' : 'failed');
|
|
744
|
+
setImmediate(() => cleanupTerminalDirectDispatches());
|
|
745
|
+
queuePendingMeshCoordinatorEvent({
|
|
746
|
+
event: kind === 'task_completed' ? 'agent:generating_completed' : 'agent:stopped',
|
|
747
|
+
meshId: args.meshId,
|
|
748
|
+
nodeLabel: nodeId ? `Node '${nodeId}'` : 'Remote agent',
|
|
749
|
+
nodeId: nodeId || undefined,
|
|
750
|
+
metadataEvent: {
|
|
751
|
+
targetSessionId: args.sessionId,
|
|
752
|
+
providerType: providerType || undefined,
|
|
753
|
+
providerSessionId: readNonEmptyString(args.providerSessionId),
|
|
754
|
+
finalSummary,
|
|
755
|
+
taskId: args.taskId,
|
|
756
|
+
workerResult,
|
|
757
|
+
completionDiagnostic: {
|
|
758
|
+
reason: 'direct_task_transcript_reconciliation',
|
|
759
|
+
terminalLedgerKind: kind,
|
|
760
|
+
terminalLedgerId: entry.id,
|
|
761
|
+
},
|
|
762
|
+
},
|
|
763
|
+
coordinatorMessage: undefined,
|
|
764
|
+
queuedAt: Date.now(),
|
|
765
|
+
...(readNonEmptyString(args.targetCoordinatorDaemonId) ? { targetCoordinatorDaemonId: readNonEmptyString(args.targetCoordinatorDaemonId) } : {}),
|
|
766
|
+
});
|
|
767
|
+
|
|
768
|
+
return { reconciled: true, kind, workerResult, ledgerEntryId: entry.id };
|
|
769
|
+
}
|
|
770
|
+
|
|
771
|
+
function buildLongGeneratingCompletionReconciliation(args: {
|
|
772
|
+
meshId: string;
|
|
773
|
+
nodeId?: string;
|
|
774
|
+
nodeLabel: string;
|
|
775
|
+
metadataEvent: Record<string, unknown>;
|
|
776
|
+
sourceInstanceId?: string;
|
|
777
|
+
}): Record<string, unknown> | null {
|
|
778
|
+
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
779
|
+
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
780
|
+
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
781
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
|
|
782
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
783
|
+
const completionDiagnostic = readRecord(args.metadataEvent.completionDiagnostic);
|
|
784
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
|
|
785
|
+
const status = readNonEmptyString(args.metadataEvent.status).toLowerCase();
|
|
786
|
+
const explicitCompletionEvidence = Boolean(
|
|
787
|
+
finalSummary
|
|
788
|
+
|| workerResult
|
|
789
|
+
|| completionDiagnostic?.finalAssistantPresent === true
|
|
790
|
+
|| status === 'idle'
|
|
791
|
+
|| status === 'ready'
|
|
792
|
+
|| status === 'completed',
|
|
793
|
+
);
|
|
794
|
+
if (explicitCompletionEvidence) {
|
|
795
|
+
return {
|
|
796
|
+
...args.metadataEvent,
|
|
797
|
+
targetSessionId: sessionId,
|
|
798
|
+
providerType,
|
|
799
|
+
providerSessionId,
|
|
800
|
+
finalSummary,
|
|
801
|
+
source: 'long_generating_reconciliation',
|
|
802
|
+
reconciledFromEvent: 'monitor:long_generating',
|
|
803
|
+
timestamp: args.metadataEvent.timestamp ?? Date.now(),
|
|
804
|
+
completionDiagnostic: {
|
|
805
|
+
...(completionDiagnostic || {}),
|
|
806
|
+
reconciliationReason: 'provider_completion_evidence',
|
|
807
|
+
},
|
|
808
|
+
};
|
|
809
|
+
}
|
|
810
|
+
|
|
811
|
+
const terminal = findRecentTerminalLedgerEvidence({
|
|
812
|
+
meshId: args.meshId,
|
|
813
|
+
sessionId: sessionId || undefined,
|
|
814
|
+
nodeId: nodeId || undefined,
|
|
815
|
+
});
|
|
816
|
+
if (!terminal) return null;
|
|
817
|
+
return {
|
|
818
|
+
...args.metadataEvent,
|
|
819
|
+
source: 'long_generating_terminal_ledger_suppression',
|
|
820
|
+
terminalLedgerKind: terminal.kind,
|
|
821
|
+
terminalLedgerAt: terminal.timestamp,
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
|
|
153
825
|
|
|
154
826
|
export function tryAssignQueueTask(
|
|
155
827
|
components: DaemonComponents,
|
|
@@ -158,7 +830,10 @@ export function tryAssignQueueTask(
|
|
|
158
830
|
sessionId: string,
|
|
159
831
|
providerType: string
|
|
160
832
|
): boolean {
|
|
161
|
-
const
|
|
833
|
+
const mesh = getMeshWithCache(components, meshId);
|
|
834
|
+
const node = mesh?.nodes.find((n: any) => n.id === nodeId);
|
|
835
|
+
const capabilityTags = buildMeshNodeCapabilityTags(node, providerType);
|
|
836
|
+
const task = claimNextTask(meshId, nodeId, sessionId, capabilityTags);
|
|
162
837
|
if (!task) {
|
|
163
838
|
return false;
|
|
164
839
|
}
|
|
@@ -166,9 +841,6 @@ export function tryAssignQueueTask(
|
|
|
166
841
|
LOG.info('MeshQueue', `Node ${nodeId} (${sessionId}) pulled task ${task.id}`);
|
|
167
842
|
|
|
168
843
|
// Check if the node is remote
|
|
169
|
-
const mesh = getMeshWithCache(components, meshId);
|
|
170
|
-
const node = mesh?.nodes.find((n: any) => n.id === nodeId);
|
|
171
|
-
|
|
172
844
|
// If the node is explicitly remote and we have a dispatch mechanism, route via P2P
|
|
173
845
|
if (node?.daemonId && components.dispatchMeshCommand) {
|
|
174
846
|
const isLocalNode = components.cliManager.adapters.has(sessionId);
|
|
@@ -180,7 +852,16 @@ export function tryAssignQueueTask(
|
|
|
180
852
|
message: task.message,
|
|
181
853
|
}).catch((e: any) => {
|
|
182
854
|
LOG.error('MeshQueue', `Failed to dispatch task via P2P to remote node ${nodeId}: ${e?.message}`);
|
|
183
|
-
|
|
855
|
+
// Revert to pending so the task can be retried rather than permanently failing
|
|
856
|
+
updateTaskStatus(meshId, task.id, 'pending');
|
|
857
|
+
try {
|
|
858
|
+
appendLedgerEntry(meshId, {
|
|
859
|
+
kind: 'dispatch_failed' as any,
|
|
860
|
+
nodeId,
|
|
861
|
+
sessionId,
|
|
862
|
+
payload: { taskId: task.id, error: e?.message, retryable: true },
|
|
863
|
+
});
|
|
864
|
+
} catch { /* ledger write is best-effort */ }
|
|
184
865
|
});
|
|
185
866
|
return true;
|
|
186
867
|
}
|
|
@@ -204,6 +885,13 @@ const autoLaunchInProgress = new Set<string>();
|
|
|
204
885
|
const autoLaunchCooldownUntil = new Map<string, number>();
|
|
205
886
|
const AUTO_LAUNCH_COOLDOWN_MS = 5_000;
|
|
206
887
|
|
|
888
|
+
function sweepExpiredCooldowns(): void {
|
|
889
|
+
const now = Date.now();
|
|
890
|
+
for (const [key, until] of autoLaunchCooldownUntil) {
|
|
891
|
+
if (now >= until) autoLaunchCooldownUntil.delete(key);
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
|
|
207
895
|
function normalizeProviderPriority(policy: unknown): string[] {
|
|
208
896
|
const raw = policy && typeof policy === 'object' && !Array.isArray(policy)
|
|
209
897
|
? (policy as Record<string, unknown>).providerPriority
|
|
@@ -277,6 +965,10 @@ function nodeHasActiveAssignment(meshId: string, nodeId: string): boolean {
|
|
|
277
965
|
return getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedNodeId === nodeId);
|
|
278
966
|
}
|
|
279
967
|
|
|
968
|
+
function sessionHasActiveAssignment(meshId: string, sessionId: string): boolean {
|
|
969
|
+
return getQueue(meshId, { status: ['assigned'] as any }).some(task => task.assignedSessionId === sessionId);
|
|
970
|
+
}
|
|
971
|
+
|
|
280
972
|
function liveSessionCountForNode(components: DaemonComponents, meshId: string, nodeId: string): number {
|
|
281
973
|
return components.instanceManager.getByCategory('cli').filter((inst: any) => {
|
|
282
974
|
const state = inst.getState();
|
|
@@ -406,12 +1098,14 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
406
1098
|
const nodeId = readNonEmptyString(node?.id);
|
|
407
1099
|
if (!nodeId) continue;
|
|
408
1100
|
const launchKey = `${meshId}:${nodeId}`;
|
|
1101
|
+
const now = Date.now();
|
|
409
1102
|
const cooldownUntil = autoLaunchCooldownUntil.get(launchKey) || 0;
|
|
1103
|
+
if (cooldownUntil > 0 && now >= cooldownUntil) autoLaunchCooldownUntil.delete(launchKey);
|
|
410
1104
|
if (autoLaunchInProgress.has(launchKey)) {
|
|
411
1105
|
markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'auto_launch_in_progress', nodeId });
|
|
412
1106
|
continue;
|
|
413
1107
|
}
|
|
414
|
-
if (
|
|
1108
|
+
if (now < cooldownUntil) {
|
|
415
1109
|
markAutoLaunch(meshId, task.id, { status: 'skipped', reason: 'auto_launch_cooldown', nodeId });
|
|
416
1110
|
continue;
|
|
417
1111
|
}
|
|
@@ -461,13 +1155,13 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
461
1155
|
if (!launchResult?.success) {
|
|
462
1156
|
const reason = launchResult?.error || 'launch_cli_failed';
|
|
463
1157
|
markAutoLaunch(meshId, task.id, { status: 'failed', reason, nodeId, providerType: resolved.providerType });
|
|
464
|
-
autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS);
|
|
1158
|
+
autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS); sweepExpiredCooldowns();
|
|
465
1159
|
return false;
|
|
466
1160
|
}
|
|
467
1161
|
const sessionId = readNonEmptyString(launchResult.sessionId) || readNonEmptyString(launchResult.id) || readNonEmptyString(launchResult.runtimeSessionId);
|
|
468
1162
|
if (!sessionId) {
|
|
469
1163
|
markAutoLaunch(meshId, task.id, { status: 'failed', reason: 'launch_missing_session_id', nodeId, providerType: resolved.providerType });
|
|
470
|
-
autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS);
|
|
1164
|
+
autoLaunchCooldownUntil.set(launchKey, Date.now() + AUTO_LAUNCH_COOLDOWN_MS); sweepExpiredCooldowns();
|
|
471
1165
|
return false;
|
|
472
1166
|
}
|
|
473
1167
|
markAutoLaunch(meshId, task.id, { status: 'completed', nodeId, providerType: resolved.providerType, sessionId });
|
|
@@ -485,13 +1179,69 @@ async function maybeAutoLaunchOneQueueSession(components: DaemonComponents, mesh
|
|
|
485
1179
|
return false;
|
|
486
1180
|
}
|
|
487
1181
|
|
|
1182
|
+
export interface MeshQueueTriggerResult {
|
|
1183
|
+
success: true;
|
|
1184
|
+
meshId: string;
|
|
1185
|
+
pendingBefore: number;
|
|
1186
|
+
assignedBefore: number;
|
|
1187
|
+
pendingAfter: number;
|
|
1188
|
+
assignedAfter: number;
|
|
1189
|
+
claimed: boolean;
|
|
1190
|
+
newlyAssignedTasks: Array<{
|
|
1191
|
+
id: string;
|
|
1192
|
+
nodeId?: string;
|
|
1193
|
+
sessionId?: string;
|
|
1194
|
+
}>;
|
|
1195
|
+
localIdleSessionsChecked: number;
|
|
1196
|
+
remoteIdleSessionsChecked: number;
|
|
1197
|
+
skippedSessions: Array<{
|
|
1198
|
+
nodeId?: string;
|
|
1199
|
+
sessionId?: string;
|
|
1200
|
+
reason: string;
|
|
1201
|
+
status?: string;
|
|
1202
|
+
}>;
|
|
1203
|
+
autoLaunchStarted: boolean;
|
|
1204
|
+
noIdleMeshSessionAvailable?: boolean;
|
|
1205
|
+
}
|
|
1206
|
+
|
|
1207
|
+
function countQueueStatus(meshId: string, status: 'pending' | 'assigned'): number {
|
|
1208
|
+
return getQueue(meshId, { status: [status] as any }).length;
|
|
1209
|
+
}
|
|
1210
|
+
|
|
1211
|
+
function getQueueStatusById(meshId: string): Map<string, string> {
|
|
1212
|
+
return new Map(getQueue(meshId).map(task => [task.id, task.status]));
|
|
1213
|
+
}
|
|
1214
|
+
|
|
488
1215
|
/**
|
|
489
1216
|
* Triggers a queue check for all nodes in the mesh.
|
|
490
1217
|
* Called when a new task is enqueued, in case nodes are already idle.
|
|
491
1218
|
*/
|
|
492
|
-
export async function triggerMeshQueue(components: DaemonComponents, meshId: string): Promise<
|
|
1219
|
+
export async function triggerMeshQueue(components: DaemonComponents, meshId: string): Promise<MeshQueueTriggerResult> {
|
|
493
1220
|
const mesh = getMeshWithCache(components, meshId);
|
|
494
|
-
|
|
1221
|
+
const pendingBefore = countQueueStatus(meshId, 'pending');
|
|
1222
|
+
const assignedBefore = countQueueStatus(meshId, 'assigned');
|
|
1223
|
+
const beforeStatus = getQueueStatusById(meshId);
|
|
1224
|
+
const skippedSessions: MeshQueueTriggerResult['skippedSessions'] = [];
|
|
1225
|
+
let localIdleSessionsChecked = 0;
|
|
1226
|
+
let remoteIdleSessionsChecked = 0;
|
|
1227
|
+
let autoLaunchStarted = false;
|
|
1228
|
+
if (!mesh) {
|
|
1229
|
+
return {
|
|
1230
|
+
success: true,
|
|
1231
|
+
meshId,
|
|
1232
|
+
pendingBefore,
|
|
1233
|
+
assignedBefore,
|
|
1234
|
+
pendingAfter: pendingBefore,
|
|
1235
|
+
assignedAfter: assignedBefore,
|
|
1236
|
+
claimed: false,
|
|
1237
|
+
newlyAssignedTasks: [],
|
|
1238
|
+
localIdleSessionsChecked,
|
|
1239
|
+
remoteIdleSessionsChecked,
|
|
1240
|
+
skippedSessions: [{ reason: 'mesh_not_found' }],
|
|
1241
|
+
autoLaunchStarted,
|
|
1242
|
+
noIdleMeshSessionAvailable: true,
|
|
1243
|
+
};
|
|
1244
|
+
}
|
|
495
1245
|
|
|
496
1246
|
// Find all CLI instances that belong to this mesh and are idle
|
|
497
1247
|
const cliInstances = components.instanceManager.getByCategory('cli');
|
|
@@ -508,14 +1258,30 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
|
|
|
508
1258
|
// Only genuinely idle live sessions can pull work. Restored/stopped
|
|
509
1259
|
// records are kept for transcript/recovery visibility, but assigning
|
|
510
1260
|
// queue items to them strands tasks in assigned/pending without chat.
|
|
511
|
-
if (!isIdleSessionState(state))
|
|
1261
|
+
if (!isIdleSessionState(state)) {
|
|
1262
|
+
const status = readNonEmptyString(state.status).toLowerCase();
|
|
1263
|
+
skippedSessions.push({
|
|
1264
|
+
nodeId,
|
|
1265
|
+
sessionId: readNonEmptyString(state.instanceId),
|
|
1266
|
+
reason: isTerminalSessionStatus(status) ? 'terminal_session' : 'session_not_idle',
|
|
1267
|
+
status: status || undefined,
|
|
1268
|
+
});
|
|
1269
|
+
continue;
|
|
1270
|
+
}
|
|
512
1271
|
|
|
513
1272
|
const sessionId = state.instanceId;
|
|
514
1273
|
const providerType = state.type || readNonEmptyString(settings.providerType);
|
|
515
1274
|
|
|
516
1275
|
if (providerType) {
|
|
517
1276
|
// Try to assign a task to this idle node
|
|
1277
|
+
localIdleSessionsChecked += 1;
|
|
518
1278
|
tryAssignQueueTask(components, meshId, nodeId, sessionId, providerType);
|
|
1279
|
+
} else {
|
|
1280
|
+
skippedSessions.push({
|
|
1281
|
+
nodeId,
|
|
1282
|
+
sessionId,
|
|
1283
|
+
reason: 'provider_type_missing',
|
|
1284
|
+
});
|
|
519
1285
|
}
|
|
520
1286
|
}
|
|
521
1287
|
|
|
@@ -524,6 +1290,7 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
|
|
|
524
1290
|
// Find if this node is in the same mesh
|
|
525
1291
|
const node = mesh.nodes.find((n: any) => n.id === idle.nodeId);
|
|
526
1292
|
if (node) {
|
|
1293
|
+
remoteIdleSessionsChecked += 1;
|
|
527
1294
|
const assigned = tryAssignQueueTask(components, meshId, idle.nodeId, idle.sessionId, idle.providerType);
|
|
528
1295
|
if (assigned) {
|
|
529
1296
|
remoteIdleSessions.delete(key);
|
|
@@ -531,7 +1298,100 @@ export async function triggerMeshQueue(components: DaemonComponents, meshId: str
|
|
|
531
1298
|
}
|
|
532
1299
|
}
|
|
533
1300
|
|
|
534
|
-
await maybeAutoLaunchOneQueueSession(components, meshId, mesh);
|
|
1301
|
+
autoLaunchStarted = await maybeAutoLaunchOneQueueSession(components, meshId, mesh);
|
|
1302
|
+
const afterQueue = getQueue(meshId);
|
|
1303
|
+
const pendingAfter = afterQueue.filter(task => task.status === 'pending').length;
|
|
1304
|
+
const assignedAfter = afterQueue.filter(task => task.status === 'assigned').length;
|
|
1305
|
+
const newlyAssignedTasks = afterQueue
|
|
1306
|
+
.filter(task => task.status === 'assigned' && beforeStatus.get(task.id) !== 'assigned')
|
|
1307
|
+
.map(task => ({
|
|
1308
|
+
id: task.id,
|
|
1309
|
+
nodeId: task.assignedNodeId,
|
|
1310
|
+
sessionId: task.assignedSessionId,
|
|
1311
|
+
}));
|
|
1312
|
+
return {
|
|
1313
|
+
success: true,
|
|
1314
|
+
meshId,
|
|
1315
|
+
pendingBefore,
|
|
1316
|
+
assignedBefore,
|
|
1317
|
+
pendingAfter,
|
|
1318
|
+
assignedAfter,
|
|
1319
|
+
claimed: newlyAssignedTasks.length > 0,
|
|
1320
|
+
newlyAssignedTasks,
|
|
1321
|
+
localIdleSessionsChecked,
|
|
1322
|
+
remoteIdleSessionsChecked,
|
|
1323
|
+
skippedSessions,
|
|
1324
|
+
autoLaunchStarted,
|
|
1325
|
+
...(pendingAfter > 0 && newlyAssignedTasks.length === 0 && localIdleSessionsChecked === 0 && remoteIdleSessionsChecked === 0 && !autoLaunchStarted
|
|
1326
|
+
? { noIdleMeshSessionAvailable: true }
|
|
1327
|
+
: {}),
|
|
1328
|
+
};
|
|
1329
|
+
}
|
|
1330
|
+
|
|
1331
|
+
async function maybeAutoFastForwardIdleNode(components: DaemonComponents, args: {
|
|
1332
|
+
meshId: string;
|
|
1333
|
+
nodeId: string;
|
|
1334
|
+
sessionId?: string;
|
|
1335
|
+
providerType?: string;
|
|
1336
|
+
}): Promise<void> {
|
|
1337
|
+
const mesh = getMeshWithCache(components, args.meshId);
|
|
1338
|
+
const node = mesh?.nodes?.find((candidate: any) => candidate?.id === args.nodeId || candidate?.nodeId === args.nodeId);
|
|
1339
|
+
const workspace = readNonEmptyString(node?.workspace);
|
|
1340
|
+
if (!workspace) return;
|
|
1341
|
+
if (!existsSync(workspace)) return;
|
|
1342
|
+
|
|
1343
|
+
const throttleKey = `${args.meshId}:${args.nodeId}`;
|
|
1344
|
+
const now = Date.now();
|
|
1345
|
+
const lastAttempt = idleAutoFastForwardLastAttempt.get(throttleKey) || 0;
|
|
1346
|
+
if (now - lastAttempt < IDLE_AUTO_FAST_FORWARD_THROTTLE_MS) return;
|
|
1347
|
+
idleAutoFastForwardLastAttempt.set(throttleKey, now);
|
|
1348
|
+
|
|
1349
|
+
const submoduleIgnorePaths = Array.isArray(node?.policy?.submoduleIgnorePaths)
|
|
1350
|
+
? node.policy.submoduleIgnorePaths.filter((value: unknown): value is string => typeof value === 'string')
|
|
1351
|
+
: undefined;
|
|
1352
|
+
try {
|
|
1353
|
+
const dryRun = await fastForwardMeshNode({
|
|
1354
|
+
meshId: args.meshId,
|
|
1355
|
+
nodeId: args.nodeId,
|
|
1356
|
+
workspace,
|
|
1357
|
+
execute: false,
|
|
1358
|
+
dryRun: true,
|
|
1359
|
+
updateSubmodules: false,
|
|
1360
|
+
submoduleIgnorePaths,
|
|
1361
|
+
trigger: 'idle_auto',
|
|
1362
|
+
});
|
|
1363
|
+
if (!dryRun || dryRun.code !== 'fast_forward_available' || dryRun.allowed !== true) return;
|
|
1364
|
+
await fastForwardMeshNode({
|
|
1365
|
+
meshId: args.meshId,
|
|
1366
|
+
nodeId: args.nodeId,
|
|
1367
|
+
workspace,
|
|
1368
|
+
execute: true,
|
|
1369
|
+
dryRun: false,
|
|
1370
|
+
updateSubmodules: false,
|
|
1371
|
+
submoduleIgnorePaths,
|
|
1372
|
+
trigger: 'idle_auto',
|
|
1373
|
+
});
|
|
1374
|
+
} catch (e: any) {
|
|
1375
|
+
LOG.warn('MeshFastForward', `Idle auto fast-forward check failed for ${args.nodeId}: ${e?.message || e}`);
|
|
1376
|
+
}
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function runIdleMaintenanceThenAssignQueue(components: DaemonComponents, args: {
|
|
1380
|
+
meshId: string;
|
|
1381
|
+
nodeId: string;
|
|
1382
|
+
sessionId: string;
|
|
1383
|
+
providerType: string;
|
|
1384
|
+
}): void {
|
|
1385
|
+
setImmediate(() => {
|
|
1386
|
+
maybeAutoFastForwardIdleNode(components, args)
|
|
1387
|
+
.finally(() => {
|
|
1388
|
+
try {
|
|
1389
|
+
tryAssignQueueTask(components, args.meshId, args.nodeId, args.sessionId, args.providerType);
|
|
1390
|
+
} catch (e: any) {
|
|
1391
|
+
LOG.warn('MeshQueue', `Failed to assign idle queue task after maintenance for ${args.nodeId}: ${e?.message || e}`);
|
|
1392
|
+
}
|
|
1393
|
+
});
|
|
1394
|
+
});
|
|
535
1395
|
}
|
|
536
1396
|
|
|
537
1397
|
function buildMeshSystemMessage(args: {
|
|
@@ -542,6 +1402,9 @@ function buildMeshSystemMessage(args: {
|
|
|
542
1402
|
}): string {
|
|
543
1403
|
const metadata = formatCompletionMetadata(args.metadataEvent);
|
|
544
1404
|
if (args.event === 'agent:generating_completed') {
|
|
1405
|
+
if (args.metadataEvent.source === 'long_generating_reconciliation') {
|
|
1406
|
+
return `[System] ${args.nodeLabel} already has completion evidence${metadata}. The long-generating monitor reconciled the terminal handoff and marked the session complete; wait for the queued completion event/status refresh before doing any manual transcript check.`;
|
|
1407
|
+
}
|
|
545
1408
|
return `[System] ${args.nodeLabel} has completed its task and is now idle${metadata}. This completion came from the agent status event path; use mesh_read_chat once to review its final progress, but do not poll repeatedly.`;
|
|
546
1409
|
}
|
|
547
1410
|
if (args.event === 'agent:waiting_approval') {
|
|
@@ -573,7 +1436,72 @@ function buildMeshSystemMessage(args: {
|
|
|
573
1436
|
return `[System] ${args.nodeLabel} has stopped${metadata}. Use mesh_read_chat once if you need to inspect its last output.`;
|
|
574
1437
|
}
|
|
575
1438
|
if (args.event === 'monitor:long_generating') {
|
|
576
|
-
return `[System] ${args.nodeLabel}
|
|
1439
|
+
return `[System] ${args.nodeLabel} is still reported as generating after a long interval${metadata}. Wait for pendingCoordinatorEvents or a completion/status event; if the user explicitly asks for status, make one bounded status check and then wait again.`;
|
|
1440
|
+
}
|
|
1441
|
+
if (args.event === 'refine:accepted') {
|
|
1442
|
+
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
1443
|
+
return `[System] Refinery accepted async job${jobId ? ` ${jobId}` : ''} for ${args.nodeLabel}. Completion/failure will be delivered as a terminal refine event; do not poll repeatedly.`;
|
|
1444
|
+
}
|
|
1445
|
+
if (args.event === 'refine:completed') {
|
|
1446
|
+
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
1447
|
+
const result = readRecord(args.metadataEvent.result);
|
|
1448
|
+
const validationSummary = readRecord(result?.validationSummary);
|
|
1449
|
+
const patchEquivalence = readRecord(result?.patchEquivalence);
|
|
1450
|
+
const finalConvergence = readRecord(result?.finalBranchConvergenceState);
|
|
1451
|
+
const validationStatus = readNonEmptyString(validationSummary?.status);
|
|
1452
|
+
const patchStatus = readNonEmptyString(patchEquivalence?.status)
|
|
1453
|
+
|| (patchEquivalence?.equivalent === true ? 'passed' : '');
|
|
1454
|
+
const into = readNonEmptyString(result?.into);
|
|
1455
|
+
const branch = readNonEmptyString(result?.branch);
|
|
1456
|
+
const mergeStatus = result?.merged === true ? 'merged' : readNonEmptyString(finalConvergence?.status);
|
|
1457
|
+
const convergenceStatus = readNonEmptyString(finalConvergence?.status);
|
|
1458
|
+
const nextStep = readNonEmptyString(result?.nextStep)
|
|
1459
|
+
|| readNonEmptyString(finalConvergence?.nextStep)
|
|
1460
|
+
|| 'Continue from the updated mesh state.';
|
|
1461
|
+
const details = [
|
|
1462
|
+
jobId ? `job_id=${jobId}` : '',
|
|
1463
|
+
branch && into ? `${branch}→${into}` : '',
|
|
1464
|
+
validationStatus ? `validation=${validationStatus}` : '',
|
|
1465
|
+
patchStatus ? `patch_equivalence=${patchStatus}` : '',
|
|
1466
|
+
mergeStatus ? `merge=${mergeStatus}` : '',
|
|
1467
|
+
convergenceStatus ? `final_convergence=${convergenceStatus}` : '',
|
|
1468
|
+
].filter(Boolean).join('; ');
|
|
1469
|
+
return `[System] Refinery async job for ${args.nodeLabel} completed successfully${details ? ` (${details})` : ''}.\nNext step: ${nextStep}`;
|
|
1470
|
+
}
|
|
1471
|
+
if (args.event === 'refine:failed') {
|
|
1472
|
+
const jobId = readRefineJobId({ metadataEvent: args.metadataEvent });
|
|
1473
|
+
const result = readRecord(args.metadataEvent.result);
|
|
1474
|
+
const validationSummary = readRecord(result?.validationSummary);
|
|
1475
|
+
const patchEquivalence = readRecord(result?.patchEquivalence);
|
|
1476
|
+
const finalConvergence = readRecord(result?.finalBranchConvergenceState);
|
|
1477
|
+
const code = readNonEmptyString(result?.code);
|
|
1478
|
+
const error = readNonEmptyString(result?.error);
|
|
1479
|
+
const validationStatus = readNonEmptyString(validationSummary?.status);
|
|
1480
|
+
const patchStatus = readNonEmptyString(patchEquivalence?.status)
|
|
1481
|
+
|| (patchEquivalence?.equivalent === true ? 'passed' : '');
|
|
1482
|
+
const mergeStatus = result?.merged === true
|
|
1483
|
+
? 'merged'
|
|
1484
|
+
: finalConvergence?.merged === false
|
|
1485
|
+
? 'not_merged'
|
|
1486
|
+
: '';
|
|
1487
|
+
const convergenceStatus = readNonEmptyString(result?.convergenceStatus)
|
|
1488
|
+
|| readNonEmptyString(finalConvergence?.status);
|
|
1489
|
+
const blockedReason = readNonEmptyString(result?.blockedReason);
|
|
1490
|
+
const nextStep = readNonEmptyString(result?.nextStep) || readNonEmptyString(finalConvergence?.nextStep);
|
|
1491
|
+
const details = [
|
|
1492
|
+
jobId ? `job_id=${jobId}` : '',
|
|
1493
|
+
code ? `code=${code}` : '',
|
|
1494
|
+
validationStatus ? `validation=${validationStatus}` : '',
|
|
1495
|
+
patchStatus ? `patch_equivalence=${patchStatus}` : '',
|
|
1496
|
+
mergeStatus ? `merge=${mergeStatus}` : '',
|
|
1497
|
+
convergenceStatus ? `convergence=${convergenceStatus}` : '',
|
|
1498
|
+
blockedReason ? `reason=${blockedReason}` : '',
|
|
1499
|
+
].filter(Boolean).join('; ');
|
|
1500
|
+
const parts = [
|
|
1501
|
+
`[System] Refinery async job for ${args.nodeLabel} failed${details ? ` (${details})` : ''}${error ? `: ${error}` : '.'}`,
|
|
1502
|
+
nextStep ? `Next step: ${nextStep}` : 'Review the terminal refine event/ledger before retrying.',
|
|
1503
|
+
];
|
|
1504
|
+
return parts.join('\n');
|
|
577
1505
|
}
|
|
578
1506
|
return '';
|
|
579
1507
|
}
|
|
@@ -588,6 +1516,15 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
588
1516
|
}) {
|
|
589
1517
|
const eventSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
590
1518
|
const eventNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
1519
|
+
|
|
1520
|
+
// Resolve coordinator ownership early — used in fingerprinting and coordinator routing.
|
|
1521
|
+
const sourceSession = args.sourceInstanceId
|
|
1522
|
+
? components.instanceManager.getInstance(args.sourceInstanceId)
|
|
1523
|
+
: undefined;
|
|
1524
|
+
const workerCoordinatorDaemonId = readNonEmptyString(
|
|
1525
|
+
(sourceSession?.getState()?.settings as Record<string, unknown>)?.meshCoordinatorDaemonId,
|
|
1526
|
+
);
|
|
1527
|
+
const localDaemonId = readNonEmptyString(loadConfig().machineId);
|
|
591
1528
|
const intentionalCleanupStop = shouldSuppressIntentionalCleanupStop({
|
|
592
1529
|
event: args.event,
|
|
593
1530
|
meshId: args.meshId,
|
|
@@ -603,6 +1540,97 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
603
1540
|
return { success: true, forwarded: 0, suppressed: true, intentionalCleanupStop: true };
|
|
604
1541
|
}
|
|
605
1542
|
|
|
1543
|
+
if (args.event === 'monitor:long_generating') {
|
|
1544
|
+
const reconciledCompletion = buildLongGeneratingCompletionReconciliation({
|
|
1545
|
+
meshId: args.meshId,
|
|
1546
|
+
nodeId: args.nodeId,
|
|
1547
|
+
nodeLabel: args.nodeLabel,
|
|
1548
|
+
metadataEvent: args.metadataEvent,
|
|
1549
|
+
sourceInstanceId: args.sourceInstanceId,
|
|
1550
|
+
});
|
|
1551
|
+
if (reconciledCompletion?.source === 'long_generating_reconciliation') {
|
|
1552
|
+
LOG.info('MeshEvents', `Reconciled long-generating monitor to completion for session ${eventSessionId || '(unknown session)'}`);
|
|
1553
|
+
return injectMeshSystemMessage(components, {
|
|
1554
|
+
...args,
|
|
1555
|
+
event: 'agent:generating_completed',
|
|
1556
|
+
metadataEvent: reconciledCompletion,
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
if (reconciledCompletion?.source === 'long_generating_terminal_ledger_suppression') {
|
|
1560
|
+
LOG.info('MeshEvents', `Suppressed long-generating monitor because terminal ledger evidence already exists for session ${eventSessionId || '(unknown session)'}`);
|
|
1561
|
+
return {
|
|
1562
|
+
success: true,
|
|
1563
|
+
forwarded: 0,
|
|
1564
|
+
suppressed: true,
|
|
1565
|
+
terminalLedgerEvidence: true,
|
|
1566
|
+
terminalLedgerKind: reconciledCompletion.terminalLedgerKind,
|
|
1567
|
+
};
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
|
|
1571
|
+
if (isDuplicateRefineTerminalEvent(args.meshId, args.event, args.metadataEvent)) {
|
|
1572
|
+
LOG.info('MeshEvents', `Suppressed duplicate ${args.event} for refine job ${readRefineJobId({ metadataEvent: args.metadataEvent })}`);
|
|
1573
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateRefineTerminalEvent: true };
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
const eventTimestamp = readEventTimestamp(args.metadataEvent.timestamp);
|
|
1577
|
+
if (args.event === 'agent:waiting_approval' && eventSessionId) {
|
|
1578
|
+
const duplicateApproval = isDuplicateMeshApprovalEvent({
|
|
1579
|
+
meshId: args.meshId,
|
|
1580
|
+
sessionId: eventSessionId,
|
|
1581
|
+
providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
|
|
1582
|
+
timestamp: eventTimestamp,
|
|
1583
|
+
modalMessage: readNonEmptyString(args.metadataEvent.modalMessage) || undefined,
|
|
1584
|
+
modalButtons: args.metadataEvent.modalButtons,
|
|
1585
|
+
});
|
|
1586
|
+
if (duplicateApproval) {
|
|
1587
|
+
LOG.info('MeshEvents', `Suppressed duplicate approval event for mesh ${args.meshId} session ${eventSessionId}`);
|
|
1588
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateApproval: true };
|
|
1589
|
+
}
|
|
1590
|
+
}
|
|
1591
|
+
if (args.event === 'agent:generating_completed' && eventSessionId) {
|
|
1592
|
+
const terminal = findRecentTerminalLedgerEvidence({
|
|
1593
|
+
meshId: args.meshId,
|
|
1594
|
+
sessionId: eventSessionId,
|
|
1595
|
+
nodeId: eventNodeId || undefined,
|
|
1596
|
+
});
|
|
1597
|
+
if (terminal?.kind === 'task_completed' && !sessionHasActiveAssignment(args.meshId, eventSessionId)) {
|
|
1598
|
+
// If a new task_dispatched was recorded for this session after the prior terminal,
|
|
1599
|
+
// this completion belongs to the new task — never suppress it as a duplicate.
|
|
1600
|
+
const newDispatchAfterTerminal = hasDispatchAfterTerminal(args.meshId, eventSessionId, terminal.id);
|
|
1601
|
+
if (!newDispatchAfterTerminal) {
|
|
1602
|
+
const terminalProviderSessionId = readNonEmptyString(terminal.payload.providerSessionId);
|
|
1603
|
+
const terminalFinalSummary = readNonEmptyString(terminal.payload.finalSummary);
|
|
1604
|
+
const eventProviderSessionId = readNonEmptyString(args.metadataEvent.providerSessionId);
|
|
1605
|
+
const eventFinalSummary = readNonEmptyString(args.metadataEvent.finalSummary);
|
|
1606
|
+
if (
|
|
1607
|
+
(terminalProviderSessionId && terminalProviderSessionId === eventProviderSessionId)
|
|
1608
|
+
|| (terminalFinalSummary && terminalFinalSummary === eventFinalSummary)
|
|
1609
|
+
|| args.metadataEvent.source === 'long_generating_reconciliation'
|
|
1610
|
+
) {
|
|
1611
|
+
LOG.info('MeshEvents', `Suppressed duplicate completion with existing terminal ledger evidence for mesh ${args.meshId} session ${eventSessionId}`);
|
|
1612
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true, terminalLedgerEvidence: true };
|
|
1613
|
+
}
|
|
1614
|
+
}
|
|
1615
|
+
}
|
|
1616
|
+
const duplicateCompletion = isDuplicateMeshCompletionEvent({
|
|
1617
|
+
meshId: args.meshId,
|
|
1618
|
+
event: args.event,
|
|
1619
|
+
sessionId: eventSessionId,
|
|
1620
|
+
providerType: readNonEmptyString(args.metadataEvent.providerType) || undefined,
|
|
1621
|
+
providerSessionId: readNonEmptyString(args.metadataEvent.providerSessionId) || undefined,
|
|
1622
|
+
timestamp: eventTimestamp,
|
|
1623
|
+
finalSummary: readNonEmptyString(args.metadataEvent.finalSummary) || undefined,
|
|
1624
|
+
// Scope dedup to the coordinator daemon so two coordinators for the same mesh
|
|
1625
|
+
// don't suppress each other's completion events via shared fingerprint table.
|
|
1626
|
+
coordinatorDaemonId: workerCoordinatorDaemonId || undefined,
|
|
1627
|
+
});
|
|
1628
|
+
if (duplicateCompletion) {
|
|
1629
|
+
LOG.info('MeshEvents', `Suppressed duplicate completion for mesh ${args.meshId} session ${eventSessionId}`);
|
|
1630
|
+
return { success: true, forwarded: 0, suppressed: true, duplicateCompletion: true };
|
|
1631
|
+
}
|
|
1632
|
+
}
|
|
1633
|
+
|
|
606
1634
|
// ── Task Queue & Ledger ──
|
|
607
1635
|
let completedTaskForLedger: { id?: string } | null = null;
|
|
608
1636
|
if (args.event === 'agent:generating_completed') {
|
|
@@ -611,24 +1639,31 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
611
1639
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
612
1640
|
|
|
613
1641
|
if (sessionId) {
|
|
614
|
-
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed'
|
|
1642
|
+
const completedTask = updateSessionTaskStatus(args.meshId, sessionId, 'completed', {
|
|
1643
|
+
occurredAt: eventTimestamp !== null ? new Date(eventTimestamp).toISOString() : undefined,
|
|
1644
|
+
});
|
|
615
1645
|
completedTaskForLedger = completedTask ? { id: completedTask.id } : null;
|
|
1646
|
+
updateDirectDispatchStatus(args.meshId, sessionId, 'completed');
|
|
1647
|
+
setImmediate(() => cleanupTerminalDirectDispatches());
|
|
616
1648
|
if (nodeId && providerType) {
|
|
617
|
-
|
|
618
|
-
setTimeout(() => {
|
|
619
|
-
tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
620
|
-
}, 500);
|
|
1649
|
+
runIdleMaintenanceThenAssignQueue(components, { meshId: args.meshId, nodeId, sessionId, providerType });
|
|
621
1650
|
}
|
|
622
1651
|
}
|
|
623
1652
|
} else if (args.event === 'agent:ready') {
|
|
624
1653
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
625
1654
|
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
626
1655
|
const providerType = readNonEmptyString(args.metadataEvent.providerType);
|
|
627
|
-
const
|
|
1656
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
|
|
1657
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
|
|
1658
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
1659
|
+
const hasCompletionEvidence = !!finalSummary || !!workerResult;
|
|
1660
|
+
const completedTask = sessionId && hasCompletionEvidence
|
|
628
1661
|
? updateSessionTaskStatus(args.meshId, sessionId, 'completed')
|
|
629
1662
|
: null;
|
|
630
|
-
if (completedTask) {
|
|
1663
|
+
if (completedTask && sessionId) {
|
|
631
1664
|
completedTaskForLedger = { id: completedTask.id };
|
|
1665
|
+
updateDirectDispatchStatus(args.meshId, sessionId, 'completed');
|
|
1666
|
+
setImmediate(() => cleanupTerminalDirectDispatches());
|
|
632
1667
|
try {
|
|
633
1668
|
appendLedgerEntry(args.meshId, {
|
|
634
1669
|
kind: 'task_completed',
|
|
@@ -640,15 +1675,17 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
640
1675
|
nodeLabel: args.nodeLabel,
|
|
641
1676
|
taskId: completedTask.id,
|
|
642
1677
|
completedViaReady: true,
|
|
643
|
-
providerSessionId
|
|
644
|
-
finalSummary
|
|
1678
|
+
providerSessionId,
|
|
1679
|
+
finalSummary,
|
|
1680
|
+
workerResult,
|
|
645
1681
|
evidence: buildTaskCompletionEvidence({
|
|
646
1682
|
event: 'agent:ready',
|
|
647
1683
|
nodeId,
|
|
648
1684
|
sessionId,
|
|
649
1685
|
providerType: providerType || undefined,
|
|
650
|
-
providerSessionId
|
|
651
|
-
finalSummary
|
|
1686
|
+
providerSessionId,
|
|
1687
|
+
finalSummary,
|
|
1688
|
+
workerResult,
|
|
652
1689
|
}),
|
|
653
1690
|
},
|
|
654
1691
|
});
|
|
@@ -658,13 +1695,22 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
658
1695
|
}
|
|
659
1696
|
|
|
660
1697
|
if (sessionId && nodeId && providerType) {
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
1698
|
+
sweepExpiredRemoteIdleSessions();
|
|
1699
|
+
remoteIdleSessions.set(`${nodeId}:${sessionId}`, {
|
|
1700
|
+
nodeId, sessionId, providerType,
|
|
1701
|
+
expiresAt: Date.now() + REMOTE_IDLE_SESSION_TTL_MS,
|
|
1702
|
+
});
|
|
1703
|
+
setImmediate(() => {
|
|
1704
|
+
maybeAutoFastForwardIdleNode(components, { meshId: args.meshId, nodeId, sessionId, providerType })
|
|
1705
|
+
.finally(() => {
|
|
1706
|
+
try {
|
|
1707
|
+
const assigned = tryAssignQueueTask(components, args.meshId, nodeId, sessionId, providerType);
|
|
1708
|
+
if (assigned) remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
1709
|
+
} catch (e: any) {
|
|
1710
|
+
LOG.warn('MeshQueue', `Failed to assign idle queue task after maintenance for ${nodeId}: ${e?.message || e}`);
|
|
1711
|
+
}
|
|
1712
|
+
});
|
|
1713
|
+
});
|
|
668
1714
|
}
|
|
669
1715
|
} else if (args.event === 'agent:generating_started') {
|
|
670
1716
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
@@ -672,6 +1718,10 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
672
1718
|
if (sessionId && nodeId) {
|
|
673
1719
|
remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
674
1720
|
}
|
|
1721
|
+
if (sessionId) {
|
|
1722
|
+
// Mark direct dispatch as acknowledged — the session started generating.
|
|
1723
|
+
updateDirectDispatchStatus(args.meshId, sessionId, 'acked');
|
|
1724
|
+
}
|
|
675
1725
|
} else if (args.event === 'agent:stopped') {
|
|
676
1726
|
const sessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId);
|
|
677
1727
|
const nodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId);
|
|
@@ -679,7 +1729,9 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
679
1729
|
remoteIdleSessions.delete(`${nodeId}:${sessionId}`);
|
|
680
1730
|
}
|
|
681
1731
|
if (sessionId) {
|
|
682
|
-
updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
1732
|
+
const failedTask = updateSessionTaskStatus(args.meshId, sessionId, 'failed');
|
|
1733
|
+
completedTaskForLedger = failedTask ? { id: failedTask.id } : null;
|
|
1734
|
+
updateDirectDispatchStatus(args.meshId, sessionId, 'failed');
|
|
683
1735
|
}
|
|
684
1736
|
}
|
|
685
1737
|
|
|
@@ -689,14 +1741,18 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
689
1741
|
const ledgerNodeId = readNonEmptyString(args.nodeId) || readNonEmptyString(args.metadataEvent.meshNodeId) || undefined;
|
|
690
1742
|
const ledgerSessionId = resolveEventSessionId(args.metadataEvent, args.sourceInstanceId) || undefined;
|
|
691
1743
|
const ledgerProviderType = readNonEmptyString(args.metadataEvent.providerType) || undefined;
|
|
1744
|
+
const providerSessionId = readNonEmptyString(args.metadataEvent.providerSessionId) || undefined;
|
|
1745
|
+
const finalSummary = readNonEmptyString(args.metadataEvent.finalSummary) || undefined;
|
|
1746
|
+
const workerResult = readWorkerResultMetadata(args.metadataEvent);
|
|
692
1747
|
const completionEvidence = ledgerKind === 'task_completed' && ledgerNodeId && ledgerSessionId
|
|
693
1748
|
? buildTaskCompletionEvidence({
|
|
694
1749
|
event: 'agent:generating_completed',
|
|
695
1750
|
nodeId: ledgerNodeId,
|
|
696
1751
|
sessionId: ledgerSessionId,
|
|
697
1752
|
providerType: ledgerProviderType,
|
|
698
|
-
providerSessionId
|
|
699
|
-
finalSummary
|
|
1753
|
+
providerSessionId,
|
|
1754
|
+
finalSummary,
|
|
1755
|
+
workerResult,
|
|
700
1756
|
})
|
|
701
1757
|
: undefined;
|
|
702
1758
|
appendLedgerEntry(args.meshId, {
|
|
@@ -708,8 +1764,12 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
708
1764
|
event: args.event,
|
|
709
1765
|
nodeLabel: args.nodeLabel,
|
|
710
1766
|
taskId: completedTaskForLedger?.id || undefined,
|
|
711
|
-
providerSessionId
|
|
712
|
-
finalSummary
|
|
1767
|
+
providerSessionId,
|
|
1768
|
+
finalSummary,
|
|
1769
|
+
workerResult,
|
|
1770
|
+
completionDiagnostic: args.metadataEvent.completionDiagnostic && typeof args.metadataEvent.completionDiagnostic === 'object'
|
|
1771
|
+
? args.metadataEvent.completionDiagnostic
|
|
1772
|
+
: undefined,
|
|
713
1773
|
evidence: completionEvidence,
|
|
714
1774
|
},
|
|
715
1775
|
});
|
|
@@ -782,15 +1842,33 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
782
1842
|
}
|
|
783
1843
|
}
|
|
784
1844
|
|
|
1845
|
+
const messageText = buildMeshSystemMessage({
|
|
1846
|
+
event: args.event,
|
|
1847
|
+
nodeLabel: args.nodeLabel,
|
|
1848
|
+
metadataEvent: args.metadataEvent,
|
|
1849
|
+
recoveryContext,
|
|
1850
|
+
});
|
|
1851
|
+
if (!messageText) return { success: false, error: 'unsupported mesh event' };
|
|
1852
|
+
|
|
785
1853
|
const coordinatorInstances = components.instanceManager.getByCategory('cli').filter((inst) => {
|
|
786
1854
|
const instState = inst.getState();
|
|
787
1855
|
if (instState.settings?.meshCoordinatorFor !== args.meshId) return false;
|
|
788
1856
|
if (args.sourceInstanceId && instState.instanceId === args.sourceInstanceId) return false;
|
|
1857
|
+
// If the worker knows which coordinator daemon launched it, only route to coordinators
|
|
1858
|
+
// on that specific daemon. This prevents cross-contamination when multiple coordinator
|
|
1859
|
+
// sessions run simultaneously for the same mesh on different daemons.
|
|
1860
|
+
if (workerCoordinatorDaemonId && localDaemonId && workerCoordinatorDaemonId !== localDaemonId) return false;
|
|
789
1861
|
return true;
|
|
790
1862
|
});
|
|
791
1863
|
|
|
1864
|
+
// Refine terminal events (refine:completed, refine:failed) are coordinator-delivered
|
|
1865
|
+
// synchronously; only buffer them for MCP when no CLI coordinator is present.
|
|
1866
|
+
// Agent runtime events (agent:*) use dual delivery so both CLI and MCP coordinators
|
|
1867
|
+
// receive them regardless of whether a live CLI coordinator session is active.
|
|
1868
|
+
const isRefineTerminalEvent = REFINE_TERMINAL_EVENTS.has(args.event);
|
|
1869
|
+
|
|
792
1870
|
if (coordinatorInstances.length === 0) {
|
|
793
|
-
// No CLI coordinator
|
|
1871
|
+
// No local CLI coordinator — buffer for MCP-based coordinator on the target daemon.
|
|
794
1872
|
if (queuePendingMeshCoordinatorEvent({
|
|
795
1873
|
event: args.event,
|
|
796
1874
|
meshId: args.meshId,
|
|
@@ -801,20 +1879,69 @@ function injectMeshSystemMessage(components: DaemonComponents, args: {
|
|
|
801
1879
|
...args.metadataEvent,
|
|
802
1880
|
...(recoveryContext ? { recoveryContext } : {}),
|
|
803
1881
|
},
|
|
1882
|
+
coordinatorMessage: messageText,
|
|
804
1883
|
queuedAt: Date.now(),
|
|
1884
|
+
// Scope to the coordinator daemon that launched this worker so drain
|
|
1885
|
+
// by other coordinators on the same daemon doesn't consume this event.
|
|
1886
|
+
...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
|
|
805
1887
|
})) {
|
|
806
|
-
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
1888
|
+
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId}${workerCoordinatorDaemonId ? `, coordinator daemon ${workerCoordinatorDaemonId}` : ''})`);
|
|
807
1889
|
}
|
|
808
1890
|
return { success: true, forwarded: 0 };
|
|
809
1891
|
}
|
|
810
1892
|
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
1893
|
+
// CLI coordinator is present. For non-refine events, also buffer for MCP coordinators
|
|
1894
|
+
// that poll via get_pending_mesh_events (dual delivery). Refine terminal events are
|
|
1895
|
+
// forwarded directly only — they must not accumulate in the pending queue when a live
|
|
1896
|
+
// coordinator already received them.
|
|
1897
|
+
//
|
|
1898
|
+
// Exception: if ALL live CLI coordinator instances are in a generating/active state
|
|
1899
|
+
// when a refine terminal event fires (e.g. a Codex CLI coordinator that triggered an
|
|
1900
|
+
// async refine job and is still in the generating turn that sent it), the coordinator
|
|
1901
|
+
// cannot immediately receive send_message input. Buffer the event to the pending queue
|
|
1902
|
+
// so it is available via get_pending_mesh_events when the coordinator returns to idle.
|
|
1903
|
+
// Critically: do NOT attempt send_message injection into a generating PTY coordinator for
|
|
1904
|
+
// terminal refine events — injecting text into an active PTY can corrupt the input stream
|
|
1905
|
+
// and leave the coordinator stuck in generating state, unable to process the refine result.
|
|
1906
|
+
const allCoordinatorsGenerating = isRefineTerminalEvent && coordinatorInstances.every((inst) => {
|
|
1907
|
+
const s = inst.getState();
|
|
1908
|
+
const status = readNonEmptyString(s.status).toLowerCase();
|
|
1909
|
+
const activeChatStatus = readNonEmptyString(s.activeChat?.status).toLowerCase();
|
|
1910
|
+
return status === 'generating' || status === 'streaming' || status === 'long_generating'
|
|
1911
|
+
|| activeChatStatus === 'generating' || activeChatStatus === 'streaming';
|
|
816
1912
|
});
|
|
817
|
-
|
|
1913
|
+
|
|
1914
|
+
if (!isRefineTerminalEvent || allCoordinatorsGenerating) {
|
|
1915
|
+
if (queuePendingMeshCoordinatorEvent({
|
|
1916
|
+
event: args.event,
|
|
1917
|
+
meshId: args.meshId,
|
|
1918
|
+
nodeLabel: args.nodeLabel,
|
|
1919
|
+
nodeId: args.nodeId || undefined,
|
|
1920
|
+
workspace: readNonEmptyString(args.metadataEvent.workspace),
|
|
1921
|
+
metadataEvent: {
|
|
1922
|
+
...args.metadataEvent,
|
|
1923
|
+
...(recoveryContext ? { recoveryContext } : {}),
|
|
1924
|
+
},
|
|
1925
|
+
coordinatorMessage: messageText,
|
|
1926
|
+
queuedAt: Date.now(),
|
|
1927
|
+
...(workerCoordinatorDaemonId ? { targetCoordinatorDaemonId: workerCoordinatorDaemonId } : {}),
|
|
1928
|
+
})) {
|
|
1929
|
+
if (allCoordinatorsGenerating) {
|
|
1930
|
+
LOG.info('MeshEvents', `Queued ${args.event} for generating CLI coordinator (mesh ${args.meshId}) — will be delivered via get_pending_mesh_events when coordinator returns to idle`);
|
|
1931
|
+
} else {
|
|
1932
|
+
LOG.info('MeshEvents', `Queued ${args.event} for MCP coordinator (mesh ${args.meshId})`);
|
|
1933
|
+
}
|
|
1934
|
+
}
|
|
1935
|
+
}
|
|
1936
|
+
|
|
1937
|
+
// When all CLI coordinators are actively generating and a terminal refine event fires,
|
|
1938
|
+
// skip send_message injection entirely. The event is already buffered to the pending queue.
|
|
1939
|
+
// Injecting into a generating PTY coordinator can corrupt its input stream and cause it to
|
|
1940
|
+
// remain stuck in generating state, never processing the refine result.
|
|
1941
|
+
// The coordinator will drain pending events via get_pending_mesh_events on its next idle cycle.
|
|
1942
|
+
if (allCoordinatorsGenerating) {
|
|
1943
|
+
return { success: true, forwarded: 0, bufferedForGeneratingCoordinator: true };
|
|
1944
|
+
}
|
|
818
1945
|
|
|
819
1946
|
for (const coord of coordinatorInstances) {
|
|
820
1947
|
const coordState = coord.getState();
|
|
@@ -835,6 +1962,11 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
835
1962
|
const nodeId = readNonEmptyString(payload.nodeId);
|
|
836
1963
|
const workspace = readNonEmptyString(payload.workspace);
|
|
837
1964
|
const nodeLabel = nodeId ? `Node '${nodeId}'` : workspace ? `Agent at ${workspace}` : 'Remote agent';
|
|
1965
|
+
const relayModalMessage = readNonEmptyString(payload.modalMessage);
|
|
1966
|
+
const relayModalButtons = Array.isArray(payload.modalButtons)
|
|
1967
|
+
? (payload.modalButtons as unknown[]).filter((b): b is string => typeof b === 'string' && b.trim().length > 0)
|
|
1968
|
+
: null;
|
|
1969
|
+
|
|
838
1970
|
return injectMeshSystemMessage(components, {
|
|
839
1971
|
meshId,
|
|
840
1972
|
nodeId,
|
|
@@ -845,6 +1977,21 @@ export function handleMeshForwardEvent(components: DaemonComponents, payload: Re
|
|
|
845
1977
|
providerType: readNonEmptyString(payload.providerType),
|
|
846
1978
|
providerSessionId: readNonEmptyString(payload.providerSessionId),
|
|
847
1979
|
finalSummary: readNonEmptyString(payload.finalSummary) || readNonEmptyString(payload.summary),
|
|
1980
|
+
jobId: readNonEmptyString(payload.jobId),
|
|
1981
|
+
interactionId: readNonEmptyString(payload.interactionId),
|
|
1982
|
+
status: readNonEmptyString(payload.status),
|
|
1983
|
+
targetDaemonId: readNonEmptyString(payload.targetDaemonId),
|
|
1984
|
+
startedAt: readNonEmptyString(payload.startedAt),
|
|
1985
|
+
completedAt: readNonEmptyString(payload.completedAt),
|
|
1986
|
+
retryOfJobId: readNonEmptyString(payload.retryOfJobId),
|
|
1987
|
+
...(relayModalMessage ? { modalMessage: relayModalMessage } : {}),
|
|
1988
|
+
...(relayModalButtons && relayModalButtons.length > 0 ? { modalButtons: relayModalButtons } : {}),
|
|
1989
|
+
...(payload.result && typeof payload.result === 'object' && !Array.isArray(payload.result) ? { result: payload.result } : {}),
|
|
1990
|
+
...(payload.completionDiagnostic && typeof payload.completionDiagnostic === 'object' && !Array.isArray(payload.completionDiagnostic) ? { completionDiagnostic: payload.completionDiagnostic } : {}),
|
|
1991
|
+
...(payload.workerResult && typeof payload.workerResult === 'object' && !Array.isArray(payload.workerResult) ? { workerResult: payload.workerResult } : {}),
|
|
1992
|
+
...(payload.meshWorkerResult && typeof payload.meshWorkerResult === 'object' && !Array.isArray(payload.meshWorkerResult) ? { meshWorkerResult: payload.meshWorkerResult } : {}),
|
|
1993
|
+
...(payload.structuredResult && typeof payload.structuredResult === 'object' && !Array.isArray(payload.structuredResult) ? { structuredResult: payload.structuredResult } : {}),
|
|
1994
|
+
...(payload.timestamp !== undefined ? { timestamp: payload.timestamp } : {}),
|
|
848
1995
|
intentional: payload.intentional === true,
|
|
849
1996
|
intentionalStop: payload.intentionalStop === true,
|
|
850
1997
|
operatorCleanup: payload.operatorCleanup === true,
|
|
@@ -872,21 +2019,37 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
|
|
|
872
2019
|
if (!workspace) return;
|
|
873
2020
|
const settings = state.settings && typeof state.settings === 'object' ? state.settings as Record<string, unknown> : {};
|
|
874
2021
|
|
|
875
|
-
// Coordinator sessions must
|
|
876
|
-
//
|
|
877
|
-
|
|
2022
|
+
// Coordinator sessions normally must not inject events into themselves, but a
|
|
2023
|
+
// coordinator session can also be the direct-dispatch target of mesh_send_task
|
|
2024
|
+
// issued by another coordinator (e.g. a remote orchestrator delegated a task to
|
|
2025
|
+
// this local coordinator). In that case the completion event must still flow into
|
|
2026
|
+
// injectMeshSystemMessage so the ledger records task_completed and the *other*
|
|
2027
|
+
// coordinator's pendingCoordinatorEvents queue is populated. Skip only when this
|
|
2028
|
+
// session is purely a coordinator with no in-flight direct dispatch waiting on it.
|
|
2029
|
+
const coordinatorMeshId = readNonEmptyString(settings.meshCoordinatorFor);
|
|
2030
|
+
let meshIdFromDirectDispatch = '';
|
|
2031
|
+
if (coordinatorMeshId) {
|
|
2032
|
+
try {
|
|
2033
|
+
const activeDispatches = getActiveDirectDispatches(coordinatorMeshId);
|
|
2034
|
+
if (activeDispatches.some(d => d.sessionId === instanceId) || hasUnterminalDirectDispatchLedgerEntry(coordinatorMeshId, instanceId)) {
|
|
2035
|
+
meshIdFromDirectDispatch = coordinatorMeshId;
|
|
2036
|
+
}
|
|
2037
|
+
} catch { /* best-effort */ }
|
|
2038
|
+
if (!meshIdFromDirectDispatch) return;
|
|
2039
|
+
}
|
|
878
2040
|
|
|
879
|
-
const meshIdFromRuntime = readNonEmptyString(settings.meshNodeFor);
|
|
2041
|
+
const meshIdFromRuntime = readNonEmptyString(settings.meshNodeFor) || meshIdFromDirectDispatch;
|
|
880
2042
|
|
|
881
2043
|
// Only forward events for sessions that were explicitly launched as mesh-node delegates
|
|
882
|
-
// (meshNodeFor set by mesh_launch_session)
|
|
2044
|
+
// (meshNodeFor set by mesh_launch_session), carry the launchedByCoordinator flag, or
|
|
2045
|
+
// have an active direct-dispatch entry (mesh_send_task to a pre-existing session).
|
|
883
2046
|
// Do NOT fall back to workspace-based mesh lookup: that would pick up coordinator sessions
|
|
884
2047
|
// and any other CLI session that happens to share the same workspace, causing spurious
|
|
885
2048
|
// system-message injection into the coordinator's own conversation.
|
|
886
2049
|
const isMeshDelegate = Boolean(meshIdFromRuntime || settings.launchedByCoordinator);
|
|
887
2050
|
if (!isMeshDelegate) return;
|
|
888
2051
|
|
|
889
|
-
const mesh = meshIdFromRuntime ? getMeshWithCache(components, meshIdFromRuntime) :
|
|
2052
|
+
const mesh = meshIdFromRuntime ? getMeshWithCache(components, meshIdFromRuntime) : getCachedMeshByWorkspace(workspace);
|
|
890
2053
|
const meshId = meshIdFromRuntime || readNonEmptyString(mesh?.id);
|
|
891
2054
|
if (!meshId) return;
|
|
892
2055
|
|