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