@adhdev/daemon-core 1.0.28-rc.6 → 1.0.28-rc.8

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.
@@ -15,7 +15,7 @@
15
15
  import { EventEmitter } from 'events';
16
16
  import { MeshRuntimeStore } from './mesh-runtime-store.js';
17
17
  import { type MeshLedgerOriginatingCoordinatorV2 } from './contracts.js';
18
- export type MeshLedgerKind = 'task_dispatched' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'task_question_pending' | 'p2p_dispatch_failed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_joined' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled' | 'direct_fast_forward' | 'delivery_unroutable' | 'direct_dispatch_pruned' | 'event_held' | 'event_held_requeued' | 'task_reclaimed' | 'coordinator_operating_note' | 'coordinator_operating_note_tombstone' | 'mission_created' | 'mission_status_changed' | 'mission_goal_updated' | 'magi_dispatched' | 'magi_synthesis' | 'key_injection' | 'worktree_cleanup_candidate';
18
+ export type MeshLedgerKind = 'task_dispatched' | 'task_claimed' | 'task_completed' | 'task_failed' | 'task_stalled' | 'task_approval_needed' | 'task_question_pending' | 'p2p_dispatch_failed' | 'session_launched' | 'session_auto_launch' | 'session_stopped' | 'checkpoint_created' | 'node_cloned' | 'node_joined' | 'node_removed' | 'coordinator_started' | 'recovery_attempted' | 'ledger_replicated' | 'ledger_reconciled' | 'direct_fast_forward' | 'delivery_unroutable' | 'direct_dispatch_pruned' | 'event_held' | 'event_held_requeued' | 'task_reclaimed' | 'coordinator_operating_note' | 'coordinator_operating_note_tombstone' | 'mission_created' | 'mission_status_changed' | 'mission_goal_updated' | 'magi_dispatched' | 'magi_synthesis' | 'key_injection' | 'worktree_cleanup_candidate';
19
19
  export interface MeshLedgerEntry {
20
20
  id: string;
21
21
  meshId: string;
@@ -24,8 +24,12 @@ export interface MeshLedgerEntry {
24
24
  nodeId?: string;
25
25
  sessionId?: string;
26
26
  providerType?: string;
27
+ taskId?: string;
27
28
  payload: Record<string, unknown>;
28
29
  }
30
+ /** Resolve the taskId for a ledger entry, preferring the base field and falling
31
+ * back to payload.taskId (legacy rows / entries that only carry it in payload). */
32
+ export declare function ledgerEntryTaskId(entry: Pick<MeshLedgerEntry, 'taskId' | 'payload'>): string | undefined;
29
33
  export declare function isIntentionalCleanupStopEntry(entry: Pick<MeshLedgerEntry, 'kind' | 'payload'>): boolean;
30
34
  export type MeshWorkerResultStatus = 'completed' | 'failed' | 'blocked' | 'partial' | 'unknown';
31
35
  export type MeshProcessArtifactKind = 'process' | 'log' | 'port' | 'window' | 'session' | 'file' | 'url' | 'other';
@@ -5,7 +5,25 @@ import type { RepoMeshSchedulingStrategy } from '../repo-mesh-types.js';
5
5
  export declare function isWorkspaceAutoFastForwardInFlight(workspace: string | undefined): boolean;
6
6
  export declare function __resetIdleAutoFastForwardForTests(): void;
7
7
  export declare function getMeshWithCache(components: DaemonComponents, meshId: string): any | undefined;
8
- export declare function tryAssignQueueTask(components: DaemonComponents, meshId: string, nodeId: string, sessionId: string, providerType: string): boolean;
8
+ export interface MeshTaskRoutingDecision {
9
+ source: 'queue' | 'autoLaunch' | 'direct';
10
+ fitnessScore?: number;
11
+ skippedCandidates?: Array<{
12
+ nodeId: string;
13
+ reason: string;
14
+ }>;
15
+ requiredTagsResult?: {
16
+ required: string[];
17
+ satisfied: boolean;
18
+ missing: string[];
19
+ };
20
+ resolvedProviderType?: string;
21
+ resolvedModel?: string;
22
+ resolvedThinkingLevel?: string;
23
+ resolvedDifficulty?: string;
24
+ reason?: string;
25
+ }
26
+ export declare function tryAssignQueueTask(components: DaemonComponents, meshId: string, nodeId: string, sessionId: string, providerType: string, routingDecision?: MeshTaskRoutingDecision): boolean;
9
27
  export declare const AUTO_LAUNCH_AWAIT_CLAIM_MS = 90000;
10
28
  interface AwaitClaimBackoffState {
11
29
  cycles: number;
@@ -355,6 +355,7 @@ export declare class MeshRuntimeStore {
355
355
  nodeId?: string | null;
356
356
  sessionId?: string | null;
357
357
  providerType?: string | null;
358
+ taskId?: string | null;
358
359
  payload?: unknown;
359
360
  }): void;
360
361
  readLedgerEntries(meshId: string, opts?: {
@@ -370,6 +371,7 @@ export declare class MeshRuntimeStore {
370
371
  nodeId: string | null;
371
372
  sessionId: string | null;
372
373
  providerType: string | null;
374
+ taskId: string | null;
373
375
  payload: unknown;
374
376
  }>;
375
377
  /**
@@ -390,6 +392,7 @@ export declare class MeshRuntimeStore {
390
392
  nodeId: string | null;
391
393
  sessionId: string | null;
392
394
  providerType: string | null;
395
+ taskId: string | null;
393
396
  payload: unknown;
394
397
  }>;
395
398
  /** Remove all ledger entries for a mesh (mesh deletion / test cleanup). */
@@ -406,6 +409,7 @@ export declare class MeshRuntimeStore {
406
409
  nodeId?: string | null;
407
410
  sessionId?: string | null;
408
411
  providerType?: string | null;
412
+ taskId?: string | null;
409
413
  payload?: unknown;
410
414
  }>): number;
411
415
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "1.0.28-rc.6",
3
+ "version": "1.0.28-rc.8",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -49,8 +49,8 @@
49
49
  "author": "vilmire",
50
50
  "license": "AGPL-3.0-or-later",
51
51
  "dependencies": {
52
- "@adhdev/mesh-shared": "1.0.28-rc.6",
53
- "@adhdev/session-host-core": "1.0.28-rc.6",
52
+ "@adhdev/mesh-shared": "1.0.28-rc.8",
53
+ "@adhdev/session-host-core": "1.0.28-rc.8",
54
54
  "@agentclientprotocol/sdk": "^0.16.1",
55
55
  "ajv": "^8.20.0",
56
56
  "ajv-formats": "^3.0.1",
@@ -1604,7 +1604,15 @@ export class CliStateEngine {
1604
1604
  // background-tool race this was meant to guard against. If a provider
1605
1605
  // really needs the gate, the manifest can set
1606
1606
  // `requiresFinalAssistantBeforeIdle: true`.
1607
- const requiresFinalAssistant = !!this.provider.requiresFinalAssistantBeforeIdle;
1607
+ //
1608
+ // This is a completion-timing JUDGMENT, so it goes through the shared
1609
+ // transcript-authority profile rather than reading the raw flag: the
1610
+ // 'floor' class is exactly `requiresFinalAssistantBeforeIdle && !hold`
1611
+ // (see resolveTranscriptAuthorityProfile), which is equivalent to the
1612
+ // old `!!requiresFinalAssistantBeforeIdle` for every real provider
1613
+ // because none declares both flags — a hold provider (antigravity)
1614
+ // sets holdCompletionForTranscript only, so it was already false here.
1615
+ const requiresFinalAssistant = resolveTranscriptAuthorityProfile(this.provider).timing === 'floor';
1608
1616
  if (!requiresFinalAssistant) return false;
1609
1617
  if (!this.isWaitingForResponse || !this.currentTurnScope || this.hasActionableApproval()) return false;
1610
1618
  const parsedStatus = typeof parsed?.status === 'string' ? parsed.status.trim() : '';
@@ -232,6 +232,9 @@ export class ProviderCliAdapter implements CliAdapter {
232
232
  private pendingOutboundQueue: PendingOutboundMessage[] = [];
233
233
  private pendingOutboundFlushTimer: NodeJS.Timeout | null = null;
234
234
  private pendingOutboundFlushInFlight = false;
235
+ // Stale-queue watchdog: fires STALE_QUEUE_WARN_MS after the oldest queued
236
+ // message was enqueued if nothing has been flushed yet.
237
+ private pendingOutboundStaleTimer: NodeJS.Timeout | null = null;
235
238
  // Submit retry timer — PTY-level, not state machine
236
239
  private submitRetryTimer: NodeJS.Timeout | null = null;
237
240
 
@@ -1715,6 +1718,10 @@ export class ProviderCliAdapter implements CliAdapter {
1715
1718
  await new Promise<void>(resolve => setTimeout(resolve, FORCE_SUBMIT_SETTLE_MS));
1716
1719
  }
1717
1720
 
1721
+ // Stale-queue threshold: warn if a queued message has not been flushed
1722
+ // within this many milliseconds (instrumentation only, no behavior change).
1723
+ private static readonly STALE_QUEUE_WARN_MS = 15000;
1724
+
1718
1725
  private enqueuePendingOutboundMessage(text: string, reason: string, meshTaskId?: string): void {
1719
1726
  const content = String(text || '');
1720
1727
  const duplicate = this.pendingOutboundQueue.some((message) => message.content === content);
@@ -1722,6 +1729,18 @@ export class ProviderCliAdapter implements CliAdapter {
1722
1729
  return;
1723
1730
  }
1724
1731
  const queuedAt = Date.now();
1732
+ const stableMs = this.lastScreenChangeAt ? (queuedAt - this.lastScreenChangeAt) : -1;
1733
+ // Diagnostic context: which gate caused the silent-queue and the full
1734
+ // session readiness snapshot at the moment of enqueue.
1735
+ const gateContext = {
1736
+ reason,
1737
+ startupParseGate: this.startupParseGate,
1738
+ ready: this.ready,
1739
+ engineStatus: this.engine.currentStatus,
1740
+ isWaitingForResponse: this.engine.isWaitingForResponse,
1741
+ stableMs,
1742
+ sessionId: this.engine.getTraceSessionId(),
1743
+ };
1725
1744
  const message: PendingOutboundMessage = {
1726
1745
  id: `${queuedAt}:${this.pendingOutboundQueue.length}:${Math.random().toString(36).slice(2, 10)}`,
1727
1746
  role: 'user',
@@ -1731,8 +1750,23 @@ export class ProviderCliAdapter implements CliAdapter {
1731
1750
  ...(typeof meshTaskId === 'string' && meshTaskId.trim() ? { meshTaskId } : {}),
1732
1751
  };
1733
1752
  this.pendingOutboundQueue.push(message);
1734
- LOG.info('CLI', `[${this.cliType}] queued outbound message while busy (${reason}); queue=${this.pendingOutboundQueue.length}`);
1753
+ LOG.info('CLI', `[${this.cliType}] queued outbound message; gate=${JSON.stringify(gateContext)}; queue=${this.pendingOutboundQueue.length}`);
1735
1754
  this.onStatusChange?.();
1755
+ // Stale-queue watchdog: emit a warn-level log if the enqueued message
1756
+ // has not been flushed after STALE_QUEUE_WARN_MS. This fires once per
1757
+ // enqueue event (not once per message still in queue) so it does not
1758
+ // spam on a persistently stuck queue; it simply surfaces that the queue
1759
+ // is stuck and records the current gate state at warn time.
1760
+ if (!this.pendingOutboundStaleTimer) {
1761
+ this.pendingOutboundStaleTimer = setTimeout(() => {
1762
+ this.pendingOutboundStaleTimer = null;
1763
+ if (this.pendingOutboundQueue.length === 0) return;
1764
+ const oldest = this.pendingOutboundQueue[0];
1765
+ const staleSec = ((Date.now() - oldest.queuedAt) / 1000).toFixed(1);
1766
+ const nowStableMs = this.lastScreenChangeAt ? (Date.now() - this.lastScreenChangeAt) : -1;
1767
+ LOG.warn('CLI', `[${this.cliType}] STALE QUEUE: ${this.pendingOutboundQueue.length} message(s) undelivered for ${staleSec}s; gate=startupParseGate:${this.startupParseGate} ready:${this.ready} engineStatus:${this.engine.currentStatus} isWaitingForResponse:${this.engine.isWaitingForResponse} stableMs:${nowStableMs} sessionId:${this.engine.getTraceSessionId()} enqueueReason:${oldest.id}`);
1768
+ }, ProviderCliAdapter.STALE_QUEUE_WARN_MS);
1769
+ }
1736
1770
  }
1737
1771
 
1738
1772
  private shouldQueuePendingOutboundMessage(parsedStatusBeforeSend: any | null = null): string | null {
@@ -1785,6 +1819,11 @@ export class ProviderCliAdapter implements CliAdapter {
1785
1819
  try {
1786
1820
  await this.sendMessageNow(next.content, false, next.meshTaskId);
1787
1821
  this.pendingOutboundQueue.shift();
1822
+ // Clear stale watchdog once the queue drains.
1823
+ if (this.pendingOutboundQueue.length === 0 && this.pendingOutboundStaleTimer) {
1824
+ clearTimeout(this.pendingOutboundStaleTimer);
1825
+ this.pendingOutboundStaleTimer = null;
1826
+ }
1788
1827
  this.onStatusChange?.();
1789
1828
  } catch (error: any) {
1790
1829
  LOG.warn('CLI', `[${this.cliType}] queued outbound flush failed: ${error?.message || error}`);
@@ -2174,6 +2213,7 @@ export class ProviderCliAdapter implements CliAdapter {
2174
2213
  this.pendingTerminalQueryTail = '';
2175
2214
  this.ptyOutputChunks = [];
2176
2215
  if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2216
+ if (this.pendingOutboundStaleTimer) { clearTimeout(this.pendingOutboundStaleTimer); this.pendingOutboundStaleTimer = null; }
2177
2217
  this.pendingOutboundQueue = [];
2178
2218
  this.pendingOutboundFlushInFlight = false;
2179
2219
  if (this.ptyProcess) {
@@ -2197,6 +2237,7 @@ export class ProviderCliAdapter implements CliAdapter {
2197
2237
  this.pendingTerminalQueryTail = '';
2198
2238
  this.ptyOutputChunks = [];
2199
2239
  if (this.pendingOutboundFlushTimer) { clearTimeout(this.pendingOutboundFlushTimer); this.pendingOutboundFlushTimer = null; }
2240
+ if (this.pendingOutboundStaleTimer) { clearTimeout(this.pendingOutboundStaleTimer); this.pendingOutboundStaleTimer = null; }
2200
2241
  this.pendingOutboundQueue = [];
2201
2242
  this.pendingOutboundFlushInFlight = false;
2202
2243
  if (this.ptyProcess) {
package/src/index.ts CHANGED
@@ -288,7 +288,7 @@ export { loadRepoSettings } from './config/repo-settings.js';
288
288
  export type { RepoSettings, LoadRepoSettingsOptions } from './config/repo-settings.js';
289
289
 
290
290
  // ── Mesh Task Ledger ──
291
- export { appendLedgerEntry, appendRemoteLedgerEntries, buildTaskCompletionEvidence, normalizeMeshWorkerResult, readLedgerEntries, readLedgerSlice, readLedgerSliceFromStore, getLedgerSummary, getLedgerDir, getSessionRecoveryContext, MAX_LEDGER_SLICE_LIMIT, tombstoneOperatingNote, readOperatingNotes, pruneOperatingNotes, isOperatingNoteTombstoned, OPERATING_NOTE_KIND, OPERATING_NOTE_TOMBSTONE_KIND, OPERATING_NOTE_DEDUPE_WINDOW, OPERATING_NOTE_KEEP_LATEST } from './mesh/mesh-ledger.js';
291
+ export { appendLedgerEntry, appendRemoteLedgerEntries, buildTaskCompletionEvidence, normalizeMeshWorkerResult, readLedgerEntries, readLedgerSlice, readLedgerSliceFromStore, getLedgerSummary, getLedgerDir, getSessionRecoveryContext, ledgerEntryTaskId, MAX_LEDGER_SLICE_LIMIT, tombstoneOperatingNote, readOperatingNotes, pruneOperatingNotes, isOperatingNoteTombstoned, OPERATING_NOTE_KIND, OPERATING_NOTE_TOMBSTONE_KIND, OPERATING_NOTE_DEDUPE_WINDOW, OPERATING_NOTE_KEEP_LATEST } from './mesh/mesh-ledger.js';
292
292
  export type { AppendRemoteLedgerResult, MeshLedgerEntry, MeshLedgerKind, MeshLedgerSlice, MeshLedgerSummary, ReadLedgerOptions, ReadLedgerSliceOptions, SessionRecoveryContext, MeshTaskCompletionEvidence, MeshWorkerResultArtifact, MeshProcessArtifact, MeshValidationResultArtifact } from './mesh/mesh-ledger.js';
293
293
  export { fastForwardMeshNode } from './mesh/mesh-fast-forward.js';
294
294
  export type { MeshFastForwardNodeArgs, MeshFastForwardPlannedStep, MeshFastForwardResult } from './mesh/mesh-fast-forward.js';
@@ -29,6 +29,11 @@ import {
29
29
 
30
30
  export type MeshLedgerKind =
31
31
  | 'task_dispatched'
32
+ // LEDGER-TASK-TRACEABILITY (C): a queue task transitioned pending→assigned (a
33
+ // node/session claimed it). Distinct from task_dispatched (the message was handed
34
+ // to the transport): claim precedes dispatch and marks the lifecycle handoff.
35
+ // payload: { taskId, nodeId, sessionId, providerType?, claimedAt }
36
+ | 'task_claimed'
32
37
  | 'task_completed'
33
38
  | 'task_failed'
34
39
  | 'task_stalled'
@@ -111,9 +116,40 @@ export interface MeshLedgerEntry {
111
116
  nodeId?: string;
112
117
  sessionId?: string;
113
118
  providerType?: string;
119
+ // LEDGER-TASK-TRACEABILITY (B): the task this entry pertains to, promoted from
120
+ // payload.taskId to a top-level base field so a task's lifecycle
121
+ // (task_dispatched → task_claimed → task_completed/failed/stalled/reclaimed) can
122
+ // be joined by kind+taskId without an O(n) per-entry payload scan. Optional and
123
+ // back-compat: legacy rows never carried it — readers fall back to payload.taskId,
124
+ // and appendLedgerEntry auto-derives it from payload.taskId for task-lifecycle
125
+ // kinds so every such entry is uniformly queryable.
126
+ taskId?: string;
114
127
  payload: Record<string, unknown>;
115
128
  }
116
129
 
130
+ // LEDGER-TASK-TRACEABILITY (B): the kinds whose taskId base field is auto-derived
131
+ // from payload.taskId at append time (so a join by kind+taskId is uniform). Other
132
+ // kinds may still set taskId explicitly; this only backfills the common lifecycle.
133
+ const TASK_LIFECYCLE_LEDGER_KINDS: ReadonlySet<MeshLedgerKind> = new Set<MeshLedgerKind>([
134
+ 'task_dispatched',
135
+ 'task_claimed',
136
+ 'task_completed',
137
+ 'task_failed',
138
+ 'task_stalled',
139
+ 'task_reclaimed',
140
+ 'task_approval_needed',
141
+ 'task_question_pending',
142
+ 'p2p_dispatch_failed',
143
+ ]);
144
+
145
+ /** Resolve the taskId for a ledger entry, preferring the base field and falling
146
+ * back to payload.taskId (legacy rows / entries that only carry it in payload). */
147
+ export function ledgerEntryTaskId(entry: Pick<MeshLedgerEntry, 'taskId' | 'payload'>): string | undefined {
148
+ if (typeof entry.taskId === 'string' && entry.taskId.trim()) return entry.taskId.trim();
149
+ const fromPayload = entry.payload && typeof entry.payload === 'object' ? (entry.payload as Record<string, unknown>).taskId : undefined;
150
+ return typeof fromPayload === 'string' && fromPayload.trim() ? fromPayload.trim() : undefined;
151
+ }
152
+
117
153
  export function isIntentionalCleanupStopEntry(entry: Pick<MeshLedgerEntry, 'kind' | 'payload'>): boolean {
118
154
  if (entry.kind !== 'session_stopped' && entry.kind !== 'task_failed' && entry.kind !== 'task_stalled') return false;
119
155
  const payload = entry.payload && typeof entry.payload === 'object' && !Array.isArray(entry.payload)
@@ -796,6 +832,14 @@ export function appendLedgerEntry(
796
832
  ...partial,
797
833
  };
798
834
 
835
+ // LEDGER-TASK-TRACEABILITY (B): backfill the taskId base field from payload.taskId
836
+ // for task-lifecycle kinds so every dispatch/claim/complete/fail/stall/reclaim
837
+ // entry is uniformly join-able by kind+taskId. An explicit partial.taskId wins.
838
+ if (!entry.taskId && TASK_LIFECYCLE_LEDGER_KINDS.has(entry.kind)) {
839
+ const derived = ledgerEntryTaskId(entry);
840
+ if (derived) entry.taskId = derived;
841
+ }
842
+
799
843
  const filePath = getLedgerPath(meshId);
800
844
 
801
845
  // Compact or rotate based on file size
@@ -822,6 +866,7 @@ export function appendLedgerEntry(
822
866
  nodeId: entry.nodeId ?? null,
823
867
  sessionId: entry.sessionId ?? null,
824
868
  providerType: entry.providerType ?? null,
869
+ taskId: entry.taskId ?? null,
825
870
  payload: entry.payload,
826
871
  });
827
872
  } catch {
@@ -1074,7 +1119,15 @@ function readLedgerFile(meshId: string): MeshLedgerEntry[] {
1074
1119
  if (!line.trim()) continue;
1075
1120
  try {
1076
1121
  const entry = JSON.parse(line) as MeshLedgerEntry;
1077
- if (entry.id && entry.kind) entries.push(entry);
1122
+ if (entry.id && entry.kind) {
1123
+ // LEDGER-TASK-TRACEABILITY (B): backfill the base taskId from
1124
+ // payload.taskId for legacy JSONL lines that predate the field.
1125
+ if (!entry.taskId) {
1126
+ const derived = ledgerEntryTaskId(entry);
1127
+ if (derived) entry.taskId = derived;
1128
+ }
1129
+ entries.push(entry);
1130
+ }
1078
1131
  } catch { /* skip malformed lines */ }
1079
1132
  }
1080
1133
  return entries;
@@ -1108,6 +1161,7 @@ function ensureLedgerImported(store: MeshRuntimeStore, meshId: string): void {
1108
1161
  nodeId: e.nodeId ?? null,
1109
1162
  sessionId: e.sessionId ?? null,
1110
1163
  providerType: e.providerType ?? null,
1164
+ taskId: ledgerEntryTaskId(e) ?? null,
1111
1165
  payload: e.payload ?? {},
1112
1166
  })));
1113
1167
  } catch { /* import is best-effort; reads fall back to JSONL on store failure */ }
@@ -1116,16 +1170,23 @@ function ensureLedgerImported(store: MeshRuntimeStore, meshId: string): void {
1116
1170
  function readLedgerFromStore(meshId: string): MeshLedgerEntry[] {
1117
1171
  const store = MeshRuntimeStore.getInstance();
1118
1172
  ensureLedgerImported(store, meshId);
1119
- return store.readLedgerEntriesOrdered(meshId).map(r => ({
1120
- id: r.id,
1121
- meshId: r.meshId,
1122
- timestamp: r.timestamp,
1123
- kind: r.kind as MeshLedgerKind,
1124
- ...(r.nodeId ? { nodeId: r.nodeId } : {}),
1125
- ...(r.sessionId ? { sessionId: r.sessionId } : {}),
1126
- ...(r.providerType ? { providerType: r.providerType } : {}),
1127
- payload: (r.payload && typeof r.payload === 'object' ? r.payload : {}) as Record<string, unknown>,
1128
- }));
1173
+ return store.readLedgerEntriesOrdered(meshId).map(r => {
1174
+ const payload = (r.payload && typeof r.payload === 'object' ? r.payload : {}) as Record<string, unknown>;
1175
+ // LEDGER-TASK-TRACEABILITY (B): prefer the column, fall back to payload.taskId
1176
+ // for legacy rows written before the column existed (back-compat join).
1177
+ const taskId = ledgerEntryTaskId({ taskId: r.taskId ?? undefined, payload });
1178
+ return {
1179
+ id: r.id,
1180
+ meshId: r.meshId,
1181
+ timestamp: r.timestamp,
1182
+ kind: r.kind as MeshLedgerKind,
1183
+ ...(r.nodeId ? { nodeId: r.nodeId } : {}),
1184
+ ...(r.sessionId ? { sessionId: r.sessionId } : {}),
1185
+ ...(r.providerType ? { providerType: r.providerType } : {}),
1186
+ ...(taskId ? { taskId } : {}),
1187
+ payload,
1188
+ };
1189
+ });
1129
1190
  }
1130
1191
 
1131
1192
  function getCachedRawEntries(meshId: string): MeshLedgerEntry[] {