hzl-core 2.0.0 → 2.2.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/dist/__tests__/backup/backup-restore.test.js +2 -0
- package/dist/__tests__/backup/backup-restore.test.js.map +1 -1
- package/dist/__tests__/backup/import-export.test.js +2 -0
- package/dist/__tests__/backup/import-export.test.js.map +1 -1
- package/dist/__tests__/concurrency/stress.test.js +3 -1
- package/dist/__tests__/concurrency/stress.test.js.map +1 -1
- package/dist/__tests__/properties/invariants.test.js +126 -0
- package/dist/__tests__/properties/invariants.test.js.map +1 -1
- package/dist/db/__tests__/datastore.test.js +14 -0
- package/dist/db/__tests__/datastore.test.js.map +1 -1
- package/dist/db/migrations/index.d.ts.map +1 -1
- package/dist/db/migrations/index.js +44 -0
- package/dist/db/migrations/index.js.map +1 -1
- package/dist/db/schema.d.ts +1 -1
- package/dist/db/schema.d.ts.map +1 -1
- package/dist/db/schema.js +41 -0
- package/dist/db/schema.js.map +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/index.test.js +4 -0
- package/dist/index.test.js.map +1 -1
- package/dist/projections/rebuild.d.ts.map +1 -1
- package/dist/projections/rebuild.js +29 -17
- package/dist/projections/rebuild.js.map +1 -1
- package/dist/services/backup-service.d.ts.map +1 -1
- package/dist/services/backup-service.js +2 -0
- package/dist/services/backup-service.js.map +1 -1
- package/dist/services/hook-drain-service.d.ts +58 -0
- package/dist/services/hook-drain-service.d.ts.map +1 -0
- package/dist/services/hook-drain-service.js +388 -0
- package/dist/services/hook-drain-service.js.map +1 -0
- package/dist/services/hook-drain-service.test.d.ts +2 -0
- package/dist/services/hook-drain-service.test.d.ts.map +1 -0
- package/dist/services/hook-drain-service.test.js +167 -0
- package/dist/services/hook-drain-service.test.js.map +1 -0
- package/dist/services/search-service.d.ts +1 -0
- package/dist/services/search-service.d.ts.map +1 -1
- package/dist/services/search-service.js +12 -14
- package/dist/services/search-service.js.map +1 -1
- package/dist/services/search-service.test.js +14 -1
- package/dist/services/search-service.test.js.map +1 -1
- package/dist/services/task-service.d.ts +26 -4
- package/dist/services/task-service.d.ts.map +1 -1
- package/dist/services/task-service.js +97 -35
- package/dist/services/task-service.js.map +1 -1
- package/dist/services/task-service.test.js +87 -10
- package/dist/services/task-service.test.js.map +1 -1
- package/dist/services/workflow-service.d.ts +141 -0
- package/dist/services/workflow-service.d.ts.map +1 -0
- package/dist/services/workflow-service.js +664 -0
- package/dist/services/workflow-service.js.map +1 -0
- package/dist/services/workflow-service.test.d.ts +2 -0
- package/dist/services/workflow-service.test.d.ts.map +1 -0
- package/dist/services/workflow-service.test.js +213 -0
- package/dist/services/workflow-service.test.js.map +1 -0
- package/package.json +2 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"workflow-service.d.ts","sourceRoot":"","sources":["../../src/services/workflow-service.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,664 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto';
|
|
2
|
+
import { EventType, TaskStatus } from '../events/types.js';
|
|
3
|
+
import { TaskNotFoundError, } from './task-service.js';
|
|
4
|
+
const WORKFLOW_OP_PROCESSING_STALE_MS = 30 * 60 * 1000;
|
|
5
|
+
const WORKFLOW_DEFINITIONS = {
|
|
6
|
+
start: {
|
|
7
|
+
name: 'start',
|
|
8
|
+
description: 'Resume in-progress work for an agent, otherwise claim next eligible task.',
|
|
9
|
+
supports_auto_op_id: false,
|
|
10
|
+
args: [
|
|
11
|
+
{ name: '--agent <name>', required: true, description: 'Agent identity to resume/claim for.' },
|
|
12
|
+
{ name: '--project <project>', required: false, description: 'Optional project filter.' },
|
|
13
|
+
{ name: '--tags <csv>', required: false, description: 'Optional required tags filter.' },
|
|
14
|
+
{ name: '--lease <minutes>', required: false, description: 'Optional lease refresh/claim duration.' },
|
|
15
|
+
{
|
|
16
|
+
name: '--resume-policy <policy>',
|
|
17
|
+
required: false,
|
|
18
|
+
description: 'Resume policy: first | latest | priority.',
|
|
19
|
+
default: 'priority',
|
|
20
|
+
},
|
|
21
|
+
{ name: '--op-id <key>', required: false, description: 'Explicit idempotency key for retries.' },
|
|
22
|
+
],
|
|
23
|
+
notes: [
|
|
24
|
+
'--auto-op-id is intentionally unsupported for start because polling calls may legitimately return different tasks over time.',
|
|
25
|
+
'Alternates are bounded by others_limit unless explicitly set to all.',
|
|
26
|
+
],
|
|
27
|
+
},
|
|
28
|
+
handoff: {
|
|
29
|
+
name: 'handoff',
|
|
30
|
+
description: 'Complete a source task and create a follow-on task with carried checkpoint context.',
|
|
31
|
+
supports_auto_op_id: true,
|
|
32
|
+
args: [
|
|
33
|
+
{ name: '--from <task-id>', required: true, description: 'Source task id.' },
|
|
34
|
+
{ name: '--title <title>', required: true, description: 'Follow-on task title.' },
|
|
35
|
+
{ name: '--project <project>', required: false, description: 'Optional target project.' },
|
|
36
|
+
{ name: '--agent <agent>', required: false, description: 'Optional follow-on assignee.' },
|
|
37
|
+
{ name: '--carry-checkpoints <n>', required: false, description: 'Number of checkpoints to carry.', default: '3' },
|
|
38
|
+
{ name: '--carry-max-chars <n>', required: false, description: 'Maximum carried context chars.', default: '4000' },
|
|
39
|
+
{ name: '--op-id <key>', required: false, description: 'Explicit idempotency key.' },
|
|
40
|
+
{ name: '--auto-op-id', required: false, description: 'Generate deterministic idempotency key from normalized input.' },
|
|
41
|
+
],
|
|
42
|
+
notes: [
|
|
43
|
+
'Guardrail: requires --agent, --project, or both to avoid accidental implicit queue routing.',
|
|
44
|
+
],
|
|
45
|
+
},
|
|
46
|
+
delegate: {
|
|
47
|
+
name: 'delegate',
|
|
48
|
+
description: 'Create delegated work from a source task, with dependency gating by default.',
|
|
49
|
+
supports_auto_op_id: true,
|
|
50
|
+
args: [
|
|
51
|
+
{ name: '--from <task-id>', required: true, description: 'Source task id.' },
|
|
52
|
+
{ name: '--title <title>', required: true, description: 'Delegated task title.' },
|
|
53
|
+
{ name: '--project <project>', required: false, description: 'Optional target project.' },
|
|
54
|
+
{ name: '--agent <agent>', required: false, description: 'Optional delegated assignee.' },
|
|
55
|
+
{ name: '--no-depends', required: false, description: 'Disable default parent->delegated dependency edge.' },
|
|
56
|
+
{ name: '--checkpoint <text>', required: false, description: 'Checkpoint text recorded on source task.' },
|
|
57
|
+
{ name: '--pause-parent', required: false, description: 'Set parent task to blocked when currently in_progress.' },
|
|
58
|
+
{ name: '--op-id <key>', required: false, description: 'Explicit idempotency key.' },
|
|
59
|
+
{ name: '--auto-op-id', required: false, description: 'Generate deterministic idempotency key from normalized input.' },
|
|
60
|
+
],
|
|
61
|
+
notes: [
|
|
62
|
+
'Default dependency edge provides availability gating; strict blocking requires --pause-parent.',
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
export class WorkflowService {
|
|
67
|
+
cacheDb;
|
|
68
|
+
eventStore;
|
|
69
|
+
projectionEngine;
|
|
70
|
+
taskService;
|
|
71
|
+
idempotencyDb;
|
|
72
|
+
constructor(cacheDb, eventStore, projectionEngine, taskService, idempotencyDb) {
|
|
73
|
+
this.cacheDb = cacheDb;
|
|
74
|
+
this.eventStore = eventStore;
|
|
75
|
+
this.projectionEngine = projectionEngine;
|
|
76
|
+
this.taskService = taskService;
|
|
77
|
+
this.idempotencyDb = idempotencyDb;
|
|
78
|
+
}
|
|
79
|
+
listWorkflows() {
|
|
80
|
+
return Object.values(WORKFLOW_DEFINITIONS).map((definition) => ({
|
|
81
|
+
name: definition.name,
|
|
82
|
+
description: definition.description,
|
|
83
|
+
}));
|
|
84
|
+
}
|
|
85
|
+
showWorkflow(name) {
|
|
86
|
+
return WORKFLOW_DEFINITIONS[name];
|
|
87
|
+
}
|
|
88
|
+
runStart(input) {
|
|
89
|
+
const resumePolicy = input.resume_policy ?? 'priority';
|
|
90
|
+
const includeOthers = input.include_others ?? true;
|
|
91
|
+
const othersLimit = this.normalizeOthersLimit(input.others_limit);
|
|
92
|
+
if (input.auto_op_id) {
|
|
93
|
+
throw new Error('--auto-op-id is not supported for workflow run start; use --op-id only for intentional retries.');
|
|
94
|
+
}
|
|
95
|
+
return this.withIdempotency('start', {
|
|
96
|
+
agent: input.agent,
|
|
97
|
+
project: input.project ?? null,
|
|
98
|
+
tags: [...(input.tags ?? [])].sort(),
|
|
99
|
+
lease_minutes: input.lease_minutes ?? null,
|
|
100
|
+
resume_policy: resumePolicy,
|
|
101
|
+
include_others: includeOthers,
|
|
102
|
+
others_limit: othersLimit,
|
|
103
|
+
}, input.op_id, false, () => {
|
|
104
|
+
const resumeCandidates = this.getInProgressCandidates({
|
|
105
|
+
agent: input.agent,
|
|
106
|
+
project: input.project,
|
|
107
|
+
tags: input.tags,
|
|
108
|
+
resume_policy: resumePolicy,
|
|
109
|
+
});
|
|
110
|
+
const selectedResume = resumeCandidates[0];
|
|
111
|
+
if (selectedResume) {
|
|
112
|
+
let selectedTask = this.taskService.getTaskById(selectedResume.task_id);
|
|
113
|
+
if (input.lease_minutes !== undefined) {
|
|
114
|
+
selectedTask = this.refreshLease(selectedTask, input.agent, input.lease_minutes);
|
|
115
|
+
}
|
|
116
|
+
const otherCandidates = resumeCandidates.slice(1).map((task) => this.toTaskView(task));
|
|
117
|
+
return {
|
|
118
|
+
workflow: 'start',
|
|
119
|
+
mode: 'resume',
|
|
120
|
+
selected: this.toTaskView(selectedTask),
|
|
121
|
+
filters: this.buildFilters(input.project, input.tags),
|
|
122
|
+
in_progress_count: resumeCandidates.length,
|
|
123
|
+
others_total: otherCandidates.length,
|
|
124
|
+
others: includeOthers ? this.boundOthers(otherCandidates, othersLimit) : [],
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
const claimCandidates = this.taskService.getAvailableTasks({
|
|
128
|
+
project: input.project,
|
|
129
|
+
tagsAll: input.tags,
|
|
130
|
+
leafOnly: true,
|
|
131
|
+
});
|
|
132
|
+
let claimedTask = null;
|
|
133
|
+
let claimedIndex = -1;
|
|
134
|
+
for (let index = 0; index < claimCandidates.length; index += 1) {
|
|
135
|
+
const candidate = claimCandidates[index];
|
|
136
|
+
try {
|
|
137
|
+
claimedTask = this.taskService.claimTask(candidate.task_id, {
|
|
138
|
+
author: input.agent,
|
|
139
|
+
lease_until: input.lease_minutes !== undefined
|
|
140
|
+
? new Date(Date.now() + input.lease_minutes * 60000).toISOString()
|
|
141
|
+
: undefined,
|
|
142
|
+
});
|
|
143
|
+
claimedIndex = index;
|
|
144
|
+
break;
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
continue;
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
if (!claimedTask) {
|
|
151
|
+
return {
|
|
152
|
+
workflow: 'start',
|
|
153
|
+
mode: 'none',
|
|
154
|
+
selected: null,
|
|
155
|
+
filters: this.buildFilters(input.project, input.tags),
|
|
156
|
+
in_progress_count: 0,
|
|
157
|
+
others_total: 0,
|
|
158
|
+
others: [],
|
|
159
|
+
};
|
|
160
|
+
}
|
|
161
|
+
const alternatives = claimCandidates
|
|
162
|
+
.filter((_, index) => index !== claimedIndex)
|
|
163
|
+
.map((task) => this.toTaskView(this.taskService.getTaskById(task.task_id)));
|
|
164
|
+
return {
|
|
165
|
+
workflow: 'start',
|
|
166
|
+
mode: 'claim_next',
|
|
167
|
+
selected: this.toTaskView(claimedTask),
|
|
168
|
+
filters: this.buildFilters(input.project, input.tags),
|
|
169
|
+
in_progress_count: 0,
|
|
170
|
+
others_total: alternatives.length,
|
|
171
|
+
others: includeOthers ? this.boundOthers(alternatives, othersLimit) : [],
|
|
172
|
+
};
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
runHandoff(input) {
|
|
176
|
+
const carryCheckpoints = input.carry_checkpoints ?? 3;
|
|
177
|
+
const carryMaxChars = input.carry_max_chars ?? 4000;
|
|
178
|
+
return this.withIdempotency('handoff', {
|
|
179
|
+
from_task_id: input.from_task_id,
|
|
180
|
+
...(input.auto_op_id
|
|
181
|
+
? { from_last_event_id: this.getTaskLastEventId(input.from_task_id) }
|
|
182
|
+
: {}),
|
|
183
|
+
title: input.title,
|
|
184
|
+
project: input.project ?? null,
|
|
185
|
+
agent: input.agent ?? null,
|
|
186
|
+
carry_checkpoints: carryCheckpoints,
|
|
187
|
+
carry_max_chars: carryMaxChars,
|
|
188
|
+
}, input.op_id, Boolean(input.auto_op_id), () => {
|
|
189
|
+
const source = this.taskService.getTaskById(input.from_task_id);
|
|
190
|
+
if (!source) {
|
|
191
|
+
throw new TaskNotFoundError(input.from_task_id);
|
|
192
|
+
}
|
|
193
|
+
if (!input.agent && !input.project) {
|
|
194
|
+
throw new Error('handoff requires --agent, --project, or both. Omitting --agent creates a pool-routed task - specify --project to define the queue.');
|
|
195
|
+
}
|
|
196
|
+
if (source.status !== TaskStatus.InProgress && source.status !== TaskStatus.Blocked) {
|
|
197
|
+
throw new Error(`Cannot handoff task ${source.task_id}: status is ${source.status}, expected in_progress or blocked.`);
|
|
198
|
+
}
|
|
199
|
+
const checkpoints = this.taskService.getCheckpoints(source.task_id);
|
|
200
|
+
const carried = checkpoints.slice(-Math.max(0, carryCheckpoints));
|
|
201
|
+
const carriedText = this.buildCarriedCheckpointContext(carried, carryMaxChars);
|
|
202
|
+
const carriedDescription = this.sliceByBudget(carriedText, Math.min(2500, carryMaxChars));
|
|
203
|
+
const carriedCheckpointText = this.sliceByBudget(carriedText.length > carriedDescription.length
|
|
204
|
+
? carriedText.slice(carriedDescription.length)
|
|
205
|
+
: carriedText, Math.min(1500, Math.max(0, carryMaxChars - carriedDescription.length)));
|
|
206
|
+
const targetProject = input.project ?? source.project;
|
|
207
|
+
const description = carriedDescription
|
|
208
|
+
? `Handoff context from ${source.task_id}:\n\n${carriedDescription}`
|
|
209
|
+
: undefined;
|
|
210
|
+
const followOnTask = this.taskService.createTask({
|
|
211
|
+
title: input.title,
|
|
212
|
+
project: targetProject,
|
|
213
|
+
description,
|
|
214
|
+
agent: input.agent,
|
|
215
|
+
initial_status: TaskStatus.Ready,
|
|
216
|
+
}, { author: input.author });
|
|
217
|
+
try {
|
|
218
|
+
if (carriedCheckpointText) {
|
|
219
|
+
this.taskService.addCheckpoint(followOnTask.task_id, `Handoff context from ${source.task_id}`, { text: carriedCheckpointText }, { author: input.author });
|
|
220
|
+
}
|
|
221
|
+
// Complete source last so failures earlier do not mark source done without follow-on context.
|
|
222
|
+
this.taskService.completeTask(source.task_id, { author: input.author });
|
|
223
|
+
}
|
|
224
|
+
catch (error) {
|
|
225
|
+
// Best-effort rollback: archive the follow-on if handoff could not complete end-to-end.
|
|
226
|
+
try {
|
|
227
|
+
this.taskService.archiveTask(followOnTask.task_id, {
|
|
228
|
+
author: input.author,
|
|
229
|
+
comment: `handoff rollback: ${source.task_id} completion failed`,
|
|
230
|
+
});
|
|
231
|
+
}
|
|
232
|
+
catch {
|
|
233
|
+
// Swallow rollback failure; original error is more actionable.
|
|
234
|
+
}
|
|
235
|
+
throw error;
|
|
236
|
+
}
|
|
237
|
+
return {
|
|
238
|
+
workflow: 'handoff',
|
|
239
|
+
source_task_id: source.task_id,
|
|
240
|
+
follow_on: this.toTaskView(followOnTask),
|
|
241
|
+
carried_checkpoint_count: carried.length,
|
|
242
|
+
carried_chars: carriedText.length,
|
|
243
|
+
};
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
runDelegate(input) {
|
|
247
|
+
const addDependency = input.depends_on_parent ?? true;
|
|
248
|
+
return this.withIdempotency('delegate', {
|
|
249
|
+
from_task_id: input.from_task_id,
|
|
250
|
+
...(input.auto_op_id
|
|
251
|
+
? { from_last_event_id: this.getTaskLastEventId(input.from_task_id) }
|
|
252
|
+
: {}),
|
|
253
|
+
title: input.title,
|
|
254
|
+
project: input.project ?? null,
|
|
255
|
+
agent: input.agent ?? null,
|
|
256
|
+
depends_on_parent: addDependency,
|
|
257
|
+
checkpoint: input.checkpoint ?? null,
|
|
258
|
+
pause_parent: Boolean(input.pause_parent),
|
|
259
|
+
}, input.op_id, Boolean(input.auto_op_id), () => {
|
|
260
|
+
const source = this.taskService.getTaskById(input.from_task_id);
|
|
261
|
+
if (!source) {
|
|
262
|
+
throw new TaskNotFoundError(input.from_task_id);
|
|
263
|
+
}
|
|
264
|
+
const delegated = this.taskService.createTask({
|
|
265
|
+
title: input.title,
|
|
266
|
+
project: input.project ?? source.project,
|
|
267
|
+
agent: input.agent,
|
|
268
|
+
initial_status: TaskStatus.Ready,
|
|
269
|
+
}, { author: input.author });
|
|
270
|
+
let dependencyAdded = false;
|
|
271
|
+
let checkpointAdded = false;
|
|
272
|
+
let parentPaused = false;
|
|
273
|
+
try {
|
|
274
|
+
if (addDependency) {
|
|
275
|
+
const depEvent = this.eventStore.append({
|
|
276
|
+
task_id: source.task_id,
|
|
277
|
+
type: EventType.DependencyAdded,
|
|
278
|
+
data: { depends_on_id: delegated.task_id },
|
|
279
|
+
author: input.author,
|
|
280
|
+
});
|
|
281
|
+
this.projectionEngine.applyEvent(depEvent);
|
|
282
|
+
dependencyAdded = true;
|
|
283
|
+
}
|
|
284
|
+
if (input.checkpoint?.trim()) {
|
|
285
|
+
this.taskService.addCheckpoint(source.task_id, 'Delegated follow-on created', {
|
|
286
|
+
delegated_task_id: delegated.task_id,
|
|
287
|
+
delegated_title: delegated.title,
|
|
288
|
+
note: input.checkpoint.trim(),
|
|
289
|
+
}, { author: input.author });
|
|
290
|
+
checkpointAdded = true;
|
|
291
|
+
}
|
|
292
|
+
if (input.pause_parent && source.status === TaskStatus.InProgress) {
|
|
293
|
+
this.taskService.blockTask(source.task_id, {
|
|
294
|
+
author: input.author,
|
|
295
|
+
comment: `Delegated blocking work to ${delegated.task_id}; pause parent until dependency clears.`,
|
|
296
|
+
});
|
|
297
|
+
parentPaused = true;
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
catch (error) {
|
|
301
|
+
// Best-effort rollback for partial delegate operations.
|
|
302
|
+
if (parentPaused) {
|
|
303
|
+
try {
|
|
304
|
+
this.taskService.unblockTask(source.task_id, {
|
|
305
|
+
author: input.author,
|
|
306
|
+
comment: `delegate rollback: unable to complete delegation to ${delegated.task_id}`,
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
catch {
|
|
310
|
+
// Ignore rollback failure; preserve original error.
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
if (dependencyAdded) {
|
|
314
|
+
try {
|
|
315
|
+
const removeDepEvent = this.eventStore.append({
|
|
316
|
+
task_id: source.task_id,
|
|
317
|
+
type: EventType.DependencyRemoved,
|
|
318
|
+
data: { depends_on_id: delegated.task_id },
|
|
319
|
+
author: input.author,
|
|
320
|
+
});
|
|
321
|
+
this.projectionEngine.applyEvent(removeDepEvent);
|
|
322
|
+
}
|
|
323
|
+
catch {
|
|
324
|
+
// Ignore rollback failure; preserve original error.
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
try {
|
|
328
|
+
this.taskService.archiveTask(delegated.task_id, {
|
|
329
|
+
author: input.author,
|
|
330
|
+
comment: `delegate rollback: ${source.task_id}`,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
catch {
|
|
334
|
+
// Ignore rollback failure; preserve original error.
|
|
335
|
+
}
|
|
336
|
+
throw error;
|
|
337
|
+
}
|
|
338
|
+
return {
|
|
339
|
+
workflow: 'delegate',
|
|
340
|
+
source_task_id: source.task_id,
|
|
341
|
+
delegated: this.toTaskView(delegated),
|
|
342
|
+
dependency_added: dependencyAdded,
|
|
343
|
+
checkpoint_added: checkpointAdded,
|
|
344
|
+
parent_paused: parentPaused,
|
|
345
|
+
};
|
|
346
|
+
});
|
|
347
|
+
}
|
|
348
|
+
withIdempotency(workflow, inputForHash, explicitOpId, autoOpId, operation) {
|
|
349
|
+
if (explicitOpId && autoOpId) {
|
|
350
|
+
throw new Error('Cannot use --op-id and --auto-op-id together.');
|
|
351
|
+
}
|
|
352
|
+
const scope = `workflow:${workflow}`;
|
|
353
|
+
const inputHash = this.hashWorkflowInput(scope, inputForHash);
|
|
354
|
+
const resolvedOpId = explicitOpId ?? (autoOpId ? this.computeAutoOpId(workflow, inputHash) : undefined);
|
|
355
|
+
const autoGenerated = Boolean(!explicitOpId && autoOpId && resolvedOpId);
|
|
356
|
+
const opTable = this.getOpTableInfo();
|
|
357
|
+
if (resolvedOpId && opTable) {
|
|
358
|
+
const claimState = this.claimWorkflowOp(opTable, resolvedOpId, workflow, inputHash);
|
|
359
|
+
if (claimState === 'completed') {
|
|
360
|
+
const replay = this.loadWorkflowOp(opTable, resolvedOpId);
|
|
361
|
+
if (replay?.result_payload) {
|
|
362
|
+
return {
|
|
363
|
+
...replay.result_payload,
|
|
364
|
+
idempotency: {
|
|
365
|
+
op_id: resolvedOpId,
|
|
366
|
+
scope,
|
|
367
|
+
auto_generated: autoGenerated,
|
|
368
|
+
replayed: true,
|
|
369
|
+
table_available: true,
|
|
370
|
+
},
|
|
371
|
+
};
|
|
372
|
+
}
|
|
373
|
+
throw new Error(`op_id '${resolvedOpId}' is completed but has no cached result.`);
|
|
374
|
+
}
|
|
375
|
+
if (claimState === 'processing') {
|
|
376
|
+
throw new Error(`op_id '${resolvedOpId}' is already processing.`);
|
|
377
|
+
}
|
|
378
|
+
try {
|
|
379
|
+
const result = operation();
|
|
380
|
+
this.storeWorkflowOpResult(opTable, resolvedOpId, result);
|
|
381
|
+
return {
|
|
382
|
+
...result,
|
|
383
|
+
idempotency: {
|
|
384
|
+
op_id: resolvedOpId,
|
|
385
|
+
scope,
|
|
386
|
+
auto_generated: autoGenerated,
|
|
387
|
+
replayed: false,
|
|
388
|
+
table_available: true,
|
|
389
|
+
},
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
catch (error) {
|
|
393
|
+
this.storeWorkflowOpError(opTable, resolvedOpId, error);
|
|
394
|
+
throw error;
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
const result = operation();
|
|
398
|
+
return {
|
|
399
|
+
...result,
|
|
400
|
+
idempotency: {
|
|
401
|
+
op_id: resolvedOpId ?? null,
|
|
402
|
+
scope,
|
|
403
|
+
auto_generated: autoGenerated,
|
|
404
|
+
replayed: false,
|
|
405
|
+
table_available: Boolean(opTable),
|
|
406
|
+
},
|
|
407
|
+
};
|
|
408
|
+
}
|
|
409
|
+
getInProgressCandidates(options) {
|
|
410
|
+
const where = ["status = 'in_progress'", 'agent = ?'];
|
|
411
|
+
const params = [options.agent];
|
|
412
|
+
if (options.project) {
|
|
413
|
+
where.push('project = ?');
|
|
414
|
+
params.push(options.project);
|
|
415
|
+
}
|
|
416
|
+
if (options.tags && options.tags.length > 0) {
|
|
417
|
+
where.push(`(SELECT COUNT(DISTINCT tt.tag) FROM task_tags tt WHERE tt.task_id = tasks_current.task_id AND tt.tag IN (${options.tags.map(() => '?').join(', ')})) = ?`);
|
|
418
|
+
params.push(...options.tags, options.tags.length);
|
|
419
|
+
}
|
|
420
|
+
const orderBy = this.buildResumeOrder(options.resume_policy);
|
|
421
|
+
const rows = this.cacheDb
|
|
422
|
+
.prepare(`
|
|
423
|
+
SELECT task_id
|
|
424
|
+
FROM tasks_current
|
|
425
|
+
WHERE ${where.join(' AND ')}
|
|
426
|
+
ORDER BY ${orderBy}
|
|
427
|
+
`)
|
|
428
|
+
.all(...params);
|
|
429
|
+
return rows
|
|
430
|
+
.map((row) => this.taskService.getTaskById(row.task_id))
|
|
431
|
+
.filter((task) => task !== null);
|
|
432
|
+
}
|
|
433
|
+
buildResumeOrder(policy) {
|
|
434
|
+
if (policy === 'first') {
|
|
435
|
+
return 'claimed_at ASC, created_at ASC, task_id ASC';
|
|
436
|
+
}
|
|
437
|
+
if (policy === 'latest') {
|
|
438
|
+
return 'claimed_at DESC, updated_at DESC, task_id ASC';
|
|
439
|
+
}
|
|
440
|
+
return 'priority DESC, (due_at IS NULL) ASC, due_at ASC, claimed_at ASC, task_id ASC';
|
|
441
|
+
}
|
|
442
|
+
refreshLease(task, agent, leaseMinutes) {
|
|
443
|
+
const leaseUntil = new Date(Date.now() + leaseMinutes * 60000).toISOString();
|
|
444
|
+
const event = this.eventStore.append({
|
|
445
|
+
task_id: task.task_id,
|
|
446
|
+
type: EventType.StatusChanged,
|
|
447
|
+
data: {
|
|
448
|
+
from: TaskStatus.InProgress,
|
|
449
|
+
to: TaskStatus.InProgress,
|
|
450
|
+
lease_until: leaseUntil,
|
|
451
|
+
agent,
|
|
452
|
+
},
|
|
453
|
+
author: agent,
|
|
454
|
+
});
|
|
455
|
+
this.projectionEngine.applyEvent(event);
|
|
456
|
+
return this.taskService.getTaskById(task.task_id);
|
|
457
|
+
}
|
|
458
|
+
toTaskView(task) {
|
|
459
|
+
return {
|
|
460
|
+
task_id: task.task_id,
|
|
461
|
+
title: task.title,
|
|
462
|
+
project: task.project,
|
|
463
|
+
status: task.status,
|
|
464
|
+
priority: task.priority,
|
|
465
|
+
agent: task.agent,
|
|
466
|
+
lease_until: task.lease_until,
|
|
467
|
+
};
|
|
468
|
+
}
|
|
469
|
+
boundOthers(tasks, limit) {
|
|
470
|
+
if (limit === 'all')
|
|
471
|
+
return tasks;
|
|
472
|
+
return tasks.slice(0, limit);
|
|
473
|
+
}
|
|
474
|
+
normalizeOthersLimit(limit) {
|
|
475
|
+
if (limit === 'all')
|
|
476
|
+
return 'all';
|
|
477
|
+
if (limit === undefined)
|
|
478
|
+
return 5;
|
|
479
|
+
return Math.max(0, limit);
|
|
480
|
+
}
|
|
481
|
+
buildFilters(project, tags) {
|
|
482
|
+
return {
|
|
483
|
+
...(project ? { project } : {}),
|
|
484
|
+
...(tags && tags.length > 0 ? { tags } : {}),
|
|
485
|
+
};
|
|
486
|
+
}
|
|
487
|
+
buildCarriedCheckpointContext(checkpoints, maxChars) {
|
|
488
|
+
if (checkpoints.length === 0 || maxChars <= 0)
|
|
489
|
+
return '';
|
|
490
|
+
const lines = [];
|
|
491
|
+
for (const checkpoint of checkpoints) {
|
|
492
|
+
const payload = Object.keys(checkpoint.data).length
|
|
493
|
+
? ` ${JSON.stringify(checkpoint.data)}`
|
|
494
|
+
: '';
|
|
495
|
+
lines.push(`[${checkpoint.timestamp}] ${checkpoint.name}${payload}`);
|
|
496
|
+
}
|
|
497
|
+
return this.sliceByBudget(lines.join('\n'), maxChars);
|
|
498
|
+
}
|
|
499
|
+
sliceByBudget(text, maxChars) {
|
|
500
|
+
if (maxChars <= 0)
|
|
501
|
+
return '';
|
|
502
|
+
if (text.length <= maxChars)
|
|
503
|
+
return text;
|
|
504
|
+
return `${text.slice(0, Math.max(0, maxChars - 3))}...`;
|
|
505
|
+
}
|
|
506
|
+
getTaskLastEventId(taskId) {
|
|
507
|
+
const row = this.cacheDb
|
|
508
|
+
.prepare('SELECT last_event_id FROM tasks_current WHERE task_id = ?')
|
|
509
|
+
.get(taskId);
|
|
510
|
+
return row?.last_event_id ?? null;
|
|
511
|
+
}
|
|
512
|
+
hashWorkflowInput(scope, input) {
|
|
513
|
+
const canonical = stableStringify({ scope, input });
|
|
514
|
+
return createHash('sha256').update(canonical).digest('hex');
|
|
515
|
+
}
|
|
516
|
+
computeAutoOpId(workflow, inputHash) {
|
|
517
|
+
const shortHash = inputHash.slice(0, 24);
|
|
518
|
+
return `wf_${workflow}_${shortHash}`;
|
|
519
|
+
}
|
|
520
|
+
getOpTableInfo() {
|
|
521
|
+
const dbs = this.idempotencyDb && this.idempotencyDb !== this.cacheDb
|
|
522
|
+
? [this.idempotencyDb, this.cacheDb]
|
|
523
|
+
: [this.cacheDb];
|
|
524
|
+
for (const db of dbs) {
|
|
525
|
+
const exists = db
|
|
526
|
+
.prepare("SELECT name FROM sqlite_master WHERE type = 'table' AND name = 'workflow_ops'")
|
|
527
|
+
.get();
|
|
528
|
+
if (exists)
|
|
529
|
+
return { db };
|
|
530
|
+
}
|
|
531
|
+
return null;
|
|
532
|
+
}
|
|
533
|
+
loadWorkflowOp(table, opId) {
|
|
534
|
+
const row = table.db
|
|
535
|
+
.prepare(`
|
|
536
|
+
SELECT workflow_name, input_hash, state, updated_at, result_payload, error_payload
|
|
537
|
+
FROM workflow_ops
|
|
538
|
+
WHERE op_id = ?
|
|
539
|
+
LIMIT 1
|
|
540
|
+
`)
|
|
541
|
+
.get(opId);
|
|
542
|
+
if (!row)
|
|
543
|
+
return null;
|
|
544
|
+
return {
|
|
545
|
+
workflow_name: row.workflow_name,
|
|
546
|
+
input_hash: row.input_hash,
|
|
547
|
+
state: row.state,
|
|
548
|
+
updated_at: row.updated_at,
|
|
549
|
+
result_payload: parseJsonObject(row.result_payload),
|
|
550
|
+
error_payload: parseJsonObject(row.error_payload),
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
claimWorkflowOp(table, opId, workflow, inputHash) {
|
|
554
|
+
const now = new Date().toISOString();
|
|
555
|
+
const insertResult = table.db
|
|
556
|
+
.prepare(`
|
|
557
|
+
INSERT INTO workflow_ops (op_id, workflow_name, input_hash, state, created_at, updated_at)
|
|
558
|
+
VALUES (?, ?, ?, 'processing', ?, ?)
|
|
559
|
+
ON CONFLICT(op_id) DO NOTHING
|
|
560
|
+
`)
|
|
561
|
+
.run(opId, workflow, inputHash, now, now);
|
|
562
|
+
if ((insertResult.changes ?? 0) === 1)
|
|
563
|
+
return 'claimed';
|
|
564
|
+
const existing = this.loadWorkflowOp(table, opId);
|
|
565
|
+
if (!existing)
|
|
566
|
+
return 'processing';
|
|
567
|
+
if (existing.workflow_name !== workflow || existing.input_hash !== inputHash) {
|
|
568
|
+
throw new Error(`op_id '${opId}' was already used with different workflow input.`);
|
|
569
|
+
}
|
|
570
|
+
if (existing.state === 'completed')
|
|
571
|
+
return 'completed';
|
|
572
|
+
if (existing.state === 'processing') {
|
|
573
|
+
const updatedAtMs = Date.parse(existing.updated_at);
|
|
574
|
+
const isStale = Number.isFinite(updatedAtMs) &&
|
|
575
|
+
Date.now() - updatedAtMs >= WORKFLOW_OP_PROCESSING_STALE_MS;
|
|
576
|
+
if (!isStale)
|
|
577
|
+
return 'processing';
|
|
578
|
+
const staleReclaimResult = table.db
|
|
579
|
+
.prepare(`
|
|
580
|
+
UPDATE workflow_ops
|
|
581
|
+
SET state = 'processing', updated_at = ?, error_payload = NULL
|
|
582
|
+
WHERE op_id = ? AND state = 'processing' AND updated_at = ?
|
|
583
|
+
`)
|
|
584
|
+
.run(now, opId, existing.updated_at);
|
|
585
|
+
return (staleReclaimResult.changes ?? 0) === 1
|
|
586
|
+
? 'claimed'
|
|
587
|
+
: 'processing';
|
|
588
|
+
}
|
|
589
|
+
const reclaimResult = table.db
|
|
590
|
+
.prepare(`
|
|
591
|
+
UPDATE workflow_ops
|
|
592
|
+
SET state = 'processing', updated_at = ?, error_payload = NULL
|
|
593
|
+
WHERE op_id = ? AND state = 'failed'
|
|
594
|
+
`)
|
|
595
|
+
.run(now, opId);
|
|
596
|
+
return (reclaimResult.changes ?? 0) === 1 ? 'claimed' : 'processing';
|
|
597
|
+
}
|
|
598
|
+
storeWorkflowOpResult(table, opId, result) {
|
|
599
|
+
const now = new Date().toISOString();
|
|
600
|
+
table.db
|
|
601
|
+
.prepare(`
|
|
602
|
+
UPDATE workflow_ops
|
|
603
|
+
SET state = 'completed',
|
|
604
|
+
result_payload = ?,
|
|
605
|
+
error_payload = NULL,
|
|
606
|
+
updated_at = ?
|
|
607
|
+
WHERE op_id = ?
|
|
608
|
+
`)
|
|
609
|
+
.run(JSON.stringify(result), now, opId);
|
|
610
|
+
}
|
|
611
|
+
storeWorkflowOpError(table, opId, error) {
|
|
612
|
+
const now = new Date().toISOString();
|
|
613
|
+
const payload = {
|
|
614
|
+
message: error instanceof Error ? error.message : String(error),
|
|
615
|
+
name: error instanceof Error ? error.name : 'Error',
|
|
616
|
+
};
|
|
617
|
+
table.db
|
|
618
|
+
.prepare(`
|
|
619
|
+
UPDATE workflow_ops
|
|
620
|
+
SET state = 'failed',
|
|
621
|
+
error_payload = ?,
|
|
622
|
+
updated_at = ?
|
|
623
|
+
WHERE op_id = ?
|
|
624
|
+
`)
|
|
625
|
+
.run(JSON.stringify(payload), now, opId);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function parseJsonObject(raw) {
|
|
629
|
+
if (!raw)
|
|
630
|
+
return null;
|
|
631
|
+
try {
|
|
632
|
+
const parsed = JSON.parse(raw);
|
|
633
|
+
if (parsed && typeof parsed === 'object' && !Array.isArray(parsed)) {
|
|
634
|
+
return parsed;
|
|
635
|
+
}
|
|
636
|
+
return null;
|
|
637
|
+
}
|
|
638
|
+
catch {
|
|
639
|
+
return null;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
function stableStringify(value) {
|
|
643
|
+
if (value === null || typeof value !== 'object') {
|
|
644
|
+
return JSON.stringify(value);
|
|
645
|
+
}
|
|
646
|
+
if (Array.isArray(value)) {
|
|
647
|
+
return `[${value.map((item) => stableStringify(item)).join(',')}]`;
|
|
648
|
+
}
|
|
649
|
+
const record = value;
|
|
650
|
+
const keys = Object.keys(record).sort();
|
|
651
|
+
return `{${keys.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`).join(',')}}`;
|
|
652
|
+
}
|
|
653
|
+
export class UnknownWorkflowError extends Error {
|
|
654
|
+
constructor(name) {
|
|
655
|
+
super(`Unknown workflow: ${name}`);
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
export function parseWorkflowName(name) {
|
|
659
|
+
if (name === 'start' || name === 'handoff' || name === 'delegate') {
|
|
660
|
+
return name;
|
|
661
|
+
}
|
|
662
|
+
throw new UnknownWorkflowError(name);
|
|
663
|
+
}
|
|
664
|
+
//# sourceMappingURL=workflow-service.js.map
|