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