@adhdev/daemon-core 0.9.82-rc.20 → 0.9.82-rc.201
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/boot/daemon-lifecycle.d.ts +1 -0
- package/dist/boot/process-hardening.d.ts +50 -0
- package/dist/chat/source-machine.d.ts +166 -0
- package/dist/chat/source-resolver.d.ts +104 -0
- package/dist/chat/subscription-updates.d.ts +1 -0
- package/dist/cli-adapter-types.d.ts +23 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +178 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +29 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/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 +39 -10
- package/dist/index.js +25488 -8333
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +29124 -12045
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +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-delivery-policy.d.ts +126 -0
- package/dist/mesh/mesh-events.d.ts +164 -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-runtime-store.d.ts +149 -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 +13 -1
- package/dist/providers/provider-loader.d.ts +26 -4
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +60 -0
- package/dist/providers/spec/cli-adapter.d.ts +101 -0
- package/dist/providers/spec/driver.d.ts +195 -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 +599 -0
- package/dist/providers/spec/types.d.ts +282 -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-extra.d.ts +1 -1
- package/dist/shared-types.d.ts +42 -1
- package/dist/status/normalize.d.ts +1 -1
- package/dist/status/normalize.js +1 -0
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +1 -0
- package/dist/status/normalize.mjs.map +1 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +7 -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 +22 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1138 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +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 +3628 -515
- 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 +119 -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/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-delivery-policy.ts +298 -0
- package/src/mesh/mesh-events.ts +1341 -109
- 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-runtime-store.ts +769 -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 +806 -64
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +141 -6
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +19 -5
- package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +419 -0
- package/src/providers/native-history/dispatcher.ts +340 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +30 -0
- package/src/providers/provider-instance.ts +10 -1
- package/src/providers/provider-loader.ts +582 -50
- package/src/providers/provider-schema.ts +87 -14
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +229 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +176 -0
- package/src/providers/spec/cli-adapter.ts +573 -0
- package/src/providers/spec/driver.ts +733 -0
- package/src/providers/spec/evaluator.ts +373 -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 +559 -0
- package/src/providers/spec/schema.json +237 -0
- package/src/providers/spec/types.ts +301 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +425 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +118 -1
- package/src/sessions/registry.ts +3 -0
- package/src/shared-types-extra.ts +1 -1
- package/src/shared-types.ts +45 -1
- package/src/status/builders.ts +24 -6
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +15 -0
- package/src/status/snapshot.ts +84 -25
- package/src/system/host-memory.ts +29 -12
- package/src/types.ts +5 -0
- package/dist/mesh/mesh-sync.d.ts +0 -53
- package/src/mesh/mesh-sync.ts +0 -111
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MeshCoordinatorRegistry — Persisted record of active mesh coordinator sessions.
|
|
3
|
+
*
|
|
4
|
+
* Survives daemon restarts: when a CLI coordinator session is re-attached after
|
|
5
|
+
* a daemon restart the in-memory `settings.meshCoordinatorFor` on the provider
|
|
6
|
+
* instance is gone, so the registry file fills the gap.
|
|
7
|
+
*
|
|
8
|
+
* Keyed by sessionId (the CLI instance key / runtimeKey).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { join } from 'path';
|
|
12
|
+
import { existsSync, readFileSync, writeFileSync } from 'fs';
|
|
13
|
+
import { getDaemonDataDir } from '../config/config.js';
|
|
14
|
+
|
|
15
|
+
export interface CoordinatorRegistryEntry {
|
|
16
|
+
meshId: string;
|
|
17
|
+
sessionId: string;
|
|
18
|
+
workspace?: string;
|
|
19
|
+
startedAt: number;
|
|
20
|
+
/** CLI type used to launch the coordinator (claude-cli / codex-cli / …). */
|
|
21
|
+
cliType?: string;
|
|
22
|
+
/** Final system prompt sent to the coordinator after all overrides, appends,
|
|
23
|
+
* and extraSystemPrompt have been applied. Surfaced via the session-info
|
|
24
|
+
* endpoint so users can audit exactly what prompt the agent saw. */
|
|
25
|
+
systemPrompt?: string;
|
|
26
|
+
/** Per-launch extraSystemPrompt the caller passed in, if any. Stored
|
|
27
|
+
* separately from `systemPrompt` so the UI can show what the user
|
|
28
|
+
* added vs. what the daemon's default template produced. */
|
|
29
|
+
extraSystemPrompt?: string;
|
|
30
|
+
/** How the prompt was actually injected (cli_arg / context_file / …). */
|
|
31
|
+
injection?: { mode: string; target?: string };
|
|
32
|
+
/** Path of the MCP config file the daemon wrote for this session. */
|
|
33
|
+
mcpConfigPath?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const _registry = new Map<string, CoordinatorRegistryEntry>();
|
|
37
|
+
|
|
38
|
+
function getRegistryPath(): string {
|
|
39
|
+
return join(getDaemonDataDir(), 'mesh-coordinators.json');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Load persisted coordinator registry from disk into in-memory map. Called once on daemon boot. */
|
|
43
|
+
export function loadMeshCoordinatorRegistry(): void {
|
|
44
|
+
const path = getRegistryPath();
|
|
45
|
+
if (!existsSync(path)) return;
|
|
46
|
+
try {
|
|
47
|
+
const raw = JSON.parse(readFileSync(path, 'utf-8'));
|
|
48
|
+
if (!Array.isArray(raw)) return;
|
|
49
|
+
_registry.clear();
|
|
50
|
+
for (const entry of raw) {
|
|
51
|
+
if (typeof entry?.sessionId === 'string' && typeof entry?.meshId === 'string') {
|
|
52
|
+
_registry.set(entry.sessionId, entry as CoordinatorRegistryEntry);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
} catch { /* ignore corrupt file */ }
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
function saveRegistry(): void {
|
|
59
|
+
try {
|
|
60
|
+
writeFileSync(
|
|
61
|
+
getRegistryPath(),
|
|
62
|
+
JSON.stringify([..._registry.values()], null, 2),
|
|
63
|
+
{ encoding: 'utf-8', mode: 0o600 },
|
|
64
|
+
);
|
|
65
|
+
} catch { /* best-effort */ }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Register a coordinator session. Persists to disk immediately. */
|
|
69
|
+
export function registerMeshCoordinator(entry: CoordinatorRegistryEntry): void {
|
|
70
|
+
_registry.set(entry.sessionId, entry);
|
|
71
|
+
saveRegistry();
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/** Remove a coordinator session by sessionId. Persists to disk.
|
|
75
|
+
*
|
|
76
|
+
* Also best-effort strips any context_file wrapper block we wrote into
|
|
77
|
+
* the workspace at launch time (AGENTS.md / GEMINI.md), because those
|
|
78
|
+
* files are auto-loaded by their CLIs on every subsequent launch — and
|
|
79
|
+
* a wrapper block surviving the coordinator session would silently
|
|
80
|
+
* inject the coordinator system prompt into ordinary non-coordinator
|
|
81
|
+
* sessions in the same workspace, which is exactly the bug we're
|
|
82
|
+
* fixing here.
|
|
83
|
+
*
|
|
84
|
+
* Stripping rules:
|
|
85
|
+
* - Look for the start sentinel anywhere in the file; if absent,
|
|
86
|
+
* leave the file alone (user-authored content).
|
|
87
|
+
* - If the wrapper block was the only thing in the file, delete the
|
|
88
|
+
* file outright so we don't leave behind an empty AGENTS.md.
|
|
89
|
+
* - Otherwise drop only the block between the sentinels and trim a
|
|
90
|
+
* leading/trailing blank line so we don't leave dangling separators.
|
|
91
|
+
*/
|
|
92
|
+
export function unregisterMeshCoordinator(sessionId: string): void {
|
|
93
|
+
const entry = _registry.get(sessionId);
|
|
94
|
+
if (!_registry.delete(sessionId)) return;
|
|
95
|
+
saveRegistry();
|
|
96
|
+
if (!entry) return;
|
|
97
|
+
const workspace = entry.workspace;
|
|
98
|
+
const target = entry.injection?.mode === 'context_file' && typeof entry.injection.target === 'string'
|
|
99
|
+
? entry.injection.target
|
|
100
|
+
: '';
|
|
101
|
+
if (!workspace || !target) return;
|
|
102
|
+
// injection.target is workspace-relative (AGENTS.md / GEMINI.md); we don't
|
|
103
|
+
// resolve absolute paths here because we never wrote one — applyMesh-
|
|
104
|
+
// CoordinatorSystemPromptInjection joins via path.join(workspace, …).
|
|
105
|
+
const filePath = `${workspace.replace(/\/$/, '')}/${target}`;
|
|
106
|
+
try {
|
|
107
|
+
// eslint-disable-next-line @typescript-eslint/no-require-imports
|
|
108
|
+
const { stripCoordinatorWrapperFile } = require('../commands/mesh-coordinator.js');
|
|
109
|
+
stripCoordinatorWrapperFile(filePath);
|
|
110
|
+
} catch { /* best-effort cleanup; never throw out of unregister */ }
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/** Look up a coordinator entry by session ID. Returns undefined if not registered. */
|
|
114
|
+
export function getCoordinatorForSession(sessionId: string): CoordinatorRegistryEntry | undefined {
|
|
115
|
+
return _registry.get(sessionId);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** List all coordinator entries for a given workspace path. */
|
|
119
|
+
export function listCoordinatorsForWorkspace(workspace: string): CoordinatorRegistryEntry[] {
|
|
120
|
+
return [..._registry.values()].filter(e => e.workspace === workspace);
|
|
121
|
+
}
|
|
@@ -0,0 +1,437 @@
|
|
|
1
|
+
import type { MeshLedgerEntry } from './mesh-ledger.js';
|
|
2
|
+
import type { MeshWorkQueueEntry, DirectDispatchRecord } from './mesh-work-queue.js';
|
|
3
|
+
|
|
4
|
+
export type MeshActiveWorkSource = 'queue' | 'direct';
|
|
5
|
+
export type MeshActiveWorkStatus = 'pending' | 'assigned' | 'generating' | 'idle' | 'failed' | 'awaiting_approval';
|
|
6
|
+
|
|
7
|
+
export interface MeshActiveWorkRecord {
|
|
8
|
+
taskId: string;
|
|
9
|
+
source: MeshActiveWorkSource;
|
|
10
|
+
status: MeshActiveWorkStatus;
|
|
11
|
+
nodeId?: string;
|
|
12
|
+
sessionId?: string;
|
|
13
|
+
providerType?: string;
|
|
14
|
+
taskTitle: string;
|
|
15
|
+
taskSummary: string;
|
|
16
|
+
message?: string;
|
|
17
|
+
taskMode?: string;
|
|
18
|
+
createdAt: string;
|
|
19
|
+
updatedAt: string;
|
|
20
|
+
dispatchedAt?: string;
|
|
21
|
+
elapsedMs: number;
|
|
22
|
+
terminal?: boolean;
|
|
23
|
+
terminalKind?: string;
|
|
24
|
+
terminalAt?: string;
|
|
25
|
+
staleReason?: string;
|
|
26
|
+
/**
|
|
27
|
+
* When true, the session targeted by this direct dispatch still exists in the live mesh
|
|
28
|
+
* but did not transition to generating — the dispatch was not acknowledged by the provider.
|
|
29
|
+
* Distinct from historical/orphaned stale entries where the node/session is gone.
|
|
30
|
+
*/
|
|
31
|
+
staleDispatchUnacknowledged?: boolean;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface MeshActiveWorkSummary {
|
|
35
|
+
totalActiveCount: number;
|
|
36
|
+
queueActiveCount: number;
|
|
37
|
+
directActiveCount: number;
|
|
38
|
+
awaitingApprovalCount: number;
|
|
39
|
+
generatingCount: number;
|
|
40
|
+
failedCount: number;
|
|
41
|
+
idleCount: number;
|
|
42
|
+
sourceCounts: Record<MeshActiveWorkSource, number>;
|
|
43
|
+
statusCounts: Record<MeshActiveWorkStatus, number>;
|
|
44
|
+
staleDirectCount: number;
|
|
45
|
+
/**
|
|
46
|
+
* Count of stale direct entries where the target session still exists in the live mesh
|
|
47
|
+
* but never transitioned to generating — these are fresh dispatch failures, not historical orphans.
|
|
48
|
+
* Requires immediate recovery: launch a fresh session and retry the task.
|
|
49
|
+
*/
|
|
50
|
+
staleDirectUnacknowledgedCount?: number;
|
|
51
|
+
/**
|
|
52
|
+
* When staleDirectCount > 0, this note clarifies what kind of stale records exist.
|
|
53
|
+
* Fresh unacknowledged dispatches carry an actionable recovery note; orphaned historical
|
|
54
|
+
* entries carry the "historical evidence only" note. Both may coexist.
|
|
55
|
+
*/
|
|
56
|
+
staleDirectNote?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface MeshStaleDirectWorkSummary {
|
|
60
|
+
count: number;
|
|
61
|
+
sampleLimit: number;
|
|
62
|
+
sample: Array<Pick<MeshActiveWorkRecord, 'taskId' | 'status' | 'nodeId' | 'sessionId' | 'taskTitle' | 'createdAt' | 'staleReason'>>;
|
|
63
|
+
reasonCounts: Record<string, number>;
|
|
64
|
+
detailHint: string;
|
|
65
|
+
note?: string;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface BuildMeshActiveWorkOptions {
|
|
69
|
+
meshId: string;
|
|
70
|
+
queue?: MeshWorkQueueEntry[];
|
|
71
|
+
ledgerEntries?: MeshLedgerEntry[];
|
|
72
|
+
/**
|
|
73
|
+
* Active direct dispatches from MeshRuntimeStore. When provided, these are used instead of
|
|
74
|
+
* scanning ledger entries for direct dispatches — eliminates the O(n_ledger) scan.
|
|
75
|
+
* Falls back to ledger scanning when not provided.
|
|
76
|
+
*/
|
|
77
|
+
directDispatches?: DirectDispatchRecord[];
|
|
78
|
+
nodes?: any[];
|
|
79
|
+
now?: number;
|
|
80
|
+
/** Include terminal direct rows (idle/failed) for handoff/recent-work surfaces. Defaults false. */
|
|
81
|
+
includeTerminalDirect?: boolean;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const DIRECT_DISPATCH_VIA = new Set(['p2p_direct', 'local_direct', 'mesh_send_task']);
|
|
85
|
+
const TERMINAL_LEDGER_KINDS = new Set(['task_completed', 'task_failed', 'task_stalled']);
|
|
86
|
+
|
|
87
|
+
function readString(value: unknown): string | undefined {
|
|
88
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function summarizeMessage(message: string): { title: string; summary: string } {
|
|
92
|
+
const oneLine = message.replace(/\s+/g, ' ').trim();
|
|
93
|
+
const title = oneLine.length > 96 ? `${oneLine.slice(0, 93)}...` : oneLine;
|
|
94
|
+
return { title: title || '(untitled task)', summary: oneLine };
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function elapsedSince(value: string | undefined, now: number): number {
|
|
98
|
+
const started = value ? new Date(value).getTime() : Number.NaN;
|
|
99
|
+
return Number.isFinite(started) ? Math.max(0, now - started) : 0;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function sessionStatusFromNodes(nodes: any[] | undefined, nodeId?: string, sessionId?: string): { status?: MeshActiveWorkStatus; staleReason?: string } {
|
|
103
|
+
if (!Array.isArray(nodes)) return {};
|
|
104
|
+
if (!nodeId) return { staleReason: 'direct task has no node id' };
|
|
105
|
+
const node = nodes.find(item => readString(item?.id) === nodeId || readString(item?.nodeId) === nodeId || readString(item?.node_id) === nodeId);
|
|
106
|
+
if (!node) return { staleReason: 'direct task node is no longer in the live mesh' };
|
|
107
|
+
if (!sessionId) return {};
|
|
108
|
+
const candidates: any[] = [];
|
|
109
|
+
for (const value of [
|
|
110
|
+
node.sessions,
|
|
111
|
+
node.activeSessions,
|
|
112
|
+
node.active_sessions,
|
|
113
|
+
node.activeSessionDetails,
|
|
114
|
+
node.active_session_details,
|
|
115
|
+
node.sessionDetails,
|
|
116
|
+
node.session_details,
|
|
117
|
+
node.lastProbe?.sessions,
|
|
118
|
+
node.last_probe?.sessions,
|
|
119
|
+
node.lastProbe?.status?.sessions,
|
|
120
|
+
node.last_probe?.status?.sessions,
|
|
121
|
+
]) {
|
|
122
|
+
if (Array.isArray(value)) candidates.push(...value);
|
|
123
|
+
}
|
|
124
|
+
for (const value of [node.activeSession, node.active_session, node.currentSession, node.current_session, node.runtimeSession, node.runtime_session, node.session]) {
|
|
125
|
+
if (value && typeof value === 'object') candidates.push(value);
|
|
126
|
+
}
|
|
127
|
+
const session = candidates.find(item => {
|
|
128
|
+
if (typeof item === 'string') return item === sessionId;
|
|
129
|
+
const id = readString(item?.id) || readString(item?.sessionId) || readString(item?.session_id) || readString(item?.runtimeSessionId) || readString(item?.instanceId);
|
|
130
|
+
return id === sessionId;
|
|
131
|
+
});
|
|
132
|
+
if (!session) return { staleReason: 'direct task session is not present in live session records' };
|
|
133
|
+
if (typeof session === 'string') return {};
|
|
134
|
+
const raw = `${readString(session.status) || ''} ${readString(session.lifecycle) || ''} ${readString(session.state) || ''} ${readString(session.activeChat?.status) || ''}`.toLowerCase();
|
|
135
|
+
if (raw.includes('approval')) return { status: 'awaiting_approval' };
|
|
136
|
+
if (raw.includes('generating') || raw.includes('running') || raw.includes('busy')) return { status: 'generating' };
|
|
137
|
+
if (raw.includes('failed') || raw.includes('stopped') || raw.includes('terminated') || raw.includes('exited')) return { status: 'failed' };
|
|
138
|
+
if (raw.includes('idle') || raw.includes('waiting_input') || raw.includes('ready')) return { status: 'idle' };
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function isDirectDispatch(entry: MeshLedgerEntry): boolean {
|
|
143
|
+
if (entry.kind !== 'task_dispatched') return false;
|
|
144
|
+
const payload = entry.payload || {};
|
|
145
|
+
if (payload.source === 'direct') return true;
|
|
146
|
+
const via = readString(payload.via);
|
|
147
|
+
return Boolean(via && DIRECT_DISPATCH_VIA.has(via) && payload.source !== 'queue');
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function directDispatchTaskId(entry: MeshLedgerEntry): string {
|
|
151
|
+
return readString(entry.payload?.taskId) || entry.id;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function terminalMatchesDispatch(terminal: MeshLedgerEntry, dispatch: MeshLedgerEntry, taskId: string): boolean {
|
|
155
|
+
const terminalTaskId = readString(terminal.payload?.taskId);
|
|
156
|
+
if (terminalTaskId && terminalTaskId === taskId) return true;
|
|
157
|
+
if (terminalTaskId && terminalTaskId !== taskId) return false;
|
|
158
|
+
if (dispatch.sessionId && terminal.sessionId === dispatch.sessionId) return true;
|
|
159
|
+
return Boolean(dispatch.nodeId && terminal.nodeId === dispatch.nodeId && !dispatch.sessionId);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function statusFromTerminal(entry: MeshLedgerEntry): MeshActiveWorkStatus {
|
|
163
|
+
if (entry.kind === 'task_approval_needed') return 'awaiting_approval';
|
|
164
|
+
if (entry.kind === 'task_completed') return 'idle';
|
|
165
|
+
return 'failed';
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]): MeshActiveWorkSummary {
|
|
169
|
+
const statusCounts: Record<MeshActiveWorkStatus, number> = {
|
|
170
|
+
pending: 0,
|
|
171
|
+
assigned: 0,
|
|
172
|
+
generating: 0,
|
|
173
|
+
idle: 0,
|
|
174
|
+
failed: 0,
|
|
175
|
+
awaiting_approval: 0,
|
|
176
|
+
};
|
|
177
|
+
const sourceCounts: Record<MeshActiveWorkSource, number> = { queue: 0, direct: 0 };
|
|
178
|
+
for (const item of activeWork) {
|
|
179
|
+
sourceCounts[item.source] += 1;
|
|
180
|
+
statusCounts[item.status] += 1;
|
|
181
|
+
}
|
|
182
|
+
const staleDirectCount = activeWork.filter(item => item.source === 'direct' && item.staleReason).length;
|
|
183
|
+
const staleDirectUnacknowledgedCount = activeWork.filter(item => item.source === 'direct' && item.staleDispatchUnacknowledged).length;
|
|
184
|
+
return {
|
|
185
|
+
totalActiveCount: activeWork.length,
|
|
186
|
+
queueActiveCount: sourceCounts.queue,
|
|
187
|
+
directActiveCount: sourceCounts.direct,
|
|
188
|
+
awaitingApprovalCount: statusCounts.awaiting_approval,
|
|
189
|
+
generatingCount: statusCounts.generating,
|
|
190
|
+
failedCount: statusCounts.failed,
|
|
191
|
+
idleCount: statusCounts.idle,
|
|
192
|
+
sourceCounts,
|
|
193
|
+
statusCounts,
|
|
194
|
+
staleDirectCount,
|
|
195
|
+
...(staleDirectUnacknowledgedCount > 0 ? { staleDirectUnacknowledgedCount } : {}),
|
|
196
|
+
...(staleDirectCount > 0 ? { staleDirectNote: 'Stale direct records are orphaned ledger entries whose node/session no longer exists. They are historical recovery evidence only — not active or unresolved work. The queue (source: queue) is authoritative for pending/assigned tasks.' } : {}),
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
|
|
201
|
+
export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeWork: MeshActiveWorkRecord[]; staleDirectWork: MeshActiveWorkRecord[]; staleDirectWorkNote?: string; terminalDirectWork: MeshActiveWorkRecord[]; summary: MeshActiveWorkSummary } {
|
|
202
|
+
const now = opts.now ?? Date.now();
|
|
203
|
+
const records: MeshActiveWorkRecord[] = [];
|
|
204
|
+
const staleDirectWork: MeshActiveWorkRecord[] = [];
|
|
205
|
+
const terminalDirectWork: MeshActiveWorkRecord[] = [];
|
|
206
|
+
|
|
207
|
+
for (const task of opts.queue || []) {
|
|
208
|
+
if (task.status !== 'pending' && task.status !== 'assigned') continue;
|
|
209
|
+
const { title, summary } = summarizeMessage(task.message || '');
|
|
210
|
+
records.push({
|
|
211
|
+
taskId: task.id,
|
|
212
|
+
source: 'queue',
|
|
213
|
+
status: task.status,
|
|
214
|
+
nodeId: task.assignedNodeId || task.targetNodeId,
|
|
215
|
+
sessionId: task.assignedSessionId || task.targetSessionId,
|
|
216
|
+
taskTitle: title,
|
|
217
|
+
taskSummary: summary,
|
|
218
|
+
message: task.message,
|
|
219
|
+
taskMode: task.taskMode,
|
|
220
|
+
createdAt: task.createdAt,
|
|
221
|
+
updatedAt: task.updatedAt,
|
|
222
|
+
dispatchedAt: task.dispatchTimestamp,
|
|
223
|
+
elapsedMs: elapsedSince(task.dispatchTimestamp || task.createdAt, now),
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// When MeshRuntimeStore direct dispatches are provided, use them for LOCAL dispatches (O(1) indexed).
|
|
228
|
+
// ALSO scan ledger for remote dispatches (P2P) whose taskIds are not in MeshRuntimeStore — these
|
|
229
|
+
// are never written to the local MeshRuntimeStore since they're dispatched from a remote daemon.
|
|
230
|
+
if (opts.directDispatches !== undefined) {
|
|
231
|
+
const dbTaskIds = new Set(opts.directDispatches.map(d => d.taskId));
|
|
232
|
+
for (const dispatch of opts.directDispatches) {
|
|
233
|
+
const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId ?? undefined, dispatch.sessionId ?? undefined);
|
|
234
|
+
const dbStatus = dispatch.status; // 'dispatched' | 'acked' | 'completed' | 'failed' | 'stale'
|
|
235
|
+
const isTerminal = dbStatus === 'completed' || dbStatus === 'failed' || dbStatus === 'stale';
|
|
236
|
+
const status: MeshActiveWorkStatus = isTerminal
|
|
237
|
+
? (dbStatus === 'completed' ? 'idle' : 'failed')
|
|
238
|
+
: live.status || (dbStatus === 'acked' ? 'generating' : 'assigned');
|
|
239
|
+
const isNoTransition = !isTerminal && !live.status;
|
|
240
|
+
const isIdleUnacknowledged = status === 'idle' && !isTerminal;
|
|
241
|
+
const ledgerOnlyStaleReason = !isTerminal && (isIdleUnacknowledged || isNoTransition || (dispatch.dispatchedToIdleSession && isIdleUnacknowledged))
|
|
242
|
+
? 'direct task dispatch has no provider acknowledgement, transcript append, or active runtime transition'
|
|
243
|
+
: undefined;
|
|
244
|
+
const isFreshUnacknowledged = Boolean(ledgerOnlyStaleReason && !live.staleReason);
|
|
245
|
+
const { title, summary } = summarizeMessage(dispatch.message || '');
|
|
246
|
+
const record: MeshActiveWorkRecord = {
|
|
247
|
+
taskId: dispatch.taskId,
|
|
248
|
+
source: 'direct',
|
|
249
|
+
status,
|
|
250
|
+
nodeId: dispatch.nodeId ?? undefined,
|
|
251
|
+
sessionId: dispatch.sessionId ?? undefined,
|
|
252
|
+
providerType: dispatch.providerType ?? undefined,
|
|
253
|
+
taskTitle: title,
|
|
254
|
+
taskSummary: summary,
|
|
255
|
+
message: dispatch.message,
|
|
256
|
+
taskMode: dispatch.taskMode ?? undefined,
|
|
257
|
+
createdAt: dispatch.dispatchedAt,
|
|
258
|
+
updatedAt: dispatch.updatedAt,
|
|
259
|
+
dispatchedAt: dispatch.dispatchedAt,
|
|
260
|
+
elapsedMs: elapsedSince(dispatch.dispatchedAt, now),
|
|
261
|
+
terminal: isTerminal,
|
|
262
|
+
terminalKind: isTerminal ? (dbStatus === 'completed' ? 'task_completed' : 'task_failed') : undefined,
|
|
263
|
+
terminalAt: isTerminal ? dispatch.updatedAt : undefined,
|
|
264
|
+
staleReason: live.staleReason || ledgerOnlyStaleReason,
|
|
265
|
+
...(isFreshUnacknowledged ? { staleDispatchUnacknowledged: true } : {}),
|
|
266
|
+
};
|
|
267
|
+
if (isTerminal) {
|
|
268
|
+
terminalDirectWork.push(record);
|
|
269
|
+
if (opts.includeTerminalDirect !== true) continue;
|
|
270
|
+
}
|
|
271
|
+
if ((live.staleReason || ledgerOnlyStaleReason) && !isTerminal) {
|
|
272
|
+
staleDirectWork.push(record);
|
|
273
|
+
continue;
|
|
274
|
+
}
|
|
275
|
+
records.push(record);
|
|
276
|
+
}
|
|
277
|
+
// Also scan ledger for remote dispatches (via p2p_direct) whose taskIds are NOT in MeshRuntimeStore.
|
|
278
|
+
// Remote daemons write their own local MeshRuntimeStore; this coordinator's MeshRuntimeStore only has local dispatches.
|
|
279
|
+
const ledgerEntries = (opts.ledgerEntries || []).slice().sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
280
|
+
const terminals = ledgerEntries.filter(entry => TERMINAL_LEDGER_KINDS.has(entry.kind) || entry.kind === 'task_approval_needed');
|
|
281
|
+
for (const dispatch of ledgerEntries.filter(isDirectDispatch)) {
|
|
282
|
+
const taskId = directDispatchTaskId(dispatch);
|
|
283
|
+
if (dbTaskIds.has(taskId)) continue; // already covered by MeshRuntimeStore path above
|
|
284
|
+
const terminal = terminals
|
|
285
|
+
.filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
|
|
286
|
+
.find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
|
|
287
|
+
const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
|
|
288
|
+
const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
|
|
289
|
+
const status = terminalStatus || live.status || 'assigned';
|
|
290
|
+
const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
|
|
291
|
+
const dispatchedToIdleSession = dispatch.payload?.dispatchedToIdleSession === true;
|
|
292
|
+
const isNoTransition = !terminalStatus && !live.status;
|
|
293
|
+
const isIdleUnacknowledged = status === 'idle';
|
|
294
|
+
const ledgerOnlyStaleReason = !terminalRow && (isIdleUnacknowledged || isNoTransition || (dispatchedToIdleSession && isIdleUnacknowledged))
|
|
295
|
+
? 'direct task dispatch has no provider acknowledgement, transcript append, or active runtime transition'
|
|
296
|
+
: undefined;
|
|
297
|
+
const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
|
|
298
|
+
const { title, summary } = summarizeMessage(message);
|
|
299
|
+
const isFreshUnacknowledged = Boolean(ledgerOnlyStaleReason && !live.staleReason);
|
|
300
|
+
const record: MeshActiveWorkRecord = {
|
|
301
|
+
taskId,
|
|
302
|
+
source: 'direct',
|
|
303
|
+
status,
|
|
304
|
+
nodeId: dispatch.nodeId,
|
|
305
|
+
sessionId: dispatch.sessionId,
|
|
306
|
+
providerType: dispatch.providerType || readString(dispatch.payload?.providerType),
|
|
307
|
+
taskTitle: readString(dispatch.payload?.taskTitle) || title,
|
|
308
|
+
taskSummary: readString(dispatch.payload?.taskSummary) || summary,
|
|
309
|
+
message,
|
|
310
|
+
taskMode: readString(dispatch.payload?.taskMode),
|
|
311
|
+
createdAt: dispatch.timestamp,
|
|
312
|
+
updatedAt: terminal?.timestamp || dispatch.timestamp,
|
|
313
|
+
dispatchedAt: dispatch.timestamp,
|
|
314
|
+
elapsedMs: elapsedSince(dispatch.timestamp, now),
|
|
315
|
+
terminal: terminalRow,
|
|
316
|
+
terminalKind: terminal?.kind,
|
|
317
|
+
terminalAt: terminal?.timestamp,
|
|
318
|
+
staleReason: live.staleReason || ledgerOnlyStaleReason,
|
|
319
|
+
...(isFreshUnacknowledged ? { staleDispatchUnacknowledged: true } : {}),
|
|
320
|
+
};
|
|
321
|
+
if (terminalRow) {
|
|
322
|
+
terminalDirectWork.push(record);
|
|
323
|
+
if (opts.includeTerminalDirect !== true) continue;
|
|
324
|
+
}
|
|
325
|
+
if ((live.staleReason || ledgerOnlyStaleReason) && !terminalRow) {
|
|
326
|
+
staleDirectWork.push(record);
|
|
327
|
+
continue;
|
|
328
|
+
}
|
|
329
|
+
records.push(record);
|
|
330
|
+
}
|
|
331
|
+
} else {
|
|
332
|
+
// Full ledger scan: no MeshRuntimeStore direct dispatches available (standalone mode or empty).
|
|
333
|
+
const ledgerEntries = (opts.ledgerEntries || []).slice().sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
334
|
+
const terminals = ledgerEntries.filter(entry => TERMINAL_LEDGER_KINDS.has(entry.kind) || entry.kind === 'task_approval_needed');
|
|
335
|
+
for (const dispatch of ledgerEntries.filter(isDirectDispatch)) {
|
|
336
|
+
const taskId = directDispatchTaskId(dispatch);
|
|
337
|
+
const terminal = terminals
|
|
338
|
+
.filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
|
|
339
|
+
.find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
|
|
340
|
+
const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
|
|
341
|
+
const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
|
|
342
|
+
const status = terminalStatus || live.status || 'assigned';
|
|
343
|
+
const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
|
|
344
|
+
const dispatchedToIdleSession = dispatch.payload?.dispatchedToIdleSession === true;
|
|
345
|
+
const isNoTransition = !terminalStatus && !live.status;
|
|
346
|
+
const isIdleUnacknowledged = status === 'idle';
|
|
347
|
+
const ledgerOnlyStaleReason = !terminalRow && (isIdleUnacknowledged || isNoTransition || (dispatchedToIdleSession && isIdleUnacknowledged))
|
|
348
|
+
? 'direct task dispatch has no provider acknowledgement, transcript append, or active runtime transition'
|
|
349
|
+
: undefined;
|
|
350
|
+
const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
|
|
351
|
+
const { title, summary } = summarizeMessage(message);
|
|
352
|
+
const isFreshUnacknowledged = Boolean(ledgerOnlyStaleReason && !live.staleReason);
|
|
353
|
+
const record: MeshActiveWorkRecord = {
|
|
354
|
+
taskId,
|
|
355
|
+
source: 'direct',
|
|
356
|
+
status,
|
|
357
|
+
nodeId: dispatch.nodeId,
|
|
358
|
+
sessionId: dispatch.sessionId,
|
|
359
|
+
providerType: dispatch.providerType || readString(dispatch.payload?.providerType),
|
|
360
|
+
taskTitle: readString(dispatch.payload?.taskTitle) || title,
|
|
361
|
+
taskSummary: readString(dispatch.payload?.taskSummary) || summary,
|
|
362
|
+
message,
|
|
363
|
+
taskMode: readString(dispatch.payload?.taskMode),
|
|
364
|
+
createdAt: dispatch.timestamp,
|
|
365
|
+
updatedAt: terminal?.timestamp || dispatch.timestamp,
|
|
366
|
+
dispatchedAt: dispatch.timestamp,
|
|
367
|
+
elapsedMs: elapsedSince(dispatch.timestamp, now),
|
|
368
|
+
terminal: terminalRow,
|
|
369
|
+
terminalKind: terminal?.kind,
|
|
370
|
+
terminalAt: terminal?.timestamp,
|
|
371
|
+
staleReason: live.staleReason || ledgerOnlyStaleReason,
|
|
372
|
+
...(isFreshUnacknowledged ? { staleDispatchUnacknowledged: true } : {}),
|
|
373
|
+
};
|
|
374
|
+
if (terminalRow) {
|
|
375
|
+
terminalDirectWork.push(record);
|
|
376
|
+
if (opts.includeTerminalDirect !== true) continue;
|
|
377
|
+
}
|
|
378
|
+
if ((live.staleReason || ledgerOnlyStaleReason) && !terminalRow) {
|
|
379
|
+
staleDirectWork.push(record);
|
|
380
|
+
continue;
|
|
381
|
+
}
|
|
382
|
+
records.push(record);
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
records.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
387
|
+
staleDirectWork.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
388
|
+
terminalDirectWork.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
389
|
+
const summary = buildMeshActiveWorkSummary(records);
|
|
390
|
+
summary.staleDirectCount = staleDirectWork.length;
|
|
391
|
+
const unacknowledgedCount = staleDirectWork.filter(r => r.staleDispatchUnacknowledged).length;
|
|
392
|
+
if (unacknowledgedCount > 0) {
|
|
393
|
+
summary.staleDirectUnacknowledgedCount = unacknowledgedCount;
|
|
394
|
+
}
|
|
395
|
+
// Build a note that accurately describes what's in the stale set.
|
|
396
|
+
// When there are fresh unacknowledged entries (node/session still live, session never started),
|
|
397
|
+
// use the actionable recovery note instead of the misleading "historical evidence only" note.
|
|
398
|
+
const staleDirectWorkNote = staleDirectWork.length > 0
|
|
399
|
+
? unacknowledgedCount > 0 && unacknowledgedCount === staleDirectWork.length
|
|
400
|
+
? `${unacknowledgedCount} direct dispatch(es) were not acknowledged by the target session — the session received the agent_command but never transitioned to generating. This is a fresh dispatch failure, not historical noise. Recovery: launch a fresh session on the same node and retry the task, or use mesh_enqueue_task for queue-based assignment.`
|
|
401
|
+
: unacknowledgedCount > 0
|
|
402
|
+
? `${unacknowledgedCount} of ${staleDirectWork.length} stale direct record(s) are fresh unacknowledged dispatch failures (session still live but never transitioned to generating); the rest are orphaned historical entries whose node/session no longer exists. Fresh unacknowledged dispatches need recovery: launch a fresh session and retry. Orphaned entries are historical evidence only — not active or unresolved work.`
|
|
403
|
+
: 'These are orphaned ledger entries whose original node or session no longer exists in the live mesh. They are historical/recovery evidence only — not active or unresolved work. Do not treat staleDirectCount as a status mismatch; use the queue (source: queue) as authoritative for pending/assigned tasks.'
|
|
404
|
+
: undefined;
|
|
405
|
+
if (staleDirectWorkNote) {
|
|
406
|
+
summary.staleDirectNote = staleDirectWorkNote;
|
|
407
|
+
}
|
|
408
|
+
return { activeWork: records, staleDirectWork, staleDirectWorkNote, terminalDirectWork, summary };
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
export function buildCompactStaleDirectWorkSummary(
|
|
412
|
+
staleDirectWork: MeshActiveWorkRecord[],
|
|
413
|
+
opts: { sampleLimit?: number; detailHint?: string; note?: string } = {},
|
|
414
|
+
): MeshStaleDirectWorkSummary {
|
|
415
|
+
const sampleLimit = Math.max(0, Math.min(10, Math.floor(opts.sampleLimit ?? 3)));
|
|
416
|
+
const reasonCounts: Record<string, number> = {};
|
|
417
|
+
for (const entry of staleDirectWork) {
|
|
418
|
+
const reason = entry.staleReason || 'unknown';
|
|
419
|
+
reasonCounts[reason] = (reasonCounts[reason] || 0) + 1;
|
|
420
|
+
}
|
|
421
|
+
return {
|
|
422
|
+
count: staleDirectWork.length,
|
|
423
|
+
sampleLimit,
|
|
424
|
+
sample: staleDirectWork.slice(0, sampleLimit).map(entry => ({
|
|
425
|
+
taskId: entry.taskId,
|
|
426
|
+
status: entry.status,
|
|
427
|
+
nodeId: entry.nodeId,
|
|
428
|
+
sessionId: entry.sessionId,
|
|
429
|
+
taskTitle: entry.taskTitle,
|
|
430
|
+
createdAt: entry.createdAt,
|
|
431
|
+
staleReason: entry.staleReason,
|
|
432
|
+
})),
|
|
433
|
+
reasonCounts,
|
|
434
|
+
detailHint: opts.detailHint || 'Stale direct records are historical recovery evidence only. Use mesh_task_history for full ledger details, or request includeStaleDirectWorkDetails when supported by the caller.',
|
|
435
|
+
...(opts.note ? { note: opts.note } : {}),
|
|
436
|
+
};
|
|
437
|
+
}
|