@alphacoder-v0/pi-loops 0.22.3 → 0.22.5
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 +91 -0
- package/README.md +2 -2
- package/README.zh-CN.md +2 -1
- package/docs/cli.md +8 -1
- package/docs/configuration.md +1 -0
- package/docs/design.md +7 -5
- package/docs/loops.md +8 -2
- package/docs/recipes.md +2 -1
- package/docs/web-ui-parity.md +17 -6
- package/package.json +1 -1
- package/src/cli.ts +10 -4
- package/src/goal.ts +5 -1
- package/src/job-lines.ts +68 -0
- package/src/pi-loops.ts +54 -55
- package/src/recipe.ts +26 -6
- package/src/scheduler.ts +97 -27
- package/src/session-head.ts +6 -2
- package/src/store.ts +55 -8
- package/src/tools.ts +3 -2
- package/src/triggers.ts +24 -0
- package/src/web.mjs +61 -22
package/src/web.mjs
CHANGED
|
@@ -210,6 +210,8 @@ const STDERR_KEEP = 8000;
|
|
|
210
210
|
/** What actually leaves this process: the tail, which is the part that says why pi stopped. */
|
|
211
211
|
const STDERR_SEND = 4000;
|
|
212
212
|
let stderrTail = "";
|
|
213
|
+
/** Characters the keep cut has already thrown away, so the send cut can count everything dropped. */
|
|
214
|
+
let stderrDropped = 0;
|
|
213
215
|
/**
|
|
214
216
|
* The shapes a credential takes in a line of output.
|
|
215
217
|
*
|
|
@@ -258,14 +260,25 @@ function maskSecrets(text) {
|
|
|
258
260
|
}
|
|
259
261
|
|
|
260
262
|
function recentStderr() {
|
|
261
|
-
// Redact the whole kept tail, then cut.
|
|
262
|
-
//
|
|
263
|
-
//
|
|
264
|
-
|
|
263
|
+
// Redact the whole kept tail, then cut by characters. `slice` counted UTF-16 code units, so the
|
|
264
|
+
// served tail could begin on the low half of an astral character — a replacement glyph where the
|
|
265
|
+
// error should start — and the page's own cap at 4000 could never fire: it was always handed 4000
|
|
266
|
+
// code units or fewer, and code points are never more numerous than code units, so nothing ever
|
|
267
|
+
// told the person anything was dropped. The count has to be put here rather than in the page,
|
|
268
|
+
// because `capped` keeps the first n characters and a tail keeps the last.
|
|
269
|
+
const kept = Array.from(maskSecrets(stderrTail.trim()));
|
|
270
|
+
if (!stderrDropped && kept.length <= STDERR_SEND) return kept.join("");
|
|
271
|
+
const room = STDERR_SEND - 40; // the marker line below is shorter than this
|
|
272
|
+
const shown = kept.slice(-room);
|
|
273
|
+
return `… (${stderrDropped + kept.length - shown.length} chars dropped)\n${shown.join("")}`;
|
|
265
274
|
}
|
|
266
275
|
pi.stderr.on("data", (d) => {
|
|
267
276
|
process.stderr.write(d);
|
|
268
|
-
|
|
277
|
+
// Characters, not code units: a cut on the code-unit half of an astral character leaves a lone
|
|
278
|
+
// surrogate in the tail, which `textContent` draws as a replacement glyph.
|
|
279
|
+
const all = Array.from(stderrTail + d.toString("utf8"));
|
|
280
|
+
stderrDropped += Math.max(0, all.length - STDERR_KEEP);
|
|
281
|
+
stderrTail = all.slice(-STDERR_KEEP).join("");
|
|
269
282
|
});
|
|
270
283
|
|
|
271
284
|
let nextId = 1;
|
|
@@ -464,9 +477,9 @@ function formatSchedule(s) {
|
|
|
464
477
|
* Missing file, older pi-loops, no leader yet: no next run shown, which is what was shown before.
|
|
465
478
|
*/
|
|
466
479
|
function nextRuns() {
|
|
467
|
-
//
|
|
468
|
-
//
|
|
469
|
-
//
|
|
480
|
+
// The scheduler's one file. If two machines shared a `$HOME` they would both run every job and
|
|
481
|
+
// overwrite one another's answers here; that is unsupported rather than handled
|
|
482
|
+
// (docs/design.md §19).
|
|
470
483
|
const doc = readJson(path.join(LOOPS_DIR, "next-runs.json"), { next: {} });
|
|
471
484
|
return doc?.next && typeof doc.next === "object" ? doc.next : {};
|
|
472
485
|
}
|
|
@@ -502,10 +515,13 @@ function automation(cwd) {
|
|
|
502
515
|
runCount: j.runCount ?? 0,
|
|
503
516
|
lastError: maskSecrets(j.lastError),
|
|
504
517
|
running: !!j.running,
|
|
505
|
-
// The open body of a card shows these
|
|
506
|
-
//
|
|
518
|
+
// The open body of a card shows these, which the terminal's `/cron` line has and the panel
|
|
519
|
+
// did not; they leave the store as they were written, no reshaping.
|
|
507
520
|
cwd: j.cwd,
|
|
508
521
|
lastFiredAt: j.lastFiredAt,
|
|
522
|
+
// Due ticks a run was too slow to take. The `lastError` a skip writes is cleared by the next
|
|
523
|
+
// run; this count is not, so it is the last evidence a loop is losing cadence.
|
|
524
|
+
skippedOverlap: j.skippedOverlap ?? 0,
|
|
509
525
|
// Set only when it is not this machine's: the panel says so, and nothing else has to guess.
|
|
510
526
|
// What to type to fix it. The terminal's version of this line says `/cron set <n>`, where n
|
|
511
527
|
// is the position in a numbered list — which this panel does not have, so telling someone to
|
|
@@ -822,11 +838,15 @@ function listSessionsHere(limit = 40) {
|
|
|
822
838
|
/** The header, a name if one was set early, and the first thing a person said. */
|
|
823
839
|
function readHead(file) {
|
|
824
840
|
let text;
|
|
841
|
+
let readBytes = 0;
|
|
825
842
|
try {
|
|
826
843
|
const fd = fs.openSync(file, "r");
|
|
827
844
|
try {
|
|
828
845
|
const buf = Buffer.alloc(SESSION_HEAD);
|
|
829
|
-
|
|
846
|
+
// `SESSION_HEAD` is a byte window, so the decision to cut is the byte count read, not the
|
|
847
|
+
// decoded string's length: a multi-byte character makes those differ.
|
|
848
|
+
readBytes = fs.readSync(fd, buf, 0, SESSION_HEAD, 0);
|
|
849
|
+
text = buf.toString("utf8", 0, readBytes);
|
|
830
850
|
} finally {
|
|
831
851
|
fs.closeSync(fd);
|
|
832
852
|
}
|
|
@@ -841,7 +861,7 @@ function readHead(file) {
|
|
|
841
861
|
return undefined;
|
|
842
862
|
}
|
|
843
863
|
if (header?.type !== "session" || typeof header.id !== "string") return undefined;
|
|
844
|
-
const cut =
|
|
864
|
+
const cut = readBytes >= SESSION_HEAD;
|
|
845
865
|
let name;
|
|
846
866
|
let first;
|
|
847
867
|
let messages = 0;
|
|
@@ -2024,15 +2044,29 @@ function previewRoot(file) {
|
|
|
2024
2044
|
return undefined;
|
|
2025
2045
|
}
|
|
2026
2046
|
|
|
2027
|
-
/**
|
|
2047
|
+
/**
|
|
2048
|
+
* The path the filesystem actually means: every symlink above it resolved, whether or not the path
|
|
2049
|
+
* itself exists. The deepest ancestor that does exist is resolved and the rest re-attached.
|
|
2050
|
+
*
|
|
2051
|
+
* One level up was not enough. A job whose cwd is a removed worktree or a checkout that moved is two
|
|
2052
|
+
* or more levels below the deepest thing that exists, and a parent-only fallback returned the
|
|
2053
|
+
* unresolved path — so `sameProject` said "another project" about a directory plainly inside this
|
|
2054
|
+
* one, and the panel dropped a job `/cron` still lists with `[orphan: cwd missing]`. This is the
|
|
2055
|
+
* walk `src/paths.ts` does as `realpathish`, copied because the front end is one file with no
|
|
2056
|
+
* imports on purpose.
|
|
2057
|
+
*/
|
|
2028
2058
|
function realOf(file) {
|
|
2029
|
-
|
|
2030
|
-
|
|
2031
|
-
|
|
2059
|
+
if (!file) return "";
|
|
2060
|
+
let at = path.resolve(file);
|
|
2061
|
+
const tail = [];
|
|
2062
|
+
for (;;) {
|
|
2032
2063
|
try {
|
|
2033
|
-
return path.join(fs.realpathSync(
|
|
2064
|
+
return path.join(fs.realpathSync(at), ...tail);
|
|
2034
2065
|
} catch {
|
|
2035
|
-
|
|
2066
|
+
const parent = path.dirname(at);
|
|
2067
|
+
if (parent === at) return path.resolve(file); // reached the root and nothing resolved
|
|
2068
|
+
tail.unshift(path.basename(at));
|
|
2069
|
+
at = parent;
|
|
2036
2070
|
}
|
|
2037
2071
|
}
|
|
2038
2072
|
}
|
|
@@ -3038,10 +3072,15 @@ function copyBtn(getText) {
|
|
|
3038
3072
|
* they reported the drop three different ways, one of them not at all: a result that stopped at 8000
|
|
3039
3073
|
* characters looked like a result that ended there. A cap that hides its own existence is a lie
|
|
3040
3074
|
* about the output.
|
|
3075
|
+
*
|
|
3076
|
+
* Counted and cut in characters, which is what the message says and what capChars in src/protocol.ts
|
|
3077
|
+
* and previewText in src/session-head.ts have always counted. String.length is code units, so an
|
|
3078
|
+
* emoji is two of them: the count came out larger than what was shown, and a cut between the two
|
|
3079
|
+
* halves of one left half a character, which a browser draws as a replacement glyph.
|
|
3041
3080
|
*/
|
|
3042
3081
|
function capped(text, n) {
|
|
3043
|
-
const
|
|
3044
|
-
return
|
|
3082
|
+
const chars = Array.from(String(text ?? ""));
|
|
3083
|
+
return chars.length > n ? chars.slice(0, n).join("") + "\n… (" + chars.length + " chars)" : chars.join("");
|
|
3045
3084
|
}
|
|
3046
3085
|
|
|
3047
3086
|
/** Replace a row's plain text with its rendered Markdown, keeping the source for the copy button. */
|
|
@@ -3505,7 +3544,7 @@ function renderSidebar(s) {
|
|
|
3505
3544
|
'<div class="body">' +
|
|
3506
3545
|
'<pre class="full">' + prompt + "</pre>" +
|
|
3507
3546
|
'<button class="copy" data-copy="' + safeText(j.id) + '">copy</button>' +
|
|
3508
|
-
'<div class="m">id ' + safeText(j.id) + " · cwd " + safeText(j.cwd) + (j.lastFiredAt ? " · last fired " + safeText(j.lastFiredAt) : "") + "</div>" +
|
|
3547
|
+
'<div class="m">id ' + safeText(j.id) + " · cwd " + safeText(j.cwd) + (j.lastFiredAt ? " · last fired " + safeText(j.lastFiredAt) : "") + (j.skippedOverlap ? " · skipped overlaps " + num(j.skippedOverlap) : "") + "</div>" +
|
|
3509
3548
|
// A job belonging to a hostname this machine no longer has: it is listed, because it exists,
|
|
3510
3549
|
// and it says why nothing is happening rather than leaving you to find out from the silence.
|
|
3511
3550
|
(j.lastError ? '<div class="m" style="color:#c66">' + safeText(j.lastError) + "</div>" : "") +
|
|
@@ -3745,7 +3784,7 @@ function renderRuntime(rt) {
|
|
|
3745
3784
|
// An MCP server's name, its state and its last error are all that server's own words.
|
|
3746
3785
|
html += "<div>" + dot(m.state) + safeText(m.name) + " <span class=\"m\">" + safeText(m.state) + " · " + countOf((m.tools || []).length, "tools", m.tools) +
|
|
3747
3786
|
(m.injects ? " · injects" : "") + (m.queued ? " · " + num(m.queued) + " queued" : "") + "</span></div>" +
|
|
3748
|
-
(m.lastError ? '<div class="m" style="color:#c66"> ' + safeText(str(m.lastError)
|
|
3787
|
+
(m.lastError ? '<div class="m" style="color:#c66"> ' + safeText(str(m.lastError)) + "</div>" : "");
|
|
3749
3788
|
}
|
|
3750
3789
|
if (rt.mcpConfigError) html += '<div class="m" style="color:#c66">mcp.toml: ' + safeText(rt.mcpConfigError) + "</div>";
|
|
3751
3790
|
const h = rt.hooks || {};
|