weir-harness 1.1.6 → 1.2.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/lib/client.blackboard-model.js +35 -2
- package/lib/client.blackboard-panel.js +61 -9
- package/lib/client.js +254 -223
- package/lib/client.settings-page.js +132 -46
- package/package.json +1 -1
- package/src/blackboard/durable.js +13 -0
- package/src/blackboard/index.js +45 -1
- package/src/blackboard/remote.js +69 -16
- package/src/blackboard/storage.js +56 -0
- package/src/edit-lock/binding-guidance.js +15 -4
- package/src/edit-lock/domain.js +12 -0
- package/src/edit-lock/domains.js +67 -0
- package/src/edit-lock/index.js +209 -45
- package/src/edit-lock/lifecycle.js +70 -4
- package/src/edit-lock/remote.js +23 -4
- package/src/settings/sections.js +25 -16
|
@@ -53,33 +53,43 @@ window.__ModuleLoader__.load({
|
|
|
53
53
|
const CLASSIFIER_JEV = Object.freeze({ key: "intentGateClassifier", equals: "jev" });
|
|
54
54
|
const WINDOWS_ONLY = Object.freeze({ env: "platform", in: Object.freeze(["win32"]) });
|
|
55
55
|
const ROBASH_WINDOWS = Object.freeze({ all: Object.freeze([enabledWhen("robashEnabled"), WINDOWS_ONLY]) });
|
|
56
|
-
|
|
57
|
-
//
|
|
58
|
-
//
|
|
59
|
-
//
|
|
60
|
-
//
|
|
61
|
-
//
|
|
62
|
-
//
|
|
63
|
-
//
|
|
64
|
-
//
|
|
65
|
-
//
|
|
66
|
-
//
|
|
67
|
-
//
|
|
56
|
+
const CLASSIFIER_LLM_OR_JEV = Object.freeze({ any: Object.freeze([CLASSIFIER_LLM, CLASSIFIER_JEV]) });
|
|
57
|
+
// The GROUPS field table. Every group: { id, page?, fields }. page is
|
|
58
|
+
// "main" (default when absent) or "routing" — the model-routing groups
|
|
59
|
+
// form the separate routing settings page (WeirCard/WeirCardController
|
|
60
|
+
// filter by it; validateLayout rejects any other value). Every node:
|
|
61
|
+
// { field?, kind, values?, display?, when?, parent?, folded?, slot? }.
|
|
62
|
+
// kind: boolean | enum | number | text | custom. An enum node may set
|
|
63
|
+
// `display: "select"` to render a compact native dropdown instead of the
|
|
64
|
+
// segmented control (long option labels squeeze the row label). `custom`
|
|
65
|
+
// nodes carry a render `slot` identifier and share parent/when with
|
|
66
|
+
// plain rows; a custom node with a `field` is text-backed at the form
|
|
67
|
+
// layer. `folded` fields stay in the form but render inside another row
|
|
68
|
+
// (the model picker folds intentGateModel / intentGateReasoningEffort
|
|
69
|
+
// into the intentGateProvider row; their `when` matches the carrier row
|
|
70
|
+
// by declaration). Field-less custom nodes (maintenance/permission
|
|
71
|
+
// panels) hang in the same tree. Keys marked page:false in
|
|
72
|
+
// src/settings/sections.js (CONFIG_FACE_ONLY_KEYS) never appear here —
|
|
73
|
+
// config-face-only by declaration, pinned by test.
|
|
68
74
|
const GROUPS = [
|
|
69
75
|
{ id: "intent", fields: [
|
|
70
76
|
{ field: "intentGateClassifier", kind: "enum", values: ["regex", "llm", "jev"] },
|
|
71
77
|
{ field: "intentGateProvider", kind: "custom", slot: "modelPicker", when: CLASSIFIER_LLM },
|
|
72
78
|
{ field: "intentGateModel", kind: "text", folded: true, when: CLASSIFIER_LLM },
|
|
73
79
|
{ field: "intentGateReasoningEffort", kind: "text", folded: true, when: CLASSIFIER_LLM },
|
|
74
|
-
{ field: "intentGateTimeoutMs", kind: "number" },
|
|
80
|
+
{ field: "intentGateTimeoutMs", kind: "number", when: CLASSIFIER_LLM_OR_JEV },
|
|
75
81
|
{ field: "jevEndpoint", kind: "text", when: CLASSIFIER_JEV },
|
|
76
82
|
{ field: "jevModel", kind: "text", when: CLASSIFIER_JEV },
|
|
77
83
|
{ field: "jevApiKeyEnv", kind: "text", when: CLASSIFIER_JEV }
|
|
78
84
|
] },
|
|
79
|
-
{ id: "
|
|
85
|
+
{ id: "routingCategories", page: "routing", fields: [
|
|
80
86
|
{ field: "delegateCategoryChains", kind: "custom", slot: "chainEditor" },
|
|
81
|
-
{ field: "
|
|
82
|
-
|
|
87
|
+
{ field: "delegateDisabledCategories", kind: "custom", slot: "disabledCategoriesEditor" }
|
|
88
|
+
] },
|
|
89
|
+
{ id: "routingAgents", page: "routing", fields: [
|
|
90
|
+
{ field: "delegateAgentChains", kind: "custom", slot: "agentChainEditor" }
|
|
91
|
+
] },
|
|
92
|
+
{ id: "routingSupervision", page: "routing", fields: [
|
|
83
93
|
{ field: "supervisionMaxRetries", kind: "number" },
|
|
84
94
|
{ field: "supervisionInitialBackoffMs", kind: "number" },
|
|
85
95
|
{ field: "supervisionMaxBackoffMs", kind: "number" }
|
|
@@ -87,9 +97,7 @@ window.__ModuleLoader__.load({
|
|
|
87
97
|
{ id: "todo", fields: [
|
|
88
98
|
{ field: "todoEnabled", kind: "boolean" },
|
|
89
99
|
{ field: "todoMaxConsecutive", kind: "number", parent: "todoEnabled", when: enabledWhen("todoEnabled") },
|
|
90
|
-
{ field: "todoErrorRetryMax", kind: "number", parent: "todoEnabled", when: enabledWhen("todoEnabled") }
|
|
91
|
-
{ field: "todoErrorBackoffBaseMs", kind: "number", parent: "todoEnabled", when: enabledWhen("todoEnabled") },
|
|
92
|
-
{ field: "todoErrorBackoffCapMs", kind: "number", parent: "todoEnabled", when: enabledWhen("todoEnabled") }
|
|
100
|
+
{ field: "todoErrorRetryMax", kind: "number", parent: "todoEnabled", when: enabledWhen("todoEnabled") }
|
|
93
101
|
] },
|
|
94
102
|
{ id: "guard", fields: [
|
|
95
103
|
{ field: "guardEnabled", kind: "boolean" },
|
|
@@ -101,11 +109,6 @@ window.__ModuleLoader__.load({
|
|
|
101
109
|
{ field: "editLockEnabled", kind: "boolean" },
|
|
102
110
|
{ field: "editLockAutoResume", kind: "boolean", parent: "editLockEnabled", when: enabledWhen("editLockEnabled") },
|
|
103
111
|
{ field: "editLockStaleSweep", kind: "boolean", parent: "editLockEnabled", when: enabledWhen("editLockEnabled") },
|
|
104
|
-
{ field: "editLockHoldDefaultMinutes", kind: "number", parent: "editLockEnabled", when: enabledWhen("editLockEnabled") },
|
|
105
|
-
{ field: "editLockHoldSingleMaxMinutes", kind: "number", parent: "editLockEnabled", when: enabledWhen("editLockEnabled") },
|
|
106
|
-
{ field: "editLockHoldCumulativeMaxMinutes", kind: "number", parent: "editLockEnabled", when: enabledWhen("editLockEnabled") },
|
|
107
|
-
{ field: "editLockNudgeAttempts", kind: "number", parent: "editLockEnabled", when: enabledWhen("editLockEnabled") },
|
|
108
|
-
{ field: "editLockNudgeFallback", kind: "text", parent: "editLockEnabled", when: enabledWhen("editLockEnabled") },
|
|
109
112
|
// Diagnostics must remain reachable when enforcement is off.
|
|
110
113
|
{ kind: "custom", slot: "editLockMaintenance" }
|
|
111
114
|
] },
|
|
@@ -127,9 +130,6 @@ window.__ModuleLoader__.load({
|
|
|
127
130
|
] },
|
|
128
131
|
{ id: "lsp", fields: [
|
|
129
132
|
{ field: "lspEnabled", kind: "boolean" },
|
|
130
|
-
{ field: "lspIdleMs", kind: "number", parent: "lspEnabled", when: enabledWhen("lspEnabled") },
|
|
131
|
-
{ field: "lspRequestTimeoutMs", kind: "number", parent: "lspEnabled", when: enabledWhen("lspEnabled") },
|
|
132
|
-
{ field: "lspDiagnosticsWaitMs", kind: "number", parent: "lspEnabled", when: enabledWhen("lspEnabled") },
|
|
133
133
|
// The raw lspServers JSON row is folded into the manager row: one
|
|
134
134
|
// custom node carrying the field (the robashList pattern —
|
|
135
135
|
// text-backed at the form layer via specFor), so the panel owns the
|
|
@@ -149,19 +149,25 @@ window.__ModuleLoader__.load({
|
|
|
149
149
|
// confirms its platform (self-gating), so it stays unconditional
|
|
150
150
|
// here even as a custom node in the tree.
|
|
151
151
|
{ kind: "custom", slot: "notifyPermissions" }
|
|
152
|
-
] },
|
|
153
|
-
// Session blackboard (2.4a): the write-token TTL leaves
|
|
154
|
-
// config-face-only and rides its own group. A single live number row
|
|
155
|
-
// (volatile config — no restart marker, no master switch).
|
|
156
|
-
{ id: "blackboard", fields: [
|
|
157
|
-
{ field: "blackboardWriteTokenTtlMinutes", kind: "number" }
|
|
158
152
|
] }
|
|
159
153
|
];
|
|
160
154
|
// Form fields: every node that names a flat settings key (custom
|
|
161
|
-
// panels without a field render only)
|
|
162
|
-
//
|
|
155
|
+
// panels without a field render only), in GROUPS declaration order
|
|
156
|
+
// across both pages — test pins this list.
|
|
163
157
|
const FIELDS = GROUPS.flatMap((group) => group.fields).filter((descriptor) => typeof descriptor.field === "string");
|
|
164
158
|
const FIELD_BY_NAME = Object.fromEntries(FIELDS.map((descriptor) => [descriptor.field, descriptor]));
|
|
159
|
+
// The page one field's group belongs to ("main" when the group declares
|
|
160
|
+
// no page): the controller's form specs and projection cover exactly the
|
|
161
|
+
// page's fields, and WeirCard renders only the page's groups.
|
|
162
|
+
const FIELD_PAGE = new Map();
|
|
163
|
+
for (const group of GROUPS) {
|
|
164
|
+
for (const node of group.fields) {
|
|
165
|
+
if (typeof node.field === "string") FIELD_PAGE.set(node.field, group.page ?? "main");
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
function fieldsForPage(page) {
|
|
169
|
+
return FIELDS.filter((descriptor) => FIELD_PAGE.get(descriptor.field) === page);
|
|
170
|
+
}
|
|
165
171
|
/** Product defaults of every flat settings key with a concrete
|
|
166
172
|
* default — hand-maintained mirror of FIELD_DEFAULTS in
|
|
167
173
|
* src/settings/sections.js (same-package sync require is impossible
|
|
@@ -352,8 +358,14 @@ window.__ModuleLoader__.load({
|
|
|
352
358
|
}
|
|
353
359
|
/** Fail-loud declaration validation, run at module load: unknown or
|
|
354
360
|
* cross-group parents, parent cycles, conditions on unknown keys or
|
|
355
|
-
* unknown env facts — every error names the
|
|
361
|
+
* unknown env facts, an invalid group page — every error names the
|
|
362
|
+
* offending key or group. */
|
|
356
363
|
function validateLayout(groups) {
|
|
364
|
+
for (const group of groups) {
|
|
365
|
+
if (group.page !== void 0 && group.page !== "main" && group.page !== "routing") {
|
|
366
|
+
throw settingsLayoutError(`group '${String(group.id)}' declares an invalid page '${String(group.page)}'`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
357
369
|
const keyToGroup = new Map();
|
|
358
370
|
for (const group of groups) {
|
|
359
371
|
for (const node of group.fields) {
|
|
@@ -409,7 +421,7 @@ window.__ModuleLoader__.load({
|
|
|
409
421
|
for (const child of node.children) assignDepth(child, depth + 1);
|
|
410
422
|
};
|
|
411
423
|
for (const root of roots) assignDepth(root, 0);
|
|
412
|
-
return { id: group.id, roots };
|
|
424
|
+
return { id: group.id, page: group.page ?? "main", roots };
|
|
413
425
|
});
|
|
414
426
|
}
|
|
415
427
|
// Declaration-time validation (D4): a bad GROUPS table fails chunk load
|
|
@@ -455,7 +467,12 @@ window.__ModuleLoader__.load({
|
|
|
455
467
|
readOnly: t("readOnly"),
|
|
456
468
|
saveFailed: t("saveFailed"),
|
|
457
469
|
save: t("save"),
|
|
458
|
-
saving: t("saving")
|
|
470
|
+
saving: t("saving"),
|
|
471
|
+
discard: t("discardChanges"),
|
|
472
|
+
// The save bar's pending count interpolates per render, so a
|
|
473
|
+
// function label: the host locale's second argument is the
|
|
474
|
+
// interpolation VARS object (never a fallback string).
|
|
475
|
+
pendingChanges: (n) => t("pendingChanges", { n })
|
|
459
476
|
};
|
|
460
477
|
}
|
|
461
478
|
// ---- environment facts (design D6) ----
|
|
@@ -479,14 +496,19 @@ window.__ModuleLoader__.load({
|
|
|
479
496
|
var WeirCardController = class {
|
|
480
497
|
form;
|
|
481
498
|
store;
|
|
482
|
-
constructor(scope, deps) {
|
|
499
|
+
constructor(scope, deps, page) {
|
|
500
|
+
// page: "main" (default) or "routing" — the form model tracks only
|
|
501
|
+
// that page's fields, so one scope hosts two independent forms (the
|
|
502
|
+
// routing page edits the same namespace, disjoint key set).
|
|
503
|
+
this.page = page === "routing" ? "routing" : "main";
|
|
504
|
+
this.fields = fieldsForPage(this.page);
|
|
483
505
|
// Prop-injected dependencies: the entry-owned settings bus (save →
|
|
484
506
|
// toggle re-check) and a getSession closure resolving the
|
|
485
507
|
// remote.session domain lazily at call time (it may not be wired
|
|
486
508
|
// yet during apply, S17). fetchEnv is the injectable environment
|
|
487
509
|
// facts read (tests); production uses the fetch above.
|
|
488
510
|
this.deps = deps;
|
|
489
|
-
this.form = new primitives.SettingsFormModel(scope,
|
|
511
|
+
this.form = new primitives.SettingsFormModel(scope, this.fields.map(specFor));
|
|
490
512
|
this.store = this.form.bind(() => this.projection());
|
|
491
513
|
// Restart-required keys touched by the last landed save, in
|
|
492
514
|
// registry order; null until such a save lands or after the
|
|
@@ -518,7 +540,7 @@ window.__ModuleLoader__.load({
|
|
|
518
540
|
}
|
|
519
541
|
projection() {
|
|
520
542
|
const fields = {};
|
|
521
|
-
for (const descriptor of
|
|
543
|
+
for (const descriptor of this.fields) {
|
|
522
544
|
// `saved` rides along so condition evaluation can fall back
|
|
523
545
|
// to the saved value while an unparseable draft is staged
|
|
524
546
|
// (D3) — the merged field text alone cannot express it.
|
|
@@ -527,6 +549,9 @@ window.__ModuleLoader__.load({
|
|
|
527
549
|
return {
|
|
528
550
|
...this.form.shell(),
|
|
529
551
|
fields,
|
|
552
|
+
// Staged-write count for the save bar's pending label —
|
|
553
|
+
// plan() is a pure read (the same read save() plans from).
|
|
554
|
+
pendingCount: this.form.plan().length,
|
|
530
555
|
restartReminder: this.restartReminder,
|
|
531
556
|
env: this.env
|
|
532
557
|
};
|
|
@@ -755,10 +780,65 @@ window.__ModuleLoader__.load({
|
|
|
755
780
|
if (picker === void 0 || picker === null) return manualFallback;
|
|
756
781
|
return react_jsx_runtime.jsx(picker.ModelPickerBoundary, { fallback: manualFallback, children: pickerRow(picker) });
|
|
757
782
|
}
|
|
783
|
+
// ---- floating save bar + local form shell ----
|
|
784
|
+
// The bar renders ONLY while the form is dirty — a clean page gets no
|
|
785
|
+
// new visual noise. It sits in the normal flow as the form's last
|
|
786
|
+
// element with position: sticky, so once the page scrolls it pins to
|
|
787
|
+
// the bottom edge of the settings scroll container. Chrome mirrors the
|
|
788
|
+
// host's Toast surface (dark in BOTH themes) instead of the group cards:
|
|
789
|
+
// a floating action bar must read as a surface ABOVE the rows, not as
|
|
790
|
+
// one more row. Colors only from theme tokens, geometry hardcoded.
|
|
791
|
+
const saveBarStyle = { position: "sticky", bottom: "12px", zIndex: 1, display: "flex", alignItems: "center", justifyContent: "space-between", gap: "12px", marginTop: "12px", padding: "10px 14px", background: "var(--dsw-alias-toast-bg)", border: "none", borderRadius: "var(--dsw-radius-lg, 12px)", boxShadow: "var(--dsw-shadow-lv3, 0 8px 24px rgba(0,0,0,.18))" };
|
|
792
|
+
const saveBarTextStyle = { display: "flex", alignItems: "center", gap: "8px", minWidth: 0, fontSize: "12px", lineHeight: "16px", color: "var(--dsw-alias-toast-label)" };
|
|
793
|
+
// warn-label (not warn-primary) is the token designed for dark surfaces.
|
|
794
|
+
const saveBarFailedStyle = { color: "var(--dsw-alias-state-warn-label)" };
|
|
795
|
+
// Discard as a plain text action (the host toast's action pattern): the
|
|
796
|
+
// outline Button variant is tuned for regular surfaces and reads poorly
|
|
797
|
+
// on the always-dark toast surface.
|
|
798
|
+
const saveBarDiscardStyle = { background: "none", border: "none", cursor: "pointer", fontSize: "12px", lineHeight: "16px", padding: "4px 2px", color: "var(--dsw-static-deepseek-400, var(--dsw-alias-brand-primary))" };
|
|
799
|
+
const saveBarActionsStyle = { display: "flex", alignItems: "center", gap: "8px", flexShrink: 0 };
|
|
800
|
+
// The form-shell notice lines (unavailable / readOnly) keep the host
|
|
801
|
+
// contract's role="status" lines in the page's hint styling.
|
|
802
|
+
const formNoticeStyle = { fontSize: "13px", lineHeight: "18px", color: "var(--dsw-alias-label-secondary)", margin: "0 0 12px" };
|
|
803
|
+
/** Local form shell replacing the host primitives.SettingsForm (whose
|
|
804
|
+
* footer sits at the bottom of a long page and cannot be restyled from
|
|
805
|
+
* outside). The host contract is preserved verbatim: staged edits are
|
|
806
|
+
* discarded on unmount (the ref follows the latest onDiscard; hooks run
|
|
807
|
+
* before the early return so the cleanup registers in every state), an
|
|
808
|
+
* unavailable namespace renders the notice line INSTEAD of the form,
|
|
809
|
+
* and a read-only deployment shows the readOnly notice above the
|
|
810
|
+
* controls. The save affordance is the sticky bar: pending count +
|
|
811
|
+
* failure text left, discard/save right; invalid or saving disables
|
|
812
|
+
* save (the bar existing already implies dirty). */
|
|
813
|
+
function WeirSettingsForm(props) {
|
|
814
|
+
const { state, labels } = props;
|
|
815
|
+
const discardRef = react.useRef(props.onDiscard);
|
|
816
|
+
discardRef.current = props.onDiscard;
|
|
817
|
+
react.useEffect(() => () => discardRef.current(), []);
|
|
818
|
+
if (!state.available) return react_jsx_runtime.jsx("p", { style: formNoticeStyle, role: "status", children: labels.unavailable });
|
|
819
|
+
return react_jsx_runtime.jsxs("div", { children: [
|
|
820
|
+
!state.writable ? react_jsx_runtime.jsx("p", { style: formNoticeStyle, role: "status", children: labels.readOnly, key: "read-only" }) : null,
|
|
821
|
+
props.children,
|
|
822
|
+
state.dirty ? react_jsx_runtime.jsxs("div", { style: saveBarStyle, "data-save-bar": "", children: [
|
|
823
|
+
react_jsx_runtime.jsxs("div", { style: saveBarTextStyle, children: [
|
|
824
|
+
react_jsx_runtime.jsx("span", { children: labels.pendingChanges(state.pendingCount ?? 0), key: "pending" }),
|
|
825
|
+
state.failed ? react_jsx_runtime.jsx("span", { style: saveBarFailedStyle, role: "status", children: labels.saveFailed, key: "failed" }) : null
|
|
826
|
+
] }),
|
|
827
|
+
react_jsx_runtime.jsxs("div", { style: saveBarActionsStyle, children: [
|
|
828
|
+
react_jsx_runtime.jsx("button", { type: "button", style: saveBarDiscardStyle, onClick: props.onDiscard, children: labels.discard }),
|
|
829
|
+
react_jsx_runtime.jsx(primitives.Button, { variant: "primary", size: "sm", disabled: state.invalid || state.saving, onClick: props.onSave, children: state.saving ? labels.saving : labels.save })
|
|
830
|
+
] })
|
|
831
|
+
] }, "save-bar") : null
|
|
832
|
+
] });
|
|
833
|
+
}
|
|
758
834
|
function WeirCard(props) {
|
|
759
835
|
const state = props.useWeirSettingsCard((snapshot) => snapshot);
|
|
760
836
|
const { t } = props;
|
|
761
|
-
|
|
837
|
+
// Dual-page card: props.page ("main" default, "routing") selects the
|
|
838
|
+
// summary text and the rendered group subset; conditions/restartTag/
|
|
839
|
+
// displayText are page-independent.
|
|
840
|
+
const page = props.page === "routing" ? "routing" : "main";
|
|
841
|
+
if (props.view === "summary") return t(page === "routing" ? "routingDescription" : "description");
|
|
762
842
|
const disabled = !state.writable;
|
|
763
843
|
// env three-state (D6): pending/failed hide env-gated rows only.
|
|
764
844
|
const env = state.env ?? { status: "ready", facts: {} };
|
|
@@ -893,7 +973,7 @@ window.__ModuleLoader__.load({
|
|
|
893
973
|
// Card sections: the tree walk prunes hidden subtrees wholesale (a
|
|
894
974
|
// hidden parent hides every descendant), and children land directly
|
|
895
975
|
// under their parent's subtree in DFS order.
|
|
896
|
-
const cards = LAYOUT.map((groupLayout) => {
|
|
976
|
+
const cards = LAYOUT.filter((groupLayout) => groupLayout.page === page).map((groupLayout) => {
|
|
897
977
|
const rows = [];
|
|
898
978
|
const walk = (node, ancestorsVisible) => {
|
|
899
979
|
const isVisible = ancestorsVisible && visible(node);
|
|
@@ -931,7 +1011,9 @@ window.__ModuleLoader__.load({
|
|
|
931
1011
|
key: "restart-reminder"
|
|
932
1012
|
}));
|
|
933
1013
|
}
|
|
934
|
-
|
|
1014
|
+
// The local form shell (NOT the host primitives.SettingsForm): same
|
|
1015
|
+
// contract, with the save affordance as the sticky bar above.
|
|
1016
|
+
return react_jsx_runtime.jsxs(WeirSettingsForm, {
|
|
935
1017
|
labels: formLabels(t),
|
|
936
1018
|
state,
|
|
937
1019
|
onSave: props.save,
|
|
@@ -945,9 +1027,12 @@ window.__ModuleLoader__.load({
|
|
|
945
1027
|
gap: "var(--dsw-spacing-3, 12px)"
|
|
946
1028
|
};
|
|
947
1029
|
const ITEM_SLOT = "settings.weir.item";
|
|
948
|
-
|
|
1030
|
+
// The section shell is page-agnostic: the composition root passes the
|
|
1031
|
+
// child item slot of the page being hosted (main by default, the
|
|
1032
|
+
// routing page's "settings.weir-routing.item" for the routing section).
|
|
1033
|
+
const WeirSection = ({ renderSlot, slot }) => react_jsx_runtime.jsx("div", {
|
|
949
1034
|
style: columnStyle,
|
|
950
|
-
children: renderSlot(ITEM_SLOT)
|
|
1035
|
+
children: renderSlot(slot ?? ITEM_SLOT)
|
|
951
1036
|
});
|
|
952
1037
|
exports.GROUPS = GROUPS;
|
|
953
1038
|
exports.BOOLEAN_DEFAULTS = BOOLEAN_DEFAULTS;
|
|
@@ -957,6 +1042,7 @@ window.__ModuleLoader__.load({
|
|
|
957
1042
|
exports.CATEGORY_NAMES = CATEGORY_NAMES;
|
|
958
1043
|
exports.RESTART_FIELDS = RESTART_FIELDS;
|
|
959
1044
|
exports.FIELDS = FIELDS;
|
|
1045
|
+
exports.fieldsForPage = fieldsForPage;
|
|
960
1046
|
exports.ENV_FACTS = ENV_FACTS;
|
|
961
1047
|
exports.evaluateCondition = evaluateCondition;
|
|
962
1048
|
exports.conditionUsesEnv = conditionUsesEnv;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "weir-harness",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.2.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Weir \u2014 orchestrator-centric DSH agent preset: intent gate, category delegation, todo driver, pull-based background jobs, auto-compaction, hashline edit.",
|
|
6
6
|
"license": "MIT",
|
|
@@ -186,6 +186,19 @@ export async function resolveDurableRoot(ctx, agent, runtime, mount = undefined)
|
|
|
186
186
|
if (mount) mounts.add(mount)
|
|
187
187
|
runtime.members.set(agent.id, { agent, root: id, mounts })
|
|
188
188
|
}
|
|
189
|
+
// A fork (a depth-0 Session carrying parentSession) is its own board root,
|
|
190
|
+
// but its board starts as a one-time snapshot of the fork source's durable
|
|
191
|
+
// root board: seed before the first ensure can materialize an empty sidecar.
|
|
192
|
+
// Once per runtime; the no-sidecar disk check keeps it idempotent across
|
|
193
|
+
// restarts. Best-effort: a failed seed never blocks board availability.
|
|
194
|
+
const parentId = header.parentSession
|
|
195
|
+
if (typeof parentId === 'string' && parentId.length > 0) {
|
|
196
|
+
runtime.forkSeeded ??= new Set()
|
|
197
|
+
if (!runtime.forkSeeded.has(id)) {
|
|
198
|
+
runtime.forkSeeded.add(id)
|
|
199
|
+
try { await runtime.storage.seedFork(id, parentId, where.header.cwd) } catch (error) { ctx.logger?.warn?.(`blackboard fork seeding failed: ${String(error)}`) }
|
|
200
|
+
}
|
|
201
|
+
}
|
|
189
202
|
runtime.kernel.bind(id, where.header.cwd)
|
|
190
203
|
await runtime.kernel.ensure(id)
|
|
191
204
|
if (wasLive && (runtime.revoked?.has(agent) || ctx.get?.('agents')?.get?.(agent.id) !== agent)) throw new Error('blackboard: Agent disposed while resolving its root')
|
package/src/blackboard/index.js
CHANGED
|
@@ -346,6 +346,50 @@ export function createBlackboardPlugin({ kernel, resolveTtl = resolveWriteTokenT
|
|
|
346
346
|
return id
|
|
347
347
|
}
|
|
348
348
|
|
|
349
|
+
/**
|
|
350
|
+
* The conversation root for a BARE session id (the remote bridge's
|
|
351
|
+
* resolve face — the side panel asks for the board it should bind
|
|
352
|
+
* before any lookup-gated verb runs). Same root semantics as boardOf
|
|
353
|
+
* and resolveDurableRoot: a depth-0 session (including a fork that
|
|
354
|
+
* still carries a parentSession) is its own root; a delegated child
|
|
355
|
+
* ascends the parentSession chain. Each hop reads the live sessions
|
|
356
|
+
* registry first, then the durable header through
|
|
357
|
+
* sessionQuery.readSession (the parentOf seam — a child between
|
|
358
|
+
* turns has no live registry entry). A hop that yields no header, a
|
|
359
|
+
* missing parent, a cycle, or the hop cap degrades to the deepest
|
|
360
|
+
* known ancestor: boards never silently cross-contaminate
|
|
361
|
+
* conversations.
|
|
362
|
+
* @param {string} sessionId
|
|
363
|
+
* @returns {Promise<string>}
|
|
364
|
+
*/
|
|
365
|
+
const rootOf = async (sessionId) => {
|
|
366
|
+
const headerOf = async (/** @type {string} */ id) => {
|
|
367
|
+
try {
|
|
368
|
+
const live = ctx.get?.('sessions')?.get?.(id)
|
|
369
|
+
if (live?.header && typeof live.header === 'object') return live.header
|
|
370
|
+
} catch { /* fall through to the durable lookup */ }
|
|
371
|
+
try {
|
|
372
|
+
// readSession returns { session, events } — the session record IS
|
|
373
|
+
// the durable header (id, parentSession, delegationDepth, ...).
|
|
374
|
+
const read = await ctx.get?.('sessionQuery')?.readSession?.(id)
|
|
375
|
+
if (read?.session && typeof read.session === 'object') return read.session
|
|
376
|
+
} catch { /* an unreadable log yields no header */ }
|
|
377
|
+
return undefined
|
|
378
|
+
}
|
|
379
|
+
let id = sessionId
|
|
380
|
+
const seen = new Set([id])
|
|
381
|
+
for (let hop = 0; hop < MAX_ANCESTORS; hop++) {
|
|
382
|
+
const header = await headerOf(id)
|
|
383
|
+
if (!header) break
|
|
384
|
+
if ((header.delegationDepth ?? 0) === 0) return id
|
|
385
|
+
const parentId = header.parentSession
|
|
386
|
+
if (typeof parentId !== 'string' || parentId.length === 0 || seen.has(parentId)) break
|
|
387
|
+
seen.add(parentId)
|
|
388
|
+
id = parentId
|
|
389
|
+
}
|
|
390
|
+
return id
|
|
391
|
+
}
|
|
392
|
+
|
|
349
393
|
const call = (exec, action) => {
|
|
350
394
|
if (lifecycle) return lifecycle.run(exec?.agent, root => { exec?.signal?.throwIfAborted?.(); return action(root) })
|
|
351
395
|
const id = boardOf(exec?.agent)
|
|
@@ -382,7 +426,7 @@ export function createBlackboardPlugin({ kernel, resolveTtl = resolveWriteTokenT
|
|
|
382
426
|
resolveMainAgent: agent => agent,
|
|
383
427
|
logger: ctx.logger,
|
|
384
428
|
})
|
|
385
|
-
const offBridge = feedBlackboardRemoteBridge({ kernel: kernelInstance, boardOf, run: lifecycle?.run, ttlMs, requestPromotion: async (/** @type {any} */ agent) => {
|
|
429
|
+
const offBridge = feedBlackboardRemoteBridge({ kernel: kernelInstance, boardOf, rootOf, run: lifecycle?.run, ttlMs, requestPromotion: async (/** @type {any} */ agent) => {
|
|
386
430
|
const root = await boardOf(agent)
|
|
387
431
|
const main = root === agent.id ? agent : ctx.get?.('agents')?.get?.(root)
|
|
388
432
|
if (!main) throw new Error('blackboard: root Agent is not live for promotion')
|
package/src/blackboard/remote.js
CHANGED
|
@@ -16,11 +16,12 @@
|
|
|
16
16
|
// WIRE CONTRACT (pinned — the parallel panel lane builds against this exact
|
|
17
17
|
// shape; see docs/features/blackboard.md 面板数据通道):
|
|
18
18
|
//
|
|
19
|
-
// namespace `weirBlackboard
|
|
20
|
-
//
|
|
21
|
-
// scope context `agent`) — the panel page's
|
|
22
|
-
// same way the stock `fileReferences`
|
|
23
|
-
// below need no session field
|
|
19
|
+
// namespace `weirBlackboard`. The seven board verbs each take one JSON
|
|
20
|
+
// `args` object, and the acting session rides the `agent` lookup
|
|
21
|
+
// parameter (wire `agentId`, scope context `agent`) — the panel page's
|
|
22
|
+
// current session injects it the same way the stock `fileReferences`
|
|
23
|
+
// remote does, so the domain signatures below need no session field.
|
|
24
|
+
// The eighth method, resolve, is the deliberate exception named below.
|
|
24
25
|
//
|
|
25
26
|
// - list({ type?, query? }) → { entries: [{ key, entryType, summary,
|
|
26
27
|
// readCount, subscribeCount, updatedAt, promoted? }] }
|
|
@@ -40,6 +41,16 @@
|
|
|
40
41
|
// - markPromoted({ key, destination }) → { ok: true, destination }
|
|
41
42
|
// | { ok: false, error } — makes the entry
|
|
42
43
|
// read-only with the promoted marker
|
|
44
|
+
// - resolve → { sessionId } — the panel's root
|
|
45
|
+
// resolution (slice 4): NO scope, NO agent
|
|
46
|
+
// lookup. The gateway's agent lookup refuses
|
|
47
|
+
// subagent-owned sessions before this service
|
|
48
|
+
// can run (the subagent-panel 404 root cause),
|
|
49
|
+
// so the browsing session id crosses as a bare
|
|
50
|
+
// JSON `agentId` parameter (source 'json' — the
|
|
51
|
+
// capability-remote precedent) and the answer
|
|
52
|
+
// is the conversation root whose board the
|
|
53
|
+
// panel binds before any lookup-gated verb.
|
|
43
54
|
//
|
|
44
55
|
// `token` is an opaque per-acquisition identifier (the authority itself is
|
|
45
56
|
// the acting session's live kernel token, re-checked at every mutation);
|
|
@@ -56,7 +67,7 @@
|
|
|
56
67
|
export const BLACKBOARD_REMOTE_SERVICE_KEY = 'weirBlackboardRemote'
|
|
57
68
|
/** The wire namespace: POST /api/weirBlackboard/<method>, client ctx.remote.weirBlackboard.*. */
|
|
58
69
|
export const BLACKBOARD_REMOTE_NAMESPACE = 'weirBlackboard'
|
|
59
|
-
/** The seven
|
|
70
|
+
/** The seven board verbs, each taking one JSON `args` parameter plus the acting-session lookup. The eighth wire method, resolve, is built separately in the contribution (no lookup, no scope). */
|
|
60
71
|
export const BLACKBOARD_REMOTE_METHODS = Object.freeze(['list', 'read', 'apply', 'write', 'remove', 'requestPromotion', 'markPromoted'])
|
|
61
72
|
/** The wire package identity (distinct from 'weir-harness' — the capability-remote contribution already owns that package face). */
|
|
62
73
|
const BLACKBOARD_REMOTE_PACKAGE = 'weir-blackboard'
|
|
@@ -90,6 +101,7 @@ export class BlackboardRemoteError extends Error {
|
|
|
90
101
|
* module-level slot pattern as the capability read bridge. Faces:
|
|
91
102
|
* kernel() — the shared arbitration kernel (list/read/apply/write/deleteKey)
|
|
92
103
|
* boardOf(agent) — conversation-root board id for an agent handle
|
|
104
|
+
* rootOf(sessionId) — conversation-root session id for a BARE session id
|
|
93
105
|
* ttlMs() — the settings-driven write-token TTL in milliseconds
|
|
94
106
|
*/
|
|
95
107
|
let bridgeFaces = null
|
|
@@ -97,7 +109,7 @@ let bridgeFaces = null
|
|
|
97
109
|
/**
|
|
98
110
|
* Feed the bridge (blackboard plugin apply). Returns the unregister the plugin
|
|
99
111
|
* disposes with its own lifecycle.
|
|
100
|
-
* @param {{ kernel: any, boardOf: (agent: any) => string | undefined, ttlMs: () => number }} faces
|
|
112
|
+
* @param {{ kernel: any, boardOf: (agent: any) => string | undefined, rootOf?: (sessionId: string) => Promise<string>, ttlMs: () => number }} faces
|
|
101
113
|
*/
|
|
102
114
|
export function feedBlackboardRemoteBridge(faces) {
|
|
103
115
|
bridgeFaces = faces
|
|
@@ -155,7 +167,7 @@ export function createBlackboardRemoteService(dependencies = {}) {
|
|
|
155
167
|
* @param {any} agent
|
|
156
168
|
* @returns {{ sessionId: string, boardId: string, faces: { kernel: any, boardOf: Function, ttlMs: Function } }}
|
|
157
169
|
*/
|
|
158
|
-
async function
|
|
170
|
+
async function resolveActing(agent) {
|
|
159
171
|
if (!ensure()) throw new BlackboardRemoteError('channel-unavailable', 'blackboard remote is not registered with the gateway')
|
|
160
172
|
const faces = bridgeFor()
|
|
161
173
|
if (!faces) throw new BlackboardRemoteError('bridge-absent', 'blackboard remote is not offered (non-Weir preset or unmounted bridge)')
|
|
@@ -187,7 +199,7 @@ export function createBlackboardRemoteService(dependencies = {}) {
|
|
|
187
199
|
/** Aggregate rows: key + entryType + summary + counts, never content. */
|
|
188
200
|
async list(agent, args) {
|
|
189
201
|
return payloadWrap('list', async () => {
|
|
190
|
-
const { boardId, faces } = await
|
|
202
|
+
const { boardId, faces } = await resolveActing(agent)
|
|
191
203
|
const rows = await faces.kernel.list(boardId, { entryType: args?.type, query: args?.query })
|
|
192
204
|
return { entries: rows.map((/** @type {any} */ entry) => wireEntry(entry)) }
|
|
193
205
|
})
|
|
@@ -198,7 +210,7 @@ export function createBlackboardRemoteService(dependencies = {}) {
|
|
|
198
210
|
* the agent-tool path's reuse evidence, not a page-view metric. */
|
|
199
211
|
async read(agent, args) {
|
|
200
212
|
return payloadWrap('read', async () => {
|
|
201
|
-
const { boardId, faces } = await
|
|
213
|
+
const { boardId, faces } = await resolveActing(agent)
|
|
202
214
|
const result = await faces.kernel.read(boardId, args?.keys ?? [], { count: false })
|
|
203
215
|
return { entries: result.found.map((/** @type {any} */ entry) => wireEntry(entry, { content: true })) }
|
|
204
216
|
})
|
|
@@ -207,7 +219,7 @@ export function createBlackboardRemoteService(dependencies = {}) {
|
|
|
207
219
|
/** Acquire the one-shot write authority for one key (auto-subscribes on contention; a promoted key refuses explicitly). */
|
|
208
220
|
async apply(agent, args) {
|
|
209
221
|
return payloadWrap('apply', async () => {
|
|
210
|
-
const { sessionId, boardId, faces } = await
|
|
222
|
+
const { sessionId, boardId, faces } = await resolveActing(agent)
|
|
211
223
|
const result = await faces.kernel.apply(boardId, { holderId: sessionId, key: args?.key, ttlMs: faces.ttlMs() })
|
|
212
224
|
if (result.status === 'contended') return { acquired: false, holder: result.holder, subscribed: true }
|
|
213
225
|
if (result.status === 'promoted') return { acquired: false, promoted: true, destination: result.destination }
|
|
@@ -218,7 +230,7 @@ export function createBlackboardRemoteService(dependencies = {}) {
|
|
|
218
230
|
/** Create or update one entry under the acting session's live write authority. */
|
|
219
231
|
async write(agent, args) {
|
|
220
232
|
try {
|
|
221
|
-
const { sessionId, boardId, faces } = await
|
|
233
|
+
const { sessionId, boardId, faces } = await resolveActing(agent)
|
|
222
234
|
const result = await faces.kernel.write(boardId, {
|
|
223
235
|
holderId: sessionId,
|
|
224
236
|
key: args?.key,
|
|
@@ -249,7 +261,7 @@ export function createBlackboardRemoteService(dependencies = {}) {
|
|
|
249
261
|
/** Delete one entry under the acting session's live write authority. */
|
|
250
262
|
async remove(agent, args) {
|
|
251
263
|
try {
|
|
252
|
-
const { sessionId, boardId, faces } = await
|
|
264
|
+
const { sessionId, boardId, faces } = await resolveActing(agent)
|
|
253
265
|
const result = await faces.kernel.deleteKey(boardId, { holderId: sessionId, key: args?.key })
|
|
254
266
|
if (result.status === 'missing') return { ok: false, error: `blackboard key "${result.key}" does not exist` }
|
|
255
267
|
if (result.status === 'promoted') {
|
|
@@ -271,7 +283,7 @@ export function createBlackboardRemoteService(dependencies = {}) {
|
|
|
271
283
|
/** Request the promotion-evaluation brief into the session's main agent (slice 3). Folds failures like the mutations. */
|
|
272
284
|
async requestPromotion(agent, args) {
|
|
273
285
|
try {
|
|
274
|
-
const { faces } = await
|
|
286
|
+
const { faces } = await resolveActing(agent)
|
|
275
287
|
if (typeof faces.requestPromotion !== 'function') {
|
|
276
288
|
return { ok: false, error: 'the blackboard bridge does not offer promotion requests' }
|
|
277
289
|
}
|
|
@@ -288,7 +300,7 @@ export function createBlackboardRemoteService(dependencies = {}) {
|
|
|
288
300
|
/** Mark one entry promoted under the user-adjudicated destination (slice 3); marking is an annotation, not a payload mutation. */
|
|
289
301
|
async markPromoted(agent, args) {
|
|
290
302
|
try {
|
|
291
|
-
const { boardId, faces } = await
|
|
303
|
+
const { boardId, faces } = await resolveActing(agent)
|
|
292
304
|
const result = await faces.kernel.markPromoted(boardId, { key: args?.key, destination: args?.destination })
|
|
293
305
|
if (result.status === 'missing') return { ok: false, error: `blackboard key "${result.key}" does not exist` }
|
|
294
306
|
if (result.status === 'already-promoted') return { ok: true, destination: result.destination, alreadyPromoted: true }
|
|
@@ -300,6 +312,28 @@ export function createBlackboardRemoteService(dependencies = {}) {
|
|
|
300
312
|
return { ok: false, error }
|
|
301
313
|
}
|
|
302
314
|
},
|
|
315
|
+
|
|
316
|
+
/**
|
|
317
|
+
* The panel's root resolution (slice 4): the browsing session id
|
|
318
|
+
* crosses as a bare JSON value — NO agent lookup, NO scope — because
|
|
319
|
+
* the gateway's lookup refuses subagent-owned sessions before this
|
|
320
|
+
* service can run. Answers the conversation root whose board the
|
|
321
|
+
* panel binds; a bridge without the rootOf face (an older blackboard
|
|
322
|
+
* plugin) or a face returning an unusable id degrades to the
|
|
323
|
+
* browsing id itself, which is exactly the pre-resolve behavior.
|
|
324
|
+
*/
|
|
325
|
+
async resolve(sessionId) {
|
|
326
|
+
return payloadWrap('resolve', async () => {
|
|
327
|
+
if (!ensure()) throw new BlackboardRemoteError('channel-unavailable', 'blackboard remote is not registered with the gateway')
|
|
328
|
+
const faces = bridgeFor()
|
|
329
|
+
if (!faces) throw new BlackboardRemoteError('bridge-absent', 'blackboard remote is not offered (non-Weir preset or unmounted bridge)')
|
|
330
|
+
const id = typeof sessionId === 'string' && sessionId.length > 0 ? sessionId : undefined
|
|
331
|
+
if (id === undefined) throw new BlackboardRemoteError('unknown-session', 'blackboard remote resolve: the browsing session identity is unavailable')
|
|
332
|
+
if (typeof faces.rootOf !== 'function') return { sessionId: id }
|
|
333
|
+
const root = await faces.rootOf(id)
|
|
334
|
+
return { sessionId: typeof root === 'string' && root.length > 0 ? root : id }
|
|
335
|
+
})
|
|
336
|
+
},
|
|
303
337
|
}
|
|
304
338
|
}
|
|
305
339
|
|
|
@@ -321,7 +355,8 @@ export function blackboardRemoteContribution() {
|
|
|
321
355
|
face: 'host',
|
|
322
356
|
schemas: [],
|
|
323
357
|
model: { services: [], events: [], objects: [] },
|
|
324
|
-
invocations:
|
|
358
|
+
invocations: [
|
|
359
|
+
...BLACKBOARD_REMOTE_METHODS.map(method => ({
|
|
325
360
|
id: `${BLACKBOARD_REMOTE_PACKAGE}.${BLACKBOARD_REMOTE_NAMESPACE}.${method}`,
|
|
326
361
|
service: BLACKBOARD_REMOTE_SERVICE_KEY,
|
|
327
362
|
namespace: BLACKBOARD_REMOTE_NAMESPACE,
|
|
@@ -334,6 +369,24 @@ export function blackboardRemoteContribution() {
|
|
|
334
369
|
],
|
|
335
370
|
result: { mode: 'src-json' },
|
|
336
371
|
})),
|
|
372
|
+
// The eighth invocation, resolve, deliberately breaks the shared
|
|
373
|
+
// shape: NO agent-lookup parameter, NO scope projection — the
|
|
374
|
+
// gateway's agent lookup refuses subagent-owned sessions before
|
|
375
|
+
// this service can run (the subagent-panel 404 root cause), so the
|
|
376
|
+
// browsing session id crosses as a bare JSON value on the same
|
|
377
|
+
// `agentId` wire field (the capability-remote precedent, src-json).
|
|
378
|
+
{
|
|
379
|
+
id: `${BLACKBOARD_REMOTE_PACKAGE}.${BLACKBOARD_REMOTE_NAMESPACE}.resolve`,
|
|
380
|
+
service: BLACKBOARD_REMOTE_SERVICE_KEY,
|
|
381
|
+
namespace: BLACKBOARD_REMOTE_NAMESPACE,
|
|
382
|
+
method: 'resolve',
|
|
383
|
+
invocation: { kind: 'direct' },
|
|
384
|
+
parameters: [
|
|
385
|
+
{ name: 'sessionId', wire: 'agentId', source: 'json', codec: { mode: 'src-json' } },
|
|
386
|
+
],
|
|
387
|
+
result: { mode: 'src-json' },
|
|
388
|
+
},
|
|
389
|
+
],
|
|
337
390
|
}
|
|
338
391
|
}
|
|
339
392
|
|