@granular-software/sdk 0.4.58 → 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
@@ -16833,12 +16833,13 @@ ${effectMetamodelTable}
16833
16833
  | API | Purpose |
16834
16834
  |-----|---------|
16835
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\`. |
16836
- | \`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. |
16837
16837
  | \`getDomain()\`, \`getDomainTypes()\`, \`getDomainDocs()\`, \`getDomainDocumentation()\` | Domain summary and generated TypeScript / docs. |
16838
16838
  | \`getEffects()\` / \`getTools()\`, \`session.on("effects:changed", ...)\` | Effect catalog and live updates. |
16839
16839
  | \`checkReadiness()\`, \`on('readiness', ...)\` | Runtime readiness. |
16840
16840
  | \`document\`, \`getHeap()\` | Current live session state from the WebSocket. |
16841
- | \`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. |
16842
16843
  | \`disconnect()\` | Close the live runtime session. |
16843
16844
  | \`rpc(method, params)\` | Low-level session RPC (advanced). |
16844
16845
  | \`disconnect()\` | End the session. |
@@ -19530,6 +19531,1257 @@ async function simulateCommand(sandboxIdArg, options) {
19530
19531
  info(`Opening simulator: ${url}`);
19531
19532
  openUrl(url);
19532
19533
  }
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;
19541
+ }
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);
19563
+ }
19564
+ return [...byId.values()].sort((left, right) => left.ordinal - right.ordinal);
19565
+ }
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);
19728
+ default:
19729
+ return true;
19730
+ }
19731
+ }
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;
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);
19790
+ }
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
+ };
19801
+ }
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;
19810
+ }
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);
19822
+ }
19823
+ }
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
+ });
19839
+ }
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;
19875
+ }
19876
+ expected += 1;
19877
+ }
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.`);
19915
+ }
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.");
19936
+ }
19937
+ }
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
+ });
20026
+ }
20027
+ if (needsRepair) {
20028
+ this.startRepair(this.snapshot.lastSequence);
20029
+ }
20030
+ }
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;
20038
+ }
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);
20046
+ }
20047
+ }
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
+ );
20071
+ }
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 });
20099
+ }
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
19533
20785
  var GlobalWebSocket = void 0;
19534
20786
  if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
19535
20787
  GlobalWebSocket = globalThis.WebSocket;
@@ -19543,8 +20795,42 @@ var DEFAULT_RPC_TIMEOUT_MS = 3e4;
19543
20795
  var DOMAIN_PACKAGE_RPC_TIMEOUT_MS = 12e4;
19544
20796
  var EFFECT_CONTROL_RPC_TIMEOUT_MS = 12e4;
19545
20797
  var HARNESS_RUN_RPC_TIMEOUT_MS = 6e5;
20798
+ var HEARTBEAT_RPC_TIMEOUT_MS = 15e3;
20799
+ var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
19546
20800
  var DEFAULT_RECONNECT_DELAY_MS = 3e3;
