@sigrun/sigrun 0.8.0 → 0.10.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 +55 -87
- 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 +268 -3
- package/dist/harness/boot.js +113 -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 +243 -1
- package/dist/harness/packages.js +7 -0
- package/dist/harness/published-hooks.js +208 -0
- package/dist/harness/seam-host.js +136 -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/index.js +3 -0
- package/dist/jobs/observe.js +27 -2
- package/dist/jobs/spec.js +15 -1
- package/dist/server/conversation-ops.js +31 -0
- package/dist/server/core-views/jobs-form.js +335 -0
- package/dist/server/core-views/jobs.js +300 -15
- 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 +53 -1
- package/dist/server/index.js +2 -1
- 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/providers.js +65 -8
- package/dist/server/routes/submissions.js +3 -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/jobs.d.ts +4 -0
- package/node_modules/@sigrun/shared/dist/settings.d.ts +32 -0
- package/node_modules/@sigrun/shared/dist/views.d.ts +58 -0
- package/node_modules/@sigrun/shared/package.json +6 -2
- package/package.json +8 -8
- package/webui/dist/assets/index-By63QKOA.js +55 -0
- package/webui/dist/assets/index-D5KowS7k.css +1 -0
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-B3uMNF59.css +0 -1
- package/webui/dist/assets/index-BXsCyod4.js +0 -55
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,61 @@ 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.10.0] - 2026-11-06
|
|
9
|
+
|
|
10
|
+
### Added
|
|
11
|
+
|
|
12
|
+
- Jobs editor in the `core/jobs` view: a New job form (prompt jobs, schedule cron/every/once,
|
|
13
|
+
model + thinking pickers from the served catalogue), Edit/Delete for api jobs (edit preserves
|
|
14
|
+
conversation/deliver/runSharing/task targets; clearing the description removes it), details
|
|
15
|
+
and provenance rows (Defined in / Owner / action fields) with a read-only callout for declared
|
|
16
|
+
(`file:`/`ext:`) jobs, and the job description as the sidebar row subtitle.
|
|
17
|
+
- `description` on `JobSpec` (optional, trimmed, ≤ 500 chars) for every job source — settings.json
|
|
18
|
+
boot-gates an invalid one; an extension's declaration quarantines.
|
|
19
|
+
- TUI: extension views (Jobs, Skills, every extension surface) now render in the main pane with
|
|
20
|
+
key routing, Esc back-to-list/close, overlay-aware Esc, notice flashes on the status bar, and
|
|
21
|
+
block navigation (row activation, page actions like New job, post-action navigation) — plus a
|
|
22
|
+
fix for the TUI ext-transport URL builder (suffix routes were appended after `?params=`).
|
|
23
|
+
- Live refresh for principal-scoped views (`core/jobs`, `core/skills`, curator): both clients'
|
|
24
|
+
surface hubs now dispatch `surface.changed` by (slug, id) instead of a client-side params hash
|
|
25
|
+
that never matched the server's principal-scoped key — background job runs now appear in an
|
|
26
|
+
open view without a reload.
|
|
27
|
+
|
|
28
|
+
## [0.9.0] - 2026-11-05
|
|
29
|
+
|
|
30
|
+
### Added
|
|
31
|
+
|
|
32
|
+
- Decision providers: a factory result's `decisionProviders` contributes typed-question models
|
|
33
|
+
(noul / choice / score) keyed in `auth.json` with a `credentialEnv` fallback; credentials write
|
|
34
|
+
through `PUT/DELETE /v1/providers/:id/credential` (a decision-only id re-reads readiness
|
|
35
|
+
instead of refreshing). `host.decisions.evaluate` (never throws; validated against the wire
|
|
36
|
+
schema and the model's limits; a 15 s deadline capped by `server.decisions.timeoutMs` /
|
|
37
|
+
`SIGRUN_DECISION_TIMEOUT_MS`), the `decisionModel` setting (live), decision entries with
|
|
38
|
+
`kind` + model ids on `GET /v1/providers`, a `decisions` projection on `GET /v1/meta`, and the
|
|
39
|
+
opt-in `decision_evaluate` tool (`decisions.tool`, default false). Docs:
|
|
40
|
+
"Contributing decision providers" in docs/extending.md.
|
|
41
|
+
- `host.jobs.activeRun(conversationId)`: the job whose fire is CURRENTLY running in a
|
|
42
|
+
conversation (both fire shapes), with its effective owner — the unattended-run marker
|
|
43
|
+
extensions need to route destructive calls. Fixed-conversation fires settle their runs via the
|
|
44
|
+
submission's `job:` requestId (no meta stamp; a fire into an existing conversation does not
|
|
45
|
+
mark its later runs job-driven).
|
|
46
|
+
- `host.models.complete(input)`: one-shot chat completions for extensions (no conversation);
|
|
47
|
+
explicit model → `namingModel` → default, `role: "utility"`, a 60 s default timeout, both the
|
|
48
|
+
timer and the caller's signal bounding the wait.
|
|
49
|
+
- Declinable interaction records: `declinable: true` marks a pending record declinable;
|
|
50
|
+
`POST /v1/conversations/:id/interactions/:callId/decline` (write access) settles it
|
|
51
|
+
`skipped` with `declinedVia: "button"` (404 `no_such_interaction` / 404 `not_pending` /
|
|
52
|
+
409 `not_declinable`); `host.declineInteraction(conversationId, callId, context, {user})`
|
|
53
|
+
is the extension-side peer; `interactionSettled` payloads carry `declinedVia`. Declining
|
|
54
|
+
sweeps the record's unread secret delivery inside the same commit.
|
|
55
|
+
- Interactive tools: a factory result's `interactiveTools` names the registered tools that
|
|
56
|
+
need a human present. Core hides them in job-run conversations (`conversation: "new"`
|
|
57
|
+
fires, `host.conversations.create({job})`) and curator runs (digest agents exclude them;
|
|
58
|
+
every run's `allow` list loses them with one warning; fork reviews keep them listed for
|
|
59
|
+
prompt-cache parity and the gate blocks a call instead). `host.jobs.isJobRun` tells an
|
|
60
|
+
interactive tool's execute whether the current run is job-driven (a job firing into an
|
|
61
|
+
existing conversation is not covered by the creation-time filter — refuse there).
|
|
62
|
+
|
|
8
63
|
## [0.8.0] - 2026-11-04
|
|
9
64
|
|
|
10
65
|
Core notices: a per-user notice system for extensions. An extension posts a short notice
|
|
@@ -91,93 +146,6 @@ fresh-eyes clean-room install verification.
|
|
|
91
146
|
|
|
92
147
|
- Release-pin bump to 0.6.1 (root, shared, tui, sandbox platform packages, chart appVersion, compose defaults).
|
|
93
148
|
|
|
94
|
-
## [Unreleased]
|
|
95
|
-
|
|
96
|
-
### Added
|
|
97
|
-
- **Core notices** — a per-user message channel with buttons. An
|
|
98
|
-
extension posts a short notice to ONE directory user through
|
|
99
|
-
`host.notices` (`post`/`clear`/`list`); `noticeActions` on the factory
|
|
100
|
-
result declares the handlers its `extension` buttons may name. A
|
|
101
|
-
notice is title + body + up to three actions (`navigate`, `open` —
|
|
102
|
-
https only, `extension`), bounded by `NOTICE_LIMITS` and capped at 10
|
|
103
|
-
per extension / 50 per user. Nothing expires on a timer: a notice
|
|
104
|
-
clears on its ✕, the user's next prompt in that conversation, a
|
|
105
|
-
navigate/open button, a successful extension action, Clear all, the
|
|
106
|
-
posting extension's `clear`, or eviction past a cap. Nobody but the
|
|
107
|
-
addressed user ever sees one (a foreign id answers 404, never 403),
|
|
108
|
-
forks never copy them, and a revoked share drops that conversation's
|
|
109
|
-
notices on the next read. `GET /v1/notices`,
|
|
110
|
-
`POST /v1/notices/{id}/dismiss`, `POST /v1/notices/dismiss-all`,
|
|
111
|
-
`POST /v1/notices/{id}/actions/{actionId}`, and the user-only
|
|
112
|
-
`notice.changed` event. Store: `<dataDir>/users/<user>/notices.json`
|
|
113
|
-
(inside the `data` backup section).
|
|
114
|
-
- **Notices UI** — a bell with a count on every tab (rail on desktop,
|
|
115
|
-
tab header on phones), a notices pane that docks on Chats and slides
|
|
116
|
-
over every other tab, an inline stack above the composer (at most
|
|
117
|
-
three, then "N more"), and a 6 s peek that never takes focus. The TUI
|
|
118
|
-
gains an inline line, a status-bar count, and an overlay
|
|
119
|
-
(`ctrl+g` / `gN` / `M-N`).
|
|
120
|
-
- **Ambient buttons work (webui).** The webui's ambient renderer now
|
|
121
|
-
dispatches `navigate` and `open` actions (links open with
|
|
122
|
-
`rel="noopener noreferrer"`) instead of dropping them. The TUI's
|
|
123
|
-
ambient dispatcher is wired the same way but has no production caller
|
|
124
|
-
yet.
|
|
125
|
-
- **Views are tabs (webui).** The webui shell registers views as rail
|
|
126
|
-
tabs with remembered routes; the chat view keeps the conversation
|
|
127
|
-
list and right panel to itself. Below the tablet breakpoint a phone
|
|
128
|
-
bottom tab bar (with a More sheet) replaces the rail.
|
|
129
|
-
- **`page.selection`** — a server-declared, presentation-only
|
|
130
|
-
list-first drill-in for compact widths (`{selected, back}` on a
|
|
131
|
-
`page`; webui and TUI render it, `toText` ignores it).
|
|
132
|
-
- **Core views.** Core attaches its own view surfaces under the
|
|
133
|
-
reserved `core` slug and lists them as `coreViews` (one
|
|
134
|
-
`CoreViewInfo {id, title, icon, refreshMs?}` per view) on
|
|
135
|
-
`/v1/extensions` and `/v1/meta`; they are never `extensions[]` rows,
|
|
136
|
-
so the health dot is unaffected. `job.changed` now bumps every
|
|
137
|
-
rendered core/jobs instance.
|
|
138
|
-
- **The Jobs tab** (`core/jobs`): the jobs surface as a core blocks
|
|
139
|
-
view — sidebar of the caller-visible jobs, drill-in with summary,
|
|
140
|
-
paged runs and revisions (a selected revision diffs its spec JSON
|
|
141
|
-
against the previous one), Run now / Reset state for write access,
|
|
142
|
-
and per-row navigation into a run's conversation. The Jobs panel is
|
|
143
|
-
gone.
|
|
144
|
-
- **The Skills tab** (`core/skills`): every skill root the deployment
|
|
145
|
-
serves — Workspace / Learned / Config dir (the deployment-wide
|
|
146
|
-
`<config-dir>/skills/`, read-only) / one group per installed package
|
|
147
|
-
(`· <pkg>`, with a per-row package badge) / Deleted — with shadowing visible, owned-skill CRUD, per-skill git
|
|
148
|
-
history (25 entries per page, per-file diffs, restore), deleted-skill
|
|
149
|
-
restore, and per-user overlays as the only write to the read-only
|
|
150
|
-
shared roots. Some surface actions return `navigate`; the webui
|
|
151
|
-
applies that navigation, the TUI does not yet (a known channel gap).
|
|
152
|
-
- **Every-root skill discovery** (`src/skills/roots.ts`): workspace,
|
|
153
|
-
learned, user dir, and every installed package's skills listed in one
|
|
154
|
-
pass, with precedence/shadowing resolution and symlink-safe reads.
|
|
155
|
-
- **GitHistory** (`src/history/git.ts`) — isolated local git for
|
|
156
|
-
sigrun-owned history stores: execFile with explicit `--git-dir` /
|
|
157
|
-
`--work-tree`, a hermetic git env, per-repo locking, and fail-soft
|
|
158
|
-
writes (a broken repo is quarantined aside and re-initialized;
|
|
159
|
-
mutations never fail because history did).
|
|
160
|
-
- **Curation stores on git** (Task 9): each user's learned skills,
|
|
161
|
-
overlays, and curator state live in a per-user git history repo; the
|
|
162
|
-
change feed's `actor` widens to `"outside"` and its `id`/`before`/
|
|
163
|
-
`after` are commit SHAs. A once-per-process migration imports the
|
|
164
|
-
pre-git store (the legacy files are left read-only, outside the
|
|
165
|
-
repo's pathspec).
|
|
166
|
-
- **Workspace skills history** (Task 10): the workspace `skills/`
|
|
167
|
-
tree gets its own per-user repo (git dir outside the work tree),
|
|
168
|
-
caught up on settled runs and tab opens; ownerless conversations
|
|
169
|
-
skip it.
|
|
170
|
-
|
|
171
|
-
### Changed
|
|
172
|
-
- **The curation seal**: workspaces and grants that sit inside or
|
|
173
|
-
contain a user's `curation/` tree are rejected; boot revalidates
|
|
174
|
-
stored records and names each failing one loudly. A user whose
|
|
175
|
-
workspace was the bare user area loses agent tools until an admin
|
|
176
|
-
re-points it: `PATCH /v1/users/<u> {"workspace": "<userArea>/workspace"}`.
|
|
177
|
-
History tamper-resistance holds under active sandbox isolation; a
|
|
178
|
-
single-user install without isolation runs the agent unsandboxed
|
|
179
|
-
(the existing trust model).
|
|
180
|
-
|
|
181
149
|
## [0.6.0] - 2026-11-02
|
|
182
150
|
|
|
183
151
|
### Added
|
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 });
|