@cydm/pie 2.7.13 → 2.7.15
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 +16 -0
- package/dist/builtin/extensions/ask-user/index.js +3 -3
- package/dist/builtin/extensions/cron/index.js +669 -0
- package/dist/builtin/extensions/cron/package.json +11 -0
- package/dist/builtin/extensions/document-attachments/index.js +3 -0
- package/dist/builtin/extensions/im-forward/index.js +2278 -0
- package/dist/builtin/extensions/im-forward/package.json +11 -0
- package/dist/builtin/extensions/pie-ipc/index.js +1969 -0
- package/dist/builtin/extensions/pie-ipc/package.json +11 -0
- package/dist/builtin/extensions/plan-mode/index.js +3 -3
- package/dist/builtin/extensions/subagent/index.js +4 -4
- package/dist/builtin/extensions/todo/index.js +3 -3
- package/dist/chunks/{chunk-CIFC6QVB.js → chunk-C5UTDL3S.js} +339 -47
- package/dist/chunks/{chunk-D7RMNFVM.js → chunk-CLEPSTWH.js} +2 -2
- package/dist/chunks/chunk-F7EBZTZO.js +26 -0
- package/dist/chunks/{chunk-R46YPPTU.js → chunk-NM2JLPJ6.js} +156 -101
- package/dist/chunks/{chunk-HGUYS5SF.js → chunk-TDHVB2FH.js} +1 -1
- package/dist/chunks/{src-5JJZZ5H7.js → src-IY6TEIL7.js} +2 -2
- package/dist/chunks/{test-stream-ZBTSPU7J.js → test-stream-EXSL5TIO.js} +1 -1
- package/dist/cli.js +498 -136
- package/package.json +6 -2
|
@@ -0,0 +1,669 @@
|
|
|
1
|
+
import { createRequire as __createRequire } from "node:module"; const require = __createRequire(import.meta.url);
|
|
2
|
+
import {
|
|
3
|
+
Type
|
|
4
|
+
} from "../../../chunks/chunk-XXGYUYXI.js";
|
|
5
|
+
import "../../../chunks/chunk-LZQXQOPJ.js";
|
|
6
|
+
|
|
7
|
+
// builtin/extensions/cron/index.ts
|
|
8
|
+
import * as path4 from "node:path";
|
|
9
|
+
import * as fs3 from "node:fs";
|
|
10
|
+
import { homedir } from "node:os";
|
|
11
|
+
|
|
12
|
+
// builtin/extensions/cron/store.ts
|
|
13
|
+
import * as fs from "node:fs";
|
|
14
|
+
import * as path from "node:path";
|
|
15
|
+
import { randomUUID } from "node:crypto";
|
|
16
|
+
|
|
17
|
+
// builtin/extensions/cron/schedule.ts
|
|
18
|
+
var MINUTE = 6e4;
|
|
19
|
+
function fields(expression) {
|
|
20
|
+
const parts = expression.trim().split(/\s+/);
|
|
21
|
+
if (parts.length !== 5) throw new Error("cron requires five numeric UTC fields");
|
|
22
|
+
return parts.map((part, i) => {
|
|
23
|
+
const [min, max] = [[0, 59], [0, 23], [1, 31], [1, 12], [0, 6]][i];
|
|
24
|
+
const values = /* @__PURE__ */ new Set();
|
|
25
|
+
for (const item of part.split(",")) {
|
|
26
|
+
if (!/^(\*|\d+(?:-\d+)?)(?:\/\d+)?$/.test(item)) throw new Error("invalid cron field");
|
|
27
|
+
const [range, stepText] = item.split("/");
|
|
28
|
+
const step = stepText === void 0 ? 1 : Number(stepText);
|
|
29
|
+
const ends = range === "*" ? [min, max] : range.split("-").map(Number);
|
|
30
|
+
const a = ends[0], b = ends[1] ?? (stepText === void 0 ? a : max);
|
|
31
|
+
if (a < min || b > max || a > b || step < 1 || step > max + 1) throw new Error("cron field out of bounds");
|
|
32
|
+
for (let n = a; n <= b; n += step) values.add(n);
|
|
33
|
+
}
|
|
34
|
+
return values;
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
function validateSchedule(value) {
|
|
38
|
+
if (!value || typeof value !== "object" || Object.keys(value).length !== 1) throw new Error("provide exactly one of at, every, cron");
|
|
39
|
+
const s = value;
|
|
40
|
+
if ("at" in s && typeof s.at === "string") {
|
|
41
|
+
const m = s.at.match(/^(\d{4})-(\d\d)-(\d\d)T(\d\d):(\d\d)(?::(\d\d)(?:\.\d{1,3})?)?(?:Z|[+-]\d\d:\d\d)$/);
|
|
42
|
+
if (m && +m[1] >= 1970 && +m[2] >= 1 && +m[2] <= 12 && +m[3] >= 1 && +m[3] <= new Date(Date.UTC(+m[1], +m[2], 0)).getUTCDate() && +m[4] < 24 && +m[5] < 60 && +(m[6] ?? 0) < 60 && Number.isFinite(Date.parse(s.at))) return s;
|
|
43
|
+
}
|
|
44
|
+
if ("every" in s && Number.isSafeInteger(s.every) && s.every >= 60 && s.every <= 31536e3) return s;
|
|
45
|
+
if ("cron" in s && typeof s.cron === "string" && s.cron.length <= 256) {
|
|
46
|
+
fields(s.cron);
|
|
47
|
+
return s;
|
|
48
|
+
}
|
|
49
|
+
throw new Error("invalid schedule (every: integer seconds 60..31536000; at: ISO timestamp with timezone)");
|
|
50
|
+
}
|
|
51
|
+
function nextTime(s, after) {
|
|
52
|
+
if ("at" in s) return Date.parse(s.at) > after ? Date.parse(s.at) : null;
|
|
53
|
+
if ("every" in s) return after + s.every * 1e3;
|
|
54
|
+
const f = fields(s.cron), parts = s.cron.trim().split(/\s+/);
|
|
55
|
+
const end = after + 8 * 366 * 864e5;
|
|
56
|
+
for (let t = Math.floor(after / MINUTE) * MINUTE + MINUTE; t <= end; t += MINUTE) {
|
|
57
|
+
const d = new Date(t);
|
|
58
|
+
if (!f[3].has(d.getUTCMonth() + 1) || !f[1].has(d.getUTCHours()) || !f[0].has(d.getUTCMinutes())) continue;
|
|
59
|
+
const dom = f[2].has(d.getUTCDate()), dow = f[4].has(d.getUTCDay());
|
|
60
|
+
if (parts[2] !== "*" && parts[4] !== "*" ? dom || dow : dom && dow) return t;
|
|
61
|
+
}
|
|
62
|
+
throw new Error("cron has no occurrence within eight years");
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// builtin/extensions/cron/store.ts
|
|
66
|
+
var Store = class {
|
|
67
|
+
constructor(dir) {
|
|
68
|
+
this.dir = dir;
|
|
69
|
+
fs.mkdirSync(dir, { recursive: true, mode: 448 });
|
|
70
|
+
}
|
|
71
|
+
dir;
|
|
72
|
+
transaction(fn) {
|
|
73
|
+
const lock = path.join(this.dir, "write.lock");
|
|
74
|
+
fs.mkdirSync(lock, { mode: 448 });
|
|
75
|
+
try {
|
|
76
|
+
const state = this.read();
|
|
77
|
+
const result = fn(state);
|
|
78
|
+
this.save(state);
|
|
79
|
+
return result;
|
|
80
|
+
} finally {
|
|
81
|
+
fs.rmdirSync(lock);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
read() {
|
|
85
|
+
const file = path.join(this.dir, "state.json");
|
|
86
|
+
if (!fs.existsSync(file)) return { version: 1, jobs: [], owners: {} };
|
|
87
|
+
if (fs.statSync(file).size > 4e6) throw new Error("cron state exceeds size limit");
|
|
88
|
+
const s = JSON.parse(fs.readFileSync(file, "utf8"));
|
|
89
|
+
if (s.version !== 1 || !Array.isArray(s.jobs) || !s.owners || typeof s.owners !== "object") throw new Error("invalid cron state; manual recovery required");
|
|
90
|
+
if (Array.isArray(s.owners) || s.jobs.length > 100 || Object.keys(s.owners).length > 1e3) throw new Error("cron state exceeds record limit");
|
|
91
|
+
const ids = /* @__PURE__ */ new Set();
|
|
92
|
+
for (const j of s.jobs) {
|
|
93
|
+
if (!j || typeof j.id !== "string" || ids.has(j.id) || typeof j.sessionId !== "string" || !j.sessionId || j.sessionId.length > 256 || typeof j.cwd !== "string" || !path.isAbsolute(j.cwd) || typeof j.prompt !== "string" || !j.prompt.trim() || j.prompt.length > 16e3 || typeof j.enabled !== "boolean" || !["ready", "running", "blocked"].includes(j.state) || j.next !== null && !Number.isSafeInteger(j.next)) throw new Error("invalid cron job; manual recovery required");
|
|
94
|
+
ids.add(j.id);
|
|
95
|
+
validateSchedule(j.schedule);
|
|
96
|
+
}
|
|
97
|
+
for (const o of Object.values(s.owners)) {
|
|
98
|
+
if (!o || o.sessionId !== null && typeof o.sessionId !== "string" || typeof o.cwd !== "string" || !path.isAbsolute(o.cwd) || typeof o.busy !== "boolean") throw new Error("invalid cron owner; manual recovery required");
|
|
99
|
+
}
|
|
100
|
+
return s;
|
|
101
|
+
}
|
|
102
|
+
save(state) {
|
|
103
|
+
const content = JSON.stringify(state, null, 2);
|
|
104
|
+
const projected = {
|
|
105
|
+
...state,
|
|
106
|
+
owners: Object.fromEntries(Object.entries(state.owners).map(([id, owner]) => [id, { ...owner, busy: false }])),
|
|
107
|
+
jobs: state.jobs.map((job) => ({
|
|
108
|
+
...job,
|
|
109
|
+
enabled: false,
|
|
110
|
+
state: "blocked",
|
|
111
|
+
next: -Number.MAX_SAFE_INTEGER,
|
|
112
|
+
occurrence: -Number.MAX_SAFE_INTEGER,
|
|
113
|
+
target: "x".repeat(64),
|
|
114
|
+
deliveryStarted: false,
|
|
115
|
+
error: "".repeat(500)
|
|
116
|
+
}))
|
|
117
|
+
};
|
|
118
|
+
if (state.jobs.length > 100 || Object.keys(state.owners).length > 1e3 || Buffer.byteLength(content, "utf8") > 4e6 || Buffer.byteLength(JSON.stringify(projected, null, 2), "utf8") > 4e6) throw new Error("cron state exceeds storage budget; no changes saved");
|
|
119
|
+
const temp = path.join(this.dir, `state.${randomUUID()}.tmp`), dest = path.join(this.dir, "state.json");
|
|
120
|
+
const fd = fs.openSync(temp, "wx", 384);
|
|
121
|
+
try {
|
|
122
|
+
fs.writeFileSync(fd, content);
|
|
123
|
+
fs.fsyncSync(fd);
|
|
124
|
+
} finally {
|
|
125
|
+
fs.closeSync(fd);
|
|
126
|
+
}
|
|
127
|
+
fs.renameSync(temp, dest);
|
|
128
|
+
if (process.platform !== "win32") {
|
|
129
|
+
const dir = fs.openSync(this.dir, "r");
|
|
130
|
+
try {
|
|
131
|
+
fs.fsyncSync(dir);
|
|
132
|
+
} finally {
|
|
133
|
+
fs.closeSync(dir);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
add(sessionId, cwd, prompt, schedule, now = Date.now()) {
|
|
138
|
+
if (!sessionId || sessionId.length > 256) throw new Error("an existing session is required");
|
|
139
|
+
if (typeof prompt !== "string" || !prompt.trim() || prompt.length > 16e3) throw new Error("prompt must contain 1..16000 characters");
|
|
140
|
+
const validated = validateSchedule(schedule), next = nextTime(validated, now);
|
|
141
|
+
if (next === null) throw new Error("at must be in the future");
|
|
142
|
+
const job = { id: randomUUID(), sessionId, cwd: fs.realpathSync(cwd), prompt, schedule: validated, enabled: true, next, state: "ready" };
|
|
143
|
+
return this.transaction((s) => {
|
|
144
|
+
if (s.jobs.length >= 100) throw new Error("maximum 100 jobs");
|
|
145
|
+
s.jobs.push(job);
|
|
146
|
+
return job;
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
change(id, action, now = Date.now()) {
|
|
150
|
+
this.transaction((s) => {
|
|
151
|
+
const j = s.jobs.find((j2) => j2.id === id);
|
|
152
|
+
if (!j) throw new Error("unknown job");
|
|
153
|
+
if (j.state === "running") throw new Error("job is running; wait for completion or recover offline");
|
|
154
|
+
if (action === "remove") {
|
|
155
|
+
s.jobs = s.jobs.filter((j2) => j2.id !== id);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
j.enabled = action === "resume";
|
|
159
|
+
if (j.enabled) {
|
|
160
|
+
j.next = nextTime(j.schedule, Math.max(now, j.occurrence ?? now));
|
|
161
|
+
if (j.next === null) throw new Error("one-shot expired; create a new job");
|
|
162
|
+
j.state = "ready";
|
|
163
|
+
delete j.error;
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
};
|
|
168
|
+
function complete(job, error, now = Date.now()) {
|
|
169
|
+
delete job.target;
|
|
170
|
+
delete job.deliveryStarted;
|
|
171
|
+
if (error) {
|
|
172
|
+
job.state = "blocked";
|
|
173
|
+
job.error = error.slice(0, 500);
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
job.state = "ready";
|
|
177
|
+
delete job.error;
|
|
178
|
+
job.next = "at" in job.schedule ? null : nextTime(job.schedule, Math.max(now, job.occurrence ?? now));
|
|
179
|
+
if (job.next === null) job.enabled = false;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// builtin/extensions/cron/runtime.ts
|
|
183
|
+
import * as fs2 from "node:fs";
|
|
184
|
+
import * as path3 from "node:path";
|
|
185
|
+
import { randomUUID as randomUUID3 } from "node:crypto";
|
|
186
|
+
|
|
187
|
+
// builtin/extensions/cron/worker.ts
|
|
188
|
+
import { spawn } from "node:child_process";
|
|
189
|
+
import { randomUUID as randomUUID2 } from "node:crypto";
|
|
190
|
+
import * as path2 from "node:path";
|
|
191
|
+
import { StringDecoder } from "node:string_decoder";
|
|
192
|
+
function runWorker(job, signal, cli = process.argv[1], configDir) {
|
|
193
|
+
if (signal.aborted) return Promise.reject(new Error("worker interrupted before launch"));
|
|
194
|
+
return new Promise((resolve3, reject) => {
|
|
195
|
+
const child = spawn(process.execPath, [path2.resolve(cli), "--json-events", "--session-id", job.sessionId, "--resume-required"], {
|
|
196
|
+
cwd: job.cwd,
|
|
197
|
+
env: { ...process.env, ...configDir ? { PIE_CODING_AGENT_DIR: configDir } : {}, PIE_CRON_ROLE: "worker" },
|
|
198
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
199
|
+
windowsHide: true
|
|
200
|
+
});
|
|
201
|
+
const decoder = new StringDecoder("utf8");
|
|
202
|
+
const turnId = randomUUID2();
|
|
203
|
+
let buffer = "", settled = false, sent = false, completed = false;
|
|
204
|
+
let failure;
|
|
205
|
+
let killTimer;
|
|
206
|
+
const stop = () => {
|
|
207
|
+
failure ??= "worker interrupted; outcome uncertain";
|
|
208
|
+
child.kill();
|
|
209
|
+
killTimer ??= setTimeout(() => child.kill("SIGKILL"), 5e3);
|
|
210
|
+
killTimer.unref();
|
|
211
|
+
};
|
|
212
|
+
const protocolError = (message) => {
|
|
213
|
+
failure = message;
|
|
214
|
+
stop();
|
|
215
|
+
};
|
|
216
|
+
signal.addEventListener("abort", stop, { once: true });
|
|
217
|
+
const timer = setTimeout(stop, 15 * 6e4);
|
|
218
|
+
timer.unref();
|
|
219
|
+
child.stdin.on("error", () => {
|
|
220
|
+
});
|
|
221
|
+
child.stderr.resume();
|
|
222
|
+
child.stdout.on("data", (chunk) => {
|
|
223
|
+
buffer += decoder.write(chunk);
|
|
224
|
+
if (buffer.length > 2e6) {
|
|
225
|
+
protocolError("oversized worker event; outcome uncertain");
|
|
226
|
+
return;
|
|
227
|
+
}
|
|
228
|
+
let index;
|
|
229
|
+
while ((index = buffer.indexOf("\n")) >= 0) {
|
|
230
|
+
const line = buffer.slice(0, index);
|
|
231
|
+
buffer = buffer.slice(index + 1);
|
|
232
|
+
try {
|
|
233
|
+
const event = JSON.parse(line);
|
|
234
|
+
if (!event || typeof event.type !== "string") throw new Error("invalid event");
|
|
235
|
+
if (event.type === "agent.ready") {
|
|
236
|
+
if (sent || event.sessionId !== job.sessionId) {
|
|
237
|
+
protocolError("worker readiness/session mismatch");
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
sent = true;
|
|
241
|
+
child.stdin.write(JSON.stringify({ type: "turn.request", sessionId: job.sessionId, turnId, prompt: job.prompt }) + "\n");
|
|
242
|
+
}
|
|
243
|
+
if (["turn.completed", "turn.failed", "turn.cancelled", "turn.stalled", "turn.settled"].includes(event.type)) {
|
|
244
|
+
if (!sent || event.sessionId !== job.sessionId || event.turnId !== turnId || settled) {
|
|
245
|
+
protocolError("worker turn identity/order mismatch");
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
248
|
+
if (event.type === "turn.completed") {
|
|
249
|
+
if (completed) {
|
|
250
|
+
protocolError("duplicate worker completion");
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
completed = true;
|
|
254
|
+
} else if (event.type === "turn.settled") {
|
|
255
|
+
if (!completed) {
|
|
256
|
+
protocolError("worker settled without completion");
|
|
257
|
+
return;
|
|
258
|
+
}
|
|
259
|
+
settled = true;
|
|
260
|
+
child.stdin.end(JSON.stringify({ type: "shutdown" }) + "\n");
|
|
261
|
+
} else {
|
|
262
|
+
protocolError(`worker ${event.type}`);
|
|
263
|
+
return;
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
if (event.type === "error") {
|
|
267
|
+
protocolError("worker protocol error; inspect session before resuming");
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
} catch {
|
|
271
|
+
protocolError("invalid worker output");
|
|
272
|
+
return;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
});
|
|
276
|
+
child.on("error", (error) => {
|
|
277
|
+
failure = error.message;
|
|
278
|
+
});
|
|
279
|
+
child.on("close", (code) => {
|
|
280
|
+
clearTimeout(timer);
|
|
281
|
+
clearTimeout(killTimer);
|
|
282
|
+
signal.removeEventListener("abort", stop);
|
|
283
|
+
buffer += decoder.end();
|
|
284
|
+
if (buffer.trim()) failure ??= "truncated worker event; outcome uncertain";
|
|
285
|
+
if (failure || !settled || !completed || code !== 0) reject(new Error(failure ?? `worker exited ${code}; outcome uncertain`));
|
|
286
|
+
else resolve3();
|
|
287
|
+
});
|
|
288
|
+
if (signal.aborted) stop();
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
// builtin/extensions/cron/runtime.ts
|
|
293
|
+
var delay = (ms) => new Promise((resolve3) => setTimeout(resolve3, ms));
|
|
294
|
+
var isContention = (error) => error?.code === "EEXIST";
|
|
295
|
+
var Runtime = class {
|
|
296
|
+
constructor(ctx, store, execute = runWorker) {
|
|
297
|
+
this.ctx = ctx;
|
|
298
|
+
this.store = store;
|
|
299
|
+
this.execute = execute;
|
|
300
|
+
this.cwd = fs2.realpathSync(ctx.cwd);
|
|
301
|
+
}
|
|
302
|
+
ctx;
|
|
303
|
+
store;
|
|
304
|
+
execute;
|
|
305
|
+
id = randomUUID3();
|
|
306
|
+
sessionId = null;
|
|
307
|
+
leader = false;
|
|
308
|
+
recovering = false;
|
|
309
|
+
stopped = false;
|
|
310
|
+
initialized = false;
|
|
311
|
+
binding = 0;
|
|
312
|
+
polling = false;
|
|
313
|
+
timer;
|
|
314
|
+
abort = new AbortController();
|
|
315
|
+
worker;
|
|
316
|
+
cwd;
|
|
317
|
+
notices = /* @__PURE__ */ new Set();
|
|
318
|
+
completions = /* @__PURE__ */ new Map();
|
|
319
|
+
accepted;
|
|
320
|
+
get ready() {
|
|
321
|
+
return this.initialized && !this.stopped;
|
|
322
|
+
}
|
|
323
|
+
get generation() {
|
|
324
|
+
return this.binding;
|
|
325
|
+
}
|
|
326
|
+
onStart() {
|
|
327
|
+
if (this.accepted?.consumed) this.accepted.busy = true;
|
|
328
|
+
}
|
|
329
|
+
onMessage(message) {
|
|
330
|
+
if (!this.accepted || message.role !== "user") return;
|
|
331
|
+
const text = typeof message.content === "string" ? message.content : message.content.filter((block) => block.type === "text").map((block) => block.text).join("\n");
|
|
332
|
+
if (text.includes(this.accepted.content)) {
|
|
333
|
+
this.accepted.consumed = true;
|
|
334
|
+
this.accepted.busy = true;
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
onFailure() {
|
|
338
|
+
if (this.accepted?.consumed) this.accepted.error = "interactive turn failed or cancelled; inspect before resuming";
|
|
339
|
+
}
|
|
340
|
+
start() {
|
|
341
|
+
this.initialized = false;
|
|
342
|
+
this.timer = setInterval(() => void this.poll(), 1e3);
|
|
343
|
+
this.timer.unref();
|
|
344
|
+
}
|
|
345
|
+
markUnsafe() {
|
|
346
|
+
fs2.mkdirSync(path3.join(this.store.dir, "unsafe.lock"), { recursive: true, mode: 448 });
|
|
347
|
+
}
|
|
348
|
+
persistOwner(sessionId) {
|
|
349
|
+
this.store.transaction((state) => {
|
|
350
|
+
state.owners[this.id] = { sessionId, cwd: this.cwd, busy: !this.ctx.isIdle() };
|
|
351
|
+
});
|
|
352
|
+
this.sessionId = sessionId;
|
|
353
|
+
this.initialized = true;
|
|
354
|
+
}
|
|
355
|
+
register(sessionId) {
|
|
356
|
+
try {
|
|
357
|
+
this.persistOwner(sessionId);
|
|
358
|
+
} catch (error) {
|
|
359
|
+
this.markUnsafe();
|
|
360
|
+
throw error;
|
|
361
|
+
}
|
|
362
|
+
}
|
|
363
|
+
async bind(sessionId) {
|
|
364
|
+
const generation = ++this.binding;
|
|
365
|
+
this.initialized = false;
|
|
366
|
+
for (let attempt = 0; attempt < 20; attempt++) {
|
|
367
|
+
if (this.stopped || generation !== this.binding) return;
|
|
368
|
+
try {
|
|
369
|
+
this.persistOwner(sessionId);
|
|
370
|
+
return;
|
|
371
|
+
} catch (error) {
|
|
372
|
+
if (!isContention(error)) break;
|
|
373
|
+
await delay(25 + Math.floor(Math.random() * 25));
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
if (!this.stopped && generation === this.binding) this.register(sessionId);
|
|
377
|
+
}
|
|
378
|
+
notify(job) {
|
|
379
|
+
if (job.sessionId !== this.sessionId || !job.error) return;
|
|
380
|
+
const key = job.id + job.error;
|
|
381
|
+
if (!this.notices.has(key)) {
|
|
382
|
+
this.notices.add(key);
|
|
383
|
+
this.ctx.ui.notify(`cron ${job.id}: ${job.error}`, "warning");
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
flushCompletions() {
|
|
387
|
+
for (const [id, error] of this.completions) {
|
|
388
|
+
try {
|
|
389
|
+
this.store.transaction((state) => {
|
|
390
|
+
const job = state.jobs.find((item) => item.id === id);
|
|
391
|
+
if (job?.state === "running") complete(job, error);
|
|
392
|
+
});
|
|
393
|
+
this.completions.delete(id);
|
|
394
|
+
} catch (failure) {
|
|
395
|
+
if (!isContention(failure)) this.markUnsafe();
|
|
396
|
+
return;
|
|
397
|
+
}
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
finish(id, error) {
|
|
401
|
+
this.completions.set(id, error);
|
|
402
|
+
this.flushCompletions();
|
|
403
|
+
}
|
|
404
|
+
async poll(now = Date.now()) {
|
|
405
|
+
if (this.stopped || this.polling || !this.initialized) return;
|
|
406
|
+
if (fs2.existsSync(path3.join(this.store.dir, "unsafe.lock"))) {
|
|
407
|
+
this.abort.abort();
|
|
408
|
+
this.ctx.ui.setStatus?.("cron", void 0);
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
this.polling = true;
|
|
412
|
+
try {
|
|
413
|
+
this.flushCompletions();
|
|
414
|
+
if (this.completions.size) return;
|
|
415
|
+
if (!this.leader) {
|
|
416
|
+
try {
|
|
417
|
+
fs2.mkdirSync(path3.join(this.store.dir, "scheduler.lock"), { mode: 448 });
|
|
418
|
+
this.leader = true;
|
|
419
|
+
this.recovering = true;
|
|
420
|
+
} catch (error) {
|
|
421
|
+
if (!isContention(error)) throw error;
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
let delivery, launch;
|
|
425
|
+
this.store.transaction((state2) => {
|
|
426
|
+
if (this.recovering) {
|
|
427
|
+
for (const job of state2.jobs) {
|
|
428
|
+
if (job.state === "running" && (job.target === "worker" || !job.target || !state2.owners[job.target])) {
|
|
429
|
+
complete(job, "coordinator restarted; outcome uncertain");
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
state2.owners[this.id] = { sessionId: this.sessionId, cwd: this.cwd, busy: !this.ctx.isIdle() };
|
|
434
|
+
for (const job of state2.jobs) {
|
|
435
|
+
const owners = Object.entries(state2.owners).filter(([, owner]) => owner.sessionId === job.sessionId);
|
|
436
|
+
if (this.leader && job.state === "running" && job.target === "worker" && owners.length) this.abort.abort();
|
|
437
|
+
if (job.enabled && owners.length > 1 && job.state !== "running") complete(job, "duplicate participating session owners; close duplicate and resume job");
|
|
438
|
+
if (job.state === "running" && job.target === this.id && this.accepted?.id === job.id) {
|
|
439
|
+
if (!this.accepted.consumed) {
|
|
440
|
+
for (const message of this.ctx.getMessages?.() ?? []) this.onMessage(message);
|
|
441
|
+
}
|
|
442
|
+
if (owners.length !== 1 || this.sessionId !== job.sessionId) {
|
|
443
|
+
complete(job, "owner changed or duplicate during delivery");
|
|
444
|
+
this.accepted = void 0;
|
|
445
|
+
} else if (this.accepted.consumed && this.accepted.busy && this.ctx.isIdle()) {
|
|
446
|
+
complete(job, this.accepted.error);
|
|
447
|
+
this.accepted = void 0;
|
|
448
|
+
} else if (!this.accepted.consumed && now - this.accepted.at > 6e4) {
|
|
449
|
+
complete(job, "input accepted but execution not observed; inspect before resuming");
|
|
450
|
+
this.accepted = void 0;
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
if (job.state === "running" && job.target === this.id && !job.deliveryStarted && this.ctx.isIdle()) {
|
|
454
|
+
if (owners.length !== 1 || owners[0][0] !== this.id || this.sessionId !== job.sessionId || this.cwd !== job.cwd) {
|
|
455
|
+
complete(job, "owner changed or duplicate; delivery blocked");
|
|
456
|
+
} else {
|
|
457
|
+
job.deliveryStarted = true;
|
|
458
|
+
delivery = { ...job };
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
if (this.leader && !state2.jobs.some((job) => job.state === "running")) {
|
|
463
|
+
const due = state2.jobs.filter((job) => job.enabled && job.state === "ready" && job.next !== null && job.next <= now).sort((a, b) => a.next - b.next);
|
|
464
|
+
for (const job of due) {
|
|
465
|
+
if (state2.jobs.some((other) => other.enabled && other.sessionId === job.sessionId && other.state === "blocked")) continue;
|
|
466
|
+
const owners = Object.entries(state2.owners).filter(([, owner]) => owner.sessionId === job.sessionId);
|
|
467
|
+
if (owners.length > 1) {
|
|
468
|
+
complete(job, "duplicate participating session owners");
|
|
469
|
+
continue;
|
|
470
|
+
}
|
|
471
|
+
if (owners.length === 1 && owners[0][1].cwd !== job.cwd) {
|
|
472
|
+
complete(job, "owner cwd differs from bound cwd");
|
|
473
|
+
continue;
|
|
474
|
+
}
|
|
475
|
+
if (owners[0]?.[1].busy) continue;
|
|
476
|
+
job.state = "running";
|
|
477
|
+
job.occurrence = job.next;
|
|
478
|
+
job.target = owners[0]?.[0] ?? "worker";
|
|
479
|
+
if (job.target === "worker") launch = { ...job };
|
|
480
|
+
break;
|
|
481
|
+
}
|
|
482
|
+
}
|
|
483
|
+
});
|
|
484
|
+
this.recovering = false;
|
|
485
|
+
if (delivery) {
|
|
486
|
+
const job = delivery;
|
|
487
|
+
const scope = this.ctx.beginUserInput?.(() => !this.stopped && this.ready && this.sessionId === job.sessionId && !fs2.existsSync(path3.join(this.store.dir, "unsafe.lock")) && Object.values(this.store.read().owners).filter((owner) => owner.sessionId === job.sessionId).length === 1);
|
|
488
|
+
let error;
|
|
489
|
+
const content = `[Cron job=${job.id} scheduledAt=${job.occurrence}]
|
|
490
|
+
${job.prompt}`;
|
|
491
|
+
this.accepted = { id: job.id, at: now, busy: false, consumed: false, content };
|
|
492
|
+
try {
|
|
493
|
+
if (!scope || !await scope.submit(content)) error = "input rejected or session changed; inspect before resuming";
|
|
494
|
+
} catch (failure) {
|
|
495
|
+
error = `input outcome uncertain: ${String(failure)}`;
|
|
496
|
+
} finally {
|
|
497
|
+
scope?.dispose();
|
|
498
|
+
}
|
|
499
|
+
if (error) {
|
|
500
|
+
this.accepted = void 0;
|
|
501
|
+
this.finish(job.id, error);
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (launch) {
|
|
505
|
+
const job = launch;
|
|
506
|
+
const latest = this.store.read();
|
|
507
|
+
if (this.stopped || fs2.existsSync(path3.join(this.store.dir, "unsafe.lock")) || Object.values(latest.owners).some((owner) => owner.sessionId === job.sessionId)) {
|
|
508
|
+
this.finish(job.id, "ownership changed before worker launch; inspect before resuming");
|
|
509
|
+
return;
|
|
510
|
+
}
|
|
511
|
+
this.abort = new AbortController();
|
|
512
|
+
this.worker = this.execute(job, this.abort.signal, void 0, path3.dirname(this.store.dir)).then(() => this.finish(job.id), (error) => this.finish(job.id, String(error))).catch(() => {
|
|
513
|
+
try {
|
|
514
|
+
this.markUnsafe();
|
|
515
|
+
} catch {
|
|
516
|
+
}
|
|
517
|
+
});
|
|
518
|
+
}
|
|
519
|
+
const state = this.store.read(), enabled = state.jobs.filter((job) => job.enabled);
|
|
520
|
+
this.ctx.ui.setStatus?.("cron", this.leader && enabled.some((job) => job.sessionId === this.sessionId) ? `cron: ${enabled.length} enabled, ${enabled.filter((job) => job.state === "blocked").length} blocked` : void 0);
|
|
521
|
+
for (const job of state.jobs) this.notify(job);
|
|
522
|
+
} catch {
|
|
523
|
+
this.ctx.ui.setStatus?.("cron", void 0);
|
|
524
|
+
} finally {
|
|
525
|
+
this.polling = false;
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
async stop() {
|
|
529
|
+
this.stopped = true;
|
|
530
|
+
clearInterval(this.timer);
|
|
531
|
+
this.abort.abort();
|
|
532
|
+
await this.worker;
|
|
533
|
+
while (this.polling) await delay(10);
|
|
534
|
+
for (let attempt = 0; ; attempt++) {
|
|
535
|
+
try {
|
|
536
|
+
this.store.transaction((state) => {
|
|
537
|
+
for (const [id, error] of this.completions) {
|
|
538
|
+
const job = state.jobs.find((item) => item.id === id);
|
|
539
|
+
if (job?.state === "running") complete(job, error);
|
|
540
|
+
}
|
|
541
|
+
delete state.owners[this.id];
|
|
542
|
+
for (const job of state.jobs) {
|
|
543
|
+
if (job.state === "running" && job.target === this.id) complete(job, "owner closed before delivery; inspect before resuming");
|
|
544
|
+
}
|
|
545
|
+
});
|
|
546
|
+
this.completions.clear();
|
|
547
|
+
break;
|
|
548
|
+
} catch (error) {
|
|
549
|
+
if (!isContention(error) || attempt >= 39) throw error;
|
|
550
|
+
await delay(25 + Math.floor(Math.random() * 25));
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
if (this.leader) {
|
|
554
|
+
fs2.rmdirSync(path3.join(this.store.dir, "scheduler.lock"));
|
|
555
|
+
this.leader = false;
|
|
556
|
+
}
|
|
557
|
+
this.ctx.ui.setStatus?.("cron", void 0);
|
|
558
|
+
}
|
|
559
|
+
};
|
|
560
|
+
|
|
561
|
+
// builtin/extensions/cron/index.ts
|
|
562
|
+
function configPath() {
|
|
563
|
+
const env = process.env.PIE_CODING_AGENT_DIR;
|
|
564
|
+
return path4.resolve(env === "~" ? homedir() : env?.startsWith("~/") ? path4.join(homedir(), env.slice(2)) : env || path4.join(homedir(), ".pie"));
|
|
565
|
+
}
|
|
566
|
+
function cron(ctx) {
|
|
567
|
+
if (process.env.PIE_CRON_ROLE === "worker" || !ctx.hasUI) return;
|
|
568
|
+
const store = new Store(path4.join(configPath(), "cron"));
|
|
569
|
+
const runtime = new Runtime(ctx, store);
|
|
570
|
+
let startupError;
|
|
571
|
+
try {
|
|
572
|
+
runtime.start();
|
|
573
|
+
} catch (e) {
|
|
574
|
+
startupError = String(e);
|
|
575
|
+
}
|
|
576
|
+
ctx.registerCleanup?.(() => runtime.stop());
|
|
577
|
+
ctx.on("agent:start", () => runtime.onStart());
|
|
578
|
+
ctx.on("message:end", (e) => runtime.onMessage(e.message));
|
|
579
|
+
ctx.on("turn:failed", () => runtime.onFailure());
|
|
580
|
+
ctx.on("turn:cancelled", () => runtime.onFailure());
|
|
581
|
+
ctx.on("session:changed", async (e) => {
|
|
582
|
+
try {
|
|
583
|
+
await runtime.bind(e.sessionId);
|
|
584
|
+
} catch (e2) {
|
|
585
|
+
startupError = String(e2);
|
|
586
|
+
}
|
|
587
|
+
});
|
|
588
|
+
ctx.on("terminal:input", (e) => {
|
|
589
|
+
if (/^\/cron(?:\s|$)/.test(e.text)) return;
|
|
590
|
+
try {
|
|
591
|
+
const state = store.read();
|
|
592
|
+
if (!state.jobs.some((j) => j.sessionId === runtime.sessionId && (j.enabled || j.state === "running"))) return;
|
|
593
|
+
const duplicates = Object.values(state.owners).filter((o) => o.sessionId === runtime.sessionId).length > 1;
|
|
594
|
+
const worker = state.jobs.some((j) => j.sessionId === runtime.sessionId && j.state === "running" && j.target === "worker");
|
|
595
|
+
if (!startupError && runtime.ready && !duplicates && !worker && !fs3.existsSync(path4.join(store.dir, "unsafe.lock"))) return;
|
|
596
|
+
} catch {
|
|
597
|
+
}
|
|
598
|
+
ctx.ui.notify("cron: session ownership is uncertain or duplicated; input blocked. Close the duplicate/worker and inspect /cron.", "warning");
|
|
599
|
+
return { action: "consume" };
|
|
600
|
+
});
|
|
601
|
+
ctx.on("session:before_switch", (e) => {
|
|
602
|
+
if (!e.targetSessionId) return;
|
|
603
|
+
try {
|
|
604
|
+
const s = store.read();
|
|
605
|
+
const relevant = s.jobs.some((j) => j.sessionId === e.targetSessionId && (j.enabled || j.state === "running"));
|
|
606
|
+
if (relevant && (Object.entries(s.owners).some(([id, o]) => id !== runtime.id && o.sessionId === e.targetSessionId) || s.jobs.some((j) => j.sessionId === e.targetSessionId && j.state === "running"))) {
|
|
607
|
+
ctx.ui.notify("cron: target session is owned or has an uncertain occurrence. Switch cancelled.", "warning");
|
|
608
|
+
return { cancel: true };
|
|
609
|
+
}
|
|
610
|
+
} catch {
|
|
611
|
+
return { cancel: true };
|
|
612
|
+
}
|
|
613
|
+
});
|
|
614
|
+
const run = (action, id, prompt, schedule) => {
|
|
615
|
+
if (startupError) throw new Error(`cron unavailable: ${startupError}; see docs/cron.md recovery`);
|
|
616
|
+
if (action === "list") return JSON.stringify({ ...store.read(), recoveryRequired: fs3.existsSync(path4.join(store.dir, "unsafe.lock")), coordinatorPresent: fs3.existsSync(path4.join(store.dir, "scheduler.lock")) }, null, 2);
|
|
617
|
+
if (action === "add") {
|
|
618
|
+
if (!runtime.ready || !runtime.sessionId) throw new Error("wait for an existing session to load and register");
|
|
619
|
+
return JSON.stringify({
|
|
620
|
+
job: store.add(runtime.sessionId, ctx.cwd, prompt, schedule),
|
|
621
|
+
note: "Saved. The original session/cwd will be resumed automatically. Keep at least one cron-enabled Pie open; no execution while all are closed or the computer sleeps."
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
if (action === "pause" || action === "resume" || action === "remove") {
|
|
625
|
+
store.change(id ?? "", action);
|
|
626
|
+
return `${action}: ${id}`;
|
|
627
|
+
}
|
|
628
|
+
throw new Error('Usage: /cron list | add {"prompt":"...","schedule":{"every":3600}} | pause|resume|remove <id>');
|
|
629
|
+
};
|
|
630
|
+
ctx.registerCommand({ path: ["cron"], description: "Persistent same-session schedules (UTC): list/add/pause/resume/remove", handler: async (_ctx, args) => {
|
|
631
|
+
try {
|
|
632
|
+
const match = (args ?? "").trim().match(/^(\S+)(?:\s+([\s\S]*))?$/);
|
|
633
|
+
const action = match?.[1] ?? "list", rest = match?.[2];
|
|
634
|
+
const data = action === "add" ? JSON.parse(rest ?? "{}") : void 0;
|
|
635
|
+
ctx.ui.notify(run(action, rest, data?.prompt, data?.schedule), "info");
|
|
636
|
+
} catch (e) {
|
|
637
|
+
ctx.ui.notify(String(e), "warning");
|
|
638
|
+
}
|
|
639
|
+
} });
|
|
640
|
+
ctx.registerTool({
|
|
641
|
+
name: "cron_manage",
|
|
642
|
+
label: "Cron",
|
|
643
|
+
description: "Manage persistent scheduled prompts bound to THIS existing session and cwd. No new sessions. Actions list/add/pause/resume/remove. every is seconds (minimum 60), at is ISO with timezone, cron is five numeric UTC fields. Missed intervals coalesce; blocked/uncertain tasks require human review before resume.",
|
|
644
|
+
parameters: Type.Object({ action: Type.Union(["list", "add", "pause", "resume", "remove"].map((s) => Type.Literal(s))), id: Type.Optional(Type.String()), prompt: Type.Optional(Type.String({ maxLength: 16e3 })), schedule: Type.Optional(Type.Union([Type.Object({ at: Type.String() }), Type.Object({ every: Type.Integer({ minimum: 60, maximum: 31536e3 }) }), Type.Object({ cron: Type.String({ maxLength: 256 }) })])) }),
|
|
645
|
+
risk: { riskClass: "external_side_effect", permissionScope: "host", availableInPlanMode: false, requiresPermission: true, externalSideEffect: true, allowedForSubagentByDefault: false, concurrencySafe: false },
|
|
646
|
+
execute: async (args) => {
|
|
647
|
+
try {
|
|
648
|
+
if (args.action !== "list") {
|
|
649
|
+
const sessionId = runtime.sessionId, generation = runtime.generation;
|
|
650
|
+
if (!runtime.ready) throw new Error("session binding is in progress; no changes saved");
|
|
651
|
+
if (!ctx.interactions.isAvailable() || !await ctx.ui.confirm(
|
|
652
|
+
"Approve cron change",
|
|
653
|
+
JSON.stringify({ action: args.action, id: args.id, sessionId, cwd: ctx.cwd, prompt: args.prompt, schedule: args.schedule }) + "\nTasks persist and may execute later while any participating Pie is open, using saved configuration."
|
|
654
|
+
)) {
|
|
655
|
+
throw new Error("cron change was not approved; no changes saved");
|
|
656
|
+
}
|
|
657
|
+
if (!runtime.ready || generation !== runtime.generation || sessionId !== runtime.sessionId) throw new Error("session changed during approval; no changes saved");
|
|
658
|
+
}
|
|
659
|
+
return { content: [{ type: "text", text: run(args.action, args.id, args.prompt, args.schedule) }] };
|
|
660
|
+
} catch (e) {
|
|
661
|
+
return { content: [{ type: "text", text: String(e) }], isError: true };
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
});
|
|
665
|
+
}
|
|
666
|
+
export {
|
|
667
|
+
configPath,
|
|
668
|
+
cron as default
|
|
669
|
+
};
|