@githolon/dsl 0.105.3 → 0.105.5

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/dart/CHANGELOG.md CHANGED
@@ -1,3 +1,22 @@
1
+ ## 0.64.20
2
+
3
+ - NEW: `bridge.localWorkRejectedEvents` — a typed stream of `NomosLocalWorkRejectedEvent` (from
4
+ nomos_types 0.8.6) forwarded over the runner bridge, so apps render honest UX ("queued, will
5
+ retry" vs "preserved for review — contact support") instead of scraping raw refusal prose.
6
+ - `parentSignerEnrollment` gains `remote: true` — force the CLOUD-gateway read even when this holon
7
+ IS the parent: the seated barrier's proof-by-read (enrolled there ⇒ durably in custody, not merely
8
+ locally folded).
9
+ - Rebuild the bundled runner from @githolon/client 0.105.5 (the seal-obligation release: footage-first
10
+ offer lane, scoreboard truth, FF-only seals, seal retry, no-evict-while-dirty).
11
+
12
+ ## 0.64.19
13
+
14
+ - Rebuild the bundled runner assets from @githolon/client 0.105.3: fixes `ensureParentSignerEnrollment`
15
+ routing `enrollFirstDevice` when the caller is already connected directly to the target workspace
16
+ (`parent === workspace`) — the local structural lookup can never find it (deliberately absent from
17
+ public business OpenAPI), and the framework-route fallback was previously gated to skip exactly this
18
+ case.
19
+
1
20
  ## 0.64.18
2
21
 
3
22
  - Restaged with the current kernel candidate: regenerated vendored assets (kernel release identity is baked into them). Published pub.dev archives are immutable, so regenerated bytes ride their own version.
@@ -338,6 +338,10 @@ function createWebviewRunner({ post: post2, openRealm: openRealm2 }) {
338
338
  holon.onCustodyEvent?.((evt) => post2({ v: WEBVIEW_BRIDGE_VERSION, event: "custody", session, custody: evt }));
339
339
  } catch {
340
340
  }
