@adhdev/daemon-core 0.9.82-rc.29 → 0.9.82-rc.291
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 +8 -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 +23 -2
- package/dist/cli-adapters/cli-script-runner.d.ts +117 -0
- package/dist/cli-adapters/cli-state-engine.d.ts +198 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +106 -63
- package/dist/cli-adapters/provider-cli-parse.d.ts +4 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +38 -0
- package/dist/cli-adapters/raw-terminal-io.d.ts +37 -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/cli-manager.d.ts +2 -1
- package/dist/commands/handler.d.ts +110 -0
- package/dist/commands/mesh-coordinator.d.ts +85 -1
- package/dist/commands/router.d.ts +160 -1
- package/dist/config/chat-history.d.ts +9 -0
- package/dist/config/config.d.ts +5 -0
- package/dist/config/mesh-config.d.ts +72 -1
- package/dist/git/git-commands.d.ts +9 -1
- package/dist/git/git-diff.d.ts +6 -0
- package/dist/git/git-status.d.ts +7 -0
- package/dist/git/git-types.d.ts +2 -50
- package/dist/index.d.ts +51 -12
- package/dist/index.js +34510 -12948
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +38172 -16711
- package/dist/index.mjs.map +1 -1
- package/dist/installer.d.ts +1 -4
- package/dist/ipc/local-ipc-server.d.ts +91 -0
- package/dist/launch.d.ts +1 -1
- package/dist/logging/async-batch-writer.d.ts +10 -0
- package/dist/mesh/contracts.d.ts +164 -0
- package/dist/mesh/coordinator-prompt.d.ts +36 -0
- package/dist/mesh/coordinator-registry.d.ts +59 -0
- package/dist/mesh/mesh-active-work.d.ts +108 -0
- package/dist/mesh/mesh-delivery-policy.d.ts +131 -0
- package/dist/mesh/mesh-events-coordinator.d.ts +133 -0
- package/dist/mesh/mesh-events-pending.d.ts +45 -0
- package/dist/mesh/mesh-events-stale.d.ts +40 -0
- package/dist/mesh/mesh-events-utils.d.ts +47 -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 +9 -0
- package/dist/mesh/mesh-ledger-reconciliation.d.ts +23 -3
- package/dist/mesh/mesh-ledger.d.ts +77 -1
- package/dist/mesh/mesh-missions.d.ts +68 -0
- package/dist/mesh/mesh-reconcile-loop.d.ts +7 -0
- package/dist/mesh/mesh-refine-batch.d.ts +68 -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 +345 -0
- package/dist/mesh/mesh-task-stats.d.ts +49 -0
- package/dist/mesh/mesh-work-queue.d.ts +166 -5
- package/dist/mesh/preview-freshness.d.ts +18 -0
- package/dist/mesh/refine-config.d.ts +215 -0
- package/dist/mesh/worktree-bootstrap-config.d.ts +135 -0
- package/dist/providers/acp-provider-instance.d.ts +2 -0
- package/dist/providers/approval-utils.d.ts +13 -0
- package/dist/providers/cli-provider-instance.d.ts +61 -3
- package/dist/providers/contracts.d.ts +130 -6
- package/dist/providers/external-sources.d.ts +71 -0
- package/dist/providers/native-history/antigravity-cli-transcript.d.ts +100 -0
- package/dist/providers/native-history/claude-cli-transcript.d.ts +70 -0
- package/dist/providers/native-history/codex-cli-transcript.d.ts +73 -0
- package/dist/providers/native-history/dispatcher.d.ts +27 -0
- package/dist/providers/native-history/hermes-cli-transcript.d.ts +30 -0
- package/dist/providers/native-history/index.d.ts +13 -0
- package/dist/providers/provider-instance-manager.d.ts +13 -0
- package/dist/providers/provider-instance.d.ts +13 -1
- package/dist/providers/provider-loader.d.ts +26 -4
- package/dist/providers/provider-trust.d.ts +31 -0
- package/dist/providers/read-chat-contract.d.ts +29 -0
- package/dist/providers/sdk/v1/builders/acp/detect-status.d.ts +68 -0
- package/dist/providers/sdk/v1/builders/cli/detect-status.d.ts +85 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval-squash.d.ts +59 -0
- package/dist/providers/sdk/v1/builders/cli/parse-approval.d.ts +65 -0
- package/dist/providers/sdk/v1/builders/cli/parse-session.d.ts +91 -0
- package/dist/providers/sdk/v1/builders/cli/visible-region.d.ts +42 -0
- package/dist/providers/sdk/v1/fixture-tooling/format.d.ts +126 -0
- package/dist/providers/sdk/v1/fixture-tooling/index.d.ts +8 -0
- package/dist/providers/sdk/v1/fixture-tooling/replay.d.ts +38 -0
- package/dist/providers/sdk/v1/index.d.ts +30 -0
- package/dist/providers/sdk/v1/sandbox/README-design.d.ts +193 -0
- package/dist/providers/sdk/v1/sandbox/require-whitelist.d.ts +74 -0
- package/dist/providers/sdk/v1/sandbox/script-runner.d.ts +98 -0
- package/dist/providers/sdk/v1/types/cli/index.d.ts +277 -0
- package/dist/providers/sdk/v1/types/common/index.d.ts +203 -0
- package/dist/providers/sdk/v1/validators/index.d.ts +5 -0
- package/dist/providers/sdk/v1/validators/manifest.d.ts +40 -0
- package/dist/providers/sdk/v1/validators/taint.d.ts +52 -0
- package/dist/providers/spec/adapter.d.ts +59 -0
- package/dist/providers/spec/cli-adapter.d.ts +140 -0
- package/dist/providers/spec/evaluator.d.ts +23 -0
- package/dist/providers/spec/fsm-driver.d.ts +278 -0
- package/dist/providers/spec/fsm-evaluator.d.ts +72 -0
- package/dist/providers/spec/fsm-loader.d.ts +14 -0
- package/dist/providers/spec/fsm-types.d.ts +128 -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 +144 -0
- package/dist/providers/transcript-v2.d.ts +176 -0
- package/dist/providers/types/interactive-prompt.d.ts +48 -0
- package/dist/repo-mesh-types.d.ts +148 -13
- package/dist/sessions/registry.d.ts +6 -0
- package/dist/shared-types-extra.d.ts +2 -4
- package/dist/shared-types.d.ts +42 -1
- package/dist/status/normalize.d.ts +1 -1
- package/dist/status/normalize.js +1 -0
- package/dist/status/normalize.js.map +1 -1
- package/dist/status/normalize.mjs +1 -0
- package/dist/status/normalize.mjs.map +1 -1
- package/dist/status/reporter.d.ts +2 -0
- package/dist/status/snapshot.d.ts +1 -0
- package/dist/types.d.ts +5 -0
- package/package.json +7 -3
- package/src/agent-stream/poller.ts +2 -3
- package/src/boot/daemon-lifecycle.ts +55 -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 +1 -0
- package/src/cli-adapter-types.ts +22 -2
- package/src/cli-adapters/cli-script-runner.ts +421 -0
- package/src/cli-adapters/cli-state-engine.ts +1215 -0
- package/src/cli-adapters/provider-cli-adapter.d.ts +1 -1
- package/src/cli-adapters/provider-cli-adapter.ts +674 -1139
- package/src/cli-adapters/provider-cli-parse.d.ts +1 -0
- package/src/cli-adapters/provider-cli-parse.ts +13 -0
- package/src/cli-adapters/provider-cli-runtime.ts +3 -1
- package/src/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/src/cli-adapters/provider-cli-shared.ts +68 -11
- package/src/cli-adapters/raw-terminal-io.ts +252 -0
- package/src/cli-adapters/terminal-backends/ghostty-vt-backend.ts +53 -29
- package/src/cli-adapters/terminal-backends/types.ts +10 -3
- package/src/cli-adapters/terminal-screen.ts +16 -81
- package/src/commands/chat-commands.ts +1813 -60
- package/src/commands/cli-manager.ts +286 -14
- package/src/commands/handler.ts +809 -2
- package/src/commands/mesh-coordinator.ts +332 -122
- package/src/commands/router.ts +5721 -546
- package/src/config/chat-history.ts +229 -24
- package/src/config/config.ts +12 -0
- package/src/config/mesh-config.ts +303 -3
- package/src/config/recent-activity.ts +8 -2
- package/src/daemon/dev-auto-implement.ts +3 -2
- package/src/daemon/dev-cli-debug.ts +10 -1
- package/src/detection/ide-detector.ts +26 -16
- package/src/git/git-commands.ts +42 -12
- package/src/git/git-diff.ts +53 -0
- package/src/git/git-status.ts +198 -7
- package/src/git/git-types.ts +14 -62
- package/src/git/git-worktree.ts +8 -1
- package/src/index.ts +131 -11
- package/src/installer.d.ts +1 -1
- package/src/installer.ts +8 -6
- package/src/ipc/local-ipc-server.ts +278 -0
- package/src/launch.d.ts +1 -1
- package/src/launch.ts +37 -28
- package/src/logging/async-batch-writer.ts +55 -0
- package/src/logging/logger.ts +2 -1
- package/src/mesh/contracts.ts +329 -0
- package/src/mesh/coordinator-prompt.ts +219 -31
- package/src/mesh/coordinator-registry.ts +121 -0
- package/src/mesh/mesh-active-work.ts +468 -0
- package/src/mesh/mesh-delivery-policy.ts +315 -0
- package/src/mesh/mesh-events-coordinator.ts +1679 -0
- package/src/mesh/mesh-events-pending.ts +508 -0
- package/src/mesh/mesh-events-stale.ts +283 -0
- package/src/mesh/mesh-events-utils.ts +254 -0
- package/src/mesh/mesh-events.ts +30 -1035
- package/src/mesh/mesh-fast-forward.ts +839 -0
- package/src/mesh/mesh-host-ownership.ts +73 -0
- package/src/mesh/mesh-ledger-reconciliation.ts +12 -5
- package/src/mesh/mesh-ledger.ts +589 -102
- package/src/mesh/mesh-missions.ts +172 -0
- package/src/mesh/mesh-reconcile-loop.ts +411 -0
- package/src/mesh/mesh-refine-batch.ts +197 -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 +275 -0
- package/src/mesh/mesh-runtime-store.ts +1399 -0
- package/src/mesh/mesh-task-stats.ts +154 -0
- package/src/mesh/mesh-work-queue.ts +586 -145
- package/src/mesh/preview-freshness.ts +118 -0
- package/src/mesh/refine-config.ts +403 -0
- package/src/mesh/worktree-bootstrap-config.ts +326 -0
- package/src/providers/acp-provider-instance.ts +15 -1
- package/src/providers/approval-utils.d.ts +4 -0
- package/src/providers/approval-utils.ts +47 -5
- package/src/providers/chat-message-normalization.ts +4 -11
- package/src/providers/cli-provider-instance.ts +920 -81
- package/src/providers/contracts.d.ts +55 -0
- package/src/providers/contracts.ts +141 -6
- package/src/providers/external-sources.ts +218 -0
- package/src/providers/ide-provider-instance.ts +19 -5
- package/src/providers/native-history/antigravity-cli-transcript.ts +643 -0
- package/src/providers/native-history/claude-cli-transcript.ts +396 -0
- package/src/providers/native-history/codex-cli-transcript.ts +479 -0
- package/src/providers/native-history/dispatcher.ts +340 -0
- package/src/providers/native-history/hermes-cli-transcript.ts +230 -0
- package/src/providers/native-history/index.ts +30 -0
- package/src/providers/provider-instance-manager.ts +30 -0
- package/src/providers/provider-instance.ts +10 -1
- package/src/providers/provider-loader.ts +584 -50
- package/src/providers/provider-schema.ts +87 -14
- package/src/providers/provider-trust.ts +114 -0
- package/src/providers/read-chat-contract.ts +76 -16
- package/src/providers/sdk/README.md +49 -0
- package/src/providers/sdk/v1/builders/acp/detect-status.ts +144 -0
- package/src/providers/sdk/v1/builders/cli/detect-status.ts +262 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval-squash.ts +158 -0
- package/src/providers/sdk/v1/builders/cli/parse-approval.ts +250 -0
- package/src/providers/sdk/v1/builders/cli/parse-session.ts +276 -0
- package/src/providers/sdk/v1/builders/cli/visible-region.ts +143 -0
- package/src/providers/sdk/v1/fixture-tooling/format.ts +130 -0
- package/src/providers/sdk/v1/fixture-tooling/index.ts +22 -0
- package/src/providers/sdk/v1/fixture-tooling/replay.ts +352 -0
- package/src/providers/sdk/v1/index.ts +152 -0
- package/src/providers/sdk/v1/sandbox/README-design.ts +195 -0
- package/src/providers/sdk/v1/sandbox/require-whitelist.ts +472 -0
- package/src/providers/sdk/v1/sandbox/script-runner.ts +150 -0
- package/src/providers/sdk/v1/schemas/cli/provider.schema.json +504 -0
- package/src/providers/sdk/v1/schemas/primitives/acp-session-protocol-v1.json +131 -0
- package/src/providers/sdk/v1/schemas/primitives/native-history-codex-rollout-v1.json +66 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-approval-squash-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-assistant-block-v1.json +91 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-cue-ordering-v1.json +47 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-dispatch-order-v1.json +32 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-footer-chrome-v1.json +42 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-index-finder-v1.json +27 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-modal-v1.json +125 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-prompt-marker-v1.json +45 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-session-id-extraction-v1.json +46 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-settled-prompt-v1.json +71 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-spinner-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-transcript-pty-v1.json +83 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-visible-region-v1.json +57 -0
- package/src/providers/sdk/v1/schemas/primitives/tui-welcome-screen-v1.json +35 -0
- package/src/providers/sdk/v1/types/cli/index.ts +365 -0
- package/src/providers/sdk/v1/types/common/index.ts +229 -0
- package/src/providers/sdk/v1/validators/index.ts +19 -0
- package/src/providers/sdk/v1/validators/manifest.ts +110 -0
- package/src/providers/sdk/v1/validators/taint.ts +309 -0
- package/src/providers/spec/adapter.ts +155 -0
- package/src/providers/spec/cli-adapter.ts +730 -0
- package/src/providers/spec/evaluator.ts +345 -0
- package/src/providers/spec/fsm-driver.ts +875 -0
- package/src/providers/spec/fsm-evaluator.ts +255 -0
- package/src/providers/spec/fsm-loader.ts +112 -0
- package/src/providers/spec/fsm-types.ts +222 -0
- package/src/providers/spec/native-history-executor.ts +943 -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 +189 -0
- package/src/providers/transcript-v2.ts +567 -0
- package/src/providers/types/interactive-prompt.ts +488 -0
- package/src/providers/version-archive.ts +38 -20
- package/src/repo-mesh-types.ts +177 -13
- package/src/sessions/registry.ts +6 -0
- package/src/shared-types-extra.ts +2 -4
- package/src/shared-types.ts +45 -1
- package/src/status/builders.ts +26 -6
- package/src/status/normalize.ts +2 -0
- package/src/status/reporter.ts +15 -0
- package/src/status/snapshot.ts +84 -25
- package/src/system/host-memory.ts +29 -12
- package/src/types.ts +5 -0
- package/dist/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
|
@@ -1,24 +1,149 @@
|
|
|
1
|
-
import { existsSync, writeFileSync, readFileSync, openSync, closeSync, unlinkSync } from 'fs';
|
|
2
|
-
import { join } from 'path';
|
|
3
1
|
import { randomUUID } from 'crypto';
|
|
4
|
-
import {
|
|
2
|
+
import { requireMeshHostQueueOwner } from './mesh-host-ownership.js';
|
|
3
|
+
import type { RepoMeshDaemonRole } from '../repo-mesh-types.js';
|
|
4
|
+
import { MeshRuntimeStore } from './mesh-runtime-store.js';
|
|
5
|
+
import { getMesh } from '../config/mesh-config.js';
|
|
5
6
|
|
|
6
7
|
export type MeshTaskStatus = 'pending' | 'assigned' | 'completed' | 'failed' | 'cancelled';
|
|
7
8
|
export type MeshActiveTaskStatus = Extract<MeshTaskStatus, 'pending' | 'assigned'>;
|
|
8
9
|
export type MeshHistoricalTaskStatus = Extract<MeshTaskStatus, 'completed' | 'failed' | 'cancelled'>;
|
|
10
|
+
export type MeshTaskMode = 'code_change' | 'validation' | 'live_debug_readonly' | 'launch_app' | 'convergence';
|
|
9
11
|
|
|
10
12
|
export const ACTIVE_MESH_QUEUE_STATUSES: MeshActiveTaskStatus[] = ['pending', 'assigned'];
|
|
11
13
|
export const HISTORICAL_MESH_QUEUE_STATUSES: MeshHistoricalTaskStatus[] = ['completed', 'failed', 'cancelled'];
|
|
14
|
+
export const MESH_TASK_MODES: MeshTaskMode[] = ['code_change', 'validation', 'live_debug_readonly', 'launch_app', 'convergence'];
|
|
15
|
+
|
|
16
|
+
export interface MeshTaskModeValidationResult {
|
|
17
|
+
valid: boolean;
|
|
18
|
+
taskMode?: MeshTaskMode;
|
|
19
|
+
violations: string[];
|
|
20
|
+
allowedOperations?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const LIVE_DEBUG_READONLY_FORBIDDEN: Array<{ label: string; pattern: RegExp }> = [
|
|
24
|
+
{ label: 'source_edit', pattern: /\b(edit|modify|patch|apply\s+patch|write\s+(?:to\s+)?(?:file|source)|overwrite|delete\s+file|remove\s+file|create\s+file|touch\s+file)\b/i },
|
|
25
|
+
{ label: 'checkpoint', pattern: /\b(checkpoint|mesh_checkpoint)\b/i },
|
|
26
|
+
{ label: 'deploy_or_version_bump', pattern: /\b(deploy|wrangler\s+deploy|version[-\s]?bump|npm\s+version|release|npm\s+publish|yarn\s+publish|pnpm\s+publish)\b/i },
|
|
27
|
+
{ label: 'destructive_shell', pattern: /\b(rm\s+-rf|mv\s+\S+\s+\S+|truncate\s|tee\s+\S+|sed\s+-i|shred\b)\b/i },
|
|
28
|
+
{ label: 'package_install', pattern: /\b(npm\s+(?:install|i|add|link|uninstall|remove)|yarn\s+(?:add|remove|link)|pnpm\s+(?:add|remove|link)|pip\s+install|brew\s+install|apt\s+install|cargo\s+install)\b/i },
|
|
29
|
+
{ label: 'container_mutation', pattern: /\b(docker\s+(?:build|run|exec|push|tag|rmi|rm|create|start|stop|kill)|kubectl\s+(?:apply|delete|patch|replace|create|scale))\b/i },
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Git subcommands that mutate the working tree, index, refs, or remote.
|
|
34
|
+
* `stash` and `checkout` are intentionally absent here: they have read-only
|
|
35
|
+
* variants (`git stash list`/`show`, `git checkout-index`) and are classified
|
|
36
|
+
* token-by-token in {@link detectGitMutation} rather than by bare keyword.
|
|
37
|
+
*/
|
|
38
|
+
const GIT_MUTATION_SUBCOMMANDS = new Set([
|
|
39
|
+
'add', 'commit', 'push', 'reset', 'rebase', 'clean', 'switch', 'merge',
|
|
40
|
+
'tag', 'restore', 'rm', 'mv', 'cherry-pick', 'revert', 'pull', 'fetch',
|
|
41
|
+
'am', 'apply', 'gc', 'prune',
|
|
42
|
+
]);
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read-only `git stash` variants. Any other `git stash <x>` (pop/apply/drop/
|
|
46
|
+
* push/save/clear, or bare `git stash` which defaults to push) is a mutation.
|
|
47
|
+
*/
|
|
48
|
+
const GIT_STASH_READONLY_SUBCOMMANDS = new Set(['list', 'show']);
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Detects a true git mutation in free-text task message, token-aware so that
|
|
52
|
+
* read-only diagnostics (`git stash list`, `git stash show --stat`,
|
|
53
|
+
* `git checkout-index`, `git status`, `git diff`, `git log`, ...) are allowed.
|
|
54
|
+
* Returns true only when a genuine mutating git invocation is present.
|
|
55
|
+
*/
|
|
56
|
+
function detectGitMutation(message: string): boolean {
|
|
57
|
+
const re = /\bgit\s+([a-z][a-z0-9-]*)/gi;
|
|
58
|
+
let match: RegExpExecArray | null;
|
|
59
|
+
while ((match = re.exec(message)) !== null) {
|
|
60
|
+
const sub = match[1].toLowerCase();
|
|
61
|
+
if (GIT_MUTATION_SUBCOMMANDS.has(sub)) return true;
|
|
62
|
+
if (sub === 'stash') {
|
|
63
|
+
// Token following `git stash`; read-only only for list/show.
|
|
64
|
+
const after = message.slice(re.lastIndex).match(/^\s+([a-z][a-z0-9-]*)/i);
|
|
65
|
+
const next = after ? after[1].toLowerCase() : '';
|
|
66
|
+
if (!GIT_STASH_READONLY_SUBCOMMANDS.has(next)) return true; // bare stash = push, or pop/apply/drop/...
|
|
67
|
+
} else if (sub === 'checkout') {
|
|
68
|
+
// `git checkout <ref/path>` mutates; `git checkout-index` is matched
|
|
69
|
+
// as its own token by the regex (sub === 'checkout-index') and is read-only.
|
|
70
|
+
return true;
|
|
71
|
+
} else if (sub === 'submodule') {
|
|
72
|
+
// `git submodule update` mutates; `git submodule status` is read-only.
|
|
73
|
+
const after = message.slice(re.lastIndex).match(/^\s+([a-z][a-z0-9-]*)/i);
|
|
74
|
+
const next = after ? after[1].toLowerCase() : '';
|
|
75
|
+
if (next === 'update' || next === 'add' || next === 'sync' || next === 'deinit') return true;
|
|
76
|
+
} else if (sub === 'worktree') {
|
|
77
|
+
const after = message.slice(re.lastIndex).match(/^\s+([a-z][a-z0-9-]*)/i);
|
|
78
|
+
const next = after ? after[1].toLowerCase() : '';
|
|
79
|
+
if (next === 'add' || next === 'remove' || next === 'move' || next === 'prune') return true;
|
|
80
|
+
}
|
|
81
|
+
// checkout-index, stash-with-no-next-already-handled, status/diff/log/show/
|
|
82
|
+
// rev-parse/branch/submodule status fall through as read-only.
|
|
83
|
+
}
|
|
84
|
+
return false;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function normalizeMeshTaskMode(value: unknown): MeshTaskMode | undefined {
|
|
88
|
+
if (typeof value !== 'string') return undefined;
|
|
89
|
+
const normalized = value.trim() as MeshTaskMode;
|
|
90
|
+
return (MESH_TASK_MODES as string[]).includes(normalized) ? normalized : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export function validateMeshTaskModeRequest(mode: unknown, message: string): MeshTaskModeValidationResult {
|
|
94
|
+
const taskMode = normalizeMeshTaskMode(mode);
|
|
95
|
+
if (!taskMode) {
|
|
96
|
+
return { valid: true, violations: [] };
|
|
97
|
+
}
|
|
98
|
+
if (taskMode !== 'live_debug_readonly') {
|
|
99
|
+
return { valid: true, taskMode, violations: [] };
|
|
100
|
+
}
|
|
101
|
+
const text = message || '';
|
|
102
|
+
const violations = LIVE_DEBUG_READONLY_FORBIDDEN
|
|
103
|
+
.filter(rule => rule.pattern.test(text))
|
|
104
|
+
.map(rule => rule.label);
|
|
105
|
+
if (detectGitMutation(text)) {
|
|
106
|
+
violations.push('git_mutation');
|
|
107
|
+
}
|
|
108
|
+
return {
|
|
109
|
+
valid: violations.length === 0,
|
|
110
|
+
taskMode,
|
|
111
|
+
violations,
|
|
112
|
+
allowedOperations: [
|
|
113
|
+
'process/log/window/port/session inspection',
|
|
114
|
+
'read-only filesystem listing/reading',
|
|
115
|
+
'status probes and keep-running handle reporting',
|
|
116
|
+
'diagnostic summaries without source edits, commits, checkpoints, pushes, deploys, resets, rebases, or destructive cleanups',
|
|
117
|
+
],
|
|
118
|
+
};
|
|
119
|
+
}
|
|
12
120
|
|
|
13
121
|
export interface MeshWorkQueueEntry {
|
|
14
122
|
id: string;
|
|
15
123
|
meshId: string;
|
|
16
124
|
message: string;
|
|
17
125
|
status: MeshTaskStatus;
|
|
126
|
+
taskMode?: MeshTaskMode;
|
|
18
127
|
/** If specified, only this node can claim the task (used by legacy mesh_send_task) */
|
|
19
128
|
targetNodeId?: string;
|
|
20
129
|
/** If specified, only this runtime session can claim the task */
|
|
21
130
|
targetSessionId?: string;
|
|
131
|
+
/** If specified, a node must expose all tags before it can claim the task. */
|
|
132
|
+
requiredTags?: string[];
|
|
133
|
+
/**
|
|
134
|
+
* M1: ids of tasks that must reach 'completed' before this task is claimable.
|
|
135
|
+
* Forward references (ids not yet enqueued) are allowed for batch flows and
|
|
136
|
+
* simply keep the task waiting until the referenced task exists and completes.
|
|
137
|
+
*/
|
|
138
|
+
dependsOn?: string[];
|
|
139
|
+
/** M1/M3: mission this task belongs to (joins mesh_missions). */
|
|
140
|
+
missionId?: string;
|
|
141
|
+
/**
|
|
142
|
+
* M1: why this task is held back (e.g. "dependency_failed:<taskId>").
|
|
143
|
+
* Only set by the system on dependency failure under the 'block' policy;
|
|
144
|
+
* waiting-on-dependency state is computed at view time, not stored.
|
|
145
|
+
*/
|
|
146
|
+
blockedReason?: string;
|
|
22
147
|
/** The node that actually claimed and is executing the task */
|
|
23
148
|
assignedNodeId?: string;
|
|
24
149
|
/** The session currently executing the task */
|
|
@@ -30,6 +155,8 @@ export interface MeshWorkQueueEntry {
|
|
|
30
155
|
requeueReason?: string;
|
|
31
156
|
requeuedAt?: string;
|
|
32
157
|
requeueCount?: number;
|
|
158
|
+
/** Max automatic requeue attempts. When requeueCount reaches this, task is auto-failed. */
|
|
159
|
+
maxRetries?: number;
|
|
33
160
|
/** Last automatic queue session spin-up attempt, for mesh_view_queue/debug visibility. */
|
|
34
161
|
autoLaunch?: {
|
|
35
162
|
status: 'skipped' | 'started' | 'failed' | 'completed';
|
|
@@ -45,50 +172,112 @@ export interface MeshWorkQueueEntry {
|
|
|
45
172
|
updatedAt: string;
|
|
46
173
|
}
|
|
47
174
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
return join(getLedgerDir(), `${safe}.queue.json`);
|
|
175
|
+
export interface MeshQueueMutationOptions {
|
|
176
|
+
ownerRole?: RepoMeshDaemonRole;
|
|
51
177
|
}
|
|
52
178
|
|
|
53
|
-
function
|
|
54
|
-
|
|
55
|
-
|
|
179
|
+
export function normalizeMeshCapabilityTags(value: unknown): string[] {
|
|
180
|
+
if (!Array.isArray(value)) return [];
|
|
181
|
+
const seen = new Set<string>();
|
|
182
|
+
return value
|
|
183
|
+
.map(tag => typeof tag === 'string' ? tag.trim() : '')
|
|
184
|
+
.filter(Boolean)
|
|
185
|
+
.filter(tag => {
|
|
186
|
+
if (seen.has(tag)) return false;
|
|
187
|
+
seen.add(tag);
|
|
188
|
+
return true;
|
|
189
|
+
});
|
|
56
190
|
}
|
|
57
191
|
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
192
|
+
function firstProviderPriority(policy: unknown): string | undefined {
|
|
193
|
+
const raw = policy && typeof policy === 'object' && !Array.isArray(policy)
|
|
194
|
+
? (policy as Record<string, unknown>).providerPriority
|
|
195
|
+
: undefined;
|
|
196
|
+
if (!Array.isArray(raw)) return undefined;
|
|
197
|
+
return raw.find(type => typeof type === 'string' && type.trim())?.trim();
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
export function buildMeshNodeCapabilityTags(
|
|
201
|
+
node: { capabilities?: unknown; policy?: unknown; isLocalWorktree?: unknown; worktreeBranch?: unknown } | undefined,
|
|
202
|
+
providerType?: string,
|
|
203
|
+
): string[] {
|
|
204
|
+
const provider = typeof providerType === 'string' && providerType.trim()
|
|
205
|
+
? providerType.trim()
|
|
206
|
+
: firstProviderPriority(node?.policy);
|
|
207
|
+
const worktreeBranch = typeof node?.worktreeBranch === 'string' && node.worktreeBranch.trim()
|
|
208
|
+
? node.worktreeBranch.trim()
|
|
209
|
+
: null;
|
|
210
|
+
return normalizeMeshCapabilityTags([
|
|
211
|
+
...(Array.isArray(node?.capabilities) ? node.capabilities : []),
|
|
212
|
+
`os=${process.platform}`,
|
|
213
|
+
`arch=${process.arch}`,
|
|
214
|
+
...(provider ? [`provider=${provider}`] : []),
|
|
215
|
+
// Worktree nodes automatically expose a "worktree=<branch>" tag so that
|
|
216
|
+
// mesh_enqueue_task with required_tags: ["worktree=<branch>"] routes
|
|
217
|
+
// only to the matching worktree node.
|
|
218
|
+
...(node?.isLocalWorktree === true && worktreeBranch ? [`worktree=${worktreeBranch}`] : []),
|
|
219
|
+
]);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export function nodeSatisfiesRequiredTags(requiredTags: unknown, capabilityTags: unknown): boolean {
|
|
223
|
+
const required = normalizeMeshCapabilityTags(requiredTags);
|
|
224
|
+
if (required.length === 0) return true;
|
|
225
|
+
const available = new Set(normalizeMeshCapabilityTags(capabilityTags));
|
|
226
|
+
return required.every(tag => available.has(tag));
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function withQueueLock<T>(_meshId: string, fn: () => T): T {
|
|
230
|
+
return MeshRuntimeStore.getInstance().transaction(fn);
|
|
76
231
|
}
|
|
77
232
|
|
|
78
233
|
function readQueue(meshId: string): MeshWorkQueueEntry[] {
|
|
79
|
-
|
|
80
|
-
if (!existsSync(path)) return [];
|
|
81
|
-
try {
|
|
82
|
-
const content = readFileSync(path, 'utf-8');
|
|
83
|
-
return JSON.parse(content) as MeshWorkQueueEntry[];
|
|
84
|
-
} catch {
|
|
85
|
-
return [];
|
|
86
|
-
}
|
|
234
|
+
return MeshRuntimeStore.getInstance().getQueueEntries(meshId);
|
|
87
235
|
}
|
|
88
236
|
|
|
89
237
|
function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
|
|
90
|
-
|
|
91
|
-
|
|
238
|
+
MeshRuntimeStore.getInstance().replaceQueue(meshId, queue);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
function normalizeDependsOn(value: unknown): string[] {
|
|
242
|
+
if (!Array.isArray(value)) return [];
|
|
243
|
+
const seen = new Set<string>();
|
|
244
|
+
return value
|
|
245
|
+
.map(id => typeof id === 'string' ? id.trim() : '')
|
|
246
|
+
.filter(Boolean)
|
|
247
|
+
.filter(id => {
|
|
248
|
+
if (seen.has(id)) return false;
|
|
249
|
+
seen.add(id);
|
|
250
|
+
return true;
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* M1: detect dependency cycles before enqueue. Walks the dependency graph of
|
|
256
|
+
* existing queue entries plus the new task's edges. Fail-closed: a cycle
|
|
257
|
+
* rejects the enqueue entirely. Synchronous and bounded by queue size.
|
|
258
|
+
*/
|
|
259
|
+
export function assertNoDependencyCycle(meshId: string, newTaskId: string, dependsOn: string[]): void {
|
|
260
|
+
if (dependsOn.length === 0) return;
|
|
261
|
+
if (dependsOn.includes(newTaskId)) {
|
|
262
|
+
throw new Error(`dependency_cycle_detected: task '${newTaskId}' cannot depend on itself`);
|
|
263
|
+
}
|
|
264
|
+
const adjacency = new Map<string, string[]>();
|
|
265
|
+
for (const entry of readQueue(meshId)) {
|
|
266
|
+
adjacency.set(entry.id, normalizeDependsOn(entry.dependsOn));
|
|
267
|
+
}
|
|
268
|
+
adjacency.set(newTaskId, dependsOn);
|
|
269
|
+
// DFS from the new task: if we can reach newTaskId again, the edges form a cycle.
|
|
270
|
+
const stack = [...dependsOn];
|
|
271
|
+
const visited = new Set<string>();
|
|
272
|
+
while (stack.length > 0) {
|
|
273
|
+
const current = stack.pop()!;
|
|
274
|
+
if (current === newTaskId) {
|
|
275
|
+
throw new Error(`dependency_cycle_detected: task '${newTaskId}' is part of a dependency cycle via '${dependsOn.join(', ')}'`);
|
|
276
|
+
}
|
|
277
|
+
if (visited.has(current)) continue;
|
|
278
|
+
visited.add(current);
|
|
279
|
+
stack.push(...(adjacency.get(current) ?? []));
|
|
280
|
+
}
|
|
92
281
|
}
|
|
93
282
|
|
|
94
283
|
/**
|
|
@@ -97,22 +286,108 @@ function writeQueue(meshId: string, queue: MeshWorkQueueEntry[]): void {
|
|
|
97
286
|
export function enqueueTask(
|
|
98
287
|
meshId: string,
|
|
99
288
|
message: string,
|
|
100
|
-
opts?: {
|
|
289
|
+
opts?: {
|
|
290
|
+
targetNodeId?: string;
|
|
291
|
+
targetSessionId?: string;
|
|
292
|
+
taskMode?: MeshTaskMode | string;
|
|
293
|
+
requiredTags?: string[];
|
|
294
|
+
/** M1: tasks that must complete before this one is claimable. */
|
|
295
|
+
dependsOn?: string[];
|
|
296
|
+
/** M1/M3: mission this task belongs to. */
|
|
297
|
+
missionId?: string;
|
|
298
|
+
/** Explicit task id for batch/template flows (M5). Random UUID when omitted. */
|
|
299
|
+
id?: string;
|
|
300
|
+
} & MeshQueueMutationOptions,
|
|
101
301
|
): MeshWorkQueueEntry {
|
|
302
|
+
requireMeshHostQueueOwner(opts);
|
|
303
|
+
const modeValidation = validateMeshTaskModeRequest(opts?.taskMode, message);
|
|
304
|
+
if (!modeValidation.valid) {
|
|
305
|
+
throw new Error(`live_debug_readonly_guardrail_violation: forbidden operations (${modeValidation.violations.join(', ')})`);
|
|
306
|
+
}
|
|
307
|
+
const id = typeof opts?.id === 'string' && opts.id.trim() ? opts.id.trim() : randomUUID();
|
|
308
|
+
const dependsOn = normalizeDependsOn(opts?.dependsOn);
|
|
102
309
|
return withQueueLock(meshId, () => {
|
|
103
|
-
|
|
310
|
+
if (MeshRuntimeStore.getInstance().findQueueEntryById(meshId, id)) {
|
|
311
|
+
throw new Error(`duplicate_task_id: task '${id}' already exists in mesh '${meshId}'`);
|
|
312
|
+
}
|
|
313
|
+
assertNoDependencyCycle(meshId, id, dependsOn);
|
|
104
314
|
const entry: MeshWorkQueueEntry = {
|
|
105
|
-
id
|
|
315
|
+
id,
|
|
106
316
|
meshId,
|
|
107
317
|
message,
|
|
108
318
|
status: 'pending',
|
|
319
|
+
taskMode: modeValidation.taskMode,
|
|
109
320
|
targetNodeId: opts?.targetNodeId,
|
|
110
321
|
targetSessionId: opts?.targetSessionId,
|
|
322
|
+
requiredTags: normalizeMeshCapabilityTags(opts?.requiredTags),
|
|
323
|
+
...(dependsOn.length > 0 ? { dependsOn } : {}),
|
|
324
|
+
...(typeof opts?.missionId === 'string' && opts.missionId.trim() ? { missionId: opts.missionId.trim() } : {}),
|
|
111
325
|
createdAt: new Date().toISOString(),
|
|
112
326
|
updatedAt: new Date().toISOString(),
|
|
113
327
|
};
|
|
114
|
-
|
|
115
|
-
|
|
328
|
+
MeshRuntimeStore.getInstance().insertQueueEntry(entry);
|
|
329
|
+
return entry;
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
* Record a direct-dispatch task (mesh_send_task) as an already-assigned queue
|
|
335
|
+
* entry so it is attributable to a mission.
|
|
336
|
+
*
|
|
337
|
+
* Direct dispatch normally bypasses the queue entirely — the task lives only in
|
|
338
|
+
* the ledger + mesh_direct_dispatches table, neither of which carries a
|
|
339
|
+
* missionId, so {@link summarizeMissionTasks}/{@link computeMeshTaskStats}
|
|
340
|
+
* (which both scan the queue for `task.missionId`) count it as 0. When a
|
|
341
|
+
* mission is attached, we materialise the same queue entry shape an enqueued
|
|
342
|
+
* task would have, but pre-assigned to the dispatched node/session and stamped
|
|
343
|
+
* with the dispatch timestamp. The terminal event path (updateSessionTaskStatus
|
|
344
|
+
* → findAssignedBySession) then flips it to completed/failed exactly like a
|
|
345
|
+
* pulled task, so mission total + completed aggregates work with no extra wiring.
|
|
346
|
+
*
|
|
347
|
+
* Intentionally separate from {@link enqueueTask}: enqueue creates `pending`
|
|
348
|
+
* work for the queue to assign, whereas this records work already dispatched
|
|
349
|
+
* out-of-band. They share the missionId stamping rule and mode validation.
|
|
350
|
+
*/
|
|
351
|
+
export function recordDirectDispatchTask(
|
|
352
|
+
meshId: string,
|
|
353
|
+
message: string,
|
|
354
|
+
opts: {
|
|
355
|
+
id: string;
|
|
356
|
+
missionId: string;
|
|
357
|
+
assignedNodeId?: string;
|
|
358
|
+
assignedSessionId?: string;
|
|
359
|
+
taskMode?: MeshTaskMode | string;
|
|
360
|
+
dispatchedAt?: string;
|
|
361
|
+
},
|
|
362
|
+
): MeshWorkQueueEntry | null {
|
|
363
|
+
const missionId = typeof opts.missionId === 'string' ? opts.missionId.trim() : '';
|
|
364
|
+
if (!missionId) return null;
|
|
365
|
+
const taskId = typeof opts.id === 'string' ? opts.id.trim() : '';
|
|
366
|
+
if (!taskId) return null;
|
|
367
|
+
const modeValidation = validateMeshTaskModeRequest(opts.taskMode, message);
|
|
368
|
+
if (!modeValidation.valid) {
|
|
369
|
+
throw new Error(`live_debug_readonly_guardrail_violation: forbidden operations (${modeValidation.violations.join(', ')})`);
|
|
370
|
+
}
|
|
371
|
+
const now = opts.dispatchedAt && opts.dispatchedAt.trim() ? opts.dispatchedAt : new Date().toISOString();
|
|
372
|
+
return withQueueLock(meshId, () => {
|
|
373
|
+
if (MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId)) {
|
|
374
|
+
// Already materialised (e.g. retry of the same dispatch) — leave it untouched.
|
|
375
|
+
return null;
|
|
376
|
+
}
|
|
377
|
+
const entry: MeshWorkQueueEntry = {
|
|
378
|
+
id: taskId,
|
|
379
|
+
meshId,
|
|
380
|
+
message,
|
|
381
|
+
status: 'assigned',
|
|
382
|
+
...(modeValidation.taskMode ? { taskMode: modeValidation.taskMode } : {}),
|
|
383
|
+
missionId,
|
|
384
|
+
...(opts.assignedNodeId ? { targetNodeId: opts.assignedNodeId, assignedNodeId: opts.assignedNodeId } : {}),
|
|
385
|
+
...(opts.assignedSessionId ? { targetSessionId: opts.assignedSessionId, assignedSessionId: opts.assignedSessionId } : {}),
|
|
386
|
+
dispatchTimestamp: now,
|
|
387
|
+
createdAt: now,
|
|
388
|
+
updatedAt: now,
|
|
389
|
+
};
|
|
390
|
+
MeshRuntimeStore.getInstance().insertQueueEntry(entry);
|
|
116
391
|
return entry;
|
|
117
392
|
});
|
|
118
393
|
}
|
|
@@ -121,43 +396,71 @@ export function enqueueTask(
|
|
|
121
396
|
* Get all tasks in the queue, optionally filtered by status.
|
|
122
397
|
*/
|
|
123
398
|
export function getQueue(meshId: string, opts?: { status?: MeshTaskStatus[] }): MeshWorkQueueEntry[] {
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
return queue;
|
|
399
|
+
return MeshRuntimeStore.getInstance().getQueueEntries(meshId, opts?.status?.length ? opts.status : undefined);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
export function getMeshQueueRevision(meshId: string): string {
|
|
403
|
+
return MeshRuntimeStore.getInstance().getQueueRevision(meshId);
|
|
130
404
|
}
|
|
131
405
|
|
|
132
406
|
/**
|
|
133
407
|
* Find the next pending task that this node is allowed to claim, and mark it as assigned.
|
|
134
408
|
*/
|
|
135
|
-
export function claimNextTask(meshId: string, nodeId: string, sessionId: string): MeshWorkQueueEntry | null {
|
|
136
|
-
return
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
|
|
409
|
+
export function claimNextTask(meshId: string, nodeId: string, sessionId: string, capabilityTags?: string[]): MeshWorkQueueEntry | null {
|
|
410
|
+
return MeshRuntimeStore.getInstance().claimNextQueueTask(meshId, nodeId, sessionId, capabilityTags);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
// ─── M1: Dependency Failure Propagation ─────────
|
|
414
|
+
|
|
415
|
+
export type DependencyFailurePolicy = 'block' | 'cancel';
|
|
416
|
+
|
|
417
|
+
function resolveDependencyFailurePolicy(meshId: string): DependencyFailurePolicy {
|
|
418
|
+
try {
|
|
419
|
+
const policy = (getMesh(meshId)?.policy ?? {}) as Record<string, unknown>;
|
|
420
|
+
return policy.onDependencyFailure === 'cancel' ? 'cancel' : 'block';
|
|
421
|
+
} catch {
|
|
422
|
+
return 'block';
|
|
423
|
+
}
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Apply the mesh's onDependencyFailure policy to pending dependents of a task
|
|
428
|
+
* that just reached a failed/cancelled terminal state.
|
|
429
|
+
*
|
|
430
|
+
* - 'block' (default): dependents stay pending with blockedReason
|
|
431
|
+
* "dependency_failed:<taskId>" so an operator can requeue/cancel them.
|
|
432
|
+
* - 'cancel': dependents are cancelled (cascading to their own dependents).
|
|
433
|
+
*
|
|
434
|
+
* Must be called inside the queue lock of the triggering transition.
|
|
435
|
+
*/
|
|
436
|
+
function propagateDependencyFailure(meshId: string, failedTaskId: string): void {
|
|
437
|
+
const policy = resolveDependencyFailurePolicy(meshId);
|
|
438
|
+
const store = MeshRuntimeStore.getInstance();
|
|
439
|
+
const frontier = [failedTaskId];
|
|
440
|
+
const seen = new Set<string>(frontier);
|
|
441
|
+
while (frontier.length > 0) {
|
|
442
|
+
const currentId = frontier.pop()!;
|
|
443
|
+
const dependents = store.getQueueEntries(meshId, ['pending'])
|
|
444
|
+
.filter(entry => Array.isArray(entry.dependsOn) && entry.dependsOn.includes(currentId));
|
|
445
|
+
for (const dependent of dependents) {
|
|
446
|
+
if (seen.has(dependent.id)) continue;
|
|
447
|
+
seen.add(dependent.id);
|
|
448
|
+
if (policy === 'cancel') {
|
|
449
|
+
dependent.status = 'cancelled';
|
|
450
|
+
dependent.cancelledAt = new Date().toISOString();
|
|
451
|
+
dependent.cancelReason = `dependency_failed:${currentId}`;
|
|
452
|
+
store.updateQueueEntry(dependent);
|
|
453
|
+
frontier.push(dependent.id); // cascade to transitive dependents
|
|
454
|
+
} else {
|
|
455
|
+
dependent.blockedReason = `dependency_failed:${currentId}`;
|
|
456
|
+
store.updateQueueEntry(dependent);
|
|
457
|
+
}
|
|
148
458
|
}
|
|
149
|
-
|
|
150
|
-
const entry = queue[targetIdx];
|
|
151
|
-
entry.status = 'assigned';
|
|
152
|
-
entry.assignedNodeId = nodeId;
|
|
153
|
-
entry.assignedSessionId = sessionId;
|
|
154
|
-
entry.dispatchTimestamp = new Date().toISOString();
|
|
155
|
-
entry.updatedAt = new Date().toISOString();
|
|
156
|
-
writeQueue(meshId, queue);
|
|
157
|
-
return entry;
|
|
158
|
-
});
|
|
459
|
+
}
|
|
159
460
|
}
|
|
160
461
|
|
|
462
|
+
const DEPENDENCY_FAILURE_TERMINALS = new Set<MeshTaskStatus>(['failed', 'cancelled']);
|
|
463
|
+
|
|
161
464
|
/**
|
|
162
465
|
* Update the status of a specific task.
|
|
163
466
|
* Used when a session completes, fails, or stalls.
|
|
@@ -166,15 +469,16 @@ export function updateTaskStatus(
|
|
|
166
469
|
meshId: string,
|
|
167
470
|
taskId: string,
|
|
168
471
|
status: MeshTaskStatus,
|
|
472
|
+
opts?: MeshQueueMutationOptions,
|
|
169
473
|
): MeshWorkQueueEntry | null {
|
|
474
|
+
requireMeshHostQueueOwner(opts);
|
|
170
475
|
return withQueueLock(meshId, () => {
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
return queue[idx];
|
|
476
|
+
const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
|
|
477
|
+
if (!entry) return null;
|
|
478
|
+
entry.status = status;
|
|
479
|
+
MeshRuntimeStore.getInstance().updateQueueEntry(entry);
|
|
480
|
+
if (DEPENDENCY_FAILURE_TERMINALS.has(status)) propagateDependencyFailure(meshId, taskId);
|
|
481
|
+
return entry;
|
|
178
482
|
});
|
|
179
483
|
}
|
|
180
484
|
|
|
@@ -184,14 +488,12 @@ export function recordTaskAutoLaunch(
|
|
|
184
488
|
autoLaunch: Omit<NonNullable<MeshWorkQueueEntry['autoLaunch']>, 'updatedAt'>,
|
|
185
489
|
): MeshWorkQueueEntry | null {
|
|
186
490
|
return withQueueLock(meshId, () => {
|
|
187
|
-
const
|
|
188
|
-
|
|
189
|
-
if (idx === -1) return null;
|
|
491
|
+
const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
|
|
492
|
+
if (!entry) return null;
|
|
190
493
|
const now = new Date().toISOString();
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
return queue[idx];
|
|
494
|
+
entry.autoLaunch = { ...autoLaunch, updatedAt: now };
|
|
495
|
+
MeshRuntimeStore.getInstance().updateQueueEntry(entry);
|
|
496
|
+
return entry;
|
|
195
497
|
});
|
|
196
498
|
}
|
|
197
499
|
|
|
@@ -201,19 +503,19 @@ export function recordTaskAutoLaunch(
|
|
|
201
503
|
export function cancelTask(
|
|
202
504
|
meshId: string,
|
|
203
505
|
taskId: string,
|
|
204
|
-
opts?: { reason?: string },
|
|
506
|
+
opts?: { reason?: string } & MeshQueueMutationOptions,
|
|
205
507
|
): MeshWorkQueueEntry | null {
|
|
508
|
+
requireMeshHostQueueOwner(opts);
|
|
206
509
|
return withQueueLock(meshId, () => {
|
|
207
|
-
const
|
|
208
|
-
|
|
209
|
-
if (idx === -1) return null;
|
|
510
|
+
const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
|
|
511
|
+
if (!entry) return null;
|
|
210
512
|
const now = new Date().toISOString();
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
return
|
|
513
|
+
entry.status = 'cancelled';
|
|
514
|
+
entry.cancelledAt = now;
|
|
515
|
+
if (opts?.reason) entry.cancelReason = opts.reason;
|
|
516
|
+
MeshRuntimeStore.getInstance().updateQueueEntry(entry);
|
|
517
|
+
propagateDependencyFailure(meshId, taskId);
|
|
518
|
+
return entry;
|
|
217
519
|
});
|
|
218
520
|
}
|
|
219
521
|
|
|
@@ -221,6 +523,11 @@ export function cancelTask(
|
|
|
221
523
|
* Return a queue task to pending for retry. By default, dead session targeting
|
|
222
524
|
* and assigned ownership are cleared so stale assignments do not strand again.
|
|
223
525
|
*/
|
|
526
|
+
export type RequeueResult =
|
|
527
|
+
| { status: 'requeued'; entry: MeshWorkQueueEntry }
|
|
528
|
+
| { status: 'failed_max_retries'; entry: MeshWorkQueueEntry; maxRetries: number; requeueCount: number }
|
|
529
|
+
| { status: 'not_found' };
|
|
530
|
+
|
|
224
531
|
export function requeueTask(
|
|
225
532
|
meshId: string,
|
|
226
533
|
taskId: string,
|
|
@@ -230,15 +537,34 @@ export function requeueTask(
|
|
|
230
537
|
targetSessionId?: string;
|
|
231
538
|
clearTargetNode?: boolean;
|
|
232
539
|
clearTargetSession?: boolean;
|
|
233
|
-
|
|
540
|
+
/**
|
|
541
|
+
* Override the retry cap for this call. Use only for explicit operator actions.
|
|
542
|
+
* If true, the task is requeued even when requeueCount >= maxRetries.
|
|
543
|
+
*/
|
|
544
|
+
force?: boolean;
|
|
545
|
+
/** Per-task retry cap override. Falls back to mesh policy maxTaskRetries (default 1). */
|
|
546
|
+
maxRetries?: number;
|
|
547
|
+
} & MeshQueueMutationOptions,
|
|
234
548
|
): MeshWorkQueueEntry | null {
|
|
549
|
+
requireMeshHostQueueOwner(opts);
|
|
235
550
|
return withQueueLock(meshId, () => {
|
|
236
|
-
const
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
const
|
|
240
|
-
|
|
551
|
+
const entry = MeshRuntimeStore.getInstance().findQueueEntryById(meshId, taskId);
|
|
552
|
+
if (!entry) return null;
|
|
553
|
+
const currentCount = entry.requeueCount || 0;
|
|
554
|
+
const maxRetries = opts?.maxRetries ?? entry.maxRetries ?? 1;
|
|
555
|
+
if (!opts?.force && currentCount >= maxRetries) {
|
|
556
|
+
// Auto-fail: cap exceeded without explicit force override.
|
|
557
|
+
entry.status = 'failed';
|
|
558
|
+
entry.cancelReason = `max_retries_exceeded: requeued ${currentCount} time(s), limit is ${maxRetries}`;
|
|
559
|
+
entry.updatedAt = new Date().toISOString();
|
|
560
|
+
MeshRuntimeStore.getInstance().updateQueueEntry(entry);
|
|
561
|
+
propagateDependencyFailure(meshId, taskId);
|
|
562
|
+
return entry;
|
|
563
|
+
}
|
|
241
564
|
entry.status = 'pending';
|
|
565
|
+
// Operator requeue clears a dependency-failure block — the operator is
|
|
566
|
+
// explicitly overriding the held-back state.
|
|
567
|
+
delete entry.blockedReason;
|
|
242
568
|
delete entry.assignedNodeId;
|
|
243
569
|
delete entry.assignedSessionId;
|
|
244
570
|
delete entry.cancelledAt;
|
|
@@ -247,11 +573,10 @@ export function requeueTask(
|
|
|
247
573
|
if (typeof opts?.targetNodeId === 'string') entry.targetNodeId = opts.targetNodeId;
|
|
248
574
|
if (opts?.clearTargetSession !== false) delete entry.targetSessionId;
|
|
249
575
|
if (typeof opts?.targetSessionId === 'string') entry.targetSessionId = opts.targetSessionId;
|
|
250
|
-
entry.
|
|
251
|
-
entry.
|
|
252
|
-
entry.requeueCount = (entry.requeueCount || 0) + 1;
|
|
576
|
+
entry.requeuedAt = new Date().toISOString();
|
|
577
|
+
entry.requeueCount = currentCount + 1;
|
|
253
578
|
if (opts?.reason) entry.requeueReason = opts.reason;
|
|
254
|
-
|
|
579
|
+
MeshRuntimeStore.getInstance().updateQueueEntry(entry);
|
|
255
580
|
return entry;
|
|
256
581
|
});
|
|
257
582
|
}
|
|
@@ -266,25 +591,42 @@ export function updateSessionTaskStatus(
|
|
|
266
591
|
opts?: { occurredAt?: string },
|
|
267
592
|
): MeshWorkQueueEntry | null {
|
|
268
593
|
return withQueueLock(meshId, () => {
|
|
269
|
-
const
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
const time = new Date(queue[i].dispatchTimestamp || queue[i].updatedAt).getTime();
|
|
277
|
-
if (hasOccurredAt && Number.isFinite(time) && time > occurredAtTime) continue;
|
|
278
|
-
if (time > bestTime) { bestTime = time; bestIdx = i; }
|
|
279
|
-
}
|
|
280
|
-
if (bestIdx === -1) return null;
|
|
281
|
-
queue[bestIdx].status = status;
|
|
282
|
-
queue[bestIdx].updatedAt = new Date().toISOString();
|
|
283
|
-
writeQueue(meshId, queue);
|
|
284
|
-
return queue[bestIdx];
|
|
594
|
+
const occurredAtIso = opts?.occurredAt ? new Date(opts.occurredAt).toISOString() : undefined;
|
|
595
|
+
const entry = MeshRuntimeStore.getInstance().findAssignedBySession(meshId, sessionId, occurredAtIso);
|
|
596
|
+
if (!entry) return null;
|
|
597
|
+
entry.status = status;
|
|
598
|
+
MeshRuntimeStore.getInstance().updateQueueEntry(entry);
|
|
599
|
+
if (DEPENDENCY_FAILURE_TERMINALS.has(status)) propagateDependencyFailure(meshId, entry.id);
|
|
600
|
+
return entry;
|
|
285
601
|
});
|
|
286
602
|
}
|
|
287
603
|
|
|
604
|
+
/**
|
|
605
|
+
* M1-3: true when at least one pending task is waiting on the given task.
|
|
606
|
+
* Used by the completion event path to decide whether to wake the queue.
|
|
607
|
+
*/
|
|
608
|
+
export function hasPendingDependents(meshId: string, taskId: string): boolean {
|
|
609
|
+
return MeshRuntimeStore.getInstance().getQueueEntries(meshId, ['pending'])
|
|
610
|
+
.some(entry => Array.isArray(entry.dependsOn) && entry.dependsOn.includes(taskId));
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* M1-4: view-time dependency state for a task — unmet dependency ids and
|
|
615
|
+
* whether the task is currently claimable from a dependency standpoint.
|
|
616
|
+
* Not stored (truth stays in task statuses).
|
|
617
|
+
*/
|
|
618
|
+
export function describeTaskDependencyState(
|
|
619
|
+
entry: Pick<MeshWorkQueueEntry, 'dependsOn' | 'blockedReason'>,
|
|
620
|
+
statusById: Map<string, MeshTaskStatus | string>,
|
|
621
|
+
): { waitingOn: string[]; dependenciesSatisfied: boolean } {
|
|
622
|
+
const deps = Array.isArray(entry.dependsOn) ? entry.dependsOn : [];
|
|
623
|
+
const waitingOn = deps.filter(depId => statusById.get(depId) !== 'completed');
|
|
624
|
+
return {
|
|
625
|
+
waitingOn,
|
|
626
|
+
dependenciesSatisfied: waitingOn.length === 0 && !entry.blockedReason,
|
|
627
|
+
};
|
|
628
|
+
}
|
|
629
|
+
|
|
288
630
|
export interface MeshWorkQueueStats {
|
|
289
631
|
total: number;
|
|
290
632
|
active: number;
|
|
@@ -310,14 +652,16 @@ export interface MeshWorkQueueStats {
|
|
|
310
652
|
* Return aggregate queue statistics for the given mesh.
|
|
311
653
|
*/
|
|
312
654
|
export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
|
|
313
|
-
const
|
|
314
|
-
const
|
|
315
|
-
const
|
|
316
|
-
const
|
|
317
|
-
const
|
|
318
|
-
const
|
|
655
|
+
const rows = MeshRuntimeStore.getInstance().getQueueStatsByStatus(meshId);
|
|
656
|
+
const counts: Record<string, number> = {};
|
|
657
|
+
for (const r of rows) counts[r.status] = r.count;
|
|
658
|
+
const pending = counts['pending'] ?? 0;
|
|
659
|
+
const assigned = counts['assigned'] ?? 0;
|
|
660
|
+
const completed = counts['completed'] ?? 0;
|
|
661
|
+
const failed = counts['failed'] ?? 0;
|
|
662
|
+
const cancelled = counts['cancelled'] ?? 0;
|
|
319
663
|
return {
|
|
320
|
-
total:
|
|
664
|
+
total: pending + assigned + completed + failed + cancelled,
|
|
321
665
|
active: pending + assigned,
|
|
322
666
|
historical: completed + failed + cancelled,
|
|
323
667
|
pending,
|
|
@@ -325,22 +669,119 @@ export function getMeshQueueStats(meshId: string): MeshWorkQueueStats {
|
|
|
325
669
|
completed,
|
|
326
670
|
failed,
|
|
327
671
|
cancelled,
|
|
328
|
-
activeCounts: {
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
},
|
|
332
|
-
historicalCounts: {
|
|
333
|
-
completed,
|
|
334
|
-
failed,
|
|
335
|
-
cancelled,
|
|
336
|
-
},
|
|
337
|
-
activeAssignments: queue
|
|
338
|
-
.filter(q => q.status === 'assigned')
|
|
339
|
-
.map(q => ({
|
|
340
|
-
id: q.id,
|
|
341
|
-
nodeId: q.assignedNodeId,
|
|
342
|
-
sessionId: q.assignedSessionId,
|
|
343
|
-
message: q.message,
|
|
344
|
-
})),
|
|
672
|
+
activeCounts: { pending, assigned },
|
|
673
|
+
historicalCounts: { completed, failed, cancelled },
|
|
674
|
+
activeAssignments: MeshRuntimeStore.getInstance().getActiveAssignmentDetails(meshId),
|
|
345
675
|
};
|
|
346
676
|
}
|
|
677
|
+
|
|
678
|
+
export function __replaceMeshQueueForTests(meshId: string, queue: MeshWorkQueueEntry[]): void {
|
|
679
|
+
MeshRuntimeStore.getInstance().transaction(() => {
|
|
680
|
+
MeshRuntimeStore.getInstance().replaceQueue(meshId, queue);
|
|
681
|
+
});
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
export function __clearMeshQueueForTests(meshId: string): void {
|
|
685
|
+
MeshRuntimeStore.getInstance().deleteQueue(meshId);
|
|
686
|
+
}
|
|
687
|
+
|
|
688
|
+
export function __clearDirectDispatchesForTests(meshId: string): void {
|
|
689
|
+
MeshRuntimeStore.getInstance().deleteDirectDispatches(meshId);
|
|
690
|
+
}
|
|
691
|
+
|
|
692
|
+
export function __resetMeshRuntimeStoreForTests(): void {
|
|
693
|
+
MeshRuntimeStore.resetForTests();
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
// ── Direct Dispatch Tracking ─────────────────────────────────────────────────
|
|
697
|
+
// Persists direct (non-queue) task dispatches so buildMeshActiveWork can read
|
|
698
|
+
// active work from MeshRuntimeStore instead of scanning ledger JSONL entries.
|
|
699
|
+
|
|
700
|
+
export type DirectDispatchRecord = ReturnType<MeshRuntimeStore['getActiveDirectDispatches']>[number];
|
|
701
|
+
|
|
702
|
+
export function insertDirectDispatch(
|
|
703
|
+
meshId: string,
|
|
704
|
+
data: {
|
|
705
|
+
taskId: string;
|
|
706
|
+
nodeId?: string;
|
|
707
|
+
sessionId?: string;
|
|
708
|
+
providerType?: string;
|
|
709
|
+
message: string;
|
|
710
|
+
taskMode?: string;
|
|
711
|
+
via: string;
|
|
712
|
+
dispatchedToIdleSession?: boolean;
|
|
713
|
+
dispatchedAt: string;
|
|
714
|
+
},
|
|
715
|
+
): void {
|
|
716
|
+
try {
|
|
717
|
+
MeshRuntimeStore.getInstance().insertDirectDispatch({ ...data, meshId });
|
|
718
|
+
} catch (e: any) {
|
|
719
|
+
process.stderr.write(`[adhdev-mesh] insertDirectDispatch failed for task ${data.taskId}: ${e?.message || e}\n`);
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
export function getActiveDirectDispatches(meshId: string): DirectDispatchRecord[] {
|
|
724
|
+
try {
|
|
725
|
+
return MeshRuntimeStore.getInstance().getActiveDirectDispatches(meshId);
|
|
726
|
+
} catch {
|
|
727
|
+
return [];
|
|
728
|
+
}
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
export function updateDirectDispatchStatus(
|
|
732
|
+
meshId: string,
|
|
733
|
+
sessionId: string,
|
|
734
|
+
status: 'acked' | 'completed' | 'failed' | 'stale',
|
|
735
|
+
): void {
|
|
736
|
+
try {
|
|
737
|
+
MeshRuntimeStore.getInstance().updateDirectDispatchStatus(meshId, sessionId, status);
|
|
738
|
+
} catch { /* best-effort */ }
|
|
739
|
+
}
|
|
740
|
+
|
|
741
|
+
export function cleanupTerminalDirectDispatches(olderThanMs = 7 * 24 * 60 * 60_000): void {
|
|
742
|
+
try {
|
|
743
|
+
MeshRuntimeStore.getInstance().cleanupTerminalDirectDispatches(olderThanMs);
|
|
744
|
+
} catch { /* best-effort */ }
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
export function markStaleDirectDispatches(meshId: string, olderThanMs = 60 * 60_000): void {
|
|
748
|
+
try {
|
|
749
|
+
MeshRuntimeStore.getInstance().markStaleDirectDispatches(meshId, olderThanMs);
|
|
750
|
+
} catch { /* best-effort */ }
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
/**
|
|
754
|
+
* Delete specific direct dispatch rows by taskId. Returns the number of rows deleted.
|
|
755
|
+
* Used by the staleDirect prune path to evict orphaned/terminal dispatch records from the
|
|
756
|
+
* active staleDirect surface while leaving the append-only mesh ledger (audit history) intact.
|
|
757
|
+
*/
|
|
758
|
+
export function deleteDirectDispatchesByTaskId(meshId: string, taskIds: string[]): number {
|
|
759
|
+
try {
|
|
760
|
+
return MeshRuntimeStore.getInstance().deleteDirectDispatchesByTaskId(meshId, taskIds);
|
|
761
|
+
} catch {
|
|
762
|
+
return 0;
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
|
|
766
|
+
export type MeshToolCallRateResult = { rateLimitExceeded: boolean; callsInWindow: number; advisory: string | null };
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* Record a coordinator tool call and return a rate-limit advisory when the
|
|
770
|
+
* call rate for that tool exceeds the allowed threshold.
|
|
771
|
+
*
|
|
772
|
+
* Defaults: 10-second sliding window, max 5 calls before advisory is raised.
|
|
773
|
+
* Returns { rateLimitExceeded: false } on any store error so callers are not blocked.
|
|
774
|
+
*/
|
|
775
|
+
export function recordMeshToolCall(opts: {
|
|
776
|
+
meshId: string;
|
|
777
|
+
tool: string;
|
|
778
|
+
sessionId?: string | null;
|
|
779
|
+
windowMs?: number;
|
|
780
|
+
maxCalls?: number;
|
|
781
|
+
}): MeshToolCallRateResult {
|
|
782
|
+
try {
|
|
783
|
+
return MeshRuntimeStore.getInstance().recordMeshToolCall(opts);
|
|
784
|
+
} catch {
|
|
785
|
+
return { rateLimitExceeded: false, callsInWindow: 0, advisory: null };
|
|
786
|
+
}
|
|
787
|
+
}
|