@melite/estate-drafts 0.1.4-alpha → 0.1.5-pre-alpha
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/README.md +56 -0
- package/lib/bench.d.ts +48 -1
- package/lib/bench.js +87 -12
- package/lib/index.d.ts +4 -1
- package/lib/index.js +5 -1
- package/lib/remote.d.ts +29 -10
- package/lib/remote.js +347 -156
- package/lib/spec.d.ts +28 -1
- package/lib/spec.js +32 -2
- package/lib/store.d.ts +39 -5
- package/lib/store.js +130 -42
- package/lib/trail.d.ts +16 -0
- package/lib/trail.js +91 -0
- package/lib/typert.host.js +24 -21
- package/lib/typert.remote-client.js +15 -15
- package/package.json +7 -3
package/lib/remote.js
CHANGED
|
@@ -33,13 +33,13 @@ var __esDecorate = (this && this.__esDecorate) || function (ctor, descriptorIn,
|
|
|
33
33
|
done = true;
|
|
34
34
|
};
|
|
35
35
|
import { Remote, RemoteError, TypertRemoteService, } from "@deepseek-ai/dsh-typert-protocol";
|
|
36
|
-
import { admitWiring, benchBlockOf, strippedForAdmission } from "./bench.js";
|
|
36
|
+
import { admitWiring, benchBlockOf, strippedForAdmission, toleratedByRow } from "./bench.js";
|
|
37
37
|
import { STAGE_PATCH_KEYS, RESTORED_PROVENANCE_SPELLING } from "@melite/contracts/benchStage";
|
|
38
38
|
export { STAGE_PATCH_KEYS };
|
|
39
39
|
import { unwrapItemArrays } from "@melite/comfyui-card-kit";
|
|
40
40
|
import { openDraftsDomain } from "./store.js";
|
|
41
41
|
import { DRAFT_ID_RE } from "./spec.js";
|
|
42
|
-
import { addStageBench, ensureBench, notifyBenchChange, notifyBenchDocWrite, onBenchChange, setBenchChangeSeam, getBench, groupBench, patchBench, clearBench, removeStageBench, reorderBench, rewireBench, undoBench, ungroupBench, } from "./bench.js";
|
|
42
|
+
import { addStageBench, ensureBench, notifyBenchChange, notifyBenchDocWrite, onBenchChange, clearBenchChangeSeam, setBenchChangeSeam, getBench, groupBench, patchBench, clearBench, removeStageBench, reorderBench, rewireBench, undoBench, ungroupBench, } from "./bench.js";
|
|
43
43
|
import { benchDraftIdOf } from "@melite/contracts/benchStage";
|
|
44
44
|
|
|
45
45
|
|
|
@@ -59,17 +59,21 @@ import { benchDraftIdOf } from "@melite/contracts/benchStage";
|
|
|
59
59
|
|
|
60
60
|
|
|
61
61
|
|
|
62
|
+
|
|
62
63
|
export function installBenchChangeSeam(ctx) {
|
|
63
64
|
let detach = null;
|
|
64
65
|
ctx.effect(() => {
|
|
65
|
-
|
|
66
|
+
const mine = {
|
|
66
67
|
emit: (session) => { ctx.emit("bench.changed", session); },
|
|
67
68
|
on: (fn) => {
|
|
68
69
|
const stop = ctx.on("bench.changed", fn);
|
|
69
70
|
return () => { stop(); };
|
|
70
71
|
},
|
|
71
|
-
}
|
|
72
|
-
|
|
72
|
+
};
|
|
73
|
+
setBenchChangeSeam(mine);
|
|
74
|
+
// the ownership stamp: an older fiber's unwind clears only its own
|
|
75
|
+
// install — a displaced seam detaches nothing (clearBenchChangeSeam)
|
|
76
|
+
detach = () => { clearBenchChangeSeam(mine); };
|
|
73
77
|
return detach;
|
|
74
78
|
});
|
|
75
79
|
return () => { detach?.(); };
|
|
@@ -84,34 +88,8 @@ function thrownMessage(error) {
|
|
|
84
88
|
return error instanceof Error ? error.message : String(error);
|
|
85
89
|
}
|
|
86
90
|
/** ── THE OUTPUT HISTORY HELPERS (stroke 40) ──────────────────────
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
* same harvest order the client's preview reads — the history and
|
|
90
|
-
* the hero can never disagree about what a run produced). */
|
|
91
|
-
export function resultOutputUrls(result) {
|
|
92
|
-
if (typeof result !== "object" || result === null || Array.isArray(result))
|
|
93
|
-
return [];
|
|
94
|
-
const row = result;
|
|
95
|
-
const urls = [];
|
|
96
|
-
if (row["artifacts"] !== null && typeof row["artifacts"] === "object" && !Array.isArray(row["artifacts"])) {
|
|
97
|
-
for (const v of Object.values(row["artifacts"])) {
|
|
98
|
-
if (typeof v === "string" && v !== "")
|
|
99
|
-
urls.push(v);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
if (urls.length === 0 && Array.isArray(row["assetUrls"])) {
|
|
103
|
-
for (const v of row["assetUrls"]) {
|
|
104
|
-
if (typeof v === "string" && v !== "")
|
|
105
|
-
urls.push(v);
|
|
106
|
-
}
|
|
107
|
-
}
|
|
108
|
-
if (urls.length === 0 && typeof row["url"] === "string" && row["url"] !== "")
|
|
109
|
-
urls.push(row["url"]);
|
|
110
|
-
return urls;
|
|
111
|
-
}
|
|
112
|
-
/** Same OUTPUTS (the uniqueness key): two results match when their
|
|
113
|
-
* url sets are equal — a re-run landing the same urls is not a new
|
|
114
|
-
* history entry (the operator's "when it is unique"). */
|
|
91
|
+
* resultOutputUrls now lives in trail.ts (the cycle-free seam) and
|
|
92
|
+
* is re-exported below with the trail laws. ───────────────────── */
|
|
115
93
|
export function sameResultOutputs(a, b) {
|
|
116
94
|
return sameUrlSet(resultOutputUrls(a), resultOutputUrls(b));
|
|
117
95
|
}
|
|
@@ -121,6 +99,15 @@ function sameUrlSet(a, b) {
|
|
|
121
99
|
const sa = new Set(a);
|
|
122
100
|
return b.every((u) => sa.has(u));
|
|
123
101
|
}
|
|
102
|
+
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
|
|
106
|
+
export { canonicalOutputUrl, resultOutputUrls, isTerminalTrailWord as isTerminalWord, trailDedupe } from "./trail.js";
|
|
107
|
+
import { canonicalOutputUrl, isTerminalTrailWord as isTerminalWord, resultOutputUrls, sameTrailRunIdentity, } from "./trail.js";
|
|
108
|
+
function sameRunIdentity(a, b) {
|
|
109
|
+
return sameTrailRunIdentity(a, b);
|
|
110
|
+
}
|
|
124
111
|
function resolverOf(ctx) {
|
|
125
112
|
let seat;
|
|
126
113
|
try {
|
|
@@ -263,6 +250,20 @@ export function laneAdmissionOf(ctx) {
|
|
|
263
250
|
|
|
264
251
|
|
|
265
252
|
|
|
253
|
+
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
export function laneMigrationOf(ctx) {
|
|
257
|
+
const reg = resolverOf(ctx);
|
|
258
|
+
if (reg === null || reg.laneMigrate === undefined)
|
|
259
|
+
return undefined;
|
|
260
|
+
return reg.laneMigrate.bind(reg);
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
|
|
265
|
+
|
|
266
|
+
|
|
266
267
|
export function benchAdmissionOf(ctx) {
|
|
267
268
|
const modelAdmit = admissionOf(ctx);
|
|
268
269
|
const laneAdmit = laneAdmissionOf(ctx);
|
|
@@ -341,6 +342,37 @@ function docIndexFields(doc) {
|
|
|
341
342
|
return { model, name: disp };
|
|
342
343
|
}
|
|
343
344
|
/** The drafts Remote owner: list/get over the storage domain. */
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
|
|
353
|
+
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
const benchTransactions = new Map();
|
|
360
|
+
async function withBenchWrite(draftId, body) {
|
|
361
|
+
const prev = benchTransactions.get(draftId) ?? Promise.resolve();
|
|
362
|
+
let release;
|
|
363
|
+
const gate = new Promise((r) => { release = r; });
|
|
364
|
+
const mine = prev.then(() => gate);
|
|
365
|
+
benchTransactions.set(draftId, mine);
|
|
366
|
+
await prev;
|
|
367
|
+
try {
|
|
368
|
+
return await body();
|
|
369
|
+
}
|
|
370
|
+
finally {
|
|
371
|
+
release();
|
|
372
|
+
if (benchTransactions.get(draftId) === mine)
|
|
373
|
+
benchTransactions.delete(draftId);
|
|
374
|
+
}
|
|
375
|
+
}
|
|
344
376
|
let DraftsRemoteService = (() => {
|
|
345
377
|
let _classSuper = TypertRemoteService;
|
|
346
378
|
let _instanceExtraInitializers = [];
|
|
@@ -455,8 +487,14 @@ let DraftsRemoteService = (() => {
|
|
|
455
487
|
async remoteExportPatch(draftId, stageId, patch, expectRev, signal) {
|
|
456
488
|
return await this.patch(draftId, stageId, patch, expectRev, signal);
|
|
457
489
|
}
|
|
458
|
-
/** The patch arm the adapter above delegates to
|
|
490
|
+
/** The patch arm the adapter above delegates to — one write
|
|
491
|
+
* transaction on the draft's own key, shared with the bench
|
|
492
|
+
* verbs (the round-14 race audit's R3: the author arm's
|
|
493
|
+
* spread-copy of the log forked against their in-place push). */
|
|
459
494
|
async patch(draftId, stageId, patch, expectRev, signal) {
|
|
495
|
+
return withBenchWrite(draftId, () => this.patchBody(draftId, stageId, patch, expectRev, signal));
|
|
496
|
+
}
|
|
497
|
+
async patchBody(draftId, stageId, patch, expectRev, signal) {
|
|
460
498
|
signal.throwIfAborted();
|
|
461
499
|
if (!DRAFT_ID_RE.test(draftId)) {
|
|
462
500
|
throw new RemoteError("draft/bad-id", `bad draft id ${JSON.stringify(draftId)}`, {
|
|
@@ -485,6 +523,27 @@ let DraftsRemoteService = (() => {
|
|
|
485
523
|
if (Object.keys(patch).length === 0) {
|
|
486
524
|
throw new RemoteError("draft/bad-patch", "patch is empty (nothing to patch)", {});
|
|
487
525
|
}
|
|
526
|
+
// THE INPUT KEYS ARE SHAPE-GATED AT THE DOOR (audit S-S8,
|
|
527
|
+
|
|
528
|
+
// wire type permits any JsonValue) and the next compile refused
|
|
529
|
+
// the poisoned row far from the write; `inputOutputs` stored
|
|
530
|
+
// verbatim on BOTH seats while the bench verb refuses the key
|
|
531
|
+
// outright. One shape per key now: inputs is the channel ->
|
|
532
|
+
// wiring record (the bench arm's own isObj law), inputOutputs
|
|
533
|
+
// is the per-slot output-name list (string or null per slot —
|
|
534
|
+
// the wire contract FlowStage declares). The compile seat keeps
|
|
535
|
+
// the deep wiring law; this gate only refuses what could never
|
|
536
|
+
// compile, at the write that would have poisoned the doc.
|
|
537
|
+
if ("inputs" in patch && !isObj(patch["inputs"])) {
|
|
538
|
+
throw new RemoteError("draft/bad-patch", "inputs must be an object (channel -> wiring)", {});
|
|
539
|
+
}
|
|
540
|
+
if ("inputOutputs" in patch) {
|
|
541
|
+
const io = patch["inputOutputs"];
|
|
542
|
+
if (!Array.isArray(io) ||
|
|
543
|
+
!io.every((entry) => entry === null || typeof entry === "string")) {
|
|
544
|
+
throw new RemoteError("draft/bad-patch", "inputOutputs must be a list of output names (string or null per input slot)", {});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
488
547
|
let drafts;
|
|
489
548
|
try {
|
|
490
549
|
drafts = await openDraftsDomain(this.ctx);
|
|
@@ -534,18 +593,26 @@ let DraftsRemoteService = (() => {
|
|
|
534
593
|
// the domain's cached object; a refusal must leave no trace),
|
|
535
594
|
// then bump and log below. A bench that moved under the caller
|
|
536
595
|
// refuses loud, never merges under it.
|
|
537
|
-
if (!telemetry
|
|
538
|
-
|
|
596
|
+
if (!telemetry) {
|
|
597
|
+
|
|
598
|
+
// null skipped the CAS fence the bench verbs mandate (their
|
|
599
|
+
// law: a non-number expect_rev is a guaranteed refusal) — an
|
|
600
|
+
// unfenced author write merged silently under a moved bench.
|
|
601
|
+
// The door speaks the bench's voice, and the refusal carries
|
|
602
|
+
// current_rev so a rebase-and-retry lands (the client's CAS
|
|
603
|
+
// predicate already names this code).
|
|
604
|
+
if (expectRev === null) {
|
|
605
|
+
throw new RemoteError("bench/cas-conflict", "expect_rev is required (the CAS law)", { current_rev: bench.rev });
|
|
606
|
+
}
|
|
607
|
+
if (bench.rev !== expectRev) {
|
|
608
|
+
throw new RemoteError("bench/cas-conflict", `bench moved under the patch (expect_rev ${expectRev}, current ${bench.rev}) — re-read and re-apply`, { current_rev: bench.rev, expect_rev: expectRev });
|
|
609
|
+
}
|
|
539
610
|
}
|
|
540
611
|
// THE TELEMETRY WRITE IS DEFERRED (the bench audit's gap 1,
|
|
541
|
-
|
|
542
|
-
//
|
|
543
|
-
//
|
|
544
|
-
|
|
545
|
-
// result with no rev and no log (the copy-on-judge law's exact
|
|
546
|
-
// shape: a refused write must leave NOTHING behind). The arms
|
|
547
|
-
// now run as one closure AFTER both gates pass.
|
|
548
|
-
const writeTelemetry = () => {
|
|
612
|
+
// The arms run as one closure AFTER both gates pass. The closure
|
|
613
|
+
// takes the row as its PARAM (shadowing this section's binding):
|
|
614
|
+
// the R6 cure below re-resolves the LIVE row at call time.
|
|
615
|
+
const writeTelemetry = (stage) => {
|
|
549
616
|
if ("result" in patch) {
|
|
550
617
|
// THE OUTPUT HISTORY (stroke 40 — the operator's QOL law: the
|
|
551
618
|
// preview's mini row is a CARD OUTPUT HISTORY): a stage's
|
|
@@ -566,6 +633,29 @@ let DraftsRemoteService = (() => {
|
|
|
566
633
|
// edited between runs must not retroactively re-label the old
|
|
567
634
|
// generation. The archive entry carries the snapshot; the
|
|
568
635
|
// client diffs it against the live params for the chip.
|
|
636
|
+
// THE TERMINAL-HOLDS LAW (bench-trail W3, judged PRE-write): a
|
|
637
|
+
// settled run's row never un-settles — a stale poll/tail pass
|
|
638
|
+
// re-stamping a non-terminal word for the SAME run_id writes
|
|
639
|
+
// NOTHING (result included; the tile's stuck face). A
|
|
640
|
+
// different run_id is a fresh press and runs.
|
|
641
|
+
const settledRunId = typeof stage["result"] === "object" && stage["result"] !== null && !Array.isArray(stage["result"])
|
|
642
|
+
? stage["result"]["run_id"]
|
|
643
|
+
: undefined;
|
|
644
|
+
const patchRunId = typeof patch["result"] === "object" && patch["result"] !== null && !Array.isArray(patch["result"])
|
|
645
|
+
? patch["result"]["run_id"]
|
|
646
|
+
: undefined;
|
|
647
|
+
if (isTerminalWord(stage["runStatus"]) && !isTerminalWord(patch["runStatus"])
|
|
648
|
+
&& typeof patchRunId === "string" && sameRunIdentity(settledRunId, patchRunId))
|
|
649
|
+
return;
|
|
650
|
+
// THE HUSK-DOWNGRADE REFUSAL (trail-defect-hunt §2): a
|
|
651
|
+
// terminal, url-less result for a run whose settled face
|
|
652
|
+
// carries urls is bookkeeping (the seat's own mirror husk
|
|
653
|
+
// racing the client's rich patch) — the hero slot never
|
|
654
|
+
// downgrades. A DIFFERENT run_id is a fresh press and runs.
|
|
655
|
+
if (isTerminalWord(patch["runStatus"]) && sameRunIdentity(settledRunId, patchRunId)
|
|
656
|
+
&& resultOutputUrls(stage["result"]).length > 0
|
|
657
|
+
&& resultOutputUrls(patch["result"]).length === 0)
|
|
658
|
+
return;
|
|
569
659
|
const prev = stage["result"];
|
|
570
660
|
const next = patch["result"];
|
|
571
661
|
|
|
@@ -575,25 +665,49 @@ let DraftsRemoteService = (() => {
|
|
|
575
665
|
// archives ride the SAME write, oldest-first so the order in
|
|
576
666
|
// the list is append-stable:
|
|
577
667
|
// 1. SUPERSEDE — a cleared/overwritten result that carried
|
|
578
|
-
// outputs is archived (the stroke-40 law, unchanged
|
|
668
|
+
// outputs is archived (the stroke-40 law, unchanged: a
|
|
669
|
+
// face that would LOSE urls never drops them).
|
|
579
670
|
// 2. LANDING — the incoming result itself, when it CARRIES
|
|
580
|
-
// outputs
|
|
581
|
-
//
|
|
582
|
-
//
|
|
583
|
-
//
|
|
584
|
-
//
|
|
585
|
-
//
|
|
586
|
-
//
|
|
587
|
-
//
|
|
588
|
-
//
|
|
671
|
+
// outputs AND the write is not a LIVE face (the
|
|
672
|
+
// settled-landing law, bench-trail W1: the current run's
|
|
673
|
+
// urls never enter history before the card finishes; a
|
|
674
|
+
// mid-run face archives nothing). A result-only patch (no
|
|
675
|
+
// runStatus arm) is a pick/manual spell, not a run face —
|
|
676
|
+
// it lands unless the row reads a live word (the landing
|
|
677
|
+
// gate below).
|
|
678
|
+
// History[0] is ALWAYS the newest SETTLED gen. Live
|
|
679
|
+
// mid-run patches ({run_id, progress} — no urls) archive
|
|
680
|
+
// nothing either way, and the dedup makes re-landings
|
|
681
|
+
// idempotent.
|
|
589
682
|
const archiveInto = (urls, entry) => {
|
|
590
683
|
const hist = Array.isArray(stage["history"]) ? [...stage["history"]] : [];
|
|
591
|
-
|
|
684
|
+
// THE ONE-ROW-PER-RUN LAW (bench-trail W2): a run is one
|
|
685
|
+
// history entry — the media route and the gallery proxy
|
|
686
|
+
// spell the same file differently, so identity is the
|
|
687
|
+
// run_id, with the canonical-url set as the no-id fallback
|
|
688
|
+
const entryRunId = typeof entry["run_id"] === "string" ? entry["run_id"] : undefined;
|
|
689
|
+
if (entryRunId !== undefined) {
|
|
690
|
+
const already = hist.some((e) => sameRunIdentity(e["run_id"], entryRunId));
|
|
691
|
+
if (already)
|
|
692
|
+
return;
|
|
693
|
+
}
|
|
694
|
+
const canon = urls.map(canonicalOutputUrl);
|
|
695
|
+
const already = hist.some((e) => sameUrlSet(resultOutputUrls(e).map(canonicalOutputUrl), canon));
|
|
592
696
|
if (!already) {
|
|
593
697
|
hist.unshift(entry);
|
|
594
698
|
stage["history"] = hist.slice(0, 12);
|
|
595
699
|
}
|
|
596
700
|
};
|
|
701
|
+
// the landing gate (bench-trail W1 vs the pinned pick-spell
|
|
702
|
+
// contract, remote.spec "empty {} snapshot"): a write that
|
|
703
|
+
// DECLARES a live run face (a non-terminal runStatus arm)
|
|
704
|
+
// never lands; a terminal arm lands; and a patch with NO
|
|
705
|
+
// runStatus arm at all is not a run face — it lands unless the
|
|
706
|
+
// row is mid-run (a live word), which is the stuck-row wound's
|
|
707
|
+
// re-add shape.
|
|
708
|
+
const LIVE_TRAIL_WORDS = new Set(["running", "queued", "pending", "stopping"]);
|
|
709
|
+
const landingTerminal = isTerminalWord(patch["runStatus"]) ||
|
|
710
|
+
(!("runStatus" in patch) && !LIVE_TRAIL_WORDS.has(String(stage["runStatus"])));
|
|
597
711
|
// the params arm rides only when the snapshot NAMES something —
|
|
598
712
|
// an undefined or empty {} snapshot carries no key (the chip
|
|
599
713
|
// diffs against live params; an empty diff is no chip at all)
|
|
@@ -610,7 +724,7 @@ let DraftsRemoteService = (() => {
|
|
|
610
724
|
stage["result"] = patch["result"];
|
|
611
725
|
stage["resultParams"] = stage["params"];
|
|
612
726
|
const landingUrls = resultOutputUrls(next);
|
|
613
|
-
if (landingUrls.length > 0) {
|
|
727
|
+
if (landingUrls.length > 0 && landingTerminal) {
|
|
614
728
|
archiveInto(landingUrls, {
|
|
615
729
|
...next,
|
|
616
730
|
...paramsArm(stage["resultParams"]),
|
|
@@ -634,6 +748,24 @@ let DraftsRemoteService = (() => {
|
|
|
634
748
|
// when the body carries no params arm) — judged as half of the
|
|
635
749
|
// (params, inputs) PAIR before either assignment rides.
|
|
636
750
|
let pendingParams;
|
|
751
|
+
|
|
752
|
+
|
|
753
|
+
// array (the batch-P corruption family) or boolean body answered
|
|
754
|
+
// delivered:true with a rev bump while merging NOTHING, and the
|
|
755
|
+
// bench verb refuses the identical body. The GUI's keystroke
|
|
756
|
+
// door speaks the bench's voice.
|
|
757
|
+
if ("params" in patch && patch["params"] !== null &&
|
|
758
|
+
(typeof patch["params"] !== "object" || Array.isArray(patch["params"]))) {
|
|
759
|
+
throw new RemoteError("bench/bad-body", "params must be an object", {});
|
|
760
|
+
}
|
|
761
|
+
// WHOLE-MAP DELETE (the type comment's promise, S-E1's second
|
|
762
|
+
// half): the per-key law at map scale — presence IS the write,
|
|
763
|
+
// and a null params value DELETES the whole map. The empty view
|
|
764
|
+
// rides the SAME pair-gate below (a delete can orphan a $spell
|
|
765
|
+
// into a dead wire exactly like a per-key null can).
|
|
766
|
+
const paramsWholeDelete = "params" in patch && patch["params"] === null;
|
|
767
|
+
if (paramsWholeDelete)
|
|
768
|
+
pendingParams = {};
|
|
637
769
|
if ("params" in patch && patch["params"] !== null && typeof patch["params"] === "object" && !Array.isArray(patch["params"])) {
|
|
638
770
|
// PER-KEY merge (never replace) — the old door's law, now with
|
|
639
771
|
// JSON Merge Patch null semantics: a null value DELETES the
|
|
@@ -745,7 +877,16 @@ let DraftsRemoteService = (() => {
|
|
|
745
877
|
.filter((id) => id !== stageId);
|
|
746
878
|
admitWiring(stage, pendingParams !== undefined
|
|
747
879
|
? pendingParams
|
|
748
|
-
: (isObj(stage["params"]) ? stage["params"] : {}), mergedInputs, liveIds, stageId, draftId.replace(/^bench-/, "")
|
|
880
|
+
: (isObj(stage["params"]) ? stage["params"] : {}), mergedInputs, liveIds, stageId, draftId.replace(/^bench-/, ""),
|
|
881
|
+
|
|
882
|
+
// author seat tolerates the row's PRE wounds — a
|
|
883
|
+
// pre-broken input ref (dead before this patch) must not
|
|
884
|
+
// wedge every later keystroke with a 400 on the GUI's own
|
|
885
|
+
// param door. The tolerate set is built from the PRE row's
|
|
886
|
+
// inputs against the pre-live ids (the assignments below
|
|
887
|
+
// have not landed yet), so a fresh dead ref this patch
|
|
888
|
+
// introduces stays strict.
|
|
889
|
+
toleratedByRow([stage], new Set(liveIds)).get(stageId));
|
|
749
890
|
}
|
|
750
891
|
stage["inputs"] = mergedInputs;
|
|
751
892
|
}
|
|
@@ -755,16 +896,30 @@ let DraftsRemoteService = (() => {
|
|
|
755
896
|
// the raw one — so a doc written here held a self-naming model
|
|
756
897
|
// key that add/load never store, and the next reader judged a
|
|
757
898
|
// different object than the one on disk. Strip once, judge that,
|
|
758
|
-
// store that.
|
|
759
|
-
|
|
899
|
+
// store that. The whole-map delete takes the key out entirely —
|
|
900
|
+
// the row goes bare, the same shape a fresh add carries.
|
|
901
|
+
if (paramsWholeDelete)
|
|
902
|
+
delete stage["params"];
|
|
903
|
+
else
|
|
904
|
+
stage["params"] = strippedForAdmission(typeof stage["card"] === "string" ? stage["card"] : "", pendingParams);
|
|
760
905
|
}
|
|
761
906
|
if ("inputOutputs" in patch)
|
|
762
907
|
stage["inputOutputs"] = patch["inputOutputs"];
|
|
763
908
|
// EVERY GATE PASSED — the telemetry write lands now (deferred
|
|
764
909
|
// above so a refusal left no result behind; the run seat's own
|
|
765
910
|
// polls ride the same door and are judged by nothing, which is
|
|
766
|
-
// correct: a poll is not a composition)
|
|
767
|
-
|
|
911
|
+
// correct: a poll is not a composition).
|
|
912
|
+
// R6 CURE (round-14 race audit, LOW — telemetry-vs-remove window):
|
|
913
|
+
// the row this section resolved at its top can be REMOVED while
|
|
914
|
+
// the gates ran (remove_stage's resume section between the lookup
|
|
915
|
+
// and this call) — writing into the detached row then persisted
|
|
916
|
+
// the removal with the outcome written nowhere. Re-resolve the
|
|
917
|
+
// LIVE row here, immediately before the write: a still-present
|
|
918
|
+
// row takes the telemetry; a removed row skips the write (its
|
|
919
|
+
// next poll reads the honest delivered:false at the top lookup).
|
|
920
|
+
const liveStage = doc["stages"].find((s) => typeof s === "object" && s !== null && s["id"] === stageId);
|
|
921
|
+
if (liveStage !== undefined)
|
|
922
|
+
writeTelemetry(liveStage);
|
|
768
923
|
// THE BUMP (author patches only — the telemetry law above): the
|
|
769
924
|
// rev advances and the log carries the entry; a telemetry write
|
|
770
925
|
// persists the row WITHOUT touching the bench block (no bump, no
|
|
@@ -899,69 +1054,86 @@ let BenchRemoteService = (() => {
|
|
|
899
1054
|
async *subscribe(sessionKey, signal) {
|
|
900
1055
|
signal.throwIfAborted();
|
|
901
1056
|
for (;;) {
|
|
902
|
-
|
|
903
|
-
//
|
|
904
|
-
//
|
|
905
|
-
//
|
|
906
|
-
//
|
|
907
|
-
// the
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
let row;
|
|
1057
|
+
|
|
1058
|
+
// watch attaches BEFORE the read, not after the yield. A write
|
|
1059
|
+
// landing while the consumer still holds the delivered row (the
|
|
1060
|
+
// generator suspended at the yield — no park listener attached)
|
|
1061
|
+
// once matched no listener and the park slept until the NEXT
|
|
1062
|
+
// write; the flag re-loops at once instead of parking past it.
|
|
1063
|
+
let changedMidFlight = false;
|
|
1064
|
+
const offWatch = onBenchChange((session) => {
|
|
1065
|
+
if (session === sessionKey)
|
|
1066
|
+
changedMidFlight = true;
|
|
1067
|
+
});
|
|
914
1068
|
try {
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
else {
|
|
930
|
-
throw error;
|
|
1069
|
+
// THE ZERO-STATE LAW (the stream tail's birth watch): a fresh
|
|
1070
|
+
// conversation's bench does not exist until its first write.
|
|
1071
|
+
// The gateway's RemoteStream classifies a REJECTED open as
|
|
1072
|
+
// TERMINAL (no retry — terminalStreamFailure), so a throwing
|
|
1073
|
+
// first read here would kill the browser's whole bench stream
|
|
1074
|
+
// the moment a conversation starts. The watch instead serves
|
|
1075
|
+
// the ZERO STATE (rev 0, empty stages — the same row shape the
|
|
1076
|
+
// client's emptyBench served in the poll era) and stays open:
|
|
1077
|
+
// the birth (ensure's notifyBenchChange) wakes the park and the
|
|
1078
|
+
// next read serves the born bench. `bench/get` still refuses
|
|
1079
|
+
// not-found for direct unary callers — only the WATCH degrades.
|
|
1080
|
+
let row;
|
|
1081
|
+
try {
|
|
1082
|
+
row = await this.get(sessionKey, signal);
|
|
931
1083
|
}
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
1084
|
+
catch (error) {
|
|
1085
|
+
if (error instanceof RemoteError &&
|
|
1086
|
+
error.code === "bench/not-found") {
|
|
1087
|
+
row = {
|
|
1088
|
+
draft_id: benchDraftIdOf(sessionKey),
|
|
1089
|
+
session: sessionKey,
|
|
1090
|
+
rev: 0,
|
|
1091
|
+
intent: "",
|
|
1092
|
+
stages: [],
|
|
1093
|
+
log: [],
|
|
1094
|
+
};
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
throw error;
|
|
1098
|
+
}
|
|
945
1099
|
}
|
|
946
|
-
|
|
947
|
-
//
|
|
948
|
-
//
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
if (
|
|
1100
|
+
yield row;
|
|
1101
|
+
// wake on the session's next write (or abort). An abort that
|
|
1102
|
+
// landed while the generator was suspended AT A YIELD has no
|
|
1103
|
+
// park listener attached — the executor re-checks
|
|
1104
|
+
// signal.aborted AND the mid-flight watch flag (S-S3) so the
|
|
1105
|
+
// missed event still wakes the park. The stream then ends
|
|
1106
|
+
// CLEAN on abort (done: true — the holder released; a throw
|
|
1107
|
+
// would surface as an error on a normal close). One finish arm
|
|
1108
|
+
// removes BOTH listeners: no stale abort listener per park.
|
|
1109
|
+
await new Promise((resolve) => {
|
|
1110
|
+
if (signal.aborted || changedMidFlight) {
|
|
1111
|
+
resolve();
|
|
957
1112
|
return;
|
|
958
|
-
|
|
1113
|
+
}
|
|
1114
|
+
// One finish arm, no re-entry guard needed: finish detaches
|
|
1115
|
+
// BOTH listeners before resolving, so it can never run twice
|
|
1116
|
+
// (the old settled-guard was unreachable by construction).
|
|
1117
|
+
const offAbort = () => { signal.removeEventListener("abort", onAbort); };
|
|
1118
|
+
const finish = () => {
|
|
1119
|
+
offChange();
|
|
1120
|
+
offAbort();
|
|
1121
|
+
resolve();
|
|
1122
|
+
};
|
|
1123
|
+
const offChange = onBenchChange((session) => {
|
|
1124
|
+
if (session !== sessionKey)
|
|
1125
|
+
return;
|
|
1126
|
+
finish();
|
|
1127
|
+
});
|
|
1128
|
+
const onAbort = () => { finish(); };
|
|
1129
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
959
1130
|
});
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
}
|
|
963
|
-
|
|
964
|
-
|
|
1131
|
+
if (signal.aborted)
|
|
1132
|
+
return;
|
|
1133
|
+
}
|
|
1134
|
+
finally {
|
|
1135
|
+
offWatch();
|
|
1136
|
+
}
|
|
965
1137
|
}
|
|
966
1138
|
}
|
|
967
1139
|
/** Endpoint `bench/ensure` — birth or load (the only creating arm). */
|
|
@@ -996,7 +1168,8 @@ let BenchRemoteService = (() => {
|
|
|
996
1168
|
catch (error) {
|
|
997
1169
|
throw new RemoteError("bench/store-failed", thrownMessage(error), {}, { cause: error });
|
|
998
1170
|
}
|
|
999
|
-
const
|
|
1171
|
+
const session = typeof doc["session"] === "string" ? doc["session"] : "";
|
|
1172
|
+
const row = await withBenchWrite(benchDraftIdOf(session), () => ensureBench(drafts, doc, benchAdmissionOf(this.ctx)));
|
|
1000
1173
|
// ensureBench gates doc.session as a valid key before this line —
|
|
1001
1174
|
// the row's own session can never differ here (no dead fallback).
|
|
1002
1175
|
notifyBenchChange(String(doc["session"]));
|
|
@@ -1030,27 +1203,33 @@ let BenchRemoteService = (() => {
|
|
|
1030
1203
|
|
|
1031
1204
|
async remoteExportReorder(sessionKey, doc, signal) {
|
|
1032
1205
|
signal.throwIfAborted();
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1206
|
+
return withBenchWrite(benchDraftIdOf(sessionKey), async () => {
|
|
1207
|
+
const drafts = await this.open(signal);
|
|
1208
|
+
const row = await reorderBench(drafts, sessionKey, doc);
|
|
1209
|
+
notifyBenchChange(sessionKey);
|
|
1210
|
+
return row;
|
|
1211
|
+
});
|
|
1037
1212
|
}
|
|
1038
1213
|
|
|
1039
1214
|
|
|
1040
1215
|
async remoteExportGroup(sessionKey, doc, signal) {
|
|
1041
1216
|
signal.throwIfAborted();
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1217
|
+
return withBenchWrite(benchDraftIdOf(sessionKey), async () => {
|
|
1218
|
+
const drafts = await this.open(signal);
|
|
1219
|
+
const row = await groupBench(drafts, sessionKey, doc, benchAdmissionOf(this.ctx));
|
|
1220
|
+
notifyBenchChange(sessionKey);
|
|
1221
|
+
return row;
|
|
1222
|
+
});
|
|
1046
1223
|
}
|
|
1047
1224
|
/** Endpoint `bench/ungroup` — spill a compound's children back. */
|
|
1048
1225
|
async remoteExportUngroup(sessionKey, doc, signal) {
|
|
1049
1226
|
signal.throwIfAborted();
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1227
|
+
return withBenchWrite(benchDraftIdOf(sessionKey), async () => {
|
|
1228
|
+
const drafts = await this.open(signal);
|
|
1229
|
+
const row = await ungroupBench(drafts, sessionKey, doc, benchAdmissionOf(this.ctx));
|
|
1230
|
+
notifyBenchChange(sessionKey);
|
|
1231
|
+
return row;
|
|
1232
|
+
});
|
|
1054
1233
|
}
|
|
1055
1234
|
/** Endpoint `bench/undo` — walk the log tail, apply inverses. */
|
|
1056
1235
|
async remoteExportUndo(sessionKey, doc, signal) {
|
|
@@ -1070,45 +1249,57 @@ let BenchRemoteService = (() => {
|
|
|
1070
1249
|
}
|
|
1071
1250
|
async patch(session, doc, signal) {
|
|
1072
1251
|
signal.throwIfAborted();
|
|
1073
|
-
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1252
|
+
return withBenchWrite(benchDraftIdOf(session), async () => {
|
|
1253
|
+
const drafts = await this.open(signal);
|
|
1254
|
+
const row = await patchBench(drafts, session, doc, benchAdmissionOf(this.ctx), laneMigrationOf(this.ctx));
|
|
1255
|
+
notifyBenchChange(session);
|
|
1256
|
+
return row;
|
|
1257
|
+
});
|
|
1077
1258
|
}
|
|
1078
1259
|
async addStage(session, doc, signal) {
|
|
1079
1260
|
signal.throwIfAborted();
|
|
1080
|
-
|
|
1081
|
-
|
|
1082
|
-
|
|
1083
|
-
|
|
1261
|
+
return withBenchWrite(benchDraftIdOf(session), async () => {
|
|
1262
|
+
const drafts = await this.open(signal);
|
|
1263
|
+
const row = await addStageBench(drafts, session, doc, benchAdmissionOf(this.ctx));
|
|
1264
|
+
notifyBenchChange(session);
|
|
1265
|
+
return row;
|
|
1266
|
+
});
|
|
1084
1267
|
}
|
|
1085
1268
|
async removeStage(session, doc, signal) {
|
|
1086
1269
|
signal.throwIfAborted();
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1270
|
+
return withBenchWrite(benchDraftIdOf(session), async () => {
|
|
1271
|
+
const drafts = await this.open(signal);
|
|
1272
|
+
const row = await removeStageBench(drafts, session, doc);
|
|
1273
|
+
notifyBenchChange(session);
|
|
1274
|
+
return row;
|
|
1275
|
+
});
|
|
1091
1276
|
}
|
|
1092
1277
|
async clear(session, doc, signal) {
|
|
1093
1278
|
signal.throwIfAborted();
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1279
|
+
return withBenchWrite(benchDraftIdOf(session), async () => {
|
|
1280
|
+
const drafts = await this.open(signal);
|
|
1281
|
+
const row = await clearBench(drafts, session, doc);
|
|
1282
|
+
notifyBenchChange(session);
|
|
1283
|
+
return row;
|
|
1284
|
+
});
|
|
1098
1285
|
}
|
|
1099
1286
|
async rewire(session, doc, signal) {
|
|
1100
1287
|
signal.throwIfAborted();
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1288
|
+
return withBenchWrite(benchDraftIdOf(session), async () => {
|
|
1289
|
+
const drafts = await this.open(signal);
|
|
1290
|
+
const row = await rewireBench(drafts, session, doc);
|
|
1291
|
+
notifyBenchChange(session);
|
|
1292
|
+
return row;
|
|
1293
|
+
});
|
|
1105
1294
|
}
|
|
1106
1295
|
async undo(session, doc, signal) {
|
|
1107
1296
|
signal.throwIfAborted();
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1297
|
+
return withBenchWrite(benchDraftIdOf(session), async () => {
|
|
1298
|
+
const drafts = await this.open(signal);
|
|
1299
|
+
const row = await undoBench(drafts, session, doc, benchAdmissionOf(this.ctx));
|
|
1300
|
+
notifyBenchChange(session);
|
|
1301
|
+
return row;
|
|
1302
|
+
});
|
|
1112
1303
|
}
|
|
1113
1304
|
};
|
|
1114
1305
|
})();
|