@sigrun/sigrun 0.7.0 → 0.9.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 +78 -59
- package/README.md +32 -0
- package/dist/config.js +47 -0
- package/dist/curation/host.js +125 -21
- package/dist/curation/runs.js +15 -4
- package/dist/extension.d.ts +331 -4
- package/dist/harness/boot.js +114 -3
- package/dist/harness/complete.js +127 -0
- package/dist/harness/decision-tool.js +93 -0
- package/dist/harness/decisions.js +477 -0
- package/dist/harness/event-bus.js +12 -0
- package/dist/harness/interactions.js +81 -0
- package/dist/harness/interactive-tools.js +97 -0
- package/dist/harness/loader.js +285 -1
- package/dist/harness/packages.js +7 -0
- package/dist/harness/published-hooks.js +208 -0
- package/dist/harness/seam-host.js +149 -1
- package/dist/harness/settings.js +41 -0
- package/dist/history/git.js +6 -2
- package/dist/jobs/fire.js +10 -0
- package/dist/jobs/observe.js +27 -2
- package/dist/notices/events.js +8 -0
- package/dist/notices/service.js +297 -0
- package/dist/notices/store.js +331 -0
- package/dist/notices/validate.js +311 -0
- package/dist/server/conversation-ops.js +50 -0
- package/dist/server/core-views/skills-tree.js +3 -12
- package/dist/server/core-views/skills.js +11 -23
- package/dist/server/host-bridge.js +62 -1
- package/dist/server/index.js +26 -0
- package/dist/server/routes/events.js +20 -0
- package/dist/server/routes/extensions.js +5 -0
- package/dist/server/routes/interactions.js +48 -1
- package/dist/server/routes/meta.js +17 -0
- package/dist/server/routes/notices.js +94 -0
- package/dist/server/routes/providers.js +65 -8
- package/dist/server/routes/submissions.js +3 -0
- package/dist/server/routes/users.js +4 -0
- package/dist/server/surfaces/dedup.js +6 -0
- package/dist/server/visibility.js +30 -0
- package/dist/settings/registry.js +18 -0
- package/node_modules/@sigrun/shared/CHANGELOG.md +12 -0
- package/node_modules/@sigrun/shared/dist/decisions.d.ts +95 -0
- package/node_modules/@sigrun/shared/dist/decisions.js +16 -0
- package/node_modules/@sigrun/shared/dist/interactions.d.ts +14 -0
- package/node_modules/@sigrun/shared/dist/notices.d.ts +112 -0
- package/node_modules/@sigrun/shared/dist/notices.js +49 -0
- package/node_modules/@sigrun/shared/dist/settings.d.ts +32 -0
- package/node_modules/@sigrun/shared/dist/views.d.ts +63 -0
- package/node_modules/@sigrun/shared/package.json +9 -1
- package/package.json +7 -7
- package/webui/dist/assets/index-D5KowS7k.css +1 -0
- package/webui/dist/assets/index-DGIrLC6F.js +55 -0
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-BI5k-Oci.js +0 -55
- package/webui/dist/assets/index-EGluyRXk.css +0 -1
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,84 @@ 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.9.0] - 2026-11-05
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Decision providers: a factory result's `decisionProviders` contributes typed-question models
|
|
13
|
+
(noul / choice / score) keyed in `auth.json` with a `credentialEnv` fallback; credentials write
|
|
14
|
+
through `PUT/DELETE /v1/providers/:id/credential` (a decision-only id re-reads readiness
|
|
15
|
+
instead of refreshing). `host.decisions.evaluate` (never throws; validated against the wire
|
|
16
|
+
schema and the model's limits; a 15 s deadline capped by `server.decisions.timeoutMs` /
|
|
17
|
+
`SIGRUN_DECISION_TIMEOUT_MS`), the `decisionModel` setting (live), decision entries with
|
|
18
|
+
`kind` + model ids on `GET /v1/providers`, a `decisions` projection on `GET /v1/meta`, and the
|
|
19
|
+
opt-in `decision_evaluate` tool (`decisions.tool`, default false). Docs:
|
|
20
|
+
"Contributing decision providers" in docs/extending.md.
|
|
21
|
+
- `host.jobs.activeRun(conversationId)`: the job whose fire is CURRENTLY running in a
|
|
22
|
+
conversation (both fire shapes), with its effective owner — the unattended-run marker
|
|
23
|
+
extensions need to route destructive calls. Fixed-conversation fires settle their runs via the
|
|
24
|
+
submission's `job:` requestId (no meta stamp; a fire into an existing conversation does not
|
|
25
|
+
mark its later runs job-driven).
|
|
26
|
+
- `host.models.complete(input)`: one-shot chat completions for extensions (no conversation);
|
|
27
|
+
explicit model → `namingModel` → default, `role: "utility"`, a 60 s default timeout, both the
|
|
28
|
+
timer and the caller's signal bounding the wait.
|
|
29
|
+
- Declinable interaction records: `declinable: true` marks a pending record declinable;
|
|
30
|
+
`POST /v1/conversations/:id/interactions/:callId/decline` (write access) settles it
|
|
31
|
+
`skipped` with `declinedVia: "button"` (404 `no_such_interaction` / 404 `not_pending` /
|
|
32
|
+
409 `not_declinable`); `host.declineInteraction(conversationId, callId, context, {user})`
|
|
33
|
+
is the extension-side peer; `interactionSettled` payloads carry `declinedVia`. Declining
|
|
34
|
+
sweeps the record's unread secret delivery inside the same commit.
|
|
35
|
+
- Interactive tools: a factory result's `interactiveTools` names the registered tools that
|
|
36
|
+
need a human present. Core hides them in job-run conversations (`conversation: "new"`
|
|
37
|
+
fires, `host.conversations.create({job})`) and curator runs (digest agents exclude them;
|
|
38
|
+
every run's `allow` list loses them with one warning; fork reviews keep them listed for
|
|
39
|
+
prompt-cache parity and the gate blocks a call instead). `host.jobs.isJobRun` tells an
|
|
40
|
+
interactive tool's execute whether the current run is job-driven (a job firing into an
|
|
41
|
+
existing conversation is not covered by the creation-time filter — refuse there).
|
|
42
|
+
|
|
43
|
+
## [0.8.0] - 2026-11-04
|
|
44
|
+
|
|
45
|
+
Core notices: a per-user notice system for extensions. An extension posts a short notice
|
|
46
|
+
(title, body, tone, up to 3 actions) to ONE directory user; only that user ever sees it,
|
|
47
|
+
by route or by event; it clears on dismiss, the user's own next prompt in the conversation,
|
|
48
|
+
acting on it, clear-all, eviction past the caps, or deleting the user (the file is removed
|
|
49
|
+
and a re-created username starts empty). Notices are transient by design — nothing expires
|
|
50
|
+
on a timer.
|
|
51
|
+
|
|
52
|
+
### Core
|
|
53
|
+
|
|
54
|
+
- `host.notices` (`post`/`clear`/`list`, extension-scoped keys, capped 10 per extension and
|
|
55
|
+
50 per user), the `noticeActions` factory field (handlers run with the clicker's
|
|
56
|
+
principal, idempotent, 10s timeout, dedup + rate limit reusing the surface-action
|
|
57
|
+
primitives), `/v1/notices` routes (404-never-403 for foreign ids; `GET` prunes notices
|
|
58
|
+
whose conversation share was revoked), and the user-only `notice.changed` event.
|
|
59
|
+
- Storage: one stamped, atomically-written `notices.json` per user under
|
|
60
|
+
`<dataDir>/users/<user>/` (backup-archived by the existing `data` section); invalid
|
|
61
|
+
entries are dropped on load; the agent sandbox cannot write it.
|
|
62
|
+
- Ambient `navigate`/`open` buttons work in the web UI (the no-op dispatcher is gone;
|
|
63
|
+
`open` uses `noopener noreferrer`).
|
|
64
|
+
|
|
65
|
+
### Web UI
|
|
66
|
+
|
|
67
|
+
- Bell with a count (rail on desktop, every tab's header on phones), a notices pane
|
|
68
|
+
(docked right panel on Chats, overlay on other tabs, full-height sheet on phones),
|
|
69
|
+
inline notices above the composer (max 3, then "N more"), and a 6s peek (pause on
|
|
70
|
+
hover, arrivals merge, none for an on-screen conversation's notice or while the tab
|
|
71
|
+
is hidden).
|
|
72
|
+
|
|
73
|
+
### TUI
|
|
74
|
+
|
|
75
|
+
- Inline notice line above the editor, `notices:N` in the status bar, and an overlay
|
|
76
|
+
with dismiss, Clear-all-free navigation, and the buttons (`open` prints the URL; no
|
|
77
|
+
peek — the count is the announcement).
|
|
78
|
+
|
|
79
|
+
### Companions
|
|
80
|
+
|
|
81
|
+
- ext-curator 0.3.0 (published first): the "Learned: …" review notice moved onto core
|
|
82
|
+
notices with Undo as an extension action on the notice; ext-gateway 0.1.16 widened its
|
|
83
|
+
`@sigrun/shared` peer to the 0.8.0 line (0.1.15 is a dead scar — tagged before its fake
|
|
84
|
+
host carried `SigrunHost.notices`, CI failed against core main).
|
|
85
|
+
|
|
8
86
|
## [0.7.0] - 2026-11-04
|
|
9
87
|
|
|
10
88
|
The polish pass: three waves of behaviour fixes, the full deferred-minors sweep, and a
|
|
@@ -48,65 +126,6 @@ fresh-eyes clean-room install verification.
|
|
|
48
126
|
|
|
49
127
|
- Release-pin bump to 0.6.1 (root, shared, tui, sandbox platform packages, chart appVersion, compose defaults).
|
|
50
128
|
|
|
51
|
-
## [Unreleased]
|
|
52
|
-
|
|
53
|
-
### Added
|
|
54
|
-
- **Views are tabs (webui).** The webui shell registers views as rail
|
|
55
|
-
tabs with remembered routes; the chat view keeps the conversation
|
|
56
|
-
list and right panel to itself. Below the tablet breakpoint a phone
|
|
57
|
-
bottom tab bar (with a More sheet) replaces the rail.
|
|
58
|
-
- **`page.selection`** — a server-declared, presentation-only
|
|
59
|
-
list-first drill-in for compact widths (`{selected, back}` on a
|
|
60
|
-
`page`; webui and TUI render it, `toText` ignores it).
|
|
61
|
-
- **Core views.** Core attaches its own view surfaces under the
|
|
62
|
-
reserved `core` slug and lists them as `coreViews` (one
|
|
63
|
-
`CoreViewInfo {id, title, icon, refreshMs?}` per view) on
|
|
64
|
-
`/v1/extensions` and `/v1/meta`; they are never `extensions[]` rows,
|
|
65
|
-
so the health dot is unaffected. `job.changed` now bumps every
|
|
66
|
-
rendered core/jobs instance.
|
|
67
|
-
- **The Jobs tab** (`core/jobs`): the jobs surface as a core blocks
|
|
68
|
-
view — sidebar of the caller-visible jobs, drill-in with summary,
|
|
69
|
-
paged runs and revisions (a selected revision diffs its spec JSON
|
|
70
|
-
against the previous one), Run now / Reset state for write access,
|
|
71
|
-
and per-row navigation into a run's conversation. The Jobs panel is
|
|
72
|
-
gone.
|
|
73
|
-
- **The Skills tab** (`core/skills`): every skill root the deployment
|
|
74
|
-
serves — Workspace / Learned / Config dir (the deployment-wide
|
|
75
|
-
`<config-dir>/skills/`, read-only) / one group per installed package
|
|
76
|
-
(`· <pkg>`, with a per-row package badge) / Deleted — with shadowing visible, owned-skill CRUD, per-skill git
|
|
77
|
-
history (25 entries per page, per-file diffs, restore), deleted-skill
|
|
78
|
-
restore, and per-user overlays as the only write to the read-only
|
|
79
|
-
shared roots. Some surface actions return `navigate`; the webui
|
|
80
|
-
applies that navigation, the TUI does not yet (a known channel gap).
|
|
81
|
-
- **Every-root skill discovery** (`src/skills/roots.ts`): workspace,
|
|
82
|
-
learned, user dir, and every installed package's skills listed in one
|
|
83
|
-
pass, with precedence/shadowing resolution and symlink-safe reads.
|
|
84
|
-
- **GitHistory** (`src/history/git.ts`) — isolated local git for
|
|
85
|
-
sigrun-owned history stores: execFile with explicit `--git-dir` /
|
|
86
|
-
`--work-tree`, a hermetic git env, per-repo locking, and fail-soft
|
|
87
|
-
writes (a broken repo is quarantined aside and re-initialized;
|
|
88
|
-
mutations never fail because history did).
|
|
89
|
-
- **Curation stores on git** (Task 9): each user's learned skills,
|
|
90
|
-
overlays, and curator state live in a per-user git history repo; the
|
|
91
|
-
change feed's `actor` widens to `"outside"` and its `id`/`before`/
|
|
92
|
-
`after` are commit SHAs. A once-per-process migration imports the
|
|
93
|
-
pre-git store (the legacy files are left read-only, outside the
|
|
94
|
-
repo's pathspec).
|
|
95
|
-
- **Workspace skills history** (Task 10): the workspace `skills/`
|
|
96
|
-
tree gets its own per-user repo (git dir outside the work tree),
|
|
97
|
-
caught up on settled runs and tab opens; ownerless conversations
|
|
98
|
-
skip it.
|
|
99
|
-
|
|
100
|
-
### Changed
|
|
101
|
-
- **The curation seal**: workspaces and grants that sit inside or
|
|
102
|
-
contain a user's `curation/` tree are rejected; boot revalidates
|
|
103
|
-
stored records and names each failing one loudly. A user whose
|
|
104
|
-
workspace was the bare user area loses agent tools until an admin
|
|
105
|
-
re-points it: `PATCH /v1/users/<u> {"workspace": "<userArea>/workspace"}`.
|
|
106
|
-
History tamper-resistance holds under active sandbox isolation; a
|
|
107
|
-
single-user install without isolation runs the agent unsandboxed
|
|
108
|
-
(the existing trust model).
|
|
109
|
-
|
|
110
129
|
## [0.6.0] - 2026-11-02
|
|
111
130
|
|
|
112
131
|
### Added
|
package/README.md
CHANGED
|
@@ -1299,6 +1299,34 @@ curl -X PATCH localhost:3000/v1/users/<u> \
|
|
|
1299
1299
|
-d '{"workspace": "<userArea>/workspace"}'
|
|
1300
1300
|
```
|
|
1301
1301
|
|
|
1302
|
+
## Notices
|
|
1303
|
+
|
|
1304
|
+
A **notice** is a short message from an extension aimed at you alone:
|
|
1305
|
+
a title, a line or two of text, and up to three buttons (open a view,
|
|
1306
|
+
open a link, or run an action such as Undo) — the curator's
|
|
1307
|
+
"Learned: …" line is the first one you will see. Notices are per-user,
|
|
1308
|
+
never shared, and never copied into a fork. The **bell** —
|
|
1309
|
+
at the top of the rail on desktop, in the tab header on phones, and as
|
|
1310
|
+
a count in the TUI's status bar — opens the **notices pane**: a docked
|
|
1311
|
+
panel on the Chats tab and a slide-over on every other tab, newest
|
|
1312
|
+
first, with ✕ per notice and a Clear all. A conversation's own notices
|
|
1313
|
+
appear inline above the composer, and a new one **peeks** by the bell
|
|
1314
|
+
for six seconds (not when its conversation is already on screen),
|
|
1315
|
+
staying put while you hover it — several arriving
|
|
1316
|
+
together merge into "N new notices". In the TUI the newest notice for
|
|
1317
|
+
the open conversation (else the newest app-wide one) is a line above
|
|
1318
|
+
the editor (`alt+d` standard, `gx` vim, `M-d` emacs dismisses it), the
|
|
1319
|
+
status bar carries the count, and the overlay opens with `ctrl+g`
|
|
1320
|
+
(standard), `gN` (vim), or `M-N` (emacs).
|
|
1321
|
+
|
|
1322
|
+
What clears a notice: its ✕, **your next prompt in that conversation**,
|
|
1323
|
+
a button that navigates or opens a link, an extension button that
|
|
1324
|
+
succeeds, Clear all, the extension that posted it, or — when the pile
|
|
1325
|
+
outgrows 10 notices from one extension or 50 in total — eviction of the
|
|
1326
|
+
oldest. Nothing expires on a timer, and another user's prompt never
|
|
1327
|
+
clears yours. A revoked share drops that conversation's notices on
|
|
1328
|
+
your next read; an archived conversation keeps its own.
|
|
1329
|
+
|
|
1302
1330
|
## Jobs
|
|
1303
1331
|
|
|
1304
1332
|
A job is a schedule plus an action, durable by construction — one
|
|
@@ -1440,6 +1468,10 @@ curl -X POST -H "Authorization: Bearer $TOKEN" \
|
|
|
1440
1468
|
(SSE streaming views included). A submission body is
|
|
1441
1469
|
`{"input": "..."}` (or a `{"input": [parts]}` array of
|
|
1442
1470
|
text/image/file content parts).
|
|
1471
|
+
- `GET /v1/notices`, `POST /v1/notices/{id}/dismiss`,
|
|
1472
|
+
`POST /v1/notices/dismiss-all`,
|
|
1473
|
+
`POST /v1/notices/{id}/actions/{actionId}` — the caller's own notices
|
|
1474
|
+
(see [Notices](#notices)); a foreign id answers 404.
|
|
1443
1475
|
- `GET /v1/providers` — per-provider readiness, statuses only (never
|
|
1444
1476
|
credentials); `GET /v1/auth/check` is the deprecated alias.
|
|
1445
1477
|
- `GET /v1/onboarding`, `POST /v1/onboarding/claim` — the unclaimed
|
package/dist/config.js
CHANGED
|
@@ -20,6 +20,28 @@ import { deriveUsername, isUsernameKey } from "@sigrun/shared/usernames";
|
|
|
20
20
|
export const DEFAULT_MODEL_ENV = "SIGRUN_DEFAULT_MODEL";
|
|
21
21
|
/** Deprecated alias for {@link DEFAULT_MODEL_ENV} (warns once per boot). */
|
|
22
22
|
export const DEPRECATED_MODEL_ENV = "SIGRUN_MODEL";
|
|
23
|
+
/**
|
|
24
|
+
* The published-hook call deadline (knowledge C7, Task 2):
|
|
25
|
+
* `SIGRUN_PUBLISHED_HOOK_DEADLINE`, over the file's
|
|
26
|
+
* `server.publishedHookDeadlineMs`, default 30000 ms. Its own knob, NOT
|
|
27
|
+
* the memory-seam `SIGRUN_HOOK_DEADLINE` (hook calls can sit on the
|
|
28
|
+
* reply path; the memory knob tunes background handlers). A positive
|
|
29
|
+
* integer; `parseMemorySeamCount`'s posture (fail loud, `""` is unset).
|
|
30
|
+
*/
|
|
31
|
+
export const PUBLISHED_HOOK_DEADLINE_ENV = "SIGRUN_PUBLISHED_HOOK_DEADLINE";
|
|
32
|
+
/** The published-hook deadline default (30 s). */
|
|
33
|
+
export const DEFAULT_PUBLISHED_HOOK_DEADLINE_MS = 30_000;
|
|
34
|
+
/**
|
|
35
|
+
* The decision-provider call timeout (warden Task 6):
|
|
36
|
+
* `SIGRUN_DECISION_TIMEOUT_MS`, over the file's
|
|
37
|
+
* `server.decisions.timeoutMs`, default 15000 ms — both the default AND
|
|
38
|
+
* the cap for one `host.decisions.evaluate` call (`min(timeoutMs, cap)`).
|
|
39
|
+
* A positive integer; `parseMemorySeamCount`'s posture (fail loud,
|
|
40
|
+
* `""` is unset).
|
|
41
|
+
*/
|
|
42
|
+
export const DECISION_TIMEOUT_ENV = "SIGRUN_DECISION_TIMEOUT_MS";
|
|
43
|
+
/** The decision timeout default (15 s). */
|
|
44
|
+
export const DEFAULT_DECISION_TIMEOUT_MS = 15_000;
|
|
23
45
|
/** The provider id used when the model spec carries no `provider/` prefix. */
|
|
24
46
|
export const DEFAULT_PROVIDER = "openai";
|
|
25
47
|
/**
|
|
@@ -820,6 +842,8 @@ export function resolvedServerSection(config) {
|
|
|
820
842
|
...(webui === undefined ? {} : { webui }),
|
|
821
843
|
compaction: { ...config.compaction },
|
|
822
844
|
memorySeam: { ...config.memorySeam },
|
|
845
|
+
publishedHookDeadlineMs: config.publishedHookDeadlineMs,
|
|
846
|
+
decisions: { ...config.decisions },
|
|
823
847
|
isolation: { ...config.isolation },
|
|
824
848
|
providers: { ...config.providers },
|
|
825
849
|
...(auth === undefined ? {} : { auth }),
|
|
@@ -860,6 +884,8 @@ export function resolvedServerSection(config) {
|
|
|
860
884
|
* | `SIGRUN_IDLE_THRESHOLD` | `memorySeam.idleThresholdMs` |
|
|
861
885
|
* | `SIGRUN_WORKER_DEADLINE` | `memorySeam.workerDeadlineMs` |
|
|
862
886
|
* | `SIGRUN_QUEUE_MAX` | `memorySeam.queueMax` |
|
|
887
|
+
* | `SIGRUN_PUBLISHED_HOOK_DEADLINE` | `publishedHookDeadlineMs` |
|
|
888
|
+
* | `SIGRUN_DECISION_TIMEOUT_MS` | `decisions.timeoutMs` |
|
|
863
889
|
* | `SIGRUN_ISOLATION` | `isolation.mode` |
|
|
864
890
|
* | `SIGRUN_PROVIDER_REFRESH_MINUTES` | `providers.refreshIntervalMinutes` |
|
|
865
891
|
* | `SIGRUN_BACKUP_*` | `backup` |
|
|
@@ -917,6 +943,14 @@ export function envServerSection(env) {
|
|
|
917
943
|
});
|
|
918
944
|
if (memorySeam !== undefined)
|
|
919
945
|
section.memorySeam = memorySeam;
|
|
946
|
+
const publishedHookDeadlineMs = parseMemorySeamCount(env, PUBLISHED_HOOK_DEADLINE_ENV);
|
|
947
|
+
if (publishedHookDeadlineMs !== undefined) {
|
|
948
|
+
section.publishedHookDeadlineMs = publishedHookDeadlineMs;
|
|
949
|
+
}
|
|
950
|
+
const decisionTimeoutMs = parseMemorySeamCount(env, DECISION_TIMEOUT_ENV);
|
|
951
|
+
if (decisionTimeoutMs !== undefined) {
|
|
952
|
+
section.decisions = { timeoutMs: decisionTimeoutMs };
|
|
953
|
+
}
|
|
920
954
|
const isolationMode = parseIsolationMode(env);
|
|
921
955
|
if (isolationMode !== undefined)
|
|
922
956
|
section.isolation = { mode: isolationMode };
|
|
@@ -1020,6 +1054,11 @@ export function effectiveServerSection(env, fileServer, config) {
|
|
|
1020
1054
|
"memorySeam",
|
|
1021
1055
|
"queueMax",
|
|
1022
1056
|
]);
|
|
1057
|
+
emit("publishedHookDeadlineMs", config.publishedHookDeadlineMs, ["publishedHookDeadlineMs"]);
|
|
1058
|
+
emit("decisions.timeoutMs", config.decisions.timeoutMs, [
|
|
1059
|
+
"decisions",
|
|
1060
|
+
"timeoutMs",
|
|
1061
|
+
]);
|
|
1023
1062
|
emit("auth.sessionDays", config.auth.sessionDays, ["auth", "sessionDays"]);
|
|
1024
1063
|
emit("isolation.mode", config.isolation.mode, ["isolation", "mode"]);
|
|
1025
1064
|
emit("isolation.osUsers", config.isolation.osUsers, ["isolation", "osUsers"]);
|
|
@@ -1173,6 +1212,14 @@ export function loadConfig(env, settingsFile) {
|
|
|
1173
1212
|
backupStaleAfterHours: server?.backup?.staleAfterHours ?? DEFAULT_BACKUP_STALE_AFTER_HOURS,
|
|
1174
1213
|
compaction: parseCompaction(env, server?.compaction),
|
|
1175
1214
|
memorySeam: parseMemorySeam(env, server?.memorySeam),
|
|
1215
|
+
publishedHookDeadlineMs: parseMemorySeamCount(env, PUBLISHED_HOOK_DEADLINE_ENV) ??
|
|
1216
|
+
server?.publishedHookDeadlineMs ??
|
|
1217
|
+
DEFAULT_PUBLISHED_HOOK_DEADLINE_MS,
|
|
1218
|
+
decisions: {
|
|
1219
|
+
timeoutMs: parseMemorySeamCount(env, DECISION_TIMEOUT_ENV) ??
|
|
1220
|
+
server?.decisions?.timeoutMs ??
|
|
1221
|
+
DEFAULT_DECISION_TIMEOUT_MS,
|
|
1222
|
+
},
|
|
1176
1223
|
isolation: parseIsolation(env, server?.isolation),
|
|
1177
1224
|
providers: {
|
|
1178
1225
|
refreshIntervalMinutes: parseRefreshIntervalMinutes(env, server?.providers?.refreshIntervalMinutes),
|
package/dist/curation/host.js
CHANGED
|
@@ -25,6 +25,8 @@ import { digestRunRun, forkReviewRun, setChangeTargetResolver } from "./runs.js"
|
|
|
25
25
|
import { resolveUpstream } from "./overlays.js";
|
|
26
26
|
import { jobSkillRefs } from "../jobs/skills-refs.js";
|
|
27
27
|
import { SessionMetaDoc } from "../server/session-meta.js";
|
|
28
|
+
/** Per-target bound for one `wantsReview` answer (C5). */
|
|
29
|
+
const WANTS_REVIEW_BOUND_MS = 2000;
|
|
28
30
|
/** modelCallsSince's entry-page size (cursor-walked, never a cap). */
|
|
29
31
|
const MODEL_CALLS_PAGE = 500;
|
|
30
32
|
/** Scan bound for the cursor walk: a broken cursor loops, a count does not. */
|
|
@@ -62,7 +64,14 @@ export function createCurationHost(deps) {
|
|
|
62
64
|
*/
|
|
63
65
|
const digestFor = async (user, input, tools, deadline) => {
|
|
64
66
|
const remaining = Math.max(1, deadline - Date.now());
|
|
65
|
-
|
|
67
|
+
const runDeps = {
|
|
68
|
+
harness,
|
|
69
|
+
registry,
|
|
70
|
+
...(deps.interactiveTools === undefined
|
|
71
|
+
? {}
|
|
72
|
+
: { interactiveTools: deps.interactiveTools }),
|
|
73
|
+
};
|
|
74
|
+
return await digestRunRun(runDeps, user, {
|
|
66
75
|
instruction: input,
|
|
67
76
|
allow: tools,
|
|
68
77
|
maxToolCalls: 8,
|
|
@@ -151,37 +160,83 @@ export function createCurationHost(deps) {
|
|
|
151
160
|
return await takeHints(conversation, BACKGROUND_CONTEXT);
|
|
152
161
|
},
|
|
153
162
|
async targets(user, id) {
|
|
154
|
-
// The
|
|
155
|
-
// only when
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
+
// The projection drops the C5 callbacks (the info is data —
|
|
164
|
+
// readTools rides only when the target declared it).
|
|
165
|
+
return (await servedTargets(user, id)).map((target) => ({
|
|
166
|
+
id: target.id,
|
|
167
|
+
tools: target.tools,
|
|
168
|
+
...(target.readTools.length === 0
|
|
169
|
+
? {}
|
|
170
|
+
: { readTools: target.readTools }),
|
|
171
|
+
guidance: target.guidance,
|
|
172
|
+
maintain: target.maintain,
|
|
173
|
+
}));
|
|
174
|
+
},
|
|
175
|
+
async wantsReview(user, id) {
|
|
176
|
+
// C5: any ONE served target requesting a review is enough. Each
|
|
177
|
+
// target's answer is bounded — a throw or a slow target counts
|
|
178
|
+
// as `false` with one log line naming the target (the review
|
|
179
|
+
// loop must never stall on one target's clock).
|
|
180
|
+
for (const target of await servedTargets(user, id)) {
|
|
181
|
+
if (target.wantsReview === undefined)
|
|
182
|
+
continue;
|
|
183
|
+
let wants = false;
|
|
184
|
+
try {
|
|
185
|
+
wants = await bounded(Promise.resolve(target.wantsReview(user, id)));
|
|
163
186
|
}
|
|
164
|
-
|
|
165
|
-
|
|
187
|
+
catch (error) {
|
|
188
|
+
console.warn(`curation: wantsReview of ${target.id} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
166
189
|
continue;
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
guidance: [...guidance],
|
|
171
|
-
maintain: target.maintain !== undefined,
|
|
172
|
-
});
|
|
190
|
+
}
|
|
191
|
+
if (wants === true)
|
|
192
|
+
return true;
|
|
173
193
|
}
|
|
174
|
-
return
|
|
194
|
+
return false;
|
|
175
195
|
},
|
|
176
196
|
async tools(id) {
|
|
177
197
|
const visible = await modelVisibleTools(id);
|
|
178
198
|
return [...visible];
|
|
179
199
|
},
|
|
180
200
|
async forkReview(id, opts) {
|
|
181
|
-
|
|
201
|
+
const result = await forkReviewRun({
|
|
202
|
+
harness,
|
|
203
|
+
registry,
|
|
204
|
+
...(deps.interactiveTools === undefined
|
|
205
|
+
? {}
|
|
206
|
+
: { interactiveTools: deps.interactiveTools }),
|
|
207
|
+
}, id, opts);
|
|
208
|
+
// C5: after a COMPLETED review, tell every served target the
|
|
209
|
+
// review consumed its hints — the target's signal to retire
|
|
210
|
+
// them (errors caught and logged; a target never blocks the
|
|
211
|
+
// result). Owner read from the conversation's SessionMetaDoc,
|
|
212
|
+
// as the run driver does; other statuses and an ownerless
|
|
213
|
+
// conversation fire nothing.
|
|
214
|
+
if (result.status !== "completed")
|
|
215
|
+
return result;
|
|
216
|
+
const meta = await harness.snapshot(SessionMetaDoc, id, BACKGROUND_CONTEXT);
|
|
217
|
+
const owner = meta?.owner;
|
|
218
|
+
if (owner === undefined)
|
|
219
|
+
return result;
|
|
220
|
+
for (const target of await servedTargets(owner, id)) {
|
|
221
|
+
if (target.reviewed === undefined)
|
|
222
|
+
continue;
|
|
223
|
+
try {
|
|
224
|
+
await target.reviewed(owner, id);
|
|
225
|
+
}
|
|
226
|
+
catch (error) {
|
|
227
|
+
console.warn(`curation: reviewed of ${target.id} failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
return result;
|
|
182
231
|
},
|
|
183
232
|
async digestRun(user, opts) {
|
|
184
|
-
return await digestRunRun({
|
|
233
|
+
return await digestRunRun({
|
|
234
|
+
harness,
|
|
235
|
+
registry,
|
|
236
|
+
...(deps.interactiveTools === undefined
|
|
237
|
+
? {}
|
|
238
|
+
: { interactiveTools: deps.interactiveTools }),
|
|
239
|
+
}, user, opts, deps.defaultModel, deps.defaultAgent);
|
|
185
240
|
},
|
|
186
241
|
async maintain(user, targetId, budget) {
|
|
187
242
|
const target = curation
|
|
@@ -280,6 +335,55 @@ export function createCurationHost(deps) {
|
|
|
280
335
|
const agent = await conversation.agent(BACKGROUND_CONTEXT);
|
|
281
336
|
return new Set(agent.tools.map((tool) => tool.name));
|
|
282
337
|
}
|
|
338
|
+
/**
|
|
339
|
+
* The targets served for `(user, id)`: the selection filter (every
|
|
340
|
+
* listed tool — `tools` AND `readTools` — model-visible) and the
|
|
341
|
+
* guidance filter (`[]` = unavailable for the user), projected for
|
|
342
|
+
* the caller. No id: every target (selection-independent).
|
|
343
|
+
*/
|
|
344
|
+
async function servedTargets(user, id) {
|
|
345
|
+
const visible = id === undefined ? undefined : await modelVisibleTools(id);
|
|
346
|
+
const result = [];
|
|
347
|
+
for (const target of curation.targets()) {
|
|
348
|
+
if (visible !== undefined) {
|
|
349
|
+
if (target.tools.some((tool) => !visible.has(tool)))
|
|
350
|
+
continue;
|
|
351
|
+
if ((target.readTools ?? []).some((tool) => !visible.has(tool))) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
const guidance = await target.guidance(user);
|
|
356
|
+
if (guidance.length === 0)
|
|
357
|
+
continue;
|
|
358
|
+
result.push({
|
|
359
|
+
id: target.id,
|
|
360
|
+
tools: [...target.tools],
|
|
361
|
+
readTools: [...(target.readTools ?? [])],
|
|
362
|
+
guidance: [...guidance],
|
|
363
|
+
maintain: target.maintain !== undefined,
|
|
364
|
+
...(target.wantsReview === undefined
|
|
365
|
+
? {}
|
|
366
|
+
: { wantsReview: target.wantsReview }),
|
|
367
|
+
...(target.reviewed === undefined
|
|
368
|
+
? {}
|
|
369
|
+
: { reviewed: target.reviewed }),
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
return result;
|
|
373
|
+
}
|
|
374
|
+
/** Bound one target answer at the C5 deadline; rejects past it. */
|
|
375
|
+
async function bounded(answer) {
|
|
376
|
+
return await new Promise((resolve, reject) => {
|
|
377
|
+
const timer = setTimeout(() => reject(new Error("wantsReview timed out")), WANTS_REVIEW_BOUND_MS);
|
|
378
|
+
answer.then((value) => {
|
|
379
|
+
clearTimeout(timer);
|
|
380
|
+
resolve(value === true);
|
|
381
|
+
}, (error) => {
|
|
382
|
+
clearTimeout(timer);
|
|
383
|
+
reject(error instanceof Error ? error : new Error(String(error)));
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
}
|
|
283
387
|
/** The feed entries one review caused (newest-first walk). */
|
|
284
388
|
async function feedOf(store, reviewId) {
|
|
285
389
|
const found = [];
|
package/dist/curation/runs.js
CHANGED
|
@@ -42,6 +42,7 @@ import { ensureRequestId, recordAuthor } from "./attribution.js";
|
|
|
42
42
|
import { isInternal, markInternal } from "./internal.js";
|
|
43
43
|
import { curationRun, CurationRunDoc } from "./run-doc.js";
|
|
44
44
|
import { CURATION_GATE_EXTENSION } from "./gate.js";
|
|
45
|
+
import { excludeInteractiveTools, withoutInteractive, } from "../harness/interactive-tools.js";
|
|
45
46
|
import { SessionMetaDoc } from "../server/session-meta.js";
|
|
46
47
|
/** The target-id resolver for collected changes (the host installs it). */
|
|
47
48
|
let targetOfTool = (tool) => tool === "skill_manage" ? "sigrun.skills" : `tool:${tool}`;
|
|
@@ -495,7 +496,12 @@ async function answerText(conversation, answer) {
|
|
|
495
496
|
* when the parent refuses.
|
|
496
497
|
*/
|
|
497
498
|
export async function forkReviewRun(deps, id, opts) {
|
|
498
|
-
|
|
499
|
+
// Ask-user Task 3: the allow list loses interactive tools BEFORE the
|
|
500
|
+
// non-empty check (an all-interactive list still fails loudly, with
|
|
501
|
+
// the strip warning logged first). Fork reviews KEEP them listed
|
|
502
|
+
// (cache parity) — the gate blocks a call instead.
|
|
503
|
+
const allow = withoutInteractive(opts.allow, deps.interactiveTools ?? new Set(), console.warn, "fork review");
|
|
504
|
+
checkRunOpts({ ...opts, allow });
|
|
499
505
|
const { harness } = deps;
|
|
500
506
|
const parent = await harness.conversation(id, BACKGROUND_CONTEXT);
|
|
501
507
|
if (parent === undefined) {
|
|
@@ -537,7 +543,7 @@ export async function forkReviewRun(deps, id, opts) {
|
|
|
537
543
|
const doc = await tx.doc(CurationRunDoc, forkId);
|
|
538
544
|
doc.user = owner;
|
|
539
545
|
doc.reviewId = opts.reviewId;
|
|
540
|
-
doc.allow = [...
|
|
546
|
+
doc.allow = [...allow];
|
|
541
547
|
doc.parent = id;
|
|
542
548
|
// The fork's copied meta carries the parent's sharing; an
|
|
543
549
|
// internal run is nobody's shared copy (A2's invisibility
|
|
@@ -561,7 +567,8 @@ export async function forkReviewRun(deps, id, opts) {
|
|
|
561
567
|
* default + gate, same run doc and driver.
|
|
562
568
|
*/
|
|
563
569
|
export async function digestRunRun(deps, user, opts, defaultModel, defaultAgent) {
|
|
564
|
-
|
|
570
|
+
const allow = withoutInteractive(opts.allow, deps.interactiveTools ?? new Set(), console.warn, "digest run");
|
|
571
|
+
checkRunOpts({ ...opts, allow });
|
|
565
572
|
const { harness } = deps;
|
|
566
573
|
const conversation = await harness.createConversation({
|
|
567
574
|
ownership: { kind: "ownerless" },
|
|
@@ -576,10 +583,14 @@ export async function digestRunRun(deps, user, opts, defaultModel, defaultAgent)
|
|
|
576
583
|
},
|
|
577
584
|
init: async (tx, id) => {
|
|
578
585
|
await markInternal(tx, id, user);
|
|
586
|
+
// Ask-user Task 3: the digest conversation's fresh agent
|
|
587
|
+
// selection loses the interactive tools (the tool is never
|
|
588
|
+
// listed, so the model never tries).
|
|
589
|
+
await excludeInteractiveTools(tx, id, deps.interactiveTools ?? new Set());
|
|
579
590
|
const doc = await tx.doc(CurationRunDoc, id);
|
|
580
591
|
doc.user = user;
|
|
581
592
|
doc.reviewId = opts.reviewId;
|
|
582
|
-
doc.allow = [...
|
|
593
|
+
doc.allow = [...allow];
|
|
583
594
|
},
|
|
584
595
|
}, BACKGROUND_CONTEXT);
|
|
585
596
|
return await driveRun(deps, async () => conversation, { ...opts, instruction: opts.input });
|