@granular-software/sdk 0.4.57 → 0.4.59

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/dist/cli/index.js CHANGED
@@ -7917,12 +7917,12 @@ var ApiClient = class {
7917
7917
  targetVersionId: tag2.targetVersionId ?? tag2.targetBuildId ?? null
7918
7918
  }));
7919
7919
  }
7920
- async moveTag(tagId, targetBuildId) {
7920
+ async moveTag(tagId, targetBuildId, expectedProdBuildId) {
7921
7921
  const moved = await this.request(
7922
7922
  `/control/tags/${tagId}/move`,
7923
7923
  {
7924
7924
  method: "POST",
7925
- body: JSON.stringify({ targetBuildId })
7925
+ body: JSON.stringify({ targetBuildId, expectedProdBuildId })
7926
7926
  }
7927
7927
  );
7928
7928
  return {
@@ -7930,22 +7930,24 @@ var ApiClient = class {
7930
7930
  targetVersionId: moved.targetVersionId ?? moved.targetBuildId ?? null
7931
7931
  };
7932
7932
  }
7933
- async moveVersionTag(tagId, targetVersionId) {
7934
- return this.moveTag(tagId, targetVersionId);
7933
+ async moveVersionTag(tagId, targetVersionId, expectedProdBuildId) {
7934
+ return this.moveTag(tagId, targetVersionId, expectedProdBuildId);
7935
7935
  }
7936
- async promoteToProd(buildId) {
7936
+ async promoteToProd(buildId, expectedProdBuildId) {
7937
7937
  return this.request(
7938
7938
  `/control/builds/${buildId}/promote`,
7939
7939
  {
7940
- method: "POST"
7940
+ method: "POST",
7941
+ body: JSON.stringify({ expectedProdBuildId })
7941
7942
  }
7942
7943
  );
7943
7944
  }
7944
- async promoteVersionToProd(versionId) {
7945
+ async promoteVersionToProd(versionId, expectedProdBuildId) {
7945
7946
  return this.request(
7946
7947
  `/control/versions/${versionId}/promote`,
7947
7948
  {
7948
- method: "POST"
7949
+ method: "POST",
7950
+ body: JSON.stringify({ expectedProdBuildId })
7949
7951
  }
7950
7952
  );
7951
7953
  }
@@ -16831,12 +16833,13 @@ ${effectMetamodelTable}
16831
16833
  | API | Purpose |
16832
16834
  |-----|---------|
16833
16835
  | \`submitJob(code)\` | Run code in the sandbox; import generated classes from \`@granular/domain/<Class>\` and actions from \`@granular/actions/backend\` or \`@granular/actions/frontend\`. |
16834
- | \`answerPrompt(...)\`, \`appendMessage(...)\` | Human-in-the-loop and conversation APIs. |
16836
+ | \`answerPrompt(...)\`, \`appendUserMessage(...)\` | Human-in-the-loop user ingress APIs. Assistant and component output is authored through trusted runtime/feed paths. |
16835
16837
  | \`getDomain()\`, \`getDomainTypes()\`, \`getDomainDocs()\`, \`getDomainDocumentation()\` | Domain summary and generated TypeScript / docs. |
16836
16838
  | \`getEffects()\` / \`getTools()\`, \`session.on("effects:changed", ...)\` | Effect catalog and live updates. |
16837
16839
  | \`checkReadiness()\`, \`on('readiness', ...)\` | Runtime readiness. |
16838
16840
  | \`document\`, \`getHeap()\` | Current live session state from the WebSocket. |
16839
- | \`messages.list()\`, \`timeline.list()\`, \`jobs.list/get()\`, \`heap.entries.list/get()\`, \`heap.lists.list/get()\`, \`transcript.list()\` | Durable session history and saved artifacts. |
16841
+ | \`feed.list()\`, \`transcript.list()\`, \`heap.entries.list/get()\`, \`heap.lists.list/get()\` | Canonical presentation history and saved artifacts. \`transcript.list()\` is projected only from feed-v1. |
16842
+ | \`timeline.list()\`, \`jobs.list/get()\` | Diagnostics and execution state; never reconstruct customer-visible chronology from these collections. |
16840
16843
  | \`disconnect()\` | Close the live runtime session. |
16841
16844
  | \`rpc(method, params)\` | Low-level session RPC (advanced). |
16842
16845
  | \`disconnect()\` | End the session. |
@@ -18993,7 +18996,10 @@ async function deployCommand(options = {}) {
18993
18996
  }
18994
18997
  if (options.prod) {
18995
18998
  const promoting = spinner("Promoting prod...");
18996
- await api.promoteVersionToProd(completed.buildId);
18999
+ await api.promoteVersionToProd(
19000
+ completed.buildId,
19001
+ prodTag?.targetVersionId || prodTag?.targetBuildId || null
19002
+ );
18997
19003
  promoting.succeed(
18998
19004
  ` Prod now points to ${brand.secondary(completed.buildId)}`
18999
19005
  );
@@ -19525,168 +19531,1459 @@ async function simulateCommand(sandboxIdArg, options) {
19525
19531
  info(`Opening simulator: ${url}`);
19526
19532
  openUrl(url);
19527
19533
  }
19528
- var GlobalWebSocket = void 0;
19529
- if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
19530
- GlobalWebSocket = globalThis.WebSocket;
19534
+
19535
+ // src/feed.ts
19536
+ function requirePositiveFeedPosition(value) {
19537
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
19538
+ throw new Error("Invalid feed chronology.");
19539
+ }
19540
+ return value;
19531
19541
  }
19532
- var READY_STATE_OPEN = 1;
19533
- var TOKEN_REFRESH_LEEWAY_MS = 2 * 60 * 1e3;
19534
- var TOKEN_REFRESH_RETRY_MS = 30 * 1e3;
19535
- var MAX_TIMER_DELAY_MS = 2147483647;
19536
- var DEBUG_WS = process.env.GRANULAR_DEBUG_WS === "1";
19537
- var DEFAULT_RPC_TIMEOUT_MS = 3e4;
19538
- var DOMAIN_PACKAGE_RPC_TIMEOUT_MS = 12e4;
19539
- var EFFECT_CONTROL_RPC_TIMEOUT_MS = 12e4;
19540
- var HARNESS_RUN_RPC_TIMEOUT_MS = 6e5;
19541
- var DEFAULT_RECONNECT_DELAY_MS = 3e3;
19542
- var DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
19543
- function debugWs(...args) {
19544
- if (DEBUG_WS) {
19545
- console.log(...args);
19542
+ function orderTransientFeedItems(items) {
19543
+ const byId = /* @__PURE__ */ new Map();
19544
+ const ordinalById = /* @__PURE__ */ new Map();
19545
+ const idByOrdinal = /* @__PURE__ */ new Map();
19546
+ for (const item of items) {
19547
+ const id = String(item.id || "");
19548
+ const ordinal = requirePositiveFeedPosition(item.ordinal);
19549
+ const knownOrdinal = ordinalById.get(id);
19550
+ const ordinalOwner = idByOrdinal.get(ordinal);
19551
+ if (!id || knownOrdinal !== void 0 && knownOrdinal !== ordinal || ordinalOwner && ordinalOwner !== id) {
19552
+ throw new Error("Invalid feed chronology.");
19553
+ }
19554
+ ordinalById.set(id, ordinal);
19555
+ idByOrdinal.set(ordinal, id);
19556
+ const existing = byId.get(id);
19557
+ if (existing && item.revision === existing.revision && stableValueFingerprint(item) !== stableValueFingerprint(existing)) {
19558
+ throw new Error(
19559
+ "Invalid feed chronology: conflicting transient revision."
19560
+ );
19561
+ }
19562
+ if (!existing || item.revision > existing.revision) byId.set(id, item);
19546
19563
  }
19564
+ return [...byId.values()].sort((left, right) => left.ordinal - right.ordinal);
19547
19565
  }
19548
- function rpcTimeoutMsForMethod(method) {
19549
- switch (method) {
19550
- case "domain.fetchPackagePart":
19551
- case "domain.getSummary":
19552
- return DOMAIN_PACKAGE_RPC_TIMEOUT_MS;
19553
- case "client.heartbeat":
19554
- case "effects.publishCatalog":
19555
- case "effects.resetCatalog":
19556
- case "effects.addCatalog":
19557
- case "effects.removeCatalog":
19558
- case "effects.refresh":
19559
- case "harness.prepare":
19560
- return EFFECT_CONTROL_RPC_TIMEOUT_MS;
19561
- case "harness.run":
19562
- return HARNESS_RUN_RPC_TIMEOUT_MS;
19566
+ var GRANULAR_FEED_DIAGNOSTIC_EVENT = "granular:feed-diagnostic";
19567
+ function normalizeFeedDiagnosticKind(kind) {
19568
+ return /^[a-z][a-z0-9_]{0,63}$/.test(kind) ? kind : "invalid_unknown_kind";
19569
+ }
19570
+ function normalizeFeedDiagnostic(diagnostic) {
19571
+ if (diagnostic.type !== "unknown_kind") return diagnostic;
19572
+ const kind = normalizeFeedDiagnosticKind(diagnostic.kind);
19573
+ return kind === diagnostic.kind ? diagnostic : { ...diagnostic, kind };
19574
+ }
19575
+ function emitFeedDiagnosticToDefaultSink(diagnostic, target = globalThis) {
19576
+ try {
19577
+ const normalized = normalizeFeedDiagnostic(diagnostic);
19578
+ const EventConstructor = target.CustomEvent || globalThis.CustomEvent;
19579
+ if (typeof target.dispatchEvent !== "function" || typeof EventConstructor !== "function") {
19580
+ return;
19581
+ }
19582
+ target.dispatchEvent(
19583
+ new EventConstructor(GRANULAR_FEED_DIAGNOSTIC_EVENT, {
19584
+ detail: Object.freeze({ ...normalized })
19585
+ })
19586
+ );
19587
+ } catch {
19588
+ }
19589
+ }
19590
+ function emitFeedDiagnostic(diagnostic, listener) {
19591
+ const normalized = normalizeFeedDiagnostic(diagnostic);
19592
+ emitFeedDiagnosticToDefaultSink(normalized);
19593
+ if (!listener) return;
19594
+ try {
19595
+ listener(normalized);
19596
+ } catch {
19597
+ console.warn("[Granular] Session feed diagnostic listener failed");
19598
+ }
19599
+ }
19600
+ function asRecord(value) {
19601
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
19602
+ return null;
19603
+ }
19604
+ return value;
19605
+ }
19606
+ function isSafeInteger(value, minimum = 0) {
19607
+ return typeof value === "number" && Number.isSafeInteger(value) && value >= minimum;
19608
+ }
19609
+ function finiteInteger(value, fallback2, minimum = 0) {
19610
+ return isSafeInteger(value, minimum) ? value : fallback2;
19611
+ }
19612
+ function cloneAndFreeze(value, seen = /* @__PURE__ */ new WeakMap()) {
19613
+ if (!value || typeof value !== "object") {
19614
+ return value;
19615
+ }
19616
+ const object = value;
19617
+ const cached = seen.get(object);
19618
+ if (cached) {
19619
+ return cached;
19620
+ }
19621
+ if (Array.isArray(value)) {
19622
+ const result2 = [];
19623
+ seen.set(object, result2);
19624
+ for (const entry of value) {
19625
+ result2.push(cloneAndFreeze(entry, seen));
19626
+ }
19627
+ return Object.freeze(result2);
19628
+ }
19629
+ const result = /* @__PURE__ */ Object.create(null);
19630
+ seen.set(object, result);
19631
+ for (const [key, entry] of Object.entries(value)) {
19632
+ result[key] = cloneAndFreeze(entry, seen);
19633
+ }
19634
+ return Object.freeze(result);
19635
+ }
19636
+ function snapshotWith(snapshot, patch) {
19637
+ return Object.freeze({ ...snapshot, ...patch });
19638
+ }
19639
+ function emptyFeedSnapshot(isHydrated = false) {
19640
+ return Object.freeze({
19641
+ tail: Object.freeze([]),
19642
+ transients: Object.freeze([]),
19643
+ revision: 0,
19644
+ documentEpoch: 0,
19645
+ documentRevision: 0,
19646
+ lastSequence: 0,
19647
+ archivedThroughSequence: 0,
19648
+ hasOlder: false,
19649
+ isHydrated,
19650
+ isRepairing: false,
19651
+ error: null
19652
+ });
19653
+ }
19654
+ function hasCanonicalSessionFeedActivation(document) {
19655
+ const documentRecord = asRecord(document);
19656
+ const feed = asRecord(documentRecord?.feed);
19657
+ const activation = asRecord(feed?.activation);
19658
+ return activation?.mode === "canonical";
19659
+ }
19660
+ function isCanonicalSessionFeedDocument(document) {
19661
+ return hasCanonicalSessionFeedActivation(document);
19662
+ }
19663
+ function canonicalSessionFeedStructureError(document) {
19664
+ if (!hasCanonicalSessionFeedActivation(document)) return null;
19665
+ const documentRecord = asRecord(document);
19666
+ const feed = asRecord(documentRecord?.feed);
19667
+ const activation = asRecord(feed?.activation);
19668
+ if (feed?.schemaVersion !== 1) {
19669
+ return new Error("Canonical feed schemaVersion must be 1.");
19670
+ }
19671
+ if (!isSafeInteger(activation?.activatedAt)) {
19672
+ return new Error("Canonical feed activation timestamp is invalid.");
19673
+ }
19674
+ if (!Array.isArray(feed.tail)) {
19675
+ return new Error("Canonical feed tail is not an array.");
19676
+ }
19677
+ if (!asRecord(feed.transientById)) {
19678
+ return new Error("Canonical feed transientById is not an object.");
19679
+ }
19680
+ if ([
19681
+ feed.revision,
19682
+ feed.lastSequence,
19683
+ feed.archivedThroughSequence,
19684
+ feed.lastTransientOrdinal,
19685
+ documentRecord?.documentEpoch,
19686
+ documentRecord?.documentRevision
19687
+ ].some((value) => !isSafeInteger(value))) {
19688
+ return new Error("Canonical feed chronology scalar is invalid.");
19689
+ }
19690
+ if (!isSafeInteger(documentRecord?.documentEpoch, 1)) {
19691
+ return new Error("Canonical feed documentEpoch must be positive.");
19692
+ }
19693
+ return null;
19694
+ }
19695
+ function isNonEmptyString(value) {
19696
+ return typeof value === "string" && value.length > 0;
19697
+ }
19698
+ function isKnownDurableFeedKind(kind) {
19699
+ return kind === "message" || kind === "feedback" || kind === "objects" || kind === "table" || kind === "artifact" || kind === "file" || kind === "action_suggestion" || kind === "prompt";
19700
+ }
19701
+ function hasValidKnownFeedPayload(kind, value) {
19702
+ const payload = asRecord(value);
19703
+ if (!payload) return false;
19704
+ switch (kind) {
19705
+ case "message":
19706
+ return (payload.role === "user" || payload.role === "assistant" || payload.role === "system") && typeof payload.text === "string";
19707
+ case "feedback":
19708
+ return typeof payload.text === "string" && payload.audience === "customer" && (payload.tone === "info" || payload.tone === "working" || payload.tone === "awaiting" || payload.tone === "success" || payload.tone === "warning" || payload.tone === "error");
19709
+ case "objects":
19710
+ return Array.isArray(payload.refs) && payload.refs.every((value2) => {
19711
+ const ref = asRecord(value2);
19712
+ return ref?.type === "entry" && isNonEmptyString(ref.path) || ref?.type === "list" && isNonEmptyString(ref.name) || ref?.type === "variable" && isNonEmptyString(ref.name);
19713
+ });
19714
+ case "table":
19715
+ return isNonEmptyString(payload.tableId) && Array.isArray(payload.columns) && Array.isArray(payload.rows);
19716
+ case "artifact": {
19717
+ const fallback2 = asRecord(payload.fallback);
19718
+ return isNonEmptyString(payload.artifactId) && Boolean(fallback2) && isNonEmptyString(fallback2?.label) && (fallback2?.kind === "effect" || fallback2?.kind === "batch" || fallback2?.kind === "state_path");
19719
+ }
19720
+ case "file": {
19721
+ const fallback2 = asRecord(payload.fallback);
19722
+ return isNonEmptyString(payload.fileId) && Boolean(fallback2) && isNonEmptyString(fallback2?.filename);
19723
+ }
19724
+ case "action_suggestion":
19725
+ return isNonEmptyString(payload.suggestionId) && isNonEmptyString(payload.label);
19726
+ case "prompt":
19727
+ return isNonEmptyString(payload.promptId) && (payload.type === "choice" || payload.type === "confirm" || payload.type === "input") && typeof payload.title === "string" && typeof payload.message === "string" && typeof payload.openedAt === "number" && Number.isFinite(payload.openedAt);
19563
19728
  default:
19564
- return DEFAULT_RPC_TIMEOUT_MS;
19729
+ return true;
19565
19730
  }
19566
19731
  }
19567
- var WSClient = class {
19568
- ws = null;
19569
- url;
19570
- sessionId;
19571
- token;
19572
- messageQueue = [];
19573
- syncHandlers = [];
19574
- rpcHandlers = /* @__PURE__ */ new Map();
19575
- eventHandlers = /* @__PURE__ */ new Map();
19576
- nextRpcId = 1;
19577
- doc = Automerge__namespace.init();
19578
- syncState = Automerge__namespace.initSyncState();
19579
- reconnectTimer = null;
19580
- tokenRefreshTimer = null;
19581
- isExplicitlyDisconnected = false;
19582
- reconnectAttempts = 0;
19583
- connectPromise = null;
19584
- connectionEpoch = 0;
19585
- cancelConnectAttempt = null;
19586
- options;
19587
- constructor(options) {
19588
- this.options = options;
19589
- this.url = options.url;
19590
- this.sessionId = options.sessionId;
19591
- this.token = options.token;
19592
- if (options.initialDocumentSnapshot) {
19593
- this.seedDocumentSnapshot(options.initialDocumentSnapshot);
19732
+ function normalizeFeedItem(value) {
19733
+ const item = asRecord(value);
19734
+ if (!item || typeof item.id !== "string" || !item.id || !isSafeInteger(item.sequence, 1) || typeof item.kind !== "string" || !item.kind || typeof item.occurredAt !== "number" || !Number.isFinite(item.occurredAt) || !isNonEmptyString(item.operationId) || !isSafeInteger(item.batchIndex) || !hasValidKnownFeedPayload(item.kind, item.payload)) {
19735
+ return null;
19736
+ }
19737
+ return cloneAndFreeze(item);
19738
+ }
19739
+ function normalizeTransientFeedItem(value) {
19740
+ const item = asRecord(value);
19741
+ const payload = asRecord(item?.payload);
19742
+ if (!item || typeof item.id !== "string" || !item.id || item.kind !== "message" && item.kind !== "feedback" || !isSafeInteger(item.ordinal, 1) || !isSafeInteger(item.revision, 1) || typeof item.createdAt !== "number" || !Number.isFinite(item.createdAt) || typeof item.updatedAt !== "number" || !Number.isFinite(item.updatedAt) || !payload || typeof payload.text !== "string" || item.kind === "message" && (payload.role !== "assistant" || !isSafeInteger(item.producerRevision, 0)) || item.kind === "feedback" && (Object.prototype.hasOwnProperty.call(item, "producerRevision") || payload.audience !== "customer" || !isFeedTransientFeedbackTone(payload.tone))) {
19743
+ return null;
19744
+ }
19745
+ return cloneAndFreeze(item);
19746
+ }
19747
+ var FEED_TRANSIENT_FEEDBACK_TONES = /* @__PURE__ */ new Set(["info", "working", "awaiting", "warning", "error"]);
19748
+ function isFeedTransientFeedbackTone(value) {
19749
+ return typeof value === "string" && FEED_TRANSIENT_FEEDBACK_TONES.has(value);
19750
+ }
19751
+ function normalizeFeedItems(values) {
19752
+ if (!Array.isArray(values)) {
19753
+ return {
19754
+ items: [],
19755
+ error: new Error("Canonical feed tail is not an array.")
19756
+ };
19757
+ }
19758
+ const items = [];
19759
+ const ids = /* @__PURE__ */ new Map();
19760
+ const sequences = /* @__PURE__ */ new Map();
19761
+ const itemBySequence = /* @__PURE__ */ new Map();
19762
+ let error2 = null;
19763
+ for (const value of values) {
19764
+ const item = normalizeFeedItem(value);
19765
+ if (!item) {
19766
+ error2 ||= new Error("Canonical feed contains an invalid durable item.");
19767
+ continue;
19768
+ }
19769
+ const idSequence = ids.get(item.id);
19770
+ const sequenceId = sequences.get(item.sequence);
19771
+ if (idSequence !== void 0 && idSequence !== item.sequence || sequenceId !== void 0 && sequenceId !== item.id) {
19772
+ error2 ||= new Error(
19773
+ `Canonical feed identity conflict at sequence ${item.sequence}.`
19774
+ );
19775
+ continue;
19776
+ }
19777
+ if (idSequence === item.sequence || sequenceId === item.id) {
19778
+ const duplicate = itemBySequence.get(item.sequence);
19779
+ if (duplicate && !sameDurableItem(duplicate, item)) {
19780
+ error2 ||= new Error(
19781
+ `Immutable feed item ${item.id} has conflicting payloads.`
19782
+ );
19783
+ }
19784
+ continue;
19594
19785
  }
19786
+ ids.set(item.id, item.sequence);
19787
+ sequences.set(item.sequence, item.id);
19788
+ itemBySequence.set(item.sequence, item);
19789
+ items.push(item);
19595
19790
  }
19596
- get currentSessionId() {
19597
- return this.sessionId;
19791
+ items.sort((left, right) => left.sequence - right.sequence);
19792
+ return { items: Object.freeze(items), error: error2 };
19793
+ }
19794
+ function normalizeTransients(values) {
19795
+ const record = asRecord(values);
19796
+ if (!record) {
19797
+ return {
19798
+ items: [],
19799
+ error: new Error("Canonical feed transientById is not an object.")
19800
+ };
19598
19801
  }
19599
- seedDocumentSnapshot(document) {
19600
- if (!document || typeof document !== "object" || Array.isArray(document)) {
19601
- return;
19802
+ let error2 = null;
19803
+ const byId = /* @__PURE__ */ new Map();
19804
+ const idByOrdinal = /* @__PURE__ */ new Map();
19805
+ for (const [key, value] of Object.entries(record)) {
19806
+ const item = normalizeTransientFeedItem(value);
19807
+ if (!item || item.id !== key) {
19808
+ error2 ||= new Error("Canonical feed contains an invalid transient item.");
19809
+ continue;
19602
19810
  }
19603
- try {
19604
- this.doc = document instanceof Uint8Array ? Automerge__namespace.load(document) : Automerge__namespace.from(document);
19605
- this.syncState = Automerge__namespace.initSyncState();
19606
- this.emit("sync", this.doc);
19607
- } catch (error2) {
19608
- console.warn("[Granular] Failed to seed cached session document", error2);
19811
+ const ordinalOwner = idByOrdinal.get(item.ordinal);
19812
+ if (ordinalOwner && ordinalOwner !== item.id) {
19813
+ error2 ||= new Error(
19814
+ `Canonical feed transient ordinal ${item.ordinal} is not unique.`
19815
+ );
19816
+ } else {
19817
+ idByOrdinal.set(item.ordinal, item.id);
19818
+ }
19819
+ const current = byId.get(item.id);
19820
+ if (!current || item.revision > current.revision) {
19821
+ byId.set(item.id, item);
19609
19822
  }
19610
19823
  }
19611
- saveDocumentSnapshot() {
19612
- return Automerge__namespace.save(this.doc);
19824
+ const items = [...byId.values()].sort(
19825
+ (left, right) => left.ordinal - right.ordinal || left.id.localeCompare(right.id)
19826
+ );
19827
+ return {
19828
+ items: Object.freeze(items),
19829
+ error: error2
19830
+ };
19831
+ }
19832
+ function readSessionFeedSnapshot(document, options = {}) {
19833
+ const isHydrated = options.isHydrated ?? true;
19834
+ if (!hasCanonicalSessionFeedActivation(document)) {
19835
+ return snapshotWith(emptyFeedSnapshot(isHydrated), {
19836
+ isRepairing: options.isRepairing || false,
19837
+ error: options.error || (isHydrated ? new Error("Canonical session feed-v1 is required.") : null)
19838
+ });
19613
19839
  }
19614
- clearTokenRefreshTimer() {
19615
- if (this.tokenRefreshTimer) {
19616
- clearTimeout(this.tokenRefreshTimer);
19617
- this.tokenRefreshTimer = null;
19840
+ const documentRecord = asRecord(document);
19841
+ const feed = asRecord(documentRecord.feed);
19842
+ const structureError = canonicalSessionFeedStructureError(document);
19843
+ if (structureError) {
19844
+ const lastSequence2 = finiteInteger(feed.lastSequence, 0);
19845
+ const archivedThroughSequence2 = finiteInteger(
19846
+ feed.archivedThroughSequence,
19847
+ 0
19848
+ );
19849
+ return snapshotWith(emptyFeedSnapshot(isHydrated), {
19850
+ revision: finiteInteger(feed.revision, 0),
19851
+ documentEpoch: finiteInteger(documentRecord.documentEpoch, 0),
19852
+ documentRevision: finiteInteger(documentRecord.documentRevision, 0),
19853
+ lastSequence: lastSequence2,
19854
+ archivedThroughSequence: archivedThroughSequence2,
19855
+ hasOlder: archivedThroughSequence2 > 0,
19856
+ isRepairing: options.isRepairing || false,
19857
+ error: options.error || structureError
19858
+ });
19859
+ }
19860
+ const normalizedTail = normalizeFeedItems(feed.tail);
19861
+ const normalizedTransients = normalizeTransients(feed.transientById);
19862
+ const lastSequence = finiteInteger(feed.lastSequence, 0);
19863
+ const archivedThroughSequence = finiteInteger(
19864
+ feed.archivedThroughSequence,
19865
+ 0
19866
+ );
19867
+ let error2 = options.error || normalizedTail.error || normalizedTransients.error;
19868
+ let expected = archivedThroughSequence + 1;
19869
+ for (const item of normalizedTail.items) {
19870
+ if (item.sequence !== expected) {
19871
+ error2 ||= new Error(
19872
+ `Canonical feed tail has a sequence gap before ${item.sequence}; expected ${expected}.`
19873
+ );
19874
+ break;
19618
19875
  }
19876
+ expected += 1;
19619
19877
  }
19620
- decodeBase64Url(payload) {
19621
- const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
19622
- const padded = base64 + "=".repeat((4 - (base64.length % 4 || 4)) % 4);
19623
- if (typeof atob === "function") {
19624
- return atob(padded);
19878
+ const tailLastSequence = normalizedTail.items[normalizedTail.items.length - 1]?.sequence || archivedThroughSequence;
19879
+ if (tailLastSequence !== lastSequence) {
19880
+ error2 ||= new Error(
19881
+ `Canonical feed tail ends at ${tailLastSequence}, but lastSequence is ${lastSequence}.`
19882
+ );
19883
+ }
19884
+ if (archivedThroughSequence > lastSequence) {
19885
+ error2 ||= new Error(
19886
+ "Canonical feed archivedThroughSequence exceeds lastSequence."
19887
+ );
19888
+ }
19889
+ return Object.freeze({
19890
+ tail: normalizedTail.items,
19891
+ transients: normalizedTransients.items,
19892
+ revision: finiteInteger(feed.revision, 0),
19893
+ documentEpoch: finiteInteger(documentRecord.documentEpoch, 0),
19894
+ documentRevision: finiteInteger(documentRecord.documentRevision, 0),
19895
+ lastSequence,
19896
+ archivedThroughSequence,
19897
+ hasOlder: archivedThroughSequence > 0,
19898
+ isHydrated,
19899
+ isRepairing: options.isRepairing || false,
19900
+ error: error2
19901
+ });
19902
+ }
19903
+ function validateFeedListOptions(options) {
19904
+ if (options.afterSequence !== void 0 && options.beforeSequence !== void 0) {
19905
+ throw new RangeError(
19906
+ "Feed list accepts afterSequence or beforeSequence, not both."
19907
+ );
19908
+ }
19909
+ for (const [name, value] of [
19910
+ ["afterSequence", options.afterSequence],
19911
+ ["beforeSequence", options.beforeSequence]
19912
+ ]) {
19913
+ if (value !== void 0 && !isSafeInteger(value)) {
19914
+ throw new RangeError(`${name} must be a non-negative integer.`);
19625
19915
  }
19626
- const maybeBuffer = globalThis.Buffer;
19627
- if (maybeBuffer) {
19628
- return maybeBuffer.from(padded, "base64").toString("utf8");
19916
+ }
19917
+ if (options.limit !== void 0 && (!isSafeInteger(options.limit, 1) || options.limit > 500)) {
19918
+ throw new RangeError("Feed list limit must be an integer from 1 to 500.");
19919
+ }
19920
+ return { ...options };
19921
+ }
19922
+ function normalizeFeedPage(value) {
19923
+ const page = asRecord(value);
19924
+ if (!page) {
19925
+ throw new Error("Feed list transport returned an invalid page.");
19926
+ }
19927
+ const normalized = normalizeFeedItems(page.items);
19928
+ if (normalized.error) {
19929
+ throw normalized.error;
19930
+ }
19931
+ const first = normalized.items[0]?.sequence || null;
19932
+ const last = normalized.items[normalized.items.length - 1]?.sequence || null;
19933
+ for (let index = 1; index < normalized.items.length; index += 1) {
19934
+ if (normalized.items[index].sequence !== normalized.items[index - 1].sequence + 1) {
19935
+ throw new Error("Feed list page contains a sequence gap.");
19629
19936
  }
19630
- throw new Error("No base64 decoder available");
19631
19937
  }
19632
- getTokenExpiryMs(token) {
19633
- const parts = token.split(".");
19634
- if (parts.length < 2) {
19635
- return null;
19938
+ if (first === null && (page.firstSequence !== null || page.lastSequence !== null) || first !== null && (!isSafeInteger(page.firstSequence, 1) || !isSafeInteger(page.lastSequence, 1) || page.firstSequence !== first || page.lastSequence !== last) || typeof page.hasMoreBefore !== "boolean" || typeof page.hasMoreAfter !== "boolean") {
19939
+ throw new Error(
19940
+ "Feed list page sequence metadata does not match its items."
19941
+ );
19942
+ }
19943
+ return Object.freeze({
19944
+ items: normalized.items,
19945
+ firstSequence: first,
19946
+ lastSequence: last,
19947
+ hasMoreBefore: page.hasMoreBefore,
19948
+ hasMoreAfter: page.hasMoreAfter
19949
+ });
19950
+ }
19951
+ function stableValueFingerprint(value, ancestors = /* @__PURE__ */ new WeakSet()) {
19952
+ if (!value || typeof value !== "object") {
19953
+ return JSON.stringify(value);
19954
+ }
19955
+ if (ancestors.has(value)) return '"[circular]"';
19956
+ ancestors.add(value);
19957
+ const result = Array.isArray(value) ? `[${value.map((entry) => stableValueFingerprint(entry, ancestors)).join(",")}]` : `{${Object.keys(value).sort().map(
19958
+ (key) => `${JSON.stringify(key)}:${stableValueFingerprint(
19959
+ value[key],
19960
+ ancestors
19961
+ )}`
19962
+ ).join(",")}}`;
19963
+ ancestors.delete(value);
19964
+ return result;
19965
+ }
19966
+ function sameDurableItem(left, right) {
19967
+ return stableValueFingerprint(left) === stableValueFingerprint(right);
19968
+ }
19969
+ function contiguousLocalTailWatermark(snapshot) {
19970
+ let watermark = 0;
19971
+ for (const item of snapshot.tail) {
19972
+ if (item.sequence !== watermark + 1) break;
19973
+ watermark = item.sequence;
19974
+ }
19975
+ return watermark;
19976
+ }
19977
+ var SessionFeedController = class {
19978
+ snapshot;
19979
+ canonical;
19980
+ canonicalStructureValid;
19981
+ deliveredThrough;
19982
+ knownBySequence = /* @__PURE__ */ new Map();
19983
+ sequenceById = /* @__PURE__ */ new Map();
19984
+ subscribers = /* @__PURE__ */ new Set();
19985
+ listTransport;
19986
+ repairGeneration = 0;
19987
+ repairPromise = null;
19988
+ repairRetryHandle = null;
19989
+ repairRetryToken = 0;
19990
+ repairFailureAttempt = 0;
19991
+ scheduleRepairRetryCallback;
19992
+ cancelRepairRetryCallback;
19993
+ initialRepairRetryDelayMs;
19994
+ maxRepairRetryDelayMs;
19995
+ disposed = false;
19996
+ diagnosticListener;
19997
+ diagnosticNow;
19998
+ observedUnknownPositions = /* @__PURE__ */ new Set();
19999
+ constructor(initialDocument, options = {}) {
20000
+ this.diagnosticListener = options.onDiagnostic || null;
20001
+ this.diagnosticNow = options.now || Date.now;
20002
+ this.scheduleRepairRetryCallback = options.scheduleRepairRetry || ((callback, delayMs) => setTimeout(callback, delayMs));
20003
+ this.cancelRepairRetryCallback = options.cancelRepairRetry || ((handle) => clearTimeout(handle));
20004
+ this.initialRepairRetryDelayMs = Math.max(
20005
+ 1,
20006
+ options.initialRepairRetryDelayMs ?? 500
20007
+ );
20008
+ this.maxRepairRetryDelayMs = Math.max(
20009
+ this.initialRepairRetryDelayMs,
20010
+ options.maxRepairRetryDelayMs ?? 1e4
20011
+ );
20012
+ this.canonical = hasCanonicalSessionFeedActivation(initialDocument);
20013
+ this.canonicalStructureValid = this.canonical && canonicalSessionFeedStructureError(initialDocument) === null;
20014
+ this.snapshot = readSessionFeedSnapshot(initialDocument, {
20015
+ isHydrated: options.isHydrated ?? this.canonical
20016
+ });
20017
+ this.listTransport = options.listTransport || null;
20018
+ this.deliveredThrough = contiguousLocalTailWatermark(this.snapshot);
20019
+ const identityError = this.remember(this.snapshot.tail);
20020
+ const needsRepair = this.canonicalStructureValid && this.deliveredThrough < this.snapshot.lastSequence;
20021
+ if (identityError || needsRepair) {
20022
+ this.snapshot = snapshotWith(this.snapshot, {
20023
+ isRepairing: needsRepair && Boolean(this.listTransport),
20024
+ error: identityError || this.snapshot.error
20025
+ });
19636
20026
  }
19637
- try {
19638
- const payloadSegment = parts[1];
19639
- if (!payloadSegment) {
19640
- return null;
19641
- }
19642
- const payloadRaw = this.decodeBase64Url(payloadSegment);
19643
- const payload = JSON.parse(payloadRaw);
19644
- if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
19645
- return null;
19646
- }
19647
- return payload.exp * 1e3;
19648
- } catch {
19649
- return null;
20027
+ if (needsRepair) {
20028
+ this.startRepair(this.snapshot.lastSequence);
19650
20029
  }
19651
20030
  }
19652
- scheduleTokenRefresh() {
19653
- this.clearTokenRefreshTimer();
19654
- if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
19655
- return;
20031
+ setListTransport(transport) {
20032
+ if (this.disposed) return;
20033
+ if (transport !== this.listTransport) {
20034
+ this.cancelScheduledRepairRetry();
20035
+ this.repairFailureAttempt = 0;
20036
+ this.repairGeneration += 1;
20037
+ this.repairPromise = null;
19656
20038
  }
19657
- const expiresAt = this.getTokenExpiryMs(this.token);
19658
- if (!expiresAt) {
19659
- return;
20039
+ this.listTransport = transport;
20040
+ if (transport && this.canonical && this.canonicalStructureValid && this.deliveredThrough < this.snapshot.lastSequence) {
20041
+ this.snapshot = snapshotWith(this.snapshot, {
20042
+ isRepairing: true,
20043
+ error: null
20044
+ });
20045
+ this.startRepair(this.snapshot.lastSequence);
19660
20046
  }
19661
- const refreshInMs = Math.max(
19662
- 1e3,
19663
- expiresAt - Date.now() - TOKEN_REFRESH_LEEWAY_MS
19664
- );
19665
- const delay = Math.min(refreshInMs, MAX_TIMER_DELAY_MS);
19666
- this.tokenRefreshTimer = setTimeout(() => {
19667
- void this.refreshTokenInBackground();
19668
- }, delay);
19669
20047
  }
19670
- async refreshTokenInBackground() {
19671
- if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
19672
- return;
20048
+ getSnapshot() {
20049
+ return this.snapshot;
20050
+ }
20051
+ /**
20052
+ * Stop background archive work when its owning Session is replaced or
20053
+ * explicitly disconnected. Late transport completions are quarantined by
20054
+ * the generation check and cannot update subscribers.
20055
+ */
20056
+ dispose() {
20057
+ if (this.disposed) return;
20058
+ this.disposed = true;
20059
+ this.cancelScheduledRepairRetry();
20060
+ this.repairGeneration += 1;
20061
+ this.repairPromise = null;
20062
+ this.listTransport = null;
20063
+ this.subscribers.clear();
20064
+ }
20065
+ async list(options = {}) {
20066
+ const validated = validateFeedListOptions(options);
20067
+ if (!this.listTransport) {
20068
+ throw new Error(
20069
+ "Historical feed transport is unavailable for this Session."
20070
+ );
19673
20071
  }
19674
- try {
19675
- const refreshedToken = await this.options.tokenProvider();
19676
- if (typeof refreshedToken !== "string" || refreshedToken.length === 0) {
19677
- throw new Error("Token provider returned no token");
19678
- }
19679
- this.token = refreshedToken;
19680
- this.scheduleTokenRefresh();
19681
- } catch (error2) {
19682
- if (this.isExplicitlyDisconnected) {
19683
- return;
20072
+ const page = normalizeFeedPage(await this.listTransport(validated));
20073
+ this.observeUnknownKinds(page.items);
20074
+ return page;
20075
+ }
20076
+ subscribe(listener, options = {}) {
20077
+ const afterSequence = options.afterSequence;
20078
+ if (afterSequence !== void 0 && !isSafeInteger(afterSequence)) {
20079
+ throw new RangeError(
20080
+ "Feed afterSequence must be a non-negative safe integer."
20081
+ );
20082
+ }
20083
+ this.subscribers.add(listener);
20084
+ if (afterSequence !== void 0 && afterSequence < this.snapshot.lastSequence) {
20085
+ const items = this.snapshot.tail.filter(
20086
+ (item) => item.sequence > afterSequence
20087
+ );
20088
+ if (items.length > 0 && items[0].sequence === afterSequence + 1 && items[items.length - 1].sequence === this.snapshot.lastSequence) {
20089
+ listener({ type: "append", items });
20090
+ if (this.snapshot.transients.length > 0) {
20091
+ listener({
20092
+ type: "transients",
20093
+ items: this.snapshot.transients,
20094
+ revision: this.snapshot.revision
20095
+ });
20096
+ }
20097
+ } else {
20098
+ listener({ type: "reset", snapshot: this.snapshot });
19684
20099
  }
19685
- console.warn("[Granular] Token refresh failed, retrying soon:", error2);
19686
- this.clearTokenRefreshTimer();
19687
- this.tokenRefreshTimer = setTimeout(() => {
19688
- void this.refreshTokenInBackground();
19689
- }, TOKEN_REFRESH_RETRY_MS);
20100
+ } else {
20101
+ listener({ type: "reset", snapshot: this.snapshot });
20102
+ }
20103
+ return () => {
20104
+ this.subscribers.delete(listener);
20105
+ };
20106
+ }
20107
+ /**
20108
+ * Accept the latest synced document. Calls may arrive out of order after a
20109
+ * reconnect; freshness watermarks prevent an older snapshot from regressing
20110
+ * durable positions or transient state.
20111
+ */
20112
+ updateDocument(document, options = {}) {
20113
+ if (this.disposed) return;
20114
+ const nextCanonical = hasCanonicalSessionFeedActivation(document);
20115
+ const next = readSessionFeedSnapshot(document, { isHydrated: true });
20116
+ if (!nextCanonical) {
20117
+ if (!this.canonical) {
20118
+ this.snapshot = next;
20119
+ this.emit({ type: "reset", snapshot: this.snapshot });
20120
+ return;
20121
+ }
20122
+ this.rejectSnapshotRegression("canonical_deactivation", next);
20123
+ return;
20124
+ }
20125
+ const nextStructureError = canonicalSessionFeedStructureError(document);
20126
+ if (nextStructureError) {
20127
+ if (!this.canonical) {
20128
+ this.canonical = true;
20129
+ this.canonicalStructureValid = false;
20130
+ this.cancelScheduledRepairRetry();
20131
+ this.repairFailureAttempt = 0;
20132
+ this.repairGeneration += 1;
20133
+ this.repairPromise = null;
20134
+ this.deliveredThrough = 0;
20135
+ this.knownBySequence.clear();
20136
+ this.sequenceById.clear();
20137
+ this.observedUnknownPositions.clear();
20138
+ this.snapshot = next;
20139
+ this.emit({ type: "reset", snapshot: this.snapshot });
20140
+ return;
20141
+ }
20142
+ this.rejectSnapshotRegression("invalid_canonical_document", next);
20143
+ if (!this.canonicalStructureValid) {
20144
+ const current2 = this.snapshot;
20145
+ const incomingIsNewer = next.documentEpoch > current2.documentEpoch || next.documentEpoch === current2.documentEpoch && next.documentRevision >= current2.documentRevision;
20146
+ if (incomingIsNewer && next.lastSequence >= current2.lastSequence) {
20147
+ this.snapshot = next;
20148
+ }
20149
+ } else {
20150
+ this.snapshot = snapshotWith(this.snapshot, {
20151
+ error: nextStructureError
20152
+ });
20153
+ }
20154
+ this.emit({ type: "reset", snapshot: this.snapshot });
20155
+ return;
20156
+ }
20157
+ if (!this.canonical || options.forceReset) {
20158
+ this.acceptReset(next);
20159
+ return;
20160
+ }
20161
+ if (!this.canonicalStructureValid) {
20162
+ const current2 = this.snapshot;
20163
+ if (next.documentEpoch < current2.documentEpoch) {
20164
+ this.rejectSnapshotRegression("older_document_epoch", next);
20165
+ return;
20166
+ }
20167
+ if (next.lastSequence < current2.lastSequence) {
20168
+ this.rejectSnapshotRegression("last_sequence_regression", next);
20169
+ return;
20170
+ }
20171
+ if (next.documentEpoch === current2.documentEpoch && next.documentRevision < current2.documentRevision) {
20172
+ this.rejectSnapshotRegression("document_revision_regression", next);
20173
+ return;
20174
+ }
20175
+ if (next.documentEpoch === current2.documentEpoch && next.revision < current2.revision) {
20176
+ this.rejectSnapshotRegression("feed_revision_regression", next);
20177
+ return;
20178
+ }
20179
+ this.acceptReset(next);
20180
+ return;
20181
+ }
20182
+ const current = this.snapshot;
20183
+ if (next.documentEpoch < current.documentEpoch) {
20184
+ this.rejectSnapshotRegression("older_document_epoch", next);
20185
+ return;
20186
+ }
20187
+ if (next.documentEpoch > current.documentEpoch) {
20188
+ if (next.lastSequence < current.lastSequence) {
20189
+ this.rejectSnapshotRegression("last_sequence_regression", next);
20190
+ return;
20191
+ }
20192
+ const identityError2 = this.findIdentityConflict(next.tail);
20193
+ if (identityError2) {
20194
+ this.rejectSnapshotRegression("identity_conflict", next);
20195
+ this.snapshot = snapshotWith(current, { error: identityError2 });
20196
+ this.emit({ type: "reset", snapshot: this.snapshot });
20197
+ return;
20198
+ }
20199
+ this.acceptReset(next);
20200
+ return;
20201
+ }
20202
+ if (next.lastSequence < current.lastSequence) {
20203
+ this.rejectSnapshotRegression("last_sequence_regression", next);
20204
+ if (next.documentRevision > current.documentRevision) {
20205
+ this.emit({
20206
+ type: "resource_refresh",
20207
+ documentRevision: next.documentRevision
20208
+ });
20209
+ this.snapshot = snapshotWith(current, {
20210
+ documentRevision: next.documentRevision
20211
+ });
20212
+ }
20213
+ return;
20214
+ }
20215
+ const previousDocumentRevision = current.documentRevision;
20216
+ const previousRevision = current.revision;
20217
+ if (next.lastSequence > current.lastSequence && next.revision <= previousRevision) {
20218
+ this.rejectSnapshotRegression("last_sequence_without_revision", next);
20219
+ const documentRevision = Math.max(
20220
+ previousDocumentRevision,
20221
+ next.documentRevision
20222
+ );
20223
+ this.snapshot = snapshotWith(current, {
20224
+ documentRevision,
20225
+ error: new Error(
20226
+ "Feed lastSequence advanced without a newer feed revision."
20227
+ )
20228
+ });
20229
+ if (next.documentRevision > previousDocumentRevision) {
20230
+ this.emit({
20231
+ type: "resource_refresh",
20232
+ documentRevision: next.documentRevision
20233
+ });
20234
+ }
20235
+ this.emit({ type: "reset", snapshot: this.snapshot });
20236
+ return;
20237
+ }
20238
+ const previousTransients = current.transients;
20239
+ const transientIdentityError = next.revision === previousRevision && !sameTransientSet(previousTransients, next.transients) ? new Error("Transient feed changed without a newer feed revision.") : null;
20240
+ const transientSelection = this.selectNewerTransients(current, next);
20241
+ const mergedTransients = transientSelection.items;
20242
+ const identityError = transientIdentityError || transientSelection.error || this.remember(next.tail);
20243
+ if (identityError) {
20244
+ this.rejectSnapshotRegression("identity_conflict", next);
20245
+ this.snapshot = snapshotWith(current, {
20246
+ documentRevision: Math.max(
20247
+ previousDocumentRevision,
20248
+ next.documentRevision
20249
+ ),
20250
+ error: identityError
20251
+ });
20252
+ this.emit({ type: "reset", snapshot: this.snapshot });
20253
+ return;
20254
+ }
20255
+ const keepCurrentFeedState = next.lastSequence === current.lastSequence && next.revision < current.revision;
20256
+ if (keepCurrentFeedState) {
20257
+ this.rejectSnapshotRegression("feed_revision_regression", next);
20258
+ }
20259
+ let nextSnapshot = snapshotWith(keepCurrentFeedState ? current : next, {
20260
+ transients: mergedTransients,
20261
+ revision: Math.max(previousRevision, next.revision),
20262
+ documentRevision: Math.max(
20263
+ previousDocumentRevision,
20264
+ next.documentRevision
20265
+ )
20266
+ });
20267
+ const appended = next.lastSequence > this.deliveredThrough ? this.drainContiguous(next.lastSequence) : [];
20268
+ const needsRepair = this.deliveredThrough < next.lastSequence;
20269
+ const incomingAdvanced = next.documentEpoch > current.documentEpoch || next.documentRevision > current.documentRevision || next.revision > current.revision || next.lastSequence > current.lastSequence;
20270
+ if (incomingAdvanced || !needsRepair) {
20271
+ this.cancelScheduledRepairRetry();
20272
+ this.repairFailureAttempt = 0;
20273
+ }
20274
+ nextSnapshot = snapshotWith(nextSnapshot, {
20275
+ isRepairing: needsRepair,
20276
+ error: nextSnapshot.error
20277
+ });
20278
+ this.snapshot = nextSnapshot;
20279
+ if (appended.length > 0) {
20280
+ this.emit({ type: "append", items: appended });
20281
+ }
20282
+ if (!sameTransientSet(previousTransients, mergedTransients) && next.revision > previousRevision) {
20283
+ this.emit({
20284
+ type: "transients",
20285
+ items: mergedTransients,
20286
+ revision: this.snapshot.revision
20287
+ });
20288
+ }
20289
+ if (next.documentRevision > previousDocumentRevision) {
20290
+ this.emit({
20291
+ type: "resource_refresh",
20292
+ documentRevision: next.documentRevision
20293
+ });
20294
+ }
20295
+ if (needsRepair) {
20296
+ this.startRepair(next.lastSequence);
20297
+ }
20298
+ }
20299
+ /**
20300
+ * Quarantine a canonical replacement rejected by the document transport.
20301
+ * The notice deliberately contains no replacement document, so accepted
20302
+ * feed history remains the only data visible to subscribers during repair.
20303
+ *
20304
+ * @internal
20305
+ */
20306
+ quarantineCanonicalReplacement(quarantine) {
20307
+ if (this.disposed) return;
20308
+ const incoming = snapshotWith(emptyFeedSnapshot(true), {
20309
+ documentEpoch: quarantine.documentEpoch,
20310
+ documentRevision: quarantine.documentRevision,
20311
+ isRepairing: true,
20312
+ error: quarantine.error
20313
+ });
20314
+ if (!this.canonical) {
20315
+ this.canonical = true;
20316
+ this.canonicalStructureValid = false;
20317
+ this.repairGeneration += 1;
20318
+ this.repairPromise = null;
20319
+ this.cancelScheduledRepairRetry();
20320
+ this.repairFailureAttempt = 0;
20321
+ this.deliveredThrough = 0;
20322
+ this.knownBySequence.clear();
20323
+ this.sequenceById.clear();
20324
+ this.observedUnknownPositions.clear();
20325
+ this.snapshot = incoming;
20326
+ this.emit({ type: "reset", snapshot: this.snapshot });
20327
+ return;
20328
+ }
20329
+ this.rejectSnapshotRegression("invalid_canonical_document", incoming);
20330
+ this.repairGeneration += 1;
20331
+ this.repairPromise = null;
20332
+ this.cancelScheduledRepairRetry();
20333
+ this.repairFailureAttempt = 0;
20334
+ if (!this.canonicalStructureValid) {
20335
+ const current = this.snapshot;
20336
+ const incomingIsNewer = incoming.documentEpoch > current.documentEpoch || incoming.documentEpoch === current.documentEpoch && incoming.documentRevision >= current.documentRevision;
20337
+ if (incomingIsNewer) {
20338
+ this.snapshot = incoming;
20339
+ } else {
20340
+ this.snapshot = snapshotWith(current, {
20341
+ isRepairing: true,
20342
+ error: quarantine.error
20343
+ });
20344
+ }
20345
+ } else {
20346
+ this.snapshot = snapshotWith(this.snapshot, {
20347
+ isRepairing: true,
20348
+ error: quarantine.error
20349
+ });
20350
+ }
20351
+ this.emit({ type: "reset", snapshot: this.snapshot });
20352
+ }
20353
+ acceptReset(snapshot) {
20354
+ this.cancelScheduledRepairRetry();
20355
+ this.repairFailureAttempt = 0;
20356
+ this.repairGeneration += 1;
20357
+ this.repairPromise = null;
20358
+ this.canonical = true;
20359
+ this.canonicalStructureValid = true;
20360
+ this.knownBySequence.clear();
20361
+ this.sequenceById.clear();
20362
+ this.observedUnknownPositions.clear();
20363
+ this.deliveredThrough = contiguousLocalTailWatermark(snapshot);
20364
+ const identityError = this.remember(snapshot.tail);
20365
+ const needsRepair = this.deliveredThrough < snapshot.lastSequence;
20366
+ this.snapshot = snapshotWith(snapshot, {
20367
+ isRepairing: needsRepair && Boolean(this.listTransport),
20368
+ error: identityError || snapshot.error
20369
+ });
20370
+ this.emit({ type: "reset", snapshot: this.snapshot });
20371
+ if (needsRepair) {
20372
+ this.startRepair(snapshot.lastSequence);
20373
+ }
20374
+ }
20375
+ emitDiagnostic(diagnostic) {
20376
+ emitFeedDiagnostic(diagnostic, this.diagnosticListener);
20377
+ }
20378
+ rejectSnapshotRegression(reason, incoming) {
20379
+ const current = this.snapshot;
20380
+ this.emitDiagnostic({
20381
+ type: "snapshot_regression_rejected",
20382
+ reason,
20383
+ currentDocumentEpoch: current.documentEpoch,
20384
+ incomingDocumentEpoch: incoming.documentEpoch,
20385
+ currentDocumentRevision: current.documentRevision,
20386
+ incomingDocumentRevision: incoming.documentRevision,
20387
+ currentFeedRevision: current.revision,
20388
+ incomingFeedRevision: incoming.revision,
20389
+ currentLastSequence: current.lastSequence,
20390
+ incomingLastSequence: incoming.lastSequence
20391
+ });
20392
+ }
20393
+ observeUnknownKinds(items) {
20394
+ for (const item of items) {
20395
+ const runtimeKind = String(item.kind);
20396
+ if (isKnownDurableFeedKind(runtimeKind)) continue;
20397
+ const diagnosticKind = normalizeFeedDiagnosticKind(runtimeKind);
20398
+ const position = `${item.sequence}:${diagnosticKind}`;
20399
+ if (this.observedUnknownPositions.has(position)) continue;
20400
+ this.observedUnknownPositions.add(position);
20401
+ this.emitDiagnostic({
20402
+ type: "unknown_kind",
20403
+ kind: diagnosticKind,
20404
+ sequence: item.sequence,
20405
+ consumer: "sdk"
20406
+ });
20407
+ }
20408
+ }
20409
+ selectNewerTransients(current, next) {
20410
+ if (next.revision <= current.revision) {
20411
+ return { items: current.transients, error: null };
20412
+ }
20413
+ let newestById;
20414
+ try {
20415
+ newestById = new Map(
20416
+ orderTransientFeedItems([
20417
+ ...current.transients,
20418
+ ...next.transients
20419
+ ]).map((item) => [item.id, item])
20420
+ );
20421
+ } catch {
20422
+ return {
20423
+ items: current.transients,
20424
+ error: new Error(
20425
+ "Transient feed changed immutable identity or revision."
20426
+ )
20427
+ };
20428
+ }
20429
+ const currentById = new Map(
20430
+ current.transients.map((item) => [item.id, item])
20431
+ );
20432
+ const selected = [];
20433
+ for (const incoming of next.transients) {
20434
+ const previous = currentById.get(incoming.id);
20435
+ if (previous && previous.kind !== incoming.kind) {
20436
+ return {
20437
+ items: current.transients,
20438
+ error: new Error(
20439
+ `Transient feed item ${incoming.id} changed its immutable kind.`
20440
+ )
20441
+ };
20442
+ }
20443
+ if (previous?.kind === "message" && incoming.kind === "message" && incoming.revision > previous.revision && incoming.producerRevision < previous.producerRevision) {
20444
+ selected.push(previous);
20445
+ continue;
20446
+ }
20447
+ selected.push(newestById.get(incoming.id) || incoming);
20448
+ }
20449
+ return {
20450
+ items: orderTransientFeedItems(selected),
20451
+ error: null
20452
+ };
20453
+ }
20454
+ remember(items) {
20455
+ const stagedBySequence = /* @__PURE__ */ new Map();
20456
+ const stagedSequenceById = /* @__PURE__ */ new Map();
20457
+ for (const item of items) {
20458
+ const knownSequence = stagedSequenceById.get(item.id) ?? this.sequenceById.get(item.id);
20459
+ const knownItem = stagedBySequence.get(item.sequence) || this.knownBySequence.get(item.sequence);
20460
+ if (knownSequence !== void 0 && knownSequence !== item.sequence || knownItem && knownItem.id !== item.id) {
20461
+ return new Error(
20462
+ `Feed identity changed at sequence ${item.sequence}; a reset is required.`
20463
+ );
20464
+ }
20465
+ if (knownItem && !sameDurableItem(knownItem, item)) {
20466
+ return new Error(
20467
+ `Immutable feed item ${item.id} changed at sequence ${item.sequence}.`
20468
+ );
20469
+ }
20470
+ stagedSequenceById.set(item.id, item.sequence);
20471
+ stagedBySequence.set(item.sequence, item);
20472
+ }
20473
+ for (const item of stagedBySequence.values()) {
20474
+ this.sequenceById.set(item.id, item.sequence);
20475
+ this.knownBySequence.set(item.sequence, item);
20476
+ }
20477
+ this.observeUnknownKinds([...stagedBySequence.values()]);
20478
+ return null;
20479
+ }
20480
+ findIdentityConflict(items) {
20481
+ for (const item of items) {
20482
+ const knownSequence = this.sequenceById.get(item.id);
20483
+ const knownItem = this.knownBySequence.get(item.sequence);
20484
+ if (knownSequence !== void 0 && knownSequence !== item.sequence || knownItem && knownItem.id !== item.id) {
20485
+ return new Error(
20486
+ `Feed identity changed at sequence ${item.sequence}; the checkpoint was rejected.`
20487
+ );
20488
+ }
20489
+ if (knownItem && !sameDurableItem(knownItem, item)) {
20490
+ return new Error(
20491
+ `Immutable feed item ${item.id} changed at sequence ${item.sequence}.`
20492
+ );
20493
+ }
20494
+ }
20495
+ return null;
20496
+ }
20497
+ drainContiguous(targetSequence) {
20498
+ const appended = [];
20499
+ let sequence = this.deliveredThrough + 1;
20500
+ while (sequence <= targetSequence) {
20501
+ const item = this.knownBySequence.get(sequence);
20502
+ if (!item) break;
20503
+ appended.push(item);
20504
+ this.deliveredThrough = sequence;
20505
+ sequence += 1;
20506
+ }
20507
+ return Object.freeze(appended);
20508
+ }
20509
+ startRepair(targetSequence) {
20510
+ if (this.disposed || this.repairPromise || this.repairRetryHandle !== null) {
20511
+ return;
20512
+ }
20513
+ if (!this.listTransport) {
20514
+ this.emitDiagnostic({
20515
+ type: "gap_detected",
20516
+ expectedSequence: this.deliveredThrough + 1,
20517
+ targetSequence,
20518
+ repairAvailable: false
20519
+ });
20520
+ this.snapshot = snapshotWith(this.snapshot, {
20521
+ isRepairing: false,
20522
+ error: this.snapshot.error || new Error(
20523
+ "Feed sequence gap cannot be repaired without history transport."
20524
+ )
20525
+ });
20526
+ this.emit({ type: "reset", snapshot: this.snapshot });
20527
+ return;
20528
+ }
20529
+ this.emitDiagnostic({
20530
+ type: "gap_detected",
20531
+ expectedSequence: this.deliveredThrough + 1,
20532
+ targetSequence,
20533
+ repairAvailable: true
20534
+ });
20535
+ const generation = ++this.repairGeneration;
20536
+ this.repairPromise = this.repair(targetSequence, generation).finally(() => {
20537
+ if (generation === this.repairGeneration) {
20538
+ this.repairPromise = null;
20539
+ if (this.deliveredThrough < this.snapshot.lastSequence) {
20540
+ if (this.snapshot.error) {
20541
+ this.scheduleQuietRepairRetry();
20542
+ } else {
20543
+ this.snapshot = snapshotWith(this.snapshot, { isRepairing: true });
20544
+ this.startRepair(this.snapshot.lastSequence);
20545
+ }
20546
+ } else {
20547
+ this.repairFailureAttempt = 0;
20548
+ }
20549
+ }
20550
+ });
20551
+ }
20552
+ cancelScheduledRepairRetry() {
20553
+ if (this.repairRetryHandle === null) return;
20554
+ const handle = this.repairRetryHandle;
20555
+ this.repairRetryHandle = null;
20556
+ this.repairRetryToken += 1;
20557
+ this.cancelRepairRetryCallback(handle);
20558
+ }
20559
+ scheduleQuietRepairRetry() {
20560
+ if (this.disposed || this.repairRetryHandle !== null || this.repairPromise || !this.listTransport || this.deliveredThrough >= this.snapshot.lastSequence) {
20561
+ return;
20562
+ }
20563
+ this.repairFailureAttempt += 1;
20564
+ const delayMs = Math.min(
20565
+ this.maxRepairRetryDelayMs,
20566
+ this.initialRepairRetryDelayMs * 2 ** Math.min(30, Math.max(0, this.repairFailureAttempt - 1))
20567
+ );
20568
+ const retryToken = ++this.repairRetryToken;
20569
+ let handle;
20570
+ handle = this.scheduleRepairRetryCallback(() => {
20571
+ if (retryToken !== this.repairRetryToken || this.repairRetryHandle !== handle) {
20572
+ return;
20573
+ }
20574
+ this.repairRetryHandle = null;
20575
+ if (this.disposed) return;
20576
+ this.snapshot = snapshotWith(this.snapshot, {
20577
+ isRepairing: true,
20578
+ error: null
20579
+ });
20580
+ this.emit({ type: "reset", snapshot: this.snapshot });
20581
+ this.startRepair(this.snapshot.lastSequence);
20582
+ }, delayMs);
20583
+ this.repairRetryHandle = handle;
20584
+ }
20585
+ async repair(targetSequence, generation) {
20586
+ const startedAt = this.diagnosticNow();
20587
+ let pageCount = 0;
20588
+ let outcome = "success";
20589
+ try {
20590
+ let attempts = 0;
20591
+ while (generation === this.repairGeneration && this.deliveredThrough < targetSequence) {
20592
+ if (++attempts > 100) {
20593
+ throw new Error("Feed gap repair exceeded its page limit.");
20594
+ }
20595
+ const page = await this.list({
20596
+ afterSequence: this.deliveredThrough,
20597
+ limit: 500
20598
+ });
20599
+ pageCount += 1;
20600
+ if (generation !== this.repairGeneration) {
20601
+ outcome = "cancelled";
20602
+ return;
20603
+ }
20604
+ if (page.items.length === 0) {
20605
+ throw new Error(
20606
+ `Feed gap repair returned no item after sequence ${this.deliveredThrough}.`
20607
+ );
20608
+ }
20609
+ const deliveredBeforePage = this.deliveredThrough;
20610
+ const identityError = this.remember(page.items);
20611
+ if (identityError) throw identityError;
20612
+ const appended = this.drainContiguous(targetSequence);
20613
+ const reachedTarget = this.deliveredThrough >= targetSequence;
20614
+ if (reachedTarget) {
20615
+ this.snapshot = snapshotWith(this.snapshot, {
20616
+ isRepairing: this.deliveredThrough < this.snapshot.lastSequence,
20617
+ error: null
20618
+ });
20619
+ }
20620
+ if (appended.length > 0) {
20621
+ this.emit({ type: "append", items: appended });
20622
+ }
20623
+ if (this.deliveredThrough === deliveredBeforePage) {
20624
+ throw new Error(
20625
+ `Feed gap repair did not return expected sequence ${deliveredBeforePage + 1}.`
20626
+ );
20627
+ }
20628
+ if (this.deliveredThrough < targetSequence && !page.hasMoreAfter && (page.lastSequence || 0) < targetSequence) {
20629
+ throw new Error(
20630
+ `Feed gap remains after sequence ${this.deliveredThrough}.`
20631
+ );
20632
+ }
20633
+ }
20634
+ if (generation !== this.repairGeneration) {
20635
+ outcome = "cancelled";
20636
+ return;
20637
+ }
20638
+ } catch (error2) {
20639
+ if (generation !== this.repairGeneration) {
20640
+ outcome = "cancelled";
20641
+ return;
20642
+ }
20643
+ outcome = "failure";
20644
+ this.snapshot = snapshotWith(this.snapshot, {
20645
+ isRepairing: false,
20646
+ error: error2 instanceof Error ? error2 : new Error(String(error2))
20647
+ });
20648
+ this.emit({ type: "reset", snapshot: this.snapshot });
20649
+ } finally {
20650
+ this.emitDiagnostic({
20651
+ type: "gap_repair",
20652
+ outcome,
20653
+ durationMs: Math.max(0, this.diagnosticNow() - startedAt),
20654
+ pageCount,
20655
+ repairedThroughSequence: this.deliveredThrough,
20656
+ targetSequence
20657
+ });
20658
+ }
20659
+ }
20660
+ emit(change) {
20661
+ for (const subscriber of this.subscribers) {
20662
+ try {
20663
+ subscriber(change);
20664
+ } catch (error2) {
20665
+ console.error("[Granular] Session feed subscriber failed", error2);
20666
+ }
20667
+ }
20668
+ }
20669
+ };
20670
+ function sameTransientSet(left, right) {
20671
+ return stableValueFingerprint(left) === stableValueFingerprint(right);
20672
+ }
20673
+ function operationId(prefix) {
20674
+ const randomUuid = globalThis.crypto?.randomUUID?.();
20675
+ return randomUuid ? `${prefix}_${randomUuid}` : `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
20676
+ }
20677
+ function unwrapPublishedItem(value) {
20678
+ const record = asRecord(value);
20679
+ const directItems = Array.isArray(record?.items) ? record.items : [];
20680
+ const durable = asRecord(record?.durable);
20681
+ const durableItems = Array.isArray(durable?.items) ? durable.items : [];
20682
+ return record?.item || directItems[0] || durableItems[0] || value;
20683
+ }
20684
+ function requireFeedbackItem(value) {
20685
+ const item = normalizeFeedItem(unwrapPublishedItem(value));
20686
+ if (!item || item.kind !== "feedback") {
20687
+ throw new Error("Feed publisher returned an invalid feedback item.");
20688
+ }
20689
+ return item;
20690
+ }
20691
+ function requireTransientFeedbackItem(value) {
20692
+ const item = normalizeTransientFeedItem(unwrapPublishedItem(value));
20693
+ if (!item || item.kind !== "feedback") {
20694
+ throw new Error(
20695
+ "Feed publisher returned an invalid transient feedback item."
20696
+ );
20697
+ }
20698
+ return item;
20699
+ }
20700
+ function createFeedPublisher(publish) {
20701
+ const makeHandle = (initial) => {
20702
+ let current = initial;
20703
+ let mutationQueue = Promise.resolve();
20704
+ const enqueueMutation = (mutation) => {
20705
+ const result = mutationQueue.then(mutation);
20706
+ mutationQueue = result.then(
20707
+ () => void 0,
20708
+ () => void 0
20709
+ );
20710
+ return result;
20711
+ };
20712
+ const handle = {
20713
+ get id() {
20714
+ return current.id;
20715
+ },
20716
+ get ordinal() {
20717
+ return current.ordinal;
20718
+ },
20719
+ get revision() {
20720
+ return current.revision;
20721
+ },
20722
+ update(text, options = {}) {
20723
+ const reservedOperationId = options.operationId || operationId("feed_transient_update");
20724
+ const reservedOptions = { ...options };
20725
+ return enqueueMutation(async () => {
20726
+ const response = await publish("feed.transient.update", {
20727
+ transientId: current.id,
20728
+ expectedRevision: current.revision,
20729
+ text,
20730
+ ...reservedOptions,
20731
+ operationId: reservedOperationId
20732
+ });
20733
+ current = requireTransientFeedbackItem(response);
20734
+ return handle;
20735
+ });
20736
+ },
20737
+ settle(text, options = {}) {
20738
+ const reservedOperationId = options.operationId || operationId("feed_transient_settle");
20739
+ const reservedOptions = { ...options };
20740
+ return enqueueMutation(async () => {
20741
+ const response = await publish("feed.transient.settle", {
20742
+ transientId: current.id,
20743
+ expectedRevision: current.revision,
20744
+ ...text === void 0 ? {} : { text },
20745
+ ...reservedOptions,
20746
+ operationId: reservedOperationId
20747
+ });
20748
+ const responseRecord = asRecord(response);
20749
+ const durable = asRecord(responseRecord?.durable);
20750
+ const durableItems = Array.isArray(durable?.items) ? durable.items : [];
20751
+ const rawItem = responseRecord?.item ?? responseRecord?.durableItem ?? durableItems[0];
20752
+ if (rawItem === null || rawItem === void 0) {
20753
+ return null;
20754
+ }
20755
+ return requireFeedbackItem(rawItem);
20756
+ });
20757
+ }
20758
+ };
20759
+ return handle;
20760
+ };
20761
+ return {
20762
+ async feedback(text, options = {}) {
20763
+ return requireFeedbackItem(
20764
+ await publish("feed.feedback", {
20765
+ text,
20766
+ ...options,
20767
+ operationId: options.operationId || operationId("feed_feedback")
20768
+ })
20769
+ );
20770
+ },
20771
+ async transientFeedback(text, options = {}) {
20772
+ const item = requireTransientFeedbackItem(
20773
+ await publish("feed.transient.create", {
20774
+ text,
20775
+ ...options,
20776
+ operationId: options.operationId || operationId("feed_transient_create")
20777
+ })
20778
+ );
20779
+ return makeHandle(item);
20780
+ }
20781
+ };
20782
+ }
20783
+
20784
+ // src/ws-client.ts
20785
+ var GlobalWebSocket = void 0;
20786
+ if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
20787
+ GlobalWebSocket = globalThis.WebSocket;
20788
+ }
20789
+ var READY_STATE_OPEN = 1;
20790
+ var TOKEN_REFRESH_LEEWAY_MS = 2 * 60 * 1e3;
20791
+ var TOKEN_REFRESH_RETRY_MS = 30 * 1e3;
20792
+ var MAX_TIMER_DELAY_MS = 2147483647;
20793
+ var DEBUG_WS = process.env.GRANULAR_DEBUG_WS === "1";
20794
+ var DEFAULT_RPC_TIMEOUT_MS = 3e4;
20795
+ var DOMAIN_PACKAGE_RPC_TIMEOUT_MS = 12e4;
20796
+ var EFFECT_CONTROL_RPC_TIMEOUT_MS = 12e4;
20797
+ var HARNESS_RUN_RPC_TIMEOUT_MS = 6e5;
20798
+ var HEARTBEAT_RPC_TIMEOUT_MS = 15e3;
20799
+ var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
20800
+ var DEFAULT_RECONNECT_DELAY_MS = 3e3;
20801
+ var DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
20802
+ var DOCUMENT_RESYNC_CLOSE_CODE = 4e3;
20803
+ var DOCUMENT_RESYNC_CLOSE_REASON = "Document resync required";
20804
+ function documentVersion(document) {
20805
+ const record = document;
20806
+ const epochValue = record.documentEpoch;
20807
+ const revisionValue = record.documentRevision;
20808
+ const epoch = epochValue === void 0 ? 0 : epochValue;
20809
+ const revision = revisionValue === void 0 ? 0 : revisionValue;
20810
+ if (!Number.isSafeInteger(epoch) || Number(epoch) < 0 || !Number.isSafeInteger(revision) || Number(revision) < 0) {
20811
+ throw new Error("Session document version metadata is invalid");
20812
+ }
20813
+ return {
20814
+ epoch: Number(epoch),
20815
+ revision: Number(revision)
20816
+ };
20817
+ }
20818
+ function stableDocumentFingerprint(value) {
20819
+ if (value === null) return "null";
20820
+ if (value === void 0) return '"[undefined]"';
20821
+ if (typeof value !== "object") return JSON.stringify(value) ?? String(value);
20822
+ if (value instanceof Date) return `date:${value.toISOString()}`;
20823
+ if (value instanceof Uint8Array) {
20824
+ return `bytes:${Array.from(value).join(",")}`;
20825
+ }
20826
+ if (Array.isArray(value)) {
20827
+ return `[${value.map(stableDocumentFingerprint).join(",")}]`;
20828
+ }
20829
+ const record = value;
20830
+ return `{${Object.keys(record).sort().map(
20831
+ (key) => `${JSON.stringify(key)}:${stableDocumentFingerprint(record[key])}`
20832
+ ).join(",")}}`;
20833
+ }
20834
+ function debugWs(...args) {
20835
+ if (DEBUG_WS) {
20836
+ console.log(...args);
20837
+ }
20838
+ }
20839
+ function rpcTimeoutMsForMethod(method) {
20840
+ switch (method) {
20841
+ case "domain.fetchPackagePart":
20842
+ case "domain.getSummary":
20843
+ return DOMAIN_PACKAGE_RPC_TIMEOUT_MS;
20844
+ case "client.heartbeat":
20845
+ return HEARTBEAT_RPC_TIMEOUT_MS;
20846
+ case "effects.publishCatalog":
20847
+ case "effects.resetCatalog":
20848
+ case "effects.addCatalog":
20849
+ case "effects.removeCatalog":
20850
+ case "effects.refresh":
20851
+ case "harness.prepare":
20852
+ return EFFECT_CONTROL_RPC_TIMEOUT_MS;
20853
+ case "harness.run":
20854
+ return HARNESS_RUN_RPC_TIMEOUT_MS;
20855
+ default:
20856
+ return DEFAULT_RPC_TIMEOUT_MS;
20857
+ }
20858
+ }
20859
+ var WSClient = class {
20860
+ ws = null;
20861
+ url;
20862
+ sessionId;
20863
+ token;
20864
+ messageQueue = [];
20865
+ rpcHandlers = /* @__PURE__ */ new Map();
20866
+ eventHandlers = /* @__PURE__ */ new Map();
20867
+ canonicalDocumentQuarantineHandlers = /* @__PURE__ */ new Set();
20868
+ nextRpcId = 1;
20869
+ doc = Automerge__namespace.init();
20870
+ reconnectTimer = null;
20871
+ tokenRefreshTimer = null;
20872
+ isExplicitlyDisconnected = false;
20873
+ reconnectAttempts = 0;
20874
+ connectPromise = null;
20875
+ connectionEpoch = 0;
20876
+ cancelConnectAttempt = null;
20877
+ documentRepairRequired = false;
20878
+ canonicalDocumentActivated = false;
20879
+ options;
20880
+ constructor(options) {
20881
+ this.options = options;
20882
+ this.url = options.url;
20883
+ this.sessionId = options.sessionId;
20884
+ this.token = options.token;
20885
+ if (options.initialDocumentSnapshot) {
20886
+ this.seedDocumentSnapshot(options.initialDocumentSnapshot);
20887
+ }
20888
+ }
20889
+ get currentSessionId() {
20890
+ return this.sessionId;
20891
+ }
20892
+ seedDocumentSnapshot(document) {
20893
+ if (!document || typeof document !== "object" || Array.isArray(document)) {
20894
+ return;
20895
+ }
20896
+ try {
20897
+ const replacement = document instanceof Uint8Array ? Automerge__namespace.load(document) : Automerge__namespace.from(document);
20898
+ if (!this.canAcceptDocumentReplacement(replacement, false)) {
20899
+ return;
20900
+ }
20901
+ this.doc = replacement;
20902
+ this.rememberCanonicalActivation(replacement);
20903
+ this.emit("sync", this.doc);
20904
+ } catch (error2) {
20905
+ console.warn("[Granular] Failed to seed cached session document", error2);
20906
+ }
20907
+ }
20908
+ saveDocumentSnapshot() {
20909
+ return Automerge__namespace.save(this.doc);
20910
+ }
20911
+ clearTokenRefreshTimer() {
20912
+ if (this.tokenRefreshTimer) {
20913
+ clearTimeout(this.tokenRefreshTimer);
20914
+ this.tokenRefreshTimer = null;
20915
+ }
20916
+ }
20917
+ decodeBase64Url(payload) {
20918
+ const base64 = payload.replace(/-/g, "+").replace(/_/g, "/");
20919
+ const padded = base64 + "=".repeat((4 - (base64.length % 4 || 4)) % 4);
20920
+ if (typeof atob === "function") {
20921
+ return atob(padded);
20922
+ }
20923
+ const maybeBuffer = globalThis.Buffer;
20924
+ if (maybeBuffer) {
20925
+ return maybeBuffer.from(padded, "base64").toString("utf8");
20926
+ }
20927
+ throw new Error("No base64 decoder available");
20928
+ }
20929
+ getTokenExpiryMs(token) {
20930
+ const parts = token.split(".");
20931
+ if (parts.length < 2) {
20932
+ return null;
20933
+ }
20934
+ try {
20935
+ const payloadSegment = parts[1];
20936
+ if (!payloadSegment) {
20937
+ return null;
20938
+ }
20939
+ const payloadRaw = this.decodeBase64Url(payloadSegment);
20940
+ const payload = JSON.parse(payloadRaw);
20941
+ if (typeof payload.exp !== "number" || !Number.isFinite(payload.exp)) {
20942
+ return null;
20943
+ }
20944
+ return payload.exp * 1e3;
20945
+ } catch {
20946
+ return null;
20947
+ }
20948
+ }
20949
+ scheduleTokenRefresh() {
20950
+ this.clearTokenRefreshTimer();
20951
+ if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
20952
+ return;
20953
+ }
20954
+ const expiresAt = this.getTokenExpiryMs(this.token);
20955
+ if (!expiresAt) {
20956
+ return;
20957
+ }
20958
+ const refreshInMs = Math.max(
20959
+ 1e3,
20960
+ expiresAt - Date.now() - TOKEN_REFRESH_LEEWAY_MS
20961
+ );
20962
+ const delay = Math.min(refreshInMs, MAX_TIMER_DELAY_MS);
20963
+ this.tokenRefreshTimer = setTimeout(() => {
20964
+ void this.refreshTokenInBackground();
20965
+ }, delay);
20966
+ }
20967
+ async refreshTokenInBackground() {
20968
+ if (!this.options.tokenProvider || this.isExplicitlyDisconnected) {
20969
+ return;
20970
+ }
20971
+ try {
20972
+ const refreshedToken = await this.options.tokenProvider();
20973
+ if (typeof refreshedToken !== "string" || refreshedToken.length === 0) {
20974
+ throw new Error("Token provider returned no token");
20975
+ }
20976
+ this.token = refreshedToken;
20977
+ this.scheduleTokenRefresh();
20978
+ } catch (error2) {
20979
+ if (this.isExplicitlyDisconnected) {
20980
+ return;
20981
+ }
20982
+ console.warn("[Granular] Token refresh failed, retrying soon:", error2);
20983
+ this.clearTokenRefreshTimer();
20984
+ this.tokenRefreshTimer = setTimeout(() => {
20985
+ void this.refreshTokenInBackground();
20986
+ }, TOKEN_REFRESH_RETRY_MS);
19690
20987
  }
19691
20988
  }
19692
20989
  async resolveTokenForConnect() {
@@ -19733,6 +21030,26 @@ var WSClient = class {
19733
21030
  }
19734
21031
  }
19735
21032
  }
21033
+ /**
21034
+ * Mark the current transport as unusable and schedule the normal reconnect
21035
+ * path. Browser WebSockets can remain in OPEN state after a proxy/worker
21036
+ * restart, so a timed-out heartbeat must revoke the stale socket explicitly.
21037
+ */
21038
+ reportTransportFailure(reason = "WebSocket transport failed") {
21039
+ if (this.isExplicitlyDisconnected) return;
21040
+ const socket = this.ws;
21041
+ this.connectionEpoch += 1;
21042
+ this.ws = null;
21043
+ try {
21044
+ socket?.close(4001, "Transport failure");
21045
+ } catch {
21046
+ }
21047
+ this.handleDisconnect({
21048
+ code: 4001,
21049
+ reason: reason instanceof Error ? reason.message : String(reason),
21050
+ wasClean: false
21051
+ });
21052
+ }
19736
21053
  async connectAttempt(signal) {
19737
21054
  if (signal?.aborted) throw new Error("WebSocket connect aborted");
19738
21055
  const token = await this.resolveTokenForConnect();
@@ -19764,10 +21081,17 @@ var WSClient = class {
19764
21081
  this.ws = socket;
19765
21082
  return new Promise((resolve2, reject) => {
19766
21083
  let settled = false;
21084
+ const configuredConnectTimeoutMs = this.options.connectTimeoutMs;
21085
+ const connectTimeoutMs = typeof configuredConnectTimeoutMs === "number" && Number.isFinite(configuredConnectTimeoutMs) && configuredConnectTimeoutMs > 0 ? configuredConnectTimeoutMs : DEFAULT_CONNECT_TIMEOUT_MS;
21086
+ let connectTimeout = null;
19767
21087
  const isCurrent = () => this.connectionEpoch === epoch && this.ws === socket;
19768
21088
  const finish = (error2) => {
19769
21089
  if (settled) return;
19770
21090
  settled = true;
21091
+ if (connectTimeout) {
21092
+ clearTimeout(connectTimeout);
21093
+ connectTimeout = null;
21094
+ }
19771
21095
  if (this.cancelConnectAttempt === handleAbort) {
19772
21096
  this.cancelConnectAttempt = null;
19773
21097
  }
@@ -19832,6 +21156,17 @@ var WSClient = class {
19832
21156
  wasClean: close.wasClean
19833
21157
  });
19834
21158
  };
21159
+ connectTimeout = setTimeout(() => {
21160
+ if (!isCurrent()) return;
21161
+ this.connectionEpoch += 1;
21162
+ this.ws = null;
21163
+ closeStaleSocket();
21164
+ finish(
21165
+ new Error(
21166
+ `WebSocket connect timed out after ${connectTimeoutMs}ms`
21167
+ )
21168
+ );
21169
+ }, connectTimeoutMs);
19835
21170
  signal?.addEventListener("abort", handleAbort, { once: true });
19836
21171
  const nodeSocket = socket;
19837
21172
  if (typeof nodeSocket.on === "function") {
@@ -19870,11 +21205,12 @@ var WSClient = class {
19870
21205
  });
19871
21206
  this.messageQueue = [];
19872
21207
  }
19873
- emitReconnectErrorMessage(error2) {
21208
+ emitReconnectErrorMessage(error2, terminal2 = false) {
19874
21209
  const reconnectInfo = {
19875
21210
  error: error2,
19876
21211
  sessionId: this.sessionId,
19877
- timestamp: Date.now()
21212
+ timestamp: Date.now(),
21213
+ terminal: terminal2
19878
21214
  };
19879
21215
  this.emit("reconnect_error", reconnectInfo);
19880
21216
  if (this.options.onReconnectError) {
@@ -19894,7 +21230,8 @@ var WSClient = class {
19894
21230
  const maxReconnectAttempts = typeof this.options.maxReconnectAttempts === "number" && Number.isFinite(this.options.maxReconnectAttempts) && this.options.maxReconnectAttempts >= 0 ? Math.floor(this.options.maxReconnectAttempts) : DEFAULT_MAX_RECONNECT_ATTEMPTS;
19895
21231
  if (this.reconnectAttempts >= maxReconnectAttempts) {
19896
21232
  this.emitReconnectErrorMessage(
19897
- `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`
21233
+ `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`,
21234
+ true
19898
21235
  );
19899
21236
  return null;
19900
21237
  }
@@ -19924,6 +21261,190 @@ var WSClient = class {
19924
21261
  const suffix = details ? ` (${details})` : "";
19925
21262
  return new Error(`WebSocket disconnected${suffix}`);
19926
21263
  }
21264
+ decodeDocumentBytes(payload, envelopeType) {
21265
+ if (typeof payload === "string") {
21266
+ const binaryString = atob(payload);
21267
+ const bytes = new Uint8Array(binaryString.length);
21268
+ for (let index = 0; index < binaryString.length; index += 1) {
21269
+ bytes[index] = binaryString.charCodeAt(index);
21270
+ }
21271
+ return bytes;
21272
+ }
21273
+ if (payload instanceof Uint8Array) {
21274
+ return payload;
21275
+ }
21276
+ if (Array.isArray(payload) && payload.every(
21277
+ (value) => typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 255
21278
+ )) {
21279
+ return new Uint8Array(payload);
21280
+ }
21281
+ throw new Error(`${envelopeType} payload is not valid byte data`);
21282
+ }
21283
+ rememberCanonicalActivation(document) {
21284
+ if (isCanonicalSessionFeedDocument(document)) {
21285
+ this.canonicalDocumentActivated = true;
21286
+ }
21287
+ }
21288
+ canAcceptDocumentReplacement(replacement, strictlyNewer) {
21289
+ const replacementCanonical = isCanonicalSessionFeedDocument(replacement);
21290
+ const currentCanonical = isCanonicalSessionFeedDocument(this.doc);
21291
+ if (replacementCanonical) {
21292
+ this.canonicalDocumentActivated = true;
21293
+ }
21294
+ if (this.canonicalDocumentActivated && !replacementCanonical) {
21295
+ debugWs(
21296
+ "[Granular DEBUG] Rejected session document replacement that would deactivate the canonical feed."
21297
+ );
21298
+ return false;
21299
+ }
21300
+ const current = documentVersion(this.doc);
21301
+ const incoming = documentVersion(replacement);
21302
+ if (incoming.epoch < current.epoch) {
21303
+ debugWs(
21304
+ `[Granular DEBUG] Rejected stale session document epoch ${incoming.epoch}; current epoch is ${current.epoch}.`
21305
+ );
21306
+ return false;
21307
+ }
21308
+ if (incoming.epoch === current.epoch) {
21309
+ const minimumRevision = strictlyNewer ? current.revision + 1 : current.revision;
21310
+ if (incoming.revision < minimumRevision) {
21311
+ debugWs(
21312
+ `[Granular DEBUG] Rejected stale session document revision ${incoming.revision}; current revision is ${current.revision}.`
21313
+ );
21314
+ return false;
21315
+ }
21316
+ }
21317
+ if (replacementCanonical) {
21318
+ const incomingSnapshot = readSessionFeedSnapshot(replacement);
21319
+ if (incomingSnapshot.error) {
21320
+ this.documentRepairRequired = true;
21321
+ this.exposeCanonicalQuarantine(
21322
+ replacement,
21323
+ currentCanonical,
21324
+ incomingSnapshot.error
21325
+ );
21326
+ debugWs(
21327
+ `[Granular DEBUG] Rejected malformed canonical snapshot: ${incomingSnapshot.error.message}`
21328
+ );
21329
+ return false;
21330
+ }
21331
+ }
21332
+ const canonicalError = this.canonicalReplacementError(replacement);
21333
+ if (canonicalError) {
21334
+ debugWs(`[Granular DEBUG] Rejected session snapshot: ${canonicalError}`);
21335
+ return false;
21336
+ }
21337
+ if (this.canonicalDocumentActivated && incoming.epoch === current.epoch && incoming.revision === current.revision && !readSessionFeedSnapshot(this.doc).error && stableDocumentFingerprint(Automerge__namespace.toJS(replacement)) !== stableDocumentFingerprint(Automerge__namespace.toJS(this.doc))) {
21338
+ debugWs(
21339
+ "[Granular DEBUG] Rejected divergent canonical snapshot at the accepted document version."
21340
+ );
21341
+ return false;
21342
+ }
21343
+ return true;
21344
+ }
21345
+ exposeCanonicalQuarantine(replacement, currentCanonical, error2) {
21346
+ const version2 = documentVersion(replacement);
21347
+ if (currentCanonical) {
21348
+ const quarantine2 = Object.freeze({
21349
+ documentEpoch: version2.epoch,
21350
+ documentRevision: version2.revision,
21351
+ error: new Error(error2.message)
21352
+ });
21353
+ for (const handler of this.canonicalDocumentQuarantineHandlers) {
21354
+ handler(quarantine2);
21355
+ }
21356
+ return;
21357
+ }
21358
+ const accepted = Automerge__namespace.toJS(this.doc);
21359
+ const quarantine = Automerge__namespace.from({
21360
+ ...accepted,
21361
+ documentEpoch: version2.epoch,
21362
+ documentRevision: version2.revision,
21363
+ feed: {
21364
+ activation: { mode: "canonical" }
21365
+ }
21366
+ });
21367
+ this.doc = quarantine;
21368
+ this.emit("sync", this.doc);
21369
+ debugWs(
21370
+ `[Granular DEBUG] Canonical activation quarantined pending repair: ${error2.message}`
21371
+ );
21372
+ }
21373
+ canonicalReplacementError(replacement) {
21374
+ if (!isCanonicalSessionFeedDocument(replacement)) return null;
21375
+ const incoming = readSessionFeedSnapshot(replacement);
21376
+ if (incoming.error) {
21377
+ return `canonical feed is invalid: ${incoming.error.message}`;
21378
+ }
21379
+ if (!this.canonicalDocumentActivated) return null;
21380
+ const current = readSessionFeedSnapshot(this.doc);
21381
+ if (current.error) {
21382
+ return null;
21383
+ }
21384
+ if (incoming.lastSequence < current.lastSequence) {
21385
+ return `canonical lastSequence ${incoming.lastSequence} regresses accepted ${current.lastSequence}`;
21386
+ }
21387
+ const currentBySequence = new Map(
21388
+ current.tail.map((item) => [item.sequence, item])
21389
+ );
21390
+ for (const item of incoming.tail) {
21391
+ const accepted = currentBySequence.get(item.sequence);
21392
+ if (accepted && stableDocumentFingerprint(item) !== stableDocumentFingerprint(accepted)) {
21393
+ return `canonical occurrence ${item.sequence} conflicts with accepted history`;
21394
+ }
21395
+ }
21396
+ return null;
21397
+ }
21398
+ assertIncrementalDocumentIsSafe(replacement) {
21399
+ const missingDependencies = Automerge__namespace.getMissingDeps(replacement, []);
21400
+ if (missingDependencies.length > 0) {
21401
+ throw new Error(
21402
+ `Incremental session update is missing ${missingDependencies.length} causal dependency/dependencies`
21403
+ );
21404
+ }
21405
+ if (this.canonicalDocumentActivated && !isCanonicalSessionFeedDocument(replacement)) {
21406
+ throw new Error(
21407
+ "Incremental session update would deactivate the canonical feed"
21408
+ );
21409
+ }
21410
+ const current = documentVersion(this.doc);
21411
+ const incoming = documentVersion(replacement);
21412
+ if (incoming.epoch < current.epoch || incoming.epoch === current.epoch && incoming.revision < current.revision) {
21413
+ throw new Error("Incremental session update regressed document version");
21414
+ }
21415
+ const canonicalError = this.canonicalReplacementError(replacement);
21416
+ if (canonicalError) {
21417
+ throw new Error(canonicalError);
21418
+ }
21419
+ if (this.canonicalDocumentActivated && incoming.epoch === current.epoch && incoming.revision === current.revision && stableDocumentFingerprint(Automerge__namespace.toJS(replacement)) !== stableDocumentFingerprint(Automerge__namespace.toJS(this.doc))) {
21420
+ throw new Error(
21421
+ "Incremental session update diverged without advancing document revision"
21422
+ );
21423
+ }
21424
+ }
21425
+ requireDocumentResync(envelopeType, error2) {
21426
+ this.documentRepairRequired = true;
21427
+ console.warn(
21428
+ `[Granular] ${envelopeType} could not be applied; reconnecting for a fresh session snapshot.`,
21429
+ error2
21430
+ );
21431
+ const socket = this.ws;
21432
+ if (!socket) {
21433
+ this.scheduleReconnectAttempt();
21434
+ return;
21435
+ }
21436
+ this.connectionEpoch += 1;
21437
+ this.ws = null;
21438
+ try {
21439
+ socket.close(DOCUMENT_RESYNC_CLOSE_CODE, DOCUMENT_RESYNC_CLOSE_REASON);
21440
+ } catch {
21441
+ }
21442
+ this.handleDisconnect({
21443
+ code: DOCUMENT_RESYNC_CLOSE_CODE,
21444
+ reason: DOCUMENT_RESYNC_CLOSE_REASON,
21445
+ wasClean: false
21446
+ });
21447
+ }
19927
21448
  handleDisconnect(close = {}) {
19928
21449
  const unexpected = !this.isExplicitlyDisconnected;
19929
21450
  const info2 = {
@@ -19942,11 +21463,11 @@ var WSClient = class {
19942
21463
  }
19943
21464
  if (unexpected) {
19944
21465
  const disconnectError = this.buildDisconnectError(info2);
19945
- this.rejectPending(disconnectError);
19946
- this.emit("disconnect", info2);
19947
21466
  const reconnectDelayMs = this.scheduleReconnectAttempt();
19948
21467
  info2.reconnectScheduled = reconnectDelayMs !== null;
19949
21468
  if (reconnectDelayMs !== null) info2.reconnectDelayMs = reconnectDelayMs;
21469
+ this.rejectPending(disconnectError);
21470
+ this.emit("disconnect", info2);
19950
21471
  if (this.options.onUnexpectedClose) {
19951
21472
  try {
19952
21473
  this.options.onUnexpectedClose(info2);
@@ -19966,100 +21487,109 @@ var WSClient = class {
19966
21487
  JSON.stringify(message).slice(0, 500)
19967
21488
  );
19968
21489
  if ("type" in message && message.type === "sync") {
19969
- const syncMessage = message;
19970
- let bytes;
21490
+ this.requireDocumentResync(
21491
+ "Unsupported Automerge sync envelope",
21492
+ new Error("Use snapshot, snapshot_reset, or change.")
21493
+ );
21494
+ return;
21495
+ }
21496
+ if ("type" in message && message.type === "snapshot_reset") {
21497
+ const resetMessage = message;
19971
21498
  try {
19972
- const payload = syncMessage.message || syncMessage.data;
19973
- if (typeof payload === "string") {
19974
- const binaryString = atob(payload);
19975
- const len = binaryString.length;
19976
- bytes = new Uint8Array(len);
19977
- for (let i = 0; i < len; i++) {
19978
- bytes[i] = binaryString.charCodeAt(i);
19979
- }
19980
- } else if (Array.isArray(payload)) {
19981
- bytes = new Uint8Array(payload);
19982
- } else if (payload instanceof Uint8Array) {
19983
- bytes = payload;
19984
- } else {
19985
- return;
21499
+ if (!Number.isSafeInteger(resetMessage.documentEpoch) || resetMessage.documentEpoch <= 0 || !Number.isSafeInteger(resetMessage.documentRevision) || resetMessage.documentRevision < 0 || !Array.isArray(resetMessage.data)) {
21500
+ throw new Error("snapshot_reset metadata is invalid");
19986
21501
  }
19987
- debugWs("[Granular DEBUG] Applying sync bytes:", bytes.length);
19988
- const [newDoc, newSyncState] = Automerge__namespace.receiveSyncMessage(
19989
- this.doc,
19990
- this.syncState,
19991
- bytes
21502
+ const replacement = Automerge__namespace.load(
21503
+ this.decodeDocumentBytes(
21504
+ resetMessage.data,
21505
+ "Automerge snapshot_reset"
21506
+ )
19992
21507
  );
19993
- this.doc = newDoc;
19994
- this.syncState = newSyncState;
19995
- const docAny = this.doc;
19996
- if (docAny.catalog) {
19997
- debugWs(
19998
- "[Granular DEBUG] Doc catalog sync applied. Keys in catalog:",
19999
- Object.keys(docAny.catalog || {})
20000
- );
20001
- debugWs(
20002
- "[Granular DEBUG] RawToolCatalogs:",
20003
- Object.keys(docAny.catalog.rawToolCatalogs || {})
20004
- );
20005
- } else {
20006
- debugWs(
20007
- "[Granular DEBUG] Doc synced but no catalog yet. Keys in doc:",
20008
- Object.keys(docAny)
21508
+ const replacementVersion = documentVersion(replacement);
21509
+ const replacementEpoch = replacementVersion.epoch;
21510
+ const replacementRevision = replacementVersion.revision;
21511
+ if (replacementEpoch !== resetMessage.documentEpoch || replacementRevision !== resetMessage.documentRevision) {
21512
+ throw new Error(
21513
+ "snapshot_reset metadata does not match the saved document"
20009
21514
  );
20010
21515
  }
20011
- this.emit("sync", this.doc);
20012
- } catch (e) {
20013
- try {
20014
- debugWs(
20015
- "[Granular DEBUG] receiveSyncMessage failed, trying applyChanges..."
20016
- );
20017
- const [newDoc] = Automerge__namespace.applyChanges(this.doc, [bytes]);
20018
- this.doc = newDoc;
20019
- this.emit("sync", this.doc);
20020
- debugWs(
20021
- "[Granular DEBUG] applyChanges succeeded. Doc:",
20022
- JSON.stringify(Automerge__namespace.toJS(this.doc))
20023
- );
20024
- } catch (applyError) {
20025
- console.warn(
20026
- "[Granular] Failed to apply sync message (both sync & applyChanges)",
20027
- e,
20028
- applyError
20029
- );
21516
+ if (!this.canAcceptDocumentReplacement(replacement, true)) {
21517
+ if (this.documentRepairRequired) {
21518
+ this.requireDocumentResync(
21519
+ "Stale Automerge snapshot_reset during document repair",
21520
+ new Error("Replacement reset did not advance accepted state")
21521
+ );
21522
+ }
21523
+ return;
20030
21524
  }
21525
+ this.doc = replacement;
21526
+ this.documentRepairRequired = false;
21527
+ this.rememberCanonicalActivation(replacement);
21528
+ this.emit("snapshot_reset", {
21529
+ documentEpoch: replacementEpoch,
21530
+ documentRevision: replacementRevision
21531
+ });
21532
+ this.emit("sync", this.doc);
21533
+ } catch (error2) {
21534
+ this.requireDocumentResync("Automerge snapshot_reset", error2);
20031
21535
  }
20032
21536
  return;
20033
21537
  }
20034
21538
  if ("type" in message && message.type === "snapshot") {
20035
21539
  const snapshotMessage = message;
20036
21540
  try {
20037
- const bytes = new Uint8Array(snapshotMessage.data);
21541
+ const bytes = this.decodeDocumentBytes(
21542
+ snapshotMessage.data,
21543
+ "Automerge snapshot"
21544
+ );
20038
21545
  debugWs(
20039
21546
  "[Granular DEBUG] Loading Automerge session snapshot bytes:",
20040
21547
  bytes.length
20041
21548
  );
20042
- this.doc = Automerge__namespace.load(bytes);
21549
+ const replacement = Automerge__namespace.load(bytes);
21550
+ if (!this.canAcceptDocumentReplacement(replacement, false)) {
21551
+ if (this.documentRepairRequired) {
21552
+ this.requireDocumentResync(
21553
+ "Stale Automerge snapshot during document repair",
21554
+ new Error("Replacement snapshot regressed accepted state")
21555
+ );
21556
+ }
21557
+ return;
21558
+ }
21559
+ this.doc = replacement;
21560
+ this.documentRepairRequired = false;
21561
+ this.rememberCanonicalActivation(replacement);
20043
21562
  this.emit("sync", this.doc);
20044
21563
  debugWs(
20045
21564
  "[Granular DEBUG] Automerge session snapshot loaded. Doc:",
20046
21565
  JSON.stringify(Automerge__namespace.toJS(this.doc))
20047
21566
  );
20048
- } catch (e) {
20049
- console.warn("[Granular] Failed to load snapshot message", e);
21567
+ } catch (error2) {
21568
+ this.requireDocumentResync("Automerge snapshot", error2);
20050
21569
  }
20051
21570
  return;
20052
21571
  }
20053
21572
  if ("type" in message && message.type === "change") {
21573
+ if (this.documentRepairRequired) {
21574
+ debugWs(
21575
+ "[Granular DEBUG] Ignoring raw change while a replacement snapshot is required."
21576
+ );
21577
+ return;
21578
+ }
20054
21579
  const changeMessage = message;
20055
21580
  try {
20056
- const bytes = new Uint8Array(changeMessage.data);
21581
+ const bytes = this.decodeDocumentBytes(
21582
+ changeMessage.data,
21583
+ "Automerge change"
21584
+ );
20057
21585
  const [newDoc] = Automerge__namespace.applyChanges(this.doc, [bytes]);
21586
+ this.assertIncrementalDocumentIsSafe(newDoc);
20058
21587
  this.doc = newDoc;
21588
+ this.rememberCanonicalActivation(newDoc);
20059
21589
  this.emit("change", changeMessage);
20060
21590
  this.emit("sync", this.doc);
20061
- } catch (e) {
20062
- console.warn("[Granular] Failed to apply change message", e);
21591
+ } catch (error2) {
21592
+ this.requireDocumentResync("Automerge change message", error2);
20063
21593
  }
20064
21594
  return;
20065
21595
  }
@@ -20178,6 +21708,18 @@ var WSClient = class {
20178
21708
  }
20179
21709
  this.eventHandlers.get(event).push(handler);
20180
21710
  }
21711
+ /**
21712
+ * Subscribe to rejected canonical replacement metadata without exposing the
21713
+ * malformed document through the public sync stream.
21714
+ *
21715
+ * @internal Session uses this to quarantine only its feed projection.
21716
+ */
21717
+ onCanonicalDocumentQuarantine(handler) {
21718
+ this.canonicalDocumentQuarantineHandlers.add(handler);
21719
+ return () => {
21720
+ this.canonicalDocumentQuarantineHandlers.delete(handler);
21721
+ };
21722
+ }
20181
21723
  /**
20182
21724
  * Register an RPC handler for incoming server requests
20183
21725
  * @param {string} method - RPC method name
@@ -20235,7 +21777,7 @@ var WSClient = class {
20235
21777
  };
20236
21778
 
20237
21779
  // src/prompt-utils.ts
20238
- function asRecord(value) {
21780
+ function asRecord2(value) {
20239
21781
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
20240
21782
  return value;
20241
21783
  }
@@ -20250,7 +21792,7 @@ function parseJsonPromptChoiceOption(option) {
20250
21792
  if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
20251
21793
  try {
20252
21794
  const parsed = JSON.parse(trimmed);
20253
- return asRecord(parsed);
21795
+ return asRecord2(parsed);
20254
21796
  } catch {
20255
21797
  return null;
20256
21798
  }
@@ -20312,33 +21854,37 @@ function normalizePromptType(raw) {
20312
21854
  if (kind === "confirm" || kind === "choice" || kind === "input") return kind;
20313
21855
  if (promptType === "confirm" || promptType === "choice" || promptType === "input")
20314
21856
  return promptType;
20315
- return "input";
21857
+ return null;
20316
21858
  }
20317
21859
  function normalizePrompt(rawValue) {
20318
- const raw = asRecord(rawValue);
21860
+ const raw = asRecord2(rawValue);
20319
21861
  if (!raw) return null;
20320
- const promptRecord = asRecord(raw.prompt);
21862
+ const promptRecord = asRecord2(raw.prompt);
20321
21863
  const source = promptRecord || raw;
20322
21864
  const id = typeof source.id === "string" ? source.id : typeof raw.id === "string" ? raw.id : typeof raw.promptId === "string" ? raw.promptId : "";
20323
21865
  if (!id) return null;
20324
21866
  const jobId = typeof source.jobId === "string" && source.jobId.trim() ? source.jobId.trim() : typeof raw.jobId === "string" && raw.jobId.trim() ? raw.jobId.trim() : void 0;
20325
21867
  const turnId = typeof source.turnId === "string" && source.turnId.trim() ? source.turnId.trim() : typeof raw.turnId === "string" && raw.turnId.trim() ? raw.turnId.trim() : void 0;
21868
+ const type = normalizePromptType(
21869
+ source === raw ? raw : { ...raw, ...source }
21870
+ );
21871
+ if (!type) return null;
20326
21872
  return {
20327
21873
  id,
20328
21874
  ...jobId ? { jobId } : {},
20329
21875
  ...turnId ? { turnId } : {},
20330
- type: normalizePromptType(source === raw ? raw : { ...raw, ...source }),
21876
+ type,
20331
21877
  title: typeof source.title === "string" ? source.title : "Input required",
20332
21878
  message: typeof source.message === "string" ? source.message : "",
20333
21879
  options: Array.isArray(source.options) ? source.options.map(
20334
- (option) => typeof option === "string" || asRecord(option) ? normalizePromptChoiceOption(
21880
+ (option) => typeof option === "string" || asRecord2(option) ? normalizePromptChoiceOption(
20335
21881
  option
20336
21882
  ) : option
20337
21883
  ) : void 0,
20338
21884
  defaultValue: source.defaultValue,
20339
21885
  placeholder: typeof source.placeholder === "string" ? source.placeholder : void 0,
20340
21886
  allowEmpty: typeof source.allowEmpty === "boolean" ? source.allowEmpty : void 0,
20341
- metadata: asRecord(source.metadata) || void 0
21887
+ metadata: asRecord2(source.metadata) || void 0
20342
21888
  };
20343
21889
  }
20344
21890
  function resolvePromptAnswer(prompt3, answer) {
@@ -20366,23 +21912,36 @@ function resolvePromptAnswer(prompt3, answer) {
20366
21912
  }
20367
21913
 
20368
21914
  // src/session.ts
20369
- var PROMPT_TRANSCRIPT_APPEND_TIMEOUT_MS = 5e3;
20370
21915
  function toPascalCase3(value) {
20371
21916
  return value.split(/[_:\-\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
20372
21917
  }
20373
- function withPromptTranscriptTimeout(promise) {
20374
- let timeout = null;
20375
- return Promise.race([
20376
- promise,
20377
- new Promise((_, reject) => {
20378
- timeout = setTimeout(() => {
20379
- reject(new Error("Timed out appending prompt answer transcript."));
20380
- }, PROMPT_TRANSCRIPT_APPEND_TIMEOUT_MS);
20381
- })
20382
- ]).finally(() => {
20383
- if (timeout) {
20384
- clearTimeout(timeout);
20385
- }
21918
+ function reserveUserMessageId() {
21919
+ const randomUuid = globalThis.crypto?.randomUUID?.();
21920
+ return randomUuid ? `message_${randomUuid}` : `message_${Date.now()}_${Math.random().toString(36).slice(2)}`;
21921
+ }
21922
+ function normalizeUserMessageIdentity(value, field) {
21923
+ if (value === void 0) return void 0;
21924
+ if (typeof value !== "string" || !value.trim()) {
21925
+ throw new Error(`User message ${field} must be a non-empty string.`);
21926
+ }
21927
+ return value.trim();
21928
+ }
21929
+ function recordFromUnknown(value) {
21930
+ return value && typeof value === "object" && !Array.isArray(value) ? value : null;
21931
+ }
21932
+ function promptSnapshotFingerprint(prompt3) {
21933
+ return JSON.stringify({
21934
+ id: prompt3.id,
21935
+ jobId: prompt3.jobId || null,
21936
+ turnId: prompt3.turnId || null,
21937
+ type: prompt3.type,
21938
+ title: prompt3.title,
21939
+ message: prompt3.message,
21940
+ options: prompt3.options || null,
21941
+ defaultValue: prompt3.defaultValue,
21942
+ placeholder: prompt3.placeholder || null,
21943
+ allowEmpty: prompt3.allowEmpty,
21944
+ metadata: prompt3.metadata || null
20386
21945
  });
20387
21946
  }
20388
21947
  var Session = class {
@@ -20390,7 +21949,6 @@ var Session = class {
20390
21949
  clientId;
20391
21950
  initialQuota;
20392
21951
  jobsMap = /* @__PURE__ */ new Map();
20393
- pendingAgentMessagesByJobId = /* @__PURE__ */ new Map();
20394
21952
  eventListeners = /* @__PURE__ */ new Map();
20395
21953
  toolHandlers = /* @__PURE__ */ new Map();
20396
21954
  /** Tracks which tools are instance methods (className set, not static) */
@@ -20407,10 +21965,23 @@ var Session = class {
20407
21965
  domainPackagePartCache = /* @__PURE__ */ new Map();
20408
21966
  domainPackagePartPromises = /* @__PURE__ */ new Map();
20409
21967
  domainPackageFetchQueue = Promise.resolve();
21968
+ feedController;
21969
+ feed;
20410
21970
  constructor(client, clientId, options = {}) {
20411
21971
  this.client = client;
20412
21972
  this.clientId = clientId || `client_${Date.now()}`;
20413
21973
  this.initialQuota = options.initialQuota || null;
21974
+ this.feedController = new SessionFeedController(this.client.doc, {
21975
+ listTransport: (feedOptions) => this.client.call(
21976
+ "feed.list",
21977
+ feedOptions
21978
+ )
21979
+ });
21980
+ this.feed = Object.freeze({
21981
+ getSnapshot: () => this.feedController.getSnapshot(),
21982
+ list: (feedOptions = {}) => this.feedController.list(feedOptions),
21983
+ subscribe: (listener, subscribeOptions = {}) => this.feedController.subscribe(listener, subscribeOptions)
21984
+ });
20414
21985
  this.setupEventHandlers();
20415
21986
  this.setupToolInvokeHandler();
20416
21987
  this.currentDomainRevision = this.extractDomainRevisionFromDoc(
@@ -20429,34 +22000,160 @@ var Session = class {
20429
22000
  }
20430
22001
  return null;
20431
22002
  }
20432
- buildLegacyEffectContext() {
22003
+ /**
22004
+ * Prompt delivery is deliberately event-driven while a socket is live, but
22005
+ * an existing session can be attached from a fresh tab/client after the
22006
+ * event was originally sent. Rebuild that client's prompt cache from the
22007
+ * canonical document on every sync so an open durable prompt remains
22008
+ * actionable after reconnect without manufacturing a second prompt.
22009
+ */
22010
+ reconcilePromptCacheFromCanonicalDocument(doc) {
22011
+ const jobsById = recordFromUnknown(recordFromUnknown(doc)?.jobs)?.byId;
22012
+ const jobs = recordFromUnknown(jobsById) || {};
22013
+ const openPromptIds = /* @__PURE__ */ new Set();
22014
+ for (const [jobId, jobValue] of Object.entries(jobs)) {
22015
+ const prompts = recordFromUnknown(recordFromUnknown(jobValue)?.prompts);
22016
+ if (!prompts) continue;
22017
+ for (const [promptId, promptValue] of Object.entries(prompts)) {
22018
+ const persisted = recordFromUnknown(promptValue);
22019
+ if (!persisted || persisted.status !== "open") continue;
22020
+ const prompt3 = normalizePrompt({
22021
+ promptId,
22022
+ jobId,
22023
+ kind: persisted.kind,
22024
+ type: persisted.type,
22025
+ title: persisted.title,
22026
+ message: persisted.message,
22027
+ options: persisted.options,
22028
+ defaultValue: persisted.defaultValue,
22029
+ placeholder: persisted.placeholder,
22030
+ allowEmpty: persisted.allowEmpty,
22031
+ metadata: persisted.metadata
22032
+ });
22033
+ if (!prompt3) continue;
22034
+ openPromptIds.add(prompt3.id);
22035
+ if (this.hiddenPromptIds.has(prompt3.id)) continue;
22036
+ const previous = this.promptCache.get(prompt3.id);
22037
+ this.promptCache.set(prompt3.id, prompt3);
22038
+ if (!previous || promptSnapshotFingerprint(previous) !== promptSnapshotFingerprint(prompt3)) {
22039
+ this.emit("prompt", prompt3);
22040
+ }
22041
+ }
22042
+ }
22043
+ for (const promptId of this.promptCache.keys()) {
22044
+ if (!openPromptIds.has(promptId)) {
22045
+ this.promptCache.delete(promptId);
22046
+ }
22047
+ }
22048
+ }
22049
+ buildDirectedInvocationEffectContext(params, feedbackContext) {
20433
22050
  return {
20434
22051
  effectClientId: this.clientId,
20435
- sandboxId: "",
20436
- environmentId: "",
20437
- sessionId: "",
22052
+ sandboxId: params.sandboxId || "",
22053
+ environmentId: params.environmentId || "",
22054
+ invocationId: params.callId,
22055
+ jobId: params.jobId,
22056
+ sessionId: params.sessionId || this.client.currentSessionId,
20438
22057
  user: {
20439
22058
  granularId: "",
20440
22059
  userId: "",
20441
22060
  subjectId: ""
20442
- }
22061
+ },
22062
+ ...feedbackContext ? {
22063
+ feedback: feedbackContext.feedback,
22064
+ transientFeedback: feedbackContext.transientFeedback
22065
+ } : {}
20443
22066
  };
20444
22067
  }
20445
- stringifyConversationValue(value) {
20446
- if (typeof value === "string") {
20447
- return value;
20448
- }
20449
- if (typeof value === "boolean") {
20450
- return value ? "Confirmed" : "Canceled";
20451
- }
20452
- if (value === void 0) {
20453
- return "";
20454
- }
20455
- try {
20456
- return JSON.stringify(value, null, 2);
20457
- } catch {
20458
- return String(value);
20459
- }
22068
+ createDirectedInvocationFeedbackContext(params) {
22069
+ if (!params.feedbackCapability) return null;
22070
+ const publications = [];
22071
+ let nextOperationOrdinal = 0;
22072
+ const nextOperationId = (kind) => `${kind}:${nextOperationOrdinal++}`;
22073
+ const track = (publication) => {
22074
+ const tracked = Promise.resolve(publication);
22075
+ publications.push(tracked);
22076
+ void tracked.catch(() => void 0);
22077
+ return tracked;
22078
+ };
22079
+ const methodMap = {
22080
+ "feed.feedback": "tool.feedback",
22081
+ "feed.transient.create": "tool.transient.create",
22082
+ "feed.transient.update": "tool.transient.update",
22083
+ "feed.transient.settle": "tool.transient.settle"
22084
+ };
22085
+ const publisher = createFeedPublisher((method, publishParams) => {
22086
+ const directedFeedbackMethod = methodMap[method];
22087
+ if (!directedFeedbackMethod) {
22088
+ throw new Error(`Unsupported directed feedback method: ${method}`);
22089
+ }
22090
+ return this.client.call(directedFeedbackMethod, {
22091
+ ...publishParams,
22092
+ callId: params.callId,
22093
+ feedbackCapability: params.feedbackCapability
22094
+ });
22095
+ });
22096
+ const wrapTransientHandle = (initial) => {
22097
+ let current = initial;
22098
+ const wrapped = {
22099
+ get id() {
22100
+ return current.id;
22101
+ },
22102
+ get ordinal() {
22103
+ return current.ordinal;
22104
+ },
22105
+ get revision() {
22106
+ return current.revision;
22107
+ },
22108
+ async update(text, options = {}) {
22109
+ current = await track(
22110
+ current.update(text, {
22111
+ ...options,
22112
+ operationId: options.operationId || nextOperationId("transient-update")
22113
+ })
22114
+ );
22115
+ return wrapped;
22116
+ },
22117
+ settle(text, options = {}) {
22118
+ return track(
22119
+ current.settle(text, {
22120
+ ...options,
22121
+ operationId: options.operationId || nextOperationId("transient-settle")
22122
+ })
22123
+ );
22124
+ }
22125
+ };
22126
+ return wrapped;
22127
+ };
22128
+ return {
22129
+ feedback: (text, options = {}) => track(
22130
+ publisher.feedback(text, {
22131
+ ...options,
22132
+ operationId: options.operationId || nextOperationId("feedback")
22133
+ })
22134
+ ),
22135
+ transientFeedback: (text, options = {}) => track(
22136
+ publisher.transientFeedback(text, {
22137
+ ...options,
22138
+ operationId: options.operationId || nextOperationId("transient-create")
22139
+ }).then(wrapTransientHandle)
22140
+ ),
22141
+ async flush() {
22142
+ let cursor = 0;
22143
+ let firstError;
22144
+ while (cursor < publications.length) {
22145
+ const batch = publications.slice(cursor);
22146
+ cursor = publications.length;
22147
+ const results = await Promise.allSettled(batch);
22148
+ for (const result of results) {
22149
+ if (result.status === "rejected" && firstError === void 0) {
22150
+ firstError = result.reason;
22151
+ }
22152
+ }
22153
+ }
22154
+ if (firstError !== void 0) throw firstError;
22155
+ }
22156
+ };
20460
22157
  }
20461
22158
  // --- Public API ---
20462
22159
  get document() {
@@ -20588,15 +22285,6 @@ var Session = class {
20588
22285
  createdAt: Date.now()
20589
22286
  });
20590
22287
  this.jobsMap.set(result.jobId, job2);
20591
- const pendingAgentMessages = this.pendingAgentMessagesByJobId.get(
20592
- result.jobId
20593
- );
20594
- if (pendingAgentMessages && pendingAgentMessages.length > 0) {
20595
- this.pendingAgentMessagesByJobId.delete(result.jobId);
20596
- for (const message of pendingAgentMessages) {
20597
- job2.replayAgentMessage(message);
20598
- }
20599
- }
20600
22288
  return job2;
20601
22289
  }
20602
22290
  /**
@@ -20617,50 +22305,44 @@ var Session = class {
20617
22305
  async answerPrompt(promptId, answer) {
20618
22306
  const prompt3 = this.promptCache.get(promptId);
20619
22307
  const resolvedAnswer = resolvePromptAnswer(prompt3, answer);
22308
+ const response = await this.client.call("prompt.answer", {
22309
+ promptId,
22310
+ answer: resolvedAnswer,
22311
+ value: resolvedAnswer
22312
+ });
22313
+ if (response && typeof response === "object" && "ok" in response && response.ok === false) {
22314
+ const rejected = response;
22315
+ const errorMessage = typeof rejected.error === "string" ? rejected.error : "Prompt answer was rejected.";
22316
+ throw new Error(errorMessage);
22317
+ }
20620
22318
  this.promptCache.delete(promptId);
20621
22319
  this.hiddenPromptIds.add(promptId);
20622
22320
  this.emit("prompt:answered", {
20623
22321
  ...prompt3 || { id: promptId },
20624
22322
  id: promptId,
22323
+ answer: resolvedAnswer,
20625
22324
  status: "answered"
20626
22325
  });
20627
- try {
20628
- const response = await this.client.call("prompt.answer", {
20629
- promptId,
20630
- answer: resolvedAnswer,
20631
- value: resolvedAnswer
20632
- });
20633
- if (response && typeof response === "object" && "ok" in response && response.ok === false) {
20634
- const rejected = response;
20635
- const errorMessage = typeof rejected.error === "string" ? rejected.error : "Prompt answer was rejected.";
20636
- throw new Error(errorMessage);
20637
- }
20638
- } catch (error2) {
20639
- this.hiddenPromptIds.delete(promptId);
20640
- if (prompt3) {
20641
- this.promptCache.set(promptId, prompt3);
20642
- }
20643
- throw error2;
20644
- }
20645
- try {
20646
- const content = this.stringifyConversationValue(resolvedAnswer);
20647
- if (content.trim()) {
20648
- await withPromptTranscriptTimeout(
20649
- this.appendConversationMessage({
20650
- role: "user",
20651
- content,
20652
- promptId
20653
- })
20654
- );
20655
- }
20656
- } catch {
20657
- }
20658
22326
  }
20659
- async appendConversationMessage(input) {
20660
- return this.client.call(
20661
- "conversation.append",
20662
- input
22327
+ async appendUserMessage(input) {
22328
+ const requestedId = normalizeUserMessageIdentity(input.id, "id");
22329
+ const requestedOperationId = normalizeUserMessageIdentity(
22330
+ input.operationId,
22331
+ "operationId"
20663
22332
  );
22333
+ const id = requestedId || (requestedOperationId ? void 0 : reserveUserMessageId());
22334
+ const operationId2 = requestedOperationId || `conversation.append:${id}`;
22335
+ const {
22336
+ id: _ignoredInputId,
22337
+ operationId: _ignoredInputOperationId,
22338
+ ...message
22339
+ } = input;
22340
+ return this.client.call("conversation.append", {
22341
+ ...message,
22342
+ role: "user",
22343
+ ...id ? { id } : {},
22344
+ operationId: operationId2
22345
+ });
20664
22346
  }
20665
22347
  /**
20666
22348
  * Get the current list of available effects.
@@ -21043,6 +22725,7 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21043
22725
  * Close the session and disconnect from the sandbox
21044
22726
  */
21045
22727
  async disconnect() {
22728
+ this.disposeSessionFeed();
21046
22729
  try {
21047
22730
  await this.client.call("client.goodbye", {
21048
22731
  clientId: this.clientId,
@@ -21052,6 +22735,10 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21052
22735
  }
21053
22736
  this.client.disconnect();
21054
22737
  }
22738
+ /** Stop feed repair work without detaching a reconnectable transport. */
22739
+ disposeSessionFeed() {
22740
+ this.feedController.dispose();
22741
+ }
21055
22742
  // --- Event Handling ---
21056
22743
  /**
21057
22744
  * Subscribe to session events
@@ -21076,9 +22763,14 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21076
22763
  }
21077
22764
  }
21078
22765
  // --- Internal ---
22766
+ setFeedListTransport(transport) {
22767
+ this.feedController.setListTransport(transport);
22768
+ }
21079
22769
  setupToolInvokeHandler() {
21080
22770
  this.client.registerRpcHandler("tool.invoke", async (params) => {
21081
- const { callId, toolName, input } = params;
22771
+ const invocation = params;
22772
+ const { callId, toolName, input, feedbackCapability } = invocation;
22773
+ const capabilityResultParams = feedbackCapability ? { feedbackCapability } : {};
21082
22774
  this.emit("effect:invoke", {
21083
22775
  callId,
21084
22776
  effectKey: toolName,
@@ -21090,6 +22782,7 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21090
22782
  if (!handler) {
21091
22783
  await this.client.call("tool.result", {
21092
22784
  callId,
22785
+ ...capabilityResultParams,
21093
22786
  error: {
21094
22787
  code: "TOOL_NOT_FOUND",
21095
22788
  message: `Tool handler not found: ${toolName}`
@@ -21099,21 +22792,45 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21099
22792
  }
21100
22793
  try {
21101
22794
  let result;
21102
- const invocationContext = this.buildLegacyEffectContext();
21103
- if (this.instanceTools.has(toolName) && input && typeof input === "object" && "_objectId" in input) {
21104
- const { _objectId, ...restParams } = input;
21105
- result = await handler(
21106
- _objectId,
21107
- restParams,
21108
- invocationContext
21109
- );
21110
- } else {
21111
- result = await handler(input, invocationContext);
22795
+ const feedbackContext = this.createDirectedInvocationFeedbackContext(invocation);
22796
+ const invocationContext = this.buildDirectedInvocationEffectContext(
22797
+ invocation,
22798
+ feedbackContext
22799
+ );
22800
+ let handlerError;
22801
+ let handlerFailed = false;
22802
+ try {
22803
+ if (this.instanceTools.has(toolName) && input && typeof input === "object" && "_objectId" in input) {
22804
+ const { _objectId, ...restParams } = input;
22805
+ result = await handler(
22806
+ _objectId,
22807
+ restParams,
22808
+ invocationContext
22809
+ );
22810
+ } else {
22811
+ result = await handler(input, invocationContext);
22812
+ }
22813
+ } catch (error2) {
22814
+ handlerFailed = true;
22815
+ handlerError = error2;
21112
22816
  }
22817
+ let feedbackError;
22818
+ let feedbackFailed = false;
22819
+ if (feedbackContext) {
22820
+ try {
22821
+ await feedbackContext.flush();
22822
+ } catch (error2) {
22823
+ feedbackFailed = true;
22824
+ feedbackError = error2;
22825
+ }
22826
+ }
22827
+ if (handlerFailed) throw handlerError;
22828
+ if (feedbackFailed) throw feedbackError;
21113
22829
  this.emit("effect:result", { callId, effectKey: toolName, result });
21114
22830
  this.emit("tool:result", { callId, result });
21115
22831
  await this.client.call("tool.result", {
21116
22832
  callId,
22833
+ ...capabilityResultParams,
21117
22834
  result
21118
22835
  });
21119
22836
  } catch (error2) {
@@ -21126,6 +22843,7 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21126
22843
  this.emit("tool:result", { callId, error: errorMessage });
21127
22844
  await this.client.call("tool.result", {
21128
22845
  callId,
22846
+ ...capabilityResultParams,
21129
22847
  error: { code: "TOOL_EXECUTION_FAILED", message: errorMessage }
21130
22848
  });
21131
22849
  }
@@ -21138,9 +22856,14 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21138
22856
  );
21139
22857
  this.client.on("sync", (doc) => {
21140
22858
  this.currentDomainRevision = this.extractDomainRevisionFromDoc(doc);
22859
+ this.feedController.updateDocument(doc);
22860
+ this.reconcilePromptCacheFromCanonicalDocument(doc);
21141
22861
  this.emit("sync", doc);
21142
22862
  this.checkForToolChanges();
21143
22863
  });
22864
+ this.client.onCanonicalDocumentQuarantine?.((quarantine) => {
22865
+ this.feedController.quarantineCanonicalReplacement(quarantine);
22866
+ });
21144
22867
  const emitPrompt = (payload) => {
21145
22868
  const prompt3 = normalizePrompt(payload);
21146
22869
  if (!prompt3) return;
@@ -21173,23 +22896,6 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21173
22896
  this.client.on("harness.text_response.delta", (data) => {
21174
22897
  this.emit("harness:text_response_delta", data);
21175
22898
  });
21176
- this.client.on("job.agent_message", (data) => {
21177
- const normalized = normalizeJobAgentMessageEnvelope(data);
21178
- if (!normalized) return;
21179
- this.emit("job:agent_message", normalized);
21180
- if (this.jobsMap.has(normalized.jobId)) return;
21181
- const pending = this.pendingAgentMessagesByJobId.get(normalized.jobId) || [];
21182
- if (normalized.message.messageId && pending.some(
21183
- (message) => message.messageId === normalized.message.messageId
21184
- )) {
21185
- return;
21186
- }
21187
- pending.push(normalized.message);
21188
- this.pendingAgentMessagesByJobId.set(
21189
- normalized.jobId,
21190
- pending.slice(-25)
21191
- );
21192
- });
21193
22899
  this.client.on("exec.completed", (data) => {
21194
22900
  this.emit("exec:completed", data);
21195
22901
  });
@@ -21314,24 +23020,6 @@ function sanitizeFeedbackValue(value, depth = 0, seen = /* @__PURE__ */ new Weak
21314
23020
  }
21315
23021
  return truncateFeedbackString(String(value));
21316
23022
  }
21317
- function normalizeJobAgentMessageEnvelope(data) {
21318
- const d = data;
21319
- if (typeof d?.jobId !== "string" || !d.jobId) {
21320
- return null;
21321
- }
21322
- return {
21323
- jobId: d.jobId,
21324
- ...typeof d.turnId === "string" && d.turnId.trim() ? { turnId: d.turnId.trim() } : {},
21325
- message: {
21326
- messageId: d.messageId,
21327
- kind: d.kind === "artifacts" ? "artifacts" : "text",
21328
- reply: typeof d.reply === "string" ? d.reply : "",
21329
- show: d.show,
21330
- actions: Array.isArray(d.actions) ? d.actions : void 0,
21331
- timestamp: d.timestamp || Date.now()
21332
- }
21333
- };
21334
- }
21335
23023
  var JobImplementation = class {
21336
23024
  id;
21337
23025
  client;
@@ -21340,8 +23028,6 @@ var JobImplementation = class {
21340
23028
  _resolveResult;
21341
23029
  _rejectResult;
21342
23030
  eventListeners = /* @__PURE__ */ new Map();
21343
- bufferedAgentMessages = [];
21344
- bufferedAgentMessageIds = /* @__PURE__ */ new Set();
21345
23031
  resultSettled = false;
21346
23032
  metadata;
21347
23033
  constructor(id, client, initialState) {
@@ -21501,12 +23187,6 @@ var JobImplementation = class {
21501
23187
  });
21502
23188
  }
21503
23189
  });
21504
- this.client.on("job.agent_message", (data) => {
21505
- const normalized = normalizeJobAgentMessageEnvelope(data);
21506
- if (normalized?.jobId === id) {
21507
- this.captureAgentMessage(normalized.message);
21508
- }
21509
- });
21510
23190
  }
21511
23191
  get result() {
21512
23192
  return this._resultPromise;
@@ -21531,11 +23211,6 @@ var JobImplementation = class {
21531
23211
  this.eventListeners.set(event, []);
21532
23212
  }
21533
23213
  this.eventListeners.get(event).push(handler);
21534
- if (event === "agentMessage" && this.bufferedAgentMessages.length > 0) {
21535
- for (const message of this.bufferedAgentMessages) {
21536
- handler(message);
21537
- }
21538
- }
21539
23214
  return () => {
21540
23215
  const handlers = this.eventListeners.get(event);
21541
23216
  if (!handlers) {
@@ -21547,9 +23222,6 @@ var JobImplementation = class {
21547
23222
  );
21548
23223
  };
21549
23224
  }
21550
- replayAgentMessage(message) {
21551
- this.captureAgentMessage(message);
21552
- }
21553
23225
  buildFeedbackMetadata() {
21554
23226
  const startedAt = this.metadata.startedAt;
21555
23227
  const completedAt = this.metadata.completedAt;
@@ -21622,317 +23294,11 @@ var JobImplementation = class {
21622
23294
  handlers.forEach((h) => h(data));
21623
23295
  }
21624
23296
  }
21625
- captureAgentMessage(message) {
21626
- if (message.messageId && this.bufferedAgentMessageIds.has(message.messageId)) {
21627
- return;
21628
- }
21629
- if (message.messageId) {
21630
- this.bufferedAgentMessageIds.add(message.messageId);
21631
- }
21632
- this.bufferedAgentMessages = [...this.bufferedAgentMessages, message].slice(
21633
- -25
21634
- );
21635
- this.emit("agentMessage", message);
21636
- }
21637
23297
  };
21638
23298
 
21639
- // src/job-presentation.ts
21640
- var RESPONSE_KEYS = [
21641
- "reply",
21642
- "response",
21643
- "text",
21644
- "message",
21645
- "summary",
21646
- "answer"
21647
- ];
21648
- var ENTRY_KEY_CANDIDATES = ["entryPath", "path"];
21649
- var ENTRY_ARRAY_KEY_CANDIDATES = ["entryPaths", "paths"];
21650
- var LIST_KEY_CANDIDATES = ["listName"];
21651
- var LIST_ARRAY_KEY_CANDIDATES = ["listNames"];
21652
- var VARIABLE_KEY_CANDIDATES = ["variableName"];
21653
- var VARIABLE_ARRAY_KEY_CANDIDATES = ["variableNames"];
21654
- var UI_CONTAINER_KEYS = ["show", "display", "present", "ui"];
21655
- function asRecord2(value) {
21656
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
21657
- return value;
21658
- }
21659
- function normalizeText(value) {
21660
- if (typeof value !== "string") return null;
21661
- const trimmed = value.trim();
21662
- if (!trimmed) return null;
21663
- if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
21664
- return null;
21665
- }
21666
- return trimmed;
21667
- }
21668
- function humanTextFromStdout(stdout) {
21669
- for (const line of [...stdout].reverse()) {
21670
- const normalized = normalizeText(line);
21671
- if (!normalized) continue;
21672
- if (/^[A-Z_]+:/.test(normalized)) continue;
21673
- return normalized;
21674
- }
21675
- return null;
21676
- }
21677
- function responseTextFromAgentMessages(agentMessages) {
21678
- for (const message of [...agentMessages].reverse()) {
21679
- const record = asRecord2(message);
21680
- if (!record) continue;
21681
- for (const key of RESPONSE_KEYS) {
21682
- const normalized = normalizeText(record[key]);
21683
- if (normalized) return normalized;
21684
- }
21685
- }
21686
- return null;
21687
- }
21688
- function pushString(target, value) {
21689
- if (typeof value === "string" && value.trim()) {
21690
- target.add(value.trim());
21691
- }
21692
- }
21693
- function pushStringArray(target, value) {
21694
- if (!Array.isArray(value)) return;
21695
- for (const item of value) {
21696
- pushString(target, item);
21697
- }
21698
- }
21699
- function collectReferencesFromRecord(record, refs) {
21700
- for (const key of ENTRY_KEY_CANDIDATES)
21701
- pushString(refs.entryPaths, record[key]);
21702
- for (const key of ENTRY_ARRAY_KEY_CANDIDATES)
21703
- pushStringArray(refs.entryPaths, record[key]);
21704
- for (const key of LIST_KEY_CANDIDATES)
21705
- pushString(refs.listNames, record[key]);
21706
- for (const key of LIST_ARRAY_KEY_CANDIDATES)
21707
- pushStringArray(refs.listNames, record[key]);
21708
- for (const key of VARIABLE_KEY_CANDIDATES)
21709
- pushString(refs.variableNames, record[key]);
21710
- for (const key of VARIABLE_ARRAY_KEY_CANDIDATES)
21711
- pushStringArray(refs.variableNames, record[key]);
21712
- }
21713
- function stringValue(record, keys) {
21714
- for (const key of keys) {
21715
- const value = record[key];
21716
- if (typeof value === "string" && value.trim()) {
21717
- return value.trim();
21718
- }
21719
- }
21720
- return null;
21721
- }
21722
- function findEntryPathForRecord(record, heap) {
21723
- const directPath = stringValue(record, ["entryPath", "path"]);
21724
- if (directPath && heap.entriesByPath?.[directPath]) {
21725
- return directPath;
21726
- }
21727
- const id = stringValue(record, ["id", "_id", "recordId", "objectId"]);
21728
- if (!id) {
21729
- return null;
21730
- }
21731
- const className = stringValue(record, [
21732
- "className",
21733
- "_className",
21734
- "__className",
21735
- "prototype",
21736
- "type"
21737
- ]);
21738
- const entries = Object.values(heap.entriesByPath || {});
21739
- const exact = entries.find(
21740
- (entry) => entry.id === id && (!className || entry.className === className || entry.prototypes?.includes(className))
21741
- );
21742
- if (exact?.path) {
21743
- return exact.path;
21744
- }
21745
- const idOnlyMatches = entries.filter((entry) => entry.id === id);
21746
- return idOnlyMatches.length === 1 ? idOnlyMatches[0].path : null;
21747
- }
21748
- function scanForHeapReferences(value, heap, refs, depth = 0, seen = /* @__PURE__ */ new Set()) {
21749
- if (value === null || value === void 0 || depth > 4 || seen.has(value))
21750
- return;
21751
- if (typeof value === "string") {
21752
- const trimmed = value.trim();
21753
- if (heap.entriesByPath?.[trimmed]) refs.entryPaths.add(trimmed);
21754
- if (heap.listsByName?.[trimmed]) refs.listNames.add(trimmed);
21755
- if (heap.variablesByName?.[trimmed]) refs.variableNames.add(trimmed);
21756
- return;
21757
- }
21758
- if (Array.isArray(value)) {
21759
- seen.add(value);
21760
- for (const item of value.slice(0, 24)) {
21761
- scanForHeapReferences(item, heap, refs, depth + 1, seen);
21762
- }
21763
- return;
21764
- }
21765
- const record = asRecord2(value);
21766
- if (!record) return;
21767
- seen.add(value);
21768
- const entryPath = findEntryPathForRecord(record, heap);
21769
- if (entryPath) refs.entryPaths.add(entryPath);
21770
- collectReferencesFromRecord(record, refs);
21771
- for (const key of UI_CONTAINER_KEYS) {
21772
- const nested = asRecord2(record[key]);
21773
- if (nested) collectReferencesFromRecord(nested, refs);
21774
- }
21775
- for (const nested of Object.values(record).slice(0, 24)) {
21776
- scanForHeapReferences(nested, heap, refs, depth + 1, seen);
21777
- }
21778
- }
21779
- function resolveVariablesToReferences(variableNames, heap, refs) {
21780
- for (const variableName of variableNames) {
21781
- const variable = heap.variablesByName?.[variableName];
21782
- if (!variable) continue;
21783
- if (variable.kind === "entry" && variable.entryPath) {
21784
- refs.entryPaths.add(variable.entryPath);
21785
- }
21786
- if (variable.kind === "list" && variable.listName) {
21787
- refs.listNames.add(variable.listName);
21788
- }
21789
- }
21790
- }
21791
- function sortEntries(entries) {
21792
- return [...entries].sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
21793
- }
21794
- function sortLists(lists) {
21795
- return [...lists].sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
21796
- }
21797
- function dedupeEntries(entries) {
21798
- const seen = /* @__PURE__ */ new Set();
21799
- const result = [];
21800
- for (const entry of entries) {
21801
- if (!entry?.path || seen.has(entry.path)) continue;
21802
- seen.add(entry.path);
21803
- result.push(entry);
21804
- }
21805
- return result;
21806
- }
21807
- function dedupeLists(lists) {
21808
- const seen = /* @__PURE__ */ new Set();
21809
- const result = [];
21810
- for (const list of lists) {
21811
- if (!list?.name || seen.has(list.name)) continue;
21812
- seen.add(list.name);
21813
- result.push(list);
21814
- }
21815
- return result;
21816
- }
21817
- function extractResponseText(result, stdout) {
21818
- const directText = normalizeText(result);
21819
- if (directText) return directText;
21820
- const record = asRecord2(result);
21821
- if (record) {
21822
- for (const key of RESPONSE_KEYS) {
21823
- const normalized = normalizeText(record[key]);
21824
- if (normalized) return normalized;
21825
- }
21826
- for (const containerKey of UI_CONTAINER_KEYS) {
21827
- const nested = asRecord2(record[containerKey]);
21828
- if (!nested) continue;
21829
- for (const key of RESPONSE_KEYS) {
21830
- const normalized = normalizeText(nested[key]);
21831
- if (normalized) return normalized;
21832
- }
21833
- }
21834
- }
21835
- return humanTextFromStdout(stdout);
21836
- }
21837
- function fallbackResponseText(entries, lists) {
21838
- if (entries.length > 0) {
21839
- return entries.length === 1 ? "I found one relevant record." : `I found ${entries.length} relevant records.`;
21840
- }
21841
- if (lists.length > 0) {
21842
- const emptyOnly = lists.every((list) => (list.paths || []).length === 0);
21843
- if (emptyOnly) {
21844
- return lists.length === 1 ? "I saved one empty result set." : `I saved ${lists.length} empty result sets.`;
21845
- }
21846
- return lists.length === 1 ? "I saved one result set." : `I saved ${lists.length} result sets.`;
21847
- }
21848
- return null;
21849
- }
21850
- function getJobRelatedEntries(heap, jobId) {
21851
- return sortEntries(
21852
- Object.values(heap.entriesByPath || {}).filter(
21853
- (entry) => entry.relatedJobIds?.includes(jobId)
21854
- )
21855
- );
21856
- }
21857
- function getJobRelatedLists(heap, jobId) {
21858
- return sortLists(
21859
- Object.values(heap.listsByName || {}).filter(
21860
- (list) => list.relatedJobIds?.includes(jobId)
21861
- )
21862
- );
21863
- }
21864
- function entriesFromLists(lists, heap) {
21865
- const entries = [];
21866
- for (const list of lists) {
21867
- for (const path7 of list.paths || []) {
21868
- const entry = heap.entriesByPath?.[path7];
21869
- if (entry) entries.push(entry);
21870
- }
21871
- }
21872
- return entries;
21873
- }
21874
- function resolveJobPresentation({
21875
- jobId,
21876
- result,
21877
- stdout = [],
21878
- agentMessages = [],
21879
- sessionHeap,
21880
- allowExplicitArtifacts = true
21881
- }) {
21882
- const refs = {
21883
- entryPaths: /* @__PURE__ */ new Set(),
21884
- listNames: /* @__PURE__ */ new Set(),
21885
- variableNames: /* @__PURE__ */ new Set()
21886
- };
21887
- if (allowExplicitArtifacts) {
21888
- scanForHeapReferences(result, sessionHeap, refs);
21889
- resolveVariablesToReferences(refs.variableNames, sessionHeap, refs);
21890
- }
21891
- const referencedLists = sortLists(
21892
- [...refs.listNames].map((name) => sessionHeap.listsByName?.[name]).filter((list) => Boolean(list))
21893
- );
21894
- const referencedEntries = sortEntries(
21895
- [...refs.entryPaths].map((path7) => sessionHeap.entriesByPath?.[path7]).filter((entry) => Boolean(entry))
21896
- );
21897
- const jobLists = getJobRelatedLists(sessionHeap, jobId);
21898
- const jobEntries = getJobRelatedEntries(sessionHeap, jobId);
21899
- const changedEntries = dedupeEntries([
21900
- ...jobEntries,
21901
- ...entriesFromLists(jobLists, sessionHeap)
21902
- ]);
21903
- const explicitLists = dedupeLists(referencedLists);
21904
- const explicitEntries = dedupeEntries([
21905
- ...referencedEntries,
21906
- ...entriesFromLists(referencedLists, sessionHeap)
21907
- ]);
21908
- const hasExplicitArtifacts = allowExplicitArtifacts && (explicitEntries.length > 0 || explicitLists.length > 0);
21909
- const lists = hasExplicitArtifacts ? explicitLists : jobLists;
21910
- const entries = hasExplicitArtifacts ? explicitEntries : changedEntries;
21911
- const responseText = extractResponseText(result, stdout) || responseTextFromAgentMessages(agentMessages) || fallbackResponseText(entries, lists);
21912
- return {
21913
- responseText,
21914
- entries,
21915
- lists,
21916
- changedEntries,
21917
- changedLists: jobLists,
21918
- hasExplicitArtifacts
21919
- };
21920
- }
21921
-
21922
23299
  // src/session-transcript.ts
21923
- var EMPTY_HEAP = {
21924
- entriesByPath: {},
21925
- listsByName: {},
21926
- variablesByName: {}};
21927
23300
  function asRecord3(value) {
21928
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
21929
- return value;
21930
- }
21931
- function asArray(value) {
21932
- return Array.isArray(value) ? value : [];
21933
- }
21934
- function asNumber(value) {
21935
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
23301
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
21936
23302
  }
21937
23303
  function asString(value) {
21938
23304
  return typeof value === "string" ? value : void 0;
@@ -21957,138 +23323,13 @@ function artifactRecordsById(liveDoc) {
21957
23323
  Object.entries(byId).map(([artifactId, value]) => {
21958
23324
  const record = asRecord3(value);
21959
23325
  return record ? [artifactId, record] : null;
21960
- }).filter(
21961
- (entry) => Boolean(entry)
21962
- )
21963
- );
21964
- }
21965
- function normalizeShowRefs(value) {
21966
- const record = asRecord3(value);
21967
- if (!record) return void 0;
21968
- const normalizeRefs = (input) => {
21969
- if (!Array.isArray(input)) return void 0;
21970
- const refs = Array.from(
21971
- new Set(
21972
- input.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean)
21973
- )
21974
- );
21975
- return refs.length > 0 ? refs : void 0;
21976
- };
21977
- const show = {
21978
- entryPaths: normalizeRefs(record.entryPaths),
21979
- listNames: normalizeRefs(record.listNames),
21980
- variableNames: normalizeRefs(record.variableNames),
21981
- fileIds: normalizeRefs(record.fileIds),
21982
- sessionArtifactIds: normalizeRefs(record.sessionArtifactIds),
21983
- actionSuggestions: normalizeActionSuggestions(record.actionSuggestions),
21984
- tables: Array.isArray(record.tables) ? record.tables.filter(
21985
- (table2) => Boolean(
21986
- table2 && typeof table2 === "object" && !Array.isArray(table2) && Array.isArray(table2.columns) && Array.isArray(table2.rows)
21987
- )
21988
- ) : void 0
21989
- };
21990
- return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions || show.tables ? show : void 0;
21991
- }
21992
- function normalizeActionSuggestions(value) {
21993
- if (!Array.isArray(value)) return void 0;
21994
- const suggestions = [];
21995
- for (const item of value) {
21996
- const record = asRecord3(item);
21997
- if (!record) continue;
21998
- const label = trimString(record.label);
21999
- if (!label) continue;
22000
- const suggestionId = trimString(record.suggestionId) || trimString(record.id) || label;
22001
- suggestions.push({
22002
- suggestionId,
22003
- label,
22004
- ...typeof record.description === "string" ? { description: record.description } : {},
22005
- ...asRecord3(record.artifact) ? { artifact: asRecord3(record.artifact) } : {},
22006
- ...asRecord3(record.target) ? { target: asRecord3(record.target) } : {},
22007
- ...asRecord3(record.metadata) ? { metadata: asRecord3(record.metadata) } : {}
22008
- });
22009
- }
22010
- return suggestions.length ? suggestions : void 0;
22011
- }
22012
- var TRANSCRIPT_MESSAGE_PART_LIMIT = 128;
22013
- var TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT = 2e5;
22014
- var TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT = 1e3;
22015
- var TRANSCRIPT_MESSAGE_ACTION_LIMIT = 64;
22016
- function normalizeConversationMessageActions(value) {
22017
- if (!Array.isArray(value) || value.length === 0) return void 0;
22018
- const actions = [];
22019
- for (const item of value.slice(0, TRANSCRIPT_MESSAGE_ACTION_LIMIT)) {
22020
- const record = asRecord3(item);
22021
- const kind = record?.kind;
22022
- const label = trimString(record?.label ?? record?.title);
22023
- const status = record?.status;
22024
- if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
22025
- continue;
22026
- }
22027
- actions.push({
22028
- kind,
22029
- label,
22030
- ...status === "done" || status === "queued" || status === "failed" ? { status } : {}
22031
- });
22032
- }
22033
- return actions.length ? actions : void 0;
22034
- }
22035
- function normalizeConversationMessageParts(value, canonicalContent, canonicalActions) {
22036
- if (!Array.isArray(value) || value.length === 0 || value.length > TRANSCRIPT_MESSAGE_PART_LIMIT) {
22037
- return void 0;
22038
- }
22039
- const parts = [];
22040
- const canonicalActionsById = new Map(
22041
- (canonicalActions || []).map((action) => [
22042
- `${action.kind}:${action.label}`,
22043
- action
22044
- ])
23326
+ }).filter((entry) => Boolean(entry))
22045
23327
  );
22046
- const seenActionIds = /* @__PURE__ */ new Set();
22047
- let textLength = 0;
22048
- for (const item of value) {
22049
- const record = asRecord3(item);
22050
- if (!record) return void 0;
22051
- if (record.type === "text") {
22052
- if (typeof record.text !== "string" || record.text.length === 0) {
22053
- return void 0;
22054
- }
22055
- textLength += record.text.length;
22056
- if (textLength > TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT) return void 0;
22057
- parts.push({ type: "text", text: record.text });
22058
- continue;
22059
- }
22060
- if (record.type !== "action") return void 0;
22061
- const action = asRecord3(record.action);
22062
- const kind = action?.kind;
22063
- const label = trimString(action?.label);
22064
- if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
22065
- return void 0;
22066
- }
22067
- const actionId = `${kind}:${label}`;
22068
- const canonicalAction = canonicalActionsById.get(actionId);
22069
- if (!canonicalAction) return void 0;
22070
- if (seenActionIds.has(actionId)) continue;
22071
- seenActionIds.add(actionId);
22072
- parts.push({
22073
- type: "action",
22074
- action: canonicalAction
22075
- });
22076
- }
22077
- const orderedText = parts.filter(
22078
- (part) => part.type === "text"
22079
- ).map((part) => part.text).join("");
22080
- return orderedText === canonicalContent ? parts : void 0;
22081
23328
  }
22082
23329
  function stringifyTranscriptValue(value, fallback2 = "") {
22083
- if (typeof value === "string") {
22084
- return value.trim() || fallback2;
22085
- }
22086
- if (typeof value === "boolean") {
22087
- return value ? "Confirmed" : "Canceled";
22088
- }
22089
- if (value === void 0) {
22090
- return fallback2;
22091
- }
23330
+ if (typeof value === "string") return value.trim() || fallback2;
23331
+ if (typeof value === "boolean") return value ? "Confirmed" : "Canceled";
23332
+ if (value === void 0) return fallback2;
22092
23333
  try {
22093
23334
  const json = JSON.stringify(value, null, 2);
22094
23335
  if (!json || json === "undefined") return fallback2;
@@ -22229,262 +23470,121 @@ ${stringifyTranscriptValue({ show }, "")}`;
22229
23470
  hasOtherRefs ? `Other shown refs: ${stringifyTranscriptValue(otherRefs, "")}` : null
22230
23471
  ].filter(Boolean).join("\n");
22231
23472
  }
22232
- function normalizeConversationMessage(raw, artifactsById) {
22233
- const record = asRecord3(raw);
22234
- if (!record) return null;
22235
- const role = record.role === "user" ? "user" : record.role === "assistant" ? "assistant" : null;
22236
- if (!role) return null;
22237
- const content = trimString(
22238
- record.content ?? record.reply ?? record.message ?? record.text
22239
- );
22240
- const actions = role === "assistant" ? normalizeConversationMessageActions(record.actions) : void 0;
22241
- const parts = role === "assistant" ? normalizeConversationMessageParts(record.parts, content, actions) : void 0;
22242
- const show = normalizeShowRefs(record.show);
22243
- const id = asString(record.id) || crypto.randomUUID();
22244
- const timestamp = asNumber(record.timestamp) || asNumber(record.ts) || 0;
22245
- if (!content && !show && !actions?.length) return null;
22246
- const artifactHistory = buildArtifactHistory(show, artifactsById);
22247
- const historyContent = role === "assistant" ? content && artifactHistory ? `[Assistant reply]
22248
- ${content}
22249
-
22250
- ${artifactHistory}` : content ? `[Assistant reply]
22251
- ${content}` : artifactHistory : void 0;
22252
- return {
22253
- id,
22254
- role,
22255
- content,
22256
- timestamp,
22257
- jobId: asString(record.jobId),
22258
- promptId: asString(record.promptId),
22259
- show,
22260
- actions,
22261
- parts,
22262
- historyContent,
22263
- source: "conversation"
22264
- };
23473
+ function feedItemShow(item) {
23474
+ switch (item.kind) {
23475
+ case "objects": {
23476
+ const entryPaths = [];
23477
+ const listNames = [];
23478
+ const variableNames = [];
23479
+ for (const ref of item.payload.refs) {
23480
+ if (ref.type === "entry") entryPaths.push(ref.path);
23481
+ if (ref.type === "list") listNames.push(ref.name);
23482
+ if (ref.type === "variable") variableNames.push(ref.name);
23483
+ }
23484
+ return {
23485
+ ...entryPaths.length ? { entryPaths } : {},
23486
+ ...listNames.length ? { listNames } : {},
23487
+ ...variableNames.length ? { variableNames } : {}
23488
+ };
23489
+ }
23490
+ case "table":
23491
+ return {
23492
+ tables: [
23493
+ {
23494
+ id: item.payload.tableId,
23495
+ label: item.payload.label,
23496
+ columns: item.payload.columns,
23497
+ rows: item.payload.rows
23498
+ }
23499
+ ]
23500
+ };
23501
+ case "artifact":
23502
+ return { sessionArtifactIds: [item.payload.artifactId] };
23503
+ case "file":
23504
+ return { fileIds: [item.payload.fileId] };
23505
+ case "action_suggestion":
23506
+ return {
23507
+ actionSuggestions: [
23508
+ {
23509
+ suggestionId: item.payload.suggestionId,
23510
+ label: item.payload.label,
23511
+ description: item.payload.description,
23512
+ target: item.payload.target ? { ...item.payload.target } : void 0,
23513
+ artifact: item.payload.proposedArtifact ? { ...item.payload.proposedArtifact } : void 0
23514
+ }
23515
+ ]
23516
+ };
23517
+ default:
23518
+ return void 0;
23519
+ }
22265
23520
  }
22266
- function normalizePromptEntries(jobId, rawPrompts, conversationPromptIds) {
22267
- const promptsById = asRecord3(rawPrompts) || {};
22268
- return Object.values(promptsById).map((value) => asRecord3(value)).filter((value) => Boolean(value)).sort(
22269
- (left, right) => (asNumber(left.openedAt) || asNumber(left.answeredAt) || 0) - (asNumber(right.openedAt) || asNumber(right.answeredAt) || 0)
22270
- ).flatMap((prompt3) => {
22271
- const promptId = asString(prompt3.promptId);
22272
- if (!promptId || conversationPromptIds.has(promptId)) return [];
22273
- const title = trimString(prompt3.title);
22274
- const message = trimString(prompt3.message);
22275
- const assistantContent = message || title || "Input required";
22276
- const openedAt = asNumber(prompt3.openedAt) || 0;
22277
- const answeredAt = asNumber(prompt3.answeredAt) || openedAt;
22278
- const entries = [
22279
- {
22280
- id: `prompt:${promptId}:assistant`,
22281
- role: "assistant",
22282
- content: assistantContent,
22283
- timestamp: openedAt,
22284
- jobId,
22285
- promptId,
22286
- historyContent: `[Assistant reply]
22287
- ${assistantContent}`,
22288
- source: "job_prompt"
22289
- }
22290
- ];
22291
- if (Object.prototype.hasOwnProperty.call(prompt3, "answer")) {
23521
+ function buildSessionTranscriptFromFeedItems(input) {
23522
+ const artifactsById = artifactRecordsById(input.liveDoc);
23523
+ const entries = [];
23524
+ for (const item of [...input.items].sort(
23525
+ (left, right) => left.sequence - right.sequence
23526
+ )) {
23527
+ if (item.kind === "feedback") continue;
23528
+ if (item.kind === "message") {
23529
+ if (item.payload.role === "system") continue;
23530
+ const content = item.payload.text;
22292
23531
  entries.push({
22293
- id: `prompt:${promptId}:user`,
22294
- role: "user",
22295
- content: stringifyTranscriptValue(prompt3.answer, ""),
22296
- timestamp: answeredAt,
22297
- jobId,
22298
- promptId,
22299
- source: "job_prompt"
23532
+ id: item.id,
23533
+ role: item.payload.role,
23534
+ content,
23535
+ timestamp: item.occurredAt,
23536
+ sequence: item.sequence,
23537
+ jobId: item.source?.jobId,
23538
+ promptId: item.payload.inReplyToPromptId,
23539
+ historyContent: item.payload.role === "assistant" ? `[Assistant reply]
23540
+ ${content}` : void 0,
23541
+ source: "feed"
22300
23542
  });
23543
+ continue;
22301
23544
  }
22302
- return entries;
22303
- });
22304
- }
22305
- function normalizeAgentMessageEntries(jobId, rawMessages, artifactsById) {
22306
- return asArray(rawMessages).map((value) => asRecord3(value)).filter((value) => Boolean(value)).sort(
22307
- (left, right) => (asNumber(left.timestamp) || asNumber(left.ts) || 0) - (asNumber(right.timestamp) || asNumber(right.ts) || 0)
22308
- ).flatMap((message) => {
22309
- const messageId = asString(message.messageId) || asString(message.id) || crypto.randomUUID();
22310
- const timestamp = asNumber(message.timestamp) || asNumber(message.ts) || 0;
22311
- const reply = trimString(
22312
- message.reply ?? message.message ?? message.text ?? message.content
22313
- );
22314
- const show = normalizeShowRefs(message.show);
22315
- const entries = [];
22316
- if (reply) {
23545
+ if (item.kind === "prompt") {
23546
+ const content = item.payload.message || item.payload.title;
22317
23547
  entries.push({
22318
- id: `agent:${messageId}:text`,
23548
+ id: item.id,
22319
23549
  role: "assistant",
22320
- content: reply,
22321
- timestamp,
22322
- jobId,
23550
+ content,
23551
+ timestamp: item.occurredAt,
23552
+ sequence: item.sequence,
23553
+ jobId: item.source?.jobId,
23554
+ promptId: item.payload.promptId,
22323
23555
  historyContent: `[Assistant reply]
22324
- ${reply}`,
22325
- source: "job_agent_message"
22326
- });
22327
- }
22328
- if (show) {
22329
- entries.push({
22330
- id: `agent:${messageId}:artifacts`,
22331
- role: "assistant",
22332
- content: "",
22333
- timestamp,
22334
- jobId,
22335
- show,
22336
- historyContent: buildArtifactHistory(show, artifactsById),
22337
- source: "job_agent_message"
23556
+ ${content}`,
23557
+ source: "feed"
22338
23558
  });
23559
+ continue;
22339
23560
  }
22340
- return entries;
22341
- });
22342
- }
22343
- function buildJobFallbackEntries(jobId, job2, sessionHeap, artifactsById) {
22344
- const timestamp = asNumber(job2.finishedAt) || asNumber(job2.startedAt) || asNumber(job2.submittedAt) || 0;
22345
- const resultPreview = stringifyTranscriptValue(
22346
- job2.result,
22347
- "No job result recorded."
22348
- );
22349
- const presentation = resolveJobPresentation({
22350
- jobId,
22351
- result: job2.result,
22352
- stdout: [],
22353
- sessionHeap
22354
- });
22355
- const entries = [];
22356
- const responseText = presentation.responseText || "";
22357
- if (responseText) {
22358
- entries.push({
22359
- id: `job:${jobId}:result-text`,
22360
- role: "assistant",
22361
- content: responseText,
22362
- timestamp,
22363
- jobId,
22364
- historyContent: `[Assistant reply]
22365
- ${responseText}`,
22366
- source: "job_result"
22367
- });
22368
- }
22369
- const show = {
22370
- entryPaths: presentation.entries.map((entry) => entry.path),
22371
- listNames: presentation.lists.map((list) => list.name)
22372
- };
22373
- if (show.entryPaths && show.entryPaths.length > 0 || show.listNames && show.listNames.length > 0) {
23561
+ const show = feedItemShow(item);
23562
+ if (!show) continue;
22374
23563
  entries.push({
22375
- id: `job:${jobId}:result-artifacts`,
23564
+ id: item.id,
22376
23565
  role: "assistant",
22377
23566
  content: "",
22378
- timestamp,
22379
- jobId,
23567
+ timestamp: item.occurredAt,
23568
+ sequence: item.sequence,
23569
+ jobId: item.source?.jobId,
22380
23570
  show,
22381
23571
  historyContent: buildArtifactHistory(show, artifactsById),
22382
- source: "job_result"
22383
- });
22384
- }
22385
- if (entries.length === 0 && trimString(job2.error)) {
22386
- entries.push({
22387
- id: `job:${jobId}:result-error`,
22388
- role: "assistant",
22389
- content: trimString(job2.error),
22390
- timestamp,
22391
- jobId,
22392
- historyContent: `[Assistant reply]
22393
- ${trimString(job2.error)}`,
22394
- source: "job_result"
22395
- });
22396
- }
22397
- if (entries.length === 0 && resultPreview && resultPreview !== "No job result recorded.") {
22398
- entries.push({
22399
- id: `job:${jobId}:result-preview`,
22400
- role: "assistant",
22401
- content: resultPreview,
22402
- timestamp,
22403
- jobId,
22404
- historyContent: `[Assistant reply]
22405
- ${resultPreview}`,
22406
- source: "job_result"
23572
+ source: "feed"
22407
23573
  });
22408
23574
  }
22409
23575
  return entries;
22410
23576
  }
22411
- function buildJobCodeEntry(jobId, job2) {
22412
- const code = trimString(job2.source);
22413
- if (!code) return null;
22414
- const jobStatus = asString(job2.status);
22415
- const error2 = jobStatus === "failed" || jobStatus === "canceled" || jobStatus === "timeout" ? trimString(job2.error) || `Job ${jobStatus}` : void 0;
22416
- return {
22417
- id: `job:${jobId}:code`,
22418
- role: "assistant",
22419
- content: "",
22420
- timestamp: asNumber(job2.submittedAt) || asNumber(job2.startedAt) || asNumber(job2.finishedAt) || 0,
22421
- jobId,
22422
- code,
22423
- jobStatus,
22424
- jobResultPreview: stringifyTranscriptValue(
22425
- job2.result,
22426
- "No job result recorded."
22427
- ),
22428
- error: error2,
22429
- source: "job_code"
22430
- };
22431
- }
23577
+ var CANONICAL_FEED_REQUIRED = "Canonical session feed-v1 is required; past message/job transcript reconstruction is not supported.";
22432
23578
  function buildSessionTranscript(input) {
22433
23579
  const liveDoc = input.liveDoc || null;
22434
- const sessionHeap = input.sessionHeap || EMPTY_HEAP;
22435
- const artifactsById = artifactRecordsById(liveDoc);
22436
- const transcript = [];
22437
- const conversationMessages = asArray(
22438
- asRecord3(liveDoc?.conversation)?.messages
22439
- ).map((message) => normalizeConversationMessage(message, artifactsById)).filter((message) => Boolean(message));
22440
- const conversationPromptIds = new Set(
22441
- conversationMessages.map((message) => message.promptId).filter((promptId) => Boolean(promptId))
22442
- );
22443
- const assistantConversationJobIds = new Set(
22444
- conversationMessages.filter(
22445
- (message) => message.role === "assistant" && Boolean(message.jobId)
22446
- ).map((message) => message.jobId)
22447
- );
22448
- transcript.push(...conversationMessages);
22449
- const jobsById = asRecord3(asRecord3(liveDoc?.jobs)?.byId) || {};
22450
- const jobs = Object.values(jobsById).map((value) => asRecord3(value)).filter((value) => Boolean(value)).sort(
22451
- (left, right) => (asNumber(left.submittedAt) || asNumber(left.startedAt) || asNumber(left.finishedAt) || 0) - (asNumber(right.submittedAt) || asNumber(right.startedAt) || asNumber(right.finishedAt) || 0)
22452
- );
22453
- for (const job2 of jobs) {
22454
- const jobId = asString(job2.jobId);
22455
- if (!jobId) continue;
22456
- const codeEntry = buildJobCodeEntry(jobId, job2);
22457
- if (codeEntry) {
22458
- transcript.push(codeEntry);
22459
- }
22460
- transcript.push(
22461
- ...normalizePromptEntries(jobId, job2.prompts, conversationPromptIds)
22462
- );
22463
- if (!assistantConversationJobIds.has(jobId)) {
22464
- const agentEntries = normalizeAgentMessageEntries(
22465
- jobId,
22466
- job2.agentMessages,
22467
- artifactsById
22468
- );
22469
- if (agentEntries.length > 0) {
22470
- transcript.push(...agentEntries);
22471
- } else {
22472
- transcript.push(
22473
- ...buildJobFallbackEntries(
22474
- jobId,
22475
- job2,
22476
- sessionHeap,
22477
- artifactsById
22478
- )
22479
- );
22480
- }
22481
- }
22482
- }
22483
- return transcript.sort((left, right) => {
22484
- if (left.timestamp !== right.timestamp) {
22485
- return left.timestamp - right.timestamp;
22486
- }
22487
- return left.id.localeCompare(right.id);
23580
+ if (!isCanonicalSessionFeedDocument(liveDoc)) {
23581
+ throw new Error(CANONICAL_FEED_REQUIRED);
23582
+ }
23583
+ const snapshot = readSessionFeedSnapshot(liveDoc);
23584
+ if (snapshot.error) throw snapshot.error;
23585
+ return buildSessionTranscriptFromFeedItems({
23586
+ items: input.canonicalFeedItems || snapshot.tail,
23587
+ liveDoc
22488
23588
  });
22489
23589
  }
22490
23590
 
@@ -22547,7 +23647,7 @@ function normalizeEffectBehaviors(value) {
22547
23647
  }
22548
23648
  function resolveInvocationMode(context) {
22549
23649
  const mode = context?.invocation?.mode;
22550
- if (mode === "dryRun" || mode === "reverse") {
23650
+ if (mode === "dryRun" || mode === "reverse" || mode === "artifactOptions") {
22551
23651
  return mode;
22552
23652
  }
22553
23653
  return "execute";
@@ -22601,6 +23701,18 @@ function resolveHandlerForMode(effectMap, effect, request) {
22601
23701
  request.context?.behaviors || effect.metamodels || void 0
22602
23702
  );
22603
23703
  const mode = resolveInvocationMode(request.context);
23704
+ if (mode === "artifactOptions") {
23705
+ if (!effect.artifactOptionsHandler) {
23706
+ throw new Error(
23707
+ `Artifact relationship options are not supported for ${request.effectKey}`
23708
+ );
23709
+ }
23710
+ return {
23711
+ effect,
23712
+ mode,
23713
+ handler: effect.artifactOptionsHandler
23714
+ };
23715
+ }
22604
23716
  if (mode === "dryRun") {
22605
23717
  if (effect.dryRunHandler) {
22606
23718
  return { effect, mode, handler: effect.dryRunHandler };
@@ -22633,7 +23745,91 @@ function resolveHandlerForMode(effectMap, effect, request) {
22633
23745
  }
22634
23746
  return { effect, mode, handler: effect.handler };
22635
23747
  }
22636
- async function invokeRegisteredEffect(effectMap, request) {
23748
+ function createInvocationFeedbackContext(bridge) {
23749
+ const publications = [];
23750
+ let nextOperationOrdinal = 0;
23751
+ const nextOperationId = (kind) => `${kind}:${nextOperationOrdinal++}`;
23752
+ const track = (publication) => {
23753
+ const tracked = Promise.resolve(publication);
23754
+ publications.push(tracked);
23755
+ void tracked.catch(() => void 0);
23756
+ return tracked;
23757
+ };
23758
+ const publisher = createFeedPublisher(
23759
+ (method, params) => bridge.publish(method, {
23760
+ ...params,
23761
+ invocationId: bridge.invocationId
23762
+ })
23763
+ );
23764
+ const wrapTransientHandle = (initial) => {
23765
+ let current = initial;
23766
+ const wrapped = {
23767
+ get id() {
23768
+ return current.id;
23769
+ },
23770
+ get ordinal() {
23771
+ return current.ordinal;
23772
+ },
23773
+ get revision() {
23774
+ return current.revision;
23775
+ },
23776
+ async update(text, options = {}) {
23777
+ current = await track(
23778
+ current.update(text, {
23779
+ ...options,
23780
+ operationId: options.operationId || nextOperationId("transient-update")
23781
+ })
23782
+ );
23783
+ return wrapped;
23784
+ },
23785
+ settle(text, options = {}) {
23786
+ return track(
23787
+ current.settle(text, {
23788
+ ...options,
23789
+ operationId: options.operationId || nextOperationId("transient-settle")
23790
+ })
23791
+ );
23792
+ }
23793
+ };
23794
+ return wrapped;
23795
+ };
23796
+ return {
23797
+ feedback(text, options = {}) {
23798
+ return track(
23799
+ publisher.feedback(text, {
23800
+ ...options,
23801
+ operationId: options.operationId || nextOperationId("feedback")
23802
+ })
23803
+ );
23804
+ },
23805
+ transientFeedback(text, options = {}) {
23806
+ return track(
23807
+ publisher.transientFeedback(text, {
23808
+ ...options,
23809
+ operationId: options.operationId || nextOperationId("transient-create")
23810
+ }).then(wrapTransientHandle)
23811
+ );
23812
+ },
23813
+ async flush() {
23814
+ let cursor = 0;
23815
+ let firstError;
23816
+ while (cursor < publications.length) {
23817
+ const batch = publications.slice(cursor);
23818
+ cursor = publications.length;
23819
+ const results = await Promise.allSettled(batch);
23820
+ for (const result of results) {
23821
+ if (result.status === "rejected" && firstError === void 0) {
23822
+ firstError = result.reason;
23823
+ }
23824
+ }
23825
+ }
23826
+ if (firstError !== void 0) {
23827
+ throw firstError;
23828
+ }
23829
+ }
23830
+ };
23831
+ }
23832
+ async function invokeRegisteredEffect(effectMap, request, options = {}) {
22637
23833
  const effect = selectRegisteredEffect(
22638
23834
  effectMap,
22639
23835
  request.effectKey,
@@ -22652,14 +23848,49 @@ async function invokeRegisteredEffect(effectMap, request) {
22652
23848
  mode: resolved.mode,
22653
23849
  sourceEffectKey: request.effectKey,
22654
23850
  sourceEffectName: request.effectName,
22655
- ...request.context?.invocation?.reverseHandler ? { reverseHandler: request.context.invocation.reverseHandler } : {}
23851
+ ...request.context?.invocation?.reverseHandler ? { reverseHandler: request.context.invocation.reverseHandler } : {},
23852
+ ...request.context?.invocation?.artifactId ? { artifactId: request.context.invocation.artifactId } : {},
23853
+ ...request.context?.invocation?.artifactOptions ? { artifactOptions: request.context.invocation.artifactOptions } : {}
22656
23854
  }
22657
23855
  };
22658
- if (resolved.effect.className && !resolved.effect.static && request.input && typeof request.input === "object" && "_objectId" in request.input) {
22659
- const { _objectId, ...rest } = request.input;
22660
- return resolved.handler(_objectId, rest, context);
23856
+ const feedbackContext = options.feedback ? createInvocationFeedbackContext(options.feedback) : null;
23857
+ if (feedbackContext) {
23858
+ context.feedback = feedbackContext.feedback;
23859
+ context.transientFeedback = feedbackContext.transientFeedback;
23860
+ }
23861
+ let handlerResult;
23862
+ let handlerError;
23863
+ let handlerFailed = false;
23864
+ try {
23865
+ if (resolved.mode === "artifactOptions") {
23866
+ handlerResult = await resolved.handler(request.input, context);
23867
+ } else if (resolved.effect.className && !resolved.effect.static && request.input && typeof request.input === "object" && "_objectId" in request.input) {
23868
+ const { _objectId, ...rest } = request.input;
23869
+ handlerResult = await resolved.handler(_objectId, rest, context);
23870
+ } else {
23871
+ handlerResult = await resolved.handler(request.input, context);
23872
+ }
23873
+ } catch (error2) {
23874
+ handlerFailed = true;
23875
+ handlerError = error2;
23876
+ }
23877
+ let feedbackError;
23878
+ let feedbackFailed = false;
23879
+ if (feedbackContext) {
23880
+ try {
23881
+ await feedbackContext.flush();
23882
+ } catch (error2) {
23883
+ feedbackFailed = true;
23884
+ feedbackError = error2;
23885
+ }
23886
+ }
23887
+ if (handlerFailed) {
23888
+ throw handlerError;
23889
+ }
23890
+ if (feedbackFailed) {
23891
+ throw feedbackError;
22661
23892
  }
22662
- return resolved.handler(request.input, context);
23893
+ return handlerResult;
22663
23894
  }
22664
23895
 
22665
23896
  // src/client-normalizers.ts
@@ -22986,6 +24217,36 @@ function buildEffectMetamodelMutations(toolPath, spec) {
22986
24217
  var DEFAULT_CONVERSATION_SESSION_LIST_LIMIT = 100;
22987
24218
  var MAX_CONVERSATION_SESSION_LIST_LIMIT = 500;
22988
24219
  var MAX_CONVERSATION_SESSION_LIST_OFFSET = 1e5;
24220
+ function requireUserEnvironmentSequence(value, field) {
24221
+ if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
24222
+ throw new Error(
24223
+ `Invalid user-environment state: ${field} must be a non-negative safe integer.`
24224
+ );
24225
+ }
24226
+ return value;
24227
+ }
24228
+ function normalizeReadThroughSequenceMap(value) {
24229
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
24230
+ throw new Error(
24231
+ "Invalid user-environment state: readThroughSequenceBySessionId is required."
24232
+ );
24233
+ }
24234
+ return Object.fromEntries(
24235
+ Object.entries(value).map(([sessionId, sequence]) => [
24236
+ sessionId,
24237
+ requireUserEnvironmentSequence(
24238
+ sequence,
24239
+ `readThroughSequenceBySessionId.${sessionId}`
24240
+ )
24241
+ ])
24242
+ );
24243
+ }
24244
+ function requiredAssistantReplyString(value, field) {
24245
+ if (typeof value !== "string" || !value.trim()) {
24246
+ throw new Error(`Assistant reply ${field} must be a non-empty string.`);
24247
+ }
24248
+ return field === "text" ? value : value.trim();
24249
+ }
22989
24250
  function boundedSessionListInteger(value, name, fallback2, minimum, maximum) {
22990
24251
  if (value === void 0) return fallback2;
22991
24252
  if (!Number.isInteger(value) || value < minimum || value > maximum) {
@@ -24559,12 +25820,55 @@ var Environment = class _Environment {
24559
25820
  var EnvironmentSession = class extends Session {
24560
25821
  environment;
24561
25822
  sessionDataRoutePrefix;
25823
+ sessionDataHeaders;
25824
+ heartbeatTimer = null;
25825
+ heartbeatInFlight = false;
24562
25826
  /** The last known graph container status, updated by checkReadiness() or on heartbeat */
24563
25827
  graphContainerStatus = null;
24564
25828
  constructor(client, environment, clientId, options = {}) {
24565
25829
  super(client, clientId, { initialQuota: options.initialQuota });
24566
25830
  this.environment = environment;
24567
25831
  this.sessionDataRoutePrefix = options.sessionDataRoutePrefix || "/orchestrator/ws/sessions";
25832
+ this.sessionDataHeaders = options.sessionDataHeaders || {};
25833
+ this.setFeedListTransport(
25834
+ (feedOptions) => this.sessionDataRequest("/feed", feedOptions)
25835
+ );
25836
+ }
25837
+ /**
25838
+ * Keep the browser session transport observable from the client side.
25839
+ * A worker/proxy restart can leave a browser WebSocket appearing OPEN even
25840
+ * though the server-side Durable Object has already closed its peer. The
25841
+ * heartbeat gives the SDK a bounded failure signal so it can revoke that
25842
+ * stale socket and use WSClient's normal reconnect path.
25843
+ */
25844
+ startHeartbeat() {
25845
+ if (this.heartbeatTimer) return;
25846
+ this.heartbeatTimer = setInterval(() => {
25847
+ if (this.heartbeatInFlight) return;
25848
+ this.heartbeatInFlight = true;
25849
+ void this.client.call("client.heartbeat", {}).then((result) => {
25850
+ if (result?.graphContainerStatus) {
25851
+ this.graphContainerStatus = result.graphContainerStatus;
25852
+ }
25853
+ }).catch((error2) => {
25854
+ this.client.reportTransportFailure(error2);
25855
+ }).finally(() => {
25856
+ this.heartbeatInFlight = false;
25857
+ });
25858
+ }, 5e3);
25859
+ this.heartbeatTimer.unref?.();
25860
+ }
25861
+ stopHeartbeat() {
25862
+ if (this.heartbeatTimer) {
25863
+ clearInterval(this.heartbeatTimer);
25864
+ this.heartbeatTimer = null;
25865
+ }
25866
+ this.heartbeatInFlight = false;
25867
+ }
25868
+ async hello() {
25869
+ const result = await super.hello();
25870
+ this.startHeartbeat();
25871
+ return result;
24568
25872
  }
24569
25873
  get environmentId() {
24570
25874
  return this.environment.environmentId;
@@ -24624,6 +25928,11 @@ var EnvironmentSession = class extends Session {
24624
25928
  const response = await fetch(url, {
24625
25929
  method: init2.method || "GET",
24626
25930
  headers,
25931
+ // Session documents, feed pages, and artifact reads are live
25932
+ // Automerge-backed state. A browser cache entry for the identical
25933
+ // artifact URL can otherwise make the Dock poll the same stale
25934
+ // `running` record until a full reload revalidates it.
25935
+ cache: "no-store",
24627
25936
  ...typeof init2.body === "undefined" ? {} : { body: init2.body }
24628
25937
  });
24629
25938
  if (response.ok) {
@@ -24652,34 +25961,50 @@ var EnvironmentSession = class extends Session {
24652
25961
  const body = typeof init2.body === "undefined" ? void 0 : JSON.stringify(init2.body);
24653
25962
  const response = await this.sessionDataFetch(path7, query, {
24654
25963
  method: init2.method || "GET",
24655
- headers: { "Content-Type": "application/json" },
25964
+ headers: {
25965
+ ...this.sessionDataHeaders,
25966
+ "Content-Type": "application/json"
25967
+ },
24656
25968
  ...typeof body === "undefined" ? {} : { body }
24657
25969
  });
24658
25970
  return response.json();
24659
25971
  }
24660
- async collectAllSessionItems(listPage) {
24661
- const items = [];
24662
- let cursor = null;
24663
- do {
24664
- const page = await listPage({ limit: 500, cursor });
24665
- items.push(...page.items);
24666
- cursor = page.nextCursor;
24667
- } while (cursor);
24668
- return items;
24669
- }
24670
25972
  /**
24671
25973
  * Fetch the live session document from the runtime DO.
24672
25974
  *
24673
- * For history and saved artifacts, prefer the collection APIs on
24674
- * `messages`, `timeline`, `jobs`, and `heap`.
25975
+ * For presentation history use `feed.list()` or `transcript.list()`.
25976
+ * Timeline and job collections are diagnostic/execution data only.
24675
25977
  */
24676
25978
  async getDocument() {
24677
25979
  return this.sessionDataRequest("/document");
24678
25980
  }
24679
- get messages() {
24680
- return {
24681
- list: (options = {}) => this.sessionDataRequest("/messages", options)
24682
- };
25981
+ /**
25982
+ * Publish one terminal assistant reply from a trusted server integration.
25983
+ *
25984
+ * This uses the API-key-authenticated HTTP session boundary. Delegated
25985
+ * browser sessions cannot use it and never receive assistant feed-authoring
25986
+ * capability through their WebSocket.
25987
+ */
25988
+ async publishAssistantReply(input) {
25989
+ if (this.sessionDataRoutePrefix === "/sdk/browser-sessions") {
25990
+ throw new Error(
25991
+ "Assistant replies require a server API-key session connection."
25992
+ );
25993
+ }
25994
+ const id = requiredAssistantReplyString(input.id, "id");
25995
+ const operationId2 = requiredAssistantReplyString(
25996
+ input.operationId,
25997
+ "operationId"
25998
+ );
25999
+ const text = requiredAssistantReplyString(input.text, "text");
26000
+ return this.sessionDataRequest(
26001
+ "/assistant-replies",
26002
+ void 0,
26003
+ {
26004
+ method: "POST",
26005
+ body: { id, operationId: operationId2, text }
26006
+ }
26007
+ );
24683
26008
  }
24684
26009
  get timeline() {
24685
26010
  return {
@@ -24721,7 +26046,12 @@ var EnvironmentSession = class extends Session {
24721
26046
  latestJob: true
24722
26047
  }),
24723
26048
  get: (artifactId) => this.sessionDataRequest(
24724
- `/artifacts/${encodeURIComponent(artifactId)}`
26049
+ `/artifacts/${encodeURIComponent(artifactId)}`,
26050
+ // The artifact endpoint is polled while an effect is running. Keep
26051
+ // each read addressable as a fresh resource as well as using
26052
+ // `cache: no-store`; this also bypasses intermediaries that ignore
26053
+ // the Fetch cache directive for an otherwise identical GET URL.
26054
+ { _granularLiveRead: Date.now() }
24725
26055
  ),
24726
26056
  create: (artifact) => this.sessionDataRequest(
24727
26057
  "/artifacts",
@@ -24731,6 +26061,14 @@ var EnvironmentSession = class extends Session {
24731
26061
  body: artifact
24732
26062
  }
24733
26063
  ),
26064
+ acceptSuggestion: (sequence) => this.sessionDataRequest(
26065
+ "/artifacts/suggestions/accept",
26066
+ void 0,
26067
+ {
26068
+ method: "POST",
26069
+ body: { sequence }
26070
+ }
26071
+ ),
24734
26072
  updateInputs: (artifactId, patch) => this.sessionDataRequest(
24735
26073
  `/artifacts/${encodeURIComponent(artifactId)}`,
24736
26074
  void 0,
@@ -24739,6 +26077,16 @@ var EnvironmentSession = class extends Session {
24739
26077
  body: patch
24740
26078
  }
24741
26079
  ),
26080
+ relationshipOptions: (artifactId, input) => this.sessionDataRequest(
26081
+ `/artifacts/${encodeURIComponent(artifactId)}/relationship-options`,
26082
+ void 0,
26083
+ { method: "POST", body: input }
26084
+ ),
26085
+ relationshipCreate: (artifactId, input) => this.sessionDataRequest(
26086
+ `/artifacts/${encodeURIComponent(artifactId)}/relationship-create`,
26087
+ void 0,
26088
+ { method: "POST", body: input }
26089
+ ),
24742
26090
  validate: (artifactId) => this.sessionDataRequest(
24743
26091
  `/artifacts/${encodeURIComponent(artifactId)}/validate`,
24744
26092
  void 0,
@@ -24876,73 +26224,50 @@ var EnvironmentSession = class extends Session {
24876
26224
  get transcript() {
24877
26225
  return {
24878
26226
  list: async (options = {}) => {
24879
- const [messages, jobs, entries, lists, artifacts] = await Promise.all([
24880
- this.collectAllSessionItems(this.messages.list),
24881
- this.collectAllSessionItems(
24882
- (pageOptions) => this.jobs.list({ ...pageOptions, status: "all" })
24883
- ),
24884
- this.collectAllSessionItems(this.heap.entries.list),
24885
- this.collectAllSessionItems(this.heap.lists.list),
24886
- this.collectAllSessionItems(
24887
- (pageOptions) => this.artifacts.list({ ...pageOptions, status: "all" })
24888
- )
24889
- ]);
24890
- const liveDoc = {
24891
- conversation: { messages },
24892
- jobs: {
24893
- byId: Object.fromEntries(
24894
- jobs.map((job2) => {
24895
- const record = job2 && typeof job2 === "object" ? job2 : null;
24896
- const id = typeof record?.jobId === "string" ? record.jobId : typeof record?.id === "string" ? record.id : null;
24897
- return id ? [id, record] : null;
24898
- }).filter(
24899
- (entry) => Boolean(entry)
24900
- )
24901
- )
24902
- },
24903
- artifacts: {
24904
- byId: Object.fromEntries(
24905
- artifacts.map((artifact) => {
24906
- return artifact?.artifactId ? [
24907
- artifact.artifactId,
24908
- artifact
24909
- ] : null;
24910
- }).filter(
24911
- (entry) => Boolean(entry)
24912
- )
24913
- ),
24914
- order: artifacts.map((artifact) => artifact?.artifactId).filter(
24915
- (artifactId) => Boolean(artifactId)
24916
- )
26227
+ if (!isCanonicalSessionFeedDocument(this.document)) {
26228
+ throw new Error(
26229
+ "Canonical session feed-v1 is required; transcript.list() does not reconstruct past message or job collections."
26230
+ );
26231
+ }
26232
+ const canonicalFeedItems = [];
26233
+ let afterSequence = 0;
26234
+ let pageCount = 0;
26235
+ for (; ; ) {
26236
+ if (++pageCount > 1e4) {
26237
+ throw new Error(
26238
+ "Canonical transcript history exceeded its page limit."
26239
+ );
24917
26240
  }
24918
- };
24919
- const heap = normalizeHeapSnapshot({
24920
- entriesByPath: Object.fromEntries(
24921
- entries.map((entry) => {
24922
- return entry?.path ? [entry.path, entry] : null;
24923
- }).filter(
24924
- (entry) => Boolean(entry)
24925
- )
24926
- ),
24927
- listsByName: Object.fromEntries(
24928
- lists.map((list) => {
24929
- return list?.name ? [list.name, list] : null;
24930
- }).filter(
24931
- (entry) => Boolean(entry)
24932
- )
24933
- ),
24934
- variablesByName: this.getHeap().variablesByName,
24935
- updatedAt: Date.now()
24936
- });
26241
+ const page = await this.feed.list({
26242
+ afterSequence,
26243
+ limit: 500
26244
+ });
26245
+ if (page.items.length === 0) {
26246
+ if (page.hasMoreAfter) {
26247
+ throw new Error(
26248
+ `Canonical transcript history stopped after sequence ${afterSequence}.`
26249
+ );
26250
+ }
26251
+ break;
26252
+ }
26253
+ if (page.items[0].sequence !== afterSequence + 1) {
26254
+ throw new Error(
26255
+ `Canonical transcript history has a gap after sequence ${afterSequence}.`
26256
+ );
26257
+ }
26258
+ canonicalFeedItems.push(...page.items);
26259
+ afterSequence = page.items[page.items.length - 1].sequence;
26260
+ if (!page.hasMoreAfter) break;
26261
+ }
24937
26262
  const allItems = buildSessionTranscript({
24938
- liveDoc,
24939
- sessionHeap: heap
26263
+ liveDoc: this.document,
26264
+ canonicalFeedItems
24940
26265
  });
24941
26266
  const limit = Math.max(
24942
26267
  1,
24943
26268
  Math.min(500, Math.floor(options.limit ?? 100))
24944
26269
  );
24945
- const offset = typeof options.cursor === "string" ? Number.parseInt(options.cursor, 10) || 0 : 0;
26270
+ const offset = options.latest ? Math.max(0, allItems.length - limit) : typeof options.cursor === "string" ? Number.parseInt(options.cursor, 10) || 0 : 0;
24946
26271
  const items = allItems.slice(offset, offset + limit);
24947
26272
  const nextOffset = offset + items.length;
24948
26273
  return {
@@ -25028,6 +26353,8 @@ var EnvironmentSession = class extends Session {
25028
26353
  * acknowledgement was observed.
25029
26354
  */
25030
26355
  async disconnect() {
26356
+ this.stopHeartbeat();
26357
+ this.disposeSessionFeed();
25031
26358
  let wsNotifiedRuntime = false;
25032
26359
  try {
25033
26360
  const goodbye = await this.rpc(
@@ -25066,6 +26393,7 @@ var EnvironmentSession = class extends Session {
25066
26393
  * Close only the socket transport without sending `client.goodbye`.
25067
26394
  */
25068
26395
  disconnectTransport() {
26396
+ this.stopHeartbeat();
25069
26397
  this.client.disconnect({ reason: "Transport detach" });
25070
26398
  }
25071
26399
  /**
@@ -25530,24 +26858,72 @@ var Granular = class _Granular {
25530
26858
  if (!tagName) {
25531
26859
  throw new Error(`${methodName}() requires \`tag\`.`);
25532
26860
  }
25533
- const user = await this.resolveConnectUser(options);
25534
- if (!Array.isArray(user.permissions) || user.permissions.length === 0) {
26861
+ const assignmentMode = options.assignmentMode ?? "ensure";
26862
+ if (assignmentMode !== "ensure" && assignmentMode !== "require-existing") {
25535
26863
  throw new Error(
25536
- `${methodName}() requires at least one permission so the SDK can ensure assignments for new users.`
26864
+ `${methodName}() received an unsupported assignmentMode: ${String(options.assignmentMode)}.`
25537
26865
  );
25538
26866
  }
25539
- const sandbox = await this.findOrCreateSandbox(ontology);
25540
- for (const profileName of user.permissions) {
25541
- const profileId = await this.ensurePermissionProfile(
25542
- sandbox.sandboxId,
25543
- profileName
26867
+ const requiredGranularId = options.granularId?.trim();
26868
+ if (assignmentMode === "require-existing" && (!requiredGranularId || options.userId || options.user)) {
26869
+ throw new Error(
26870
+ `${methodName}() with assignmentMode "require-existing" requires granularId and does not accept userId or user.`
25544
26871
  );
25545
- await this.ensureAssignment(
25546
- user.granularId,
25547
- sandbox.sandboxId,
25548
- profileId
26872
+ }
26873
+ const user = await this.resolveConnectUser(
26874
+ assignmentMode === "require-existing" ? { ...options, granularId: requiredGranularId } : options
26875
+ );
26876
+ let requiredPermissionProfileId = null;
26877
+ const sandbox = assignmentMode === "require-existing" ? { sandboxId: await this._resolveSandboxId(ontology) } : await this.findOrCreateSandbox(ontology);
26878
+ if (assignmentMode === "require-existing") {
26879
+ const assignments = await this.subjects.listAssignments(user.granularId);
26880
+ const assignment = assignments.items.find(
26881
+ (item) => item.sandboxId === sandbox.sandboxId
25549
26882
  );
26883
+ if (!assignment) {
26884
+ throw new Error(
26885
+ `${methodName}() requires an existing assignment for subject ${user.granularId} in ontology ${sandbox.sandboxId}.`
26886
+ );
26887
+ }
26888
+ const assignedPermissionProfileId = assignment.permissionProfileId?.trim();
26889
+ if (!assignedPermissionProfileId) {
26890
+ throw new Error(
26891
+ `${methodName}() found an invalid assignment without a permission profile for subject ${user.granularId} in ontology ${sandbox.sandboxId}.`
26892
+ );
26893
+ }
26894
+ const expectedPermissionProfileId = options.expectedPermissionProfileId?.trim();
26895
+ if (expectedPermissionProfileId && assignedPermissionProfileId !== expectedPermissionProfileId) {
26896
+ throw new Error(
26897
+ `${methodName}() assignment mismatch for subject ${user.granularId}: expected permission profile ${expectedPermissionProfileId}, received ${assignedPermissionProfileId}.`
26898
+ );
26899
+ }
26900
+ requiredPermissionProfileId = assignedPermissionProfileId;
26901
+ } else {
26902
+ if (!Array.isArray(user.permissions) || user.permissions.length === 0) {
26903
+ throw new Error(
26904
+ `${methodName}() requires at least one permission so the SDK can ensure assignments for new users.`
26905
+ );
26906
+ }
26907
+ for (const profileName of user.permissions) {
26908
+ const profileId = await this.ensurePermissionProfile(
26909
+ sandbox.sandboxId,
26910
+ profileName
26911
+ );
26912
+ await this.ensureAssignment(
26913
+ user.granularId,
26914
+ sandbox.sandboxId,
26915
+ profileId
26916
+ );
26917
+ }
25550
26918
  }
26919
+ const requireMatchingEnvironmentProfile = (environment2) => {
26920
+ if (requiredPermissionProfileId && environment2.permissionProfileId !== requiredPermissionProfileId) {
26921
+ throw new Error(
26922
+ `${methodName}() resolved environment ${environment2.environmentId} with permission profile ${environment2.permissionProfileId || "none"}; current assignment requires ${requiredPermissionProfileId}.`
26923
+ );
26924
+ }
26925
+ return environment2;
26926
+ };
25551
26927
  const tags = await this.request(
25552
26928
  `/control/sandboxes/${sandbox.sandboxId}/tags`
25553
26929
  );
@@ -25570,6 +26946,9 @@ var Granular = class _Granular {
25570
26946
  const resolveActive = async (operationKey) => {
25571
26947
  const query = new URLSearchParams({ tagId: tag2.tagId, slot });
25572
26948
  if (operationKey) query.set("operationKey", operationKey);
26949
+ if (requiredPermissionProfileId) {
26950
+ query.set("expectedPermissionProfileId", requiredPermissionProfileId);
26951
+ }
25573
26952
  try {
25574
26953
  const payload = await this.request(
25575
26954
  `/control/sandboxes/${encodeURIComponent(sandbox.sandboxId)}/subjects/${encodeURIComponent(user.granularId)}/active-environment?${query.toString()}`
@@ -25593,7 +26972,8 @@ var Granular = class _Granular {
25593
26972
  tagId: tag2.tagId,
25594
26973
  slot,
25595
26974
  operationKey: resetKey,
25596
- operation: resetKey ? "explicit_reset" : void 0
26975
+ operation: resetKey ? "explicit_reset" : void 0,
26976
+ expectedPermissionProfileId: requiredPermissionProfileId || void 0
25597
26977
  })
25598
26978
  }
25599
26979
  );
@@ -25603,7 +26983,7 @@ var Granular = class _Granular {
25603
26983
  const resetEnvironment = await resolveActive(resetKey);
25604
26984
  if (resetEnvironment) {
25605
26985
  return {
25606
- environment: resetEnvironment,
26986
+ environment: requireMatchingEnvironmentProfile(resetEnvironment),
25607
26987
  requestedOntology: ontology,
25608
26988
  sandboxId: sandbox.sandboxId,
25609
26989
  subjectId: user.granularId,
@@ -25619,7 +26999,7 @@ var Granular = class _Granular {
25619
26999
  const active = await resolveActive();
25620
27000
  if (active && (active.versionId === targetVersionId || options.createFreshIfOutdated !== true)) {
25621
27001
  return {
25622
- environment: active,
27002
+ environment: requireMatchingEnvironmentProfile(active),
25623
27003
  requestedOntology: ontology,
25624
27004
  sandboxId: sandbox.sandboxId,
25625
27005
  subjectId: user.granularId,
@@ -25637,7 +27017,10 @@ var Granular = class _Granular {
25637
27017
  )
25638
27018
  );
25639
27019
  if (!resetKey && currentMatches.length > 0) {
25640
- const environment2 = await activate(currentMatches[0]);
27020
+ requireMatchingEnvironmentProfile(currentMatches[0]);
27021
+ const environment2 = requireMatchingEnvironmentProfile(
27022
+ await activate(currentMatches[0])
27023
+ );
25641
27024
  return {
25642
27025
  environment: environment2,
25643
27026
  requestedOntology: ontology,
@@ -25650,7 +27033,10 @@ var Granular = class _Granular {
25650
27033
  userEnvironments.filter((environment2) => environment2.tagId === tag2.tagId)
25651
27034
  );
25652
27035
  if (outdatedMatches.length > 0 && options.createFreshIfOutdated !== true) {
25653
- const environment2 = await activate(outdatedMatches[0]);
27036
+ requireMatchingEnvironmentProfile(outdatedMatches[0]);
27037
+ const environment2 = requireMatchingEnvironmentProfile(
27038
+ await activate(outdatedMatches[0])
27039
+ );
25654
27040
  return {
25655
27041
  environment: environment2,
25656
27042
  requestedOntology: ontology,
@@ -25669,7 +27055,10 @@ var Granular = class _Granular {
25669
27055
  tagId: tag2.tagId,
25670
27056
  permissionProfileId: null
25671
27057
  });
25672
- const environment = await activate(created);
27058
+ requireMatchingEnvironmentProfile(created);
27059
+ const environment = requireMatchingEnvironmentProfile(
27060
+ await activate(created)
27061
+ );
25673
27062
  return {
25674
27063
  environment,
25675
27064
  requestedOntology: ontology,
@@ -25769,16 +27158,22 @@ var Granular = class _Granular {
25769
27158
  return this.normalizeUserEnvironmentState(state);
25770
27159
  }
25771
27160
  async markUserEnvironmentSessionsRead(options) {
27161
+ const readThroughSequence = requireUserEnvironmentSequence(
27162
+ options.readThroughSequence,
27163
+ "readThroughSequence"
27164
+ );
25772
27165
  const result = await this.request("/sdk/user-environment-state/read", {
25773
27166
  method: "POST",
25774
27167
  body: JSON.stringify({
25775
27168
  environmentId: options.environmentId,
25776
27169
  sessionId: options.sessionId,
25777
27170
  sessionIds: options.sessionIds,
25778
- readAt: options.readAt
27171
+ readThroughSequence
25779
27172
  })
25780
27173
  });
25781
- return result.readAtBySessionId || {};
27174
+ return normalizeReadThroughSequenceMap(
27175
+ result.readThroughSequenceBySessionId
27176
+ );
25782
27177
  }
25783
27178
  normalizeConversationSession(row) {
25784
27179
  const sessionId = String(row.sessionId ?? row.session_id ?? "");
@@ -25803,21 +27198,48 @@ var Granular = class _Granular {
25803
27198
  };
25804
27199
  }
25805
27200
  normalizeUserEnvironmentState(state) {
27201
+ if (!Array.isArray(state.sessions)) {
27202
+ throw new Error(
27203
+ "Invalid user-environment state: sessions must be an array."
27204
+ );
27205
+ }
27206
+ const sessions = state.sessions.map((item, index) => ({
27207
+ ...item,
27208
+ readThroughSequence: requireUserEnvironmentSequence(
27209
+ item?.readThroughSequence,
27210
+ `sessions[${index}].readThroughSequence`
27211
+ ),
27212
+ messagePreview: {
27213
+ ...item.messagePreview,
27214
+ latestMessageSequence: requireUserEnvironmentSequence(
27215
+ item.messagePreview?.latestMessageSequence,
27216
+ `sessions[${index}].messagePreview.latestMessageSequence`
27217
+ ),
27218
+ latestAssistantSequence: requireUserEnvironmentSequence(
27219
+ item.messagePreview?.latestAssistantSequence,
27220
+ `sessions[${index}].messagePreview.latestAssistantSequence`
27221
+ ),
27222
+ unreadProducingSequence: requireUserEnvironmentSequence(
27223
+ item.messagePreview?.unreadProducingSequence,
27224
+ `sessions[${index}].messagePreview.unreadProducingSequence`
27225
+ )
27226
+ },
27227
+ session: this.normalizeConversationSession(
27228
+ item.session
27229
+ )
27230
+ }));
25806
27231
  return {
25807
27232
  ...state,
25808
- sessions: Array.isArray(state.sessions) ? state.sessions.map((item) => ({
25809
- ...item,
25810
- session: this.normalizeConversationSession(
25811
- item.session
25812
- )
25813
- })) : [],
27233
+ sessions,
25814
27234
  attention: {
25815
27235
  prompts: Array.isArray(state.attention?.prompts) ? state.attention.prompts : [],
25816
27236
  count: typeof state.attention?.count === "number" ? state.attention.count : 0,
25817
27237
  activePrompt: state.attention?.activePrompt || null
25818
27238
  },
25819
27239
  unreadCount: typeof state.unreadCount === "number" ? state.unreadCount : 0,
25820
- readAtBySessionId: state.readAtBySessionId || {}
27240
+ readThroughSequenceBySessionId: normalizeReadThroughSequenceMap(
27241
+ state.readThroughSequenceBySessionId
27242
+ )
25821
27243
  };
25822
27244
  }
25823
27245
  static coerceIsoDate(value) {
@@ -25848,7 +27270,9 @@ var Granular = class _Granular {
25848
27270
  environmentId: options.environmentId,
25849
27271
  clientId,
25850
27272
  sessionScope,
25851
- capabilities: sessionScope ? { sessionScope } : void 0,
27273
+ capabilities: {
27274
+ ...sessionScope ? { sessionScope } : {}
27275
+ },
25852
27276
  initialHeap: options.initialHeap
25853
27277
  })
25854
27278
  });
@@ -25879,22 +27303,16 @@ var Granular = class _Granular {
25879
27303
  metadata: options?.metadata
25880
27304
  });
25881
27305
  }
25882
- /**
25883
- * Mark a session closed in the control plane. If `environment` is the connected handle for that
25884
- * `sessionId`, disconnects the WebSocket so the runtime tears down cleanly.
25885
- */
27306
+ /** End a session through the runtime, or disconnect its connected handle. */
25886
27307
  async closeSession(sessionId, environment) {
25887
27308
  if (environment && environment.sessionId === sessionId) {
25888
27309
  await environment.disconnect();
25889
27310
  return;
25890
27311
  }
25891
- await this.request(`/control/sessions/${encodeURIComponent(sessionId)}`, {
25892
- method: "PATCH",
25893
- body: JSON.stringify({
25894
- status: "closed",
25895
- lastSeenAt: (/* @__PURE__ */ new Date()).toISOString()
25896
- })
25897
- });
27312
+ await this.request(
27313
+ `/control/sessions/${encodeURIComponent(sessionId)}/close`,
27314
+ { method: "POST" }
27315
+ );
25898
27316
  }
25899
27317
  /**
25900
27318
  * Re-open a closed session in the index and connect to its existing runtime document.
@@ -26224,6 +27642,21 @@ var Granular = class _Granular {
26224
27642
  reconnectError
26225
27643
  );
26226
27644
  console.error("[Granular] Original heartbeat failure:", error2);
27645
+ if (this.onReconnectError) {
27646
+ try {
27647
+ this.onReconnectError({
27648
+ sessionId: `effect-host:${host.effectClientId}`,
27649
+ error: reconnectError instanceof Error ? reconnectError.message : String(reconnectError),
27650
+ timestamp: Date.now(),
27651
+ terminal: false
27652
+ });
27653
+ } catch (callbackError) {
27654
+ console.error(
27655
+ "[Granular] onReconnectError callback failed after effect-host recovery failure:",
27656
+ callbackError
27657
+ );
27658
+ }
27659
+ }
26227
27660
  }
26228
27661
  );
26229
27662
  }
@@ -26326,7 +27759,13 @@ var Granular = class _Granular {
26326
27759
  const request = params;
26327
27760
  return invokeRegisteredEffect(
26328
27761
  this.getSandboxEffectMap(sandboxId),
26329
- request
27762
+ request,
27763
+ {
27764
+ feedback: {
27765
+ invocationId: request.callId,
27766
+ publish: (method, publishParams) => wsClient.call(method, publishParams)
27767
+ }
27768
+ }
26330
27769
  );
26331
27770
  });
26332
27771
  wsClient.on("open", () => {
@@ -26340,14 +27779,20 @@ var Granular = class _Granular {
26340
27779
  wsClient.on("disconnect", () => {
26341
27780
  this.stopEffectHostHeartbeat(host);
26342
27781
  });
26343
- await withTimeout(
26344
- wsClient.connect(),
26345
- EFFECT_HOST_CONNECT_TIMEOUT_MS,
26346
- `effect host WebSocket connect for sandbox ${sandboxId}`
26347
- );
26348
- await this.synchronizeEffectHost(host);
26349
- this.sandboxEffectHosts.set(sandboxId, host);
26350
- return host;
27782
+ try {
27783
+ await withTimeout(
27784
+ wsClient.connect(),
27785
+ EFFECT_HOST_CONNECT_TIMEOUT_MS,
27786
+ `effect host WebSocket connect for sandbox ${sandboxId}`
27787
+ );
27788
+ await this.synchronizeEffectHost(host);
27789
+ this.sandboxEffectHosts.set(sandboxId, host);
27790
+ return host;
27791
+ } catch (error2) {
27792
+ this.stopEffectHostHeartbeat(host);
27793
+ wsClient.disconnect({ reason: "Effect host initialization failed" });
27794
+ throw error2;
27795
+ }
26351
27796
  })();
26352
27797
  this.sandboxEffectHostPromises.set(sandboxId, connectPromise);
26353
27798
  try {
@@ -28104,7 +29549,11 @@ async function tagMoveCommand(tagName, versionId) {
28104
29549
  process.exit(1);
28105
29550
  }
28106
29551
  const previousVersionId = tag2.targetVersionId || tag2.targetBuildId || null;
28107
- const moved = await api.moveVersionTag(tag2.tagId, versionId);
29552
+ const moved = await api.moveVersionTag(
29553
+ tag2.tagId,
29554
+ versionId,
29555
+ tag2.name === "prod" ? previousVersionId : void 0
29556
+ );
28108
29557
  spin.succeed(` ${tagName} now points to ${versionId}.`);
28109
29558
  console.log();
28110
29559
  keyValue({