@granular-software/sdk 0.4.58 → 0.4.60
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +38 -4
- package/dist/agent-evals.d.mts +2 -3
- package/dist/agent-evals.d.ts +2 -3
- package/dist/agent-evals.js +2910 -1329
- package/dist/agent-evals.js.map +1 -1
- package/dist/agent-evals.mjs +2910 -1329
- package/dist/agent-evals.mjs.map +1 -1
- package/dist/agent-harness.d.mts +3 -3
- package/dist/agent-harness.d.ts +3 -3
- package/dist/agent-harness.js +67 -131
- package/dist/agent-harness.js.map +1 -1
- package/dist/agent-harness.mjs +67 -131
- package/dist/agent-harness.mjs.map +1 -1
- package/dist/cli/index.js +2394 -1009
- package/dist/{client-DFaCRSto.d.mts → client-BzgQvNyG.d.ts} +90 -17
- package/dist/{client-UdGt44dF.d.ts → client-DSkGcmmX.d.mts} +90 -17
- package/dist/index.d.mts +33 -11
- package/dist/index.d.ts +33 -11
- package/dist/index.js +2840 -1252
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +2826 -1253
- package/dist/index.mjs.map +1 -1
- package/dist/{spend-czRX7l0X.d.mts → spend-CNe-Ff6U.d.mts} +670 -98
- package/dist/{spend-czRX7l0X.d.ts → spend-CNe-Ff6U.d.ts} +670 -98
- package/dist/spend.d.mts +1 -1
- package/dist/spend.d.ts +1 -1
- package/package.json +5 -2
package/dist/index.mjs
CHANGED
|
@@ -3989,6 +3989,1285 @@ var init_wrapper = __esm({
|
|
|
3989
3989
|
wrapper_default = import_websocket.default;
|
|
3990
3990
|
}
|
|
3991
3991
|
});
|
|
3992
|
+
|
|
3993
|
+
// src/feed.ts
|
|
3994
|
+
function requirePositiveFeedPosition(value) {
|
|
3995
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
|
|
3996
|
+
throw new Error("Invalid feed chronology.");
|
|
3997
|
+
}
|
|
3998
|
+
return value;
|
|
3999
|
+
}
|
|
4000
|
+
function mergeFeedItemsBySequence(...collections) {
|
|
4001
|
+
const bySequence = /* @__PURE__ */ new Map();
|
|
4002
|
+
const sequenceById = /* @__PURE__ */ new Map();
|
|
4003
|
+
for (const collection of collections) {
|
|
4004
|
+
let priorSequence = 0;
|
|
4005
|
+
for (const item of collection) {
|
|
4006
|
+
const sequence = requirePositiveFeedPosition(item.sequence);
|
|
4007
|
+
if (sequence <= priorSequence || priorSequence > 0 && sequence !== priorSequence + 1) {
|
|
4008
|
+
throw new Error("Invalid feed chronology.");
|
|
4009
|
+
}
|
|
4010
|
+
priorSequence = sequence;
|
|
4011
|
+
const id = String(item.id || "");
|
|
4012
|
+
const knownSequence = sequenceById.get(id);
|
|
4013
|
+
const existing = bySequence.get(sequence);
|
|
4014
|
+
if (!id || knownSequence !== void 0 && knownSequence !== sequence || existing && existing.id !== id) {
|
|
4015
|
+
throw new Error("Invalid feed chronology.");
|
|
4016
|
+
}
|
|
4017
|
+
if (existing && !sameDurableItem(existing, item)) {
|
|
4018
|
+
throw new Error(
|
|
4019
|
+
"Invalid feed chronology: immutable occurrence collision."
|
|
4020
|
+
);
|
|
4021
|
+
}
|
|
4022
|
+
sequenceById.set(id, sequence);
|
|
4023
|
+
if (!existing) bySequence.set(sequence, item);
|
|
4024
|
+
}
|
|
4025
|
+
}
|
|
4026
|
+
return [...bySequence.entries()].sort(([left], [right]) => left - right).map(([, item]) => item);
|
|
4027
|
+
}
|
|
4028
|
+
function orderTransientFeedItems(items) {
|
|
4029
|
+
const byId = /* @__PURE__ */ new Map();
|
|
4030
|
+
const ordinalById = /* @__PURE__ */ new Map();
|
|
4031
|
+
const idByOrdinal = /* @__PURE__ */ new Map();
|
|
4032
|
+
for (const item of items) {
|
|
4033
|
+
const id = String(item.id || "");
|
|
4034
|
+
const ordinal = requirePositiveFeedPosition(item.ordinal);
|
|
4035
|
+
const knownOrdinal = ordinalById.get(id);
|
|
4036
|
+
const ordinalOwner = idByOrdinal.get(ordinal);
|
|
4037
|
+
if (!id || knownOrdinal !== void 0 && knownOrdinal !== ordinal || ordinalOwner && ordinalOwner !== id) {
|
|
4038
|
+
throw new Error("Invalid feed chronology.");
|
|
4039
|
+
}
|
|
4040
|
+
ordinalById.set(id, ordinal);
|
|
4041
|
+
idByOrdinal.set(ordinal, id);
|
|
4042
|
+
const existing = byId.get(id);
|
|
4043
|
+
if (existing && item.revision === existing.revision && stableValueFingerprint(item) !== stableValueFingerprint(existing)) {
|
|
4044
|
+
throw new Error(
|
|
4045
|
+
"Invalid feed chronology: conflicting transient revision."
|
|
4046
|
+
);
|
|
4047
|
+
}
|
|
4048
|
+
if (!existing || item.revision > existing.revision) byId.set(id, item);
|
|
4049
|
+
}
|
|
4050
|
+
return [...byId.values()].sort((left, right) => left.ordinal - right.ordinal);
|
|
4051
|
+
}
|
|
4052
|
+
var GRANULAR_FEED_DIAGNOSTIC_EVENT = "granular:feed-diagnostic";
|
|
4053
|
+
function normalizeFeedDiagnosticKind(kind) {
|
|
4054
|
+
return /^[a-z][a-z0-9_]{0,63}$/.test(kind) ? kind : "invalid_unknown_kind";
|
|
4055
|
+
}
|
|
4056
|
+
function normalizeFeedDiagnostic(diagnostic) {
|
|
4057
|
+
if (diagnostic.type !== "unknown_kind") return diagnostic;
|
|
4058
|
+
const kind = normalizeFeedDiagnosticKind(diagnostic.kind);
|
|
4059
|
+
return kind === diagnostic.kind ? diagnostic : { ...diagnostic, kind };
|
|
4060
|
+
}
|
|
4061
|
+
function emitFeedDiagnosticToDefaultSink(diagnostic, target = globalThis) {
|
|
4062
|
+
try {
|
|
4063
|
+
const normalized = normalizeFeedDiagnostic(diagnostic);
|
|
4064
|
+
const EventConstructor = target.CustomEvent || globalThis.CustomEvent;
|
|
4065
|
+
if (typeof target.dispatchEvent !== "function" || typeof EventConstructor !== "function") {
|
|
4066
|
+
return;
|
|
4067
|
+
}
|
|
4068
|
+
target.dispatchEvent(
|
|
4069
|
+
new EventConstructor(GRANULAR_FEED_DIAGNOSTIC_EVENT, {
|
|
4070
|
+
detail: Object.freeze({ ...normalized })
|
|
4071
|
+
})
|
|
4072
|
+
);
|
|
4073
|
+
} catch {
|
|
4074
|
+
}
|
|
4075
|
+
}
|
|
4076
|
+
function emitFeedDiagnostic(diagnostic, listener) {
|
|
4077
|
+
const normalized = normalizeFeedDiagnostic(diagnostic);
|
|
4078
|
+
emitFeedDiagnosticToDefaultSink(normalized);
|
|
4079
|
+
if (!listener) return;
|
|
4080
|
+
try {
|
|
4081
|
+
listener(normalized);
|
|
4082
|
+
} catch {
|
|
4083
|
+
console.warn("[Granular] Session feed diagnostic listener failed");
|
|
4084
|
+
}
|
|
4085
|
+
}
|
|
4086
|
+
function asRecord(value) {
|
|
4087
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4088
|
+
return null;
|
|
4089
|
+
}
|
|
4090
|
+
return value;
|
|
4091
|
+
}
|
|
4092
|
+
function isSafeInteger(value, minimum = 0) {
|
|
4093
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= minimum;
|
|
4094
|
+
}
|
|
4095
|
+
function finiteInteger(value, fallback, minimum = 0) {
|
|
4096
|
+
return isSafeInteger(value, minimum) ? value : fallback;
|
|
4097
|
+
}
|
|
4098
|
+
function cloneAndFreeze(value, seen = /* @__PURE__ */ new WeakMap()) {
|
|
4099
|
+
if (!value || typeof value !== "object") {
|
|
4100
|
+
return value;
|
|
4101
|
+
}
|
|
4102
|
+
const object = value;
|
|
4103
|
+
const cached = seen.get(object);
|
|
4104
|
+
if (cached) {
|
|
4105
|
+
return cached;
|
|
4106
|
+
}
|
|
4107
|
+
if (Array.isArray(value)) {
|
|
4108
|
+
const result2 = [];
|
|
4109
|
+
seen.set(object, result2);
|
|
4110
|
+
for (const entry of value) {
|
|
4111
|
+
result2.push(cloneAndFreeze(entry, seen));
|
|
4112
|
+
}
|
|
4113
|
+
return Object.freeze(result2);
|
|
4114
|
+
}
|
|
4115
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
4116
|
+
seen.set(object, result);
|
|
4117
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
4118
|
+
result[key] = cloneAndFreeze(entry, seen);
|
|
4119
|
+
}
|
|
4120
|
+
return Object.freeze(result);
|
|
4121
|
+
}
|
|
4122
|
+
function snapshotWith(snapshot, patch) {
|
|
4123
|
+
return Object.freeze({ ...snapshot, ...patch });
|
|
4124
|
+
}
|
|
4125
|
+
function emptyFeedSnapshot(isHydrated = false) {
|
|
4126
|
+
return Object.freeze({
|
|
4127
|
+
tail: Object.freeze([]),
|
|
4128
|
+
transients: Object.freeze([]),
|
|
4129
|
+
revision: 0,
|
|
4130
|
+
documentEpoch: 0,
|
|
4131
|
+
documentRevision: 0,
|
|
4132
|
+
lastSequence: 0,
|
|
4133
|
+
archivedThroughSequence: 0,
|
|
4134
|
+
hasOlder: false,
|
|
4135
|
+
isHydrated,
|
|
4136
|
+
isRepairing: false,
|
|
4137
|
+
error: null
|
|
4138
|
+
});
|
|
4139
|
+
}
|
|
4140
|
+
function hasCanonicalSessionFeedActivation(document) {
|
|
4141
|
+
const documentRecord = asRecord(document);
|
|
4142
|
+
const feed = asRecord(documentRecord?.feed);
|
|
4143
|
+
const activation = asRecord(feed?.activation);
|
|
4144
|
+
return activation?.mode === "canonical";
|
|
4145
|
+
}
|
|
4146
|
+
function isCanonicalSessionFeedDocument(document) {
|
|
4147
|
+
return hasCanonicalSessionFeedActivation(document);
|
|
4148
|
+
}
|
|
4149
|
+
function canonicalSessionFeedStructureError(document) {
|
|
4150
|
+
if (!hasCanonicalSessionFeedActivation(document)) return null;
|
|
4151
|
+
const documentRecord = asRecord(document);
|
|
4152
|
+
const feed = asRecord(documentRecord?.feed);
|
|
4153
|
+
const activation = asRecord(feed?.activation);
|
|
4154
|
+
if (feed?.schemaVersion !== 1) {
|
|
4155
|
+
return new Error("Canonical feed schemaVersion must be 1.");
|
|
4156
|
+
}
|
|
4157
|
+
if (!isSafeInteger(activation?.activatedAt)) {
|
|
4158
|
+
return new Error("Canonical feed activation timestamp is invalid.");
|
|
4159
|
+
}
|
|
4160
|
+
if (!Array.isArray(feed.tail)) {
|
|
4161
|
+
return new Error("Canonical feed tail is not an array.");
|
|
4162
|
+
}
|
|
4163
|
+
if (!asRecord(feed.transientById)) {
|
|
4164
|
+
return new Error("Canonical feed transientById is not an object.");
|
|
4165
|
+
}
|
|
4166
|
+
if ([
|
|
4167
|
+
feed.revision,
|
|
4168
|
+
feed.lastSequence,
|
|
4169
|
+
feed.archivedThroughSequence,
|
|
4170
|
+
feed.lastTransientOrdinal,
|
|
4171
|
+
documentRecord?.documentEpoch,
|
|
4172
|
+
documentRecord?.documentRevision
|
|
4173
|
+
].some((value) => !isSafeInteger(value))) {
|
|
4174
|
+
return new Error("Canonical feed chronology scalar is invalid.");
|
|
4175
|
+
}
|
|
4176
|
+
if (!isSafeInteger(documentRecord?.documentEpoch, 1)) {
|
|
4177
|
+
return new Error("Canonical feed documentEpoch must be positive.");
|
|
4178
|
+
}
|
|
4179
|
+
return null;
|
|
4180
|
+
}
|
|
4181
|
+
function isNonEmptyString(value) {
|
|
4182
|
+
return typeof value === "string" && value.length > 0;
|
|
4183
|
+
}
|
|
4184
|
+
function isKnownDurableFeedKind(kind) {
|
|
4185
|
+
return kind === "message" || kind === "feedback" || kind === "objects" || kind === "table" || kind === "artifact" || kind === "file" || kind === "action_suggestion" || kind === "prompt";
|
|
4186
|
+
}
|
|
4187
|
+
function hasValidKnownFeedPayload(kind, value) {
|
|
4188
|
+
const payload = asRecord(value);
|
|
4189
|
+
if (!payload) return false;
|
|
4190
|
+
switch (kind) {
|
|
4191
|
+
case "message":
|
|
4192
|
+
return (payload.role === "user" || payload.role === "assistant" || payload.role === "system") && typeof payload.text === "string";
|
|
4193
|
+
case "feedback":
|
|
4194
|
+
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");
|
|
4195
|
+
case "objects":
|
|
4196
|
+
return Array.isArray(payload.refs) && payload.refs.every((value2) => {
|
|
4197
|
+
const ref = asRecord(value2);
|
|
4198
|
+
return ref?.type === "entry" && isNonEmptyString(ref.path) || ref?.type === "list" && isNonEmptyString(ref.name) || ref?.type === "variable" && isNonEmptyString(ref.name);
|
|
4199
|
+
});
|
|
4200
|
+
case "table":
|
|
4201
|
+
return isNonEmptyString(payload.tableId) && Array.isArray(payload.columns) && Array.isArray(payload.rows);
|
|
4202
|
+
case "artifact": {
|
|
4203
|
+
const fallback = asRecord(payload.fallback);
|
|
4204
|
+
return isNonEmptyString(payload.artifactId) && Boolean(fallback) && isNonEmptyString(fallback?.label) && (fallback?.kind === "effect" || fallback?.kind === "batch" || fallback?.kind === "state_path");
|
|
4205
|
+
}
|
|
4206
|
+
case "file": {
|
|
4207
|
+
const fallback = asRecord(payload.fallback);
|
|
4208
|
+
return isNonEmptyString(payload.fileId) && Boolean(fallback) && isNonEmptyString(fallback?.filename);
|
|
4209
|
+
}
|
|
4210
|
+
case "action_suggestion":
|
|
4211
|
+
return isNonEmptyString(payload.suggestionId) && isNonEmptyString(payload.label);
|
|
4212
|
+
case "prompt":
|
|
4213
|
+
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);
|
|
4214
|
+
default:
|
|
4215
|
+
return true;
|
|
4216
|
+
}
|
|
4217
|
+
}
|
|
4218
|
+
function normalizeFeedItem(value) {
|
|
4219
|
+
const item = asRecord(value);
|
|
4220
|
+
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)) {
|
|
4221
|
+
return null;
|
|
4222
|
+
}
|
|
4223
|
+
return cloneAndFreeze(item);
|
|
4224
|
+
}
|
|
4225
|
+
function normalizeTransientFeedItem(value) {
|
|
4226
|
+
const item = asRecord(value);
|
|
4227
|
+
const payload = asRecord(item?.payload);
|
|
4228
|
+
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))) {
|
|
4229
|
+
return null;
|
|
4230
|
+
}
|
|
4231
|
+
return cloneAndFreeze(item);
|
|
4232
|
+
}
|
|
4233
|
+
var FEED_TRANSIENT_FEEDBACK_TONES = /* @__PURE__ */ new Set(["info", "working", "awaiting", "warning", "error"]);
|
|
4234
|
+
function isFeedTransientFeedbackTone(value) {
|
|
4235
|
+
return typeof value === "string" && FEED_TRANSIENT_FEEDBACK_TONES.has(value);
|
|
4236
|
+
}
|
|
4237
|
+
function normalizeFeedItems(values) {
|
|
4238
|
+
if (!Array.isArray(values)) {
|
|
4239
|
+
return {
|
|
4240
|
+
items: [],
|
|
4241
|
+
error: new Error("Canonical feed tail is not an array.")
|
|
4242
|
+
};
|
|
4243
|
+
}
|
|
4244
|
+
const items = [];
|
|
4245
|
+
const ids = /* @__PURE__ */ new Map();
|
|
4246
|
+
const sequences = /* @__PURE__ */ new Map();
|
|
4247
|
+
const itemBySequence = /* @__PURE__ */ new Map();
|
|
4248
|
+
let error = null;
|
|
4249
|
+
for (const value of values) {
|
|
4250
|
+
const item = normalizeFeedItem(value);
|
|
4251
|
+
if (!item) {
|
|
4252
|
+
error ||= new Error("Canonical feed contains an invalid durable item.");
|
|
4253
|
+
continue;
|
|
4254
|
+
}
|
|
4255
|
+
const idSequence = ids.get(item.id);
|
|
4256
|
+
const sequenceId = sequences.get(item.sequence);
|
|
4257
|
+
if (idSequence !== void 0 && idSequence !== item.sequence || sequenceId !== void 0 && sequenceId !== item.id) {
|
|
4258
|
+
error ||= new Error(
|
|
4259
|
+
`Canonical feed identity conflict at sequence ${item.sequence}.`
|
|
4260
|
+
);
|
|
4261
|
+
continue;
|
|
4262
|
+
}
|
|
4263
|
+
if (idSequence === item.sequence || sequenceId === item.id) {
|
|
4264
|
+
const duplicate = itemBySequence.get(item.sequence);
|
|
4265
|
+
if (duplicate && !sameDurableItem(duplicate, item)) {
|
|
4266
|
+
error ||= new Error(
|
|
4267
|
+
`Immutable feed item ${item.id} has conflicting payloads.`
|
|
4268
|
+
);
|
|
4269
|
+
}
|
|
4270
|
+
continue;
|
|
4271
|
+
}
|
|
4272
|
+
ids.set(item.id, item.sequence);
|
|
4273
|
+
sequences.set(item.sequence, item.id);
|
|
4274
|
+
itemBySequence.set(item.sequence, item);
|
|
4275
|
+
items.push(item);
|
|
4276
|
+
}
|
|
4277
|
+
items.sort((left, right) => left.sequence - right.sequence);
|
|
4278
|
+
return { items: Object.freeze(items), error };
|
|
4279
|
+
}
|
|
4280
|
+
function normalizeTransients(values) {
|
|
4281
|
+
const record = asRecord(values);
|
|
4282
|
+
if (!record) {
|
|
4283
|
+
return {
|
|
4284
|
+
items: [],
|
|
4285
|
+
error: new Error("Canonical feed transientById is not an object.")
|
|
4286
|
+
};
|
|
4287
|
+
}
|
|
4288
|
+
let error = null;
|
|
4289
|
+
const byId = /* @__PURE__ */ new Map();
|
|
4290
|
+
const idByOrdinal = /* @__PURE__ */ new Map();
|
|
4291
|
+
for (const [key, value] of Object.entries(record)) {
|
|
4292
|
+
const item = normalizeTransientFeedItem(value);
|
|
4293
|
+
if (!item || item.id !== key) {
|
|
4294
|
+
error ||= new Error("Canonical feed contains an invalid transient item.");
|
|
4295
|
+
continue;
|
|
4296
|
+
}
|
|
4297
|
+
const ordinalOwner = idByOrdinal.get(item.ordinal);
|
|
4298
|
+
if (ordinalOwner && ordinalOwner !== item.id) {
|
|
4299
|
+
error ||= new Error(
|
|
4300
|
+
`Canonical feed transient ordinal ${item.ordinal} is not unique.`
|
|
4301
|
+
);
|
|
4302
|
+
} else {
|
|
4303
|
+
idByOrdinal.set(item.ordinal, item.id);
|
|
4304
|
+
}
|
|
4305
|
+
const current = byId.get(item.id);
|
|
4306
|
+
if (!current || item.revision > current.revision) {
|
|
4307
|
+
byId.set(item.id, item);
|
|
4308
|
+
}
|
|
4309
|
+
}
|
|
4310
|
+
const items = [...byId.values()].sort(
|
|
4311
|
+
(left, right) => left.ordinal - right.ordinal || left.id.localeCompare(right.id)
|
|
4312
|
+
);
|
|
4313
|
+
return {
|
|
4314
|
+
items: Object.freeze(items),
|
|
4315
|
+
error
|
|
4316
|
+
};
|
|
4317
|
+
}
|
|
4318
|
+
function readSessionFeedSnapshot(document, options = {}) {
|
|
4319
|
+
const isHydrated = options.isHydrated ?? true;
|
|
4320
|
+
if (!hasCanonicalSessionFeedActivation(document)) {
|
|
4321
|
+
return snapshotWith(emptyFeedSnapshot(isHydrated), {
|
|
4322
|
+
isRepairing: options.isRepairing || false,
|
|
4323
|
+
error: options.error || (isHydrated ? new Error("Canonical session feed-v1 is required.") : null)
|
|
4324
|
+
});
|
|
4325
|
+
}
|
|
4326
|
+
const documentRecord = asRecord(document);
|
|
4327
|
+
const feed = asRecord(documentRecord.feed);
|
|
4328
|
+
const structureError = canonicalSessionFeedStructureError(document);
|
|
4329
|
+
if (structureError) {
|
|
4330
|
+
const lastSequence2 = finiteInteger(feed.lastSequence, 0);
|
|
4331
|
+
const archivedThroughSequence2 = finiteInteger(
|
|
4332
|
+
feed.archivedThroughSequence,
|
|
4333
|
+
0
|
|
4334
|
+
);
|
|
4335
|
+
return snapshotWith(emptyFeedSnapshot(isHydrated), {
|
|
4336
|
+
revision: finiteInteger(feed.revision, 0),
|
|
4337
|
+
documentEpoch: finiteInteger(documentRecord.documentEpoch, 0),
|
|
4338
|
+
documentRevision: finiteInteger(documentRecord.documentRevision, 0),
|
|
4339
|
+
lastSequence: lastSequence2,
|
|
4340
|
+
archivedThroughSequence: archivedThroughSequence2,
|
|
4341
|
+
hasOlder: archivedThroughSequence2 > 0,
|
|
4342
|
+
isRepairing: options.isRepairing || false,
|
|
4343
|
+
error: options.error || structureError
|
|
4344
|
+
});
|
|
4345
|
+
}
|
|
4346
|
+
const normalizedTail = normalizeFeedItems(feed.tail);
|
|
4347
|
+
const normalizedTransients = normalizeTransients(feed.transientById);
|
|
4348
|
+
const lastSequence = finiteInteger(feed.lastSequence, 0);
|
|
4349
|
+
const archivedThroughSequence = finiteInteger(
|
|
4350
|
+
feed.archivedThroughSequence,
|
|
4351
|
+
0
|
|
4352
|
+
);
|
|
4353
|
+
let error = options.error || normalizedTail.error || normalizedTransients.error;
|
|
4354
|
+
let expected = archivedThroughSequence + 1;
|
|
4355
|
+
for (const item of normalizedTail.items) {
|
|
4356
|
+
if (item.sequence !== expected) {
|
|
4357
|
+
error ||= new Error(
|
|
4358
|
+
`Canonical feed tail has a sequence gap before ${item.sequence}; expected ${expected}.`
|
|
4359
|
+
);
|
|
4360
|
+
break;
|
|
4361
|
+
}
|
|
4362
|
+
expected += 1;
|
|
4363
|
+
}
|
|
4364
|
+
const tailLastSequence = normalizedTail.items[normalizedTail.items.length - 1]?.sequence || archivedThroughSequence;
|
|
4365
|
+
if (tailLastSequence !== lastSequence) {
|
|
4366
|
+
error ||= new Error(
|
|
4367
|
+
`Canonical feed tail ends at ${tailLastSequence}, but lastSequence is ${lastSequence}.`
|
|
4368
|
+
);
|
|
4369
|
+
}
|
|
4370
|
+
if (archivedThroughSequence > lastSequence) {
|
|
4371
|
+
error ||= new Error(
|
|
4372
|
+
"Canonical feed archivedThroughSequence exceeds lastSequence."
|
|
4373
|
+
);
|
|
4374
|
+
}
|
|
4375
|
+
return Object.freeze({
|
|
4376
|
+
tail: normalizedTail.items,
|
|
4377
|
+
transients: normalizedTransients.items,
|
|
4378
|
+
revision: finiteInteger(feed.revision, 0),
|
|
4379
|
+
documentEpoch: finiteInteger(documentRecord.documentEpoch, 0),
|
|
4380
|
+
documentRevision: finiteInteger(documentRecord.documentRevision, 0),
|
|
4381
|
+
lastSequence,
|
|
4382
|
+
archivedThroughSequence,
|
|
4383
|
+
hasOlder: archivedThroughSequence > 0,
|
|
4384
|
+
isHydrated,
|
|
4385
|
+
isRepairing: options.isRepairing || false,
|
|
4386
|
+
error
|
|
4387
|
+
});
|
|
4388
|
+
}
|
|
4389
|
+
function validateFeedListOptions(options) {
|
|
4390
|
+
if (options.afterSequence !== void 0 && options.beforeSequence !== void 0) {
|
|
4391
|
+
throw new RangeError(
|
|
4392
|
+
"Feed list accepts afterSequence or beforeSequence, not both."
|
|
4393
|
+
);
|
|
4394
|
+
}
|
|
4395
|
+
for (const [name, value] of [
|
|
4396
|
+
["afterSequence", options.afterSequence],
|
|
4397
|
+
["beforeSequence", options.beforeSequence]
|
|
4398
|
+
]) {
|
|
4399
|
+
if (value !== void 0 && !isSafeInteger(value)) {
|
|
4400
|
+
throw new RangeError(`${name} must be a non-negative integer.`);
|
|
4401
|
+
}
|
|
4402
|
+
}
|
|
4403
|
+
if (options.limit !== void 0 && (!isSafeInteger(options.limit, 1) || options.limit > 500)) {
|
|
4404
|
+
throw new RangeError("Feed list limit must be an integer from 1 to 500.");
|
|
4405
|
+
}
|
|
4406
|
+
return { ...options };
|
|
4407
|
+
}
|
|
4408
|
+
function normalizeFeedPage(value) {
|
|
4409
|
+
const page = asRecord(value);
|
|
4410
|
+
if (!page) {
|
|
4411
|
+
throw new Error("Feed list transport returned an invalid page.");
|
|
4412
|
+
}
|
|
4413
|
+
const normalized = normalizeFeedItems(page.items);
|
|
4414
|
+
if (normalized.error) {
|
|
4415
|
+
throw normalized.error;
|
|
4416
|
+
}
|
|
4417
|
+
const first = normalized.items[0]?.sequence || null;
|
|
4418
|
+
const last = normalized.items[normalized.items.length - 1]?.sequence || null;
|
|
4419
|
+
for (let index = 1; index < normalized.items.length; index += 1) {
|
|
4420
|
+
if (normalized.items[index].sequence !== normalized.items[index - 1].sequence + 1) {
|
|
4421
|
+
throw new Error("Feed list page contains a sequence gap.");
|
|
4422
|
+
}
|
|
4423
|
+
}
|
|
4424
|
+
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") {
|
|
4425
|
+
throw new Error(
|
|
4426
|
+
"Feed list page sequence metadata does not match its items."
|
|
4427
|
+
);
|
|
4428
|
+
}
|
|
4429
|
+
return Object.freeze({
|
|
4430
|
+
items: normalized.items,
|
|
4431
|
+
firstSequence: first,
|
|
4432
|
+
lastSequence: last,
|
|
4433
|
+
hasMoreBefore: page.hasMoreBefore,
|
|
4434
|
+
hasMoreAfter: page.hasMoreAfter
|
|
4435
|
+
});
|
|
4436
|
+
}
|
|
4437
|
+
function stableValueFingerprint(value, ancestors = /* @__PURE__ */ new WeakSet()) {
|
|
4438
|
+
if (!value || typeof value !== "object") {
|
|
4439
|
+
return JSON.stringify(value);
|
|
4440
|
+
}
|
|
4441
|
+
if (ancestors.has(value)) return '"[circular]"';
|
|
4442
|
+
ancestors.add(value);
|
|
4443
|
+
const result = Array.isArray(value) ? `[${value.map((entry) => stableValueFingerprint(entry, ancestors)).join(",")}]` : `{${Object.keys(value).sort().map(
|
|
4444
|
+
(key) => `${JSON.stringify(key)}:${stableValueFingerprint(
|
|
4445
|
+
value[key],
|
|
4446
|
+
ancestors
|
|
4447
|
+
)}`
|
|
4448
|
+
).join(",")}}`;
|
|
4449
|
+
ancestors.delete(value);
|
|
4450
|
+
return result;
|
|
4451
|
+
}
|
|
4452
|
+
function sameDurableItem(left, right) {
|
|
4453
|
+
return stableValueFingerprint(left) === stableValueFingerprint(right);
|
|
4454
|
+
}
|
|
4455
|
+
function contiguousLocalTailWatermark(snapshot) {
|
|
4456
|
+
let watermark = 0;
|
|
4457
|
+
for (const item of snapshot.tail) {
|
|
4458
|
+
if (item.sequence !== watermark + 1) break;
|
|
4459
|
+
watermark = item.sequence;
|
|
4460
|
+
}
|
|
4461
|
+
return watermark;
|
|
4462
|
+
}
|
|
4463
|
+
var SessionFeedController = class {
|
|
4464
|
+
snapshot;
|
|
4465
|
+
canonical;
|
|
4466
|
+
canonicalStructureValid;
|
|
4467
|
+
deliveredThrough;
|
|
4468
|
+
knownBySequence = /* @__PURE__ */ new Map();
|
|
4469
|
+
sequenceById = /* @__PURE__ */ new Map();
|
|
4470
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
4471
|
+
listTransport;
|
|
4472
|
+
repairGeneration = 0;
|
|
4473
|
+
repairPromise = null;
|
|
4474
|
+
repairRetryHandle = null;
|
|
4475
|
+
repairRetryToken = 0;
|
|
4476
|
+
repairFailureAttempt = 0;
|
|
4477
|
+
scheduleRepairRetryCallback;
|
|
4478
|
+
cancelRepairRetryCallback;
|
|
4479
|
+
initialRepairRetryDelayMs;
|
|
4480
|
+
maxRepairRetryDelayMs;
|
|
4481
|
+
disposed = false;
|
|
4482
|
+
diagnosticListener;
|
|
4483
|
+
diagnosticNow;
|
|
4484
|
+
observedUnknownPositions = /* @__PURE__ */ new Set();
|
|
4485
|
+
constructor(initialDocument, options = {}) {
|
|
4486
|
+
this.diagnosticListener = options.onDiagnostic || null;
|
|
4487
|
+
this.diagnosticNow = options.now || Date.now;
|
|
4488
|
+
this.scheduleRepairRetryCallback = options.scheduleRepairRetry || ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
4489
|
+
this.cancelRepairRetryCallback = options.cancelRepairRetry || ((handle) => clearTimeout(handle));
|
|
4490
|
+
this.initialRepairRetryDelayMs = Math.max(
|
|
4491
|
+
1,
|
|
4492
|
+
options.initialRepairRetryDelayMs ?? 500
|
|
4493
|
+
);
|
|
4494
|
+
this.maxRepairRetryDelayMs = Math.max(
|
|
4495
|
+
this.initialRepairRetryDelayMs,
|
|
4496
|
+
options.maxRepairRetryDelayMs ?? 1e4
|
|
4497
|
+
);
|
|
4498
|
+
this.canonical = hasCanonicalSessionFeedActivation(initialDocument);
|
|
4499
|
+
this.canonicalStructureValid = this.canonical && canonicalSessionFeedStructureError(initialDocument) === null;
|
|
4500
|
+
this.snapshot = readSessionFeedSnapshot(initialDocument, {
|
|
4501
|
+
isHydrated: options.isHydrated ?? this.canonical
|
|
4502
|
+
});
|
|
4503
|
+
this.listTransport = options.listTransport || null;
|
|
4504
|
+
this.deliveredThrough = contiguousLocalTailWatermark(this.snapshot);
|
|
4505
|
+
const identityError = this.remember(this.snapshot.tail);
|
|
4506
|
+
const needsRepair = this.canonicalStructureValid && this.deliveredThrough < this.snapshot.lastSequence;
|
|
4507
|
+
if (identityError || needsRepair) {
|
|
4508
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
4509
|
+
isRepairing: needsRepair && Boolean(this.listTransport),
|
|
4510
|
+
error: identityError || this.snapshot.error
|
|
4511
|
+
});
|
|
4512
|
+
}
|
|
4513
|
+
if (needsRepair) {
|
|
4514
|
+
this.startRepair(this.snapshot.lastSequence);
|
|
4515
|
+
}
|
|
4516
|
+
}
|
|
4517
|
+
setListTransport(transport) {
|
|
4518
|
+
if (this.disposed) return;
|
|
4519
|
+
if (transport !== this.listTransport) {
|
|
4520
|
+
this.cancelScheduledRepairRetry();
|
|
4521
|
+
this.repairFailureAttempt = 0;
|
|
4522
|
+
this.repairGeneration += 1;
|
|
4523
|
+
this.repairPromise = null;
|
|
4524
|
+
}
|
|
4525
|
+
this.listTransport = transport;
|
|
4526
|
+
if (transport && this.canonical && this.canonicalStructureValid && this.deliveredThrough < this.snapshot.lastSequence) {
|
|
4527
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
4528
|
+
isRepairing: true,
|
|
4529
|
+
error: null
|
|
4530
|
+
});
|
|
4531
|
+
this.startRepair(this.snapshot.lastSequence);
|
|
4532
|
+
}
|
|
4533
|
+
}
|
|
4534
|
+
getSnapshot() {
|
|
4535
|
+
return this.snapshot;
|
|
4536
|
+
}
|
|
4537
|
+
/**
|
|
4538
|
+
* Stop background archive work when its owning Session is replaced or
|
|
4539
|
+
* explicitly disconnected. Late transport completions are quarantined by
|
|
4540
|
+
* the generation check and cannot update subscribers.
|
|
4541
|
+
*/
|
|
4542
|
+
dispose() {
|
|
4543
|
+
if (this.disposed) return;
|
|
4544
|
+
this.disposed = true;
|
|
4545
|
+
this.cancelScheduledRepairRetry();
|
|
4546
|
+
this.repairGeneration += 1;
|
|
4547
|
+
this.repairPromise = null;
|
|
4548
|
+
this.listTransport = null;
|
|
4549
|
+
this.subscribers.clear();
|
|
4550
|
+
}
|
|
4551
|
+
async list(options = {}) {
|
|
4552
|
+
const validated = validateFeedListOptions(options);
|
|
4553
|
+
if (!this.listTransport) {
|
|
4554
|
+
throw new Error(
|
|
4555
|
+
"Historical feed transport is unavailable for this Session."
|
|
4556
|
+
);
|
|
4557
|
+
}
|
|
4558
|
+
const page = normalizeFeedPage(await this.listTransport(validated));
|
|
4559
|
+
this.observeUnknownKinds(page.items);
|
|
4560
|
+
return page;
|
|
4561
|
+
}
|
|
4562
|
+
subscribe(listener, options = {}) {
|
|
4563
|
+
const afterSequence = options.afterSequence;
|
|
4564
|
+
if (afterSequence !== void 0 && !isSafeInteger(afterSequence)) {
|
|
4565
|
+
throw new RangeError(
|
|
4566
|
+
"Feed afterSequence must be a non-negative safe integer."
|
|
4567
|
+
);
|
|
4568
|
+
}
|
|
4569
|
+
this.subscribers.add(listener);
|
|
4570
|
+
if (afterSequence !== void 0 && afterSequence < this.snapshot.lastSequence) {
|
|
4571
|
+
const items = this.snapshot.tail.filter(
|
|
4572
|
+
(item) => item.sequence > afterSequence
|
|
4573
|
+
);
|
|
4574
|
+
if (items.length > 0 && items[0].sequence === afterSequence + 1 && items[items.length - 1].sequence === this.snapshot.lastSequence) {
|
|
4575
|
+
listener({ type: "append", items });
|
|
4576
|
+
if (this.snapshot.transients.length > 0) {
|
|
4577
|
+
listener({
|
|
4578
|
+
type: "transients",
|
|
4579
|
+
items: this.snapshot.transients,
|
|
4580
|
+
revision: this.snapshot.revision
|
|
4581
|
+
});
|
|
4582
|
+
}
|
|
4583
|
+
} else {
|
|
4584
|
+
listener({ type: "reset", snapshot: this.snapshot });
|
|
4585
|
+
}
|
|
4586
|
+
} else {
|
|
4587
|
+
listener({ type: "reset", snapshot: this.snapshot });
|
|
4588
|
+
}
|
|
4589
|
+
return () => {
|
|
4590
|
+
this.subscribers.delete(listener);
|
|
4591
|
+
};
|
|
4592
|
+
}
|
|
4593
|
+
/**
|
|
4594
|
+
* Accept the latest synced document. Calls may arrive out of order after a
|
|
4595
|
+
* reconnect; freshness watermarks prevent an older snapshot from regressing
|
|
4596
|
+
* durable positions or transient state.
|
|
4597
|
+
*/
|
|
4598
|
+
updateDocument(document, options = {}) {
|
|
4599
|
+
if (this.disposed) return;
|
|
4600
|
+
const nextCanonical = hasCanonicalSessionFeedActivation(document);
|
|
4601
|
+
const next = readSessionFeedSnapshot(document, { isHydrated: true });
|
|
4602
|
+
if (!nextCanonical) {
|
|
4603
|
+
if (!this.canonical) {
|
|
4604
|
+
this.snapshot = next;
|
|
4605
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4606
|
+
return;
|
|
4607
|
+
}
|
|
4608
|
+
this.rejectSnapshotRegression("canonical_deactivation", next);
|
|
4609
|
+
return;
|
|
4610
|
+
}
|
|
4611
|
+
const nextStructureError = canonicalSessionFeedStructureError(document);
|
|
4612
|
+
if (nextStructureError) {
|
|
4613
|
+
if (!this.canonical) {
|
|
4614
|
+
this.canonical = true;
|
|
4615
|
+
this.canonicalStructureValid = false;
|
|
4616
|
+
this.cancelScheduledRepairRetry();
|
|
4617
|
+
this.repairFailureAttempt = 0;
|
|
4618
|
+
this.repairGeneration += 1;
|
|
4619
|
+
this.repairPromise = null;
|
|
4620
|
+
this.deliveredThrough = 0;
|
|
4621
|
+
this.knownBySequence.clear();
|
|
4622
|
+
this.sequenceById.clear();
|
|
4623
|
+
this.observedUnknownPositions.clear();
|
|
4624
|
+
this.snapshot = next;
|
|
4625
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4626
|
+
return;
|
|
4627
|
+
}
|
|
4628
|
+
this.rejectSnapshotRegression("invalid_canonical_document", next);
|
|
4629
|
+
if (!this.canonicalStructureValid) {
|
|
4630
|
+
const current2 = this.snapshot;
|
|
4631
|
+
const incomingIsNewer = next.documentEpoch > current2.documentEpoch || next.documentEpoch === current2.documentEpoch && next.documentRevision >= current2.documentRevision;
|
|
4632
|
+
if (incomingIsNewer && next.lastSequence >= current2.lastSequence) {
|
|
4633
|
+
this.snapshot = next;
|
|
4634
|
+
}
|
|
4635
|
+
} else {
|
|
4636
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
4637
|
+
error: nextStructureError
|
|
4638
|
+
});
|
|
4639
|
+
}
|
|
4640
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4641
|
+
return;
|
|
4642
|
+
}
|
|
4643
|
+
if (!this.canonical || options.forceReset) {
|
|
4644
|
+
this.acceptReset(next);
|
|
4645
|
+
return;
|
|
4646
|
+
}
|
|
4647
|
+
if (!this.canonicalStructureValid) {
|
|
4648
|
+
const current2 = this.snapshot;
|
|
4649
|
+
if (next.documentEpoch < current2.documentEpoch) {
|
|
4650
|
+
this.rejectSnapshotRegression("older_document_epoch", next);
|
|
4651
|
+
return;
|
|
4652
|
+
}
|
|
4653
|
+
if (next.lastSequence < current2.lastSequence) {
|
|
4654
|
+
this.rejectSnapshotRegression("last_sequence_regression", next);
|
|
4655
|
+
return;
|
|
4656
|
+
}
|
|
4657
|
+
if (next.documentEpoch === current2.documentEpoch && next.documentRevision < current2.documentRevision) {
|
|
4658
|
+
this.rejectSnapshotRegression("document_revision_regression", next);
|
|
4659
|
+
return;
|
|
4660
|
+
}
|
|
4661
|
+
if (next.documentEpoch === current2.documentEpoch && next.revision < current2.revision) {
|
|
4662
|
+
this.rejectSnapshotRegression("feed_revision_regression", next);
|
|
4663
|
+
return;
|
|
4664
|
+
}
|
|
4665
|
+
this.acceptReset(next);
|
|
4666
|
+
return;
|
|
4667
|
+
}
|
|
4668
|
+
const current = this.snapshot;
|
|
4669
|
+
if (next.documentEpoch < current.documentEpoch) {
|
|
4670
|
+
this.rejectSnapshotRegression("older_document_epoch", next);
|
|
4671
|
+
return;
|
|
4672
|
+
}
|
|
4673
|
+
if (next.documentEpoch > current.documentEpoch) {
|
|
4674
|
+
if (next.lastSequence < current.lastSequence) {
|
|
4675
|
+
this.rejectSnapshotRegression("last_sequence_regression", next);
|
|
4676
|
+
return;
|
|
4677
|
+
}
|
|
4678
|
+
const identityError2 = this.findIdentityConflict(next.tail);
|
|
4679
|
+
if (identityError2) {
|
|
4680
|
+
this.rejectSnapshotRegression("identity_conflict", next);
|
|
4681
|
+
this.snapshot = snapshotWith(current, { error: identityError2 });
|
|
4682
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4683
|
+
return;
|
|
4684
|
+
}
|
|
4685
|
+
this.acceptReset(next);
|
|
4686
|
+
return;
|
|
4687
|
+
}
|
|
4688
|
+
if (next.lastSequence < current.lastSequence) {
|
|
4689
|
+
this.rejectSnapshotRegression("last_sequence_regression", next);
|
|
4690
|
+
if (next.documentRevision > current.documentRevision) {
|
|
4691
|
+
this.emit({
|
|
4692
|
+
type: "resource_refresh",
|
|
4693
|
+
documentRevision: next.documentRevision
|
|
4694
|
+
});
|
|
4695
|
+
this.snapshot = snapshotWith(current, {
|
|
4696
|
+
documentRevision: next.documentRevision
|
|
4697
|
+
});
|
|
4698
|
+
}
|
|
4699
|
+
return;
|
|
4700
|
+
}
|
|
4701
|
+
const previousDocumentRevision = current.documentRevision;
|
|
4702
|
+
const previousRevision = current.revision;
|
|
4703
|
+
if (next.lastSequence > current.lastSequence && next.revision <= previousRevision) {
|
|
4704
|
+
this.rejectSnapshotRegression("last_sequence_without_revision", next);
|
|
4705
|
+
const documentRevision = Math.max(
|
|
4706
|
+
previousDocumentRevision,
|
|
4707
|
+
next.documentRevision
|
|
4708
|
+
);
|
|
4709
|
+
this.snapshot = snapshotWith(current, {
|
|
4710
|
+
documentRevision,
|
|
4711
|
+
error: new Error(
|
|
4712
|
+
"Feed lastSequence advanced without a newer feed revision."
|
|
4713
|
+
)
|
|
4714
|
+
});
|
|
4715
|
+
if (next.documentRevision > previousDocumentRevision) {
|
|
4716
|
+
this.emit({
|
|
4717
|
+
type: "resource_refresh",
|
|
4718
|
+
documentRevision: next.documentRevision
|
|
4719
|
+
});
|
|
4720
|
+
}
|
|
4721
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4722
|
+
return;
|
|
4723
|
+
}
|
|
4724
|
+
const previousTransients = current.transients;
|
|
4725
|
+
const transientIdentityError = next.revision === previousRevision && !sameTransientSet(previousTransients, next.transients) ? new Error("Transient feed changed without a newer feed revision.") : null;
|
|
4726
|
+
const transientSelection = this.selectNewerTransients(current, next);
|
|
4727
|
+
const mergedTransients = transientSelection.items;
|
|
4728
|
+
const identityError = transientIdentityError || transientSelection.error || this.remember(next.tail);
|
|
4729
|
+
if (identityError) {
|
|
4730
|
+
this.rejectSnapshotRegression("identity_conflict", next);
|
|
4731
|
+
this.snapshot = snapshotWith(current, {
|
|
4732
|
+
documentRevision: Math.max(
|
|
4733
|
+
previousDocumentRevision,
|
|
4734
|
+
next.documentRevision
|
|
4735
|
+
),
|
|
4736
|
+
error: identityError
|
|
4737
|
+
});
|
|
4738
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4739
|
+
return;
|
|
4740
|
+
}
|
|
4741
|
+
const keepCurrentFeedState = next.lastSequence === current.lastSequence && next.revision < current.revision;
|
|
4742
|
+
if (keepCurrentFeedState) {
|
|
4743
|
+
this.rejectSnapshotRegression("feed_revision_regression", next);
|
|
4744
|
+
}
|
|
4745
|
+
let nextSnapshot = snapshotWith(keepCurrentFeedState ? current : next, {
|
|
4746
|
+
transients: mergedTransients,
|
|
4747
|
+
revision: Math.max(previousRevision, next.revision),
|
|
4748
|
+
documentRevision: Math.max(
|
|
4749
|
+
previousDocumentRevision,
|
|
4750
|
+
next.documentRevision
|
|
4751
|
+
)
|
|
4752
|
+
});
|
|
4753
|
+
const appended = next.lastSequence > this.deliveredThrough ? this.drainContiguous(next.lastSequence) : [];
|
|
4754
|
+
const needsRepair = this.deliveredThrough < next.lastSequence;
|
|
4755
|
+
const incomingAdvanced = next.documentEpoch > current.documentEpoch || next.documentRevision > current.documentRevision || next.revision > current.revision || next.lastSequence > current.lastSequence;
|
|
4756
|
+
if (incomingAdvanced || !needsRepair) {
|
|
4757
|
+
this.cancelScheduledRepairRetry();
|
|
4758
|
+
this.repairFailureAttempt = 0;
|
|
4759
|
+
}
|
|
4760
|
+
nextSnapshot = snapshotWith(nextSnapshot, {
|
|
4761
|
+
isRepairing: needsRepair,
|
|
4762
|
+
error: nextSnapshot.error
|
|
4763
|
+
});
|
|
4764
|
+
this.snapshot = nextSnapshot;
|
|
4765
|
+
if (appended.length > 0) {
|
|
4766
|
+
this.emit({ type: "append", items: appended });
|
|
4767
|
+
}
|
|
4768
|
+
if (!sameTransientSet(previousTransients, mergedTransients) && next.revision > previousRevision) {
|
|
4769
|
+
this.emit({
|
|
4770
|
+
type: "transients",
|
|
4771
|
+
items: mergedTransients,
|
|
4772
|
+
revision: this.snapshot.revision
|
|
4773
|
+
});
|
|
4774
|
+
}
|
|
4775
|
+
if (next.documentRevision > previousDocumentRevision) {
|
|
4776
|
+
this.emit({
|
|
4777
|
+
type: "resource_refresh",
|
|
4778
|
+
documentRevision: next.documentRevision
|
|
4779
|
+
});
|
|
4780
|
+
}
|
|
4781
|
+
if (needsRepair) {
|
|
4782
|
+
this.startRepair(next.lastSequence);
|
|
4783
|
+
}
|
|
4784
|
+
}
|
|
4785
|
+
/**
|
|
4786
|
+
* Quarantine a canonical replacement rejected by the document transport.
|
|
4787
|
+
* The notice deliberately contains no replacement document, so accepted
|
|
4788
|
+
* feed history remains the only data visible to subscribers during repair.
|
|
4789
|
+
*
|
|
4790
|
+
* @internal
|
|
4791
|
+
*/
|
|
4792
|
+
quarantineCanonicalReplacement(quarantine) {
|
|
4793
|
+
if (this.disposed) return;
|
|
4794
|
+
const incoming = snapshotWith(emptyFeedSnapshot(true), {
|
|
4795
|
+
documentEpoch: quarantine.documentEpoch,
|
|
4796
|
+
documentRevision: quarantine.documentRevision,
|
|
4797
|
+
isRepairing: true,
|
|
4798
|
+
error: quarantine.error
|
|
4799
|
+
});
|
|
4800
|
+
if (!this.canonical) {
|
|
4801
|
+
this.canonical = true;
|
|
4802
|
+
this.canonicalStructureValid = false;
|
|
4803
|
+
this.repairGeneration += 1;
|
|
4804
|
+
this.repairPromise = null;
|
|
4805
|
+
this.cancelScheduledRepairRetry();
|
|
4806
|
+
this.repairFailureAttempt = 0;
|
|
4807
|
+
this.deliveredThrough = 0;
|
|
4808
|
+
this.knownBySequence.clear();
|
|
4809
|
+
this.sequenceById.clear();
|
|
4810
|
+
this.observedUnknownPositions.clear();
|
|
4811
|
+
this.snapshot = incoming;
|
|
4812
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4813
|
+
return;
|
|
4814
|
+
}
|
|
4815
|
+
this.rejectSnapshotRegression("invalid_canonical_document", incoming);
|
|
4816
|
+
this.repairGeneration += 1;
|
|
4817
|
+
this.repairPromise = null;
|
|
4818
|
+
this.cancelScheduledRepairRetry();
|
|
4819
|
+
this.repairFailureAttempt = 0;
|
|
4820
|
+
if (!this.canonicalStructureValid) {
|
|
4821
|
+
const current = this.snapshot;
|
|
4822
|
+
const incomingIsNewer = incoming.documentEpoch > current.documentEpoch || incoming.documentEpoch === current.documentEpoch && incoming.documentRevision >= current.documentRevision;
|
|
4823
|
+
if (incomingIsNewer) {
|
|
4824
|
+
this.snapshot = incoming;
|
|
4825
|
+
} else {
|
|
4826
|
+
this.snapshot = snapshotWith(current, {
|
|
4827
|
+
isRepairing: true,
|
|
4828
|
+
error: quarantine.error
|
|
4829
|
+
});
|
|
4830
|
+
}
|
|
4831
|
+
} else {
|
|
4832
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
4833
|
+
isRepairing: true,
|
|
4834
|
+
error: quarantine.error
|
|
4835
|
+
});
|
|
4836
|
+
}
|
|
4837
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4838
|
+
}
|
|
4839
|
+
acceptReset(snapshot) {
|
|
4840
|
+
this.cancelScheduledRepairRetry();
|
|
4841
|
+
this.repairFailureAttempt = 0;
|
|
4842
|
+
this.repairGeneration += 1;
|
|
4843
|
+
this.repairPromise = null;
|
|
4844
|
+
this.canonical = true;
|
|
4845
|
+
this.canonicalStructureValid = true;
|
|
4846
|
+
this.knownBySequence.clear();
|
|
4847
|
+
this.sequenceById.clear();
|
|
4848
|
+
this.observedUnknownPositions.clear();
|
|
4849
|
+
this.deliveredThrough = contiguousLocalTailWatermark(snapshot);
|
|
4850
|
+
const identityError = this.remember(snapshot.tail);
|
|
4851
|
+
const needsRepair = this.deliveredThrough < snapshot.lastSequence;
|
|
4852
|
+
this.snapshot = snapshotWith(snapshot, {
|
|
4853
|
+
isRepairing: needsRepair && Boolean(this.listTransport),
|
|
4854
|
+
error: identityError || snapshot.error
|
|
4855
|
+
});
|
|
4856
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4857
|
+
if (needsRepair) {
|
|
4858
|
+
this.startRepair(snapshot.lastSequence);
|
|
4859
|
+
}
|
|
4860
|
+
}
|
|
4861
|
+
emitDiagnostic(diagnostic) {
|
|
4862
|
+
emitFeedDiagnostic(diagnostic, this.diagnosticListener);
|
|
4863
|
+
}
|
|
4864
|
+
rejectSnapshotRegression(reason, incoming) {
|
|
4865
|
+
const current = this.snapshot;
|
|
4866
|
+
this.emitDiagnostic({
|
|
4867
|
+
type: "snapshot_regression_rejected",
|
|
4868
|
+
reason,
|
|
4869
|
+
currentDocumentEpoch: current.documentEpoch,
|
|
4870
|
+
incomingDocumentEpoch: incoming.documentEpoch,
|
|
4871
|
+
currentDocumentRevision: current.documentRevision,
|
|
4872
|
+
incomingDocumentRevision: incoming.documentRevision,
|
|
4873
|
+
currentFeedRevision: current.revision,
|
|
4874
|
+
incomingFeedRevision: incoming.revision,
|
|
4875
|
+
currentLastSequence: current.lastSequence,
|
|
4876
|
+
incomingLastSequence: incoming.lastSequence
|
|
4877
|
+
});
|
|
4878
|
+
}
|
|
4879
|
+
observeUnknownKinds(items) {
|
|
4880
|
+
for (const item of items) {
|
|
4881
|
+
const runtimeKind = String(item.kind);
|
|
4882
|
+
if (isKnownDurableFeedKind(runtimeKind)) continue;
|
|
4883
|
+
const diagnosticKind = normalizeFeedDiagnosticKind(runtimeKind);
|
|
4884
|
+
const position = `${item.sequence}:${diagnosticKind}`;
|
|
4885
|
+
if (this.observedUnknownPositions.has(position)) continue;
|
|
4886
|
+
this.observedUnknownPositions.add(position);
|
|
4887
|
+
this.emitDiagnostic({
|
|
4888
|
+
type: "unknown_kind",
|
|
4889
|
+
kind: diagnosticKind,
|
|
4890
|
+
sequence: item.sequence,
|
|
4891
|
+
consumer: "sdk"
|
|
4892
|
+
});
|
|
4893
|
+
}
|
|
4894
|
+
}
|
|
4895
|
+
selectNewerTransients(current, next) {
|
|
4896
|
+
if (next.revision <= current.revision) {
|
|
4897
|
+
return { items: current.transients, error: null };
|
|
4898
|
+
}
|
|
4899
|
+
let newestById;
|
|
4900
|
+
try {
|
|
4901
|
+
newestById = new Map(
|
|
4902
|
+
orderTransientFeedItems([
|
|
4903
|
+
...current.transients,
|
|
4904
|
+
...next.transients
|
|
4905
|
+
]).map((item) => [item.id, item])
|
|
4906
|
+
);
|
|
4907
|
+
} catch {
|
|
4908
|
+
return {
|
|
4909
|
+
items: current.transients,
|
|
4910
|
+
error: new Error(
|
|
4911
|
+
"Transient feed changed immutable identity or revision."
|
|
4912
|
+
)
|
|
4913
|
+
};
|
|
4914
|
+
}
|
|
4915
|
+
const currentById = new Map(
|
|
4916
|
+
current.transients.map((item) => [item.id, item])
|
|
4917
|
+
);
|
|
4918
|
+
const selected = [];
|
|
4919
|
+
for (const incoming of next.transients) {
|
|
4920
|
+
const previous = currentById.get(incoming.id);
|
|
4921
|
+
if (previous && previous.kind !== incoming.kind) {
|
|
4922
|
+
return {
|
|
4923
|
+
items: current.transients,
|
|
4924
|
+
error: new Error(
|
|
4925
|
+
`Transient feed item ${incoming.id} changed its immutable kind.`
|
|
4926
|
+
)
|
|
4927
|
+
};
|
|
4928
|
+
}
|
|
4929
|
+
if (previous?.kind === "message" && incoming.kind === "message" && incoming.revision > previous.revision && incoming.producerRevision < previous.producerRevision) {
|
|
4930
|
+
selected.push(previous);
|
|
4931
|
+
continue;
|
|
4932
|
+
}
|
|
4933
|
+
selected.push(newestById.get(incoming.id) || incoming);
|
|
4934
|
+
}
|
|
4935
|
+
return {
|
|
4936
|
+
items: orderTransientFeedItems(selected),
|
|
4937
|
+
error: null
|
|
4938
|
+
};
|
|
4939
|
+
}
|
|
4940
|
+
remember(items) {
|
|
4941
|
+
const stagedBySequence = /* @__PURE__ */ new Map();
|
|
4942
|
+
const stagedSequenceById = /* @__PURE__ */ new Map();
|
|
4943
|
+
for (const item of items) {
|
|
4944
|
+
const knownSequence = stagedSequenceById.get(item.id) ?? this.sequenceById.get(item.id);
|
|
4945
|
+
const knownItem = stagedBySequence.get(item.sequence) || this.knownBySequence.get(item.sequence);
|
|
4946
|
+
if (knownSequence !== void 0 && knownSequence !== item.sequence || knownItem && knownItem.id !== item.id) {
|
|
4947
|
+
return new Error(
|
|
4948
|
+
`Feed identity changed at sequence ${item.sequence}; a reset is required.`
|
|
4949
|
+
);
|
|
4950
|
+
}
|
|
4951
|
+
if (knownItem && !sameDurableItem(knownItem, item)) {
|
|
4952
|
+
return new Error(
|
|
4953
|
+
`Immutable feed item ${item.id} changed at sequence ${item.sequence}.`
|
|
4954
|
+
);
|
|
4955
|
+
}
|
|
4956
|
+
stagedSequenceById.set(item.id, item.sequence);
|
|
4957
|
+
stagedBySequence.set(item.sequence, item);
|
|
4958
|
+
}
|
|
4959
|
+
for (const item of stagedBySequence.values()) {
|
|
4960
|
+
this.sequenceById.set(item.id, item.sequence);
|
|
4961
|
+
this.knownBySequence.set(item.sequence, item);
|
|
4962
|
+
}
|
|
4963
|
+
this.observeUnknownKinds([...stagedBySequence.values()]);
|
|
4964
|
+
return null;
|
|
4965
|
+
}
|
|
4966
|
+
findIdentityConflict(items) {
|
|
4967
|
+
for (const item of items) {
|
|
4968
|
+
const knownSequence = this.sequenceById.get(item.id);
|
|
4969
|
+
const knownItem = this.knownBySequence.get(item.sequence);
|
|
4970
|
+
if (knownSequence !== void 0 && knownSequence !== item.sequence || knownItem && knownItem.id !== item.id) {
|
|
4971
|
+
return new Error(
|
|
4972
|
+
`Feed identity changed at sequence ${item.sequence}; the checkpoint was rejected.`
|
|
4973
|
+
);
|
|
4974
|
+
}
|
|
4975
|
+
if (knownItem && !sameDurableItem(knownItem, item)) {
|
|
4976
|
+
return new Error(
|
|
4977
|
+
`Immutable feed item ${item.id} changed at sequence ${item.sequence}.`
|
|
4978
|
+
);
|
|
4979
|
+
}
|
|
4980
|
+
}
|
|
4981
|
+
return null;
|
|
4982
|
+
}
|
|
4983
|
+
drainContiguous(targetSequence) {
|
|
4984
|
+
const appended = [];
|
|
4985
|
+
let sequence = this.deliveredThrough + 1;
|
|
4986
|
+
while (sequence <= targetSequence) {
|
|
4987
|
+
const item = this.knownBySequence.get(sequence);
|
|
4988
|
+
if (!item) break;
|
|
4989
|
+
appended.push(item);
|
|
4990
|
+
this.deliveredThrough = sequence;
|
|
4991
|
+
sequence += 1;
|
|
4992
|
+
}
|
|
4993
|
+
return Object.freeze(appended);
|
|
4994
|
+
}
|
|
4995
|
+
startRepair(targetSequence) {
|
|
4996
|
+
if (this.disposed || this.repairPromise || this.repairRetryHandle !== null) {
|
|
4997
|
+
return;
|
|
4998
|
+
}
|
|
4999
|
+
if (!this.listTransport) {
|
|
5000
|
+
this.emitDiagnostic({
|
|
5001
|
+
type: "gap_detected",
|
|
5002
|
+
expectedSequence: this.deliveredThrough + 1,
|
|
5003
|
+
targetSequence,
|
|
5004
|
+
repairAvailable: false
|
|
5005
|
+
});
|
|
5006
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
5007
|
+
isRepairing: false,
|
|
5008
|
+
error: this.snapshot.error || new Error(
|
|
5009
|
+
"Feed sequence gap cannot be repaired without history transport."
|
|
5010
|
+
)
|
|
5011
|
+
});
|
|
5012
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
5013
|
+
return;
|
|
5014
|
+
}
|
|
5015
|
+
this.emitDiagnostic({
|
|
5016
|
+
type: "gap_detected",
|
|
5017
|
+
expectedSequence: this.deliveredThrough + 1,
|
|
5018
|
+
targetSequence,
|
|
5019
|
+
repairAvailable: true
|
|
5020
|
+
});
|
|
5021
|
+
const generation = ++this.repairGeneration;
|
|
5022
|
+
this.repairPromise = this.repair(targetSequence, generation).finally(() => {
|
|
5023
|
+
if (generation === this.repairGeneration) {
|
|
5024
|
+
this.repairPromise = null;
|
|
5025
|
+
if (this.deliveredThrough < this.snapshot.lastSequence) {
|
|
5026
|
+
if (this.snapshot.error) {
|
|
5027
|
+
this.scheduleQuietRepairRetry();
|
|
5028
|
+
} else {
|
|
5029
|
+
this.snapshot = snapshotWith(this.snapshot, { isRepairing: true });
|
|
5030
|
+
this.startRepair(this.snapshot.lastSequence);
|
|
5031
|
+
}
|
|
5032
|
+
} else {
|
|
5033
|
+
this.repairFailureAttempt = 0;
|
|
5034
|
+
}
|
|
5035
|
+
}
|
|
5036
|
+
});
|
|
5037
|
+
}
|
|
5038
|
+
cancelScheduledRepairRetry() {
|
|
5039
|
+
if (this.repairRetryHandle === null) return;
|
|
5040
|
+
const handle = this.repairRetryHandle;
|
|
5041
|
+
this.repairRetryHandle = null;
|
|
5042
|
+
this.repairRetryToken += 1;
|
|
5043
|
+
this.cancelRepairRetryCallback(handle);
|
|
5044
|
+
}
|
|
5045
|
+
scheduleQuietRepairRetry() {
|
|
5046
|
+
if (this.disposed || this.repairRetryHandle !== null || this.repairPromise || !this.listTransport || this.deliveredThrough >= this.snapshot.lastSequence) {
|
|
5047
|
+
return;
|
|
5048
|
+
}
|
|
5049
|
+
this.repairFailureAttempt += 1;
|
|
5050
|
+
const delayMs = Math.min(
|
|
5051
|
+
this.maxRepairRetryDelayMs,
|
|
5052
|
+
this.initialRepairRetryDelayMs * 2 ** Math.min(30, Math.max(0, this.repairFailureAttempt - 1))
|
|
5053
|
+
);
|
|
5054
|
+
const retryToken = ++this.repairRetryToken;
|
|
5055
|
+
let handle;
|
|
5056
|
+
handle = this.scheduleRepairRetryCallback(() => {
|
|
5057
|
+
if (retryToken !== this.repairRetryToken || this.repairRetryHandle !== handle) {
|
|
5058
|
+
return;
|
|
5059
|
+
}
|
|
5060
|
+
this.repairRetryHandle = null;
|
|
5061
|
+
if (this.disposed) return;
|
|
5062
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
5063
|
+
isRepairing: true,
|
|
5064
|
+
error: null
|
|
5065
|
+
});
|
|
5066
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
5067
|
+
this.startRepair(this.snapshot.lastSequence);
|
|
5068
|
+
}, delayMs);
|
|
5069
|
+
this.repairRetryHandle = handle;
|
|
5070
|
+
}
|
|
5071
|
+
async repair(targetSequence, generation) {
|
|
5072
|
+
const startedAt = this.diagnosticNow();
|
|
5073
|
+
let pageCount = 0;
|
|
5074
|
+
let outcome = "success";
|
|
5075
|
+
try {
|
|
5076
|
+
let attempts = 0;
|
|
5077
|
+
while (generation === this.repairGeneration && this.deliveredThrough < targetSequence) {
|
|
5078
|
+
if (++attempts > 100) {
|
|
5079
|
+
throw new Error("Feed gap repair exceeded its page limit.");
|
|
5080
|
+
}
|
|
5081
|
+
const page = await this.list({
|
|
5082
|
+
afterSequence: this.deliveredThrough,
|
|
5083
|
+
limit: 500
|
|
5084
|
+
});
|
|
5085
|
+
pageCount += 1;
|
|
5086
|
+
if (generation !== this.repairGeneration) {
|
|
5087
|
+
outcome = "cancelled";
|
|
5088
|
+
return;
|
|
5089
|
+
}
|
|
5090
|
+
if (page.items.length === 0) {
|
|
5091
|
+
throw new Error(
|
|
5092
|
+
`Feed gap repair returned no item after sequence ${this.deliveredThrough}.`
|
|
5093
|
+
);
|
|
5094
|
+
}
|
|
5095
|
+
const deliveredBeforePage = this.deliveredThrough;
|
|
5096
|
+
const identityError = this.remember(page.items);
|
|
5097
|
+
if (identityError) throw identityError;
|
|
5098
|
+
const appended = this.drainContiguous(targetSequence);
|
|
5099
|
+
const reachedTarget = this.deliveredThrough >= targetSequence;
|
|
5100
|
+
if (reachedTarget) {
|
|
5101
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
5102
|
+
isRepairing: this.deliveredThrough < this.snapshot.lastSequence,
|
|
5103
|
+
error: null
|
|
5104
|
+
});
|
|
5105
|
+
}
|
|
5106
|
+
if (appended.length > 0) {
|
|
5107
|
+
this.emit({ type: "append", items: appended });
|
|
5108
|
+
}
|
|
5109
|
+
if (this.deliveredThrough === deliveredBeforePage) {
|
|
5110
|
+
throw new Error(
|
|
5111
|
+
`Feed gap repair did not return expected sequence ${deliveredBeforePage + 1}.`
|
|
5112
|
+
);
|
|
5113
|
+
}
|
|
5114
|
+
if (this.deliveredThrough < targetSequence && !page.hasMoreAfter && (page.lastSequence || 0) < targetSequence) {
|
|
5115
|
+
throw new Error(
|
|
5116
|
+
`Feed gap remains after sequence ${this.deliveredThrough}.`
|
|
5117
|
+
);
|
|
5118
|
+
}
|
|
5119
|
+
}
|
|
5120
|
+
if (generation !== this.repairGeneration) {
|
|
5121
|
+
outcome = "cancelled";
|
|
5122
|
+
return;
|
|
5123
|
+
}
|
|
5124
|
+
} catch (error) {
|
|
5125
|
+
if (generation !== this.repairGeneration) {
|
|
5126
|
+
outcome = "cancelled";
|
|
5127
|
+
return;
|
|
5128
|
+
}
|
|
5129
|
+
outcome = "failure";
|
|
5130
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
5131
|
+
isRepairing: false,
|
|
5132
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
5133
|
+
});
|
|
5134
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
5135
|
+
} finally {
|
|
5136
|
+
this.emitDiagnostic({
|
|
5137
|
+
type: "gap_repair",
|
|
5138
|
+
outcome,
|
|
5139
|
+
durationMs: Math.max(0, this.diagnosticNow() - startedAt),
|
|
5140
|
+
pageCount,
|
|
5141
|
+
repairedThroughSequence: this.deliveredThrough,
|
|
5142
|
+
targetSequence
|
|
5143
|
+
});
|
|
5144
|
+
}
|
|
5145
|
+
}
|
|
5146
|
+
emit(change) {
|
|
5147
|
+
for (const subscriber of this.subscribers) {
|
|
5148
|
+
try {
|
|
5149
|
+
subscriber(change);
|
|
5150
|
+
} catch (error) {
|
|
5151
|
+
console.error("[Granular] Session feed subscriber failed", error);
|
|
5152
|
+
}
|
|
5153
|
+
}
|
|
5154
|
+
}
|
|
5155
|
+
};
|
|
5156
|
+
function sameTransientSet(left, right) {
|
|
5157
|
+
return stableValueFingerprint(left) === stableValueFingerprint(right);
|
|
5158
|
+
}
|
|
5159
|
+
function operationId(prefix) {
|
|
5160
|
+
const randomUuid = globalThis.crypto?.randomUUID?.();
|
|
5161
|
+
return randomUuid ? `${prefix}_${randomUuid}` : `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
5162
|
+
}
|
|
5163
|
+
function unwrapPublishedItem(value) {
|
|
5164
|
+
const record = asRecord(value);
|
|
5165
|
+
const directItems = Array.isArray(record?.items) ? record.items : [];
|
|
5166
|
+
const durable = asRecord(record?.durable);
|
|
5167
|
+
const durableItems = Array.isArray(durable?.items) ? durable.items : [];
|
|
5168
|
+
return record?.item || directItems[0] || durableItems[0] || value;
|
|
5169
|
+
}
|
|
5170
|
+
function requireFeedbackItem(value) {
|
|
5171
|
+
const item = normalizeFeedItem(unwrapPublishedItem(value));
|
|
5172
|
+
if (!item || item.kind !== "feedback") {
|
|
5173
|
+
throw new Error("Feed publisher returned an invalid feedback item.");
|
|
5174
|
+
}
|
|
5175
|
+
return item;
|
|
5176
|
+
}
|
|
5177
|
+
function requireTransientFeedbackItem(value) {
|
|
5178
|
+
const item = normalizeTransientFeedItem(unwrapPublishedItem(value));
|
|
5179
|
+
if (!item || item.kind !== "feedback") {
|
|
5180
|
+
throw new Error(
|
|
5181
|
+
"Feed publisher returned an invalid transient feedback item."
|
|
5182
|
+
);
|
|
5183
|
+
}
|
|
5184
|
+
return item;
|
|
5185
|
+
}
|
|
5186
|
+
function createFeedPublisher(publish) {
|
|
5187
|
+
const makeHandle = (initial) => {
|
|
5188
|
+
let current = initial;
|
|
5189
|
+
let mutationQueue = Promise.resolve();
|
|
5190
|
+
const enqueueMutation = (mutation) => {
|
|
5191
|
+
const result = mutationQueue.then(mutation);
|
|
5192
|
+
mutationQueue = result.then(
|
|
5193
|
+
() => void 0,
|
|
5194
|
+
() => void 0
|
|
5195
|
+
);
|
|
5196
|
+
return result;
|
|
5197
|
+
};
|
|
5198
|
+
const handle = {
|
|
5199
|
+
get id() {
|
|
5200
|
+
return current.id;
|
|
5201
|
+
},
|
|
5202
|
+
get ordinal() {
|
|
5203
|
+
return current.ordinal;
|
|
5204
|
+
},
|
|
5205
|
+
get revision() {
|
|
5206
|
+
return current.revision;
|
|
5207
|
+
},
|
|
5208
|
+
update(text, options = {}) {
|
|
5209
|
+
const reservedOperationId = options.operationId || operationId("feed_transient_update");
|
|
5210
|
+
const reservedOptions = { ...options };
|
|
5211
|
+
return enqueueMutation(async () => {
|
|
5212
|
+
const response = await publish("feed.transient.update", {
|
|
5213
|
+
transientId: current.id,
|
|
5214
|
+
expectedRevision: current.revision,
|
|
5215
|
+
text,
|
|
5216
|
+
...reservedOptions,
|
|
5217
|
+
operationId: reservedOperationId
|
|
5218
|
+
});
|
|
5219
|
+
current = requireTransientFeedbackItem(response);
|
|
5220
|
+
return handle;
|
|
5221
|
+
});
|
|
5222
|
+
},
|
|
5223
|
+
settle(text, options = {}) {
|
|
5224
|
+
const reservedOperationId = options.operationId || operationId("feed_transient_settle");
|
|
5225
|
+
const reservedOptions = { ...options };
|
|
5226
|
+
return enqueueMutation(async () => {
|
|
5227
|
+
const response = await publish("feed.transient.settle", {
|
|
5228
|
+
transientId: current.id,
|
|
5229
|
+
expectedRevision: current.revision,
|
|
5230
|
+
...text === void 0 ? {} : { text },
|
|
5231
|
+
...reservedOptions,
|
|
5232
|
+
operationId: reservedOperationId
|
|
5233
|
+
});
|
|
5234
|
+
const responseRecord = asRecord(response);
|
|
5235
|
+
const durable = asRecord(responseRecord?.durable);
|
|
5236
|
+
const durableItems = Array.isArray(durable?.items) ? durable.items : [];
|
|
5237
|
+
const rawItem = responseRecord?.item ?? responseRecord?.durableItem ?? durableItems[0];
|
|
5238
|
+
if (rawItem === null || rawItem === void 0) {
|
|
5239
|
+
return null;
|
|
5240
|
+
}
|
|
5241
|
+
return requireFeedbackItem(rawItem);
|
|
5242
|
+
});
|
|
5243
|
+
}
|
|
5244
|
+
};
|
|
5245
|
+
return handle;
|
|
5246
|
+
};
|
|
5247
|
+
return {
|
|
5248
|
+
async feedback(text, options = {}) {
|
|
5249
|
+
return requireFeedbackItem(
|
|
5250
|
+
await publish("feed.feedback", {
|
|
5251
|
+
text,
|
|
5252
|
+
...options,
|
|
5253
|
+
operationId: options.operationId || operationId("feed_feedback")
|
|
5254
|
+
})
|
|
5255
|
+
);
|
|
5256
|
+
},
|
|
5257
|
+
async transientFeedback(text, options = {}) {
|
|
5258
|
+
const item = requireTransientFeedbackItem(
|
|
5259
|
+
await publish("feed.transient.create", {
|
|
5260
|
+
text,
|
|
5261
|
+
...options,
|
|
5262
|
+
operationId: options.operationId || operationId("feed_transient_create")
|
|
5263
|
+
})
|
|
5264
|
+
);
|
|
5265
|
+
return makeHandle(item);
|
|
5266
|
+
}
|
|
5267
|
+
};
|
|
5268
|
+
}
|
|
5269
|
+
|
|
5270
|
+
// src/ws-client.ts
|
|
3992
5271
|
var GlobalWebSocket = void 0;
|
|
3993
5272
|
if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
|
|
3994
5273
|
GlobalWebSocket = globalThis.WebSocket;
|
|
@@ -4002,8 +5281,42 @@ var DEFAULT_RPC_TIMEOUT_MS = 3e4;
|
|
|
4002
5281
|
var DOMAIN_PACKAGE_RPC_TIMEOUT_MS = 12e4;
|
|
4003
5282
|
var EFFECT_CONTROL_RPC_TIMEOUT_MS = 12e4;
|
|
4004
5283
|
var HARNESS_RUN_RPC_TIMEOUT_MS = 6e5;
|
|
5284
|
+
var HEARTBEAT_RPC_TIMEOUT_MS = 15e3;
|
|
5285
|
+
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
4005
5286
|
var DEFAULT_RECONNECT_DELAY_MS = 3e3;
|
|
4006
5287
|
var DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
|
|
5288
|
+
var DOCUMENT_RESYNC_CLOSE_CODE = 4e3;
|
|
5289
|
+
var DOCUMENT_RESYNC_CLOSE_REASON = "Document resync required";
|
|
5290
|
+
function documentVersion(document) {
|
|
5291
|
+
const record = document;
|
|
5292
|
+
const epochValue = record.documentEpoch;
|
|
5293
|
+
const revisionValue = record.documentRevision;
|
|
5294
|
+
const epoch = epochValue === void 0 ? 0 : epochValue;
|
|
5295
|
+
const revision = revisionValue === void 0 ? 0 : revisionValue;
|
|
5296
|
+
if (!Number.isSafeInteger(epoch) || Number(epoch) < 0 || !Number.isSafeInteger(revision) || Number(revision) < 0) {
|
|
5297
|
+
throw new Error("Session document version metadata is invalid");
|
|
5298
|
+
}
|
|
5299
|
+
return {
|
|
5300
|
+
epoch: Number(epoch),
|
|
5301
|
+
revision: Number(revision)
|
|
5302
|
+
};
|
|
5303
|
+
}
|
|
5304
|
+
function stableDocumentFingerprint(value) {
|
|
5305
|
+
if (value === null) return "null";
|
|
5306
|
+
if (value === void 0) return '"[undefined]"';
|
|
5307
|
+
if (typeof value !== "object") return JSON.stringify(value) ?? String(value);
|
|
5308
|
+
if (value instanceof Date) return `date:${value.toISOString()}`;
|
|
5309
|
+
if (value instanceof Uint8Array) {
|
|
5310
|
+
return `bytes:${Array.from(value).join(",")}`;
|
|
5311
|
+
}
|
|
5312
|
+
if (Array.isArray(value)) {
|
|
5313
|
+
return `[${value.map(stableDocumentFingerprint).join(",")}]`;
|
|
5314
|
+
}
|
|
5315
|
+
const record = value;
|
|
5316
|
+
return `{${Object.keys(record).sort().map(
|
|
5317
|
+
(key) => `${JSON.stringify(key)}:${stableDocumentFingerprint(record[key])}`
|
|
5318
|
+
).join(",")}}`;
|
|
5319
|
+
}
|
|
4007
5320
|
function debugWs(...args) {
|
|
4008
5321
|
if (DEBUG_WS) {
|
|
4009
5322
|
console.log(...args);
|
|
@@ -4015,6 +5328,7 @@ function rpcTimeoutMsForMethod(method) {
|
|
|
4015
5328
|
case "domain.getSummary":
|
|
4016
5329
|
return DOMAIN_PACKAGE_RPC_TIMEOUT_MS;
|
|
4017
5330
|
case "client.heartbeat":
|
|
5331
|
+
return HEARTBEAT_RPC_TIMEOUT_MS;
|
|
4018
5332
|
case "effects.publishCatalog":
|
|
4019
5333
|
case "effects.resetCatalog":
|
|
4020
5334
|
case "effects.addCatalog":
|
|
@@ -4034,12 +5348,11 @@ var WSClient = class {
|
|
|
4034
5348
|
sessionId;
|
|
4035
5349
|
token;
|
|
4036
5350
|
messageQueue = [];
|
|
4037
|
-
syncHandlers = [];
|
|
4038
5351
|
rpcHandlers = /* @__PURE__ */ new Map();
|
|
4039
5352
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
5353
|
+
canonicalDocumentQuarantineHandlers = /* @__PURE__ */ new Set();
|
|
4040
5354
|
nextRpcId = 1;
|
|
4041
5355
|
doc = Automerge.init();
|
|
4042
|
-
syncState = Automerge.initSyncState();
|
|
4043
5356
|
reconnectTimer = null;
|
|
4044
5357
|
tokenRefreshTimer = null;
|
|
4045
5358
|
isExplicitlyDisconnected = false;
|
|
@@ -4047,6 +5360,8 @@ var WSClient = class {
|
|
|
4047
5360
|
connectPromise = null;
|
|
4048
5361
|
connectionEpoch = 0;
|
|
4049
5362
|
cancelConnectAttempt = null;
|
|
5363
|
+
documentRepairRequired = false;
|
|
5364
|
+
canonicalDocumentActivated = false;
|
|
4050
5365
|
options;
|
|
4051
5366
|
constructor(options) {
|
|
4052
5367
|
this.options = options;
|
|
@@ -4065,8 +5380,12 @@ var WSClient = class {
|
|
|
4065
5380
|
return;
|
|
4066
5381
|
}
|
|
4067
5382
|
try {
|
|
4068
|
-
|
|
4069
|
-
this.
|
|
5383
|
+
const replacement = document instanceof Uint8Array ? Automerge.load(document) : Automerge.from(document);
|
|
5384
|
+
if (!this.canAcceptDocumentReplacement(replacement, false)) {
|
|
5385
|
+
return;
|
|
5386
|
+
}
|
|
5387
|
+
this.doc = replacement;
|
|
5388
|
+
this.rememberCanonicalActivation(replacement);
|
|
4070
5389
|
this.emit("sync", this.doc);
|
|
4071
5390
|
} catch (error) {
|
|
4072
5391
|
console.warn("[Granular] Failed to seed cached session document", error);
|
|
@@ -4197,6 +5516,26 @@ var WSClient = class {
|
|
|
4197
5516
|
}
|
|
4198
5517
|
}
|
|
4199
5518
|
}
|
|
5519
|
+
/**
|
|
5520
|
+
* Mark the current transport as unusable and schedule the normal reconnect
|
|
5521
|
+
* path. Browser WebSockets can remain in OPEN state after a proxy/worker
|
|
5522
|
+
* restart, so a timed-out heartbeat must revoke the stale socket explicitly.
|
|
5523
|
+
*/
|
|
5524
|
+
reportTransportFailure(reason = "WebSocket transport failed") {
|
|
5525
|
+
if (this.isExplicitlyDisconnected) return;
|
|
5526
|
+
const socket = this.ws;
|
|
5527
|
+
this.connectionEpoch += 1;
|
|
5528
|
+
this.ws = null;
|
|
5529
|
+
try {
|
|
5530
|
+
socket?.close(4001, "Transport failure");
|
|
5531
|
+
} catch {
|
|
5532
|
+
}
|
|
5533
|
+
this.handleDisconnect({
|
|
5534
|
+
code: 4001,
|
|
5535
|
+
reason: reason instanceof Error ? reason.message : String(reason),
|
|
5536
|
+
wasClean: false
|
|
5537
|
+
});
|
|
5538
|
+
}
|
|
4200
5539
|
async connectAttempt(signal) {
|
|
4201
5540
|
if (signal?.aborted) throw new Error("WebSocket connect aborted");
|
|
4202
5541
|
const token = await this.resolveTokenForConnect();
|
|
@@ -4228,10 +5567,17 @@ var WSClient = class {
|
|
|
4228
5567
|
this.ws = socket;
|
|
4229
5568
|
return new Promise((resolve, reject) => {
|
|
4230
5569
|
let settled = false;
|
|
5570
|
+
const configuredConnectTimeoutMs = this.options.connectTimeoutMs;
|
|
5571
|
+
const connectTimeoutMs = typeof configuredConnectTimeoutMs === "number" && Number.isFinite(configuredConnectTimeoutMs) && configuredConnectTimeoutMs > 0 ? configuredConnectTimeoutMs : DEFAULT_CONNECT_TIMEOUT_MS;
|
|
5572
|
+
let connectTimeout = null;
|
|
4231
5573
|
const isCurrent = () => this.connectionEpoch === epoch && this.ws === socket;
|
|
4232
5574
|
const finish = (error) => {
|
|
4233
5575
|
if (settled) return;
|
|
4234
5576
|
settled = true;
|
|
5577
|
+
if (connectTimeout) {
|
|
5578
|
+
clearTimeout(connectTimeout);
|
|
5579
|
+
connectTimeout = null;
|
|
5580
|
+
}
|
|
4235
5581
|
if (this.cancelConnectAttempt === handleAbort) {
|
|
4236
5582
|
this.cancelConnectAttempt = null;
|
|
4237
5583
|
}
|
|
@@ -4296,6 +5642,17 @@ var WSClient = class {
|
|
|
4296
5642
|
wasClean: close.wasClean
|
|
4297
5643
|
});
|
|
4298
5644
|
};
|
|
5645
|
+
connectTimeout = setTimeout(() => {
|
|
5646
|
+
if (!isCurrent()) return;
|
|
5647
|
+
this.connectionEpoch += 1;
|
|
5648
|
+
this.ws = null;
|
|
5649
|
+
closeStaleSocket();
|
|
5650
|
+
finish(
|
|
5651
|
+
new Error(
|
|
5652
|
+
`WebSocket connect timed out after ${connectTimeoutMs}ms`
|
|
5653
|
+
)
|
|
5654
|
+
);
|
|
5655
|
+
}, connectTimeoutMs);
|
|
4299
5656
|
signal?.addEventListener("abort", handleAbort, { once: true });
|
|
4300
5657
|
const nodeSocket = socket;
|
|
4301
5658
|
if (typeof nodeSocket.on === "function") {
|
|
@@ -4334,11 +5691,12 @@ var WSClient = class {
|
|
|
4334
5691
|
});
|
|
4335
5692
|
this.messageQueue = [];
|
|
4336
5693
|
}
|
|
4337
|
-
emitReconnectErrorMessage(error) {
|
|
5694
|
+
emitReconnectErrorMessage(error, terminal = false) {
|
|
4338
5695
|
const reconnectInfo = {
|
|
4339
5696
|
error,
|
|
4340
5697
|
sessionId: this.sessionId,
|
|
4341
|
-
timestamp: Date.now()
|
|
5698
|
+
timestamp: Date.now(),
|
|
5699
|
+
terminal
|
|
4342
5700
|
};
|
|
4343
5701
|
this.emit("reconnect_error", reconnectInfo);
|
|
4344
5702
|
if (this.options.onReconnectError) {
|
|
@@ -4358,7 +5716,8 @@ var WSClient = class {
|
|
|
4358
5716
|
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;
|
|
4359
5717
|
if (this.reconnectAttempts >= maxReconnectAttempts) {
|
|
4360
5718
|
this.emitReconnectErrorMessage(
|
|
4361
|
-
`WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s)
|
|
5719
|
+
`WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`,
|
|
5720
|
+
true
|
|
4362
5721
|
);
|
|
4363
5722
|
return null;
|
|
4364
5723
|
}
|
|
@@ -4388,6 +5747,190 @@ var WSClient = class {
|
|
|
4388
5747
|
const suffix = details ? ` (${details})` : "";
|
|
4389
5748
|
return new Error(`WebSocket disconnected${suffix}`);
|
|
4390
5749
|
}
|
|
5750
|
+
decodeDocumentBytes(payload, envelopeType) {
|
|
5751
|
+
if (typeof payload === "string") {
|
|
5752
|
+
const binaryString = atob(payload);
|
|
5753
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
5754
|
+
for (let index = 0; index < binaryString.length; index += 1) {
|
|
5755
|
+
bytes[index] = binaryString.charCodeAt(index);
|
|
5756
|
+
}
|
|
5757
|
+
return bytes;
|
|
5758
|
+
}
|
|
5759
|
+
if (payload instanceof Uint8Array) {
|
|
5760
|
+
return payload;
|
|
5761
|
+
}
|
|
5762
|
+
if (Array.isArray(payload) && payload.every(
|
|
5763
|
+
(value) => typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 255
|
|
5764
|
+
)) {
|
|
5765
|
+
return new Uint8Array(payload);
|
|
5766
|
+
}
|
|
5767
|
+
throw new Error(`${envelopeType} payload is not valid byte data`);
|
|
5768
|
+
}
|
|
5769
|
+
rememberCanonicalActivation(document) {
|
|
5770
|
+
if (isCanonicalSessionFeedDocument(document)) {
|
|
5771
|
+
this.canonicalDocumentActivated = true;
|
|
5772
|
+
}
|
|
5773
|
+
}
|
|
5774
|
+
canAcceptDocumentReplacement(replacement, strictlyNewer) {
|
|
5775
|
+
const replacementCanonical = isCanonicalSessionFeedDocument(replacement);
|
|
5776
|
+
const currentCanonical = isCanonicalSessionFeedDocument(this.doc);
|
|
5777
|
+
if (replacementCanonical) {
|
|
5778
|
+
this.canonicalDocumentActivated = true;
|
|
5779
|
+
}
|
|
5780
|
+
if (this.canonicalDocumentActivated && !replacementCanonical) {
|
|
5781
|
+
debugWs(
|
|
5782
|
+
"[Granular DEBUG] Rejected session document replacement that would deactivate the canonical feed."
|
|
5783
|
+
);
|
|
5784
|
+
return false;
|
|
5785
|
+
}
|
|
5786
|
+
const current = documentVersion(this.doc);
|
|
5787
|
+
const incoming = documentVersion(replacement);
|
|
5788
|
+
if (incoming.epoch < current.epoch) {
|
|
5789
|
+
debugWs(
|
|
5790
|
+
`[Granular DEBUG] Rejected stale session document epoch ${incoming.epoch}; current epoch is ${current.epoch}.`
|
|
5791
|
+
);
|
|
5792
|
+
return false;
|
|
5793
|
+
}
|
|
5794
|
+
if (incoming.epoch === current.epoch) {
|
|
5795
|
+
const minimumRevision = strictlyNewer ? current.revision + 1 : current.revision;
|
|
5796
|
+
if (incoming.revision < minimumRevision) {
|
|
5797
|
+
debugWs(
|
|
5798
|
+
`[Granular DEBUG] Rejected stale session document revision ${incoming.revision}; current revision is ${current.revision}.`
|
|
5799
|
+
);
|
|
5800
|
+
return false;
|
|
5801
|
+
}
|
|
5802
|
+
}
|
|
5803
|
+
if (replacementCanonical) {
|
|
5804
|
+
const incomingSnapshot = readSessionFeedSnapshot(replacement);
|
|
5805
|
+
if (incomingSnapshot.error) {
|
|
5806
|
+
this.documentRepairRequired = true;
|
|
5807
|
+
this.exposeCanonicalQuarantine(
|
|
5808
|
+
replacement,
|
|
5809
|
+
currentCanonical,
|
|
5810
|
+
incomingSnapshot.error
|
|
5811
|
+
);
|
|
5812
|
+
debugWs(
|
|
5813
|
+
`[Granular DEBUG] Rejected malformed canonical snapshot: ${incomingSnapshot.error.message}`
|
|
5814
|
+
);
|
|
5815
|
+
return false;
|
|
5816
|
+
}
|
|
5817
|
+
}
|
|
5818
|
+
const canonicalError = this.canonicalReplacementError(replacement);
|
|
5819
|
+
if (canonicalError) {
|
|
5820
|
+
debugWs(`[Granular DEBUG] Rejected session snapshot: ${canonicalError}`);
|
|
5821
|
+
return false;
|
|
5822
|
+
}
|
|
5823
|
+
if (this.canonicalDocumentActivated && incoming.epoch === current.epoch && incoming.revision === current.revision && !readSessionFeedSnapshot(this.doc).error && stableDocumentFingerprint(Automerge.toJS(replacement)) !== stableDocumentFingerprint(Automerge.toJS(this.doc))) {
|
|
5824
|
+
debugWs(
|
|
5825
|
+
"[Granular DEBUG] Rejected divergent canonical snapshot at the accepted document version."
|
|
5826
|
+
);
|
|
5827
|
+
return false;
|
|
5828
|
+
}
|
|
5829
|
+
return true;
|
|
5830
|
+
}
|
|
5831
|
+
exposeCanonicalQuarantine(replacement, currentCanonical, error) {
|
|
5832
|
+
const version = documentVersion(replacement);
|
|
5833
|
+
if (currentCanonical) {
|
|
5834
|
+
const quarantine2 = Object.freeze({
|
|
5835
|
+
documentEpoch: version.epoch,
|
|
5836
|
+
documentRevision: version.revision,
|
|
5837
|
+
error: new Error(error.message)
|
|
5838
|
+
});
|
|
5839
|
+
for (const handler of this.canonicalDocumentQuarantineHandlers) {
|
|
5840
|
+
handler(quarantine2);
|
|
5841
|
+
}
|
|
5842
|
+
return;
|
|
5843
|
+
}
|
|
5844
|
+
const accepted = Automerge.toJS(this.doc);
|
|
5845
|
+
const quarantine = Automerge.from({
|
|
5846
|
+
...accepted,
|
|
5847
|
+
documentEpoch: version.epoch,
|
|
5848
|
+
documentRevision: version.revision,
|
|
5849
|
+
feed: {
|
|
5850
|
+
activation: { mode: "canonical" }
|
|
5851
|
+
}
|
|
5852
|
+
});
|
|
5853
|
+
this.doc = quarantine;
|
|
5854
|
+
this.emit("sync", this.doc);
|
|
5855
|
+
debugWs(
|
|
5856
|
+
`[Granular DEBUG] Canonical activation quarantined pending repair: ${error.message}`
|
|
5857
|
+
);
|
|
5858
|
+
}
|
|
5859
|
+
canonicalReplacementError(replacement) {
|
|
5860
|
+
if (!isCanonicalSessionFeedDocument(replacement)) return null;
|
|
5861
|
+
const incoming = readSessionFeedSnapshot(replacement);
|
|
5862
|
+
if (incoming.error) {
|
|
5863
|
+
return `canonical feed is invalid: ${incoming.error.message}`;
|
|
5864
|
+
}
|
|
5865
|
+
if (!this.canonicalDocumentActivated) return null;
|
|
5866
|
+
const current = readSessionFeedSnapshot(this.doc);
|
|
5867
|
+
if (current.error) {
|
|
5868
|
+
return null;
|
|
5869
|
+
}
|
|
5870
|
+
if (incoming.lastSequence < current.lastSequence) {
|
|
5871
|
+
return `canonical lastSequence ${incoming.lastSequence} regresses accepted ${current.lastSequence}`;
|
|
5872
|
+
}
|
|
5873
|
+
const currentBySequence = new Map(
|
|
5874
|
+
current.tail.map((item) => [item.sequence, item])
|
|
5875
|
+
);
|
|
5876
|
+
for (const item of incoming.tail) {
|
|
5877
|
+
const accepted = currentBySequence.get(item.sequence);
|
|
5878
|
+
if (accepted && stableDocumentFingerprint(item) !== stableDocumentFingerprint(accepted)) {
|
|
5879
|
+
return `canonical occurrence ${item.sequence} conflicts with accepted history`;
|
|
5880
|
+
}
|
|
5881
|
+
}
|
|
5882
|
+
return null;
|
|
5883
|
+
}
|
|
5884
|
+
assertIncrementalDocumentIsSafe(replacement) {
|
|
5885
|
+
const missingDependencies = Automerge.getMissingDeps(replacement, []);
|
|
5886
|
+
if (missingDependencies.length > 0) {
|
|
5887
|
+
throw new Error(
|
|
5888
|
+
`Incremental session update is missing ${missingDependencies.length} causal dependency/dependencies`
|
|
5889
|
+
);
|
|
5890
|
+
}
|
|
5891
|
+
if (this.canonicalDocumentActivated && !isCanonicalSessionFeedDocument(replacement)) {
|
|
5892
|
+
throw new Error(
|
|
5893
|
+
"Incremental session update would deactivate the canonical feed"
|
|
5894
|
+
);
|
|
5895
|
+
}
|
|
5896
|
+
const current = documentVersion(this.doc);
|
|
5897
|
+
const incoming = documentVersion(replacement);
|
|
5898
|
+
if (incoming.epoch < current.epoch || incoming.epoch === current.epoch && incoming.revision < current.revision) {
|
|
5899
|
+
throw new Error("Incremental session update regressed document version");
|
|
5900
|
+
}
|
|
5901
|
+
const canonicalError = this.canonicalReplacementError(replacement);
|
|
5902
|
+
if (canonicalError) {
|
|
5903
|
+
throw new Error(canonicalError);
|
|
5904
|
+
}
|
|
5905
|
+
if (this.canonicalDocumentActivated && incoming.epoch === current.epoch && incoming.revision === current.revision && stableDocumentFingerprint(Automerge.toJS(replacement)) !== stableDocumentFingerprint(Automerge.toJS(this.doc))) {
|
|
5906
|
+
throw new Error(
|
|
5907
|
+
"Incremental session update diverged without advancing document revision"
|
|
5908
|
+
);
|
|
5909
|
+
}
|
|
5910
|
+
}
|
|
5911
|
+
requireDocumentResync(envelopeType, error) {
|
|
5912
|
+
this.documentRepairRequired = true;
|
|
5913
|
+
console.warn(
|
|
5914
|
+
`[Granular] ${envelopeType} could not be applied; reconnecting for a fresh session snapshot.`,
|
|
5915
|
+
error
|
|
5916
|
+
);
|
|
5917
|
+
const socket = this.ws;
|
|
5918
|
+
if (!socket) {
|
|
5919
|
+
this.scheduleReconnectAttempt();
|
|
5920
|
+
return;
|
|
5921
|
+
}
|
|
5922
|
+
this.connectionEpoch += 1;
|
|
5923
|
+
this.ws = null;
|
|
5924
|
+
try {
|
|
5925
|
+
socket.close(DOCUMENT_RESYNC_CLOSE_CODE, DOCUMENT_RESYNC_CLOSE_REASON);
|
|
5926
|
+
} catch {
|
|
5927
|
+
}
|
|
5928
|
+
this.handleDisconnect({
|
|
5929
|
+
code: DOCUMENT_RESYNC_CLOSE_CODE,
|
|
5930
|
+
reason: DOCUMENT_RESYNC_CLOSE_REASON,
|
|
5931
|
+
wasClean: false
|
|
5932
|
+
});
|
|
5933
|
+
}
|
|
4391
5934
|
handleDisconnect(close = {}) {
|
|
4392
5935
|
const unexpected = !this.isExplicitlyDisconnected;
|
|
4393
5936
|
const info = {
|
|
@@ -4406,11 +5949,11 @@ var WSClient = class {
|
|
|
4406
5949
|
}
|
|
4407
5950
|
if (unexpected) {
|
|
4408
5951
|
const disconnectError = this.buildDisconnectError(info);
|
|
4409
|
-
this.rejectPending(disconnectError);
|
|
4410
|
-
this.emit("disconnect", info);
|
|
4411
5952
|
const reconnectDelayMs = this.scheduleReconnectAttempt();
|
|
4412
5953
|
info.reconnectScheduled = reconnectDelayMs !== null;
|
|
4413
5954
|
if (reconnectDelayMs !== null) info.reconnectDelayMs = reconnectDelayMs;
|
|
5955
|
+
this.rejectPending(disconnectError);
|
|
5956
|
+
this.emit("disconnect", info);
|
|
4414
5957
|
if (this.options.onUnexpectedClose) {
|
|
4415
5958
|
try {
|
|
4416
5959
|
this.options.onUnexpectedClose(info);
|
|
@@ -4430,100 +5973,109 @@ var WSClient = class {
|
|
|
4430
5973
|
JSON.stringify(message).slice(0, 500)
|
|
4431
5974
|
);
|
|
4432
5975
|
if ("type" in message && message.type === "sync") {
|
|
4433
|
-
|
|
4434
|
-
|
|
5976
|
+
this.requireDocumentResync(
|
|
5977
|
+
"Unsupported Automerge sync envelope",
|
|
5978
|
+
new Error("Use snapshot, snapshot_reset, or change.")
|
|
5979
|
+
);
|
|
5980
|
+
return;
|
|
5981
|
+
}
|
|
5982
|
+
if ("type" in message && message.type === "snapshot_reset") {
|
|
5983
|
+
const resetMessage = message;
|
|
4435
5984
|
try {
|
|
4436
|
-
|
|
4437
|
-
|
|
4438
|
-
const binaryString = atob(payload);
|
|
4439
|
-
const len = binaryString.length;
|
|
4440
|
-
bytes = new Uint8Array(len);
|
|
4441
|
-
for (let i = 0; i < len; i++) {
|
|
4442
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
4443
|
-
}
|
|
4444
|
-
} else if (Array.isArray(payload)) {
|
|
4445
|
-
bytes = new Uint8Array(payload);
|
|
4446
|
-
} else if (payload instanceof Uint8Array) {
|
|
4447
|
-
bytes = payload;
|
|
4448
|
-
} else {
|
|
4449
|
-
return;
|
|
5985
|
+
if (!Number.isSafeInteger(resetMessage.documentEpoch) || resetMessage.documentEpoch <= 0 || !Number.isSafeInteger(resetMessage.documentRevision) || resetMessage.documentRevision < 0 || !Array.isArray(resetMessage.data)) {
|
|
5986
|
+
throw new Error("snapshot_reset metadata is invalid");
|
|
4450
5987
|
}
|
|
4451
|
-
|
|
4452
|
-
|
|
4453
|
-
|
|
4454
|
-
|
|
4455
|
-
|
|
5988
|
+
const replacement = Automerge.load(
|
|
5989
|
+
this.decodeDocumentBytes(
|
|
5990
|
+
resetMessage.data,
|
|
5991
|
+
"Automerge snapshot_reset"
|
|
5992
|
+
)
|
|
4456
5993
|
);
|
|
4457
|
-
|
|
4458
|
-
|
|
4459
|
-
const
|
|
4460
|
-
if (
|
|
4461
|
-
|
|
4462
|
-
"
|
|
4463
|
-
Object.keys(docAny.catalog || {})
|
|
4464
|
-
);
|
|
4465
|
-
debugWs(
|
|
4466
|
-
"[Granular DEBUG] RawToolCatalogs:",
|
|
4467
|
-
Object.keys(docAny.catalog.rawToolCatalogs || {})
|
|
4468
|
-
);
|
|
4469
|
-
} else {
|
|
4470
|
-
debugWs(
|
|
4471
|
-
"[Granular DEBUG] Doc synced but no catalog yet. Keys in doc:",
|
|
4472
|
-
Object.keys(docAny)
|
|
5994
|
+
const replacementVersion = documentVersion(replacement);
|
|
5995
|
+
const replacementEpoch = replacementVersion.epoch;
|
|
5996
|
+
const replacementRevision = replacementVersion.revision;
|
|
5997
|
+
if (replacementEpoch !== resetMessage.documentEpoch || replacementRevision !== resetMessage.documentRevision) {
|
|
5998
|
+
throw new Error(
|
|
5999
|
+
"snapshot_reset metadata does not match the saved document"
|
|
4473
6000
|
);
|
|
4474
6001
|
}
|
|
4475
|
-
this.
|
|
4476
|
-
|
|
4477
|
-
|
|
4478
|
-
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
|
|
4482
|
-
|
|
4483
|
-
this.emit("sync", this.doc);
|
|
4484
|
-
debugWs(
|
|
4485
|
-
"[Granular DEBUG] applyChanges succeeded. Doc:",
|
|
4486
|
-
JSON.stringify(Automerge.toJS(this.doc))
|
|
4487
|
-
);
|
|
4488
|
-
} catch (applyError) {
|
|
4489
|
-
console.warn(
|
|
4490
|
-
"[Granular] Failed to apply sync message (both sync & applyChanges)",
|
|
4491
|
-
e,
|
|
4492
|
-
applyError
|
|
4493
|
-
);
|
|
6002
|
+
if (!this.canAcceptDocumentReplacement(replacement, true)) {
|
|
6003
|
+
if (this.documentRepairRequired) {
|
|
6004
|
+
this.requireDocumentResync(
|
|
6005
|
+
"Stale Automerge snapshot_reset during document repair",
|
|
6006
|
+
new Error("Replacement reset did not advance accepted state")
|
|
6007
|
+
);
|
|
6008
|
+
}
|
|
6009
|
+
return;
|
|
4494
6010
|
}
|
|
6011
|
+
this.doc = replacement;
|
|
6012
|
+
this.documentRepairRequired = false;
|
|
6013
|
+
this.rememberCanonicalActivation(replacement);
|
|
6014
|
+
this.emit("snapshot_reset", {
|
|
6015
|
+
documentEpoch: replacementEpoch,
|
|
6016
|
+
documentRevision: replacementRevision
|
|
6017
|
+
});
|
|
6018
|
+
this.emit("sync", this.doc);
|
|
6019
|
+
} catch (error) {
|
|
6020
|
+
this.requireDocumentResync("Automerge snapshot_reset", error);
|
|
4495
6021
|
}
|
|
4496
6022
|
return;
|
|
4497
6023
|
}
|
|
4498
6024
|
if ("type" in message && message.type === "snapshot") {
|
|
4499
6025
|
const snapshotMessage = message;
|
|
4500
6026
|
try {
|
|
4501
|
-
const bytes =
|
|
6027
|
+
const bytes = this.decodeDocumentBytes(
|
|
6028
|
+
snapshotMessage.data,
|
|
6029
|
+
"Automerge snapshot"
|
|
6030
|
+
);
|
|
4502
6031
|
debugWs(
|
|
4503
6032
|
"[Granular DEBUG] Loading Automerge session snapshot bytes:",
|
|
4504
6033
|
bytes.length
|
|
4505
6034
|
);
|
|
4506
|
-
|
|
6035
|
+
const replacement = Automerge.load(bytes);
|
|
6036
|
+
if (!this.canAcceptDocumentReplacement(replacement, false)) {
|
|
6037
|
+
if (this.documentRepairRequired) {
|
|
6038
|
+
this.requireDocumentResync(
|
|
6039
|
+
"Stale Automerge snapshot during document repair",
|
|
6040
|
+
new Error("Replacement snapshot regressed accepted state")
|
|
6041
|
+
);
|
|
6042
|
+
}
|
|
6043
|
+
return;
|
|
6044
|
+
}
|
|
6045
|
+
this.doc = replacement;
|
|
6046
|
+
this.documentRepairRequired = false;
|
|
6047
|
+
this.rememberCanonicalActivation(replacement);
|
|
4507
6048
|
this.emit("sync", this.doc);
|
|
4508
6049
|
debugWs(
|
|
4509
6050
|
"[Granular DEBUG] Automerge session snapshot loaded. Doc:",
|
|
4510
6051
|
JSON.stringify(Automerge.toJS(this.doc))
|
|
4511
6052
|
);
|
|
4512
|
-
} catch (
|
|
4513
|
-
|
|
6053
|
+
} catch (error) {
|
|
6054
|
+
this.requireDocumentResync("Automerge snapshot", error);
|
|
4514
6055
|
}
|
|
4515
6056
|
return;
|
|
4516
6057
|
}
|
|
4517
6058
|
if ("type" in message && message.type === "change") {
|
|
6059
|
+
if (this.documentRepairRequired) {
|
|
6060
|
+
debugWs(
|
|
6061
|
+
"[Granular DEBUG] Ignoring raw change while a replacement snapshot is required."
|
|
6062
|
+
);
|
|
6063
|
+
return;
|
|
6064
|
+
}
|
|
4518
6065
|
const changeMessage = message;
|
|
4519
6066
|
try {
|
|
4520
|
-
const bytes =
|
|
6067
|
+
const bytes = this.decodeDocumentBytes(
|
|
6068
|
+
changeMessage.data,
|
|
6069
|
+
"Automerge change"
|
|
6070
|
+
);
|
|
4521
6071
|
const [newDoc] = Automerge.applyChanges(this.doc, [bytes]);
|
|
6072
|
+
this.assertIncrementalDocumentIsSafe(newDoc);
|
|
4522
6073
|
this.doc = newDoc;
|
|
6074
|
+
this.rememberCanonicalActivation(newDoc);
|
|
4523
6075
|
this.emit("change", changeMessage);
|
|
4524
6076
|
this.emit("sync", this.doc);
|
|
4525
|
-
} catch (
|
|
4526
|
-
|
|
6077
|
+
} catch (error) {
|
|
6078
|
+
this.requireDocumentResync("Automerge change message", error);
|
|
4527
6079
|
}
|
|
4528
6080
|
return;
|
|
4529
6081
|
}
|
|
@@ -4642,6 +6194,18 @@ var WSClient = class {
|
|
|
4642
6194
|
}
|
|
4643
6195
|
this.eventHandlers.get(event).push(handler);
|
|
4644
6196
|
}
|
|
6197
|
+
/**
|
|
6198
|
+
* Subscribe to rejected canonical replacement metadata without exposing the
|
|
6199
|
+
* malformed document through the public sync stream.
|
|
6200
|
+
*
|
|
6201
|
+
* @internal Session uses this to quarantine only its feed projection.
|
|
6202
|
+
*/
|
|
6203
|
+
onCanonicalDocumentQuarantine(handler) {
|
|
6204
|
+
this.canonicalDocumentQuarantineHandlers.add(handler);
|
|
6205
|
+
return () => {
|
|
6206
|
+
this.canonicalDocumentQuarantineHandlers.delete(handler);
|
|
6207
|
+
};
|
|
6208
|
+
}
|
|
4645
6209
|
/**
|
|
4646
6210
|
* Register an RPC handler for incoming server requests
|
|
4647
6211
|
* @param {string} method - RPC method name
|
|
@@ -4699,7 +6263,7 @@ var WSClient = class {
|
|
|
4699
6263
|
};
|
|
4700
6264
|
|
|
4701
6265
|
// src/prompt-utils.ts
|
|
4702
|
-
function
|
|
6266
|
+
function asRecord2(value) {
|
|
4703
6267
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
4704
6268
|
return value;
|
|
4705
6269
|
}
|
|
@@ -4714,7 +6278,7 @@ function parseJsonPromptChoiceOption(option) {
|
|
|
4714
6278
|
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
|
4715
6279
|
try {
|
|
4716
6280
|
const parsed = JSON.parse(trimmed);
|
|
4717
|
-
return
|
|
6281
|
+
return asRecord2(parsed);
|
|
4718
6282
|
} catch {
|
|
4719
6283
|
return null;
|
|
4720
6284
|
}
|
|
@@ -4776,33 +6340,37 @@ function normalizePromptType(raw) {
|
|
|
4776
6340
|
if (kind === "confirm" || kind === "choice" || kind === "input") return kind;
|
|
4777
6341
|
if (promptType === "confirm" || promptType === "choice" || promptType === "input")
|
|
4778
6342
|
return promptType;
|
|
4779
|
-
return
|
|
6343
|
+
return null;
|
|
4780
6344
|
}
|
|
4781
6345
|
function normalizePrompt(rawValue) {
|
|
4782
|
-
const raw =
|
|
6346
|
+
const raw = asRecord2(rawValue);
|
|
4783
6347
|
if (!raw) return null;
|
|
4784
|
-
const promptRecord =
|
|
6348
|
+
const promptRecord = asRecord2(raw.prompt);
|
|
4785
6349
|
const source = promptRecord || raw;
|
|
4786
6350
|
const id = typeof source.id === "string" ? source.id : typeof raw.id === "string" ? raw.id : typeof raw.promptId === "string" ? raw.promptId : "";
|
|
4787
6351
|
if (!id) return null;
|
|
4788
6352
|
const jobId = typeof source.jobId === "string" && source.jobId.trim() ? source.jobId.trim() : typeof raw.jobId === "string" && raw.jobId.trim() ? raw.jobId.trim() : void 0;
|
|
4789
6353
|
const turnId = typeof source.turnId === "string" && source.turnId.trim() ? source.turnId.trim() : typeof raw.turnId === "string" && raw.turnId.trim() ? raw.turnId.trim() : void 0;
|
|
6354
|
+
const type = normalizePromptType(
|
|
6355
|
+
source === raw ? raw : { ...raw, ...source }
|
|
6356
|
+
);
|
|
6357
|
+
if (!type) return null;
|
|
4790
6358
|
return {
|
|
4791
6359
|
id,
|
|
4792
6360
|
...jobId ? { jobId } : {},
|
|
4793
6361
|
...turnId ? { turnId } : {},
|
|
4794
|
-
type
|
|
6362
|
+
type,
|
|
4795
6363
|
title: typeof source.title === "string" ? source.title : "Input required",
|
|
4796
6364
|
message: typeof source.message === "string" ? source.message : "",
|
|
4797
6365
|
options: Array.isArray(source.options) ? source.options.map(
|
|
4798
|
-
(option) => typeof option === "string" ||
|
|
6366
|
+
(option) => typeof option === "string" || asRecord2(option) ? normalizePromptChoiceOption(
|
|
4799
6367
|
option
|
|
4800
6368
|
) : option
|
|
4801
6369
|
) : void 0,
|
|
4802
6370
|
defaultValue: source.defaultValue,
|
|
4803
6371
|
placeholder: typeof source.placeholder === "string" ? source.placeholder : void 0,
|
|
4804
6372
|
allowEmpty: typeof source.allowEmpty === "boolean" ? source.allowEmpty : void 0,
|
|
4805
|
-
metadata:
|
|
6373
|
+
metadata: asRecord2(source.metadata) || void 0
|
|
4806
6374
|
};
|
|
4807
6375
|
}
|
|
4808
6376
|
function resolvePromptAnswer(prompt, answer) {
|
|
@@ -4830,23 +6398,36 @@ function resolvePromptAnswer(prompt, answer) {
|
|
|
4830
6398
|
}
|
|
4831
6399
|
|
|
4832
6400
|
// src/session.ts
|
|
4833
|
-
var PROMPT_TRANSCRIPT_APPEND_TIMEOUT_MS = 5e3;
|
|
4834
6401
|
function toPascalCase(value) {
|
|
4835
6402
|
return value.split(/[_:\-\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
4836
6403
|
}
|
|
4837
|
-
function
|
|
4838
|
-
|
|
4839
|
-
return
|
|
4840
|
-
|
|
4841
|
-
|
|
4842
|
-
|
|
4843
|
-
|
|
4844
|
-
|
|
4845
|
-
|
|
4846
|
-
|
|
4847
|
-
|
|
4848
|
-
|
|
4849
|
-
|
|
6404
|
+
function reserveUserMessageId() {
|
|
6405
|
+
const randomUuid = globalThis.crypto?.randomUUID?.();
|
|
6406
|
+
return randomUuid ? `message_${randomUuid}` : `message_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
6407
|
+
}
|
|
6408
|
+
function normalizeUserMessageIdentity(value, field) {
|
|
6409
|
+
if (value === void 0) return void 0;
|
|
6410
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
6411
|
+
throw new Error(`User message ${field} must be a non-empty string.`);
|
|
6412
|
+
}
|
|
6413
|
+
return value.trim();
|
|
6414
|
+
}
|
|
6415
|
+
function recordFromUnknown(value) {
|
|
6416
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
6417
|
+
}
|
|
6418
|
+
function promptSnapshotFingerprint(prompt) {
|
|
6419
|
+
return JSON.stringify({
|
|
6420
|
+
id: prompt.id,
|
|
6421
|
+
jobId: prompt.jobId || null,
|
|
6422
|
+
turnId: prompt.turnId || null,
|
|
6423
|
+
type: prompt.type,
|
|
6424
|
+
title: prompt.title,
|
|
6425
|
+
message: prompt.message,
|
|
6426
|
+
options: prompt.options || null,
|
|
6427
|
+
defaultValue: prompt.defaultValue,
|
|
6428
|
+
placeholder: prompt.placeholder || null,
|
|
6429
|
+
allowEmpty: prompt.allowEmpty,
|
|
6430
|
+
metadata: prompt.metadata || null
|
|
4850
6431
|
});
|
|
4851
6432
|
}
|
|
4852
6433
|
var Session = class {
|
|
@@ -4854,7 +6435,6 @@ var Session = class {
|
|
|
4854
6435
|
clientId;
|
|
4855
6436
|
initialQuota;
|
|
4856
6437
|
jobsMap = /* @__PURE__ */ new Map();
|
|
4857
|
-
pendingAgentMessagesByJobId = /* @__PURE__ */ new Map();
|
|
4858
6438
|
eventListeners = /* @__PURE__ */ new Map();
|
|
4859
6439
|
toolHandlers = /* @__PURE__ */ new Map();
|
|
4860
6440
|
/** Tracks which tools are instance methods (className set, not static) */
|
|
@@ -4871,10 +6451,23 @@ var Session = class {
|
|
|
4871
6451
|
domainPackagePartCache = /* @__PURE__ */ new Map();
|
|
4872
6452
|
domainPackagePartPromises = /* @__PURE__ */ new Map();
|
|
4873
6453
|
domainPackageFetchQueue = Promise.resolve();
|
|
6454
|
+
feedController;
|
|
6455
|
+
feed;
|
|
4874
6456
|
constructor(client, clientId, options = {}) {
|
|
4875
6457
|
this.client = client;
|
|
4876
6458
|
this.clientId = clientId || `client_${Date.now()}`;
|
|
4877
6459
|
this.initialQuota = options.initialQuota || null;
|
|
6460
|
+
this.feedController = new SessionFeedController(this.client.doc, {
|
|
6461
|
+
listTransport: (feedOptions) => this.client.call(
|
|
6462
|
+
"feed.list",
|
|
6463
|
+
feedOptions
|
|
6464
|
+
)
|
|
6465
|
+
});
|
|
6466
|
+
this.feed = Object.freeze({
|
|
6467
|
+
getSnapshot: () => this.feedController.getSnapshot(),
|
|
6468
|
+
list: (feedOptions = {}) => this.feedController.list(feedOptions),
|
|
6469
|
+
subscribe: (listener, subscribeOptions = {}) => this.feedController.subscribe(listener, subscribeOptions)
|
|
6470
|
+
});
|
|
4878
6471
|
this.setupEventHandlers();
|
|
4879
6472
|
this.setupToolInvokeHandler();
|
|
4880
6473
|
this.currentDomainRevision = this.extractDomainRevisionFromDoc(
|
|
@@ -4893,34 +6486,160 @@ var Session = class {
|
|
|
4893
6486
|
}
|
|
4894
6487
|
return null;
|
|
4895
6488
|
}
|
|
4896
|
-
|
|
6489
|
+
/**
|
|
6490
|
+
* Prompt delivery is deliberately event-driven while a socket is live, but
|
|
6491
|
+
* an existing session can be attached from a fresh tab/client after the
|
|
6492
|
+
* event was originally sent. Rebuild that client's prompt cache from the
|
|
6493
|
+
* canonical document on every sync so an open durable prompt remains
|
|
6494
|
+
* actionable after reconnect without manufacturing a second prompt.
|
|
6495
|
+
*/
|
|
6496
|
+
reconcilePromptCacheFromCanonicalDocument(doc) {
|
|
6497
|
+
const jobsById = recordFromUnknown(recordFromUnknown(doc)?.jobs)?.byId;
|
|
6498
|
+
const jobs = recordFromUnknown(jobsById) || {};
|
|
6499
|
+
const openPromptIds = /* @__PURE__ */ new Set();
|
|
6500
|
+
for (const [jobId, jobValue] of Object.entries(jobs)) {
|
|
6501
|
+
const prompts = recordFromUnknown(recordFromUnknown(jobValue)?.prompts);
|
|
6502
|
+
if (!prompts) continue;
|
|
6503
|
+
for (const [promptId, promptValue] of Object.entries(prompts)) {
|
|
6504
|
+
const persisted = recordFromUnknown(promptValue);
|
|
6505
|
+
if (!persisted || persisted.status !== "open") continue;
|
|
6506
|
+
const prompt = normalizePrompt({
|
|
6507
|
+
promptId,
|
|
6508
|
+
jobId,
|
|
6509
|
+
kind: persisted.kind,
|
|
6510
|
+
type: persisted.type,
|
|
6511
|
+
title: persisted.title,
|
|
6512
|
+
message: persisted.message,
|
|
6513
|
+
options: persisted.options,
|
|
6514
|
+
defaultValue: persisted.defaultValue,
|
|
6515
|
+
placeholder: persisted.placeholder,
|
|
6516
|
+
allowEmpty: persisted.allowEmpty,
|
|
6517
|
+
metadata: persisted.metadata
|
|
6518
|
+
});
|
|
6519
|
+
if (!prompt) continue;
|
|
6520
|
+
openPromptIds.add(prompt.id);
|
|
6521
|
+
if (this.hiddenPromptIds.has(prompt.id)) continue;
|
|
6522
|
+
const previous = this.promptCache.get(prompt.id);
|
|
6523
|
+
this.promptCache.set(prompt.id, prompt);
|
|
6524
|
+
if (!previous || promptSnapshotFingerprint(previous) !== promptSnapshotFingerprint(prompt)) {
|
|
6525
|
+
this.emit("prompt", prompt);
|
|
6526
|
+
}
|
|
6527
|
+
}
|
|
6528
|
+
}
|
|
6529
|
+
for (const promptId of this.promptCache.keys()) {
|
|
6530
|
+
if (!openPromptIds.has(promptId)) {
|
|
6531
|
+
this.promptCache.delete(promptId);
|
|
6532
|
+
}
|
|
6533
|
+
}
|
|
6534
|
+
}
|
|
6535
|
+
buildDirectedInvocationEffectContext(params, feedbackContext) {
|
|
4897
6536
|
return {
|
|
4898
6537
|
effectClientId: this.clientId,
|
|
4899
|
-
sandboxId: "",
|
|
4900
|
-
environmentId: "",
|
|
4901
|
-
|
|
6538
|
+
sandboxId: params.sandboxId || "",
|
|
6539
|
+
environmentId: params.environmentId || "",
|
|
6540
|
+
invocationId: params.callId,
|
|
6541
|
+
jobId: params.jobId,
|
|
6542
|
+
sessionId: params.sessionId || this.client.currentSessionId,
|
|
4902
6543
|
user: {
|
|
4903
6544
|
granularId: "",
|
|
4904
6545
|
userId: "",
|
|
4905
6546
|
subjectId: ""
|
|
4906
|
-
}
|
|
6547
|
+
},
|
|
6548
|
+
...feedbackContext ? {
|
|
6549
|
+
feedback: feedbackContext.feedback,
|
|
6550
|
+
transientFeedback: feedbackContext.transientFeedback
|
|
6551
|
+
} : {}
|
|
4907
6552
|
};
|
|
4908
6553
|
}
|
|
4909
|
-
|
|
4910
|
-
if (
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
4921
|
-
|
|
4922
|
-
|
|
4923
|
-
|
|
6554
|
+
createDirectedInvocationFeedbackContext(params) {
|
|
6555
|
+
if (!params.feedbackCapability) return null;
|
|
6556
|
+
const publications = [];
|
|
6557
|
+
let nextOperationOrdinal = 0;
|
|
6558
|
+
const nextOperationId = (kind) => `${kind}:${nextOperationOrdinal++}`;
|
|
6559
|
+
const track = (publication) => {
|
|
6560
|
+
const tracked = Promise.resolve(publication);
|
|
6561
|
+
publications.push(tracked);
|
|
6562
|
+
void tracked.catch(() => void 0);
|
|
6563
|
+
return tracked;
|
|
6564
|
+
};
|
|
6565
|
+
const methodMap = {
|
|
6566
|
+
"feed.feedback": "tool.feedback",
|
|
6567
|
+
"feed.transient.create": "tool.transient.create",
|
|
6568
|
+
"feed.transient.update": "tool.transient.update",
|
|
6569
|
+
"feed.transient.settle": "tool.transient.settle"
|
|
6570
|
+
};
|
|
6571
|
+
const publisher = createFeedPublisher((method, publishParams) => {
|
|
6572
|
+
const directedFeedbackMethod = methodMap[method];
|
|
6573
|
+
if (!directedFeedbackMethod) {
|
|
6574
|
+
throw new Error(`Unsupported directed feedback method: ${method}`);
|
|
6575
|
+
}
|
|
6576
|
+
return this.client.call(directedFeedbackMethod, {
|
|
6577
|
+
...publishParams,
|
|
6578
|
+
callId: params.callId,
|
|
6579
|
+
feedbackCapability: params.feedbackCapability
|
|
6580
|
+
});
|
|
6581
|
+
});
|
|
6582
|
+
const wrapTransientHandle = (initial) => {
|
|
6583
|
+
let current = initial;
|
|
6584
|
+
const wrapped = {
|
|
6585
|
+
get id() {
|
|
6586
|
+
return current.id;
|
|
6587
|
+
},
|
|
6588
|
+
get ordinal() {
|
|
6589
|
+
return current.ordinal;
|
|
6590
|
+
},
|
|
6591
|
+
get revision() {
|
|
6592
|
+
return current.revision;
|
|
6593
|
+
},
|
|
6594
|
+
async update(text, options = {}) {
|
|
6595
|
+
current = await track(
|
|
6596
|
+
current.update(text, {
|
|
6597
|
+
...options,
|
|
6598
|
+
operationId: options.operationId || nextOperationId("transient-update")
|
|
6599
|
+
})
|
|
6600
|
+
);
|
|
6601
|
+
return wrapped;
|
|
6602
|
+
},
|
|
6603
|
+
settle(text, options = {}) {
|
|
6604
|
+
return track(
|
|
6605
|
+
current.settle(text, {
|
|
6606
|
+
...options,
|
|
6607
|
+
operationId: options.operationId || nextOperationId("transient-settle")
|
|
6608
|
+
})
|
|
6609
|
+
);
|
|
6610
|
+
}
|
|
6611
|
+
};
|
|
6612
|
+
return wrapped;
|
|
6613
|
+
};
|
|
6614
|
+
return {
|
|
6615
|
+
feedback: (text, options = {}) => track(
|
|
6616
|
+
publisher.feedback(text, {
|
|
6617
|
+
...options,
|
|
6618
|
+
operationId: options.operationId || nextOperationId("feedback")
|
|
6619
|
+
})
|
|
6620
|
+
),
|
|
6621
|
+
transientFeedback: (text, options = {}) => track(
|
|
6622
|
+
publisher.transientFeedback(text, {
|
|
6623
|
+
...options,
|
|
6624
|
+
operationId: options.operationId || nextOperationId("transient-create")
|
|
6625
|
+
}).then(wrapTransientHandle)
|
|
6626
|
+
),
|
|
6627
|
+
async flush() {
|
|
6628
|
+
let cursor = 0;
|
|
6629
|
+
let firstError;
|
|
6630
|
+
while (cursor < publications.length) {
|
|
6631
|
+
const batch = publications.slice(cursor);
|
|
6632
|
+
cursor = publications.length;
|
|
6633
|
+
const results = await Promise.allSettled(batch);
|
|
6634
|
+
for (const result of results) {
|
|
6635
|
+
if (result.status === "rejected" && firstError === void 0) {
|
|
6636
|
+
firstError = result.reason;
|
|
6637
|
+
}
|
|
6638
|
+
}
|
|
6639
|
+
}
|
|
6640
|
+
if (firstError !== void 0) throw firstError;
|
|
6641
|
+
}
|
|
6642
|
+
};
|
|
4924
6643
|
}
|
|
4925
6644
|
// --- Public API ---
|
|
4926
6645
|
get document() {
|
|
@@ -5050,17 +6769,8 @@ var Session = class {
|
|
|
5050
6769
|
code,
|
|
5051
6770
|
domainRevision: revision,
|
|
5052
6771
|
createdAt: Date.now()
|
|
5053
|
-
});
|
|
5054
|
-
this.jobsMap.set(result.jobId, job);
|
|
5055
|
-
const pendingAgentMessages = this.pendingAgentMessagesByJobId.get(
|
|
5056
|
-
result.jobId
|
|
5057
|
-
);
|
|
5058
|
-
if (pendingAgentMessages && pendingAgentMessages.length > 0) {
|
|
5059
|
-
this.pendingAgentMessagesByJobId.delete(result.jobId);
|
|
5060
|
-
for (const message of pendingAgentMessages) {
|
|
5061
|
-
job.replayAgentMessage(message);
|
|
5062
|
-
}
|
|
5063
|
-
}
|
|
6772
|
+
});
|
|
6773
|
+
this.jobsMap.set(result.jobId, job);
|
|
5064
6774
|
return job;
|
|
5065
6775
|
}
|
|
5066
6776
|
/**
|
|
@@ -5081,50 +6791,44 @@ var Session = class {
|
|
|
5081
6791
|
async answerPrompt(promptId, answer) {
|
|
5082
6792
|
const prompt = this.promptCache.get(promptId);
|
|
5083
6793
|
const resolvedAnswer = resolvePromptAnswer(prompt, answer);
|
|
6794
|
+
const response = await this.client.call("prompt.answer", {
|
|
6795
|
+
promptId,
|
|
6796
|
+
answer: resolvedAnswer,
|
|
6797
|
+
value: resolvedAnswer
|
|
6798
|
+
});
|
|
6799
|
+
if (response && typeof response === "object" && "ok" in response && response.ok === false) {
|
|
6800
|
+
const rejected = response;
|
|
6801
|
+
const errorMessage = typeof rejected.error === "string" ? rejected.error : "Prompt answer was rejected.";
|
|
6802
|
+
throw new Error(errorMessage);
|
|
6803
|
+
}
|
|
5084
6804
|
this.promptCache.delete(promptId);
|
|
5085
6805
|
this.hiddenPromptIds.add(promptId);
|
|
5086
6806
|
this.emit("prompt:answered", {
|
|
5087
6807
|
...prompt || { id: promptId },
|
|
5088
6808
|
id: promptId,
|
|
6809
|
+
answer: resolvedAnswer,
|
|
5089
6810
|
status: "answered"
|
|
5090
6811
|
});
|
|
5091
|
-
try {
|
|
5092
|
-
const response = await this.client.call("prompt.answer", {
|
|
5093
|
-
promptId,
|
|
5094
|
-
answer: resolvedAnswer,
|
|
5095
|
-
value: resolvedAnswer
|
|
5096
|
-
});
|
|
5097
|
-
if (response && typeof response === "object" && "ok" in response && response.ok === false) {
|
|
5098
|
-
const rejected = response;
|
|
5099
|
-
const errorMessage = typeof rejected.error === "string" ? rejected.error : "Prompt answer was rejected.";
|
|
5100
|
-
throw new Error(errorMessage);
|
|
5101
|
-
}
|
|
5102
|
-
} catch (error) {
|
|
5103
|
-
this.hiddenPromptIds.delete(promptId);
|
|
5104
|
-
if (prompt) {
|
|
5105
|
-
this.promptCache.set(promptId, prompt);
|
|
5106
|
-
}
|
|
5107
|
-
throw error;
|
|
5108
|
-
}
|
|
5109
|
-
try {
|
|
5110
|
-
const content = this.stringifyConversationValue(resolvedAnswer);
|
|
5111
|
-
if (content.trim()) {
|
|
5112
|
-
await withPromptTranscriptTimeout(
|
|
5113
|
-
this.appendConversationMessage({
|
|
5114
|
-
role: "user",
|
|
5115
|
-
content,
|
|
5116
|
-
promptId
|
|
5117
|
-
})
|
|
5118
|
-
);
|
|
5119
|
-
}
|
|
5120
|
-
} catch {
|
|
5121
|
-
}
|
|
5122
6812
|
}
|
|
5123
|
-
async
|
|
5124
|
-
|
|
5125
|
-
|
|
5126
|
-
input
|
|
6813
|
+
async appendUserMessage(input) {
|
|
6814
|
+
const requestedId = normalizeUserMessageIdentity(input.id, "id");
|
|
6815
|
+
const requestedOperationId = normalizeUserMessageIdentity(
|
|
6816
|
+
input.operationId,
|
|
6817
|
+
"operationId"
|
|
5127
6818
|
);
|
|
6819
|
+
const id = requestedId || (requestedOperationId ? void 0 : reserveUserMessageId());
|
|
6820
|
+
const operationId2 = requestedOperationId || `conversation.append:${id}`;
|
|
6821
|
+
const {
|
|
6822
|
+
id: _ignoredInputId,
|
|
6823
|
+
operationId: _ignoredInputOperationId,
|
|
6824
|
+
...message
|
|
6825
|
+
} = input;
|
|
6826
|
+
return this.client.call("conversation.append", {
|
|
6827
|
+
...message,
|
|
6828
|
+
role: "user",
|
|
6829
|
+
...id ? { id } : {},
|
|
6830
|
+
operationId: operationId2
|
|
6831
|
+
});
|
|
5128
6832
|
}
|
|
5129
6833
|
/**
|
|
5130
6834
|
* Get the current list of available effects.
|
|
@@ -5507,6 +7211,7 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5507
7211
|
* Close the session and disconnect from the sandbox
|
|
5508
7212
|
*/
|
|
5509
7213
|
async disconnect() {
|
|
7214
|
+
this.disposeSessionFeed();
|
|
5510
7215
|
try {
|
|
5511
7216
|
await this.client.call("client.goodbye", {
|
|
5512
7217
|
clientId: this.clientId,
|
|
@@ -5516,6 +7221,10 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5516
7221
|
}
|
|
5517
7222
|
this.client.disconnect();
|
|
5518
7223
|
}
|
|
7224
|
+
/** Stop feed repair work without detaching a reconnectable transport. */
|
|
7225
|
+
disposeSessionFeed() {
|
|
7226
|
+
this.feedController.dispose();
|
|
7227
|
+
}
|
|
5519
7228
|
// --- Event Handling ---
|
|
5520
7229
|
/**
|
|
5521
7230
|
* Subscribe to session events
|
|
@@ -5540,9 +7249,14 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5540
7249
|
}
|
|
5541
7250
|
}
|
|
5542
7251
|
// --- Internal ---
|
|
7252
|
+
setFeedListTransport(transport) {
|
|
7253
|
+
this.feedController.setListTransport(transport);
|
|
7254
|
+
}
|
|
5543
7255
|
setupToolInvokeHandler() {
|
|
5544
7256
|
this.client.registerRpcHandler("tool.invoke", async (params) => {
|
|
5545
|
-
const
|
|
7257
|
+
const invocation = params;
|
|
7258
|
+
const { callId, toolName, input, feedbackCapability } = invocation;
|
|
7259
|
+
const capabilityResultParams = feedbackCapability ? { feedbackCapability } : {};
|
|
5546
7260
|
this.emit("effect:invoke", {
|
|
5547
7261
|
callId,
|
|
5548
7262
|
effectKey: toolName,
|
|
@@ -5554,6 +7268,7 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5554
7268
|
if (!handler) {
|
|
5555
7269
|
await this.client.call("tool.result", {
|
|
5556
7270
|
callId,
|
|
7271
|
+
...capabilityResultParams,
|
|
5557
7272
|
error: {
|
|
5558
7273
|
code: "TOOL_NOT_FOUND",
|
|
5559
7274
|
message: `Tool handler not found: ${toolName}`
|
|
@@ -5563,21 +7278,45 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5563
7278
|
}
|
|
5564
7279
|
try {
|
|
5565
7280
|
let result;
|
|
5566
|
-
const
|
|
5567
|
-
|
|
5568
|
-
|
|
5569
|
-
|
|
5570
|
-
|
|
5571
|
-
|
|
5572
|
-
|
|
5573
|
-
|
|
5574
|
-
|
|
5575
|
-
|
|
7281
|
+
const feedbackContext = this.createDirectedInvocationFeedbackContext(invocation);
|
|
7282
|
+
const invocationContext = this.buildDirectedInvocationEffectContext(
|
|
7283
|
+
invocation,
|
|
7284
|
+
feedbackContext
|
|
7285
|
+
);
|
|
7286
|
+
let handlerError;
|
|
7287
|
+
let handlerFailed = false;
|
|
7288
|
+
try {
|
|
7289
|
+
if (this.instanceTools.has(toolName) && input && typeof input === "object" && "_objectId" in input) {
|
|
7290
|
+
const { _objectId, ...restParams } = input;
|
|
7291
|
+
result = await handler(
|
|
7292
|
+
_objectId,
|
|
7293
|
+
restParams,
|
|
7294
|
+
invocationContext
|
|
7295
|
+
);
|
|
7296
|
+
} else {
|
|
7297
|
+
result = await handler(input, invocationContext);
|
|
7298
|
+
}
|
|
7299
|
+
} catch (error) {
|
|
7300
|
+
handlerFailed = true;
|
|
7301
|
+
handlerError = error;
|
|
5576
7302
|
}
|
|
7303
|
+
let feedbackError;
|
|
7304
|
+
let feedbackFailed = false;
|
|
7305
|
+
if (feedbackContext) {
|
|
7306
|
+
try {
|
|
7307
|
+
await feedbackContext.flush();
|
|
7308
|
+
} catch (error) {
|
|
7309
|
+
feedbackFailed = true;
|
|
7310
|
+
feedbackError = error;
|
|
7311
|
+
}
|
|
7312
|
+
}
|
|
7313
|
+
if (handlerFailed) throw handlerError;
|
|
7314
|
+
if (feedbackFailed) throw feedbackError;
|
|
5577
7315
|
this.emit("effect:result", { callId, effectKey: toolName, result });
|
|
5578
7316
|
this.emit("tool:result", { callId, result });
|
|
5579
7317
|
await this.client.call("tool.result", {
|
|
5580
7318
|
callId,
|
|
7319
|
+
...capabilityResultParams,
|
|
5581
7320
|
result
|
|
5582
7321
|
});
|
|
5583
7322
|
} catch (error) {
|
|
@@ -5590,6 +7329,7 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5590
7329
|
this.emit("tool:result", { callId, error: errorMessage });
|
|
5591
7330
|
await this.client.call("tool.result", {
|
|
5592
7331
|
callId,
|
|
7332
|
+
...capabilityResultParams,
|
|
5593
7333
|
error: { code: "TOOL_EXECUTION_FAILED", message: errorMessage }
|
|
5594
7334
|
});
|
|
5595
7335
|
}
|
|
@@ -5602,9 +7342,14 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5602
7342
|
);
|
|
5603
7343
|
this.client.on("sync", (doc) => {
|
|
5604
7344
|
this.currentDomainRevision = this.extractDomainRevisionFromDoc(doc);
|
|
7345
|
+
this.feedController.updateDocument(doc);
|
|
7346
|
+
this.reconcilePromptCacheFromCanonicalDocument(doc);
|
|
5605
7347
|
this.emit("sync", doc);
|
|
5606
7348
|
this.checkForToolChanges();
|
|
5607
7349
|
});
|
|
7350
|
+
this.client.onCanonicalDocumentQuarantine?.((quarantine) => {
|
|
7351
|
+
this.feedController.quarantineCanonicalReplacement(quarantine);
|
|
7352
|
+
});
|
|
5608
7353
|
const emitPrompt = (payload) => {
|
|
5609
7354
|
const prompt = normalizePrompt(payload);
|
|
5610
7355
|
if (!prompt) return;
|
|
@@ -5637,23 +7382,6 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5637
7382
|
this.client.on("harness.text_response.delta", (data) => {
|
|
5638
7383
|
this.emit("harness:text_response_delta", data);
|
|
5639
7384
|
});
|
|
5640
|
-
this.client.on("job.agent_message", (data) => {
|
|
5641
|
-
const normalized = normalizeJobAgentMessageEnvelope(data);
|
|
5642
|
-
if (!normalized) return;
|
|
5643
|
-
this.emit("job:agent_message", normalized);
|
|
5644
|
-
if (this.jobsMap.has(normalized.jobId)) return;
|
|
5645
|
-
const pending = this.pendingAgentMessagesByJobId.get(normalized.jobId) || [];
|
|
5646
|
-
if (normalized.message.messageId && pending.some(
|
|
5647
|
-
(message) => message.messageId === normalized.message.messageId
|
|
5648
|
-
)) {
|
|
5649
|
-
return;
|
|
5650
|
-
}
|
|
5651
|
-
pending.push(normalized.message);
|
|
5652
|
-
this.pendingAgentMessagesByJobId.set(
|
|
5653
|
-
normalized.jobId,
|
|
5654
|
-
pending.slice(-25)
|
|
5655
|
-
);
|
|
5656
|
-
});
|
|
5657
7385
|
this.client.on("exec.completed", (data) => {
|
|
5658
7386
|
this.emit("exec:completed", data);
|
|
5659
7387
|
});
|
|
@@ -5778,24 +7506,6 @@ function sanitizeFeedbackValue(value, depth = 0, seen = /* @__PURE__ */ new Weak
|
|
|
5778
7506
|
}
|
|
5779
7507
|
return truncateFeedbackString(String(value));
|
|
5780
7508
|
}
|
|
5781
|
-
function normalizeJobAgentMessageEnvelope(data) {
|
|
5782
|
-
const d = data;
|
|
5783
|
-
if (typeof d?.jobId !== "string" || !d.jobId) {
|
|
5784
|
-
return null;
|
|
5785
|
-
}
|
|
5786
|
-
return {
|
|
5787
|
-
jobId: d.jobId,
|
|
5788
|
-
...typeof d.turnId === "string" && d.turnId.trim() ? { turnId: d.turnId.trim() } : {},
|
|
5789
|
-
message: {
|
|
5790
|
-
messageId: d.messageId,
|
|
5791
|
-
kind: d.kind === "artifacts" ? "artifacts" : "text",
|
|
5792
|
-
reply: typeof d.reply === "string" ? d.reply : "",
|
|
5793
|
-
show: d.show,
|
|
5794
|
-
actions: Array.isArray(d.actions) ? d.actions : void 0,
|
|
5795
|
-
timestamp: d.timestamp || Date.now()
|
|
5796
|
-
}
|
|
5797
|
-
};
|
|
5798
|
-
}
|
|
5799
7509
|
var JobImplementation = class {
|
|
5800
7510
|
id;
|
|
5801
7511
|
client;
|
|
@@ -5804,8 +7514,6 @@ var JobImplementation = class {
|
|
|
5804
7514
|
_resolveResult;
|
|
5805
7515
|
_rejectResult;
|
|
5806
7516
|
eventListeners = /* @__PURE__ */ new Map();
|
|
5807
|
-
bufferedAgentMessages = [];
|
|
5808
|
-
bufferedAgentMessageIds = /* @__PURE__ */ new Set();
|
|
5809
7517
|
resultSettled = false;
|
|
5810
7518
|
metadata;
|
|
5811
7519
|
constructor(id, client, initialState) {
|
|
@@ -5964,439 +7672,119 @@ var JobImplementation = class {
|
|
|
5964
7672
|
timestamp: d.timestamp
|
|
5965
7673
|
});
|
|
5966
7674
|
}
|
|
5967
|
-
});
|
|
5968
|
-
this.client.on("job.agent_message", (data) => {
|
|
5969
|
-
const normalized = normalizeJobAgentMessageEnvelope(data);
|
|
5970
|
-
if (normalized?.jobId === id) {
|
|
5971
|
-
this.captureAgentMessage(normalized.message);
|
|
5972
|
-
}
|
|
5973
|
-
});
|
|
5974
|
-
}
|
|
5975
|
-
get result() {
|
|
5976
|
-
return this._resultPromise;
|
|
5977
|
-
}
|
|
5978
|
-
async leaveFeedback(input) {
|
|
5979
|
-
const sentiment = input.sentiment;
|
|
5980
|
-
if (sentiment !== "good" && sentiment !== "bad") {
|
|
5981
|
-
throw new Error('Job feedback sentiment must be "good" or "bad".');
|
|
5982
|
-
}
|
|
5983
|
-
const comment = typeof input.comment === "string" ? input.comment.trim() : "";
|
|
5984
|
-
const response = await this.client.call("job.feedback", {
|
|
5985
|
-
jobId: this.id,
|
|
5986
|
-
sentiment,
|
|
5987
|
-
comment: comment || void 0,
|
|
5988
|
-
metadata: this.buildFeedbackMetadata()
|
|
5989
|
-
});
|
|
5990
|
-
this.emit("feedback", response);
|
|
5991
|
-
return response;
|
|
5992
|
-
}
|
|
5993
|
-
on(event, handler) {
|
|
5994
|
-
if (!this.eventListeners.has(event)) {
|
|
5995
|
-
this.eventListeners.set(event, []);
|
|
5996
|
-
}
|
|
5997
|
-
this.eventListeners.get(event).push(handler);
|
|
5998
|
-
if (event === "agentMessage" && this.bufferedAgentMessages.length > 0) {
|
|
5999
|
-
for (const message of this.bufferedAgentMessages) {
|
|
6000
|
-
handler(message);
|
|
6001
|
-
}
|
|
6002
|
-
}
|
|
6003
|
-
return () => {
|
|
6004
|
-
const handlers = this.eventListeners.get(event);
|
|
6005
|
-
if (!handlers) {
|
|
6006
|
-
return;
|
|
6007
|
-
}
|
|
6008
|
-
this.eventListeners.set(
|
|
6009
|
-
event,
|
|
6010
|
-
handlers.filter((current) => current !== handler)
|
|
6011
|
-
);
|
|
6012
|
-
};
|
|
6013
|
-
}
|
|
6014
|
-
replayAgentMessage(message) {
|
|
6015
|
-
this.captureAgentMessage(message);
|
|
6016
|
-
}
|
|
6017
|
-
buildFeedbackMetadata() {
|
|
6018
|
-
const startedAt = this.metadata.startedAt;
|
|
6019
|
-
const completedAt = this.metadata.completedAt;
|
|
6020
|
-
return {
|
|
6021
|
-
...this.metadata,
|
|
6022
|
-
durationMs: typeof startedAt === "number" && typeof completedAt === "number" ? completedAt - startedAt : this.metadata.durationMs ?? null,
|
|
6023
|
-
stdout: [...this.metadata.stdout],
|
|
6024
|
-
stderr: [...this.metadata.stderr],
|
|
6025
|
-
toolCalls: this.metadata.toolCalls.slice(0, MAX_FEEDBACK_TOOL_CALLS).map((call) => ({ ...call }))
|
|
6026
|
-
};
|
|
6027
|
-
}
|
|
6028
|
-
markStarted(timestamp = Date.now()) {
|
|
6029
|
-
if (!this.metadata.startedAt) {
|
|
6030
|
-
this.metadata.startedAt = timestamp;
|
|
6031
|
-
}
|
|
6032
|
-
if (this.status === "queued") {
|
|
6033
|
-
this.status = "running";
|
|
6034
|
-
}
|
|
6035
|
-
if (this.metadata.status === "queued") {
|
|
6036
|
-
this.metadata.status = "running";
|
|
6037
|
-
}
|
|
6038
|
-
}
|
|
6039
|
-
finalize(status, result, error, options = {}) {
|
|
6040
|
-
if (!this.metadata.startedAt) {
|
|
6041
|
-
this.metadata.startedAt = Date.now();
|
|
6042
|
-
}
|
|
6043
|
-
this.status = status;
|
|
6044
|
-
this.metadata.status = status;
|
|
6045
|
-
this.metadata.completedAt = this.metadata.completedAt || Date.now();
|
|
6046
|
-
this.metadata.durationMs = this.metadata.completedAt - this.metadata.startedAt;
|
|
6047
|
-
if (!this.resultSettled && (options.hasResult || result !== void 0)) {
|
|
6048
|
-
this.metadata.result = sanitizeFeedbackValue(result);
|
|
6049
|
-
this.resultSettled = true;
|
|
6050
|
-
this._resolveResult(result);
|
|
6051
|
-
}
|
|
6052
|
-
if (!this.resultSettled && (error !== void 0 || status === "failed" || status === "timeout" || status === "canceled")) {
|
|
6053
|
-
const fallbackError = new Error(`Job ${this.id} ${status}.`);
|
|
6054
|
-
const cause = error ?? fallbackError;
|
|
6055
|
-
const message = cause instanceof Error ? cause.message : String(cause);
|
|
6056
|
-
this.metadata.error = truncateFeedbackString(message);
|
|
6057
|
-
this.resultSettled = true;
|
|
6058
|
-
this._rejectResult(cause);
|
|
6059
|
-
}
|
|
6060
|
-
}
|
|
6061
|
-
upsertToolCall(next) {
|
|
6062
|
-
const callId = next.callId || `tool-call-${Date.now()}`;
|
|
6063
|
-
const existingIndex = this.metadata.toolCalls.findIndex(
|
|
6064
|
-
(entry) => entry.callId === callId
|
|
6065
|
-
);
|
|
6066
|
-
const existing = existingIndex >= 0 ? this.metadata.toolCalls[existingIndex] : void 0;
|
|
6067
|
-
const merged = {
|
|
6068
|
-
...existing,
|
|
6069
|
-
...next,
|
|
6070
|
-
callId,
|
|
6071
|
-
toolName: next.toolName || existing?.toolName
|
|
6072
|
-
};
|
|
6073
|
-
if (merged.durationMs === void 0 && typeof merged.startedAt === "number" && typeof merged.completedAt === "number") {
|
|
6074
|
-
merged.durationMs = merged.completedAt - merged.startedAt;
|
|
6075
|
-
}
|
|
6076
|
-
if (existingIndex >= 0) {
|
|
6077
|
-
const updated = [...this.metadata.toolCalls];
|
|
6078
|
-
updated[existingIndex] = merged;
|
|
6079
|
-
return updated.slice(-MAX_FEEDBACK_TOOL_CALLS);
|
|
6080
|
-
}
|
|
6081
|
-
return [...this.metadata.toolCalls, merged].slice(-MAX_FEEDBACK_TOOL_CALLS);
|
|
6082
|
-
}
|
|
6083
|
-
emit(event, data) {
|
|
6084
|
-
const handlers = this.eventListeners.get(event);
|
|
6085
|
-
if (handlers) {
|
|
6086
|
-
handlers.forEach((h) => h(data));
|
|
6087
|
-
}
|
|
6088
|
-
}
|
|
6089
|
-
captureAgentMessage(message) {
|
|
6090
|
-
if (message.messageId && this.bufferedAgentMessageIds.has(message.messageId)) {
|
|
6091
|
-
return;
|
|
6092
|
-
}
|
|
6093
|
-
if (message.messageId) {
|
|
6094
|
-
this.bufferedAgentMessageIds.add(message.messageId);
|
|
6095
|
-
}
|
|
6096
|
-
this.bufferedAgentMessages = [...this.bufferedAgentMessages, message].slice(
|
|
6097
|
-
-25
|
|
6098
|
-
);
|
|
6099
|
-
this.emit("agentMessage", message);
|
|
6100
|
-
}
|
|
6101
|
-
};
|
|
6102
|
-
|
|
6103
|
-
// src/job-presentation.ts
|
|
6104
|
-
var RESPONSE_KEYS = [
|
|
6105
|
-
"reply",
|
|
6106
|
-
"response",
|
|
6107
|
-
"text",
|
|
6108
|
-
"message",
|
|
6109
|
-
"summary",
|
|
6110
|
-
"answer"
|
|
6111
|
-
];
|
|
6112
|
-
var ENTRY_KEY_CANDIDATES = ["entryPath", "path"];
|
|
6113
|
-
var ENTRY_ARRAY_KEY_CANDIDATES = ["entryPaths", "paths"];
|
|
6114
|
-
var LIST_KEY_CANDIDATES = ["listName"];
|
|
6115
|
-
var LIST_ARRAY_KEY_CANDIDATES = ["listNames"];
|
|
6116
|
-
var VARIABLE_KEY_CANDIDATES = ["variableName"];
|
|
6117
|
-
var VARIABLE_ARRAY_KEY_CANDIDATES = ["variableNames"];
|
|
6118
|
-
var UI_CONTAINER_KEYS = ["show", "display", "present", "ui"];
|
|
6119
|
-
function asRecord2(value) {
|
|
6120
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
6121
|
-
return value;
|
|
6122
|
-
}
|
|
6123
|
-
function normalizeText(value) {
|
|
6124
|
-
if (typeof value !== "string") return null;
|
|
6125
|
-
const trimmed = value.trim();
|
|
6126
|
-
if (!trimmed) return null;
|
|
6127
|
-
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
6128
|
-
return null;
|
|
6129
|
-
}
|
|
6130
|
-
return trimmed;
|
|
6131
|
-
}
|
|
6132
|
-
function humanTextFromStdout(stdout) {
|
|
6133
|
-
for (const line of [...stdout].reverse()) {
|
|
6134
|
-
const normalized = normalizeText(line);
|
|
6135
|
-
if (!normalized) continue;
|
|
6136
|
-
if (/^[A-Z_]+:/.test(normalized)) continue;
|
|
6137
|
-
return normalized;
|
|
6138
|
-
}
|
|
6139
|
-
return null;
|
|
6140
|
-
}
|
|
6141
|
-
function responseTextFromAgentMessages(agentMessages) {
|
|
6142
|
-
for (const message of [...agentMessages].reverse()) {
|
|
6143
|
-
const record = asRecord2(message);
|
|
6144
|
-
if (!record) continue;
|
|
6145
|
-
for (const key of RESPONSE_KEYS) {
|
|
6146
|
-
const normalized = normalizeText(record[key]);
|
|
6147
|
-
if (normalized) return normalized;
|
|
6148
|
-
}
|
|
6149
|
-
}
|
|
6150
|
-
return null;
|
|
6151
|
-
}
|
|
6152
|
-
function pushString(target, value) {
|
|
6153
|
-
if (typeof value === "string" && value.trim()) {
|
|
6154
|
-
target.add(value.trim());
|
|
6155
|
-
}
|
|
6156
|
-
}
|
|
6157
|
-
function pushStringArray(target, value) {
|
|
6158
|
-
if (!Array.isArray(value)) return;
|
|
6159
|
-
for (const item of value) {
|
|
6160
|
-
pushString(target, item);
|
|
6161
|
-
}
|
|
6162
|
-
}
|
|
6163
|
-
function collectReferencesFromRecord(record, refs) {
|
|
6164
|
-
for (const key of ENTRY_KEY_CANDIDATES)
|
|
6165
|
-
pushString(refs.entryPaths, record[key]);
|
|
6166
|
-
for (const key of ENTRY_ARRAY_KEY_CANDIDATES)
|
|
6167
|
-
pushStringArray(refs.entryPaths, record[key]);
|
|
6168
|
-
for (const key of LIST_KEY_CANDIDATES)
|
|
6169
|
-
pushString(refs.listNames, record[key]);
|
|
6170
|
-
for (const key of LIST_ARRAY_KEY_CANDIDATES)
|
|
6171
|
-
pushStringArray(refs.listNames, record[key]);
|
|
6172
|
-
for (const key of VARIABLE_KEY_CANDIDATES)
|
|
6173
|
-
pushString(refs.variableNames, record[key]);
|
|
6174
|
-
for (const key of VARIABLE_ARRAY_KEY_CANDIDATES)
|
|
6175
|
-
pushStringArray(refs.variableNames, record[key]);
|
|
6176
|
-
}
|
|
6177
|
-
function stringValue(record, keys) {
|
|
6178
|
-
for (const key of keys) {
|
|
6179
|
-
const value = record[key];
|
|
6180
|
-
if (typeof value === "string" && value.trim()) {
|
|
6181
|
-
return value.trim();
|
|
6182
|
-
}
|
|
6183
|
-
}
|
|
6184
|
-
return null;
|
|
6185
|
-
}
|
|
6186
|
-
function findEntryPathForRecord(record, heap) {
|
|
6187
|
-
const directPath = stringValue(record, ["entryPath", "path"]);
|
|
6188
|
-
if (directPath && heap.entriesByPath?.[directPath]) {
|
|
6189
|
-
return directPath;
|
|
6190
|
-
}
|
|
6191
|
-
const id = stringValue(record, ["id", "_id", "recordId", "objectId"]);
|
|
6192
|
-
if (!id) {
|
|
6193
|
-
return null;
|
|
6194
|
-
}
|
|
6195
|
-
const className = stringValue(record, [
|
|
6196
|
-
"className",
|
|
6197
|
-
"_className",
|
|
6198
|
-
"__className",
|
|
6199
|
-
"prototype",
|
|
6200
|
-
"type"
|
|
6201
|
-
]);
|
|
6202
|
-
const entries = Object.values(heap.entriesByPath || {});
|
|
6203
|
-
const exact = entries.find(
|
|
6204
|
-
(entry) => entry.id === id && (!className || entry.className === className || entry.prototypes?.includes(className))
|
|
6205
|
-
);
|
|
6206
|
-
if (exact?.path) {
|
|
6207
|
-
return exact.path;
|
|
6208
|
-
}
|
|
6209
|
-
const idOnlyMatches = entries.filter((entry) => entry.id === id);
|
|
6210
|
-
return idOnlyMatches.length === 1 ? idOnlyMatches[0].path : null;
|
|
6211
|
-
}
|
|
6212
|
-
function scanForHeapReferences(value, heap, refs, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
6213
|
-
if (value === null || value === void 0 || depth > 4 || seen.has(value))
|
|
6214
|
-
return;
|
|
6215
|
-
if (typeof value === "string") {
|
|
6216
|
-
const trimmed = value.trim();
|
|
6217
|
-
if (heap.entriesByPath?.[trimmed]) refs.entryPaths.add(trimmed);
|
|
6218
|
-
if (heap.listsByName?.[trimmed]) refs.listNames.add(trimmed);
|
|
6219
|
-
if (heap.variablesByName?.[trimmed]) refs.variableNames.add(trimmed);
|
|
6220
|
-
return;
|
|
7675
|
+
});
|
|
6221
7676
|
}
|
|
6222
|
-
|
|
6223
|
-
|
|
6224
|
-
|
|
6225
|
-
|
|
7677
|
+
get result() {
|
|
7678
|
+
return this._resultPromise;
|
|
7679
|
+
}
|
|
7680
|
+
async leaveFeedback(input) {
|
|
7681
|
+
const sentiment = input.sentiment;
|
|
7682
|
+
if (sentiment !== "good" && sentiment !== "bad") {
|
|
7683
|
+
throw new Error('Job feedback sentiment must be "good" or "bad".');
|
|
6226
7684
|
}
|
|
6227
|
-
|
|
7685
|
+
const comment = typeof input.comment === "string" ? input.comment.trim() : "";
|
|
7686
|
+
const response = await this.client.call("job.feedback", {
|
|
7687
|
+
jobId: this.id,
|
|
7688
|
+
sentiment,
|
|
7689
|
+
comment: comment || void 0,
|
|
7690
|
+
metadata: this.buildFeedbackMetadata()
|
|
7691
|
+
});
|
|
7692
|
+
this.emit("feedback", response);
|
|
7693
|
+
return response;
|
|
6228
7694
|
}
|
|
6229
|
-
|
|
6230
|
-
|
|
6231
|
-
|
|
6232
|
-
|
|
6233
|
-
|
|
6234
|
-
|
|
6235
|
-
|
|
6236
|
-
|
|
6237
|
-
|
|
7695
|
+
on(event, handler) {
|
|
7696
|
+
if (!this.eventListeners.has(event)) {
|
|
7697
|
+
this.eventListeners.set(event, []);
|
|
7698
|
+
}
|
|
7699
|
+
this.eventListeners.get(event).push(handler);
|
|
7700
|
+
return () => {
|
|
7701
|
+
const handlers = this.eventListeners.get(event);
|
|
7702
|
+
if (!handlers) {
|
|
7703
|
+
return;
|
|
7704
|
+
}
|
|
7705
|
+
this.eventListeners.set(
|
|
7706
|
+
event,
|
|
7707
|
+
handlers.filter((current) => current !== handler)
|
|
7708
|
+
);
|
|
7709
|
+
};
|
|
6238
7710
|
}
|
|
6239
|
-
|
|
6240
|
-
|
|
7711
|
+
buildFeedbackMetadata() {
|
|
7712
|
+
const startedAt = this.metadata.startedAt;
|
|
7713
|
+
const completedAt = this.metadata.completedAt;
|
|
7714
|
+
return {
|
|
7715
|
+
...this.metadata,
|
|
7716
|
+
durationMs: typeof startedAt === "number" && typeof completedAt === "number" ? completedAt - startedAt : this.metadata.durationMs ?? null,
|
|
7717
|
+
stdout: [...this.metadata.stdout],
|
|
7718
|
+
stderr: [...this.metadata.stderr],
|
|
7719
|
+
toolCalls: this.metadata.toolCalls.slice(0, MAX_FEEDBACK_TOOL_CALLS).map((call) => ({ ...call }))
|
|
7720
|
+
};
|
|
6241
7721
|
}
|
|
6242
|
-
|
|
6243
|
-
|
|
6244
|
-
|
|
6245
|
-
const variable = heap.variablesByName?.[variableName];
|
|
6246
|
-
if (!variable) continue;
|
|
6247
|
-
if (variable.kind === "entry" && variable.entryPath) {
|
|
6248
|
-
refs.entryPaths.add(variable.entryPath);
|
|
7722
|
+
markStarted(timestamp = Date.now()) {
|
|
7723
|
+
if (!this.metadata.startedAt) {
|
|
7724
|
+
this.metadata.startedAt = timestamp;
|
|
6249
7725
|
}
|
|
6250
|
-
if (
|
|
6251
|
-
|
|
7726
|
+
if (this.status === "queued") {
|
|
7727
|
+
this.status = "running";
|
|
7728
|
+
}
|
|
7729
|
+
if (this.metadata.status === "queued") {
|
|
7730
|
+
this.metadata.status = "running";
|
|
6252
7731
|
}
|
|
6253
7732
|
}
|
|
6254
|
-
}
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
}
|
|
6258
|
-
function sortLists(lists) {
|
|
6259
|
-
return [...lists].sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
6260
|
-
}
|
|
6261
|
-
function dedupeEntries(entries) {
|
|
6262
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6263
|
-
const result = [];
|
|
6264
|
-
for (const entry of entries) {
|
|
6265
|
-
if (!entry?.path || seen.has(entry.path)) continue;
|
|
6266
|
-
seen.add(entry.path);
|
|
6267
|
-
result.push(entry);
|
|
6268
|
-
}
|
|
6269
|
-
return result;
|
|
6270
|
-
}
|
|
6271
|
-
function dedupeLists(lists) {
|
|
6272
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6273
|
-
const result = [];
|
|
6274
|
-
for (const list of lists) {
|
|
6275
|
-
if (!list?.name || seen.has(list.name)) continue;
|
|
6276
|
-
seen.add(list.name);
|
|
6277
|
-
result.push(list);
|
|
6278
|
-
}
|
|
6279
|
-
return result;
|
|
6280
|
-
}
|
|
6281
|
-
function extractResponseText(result, stdout) {
|
|
6282
|
-
const directText = normalizeText(result);
|
|
6283
|
-
if (directText) return directText;
|
|
6284
|
-
const record = asRecord2(result);
|
|
6285
|
-
if (record) {
|
|
6286
|
-
for (const key of RESPONSE_KEYS) {
|
|
6287
|
-
const normalized = normalizeText(record[key]);
|
|
6288
|
-
if (normalized) return normalized;
|
|
7733
|
+
finalize(status, result, error, options = {}) {
|
|
7734
|
+
if (!this.metadata.startedAt) {
|
|
7735
|
+
this.metadata.startedAt = Date.now();
|
|
6289
7736
|
}
|
|
6290
|
-
|
|
6291
|
-
|
|
6292
|
-
|
|
6293
|
-
|
|
6294
|
-
|
|
6295
|
-
|
|
6296
|
-
|
|
7737
|
+
this.status = status;
|
|
7738
|
+
this.metadata.status = status;
|
|
7739
|
+
this.metadata.completedAt = this.metadata.completedAt || Date.now();
|
|
7740
|
+
this.metadata.durationMs = this.metadata.completedAt - this.metadata.startedAt;
|
|
7741
|
+
if (!this.resultSettled && (options.hasResult || result !== void 0)) {
|
|
7742
|
+
this.metadata.result = sanitizeFeedbackValue(result);
|
|
7743
|
+
this.resultSettled = true;
|
|
7744
|
+
this._resolveResult(result);
|
|
6297
7745
|
}
|
|
6298
|
-
|
|
6299
|
-
|
|
6300
|
-
|
|
6301
|
-
|
|
6302
|
-
|
|
6303
|
-
|
|
6304
|
-
|
|
6305
|
-
if (lists.length > 0) {
|
|
6306
|
-
const emptyOnly = lists.every((list) => (list.paths || []).length === 0);
|
|
6307
|
-
if (emptyOnly) {
|
|
6308
|
-
return lists.length === 1 ? "I saved one empty result set." : `I saved ${lists.length} empty result sets.`;
|
|
7746
|
+
if (!this.resultSettled && (error !== void 0 || status === "failed" || status === "timeout" || status === "canceled")) {
|
|
7747
|
+
const fallbackError = new Error(`Job ${this.id} ${status}.`);
|
|
7748
|
+
const cause = error ?? fallbackError;
|
|
7749
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
7750
|
+
this.metadata.error = truncateFeedbackString(message);
|
|
7751
|
+
this.resultSettled = true;
|
|
7752
|
+
this._rejectResult(cause);
|
|
6309
7753
|
}
|
|
6310
|
-
return lists.length === 1 ? "I saved one result set." : `I saved ${lists.length} result sets.`;
|
|
6311
7754
|
}
|
|
6312
|
-
|
|
6313
|
-
}
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
6319
|
-
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
|
|
6328
|
-
|
|
6329
|
-
|
|
6330
|
-
|
|
6331
|
-
for (const path of list.paths || []) {
|
|
6332
|
-
const entry = heap.entriesByPath?.[path];
|
|
6333
|
-
if (entry) entries.push(entry);
|
|
7755
|
+
upsertToolCall(next) {
|
|
7756
|
+
const callId = next.callId || `tool-call-${Date.now()}`;
|
|
7757
|
+
const existingIndex = this.metadata.toolCalls.findIndex(
|
|
7758
|
+
(entry) => entry.callId === callId
|
|
7759
|
+
);
|
|
7760
|
+
const existing = existingIndex >= 0 ? this.metadata.toolCalls[existingIndex] : void 0;
|
|
7761
|
+
const merged = {
|
|
7762
|
+
...existing,
|
|
7763
|
+
...next,
|
|
7764
|
+
callId,
|
|
7765
|
+
toolName: next.toolName || existing?.toolName
|
|
7766
|
+
};
|
|
7767
|
+
if (merged.durationMs === void 0 && typeof merged.startedAt === "number" && typeof merged.completedAt === "number") {
|
|
7768
|
+
merged.durationMs = merged.completedAt - merged.startedAt;
|
|
7769
|
+
}
|
|
7770
|
+
if (existingIndex >= 0) {
|
|
7771
|
+
const updated = [...this.metadata.toolCalls];
|
|
7772
|
+
updated[existingIndex] = merged;
|
|
7773
|
+
return updated.slice(-MAX_FEEDBACK_TOOL_CALLS);
|
|
6334
7774
|
}
|
|
7775
|
+
return [...this.metadata.toolCalls, merged].slice(-MAX_FEEDBACK_TOOL_CALLS);
|
|
6335
7776
|
}
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
stdout = [],
|
|
6342
|
-
agentMessages = [],
|
|
6343
|
-
sessionHeap,
|
|
6344
|
-
allowExplicitArtifacts = true
|
|
6345
|
-
}) {
|
|
6346
|
-
const refs = {
|
|
6347
|
-
entryPaths: /* @__PURE__ */ new Set(),
|
|
6348
|
-
listNames: /* @__PURE__ */ new Set(),
|
|
6349
|
-
variableNames: /* @__PURE__ */ new Set()
|
|
6350
|
-
};
|
|
6351
|
-
if (allowExplicitArtifacts) {
|
|
6352
|
-
scanForHeapReferences(result, sessionHeap, refs);
|
|
6353
|
-
resolveVariablesToReferences(refs.variableNames, sessionHeap, refs);
|
|
7777
|
+
emit(event, data) {
|
|
7778
|
+
const handlers = this.eventListeners.get(event);
|
|
7779
|
+
if (handlers) {
|
|
7780
|
+
handlers.forEach((h) => h(data));
|
|
7781
|
+
}
|
|
6354
7782
|
}
|
|
6355
|
-
|
|
6356
|
-
[...refs.listNames].map((name) => sessionHeap.listsByName?.[name]).filter((list) => Boolean(list))
|
|
6357
|
-
);
|
|
6358
|
-
const referencedEntries = sortEntries(
|
|
6359
|
-
[...refs.entryPaths].map((path) => sessionHeap.entriesByPath?.[path]).filter((entry) => Boolean(entry))
|
|
6360
|
-
);
|
|
6361
|
-
const jobLists = getJobRelatedLists(sessionHeap, jobId);
|
|
6362
|
-
const jobEntries = getJobRelatedEntries(sessionHeap, jobId);
|
|
6363
|
-
const changedEntries = dedupeEntries([
|
|
6364
|
-
...jobEntries,
|
|
6365
|
-
...entriesFromLists(jobLists, sessionHeap)
|
|
6366
|
-
]);
|
|
6367
|
-
const explicitLists = dedupeLists(referencedLists);
|
|
6368
|
-
const explicitEntries = dedupeEntries([
|
|
6369
|
-
...referencedEntries,
|
|
6370
|
-
...entriesFromLists(referencedLists, sessionHeap)
|
|
6371
|
-
]);
|
|
6372
|
-
const hasExplicitArtifacts = allowExplicitArtifacts && (explicitEntries.length > 0 || explicitLists.length > 0);
|
|
6373
|
-
const lists = hasExplicitArtifacts ? explicitLists : jobLists;
|
|
6374
|
-
const entries = hasExplicitArtifacts ? explicitEntries : changedEntries;
|
|
6375
|
-
const responseText = extractResponseText(result, stdout) || responseTextFromAgentMessages(agentMessages) || fallbackResponseText(entries, lists);
|
|
6376
|
-
return {
|
|
6377
|
-
responseText,
|
|
6378
|
-
entries,
|
|
6379
|
-
lists,
|
|
6380
|
-
changedEntries,
|
|
6381
|
-
changedLists: jobLists,
|
|
6382
|
-
hasExplicitArtifacts
|
|
6383
|
-
};
|
|
6384
|
-
}
|
|
7783
|
+
};
|
|
6385
7784
|
|
|
6386
7785
|
// src/session-transcript.ts
|
|
6387
|
-
var EMPTY_HEAP = {
|
|
6388
|
-
entriesByPath: {},
|
|
6389
|
-
listsByName: {},
|
|
6390
|
-
variablesByName: {}};
|
|
6391
7786
|
function asRecord3(value) {
|
|
6392
|
-
|
|
6393
|
-
return value;
|
|
6394
|
-
}
|
|
6395
|
-
function asArray(value) {
|
|
6396
|
-
return Array.isArray(value) ? value : [];
|
|
6397
|
-
}
|
|
6398
|
-
function asNumber(value) {
|
|
6399
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
7787
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
6400
7788
|
}
|
|
6401
7789
|
function asString(value) {
|
|
6402
7790
|
return typeof value === "string" ? value : void 0;
|
|
@@ -6417,142 +7805,17 @@ function compactJson(value, maxLength = 320) {
|
|
|
6417
7805
|
function artifactRecordsById(liveDoc) {
|
|
6418
7806
|
const artifacts = asRecord3(liveDoc?.artifacts);
|
|
6419
7807
|
const byId = asRecord3(artifacts?.byId) || {};
|
|
6420
|
-
return Object.fromEntries(
|
|
6421
|
-
Object.entries(byId).map(([artifactId, value]) => {
|
|
6422
|
-
const record = asRecord3(value);
|
|
6423
|
-
return record ? [artifactId, record] : null;
|
|
6424
|
-
}).filter(
|
|
6425
|
-
|
|
6426
|
-
)
|
|
6427
|
-
);
|
|
6428
|
-
}
|
|
6429
|
-
function normalizeShowRefs(value) {
|
|
6430
|
-
const record = asRecord3(value);
|
|
6431
|
-
if (!record) return void 0;
|
|
6432
|
-
const normalizeRefs = (input) => {
|
|
6433
|
-
if (!Array.isArray(input)) return void 0;
|
|
6434
|
-
const refs = Array.from(
|
|
6435
|
-
new Set(
|
|
6436
|
-
input.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean)
|
|
6437
|
-
)
|
|
6438
|
-
);
|
|
6439
|
-
return refs.length > 0 ? refs : void 0;
|
|
6440
|
-
};
|
|
6441
|
-
const show = {
|
|
6442
|
-
entryPaths: normalizeRefs(record.entryPaths),
|
|
6443
|
-
listNames: normalizeRefs(record.listNames),
|
|
6444
|
-
variableNames: normalizeRefs(record.variableNames),
|
|
6445
|
-
fileIds: normalizeRefs(record.fileIds),
|
|
6446
|
-
sessionArtifactIds: normalizeRefs(record.sessionArtifactIds),
|
|
6447
|
-
actionSuggestions: normalizeActionSuggestions(record.actionSuggestions),
|
|
6448
|
-
tables: Array.isArray(record.tables) ? record.tables.filter(
|
|
6449
|
-
(table) => Boolean(
|
|
6450
|
-
table && typeof table === "object" && !Array.isArray(table) && Array.isArray(table.columns) && Array.isArray(table.rows)
|
|
6451
|
-
)
|
|
6452
|
-
) : void 0
|
|
6453
|
-
};
|
|
6454
|
-
return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions || show.tables ? show : void 0;
|
|
6455
|
-
}
|
|
6456
|
-
function normalizeActionSuggestions(value) {
|
|
6457
|
-
if (!Array.isArray(value)) return void 0;
|
|
6458
|
-
const suggestions = [];
|
|
6459
|
-
for (const item of value) {
|
|
6460
|
-
const record = asRecord3(item);
|
|
6461
|
-
if (!record) continue;
|
|
6462
|
-
const label = trimString(record.label);
|
|
6463
|
-
if (!label) continue;
|
|
6464
|
-
const suggestionId = trimString(record.suggestionId) || trimString(record.id) || label;
|
|
6465
|
-
suggestions.push({
|
|
6466
|
-
suggestionId,
|
|
6467
|
-
label,
|
|
6468
|
-
...typeof record.description === "string" ? { description: record.description } : {},
|
|
6469
|
-
...asRecord3(record.artifact) ? { artifact: asRecord3(record.artifact) } : {},
|
|
6470
|
-
...asRecord3(record.target) ? { target: asRecord3(record.target) } : {},
|
|
6471
|
-
...asRecord3(record.metadata) ? { metadata: asRecord3(record.metadata) } : {}
|
|
6472
|
-
});
|
|
6473
|
-
}
|
|
6474
|
-
return suggestions.length ? suggestions : void 0;
|
|
6475
|
-
}
|
|
6476
|
-
var TRANSCRIPT_MESSAGE_PART_LIMIT = 128;
|
|
6477
|
-
var TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT = 2e5;
|
|
6478
|
-
var TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT = 1e3;
|
|
6479
|
-
var TRANSCRIPT_MESSAGE_ACTION_LIMIT = 64;
|
|
6480
|
-
function normalizeConversationMessageActions(value) {
|
|
6481
|
-
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
6482
|
-
const actions = [];
|
|
6483
|
-
for (const item of value.slice(0, TRANSCRIPT_MESSAGE_ACTION_LIMIT)) {
|
|
6484
|
-
const record = asRecord3(item);
|
|
6485
|
-
const kind = record?.kind;
|
|
6486
|
-
const label = trimString(record?.label ?? record?.title);
|
|
6487
|
-
const status = record?.status;
|
|
6488
|
-
if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
|
|
6489
|
-
continue;
|
|
6490
|
-
}
|
|
6491
|
-
actions.push({
|
|
6492
|
-
kind,
|
|
6493
|
-
label,
|
|
6494
|
-
...status === "done" || status === "queued" || status === "failed" ? { status } : {}
|
|
6495
|
-
});
|
|
6496
|
-
}
|
|
6497
|
-
return actions.length ? actions : void 0;
|
|
6498
|
-
}
|
|
6499
|
-
function normalizeConversationMessageParts(value, canonicalContent, canonicalActions) {
|
|
6500
|
-
if (!Array.isArray(value) || value.length === 0 || value.length > TRANSCRIPT_MESSAGE_PART_LIMIT) {
|
|
6501
|
-
return void 0;
|
|
6502
|
-
}
|
|
6503
|
-
const parts = [];
|
|
6504
|
-
const canonicalActionsById = new Map(
|
|
6505
|
-
(canonicalActions || []).map((action) => [
|
|
6506
|
-
`${action.kind}:${action.label}`,
|
|
6507
|
-
action
|
|
6508
|
-
])
|
|
6509
|
-
);
|
|
6510
|
-
const seenActionIds = /* @__PURE__ */ new Set();
|
|
6511
|
-
let textLength = 0;
|
|
6512
|
-
for (const item of value) {
|
|
6513
|
-
const record = asRecord3(item);
|
|
6514
|
-
if (!record) return void 0;
|
|
6515
|
-
if (record.type === "text") {
|
|
6516
|
-
if (typeof record.text !== "string" || record.text.length === 0) {
|
|
6517
|
-
return void 0;
|
|
6518
|
-
}
|
|
6519
|
-
textLength += record.text.length;
|
|
6520
|
-
if (textLength > TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT) return void 0;
|
|
6521
|
-
parts.push({ type: "text", text: record.text });
|
|
6522
|
-
continue;
|
|
6523
|
-
}
|
|
6524
|
-
if (record.type !== "action") return void 0;
|
|
6525
|
-
const action = asRecord3(record.action);
|
|
6526
|
-
const kind = action?.kind;
|
|
6527
|
-
const label = trimString(action?.label);
|
|
6528
|
-
if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
|
|
6529
|
-
return void 0;
|
|
6530
|
-
}
|
|
6531
|
-
const actionId = `${kind}:${label}`;
|
|
6532
|
-
const canonicalAction = canonicalActionsById.get(actionId);
|
|
6533
|
-
if (!canonicalAction) return void 0;
|
|
6534
|
-
if (seenActionIds.has(actionId)) continue;
|
|
6535
|
-
seenActionIds.add(actionId);
|
|
6536
|
-
parts.push({
|
|
6537
|
-
type: "action",
|
|
6538
|
-
action: canonicalAction
|
|
6539
|
-
});
|
|
6540
|
-
}
|
|
6541
|
-
const orderedText = parts.filter(
|
|
6542
|
-
(part) => part.type === "text"
|
|
6543
|
-
).map((part) => part.text).join("");
|
|
6544
|
-
return orderedText === canonicalContent ? parts : void 0;
|
|
7808
|
+
return Object.fromEntries(
|
|
7809
|
+
Object.entries(byId).map(([artifactId, value]) => {
|
|
7810
|
+
const record = asRecord3(value);
|
|
7811
|
+
return record ? [artifactId, record] : null;
|
|
7812
|
+
}).filter((entry) => Boolean(entry))
|
|
7813
|
+
);
|
|
6545
7814
|
}
|
|
6546
7815
|
function stringifyTranscriptValue(value, fallback = "") {
|
|
6547
|
-
if (typeof value === "string")
|
|
6548
|
-
|
|
6549
|
-
|
|
6550
|
-
if (typeof value === "boolean") {
|
|
6551
|
-
return value ? "Confirmed" : "Canceled";
|
|
6552
|
-
}
|
|
6553
|
-
if (value === void 0) {
|
|
6554
|
-
return fallback;
|
|
6555
|
-
}
|
|
7816
|
+
if (typeof value === "string") return value.trim() || fallback;
|
|
7817
|
+
if (typeof value === "boolean") return value ? "Confirmed" : "Canceled";
|
|
7818
|
+
if (value === void 0) return fallback;
|
|
6556
7819
|
try {
|
|
6557
7820
|
const json = JSON.stringify(value, null, 2);
|
|
6558
7821
|
if (!json || json === "undefined") return fallback;
|
|
@@ -6693,262 +7956,121 @@ ${stringifyTranscriptValue({ show }, "")}`;
|
|
|
6693
7956
|
hasOtherRefs ? `Other shown refs: ${stringifyTranscriptValue(otherRefs, "")}` : null
|
|
6694
7957
|
].filter(Boolean).join("\n");
|
|
6695
7958
|
}
|
|
6696
|
-
function
|
|
6697
|
-
|
|
6698
|
-
|
|
6699
|
-
|
|
6700
|
-
|
|
6701
|
-
|
|
6702
|
-
|
|
6703
|
-
|
|
6704
|
-
|
|
6705
|
-
|
|
6706
|
-
const show = normalizeShowRefs(record.show);
|
|
6707
|
-
const id = asString(record.id) || crypto.randomUUID();
|
|
6708
|
-
const timestamp = asNumber(record.timestamp) || asNumber(record.ts) || 0;
|
|
6709
|
-
if (!content && !show && !actions?.length) return null;
|
|
6710
|
-
const artifactHistory = buildArtifactHistory(show, artifactsById);
|
|
6711
|
-
const historyContent = role === "assistant" ? content && artifactHistory ? `[Assistant reply]
|
|
6712
|
-
${content}
|
|
6713
|
-
|
|
6714
|
-
${artifactHistory}` : content ? `[Assistant reply]
|
|
6715
|
-
${content}` : artifactHistory : void 0;
|
|
6716
|
-
return {
|
|
6717
|
-
id,
|
|
6718
|
-
role,
|
|
6719
|
-
content,
|
|
6720
|
-
timestamp,
|
|
6721
|
-
jobId: asString(record.jobId),
|
|
6722
|
-
promptId: asString(record.promptId),
|
|
6723
|
-
show,
|
|
6724
|
-
actions,
|
|
6725
|
-
parts,
|
|
6726
|
-
historyContent,
|
|
6727
|
-
source: "conversation"
|
|
6728
|
-
};
|
|
6729
|
-
}
|
|
6730
|
-
function normalizePromptEntries(jobId, rawPrompts, conversationPromptIds) {
|
|
6731
|
-
const promptsById = asRecord3(rawPrompts) || {};
|
|
6732
|
-
return Object.values(promptsById).map((value) => asRecord3(value)).filter((value) => Boolean(value)).sort(
|
|
6733
|
-
(left, right) => (asNumber(left.openedAt) || asNumber(left.answeredAt) || 0) - (asNumber(right.openedAt) || asNumber(right.answeredAt) || 0)
|
|
6734
|
-
).flatMap((prompt) => {
|
|
6735
|
-
const promptId = asString(prompt.promptId);
|
|
6736
|
-
if (!promptId || conversationPromptIds.has(promptId)) return [];
|
|
6737
|
-
const title = trimString(prompt.title);
|
|
6738
|
-
const message = trimString(prompt.message);
|
|
6739
|
-
const assistantContent = message || title || "Input required";
|
|
6740
|
-
const openedAt = asNumber(prompt.openedAt) || 0;
|
|
6741
|
-
const answeredAt = asNumber(prompt.answeredAt) || openedAt;
|
|
6742
|
-
const entries = [
|
|
6743
|
-
{
|
|
6744
|
-
id: `prompt:${promptId}:assistant`,
|
|
6745
|
-
role: "assistant",
|
|
6746
|
-
content: assistantContent,
|
|
6747
|
-
timestamp: openedAt,
|
|
6748
|
-
jobId,
|
|
6749
|
-
promptId,
|
|
6750
|
-
historyContent: `[Assistant reply]
|
|
6751
|
-
${assistantContent}`,
|
|
6752
|
-
source: "job_prompt"
|
|
7959
|
+
function feedItemShow(item) {
|
|
7960
|
+
switch (item.kind) {
|
|
7961
|
+
case "objects": {
|
|
7962
|
+
const entryPaths = [];
|
|
7963
|
+
const listNames = [];
|
|
7964
|
+
const variableNames = [];
|
|
7965
|
+
for (const ref of item.payload.refs) {
|
|
7966
|
+
if (ref.type === "entry") entryPaths.push(ref.path);
|
|
7967
|
+
if (ref.type === "list") listNames.push(ref.name);
|
|
7968
|
+
if (ref.type === "variable") variableNames.push(ref.name);
|
|
6753
7969
|
}
|
|
6754
|
-
|
|
6755
|
-
|
|
6756
|
-
|
|
6757
|
-
|
|
6758
|
-
|
|
6759
|
-
content: stringifyTranscriptValue(prompt.answer, ""),
|
|
6760
|
-
timestamp: answeredAt,
|
|
6761
|
-
jobId,
|
|
6762
|
-
promptId,
|
|
6763
|
-
source: "job_prompt"
|
|
6764
|
-
});
|
|
7970
|
+
return {
|
|
7971
|
+
...entryPaths.length ? { entryPaths } : {},
|
|
7972
|
+
...listNames.length ? { listNames } : {},
|
|
7973
|
+
...variableNames.length ? { variableNames } : {}
|
|
7974
|
+
};
|
|
6765
7975
|
}
|
|
6766
|
-
|
|
6767
|
-
|
|
7976
|
+
case "table":
|
|
7977
|
+
return {
|
|
7978
|
+
tables: [
|
|
7979
|
+
{
|
|
7980
|
+
id: item.payload.tableId,
|
|
7981
|
+
label: item.payload.label,
|
|
7982
|
+
columns: item.payload.columns,
|
|
7983
|
+
rows: item.payload.rows
|
|
7984
|
+
}
|
|
7985
|
+
]
|
|
7986
|
+
};
|
|
7987
|
+
case "artifact":
|
|
7988
|
+
return { sessionArtifactIds: [item.payload.artifactId] };
|
|
7989
|
+
case "file":
|
|
7990
|
+
return { fileIds: [item.payload.fileId] };
|
|
7991
|
+
case "action_suggestion":
|
|
7992
|
+
return {
|
|
7993
|
+
actionSuggestions: [
|
|
7994
|
+
{
|
|
7995
|
+
suggestionId: item.payload.suggestionId,
|
|
7996
|
+
label: item.payload.label,
|
|
7997
|
+
description: item.payload.description,
|
|
7998
|
+
target: item.payload.target ? { ...item.payload.target } : void 0,
|
|
7999
|
+
artifact: item.payload.proposedArtifact ? { ...item.payload.proposedArtifact } : void 0
|
|
8000
|
+
}
|
|
8001
|
+
]
|
|
8002
|
+
};
|
|
8003
|
+
default:
|
|
8004
|
+
return void 0;
|
|
8005
|
+
}
|
|
6768
8006
|
}
|
|
6769
|
-
function
|
|
6770
|
-
|
|
6771
|
-
|
|
6772
|
-
|
|
6773
|
-
|
|
6774
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
const entries = [];
|
|
6780
|
-
if (reply) {
|
|
8007
|
+
function buildSessionTranscriptFromFeedItems(input) {
|
|
8008
|
+
const artifactsById = artifactRecordsById(input.liveDoc);
|
|
8009
|
+
const entries = [];
|
|
8010
|
+
for (const item of [...input.items].sort(
|
|
8011
|
+
(left, right) => left.sequence - right.sequence
|
|
8012
|
+
)) {
|
|
8013
|
+
if (item.kind === "feedback") continue;
|
|
8014
|
+
if (item.kind === "message") {
|
|
8015
|
+
if (item.payload.role === "system") continue;
|
|
8016
|
+
const content = item.payload.text;
|
|
6781
8017
|
entries.push({
|
|
6782
|
-
id:
|
|
6783
|
-
role:
|
|
6784
|
-
content
|
|
6785
|
-
timestamp,
|
|
6786
|
-
|
|
6787
|
-
|
|
6788
|
-
|
|
6789
|
-
|
|
8018
|
+
id: item.id,
|
|
8019
|
+
role: item.payload.role,
|
|
8020
|
+
content,
|
|
8021
|
+
timestamp: item.occurredAt,
|
|
8022
|
+
sequence: item.sequence,
|
|
8023
|
+
jobId: item.source?.jobId,
|
|
8024
|
+
promptId: item.payload.inReplyToPromptId,
|
|
8025
|
+
historyContent: item.payload.role === "assistant" ? `[Assistant reply]
|
|
8026
|
+
${content}` : void 0,
|
|
8027
|
+
source: "feed"
|
|
6790
8028
|
});
|
|
8029
|
+
continue;
|
|
6791
8030
|
}
|
|
6792
|
-
if (
|
|
8031
|
+
if (item.kind === "prompt") {
|
|
8032
|
+
const content = item.payload.message || item.payload.title;
|
|
6793
8033
|
entries.push({
|
|
6794
|
-
id:
|
|
8034
|
+
id: item.id,
|
|
6795
8035
|
role: "assistant",
|
|
6796
|
-
content
|
|
6797
|
-
timestamp,
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
|
|
8036
|
+
content,
|
|
8037
|
+
timestamp: item.occurredAt,
|
|
8038
|
+
sequence: item.sequence,
|
|
8039
|
+
jobId: item.source?.jobId,
|
|
8040
|
+
promptId: item.payload.promptId,
|
|
8041
|
+
historyContent: `[Assistant reply]
|
|
8042
|
+
${content}`,
|
|
8043
|
+
source: "feed"
|
|
6802
8044
|
});
|
|
8045
|
+
continue;
|
|
6803
8046
|
}
|
|
6804
|
-
|
|
6805
|
-
|
|
6806
|
-
}
|
|
6807
|
-
function buildJobFallbackEntries(jobId, job, sessionHeap, artifactsById) {
|
|
6808
|
-
const timestamp = asNumber(job.finishedAt) || asNumber(job.startedAt) || asNumber(job.submittedAt) || 0;
|
|
6809
|
-
const resultPreview = stringifyTranscriptValue(
|
|
6810
|
-
job.result,
|
|
6811
|
-
"No job result recorded."
|
|
6812
|
-
);
|
|
6813
|
-
const presentation = resolveJobPresentation({
|
|
6814
|
-
jobId,
|
|
6815
|
-
result: job.result,
|
|
6816
|
-
stdout: [],
|
|
6817
|
-
sessionHeap
|
|
6818
|
-
});
|
|
6819
|
-
const entries = [];
|
|
6820
|
-
const responseText = presentation.responseText || "";
|
|
6821
|
-
if (responseText) {
|
|
6822
|
-
entries.push({
|
|
6823
|
-
id: `job:${jobId}:result-text`,
|
|
6824
|
-
role: "assistant",
|
|
6825
|
-
content: responseText,
|
|
6826
|
-
timestamp,
|
|
6827
|
-
jobId,
|
|
6828
|
-
historyContent: `[Assistant reply]
|
|
6829
|
-
${responseText}`,
|
|
6830
|
-
source: "job_result"
|
|
6831
|
-
});
|
|
6832
|
-
}
|
|
6833
|
-
const show = {
|
|
6834
|
-
entryPaths: presentation.entries.map((entry) => entry.path),
|
|
6835
|
-
listNames: presentation.lists.map((list) => list.name)
|
|
6836
|
-
};
|
|
6837
|
-
if (show.entryPaths && show.entryPaths.length > 0 || show.listNames && show.listNames.length > 0) {
|
|
8047
|
+
const show = feedItemShow(item);
|
|
8048
|
+
if (!show) continue;
|
|
6838
8049
|
entries.push({
|
|
6839
|
-
id:
|
|
8050
|
+
id: item.id,
|
|
6840
8051
|
role: "assistant",
|
|
6841
8052
|
content: "",
|
|
6842
|
-
timestamp,
|
|
6843
|
-
|
|
8053
|
+
timestamp: item.occurredAt,
|
|
8054
|
+
sequence: item.sequence,
|
|
8055
|
+
jobId: item.source?.jobId,
|
|
6844
8056
|
show,
|
|
6845
8057
|
historyContent: buildArtifactHistory(show, artifactsById),
|
|
6846
|
-
source: "
|
|
6847
|
-
});
|
|
6848
|
-
}
|
|
6849
|
-
if (entries.length === 0 && trimString(job.error)) {
|
|
6850
|
-
entries.push({
|
|
6851
|
-
id: `job:${jobId}:result-error`,
|
|
6852
|
-
role: "assistant",
|
|
6853
|
-
content: trimString(job.error),
|
|
6854
|
-
timestamp,
|
|
6855
|
-
jobId,
|
|
6856
|
-
historyContent: `[Assistant reply]
|
|
6857
|
-
${trimString(job.error)}`,
|
|
6858
|
-
source: "job_result"
|
|
6859
|
-
});
|
|
6860
|
-
}
|
|
6861
|
-
if (entries.length === 0 && resultPreview && resultPreview !== "No job result recorded.") {
|
|
6862
|
-
entries.push({
|
|
6863
|
-
id: `job:${jobId}:result-preview`,
|
|
6864
|
-
role: "assistant",
|
|
6865
|
-
content: resultPreview,
|
|
6866
|
-
timestamp,
|
|
6867
|
-
jobId,
|
|
6868
|
-
historyContent: `[Assistant reply]
|
|
6869
|
-
${resultPreview}`,
|
|
6870
|
-
source: "job_result"
|
|
8058
|
+
source: "feed"
|
|
6871
8059
|
});
|
|
6872
8060
|
}
|
|
6873
8061
|
return entries;
|
|
6874
8062
|
}
|
|
6875
|
-
|
|
6876
|
-
const code = trimString(job.source);
|
|
6877
|
-
if (!code) return null;
|
|
6878
|
-
const jobStatus = asString(job.status);
|
|
6879
|
-
const error = jobStatus === "failed" || jobStatus === "canceled" || jobStatus === "timeout" ? trimString(job.error) || `Job ${jobStatus}` : void 0;
|
|
6880
|
-
return {
|
|
6881
|
-
id: `job:${jobId}:code`,
|
|
6882
|
-
role: "assistant",
|
|
6883
|
-
content: "",
|
|
6884
|
-
timestamp: asNumber(job.submittedAt) || asNumber(job.startedAt) || asNumber(job.finishedAt) || 0,
|
|
6885
|
-
jobId,
|
|
6886
|
-
code,
|
|
6887
|
-
jobStatus,
|
|
6888
|
-
jobResultPreview: stringifyTranscriptValue(
|
|
6889
|
-
job.result,
|
|
6890
|
-
"No job result recorded."
|
|
6891
|
-
),
|
|
6892
|
-
error,
|
|
6893
|
-
source: "job_code"
|
|
6894
|
-
};
|
|
6895
|
-
}
|
|
8063
|
+
var CANONICAL_FEED_REQUIRED = "Canonical session feed-v1 is required; past message/job transcript reconstruction is not supported.";
|
|
6896
8064
|
function buildSessionTranscript(input) {
|
|
6897
8065
|
const liveDoc = input.liveDoc || null;
|
|
6898
|
-
|
|
6899
|
-
|
|
6900
|
-
|
|
6901
|
-
const
|
|
6902
|
-
|
|
6903
|
-
|
|
6904
|
-
|
|
6905
|
-
|
|
6906
|
-
);
|
|
6907
|
-
const assistantConversationJobIds = new Set(
|
|
6908
|
-
conversationMessages.filter(
|
|
6909
|
-
(message) => message.role === "assistant" && Boolean(message.jobId)
|
|
6910
|
-
).map((message) => message.jobId)
|
|
6911
|
-
);
|
|
6912
|
-
transcript.push(...conversationMessages);
|
|
6913
|
-
const jobsById = asRecord3(asRecord3(liveDoc?.jobs)?.byId) || {};
|
|
6914
|
-
const jobs = Object.values(jobsById).map((value) => asRecord3(value)).filter((value) => Boolean(value)).sort(
|
|
6915
|
-
(left, right) => (asNumber(left.submittedAt) || asNumber(left.startedAt) || asNumber(left.finishedAt) || 0) - (asNumber(right.submittedAt) || asNumber(right.startedAt) || asNumber(right.finishedAt) || 0)
|
|
6916
|
-
);
|
|
6917
|
-
for (const job of jobs) {
|
|
6918
|
-
const jobId = asString(job.jobId);
|
|
6919
|
-
if (!jobId) continue;
|
|
6920
|
-
const codeEntry = buildJobCodeEntry(jobId, job);
|
|
6921
|
-
if (codeEntry) {
|
|
6922
|
-
transcript.push(codeEntry);
|
|
6923
|
-
}
|
|
6924
|
-
transcript.push(
|
|
6925
|
-
...normalizePromptEntries(jobId, job.prompts, conversationPromptIds)
|
|
6926
|
-
);
|
|
6927
|
-
if (!assistantConversationJobIds.has(jobId)) {
|
|
6928
|
-
const agentEntries = normalizeAgentMessageEntries(
|
|
6929
|
-
jobId,
|
|
6930
|
-
job.agentMessages,
|
|
6931
|
-
artifactsById
|
|
6932
|
-
);
|
|
6933
|
-
if (agentEntries.length > 0) {
|
|
6934
|
-
transcript.push(...agentEntries);
|
|
6935
|
-
} else {
|
|
6936
|
-
transcript.push(
|
|
6937
|
-
...buildJobFallbackEntries(
|
|
6938
|
-
jobId,
|
|
6939
|
-
job,
|
|
6940
|
-
sessionHeap,
|
|
6941
|
-
artifactsById
|
|
6942
|
-
)
|
|
6943
|
-
);
|
|
6944
|
-
}
|
|
6945
|
-
}
|
|
6946
|
-
}
|
|
6947
|
-
return transcript.sort((left, right) => {
|
|
6948
|
-
if (left.timestamp !== right.timestamp) {
|
|
6949
|
-
return left.timestamp - right.timestamp;
|
|
6950
|
-
}
|
|
6951
|
-
return left.id.localeCompare(right.id);
|
|
8066
|
+
if (!isCanonicalSessionFeedDocument(liveDoc)) {
|
|
8067
|
+
throw new Error(CANONICAL_FEED_REQUIRED);
|
|
8068
|
+
}
|
|
8069
|
+
const snapshot = readSessionFeedSnapshot(liveDoc);
|
|
8070
|
+
if (snapshot.error) throw snapshot.error;
|
|
8071
|
+
return buildSessionTranscriptFromFeedItems({
|
|
8072
|
+
items: input.canonicalFeedItems || snapshot.tail,
|
|
8073
|
+
liveDoc
|
|
6952
8074
|
});
|
|
6953
8075
|
}
|
|
6954
8076
|
|
|
@@ -11940,7 +13062,7 @@ function normalizeEffectBehaviors(value) {
|
|
|
11940
13062
|
}
|
|
11941
13063
|
function resolveInvocationMode(context) {
|
|
11942
13064
|
const mode = context?.invocation?.mode;
|
|
11943
|
-
if (mode === "dryRun" || mode === "reverse") {
|
|
13065
|
+
if (mode === "dryRun" || mode === "reverse" || mode === "artifactOptions") {
|
|
11944
13066
|
return mode;
|
|
11945
13067
|
}
|
|
11946
13068
|
return "execute";
|
|
@@ -11994,6 +13116,18 @@ function resolveHandlerForMode(effectMap, effect, request) {
|
|
|
11994
13116
|
request.context?.behaviors || effect.metamodels || void 0
|
|
11995
13117
|
);
|
|
11996
13118
|
const mode = resolveInvocationMode(request.context);
|
|
13119
|
+
if (mode === "artifactOptions") {
|
|
13120
|
+
if (!effect.artifactOptionsHandler) {
|
|
13121
|
+
throw new Error(
|
|
13122
|
+
`Artifact relationship options are not supported for ${request.effectKey}`
|
|
13123
|
+
);
|
|
13124
|
+
}
|
|
13125
|
+
return {
|
|
13126
|
+
effect,
|
|
13127
|
+
mode,
|
|
13128
|
+
handler: effect.artifactOptionsHandler
|
|
13129
|
+
};
|
|
13130
|
+
}
|
|
11997
13131
|
if (mode === "dryRun") {
|
|
11998
13132
|
if (effect.dryRunHandler) {
|
|
11999
13133
|
return { effect, mode, handler: effect.dryRunHandler };
|
|
@@ -12026,7 +13160,91 @@ function resolveHandlerForMode(effectMap, effect, request) {
|
|
|
12026
13160
|
}
|
|
12027
13161
|
return { effect, mode, handler: effect.handler };
|
|
12028
13162
|
}
|
|
12029
|
-
|
|
13163
|
+
function createInvocationFeedbackContext(bridge) {
|
|
13164
|
+
const publications = [];
|
|
13165
|
+
let nextOperationOrdinal = 0;
|
|
13166
|
+
const nextOperationId = (kind) => `${kind}:${nextOperationOrdinal++}`;
|
|
13167
|
+
const track = (publication) => {
|
|
13168
|
+
const tracked = Promise.resolve(publication);
|
|
13169
|
+
publications.push(tracked);
|
|
13170
|
+
void tracked.catch(() => void 0);
|
|
13171
|
+
return tracked;
|
|
13172
|
+
};
|
|
13173
|
+
const publisher = createFeedPublisher(
|
|
13174
|
+
(method, params) => bridge.publish(method, {
|
|
13175
|
+
...params,
|
|
13176
|
+
invocationId: bridge.invocationId
|
|
13177
|
+
})
|
|
13178
|
+
);
|
|
13179
|
+
const wrapTransientHandle = (initial) => {
|
|
13180
|
+
let current = initial;
|
|
13181
|
+
const wrapped = {
|
|
13182
|
+
get id() {
|
|
13183
|
+
return current.id;
|
|
13184
|
+
},
|
|
13185
|
+
get ordinal() {
|
|
13186
|
+
return current.ordinal;
|
|
13187
|
+
},
|
|
13188
|
+
get revision() {
|
|
13189
|
+
return current.revision;
|
|
13190
|
+
},
|
|
13191
|
+
async update(text, options = {}) {
|
|
13192
|
+
current = await track(
|
|
13193
|
+
current.update(text, {
|
|
13194
|
+
...options,
|
|
13195
|
+
operationId: options.operationId || nextOperationId("transient-update")
|
|
13196
|
+
})
|
|
13197
|
+
);
|
|
13198
|
+
return wrapped;
|
|
13199
|
+
},
|
|
13200
|
+
settle(text, options = {}) {
|
|
13201
|
+
return track(
|
|
13202
|
+
current.settle(text, {
|
|
13203
|
+
...options,
|
|
13204
|
+
operationId: options.operationId || nextOperationId("transient-settle")
|
|
13205
|
+
})
|
|
13206
|
+
);
|
|
13207
|
+
}
|
|
13208
|
+
};
|
|
13209
|
+
return wrapped;
|
|
13210
|
+
};
|
|
13211
|
+
return {
|
|
13212
|
+
feedback(text, options = {}) {
|
|
13213
|
+
return track(
|
|
13214
|
+
publisher.feedback(text, {
|
|
13215
|
+
...options,
|
|
13216
|
+
operationId: options.operationId || nextOperationId("feedback")
|
|
13217
|
+
})
|
|
13218
|
+
);
|
|
13219
|
+
},
|
|
13220
|
+
transientFeedback(text, options = {}) {
|
|
13221
|
+
return track(
|
|
13222
|
+
publisher.transientFeedback(text, {
|
|
13223
|
+
...options,
|
|
13224
|
+
operationId: options.operationId || nextOperationId("transient-create")
|
|
13225
|
+
}).then(wrapTransientHandle)
|
|
13226
|
+
);
|
|
13227
|
+
},
|
|
13228
|
+
async flush() {
|
|
13229
|
+
let cursor = 0;
|
|
13230
|
+
let firstError;
|
|
13231
|
+
while (cursor < publications.length) {
|
|
13232
|
+
const batch = publications.slice(cursor);
|
|
13233
|
+
cursor = publications.length;
|
|
13234
|
+
const results = await Promise.allSettled(batch);
|
|
13235
|
+
for (const result of results) {
|
|
13236
|
+
if (result.status === "rejected" && firstError === void 0) {
|
|
13237
|
+
firstError = result.reason;
|
|
13238
|
+
}
|
|
13239
|
+
}
|
|
13240
|
+
}
|
|
13241
|
+
if (firstError !== void 0) {
|
|
13242
|
+
throw firstError;
|
|
13243
|
+
}
|
|
13244
|
+
}
|
|
13245
|
+
};
|
|
13246
|
+
}
|
|
13247
|
+
async function invokeRegisteredEffect(effectMap, request, options = {}) {
|
|
12030
13248
|
const effect = selectRegisteredEffect(
|
|
12031
13249
|
effectMap,
|
|
12032
13250
|
request.effectKey,
|
|
@@ -12045,14 +13263,49 @@ async function invokeRegisteredEffect(effectMap, request) {
|
|
|
12045
13263
|
mode: resolved.mode,
|
|
12046
13264
|
sourceEffectKey: request.effectKey,
|
|
12047
13265
|
sourceEffectName: request.effectName,
|
|
12048
|
-
...request.context?.invocation?.reverseHandler ? { reverseHandler: request.context.invocation.reverseHandler } : {}
|
|
13266
|
+
...request.context?.invocation?.reverseHandler ? { reverseHandler: request.context.invocation.reverseHandler } : {},
|
|
13267
|
+
...request.context?.invocation?.artifactId ? { artifactId: request.context.invocation.artifactId } : {},
|
|
13268
|
+
...request.context?.invocation?.artifactOptions ? { artifactOptions: request.context.invocation.artifactOptions } : {}
|
|
12049
13269
|
}
|
|
12050
13270
|
};
|
|
12051
|
-
|
|
12052
|
-
|
|
12053
|
-
|
|
13271
|
+
const feedbackContext = options.feedback ? createInvocationFeedbackContext(options.feedback) : null;
|
|
13272
|
+
if (feedbackContext) {
|
|
13273
|
+
context.feedback = feedbackContext.feedback;
|
|
13274
|
+
context.transientFeedback = feedbackContext.transientFeedback;
|
|
13275
|
+
}
|
|
13276
|
+
let handlerResult;
|
|
13277
|
+
let handlerError;
|
|
13278
|
+
let handlerFailed = false;
|
|
13279
|
+
try {
|
|
13280
|
+
if (resolved.mode === "artifactOptions") {
|
|
13281
|
+
handlerResult = await resolved.handler(request.input, context);
|
|
13282
|
+
} else if (resolved.effect.className && !resolved.effect.static && request.input && typeof request.input === "object" && "_objectId" in request.input) {
|
|
13283
|
+
const { _objectId, ...rest } = request.input;
|
|
13284
|
+
handlerResult = await resolved.handler(_objectId, rest, context);
|
|
13285
|
+
} else {
|
|
13286
|
+
handlerResult = await resolved.handler(request.input, context);
|
|
13287
|
+
}
|
|
13288
|
+
} catch (error) {
|
|
13289
|
+
handlerFailed = true;
|
|
13290
|
+
handlerError = error;
|
|
12054
13291
|
}
|
|
12055
|
-
|
|
13292
|
+
let feedbackError;
|
|
13293
|
+
let feedbackFailed = false;
|
|
13294
|
+
if (feedbackContext) {
|
|
13295
|
+
try {
|
|
13296
|
+
await feedbackContext.flush();
|
|
13297
|
+
} catch (error) {
|
|
13298
|
+
feedbackFailed = true;
|
|
13299
|
+
feedbackError = error;
|
|
13300
|
+
}
|
|
13301
|
+
}
|
|
13302
|
+
if (handlerFailed) {
|
|
13303
|
+
throw handlerError;
|
|
13304
|
+
}
|
|
13305
|
+
if (feedbackFailed) {
|
|
13306
|
+
throw feedbackError;
|
|
13307
|
+
}
|
|
13308
|
+
return handlerResult;
|
|
12056
13309
|
}
|
|
12057
13310
|
|
|
12058
13311
|
// src/client-normalizers.ts
|
|
@@ -14062,6 +15315,36 @@ function buildEffectMetamodelMutations(toolPath, spec) {
|
|
|
14062
15315
|
var DEFAULT_CONVERSATION_SESSION_LIST_LIMIT = 100;
|
|
14063
15316
|
var MAX_CONVERSATION_SESSION_LIST_LIMIT = 500;
|
|
14064
15317
|
var MAX_CONVERSATION_SESSION_LIST_OFFSET = 1e5;
|
|
15318
|
+
function requireUserEnvironmentSequence(value, field) {
|
|
15319
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
15320
|
+
throw new Error(
|
|
15321
|
+
`Invalid user-environment state: ${field} must be a non-negative safe integer.`
|
|
15322
|
+
);
|
|
15323
|
+
}
|
|
15324
|
+
return value;
|
|
15325
|
+
}
|
|
15326
|
+
function normalizeReadThroughSequenceMap(value) {
|
|
15327
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
15328
|
+
throw new Error(
|
|
15329
|
+
"Invalid user-environment state: readThroughSequenceBySessionId is required."
|
|
15330
|
+
);
|
|
15331
|
+
}
|
|
15332
|
+
return Object.fromEntries(
|
|
15333
|
+
Object.entries(value).map(([sessionId, sequence]) => [
|
|
15334
|
+
sessionId,
|
|
15335
|
+
requireUserEnvironmentSequence(
|
|
15336
|
+
sequence,
|
|
15337
|
+
`readThroughSequenceBySessionId.${sessionId}`
|
|
15338
|
+
)
|
|
15339
|
+
])
|
|
15340
|
+
);
|
|
15341
|
+
}
|
|
15342
|
+
function requiredAssistantReplyString(value, field) {
|
|
15343
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
15344
|
+
throw new Error(`Assistant reply ${field} must be a non-empty string.`);
|
|
15345
|
+
}
|
|
15346
|
+
return field === "text" ? value : value.trim();
|
|
15347
|
+
}
|
|
14065
15348
|
function boundedSessionListInteger(value, name, fallback, minimum, maximum) {
|
|
14066
15349
|
if (value === void 0) return fallback;
|
|
14067
15350
|
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
|
@@ -15635,12 +16918,55 @@ var Environment = class _Environment {
|
|
|
15635
16918
|
var EnvironmentSession = class extends Session {
|
|
15636
16919
|
environment;
|
|
15637
16920
|
sessionDataRoutePrefix;
|
|
16921
|
+
sessionDataHeaders;
|
|
16922
|
+
heartbeatTimer = null;
|
|
16923
|
+
heartbeatInFlight = false;
|
|
15638
16924
|
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
15639
16925
|
graphContainerStatus = null;
|
|
15640
16926
|
constructor(client, environment, clientId, options = {}) {
|
|
15641
16927
|
super(client, clientId, { initialQuota: options.initialQuota });
|
|
15642
16928
|
this.environment = environment;
|
|
15643
16929
|
this.sessionDataRoutePrefix = options.sessionDataRoutePrefix || "/orchestrator/ws/sessions";
|
|
16930
|
+
this.sessionDataHeaders = options.sessionDataHeaders || {};
|
|
16931
|
+
this.setFeedListTransport(
|
|
16932
|
+
(feedOptions) => this.sessionDataRequest("/feed", feedOptions)
|
|
16933
|
+
);
|
|
16934
|
+
}
|
|
16935
|
+
/**
|
|
16936
|
+
* Keep the browser session transport observable from the client side.
|
|
16937
|
+
* A worker/proxy restart can leave a browser WebSocket appearing OPEN even
|
|
16938
|
+
* though the server-side Durable Object has already closed its peer. The
|
|
16939
|
+
* heartbeat gives the SDK a bounded failure signal so it can revoke that
|
|
16940
|
+
* stale socket and use WSClient's normal reconnect path.
|
|
16941
|
+
*/
|
|
16942
|
+
startHeartbeat() {
|
|
16943
|
+
if (this.heartbeatTimer) return;
|
|
16944
|
+
this.heartbeatTimer = setInterval(() => {
|
|
16945
|
+
if (this.heartbeatInFlight) return;
|
|
16946
|
+
this.heartbeatInFlight = true;
|
|
16947
|
+
void this.client.call("client.heartbeat", {}).then((result) => {
|
|
16948
|
+
if (result?.graphContainerStatus) {
|
|
16949
|
+
this.graphContainerStatus = result.graphContainerStatus;
|
|
16950
|
+
}
|
|
16951
|
+
}).catch((error) => {
|
|
16952
|
+
this.client.reportTransportFailure(error);
|
|
16953
|
+
}).finally(() => {
|
|
16954
|
+
this.heartbeatInFlight = false;
|
|
16955
|
+
});
|
|
16956
|
+
}, 5e3);
|
|
16957
|
+
this.heartbeatTimer.unref?.();
|
|
16958
|
+
}
|
|
16959
|
+
stopHeartbeat() {
|
|
16960
|
+
if (this.heartbeatTimer) {
|
|
16961
|
+
clearInterval(this.heartbeatTimer);
|
|
16962
|
+
this.heartbeatTimer = null;
|
|
16963
|
+
}
|
|
16964
|
+
this.heartbeatInFlight = false;
|
|
16965
|
+
}
|
|
16966
|
+
async hello() {
|
|
16967
|
+
const result = await super.hello();
|
|
16968
|
+
this.startHeartbeat();
|
|
16969
|
+
return result;
|
|
15644
16970
|
}
|
|
15645
16971
|
get environmentId() {
|
|
15646
16972
|
return this.environment.environmentId;
|
|
@@ -15700,6 +17026,11 @@ var EnvironmentSession = class extends Session {
|
|
|
15700
17026
|
const response = await fetch(url, {
|
|
15701
17027
|
method: init2.method || "GET",
|
|
15702
17028
|
headers,
|
|
17029
|
+
// Session documents, feed pages, and artifact reads are live
|
|
17030
|
+
// Automerge-backed state. A browser cache entry for the identical
|
|
17031
|
+
// artifact URL can otherwise make the Dock poll the same stale
|
|
17032
|
+
// `running` record until a full reload revalidates it.
|
|
17033
|
+
cache: "no-store",
|
|
15703
17034
|
...typeof init2.body === "undefined" ? {} : { body: init2.body }
|
|
15704
17035
|
});
|
|
15705
17036
|
if (response.ok) {
|
|
@@ -15728,34 +17059,50 @@ var EnvironmentSession = class extends Session {
|
|
|
15728
17059
|
const body = typeof init2.body === "undefined" ? void 0 : JSON.stringify(init2.body);
|
|
15729
17060
|
const response = await this.sessionDataFetch(path, query, {
|
|
15730
17061
|
method: init2.method || "GET",
|
|
15731
|
-
headers: {
|
|
17062
|
+
headers: {
|
|
17063
|
+
...this.sessionDataHeaders,
|
|
17064
|
+
"Content-Type": "application/json"
|
|
17065
|
+
},
|
|
15732
17066
|
...typeof body === "undefined" ? {} : { body }
|
|
15733
17067
|
});
|
|
15734
17068
|
return response.json();
|
|
15735
17069
|
}
|
|
15736
|
-
async collectAllSessionItems(listPage) {
|
|
15737
|
-
const items = [];
|
|
15738
|
-
let cursor = null;
|
|
15739
|
-
do {
|
|
15740
|
-
const page = await listPage({ limit: 500, cursor });
|
|
15741
|
-
items.push(...page.items);
|
|
15742
|
-
cursor = page.nextCursor;
|
|
15743
|
-
} while (cursor);
|
|
15744
|
-
return items;
|
|
15745
|
-
}
|
|
15746
17070
|
/**
|
|
15747
17071
|
* Fetch the live session document from the runtime DO.
|
|
15748
17072
|
*
|
|
15749
|
-
* For history
|
|
15750
|
-
*
|
|
17073
|
+
* For presentation history use `feed.list()` or `transcript.list()`.
|
|
17074
|
+
* Timeline and job collections are diagnostic/execution data only.
|
|
15751
17075
|
*/
|
|
15752
17076
|
async getDocument() {
|
|
15753
17077
|
return this.sessionDataRequest("/document");
|
|
15754
17078
|
}
|
|
15755
|
-
|
|
15756
|
-
|
|
15757
|
-
|
|
15758
|
-
|
|
17079
|
+
/**
|
|
17080
|
+
* Publish one terminal assistant reply from a trusted server integration.
|
|
17081
|
+
*
|
|
17082
|
+
* This uses the API-key-authenticated HTTP session boundary. Delegated
|
|
17083
|
+
* browser sessions cannot use it and never receive assistant feed-authoring
|
|
17084
|
+
* capability through their WebSocket.
|
|
17085
|
+
*/
|
|
17086
|
+
async publishAssistantReply(input) {
|
|
17087
|
+
if (this.sessionDataRoutePrefix === "/sdk/browser-sessions") {
|
|
17088
|
+
throw new Error(
|
|
17089
|
+
"Assistant replies require a server API-key session connection."
|
|
17090
|
+
);
|
|
17091
|
+
}
|
|
17092
|
+
const id = requiredAssistantReplyString(input.id, "id");
|
|
17093
|
+
const operationId2 = requiredAssistantReplyString(
|
|
17094
|
+
input.operationId,
|
|
17095
|
+
"operationId"
|
|
17096
|
+
);
|
|
17097
|
+
const text = requiredAssistantReplyString(input.text, "text");
|
|
17098
|
+
return this.sessionDataRequest(
|
|
17099
|
+
"/assistant-replies",
|
|
17100
|
+
void 0,
|
|
17101
|
+
{
|
|
17102
|
+
method: "POST",
|
|
17103
|
+
body: { id, operationId: operationId2, text }
|
|
17104
|
+
}
|
|
17105
|
+
);
|
|
15759
17106
|
}
|
|
15760
17107
|
get timeline() {
|
|
15761
17108
|
return {
|
|
@@ -15797,7 +17144,12 @@ var EnvironmentSession = class extends Session {
|
|
|
15797
17144
|
latestJob: true
|
|
15798
17145
|
}),
|
|
15799
17146
|
get: (artifactId) => this.sessionDataRequest(
|
|
15800
|
-
`/artifacts/${encodeURIComponent(artifactId)}
|
|
17147
|
+
`/artifacts/${encodeURIComponent(artifactId)}`,
|
|
17148
|
+
// The artifact endpoint is polled while an effect is running. Keep
|
|
17149
|
+
// each read addressable as a fresh resource as well as using
|
|
17150
|
+
// `cache: no-store`; this also bypasses intermediaries that ignore
|
|
17151
|
+
// the Fetch cache directive for an otherwise identical GET URL.
|
|
17152
|
+
{ _granularLiveRead: Date.now() }
|
|
15801
17153
|
),
|
|
15802
17154
|
create: (artifact) => this.sessionDataRequest(
|
|
15803
17155
|
"/artifacts",
|
|
@@ -15807,6 +17159,14 @@ var EnvironmentSession = class extends Session {
|
|
|
15807
17159
|
body: artifact
|
|
15808
17160
|
}
|
|
15809
17161
|
),
|
|
17162
|
+
acceptSuggestion: (sequence) => this.sessionDataRequest(
|
|
17163
|
+
"/artifacts/suggestions/accept",
|
|
17164
|
+
void 0,
|
|
17165
|
+
{
|
|
17166
|
+
method: "POST",
|
|
17167
|
+
body: { sequence }
|
|
17168
|
+
}
|
|
17169
|
+
),
|
|
15810
17170
|
updateInputs: (artifactId, patch) => this.sessionDataRequest(
|
|
15811
17171
|
`/artifacts/${encodeURIComponent(artifactId)}`,
|
|
15812
17172
|
void 0,
|
|
@@ -15815,6 +17175,16 @@ var EnvironmentSession = class extends Session {
|
|
|
15815
17175
|
body: patch
|
|
15816
17176
|
}
|
|
15817
17177
|
),
|
|
17178
|
+
relationshipOptions: (artifactId, input) => this.sessionDataRequest(
|
|
17179
|
+
`/artifacts/${encodeURIComponent(artifactId)}/relationship-options`,
|
|
17180
|
+
void 0,
|
|
17181
|
+
{ method: "POST", body: input }
|
|
17182
|
+
),
|
|
17183
|
+
relationshipCreate: (artifactId, input) => this.sessionDataRequest(
|
|
17184
|
+
`/artifacts/${encodeURIComponent(artifactId)}/relationship-create`,
|
|
17185
|
+
void 0,
|
|
17186
|
+
{ method: "POST", body: input }
|
|
17187
|
+
),
|
|
15818
17188
|
validate: (artifactId) => this.sessionDataRequest(
|
|
15819
17189
|
`/artifacts/${encodeURIComponent(artifactId)}/validate`,
|
|
15820
17190
|
void 0,
|
|
@@ -15952,73 +17322,50 @@ var EnvironmentSession = class extends Session {
|
|
|
15952
17322
|
get transcript() {
|
|
15953
17323
|
return {
|
|
15954
17324
|
list: async (options = {}) => {
|
|
15955
|
-
|
|
15956
|
-
|
|
15957
|
-
|
|
15958
|
-
|
|
15959
|
-
|
|
15960
|
-
|
|
15961
|
-
|
|
15962
|
-
|
|
15963
|
-
|
|
15964
|
-
)
|
|
15965
|
-
|
|
15966
|
-
|
|
15967
|
-
|
|
15968
|
-
jobs: {
|
|
15969
|
-
byId: Object.fromEntries(
|
|
15970
|
-
jobs.map((job) => {
|
|
15971
|
-
const record = job && typeof job === "object" ? job : null;
|
|
15972
|
-
const id = typeof record?.jobId === "string" ? record.jobId : typeof record?.id === "string" ? record.id : null;
|
|
15973
|
-
return id ? [id, record] : null;
|
|
15974
|
-
}).filter(
|
|
15975
|
-
(entry) => Boolean(entry)
|
|
15976
|
-
)
|
|
15977
|
-
)
|
|
15978
|
-
},
|
|
15979
|
-
artifacts: {
|
|
15980
|
-
byId: Object.fromEntries(
|
|
15981
|
-
artifacts.map((artifact) => {
|
|
15982
|
-
return artifact?.artifactId ? [
|
|
15983
|
-
artifact.artifactId,
|
|
15984
|
-
artifact
|
|
15985
|
-
] : null;
|
|
15986
|
-
}).filter(
|
|
15987
|
-
(entry) => Boolean(entry)
|
|
15988
|
-
)
|
|
15989
|
-
),
|
|
15990
|
-
order: artifacts.map((artifact) => artifact?.artifactId).filter(
|
|
15991
|
-
(artifactId) => Boolean(artifactId)
|
|
15992
|
-
)
|
|
17325
|
+
if (!isCanonicalSessionFeedDocument(this.document)) {
|
|
17326
|
+
throw new Error(
|
|
17327
|
+
"Canonical session feed-v1 is required; transcript.list() does not reconstruct past message or job collections."
|
|
17328
|
+
);
|
|
17329
|
+
}
|
|
17330
|
+
const canonicalFeedItems = [];
|
|
17331
|
+
let afterSequence = 0;
|
|
17332
|
+
let pageCount = 0;
|
|
17333
|
+
for (; ; ) {
|
|
17334
|
+
if (++pageCount > 1e4) {
|
|
17335
|
+
throw new Error(
|
|
17336
|
+
"Canonical transcript history exceeded its page limit."
|
|
17337
|
+
);
|
|
15993
17338
|
}
|
|
15994
|
-
|
|
15995
|
-
|
|
15996
|
-
|
|
15997
|
-
|
|
15998
|
-
|
|
15999
|
-
|
|
16000
|
-
|
|
16001
|
-
|
|
16002
|
-
|
|
16003
|
-
|
|
16004
|
-
|
|
16005
|
-
|
|
16006
|
-
|
|
16007
|
-
|
|
16008
|
-
|
|
16009
|
-
|
|
16010
|
-
|
|
16011
|
-
|
|
16012
|
-
|
|
17339
|
+
const page = await this.feed.list({
|
|
17340
|
+
afterSequence,
|
|
17341
|
+
limit: 500
|
|
17342
|
+
});
|
|
17343
|
+
if (page.items.length === 0) {
|
|
17344
|
+
if (page.hasMoreAfter) {
|
|
17345
|
+
throw new Error(
|
|
17346
|
+
`Canonical transcript history stopped after sequence ${afterSequence}.`
|
|
17347
|
+
);
|
|
17348
|
+
}
|
|
17349
|
+
break;
|
|
17350
|
+
}
|
|
17351
|
+
if (page.items[0].sequence !== afterSequence + 1) {
|
|
17352
|
+
throw new Error(
|
|
17353
|
+
`Canonical transcript history has a gap after sequence ${afterSequence}.`
|
|
17354
|
+
);
|
|
17355
|
+
}
|
|
17356
|
+
canonicalFeedItems.push(...page.items);
|
|
17357
|
+
afterSequence = page.items[page.items.length - 1].sequence;
|
|
17358
|
+
if (!page.hasMoreAfter) break;
|
|
17359
|
+
}
|
|
16013
17360
|
const allItems = buildSessionTranscript({
|
|
16014
|
-
liveDoc,
|
|
16015
|
-
|
|
17361
|
+
liveDoc: this.document,
|
|
17362
|
+
canonicalFeedItems
|
|
16016
17363
|
});
|
|
16017
17364
|
const limit = Math.max(
|
|
16018
17365
|
1,
|
|
16019
17366
|
Math.min(500, Math.floor(options.limit ?? 100))
|
|
16020
17367
|
);
|
|
16021
|
-
const offset = typeof options.cursor === "string" ? Number.parseInt(options.cursor, 10) || 0 : 0;
|
|
17368
|
+
const offset = options.latest ? Math.max(0, allItems.length - limit) : typeof options.cursor === "string" ? Number.parseInt(options.cursor, 10) || 0 : 0;
|
|
16022
17369
|
const items = allItems.slice(offset, offset + limit);
|
|
16023
17370
|
const nextOffset = offset + items.length;
|
|
16024
17371
|
return {
|
|
@@ -16104,6 +17451,8 @@ var EnvironmentSession = class extends Session {
|
|
|
16104
17451
|
* acknowledgement was observed.
|
|
16105
17452
|
*/
|
|
16106
17453
|
async disconnect() {
|
|
17454
|
+
this.stopHeartbeat();
|
|
17455
|
+
this.disposeSessionFeed();
|
|
16107
17456
|
let wsNotifiedRuntime = false;
|
|
16108
17457
|
try {
|
|
16109
17458
|
const goodbye = await this.rpc(
|
|
@@ -16142,6 +17491,7 @@ var EnvironmentSession = class extends Session {
|
|
|
16142
17491
|
* Close only the socket transport without sending `client.goodbye`.
|
|
16143
17492
|
*/
|
|
16144
17493
|
disconnectTransport() {
|
|
17494
|
+
this.stopHeartbeat();
|
|
16145
17495
|
this.client.disconnect({ reason: "Transport detach" });
|
|
16146
17496
|
}
|
|
16147
17497
|
/**
|
|
@@ -16906,16 +18256,22 @@ var Granular = class _Granular {
|
|
|
16906
18256
|
return this.normalizeUserEnvironmentState(state);
|
|
16907
18257
|
}
|
|
16908
18258
|
async markUserEnvironmentSessionsRead(options) {
|
|
18259
|
+
const readThroughSequence = requireUserEnvironmentSequence(
|
|
18260
|
+
options.readThroughSequence,
|
|
18261
|
+
"readThroughSequence"
|
|
18262
|
+
);
|
|
16909
18263
|
const result = await this.request("/sdk/user-environment-state/read", {
|
|
16910
18264
|
method: "POST",
|
|
16911
18265
|
body: JSON.stringify({
|
|
16912
18266
|
environmentId: options.environmentId,
|
|
16913
18267
|
sessionId: options.sessionId,
|
|
16914
18268
|
sessionIds: options.sessionIds,
|
|
16915
|
-
|
|
18269
|
+
readThroughSequence
|
|
16916
18270
|
})
|
|
16917
18271
|
});
|
|
16918
|
-
return
|
|
18272
|
+
return normalizeReadThroughSequenceMap(
|
|
18273
|
+
result.readThroughSequenceBySessionId
|
|
18274
|
+
);
|
|
16919
18275
|
}
|
|
16920
18276
|
normalizeConversationSession(row) {
|
|
16921
18277
|
const sessionId = String(row.sessionId ?? row.session_id ?? "");
|
|
@@ -16940,21 +18296,48 @@ var Granular = class _Granular {
|
|
|
16940
18296
|
};
|
|
16941
18297
|
}
|
|
16942
18298
|
normalizeUserEnvironmentState(state) {
|
|
18299
|
+
if (!Array.isArray(state.sessions)) {
|
|
18300
|
+
throw new Error(
|
|
18301
|
+
"Invalid user-environment state: sessions must be an array."
|
|
18302
|
+
);
|
|
18303
|
+
}
|
|
18304
|
+
const sessions = state.sessions.map((item, index) => ({
|
|
18305
|
+
...item,
|
|
18306
|
+
readThroughSequence: requireUserEnvironmentSequence(
|
|
18307
|
+
item?.readThroughSequence,
|
|
18308
|
+
`sessions[${index}].readThroughSequence`
|
|
18309
|
+
),
|
|
18310
|
+
messagePreview: {
|
|
18311
|
+
...item.messagePreview,
|
|
18312
|
+
latestMessageSequence: requireUserEnvironmentSequence(
|
|
18313
|
+
item.messagePreview?.latestMessageSequence,
|
|
18314
|
+
`sessions[${index}].messagePreview.latestMessageSequence`
|
|
18315
|
+
),
|
|
18316
|
+
latestAssistantSequence: requireUserEnvironmentSequence(
|
|
18317
|
+
item.messagePreview?.latestAssistantSequence,
|
|
18318
|
+
`sessions[${index}].messagePreview.latestAssistantSequence`
|
|
18319
|
+
),
|
|
18320
|
+
unreadProducingSequence: requireUserEnvironmentSequence(
|
|
18321
|
+
item.messagePreview?.unreadProducingSequence,
|
|
18322
|
+
`sessions[${index}].messagePreview.unreadProducingSequence`
|
|
18323
|
+
)
|
|
18324
|
+
},
|
|
18325
|
+
session: this.normalizeConversationSession(
|
|
18326
|
+
item.session
|
|
18327
|
+
)
|
|
18328
|
+
}));
|
|
16943
18329
|
return {
|
|
16944
18330
|
...state,
|
|
16945
|
-
sessions
|
|
16946
|
-
...item,
|
|
16947
|
-
session: this.normalizeConversationSession(
|
|
16948
|
-
item.session
|
|
16949
|
-
)
|
|
16950
|
-
})) : [],
|
|
18331
|
+
sessions,
|
|
16951
18332
|
attention: {
|
|
16952
18333
|
prompts: Array.isArray(state.attention?.prompts) ? state.attention.prompts : [],
|
|
16953
18334
|
count: typeof state.attention?.count === "number" ? state.attention.count : 0,
|
|
16954
18335
|
activePrompt: state.attention?.activePrompt || null
|
|
16955
18336
|
},
|
|
16956
18337
|
unreadCount: typeof state.unreadCount === "number" ? state.unreadCount : 0,
|
|
16957
|
-
|
|
18338
|
+
readThroughSequenceBySessionId: normalizeReadThroughSequenceMap(
|
|
18339
|
+
state.readThroughSequenceBySessionId
|
|
18340
|
+
)
|
|
16958
18341
|
};
|
|
16959
18342
|
}
|
|
16960
18343
|
static coerceIsoDate(value) {
|
|
@@ -16985,7 +18368,9 @@ var Granular = class _Granular {
|
|
|
16985
18368
|
environmentId: options.environmentId,
|
|
16986
18369
|
clientId,
|
|
16987
18370
|
sessionScope,
|
|
16988
|
-
capabilities:
|
|
18371
|
+
capabilities: {
|
|
18372
|
+
...sessionScope ? { sessionScope } : {}
|
|
18373
|
+
},
|
|
16989
18374
|
initialHeap: options.initialHeap
|
|
16990
18375
|
})
|
|
16991
18376
|
});
|
|
@@ -17355,6 +18740,21 @@ var Granular = class _Granular {
|
|
|
17355
18740
|
reconnectError
|
|
17356
18741
|
);
|
|
17357
18742
|
console.error("[Granular] Original heartbeat failure:", error);
|
|
18743
|
+
if (this.onReconnectError) {
|
|
18744
|
+
try {
|
|
18745
|
+
this.onReconnectError({
|
|
18746
|
+
sessionId: `effect-host:${host.effectClientId}`,
|
|
18747
|
+
error: reconnectError instanceof Error ? reconnectError.message : String(reconnectError),
|
|
18748
|
+
timestamp: Date.now(),
|
|
18749
|
+
terminal: false
|
|
18750
|
+
});
|
|
18751
|
+
} catch (callbackError) {
|
|
18752
|
+
console.error(
|
|
18753
|
+
"[Granular] onReconnectError callback failed after effect-host recovery failure:",
|
|
18754
|
+
callbackError
|
|
18755
|
+
);
|
|
18756
|
+
}
|
|
18757
|
+
}
|
|
17358
18758
|
}
|
|
17359
18759
|
);
|
|
17360
18760
|
}
|
|
@@ -17457,7 +18857,13 @@ var Granular = class _Granular {
|
|
|
17457
18857
|
const request = params;
|
|
17458
18858
|
return invokeRegisteredEffect(
|
|
17459
18859
|
this.getSandboxEffectMap(sandboxId),
|
|
17460
|
-
request
|
|
18860
|
+
request,
|
|
18861
|
+
{
|
|
18862
|
+
feedback: {
|
|
18863
|
+
invocationId: request.callId,
|
|
18864
|
+
publish: (method, publishParams) => wsClient.call(method, publishParams)
|
|
18865
|
+
}
|
|
18866
|
+
}
|
|
17461
18867
|
);
|
|
17462
18868
|
});
|
|
17463
18869
|
wsClient.on("open", () => {
|
|
@@ -17471,14 +18877,20 @@ var Granular = class _Granular {
|
|
|
17471
18877
|
wsClient.on("disconnect", () => {
|
|
17472
18878
|
this.stopEffectHostHeartbeat(host);
|
|
17473
18879
|
});
|
|
17474
|
-
|
|
17475
|
-
|
|
17476
|
-
|
|
17477
|
-
|
|
17478
|
-
|
|
17479
|
-
|
|
17480
|
-
|
|
17481
|
-
|
|
18880
|
+
try {
|
|
18881
|
+
await withTimeout(
|
|
18882
|
+
wsClient.connect(),
|
|
18883
|
+
EFFECT_HOST_CONNECT_TIMEOUT_MS,
|
|
18884
|
+
`effect host WebSocket connect for sandbox ${sandboxId}`
|
|
18885
|
+
);
|
|
18886
|
+
await this.synchronizeEffectHost(host);
|
|
18887
|
+
this.sandboxEffectHosts.set(sandboxId, host);
|
|
18888
|
+
return host;
|
|
18889
|
+
} catch (error) {
|
|
18890
|
+
this.stopEffectHostHeartbeat(host);
|
|
18891
|
+
wsClient.disconnect({ reason: "Effect host initialization failed" });
|
|
18892
|
+
throw error;
|
|
18893
|
+
}
|
|
17482
18894
|
})();
|
|
17483
18895
|
this.sandboxEffectHostPromises.set(sandboxId, connectPromise);
|
|
17484
18896
|
try {
|
|
@@ -18355,7 +19767,7 @@ function asRecord4(value) {
|
|
|
18355
19767
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
18356
19768
|
return value;
|
|
18357
19769
|
}
|
|
18358
|
-
function
|
|
19770
|
+
function asArray(value) {
|
|
18359
19771
|
return Array.isArray(value) ? value : [];
|
|
18360
19772
|
}
|
|
18361
19773
|
function toSortedRecords(value) {
|
|
@@ -18530,14 +19942,30 @@ function hasNamedModuleImport(source, moduleName, name) {
|
|
|
18530
19942
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
18531
19943
|
const imports = source.matchAll(
|
|
18532
19944
|
new RegExp(
|
|
18533
|
-
`import\\s*\\{([
|
|
19945
|
+
`import\\s*\\{([^}]*)\\}\\s*from\\s*['"]${escapedModule}['"]`,
|
|
18534
19946
|
"g"
|
|
18535
19947
|
)
|
|
18536
19948
|
);
|
|
18537
19949
|
for (const match of imports) {
|
|
18538
19950
|
if (new RegExp(`\\b${escaped}\\b`).test(match[1])) return true;
|
|
18539
19951
|
}
|
|
18540
|
-
return false;
|
|
19952
|
+
return false;
|
|
19953
|
+
}
|
|
19954
|
+
function namedModuleImports(source, moduleName) {
|
|
19955
|
+
const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
19956
|
+
const names = /* @__PURE__ */ new Set();
|
|
19957
|
+
for (const match of source.matchAll(
|
|
19958
|
+
new RegExp(
|
|
19959
|
+
`import\\s*\\{([^}]*)\\}\\s*from\\s*['"]${escapedModule}['"]`,
|
|
19960
|
+
"g"
|
|
19961
|
+
)
|
|
19962
|
+
)) {
|
|
19963
|
+
for (const specifier of match[1].split(",")) {
|
|
19964
|
+
const imported = specifier.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0]?.trim();
|
|
19965
|
+
if (imported) names.add(imported);
|
|
19966
|
+
}
|
|
19967
|
+
}
|
|
19968
|
+
return [...names];
|
|
18541
19969
|
}
|
|
18542
19970
|
function hasNamedAgentImport(source, name) {
|
|
18543
19971
|
return hasNamedModuleImport(source, HARNESS_V3_AGENT_MODULE, name);
|
|
@@ -18599,18 +20027,54 @@ function reviewGeneratedJobCode(code, _options = {}) {
|
|
|
18599
20027
|
message: "Generated jobs must not call process.exit(...). Return from the job or emit a runtime message instead."
|
|
18600
20028
|
});
|
|
18601
20029
|
}
|
|
20030
|
+
const supportedAgentExports = /* @__PURE__ */ new Set([
|
|
20031
|
+
"actions",
|
|
20032
|
+
"artifacts",
|
|
20033
|
+
"feedback",
|
|
20034
|
+
"formatBlockers",
|
|
20035
|
+
"relativeTime",
|
|
20036
|
+
"replyToUser",
|
|
20037
|
+
"showAgentResponse",
|
|
20038
|
+
"showObjects",
|
|
20039
|
+
"table",
|
|
20040
|
+
"transientFeedback"
|
|
20041
|
+
]);
|
|
20042
|
+
for (const imported of namedModuleImports(
|
|
20043
|
+
normalized,
|
|
20044
|
+
HARNESS_V3_AGENT_MODULE
|
|
20045
|
+
)) {
|
|
20046
|
+
if (!supportedAgentExports.has(imported)) {
|
|
20047
|
+
issues.push({
|
|
20048
|
+
code: "unknown_runtime_import",
|
|
20049
|
+
severity: "error",
|
|
20050
|
+
message: `Generated code imports unknown runtime value \`${imported}\` from ${HARNESS_V3_AGENT_MODULE}. Import only values listed in [Runtime Imports] and [Types].`
|
|
20051
|
+
});
|
|
20052
|
+
}
|
|
20053
|
+
}
|
|
20054
|
+
const removedAgentHelpers = [
|
|
20055
|
+
["agent", "text", "message"].join("_"),
|
|
20056
|
+
["agent", "heap", "objects"].join("_"),
|
|
20057
|
+
["agent", "message"].join("_")
|
|
20058
|
+
];
|
|
20059
|
+
for (const name of removedAgentHelpers) {
|
|
20060
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20061
|
+
if (new RegExp(`\\b${escaped}\\s*\\(`).test(normalized)) {
|
|
20062
|
+
issues.push({
|
|
20063
|
+
code: "unsupported_runtime_helper",
|
|
20064
|
+
severity: "error",
|
|
20065
|
+
message: `Generated code calls removed runtime helper \`${name}\`. Import and call only the canonical feed helpers listed for ${HARNESS_V3_AGENT_MODULE}.`
|
|
20066
|
+
});
|
|
20067
|
+
}
|
|
20068
|
+
}
|
|
18602
20069
|
for (const [name, replacement, pattern] of [
|
|
18603
|
-
["agent_text_message", "replyToUser", /\bagent_text_message\s*\(/],
|
|
18604
|
-
["agent_heap_objects", "showObjects", /\bagent_heap_objects\s*\(/],
|
|
18605
|
-
["agent_message", "showAgentResponse", /\bagent_message\s*\(/],
|
|
18606
20070
|
["heap", "groundedObjects", /\bheap\./],
|
|
18607
20071
|
["loop", "userInteraction or work", /\bloop\./]
|
|
18608
20072
|
]) {
|
|
18609
20073
|
if (pattern.test(normalized)) {
|
|
18610
20074
|
issues.push({
|
|
18611
|
-
code: "
|
|
20075
|
+
code: "unsupported_runtime_helper",
|
|
18612
20076
|
severity: "error",
|
|
18613
|
-
message: `Generated code uses
|
|
20077
|
+
message: `Generated code uses unsupported runtime helper \`${name}\`. Use \`${replacement}\` from the modules listed in [Runtime Imports].`
|
|
18614
20078
|
});
|
|
18615
20079
|
}
|
|
18616
20080
|
}
|
|
@@ -18779,116 +20243,7 @@ function normalizeActionSummaryForPrompt(line) {
|
|
|
18779
20243
|
}
|
|
18780
20244
|
function collectConversationReferents(liveDoc) {
|
|
18781
20245
|
const conversation = asRecord4(liveDoc?.conversation);
|
|
18782
|
-
|
|
18783
|
-
if (persistedReferents.length > 0) {
|
|
18784
|
-
return persistedReferents.slice().sort((left, right) => (right.ts || 0) - (left.ts || 0));
|
|
18785
|
-
}
|
|
18786
|
-
const heap = asRecord4(liveDoc?.heap);
|
|
18787
|
-
const entriesByPath = asRecord4(heap?.entriesByPath) || {};
|
|
18788
|
-
const listsByName = asRecord4(heap?.listsByName) || {};
|
|
18789
|
-
const variablesByName = asRecord4(heap?.variablesByName) || {};
|
|
18790
|
-
const messages = asArray2(conversation?.messages).map((value) => asRecord4(value)).filter((value) => Boolean(value)).slice().sort((left, right) => (Number(right.ts) || 0) - (Number(left.ts) || 0));
|
|
18791
|
-
const referents = [];
|
|
18792
|
-
const seen = /* @__PURE__ */ new Set();
|
|
18793
|
-
const pushReferent = (referent) => {
|
|
18794
|
-
if (!referent?.kind || !referent.ref) return;
|
|
18795
|
-
const key = `${referent.kind}:${referent.ref}`;
|
|
18796
|
-
if (seen.has(key)) return;
|
|
18797
|
-
seen.add(key);
|
|
18798
|
-
referents.push(referent);
|
|
18799
|
-
};
|
|
18800
|
-
for (const message of messages) {
|
|
18801
|
-
if (message.role !== "assistant") continue;
|
|
18802
|
-
const show = asRecord4(message.show);
|
|
18803
|
-
if (!show) continue;
|
|
18804
|
-
const ts = Number(message.ts) || 0;
|
|
18805
|
-
const messageId = typeof message.id === "string" ? message.id : void 0;
|
|
18806
|
-
const jobId = typeof message.jobId === "string" ? message.jobId : void 0;
|
|
18807
|
-
const entryPaths = uniqueStrings(asArray2(show.entryPaths));
|
|
18808
|
-
const entryClassCounts = /* @__PURE__ */ new Map();
|
|
18809
|
-
const entryMetadata = entryPaths.map((entryPath) => {
|
|
18810
|
-
const entry = asRecord4(entriesByPath[entryPath]);
|
|
18811
|
-
const className = typeof entry?.className === "string" ? entry.className : void 0;
|
|
18812
|
-
if (className) {
|
|
18813
|
-
entryClassCounts.set(
|
|
18814
|
-
className,
|
|
18815
|
-
(entryClassCounts.get(className) || 0) + 1
|
|
18816
|
-
);
|
|
18817
|
-
}
|
|
18818
|
-
return { entryPath, entry, className };
|
|
18819
|
-
});
|
|
18820
|
-
const displayGroupId = entryMetadata.length > 1 ? `message:${messageId || jobId || ts}:entries` : void 0;
|
|
18821
|
-
for (const [
|
|
18822
|
-
index,
|
|
18823
|
-
{ entryPath, entry, className }
|
|
18824
|
-
] of entryMetadata.entries()) {
|
|
18825
|
-
pushReferent({
|
|
18826
|
-
id: `entry:${entryPath}`,
|
|
18827
|
-
kind: "entry",
|
|
18828
|
-
ref: entryPath,
|
|
18829
|
-
role: "assistant",
|
|
18830
|
-
source: "heap_objects",
|
|
18831
|
-
entryPath,
|
|
18832
|
-
recordId: typeof entry?.id === "string" ? entry.id : void 0,
|
|
18833
|
-
className,
|
|
18834
|
-
label: typeof entry?.label === "string" && entry.label.trim() ? entry.label.trim() : typeof entry?.id === "string" && entry.id.trim() ? entry.id.trim() : entryPath,
|
|
18835
|
-
...displayGroupId ? {
|
|
18836
|
-
displayGroupId,
|
|
18837
|
-
displayGroupIndex: index,
|
|
18838
|
-
displayGroupSize: entryMetadata.length,
|
|
18839
|
-
...className && (entryClassCounts.get(className) || 0) > 1 ? { displayGroupSameTypeSize: entryClassCounts.get(className) } : {}
|
|
18840
|
-
} : {},
|
|
18841
|
-
messageId,
|
|
18842
|
-
jobId,
|
|
18843
|
-
ts
|
|
18844
|
-
});
|
|
18845
|
-
}
|
|
18846
|
-
for (const listName of uniqueStrings(asArray2(show.listNames))) {
|
|
18847
|
-
const list = asRecord4(listsByName[listName]);
|
|
18848
|
-
pushReferent({
|
|
18849
|
-
id: `list:${listName}`,
|
|
18850
|
-
kind: "list",
|
|
18851
|
-
ref: listName,
|
|
18852
|
-
role: "assistant",
|
|
18853
|
-
source: "heap_objects",
|
|
18854
|
-
listName,
|
|
18855
|
-
className: typeof list?.className === "string" ? list.className : void 0,
|
|
18856
|
-
count: Array.isArray(list?.paths) ? list.paths.length : null,
|
|
18857
|
-
messageId,
|
|
18858
|
-
jobId,
|
|
18859
|
-
ts
|
|
18860
|
-
});
|
|
18861
|
-
}
|
|
18862
|
-
for (const variableName of uniqueStrings(
|
|
18863
|
-
asArray2(show.variableNames)
|
|
18864
|
-
)) {
|
|
18865
|
-
const variable = asRecord4(variablesByName[variableName]);
|
|
18866
|
-
const entryPath = typeof variable?.entryPath === "string" ? variable.entryPath : void 0;
|
|
18867
|
-
const listName = typeof variable?.listName === "string" ? variable.listName : void 0;
|
|
18868
|
-
const entry = entryPath ? asRecord4(entriesByPath[entryPath]) : null;
|
|
18869
|
-
const list = listName ? asRecord4(listsByName[listName]) : null;
|
|
18870
|
-
pushReferent({
|
|
18871
|
-
id: `variable:${variableName}`,
|
|
18872
|
-
kind: "variable",
|
|
18873
|
-
ref: variableName,
|
|
18874
|
-
role: "assistant",
|
|
18875
|
-
source: "heap_objects",
|
|
18876
|
-
variableName,
|
|
18877
|
-
variableKind: typeof variable?.kind === "string" ? variable.kind : void 0,
|
|
18878
|
-
entryPath,
|
|
18879
|
-
recordId: typeof entry?.id === "string" ? entry.id : void 0,
|
|
18880
|
-
listName,
|
|
18881
|
-
className: typeof variable?.className === "string" ? variable.className : typeof entry?.className === "string" ? entry.className : typeof list?.className === "string" ? list.className : void 0,
|
|
18882
|
-
label: typeof entry?.label === "string" && entry.label.trim() ? entry.label.trim() : typeof entry?.id === "string" && entry.id.trim() ? entry.id.trim() : null,
|
|
18883
|
-
count: variable?.kind === "list" && Array.isArray(list?.paths) ? list.paths.length : null,
|
|
18884
|
-
scalarValue: variable?.kind === "scalar" && (typeof variable.value === "string" || typeof variable.value === "number" || typeof variable.value === "boolean" || variable.value === null) ? variable.value : void 0,
|
|
18885
|
-
messageId,
|
|
18886
|
-
jobId,
|
|
18887
|
-
ts
|
|
18888
|
-
});
|
|
18889
|
-
}
|
|
18890
|
-
}
|
|
18891
|
-
return referents;
|
|
20246
|
+
return asArray(conversation?.referents).map((value) => asRecord4(value)).filter((value) => Boolean(value)).slice().sort((left, right) => (right.ts || 0) - (left.ts || 0));
|
|
18892
20247
|
}
|
|
18893
20248
|
function projectConversationReferentFocus(liveDoc) {
|
|
18894
20249
|
const heap = asRecord4(liveDoc?.heap);
|
|
@@ -18910,7 +20265,7 @@ function projectConversationReferentFocus(liveDoc) {
|
|
|
18910
20265
|
listCount += 1;
|
|
18911
20266
|
listNames.push(referent.listName);
|
|
18912
20267
|
const list = asRecord4(listsByName[referent.listName]);
|
|
18913
|
-
entryPaths.push(...
|
|
20268
|
+
entryPaths.push(...asArray(list?.paths).slice(0, 4));
|
|
18914
20269
|
continue;
|
|
18915
20270
|
}
|
|
18916
20271
|
if (referent.kind === "variable" && typeof referent.variableName === "string" && variableCount < 4) {
|
|
@@ -18922,7 +20277,7 @@ function projectConversationReferentFocus(liveDoc) {
|
|
|
18922
20277
|
if (typeof referent.listName === "string") {
|
|
18923
20278
|
listNames.push(referent.listName);
|
|
18924
20279
|
const list = asRecord4(listsByName[referent.listName]);
|
|
18925
|
-
entryPaths.push(...
|
|
20280
|
+
entryPaths.push(...asArray(list?.paths).slice(0, 4));
|
|
18926
20281
|
}
|
|
18927
20282
|
}
|
|
18928
20283
|
}
|
|
@@ -19100,12 +20455,12 @@ function projectWorkflowFocus(liveDoc, pendingPrompts = [], options) {
|
|
|
19100
20455
|
const openDecisionIds = [];
|
|
19101
20456
|
const openPromptIds = [];
|
|
19102
20457
|
for (const job of jobs) {
|
|
19103
|
-
for (const line of
|
|
20458
|
+
for (const line of asArray(job.actionSummary)) {
|
|
19104
20459
|
if (typeof line === "string" && line.trim()) {
|
|
19105
20460
|
actionSummaryLines.push(line.trim());
|
|
19106
20461
|
}
|
|
19107
20462
|
}
|
|
19108
|
-
for (const rawEvent of
|
|
20463
|
+
for (const rawEvent of asArray(job.actionTrace)) {
|
|
19109
20464
|
const event = asRecord4(rawEvent);
|
|
19110
20465
|
const details = asRecord4(event?.details);
|
|
19111
20466
|
const outcome = asRecord4(event?.outcome);
|
|
@@ -19202,7 +20557,7 @@ function projectWorkflowFocus(liveDoc, pendingPrompts = [], options) {
|
|
|
19202
20557
|
}
|
|
19203
20558
|
for (const listName of uniqueStrings(listNames)) {
|
|
19204
20559
|
const list = asRecord4(listsByName[listName]);
|
|
19205
|
-
for (const path of
|
|
20560
|
+
for (const path of asArray(list?.paths).slice(0, 4)) {
|
|
19206
20561
|
entryPaths.push(path);
|
|
19207
20562
|
}
|
|
19208
20563
|
}
|
|
@@ -19322,7 +20677,7 @@ function projectLoopSummary(liveDoc, pendingPrompts = [], options) {
|
|
|
19322
20677
|
id: typeof decision.decisionId === "string" ? decision.decisionId : "unknown",
|
|
19323
20678
|
title: typeof decision.title === "string" && decision.title.trim() ? decision.title.trim() : "Decision",
|
|
19324
20679
|
status,
|
|
19325
|
-
candidates: status === "open" ?
|
|
20680
|
+
candidates: status === "open" ? asArray(decision.candidates).slice(0, 5).map((candidate) => {
|
|
19326
20681
|
const record = asRecord4(candidate);
|
|
19327
20682
|
if (!record) return null;
|
|
19328
20683
|
return {
|
|
@@ -19480,7 +20835,7 @@ function projectHeapSummary(heap, options) {
|
|
|
19480
20835
|
type: entry.className || "unknown",
|
|
19481
20836
|
id: entry.id || null,
|
|
19482
20837
|
label: entry.label || entry.id || null,
|
|
19483
|
-
fields:
|
|
20838
|
+
fields: asArray(entry.fields).filter(
|
|
19484
20839
|
(field) => field?.name && field.name !== "_realId" && field.name !== "real_id"
|
|
19485
20840
|
).slice(0, 3).map((field) => ({
|
|
19486
20841
|
name: field.name,
|
|
@@ -19644,7 +20999,7 @@ function buildGranularAgentManualActionBlock(manualActionSummary) {
|
|
|
19644
20999
|
function projectSessionFileSummary(liveDoc) {
|
|
19645
21000
|
const files = asRecord4(liveDoc?.files);
|
|
19646
21001
|
const byId = asRecord4(files?.byId) || {};
|
|
19647
|
-
const order =
|
|
21002
|
+
const order = asArray(files?.order);
|
|
19648
21003
|
const items = order.map((fileId) => asRecord4(byId[fileId])).filter((file) => Boolean(file)).filter((file) => file.status !== "deleted").slice(0, 24).map((file) => ({
|
|
19649
21004
|
fileId: typeof file.fileId === "string" ? file.fileId : null,
|
|
19650
21005
|
filename: typeof file.filename === "string" ? file.filename : typeof file.safeFilename === "string" ? file.safeFilename : null,
|
|
@@ -19682,13 +21037,7 @@ function extractRuntimeContractExports(domainBlock) {
|
|
|
19682
21037
|
}
|
|
19683
21038
|
const actionPattern = /export\s+declare\s+function\s+([A-Za-z_$][\w$]*)/g;
|
|
19684
21039
|
for (const match of domainBlock.matchAll(actionPattern)) {
|
|
19685
|
-
|
|
19686
|
-
if (["agent_text_message", "agent_heap_objects", "agent_message"].includes(
|
|
19687
|
-
name
|
|
19688
|
-
)) {
|
|
19689
|
-
continue;
|
|
19690
|
-
}
|
|
19691
|
-
actions.add(name);
|
|
21040
|
+
actions.add(match[1]);
|
|
19692
21041
|
}
|
|
19693
21042
|
return {
|
|
19694
21043
|
classes: Array.from(classes).sort(),
|
|
@@ -19745,7 +21094,7 @@ function buildGranularAgentRuntimeImportsBlock(input) {
|
|
|
19745
21094
|
importStyle: "named ESM imports only",
|
|
19746
21095
|
exports: ["replyToUser", "showObjects", "showAgentResponse"],
|
|
19747
21096
|
contains: "User-facing Harness response helpers for text, grounded object displays, and combined responses.",
|
|
19748
|
-
rule: "Import reply/display helpers from this module;
|
|
21097
|
+
rule: "Import reply/display helpers from this module; only the listed exports are available."
|
|
19749
21098
|
},
|
|
19750
21099
|
[HARNESS_V3_SESSION_MODULE]: {
|
|
19751
21100
|
importStyle: "named ESM imports only",
|
|
@@ -19986,7 +21335,7 @@ function summarizeObjectSchema(schema) {
|
|
|
19986
21335
|
if (!properties || Object.keys(properties).length === 0) {
|
|
19987
21336
|
return record ? "{}" : null;
|
|
19988
21337
|
}
|
|
19989
|
-
const required = new Set(
|
|
21338
|
+
const required = new Set(asArray(record?.required));
|
|
19990
21339
|
const fields = Object.entries(properties).slice(0, 8).map(([name, property]) => {
|
|
19991
21340
|
const marker = required.has(name) ? "*" : "?";
|
|
19992
21341
|
return `${name}${marker}: ${jsonSchemaTypeName(property)}`;
|
|
@@ -20023,9 +21372,9 @@ function splitDomainDocumentation(domainDocumentation) {
|
|
|
20023
21372
|
return { types: normalized, docs: "" };
|
|
20024
21373
|
}
|
|
20025
21374
|
var DOMAIN_HELPER_FUNCTION_NAMES = /* @__PURE__ */ new Set([
|
|
20026
|
-
"
|
|
20027
|
-
"
|
|
20028
|
-
"
|
|
21375
|
+
"replyToUser",
|
|
21376
|
+
"showAgentResponse",
|
|
21377
|
+
"showObjects"
|
|
20029
21378
|
]);
|
|
20030
21379
|
function inferGlobalActionToolsFromDomainTypes(domainTypes) {
|
|
20031
21380
|
const inferred = [];
|
|
@@ -20161,7 +21510,7 @@ function buildKnownFactsFromCheckpoint(checkpoint) {
|
|
|
20161
21510
|
return facts.slice(0, 8);
|
|
20162
21511
|
}
|
|
20163
21512
|
function buildGranularAgentSystemPrompt(input) {
|
|
20164
|
-
const outputMode = input.outputMode || "
|
|
21513
|
+
const outputMode = input.outputMode || "feed";
|
|
20165
21514
|
const promptCapabilities = resolvePromptCapabilities(input.capabilities);
|
|
20166
21515
|
const domainSections = splitDomainDocumentation(input.domainDocumentation);
|
|
20167
21516
|
const promptTools = resolvePromptTools(input.tools, domainSections.types);
|
|
@@ -20194,7 +21543,7 @@ function buildGranularAgentSystemPrompt(input) {
|
|
|
20194
21543
|
- For multi-record display, prefer a saved list/listName so the UI can render a table; use entryPaths for a few individual records.
|
|
20195
21544
|
- When the user asks to show, list, display, open, or "show them" for records you found, include those grounded records in \`show\`; do not answer only with a count or text summary.
|
|
20196
21545
|
- For count-only questions such as "how many", "how many X do I have", or "what is the total number of X", call the entity \`.count(...)\` or use page \`totalCount\` only when a page is already needed for other reasons. Answer with text only. Do not fetch, save, or display sample records just to ground a numeric count.
|
|
20197
|
-
- Use \`replyToUser(...)\`, \`showObjects(...)\`, or \`showAgentResponse(...)\` from \`@granular/agent\` when the host exposes job output helpers
|
|
21546
|
+
- Use \`replyToUser(...)\`, \`showObjects(...)\`, or \`showAgentResponse(...)\` from \`@granular/agent\` when the host exposes job output helpers.` : `- End every user-facing job by returning a short natural-language string.` : promptCapabilities.showRecords ? `- Every job that answers the user must emit \`replyToUser(...)\`, \`showObjects(...)\`, and/or \`showAgentResponse(...)\` from \`@granular/agent\`.
|
|
20198
21547
|
- \`replyToUser(...)\` displays text directly to the user in the host UI. Treat it as the user-facing progress and reply channel, not as a debug log.
|
|
20199
21548
|
- For long-running or multi-step jobs, send several short \`replyToUser(...)\` updates as useful milestones are reached so the user can see what is happening instead of waiting in silence.
|
|
20200
21549
|
- Write \`replyToUser(...)\` content in a friendly, readable product-assistant style: concrete, concise, and natural. Avoid robotic status dumps, raw implementation names, and unexplained IDs unless the ID helps the user.
|
|
@@ -20204,11 +21553,10 @@ function buildGranularAgentSystemPrompt(input) {
|
|
|
20204
21553
|
- Do not use \`showObjects({ entries: [...] })\` or \`showObjects({ saveAs, entries })\`. Save ordered pages, queues, search results, or ranked lists with \`groundedObjects.save(...)\`, then call \`showObjects({ variableNames: [...] })\` once.
|
|
20205
21554
|
- Use \`showAgentResponse({ reply, show: [record, action] })\` when one assistant message should combine text, grounded records, files, prepared actions, or action suggestions. The \`action\` can be a state handle such as \`record.lifecycle.approved\` or an action handle such as \`record.lifecycle.approved.reach()\`.
|
|
20206
21555
|
- Pass grounded records directly in \`show\` when the default record presentation answers the request. When the user asks for particular columns, comparisons, or computed values, import \`table\` (and \`relativeTime\` when useful) from \`@granular/agent\` and call \`showAgentResponse({ reply, show: table(records, [{ label: "Object", value: record => record.label }, { label: "When", value: record => relativeTime(record.timestamp) }]) })\`. Column callbacks must be synchronous and return a scalar, \`Date\`, or \`relativeTime(...)\`; they run inside the job and only resolved cells are persisted.
|
|
20207
|
-
- Do not use deprecated side-channel helpers such as \`agent_text_message(...)\`, \`agent_heap_objects(...)\`, or \`agent_message(...)\` unless the generated types expose no Harness v3 helper alternative.
|
|
20208
21556
|
- When the user asks to show, list, display, open, or "show them" for records you found, call \`showObjects(...)\`; do not answer only with a count or text summary.
|
|
20209
21557
|
- For count-only questions such as "how many", "how many X do I have", or "what is the total number of X", call the entity \`.count(...)\` or use page \`totalCount\` only when a page is already needed for other reasons. Answer with \`replyToUser(...)\` only. Do not call \`showObjects(...)\`, \`saveAs\`, or \`groundedObjects.save(...)\` unless the user also asked to see records or a later requested action needs a reusable record selection.
|
|
20210
21558
|
- Use stable saved list names that preserve identity and ordering so later references such as "the second item" or "back on the first slice" resolve to the correct earlier slice, not merely the most recent record.
|
|
20211
|
-
- Do not rely on the final return value for UI output. Do not return ad-hoc \`reply\` / \`show\` payloads instead of explicit
|
|
21559
|
+
- Do not rely on the final return value for UI output. Do not return ad-hoc \`reply\` / \`show\` payloads instead of explicit canonical feed calls.` : `- Every job that answers the user must emit \`replyToUser(...)\` from \`@granular/agent\`.
|
|
20212
21560
|
- \`replyToUser(...)\` displays text directly to the user in the host UI. Treat it as the user-facing progress and reply channel, not as a debug log.
|
|
20213
21561
|
- For long-running or multi-step jobs, send several short \`replyToUser(...)\` updates as useful milestones are reached so the user can see what is happening instead of waiting in silence.
|
|
20214
21562
|
- Write \`replyToUser(...)\` content in a friendly, readable product-assistant style: concrete, concise, and natural. Avoid robotic status dumps, raw implementation names, and unexplained IDs unless the ID helps the user.
|
|
@@ -20708,6 +22056,231 @@ function buildContinuationInstructionFromTemplate(resultPreview, options) {
|
|
|
20708
22056
|
return renderContinuationInstructionFromTemplate(resultPreview, options).instruction;
|
|
20709
22057
|
}
|
|
20710
22058
|
|
|
22059
|
+
// src/job-presentation.ts
|
|
22060
|
+
var ENTRY_KEY_CANDIDATES = ["entryPath", "path"];
|
|
22061
|
+
var ENTRY_ARRAY_KEY_CANDIDATES = ["entryPaths", "paths"];
|
|
22062
|
+
var LIST_KEY_CANDIDATES = ["listName"];
|
|
22063
|
+
var LIST_ARRAY_KEY_CANDIDATES = ["listNames"];
|
|
22064
|
+
var VARIABLE_KEY_CANDIDATES = ["variableName"];
|
|
22065
|
+
var VARIABLE_ARRAY_KEY_CANDIDATES = ["variableNames"];
|
|
22066
|
+
var UI_CONTAINER_KEYS = ["show", "display", "present", "ui"];
|
|
22067
|
+
function asRecord5(value) {
|
|
22068
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
22069
|
+
return value;
|
|
22070
|
+
}
|
|
22071
|
+
function responseTextFromFeedItems(feedItems, jobId) {
|
|
22072
|
+
let latestSequence = -1;
|
|
22073
|
+
let latestText = null;
|
|
22074
|
+
for (const item of feedItems) {
|
|
22075
|
+
if (item.kind !== "message" || item.payload.role !== "assistant" || item.source?.jobId !== jobId || item.sequence <= latestSequence) {
|
|
22076
|
+
continue;
|
|
22077
|
+
}
|
|
22078
|
+
const text = item.payload.text.trim();
|
|
22079
|
+
if (!text) continue;
|
|
22080
|
+
latestSequence = item.sequence;
|
|
22081
|
+
latestText = text;
|
|
22082
|
+
}
|
|
22083
|
+
return latestText;
|
|
22084
|
+
}
|
|
22085
|
+
function pushString(target, value) {
|
|
22086
|
+
if (typeof value === "string" && value.trim()) {
|
|
22087
|
+
target.add(value.trim());
|
|
22088
|
+
}
|
|
22089
|
+
}
|
|
22090
|
+
function pushStringArray(target, value) {
|
|
22091
|
+
if (!Array.isArray(value)) return;
|
|
22092
|
+
for (const item of value) {
|
|
22093
|
+
pushString(target, item);
|
|
22094
|
+
}
|
|
22095
|
+
}
|
|
22096
|
+
function collectReferencesFromRecord(record, refs) {
|
|
22097
|
+
for (const key of ENTRY_KEY_CANDIDATES)
|
|
22098
|
+
pushString(refs.entryPaths, record[key]);
|
|
22099
|
+
for (const key of ENTRY_ARRAY_KEY_CANDIDATES)
|
|
22100
|
+
pushStringArray(refs.entryPaths, record[key]);
|
|
22101
|
+
for (const key of LIST_KEY_CANDIDATES)
|
|
22102
|
+
pushString(refs.listNames, record[key]);
|
|
22103
|
+
for (const key of LIST_ARRAY_KEY_CANDIDATES)
|
|
22104
|
+
pushStringArray(refs.listNames, record[key]);
|
|
22105
|
+
for (const key of VARIABLE_KEY_CANDIDATES)
|
|
22106
|
+
pushString(refs.variableNames, record[key]);
|
|
22107
|
+
for (const key of VARIABLE_ARRAY_KEY_CANDIDATES)
|
|
22108
|
+
pushStringArray(refs.variableNames, record[key]);
|
|
22109
|
+
}
|
|
22110
|
+
function stringValue(record, keys) {
|
|
22111
|
+
for (const key of keys) {
|
|
22112
|
+
const value = record[key];
|
|
22113
|
+
if (typeof value === "string" && value.trim()) {
|
|
22114
|
+
return value.trim();
|
|
22115
|
+
}
|
|
22116
|
+
}
|
|
22117
|
+
return null;
|
|
22118
|
+
}
|
|
22119
|
+
function findEntryPathForRecord(record, heap) {
|
|
22120
|
+
const directPath = stringValue(record, ["entryPath", "path"]);
|
|
22121
|
+
if (directPath && heap.entriesByPath?.[directPath]) {
|
|
22122
|
+
return directPath;
|
|
22123
|
+
}
|
|
22124
|
+
const id = stringValue(record, ["id", "_id", "recordId", "objectId"]);
|
|
22125
|
+
if (!id) {
|
|
22126
|
+
return null;
|
|
22127
|
+
}
|
|
22128
|
+
const className = stringValue(record, [
|
|
22129
|
+
"className",
|
|
22130
|
+
"_className",
|
|
22131
|
+
"__className",
|
|
22132
|
+
"prototype",
|
|
22133
|
+
"type"
|
|
22134
|
+
]);
|
|
22135
|
+
const entries = Object.values(heap.entriesByPath || {});
|
|
22136
|
+
const exact = entries.find(
|
|
22137
|
+
(entry) => entry.id === id && (!className || entry.className === className || entry.prototypes?.includes(className))
|
|
22138
|
+
);
|
|
22139
|
+
if (exact?.path) {
|
|
22140
|
+
return exact.path;
|
|
22141
|
+
}
|
|
22142
|
+
const idOnlyMatches = entries.filter((entry) => entry.id === id);
|
|
22143
|
+
return idOnlyMatches.length === 1 ? idOnlyMatches[0].path : null;
|
|
22144
|
+
}
|
|
22145
|
+
function scanForHeapReferences(value, heap, refs, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
22146
|
+
if (value === null || value === void 0 || depth > 4 || seen.has(value))
|
|
22147
|
+
return;
|
|
22148
|
+
if (typeof value === "string") {
|
|
22149
|
+
const trimmed = value.trim();
|
|
22150
|
+
if (heap.entriesByPath?.[trimmed]) refs.entryPaths.add(trimmed);
|
|
22151
|
+
if (heap.listsByName?.[trimmed]) refs.listNames.add(trimmed);
|
|
22152
|
+
if (heap.variablesByName?.[trimmed]) refs.variableNames.add(trimmed);
|
|
22153
|
+
return;
|
|
22154
|
+
}
|
|
22155
|
+
if (Array.isArray(value)) {
|
|
22156
|
+
seen.add(value);
|
|
22157
|
+
for (const item of value.slice(0, 24)) {
|
|
22158
|
+
scanForHeapReferences(item, heap, refs, depth + 1, seen);
|
|
22159
|
+
}
|
|
22160
|
+
return;
|
|
22161
|
+
}
|
|
22162
|
+
const record = asRecord5(value);
|
|
22163
|
+
if (!record) return;
|
|
22164
|
+
seen.add(value);
|
|
22165
|
+
const entryPath = findEntryPathForRecord(record, heap);
|
|
22166
|
+
if (entryPath) refs.entryPaths.add(entryPath);
|
|
22167
|
+
collectReferencesFromRecord(record, refs);
|
|
22168
|
+
for (const key of UI_CONTAINER_KEYS) {
|
|
22169
|
+
const nested = asRecord5(record[key]);
|
|
22170
|
+
if (nested) collectReferencesFromRecord(nested, refs);
|
|
22171
|
+
}
|
|
22172
|
+
for (const nested of Object.values(record).slice(0, 24)) {
|
|
22173
|
+
scanForHeapReferences(nested, heap, refs, depth + 1, seen);
|
|
22174
|
+
}
|
|
22175
|
+
}
|
|
22176
|
+
function resolveVariablesToReferences(variableNames, heap, refs) {
|
|
22177
|
+
for (const variableName of variableNames) {
|
|
22178
|
+
const variable = heap.variablesByName?.[variableName];
|
|
22179
|
+
if (!variable) continue;
|
|
22180
|
+
if (variable.kind === "entry" && variable.entryPath) {
|
|
22181
|
+
refs.entryPaths.add(variable.entryPath);
|
|
22182
|
+
}
|
|
22183
|
+
if (variable.kind === "list" && variable.listName) {
|
|
22184
|
+
refs.listNames.add(variable.listName);
|
|
22185
|
+
}
|
|
22186
|
+
}
|
|
22187
|
+
}
|
|
22188
|
+
function sortEntries(entries) {
|
|
22189
|
+
return [...entries].sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
22190
|
+
}
|
|
22191
|
+
function sortLists(lists) {
|
|
22192
|
+
return [...lists].sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
22193
|
+
}
|
|
22194
|
+
function dedupeEntries(entries) {
|
|
22195
|
+
const seen = /* @__PURE__ */ new Set();
|
|
22196
|
+
const result = [];
|
|
22197
|
+
for (const entry of entries) {
|
|
22198
|
+
if (!entry?.path || seen.has(entry.path)) continue;
|
|
22199
|
+
seen.add(entry.path);
|
|
22200
|
+
result.push(entry);
|
|
22201
|
+
}
|
|
22202
|
+
return result;
|
|
22203
|
+
}
|
|
22204
|
+
function dedupeLists(lists) {
|
|
22205
|
+
const seen = /* @__PURE__ */ new Set();
|
|
22206
|
+
const result = [];
|
|
22207
|
+
for (const list of lists) {
|
|
22208
|
+
if (!list?.name || seen.has(list.name)) continue;
|
|
22209
|
+
seen.add(list.name);
|
|
22210
|
+
result.push(list);
|
|
22211
|
+
}
|
|
22212
|
+
return result;
|
|
22213
|
+
}
|
|
22214
|
+
function getJobRelatedEntries(heap, jobId) {
|
|
22215
|
+
return sortEntries(
|
|
22216
|
+
Object.values(heap.entriesByPath || {}).filter(
|
|
22217
|
+
(entry) => entry.relatedJobIds?.includes(jobId)
|
|
22218
|
+
)
|
|
22219
|
+
);
|
|
22220
|
+
}
|
|
22221
|
+
function getJobRelatedLists(heap, jobId) {
|
|
22222
|
+
return sortLists(
|
|
22223
|
+
Object.values(heap.listsByName || {}).filter(
|
|
22224
|
+
(list) => list.relatedJobIds?.includes(jobId)
|
|
22225
|
+
)
|
|
22226
|
+
);
|
|
22227
|
+
}
|
|
22228
|
+
function entriesFromLists(lists, heap) {
|
|
22229
|
+
const entries = [];
|
|
22230
|
+
for (const list of lists) {
|
|
22231
|
+
for (const path of list.paths || []) {
|
|
22232
|
+
const entry = heap.entriesByPath?.[path];
|
|
22233
|
+
if (entry) entries.push(entry);
|
|
22234
|
+
}
|
|
22235
|
+
}
|
|
22236
|
+
return entries;
|
|
22237
|
+
}
|
|
22238
|
+
function resolveJobPresentation({
|
|
22239
|
+
jobId,
|
|
22240
|
+
result,
|
|
22241
|
+
feedItems = [],
|
|
22242
|
+
sessionHeap,
|
|
22243
|
+
allowExplicitArtifacts = true
|
|
22244
|
+
}) {
|
|
22245
|
+
const refs = {
|
|
22246
|
+
entryPaths: /* @__PURE__ */ new Set(),
|
|
22247
|
+
listNames: /* @__PURE__ */ new Set(),
|
|
22248
|
+
variableNames: /* @__PURE__ */ new Set()
|
|
22249
|
+
};
|
|
22250
|
+
if (allowExplicitArtifacts) {
|
|
22251
|
+
scanForHeapReferences(result, sessionHeap, refs);
|
|
22252
|
+
resolveVariablesToReferences(refs.variableNames, sessionHeap, refs);
|
|
22253
|
+
}
|
|
22254
|
+
const referencedLists = sortLists(
|
|
22255
|
+
[...refs.listNames].map((name) => sessionHeap.listsByName?.[name]).filter((list) => Boolean(list))
|
|
22256
|
+
);
|
|
22257
|
+
const referencedEntries = sortEntries(
|
|
22258
|
+
[...refs.entryPaths].map((path) => sessionHeap.entriesByPath?.[path]).filter((entry) => Boolean(entry))
|
|
22259
|
+
);
|
|
22260
|
+
const jobLists = getJobRelatedLists(sessionHeap, jobId);
|
|
22261
|
+
const jobEntries = getJobRelatedEntries(sessionHeap, jobId);
|
|
22262
|
+
const changedEntries = dedupeEntries([
|
|
22263
|
+
...jobEntries,
|
|
22264
|
+
...entriesFromLists(jobLists, sessionHeap)
|
|
22265
|
+
]);
|
|
22266
|
+
const explicitLists = dedupeLists(referencedLists);
|
|
22267
|
+
const explicitEntries = dedupeEntries([
|
|
22268
|
+
...referencedEntries,
|
|
22269
|
+
...entriesFromLists(referencedLists, sessionHeap)
|
|
22270
|
+
]);
|
|
22271
|
+
const hasExplicitArtifacts = allowExplicitArtifacts && (explicitEntries.length > 0 || explicitLists.length > 0);
|
|
22272
|
+
const lists = hasExplicitArtifacts ? explicitLists : jobLists;
|
|
22273
|
+
const entries = hasExplicitArtifacts ? explicitEntries : changedEntries;
|
|
22274
|
+
return {
|
|
22275
|
+
responseText: responseTextFromFeedItems(feedItems, jobId),
|
|
22276
|
+
entries,
|
|
22277
|
+
lists,
|
|
22278
|
+
changedEntries,
|
|
22279
|
+
changedLists: jobLists,
|
|
22280
|
+
hasExplicitArtifacts
|
|
22281
|
+
};
|
|
22282
|
+
}
|
|
22283
|
+
|
|
20711
22284
|
// src/openai-usage.ts
|
|
20712
22285
|
var OPENAI_GPT_5_4_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
|
|
20713
22286
|
var OPENAI_GPT_5_6_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/pricing";
|
|
@@ -20727,22 +22300,22 @@ var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
|
|
|
20727
22300
|
provider: "openai",
|
|
20728
22301
|
model: "gpt-5.6-luna",
|
|
20729
22302
|
currency: "USD",
|
|
20730
|
-
inputUsdPerMillion:
|
|
20731
|
-
cachedInputUsdPerMillion: 0.
|
|
20732
|
-
cacheWriteUsdPerMillion:
|
|
20733
|
-
outputUsdPerMillion:
|
|
22303
|
+
inputUsdPerMillion: 0.2,
|
|
22304
|
+
cachedInputUsdPerMillion: 0.02,
|
|
22305
|
+
cacheWriteUsdPerMillion: 0.25,
|
|
22306
|
+
outputUsdPerMillion: 1.2,
|
|
20734
22307
|
sourceUrl: OPENAI_GPT_5_6_PRICING_SOURCE_URL,
|
|
20735
|
-
effectiveDate: "2026-07-
|
|
22308
|
+
effectiveDate: "2026-07-30",
|
|
20736
22309
|
longContextThresholdTokens: OPENAI_LONG_CONTEXT_THRESHOLD_TOKENS,
|
|
20737
22310
|
longContextPricing: {
|
|
20738
|
-
inputUsdPerMillion:
|
|
20739
|
-
cachedInputUsdPerMillion: 0.
|
|
20740
|
-
cacheWriteUsdPerMillion:
|
|
20741
|
-
outputUsdPerMillion:
|
|
22311
|
+
inputUsdPerMillion: 0.4,
|
|
22312
|
+
cachedInputUsdPerMillion: 0.04,
|
|
22313
|
+
cacheWriteUsdPerMillion: 0.5,
|
|
22314
|
+
outputUsdPerMillion: 1.8
|
|
20742
22315
|
}
|
|
20743
22316
|
}
|
|
20744
22317
|
};
|
|
20745
|
-
function
|
|
22318
|
+
function asRecord6(value) {
|
|
20746
22319
|
return value && typeof value === "object" ? value : null;
|
|
20747
22320
|
}
|
|
20748
22321
|
function numberField(record, key) {
|
|
@@ -20766,7 +22339,7 @@ function getOpenAIModelPricing(model, inputTokens = 0) {
|
|
|
20766
22339
|
return { ...pricing, contextTier: "short" };
|
|
20767
22340
|
}
|
|
20768
22341
|
function normalizeOpenAIUsage(rawUsage) {
|
|
20769
|
-
const usage =
|
|
22342
|
+
const usage = asRecord6(rawUsage);
|
|
20770
22343
|
if (!usage) {
|
|
20771
22344
|
return {
|
|
20772
22345
|
inputTokens: 0,
|
|
@@ -20781,8 +22354,8 @@ function normalizeOpenAIUsage(rawUsage) {
|
|
|
20781
22354
|
const inputTokens = numberField(usage, "prompt_tokens") || numberField(usage, "input_tokens");
|
|
20782
22355
|
const outputTokens = numberField(usage, "completion_tokens") || numberField(usage, "output_tokens");
|
|
20783
22356
|
const totalTokens = numberField(usage, "total_tokens") || inputTokens + outputTokens;
|
|
20784
|
-
const inputDetails =
|
|
20785
|
-
const outputDetails =
|
|
22357
|
+
const inputDetails = asRecord6(usage.prompt_tokens_details) || asRecord6(usage.input_tokens_details);
|
|
22358
|
+
const outputDetails = asRecord6(usage.completion_tokens_details) || asRecord6(usage.output_tokens_details);
|
|
20786
22359
|
const cachedInputTokens = Math.min(
|
|
20787
22360
|
inputTokens,
|
|
20788
22361
|
numberField(inputDetails, "cached_tokens") || numberField(inputDetails, "cached_input_tokens")
|
|
@@ -20850,6 +22423,6 @@ function calculateOpenAITokenSpend(model, rawUsage) {
|
|
|
20850
22423
|
};
|
|
20851
22424
|
}
|
|
20852
22425
|
|
|
20853
|
-
export { Environment, EnvironmentSession, Granular, OPENAI_MODEL_PRICING_USD_PER_MILLION, OntologyHandle, Session, WSClient, buildContinuationInstruction, buildContinuationInstructionFromTemplate, buildGranularAgentCheckpointBlock, buildGranularAgentDomainBlock, buildGranularAgentFileBlock, buildGranularAgentHeapBlock, buildGranularAgentLoopBlock, buildGranularAgentManualActionBlock, buildGranularAgentManualActionMemorySummary, buildGranularAgentReferentBlock, buildGranularAgentRuntimeImportsBlock, buildGranularAgentSessionBlock, buildGranularAgentSystemPrompt, buildGranularAgentSystemPromptFromTemplate, buildGranularAgentToolBlock, buildGranularAgentWorkflowBlock, buildOpenAISpendEventId, buildSessionTranscript, calculateOpenAITokenSpend, consumeGranularReasoningOnlyChunk, consumeGranularReasoningTraceChunk, createHarnessVerifierSnapshot, evaluateContinuation, evaluateValidationRule, extractPromptTokens, getCurrentClosureId, getDefaultHarnessTemplateId, getExclusivePromptTarget, getOpenAIModelPricing, hasOpenPrompt, hashHarnessTemplateValue, invokeRegisteredEffect, isLocalApiUrl, listHarnessTemplates, normalizeEffectBehaviors, normalizeOpenAIUsage, normalizePrompt, normalizePromptChoiceOption, normalizePromptText, normalizePromptType, projectConversationReferentFocus, projectConversationReferentSummary, projectHeapSummary, projectLoopSummary, projectSessionFileSummary, projectWorkflowFocus, projectWorkflowSummary, recordOpenAIUsageSpend, renderContinuationInstructionFromTemplate, renderGranularAgentSystemPromptFromTemplate, resolveApiUrl, resolveAuthTokenForApiUrl, resolveHarnessTemplate, resolveJobPresentation, resolvePromptAnswer, reviewGeneratedJobCode, scorePromptChoiceMatch, stripGranularReasoningTrace, toGranularHttpBase, validateHarnessTemplateManifest, validationRuleFailureMessage };
|
|
22426
|
+
export { Environment, EnvironmentSession, GRANULAR_FEED_DIAGNOSTIC_EVENT, Granular, OPENAI_MODEL_PRICING_USD_PER_MILLION, OntologyHandle, Session, SessionFeedController, WSClient, buildContinuationInstruction, buildContinuationInstructionFromTemplate, buildGranularAgentCheckpointBlock, buildGranularAgentDomainBlock, buildGranularAgentFileBlock, buildGranularAgentHeapBlock, buildGranularAgentLoopBlock, buildGranularAgentManualActionBlock, buildGranularAgentManualActionMemorySummary, buildGranularAgentReferentBlock, buildGranularAgentRuntimeImportsBlock, buildGranularAgentSessionBlock, buildGranularAgentSystemPrompt, buildGranularAgentSystemPromptFromTemplate, buildGranularAgentToolBlock, buildGranularAgentWorkflowBlock, buildOpenAISpendEventId, buildSessionTranscript, buildSessionTranscriptFromFeedItems, calculateOpenAITokenSpend, consumeGranularReasoningOnlyChunk, consumeGranularReasoningTraceChunk, createFeedPublisher, createHarnessVerifierSnapshot, emitFeedDiagnostic, emitFeedDiagnosticToDefaultSink, emptyFeedSnapshot, evaluateContinuation, evaluateValidationRule, extractPromptTokens, getCurrentClosureId, getDefaultHarnessTemplateId, getExclusivePromptTarget, getOpenAIModelPricing, hasCanonicalSessionFeedActivation, hasOpenPrompt, hashHarnessTemplateValue, invokeRegisteredEffect, isCanonicalSessionFeedDocument, isLocalApiUrl, listHarnessTemplates, mergeFeedItemsBySequence, normalizeEffectBehaviors, normalizeFeedDiagnostic, normalizeFeedDiagnosticKind, normalizeFeedPage, normalizeOpenAIUsage, normalizePrompt, normalizePromptChoiceOption, normalizePromptText, normalizePromptType, orderTransientFeedItems, projectConversationReferentFocus, projectConversationReferentSummary, projectHeapSummary, projectLoopSummary, projectSessionFileSummary, projectWorkflowFocus, projectWorkflowSummary, readSessionFeedSnapshot, recordOpenAIUsageSpend, renderContinuationInstructionFromTemplate, renderGranularAgentSystemPromptFromTemplate, resolveApiUrl, resolveAuthTokenForApiUrl, resolveHarnessTemplate, resolveJobPresentation, resolvePromptAnswer, reviewGeneratedJobCode, scorePromptChoiceMatch, stripGranularReasoningTrace, toGranularHttpBase, validateHarnessTemplateManifest, validationRuleFailureMessage };
|
|
20854
22427
|
//# sourceMappingURL=index.mjs.map
|
|
20855
22428
|
//# sourceMappingURL=index.mjs.map
|