@adhdev/daemon-core 0.9.82-rc.1 → 0.9.82-rc.100
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 +2 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +10 -0
- package/dist/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +10 -0
- package/dist/commands/router.d.ts +24 -0
- package/dist/config/mesh-config.d.ts +66 -1
- package/dist/git/git-commands.d.ts +1 -0
- package/dist/git/git-status.d.ts +5 -0
- package/dist/git/git-types.d.ts +10 -0
- package/dist/index.d.ts +13 -6
- package/dist/index.js +6068 -1214
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +6033 -1201
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/beads-db.d.ts +18 -0
- package/dist/mesh/mesh-active-work.d.ts +60 -0
- package/dist/mesh/mesh-events.d.ts +29 -5
- package/dist/mesh/mesh-fast-forward.d.ts +39 -0
- package/dist/mesh/mesh-host-ownership.d.ts +9 -0
- package/dist/mesh/mesh-ledger.d.ts +38 -1
- package/dist/mesh/mesh-work-queue.d.ts +27 -5
- package/dist/mesh/refine-config.d.ts +176 -0
- package/dist/providers/chat-message-normalization.d.ts +1 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -1
- package/dist/repo-mesh-types.d.ts +167 -0
- package/dist/status/reporter.d.ts +2 -0
- package/package.json +3 -1
- package/src/boot/daemon-lifecycle.ts +4 -0
- package/src/cli-adapters/provider-cli-adapter.ts +255 -14
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +4 -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 +28 -10
- package/src/commands/chat-commands.ts +570 -20
- package/src/commands/cli-manager.ts +129 -1
- package/src/commands/handler.ts +8 -1
- package/src/commands/mesh-coordinator.ts +13 -143
- package/src/commands/router.ts +3095 -406
- package/src/config/chat-history.ts +9 -7
- package/src/config/mesh-config.ts +245 -1
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +3 -3
- package/src/git/git-status.ts +97 -6
- package/src/git/git-summary.ts +3 -0
- package/src/git/git-types.ts +11 -0
- package/src/index.ts +39 -5
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- 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 +176 -0
- package/src/mesh/coordinator-prompt.ts +31 -8
- package/src/mesh/mesh-active-work.ts +255 -0
- package/src/mesh/mesh-events.ts +400 -47
- package/src/mesh/mesh-fast-forward.ts +430 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger.ts +138 -1
- package/src/mesh/mesh-work-queue.ts +199 -137
- package/src/mesh/refine-config.ts +356 -0
- package/src/providers/chat-message-normalization.ts +7 -12
- package/src/providers/cli-provider-instance.ts +93 -14
- package/src/providers/ide-provider-instance.ts +17 -3
- package/src/providers/provider-loader.ts +10 -4
- package/src/providers/read-chat-contract.ts +1 -1
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +182 -0
- package/src/status/reporter.ts +15 -0
- package/src/system/host-memory.ts +29 -12
|
@@ -76,7 +76,12 @@ Repository: \`${mesh.repoIdentity}\`${mesh.defaultBranch ? `\nDefault branch: \`
|
|
|
76
76
|
// ─── Section Builders ───────────────────────────
|
|
77
77
|
|
|
78
78
|
function buildNodeStatusSection(nodes: RepoMeshNodeStatus[]): string {
|
|
79
|
-
const lines = [
|
|
79
|
+
const lines = [
|
|
80
|
+
'## Current Node Status',
|
|
81
|
+
'',
|
|
82
|
+
'Node labels are display context, not aliases. Use exact `nodeId` values in mesh tool calls; do not invent shorthand names such as M1/M2 unless they are explicitly configured labels.',
|
|
83
|
+
'',
|
|
84
|
+
];
|
|
80
85
|
for (const n of nodes) {
|
|
81
86
|
const healthIcon = n.health === 'online' ? '🟢' :
|
|
82
87
|
n.health === 'dirty' ? '🟡' :
|
|
@@ -85,21 +90,33 @@ function buildNodeStatusSection(nodes: RepoMeshNodeStatus[]): string {
|
|
|
85
90
|
? `sessions: ${n.activeSessions.join(', ')}`
|
|
86
91
|
: 'no active sessions';
|
|
87
92
|
const branch = n.git?.branch ? `branch: \`${n.git.branch}\`` : '';
|
|
88
|
-
|
|
89
|
-
|
|
93
|
+
const context = [
|
|
94
|
+
n.daemonId ? `daemon: \`${n.daemonId}\`` : '',
|
|
95
|
+
n.providers?.length ? `providers: ${n.providers.join(', ')}` : '',
|
|
96
|
+
].filter(Boolean).join(' | ');
|
|
97
|
+
lines.push(`- ${healthIcon} **${n.machineLabel}** (nodeId: \`${n.nodeId}\`)`);
|
|
98
|
+
lines.push(` workspace: \`${n.workspace}\`${context ? ` | ${context}` : ''} | ${branch} | ${sessions}`);
|
|
90
99
|
if (n.error) lines.push(` ⚠️ ${n.error}`);
|
|
91
100
|
}
|
|
92
101
|
return lines.join('\n');
|
|
93
102
|
}
|
|
94
103
|
|
|
95
104
|
function buildNodeConfigSection(mesh: LocalMeshEntry): string {
|
|
96
|
-
const lines = [
|
|
105
|
+
const lines = [
|
|
106
|
+
'## Configured Nodes',
|
|
107
|
+
'',
|
|
108
|
+
'Node labels are display context, not aliases. Use exact `nodeId` values in mesh tool calls; do not invent shorthand names such as M1/M2 unless they are explicitly configured labels.',
|
|
109
|
+
'',
|
|
110
|
+
];
|
|
97
111
|
for (const n of mesh.nodes) {
|
|
98
112
|
const labels: string[] = [];
|
|
99
113
|
if (n.isLocalWorktree) labels.push('worktree');
|
|
100
114
|
if (n.policy?.readOnly) labels.push('read-only');
|
|
101
115
|
const suffix = labels.length ? ` [${labels.join(', ')}]` : '';
|
|
102
|
-
|
|
116
|
+
const explicitMachineLabel = typeof (n as any).machineLabel === 'string' ? (n as any).machineLabel : '';
|
|
117
|
+
const explicitLabel = explicitMachineLabel ? ` label: **${explicitMachineLabel}** |` : '';
|
|
118
|
+
const providerPriority = n.policy?.providerPriority?.length ? ` | providers: ${n.policy.providerPriority.join(', ')}` : '';
|
|
119
|
+
lines.push(`- ${explicitLabel} nodeId: \`${n.id}\` | workspace: \`${n.workspace}\`${n.daemonId ? ` | daemon: \`${n.daemonId}\`` : ''}${providerPriority}${suffix}`);
|
|
103
120
|
}
|
|
104
121
|
lines.push('', '_Use `mesh_status` to probe live health before delegating work._');
|
|
105
122
|
return lines.join('\n');
|
|
@@ -110,6 +127,9 @@ function buildPolicySection(policy: RepoMeshPolicy): string {
|
|
|
110
127
|
if (policy.requirePreTaskCheckpoint) rules.push('- Create a git checkpoint **before** starting each task');
|
|
111
128
|
if (policy.requirePostTaskCheckpoint) rules.push('- Create a git checkpoint **after** each task completes');
|
|
112
129
|
if (policy.requireApprovalForPush) rules.push('- **Ask for user approval** before pushing to remote');
|
|
130
|
+
if (policy.allowAutoPublishSubmoduleMainCommits) {
|
|
131
|
+
rules.push('- Refinery may auto-publish unreachable submodule gitlink commits to submodule origin/main with non-force pushes after validation and patch-equivalence pass');
|
|
132
|
+
}
|
|
113
133
|
if (policy.requireApprovalForDestructiveGit) rules.push('- **Ask for user approval** before destructive git operations (force push, reset, etc.)');
|
|
114
134
|
|
|
115
135
|
const dirtyBehavior = {
|
|
@@ -140,6 +160,7 @@ const TOOLS_SECTION = `## Available Tools
|
|
|
140
160
|
| \`mesh_read_debug\` | Collect a daemon-side chat/parser debug bundle for a session |
|
|
141
161
|
| \`mesh_task_history\` | Read the task ledger — dispatches, completions, failures. Use to understand what has been done before deciding next steps |
|
|
142
162
|
| \`mesh_git_status\` | Check git status on a specific node |
|
|
163
|
+
| \`mesh_fast_forward_node\` | Safely dry-run or explicitly execute an obvious clean fast-forward without launching an agent session |
|
|
143
164
|
| \`mesh_checkpoint\` | Create a git checkpoint on a node |
|
|
144
165
|
| \`mesh_approve\` | Approve/reject a pending agent action |
|
|
145
166
|
| \`mesh_clone_node\` | Create a worktree node for isolated parallel branch work |
|
|
@@ -161,10 +182,10 @@ const WORKFLOW_SECTION = `## Orchestration Workflow
|
|
|
161
182
|
c. **Targeted Tasks**: Use \`mesh_send_task\` only when you need to bypass the queue and force a specific node to execute a task immediately.
|
|
162
183
|
d. For the first dispatch of a new task, provide a **complete, self-contained** instruction that includes all context the agent needs (file paths, line numbers, what to change, why). Do not send partial instructions expecting future follow-up.
|
|
163
184
|
e. For a continuation of the same issue in an existing session, send a concise **delta instruction**: current verified state, the exact failed/blocked step, the newly approved action, and final reporting requirements. Do not resend the full original task or open a new chat solely to continue the same work; that wastes coordinator and worker context.
|
|
164
|
-
4. **Monitor** — Prefer event-driven completion/status notifications. Do **not** poll \`mesh_read_chat\` repeatedly.
|
|
185
|
+
4. **Monitor** — Prefer event-driven completion/status notifications. Do **not** poll \`mesh_read_chat\` repeatedly. Do **not** repeatedly call \`mesh_status\` or \`mesh_view_queue\` just to wait for assigned/generating work. After dispatching a direct or queued task, send one progress update with the task/session handle, then stop. Wait for \`pendingCoordinatorEvents\` or another completion/approval/status signal, an explicit user status request, or a real timeout/stall signal before reading status/chat/queue again. Use at most one compact \`mesh_read_chat\` check after a terminal signal. Handle approvals via \`mesh_approve\`.
|
|
165
186
|
5. **Verify** — When a task reports completion or git work is visible, call \`mesh_git_status\` to verify changes were made.
|
|
166
187
|
6. **Checkpoint** — Call \`mesh_checkpoint\` to save the work.
|
|
167
|
-
7. **Converge branches** — Before marking any task complete, classify every touched node/branch into exactly one final state: \`merged_to_main\`, \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\`. Use \`mesh_status\` branchConvergenceSummary and \`mesh_refine_node\` for clean worktree branches when safe. A task that remains on a non-main branch is not fully complete unless the final report names the follow-up state and next step.
|
|
188
|
+
7. **Converge branches** — Before marking any task complete, classify every touched node/branch into exactly one final state: \`merged_to_main\`, \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\`. Use \`mesh_status\` branchConvergenceSummary. For obvious clean branch catch-up (ahead 0, behind > 0, upstream fresh, no dirty/stash/submodule issues), use \`mesh_fast_forward_node\` dry-run first and execute only when explicitly safe/approved; this avoids consuming an agent session. Use \`mesh_refine_node\` for clean worktree branches when safe. Before/refine merging root commits that contain submodule gitlink changes, require each submodule commit to be reachable from the configured submodule remote main branch, not merely present on a feature ref or local checkout. If \`mesh_refine_node\` returns \`submodule_reachability_failed\` or publish-required evidence, keep the public convergence bucket as \`blocked_review\`; unless \`allowAutoPublishSubmoduleMainCommits\` is explicitly enabled and Refinery reports successful non-force publish plus post-publish verification, ask the user for explicit approval to push/publish the unreachable submodule commit(s) to submodule main, then rerun \`mesh_refine_node\`. Do not merge the root branch until the submodule commit(s) are reachable from submodule origin/main. A task that remains on a non-main branch is not fully complete unless the final report names the follow-up state and next step.
|
|
168
189
|
8. **Clean up** — Remove worktree nodes via \`mesh_remove_node\` after their work is merged or no longer needed.
|
|
169
190
|
9. **Report** — Summarize what was done, what changed, any issues, and the branch convergence state.
|
|
170
191
|
|
|
@@ -201,6 +222,8 @@ function buildRulesSection(coordinatorCliType?: string): string {
|
|
|
201
222
|
- **Respect node capabilities.** Don't send build tasks to read-only nodes. Don't push from nodes that aren't allowed to.
|
|
202
223
|
- **Never fabricate tool results.** Always call the actual tool; never pretend you did.
|
|
203
224
|
- **Clean up worktree nodes.** After a worktree task completes and its changes are merged or checkpointed, call \`mesh_remove_node\` to free resources.
|
|
204
|
-
- **Do not strand completed branches.** A checkpointed or clean feature/worktree branch is not done by itself. Merge/refine it to the mesh default branch, or explicitly report one of \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\` with the next action.
|
|
225
|
+
- **Do not strand completed branches.** A checkpointed or clean feature/worktree branch is not done by itself. Merge/refine it to the mesh default branch, fast-forward obvious clean behind-only branches with \`mesh_fast_forward_node\`, or explicitly report one of \`pushed_feature_branch_needs_merge\`, \`blocked_review\`, \`cleanup_candidate\`, or \`not_mergeable\` with the next action.
|
|
226
|
+
- **Keep Refinery validation project-configurable.** \`mesh_refine_node\` must execute validation from repo mesh/refine config (for example \`.adhdev/refine.{json,yaml,yml}\`, \`.adhdev/repo-mesh-refine.*\`, or \`repo-mesh.refine.*\`). Heuristics are suggestions/scaffolding only, not the execution path.
|
|
227
|
+
- **Treat submodule main reachability as publish-needed.** A \`submodule_reachability_failed\` refine result means the root gitlink points at a submodule commit that is not reachable from the configured submodule remote main branch. Do not treat feature-branch reachability as complete, retry validation blindly, or start code review first. Classify it as \`blocked_review\`, request user approval to push/publish the submodule commit to submodule main, then rerun \`mesh_refine_node\`, unless the mesh or repo refine config explicitly enabled \`allowAutoPublishSubmoduleMainCommits\` and Refinery reports exact path/commit/remote/branch evidence with post-publish verification.
|
|
205
228
|
- **Name worktree branches meaningfully.** Use descriptive names like \`feat/auth-refactor\` or \`fix/build-123\`.${coordinatorNote}`;
|
|
206
229
|
}
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
import type { MeshLedgerEntry } from './mesh-ledger.js';
|
|
2
|
+
import type { MeshWorkQueueEntry } 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
|
+
|
|
28
|
+
export interface MeshActiveWorkSummary {
|
|
29
|
+
totalActiveCount: number;
|
|
30
|
+
queueActiveCount: number;
|
|
31
|
+
directActiveCount: number;
|
|
32
|
+
awaitingApprovalCount: number;
|
|
33
|
+
generatingCount: number;
|
|
34
|
+
failedCount: number;
|
|
35
|
+
idleCount: number;
|
|
36
|
+
sourceCounts: Record<MeshActiveWorkSource, number>;
|
|
37
|
+
statusCounts: Record<MeshActiveWorkStatus, number>;
|
|
38
|
+
staleDirectCount: number;
|
|
39
|
+
/**
|
|
40
|
+
* When staleDirectCount > 0, this note clarifies that stale direct records are
|
|
41
|
+
* historical/recovery evidence — orphaned ledger entries whose original node or session
|
|
42
|
+
* is no longer present in the live mesh. They are NOT active or unresolved work items.
|
|
43
|
+
* The active queue (queue source) is the authoritative source for pending/assigned work.
|
|
44
|
+
*/
|
|
45
|
+
staleDirectNote?: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface BuildMeshActiveWorkOptions {
|
|
49
|
+
meshId: string;
|
|
50
|
+
queue?: MeshWorkQueueEntry[];
|
|
51
|
+
ledgerEntries?: MeshLedgerEntry[];
|
|
52
|
+
nodes?: any[];
|
|
53
|
+
now?: number;
|
|
54
|
+
/** Include terminal direct rows (idle/failed) for handoff/recent-work surfaces. Defaults false. */
|
|
55
|
+
includeTerminalDirect?: boolean;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const DIRECT_DISPATCH_VIA = new Set(['p2p_direct', 'local_direct', 'mesh_send_task']);
|
|
59
|
+
const TERMINAL_LEDGER_KINDS = new Set(['task_completed', 'task_failed', 'task_stalled']);
|
|
60
|
+
|
|
61
|
+
function readString(value: unknown): string | undefined {
|
|
62
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function summarizeMessage(message: string): { title: string; summary: string } {
|
|
66
|
+
const oneLine = message.replace(/\s+/g, ' ').trim();
|
|
67
|
+
const title = oneLine.length > 96 ? `${oneLine.slice(0, 93)}...` : oneLine;
|
|
68
|
+
return { title: title || '(untitled task)', summary: oneLine };
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function elapsedSince(value: string | undefined, now: number): number {
|
|
72
|
+
const started = value ? new Date(value).getTime() : Number.NaN;
|
|
73
|
+
return Number.isFinite(started) ? Math.max(0, now - started) : 0;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function sessionStatusFromNodes(nodes: any[] | undefined, nodeId?: string, sessionId?: string): { status?: MeshActiveWorkStatus; staleReason?: string } {
|
|
77
|
+
if (!Array.isArray(nodes)) return {};
|
|
78
|
+
if (!nodeId) return { staleReason: 'direct task has no node id' };
|
|
79
|
+
const node = nodes.find(item => readString(item?.id) === nodeId || readString(item?.nodeId) === nodeId || readString(item?.node_id) === nodeId);
|
|
80
|
+
if (!node) return { staleReason: 'direct task node is no longer in the live mesh' };
|
|
81
|
+
if (!sessionId) return {};
|
|
82
|
+
const candidates: any[] = [];
|
|
83
|
+
for (const value of [
|
|
84
|
+
node.sessions,
|
|
85
|
+
node.activeSessions,
|
|
86
|
+
node.active_sessions,
|
|
87
|
+
node.activeSessionDetails,
|
|
88
|
+
node.active_session_details,
|
|
89
|
+
node.sessionDetails,
|
|
90
|
+
node.session_details,
|
|
91
|
+
node.lastProbe?.sessions,
|
|
92
|
+
node.last_probe?.sessions,
|
|
93
|
+
node.lastProbe?.status?.sessions,
|
|
94
|
+
node.last_probe?.status?.sessions,
|
|
95
|
+
]) {
|
|
96
|
+
if (Array.isArray(value)) candidates.push(...value);
|
|
97
|
+
}
|
|
98
|
+
for (const value of [node.activeSession, node.active_session, node.currentSession, node.current_session, node.runtimeSession, node.runtime_session, node.session]) {
|
|
99
|
+
if (value && typeof value === 'object') candidates.push(value);
|
|
100
|
+
}
|
|
101
|
+
const session = candidates.find(item => {
|
|
102
|
+
if (typeof item === 'string') return item === sessionId;
|
|
103
|
+
const id = readString(item?.id) || readString(item?.sessionId) || readString(item?.session_id) || readString(item?.runtimeSessionId) || readString(item?.instanceId);
|
|
104
|
+
return id === sessionId;
|
|
105
|
+
});
|
|
106
|
+
if (!session) return { staleReason: 'direct task session is not present in live session records' };
|
|
107
|
+
if (typeof session === 'string') return {};
|
|
108
|
+
const raw = `${readString(session.status) || ''} ${readString(session.lifecycle) || ''} ${readString(session.state) || ''} ${readString(session.activeChat?.status) || ''}`.toLowerCase();
|
|
109
|
+
if (raw.includes('approval')) return { status: 'awaiting_approval' };
|
|
110
|
+
if (raw.includes('generating') || raw.includes('running') || raw.includes('busy')) return { status: 'generating' };
|
|
111
|
+
if (raw.includes('failed') || raw.includes('stopped') || raw.includes('terminated') || raw.includes('exited')) return { status: 'failed' };
|
|
112
|
+
if (raw.includes('idle') || raw.includes('waiting_input') || raw.includes('ready')) return { status: 'idle' };
|
|
113
|
+
return {};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isDirectDispatch(entry: MeshLedgerEntry): boolean {
|
|
117
|
+
if (entry.kind !== 'task_dispatched') return false;
|
|
118
|
+
const payload = entry.payload || {};
|
|
119
|
+
if (payload.source === 'direct') return true;
|
|
120
|
+
const via = readString(payload.via);
|
|
121
|
+
return Boolean(via && DIRECT_DISPATCH_VIA.has(via) && payload.source !== 'queue');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
function directDispatchTaskId(entry: MeshLedgerEntry): string {
|
|
125
|
+
return readString(entry.payload?.taskId) || entry.id;
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function terminalMatchesDispatch(terminal: MeshLedgerEntry, dispatch: MeshLedgerEntry, taskId: string): boolean {
|
|
129
|
+
const terminalTaskId = readString(terminal.payload?.taskId);
|
|
130
|
+
if (terminalTaskId && terminalTaskId === taskId) return true;
|
|
131
|
+
if (terminalTaskId && terminalTaskId !== taskId) return false;
|
|
132
|
+
if (dispatch.sessionId && terminal.sessionId === dispatch.sessionId) return true;
|
|
133
|
+
return Boolean(dispatch.nodeId && terminal.nodeId === dispatch.nodeId && !dispatch.sessionId);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function statusFromTerminal(entry: MeshLedgerEntry): MeshActiveWorkStatus {
|
|
137
|
+
if (entry.kind === 'task_approval_needed') return 'awaiting_approval';
|
|
138
|
+
if (entry.kind === 'task_completed') return 'idle';
|
|
139
|
+
return 'failed';
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function buildMeshActiveWorkSummary(activeWork: MeshActiveWorkRecord[]): MeshActiveWorkSummary {
|
|
143
|
+
const statusCounts: Record<MeshActiveWorkStatus, number> = {
|
|
144
|
+
pending: 0,
|
|
145
|
+
assigned: 0,
|
|
146
|
+
generating: 0,
|
|
147
|
+
idle: 0,
|
|
148
|
+
failed: 0,
|
|
149
|
+
awaiting_approval: 0,
|
|
150
|
+
};
|
|
151
|
+
const sourceCounts: Record<MeshActiveWorkSource, number> = { queue: 0, direct: 0 };
|
|
152
|
+
for (const item of activeWork) {
|
|
153
|
+
sourceCounts[item.source] += 1;
|
|
154
|
+
statusCounts[item.status] += 1;
|
|
155
|
+
}
|
|
156
|
+
const staleDirectCount = activeWork.filter(item => item.source === 'direct' && item.staleReason).length;
|
|
157
|
+
return {
|
|
158
|
+
totalActiveCount: activeWork.length,
|
|
159
|
+
queueActiveCount: sourceCounts.queue,
|
|
160
|
+
directActiveCount: sourceCounts.direct,
|
|
161
|
+
awaitingApprovalCount: statusCounts.awaiting_approval,
|
|
162
|
+
generatingCount: statusCounts.generating,
|
|
163
|
+
failedCount: statusCounts.failed,
|
|
164
|
+
idleCount: statusCounts.idle,
|
|
165
|
+
sourceCounts,
|
|
166
|
+
statusCounts,
|
|
167
|
+
staleDirectCount,
|
|
168
|
+
...(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.' } : {}),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
export function buildMeshActiveWork(opts: BuildMeshActiveWorkOptions): { activeWork: MeshActiveWorkRecord[]; staleDirectWork: MeshActiveWorkRecord[]; staleDirectWorkNote?: string; terminalDirectWork: MeshActiveWorkRecord[]; summary: MeshActiveWorkSummary } {
|
|
174
|
+
const now = opts.now ?? Date.now();
|
|
175
|
+
const records: MeshActiveWorkRecord[] = [];
|
|
176
|
+
const staleDirectWork: MeshActiveWorkRecord[] = [];
|
|
177
|
+
const terminalDirectWork: MeshActiveWorkRecord[] = [];
|
|
178
|
+
|
|
179
|
+
for (const task of opts.queue || []) {
|
|
180
|
+
if (task.status !== 'pending' && task.status !== 'assigned') continue;
|
|
181
|
+
const { title, summary } = summarizeMessage(task.message || '');
|
|
182
|
+
records.push({
|
|
183
|
+
taskId: task.id,
|
|
184
|
+
source: 'queue',
|
|
185
|
+
status: task.status,
|
|
186
|
+
nodeId: task.assignedNodeId || task.targetNodeId,
|
|
187
|
+
sessionId: task.assignedSessionId || task.targetSessionId,
|
|
188
|
+
taskTitle: title,
|
|
189
|
+
taskSummary: summary,
|
|
190
|
+
message: task.message,
|
|
191
|
+
taskMode: task.taskMode,
|
|
192
|
+
createdAt: task.createdAt,
|
|
193
|
+
updatedAt: task.updatedAt,
|
|
194
|
+
dispatchedAt: task.dispatchTimestamp,
|
|
195
|
+
elapsedMs: elapsedSince(task.dispatchTimestamp || task.createdAt, now),
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const ledgerEntries = (opts.ledgerEntries || []).slice().sort((a, b) => new Date(a.timestamp).getTime() - new Date(b.timestamp).getTime());
|
|
200
|
+
const terminals = ledgerEntries.filter(entry => TERMINAL_LEDGER_KINDS.has(entry.kind) || entry.kind === 'task_approval_needed');
|
|
201
|
+
for (const dispatch of ledgerEntries.filter(isDirectDispatch)) {
|
|
202
|
+
const taskId = directDispatchTaskId(dispatch);
|
|
203
|
+
const terminal = terminals
|
|
204
|
+
.filter(entry => new Date(entry.timestamp).getTime() >= new Date(dispatch.timestamp).getTime())
|
|
205
|
+
.find(entry => terminalMatchesDispatch(entry, dispatch, taskId));
|
|
206
|
+
const terminalStatus = terminal ? statusFromTerminal(terminal) : undefined;
|
|
207
|
+
const live = sessionStatusFromNodes(opts.nodes, dispatch.nodeId, dispatch.sessionId);
|
|
208
|
+
const status = terminalStatus || live.status || 'assigned';
|
|
209
|
+
const terminalRow = Boolean(terminal && terminal.kind !== 'task_approval_needed');
|
|
210
|
+
const message = readString(dispatch.payload?.message) || readString(dispatch.payload?.summary) || '';
|
|
211
|
+
const { title, summary } = summarizeMessage(message);
|
|
212
|
+
const record: MeshActiveWorkRecord = {
|
|
213
|
+
taskId,
|
|
214
|
+
source: 'direct',
|
|
215
|
+
status,
|
|
216
|
+
nodeId: dispatch.nodeId,
|
|
217
|
+
sessionId: dispatch.sessionId,
|
|
218
|
+
providerType: dispatch.providerType || readString(dispatch.payload?.providerType),
|
|
219
|
+
taskTitle: readString(dispatch.payload?.taskTitle) || title,
|
|
220
|
+
taskSummary: readString(dispatch.payload?.taskSummary) || summary,
|
|
221
|
+
message,
|
|
222
|
+
taskMode: readString(dispatch.payload?.taskMode),
|
|
223
|
+
createdAt: dispatch.timestamp,
|
|
224
|
+
updatedAt: terminal?.timestamp || dispatch.timestamp,
|
|
225
|
+
dispatchedAt: dispatch.timestamp,
|
|
226
|
+
elapsedMs: elapsedSince(dispatch.timestamp, now),
|
|
227
|
+
terminal: terminalRow,
|
|
228
|
+
terminalKind: terminal?.kind,
|
|
229
|
+
terminalAt: terminal?.timestamp,
|
|
230
|
+
staleReason: live.staleReason,
|
|
231
|
+
};
|
|
232
|
+
if (terminalRow) {
|
|
233
|
+
terminalDirectWork.push(record);
|
|
234
|
+
if (opts.includeTerminalDirect !== true) continue;
|
|
235
|
+
}
|
|
236
|
+
if (live.staleReason && !terminalRow) {
|
|
237
|
+
staleDirectWork.push(record);
|
|
238
|
+
continue;
|
|
239
|
+
}
|
|
240
|
+
records.push(record);
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
records.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
244
|
+
staleDirectWork.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
245
|
+
terminalDirectWork.sort((a, b) => new Date(a.createdAt).getTime() - new Date(b.createdAt).getTime());
|
|
246
|
+
const summary = buildMeshActiveWorkSummary(records);
|
|
247
|
+
summary.staleDirectCount = staleDirectWork.length;
|
|
248
|
+
const staleDirectWorkNote = staleDirectWork.length > 0
|
|
249
|
+
? '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.'
|
|
250
|
+
: undefined;
|
|
251
|
+
if (staleDirectWorkNote) {
|
|
252
|
+
summary.staleDirectNote = staleDirectWorkNote;
|
|
253
|
+
}
|
|
254
|
+
return { activeWork: records, staleDirectWork, staleDirectWorkNote, terminalDirectWork, summary };
|
|
255
|
+
}
|