@sigrun/sigrun 0.10.1 → 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 +36 -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 +8 -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-Bh9dImMT.js → index-B77MiI5E.js} +2 -2
- package/webui/dist/assets/index-BvOKatk4.css +1 -0
- package/webui/dist/index.html +2 -2
- package/dist/jobs/parked.js +0 -38
- package/webui/dist/assets/index-B3T0KKiX.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,42 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
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
|
+
## [0.11.0] - 2026-11-07
|
|
9
|
+
|
|
10
|
+
### Changed
|
|
11
|
+
|
|
12
|
+
- BREAKING: pi-durable 1.5.0. Conversation data written by this release cannot be read by 0.9.x;
|
|
13
|
+
the first boot under a NEWER pi-durable minor writes `<db>.pre-pi-durable-1.5.0-<stamp>.bak`
|
|
14
|
+
(when no schema-migration snapshot was already taken this boot — a store jumping several
|
|
15
|
+
schema versions snapshots at the schema guard instead); downgrade = restore that file.
|
|
16
|
+
- BREAKING: Transcripts now show the full raw history: turns before a compaction stay above the
|
|
17
|
+
"context compacted" divider.
|
|
18
|
+
- BREAKING: `POST /v1/admin/tasks/:id/retry` removed; a schedule that hits the restart limit
|
|
19
|
+
blocks its job; resume with `POST /v1/jobs/:id/resume`.
|
|
20
|
+
- BREAKING: Durable report metric labels follow pi-durable 1.5.0 (`session_failed` →
|
|
21
|
+
`storage_failed`, `task_parked` removed, `task_interrupted` added); the live-task status label
|
|
22
|
+
`ready` is now `pending`.
|
|
23
|
+
- BREAKING: Every conversation read (SSE connect, `GET /v1/conversations/:id`, extension
|
|
24
|
+
watches) now loads the full history; very long conversations cost more until windowed
|
|
25
|
+
streaming lands.
|
|
26
|
+
- BREAKING: A host killed mid-answer no longer resends the request on restart; the run retries
|
|
27
|
+
per `settings.retry` or ends `interrupted`.
|
|
28
|
+
- BREAKING: Extensions built for pi-durable 1.4 are quarantined with a peer-range message.
|
|
29
|
+
|
|
30
|
+
## [0.10.2] - 2026-11-06
|
|
31
|
+
|
|
32
|
+
### Fixed
|
|
33
|
+
|
|
34
|
+
- The multi-select bulk action bar stays on screen: it rendered at the top
|
|
35
|
+
of the list, so ticking rows further down a scrolled list (the escalation
|
|
36
|
+
inbox sidebar) left "Dismiss selected" out of view. It is now sticky to
|
|
37
|
+
the collection's scroll container and shows "N selected".
|
|
38
|
+
|
|
39
|
+
### Changed
|
|
40
|
+
|
|
41
|
+
- The family bumps to 0.10.2; companion: `@sigrun/ext-gateway` 0.1.21
|
|
42
|
+
(shared peer gains 0.10.2).
|
|
43
|
+
|
|
8
44
|
## [0.10.1] - 2026-11-06
|
|
9
45
|
|
|
10
46
|
### Fixed
|
package/README.md
CHANGED
|
@@ -978,7 +978,8 @@ backup — are omitted rather than zeroed):
|
|
|
978
978
|
| `sigrun_ready` | 1 when the readiness storage check (the root-conversation read `/readyz` gates on) succeeds; 0 when it fails — fires even while the process and the scrape stay up. |
|
|
979
979
|
| `sigrun_live_runs` | Conversations with a placed-but-unsettled input submission. |
|
|
980
980
|
| `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). |
|
|
981
|
-
| `sigrun_tasks_live{status}` | Live tasks by scheduler state (running/
|
|
981
|
+
| `sigrun_tasks_live{status}` | Live tasks by scheduler state (running/pending/waiting/completing/blocked, 0s included). |
|
|
982
|
+
| `sigrun_jobs_blocked{reason}` | Jobs whose record is blocked, by reason (`restart_limit` — the schedule hit pi-durable's restart limit; resume via `POST /v1/jobs/:id/resume`). |
|
|
982
983
|
| `sigrun_provider_ready{provider,status}` | 1 per provider at its current auth-readiness status (`ready`, `degraded`, `invalid`, `not_ready`, `not_configured`). |
|
|
983
984
|
| `sigrun_extensions{status}` | Configured extensions by load outcome (`ok` / `quarantined`). |
|
|
984
985
|
| `sigrun_job_last_run_timestamp_seconds{job,outcome}` | Unix time of each job's last run, by outcome. |
|
|
@@ -995,6 +996,7 @@ backup — are omitted rather than zeroed):
|
|
|
995
996
|
| `SigrunSubmissionWedged` | `sigrun_inflight_submission_oldest_seconds > 1800` for 5m — a live run is making no progress. |
|
|
996
997
|
| `SigrunProviderNotReady` | A provider sits at a non-ready auth status for 15m. |
|
|
997
998
|
| `SigrunJobFailed` | `increase(sigrun_job_runs_total{outcome="failed"}[1h]) > 0` — a scheduled job failed. |
|
|
999
|
+
| `SigrunJobRestartLimited` | `sigrun_jobs_blocked{reason="restart_limit"} > 0` for 5m — a job hit the restart limit and is blocked until resumed (`POST /v1/jobs/:id/resume`; see Blocked jobs). |
|
|
998
1000
|
| `SigrunBackupStale` | The backup marker is older than 36h while backup is enabled (the rule is absent on an unbacked deploy). |
|
|
999
1001
|
|
|
1000
1002
|
## Extensions
|
|
@@ -1420,32 +1422,30 @@ launcher) is not supervised either; there the callout says "Restart
|
|
|
1420
1422
|
sigrun to apply" with no button. `GET /v1/meta` reports the posture
|
|
1421
1423
|
(`supervised`).
|
|
1422
1424
|
|
|
1423
|
-
##
|
|
1425
|
+
## Blocked jobs
|
|
1424
1426
|
|
|
1425
|
-
A
|
|
1426
|
-
pi-durable's restart limit:
|
|
1427
|
-
`
|
|
1428
|
-
it. The `
|
|
1429
|
-
|
|
1427
|
+
A schedule cut off repeatedly without progress (a crash loop) hits
|
|
1428
|
+
pi-durable's restart limit: the run ENDS and the job's record is marked
|
|
1429
|
+
`blocked` with reason `restart_limit`; it does not fire again until
|
|
1430
|
+
someone with the job's write access resumes it. The `SigrunJobRestartLimited` alert fires on
|
|
1431
|
+
this (`sigrun_jobs_blocked{reason="restart_limit"} > 0`).
|
|
1430
1432
|
|
|
1431
|
-
- `
|
|
1432
|
-
|
|
1433
|
+
- `POST /v1/jobs/:id/resume` — resumes a restart-blocked job: the
|
|
1434
|
+
record returns to its normal scheduled state and the next fire
|
|
1435
|
+
proceeds. (Admin-only; 409 for a job that isn't blocked.)
|
|
1436
|
+
- `GET /v1/admin/tasks?state=blocked` — lists blocked scheduler tasks
|
|
1437
|
+
with `{id, kind, reason, conversationId, owner?, extension?}` per row
|
|
1433
1438
|
(owner is the conversation's session owner when it has one; extension
|
|
1434
1439
|
is the registered task definition's owner when known).
|
|
1435
|
-
- `POST /v1/admin/tasks/:id/retry` — re-runs a restart-parked task:
|
|
1436
|
-
204 on success, 404 `unknown_task` for an unknown id, 409
|
|
1437
|
-
`not_parked` for a task that isn't parked (including one blocked for
|
|
1438
|
-
another reason, such as `missing_task`). A retried schedule job's
|
|
1439
|
-
status returns to `waiting` as the task runs again.
|
|
1440
1440
|
|
|
1441
1441
|
```sh
|
|
1442
1442
|
# List blocked tasks (admin token).
|
|
1443
1443
|
curl -H "Authorization: Bearer $TOKEN" \
|
|
1444
1444
|
"http://localhost:3000/v1/admin/tasks?state=blocked"
|
|
1445
1445
|
|
|
1446
|
-
#
|
|
1446
|
+
# Resume a restart-blocked job.
|
|
1447
1447
|
curl -X POST -H "Authorization: Bearer $TOKEN" \
|
|
1448
|
-
http://localhost:3000/v1/
|
|
1448
|
+
http://localhost:3000/v1/jobs/7/resume
|
|
1449
1449
|
```
|
|
1450
1450
|
|
|
1451
1451
|
## API sketch
|
|
@@ -1457,8 +1457,9 @@ curl -X POST -H "Authorization: Bearer $TOKEN" \
|
|
|
1457
1457
|
differs from boot) and PATCH reports `{applied, pending}`.
|
|
1458
1458
|
- `POST /v1/admin/restart` — graceful restart (admin, supervised
|
|
1459
1459
|
deploys only; see Restarting).
|
|
1460
|
-
- `GET /v1/admin/tasks?state=blocked`, `POST /v1/
|
|
1461
|
-
—
|
|
1460
|
+
- `GET /v1/admin/tasks?state=blocked`, `POST /v1/jobs/{id}/resume`
|
|
1461
|
+
— blocked-task inspection and blocked-job recovery (admin; see
|
|
1462
|
+
Blocked jobs).
|
|
1462
1463
|
- `GET /v1/jobs`, `POST /v1/jobs`, `PATCH`/`DELETE /v1/jobs/{id}` —
|
|
1463
1464
|
the jobs surface (see [Jobs](#jobs)); `POST /v1/jobs/{id}/run` fires
|
|
1464
1465
|
now; `GET /v1/jobs/{id}/runs` and `GET /v1/jobs/{id}/revisions` are
|
package/dist/curation/runs.js
CHANGED
|
@@ -285,7 +285,7 @@ export async function driveRun(deps, prepare, opts, parent) {
|
|
|
285
285
|
// records; two watches run in ONE listener — (a) this run's tool
|
|
286
286
|
// results (the maxToolCalls budget), and (b) the PARENT's drift.
|
|
287
287
|
// Both are synchronous reads of the publication's own changes (the
|
|
288
|
-
//
|
|
288
|
+
// observeCommits listener contract: no blocking, no Session calls).
|
|
289
289
|
//
|
|
290
290
|
// Parent drift, precisely: an ENTRY change on the parent beyond the
|
|
291
291
|
// start-captured head (a reset/compaction marker or a newly admitted
|
|
@@ -310,61 +310,76 @@ export async function driveRun(deps, prepare, opts, parent) {
|
|
|
310
310
|
let watcher;
|
|
311
311
|
const watch = () => {
|
|
312
312
|
watcher?.();
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
change.
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
313
|
+
const observation = harness.observeCommits({
|
|
314
|
+
advance: (publication) => {
|
|
315
|
+
try {
|
|
316
|
+
for (const change of publication.changes) {
|
|
317
|
+
if (change.type === "entry") {
|
|
318
|
+
if (runId !== undefined &&
|
|
319
|
+
change.value.conversationId === runId &&
|
|
320
|
+
change.value.kind === ToolResultEntry.kind) {
|
|
321
|
+
toolResults += 1;
|
|
322
|
+
if (toolResults >= opts.maxToolCalls) {
|
|
323
|
+
stop({ kind: "maxToolCalls" });
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
if (parent !== undefined && start !== undefined) {
|
|
327
|
+
if (change.value.conversationId === parent &&
|
|
328
|
+
change.value.id !== start.head &&
|
|
329
|
+
isAfter(change.value.id, start.head)) {
|
|
330
|
+
stop({
|
|
331
|
+
kind: "parent",
|
|
332
|
+
what: change.value.kind === "pi.reset"
|
|
333
|
+
? "the conversation was reset or compacted"
|
|
334
|
+
: "a new run was admitted",
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
continue;
|
|
322
339
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (change.
|
|
326
|
-
change.value.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
what: change.value.kind === "pi.reset"
|
|
331
|
-
? "the conversation was reset or compacted"
|
|
332
|
-
: "a new run was admitted",
|
|
333
|
-
});
|
|
340
|
+
if (parent === undefined || start === undefined)
|
|
341
|
+
continue;
|
|
342
|
+
if (change.type === "submission") {
|
|
343
|
+
if (change.value.conversationId === parent) {
|
|
344
|
+
stop({ kind: "parent", what: "a new run was admitted" });
|
|
345
|
+
}
|
|
346
|
+
continue;
|
|
334
347
|
}
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
// the adopted JSON revision; the structural read
|
|
355
|
-
// touches two optional fields only.
|
|
356
|
-
const value = change.value;
|
|
357
|
-
if (value.owner !== start.owner ||
|
|
358
|
-
sharingKey(value.sharing) !== sharingKey(start.sharing)) {
|
|
359
|
-
stop({
|
|
360
|
-
kind: "parent",
|
|
361
|
-
what: "the parent's owner or sharing changed",
|
|
362
|
-
});
|
|
348
|
+
if (change.type === "document") {
|
|
349
|
+
if (change.conversationId === parent &&
|
|
350
|
+
change.record.kind === "sigrun.session-meta" &&
|
|
351
|
+
change.kind === "change") {
|
|
352
|
+
// Compare the COMMITTED VALUE's owner/sharing fields —
|
|
353
|
+
// the activity stamper writes `lastActivityAt` on the
|
|
354
|
+
// same doc after every settle (a benign change that
|
|
355
|
+
// must never cancel a review). SAFETY: the value is
|
|
356
|
+
// the adopted JSON revision; the structural read
|
|
357
|
+
// touches two optional fields only.
|
|
358
|
+
const value = change.value;
|
|
359
|
+
if (value.owner !== start.owner ||
|
|
360
|
+
sharingKey(value.sharing) !== sharingKey(start.sharing)) {
|
|
361
|
+
stop({
|
|
362
|
+
kind: "parent",
|
|
363
|
+
what: "the parent's owner or sharing changed",
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
}
|
|
363
367
|
}
|
|
364
368
|
}
|
|
365
369
|
}
|
|
366
|
-
|
|
370
|
+
catch (error) {
|
|
371
|
+
console.error("curation/runs: commit observer failed", error);
|
|
372
|
+
}
|
|
373
|
+
},
|
|
374
|
+
end: (end) => {
|
|
375
|
+
if (end.kind === "ended")
|
|
376
|
+
return;
|
|
377
|
+
if (end.kind === "failed" && end.error instanceof Error && end.error.message === "session closed")
|
|
378
|
+
return;
|
|
379
|
+
console.error("curation/runs: commit observation failed", end.kind === "failed" ? end.error : end);
|
|
380
|
+
},
|
|
367
381
|
});
|
|
382
|
+
watcher = () => observation.detach();
|
|
368
383
|
};
|
|
369
384
|
watch();
|
|
370
385
|
const cleanup = () => {
|
|
@@ -25,7 +25,7 @@
|
|
|
25
25
|
* — the change id for the P8 undo surface and a one-line summary for
|
|
26
26
|
* the UI.
|
|
27
27
|
*/
|
|
28
|
-
import { Type } from "
|
|
28
|
+
import { Type } from "typebox";
|
|
29
29
|
import { defineTool } from "@earendil-works/pi-durable";
|
|
30
30
|
import { OVERLAY_HEADING, withOverlay, } from "./overlays.js";
|
|
31
31
|
import { curationRun, markViewed } from "./run-doc.js";
|
package/dist/extension.d.ts
CHANGED
|
@@ -32,7 +32,8 @@
|
|
|
32
32
|
* validated candidate shape and a compile-time key assertion, so the
|
|
33
33
|
* implementation and this contract cannot drift silently.
|
|
34
34
|
*/
|
|
35
|
-
import type { AssistantMessage, Provider
|
|
35
|
+
import type { AssistantMessage, Provider } from "@earendil-works/pi-ai";
|
|
36
|
+
import type { TSchema } from "typebox";
|
|
36
37
|
import type { ConversationDocToken, ConversationId, Cursor, EntryId, EntryQuery, EntryRecord, Extension, InputSubmissionDraft, JsonObject, ModelRef, Page, SubmissionId } from "@earendil-works/pi-durable";
|
|
37
38
|
import type { Access, Sharing } from "@sigrun/shared/access";
|
|
38
39
|
import type { DecisionAnswers, DecisionLimits, DecisionRequest, DecisionResult } from "@sigrun/shared/decisions";
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
* bind, the seam carries the events to in-process listeners only — the
|
|
32
32
|
* default subscriber set is empty, so nothing is lost by not wiring it.
|
|
33
33
|
*/
|
|
34
|
-
import { Type } from "
|
|
34
|
+
import { Type } from "typebox";
|
|
35
35
|
import { defineExtension, defineTool, } from "@earendil-works/pi-durable";
|
|
36
36
|
import { applyOps, DataOpError, toText } from "@sigrun/shared/blocks";
|
|
37
37
|
import { validateData, validateTree } from "../../blocks/validate.js";
|
package/dist/harness/boot.js
CHANGED
|
@@ -14,11 +14,11 @@ import { openNodeSqliteStorage } from "@earendil-works/pi-durable/storage/sqlite
|
|
|
14
14
|
import { DECISION_TIMEOUT_ENV, effectiveMethods } from "../config.js";
|
|
15
15
|
import { requiresProxyVouch } from "../server/auth/proxy-only.js";
|
|
16
16
|
import { wireActivity } from "./activity.js";
|
|
17
|
-
import { snapshotBeforeMigration } from "./migration-snapshot.js";
|
|
17
|
+
import { snapshotBeforeDurableUpgrade, snapshotBeforeMigration, writeDurableVersionMarker, } from "./migration-snapshot.js";
|
|
18
18
|
import { createScheduleTask } from "../jobs/task.js";
|
|
19
19
|
import { createJobsService, jobWireOf } from "../jobs/index.js";
|
|
20
20
|
import { reconcileJobs } from "../jobs/reconcile.js";
|
|
21
|
-
import {
|
|
21
|
+
import { markRestartLimitedJob } from "../jobs/restart-limit.js";
|
|
22
22
|
import { createDurableReports } from "./reports.js";
|
|
23
23
|
import { registerJobObservers } from "../jobs/observe.js";
|
|
24
24
|
import { JobDoc, JobRunDoc, listRevisions } from "../jobs/docs.js";
|
|
@@ -372,12 +372,33 @@ export async function openHarness(config, options = {}) {
|
|
|
372
372
|
// openNodeSqliteStorage applies its one-way durable migration. The
|
|
373
373
|
// injected clock keeps snapshots deterministic in tests; a needed
|
|
374
374
|
// snapshot failure aborts boot here, before any migration write.
|
|
375
|
-
await snapshotBeforeMigration(config.storagePath, {
|
|
375
|
+
const schemaSnapshot = await snapshotBeforeMigration(config.storagePath, {
|
|
376
376
|
env,
|
|
377
377
|
now: options.now ?? (() => new Date()),
|
|
378
378
|
log: (line) => console.log(line),
|
|
379
379
|
});
|
|
380
|
-
|
|
380
|
+
// D4 (1.5.0 upgrade): a store last written by an older pi-durable
|
|
381
|
+
// minor gets its own snapshot — skipped when the schema guard above
|
|
382
|
+
// already wrote one this boot. The version marker is written only
|
|
383
|
+
// AFTER a successful open, so it never claims a version the store
|
|
384
|
+
// has not actually been opened by.
|
|
385
|
+
if (schemaSnapshot === undefined) {
|
|
386
|
+
await snapshotBeforeDurableUpgrade(config.storagePath, {
|
|
387
|
+
env,
|
|
388
|
+
now: options.now ?? (() => new Date()),
|
|
389
|
+
log: (line) => console.log(line),
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
const opened = await openNodeSqliteStorage(config.storagePath);
|
|
393
|
+
try {
|
|
394
|
+
await writeDurableVersionMarker(config.storagePath);
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
// A failed stamp must not leak the open storage handle.
|
|
398
|
+
await opened.close(BACKGROUND_CONTEXT).catch(() => { });
|
|
399
|
+
throw error;
|
|
400
|
+
}
|
|
401
|
+
return opened;
|
|
381
402
|
})();
|
|
382
403
|
const registry = createRegistry();
|
|
383
404
|
// Late-filled by the loader: factories publish through the same registry
|
|
@@ -1192,7 +1213,7 @@ export async function openHarness(config, options = {}) {
|
|
|
1192
1213
|
// the jobs tests' time-injection lever).
|
|
1193
1214
|
const clockNow = options.now ?? (() => new Date());
|
|
1194
1215
|
// D5: reports may arrive inside Harness.open. The collector logs/counts
|
|
1195
|
-
// synchronously
|
|
1216
|
+
// synchronously (no durable follow-up work from a report survives 1.5.0).
|
|
1196
1217
|
const reports = createDurableReports({
|
|
1197
1218
|
log: (line) => console.error(line),
|
|
1198
1219
|
onReport: options.onReport,
|
|
@@ -1256,6 +1277,11 @@ export async function openHarness(config, options = {}) {
|
|
|
1256
1277
|
entry.name !== CURATION_GATE_EXTENSION);
|
|
1257
1278
|
},
|
|
1258
1279
|
toolExecution: callerSettings?.toolExecution,
|
|
1280
|
+
// pi-durable 1.5.0: restart-limited tasks END instead of parking
|
|
1281
|
+
// (no file backing — caller seam and default only, like
|
|
1282
|
+
// toolExecution). The battery pins it to make crash outcomes
|
|
1283
|
+
// deterministic.
|
|
1284
|
+
restartLimit: callerSettings?.restartLimit,
|
|
1259
1285
|
compaction: {
|
|
1260
1286
|
...config.compaction,
|
|
1261
1287
|
...(config.compaction.modelOverrides === undefined &&
|
|
@@ -1282,9 +1308,8 @@ export async function openHarness(config, options = {}) {
|
|
|
1282
1308
|
jobRuntimeDeps.harness = harness;
|
|
1283
1309
|
jobRuntimeDeps.storage = storage;
|
|
1284
1310
|
jobRuntimeDeps.interactiveTools = new Set(loadedInteractiveTools);
|
|
1285
|
-
//
|
|
1286
|
-
//
|
|
1287
|
-
await applyParkedJobs(harness, reports.takeParked(), BACKGROUND_CONTEXT);
|
|
1311
|
+
// pi-durable 1.5.0 removed task_parked/retryTask (the parked-job drain
|
|
1312
|
+
// existed for that pair); Task 4 wires the replacement recovery surface.
|
|
1288
1313
|
// The memory-seam event bus (Tasks 2-4): subscribe BEFORE `resume()` so
|
|
1289
1314
|
// recovery settles (faulted/orphaned submissions the scheduler drains)
|
|
1290
1315
|
// are observed like any other commit. Extension handlers register under
|
|
@@ -1437,14 +1462,28 @@ export async function openHarness(config, options = {}) {
|
|
|
1437
1462
|
// open, and a no-op once the queue is found empty. Events enqueued
|
|
1438
1463
|
// while a drain is already pumping are picked up by that same loop.
|
|
1439
1464
|
let drainTimer;
|
|
1440
|
-
harness.
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1465
|
+
harness.observeCommits({
|
|
1466
|
+
advance: () => {
|
|
1467
|
+
try {
|
|
1468
|
+
if (drainTimer !== undefined)
|
|
1469
|
+
return;
|
|
1470
|
+
drainTimer = setTimeout(() => {
|
|
1471
|
+
drainTimer = undefined;
|
|
1472
|
+
void eventBus.drain(hookDeadlineMs, BACKGROUND_CONTEXT);
|
|
1473
|
+
});
|
|
1474
|
+
drainTimer.unref?.();
|
|
1475
|
+
}
|
|
1476
|
+
catch (error) {
|
|
1477
|
+
console.error("boot: commit observer failed", error);
|
|
1478
|
+
}
|
|
1479
|
+
},
|
|
1480
|
+
end: (end) => {
|
|
1481
|
+
if (end.kind === "ended")
|
|
1482
|
+
return;
|
|
1483
|
+
if (end.kind === "failed" && end.error instanceof Error && end.error.message === "session closed")
|
|
1484
|
+
return;
|
|
1485
|
+
console.error("boot: commit observation failed", end.kind === "failed" ? end.error : end);
|
|
1486
|
+
},
|
|
1448
1487
|
});
|
|
1449
1488
|
// A failed Session closes itself (the upstream contract): the bus's
|
|
1450
1489
|
// sweep + subscription must not outlive it. shutdownSequence (the
|
|
@@ -1528,19 +1567,35 @@ export async function openHarness(config, options = {}) {
|
|
|
1528
1567
|
// the counter feeds `sigrun_job_runs_total{outcome}`). A creation
|
|
1529
1568
|
// write of an already-settled run (the overlap skip, the catch-up
|
|
1530
1569
|
// skip) counts once at creation; a `running` run counts at its settle;
|
|
1531
|
-
// prune retirements (
|
|
1570
|
+
// prune retirements (tagged `kind === "retire"`) never count.
|
|
1532
1571
|
const jobRunTotals = new Map();
|
|
1533
|
-
harness.
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
|
|
1543
|
-
|
|
1572
|
+
harness.observeCommits({
|
|
1573
|
+
advance: (publication) => {
|
|
1574
|
+
try {
|
|
1575
|
+
for (const change of publication.changes) {
|
|
1576
|
+
if (change.type !== "document")
|
|
1577
|
+
continue;
|
|
1578
|
+
if (change.kind !== "change")
|
|
1579
|
+
continue;
|
|
1580
|
+
if (change.record.kind !== JobRunDoc.definition.kind)
|
|
1581
|
+
continue;
|
|
1582
|
+
const outcome = change.value?.outcome;
|
|
1583
|
+
if (typeof outcome !== "string" || outcome === "running")
|
|
1584
|
+
continue;
|
|
1585
|
+
jobRunTotals.set(outcome, (jobRunTotals.get(outcome) ?? 0) + 1);
|
|
1586
|
+
}
|
|
1587
|
+
}
|
|
1588
|
+
catch (error) {
|
|
1589
|
+
console.error("boot: commit observer failed", error);
|
|
1590
|
+
}
|
|
1591
|
+
},
|
|
1592
|
+
end: (end) => {
|
|
1593
|
+
if (end.kind === "ended")
|
|
1594
|
+
return;
|
|
1595
|
+
if (end.kind === "failed" && end.error instanceof Error && end.error.message === "session closed")
|
|
1596
|
+
return;
|
|
1597
|
+
console.error("boot: commit observation failed", end.kind === "failed" ? end.error : end);
|
|
1598
|
+
},
|
|
1544
1599
|
});
|
|
1545
1600
|
jobs = createJobsService({
|
|
1546
1601
|
harness,
|
|
@@ -1585,6 +1640,17 @@ export async function openHarness(config, options = {}) {
|
|
|
1585
1640
|
console.warn(`jobs: declared job "${spec.name}" names owner "${spec.owner}" who is not in the user directory — it runs ownerless until its source is edited`);
|
|
1586
1641
|
}
|
|
1587
1642
|
}
|
|
1643
|
+
// D2 restart-limit recovery: reports that arrived DURING
|
|
1644
|
+
// Harness.open sat queued in the collector. Register the live
|
|
1645
|
+
// listener now (drains the backlog via setImmediate, then every
|
|
1646
|
+
// later restart-limited report flows straight to it); the listener
|
|
1647
|
+
// owns all Harness reads/commits — the collector never calls the
|
|
1648
|
+
// Harness from inside onReport.
|
|
1649
|
+
reports.onRestartLimited((task) => {
|
|
1650
|
+
void markRestartLimitedJob(harness, task, BACKGROUND_CONTEXT).catch((error) => {
|
|
1651
|
+
console.error("jobs: restart-limited listener failed:", error);
|
|
1652
|
+
});
|
|
1653
|
+
});
|
|
1588
1654
|
harness.resume();
|
|
1589
1655
|
return {
|
|
1590
1656
|
harness,
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* `structuredOutput` under the declared schema (pi-durable rejects
|
|
19
19
|
* structuredOutput without one).
|
|
20
20
|
*/
|
|
21
|
-
import { Type } from "
|
|
21
|
+
import { Type } from "typebox";
|
|
22
22
|
import { defineExtension, defineTool } from "@earendil-works/pi-durable";
|
|
23
23
|
import { validateDecisionRequest } from "./decisions.js";
|
|
24
24
|
/** The core extension's reserved slug (boot.ts keeps the core slugs). */
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* `ok:false` with one of the spec's codes. Error text never carries the
|
|
11
11
|
* credential (scrubbed) nor the submitted request.
|
|
12
12
|
*/
|
|
13
|
-
import { Type } from "
|
|
13
|
+
import { Type } from "typebox";
|
|
14
14
|
import { Compile } from "typebox/compile";
|
|
15
15
|
import { DECISION_MAX_BYTES, DECISION_MAX_QUESTIONS, } from "@sigrun/shared/decisions";
|
|
16
16
|
// A strict-JSON value schema. `Type.Unknown()` leaves a node unchecked;
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
* side lane + idle detection + shutdown sequencing. See the memory-seam
|
|
4
4
|
* spec §"Design".
|
|
5
5
|
*
|
|
6
|
-
* The bus subscribes to pi-durable's `Session.
|
|
6
|
+
* The bus subscribes to pi-durable's `Session.observeCommits` (sigrun's
|
|
7
7
|
* harness IS a Session) and reduces each `CommitPublication` into the
|
|
8
8
|
* memory seam's host events — settled-run groups and conversation
|
|
9
9
|
* lifecycle — without touching run logic. The upstream listener contract
|
|
@@ -13,10 +13,10 @@
|
|
|
13
13
|
* deadline-bounded (`Promise.race` against `hookDeadlineMs`), rejections
|
|
14
14
|
* are caught + logged, and overflow drops the OLDEST queued event.
|
|
15
15
|
*
|
|
16
|
-
* Detection rules (verified against pi-durable 1.
|
|
16
|
+
* Detection rules (verified against pi-durable 1.5.0's publication shapes
|
|
17
17
|
* — table changes are the raw `StorageWrite` records; document changes are
|
|
18
18
|
* `DocumentCommitChange`s whose `record.createdAt === publication.seq`
|
|
19
|
-
* marks creation, `
|
|
19
|
+
* marks creation, the tagged `kind === "retire"` marks retirement):
|
|
20
20
|
*
|
|
21
21
|
* - **Settle**: a `submission` table write with `value.type === "input"`
|
|
22
22
|
* and a terminal status (`done`/`unanswered`). Write-type submissions
|
|
@@ -171,9 +171,19 @@ export class EventBus {
|
|
|
171
171
|
if (options.onHostEvent !== undefined) {
|
|
172
172
|
this.onHostEvent(options.onHostEvent);
|
|
173
173
|
}
|
|
174
|
-
|
|
175
|
-
|
|
174
|
+
const observation = harness.observeCommits({
|
|
175
|
+
advance: (publication) => {
|
|
176
|
+
this.observe(publication);
|
|
177
|
+
},
|
|
178
|
+
end: (end) => {
|
|
179
|
+
if (end.kind === "ended")
|
|
180
|
+
return;
|
|
181
|
+
if (end.kind === "failed" && end.error instanceof Error && end.error.message === "session closed")
|
|
182
|
+
return;
|
|
183
|
+
console.error("event-bus: commit observation failed", end.kind === "failed" ? end.error : end);
|
|
184
|
+
},
|
|
176
185
|
});
|
|
186
|
+
this.#unsubscribe = () => observation.detach();
|
|
177
187
|
// The idle sweep is host-owned machinery (never an extension factory
|
|
178
188
|
// concern — the twill lesson) and binds to THIS bus's lifetime: close
|
|
179
189
|
// and shutdownSequence both clear it. Unref'd so a booted-but-idle
|
|
@@ -707,7 +717,7 @@ export class EventBus {
|
|
|
707
717
|
continue;
|
|
708
718
|
if (change.conversationId === undefined)
|
|
709
719
|
continue;
|
|
710
|
-
this.#internal.set(change.conversationId, change.value
|
|
720
|
+
this.#internal.set(change.conversationId, change.kind === "change" && change.value.internal === true);
|
|
711
721
|
}
|
|
712
722
|
for (const change of publication.changes) {
|
|
713
723
|
if (change.type !== "conversation")
|
|
@@ -758,9 +768,9 @@ export class EventBus {
|
|
|
758
768
|
// their host events here (ordinary document changes only — a
|
|
759
769
|
// fork's document.copy carries no status transition).
|
|
760
770
|
if (change.type === "document" &&
|
|
771
|
+
change.kind === "change" &&
|
|
761
772
|
kind === InteractionDoc.definition.kind &&
|
|
762
|
-
change.conversationId !== undefined
|
|
763
|
-
change.value !== null) {
|
|
773
|
+
change.conversationId !== undefined) {
|
|
764
774
|
const record = change.value;
|
|
765
775
|
if (typeof record.callId === "string" &&
|
|
766
776
|
typeof record.kind === "string" &&
|
|
@@ -874,7 +884,7 @@ export class EventBus {
|
|
|
874
884
|
this.#metaBasis.set(conversationId, childBasis);
|
|
875
885
|
continue;
|
|
876
886
|
}
|
|
877
|
-
if (change.
|
|
887
|
+
if (change.type === "document" && change.kind === "retire") {
|
|
878
888
|
this.#archivedBasis.delete(conversationId);
|
|
879
889
|
this.#metaBasis.delete(conversationId);
|
|
880
890
|
continue;
|
package/dist/harness/loader.js
CHANGED
|
@@ -1262,8 +1262,9 @@ const DECLARED_PEER_MODULES = [
|
|
|
1262
1262
|
"@earendil-works/pi-ai",
|
|
1263
1263
|
];
|
|
1264
1264
|
/**
|
|
1265
|
-
* The declared-peer guard (pi-durable 1.4.0 D7
|
|
1266
|
-
*
|
|
1265
|
+
* The declared-peer guard (pi-durable 1.4.0 D7; 1.5.0 D3 moved it
|
|
1266
|
+
* PRE-import): reject an Earendil peer range that excludes the running
|
|
1267
|
+
* host before the extension's module code executes.
|
|
1267
1268
|
* Non-semver specifiers (file:, link:, git:, URLs, workspace:) and wildcards
|
|
1268
1269
|
* pass; a missing or unreadable manifest also passes because the guard never
|
|
1269
1270
|
* quarantines what it cannot read.
|
|
@@ -1843,6 +1844,17 @@ export async function loadExtensions(specifiers, registry, options = {}) {
|
|
|
1843
1844
|
if (/\.ts$/.test(loaded)) {
|
|
1844
1845
|
throw new Error(`the resolved entry is a TypeScript source (${loaded}) — build the package and ship its built entry (dist/index.js); the built server loads JavaScript`);
|
|
1845
1846
|
}
|
|
1847
|
+
// Declared-peer preflight (1.5.0 upgrade, spec D3): the peer-range
|
|
1848
|
+
// guard runs BEFORE the import so a previous-minor extension's
|
|
1849
|
+
// module-level code (which may destructure host shapes the new
|
|
1850
|
+
// version removed) never executes. A throw here rides the same
|
|
1851
|
+
// per-extension try as every other quarantine.
|
|
1852
|
+
const moduleDir = extensionModuleDir(loaded);
|
|
1853
|
+
if (moduleDir !== undefined) {
|
|
1854
|
+
const peerReason = checkDeclaredPeers(moduleDir);
|
|
1855
|
+
if (peerReason !== undefined)
|
|
1856
|
+
throw new Error(peerReason);
|
|
1857
|
+
}
|
|
1846
1858
|
const module = (await import(loaded));
|
|
1847
1859
|
// Host-module version guard (spec §6.4 R6): the module tree the
|
|
1848
1860
|
// extension's bare imports resolve from must agree with the copy
|
|
@@ -1851,12 +1863,8 @@ export async function loadExtensions(specifiers, registry, options = {}) {
|
|
|
1851
1863
|
// no trace (no seam host, no bindings, no registry entry). A throw
|
|
1852
1864
|
// here rides the existing per-extension try (quarantine, boot
|
|
1853
1865
|
// continues) exactly like the reserved-section check below.
|
|
1854
|
-
const moduleDir = extensionModuleDir(loaded);
|
|
1855
1866
|
if (moduleDir !== undefined) {
|
|
1856
1867
|
checkHostModules(moduleDir);
|
|
1857
|
-
const peerReason = checkDeclaredPeers(moduleDir);
|
|
1858
|
-
if (peerReason !== undefined)
|
|
1859
|
-
throw new Error(peerReason);
|
|
1860
1868
|
}
|
|
1861
1869
|
// Two-phase host: the factory receives the host at CALL time, but
|
|
1862
1870
|
// the extension name and kinds it declares are only known from its
|