@granular-software/sdk 0.4.58 → 0.4.59
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/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-BHhqFMeU.d.ts} +90 -17
- package/dist/{client-UdGt44dF.d.ts → client-aWpvZfX4.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-3DNM1u9N.d.mts} +655 -98
- package/dist/{spend-czRX7l0X.d.ts → spend-3DNM1u9N.d.ts} +655 -98
- package/dist/spend.d.mts +1 -1
- package/dist/spend.d.ts +1 -1
- package/package.json +5 -2
package/dist/index.js
CHANGED
|
@@ -4011,6 +4011,1285 @@ var init_wrapper = __esm({
|
|
|
4011
4011
|
wrapper_default = import_websocket.default;
|
|
4012
4012
|
}
|
|
4013
4013
|
});
|
|
4014
|
+
|
|
4015
|
+
// src/feed.ts
|
|
4016
|
+
function requirePositiveFeedPosition(value) {
|
|
4017
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 1) {
|
|
4018
|
+
throw new Error("Invalid feed chronology.");
|
|
4019
|
+
}
|
|
4020
|
+
return value;
|
|
4021
|
+
}
|
|
4022
|
+
function mergeFeedItemsBySequence(...collections) {
|
|
4023
|
+
const bySequence = /* @__PURE__ */ new Map();
|
|
4024
|
+
const sequenceById = /* @__PURE__ */ new Map();
|
|
4025
|
+
for (const collection of collections) {
|
|
4026
|
+
let priorSequence = 0;
|
|
4027
|
+
for (const item of collection) {
|
|
4028
|
+
const sequence = requirePositiveFeedPosition(item.sequence);
|
|
4029
|
+
if (sequence <= priorSequence || priorSequence > 0 && sequence !== priorSequence + 1) {
|
|
4030
|
+
throw new Error("Invalid feed chronology.");
|
|
4031
|
+
}
|
|
4032
|
+
priorSequence = sequence;
|
|
4033
|
+
const id = String(item.id || "");
|
|
4034
|
+
const knownSequence = sequenceById.get(id);
|
|
4035
|
+
const existing = bySequence.get(sequence);
|
|
4036
|
+
if (!id || knownSequence !== void 0 && knownSequence !== sequence || existing && existing.id !== id) {
|
|
4037
|
+
throw new Error("Invalid feed chronology.");
|
|
4038
|
+
}
|
|
4039
|
+
if (existing && !sameDurableItem(existing, item)) {
|
|
4040
|
+
throw new Error(
|
|
4041
|
+
"Invalid feed chronology: immutable occurrence collision."
|
|
4042
|
+
);
|
|
4043
|
+
}
|
|
4044
|
+
sequenceById.set(id, sequence);
|
|
4045
|
+
if (!existing) bySequence.set(sequence, item);
|
|
4046
|
+
}
|
|
4047
|
+
}
|
|
4048
|
+
return [...bySequence.entries()].sort(([left], [right]) => left - right).map(([, item]) => item);
|
|
4049
|
+
}
|
|
4050
|
+
function orderTransientFeedItems(items) {
|
|
4051
|
+
const byId = /* @__PURE__ */ new Map();
|
|
4052
|
+
const ordinalById = /* @__PURE__ */ new Map();
|
|
4053
|
+
const idByOrdinal = /* @__PURE__ */ new Map();
|
|
4054
|
+
for (const item of items) {
|
|
4055
|
+
const id = String(item.id || "");
|
|
4056
|
+
const ordinal = requirePositiveFeedPosition(item.ordinal);
|
|
4057
|
+
const knownOrdinal = ordinalById.get(id);
|
|
4058
|
+
const ordinalOwner = idByOrdinal.get(ordinal);
|
|
4059
|
+
if (!id || knownOrdinal !== void 0 && knownOrdinal !== ordinal || ordinalOwner && ordinalOwner !== id) {
|
|
4060
|
+
throw new Error("Invalid feed chronology.");
|
|
4061
|
+
}
|
|
4062
|
+
ordinalById.set(id, ordinal);
|
|
4063
|
+
idByOrdinal.set(ordinal, id);
|
|
4064
|
+
const existing = byId.get(id);
|
|
4065
|
+
if (existing && item.revision === existing.revision && stableValueFingerprint(item) !== stableValueFingerprint(existing)) {
|
|
4066
|
+
throw new Error(
|
|
4067
|
+
"Invalid feed chronology: conflicting transient revision."
|
|
4068
|
+
);
|
|
4069
|
+
}
|
|
4070
|
+
if (!existing || item.revision > existing.revision) byId.set(id, item);
|
|
4071
|
+
}
|
|
4072
|
+
return [...byId.values()].sort((left, right) => left.ordinal - right.ordinal);
|
|
4073
|
+
}
|
|
4074
|
+
var GRANULAR_FEED_DIAGNOSTIC_EVENT = "granular:feed-diagnostic";
|
|
4075
|
+
function normalizeFeedDiagnosticKind(kind) {
|
|
4076
|
+
return /^[a-z][a-z0-9_]{0,63}$/.test(kind) ? kind : "invalid_unknown_kind";
|
|
4077
|
+
}
|
|
4078
|
+
function normalizeFeedDiagnostic(diagnostic) {
|
|
4079
|
+
if (diagnostic.type !== "unknown_kind") return diagnostic;
|
|
4080
|
+
const kind = normalizeFeedDiagnosticKind(diagnostic.kind);
|
|
4081
|
+
return kind === diagnostic.kind ? diagnostic : { ...diagnostic, kind };
|
|
4082
|
+
}
|
|
4083
|
+
function emitFeedDiagnosticToDefaultSink(diagnostic, target = globalThis) {
|
|
4084
|
+
try {
|
|
4085
|
+
const normalized = normalizeFeedDiagnostic(diagnostic);
|
|
4086
|
+
const EventConstructor = target.CustomEvent || globalThis.CustomEvent;
|
|
4087
|
+
if (typeof target.dispatchEvent !== "function" || typeof EventConstructor !== "function") {
|
|
4088
|
+
return;
|
|
4089
|
+
}
|
|
4090
|
+
target.dispatchEvent(
|
|
4091
|
+
new EventConstructor(GRANULAR_FEED_DIAGNOSTIC_EVENT, {
|
|
4092
|
+
detail: Object.freeze({ ...normalized })
|
|
4093
|
+
})
|
|
4094
|
+
);
|
|
4095
|
+
} catch {
|
|
4096
|
+
}
|
|
4097
|
+
}
|
|
4098
|
+
function emitFeedDiagnostic(diagnostic, listener) {
|
|
4099
|
+
const normalized = normalizeFeedDiagnostic(diagnostic);
|
|
4100
|
+
emitFeedDiagnosticToDefaultSink(normalized);
|
|
4101
|
+
if (!listener) return;
|
|
4102
|
+
try {
|
|
4103
|
+
listener(normalized);
|
|
4104
|
+
} catch {
|
|
4105
|
+
console.warn("[Granular] Session feed diagnostic listener failed");
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4108
|
+
function asRecord(value) {
|
|
4109
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
4110
|
+
return null;
|
|
4111
|
+
}
|
|
4112
|
+
return value;
|
|
4113
|
+
}
|
|
4114
|
+
function isSafeInteger(value, minimum = 0) {
|
|
4115
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= minimum;
|
|
4116
|
+
}
|
|
4117
|
+
function finiteInteger(value, fallback, minimum = 0) {
|
|
4118
|
+
return isSafeInteger(value, minimum) ? value : fallback;
|
|
4119
|
+
}
|
|
4120
|
+
function cloneAndFreeze(value, seen = /* @__PURE__ */ new WeakMap()) {
|
|
4121
|
+
if (!value || typeof value !== "object") {
|
|
4122
|
+
return value;
|
|
4123
|
+
}
|
|
4124
|
+
const object = value;
|
|
4125
|
+
const cached = seen.get(object);
|
|
4126
|
+
if (cached) {
|
|
4127
|
+
return cached;
|
|
4128
|
+
}
|
|
4129
|
+
if (Array.isArray(value)) {
|
|
4130
|
+
const result2 = [];
|
|
4131
|
+
seen.set(object, result2);
|
|
4132
|
+
for (const entry of value) {
|
|
4133
|
+
result2.push(cloneAndFreeze(entry, seen));
|
|
4134
|
+
}
|
|
4135
|
+
return Object.freeze(result2);
|
|
4136
|
+
}
|
|
4137
|
+
const result = /* @__PURE__ */ Object.create(null);
|
|
4138
|
+
seen.set(object, result);
|
|
4139
|
+
for (const [key, entry] of Object.entries(value)) {
|
|
4140
|
+
result[key] = cloneAndFreeze(entry, seen);
|
|
4141
|
+
}
|
|
4142
|
+
return Object.freeze(result);
|
|
4143
|
+
}
|
|
4144
|
+
function snapshotWith(snapshot, patch) {
|
|
4145
|
+
return Object.freeze({ ...snapshot, ...patch });
|
|
4146
|
+
}
|
|
4147
|
+
function emptyFeedSnapshot(isHydrated = false) {
|
|
4148
|
+
return Object.freeze({
|
|
4149
|
+
tail: Object.freeze([]),
|
|
4150
|
+
transients: Object.freeze([]),
|
|
4151
|
+
revision: 0,
|
|
4152
|
+
documentEpoch: 0,
|
|
4153
|
+
documentRevision: 0,
|
|
4154
|
+
lastSequence: 0,
|
|
4155
|
+
archivedThroughSequence: 0,
|
|
4156
|
+
hasOlder: false,
|
|
4157
|
+
isHydrated,
|
|
4158
|
+
isRepairing: false,
|
|
4159
|
+
error: null
|
|
4160
|
+
});
|
|
4161
|
+
}
|
|
4162
|
+
function hasCanonicalSessionFeedActivation(document) {
|
|
4163
|
+
const documentRecord = asRecord(document);
|
|
4164
|
+
const feed = asRecord(documentRecord?.feed);
|
|
4165
|
+
const activation = asRecord(feed?.activation);
|
|
4166
|
+
return activation?.mode === "canonical";
|
|
4167
|
+
}
|
|
4168
|
+
function isCanonicalSessionFeedDocument(document) {
|
|
4169
|
+
return hasCanonicalSessionFeedActivation(document);
|
|
4170
|
+
}
|
|
4171
|
+
function canonicalSessionFeedStructureError(document) {
|
|
4172
|
+
if (!hasCanonicalSessionFeedActivation(document)) return null;
|
|
4173
|
+
const documentRecord = asRecord(document);
|
|
4174
|
+
const feed = asRecord(documentRecord?.feed);
|
|
4175
|
+
const activation = asRecord(feed?.activation);
|
|
4176
|
+
if (feed?.schemaVersion !== 1) {
|
|
4177
|
+
return new Error("Canonical feed schemaVersion must be 1.");
|
|
4178
|
+
}
|
|
4179
|
+
if (!isSafeInteger(activation?.activatedAt)) {
|
|
4180
|
+
return new Error("Canonical feed activation timestamp is invalid.");
|
|
4181
|
+
}
|
|
4182
|
+
if (!Array.isArray(feed.tail)) {
|
|
4183
|
+
return new Error("Canonical feed tail is not an array.");
|
|
4184
|
+
}
|
|
4185
|
+
if (!asRecord(feed.transientById)) {
|
|
4186
|
+
return new Error("Canonical feed transientById is not an object.");
|
|
4187
|
+
}
|
|
4188
|
+
if ([
|
|
4189
|
+
feed.revision,
|
|
4190
|
+
feed.lastSequence,
|
|
4191
|
+
feed.archivedThroughSequence,
|
|
4192
|
+
feed.lastTransientOrdinal,
|
|
4193
|
+
documentRecord?.documentEpoch,
|
|
4194
|
+
documentRecord?.documentRevision
|
|
4195
|
+
].some((value) => !isSafeInteger(value))) {
|
|
4196
|
+
return new Error("Canonical feed chronology scalar is invalid.");
|
|
4197
|
+
}
|
|
4198
|
+
if (!isSafeInteger(documentRecord?.documentEpoch, 1)) {
|
|
4199
|
+
return new Error("Canonical feed documentEpoch must be positive.");
|
|
4200
|
+
}
|
|
4201
|
+
return null;
|
|
4202
|
+
}
|
|
4203
|
+
function isNonEmptyString(value) {
|
|
4204
|
+
return typeof value === "string" && value.length > 0;
|
|
4205
|
+
}
|
|
4206
|
+
function isKnownDurableFeedKind(kind) {
|
|
4207
|
+
return kind === "message" || kind === "feedback" || kind === "objects" || kind === "table" || kind === "artifact" || kind === "file" || kind === "action_suggestion" || kind === "prompt";
|
|
4208
|
+
}
|
|
4209
|
+
function hasValidKnownFeedPayload(kind, value) {
|
|
4210
|
+
const payload = asRecord(value);
|
|
4211
|
+
if (!payload) return false;
|
|
4212
|
+
switch (kind) {
|
|
4213
|
+
case "message":
|
|
4214
|
+
return (payload.role === "user" || payload.role === "assistant" || payload.role === "system") && typeof payload.text === "string";
|
|
4215
|
+
case "feedback":
|
|
4216
|
+
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");
|
|
4217
|
+
case "objects":
|
|
4218
|
+
return Array.isArray(payload.refs) && payload.refs.every((value2) => {
|
|
4219
|
+
const ref = asRecord(value2);
|
|
4220
|
+
return ref?.type === "entry" && isNonEmptyString(ref.path) || ref?.type === "list" && isNonEmptyString(ref.name) || ref?.type === "variable" && isNonEmptyString(ref.name);
|
|
4221
|
+
});
|
|
4222
|
+
case "table":
|
|
4223
|
+
return isNonEmptyString(payload.tableId) && Array.isArray(payload.columns) && Array.isArray(payload.rows);
|
|
4224
|
+
case "artifact": {
|
|
4225
|
+
const fallback = asRecord(payload.fallback);
|
|
4226
|
+
return isNonEmptyString(payload.artifactId) && Boolean(fallback) && isNonEmptyString(fallback?.label) && (fallback?.kind === "effect" || fallback?.kind === "batch" || fallback?.kind === "state_path");
|
|
4227
|
+
}
|
|
4228
|
+
case "file": {
|
|
4229
|
+
const fallback = asRecord(payload.fallback);
|
|
4230
|
+
return isNonEmptyString(payload.fileId) && Boolean(fallback) && isNonEmptyString(fallback?.filename);
|
|
4231
|
+
}
|
|
4232
|
+
case "action_suggestion":
|
|
4233
|
+
return isNonEmptyString(payload.suggestionId) && isNonEmptyString(payload.label);
|
|
4234
|
+
case "prompt":
|
|
4235
|
+
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);
|
|
4236
|
+
default:
|
|
4237
|
+
return true;
|
|
4238
|
+
}
|
|
4239
|
+
}
|
|
4240
|
+
function normalizeFeedItem(value) {
|
|
4241
|
+
const item = asRecord(value);
|
|
4242
|
+
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)) {
|
|
4243
|
+
return null;
|
|
4244
|
+
}
|
|
4245
|
+
return cloneAndFreeze(item);
|
|
4246
|
+
}
|
|
4247
|
+
function normalizeTransientFeedItem(value) {
|
|
4248
|
+
const item = asRecord(value);
|
|
4249
|
+
const payload = asRecord(item?.payload);
|
|
4250
|
+
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))) {
|
|
4251
|
+
return null;
|
|
4252
|
+
}
|
|
4253
|
+
return cloneAndFreeze(item);
|
|
4254
|
+
}
|
|
4255
|
+
var FEED_TRANSIENT_FEEDBACK_TONES = /* @__PURE__ */ new Set(["info", "working", "awaiting", "warning", "error"]);
|
|
4256
|
+
function isFeedTransientFeedbackTone(value) {
|
|
4257
|
+
return typeof value === "string" && FEED_TRANSIENT_FEEDBACK_TONES.has(value);
|
|
4258
|
+
}
|
|
4259
|
+
function normalizeFeedItems(values) {
|
|
4260
|
+
if (!Array.isArray(values)) {
|
|
4261
|
+
return {
|
|
4262
|
+
items: [],
|
|
4263
|
+
error: new Error("Canonical feed tail is not an array.")
|
|
4264
|
+
};
|
|
4265
|
+
}
|
|
4266
|
+
const items = [];
|
|
4267
|
+
const ids = /* @__PURE__ */ new Map();
|
|
4268
|
+
const sequences = /* @__PURE__ */ new Map();
|
|
4269
|
+
const itemBySequence = /* @__PURE__ */ new Map();
|
|
4270
|
+
let error = null;
|
|
4271
|
+
for (const value of values) {
|
|
4272
|
+
const item = normalizeFeedItem(value);
|
|
4273
|
+
if (!item) {
|
|
4274
|
+
error ||= new Error("Canonical feed contains an invalid durable item.");
|
|
4275
|
+
continue;
|
|
4276
|
+
}
|
|
4277
|
+
const idSequence = ids.get(item.id);
|
|
4278
|
+
const sequenceId = sequences.get(item.sequence);
|
|
4279
|
+
if (idSequence !== void 0 && idSequence !== item.sequence || sequenceId !== void 0 && sequenceId !== item.id) {
|
|
4280
|
+
error ||= new Error(
|
|
4281
|
+
`Canonical feed identity conflict at sequence ${item.sequence}.`
|
|
4282
|
+
);
|
|
4283
|
+
continue;
|
|
4284
|
+
}
|
|
4285
|
+
if (idSequence === item.sequence || sequenceId === item.id) {
|
|
4286
|
+
const duplicate = itemBySequence.get(item.sequence);
|
|
4287
|
+
if (duplicate && !sameDurableItem(duplicate, item)) {
|
|
4288
|
+
error ||= new Error(
|
|
4289
|
+
`Immutable feed item ${item.id} has conflicting payloads.`
|
|
4290
|
+
);
|
|
4291
|
+
}
|
|
4292
|
+
continue;
|
|
4293
|
+
}
|
|
4294
|
+
ids.set(item.id, item.sequence);
|
|
4295
|
+
sequences.set(item.sequence, item.id);
|
|
4296
|
+
itemBySequence.set(item.sequence, item);
|
|
4297
|
+
items.push(item);
|
|
4298
|
+
}
|
|
4299
|
+
items.sort((left, right) => left.sequence - right.sequence);
|
|
4300
|
+
return { items: Object.freeze(items), error };
|
|
4301
|
+
}
|
|
4302
|
+
function normalizeTransients(values) {
|
|
4303
|
+
const record = asRecord(values);
|
|
4304
|
+
if (!record) {
|
|
4305
|
+
return {
|
|
4306
|
+
items: [],
|
|
4307
|
+
error: new Error("Canonical feed transientById is not an object.")
|
|
4308
|
+
};
|
|
4309
|
+
}
|
|
4310
|
+
let error = null;
|
|
4311
|
+
const byId = /* @__PURE__ */ new Map();
|
|
4312
|
+
const idByOrdinal = /* @__PURE__ */ new Map();
|
|
4313
|
+
for (const [key, value] of Object.entries(record)) {
|
|
4314
|
+
const item = normalizeTransientFeedItem(value);
|
|
4315
|
+
if (!item || item.id !== key) {
|
|
4316
|
+
error ||= new Error("Canonical feed contains an invalid transient item.");
|
|
4317
|
+
continue;
|
|
4318
|
+
}
|
|
4319
|
+
const ordinalOwner = idByOrdinal.get(item.ordinal);
|
|
4320
|
+
if (ordinalOwner && ordinalOwner !== item.id) {
|
|
4321
|
+
error ||= new Error(
|
|
4322
|
+
`Canonical feed transient ordinal ${item.ordinal} is not unique.`
|
|
4323
|
+
);
|
|
4324
|
+
} else {
|
|
4325
|
+
idByOrdinal.set(item.ordinal, item.id);
|
|
4326
|
+
}
|
|
4327
|
+
const current = byId.get(item.id);
|
|
4328
|
+
if (!current || item.revision > current.revision) {
|
|
4329
|
+
byId.set(item.id, item);
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
const items = [...byId.values()].sort(
|
|
4333
|
+
(left, right) => left.ordinal - right.ordinal || left.id.localeCompare(right.id)
|
|
4334
|
+
);
|
|
4335
|
+
return {
|
|
4336
|
+
items: Object.freeze(items),
|
|
4337
|
+
error
|
|
4338
|
+
};
|
|
4339
|
+
}
|
|
4340
|
+
function readSessionFeedSnapshot(document, options = {}) {
|
|
4341
|
+
const isHydrated = options.isHydrated ?? true;
|
|
4342
|
+
if (!hasCanonicalSessionFeedActivation(document)) {
|
|
4343
|
+
return snapshotWith(emptyFeedSnapshot(isHydrated), {
|
|
4344
|
+
isRepairing: options.isRepairing || false,
|
|
4345
|
+
error: options.error || (isHydrated ? new Error("Canonical session feed-v1 is required.") : null)
|
|
4346
|
+
});
|
|
4347
|
+
}
|
|
4348
|
+
const documentRecord = asRecord(document);
|
|
4349
|
+
const feed = asRecord(documentRecord.feed);
|
|
4350
|
+
const structureError = canonicalSessionFeedStructureError(document);
|
|
4351
|
+
if (structureError) {
|
|
4352
|
+
const lastSequence2 = finiteInteger(feed.lastSequence, 0);
|
|
4353
|
+
const archivedThroughSequence2 = finiteInteger(
|
|
4354
|
+
feed.archivedThroughSequence,
|
|
4355
|
+
0
|
|
4356
|
+
);
|
|
4357
|
+
return snapshotWith(emptyFeedSnapshot(isHydrated), {
|
|
4358
|
+
revision: finiteInteger(feed.revision, 0),
|
|
4359
|
+
documentEpoch: finiteInteger(documentRecord.documentEpoch, 0),
|
|
4360
|
+
documentRevision: finiteInteger(documentRecord.documentRevision, 0),
|
|
4361
|
+
lastSequence: lastSequence2,
|
|
4362
|
+
archivedThroughSequence: archivedThroughSequence2,
|
|
4363
|
+
hasOlder: archivedThroughSequence2 > 0,
|
|
4364
|
+
isRepairing: options.isRepairing || false,
|
|
4365
|
+
error: options.error || structureError
|
|
4366
|
+
});
|
|
4367
|
+
}
|
|
4368
|
+
const normalizedTail = normalizeFeedItems(feed.tail);
|
|
4369
|
+
const normalizedTransients = normalizeTransients(feed.transientById);
|
|
4370
|
+
const lastSequence = finiteInteger(feed.lastSequence, 0);
|
|
4371
|
+
const archivedThroughSequence = finiteInteger(
|
|
4372
|
+
feed.archivedThroughSequence,
|
|
4373
|
+
0
|
|
4374
|
+
);
|
|
4375
|
+
let error = options.error || normalizedTail.error || normalizedTransients.error;
|
|
4376
|
+
let expected = archivedThroughSequence + 1;
|
|
4377
|
+
for (const item of normalizedTail.items) {
|
|
4378
|
+
if (item.sequence !== expected) {
|
|
4379
|
+
error ||= new Error(
|
|
4380
|
+
`Canonical feed tail has a sequence gap before ${item.sequence}; expected ${expected}.`
|
|
4381
|
+
);
|
|
4382
|
+
break;
|
|
4383
|
+
}
|
|
4384
|
+
expected += 1;
|
|
4385
|
+
}
|
|
4386
|
+
const tailLastSequence = normalizedTail.items[normalizedTail.items.length - 1]?.sequence || archivedThroughSequence;
|
|
4387
|
+
if (tailLastSequence !== lastSequence) {
|
|
4388
|
+
error ||= new Error(
|
|
4389
|
+
`Canonical feed tail ends at ${tailLastSequence}, but lastSequence is ${lastSequence}.`
|
|
4390
|
+
);
|
|
4391
|
+
}
|
|
4392
|
+
if (archivedThroughSequence > lastSequence) {
|
|
4393
|
+
error ||= new Error(
|
|
4394
|
+
"Canonical feed archivedThroughSequence exceeds lastSequence."
|
|
4395
|
+
);
|
|
4396
|
+
}
|
|
4397
|
+
return Object.freeze({
|
|
4398
|
+
tail: normalizedTail.items,
|
|
4399
|
+
transients: normalizedTransients.items,
|
|
4400
|
+
revision: finiteInteger(feed.revision, 0),
|
|
4401
|
+
documentEpoch: finiteInteger(documentRecord.documentEpoch, 0),
|
|
4402
|
+
documentRevision: finiteInteger(documentRecord.documentRevision, 0),
|
|
4403
|
+
lastSequence,
|
|
4404
|
+
archivedThroughSequence,
|
|
4405
|
+
hasOlder: archivedThroughSequence > 0,
|
|
4406
|
+
isHydrated,
|
|
4407
|
+
isRepairing: options.isRepairing || false,
|
|
4408
|
+
error
|
|
4409
|
+
});
|
|
4410
|
+
}
|
|
4411
|
+
function validateFeedListOptions(options) {
|
|
4412
|
+
if (options.afterSequence !== void 0 && options.beforeSequence !== void 0) {
|
|
4413
|
+
throw new RangeError(
|
|
4414
|
+
"Feed list accepts afterSequence or beforeSequence, not both."
|
|
4415
|
+
);
|
|
4416
|
+
}
|
|
4417
|
+
for (const [name, value] of [
|
|
4418
|
+
["afterSequence", options.afterSequence],
|
|
4419
|
+
["beforeSequence", options.beforeSequence]
|
|
4420
|
+
]) {
|
|
4421
|
+
if (value !== void 0 && !isSafeInteger(value)) {
|
|
4422
|
+
throw new RangeError(`${name} must be a non-negative integer.`);
|
|
4423
|
+
}
|
|
4424
|
+
}
|
|
4425
|
+
if (options.limit !== void 0 && (!isSafeInteger(options.limit, 1) || options.limit > 500)) {
|
|
4426
|
+
throw new RangeError("Feed list limit must be an integer from 1 to 500.");
|
|
4427
|
+
}
|
|
4428
|
+
return { ...options };
|
|
4429
|
+
}
|
|
4430
|
+
function normalizeFeedPage(value) {
|
|
4431
|
+
const page = asRecord(value);
|
|
4432
|
+
if (!page) {
|
|
4433
|
+
throw new Error("Feed list transport returned an invalid page.");
|
|
4434
|
+
}
|
|
4435
|
+
const normalized = normalizeFeedItems(page.items);
|
|
4436
|
+
if (normalized.error) {
|
|
4437
|
+
throw normalized.error;
|
|
4438
|
+
}
|
|
4439
|
+
const first = normalized.items[0]?.sequence || null;
|
|
4440
|
+
const last = normalized.items[normalized.items.length - 1]?.sequence || null;
|
|
4441
|
+
for (let index = 1; index < normalized.items.length; index += 1) {
|
|
4442
|
+
if (normalized.items[index].sequence !== normalized.items[index - 1].sequence + 1) {
|
|
4443
|
+
throw new Error("Feed list page contains a sequence gap.");
|
|
4444
|
+
}
|
|
4445
|
+
}
|
|
4446
|
+
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") {
|
|
4447
|
+
throw new Error(
|
|
4448
|
+
"Feed list page sequence metadata does not match its items."
|
|
4449
|
+
);
|
|
4450
|
+
}
|
|
4451
|
+
return Object.freeze({
|
|
4452
|
+
items: normalized.items,
|
|
4453
|
+
firstSequence: first,
|
|
4454
|
+
lastSequence: last,
|
|
4455
|
+
hasMoreBefore: page.hasMoreBefore,
|
|
4456
|
+
hasMoreAfter: page.hasMoreAfter
|
|
4457
|
+
});
|
|
4458
|
+
}
|
|
4459
|
+
function stableValueFingerprint(value, ancestors = /* @__PURE__ */ new WeakSet()) {
|
|
4460
|
+
if (!value || typeof value !== "object") {
|
|
4461
|
+
return JSON.stringify(value);
|
|
4462
|
+
}
|
|
4463
|
+
if (ancestors.has(value)) return '"[circular]"';
|
|
4464
|
+
ancestors.add(value);
|
|
4465
|
+
const result = Array.isArray(value) ? `[${value.map((entry) => stableValueFingerprint(entry, ancestors)).join(",")}]` : `{${Object.keys(value).sort().map(
|
|
4466
|
+
(key) => `${JSON.stringify(key)}:${stableValueFingerprint(
|
|
4467
|
+
value[key],
|
|
4468
|
+
ancestors
|
|
4469
|
+
)}`
|
|
4470
|
+
).join(",")}}`;
|
|
4471
|
+
ancestors.delete(value);
|
|
4472
|
+
return result;
|
|
4473
|
+
}
|
|
4474
|
+
function sameDurableItem(left, right) {
|
|
4475
|
+
return stableValueFingerprint(left) === stableValueFingerprint(right);
|
|
4476
|
+
}
|
|
4477
|
+
function contiguousLocalTailWatermark(snapshot) {
|
|
4478
|
+
let watermark = 0;
|
|
4479
|
+
for (const item of snapshot.tail) {
|
|
4480
|
+
if (item.sequence !== watermark + 1) break;
|
|
4481
|
+
watermark = item.sequence;
|
|
4482
|
+
}
|
|
4483
|
+
return watermark;
|
|
4484
|
+
}
|
|
4485
|
+
var SessionFeedController = class {
|
|
4486
|
+
snapshot;
|
|
4487
|
+
canonical;
|
|
4488
|
+
canonicalStructureValid;
|
|
4489
|
+
deliveredThrough;
|
|
4490
|
+
knownBySequence = /* @__PURE__ */ new Map();
|
|
4491
|
+
sequenceById = /* @__PURE__ */ new Map();
|
|
4492
|
+
subscribers = /* @__PURE__ */ new Set();
|
|
4493
|
+
listTransport;
|
|
4494
|
+
repairGeneration = 0;
|
|
4495
|
+
repairPromise = null;
|
|
4496
|
+
repairRetryHandle = null;
|
|
4497
|
+
repairRetryToken = 0;
|
|
4498
|
+
repairFailureAttempt = 0;
|
|
4499
|
+
scheduleRepairRetryCallback;
|
|
4500
|
+
cancelRepairRetryCallback;
|
|
4501
|
+
initialRepairRetryDelayMs;
|
|
4502
|
+
maxRepairRetryDelayMs;
|
|
4503
|
+
disposed = false;
|
|
4504
|
+
diagnosticListener;
|
|
4505
|
+
diagnosticNow;
|
|
4506
|
+
observedUnknownPositions = /* @__PURE__ */ new Set();
|
|
4507
|
+
constructor(initialDocument, options = {}) {
|
|
4508
|
+
this.diagnosticListener = options.onDiagnostic || null;
|
|
4509
|
+
this.diagnosticNow = options.now || Date.now;
|
|
4510
|
+
this.scheduleRepairRetryCallback = options.scheduleRepairRetry || ((callback, delayMs) => setTimeout(callback, delayMs));
|
|
4511
|
+
this.cancelRepairRetryCallback = options.cancelRepairRetry || ((handle) => clearTimeout(handle));
|
|
4512
|
+
this.initialRepairRetryDelayMs = Math.max(
|
|
4513
|
+
1,
|
|
4514
|
+
options.initialRepairRetryDelayMs ?? 500
|
|
4515
|
+
);
|
|
4516
|
+
this.maxRepairRetryDelayMs = Math.max(
|
|
4517
|
+
this.initialRepairRetryDelayMs,
|
|
4518
|
+
options.maxRepairRetryDelayMs ?? 1e4
|
|
4519
|
+
);
|
|
4520
|
+
this.canonical = hasCanonicalSessionFeedActivation(initialDocument);
|
|
4521
|
+
this.canonicalStructureValid = this.canonical && canonicalSessionFeedStructureError(initialDocument) === null;
|
|
4522
|
+
this.snapshot = readSessionFeedSnapshot(initialDocument, {
|
|
4523
|
+
isHydrated: options.isHydrated ?? this.canonical
|
|
4524
|
+
});
|
|
4525
|
+
this.listTransport = options.listTransport || null;
|
|
4526
|
+
this.deliveredThrough = contiguousLocalTailWatermark(this.snapshot);
|
|
4527
|
+
const identityError = this.remember(this.snapshot.tail);
|
|
4528
|
+
const needsRepair = this.canonicalStructureValid && this.deliveredThrough < this.snapshot.lastSequence;
|
|
4529
|
+
if (identityError || needsRepair) {
|
|
4530
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
4531
|
+
isRepairing: needsRepair && Boolean(this.listTransport),
|
|
4532
|
+
error: identityError || this.snapshot.error
|
|
4533
|
+
});
|
|
4534
|
+
}
|
|
4535
|
+
if (needsRepair) {
|
|
4536
|
+
this.startRepair(this.snapshot.lastSequence);
|
|
4537
|
+
}
|
|
4538
|
+
}
|
|
4539
|
+
setListTransport(transport) {
|
|
4540
|
+
if (this.disposed) return;
|
|
4541
|
+
if (transport !== this.listTransport) {
|
|
4542
|
+
this.cancelScheduledRepairRetry();
|
|
4543
|
+
this.repairFailureAttempt = 0;
|
|
4544
|
+
this.repairGeneration += 1;
|
|
4545
|
+
this.repairPromise = null;
|
|
4546
|
+
}
|
|
4547
|
+
this.listTransport = transport;
|
|
4548
|
+
if (transport && this.canonical && this.canonicalStructureValid && this.deliveredThrough < this.snapshot.lastSequence) {
|
|
4549
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
4550
|
+
isRepairing: true,
|
|
4551
|
+
error: null
|
|
4552
|
+
});
|
|
4553
|
+
this.startRepair(this.snapshot.lastSequence);
|
|
4554
|
+
}
|
|
4555
|
+
}
|
|
4556
|
+
getSnapshot() {
|
|
4557
|
+
return this.snapshot;
|
|
4558
|
+
}
|
|
4559
|
+
/**
|
|
4560
|
+
* Stop background archive work when its owning Session is replaced or
|
|
4561
|
+
* explicitly disconnected. Late transport completions are quarantined by
|
|
4562
|
+
* the generation check and cannot update subscribers.
|
|
4563
|
+
*/
|
|
4564
|
+
dispose() {
|
|
4565
|
+
if (this.disposed) return;
|
|
4566
|
+
this.disposed = true;
|
|
4567
|
+
this.cancelScheduledRepairRetry();
|
|
4568
|
+
this.repairGeneration += 1;
|
|
4569
|
+
this.repairPromise = null;
|
|
4570
|
+
this.listTransport = null;
|
|
4571
|
+
this.subscribers.clear();
|
|
4572
|
+
}
|
|
4573
|
+
async list(options = {}) {
|
|
4574
|
+
const validated = validateFeedListOptions(options);
|
|
4575
|
+
if (!this.listTransport) {
|
|
4576
|
+
throw new Error(
|
|
4577
|
+
"Historical feed transport is unavailable for this Session."
|
|
4578
|
+
);
|
|
4579
|
+
}
|
|
4580
|
+
const page = normalizeFeedPage(await this.listTransport(validated));
|
|
4581
|
+
this.observeUnknownKinds(page.items);
|
|
4582
|
+
return page;
|
|
4583
|
+
}
|
|
4584
|
+
subscribe(listener, options = {}) {
|
|
4585
|
+
const afterSequence = options.afterSequence;
|
|
4586
|
+
if (afterSequence !== void 0 && !isSafeInteger(afterSequence)) {
|
|
4587
|
+
throw new RangeError(
|
|
4588
|
+
"Feed afterSequence must be a non-negative safe integer."
|
|
4589
|
+
);
|
|
4590
|
+
}
|
|
4591
|
+
this.subscribers.add(listener);
|
|
4592
|
+
if (afterSequence !== void 0 && afterSequence < this.snapshot.lastSequence) {
|
|
4593
|
+
const items = this.snapshot.tail.filter(
|
|
4594
|
+
(item) => item.sequence > afterSequence
|
|
4595
|
+
);
|
|
4596
|
+
if (items.length > 0 && items[0].sequence === afterSequence + 1 && items[items.length - 1].sequence === this.snapshot.lastSequence) {
|
|
4597
|
+
listener({ type: "append", items });
|
|
4598
|
+
if (this.snapshot.transients.length > 0) {
|
|
4599
|
+
listener({
|
|
4600
|
+
type: "transients",
|
|
4601
|
+
items: this.snapshot.transients,
|
|
4602
|
+
revision: this.snapshot.revision
|
|
4603
|
+
});
|
|
4604
|
+
}
|
|
4605
|
+
} else {
|
|
4606
|
+
listener({ type: "reset", snapshot: this.snapshot });
|
|
4607
|
+
}
|
|
4608
|
+
} else {
|
|
4609
|
+
listener({ type: "reset", snapshot: this.snapshot });
|
|
4610
|
+
}
|
|
4611
|
+
return () => {
|
|
4612
|
+
this.subscribers.delete(listener);
|
|
4613
|
+
};
|
|
4614
|
+
}
|
|
4615
|
+
/**
|
|
4616
|
+
* Accept the latest synced document. Calls may arrive out of order after a
|
|
4617
|
+
* reconnect; freshness watermarks prevent an older snapshot from regressing
|
|
4618
|
+
* durable positions or transient state.
|
|
4619
|
+
*/
|
|
4620
|
+
updateDocument(document, options = {}) {
|
|
4621
|
+
if (this.disposed) return;
|
|
4622
|
+
const nextCanonical = hasCanonicalSessionFeedActivation(document);
|
|
4623
|
+
const next = readSessionFeedSnapshot(document, { isHydrated: true });
|
|
4624
|
+
if (!nextCanonical) {
|
|
4625
|
+
if (!this.canonical) {
|
|
4626
|
+
this.snapshot = next;
|
|
4627
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4628
|
+
return;
|
|
4629
|
+
}
|
|
4630
|
+
this.rejectSnapshotRegression("canonical_deactivation", next);
|
|
4631
|
+
return;
|
|
4632
|
+
}
|
|
4633
|
+
const nextStructureError = canonicalSessionFeedStructureError(document);
|
|
4634
|
+
if (nextStructureError) {
|
|
4635
|
+
if (!this.canonical) {
|
|
4636
|
+
this.canonical = true;
|
|
4637
|
+
this.canonicalStructureValid = false;
|
|
4638
|
+
this.cancelScheduledRepairRetry();
|
|
4639
|
+
this.repairFailureAttempt = 0;
|
|
4640
|
+
this.repairGeneration += 1;
|
|
4641
|
+
this.repairPromise = null;
|
|
4642
|
+
this.deliveredThrough = 0;
|
|
4643
|
+
this.knownBySequence.clear();
|
|
4644
|
+
this.sequenceById.clear();
|
|
4645
|
+
this.observedUnknownPositions.clear();
|
|
4646
|
+
this.snapshot = next;
|
|
4647
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4648
|
+
return;
|
|
4649
|
+
}
|
|
4650
|
+
this.rejectSnapshotRegression("invalid_canonical_document", next);
|
|
4651
|
+
if (!this.canonicalStructureValid) {
|
|
4652
|
+
const current2 = this.snapshot;
|
|
4653
|
+
const incomingIsNewer = next.documentEpoch > current2.documentEpoch || next.documentEpoch === current2.documentEpoch && next.documentRevision >= current2.documentRevision;
|
|
4654
|
+
if (incomingIsNewer && next.lastSequence >= current2.lastSequence) {
|
|
4655
|
+
this.snapshot = next;
|
|
4656
|
+
}
|
|
4657
|
+
} else {
|
|
4658
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
4659
|
+
error: nextStructureError
|
|
4660
|
+
});
|
|
4661
|
+
}
|
|
4662
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4663
|
+
return;
|
|
4664
|
+
}
|
|
4665
|
+
if (!this.canonical || options.forceReset) {
|
|
4666
|
+
this.acceptReset(next);
|
|
4667
|
+
return;
|
|
4668
|
+
}
|
|
4669
|
+
if (!this.canonicalStructureValid) {
|
|
4670
|
+
const current2 = this.snapshot;
|
|
4671
|
+
if (next.documentEpoch < current2.documentEpoch) {
|
|
4672
|
+
this.rejectSnapshotRegression("older_document_epoch", next);
|
|
4673
|
+
return;
|
|
4674
|
+
}
|
|
4675
|
+
if (next.lastSequence < current2.lastSequence) {
|
|
4676
|
+
this.rejectSnapshotRegression("last_sequence_regression", next);
|
|
4677
|
+
return;
|
|
4678
|
+
}
|
|
4679
|
+
if (next.documentEpoch === current2.documentEpoch && next.documentRevision < current2.documentRevision) {
|
|
4680
|
+
this.rejectSnapshotRegression("document_revision_regression", next);
|
|
4681
|
+
return;
|
|
4682
|
+
}
|
|
4683
|
+
if (next.documentEpoch === current2.documentEpoch && next.revision < current2.revision) {
|
|
4684
|
+
this.rejectSnapshotRegression("feed_revision_regression", next);
|
|
4685
|
+
return;
|
|
4686
|
+
}
|
|
4687
|
+
this.acceptReset(next);
|
|
4688
|
+
return;
|
|
4689
|
+
}
|
|
4690
|
+
const current = this.snapshot;
|
|
4691
|
+
if (next.documentEpoch < current.documentEpoch) {
|
|
4692
|
+
this.rejectSnapshotRegression("older_document_epoch", next);
|
|
4693
|
+
return;
|
|
4694
|
+
}
|
|
4695
|
+
if (next.documentEpoch > current.documentEpoch) {
|
|
4696
|
+
if (next.lastSequence < current.lastSequence) {
|
|
4697
|
+
this.rejectSnapshotRegression("last_sequence_regression", next);
|
|
4698
|
+
return;
|
|
4699
|
+
}
|
|
4700
|
+
const identityError2 = this.findIdentityConflict(next.tail);
|
|
4701
|
+
if (identityError2) {
|
|
4702
|
+
this.rejectSnapshotRegression("identity_conflict", next);
|
|
4703
|
+
this.snapshot = snapshotWith(current, { error: identityError2 });
|
|
4704
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4705
|
+
return;
|
|
4706
|
+
}
|
|
4707
|
+
this.acceptReset(next);
|
|
4708
|
+
return;
|
|
4709
|
+
}
|
|
4710
|
+
if (next.lastSequence < current.lastSequence) {
|
|
4711
|
+
this.rejectSnapshotRegression("last_sequence_regression", next);
|
|
4712
|
+
if (next.documentRevision > current.documentRevision) {
|
|
4713
|
+
this.emit({
|
|
4714
|
+
type: "resource_refresh",
|
|
4715
|
+
documentRevision: next.documentRevision
|
|
4716
|
+
});
|
|
4717
|
+
this.snapshot = snapshotWith(current, {
|
|
4718
|
+
documentRevision: next.documentRevision
|
|
4719
|
+
});
|
|
4720
|
+
}
|
|
4721
|
+
return;
|
|
4722
|
+
}
|
|
4723
|
+
const previousDocumentRevision = current.documentRevision;
|
|
4724
|
+
const previousRevision = current.revision;
|
|
4725
|
+
if (next.lastSequence > current.lastSequence && next.revision <= previousRevision) {
|
|
4726
|
+
this.rejectSnapshotRegression("last_sequence_without_revision", next);
|
|
4727
|
+
const documentRevision = Math.max(
|
|
4728
|
+
previousDocumentRevision,
|
|
4729
|
+
next.documentRevision
|
|
4730
|
+
);
|
|
4731
|
+
this.snapshot = snapshotWith(current, {
|
|
4732
|
+
documentRevision,
|
|
4733
|
+
error: new Error(
|
|
4734
|
+
"Feed lastSequence advanced without a newer feed revision."
|
|
4735
|
+
)
|
|
4736
|
+
});
|
|
4737
|
+
if (next.documentRevision > previousDocumentRevision) {
|
|
4738
|
+
this.emit({
|
|
4739
|
+
type: "resource_refresh",
|
|
4740
|
+
documentRevision: next.documentRevision
|
|
4741
|
+
});
|
|
4742
|
+
}
|
|
4743
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4744
|
+
return;
|
|
4745
|
+
}
|
|
4746
|
+
const previousTransients = current.transients;
|
|
4747
|
+
const transientIdentityError = next.revision === previousRevision && !sameTransientSet(previousTransients, next.transients) ? new Error("Transient feed changed without a newer feed revision.") : null;
|
|
4748
|
+
const transientSelection = this.selectNewerTransients(current, next);
|
|
4749
|
+
const mergedTransients = transientSelection.items;
|
|
4750
|
+
const identityError = transientIdentityError || transientSelection.error || this.remember(next.tail);
|
|
4751
|
+
if (identityError) {
|
|
4752
|
+
this.rejectSnapshotRegression("identity_conflict", next);
|
|
4753
|
+
this.snapshot = snapshotWith(current, {
|
|
4754
|
+
documentRevision: Math.max(
|
|
4755
|
+
previousDocumentRevision,
|
|
4756
|
+
next.documentRevision
|
|
4757
|
+
),
|
|
4758
|
+
error: identityError
|
|
4759
|
+
});
|
|
4760
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4761
|
+
return;
|
|
4762
|
+
}
|
|
4763
|
+
const keepCurrentFeedState = next.lastSequence === current.lastSequence && next.revision < current.revision;
|
|
4764
|
+
if (keepCurrentFeedState) {
|
|
4765
|
+
this.rejectSnapshotRegression("feed_revision_regression", next);
|
|
4766
|
+
}
|
|
4767
|
+
let nextSnapshot = snapshotWith(keepCurrentFeedState ? current : next, {
|
|
4768
|
+
transients: mergedTransients,
|
|
4769
|
+
revision: Math.max(previousRevision, next.revision),
|
|
4770
|
+
documentRevision: Math.max(
|
|
4771
|
+
previousDocumentRevision,
|
|
4772
|
+
next.documentRevision
|
|
4773
|
+
)
|
|
4774
|
+
});
|
|
4775
|
+
const appended = next.lastSequence > this.deliveredThrough ? this.drainContiguous(next.lastSequence) : [];
|
|
4776
|
+
const needsRepair = this.deliveredThrough < next.lastSequence;
|
|
4777
|
+
const incomingAdvanced = next.documentEpoch > current.documentEpoch || next.documentRevision > current.documentRevision || next.revision > current.revision || next.lastSequence > current.lastSequence;
|
|
4778
|
+
if (incomingAdvanced || !needsRepair) {
|
|
4779
|
+
this.cancelScheduledRepairRetry();
|
|
4780
|
+
this.repairFailureAttempt = 0;
|
|
4781
|
+
}
|
|
4782
|
+
nextSnapshot = snapshotWith(nextSnapshot, {
|
|
4783
|
+
isRepairing: needsRepair,
|
|
4784
|
+
error: nextSnapshot.error
|
|
4785
|
+
});
|
|
4786
|
+
this.snapshot = nextSnapshot;
|
|
4787
|
+
if (appended.length > 0) {
|
|
4788
|
+
this.emit({ type: "append", items: appended });
|
|
4789
|
+
}
|
|
4790
|
+
if (!sameTransientSet(previousTransients, mergedTransients) && next.revision > previousRevision) {
|
|
4791
|
+
this.emit({
|
|
4792
|
+
type: "transients",
|
|
4793
|
+
items: mergedTransients,
|
|
4794
|
+
revision: this.snapshot.revision
|
|
4795
|
+
});
|
|
4796
|
+
}
|
|
4797
|
+
if (next.documentRevision > previousDocumentRevision) {
|
|
4798
|
+
this.emit({
|
|
4799
|
+
type: "resource_refresh",
|
|
4800
|
+
documentRevision: next.documentRevision
|
|
4801
|
+
});
|
|
4802
|
+
}
|
|
4803
|
+
if (needsRepair) {
|
|
4804
|
+
this.startRepair(next.lastSequence);
|
|
4805
|
+
}
|
|
4806
|
+
}
|
|
4807
|
+
/**
|
|
4808
|
+
* Quarantine a canonical replacement rejected by the document transport.
|
|
4809
|
+
* The notice deliberately contains no replacement document, so accepted
|
|
4810
|
+
* feed history remains the only data visible to subscribers during repair.
|
|
4811
|
+
*
|
|
4812
|
+
* @internal
|
|
4813
|
+
*/
|
|
4814
|
+
quarantineCanonicalReplacement(quarantine) {
|
|
4815
|
+
if (this.disposed) return;
|
|
4816
|
+
const incoming = snapshotWith(emptyFeedSnapshot(true), {
|
|
4817
|
+
documentEpoch: quarantine.documentEpoch,
|
|
4818
|
+
documentRevision: quarantine.documentRevision,
|
|
4819
|
+
isRepairing: true,
|
|
4820
|
+
error: quarantine.error
|
|
4821
|
+
});
|
|
4822
|
+
if (!this.canonical) {
|
|
4823
|
+
this.canonical = true;
|
|
4824
|
+
this.canonicalStructureValid = false;
|
|
4825
|
+
this.repairGeneration += 1;
|
|
4826
|
+
this.repairPromise = null;
|
|
4827
|
+
this.cancelScheduledRepairRetry();
|
|
4828
|
+
this.repairFailureAttempt = 0;
|
|
4829
|
+
this.deliveredThrough = 0;
|
|
4830
|
+
this.knownBySequence.clear();
|
|
4831
|
+
this.sequenceById.clear();
|
|
4832
|
+
this.observedUnknownPositions.clear();
|
|
4833
|
+
this.snapshot = incoming;
|
|
4834
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4835
|
+
return;
|
|
4836
|
+
}
|
|
4837
|
+
this.rejectSnapshotRegression("invalid_canonical_document", incoming);
|
|
4838
|
+
this.repairGeneration += 1;
|
|
4839
|
+
this.repairPromise = null;
|
|
4840
|
+
this.cancelScheduledRepairRetry();
|
|
4841
|
+
this.repairFailureAttempt = 0;
|
|
4842
|
+
if (!this.canonicalStructureValid) {
|
|
4843
|
+
const current = this.snapshot;
|
|
4844
|
+
const incomingIsNewer = incoming.documentEpoch > current.documentEpoch || incoming.documentEpoch === current.documentEpoch && incoming.documentRevision >= current.documentRevision;
|
|
4845
|
+
if (incomingIsNewer) {
|
|
4846
|
+
this.snapshot = incoming;
|
|
4847
|
+
} else {
|
|
4848
|
+
this.snapshot = snapshotWith(current, {
|
|
4849
|
+
isRepairing: true,
|
|
4850
|
+
error: quarantine.error
|
|
4851
|
+
});
|
|
4852
|
+
}
|
|
4853
|
+
} else {
|
|
4854
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
4855
|
+
isRepairing: true,
|
|
4856
|
+
error: quarantine.error
|
|
4857
|
+
});
|
|
4858
|
+
}
|
|
4859
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4860
|
+
}
|
|
4861
|
+
acceptReset(snapshot) {
|
|
4862
|
+
this.cancelScheduledRepairRetry();
|
|
4863
|
+
this.repairFailureAttempt = 0;
|
|
4864
|
+
this.repairGeneration += 1;
|
|
4865
|
+
this.repairPromise = null;
|
|
4866
|
+
this.canonical = true;
|
|
4867
|
+
this.canonicalStructureValid = true;
|
|
4868
|
+
this.knownBySequence.clear();
|
|
4869
|
+
this.sequenceById.clear();
|
|
4870
|
+
this.observedUnknownPositions.clear();
|
|
4871
|
+
this.deliveredThrough = contiguousLocalTailWatermark(snapshot);
|
|
4872
|
+
const identityError = this.remember(snapshot.tail);
|
|
4873
|
+
const needsRepair = this.deliveredThrough < snapshot.lastSequence;
|
|
4874
|
+
this.snapshot = snapshotWith(snapshot, {
|
|
4875
|
+
isRepairing: needsRepair && Boolean(this.listTransport),
|
|
4876
|
+
error: identityError || snapshot.error
|
|
4877
|
+
});
|
|
4878
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
4879
|
+
if (needsRepair) {
|
|
4880
|
+
this.startRepair(snapshot.lastSequence);
|
|
4881
|
+
}
|
|
4882
|
+
}
|
|
4883
|
+
emitDiagnostic(diagnostic) {
|
|
4884
|
+
emitFeedDiagnostic(diagnostic, this.diagnosticListener);
|
|
4885
|
+
}
|
|
4886
|
+
rejectSnapshotRegression(reason, incoming) {
|
|
4887
|
+
const current = this.snapshot;
|
|
4888
|
+
this.emitDiagnostic({
|
|
4889
|
+
type: "snapshot_regression_rejected",
|
|
4890
|
+
reason,
|
|
4891
|
+
currentDocumentEpoch: current.documentEpoch,
|
|
4892
|
+
incomingDocumentEpoch: incoming.documentEpoch,
|
|
4893
|
+
currentDocumentRevision: current.documentRevision,
|
|
4894
|
+
incomingDocumentRevision: incoming.documentRevision,
|
|
4895
|
+
currentFeedRevision: current.revision,
|
|
4896
|
+
incomingFeedRevision: incoming.revision,
|
|
4897
|
+
currentLastSequence: current.lastSequence,
|
|
4898
|
+
incomingLastSequence: incoming.lastSequence
|
|
4899
|
+
});
|
|
4900
|
+
}
|
|
4901
|
+
observeUnknownKinds(items) {
|
|
4902
|
+
for (const item of items) {
|
|
4903
|
+
const runtimeKind = String(item.kind);
|
|
4904
|
+
if (isKnownDurableFeedKind(runtimeKind)) continue;
|
|
4905
|
+
const diagnosticKind = normalizeFeedDiagnosticKind(runtimeKind);
|
|
4906
|
+
const position = `${item.sequence}:${diagnosticKind}`;
|
|
4907
|
+
if (this.observedUnknownPositions.has(position)) continue;
|
|
4908
|
+
this.observedUnknownPositions.add(position);
|
|
4909
|
+
this.emitDiagnostic({
|
|
4910
|
+
type: "unknown_kind",
|
|
4911
|
+
kind: diagnosticKind,
|
|
4912
|
+
sequence: item.sequence,
|
|
4913
|
+
consumer: "sdk"
|
|
4914
|
+
});
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
selectNewerTransients(current, next) {
|
|
4918
|
+
if (next.revision <= current.revision) {
|
|
4919
|
+
return { items: current.transients, error: null };
|
|
4920
|
+
}
|
|
4921
|
+
let newestById;
|
|
4922
|
+
try {
|
|
4923
|
+
newestById = new Map(
|
|
4924
|
+
orderTransientFeedItems([
|
|
4925
|
+
...current.transients,
|
|
4926
|
+
...next.transients
|
|
4927
|
+
]).map((item) => [item.id, item])
|
|
4928
|
+
);
|
|
4929
|
+
} catch {
|
|
4930
|
+
return {
|
|
4931
|
+
items: current.transients,
|
|
4932
|
+
error: new Error(
|
|
4933
|
+
"Transient feed changed immutable identity or revision."
|
|
4934
|
+
)
|
|
4935
|
+
};
|
|
4936
|
+
}
|
|
4937
|
+
const currentById = new Map(
|
|
4938
|
+
current.transients.map((item) => [item.id, item])
|
|
4939
|
+
);
|
|
4940
|
+
const selected = [];
|
|
4941
|
+
for (const incoming of next.transients) {
|
|
4942
|
+
const previous = currentById.get(incoming.id);
|
|
4943
|
+
if (previous && previous.kind !== incoming.kind) {
|
|
4944
|
+
return {
|
|
4945
|
+
items: current.transients,
|
|
4946
|
+
error: new Error(
|
|
4947
|
+
`Transient feed item ${incoming.id} changed its immutable kind.`
|
|
4948
|
+
)
|
|
4949
|
+
};
|
|
4950
|
+
}
|
|
4951
|
+
if (previous?.kind === "message" && incoming.kind === "message" && incoming.revision > previous.revision && incoming.producerRevision < previous.producerRevision) {
|
|
4952
|
+
selected.push(previous);
|
|
4953
|
+
continue;
|
|
4954
|
+
}
|
|
4955
|
+
selected.push(newestById.get(incoming.id) || incoming);
|
|
4956
|
+
}
|
|
4957
|
+
return {
|
|
4958
|
+
items: orderTransientFeedItems(selected),
|
|
4959
|
+
error: null
|
|
4960
|
+
};
|
|
4961
|
+
}
|
|
4962
|
+
remember(items) {
|
|
4963
|
+
const stagedBySequence = /* @__PURE__ */ new Map();
|
|
4964
|
+
const stagedSequenceById = /* @__PURE__ */ new Map();
|
|
4965
|
+
for (const item of items) {
|
|
4966
|
+
const knownSequence = stagedSequenceById.get(item.id) ?? this.sequenceById.get(item.id);
|
|
4967
|
+
const knownItem = stagedBySequence.get(item.sequence) || this.knownBySequence.get(item.sequence);
|
|
4968
|
+
if (knownSequence !== void 0 && knownSequence !== item.sequence || knownItem && knownItem.id !== item.id) {
|
|
4969
|
+
return new Error(
|
|
4970
|
+
`Feed identity changed at sequence ${item.sequence}; a reset is required.`
|
|
4971
|
+
);
|
|
4972
|
+
}
|
|
4973
|
+
if (knownItem && !sameDurableItem(knownItem, item)) {
|
|
4974
|
+
return new Error(
|
|
4975
|
+
`Immutable feed item ${item.id} changed at sequence ${item.sequence}.`
|
|
4976
|
+
);
|
|
4977
|
+
}
|
|
4978
|
+
stagedSequenceById.set(item.id, item.sequence);
|
|
4979
|
+
stagedBySequence.set(item.sequence, item);
|
|
4980
|
+
}
|
|
4981
|
+
for (const item of stagedBySequence.values()) {
|
|
4982
|
+
this.sequenceById.set(item.id, item.sequence);
|
|
4983
|
+
this.knownBySequence.set(item.sequence, item);
|
|
4984
|
+
}
|
|
4985
|
+
this.observeUnknownKinds([...stagedBySequence.values()]);
|
|
4986
|
+
return null;
|
|
4987
|
+
}
|
|
4988
|
+
findIdentityConflict(items) {
|
|
4989
|
+
for (const item of items) {
|
|
4990
|
+
const knownSequence = this.sequenceById.get(item.id);
|
|
4991
|
+
const knownItem = this.knownBySequence.get(item.sequence);
|
|
4992
|
+
if (knownSequence !== void 0 && knownSequence !== item.sequence || knownItem && knownItem.id !== item.id) {
|
|
4993
|
+
return new Error(
|
|
4994
|
+
`Feed identity changed at sequence ${item.sequence}; the checkpoint was rejected.`
|
|
4995
|
+
);
|
|
4996
|
+
}
|
|
4997
|
+
if (knownItem && !sameDurableItem(knownItem, item)) {
|
|
4998
|
+
return new Error(
|
|
4999
|
+
`Immutable feed item ${item.id} changed at sequence ${item.sequence}.`
|
|
5000
|
+
);
|
|
5001
|
+
}
|
|
5002
|
+
}
|
|
5003
|
+
return null;
|
|
5004
|
+
}
|
|
5005
|
+
drainContiguous(targetSequence) {
|
|
5006
|
+
const appended = [];
|
|
5007
|
+
let sequence = this.deliveredThrough + 1;
|
|
5008
|
+
while (sequence <= targetSequence) {
|
|
5009
|
+
const item = this.knownBySequence.get(sequence);
|
|
5010
|
+
if (!item) break;
|
|
5011
|
+
appended.push(item);
|
|
5012
|
+
this.deliveredThrough = sequence;
|
|
5013
|
+
sequence += 1;
|
|
5014
|
+
}
|
|
5015
|
+
return Object.freeze(appended);
|
|
5016
|
+
}
|
|
5017
|
+
startRepair(targetSequence) {
|
|
5018
|
+
if (this.disposed || this.repairPromise || this.repairRetryHandle !== null) {
|
|
5019
|
+
return;
|
|
5020
|
+
}
|
|
5021
|
+
if (!this.listTransport) {
|
|
5022
|
+
this.emitDiagnostic({
|
|
5023
|
+
type: "gap_detected",
|
|
5024
|
+
expectedSequence: this.deliveredThrough + 1,
|
|
5025
|
+
targetSequence,
|
|
5026
|
+
repairAvailable: false
|
|
5027
|
+
});
|
|
5028
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
5029
|
+
isRepairing: false,
|
|
5030
|
+
error: this.snapshot.error || new Error(
|
|
5031
|
+
"Feed sequence gap cannot be repaired without history transport."
|
|
5032
|
+
)
|
|
5033
|
+
});
|
|
5034
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
5035
|
+
return;
|
|
5036
|
+
}
|
|
5037
|
+
this.emitDiagnostic({
|
|
5038
|
+
type: "gap_detected",
|
|
5039
|
+
expectedSequence: this.deliveredThrough + 1,
|
|
5040
|
+
targetSequence,
|
|
5041
|
+
repairAvailable: true
|
|
5042
|
+
});
|
|
5043
|
+
const generation = ++this.repairGeneration;
|
|
5044
|
+
this.repairPromise = this.repair(targetSequence, generation).finally(() => {
|
|
5045
|
+
if (generation === this.repairGeneration) {
|
|
5046
|
+
this.repairPromise = null;
|
|
5047
|
+
if (this.deliveredThrough < this.snapshot.lastSequence) {
|
|
5048
|
+
if (this.snapshot.error) {
|
|
5049
|
+
this.scheduleQuietRepairRetry();
|
|
5050
|
+
} else {
|
|
5051
|
+
this.snapshot = snapshotWith(this.snapshot, { isRepairing: true });
|
|
5052
|
+
this.startRepair(this.snapshot.lastSequence);
|
|
5053
|
+
}
|
|
5054
|
+
} else {
|
|
5055
|
+
this.repairFailureAttempt = 0;
|
|
5056
|
+
}
|
|
5057
|
+
}
|
|
5058
|
+
});
|
|
5059
|
+
}
|
|
5060
|
+
cancelScheduledRepairRetry() {
|
|
5061
|
+
if (this.repairRetryHandle === null) return;
|
|
5062
|
+
const handle = this.repairRetryHandle;
|
|
5063
|
+
this.repairRetryHandle = null;
|
|
5064
|
+
this.repairRetryToken += 1;
|
|
5065
|
+
this.cancelRepairRetryCallback(handle);
|
|
5066
|
+
}
|
|
5067
|
+
scheduleQuietRepairRetry() {
|
|
5068
|
+
if (this.disposed || this.repairRetryHandle !== null || this.repairPromise || !this.listTransport || this.deliveredThrough >= this.snapshot.lastSequence) {
|
|
5069
|
+
return;
|
|
5070
|
+
}
|
|
5071
|
+
this.repairFailureAttempt += 1;
|
|
5072
|
+
const delayMs = Math.min(
|
|
5073
|
+
this.maxRepairRetryDelayMs,
|
|
5074
|
+
this.initialRepairRetryDelayMs * 2 ** Math.min(30, Math.max(0, this.repairFailureAttempt - 1))
|
|
5075
|
+
);
|
|
5076
|
+
const retryToken = ++this.repairRetryToken;
|
|
5077
|
+
let handle;
|
|
5078
|
+
handle = this.scheduleRepairRetryCallback(() => {
|
|
5079
|
+
if (retryToken !== this.repairRetryToken || this.repairRetryHandle !== handle) {
|
|
5080
|
+
return;
|
|
5081
|
+
}
|
|
5082
|
+
this.repairRetryHandle = null;
|
|
5083
|
+
if (this.disposed) return;
|
|
5084
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
5085
|
+
isRepairing: true,
|
|
5086
|
+
error: null
|
|
5087
|
+
});
|
|
5088
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
5089
|
+
this.startRepair(this.snapshot.lastSequence);
|
|
5090
|
+
}, delayMs);
|
|
5091
|
+
this.repairRetryHandle = handle;
|
|
5092
|
+
}
|
|
5093
|
+
async repair(targetSequence, generation) {
|
|
5094
|
+
const startedAt = this.diagnosticNow();
|
|
5095
|
+
let pageCount = 0;
|
|
5096
|
+
let outcome = "success";
|
|
5097
|
+
try {
|
|
5098
|
+
let attempts = 0;
|
|
5099
|
+
while (generation === this.repairGeneration && this.deliveredThrough < targetSequence) {
|
|
5100
|
+
if (++attempts > 100) {
|
|
5101
|
+
throw new Error("Feed gap repair exceeded its page limit.");
|
|
5102
|
+
}
|
|
5103
|
+
const page = await this.list({
|
|
5104
|
+
afterSequence: this.deliveredThrough,
|
|
5105
|
+
limit: 500
|
|
5106
|
+
});
|
|
5107
|
+
pageCount += 1;
|
|
5108
|
+
if (generation !== this.repairGeneration) {
|
|
5109
|
+
outcome = "cancelled";
|
|
5110
|
+
return;
|
|
5111
|
+
}
|
|
5112
|
+
if (page.items.length === 0) {
|
|
5113
|
+
throw new Error(
|
|
5114
|
+
`Feed gap repair returned no item after sequence ${this.deliveredThrough}.`
|
|
5115
|
+
);
|
|
5116
|
+
}
|
|
5117
|
+
const deliveredBeforePage = this.deliveredThrough;
|
|
5118
|
+
const identityError = this.remember(page.items);
|
|
5119
|
+
if (identityError) throw identityError;
|
|
5120
|
+
const appended = this.drainContiguous(targetSequence);
|
|
5121
|
+
const reachedTarget = this.deliveredThrough >= targetSequence;
|
|
5122
|
+
if (reachedTarget) {
|
|
5123
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
5124
|
+
isRepairing: this.deliveredThrough < this.snapshot.lastSequence,
|
|
5125
|
+
error: null
|
|
5126
|
+
});
|
|
5127
|
+
}
|
|
5128
|
+
if (appended.length > 0) {
|
|
5129
|
+
this.emit({ type: "append", items: appended });
|
|
5130
|
+
}
|
|
5131
|
+
if (this.deliveredThrough === deliveredBeforePage) {
|
|
5132
|
+
throw new Error(
|
|
5133
|
+
`Feed gap repair did not return expected sequence ${deliveredBeforePage + 1}.`
|
|
5134
|
+
);
|
|
5135
|
+
}
|
|
5136
|
+
if (this.deliveredThrough < targetSequence && !page.hasMoreAfter && (page.lastSequence || 0) < targetSequence) {
|
|
5137
|
+
throw new Error(
|
|
5138
|
+
`Feed gap remains after sequence ${this.deliveredThrough}.`
|
|
5139
|
+
);
|
|
5140
|
+
}
|
|
5141
|
+
}
|
|
5142
|
+
if (generation !== this.repairGeneration) {
|
|
5143
|
+
outcome = "cancelled";
|
|
5144
|
+
return;
|
|
5145
|
+
}
|
|
5146
|
+
} catch (error) {
|
|
5147
|
+
if (generation !== this.repairGeneration) {
|
|
5148
|
+
outcome = "cancelled";
|
|
5149
|
+
return;
|
|
5150
|
+
}
|
|
5151
|
+
outcome = "failure";
|
|
5152
|
+
this.snapshot = snapshotWith(this.snapshot, {
|
|
5153
|
+
isRepairing: false,
|
|
5154
|
+
error: error instanceof Error ? error : new Error(String(error))
|
|
5155
|
+
});
|
|
5156
|
+
this.emit({ type: "reset", snapshot: this.snapshot });
|
|
5157
|
+
} finally {
|
|
5158
|
+
this.emitDiagnostic({
|
|
5159
|
+
type: "gap_repair",
|
|
5160
|
+
outcome,
|
|
5161
|
+
durationMs: Math.max(0, this.diagnosticNow() - startedAt),
|
|
5162
|
+
pageCount,
|
|
5163
|
+
repairedThroughSequence: this.deliveredThrough,
|
|
5164
|
+
targetSequence
|
|
5165
|
+
});
|
|
5166
|
+
}
|
|
5167
|
+
}
|
|
5168
|
+
emit(change) {
|
|
5169
|
+
for (const subscriber of this.subscribers) {
|
|
5170
|
+
try {
|
|
5171
|
+
subscriber(change);
|
|
5172
|
+
} catch (error) {
|
|
5173
|
+
console.error("[Granular] Session feed subscriber failed", error);
|
|
5174
|
+
}
|
|
5175
|
+
}
|
|
5176
|
+
}
|
|
5177
|
+
};
|
|
5178
|
+
function sameTransientSet(left, right) {
|
|
5179
|
+
return stableValueFingerprint(left) === stableValueFingerprint(right);
|
|
5180
|
+
}
|
|
5181
|
+
function operationId(prefix) {
|
|
5182
|
+
const randomUuid = globalThis.crypto?.randomUUID?.();
|
|
5183
|
+
return randomUuid ? `${prefix}_${randomUuid}` : `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
5184
|
+
}
|
|
5185
|
+
function unwrapPublishedItem(value) {
|
|
5186
|
+
const record = asRecord(value);
|
|
5187
|
+
const directItems = Array.isArray(record?.items) ? record.items : [];
|
|
5188
|
+
const durable = asRecord(record?.durable);
|
|
5189
|
+
const durableItems = Array.isArray(durable?.items) ? durable.items : [];
|
|
5190
|
+
return record?.item || directItems[0] || durableItems[0] || value;
|
|
5191
|
+
}
|
|
5192
|
+
function requireFeedbackItem(value) {
|
|
5193
|
+
const item = normalizeFeedItem(unwrapPublishedItem(value));
|
|
5194
|
+
if (!item || item.kind !== "feedback") {
|
|
5195
|
+
throw new Error("Feed publisher returned an invalid feedback item.");
|
|
5196
|
+
}
|
|
5197
|
+
return item;
|
|
5198
|
+
}
|
|
5199
|
+
function requireTransientFeedbackItem(value) {
|
|
5200
|
+
const item = normalizeTransientFeedItem(unwrapPublishedItem(value));
|
|
5201
|
+
if (!item || item.kind !== "feedback") {
|
|
5202
|
+
throw new Error(
|
|
5203
|
+
"Feed publisher returned an invalid transient feedback item."
|
|
5204
|
+
);
|
|
5205
|
+
}
|
|
5206
|
+
return item;
|
|
5207
|
+
}
|
|
5208
|
+
function createFeedPublisher(publish) {
|
|
5209
|
+
const makeHandle = (initial) => {
|
|
5210
|
+
let current = initial;
|
|
5211
|
+
let mutationQueue = Promise.resolve();
|
|
5212
|
+
const enqueueMutation = (mutation) => {
|
|
5213
|
+
const result = mutationQueue.then(mutation);
|
|
5214
|
+
mutationQueue = result.then(
|
|
5215
|
+
() => void 0,
|
|
5216
|
+
() => void 0
|
|
5217
|
+
);
|
|
5218
|
+
return result;
|
|
5219
|
+
};
|
|
5220
|
+
const handle = {
|
|
5221
|
+
get id() {
|
|
5222
|
+
return current.id;
|
|
5223
|
+
},
|
|
5224
|
+
get ordinal() {
|
|
5225
|
+
return current.ordinal;
|
|
5226
|
+
},
|
|
5227
|
+
get revision() {
|
|
5228
|
+
return current.revision;
|
|
5229
|
+
},
|
|
5230
|
+
update(text, options = {}) {
|
|
5231
|
+
const reservedOperationId = options.operationId || operationId("feed_transient_update");
|
|
5232
|
+
const reservedOptions = { ...options };
|
|
5233
|
+
return enqueueMutation(async () => {
|
|
5234
|
+
const response = await publish("feed.transient.update", {
|
|
5235
|
+
transientId: current.id,
|
|
5236
|
+
expectedRevision: current.revision,
|
|
5237
|
+
text,
|
|
5238
|
+
...reservedOptions,
|
|
5239
|
+
operationId: reservedOperationId
|
|
5240
|
+
});
|
|
5241
|
+
current = requireTransientFeedbackItem(response);
|
|
5242
|
+
return handle;
|
|
5243
|
+
});
|
|
5244
|
+
},
|
|
5245
|
+
settle(text, options = {}) {
|
|
5246
|
+
const reservedOperationId = options.operationId || operationId("feed_transient_settle");
|
|
5247
|
+
const reservedOptions = { ...options };
|
|
5248
|
+
return enqueueMutation(async () => {
|
|
5249
|
+
const response = await publish("feed.transient.settle", {
|
|
5250
|
+
transientId: current.id,
|
|
5251
|
+
expectedRevision: current.revision,
|
|
5252
|
+
...text === void 0 ? {} : { text },
|
|
5253
|
+
...reservedOptions,
|
|
5254
|
+
operationId: reservedOperationId
|
|
5255
|
+
});
|
|
5256
|
+
const responseRecord = asRecord(response);
|
|
5257
|
+
const durable = asRecord(responseRecord?.durable);
|
|
5258
|
+
const durableItems = Array.isArray(durable?.items) ? durable.items : [];
|
|
5259
|
+
const rawItem = responseRecord?.item ?? responseRecord?.durableItem ?? durableItems[0];
|
|
5260
|
+
if (rawItem === null || rawItem === void 0) {
|
|
5261
|
+
return null;
|
|
5262
|
+
}
|
|
5263
|
+
return requireFeedbackItem(rawItem);
|
|
5264
|
+
});
|
|
5265
|
+
}
|
|
5266
|
+
};
|
|
5267
|
+
return handle;
|
|
5268
|
+
};
|
|
5269
|
+
return {
|
|
5270
|
+
async feedback(text, options = {}) {
|
|
5271
|
+
return requireFeedbackItem(
|
|
5272
|
+
await publish("feed.feedback", {
|
|
5273
|
+
text,
|
|
5274
|
+
...options,
|
|
5275
|
+
operationId: options.operationId || operationId("feed_feedback")
|
|
5276
|
+
})
|
|
5277
|
+
);
|
|
5278
|
+
},
|
|
5279
|
+
async transientFeedback(text, options = {}) {
|
|
5280
|
+
const item = requireTransientFeedbackItem(
|
|
5281
|
+
await publish("feed.transient.create", {
|
|
5282
|
+
text,
|
|
5283
|
+
...options,
|
|
5284
|
+
operationId: options.operationId || operationId("feed_transient_create")
|
|
5285
|
+
})
|
|
5286
|
+
);
|
|
5287
|
+
return makeHandle(item);
|
|
5288
|
+
}
|
|
5289
|
+
};
|
|
5290
|
+
}
|
|
5291
|
+
|
|
5292
|
+
// src/ws-client.ts
|
|
4014
5293
|
var GlobalWebSocket = void 0;
|
|
4015
5294
|
if (typeof globalThis !== "undefined" && globalThis.WebSocket) {
|
|
4016
5295
|
GlobalWebSocket = globalThis.WebSocket;
|
|
@@ -4024,8 +5303,42 @@ var DEFAULT_RPC_TIMEOUT_MS = 3e4;
|
|
|
4024
5303
|
var DOMAIN_PACKAGE_RPC_TIMEOUT_MS = 12e4;
|
|
4025
5304
|
var EFFECT_CONTROL_RPC_TIMEOUT_MS = 12e4;
|
|
4026
5305
|
var HARNESS_RUN_RPC_TIMEOUT_MS = 6e5;
|
|
5306
|
+
var HEARTBEAT_RPC_TIMEOUT_MS = 15e3;
|
|
5307
|
+
var DEFAULT_CONNECT_TIMEOUT_MS = 1e4;
|
|
4027
5308
|
var DEFAULT_RECONNECT_DELAY_MS = 3e3;
|
|
4028
5309
|
var DEFAULT_MAX_RECONNECT_ATTEMPTS = 5;
|
|
5310
|
+
var DOCUMENT_RESYNC_CLOSE_CODE = 4e3;
|
|
5311
|
+
var DOCUMENT_RESYNC_CLOSE_REASON = "Document resync required";
|
|
5312
|
+
function documentVersion(document) {
|
|
5313
|
+
const record = document;
|
|
5314
|
+
const epochValue = record.documentEpoch;
|
|
5315
|
+
const revisionValue = record.documentRevision;
|
|
5316
|
+
const epoch = epochValue === void 0 ? 0 : epochValue;
|
|
5317
|
+
const revision = revisionValue === void 0 ? 0 : revisionValue;
|
|
5318
|
+
if (!Number.isSafeInteger(epoch) || Number(epoch) < 0 || !Number.isSafeInteger(revision) || Number(revision) < 0) {
|
|
5319
|
+
throw new Error("Session document version metadata is invalid");
|
|
5320
|
+
}
|
|
5321
|
+
return {
|
|
5322
|
+
epoch: Number(epoch),
|
|
5323
|
+
revision: Number(revision)
|
|
5324
|
+
};
|
|
5325
|
+
}
|
|
5326
|
+
function stableDocumentFingerprint(value) {
|
|
5327
|
+
if (value === null) return "null";
|
|
5328
|
+
if (value === void 0) return '"[undefined]"';
|
|
5329
|
+
if (typeof value !== "object") return JSON.stringify(value) ?? String(value);
|
|
5330
|
+
if (value instanceof Date) return `date:${value.toISOString()}`;
|
|
5331
|
+
if (value instanceof Uint8Array) {
|
|
5332
|
+
return `bytes:${Array.from(value).join(",")}`;
|
|
5333
|
+
}
|
|
5334
|
+
if (Array.isArray(value)) {
|
|
5335
|
+
return `[${value.map(stableDocumentFingerprint).join(",")}]`;
|
|
5336
|
+
}
|
|
5337
|
+
const record = value;
|
|
5338
|
+
return `{${Object.keys(record).sort().map(
|
|
5339
|
+
(key) => `${JSON.stringify(key)}:${stableDocumentFingerprint(record[key])}`
|
|
5340
|
+
).join(",")}}`;
|
|
5341
|
+
}
|
|
4029
5342
|
function debugWs(...args) {
|
|
4030
5343
|
if (DEBUG_WS) {
|
|
4031
5344
|
console.log(...args);
|
|
@@ -4037,6 +5350,7 @@ function rpcTimeoutMsForMethod(method) {
|
|
|
4037
5350
|
case "domain.getSummary":
|
|
4038
5351
|
return DOMAIN_PACKAGE_RPC_TIMEOUT_MS;
|
|
4039
5352
|
case "client.heartbeat":
|
|
5353
|
+
return HEARTBEAT_RPC_TIMEOUT_MS;
|
|
4040
5354
|
case "effects.publishCatalog":
|
|
4041
5355
|
case "effects.resetCatalog":
|
|
4042
5356
|
case "effects.addCatalog":
|
|
@@ -4056,12 +5370,11 @@ var WSClient = class {
|
|
|
4056
5370
|
sessionId;
|
|
4057
5371
|
token;
|
|
4058
5372
|
messageQueue = [];
|
|
4059
|
-
syncHandlers = [];
|
|
4060
5373
|
rpcHandlers = /* @__PURE__ */ new Map();
|
|
4061
5374
|
eventHandlers = /* @__PURE__ */ new Map();
|
|
5375
|
+
canonicalDocumentQuarantineHandlers = /* @__PURE__ */ new Set();
|
|
4062
5376
|
nextRpcId = 1;
|
|
4063
5377
|
doc = Automerge__namespace.init();
|
|
4064
|
-
syncState = Automerge__namespace.initSyncState();
|
|
4065
5378
|
reconnectTimer = null;
|
|
4066
5379
|
tokenRefreshTimer = null;
|
|
4067
5380
|
isExplicitlyDisconnected = false;
|
|
@@ -4069,6 +5382,8 @@ var WSClient = class {
|
|
|
4069
5382
|
connectPromise = null;
|
|
4070
5383
|
connectionEpoch = 0;
|
|
4071
5384
|
cancelConnectAttempt = null;
|
|
5385
|
+
documentRepairRequired = false;
|
|
5386
|
+
canonicalDocumentActivated = false;
|
|
4072
5387
|
options;
|
|
4073
5388
|
constructor(options) {
|
|
4074
5389
|
this.options = options;
|
|
@@ -4087,8 +5402,12 @@ var WSClient = class {
|
|
|
4087
5402
|
return;
|
|
4088
5403
|
}
|
|
4089
5404
|
try {
|
|
4090
|
-
|
|
4091
|
-
this.
|
|
5405
|
+
const replacement = document instanceof Uint8Array ? Automerge__namespace.load(document) : Automerge__namespace.from(document);
|
|
5406
|
+
if (!this.canAcceptDocumentReplacement(replacement, false)) {
|
|
5407
|
+
return;
|
|
5408
|
+
}
|
|
5409
|
+
this.doc = replacement;
|
|
5410
|
+
this.rememberCanonicalActivation(replacement);
|
|
4092
5411
|
this.emit("sync", this.doc);
|
|
4093
5412
|
} catch (error) {
|
|
4094
5413
|
console.warn("[Granular] Failed to seed cached session document", error);
|
|
@@ -4219,6 +5538,26 @@ var WSClient = class {
|
|
|
4219
5538
|
}
|
|
4220
5539
|
}
|
|
4221
5540
|
}
|
|
5541
|
+
/**
|
|
5542
|
+
* Mark the current transport as unusable and schedule the normal reconnect
|
|
5543
|
+
* path. Browser WebSockets can remain in OPEN state after a proxy/worker
|
|
5544
|
+
* restart, so a timed-out heartbeat must revoke the stale socket explicitly.
|
|
5545
|
+
*/
|
|
5546
|
+
reportTransportFailure(reason = "WebSocket transport failed") {
|
|
5547
|
+
if (this.isExplicitlyDisconnected) return;
|
|
5548
|
+
const socket = this.ws;
|
|
5549
|
+
this.connectionEpoch += 1;
|
|
5550
|
+
this.ws = null;
|
|
5551
|
+
try {
|
|
5552
|
+
socket?.close(4001, "Transport failure");
|
|
5553
|
+
} catch {
|
|
5554
|
+
}
|
|
5555
|
+
this.handleDisconnect({
|
|
5556
|
+
code: 4001,
|
|
5557
|
+
reason: reason instanceof Error ? reason.message : String(reason),
|
|
5558
|
+
wasClean: false
|
|
5559
|
+
});
|
|
5560
|
+
}
|
|
4222
5561
|
async connectAttempt(signal) {
|
|
4223
5562
|
if (signal?.aborted) throw new Error("WebSocket connect aborted");
|
|
4224
5563
|
const token = await this.resolveTokenForConnect();
|
|
@@ -4250,10 +5589,17 @@ var WSClient = class {
|
|
|
4250
5589
|
this.ws = socket;
|
|
4251
5590
|
return new Promise((resolve, reject) => {
|
|
4252
5591
|
let settled = false;
|
|
5592
|
+
const configuredConnectTimeoutMs = this.options.connectTimeoutMs;
|
|
5593
|
+
const connectTimeoutMs = typeof configuredConnectTimeoutMs === "number" && Number.isFinite(configuredConnectTimeoutMs) && configuredConnectTimeoutMs > 0 ? configuredConnectTimeoutMs : DEFAULT_CONNECT_TIMEOUT_MS;
|
|
5594
|
+
let connectTimeout = null;
|
|
4253
5595
|
const isCurrent = () => this.connectionEpoch === epoch && this.ws === socket;
|
|
4254
5596
|
const finish = (error) => {
|
|
4255
5597
|
if (settled) return;
|
|
4256
5598
|
settled = true;
|
|
5599
|
+
if (connectTimeout) {
|
|
5600
|
+
clearTimeout(connectTimeout);
|
|
5601
|
+
connectTimeout = null;
|
|
5602
|
+
}
|
|
4257
5603
|
if (this.cancelConnectAttempt === handleAbort) {
|
|
4258
5604
|
this.cancelConnectAttempt = null;
|
|
4259
5605
|
}
|
|
@@ -4318,6 +5664,17 @@ var WSClient = class {
|
|
|
4318
5664
|
wasClean: close.wasClean
|
|
4319
5665
|
});
|
|
4320
5666
|
};
|
|
5667
|
+
connectTimeout = setTimeout(() => {
|
|
5668
|
+
if (!isCurrent()) return;
|
|
5669
|
+
this.connectionEpoch += 1;
|
|
5670
|
+
this.ws = null;
|
|
5671
|
+
closeStaleSocket();
|
|
5672
|
+
finish(
|
|
5673
|
+
new Error(
|
|
5674
|
+
`WebSocket connect timed out after ${connectTimeoutMs}ms`
|
|
5675
|
+
)
|
|
5676
|
+
);
|
|
5677
|
+
}, connectTimeoutMs);
|
|
4321
5678
|
signal?.addEventListener("abort", handleAbort, { once: true });
|
|
4322
5679
|
const nodeSocket = socket;
|
|
4323
5680
|
if (typeof nodeSocket.on === "function") {
|
|
@@ -4356,11 +5713,12 @@ var WSClient = class {
|
|
|
4356
5713
|
});
|
|
4357
5714
|
this.messageQueue = [];
|
|
4358
5715
|
}
|
|
4359
|
-
emitReconnectErrorMessage(error) {
|
|
5716
|
+
emitReconnectErrorMessage(error, terminal = false) {
|
|
4360
5717
|
const reconnectInfo = {
|
|
4361
5718
|
error,
|
|
4362
5719
|
sessionId: this.sessionId,
|
|
4363
|
-
timestamp: Date.now()
|
|
5720
|
+
timestamp: Date.now(),
|
|
5721
|
+
terminal
|
|
4364
5722
|
};
|
|
4365
5723
|
this.emit("reconnect_error", reconnectInfo);
|
|
4366
5724
|
if (this.options.onReconnectError) {
|
|
@@ -4380,7 +5738,8 @@ var WSClient = class {
|
|
|
4380
5738
|
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;
|
|
4381
5739
|
if (this.reconnectAttempts >= maxReconnectAttempts) {
|
|
4382
5740
|
this.emitReconnectErrorMessage(
|
|
4383
|
-
`WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s)
|
|
5741
|
+
`WebSocket reconnect attempts exhausted after ${maxReconnectAttempts} attempt(s).`,
|
|
5742
|
+
true
|
|
4384
5743
|
);
|
|
4385
5744
|
return null;
|
|
4386
5745
|
}
|
|
@@ -4410,6 +5769,190 @@ var WSClient = class {
|
|
|
4410
5769
|
const suffix = details ? ` (${details})` : "";
|
|
4411
5770
|
return new Error(`WebSocket disconnected${suffix}`);
|
|
4412
5771
|
}
|
|
5772
|
+
decodeDocumentBytes(payload, envelopeType) {
|
|
5773
|
+
if (typeof payload === "string") {
|
|
5774
|
+
const binaryString = atob(payload);
|
|
5775
|
+
const bytes = new Uint8Array(binaryString.length);
|
|
5776
|
+
for (let index = 0; index < binaryString.length; index += 1) {
|
|
5777
|
+
bytes[index] = binaryString.charCodeAt(index);
|
|
5778
|
+
}
|
|
5779
|
+
return bytes;
|
|
5780
|
+
}
|
|
5781
|
+
if (payload instanceof Uint8Array) {
|
|
5782
|
+
return payload;
|
|
5783
|
+
}
|
|
5784
|
+
if (Array.isArray(payload) && payload.every(
|
|
5785
|
+
(value) => typeof value === "number" && Number.isInteger(value) && value >= 0 && value <= 255
|
|
5786
|
+
)) {
|
|
5787
|
+
return new Uint8Array(payload);
|
|
5788
|
+
}
|
|
5789
|
+
throw new Error(`${envelopeType} payload is not valid byte data`);
|
|
5790
|
+
}
|
|
5791
|
+
rememberCanonicalActivation(document) {
|
|
5792
|
+
if (isCanonicalSessionFeedDocument(document)) {
|
|
5793
|
+
this.canonicalDocumentActivated = true;
|
|
5794
|
+
}
|
|
5795
|
+
}
|
|
5796
|
+
canAcceptDocumentReplacement(replacement, strictlyNewer) {
|
|
5797
|
+
const replacementCanonical = isCanonicalSessionFeedDocument(replacement);
|
|
5798
|
+
const currentCanonical = isCanonicalSessionFeedDocument(this.doc);
|
|
5799
|
+
if (replacementCanonical) {
|
|
5800
|
+
this.canonicalDocumentActivated = true;
|
|
5801
|
+
}
|
|
5802
|
+
if (this.canonicalDocumentActivated && !replacementCanonical) {
|
|
5803
|
+
debugWs(
|
|
5804
|
+
"[Granular DEBUG] Rejected session document replacement that would deactivate the canonical feed."
|
|
5805
|
+
);
|
|
5806
|
+
return false;
|
|
5807
|
+
}
|
|
5808
|
+
const current = documentVersion(this.doc);
|
|
5809
|
+
const incoming = documentVersion(replacement);
|
|
5810
|
+
if (incoming.epoch < current.epoch) {
|
|
5811
|
+
debugWs(
|
|
5812
|
+
`[Granular DEBUG] Rejected stale session document epoch ${incoming.epoch}; current epoch is ${current.epoch}.`
|
|
5813
|
+
);
|
|
5814
|
+
return false;
|
|
5815
|
+
}
|
|
5816
|
+
if (incoming.epoch === current.epoch) {
|
|
5817
|
+
const minimumRevision = strictlyNewer ? current.revision + 1 : current.revision;
|
|
5818
|
+
if (incoming.revision < minimumRevision) {
|
|
5819
|
+
debugWs(
|
|
5820
|
+
`[Granular DEBUG] Rejected stale session document revision ${incoming.revision}; current revision is ${current.revision}.`
|
|
5821
|
+
);
|
|
5822
|
+
return false;
|
|
5823
|
+
}
|
|
5824
|
+
}
|
|
5825
|
+
if (replacementCanonical) {
|
|
5826
|
+
const incomingSnapshot = readSessionFeedSnapshot(replacement);
|
|
5827
|
+
if (incomingSnapshot.error) {
|
|
5828
|
+
this.documentRepairRequired = true;
|
|
5829
|
+
this.exposeCanonicalQuarantine(
|
|
5830
|
+
replacement,
|
|
5831
|
+
currentCanonical,
|
|
5832
|
+
incomingSnapshot.error
|
|
5833
|
+
);
|
|
5834
|
+
debugWs(
|
|
5835
|
+
`[Granular DEBUG] Rejected malformed canonical snapshot: ${incomingSnapshot.error.message}`
|
|
5836
|
+
);
|
|
5837
|
+
return false;
|
|
5838
|
+
}
|
|
5839
|
+
}
|
|
5840
|
+
const canonicalError = this.canonicalReplacementError(replacement);
|
|
5841
|
+
if (canonicalError) {
|
|
5842
|
+
debugWs(`[Granular DEBUG] Rejected session snapshot: ${canonicalError}`);
|
|
5843
|
+
return false;
|
|
5844
|
+
}
|
|
5845
|
+
if (this.canonicalDocumentActivated && incoming.epoch === current.epoch && incoming.revision === current.revision && !readSessionFeedSnapshot(this.doc).error && stableDocumentFingerprint(Automerge__namespace.toJS(replacement)) !== stableDocumentFingerprint(Automerge__namespace.toJS(this.doc))) {
|
|
5846
|
+
debugWs(
|
|
5847
|
+
"[Granular DEBUG] Rejected divergent canonical snapshot at the accepted document version."
|
|
5848
|
+
);
|
|
5849
|
+
return false;
|
|
5850
|
+
}
|
|
5851
|
+
return true;
|
|
5852
|
+
}
|
|
5853
|
+
exposeCanonicalQuarantine(replacement, currentCanonical, error) {
|
|
5854
|
+
const version = documentVersion(replacement);
|
|
5855
|
+
if (currentCanonical) {
|
|
5856
|
+
const quarantine2 = Object.freeze({
|
|
5857
|
+
documentEpoch: version.epoch,
|
|
5858
|
+
documentRevision: version.revision,
|
|
5859
|
+
error: new Error(error.message)
|
|
5860
|
+
});
|
|
5861
|
+
for (const handler of this.canonicalDocumentQuarantineHandlers) {
|
|
5862
|
+
handler(quarantine2);
|
|
5863
|
+
}
|
|
5864
|
+
return;
|
|
5865
|
+
}
|
|
5866
|
+
const accepted = Automerge__namespace.toJS(this.doc);
|
|
5867
|
+
const quarantine = Automerge__namespace.from({
|
|
5868
|
+
...accepted,
|
|
5869
|
+
documentEpoch: version.epoch,
|
|
5870
|
+
documentRevision: version.revision,
|
|
5871
|
+
feed: {
|
|
5872
|
+
activation: { mode: "canonical" }
|
|
5873
|
+
}
|
|
5874
|
+
});
|
|
5875
|
+
this.doc = quarantine;
|
|
5876
|
+
this.emit("sync", this.doc);
|
|
5877
|
+
debugWs(
|
|
5878
|
+
`[Granular DEBUG] Canonical activation quarantined pending repair: ${error.message}`
|
|
5879
|
+
);
|
|
5880
|
+
}
|
|
5881
|
+
canonicalReplacementError(replacement) {
|
|
5882
|
+
if (!isCanonicalSessionFeedDocument(replacement)) return null;
|
|
5883
|
+
const incoming = readSessionFeedSnapshot(replacement);
|
|
5884
|
+
if (incoming.error) {
|
|
5885
|
+
return `canonical feed is invalid: ${incoming.error.message}`;
|
|
5886
|
+
}
|
|
5887
|
+
if (!this.canonicalDocumentActivated) return null;
|
|
5888
|
+
const current = readSessionFeedSnapshot(this.doc);
|
|
5889
|
+
if (current.error) {
|
|
5890
|
+
return null;
|
|
5891
|
+
}
|
|
5892
|
+
if (incoming.lastSequence < current.lastSequence) {
|
|
5893
|
+
return `canonical lastSequence ${incoming.lastSequence} regresses accepted ${current.lastSequence}`;
|
|
5894
|
+
}
|
|
5895
|
+
const currentBySequence = new Map(
|
|
5896
|
+
current.tail.map((item) => [item.sequence, item])
|
|
5897
|
+
);
|
|
5898
|
+
for (const item of incoming.tail) {
|
|
5899
|
+
const accepted = currentBySequence.get(item.sequence);
|
|
5900
|
+
if (accepted && stableDocumentFingerprint(item) !== stableDocumentFingerprint(accepted)) {
|
|
5901
|
+
return `canonical occurrence ${item.sequence} conflicts with accepted history`;
|
|
5902
|
+
}
|
|
5903
|
+
}
|
|
5904
|
+
return null;
|
|
5905
|
+
}
|
|
5906
|
+
assertIncrementalDocumentIsSafe(replacement) {
|
|
5907
|
+
const missingDependencies = Automerge__namespace.getMissingDeps(replacement, []);
|
|
5908
|
+
if (missingDependencies.length > 0) {
|
|
5909
|
+
throw new Error(
|
|
5910
|
+
`Incremental session update is missing ${missingDependencies.length} causal dependency/dependencies`
|
|
5911
|
+
);
|
|
5912
|
+
}
|
|
5913
|
+
if (this.canonicalDocumentActivated && !isCanonicalSessionFeedDocument(replacement)) {
|
|
5914
|
+
throw new Error(
|
|
5915
|
+
"Incremental session update would deactivate the canonical feed"
|
|
5916
|
+
);
|
|
5917
|
+
}
|
|
5918
|
+
const current = documentVersion(this.doc);
|
|
5919
|
+
const incoming = documentVersion(replacement);
|
|
5920
|
+
if (incoming.epoch < current.epoch || incoming.epoch === current.epoch && incoming.revision < current.revision) {
|
|
5921
|
+
throw new Error("Incremental session update regressed document version");
|
|
5922
|
+
}
|
|
5923
|
+
const canonicalError = this.canonicalReplacementError(replacement);
|
|
5924
|
+
if (canonicalError) {
|
|
5925
|
+
throw new Error(canonicalError);
|
|
5926
|
+
}
|
|
5927
|
+
if (this.canonicalDocumentActivated && incoming.epoch === current.epoch && incoming.revision === current.revision && stableDocumentFingerprint(Automerge__namespace.toJS(replacement)) !== stableDocumentFingerprint(Automerge__namespace.toJS(this.doc))) {
|
|
5928
|
+
throw new Error(
|
|
5929
|
+
"Incremental session update diverged without advancing document revision"
|
|
5930
|
+
);
|
|
5931
|
+
}
|
|
5932
|
+
}
|
|
5933
|
+
requireDocumentResync(envelopeType, error) {
|
|
5934
|
+
this.documentRepairRequired = true;
|
|
5935
|
+
console.warn(
|
|
5936
|
+
`[Granular] ${envelopeType} could not be applied; reconnecting for a fresh session snapshot.`,
|
|
5937
|
+
error
|
|
5938
|
+
);
|
|
5939
|
+
const socket = this.ws;
|
|
5940
|
+
if (!socket) {
|
|
5941
|
+
this.scheduleReconnectAttempt();
|
|
5942
|
+
return;
|
|
5943
|
+
}
|
|
5944
|
+
this.connectionEpoch += 1;
|
|
5945
|
+
this.ws = null;
|
|
5946
|
+
try {
|
|
5947
|
+
socket.close(DOCUMENT_RESYNC_CLOSE_CODE, DOCUMENT_RESYNC_CLOSE_REASON);
|
|
5948
|
+
} catch {
|
|
5949
|
+
}
|
|
5950
|
+
this.handleDisconnect({
|
|
5951
|
+
code: DOCUMENT_RESYNC_CLOSE_CODE,
|
|
5952
|
+
reason: DOCUMENT_RESYNC_CLOSE_REASON,
|
|
5953
|
+
wasClean: false
|
|
5954
|
+
});
|
|
5955
|
+
}
|
|
4413
5956
|
handleDisconnect(close = {}) {
|
|
4414
5957
|
const unexpected = !this.isExplicitlyDisconnected;
|
|
4415
5958
|
const info = {
|
|
@@ -4428,11 +5971,11 @@ var WSClient = class {
|
|
|
4428
5971
|
}
|
|
4429
5972
|
if (unexpected) {
|
|
4430
5973
|
const disconnectError = this.buildDisconnectError(info);
|
|
4431
|
-
this.rejectPending(disconnectError);
|
|
4432
|
-
this.emit("disconnect", info);
|
|
4433
5974
|
const reconnectDelayMs = this.scheduleReconnectAttempt();
|
|
4434
5975
|
info.reconnectScheduled = reconnectDelayMs !== null;
|
|
4435
5976
|
if (reconnectDelayMs !== null) info.reconnectDelayMs = reconnectDelayMs;
|
|
5977
|
+
this.rejectPending(disconnectError);
|
|
5978
|
+
this.emit("disconnect", info);
|
|
4436
5979
|
if (this.options.onUnexpectedClose) {
|
|
4437
5980
|
try {
|
|
4438
5981
|
this.options.onUnexpectedClose(info);
|
|
@@ -4452,100 +5995,109 @@ var WSClient = class {
|
|
|
4452
5995
|
JSON.stringify(message).slice(0, 500)
|
|
4453
5996
|
);
|
|
4454
5997
|
if ("type" in message && message.type === "sync") {
|
|
4455
|
-
|
|
4456
|
-
|
|
5998
|
+
this.requireDocumentResync(
|
|
5999
|
+
"Unsupported Automerge sync envelope",
|
|
6000
|
+
new Error("Use snapshot, snapshot_reset, or change.")
|
|
6001
|
+
);
|
|
6002
|
+
return;
|
|
6003
|
+
}
|
|
6004
|
+
if ("type" in message && message.type === "snapshot_reset") {
|
|
6005
|
+
const resetMessage = message;
|
|
4457
6006
|
try {
|
|
4458
|
-
|
|
4459
|
-
|
|
4460
|
-
const binaryString = atob(payload);
|
|
4461
|
-
const len = binaryString.length;
|
|
4462
|
-
bytes = new Uint8Array(len);
|
|
4463
|
-
for (let i = 0; i < len; i++) {
|
|
4464
|
-
bytes[i] = binaryString.charCodeAt(i);
|
|
4465
|
-
}
|
|
4466
|
-
} else if (Array.isArray(payload)) {
|
|
4467
|
-
bytes = new Uint8Array(payload);
|
|
4468
|
-
} else if (payload instanceof Uint8Array) {
|
|
4469
|
-
bytes = payload;
|
|
4470
|
-
} else {
|
|
4471
|
-
return;
|
|
6007
|
+
if (!Number.isSafeInteger(resetMessage.documentEpoch) || resetMessage.documentEpoch <= 0 || !Number.isSafeInteger(resetMessage.documentRevision) || resetMessage.documentRevision < 0 || !Array.isArray(resetMessage.data)) {
|
|
6008
|
+
throw new Error("snapshot_reset metadata is invalid");
|
|
4472
6009
|
}
|
|
4473
|
-
|
|
4474
|
-
|
|
4475
|
-
|
|
4476
|
-
|
|
4477
|
-
|
|
6010
|
+
const replacement = Automerge__namespace.load(
|
|
6011
|
+
this.decodeDocumentBytes(
|
|
6012
|
+
resetMessage.data,
|
|
6013
|
+
"Automerge snapshot_reset"
|
|
6014
|
+
)
|
|
4478
6015
|
);
|
|
4479
|
-
|
|
4480
|
-
|
|
4481
|
-
const
|
|
4482
|
-
if (
|
|
4483
|
-
|
|
4484
|
-
"
|
|
4485
|
-
Object.keys(docAny.catalog || {})
|
|
4486
|
-
);
|
|
4487
|
-
debugWs(
|
|
4488
|
-
"[Granular DEBUG] RawToolCatalogs:",
|
|
4489
|
-
Object.keys(docAny.catalog.rawToolCatalogs || {})
|
|
4490
|
-
);
|
|
4491
|
-
} else {
|
|
4492
|
-
debugWs(
|
|
4493
|
-
"[Granular DEBUG] Doc synced but no catalog yet. Keys in doc:",
|
|
4494
|
-
Object.keys(docAny)
|
|
6016
|
+
const replacementVersion = documentVersion(replacement);
|
|
6017
|
+
const replacementEpoch = replacementVersion.epoch;
|
|
6018
|
+
const replacementRevision = replacementVersion.revision;
|
|
6019
|
+
if (replacementEpoch !== resetMessage.documentEpoch || replacementRevision !== resetMessage.documentRevision) {
|
|
6020
|
+
throw new Error(
|
|
6021
|
+
"snapshot_reset metadata does not match the saved document"
|
|
4495
6022
|
);
|
|
4496
6023
|
}
|
|
4497
|
-
this.
|
|
4498
|
-
|
|
4499
|
-
|
|
4500
|
-
|
|
4501
|
-
|
|
4502
|
-
|
|
4503
|
-
|
|
4504
|
-
|
|
4505
|
-
this.emit("sync", this.doc);
|
|
4506
|
-
debugWs(
|
|
4507
|
-
"[Granular DEBUG] applyChanges succeeded. Doc:",
|
|
4508
|
-
JSON.stringify(Automerge__namespace.toJS(this.doc))
|
|
4509
|
-
);
|
|
4510
|
-
} catch (applyError) {
|
|
4511
|
-
console.warn(
|
|
4512
|
-
"[Granular] Failed to apply sync message (both sync & applyChanges)",
|
|
4513
|
-
e,
|
|
4514
|
-
applyError
|
|
4515
|
-
);
|
|
6024
|
+
if (!this.canAcceptDocumentReplacement(replacement, true)) {
|
|
6025
|
+
if (this.documentRepairRequired) {
|
|
6026
|
+
this.requireDocumentResync(
|
|
6027
|
+
"Stale Automerge snapshot_reset during document repair",
|
|
6028
|
+
new Error("Replacement reset did not advance accepted state")
|
|
6029
|
+
);
|
|
6030
|
+
}
|
|
6031
|
+
return;
|
|
4516
6032
|
}
|
|
6033
|
+
this.doc = replacement;
|
|
6034
|
+
this.documentRepairRequired = false;
|
|
6035
|
+
this.rememberCanonicalActivation(replacement);
|
|
6036
|
+
this.emit("snapshot_reset", {
|
|
6037
|
+
documentEpoch: replacementEpoch,
|
|
6038
|
+
documentRevision: replacementRevision
|
|
6039
|
+
});
|
|
6040
|
+
this.emit("sync", this.doc);
|
|
6041
|
+
} catch (error) {
|
|
6042
|
+
this.requireDocumentResync("Automerge snapshot_reset", error);
|
|
4517
6043
|
}
|
|
4518
6044
|
return;
|
|
4519
6045
|
}
|
|
4520
6046
|
if ("type" in message && message.type === "snapshot") {
|
|
4521
6047
|
const snapshotMessage = message;
|
|
4522
6048
|
try {
|
|
4523
|
-
const bytes =
|
|
6049
|
+
const bytes = this.decodeDocumentBytes(
|
|
6050
|
+
snapshotMessage.data,
|
|
6051
|
+
"Automerge snapshot"
|
|
6052
|
+
);
|
|
4524
6053
|
debugWs(
|
|
4525
6054
|
"[Granular DEBUG] Loading Automerge session snapshot bytes:",
|
|
4526
6055
|
bytes.length
|
|
4527
6056
|
);
|
|
4528
|
-
|
|
6057
|
+
const replacement = Automerge__namespace.load(bytes);
|
|
6058
|
+
if (!this.canAcceptDocumentReplacement(replacement, false)) {
|
|
6059
|
+
if (this.documentRepairRequired) {
|
|
6060
|
+
this.requireDocumentResync(
|
|
6061
|
+
"Stale Automerge snapshot during document repair",
|
|
6062
|
+
new Error("Replacement snapshot regressed accepted state")
|
|
6063
|
+
);
|
|
6064
|
+
}
|
|
6065
|
+
return;
|
|
6066
|
+
}
|
|
6067
|
+
this.doc = replacement;
|
|
6068
|
+
this.documentRepairRequired = false;
|
|
6069
|
+
this.rememberCanonicalActivation(replacement);
|
|
4529
6070
|
this.emit("sync", this.doc);
|
|
4530
6071
|
debugWs(
|
|
4531
6072
|
"[Granular DEBUG] Automerge session snapshot loaded. Doc:",
|
|
4532
6073
|
JSON.stringify(Automerge__namespace.toJS(this.doc))
|
|
4533
6074
|
);
|
|
4534
|
-
} catch (
|
|
4535
|
-
|
|
6075
|
+
} catch (error) {
|
|
6076
|
+
this.requireDocumentResync("Automerge snapshot", error);
|
|
4536
6077
|
}
|
|
4537
6078
|
return;
|
|
4538
6079
|
}
|
|
4539
6080
|
if ("type" in message && message.type === "change") {
|
|
6081
|
+
if (this.documentRepairRequired) {
|
|
6082
|
+
debugWs(
|
|
6083
|
+
"[Granular DEBUG] Ignoring raw change while a replacement snapshot is required."
|
|
6084
|
+
);
|
|
6085
|
+
return;
|
|
6086
|
+
}
|
|
4540
6087
|
const changeMessage = message;
|
|
4541
6088
|
try {
|
|
4542
|
-
const bytes =
|
|
6089
|
+
const bytes = this.decodeDocumentBytes(
|
|
6090
|
+
changeMessage.data,
|
|
6091
|
+
"Automerge change"
|
|
6092
|
+
);
|
|
4543
6093
|
const [newDoc] = Automerge__namespace.applyChanges(this.doc, [bytes]);
|
|
6094
|
+
this.assertIncrementalDocumentIsSafe(newDoc);
|
|
4544
6095
|
this.doc = newDoc;
|
|
6096
|
+
this.rememberCanonicalActivation(newDoc);
|
|
4545
6097
|
this.emit("change", changeMessage);
|
|
4546
6098
|
this.emit("sync", this.doc);
|
|
4547
|
-
} catch (
|
|
4548
|
-
|
|
6099
|
+
} catch (error) {
|
|
6100
|
+
this.requireDocumentResync("Automerge change message", error);
|
|
4549
6101
|
}
|
|
4550
6102
|
return;
|
|
4551
6103
|
}
|
|
@@ -4664,6 +6216,18 @@ var WSClient = class {
|
|
|
4664
6216
|
}
|
|
4665
6217
|
this.eventHandlers.get(event).push(handler);
|
|
4666
6218
|
}
|
|
6219
|
+
/**
|
|
6220
|
+
* Subscribe to rejected canonical replacement metadata without exposing the
|
|
6221
|
+
* malformed document through the public sync stream.
|
|
6222
|
+
*
|
|
6223
|
+
* @internal Session uses this to quarantine only its feed projection.
|
|
6224
|
+
*/
|
|
6225
|
+
onCanonicalDocumentQuarantine(handler) {
|
|
6226
|
+
this.canonicalDocumentQuarantineHandlers.add(handler);
|
|
6227
|
+
return () => {
|
|
6228
|
+
this.canonicalDocumentQuarantineHandlers.delete(handler);
|
|
6229
|
+
};
|
|
6230
|
+
}
|
|
4667
6231
|
/**
|
|
4668
6232
|
* Register an RPC handler for incoming server requests
|
|
4669
6233
|
* @param {string} method - RPC method name
|
|
@@ -4721,7 +6285,7 @@ var WSClient = class {
|
|
|
4721
6285
|
};
|
|
4722
6286
|
|
|
4723
6287
|
// src/prompt-utils.ts
|
|
4724
|
-
function
|
|
6288
|
+
function asRecord2(value) {
|
|
4725
6289
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
4726
6290
|
return value;
|
|
4727
6291
|
}
|
|
@@ -4736,7 +6300,7 @@ function parseJsonPromptChoiceOption(option) {
|
|
|
4736
6300
|
if (!trimmed.startsWith("{") || !trimmed.endsWith("}")) return null;
|
|
4737
6301
|
try {
|
|
4738
6302
|
const parsed = JSON.parse(trimmed);
|
|
4739
|
-
return
|
|
6303
|
+
return asRecord2(parsed);
|
|
4740
6304
|
} catch {
|
|
4741
6305
|
return null;
|
|
4742
6306
|
}
|
|
@@ -4798,33 +6362,37 @@ function normalizePromptType(raw) {
|
|
|
4798
6362
|
if (kind === "confirm" || kind === "choice" || kind === "input") return kind;
|
|
4799
6363
|
if (promptType === "confirm" || promptType === "choice" || promptType === "input")
|
|
4800
6364
|
return promptType;
|
|
4801
|
-
return
|
|
6365
|
+
return null;
|
|
4802
6366
|
}
|
|
4803
6367
|
function normalizePrompt(rawValue) {
|
|
4804
|
-
const raw =
|
|
6368
|
+
const raw = asRecord2(rawValue);
|
|
4805
6369
|
if (!raw) return null;
|
|
4806
|
-
const promptRecord =
|
|
6370
|
+
const promptRecord = asRecord2(raw.prompt);
|
|
4807
6371
|
const source = promptRecord || raw;
|
|
4808
6372
|
const id = typeof source.id === "string" ? source.id : typeof raw.id === "string" ? raw.id : typeof raw.promptId === "string" ? raw.promptId : "";
|
|
4809
6373
|
if (!id) return null;
|
|
4810
6374
|
const jobId = typeof source.jobId === "string" && source.jobId.trim() ? source.jobId.trim() : typeof raw.jobId === "string" && raw.jobId.trim() ? raw.jobId.trim() : void 0;
|
|
4811
6375
|
const turnId = typeof source.turnId === "string" && source.turnId.trim() ? source.turnId.trim() : typeof raw.turnId === "string" && raw.turnId.trim() ? raw.turnId.trim() : void 0;
|
|
6376
|
+
const type = normalizePromptType(
|
|
6377
|
+
source === raw ? raw : { ...raw, ...source }
|
|
6378
|
+
);
|
|
6379
|
+
if (!type) return null;
|
|
4812
6380
|
return {
|
|
4813
6381
|
id,
|
|
4814
6382
|
...jobId ? { jobId } : {},
|
|
4815
6383
|
...turnId ? { turnId } : {},
|
|
4816
|
-
type
|
|
6384
|
+
type,
|
|
4817
6385
|
title: typeof source.title === "string" ? source.title : "Input required",
|
|
4818
6386
|
message: typeof source.message === "string" ? source.message : "",
|
|
4819
6387
|
options: Array.isArray(source.options) ? source.options.map(
|
|
4820
|
-
(option) => typeof option === "string" ||
|
|
6388
|
+
(option) => typeof option === "string" || asRecord2(option) ? normalizePromptChoiceOption(
|
|
4821
6389
|
option
|
|
4822
6390
|
) : option
|
|
4823
6391
|
) : void 0,
|
|
4824
6392
|
defaultValue: source.defaultValue,
|
|
4825
6393
|
placeholder: typeof source.placeholder === "string" ? source.placeholder : void 0,
|
|
4826
6394
|
allowEmpty: typeof source.allowEmpty === "boolean" ? source.allowEmpty : void 0,
|
|
4827
|
-
metadata:
|
|
6395
|
+
metadata: asRecord2(source.metadata) || void 0
|
|
4828
6396
|
};
|
|
4829
6397
|
}
|
|
4830
6398
|
function resolvePromptAnswer(prompt, answer) {
|
|
@@ -4852,23 +6420,36 @@ function resolvePromptAnswer(prompt, answer) {
|
|
|
4852
6420
|
}
|
|
4853
6421
|
|
|
4854
6422
|
// src/session.ts
|
|
4855
|
-
var PROMPT_TRANSCRIPT_APPEND_TIMEOUT_MS = 5e3;
|
|
4856
6423
|
function toPascalCase(value) {
|
|
4857
6424
|
return value.split(/[_:\-\s]+/).filter(Boolean).map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join("");
|
|
4858
6425
|
}
|
|
4859
|
-
function
|
|
4860
|
-
|
|
4861
|
-
return
|
|
4862
|
-
|
|
4863
|
-
|
|
4864
|
-
|
|
4865
|
-
|
|
4866
|
-
|
|
4867
|
-
|
|
4868
|
-
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
6426
|
+
function reserveUserMessageId() {
|
|
6427
|
+
const randomUuid = globalThis.crypto?.randomUUID?.();
|
|
6428
|
+
return randomUuid ? `message_${randomUuid}` : `message_${Date.now()}_${Math.random().toString(36).slice(2)}`;
|
|
6429
|
+
}
|
|
6430
|
+
function normalizeUserMessageIdentity(value, field) {
|
|
6431
|
+
if (value === void 0) return void 0;
|
|
6432
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
6433
|
+
throw new Error(`User message ${field} must be a non-empty string.`);
|
|
6434
|
+
}
|
|
6435
|
+
return value.trim();
|
|
6436
|
+
}
|
|
6437
|
+
function recordFromUnknown(value) {
|
|
6438
|
+
return value && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
6439
|
+
}
|
|
6440
|
+
function promptSnapshotFingerprint(prompt) {
|
|
6441
|
+
return JSON.stringify({
|
|
6442
|
+
id: prompt.id,
|
|
6443
|
+
jobId: prompt.jobId || null,
|
|
6444
|
+
turnId: prompt.turnId || null,
|
|
6445
|
+
type: prompt.type,
|
|
6446
|
+
title: prompt.title,
|
|
6447
|
+
message: prompt.message,
|
|
6448
|
+
options: prompt.options || null,
|
|
6449
|
+
defaultValue: prompt.defaultValue,
|
|
6450
|
+
placeholder: prompt.placeholder || null,
|
|
6451
|
+
allowEmpty: prompt.allowEmpty,
|
|
6452
|
+
metadata: prompt.metadata || null
|
|
4872
6453
|
});
|
|
4873
6454
|
}
|
|
4874
6455
|
var Session = class {
|
|
@@ -4876,7 +6457,6 @@ var Session = class {
|
|
|
4876
6457
|
clientId;
|
|
4877
6458
|
initialQuota;
|
|
4878
6459
|
jobsMap = /* @__PURE__ */ new Map();
|
|
4879
|
-
pendingAgentMessagesByJobId = /* @__PURE__ */ new Map();
|
|
4880
6460
|
eventListeners = /* @__PURE__ */ new Map();
|
|
4881
6461
|
toolHandlers = /* @__PURE__ */ new Map();
|
|
4882
6462
|
/** Tracks which tools are instance methods (className set, not static) */
|
|
@@ -4893,10 +6473,23 @@ var Session = class {
|
|
|
4893
6473
|
domainPackagePartCache = /* @__PURE__ */ new Map();
|
|
4894
6474
|
domainPackagePartPromises = /* @__PURE__ */ new Map();
|
|
4895
6475
|
domainPackageFetchQueue = Promise.resolve();
|
|
6476
|
+
feedController;
|
|
6477
|
+
feed;
|
|
4896
6478
|
constructor(client, clientId, options = {}) {
|
|
4897
6479
|
this.client = client;
|
|
4898
6480
|
this.clientId = clientId || `client_${Date.now()}`;
|
|
4899
6481
|
this.initialQuota = options.initialQuota || null;
|
|
6482
|
+
this.feedController = new SessionFeedController(this.client.doc, {
|
|
6483
|
+
listTransport: (feedOptions) => this.client.call(
|
|
6484
|
+
"feed.list",
|
|
6485
|
+
feedOptions
|
|
6486
|
+
)
|
|
6487
|
+
});
|
|
6488
|
+
this.feed = Object.freeze({
|
|
6489
|
+
getSnapshot: () => this.feedController.getSnapshot(),
|
|
6490
|
+
list: (feedOptions = {}) => this.feedController.list(feedOptions),
|
|
6491
|
+
subscribe: (listener, subscribeOptions = {}) => this.feedController.subscribe(listener, subscribeOptions)
|
|
6492
|
+
});
|
|
4900
6493
|
this.setupEventHandlers();
|
|
4901
6494
|
this.setupToolInvokeHandler();
|
|
4902
6495
|
this.currentDomainRevision = this.extractDomainRevisionFromDoc(
|
|
@@ -4915,34 +6508,160 @@ var Session = class {
|
|
|
4915
6508
|
}
|
|
4916
6509
|
return null;
|
|
4917
6510
|
}
|
|
4918
|
-
|
|
6511
|
+
/**
|
|
6512
|
+
* Prompt delivery is deliberately event-driven while a socket is live, but
|
|
6513
|
+
* an existing session can be attached from a fresh tab/client after the
|
|
6514
|
+
* event was originally sent. Rebuild that client's prompt cache from the
|
|
6515
|
+
* canonical document on every sync so an open durable prompt remains
|
|
6516
|
+
* actionable after reconnect without manufacturing a second prompt.
|
|
6517
|
+
*/
|
|
6518
|
+
reconcilePromptCacheFromCanonicalDocument(doc) {
|
|
6519
|
+
const jobsById = recordFromUnknown(recordFromUnknown(doc)?.jobs)?.byId;
|
|
6520
|
+
const jobs = recordFromUnknown(jobsById) || {};
|
|
6521
|
+
const openPromptIds = /* @__PURE__ */ new Set();
|
|
6522
|
+
for (const [jobId, jobValue] of Object.entries(jobs)) {
|
|
6523
|
+
const prompts = recordFromUnknown(recordFromUnknown(jobValue)?.prompts);
|
|
6524
|
+
if (!prompts) continue;
|
|
6525
|
+
for (const [promptId, promptValue] of Object.entries(prompts)) {
|
|
6526
|
+
const persisted = recordFromUnknown(promptValue);
|
|
6527
|
+
if (!persisted || persisted.status !== "open") continue;
|
|
6528
|
+
const prompt = normalizePrompt({
|
|
6529
|
+
promptId,
|
|
6530
|
+
jobId,
|
|
6531
|
+
kind: persisted.kind,
|
|
6532
|
+
type: persisted.type,
|
|
6533
|
+
title: persisted.title,
|
|
6534
|
+
message: persisted.message,
|
|
6535
|
+
options: persisted.options,
|
|
6536
|
+
defaultValue: persisted.defaultValue,
|
|
6537
|
+
placeholder: persisted.placeholder,
|
|
6538
|
+
allowEmpty: persisted.allowEmpty,
|
|
6539
|
+
metadata: persisted.metadata
|
|
6540
|
+
});
|
|
6541
|
+
if (!prompt) continue;
|
|
6542
|
+
openPromptIds.add(prompt.id);
|
|
6543
|
+
if (this.hiddenPromptIds.has(prompt.id)) continue;
|
|
6544
|
+
const previous = this.promptCache.get(prompt.id);
|
|
6545
|
+
this.promptCache.set(prompt.id, prompt);
|
|
6546
|
+
if (!previous || promptSnapshotFingerprint(previous) !== promptSnapshotFingerprint(prompt)) {
|
|
6547
|
+
this.emit("prompt", prompt);
|
|
6548
|
+
}
|
|
6549
|
+
}
|
|
6550
|
+
}
|
|
6551
|
+
for (const promptId of this.promptCache.keys()) {
|
|
6552
|
+
if (!openPromptIds.has(promptId)) {
|
|
6553
|
+
this.promptCache.delete(promptId);
|
|
6554
|
+
}
|
|
6555
|
+
}
|
|
6556
|
+
}
|
|
6557
|
+
buildDirectedInvocationEffectContext(params, feedbackContext) {
|
|
4919
6558
|
return {
|
|
4920
6559
|
effectClientId: this.clientId,
|
|
4921
|
-
sandboxId: "",
|
|
4922
|
-
environmentId: "",
|
|
4923
|
-
|
|
6560
|
+
sandboxId: params.sandboxId || "",
|
|
6561
|
+
environmentId: params.environmentId || "",
|
|
6562
|
+
invocationId: params.callId,
|
|
6563
|
+
jobId: params.jobId,
|
|
6564
|
+
sessionId: params.sessionId || this.client.currentSessionId,
|
|
4924
6565
|
user: {
|
|
4925
6566
|
granularId: "",
|
|
4926
6567
|
userId: "",
|
|
4927
6568
|
subjectId: ""
|
|
4928
|
-
}
|
|
6569
|
+
},
|
|
6570
|
+
...feedbackContext ? {
|
|
6571
|
+
feedback: feedbackContext.feedback,
|
|
6572
|
+
transientFeedback: feedbackContext.transientFeedback
|
|
6573
|
+
} : {}
|
|
4929
6574
|
};
|
|
4930
6575
|
}
|
|
4931
|
-
|
|
4932
|
-
if (
|
|
4933
|
-
|
|
4934
|
-
|
|
4935
|
-
|
|
4936
|
-
|
|
4937
|
-
|
|
4938
|
-
|
|
4939
|
-
|
|
4940
|
-
|
|
4941
|
-
|
|
4942
|
-
|
|
4943
|
-
|
|
4944
|
-
|
|
4945
|
-
|
|
6576
|
+
createDirectedInvocationFeedbackContext(params) {
|
|
6577
|
+
if (!params.feedbackCapability) return null;
|
|
6578
|
+
const publications = [];
|
|
6579
|
+
let nextOperationOrdinal = 0;
|
|
6580
|
+
const nextOperationId = (kind) => `${kind}:${nextOperationOrdinal++}`;
|
|
6581
|
+
const track = (publication) => {
|
|
6582
|
+
const tracked = Promise.resolve(publication);
|
|
6583
|
+
publications.push(tracked);
|
|
6584
|
+
void tracked.catch(() => void 0);
|
|
6585
|
+
return tracked;
|
|
6586
|
+
};
|
|
6587
|
+
const methodMap = {
|
|
6588
|
+
"feed.feedback": "tool.feedback",
|
|
6589
|
+
"feed.transient.create": "tool.transient.create",
|
|
6590
|
+
"feed.transient.update": "tool.transient.update",
|
|
6591
|
+
"feed.transient.settle": "tool.transient.settle"
|
|
6592
|
+
};
|
|
6593
|
+
const publisher = createFeedPublisher((method, publishParams) => {
|
|
6594
|
+
const directedFeedbackMethod = methodMap[method];
|
|
6595
|
+
if (!directedFeedbackMethod) {
|
|
6596
|
+
throw new Error(`Unsupported directed feedback method: ${method}`);
|
|
6597
|
+
}
|
|
6598
|
+
return this.client.call(directedFeedbackMethod, {
|
|
6599
|
+
...publishParams,
|
|
6600
|
+
callId: params.callId,
|
|
6601
|
+
feedbackCapability: params.feedbackCapability
|
|
6602
|
+
});
|
|
6603
|
+
});
|
|
6604
|
+
const wrapTransientHandle = (initial) => {
|
|
6605
|
+
let current = initial;
|
|
6606
|
+
const wrapped = {
|
|
6607
|
+
get id() {
|
|
6608
|
+
return current.id;
|
|
6609
|
+
},
|
|
6610
|
+
get ordinal() {
|
|
6611
|
+
return current.ordinal;
|
|
6612
|
+
},
|
|
6613
|
+
get revision() {
|
|
6614
|
+
return current.revision;
|
|
6615
|
+
},
|
|
6616
|
+
async update(text, options = {}) {
|
|
6617
|
+
current = await track(
|
|
6618
|
+
current.update(text, {
|
|
6619
|
+
...options,
|
|
6620
|
+
operationId: options.operationId || nextOperationId("transient-update")
|
|
6621
|
+
})
|
|
6622
|
+
);
|
|
6623
|
+
return wrapped;
|
|
6624
|
+
},
|
|
6625
|
+
settle(text, options = {}) {
|
|
6626
|
+
return track(
|
|
6627
|
+
current.settle(text, {
|
|
6628
|
+
...options,
|
|
6629
|
+
operationId: options.operationId || nextOperationId("transient-settle")
|
|
6630
|
+
})
|
|
6631
|
+
);
|
|
6632
|
+
}
|
|
6633
|
+
};
|
|
6634
|
+
return wrapped;
|
|
6635
|
+
};
|
|
6636
|
+
return {
|
|
6637
|
+
feedback: (text, options = {}) => track(
|
|
6638
|
+
publisher.feedback(text, {
|
|
6639
|
+
...options,
|
|
6640
|
+
operationId: options.operationId || nextOperationId("feedback")
|
|
6641
|
+
})
|
|
6642
|
+
),
|
|
6643
|
+
transientFeedback: (text, options = {}) => track(
|
|
6644
|
+
publisher.transientFeedback(text, {
|
|
6645
|
+
...options,
|
|
6646
|
+
operationId: options.operationId || nextOperationId("transient-create")
|
|
6647
|
+
}).then(wrapTransientHandle)
|
|
6648
|
+
),
|
|
6649
|
+
async flush() {
|
|
6650
|
+
let cursor = 0;
|
|
6651
|
+
let firstError;
|
|
6652
|
+
while (cursor < publications.length) {
|
|
6653
|
+
const batch = publications.slice(cursor);
|
|
6654
|
+
cursor = publications.length;
|
|
6655
|
+
const results = await Promise.allSettled(batch);
|
|
6656
|
+
for (const result of results) {
|
|
6657
|
+
if (result.status === "rejected" && firstError === void 0) {
|
|
6658
|
+
firstError = result.reason;
|
|
6659
|
+
}
|
|
6660
|
+
}
|
|
6661
|
+
}
|
|
6662
|
+
if (firstError !== void 0) throw firstError;
|
|
6663
|
+
}
|
|
6664
|
+
};
|
|
4946
6665
|
}
|
|
4947
6666
|
// --- Public API ---
|
|
4948
6667
|
get document() {
|
|
@@ -5072,17 +6791,8 @@ var Session = class {
|
|
|
5072
6791
|
code,
|
|
5073
6792
|
domainRevision: revision,
|
|
5074
6793
|
createdAt: Date.now()
|
|
5075
|
-
});
|
|
5076
|
-
this.jobsMap.set(result.jobId, job);
|
|
5077
|
-
const pendingAgentMessages = this.pendingAgentMessagesByJobId.get(
|
|
5078
|
-
result.jobId
|
|
5079
|
-
);
|
|
5080
|
-
if (pendingAgentMessages && pendingAgentMessages.length > 0) {
|
|
5081
|
-
this.pendingAgentMessagesByJobId.delete(result.jobId);
|
|
5082
|
-
for (const message of pendingAgentMessages) {
|
|
5083
|
-
job.replayAgentMessage(message);
|
|
5084
|
-
}
|
|
5085
|
-
}
|
|
6794
|
+
});
|
|
6795
|
+
this.jobsMap.set(result.jobId, job);
|
|
5086
6796
|
return job;
|
|
5087
6797
|
}
|
|
5088
6798
|
/**
|
|
@@ -5103,50 +6813,44 @@ var Session = class {
|
|
|
5103
6813
|
async answerPrompt(promptId, answer) {
|
|
5104
6814
|
const prompt = this.promptCache.get(promptId);
|
|
5105
6815
|
const resolvedAnswer = resolvePromptAnswer(prompt, answer);
|
|
6816
|
+
const response = await this.client.call("prompt.answer", {
|
|
6817
|
+
promptId,
|
|
6818
|
+
answer: resolvedAnswer,
|
|
6819
|
+
value: resolvedAnswer
|
|
6820
|
+
});
|
|
6821
|
+
if (response && typeof response === "object" && "ok" in response && response.ok === false) {
|
|
6822
|
+
const rejected = response;
|
|
6823
|
+
const errorMessage = typeof rejected.error === "string" ? rejected.error : "Prompt answer was rejected.";
|
|
6824
|
+
throw new Error(errorMessage);
|
|
6825
|
+
}
|
|
5106
6826
|
this.promptCache.delete(promptId);
|
|
5107
6827
|
this.hiddenPromptIds.add(promptId);
|
|
5108
6828
|
this.emit("prompt:answered", {
|
|
5109
6829
|
...prompt || { id: promptId },
|
|
5110
6830
|
id: promptId,
|
|
6831
|
+
answer: resolvedAnswer,
|
|
5111
6832
|
status: "answered"
|
|
5112
6833
|
});
|
|
5113
|
-
try {
|
|
5114
|
-
const response = await this.client.call("prompt.answer", {
|
|
5115
|
-
promptId,
|
|
5116
|
-
answer: resolvedAnswer,
|
|
5117
|
-
value: resolvedAnswer
|
|
5118
|
-
});
|
|
5119
|
-
if (response && typeof response === "object" && "ok" in response && response.ok === false) {
|
|
5120
|
-
const rejected = response;
|
|
5121
|
-
const errorMessage = typeof rejected.error === "string" ? rejected.error : "Prompt answer was rejected.";
|
|
5122
|
-
throw new Error(errorMessage);
|
|
5123
|
-
}
|
|
5124
|
-
} catch (error) {
|
|
5125
|
-
this.hiddenPromptIds.delete(promptId);
|
|
5126
|
-
if (prompt) {
|
|
5127
|
-
this.promptCache.set(promptId, prompt);
|
|
5128
|
-
}
|
|
5129
|
-
throw error;
|
|
5130
|
-
}
|
|
5131
|
-
try {
|
|
5132
|
-
const content = this.stringifyConversationValue(resolvedAnswer);
|
|
5133
|
-
if (content.trim()) {
|
|
5134
|
-
await withPromptTranscriptTimeout(
|
|
5135
|
-
this.appendConversationMessage({
|
|
5136
|
-
role: "user",
|
|
5137
|
-
content,
|
|
5138
|
-
promptId
|
|
5139
|
-
})
|
|
5140
|
-
);
|
|
5141
|
-
}
|
|
5142
|
-
} catch {
|
|
5143
|
-
}
|
|
5144
6834
|
}
|
|
5145
|
-
async
|
|
5146
|
-
|
|
5147
|
-
|
|
5148
|
-
input
|
|
6835
|
+
async appendUserMessage(input) {
|
|
6836
|
+
const requestedId = normalizeUserMessageIdentity(input.id, "id");
|
|
6837
|
+
const requestedOperationId = normalizeUserMessageIdentity(
|
|
6838
|
+
input.operationId,
|
|
6839
|
+
"operationId"
|
|
5149
6840
|
);
|
|
6841
|
+
const id = requestedId || (requestedOperationId ? void 0 : reserveUserMessageId());
|
|
6842
|
+
const operationId2 = requestedOperationId || `conversation.append:${id}`;
|
|
6843
|
+
const {
|
|
6844
|
+
id: _ignoredInputId,
|
|
6845
|
+
operationId: _ignoredInputOperationId,
|
|
6846
|
+
...message
|
|
6847
|
+
} = input;
|
|
6848
|
+
return this.client.call("conversation.append", {
|
|
6849
|
+
...message,
|
|
6850
|
+
role: "user",
|
|
6851
|
+
...id ? { id } : {},
|
|
6852
|
+
operationId: operationId2
|
|
6853
|
+
});
|
|
5150
6854
|
}
|
|
5151
6855
|
/**
|
|
5152
6856
|
* Get the current list of available effects.
|
|
@@ -5529,6 +7233,7 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5529
7233
|
* Close the session and disconnect from the sandbox
|
|
5530
7234
|
*/
|
|
5531
7235
|
async disconnect() {
|
|
7236
|
+
this.disposeSessionFeed();
|
|
5532
7237
|
try {
|
|
5533
7238
|
await this.client.call("client.goodbye", {
|
|
5534
7239
|
clientId: this.clientId,
|
|
@@ -5538,6 +7243,10 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5538
7243
|
}
|
|
5539
7244
|
this.client.disconnect();
|
|
5540
7245
|
}
|
|
7246
|
+
/** Stop feed repair work without detaching a reconnectable transport. */
|
|
7247
|
+
disposeSessionFeed() {
|
|
7248
|
+
this.feedController.dispose();
|
|
7249
|
+
}
|
|
5541
7250
|
// --- Event Handling ---
|
|
5542
7251
|
/**
|
|
5543
7252
|
* Subscribe to session events
|
|
@@ -5562,9 +7271,14 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5562
7271
|
}
|
|
5563
7272
|
}
|
|
5564
7273
|
// --- Internal ---
|
|
7274
|
+
setFeedListTransport(transport) {
|
|
7275
|
+
this.feedController.setListTransport(transport);
|
|
7276
|
+
}
|
|
5565
7277
|
setupToolInvokeHandler() {
|
|
5566
7278
|
this.client.registerRpcHandler("tool.invoke", async (params) => {
|
|
5567
|
-
const
|
|
7279
|
+
const invocation = params;
|
|
7280
|
+
const { callId, toolName, input, feedbackCapability } = invocation;
|
|
7281
|
+
const capabilityResultParams = feedbackCapability ? { feedbackCapability } : {};
|
|
5568
7282
|
this.emit("effect:invoke", {
|
|
5569
7283
|
callId,
|
|
5570
7284
|
effectKey: toolName,
|
|
@@ -5576,6 +7290,7 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5576
7290
|
if (!handler) {
|
|
5577
7291
|
await this.client.call("tool.result", {
|
|
5578
7292
|
callId,
|
|
7293
|
+
...capabilityResultParams,
|
|
5579
7294
|
error: {
|
|
5580
7295
|
code: "TOOL_NOT_FOUND",
|
|
5581
7296
|
message: `Tool handler not found: ${toolName}`
|
|
@@ -5585,21 +7300,45 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5585
7300
|
}
|
|
5586
7301
|
try {
|
|
5587
7302
|
let result;
|
|
5588
|
-
const
|
|
5589
|
-
|
|
5590
|
-
|
|
5591
|
-
|
|
5592
|
-
|
|
5593
|
-
|
|
5594
|
-
|
|
5595
|
-
|
|
5596
|
-
|
|
5597
|
-
|
|
7303
|
+
const feedbackContext = this.createDirectedInvocationFeedbackContext(invocation);
|
|
7304
|
+
const invocationContext = this.buildDirectedInvocationEffectContext(
|
|
7305
|
+
invocation,
|
|
7306
|
+
feedbackContext
|
|
7307
|
+
);
|
|
7308
|
+
let handlerError;
|
|
7309
|
+
let handlerFailed = false;
|
|
7310
|
+
try {
|
|
7311
|
+
if (this.instanceTools.has(toolName) && input && typeof input === "object" && "_objectId" in input) {
|
|
7312
|
+
const { _objectId, ...restParams } = input;
|
|
7313
|
+
result = await handler(
|
|
7314
|
+
_objectId,
|
|
7315
|
+
restParams,
|
|
7316
|
+
invocationContext
|
|
7317
|
+
);
|
|
7318
|
+
} else {
|
|
7319
|
+
result = await handler(input, invocationContext);
|
|
7320
|
+
}
|
|
7321
|
+
} catch (error) {
|
|
7322
|
+
handlerFailed = true;
|
|
7323
|
+
handlerError = error;
|
|
5598
7324
|
}
|
|
7325
|
+
let feedbackError;
|
|
7326
|
+
let feedbackFailed = false;
|
|
7327
|
+
if (feedbackContext) {
|
|
7328
|
+
try {
|
|
7329
|
+
await feedbackContext.flush();
|
|
7330
|
+
} catch (error) {
|
|
7331
|
+
feedbackFailed = true;
|
|
7332
|
+
feedbackError = error;
|
|
7333
|
+
}
|
|
7334
|
+
}
|
|
7335
|
+
if (handlerFailed) throw handlerError;
|
|
7336
|
+
if (feedbackFailed) throw feedbackError;
|
|
5599
7337
|
this.emit("effect:result", { callId, effectKey: toolName, result });
|
|
5600
7338
|
this.emit("tool:result", { callId, result });
|
|
5601
7339
|
await this.client.call("tool.result", {
|
|
5602
7340
|
callId,
|
|
7341
|
+
...capabilityResultParams,
|
|
5603
7342
|
result
|
|
5604
7343
|
});
|
|
5605
7344
|
} catch (error) {
|
|
@@ -5612,6 +7351,7 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5612
7351
|
this.emit("tool:result", { callId, error: errorMessage });
|
|
5613
7352
|
await this.client.call("tool.result", {
|
|
5614
7353
|
callId,
|
|
7354
|
+
...capabilityResultParams,
|
|
5615
7355
|
error: { code: "TOOL_EXECUTION_FAILED", message: errorMessage }
|
|
5616
7356
|
});
|
|
5617
7357
|
}
|
|
@@ -5624,9 +7364,14 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5624
7364
|
);
|
|
5625
7365
|
this.client.on("sync", (doc) => {
|
|
5626
7366
|
this.currentDomainRevision = this.extractDomainRevisionFromDoc(doc);
|
|
7367
|
+
this.feedController.updateDocument(doc);
|
|
7368
|
+
this.reconcilePromptCacheFromCanonicalDocument(doc);
|
|
5627
7369
|
this.emit("sync", doc);
|
|
5628
7370
|
this.checkForToolChanges();
|
|
5629
7371
|
});
|
|
7372
|
+
this.client.onCanonicalDocumentQuarantine?.((quarantine) => {
|
|
7373
|
+
this.feedController.quarantineCanonicalReplacement(quarantine);
|
|
7374
|
+
});
|
|
5630
7375
|
const emitPrompt = (payload) => {
|
|
5631
7376
|
const prompt = normalizePrompt(payload);
|
|
5632
7377
|
if (!prompt) return;
|
|
@@ -5659,23 +7404,6 @@ import { ${tools[0]?.name || "example"} } from "@granular/actions/backend";
|
|
|
5659
7404
|
this.client.on("harness.text_response.delta", (data) => {
|
|
5660
7405
|
this.emit("harness:text_response_delta", data);
|
|
5661
7406
|
});
|
|
5662
|
-
this.client.on("job.agent_message", (data) => {
|
|
5663
|
-
const normalized = normalizeJobAgentMessageEnvelope(data);
|
|
5664
|
-
if (!normalized) return;
|
|
5665
|
-
this.emit("job:agent_message", normalized);
|
|
5666
|
-
if (this.jobsMap.has(normalized.jobId)) return;
|
|
5667
|
-
const pending = this.pendingAgentMessagesByJobId.get(normalized.jobId) || [];
|
|
5668
|
-
if (normalized.message.messageId && pending.some(
|
|
5669
|
-
(message) => message.messageId === normalized.message.messageId
|
|
5670
|
-
)) {
|
|
5671
|
-
return;
|
|
5672
|
-
}
|
|
5673
|
-
pending.push(normalized.message);
|
|
5674
|
-
this.pendingAgentMessagesByJobId.set(
|
|
5675
|
-
normalized.jobId,
|
|
5676
|
-
pending.slice(-25)
|
|
5677
|
-
);
|
|
5678
|
-
});
|
|
5679
7407
|
this.client.on("exec.completed", (data) => {
|
|
5680
7408
|
this.emit("exec:completed", data);
|
|
5681
7409
|
});
|
|
@@ -5800,24 +7528,6 @@ function sanitizeFeedbackValue(value, depth = 0, seen = /* @__PURE__ */ new Weak
|
|
|
5800
7528
|
}
|
|
5801
7529
|
return truncateFeedbackString(String(value));
|
|
5802
7530
|
}
|
|
5803
|
-
function normalizeJobAgentMessageEnvelope(data) {
|
|
5804
|
-
const d = data;
|
|
5805
|
-
if (typeof d?.jobId !== "string" || !d.jobId) {
|
|
5806
|
-
return null;
|
|
5807
|
-
}
|
|
5808
|
-
return {
|
|
5809
|
-
jobId: d.jobId,
|
|
5810
|
-
...typeof d.turnId === "string" && d.turnId.trim() ? { turnId: d.turnId.trim() } : {},
|
|
5811
|
-
message: {
|
|
5812
|
-
messageId: d.messageId,
|
|
5813
|
-
kind: d.kind === "artifacts" ? "artifacts" : "text",
|
|
5814
|
-
reply: typeof d.reply === "string" ? d.reply : "",
|
|
5815
|
-
show: d.show,
|
|
5816
|
-
actions: Array.isArray(d.actions) ? d.actions : void 0,
|
|
5817
|
-
timestamp: d.timestamp || Date.now()
|
|
5818
|
-
}
|
|
5819
|
-
};
|
|
5820
|
-
}
|
|
5821
7531
|
var JobImplementation = class {
|
|
5822
7532
|
id;
|
|
5823
7533
|
client;
|
|
@@ -5826,8 +7536,6 @@ var JobImplementation = class {
|
|
|
5826
7536
|
_resolveResult;
|
|
5827
7537
|
_rejectResult;
|
|
5828
7538
|
eventListeners = /* @__PURE__ */ new Map();
|
|
5829
|
-
bufferedAgentMessages = [];
|
|
5830
|
-
bufferedAgentMessageIds = /* @__PURE__ */ new Set();
|
|
5831
7539
|
resultSettled = false;
|
|
5832
7540
|
metadata;
|
|
5833
7541
|
constructor(id, client, initialState) {
|
|
@@ -5986,439 +7694,119 @@ var JobImplementation = class {
|
|
|
5986
7694
|
timestamp: d.timestamp
|
|
5987
7695
|
});
|
|
5988
7696
|
}
|
|
5989
|
-
});
|
|
5990
|
-
this.client.on("job.agent_message", (data) => {
|
|
5991
|
-
const normalized = normalizeJobAgentMessageEnvelope(data);
|
|
5992
|
-
if (normalized?.jobId === id) {
|
|
5993
|
-
this.captureAgentMessage(normalized.message);
|
|
5994
|
-
}
|
|
5995
|
-
});
|
|
5996
|
-
}
|
|
5997
|
-
get result() {
|
|
5998
|
-
return this._resultPromise;
|
|
5999
|
-
}
|
|
6000
|
-
async leaveFeedback(input) {
|
|
6001
|
-
const sentiment = input.sentiment;
|
|
6002
|
-
if (sentiment !== "good" && sentiment !== "bad") {
|
|
6003
|
-
throw new Error('Job feedback sentiment must be "good" or "bad".');
|
|
6004
|
-
}
|
|
6005
|
-
const comment = typeof input.comment === "string" ? input.comment.trim() : "";
|
|
6006
|
-
const response = await this.client.call("job.feedback", {
|
|
6007
|
-
jobId: this.id,
|
|
6008
|
-
sentiment,
|
|
6009
|
-
comment: comment || void 0,
|
|
6010
|
-
metadata: this.buildFeedbackMetadata()
|
|
6011
|
-
});
|
|
6012
|
-
this.emit("feedback", response);
|
|
6013
|
-
return response;
|
|
6014
|
-
}
|
|
6015
|
-
on(event, handler) {
|
|
6016
|
-
if (!this.eventListeners.has(event)) {
|
|
6017
|
-
this.eventListeners.set(event, []);
|
|
6018
|
-
}
|
|
6019
|
-
this.eventListeners.get(event).push(handler);
|
|
6020
|
-
if (event === "agentMessage" && this.bufferedAgentMessages.length > 0) {
|
|
6021
|
-
for (const message of this.bufferedAgentMessages) {
|
|
6022
|
-
handler(message);
|
|
6023
|
-
}
|
|
6024
|
-
}
|
|
6025
|
-
return () => {
|
|
6026
|
-
const handlers = this.eventListeners.get(event);
|
|
6027
|
-
if (!handlers) {
|
|
6028
|
-
return;
|
|
6029
|
-
}
|
|
6030
|
-
this.eventListeners.set(
|
|
6031
|
-
event,
|
|
6032
|
-
handlers.filter((current) => current !== handler)
|
|
6033
|
-
);
|
|
6034
|
-
};
|
|
6035
|
-
}
|
|
6036
|
-
replayAgentMessage(message) {
|
|
6037
|
-
this.captureAgentMessage(message);
|
|
6038
|
-
}
|
|
6039
|
-
buildFeedbackMetadata() {
|
|
6040
|
-
const startedAt = this.metadata.startedAt;
|
|
6041
|
-
const completedAt = this.metadata.completedAt;
|
|
6042
|
-
return {
|
|
6043
|
-
...this.metadata,
|
|
6044
|
-
durationMs: typeof startedAt === "number" && typeof completedAt === "number" ? completedAt - startedAt : this.metadata.durationMs ?? null,
|
|
6045
|
-
stdout: [...this.metadata.stdout],
|
|
6046
|
-
stderr: [...this.metadata.stderr],
|
|
6047
|
-
toolCalls: this.metadata.toolCalls.slice(0, MAX_FEEDBACK_TOOL_CALLS).map((call) => ({ ...call }))
|
|
6048
|
-
};
|
|
6049
|
-
}
|
|
6050
|
-
markStarted(timestamp = Date.now()) {
|
|
6051
|
-
if (!this.metadata.startedAt) {
|
|
6052
|
-
this.metadata.startedAt = timestamp;
|
|
6053
|
-
}
|
|
6054
|
-
if (this.status === "queued") {
|
|
6055
|
-
this.status = "running";
|
|
6056
|
-
}
|
|
6057
|
-
if (this.metadata.status === "queued") {
|
|
6058
|
-
this.metadata.status = "running";
|
|
6059
|
-
}
|
|
6060
|
-
}
|
|
6061
|
-
finalize(status, result, error, options = {}) {
|
|
6062
|
-
if (!this.metadata.startedAt) {
|
|
6063
|
-
this.metadata.startedAt = Date.now();
|
|
6064
|
-
}
|
|
6065
|
-
this.status = status;
|
|
6066
|
-
this.metadata.status = status;
|
|
6067
|
-
this.metadata.completedAt = this.metadata.completedAt || Date.now();
|
|
6068
|
-
this.metadata.durationMs = this.metadata.completedAt - this.metadata.startedAt;
|
|
6069
|
-
if (!this.resultSettled && (options.hasResult || result !== void 0)) {
|
|
6070
|
-
this.metadata.result = sanitizeFeedbackValue(result);
|
|
6071
|
-
this.resultSettled = true;
|
|
6072
|
-
this._resolveResult(result);
|
|
6073
|
-
}
|
|
6074
|
-
if (!this.resultSettled && (error !== void 0 || status === "failed" || status === "timeout" || status === "canceled")) {
|
|
6075
|
-
const fallbackError = new Error(`Job ${this.id} ${status}.`);
|
|
6076
|
-
const cause = error ?? fallbackError;
|
|
6077
|
-
const message = cause instanceof Error ? cause.message : String(cause);
|
|
6078
|
-
this.metadata.error = truncateFeedbackString(message);
|
|
6079
|
-
this.resultSettled = true;
|
|
6080
|
-
this._rejectResult(cause);
|
|
6081
|
-
}
|
|
6082
|
-
}
|
|
6083
|
-
upsertToolCall(next) {
|
|
6084
|
-
const callId = next.callId || `tool-call-${Date.now()}`;
|
|
6085
|
-
const existingIndex = this.metadata.toolCalls.findIndex(
|
|
6086
|
-
(entry) => entry.callId === callId
|
|
6087
|
-
);
|
|
6088
|
-
const existing = existingIndex >= 0 ? this.metadata.toolCalls[existingIndex] : void 0;
|
|
6089
|
-
const merged = {
|
|
6090
|
-
...existing,
|
|
6091
|
-
...next,
|
|
6092
|
-
callId,
|
|
6093
|
-
toolName: next.toolName || existing?.toolName
|
|
6094
|
-
};
|
|
6095
|
-
if (merged.durationMs === void 0 && typeof merged.startedAt === "number" && typeof merged.completedAt === "number") {
|
|
6096
|
-
merged.durationMs = merged.completedAt - merged.startedAt;
|
|
6097
|
-
}
|
|
6098
|
-
if (existingIndex >= 0) {
|
|
6099
|
-
const updated = [...this.metadata.toolCalls];
|
|
6100
|
-
updated[existingIndex] = merged;
|
|
6101
|
-
return updated.slice(-MAX_FEEDBACK_TOOL_CALLS);
|
|
6102
|
-
}
|
|
6103
|
-
return [...this.metadata.toolCalls, merged].slice(-MAX_FEEDBACK_TOOL_CALLS);
|
|
6104
|
-
}
|
|
6105
|
-
emit(event, data) {
|
|
6106
|
-
const handlers = this.eventListeners.get(event);
|
|
6107
|
-
if (handlers) {
|
|
6108
|
-
handlers.forEach((h) => h(data));
|
|
6109
|
-
}
|
|
6110
|
-
}
|
|
6111
|
-
captureAgentMessage(message) {
|
|
6112
|
-
if (message.messageId && this.bufferedAgentMessageIds.has(message.messageId)) {
|
|
6113
|
-
return;
|
|
6114
|
-
}
|
|
6115
|
-
if (message.messageId) {
|
|
6116
|
-
this.bufferedAgentMessageIds.add(message.messageId);
|
|
6117
|
-
}
|
|
6118
|
-
this.bufferedAgentMessages = [...this.bufferedAgentMessages, message].slice(
|
|
6119
|
-
-25
|
|
6120
|
-
);
|
|
6121
|
-
this.emit("agentMessage", message);
|
|
6122
|
-
}
|
|
6123
|
-
};
|
|
6124
|
-
|
|
6125
|
-
// src/job-presentation.ts
|
|
6126
|
-
var RESPONSE_KEYS = [
|
|
6127
|
-
"reply",
|
|
6128
|
-
"response",
|
|
6129
|
-
"text",
|
|
6130
|
-
"message",
|
|
6131
|
-
"summary",
|
|
6132
|
-
"answer"
|
|
6133
|
-
];
|
|
6134
|
-
var ENTRY_KEY_CANDIDATES = ["entryPath", "path"];
|
|
6135
|
-
var ENTRY_ARRAY_KEY_CANDIDATES = ["entryPaths", "paths"];
|
|
6136
|
-
var LIST_KEY_CANDIDATES = ["listName"];
|
|
6137
|
-
var LIST_ARRAY_KEY_CANDIDATES = ["listNames"];
|
|
6138
|
-
var VARIABLE_KEY_CANDIDATES = ["variableName"];
|
|
6139
|
-
var VARIABLE_ARRAY_KEY_CANDIDATES = ["variableNames"];
|
|
6140
|
-
var UI_CONTAINER_KEYS = ["show", "display", "present", "ui"];
|
|
6141
|
-
function asRecord2(value) {
|
|
6142
|
-
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
6143
|
-
return value;
|
|
6144
|
-
}
|
|
6145
|
-
function normalizeText(value) {
|
|
6146
|
-
if (typeof value !== "string") return null;
|
|
6147
|
-
const trimmed = value.trim();
|
|
6148
|
-
if (!trimmed) return null;
|
|
6149
|
-
if (trimmed.startsWith("{") && trimmed.endsWith("}") || trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
|
6150
|
-
return null;
|
|
6151
|
-
}
|
|
6152
|
-
return trimmed;
|
|
6153
|
-
}
|
|
6154
|
-
function humanTextFromStdout(stdout) {
|
|
6155
|
-
for (const line of [...stdout].reverse()) {
|
|
6156
|
-
const normalized = normalizeText(line);
|
|
6157
|
-
if (!normalized) continue;
|
|
6158
|
-
if (/^[A-Z_]+:/.test(normalized)) continue;
|
|
6159
|
-
return normalized;
|
|
6160
|
-
}
|
|
6161
|
-
return null;
|
|
6162
|
-
}
|
|
6163
|
-
function responseTextFromAgentMessages(agentMessages) {
|
|
6164
|
-
for (const message of [...agentMessages].reverse()) {
|
|
6165
|
-
const record = asRecord2(message);
|
|
6166
|
-
if (!record) continue;
|
|
6167
|
-
for (const key of RESPONSE_KEYS) {
|
|
6168
|
-
const normalized = normalizeText(record[key]);
|
|
6169
|
-
if (normalized) return normalized;
|
|
6170
|
-
}
|
|
6171
|
-
}
|
|
6172
|
-
return null;
|
|
6173
|
-
}
|
|
6174
|
-
function pushString(target, value) {
|
|
6175
|
-
if (typeof value === "string" && value.trim()) {
|
|
6176
|
-
target.add(value.trim());
|
|
6177
|
-
}
|
|
6178
|
-
}
|
|
6179
|
-
function pushStringArray(target, value) {
|
|
6180
|
-
if (!Array.isArray(value)) return;
|
|
6181
|
-
for (const item of value) {
|
|
6182
|
-
pushString(target, item);
|
|
6183
|
-
}
|
|
6184
|
-
}
|
|
6185
|
-
function collectReferencesFromRecord(record, refs) {
|
|
6186
|
-
for (const key of ENTRY_KEY_CANDIDATES)
|
|
6187
|
-
pushString(refs.entryPaths, record[key]);
|
|
6188
|
-
for (const key of ENTRY_ARRAY_KEY_CANDIDATES)
|
|
6189
|
-
pushStringArray(refs.entryPaths, record[key]);
|
|
6190
|
-
for (const key of LIST_KEY_CANDIDATES)
|
|
6191
|
-
pushString(refs.listNames, record[key]);
|
|
6192
|
-
for (const key of LIST_ARRAY_KEY_CANDIDATES)
|
|
6193
|
-
pushStringArray(refs.listNames, record[key]);
|
|
6194
|
-
for (const key of VARIABLE_KEY_CANDIDATES)
|
|
6195
|
-
pushString(refs.variableNames, record[key]);
|
|
6196
|
-
for (const key of VARIABLE_ARRAY_KEY_CANDIDATES)
|
|
6197
|
-
pushStringArray(refs.variableNames, record[key]);
|
|
6198
|
-
}
|
|
6199
|
-
function stringValue(record, keys) {
|
|
6200
|
-
for (const key of keys) {
|
|
6201
|
-
const value = record[key];
|
|
6202
|
-
if (typeof value === "string" && value.trim()) {
|
|
6203
|
-
return value.trim();
|
|
6204
|
-
}
|
|
6205
|
-
}
|
|
6206
|
-
return null;
|
|
6207
|
-
}
|
|
6208
|
-
function findEntryPathForRecord(record, heap) {
|
|
6209
|
-
const directPath = stringValue(record, ["entryPath", "path"]);
|
|
6210
|
-
if (directPath && heap.entriesByPath?.[directPath]) {
|
|
6211
|
-
return directPath;
|
|
6212
|
-
}
|
|
6213
|
-
const id = stringValue(record, ["id", "_id", "recordId", "objectId"]);
|
|
6214
|
-
if (!id) {
|
|
6215
|
-
return null;
|
|
6216
|
-
}
|
|
6217
|
-
const className = stringValue(record, [
|
|
6218
|
-
"className",
|
|
6219
|
-
"_className",
|
|
6220
|
-
"__className",
|
|
6221
|
-
"prototype",
|
|
6222
|
-
"type"
|
|
6223
|
-
]);
|
|
6224
|
-
const entries = Object.values(heap.entriesByPath || {});
|
|
6225
|
-
const exact = entries.find(
|
|
6226
|
-
(entry) => entry.id === id && (!className || entry.className === className || entry.prototypes?.includes(className))
|
|
6227
|
-
);
|
|
6228
|
-
if (exact?.path) {
|
|
6229
|
-
return exact.path;
|
|
6230
|
-
}
|
|
6231
|
-
const idOnlyMatches = entries.filter((entry) => entry.id === id);
|
|
6232
|
-
return idOnlyMatches.length === 1 ? idOnlyMatches[0].path : null;
|
|
6233
|
-
}
|
|
6234
|
-
function scanForHeapReferences(value, heap, refs, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
6235
|
-
if (value === null || value === void 0 || depth > 4 || seen.has(value))
|
|
6236
|
-
return;
|
|
6237
|
-
if (typeof value === "string") {
|
|
6238
|
-
const trimmed = value.trim();
|
|
6239
|
-
if (heap.entriesByPath?.[trimmed]) refs.entryPaths.add(trimmed);
|
|
6240
|
-
if (heap.listsByName?.[trimmed]) refs.listNames.add(trimmed);
|
|
6241
|
-
if (heap.variablesByName?.[trimmed]) refs.variableNames.add(trimmed);
|
|
6242
|
-
return;
|
|
7697
|
+
});
|
|
6243
7698
|
}
|
|
6244
|
-
|
|
6245
|
-
|
|
6246
|
-
|
|
6247
|
-
|
|
7699
|
+
get result() {
|
|
7700
|
+
return this._resultPromise;
|
|
7701
|
+
}
|
|
7702
|
+
async leaveFeedback(input) {
|
|
7703
|
+
const sentiment = input.sentiment;
|
|
7704
|
+
if (sentiment !== "good" && sentiment !== "bad") {
|
|
7705
|
+
throw new Error('Job feedback sentiment must be "good" or "bad".');
|
|
6248
7706
|
}
|
|
6249
|
-
|
|
7707
|
+
const comment = typeof input.comment === "string" ? input.comment.trim() : "";
|
|
7708
|
+
const response = await this.client.call("job.feedback", {
|
|
7709
|
+
jobId: this.id,
|
|
7710
|
+
sentiment,
|
|
7711
|
+
comment: comment || void 0,
|
|
7712
|
+
metadata: this.buildFeedbackMetadata()
|
|
7713
|
+
});
|
|
7714
|
+
this.emit("feedback", response);
|
|
7715
|
+
return response;
|
|
6250
7716
|
}
|
|
6251
|
-
|
|
6252
|
-
|
|
6253
|
-
|
|
6254
|
-
|
|
6255
|
-
|
|
6256
|
-
|
|
6257
|
-
|
|
6258
|
-
|
|
6259
|
-
|
|
7717
|
+
on(event, handler) {
|
|
7718
|
+
if (!this.eventListeners.has(event)) {
|
|
7719
|
+
this.eventListeners.set(event, []);
|
|
7720
|
+
}
|
|
7721
|
+
this.eventListeners.get(event).push(handler);
|
|
7722
|
+
return () => {
|
|
7723
|
+
const handlers = this.eventListeners.get(event);
|
|
7724
|
+
if (!handlers) {
|
|
7725
|
+
return;
|
|
7726
|
+
}
|
|
7727
|
+
this.eventListeners.set(
|
|
7728
|
+
event,
|
|
7729
|
+
handlers.filter((current) => current !== handler)
|
|
7730
|
+
);
|
|
7731
|
+
};
|
|
6260
7732
|
}
|
|
6261
|
-
|
|
6262
|
-
|
|
7733
|
+
buildFeedbackMetadata() {
|
|
7734
|
+
const startedAt = this.metadata.startedAt;
|
|
7735
|
+
const completedAt = this.metadata.completedAt;
|
|
7736
|
+
return {
|
|
7737
|
+
...this.metadata,
|
|
7738
|
+
durationMs: typeof startedAt === "number" && typeof completedAt === "number" ? completedAt - startedAt : this.metadata.durationMs ?? null,
|
|
7739
|
+
stdout: [...this.metadata.stdout],
|
|
7740
|
+
stderr: [...this.metadata.stderr],
|
|
7741
|
+
toolCalls: this.metadata.toolCalls.slice(0, MAX_FEEDBACK_TOOL_CALLS).map((call) => ({ ...call }))
|
|
7742
|
+
};
|
|
6263
7743
|
}
|
|
6264
|
-
|
|
6265
|
-
|
|
6266
|
-
|
|
6267
|
-
const variable = heap.variablesByName?.[variableName];
|
|
6268
|
-
if (!variable) continue;
|
|
6269
|
-
if (variable.kind === "entry" && variable.entryPath) {
|
|
6270
|
-
refs.entryPaths.add(variable.entryPath);
|
|
7744
|
+
markStarted(timestamp = Date.now()) {
|
|
7745
|
+
if (!this.metadata.startedAt) {
|
|
7746
|
+
this.metadata.startedAt = timestamp;
|
|
6271
7747
|
}
|
|
6272
|
-
if (
|
|
6273
|
-
|
|
7748
|
+
if (this.status === "queued") {
|
|
7749
|
+
this.status = "running";
|
|
7750
|
+
}
|
|
7751
|
+
if (this.metadata.status === "queued") {
|
|
7752
|
+
this.metadata.status = "running";
|
|
6274
7753
|
}
|
|
6275
7754
|
}
|
|
6276
|
-
}
|
|
6277
|
-
|
|
6278
|
-
|
|
6279
|
-
}
|
|
6280
|
-
function sortLists(lists) {
|
|
6281
|
-
return [...lists].sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
6282
|
-
}
|
|
6283
|
-
function dedupeEntries(entries) {
|
|
6284
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6285
|
-
const result = [];
|
|
6286
|
-
for (const entry of entries) {
|
|
6287
|
-
if (!entry?.path || seen.has(entry.path)) continue;
|
|
6288
|
-
seen.add(entry.path);
|
|
6289
|
-
result.push(entry);
|
|
6290
|
-
}
|
|
6291
|
-
return result;
|
|
6292
|
-
}
|
|
6293
|
-
function dedupeLists(lists) {
|
|
6294
|
-
const seen = /* @__PURE__ */ new Set();
|
|
6295
|
-
const result = [];
|
|
6296
|
-
for (const list of lists) {
|
|
6297
|
-
if (!list?.name || seen.has(list.name)) continue;
|
|
6298
|
-
seen.add(list.name);
|
|
6299
|
-
result.push(list);
|
|
6300
|
-
}
|
|
6301
|
-
return result;
|
|
6302
|
-
}
|
|
6303
|
-
function extractResponseText(result, stdout) {
|
|
6304
|
-
const directText = normalizeText(result);
|
|
6305
|
-
if (directText) return directText;
|
|
6306
|
-
const record = asRecord2(result);
|
|
6307
|
-
if (record) {
|
|
6308
|
-
for (const key of RESPONSE_KEYS) {
|
|
6309
|
-
const normalized = normalizeText(record[key]);
|
|
6310
|
-
if (normalized) return normalized;
|
|
7755
|
+
finalize(status, result, error, options = {}) {
|
|
7756
|
+
if (!this.metadata.startedAt) {
|
|
7757
|
+
this.metadata.startedAt = Date.now();
|
|
6311
7758
|
}
|
|
6312
|
-
|
|
6313
|
-
|
|
6314
|
-
|
|
6315
|
-
|
|
6316
|
-
|
|
6317
|
-
|
|
6318
|
-
|
|
7759
|
+
this.status = status;
|
|
7760
|
+
this.metadata.status = status;
|
|
7761
|
+
this.metadata.completedAt = this.metadata.completedAt || Date.now();
|
|
7762
|
+
this.metadata.durationMs = this.metadata.completedAt - this.metadata.startedAt;
|
|
7763
|
+
if (!this.resultSettled && (options.hasResult || result !== void 0)) {
|
|
7764
|
+
this.metadata.result = sanitizeFeedbackValue(result);
|
|
7765
|
+
this.resultSettled = true;
|
|
7766
|
+
this._resolveResult(result);
|
|
6319
7767
|
}
|
|
6320
|
-
|
|
6321
|
-
|
|
6322
|
-
|
|
6323
|
-
|
|
6324
|
-
|
|
6325
|
-
|
|
6326
|
-
|
|
6327
|
-
if (lists.length > 0) {
|
|
6328
|
-
const emptyOnly = lists.every((list) => (list.paths || []).length === 0);
|
|
6329
|
-
if (emptyOnly) {
|
|
6330
|
-
return lists.length === 1 ? "I saved one empty result set." : `I saved ${lists.length} empty result sets.`;
|
|
7768
|
+
if (!this.resultSettled && (error !== void 0 || status === "failed" || status === "timeout" || status === "canceled")) {
|
|
7769
|
+
const fallbackError = new Error(`Job ${this.id} ${status}.`);
|
|
7770
|
+
const cause = error ?? fallbackError;
|
|
7771
|
+
const message = cause instanceof Error ? cause.message : String(cause);
|
|
7772
|
+
this.metadata.error = truncateFeedbackString(message);
|
|
7773
|
+
this.resultSettled = true;
|
|
7774
|
+
this._rejectResult(cause);
|
|
6331
7775
|
}
|
|
6332
|
-
return lists.length === 1 ? "I saved one result set." : `I saved ${lists.length} result sets.`;
|
|
6333
7776
|
}
|
|
6334
|
-
|
|
6335
|
-
}
|
|
6336
|
-
|
|
6337
|
-
|
|
6338
|
-
|
|
6339
|
-
|
|
6340
|
-
|
|
6341
|
-
|
|
6342
|
-
|
|
6343
|
-
|
|
6344
|
-
|
|
6345
|
-
|
|
6346
|
-
|
|
6347
|
-
|
|
6348
|
-
|
|
6349
|
-
|
|
6350
|
-
|
|
6351
|
-
|
|
6352
|
-
|
|
6353
|
-
for (const path of list.paths || []) {
|
|
6354
|
-
const entry = heap.entriesByPath?.[path];
|
|
6355
|
-
if (entry) entries.push(entry);
|
|
7777
|
+
upsertToolCall(next) {
|
|
7778
|
+
const callId = next.callId || `tool-call-${Date.now()}`;
|
|
7779
|
+
const existingIndex = this.metadata.toolCalls.findIndex(
|
|
7780
|
+
(entry) => entry.callId === callId
|
|
7781
|
+
);
|
|
7782
|
+
const existing = existingIndex >= 0 ? this.metadata.toolCalls[existingIndex] : void 0;
|
|
7783
|
+
const merged = {
|
|
7784
|
+
...existing,
|
|
7785
|
+
...next,
|
|
7786
|
+
callId,
|
|
7787
|
+
toolName: next.toolName || existing?.toolName
|
|
7788
|
+
};
|
|
7789
|
+
if (merged.durationMs === void 0 && typeof merged.startedAt === "number" && typeof merged.completedAt === "number") {
|
|
7790
|
+
merged.durationMs = merged.completedAt - merged.startedAt;
|
|
7791
|
+
}
|
|
7792
|
+
if (existingIndex >= 0) {
|
|
7793
|
+
const updated = [...this.metadata.toolCalls];
|
|
7794
|
+
updated[existingIndex] = merged;
|
|
7795
|
+
return updated.slice(-MAX_FEEDBACK_TOOL_CALLS);
|
|
6356
7796
|
}
|
|
7797
|
+
return [...this.metadata.toolCalls, merged].slice(-MAX_FEEDBACK_TOOL_CALLS);
|
|
6357
7798
|
}
|
|
6358
|
-
|
|
6359
|
-
|
|
6360
|
-
|
|
6361
|
-
|
|
6362
|
-
|
|
6363
|
-
stdout = [],
|
|
6364
|
-
agentMessages = [],
|
|
6365
|
-
sessionHeap,
|
|
6366
|
-
allowExplicitArtifacts = true
|
|
6367
|
-
}) {
|
|
6368
|
-
const refs = {
|
|
6369
|
-
entryPaths: /* @__PURE__ */ new Set(),
|
|
6370
|
-
listNames: /* @__PURE__ */ new Set(),
|
|
6371
|
-
variableNames: /* @__PURE__ */ new Set()
|
|
6372
|
-
};
|
|
6373
|
-
if (allowExplicitArtifacts) {
|
|
6374
|
-
scanForHeapReferences(result, sessionHeap, refs);
|
|
6375
|
-
resolveVariablesToReferences(refs.variableNames, sessionHeap, refs);
|
|
7799
|
+
emit(event, data) {
|
|
7800
|
+
const handlers = this.eventListeners.get(event);
|
|
7801
|
+
if (handlers) {
|
|
7802
|
+
handlers.forEach((h) => h(data));
|
|
7803
|
+
}
|
|
6376
7804
|
}
|
|
6377
|
-
|
|
6378
|
-
[...refs.listNames].map((name) => sessionHeap.listsByName?.[name]).filter((list) => Boolean(list))
|
|
6379
|
-
);
|
|
6380
|
-
const referencedEntries = sortEntries(
|
|
6381
|
-
[...refs.entryPaths].map((path) => sessionHeap.entriesByPath?.[path]).filter((entry) => Boolean(entry))
|
|
6382
|
-
);
|
|
6383
|
-
const jobLists = getJobRelatedLists(sessionHeap, jobId);
|
|
6384
|
-
const jobEntries = getJobRelatedEntries(sessionHeap, jobId);
|
|
6385
|
-
const changedEntries = dedupeEntries([
|
|
6386
|
-
...jobEntries,
|
|
6387
|
-
...entriesFromLists(jobLists, sessionHeap)
|
|
6388
|
-
]);
|
|
6389
|
-
const explicitLists = dedupeLists(referencedLists);
|
|
6390
|
-
const explicitEntries = dedupeEntries([
|
|
6391
|
-
...referencedEntries,
|
|
6392
|
-
...entriesFromLists(referencedLists, sessionHeap)
|
|
6393
|
-
]);
|
|
6394
|
-
const hasExplicitArtifacts = allowExplicitArtifacts && (explicitEntries.length > 0 || explicitLists.length > 0);
|
|
6395
|
-
const lists = hasExplicitArtifacts ? explicitLists : jobLists;
|
|
6396
|
-
const entries = hasExplicitArtifacts ? explicitEntries : changedEntries;
|
|
6397
|
-
const responseText = extractResponseText(result, stdout) || responseTextFromAgentMessages(agentMessages) || fallbackResponseText(entries, lists);
|
|
6398
|
-
return {
|
|
6399
|
-
responseText,
|
|
6400
|
-
entries,
|
|
6401
|
-
lists,
|
|
6402
|
-
changedEntries,
|
|
6403
|
-
changedLists: jobLists,
|
|
6404
|
-
hasExplicitArtifacts
|
|
6405
|
-
};
|
|
6406
|
-
}
|
|
7805
|
+
};
|
|
6407
7806
|
|
|
6408
7807
|
// src/session-transcript.ts
|
|
6409
|
-
var EMPTY_HEAP = {
|
|
6410
|
-
entriesByPath: {},
|
|
6411
|
-
listsByName: {},
|
|
6412
|
-
variablesByName: {}};
|
|
6413
7808
|
function asRecord3(value) {
|
|
6414
|
-
|
|
6415
|
-
return value;
|
|
6416
|
-
}
|
|
6417
|
-
function asArray(value) {
|
|
6418
|
-
return Array.isArray(value) ? value : [];
|
|
6419
|
-
}
|
|
6420
|
-
function asNumber(value) {
|
|
6421
|
-
return typeof value === "number" && Number.isFinite(value) ? value : void 0;
|
|
7809
|
+
return value !== null && typeof value === "object" && !Array.isArray(value) ? value : null;
|
|
6422
7810
|
}
|
|
6423
7811
|
function asString(value) {
|
|
6424
7812
|
return typeof value === "string" ? value : void 0;
|
|
@@ -6439,142 +7827,17 @@ function compactJson(value, maxLength = 320) {
|
|
|
6439
7827
|
function artifactRecordsById(liveDoc) {
|
|
6440
7828
|
const artifacts = asRecord3(liveDoc?.artifacts);
|
|
6441
7829
|
const byId = asRecord3(artifacts?.byId) || {};
|
|
6442
|
-
return Object.fromEntries(
|
|
6443
|
-
Object.entries(byId).map(([artifactId, value]) => {
|
|
6444
|
-
const record = asRecord3(value);
|
|
6445
|
-
return record ? [artifactId, record] : null;
|
|
6446
|
-
}).filter(
|
|
6447
|
-
|
|
6448
|
-
)
|
|
6449
|
-
);
|
|
6450
|
-
}
|
|
6451
|
-
function normalizeShowRefs(value) {
|
|
6452
|
-
const record = asRecord3(value);
|
|
6453
|
-
if (!record) return void 0;
|
|
6454
|
-
const normalizeRefs = (input) => {
|
|
6455
|
-
if (!Array.isArray(input)) return void 0;
|
|
6456
|
-
const refs = Array.from(
|
|
6457
|
-
new Set(
|
|
6458
|
-
input.filter((item) => typeof item === "string").map((item) => item.trim()).filter(Boolean)
|
|
6459
|
-
)
|
|
6460
|
-
);
|
|
6461
|
-
return refs.length > 0 ? refs : void 0;
|
|
6462
|
-
};
|
|
6463
|
-
const show = {
|
|
6464
|
-
entryPaths: normalizeRefs(record.entryPaths),
|
|
6465
|
-
listNames: normalizeRefs(record.listNames),
|
|
6466
|
-
variableNames: normalizeRefs(record.variableNames),
|
|
6467
|
-
fileIds: normalizeRefs(record.fileIds),
|
|
6468
|
-
sessionArtifactIds: normalizeRefs(record.sessionArtifactIds),
|
|
6469
|
-
actionSuggestions: normalizeActionSuggestions(record.actionSuggestions),
|
|
6470
|
-
tables: Array.isArray(record.tables) ? record.tables.filter(
|
|
6471
|
-
(table) => Boolean(
|
|
6472
|
-
table && typeof table === "object" && !Array.isArray(table) && Array.isArray(table.columns) && Array.isArray(table.rows)
|
|
6473
|
-
)
|
|
6474
|
-
) : void 0
|
|
6475
|
-
};
|
|
6476
|
-
return show.entryPaths || show.listNames || show.variableNames || show.fileIds || show.sessionArtifactIds || show.actionSuggestions || show.tables ? show : void 0;
|
|
6477
|
-
}
|
|
6478
|
-
function normalizeActionSuggestions(value) {
|
|
6479
|
-
if (!Array.isArray(value)) return void 0;
|
|
6480
|
-
const suggestions = [];
|
|
6481
|
-
for (const item of value) {
|
|
6482
|
-
const record = asRecord3(item);
|
|
6483
|
-
if (!record) continue;
|
|
6484
|
-
const label = trimString(record.label);
|
|
6485
|
-
if (!label) continue;
|
|
6486
|
-
const suggestionId = trimString(record.suggestionId) || trimString(record.id) || label;
|
|
6487
|
-
suggestions.push({
|
|
6488
|
-
suggestionId,
|
|
6489
|
-
label,
|
|
6490
|
-
...typeof record.description === "string" ? { description: record.description } : {},
|
|
6491
|
-
...asRecord3(record.artifact) ? { artifact: asRecord3(record.artifact) } : {},
|
|
6492
|
-
...asRecord3(record.target) ? { target: asRecord3(record.target) } : {},
|
|
6493
|
-
...asRecord3(record.metadata) ? { metadata: asRecord3(record.metadata) } : {}
|
|
6494
|
-
});
|
|
6495
|
-
}
|
|
6496
|
-
return suggestions.length ? suggestions : void 0;
|
|
6497
|
-
}
|
|
6498
|
-
var TRANSCRIPT_MESSAGE_PART_LIMIT = 128;
|
|
6499
|
-
var TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT = 2e5;
|
|
6500
|
-
var TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT = 1e3;
|
|
6501
|
-
var TRANSCRIPT_MESSAGE_ACTION_LIMIT = 64;
|
|
6502
|
-
function normalizeConversationMessageActions(value) {
|
|
6503
|
-
if (!Array.isArray(value) || value.length === 0) return void 0;
|
|
6504
|
-
const actions = [];
|
|
6505
|
-
for (const item of value.slice(0, TRANSCRIPT_MESSAGE_ACTION_LIMIT)) {
|
|
6506
|
-
const record = asRecord3(item);
|
|
6507
|
-
const kind = record?.kind;
|
|
6508
|
-
const label = trimString(record?.label ?? record?.title);
|
|
6509
|
-
const status = record?.status;
|
|
6510
|
-
if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
|
|
6511
|
-
continue;
|
|
6512
|
-
}
|
|
6513
|
-
actions.push({
|
|
6514
|
-
kind,
|
|
6515
|
-
label,
|
|
6516
|
-
...status === "done" || status === "queued" || status === "failed" ? { status } : {}
|
|
6517
|
-
});
|
|
6518
|
-
}
|
|
6519
|
-
return actions.length ? actions : void 0;
|
|
6520
|
-
}
|
|
6521
|
-
function normalizeConversationMessageParts(value, canonicalContent, canonicalActions) {
|
|
6522
|
-
if (!Array.isArray(value) || value.length === 0 || value.length > TRANSCRIPT_MESSAGE_PART_LIMIT) {
|
|
6523
|
-
return void 0;
|
|
6524
|
-
}
|
|
6525
|
-
const parts = [];
|
|
6526
|
-
const canonicalActionsById = new Map(
|
|
6527
|
-
(canonicalActions || []).map((action) => [
|
|
6528
|
-
`${action.kind}:${action.label}`,
|
|
6529
|
-
action
|
|
6530
|
-
])
|
|
6531
|
-
);
|
|
6532
|
-
const seenActionIds = /* @__PURE__ */ new Set();
|
|
6533
|
-
let textLength = 0;
|
|
6534
|
-
for (const item of value) {
|
|
6535
|
-
const record = asRecord3(item);
|
|
6536
|
-
if (!record) return void 0;
|
|
6537
|
-
if (record.type === "text") {
|
|
6538
|
-
if (typeof record.text !== "string" || record.text.length === 0) {
|
|
6539
|
-
return void 0;
|
|
6540
|
-
}
|
|
6541
|
-
textLength += record.text.length;
|
|
6542
|
-
if (textLength > TRANSCRIPT_MESSAGE_PART_TEXT_LIMIT) return void 0;
|
|
6543
|
-
parts.push({ type: "text", text: record.text });
|
|
6544
|
-
continue;
|
|
6545
|
-
}
|
|
6546
|
-
if (record.type !== "action") return void 0;
|
|
6547
|
-
const action = asRecord3(record.action);
|
|
6548
|
-
const kind = action?.kind;
|
|
6549
|
-
const label = trimString(action?.label);
|
|
6550
|
-
if (kind !== "frontend" && kind !== "backend" && kind !== "system" || !label || label.length > TRANSCRIPT_MESSAGE_PART_ACTION_LABEL_LIMIT) {
|
|
6551
|
-
return void 0;
|
|
6552
|
-
}
|
|
6553
|
-
const actionId = `${kind}:${label}`;
|
|
6554
|
-
const canonicalAction = canonicalActionsById.get(actionId);
|
|
6555
|
-
if (!canonicalAction) return void 0;
|
|
6556
|
-
if (seenActionIds.has(actionId)) continue;
|
|
6557
|
-
seenActionIds.add(actionId);
|
|
6558
|
-
parts.push({
|
|
6559
|
-
type: "action",
|
|
6560
|
-
action: canonicalAction
|
|
6561
|
-
});
|
|
6562
|
-
}
|
|
6563
|
-
const orderedText = parts.filter(
|
|
6564
|
-
(part) => part.type === "text"
|
|
6565
|
-
).map((part) => part.text).join("");
|
|
6566
|
-
return orderedText === canonicalContent ? parts : void 0;
|
|
7830
|
+
return Object.fromEntries(
|
|
7831
|
+
Object.entries(byId).map(([artifactId, value]) => {
|
|
7832
|
+
const record = asRecord3(value);
|
|
7833
|
+
return record ? [artifactId, record] : null;
|
|
7834
|
+
}).filter((entry) => Boolean(entry))
|
|
7835
|
+
);
|
|
6567
7836
|
}
|
|
6568
7837
|
function stringifyTranscriptValue(value, fallback = "") {
|
|
6569
|
-
if (typeof value === "string")
|
|
6570
|
-
|
|
6571
|
-
|
|
6572
|
-
if (typeof value === "boolean") {
|
|
6573
|
-
return value ? "Confirmed" : "Canceled";
|
|
6574
|
-
}
|
|
6575
|
-
if (value === void 0) {
|
|
6576
|
-
return fallback;
|
|
6577
|
-
}
|
|
7838
|
+
if (typeof value === "string") return value.trim() || fallback;
|
|
7839
|
+
if (typeof value === "boolean") return value ? "Confirmed" : "Canceled";
|
|
7840
|
+
if (value === void 0) return fallback;
|
|
6578
7841
|
try {
|
|
6579
7842
|
const json = JSON.stringify(value, null, 2);
|
|
6580
7843
|
if (!json || json === "undefined") return fallback;
|
|
@@ -6715,262 +7978,121 @@ ${stringifyTranscriptValue({ show }, "")}`;
|
|
|
6715
7978
|
hasOtherRefs ? `Other shown refs: ${stringifyTranscriptValue(otherRefs, "")}` : null
|
|
6716
7979
|
].filter(Boolean).join("\n");
|
|
6717
7980
|
}
|
|
6718
|
-
function
|
|
6719
|
-
|
|
6720
|
-
|
|
6721
|
-
|
|
6722
|
-
|
|
6723
|
-
|
|
6724
|
-
|
|
6725
|
-
|
|
6726
|
-
|
|
6727
|
-
|
|
6728
|
-
const show = normalizeShowRefs(record.show);
|
|
6729
|
-
const id = asString(record.id) || crypto.randomUUID();
|
|
6730
|
-
const timestamp = asNumber(record.timestamp) || asNumber(record.ts) || 0;
|
|
6731
|
-
if (!content && !show && !actions?.length) return null;
|
|
6732
|
-
const artifactHistory = buildArtifactHistory(show, artifactsById);
|
|
6733
|
-
const historyContent = role === "assistant" ? content && artifactHistory ? `[Assistant reply]
|
|
6734
|
-
${content}
|
|
6735
|
-
|
|
6736
|
-
${artifactHistory}` : content ? `[Assistant reply]
|
|
6737
|
-
${content}` : artifactHistory : void 0;
|
|
6738
|
-
return {
|
|
6739
|
-
id,
|
|
6740
|
-
role,
|
|
6741
|
-
content,
|
|
6742
|
-
timestamp,
|
|
6743
|
-
jobId: asString(record.jobId),
|
|
6744
|
-
promptId: asString(record.promptId),
|
|
6745
|
-
show,
|
|
6746
|
-
actions,
|
|
6747
|
-
parts,
|
|
6748
|
-
historyContent,
|
|
6749
|
-
source: "conversation"
|
|
6750
|
-
};
|
|
6751
|
-
}
|
|
6752
|
-
function normalizePromptEntries(jobId, rawPrompts, conversationPromptIds) {
|
|
6753
|
-
const promptsById = asRecord3(rawPrompts) || {};
|
|
6754
|
-
return Object.values(promptsById).map((value) => asRecord3(value)).filter((value) => Boolean(value)).sort(
|
|
6755
|
-
(left, right) => (asNumber(left.openedAt) || asNumber(left.answeredAt) || 0) - (asNumber(right.openedAt) || asNumber(right.answeredAt) || 0)
|
|
6756
|
-
).flatMap((prompt) => {
|
|
6757
|
-
const promptId = asString(prompt.promptId);
|
|
6758
|
-
if (!promptId || conversationPromptIds.has(promptId)) return [];
|
|
6759
|
-
const title = trimString(prompt.title);
|
|
6760
|
-
const message = trimString(prompt.message);
|
|
6761
|
-
const assistantContent = message || title || "Input required";
|
|
6762
|
-
const openedAt = asNumber(prompt.openedAt) || 0;
|
|
6763
|
-
const answeredAt = asNumber(prompt.answeredAt) || openedAt;
|
|
6764
|
-
const entries = [
|
|
6765
|
-
{
|
|
6766
|
-
id: `prompt:${promptId}:assistant`,
|
|
6767
|
-
role: "assistant",
|
|
6768
|
-
content: assistantContent,
|
|
6769
|
-
timestamp: openedAt,
|
|
6770
|
-
jobId,
|
|
6771
|
-
promptId,
|
|
6772
|
-
historyContent: `[Assistant reply]
|
|
6773
|
-
${assistantContent}`,
|
|
6774
|
-
source: "job_prompt"
|
|
7981
|
+
function feedItemShow(item) {
|
|
7982
|
+
switch (item.kind) {
|
|
7983
|
+
case "objects": {
|
|
7984
|
+
const entryPaths = [];
|
|
7985
|
+
const listNames = [];
|
|
7986
|
+
const variableNames = [];
|
|
7987
|
+
for (const ref of item.payload.refs) {
|
|
7988
|
+
if (ref.type === "entry") entryPaths.push(ref.path);
|
|
7989
|
+
if (ref.type === "list") listNames.push(ref.name);
|
|
7990
|
+
if (ref.type === "variable") variableNames.push(ref.name);
|
|
6775
7991
|
}
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
6781
|
-
content: stringifyTranscriptValue(prompt.answer, ""),
|
|
6782
|
-
timestamp: answeredAt,
|
|
6783
|
-
jobId,
|
|
6784
|
-
promptId,
|
|
6785
|
-
source: "job_prompt"
|
|
6786
|
-
});
|
|
7992
|
+
return {
|
|
7993
|
+
...entryPaths.length ? { entryPaths } : {},
|
|
7994
|
+
...listNames.length ? { listNames } : {},
|
|
7995
|
+
...variableNames.length ? { variableNames } : {}
|
|
7996
|
+
};
|
|
6787
7997
|
}
|
|
6788
|
-
|
|
6789
|
-
|
|
7998
|
+
case "table":
|
|
7999
|
+
return {
|
|
8000
|
+
tables: [
|
|
8001
|
+
{
|
|
8002
|
+
id: item.payload.tableId,
|
|
8003
|
+
label: item.payload.label,
|
|
8004
|
+
columns: item.payload.columns,
|
|
8005
|
+
rows: item.payload.rows
|
|
8006
|
+
}
|
|
8007
|
+
]
|
|
8008
|
+
};
|
|
8009
|
+
case "artifact":
|
|
8010
|
+
return { sessionArtifactIds: [item.payload.artifactId] };
|
|
8011
|
+
case "file":
|
|
8012
|
+
return { fileIds: [item.payload.fileId] };
|
|
8013
|
+
case "action_suggestion":
|
|
8014
|
+
return {
|
|
8015
|
+
actionSuggestions: [
|
|
8016
|
+
{
|
|
8017
|
+
suggestionId: item.payload.suggestionId,
|
|
8018
|
+
label: item.payload.label,
|
|
8019
|
+
description: item.payload.description,
|
|
8020
|
+
target: item.payload.target ? { ...item.payload.target } : void 0,
|
|
8021
|
+
artifact: item.payload.proposedArtifact ? { ...item.payload.proposedArtifact } : void 0
|
|
8022
|
+
}
|
|
8023
|
+
]
|
|
8024
|
+
};
|
|
8025
|
+
default:
|
|
8026
|
+
return void 0;
|
|
8027
|
+
}
|
|
6790
8028
|
}
|
|
6791
|
-
function
|
|
6792
|
-
|
|
6793
|
-
|
|
6794
|
-
|
|
6795
|
-
|
|
6796
|
-
|
|
6797
|
-
|
|
6798
|
-
|
|
6799
|
-
|
|
6800
|
-
|
|
6801
|
-
const entries = [];
|
|
6802
|
-
if (reply) {
|
|
8029
|
+
function buildSessionTranscriptFromFeedItems(input) {
|
|
8030
|
+
const artifactsById = artifactRecordsById(input.liveDoc);
|
|
8031
|
+
const entries = [];
|
|
8032
|
+
for (const item of [...input.items].sort(
|
|
8033
|
+
(left, right) => left.sequence - right.sequence
|
|
8034
|
+
)) {
|
|
8035
|
+
if (item.kind === "feedback") continue;
|
|
8036
|
+
if (item.kind === "message") {
|
|
8037
|
+
if (item.payload.role === "system") continue;
|
|
8038
|
+
const content = item.payload.text;
|
|
6803
8039
|
entries.push({
|
|
6804
|
-
id:
|
|
6805
|
-
role:
|
|
6806
|
-
content
|
|
6807
|
-
timestamp,
|
|
6808
|
-
|
|
6809
|
-
|
|
6810
|
-
|
|
6811
|
-
|
|
8040
|
+
id: item.id,
|
|
8041
|
+
role: item.payload.role,
|
|
8042
|
+
content,
|
|
8043
|
+
timestamp: item.occurredAt,
|
|
8044
|
+
sequence: item.sequence,
|
|
8045
|
+
jobId: item.source?.jobId,
|
|
8046
|
+
promptId: item.payload.inReplyToPromptId,
|
|
8047
|
+
historyContent: item.payload.role === "assistant" ? `[Assistant reply]
|
|
8048
|
+
${content}` : void 0,
|
|
8049
|
+
source: "feed"
|
|
6812
8050
|
});
|
|
8051
|
+
continue;
|
|
6813
8052
|
}
|
|
6814
|
-
if (
|
|
8053
|
+
if (item.kind === "prompt") {
|
|
8054
|
+
const content = item.payload.message || item.payload.title;
|
|
6815
8055
|
entries.push({
|
|
6816
|
-
id:
|
|
8056
|
+
id: item.id,
|
|
6817
8057
|
role: "assistant",
|
|
6818
|
-
content
|
|
6819
|
-
timestamp,
|
|
6820
|
-
|
|
6821
|
-
|
|
6822
|
-
|
|
6823
|
-
|
|
8058
|
+
content,
|
|
8059
|
+
timestamp: item.occurredAt,
|
|
8060
|
+
sequence: item.sequence,
|
|
8061
|
+
jobId: item.source?.jobId,
|
|
8062
|
+
promptId: item.payload.promptId,
|
|
8063
|
+
historyContent: `[Assistant reply]
|
|
8064
|
+
${content}`,
|
|
8065
|
+
source: "feed"
|
|
6824
8066
|
});
|
|
8067
|
+
continue;
|
|
6825
8068
|
}
|
|
6826
|
-
|
|
6827
|
-
|
|
6828
|
-
}
|
|
6829
|
-
function buildJobFallbackEntries(jobId, job, sessionHeap, artifactsById) {
|
|
6830
|
-
const timestamp = asNumber(job.finishedAt) || asNumber(job.startedAt) || asNumber(job.submittedAt) || 0;
|
|
6831
|
-
const resultPreview = stringifyTranscriptValue(
|
|
6832
|
-
job.result,
|
|
6833
|
-
"No job result recorded."
|
|
6834
|
-
);
|
|
6835
|
-
const presentation = resolveJobPresentation({
|
|
6836
|
-
jobId,
|
|
6837
|
-
result: job.result,
|
|
6838
|
-
stdout: [],
|
|
6839
|
-
sessionHeap
|
|
6840
|
-
});
|
|
6841
|
-
const entries = [];
|
|
6842
|
-
const responseText = presentation.responseText || "";
|
|
6843
|
-
if (responseText) {
|
|
6844
|
-
entries.push({
|
|
6845
|
-
id: `job:${jobId}:result-text`,
|
|
6846
|
-
role: "assistant",
|
|
6847
|
-
content: responseText,
|
|
6848
|
-
timestamp,
|
|
6849
|
-
jobId,
|
|
6850
|
-
historyContent: `[Assistant reply]
|
|
6851
|
-
${responseText}`,
|
|
6852
|
-
source: "job_result"
|
|
6853
|
-
});
|
|
6854
|
-
}
|
|
6855
|
-
const show = {
|
|
6856
|
-
entryPaths: presentation.entries.map((entry) => entry.path),
|
|
6857
|
-
listNames: presentation.lists.map((list) => list.name)
|
|
6858
|
-
};
|
|
6859
|
-
if (show.entryPaths && show.entryPaths.length > 0 || show.listNames && show.listNames.length > 0) {
|
|
8069
|
+
const show = feedItemShow(item);
|
|
8070
|
+
if (!show) continue;
|
|
6860
8071
|
entries.push({
|
|
6861
|
-
id:
|
|
8072
|
+
id: item.id,
|
|
6862
8073
|
role: "assistant",
|
|
6863
8074
|
content: "",
|
|
6864
|
-
timestamp,
|
|
6865
|
-
|
|
8075
|
+
timestamp: item.occurredAt,
|
|
8076
|
+
sequence: item.sequence,
|
|
8077
|
+
jobId: item.source?.jobId,
|
|
6866
8078
|
show,
|
|
6867
8079
|
historyContent: buildArtifactHistory(show, artifactsById),
|
|
6868
|
-
source: "
|
|
6869
|
-
});
|
|
6870
|
-
}
|
|
6871
|
-
if (entries.length === 0 && trimString(job.error)) {
|
|
6872
|
-
entries.push({
|
|
6873
|
-
id: `job:${jobId}:result-error`,
|
|
6874
|
-
role: "assistant",
|
|
6875
|
-
content: trimString(job.error),
|
|
6876
|
-
timestamp,
|
|
6877
|
-
jobId,
|
|
6878
|
-
historyContent: `[Assistant reply]
|
|
6879
|
-
${trimString(job.error)}`,
|
|
6880
|
-
source: "job_result"
|
|
6881
|
-
});
|
|
6882
|
-
}
|
|
6883
|
-
if (entries.length === 0 && resultPreview && resultPreview !== "No job result recorded.") {
|
|
6884
|
-
entries.push({
|
|
6885
|
-
id: `job:${jobId}:result-preview`,
|
|
6886
|
-
role: "assistant",
|
|
6887
|
-
content: resultPreview,
|
|
6888
|
-
timestamp,
|
|
6889
|
-
jobId,
|
|
6890
|
-
historyContent: `[Assistant reply]
|
|
6891
|
-
${resultPreview}`,
|
|
6892
|
-
source: "job_result"
|
|
8080
|
+
source: "feed"
|
|
6893
8081
|
});
|
|
6894
8082
|
}
|
|
6895
8083
|
return entries;
|
|
6896
8084
|
}
|
|
6897
|
-
|
|
6898
|
-
const code = trimString(job.source);
|
|
6899
|
-
if (!code) return null;
|
|
6900
|
-
const jobStatus = asString(job.status);
|
|
6901
|
-
const error = jobStatus === "failed" || jobStatus === "canceled" || jobStatus === "timeout" ? trimString(job.error) || `Job ${jobStatus}` : void 0;
|
|
6902
|
-
return {
|
|
6903
|
-
id: `job:${jobId}:code`,
|
|
6904
|
-
role: "assistant",
|
|
6905
|
-
content: "",
|
|
6906
|
-
timestamp: asNumber(job.submittedAt) || asNumber(job.startedAt) || asNumber(job.finishedAt) || 0,
|
|
6907
|
-
jobId,
|
|
6908
|
-
code,
|
|
6909
|
-
jobStatus,
|
|
6910
|
-
jobResultPreview: stringifyTranscriptValue(
|
|
6911
|
-
job.result,
|
|
6912
|
-
"No job result recorded."
|
|
6913
|
-
),
|
|
6914
|
-
error,
|
|
6915
|
-
source: "job_code"
|
|
6916
|
-
};
|
|
6917
|
-
}
|
|
8085
|
+
var CANONICAL_FEED_REQUIRED = "Canonical session feed-v1 is required; past message/job transcript reconstruction is not supported.";
|
|
6918
8086
|
function buildSessionTranscript(input) {
|
|
6919
8087
|
const liveDoc = input.liveDoc || null;
|
|
6920
|
-
|
|
6921
|
-
|
|
6922
|
-
|
|
6923
|
-
const
|
|
6924
|
-
|
|
6925
|
-
|
|
6926
|
-
|
|
6927
|
-
|
|
6928
|
-
);
|
|
6929
|
-
const assistantConversationJobIds = new Set(
|
|
6930
|
-
conversationMessages.filter(
|
|
6931
|
-
(message) => message.role === "assistant" && Boolean(message.jobId)
|
|
6932
|
-
).map((message) => message.jobId)
|
|
6933
|
-
);
|
|
6934
|
-
transcript.push(...conversationMessages);
|
|
6935
|
-
const jobsById = asRecord3(asRecord3(liveDoc?.jobs)?.byId) || {};
|
|
6936
|
-
const jobs = Object.values(jobsById).map((value) => asRecord3(value)).filter((value) => Boolean(value)).sort(
|
|
6937
|
-
(left, right) => (asNumber(left.submittedAt) || asNumber(left.startedAt) || asNumber(left.finishedAt) || 0) - (asNumber(right.submittedAt) || asNumber(right.startedAt) || asNumber(right.finishedAt) || 0)
|
|
6938
|
-
);
|
|
6939
|
-
for (const job of jobs) {
|
|
6940
|
-
const jobId = asString(job.jobId);
|
|
6941
|
-
if (!jobId) continue;
|
|
6942
|
-
const codeEntry = buildJobCodeEntry(jobId, job);
|
|
6943
|
-
if (codeEntry) {
|
|
6944
|
-
transcript.push(codeEntry);
|
|
6945
|
-
}
|
|
6946
|
-
transcript.push(
|
|
6947
|
-
...normalizePromptEntries(jobId, job.prompts, conversationPromptIds)
|
|
6948
|
-
);
|
|
6949
|
-
if (!assistantConversationJobIds.has(jobId)) {
|
|
6950
|
-
const agentEntries = normalizeAgentMessageEntries(
|
|
6951
|
-
jobId,
|
|
6952
|
-
job.agentMessages,
|
|
6953
|
-
artifactsById
|
|
6954
|
-
);
|
|
6955
|
-
if (agentEntries.length > 0) {
|
|
6956
|
-
transcript.push(...agentEntries);
|
|
6957
|
-
} else {
|
|
6958
|
-
transcript.push(
|
|
6959
|
-
...buildJobFallbackEntries(
|
|
6960
|
-
jobId,
|
|
6961
|
-
job,
|
|
6962
|
-
sessionHeap,
|
|
6963
|
-
artifactsById
|
|
6964
|
-
)
|
|
6965
|
-
);
|
|
6966
|
-
}
|
|
6967
|
-
}
|
|
6968
|
-
}
|
|
6969
|
-
return transcript.sort((left, right) => {
|
|
6970
|
-
if (left.timestamp !== right.timestamp) {
|
|
6971
|
-
return left.timestamp - right.timestamp;
|
|
6972
|
-
}
|
|
6973
|
-
return left.id.localeCompare(right.id);
|
|
8088
|
+
if (!isCanonicalSessionFeedDocument(liveDoc)) {
|
|
8089
|
+
throw new Error(CANONICAL_FEED_REQUIRED);
|
|
8090
|
+
}
|
|
8091
|
+
const snapshot = readSessionFeedSnapshot(liveDoc);
|
|
8092
|
+
if (snapshot.error) throw snapshot.error;
|
|
8093
|
+
return buildSessionTranscriptFromFeedItems({
|
|
8094
|
+
items: input.canonicalFeedItems || snapshot.tail,
|
|
8095
|
+
liveDoc
|
|
6974
8096
|
});
|
|
6975
8097
|
}
|
|
6976
8098
|
|
|
@@ -11962,7 +13084,7 @@ function normalizeEffectBehaviors(value) {
|
|
|
11962
13084
|
}
|
|
11963
13085
|
function resolveInvocationMode(context) {
|
|
11964
13086
|
const mode = context?.invocation?.mode;
|
|
11965
|
-
if (mode === "dryRun" || mode === "reverse") {
|
|
13087
|
+
if (mode === "dryRun" || mode === "reverse" || mode === "artifactOptions") {
|
|
11966
13088
|
return mode;
|
|
11967
13089
|
}
|
|
11968
13090
|
return "execute";
|
|
@@ -12016,6 +13138,18 @@ function resolveHandlerForMode(effectMap, effect, request) {
|
|
|
12016
13138
|
request.context?.behaviors || effect.metamodels || void 0
|
|
12017
13139
|
);
|
|
12018
13140
|
const mode = resolveInvocationMode(request.context);
|
|
13141
|
+
if (mode === "artifactOptions") {
|
|
13142
|
+
if (!effect.artifactOptionsHandler) {
|
|
13143
|
+
throw new Error(
|
|
13144
|
+
`Artifact relationship options are not supported for ${request.effectKey}`
|
|
13145
|
+
);
|
|
13146
|
+
}
|
|
13147
|
+
return {
|
|
13148
|
+
effect,
|
|
13149
|
+
mode,
|
|
13150
|
+
handler: effect.artifactOptionsHandler
|
|
13151
|
+
};
|
|
13152
|
+
}
|
|
12019
13153
|
if (mode === "dryRun") {
|
|
12020
13154
|
if (effect.dryRunHandler) {
|
|
12021
13155
|
return { effect, mode, handler: effect.dryRunHandler };
|
|
@@ -12048,7 +13182,91 @@ function resolveHandlerForMode(effectMap, effect, request) {
|
|
|
12048
13182
|
}
|
|
12049
13183
|
return { effect, mode, handler: effect.handler };
|
|
12050
13184
|
}
|
|
12051
|
-
|
|
13185
|
+
function createInvocationFeedbackContext(bridge) {
|
|
13186
|
+
const publications = [];
|
|
13187
|
+
let nextOperationOrdinal = 0;
|
|
13188
|
+
const nextOperationId = (kind) => `${kind}:${nextOperationOrdinal++}`;
|
|
13189
|
+
const track = (publication) => {
|
|
13190
|
+
const tracked = Promise.resolve(publication);
|
|
13191
|
+
publications.push(tracked);
|
|
13192
|
+
void tracked.catch(() => void 0);
|
|
13193
|
+
return tracked;
|
|
13194
|
+
};
|
|
13195
|
+
const publisher = createFeedPublisher(
|
|
13196
|
+
(method, params) => bridge.publish(method, {
|
|
13197
|
+
...params,
|
|
13198
|
+
invocationId: bridge.invocationId
|
|
13199
|
+
})
|
|
13200
|
+
);
|
|
13201
|
+
const wrapTransientHandle = (initial) => {
|
|
13202
|
+
let current = initial;
|
|
13203
|
+
const wrapped = {
|
|
13204
|
+
get id() {
|
|
13205
|
+
return current.id;
|
|
13206
|
+
},
|
|
13207
|
+
get ordinal() {
|
|
13208
|
+
return current.ordinal;
|
|
13209
|
+
},
|
|
13210
|
+
get revision() {
|
|
13211
|
+
return current.revision;
|
|
13212
|
+
},
|
|
13213
|
+
async update(text, options = {}) {
|
|
13214
|
+
current = await track(
|
|
13215
|
+
current.update(text, {
|
|
13216
|
+
...options,
|
|
13217
|
+
operationId: options.operationId || nextOperationId("transient-update")
|
|
13218
|
+
})
|
|
13219
|
+
);
|
|
13220
|
+
return wrapped;
|
|
13221
|
+
},
|
|
13222
|
+
settle(text, options = {}) {
|
|
13223
|
+
return track(
|
|
13224
|
+
current.settle(text, {
|
|
13225
|
+
...options,
|
|
13226
|
+
operationId: options.operationId || nextOperationId("transient-settle")
|
|
13227
|
+
})
|
|
13228
|
+
);
|
|
13229
|
+
}
|
|
13230
|
+
};
|
|
13231
|
+
return wrapped;
|
|
13232
|
+
};
|
|
13233
|
+
return {
|
|
13234
|
+
feedback(text, options = {}) {
|
|
13235
|
+
return track(
|
|
13236
|
+
publisher.feedback(text, {
|
|
13237
|
+
...options,
|
|
13238
|
+
operationId: options.operationId || nextOperationId("feedback")
|
|
13239
|
+
})
|
|
13240
|
+
);
|
|
13241
|
+
},
|
|
13242
|
+
transientFeedback(text, options = {}) {
|
|
13243
|
+
return track(
|
|
13244
|
+
publisher.transientFeedback(text, {
|
|
13245
|
+
...options,
|
|
13246
|
+
operationId: options.operationId || nextOperationId("transient-create")
|
|
13247
|
+
}).then(wrapTransientHandle)
|
|
13248
|
+
);
|
|
13249
|
+
},
|
|
13250
|
+
async flush() {
|
|
13251
|
+
let cursor = 0;
|
|
13252
|
+
let firstError;
|
|
13253
|
+
while (cursor < publications.length) {
|
|
13254
|
+
const batch = publications.slice(cursor);
|
|
13255
|
+
cursor = publications.length;
|
|
13256
|
+
const results = await Promise.allSettled(batch);
|
|
13257
|
+
for (const result of results) {
|
|
13258
|
+
if (result.status === "rejected" && firstError === void 0) {
|
|
13259
|
+
firstError = result.reason;
|
|
13260
|
+
}
|
|
13261
|
+
}
|
|
13262
|
+
}
|
|
13263
|
+
if (firstError !== void 0) {
|
|
13264
|
+
throw firstError;
|
|
13265
|
+
}
|
|
13266
|
+
}
|
|
13267
|
+
};
|
|
13268
|
+
}
|
|
13269
|
+
async function invokeRegisteredEffect(effectMap, request, options = {}) {
|
|
12052
13270
|
const effect = selectRegisteredEffect(
|
|
12053
13271
|
effectMap,
|
|
12054
13272
|
request.effectKey,
|
|
@@ -12067,14 +13285,49 @@ async function invokeRegisteredEffect(effectMap, request) {
|
|
|
12067
13285
|
mode: resolved.mode,
|
|
12068
13286
|
sourceEffectKey: request.effectKey,
|
|
12069
13287
|
sourceEffectName: request.effectName,
|
|
12070
|
-
...request.context?.invocation?.reverseHandler ? { reverseHandler: request.context.invocation.reverseHandler } : {}
|
|
13288
|
+
...request.context?.invocation?.reverseHandler ? { reverseHandler: request.context.invocation.reverseHandler } : {},
|
|
13289
|
+
...request.context?.invocation?.artifactId ? { artifactId: request.context.invocation.artifactId } : {},
|
|
13290
|
+
...request.context?.invocation?.artifactOptions ? { artifactOptions: request.context.invocation.artifactOptions } : {}
|
|
12071
13291
|
}
|
|
12072
13292
|
};
|
|
12073
|
-
|
|
12074
|
-
|
|
12075
|
-
|
|
13293
|
+
const feedbackContext = options.feedback ? createInvocationFeedbackContext(options.feedback) : null;
|
|
13294
|
+
if (feedbackContext) {
|
|
13295
|
+
context.feedback = feedbackContext.feedback;
|
|
13296
|
+
context.transientFeedback = feedbackContext.transientFeedback;
|
|
13297
|
+
}
|
|
13298
|
+
let handlerResult;
|
|
13299
|
+
let handlerError;
|
|
13300
|
+
let handlerFailed = false;
|
|
13301
|
+
try {
|
|
13302
|
+
if (resolved.mode === "artifactOptions") {
|
|
13303
|
+
handlerResult = await resolved.handler(request.input, context);
|
|
13304
|
+
} else if (resolved.effect.className && !resolved.effect.static && request.input && typeof request.input === "object" && "_objectId" in request.input) {
|
|
13305
|
+
const { _objectId, ...rest } = request.input;
|
|
13306
|
+
handlerResult = await resolved.handler(_objectId, rest, context);
|
|
13307
|
+
} else {
|
|
13308
|
+
handlerResult = await resolved.handler(request.input, context);
|
|
13309
|
+
}
|
|
13310
|
+
} catch (error) {
|
|
13311
|
+
handlerFailed = true;
|
|
13312
|
+
handlerError = error;
|
|
12076
13313
|
}
|
|
12077
|
-
|
|
13314
|
+
let feedbackError;
|
|
13315
|
+
let feedbackFailed = false;
|
|
13316
|
+
if (feedbackContext) {
|
|
13317
|
+
try {
|
|
13318
|
+
await feedbackContext.flush();
|
|
13319
|
+
} catch (error) {
|
|
13320
|
+
feedbackFailed = true;
|
|
13321
|
+
feedbackError = error;
|
|
13322
|
+
}
|
|
13323
|
+
}
|
|
13324
|
+
if (handlerFailed) {
|
|
13325
|
+
throw handlerError;
|
|
13326
|
+
}
|
|
13327
|
+
if (feedbackFailed) {
|
|
13328
|
+
throw feedbackError;
|
|
13329
|
+
}
|
|
13330
|
+
return handlerResult;
|
|
12078
13331
|
}
|
|
12079
13332
|
|
|
12080
13333
|
// src/client-normalizers.ts
|
|
@@ -14084,6 +15337,36 @@ function buildEffectMetamodelMutations(toolPath, spec) {
|
|
|
14084
15337
|
var DEFAULT_CONVERSATION_SESSION_LIST_LIMIT = 100;
|
|
14085
15338
|
var MAX_CONVERSATION_SESSION_LIST_LIMIT = 500;
|
|
14086
15339
|
var MAX_CONVERSATION_SESSION_LIST_OFFSET = 1e5;
|
|
15340
|
+
function requireUserEnvironmentSequence(value, field) {
|
|
15341
|
+
if (typeof value !== "number" || !Number.isSafeInteger(value) || value < 0) {
|
|
15342
|
+
throw new Error(
|
|
15343
|
+
`Invalid user-environment state: ${field} must be a non-negative safe integer.`
|
|
15344
|
+
);
|
|
15345
|
+
}
|
|
15346
|
+
return value;
|
|
15347
|
+
}
|
|
15348
|
+
function normalizeReadThroughSequenceMap(value) {
|
|
15349
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
15350
|
+
throw new Error(
|
|
15351
|
+
"Invalid user-environment state: readThroughSequenceBySessionId is required."
|
|
15352
|
+
);
|
|
15353
|
+
}
|
|
15354
|
+
return Object.fromEntries(
|
|
15355
|
+
Object.entries(value).map(([sessionId, sequence]) => [
|
|
15356
|
+
sessionId,
|
|
15357
|
+
requireUserEnvironmentSequence(
|
|
15358
|
+
sequence,
|
|
15359
|
+
`readThroughSequenceBySessionId.${sessionId}`
|
|
15360
|
+
)
|
|
15361
|
+
])
|
|
15362
|
+
);
|
|
15363
|
+
}
|
|
15364
|
+
function requiredAssistantReplyString(value, field) {
|
|
15365
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
15366
|
+
throw new Error(`Assistant reply ${field} must be a non-empty string.`);
|
|
15367
|
+
}
|
|
15368
|
+
return field === "text" ? value : value.trim();
|
|
15369
|
+
}
|
|
14087
15370
|
function boundedSessionListInteger(value, name, fallback, minimum, maximum) {
|
|
14088
15371
|
if (value === void 0) return fallback;
|
|
14089
15372
|
if (!Number.isInteger(value) || value < minimum || value > maximum) {
|
|
@@ -15657,12 +16940,55 @@ var Environment = class _Environment {
|
|
|
15657
16940
|
var EnvironmentSession = class extends Session {
|
|
15658
16941
|
environment;
|
|
15659
16942
|
sessionDataRoutePrefix;
|
|
16943
|
+
sessionDataHeaders;
|
|
16944
|
+
heartbeatTimer = null;
|
|
16945
|
+
heartbeatInFlight = false;
|
|
15660
16946
|
/** The last known graph container status, updated by checkReadiness() or on heartbeat */
|
|
15661
16947
|
graphContainerStatus = null;
|
|
15662
16948
|
constructor(client, environment, clientId, options = {}) {
|
|
15663
16949
|
super(client, clientId, { initialQuota: options.initialQuota });
|
|
15664
16950
|
this.environment = environment;
|
|
15665
16951
|
this.sessionDataRoutePrefix = options.sessionDataRoutePrefix || "/orchestrator/ws/sessions";
|
|
16952
|
+
this.sessionDataHeaders = options.sessionDataHeaders || {};
|
|
16953
|
+
this.setFeedListTransport(
|
|
16954
|
+
(feedOptions) => this.sessionDataRequest("/feed", feedOptions)
|
|
16955
|
+
);
|
|
16956
|
+
}
|
|
16957
|
+
/**
|
|
16958
|
+
* Keep the browser session transport observable from the client side.
|
|
16959
|
+
* A worker/proxy restart can leave a browser WebSocket appearing OPEN even
|
|
16960
|
+
* though the server-side Durable Object has already closed its peer. The
|
|
16961
|
+
* heartbeat gives the SDK a bounded failure signal so it can revoke that
|
|
16962
|
+
* stale socket and use WSClient's normal reconnect path.
|
|
16963
|
+
*/
|
|
16964
|
+
startHeartbeat() {
|
|
16965
|
+
if (this.heartbeatTimer) return;
|
|
16966
|
+
this.heartbeatTimer = setInterval(() => {
|
|
16967
|
+
if (this.heartbeatInFlight) return;
|
|
16968
|
+
this.heartbeatInFlight = true;
|
|
16969
|
+
void this.client.call("client.heartbeat", {}).then((result) => {
|
|
16970
|
+
if (result?.graphContainerStatus) {
|
|
16971
|
+
this.graphContainerStatus = result.graphContainerStatus;
|
|
16972
|
+
}
|
|
16973
|
+
}).catch((error) => {
|
|
16974
|
+
this.client.reportTransportFailure(error);
|
|
16975
|
+
}).finally(() => {
|
|
16976
|
+
this.heartbeatInFlight = false;
|
|
16977
|
+
});
|
|
16978
|
+
}, 5e3);
|
|
16979
|
+
this.heartbeatTimer.unref?.();
|
|
16980
|
+
}
|
|
16981
|
+
stopHeartbeat() {
|
|
16982
|
+
if (this.heartbeatTimer) {
|
|
16983
|
+
clearInterval(this.heartbeatTimer);
|
|
16984
|
+
this.heartbeatTimer = null;
|
|
16985
|
+
}
|
|
16986
|
+
this.heartbeatInFlight = false;
|
|
16987
|
+
}
|
|
16988
|
+
async hello() {
|
|
16989
|
+
const result = await super.hello();
|
|
16990
|
+
this.startHeartbeat();
|
|
16991
|
+
return result;
|
|
15666
16992
|
}
|
|
15667
16993
|
get environmentId() {
|
|
15668
16994
|
return this.environment.environmentId;
|
|
@@ -15722,6 +17048,11 @@ var EnvironmentSession = class extends Session {
|
|
|
15722
17048
|
const response = await fetch(url, {
|
|
15723
17049
|
method: init2.method || "GET",
|
|
15724
17050
|
headers,
|
|
17051
|
+
// Session documents, feed pages, and artifact reads are live
|
|
17052
|
+
// Automerge-backed state. A browser cache entry for the identical
|
|
17053
|
+
// artifact URL can otherwise make the Dock poll the same stale
|
|
17054
|
+
// `running` record until a full reload revalidates it.
|
|
17055
|
+
cache: "no-store",
|
|
15725
17056
|
...typeof init2.body === "undefined" ? {} : { body: init2.body }
|
|
15726
17057
|
});
|
|
15727
17058
|
if (response.ok) {
|
|
@@ -15750,34 +17081,50 @@ var EnvironmentSession = class extends Session {
|
|
|
15750
17081
|
const body = typeof init2.body === "undefined" ? void 0 : JSON.stringify(init2.body);
|
|
15751
17082
|
const response = await this.sessionDataFetch(path, query, {
|
|
15752
17083
|
method: init2.method || "GET",
|
|
15753
|
-
headers: {
|
|
17084
|
+
headers: {
|
|
17085
|
+
...this.sessionDataHeaders,
|
|
17086
|
+
"Content-Type": "application/json"
|
|
17087
|
+
},
|
|
15754
17088
|
...typeof body === "undefined" ? {} : { body }
|
|
15755
17089
|
});
|
|
15756
17090
|
return response.json();
|
|
15757
17091
|
}
|
|
15758
|
-
async collectAllSessionItems(listPage) {
|
|
15759
|
-
const items = [];
|
|
15760
|
-
let cursor = null;
|
|
15761
|
-
do {
|
|
15762
|
-
const page = await listPage({ limit: 500, cursor });
|
|
15763
|
-
items.push(...page.items);
|
|
15764
|
-
cursor = page.nextCursor;
|
|
15765
|
-
} while (cursor);
|
|
15766
|
-
return items;
|
|
15767
|
-
}
|
|
15768
17092
|
/**
|
|
15769
17093
|
* Fetch the live session document from the runtime DO.
|
|
15770
17094
|
*
|
|
15771
|
-
* For history
|
|
15772
|
-
*
|
|
17095
|
+
* For presentation history use `feed.list()` or `transcript.list()`.
|
|
17096
|
+
* Timeline and job collections are diagnostic/execution data only.
|
|
15773
17097
|
*/
|
|
15774
17098
|
async getDocument() {
|
|
15775
17099
|
return this.sessionDataRequest("/document");
|
|
15776
17100
|
}
|
|
15777
|
-
|
|
15778
|
-
|
|
15779
|
-
|
|
15780
|
-
|
|
17101
|
+
/**
|
|
17102
|
+
* Publish one terminal assistant reply from a trusted server integration.
|
|
17103
|
+
*
|
|
17104
|
+
* This uses the API-key-authenticated HTTP session boundary. Delegated
|
|
17105
|
+
* browser sessions cannot use it and never receive assistant feed-authoring
|
|
17106
|
+
* capability through their WebSocket.
|
|
17107
|
+
*/
|
|
17108
|
+
async publishAssistantReply(input) {
|
|
17109
|
+
if (this.sessionDataRoutePrefix === "/sdk/browser-sessions") {
|
|
17110
|
+
throw new Error(
|
|
17111
|
+
"Assistant replies require a server API-key session connection."
|
|
17112
|
+
);
|
|
17113
|
+
}
|
|
17114
|
+
const id = requiredAssistantReplyString(input.id, "id");
|
|
17115
|
+
const operationId2 = requiredAssistantReplyString(
|
|
17116
|
+
input.operationId,
|
|
17117
|
+
"operationId"
|
|
17118
|
+
);
|
|
17119
|
+
const text = requiredAssistantReplyString(input.text, "text");
|
|
17120
|
+
return this.sessionDataRequest(
|
|
17121
|
+
"/assistant-replies",
|
|
17122
|
+
void 0,
|
|
17123
|
+
{
|
|
17124
|
+
method: "POST",
|
|
17125
|
+
body: { id, operationId: operationId2, text }
|
|
17126
|
+
}
|
|
17127
|
+
);
|
|
15781
17128
|
}
|
|
15782
17129
|
get timeline() {
|
|
15783
17130
|
return {
|
|
@@ -15819,7 +17166,12 @@ var EnvironmentSession = class extends Session {
|
|
|
15819
17166
|
latestJob: true
|
|
15820
17167
|
}),
|
|
15821
17168
|
get: (artifactId) => this.sessionDataRequest(
|
|
15822
|
-
`/artifacts/${encodeURIComponent(artifactId)}
|
|
17169
|
+
`/artifacts/${encodeURIComponent(artifactId)}`,
|
|
17170
|
+
// The artifact endpoint is polled while an effect is running. Keep
|
|
17171
|
+
// each read addressable as a fresh resource as well as using
|
|
17172
|
+
// `cache: no-store`; this also bypasses intermediaries that ignore
|
|
17173
|
+
// the Fetch cache directive for an otherwise identical GET URL.
|
|
17174
|
+
{ _granularLiveRead: Date.now() }
|
|
15823
17175
|
),
|
|
15824
17176
|
create: (artifact) => this.sessionDataRequest(
|
|
15825
17177
|
"/artifacts",
|
|
@@ -15829,6 +17181,14 @@ var EnvironmentSession = class extends Session {
|
|
|
15829
17181
|
body: artifact
|
|
15830
17182
|
}
|
|
15831
17183
|
),
|
|
17184
|
+
acceptSuggestion: (sequence) => this.sessionDataRequest(
|
|
17185
|
+
"/artifacts/suggestions/accept",
|
|
17186
|
+
void 0,
|
|
17187
|
+
{
|
|
17188
|
+
method: "POST",
|
|
17189
|
+
body: { sequence }
|
|
17190
|
+
}
|
|
17191
|
+
),
|
|
15832
17192
|
updateInputs: (artifactId, patch) => this.sessionDataRequest(
|
|
15833
17193
|
`/artifacts/${encodeURIComponent(artifactId)}`,
|
|
15834
17194
|
void 0,
|
|
@@ -15837,6 +17197,16 @@ var EnvironmentSession = class extends Session {
|
|
|
15837
17197
|
body: patch
|
|
15838
17198
|
}
|
|
15839
17199
|
),
|
|
17200
|
+
relationshipOptions: (artifactId, input) => this.sessionDataRequest(
|
|
17201
|
+
`/artifacts/${encodeURIComponent(artifactId)}/relationship-options`,
|
|
17202
|
+
void 0,
|
|
17203
|
+
{ method: "POST", body: input }
|
|
17204
|
+
),
|
|
17205
|
+
relationshipCreate: (artifactId, input) => this.sessionDataRequest(
|
|
17206
|
+
`/artifacts/${encodeURIComponent(artifactId)}/relationship-create`,
|
|
17207
|
+
void 0,
|
|
17208
|
+
{ method: "POST", body: input }
|
|
17209
|
+
),
|
|
15840
17210
|
validate: (artifactId) => this.sessionDataRequest(
|
|
15841
17211
|
`/artifacts/${encodeURIComponent(artifactId)}/validate`,
|
|
15842
17212
|
void 0,
|
|
@@ -15974,73 +17344,50 @@ var EnvironmentSession = class extends Session {
|
|
|
15974
17344
|
get transcript() {
|
|
15975
17345
|
return {
|
|
15976
17346
|
list: async (options = {}) => {
|
|
15977
|
-
|
|
15978
|
-
|
|
15979
|
-
|
|
15980
|
-
|
|
15981
|
-
|
|
15982
|
-
|
|
15983
|
-
|
|
15984
|
-
|
|
15985
|
-
|
|
15986
|
-
)
|
|
15987
|
-
|
|
15988
|
-
|
|
15989
|
-
|
|
15990
|
-
jobs: {
|
|
15991
|
-
byId: Object.fromEntries(
|
|
15992
|
-
jobs.map((job) => {
|
|
15993
|
-
const record = job && typeof job === "object" ? job : null;
|
|
15994
|
-
const id = typeof record?.jobId === "string" ? record.jobId : typeof record?.id === "string" ? record.id : null;
|
|
15995
|
-
return id ? [id, record] : null;
|
|
15996
|
-
}).filter(
|
|
15997
|
-
(entry) => Boolean(entry)
|
|
15998
|
-
)
|
|
15999
|
-
)
|
|
16000
|
-
},
|
|
16001
|
-
artifacts: {
|
|
16002
|
-
byId: Object.fromEntries(
|
|
16003
|
-
artifacts.map((artifact) => {
|
|
16004
|
-
return artifact?.artifactId ? [
|
|
16005
|
-
artifact.artifactId,
|
|
16006
|
-
artifact
|
|
16007
|
-
] : null;
|
|
16008
|
-
}).filter(
|
|
16009
|
-
(entry) => Boolean(entry)
|
|
16010
|
-
)
|
|
16011
|
-
),
|
|
16012
|
-
order: artifacts.map((artifact) => artifact?.artifactId).filter(
|
|
16013
|
-
(artifactId) => Boolean(artifactId)
|
|
16014
|
-
)
|
|
17347
|
+
if (!isCanonicalSessionFeedDocument(this.document)) {
|
|
17348
|
+
throw new Error(
|
|
17349
|
+
"Canonical session feed-v1 is required; transcript.list() does not reconstruct past message or job collections."
|
|
17350
|
+
);
|
|
17351
|
+
}
|
|
17352
|
+
const canonicalFeedItems = [];
|
|
17353
|
+
let afterSequence = 0;
|
|
17354
|
+
let pageCount = 0;
|
|
17355
|
+
for (; ; ) {
|
|
17356
|
+
if (++pageCount > 1e4) {
|
|
17357
|
+
throw new Error(
|
|
17358
|
+
"Canonical transcript history exceeded its page limit."
|
|
17359
|
+
);
|
|
16015
17360
|
}
|
|
16016
|
-
|
|
16017
|
-
|
|
16018
|
-
|
|
16019
|
-
|
|
16020
|
-
|
|
16021
|
-
|
|
16022
|
-
|
|
16023
|
-
|
|
16024
|
-
|
|
16025
|
-
|
|
16026
|
-
|
|
16027
|
-
|
|
16028
|
-
|
|
16029
|
-
|
|
16030
|
-
|
|
16031
|
-
|
|
16032
|
-
|
|
16033
|
-
|
|
16034
|
-
|
|
17361
|
+
const page = await this.feed.list({
|
|
17362
|
+
afterSequence,
|
|
17363
|
+
limit: 500
|
|
17364
|
+
});
|
|
17365
|
+
if (page.items.length === 0) {
|
|
17366
|
+
if (page.hasMoreAfter) {
|
|
17367
|
+
throw new Error(
|
|
17368
|
+
`Canonical transcript history stopped after sequence ${afterSequence}.`
|
|
17369
|
+
);
|
|
17370
|
+
}
|
|
17371
|
+
break;
|
|
17372
|
+
}
|
|
17373
|
+
if (page.items[0].sequence !== afterSequence + 1) {
|
|
17374
|
+
throw new Error(
|
|
17375
|
+
`Canonical transcript history has a gap after sequence ${afterSequence}.`
|
|
17376
|
+
);
|
|
17377
|
+
}
|
|
17378
|
+
canonicalFeedItems.push(...page.items);
|
|
17379
|
+
afterSequence = page.items[page.items.length - 1].sequence;
|
|
17380
|
+
if (!page.hasMoreAfter) break;
|
|
17381
|
+
}
|
|
16035
17382
|
const allItems = buildSessionTranscript({
|
|
16036
|
-
liveDoc,
|
|
16037
|
-
|
|
17383
|
+
liveDoc: this.document,
|
|
17384
|
+
canonicalFeedItems
|
|
16038
17385
|
});
|
|
16039
17386
|
const limit = Math.max(
|
|
16040
17387
|
1,
|
|
16041
17388
|
Math.min(500, Math.floor(options.limit ?? 100))
|
|
16042
17389
|
);
|
|
16043
|
-
const offset = typeof options.cursor === "string" ? Number.parseInt(options.cursor, 10) || 0 : 0;
|
|
17390
|
+
const offset = options.latest ? Math.max(0, allItems.length - limit) : typeof options.cursor === "string" ? Number.parseInt(options.cursor, 10) || 0 : 0;
|
|
16044
17391
|
const items = allItems.slice(offset, offset + limit);
|
|
16045
17392
|
const nextOffset = offset + items.length;
|
|
16046
17393
|
return {
|
|
@@ -16126,6 +17473,8 @@ var EnvironmentSession = class extends Session {
|
|
|
16126
17473
|
* acknowledgement was observed.
|
|
16127
17474
|
*/
|
|
16128
17475
|
async disconnect() {
|
|
17476
|
+
this.stopHeartbeat();
|
|
17477
|
+
this.disposeSessionFeed();
|
|
16129
17478
|
let wsNotifiedRuntime = false;
|
|
16130
17479
|
try {
|
|
16131
17480
|
const goodbye = await this.rpc(
|
|
@@ -16164,6 +17513,7 @@ var EnvironmentSession = class extends Session {
|
|
|
16164
17513
|
* Close only the socket transport without sending `client.goodbye`.
|
|
16165
17514
|
*/
|
|
16166
17515
|
disconnectTransport() {
|
|
17516
|
+
this.stopHeartbeat();
|
|
16167
17517
|
this.client.disconnect({ reason: "Transport detach" });
|
|
16168
17518
|
}
|
|
16169
17519
|
/**
|
|
@@ -16928,16 +18278,22 @@ var Granular = class _Granular {
|
|
|
16928
18278
|
return this.normalizeUserEnvironmentState(state);
|
|
16929
18279
|
}
|
|
16930
18280
|
async markUserEnvironmentSessionsRead(options) {
|
|
18281
|
+
const readThroughSequence = requireUserEnvironmentSequence(
|
|
18282
|
+
options.readThroughSequence,
|
|
18283
|
+
"readThroughSequence"
|
|
18284
|
+
);
|
|
16931
18285
|
const result = await this.request("/sdk/user-environment-state/read", {
|
|
16932
18286
|
method: "POST",
|
|
16933
18287
|
body: JSON.stringify({
|
|
16934
18288
|
environmentId: options.environmentId,
|
|
16935
18289
|
sessionId: options.sessionId,
|
|
16936
18290
|
sessionIds: options.sessionIds,
|
|
16937
|
-
|
|
18291
|
+
readThroughSequence
|
|
16938
18292
|
})
|
|
16939
18293
|
});
|
|
16940
|
-
return
|
|
18294
|
+
return normalizeReadThroughSequenceMap(
|
|
18295
|
+
result.readThroughSequenceBySessionId
|
|
18296
|
+
);
|
|
16941
18297
|
}
|
|
16942
18298
|
normalizeConversationSession(row) {
|
|
16943
18299
|
const sessionId = String(row.sessionId ?? row.session_id ?? "");
|
|
@@ -16962,21 +18318,48 @@ var Granular = class _Granular {
|
|
|
16962
18318
|
};
|
|
16963
18319
|
}
|
|
16964
18320
|
normalizeUserEnvironmentState(state) {
|
|
18321
|
+
if (!Array.isArray(state.sessions)) {
|
|
18322
|
+
throw new Error(
|
|
18323
|
+
"Invalid user-environment state: sessions must be an array."
|
|
18324
|
+
);
|
|
18325
|
+
}
|
|
18326
|
+
const sessions = state.sessions.map((item, index) => ({
|
|
18327
|
+
...item,
|
|
18328
|
+
readThroughSequence: requireUserEnvironmentSequence(
|
|
18329
|
+
item?.readThroughSequence,
|
|
18330
|
+
`sessions[${index}].readThroughSequence`
|
|
18331
|
+
),
|
|
18332
|
+
messagePreview: {
|
|
18333
|
+
...item.messagePreview,
|
|
18334
|
+
latestMessageSequence: requireUserEnvironmentSequence(
|
|
18335
|
+
item.messagePreview?.latestMessageSequence,
|
|
18336
|
+
`sessions[${index}].messagePreview.latestMessageSequence`
|
|
18337
|
+
),
|
|
18338
|
+
latestAssistantSequence: requireUserEnvironmentSequence(
|
|
18339
|
+
item.messagePreview?.latestAssistantSequence,
|
|
18340
|
+
`sessions[${index}].messagePreview.latestAssistantSequence`
|
|
18341
|
+
),
|
|
18342
|
+
unreadProducingSequence: requireUserEnvironmentSequence(
|
|
18343
|
+
item.messagePreview?.unreadProducingSequence,
|
|
18344
|
+
`sessions[${index}].messagePreview.unreadProducingSequence`
|
|
18345
|
+
)
|
|
18346
|
+
},
|
|
18347
|
+
session: this.normalizeConversationSession(
|
|
18348
|
+
item.session
|
|
18349
|
+
)
|
|
18350
|
+
}));
|
|
16965
18351
|
return {
|
|
16966
18352
|
...state,
|
|
16967
|
-
sessions
|
|
16968
|
-
...item,
|
|
16969
|
-
session: this.normalizeConversationSession(
|
|
16970
|
-
item.session
|
|
16971
|
-
)
|
|
16972
|
-
})) : [],
|
|
18353
|
+
sessions,
|
|
16973
18354
|
attention: {
|
|
16974
18355
|
prompts: Array.isArray(state.attention?.prompts) ? state.attention.prompts : [],
|
|
16975
18356
|
count: typeof state.attention?.count === "number" ? state.attention.count : 0,
|
|
16976
18357
|
activePrompt: state.attention?.activePrompt || null
|
|
16977
18358
|
},
|
|
16978
18359
|
unreadCount: typeof state.unreadCount === "number" ? state.unreadCount : 0,
|
|
16979
|
-
|
|
18360
|
+
readThroughSequenceBySessionId: normalizeReadThroughSequenceMap(
|
|
18361
|
+
state.readThroughSequenceBySessionId
|
|
18362
|
+
)
|
|
16980
18363
|
};
|
|
16981
18364
|
}
|
|
16982
18365
|
static coerceIsoDate(value) {
|
|
@@ -17007,7 +18390,9 @@ var Granular = class _Granular {
|
|
|
17007
18390
|
environmentId: options.environmentId,
|
|
17008
18391
|
clientId,
|
|
17009
18392
|
sessionScope,
|
|
17010
|
-
capabilities:
|
|
18393
|
+
capabilities: {
|
|
18394
|
+
...sessionScope ? { sessionScope } : {}
|
|
18395
|
+
},
|
|
17011
18396
|
initialHeap: options.initialHeap
|
|
17012
18397
|
})
|
|
17013
18398
|
});
|
|
@@ -17377,6 +18762,21 @@ var Granular = class _Granular {
|
|
|
17377
18762
|
reconnectError
|
|
17378
18763
|
);
|
|
17379
18764
|
console.error("[Granular] Original heartbeat failure:", error);
|
|
18765
|
+
if (this.onReconnectError) {
|
|
18766
|
+
try {
|
|
18767
|
+
this.onReconnectError({
|
|
18768
|
+
sessionId: `effect-host:${host.effectClientId}`,
|
|
18769
|
+
error: reconnectError instanceof Error ? reconnectError.message : String(reconnectError),
|
|
18770
|
+
timestamp: Date.now(),
|
|
18771
|
+
terminal: false
|
|
18772
|
+
});
|
|
18773
|
+
} catch (callbackError) {
|
|
18774
|
+
console.error(
|
|
18775
|
+
"[Granular] onReconnectError callback failed after effect-host recovery failure:",
|
|
18776
|
+
callbackError
|
|
18777
|
+
);
|
|
18778
|
+
}
|
|
18779
|
+
}
|
|
17380
18780
|
}
|
|
17381
18781
|
);
|
|
17382
18782
|
}
|
|
@@ -17479,7 +18879,13 @@ var Granular = class _Granular {
|
|
|
17479
18879
|
const request = params;
|
|
17480
18880
|
return invokeRegisteredEffect(
|
|
17481
18881
|
this.getSandboxEffectMap(sandboxId),
|
|
17482
|
-
request
|
|
18882
|
+
request,
|
|
18883
|
+
{
|
|
18884
|
+
feedback: {
|
|
18885
|
+
invocationId: request.callId,
|
|
18886
|
+
publish: (method, publishParams) => wsClient.call(method, publishParams)
|
|
18887
|
+
}
|
|
18888
|
+
}
|
|
17483
18889
|
);
|
|
17484
18890
|
});
|
|
17485
18891
|
wsClient.on("open", () => {
|
|
@@ -17493,14 +18899,20 @@ var Granular = class _Granular {
|
|
|
17493
18899
|
wsClient.on("disconnect", () => {
|
|
17494
18900
|
this.stopEffectHostHeartbeat(host);
|
|
17495
18901
|
});
|
|
17496
|
-
|
|
17497
|
-
|
|
17498
|
-
|
|
17499
|
-
|
|
17500
|
-
|
|
17501
|
-
|
|
17502
|
-
|
|
17503
|
-
|
|
18902
|
+
try {
|
|
18903
|
+
await withTimeout(
|
|
18904
|
+
wsClient.connect(),
|
|
18905
|
+
EFFECT_HOST_CONNECT_TIMEOUT_MS,
|
|
18906
|
+
`effect host WebSocket connect for sandbox ${sandboxId}`
|
|
18907
|
+
);
|
|
18908
|
+
await this.synchronizeEffectHost(host);
|
|
18909
|
+
this.sandboxEffectHosts.set(sandboxId, host);
|
|
18910
|
+
return host;
|
|
18911
|
+
} catch (error) {
|
|
18912
|
+
this.stopEffectHostHeartbeat(host);
|
|
18913
|
+
wsClient.disconnect({ reason: "Effect host initialization failed" });
|
|
18914
|
+
throw error;
|
|
18915
|
+
}
|
|
17504
18916
|
})();
|
|
17505
18917
|
this.sandboxEffectHostPromises.set(sandboxId, connectPromise);
|
|
17506
18918
|
try {
|
|
@@ -18377,7 +19789,7 @@ function asRecord4(value) {
|
|
|
18377
19789
|
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
18378
19790
|
return value;
|
|
18379
19791
|
}
|
|
18380
|
-
function
|
|
19792
|
+
function asArray(value) {
|
|
18381
19793
|
return Array.isArray(value) ? value : [];
|
|
18382
19794
|
}
|
|
18383
19795
|
function toSortedRecords(value) {
|
|
@@ -18552,14 +19964,30 @@ function hasNamedModuleImport(source, moduleName, name) {
|
|
|
18552
19964
|
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
18553
19965
|
const imports = source.matchAll(
|
|
18554
19966
|
new RegExp(
|
|
18555
|
-
`import\\s*\\{([
|
|
19967
|
+
`import\\s*\\{([^}]*)\\}\\s*from\\s*['"]${escapedModule}['"]`,
|
|
18556
19968
|
"g"
|
|
18557
19969
|
)
|
|
18558
19970
|
);
|
|
18559
19971
|
for (const match of imports) {
|
|
18560
19972
|
if (new RegExp(`\\b${escaped}\\b`).test(match[1])) return true;
|
|
18561
19973
|
}
|
|
18562
|
-
return false;
|
|
19974
|
+
return false;
|
|
19975
|
+
}
|
|
19976
|
+
function namedModuleImports(source, moduleName) {
|
|
19977
|
+
const escapedModule = moduleName.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
19978
|
+
const names = /* @__PURE__ */ new Set();
|
|
19979
|
+
for (const match of source.matchAll(
|
|
19980
|
+
new RegExp(
|
|
19981
|
+
`import\\s*\\{([^}]*)\\}\\s*from\\s*['"]${escapedModule}['"]`,
|
|
19982
|
+
"g"
|
|
19983
|
+
)
|
|
19984
|
+
)) {
|
|
19985
|
+
for (const specifier of match[1].split(",")) {
|
|
19986
|
+
const imported = specifier.trim().replace(/^type\s+/, "").split(/\s+as\s+/)[0]?.trim();
|
|
19987
|
+
if (imported) names.add(imported);
|
|
19988
|
+
}
|
|
19989
|
+
}
|
|
19990
|
+
return [...names];
|
|
18563
19991
|
}
|
|
18564
19992
|
function hasNamedAgentImport(source, name) {
|
|
18565
19993
|
return hasNamedModuleImport(source, HARNESS_V3_AGENT_MODULE, name);
|
|
@@ -18621,18 +20049,54 @@ function reviewGeneratedJobCode(code, _options = {}) {
|
|
|
18621
20049
|
message: "Generated jobs must not call process.exit(...). Return from the job or emit a runtime message instead."
|
|
18622
20050
|
});
|
|
18623
20051
|
}
|
|
20052
|
+
const supportedAgentExports = /* @__PURE__ */ new Set([
|
|
20053
|
+
"actions",
|
|
20054
|
+
"artifacts",
|
|
20055
|
+
"feedback",
|
|
20056
|
+
"formatBlockers",
|
|
20057
|
+
"relativeTime",
|
|
20058
|
+
"replyToUser",
|
|
20059
|
+
"showAgentResponse",
|
|
20060
|
+
"showObjects",
|
|
20061
|
+
"table",
|
|
20062
|
+
"transientFeedback"
|
|
20063
|
+
]);
|
|
20064
|
+
for (const imported of namedModuleImports(
|
|
20065
|
+
normalized,
|
|
20066
|
+
HARNESS_V3_AGENT_MODULE
|
|
20067
|
+
)) {
|
|
20068
|
+
if (!supportedAgentExports.has(imported)) {
|
|
20069
|
+
issues.push({
|
|
20070
|
+
code: "unknown_runtime_import",
|
|
20071
|
+
severity: "error",
|
|
20072
|
+
message: `Generated code imports unknown runtime value \`${imported}\` from ${HARNESS_V3_AGENT_MODULE}. Import only values listed in [Runtime Imports] and [Types].`
|
|
20073
|
+
});
|
|
20074
|
+
}
|
|
20075
|
+
}
|
|
20076
|
+
const removedAgentHelpers = [
|
|
20077
|
+
["agent", "text", "message"].join("_"),
|
|
20078
|
+
["agent", "heap", "objects"].join("_"),
|
|
20079
|
+
["agent", "message"].join("_")
|
|
20080
|
+
];
|
|
20081
|
+
for (const name of removedAgentHelpers) {
|
|
20082
|
+
const escaped = name.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
|
|
20083
|
+
if (new RegExp(`\\b${escaped}\\s*\\(`).test(normalized)) {
|
|
20084
|
+
issues.push({
|
|
20085
|
+
code: "unsupported_runtime_helper",
|
|
20086
|
+
severity: "error",
|
|
20087
|
+
message: `Generated code calls removed runtime helper \`${name}\`. Import and call only the canonical feed helpers listed for ${HARNESS_V3_AGENT_MODULE}.`
|
|
20088
|
+
});
|
|
20089
|
+
}
|
|
20090
|
+
}
|
|
18624
20091
|
for (const [name, replacement, pattern] of [
|
|
18625
|
-
["agent_text_message", "replyToUser", /\bagent_text_message\s*\(/],
|
|
18626
|
-
["agent_heap_objects", "showObjects", /\bagent_heap_objects\s*\(/],
|
|
18627
|
-
["agent_message", "showAgentResponse", /\bagent_message\s*\(/],
|
|
18628
20092
|
["heap", "groundedObjects", /\bheap\./],
|
|
18629
20093
|
["loop", "userInteraction or work", /\bloop\./]
|
|
18630
20094
|
]) {
|
|
18631
20095
|
if (pattern.test(normalized)) {
|
|
18632
20096
|
issues.push({
|
|
18633
|
-
code: "
|
|
20097
|
+
code: "unsupported_runtime_helper",
|
|
18634
20098
|
severity: "error",
|
|
18635
|
-
message: `Generated code uses
|
|
20099
|
+
message: `Generated code uses unsupported runtime helper \`${name}\`. Use \`${replacement}\` from the modules listed in [Runtime Imports].`
|
|
18636
20100
|
});
|
|
18637
20101
|
}
|
|
18638
20102
|
}
|
|
@@ -18801,116 +20265,7 @@ function normalizeActionSummaryForPrompt(line) {
|
|
|
18801
20265
|
}
|
|
18802
20266
|
function collectConversationReferents(liveDoc) {
|
|
18803
20267
|
const conversation = asRecord4(liveDoc?.conversation);
|
|
18804
|
-
|
|
18805
|
-
if (persistedReferents.length > 0) {
|
|
18806
|
-
return persistedReferents.slice().sort((left, right) => (right.ts || 0) - (left.ts || 0));
|
|
18807
|
-
}
|
|
18808
|
-
const heap = asRecord4(liveDoc?.heap);
|
|
18809
|
-
const entriesByPath = asRecord4(heap?.entriesByPath) || {};
|
|
18810
|
-
const listsByName = asRecord4(heap?.listsByName) || {};
|
|
18811
|
-
const variablesByName = asRecord4(heap?.variablesByName) || {};
|
|
18812
|
-
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));
|
|
18813
|
-
const referents = [];
|
|
18814
|
-
const seen = /* @__PURE__ */ new Set();
|
|
18815
|
-
const pushReferent = (referent) => {
|
|
18816
|
-
if (!referent?.kind || !referent.ref) return;
|
|
18817
|
-
const key = `${referent.kind}:${referent.ref}`;
|
|
18818
|
-
if (seen.has(key)) return;
|
|
18819
|
-
seen.add(key);
|
|
18820
|
-
referents.push(referent);
|
|
18821
|
-
};
|
|
18822
|
-
for (const message of messages) {
|
|
18823
|
-
if (message.role !== "assistant") continue;
|
|
18824
|
-
const show = asRecord4(message.show);
|
|
18825
|
-
if (!show) continue;
|
|
18826
|
-
const ts = Number(message.ts) || 0;
|
|
18827
|
-
const messageId = typeof message.id === "string" ? message.id : void 0;
|
|
18828
|
-
const jobId = typeof message.jobId === "string" ? message.jobId : void 0;
|
|
18829
|
-
const entryPaths = uniqueStrings(asArray2(show.entryPaths));
|
|
18830
|
-
const entryClassCounts = /* @__PURE__ */ new Map();
|
|
18831
|
-
const entryMetadata = entryPaths.map((entryPath) => {
|
|
18832
|
-
const entry = asRecord4(entriesByPath[entryPath]);
|
|
18833
|
-
const className = typeof entry?.className === "string" ? entry.className : void 0;
|
|
18834
|
-
if (className) {
|
|
18835
|
-
entryClassCounts.set(
|
|
18836
|
-
className,
|
|
18837
|
-
(entryClassCounts.get(className) || 0) + 1
|
|
18838
|
-
);
|
|
18839
|
-
}
|
|
18840
|
-
return { entryPath, entry, className };
|
|
18841
|
-
});
|
|
18842
|
-
const displayGroupId = entryMetadata.length > 1 ? `message:${messageId || jobId || ts}:entries` : void 0;
|
|
18843
|
-
for (const [
|
|
18844
|
-
index,
|
|
18845
|
-
{ entryPath, entry, className }
|
|
18846
|
-
] of entryMetadata.entries()) {
|
|
18847
|
-
pushReferent({
|
|
18848
|
-
id: `entry:${entryPath}`,
|
|
18849
|
-
kind: "entry",
|
|
18850
|
-
ref: entryPath,
|
|
18851
|
-
role: "assistant",
|
|
18852
|
-
source: "heap_objects",
|
|
18853
|
-
entryPath,
|
|
18854
|
-
recordId: typeof entry?.id === "string" ? entry.id : void 0,
|
|
18855
|
-
className,
|
|
18856
|
-
label: typeof entry?.label === "string" && entry.label.trim() ? entry.label.trim() : typeof entry?.id === "string" && entry.id.trim() ? entry.id.trim() : entryPath,
|
|
18857
|
-
...displayGroupId ? {
|
|
18858
|
-
displayGroupId,
|
|
18859
|
-
displayGroupIndex: index,
|
|
18860
|
-
displayGroupSize: entryMetadata.length,
|
|
18861
|
-
...className && (entryClassCounts.get(className) || 0) > 1 ? { displayGroupSameTypeSize: entryClassCounts.get(className) } : {}
|
|
18862
|
-
} : {},
|
|
18863
|
-
messageId,
|
|
18864
|
-
jobId,
|
|
18865
|
-
ts
|
|
18866
|
-
});
|
|
18867
|
-
}
|
|
18868
|
-
for (const listName of uniqueStrings(asArray2(show.listNames))) {
|
|
18869
|
-
const list = asRecord4(listsByName[listName]);
|
|
18870
|
-
pushReferent({
|
|
18871
|
-
id: `list:${listName}`,
|
|
18872
|
-
kind: "list",
|
|
18873
|
-
ref: listName,
|
|
18874
|
-
role: "assistant",
|
|
18875
|
-
source: "heap_objects",
|
|
18876
|
-
listName,
|
|
18877
|
-
className: typeof list?.className === "string" ? list.className : void 0,
|
|
18878
|
-
count: Array.isArray(list?.paths) ? list.paths.length : null,
|
|
18879
|
-
messageId,
|
|
18880
|
-
jobId,
|
|
18881
|
-
ts
|
|
18882
|
-
});
|
|
18883
|
-
}
|
|
18884
|
-
for (const variableName of uniqueStrings(
|
|
18885
|
-
asArray2(show.variableNames)
|
|
18886
|
-
)) {
|
|
18887
|
-
const variable = asRecord4(variablesByName[variableName]);
|
|
18888
|
-
const entryPath = typeof variable?.entryPath === "string" ? variable.entryPath : void 0;
|
|
18889
|
-
const listName = typeof variable?.listName === "string" ? variable.listName : void 0;
|
|
18890
|
-
const entry = entryPath ? asRecord4(entriesByPath[entryPath]) : null;
|
|
18891
|
-
const list = listName ? asRecord4(listsByName[listName]) : null;
|
|
18892
|
-
pushReferent({
|
|
18893
|
-
id: `variable:${variableName}`,
|
|
18894
|
-
kind: "variable",
|
|
18895
|
-
ref: variableName,
|
|
18896
|
-
role: "assistant",
|
|
18897
|
-
source: "heap_objects",
|
|
18898
|
-
variableName,
|
|
18899
|
-
variableKind: typeof variable?.kind === "string" ? variable.kind : void 0,
|
|
18900
|
-
entryPath,
|
|
18901
|
-
recordId: typeof entry?.id === "string" ? entry.id : void 0,
|
|
18902
|
-
listName,
|
|
18903
|
-
className: typeof variable?.className === "string" ? variable.className : typeof entry?.className === "string" ? entry.className : typeof list?.className === "string" ? list.className : void 0,
|
|
18904
|
-
label: typeof entry?.label === "string" && entry.label.trim() ? entry.label.trim() : typeof entry?.id === "string" && entry.id.trim() ? entry.id.trim() : null,
|
|
18905
|
-
count: variable?.kind === "list" && Array.isArray(list?.paths) ? list.paths.length : null,
|
|
18906
|
-
scalarValue: variable?.kind === "scalar" && (typeof variable.value === "string" || typeof variable.value === "number" || typeof variable.value === "boolean" || variable.value === null) ? variable.value : void 0,
|
|
18907
|
-
messageId,
|
|
18908
|
-
jobId,
|
|
18909
|
-
ts
|
|
18910
|
-
});
|
|
18911
|
-
}
|
|
18912
|
-
}
|
|
18913
|
-
return referents;
|
|
20268
|
+
return asArray(conversation?.referents).map((value) => asRecord4(value)).filter((value) => Boolean(value)).slice().sort((left, right) => (right.ts || 0) - (left.ts || 0));
|
|
18914
20269
|
}
|
|
18915
20270
|
function projectConversationReferentFocus(liveDoc) {
|
|
18916
20271
|
const heap = asRecord4(liveDoc?.heap);
|
|
@@ -18932,7 +20287,7 @@ function projectConversationReferentFocus(liveDoc) {
|
|
|
18932
20287
|
listCount += 1;
|
|
18933
20288
|
listNames.push(referent.listName);
|
|
18934
20289
|
const list = asRecord4(listsByName[referent.listName]);
|
|
18935
|
-
entryPaths.push(...
|
|
20290
|
+
entryPaths.push(...asArray(list?.paths).slice(0, 4));
|
|
18936
20291
|
continue;
|
|
18937
20292
|
}
|
|
18938
20293
|
if (referent.kind === "variable" && typeof referent.variableName === "string" && variableCount < 4) {
|
|
@@ -18944,7 +20299,7 @@ function projectConversationReferentFocus(liveDoc) {
|
|
|
18944
20299
|
if (typeof referent.listName === "string") {
|
|
18945
20300
|
listNames.push(referent.listName);
|
|
18946
20301
|
const list = asRecord4(listsByName[referent.listName]);
|
|
18947
|
-
entryPaths.push(...
|
|
20302
|
+
entryPaths.push(...asArray(list?.paths).slice(0, 4));
|
|
18948
20303
|
}
|
|
18949
20304
|
}
|
|
18950
20305
|
}
|
|
@@ -19122,12 +20477,12 @@ function projectWorkflowFocus(liveDoc, pendingPrompts = [], options) {
|
|
|
19122
20477
|
const openDecisionIds = [];
|
|
19123
20478
|
const openPromptIds = [];
|
|
19124
20479
|
for (const job of jobs) {
|
|
19125
|
-
for (const line of
|
|
20480
|
+
for (const line of asArray(job.actionSummary)) {
|
|
19126
20481
|
if (typeof line === "string" && line.trim()) {
|
|
19127
20482
|
actionSummaryLines.push(line.trim());
|
|
19128
20483
|
}
|
|
19129
20484
|
}
|
|
19130
|
-
for (const rawEvent of
|
|
20485
|
+
for (const rawEvent of asArray(job.actionTrace)) {
|
|
19131
20486
|
const event = asRecord4(rawEvent);
|
|
19132
20487
|
const details = asRecord4(event?.details);
|
|
19133
20488
|
const outcome = asRecord4(event?.outcome);
|
|
@@ -19224,7 +20579,7 @@ function projectWorkflowFocus(liveDoc, pendingPrompts = [], options) {
|
|
|
19224
20579
|
}
|
|
19225
20580
|
for (const listName of uniqueStrings(listNames)) {
|
|
19226
20581
|
const list = asRecord4(listsByName[listName]);
|
|
19227
|
-
for (const path of
|
|
20582
|
+
for (const path of asArray(list?.paths).slice(0, 4)) {
|
|
19228
20583
|
entryPaths.push(path);
|
|
19229
20584
|
}
|
|
19230
20585
|
}
|
|
@@ -19344,7 +20699,7 @@ function projectLoopSummary(liveDoc, pendingPrompts = [], options) {
|
|
|
19344
20699
|
id: typeof decision.decisionId === "string" ? decision.decisionId : "unknown",
|
|
19345
20700
|
title: typeof decision.title === "string" && decision.title.trim() ? decision.title.trim() : "Decision",
|
|
19346
20701
|
status,
|
|
19347
|
-
candidates: status === "open" ?
|
|
20702
|
+
candidates: status === "open" ? asArray(decision.candidates).slice(0, 5).map((candidate) => {
|
|
19348
20703
|
const record = asRecord4(candidate);
|
|
19349
20704
|
if (!record) return null;
|
|
19350
20705
|
return {
|
|
@@ -19502,7 +20857,7 @@ function projectHeapSummary(heap, options) {
|
|
|
19502
20857
|
type: entry.className || "unknown",
|
|
19503
20858
|
id: entry.id || null,
|
|
19504
20859
|
label: entry.label || entry.id || null,
|
|
19505
|
-
fields:
|
|
20860
|
+
fields: asArray(entry.fields).filter(
|
|
19506
20861
|
(field) => field?.name && field.name !== "_realId" && field.name !== "real_id"
|
|
19507
20862
|
).slice(0, 3).map((field) => ({
|
|
19508
20863
|
name: field.name,
|
|
@@ -19666,7 +21021,7 @@ function buildGranularAgentManualActionBlock(manualActionSummary) {
|
|
|
19666
21021
|
function projectSessionFileSummary(liveDoc) {
|
|
19667
21022
|
const files = asRecord4(liveDoc?.files);
|
|
19668
21023
|
const byId = asRecord4(files?.byId) || {};
|
|
19669
|
-
const order =
|
|
21024
|
+
const order = asArray(files?.order);
|
|
19670
21025
|
const items = order.map((fileId) => asRecord4(byId[fileId])).filter((file) => Boolean(file)).filter((file) => file.status !== "deleted").slice(0, 24).map((file) => ({
|
|
19671
21026
|
fileId: typeof file.fileId === "string" ? file.fileId : null,
|
|
19672
21027
|
filename: typeof file.filename === "string" ? file.filename : typeof file.safeFilename === "string" ? file.safeFilename : null,
|
|
@@ -19704,13 +21059,7 @@ function extractRuntimeContractExports(domainBlock) {
|
|
|
19704
21059
|
}
|
|
19705
21060
|
const actionPattern = /export\s+declare\s+function\s+([A-Za-z_$][\w$]*)/g;
|
|
19706
21061
|
for (const match of domainBlock.matchAll(actionPattern)) {
|
|
19707
|
-
|
|
19708
|
-
if (["agent_text_message", "agent_heap_objects", "agent_message"].includes(
|
|
19709
|
-
name
|
|
19710
|
-
)) {
|
|
19711
|
-
continue;
|
|
19712
|
-
}
|
|
19713
|
-
actions.add(name);
|
|
21062
|
+
actions.add(match[1]);
|
|
19714
21063
|
}
|
|
19715
21064
|
return {
|
|
19716
21065
|
classes: Array.from(classes).sort(),
|
|
@@ -19767,7 +21116,7 @@ function buildGranularAgentRuntimeImportsBlock(input) {
|
|
|
19767
21116
|
importStyle: "named ESM imports only",
|
|
19768
21117
|
exports: ["replyToUser", "showObjects", "showAgentResponse"],
|
|
19769
21118
|
contains: "User-facing Harness response helpers for text, grounded object displays, and combined responses.",
|
|
19770
|
-
rule: "Import reply/display helpers from this module;
|
|
21119
|
+
rule: "Import reply/display helpers from this module; only the listed exports are available."
|
|
19771
21120
|
},
|
|
19772
21121
|
[HARNESS_V3_SESSION_MODULE]: {
|
|
19773
21122
|
importStyle: "named ESM imports only",
|
|
@@ -20008,7 +21357,7 @@ function summarizeObjectSchema(schema) {
|
|
|
20008
21357
|
if (!properties || Object.keys(properties).length === 0) {
|
|
20009
21358
|
return record ? "{}" : null;
|
|
20010
21359
|
}
|
|
20011
|
-
const required = new Set(
|
|
21360
|
+
const required = new Set(asArray(record?.required));
|
|
20012
21361
|
const fields = Object.entries(properties).slice(0, 8).map(([name, property]) => {
|
|
20013
21362
|
const marker = required.has(name) ? "*" : "?";
|
|
20014
21363
|
return `${name}${marker}: ${jsonSchemaTypeName(property)}`;
|
|
@@ -20045,9 +21394,9 @@ function splitDomainDocumentation(domainDocumentation) {
|
|
|
20045
21394
|
return { types: normalized, docs: "" };
|
|
20046
21395
|
}
|
|
20047
21396
|
var DOMAIN_HELPER_FUNCTION_NAMES = /* @__PURE__ */ new Set([
|
|
20048
|
-
"
|
|
20049
|
-
"
|
|
20050
|
-
"
|
|
21397
|
+
"replyToUser",
|
|
21398
|
+
"showAgentResponse",
|
|
21399
|
+
"showObjects"
|
|
20051
21400
|
]);
|
|
20052
21401
|
function inferGlobalActionToolsFromDomainTypes(domainTypes) {
|
|
20053
21402
|
const inferred = [];
|
|
@@ -20183,7 +21532,7 @@ function buildKnownFactsFromCheckpoint(checkpoint) {
|
|
|
20183
21532
|
return facts.slice(0, 8);
|
|
20184
21533
|
}
|
|
20185
21534
|
function buildGranularAgentSystemPrompt(input) {
|
|
20186
|
-
const outputMode = input.outputMode || "
|
|
21535
|
+
const outputMode = input.outputMode || "feed";
|
|
20187
21536
|
const promptCapabilities = resolvePromptCapabilities(input.capabilities);
|
|
20188
21537
|
const domainSections = splitDomainDocumentation(input.domainDocumentation);
|
|
20189
21538
|
const promptTools = resolvePromptTools(input.tools, domainSections.types);
|
|
@@ -20216,7 +21565,7 @@ function buildGranularAgentSystemPrompt(input) {
|
|
|
20216
21565
|
- For multi-record display, prefer a saved list/listName so the UI can render a table; use entryPaths for a few individual records.
|
|
20217
21566
|
- 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.
|
|
20218
21567
|
- 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.
|
|
20219
|
-
- Use \`replyToUser(...)\`, \`showObjects(...)\`, or \`showAgentResponse(...)\` from \`@granular/agent\` when the host exposes job output helpers
|
|
21568
|
+
- 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\`.
|
|
20220
21569
|
- \`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.
|
|
20221
21570
|
- 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.
|
|
20222
21571
|
- 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.
|
|
@@ -20226,11 +21575,10 @@ function buildGranularAgentSystemPrompt(input) {
|
|
|
20226
21575
|
- 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.
|
|
20227
21576
|
- 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()\`.
|
|
20228
21577
|
- 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.
|
|
20229
|
-
- 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.
|
|
20230
21578
|
- 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.
|
|
20231
21579
|
- 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.
|
|
20232
21580
|
- 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.
|
|
20233
|
-
- Do not rely on the final return value for UI output. Do not return ad-hoc \`reply\` / \`show\` payloads instead of explicit
|
|
21581
|
+
- 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\`.
|
|
20234
21582
|
- \`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.
|
|
20235
21583
|
- 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.
|
|
20236
21584
|
- 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.
|
|
@@ -20730,6 +22078,231 @@ function buildContinuationInstructionFromTemplate(resultPreview, options) {
|
|
|
20730
22078
|
return renderContinuationInstructionFromTemplate(resultPreview, options).instruction;
|
|
20731
22079
|
}
|
|
20732
22080
|
|
|
22081
|
+
// src/job-presentation.ts
|
|
22082
|
+
var ENTRY_KEY_CANDIDATES = ["entryPath", "path"];
|
|
22083
|
+
var ENTRY_ARRAY_KEY_CANDIDATES = ["entryPaths", "paths"];
|
|
22084
|
+
var LIST_KEY_CANDIDATES = ["listName"];
|
|
22085
|
+
var LIST_ARRAY_KEY_CANDIDATES = ["listNames"];
|
|
22086
|
+
var VARIABLE_KEY_CANDIDATES = ["variableName"];
|
|
22087
|
+
var VARIABLE_ARRAY_KEY_CANDIDATES = ["variableNames"];
|
|
22088
|
+
var UI_CONTAINER_KEYS = ["show", "display", "present", "ui"];
|
|
22089
|
+
function asRecord5(value) {
|
|
22090
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return null;
|
|
22091
|
+
return value;
|
|
22092
|
+
}
|
|
22093
|
+
function responseTextFromFeedItems(feedItems, jobId) {
|
|
22094
|
+
let latestSequence = -1;
|
|
22095
|
+
let latestText = null;
|
|
22096
|
+
for (const item of feedItems) {
|
|
22097
|
+
if (item.kind !== "message" || item.payload.role !== "assistant" || item.source?.jobId !== jobId || item.sequence <= latestSequence) {
|
|
22098
|
+
continue;
|
|
22099
|
+
}
|
|
22100
|
+
const text = item.payload.text.trim();
|
|
22101
|
+
if (!text) continue;
|
|
22102
|
+
latestSequence = item.sequence;
|
|
22103
|
+
latestText = text;
|
|
22104
|
+
}
|
|
22105
|
+
return latestText;
|
|
22106
|
+
}
|
|
22107
|
+
function pushString(target, value) {
|
|
22108
|
+
if (typeof value === "string" && value.trim()) {
|
|
22109
|
+
target.add(value.trim());
|
|
22110
|
+
}
|
|
22111
|
+
}
|
|
22112
|
+
function pushStringArray(target, value) {
|
|
22113
|
+
if (!Array.isArray(value)) return;
|
|
22114
|
+
for (const item of value) {
|
|
22115
|
+
pushString(target, item);
|
|
22116
|
+
}
|
|
22117
|
+
}
|
|
22118
|
+
function collectReferencesFromRecord(record, refs) {
|
|
22119
|
+
for (const key of ENTRY_KEY_CANDIDATES)
|
|
22120
|
+
pushString(refs.entryPaths, record[key]);
|
|
22121
|
+
for (const key of ENTRY_ARRAY_KEY_CANDIDATES)
|
|
22122
|
+
pushStringArray(refs.entryPaths, record[key]);
|
|
22123
|
+
for (const key of LIST_KEY_CANDIDATES)
|
|
22124
|
+
pushString(refs.listNames, record[key]);
|
|
22125
|
+
for (const key of LIST_ARRAY_KEY_CANDIDATES)
|
|
22126
|
+
pushStringArray(refs.listNames, record[key]);
|
|
22127
|
+
for (const key of VARIABLE_KEY_CANDIDATES)
|
|
22128
|
+
pushString(refs.variableNames, record[key]);
|
|
22129
|
+
for (const key of VARIABLE_ARRAY_KEY_CANDIDATES)
|
|
22130
|
+
pushStringArray(refs.variableNames, record[key]);
|
|
22131
|
+
}
|
|
22132
|
+
function stringValue(record, keys) {
|
|
22133
|
+
for (const key of keys) {
|
|
22134
|
+
const value = record[key];
|
|
22135
|
+
if (typeof value === "string" && value.trim()) {
|
|
22136
|
+
return value.trim();
|
|
22137
|
+
}
|
|
22138
|
+
}
|
|
22139
|
+
return null;
|
|
22140
|
+
}
|
|
22141
|
+
function findEntryPathForRecord(record, heap) {
|
|
22142
|
+
const directPath = stringValue(record, ["entryPath", "path"]);
|
|
22143
|
+
if (directPath && heap.entriesByPath?.[directPath]) {
|
|
22144
|
+
return directPath;
|
|
22145
|
+
}
|
|
22146
|
+
const id = stringValue(record, ["id", "_id", "recordId", "objectId"]);
|
|
22147
|
+
if (!id) {
|
|
22148
|
+
return null;
|
|
22149
|
+
}
|
|
22150
|
+
const className = stringValue(record, [
|
|
22151
|
+
"className",
|
|
22152
|
+
"_className",
|
|
22153
|
+
"__className",
|
|
22154
|
+
"prototype",
|
|
22155
|
+
"type"
|
|
22156
|
+
]);
|
|
22157
|
+
const entries = Object.values(heap.entriesByPath || {});
|
|
22158
|
+
const exact = entries.find(
|
|
22159
|
+
(entry) => entry.id === id && (!className || entry.className === className || entry.prototypes?.includes(className))
|
|
22160
|
+
);
|
|
22161
|
+
if (exact?.path) {
|
|
22162
|
+
return exact.path;
|
|
22163
|
+
}
|
|
22164
|
+
const idOnlyMatches = entries.filter((entry) => entry.id === id);
|
|
22165
|
+
return idOnlyMatches.length === 1 ? idOnlyMatches[0].path : null;
|
|
22166
|
+
}
|
|
22167
|
+
function scanForHeapReferences(value, heap, refs, depth = 0, seen = /* @__PURE__ */ new Set()) {
|
|
22168
|
+
if (value === null || value === void 0 || depth > 4 || seen.has(value))
|
|
22169
|
+
return;
|
|
22170
|
+
if (typeof value === "string") {
|
|
22171
|
+
const trimmed = value.trim();
|
|
22172
|
+
if (heap.entriesByPath?.[trimmed]) refs.entryPaths.add(trimmed);
|
|
22173
|
+
if (heap.listsByName?.[trimmed]) refs.listNames.add(trimmed);
|
|
22174
|
+
if (heap.variablesByName?.[trimmed]) refs.variableNames.add(trimmed);
|
|
22175
|
+
return;
|
|
22176
|
+
}
|
|
22177
|
+
if (Array.isArray(value)) {
|
|
22178
|
+
seen.add(value);
|
|
22179
|
+
for (const item of value.slice(0, 24)) {
|
|
22180
|
+
scanForHeapReferences(item, heap, refs, depth + 1, seen);
|
|
22181
|
+
}
|
|
22182
|
+
return;
|
|
22183
|
+
}
|
|
22184
|
+
const record = asRecord5(value);
|
|
22185
|
+
if (!record) return;
|
|
22186
|
+
seen.add(value);
|
|
22187
|
+
const entryPath = findEntryPathForRecord(record, heap);
|
|
22188
|
+
if (entryPath) refs.entryPaths.add(entryPath);
|
|
22189
|
+
collectReferencesFromRecord(record, refs);
|
|
22190
|
+
for (const key of UI_CONTAINER_KEYS) {
|
|
22191
|
+
const nested = asRecord5(record[key]);
|
|
22192
|
+
if (nested) collectReferencesFromRecord(nested, refs);
|
|
22193
|
+
}
|
|
22194
|
+
for (const nested of Object.values(record).slice(0, 24)) {
|
|
22195
|
+
scanForHeapReferences(nested, heap, refs, depth + 1, seen);
|
|
22196
|
+
}
|
|
22197
|
+
}
|
|
22198
|
+
function resolveVariablesToReferences(variableNames, heap, refs) {
|
|
22199
|
+
for (const variableName of variableNames) {
|
|
22200
|
+
const variable = heap.variablesByName?.[variableName];
|
|
22201
|
+
if (!variable) continue;
|
|
22202
|
+
if (variable.kind === "entry" && variable.entryPath) {
|
|
22203
|
+
refs.entryPaths.add(variable.entryPath);
|
|
22204
|
+
}
|
|
22205
|
+
if (variable.kind === "list" && variable.listName) {
|
|
22206
|
+
refs.listNames.add(variable.listName);
|
|
22207
|
+
}
|
|
22208
|
+
}
|
|
22209
|
+
}
|
|
22210
|
+
function sortEntries(entries) {
|
|
22211
|
+
return [...entries].sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
22212
|
+
}
|
|
22213
|
+
function sortLists(lists) {
|
|
22214
|
+
return [...lists].sort((a, b) => (b.updatedAt || 0) - (a.updatedAt || 0));
|
|
22215
|
+
}
|
|
22216
|
+
function dedupeEntries(entries) {
|
|
22217
|
+
const seen = /* @__PURE__ */ new Set();
|
|
22218
|
+
const result = [];
|
|
22219
|
+
for (const entry of entries) {
|
|
22220
|
+
if (!entry?.path || seen.has(entry.path)) continue;
|
|
22221
|
+
seen.add(entry.path);
|
|
22222
|
+
result.push(entry);
|
|
22223
|
+
}
|
|
22224
|
+
return result;
|
|
22225
|
+
}
|
|
22226
|
+
function dedupeLists(lists) {
|
|
22227
|
+
const seen = /* @__PURE__ */ new Set();
|
|
22228
|
+
const result = [];
|
|
22229
|
+
for (const list of lists) {
|
|
22230
|
+
if (!list?.name || seen.has(list.name)) continue;
|
|
22231
|
+
seen.add(list.name);
|
|
22232
|
+
result.push(list);
|
|
22233
|
+
}
|
|
22234
|
+
return result;
|
|
22235
|
+
}
|
|
22236
|
+
function getJobRelatedEntries(heap, jobId) {
|
|
22237
|
+
return sortEntries(
|
|
22238
|
+
Object.values(heap.entriesByPath || {}).filter(
|
|
22239
|
+
(entry) => entry.relatedJobIds?.includes(jobId)
|
|
22240
|
+
)
|
|
22241
|
+
);
|
|
22242
|
+
}
|
|
22243
|
+
function getJobRelatedLists(heap, jobId) {
|
|
22244
|
+
return sortLists(
|
|
22245
|
+
Object.values(heap.listsByName || {}).filter(
|
|
22246
|
+
(list) => list.relatedJobIds?.includes(jobId)
|
|
22247
|
+
)
|
|
22248
|
+
);
|
|
22249
|
+
}
|
|
22250
|
+
function entriesFromLists(lists, heap) {
|
|
22251
|
+
const entries = [];
|
|
22252
|
+
for (const list of lists) {
|
|
22253
|
+
for (const path of list.paths || []) {
|
|
22254
|
+
const entry = heap.entriesByPath?.[path];
|
|
22255
|
+
if (entry) entries.push(entry);
|
|
22256
|
+
}
|
|
22257
|
+
}
|
|
22258
|
+
return entries;
|
|
22259
|
+
}
|
|
22260
|
+
function resolveJobPresentation({
|
|
22261
|
+
jobId,
|
|
22262
|
+
result,
|
|
22263
|
+
feedItems = [],
|
|
22264
|
+
sessionHeap,
|
|
22265
|
+
allowExplicitArtifacts = true
|
|
22266
|
+
}) {
|
|
22267
|
+
const refs = {
|
|
22268
|
+
entryPaths: /* @__PURE__ */ new Set(),
|
|
22269
|
+
listNames: /* @__PURE__ */ new Set(),
|
|
22270
|
+
variableNames: /* @__PURE__ */ new Set()
|
|
22271
|
+
};
|
|
22272
|
+
if (allowExplicitArtifacts) {
|
|
22273
|
+
scanForHeapReferences(result, sessionHeap, refs);
|
|
22274
|
+
resolveVariablesToReferences(refs.variableNames, sessionHeap, refs);
|
|
22275
|
+
}
|
|
22276
|
+
const referencedLists = sortLists(
|
|
22277
|
+
[...refs.listNames].map((name) => sessionHeap.listsByName?.[name]).filter((list) => Boolean(list))
|
|
22278
|
+
);
|
|
22279
|
+
const referencedEntries = sortEntries(
|
|
22280
|
+
[...refs.entryPaths].map((path) => sessionHeap.entriesByPath?.[path]).filter((entry) => Boolean(entry))
|
|
22281
|
+
);
|
|
22282
|
+
const jobLists = getJobRelatedLists(sessionHeap, jobId);
|
|
22283
|
+
const jobEntries = getJobRelatedEntries(sessionHeap, jobId);
|
|
22284
|
+
const changedEntries = dedupeEntries([
|
|
22285
|
+
...jobEntries,
|
|
22286
|
+
...entriesFromLists(jobLists, sessionHeap)
|
|
22287
|
+
]);
|
|
22288
|
+
const explicitLists = dedupeLists(referencedLists);
|
|
22289
|
+
const explicitEntries = dedupeEntries([
|
|
22290
|
+
...referencedEntries,
|
|
22291
|
+
...entriesFromLists(referencedLists, sessionHeap)
|
|
22292
|
+
]);
|
|
22293
|
+
const hasExplicitArtifacts = allowExplicitArtifacts && (explicitEntries.length > 0 || explicitLists.length > 0);
|
|
22294
|
+
const lists = hasExplicitArtifacts ? explicitLists : jobLists;
|
|
22295
|
+
const entries = hasExplicitArtifacts ? explicitEntries : changedEntries;
|
|
22296
|
+
return {
|
|
22297
|
+
responseText: responseTextFromFeedItems(feedItems, jobId),
|
|
22298
|
+
entries,
|
|
22299
|
+
lists,
|
|
22300
|
+
changedEntries,
|
|
22301
|
+
changedLists: jobLists,
|
|
22302
|
+
hasExplicitArtifacts
|
|
22303
|
+
};
|
|
22304
|
+
}
|
|
22305
|
+
|
|
20733
22306
|
// src/openai-usage.ts
|
|
20734
22307
|
var OPENAI_GPT_5_4_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/models/gpt-5.4/";
|
|
20735
22308
|
var OPENAI_GPT_5_6_PRICING_SOURCE_URL = "https://developers.openai.com/api/docs/pricing";
|
|
@@ -20749,22 +22322,22 @@ var OPENAI_MODEL_PRICING_USD_PER_MILLION = {
|
|
|
20749
22322
|
provider: "openai",
|
|
20750
22323
|
model: "gpt-5.6-luna",
|
|
20751
22324
|
currency: "USD",
|
|
20752
|
-
inputUsdPerMillion:
|
|
20753
|
-
cachedInputUsdPerMillion: 0.
|
|
20754
|
-
cacheWriteUsdPerMillion:
|
|
20755
|
-
outputUsdPerMillion:
|
|
22325
|
+
inputUsdPerMillion: 0.2,
|
|
22326
|
+
cachedInputUsdPerMillion: 0.02,
|
|
22327
|
+
cacheWriteUsdPerMillion: 0.25,
|
|
22328
|
+
outputUsdPerMillion: 1.2,
|
|
20756
22329
|
sourceUrl: OPENAI_GPT_5_6_PRICING_SOURCE_URL,
|
|
20757
|
-
effectiveDate: "2026-07-
|
|
22330
|
+
effectiveDate: "2026-07-30",
|
|
20758
22331
|
longContextThresholdTokens: OPENAI_LONG_CONTEXT_THRESHOLD_TOKENS,
|
|
20759
22332
|
longContextPricing: {
|
|
20760
|
-
inputUsdPerMillion:
|
|
20761
|
-
cachedInputUsdPerMillion: 0.
|
|
20762
|
-
cacheWriteUsdPerMillion:
|
|
20763
|
-
outputUsdPerMillion:
|
|
22333
|
+
inputUsdPerMillion: 0.4,
|
|
22334
|
+
cachedInputUsdPerMillion: 0.04,
|
|
22335
|
+
cacheWriteUsdPerMillion: 0.5,
|
|
22336
|
+
outputUsdPerMillion: 1.8
|
|
20764
22337
|
}
|
|
20765
22338
|
}
|
|
20766
22339
|
};
|
|
20767
|
-
function
|
|
22340
|
+
function asRecord6(value) {
|
|
20768
22341
|
return value && typeof value === "object" ? value : null;
|
|
20769
22342
|
}
|
|
20770
22343
|
function numberField(record, key) {
|
|
@@ -20788,7 +22361,7 @@ function getOpenAIModelPricing(model, inputTokens = 0) {
|
|
|
20788
22361
|
return { ...pricing, contextTier: "short" };
|
|
20789
22362
|
}
|
|
20790
22363
|
function normalizeOpenAIUsage(rawUsage) {
|
|
20791
|
-
const usage =
|
|
22364
|
+
const usage = asRecord6(rawUsage);
|
|
20792
22365
|
if (!usage) {
|
|
20793
22366
|
return {
|
|
20794
22367
|
inputTokens: 0,
|
|
@@ -20803,8 +22376,8 @@ function normalizeOpenAIUsage(rawUsage) {
|
|
|
20803
22376
|
const inputTokens = numberField(usage, "prompt_tokens") || numberField(usage, "input_tokens");
|
|
20804
22377
|
const outputTokens = numberField(usage, "completion_tokens") || numberField(usage, "output_tokens");
|
|
20805
22378
|
const totalTokens = numberField(usage, "total_tokens") || inputTokens + outputTokens;
|
|
20806
|
-
const inputDetails =
|
|
20807
|
-
const outputDetails =
|
|
22379
|
+
const inputDetails = asRecord6(usage.prompt_tokens_details) || asRecord6(usage.input_tokens_details);
|
|
22380
|
+
const outputDetails = asRecord6(usage.completion_tokens_details) || asRecord6(usage.output_tokens_details);
|
|
20808
22381
|
const cachedInputTokens = Math.min(
|
|
20809
22382
|
inputTokens,
|
|
20810
22383
|
numberField(inputDetails, "cached_tokens") || numberField(inputDetails, "cached_input_tokens")
|
|
@@ -20874,10 +22447,12 @@ function calculateOpenAITokenSpend(model, rawUsage) {
|
|
|
20874
22447
|
|
|
20875
22448
|
exports.Environment = Environment;
|
|
20876
22449
|
exports.EnvironmentSession = EnvironmentSession;
|
|
22450
|
+
exports.GRANULAR_FEED_DIAGNOSTIC_EVENT = GRANULAR_FEED_DIAGNOSTIC_EVENT;
|
|
20877
22451
|
exports.Granular = Granular;
|
|
20878
22452
|
exports.OPENAI_MODEL_PRICING_USD_PER_MILLION = OPENAI_MODEL_PRICING_USD_PER_MILLION;
|
|
20879
22453
|
exports.OntologyHandle = OntologyHandle;
|
|
20880
22454
|
exports.Session = Session;
|
|
22455
|
+
exports.SessionFeedController = SessionFeedController;
|
|
20881
22456
|
exports.WSClient = WSClient;
|
|
20882
22457
|
exports.buildContinuationInstruction = buildContinuationInstruction;
|
|
20883
22458
|
exports.buildContinuationInstructionFromTemplate = buildContinuationInstructionFromTemplate;
|
|
@@ -20897,10 +22472,15 @@ exports.buildGranularAgentToolBlock = buildGranularAgentToolBlock;
|
|
|
20897
22472
|
exports.buildGranularAgentWorkflowBlock = buildGranularAgentWorkflowBlock;
|
|
20898
22473
|
exports.buildOpenAISpendEventId = buildOpenAISpendEventId;
|
|
20899
22474
|
exports.buildSessionTranscript = buildSessionTranscript;
|
|
22475
|
+
exports.buildSessionTranscriptFromFeedItems = buildSessionTranscriptFromFeedItems;
|
|
20900
22476
|
exports.calculateOpenAITokenSpend = calculateOpenAITokenSpend;
|
|
20901
22477
|
exports.consumeGranularReasoningOnlyChunk = consumeGranularReasoningOnlyChunk;
|
|
20902
22478
|
exports.consumeGranularReasoningTraceChunk = consumeGranularReasoningTraceChunk;
|
|
22479
|
+
exports.createFeedPublisher = createFeedPublisher;
|
|
20903
22480
|
exports.createHarnessVerifierSnapshot = createHarnessVerifierSnapshot;
|
|
22481
|
+
exports.emitFeedDiagnostic = emitFeedDiagnostic;
|
|
22482
|
+
exports.emitFeedDiagnosticToDefaultSink = emitFeedDiagnosticToDefaultSink;
|
|
22483
|
+
exports.emptyFeedSnapshot = emptyFeedSnapshot;
|
|
20904
22484
|
exports.evaluateContinuation = evaluateContinuation;
|
|
20905
22485
|
exports.evaluateValidationRule = evaluateValidationRule;
|
|
20906
22486
|
exports.extractPromptTokens = extractPromptTokens;
|
|
@@ -20908,17 +22488,24 @@ exports.getCurrentClosureId = getCurrentClosureId;
|
|
|
20908
22488
|
exports.getDefaultHarnessTemplateId = getDefaultHarnessTemplateId;
|
|
20909
22489
|
exports.getExclusivePromptTarget = getExclusivePromptTarget;
|
|
20910
22490
|
exports.getOpenAIModelPricing = getOpenAIModelPricing;
|
|
22491
|
+
exports.hasCanonicalSessionFeedActivation = hasCanonicalSessionFeedActivation;
|
|
20911
22492
|
exports.hasOpenPrompt = hasOpenPrompt;
|
|
20912
22493
|
exports.hashHarnessTemplateValue = hashHarnessTemplateValue;
|
|
20913
22494
|
exports.invokeRegisteredEffect = invokeRegisteredEffect;
|
|
22495
|
+
exports.isCanonicalSessionFeedDocument = isCanonicalSessionFeedDocument;
|
|
20914
22496
|
exports.isLocalApiUrl = isLocalApiUrl;
|
|
20915
22497
|
exports.listHarnessTemplates = listHarnessTemplates;
|
|
22498
|
+
exports.mergeFeedItemsBySequence = mergeFeedItemsBySequence;
|
|
20916
22499
|
exports.normalizeEffectBehaviors = normalizeEffectBehaviors;
|
|
22500
|
+
exports.normalizeFeedDiagnostic = normalizeFeedDiagnostic;
|
|
22501
|
+
exports.normalizeFeedDiagnosticKind = normalizeFeedDiagnosticKind;
|
|
22502
|
+
exports.normalizeFeedPage = normalizeFeedPage;
|
|
20917
22503
|
exports.normalizeOpenAIUsage = normalizeOpenAIUsage;
|
|
20918
22504
|
exports.normalizePrompt = normalizePrompt;
|
|
20919
22505
|
exports.normalizePromptChoiceOption = normalizePromptChoiceOption;
|
|
20920
22506
|
exports.normalizePromptText = normalizePromptText;
|
|
20921
22507
|
exports.normalizePromptType = normalizePromptType;
|
|
22508
|
+
exports.orderTransientFeedItems = orderTransientFeedItems;
|
|
20922
22509
|
exports.projectConversationReferentFocus = projectConversationReferentFocus;
|
|
20923
22510
|
exports.projectConversationReferentSummary = projectConversationReferentSummary;
|
|
20924
22511
|
exports.projectHeapSummary = projectHeapSummary;
|
|
@@ -20926,6 +22513,7 @@ exports.projectLoopSummary = projectLoopSummary;
|
|
|
20926
22513
|
exports.projectSessionFileSummary = projectSessionFileSummary;
|
|
20927
22514
|
exports.projectWorkflowFocus = projectWorkflowFocus;
|
|
20928
22515
|
exports.projectWorkflowSummary = projectWorkflowSummary;
|
|
22516
|
+
exports.readSessionFeedSnapshot = readSessionFeedSnapshot;
|
|
20929
22517
|
exports.recordOpenAIUsageSpend = recordOpenAIUsageSpend;
|
|
20930
22518
|
exports.renderContinuationInstructionFromTemplate = renderContinuationInstructionFromTemplate;
|
|
20931
22519
|
exports.renderGranularAgentSystemPromptFromTemplate = renderGranularAgentSystemPromptFromTemplate;
|