@granular-software/sdk 0.4.58 → 0.4.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -4
- package/dist/agent-evals.d.mts +2 -3
- package/dist/agent-evals.d.ts +2 -3
- package/dist/agent-evals.js +2910 -1329
- package/dist/agent-evals.js.map +1 -1
- package/dist/agent-evals.mjs +2910 -1329
- package/dist/agent-evals.mjs.map +1 -1
- package/dist/agent-harness.d.mts +3 -3
- package/dist/agent-harness.d.ts +3 -3
- package/dist/agent-harness.js +67 -131
- package/dist/agent-harness.js.map +1 -1
- package/dist/agent-harness.mjs +67 -131
- package/dist/agent-harness.mjs.map +1 -1
- package/dist/cli/index.js +2394 -1009
- package/dist/{client-DFaCRSto.d.mts → client-BzgQvNyG.d.ts} +90 -17
- package/dist/{client-UdGt44dF.d.ts → client-DSkGcmmX.d.mts} +90 -17
- package/dist/index.d.mts +33 -11
- package/dist/index.d.ts +33 -11
- package/dist/index.js +2840 -1252
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2826 -1253
- package/dist/index.mjs.map +1 -1
- package/dist/{spend-czRX7l0X.d.mts → spend-CNe-Ff6U.d.mts} +670 -98
- package/dist/{spend-czRX7l0X.d.ts → spend-CNe-Ff6U.d.ts} +670 -98
- package/dist/spend.d.mts +1 -1
- package/dist/spend.d.ts +1 -1
- package/package.json +5 -2
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(...)\`, \`
|
|
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
|
-
| \`
|
|
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
|
-
|
|
19610
|
-
this.
|
|
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
|
-
|
|
19975
|
-
|
|
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
|
-
|
|
19978
|
-
|
|
19979
|
-
|
|
19980
|
-
|
|
19981
|
-
|
|
19982
|
-
|
|
19983
|
-
|
|
19984
|
-
|
|
19985
|
-
|
|
19986
|
-
|
|
19987
|
-
|
|
19988
|
-
|
|
19989
|
-
|
|
19990
|
-
|
|
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 =
|
|
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
|
-
|
|
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 (
|
|
20054
|
-
|
|
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 =
|
|
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 (
|
|
20067
|
-
|
|
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
|
|
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
|
|
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
|
|
21857
|
+
return null;
|
|
20321
21858
|
}
|
|
20322
21859
|
function normalizePrompt(rawValue) {
|
|
20323
|
-
const raw =
|
|
21860
|
+
const raw = asRecord2(rawValue);
|
|
20324
21861
|
if (!raw) return null;
|
|
20325
|
-
const promptRecord =
|
|
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
|
|
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" ||
|
|
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:
|
|
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
|
|
20379
|
-
|
|
20380
|
-
return
|
|
20381
|
-
|
|
20382
|
-
|
|
20383
|
-
|
|
20384
|
-
|
|
20385
|
-
|
|
20386
|
-
|
|
20387
|
-
|
|
20388
|
-
|
|
20389
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
20451
|
-
if (
|
|
20452
|
-
|
|
20453
|
-
|
|
20454
|
-
|
|
20455
|
-
|
|
20456
|
-
|
|
20457
|
-
|
|
20458
|
-
|
|
20459
|
-
|
|
20460
|
-
|
|
20461
|
-
|
|
20462
|
-
|
|
20463
|
-
|
|
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
|
|
20665
|
-
|
|
20666
|
-
|
|
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
|
|
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
|
|
21108
|
-
|
|
21109
|
-
|
|
21110
|
-
|
|
21111
|
-
|
|
21112
|
-
|
|
21113
|
-
|
|
21114
|
-
|
|
21115
|
-
|
|
21116
|
-
|
|
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
|
-
|
|
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
|
|
22041
|
-
|
|
22042
|
-
|
|
22043
|
-
|
|
22044
|
-
|
|
22045
|
-
|
|
22046
|
-
|
|
22047
|
-
|
|
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
|
-
|
|
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
|
|
22238
|
-
|
|
22239
|
-
|
|
22240
|
-
|
|
22241
|
-
|
|
22242
|
-
|
|
22243
|
-
|
|
22244
|
-
|
|
22245
|
-
|
|
22246
|
-
|
|
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
|
-
|
|
22297
|
-
|
|
22298
|
-
|
|
22299
|
-
|
|
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
|
-
|
|
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
|
|
22311
|
-
|
|
22312
|
-
|
|
22313
|
-
|
|
22314
|
-
|
|
22315
|
-
|
|
22316
|
-
|
|
22317
|
-
|
|
22318
|
-
|
|
22319
|
-
|
|
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:
|
|
22324
|
-
role:
|
|
22325
|
-
content
|
|
22326
|
-
timestamp,
|
|
22327
|
-
|
|
22328
|
-
|
|
22329
|
-
|
|
22330
|
-
|
|
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 (
|
|
23545
|
+
if (item.kind === "prompt") {
|
|
23546
|
+
const content = item.payload.message || item.payload.title;
|
|
22334
23547
|
entries.push({
|
|
22335
|
-
id:
|
|
23548
|
+
id: item.id,
|
|
22336
23549
|
role: "assistant",
|
|
22337
|
-
content
|
|
22338
|
-
timestamp,
|
|
22339
|
-
|
|
22340
|
-
|
|
22341
|
-
|
|
22342
|
-
|
|
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
|
-
|
|
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:
|
|
23564
|
+
id: item.id,
|
|
22381
23565
|
role: "assistant",
|
|
22382
23566
|
content: "",
|
|
22383
|
-
timestamp,
|
|
22384
|
-
|
|
23567
|
+
timestamp: item.occurredAt,
|
|
23568
|
+
sequence: item.sequence,
|
|
23569
|
+
jobId: item.source?.jobId,
|
|
22385
23570
|
show,
|
|
22386
23571
|
historyContent: buildArtifactHistory(show, artifactsById),
|
|
22387
|
-
source: "
|
|
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
|
-
|
|
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
|
-
|
|
22440
|
-
|
|
22441
|
-
|
|
22442
|
-
const
|
|
22443
|
-
|
|
22444
|
-
|
|
22445
|
-
|
|
22446
|
-
|
|
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
|
-
|
|
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
|
-
|
|
22664
|
-
|
|
22665
|
-
|
|
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
|
|
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: {
|
|
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
|
|
24679
|
-
*
|
|
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
|
-
|
|
24685
|
-
|
|
24686
|
-
|
|
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
|
-
|
|
24885
|
-
|
|
24886
|
-
|
|
24887
|
-
|
|
24888
|
-
|
|
24889
|
-
|
|
24890
|
-
|
|
24891
|
-
|
|
24892
|
-
|
|
24893
|
-
)
|
|
24894
|
-
|
|
24895
|
-
|
|
24896
|
-
|
|
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
|
-
|
|
24925
|
-
|
|
24926
|
-
|
|
24927
|
-
|
|
24928
|
-
|
|
24929
|
-
|
|
24930
|
-
|
|
24931
|
-
|
|
24932
|
-
|
|
24933
|
-
|
|
24934
|
-
|
|
24935
|
-
|
|
24936
|
-
|
|
24937
|
-
|
|
24938
|
-
|
|
24939
|
-
|
|
24940
|
-
|
|
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
|
-
|
|
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
|
-
|
|
27171
|
+
readThroughSequence
|
|
25845
27172
|
})
|
|
25846
27173
|
});
|
|
25847
|
-
return
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
26404
|
-
|
|
26405
|
-
|
|
26406
|
-
|
|
26407
|
-
|
|
26408
|
-
|
|
26409
|
-
|
|
26410
|
-
|
|
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 {
|