@adhdev/daemon-core 0.9.82-rc.45 → 0.9.82-rc.450
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 +9 -0
- package/dist/boot/process-hardening.d.ts +50 -0
- package/dist/build-info.d.ts +37 -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 +68 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +200 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +126 -64
- package/dist/cli-adapters/provider-cli-parse.d.ts +5 -0
- package/dist/cli-adapters/provider-cli-runtime.d.ts +34 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +74 -0
- package/dist/cli-adapters/pty-write-chunking.d.ts +34 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -0
- package/dist/cli-adapters/resolve-executable.d.ts +69 -0
- package/dist/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +6 -3
- package/dist/cli-adapters/terminal-backends/types.d.ts +10 -2
- package/dist/cli-adapters/terminal-screen.d.ts +4 -11
- package/dist/commands/chat-commands-debug-bundle.d.ts +14 -0
- package/dist/commands/chat-commands-read.d.ts +7 -0
- package/dist/commands/chat-commands-scope.d.ts +39 -0
- package/dist/commands/chat-commands-shared.d.ts +33 -0
- package/dist/commands/chat-commands-write.d.ts +14 -0
- package/dist/commands/chat-commands.d.ts +10 -23
- package/dist/commands/cli-manager.d.ts +25 -1
- package/dist/commands/handler.d.ts +125 -0
- package/dist/commands/high-family/index.d.ts +3 -0
- package/dist/commands/high-family/mesh-coordinator-launch.d.ts +2 -0
- package/dist/commands/high-family/mesh-events.d.ts +2 -0
- package/dist/commands/high-family/mesh-status.d.ts +2 -0
- package/dist/commands/high-family/types.d.ts +64 -0
- package/dist/commands/low-family/coordinator-prompt.d.ts +9 -0
- package/dist/commands/low-family/daemon-lifecycle.d.ts +2 -0
- package/dist/commands/low-family/diagnostics.d.ts +2 -0
- package/dist/commands/low-family/index.d.ts +3 -0
- package/dist/commands/low-family/mesh-ledger.d.ts +10 -0
- package/dist/commands/low-family/mesh-node-logs.d.ts +2 -0
- package/dist/commands/low-family/notification.d.ts +2 -0
- package/dist/commands/low-family/refine-config.d.ts +2 -0
- package/dist/commands/low-family/session-host.d.ts +2 -0
- package/dist/commands/low-family/spec-providerdev.d.ts +11 -0
- package/dist/commands/low-family/status-meta.d.ts +2 -0
- package/dist/commands/low-family/types.d.ts +33 -0
- package/dist/commands/med-family/cli-agent.d.ts +2 -0
- package/dist/commands/med-family/fast-forward.d.ts +2 -0
- package/dist/commands/med-family/ide.d.ts +10 -0
- package/dist/commands/med-family/index.d.ts +3 -0
- package/dist/commands/med-family/mesh-crud.d.ts +29 -0
- package/dist/commands/med-family/mesh-host-pairing.d.ts +2 -0
- package/dist/commands/med-family/mesh-queue.d.ts +2 -0
- package/dist/commands/med-family/mesh-restart.d.ts +2 -0
- package/dist/commands/med-family/types.d.ts +152 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router.d.ts +279 -2
- package/dist/commands/upgrade-helper.d.ts +41 -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 +129 -1
- package/dist/config/mesh-json-config.d.ts +199 -0
- package/dist/config/repo-settings.d.ts +77 -0
- package/dist/daemon/dev-server.d.ts +0 -2
- package/dist/detection/ide-detector.d.ts +13 -0
- package/dist/detection/win32-ide-version.d.ts +37 -0
- package/dist/git/change-impact-config.d.ts +159 -0
- package/dist/git/git-commands.d.ts +12 -1
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-executor.d.ts +11 -0
- package/dist/git/git-status.d.ts +57 -0
- package/dist/git/git-types.d.ts +2 -50
- package/dist/git/git-worktree.d.ts +71 -1
- package/dist/git/index.d.ts +3 -1
- package/dist/index.d.ts +66 -14
- package/dist/index.js +48475 -17084
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +49521 -18300
- 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/logging/log-redactor.d.ts +24 -0
- package/dist/logging/log-tail-reader.d.ts +81 -0
- package/dist/logging/logger.d.ts +1 -1
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +89 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +174 -0
- package/dist/mesh/mesh-clone-grace.d.ts +8 -0
- package/dist/mesh/mesh-coordinator-config.d.ts +21 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
- package/dist/mesh/mesh-event-classify.d.ts +5 -0
- package/dist/mesh/mesh-event-forwarding.d.ts +41 -0
- package/dist/mesh/mesh-event-trace.d.ts +21 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +4 -0
- package/dist/mesh/mesh-events-pending.d.ts +69 -0
- package/dist/mesh/mesh-events-stale.d.ts +61 -0
- package/dist/mesh/mesh-events-utils.d.ts +75 -0
- package/dist/mesh/mesh-events.d.ts +6 -49
- package/dist/mesh/mesh-fast-forward.d.ts +81 -0
- package/dist/mesh/mesh-host-ownership.d.ts +29 -0
- package/dist/mesh/mesh-init.d.ts +128 -0
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -4
- package/dist/mesh/mesh-ledger.d.ts +115 -1
- package/dist/mesh/mesh-magi-status.d.ts +63 -0
- package/dist/mesh/mesh-missions.d.ts +189 -0
- package/dist/mesh/mesh-node-identity.d.ts +306 -0
- package/dist/mesh/mesh-queue-assignment.d.ts +113 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +79 -0
- package/dist/mesh/mesh-refine-batch.d.ts +68 -0
- package/dist/mesh/mesh-refine-gates.d.ts +428 -0
- package/dist/mesh/mesh-refine-status.d.ts +62 -0
- package/dist/mesh/mesh-review-inbox.d.ts +90 -0
- package/dist/mesh/mesh-routing.d.ts +70 -0
- package/dist/mesh/mesh-runtime-store.d.ts +450 -0
- package/dist/mesh/mesh-scheduling-runtime.d.ts +78 -0
- package/dist/mesh/mesh-task-inflight.d.ts +46 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-unresolved-forward-outbox.d.ts +30 -0
- package/dist/mesh/mesh-warmup-deadline.d.ts +68 -0
- package/dist/mesh/mesh-work-queue.d.ts +300 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +216 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +145 -0
- package/dist/providers/acp-provider-instance.d.ts +5 -0
- package/dist/providers/approval-utils.d.ts +20 -0
- package/dist/providers/chat-message-normalization.d.ts +32 -1
- package/dist/providers/cli-provider-instance.d.ts +254 -3
- package/dist/providers/contracts.d.ts +149 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/manual-attendance.d.ts +63 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +126 -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/constants.d.ts +12 -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 +29 -0
- package/dist/providers/provider-instance.d.ts +23 -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 +86 -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 +98 -0
- package/dist/providers/spec/cli-adapter.d.ts +223 -0
- package/dist/providers/spec/evaluator.d.ts +45 -0
- package/dist/providers/spec/fsm-driver.d.ts +444 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +93 -0
- package/dist/providers/spec/fsm-loader.d.ts +14 -0
- package/dist/providers/spec/fsm-types.d.ts +202 -0
- package/dist/providers/spec/native-history-executor.d.ts +41 -0
- package/dist/providers/spec/pre-launch-trust.d.ts +16 -0
- package/dist/providers/spec/route.d.ts +4 -0
- package/dist/providers/spec/types.d.ts +222 -0
- package/dist/providers/status-monitor.d.ts +7 -7
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +84 -0
- package/dist/providers/working-dir.d.ts +17 -0
- package/dist/repo-mesh-types.d.ts +519 -13
- package/dist/runtime-defaults.d.ts +2 -0
- package/dist/session-host/managed-host.d.ts +64 -0
- package/dist/sessions/registry.d.ts +6 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +59 -2
- 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 +26 -0
- package/dist/system/hash.d.ts +8 -0
- package/dist/system/load-better-sqlite3.d.ts +21 -0
- package/dist/types.d.ts +5 -0
- package/package.json +7 -3
- package/src/agent-stream/poller.ts +2 -3
- package/src/agent-stream/provider-adapter.ts +1 -1
- package/src/boot/daemon-lifecycle.ts +63 -12
- package/src/boot/process-hardening.ts +89 -0
- package/src/build-info.ts +73 -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 +3 -1
- package/src/cli-adapter-types.ts +68 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1246 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +914 -1144
- package/src/cli-adapters/provider-cli-parse.d.ts +2 -0
- package/src/cli-adapters/provider-cli-parse.ts +19 -0
- package/src/cli-adapters/provider-cli-runtime.ts +56 -7
- package/src/cli-adapters/provider-cli-shared.d.ts +4 -0
- package/src/cli-adapters/provider-cli-shared.ts +121 -11
- package/src/cli-adapters/pty-transport.ts +2 -1
- package/src/cli-adapters/pty-write-chunking.ts +106 -0
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/resolve-executable.ts +204 -0
- package/src/cli-adapters/session-host-transport.ts +2 -1
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +63 -29
- package/src/cli-adapters/terminal-backends/types.ts +10 -3
- package/src/cli-adapters/terminal-screen.ts +16 -81
- package/src/commands/WINDOWS-UPGRADE-LOCK-FAILURE.md +198 -0
- package/src/commands/chat-commands-debug-bundle.ts +398 -0
- package/src/commands/chat-commands-read.ts +2488 -0
- package/src/commands/chat-commands-scope.ts +54 -0
- package/src/commands/chat-commands-shared.ts +114 -0
- package/src/commands/chat-commands-write.ts +891 -0
- package/src/commands/chat-commands.ts +19 -1841
- package/src/commands/cli-manager.ts +596 -27
- package/src/commands/handler.ts +841 -2
- package/src/commands/high-family/index.ts +28 -0
- package/src/commands/high-family/mesh-coordinator-launch.ts +680 -0
- package/src/commands/high-family/mesh-events.ts +89 -0
- package/src/commands/high-family/mesh-status.ts +822 -0
- package/src/commands/high-family/types.ts +80 -0
- package/src/commands/low-family/coordinator-prompt.ts +72 -0
- package/src/commands/low-family/daemon-lifecycle.ts +107 -0
- package/src/commands/low-family/diagnostics.ts +57 -0
- package/src/commands/low-family/index.ts +37 -0
- package/src/commands/low-family/mesh-ledger.ts +62 -0
- package/src/commands/low-family/mesh-node-logs.ts +87 -0
- package/src/commands/low-family/notification.ts +116 -0
- package/src/commands/low-family/refine-config.ts +106 -0
- package/src/commands/low-family/session-host.ts +274 -0
- package/src/commands/low-family/spec-providerdev.ts +217 -0
- package/src/commands/low-family/status-meta.ts +112 -0
- package/src/commands/low-family/types.ts +39 -0
- package/src/commands/med-family/cli-agent.ts +238 -0
- package/src/commands/med-family/fast-forward.ts +230 -0
- package/src/commands/med-family/ide.ts +163 -0
- package/src/commands/med-family/index.ts +37 -0
- package/src/commands/med-family/mesh-crud.ts +1253 -0
- package/src/commands/med-family/mesh-host-pairing.ts +234 -0
- package/src/commands/med-family/mesh-queue.ts +167 -0
- package/src/commands/med-family/mesh-restart.ts +92 -0
- package/src/commands/med-family/types.ts +153 -0
- package/src/commands/mesh-coordinator.ts +334 -124
- package/src/commands/router.ts +2893 -3536
- package/src/commands/stream-commands.ts +8 -0
- package/src/commands/upgrade-helper.ts +310 -45
- package/src/config/chat-history.ts +493 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +619 -23
- package/src/config/mesh-json-config.ts +376 -0
- package/src/config/recent-activity.ts +8 -2
- package/src/config/repo-settings.ts +111 -0
- package/src/daemon/dev-auto-implement.ts +3 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/daemon/dev-server.ts +0 -541
- package/src/detection/cli-detector.ts +28 -9
- package/src/detection/ide-detector.ts +55 -16
- package/src/detection/win32-ide-version.ts +106 -0
- package/src/git/change-impact-config.ts +354 -0
- package/src/git/git-commands.ts +78 -15
- package/src/git/git-diff.ts +81 -11
- package/src/git/git-executor.ts +12 -0
- package/src/git/git-status.ts +767 -48
- package/src/git/git-types.ts +14 -62
- package/src/git/git-worktree.ts +261 -4
- package/src/git/index.ts +17 -0
- package/src/index.ts +198 -12
- 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/command-log.ts +7 -5
- package/src/logging/log-redactor.ts +100 -0
- package/src/logging/log-tail-reader.ts +341 -0
- package/src/logging/logger.ts +14 -7
- package/src/mesh/contracts.ts +338 -0
- package/src/mesh/coordinator-prompt.ts +416 -34
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +645 -0
- package/src/mesh/mesh-clone-grace.ts +68 -0
- package/src/mesh/mesh-coordinator-config.ts +97 -0
- package/src/mesh/mesh-delivery-policy.ts +315 -0
- package/src/mesh/mesh-event-classify.ts +51 -0
- package/src/mesh/mesh-event-forwarding.ts +1897 -0
- package/src/mesh/mesh-event-trace.ts +67 -0
- package/src/mesh/mesh-events-coordinator.ts +32 -0
- package/src/mesh/mesh-events-pending.ts +656 -0
- package/src/mesh/mesh-events-stale.ts +389 -0
- package/src/mesh/mesh-events-utils.ts +443 -0
- package/src/mesh/mesh-events.ts +33 -1035
- package/src/mesh/mesh-fast-forward.ts +843 -0
- package/src/mesh/mesh-host-ownership.ts +111 -0
- package/src/mesh/mesh-init.ts +350 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -7
- package/src/mesh/mesh-ledger.ts +841 -102
- package/src/mesh/mesh-magi-status.ts +223 -0
- package/src/mesh/mesh-missions.ts +449 -0
- package/src/mesh/mesh-node-identity.ts +1985 -0
- package/src/mesh/mesh-queue-assignment.ts +1924 -0
- package/src/mesh/mesh-reconcile-loop.ts +2018 -0
- package/src/mesh/mesh-refine-batch.ts +205 -0
- package/src/mesh/mesh-refine-gates.ts +1673 -0
- package/src/mesh/mesh-refine-status.ts +231 -0
- package/src/mesh/mesh-review-inbox.ts +307 -0
- package/src/mesh/mesh-routing.ts +291 -0
- package/src/mesh/mesh-runtime-store.ts +1853 -0
- package/src/mesh/mesh-scheduling-runtime.ts +198 -0
- package/src/mesh/mesh-task-inflight.ts +70 -0
- package/src/mesh/mesh-task-stats.ts +161 -0
- package/src/mesh/mesh-unresolved-forward-outbox.ts +185 -0
- package/src/mesh/mesh-warmup-deadline.ts +152 -0
- package/src/mesh/mesh-work-queue.ts +1254 -141
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +423 -0
- package/src/mesh/worktree-bootstrap-config.ts +450 -0
- package/src/providers/acp-provider-instance.ts +43 -10
- package/src/providers/approval-utils.d.ts +5 -0
- package/src/providers/approval-utils.ts +57 -5
- package/src/providers/chat-message-normalization.ts +92 -4
- package/src/providers/cli-provider-instance.ts +2177 -107
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +160 -6
- package/src/providers/extension-provider-instance.ts +12 -7
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +35 -12
- package/src/providers/manual-attendance.ts +85 -0
- package/src/providers/native-history/antigravity-cli-transcript.ts +1043 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +479 -0
- package/src/providers/native-history/constants.ts +19 -0
- package/src/providers/native-history/dispatcher.ts +373 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +260 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +71 -0
- package/src/providers/provider-instance.ts +19 -1
- package/src/providers/provider-loader.ts +668 -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 +313 -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 +509 -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 +246 -0
- package/src/providers/spec/cli-adapter.ts +1064 -0
- package/src/providers/spec/evaluator.ts +407 -0
- package/src/providers/spec/fsm-driver.ts +1481 -0
- package/src/providers/spec/fsm-evaluator.ts +290 -0
- package/src/providers/spec/fsm-loader.ts +128 -0
- package/src/providers/spec/fsm-types.ts +301 -0
- package/src/providers/spec/native-history-executor.ts +1174 -0
- package/src/providers/spec/pre-launch-trust.ts +104 -0
- package/src/providers/spec/route.ts +51 -0
- package/src/providers/spec/types.ts +262 -0
- package/src/providers/status-monitor.d.ts +7 -7
- package/src/providers/status-monitor.ts +37 -22
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +536 -0
- package/src/providers/version-archive.ts +64 -24
- package/src/providers/working-dir.ts +23 -0
- package/src/repo-mesh-types.ts +758 -14
- package/src/runtime-defaults.ts +39 -0
- package/src/session-host/managed-host.ts +218 -0
- package/src/sessions/registry.ts +6 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.d.ts +8 -0
- package/src/shared-types.ts +64 -1
- package/src/status/builders.ts +26 -6
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +19 -1
- package/src/status/snapshot.ts +95 -26
- package/src/system/hash.ts +23 -0
- package/src/system/host-memory.ts +29 -12
- package/src/system/load-better-sqlite3.ts +68 -0
- package/src/types.ts +5 -0
- package/dist/cli-adapters/terminal-backends/xterm-backend.d.ts +0 -17
- package/dist/mesh/mesh-sync.d.ts +0 -53
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.d.ts +0 -16
- package/src/cli-adapters/terminal-backends/xterm-backend.ts +0 -97
- package/src/mesh/mesh-sync.ts +0 -111
package/src/mesh/mesh-ledger.ts
CHANGED
|
@@ -13,11 +13,12 @@
|
|
|
13
13
|
* Safety: mode 0o600, atomic append via appendFileSync
|
|
14
14
|
*/
|
|
15
15
|
|
|
16
|
-
import { existsSync, mkdirSync, readFileSync,
|
|
16
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync, statSync, renameSync, writeFileSync } from 'fs';
|
|
17
17
|
import { join } from 'path';
|
|
18
18
|
import { randomUUID } from 'crypto';
|
|
19
19
|
import { getConfigDir } from '../config/config.js';
|
|
20
20
|
import { EventEmitter } from 'events';
|
|
21
|
+
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
21
22
|
// ─── Types ──────────────────────────────────────
|
|
22
23
|
|
|
23
24
|
export type MeshLedgerKind =
|
|
@@ -26,16 +27,50 @@ export type MeshLedgerKind =
|
|
|
26
27
|
| 'task_failed'
|
|
27
28
|
| 'task_stalled'
|
|
28
29
|
| 'task_approval_needed'
|
|
30
|
+
| 'p2p_dispatch_failed'
|
|
29
31
|
| 'session_launched'
|
|
30
32
|
| 'session_auto_launch'
|
|
31
33
|
| 'session_stopped'
|
|
32
34
|
| 'checkpoint_created'
|
|
33
35
|
| 'node_cloned'
|
|
36
|
+
| 'node_joined'
|
|
34
37
|
| 'node_removed'
|
|
35
38
|
| 'coordinator_started'
|
|
36
39
|
| 'recovery_attempted'
|
|
37
40
|
| 'ledger_replicated'
|
|
38
41
|
| 'ledger_reconciled'
|
|
42
|
+
| 'direct_fast_forward'
|
|
43
|
+
| 'delivery_unroutable'
|
|
44
|
+
| 'direct_dispatch_pruned'
|
|
45
|
+
| 'event_held'
|
|
46
|
+
| 'task_reclaimed'
|
|
47
|
+
// Gap2-A: a coordinator-recorded operating note — a runtime-accumulated
|
|
48
|
+
// lesson (provider quirk, pattern to avoid, recovery lesson) persisted in
|
|
49
|
+
// the ledger so it survives coordinator restarts and is provider-neutral.
|
|
50
|
+
// payload: { text, category?, createdAt?, sourceCoordinator? }
|
|
51
|
+
| 'coordinator_operating_note'
|
|
52
|
+
// Retraction of a coordinator_operating_note. Append-only (history preserved);
|
|
53
|
+
// readers filter out the targeted note so it leaves the prompt/list. Targets by
|
|
54
|
+
// note id (exact) and/or by trimmed-text fingerprint (matches all notes with that
|
|
55
|
+
// text). payload: { targetNoteId?, targetFingerprint?, reason?, forgottenAt? }
|
|
56
|
+
| 'coordinator_operating_note_tombstone'
|
|
57
|
+
// Mission audit trail: mission record mutations (mesh_mission_upsert) so the
|
|
58
|
+
// ledger captures mission lifecycle, not just task events. Without these a
|
|
59
|
+
// mission create / goal rewrite / status transition left no ledger trace,
|
|
60
|
+
// breaking audit continuity and post-restart recovery.
|
|
61
|
+
// mission_created payload: { missionId, title, goalSummary, goalLength, goalTruncated, status }
|
|
62
|
+
// mission_status_changed payload: { missionId, title, fromStatus, toStatus }
|
|
63
|
+
// mission_goal_updated payload: { missionId, title, prevGoalSummary, nextGoalSummary, prevGoalLength, nextGoalLength, goalTruncated }
|
|
64
|
+
| 'mission_created'
|
|
65
|
+
| 'mission_status_changed'
|
|
66
|
+
| 'mission_goal_updated'
|
|
67
|
+
// MAGI (Multi-Agent Ground-truth Insight) cross-verification activity. Persisted
|
|
68
|
+
// so a wait=false fan-out and its later synthesis survive coordinator restarts and
|
|
69
|
+
// are foldable into mesh_status (keyed by consensusGroupId).
|
|
70
|
+
// magi_dispatched payload: { source:'magi', consensusGroupId, missionId?, panel?, question?, replicaCount }
|
|
71
|
+
// magi_synthesis payload: { source:'magi', consensusGroupId, missionId?, panel?, question?, synthesis }
|
|
72
|
+
| 'magi_dispatched'
|
|
73
|
+
| 'magi_synthesis'
|
|
39
74
|
;
|
|
40
75
|
|
|
41
76
|
export interface MeshLedgerEntry {
|
|
@@ -61,6 +96,51 @@ export function isIntentionalCleanupStopEntry(entry: Pick<MeshLedgerEntry, 'kind
|
|
|
61
96
|
|| payload.source === 'mesh_remove_node');
|
|
62
97
|
}
|
|
63
98
|
|
|
99
|
+
export type MeshWorkerResultStatus = 'completed' | 'failed' | 'blocked' | 'partial' | 'unknown';
|
|
100
|
+
export type MeshProcessArtifactKind = 'process' | 'log' | 'port' | 'window' | 'session' | 'file' | 'url' | 'other';
|
|
101
|
+
|
|
102
|
+
export interface MeshValidationResultArtifact {
|
|
103
|
+
command?: string;
|
|
104
|
+
status: 'passed' | 'failed' | 'skipped' | 'unknown';
|
|
105
|
+
durationMs?: number;
|
|
106
|
+
outputPath?: string;
|
|
107
|
+
summary?: string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export interface MeshProcessArtifact {
|
|
111
|
+
kind: MeshProcessArtifactKind;
|
|
112
|
+
id?: string;
|
|
113
|
+
label?: string;
|
|
114
|
+
locator?: string;
|
|
115
|
+
pid?: number;
|
|
116
|
+
port?: number;
|
|
117
|
+
url?: string;
|
|
118
|
+
path?: string;
|
|
119
|
+
sessionId?: string;
|
|
120
|
+
keepRunning?: boolean;
|
|
121
|
+
metadata?: Record<string, unknown>;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
export interface MeshWorkerResultArtifact {
|
|
125
|
+
status: MeshWorkerResultStatus;
|
|
126
|
+
classification?: string;
|
|
127
|
+
changedFiles: string[];
|
|
128
|
+
validationResults: MeshValidationResultArtifact[];
|
|
129
|
+
gitStatus?: Record<string, unknown>;
|
|
130
|
+
processArtifacts: MeshProcessArtifact[];
|
|
131
|
+
errors: string[];
|
|
132
|
+
nextAction?: string;
|
|
133
|
+
requiresUserAction: boolean;
|
|
134
|
+
// NOTIF Defect-2b: `parseable_answer` = the final summary held a parseable JSON
|
|
135
|
+
// ANSWER (e.g. a MAGI claim_audit / rca envelope) that is NOT worker-result-shaped
|
|
136
|
+
// (no status + changedFiles/errors/…). It is still concrete evidence that the worker
|
|
137
|
+
// produced a real, parseable answer — so it must NOT be labelled evidenceLevel
|
|
138
|
+
// 'insufficient' — but it is NOT a self-attributing worker result, so it is deliberately
|
|
139
|
+
// distinct from 'final_summary_json' and stays subject to the direct-dispatch grace gate
|
|
140
|
+
// in mesh-events-stale.ts (which keys on `!== 'final_summary_json'`).
|
|
141
|
+
source: 'explicit_metadata' | 'final_summary_json' | 'parseable_answer' | 'default';
|
|
142
|
+
}
|
|
143
|
+
|
|
64
144
|
export interface MeshTaskCompletionEvidence {
|
|
65
145
|
source: 'agent_status_event';
|
|
66
146
|
event: 'agent:generating_completed' | 'agent:ready';
|
|
@@ -74,6 +154,7 @@ export interface MeshTaskCompletionEvidence {
|
|
|
74
154
|
providerSessionId?: string;
|
|
75
155
|
finalSummaryAvailable: boolean;
|
|
76
156
|
};
|
|
157
|
+
workerResult: MeshWorkerResultArtifact;
|
|
77
158
|
git: {
|
|
78
159
|
status: 'deferred';
|
|
79
160
|
reason: string;
|
|
@@ -96,6 +177,7 @@ export interface BuildTaskCompletionEvidenceOptions {
|
|
|
96
177
|
providerType?: string;
|
|
97
178
|
providerSessionId?: string;
|
|
98
179
|
finalSummary?: string;
|
|
180
|
+
workerResult?: Record<string, unknown>;
|
|
99
181
|
completedAt?: string;
|
|
100
182
|
}
|
|
101
183
|
|
|
@@ -160,11 +242,48 @@ export interface AppendRemoteLedgerResult {
|
|
|
160
242
|
// ─── Constants ──────────────────────────────────
|
|
161
243
|
|
|
162
244
|
const LEDGER_DIR_NAME = 'mesh-ledger';
|
|
163
|
-
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB
|
|
245
|
+
const MAX_FILE_SIZE_BYTES = 10 * 1024 * 1024; // 10 MB — full rotation threshold
|
|
246
|
+
const COMPACT_THRESHOLD_BYTES = 2 * 1024 * 1024; // 2 MB — compaction threshold
|
|
247
|
+
const ARCHIVE_TERMINAL_OLDER_THAN_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
|
|
164
248
|
const RECENT_FAILURE_WINDOW_MS = 30 * 60 * 1000; // 30 minutes
|
|
249
|
+
|
|
250
|
+
// Kinds that accumulate indefinitely and are safe to archive after ARCHIVE_TERMINAL_OLDER_THAN_MS.
|
|
251
|
+
// Non-terminal kinds (dispatched, sessions, nodes, checkpoints) are always kept in the active file.
|
|
252
|
+
const ARCHIVABLE_KINDS: ReadonlySet<MeshLedgerKind> = new Set([
|
|
253
|
+
'task_completed',
|
|
254
|
+
'task_failed',
|
|
255
|
+
'task_stalled',
|
|
256
|
+
'recovery_attempted',
|
|
257
|
+
] as MeshLedgerKind[]);
|
|
165
258
|
const DEFAULT_LEDGER_SLICE_LIMIT = 100;
|
|
166
259
|
export const MAX_LEDGER_SLICE_LIMIT = 500;
|
|
167
260
|
|
|
261
|
+
// ─── Operating-note growth control ──────────────
|
|
262
|
+
// coordinator_operating_note is append-only and, unlike task_* entries, is never
|
|
263
|
+
// archived by compactLedger (it is not in ARCHIVABLE_KINDS — it must survive
|
|
264
|
+
// restarts and there is no time-based cutoff for a "lesson"). Without dedicated
|
|
265
|
+
// controls the note set grows without bound and duplicate/stale notes crowd the
|
|
266
|
+
// bounded tail that rides into the coordinator prompt. These three constants back
|
|
267
|
+
// the three growth controls: dedupe-on-record, tombstone/forget, keep-latest-N.
|
|
268
|
+
|
|
269
|
+
// Kind marking an operating note as retracted. A tombstone is itself an
|
|
270
|
+
// append-only ledger entry (history is never destroyed); readers filter out the
|
|
271
|
+
// notes it targets. payload: { targetNoteId?, targetFingerprint?, reason? }
|
|
272
|
+
export const OPERATING_NOTE_KIND: MeshLedgerKind = 'coordinator_operating_note';
|
|
273
|
+
export const OPERATING_NOTE_TOMBSTONE_KIND: MeshLedgerKind = 'coordinator_operating_note_tombstone';
|
|
274
|
+
|
|
275
|
+
// Dedupe window: when recording a note, if the same trimmed text already appears
|
|
276
|
+
// among the most recent OPERATING_NOTE_DEDUPE_WINDOW notes, the record is a no-op
|
|
277
|
+
// (the existing entry is returned). Keeps the prompt tail from filling with the
|
|
278
|
+
// same lesson recorded 20 times.
|
|
279
|
+
export const OPERATING_NOTE_DEDUPE_WINDOW = 40;
|
|
280
|
+
|
|
281
|
+
// Keep-latest-N: pruneOperatingNotes retains at most this many live (non-tombstoned)
|
|
282
|
+
// operating notes, removing the oldest surplus and any tombstoned notes from the
|
|
283
|
+
// store. The prompt reads a much smaller tail (20), so this bound never trims what
|
|
284
|
+
// a coordinator actually sees while still capping unbounded store growth.
|
|
285
|
+
export const OPERATING_NOTE_KEEP_LATEST = 100;
|
|
286
|
+
|
|
168
287
|
// ─── Path Helpers ───────────────────────────────
|
|
169
288
|
|
|
170
289
|
export function getLedgerDir(): string {
|
|
@@ -186,8 +305,294 @@ function getRotatedPath(meshId: string, index: number): string {
|
|
|
186
305
|
return join(getLedgerDir(), `${safe}.${index}.jsonl`);
|
|
187
306
|
}
|
|
188
307
|
|
|
308
|
+
function getArchivePath(meshId: string): string {
|
|
309
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
310
|
+
return join(getLedgerDir(), `${safe}.archive.jsonl`);
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function getRotatedArchivePath(meshId: string, index: number): string {
|
|
314
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
315
|
+
return join(getLedgerDir(), `${safe}.archive.${index}.jsonl`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
function getArchivedCountsPath(meshId: string): string {
|
|
319
|
+
const safe = meshId.replace(/[^a-zA-Z0-9_-]/g, '_');
|
|
320
|
+
return join(getLedgerDir(), `${safe}.archived-counts.json`);
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
function rotateArchiveFile(meshId: string, archivePath: string): void {
|
|
324
|
+
let index = 1;
|
|
325
|
+
while (existsSync(getRotatedArchivePath(meshId, index))) {
|
|
326
|
+
index++;
|
|
327
|
+
if (index > 5) break;
|
|
328
|
+
}
|
|
329
|
+
if (index > 5) index = 5;
|
|
330
|
+
try {
|
|
331
|
+
renameSync(archivePath, getRotatedArchivePath(meshId, index));
|
|
332
|
+
} catch (e: any) {
|
|
333
|
+
process.stderr.write(`[adhdev-mesh] Archive rotation failed for mesh ${meshId}: ${e?.message || e}\n`);
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
interface LedgerArchivedCounts {
|
|
338
|
+
taskCompleted: number;
|
|
339
|
+
taskFailed: number;
|
|
340
|
+
taskStalled: number;
|
|
341
|
+
recoveryAttempted: number;
|
|
342
|
+
totalArchived: number;
|
|
343
|
+
lastArchivedAt: string;
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
function readArchivedCounts(meshId: string): LedgerArchivedCounts {
|
|
347
|
+
const path = getArchivedCountsPath(meshId);
|
|
348
|
+
if (!existsSync(path)) return { taskCompleted: 0, taskFailed: 0, taskStalled: 0, recoveryAttempted: 0, totalArchived: 0, lastArchivedAt: '' };
|
|
349
|
+
try { return JSON.parse(readFileSync(path, 'utf-8')) as LedgerArchivedCounts; } catch { return { taskCompleted: 0, taskFailed: 0, taskStalled: 0, recoveryAttempted: 0, totalArchived: 0, lastArchivedAt: '' }; }
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function updateArchivedCounts(meshId: string, archived: MeshLedgerEntry[]): void {
|
|
353
|
+
const counts = readArchivedCounts(meshId);
|
|
354
|
+
for (const e of archived) {
|
|
355
|
+
if (e.kind === 'task_completed') counts.taskCompleted++;
|
|
356
|
+
else if (e.kind === 'task_failed') counts.taskFailed++;
|
|
357
|
+
else if (e.kind === 'task_stalled') counts.taskStalled++;
|
|
358
|
+
else if (e.kind === 'recovery_attempted') counts.recoveryAttempted++;
|
|
359
|
+
}
|
|
360
|
+
counts.totalArchived += archived.length;
|
|
361
|
+
counts.lastArchivedAt = new Date().toISOString();
|
|
362
|
+
try { writeFileSync(getArchivedCountsPath(meshId), JSON.stringify(counts), { encoding: 'utf-8', mode: 0o600 }); } catch { /* best-effort */ }
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
// ─── Worker Result Footer ───────────────────────
|
|
366
|
+
|
|
367
|
+
/**
|
|
368
|
+
* Footer to append to worker task messages so workers output structured results
|
|
369
|
+
* that the daemon parses via extractJsonObjectFromSummary / normalizeMeshWorkerResult.
|
|
370
|
+
*
|
|
371
|
+
* Usage: append buildWorkerTaskFooter() to the task message in mesh_send_task /
|
|
372
|
+
* mesh_enqueue_task. The coordinator prompt rules instruct coordinators to do this.
|
|
373
|
+
*/
|
|
374
|
+
export function buildWorkerTaskFooter(): string {
|
|
375
|
+
return `
|
|
376
|
+
|
|
377
|
+
---
|
|
378
|
+
When your task is done, end your final response with a JSON code block in this exact format (omit fields that don't apply):
|
|
379
|
+
\`\`\`json
|
|
380
|
+
{
|
|
381
|
+
"status": "completed",
|
|
382
|
+
"changedFiles": ["src/foo.ts", "tests/foo.test.ts"],
|
|
383
|
+
"gitStatus": { "branch": "feat/your-branch", "committed": true, "pushed": false },
|
|
384
|
+
"validationResults": [{ "command": "npm test", "status": "passed" }],
|
|
385
|
+
"errors": [],
|
|
386
|
+
"nextAction": "optional guidance for the coordinator"
|
|
387
|
+
}
|
|
388
|
+
\`\`\`
|
|
389
|
+
Valid status values: \`completed\` | \`failed\` | \`blocked\` | \`partial\`.`;
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
// ─── Ledger Compaction ──────────────────────────
|
|
393
|
+
|
|
394
|
+
/**
|
|
395
|
+
* Compact the active ledger file for a mesh by moving old terminal entries
|
|
396
|
+
* (task_completed, task_failed, task_stalled, recovery_attempted older than 7 days)
|
|
397
|
+
* to <meshId>.archive.jsonl, keeping the active file lean.
|
|
398
|
+
*
|
|
399
|
+
* Non-terminal entries (dispatch, sessions, node lifecycle) are always retained.
|
|
400
|
+
* Called automatically from appendLedgerEntry when the file exceeds COMPACT_THRESHOLD_BYTES.
|
|
401
|
+
*/
|
|
402
|
+
export function compactLedger(meshId: string): { archivedCount: number; retainedCount: number } {
|
|
403
|
+
const filePath = getLedgerPath(meshId);
|
|
404
|
+
if (!existsSync(filePath)) return { archivedCount: 0, retainedCount: 0 };
|
|
405
|
+
|
|
406
|
+
const cutoff = Date.now() - ARCHIVE_TERMINAL_OLDER_THAN_MS;
|
|
407
|
+
const entries = readLedgerEntries(meshId);
|
|
408
|
+
|
|
409
|
+
const keep: MeshLedgerEntry[] = [];
|
|
410
|
+
const archive: MeshLedgerEntry[] = [];
|
|
411
|
+
for (const entry of entries) {
|
|
412
|
+
if (ARCHIVABLE_KINDS.has(entry.kind) && new Date(entry.timestamp).getTime() < cutoff) {
|
|
413
|
+
archive.push(entry);
|
|
414
|
+
} else {
|
|
415
|
+
keep.push(entry);
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
if (archive.length === 0) return { archivedCount: 0, retainedCount: keep.length };
|
|
420
|
+
|
|
421
|
+
// Append archived entries to the archive file, rotate if it exceeds 50MB
|
|
422
|
+
const archivePath = getArchivePath(meshId);
|
|
423
|
+
try {
|
|
424
|
+
if (existsSync(archivePath) && statSync(archivePath).size > 50 * 1024 * 1024) {
|
|
425
|
+
rotateArchiveFile(meshId, archivePath);
|
|
426
|
+
}
|
|
427
|
+
const archiveLines = archive.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
428
|
+
appendFileSync(archivePath, archiveLines, { encoding: 'utf-8', mode: 0o600 });
|
|
429
|
+
updateArchivedCounts(meshId, archive);
|
|
430
|
+
} catch (e: any) {
|
|
431
|
+
process.stderr.write(`[adhdev-mesh] Ledger archive write failed for mesh ${meshId}: ${e?.message || e}\n`);
|
|
432
|
+
return { archivedCount: 0, retainedCount: entries.length };
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
// Rewrite active file with retained entries only
|
|
436
|
+
try {
|
|
437
|
+
const keepLines = keep.length ? keep.map(e => JSON.stringify(e)).join('\n') + '\n' : '';
|
|
438
|
+
writeFileSync(filePath, keepLines, { encoding: 'utf-8', mode: 0o600 });
|
|
439
|
+
invalidateLedgerCache(meshId);
|
|
440
|
+
} catch (e: any) {
|
|
441
|
+
process.stderr.write(`[adhdev-mesh] Ledger compaction rewrite failed for mesh ${meshId}: ${e?.message || e}\n`);
|
|
442
|
+
return { archivedCount: archive.length, retainedCount: keep.length };
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
// G2: mirror the compaction in SQLite so the runtime store matches the active
|
|
446
|
+
// ledger set. Archived entries live on in the JSONL archive files (export/debug).
|
|
447
|
+
try {
|
|
448
|
+
MeshRuntimeStore.getInstance().deleteLedgerEntries(meshId, archive.map(e => e.id));
|
|
449
|
+
invalidateLedgerCache(meshId);
|
|
450
|
+
} catch { /* best-effort; summary counts absorb the difference via archived counts */ }
|
|
451
|
+
|
|
452
|
+
return { archivedCount: archive.length, retainedCount: keep.length };
|
|
453
|
+
}
|
|
454
|
+
|
|
189
455
|
// ─── Core API ───────────────────────────────────
|
|
190
456
|
|
|
457
|
+
function readNonEmptyString(value: unknown): string | undefined {
|
|
458
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
function readStringArray(value: unknown): string[] {
|
|
462
|
+
if (!Array.isArray(value)) return [];
|
|
463
|
+
return value.map(item => readNonEmptyString(item)).filter(Boolean) as string[];
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function extractJsonObjectFromSummary(summary?: string): Record<string, unknown> | undefined {
|
|
467
|
+
const text = readNonEmptyString(summary);
|
|
468
|
+
if (!text) return undefined;
|
|
469
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
470
|
+
const candidates = [fenced?.[1], text].filter(Boolean) as string[];
|
|
471
|
+
for (const candidate of candidates) {
|
|
472
|
+
const trimmed = candidate.trim();
|
|
473
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
|
|
474
|
+
try {
|
|
475
|
+
const parsed = JSON.parse(trimmed);
|
|
476
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
477
|
+
// Require at least one mesh worker result field to avoid false positives
|
|
478
|
+
// (e.g. JSON from tool call outputs or log lines in the final summary).
|
|
479
|
+
const hasWorkerShape = 'status' in parsed && (
|
|
480
|
+
'changedFiles' in parsed || 'errors' in parsed
|
|
481
|
+
|| 'gitStatus' in parsed || 'nextAction' in parsed
|
|
482
|
+
|| 'validationResults' in parsed
|
|
483
|
+
);
|
|
484
|
+
if (hasWorkerShape) return parsed;
|
|
485
|
+
}
|
|
486
|
+
} catch { /* try next candidate */ }
|
|
487
|
+
}
|
|
488
|
+
return undefined;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
function normalizeValidationResults(value: unknown): MeshValidationResultArtifact[] {
|
|
492
|
+
if (!Array.isArray(value)) return [];
|
|
493
|
+
return value
|
|
494
|
+
.filter(item => item && typeof item === 'object' && !Array.isArray(item))
|
|
495
|
+
.map((item: any) => {
|
|
496
|
+
const status = ['passed', 'failed', 'skipped', 'unknown'].includes(item.status) ? item.status : 'unknown';
|
|
497
|
+
return {
|
|
498
|
+
...(readNonEmptyString(item.command) ? { command: readNonEmptyString(item.command) } : {}),
|
|
499
|
+
status,
|
|
500
|
+
...(Number.isFinite(Number(item.durationMs)) ? { durationMs: Number(item.durationMs) } : {}),
|
|
501
|
+
...(readNonEmptyString(item.outputPath) ? { outputPath: readNonEmptyString(item.outputPath) } : {}),
|
|
502
|
+
...(readNonEmptyString(item.summary) ? { summary: readNonEmptyString(item.summary) } : {}),
|
|
503
|
+
};
|
|
504
|
+
});
|
|
505
|
+
}
|
|
506
|
+
|
|
507
|
+
function normalizeProcessArtifacts(value: unknown): MeshProcessArtifact[] {
|
|
508
|
+
if (!Array.isArray(value)) return [];
|
|
509
|
+
const kinds = new Set(['process', 'log', 'port', 'window', 'session', 'file', 'url', 'other']);
|
|
510
|
+
return value
|
|
511
|
+
.filter(item => item && typeof item === 'object' && !Array.isArray(item))
|
|
512
|
+
.map((item: any) => ({
|
|
513
|
+
kind: kinds.has(item.kind) ? item.kind : 'other',
|
|
514
|
+
...(readNonEmptyString(item.id) ? { id: readNonEmptyString(item.id) } : {}),
|
|
515
|
+
...(readNonEmptyString(item.label) ? { label: readNonEmptyString(item.label) } : {}),
|
|
516
|
+
...(readNonEmptyString(item.locator) ? { locator: readNonEmptyString(item.locator) } : {}),
|
|
517
|
+
...(Number.isFinite(Number(item.pid)) ? { pid: Number(item.pid) } : {}),
|
|
518
|
+
...(Number.isFinite(Number(item.port)) ? { port: Number(item.port) } : {}),
|
|
519
|
+
...(readNonEmptyString(item.url) ? { url: readNonEmptyString(item.url) } : {}),
|
|
520
|
+
...(readNonEmptyString(item.path) ? { path: readNonEmptyString(item.path) } : {}),
|
|
521
|
+
...(readNonEmptyString(item.sessionId) ? { sessionId: readNonEmptyString(item.sessionId) } : {}),
|
|
522
|
+
...(typeof item.keepRunning === 'boolean' ? { keepRunning: item.keepRunning } : {}),
|
|
523
|
+
...(item.metadata && typeof item.metadata === 'object' && !Array.isArray(item.metadata) ? { metadata: item.metadata as Record<string, unknown> } : {}),
|
|
524
|
+
}));
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
export function normalizeMeshWorkerResult(input?: Record<string, unknown>, source: MeshWorkerResultArtifact['source'] = 'explicit_metadata'): MeshWorkerResultArtifact {
|
|
528
|
+
const raw = input && typeof input === 'object' ? input : {};
|
|
529
|
+
const status = ['completed', 'failed', 'blocked', 'partial', 'unknown'].includes(String(raw.status))
|
|
530
|
+
? raw.status as MeshWorkerResultStatus
|
|
531
|
+
: 'unknown';
|
|
532
|
+
const gitStatus = raw.gitStatus && typeof raw.gitStatus === 'object' && !Array.isArray(raw.gitStatus)
|
|
533
|
+
? raw.gitStatus as Record<string, unknown>
|
|
534
|
+
: undefined;
|
|
535
|
+
return {
|
|
536
|
+
status,
|
|
537
|
+
...(readNonEmptyString(raw.classification) ? { classification: readNonEmptyString(raw.classification) } : {}),
|
|
538
|
+
changedFiles: readStringArray(raw.changedFiles),
|
|
539
|
+
validationResults: normalizeValidationResults(raw.validationResults),
|
|
540
|
+
...(gitStatus ? { gitStatus } : {}),
|
|
541
|
+
processArtifacts: normalizeProcessArtifacts(raw.processArtifacts),
|
|
542
|
+
errors: readStringArray(raw.errors),
|
|
543
|
+
...(readNonEmptyString(raw.nextAction) ? { nextAction: readNonEmptyString(raw.nextAction) } : {}),
|
|
544
|
+
requiresUserAction: raw.requiresUserAction === true,
|
|
545
|
+
source,
|
|
546
|
+
};
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* NOTIF Defect-2b: does the summary contain ANY parseable JSON object answer (not just a
|
|
551
|
+
* worker-result-shaped one)? Some providers (and every MAGI replica) emit a complete, valid
|
|
552
|
+
* answer as a JSON envelope that has no `status`/`changedFiles` worker-result fields, so
|
|
553
|
+
* extractJsonObjectFromSummary returns undefined and the completion is mislabelled
|
|
554
|
+
* source='default' → evidenceLevel='insufficient' even though a real answer was produced.
|
|
555
|
+
* This is a conservative existence check: it only returns true when a JSON object actually
|
|
556
|
+
* parses out of the summary (raw or fenced), so an empty / prose-only / unparseable summary
|
|
557
|
+
* still resolves to 'default'.
|
|
558
|
+
*/
|
|
559
|
+
function summaryHasParseableJsonAnswer(summary?: string): boolean {
|
|
560
|
+
const text = readNonEmptyString(summary);
|
|
561
|
+
if (!text) return false;
|
|
562
|
+
const fenced = text.match(/```(?:json)?\s*([\s\S]*?)```/i);
|
|
563
|
+
const candidates = [fenced?.[1], text].filter(Boolean) as string[];
|
|
564
|
+
for (const candidate of candidates) {
|
|
565
|
+
const trimmed = candidate.trim();
|
|
566
|
+
if (!trimmed.startsWith('{') || !trimmed.endsWith('}')) continue;
|
|
567
|
+
try {
|
|
568
|
+
const parsed = JSON.parse(trimmed);
|
|
569
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)
|
|
570
|
+
&& Object.keys(parsed).length > 0) {
|
|
571
|
+
return true;
|
|
572
|
+
}
|
|
573
|
+
} catch { /* try next candidate */ }
|
|
574
|
+
}
|
|
575
|
+
return false;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
function resolveWorkerResult(opts: BuildTaskCompletionEvidenceOptions): MeshWorkerResultArtifact {
|
|
579
|
+
if (opts.workerResult && typeof opts.workerResult === 'object') {
|
|
580
|
+
return normalizeMeshWorkerResult(opts.workerResult, 'explicit_metadata');
|
|
581
|
+
}
|
|
582
|
+
const parsed = extractJsonObjectFromSummary(opts.finalSummary);
|
|
583
|
+
if (parsed) {
|
|
584
|
+
return normalizeMeshWorkerResult(parsed, 'final_summary_json');
|
|
585
|
+
}
|
|
586
|
+
// NOTIF Defect-2b: no worker-result-shaped JSON, but a parseable JSON answer IS present
|
|
587
|
+
// (the common MAGI / answer-only case). Treat it as concrete evidence so the completion is
|
|
588
|
+
// not labelled 'insufficient', while keeping the worker-result fields empty (status stays
|
|
589
|
+
// 'unknown') — we only know an answer parsed, not its task outcome.
|
|
590
|
+
if (summaryHasParseableJsonAnswer(opts.finalSummary)) {
|
|
591
|
+
return normalizeMeshWorkerResult(undefined, 'parseable_answer');
|
|
592
|
+
}
|
|
593
|
+
return normalizeMeshWorkerResult(undefined, 'default');
|
|
594
|
+
}
|
|
595
|
+
|
|
191
596
|
export function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvidenceOptions): MeshTaskCompletionEvidence {
|
|
192
597
|
const providerSessionId = opts.providerSessionId?.trim() || undefined;
|
|
193
598
|
const providerType = opts.providerType?.trim() || undefined;
|
|
@@ -204,6 +609,7 @@ export function buildTaskCompletionEvidence(opts: BuildTaskCompletionEvidenceOpt
|
|
|
204
609
|
providerSessionId,
|
|
205
610
|
finalSummaryAvailable: typeof opts.finalSummary === 'string' && opts.finalSummary.trim().length > 0,
|
|
206
611
|
},
|
|
612
|
+
workerResult: resolveWorkerResult(opts),
|
|
207
613
|
git: {
|
|
208
614
|
status: 'deferred',
|
|
209
615
|
reason: 'ordinary_completion_git_status_not_checked',
|
|
@@ -230,6 +636,23 @@ export function appendLedgerEntry(
|
|
|
230
636
|
meshId: string,
|
|
231
637
|
partial: Omit<MeshLedgerEntry, 'id' | 'meshId' | 'timestamp'>,
|
|
232
638
|
): MeshLedgerEntry {
|
|
639
|
+
// Fix (1) dedupe-on-record: for a coordinator_operating_note, if the same
|
|
640
|
+
// trimmed text already appears among the most recent OPERATING_NOTE_DEDUPE_WINDOW
|
|
641
|
+
// notes, do NOT append a duplicate — return the existing entry so the bounded
|
|
642
|
+
// prompt tail can't be crowded by the same lesson recorded repeatedly. Other
|
|
643
|
+
// kinds (task_completed, …) are untouched.
|
|
644
|
+
if (partial.kind === OPERATING_NOTE_KIND) {
|
|
645
|
+
const text = operatingNoteText(partial.payload);
|
|
646
|
+
if (text) {
|
|
647
|
+
const recentNotes = readLedgerEntries(meshId, {
|
|
648
|
+
kind: [OPERATING_NOTE_KIND],
|
|
649
|
+
tail: OPERATING_NOTE_DEDUPE_WINDOW,
|
|
650
|
+
});
|
|
651
|
+
const existing = recentNotes.find(e => operatingNoteText(e.payload) === text);
|
|
652
|
+
if (existing) return existing;
|
|
653
|
+
}
|
|
654
|
+
}
|
|
655
|
+
|
|
233
656
|
const entry: MeshLedgerEntry = {
|
|
234
657
|
id: randomUUID(),
|
|
235
658
|
meshId,
|
|
@@ -239,28 +662,188 @@ export function appendLedgerEntry(
|
|
|
239
662
|
|
|
240
663
|
const filePath = getLedgerPath(meshId);
|
|
241
664
|
|
|
242
|
-
//
|
|
665
|
+
// Compact or rotate based on file size
|
|
243
666
|
if (existsSync(filePath)) {
|
|
244
667
|
try {
|
|
245
668
|
const stat = statSync(filePath);
|
|
246
669
|
if (stat.size >= MAX_FILE_SIZE_BYTES) {
|
|
247
670
|
rotateLedgerFile(meshId, filePath);
|
|
671
|
+
} else if (stat.size >= COMPACT_THRESHOLD_BYTES) {
|
|
672
|
+
compactLedger(meshId);
|
|
248
673
|
}
|
|
249
674
|
} catch {
|
|
250
675
|
// stat failed — proceed with append anyway
|
|
251
676
|
}
|
|
252
677
|
}
|
|
253
678
|
|
|
679
|
+
// Write to SQLite (G2: primary runtime read/write path)
|
|
680
|
+
try {
|
|
681
|
+
MeshRuntimeStore.getInstance().appendLedgerEntry({
|
|
682
|
+
id: entry.id,
|
|
683
|
+
meshId: entry.meshId,
|
|
684
|
+
timestamp: entry.timestamp,
|
|
685
|
+
kind: entry.kind,
|
|
686
|
+
nodeId: entry.nodeId ?? null,
|
|
687
|
+
sessionId: entry.sessionId ?? null,
|
|
688
|
+
providerType: entry.providerType ?? null,
|
|
689
|
+
payload: entry.payload,
|
|
690
|
+
});
|
|
691
|
+
} catch {
|
|
692
|
+
// SQLite write failed but the JSONL append below still records the entry.
|
|
693
|
+
// Reset the one-time import flag so the next read re-imports from JSONL
|
|
694
|
+
// and the store self-heals instead of silently missing this entry.
|
|
695
|
+
ledgerImportDone.delete(meshId);
|
|
696
|
+
}
|
|
697
|
+
|
|
698
|
+
// Also write to JSONL (retained as export/import/debug/legacy artifact)
|
|
254
699
|
try {
|
|
255
700
|
const line = JSON.stringify(entry) + '\n';
|
|
256
701
|
appendFileSync(filePath, line, { encoding: 'utf-8', mode: 0o600 });
|
|
702
|
+
invalidateLedgerCache(meshId);
|
|
257
703
|
meshLedgerEvents.emit('append', meshId, entry);
|
|
704
|
+
// Fix (3) keep-latest-N: operating notes are never archived by compactLedger,
|
|
705
|
+
// so cap their store footprint here. Runs only when a note (or its tombstone)
|
|
706
|
+
// is recorded, and is a no-op until the live-note count exceeds the bound.
|
|
707
|
+
if (entry.kind === OPERATING_NOTE_KIND || entry.kind === OPERATING_NOTE_TOMBSTONE_KIND) {
|
|
708
|
+
try { pruneOperatingNotes(meshId); } catch { /* prune is best-effort */ }
|
|
709
|
+
}
|
|
258
710
|
return entry;
|
|
259
711
|
} catch (e: any) {
|
|
260
712
|
throw new Error(`Failed to append to ledger for mesh ${meshId}: ${e.message}`);
|
|
261
713
|
}
|
|
262
714
|
}
|
|
263
715
|
|
|
716
|
+
// ─── Operating-note growth controls ─────────────
|
|
717
|
+
|
|
718
|
+
/** Extract the trimmed note text from a coordinator_operating_note payload. */
|
|
719
|
+
function operatingNoteText(payload: Record<string, unknown> | undefined): string | undefined {
|
|
720
|
+
const text = payload && typeof payload.text === 'string' ? payload.text.trim() : '';
|
|
721
|
+
return text || undefined;
|
|
722
|
+
}
|
|
723
|
+
|
|
724
|
+
/**
|
|
725
|
+
* Set of trimmed-text fingerprints and note ids retracted by tombstone entries in
|
|
726
|
+
* the given entry set. A tombstone targets by note id and/or by text fingerprint.
|
|
727
|
+
*/
|
|
728
|
+
function collectOperatingNoteTombstones(entries: MeshLedgerEntry[]): { ids: Set<string>; fingerprints: Set<string> } {
|
|
729
|
+
const ids = new Set<string>();
|
|
730
|
+
const fingerprints = new Set<string>();
|
|
731
|
+
for (const e of entries) {
|
|
732
|
+
if (e.kind !== OPERATING_NOTE_TOMBSTONE_KIND) continue;
|
|
733
|
+
const p = e.payload || {};
|
|
734
|
+
const targetId = typeof p.targetNoteId === 'string' ? p.targetNoteId.trim() : '';
|
|
735
|
+
const targetFp = typeof p.targetFingerprint === 'string' ? p.targetFingerprint.trim() : '';
|
|
736
|
+
if (targetId) ids.add(targetId);
|
|
737
|
+
if (targetFp) fingerprints.add(targetFp);
|
|
738
|
+
}
|
|
739
|
+
return { ids, fingerprints };
|
|
740
|
+
}
|
|
741
|
+
|
|
742
|
+
/** True if the operating note is retracted by any tombstone in `tombstones`. */
|
|
743
|
+
export function isOperatingNoteTombstoned(
|
|
744
|
+
entry: Pick<MeshLedgerEntry, 'id' | 'payload'>,
|
|
745
|
+
tombstones: { ids: Set<string>; fingerprints: Set<string> },
|
|
746
|
+
): boolean {
|
|
747
|
+
if (tombstones.ids.has(entry.id)) return true;
|
|
748
|
+
const text = operatingNoteText(entry.payload);
|
|
749
|
+
return text ? tombstones.fingerprints.has(text) : false;
|
|
750
|
+
}
|
|
751
|
+
|
|
752
|
+
/**
|
|
753
|
+
* Fix (2) supersede/remove: append a tombstone that retracts a coordinator
|
|
754
|
+
* operating note. Targets by note id and/or by exact trimmed text (a text target
|
|
755
|
+
* retracts every note with that text). History is preserved — the notes stay in
|
|
756
|
+
* the ledger but readers filter them out. Returns how many currently-live notes
|
|
757
|
+
* the tombstone will hide.
|
|
758
|
+
*/
|
|
759
|
+
export function tombstoneOperatingNote(
|
|
760
|
+
meshId: string,
|
|
761
|
+
target: { noteId?: string; text?: string; reason?: string },
|
|
762
|
+
): { tombstone: MeshLedgerEntry; matched: number } {
|
|
763
|
+
const noteId = typeof target.noteId === 'string' ? target.noteId.trim() : '';
|
|
764
|
+
const fingerprint = typeof target.text === 'string' ? target.text.trim() : '';
|
|
765
|
+
if (!noteId && !fingerprint) {
|
|
766
|
+
throw new Error('tombstoneOperatingNote requires a noteId or text target');
|
|
767
|
+
}
|
|
768
|
+
|
|
769
|
+
// Count currently-live matches (not already tombstoned) for the caller's report.
|
|
770
|
+
const notes = readOperatingNotes(meshId);
|
|
771
|
+
const matched = notes.filter(n =>
|
|
772
|
+
(noteId && n.id === noteId) || (fingerprint && operatingNoteText(n.payload) === fingerprint),
|
|
773
|
+
).length;
|
|
774
|
+
|
|
775
|
+
const tombstone = appendLedgerEntry(meshId, {
|
|
776
|
+
kind: OPERATING_NOTE_TOMBSTONE_KIND,
|
|
777
|
+
payload: {
|
|
778
|
+
...(noteId ? { targetNoteId: noteId } : {}),
|
|
779
|
+
...(fingerprint ? { targetFingerprint: fingerprint } : {}),
|
|
780
|
+
...(target.reason && target.reason.trim() ? { reason: target.reason.trim() } : {}),
|
|
781
|
+
forgottenAt: new Date().toISOString(),
|
|
782
|
+
},
|
|
783
|
+
});
|
|
784
|
+
return { tombstone, matched };
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
/**
|
|
788
|
+
* Read live operating notes (tombstoned notes filtered out), oldest→newest.
|
|
789
|
+
* `tail` bounds the number of live notes returned (the freshest N).
|
|
790
|
+
*/
|
|
791
|
+
export function readOperatingNotes(meshId: string, opts?: { tail?: number }): MeshLedgerEntry[] {
|
|
792
|
+
const raw = getCachedRawEntries(meshId);
|
|
793
|
+
const tombstones = collectOperatingNoteTombstones(raw);
|
|
794
|
+
let notes = raw.filter(e => e.kind === OPERATING_NOTE_KIND && !isOperatingNoteTombstoned(e, tombstones));
|
|
795
|
+
if (opts?.tail && opts.tail > 0 && notes.length > opts.tail) {
|
|
796
|
+
notes = notes.slice(-opts.tail);
|
|
797
|
+
}
|
|
798
|
+
return notes;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
/**
|
|
802
|
+
* Fix (3) keep-latest-N prune for coordinator_operating_note. Removes, from the
|
|
803
|
+
* store, (a) every note retracted by a tombstone, and (b) the oldest live notes
|
|
804
|
+
* beyond `keepLatest`. Tombstone entries themselves are retained as an audit trail
|
|
805
|
+
* of what was forgotten. Returns the number of note entries removed.
|
|
806
|
+
*/
|
|
807
|
+
export function pruneOperatingNotes(meshId: string, keepLatest: number = OPERATING_NOTE_KEEP_LATEST): number {
|
|
808
|
+
const raw = getCachedRawEntries(meshId);
|
|
809
|
+
const tombstones = collectOperatingNoteTombstones(raw);
|
|
810
|
+
|
|
811
|
+
const removeIds: string[] = [];
|
|
812
|
+
const liveNotes: MeshLedgerEntry[] = [];
|
|
813
|
+
for (const e of raw) {
|
|
814
|
+
if (e.kind !== OPERATING_NOTE_KIND) continue;
|
|
815
|
+
if (isOperatingNoteTombstoned(e, tombstones)) {
|
|
816
|
+
removeIds.push(e.id); // tombstoned notes are pruned first
|
|
817
|
+
} else {
|
|
818
|
+
liveNotes.push(e);
|
|
819
|
+
}
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// Drop the oldest live notes beyond keepLatest; the freshest (prompt tail) survive.
|
|
823
|
+
const bound = Math.max(0, Math.floor(keepLatest));
|
|
824
|
+
if (liveNotes.length > bound) {
|
|
825
|
+
for (const e of liveNotes.slice(0, liveNotes.length - bound)) removeIds.push(e.id);
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
if (removeIds.length === 0) return 0;
|
|
829
|
+
|
|
830
|
+
try {
|
|
831
|
+
MeshRuntimeStore.getInstance().deleteLedgerEntries(meshId, removeIds);
|
|
832
|
+
} catch { /* store unavailable — JSONL rewrite below still trims */ }
|
|
833
|
+
|
|
834
|
+
// Rewrite the JSONL mirror without the pruned note entries so the export
|
|
835
|
+
// artifact stays consistent with the store.
|
|
836
|
+
try {
|
|
837
|
+
const remaining = readLedgerFile(meshId).filter(e => !removeIds.includes(e.id));
|
|
838
|
+
const filePath = getLedgerPath(meshId);
|
|
839
|
+
const lines = remaining.length ? remaining.map(e => JSON.stringify(e)).join('\n') + '\n' : '';
|
|
840
|
+
writeFileSync(filePath, lines, { encoding: 'utf-8', mode: 0o600 });
|
|
841
|
+
} catch { /* JSONL rewrite best-effort; store is the primary read path */ }
|
|
842
|
+
|
|
843
|
+
invalidateLedgerCache(meshId);
|
|
844
|
+
return removeIds.length;
|
|
845
|
+
}
|
|
846
|
+
|
|
264
847
|
function clampLedgerSliceLimit(limit: unknown): number {
|
|
265
848
|
if (typeof limit !== 'number' || !Number.isFinite(limit)) return DEFAULT_LEDGER_SLICE_LIMIT;
|
|
266
849
|
return Math.max(1, Math.min(MAX_LEDGER_SLICE_LIMIT, Math.floor(limit)));
|
|
@@ -285,8 +868,9 @@ export function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEnt
|
|
|
285
868
|
if (entries.length === 0) return { accepted: 0, skippedDuplicate: 0, rejectedInvalid: 0, entries: [] };
|
|
286
869
|
const ledgerPath = getLedgerPath(meshId);
|
|
287
870
|
|
|
288
|
-
//
|
|
289
|
-
|
|
871
|
+
// Dedup against recent entries only — P2P replication is incremental (cursor-based),
|
|
872
|
+
// so duplicates appear in the recent tail, not deep history.
|
|
873
|
+
const existing = new Set(readLedgerEntries(meshId, { tail: 1000 }).map(e => e.id));
|
|
290
874
|
const validEntries: MeshLedgerEntry[] = [];
|
|
291
875
|
let rejectedInvalid = 0;
|
|
292
876
|
let skippedDuplicate = 0;
|
|
@@ -307,9 +891,24 @@ export function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEnt
|
|
|
307
891
|
return { accepted: 0, skippedDuplicate, rejectedInvalid, entries: [] };
|
|
308
892
|
}
|
|
309
893
|
|
|
894
|
+
// G2: write to SQLite (primary runtime store); INSERT OR IGNORE dedups by id.
|
|
895
|
+
try {
|
|
896
|
+
MeshRuntimeStore.getInstance().importLedgerEntries(validEntries.map(e => ({
|
|
897
|
+
id: e.id,
|
|
898
|
+
meshId: e.meshId,
|
|
899
|
+
timestamp: e.timestamp,
|
|
900
|
+
kind: e.kind,
|
|
901
|
+
nodeId: e.nodeId ?? null,
|
|
902
|
+
sessionId: e.sessionId ?? null,
|
|
903
|
+
providerType: e.providerType ?? null,
|
|
904
|
+
payload: e.payload ?? {},
|
|
905
|
+
})));
|
|
906
|
+
} catch { /* best-effort; JSONL append below still records the entries */ }
|
|
907
|
+
|
|
310
908
|
try {
|
|
311
909
|
const lines = validEntries.map(e => JSON.stringify(e)).join('\n') + '\n';
|
|
312
910
|
appendFileSync(ledgerPath, lines, { encoding: 'utf-8', mode: 0o600 });
|
|
911
|
+
invalidateLedgerCache(meshId);
|
|
313
912
|
for (const entry of validEntries) {
|
|
314
913
|
meshLedgerEvents.emit('append', meshId, entry);
|
|
315
914
|
}
|
|
@@ -319,103 +918,151 @@ export function appendRemoteLedgerEntries(meshId: string, entries: MeshLedgerEnt
|
|
|
319
918
|
}
|
|
320
919
|
}
|
|
321
920
|
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
921
|
+
// ─── Ledger Read Cache ─────────────────────────
|
|
922
|
+
// Absorbs repeated reads within a single event-processing burst (e.g. agent:stopped
|
|
923
|
+
// triggers shouldSuppressIntentionalCleanupStop, findRecentTerminalLedgerEvidence,
|
|
924
|
+
// hasDispatchAfterTerminal, and getSessionRecoveryContext — all reading the same store).
|
|
925
|
+
// TTL is 100ms: short enough to stay current, long enough to cover one event cycle.
|
|
926
|
+
// Cache is invalidated on every write (append, remote import, compaction).
|
|
927
|
+
|
|
928
|
+
const ledgerReadCache = new Map<string, { entries: MeshLedgerEntry[]; cachedAt: number }>();
|
|
929
|
+
const LEDGER_CACHE_TTL_MS = 100;
|
|
930
|
+
|
|
931
|
+
function readLedgerFile(meshId: string): MeshLedgerEntry[] {
|
|
326
932
|
const filePath = getLedgerPath(meshId);
|
|
327
933
|
if (!existsSync(filePath)) return [];
|
|
328
|
-
|
|
329
934
|
let content: string;
|
|
935
|
+
try { content = readFileSync(filePath, 'utf-8'); } catch { return []; }
|
|
936
|
+
const entries: MeshLedgerEntry[] = [];
|
|
937
|
+
for (const line of content.split('\n')) {
|
|
938
|
+
if (!line.trim()) continue;
|
|
939
|
+
try {
|
|
940
|
+
const entry = JSON.parse(line) as MeshLedgerEntry;
|
|
941
|
+
if (entry.id && entry.kind) entries.push(entry);
|
|
942
|
+
} catch { /* skip malformed lines */ }
|
|
943
|
+
}
|
|
944
|
+
return entries;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
// ─── G2: One-Time JSONL → SQLite Import ─────────
|
|
948
|
+
// On the first SQLite read for a mesh (per store instance), import any legacy
|
|
949
|
+
// JSONL entries into mesh_event_ledger. INSERT OR IGNORE makes this idempotent:
|
|
950
|
+
// dual-written entries are skipped, only pre-cutover legacy entries are added.
|
|
951
|
+
// Keyed by the store instance so MeshRuntimeStore.resetForTests() (fresh DB)
|
|
952
|
+
// naturally re-imports.
|
|
953
|
+
|
|
954
|
+
let ledgerImportStoreRef: MeshRuntimeStore | undefined;
|
|
955
|
+
const ledgerImportDone = new Set<string>();
|
|
956
|
+
|
|
957
|
+
function ensureLedgerImported(store: MeshRuntimeStore, meshId: string): void {
|
|
958
|
+
if (ledgerImportStoreRef !== store) {
|
|
959
|
+
ledgerImportDone.clear();
|
|
960
|
+
ledgerImportStoreRef = store;
|
|
961
|
+
}
|
|
962
|
+
if (ledgerImportDone.has(meshId)) return;
|
|
963
|
+
ledgerImportDone.add(meshId);
|
|
964
|
+
const fileEntries = readLedgerFile(meshId);
|
|
965
|
+
if (fileEntries.length === 0) return;
|
|
966
|
+
try {
|
|
967
|
+
store.importLedgerEntries(fileEntries.map(e => ({
|
|
968
|
+
id: e.id,
|
|
969
|
+
meshId: e.meshId,
|
|
970
|
+
timestamp: e.timestamp,
|
|
971
|
+
kind: e.kind,
|
|
972
|
+
nodeId: e.nodeId ?? null,
|
|
973
|
+
sessionId: e.sessionId ?? null,
|
|
974
|
+
providerType: e.providerType ?? null,
|
|
975
|
+
payload: e.payload ?? {},
|
|
976
|
+
})));
|
|
977
|
+
} catch { /* import is best-effort; reads fall back to JSONL on store failure */ }
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
function readLedgerFromStore(meshId: string): MeshLedgerEntry[] {
|
|
981
|
+
const store = MeshRuntimeStore.getInstance();
|
|
982
|
+
ensureLedgerImported(store, meshId);
|
|
983
|
+
return store.readLedgerEntriesOrdered(meshId).map(r => ({
|
|
984
|
+
id: r.id,
|
|
985
|
+
meshId: r.meshId,
|
|
986
|
+
timestamp: r.timestamp,
|
|
987
|
+
kind: r.kind as MeshLedgerKind,
|
|
988
|
+
...(r.nodeId ? { nodeId: r.nodeId } : {}),
|
|
989
|
+
...(r.sessionId ? { sessionId: r.sessionId } : {}),
|
|
990
|
+
...(r.providerType ? { providerType: r.providerType } : {}),
|
|
991
|
+
payload: (r.payload && typeof r.payload === 'object' ? r.payload : {}) as Record<string, unknown>,
|
|
992
|
+
}));
|
|
993
|
+
}
|
|
994
|
+
|
|
995
|
+
function getCachedRawEntries(meshId: string): MeshLedgerEntry[] {
|
|
996
|
+
const now = Date.now();
|
|
997
|
+
const cached = ledgerReadCache.get(meshId);
|
|
998
|
+
if (cached && now - cached.cachedAt < LEDGER_CACHE_TTL_MS) return cached.entries;
|
|
999
|
+
let entries: MeshLedgerEntry[];
|
|
330
1000
|
try {
|
|
331
|
-
|
|
1001
|
+
// G2: SQLite mesh_event_ledger is the primary runtime read path.
|
|
1002
|
+
entries = readLedgerFromStore(meshId);
|
|
332
1003
|
} catch {
|
|
333
|
-
|
|
1004
|
+
// Store unavailable — fall back to the JSONL export artifact.
|
|
1005
|
+
entries = readLedgerFile(meshId);
|
|
334
1006
|
}
|
|
1007
|
+
ledgerReadCache.set(meshId, { entries, cachedAt: now });
|
|
1008
|
+
return entries;
|
|
1009
|
+
}
|
|
335
1010
|
|
|
336
|
-
|
|
337
|
-
|
|
1011
|
+
function invalidateLedgerCache(meshId: string): void {
|
|
1012
|
+
ledgerReadCache.delete(meshId);
|
|
1013
|
+
}
|
|
338
1014
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
1015
|
+
/**
|
|
1016
|
+
* Test helper: clear all runtime ledger state for a mesh — SQLite rows, read
|
|
1017
|
+
* cache, and the one-time import flag. JSONL files are the caller's concern.
|
|
1018
|
+
*/
|
|
1019
|
+
export function __clearMeshLedgerForTests(meshId: string): void {
|
|
1020
|
+
try {
|
|
1021
|
+
MeshRuntimeStore.getInstance().clearLedgerForMesh(meshId);
|
|
1022
|
+
} catch { /* store unavailable — nothing to clear */ }
|
|
1023
|
+
ledgerReadCache.delete(meshId);
|
|
1024
|
+
ledgerImportDone.delete(meshId);
|
|
1025
|
+
}
|
|
1026
|
+
|
|
1027
|
+
/**
|
|
1028
|
+
* Read ledger entries with optional filtering.
|
|
1029
|
+
* G2: SQLite (mesh_event_ledger) is the primary read path; legacy JSONL is
|
|
1030
|
+
* imported once per store instance and otherwise retained as an
|
|
1031
|
+
* export/import/debug artifact only.
|
|
1032
|
+
*/
|
|
1033
|
+
export function readLedgerEntries(meshId: string, opts?: ReadLedgerOptions): MeshLedgerEntry[] {
|
|
1034
|
+
let entries = getCachedRawEntries(meshId);
|
|
348
1035
|
|
|
349
|
-
// Apply filters
|
|
350
1036
|
if (opts?.since) {
|
|
351
1037
|
const sinceDate = new Date(opts.since).getTime();
|
|
352
|
-
if (!isNaN(sinceDate))
|
|
353
|
-
entries = entries.filter(e => new Date(e.timestamp).getTime() >= sinceDate);
|
|
354
|
-
}
|
|
1038
|
+
if (!isNaN(sinceDate)) entries = entries.filter(e => new Date(e.timestamp).getTime() >= sinceDate);
|
|
355
1039
|
}
|
|
356
|
-
|
|
357
1040
|
if (opts?.kind?.length) {
|
|
358
1041
|
const kindSet = new Set(opts.kind);
|
|
359
1042
|
entries = entries.filter(e => kindSet.has(e.kind));
|
|
360
1043
|
}
|
|
361
|
-
|
|
362
|
-
// Apply tail (return last N entries)
|
|
363
1044
|
if (opts?.tail && opts.tail > 0 && entries.length > opts.tail) {
|
|
364
1045
|
entries = entries.slice(-opts.tail);
|
|
365
1046
|
}
|
|
366
|
-
|
|
367
1047
|
return entries;
|
|
368
1048
|
}
|
|
369
1049
|
|
|
370
1050
|
/**
|
|
371
|
-
*
|
|
372
|
-
*
|
|
373
|
-
* remote daemons on demand without Cloud/D1 becoming a ledger data-plane.
|
|
374
|
-
*/
|
|
375
|
-
export function readLedgerSlice(meshId: string, opts?: ReadLedgerSliceOptions): MeshLedgerSlice {
|
|
376
|
-
const limit = clampLedgerSliceLimit(opts?.limit);
|
|
377
|
-
let entries = readLedgerEntries(meshId, { since: opts?.since, kind: opts?.kind });
|
|
378
|
-
const afterId = typeof opts?.afterId === 'string' && opts.afterId.trim() ? opts.afterId.trim() : null;
|
|
379
|
-
if (afterId) {
|
|
380
|
-
const index = entries.findIndex(entry => entry.id === afterId);
|
|
381
|
-
entries = index >= 0 ? entries.slice(index + 1) : entries;
|
|
382
|
-
}
|
|
383
|
-
const bounded = entries.slice(0, limit);
|
|
384
|
-
return {
|
|
385
|
-
protocol: 'adhdev.mesh.ledger.slice.v1',
|
|
386
|
-
meshId,
|
|
387
|
-
entries: bounded,
|
|
388
|
-
cursor: {
|
|
389
|
-
afterId,
|
|
390
|
-
nextAfterId: bounded.length ? bounded[bounded.length - 1].id : afterId,
|
|
391
|
-
limit,
|
|
392
|
-
hasMore: entries.length > bounded.length,
|
|
393
|
-
},
|
|
394
|
-
summary: getLedgerSummary(meshId),
|
|
395
|
-
sourceOfTruth: {
|
|
396
|
-
kind: 'local_jsonl',
|
|
397
|
-
path: getLedgerPath(meshId),
|
|
398
|
-
bounded: true,
|
|
399
|
-
maxLimit: MAX_LEDGER_SLICE_LIMIT,
|
|
400
|
-
},
|
|
401
|
-
};
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
/**
|
|
405
|
-
* Get a summary of mesh activity from the ledger.
|
|
1051
|
+
* Build a ledger summary from pre-loaded entries. Used by both getLedgerSummary
|
|
1052
|
+
* and readLedgerSlice so they share a single getCachedRawEntries() call.
|
|
406
1053
|
*/
|
|
407
|
-
|
|
408
|
-
const
|
|
1054
|
+
function buildLedgerSummary(meshId: string, entries: MeshLedgerEntry[]): MeshLedgerSummary {
|
|
1055
|
+
const archived = readArchivedCounts(meshId);
|
|
409
1056
|
const now = Date.now();
|
|
410
1057
|
const recentFailureCutoff = now - RECENT_FAILURE_WINDOW_MS;
|
|
411
1058
|
|
|
412
1059
|
const summary: MeshLedgerSummary = {
|
|
413
1060
|
meshId,
|
|
414
|
-
totalEntries: entries.length,
|
|
1061
|
+
totalEntries: entries.length + archived.totalArchived,
|
|
415
1062
|
taskDispatched: 0,
|
|
416
|
-
taskCompleted:
|
|
417
|
-
taskFailed:
|
|
418
|
-
taskStalled:
|
|
1063
|
+
taskCompleted: archived.taskCompleted,
|
|
1064
|
+
taskFailed: archived.taskFailed,
|
|
1065
|
+
taskStalled: archived.taskStalled,
|
|
419
1066
|
sessionLaunched: 0,
|
|
420
1067
|
checkpointCreated: 0,
|
|
421
1068
|
lastActivityAt: null,
|
|
@@ -450,6 +1097,79 @@ export function getLedgerSummary(meshId: string): MeshLedgerSummary {
|
|
|
450
1097
|
return summary;
|
|
451
1098
|
}
|
|
452
1099
|
|
|
1100
|
+
/**
|
|
1101
|
+
* Read a bounded, cursor-addressable ledger slice for local-first/P2P replication.
|
|
1102
|
+
* The result is intentionally small and self-describing so coordinators can query
|
|
1103
|
+
* remote daemons on demand without Cloud/D1 becoming a ledger data-plane.
|
|
1104
|
+
*/
|
|
1105
|
+
export function readLedgerSlice(meshId: string, opts?: ReadLedgerSliceOptions): MeshLedgerSlice {
|
|
1106
|
+
const limit = clampLedgerSliceLimit(opts?.limit);
|
|
1107
|
+
// Load raw entries once and share between filtering, pagination, and summary.
|
|
1108
|
+
const rawEntries = getCachedRawEntries(meshId);
|
|
1109
|
+
|
|
1110
|
+
let entries: MeshLedgerEntry[] = rawEntries;
|
|
1111
|
+
if (opts?.since) {
|
|
1112
|
+
const sinceDate = new Date(opts.since).getTime();
|
|
1113
|
+
if (!isNaN(sinceDate)) entries = entries.filter(e => new Date(e.timestamp).getTime() >= sinceDate);
|
|
1114
|
+
}
|
|
1115
|
+
if (opts?.kind?.length) {
|
|
1116
|
+
const kindSet = new Set(opts.kind);
|
|
1117
|
+
entries = entries.filter(e => kindSet.has(e.kind));
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
const afterId = typeof opts?.afterId === 'string' && opts.afterId.trim() ? opts.afterId.trim() : null;
|
|
1121
|
+
if (afterId) {
|
|
1122
|
+
const index = entries.findIndex(entry => entry.id === afterId);
|
|
1123
|
+
entries = index >= 0 ? entries.slice(index + 1) : entries;
|
|
1124
|
+
}
|
|
1125
|
+
const bounded = entries.slice(0, limit);
|
|
1126
|
+
return {
|
|
1127
|
+
protocol: 'adhdev.mesh.ledger.slice.v1',
|
|
1128
|
+
meshId,
|
|
1129
|
+
entries: bounded,
|
|
1130
|
+
cursor: {
|
|
1131
|
+
afterId,
|
|
1132
|
+
nextAfterId: bounded.length ? bounded[bounded.length - 1].id : afterId,
|
|
1133
|
+
limit,
|
|
1134
|
+
hasMore: entries.length > bounded.length,
|
|
1135
|
+
},
|
|
1136
|
+
summary: buildLedgerSummary(meshId, rawEntries),
|
|
1137
|
+
sourceOfTruth: {
|
|
1138
|
+
kind: 'local_jsonl',
|
|
1139
|
+
path: getLedgerPath(meshId),
|
|
1140
|
+
bounded: true,
|
|
1141
|
+
maxLimit: MAX_LEDGER_SLICE_LIMIT,
|
|
1142
|
+
},
|
|
1143
|
+
};
|
|
1144
|
+
}
|
|
1145
|
+
|
|
1146
|
+
/**
|
|
1147
|
+
* G4: Read a bounded ledger slice from the SQLite mesh_event_ledger table.
|
|
1148
|
+
* This is the preferred P2P reconcile read path; JSONL files are retained as
|
|
1149
|
+
* export/import/debug/legacy artifacts only.
|
|
1150
|
+
*
|
|
1151
|
+
* Returns a shape structurally compatible with MeshLedgerSlice (minus the
|
|
1152
|
+
* JSONL-specific `summary` and `sourceOfTruth.path` fields) so callers can
|
|
1153
|
+
* pass it to buildMeshLedgerReplicaEvidence without modification.
|
|
1154
|
+
*/
|
|
1155
|
+
export function readLedgerSliceFromStore(meshId: string, opts?: ReadLedgerSliceOptions): ReturnType<typeof MeshRuntimeStore.prototype.readLedgerSlice> {
|
|
1156
|
+
return MeshRuntimeStore.getInstance().readLedgerSlice(meshId, {
|
|
1157
|
+
afterId: opts?.afterId,
|
|
1158
|
+
since: opts?.since,
|
|
1159
|
+
// ReadLedgerSliceOptions allows kind as array; SQLite path takes a single kind string.
|
|
1160
|
+
// Pass first kind value if provided; callers needing multi-kind filtering should use readLedgerSlice (JSONL).
|
|
1161
|
+
kind: opts?.kind?.length ? opts.kind[0] : undefined,
|
|
1162
|
+
limit: opts?.limit,
|
|
1163
|
+
});
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
/**
|
|
1167
|
+
* Get a summary of mesh activity from the ledger.
|
|
1168
|
+
*/
|
|
1169
|
+
export function getLedgerSummary(meshId: string): MeshLedgerSummary {
|
|
1170
|
+
return buildLedgerSummary(meshId, getCachedRawEntries(meshId));
|
|
1171
|
+
}
|
|
1172
|
+
|
|
453
1173
|
// ─── Recovery Context ───────────────────────────
|
|
454
1174
|
|
|
455
1175
|
export interface SessionRecoveryContext {
|
|
@@ -484,46 +1204,64 @@ export function getSessionRecoveryContext(
|
|
|
484
1204
|
},
|
|
485
1205
|
): SessionRecoveryContext {
|
|
486
1206
|
const maxRetries = opts.maxRetries ?? 1;
|
|
487
|
-
|
|
1207
|
+
// tail:500 is sufficient — task_dispatched is never archived (only terminal kinds are),
|
|
1208
|
+
// so dispatch history is always present. The 30-min failure window means we never need
|
|
1209
|
+
// more than a few dozen recent entries for consecutiveNodeFailures. Bounding to 500
|
|
1210
|
+
// avoids a full O(n) scan for meshes with many historical entries.
|
|
1211
|
+
const entries = readLedgerEntries(meshId, { tail: 500 });
|
|
488
1212
|
|
|
489
|
-
//
|
|
1213
|
+
// Single backward pass: find last task_dispatched AND count consecutive recent failures.
|
|
1214
|
+
const now = Date.now();
|
|
1215
|
+
const recentWindow = now - RECENT_FAILURE_WINDOW_MS;
|
|
490
1216
|
let lastDispatch: MeshLedgerEntry | null = null;
|
|
1217
|
+
let consecutiveNodeFailures = 0;
|
|
1218
|
+
let failureCountDone = false;
|
|
491
1219
|
for (let i = entries.length - 1; i >= 0; i--) {
|
|
492
1220
|
const e = entries[i];
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
1221
|
+
const ts = new Date(e.timestamp).getTime();
|
|
1222
|
+
|
|
1223
|
+
// Failure counting: scan until we exit the recent window or hit a chain-breaker
|
|
1224
|
+
if (!failureCountDone) {
|
|
1225
|
+
if (ts < recentWindow) {
|
|
1226
|
+
failureCountDone = true;
|
|
1227
|
+
} else if (opts.nodeId && e.nodeId !== opts.nodeId) {
|
|
1228
|
+
// Entry for a different node — skip for failure counting but continue scanning for dispatch
|
|
1229
|
+
} else if (e.kind === 'task_failed') {
|
|
1230
|
+
if (!isIntentionalCleanupStopEntry(e)) consecutiveNodeFailures++;
|
|
1231
|
+
} else if (e.kind === 'task_completed' || e.kind === 'task_dispatched') {
|
|
1232
|
+
// A completion or new dispatch breaks the consecutive failure chain
|
|
1233
|
+
failureCountDone = true;
|
|
1234
|
+
}
|
|
1235
|
+
}
|
|
1236
|
+
|
|
1237
|
+
// Dispatch search: find the last dispatch matching this session or node
|
|
1238
|
+
if (lastDispatch === null && e.kind === 'task_dispatched') {
|
|
1239
|
+
if (opts.sessionId && e.sessionId === opts.sessionId) { lastDispatch = e; }
|
|
1240
|
+
else if (!opts.sessionId && opts.nodeId && e.nodeId === opts.nodeId) { lastDispatch = e; }
|
|
1241
|
+
}
|
|
1242
|
+
|
|
1243
|
+
// Stop once both tasks are done
|
|
1244
|
+
if (lastDispatch !== null && failureCountDone) break;
|
|
496
1245
|
}
|
|
497
1246
|
|
|
498
1247
|
const lastTaskMessage = typeof lastDispatch?.payload?.message === 'string'
|
|
499
1248
|
? lastDispatch.payload.message
|
|
500
1249
|
: null;
|
|
501
1250
|
|
|
502
|
-
// Count
|
|
503
|
-
|
|
504
|
-
const recentWindow = now - RECENT_FAILURE_WINDOW_MS;
|
|
505
|
-
let consecutiveNodeFailures = 0;
|
|
506
|
-
for (let i = entries.length - 1; i >= 0; i--) {
|
|
507
|
-
const e = entries[i];
|
|
508
|
-
if (new Date(e.timestamp).getTime() < recentWindow) break;
|
|
509
|
-
if (opts.nodeId && e.nodeId !== opts.nodeId) continue;
|
|
510
|
-
if (e.kind === 'task_failed') {
|
|
511
|
-
if (isIntentionalCleanupStopEntry(e)) continue;
|
|
512
|
-
consecutiveNodeFailures++;
|
|
513
|
-
} else if (e.kind === 'task_completed' || e.kind === 'task_dispatched') {
|
|
514
|
-
// A completion or new dispatch breaks the consecutive failure chain
|
|
515
|
-
break;
|
|
516
|
-
}
|
|
517
|
-
}
|
|
518
|
-
|
|
519
|
-
// Count how many times the same task was attempted (match by message prefix)
|
|
1251
|
+
// Count how many times the same task was attempted.
|
|
1252
|
+
// Prefer exact taskId match (payload.taskId) to avoid 200-char prefix collisions.
|
|
520
1253
|
let taskAttemptCount = 0;
|
|
521
|
-
if (
|
|
522
|
-
const
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
if (e.payload
|
|
526
|
-
|
|
1254
|
+
if (lastDispatch) {
|
|
1255
|
+
const taskId = typeof lastDispatch.payload?.taskId === 'string' ? lastDispatch.payload.taskId : null;
|
|
1256
|
+
if (taskId) {
|
|
1257
|
+
for (const e of entries) {
|
|
1258
|
+
if (e.kind === 'task_dispatched' && e.payload?.taskId === taskId) taskAttemptCount++;
|
|
1259
|
+
}
|
|
1260
|
+
} else if (lastTaskMessage) {
|
|
1261
|
+
const prefix = lastTaskMessage.slice(0, 200);
|
|
1262
|
+
for (const e of entries) {
|
|
1263
|
+
if (e.kind === 'task_dispatched' && typeof e.payload?.message === 'string') {
|
|
1264
|
+
if (e.payload.message.startsWith(prefix)) taskAttemptCount++;
|
|
527
1265
|
}
|
|
528
1266
|
}
|
|
529
1267
|
}
|
|
@@ -573,7 +1311,8 @@ function rotateLedgerFile(meshId: string, currentPath: string): void {
|
|
|
573
1311
|
|
|
574
1312
|
try {
|
|
575
1313
|
renameSync(currentPath, getRotatedPath(meshId, index));
|
|
576
|
-
} catch {
|
|
1314
|
+
} catch (e: any) {
|
|
577
1315
|
// Rotation failed — the next append will just grow the file
|
|
1316
|
+
process.stderr.write(`[adhdev-mesh] Ledger rotation failed for mesh ${meshId}: ${e?.message || e}. File will continue to grow.\n`);
|
|
578
1317
|
}
|
|
579
1318
|
}
|