@flowdot.ai/daemon 1.0.0
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/LICENSE +45 -0
- package/README.md +51 -0
- package/dist/goals/DependencyResolver.d.ts +54 -0
- package/dist/goals/DependencyResolver.js +329 -0
- package/dist/goals/ErrorRecovery.d.ts +133 -0
- package/dist/goals/ErrorRecovery.js +489 -0
- package/dist/goals/GoalApiClient.d.ts +81 -0
- package/dist/goals/GoalApiClient.js +743 -0
- package/dist/goals/GoalCache.d.ts +65 -0
- package/dist/goals/GoalCache.js +243 -0
- package/dist/goals/GoalCommsHandler.d.ts +150 -0
- package/dist/goals/GoalCommsHandler.js +378 -0
- package/dist/goals/GoalExporter.d.ts +164 -0
- package/dist/goals/GoalExporter.js +318 -0
- package/dist/goals/GoalImporter.d.ts +107 -0
- package/dist/goals/GoalImporter.js +345 -0
- package/dist/goals/GoalManager.d.ts +110 -0
- package/dist/goals/GoalManager.js +535 -0
- package/dist/goals/GoalReporter.d.ts +105 -0
- package/dist/goals/GoalReporter.js +534 -0
- package/dist/goals/GoalScheduler.d.ts +102 -0
- package/dist/goals/GoalScheduler.js +209 -0
- package/dist/goals/GoalValidator.d.ts +72 -0
- package/dist/goals/GoalValidator.js +657 -0
- package/dist/goals/MetaGoalEnforcer.d.ts +111 -0
- package/dist/goals/MetaGoalEnforcer.js +536 -0
- package/dist/goals/MilestoneBreaker.d.ts +74 -0
- package/dist/goals/MilestoneBreaker.js +348 -0
- package/dist/goals/PermissionBridge.d.ts +109 -0
- package/dist/goals/PermissionBridge.js +326 -0
- package/dist/goals/ProgressTracker.d.ts +113 -0
- package/dist/goals/ProgressTracker.js +324 -0
- package/dist/goals/ReviewScheduler.d.ts +106 -0
- package/dist/goals/ReviewScheduler.js +360 -0
- package/dist/goals/TaskExecutor.d.ts +116 -0
- package/dist/goals/TaskExecutor.js +370 -0
- package/dist/goals/TaskFeedback.d.ts +126 -0
- package/dist/goals/TaskFeedback.js +402 -0
- package/dist/goals/TaskGenerator.d.ts +75 -0
- package/dist/goals/TaskGenerator.js +329 -0
- package/dist/goals/TaskQueue.d.ts +84 -0
- package/dist/goals/TaskQueue.js +331 -0
- package/dist/goals/TaskSanitizer.d.ts +61 -0
- package/dist/goals/TaskSanitizer.js +464 -0
- package/dist/goals/errors.d.ts +116 -0
- package/dist/goals/errors.js +299 -0
- package/dist/goals/index.d.ts +24 -0
- package/dist/goals/index.js +23 -0
- package/dist/goals/types.d.ts +395 -0
- package/dist/goals/types.js +230 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +3 -0
- package/dist/loop/DaemonIPC.d.ts +67 -0
- package/dist/loop/DaemonIPC.js +358 -0
- package/dist/loop/IntervalParser.d.ts +39 -0
- package/dist/loop/IntervalParser.js +217 -0
- package/dist/loop/LoopDaemon.d.ts +123 -0
- package/dist/loop/LoopDaemon.js +1821 -0
- package/dist/loop/LoopExecutor.d.ts +93 -0
- package/dist/loop/LoopExecutor.js +326 -0
- package/dist/loop/LoopManager.d.ts +79 -0
- package/dist/loop/LoopManager.js +476 -0
- package/dist/loop/LoopScheduler.d.ts +69 -0
- package/dist/loop/LoopScheduler.js +329 -0
- package/dist/loop/LoopStore.d.ts +57 -0
- package/dist/loop/LoopStore.js +406 -0
- package/dist/loop/LoopValidator.d.ts +55 -0
- package/dist/loop/LoopValidator.js +603 -0
- package/dist/loop/errors.d.ts +115 -0
- package/dist/loop/errors.js +312 -0
- package/dist/loop/index.d.ts +11 -0
- package/dist/loop/index.js +10 -0
- package/dist/loop/notifications/Notifier.d.ts +28 -0
- package/dist/loop/notifications/Notifier.js +78 -0
- package/dist/loop/notifications/SlackNotifier.d.ts +28 -0
- package/dist/loop/notifications/SlackNotifier.js +203 -0
- package/dist/loop/notifications/TerminalNotifier.d.ts +18 -0
- package/dist/loop/notifications/TerminalNotifier.js +72 -0
- package/dist/loop/notifications/WebhookNotifier.d.ts +24 -0
- package/dist/loop/notifications/WebhookNotifier.js +123 -0
- package/dist/loop/notifications/index.d.ts +24 -0
- package/dist/loop/notifications/index.js +109 -0
- package/dist/loop/types.d.ts +280 -0
- package/dist/loop/types.js +222 -0
- package/package.json +92 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
FlowDot Proprietary Software License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FlowDot LLC. All Rights Reserved.
|
|
4
|
+
|
|
5
|
+
TERMS AND CONDITIONS
|
|
6
|
+
|
|
7
|
+
1. GRANT OF LICENSE
|
|
8
|
+
FlowDot LLC grants you a limited, non-exclusive, non-transferable license
|
|
9
|
+
to install and use this software for personal or commercial purposes,
|
|
10
|
+
subject to the terms of this agreement.
|
|
11
|
+
|
|
12
|
+
2. RESTRICTIONS
|
|
13
|
+
You may NOT:
|
|
14
|
+
- Modify, alter, adapt, or create derivative works of this software
|
|
15
|
+
- Reverse engineer, decompile, disassemble, or attempt to derive the
|
|
16
|
+
source code of this software
|
|
17
|
+
- Redistribute, sublicense, lease, rent, or sell copies of this software
|
|
18
|
+
- Remove, alter, or obscure any proprietary notices, labels, or marks
|
|
19
|
+
- Use this software to create a competing product or service
|
|
20
|
+
|
|
21
|
+
3. OWNERSHIP
|
|
22
|
+
This software is licensed, not sold. FlowDot LLC retains all right, title,
|
|
23
|
+
and interest in and to the software, including all intellectual property
|
|
24
|
+
rights therein.
|
|
25
|
+
|
|
26
|
+
4. TERMINATION
|
|
27
|
+
This license is effective until terminated. Your rights under this license
|
|
28
|
+
will terminate automatically without notice if you fail to comply with any
|
|
29
|
+
of its terms.
|
|
30
|
+
|
|
31
|
+
5. DISCLAIMER OF WARRANTIES
|
|
32
|
+
THIS SOFTWARE IS PROVIDED "AS IS" WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
33
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
34
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
|
|
35
|
+
|
|
36
|
+
6. LIMITATION OF LIABILITY
|
|
37
|
+
IN NO EVENT SHALL FLOWDOT LLC BE LIABLE FOR ANY INDIRECT, INCIDENTAL,
|
|
38
|
+
SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ARISING OUT OF OR IN
|
|
39
|
+
CONNECTION WITH THIS SOFTWARE.
|
|
40
|
+
|
|
41
|
+
7. GOVERNING LAW
|
|
42
|
+
This agreement shall be governed by the laws of the State of Delaware,
|
|
43
|
+
United States, without regard to its conflict of law provisions.
|
|
44
|
+
|
|
45
|
+
For licensing inquiries, contact: hello@flowdot.ai
|
package/README.md
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
# @flowdot.ai/daemon
|
|
2
|
+
|
|
3
|
+
Shared daemon and loop/goal management for the [FlowDot](https://flowdot.ai) platform.
|
|
4
|
+
|
|
5
|
+
Used by the FlowDot CLI and the FlowDot native desktop app to run background loops, track autonomous goals, and coordinate IPC between local clients and the FlowDot Hub.
|
|
6
|
+
|
|
7
|
+
## Features
|
|
8
|
+
|
|
9
|
+
- **Loop scheduling** — background execution of scheduled FlowDot workflows
|
|
10
|
+
- **Goal tracking** — autonomous task execution with dependency resolution, progress tracking, and error recovery
|
|
11
|
+
- **IPC** — daemon-to-client communication over local sockets
|
|
12
|
+
- **Notifications** — terminal, webhook, and Slack delivery
|
|
13
|
+
- **State management** — persistent daemon state with crash recovery
|
|
14
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
```bash
|
|
18
|
+
npm install @flowdot.ai/daemon
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
`@flowdot.ai/api` is an optional peer dependency — install it if you want to use the api-backed features.
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npm install @flowdot.ai/daemon @flowdot.ai/api
|
|
25
|
+
```
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```typescript
|
|
30
|
+
import {
|
|
31
|
+
LoopDaemon,
|
|
32
|
+
LoopManager,
|
|
33
|
+
IPCClient,
|
|
34
|
+
createNotifier,
|
|
35
|
+
GoalManager,
|
|
36
|
+
TaskGenerator,
|
|
37
|
+
TaskExecutor,
|
|
38
|
+
} from '@flowdot.ai/daemon';
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
See the [FlowDot CLI source](https://github.com/ElliotTheGreek/flowdot-cli) for end-to-end integration examples.
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
|
|
45
|
+
- Node.js ≥ 20.0.0
|
|
46
|
+
|
|
47
|
+
## License
|
|
48
|
+
|
|
49
|
+
Proprietary — see [LICENSE](./LICENSE).
|
|
50
|
+
|
|
51
|
+
Copyright © 2026 FlowDot LLC. All rights reserved.
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { Goal, Task, GoalHash, Logger } from './types.js';
|
|
2
|
+
import { GoalError } from './errors.js';
|
|
3
|
+
export interface DependencyNode {
|
|
4
|
+
readonly id: string;
|
|
5
|
+
readonly dependsOn: string[];
|
|
6
|
+
readonly dependents: string[];
|
|
7
|
+
readonly status: 'pending' | 'ready' | 'blocked' | 'completed';
|
|
8
|
+
readonly blockReason?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface GoalResolutionResult {
|
|
11
|
+
readonly orderedGoals: GoalHash[];
|
|
12
|
+
readonly blockedGoals: Map<GoalHash, string>;
|
|
13
|
+
readonly circularGoals: GoalHash[];
|
|
14
|
+
readonly graph: Map<GoalHash, DependencyNode>;
|
|
15
|
+
readonly success: boolean;
|
|
16
|
+
readonly errors: string[];
|
|
17
|
+
}
|
|
18
|
+
export interface TaskDependencyNode {
|
|
19
|
+
readonly taskId: number;
|
|
20
|
+
readonly goalHash: GoalHash;
|
|
21
|
+
readonly title: string;
|
|
22
|
+
readonly dependsOn: number[];
|
|
23
|
+
readonly isReady: boolean;
|
|
24
|
+
readonly blockReason?: string;
|
|
25
|
+
}
|
|
26
|
+
export interface DependencyResolverOptions {
|
|
27
|
+
readonly logger?: Logger;
|
|
28
|
+
}
|
|
29
|
+
export declare class DependencyResolutionError extends GoalError {
|
|
30
|
+
constructor(message: string, cause?: Error);
|
|
31
|
+
}
|
|
32
|
+
export declare class DependencyResolver {
|
|
33
|
+
private readonly logger;
|
|
34
|
+
constructor(options?: DependencyResolverOptions);
|
|
35
|
+
resolveGoals(goals: Goal[]): GoalResolutionResult;
|
|
36
|
+
canStartGoal(goal: Goal, allGoals: Goal[]): {
|
|
37
|
+
canStart: boolean;
|
|
38
|
+
reason?: string;
|
|
39
|
+
waitingOn?: GoalHash[];
|
|
40
|
+
};
|
|
41
|
+
getBlockedBy(goalHash: GoalHash, allGoals: Goal[]): GoalHash[];
|
|
42
|
+
wouldCreateCycle(fromGoal: GoalHash, toGoal: GoalHash, allGoals: Goal[]): boolean;
|
|
43
|
+
resolveTasks(tasks: Task[], _goalHash: GoalHash): {
|
|
44
|
+
orderedTasks: Task[];
|
|
45
|
+
blocked: Map<number, string>;
|
|
46
|
+
};
|
|
47
|
+
getTaskExecutionOrder(tasks: Task[], milestoneOrder: number[]): Task[];
|
|
48
|
+
private detectCircularDependencies;
|
|
49
|
+
private topologicalSort;
|
|
50
|
+
}
|
|
51
|
+
export declare function getTransitiveDependencies(goalHash: GoalHash, allGoals: Goal[]): GoalHash[];
|
|
52
|
+
export declare function getTransitiveDependents(goalHash: GoalHash, allGoals: Goal[]): GoalHash[];
|
|
53
|
+
export declare function calculateCriticalPath(goals: Goal[]): GoalHash[];
|
|
54
|
+
export declare function createDependencyResolver(options?: DependencyResolverOptions): DependencyResolver;
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
function getPriorityWeight(priority) {
|
|
2
|
+
switch (priority) {
|
|
3
|
+
case 'high': return 3;
|
|
4
|
+
case 'medium': return 2;
|
|
5
|
+
case 'low': return 1;
|
|
6
|
+
default: return 0;
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
import { GoalError } from './errors.js';
|
|
10
|
+
const noopLogger = {
|
|
11
|
+
debug: () => { },
|
|
12
|
+
info: () => { },
|
|
13
|
+
warn: () => { },
|
|
14
|
+
error: () => { },
|
|
15
|
+
};
|
|
16
|
+
export class DependencyResolutionError extends GoalError {
|
|
17
|
+
constructor(message, cause) {
|
|
18
|
+
super('DEPENDENCY_RESOLUTION_ERROR', message, cause ? { cause } : {});
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
export class DependencyResolver {
|
|
22
|
+
logger;
|
|
23
|
+
constructor(options = {}) {
|
|
24
|
+
this.logger = options.logger ?? noopLogger;
|
|
25
|
+
}
|
|
26
|
+
resolveGoals(goals) {
|
|
27
|
+
const graph = new Map();
|
|
28
|
+
const errors = [];
|
|
29
|
+
for (const goal of goals) {
|
|
30
|
+
const dependsOn = goal.dependsOn ?? [];
|
|
31
|
+
graph.set(goal.hash, {
|
|
32
|
+
id: goal.hash,
|
|
33
|
+
dependsOn,
|
|
34
|
+
dependents: [],
|
|
35
|
+
status: goal.status === 'completed' ? 'completed' : 'pending',
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
for (const node of graph.values()) {
|
|
39
|
+
for (const depId of node.dependsOn) {
|
|
40
|
+
const depNode = graph.get(depId);
|
|
41
|
+
if (depNode) {
|
|
42
|
+
depNode.dependents.push(node.id);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
const circularGoals = this.detectCircularDependencies(graph);
|
|
47
|
+
if (circularGoals.length > 0) {
|
|
48
|
+
errors.push(`Circular dependencies detected: ${circularGoals.join(' -> ')}`);
|
|
49
|
+
}
|
|
50
|
+
const blockedGoals = new Map();
|
|
51
|
+
for (const [hash, node] of graph) {
|
|
52
|
+
if (circularGoals.includes(hash)) {
|
|
53
|
+
blockedGoals.set(hash, 'Circular dependency detected');
|
|
54
|
+
node.status = 'blocked';
|
|
55
|
+
node.blockReason =
|
|
56
|
+
'Circular dependency detected';
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
const incompleteDeps = [];
|
|
60
|
+
for (const depId of node.dependsOn) {
|
|
61
|
+
const depNode = graph.get(depId);
|
|
62
|
+
if (depNode && depNode.status !== 'completed') {
|
|
63
|
+
incompleteDeps.push(depId);
|
|
64
|
+
}
|
|
65
|
+
else if (!depNode) {
|
|
66
|
+
incompleteDeps.push(`${depId} (not found)`);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
if (incompleteDeps.length > 0) {
|
|
70
|
+
blockedGoals.set(hash, `Waiting on: ${incompleteDeps.join(', ')}`);
|
|
71
|
+
node.status = 'blocked';
|
|
72
|
+
node.blockReason =
|
|
73
|
+
`Waiting on: ${incompleteDeps.join(', ')}`;
|
|
74
|
+
}
|
|
75
|
+
else if (node.status !== 'completed') {
|
|
76
|
+
node.status = 'ready';
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
const orderedGoals = this.topologicalSort(graph, circularGoals);
|
|
80
|
+
this.logger.debug('DEPENDENCY_RESOLVER', 'Goals resolved', {
|
|
81
|
+
total: goals.length,
|
|
82
|
+
ready: orderedGoals.length,
|
|
83
|
+
blocked: blockedGoals.size,
|
|
84
|
+
circular: circularGoals.length,
|
|
85
|
+
});
|
|
86
|
+
return {
|
|
87
|
+
orderedGoals,
|
|
88
|
+
blockedGoals,
|
|
89
|
+
circularGoals,
|
|
90
|
+
graph,
|
|
91
|
+
success: errors.length === 0,
|
|
92
|
+
errors,
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
canStartGoal(goal, allGoals) {
|
|
96
|
+
if (!goal.dependsOn || goal.dependsOn.length === 0) {
|
|
97
|
+
return { canStart: true };
|
|
98
|
+
}
|
|
99
|
+
const waitingOn = [];
|
|
100
|
+
for (const depHash of goal.dependsOn) {
|
|
101
|
+
const depGoal = allGoals.find((g) => g.hash === depHash);
|
|
102
|
+
if (!depGoal) {
|
|
103
|
+
waitingOn.push(depHash);
|
|
104
|
+
}
|
|
105
|
+
else if (depGoal.status !== 'completed') {
|
|
106
|
+
waitingOn.push(depHash);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
if (waitingOn.length > 0) {
|
|
110
|
+
return {
|
|
111
|
+
canStart: false,
|
|
112
|
+
reason: `Waiting on ${waitingOn.length} dependent goal(s)`,
|
|
113
|
+
waitingOn,
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { canStart: true };
|
|
117
|
+
}
|
|
118
|
+
getBlockedBy(goalHash, allGoals) {
|
|
119
|
+
const blocked = [];
|
|
120
|
+
for (const goal of allGoals) {
|
|
121
|
+
if (goal.dependsOn?.includes(goalHash)) {
|
|
122
|
+
blocked.push(goal.hash);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
return blocked;
|
|
126
|
+
}
|
|
127
|
+
wouldCreateCycle(fromGoal, toGoal, allGoals) {
|
|
128
|
+
const visited = new Set();
|
|
129
|
+
const stack = [toGoal];
|
|
130
|
+
while (stack.length > 0) {
|
|
131
|
+
const current = stack.pop();
|
|
132
|
+
if (visited.has(current)) {
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
visited.add(current);
|
|
136
|
+
if (current === fromGoal) {
|
|
137
|
+
return true;
|
|
138
|
+
}
|
|
139
|
+
const goal = allGoals.find((g) => g.hash === current);
|
|
140
|
+
if (goal?.dependsOn) {
|
|
141
|
+
for (const dep of goal.dependsOn) {
|
|
142
|
+
stack.push(dep);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return false;
|
|
147
|
+
}
|
|
148
|
+
resolveTasks(tasks, _goalHash) {
|
|
149
|
+
const blocked = new Map();
|
|
150
|
+
const completed = new Set();
|
|
151
|
+
const pending = [];
|
|
152
|
+
for (const task of tasks) {
|
|
153
|
+
if (task.status === 'completed') {
|
|
154
|
+
completed.add(task.id);
|
|
155
|
+
}
|
|
156
|
+
else if (task.status === 'pending' || task.status === 'awaiting_approval') {
|
|
157
|
+
pending.push(task);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
const ready = [];
|
|
161
|
+
for (const task of pending) {
|
|
162
|
+
ready.push(task);
|
|
163
|
+
}
|
|
164
|
+
ready.sort((a, b) => getPriorityWeight(b.priority) - getPriorityWeight(a.priority));
|
|
165
|
+
return {
|
|
166
|
+
orderedTasks: ready,
|
|
167
|
+
blocked,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
getTaskExecutionOrder(tasks, milestoneOrder) {
|
|
171
|
+
const byMilestone = new Map();
|
|
172
|
+
for (const task of tasks) {
|
|
173
|
+
const key = task.milestoneId ?? null;
|
|
174
|
+
if (!byMilestone.has(key)) {
|
|
175
|
+
byMilestone.set(key, []);
|
|
176
|
+
}
|
|
177
|
+
byMilestone.get(key).push(task);
|
|
178
|
+
}
|
|
179
|
+
const ordered = [];
|
|
180
|
+
const general = byMilestone.get(null) ?? [];
|
|
181
|
+
general.sort((a, b) => getPriorityWeight(b.priority) - getPriorityWeight(a.priority));
|
|
182
|
+
ordered.push(...general);
|
|
183
|
+
for (const milestoneId of milestoneOrder) {
|
|
184
|
+
const milestoneTasks = byMilestone.get(milestoneId) ?? [];
|
|
185
|
+
milestoneTasks.sort((a, b) => getPriorityWeight(b.priority) - getPriorityWeight(a.priority));
|
|
186
|
+
ordered.push(...milestoneTasks);
|
|
187
|
+
}
|
|
188
|
+
return ordered;
|
|
189
|
+
}
|
|
190
|
+
detectCircularDependencies(graph) {
|
|
191
|
+
const circularGoals = [];
|
|
192
|
+
const visited = new Set();
|
|
193
|
+
const recursionStack = new Set();
|
|
194
|
+
const dfs = (nodeId, path) => {
|
|
195
|
+
if (recursionStack.has(nodeId)) {
|
|
196
|
+
const cycleStart = path.indexOf(nodeId);
|
|
197
|
+
const cycle = path.slice(cycleStart);
|
|
198
|
+
cycle.forEach((id) => {
|
|
199
|
+
if (!circularGoals.includes(id)) {
|
|
200
|
+
circularGoals.push(id);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
return true;
|
|
204
|
+
}
|
|
205
|
+
if (visited.has(nodeId)) {
|
|
206
|
+
return false;
|
|
207
|
+
}
|
|
208
|
+
visited.add(nodeId);
|
|
209
|
+
recursionStack.add(nodeId);
|
|
210
|
+
path.push(nodeId);
|
|
211
|
+
const node = graph.get(nodeId);
|
|
212
|
+
if (node) {
|
|
213
|
+
for (const depId of node.dependsOn) {
|
|
214
|
+
if (dfs(depId, [...path])) {
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
recursionStack.delete(nodeId);
|
|
219
|
+
return false;
|
|
220
|
+
};
|
|
221
|
+
for (const nodeId of graph.keys()) {
|
|
222
|
+
if (!visited.has(nodeId)) {
|
|
223
|
+
dfs(nodeId, []);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return circularGoals;
|
|
227
|
+
}
|
|
228
|
+
topologicalSort(graph, exclude) {
|
|
229
|
+
const result = [];
|
|
230
|
+
const visited = new Set();
|
|
231
|
+
const temp = new Set();
|
|
232
|
+
const visit = (nodeId) => {
|
|
233
|
+
if (visited.has(nodeId) || exclude.includes(nodeId)) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
if (temp.has(nodeId)) {
|
|
237
|
+
return;
|
|
238
|
+
}
|
|
239
|
+
temp.add(nodeId);
|
|
240
|
+
const node = graph.get(nodeId);
|
|
241
|
+
if (node) {
|
|
242
|
+
for (const depId of node.dependsOn) {
|
|
243
|
+
visit(depId);
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
temp.delete(nodeId);
|
|
247
|
+
visited.add(nodeId);
|
|
248
|
+
if (node && node.status === 'ready') {
|
|
249
|
+
result.push(nodeId);
|
|
250
|
+
}
|
|
251
|
+
};
|
|
252
|
+
for (const nodeId of graph.keys()) {
|
|
253
|
+
visit(nodeId);
|
|
254
|
+
}
|
|
255
|
+
return result;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
export function getTransitiveDependencies(goalHash, allGoals) {
|
|
259
|
+
const result = new Set();
|
|
260
|
+
const stack = [goalHash];
|
|
261
|
+
while (stack.length > 0) {
|
|
262
|
+
const current = stack.pop();
|
|
263
|
+
const goal = allGoals.find((g) => g.hash === current);
|
|
264
|
+
if (goal?.dependsOn) {
|
|
265
|
+
for (const dep of goal.dependsOn) {
|
|
266
|
+
if (!result.has(dep)) {
|
|
267
|
+
result.add(dep);
|
|
268
|
+
stack.push(dep);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
return Array.from(result);
|
|
274
|
+
}
|
|
275
|
+
export function getTransitiveDependents(goalHash, allGoals) {
|
|
276
|
+
const result = new Set();
|
|
277
|
+
const stack = [goalHash];
|
|
278
|
+
while (stack.length > 0) {
|
|
279
|
+
const current = stack.pop();
|
|
280
|
+
for (const goal of allGoals) {
|
|
281
|
+
if (goal.dependsOn?.includes(current) && !result.has(goal.hash)) {
|
|
282
|
+
result.add(goal.hash);
|
|
283
|
+
stack.push(goal.hash);
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
return Array.from(result);
|
|
288
|
+
}
|
|
289
|
+
export function calculateCriticalPath(goals) {
|
|
290
|
+
const endNodes = goals.filter((g) => {
|
|
291
|
+
const hasDependents = goals.some((other) => other.dependsOn?.includes(g.hash));
|
|
292
|
+
return !hasDependents && g.status !== 'completed';
|
|
293
|
+
});
|
|
294
|
+
if (endNodes.length === 0) {
|
|
295
|
+
return [];
|
|
296
|
+
}
|
|
297
|
+
let longestPath = [];
|
|
298
|
+
for (const endNode of endNodes) {
|
|
299
|
+
const path = findLongestPath(endNode.hash, goals, new Set());
|
|
300
|
+
if (path.length > longestPath.length) {
|
|
301
|
+
longestPath = path;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
return longestPath.reverse();
|
|
305
|
+
}
|
|
306
|
+
function findLongestPath(goalHash, allGoals, visited) {
|
|
307
|
+
if (visited.has(goalHash)) {
|
|
308
|
+
return [];
|
|
309
|
+
}
|
|
310
|
+
visited.add(goalHash);
|
|
311
|
+
const goal = allGoals.find((g) => g.hash === goalHash);
|
|
312
|
+
if (!goal) {
|
|
313
|
+
return [goalHash];
|
|
314
|
+
}
|
|
315
|
+
if (!goal.dependsOn || goal.dependsOn.length === 0) {
|
|
316
|
+
return [goalHash];
|
|
317
|
+
}
|
|
318
|
+
let longestDepPath = [];
|
|
319
|
+
for (const depHash of goal.dependsOn) {
|
|
320
|
+
const depPath = findLongestPath(depHash, allGoals, new Set(visited));
|
|
321
|
+
if (depPath.length > longestDepPath.length) {
|
|
322
|
+
longestDepPath = depPath;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
return [goalHash, ...longestDepPath];
|
|
326
|
+
}
|
|
327
|
+
export function createDependencyResolver(options = {}) {
|
|
328
|
+
return new DependencyResolver(options);
|
|
329
|
+
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { Goal, GoalHash, Task, TaskStatus, Logger } from './types.js';
|
|
3
|
+
import { GoalError } from './errors.js';
|
|
4
|
+
import type { ExecutionResult } from './TaskExecutor.js';
|
|
5
|
+
export type RecoveryStrategy = 'retry' | 'skip' | 'rollback' | 'escalate' | 'abort';
|
|
6
|
+
export type ErrorCategory = 'transient' | 'permission' | 'validation' | 'resource' | 'timeout' | 'network' | 'system' | 'unknown';
|
|
7
|
+
export interface Checkpoint {
|
|
8
|
+
readonly id: string;
|
|
9
|
+
readonly goalHash: GoalHash;
|
|
10
|
+
readonly taskId: number;
|
|
11
|
+
readonly taskStatus: TaskStatus;
|
|
12
|
+
readonly timestamp: Date;
|
|
13
|
+
readonly stateData: CheckpointState;
|
|
14
|
+
readonly description?: string;
|
|
15
|
+
}
|
|
16
|
+
export interface CheckpointState {
|
|
17
|
+
readonly completedTasks: number[];
|
|
18
|
+
readonly pendingTasks: number[];
|
|
19
|
+
readonly customData?: Record<string, unknown>;
|
|
20
|
+
}
|
|
21
|
+
export interface ErrorAnalysis {
|
|
22
|
+
readonly category: ErrorCategory;
|
|
23
|
+
readonly rootCause: string;
|
|
24
|
+
readonly isRecoverable: boolean;
|
|
25
|
+
readonly recommendedStrategy: RecoveryStrategy;
|
|
26
|
+
readonly confidence: number;
|
|
27
|
+
readonly suggestions: string[];
|
|
28
|
+
readonly relatedErrors?: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface RecoveryAction {
|
|
31
|
+
readonly type: RecoveryStrategy;
|
|
32
|
+
readonly taskId: number;
|
|
33
|
+
readonly goalHash: GoalHash;
|
|
34
|
+
readonly attempt: number;
|
|
35
|
+
readonly maxAttempts: number;
|
|
36
|
+
readonly delayMs: number;
|
|
37
|
+
readonly checkpointId?: string;
|
|
38
|
+
readonly timestamp: Date;
|
|
39
|
+
}
|
|
40
|
+
export interface RecoveryResult {
|
|
41
|
+
readonly success: boolean;
|
|
42
|
+
readonly strategy: RecoveryStrategy;
|
|
43
|
+
readonly details: string;
|
|
44
|
+
readonly newStatus?: TaskStatus;
|
|
45
|
+
readonly shouldContinue: boolean;
|
|
46
|
+
}
|
|
47
|
+
export interface RetryConfig {
|
|
48
|
+
readonly maxAttempts: number;
|
|
49
|
+
readonly baseDelayMs: number;
|
|
50
|
+
readonly multiplier: number;
|
|
51
|
+
readonly maxDelayMs: number;
|
|
52
|
+
readonly jitter: number;
|
|
53
|
+
}
|
|
54
|
+
export interface ErrorRecoveryOptions {
|
|
55
|
+
readonly logger?: Logger;
|
|
56
|
+
readonly retryConfig?: Partial<RetryConfig>;
|
|
57
|
+
readonly maxCheckpointsPerGoal?: number;
|
|
58
|
+
readonly autoRetryTransient?: boolean;
|
|
59
|
+
readonly escalateThreshold?: number;
|
|
60
|
+
}
|
|
61
|
+
export interface ErrorRecoveryEvents {
|
|
62
|
+
'checkpoint-created': [Checkpoint];
|
|
63
|
+
'checkpoint-restored': [Checkpoint];
|
|
64
|
+
'error-analyzed': [ErrorAnalysis, ExecutionResult];
|
|
65
|
+
'recovery-started': [RecoveryAction];
|
|
66
|
+
'recovery-completed': [RecoveryResult, RecoveryAction];
|
|
67
|
+
'recovery-failed': [RecoveryResult, RecoveryAction];
|
|
68
|
+
'escalation-triggered': [{
|
|
69
|
+
goalHash: GoalHash;
|
|
70
|
+
taskId: number;
|
|
71
|
+
reason: string;
|
|
72
|
+
}];
|
|
73
|
+
'error': [Error];
|
|
74
|
+
}
|
|
75
|
+
export declare class ErrorRecoveryError extends GoalError {
|
|
76
|
+
constructor(message: string, cause?: Error);
|
|
77
|
+
}
|
|
78
|
+
export declare class CheckpointNotFoundError extends GoalError {
|
|
79
|
+
readonly checkpointId: string;
|
|
80
|
+
constructor(checkpointId: string);
|
|
81
|
+
}
|
|
82
|
+
export declare class ErrorRecovery extends EventEmitter<ErrorRecoveryEvents> {
|
|
83
|
+
private readonly logger;
|
|
84
|
+
private readonly retryConfig;
|
|
85
|
+
private readonly maxCheckpointsPerGoal;
|
|
86
|
+
private readonly autoRetryTransient;
|
|
87
|
+
private readonly escalateThreshold;
|
|
88
|
+
private readonly checkpoints;
|
|
89
|
+
private readonly failureCounts;
|
|
90
|
+
private readonly retryState;
|
|
91
|
+
constructor(options?: ErrorRecoveryOptions);
|
|
92
|
+
createCheckpoint(goal: Goal, task: Task, state: CheckpointState, description?: string): Checkpoint;
|
|
93
|
+
getCheckpoints(goalHash: GoalHash): Checkpoint[];
|
|
94
|
+
getCheckpoint(checkpointId: string): Checkpoint | null;
|
|
95
|
+
restoreCheckpoint(checkpointId: string, restoreFunction: (state: CheckpointState) => Promise<void>): Promise<void>;
|
|
96
|
+
clearCheckpoints(goalHash: GoalHash): void;
|
|
97
|
+
analyzeError(result: ExecutionResult): ErrorAnalysis;
|
|
98
|
+
private categorizeError;
|
|
99
|
+
private isRecoverable;
|
|
100
|
+
private recommendStrategy;
|
|
101
|
+
private generateSuggestions;
|
|
102
|
+
private extractRootCause;
|
|
103
|
+
private calculateConfidence;
|
|
104
|
+
executeRecovery(action: RecoveryAction, handlers: {
|
|
105
|
+
retry: (taskId: number) => Promise<ExecutionResult>;
|
|
106
|
+
skip: (taskId: number) => Promise<void>;
|
|
107
|
+
rollback: (checkpointId: string) => Promise<void>;
|
|
108
|
+
escalate: (taskId: number, goalHash: GoalHash) => Promise<void>;
|
|
109
|
+
abort: (goalHash: GoalHash) => Promise<void>;
|
|
110
|
+
}): Promise<RecoveryResult>;
|
|
111
|
+
createRecoveryAction(strategy: RecoveryStrategy, taskId: number, goalHash: GoalHash, options?: {
|
|
112
|
+
checkpointId?: string;
|
|
113
|
+
customDelay?: number;
|
|
114
|
+
}): RecoveryAction;
|
|
115
|
+
private calculateRetryDelay;
|
|
116
|
+
private updateRetryState;
|
|
117
|
+
canRetry(taskId: number): boolean;
|
|
118
|
+
getRemainingRetries(taskId: number): number;
|
|
119
|
+
resetRetryState(taskId: number): void;
|
|
120
|
+
recordFailure(taskId: number): number;
|
|
121
|
+
getFailureCount(taskId: number): number;
|
|
122
|
+
shouldEscalate(taskId: number): boolean;
|
|
123
|
+
private generateCheckpointId;
|
|
124
|
+
private delay;
|
|
125
|
+
clearAll(): void;
|
|
126
|
+
getStats(): {
|
|
127
|
+
totalCheckpoints: number;
|
|
128
|
+
goalCheckpoints: Map<GoalHash, number>;
|
|
129
|
+
failedTasks: number;
|
|
130
|
+
pendingRetries: number;
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
export declare function createErrorRecovery(options?: ErrorRecoveryOptions): ErrorRecovery;
|