19547
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
+ }
19548
20834
  function debugWs(...args) {
19549
20835
  if (DEBUG_WS) {
19550
20836
  console.log(...args);
@@ -19556,6 +20842,7 @@ function rpcTimeoutMsForMethod(method) {
19556
20842
  case "domain.getSummary":
19557
20843
  return DOMAIN_PACKAGE_RPC_TIMEOUT_MS;
19558
20844
  case "client.heartbeat":
20845
+ return HEARTBEAT_RPC_TIMEOUT_MS;
19559
20846
  case "effects.publishCatalog":
19560
20847
  case "effects.resetCatalog":
19561
20848
  case "effects.addCatalog":
@@ -19575,12 +20862,11 @@ var WSClient = class {
19575
20862
  sessionId;
19576
20863
  token;
19577
20864
  messageQueue = [];
19578
- syncHandlers = [];
19579
20865
  rpcHandlers = /* @__PURE__ */ new Map();
19580
20866
  eventHandlers = /* @__PURE__ */ new Map();
20867
+ canonicalDocumentQuarantineHandlers = /* @__PURE__ */ new Set();
19581
20868
  nextRpcId = 1;
19582
20869
  doc = Automerge__namespace.init();
19583
- syncState = Automerge__namespace.initSyncState();
19584
20870
  reconnectTimer = null;
19585
20871
  tokenRefreshTimer = null;
19586
20872
  isExplicitlyDisconnected = false;
@@ -19588,6 +20874,8 @@ var WSClient = class {
19588
20874
  connectPromise = null;
19589
20875
  connectionEpoch = 0;
19590
20876
  cancelConnectAttempt = null;
20877
+ documentRepairRequired = false;
20878
+ canonicalDocumentActivated = false;
19591
20879
  options;
19592
20880
  constructor(options) {
19593
20881
  this.options = options;
@@ -19606,8 +20894,12 @@ var WSClient = class {
19606
20894
  return;
19607
20895
  }
19608
20896
  try {
19609
- this.doc = document instanceof Uint8Array ? Automerge__namespace.load(document) : Automerge__namespace.from(document);
19610
- this.syncState = Automerge__namespace.initSyncState();
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);
19611
20903
  this.emit("sync", this.doc);
19612
20904
  } catch (error2) {
19613
20905
  console.warn("[Granular] Failed to seed cached session document", error2);
@@ -19738,6 +21030,26 @@ var WSClient = class {
19738
21030
  }
19739
21031
  }
19740
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
+ }
19741
21053
  async connectAttempt(signal) {
19742
21054
  if (signal?.aborted) throw new Error("WebSocket connect aborted");
19743
21055
  const token = await this.resolveTokenForConnect();
@@ -19769,10 +21081,17 @@ var WSClient = class {
19769
21081
  this.ws = socket;
19770
21082
  return new Promise((resolve2, reject) => {
19771
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;
19772
21087
  const isCurrent = () => this.connectionEpoch === epoch && this.ws === socket;
19773
21088
  const finish = (error2) => {
19774
21089
  if (settled) return;
19775
21090
  settled = true;
21091
+ if (connectTimeout) {
21092
+ clearTimeout(connectTimeout);
21093
+ connectTimeout = null;
21094
+ }
19776
21095
  if (this.cancelConnectAttempt === handleAbort) {
19777
21096
  this.cancelConnectAttempt = null;
19778
21097
  }
@@ -19837,6 +21156,17 @@ var WSClient = class {
19837
21156
  wasClean: close.wasClean
19838
21157
  });
19839
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);
19840
21170
  signal?.addEventListener("abort", handleAbort, { once: true });
19841
21171
  const nodeSocket = socket;
19842
21172
  if (typeof nodeSocket.on === "function") {
@@ -19875,11 +21205,12 @@ var WSClient = class {
19875
21205
  });
19876
21206
  this.messageQueue = [];
19877
21207
  }
19878
- emitReconnectErrorMessage(error2) {
21208
+ emitReconnectErrorMessage(error2, terminal2 = false) {
19879
21209
  const reconnectInfo = {
19880
21210
  error: error2,
19881
21211
  sessionId: this.sessionId,
19882
- timestamp: Date.now()
21212
+ timestamp: Date.now(),
21213
+ terminal: terminal2
19883
21214
  };
19884
21215
  this.emit("reconnect_error", reconnectInfo);
19885
21216
  if (this.options.onReconnectError) {
@@ -19899,7 +21230,8 @@ var WSClient = class {
19899
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;
19900
21231
  if (this.reconnectAttempts >= maxReconnectAttempts) {
19901
21232
  this.emitReconnectErrorMessage(
19902
- `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`
21233
+ `WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`,
21234
+ true
19903
21235
  );
19904
21236
  return null;
19905
21237
  }
@@ -19929,6 +21261,190 @@ var WSClient = class {
19929
21261
  const suffix = details ? ` (${details})` : "";
19930
21262
  return new Error(`WebSocket disconnected${suffix}`);
19931
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
+ }
19932
21448
  handleDisconnect(close = {}) {
19933
21449
  const unexpected = !this.isExplicitlyDisconnected;
19934
21450
  const info2 = {
@@ -19947,11 +21463,11 @@ var WSClient = class {
19947
21463
  }
19948
21464
  if (unexpected) {
19949
21465
  const disconnectError = this.buildDisconnectError(info2);
19950
- this.rejectPending(disconnectError);
19951
- this.emit("disconnect", info2);
19952
21466
  const reconnectDelayMs = this.scheduleReconnectAttempt();
19953
21467
  info2.reconnectScheduled = reconnectDelayMs !== null;
19954
21468
  if (reconnectDelayMs !== null) info2.reconnectDelayMs = reconnectDelayMs;
21469
+ this.rejectPending(disconnectError);
21470
+ this.emit("disconnect", info2);
19955
21471
  if (this.options.onUnexpectedClose) {
19956
21472
  try {
19957
21473
  this.options.onUnexpectedClose(info2);
@@ -19971,100 +21487,109 @@ var WSClient = class {
19971
21487
  JSON.stringify(message).slice(0, 500)
19972
21488
  );
19973
21489
  if ("type" in message && message.type === "sync") {
19974
- const syncMessage = message;
19975
- 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;
19976
21498
  try {
19977
- const payload = syncMessage.message || syncMessage.data;
19978
- if (typeof payload === "string") {
19979
- const binaryString = atob(payload);
19980
- const len = binaryString.length;
19981
- bytes = new Uint8Array(len);
19982
- for (let i = 0; i < len; i++) {
19983
- bytes[i] = binaryString.charCodeAt(i);
19984
- }
19985
- } else if (Array.isArray(payload)) {
19986
- bytes = new Uint8Array(payload);
19987
- } else if (payload instanceof Uint8Array) {
19988
- bytes = payload;
19989
- } else {
19990
- return;
19991
- }
19992
- debugWs("[Granular DEBUG] Applying sync bytes:", bytes.length);
19993
- const [newDoc, newSyncState] = Automerge__namespace.receiveSyncMessage(
19994
- this.doc,
19995
- this.syncState,
19996
- bytes
19997
- );
19998
- this.doc = newDoc;
19999
- this.syncState = newSyncState;
20000
- const docAny = this.doc;
20001
- if (docAny.catalog) {
20002
- debugWs(
20003
- "[Granular DEBUG] Doc catalog sync applied. Keys in catalog:",
20004
- Object.keys(docAny.catalog || {})
20005
- );
20006
- debugWs(
20007
- "[Granular DEBUG] RawToolCatalogs:",
20008
- Object.keys(docAny.catalog.rawToolCatalogs || {})
20009
- );
20010
- } else {
20011
- debugWs(
20012
- "[Granular DEBUG] Doc synced but no catalog yet. Keys in doc:",
20013
- Object.keys(docAny)
20014
- );
20015
- }
20016
- this.emit("sync", this.doc);
20017
- } catch (e) {
20018
- try {
20019
- debugWs(
20020
- "[Granular DEBUG] receiveSyncMessage failed, trying applyChanges..."
20021
- );
20022
- const [newDoc] = Automerge__namespace.applyChanges(this.doc, [bytes]);
20023
- this.doc = newDoc;
20024
- this.emit("sync", this.doc);
20025
- debugWs(
20026
- "[Granular DEBUG] applyChanges succeeded. Doc:",
20027
- JSON.stringify(Automerge__namespace.toJS(this.doc))
20028
- );
20029
- } catch (applyError) {
20030
- console.warn(
20031
- "[Granular] Failed to apply sync message (both sync & applyChanges)",
20032
- e,
20033
- applyError
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");
21501
+ }
21502
+ const replacement = Automerge__namespace.load(
21503
+ this.decodeDocumentBytes(
21504
+ resetMessage.data,
21505
+ "Automerge snapshot_reset"
21506
+ )
21507
+ );
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"
20034
21514
  );
20035
21515
  }
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;
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);
20036
21535
  }
20037
21536
  return;
20038
21537
  }
20039
21538
  if ("type" in message && message.type === "snapshot") {
20040
21539
  const snapshotMessage = message;
20041
21540
  try {
20042
- const bytes = new Uint8Array(snapshotMessage.data);
21541
+ const bytes = this.decodeDocumentBytes(
21542
+ snapshotMessage.data,
21543
+ "Automerge snapshot"
21544
+ );
20043
21545
  debugWs(
20044
21546
  "[Granular DEBUG] Loading Automerge session snapshot bytes:",
20045
21547
  bytes.length
20046
21548
  );
20047
- 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);
20048
21562
  this.emit("sync", this.doc);
20049
21563
  debugWs(
20050
21564
  "[Granular DEBUG] Automerge session snapshot loaded. Doc:",
20051
21565
  JSON.stringify(Automerge__namespace.toJS(this.doc))
20052
21566
  );
20053
- } catch (e) {
20054
- console.warn("[Granular] Failed to load snapshot message", e);
21567
+ } catch (error2) {
21568
+ this.requireDocumentResync("Automerge snapshot", error2);
20055
21569
  }
20056
21570
  return;
20057
21571
  }
20058
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
+ }
20059
21579
  const changeMessage = message;
20060
21580
  try {
20061
- const bytes = new Uint8Array(changeMessage.data);
21581
+ const bytes = this.decodeDocumentBytes(
21582
+ changeMessage.data,
21583
+ "Automerge change"
21584
+ );
20062
21585
  const [newDoc] = Automerge__namespace.applyChanges(this.doc, [bytes]);
21586
+ this.assertIncrementalDocumentIsSafe(newDoc);
20063
21587
  this.doc = newDoc;
21588
+ this.rememberCanonicalActivation(newDoc);
20064
21589
  this.emit("change", changeMessage);
20065
21590
  this.emit("sync", this.doc);
20066
- } catch (e) {
20067
- console.warn("[Granular] Failed to apply change message", e);
21591
+ } catch (error2) {
21592
+ this.requireDocumentResync("Automerge change message", error2);
20068
21593
  }
20069
21594
  return;
20070
21595
  }
@@ -20183,6 +21708,18 @@ var WSClient = class {
20183
21708
  }
20184
21709
  this.eventHandlers.get(event).push(handler);
20185
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
+ }
20186
21723
  /**
20187
21724
  * Register an RPC handler for incoming server requests
20188
21725
  * @param {string} method - RPC method name
@@ -20240,7 +21777,7 @@ var WSClient = class {
20240
21777
  };
20241
21778
 
20242
21779
  // src/prompt-utils.ts
20243
- function asRecord(value) {
21780
+ function asRecord2(value) {
20244
21781
  if (!value || typeof value !== "object" || Array.isArray(value)) return null;
20245
21782
  return value;
20246
21783
  }
@@ -20255,7 +21792,7 @@ function parseJsonPromptChoiceOption(option) {
20255
21792
  if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
20256
21793
  try {
20257
21794
  const parsed = JSON.parse(trimmed);
20258
- return asRecord(parsed);
21795
+ return asRecord2(parsed);
20259
21796
  } catch {
20260
21797
  return null;
20261
21798
  }
@@ -20317,33 +21854,37 @@ function normalizePromptType(raw) {
20317
21854
  if (kind === "confirm" || kind === "choice" || kind === "input") return kind;
20318
21855
  if (promptType === "confirm" || promptType === "choice" || promptType === "input")
20319
21856
  return promptType;
20320
- return "input";
21857
+ return null;
20321
21858
  }
20322
21859
  function normalizePrompt(rawValue) {
20323
- const raw = asRecord(rawValue);
21860
+ const raw = asRecord2(rawValue);
20324
21861
  if (!raw) return null;
20325
- const promptRecord = asRecord(raw.prompt);
21862
+ const promptRecord = asRecord2(raw.prompt);
20326
21863
  const source = promptRecord || raw;
20327
21864
  const id = typeof source.id === "string" ? source.id : typeof raw.id === "string" ? raw.id : typeof raw.promptId === "string" ? raw.promptId : "";
20328
21865
  if (!id) return null;
20329
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;
20330
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;
20331
21872
  return {
20332
21873
  id,
20333
21874
  ...jobId ? { jobId } : {},
20334
21875
  ...turnId ? { turnId } : {},
20335
- type: normalizePromptType(source === raw ? raw : { ...raw, ...source }),
21876
+ type,
20336
21877
  title: typeof source.title === "string" ? source.title : "Input required",
20337
21878
  message: typeof source.message === "string" ? source.message : "",
20338
21879
  options: Array.isArray(source.options) ? source.options.map(
20339
- (option) => typeof option === "string" || asRecord(option) ? normalizePromptChoiceOption(
21880
+ (option) => typeof option === "string" || asRecord2(option) ? normalizePromptChoiceOption(
20340
21881
  option
20341
21882
  ) : option
20342
21883
  ) : void 0,
20343
21884
  defaultValue: source.defaultValue,
20344
21885
  placeholder: typeof source.placeholder === "string" ? source.placeholder : void 0,
20345
21886
  allowEmpty: typeof source.allowEmpty === "boolean" ? source.allowEmpty : void 0,
20346
- metadata: asRecord(source.metadata) || void 0
21887
+ metadata: asRecord2(source.metadata) || void 0
20347
21888
  };
20348
21889
  }
20349
21890
  function resolvePromptAnswer(prompt3, answer) {
@@ -20371,23 +21912,36 @@ function resolvePromptAnswer(prompt3, answer) {
20371
21912
  }
20372
21913
 
20373
21914
  // src/session.ts
20374
- var PROMPT_TRANSCRIPT_APPEND_TIMEOUT_MS = 5e3;
20375
21915
  function toPascalCase3(value) {
20376
21916
  return value.split(/[_:\-\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
20377
21917
  }
20378
- function withPromptTranscriptTimeout(promise) {
20379
- let timeout = null;
20380
- return Promise.race([
20381
- promise,
20382
- new Promise((_, reject) => {
20383
- timeout = setTimeout(() => {
20384
- reject(new Error("Timed out appending prompt answer transcript."));
20385
- }, PROMPT_TRANSCRIPT_APPEND_TIMEOUT_MS);
20386
- })
20387
- ]).finally(() => {
20388
- if (timeout) {
20389
- clearTimeout(timeout);
20390
- }
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
20391
21945
  });
20392
21946
  }
20393
21947
  var Session = class {
@@ -20395,7 +21949,6 @@ var Session = class {
20395
21949
  clientId;
20396
21950
  initialQuota;
20397
21951
  jobsMap = /* @__PURE__ */ new Map();
20398
- pendingAgentMessagesByJobId = /* @__PURE__ */ new Map();
20399
21952
  eventListeners = /* @__PURE__ */ new Map();
20400
21953
  toolHandlers = /* @__PURE__ */ new Map();
20401
21954
  /** Tracks which tools are instance methods (className set, not static) */
@@ -20412,10 +21965,23 @@ var Session = class {
20412
21965
  domainPackagePartCache = /* @__PURE__ */ new Map();
20413
21966
  domainPackagePartPromises = /* @__PURE__ */ new Map();
20414
21967
  domainPackageFetchQueue = Promise.resolve();
21968
+ feedController;
21969
+ feed;
20415
21970
  constructor(client, clientId, options = {}) {
20416
21971
  this.client = client;
20417
21972
  this.clientId = clientId || `client_${Date.now()}`;
20418
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
+ });
20419
21985
  this.setupEventHandlers();
20420
21986
  this.setupToolInvokeHandler();
20421
21987
  this.currentDomainRevision = this.extractDomainRevisionFromDoc(
@@ -20434,34 +22000,160 @@ var Session = class {
20434
22000
  }
20435
22001
  return null;
20436
22002
  }
20437
- 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) {
20438
22050
  return {
20439
22051
  effectClientId: this.clientId,
20440
- sandboxId: "",
20441
- environmentId: "",
20442
- sessionId: "",
22052
+ sandboxId: params.sandboxId || "",
22053
+ environmentId: params.environmentId || "",
22054
+ invocationId: params.callId,
22055
+ jobId: params.jobId,
22056
+ sessionId: params.sessionId || this.client.currentSessionId,
20443
22057
  user: {
20444
22058
  granularId: "",
20445
22059
  userId: "",
20446
22060
  subjectId: ""
20447
- }
22061
+ },
22062
+ ...feedbackContext ? {
22063
+ feedback: feedbackContext.feedback,
22064
+ transientFeedback: feedbackContext.transientFeedback
22065
+ } : {}
20448
22066
  };
20449
22067
  }
20450
- stringifyConversationValue(value) {
20451
- if (typeof value === "string") {
20452
- return value;
20453
- }
20454
- if (typeof value === "boolean") {
20455
- return value ? "Confirmed" : "Canceled";
20456
- }
20457
- if (value === void 0) {
20458
- return "";
20459
- }
20460
- try {
20461
- return JSON.stringify(value, null, 2);
20462
- } catch {
20463
- return String(value);
20464
- }
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
+ };
20465
22157
  }
20466
22158
  // --- Public API ---
20467
22159
  get document() {
@@ -20593,15 +22285,6 @@ var Session = class {
20593
22285
  createdAt: Date.now()
20594
22286
  });
20595
22287
  this.jobsMap.set(result.jobId, job2);
20596
- const pendingAgentMessages = this.pendingAgentMessagesByJobId.get(
20597
- result.jobId
20598
- );
20599
- if (pendingAgentMessages && pendingAgentMessages.length > 0) {
20600
- this.pendingAgentMessagesByJobId.delete(result.jobId);
20601
- for (const message of pendingAgentMessages) {
20602
- job2.replayAgentMessage(message);
20603
- }
20604
- }
20605
22288
  return job2;
20606
22289
  }
20607
22290
  /**
@@ -20622,50 +22305,44 @@ var Session = class {
20622
22305
  async answerPrompt(promptId, answer) {
20623
22306
  const prompt3 = this.promptCache.get(promptId);
20624
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
+ }
20625
22318
  this.promptCache.delete(promptId);
20626
22319
  this.hiddenPromptIds.add(promptId);
20627
22320
  this.emit("prompt:answered", {
20628
22321
  ...prompt3 || { id: promptId },
20629
22322
  id: promptId,
22323
+ answer: resolvedAnswer,
20630
22324
  status: "answered"
20631
22325
  });
20632
- try {
20633
- const response = await this.client.call("prompt.answer", {
20634
- promptId,
20635
- answer: resolvedAnswer,
20636
- value: resolvedAnswer
20637
- });
20638
- if (response && typeof response === "object" && "ok" in response && response.ok === false) {
20639
- const rejected = response;
20640
- const errorMessage = typeof rejected.error === "string" ? rejected.error : "Prompt answer was rejected.";
20641
- throw new Error(errorMessage);
20642
- }
20643
- } catch (error2) {
20644
- this.hiddenPromptIds.delete(promptId);
20645
- if (prompt3) {
20646
- this.promptCache.set(promptId, prompt3);
20647
- }
20648
- throw error2;
20649
- }
20650
- try {
20651
- const content = this.stringifyConversationValue(resolvedAnswer);
20652
- if (content.trim()) {
20653
- await withPromptTranscriptTimeout(
20654
- this.appendConversationMessage({
20655
- role: "user",
20656
- content,
20657
- promptId
20658
- })
20659
- );
20660
- }
20661
- } catch {
20662
- }
20663
22326
  }
20664
- async appendConversationMessage(input) {
20665
- return this.client.call(
20666
- "conversation.append",
20667
- input
22327
+ async appendUserMessage(input) {
22328
+ const requestedId = normalizeUserMessageIdentity(input.id, "id");
22329
+ const requestedOperationId = normalizeUserMessageIdentity(
22330
+ input.operationId,
22331
+ "operationId"
20668
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
+ });
20669
22346
  }
20670
22347
  /**
20671
22348
  * Get the current list of available effects.
@@ -21048,6 +22725,7 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21048
22725
  * Close the session and disconnect from the sandbox
21049
22726
  */
21050
22727
  async disconnect() {
22728
+ this.disposeSessionFeed();
21051
22729
  try {
21052
22730
  await this.client.call("client.goodbye", {
21053
22731
  clientId: this.clientId,
@@ -21057,6 +22735,10 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21057
22735
  }
21058
22736
  this.client.disconnect();
21059
22737
  }
22738
+ /** Stop feed repair work without detaching a reconnectable transport. */
22739
+ disposeSessionFeed() {
22740
+ this.feedController.dispose();
22741
+ }
21060
22742
  // --- Event Handling ---
21061
22743
  /**
21062
22744
  * Subscribe to session events
@@ -21081,9 +22763,14 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21081
22763
  }
21082
22764
  }
21083
22765
  // --- Internal ---
22766
+ setFeedListTransport(transport) {
22767
+ this.feedController.setListTransport(transport);
22768
+ }
21084
22769
  setupToolInvokeHandler() {
21085
22770
  this.client.registerRpcHandler("tool.invoke", async (params) => {
21086
- const { callId, toolName, input } = params;
22771
+ const invocation = params;
22772
+ const { callId, toolName, input, feedbackCapability } = invocation;
22773
+ const capabilityResultParams = feedbackCapability ? { feedbackCapability } : {};
21087
22774
  this.emit("effect:invoke", {
21088
22775
  callId,
21089
22776
  effectKey: toolName,
@@ -21095,6 +22782,7 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21095
22782
  if (!handler) {
21096
22783
  await this.client.call("tool.result", {
21097
22784
  callId,
22785
+ ...capabilityResultParams,
21098
22786
  error: {
21099
22787
  code: "TOOL_NOT_FOUND",
21100
22788
  message: `Tool handler not found: ${toolName}`
@@ -21104,21 +22792,45 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21104
22792
  }
21105
22793
  try {
21106
22794
  let result;
21107
- const invocationContext = this.buildLegacyEffectContext();
21108
- if (this.instanceTools.has(toolName) && input && typeof input === "object" && "_objectId" in input) {
21109
- const { _objectId, ...restParams } = input;
21110
- result = await handler(
21111
- _objectId,
21112
- restParams,
21113
- invocationContext
21114
- );
21115
- } else {
21116
- 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;
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
+ }
21117
22826
  }
22827
+ if (handlerFailed) throw handlerError;
22828
+ if (feedbackFailed) throw feedbackError;
21118
22829
  this.emit("effect:result", { callId, effectKey: toolName, result });
21119
22830
  this.emit("tool:result", { callId, result });
21120
22831
  await this.client.call("tool.result", {
21121
22832
  callId,
22833
+ ...capabilityResultParams,
21122
22834
  result
21123
22835
  });
21124
22836
  } catch (error2) {
@@ -21131,6 +22843,7 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21131
22843
  this.emit("tool:result", { callId, error: errorMessage });
21132
22844
  await this.client.call("tool.result", {
21133
22845
  callId,
22846
+ ...capabilityResultParams,
21134
22847
  error: { code: "TOOL_EXECUTION_FAILED", message: errorMessage }
21135
22848
  });
21136
22849
  }
@@ -21143,9 +22856,14 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21143
22856
  );
21144
22857
  this.client.on("sync", (doc) => {
21145
22858
  this.currentDomainRevision = this.extractDomainRevisionFromDoc(doc);
22859
+ this.feedController.updateDocument(doc);
22860
+ this.reconcilePromptCacheFromCanonicalDocument(doc);
21146
22861
  this.emit("sync", doc);
21147
22862
  this.checkForToolChanges();
21148
22863
  });
22864
+ this.client.onCanonicalDocumentQuarantine?.((quarantine) => {
22865
+ this.feedController.quarantineCanonicalReplacement(quarantine);
22866
+ });
21149
22867
  const emitPrompt = (payload) => {
21150
22868
  const prompt3 = normalizePrompt(payload);
21151
22869
  if (!prompt3) return;
@@ -21178,23 +22896,6 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
21178
22896
  this.client.on("harness.text_response.delta", (data) => {
21179
22897
  this.emit("harness:text_response_delta", data);
21180
22898
  });
21181
- this.client.on("job.agent_message", (data) => {
21182
- const normalized = normalizeJobAgentMessageEnvelope(data);
21183
- if (!normalized) return;
21184
- this.emit("job:agent_message", normalized);
21185
- if (this.jobsMap.has(normalized.jobId)) return;
21186
- const pending = this.pendingAgentMessagesByJobId.get(normalized.jobId) || [];
21187
- if (normalized.message.messageId && pending.some(
21188
- (message) => message.messageId === normalized.message.messageId
21189
- )) {
21190
- return;
21191
- }
21192
- pending.push(normalized.message);
21193
- this.pendingAgentMessagesByJobId.set(
21194
- normalized.jobId,
21195
- pending.slice(-25)
21196
- );
21197
- });
21198
22899
  this.client.on("exec.completed", (data) => {
21199
22900
  this.emit("exec:completed", data);
21200
22901
  });
@@ -21319,24 +23020,6 @@ function sanitizeFeedbackValue(value, depth = 0, seen = /* @__PURE__ */ new Weak
21319
23020
  }
21320
23021
  return truncateFeedbackString(String(value));
21321
23022
  }
21322
- function normalizeJobAgentMessageEnvelope(data) {
21323
- const d = data;
21324
- if (typeof d?.jobId !== "string" || !d.jobId) {
21325
- return null;
21326
- }
21327
- return {
21328
- jobId: d.jobId,
21329
- ...typeof d.turnId === "string" && d.turnId.trim() ? { turnId: d.turnId.trim() } : {},
21330
- message: {
21331
- messageId: d.messageId,
21332
- kind: d.kind === "artifacts" ? "artifacts" : "text",
21333
- reply: typeof d.reply === "string" ? d.reply : "",
21334
- show: d.show,
21335
- actions: Array.isArray(d.actions) ? d.actions : void 0,
21336
- timestamp: d.timestamp || Date.now()
21337
- }
21338
- };
21339
- }
21340
23023
  var JobImplementation = class {
21341
23024
  id;
21342
23025
  client;
@@ -21345,8 +23028,6 @@ var JobImplementation = class {
21345
23028
  _resolveResult;
21346
23029
  _rejectResult;
21347
23030
  eventListeners = /* @__PURE__ */ new Map();
21348
- bufferedAgentMessages = [];
21349
- bufferedAgentMessageIds = /* @__PURE__ */ new Set();
21350
23031
  resultSettled = false;
21351
23032
  metadata;
21352
23033
  constructor(id, client, initialState) {
@@ -21506,12 +23187,6 @@ var JobImplementation = class {
21506
23187
  });
21507
23188
  }
21508
23189
  });
21509
- this.client.on("job.agent_message", (data) => {
21510
- const normalized = normalizeJobAgentMessageEnvelope(data);
21511
- if (normalized?.jobId === id) {
21512
- this.captureAgentMessage(normalized.message);
21513
- }
21514
- });
21515
23190
  }
21516
23191
  get result() {
21517
23192
  return this._resultPromise;
@@ -21536,11 +23211,6 @@ var JobImplementation = class {
21536
23211
  this.eventListeners.set(event, []);
21537
23212
  }
21538
23213
  this.eventListeners.get(event).push(handler);
21539
- if (event === "agentMessage" && this.bufferedAgentMessages.length > 0) {
21540
- for (const message of this.bufferedAgentMessages) {
21541
- handler(message);
21542
- }
21543
- }
21544
23214
  return () => {
21545
23215
  const handlers = this.eventListeners.get(event);
21546
23216
  if (!handlers) {
@@ -21552,9 +23222,6 @@ var JobImplementation = class {
21552
23222
  );
21553
23223
  };
21554
23224
  }
21555
- replayAgentMessage(message) {
21556
- this.captureAgentMessage(message);
21557
- }
21558
23225
  buildFeedbackMetadata() {
21559
23226
  const startedAt = this.metadata.startedAt;
21560
23227
  const completedAt = this.metadata.completedAt;
@@ -21627,317 +23294,11 @@ var JobImplementation = class {
21627
23294
  handlers.forEach((h) => h(data));
21628
23295
  }
21629
23296
  }
21630
- captureAgentMessage(message) {
21631
- if (message.messageId && this.bufferedAgentMessageIds.has(message.messageId)) {
21632
- return;
21633
- }
21634
- if (message.messageId) {
21635
- this.bufferedAgentMessageIds.add(message.messageId);
21636
- }
21637
- this.bufferedAgentMessages = [...this.bufferedAgentMessages, message].slice(
21638
- -25
21639
- );
21640
- this.emit("agentMessage", message);
21641
- }
21642
23297
  };
21643
23298
 
21644
- // src/job-presentation.ts
21645
- var RESPONSE_KEYS = [
21646
- "reply",
21647
- "response",
21648
- "text",
21649
- "message",
21650
- "summary",
21651
- "answer"
21652
- ];
21653
- var ENTRY_KEY_CANDIDATES = ["entryPath", "path"];
21654
- var ENTRY_ARRAY_KEY_CANDIDATES = ["entryPaths", "paths"];
21655
- var LIST_KEY_CANDIDATES = ["listName"];
21656
- var LIST_ARRAY_KEY_CANDIDATES = ["listNames"];
21657
- var VARIABLE_KEY_CANDIDATES = ["variableName"];
21658
- var VARIABLE_ARRAY_KEY_CANDIDATES = ["variableNames"];
21659
- var UI_CONTAINER_KEYS = ["show", "display", "present", "ui"];
21660
- function asRecord2(value) {
21661
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
21662
- return value;
21663
- }
21664
- function normalizeText(value) {
21665
- if (typeof value !== "string") return null;
21666
- const trimmed = value.trim();
21667
- if (!trimmed) return null;
21668
- if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
21669
- return null;
21670
- }
21671
- return trimmed;
21672
- }
21673
- function humanTextFromStdout(stdout) {
21674
- for (const line of [...stdout].reverse()) {
21675
- const normalized = normalizeText(line);
21676
- if (!normalized) continue;
21677
- if (/^[A-Z_]+:/.test(normalized)) continue;
21678
- return normalized;
21679
- }
21680
- return null;
21681
- }
21682
- function responseTextFromAgentMessages(agentMessages) {
21683
- for (const message of [...agentMessages].reverse()) {
21684
- const record = asRecord2(message);
21685
- if (!record) continue;
21686
- for (const key of RESPONSE_KEYS) {
21687
- const normalized = normalizeText(record[key]);
21688
- if (normalized) return normalized;
21689
- }
21690
- }
21691
- return null;
21692
- }
21693
- function pushString(target, value) {
21694
- if (typeof value === "string" && value.trim()) {
21695
- target.add(value.trim());
21696
- }
21697
- }
21698
- function pushStringArray(target, value) {
21699
- if (!Array.isArray(value)) return;
21700
- for (const item of value) {
21701
- pushString(target, item);
21702
- }
21703
- }
21704
- function collectReferencesFromRecord(record, refs) {
21705
- for (const key of ENTRY_KEY_CANDIDATES)
21706
- pushString(refs.entryPaths, record[key]);
21707
- for (const key of ENTRY_ARRAY_KEY_CANDIDATES)
21708
- pushStringArray(refs.entryPaths, record[key]);
21709
- for (const key of LIST_KEY_CANDIDATES)
21710
- pushString(refs.listNames, record[key]);
21711
- for (const key of LIST_ARRAY_KEY_CANDIDATES)
21712
- pushStringArray(refs.listNames, record[key]);
21713
- for (const key of VARIABLE_KEY_CANDIDATES)
21714
- pushString(refs.variableNames, record[key]);
21715
- for (const key of VARIABLE_ARRAY_KEY_CANDIDATES)
21716
- pushStringArray(refs.variableNames, record[key]);
21717
- }
21718
- function stringValue(record, keys) {
21719
- for (const key of keys) {
21720
- const value = record[key];
21721
- if (typeof value === "string" && value.trim()) {
21722
- return value.trim();
21723
- }
21724
- }
21725
- return null;
21726
- }
21727
- function findEntryPathForRecord(record, heap) {
21728
- const directPath = stringValue(record, ["entryPath", "path"]);
21729
- if (directPath && heap.entriesByPath?.[directPath]) {
21730
- return directPath;
21731
- }
21732
- const id = stringValue(record, ["id", "_id", "recordId", "objectId"]);
21733
- if (!id) {
21734
- return null;
21735
- }
21736
- const className = stringValue(record, [
21737
- "className",
21738
- "_className",
21739
- "__className",
21740
- "prototype",
21741
- "type"
21742
- ]);
21743
- const entries = Object.values(heap.entriesByPath || {});
21744
- const exact = entries.find(
21745
- (entry) => entry.id === id && (!className || entry.className === className || entry.prototypes?.includes(className))
21746
- );
21747
- if (exact?.path) {
21748
- return exact.path;
21749
- }
21750
- const idOnlyMatches = entries.filter((entry) => entry.id === id);
21751
- return idOnlyMatches.length === 1 ? idOnlyMatches[0].path : null;
21752
- }
21753
- function scanForHeapReferences(value, heap, refs, depth = 0, seen = /* @__PURE__ */ new Set()) {
21754
- if (value === null || value === void 0 || depth > 4 || seen.has(value))
21755
- return;
21756
- if (typeof value === "string") {
21757
- const trimmed = value.trim();
21758
- if (heap.entriesByPath?.[trimmed]) refs.entryPaths.add(trimmed);
21759
- if (heap.listsByName?.[trimmed]) refs.listNames.add(trimmed);
21760
- if (heap.variablesByName?.[trimmed]) refs.variableNames.add(trimmed);
21761
- return;
21762
- }
21763
- if (Array.isArray(value)) {
21764
- seen.add(value);
21765
- for (const item of value.slice(0, 24)) {
21766
- scanForHeapReferences(item, heap, refs, depth + 1, seen);
21767
- }
21768
- return;
21769
- }
21770
- const record = asRecord2(value);
21771
- if (!record) return;
21772
- seen.add(value);
21773
- const entryPath = findEntryPathForRecord(record, heap);
21774
- if (entryPath) refs.entryPaths.add(entryPath);
21775
- collectReferencesFromRecord(record, refs);
21776
- for (const key of UI_CONTAINER_KEYS) {
21777
- const nested = asRecord2(record[key]);
21778
- if (nested) collectReferencesFromRecord(nested, refs);
21779
- }
21780
- for (const nested of Object.values(record).slice(0, 24)) {
21781
- scanForHeapReferences(nested, heap, refs, depth + 1, seen);
21782
- }
21783
- }
21784
- function resolveVariablesToReferences(variableNames, heap, refs) {
21785
- for (const variableName of variableNames) {
21786
- const variable = heap.variablesByName?.[variableName];
21787
- if (!variable) continue;
21788
- if (variable.kind === "entry" && variable.entryPath) {
21789
- refs.entryPaths.add(variable.entryPath);
21790
- }
21791
- if (variable.kind === "list" && variable.listName) {
21792
- refs.listNames.add(variable.listName);
21793
- }
21794
- }
21795
- }
21796
- function sortEntries(entries) {
21797
- return [...entries].sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
21798
- }
21799
- function sortLists(lists) {
21800
- return [...lists].sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
21801
- }
21802
- function dedupeEntries(entries) {
21803
- const seen = /* @__PURE__ */ new Set();
21804
- const result = [];
21805
- for (const entry of entries) {
21806
- if (!entry?.path || seen.has(entry.path)) continue;
21807
- seen.add(entry.path);
21808
- result.push(entry);
21809
- }
21810
- return result;
21811
- }
21812
- function dedupeLists(lists) {
21813
- const seen = /* @__PURE__ */ new Set();
21814
- const result = [];
21815
- for (const list of lists) {
21816
- if (!list?.name || seen.has(list.name)) continue;
21817
- seen.add(list.name);
21818
- result.push(list);
21819
- }
21820
- return result;
21821
- }
21822
- function extractResponseText(result, stdout) {
21823
- const directText = normalizeText(result);
21824
- if (directText) return directText;
21825
- const record = asRecord2(result);
21826
- if (record) {
21827
- for (const key of RESPONSE_KEYS) {
21828
- const normalized = normalizeText(record[key]);
21829
- if (normalized) return normalized;
21830
- }
21831
- for (const containerKey of UI_CONTAINER_KEYS) {
21832
- const nested = asRecord2(record[containerKey]);
21833
- if (!nested) continue;
21834
- for (const key of RESPONSE_KEYS) {
21835
- const normalized = normalizeText(nested[key]);
21836
- if (normalized) return normalized;
21837
- }
21838
- }
21839
- }
21840
- return humanTextFromStdout(stdout);
21841
- }
21842
- function fallbackResponseText(entries, lists) {
21843
- if (entries.length > 0) {
21844
- return entries.length === 1 ? "I found one relevant record." : `I found ${entries.length} relevant records.`;
21845
- }
21846
- if (lists.length > 0) {
21847
- const emptyOnly = lists.every((list) => (list.paths || []).length === 0);
21848
- if (emptyOnly) {
21849
- return lists.length === 1 ? "I saved one empty result set." : `I saved ${lists.length} empty result sets.`;
21850
- }
21851
- return lists.length === 1 ? "I saved one result set." : `I saved ${lists.length} result sets.`;
21852
- }
21853
- return null;
21854
- }
21855
- function getJobRelatedEntries(heap, jobId) {
21856
- return sortEntries(
21857
- Object.values(heap.entriesByPath || {}).filter(
21858
- (entry) => entry.relatedJobIds?.includes(jobId)
21859
- )
21860
- );
21861
- }
21862
- function getJobRelatedLists(heap, jobId) {
21863
- return sortLists(
21864
- Object.values(heap.listsByName || {}).filter(
21865
- (list) => list.relatedJobIds?.includes(jobId)
21866
- )
21867
- );
21868
- }
21869
- function entriesFromLists(lists, heap) {
21870
- const entries = [];
21871
- for (const list of lists) {
21872
- for (const path7 of list.paths || []) {
21873
- const entry = heap.entriesByPath?.[path7];
21874
- if (entry) entries.push(entry);
21875
- }
21876
- }
21877
- return entries;
21878
- }
21879
- function resolveJobPresentation({
21880
- jobId,
21881
- result,
21882
- stdout = [],
21883
- agentMessages = [],
21884
- sessionHeap,
21885
- allowExplicitArtifacts = true
21886
- }) {
21887
- const refs = {
21888
- entryPaths: /* @__PURE__ */ new Set(),
21889
- listNames: /* @__PURE__ */ new Set(),
21890
- variableNames: /* @__PURE__ */ new Set()
21891
- };
21892
- if (allowExplicitArtifacts) {
21893
- scanForHeapReferences(result, sessionHeap, refs);
21894
- resolveVariablesToReferences(refs.variableNames, sessionHeap, refs);
21895
- }
21896
- const referencedLists = sortLists(
21897
- [...refs.listNames].map((name) => sessionHeap.listsByName?.[name]).filter((list) => Boolean(list))
21898
- );
21899
- const referencedEntries = sortEntries(
21900
- [...refs.entryPaths].map((path7) => sessionHeap.entriesByPath?.[path7]).filter((entry) => Boolean(entry))
21901
- );
21902
- const jobLists = getJobRelatedLists(sessionHeap, jobId);
21903
- const jobEntries = getJobRelatedEntries(sessionHeap, jobId);
21904
- const changedEntries = dedupeEntries([
21905
- ...jobEntries,
21906
- ...entriesFromLists(jobLists, sessionHeap)
21907
- ]);
21908
- const explicitLists = dedupeLists(referencedLists);
21909
- const explicitEntries = dedupeEntries([
21910
- ...referencedEntries,
21911
- ...entriesFromLists(referencedLists, sessionHeap)
21912
- ]);
21913
- const hasExplicitArtifacts = allowExplicitArtifacts && (explicitEntries.length > 0 || explicitLists.length > 0);
21914
- const lists = hasExplicitArtifacts ? explicitLists : jobLists;
21915
- const entries = hasExplicitArtifacts ? explicitEntries : changedEntries;
21916
- const responseText = extractResponseText(result, stdout) || responseTextFromAgentMessages(agentMessages) || fallbackResponseText(entries, lists);
21917
- return {
21918
- responseText,
21919
- entries,
21920
- lists,
21921
- changedEntries,
21922
- changedLists: jobLists,
21923
- hasExplicitArtifacts
21924
- };
21925
- }
21926
-
21927
23299
  // src/session-transcript.ts
21928
- var EMPTY_HEAP = {
21929
- entriesByPath: {},
21930
- listsByName: {},
21931
- variablesByName: {}};
21932
23300
  function asRecord3(value) {
21933
- if (!value || typeof value !== "object" || Array.isArray(value)) return null;
21934
- return value;
21935
- }
21936
- function asArray(value) {
21937
- return Array.isArray(value) ? value : [];
21938
- }
21939
- function asNumber(value) {
21940
- return typeof value === "number" && Number.isFinite(value) ? value : void 0;
23301
+ return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
21941
23302
  }
21942
23303
  function asString(value) {
21943
23304
  return typeof value === "string" ? value : void 0;
@@ -21952,148 +23313,23 @@ function compactJson(value, maxLength = 320) {
21952
23313
  if (!json || json === "undefined") return void 0;
21953
23314
  return json.length > maxLength ? `${json.slice(0, maxLength)}...` : json;
21954
23315
  } catch {
21955
- return String(value);
21956
- }
21957
- }
21958
- function artifactRecordsById(liveDoc) {
21959
- const artifacts = asRecord3(liveDoc?.artifacts);
21960
- const byId = asRecord3(artifacts?.byId) || {};
21961
- return Object.fromEntries(
21962
- Object.entries(byId).map(([artifactId, value]) => {
21963
- const record = asRecord3(value);
21964
- return record ? [artifactId, record] : null;
21965
- }).filter(
21966
- (entry) => Boolean(entry)
21967
- )
21968
- );
21969
- }
21970
- function normalizeShowRefs(value) {
21971
- const record = asRecord3(value);
21972
- if (!record) return void 0;
21973
- const normalizeRefs = (input) => {
21974
- if (!Array.isArray(input)) return void 0;
21975
- const refs = Array.from(
21976
- new Set(
21977
- input.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean)
21978
- )
21979
- );
21980
- return refs.length > 0 ? refs : void 0;
21981
- };
21982
- const show = {
21983
- entryPaths: normalizeRefs(record.entryPaths),
21984
- listNames: normalizeRefs(record.listNames),
21985
- variableNames: normalizeRefs(record.variableNames),
21986
- fileIds: normalizeRefs(record.fileIds),
21987
- sessionArtifactIds: normalizeRefs(record.sessionArtifactIds),
21988
- actionSuggestions: normalizeActionSuggestions(record.actionSuggestions),
21989
- tables: Array.isArray(record.tables) ? record.tables.filter(
21990
- (table2) => Boolean(
21991
- table2 && typeof table2 === "object" && !Array.isArray(table2) && Array.isArray(table2.columns) && Array.isArray(table2.rows)
21992
- )
21993
- ) : void 0
21994
- };
21995
- return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions || show.tables ? show : void 0;
21996
- }
21997
- function normalizeActionSuggestions(value) {
21998
- if (!Array.isArray(value)) return void 0;
21999
- const suggestions = [];
22000
- for (const item of value) {
22001
- const record = asRecord3(item);
22002
- if (!record) continue;
22003
- const label = trimString(record.label);
22004
- if (!label) continue;
22005
- const suggestionId = trimString(record.suggestionId) || trimString(record.id) || label;
22006
- suggestions.push({
22007
- suggestionId,
22008
- label,
22009
- ...typeof record.description === "string" ? { description: record.description } : {},
22010
- ...asRecord3(record.artifact) ? { artifact: asRecord3(record.artifact) } : {},
22011
- ...asRecord3(record.target) ? { target: asRecord3(record.target) } : {},
22012
- ...asRecord3(record.metadata) ? { metadata: asRecord3(record.metadata) } : {}
22013
- });
22014
- }
22015
- return suggestions.length ? suggestions : void 0;
22016
- }
22017
- var TRANSCRIPT_MESSAGE_PART_LIMIT = 128;
22018
- var TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT = 2e5;
22019
- var TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT = 1e3;
22020
- var TRANSCRIPT_MESSAGE_ACTION_LIMIT = 64;
22021
- function normalizeConversationMessageActions(value) {
22022
- if (!Array.isArray(value) || value.length === 0) return void 0;
22023
- const actions = [];
22024
- for (const item of value.slice(0, TRANSCRIPT_MESSAGE_ACTION_LIMIT)) {
22025
- const record = asRecord3(item);
22026
- const kind = record?.kind;
22027
- const label = trimString(record?.label ?? record?.title);
22028
- const status = record?.status;
22029
- if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
22030
- continue;
22031
- }
22032
- actions.push({
22033
- kind,
22034
- label,
22035
- ...status === "done" || status === "queued" || status === "failed" ? { status } : {}
22036
- });
23316
+ return String(value);
22037
23317
  }
22038
- return actions.length ? actions : void 0;
22039
23318
  }