341
+ try {
342
+ holon.onLocalWorkRejectedEvent?.((evt) => post2({ v: WEBVIEW_BRIDGE_VERSION, event: "localWorkRejected", session, localWorkRejected: evt }));
343
+ } catch {
344
+ }
341
345
  if (a.onCustodyBreak === "ask") {
342
346
  holon.onCustodyBreak = (brk) => new Promise((resolve) => {
343
347
  pendingCustodyAsks.set(session, resolve);
@@ -8096,7 +8100,7 @@ function createGovernanceOffers(ctx) {
8096
8100
  * `revoked: true` means the subject's leaf was flipped `status:revoked` (remove-wins — a lawful
8097
8101
  * re-enroll of the SAME key is refused; mint + enroll a new device key instead).
8098
8102
  */
8099
- parentSignerEnrollment: async ({ parent, principal, keyHash, publicKey, authToken } = {}) => {
8103
+ parentSignerEnrollment: async ({ parent, principal, keyHash, publicKey, authToken, remote } = {}) => {
8100
8104
  if (!parent || typeof parent !== "string") throw new Error("parentSignerEnrollment requires `parent` (the workspace whose chain holds the signer tuple)");
8101
8105
  if (!principal || typeof principal !== "string") throw new Error("parentSignerEnrollment requires `principal` (the auth-provider uid)");
8102
8106
  const p = canonicalPrincipal(principal);
@@ -8106,7 +8110,7 @@ function createGovernanceOffers(ctx) {
8106
8110
  }
8107
8111
  const object = signerObjectOf(p);
8108
8112
  let rows;
8109
- if (parent === workspace) {
8113
+ if (parent === workspace && !remote) {
8110
8114
  rows = JSON.parse(call("query", { queryBytes: qBytes2({ kind: "query", queryId: "relationTuplesByObject", paramsJson: JSON.stringify({ object }) }) })) || [];
8111
8115
  } else {
8112
8116
  const headers = authToken ? { Authorization: `Bearer ${authToken}`, "x-nomos-auth": authToken } : authHeaders;
@@ -8635,7 +8639,7 @@ function birthOutcomeFailure(birthOutcomes) {
8635
8639
  err.birthOutcomes = birthOutcomes;
8636
8640
  return err;
8637
8641
  }
8638
- async function connectSingle({ cloud, workspace, clientId, authToken, restoreFrom, onSpan, authorSecret, principal, allowUnborn = false, salvageOnly = false, recover = false, mountInPlace = false, __injectCheckpoint, kernelProvider, fetchImpl, historicSnapshots, onCustodyBreak, watchRebind, openWorkspaceInPlane, __knownKernelCapabilities, __testKRootPublic, __testRootSecret }) {
8642
+ async function connectSingle({ cloud, workspace, clientId, authToken, restoreFrom, onSpan, authorSecret, principal, allowUnborn = false, salvageOnly = false, recover = false, mountInPlace = false, __injectCheckpoint, kernelProvider, fetchImpl, historicSnapshots, onCustodyBreak, onLocalWorkRejected, watchRebind, openWorkspaceInPlane, __knownKernelCapabilities, __testKRootPublic, __testRootSecret }) {
8639
8643
  if (salvageOnly) allowUnborn = true;
8640
8644
  if (!cloud || !workspace) throw new Error("connect({ cloud, workspace }) required");
8641
8645
  const _fetch = typeof fetchImpl === "function" ? fetchImpl : globalThis.fetch;
@@ -8797,6 +8801,47 @@ ${String(commit).toLowerCase()}`;
8797
8801
  }
8798
8802
  };
8799
8803
  const onCustodyBreakOpt = typeof onCustodyBreak === "function" ? onCustodyBreak : null;
8804
+ let localWorkRejectedHook = typeof onLocalWorkRejected === "function" ? onLocalWorkRejected : null;
8805
+ const localWorkRejectedListeners = /* @__PURE__ */ new Set();
8806
+ const refusalIsRetryable = (error) => {
8807
+ try {
8808
+ const d = authzDiagnosisFromRefusal(String(error ?? ""));
8809
+ return d?.code === "nomos.authz.signer_enrollment_missing";
8810
+ } catch {
8811
+ return false;
8812
+ }
8813
+ };
8814
+ const emitLocalWorkRejected = (source, refused, resetTo) => {
8815
+ if (!refused.length) return;
8816
+ const items = refused.map((r) => ({
8817
+ intentId: typeof r.intentId === "string" ? r.intentId : null,
8818
+ oid: typeof r.oid === "string" ? r.oid : null,
8819
+ domain: typeof r.domain === "string" ? r.domain : null,
8820
+ directiveId: typeof r.directiveId === "string" ? r.directiveId : null,
8821
+ error: String(r.error ?? "the law refused this offer").slice(0, 600),
8822
+ retryable: refusalIsRetryable(r.error)
8823
+ }));
8824
+ const event = {
8825
+ workspace,
8826
+ refused: items,
8827
+ parked: true,
8828
+ resetTo,
8829
+ retryable: items.every((i) => i.retryable),
8830
+ source
8831
+ };
8832
+ if (localWorkRejectedHook) {
8833
+ try {
8834
+ localWorkRejectedHook(event);
8835
+ } catch {
8836
+ }
8837
+ }
8838
+ for (const l of [...localWorkRejectedListeners]) {
8839
+ try {
8840
+ l(event);
8841
+ } catch {
8842
+ }
8843
+ }
8844
+ };
8800
8845
  function captureIncarnation(j) {
8801
8846
  const inc = j && typeof j.incarnation === "string" && OID40_RE.test(j.incarnation) ? j.incarnation : null;
8802
8847
  if (inc !== null) remoteIncarnation = inc;
@@ -9383,6 +9428,7 @@ ${String(commit).toLowerCase()}`;
9383
9428
  }
9384
9429
  }
9385
9430
  }
9431
+ if (deadLettered.length) emitLocalWorkRejected("sync", deadLettered.map((d) => ({ intentId: d.id, error: d.error })), remoteHead);
9386
9432
  invalidateInstalledLawMetadata();
9387
9433
  nudgeWatches();
9388
9434
  return {
@@ -11695,6 +11741,22 @@ ${String(commit).toLowerCase()}`;
11695
11741
  if (typeof cb === "function") custodyListeners.add(cb);
11696
11742
  return () => custodyListeners.delete(cb);
11697
11743
  },
11744
+ /** THE LOCAL-WORK-REJECTED HOOK (settable; connect({onLocalWorkRejected}) is the default) — the
11745
+ * typed "the cloud refused work your device had accepted" lifecycle event. See
11746
+ * `LocalWorkRejectedEvent`: refused intents are already parked + survivors replayed when it
11747
+ * fires; the app's only job is honest UX (retryable ⇒ "queued, will retry"; terminal ⇒
11748
+ * "preserved for review — contact support"). */
11749
+ get onLocalWorkRejected() {
11750
+ return localWorkRejectedHook;
11751
+ },
11752
+ set onLocalWorkRejected(fn) {
11753
+ localWorkRejectedHook = typeof fn === "function" ? fn : null;
11754
+ },
11755
+ /** Subscribe (multi-listener) to LocalWorkRejectedEvents. Returns an unsubscribe fn. */
11756
+ onLocalWorkRejectedEvent: (cb) => {
11757
+ if (typeof cb === "function") localWorkRejectedListeners.add(cb);
11758
+ return () => localWorkRejectedListeners.delete(cb);
11759
+ },
11698
11760
  /**
11699
11761
  * INTROSPECTION — where this holon sits on the custody spectrum:
11700
11762
  * `{ incarnation, localGenesis, remoteIncarnation, syncPosture, pendingLocalIntents, salvagedIntents,
@@ -13164,6 +13226,10 @@ ${String(commit).toLowerCase()}`;
13164
13226
  });
13165
13227
  _convSpan.end();
13166
13228
  if (result && result.custodyBreak) return await resolveCustodyBreakFlow(result.custodyBreak, { operation: "sync" });
13229
+ if (admissionRefusals && admissionRefusals.size) {
13230
+ const refusedEntries = (admission?.sessions || []).flatMap((s) => (s.rejected || []).map((r) => ({ intentId: r.intent, oid: r.oid, error: r.error })));
13231
+ emitLocalWorkRejected("admission", refusedEntries, result && typeof result.remoteMain === "string" ? result.remoteMain : null);
13232
+ }
13167
13233
  try {
13168
13234
  result.stillAhead = await resolveBranchHead() !== syncedBase;
13169
13235
  } catch {
@@ -137,6 +137,7 @@ __export(engine_exports, {
137
137
  cryptoUnwrapKey: () => cryptoUnwrapKey,
138
138
  cryptoWrapKey: () => cryptoWrapKey,
139
139
  currentLaw: () => currentLaw,
140
+ custodyConfirmedMain: () => custodyConfirmedMain,
140
141
  declaredCapabilities: () => declaredCapabilities,
141
142
  deepVerifyShard: () => deepVerifyShard,
142
143
  delegationCertSign: () => delegationCertSign,
@@ -163,6 +164,7 @@ __export(engine_exports, {
163
164
  ingestPackAsCustody: () => ingestPackAsCustody,
164
165
  installedLaw: () => installedLaw,
165
166
  intentOfferResidencies: () => intentOfferResidencies,
167
+ isDirty: () => isDirty,
166
168
  isWarranted: () => isWarranted,
167
169
  kernelStamp: () => kernelStamp,
168
170
  kgit: () => kgit,
@@ -6803,6 +6805,40 @@ async function admitIntentOffers(eng2, ws, ledger, offers, { receiptFence = null
6803
6805
  const s0 = Date.now();
6804
6806
  return (end = {}) => timings.spans.push({ id: `intent-offer-${t0}-${seq++}`, track: "cloud", name, kind, depth, t0: s0, t1: Date.now(), ...extra, ...end });
6805
6807
  };
6808
+ let footageRef = null;
6809
+ {
6810
+ const parked = [];
6811
+ for (const offer of Array.isArray(offers) ? offers : []) {
6812
+ let blob = offer?.blob instanceof Uint8Array ? offer.blob : null;
6813
+ try {
6814
+ if (!blob && typeof offer?.changeB64 === "string") blob = Uint8Array.from(atob(offer.changeB64), (c) => c.charCodeAt(0));
6815
+ } catch {
6816
+ }
6817
+ if (blob && blob.byteLength) parked.push(blob);
6818
+ }
6819
+ if (parked.length) {
6820
+ footageRef = `refs/heads/parked/offered/${t0.toString(36)}-${Math.floor(Math.random() * 16777215).toString(36)}`;
6821
+ const footageDone = span("park offer footage", "cseal", 1, { offers: parked.length });
6822
+ try {
6823
+ kgit.writeMetaCommit(eng2, gitdirOf(ws), {
6824
+ parents: [],
6825
+ files: parked.map((bytes, i) => ({ path: `${i}.change.usda`, bytes })),
6826
+ message: `offered: ${parked.length} intent(s) parked pre-judgment (footage-first)`,
6827
+ timeSecs: 0,
6828
+ ref: footageRef
6829
+ });
6830
+ await custodyPush(eng2, ws, ledger, { remoteRef: footageRef });
6831
+ } catch (e) {
6832
+ try {
6833
+ kgit.deleteRef(eng2, gitdirOf(ws), footageRef);
6834
+ } catch {
6835
+ }
6836
+ throw new Error(`offer footage not durable \u2014 custody unreachable, nothing was folded; retry the batch: ${String(e).slice(0, 300)}`, { cause: e });
6837
+ } finally {
6838
+ footageDone();
6839
+ }
6840
+ }
6841
+ }
6806
6842
  for (const offer of Array.isArray(offers) ? offers : []) {
6807
6843
  const cid = typeof offer?.session === "string" && offer.session ? offer.session : "direct";
6808
6844
  const rec = sessions.get(cid) || { session: cid, admitted: [], rejected: [], skipped: [] };
@@ -6878,10 +6914,13 @@ async function admitIntentOffers(eng2, ws, ledger, offers, { receiptFence = null
6878
6914
  }
6879
6915
  timings.sessions = sessions.size;
6880
6916
  timings.intents = admittedTotal;
6917
+ let refusalsParked = true;
6881
6918
  if (deadLetters.length) {
6882
6919
  try {
6883
- await materializeRefused(eng2, ws, ledger, deadLetters);
6920
+ const m = await materializeRefused(eng2, ws, ledger, deadLetters);
6921
+ refusalsParked = m.unparkedSessions.size === 0;
6884
6922
  } catch (e) {
6923
+ refusalsParked = false;
6885
6924
  log("refused-materialize-failed", { ws, error: String(e).slice(0, 200) });
6886
6925
  }
6887
6926
  }
@@ -6890,6 +6929,17 @@ async function admitIntentOffers(eng2, ws, ledger, offers, { receiptFence = null
6890
6929
  await commitAndPush(eng2, ws, ledger);
6891
6930
  sealDone();
6892
6931
  }
6932
+ if (footageRef && refusalsParked) {
6933
+ try {
6934
+ kgit.deleteRef(eng2, gitdirOf(ws), footageRef);
6935
+ } catch {
6936
+ }
6937
+ try {
6938
+ await custodyPush(eng2, ws, ledger, { remoteRef: footageRef, del: true });
6939
+ } catch (e) {
6940
+ log("footage-delete-push-failed", { ws, ref: footageRef, error: String(e).slice(0, 200) });
6941
+ }
6942
+ }
6893
6943
  const gitdir = gitdirOf(ws);
6894
6944
  const head = kgit.resolveRef(eng2, gitdir, BRANCH) ?? main.head;
6895
6945
  const summary = summaryCtx ? summaryOfCapture(summaryCtx, headBefore ?? "", head ?? "") : null;
@@ -6903,7 +6953,7 @@ async function admitIntentOffers(eng2, ws, ledger, offers, { receiptFence = null
6903
6953
  }
6904
6954
  log("admit-intent-offers", { ws, sessions: sessions.size, admitted: admittedTotal, rejected: [...sessions.values()].reduce((n, s) => n + s.rejected.length, 0), resultPack: resultPackB64 ? "inline" : "none" });
6905
6955
  const bornPacked = born.length ? packBornChildren(eng2, born) : born;
6906
- return { sessions: [...sessions.values()].map((s) => ({ ...s, ...s.skipped.length ? {} : { skipped: void 0 } })), main: head, deadLetters, timings, summary: summary ?? null, born: bornPacked, birthOutcomes, ...resultPackB64 ? { resultPackB64, resultBase: clientBase } : {} };
6956
+ return { sessions: [...sessions.values()].map((s) => ({ ...s, ...s.skipped.length ? {} : { skipped: void 0 } })), main: head, sealed: true, sealedMain: custodyConfirmedMain(eng2, ws), deadLetters, timings, summary: summary ?? null, born: bornPacked, birthOutcomes, ...resultPackB64 ? { resultPackB64, resultBase: clientBase } : {} };
6907
6957
  }
6908
6958
  function intentOfferResidencies(eng2, offers) {
6909
6959
  const unique = /* @__PURE__ */ new Map();
@@ -7035,6 +7085,7 @@ async function mountWorkspace(eng2, ws, ledger) {
7035
7085
  throw new Error(`kernel refused restored main ${remoteMain}: ${JSON.stringify(mounted)}`);
7036
7086
  }
7037
7087
  restored = true;
7088
+ custodyMainOf(eng2).set(ws, remoteMain);
7038
7089
  verificationVerdict = jsStruct(mounted.verdict);
7039
7090
  }
7040
7091
  } catch (e) {
@@ -7531,7 +7582,7 @@ async function custodyFetch(eng2, ws, ledger, { remoteRef = "refs/heads/main", l
7531
7582
  if (writeRef) kgit.writeRef(eng2, gitdirOf(ws), fullRef(localRef || remoteRef), tip);
7532
7583
  return tip;
7533
7584
  }
7534
- async function custodyPush(eng2, ws, ledger, { localRef = null, remoteRef, del = false, pointer = false, newOid = null }) {
7585
+ async function custodyPush(eng2, ws, ledger, { localRef = null, remoteRef, del = false, pointer = false, newOid = null, ffOnly = false }) {
7535
7586
  const headers = ledger.headers || {};
7536
7587
  const remoteRefFull = fullRef(remoteRef);
7537
7588
  let remote;
@@ -7547,22 +7598,46 @@ async function custodyPush(eng2, ws, ledger, { localRef = null, remoteRef, del =
7547
7598
  }
7548
7599
  const tgt = newOid || kgit.resolveRef(eng2, gitdirOf(ws), fullRef(localRef || remoteRef));
7549
7600
  if (!tgt) throw new Error(`custodyPush: local ref ${localRef || remoteRef} unresolved`);
7550
- if (tgt === oldOid) return;
7601
+ if (tgt === oldOid) {
7602
+ if (remoteRefFull === fullRef(BRANCH)) custodyMainOf(eng2).set(ws, tgt);
7603
+ return;
7604
+ }
7551
7605
  let pack = null;
7552
7606
  if (!pointer) {
7553
7607
  pack = oldOid !== ZERO_OID ? packDelta(eng2, ws, oldOid, tgt) : null;
7608
+ if (!pack && ffOnly && oldOid !== ZERO_OID) {
7609
+ throw new Error(`custody ${remoteRefFull} regression refused: remote ${oldOid.slice(0, 12)} is not an ancestor of local ${tgt.slice(0, 12)} \u2014 this engine's view is stale; re-mount from custody instead of forcing`);
7610
+ }
7554
7611
  if (!pack) pack = packDelta(eng2, ws, "", tgt);
7555
7612
  }
7556
7613
  const packBytes = pack?.byteLength ?? 0;
7557
7614
  try {
7558
- return await pushVia(ledger, () => pushPack(ledger.remote, headers, { updates: [{ ref: remoteRefFull, oldOid, newOid: tgt }], pack }));
7615
+ const r = await pushVia(ledger, () => pushPack(ledger.remote, headers, { updates: [{ ref: remoteRefFull, oldOid, newOid: tgt }], pack }));
7616
+ if (remoteRefFull === fullRef(BRANCH)) custodyMainOf(eng2).set(ws, tgt);
7617
+ return r;
7559
7618
  } catch (error) {
7560
7619
  throw new Error(`custody receive-pack push failed for ${remoteRefFull} (${packBytes} pack bytes): ${String(error)}`, { cause: error });
7561
7620
  }
7562
7621
  }
7622
+ function custodyMainOf(eng2) {
7623
+ eng2._custodyMain ??= /* @__PURE__ */ new Map();
7624
+ return eng2._custodyMain;
7625
+ }
7626
+ function custodyConfirmedMain(eng2, ws) {
7627
+ return custodyMainOf(eng2).get(ws) ?? null;
7628
+ }
7629
+ function isDirty(eng2, ws) {
7630
+ try {
7631
+ const resident = kgit.resolveRef(eng2, gitdirOf(ws), BRANCH);
7632
+ if (!resident) return false;
7633
+ return resident !== custodyMainOf(eng2).get(ws);
7634
+ } catch {
7635
+ return false;
7636
+ }
7637
+ }
7563
7638
  async function commitAndPush(eng2, ws, ledger) {
7564
7639
  try {
7565
- await custodyPush(eng2, ws, ledger, { remoteRef: "refs/heads/main" });
7640
+ await custodyPush(eng2, ws, ledger, { remoteRef: "refs/heads/main", ffOnly: true });
7566
7641
  } catch (e) {
7567
7642
  unmount(eng2, ws);
7568
7643
  const cause = e instanceof Error ? e.cause : void 0;
@@ -8238,6 +8313,80 @@ async function admitAll(eng2, ws, ledger, { receiptFence = null, shardRouting =
8238
8313
  _sessionDone();
8239
8314
  }
8240
8315
  }
8316
+ const offeredRefPrefix = "refs/heads/parked/offered/";
8317
+ const footageConsumed = [];
8318
+ const footageBorn = [], footageBirthOutcomes = [];
8319
+ try {
8320
+ const offeredRefs = Object.entries(await custodyLsRefs(ledger, offeredRefPrefix));
8321
+ for (const [oref, ooid] of offeredRefs) {
8322
+ if (!oref.startsWith(offeredRefPrefix) || !ooid) continue;
8323
+ const fcid = `offered:${oref.slice(offeredRefPrefix.length)}`;
8324
+ const _fDone = _span(`footage ${fcid.slice(0, 24)}`, "csession", 1, { client: fcid });
8325
+ const admitted = [], rejected = [], skipped = [];
8326
+ try {
8327
+ try {
8328
+ await custodyFetch(eng2, ws, ledger, { remoteRef: oref, have: kgit.resolveRef(eng2, gitdir, BRANCH) });
8329
+ } catch {
8330
+ await custodyFetch(eng2, ws, ledger, { remoteRef: oref, have: null });
8331
+ }
8332
+ const main = await mainIntentIds(eng2, ws);
8333
+ for (let i = 0; ; i++) {
8334
+ let blob = null;
8335
+ try {
8336
+ blob = kgit.readBlob(eng2, gitdir, ooid, `${i}.change.usda`);
8337
+ } catch {
8338
+ blob = null;
8339
+ }
8340
+ if (!blob || !blob.byteLength) break;
8341
+ try {
8342
+ const inspected = kgit.inspectChange(eng2, blob);
8343
+ const intentId = inspected.intentId || null;
8344
+ const oid = String(intentId || `${fcid}#${i}`).slice(0, 10);
8345
+ if (!inspected.authored || !inspected.call) {
8346
+ rejected.push({ oid, error: "parked offer is not an authored nominal call" });
8347
+ continue;
8348
+ }
8349
+ const { domainPath: dom, directivePath: dirId } = inspected.call;
8350
+ if (receiptFence && dirId && Array.isArray(receiptFence[dom]) && receiptFence[dom].includes(dirId)) {
8351
+ const error = `'${dom}/${dirId}' is a capability RECEIPT \u2014 receipts are executor/owner writes, never open-session writes`;
8352
+ rejected.push({ oid, error, deadLettered: true });
8353
+ deadLetters.push({ intentId, oid, session: fcid, error, blob, domain: dom, directiveId: dirId });
8354
+ continue;
8355
+ }
8356
+ if (intentId && main.ids.has(intentId)) {
8357
+ skipped.push({ oid, intent: intentId, alreadyOnMain: true });
8358
+ continue;
8359
+ }
8360
+ const res = offerAdmit(eng2, ws, blob, { defer: canDeferProjection });
8361
+ if (res.ok) {
8362
+ if (intentId) main.ids.add(intentId);
8363
+ main.oids.add(res.head);
8364
+ main.head = res.head;
8365
+ admitted.push({ oid, intent: intentId, head: res.head });
8366
+ totalAdmitted += 1;
8367
+ _intents += 1;
8368
+ if (Array.isArray(res.born) && res.born.length) footageBorn.push(...res.born);
8369
+ if (Array.isArray(res.birthOutcomes) && res.birthOutcomes.length) footageBirthOutcomes.push(...res.birthOutcomes);
8370
+ } else {
8371
+ const error = `${String(res.error || "").slice(0, 300)} \u2014 dead-lettered; fix the law (or the payload), then POST /v1/workspaces/${ws}/dead-letters/retry`;
8372
+ rejected.push({ oid, error, deadLettered: true });
8373
+ deadLetters.push({ intentId, oid, session: fcid, error, blob, domain: dom, directiveId: dirId });
8374
+ }
8375
+ } catch (e) {
8376
+ rejected.push({ oid: `${fcid}#${i}`.slice(0, 10), error: String(e).slice(0, 200) });
8377
+ }
8378
+ }
8379
+ footageConsumed.push(oref);
8380
+ results.push({ session: fcid, admitted, rejected, ...skipped.length ? { skipped } : {} });
8381
+ } catch (e) {
8382
+ results.push({ session: fcid, error: String(e).slice(0, 200) });
8383
+ } finally {
8384
+ _fDone({ admitted: admitted.length, rejected: rejected.length });
8385
+ }
8386
+ }
8387
+ } catch (e) {
8388
+ log("footage-scan-failed", { ws, error: String(e).slice(0, 200) });
8389
+ }
8241
8390
  if (canDeferProjection && totalAdmitted) {
8242
8391
  const _projDone = _span("projection flush (batch)", "cproject", 1, { admitted: totalAdmitted });
8243
8392
  try {
@@ -8261,7 +8410,8 @@ async function admitAll(eng2, ws, ledger, { receiptFence = null, shardRouting =
8261
8410
  const hasCoordination = !!(summaryCtx || placements.length || splits.length || globalsOut.length);
8262
8411
  const deferDurability = defer && !hasCoordination;
8263
8412
  let _sealed = true;
8264
- if (totalAdmitted && !deferDurability) {
8413
+ const sealNeeded = totalAdmitted > 0 || isDirty(eng2, ws);
8414
+ if (sealNeeded && !deferDurability) {
8265
8415
  const _sealDone = _span(`seal (batch)`, "cseal", 1, { sessions: consumed.length, admitted: totalAdmitted });
8266
8416
  const _s = Date.now();
8267
8417
  try {
@@ -8281,17 +8431,38 @@ async function admitAll(eng2, ws, ledger, { receiptFence = null, shardRouting =
8281
8431
  } catch {
8282
8432
  }
8283
8433
  }
8434
+ for (const oref of footageConsumed) {
8435
+ const fcid = `offered:${oref.slice(offeredRefPrefix.length)}`;
8436
+ if (unparkedSessions.has(fcid)) continue;
8437
+ try {
8438
+ kgit.deleteRef(eng2, gitdir, oref);
8439
+ } catch {
8440
+ }
8441
+ try {
8442
+ await custodyPush(eng2, ws, ledger, { remoteRef: oref, del: true });
8443
+ } catch {
8444
+ }
8445
+ }
8284
8446
  }
8285
8447
  const newMain = kgit.resolveRef(eng2, gitdir, BRANCH);
8286
- log("admit", { ws, sessions: results.length, main: newMain });
8448
+ log("admit", { ws, sessions: results.length, main: newMain, sealed: _sealed, sealedMain: custodyConfirmedMain(eng2, ws) });
8287
8449
  const summary = summaryCtx ? summaryOfCapture(summaryCtx, headBefore ?? "", newMain) : null;
8288
8450
  const _total = Date.now() - _t0;
8289
8451
  const timings = { totalMs: _total, scanMs: _scanMs, fetchMs: _fetchMs, sealMs: _sealMs, gateFoldMs: Math.max(0, _total - _scanMs - _fetchMs - _sealMs), intents: _intents, sessions: results.length, spans: _spans };
8290
8452
  return {
8453
+ // SCOREBOARD TRUTH: `main` is the resident fold head (the ack the client converges to);
8454
+ // `sealedMain` is the custody-confirmed head — THE ONLY head the host may advertise/cache
8455
+ // (seated_barrier_and_seal_obligation.md Part 2). `sealed:false` ⇒ the seal obligation is
8456
+ // outstanding and the caller must re-arm the admit alarm (retry) rather than move on.
8291
8457
  sessions: results,
8292
8458
  main: newMain,
8459
+ sealed: _sealed,
8460
+ sealedMain: custodyConfirmedMain(eng2, ws),
8293
8461
  deadLetters,
8294
8462
  timings,
8463
+ // Footage-replayed `.births()` offers: the born child custody + typed verdicts, for the caller
8464
+ // to persist exactly as the live POST lane does (packed here — the caller holds no engine FS).
8465
+ ...footageBorn.length ? { born: packBornChildren(eng2, footageBorn), birthOutcomes: footageBirthOutcomes } : {},
8295
8466
  ...summary ? { summary } : {},
8296
8467
  ...placements.length ? { placements } : {},
8297
8468
  ...splits.length ? { splits } : {},
@@ -8305,7 +8476,7 @@ async function backupHead(eng2, ws, ledger, head) {
8305
8476
  if (!eng2.mounted.has(ws)) return { ok: false, error: "not mounted" };
8306
8477
  if (!head) return { ok: false, error: "no head" };
8307
8478
  try {
8308
- await custodyPush(eng2, ws, ledger, { remoteRef: "refs/heads/main", newOid: head });
8479
+ await custodyPush(eng2, ws, ledger, { remoteRef: "refs/heads/main", newOid: head, ffOnly: true });
8309
8480
  return { ok: true, head };
8310
8481
  } catch (e) {
8311
8482
  return { ok: false, error: String(e?.message || e).slice(0, 200), head };
@@ -8318,7 +8489,7 @@ async function backupToCustody(eng2, ws, ledger, { consumed = [] } = {}) {
8318
8489
  head = kgit.resolveRef(eng2, gitdir, BRANCH);
8319
8490
  if (!head) return { ok: false, error: "no local main" };
8320
8491
  try {
8321
- await custodyPush(eng2, ws, ledger, { remoteRef: "refs/heads/main" });
8492
+ await custodyPush(eng2, ws, ledger, { remoteRef: "refs/heads/main", ffOnly: true });
8322
8493
  } catch (e) {
8323
8494
  return { ok: false, error: String(e).slice(0, 200), head };
8324
8495
  }
@@ -34,7 +34,8 @@ import 'package:nomos_types/nomos_types.dart'
34
34
  ReferenceVersionUnavailable,
35
35
  CustodyBreakPolicy,
36
36
  NomosCustodyEvent,
37
- NomosCustodyStatus;
37
+ NomosCustodyStatus,
38
+ NomosLocalWorkRejectedEvent;
38
39
  import 'package:nomos_types/src/nomos_ref.dart' show NmsInternalNomosRefs;
39
40
  import 'package:nomos_types/src/versioned_nomos_ref.dart'
40
41
  show NmsInternalVersionedNomosRefs;
@@ -1230,6 +1231,11 @@ class NomosBridge {
1230
1231
  // [resolveCustodyBreak].
1231
1232
  final StreamController<NomosCustodyEvent> _custodyEvents =
1232
1233
  StreamController<NomosCustodyEvent>.broadcast();
1234
+ // LOCAL-WORK-REJECTED LIFECYCLE (seated_barrier_and_seal_obligation.md Part 4): the typed "the
1235
+ // cloud refused work this device had accepted" event — refused intents already parked (client DLQ
1236
+ // + refused/* custody refs), survivors already replayed; the app renders honest UX from this.
1237
+ final StreamController<NomosLocalWorkRejectedEvent> _localWorkRejectedEvents =
1238
+ StreamController<NomosLocalWorkRejectedEvent>.broadcast();
1233
1239
  final StreamController<Map<String, Object?>> _flameEvents =
1234
1240
  StreamController<Map<String, Object?>>.broadcast();
1235
1241
  final StreamController<Map<String, Object?>> _spanEvents =
@@ -1505,6 +1511,17 @@ class NomosBridge {
1505
1511
  if (ev != null && !_custodyEvents.isClosed) _custodyEvents.add(ev);
1506
1512
  return;
1507
1513
  }
1514
+ if (event == 'localWorkRejected') {
1515
+ // LOCAL-WORK-REJECTED: { event:'localWorkRejected', session, localWorkRejected:{ workspace,
1516
+ // refused, parked, resetTo, retryable, source } }. The framework already parked the refused
1517
+ // intents + replayed survivors; the app's only job is the honest message (retryable ⇒ "queued,
1518
+ // will retry"; terminal ⇒ "preserved for review — contact support").
1519
+ final ev = NomosLocalWorkRejectedEvent.fromMessage(m);
1520
+ if (ev != null && !_localWorkRejectedEvents.isClosed) {
1521
+ _localWorkRejectedEvents.add(ev);
1522
+ }
1523
+ return;
1524
+ }
1508
1525
  if (event == 'transfer') {
1509
1526
  // LIVE pack byte-progress { received, total, kind } — the real loading bar (host Content-Length).
1510
1527
  final tp = TransferProgress.fromMessage(m);
@@ -1850,6 +1867,15 @@ class NomosBridge {
1850
1867
  Stream<NomosCustodyEvent> get custodyEvents =>
1851
1868
  _eventPlane._custodyEvents.stream;
1852
1869
 
1870
+ /// THE LOCAL-WORK-REJECTED STREAM (seated_barrier_and_seal_obligation.md Part 4): the cloud's gate
1871
+ /// refused work this device's own kernel had accepted. Refused intents are already durably parked
1872
+ /// (client DLQ + the workspace's refused/* refs) and surviving local work already replayed when an
1873
+ /// event arrives — bind honest UX to it: `retryable` ⇒ "your changes are queued and will retry";
1874
+ /// otherwise ⇒ "your changes were preserved for review — contact support". Never render the raw
1875
+ /// refusal text as the headline.
1876
+ Stream<NomosLocalWorkRejectedEvent> get localWorkRejectedEvents =>
1877
+ _eventPlane._localWorkRejectedEvents.stream;
1878
+
1853
1879
  /// Where a session sits on the custody spectrum (incarnation, posture, pending/salvaged local work,
1854
1880
  /// supersession history) — for a status banner or a "you're offline on old truth" indicator.
1855
1881
  Future<NomosCustodyStatus> custodyStatus({String? session}) async =>
@@ -2805,18 +2831,23 @@ class NomosBridge {
2805
2831
  /// `principal` on `parent`'s chain (`identity:<principal>#signer@key:<sha256(publicKey)>`)? One O(1)
2806
2832
  /// relation read over the open gateway; nothing is authored. Returns
2807
2833
  /// `{parent, principal, keyHash, object, relation, enrolled, revoked, checkedAt}`.
2834
+ /// Pass `remote: true` to force the CLOUD-gateway read even when this holon IS the parent — the
2835
+ /// seated barrier's proof-by-read: `enrolled: true` then means the shared chain durably holds this
2836
+ /// device's signer tuple, not merely that the local fold ran.
2808
2837
  Future<Map<String, Object?>> parentSignerEnrollment(
2809
2838
  {required String parent,
2810
2839
  required String principal,
2811
2840
  String? keyHash,
2812
2841
  String? publicKey,
2813
- String? authToken}) async =>
2842
+ String? authToken,
2843
+ bool remote = false}) async =>
2814
2844
  _asMap(await _call('parentSignerEnrollment', {
2815
2845
  'parent': parent,
2816
2846
  'principal': principal,
2817
2847
  if (keyHash != null) 'keyHash': keyHash,
2818
2848
  if (publicKey != null) 'publicKey': publicKey,
2819
2849
  if (authToken != null) 'authToken': authToken,
2850
+ if (remote) 'remote': true,
2820
2851
  }));
2821
2852
 
2822
2853
  /// IDEMPOTENT PARENT-SIGNER RECOVERY — ensure this device key is a live `signer` on `parent`,
@@ -3927,6 +3958,7 @@ class NomosBridge {
3927
3958
  if (!_syncEvents.isClosed) await _syncEvents.close();
3928
3959
  if (!_signalEvents.isClosed) await _signalEvents.close();
3929
3960
  if (!_custodyEvents.isClosed) await _custodyEvents.close();
3961
+ if (!_localWorkRejectedEvents.isClosed) await _localWorkRejectedEvents.close();
3930
3962
  if (!_flameEvents.isClosed) await _flameEvents.close();
3931
3963
  if (!_spanEvents.isClosed) await _spanEvents.close();
3932
3964
  if (!_transferEvents.isClosed) await _transferEvents.close();
package/dart/pubspec.lock CHANGED
@@ -191,7 +191,7 @@ packages:
191
191
  path: "../dart_types"
192
192
  relative: true
193
193
  source: path
194
- version: "0.8.4"
194
+ version: "0.8.5"
195
195
  package_config:
196
196
  dependency: "direct main"
197
197
  description:
package/dart/pubspec.yaml CHANGED
@@ -7,7 +7,7 @@ repository: https://github.com/Captain-App/nomos2
7
7
  description: >-
8
8
  Nomos 2 Dart client — generated domain values, intent payloads and application
9
9
  lifecycle types over the typed Nomos kernel protocol and USDA custody model.
10
- version: 0.64.18
10
+ version: 0.64.20
11
11
 
12
12
  environment:
13
13
  sdk: ^3.5.0
@@ -16,7 +16,7 @@ dependencies:
16
16
  # The pure-data core (wire types, offer codec, NomosReads, schema validation, error
17
17
  # info). nomos_client depends on and re-exports it, so existing imports are unchanged.
18
18
  # For local monorepo dev, pubspec_overrides.yaml points this at ../dart_types.
19
- nomos_types: ^0.8.4
19
+ nomos_types: ^0.8.6
20
20
  json_schema: ^5.2.2
21
21
  crypto: ^3.0.0
22
22
  package_config: ^2.1.0
@@ -1,3 +1,11 @@
1
+ ## 0.8.6
2
+
3
+ - NEW: `NomosLocalWorkRejectedEvent` / `NomosRejectedIntent` — the typed "the cloud refused work your
4
+ device had accepted" lifecycle event (architecture/seated_barrier_and_seal_obligation.md Part 4).
5
+ By the time it fires, refused intents are durably parked on BOTH sides (client DLQ + workspace
6
+ refused/* refs) and surviving local work is rebase-replayed — no work is ever lost. `retryable`
7
+ distinguishes transient-premise refusals (auto-retry clears them) from terminal ones.
8
+
1
9
  ## 0.8.5
2
10
 
3
11
  - Expose bounded generated-startup and referenced-workspace custody failures as typed data so applications
@@ -50,6 +50,10 @@ export 'src/git_object_id.dart';
50
50
  // The CUSTODY-LIFECYCLE types (rebirth_and_custody_lifecycle.md): NomosCustodyBreak / NomosCustodyStatus /
51
51
  // CustodyBreakPolicy — custody incompatibility as a first-class, typed, app-decidable event. Pure data.
52
52
  export 'src/custody.dart';
53
+ // THE LOCAL-WORK-REJECTED LIFECYCLE (seated_barrier_and_seal_obligation.md Part 4):
54
+ // NomosLocalWorkRejectedEvent / NomosRejectedIntent — "the cloud refused work your device had
55
+ // accepted" as a first-class, typed, app-renderable event (refused work is parked, never lost).
56
+ export 'src/local_work_rejected.dart';
53
57
  // SURFACED MERGE CONFLICTS (architecture/surfaced_merge_conflict.md): the read-collapse types for a
54
58
  // `.merge(Surfaced(...))` field — SurfacedValue<T> (resolved scalar | pending observations). Pure data.
55
59
  export 'src/surfaced.dart';
@@ -0,0 +1,117 @@
1
+ // THE LOCAL-WORK-REJECTED LIFECYCLE EVENT (architecture/seated_barrier_and_seal_obligation.md Part 4).
2
+ //
3
+ // Fired when work THIS device authored locally — and which its OWN kernel admitted — is REFUSED by the
4
+ // shared workspace's gate on sync/admission. Local-first means local acceptance is a real, honest
5
+ // verdict; the shared chain's gate re-judges from ITS state, and the two can disagree (a premise the
6
+ // device relied on hadn't landed yet, or the write genuinely violates shared law). By the time this
7
+ // event fires the framework has already done the right thing:
8
+ //
9
+ // • every refused intent is durably PARKED on BOTH sides (the client dead-letter queue AND the
10
+ // workspace's refused/* custody refs) — inspectable, re-offerable, NEVER lost;
11
+ // • every surviving unrefused local intent was rebase-replayed onto canonical main;
12
+ // • local state converged to [resetTo].
13
+ //
14
+ // The app's ONLY job is honest UX. [retryable] is the fork in the message:
15
+ // true ⇒ "Your changes are queued and will be retried automatically." (transient premise — e.g.
16
+ // this device's signer enrollment hadn't landed on the chain yet)
17
+ // false ⇒ "Your changes couldn't be accepted and have been preserved for review — contact support."
18
+ // Never show [NomosRejectedIntent.error] raw to an end user — it is the gate's diagnostic text.
19
+ class NomosRejectedIntent {
20
+ final String? intentId;
21
+ final String? oid;
22
+ final String? domain;
23
+ final String? directiveId;
24
+
25
+ /// The gate's own refusal text (diagnostic — for logs/support disclosure, never the headline).
26
+ final String error;
27
+
28
+ /// Transient-premise refusal that auto-retry can clear.
29
+ final bool retryable;
30
+
31
+ const NomosRejectedIntent({
32
+ this.intentId,
33
+ this.oid,
34
+ this.domain,
35
+ this.directiveId,
36
+ required this.error,
37
+ required this.retryable,
38
+ });
39
+
40
+ factory NomosRejectedIntent.fromJson(Map<String, dynamic> j) =>
41
+ NomosRejectedIntent(
42
+ intentId: j['intentId'] as String?,
43
+ oid: j['oid'] as String?,
44
+ domain: j['domain'] as String?,
45
+ directiveId: j['directiveId'] as String?,
46
+ error: (j['error'] ?? '') as String,
47
+ retryable: j['retryable'] == true,
48
+ );
49
+
50
+ Map<String, dynamic> toJson() => {
51
+ 'intentId': intentId,
52
+ 'oid': oid,
53
+ 'domain': domain,
54
+ 'directiveId': directiveId,
55
+ 'error': error,
56
+ 'retryable': retryable,
57
+ };
58
+ }
59
+
60
+ /// The typed "the cloud refused work your device had accepted" event. See library docs above —
61
+ /// refused work is already parked and survivors replayed when this fires; render honest UX only.
62
+ class NomosLocalWorkRejectedEvent {
63
+ final String workspace;
64
+ final String? session;
65
+ final List<NomosRejectedIntent> refused;
66
+
67
+ /// Always true when the event fires: the refused intents are durably parked (client DLQ +
68
+ /// workspace refused/* refs).
69
+ final bool parked;
70
+
71
+ /// The canonical main local state converged to.
72
+ final String? resetTo;
73
+
74
+ /// True iff EVERY refusal in the batch is retryable.
75
+ final bool retryable;
76
+
77
+ /// "admission" (the edge refused our offers) or "sync" (the offer-gated converge dead-lettered
78
+ /// local tail intents).
79
+ final String source;
80
+
81
+ const NomosLocalWorkRejectedEvent({
82
+ required this.workspace,
83
+ this.session,
84
+ required this.refused,
85
+ required this.parked,
86
+ this.resetTo,
87
+ required this.retryable,
88
+ required this.source,
89
+ });
90
+
91
+ static NomosLocalWorkRejectedEvent? fromMessage(Map<String, dynamic> m) {
92
+ final e = (m['localWorkRejected'] as Map?)?.cast<String, dynamic>();
93
+ if (e == null) return null;
94
+ return NomosLocalWorkRejectedEvent(
95
+ workspace: (e['workspace'] ?? '') as String,
96
+ session: m['session'] as String?,
97
+ refused: ((e['refused'] as List?) ?? const [])
98
+ .whereType<Map>()
99
+ .map((r) => NomosRejectedIntent.fromJson(r.cast<String, dynamic>()))
100
+ .toList(growable: false),
101
+ parked: e['parked'] != false,
102
+ resetTo: e['resetTo'] as String?,
103
+ retryable: e['retryable'] == true,
104
+ source: (e['source'] ?? 'sync') as String,
105
+ );
106
+ }
107
+
108
+ Map<String, dynamic> toJson() => {
109
+ 'workspace': workspace,
110
+ 'session': session,
111
+ 'refused': refused.map((r) => r.toJson()).toList(growable: false),
112
+ 'parked': parked,
113
+ 'resetTo': resetTo,
114
+ 'retryable': retryable,
115
+ 'source': source,
116
+ };
117
+ }
@@ -9,7 +9,7 @@ description: >-
9
9
  Pure kernel wire/data types for Nomos Dart frontends — no transport, no bridge,
10
10
  no runtime. Typed protobuf and USDA boundary values plus the offer codec,
11
11
  reactive read engine, schema validation, and data-only error/trace types.
12
- version: 0.8.5
12
+ version: 0.8.6
13
13
 
14
14
  environment:
15
15
  sdk: ^3.5.0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.105.3",
3
+ "version": "0.105.5",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
@@ -7147,7 +7147,7 @@ function createGovernanceOffers(ctx) {
7147
7147
  * `revoked: true` means the subject's leaf was flipped `status:revoked` (remove-wins — a lawful
7148
7148
  * re-enroll of the SAME key is refused; mint + enroll a new device key instead).
7149
7149
  */
7150
- parentSignerEnrollment: async ({ parent, principal, keyHash, publicKey, authToken } = {}) => {
7150
+ parentSignerEnrollment: async ({ parent, principal, keyHash, publicKey, authToken, remote } = {}) => {
7151
7151
  if (!parent || typeof parent !== "string") throw new Error("parentSignerEnrollment requires `parent` (the workspace whose chain holds the signer tuple)");
7152
7152
  if (!principal || typeof principal !== "string") throw new Error("parentSignerEnrollment requires `principal` (the auth-provider uid)");
7153
7153
  const p = canonicalPrincipal(principal);
@@ -7157,7 +7157,7 @@ function createGovernanceOffers(ctx) {
7157
7157
  }
7158
7158
  const object = signerObjectOf(p);
7159
7159
  let rows;
7160
- if (parent === workspace) {
7160
+ if (parent === workspace && !remote) {
7161
7161
  rows = JSON.parse(call("query", { queryBytes: qBytes2({ kind: "query", queryId: "relationTuplesByObject", paramsJson: JSON.stringify({ object }) }) })) || [];
7162
7162
  } else {
7163
7163
  const headers = authToken ? { Authorization: `Bearer ${authToken}`, "x-nomos-auth": authToken } : authHeaders;
@@ -7686,7 +7686,7 @@ function birthOutcomeFailure(birthOutcomes) {
7686
7686
  err.birthOutcomes = birthOutcomes;
7687
7687
  return err;
7688
7688
  }
7689
- async function connectSingle({ cloud, workspace, clientId, authToken, restoreFrom, onSpan, authorSecret, principal, allowUnborn = false, salvageOnly = false, recover = false, mountInPlace = false, __injectCheckpoint, kernelProvider, fetchImpl, historicSnapshots, onCustodyBreak, watchRebind, openWorkspaceInPlane, __knownKernelCapabilities, __testKRootPublic, __testRootSecret }) {
7689
+ async function connectSingle({ cloud, workspace, clientId, authToken, restoreFrom, onSpan, authorSecret, principal, allowUnborn = false, salvageOnly = false, recover = false, mountInPlace = false, __injectCheckpoint, kernelProvider, fetchImpl, historicSnapshots, onCustodyBreak, onLocalWorkRejected, watchRebind, openWorkspaceInPlane, __knownKernelCapabilities, __testKRootPublic, __testRootSecret }) {
7690
7690
  if (salvageOnly) allowUnborn = true;
7691
7691
  if (!cloud || !workspace) throw new Error("connect({ cloud, workspace }) required");
7692
7692
  const _fetch = typeof fetchImpl === "function" ? fetchImpl : globalThis.fetch;
@@ -7848,6 +7848,47 @@ ${String(commit).toLowerCase()}`;
7848
7848
  }
7849
7849
  };
7850
7850
  const onCustodyBreakOpt = typeof onCustodyBreak === "function" ? onCustodyBreak : null;
7851
+ let localWorkRejectedHook = typeof onLocalWorkRejected === "function" ? onLocalWorkRejected : null;
7852
+ const localWorkRejectedListeners = /* @__PURE__ */ new Set();
7853
+ const refusalIsRetryable = (error) => {
7854
+ try {
7855
+ const d = authzDiagnosisFromRefusal(String(error ?? ""));
7856
+ return d?.code === "nomos.authz.signer_enrollment_missing";
7857
+ } catch {
7858
+ return false;
7859
+ }
7860
+ };
7861
+ const emitLocalWorkRejected = (source, refused, resetTo) => {
7862
+ if (!refused.length) return;
7863
+ const items = refused.map((r) => ({
7864
+ intentId: typeof r.intentId === "string" ? r.intentId : null,
7865
+ oid: typeof r.oid === "string" ? r.oid : null,
7866
+ domain: typeof r.domain === "string" ? r.domain : null,
7867
+ directiveId: typeof r.directiveId === "string" ? r.directiveId : null,
7868
+ error: String(r.error ?? "the law refused this offer").slice(0, 600),
7869
+ retryable: refusalIsRetryable(r.error)
7870
+ }));
7871
+ const event = {
7872
+ workspace,
7873
+ refused: items,
7874
+ parked: true,
7875
+ resetTo,
7876
+ retryable: items.every((i) => i.retryable),
7877
+ source
7878
+ };
7879
+ if (localWorkRejectedHook) {
7880
+ try {
7881
+ localWorkRejectedHook(event);
7882
+ } catch {
7883
+ }
7884
+ }
7885
+ for (const l of [...localWorkRejectedListeners]) {
7886
+ try {
7887
+ l(event);
7888
+ } catch {
7889
+ }
7890
+ }
7891
+ };
7851
7892
  function captureIncarnation(j) {
7852
7893
  const inc = j && typeof j.incarnation === "string" && OID40_RE.test(j.incarnation) ? j.incarnation : null;
7853
7894
  if (inc !== null) remoteIncarnation = inc;
@@ -8434,6 +8475,7 @@ ${String(commit).toLowerCase()}`;
8434
8475
  }
8435
8476
  }
8436
8477
  }
8478
+ if (deadLettered.length) emitLocalWorkRejected("sync", deadLettered.map((d) => ({ intentId: d.id, error: d.error })), remoteHead);
8437
8479
  invalidateInstalledLawMetadata();
8438
8480
  nudgeWatches();
8439
8481
  return {
@@ -10746,6 +10788,22 @@ ${String(commit).toLowerCase()}`;
10746
10788
  if (typeof cb === "function") custodyListeners.add(cb);
10747
10789
  return () => custodyListeners.delete(cb);
10748
10790
  },
10791
+ /** THE LOCAL-WORK-REJECTED HOOK (settable; connect({onLocalWorkRejected}) is the default) — the
10792
+ * typed "the cloud refused work your device had accepted" lifecycle event. See
10793
+ * `LocalWorkRejectedEvent`: refused intents are already parked + survivors replayed when it
10794
+ * fires; the app's only job is honest UX (retryable ⇒ "queued, will retry"; terminal ⇒
10795
+ * "preserved for review — contact support"). */
10796
+ get onLocalWorkRejected() {
10797
+ return localWorkRejectedHook;
10798
+ },
10799
+ set onLocalWorkRejected(fn) {
10800
+ localWorkRejectedHook = typeof fn === "function" ? fn : null;
10801
+ },
10802
+ /** Subscribe (multi-listener) to LocalWorkRejectedEvents. Returns an unsubscribe fn. */
10803
+ onLocalWorkRejectedEvent: (cb) => {
10804
+ if (typeof cb === "function") localWorkRejectedListeners.add(cb);
10805
+ return () => localWorkRejectedListeners.delete(cb);
10806
+ },
10749
10807
  /**
10750
10808
  * INTROSPECTION — where this holon sits on the custody spectrum:
10751
10809
  * `{ incarnation, localGenesis, remoteIncarnation, syncPosture, pendingLocalIntents, salvagedIntents,
@@ -12215,6 +12273,10 @@ ${String(commit).toLowerCase()}`;
12215
12273
  });
12216
12274
  _convSpan.end();
12217
12275
  if (result && result.custodyBreak) return await resolveCustodyBreakFlow(result.custodyBreak, { operation: "sync" });
12276
+ if (admissionRefusals && admissionRefusals.size) {
12277
+ const refusedEntries = (admission?.sessions || []).flatMap((s) => (s.rejected || []).map((r) => ({ intentId: r.intent, oid: r.oid, error: r.error })));
12278
+ emitLocalWorkRejected("admission", refusedEntries, result && typeof result.remoteMain === "string" ? result.remoteMain : null);
12279
+ }
12218
12280
  try {
12219
12281
  result.stillAhead = await resolveBranchHead() !== syncedBase;
12220
12282
  } catch {
@@ -836,6 +836,10 @@ const DART_SUPPORT_CORE_SRC = [
836
836
  // The custody-lifecycle types (NomosCustodyBreak/NomosCustodyStatus/CustodyBreakPolicy) — the generated
837
837
  // client + the vendored bridge reference them through the barrel, so they must vendor. Pure data.
838
838
  "custody.dart",
839
+ // THE LOCAL-WORK-REJECTED LIFECYCLE (seated_barrier_and_seal_obligation.md Part 4):
840
+ // NomosLocalWorkRejectedEvent/NomosRejectedIntent — the vendored bridge's localWorkRejectedEvents
841
+ // stream types ride through the barrel, so they must vendor. Pure data.
842
+ "local_work_rejected.dart",
839
843
  // SURFACED MERGE CONFLICTS: SurfacedValue<T>/SurfacedObservation<T> — a generated read model with a
840
844
  // `.merge(Surfaced(...))` field references them through the barrel, so they must vendor. Pure data.
841
845
  "surfaced.dart",
@@ -1649,12 +1653,12 @@ const dartTypesImportFor = (generatedSource: string): string => {
1649
1653
  * just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
1650
1654
  // 0.3.0: SurfacedValue<T>/SurfacedObservation/PendingConflictRow — generated Surfaced-field typing +
1651
1655
  // the pendingConflicts read rows.
1652
- const NOMOS_TYPES_DEP = "^0.8.5";
1656
+ const NOMOS_TYPES_DEP = "^0.8.6";
1653
1657
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
1654
1658
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
1655
1659
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
1656
- const NOMOS_CLIENT_DEP = "^0.64.18";
1657
- const NOMOS_FLUTTER_DEP = "^0.63.30";
1660
+ const NOMOS_CLIENT_DEP = "^0.64.20";
1661
+ const NOMOS_FLUTTER_DEP = "^0.63.31";
1658
1662
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
1659
1663
  // and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
1660
1664
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).