@granular-software/sdk 0.4.58 → 0.4.60

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