22040
- function normalizeConversationMessageParts(value, canonicalContent, canonicalActions) {
22041
- if (!Array.isArray(value) || value.length === 0 || value.length > TRANSCRIPT_MESSAGE_PART_LIMIT) {
22042
- return void 0;
22043
- }
22044
- const parts = [];
22045
- const canonicalActionsById = new Map(
22046
- (canonicalActions || []).map((action) => [
22047
- `${action.kind}:${action.label}`,
22048
- action
22049
- ])
23319
+ function artifactRecordsById(liveDoc) {
23320
+ const artifacts = asRecord3(liveDoc?.artifacts);
23321
+ const byId = asRecord3(artifacts?.byId) || {};
23322
+ return Object.fromEntries(
23323
+ Object.entries(byId).map(([artifactId, value]) => {
23324
+ const record = asRecord3(value);
23325
+ return record ? [artifactId, record] : null;
23326
+ }).filter((entry) => Boolean(entry))
22050
23327
  );
22051
- const seenActionIds = /* @__PURE__ */ new Set();
22052
- let textLength = 0;
22053
- for (const item of value) {
22054
- const record = asRecord3(item);
22055
- if (!record) return void 0;
22056
- if (record.type === "text") {
22057
- if (typeof record.text !== "string" || record.text.length === 0) {
22058
- return void 0;
22059
- }
22060
- textLength += record.text.length;
22061
- if (textLength > TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT) return void 0;
22062
- parts.push({ type: "text", text: record.text });
22063
- continue;
22064
- }
22065
- if (record.type !== "action") return void 0;
22066
- const action = asRecord3(record.action);
22067
- const kind = action?.kind;
22068
- const label = trimString(action?.label);
22069
- if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
22070
- return void 0;
22071
- }
22072
- const actionId = `${kind}:${label}`;
22073
- const canonicalAction = canonicalActionsById.get(actionId);
22074
- if (!canonicalAction) return void 0;
22075
- if (seenActionIds.has(actionId)) continue;
22076
- seenActionIds.add(actionId);
22077
- parts.push({
22078
- type: "action",
22079
- action: canonicalAction
22080
- });
22081
- }
22082
- const orderedText = parts.filter(
22083
- (part) => part.type === "text"
22084
- ).map((part) => part.text).join("");
22085
- return orderedText === canonicalContent ? parts : void 0;
22086
23328
  }
22087
23329
  function stringifyTranscriptValue(value, fallback2 = "") {
22088
- if (typeof value === "string") {
22089
- return value.trim() || fallback2;
22090
- }
22091
- if (typeof value === "boolean") {
22092
- return value ? "Confirmed" : "Canceled";
22093
- }
22094
- if (value === void 0) {
22095
- return fallback2;
22096
- }
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;
22097
23333
  try {
22098
23334
  const json = JSON.stringify(value, null, 2);
22099
23335
  if (!json || json === "undefined") return fallback2;
@@ -22234,262 +23470,121 @@ ${stringifyTranscriptValue({ show }, "")}`;
22234
23470
  hasOtherRefs ? `Other shown refs: ${stringifyTranscriptValue(otherRefs, "")}` : null
22235
23471
  ].filter(Boolean).join("\n");
22236
23472
  }
22237
- function normalizeConversationMessage(raw, artifactsById) {
22238
- const record = asRecord3(raw);
22239
- if (!record) return null;
22240
- const role = record.role === "user" ? "user" : record.role === "assistant" ? "assistant" : null;
22241
- if (!role) return null;
22242
- const content = trimString(
22243
- record.content ?? record.reply ?? record.message ?? record.text
22244
- );
22245
- const actions = role === "assistant" ? normalizeConversationMessageActions(record.actions) : void 0;
22246
- const parts = role === "assistant" ? normalizeConversationMessageParts(record.parts, content, actions) : void 0;
22247
- const show = normalizeShowRefs(record.show);
22248
- const id = asString(record.id) || crypto.randomUUID();
22249
- const timestamp = asNumber(record.timestamp) || asNumber(record.ts) || 0;
22250
- if (!content && !show && !actions?.length) return null;
22251
- const artifactHistory = buildArtifactHistory(show, artifactsById);
22252
- const historyContent = role === "assistant" ? content && artifactHistory ? `[Assistant reply]
22253
- ${content}
22254
-
22255
- ${artifactHistory}` : content ? `[Assistant reply]
22256
- ${content}` : artifactHistory : void 0;
22257
- return {
22258
- id,
22259
- role,
22260
- content,
22261
- timestamp,
22262
- jobId: asString(record.jobId),
22263
- promptId: asString(record.promptId),
22264
- show,
22265
- actions,
22266
- parts,
22267
- historyContent,
22268
- source: "conversation"
22269
- };
22270
- }
22271
- function normalizePromptEntries(jobId, rawPrompts, conversationPromptIds) {
22272
- const promptsById = asRecord3(rawPrompts) || {};
22273
- return Object.values(promptsById).map((value) => asRecord3(value)).filter((value) => Boolean(value)).sort(
22274
- (left, right) => (asNumber(left.openedAt) || asNumber(left.answeredAt) || 0) - (asNumber(right.openedAt) || asNumber(right.answeredAt) || 0)
22275
- ).flatMap((prompt3) => {
22276
- const promptId = asString(prompt3.promptId);
22277
- if (!promptId || conversationPromptIds.has(promptId)) return [];
22278
- const title = trimString(prompt3.title);
22279
- const message = trimString(prompt3.message);
22280
- const assistantContent = message || title || "Input required";
22281
- const openedAt = asNumber(prompt3.openedAt) || 0;
22282
- const answeredAt = asNumber(prompt3.answeredAt) || openedAt;
22283
- const entries = [
22284
- {
22285
- id: `prompt:${promptId}:assistant`,
22286
- role: "assistant",
22287
- content: assistantContent,
22288
- timestamp: openedAt,
22289
- jobId,
22290
- promptId,
22291
- historyContent: `[Assistant reply]
22292
- ${assistantContent}`,
22293
- source: "job_prompt"
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);
22294
23483
  }
22295
- ];
22296
- if (Object.prototype.hasOwnProperty.call(prompt3, "answer")) {
22297
- entries.push({
22298
- id: `prompt:${promptId}:user`,
22299
- role: "user",
22300
- content: stringifyTranscriptValue(prompt3.answer, ""),
22301
- timestamp: answeredAt,
22302
- jobId,
22303
- promptId,
22304
- source: "job_prompt"
22305
- });
23484
+ return {
23485
+ ...entryPaths.length ? { entryPaths } : {},
23486
+ ...listNames.length ? { listNames } : {},
23487
+ ...variableNames.length ? { variableNames } : {}
23488
+ };
22306
23489
  }
22307
- return entries;
22308
- });
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
+ }
22309
23520
  }
22310
- function normalizeAgentMessageEntries(jobId, rawMessages, artifactsById) {
22311
- return asArray(rawMessages).map((value) => asRecord3(value)).filter((value) => Boolean(value)).sort(
22312
- (left, right) => (asNumber(left.timestamp) || asNumber(left.ts) || 0) - (asNumber(right.timestamp) || asNumber(right.ts) || 0)
22313
- ).flatMap((message) => {
22314
- const messageId = asString(message.messageId) || asString(message.id) || crypto.randomUUID();
22315
- const timestamp = asNumber(message.timestamp) || asNumber(message.ts) || 0;
22316
- const reply = trimString(
22317
- message.reply ?? message.message ?? message.text ?? message.content
22318
- );
22319
- const show = normalizeShowRefs(message.show);
22320
- const entries = [];
22321
- if (reply) {
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;
22322
23531
  entries.push({
22323
- id: `agent:${messageId}:text`,
22324
- role: "assistant",
22325
- content: reply,
22326
- timestamp,
22327
- jobId,
22328
- historyContent: `[Assistant reply]
22329
- ${reply}`,
22330
- source: "job_agent_message"
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"
22331
23542
  });
23543
+ continue;
22332
23544
  }
22333
- if (show) {
23545
+ if (item.kind === "prompt") {
23546
+ const content = item.payload.message || item.payload.title;
22334
23547
  entries.push({
22335
- id: `agent:${messageId}:artifacts`,
23548
+ id: item.id,
22336
23549
  role: "assistant",
22337
- content: "",
22338
- timestamp,
22339
- jobId,
22340
- show,
22341
- historyContent: buildArtifactHistory(show, artifactsById),
22342
- source: "job_agent_message"
23550
+ content,
23551
+ timestamp: item.occurredAt,
23552
+ sequence: item.sequence,
23553
+ jobId: item.source?.jobId,
23554
+ promptId: item.payload.promptId,
23555
+ historyContent: `[Assistant reply]
23556
+ ${content}`,
23557
+ source: "feed"
22343
23558
  });
23559
+ continue;
22344
23560
  }
22345
- return entries;
22346
- });
22347
- }
22348
- function buildJobFallbackEntries(jobId, job2, sessionHeap, artifactsById) {
22349
- const timestamp = asNumber(job2.finishedAt) || asNumber(job2.startedAt) || asNumber(job2.submittedAt) || 0;
22350
- const resultPreview = stringifyTranscriptValue(
22351
- job2.result,
22352
- "No job result recorded."
22353
- );
22354
- const presentation = resolveJobPresentation({
22355
- jobId,
22356
- result: job2.result,
22357
- stdout: [],
22358
- sessionHeap
22359
- });
22360
- const entries = [];
22361
- const responseText = presentation.responseText || "";
22362
- if (responseText) {
22363
- entries.push({
22364
- id: `job:${jobId}:result-text`,
22365
- role: "assistant",
22366
- content: responseText,
22367
- timestamp,
22368
- jobId,
22369
- historyContent: `[Assistant reply]
22370
- ${responseText}`,
22371
- source: "job_result"
22372
- });
22373
- }
22374
- const show = {
22375
- entryPaths: presentation.entries.map((entry) => entry.path),
22376
- listNames: presentation.lists.map((list) => list.name)
22377
- };
22378
- if (show.entryPaths && show.entryPaths.length > 0 || show.listNames && show.listNames.length > 0) {
23561
+ const show = feedItemShow(item);
23562
+ if (!show) continue;
22379
23563
  entries.push({
22380
- id: `job:${jobId}:result-artifacts`,
23564
+ id: item.id,
22381
23565
  role: "assistant",
22382
23566
  content: "",
22383
- timestamp,
22384
- jobId,
23567
+ timestamp: item.occurredAt,
23568
+ sequence: item.sequence,
23569
+ jobId: item.source?.jobId,
22385
23570
  show,
22386
23571
  historyContent: buildArtifactHistory(show, artifactsById),
22387
- source: "job_result"
22388
- });
22389
- }
22390
- if (entries.length === 0 && trimString(job2.error)) {
22391
- entries.push({
22392
- id: `job:${jobId}:result-error`,
22393
- role: "assistant",
22394
- content: trimString(job2.error),
22395
- timestamp,
22396
- jobId,
22397
- historyContent: `[Assistant reply]
22398
- ${trimString(job2.error)}`,
22399
- source: "job_result"
22400
- });
22401
- }
22402
- if (entries.length === 0 && resultPreview && resultPreview !== "No job result recorded.") {
22403
- entries.push({
22404
- id: `job:${jobId}:result-preview`,
22405
- role: "assistant",
22406
- content: resultPreview,
22407
- timestamp,
22408
- jobId,
22409
- historyContent: `[Assistant reply]
22410
- ${resultPreview}`,
22411
- source: "job_result"
23572
+ source: "feed"
22412
23573
  });
22413
23574
  }
22414
23575
  return entries;
22415
23576
  }
22416
- function buildJobCodeEntry(jobId, job2) {
22417
- const code = trimString(job2.source);
22418
- if (!code) return null;
22419
- const jobStatus = asString(job2.status);
22420
- const error2 = jobStatus === "failed" || jobStatus === "canceled" || jobStatus === "timeout" ? trimString(job2.error) || `Job ${jobStatus}` : void 0;
22421
- return {
22422
- id: `job:${jobId}:code`,
22423
- role: "assistant",
22424
- content: "",
22425
- timestamp: asNumber(job2.submittedAt) || asNumber(job2.startedAt) || asNumber(job2.finishedAt) || 0,
22426
- jobId,
22427
- code,
22428
- jobStatus,
22429
- jobResultPreview: stringifyTranscriptValue(
22430
- job2.result,
22431
- "No job result recorded."
22432
- ),
22433
- error: error2,
22434
- source: "job_code"
22435
- };
22436
- }
23577
+ var CANONICAL_FEED_REQUIRED = "Canonical session feed-v1 is required; past message/job transcript reconstruction is not supported.";
22437
23578
  function buildSessionTranscript(input) {
22438
23579
  const liveDoc = input.liveDoc || null;
22439
- const sessionHeap = input.sessionHeap || EMPTY_HEAP;
22440
- const artifactsById = artifactRecordsById(liveDoc);
22441
- const transcript = [];
22442
- const conversationMessages = asArray(
22443
- asRecord3(liveDoc?.conversation)?.messages
22444
- ).map((message) => normalizeConversationMessage(message, artifactsById)).filter((message) => Boolean(message));
22445
- const conversationPromptIds = new Set(
22446
- conversationMessages.map((message) => message.promptId).filter((promptId) => Boolean(promptId))
22447
- );
22448
- const assistantConversationJobIds = new Set(
22449
- conversationMessages.filter(
22450
- (message) => message.role === "assistant" && Boolean(message.jobId)
22451
- ).map((message) => message.jobId)
22452
- );
22453
- transcript.push(...conversationMessages);
22454
- const jobsById = asRecord3(asRecord3(liveDoc?.jobs)?.byId) || {};
22455
- const jobs = Object.values(jobsById).map((value) => asRecord3(value)).filter((value) => Boolean(value)).sort(
22456
- (left, right) => (asNumber(left.submittedAt) || asNumber(left.startedAt) || asNumber(left.finishedAt) || 0) - (asNumber(right.submittedAt) || asNumber(right.startedAt) || asNumber(right.finishedAt) || 0)
22457
- );
22458
- for (const job2 of jobs) {
22459
- const jobId = asString(job2.jobId);
22460
- if (!jobId) continue;
22461
- const codeEntry = buildJobCodeEntry(jobId, job2);
22462
- if (codeEntry) {
22463
- transcript.push(codeEntry);
22464
- }
22465
- transcript.push(
22466
- ...normalizePromptEntries(jobId, job2.prompts, conversationPromptIds)
22467
- );
22468
- if (!assistantConversationJobIds.has(jobId)) {
22469
- const agentEntries = normalizeAgentMessageEntries(
22470
- jobId,
22471
- job2.agentMessages,
22472
- artifactsById
22473
- );
22474
- if (agentEntries.length > 0) {
22475
- transcript.push(...agentEntries);
22476
- } else {
22477
- transcript.push(
22478
- ...buildJobFallbackEntries(
22479
- jobId,
22480
- job2,
22481
- sessionHeap,
22482
- artifactsById
22483
- )
22484
- );
22485
- }
22486
- }
22487
- }
22488
- return transcript.sort((left, right) => {
22489
- if (left.timestamp !== right.timestamp) {
22490
- return left.timestamp - right.timestamp;
22491
- }
22492
- 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
22493
23588
  });
22494
23589
  }
22495
23590
 
@@ -22552,7 +23647,7 @@ function normalizeEffectBehaviors(value) {
22552
23647
  }
22553
23648
  function resolveInvocationMode(context) {
22554
23649
  const mode = context?.invocation?.mode;
22555
- if (mode === "dryRun" || mode === "reverse") {
23650
+ if (mode === "dryRun" || mode === "reverse" || mode === "artifactOptions") {
22556
23651
  return mode;
22557
23652
  }
22558
23653
  return "execute";
@@ -22606,6 +23701,18 @@ function resolveHandlerForMode(effectMap, effect, request) {
22606
23701
  request.context?.behaviors || effect.metamodels || void 0
22607
23702
  );
22608
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
+ }
22609
23716
  if (mode === "dryRun") {
22610
23717
  if (effect.dryRunHandler) {
22611
23718
  return { effect, mode, handler: effect.dryRunHandler };
@@ -22638,7 +23745,91 @@ function resolveHandlerForMode(effectMap, effect, request) {
22638
23745
  }
22639
23746
  return { effect, mode, handler: effect.handler };
22640
23747
  }
22641
- 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 = {}) {
22642
23833
  const effect = selectRegisteredEffect(
22643
23834
  effectMap,
22644
23835
  request.effectKey,
@@ -22657,14 +23848,49 @@ async function invokeRegisteredEffect(effectMap, request) {
22657
23848
  mode: resolved.mode,
22658
23849
  sourceEffectKey: request.effectKey,
22659
23850
  sourceEffectName: request.effectName,
22660
- ...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 } : {}
22661
23854
  }
22662
23855
  };
22663
- if (resolved.effect.className && !resolved.effect.static && request.input && typeof request.input === "object" && "_objectId" in request.input) {
22664
- const { _objectId, ...rest } = request.input;
22665
- 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;
22666
23892
  }
22667
- return resolved.handler(request.input, context);
23893
+ return handlerResult;
22668
23894
  }
22669
23895
 
22670
23896
  // src/client-normalizers.ts
@@ -22991,6 +24217,36 @@ function buildEffectMetamodelMutations(toolPath, spec) {
22991
24217
  var DEFAULT_CONVERSATION_SESSION_LIST_LIMIT = 100;
22992
24218
  var MAX_CONVERSATION_SESSION_LIST_LIMIT = 500;
22993
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
+ }
22994
24250
  function boundedSessionListInteger(value, name, fallback2, minimum, maximum) {
22995
24251
  if (value === void 0) return fallback2;
22996
24252
  if (!Number.isInteger(value) || value < minimum || value > maximum) {
@@ -24564,12 +25820,55 @@ var Environment = class _Environment {
24564
25820
  var EnvironmentSession = class extends Session {
24565
25821
  environment;
24566
25822
  sessionDataRoutePrefix;
25823
+ sessionDataHeaders;
25824
+ heartbeatTimer = null;
25825
+ heartbeatInFlight = false;
24567
25826
  /** The last known graph container status, updated by checkReadiness() or on heartbeat */
24568
25827
  graphContainerStatus = null;
24569
25828
  constructor(client, environment, clientId, options = {}) {
24570
25829
  super(client, clientId, { initialQuota: options.initialQuota });
24571
25830
  this.environment = environment;
24572
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;
24573
25872
  }
24574
25873
  get environmentId() {
24575
25874
  return this.environment.environmentId;
@@ -24629,6 +25928,11 @@ var EnvironmentSession = class extends Session {
24629
25928
  const response = await fetch(url, {
24630
25929
  method: init2.method || "GET",
24631
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",
24632
25936
  ...typeof init2.body === "undefined" ? {} : { body: init2.body }
24633
25937
  });
24634
25938
  if (response.ok) {
@@ -24657,34 +25961,50 @@ var EnvironmentSession = class extends Session {
24657
25961
  const body = typeof init2.body === "undefined" ? void 0 : JSON.stringify(init2.body);
24658
25962
  const response = await this.sessionDataFetch(path7, query, {
24659
25963
  method: init2.method || "GET",
24660
- headers: { "Content-Type": "application/json" },
25964
+ headers: {
25965
+ ...this.sessionDataHeaders,
25966
+ "Content-Type": "application/json"
25967
+ },
24661
25968
  ...typeof body === "undefined" ? {} : { body }
24662
25969
  });
24663
25970
  return response.json();
24664
25971
  }
24665
- async collectAllSessionItems(listPage) {
24666
- const items = [];
24667
- let cursor = null;
24668
- do {
24669
- const page = await listPage({ limit: 500, cursor });
24670
- items.push(...page.items);
24671
- cursor = page.nextCursor;
24672
- } while (cursor);
24673
- return items;
24674
- }
24675
25972
  /**
24676
25973
  * Fetch the live session document from the runtime DO.
24677
25974
  *
24678
- * For history and saved artifacts, prefer the collection APIs on
24679
- * `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.
24680
25977
  */
24681
25978
  async getDocument() {
24682
25979
  return this.sessionDataRequest("/document");
24683
25980
  }
24684
- get messages() {
24685
- return {
24686
- list: (options = {}) => this.sessionDataRequest("/messages", options)
24687
- };
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
+ );
24688
26008
  }
24689
26009
  get timeline() {
24690
26010
  return {
@@ -24726,7 +26046,12 @@ var EnvironmentSession = class extends Session {
24726
26046
  latestJob: true
24727
26047
  }),
24728
26048
  get: (artifactId) => this.sessionDataRequest(
24729
- `/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() }
24730
26055
  ),
24731
26056
  create: (artifact) => this.sessionDataRequest(
24732
26057
  "/artifacts",
@@ -24736,6 +26061,14 @@ var EnvironmentSession = class extends Session {
24736
26061
  body: artifact
24737
26062
  }
24738
26063
  ),
26064
+ acceptSuggestion: (sequence) => this.sessionDataRequest(
26065
+ "/artifacts/suggestions/accept",
26066
+ void 0,
26067
+ {
26068
+ method: "POST",
26069
+ body: { sequence }
26070
+ }
26071
+ ),
24739
26072
  updateInputs: (artifactId, patch) => this.sessionDataRequest(
24740
26073
  `/artifacts/${encodeURIComponent(artifactId)}`,
24741
26074
  void 0,
@@ -24744,6 +26077,16 @@ var EnvironmentSession = class extends Session {
24744
26077
  body: patch
24745
26078
  }
24746
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
+ ),
24747
26090
  validate: (artifactId) => this.sessionDataRequest(
24748
26091
  `/artifacts/${encodeURIComponent(artifactId)}/validate`,
24749
26092
  void 0,
@@ -24881,73 +26224,50 @@ var EnvironmentSession = class extends Session {
24881
26224
  get transcript() {
24882
26225
  return {
24883
26226
  list: async (options = {}) => {
24884
- const [messages, jobs, entries, lists, artifacts] = await Promise.all([
24885
- this.collectAllSessionItems(this.messages.list),
24886
- this.collectAllSessionItems(
24887
- (pageOptions) => this.jobs.list({ ...pageOptions, status: "all" })
24888
- ),
24889
- this.collectAllSessionItems(this.heap.entries.list),
24890
- this.collectAllSessionItems(this.heap.lists.list),
24891
- this.collectAllSessionItems(
24892
- (pageOptions) => this.artifacts.list({ ...pageOptions, status: "all" })
24893
- )
24894
- ]);
24895
- const liveDoc = {
24896
- conversation: { messages },
24897
- jobs: {
24898
- byId: Object.fromEntries(
24899
- jobs.map((job2) => {
24900
- const record = job2 && typeof job2 === "object" ? job2 : null;
24901
- const id = typeof record?.jobId === "string" ? record.jobId : typeof record?.id === "string" ? record.id : null;
24902
- return id ? [id, record] : null;
24903
- }).filter(
24904
- (entry) => Boolean(entry)
24905
- )
24906
- )
24907
- },
24908
- artifacts: {
24909
- byId: Object.fromEntries(
24910
- artifacts.map((artifact) => {
24911
- return artifact?.artifactId ? [
24912
- artifact.artifactId,
24913
- artifact
24914
- ] : null;
24915
- }).filter(
24916
- (entry) => Boolean(entry)
24917
- )
24918
- ),
24919
- order: artifacts.map((artifact) => artifact?.artifactId).filter(
24920
- (artifactId) => Boolean(artifactId)
24921
- )
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
+ );
24922
26240
  }
24923
- };
24924
- const heap = normalizeHeapSnapshot({
24925
- entriesByPath: Object.fromEntries(
24926
- entries.map((entry) => {
24927
- return entry?.path ? [entry.path, entry] : null;
24928
- }).filter(
24929
- (entry) => Boolean(entry)
24930
- )
24931
- ),
24932
- listsByName: Object.fromEntries(
24933
- lists.map((list) => {
24934
- return list?.name ? [list.name, list] : null;
24935
- }).filter(
24936
- (entry) => Boolean(entry)
24937
- )
24938
- ),
24939
- variablesByName: this.getHeap().variablesByName,
24940
- updatedAt: Date.now()
24941
- });
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
+ }
24942
26262
  const allItems = buildSessionTranscript({
24943
- liveDoc,
24944
- sessionHeap: heap
26263
+ liveDoc: this.document,
26264
+ canonicalFeedItems
24945
26265
  });
24946
26266
  const limit = Math.max(
24947
26267
  1,
24948
26268
  Math.min(500, Math.floor(options.limit ?? 100))
24949
26269
  );
24950
- 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;
24951
26271
  const items = allItems.slice(offset, offset + limit);
24952
26272
  const nextOffset = offset + items.length;
24953
26273
  return {
@@ -25033,6 +26353,8 @@ var EnvironmentSession = class extends Session {
25033
26353
  * acknowledgement was observed.
25034
26354
  */
25035
26355
  async disconnect() {
26356
+ this.stopHeartbeat();
26357
+ this.disposeSessionFeed();
25036
26358
  let wsNotifiedRuntime = false;
25037
26359
  try {
25038
26360
  const goodbye = await this.rpc(
@@ -25071,6 +26393,7 @@ var EnvironmentSession = class extends Session {
25071
26393
  * Close only the socket transport without sending `client.goodbye`.
25072
26394
  */
25073
26395
  disconnectTransport() {
26396
+ this.stopHeartbeat();
25074
26397
  this.client.disconnect({ reason: "Transport detach" });
25075
26398
  }
25076
26399
  /**
@@ -25835,16 +27158,22 @@ var Granular = class _Granular {
25835
27158
  return this.normalizeUserEnvironmentState(state);
25836
27159
  }
25837
27160
  async markUserEnvironmentSessionsRead(options) {
27161
+ const readThroughSequence = requireUserEnvironmentSequence(
27162
+ options.readThroughSequence,
27163
+ "readThroughSequence"
27164
+ );
25838
27165
  const result = await this.request("/sdk/user-environment-state/read", {
25839
27166
  method: "POST",
25840
27167
  body: JSON.stringify({
25841
27168
  environmentId: options.environmentId,
25842
27169
  sessionId: options.sessionId,
25843
27170
  sessionIds: options.sessionIds,
25844
- readAt: options.readAt
27171
+ readThroughSequence
25845
27172
  })
25846
27173
  });
25847
- return result.readAtBySessionId || {};
27174
+ return normalizeReadThroughSequenceMap(
27175
+ result.readThroughSequenceBySessionId
27176
+ );
25848
27177
  }
25849
27178
  normalizeConversationSession(row) {
25850
27179
  const sessionId = String(row.sessionId ?? row.session_id ?? "");
@@ -25869,21 +27198,48 @@ var Granular = class _Granular {
25869
27198
  };
25870
27199
  }
25871
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
+ }));
25872
27231
  return {
25873
27232
  ...state,
25874
- sessions: Array.isArray(state.sessions) ? state.sessions.map((item) => ({
25875
- ...item,
25876
- session: this.normalizeConversationSession(
25877
- item.session
25878
- )
25879
- })) : [],
27233
+ sessions,
25880
27234
  attention: {
25881
27235
  prompts: Array.isArray(state.attention?.prompts) ? state.attention.prompts : [],
25882
27236
  count: typeof state.attention?.count === "number" ? state.attention.count : 0,
25883
27237
  activePrompt: state.attention?.activePrompt || null
25884
27238
  },
25885
27239
  unreadCount: typeof state.unreadCount === "number" ? state.unreadCount : 0,
25886
- readAtBySessionId: state.readAtBySessionId || {}
27240
+ readThroughSequenceBySessionId: normalizeReadThroughSequenceMap(
27241
+ state.readThroughSequenceBySessionId
27242
+ )
25887
27243
  };
25888
27244
  }
25889
27245
  static coerceIsoDate(value) {
@@ -25914,7 +27270,9 @@ var Granular = class _Granular {
25914
27270
  environmentId: options.environmentId,
25915
27271
  clientId,
25916
27272
  sessionScope,
25917
- capabilities: sessionScope ? { sessionScope } : void 0,
27273
+ capabilities: {
27274
+ ...sessionScope ? { sessionScope } : {}
27275
+ },
25918
27276
  initialHeap: options.initialHeap
25919
27277
  })
25920
27278
  });
@@ -26284,6 +27642,21 @@ var Granular = class _Granular {
26284
27642
  reconnectError
26285
27643
  );
26286
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
+ }
26287
27660
  }
26288
27661
  );
26289
27662
  }
@@ -26386,7 +27759,13 @@ var Granular = class _Granular {
26386
27759
  const request = params;
26387
27760
  return invokeRegisteredEffect(
26388
27761
  this.getSandboxEffectMap(sandboxId),
26389
- request
27762
+ request,
27763
+ {
27764
+ feedback: {
27765
+ invocationId: request.callId,
27766
+ publish: (method, publishParams) => wsClient.call(method, publishParams)
27767
+ }
27768
+ }
26390
27769
  );
26391
27770
  });
26392
27771
  wsClient.on("open", () => {
@@ -26400,14 +27779,20 @@ var Granular = class _Granular {
26400
27779
  wsClient.on("disconnect", () => {
26401
27780
  this.stopEffectHostHeartbeat(host);
26402
27781
  });
26403
- await withTimeout(
26404
- wsClient.connect(),
26405
- EFFECT_HOST_CONNECT_TIMEOUT_MS,
26406
- `effect host WebSocket connect for sandbox ${sandboxId}`
26407
- );
26408
- await this.synchronizeEffectHost(host);
26409
- this.sandboxEffectHosts.set(sandboxId, host);
26410
- 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
+ }
26411
27796
  })();
26412
27797
  this.sandboxEffectHostPromises.set(sandboxId, connectPromise);
26413
27798
  try {