@sigrun/sigrun 0.4.2 → 0.6.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 +92 -1
- package/README.md +63 -33
- package/dist/commands/expand.js +37 -5
- package/dist/curation/attribution.js +135 -0
- package/dist/curation/deletion.js +32 -0
- package/dist/curation/gate.js +50 -0
- package/dist/curation/host.js +342 -0
- package/dist/curation/internal.js +54 -0
- package/dist/curation/names.js +5 -0
- package/dist/curation/overlays.js +325 -0
- package/dist/curation/paths.js +17 -0
- package/dist/curation/run-doc.js +64 -0
- package/dist/curation/runs.js +579 -0
- package/dist/curation/scan.js +30 -0
- package/dist/curation/skill-tool.js +308 -0
- package/dist/curation/store.js +444 -0
- package/dist/curation/targets.js +77 -0
- package/dist/curation/usage.js +124 -0
- package/dist/curation/wrap.js +212 -0
- package/dist/extension.d.ts +336 -1
- package/dist/harness/boot.js +236 -13
- package/dist/harness/builtins.js +19 -12
- package/dist/harness/event-bus.js +64 -0
- package/dist/harness/identity/primary-admin.js +19 -0
- package/dist/harness/loader.js +114 -1
- package/dist/harness/naming.js +6 -0
- package/dist/harness/packages.js +14 -9
- package/dist/harness/reserved-sections.js +4 -0
- package/dist/harness/seam-host.js +154 -26
- package/dist/harness/sections/context-files.js +6 -3
- package/dist/harness/sections/job-state.js +24 -0
- package/dist/harness/settings.js +2 -2
- package/dist/harness/tools.js +70 -0
- package/dist/harness/workspace.js +30 -3
- package/dist/jobs/fire.js +37 -14
- package/dist/jobs/index.js +34 -7
- package/dist/jobs/reconcile.js +9 -0
- package/dist/jobs/skills-refs.js +40 -0
- package/dist/jobs/state-dir.js +16 -0
- package/dist/jobs/transfer.js +14 -0
- package/dist/sandbox/policy.js +27 -4
- package/dist/serve.js +5 -3
- package/dist/server/blocks/answers.js +9 -3
- package/dist/server/conversation-ops.js +34 -3
- package/dist/server/helpers.js +1 -1
- package/dist/server/host-bridge.js +31 -1
- package/dist/server/notifications.js +108 -16
- package/dist/server/routes/jobs.js +31 -0
- package/dist/server/routes/meta.js +8 -3
- package/dist/server/routes/submissions.js +5 -0
- package/dist/server/routes/surfaces.js +3 -1
- package/dist/server/routes/users.js +7 -0
- package/dist/server/schema-fixture.js +168 -0
- package/dist/server/session-meta.js +12 -10
- package/dist/settings/core-sections.js +18 -0
- package/dist/settings/registry.js +12 -0
- package/dist/skills/index.js +88 -19
- package/node_modules/@sigrun/shared/CHANGELOG.md +6 -1
- package/node_modules/@sigrun/shared/dist/access.d.ts +3 -0
- package/node_modules/@sigrun/shared/dist/access.js +8 -0
- package/node_modules/@sigrun/shared/dist/session-meta.d.ts +15 -4
- package/node_modules/@sigrun/shared/dist/settings.d.ts +7 -5
- package/node_modules/@sigrun/shared/package.json +1 -1
- package/package.json +9 -7
- package/webui/dist/assets/index-BR0di0OG.css +1 -0
- package/webui/dist/assets/{index-DWmsAQl2.js → index-BzrA1T4g.js} +5 -5
- package/webui/dist/index.html +2 -2
- package/webui/dist/assets/index-h7zDY9vD.css +0 -1
package/dist/harness/loader.js
CHANGED
|
@@ -93,6 +93,7 @@ import { satisfies, validRange } from "semver";
|
|
|
93
93
|
import { HOST_RUNTIME_MODULES, hostModuleVersion, readPackageAssetsDir, readPackageName, readPackageVersion, resolveExtensionEntry, } from "./packages.js";
|
|
94
94
|
import { RESERVED_CORE_SECTION_KEYS, RESERVED_EXTENSION_SLUGS, } from "./reserved-sections.js";
|
|
95
95
|
import { slugifyName } from "./stores.js";
|
|
96
|
+
import { wrapExtensionCurationTools } from "../curation/wrap.js";
|
|
96
97
|
/** Every lifecycle field name, in validation order (mirrors the contract doc). */
|
|
97
98
|
const LIFECYCLE_FIELDS = [
|
|
98
99
|
"conversationCreated",
|
|
@@ -217,6 +218,52 @@ function validateWorkers(value) {
|
|
|
217
218
|
* (the §8.2 grammar), and duplicate names within one extension are
|
|
218
219
|
* rejected (they would collide on the `ext:<slug>:<name>` id).
|
|
219
220
|
*/
|
|
221
|
+
/**
|
|
222
|
+
* Validate a factory result's `curationTargets` declaration (curator
|
|
223
|
+
* P7). Absent → undefined (no targets). Present but malformed →
|
|
224
|
+
* throw; the loader quarantines the extension with the thrown message
|
|
225
|
+
* (fail-loud: an extension that THINKS it declared a curation target
|
|
226
|
+
* must not silently lose it). Per target, validation order: name,
|
|
227
|
+
* duplicate qualified name (checked at collection — the slug is
|
|
228
|
+
* unknown here), tools list, guidance, undo (REQUIRED — no undo, no
|
|
229
|
+
* magic), maintain.
|
|
230
|
+
*
|
|
231
|
+
* The REGISTERED-TOOL check (a target listing a tool its extension
|
|
232
|
+
* does not register) and the cross-extension duplicate qualified id
|
|
233
|
+
* live in the load loop, where the registry and the slug are known.
|
|
234
|
+
*/
|
|
235
|
+
function validateCurationTargets(value) {
|
|
236
|
+
if (value === undefined)
|
|
237
|
+
return undefined;
|
|
238
|
+
if (!Array.isArray(value)) {
|
|
239
|
+
throw new Error("extension factory returned a curationTargets field that is not an array");
|
|
240
|
+
}
|
|
241
|
+
const targets = [];
|
|
242
|
+
for (const entry of value) {
|
|
243
|
+
const target = entry;
|
|
244
|
+
const name = target?.name;
|
|
245
|
+
if (typeof name !== "string" || name.length === 0) {
|
|
246
|
+
throw new Error("extension factory returned a curation target without a name");
|
|
247
|
+
}
|
|
248
|
+
const tools = target?.tools;
|
|
249
|
+
if (!Array.isArray(tools) ||
|
|
250
|
+
tools.some((tool) => typeof tool !== "string" || tool.length === 0)) {
|
|
251
|
+
throw new Error(`extension factory returned a curation target "${name}" whose tools is not an array of non-empty strings`);
|
|
252
|
+
}
|
|
253
|
+
if (typeof target?.guidance !== "function") {
|
|
254
|
+
throw new Error(`extension factory returned a curation target "${name}" whose guidance is not a function`);
|
|
255
|
+
}
|
|
256
|
+
if (typeof target?.undo !== "function") {
|
|
257
|
+
throw new Error(`curation target "${name}" has no undo`);
|
|
258
|
+
}
|
|
259
|
+
if (target?.maintain !== undefined &&
|
|
260
|
+
typeof target.maintain !== "function") {
|
|
261
|
+
throw new Error(`extension factory returned a curation target "${name}" whose maintain is not a function`);
|
|
262
|
+
}
|
|
263
|
+
targets.push(entry);
|
|
264
|
+
}
|
|
265
|
+
return targets;
|
|
266
|
+
}
|
|
220
267
|
function validateUserServices(value) {
|
|
221
268
|
if (value === undefined)
|
|
222
269
|
return undefined;
|
|
@@ -1204,6 +1251,10 @@ async function resolvedFromFactory(factory, hostContext) {
|
|
|
1204
1251
|
}
|
|
1205
1252
|
// `workers` is a contract key — the candidate read needs no cast.
|
|
1206
1253
|
const workers = validateWorkers(candidate.workers);
|
|
1254
|
+
// Curation targets (curator P7): validated without the slug (the
|
|
1255
|
+
// qualified-id duplicate check runs in the load loop where the slug
|
|
1256
|
+
// and the already-claimed ids are known).
|
|
1257
|
+
const curationTargets = validateCurationTargets(candidate.curationTargets);
|
|
1207
1258
|
return {
|
|
1208
1259
|
...(candidate.perUserTools === undefined
|
|
1209
1260
|
? {}
|
|
@@ -1244,6 +1295,7 @@ async function resolvedFromFactory(factory, hostContext) {
|
|
|
1244
1295
|
? {}
|
|
1245
1296
|
: { ambientKinds: candidate.ambientKinds }),
|
|
1246
1297
|
...(workers === undefined ? {} : { workers }),
|
|
1298
|
+
...(curationTargets === undefined ? {} : { curationTargets }),
|
|
1247
1299
|
...(providers === undefined ? {} : { providers }),
|
|
1248
1300
|
...(loginProviders === undefined ? {} : { loginProviders }),
|
|
1249
1301
|
...(webuiBundles === undefined ? {} : { webuiBundles }),
|
|
@@ -1438,6 +1490,10 @@ export async function loadExtensions(specifiers, registry, options = {}) {
|
|
|
1438
1490
|
const settingsChangedBindings = [];
|
|
1439
1491
|
const lifecycleBindings = [];
|
|
1440
1492
|
const workerBindings = [];
|
|
1493
|
+
// Curation targets (curator P7): slug-attributed declarations, one
|
|
1494
|
+
// entry per loaded extension whose result carried them — boot folds
|
|
1495
|
+
// these into the ONE CurationRegistry (src/curation/targets.ts).
|
|
1496
|
+
const curationBindings = [];
|
|
1441
1497
|
const userServiceBindings = [];
|
|
1442
1498
|
// Jobs Task 3: declared jobs, attributed to the extension slug (the
|
|
1443
1499
|
// `ext:<slug>:<name>` id's namespace).
|
|
@@ -1475,6 +1531,11 @@ export async function loadExtensions(specifiers, registry, options = {}) {
|
|
|
1475
1531
|
// Slug → declared name, for collision rejection at load (this boot's
|
|
1476
1532
|
// loaded set only — quarantined extensions never claim a slug).
|
|
1477
1533
|
const slugs = new Map();
|
|
1534
|
+
// Qualified curation-target ids ("<slug>:<name>", curator P7): the same
|
|
1535
|
+
// claimed-only-by-LOADED pattern as `slugs` — a target id names ONE
|
|
1536
|
+
// target, so a duplicate quarantines the later declarer instead of
|
|
1537
|
+
// shadowing (never last-wins).
|
|
1538
|
+
const curationIds = new Set();
|
|
1478
1539
|
// Channel id → slug (gateway C1): the same claimed-only-by-LOADED pattern
|
|
1479
1540
|
// as `slugs`. Chat bindings are keyed by channel id, so a shadowing
|
|
1480
1541
|
// adapter would inherit another adapter's chats — the collision
|
|
@@ -1695,6 +1756,32 @@ export async function loadExtensions(specifiers, registry, options = {}) {
|
|
|
1695
1756
|
// install, exactly like the channel-id collision — a second engine
|
|
1696
1757
|
// leaves no registry entry, no slug claim, and no binding. First
|
|
1697
1758
|
// loaded wins.
|
|
1759
|
+
// Curation targets (curator P7): the two slug-dependent rules
|
|
1760
|
+
// check HERE — after the slug is final, BEFORE the registry
|
|
1761
|
+
// install (a violation leaves no entry, no slug claim, no
|
|
1762
|
+
// binding — the same point as the one-engine rule below).
|
|
1763
|
+
// A target's tools list must name tools THIS extension
|
|
1764
|
+
// registers (A8's wrapping rewrites registrations — an
|
|
1765
|
+
// unregistered name would wrap nothing); a qualified id
|
|
1766
|
+
// ("<slug>:<name>") names ONE target.
|
|
1767
|
+
if (resolved.curationTargets !== undefined) {
|
|
1768
|
+
const registered = new Set((extension?.tools ?? []).map((tool) => tool.name));
|
|
1769
|
+
// Claim each id AS it validates: an intra-list duplicate
|
|
1770
|
+
// (the same name twice in one result) hits the check on
|
|
1771
|
+
// its SECOND arrival, quarantining before any install.
|
|
1772
|
+
const claimed = new Set();
|
|
1773
|
+
for (const target of resolved.curationTargets) {
|
|
1774
|
+
const id = `${slug}:${target.name}`;
|
|
1775
|
+
if (curationIds.has(id) || claimed.has(id)) {
|
|
1776
|
+
throw new Error(`duplicate curation target id "${id}" — a qualified id names one curation target`);
|
|
1777
|
+
}
|
|
1778
|
+
claimed.add(id);
|
|
1779
|
+
const unregistered = target.tools.find((tool) => !registered.has(tool));
|
|
1780
|
+
if (unregistered !== undefined) {
|
|
1781
|
+
throw new Error(`extension "${slug}" declares curation target "${target.name}" listing tool "${unregistered}" it does not register`);
|
|
1782
|
+
}
|
|
1783
|
+
}
|
|
1784
|
+
}
|
|
1698
1785
|
if (resolved.gateway !== undefined) {
|
|
1699
1786
|
if (gatewayEngine !== undefined) {
|
|
1700
1787
|
throw new Error(`gateway engine already declared by "${gatewayEngine.extension}"`);
|
|
@@ -1702,7 +1789,23 @@ export async function loadExtensions(specifiers, registry, options = {}) {
|
|
|
1702
1789
|
gatewayEngine = { extension: slug, engine: resolved.gateway };
|
|
1703
1790
|
}
|
|
1704
1791
|
if (extension !== undefined) {
|
|
1705
|
-
|
|
1792
|
+
// Curator A8 (P9): grow the wrap feed with this extension's
|
|
1793
|
+
// just-validated target tools, then WRAP every curation-tool
|
|
1794
|
+
// registration at the install — its own target tools, plus any
|
|
1795
|
+
// feed name it registers (a non-core `skill_manage`). Growth
|
|
1796
|
+
// lives HERE, after every quarantining check, so the feed
|
|
1797
|
+
// never learns a tool whose extension failed the load. The
|
|
1798
|
+
// bindings below keep pairing with the UNWRAPPED object: they
|
|
1799
|
+
// carry identity, never tools, and the wrap changes only
|
|
1800
|
+
// `execute`.
|
|
1801
|
+
if (options.curationFeed !== undefined &&
|
|
1802
|
+
resolved.curationTargets !== undefined) {
|
|
1803
|
+
for (const target of resolved.curationTargets) {
|
|
1804
|
+
for (const tool of target.tools)
|
|
1805
|
+
options.curationFeed.add(tool);
|
|
1806
|
+
}
|
|
1807
|
+
}
|
|
1808
|
+
registry.install(wrapExtensionCurationTools(extension, options.curationFeed));
|
|
1706
1809
|
}
|
|
1707
1810
|
if (resolved.perUserTools === true)
|
|
1708
1811
|
perUserSlugs.push(slug);
|
|
@@ -1773,6 +1876,15 @@ export async function loadExtensions(specifiers, registry, options = {}) {
|
|
|
1773
1876
|
workers: resolved.workers,
|
|
1774
1877
|
});
|
|
1775
1878
|
}
|
|
1879
|
+
if (resolved.curationTargets !== undefined) {
|
|
1880
|
+
curationBindings.push({
|
|
1881
|
+
extension: slug,
|
|
1882
|
+
targets: resolved.curationTargets,
|
|
1883
|
+
});
|
|
1884
|
+
for (const target of resolved.curationTargets) {
|
|
1885
|
+
curationIds.add(`${slug}:${target.name}`);
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1776
1888
|
if (resolved.jobs !== undefined) {
|
|
1777
1889
|
jobBindings.push({ extension: slug, jobs: resolved.jobs });
|
|
1778
1890
|
}
|
|
@@ -1890,6 +2002,7 @@ export async function loadExtensions(specifiers, registry, options = {}) {
|
|
|
1890
2002
|
settingsChangedBindings,
|
|
1891
2003
|
lifecycleBindings,
|
|
1892
2004
|
workerBindings,
|
|
2005
|
+
curationBindings,
|
|
1893
2006
|
jobBindings,
|
|
1894
2007
|
routeBindings,
|
|
1895
2008
|
commandBindings,
|
package/dist/harness/naming.js
CHANGED
|
@@ -104,6 +104,12 @@ export function createAutoNamer(deps) {
|
|
|
104
104
|
// skip sits next to the isTaskOwned check per the plan.
|
|
105
105
|
if (meta?.source === "cron")
|
|
106
106
|
return;
|
|
107
|
+
// Internal conversations (curator P5) are never named: they are
|
|
108
|
+
// invisible on every owner-keyed surface, and a name would exist
|
|
109
|
+
// only to be hidden. The bus already withholds the settle event;
|
|
110
|
+
// this is the belt-and-braces read-side gate.
|
|
111
|
+
if (meta?.internal === true)
|
|
112
|
+
return;
|
|
107
113
|
// An absent doc (never written — reads serve nothing, defaults are
|
|
108
114
|
// free) is an unnamed conversation with zero attempts: name it.
|
|
109
115
|
if (meta?.name !== undefined || meta?.autoName !== undefined)
|
package/dist/harness/packages.js
CHANGED
|
@@ -949,13 +949,18 @@ export async function syncGit(source, ref, gitDir, options = {}) {
|
|
|
949
949
|
return target;
|
|
950
950
|
}
|
|
951
951
|
/**
|
|
952
|
-
* The
|
|
953
|
-
*
|
|
954
|
-
*
|
|
955
|
-
*
|
|
952
|
+
* The `.ts` skip warning (spec §9.4): a `pi.extensions` entry ending in
|
|
953
|
+
* `.ts` cannot load under plain node (the build spec removed the `.ts`
|
|
954
|
+
* entry fallback) — it is skipped, keeping the package's skills loadable.
|
|
955
|
+
* ONE line per skipped entry, per discovery.
|
|
956
|
+
*
|
|
957
|
+
* The wording says `pi` MANIFEST rather than "pi extension" on purpose:
|
|
958
|
+
* the `pi` key is read only to LOCATE a file. Sigrun does not implement
|
|
959
|
+
* pi's extension API, so this skip is not "pi extensions don't work here";
|
|
960
|
+
* it is "this package's entry is unimportable by plain node".
|
|
956
961
|
*/
|
|
957
962
|
function warnTsPiExtension(entry) {
|
|
958
|
-
console.warn(`pi
|
|
963
|
+
console.warn(`pi manifest: skipping TypeScript entry ${entry} (not loadable under node); loading the package's skills only`);
|
|
959
964
|
}
|
|
960
965
|
/** Read one package root's manifest (absent/unreadable → undefined). */
|
|
961
966
|
function readDiscoveryManifest(pkgRoot) {
|
|
@@ -979,9 +984,9 @@ function manifestStringArray(value) {
|
|
|
979
984
|
}
|
|
980
985
|
/**
|
|
981
986
|
* A package root's manifest skill dirs (spec §9.4): `sigrun.skills`
|
|
982
|
-
* (string[]) else pi
|
|
983
|
-
* so no warning), each resolved against the package root, keeping
|
|
984
|
-
* EXISTING directories. No manifest, no field, or nothing existing → [].
|
|
987
|
+
* (string[]) else the `pi`-shaped `pi.skills` (string[] — same SKILL.md
|
|
988
|
+
* format, so no warning), each resolved against the package root, keeping
|
|
989
|
+
* only EXISTING directories. No manifest, no field, or nothing existing → [].
|
|
985
990
|
*/
|
|
986
991
|
export function manifestSkills(pkgRoot) {
|
|
987
992
|
const manifest = readDiscoveryManifest(pkgRoot);
|
|
@@ -1035,7 +1040,7 @@ export function manifestEntry(pkgRoot) {
|
|
|
1035
1040
|
}
|
|
1036
1041
|
return join(pkgRoot, entry);
|
|
1037
1042
|
}
|
|
1038
|
-
console.warn(`pi
|
|
1043
|
+
console.warn(`pi manifest: no loadable entry in ${pkgRoot} (all entries are TypeScript); loading the package's skills only`);
|
|
1039
1044
|
}
|
|
1040
1045
|
return undefined;
|
|
1041
1046
|
}
|
|
@@ -21,6 +21,7 @@ export const RESERVED_CORE_SECTION_KEYS = [
|
|
|
21
21
|
"skills",
|
|
22
22
|
"tool-guidance",
|
|
23
23
|
"execution-guidance",
|
|
24
|
+
"job-state",
|
|
24
25
|
];
|
|
25
26
|
/**
|
|
26
27
|
* Core-owned directory names directly under a store root (multi-user M1).
|
|
@@ -31,6 +32,7 @@ export const RESERVED_CORE_SECTION_KEYS = [
|
|
|
31
32
|
*/
|
|
32
33
|
export const RESERVED_EXTENSION_SLUGS = [
|
|
33
34
|
"users",
|
|
35
|
+
"jobs",
|
|
34
36
|
"workspace",
|
|
35
37
|
"models",
|
|
36
38
|
"sandbox",
|
|
@@ -38,6 +40,8 @@ export const RESERVED_EXTENSION_SLUGS = [
|
|
|
38
40
|
"user-settings",
|
|
39
41
|
"user-secrets",
|
|
40
42
|
"skills",
|
|
43
|
+
"versions",
|
|
44
|
+
"archive",
|
|
41
45
|
"prompts",
|
|
42
46
|
"packages",
|
|
43
47
|
"git",
|
|
@@ -37,6 +37,28 @@ export class SeamError extends Error {
|
|
|
37
37
|
* boot-built store trio (one per boot, from the resolved roots); the
|
|
38
38
|
* host scopes it to its identity's slug — `rekey` re-scopes in place.
|
|
39
39
|
*/
|
|
40
|
+
/**
|
|
41
|
+
* The pre-bind forwarder for the curation surface's SYNC store members
|
|
42
|
+
* (`skills(user)`, `overlays(user)`): returns the store handle at once;
|
|
43
|
+
* every method call awaits the bridge first (the shared pending promise
|
|
44
|
+
* carries the bind deadline). Method names are read off the resolved
|
|
45
|
+
* store at call time, so the forwarder stays shape-stable.
|
|
46
|
+
*/
|
|
47
|
+
function syncStoreForwarder(pending, member, user) {
|
|
48
|
+
return new Proxy({}, {
|
|
49
|
+
get(_t, method) {
|
|
50
|
+
return async (...args) => {
|
|
51
|
+
const resolvedHost = (await pending);
|
|
52
|
+
const store = resolvedHost[member](user);
|
|
53
|
+
const fn = store[method];
|
|
54
|
+
if (typeof fn !== "function") {
|
|
55
|
+
throw new Error(`curation: store ${member}(${user}) method ${String(method)} is not callable`);
|
|
56
|
+
}
|
|
57
|
+
return fn.apply(store, [...args]);
|
|
58
|
+
};
|
|
59
|
+
},
|
|
60
|
+
});
|
|
61
|
+
}
|
|
40
62
|
export function createSeamHost(identity, kinds, bind, sharedStores, perUserTools, services) {
|
|
41
63
|
const interactionKinds = new Set(kinds.interaction ?? []);
|
|
42
64
|
const ambientKinds = new Set(kinds.ambient ?? []);
|
|
@@ -79,10 +101,31 @@ export function createSeamHost(identity, kinds, bind, sharedStores, perUserTools
|
|
|
79
101
|
const bridgePromise = new Promise((resolve) => {
|
|
80
102
|
bridgeReleased = resolve;
|
|
81
103
|
});
|
|
82
|
-
|
|
104
|
+
// The pre-bind bound: a bridge call that waits PAST this long fails
|
|
105
|
+
// loud instead of hanging forever (the jobs-task fake-clock hang's
|
|
106
|
+
// lesson). Production never sees it — `createServer` binds the bridge
|
|
107
|
+
// at boot, long inside the bound — but a serverless embed that never
|
|
108
|
+
// builds a server would otherwise await an eternally pending promise.
|
|
109
|
+
const bridgeDeadlineMs = 60_000;
|
|
110
|
+
const hostBridge = async (member) => {
|
|
83
111
|
await bind.harness();
|
|
84
|
-
return
|
|
112
|
+
return await Promise.race([
|
|
113
|
+
bridgePromise,
|
|
114
|
+
new Promise((_, reject) => {
|
|
115
|
+
const timer = setTimeout(() => {
|
|
116
|
+
reject(new Error(`seam host: ${member} — the bridge never bound after ${bridgeDeadlineMs}ms; is createServer running? (caller: ${identity.name})`));
|
|
117
|
+
}, bridgeDeadlineMs);
|
|
118
|
+
// The rejection timer never keeps the process alive.
|
|
119
|
+
if (typeof timer === "object" && "unref" in timer)
|
|
120
|
+
timer.unref();
|
|
121
|
+
}),
|
|
122
|
+
]);
|
|
85
123
|
};
|
|
124
|
+
// The curation surface's settled bridge host (curator P6/A9): memoized
|
|
125
|
+
// HERE — in the host closure — so every `host.curation` getter call (a
|
|
126
|
+
// FRESH proxy per read) shares one settled reference. See the getter's
|
|
127
|
+
// own comment for the sync-member discipline this serves.
|
|
128
|
+
let curationHost;
|
|
86
129
|
return {
|
|
87
130
|
get stores() {
|
|
88
131
|
return { ...stores, user: userStores };
|
|
@@ -273,103 +316,188 @@ export function createSeamHost(identity, kinds, bind, sharedStores, perUserTools
|
|
|
273
316
|
// re-keyed current slug, read at call time).
|
|
274
317
|
conversations: {
|
|
275
318
|
async create(opts = {}) {
|
|
276
|
-
const bridge = await hostBridge();
|
|
319
|
+
const bridge = await hostBridge("conversations.create");
|
|
277
320
|
return bridge.conversations.create({
|
|
278
321
|
...(opts.owner === undefined ? {} : { owner: opts.owner }),
|
|
279
322
|
...(opts.sharing === undefined ? {} : { sharing: opts.sharing }),
|
|
280
323
|
...(opts.name === undefined ? {} : { name: opts.name }),
|
|
324
|
+
...(opts.job === undefined ? {} : { job: opts.job }),
|
|
281
325
|
...(opts.agent === undefined ? {} : { agent: opts.agent }),
|
|
282
326
|
// The source default: the CALLING extension's slug (spec §9.5).
|
|
283
327
|
source: opts.source ?? identity.name,
|
|
284
328
|
});
|
|
285
329
|
},
|
|
286
330
|
async submit(id, input, opts) {
|
|
287
|
-
const bridge = await hostBridge();
|
|
331
|
+
const bridge = await hostBridge("conversations.submit");
|
|
288
332
|
return bridge.conversations.submit(id, input, opts);
|
|
289
333
|
},
|
|
290
334
|
async watch(id, listener) {
|
|
291
|
-
const bridge = await hostBridge();
|
|
335
|
+
const bridge = await hostBridge("conversations.watch");
|
|
292
336
|
return bridge.conversations.watch(id, listener);
|
|
293
337
|
},
|
|
294
338
|
async list(opts) {
|
|
295
|
-
const bridge = await hostBridge();
|
|
339
|
+
const bridge = await hostBridge("conversations.list");
|
|
296
340
|
return bridge.conversations.list(opts);
|
|
297
341
|
},
|
|
298
342
|
async meta(id) {
|
|
299
|
-
const bridge = await hostBridge();
|
|
343
|
+
const bridge = await hostBridge("conversations.meta");
|
|
300
344
|
return bridge.conversations.meta(id);
|
|
301
345
|
},
|
|
346
|
+
async entries(id, query, limit, cursor) {
|
|
347
|
+
return (await hostBridge("conversations.entries")).conversations.entries(id, query, limit, cursor);
|
|
348
|
+
},
|
|
349
|
+
async entry(id, entryId) {
|
|
350
|
+
return (await hostBridge("conversations.entry")).conversations.entry(id, entryId);
|
|
351
|
+
},
|
|
302
352
|
// The C4a ops: straight delegation (the bridge resolves the id and
|
|
303
353
|
// runs the op its HTTP twin runs).
|
|
304
354
|
async abort(id) {
|
|
305
|
-
return (await hostBridge()).conversations.abort(id);
|
|
355
|
+
return (await hostBridge("conversations.abort")).conversations.abort(id);
|
|
306
356
|
},
|
|
307
357
|
async configure(id, opts) {
|
|
308
|
-
return (await hostBridge()).conversations.configure(id, opts);
|
|
358
|
+
return (await hostBridge("conversations.configure")).conversations.configure(id, opts);
|
|
309
359
|
},
|
|
310
360
|
async compact(id) {
|
|
311
|
-
return (await hostBridge()).conversations.compact(id);
|
|
361
|
+
return (await hostBridge("conversations.compact")).conversations.compact(id);
|
|
312
362
|
},
|
|
313
363
|
async fork(id, opts) {
|
|
314
|
-
return (await hostBridge()).conversations.fork(id, opts);
|
|
364
|
+
return (await hostBridge("conversations.fork")).conversations.fork(id, opts);
|
|
315
365
|
},
|
|
316
366
|
async patchMeta(id, patch) {
|
|
317
|
-
return (await hostBridge()).conversations.patchMeta(id, patch);
|
|
367
|
+
return (await hostBridge("conversations.patchMeta")).conversations.patchMeta(id, patch);
|
|
318
368
|
},
|
|
319
369
|
// The C4b ops: same straight delegation.
|
|
320
370
|
async upload(id, file) {
|
|
321
|
-
return (await hostBridge()).conversations.upload(id, file);
|
|
371
|
+
return (await hostBridge("conversations.upload")).conversations.upload(id, file);
|
|
322
372
|
},
|
|
323
373
|
async readFile(id, path) {
|
|
324
|
-
return (await hostBridge()).conversations.readFile(id, path);
|
|
374
|
+
return (await hostBridge("conversations.readFile")).conversations.readFile(id, path);
|
|
375
|
+
},
|
|
376
|
+
},
|
|
377
|
+
jobs: {
|
|
378
|
+
async get(id) {
|
|
379
|
+
return (await hostBridge("jobs.get")).jobs.get(id);
|
|
325
380
|
},
|
|
326
381
|
},
|
|
327
382
|
users: {
|
|
328
383
|
async list() {
|
|
329
|
-
return (await hostBridge()).users.list();
|
|
384
|
+
return (await hostBridge("users.list")).users.list();
|
|
330
385
|
},
|
|
331
386
|
async get(user) {
|
|
332
|
-
return (await hostBridge()).users.get(user);
|
|
387
|
+
return (await hostBridge("users.get")).users.get(user);
|
|
333
388
|
},
|
|
334
389
|
},
|
|
335
390
|
accounts: {
|
|
336
391
|
async lookup(provider, subject) {
|
|
337
|
-
return (await hostBridge()).accounts.lookup(provider, subject);
|
|
392
|
+
return (await hostBridge("accounts.lookup")).accounts.lookup(provider, subject);
|
|
338
393
|
},
|
|
339
394
|
async link(provider, subject, user, via) {
|
|
340
|
-
return (await hostBridge()).accounts.link(provider, subject, user, via);
|
|
395
|
+
return (await hostBridge("accounts.link")).accounts.link(provider, subject, user, via);
|
|
341
396
|
},
|
|
342
397
|
async unlink(provider, subject) {
|
|
343
|
-
return (await hostBridge()).accounts.unlink(provider, subject);
|
|
398
|
+
return (await hostBridge("accounts.unlink")).accounts.unlink(provider, subject);
|
|
344
399
|
},
|
|
345
400
|
async listForUser(user) {
|
|
346
|
-
return (await hostBridge()).accounts.listForUser(user);
|
|
401
|
+
return (await hostBridge("accounts.listForUser")).accounts.listForUser(user);
|
|
347
402
|
},
|
|
348
403
|
},
|
|
349
404
|
access: {
|
|
350
405
|
async accessOf(principal, resource) {
|
|
351
|
-
return (await hostBridge()).access.accessOf(principal, resource);
|
|
406
|
+
return (await hostBridge("access.accessOf")).access.accessOf(principal, resource);
|
|
352
407
|
},
|
|
353
408
|
},
|
|
354
409
|
identity: {
|
|
355
410
|
async actingUser(conversationId) {
|
|
356
|
-
return (await hostBridge()).identity.actingUser(conversationId);
|
|
411
|
+
return (await hostBridge("identity.actingUser")).identity.actingUser(conversationId);
|
|
357
412
|
},
|
|
358
413
|
},
|
|
414
|
+
// Curator P6 (A9): the boot-built curation host through the bridge —
|
|
415
|
+
// the lazy member discipline (a factory-body call waits for the
|
|
416
|
+
// bridge; a post-bind call resolves at once). Every member of the
|
|
417
|
+
// curation surface is a function, so a Proxy that forwards each
|
|
418
|
+
// property read (and call) to the RESOLVED bridge host implements
|
|
419
|
+
// the whole surface with the same wait semantics as the async
|
|
420
|
+
// delegations above. Members that return SYNCHRONOUS store objects
|
|
421
|
+
// (`skills(user)`, `overlays(user)` — store handles whose own methods
|
|
422
|
+
// are async) must come back UNAWAITED: an `async` wrapper would
|
|
423
|
+
// return a Promise of the store, and the caller's sync `.list()` —
|
|
424
|
+
// the shape the view and the targets use — would read undefined
|
|
425
|
+
// (D1's e2e found the view rendering 500 on exactly this). The
|
|
426
|
+
// settled host is memoized in the HOST closure (not the getter —
|
|
427
|
+
// every `host.curation` read returns a FRESH proxy, and a per-proxy
|
|
428
|
+
// memo would reset on each read, putting the async forwarder back
|
|
429
|
+
// in front of sync members on the second read).
|
|
430
|
+
get curation() {
|
|
431
|
+
if (curationHost !== undefined)
|
|
432
|
+
return curationHost;
|
|
433
|
+
const pending = hostBridge("curation").then((bridge) => {
|
|
434
|
+
curationHost = bridge.curation;
|
|
435
|
+
return bridge.curation;
|
|
436
|
+
});
|
|
437
|
+
// SAFETY: the Proxy target satisfies the structural member shapes
|
|
438
|
+
// because every forwarded call is an `apply` trap on the real host's
|
|
439
|
+
// own member; the cast only bridges the contract's named members.
|
|
440
|
+
return new Proxy({}, {
|
|
441
|
+
get(_target, property) {
|
|
442
|
+
// The settled-bridge fast path: the member itself, bound — a
|
|
443
|
+
// synchronous member (`skills`) returns its store synchronously,
|
|
444
|
+
// an async one keeps returning its promise. The property read is
|
|
445
|
+
// STILL a function either way (the surface has no data members),
|
|
446
|
+
// so callers observe the contract shape unchanged.
|
|
447
|
+
if (curationHost !== undefined) {
|
|
448
|
+
const member = curationHost[property];
|
|
449
|
+
if (typeof member !== "function") {
|
|
450
|
+
throw new Error(`curation: member ${String(property)} is not callable`);
|
|
451
|
+
}
|
|
452
|
+
return member.bind(curationHost);
|
|
453
|
+
}
|
|
454
|
+
// The pre-bind window: every member becomes a forwarder that
|
|
455
|
+
// waits for the bridge (the factory-body discipline). The
|
|
456
|
+
// surface's SYNC store members — `skills(user)` and
|
|
457
|
+
// `overlays(user)` return store handles whose own methods are
|
|
458
|
+
// async (the shape the view and the steps call: `.list()` with
|
|
459
|
+
// no await on the store itself) — must come back as a STORE,
|
|
460
|
+
// not a promise of one (the eval's idle pass hit exactly this:
|
|
461
|
+
// the archive step read `.list` off a promise). A lazy store
|
|
462
|
+
// whose methods each await the bridge preserves the
|
|
463
|
+
// sync-return contract while keeping the wait: the caller
|
|
464
|
+
// gets the store immediately and its first method call
|
|
465
|
+
// resolves once the bridge lands (or rejects at the bind
|
|
466
|
+
// deadline through the shared pending promise).
|
|
467
|
+
if (property === "skills" || property === "overlays") {
|
|
468
|
+
return (user) => syncStoreForwarder(pending, property, user);
|
|
469
|
+
}
|
|
470
|
+
return async (...args) => {
|
|
471
|
+
const resolvedHost = await pending;
|
|
472
|
+
// SAFETY: the property is one of the contract's own function
|
|
473
|
+
// members (the surface has no data members); the bound call is
|
|
474
|
+
// the real host's, typed at the call site.
|
|
475
|
+
const member = resolvedHost[property];
|
|
476
|
+
if (typeof member !== "function") {
|
|
477
|
+
throw new Error(`curation: member ${String(property)} is not callable`);
|
|
478
|
+
}
|
|
479
|
+
return member.apply(resolvedHost, [...args]);
|
|
480
|
+
};
|
|
481
|
+
},
|
|
482
|
+
});
|
|
483
|
+
},
|
|
359
484
|
// Gateway C4b: straight delegation (the bridge owns both bodies).
|
|
360
485
|
models: {
|
|
361
486
|
async list() {
|
|
362
|
-
return (await hostBridge()).models.list();
|
|
487
|
+
return (await hostBridge("models.list")).models.list();
|
|
363
488
|
},
|
|
364
489
|
},
|
|
365
490
|
commands: {
|
|
366
491
|
async list() {
|
|
367
|
-
return (await hostBridge()).commands.list();
|
|
492
|
+
return (await hostBridge("commands.list")).commands.list();
|
|
368
493
|
},
|
|
369
494
|
async run(conversationId, name, args, opts) {
|
|
370
|
-
return (await hostBridge()).commands.run(conversationId, name, args, opts);
|
|
495
|
+
return (await hostBridge("commands.run")).commands.run(conversationId, name, args, opts);
|
|
371
496
|
},
|
|
372
497
|
},
|
|
498
|
+
async notify(input) {
|
|
499
|
+
return (await hostBridge("notify")).notify(identity.name, input);
|
|
500
|
+
},
|
|
373
501
|
// The surfaces seam (UI-blocks U3 Task 4): `changed` awaits the
|
|
374
502
|
// bridge (so a factory-body call waits, never resolves early) and
|
|
375
503
|
// delegates to the server-side SurfaceService — the rev bump + the
|
|
@@ -385,7 +513,7 @@ export function createSeamHost(identity, kinds, bind, sharedStores, perUserTools
|
|
|
385
513
|
// production (nothing sets it).
|
|
386
514
|
globalThis.__sigrunSurfaceChanged?.(host.name);
|
|
387
515
|
};
|
|
388
|
-
void hostBridge()
|
|
516
|
+
void hostBridge("surfaces.changed")
|
|
389
517
|
.then((bridge) => {
|
|
390
518
|
ran();
|
|
391
519
|
bridge.surfaces.changed(identity.name, id, key, update);
|
|
@@ -45,10 +45,13 @@ async function readContextFile(input, fileName, context) {
|
|
|
45
45
|
/**
|
|
46
46
|
* The static `identity` section body: runtime facts only. No tool claims
|
|
47
47
|
* (the system prompt already declares the actually-offered tools per
|
|
48
|
-
* request)
|
|
49
|
-
* mid-conversation model swap, defeating the minimal-patch design)
|
|
48
|
+
* request), no model name (it would re-emit this section on every
|
|
49
|
+
* mid-conversation model swap, defeating the minimal-patch design), and
|
|
50
|
+
* no NAME: what the agent is called is `SOUL.md` territory, since a user
|
|
51
|
+
* may choose a different name for their own agent. Durability is runtime
|
|
52
|
+
* truth, not identity copy — it stays out of this section.
|
|
50
53
|
*/
|
|
51
|
-
export const IDENTITY_TEXT = "You are
|
|
54
|
+
export const IDENTITY_TEXT = "You are a small but extensible multi-user AI assistant built on the pi-durable harness. Conversations can span channels (web UI, terminal UI, HTTP API), and multiple independent conversations may be active at once.";
|
|
52
55
|
/**
|
|
53
56
|
* The persona sigrun ships when no `SOUL.md` exists. A present `SOUL.md`
|
|
54
57
|
* replaces it wholesale; the default is never merged in.
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/** The persistent state folder shared by every run of one job. */
|
|
2
|
+
import { defineExtension, section } from "@earendil-works/pi-durable";
|
|
3
|
+
import { jobStateDir } from "../../jobs/state-dir.js";
|
|
4
|
+
import { SessionMetaDoc } from "../../server/session-meta.js";
|
|
5
|
+
/** Name a new-conversation job run's persistent, per-owner state folder. */
|
|
6
|
+
export function createJobStateExtension(deps) {
|
|
7
|
+
return defineExtension({
|
|
8
|
+
name: "sigrun.job-state",
|
|
9
|
+
sections: [
|
|
10
|
+
section("job-state", async (input, context) => {
|
|
11
|
+
const meta = await input.read.snapshot(SessionMetaDoc, input.conversationId, context);
|
|
12
|
+
if (meta?.jobId === undefined)
|
|
13
|
+
return undefined;
|
|
14
|
+
const job = await deps.job(meta.jobId);
|
|
15
|
+
if (job === undefined)
|
|
16
|
+
return undefined;
|
|
17
|
+
const root = deps.jobsRoot(meta.owner);
|
|
18
|
+
if (root === undefined)
|
|
19
|
+
return undefined;
|
|
20
|
+
return `Job state: this conversation is a run of job "${job.name}" (${meta.jobId}). Its state folder is ${jobStateDir(root, meta.jobId)} — create it if missing. Use it to keep notes between runs (for example, when an issue was first seen). Other runs of this job use the same folder; write files atomically.`;
|
|
21
|
+
}),
|
|
22
|
+
],
|
|
23
|
+
});
|
|
24
|
+
}
|
package/dist/harness/settings.js
CHANGED
|
@@ -368,8 +368,8 @@ function validate(value) {
|
|
|
368
368
|
if (builtins === undefined) {
|
|
369
369
|
throw new SettingsValidationError("builtinExtensions", "must be an object");
|
|
370
370
|
}
|
|
371
|
-
checkKeys("builtinExtensions.", builtins, ["codemode", "mcp", "toolSearch", "gateway", "oauth"]);
|
|
372
|
-
for (const key of ["codemode", "mcp", "toolSearch", "gateway", "oauth"]) {
|
|
371
|
+
checkKeys("builtinExtensions.", builtins, ["codemode", "mcp", "toolSearch", "gateway", "oauth", "curator"]);
|
|
372
|
+
for (const key of ["codemode", "mcp", "toolSearch", "gateway", "oauth", "curator"]) {
|
|
373
373
|
if (builtins[key] !== undefined && typeof builtins[key] !== "boolean") {
|
|
374
374
|
throw new SettingsValidationError(`builtinExtensions.${key}`, "must be a boolean");
|
|
375
375
|
}
|