cortad 0.3.1-rc.10 → 0.3.1-rc.12
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/lib/failed-text.mjs +35 -0
- package/lib/health.mjs +53 -0
- package/lib/pyhook/sitecustomize.py +4 -1
- package/lib/replay.mjs +2 -1
- package/lib/text.mjs +22 -17
- package/lib/trace.cjs +4 -2
- package/lib/verbs.mjs +1 -1
- package/local.mjs +20 -10
- package/package.json +3 -1
- package/skill/SKILL.md +1 -1
- package/skill/references/results.md +15 -0
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { at, clip, plural } from "./words.mjs";
|
|
2
|
+
|
|
3
|
+
// The replies that did not count and the doors that were not sent to, said before the score: one
|
|
4
|
+
// sentence whose parts add up to the count, then the request behind each part that has one. A count
|
|
5
|
+
// on its own ("17 of 33 replies failed") left resumeforge's agent asking what was attempted, whose it
|
|
6
|
+
// was, and how to reproduce it.
|
|
7
|
+
|
|
8
|
+
// A row the run held back names a door and why, or, when no door could take it, only the reason.
|
|
9
|
+
export const heldBackLine = (h) => h.why === "not testable here"
|
|
10
|
+
? `Held back: ${plural(h.trials, "trial")} cannot be played here: ${clip(h.surface, 200).replace(/\.$/, "")}.`
|
|
11
|
+
: `Held back: ${plural(h.trials, "trial")} on ${h.surface}${h.why ? `, ${clip(h.why, 200)}` : ""}.`;
|
|
12
|
+
|
|
13
|
+
const HELD_SHOWN = 6;
|
|
14
|
+
|
|
15
|
+
export function heldLines(held) {
|
|
16
|
+
const rows = (held ?? []).filter((h) => h.trials > 0);
|
|
17
|
+
const lines = rows.slice(0, HELD_SHOWN).map(heldBackLine);
|
|
18
|
+
if (rows.length > HELD_SHOWN) lines.push(`Held back: ${plural(rows.slice(HELD_SHOWN).reduce((n, h) => n + h.trials, 0), "more trial")} for ${plural(rows.length - HELD_SHOWN, "other reason")}.`);
|
|
19
|
+
return lines;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function failedLines(f) {
|
|
23
|
+
if (!f) return [];
|
|
24
|
+
const lines = f.said ? [f.said] : [];
|
|
25
|
+
for (const row of f.rows ?? []) {
|
|
26
|
+
const sent = row.curl ?? row.request;
|
|
27
|
+
if (!sent && !row.verify) continue;
|
|
28
|
+
const [path, line] = (row.at ?? "").split(/:(?=\d+$)/);
|
|
29
|
+
const where = row.at ? `at ${at(path, Number(line))}` : `from ${row.door}`;
|
|
30
|
+
if (sent) lines.push(` Request for the ${row.count} ${where}: ${clip(sent, 900)}`);
|
|
31
|
+
if (sent && row.response) lines.push(` Answered: ${clip(row.response.replace(/\s+/g, " "), 300)}`);
|
|
32
|
+
if (row.verify) lines.push(sent ? ` verify ${row.verify} replays them.` : ` verify ${row.verify} replays the ${row.count} ${where}.`);
|
|
33
|
+
}
|
|
34
|
+
return [...lines, ...heldLines((f.held ?? []).map((h) => ({ surface: h.door, trials: h.trials, why: h.why })))];
|
|
35
|
+
}
|
package/lib/health.mjs
ADDED
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
// Whether the app a run is using is alive, read the way a person would: is its process still there,
|
|
2
|
+
// does anything still hold its port, and only then does it answer. An app slow to answer because it
|
|
3
|
+
// is answering our own requests is working, not dead: resumeforge held sixteen of them on a database
|
|
4
|
+
// pool of five, missed two probes of 2.5 seconds, and was restarted in the middle of the run.
|
|
5
|
+
import { connect } from "node:net";
|
|
6
|
+
|
|
7
|
+
const FLOOR_MS = 10_000;
|
|
8
|
+
const CAP_MS = 120_000;
|
|
9
|
+
const url = (host, port) => `http://${host === "::1" ? "[::1]" : host}:${port}/`;
|
|
10
|
+
|
|
11
|
+
// Opens the socket and sends nothing. Only a refusal means nobody holds the port: a listener too
|
|
12
|
+
// busy to accept still holds it.
|
|
13
|
+
export const portHeld = (host, port, ms = 3000) => new Promise((done) => {
|
|
14
|
+
const socket = connect({ host, port });
|
|
15
|
+
const end = (held) => { socket.destroy(); done(held); };
|
|
16
|
+
socket.once("connect", () => end(true));
|
|
17
|
+
socket.once("error", (e) => end(e.code !== "ECONNREFUSED"));
|
|
18
|
+
socket.setTimeout(ms, () => end(true));
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
// `gone`: the process this command started has exited. `inFlight`: the run's requests the app has
|
|
22
|
+
// not answered yet. The probe waits three times the app's last reply to it, never under ten seconds
|
|
23
|
+
// or over two minutes.
|
|
24
|
+
export function makeHealth({ host, gone, inFlight }) {
|
|
25
|
+
let took = 0;
|
|
26
|
+
const timeout = () => Math.min(CAP_MS, Math.max(FLOOR_MS, took * 3));
|
|
27
|
+
return {
|
|
28
|
+
timeout,
|
|
29
|
+
// "up", "busy" (slow while the run's requests are open: never restarted), or why it is down:
|
|
30
|
+
// "exited", "released" (nothing holds its port) or "quiet" (no answer with nothing of ours open).
|
|
31
|
+
async check(port) {
|
|
32
|
+
if (gone()) return "exited";
|
|
33
|
+
if (!(await portHeld(host(), port))) return "released";
|
|
34
|
+
const started = Date.now();
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch(url(host(), port), { method: "GET", signal: AbortSignal.timeout(timeout()), redirect: "manual" });
|
|
37
|
+
await res.body?.cancel();
|
|
38
|
+
took = Date.now() - started;
|
|
39
|
+
return "up";
|
|
40
|
+
} catch {
|
|
41
|
+
return inFlight() > 0 ? "busy" : "quiet";
|
|
42
|
+
}
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// The runner's own line for a restart, in the person's words.
|
|
48
|
+
export function downLine(state, { port, downMs, crashed = false }) {
|
|
49
|
+
if (crashed) return "your app crashed, starting it again";
|
|
50
|
+
if (state === "exited") return "your app exited, starting it again";
|
|
51
|
+
if (state === "released") return `nothing is listening on port ${port} any more, starting your app again`;
|
|
52
|
+
return `your app has not answered for ${Math.round(downMs / 1000)} seconds while no test conversation was waiting on it, starting it again`;
|
|
53
|
+
}
|
|
@@ -964,7 +964,10 @@ def _install():
|
|
|
964
964
|
# ran by it, where the read could only name the door. Its frames on this stack outside installed
|
|
965
965
|
# packages, then, when the call runs in a task another task awaits (LangChain runs each step of a
|
|
966
966
|
# chain as its own task), the awaiting task's. Relative to the folder the app was started in.
|
|
967
|
+
# Five lines: a provider class's stream, post and retry can be three lines of one file, and the
|
|
968
|
+
# line that says which path ran is past them.
|
|
967
969
|
root = os.getcwd().replace(os.sep, "/").rstrip("/") + "/"
|
|
970
|
+
callers_max = 5
|
|
968
971
|
installed = re.compile(r"/(?:site-packages|dist-packages|\.venv|venv|__pypackages__)/|/lib/python\d")
|
|
969
972
|
|
|
970
973
|
def ours(frame, out):
|
|
@@ -973,7 +976,7 @@ def _install():
|
|
|
973
976
|
at = "%s:%d" % (f[len(root):], frame.f_lineno)
|
|
974
977
|
if at not in out:
|
|
975
978
|
out.append(at)
|
|
976
|
-
return len(out) >=
|
|
979
|
+
return len(out) >= callers_max
|
|
977
980
|
|
|
978
981
|
# The task that waits on this one: its wake-up is among this one's callbacks, directly, or behind
|
|
979
982
|
# the future an asyncio.gather callback closes over.
|
package/lib/replay.mjs
CHANGED
|
@@ -124,7 +124,8 @@ const calledOf = (v) => {
|
|
|
124
124
|
const passagesOf = (v) => (texts(v).length ? { passages: texts(v).slice(0, 6) } : {});
|
|
125
125
|
// Which of the app's own lines made the call, nearest first, as "path:line".
|
|
126
126
|
const AT = /^[^\n\r\t]{1,300}:\d{1,7}$/;
|
|
127
|
-
const
|
|
127
|
+
const CALLERS_MAX = 5;
|
|
128
|
+
const callerOf = (v) => { const caller = Array.isArray(v) ? v.filter((c) => typeof c === "string" && AT.test(c)).slice(0, CALLERS_MAX) : []; return caller.length ? { caller } : {}; };
|
|
128
129
|
const meter = (() => {
|
|
129
130
|
const rows = [];
|
|
130
131
|
const deps = [];
|
package/lib/text.mjs
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { failedLines, heldLines } from "./failed-text.mjs";
|
|
1
2
|
import { readLines } from "./read-text.mjs";
|
|
2
3
|
import { at, clip, has, num, PAGE_CHARS, pageOf, plural, SIDE, upper } from "./words.mjs";
|
|
3
4
|
|
|
@@ -59,15 +60,13 @@ function measuredLines(d) {
|
|
|
59
60
|
|
|
60
61
|
function scoreText(d, of) {
|
|
61
62
|
const floor = has(d.decided) && d.decided < 22;
|
|
62
|
-
// The replies that
|
|
63
|
-
// 89 error replies unsaid, read as a clean bill.
|
|
64
|
-
const
|
|
63
|
+
// The replies that did not count stand beside the number, in the same counts the lines above it
|
|
64
|
+
// give: a 100 over the replies that answered, with 62 of 89 error replies unsaid, read as a clean bill.
|
|
65
|
+
const f = d.failedReplies;
|
|
66
|
+
const crashed = f ? (f.notCounted > 0 ? `; the ${num(f.notCounted)} of ${plural(f.replies, "reply", "replies")} that did not count are not in it` : "")
|
|
67
|
+
: d.replies && d.replies.crashed > 0 ? `; ${num(d.replies.crashed)} of ${plural(d.replies.total, "reply", "replies")} were failures and are not in it` : "";
|
|
65
68
|
return `${floor ? "provisional score" : "score"} ${num(d.score)} of 100${of ? ` on those ${num(d.questionsAsked)}` : ""}${interval(d.ci)}${floor ? `, from ${plural(d.decided, "decided reading")}: under the 22-reading floor, so not a verdict yet` : ""}${crashed}`;
|
|
66
69
|
}
|
|
67
|
-
// A row the run held back names a door and why, or, when no door could take it, only the reason.
|
|
68
|
-
const heldBackLine = (h) => h.why === "not testable here"
|
|
69
|
-
? `Held back: ${plural(h.trials, "trial")} cannot be played here: ${clip(h.surface, 200).replace(/\.$/, "")}.`
|
|
70
|
-
: `Held back: ${plural(h.trials, "trial")} on ${h.surface}${h.why ? `, ${clip(h.why, 200)}` : ""}.`;
|
|
71
70
|
const haltLine = (h) => `${h.side === "ours" ? `Stopped ${SIDE.ours}` : "Your app stopped answering"} after ${plural(h.after, "turn")}${h.why ? `: ${clip(h.why, 240)}` : ""}. The score covers the turns before that.`;
|
|
72
71
|
// The pace a slow app was played at, and the replies our own wait cut off: both said as ours. A
|
|
73
72
|
// verify says the second in its own paired line.
|
|
@@ -86,12 +85,14 @@ function runLines(d, prefix = "") {
|
|
|
86
85
|
const played = d.of ? `${num(d.played ?? 0)} of ${plural(d.of, "trial")} played` : "no trial played yet";
|
|
87
86
|
const lines = [`${upper(`${prefix}${d.kind === "verify" ? "verify" : "run"}`)} ${d.jobId}: ${d.status === "succeeded" || !d.status ? (done ? "finished" : "running") : d.status}, ${played}.`];
|
|
88
87
|
if (d.tested) lines.push(testedLine(d.tested));
|
|
88
|
+
// Each endpoint's first conversation, played alone before the others: whether it held and why not.
|
|
89
|
+
lines.push(...(d.first ?? []));
|
|
89
90
|
// Between two counts, what the run is doing now: four minutes of "0 of 15" read as nothing happening.
|
|
90
91
|
if (!done && d.now?.doing) lines.push(d.now.doing);
|
|
91
92
|
lines.push(...waitLines(d));
|
|
92
93
|
// A verify is a move, not a score: "100 of 100" on a replay whose run before decided nothing read
|
|
93
94
|
// as a clean bill to bloom's agent, beside "zero verified fixes".
|
|
94
|
-
if (done && d.kind !== "verify") lines.push(...measuredLines(d));
|
|
95
|
+
if (done && d.kind !== "verify") lines.push(...failedLines(d.failedReplies), ...measuredLines(d));
|
|
95
96
|
if (done && d.blocked) lines.push(d.blocked);
|
|
96
97
|
if (done && d.notCounted) lines.push(d.notCounted);
|
|
97
98
|
if (done && d.halted) lines.push(haltLine(d.halted));
|
|
@@ -99,10 +100,9 @@ function runLines(d, prefix = "") {
|
|
|
99
100
|
if (done && has(d.findings)) lines.push(`${plural(d.findings, "finding")}.`);
|
|
100
101
|
const trials = trialsLine(d);
|
|
101
102
|
if (trials) lines.push(trials);
|
|
102
|
-
// Every row, or six and the rest in one line, so the rows always sum to the held-back count.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
if (heldRows.length > 6) lines.push(`Held back: ${plural(heldRows.slice(6).reduce((n, h) => n + h.trials, 0), "more trial")} for ${plural(heldRows.length - 6, "other reason")}.`);
|
|
103
|
+
// Every row, or six and the rest in one line, so the rows always sum to the held-back count. A
|
|
104
|
+
// finished run says them with the replies that did not count, above the score.
|
|
105
|
+
if (!(done && d.failedReplies)) lines.push(...heldLines(d.heldBack));
|
|
106
106
|
const reads = readingsLine(d);
|
|
107
107
|
if (reads) lines.push(reads);
|
|
108
108
|
if (d.stopped) lines.push(stoppedLine(d));
|
|
@@ -140,7 +140,8 @@ function readingsLine(d) {
|
|
|
140
140
|
// before the first turn has no turn to name, and a reason written as a sentence keeps one full stop.
|
|
141
141
|
function stoppedLine(d) {
|
|
142
142
|
const s = d.stopped;
|
|
143
|
-
|
|
143
|
+
// A stop before the trials, on the endpoints' first conversations, names each one instead of a turn.
|
|
144
|
+
const turn = s.after > 0 && !s.first ? ` at turn ${s.after}` : "";
|
|
144
145
|
const why = String(s.why).replace(/\.$/, "");
|
|
145
146
|
const fix = s.fix ? ` ${s.fix}` : "";
|
|
146
147
|
if (d.of && d.played < d.of) return `Stopped at ${num(d.played)} of ${plural(d.of, "trial")}${SIDE[s.side] ? `, ${SIDE[s.side]}` : ""}: ${why}${turn}.${fix}`;
|
|
@@ -222,8 +223,10 @@ export function testedLine(t) {
|
|
|
222
223
|
return `App started ${started}${t.reloads ? " and reloads itself on save" : ""}; your last change was at ${clock(t.change.at)}.`;
|
|
223
224
|
}
|
|
224
225
|
|
|
225
|
-
|
|
226
|
-
|
|
226
|
+
// `said`: the status's own word on the app, which knows when it already answers and only its AI
|
|
227
|
+
// endpoint is still being found; bloom's agent read "still starting" beside "your app is answering".
|
|
228
|
+
export const waitingText = (p, now, limitMs, said) =>
|
|
229
|
+
`${said ? String(said).replace(/\.$/, "") : `Your app is still starting for the ${p.kind === "verify" ? "verify" : "run"}`}: ${num((now - Date.parse(p.startedAt)) / 1000)} of up to ${num(limitMs / 1000)} seconds.\nnext: run_status pending`;
|
|
227
230
|
|
|
228
231
|
// A spent plan: what the last run found, what was fixed since, what the next run would play.
|
|
229
232
|
export function refusedText(d) {
|
|
@@ -277,7 +280,7 @@ function findingsHead(d) {
|
|
|
277
280
|
if (!d.runId) return ["No run has finished on this repository yet."];
|
|
278
281
|
const lines = [`Run ${d.runId}: ${plural(d.findings?.length ?? 0, "finding")}.`];
|
|
279
282
|
if (d.tested) lines.push(testedLine(d.tested));
|
|
280
|
-
lines.push(...measuredLines(d));
|
|
283
|
+
lines.push(...failedLines(d.failedReplies), ...measuredLines(d));
|
|
281
284
|
if (d.blocked) lines.push(d.blocked);
|
|
282
285
|
if (d.halted) lines.push(haltLine(d.halted));
|
|
283
286
|
lines.push(...waitLines(d));
|
|
@@ -338,7 +341,9 @@ function findingBlock(f, placed) {
|
|
|
338
341
|
return lines.join("\n");
|
|
339
342
|
}
|
|
340
343
|
const lines = [`${f.id} ${f.asks}`];
|
|
341
|
-
|
|
344
|
+
// What the line is when it is not the check's own: the tool that ran, or only the model call.
|
|
345
|
+
if (f.addressed) lines.push(` At ${at(f.file, f.line)}: ${f.addressed}`);
|
|
346
|
+
else if (!placed && f.file) lines.push(` At ${at(f.file, f.line)}`);
|
|
342
347
|
if (f.criteria) lines.push(` Criteria: ${clip(f.criteria, 600)}`);
|
|
343
348
|
if (f.door) lines.push(` Endpoint: ${f.door}`);
|
|
344
349
|
else if (f.doors?.length) lines.push(` Endpoints: ${f.doors.join(", ")}`);
|
package/lib/trace.cjs
CHANGED
|
@@ -137,13 +137,15 @@ if (FILE) {
|
|
|
137
137
|
// The file that registered a route: the first frame outside this hook and outside node_modules.
|
|
138
138
|
const callerFile = () => { const f = stackFrames()[0]; return f && !f.file.startsWith("..") ? f.file : ""; };
|
|
139
139
|
// Which of the app's own lines made a model call, nearest first: the run names the code path that
|
|
140
|
-
// ran by it, where the read could only name the door.
|
|
140
|
+
// ran by it, where the read could only name the door. Five lines: a provider class's stream, post
|
|
141
|
+
// and retry can be three lines of one file, and the line that says which path ran is past them.
|
|
142
|
+
const CALLERS_MAX = 5;
|
|
141
143
|
const callers = () => {
|
|
142
144
|
const out = [];
|
|
143
145
|
for (const f of stackFrames()) {
|
|
144
146
|
const at = `${f.file}:${f.line}`;
|
|
145
147
|
if (!f.file.startsWith("..") && !out.includes(at)) out.push(at);
|
|
146
|
-
if (out.length ===
|
|
148
|
+
if (out.length === CALLERS_MAX) break;
|
|
147
149
|
}
|
|
148
150
|
return out.length ? out : undefined;
|
|
149
151
|
};
|
package/lib/verbs.mjs
CHANGED
|
@@ -109,7 +109,7 @@ export function makeVerbs({ api, token, fetchImpl = fetch, startApp, pending, ro
|
|
|
109
109
|
if (p.jobId) return { id: p.jobId };
|
|
110
110
|
if (p.error) return { text: `${p.error}\nnext: status`, isError: true };
|
|
111
111
|
if (stale()) return { text: "The process starting your app for the run ended without a result.\nNothing ran.\nnext: status", isError: true };
|
|
112
|
-
return { text: waitingText(p, now(), READY_MS) };
|
|
112
|
+
return { text: waitingText(p, now(), READY_MS, latest.data.app?.state === "starting" && latest.data.app.port ? latest.data.app.said : null) };
|
|
113
113
|
};
|
|
114
114
|
|
|
115
115
|
const runStatus = async ({ jobId } = {}) => {
|
package/local.mjs
CHANGED
|
@@ -34,6 +34,7 @@ import { listingUrl } from "./lib/listing.mjs";
|
|
|
34
34
|
import { installPlan, missingDependency, startPlan, workspaces } from "./lib/start.mjs";
|
|
35
35
|
import { openSwitches } from "./lib/switches.mjs";
|
|
36
36
|
import { keepData } from "./lib/data.mjs";
|
|
37
|
+
import { downLine, makeHealth } from "./lib/health.mjs";
|
|
37
38
|
import { markRun, writesDirOf } from "./lib/writes.mjs";
|
|
38
39
|
|
|
39
40
|
const argv = process.argv.slice(2);
|
|
@@ -275,6 +276,9 @@ let appGone = false;
|
|
|
275
276
|
let lastSaid = "";
|
|
276
277
|
let lastOutputAt = Date.now();
|
|
277
278
|
let forgetTold = null;
|
|
279
|
+
// The run's requests your app has not answered yet: a slow health probe while any are open is our
|
|
280
|
+
// load on it, not a hang.
|
|
281
|
+
let answering = 0;
|
|
278
282
|
const mask = (text) => { let s = String(text ?? ""); for (const v of secrets) s = s.split(v).join("[masked]"); return s; };
|
|
279
283
|
|
|
280
284
|
// ---- the wire
|
|
@@ -405,12 +409,13 @@ async function verb(job) {
|
|
|
405
409
|
const said = Object.fromEntries([...res.headers].filter(([k]) => !/^set-cookie2?$/i.test(k)).map(([k, v]) => [k, mask(v)]));
|
|
406
410
|
return { status: res.status, headers: said, body: mask(buf.subarray(0, LIMIT).toString("utf8")), truncated: buf.length > LIMIT };
|
|
407
411
|
};
|
|
408
|
-
try { return await ask(); }
|
|
412
|
+
const counted = async () => { answering += 1; try { return await ask(); } finally { answering -= 1; } };
|
|
413
|
+
try { return await counted(); }
|
|
409
414
|
catch (e) {
|
|
410
415
|
const code = String(e.cause?.code ?? e.code ?? "");
|
|
411
416
|
if (!/ECONNREFUSED|ECONNRESET|EPIPE|UND_ERR_SOCKET/.test(code)) return { error: `nothing answered at port ${port}: ${code || e.message}` };
|
|
412
417
|
for (let i = 0; i < 45 && !(await answers(port)); i++) await new Promise((r) => setTimeout(r, 1000));
|
|
413
|
-
try { return { ...(await
|
|
418
|
+
try { return { ...(await counted()), heldForRestart: true }; }
|
|
414
419
|
catch (again) { return { error: `nothing answered at port ${port}: ${String(again.cause?.code ?? again.message)}` }; }
|
|
415
420
|
}
|
|
416
421
|
}
|
|
@@ -854,6 +859,7 @@ async function restartApp() {
|
|
|
854
859
|
async function restartNow() {
|
|
855
860
|
if (!launched || !child) return { error: "You started this app yourself, so restart it in your own terminal. Most dev servers reload on save." };
|
|
856
861
|
const port = app.port;
|
|
862
|
+
say("starting your app again so it runs the code you saved, since it does not reload by itself");
|
|
857
863
|
await stopApp(child.pid);
|
|
858
864
|
// Until the port is free, not merely silent: a listener still closing does not answer and still
|
|
859
865
|
// holds the bind, and the restarted app was moved to the next port while the run kept knocking on
|
|
@@ -1073,26 +1079,30 @@ async function appLife() {
|
|
|
1073
1079
|
let toldDown = false;
|
|
1074
1080
|
let appTold = true;
|
|
1075
1081
|
forgetTold = () => { appTold = false; };
|
|
1082
|
+
const health = makeHealth({ host: () => appHost, gone: () => Boolean(launched && appGone), inFlight: () => answering });
|
|
1076
1083
|
for (let up = true; !closing;) {
|
|
1077
1084
|
await new Promise((r) => setTimeout(r, 2000));
|
|
1078
1085
|
if (closing || restarting) continue;
|
|
1079
|
-
const
|
|
1080
|
-
if (
|
|
1086
|
+
const state = await health.check(app.port);
|
|
1087
|
+
if (state === "up") {
|
|
1081
1088
|
downSince = 0; toldDown = false;
|
|
1082
1089
|
// Said until it is heard. Said once, it was lost when their app came back while the network
|
|
1083
1090
|
// was down, and the screen went on showing an app that had stopped while it answered turns.
|
|
1084
1091
|
if (!up || !appTold) { up = true; appTold = Boolean((await announce().catch(() => null))?.ok); }
|
|
1085
1092
|
continue;
|
|
1086
1093
|
}
|
|
1094
|
+
// Running, holding its port, and slow because it is answering the run's own requests: working,
|
|
1095
|
+
// not dead. Nothing is said and nothing restarts, and a silence after it is timed from here.
|
|
1096
|
+
if (state === "busy") { if (!up) downSince = Date.now(); continue; }
|
|
1087
1097
|
if (up) { up = false; downSince = Date.now(); }
|
|
1088
1098
|
appTold = false;
|
|
1089
1099
|
const downFor = Date.now() - downSince;
|
|
1090
|
-
// Not answering. A dev server restarting on a save
|
|
1091
|
-
//
|
|
1092
|
-
// A watcher that has said its app crashed is not
|
|
1100
|
+
// Not answering. A dev server restarting on a save says so in its output: it is left alone,
|
|
1101
|
+
// however long its own startup takes.
|
|
1102
|
+
// A watcher that has said its app crashed is not reloading, it is waiting, and says so.
|
|
1093
1103
|
const crashed = /app crashed - waiting|Failed running|waiting for (?:file )?changes before restart/i.test(lastSaid.slice(-600));
|
|
1094
|
-
const
|
|
1095
|
-
if (
|
|
1104
|
+
const reloading = launched && !appGone && !crashed && Date.now() - lastOutputAt < 15_000;
|
|
1105
|
+
if (reloading || downFor < (crashed ? 4_000 : 10_000)) continue;
|
|
1096
1106
|
// Down for real. The screen is told, so it never shows an app that is not there.
|
|
1097
1107
|
// `exited`: the process this command started is gone or its watcher said it crashed, not a process
|
|
1098
1108
|
// running and not answering. The run tells the two apart: one is their crash, the other may be our
|
|
@@ -1108,7 +1118,7 @@ async function appLife() {
|
|
|
1108
1118
|
restarting = true;
|
|
1109
1119
|
try {
|
|
1110
1120
|
if (child?.pid) await stopApp(child.pid);
|
|
1111
|
-
say(
|
|
1121
|
+
say(downLine(state, { port: app.port, downMs: downFor, crashed }));
|
|
1112
1122
|
let back = launched ? await launch(180_000) : await startApp();
|
|
1113
1123
|
while (!back.port && !closing) {
|
|
1114
1124
|
await call("POST", `/local/${box}/stopped`, { said: mask(back.tail || back.said || lastSaid).slice(-2000) }).catch(() => {});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "cortad",
|
|
3
|
-
"version": "0.3.1-rc.
|
|
3
|
+
"version": "0.3.1-rc.12",
|
|
4
4
|
"description": "Connects the AI app on your machine to Cortad for test conversations, and gives your coding agent the MCP and skill to run them. No dependencies.",
|
|
5
5
|
"bin": {
|
|
6
6
|
"cortad": "local.mjs"
|
|
@@ -10,7 +10,9 @@
|
|
|
10
10
|
"README.md",
|
|
11
11
|
"lib/cli.mjs",
|
|
12
12
|
"lib/data.mjs",
|
|
13
|
+
"lib/failed-text.mjs",
|
|
13
14
|
"lib/fresh.mjs",
|
|
15
|
+
"lib/health.mjs",
|
|
14
16
|
"lib/home.mjs",
|
|
15
17
|
"lib/identities.mjs",
|
|
16
18
|
"lib/keys.mjs",
|
package/skill/SKILL.md
CHANGED
|
@@ -49,7 +49,7 @@ Take the person through them worst first, each with its file and line. A long li
|
|
|
49
49
|
|
|
50
50
|
## Fixing one finding
|
|
51
51
|
|
|
52
|
-
Each finding ends with what a verify of it can show. When the trials it replays read the question too few times for a rate, the verify plays up to 12 more trials in that
|
|
52
|
+
Each finding ends with what a verify of it can show. When the trials it replays read the question too few times for a rate, the verify plays up to 12 more trials itself, in the finding's situations that were read least, counted as verify trials, and the finding says how many before one is spent. A finding listed in several situations is verified in all of them.
|
|
53
53
|
|
|
54
54
|
1. One change, in the file and near the line the finding names.
|
|
55
55
|
2. `verify <findingId>`. It answers within a second, and `run_status` follows it. When the app's code changed since the app started and nothing reloaded it, Cortad starts the app again first and the answer says so, so the saved edit is what plays; `run_status` names when the app started against the last change. When it also plays new trials in the finding's situation, the answer says how many.
|
|
@@ -130,6 +130,21 @@ For the person: the report is at https://cortad.com/lab
|
|
|
130
130
|
next: findings
|
|
131
131
|
```
|
|
132
132
|
|
|
133
|
+
Replies that did not count come before the score, each part saying whose it is, with the request to paste where there is one:
|
|
134
|
+
|
|
135
|
+
```
|
|
136
|
+
Run 8f2a1c4e-5b6d-4e7f-9a0b-1c2d3e4f5a6b: finished, 31 of 31 trials played.
|
|
137
|
+
17 of 33 replies did not count: 9 answered HTTP 502 at backend/app/api/resume_writing.py:91 (yours; request below), 6 refused the shape we sent to POST /api/drill/{id}/answer (HTTP 422) (ours; request below), 2 got no answer from /api/chat before we stopped waiting at 90 seconds (ours).
|
|
138
|
+
Request for the 9 at backend/app/api/resume_writing.py:91: curl -X POST 'http://localhost:8004/api/resume/writing' -H 'authorization: <redacted>' -H 'content-type: application/json' --data-raw '{"message":"rewrite my summary"}'
|
|
139
|
+
Answered: Bad Gateway
|
|
140
|
+
verify finding:1 replays them.
|
|
141
|
+
Request for the 6 from /api/drill/{id}/answer: curl -X POST 'http://localhost:8004/api/drill/7/answer' -H 'content-type: application/json' --data-raw '{"message":"hi"}'
|
|
142
|
+
Answered: {"detail":[{"loc":["body","answer"],"msg":"field required"}]}
|
|
143
|
+
20 of 40 checks measured; score 88 of 100 on those 20, interval 70 to 96; the 17 of 33 replies that did not count are not in it.
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
A part marked yours is the app failing: run the request, fix what it shows, then verify the finding named. A part marked ours is Cortad's to fix, and nothing in the app changes for it. A masked value (`<redacted>`) is the app's own sign-in or key; put it back before running the request.
|
|
147
|
+
|
|
133
148
|
Your app stopped during the run and was started again: that is a finding of its own, `crash:1`, with the error your app printed and the turn it happened at. Fix the error it quotes. It has no trials to replay, so `verify` refuses it; the next run that finishes without it is the proof. A `Data:` line says whether the run wrote to a copy of your app's database or into the real one.
|
|
134
149
|
|
|
135
150
|
A verify that holds: both intervals are above zero and the held-out trials moved with the visible ones.
|