@sgtbeatdown/pi-tasks 0.1.0 → 0.1.1
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/README.md +48 -468
- package/package.json +4 -3
- package/skills/task-context-summary/SKILL.md +34 -0
- package/skills/wolfpack-pi-task-delegation/SKILL.md +29 -60
- package/src/extension.ts +165 -251
- package/src/gateway-client.ts +385 -0
- package/src/metrics-cli.ts +20 -0
- package/src/metrics.ts +288 -0
- package/src/task-artifacts.ts +153 -0
- package/src/task-board.ts +365 -0
- package/src/task-inbox.ts +141 -0
- package/src/auto-notify.ts +0 -24
- package/src/protocol.ts +0 -73
- package/src/store.ts +0 -494
- package/src/stores/filesystem.ts +0 -40
- package/src/task-communication.ts +0 -68
- package/src/transports/wolfpack.ts +0 -29
- package/src/types.ts +0 -114
package/src/store.ts
DELETED
|
@@ -1,494 +0,0 @@
|
|
|
1
|
-
import { createHash, randomUUID } from "node:crypto";
|
|
2
|
-
import { appendFile, mkdir, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
|
|
3
|
-
import { dirname, join } from "node:path";
|
|
4
|
-
|
|
5
|
-
import {
|
|
6
|
-
DEFAULT_TIMEOUT_MS,
|
|
7
|
-
MAX_TIMEOUT_MS,
|
|
8
|
-
MIN_TIMEOUT_MS,
|
|
9
|
-
PROGRESS_MAX_CHARS,
|
|
10
|
-
SUMMARY_MAX_CHARS,
|
|
11
|
-
type AgentTaskRecord,
|
|
12
|
-
type CreateDispatchedTaskInput,
|
|
13
|
-
type ListInboxOptions,
|
|
14
|
-
type StoredTaskResult,
|
|
15
|
-
type TaskError,
|
|
16
|
-
type TaskEvent,
|
|
17
|
-
type TaskEventType,
|
|
18
|
-
type TaskResultPayload,
|
|
19
|
-
type TaskStatus,
|
|
20
|
-
type TerminalTaskStatus,
|
|
21
|
-
type WaitForTaskOptions,
|
|
22
|
-
} from "./types";
|
|
23
|
-
|
|
24
|
-
export const DEFAULT_TASKS_DIR = ".pi/tasks";
|
|
25
|
-
|
|
26
|
-
export interface TaskStoreScope {
|
|
27
|
-
readonly projectDir: string;
|
|
28
|
-
readonly tasksDir?: string;
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
export type TaskStoreLocation = string | TaskStoreScope;
|
|
32
|
-
|
|
33
|
-
const LOCK_RETRY_MS = 25;
|
|
34
|
-
const LOCK_TIMEOUT_MS = 5000;
|
|
35
|
-
|
|
36
|
-
const TERMINAL_STATUSES = new Set<TaskStatus>(["completed", "failed", "cancelled", "timed_out", "rejected"]);
|
|
37
|
-
|
|
38
|
-
export interface CreateOrReuseDispatchedTaskResult {
|
|
39
|
-
readonly task: AgentTaskRecord;
|
|
40
|
-
readonly created: boolean;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
export function getTasksRoot(location: TaskStoreLocation): string {
|
|
44
|
-
return join(getProjectDir(location), getTasksDir(location));
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export function getTaskDir(location: TaskStoreLocation, taskId: string): string {
|
|
48
|
-
return join(getTasksRoot(location), taskId);
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
function getProjectDir(location: TaskStoreLocation): string {
|
|
52
|
-
return typeof location === "string" ? location : location.projectDir;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function getTasksDir(location: TaskStoreLocation): string {
|
|
56
|
-
return typeof location === "string" ? DEFAULT_TASKS_DIR : location.tasksDir ?? DEFAULT_TASKS_DIR;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
function taskRef(location: TaskStoreLocation, taskId: string, fileName: string): string {
|
|
60
|
-
return `file://${getTasksDir(location)}/${taskId}/${fileName}`;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
export function isTerminalStatus(status: TaskStatus): status is TerminalTaskStatus {
|
|
64
|
-
return TERMINAL_STATUSES.has(status);
|
|
65
|
-
}
|
|
66
|
-
|
|
67
|
-
export function normalizeTimeoutMs(timeoutMs: number | undefined): number {
|
|
68
|
-
const value = timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
69
|
-
if (!Number.isInteger(value) || value < MIN_TIMEOUT_MS || value > MAX_TIMEOUT_MS) {
|
|
70
|
-
throw new Error(`timeoutMs must be an integer between ${MIN_TIMEOUT_MS} and ${MAX_TIMEOUT_MS}`);
|
|
71
|
-
}
|
|
72
|
-
return value;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function inputLocation(input: CreateDispatchedTaskInput): TaskStoreScope {
|
|
76
|
-
return { projectDir: input.projectDir, tasksDir: input.tasksDir };
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
export async function createOrReuseDispatchedTask(input: CreateDispatchedTaskInput): Promise<CreateOrReuseDispatchedTaskResult> {
|
|
80
|
-
const idempotencyKey = input.idempotencyKey;
|
|
81
|
-
if (!idempotencyKey) {
|
|
82
|
-
return { task: await createDispatchedTask(input), created: true };
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
const location = inputLocation(input);
|
|
86
|
-
return withIdempotencyLock(location, input.parentSession, idempotencyKey, async () => {
|
|
87
|
-
const existing = await findTaskByIdempotencyKey(location, input.parentSession, idempotencyKey);
|
|
88
|
-
if (existing) {
|
|
89
|
-
return { task: existing, created: false };
|
|
90
|
-
}
|
|
91
|
-
return { task: await createDispatchedTask(input), created: true };
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
export async function createDispatchedTask(input: CreateDispatchedTaskInput): Promise<AgentTaskRecord> {
|
|
96
|
-
const timeoutMs = normalizeTimeoutMs(input.timeoutMs);
|
|
97
|
-
const id = `task_${randomUUID().replace(/-/g, "")}`;
|
|
98
|
-
const now = new Date().toISOString();
|
|
99
|
-
const timeoutAt = new Date(Date.parse(now) + timeoutMs).toISOString();
|
|
100
|
-
const location = inputLocation(input);
|
|
101
|
-
const dir = getTaskDir(location, id);
|
|
102
|
-
await mkdir(dir, { recursive: true });
|
|
103
|
-
|
|
104
|
-
const assignment = typeof input.assignment === "function" ? input.assignment(id) : withTaskId(input.assignment, id);
|
|
105
|
-
await writeJsonAtomic(join(dir, "assignment.json"), assignment);
|
|
106
|
-
|
|
107
|
-
const task: AgentTaskRecord = {
|
|
108
|
-
schemaVersion: 1,
|
|
109
|
-
id,
|
|
110
|
-
projectDir: input.projectDir,
|
|
111
|
-
parentSession: input.parentSession,
|
|
112
|
-
targetSession: input.targetSession,
|
|
113
|
-
taskText: input.taskText,
|
|
114
|
-
status: "dispatched",
|
|
115
|
-
createdAt: now,
|
|
116
|
-
updatedAt: now,
|
|
117
|
-
dispatchedAt: now,
|
|
118
|
-
runningAt: undefined,
|
|
119
|
-
completedAt: undefined,
|
|
120
|
-
timeoutAt,
|
|
121
|
-
timeoutMs,
|
|
122
|
-
idempotencyKey: input.idempotencyKey,
|
|
123
|
-
assignmentRef: taskRef(location, id, "assignment.json"),
|
|
124
|
-
resultRef: undefined,
|
|
125
|
-
parentAckAt: undefined,
|
|
126
|
-
targetTaskProtocol: input.targetTaskProtocol,
|
|
127
|
-
error: undefined,
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
await writeJsonAtomic(join(dir, "task.json"), task);
|
|
131
|
-
await appendTaskEvent(location, id, "task.created", "store", { parentSession: input.parentSession });
|
|
132
|
-
await appendTaskEvent(location, id, "task.dispatched", "store", { targetSession: input.targetSession });
|
|
133
|
-
return task;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
export async function readTask(location: TaskStoreLocation, taskId: string): Promise<AgentTaskRecord> {
|
|
137
|
-
return readJson<AgentTaskRecord>(join(getTaskDir(location, taskId), "task.json"));
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
export async function readTaskResult(location: TaskStoreLocation, taskId: string): Promise<StoredTaskResult | undefined> {
|
|
141
|
-
return readExistingResult(location, taskId);
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
export async function expireTaskIfOverdue(location: TaskStoreLocation, taskId: string): Promise<AgentTaskRecord> {
|
|
145
|
-
const current = await readTask(location, taskId);
|
|
146
|
-
if (isTerminalStatus(current.status) || Date.now() < Date.parse(current.timeoutAt)) {
|
|
147
|
-
return current;
|
|
148
|
-
}
|
|
149
|
-
return writeTerminalTask(location, taskId, "timed_out", {
|
|
150
|
-
summary: "task timed out",
|
|
151
|
-
error: { code: "timed_out", message: "task timed out", retryable: true },
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
export async function appendProgress(location: TaskStoreLocation, taskId: string, message: string): Promise<AgentTaskRecord> {
|
|
156
|
-
const progress = limitString(message, PROGRESS_MAX_CHARS, "progress");
|
|
157
|
-
return withTaskLock(location, taskId, async () => {
|
|
158
|
-
const current = await readTask(location, taskId);
|
|
159
|
-
if (isTerminalStatus(current.status)) {
|
|
160
|
-
return current;
|
|
161
|
-
}
|
|
162
|
-
const now = new Date().toISOString();
|
|
163
|
-
const next = updateTask(current, {
|
|
164
|
-
status: current.status === "dispatched" ? "running" : current.status,
|
|
165
|
-
runningAt: current.runningAt ?? now,
|
|
166
|
-
updatedAt: now,
|
|
167
|
-
});
|
|
168
|
-
await writeTask(location, next);
|
|
169
|
-
if (current.status === "dispatched") {
|
|
170
|
-
await appendTaskEventUnlocked(location, taskId, "task.running", "target-tool", {});
|
|
171
|
-
}
|
|
172
|
-
await appendTaskEventUnlocked(location, taskId, "task.progress", "target-tool", { message: progress });
|
|
173
|
-
return next;
|
|
174
|
-
});
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
export async function completeTask(
|
|
178
|
-
location: TaskStoreLocation,
|
|
179
|
-
taskId: string,
|
|
180
|
-
status: TerminalTaskStatus,
|
|
181
|
-
payload: TaskResultPayload,
|
|
182
|
-
): Promise<AgentTaskRecord> {
|
|
183
|
-
return writeTerminalTask(location, taskId, status, payload);
|
|
184
|
-
}
|
|
185
|
-
|
|
186
|
-
export async function cancelTask(location: TaskStoreLocation, taskId: string, reason: string | undefined): Promise<AgentTaskRecord> {
|
|
187
|
-
return writeTerminalTask(location, taskId, "cancelled", {
|
|
188
|
-
summary: reason ?? "cancelled",
|
|
189
|
-
error: { code: "cancelled", message: reason ?? "cancelled", retryable: false },
|
|
190
|
-
});
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
export async function ackTask(location: TaskStoreLocation, taskId: string, parentSession: string): Promise<AgentTaskRecord> {
|
|
194
|
-
return withTaskLock(location, taskId, async () => {
|
|
195
|
-
const current = await readTask(location, taskId);
|
|
196
|
-
if (current.parentSession !== parentSession) {
|
|
197
|
-
throw new Error("task parent mismatch");
|
|
198
|
-
}
|
|
199
|
-
if (!isTerminalStatus(current.status)) {
|
|
200
|
-
return current;
|
|
201
|
-
}
|
|
202
|
-
if (current.parentAckAt) {
|
|
203
|
-
return current;
|
|
204
|
-
}
|
|
205
|
-
const now = new Date().toISOString();
|
|
206
|
-
const next = updateTask(current, { parentAckAt: now, updatedAt: now });
|
|
207
|
-
await writeTask(location, next);
|
|
208
|
-
await appendTaskEventUnlocked(location, taskId, "task.acknowledged", "parent-tool", { parentSession });
|
|
209
|
-
return next;
|
|
210
|
-
});
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
export async function listInbox(
|
|
214
|
-
location: TaskStoreLocation,
|
|
215
|
-
parentSession: string,
|
|
216
|
-
options: ListInboxOptions,
|
|
217
|
-
): Promise<readonly AgentTaskRecord[]> {
|
|
218
|
-
const root = getTasksRoot(location);
|
|
219
|
-
let entries: string[];
|
|
220
|
-
try {
|
|
221
|
-
entries = await readdir(root);
|
|
222
|
-
} catch {
|
|
223
|
-
return [];
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
const tasks: AgentTaskRecord[] = [];
|
|
227
|
-
for (const entry of entries) {
|
|
228
|
-
if (!entry.startsWith("task_")) continue;
|
|
229
|
-
try {
|
|
230
|
-
const task = await expireTaskIfOverdue(location, entry);
|
|
231
|
-
if (task.parentSession !== parentSession) continue;
|
|
232
|
-
if (!isTerminalStatus(task.status)) continue;
|
|
233
|
-
if (!options.includeAcknowledged && task.parentAckAt) continue;
|
|
234
|
-
tasks.push(task);
|
|
235
|
-
} catch {
|
|
236
|
-
// Ignore partial/corrupt task directories; direct status reads surface errors.
|
|
237
|
-
}
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return tasks.sort((a, b) => a.updatedAt.localeCompare(b.updatedAt));
|
|
241
|
-
}
|
|
242
|
-
|
|
243
|
-
export async function waitForTask(
|
|
244
|
-
location: TaskStoreLocation,
|
|
245
|
-
taskId: string,
|
|
246
|
-
options: WaitForTaskOptions,
|
|
247
|
-
): Promise<AgentTaskRecord> {
|
|
248
|
-
const deadline = Date.now() + options.timeoutMs;
|
|
249
|
-
while (Date.now() <= deadline) {
|
|
250
|
-
const task = await expireTaskIfOverdue(location, taskId);
|
|
251
|
-
if (isTerminalStatus(task.status)) {
|
|
252
|
-
return options.ackParentSession ? ackTask(location, taskId, options.ackParentSession) : task;
|
|
253
|
-
}
|
|
254
|
-
await sleep(options.pollMs);
|
|
255
|
-
}
|
|
256
|
-
throw new Error("task wait timed out");
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
async function writeTerminalTask(
|
|
260
|
-
location: TaskStoreLocation,
|
|
261
|
-
taskId: string,
|
|
262
|
-
status: TerminalTaskStatus,
|
|
263
|
-
payload: TaskResultPayload,
|
|
264
|
-
): Promise<AgentTaskRecord> {
|
|
265
|
-
return withTaskLock(location, taskId, async () => {
|
|
266
|
-
const current = await readTask(location, taskId);
|
|
267
|
-
const now = new Date().toISOString();
|
|
268
|
-
const cleanPayload = normalizeResultPayload(payload);
|
|
269
|
-
const result: StoredTaskResult = {
|
|
270
|
-
...cleanPayload,
|
|
271
|
-
schemaVersion: 1,
|
|
272
|
-
taskId,
|
|
273
|
-
status,
|
|
274
|
-
completedAt: now,
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
if (isTerminalStatus(current.status)) {
|
|
278
|
-
const existing = await readExistingResult(location, taskId);
|
|
279
|
-
if (current.status === status && existing && terminalPayloadKey(existing) === terminalPayloadKey(result)) {
|
|
280
|
-
return current;
|
|
281
|
-
}
|
|
282
|
-
throw new Error("terminal task conflict");
|
|
283
|
-
}
|
|
284
|
-
|
|
285
|
-
await writeJsonAtomic(join(getTaskDir(location, taskId), "result.json"), result);
|
|
286
|
-
const next = updateTask(current, {
|
|
287
|
-
status,
|
|
288
|
-
updatedAt: now,
|
|
289
|
-
completedAt: now,
|
|
290
|
-
resultRef: taskRef(location, taskId, "result.json"),
|
|
291
|
-
error: cleanPayload.error,
|
|
292
|
-
});
|
|
293
|
-
await writeTask(location, next);
|
|
294
|
-
await appendTaskEventUnlocked(location, taskId, `task.${status}` as TaskEventType, "target-tool", {
|
|
295
|
-
summary: cleanPayload.summary,
|
|
296
|
-
error: cleanPayload.error,
|
|
297
|
-
});
|
|
298
|
-
return next;
|
|
299
|
-
});
|
|
300
|
-
}
|
|
301
|
-
|
|
302
|
-
async function readExistingResult(location: TaskStoreLocation, taskId: string): Promise<StoredTaskResult | undefined> {
|
|
303
|
-
try {
|
|
304
|
-
return await readJson<StoredTaskResult>(join(getTaskDir(location, taskId), "result.json"));
|
|
305
|
-
} catch {
|
|
306
|
-
return undefined;
|
|
307
|
-
}
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
function normalizeResultPayload(payload: TaskResultPayload): TaskResultPayload {
|
|
311
|
-
return {
|
|
312
|
-
summary: limitString(payload.summary, SUMMARY_MAX_CHARS, "summary"),
|
|
313
|
-
...(payload.result && { result: payload.result }),
|
|
314
|
-
...(payload.error && { error: payload.error }),
|
|
315
|
-
...(payload.artifacts && { artifacts: payload.artifacts }),
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
|
|
319
|
-
function updateTask(current: AgentTaskRecord, patch: Partial<AgentTaskRecord>): AgentTaskRecord {
|
|
320
|
-
return { ...current, ...patch };
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
async function writeTask(location: TaskStoreLocation, task: AgentTaskRecord): Promise<void> {
|
|
324
|
-
await writeJsonAtomic(join(getTaskDir(location, task.id), "task.json"), task);
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
async function appendTaskEvent(
|
|
328
|
-
location: TaskStoreLocation,
|
|
329
|
-
taskId: string,
|
|
330
|
-
type: TaskEventType,
|
|
331
|
-
source: TaskEvent["source"],
|
|
332
|
-
payload: unknown,
|
|
333
|
-
): Promise<void> {
|
|
334
|
-
await withTaskLock(location, taskId, async () => {
|
|
335
|
-
await appendTaskEventUnlocked(location, taskId, type, source, payload);
|
|
336
|
-
});
|
|
337
|
-
}
|
|
338
|
-
|
|
339
|
-
async function appendTaskEventUnlocked(
|
|
340
|
-
location: TaskStoreLocation,
|
|
341
|
-
taskId: string,
|
|
342
|
-
type: TaskEventType,
|
|
343
|
-
source: TaskEvent["source"],
|
|
344
|
-
payload: unknown,
|
|
345
|
-
): Promise<void> {
|
|
346
|
-
const eventsPath = join(getTaskDir(location, taskId), "events.jsonl");
|
|
347
|
-
const seq = (await readLastEventSeq(eventsPath)) + 1;
|
|
348
|
-
const event: TaskEvent = {
|
|
349
|
-
schemaVersion: 1,
|
|
350
|
-
seq,
|
|
351
|
-
taskId,
|
|
352
|
-
type,
|
|
353
|
-
createdAt: new Date().toISOString(),
|
|
354
|
-
source,
|
|
355
|
-
payload,
|
|
356
|
-
};
|
|
357
|
-
await appendFile(eventsPath, `${JSON.stringify(event)}\n`, "utf8");
|
|
358
|
-
}
|
|
359
|
-
|
|
360
|
-
async function readLastEventSeq(eventsPath: string): Promise<number> {
|
|
361
|
-
try {
|
|
362
|
-
const content = await readFile(eventsPath, "utf8");
|
|
363
|
-
const lines = content.trim().split("\n").filter(Boolean);
|
|
364
|
-
const lastLine = lines.at(-1);
|
|
365
|
-
if (!lastLine) return 0;
|
|
366
|
-
const last = JSON.parse(lastLine) as { readonly seq?: unknown };
|
|
367
|
-
return typeof last.seq === "number" ? last.seq : 0;
|
|
368
|
-
} catch {
|
|
369
|
-
return 0;
|
|
370
|
-
}
|
|
371
|
-
}
|
|
372
|
-
|
|
373
|
-
async function findTaskByIdempotencyKey(
|
|
374
|
-
location: TaskStoreLocation,
|
|
375
|
-
parentSession: string,
|
|
376
|
-
idempotencyKey: string,
|
|
377
|
-
): Promise<AgentTaskRecord | undefined> {
|
|
378
|
-
const root = getTasksRoot(location);
|
|
379
|
-
let entries: string[];
|
|
380
|
-
try {
|
|
381
|
-
entries = await readdir(root);
|
|
382
|
-
} catch {
|
|
383
|
-
return undefined;
|
|
384
|
-
}
|
|
385
|
-
|
|
386
|
-
for (const entry of entries) {
|
|
387
|
-
if (!entry.startsWith("task_")) continue;
|
|
388
|
-
try {
|
|
389
|
-
const task = await readTask(location, entry);
|
|
390
|
-
if (task.parentSession === parentSession && task.idempotencyKey === idempotencyKey) {
|
|
391
|
-
return task;
|
|
392
|
-
}
|
|
393
|
-
} catch {
|
|
394
|
-
// Ignore partial/corrupt task directories; direct status reads surface errors.
|
|
395
|
-
}
|
|
396
|
-
}
|
|
397
|
-
return undefined;
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
async function withIdempotencyLock<T>(
|
|
401
|
-
location: TaskStoreLocation,
|
|
402
|
-
parentSession: string,
|
|
403
|
-
idempotencyKey: string,
|
|
404
|
-
fn: () => Promise<T>,
|
|
405
|
-
): Promise<T> {
|
|
406
|
-
const lockName = createHash("sha256").update(`${parentSession}\0${idempotencyKey}`).digest("hex");
|
|
407
|
-
const lockPath = join(getTasksRoot(location), `.idempotency-${lockName}.lock`);
|
|
408
|
-
await mkdir(getTasksRoot(location), { recursive: true });
|
|
409
|
-
return withLockPath(lockPath, fn);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
async function withTaskLock<T>(location: TaskStoreLocation, taskId: string, fn: () => Promise<T>): Promise<T> {
|
|
413
|
-
const lockPath = join(getTaskDir(location, taskId), ".lock");
|
|
414
|
-
return withLockPath(lockPath, fn);
|
|
415
|
-
}
|
|
416
|
-
|
|
417
|
-
async function withLockPath<T>(lockPath: string, fn: () => Promise<T>): Promise<T> {
|
|
418
|
-
const deadline = Date.now() + LOCK_TIMEOUT_MS;
|
|
419
|
-
while (true) {
|
|
420
|
-
try {
|
|
421
|
-
await mkdir(lockPath);
|
|
422
|
-
break;
|
|
423
|
-
} catch (error) {
|
|
424
|
-
if (!isFileExistsError(error) || Date.now() > deadline) {
|
|
425
|
-
throw error;
|
|
426
|
-
}
|
|
427
|
-
await sleep(LOCK_RETRY_MS);
|
|
428
|
-
}
|
|
429
|
-
}
|
|
430
|
-
|
|
431
|
-
try {
|
|
432
|
-
return await fn();
|
|
433
|
-
} finally {
|
|
434
|
-
await rm(lockPath, { recursive: true, force: true });
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
async function writeJsonAtomic(path: string, value: unknown): Promise<void> {
|
|
439
|
-
await mkdir(dirname(path), { recursive: true });
|
|
440
|
-
const tmpPath = `${path}.${process.pid}.${randomUUID()}.tmp`;
|
|
441
|
-
await writeFile(tmpPath, `${JSON.stringify(value, null, 2)}\n`, { encoding: "utf8", mode: 0o600 });
|
|
442
|
-
await rename(tmpPath, path);
|
|
443
|
-
}
|
|
444
|
-
|
|
445
|
-
async function readJson<T>(path: string): Promise<T> {
|
|
446
|
-
const content = await readFile(path, "utf8");
|
|
447
|
-
return JSON.parse(content) as T;
|
|
448
|
-
}
|
|
449
|
-
|
|
450
|
-
function withTaskId(assignment: unknown, taskId: string): unknown {
|
|
451
|
-
if (!assignment || typeof assignment !== "object" || Array.isArray(assignment)) {
|
|
452
|
-
return assignment;
|
|
453
|
-
}
|
|
454
|
-
return { ...assignment, taskId };
|
|
455
|
-
}
|
|
456
|
-
|
|
457
|
-
function terminalPayloadKey(result: StoredTaskResult): string {
|
|
458
|
-
return stableStringify({
|
|
459
|
-
status: result.status,
|
|
460
|
-
summary: result.summary,
|
|
461
|
-
result: result.result,
|
|
462
|
-
error: result.error,
|
|
463
|
-
artifacts: result.artifacts,
|
|
464
|
-
});
|
|
465
|
-
}
|
|
466
|
-
|
|
467
|
-
function stableStringify(value: unknown): string {
|
|
468
|
-
if (value === null || typeof value !== "object") {
|
|
469
|
-
return JSON.stringify(value);
|
|
470
|
-
}
|
|
471
|
-
if (Array.isArray(value)) {
|
|
472
|
-
return `[${value.map((item) => stableStringify(item)).join(",")}]`;
|
|
473
|
-
}
|
|
474
|
-
const record = value as Record<string, unknown>;
|
|
475
|
-
return `{${Object.keys(record)
|
|
476
|
-
.sort()
|
|
477
|
-
.map((key) => `${JSON.stringify(key)}:${stableStringify(record[key])}`)
|
|
478
|
-
.join(",")}}`;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
function limitString(value: string, maxChars: number, label: string): string {
|
|
482
|
-
if (value.length <= maxChars) {
|
|
483
|
-
return value;
|
|
484
|
-
}
|
|
485
|
-
throw new Error(`${label} exceeds ${maxChars} characters`);
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
function isFileExistsError(error: unknown): boolean {
|
|
489
|
-
return Boolean(error && typeof error === "object" && "code" in error && error.code === "EEXIST");
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
function sleep(ms: number): Promise<void> {
|
|
493
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
494
|
-
}
|
package/src/stores/filesystem.ts
DELETED
|
@@ -1,40 +0,0 @@
|
|
|
1
|
-
import type { TaskStore } from "../task-communication";
|
|
2
|
-
import {
|
|
3
|
-
ackTask,
|
|
4
|
-
cancelTask,
|
|
5
|
-
completeTask,
|
|
6
|
-
createOrReuseDispatchedTask,
|
|
7
|
-
DEFAULT_TASKS_DIR,
|
|
8
|
-
expireTaskIfOverdue,
|
|
9
|
-
listInbox,
|
|
10
|
-
readTask,
|
|
11
|
-
readTaskResult,
|
|
12
|
-
waitForTask,
|
|
13
|
-
} from "../store";
|
|
14
|
-
import type { CreateDispatchedTaskInput, ListInboxOptions, TaskResultPayload, TerminalTaskStatus, WaitForTaskOptions } from "../types";
|
|
15
|
-
|
|
16
|
-
export interface FilesystemTaskStoreOptions {
|
|
17
|
-
readonly tasksDir?: string;
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
export function createFilesystemTaskStore(options: FilesystemTaskStoreOptions = {}): TaskStore {
|
|
21
|
-
const tasksDir = options.tasksDir ?? DEFAULT_TASKS_DIR;
|
|
22
|
-
const scope = (projectDir: string): { readonly projectDir: string; readonly tasksDir: string } => ({ projectDir, tasksDir });
|
|
23
|
-
|
|
24
|
-
return {
|
|
25
|
-
name: "filesystem",
|
|
26
|
-
tasksDir,
|
|
27
|
-
createOrReuseDispatchedTask: (input: CreateDispatchedTaskInput) =>
|
|
28
|
-
createOrReuseDispatchedTask({ ...input, tasksDir: input.tasksDir ?? tasksDir }),
|
|
29
|
-
readTask: (projectDir: string, taskId: string) => readTask(scope(projectDir), taskId),
|
|
30
|
-
readTaskResult: (projectDir: string, taskId: string) => readTaskResult(scope(projectDir), taskId),
|
|
31
|
-
expireTaskIfOverdue: (projectDir: string, taskId: string) => expireTaskIfOverdue(scope(projectDir), taskId),
|
|
32
|
-
waitForTask: (projectDir: string, taskId: string, options: WaitForTaskOptions) => waitForTask(scope(projectDir), taskId, options),
|
|
33
|
-
listInbox: (projectDir: string, parentSession: string, options: ListInboxOptions) =>
|
|
34
|
-
listInbox(scope(projectDir), parentSession, options),
|
|
35
|
-
ackTask: (projectDir: string, taskId: string, parentSession: string) => ackTask(scope(projectDir), taskId, parentSession),
|
|
36
|
-
cancelTask: (projectDir: string, taskId: string, reason: string | undefined) => cancelTask(scope(projectDir), taskId, reason),
|
|
37
|
-
completeTask: (projectDir: string, taskId: string, status: TerminalTaskStatus, payload: TaskResultPayload) =>
|
|
38
|
-
completeTask(scope(projectDir), taskId, status, payload),
|
|
39
|
-
};
|
|
40
|
-
}
|
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
AgentTaskRecord,
|
|
3
|
-
CreateDispatchedTaskInput,
|
|
4
|
-
ListInboxOptions,
|
|
5
|
-
StoredTaskResult,
|
|
6
|
-
TaskResultPayload,
|
|
7
|
-
TerminalTaskStatus,
|
|
8
|
-
WaitForTaskOptions,
|
|
9
|
-
} from "./types";
|
|
10
|
-
import type { CreateOrReuseDispatchedTaskResult } from "./store";
|
|
11
|
-
|
|
12
|
-
export interface TaskCommandResult {
|
|
13
|
-
readonly code: number;
|
|
14
|
-
readonly stdout: string;
|
|
15
|
-
readonly stderr: string;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export type TaskCommandExecutor = (
|
|
19
|
-
command: string,
|
|
20
|
-
args: readonly string[],
|
|
21
|
-
options?: { readonly signal?: AbortSignal },
|
|
22
|
-
) => Promise<TaskCommandResult>;
|
|
23
|
-
|
|
24
|
-
export interface DispatchTaskInput {
|
|
25
|
-
readonly projectDir: string;
|
|
26
|
-
readonly task: AgentTaskRecord;
|
|
27
|
-
readonly target: string;
|
|
28
|
-
readonly assignment: string;
|
|
29
|
-
readonly signal?: AbortSignal;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export type DispatchTaskResult =
|
|
33
|
-
| { readonly ok: true }
|
|
34
|
-
| { readonly ok: false; readonly message: string; readonly retryable: boolean };
|
|
35
|
-
|
|
36
|
-
export interface TaskStore {
|
|
37
|
-
readonly name: string;
|
|
38
|
-
readonly tasksDir: string;
|
|
39
|
-
readonly createOrReuseDispatchedTask: (input: CreateDispatchedTaskInput) => Promise<CreateOrReuseDispatchedTaskResult>;
|
|
40
|
-
readonly readTask: (projectDir: string, taskId: string) => Promise<AgentTaskRecord>;
|
|
41
|
-
readonly readTaskResult: (projectDir: string, taskId: string) => Promise<StoredTaskResult | undefined>;
|
|
42
|
-
readonly expireTaskIfOverdue: (projectDir: string, taskId: string) => Promise<AgentTaskRecord>;
|
|
43
|
-
readonly waitForTask: (projectDir: string, taskId: string, options: WaitForTaskOptions) => Promise<AgentTaskRecord>;
|
|
44
|
-
readonly listInbox: (
|
|
45
|
-
projectDir: string,
|
|
46
|
-
parentSession: string,
|
|
47
|
-
options: ListInboxOptions,
|
|
48
|
-
) => Promise<readonly AgentTaskRecord[]>;
|
|
49
|
-
readonly ackTask: (projectDir: string, taskId: string, parentSession: string) => Promise<AgentTaskRecord>;
|
|
50
|
-
readonly cancelTask: (projectDir: string, taskId: string, reason: string | undefined) => Promise<AgentTaskRecord>;
|
|
51
|
-
readonly completeTask: (
|
|
52
|
-
projectDir: string,
|
|
53
|
-
taskId: string,
|
|
54
|
-
status: TerminalTaskStatus,
|
|
55
|
-
payload: TaskResultPayload,
|
|
56
|
-
) => Promise<AgentTaskRecord>;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
export interface TaskTransport {
|
|
60
|
-
readonly name: string;
|
|
61
|
-
readonly getCurrentSessionName: (env: Record<string, string | undefined>) => string;
|
|
62
|
-
readonly dispatchTask: (input: DispatchTaskInput) => Promise<DispatchTaskResult>;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
export interface TaskCommunicationLayer {
|
|
66
|
-
readonly store: TaskStore;
|
|
67
|
-
readonly transport: TaskTransport;
|
|
68
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import type { DispatchTaskInput, DispatchTaskResult, TaskCommandExecutor, TaskTransport } from "../task-communication";
|
|
2
|
-
|
|
3
|
-
export interface WolfpackTaskTransportOptions {
|
|
4
|
-
readonly exec: TaskCommandExecutor;
|
|
5
|
-
}
|
|
6
|
-
|
|
7
|
-
export function getCurrentWolfpackSessionName(env: Record<string, string | undefined>): string {
|
|
8
|
-
return env.WOLFPACK_SESSION_NAME || "unknown-session";
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function createWolfpackTaskTransport(options: WolfpackTaskTransportOptions): TaskTransport {
|
|
12
|
-
return {
|
|
13
|
-
name: "wolfpack",
|
|
14
|
-
getCurrentSessionName: getCurrentWolfpackSessionName,
|
|
15
|
-
dispatchTask: async (input: DispatchTaskInput): Promise<DispatchTaskResult> => {
|
|
16
|
-
const dispatch = await options.exec("wolfpack", ["session", "send", input.target, input.assignment], {
|
|
17
|
-
signal: input.signal,
|
|
18
|
-
});
|
|
19
|
-
if (dispatch.code === 0) {
|
|
20
|
-
return { ok: true };
|
|
21
|
-
}
|
|
22
|
-
return {
|
|
23
|
-
ok: false,
|
|
24
|
-
message: dispatch.stderr || dispatch.stdout || "wolfpack session send failed",
|
|
25
|
-
retryable: true,
|
|
26
|
-
};
|
|
27
|
-
},
|
|
28
|
-
};
|
|
29
|
-
}
|