@alphacoder-v0/pi-loops 0.18.0 → 0.19.2
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/CHANGELOG.md +52 -0
- package/README.md +6 -6
- package/README.zh-CN.md +6 -6
- package/docs/cli.md +6 -2
- package/docs/configuration.md +1 -1
- package/docs/design.md +20 -6
- package/docs/loops.md +25 -17
- package/docs/triggers.md +1 -1
- package/docs/troubleshooting.md +3 -2
- package/docs/web-ui-parity.md +7 -3
- package/package.json +1 -1
- package/skills/pi-loops/SKILL.md +2 -1
- package/src/archive.ts +5 -5
- package/src/args.ts +1 -8
- package/src/cli.ts +17 -5
- package/src/job-edit.ts +0 -3
- package/src/job-health.ts +6 -7
- package/src/job-owner.ts +29 -0
- package/src/pi-loops.ts +18 -25
- package/src/protocol.ts +2 -3
- package/src/scheduler.ts +13 -13
- package/src/session-head.ts +98 -0
- package/src/store.ts +7 -17
- package/src/tools.ts +14 -19
- package/src/trigger-runtime.ts +3 -5
- package/src/triggers.ts +3 -4
- package/src/web.mjs +35 -16
package/src/web.mjs
CHANGED
|
@@ -452,8 +452,7 @@ function nextRuns() {
|
|
|
452
452
|
// This machine's file. Leadership is per host, and so is the clock a cron expression is matched
|
|
453
453
|
// against, so on a shared `$HOME` each machine keeps its own answers rather than overwriting the
|
|
454
454
|
// other's with times computed in a different timezone.
|
|
455
|
-
const
|
|
456
|
-
const doc = readJson(path.join(LOOPS_DIR, `next-runs.${safe}.json`), { next: {} });
|
|
455
|
+
const doc = readJson(path.join(LOOPS_DIR, "next-runs.json"), { next: {} });
|
|
457
456
|
return doc?.next && typeof doc.next === "object" ? doc.next : {};
|
|
458
457
|
}
|
|
459
458
|
|
|
@@ -467,16 +466,13 @@ function automation(cwd) {
|
|
|
467
466
|
// The directory is the evidence, not any one file in it: a project with rules and no cron jobs
|
|
468
467
|
// has no jobs.json at all, and reporting "pi-loops not found" there would be a lie.
|
|
469
468
|
if (!fs.existsSync(LOOPS_DIR)) return { installed: false, dir: LOOPS_DIR };
|
|
469
|
+
// A plain job runs only in the session that created it; the id in this session's header says
|
|
470
|
+
// whether that is this one, and the panel says "resume it" rather than promising a time.
|
|
471
|
+
const currentSession = sessionFile ? readHead(sessionFile)?.id : undefined;
|
|
470
472
|
const jobsFile = readJson(path.join(LOOPS_DIR, "jobs.json"), { jobs: [] });
|
|
471
473
|
/**
|
|
472
|
-
* This project's,
|
|
473
|
-
*
|
|
474
|
-
* The host used to be part of this test, so a job stamped with another hostname vanished from
|
|
475
|
-
* the panel — and a hostname changes on its own: a rebuilt container, a machine renamed by
|
|
476
|
-
* DHCP, a restored backup. What you got was a job sitting enabled in `jobs.json`, never running,
|
|
477
|
-
* invisible in the one place you would look. The terminal always listed it and said whose it
|
|
478
|
-
* was; so does this, and the count of everything in other projects goes with it, because a list
|
|
479
|
-
* that silently drops things is worse than a longer list.
|
|
474
|
+
* This project's jobs, and the count of everything in other projects goes with it, because a
|
|
475
|
+
* list that silently drops things is worse than a longer list.
|
|
480
476
|
*/
|
|
481
477
|
const inThisProject = (j) => sameProject(j.cwd, cwd);
|
|
482
478
|
const allJobs = jobsFile.jobs ?? [];
|
|
@@ -492,7 +488,6 @@ function automation(cwd) {
|
|
|
492
488
|
lastError: j.lastError,
|
|
493
489
|
running: !!j.running,
|
|
494
490
|
// Set only when it is not this machine's: the panel says so, and nothing else has to guess.
|
|
495
|
-
otherHost: j.host && j.host !== HOST ? j.host : undefined,
|
|
496
491
|
// What to type to fix it. The terminal's version of this line says `/cron set <n>`, where n
|
|
497
492
|
// is the position in a numbered list — which this panel does not have, so telling someone to
|
|
498
493
|
// use one was telling them to go and find a terminal. A name or a full id resolves anywhere.
|
|
@@ -502,7 +497,9 @@ function automation(cwd) {
|
|
|
502
497
|
// follows. A time that has already passed is a stale answer rather than a next run (the job
|
|
503
498
|
// fired and no tick has rewritten the file yet), and saying nothing beats saying something
|
|
504
499
|
// visibly wrong.
|
|
505
|
-
next: j.enabled &&
|
|
500
|
+
next: j.enabled && (j.stateful || (currentSession && j.sessionId === currentSession)) ? futureOnly(nexts[j.id]) : undefined,
|
|
501
|
+
// Set for a plain job of a session that is not this one: it is asleep, and this is what wakes it.
|
|
502
|
+
asleep: !j.stateful && !(currentSession && j.sessionId === currentSession) ? "session " + String(j.sessionId || "?").slice(0, 8) + " — resume it to run" : undefined,
|
|
506
503
|
}));
|
|
507
504
|
const allRules = readJson(path.join(LOOPS_DIR, "triggers.json"), { rules: [] }).rules ?? [];
|
|
508
505
|
const rules = allRules
|
|
@@ -547,6 +544,8 @@ const PAGE_COMMANDS = [
|
|
|
547
544
|
{ name: "clear", description: "start a new session — this one stays on disk, /resume brings it back" },
|
|
548
545
|
{ name: "new", description: "start a new session — this one stays on disk, /resume brings it back" },
|
|
549
546
|
{ name: "resume", description: "go back to an earlier session in this project" },
|
|
547
|
+
{ name: "sessions", description: "list this project's sessions: id, when, what was first said" },
|
|
548
|
+
{ name: "session", description: "export [path] | import <path> — the .pisession archive commands" },
|
|
550
549
|
{ name: "compact", description: "compact the context; anything after it steers the summary" },
|
|
551
550
|
];
|
|
552
551
|
|
|
@@ -1731,7 +1730,7 @@ function liveHostPid() {
|
|
|
1731
1730
|
* their automation is dead when it is not.
|
|
1732
1731
|
*/
|
|
1733
1732
|
function automationToKeepRunning() {
|
|
1734
|
-
const ours = (x) => x.enabled
|
|
1733
|
+
const ours = (x) => x.enabled;
|
|
1735
1734
|
const jobs = (readJson(path.join(LOOPS_DIR, "jobs.json"), { jobs: [] }).jobs ?? []).filter((j) => ours(j) && j.stateful);
|
|
1736
1735
|
const rules = (readJson(path.join(LOOPS_DIR, "triggers.json"), { rules: [] }).rules ?? []).filter(ours);
|
|
1737
1736
|
return jobs.length + rules.length;
|
|
@@ -3429,9 +3428,9 @@ function renderSidebar(s) {
|
|
|
3429
3428
|
'<button data-run="' + safeText(j.id) + '">run</button></div>' +
|
|
3430
3429
|
'<div class="m">' + safeText(str(j.prompt).slice(0, 90)) + "</div>" +
|
|
3431
3430
|
'<div class="m">' + (j.running ? "running · " : "") + "runs " + num(j.runCount) + (j.next ? " · next " + safeText(whenNext(j.next)) : "") + "</div>" +
|
|
3431
|
+
(j.asleep ? '<div class="m" style="color:#c93">' + safeText(j.asleep) + "</div>" : "") +
|
|
3432
3432
|
// A job belonging to a hostname this machine no longer has: it is listed, because it exists,
|
|
3433
3433
|
// and it says why nothing is happening rather than leaving you to find out from the silence.
|
|
3434
|
-
(j.otherHost ? '<div class="m" style="color:#c93">other host: ' + safeText(j.otherHost) + " — run <b>/cron set " + safeText(j.ref) + " --host here</b></div>" : "") +
|
|
3435
3434
|
(j.lastError ? '<div class="m" style="color:#c66">' + safeText(str(j.lastError).slice(0, 120)) + "</div>" : "") + "</div>";
|
|
3436
3435
|
}
|
|
3437
3436
|
for (const r of a.rules) {
|
|
@@ -3788,7 +3787,7 @@ $("input").addEventListener("paste", (e) => {
|
|
|
3788
3787
|
$("composer").onsubmit = async (e) => {
|
|
3789
3788
|
e.preventDefault();
|
|
3790
3789
|
const input = $("input");
|
|
3791
|
-
|
|
3790
|
+
let text = input.value.trim();
|
|
3792
3791
|
if (!text && !images.length) return;
|
|
3793
3792
|
input.value = ""; hidePop();
|
|
3794
3793
|
if (text) { sentPrompts.push(text); if (sentPrompts.length > 200) sentPrompts.shift(); }
|
|
@@ -3797,6 +3796,9 @@ $("composer").onsubmit = async (e) => {
|
|
|
3797
3796
|
// instead of being sent: anything attached stays in the tray, and a session about to be replaced
|
|
3798
3797
|
// is not given a message it would carry nowhere.
|
|
3799
3798
|
if (pageCommand(text)) return;
|
|
3799
|
+
// '/session export|import' are the archive commands here. pi's own '/session' is a terminal
|
|
3800
|
+
// command that does not exist over rpc, so the two-word spelling is free to mean the extension's.
|
|
3801
|
+
text = text.replace(/^\/session\s+(export|import)\b/i, (_m, sub) => "/session-" + sub.toLowerCase());
|
|
3800
3802
|
const payload = { text, images, mode: busy ? "follow_up" : undefined };
|
|
3801
3803
|
const shown = text + (images.length ? "\n[" + images.length + " image(s)]" : "");
|
|
3802
3804
|
if (!busy) {
|
|
@@ -3918,14 +3920,31 @@ function pageCommand(text) {
|
|
|
3918
3920
|
if (!m) return false;
|
|
3919
3921
|
const name = m[1].toLowerCase();
|
|
3920
3922
|
const rest = m[2].trim();
|
|
3921
|
-
if (name
|
|
3923
|
+
if (name === "session" && /^(export|import)\b/i.test(rest)) return false; // an extension command, sent as one
|
|
3924
|
+
if (name !== "clear" && name !== "new" && name !== "resume" && name !== "compact" && name !== "sessions" && name !== "session") return false;
|
|
3922
3925
|
row("user", "you", text.trim());
|
|
3923
3926
|
if (name === "compact") compactNow(rest);
|
|
3924
3927
|
else if (name === "resume") openSessions();
|
|
3928
|
+
else if (name === "sessions") listSessionsHere();
|
|
3929
|
+
else if (name === "session") row("system", "session", "usage: /session export [path] [--exclude-triggers] | /session import <path>\n/sessions lists this project's sessions; the header shows this one's model, cost and context.");
|
|
3925
3930
|
else swap("/session/new", {}, "new session — the one you left is under resume");
|
|
3926
3931
|
return true;
|
|
3927
3932
|
}
|
|
3928
3933
|
|
|
3934
|
+
/* /sessions: one line per session, newest first — short id, when it started, what was first said. No backticks in here: this is inside the page template. */
|
|
3935
|
+
async function listSessionsHere() {
|
|
3936
|
+
const r = await api("/sessions");
|
|
3937
|
+
const list = r.sessions || [];
|
|
3938
|
+
if (!list.length) return row("system", "sessions", "(no sessions for this project yet)");
|
|
3939
|
+
const lines = list.map((s) => {
|
|
3940
|
+
const when = String(s.startedAt || "").slice(0, 16);
|
|
3941
|
+
const said = plain(s.name || s.first || "").replace(/\s+/g, " ");
|
|
3942
|
+
const chars = [...said];
|
|
3943
|
+
return String(s.id).slice(0, 16) + " " + when + (s.current ? " [this one]" : "") + " " + (chars.length > 80 ? chars.slice(0, 80).join("") + "…" : said);
|
|
3944
|
+
});
|
|
3945
|
+
row("system", "sessions", lines.join("\n"));
|
|
3946
|
+
}
|
|
3947
|
+
|
|
3929
3948
|
$("compact").onclick = () => compactNow("");
|
|
3930
3949
|
$("clear").onclick = () => swap("/session/new", {}, "new session — the one you left is under resume");
|
|
3931
3950
|
|