@sigrun/sigrun 0.10.2 → 0.11.0
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 +22 -0
- package/README.md +19 -18
- package/dist/curation/runs.js +65 -50
- package/dist/curation/skill-tool.js +1 -1
- package/dist/extension.d.ts +2 -1
- package/dist/harness/blocks/tools.js +1 -1
- package/dist/harness/boot.js +94 -28
- package/dist/harness/decision-tool.js +1 -1
- package/dist/harness/decisions.js +1 -1
- package/dist/harness/event-bus.js +19 -9
- package/dist/harness/loader.js +14 -6
- package/dist/harness/migration-snapshot.js +104 -0
- package/dist/harness/packages.js +2 -1
- package/dist/harness/reports.js +83 -35
- package/dist/harness/tools.js +1 -1
- package/dist/jobs/docs.js +23 -3
- package/dist/jobs/index.js +74 -15
- package/dist/jobs/observe.js +26 -9
- package/dist/jobs/reconcile.js +12 -3
- package/dist/jobs/restart-limit.js +39 -0
- package/dist/sandbox/protocol.js +3 -2
- package/dist/sandbox/sandboxed-env.js +4 -0
- package/dist/sandbox/worker.js +21 -4
- package/dist/server/conversation-watch.js +49 -0
- package/dist/server/helpers.js +5 -5
- package/dist/server/host-bridge.js +6 -3
- package/dist/server/routes/admin.js +7 -36
- package/dist/server/routes/jobs.js +30 -1
- package/dist/server/routes/metrics.js +31 -7
- package/dist/server/routes/stream.js +6 -3
- package/dist/server/schema-fixture.js +59 -56
- package/dist/skills/index.js +1 -1
- package/node_modules/@sigrun/shared/CHANGELOG.md +4 -0
- package/node_modules/@sigrun/shared/dist/live.d.ts +25 -0
- package/node_modules/@sigrun/shared/dist/live.js +30 -0
- package/node_modules/@sigrun/shared/dist/views.d.ts +15 -0
- package/node_modules/@sigrun/shared/package.json +1 -1
- package/package.json +14 -14
- package/webui/dist/assets/{index-bUGUhbPh.js → index-B77MiI5E.js} +1 -1
- package/webui/dist/assets/{index-DALBZuYo.css → index-BvOKatk4.css} +1 -1
- package/webui/dist/index.html +2 -2
- package/dist/jobs/parked.js +0 -38
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The full-history conversation watch (pi-durable 1.5.0 Task 2, D1-A).
|
|
3
|
+
*
|
|
4
|
+
* D1-A: a conversation view's `entries` is the append-only, fork-aware RAW
|
|
5
|
+
* HISTORY keyed by entry id — never the model's context — and nothing in
|
|
6
|
+
* core cuts entries at a `pi.compaction`/`pi.reset` head marker. Upstream
|
|
7
|
+
* windows DEFAULT to sliding (`{min: 50, max: 100}`), which would drop the
|
|
8
|
+
* oldest entries once a conversation outgrows 100; pinning the window to
|
|
9
|
+
* {@link FULL_HISTORY_WINDOW} makes the watch never slide.
|
|
10
|
+
*
|
|
11
|
+
* One window for every server-side watch: the SSE view stream
|
|
12
|
+
* (`routes/stream.ts`), the host bridge's `conversations.watch`
|
|
13
|
+
* (`host-bridge.ts`), and one-shot reads (`helpers.ts` `readView`).
|
|
14
|
+
*/
|
|
15
|
+
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
16
|
+
/**
|
|
17
|
+
* The never-sliding window: `min`/`max` at the upstream cap (`2 ** 31`
|
|
18
|
+
* validation ceiling), so the watch holds every entry the transcript ever
|
|
19
|
+
* gains and never drops its oldest.
|
|
20
|
+
*/
|
|
21
|
+
export const FULL_HISTORY_WINDOW = {
|
|
22
|
+
min: 2 ** 30,
|
|
23
|
+
max: 2 ** 31,
|
|
24
|
+
};
|
|
25
|
+
/**
|
|
26
|
+
* Watch a conversation's full history. The returned handle's `value` is the
|
|
27
|
+
* acquisition revision (every entry so far); later revisions keep the whole
|
|
28
|
+
* history because the window never slides.
|
|
29
|
+
*/
|
|
30
|
+
export function watchFullHistory(conversation, context) {
|
|
31
|
+
return conversation.watch(context, FULL_HISTORY_WINDOW);
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Read one conversation's current full-history view; disposes the watch
|
|
35
|
+
* handle (the temporary Task-1 `readView` shape, now never-sliding).
|
|
36
|
+
*/
|
|
37
|
+
export async function readFullView(conversation, context) {
|
|
38
|
+
const watch = await watchFullHistory(conversation, context);
|
|
39
|
+
try {
|
|
40
|
+
return watch.value;
|
|
41
|
+
}
|
|
42
|
+
finally {
|
|
43
|
+
await watch.stop();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
/** Convenience: the background-context read (helpers.ts `readView`). */
|
|
47
|
+
export function readFullConversationView(conversation) {
|
|
48
|
+
return readFullView(conversation, BACKGROUND_CONTEXT);
|
|
49
|
+
}
|
package/dist/server/helpers.js
CHANGED
|
@@ -4,6 +4,7 @@
|
|
|
4
4
|
* the route modules consume.
|
|
5
5
|
*/
|
|
6
6
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
7
|
+
import { readFullConversationView } from "./conversation-watch.js";
|
|
7
8
|
import { existsSync, readFileSync } from "node:fs";
|
|
8
9
|
import { homedir } from "node:os";
|
|
9
10
|
import { isAbsolute, resolve } from "node:path";
|
|
@@ -232,12 +233,11 @@ export function conversationId(id) {
|
|
|
232
233
|
export function submissionId(id) {
|
|
233
234
|
return id;
|
|
234
235
|
}
|
|
235
|
-
/** Read one conversation's current view frame; disposes the
|
|
236
|
+
/** Read one conversation's current view frame; disposes the watch handle. */
|
|
236
237
|
export async function readView(conversation) {
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
await
|
|
240
|
-
return view;
|
|
238
|
+
// Task 2 (D1-A): the full-history read — the window never slides, so
|
|
239
|
+
// `entries` is the raw append-only history, never cut at a head marker.
|
|
240
|
+
return serializeConversationView(await readFullConversationView(conversation));
|
|
241
241
|
}
|
|
242
242
|
/**
|
|
243
243
|
* Stamp the resolved caller identity onto a fresh conversation's
|
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
* superuser's HTTP list does: every row visible, `access: "write"`.
|
|
17
17
|
*/
|
|
18
18
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
19
|
+
import { watchFullHistory } from "./conversation-watch.js";
|
|
19
20
|
import { accessOf as accessOfShared } from "@sigrun/shared/access";
|
|
20
21
|
import { completeOnce } from "../harness/complete.js";
|
|
21
22
|
import { effectiveAdmin } from "../harness/identity/groups.js";
|
|
@@ -83,14 +84,16 @@ export function createHostBridge(ctx) {
|
|
|
83
84
|
// acquisition revision is the first listener call, then every
|
|
84
85
|
// watch revision after (pending frames flow because start
|
|
85
86
|
// never invokes the listener inline).
|
|
86
|
-
const watch = await conversation
|
|
87
|
+
const watch = await watchFullHistory(conversation, BACKGROUND_CONTEXT);
|
|
87
88
|
listener(serializeConversationView(watch.value));
|
|
88
|
-
watch.start((
|
|
89
|
+
watch.start((call) => {
|
|
90
|
+
if (call.kind !== "value")
|
|
91
|
+
return Promise.resolve();
|
|
89
92
|
// An extension listener that throws (or returns a rejecting
|
|
90
93
|
// promise — async listeners) must not end the watch or the
|
|
91
94
|
// process — warn and keep delivering.
|
|
92
95
|
return Promise.resolve()
|
|
93
|
-
.then(() => listener(serializeConversationView(value)))
|
|
96
|
+
.then(() => listener(serializeConversationView(call.value)))
|
|
94
97
|
.catch((cause) => {
|
|
95
98
|
console.warn(`host bridge: watch listener failed: ${cause instanceof Error ? cause.message : String(cause)}`);
|
|
96
99
|
});
|
|
@@ -18,15 +18,13 @@
|
|
|
18
18
|
* graceful-shutdown + `process.exit(0)`) owns the exit — the route
|
|
19
19
|
* never calls exit itself.
|
|
20
20
|
*
|
|
21
|
-
* D5
|
|
21
|
+
* D5 blocked-task visibility (pi-durable 1.5.0):
|
|
22
22
|
* `GET /v1/admin/tasks?state=blocked` lists blocked tasks from
|
|
23
23
|
* `harness.inspect()` (id, kind, reason, conversationId, owner from the
|
|
24
|
-
* conversation's SessionMeta when present)
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* id but SILENTLY no-ops for a task that isn't parked. Both endpoints
|
|
29
|
-
* are admin-only (403 forbidden).
|
|
24
|
+
* conversation's SessionMeta when present). Admin-only (403 forbidden).
|
|
25
|
+
* The LISTING stays; the retry route is gone — 1.5.0 removed
|
|
26
|
+
* task_parked/retryTask, and its replacement recovery surface is the
|
|
27
|
+
* job-level `POST /v1/jobs/:id/resume` (Task 4, D2).
|
|
30
28
|
*
|
|
31
29
|
* Durability (Review Focus 4): the restart rides the SAME ordered
|
|
32
30
|
* shutdown as the SIGTERM path — the event bus drains pending settles
|
|
@@ -67,9 +65,8 @@ export function registerAdminRoutes(app, ctx) {
|
|
|
67
65
|
});
|
|
68
66
|
return await reply.code(202).send({ restarting: true });
|
|
69
67
|
});
|
|
70
|
-
// D5
|
|
71
|
-
// blocked tasks from `harness.inspect()
|
|
72
|
-
// re-runs a restart-parked task. Both admin-only (403 before any
|
|
68
|
+
// D5 blocked-task visibility. `GET /v1/admin/tasks?state=blocked` lists
|
|
69
|
+
// blocked tasks from `harness.inspect()` — admin-only (403 before any
|
|
73
70
|
// inspection, so a non-admin learns nothing about the task graph).
|
|
74
71
|
app.get("/v1/admin/tasks", async (request, reply) => {
|
|
75
72
|
if (!request.principal.admin) {
|
|
@@ -126,30 +123,4 @@ export function registerAdminRoutes(app, ctx) {
|
|
|
126
123
|
});
|
|
127
124
|
return await reply.send({ tasks });
|
|
128
125
|
});
|
|
129
|
-
app.post("/v1/admin/tasks/:id/retry", async (request, reply) => {
|
|
130
|
-
if (!request.principal.admin) {
|
|
131
|
-
return await reply.code(403).send({ error: "forbidden" });
|
|
132
|
-
}
|
|
133
|
-
const { id } = request.params;
|
|
134
|
-
const taskId = Number(id);
|
|
135
|
-
if (!Number.isInteger(taskId) || taskId < 0 || String(taskId) !== id) {
|
|
136
|
-
return await reply.code(404).send({ error: "unknown_task" });
|
|
137
|
-
}
|
|
138
|
-
// Check inspect() FIRST: pi-durable's retryTask rejects an unknown
|
|
139
|
-
// id but SILENTLY no-ops for a task that isn't parked — the 409
|
|
140
|
-
// must come from the inspection, not from retryTask.
|
|
141
|
-
const inspection = await ctx.opened.harness.inspect(BACKGROUND_CONTEXT);
|
|
142
|
-
const task = inspection.tasks.find((candidate) => String(candidate.record.id) === id);
|
|
143
|
-
if (task === undefined) {
|
|
144
|
-
return await reply.code(404).send({ error: "unknown_task" });
|
|
145
|
-
}
|
|
146
|
-
if (task.state.kind !== "blocked" || task.state.reason !== "restart_limit") {
|
|
147
|
-
return await reply.code(409).send({ error: "not_parked" });
|
|
148
|
-
}
|
|
149
|
-
// For a schedule task the job's status fields return to `waiting`
|
|
150
|
-
// on the task's next committed state (the task's own phases patch
|
|
151
|
-
// the record) — the route deliberately does NOT hand-patch the job.
|
|
152
|
-
await ctx.opened.harness.retryTask(task.record.id, BACKGROUND_CONTEXT);
|
|
153
|
-
return await reply.code(204).send();
|
|
154
|
-
});
|
|
155
126
|
}
|
|
@@ -24,7 +24,7 @@
|
|
|
24
24
|
*/
|
|
25
25
|
import { accessPrincipalOf } from "../auth/access-principal.js";
|
|
26
26
|
import { jobAccess, resolveJob, } from "../jobs-access.js";
|
|
27
|
-
import { JobDeclaredError, JobNotFoundError } from "../../jobs/index.js";
|
|
27
|
+
import { JobDeclaredError, JobNotBlockedError, JobNotFoundError } from "../../jobs/index.js";
|
|
28
28
|
import { validateJobSpec } from "../../jobs/spec.js";
|
|
29
29
|
/**
|
|
30
30
|
* The shared `:id` error mapping: unknown/hidden → 404 `job_not_found`;
|
|
@@ -45,6 +45,9 @@ async function domainError(reply, error) {
|
|
|
45
45
|
if (error instanceof JobNotFoundError) {
|
|
46
46
|
return await reply.code(404).send({ error: "job_not_found" });
|
|
47
47
|
}
|
|
48
|
+
if (error instanceof JobNotBlockedError) {
|
|
49
|
+
return await reply.code(409).send({ error: "not_blocked" });
|
|
50
|
+
}
|
|
48
51
|
throw error;
|
|
49
52
|
}
|
|
50
53
|
/** Register the `/v1/jobs` routes. */
|
|
@@ -229,6 +232,32 @@ export function registerJobRoutes(app, ctx) {
|
|
|
229
232
|
}
|
|
230
233
|
return await jobs.runNow(id);
|
|
231
234
|
});
|
|
235
|
+
// POST /v1/jobs/:id/resume — restart-limit recovery (D2). EXACTLY
|
|
236
|
+
// the run route's access checks: unknown or hidden → 404
|
|
237
|
+
// job_not_found; read-only → 403 forbidden. A job that is not
|
|
238
|
+
// restart-limited → 409 not_blocked; a blocked/restart_limit job
|
|
239
|
+
// re-arms (state waiting, executor recreated) → 200 with the wire.
|
|
240
|
+
app.post("/v1/jobs/:id/resume", async (request, reply) => {
|
|
241
|
+
const { id } = request.params;
|
|
242
|
+
const wire = await jobs.get(id);
|
|
243
|
+
if (wire === undefined) {
|
|
244
|
+
return await deny(reply, "job_not_found");
|
|
245
|
+
}
|
|
246
|
+
const access = jobAccess(accessPrincipal(request), wire);
|
|
247
|
+
if (access === "none") {
|
|
248
|
+
return await deny(reply, "job_not_found");
|
|
249
|
+
}
|
|
250
|
+
if (access === "read") {
|
|
251
|
+
return await deny(reply, "forbidden");
|
|
252
|
+
}
|
|
253
|
+
try {
|
|
254
|
+
const resumed = await jobs.resume(id);
|
|
255
|
+
return { job: resumed };
|
|
256
|
+
}
|
|
257
|
+
catch (error) {
|
|
258
|
+
return await domainError(reply, error);
|
|
259
|
+
}
|
|
260
|
+
});
|
|
232
261
|
// GET /v1/jobs/:id/runs?limit= — newest first; answers for retired
|
|
233
262
|
// jobs (the revision-resolved access) until retention trims them.
|
|
234
263
|
app.get("/v1/jobs/:id/runs", async (request, reply) => {
|
|
@@ -55,14 +55,14 @@
|
|
|
55
55
|
* type is `text/plain; version=0.0.4; charset=utf-8`.
|
|
56
56
|
*/
|
|
57
57
|
import { BACKGROUND_CONTEXT } from "@earendil-works/chord/context";
|
|
58
|
-
import {
|
|
58
|
+
import { REPORT_KINDS } from "../../harness/reports.js";
|
|
59
59
|
import { readBackupMarker } from "../../backup/marker.js";
|
|
60
60
|
import { scanJobs } from "../../jobs/docs.js";
|
|
61
61
|
import { providerAuthStatus } from "../helpers.js";
|
|
62
62
|
import { checkStorage } from "../readiness.js";
|
|
63
63
|
/** The exposition content type (spec §6.9's format rule). */
|
|
64
64
|
const EXPOSITION_CONTENT_TYPE = "text/plain; version=0.0.4; charset=utf-8";
|
|
65
|
-
const BLOCKED_REASONS = ["missing_task", "task_too_old", "migration_failed"
|
|
65
|
+
const BLOCKED_REASONS = ["missing_task", "task_too_old", "migration_failed"];
|
|
66
66
|
/** Escape one label value per the exposition format: `\` → `\\`,
|
|
67
67
|
* `"` → `\"`, newline → `\n`. */
|
|
68
68
|
function escapeLabelValue(value) {
|
|
@@ -124,11 +124,12 @@ export function registerMetricsRoutes(app, ctx) {
|
|
|
124
124
|
const oldestMs = opened.eventBus.oldestLiveAdmissionMs();
|
|
125
125
|
family("sigrun_inflight_submission_oldest_seconds", "Age of the oldest in-flight submission's admission (0 when none; in-process clock — a pre-restart run is invisible until it settles)", [{ labels: {}, value: Math.round((oldestMs ?? 0) / 1000) }]);
|
|
126
126
|
// Live tasks by scheduler state kind. The full six-kind
|
|
127
|
-
// vocabulary is emitted (0s included) for label stability
|
|
128
|
-
//
|
|
127
|
+
// vocabulary is emitted (0s included) for label stability.
|
|
128
|
+
// pi-durable 1.5.0 renamed `ready` to `pending` (the queued,
|
|
129
|
+
// runnable-but-not-yet-admitted state).
|
|
129
130
|
const taskStatuses = {
|
|
130
131
|
running: 0,
|
|
131
|
-
|
|
132
|
+
pending: 0,
|
|
132
133
|
waiting: 0,
|
|
133
134
|
sleeping: 0,
|
|
134
135
|
completing: 0,
|
|
@@ -171,6 +172,26 @@ export function registerMetricsRoutes(app, ctx) {
|
|
|
171
172
|
value: epochSeconds(record.nextRun),
|
|
172
173
|
},
|
|
173
174
|
]));
|
|
175
|
+
// D2: the job-record blocked gauge. pi-durable 1.5.0 has no
|
|
176
|
+
// task park state — a restart-limited schedule task ends
|
|
177
|
+
// faulted and its JOB record carries blocked/restart_limit —
|
|
178
|
+
// so the alertable signal lives on the job records. Emitted
|
|
179
|
+
// for restart_limit plus zero-valued label stability; other
|
|
180
|
+
// blocked reasons (fire-time blocks) ride the same gauge when
|
|
181
|
+
// they occur.
|
|
182
|
+
const jobBlockedReasons = {
|
|
183
|
+
restart_limit: 0,
|
|
184
|
+
};
|
|
185
|
+
for (const { record } of jobRecords) {
|
|
186
|
+
if (record.state !== "blocked")
|
|
187
|
+
continue;
|
|
188
|
+
const reason = record.reason ?? "unknown";
|
|
189
|
+
jobBlockedReasons[reason] = (jobBlockedReasons[reason] ?? 0) + 1;
|
|
190
|
+
}
|
|
191
|
+
family("sigrun_jobs_blocked", "Jobs blocked by record reason", Object.entries(jobBlockedReasons).map(([reason, count]) => ({
|
|
192
|
+
labels: { reason },
|
|
193
|
+
value: count,
|
|
194
|
+
})));
|
|
174
195
|
}
|
|
175
196
|
catch (error) {
|
|
176
197
|
up = 0;
|
|
@@ -185,10 +206,13 @@ export function registerMetricsRoutes(app, ctx) {
|
|
|
185
206
|
// failure even while inspection (and the whole scrape) stays green.
|
|
186
207
|
const storage = await checkStorage(ctx);
|
|
187
208
|
family("sigrun_ready", "1 when the readiness storage check (the /readyz root-conversation read) succeeds", [{ labels: {}, value: storage === "ok" ? 1 : 0 }]);
|
|
188
|
-
// D5: process-local durable failure reports. Every known
|
|
209
|
+
// D5: process-local durable failure reports. Every known kind is
|
|
189
210
|
// emitted, including zeros, for scrape-stable alert expressions.
|
|
211
|
+
// pi-durable 1.5.0 kinds: `task_parked`/`session_failed` are gone;
|
|
212
|
+
// `storage_failed`/`task_interrupted` replaced them (label name
|
|
213
|
+
// `type` kept — a metrics-label rename would break alert queries).
|
|
190
214
|
const reportCounts = opened.reports.counts();
|
|
191
|
-
family("sigrun_durable_reports_total", "Durable failure reports since process start",
|
|
215
|
+
family("sigrun_durable_reports_total", "Durable failure reports since process start", REPORT_KINDS.map((kind) => ({ labels: { type: kind }, value: reportCounts[kind] })), "counter");
|
|
192
216
|
// Provider readiness: the shared derivation (`GET /v1/auth/check`'s
|
|
193
217
|
// exact chain), one 1-valued sample per (provider, current status).
|
|
194
218
|
const producersByName = new Map(opened.providerHealth.map((producer) => [producer.name, producer]));
|
|
@@ -11,6 +11,7 @@ import { loadConversation } from "../conversation-handler.js";
|
|
|
11
11
|
import { ID_PARAMS_SCHEMA } from "../helpers.js";
|
|
12
12
|
import { serializeConversationView, } from "../views.js";
|
|
13
13
|
import { diffViews, isEmptyDelta, } from "@sigrun/shared/view-delta";
|
|
14
|
+
import { watchFullHistory } from "../conversation-watch.js";
|
|
14
15
|
/** Register the SSE view stream route. */
|
|
15
16
|
export function registerStreamRoutes(app, ctx) {
|
|
16
17
|
// SSE view stream (spec §5 "Views + watch frames over SSE", §7.3 ruling
|
|
@@ -35,7 +36,7 @@ export function registerStreamRoutes(app, ctx) {
|
|
|
35
36
|
// error event. `value` is the acquisition revision (the snapshot
|
|
36
37
|
// source); frames published between here and start() are held by the
|
|
37
38
|
// watch's bounded pending delivery, so nothing is lost.
|
|
38
|
-
const watch = await conversation
|
|
39
|
+
const watch = await watchFullHistory(conversation, BACKGROUND_CONTEXT);
|
|
39
40
|
// Watcher bookkeeping (W5 Task 6, spec §9.6): one attached client.
|
|
40
41
|
// The exactly-once `finish` below decrements — the notifications
|
|
41
42
|
// surface's `"unwatched"` runSettled rule reads this count.
|
|
@@ -115,10 +116,12 @@ export function registerStreamRoutes(app, ctx) {
|
|
|
115
116
|
// view the diff loop measures against.
|
|
116
117
|
let prev = serializeConversationView(watch.value);
|
|
117
118
|
send("snapshot", prev);
|
|
118
|
-
watch.start(async (
|
|
119
|
+
watch.start(async (call) => {
|
|
119
120
|
if (finished)
|
|
120
121
|
return; // the stream is over; drop the frame
|
|
121
|
-
|
|
122
|
+
if (call.kind !== "value")
|
|
123
|
+
return;
|
|
124
|
+
const next = serializeConversationView(call.value);
|
|
122
125
|
const delta = diffViews(prev, next);
|
|
123
126
|
prev = next;
|
|
124
127
|
// Not representable as entries append/remove + changed docs (a
|
|
@@ -223,6 +223,9 @@ export const VIEW_SCHEMA = {
|
|
|
223
223
|
},
|
|
224
224
|
"timestamp": {
|
|
225
225
|
"kind": "number"
|
|
226
|
+
},
|
|
227
|
+
"thinkingLevel": {
|
|
228
|
+
"kind": "string"
|
|
226
229
|
}
|
|
227
230
|
}
|
|
228
231
|
}
|
|
@@ -292,14 +295,6 @@ export const VIEW_SCHEMA = {
|
|
|
292
295
|
"type": {
|
|
293
296
|
"kind": "string"
|
|
294
297
|
},
|
|
295
|
-
"required": {
|
|
296
|
-
"kind": "array",
|
|
297
|
-
"variants": [
|
|
298
|
-
{
|
|
299
|
-
"kind": "string"
|
|
300
|
-
}
|
|
301
|
-
]
|
|
302
|
-
},
|
|
303
298
|
"properties": {
|
|
304
299
|
"kind": "object",
|
|
305
300
|
"fields": {
|
|
@@ -315,6 +310,14 @@ export const VIEW_SCHEMA = {
|
|
|
315
310
|
}
|
|
316
311
|
}
|
|
317
312
|
}
|
|
313
|
+
},
|
|
314
|
+
"required": {
|
|
315
|
+
"kind": "array",
|
|
316
|
+
"variants": [
|
|
317
|
+
{
|
|
318
|
+
"kind": "string"
|
|
319
|
+
}
|
|
320
|
+
]
|
|
318
321
|
}
|
|
319
322
|
}
|
|
320
323
|
},
|
|
@@ -519,14 +522,6 @@ export const VIEW_SCHEMA = {
|
|
|
519
522
|
"type": {
|
|
520
523
|
"kind": "string"
|
|
521
524
|
},
|
|
522
|
-
"required": {
|
|
523
|
-
"kind": "array",
|
|
524
|
-
"variants": [
|
|
525
|
-
{
|
|
526
|
-
"kind": "string"
|
|
527
|
-
}
|
|
528
|
-
]
|
|
529
|
-
},
|
|
530
525
|
"properties": {
|
|
531
526
|
"kind": "object",
|
|
532
527
|
"fields": {
|
|
@@ -553,6 +548,14 @@ export const VIEW_SCHEMA = {
|
|
|
553
548
|
}
|
|
554
549
|
}
|
|
555
550
|
}
|
|
551
|
+
},
|
|
552
|
+
"required": {
|
|
553
|
+
"kind": "array",
|
|
554
|
+
"variants": [
|
|
555
|
+
{
|
|
556
|
+
"kind": "string"
|
|
557
|
+
}
|
|
558
|
+
]
|
|
556
559
|
}
|
|
557
560
|
}
|
|
558
561
|
}
|
|
@@ -573,14 +576,6 @@ export const VIEW_SCHEMA = {
|
|
|
573
576
|
"type": {
|
|
574
577
|
"kind": "string"
|
|
575
578
|
},
|
|
576
|
-
"required": {
|
|
577
|
-
"kind": "array",
|
|
578
|
-
"variants": [
|
|
579
|
-
{
|
|
580
|
-
"kind": "string"
|
|
581
|
-
}
|
|
582
|
-
]
|
|
583
|
-
},
|
|
584
579
|
"properties": {
|
|
585
580
|
"kind": "object",
|
|
586
581
|
"fields": {
|
|
@@ -607,6 +602,14 @@ export const VIEW_SCHEMA = {
|
|
|
607
602
|
}
|
|
608
603
|
}
|
|
609
604
|
}
|
|
605
|
+
},
|
|
606
|
+
"required": {
|
|
607
|
+
"kind": "array",
|
|
608
|
+
"variants": [
|
|
609
|
+
{
|
|
610
|
+
"kind": "string"
|
|
611
|
+
}
|
|
612
|
+
]
|
|
610
613
|
}
|
|
611
614
|
}
|
|
612
615
|
}
|
|
@@ -686,14 +689,6 @@ export const VIEW_SCHEMA = {
|
|
|
686
689
|
"type": {
|
|
687
690
|
"kind": "string"
|
|
688
691
|
},
|
|
689
|
-
"required": {
|
|
690
|
-
"kind": "array",
|
|
691
|
-
"variants": [
|
|
692
|
-
{
|
|
693
|
-
"kind": "string"
|
|
694
|
-
}
|
|
695
|
-
]
|
|
696
|
-
},
|
|
697
692
|
"properties": {
|
|
698
693
|
"kind": "object",
|
|
699
694
|
"fields": {
|
|
@@ -720,14 +715,6 @@ export const VIEW_SCHEMA = {
|
|
|
720
715
|
"type": {
|
|
721
716
|
"kind": "string"
|
|
722
717
|
},
|
|
723
|
-
"required": {
|
|
724
|
-
"kind": "array",
|
|
725
|
-
"variants": [
|
|
726
|
-
{
|
|
727
|
-
"kind": "string"
|
|
728
|
-
}
|
|
729
|
-
]
|
|
730
|
-
},
|
|
731
718
|
"properties": {
|
|
732
719
|
"kind": "object",
|
|
733
720
|
"fields": {
|
|
@@ -754,6 +741,14 @@ export const VIEW_SCHEMA = {
|
|
|
754
741
|
}
|
|
755
742
|
}
|
|
756
743
|
}
|
|
744
|
+
},
|
|
745
|
+
"required": {
|
|
746
|
+
"kind": "array",
|
|
747
|
+
"variants": [
|
|
748
|
+
{
|
|
749
|
+
"kind": "string"
|
|
750
|
+
}
|
|
751
|
+
]
|
|
757
752
|
}
|
|
758
753
|
}
|
|
759
754
|
},
|
|
@@ -763,6 +758,14 @@ export const VIEW_SCHEMA = {
|
|
|
763
758
|
}
|
|
764
759
|
}
|
|
765
760
|
}
|
|
761
|
+
},
|
|
762
|
+
"required": {
|
|
763
|
+
"kind": "array",
|
|
764
|
+
"variants": [
|
|
765
|
+
{
|
|
766
|
+
"kind": "string"
|
|
767
|
+
}
|
|
768
|
+
]
|
|
766
769
|
}
|
|
767
770
|
}
|
|
768
771
|
}
|
|
@@ -914,14 +917,6 @@ export const VIEW_SCHEMA = {
|
|
|
914
917
|
"type": {
|
|
915
918
|
"kind": "string"
|
|
916
919
|
},
|
|
917
|
-
"required": {
|
|
918
|
-
"kind": "array",
|
|
919
|
-
"variants": [
|
|
920
|
-
{
|
|
921
|
-
"kind": "string"
|
|
922
|
-
}
|
|
923
|
-
]
|
|
924
|
-
},
|
|
925
920
|
"properties": {
|
|
926
921
|
"kind": "object",
|
|
927
922
|
"fields": {
|
|
@@ -959,6 +954,14 @@ export const VIEW_SCHEMA = {
|
|
|
959
954
|
}
|
|
960
955
|
}
|
|
961
956
|
}
|
|
957
|
+
},
|
|
958
|
+
"required": {
|
|
959
|
+
"kind": "array",
|
|
960
|
+
"variants": [
|
|
961
|
+
{
|
|
962
|
+
"kind": "string"
|
|
963
|
+
}
|
|
964
|
+
]
|
|
962
965
|
}
|
|
963
966
|
}
|
|
964
967
|
}
|
|
@@ -979,14 +982,6 @@ export const VIEW_SCHEMA = {
|
|
|
979
982
|
"type": {
|
|
980
983
|
"kind": "string"
|
|
981
984
|
},
|
|
982
|
-
"required": {
|
|
983
|
-
"kind": "array",
|
|
984
|
-
"variants": [
|
|
985
|
-
{
|
|
986
|
-
"kind": "string"
|
|
987
|
-
}
|
|
988
|
-
]
|
|
989
|
-
},
|
|
990
985
|
"properties": {
|
|
991
986
|
"kind": "object",
|
|
992
987
|
"fields": {
|
|
@@ -1013,6 +1008,14 @@ export const VIEW_SCHEMA = {
|
|
|
1013
1008
|
}
|
|
1014
1009
|
}
|
|
1015
1010
|
}
|
|
1011
|
+
},
|
|
1012
|
+
"required": {
|
|
1013
|
+
"kind": "array",
|
|
1014
|
+
"variants": [
|
|
1015
|
+
{
|
|
1016
|
+
"kind": "string"
|
|
1017
|
+
}
|
|
1018
|
+
]
|
|
1016
1019
|
}
|
|
1017
1020
|
}
|
|
1018
1021
|
}
|
package/dist/skills/index.js
CHANGED
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
* (the reserved-section owner is unchanged) and never goes through the
|
|
61
61
|
* extension loader.
|
|
62
62
|
*/
|
|
63
|
-
import { Type } from "
|
|
63
|
+
import { Type } from "typebox";
|
|
64
64
|
import { defineExtension, defineTool, section, } from "@earendil-works/pi-durable";
|
|
65
65
|
import { readdirSync, readFileSync } from "node:fs";
|
|
66
66
|
import { join } from "node:path";
|
|
@@ -6,6 +6,10 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
8
|
|
|
9
|
+
## [0.11.0] - 2026-11-07
|
|
10
|
+
|
|
11
|
+
- Version-locked bump with the 0.11.0 core line (the pi-durable 1.5.0 release).
|
|
12
|
+
|
|
9
13
|
## [0.10.2] - 2026-11-06
|
|
10
14
|
|
|
11
15
|
- Version-locked bump with the 0.10.2 core line (no changes).
|
|
@@ -42,6 +42,17 @@ export interface LiveDocWire {
|
|
|
42
42
|
readonly compactionFailure?: {
|
|
43
43
|
readonly error?: unknown;
|
|
44
44
|
};
|
|
45
|
+
/**
|
|
46
|
+
* Why the newest run ended without an answer (pi-durable 1.5.0
|
|
47
|
+
* `LiveState.runFailure`): set in the commit that ends the run,
|
|
48
|
+
* removed when a later commit starts one. The clients narrow it to
|
|
49
|
+
* `{reason, detail?}` — taskId/inputs/first are the harness's
|
|
50
|
+
* bookkeeping, not display data.
|
|
51
|
+
*/
|
|
52
|
+
readonly runFailure?: {
|
|
53
|
+
readonly reason?: unknown;
|
|
54
|
+
readonly detail?: unknown;
|
|
55
|
+
};
|
|
45
56
|
}
|
|
46
57
|
/**
|
|
47
58
|
* The view's `pi.live` doc, or undefined when absent (idle and never-run
|
|
@@ -69,3 +80,17 @@ export declare function liveGenerationText(view: ConversationViewBase): string;
|
|
|
69
80
|
export declare function liveCompactions(view: ConversationViewBase): number;
|
|
70
81
|
/** The newest compaction failure's error text, when one is surfaced. */
|
|
71
82
|
export declare function liveCompactionFailure(view: ConversationViewBase): string | undefined;
|
|
83
|
+
/**
|
|
84
|
+
* The newest run's failure (`pi.live.runFailure`, pi-durable 1.5.0), as
|
|
85
|
+
* the clients display it: `{reason, detail?}`, or undefined when no
|
|
86
|
+
* failure is recorded, when `run` is present (a newer run started — the
|
|
87
|
+
* recorded failure is stale), when the reason is user-caused
|
|
88
|
+
* (`aborted`/`reset` — the user watched themselves do it), or when the
|
|
89
|
+
* doc shape drifted (fail soft to absent, like
|
|
90
|
+
* {@link liveCompactionFailure}). A non-string detail is dropped, never
|
|
91
|
+
* surfaced.
|
|
92
|
+
*/
|
|
93
|
+
export declare function liveRunFailure(view: ConversationViewBase): {
|
|
94
|
+
readonly reason: string;
|
|
95
|
+
readonly detail?: string;
|
|
96
|
+
} | undefined;
|
|
@@ -80,3 +80,33 @@ export function liveCompactionFailure(view) {
|
|
|
80
80
|
const error = failure.error;
|
|
81
81
|
return typeof error === "string" && error !== "" ? error : undefined;
|
|
82
82
|
}
|
|
83
|
+
/** The run-failure reasons the USER caused — hidden by design. */
|
|
84
|
+
const USER_CAUSED_RUN_FAILURES = new Set(["aborted", "reset"]);
|
|
85
|
+
/**
|
|
86
|
+
* The newest run's failure (`pi.live.runFailure`, pi-durable 1.5.0), as
|
|
87
|
+
* the clients display it: `{reason, detail?}`, or undefined when no
|
|
88
|
+
* failure is recorded, when `run` is present (a newer run started — the
|
|
89
|
+
* recorded failure is stale), when the reason is user-caused
|
|
90
|
+
* (`aborted`/`reset` — the user watched themselves do it), or when the
|
|
91
|
+
* doc shape drifted (fail soft to absent, like
|
|
92
|
+
* {@link liveCompactionFailure}). A non-string detail is dropped, never
|
|
93
|
+
* surfaced.
|
|
94
|
+
*/
|
|
95
|
+
export function liveRunFailure(view) {
|
|
96
|
+
const doc = liveDoc(view);
|
|
97
|
+
if (doc === undefined || doc.run !== undefined)
|
|
98
|
+
return undefined;
|
|
99
|
+
const failure = doc.runFailure;
|
|
100
|
+
if (typeof failure !== "object" || failure === null)
|
|
101
|
+
return undefined;
|
|
102
|
+
const reason = failure.reason;
|
|
103
|
+
if (typeof reason !== "string" || reason === "")
|
|
104
|
+
return undefined;
|
|
105
|
+
if (USER_CAUSED_RUN_FAILURES.has(reason))
|
|
106
|
+
return undefined;
|
|
107
|
+
const detail = failure.detail;
|
|
108
|
+
if (typeof detail === "string" && detail !== "") {
|
|
109
|
+
return { reason, detail };
|
|
110
|
+
}
|
|
111
|
+
return { reason };
|
|
112
|
+
}
|