@sentry/junior 0.120.0 → 0.122.0

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.
Files changed (69) hide show
  1. package/dist/{agent-hooks-5DDPWRYO.js → agent-hooks-HJMLQLCB.js} +8 -8
  2. package/dist/api.js +14 -14
  3. package/dist/app.js +604 -997
  4. package/dist/{catalog-runtime-5KR5XBMJ.js → catalog-runtime-CYPZUYJD.js} +5 -5
  5. package/dist/chat/agent/index.d.ts +19 -2
  6. package/dist/chat/agent/request.d.ts +8 -8
  7. package/dist/chat/agent/resume.d.ts +1 -3
  8. package/dist/chat/agent/retry-usage.d.ts +8 -0
  9. package/dist/chat/agent/skills.d.ts +0 -9
  10. package/dist/chat/conversations/messages.d.ts +7 -0
  11. package/dist/chat/conversations/projection.d.ts +16 -0
  12. package/dist/chat/logging.d.ts +46 -13
  13. package/dist/chat/runtime/processing-reaction.d.ts +2 -4
  14. package/dist/chat/runtime/slack-runtime.d.ts +2 -2
  15. package/dist/chat/sandbox/noninteractive-command.d.ts +1 -0
  16. package/dist/chat/sandbox/session.d.ts +2 -2
  17. package/dist/chat/sandbox/snapshot/install.d.ts +2 -2
  18. package/dist/chat/sandbox/snapshot/resolve.d.ts +1 -0
  19. package/dist/chat/sandbox/workspace.d.ts +3 -1
  20. package/dist/chat/services/assistant-reply.d.ts +16 -0
  21. package/dist/chat/services/empty-output-continuation.d.ts +20 -0
  22. package/dist/chat/services/turn-failure-response.d.ts +1 -3
  23. package/dist/chat/services/turn-result.d.ts +0 -5
  24. package/dist/chat/services/turn-session-record.d.ts +0 -11
  25. package/dist/chat/tool-support/zod-tool.d.ts +11 -6
  26. package/dist/chat/tools/execute-tool.d.ts +1 -33
  27. package/dist/chat/tools/execution/tool-error-handler.d.ts +2 -2
  28. package/dist/chat/tools/query-conversation-events.d.ts +3 -1
  29. package/dist/chat/tools/sandbox/bash.d.ts +1 -33
  30. package/dist/chat/tools/sandbox/edit-file.d.ts +1 -33
  31. package/dist/chat/tools/sandbox/find-files.d.ts +1 -33
  32. package/dist/chat/tools/sandbox/grep.d.ts +1 -33
  33. package/dist/chat/tools/sandbox/list-dir.d.ts +1 -33
  34. package/dist/chat/tools/sandbox/read-file.d.ts +1 -33
  35. package/dist/chat/tools/sandbox/write-file.d.ts +1 -33
  36. package/dist/{chunk-VURFJVFH.js → chunk-2RFM7STT.js} +11 -22
  37. package/dist/{chunk-6WX5SPI3.js → chunk-32FPGM2V.js} +1 -1
  38. package/dist/{chunk-ALYQOZG7.js → chunk-4SPRQJOW.js} +37 -45
  39. package/dist/{chunk-QOQPPRFS.js → chunk-4TGVC42X.js} +133 -206
  40. package/dist/{chunk-Q3RL6A4E.js → chunk-BDE5RZVF.js} +2 -2
  41. package/dist/{chunk-DJDGDJDF.js → chunk-CYW4M6IV.js} +44 -23
  42. package/dist/{chunk-M243JMZH.js → chunk-DOB5RVMH.js} +2 -2
  43. package/dist/{chunk-EIVE6GL4.js → chunk-E5SRLZQV.js} +14 -29
  44. package/dist/{chunk-YEAOUT5W.js → chunk-GFTZKOTP.js} +1 -1
  45. package/dist/{chunk-XAI2HNAY.js → chunk-GN6WW7WR.js} +16 -31
  46. package/dist/{chunk-DMKHRBI6.js → chunk-ISQYBTPG.js} +3 -3
  47. package/dist/{chunk-DWOH57FW.js → chunk-L2KJ2GNL.js} +1 -1
  48. package/dist/{chunk-ERYVNWNE.js → chunk-NIJG6G6X.js} +621 -735
  49. package/dist/{chunk-JQWP6V22.js → chunk-NNGYYGX5.js} +2061 -2015
  50. package/dist/{chunk-GNNV3UTY.js → chunk-OKUZIXQM.js} +1 -1
  51. package/dist/{chunk-HDFGNU5E.js → chunk-S7G34U75.js} +3 -1
  52. package/dist/{chunk-ALAKN7LR.js → chunk-TZVZCIYL.js} +1 -1
  53. package/dist/{chunk-24B5R6OU.js → chunk-USKP7IM2.js} +49 -92
  54. package/dist/{chunk-BHXW734F.js → chunk-V6XPIKYA.js} +13 -19
  55. package/dist/{chunk-TKUCRFNX.js → chunk-VCC7XFX2.js} +22 -45
  56. package/dist/cli/chat.js +22 -22
  57. package/dist/cli/check.js +6 -6
  58. package/dist/cli/plugins.js +11 -11
  59. package/dist/cli/snapshot-create.js +9 -9
  60. package/dist/cli/upgrade.js +6 -6
  61. package/dist/{db-T4ZY64PB.js → db-E6O2DXUQ.js} +6 -6
  62. package/dist/instrumentation.js +3 -3
  63. package/dist/nitro.js +2 -2
  64. package/dist/{oauth-callback-server-LQ6DFI26.js → oauth-callback-server-6SCPUFFD.js} +15 -15
  65. package/dist/{runner-D3ZU6TJH.js → runner-JFKCMVVY.js} +62 -51
  66. package/dist/{skills-JMXY7LIY.js → skills-RD66F2OB.js} +6 -6
  67. package/dist/{validation-OEPJS7JX.js → validation-XVRNX6IU.js} +6 -6
  68. package/dist/version.js +1 -1
  69. package/package.json +8 -8
@@ -13,17 +13,17 @@ import {
13
13
  instructionProvenanceFor,
14
14
  sanitizePostgresJson,
15
15
  withConversationEventLock
16
- } from "./chunk-M243JMZH.js";
16
+ } from "./chunk-DOB5RVMH.js";
17
17
  import {
18
18
  getStateAdapter,
19
19
  sleep
20
- } from "./chunk-ALAKN7LR.js";
20
+ } from "./chunk-TZVZCIYL.js";
21
21
  import {
22
22
  botConfig,
23
23
  escapeXml,
24
24
  resolveGatewayModel,
25
25
  unwrapCurrentInstruction
26
- } from "./chunk-VURFJVFH.js";
26
+ } from "./chunk-2RFM7STT.js";
27
27
  import {
28
28
  toStoredSlackActor
29
29
  } from "./chunk-HZF5P5UN.js";
@@ -32,7 +32,7 @@ import {
32
32
  logException,
33
33
  logWarn,
34
34
  setSpanAttributes
35
- } from "./chunk-ALYQOZG7.js";
35
+ } from "./chunk-4SPRQJOW.js";
36
36
  import {
37
37
  isRecord,
38
38
  toOptionalNumber,
@@ -147,2192 +147,2231 @@ function stripRuntimeTurnContext(messages) {
147
147
  });
148
148
  }
149
149
 
150
- // src/chat/conversations/projection.ts
151
- import { isDeepStrictEqual } from "util";
152
-
153
- // src/chat/pi/messages.ts
154
- import { z } from "zod";
155
- var piMessageSchema = z.object({
156
- role: z.string()
157
- }).passthrough().transform((value) => value);
158
- var piContentMessageSchema = z.object({
159
- content: z.array(z.unknown()),
160
- role: z.string().min(1)
161
- }).passthrough().transform((value) => value);
162
-
163
- // src/chat/pi/conversation-events.ts
164
- function authorizationObservationMessage(data, createdAtMs) {
165
- const label = data.kind === "mcp" ? "MCP authorization" : "Authorization";
166
- return piMessageSchema.parse({
167
- role: "user",
168
- content: [
169
- {
170
- type: "text",
171
- text: `${label} completed for provider "${data.provider}". Continue the blocked request and retry the provider operation if needed.`
172
- }
173
- ],
174
- timestamp: createdAtMs
175
- });
176
- }
177
- function durableMessages(message) {
178
- return stripRuntimeTurnContext([message]);
150
+ // src/chat/services/context-budget.ts
151
+ var COMPACTION_TRIGGER_RATIO = 0.9;
152
+ var CONTEXT_INPUT_LIMIT_RATIO = 0.95;
153
+ var COMPACTION_TARGET_RATIO = 0.8;
154
+ var FALLBACK_CONTEXT_WINDOW_TOKENS = 4e5;
155
+ function positiveInteger(value, fallback) {
156
+ return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
179
157
  }
180
- function historyItemFromPiMessage(message, provenance) {
181
- const parsed = piMessageSchema.parse(message);
182
- const { role, ...fields } = parsed;
183
- if (role === "user") {
184
- return agentHistoryItemSchema.parse({
185
- ...fields,
186
- type: "user_message",
187
- provenance
188
- });
189
- }
190
- if (role === "assistant") {
191
- return agentHistoryItemSchema.parse({
192
- ...fields,
193
- type: "assistant_message"
194
- });
195
- }
196
- if (role === "toolResult") {
197
- return agentHistoryItemSchema.parse({
198
- ...fields,
199
- type: "tool_result"
200
- });
201
- }
202
- throw new Error(`Unsupported durable agent message role "${role}"`);
158
+ function estimateTextTokens(text) {
159
+ return Math.ceil(text.length / 4);
203
160
  }
204
- function piMessageFromHistoryItem(data) {
205
- if (data.type === "user_message") {
206
- const { type: _2, provenance: __, ...fields2 } = data;
207
- return piMessageSchema.parse({ ...fields2, role: "user" });
208
- }
209
- if (data.type === "assistant_message") {
210
- const { type: _2, ...fields2 } = data;
211
- return piMessageSchema.parse({ ...fields2, role: "assistant" });
212
- }
213
- const { type: _, ...fields } = data;
214
- return piMessageSchema.parse({ ...fields, role: "toolResult" });
161
+ function calculateContextCompactionTriggerTokens(model) {
162
+ const contextWindow = positiveInteger(
163
+ model.contextWindow,
164
+ FALLBACK_CONTEXT_WINDOW_TOKENS
165
+ );
166
+ return Math.max(1, Math.floor(contextWindow * COMPACTION_TRIGGER_RATIO));
215
167
  }
216
- function historyItemProvenance(data) {
217
- return data.type === "user_message" ? data.provenance : contextProvenance;
168
+ function calculateContextInputLimitTokens(model) {
169
+ const contextWindow = positiveInteger(
170
+ model.contextWindow,
171
+ FALLBACK_CONTEXT_WINDOW_TOKENS
172
+ );
173
+ return Math.max(1, Math.floor(contextWindow * CONTEXT_INPUT_LIMIT_RATIO));
218
174
  }
219
- function projectConversationEvents(events, options) {
220
- const messages = [];
221
- const provenance = [];
222
- const seqs = [];
223
- let modelProfile = "standard";
224
- let modelId;
225
- for (const event of events) {
226
- if (options?.maxSeq !== void 0 && event.seq > options.maxSeq) break;
227
- if (event.data.type === "unknown") {
228
- throw new Error(
229
- `Unsupported conversation event "${event.data.originalType}" at seq ${event.seq} (schema version ${event.schemaVersion})`
230
- );
231
- }
232
- if (event.data.type === "compaction" || event.data.type === "handoff") {
233
- modelProfile = event.data.modelProfile;
234
- modelId = event.data.modelId;
235
- for (const replacement of event.data.replacementHistory) {
236
- for (const message of durableMessages(
237
- piMessageFromHistoryItem(replacement.item)
238
- )) {
239
- messages.push(message);
240
- provenance.push(historyItemProvenance(replacement.item));
241
- seqs.push(replacement.sourceEventSeq ?? event.seq);
242
- }
243
- }
244
- continue;
245
- }
246
- if (event.data.type === "user_message" || event.data.type === "assistant_message" || event.data.type === "tool_result") {
247
- for (const message of durableMessages(
248
- piMessageFromHistoryItem(event.data)
249
- )) {
250
- messages.push(message);
251
- provenance.push(historyItemProvenance(event.data));
252
- seqs.push(event.seq);
253
- }
254
- continue;
255
- }
256
- if (event.data.type === "authorization_completed") {
257
- messages.push(
258
- authorizationObservationMessage(event.data, event.createdAtMs)
259
- );
260
- provenance.push(contextProvenance);
261
- seqs.push(event.seq);
262
- }
263
- }
264
- return { messages, provenance, seqs, modelProfile, modelId };
175
+ function calculateContextCompactionTargetTokens(triggerTokens) {
176
+ return Math.max(1, Math.floor(triggerTokens * COMPACTION_TARGET_RATIO));
265
177
  }
266
-
267
- // src/chat/conversations/projection.ts
268
- function connectedMcpProvidersFromEvents(events) {
269
- const providers = /* @__PURE__ */ new Set();
270
- for (const event of events) {
271
- if (event.data.type === "mcp_provider_connected") {
272
- providers.add(event.data.provider);
273
- }
274
- }
275
- return [...providers].sort((left, right) => left.localeCompare(right));
178
+ function getModelContextBudget(modelId) {
179
+ const model = resolveGatewayModel(modelId);
180
+ const advertisedContextWindow = positiveInteger(
181
+ model.contextWindow,
182
+ FALLBACK_CONTEXT_WINDOW_TOKENS
183
+ );
184
+ return {
185
+ contextWindow: Math.min(
186
+ botConfig.contextWindowTokens,
187
+ advertisedContextWindow
188
+ )
189
+ };
276
190
  }
277
- function isUserMessage(message) {
278
- return message.role === "user";
191
+ function getAgentContextCompactionTriggerTokens(modelId) {
192
+ return calculateContextCompactionTriggerTokens(
193
+ getModelContextBudget(modelId)
194
+ );
279
195
  }
280
- function countMatchingPrefix(left, right) {
281
- const limit = Math.min(left.length, right.length);
282
- for (let index = 0; index < limit; index += 1) {
283
- if (!isDeepStrictEqual(left[index], right[index])) {
284
- return index;
285
- }
286
- }
287
- return limit;
196
+ function getAgentContextInputLimitTokens(modelId) {
197
+ return calculateContextInputLimitTokens(getModelContextBudget(modelId));
288
198
  }
289
- function normalizeDurableMessage(message) {
290
- return piMessageSchema.parse(
291
- JSON.parse(JSON.stringify(sanitizePostgresJson(message)))
199
+ function getConversationContextCompactionTriggerTokens() {
200
+ return calculateContextCompactionTriggerTokens(
201
+ getModelContextBudget(botConfig.fastModelId)
292
202
  );
293
203
  }
294
- function resolveCommitProvenance(args) {
295
- if (args.explicitProvenance) {
296
- if (args.explicitProvenance.length !== args.nextMessages.length) {
297
- throw new Error("commit provenance must align one-to-one with messages");
298
- }
299
- return args.explicitProvenance;
204
+
205
+ // src/chat/services/conversation-memory.ts
206
+ var CONTEXT_MIN_LIVE_MESSAGES = 12;
207
+ var CONTEXT_COMPACTION_BATCH_SIZE = 24;
208
+ var CONTEXT_MAX_COMPACTIONS = 16;
209
+ var CONTEXT_MAX_MESSAGE_CHARS = 3200;
210
+ var SLACK_USER_ID_DISPLAY_PATTERN = /^[UW][A-Z0-9]{5,}$/;
211
+ function generateConversationId(prefix) {
212
+ return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
213
+ }
214
+ function normalizeConversationText(text) {
215
+ return text.replace(/\r\n?/g, "\n").trim();
216
+ }
217
+ function buildImageContextSuffix(message, conversation) {
218
+ const byFileId = conversation?.vision.byFileId;
219
+ const imageFileIds = message.meta?.imageFileIds ?? [];
220
+ if (!byFileId || imageFileIds.length === 0) {
221
+ return "";
300
222
  }
301
- if (args.existing.provenance.length !== args.existing.messages.length) {
302
- throw new Error("committed provenance must align one-to-one with messages");
223
+ const summaries = imageFileIds.map((fileId) => byFileId[fileId]?.summary?.trim()).filter((summary) => Boolean(summary));
224
+ if (summaries.length === 0) {
225
+ return "";
303
226
  }
304
- const matchingPrefix = args.matchingPrefix;
305
- const provenance = args.nextMessages.map(
306
- (_, index) => index < matchingPrefix ? args.existing.provenance[index] : contextProvenance
307
- );
308
- if (args.newMessageProvenance) {
309
- for (let index = args.nextMessages.length - 1; index >= matchingPrefix; index -= 1) {
310
- if (isUserMessage(args.nextMessages[index])) {
311
- provenance[index] = args.newMessageProvenance;
312
- break;
313
- }
314
- }
227
+ return ` [image context: ${summaries.join(" | ")}]`;
228
+ }
229
+ function renderConversationMessageLine(message, conversation) {
230
+ const displayName = conversationAuthorDisplayName(message);
231
+ const text = message.text.replace(/\s+/g, " ").slice(0, CONTEXT_MAX_MESSAGE_CHARS);
232
+ const markers = [];
233
+ if (message.meta?.replied === false) {
234
+ markers.push(
235
+ `assistant skipped: ${message.meta?.skippedReason ?? "no-reply route"}`
236
+ );
315
237
  }
316
- if (args.trailingMessageProvenance) {
317
- if (args.trailingMessageProvenance.length > provenance.length) {
318
- throw new Error(
319
- "trailing commit provenance cannot exceed committed messages"
320
- );
321
- }
322
- const newMessageCount = args.nextMessages.length - matchingPrefix;
323
- if (args.trailingMessageProvenance.length > newMessageCount) {
324
- throw new Error(
325
- "trailing commit provenance must align to newly committed messages"
326
- );
327
- }
328
- const start = provenance.length - args.trailingMessageProvenance.length;
329
- args.trailingMessageProvenance.forEach((entry, offset) => {
330
- provenance[start + offset] = entry;
331
- });
238
+ if (message.meta?.explicitMention) {
239
+ markers.push("explicit mention");
332
240
  }
333
- return provenance;
334
- }
335
- async function loadProjection(args) {
336
- const events = await getConversationEventStore().loadCurrentHistory(
337
- args.conversationId
338
- );
339
- return projectConversationEvents(events).messages;
241
+ const markerSuffix = markers.length > 0 ? ` (${markers.join("; ")})` : "";
242
+ const imageContext = buildImageContextSuffix(message, conversation);
243
+ return `[${message.role}] ${displayName}: ${text}${imageContext}${markerSuffix}`;
340
244
  }
341
- async function loadConversationProjection(args) {
342
- const events = await getConversationEventStore().loadCurrentHistory(
343
- args.conversationId
344
- );
345
- return projectConversationEvents(events);
346
- }
347
- async function openConversationProjection(args) {
348
- const events = await getConversationEventStore().loadCurrentHistory(
349
- args.conversationId
350
- );
351
- return projectConversationEvents(events);
245
+ function conversationAuthorDisplayName(message) {
246
+ const author = message.author;
247
+ const fullName = authorDisplayField(author?.fullName, author?.userId);
248
+ const userName = authorDisplayField(author?.userName, author?.userId);
249
+ return fullName ?? userName ?? (message.role === "assistant" ? botConfig.userName : message.role);
352
250
  }
353
- async function loadTurnProjection(args) {
354
- const eventStore = getConversationEventStore();
355
- if (args.committedSeq < 0) {
356
- return projectConversationEvents(
357
- await eventStore.loadCurrentHistory(args.conversationId)
358
- );
359
- }
360
- const historyEvents = await eventStore.loadHistoryContaining(
361
- args.conversationId,
362
- args.committedSeq,
363
- args.includeTail ? void 0 : args.committedSeq
364
- );
365
- if (!historyEvents) {
251
+ function authorDisplayField(value, userId) {
252
+ if (!value || value === userId || SLACK_USER_ID_DISPLAY_PATTERN.test(value)) {
366
253
  return void 0;
367
254
  }
368
- return projectConversationEvents(historyEvents);
369
- }
370
- async function loadConnectedMcpProviders(args) {
371
- const events = await getConversationEventStore().loadCurrentHistory(
372
- args.conversationId
373
- );
374
- return connectedMcpProvidersFromEvents(events);
375
- }
376
- function messageTimestamp(message) {
377
- const timestamp = message.timestamp;
378
- return typeof timestamp === "number" ? timestamp : Date.now();
255
+ return value;
379
256
  }
380
- async function commitMessages(args) {
381
- const executor = args.executor ?? getSqlExecutor();
382
- return await withConversationEventLock(
383
- executor,
384
- args.conversationId,
385
- async () => executor.transaction(
386
- async () => await commitMessagesLocked(args, executor)
387
- )
257
+ function updateConversationStats(conversation) {
258
+ const contextText = buildConversationContext(conversation);
259
+ conversation.stats.estimatedContextTokens = estimateTextTokens(
260
+ contextText ?? ""
388
261
  );
262
+ conversation.stats.totalMessageCount = conversation.messages.length;
263
+ conversation.stats.updatedAtMs = Date.now();
389
264
  }
390
- async function commitMessagesLocked(args, executor) {
391
- const eventStore = createSqlConversationEventStore(executor);
392
- const currentEvents = await eventStore.loadCurrentHistory(
393
- args.conversationId
394
- );
395
- const current = projectConversationEvents(currentEvents);
396
- const nextLocalMessages = stripRuntimeTurnContext(args.messages).map(
397
- normalizeDurableMessage
398
- );
399
- const matchingPrefix = countMatchingPrefix(
400
- current.messages,
401
- nextLocalMessages
265
+ function upsertConversationMessage(conversation, message) {
266
+ const existingIndex = conversation.messages.findIndex(
267
+ (entry) => entry.id === message.id
402
268
  );
403
- const nextLocalProvenance = resolveCommitProvenance({
404
- existing: current,
405
- nextMessages: nextLocalMessages,
406
- matchingPrefix,
407
- ...args.provenance ? { explicitProvenance: args.provenance } : {},
408
- ...args.trailingMessageProvenance ? { trailingMessageProvenance: args.trailingMessageProvenance } : {},
409
- ...args.newMessageProvenance ? { newMessageProvenance: args.newMessageProvenance } : {}
410
- });
411
- if (matchingPrefix === current.messages.length) {
412
- const newMessages = nextLocalMessages.slice(matchingPrefix);
413
- const turnContext = args.turnContext;
414
- const turnContextEvents = turnContext?.contexts.map((context, index) => ({
415
- idempotencyKey: `turn:${turnContext.turnId}:context:${context.pluginName}:${index}`,
416
- createdAtMs: context.loadedAtMs,
417
- data: {
418
- type: "turn_context",
419
- turnId: turnContext.turnId,
420
- pluginName: context.pluginName,
421
- kind: context.kind,
422
- version: context.version,
423
- content: context.content
269
+ if (existingIndex >= 0) {
270
+ conversation.messages[existingIndex] = {
271
+ ...conversation.messages[existingIndex],
272
+ ...message,
273
+ meta: {
274
+ ...conversation.messages[existingIndex]?.meta,
275
+ ...message.meta
424
276
  }
425
- })) ?? [];
426
- await eventStore.append(args.conversationId, [
427
- ...newMessages.map((message, index) => ({
428
- data: historyItemFromPiMessage(
429
- message,
430
- nextLocalProvenance[matchingPrefix + index]
431
- ),
432
- createdAtMs: messageTimestamp(message)
433
- })),
434
- ...turnContextEvents
435
- ]);
436
- } else {
437
- throw new Error(
438
- `Agent history for ${args.conversationId} changed before its committed boundary`
439
- );
277
+ };
278
+ updateConversationStats(conversation);
279
+ return message.id;
440
280
  }
441
- const committedEvents = await eventStore.loadCurrentHistory(
442
- args.conversationId
443
- );
444
- const committed = projectConversationEvents(committedEvents);
445
- return {
446
- committedSeq: committedEvents.at(-1)?.seq ?? -1,
447
- historyVersion: committedEvents.at(-1)?.historyVersion ?? 0,
448
- messageSeqs: committed.seqs,
449
- messages: nextLocalMessages,
450
- provenance: nextLocalProvenance
451
- };
281
+ conversation.messages.push(message);
282
+ updateConversationStats(conversation);
283
+ return message.id;
452
284
  }
453
- async function recordMcpProviderConnected(args) {
454
- const eventStore = getConversationEventStore();
455
- const events = await eventStore.loadCurrentHistory(args.conversationId);
456
- if (connectedMcpProvidersFromEvents(events).includes(args.provider)) {
457
- return;
458
- }
459
- await eventStore.append(args.conversationId, [
460
- {
461
- data: { type: "mcp_provider_connected", provider: args.provider },
462
- createdAtMs: Date.now()
285
+ function recordDeliveredAssistantMessage(args) {
286
+ const prefix = `${args.sessionId}:assistant:`;
287
+ const ordinal = args.conversation.messages.filter(
288
+ (message) => message.id.startsWith(prefix)
289
+ ).length + 1;
290
+ const messageId = `${prefix}${ordinal}`;
291
+ markConversationMessage(args.conversation, args.userMessageId, {
292
+ replied: true,
293
+ skippedReason: void 0
294
+ });
295
+ upsertConversationMessage(args.conversation, {
296
+ id: messageId,
297
+ role: "assistant",
298
+ text: normalizeConversationText(args.text),
299
+ createdAtMs: Date.now(),
300
+ author: {
301
+ userName: botConfig.userName,
302
+ isBot: true
303
+ },
304
+ meta: {
305
+ replied: true
463
306
  }
464
- ]);
307
+ });
308
+ return messageId;
465
309
  }
466
- async function recordAuthorizationRequested(args) {
467
- const eventStore = getConversationEventStore();
468
- const events = await eventStore.loadCurrentHistory(args.conversationId);
469
- if (events.some(
470
- (event) => event.data.type === "authorization_requested" && event.data.authorizationId === args.authorizationId
471
- )) {
472
- return;
473
- }
474
- await eventStore.append(args.conversationId, [
475
- {
476
- data: {
477
- type: "authorization_requested",
478
- kind: args.kind,
479
- provider: args.provider,
480
- actorId: args.actorId,
481
- authorizationId: args.authorizationId,
482
- delivery: args.delivery
483
- },
484
- createdAtMs: Date.now()
485
- }
486
- ]);
310
+ function turnHasReply(conversation, turnId) {
311
+ const assistantPrefix = `${turnId}:assistant:`;
312
+ return conversation.messages.some(
313
+ (message) => message.role === "assistant" && message.id.startsWith(assistantPrefix)
314
+ );
487
315
  }
488
- async function recordAuthorizationCompleted(args) {
489
- const eventStore = getConversationEventStore();
490
- const events = await eventStore.loadCurrentHistory(args.conversationId);
491
- if (events.some(
492
- (event) => event.data.type === "authorization_completed" && event.data.authorizationId === args.authorizationId
493
- )) {
494
- return;
495
- }
496
- await eventStore.append(args.conversationId, [
497
- {
498
- data: {
499
- type: "authorization_completed",
500
- kind: args.kind,
501
- provider: args.provider,
502
- actorId: args.actorId,
503
- authorizationId: args.authorizationId
504
- },
505
- createdAtMs: Date.now()
316
+ function markConversationMessage(conversation, messageId, patch) {
317
+ if (!messageId) return;
318
+ const messageIndex = conversation.messages.findIndex(
319
+ (entry) => entry.id === messageId
320
+ );
321
+ if (messageIndex < 0) return;
322
+ const current = conversation.messages[messageIndex];
323
+ conversation.messages[messageIndex] = {
324
+ ...current,
325
+ meta: {
326
+ ...current.meta ?? {},
327
+ ...patch
506
328
  }
507
- ]);
329
+ };
330
+ updateConversationStats(conversation);
508
331
  }
509
- async function loadTurnRoute(args) {
510
- const event = await getConversationEventStore().loadByIdempotencyKey(
511
- args.conversationId,
512
- `turn:${args.turnId}:routed`
332
+ function buildConversationContext(conversation, options = {}) {
333
+ const messages = conversation.messages.filter(
334
+ (entry) => entry.id !== options.excludeMessageId
513
335
  );
514
- if (!event) {
336
+ if (messages.length === 0 && conversation.compactions.length === 0) {
515
337
  return void 0;
516
338
  }
517
- if (event.data.type !== "turn_routed" || event.data.turnId !== args.turnId) {
518
- throw new Error(`Turn route key for "${args.turnId}" has invalid data`);
339
+ const lines = [];
340
+ if (conversation.compactions.length > 0) {
341
+ lines.push("<thread-compactions>");
342
+ for (const [index, compaction] of conversation.compactions.entries()) {
343
+ lines.push(
344
+ ` <compaction index="${index + 1}" covered_messages="${compaction.coveredMessageCount}" created_at="${new Date(compaction.createdAtMs).toISOString()}">`,
345
+ compaction.summary,
346
+ " </compaction>"
347
+ );
348
+ }
349
+ lines.push("</thread-compactions>");
519
350
  }
520
- return event.data;
521
- }
522
- async function recordTurnRoute(args) {
523
- await getConversationEventStore().append(args.conversationId, [
524
- {
525
- idempotencyKey: `turn:${args.turnId}:routed`,
526
- createdAtMs: Date.now(),
527
- data: {
528
- type: "turn_routed",
529
- turnId: args.turnId,
530
- modelProfile: args.modelProfile,
531
- modelId: args.modelId,
532
- reasoningLevel: args.reasoningLevel,
533
- ...args.confidence !== void 0 ? { confidence: args.confidence } : {},
534
- source: args.source
535
- }
351
+ if (messages.length > 0) {
352
+ if (lines.length > 0) {
353
+ lines.push("");
536
354
  }
537
- ]);
538
- }
539
- async function recordToolExecutionStarted(args) {
540
- await getConversationEventStore().append(args.conversationId, [
541
- {
542
- data: {
543
- type: "tool_execution_started",
544
- toolCallId: args.toolCallId,
545
- toolName: args.toolName
546
- },
547
- createdAtMs: args.createdAtMs ?? Date.now()
355
+ lines.push("<thread-transcript>");
356
+ for (const [index, message] of messages.entries()) {
357
+ const author = escapeXml(conversationAuthorDisplayName(message));
358
+ const actorIdAttr = message.author?.userId ? ` actor_id="${escapeXml(message.author.userId)}"` : "";
359
+ const ts = new Date(message.createdAtMs).toISOString();
360
+ const slackTsAttr = message.meta?.slackTs ? ` slack_ts="${escapeXml(message.meta.slackTs)}"` : "";
361
+ lines.push(
362
+ ` <message index="${index + 1}" ts="${ts}" role="${message.role}" author="${author}"${actorIdAttr}${slackTsAttr}>`,
363
+ renderConversationMessageLine(message, conversation),
364
+ " </message>"
365
+ );
548
366
  }
549
- ]);
367
+ lines.push("</thread-transcript>");
368
+ }
369
+ return lines.join("\n");
550
370
  }
551
-
552
- // src/chat/state/turn-id.ts
553
- function buildDeterministicTurnId(messageId) {
554
- const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
555
- return `turn_${sanitized}`;
371
+ function pruneCompactions(compactions) {
372
+ if (compactions.length <= CONTEXT_MAX_COMPACTIONS) {
373
+ return compactions;
374
+ }
375
+ const overflowCount = compactions.length - CONTEXT_MAX_COMPACTIONS + 1;
376
+ const merged = compactions.slice(0, overflowCount);
377
+ const mergedSummary = merged.map((entry) => entry.summary).join("\n").slice(0, 3500);
378
+ const coveredMessageCount = merged.reduce(
379
+ (count, entry) => count + entry.coveredMessageCount,
380
+ 0
381
+ );
382
+ const compacted = {
383
+ id: generateConversationId("compaction"),
384
+ createdAtMs: Date.now(),
385
+ summary: mergedSummary,
386
+ coveredMessageCount
387
+ };
388
+ return [compacted, ...compactions.slice(overflowCount)];
556
389
  }
557
- function buildDeterministicAssistantMessageId(turnId) {
558
- return `${turnId}:assistant:1`;
390
+ async function summarizeConversationChunk(messages, conversation, context, deps) {
391
+ const transcript = messages.map((message) => renderConversationMessageLine(message, conversation)).join("\n");
392
+ try {
393
+ const result = await deps.completeText({
394
+ modelId: botConfig.fastModelId,
395
+ temperature: 0,
396
+ messages: [
397
+ {
398
+ role: "user",
399
+ content: [
400
+ "Summarize the following older Slack thread transcript segment for future assistant turns.",
401
+ "Keep the summary factual and concise. Do not invent details.",
402
+ "",
403
+ "Output exactly three XML sections in this order:",
404
+ "<active-asks> one bullet per outstanding user ask that has not been narrowed, answered, or replaced by a later turn. Omit the section body if none. </active-asks>",
405
+ "<resolved-or-replaced-asks> one bullet per ask that has been replaced, narrowed, answered, or already acted on in this segment. Include the replacement/outcome inline. Omit the section body if none. </resolved-or-replaced-asks>",
406
+ "<facts> one bullet per durable fact useful regardless of scope: names, ids, URLs, decisions, locations, preferences, constraints that remain true. Omit the section body if none. </facts>",
407
+ "",
408
+ "Do not output any text outside the three sections.",
409
+ "",
410
+ transcript
411
+ ].join("\n"),
412
+ timestamp: Date.now()
413
+ }
414
+ ],
415
+ metadata: {
416
+ modelId: botConfig.fastModelId,
417
+ threadId: context.threadId ?? "",
418
+ channelId: context.channelId ?? "",
419
+ actorId: context.actorId ?? "",
420
+ runId: context.runId ?? ""
421
+ }
422
+ });
423
+ const summary = result.text.trim();
424
+ if (summary.length > 0) {
425
+ return summary.slice(0, 3500);
426
+ }
427
+ } catch (error) {
428
+ logWarn("conversation.compaction.summary.failed", {
429
+ "exception.message": error instanceof Error ? error.message : String(error),
430
+ "app.compaction_messages_covered": messages.length
431
+ });
432
+ }
433
+ return transcript.slice(0, 2800);
559
434
  }
560
-
561
- // src/chat/runtime/turn.ts
562
- var ConversationTurnBoundaryError = class extends Error {
563
- eventId;
564
- failureCode;
565
- constructor(args) {
566
- super(
567
- args.cause instanceof Error ? args.cause.message : String(args.cause),
435
+ async function generateThreadTitleWithDeps(sourceText, deps) {
436
+ const result = await deps.completeText({
437
+ modelId: botConfig.fastModelId,
438
+ temperature: 0,
439
+ messages: [
568
440
  {
569
- cause: args.cause
441
+ role: "user",
442
+ content: [
443
+ "Generate a concise 5-8 word Slack conversation title from the first user message below.",
444
+ "Capture the user's main request.",
445
+ "Reply with ONLY the title, with no quotes or trailing punctuation.",
446
+ "",
447
+ `First user message: ${sourceText.slice(0, 500)}`
448
+ ].join("\n"),
449
+ timestamp: Date.now()
570
450
  }
571
- );
572
- this.name = "ConversationTurnBoundaryError";
573
- this.eventId = args.eventId;
574
- this.failureCode = args.failureCode;
575
- }
576
- };
577
- function getConversationTurnBoundaryError(error) {
578
- return error instanceof ConversationTurnBoundaryError ? error : void 0;
451
+ ],
452
+ metadata: {
453
+ modelId: botConfig.fastModelId
454
+ }
455
+ });
456
+ return result.text.trim().slice(0, 60);
579
457
  }
580
- var CooperativeTurnYieldError = class extends Error {
581
- code = "cooperative_turn_yield";
582
- constructor(message = "Agent turn yielded at a safe boundary") {
583
- super(message);
584
- this.name = "CooperativeTurnYieldError";
458
+ function getThreadTitleSourceMessage(conversation) {
459
+ let firstMessage;
460
+ for (const message of conversation.messages) {
461
+ if (!isHumanConversationMessage(message)) {
462
+ continue;
463
+ }
464
+ if (!firstMessage) {
465
+ firstMessage = message;
466
+ continue;
467
+ }
468
+ if (message.createdAtMs < firstMessage.createdAtMs) {
469
+ firstMessage = message;
470
+ continue;
471
+ }
472
+ if (message.createdAtMs === firstMessage.createdAtMs && message.id < firstMessage.id) {
473
+ firstMessage = message;
474
+ }
585
475
  }
586
- };
587
- function isCooperativeTurnYieldError(error) {
588
- return error instanceof CooperativeTurnYieldError;
476
+ return firstMessage;
589
477
  }
590
- var TurnInputCommitLostError = class extends Error {
591
- code = "turn_input_commit_lost";
592
- constructor(message = "Turn input commit lost its durable owner") {
593
- super(message);
594
- this.name = "TurnInputCommitLostError";
478
+ async function compactConversationIfNeededWithDeps(conversation, context, deps) {
479
+ updateConversationStats(conversation);
480
+ let estimatedTokens = conversation.stats.estimatedContextTokens;
481
+ setSpanAttributes({
482
+ "app.context_tokens_estimated": estimatedTokens
483
+ });
484
+ const triggerTokens = getConversationContextCompactionTriggerTokens();
485
+ const targetTokens = calculateContextCompactionTargetTokens(triggerTokens);
486
+ while (estimatedTokens > triggerTokens && conversation.messages.length > CONTEXT_MIN_LIVE_MESSAGES) {
487
+ const compactCount = Math.min(
488
+ CONTEXT_COMPACTION_BATCH_SIZE,
489
+ conversation.messages.length - CONTEXT_MIN_LIVE_MESSAGES
490
+ );
491
+ if (compactCount <= 0) {
492
+ break;
493
+ }
494
+ const compactedChunk = conversation.messages.slice(0, compactCount);
495
+ const summary = await summarizeConversationChunk(
496
+ compactedChunk,
497
+ conversation,
498
+ context,
499
+ deps
500
+ );
501
+ conversation.compactions.push({
502
+ id: generateConversationId("compaction"),
503
+ createdAtMs: Date.now(),
504
+ summary,
505
+ coveredMessageCount: compactedChunk.length
506
+ });
507
+ conversation.compactions = pruneCompactions(conversation.compactions);
508
+ conversation.messages = conversation.messages.slice(compactCount);
509
+ conversation.stats.compactedMessageCount += compactCount;
510
+ updateConversationStats(conversation);
511
+ estimatedTokens = conversation.stats.estimatedContextTokens;
512
+ setSpanAttributes({
513
+ "app.compaction_messages_covered": compactCount,
514
+ "app.compaction.trigger_tokens": triggerTokens,
515
+ "app.compaction.target_tokens": targetTokens,
516
+ "app.context_tokens_estimated": estimatedTokens
517
+ });
518
+ if (estimatedTokens <= targetTokens) {
519
+ break;
520
+ }
595
521
  }
596
- };
597
- function isTurnInputCommitLostError(error) {
598
- return error instanceof TurnInputCommitLostError;
599
522
  }
600
- var TurnInputDeferredError = class extends Error {
601
- code = "turn_input_deferred";
602
- constructor(message = "Turn input is deferred until the active resume ends") {
603
- super(message);
604
- this.name = "TurnInputDeferredError";
605
- }
606
- };
607
- function isTurnInputDeferredError(error) {
608
- return error instanceof TurnInputDeferredError;
523
+ function createConversationMemoryService(deps) {
524
+ return {
525
+ compactConversationIfNeeded: async (conversation, context) => await compactConversationIfNeededWithDeps(conversation, context, deps),
526
+ generateThreadTitle: async (sourceText) => await generateThreadTitleWithDeps(sourceText, deps)
527
+ };
609
528
  }
610
- function startActiveTurn(args) {
611
- args.conversation.processing.activeTurnId = args.nextTurnId;
612
- args.updateConversationStats(args.conversation);
529
+ function isHumanConversationMessage(message) {
530
+ return message.role === "user" && message.author?.isBot !== true;
613
531
  }
614
- function clearActiveTurn(conversation, sessionId) {
615
- if (!sessionId || conversation.processing.activeTurnId === sessionId) {
616
- conversation.processing.activeTurnId = void 0;
532
+ function getConversationMessageSlackTs(message) {
533
+ return parseSlackMessageTs(message.meta?.slackTs);
534
+ }
535
+
536
+ // src/chat/conversations/messages.ts
537
+ import { createHash } from "crypto";
538
+ import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
539
+
540
+ // src/chat/conversations/message-summaries.ts
541
+ import { isDeepStrictEqual } from "util";
542
+ function projectConversationMessageSummaries(events) {
543
+ for (let index = events.length - 1; index >= 0; index -= 1) {
544
+ const data = events[index]?.data;
545
+ if (data?.type === "messages_summarized") {
546
+ return data.compactions;
547
+ }
617
548
  }
549
+ return [];
618
550
  }
619
- function markTurnClosed(args) {
620
- clearActiveTurn(args.conversation, args.sessionId);
621
- args.conversation.processing.lastCompletedAtMs = args.nowMs;
622
- args.updateConversationStats(args.conversation);
551
+ async function persistConversationMessageSummaries(args) {
552
+ const eventStore = getConversationEventStore();
553
+ const history = await eventStore.loadMessageHistory(args.conversationId);
554
+ const existing = history.compaction ? projectConversationMessageSummaries([history.compaction]) : [];
555
+ if (isDeepStrictEqual(existing, args.conversation.compactions)) {
556
+ return;
557
+ }
558
+ const liveMessageIds = new Set(
559
+ args.conversation.messages.map((message) => message.id)
560
+ );
561
+ const historyFromSeq = history.events.find(
562
+ (event) => event.data.type === "message" && liveMessageIds.has(event.data.messageId)
563
+ )?.seq;
564
+ const nextHistoryFromSeq = historyFromSeq ?? Math.max(history.historyFromSeq, (history.events.at(-1)?.seq ?? -1) + 1);
565
+ await eventStore.append(args.conversationId, [
566
+ {
567
+ data: {
568
+ type: "messages_summarized",
569
+ historyFromSeq: nextHistoryFromSeq,
570
+ compactions: args.conversation.compactions
571
+ },
572
+ createdAtMs: args.conversation.compactions.at(-1)?.createdAtMs ?? Date.now()
573
+ }
574
+ ]);
623
575
  }
624
- function markTurnCompleted(args) {
625
- clearActiveTurn(args.conversation, args.sessionId);
626
- args.conversation.processing.lastCompletedAtMs = args.nowMs;
627
- args.updateConversationStats(args.conversation);
576
+
577
+ // src/chat/conversations/message-projection.ts
578
+ function isMessageEvent(event) {
579
+ return event.data.type === "message" || event.data.type === "message_updated" || event.data.type === "message_handled";
628
580
  }
629
- function markTurnFailed(args) {
630
- clearActiveTurn(args.conversation, args.sessionId);
631
- args.conversation.processing.lastCompletedAtMs = args.nowMs;
632
- args.markConversationMessage(args.conversation, args.userMessageId, {
633
- replied: false,
634
- skippedReason: "reply failed"
635
- });
636
- args.updateConversationStats(args.conversation);
581
+ function splitMeta(meta) {
582
+ const rest = { ...meta ?? {} };
583
+ const author = rest.author;
584
+ delete rest.author;
585
+ return {
586
+ ...author ? { author } : {},
587
+ ...Object.keys(rest).length > 0 ? { meta: rest } : {}
588
+ };
637
589
  }
638
-
639
- // src/chat/services/persist-retry.ts
640
- var PERSIST_ATTEMPTS = 3;
641
- async function persistWithRetry(persist) {
642
- let lastError;
643
- for (let attempt = 1; attempt <= PERSIST_ATTEMPTS; attempt += 1) {
644
- try {
645
- await persist();
646
- return;
647
- } catch (error) {
648
- lastError = error;
649
- if (attempt < PERSIST_ATTEMPTS) {
650
- await sleep(attempt * 100);
590
+ function missingBaselineError(event) {
591
+ return new Error(
592
+ `Message event ${event.data.type} at seq ${event.seq} references ${event.data.messageId} before message`
593
+ );
594
+ }
595
+ function projectConversationMessages(history) {
596
+ const byId = /* @__PURE__ */ new Map();
597
+ for (const event of history.events) {
598
+ if (!isMessageEvent(event)) continue;
599
+ const data = event.data;
600
+ const current = byId.get(data.messageId);
601
+ if (data.type === "message" || data.type === "message_updated") {
602
+ const message = {
603
+ id: data.messageId,
604
+ role: data.role,
605
+ text: data.text,
606
+ createdAtMs: event.createdAtMs,
607
+ ...splitMeta(data.meta)
608
+ };
609
+ if (data.type === "message" && current)
610
+ throw new Error(
611
+ `Duplicate message event for ${data.messageId} at seq ${event.seq}`
612
+ );
613
+ if (data.type === "message_updated" && !current) {
614
+ if (history.historyFromSeq > 0) continue;
615
+ throw missingBaselineError(event);
651
616
  }
617
+ byId.set(data.messageId, {
618
+ message: {
619
+ ...message,
620
+ createdAtMs: current?.message.createdAtMs ?? message.createdAtMs
621
+ },
622
+ ...current?.repliedAtMs === void 0 ? {} : { repliedAtMs: current.repliedAtMs }
623
+ });
624
+ continue;
652
625
  }
626
+ if (!current) {
627
+ if (history.historyFromSeq > 0) continue;
628
+ throw missingBaselineError(event);
629
+ }
630
+ current.repliedAtMs ??= event.createdAtMs;
653
631
  }
654
- throw lastError;
655
- }
656
-
657
- // src/chat/state/turn-session.ts
658
- import { THREAD_STATE_TTL_MS } from "chat";
659
- import {
660
- actorSchema,
661
- destinationSchema,
662
- sourceSchema
663
- } from "@sentry/junior-plugin-api";
664
- import { z as z2 } from "zod";
665
-
666
- // src/chat/state/turn-session-keys.ts
667
- var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
668
- function agentTurnSessionKey(conversationId, sessionId) {
669
- return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
670
- }
671
- function agentTurnSessionConversationIndexKey(conversationId) {
672
- return `${AGENT_TURN_SESSION_PREFIX}:conversation:${conversationId}:index`;
632
+ return [...byId.values()].map(({ message, repliedAtMs }) => {
633
+ if (repliedAtMs === void 0) return message;
634
+ return {
635
+ ...message,
636
+ meta: { ...message.meta ?? {}, replied: true }
637
+ };
638
+ });
673
639
  }
674
640
 
675
- // src/chat/state/turn-session.ts
676
- var AGENT_TURN_SESSION_INDEX_KEY = `${AGENT_TURN_SESSION_PREFIX}:index`;
677
- var AGENT_TURN_SESSION_INDEX_MAX_LENGTH = 5e3;
678
- var AGENT_TURN_SESSION_TTL_MS = THREAD_STATE_TTL_MS;
679
- var agentTurnSessionStatusSchema = z2.enum([
680
- "running",
681
- "awaiting_resume",
682
- "completed",
683
- "failed",
684
- "abandoned"
685
- ]);
686
- var agentTurnSurfaceSchema = z2.enum([
687
- "slack",
688
- "api",
689
- "scheduler",
690
- "internal"
691
- ]);
692
- var agentTurnResumeReasonSchema = z2.enum([
693
- "timeout",
694
- "auth",
695
- "yield",
696
- "retry"
697
- ]);
698
- var nonNegativeNumberSchema = z2.number().finite().nonnegative();
699
- var seqCursorSchema = z2.number().int().min(-1);
700
- var agentTurnSessionSummarySchema = z2.object({
701
- channelName: z2.string().min(1).optional(),
702
- version: z2.number().int().nonnegative(),
703
- conversationId: z2.string().min(1),
704
- cumulativeDurationMs: nonNegativeNumberSchema,
705
- cumulativeUsage: agentTurnUsageSchema.optional(),
706
- destination: destinationSchema.optional(),
707
- dispatchId: z2.string().min(1).optional(),
708
- dispatchOutcome: z2.enum(["blocked", "completed", "failed"]).optional(),
709
- resultMessageId: z2.string().min(1).optional(),
710
- source: sourceSchema.optional(),
711
- lastProgressAtMs: nonNegativeNumberSchema,
712
- loadedSkillNames: z2.array(z2.string()).optional(),
713
- modelId: z2.string().min(1).optional(),
714
- reasoningLevel: z2.string().min(1).optional(),
715
- actor: actorSchema.optional(),
716
- resumeReason: agentTurnResumeReasonSchema.optional(),
717
- resumedFromSliceId: z2.number().int().nonnegative().optional(),
718
- sessionId: z2.string().min(1),
719
- sliceId: z2.number().int().nonnegative(),
720
- startedAtMs: nonNegativeNumberSchema,
721
- state: agentTurnSessionStatusSchema,
722
- surface: agentTurnSurfaceSchema.optional(),
723
- traceId: z2.string().optional(),
724
- updatedAtMs: nonNegativeNumberSchema
725
- }).strict();
726
- var storedAgentTurnSessionRecordSchema = agentTurnSessionSummarySchema.extend({
727
- actors: z2.array(actorSchema).optional(),
728
- committedSeq: seqCursorSchema,
729
- historyVersion: z2.number().int().nonnegative().optional(),
730
- errorMessage: z2.string().optional(),
731
- turnStartSeq: seqCursorSchema.optional(),
732
- runtimeContext: z2.array(piMessageSchema).optional()
733
- }).strict();
734
- function conversationExecutionFromSummary(summary) {
735
- const status = summary.state === "completed" || summary.state === "abandoned" ? "idle" : summary.state;
641
+ // src/chat/conversations/messages.ts
642
+ function toStoredConversationMessage(message) {
643
+ const meta = {};
644
+ if (message.author) meta.author = message.author;
645
+ const { replied, ...restMeta } = message.meta ?? {};
646
+ Object.assign(meta, restMeta);
647
+ if (replied === false) meta.replied = false;
736
648
  return {
737
- status,
738
- runId: summary.sessionId,
739
- updatedAtMs: summary.updatedAtMs
649
+ messageId: message.id,
650
+ role: message.role,
651
+ text: message.text,
652
+ ...Object.keys(meta).length > 0 ? { meta } : {},
653
+ createdAtMs: message.createdAtMs
740
654
  };
741
655
  }
742
- function sessionLogActor(actor) {
743
- return actor?.platform === "slack" ? toStoredSlackActor(actor) : void 0;
744
- }
745
- function parseAgentTurnSessionRecord(value) {
746
- return storedAgentTurnSessionRecordSchema.parse(value);
656
+ function canonicalJson(value) {
657
+ if (Array.isArray(value)) return value.map(canonicalJson);
658
+ if (!value || typeof value !== "object") return value;
659
+ return Object.fromEntries(
660
+ Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalJson(item)])
661
+ );
747
662
  }
748
- function parseAgentTurnSessionSummary(value) {
749
- return agentTurnSessionSummarySchema.parse(value);
663
+ function messageStateKey(message) {
664
+ return createHash("sha256").update(JSON.stringify(canonicalJson(message))).digest("hex").slice(0, 32);
750
665
  }
751
- async function appendAgentTurnSessionSummary(summary, ttlMs) {
752
- const stateAdapter = getStateAdapter();
753
- await Promise.all([
754
- stateAdapter.appendToList(AGENT_TURN_SESSION_INDEX_KEY, summary, {
755
- maxLength: AGENT_TURN_SESSION_INDEX_MAX_LENGTH,
756
- ttlMs
757
- }),
758
- stateAdapter.appendToList(
759
- agentTurnSessionConversationIndexKey(summary.conversationId),
760
- summary,
761
- { ttlMs }
762
- )
763
- ]);
666
+ async function hydrateConversationMessages(args) {
667
+ if (!args.conversationId) {
668
+ args.conversation.messages = [];
669
+ return;
670
+ }
671
+ const history = await getConversationEventStore().loadMessageHistory(
672
+ args.conversationId
673
+ );
674
+ args.conversation.compactions = history.compaction ? projectConversationMessageSummaries([history.compaction]) : [];
675
+ args.conversation.messages = projectConversationMessages(history);
676
+ updateConversationStats(args.conversation);
764
677
  }
765
- async function recordConversationActivityMetadata(args) {
766
- const conversationStore = args.conversationStore ?? getConversationStore();
767
- const source = args.summary.destination?.platform === "local" ? "local" : args.summary.surface;
768
- await conversationStore.recordActivity({
769
- activityAtMs: args.summary.updatedAtMs,
770
- channelName: args.summary.channelName,
771
- conversationId: args.summary.conversationId,
772
- destination: args.summary.destination,
773
- nowMs: args.nowMs,
774
- actor: sessionLogActor(args.summary.actor),
775
- source,
776
- visibility: args.destinationVisibility
678
+ async function persistConversationMessages(args) {
679
+ if (!args.conversationId || args.conversation.messages.length === 0) return;
680
+ await appendConversationMessages(getConversationEventStore(), {
681
+ ...args,
682
+ conversationId: args.conversationId
777
683
  });
778
- await conversationStore.recordExecution({
779
- channelName: args.summary.channelName,
780
- conversationId: args.summary.conversationId,
781
- createdAtMs: args.summary.startedAtMs,
782
- destination: args.summary.destination,
783
- execution: conversationExecutionFromSummary(args.summary),
784
- lastActivityAtMs: args.summary.updatedAtMs,
785
- metrics: {
786
- durationMs: args.summary.cumulativeDurationMs,
787
- ...args.summary.cumulativeUsage ? { usage: args.summary.cumulativeUsage } : {}
788
- },
789
- actor: sessionLogActor(args.summary.actor),
790
- source,
791
- updatedAtMs: args.nowMs,
792
- visibility: args.destinationVisibility
684
+ }
685
+ async function appendConversationMessages(eventStore, args) {
686
+ if (args.conversation.messages.length === 0) return;
687
+ const history = await eventStore.loadMessageHistory(args.conversationId);
688
+ const existingMessages = new Map(
689
+ projectConversationMessages(history).map((message) => [
690
+ message.id,
691
+ message
692
+ ])
693
+ );
694
+ const handledAtMs = args.repliedAtMs ?? Date.now();
695
+ const events = args.conversation.messages.flatMap((message) => {
696
+ const stored = toStoredConversationMessage(message);
697
+ const existing = existingMessages.get(stored.messageId);
698
+ const changed = existing !== void 0 && !isDeepStrictEqual2(toStoredConversationMessage(existing), stored);
699
+ const messageEvent = existing ? changed ? [
700
+ {
701
+ idempotencyKey: `message:${stored.messageId}:update:${messageStateKey(stored)}`,
702
+ createdAtMs: handledAtMs,
703
+ data: {
704
+ type: "message_updated",
705
+ messageId: stored.messageId,
706
+ role: stored.role,
707
+ text: stored.text,
708
+ ...stored.authorIdentityId ? { authorIdentityId: stored.authorIdentityId } : {},
709
+ ...stored.meta ? { meta: stored.meta } : {}
710
+ }
711
+ }
712
+ ] : [] : [
713
+ {
714
+ idempotencyKey: `message:${stored.messageId}`,
715
+ createdAtMs: stored.createdAtMs,
716
+ data: {
717
+ type: "message",
718
+ messageId: stored.messageId,
719
+ role: stored.role,
720
+ text: stored.text,
721
+ ...stored.authorIdentityId ? { authorIdentityId: stored.authorIdentityId } : {},
722
+ ...stored.meta ? { meta: stored.meta } : {}
723
+ }
724
+ }
725
+ ];
726
+ return [
727
+ ...messageEvent,
728
+ ...message.meta?.replied === true ? [
729
+ {
730
+ idempotencyKey: `message:${stored.messageId}:handled`,
731
+ createdAtMs: handledAtMs,
732
+ data: {
733
+ type: "message_handled",
734
+ messageId: stored.messageId
735
+ }
736
+ }
737
+ ] : []
738
+ ];
793
739
  });
740
+ await eventStore.append(args.conversationId, events);
794
741
  }
795
- function materializeAgentTurnSessionRecord(stored, piProjection, turnStartMessageIndex, restoreVolatileContext = true) {
796
- const piMessages = restoreVolatileContext && (stored.state === "running" || stored.state === "awaiting_resume") ? restoreRuntimeContext(piProjection.messages, stored.runtimeContext) : piProjection.messages;
797
- return {
798
- version: stored.version,
799
- ...stored.channelName ? { channelName: stored.channelName } : {},
800
- conversationId: stored.conversationId,
801
- sessionId: stored.sessionId,
802
- sliceId: stored.sliceId,
803
- state: stored.state,
804
- startedAtMs: stored.startedAtMs,
805
- lastProgressAtMs: stored.lastProgressAtMs,
806
- updatedAtMs: stored.updatedAtMs,
807
- piMessages,
808
- piMessageProvenance: piProjection.provenance,
809
- actors: stored.actors ?? instructionActors(piProjection.provenance),
810
- cumulativeDurationMs: stored.cumulativeDurationMs,
811
- ...stored.destination ? { destination: stored.destination } : {},
812
- ...stored.dispatchId ? { dispatchId: stored.dispatchId } : {},
813
- ...stored.dispatchOutcome ? { dispatchOutcome: stored.dispatchOutcome } : {},
814
- ...stored.resultMessageId ? { resultMessageId: stored.resultMessageId } : {},
815
- ...stored.source ? { source: stored.source } : {},
816
- ...stored.cumulativeUsage ? { cumulativeUsage: stored.cumulativeUsage } : {},
817
- ...stored.resumeReason ? { resumeReason: stored.resumeReason } : {},
818
- ...stored.errorMessage ? { errorMessage: stored.errorMessage } : {},
819
- ...stored.loadedSkillNames ? { loadedSkillNames: stored.loadedSkillNames } : {},
820
- ...stored.modelId ? { modelId: stored.modelId } : {},
821
- ...stored.reasoningLevel ? { reasoningLevel: stored.reasoningLevel } : {},
822
- ...stored.actor ? { actor: stored.actor } : {},
823
- ...stored.resumedFromSliceId !== void 0 ? { resumedFromSliceId: stored.resumedFromSliceId } : {},
824
- ...stored.surface ? { surface: stored.surface } : {},
825
- ...stored.traceId ? { traceId: stored.traceId } : {},
826
- ...turnStartMessageIndex !== void 0 ? { turnStartMessageIndex } : {}
827
- };
742
+
743
+ // src/chat/conversations/projection.ts
744
+ import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
745
+
746
+ // src/chat/pi/messages.ts
747
+ import { z } from "zod";
748
+ var piMessageSchema = z.object({
749
+ role: z.string()
750
+ }).passthrough().transform((value) => value);
751
+ var piContentMessageSchema = z.object({
752
+ content: z.array(z.unknown()),
753
+ role: z.string().min(1)
754
+ }).passthrough().transform((value) => value);
755
+
756
+ // src/chat/pi/conversation-events.ts
757
+ function authorizationObservationMessage(data, createdAtMs) {
758
+ const label = data.kind === "mcp" ? "MCP authorization" : "Authorization";
759
+ return piMessageSchema.parse({
760
+ role: "user",
761
+ content: [
762
+ {
763
+ type: "text",
764
+ text: `${label} completed for provider "${data.provider}". Continue the blocked request and retry the provider operation if needed.`
765
+ }
766
+ ],
767
+ timestamp: createdAtMs
768
+ });
828
769
  }
829
- function restoreRuntimeContext(messages, runtimeContext) {
830
- if (!runtimeContext || runtimeContext.length === 0) return messages;
831
- const restored = [...messages];
832
- const unmatchedRuntimeContext = [];
833
- for (const runtimeMessage of runtimeContext) {
834
- const runtime = runtimeMessage;
835
- const targetIndex = restored.findIndex((message) => {
836
- const candidate = message;
837
- return candidate.role === "user" && candidate.timestamp === runtime.timestamp;
770
+ function durableMessages(message) {
771
+ return stripRuntimeTurnContext([message]);
772
+ }
773
+ function historyItemFromPiMessage(message, provenance) {
774
+ const parsed = piMessageSchema.parse(message);
775
+ const { role, ...fields } = parsed;
776
+ if (role === "user") {
777
+ return agentHistoryItemSchema.parse({
778
+ ...fields,
779
+ type: "user_message",
780
+ provenance
838
781
  });
839
- if (targetIndex < 0) {
840
- unmatchedRuntimeContext.push(runtimeMessage);
841
- continue;
842
- }
843
- const target = restored[targetIndex];
844
- restored[targetIndex] = {
845
- ...restored[targetIndex],
846
- content: [
847
- ...Array.isArray(runtime.content) ? runtime.content : [],
848
- ...Array.isArray(target.content) ? target.content : []
849
- ]
850
- };
851
782
  }
852
- return [...unmatchedRuntimeContext, ...restored];
853
- }
854
- async function getStoredAgentTurnSessionRecord(conversationId, sessionId) {
855
- const stateAdapter = getStateAdapter();
856
- await stateAdapter.connect();
857
- const value = await stateAdapter.get(
858
- agentTurnSessionKey(conversationId, sessionId)
859
- );
860
- return value == null ? void 0 : parseAgentTurnSessionRecord(value);
861
- }
862
- async function materializeStoredAgentTurnSessionRecord(conversationId, sessionId, followCurrentReplacement) {
863
- const parsed = await getStoredAgentTurnSessionRecord(
864
- conversationId,
865
- sessionId
866
- );
867
- if (!parsed) {
868
- return void 0;
783
+ if (role === "assistant") {
784
+ return agentHistoryItemSchema.parse({
785
+ ...fields,
786
+ type: "assistant_message"
787
+ });
869
788
  }
870
- const pinnedProjection = await loadTurnProjection({
871
- conversationId,
872
- committedSeq: parsed.committedSeq,
873
- // Unfinished records include the current-epoch tail so parked input
874
- // appended after the last safe boundary stays model-visible on resume.
875
- includeTail: parsed.state === "running" || parsed.state === "awaiting_resume"
876
- });
877
- if (!pinnedProjection) {
878
- return void 0;
789
+ if (role === "toolResult") {
790
+ return agentHistoryItemSchema.parse({
791
+ ...fields,
792
+ type: "tool_result"
793
+ });
879
794
  }
880
- const currentHistory = await getConversationEventStore().loadCurrentHistory(conversationId);
881
- const currentHistoryVersion = currentHistory.at(-1)?.historyVersion ?? parsed.historyVersion ?? 0;
882
- const followsReplacement = followCurrentReplacement && (parsed.state === "running" || parsed.state === "awaiting_resume") && parsed.historyVersion !== void 0 && parsed.historyVersion !== currentHistoryVersion;
883
- const piProjection = followsReplacement ? projectConversationEvents(currentHistory) : pinnedProjection;
884
- const turnStartMessageIndex = followsReplacement ? 0 : parsed.turnStartSeq === void 0 ? void 0 : piProjection.seqs.filter((seq) => seq <= parsed.turnStartSeq).length;
885
- return materializeAgentTurnSessionRecord(
886
- parsed,
887
- piProjection,
888
- turnStartMessageIndex,
889
- !followsReplacement && (parsed.historyVersion === void 0 || parsed.historyVersion === currentHistoryVersion)
890
- );
795
+ throw new Error(`Unsupported durable agent message role "${role}"`);
891
796
  }
892
- async function getAgentTurnSessionRecord(conversationId, sessionId) {
893
- return await materializeStoredAgentTurnSessionRecord(
894
- conversationId,
895
- sessionId,
896
- false
897
- );
797
+ function piMessageFromHistoryItem(data) {
798
+ if (data.type === "user_message") {
799
+ const { type: _2, provenance: __, ...fields2 } = data;
800
+ return piMessageSchema.parse({ ...fields2, role: "user" });
801
+ }
802
+ if (data.type === "assistant_message") {
803
+ const { type: _2, ...fields2 } = data;
804
+ return piMessageSchema.parse({ ...fields2, role: "assistant" });
805
+ }
806
+ const { type: _, ...fields } = data;
807
+ return piMessageSchema.parse({ ...fields, role: "toolResult" });
898
808
  }
899
- async function getAgentTurnSessionRecordForResume(conversationId, sessionId) {
900
- return await materializeStoredAgentTurnSessionRecord(
901
- conversationId,
902
- sessionId,
903
- true
904
- );
809
+ function historyItemProvenance(data) {
810
+ return data.type === "user_message" ? data.provenance : contextProvenance;
905
811
  }
906
- function buildStoredRecord(args) {
907
- const nowMs = Date.now();
908
- return {
909
- version: (args.previousVersion ?? 0) + 1,
910
- ...args.channelName ? { channelName: args.channelName } : {},
911
- conversationId: args.conversationId,
912
- sessionId: args.sessionId,
913
- sliceId: args.sliceId,
914
- state: args.state,
915
- startedAtMs: args.startedAtMs ?? nowMs,
916
- lastProgressAtMs: args.lastProgressAtMs ?? nowMs,
917
- updatedAtMs: nowMs,
918
- committedSeq: args.committedSeq,
919
- ...args.historyVersion !== void 0 ? { historyVersion: args.historyVersion } : {},
920
- ...args.turnStartSeq !== void 0 ? { turnStartSeq: args.turnStartSeq } : {},
921
- ...args.runtimeContext && args.runtimeContext.length > 0 ? { runtimeContext: args.runtimeContext } : {},
922
- cumulativeDurationMs: args.cumulativeDurationMs,
923
- ...args.cumulativeUsage ? { cumulativeUsage: args.cumulativeUsage } : {},
924
- ...args.destination ? { destination: args.destination } : {},
925
- ...args.dispatchId ? { dispatchId: args.dispatchId } : {},
926
- ...args.dispatchOutcome ? { dispatchOutcome: args.dispatchOutcome } : {},
927
- ...args.resultMessageId ? { resultMessageId: args.resultMessageId } : {},
928
- ...args.source ? { source: args.source } : {},
929
- ...args.actor ? { actor: args.actor } : {},
930
- ...args.actors ? { actors: args.actors } : {},
931
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
932
- ...args.modelId ? { modelId: args.modelId } : {},
933
- ...args.reasoningLevel ? { reasoningLevel: args.reasoningLevel } : {},
934
- ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
935
- ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
936
- ...args.resumedFromSliceId !== void 0 ? { resumedFromSliceId: args.resumedFromSliceId } : {},
937
- ...args.surface ? { surface: args.surface } : {},
938
- ...args.traceId ? { traceId: args.traceId } : {}
939
- };
812
+ function projectConversationEvents(events, options) {
813
+ const messages = [];
814
+ const provenance = [];
815
+ const seqs = [];
816
+ let modelProfile = "standard";
817
+ let modelId;
818
+ for (const event of events) {
819
+ if (options?.maxSeq !== void 0 && event.seq > options.maxSeq) break;
820
+ if (event.data.type === "unknown") {
821
+ throw new Error(
822
+ `Unsupported conversation event "${event.data.originalType}" at seq ${event.seq} (schema version ${event.schemaVersion})`
823
+ );
824
+ }
825
+ if (event.data.type === "compaction" || event.data.type === "handoff") {
826
+ modelProfile = event.data.modelProfile;
827
+ modelId = event.data.modelId;
828
+ for (const replacement of event.data.replacementHistory) {
829
+ for (const message of durableMessages(
830
+ piMessageFromHistoryItem(replacement.item)
831
+ )) {
832
+ messages.push(message);
833
+ provenance.push(historyItemProvenance(replacement.item));
834
+ seqs.push(replacement.sourceEventSeq ?? event.seq);
835
+ }
836
+ }
837
+ continue;
838
+ }
839
+ if (event.data.type === "user_message" || event.data.type === "assistant_message" || event.data.type === "tool_result") {
840
+ for (const message of durableMessages(
841
+ piMessageFromHistoryItem(event.data)
842
+ )) {
843
+ messages.push(message);
844
+ provenance.push(historyItemProvenance(event.data));
845
+ seqs.push(event.seq);
846
+ }
847
+ continue;
848
+ }
849
+ if (event.data.type === "authorization_completed") {
850
+ messages.push(
851
+ authorizationObservationMessage(event.data, event.createdAtMs)
852
+ );
853
+ provenance.push(contextProvenance);
854
+ seqs.push(event.seq);
855
+ }
856
+ }
857
+ return { messages, provenance, seqs, modelProfile, modelId };
940
858
  }
941
- async function setStoredRecord(args) {
942
- const stateAdapter = getStateAdapter();
943
- await stateAdapter.connect();
944
- await recordConversationActivityMetadata({
945
- conversationStore: args.conversationStore,
946
- destinationVisibility: args.destinationVisibility,
947
- nowMs: Date.now(),
948
- summary: args.record
949
- });
950
- await stateAdapter.set(
951
- agentTurnSessionKey(args.record.conversationId, args.record.sessionId),
952
- args.record,
953
- args.ttlMs
859
+
860
+ // src/chat/conversations/projection.ts
861
+ function connectedMcpProvidersFromEvents(events) {
862
+ const providers = /* @__PURE__ */ new Set();
863
+ for (const event of events) {
864
+ if (event.data.type === "mcp_provider_connected") {
865
+ providers.add(event.data.provider);
866
+ }
867
+ }
868
+ return [...providers].sort((left, right) => left.localeCompare(right));
869
+ }
870
+ function isUserMessage(message) {
871
+ return message.role === "user";
872
+ }
873
+ function countMatchingPrefix(left, right) {
874
+ const limit = Math.min(left.length, right.length);
875
+ for (let index = 0; index < limit; index += 1) {
876
+ if (!isDeepStrictEqual3(left[index], right[index])) {
877
+ return index;
878
+ }
879
+ }
880
+ return limit;
881
+ }
882
+ function normalizeDurableMessage(message) {
883
+ return piMessageSchema.parse(
884
+ JSON.parse(JSON.stringify(sanitizePostgresJson(message)))
954
885
  );
955
- const {
956
- actors: _actors,
957
- committedSeq: _committedSeq,
958
- historyVersion: _historyVersion,
959
- errorMessage: _errorMessage,
960
- turnStartSeq: _turnStartSeq,
961
- runtimeContext: _runtimeContext,
962
- ...summary
963
- } = args.record;
964
- await appendAgentTurnSessionSummary(summary, args.ttlMs);
965
- return materializeAgentTurnSessionRecord(
966
- args.record,
967
- {
968
- messages: [...args.piMessages],
969
- provenance: [...args.piMessageProvenance]
970
- },
971
- args.turnStartMessageIndex
886
+ }
887
+ function resolveCommitProvenance(args) {
888
+ if (args.explicitProvenance) {
889
+ if (args.explicitProvenance.length !== args.nextMessages.length) {
890
+ throw new Error("commit provenance must align one-to-one with messages");
891
+ }
892
+ return args.explicitProvenance;
893
+ }
894
+ if (args.existing.provenance.length !== args.existing.messages.length) {
895
+ throw new Error("committed provenance must align one-to-one with messages");
896
+ }
897
+ const matchingPrefix = args.matchingPrefix;
898
+ const provenance = args.nextMessages.map(
899
+ (_, index) => index < matchingPrefix ? args.existing.provenance[index] : contextProvenance
900
+ );
901
+ if (args.newMessageProvenance) {
902
+ for (let index = args.nextMessages.length - 1; index >= matchingPrefix; index -= 1) {
903
+ if (isUserMessage(args.nextMessages[index])) {
904
+ provenance[index] = args.newMessageProvenance;
905
+ break;
906
+ }
907
+ }
908
+ }
909
+ if (args.trailingMessageProvenance) {
910
+ if (args.trailingMessageProvenance.length > provenance.length) {
911
+ throw new Error(
912
+ "trailing commit provenance cannot exceed committed messages"
913
+ );
914
+ }
915
+ const newMessageCount = args.nextMessages.length - matchingPrefix;
916
+ if (args.trailingMessageProvenance.length > newMessageCount) {
917
+ throw new Error(
918
+ "trailing commit provenance must align to newly committed messages"
919
+ );
920
+ }
921
+ const start = provenance.length - args.trailingMessageProvenance.length;
922
+ args.trailingMessageProvenance.forEach((entry, offset) => {
923
+ provenance[start + offset] = entry;
924
+ });
925
+ }
926
+ return provenance;
927
+ }
928
+ async function loadProjection(args) {
929
+ const events = await getConversationEventStore().loadCurrentHistory(
930
+ args.conversationId
972
931
  );
932
+ return projectConversationEvents(events).messages;
973
933
  }
974
- async function updateAgentTurnSessionState(args) {
975
- const parsed = await getStoredAgentTurnSessionRecord(
976
- args.existing.conversationId,
977
- args.existing.sessionId
934
+ async function loadConversationProjection(args) {
935
+ const events = await getConversationEventStore().loadCurrentHistory(
936
+ args.conversationId
978
937
  );
979
- if (!parsed || parsed.version !== args.existing.version) {
938
+ return projectConversationEvents(events);
939
+ }
940
+ async function openConversationProjection(args) {
941
+ const events = await getConversationEventStore().loadCurrentHistory(
942
+ args.conversationId
943
+ );
944
+ return projectConversationEvents(events);
945
+ }
946
+ async function loadTurnProjection(args) {
947
+ const eventStore = getConversationEventStore();
948
+ if (args.committedSeq < 0) {
949
+ return projectConversationEvents(
950
+ await eventStore.loadCurrentHistory(args.conversationId)
951
+ );
952
+ }
953
+ const historyEvents = await eventStore.loadHistoryContaining(
954
+ args.conversationId,
955
+ args.committedSeq,
956
+ args.includeTail ? void 0 : args.committedSeq
957
+ );
958
+ if (!historyEvents) {
980
959
  return void 0;
981
960
  }
982
- return await setStoredRecord({
983
- piMessages: args.existing.piMessages,
984
- piMessageProvenance: args.existing.piMessageProvenance,
985
- ttlMs: AGENT_TURN_SESSION_TTL_MS,
986
- ...args.existing.turnStartMessageIndex !== void 0 ? { turnStartMessageIndex: args.existing.turnStartMessageIndex } : {},
987
- record: buildStoredRecord({
988
- conversationId: args.existing.conversationId,
989
- sessionId: args.existing.sessionId,
990
- sliceId: args.existing.sliceId,
991
- state: args.state,
992
- committedSeq: parsed.committedSeq,
993
- ...parsed.historyVersion !== void 0 ? { historyVersion: parsed.historyVersion } : {},
994
- ...parsed.turnStartSeq !== void 0 ? { turnStartSeq: parsed.turnStartSeq } : {},
995
- ...parsed.runtimeContext ? { runtimeContext: parsed.runtimeContext } : {},
996
- ...parsed.channelName ? { channelName: parsed.channelName } : {},
997
- startedAtMs: parsed.startedAtMs,
998
- lastProgressAtMs: parsed.lastProgressAtMs,
999
- previousVersion: parsed.version,
1000
- cumulativeDurationMs: args.existing.cumulativeDurationMs,
1001
- ...args.existing.cumulativeUsage ? { cumulativeUsage: args.existing.cumulativeUsage } : {},
1002
- ...args.existing.destination ? { destination: args.existing.destination } : {},
1003
- ...args.existing.dispatchId ? { dispatchId: args.existing.dispatchId } : {},
1004
- ...args.existing.dispatchOutcome ? { dispatchOutcome: args.existing.dispatchOutcome } : {},
1005
- ...args.existing.resultMessageId ? { resultMessageId: args.existing.resultMessageId } : {},
1006
- ...args.existing.source ? { source: args.existing.source } : {},
1007
- ...args.existing.loadedSkillNames ? { loadedSkillNames: args.existing.loadedSkillNames } : {},
1008
- ...args.existing.modelId ? { modelId: args.existing.modelId } : {},
1009
- ...args.existing.reasoningLevel ? { reasoningLevel: args.existing.reasoningLevel } : {},
1010
- ...args.existing.actor ? { actor: args.existing.actor } : {},
1011
- actors: args.existing.actors,
1012
- ...args.existing.resumeReason ? { resumeReason: args.existing.resumeReason } : {},
1013
- ...args.existing.resumedFromSliceId !== void 0 ? { resumedFromSliceId: args.existing.resumedFromSliceId } : {},
1014
- ...args.existing.surface ? { surface: args.existing.surface } : {},
1015
- ...args.existing.traceId ? { traceId: args.existing.traceId } : {},
1016
- ...args.errorMessage ?? args.existing.errorMessage ? { errorMessage: args.errorMessage ?? args.existing.errorMessage } : {}
1017
- })
1018
- });
961
+ return projectConversationEvents(historyEvents);
1019
962
  }
1020
- async function upsertAgentTurnSessionRecord(args) {
1021
- const existingRecord = await getStoredAgentTurnSessionRecord(
963
+ async function loadConnectedMcpProviders(args) {
964
+ const events = await getConversationEventStore().loadCurrentHistory(
965
+ args.conversationId
966
+ );
967
+ return connectedMcpProvidersFromEvents(events);
968
+ }
969
+ function messageTimestamp(message) {
970
+ const timestamp = message.timestamp;
971
+ return typeof timestamp === "number" ? timestamp : Date.now();
972
+ }
973
+ async function commitMessages(args) {
974
+ const executor = args.executor ?? getSqlExecutor();
975
+ return await withConversationEventLock(
976
+ executor,
1022
977
  args.conversationId,
1023
- args.sessionId
978
+ async () => executor.transaction(
979
+ async () => await commitMessagesLocked(args, executor)
980
+ )
1024
981
  );
1025
- const existingDispatchId = existingRecord?.dispatchId ?? (await listAgentTurnSessionSummariesForConversation(args.conversationId)).find((summary) => summary.sessionId === args.sessionId)?.dispatchId;
1026
- if (existingDispatchId && args.dispatchId && existingDispatchId !== args.dispatchId) {
1027
- throw new Error(
1028
- `Turn session ${args.sessionId} dispatchId cannot be changed`
1029
- );
1030
- }
1031
- const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
1032
- const instructionActor = args.actor ?? existingRecord?.actor;
1033
- const commit = await commitMessages({
1034
- conversationId: args.conversationId,
1035
- messages: args.piMessages,
1036
- ...instructionActor ? { newMessageProvenance: instructionProvenanceFor(instructionActor) } : {},
1037
- ...args.trailingMessageProvenance ? { trailingMessageProvenance: args.trailingMessageProvenance } : {},
1038
- ...args.turnContexts && args.turnContexts.length > 0 ? {
1039
- turnContext: {
1040
- contexts: args.turnContexts,
1041
- turnId: args.sessionId
1042
- }
1043
- } : {}
1044
- });
1045
- const durableTurnStartMessageIndex = args.turnStartMessageIndex === void 0 ? void 0 : stripRuntimeTurnContext(
1046
- args.piMessages.slice(0, args.turnStartMessageIndex)
1047
- ).length;
1048
- const runtimeContext = retainRuntimeTurnContext(args.piMessages);
1049
- const retainedRuntimeContext = runtimeContext.length > 0 ? runtimeContext : existingRecord?.historyVersion === commit.historyVersion ? existingRecord.runtimeContext : void 0;
1050
- const turnStartSeq = durableTurnStartMessageIndex === void 0 ? existingRecord?.turnStartSeq : durableTurnStartMessageIndex <= 0 ? -1 : commit.messageSeqs[durableTurnStartMessageIndex - 1] ?? commit.committedSeq;
1051
- const turnStartMessageIndex = durableTurnStartMessageIndex ?? (turnStartSeq === void 0 ? void 0 : commit.messageSeqs.filter((seq) => seq <= turnStartSeq).length);
1052
- return await setStoredRecord({
1053
- conversationStore: args.conversationStore,
1054
- destinationVisibility: args.destinationVisibility,
1055
- piMessages: commit.messages,
1056
- piMessageProvenance: commit.provenance,
1057
- ttlMs,
1058
- ...turnStartMessageIndex !== void 0 ? { turnStartMessageIndex } : {},
1059
- record: buildStoredRecord({
1060
- ...args.channelName ?? existingRecord?.channelName ? { channelName: args.channelName ?? existingRecord?.channelName } : {},
1061
- conversationId: args.conversationId,
1062
- sessionId: args.sessionId,
1063
- sliceId: args.sliceId,
1064
- state: args.state,
1065
- ...existingRecord?.startedAtMs !== void 0 ? { startedAtMs: existingRecord.startedAtMs } : {},
1066
- ...args.lastProgressAtMs !== void 0 ? { lastProgressAtMs: args.lastProgressAtMs } : {},
1067
- committedSeq: commit.committedSeq,
1068
- historyVersion: commit.historyVersion,
1069
- ...turnStartSeq !== void 0 ? { turnStartSeq } : {},
1070
- ...retainedRuntimeContext ? { runtimeContext: retainedRuntimeContext } : {},
1071
- previousVersion: existingRecord?.version,
1072
- cumulativeDurationMs: args.cumulativeDurationMs ?? existingRecord?.cumulativeDurationMs ?? 0,
1073
- ...args.cumulativeUsage ? { cumulativeUsage: args.cumulativeUsage } : {},
1074
- ...args.destination ?? existingRecord?.destination ? { destination: args.destination ?? existingRecord?.destination } : {},
1075
- ...args.dispatchId ?? existingRecord?.dispatchId ? { dispatchId: args.dispatchId ?? existingRecord?.dispatchId } : {},
1076
- ...args.dispatchOutcome ?? existingRecord?.dispatchOutcome ? {
1077
- dispatchOutcome: args.dispatchOutcome ?? existingRecord?.dispatchOutcome
1078
- } : {},
1079
- ...args.resultMessageId ?? existingRecord?.resultMessageId ? {
1080
- resultMessageId: args.resultMessageId ?? existingRecord?.resultMessageId
1081
- } : {},
1082
- ...args.source ?? existingRecord?.source ? { source: args.source ?? existingRecord?.source } : {},
1083
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
1084
- modelId: args.modelId,
1085
- ...args.reasoningLevel ?? existingRecord?.reasoningLevel ? {
1086
- reasoningLevel: args.reasoningLevel ?? existingRecord?.reasoningLevel
1087
- } : {},
1088
- ...args.actor ?? existingRecord?.actor ? { actor: args.actor ?? existingRecord?.actor } : {},
1089
- actors: instructionActors([
1090
- ...(existingRecord?.actors ?? []).map(instructionProvenanceFor),
1091
- ...(args.actors ?? []).map(instructionProvenanceFor),
1092
- ...commit.provenance
1093
- ]),
1094
- ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
1095
- ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
1096
- ...args.resumedFromSliceId !== void 0 ? { resumedFromSliceId: args.resumedFromSliceId } : {},
1097
- ...args.surface ?? existingRecord?.surface ? { surface: args.surface ?? existingRecord?.surface } : {},
1098
- ...args.traceId ?? existingRecord?.traceId ? { traceId: args.traceId ?? existingRecord?.traceId } : {}
1099
- })
1100
- });
1101
982
  }
1102
- async function recordAgentTurnSessionSummary(args) {
1103
- const stored = await getStoredAgentTurnSessionRecord(
983
+ async function commitAcceptedReply(args) {
984
+ const executor = getSqlExecutor();
985
+ await withConversationEventLock(
986
+ executor,
1104
987
  args.conversationId,
1105
- args.sessionId
1106
- );
1107
- const priorSummary = (await listAgentTurnSessionSummariesForConversation(args.conversationId)).find((summary2) => summary2.sessionId === args.sessionId);
1108
- const existing = stored ?? priorSummary;
1109
- const existingDispatchId = existing?.dispatchId;
1110
- const existingDispatchOutcome = priorSummary?.dispatchOutcome ?? stored?.dispatchOutcome;
1111
- const existingResultMessageId = priorSummary?.resultMessageId ?? stored?.resultMessageId;
1112
- if (existingDispatchId && args.dispatchId && existingDispatchId !== args.dispatchId) {
1113
- throw new Error(
1114
- `Turn session ${args.sessionId} dispatchId cannot be changed`
1115
- );
1116
- }
1117
- const nowMs = Date.now();
1118
- const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
1119
- const summary = {
1120
- version: existing?.version ?? 0,
1121
- ...args.channelName ?? existing?.channelName ? { channelName: args.channelName ?? existing?.channelName } : {},
1122
- conversationId: args.conversationId,
1123
- sessionId: args.sessionId,
1124
- sliceId: args.sliceId,
1125
- startedAtMs: existing?.startedAtMs ?? args.startedAtMs ?? nowMs,
1126
- lastProgressAtMs: args.lastProgressAtMs ?? nowMs,
1127
- state: args.state,
1128
- updatedAtMs: nowMs,
1129
- cumulativeDurationMs: args.cumulativeDurationMs ?? existing?.cumulativeDurationMs ?? 0,
1130
- ...args.cumulativeUsage ?? existing?.cumulativeUsage ? { cumulativeUsage: args.cumulativeUsage ?? existing?.cumulativeUsage } : {},
1131
- ...args.destination ?? existing?.destination ? { destination: args.destination ?? existing?.destination } : {},
1132
- ...args.dispatchId ?? existingDispatchId ? { dispatchId: args.dispatchId ?? existingDispatchId } : {},
1133
- ...args.dispatchOutcome ?? existingDispatchOutcome ? { dispatchOutcome: args.dispatchOutcome ?? existingDispatchOutcome } : {},
1134
- ...args.resultMessageId ?? existingResultMessageId ? { resultMessageId: args.resultMessageId ?? existingResultMessageId } : {},
1135
- ...args.source ?? existing?.source ? { source: args.source ?? existing?.source } : {},
1136
- ...args.actor ?? existing?.actor ? { actor: args.actor ?? existing?.actor } : {},
1137
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : existing?.loadedSkillNames ? { loadedSkillNames: existing.loadedSkillNames } : {},
1138
- ...args.modelId ?? existing?.modelId ? { modelId: args.modelId ?? existing?.modelId } : {},
1139
- ...args.reasoningLevel ?? existing?.reasoningLevel ? { reasoningLevel: args.reasoningLevel ?? existing?.reasoningLevel } : {},
1140
- ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
1141
- ...args.surface ?? existing?.surface ? { surface: args.surface ?? existing?.surface } : {},
1142
- ...args.traceId ?? existing?.traceId ? { traceId: args.traceId ?? existing?.traceId } : {}
1143
- };
1144
- await recordConversationActivityMetadata({
1145
- conversationStore: args.conversationStore,
1146
- destinationVisibility: args.destinationVisibility,
1147
- nowMs,
1148
- summary
1149
- });
1150
- await appendAgentTurnSessionSummary(summary, ttlMs);
1151
- }
1152
- async function readAgentTurnSessionSummariesFromIndex(key, stateAdapter) {
1153
- await stateAdapter.connect();
1154
- const values = await stateAdapter.getList(key);
1155
- const summaries = /* @__PURE__ */ new Map();
1156
- for (const value of [...values].reverse()) {
1157
- let summary;
1158
- try {
1159
- summary = parseAgentTurnSessionSummary(value);
1160
- } catch (error) {
1161
- logWarn(
1162
- "agent_turn_session_summary_parse_failed",
1163
- {},
988
+ async () => executor.transaction(async () => {
989
+ const agentMessage = normalizeDurableMessage(args.agentMessage);
990
+ await createSqlConversationEventStore(executor).append(
991
+ args.conversationId,
992
+ [
993
+ {
994
+ idempotencyKey: `message:${args.conversationMessageId}:agent`,
995
+ data: historyItemFromPiMessage(agentMessage, contextProvenance),
996
+ createdAtMs: messageTimestamp(agentMessage)
997
+ }
998
+ ]
999
+ );
1000
+ await appendConversationMessages(
1001
+ createSqlConversationEventStore(executor),
1164
1002
  {
1165
- "app.state.key": key,
1166
- "exception.message": error instanceof Error ? error.message : String(error)
1167
- },
1168
- "Skipping an invalid turn-session summary index entry"
1003
+ conversation: args.conversation,
1004
+ conversationId: args.conversationId,
1005
+ ...args.repliedAtMs === void 0 ? {} : { repliedAtMs: args.repliedAtMs }
1006
+ }
1169
1007
  );
1170
- continue;
1171
- }
1172
- const summaryKey = `${summary.conversationId}:${summary.sessionId}`;
1173
- if (!summaries.has(summaryKey)) {
1174
- summaries.set(summaryKey, summary);
1175
- }
1176
- }
1177
- return [...summaries.values()].sort(
1178
- (left, right) => right.updatedAtMs - left.updatedAtMs
1008
+ })
1179
1009
  );
1180
1010
  }
1181
- async function listAgentTurnSessionSummariesForConversation(conversationId) {
1182
- const stateAdapter = getStateAdapter();
1183
- const summaries = await listBoundedAgentTurnSessionSummariesForConversation(conversationId);
1184
- if (summaries.length > 0) {
1185
- return summaries;
1186
- }
1187
- return (await readAgentTurnSessionSummariesFromIndex(
1188
- AGENT_TURN_SESSION_INDEX_KEY,
1189
- stateAdapter
1190
- )).filter((summary) => summary.conversationId === conversationId);
1191
- }
1192
- async function listBoundedAgentTurnSessionSummariesForConversation(conversationId) {
1193
- return readAgentTurnSessionSummariesFromIndex(
1194
- agentTurnSessionConversationIndexKey(conversationId),
1195
- getStateAdapter()
1011
+ async function commitMessagesLocked(args, executor) {
1012
+ const eventStore = createSqlConversationEventStore(executor);
1013
+ const currentEvents = await eventStore.loadCurrentHistory(
1014
+ args.conversationId
1196
1015
  );
1197
- }
1198
- async function abandonAgentTurnSessionRecord(args) {
1199
- const existing = await getAgentTurnSessionRecord(
1200
- args.conversationId,
1201
- args.sessionId
1016
+ const current = projectConversationEvents(currentEvents);
1017
+ const nextLocalMessages = stripRuntimeTurnContext(args.messages).map(
1018
+ normalizeDurableMessage
1202
1019
  );
1203
- if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned") {
1204
- return void 0;
1205
- }
1206
- return await updateAgentTurnSessionState({
1207
- existing,
1208
- state: "abandoned",
1209
- errorMessage: args.errorMessage ?? existing.errorMessage
1210
- });
1211
- }
1212
- async function failAgentTurnSessionRecord(args) {
1213
- const existing = await getAgentTurnSessionRecord(
1214
- args.conversationId,
1215
- args.sessionId
1020
+ const matchingPrefix = countMatchingPrefix(
1021
+ current.messages,
1022
+ nextLocalMessages
1216
1023
  );
1217
- if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned" || existing.version !== args.expectedVersion) {
1218
- return void 0;
1219
- }
1220
- return await updateAgentTurnSessionState({
1221
- existing,
1222
- state: "failed",
1223
- errorMessage: args.errorMessage ?? existing.errorMessage
1024
+ const nextLocalProvenance = resolveCommitProvenance({
1025
+ existing: current,
1026
+ nextMessages: nextLocalMessages,
1027
+ matchingPrefix,
1028
+ ...args.provenance ? { explicitProvenance: args.provenance } : {},
1029
+ ...args.trailingMessageProvenance ? { trailingMessageProvenance: args.trailingMessageProvenance } : {},
1030
+ ...args.newMessageProvenance ? { newMessageProvenance: args.newMessageProvenance } : {}
1224
1031
  });
1225
- }
1226
-
1227
- // src/chat/services/turn-limit.ts
1228
- var TurnSliceLimitExceededError = class extends Error {
1229
- constructor(maxSlices) {
1230
- super(`Agent turn exceeded execution limit (${maxSlices} slices)`);
1231
- this.name = "TurnSliceLimitExceededError";
1032
+ if (matchingPrefix === current.messages.length) {
1033
+ const newMessages = nextLocalMessages.slice(matchingPrefix);
1034
+ const turnContext = args.turnContext;
1035
+ const turnContextEvents = turnContext?.contexts.map((context, index) => ({
1036
+ idempotencyKey: `turn:${turnContext.turnId}:context:${context.pluginName}:${index}`,
1037
+ createdAtMs: context.loadedAtMs,
1038
+ data: {
1039
+ type: "turn_context",
1040
+ turnId: turnContext.turnId,
1041
+ pluginName: context.pluginName,
1042
+ kind: context.kind,
1043
+ version: context.version,
1044
+ content: context.content
1045
+ }
1046
+ })) ?? [];
1047
+ await eventStore.append(args.conversationId, [
1048
+ ...newMessages.map((message, index) => ({
1049
+ data: historyItemFromPiMessage(
1050
+ message,
1051
+ nextLocalProvenance[matchingPrefix + index]
1052
+ ),
1053
+ createdAtMs: messageTimestamp(message)
1054
+ })),
1055
+ ...turnContextEvents
1056
+ ]);
1057
+ } else {
1058
+ throw new Error(
1059
+ `Agent history for ${args.conversationId} changed before its committed boundary`
1060
+ );
1232
1061
  }
1233
- };
1234
- function buildTurnLimitResponse(eventId) {
1235
- return `I couldn't finish this request because this turn reached its execution limit. Please try again with a smaller or more specific request. Reference: \`event_id=${eventId}\`.`;
1236
- }
1237
-
1238
- // src/chat/services/turn-session-record.ts
1239
- function logSessionRecordError(error, eventName, args, attributes, message) {
1240
- logException(
1241
- error,
1242
- eventName,
1243
- {
1244
- messageConversationId: args.logContext.threadId,
1245
- userId: args.logContext.actorId,
1246
- destinationName: args.logContext.channelId,
1247
- runId: args.logContext.runId,
1248
- assistantUserName: args.logContext.assistantUserName,
1249
- modelId: args.modelId
1250
- },
1251
- {
1252
- "app.ai.resume_conversation_id": args.conversationId,
1253
- "app.ai.resume_session_id": args.sessionId,
1254
- ...attributes
1255
- },
1256
- message
1062
+ const committedEvents = await eventStore.loadCurrentHistory(
1063
+ args.conversationId
1257
1064
  );
1065
+ const committed = projectConversationEvents(committedEvents);
1066
+ return {
1067
+ committedSeq: committedEvents.at(-1)?.seq ?? -1,
1068
+ historyVersion: committedEvents.at(-1)?.historyVersion ?? 0,
1069
+ messageSeqs: committed.seqs,
1070
+ messages: nextLocalMessages,
1071
+ provenance: nextLocalProvenance
1072
+ };
1258
1073
  }
1259
- function addDurationMs(prior, current) {
1260
- const total = [prior, current].reduce((sum, value) => {
1261
- if (typeof value !== "number" || !Number.isFinite(value)) {
1262
- return sum;
1263
- }
1264
- return (sum ?? 0) + Math.max(0, Math.floor(value));
1265
- }, void 0);
1266
- return total;
1074
+ async function recordMcpProviderConnected(args) {
1075
+ const eventStore = getConversationEventStore();
1076
+ const events = await eventStore.loadCurrentHistory(args.conversationId);
1077
+ if (connectedMcpProvidersFromEvents(events).includes(args.provider)) {
1078
+ return;
1079
+ }
1080
+ await eventStore.append(args.conversationId, [
1081
+ {
1082
+ data: { type: "mcp_provider_connected", provider: args.provider },
1083
+ createdAtMs: Date.now()
1084
+ }
1085
+ ]);
1267
1086
  }
1268
- function resumableBoundary(messages, fallbackMessages) {
1269
- const current = trimTrailingAssistantMessages(messages);
1270
- if (current.length > 0 && isContinuablePiBoundary(current)) {
1271
- return current;
1087
+ async function recordAuthorizationRequested(args) {
1088
+ const eventStore = getConversationEventStore();
1089
+ const events = await eventStore.loadCurrentHistory(args.conversationId);
1090
+ if (events.some(
1091
+ (event) => event.data.type === "authorization_requested" && event.data.authorizationId === args.authorizationId
1092
+ )) {
1093
+ return;
1272
1094
  }
1273
- return trimTrailingAssistantMessages(fallbackMessages ?? []);
1095
+ await eventStore.append(args.conversationId, [
1096
+ {
1097
+ data: {
1098
+ type: "authorization_requested",
1099
+ kind: args.kind,
1100
+ provider: args.provider,
1101
+ actorId: args.actorId,
1102
+ authorizationId: args.authorizationId,
1103
+ delivery: args.delivery
1104
+ },
1105
+ createdAtMs: Date.now()
1106
+ }
1107
+ ]);
1274
1108
  }
1275
- async function loadTurnSessionRecord(ctx) {
1276
- const existingSessionRecord = await getAgentTurnSessionRecordForResume(
1277
- ctx.conversationId,
1278
- ctx.sessionId
1279
- );
1280
- const hasAwaitingResumeRecord = Boolean(
1281
- existingSessionRecord && existingSessionRecord.state === "awaiting_resume"
1282
- );
1283
- return {
1284
- resumedFromSessionRecord: hasAwaitingResumeRecord,
1285
- currentSliceId: hasAwaitingResumeRecord ? existingSessionRecord.sliceId : 1,
1286
- existingSessionRecord
1287
- };
1109
+ async function recordAuthorizationCompleted(args) {
1110
+ const eventStore = getConversationEventStore();
1111
+ const events = await eventStore.loadCurrentHistory(args.conversationId);
1112
+ if (events.some(
1113
+ (event) => event.data.type === "authorization_completed" && event.data.authorizationId === args.authorizationId
1114
+ )) {
1115
+ return;
1116
+ }
1117
+ await eventStore.append(args.conversationId, [
1118
+ {
1119
+ data: {
1120
+ type: "authorization_completed",
1121
+ kind: args.kind,
1122
+ provider: args.provider,
1123
+ actorId: args.actorId,
1124
+ authorizationId: args.authorizationId
1125
+ },
1126
+ createdAtMs: Date.now()
1127
+ }
1128
+ ]);
1288
1129
  }
1289
- async function persistRunningSessionRecord(args) {
1290
- if (args.messages.length === 0 || !isContinuablePiBoundary(args.messages)) {
1291
- return false;
1130
+ async function loadTurnRoute(args) {
1131
+ const event = await getConversationEventStore().loadByIdempotencyKey(
1132
+ args.conversationId,
1133
+ `turn:${args.turnId}:routed`
1134
+ );
1135
+ if (!event) {
1136
+ return void 0;
1292
1137
  }
1293
- try {
1294
- const latestSessionRecord = await getAgentTurnSessionRecord(
1295
- args.conversationId,
1296
- args.sessionId
1297
- );
1298
- await upsertAgentTurnSessionRecord({
1299
- ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
1300
- conversationId: args.conversationId,
1301
- cumulativeDurationMs: latestSessionRecord?.cumulativeDurationMs,
1302
- cumulativeUsage: latestSessionRecord?.cumulativeUsage,
1303
- ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
1304
- ...args.dispatchId ?? latestSessionRecord?.dispatchId ? { dispatchId: args.dispatchId ?? latestSessionRecord?.dispatchId } : {},
1305
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
1306
- sessionId: args.sessionId,
1307
- sliceId: args.sliceId,
1308
- state: "running",
1309
- piMessages: args.messages,
1310
- ...args.trailingMessageProvenance ? { trailingMessageProvenance: args.trailingMessageProvenance } : {},
1311
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
1312
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
1313
- modelId: args.modelId,
1314
- ...args.reasoningLevel ? { reasoningLevel: args.reasoningLevel } : {},
1315
- ...args.turnContexts ? { turnContexts: args.turnContexts } : {},
1316
- ...args.actor ?? latestSessionRecord?.actor ? { actor: args.actor ?? latestSessionRecord?.actor } : {},
1317
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {},
1318
- ...(args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex) !== void 0 ? {
1319
- turnStartMessageIndex: args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex
1320
- } : {}
1321
- });
1322
- return true;
1323
- } catch (recordError) {
1324
- logSessionRecordError(
1325
- recordError,
1326
- "agent_turn_running_session_record_failed",
1327
- args,
1328
- {
1329
- "app.ai.resume_slice_id": args.sliceId
1138
+ if (event.data.type !== "turn_routed" || event.data.turnId !== args.turnId) {
1139
+ throw new Error(`Turn route key for "${args.turnId}" has invalid data`);
1140
+ }
1141
+ return event.data;
1142
+ }
1143
+ async function recordTurnRoute(args) {
1144
+ await getConversationEventStore().append(args.conversationId, [
1145
+ {
1146
+ idempotencyKey: `turn:${args.turnId}:routed`,
1147
+ createdAtMs: Date.now(),
1148
+ data: {
1149
+ type: "turn_routed",
1150
+ turnId: args.turnId,
1151
+ modelProfile: args.modelProfile,
1152
+ modelId: args.modelId,
1153
+ reasoningLevel: args.reasoningLevel,
1154
+ ...args.confidence !== void 0 ? { confidence: args.confidence } : {},
1155
+ source: args.source
1156
+ }
1157
+ }
1158
+ ]);
1159
+ }
1160
+ async function recordToolExecutionStarted(args) {
1161
+ await getConversationEventStore().append(args.conversationId, [
1162
+ {
1163
+ data: {
1164
+ type: "tool_execution_started",
1165
+ toolCallId: args.toolCallId,
1166
+ toolName: args.toolName
1330
1167
  },
1331
- "Failed to persist running turn session record"
1168
+ createdAtMs: args.createdAtMs ?? Date.now()
1169
+ }
1170
+ ]);
1171
+ }
1172
+
1173
+ // src/chat/state/turn-id.ts
1174
+ function buildDeterministicTurnId(messageId) {
1175
+ const sanitized = messageId.replace(/[^a-zA-Z0-9_-]/g, "_");
1176
+ return `turn_${sanitized}`;
1177
+ }
1178
+ function buildDeterministicAssistantMessageId(turnId) {
1179
+ return `${turnId}:assistant:1`;
1180
+ }
1181
+
1182
+ // src/chat/runtime/turn.ts
1183
+ var ConversationTurnBoundaryError = class extends Error {
1184
+ eventId;
1185
+ failureCode;
1186
+ constructor(args) {
1187
+ super(
1188
+ args.cause instanceof Error ? args.cause.message : String(args.cause),
1189
+ {
1190
+ cause: args.cause
1191
+ }
1332
1192
  );
1333
- return false;
1193
+ this.name = "ConversationTurnBoundaryError";
1194
+ this.eventId = args.eventId;
1195
+ this.failureCode = args.failureCode;
1334
1196
  }
1197
+ };
1198
+ function getConversationTurnBoundaryError(error) {
1199
+ return error instanceof ConversationTurnBoundaryError ? error : void 0;
1335
1200
  }
1336
- async function persistCompletedSessionRecord(args) {
1337
- let latestSessionRecord;
1338
- await persistWithRetry(async () => {
1339
- latestSessionRecord = await getAgentTurnSessionRecord(
1340
- args.conversationId,
1341
- args.sessionId
1342
- );
1343
- });
1344
- const sliceId = args.sliceId ?? latestSessionRecord?.sliceId;
1345
- if (sliceId === void 0) {
1346
- throw new Error(
1347
- "Completed session record requires a slice id from the caller or the latest stored record"
1348
- );
1201
+ var CooperativeTurnYieldError = class extends Error {
1202
+ code = "cooperative_turn_yield";
1203
+ constructor(message = "Agent turn yielded at a safe boundary") {
1204
+ super(message);
1205
+ this.name = "CooperativeTurnYieldError";
1349
1206
  }
1350
- const modelId = args.modelId;
1351
- const reasoningLevel = args.reasoningLevel ?? latestSessionRecord?.reasoningLevel;
1352
- const target = {
1353
- ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
1354
- conversationId: args.conversationId,
1355
- cumulativeDurationMs: addDurationMs(
1356
- latestSessionRecord?.cumulativeDurationMs,
1357
- args.currentDurationMs
1358
- ),
1359
- cumulativeUsage: addAgentTurnUsage(
1360
- latestSessionRecord?.cumulativeUsage,
1361
- args.currentUsage
1362
- ),
1363
- ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
1364
- ...args.dispatchId ?? latestSessionRecord?.dispatchId ? { dispatchId: args.dispatchId ?? latestSessionRecord?.dispatchId } : {},
1365
- ...args.dispatchOutcome ?? latestSessionRecord?.dispatchOutcome ? {
1366
- dispatchOutcome: args.dispatchOutcome ?? latestSessionRecord?.dispatchOutcome
1367
- } : {},
1368
- ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
1369
- ...args.resultMessageId ?? latestSessionRecord?.resultMessageId ? {
1370
- resultMessageId: args.resultMessageId ?? latestSessionRecord?.resultMessageId
1371
- } : {},
1372
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
1373
- ...args.destinationVisibility ? { destinationVisibility: args.destinationVisibility } : {},
1374
- sessionId: args.sessionId,
1375
- sliceId,
1376
- state: "completed",
1377
- piMessages: args.allMessages,
1378
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
1379
- ...args.loadedSkillNames ?? latestSessionRecord?.loadedSkillNames ? {
1380
- loadedSkillNames: args.loadedSkillNames ?? latestSessionRecord?.loadedSkillNames
1381
- } : {},
1382
- modelId,
1383
- ...reasoningLevel ? { reasoningLevel } : {},
1384
- ...args.actor ?? latestSessionRecord?.actor ? { actor: args.actor ?? latestSessionRecord?.actor } : {},
1385
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {},
1386
- ...(args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex) !== void 0 ? {
1387
- turnStartMessageIndex: args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex
1388
- } : {}
1389
- };
1390
- await persistWithRetry(async () => {
1391
- await upsertAgentTurnSessionRecord(target);
1392
- });
1207
+ };
1208
+ function isCooperativeTurnYieldError(error) {
1209
+ return error instanceof CooperativeTurnYieldError;
1393
1210
  }
1394
- async function completeDeliveredTurn(args) {
1395
- await persistCompletedSessionRecord({
1396
- channelName: args.channelName,
1397
- conversationId: args.conversationId,
1398
- currentDurationMs: args.durationMs,
1399
- currentUsage: args.usage,
1400
- destination: args.destination,
1401
- destinationVisibility: args.destinationVisibility,
1402
- dispatchId: args.dispatchId,
1403
- dispatchOutcome: args.dispatchOutcome,
1404
- errorMessage: args.errorMessage,
1405
- resultMessageId: args.resultMessageId,
1406
- source: args.source,
1407
- sessionId: args.sessionId,
1408
- sliceId: args.sliceId,
1409
- allMessages: args.messages,
1410
- loadedSkillNames: args.loadedSkillNames,
1411
- modelId: args.modelId,
1412
- actor: args.actor,
1413
- reasoningLevel: args.reasoningLevel,
1414
- surface: args.surface,
1415
- turnStartMessageIndex: args.turnStartMessageIndex
1416
- });
1211
+ var TurnInputCommitLostError = class extends Error {
1212
+ code = "turn_input_commit_lost";
1213
+ constructor(message = "Turn input commit lost its durable owner") {
1214
+ super(message);
1215
+ this.name = "TurnInputCommitLostError";
1216
+ }
1217
+ };
1218
+ function isTurnInputCommitLostError(error) {
1219
+ return error instanceof TurnInputCommitLostError;
1417
1220
  }
1418
- async function persistAuthPauseSessionRecord(args) {
1419
- const nextSliceId = args.currentSliceId + 1;
1420
- try {
1421
- const latestSessionRecord = await getAgentTurnSessionRecord(
1422
- args.conversationId,
1423
- args.sessionId
1424
- );
1425
- const piMessages = resumableBoundary(
1426
- args.messages,
1427
- latestSessionRecord?.piMessages
1428
- );
1429
- if (piMessages.length > 0 && !isContinuablePiBoundary(piMessages)) {
1430
- return void 0;
1431
- }
1432
- return await upsertAgentTurnSessionRecord({
1433
- ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
1434
- conversationId: args.conversationId,
1435
- cumulativeDurationMs: addDurationMs(
1436
- latestSessionRecord?.cumulativeDurationMs,
1437
- args.currentDurationMs
1438
- ),
1439
- cumulativeUsage: addAgentTurnUsage(
1440
- latestSessionRecord?.cumulativeUsage,
1441
- args.currentUsage
1442
- ),
1443
- ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
1444
- ...args.dispatchId ?? latestSessionRecord?.dispatchId ? { dispatchId: args.dispatchId ?? latestSessionRecord?.dispatchId } : {},
1445
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
1446
- sessionId: args.sessionId,
1447
- sliceId: nextSliceId,
1448
- state: "awaiting_resume",
1449
- piMessages,
1450
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
1451
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
1452
- modelId: args.modelId,
1453
- ...args.reasoningLevel ?? latestSessionRecord?.reasoningLevel ? {
1454
- reasoningLevel: args.reasoningLevel ?? latestSessionRecord?.reasoningLevel
1455
- } : {},
1456
- resumeReason: "auth",
1457
- resumedFromSliceId: args.currentSliceId,
1458
- errorMessage: args.errorMessage,
1459
- ...args.actor ?? latestSessionRecord?.actor ? { actor: args.actor ?? latestSessionRecord?.actor } : {},
1460
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
1461
- });
1462
- } catch (recordError) {
1463
- logSessionRecordError(
1464
- recordError,
1465
- "agent_turn_auth_resume_session_record_failed",
1466
- args,
1467
- {
1468
- "app.ai.resume_from_slice_id": args.currentSliceId,
1469
- "app.ai.resume_next_slice_id": nextSliceId
1470
- },
1471
- "Failed to persist auth session record before retry"
1472
- );
1221
+ var TurnInputDeferredError = class extends Error {
1222
+ code = "turn_input_deferred";
1223
+ constructor(message = "Turn input is deferred until the active resume ends") {
1224
+ super(message);
1225
+ this.name = "TurnInputDeferredError";
1473
1226
  }
1474
- return void 0;
1227
+ };
1228
+ function isTurnInputDeferredError(error) {
1229
+ return error instanceof TurnInputDeferredError;
1475
1230
  }
1476
- async function persistContinuationSessionRecord(args) {
1477
- const nextSliceId = args.currentSliceId + 1;
1478
- try {
1479
- const latestSessionRecord = await getAgentTurnSessionRecord(
1480
- args.conversationId,
1481
- args.sessionId
1482
- );
1483
- const piMessages = resumableBoundary(
1484
- args.messages,
1485
- latestSessionRecord?.piMessages
1486
- );
1487
- if (piMessages.length === 0 || !isContinuablePiBoundary(piMessages)) {
1488
- return void 0;
1489
- }
1490
- const cumulativeDurationMs = addDurationMs(
1491
- latestSessionRecord?.cumulativeDurationMs,
1492
- args.currentDurationMs
1493
- );
1494
- const cumulativeUsage = addAgentTurnUsage(
1495
- latestSessionRecord?.cumulativeUsage,
1496
- args.currentUsage
1497
- );
1498
- if (nextSliceId > botConfig.maxSlicesPerTurn) {
1499
- return await upsertAgentTurnSessionRecord({
1500
- ...args.channelName ?? latestSessionRecord?.channelName ? {
1501
- channelName: args.channelName ?? latestSessionRecord?.channelName
1502
- } : {},
1503
- conversationId: args.conversationId,
1504
- cumulativeDurationMs,
1505
- cumulativeUsage,
1506
- ...args.destination ?? latestSessionRecord?.destination ? {
1507
- destination: args.destination ?? latestSessionRecord?.destination
1508
- } : {},
1509
- ...args.dispatchId ?? latestSessionRecord?.dispatchId ? { dispatchId: args.dispatchId ?? latestSessionRecord?.dispatchId } : {},
1510
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
1511
- sessionId: args.sessionId,
1512
- sliceId: args.currentSliceId,
1513
- state: "failed",
1514
- piMessages,
1515
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
1516
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
1517
- modelId: args.modelId,
1518
- ...args.reasoningLevel ?? latestSessionRecord?.reasoningLevel ? {
1519
- reasoningLevel: args.reasoningLevel ?? latestSessionRecord?.reasoningLevel
1520
- } : {},
1521
- resumeReason: args.resumeReason,
1522
- resumedFromSliceId: latestSessionRecord?.resumedFromSliceId,
1523
- errorMessage: new TurnSliceLimitExceededError(
1524
- botConfig.maxSlicesPerTurn
1525
- ).message,
1526
- ...args.actor ?? latestSessionRecord?.actor ? { actor: args.actor ?? latestSessionRecord?.actor } : {},
1527
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
1528
- });
1529
- }
1530
- return await upsertAgentTurnSessionRecord({
1531
- ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
1532
- conversationId: args.conversationId,
1533
- cumulativeDurationMs,
1534
- cumulativeUsage,
1535
- ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
1536
- ...args.dispatchId ?? latestSessionRecord?.dispatchId ? { dispatchId: args.dispatchId ?? latestSessionRecord?.dispatchId } : {},
1537
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
1538
- sessionId: args.sessionId,
1539
- sliceId: nextSliceId,
1540
- state: "awaiting_resume",
1541
- piMessages,
1542
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
1543
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
1544
- modelId: args.modelId,
1545
- ...args.reasoningLevel ?? latestSessionRecord?.reasoningLevel ? {
1546
- reasoningLevel: args.reasoningLevel ?? latestSessionRecord?.reasoningLevel
1547
- } : {},
1548
- resumeReason: args.resumeReason,
1549
- resumedFromSliceId: args.currentSliceId,
1550
- errorMessage: args.errorMessage,
1551
- ...args.actor ?? latestSessionRecord?.actor ? { actor: args.actor ?? latestSessionRecord?.actor } : {},
1552
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
1553
- });
1554
- } catch (recordError) {
1555
- logSessionRecordError(
1556
- recordError,
1557
- "agent_continue_session_record_failed",
1558
- args,
1559
- {
1560
- "app.ai.resume_from_slice_id": args.currentSliceId,
1561
- "app.ai.resume_next_slice_id": nextSliceId
1562
- },
1563
- "Failed to persist session record before scheduling agent continuation"
1564
- );
1565
- return void 0;
1231
+ function startActiveTurn(args) {
1232
+ args.conversation.processing.activeTurnId = args.nextTurnId;
1233
+ args.updateConversationStats(args.conversation);
1234
+ }
1235
+ function clearActiveTurn(conversation, sessionId) {
1236
+ if (!sessionId || conversation.processing.activeTurnId === sessionId) {
1237
+ conversation.processing.activeTurnId = void 0;
1566
1238
  }
1567
1239
  }
1568
- async function persistYieldSessionRecord(args) {
1569
- try {
1570
- const piMessages = [...args.messages];
1571
- if (piMessages.length === 0 || !isContinuablePiBoundary(piMessages)) {
1572
- return void 0;
1240
+ function markTurnClosed(args) {
1241
+ clearActiveTurn(args.conversation, args.sessionId);
1242
+ args.conversation.processing.lastCompletedAtMs = args.nowMs;
1243
+ args.updateConversationStats(args.conversation);
1244
+ }
1245
+ function markTurnCompleted(args) {
1246
+ clearActiveTurn(args.conversation, args.sessionId);
1247
+ args.conversation.processing.lastCompletedAtMs = args.nowMs;
1248
+ args.updateConversationStats(args.conversation);
1249
+ }
1250
+ function markTurnFailed(args) {
1251
+ clearActiveTurn(args.conversation, args.sessionId);
1252
+ args.conversation.processing.lastCompletedAtMs = args.nowMs;
1253
+ args.markConversationMessage(args.conversation, args.userMessageId, {
1254
+ replied: false,
1255
+ skippedReason: "reply failed"
1256
+ });
1257
+ args.updateConversationStats(args.conversation);
1258
+ }
1259
+
1260
+ // src/chat/no-reply.ts
1261
+ var NO_REPLY_MARKER = "[[NO_REPLY]]";
1262
+ function isNoReplyMarker(text) {
1263
+ return text.trim() === NO_REPLY_MARKER;
1264
+ }
1265
+ function containsNoReplyMarker(text) {
1266
+ return text.includes(NO_REPLY_MARKER);
1267
+ }
1268
+
1269
+ // src/chat/services/assistant-reply.ts
1270
+ var THINKING_XML_BLOCK_PATTERN = /[ \t]*<thinking\b[^>]*>[\s\S]*?<\/thinking>[ \t]*(?:\r?\n)?/gi;
1271
+ var FENCED_CODE_BLOCK_PATTERN = /```[\s\S]*?```/g;
1272
+ function sanitizeAssistantText(text) {
1273
+ let result = "";
1274
+ let cursor = 0;
1275
+ for (const match of text.matchAll(FENCED_CODE_BLOCK_PATTERN)) {
1276
+ const start = match.index;
1277
+ if (start === void 0) continue;
1278
+ result += text.slice(cursor, start).replace(THINKING_XML_BLOCK_PATTERN, "");
1279
+ result += match[0];
1280
+ cursor = start + match[0].length;
1281
+ }
1282
+ return (result + text.slice(cursor).replace(THINKING_XML_BLOCK_PATTERN, "")).trim();
1283
+ }
1284
+ function decideReply(message) {
1285
+ if (message.content.some((part) => part.type === "toolCall")) {
1286
+ return { kind: "suppress" };
1287
+ }
1288
+ const text = sanitizeAssistantText(extractAssistantText(message));
1289
+ if (!text) return { kind: "empty" };
1290
+ if (isNoReplyMarker(text) || containsNoReplyMarker(text)) {
1291
+ return { kind: "suppress" };
1292
+ }
1293
+ return { kind: "deliver", text };
1294
+ }
1295
+ function getAssistantReplyText(message) {
1296
+ const decision = decideReply(message);
1297
+ return decision.kind === "deliver" ? decision.text : void 0;
1298
+ }
1299
+
1300
+ // src/chat/services/persist-retry.ts
1301
+ var PERSIST_ATTEMPTS = 3;
1302
+ async function persistWithRetry(persist) {
1303
+ let lastError;
1304
+ for (let attempt = 1; attempt <= PERSIST_ATTEMPTS; attempt += 1) {
1305
+ try {
1306
+ await persist();
1307
+ return;
1308
+ } catch (error) {
1309
+ lastError = error;
1310
+ if (attempt < PERSIST_ATTEMPTS) {
1311
+ await sleep(attempt * 100);
1312
+ }
1573
1313
  }
1574
- const latestSessionRecord = await getAgentTurnSessionRecord(
1575
- args.conversationId,
1576
- args.sessionId
1577
- );
1578
- return await upsertAgentTurnSessionRecord({
1579
- ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
1580
- conversationId: args.conversationId,
1581
- cumulativeDurationMs: addDurationMs(
1582
- latestSessionRecord?.cumulativeDurationMs,
1583
- args.currentDurationMs
1584
- ),
1585
- cumulativeUsage: addAgentTurnUsage(
1586
- latestSessionRecord?.cumulativeUsage,
1587
- args.currentUsage
1588
- ),
1589
- ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
1590
- ...args.dispatchId ?? latestSessionRecord?.dispatchId ? { dispatchId: args.dispatchId ?? latestSessionRecord?.dispatchId } : {},
1591
- ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
1592
- sessionId: args.sessionId,
1593
- sliceId: args.currentSliceId,
1594
- state: "awaiting_resume",
1595
- piMessages,
1596
- ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
1597
- ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
1598
- modelId: args.modelId,
1599
- ...args.reasoningLevel ?? latestSessionRecord?.reasoningLevel ? {
1600
- reasoningLevel: args.reasoningLevel ?? latestSessionRecord?.reasoningLevel
1601
- } : {},
1602
- resumeReason: "yield",
1603
- resumedFromSliceId: latestSessionRecord?.resumedFromSliceId,
1604
- errorMessage: args.errorMessage,
1605
- ...args.actor ?? latestSessionRecord?.actor ? { actor: args.actor ?? latestSessionRecord?.actor } : {},
1606
- ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
1607
- });
1608
- } catch (recordError) {
1609
- logSessionRecordError(
1610
- recordError,
1611
- "agent_turn_yield_session_record_failed",
1612
- args,
1613
- {
1614
- "app.ai.resume_slice_id": args.currentSliceId
1615
- },
1616
- "Failed to persist cooperative yield session record"
1617
- );
1618
- return void 0;
1619
1314
  }
1315
+ throw lastError;
1620
1316
  }
1621
1317
 
1622
- // src/chat/state/conversation.ts
1623
- function defaultConversationState() {
1624
- const nowMs = Date.now();
1625
- return {
1626
- schemaVersion: 1,
1627
- messages: [],
1628
- compactions: [],
1629
- backfill: {},
1630
- processing: {},
1631
- stats: {
1632
- estimatedContextTokens: 0,
1633
- totalMessageCount: 0,
1634
- compactedMessageCount: 0,
1635
- updatedAtMs: nowMs
1636
- },
1637
- vision: {
1638
- byFileId: {}
1639
- }
1318
+ // src/chat/state/turn-session.ts
1319
+ import { THREAD_STATE_TTL_MS } from "chat";
1320
+ import {
1321
+ actorSchema,
1322
+ destinationSchema,
1323
+ sourceSchema
1324
+ } from "@sentry/junior-plugin-api";
1325
+ import { z as z2 } from "zod";
1326
+
1327
+ // src/chat/state/turn-session-keys.ts
1328
+ var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
1329
+ function agentTurnSessionKey(conversationId, sessionId) {
1330
+ return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
1331
+ }
1332
+ function agentTurnSessionConversationIndexKey(conversationId) {
1333
+ return `${AGENT_TURN_SESSION_PREFIX}:conversation:${conversationId}:index`;
1334
+ }
1335
+
1336
+ // src/chat/state/turn-session.ts
1337
+ var AGENT_TURN_SESSION_INDEX_KEY = `${AGENT_TURN_SESSION_PREFIX}:index`;
1338
+ var AGENT_TURN_SESSION_INDEX_MAX_LENGTH = 5e3;
1339
+ var AGENT_TURN_SESSION_TTL_MS = THREAD_STATE_TTL_MS;
1340
+ var agentTurnSessionStatusSchema = z2.enum([
1341
+ "running",
1342
+ "awaiting_resume",
1343
+ "completed",
1344
+ "failed",
1345
+ "abandoned"
1346
+ ]);
1347
+ var agentTurnSurfaceSchema = z2.enum([
1348
+ "slack",
1349
+ "api",
1350
+ "scheduler",
1351
+ "internal"
1352
+ ]);
1353
+ var agentTurnResumeReasonSchema = z2.enum([
1354
+ "timeout",
1355
+ "auth",
1356
+ "yield",
1357
+ "retry"
1358
+ ]);
1359
+ var nonNegativeNumberSchema = z2.number().finite().nonnegative();
1360
+ var seqCursorSchema = z2.number().int().min(-1);
1361
+ var agentTurnSessionSummarySchema = z2.object({
1362
+ channelName: z2.string().min(1).optional(),
1363
+ version: z2.number().int().nonnegative(),
1364
+ conversationId: z2.string().min(1),
1365
+ cumulativeDurationMs: nonNegativeNumberSchema,
1366
+ cumulativeUsage: agentTurnUsageSchema.optional(),
1367
+ destination: destinationSchema.optional(),
1368
+ dispatchId: z2.string().min(1).optional(),
1369
+ dispatchOutcome: z2.enum(["blocked", "completed", "failed"]).optional(),
1370
+ resultMessageId: z2.string().min(1).optional(),
1371
+ source: sourceSchema.optional(),
1372
+ lastProgressAtMs: nonNegativeNumberSchema,
1373
+ loadedSkillNames: z2.array(z2.string()).optional(),
1374
+ modelId: z2.string().min(1).optional(),
1375
+ reasoningLevel: z2.string().min(1).optional(),
1376
+ actor: actorSchema.optional(),
1377
+ resumeReason: agentTurnResumeReasonSchema.optional(),
1378
+ resumedFromSliceId: z2.number().int().nonnegative().optional(),
1379
+ sessionId: z2.string().min(1),
1380
+ sliceId: z2.number().int().nonnegative(),
1381
+ startedAtMs: nonNegativeNumberSchema,
1382
+ state: agentTurnSessionStatusSchema,
1383
+ surface: agentTurnSurfaceSchema.optional(),
1384
+ traceId: z2.string().optional(),
1385
+ updatedAtMs: nonNegativeNumberSchema
1386
+ }).strict();
1387
+ var storedAgentTurnSessionRecordSchema = agentTurnSessionSummarySchema.extend({
1388
+ actors: z2.array(actorSchema).optional(),
1389
+ committedSeq: seqCursorSchema,
1390
+ historyVersion: z2.number().int().nonnegative().optional(),
1391
+ errorMessage: z2.string().optional(),
1392
+ turnStartSeq: seqCursorSchema.optional(),
1393
+ runtimeContext: z2.array(piMessageSchema).optional()
1394
+ }).strict();
1395
+ function conversationExecutionFromSummary(summary) {
1396
+ const status = summary.state === "completed" || summary.state === "abandoned" ? "idle" : summary.state;
1397
+ return {
1398
+ status,
1399
+ runId: summary.sessionId,
1400
+ updatedAtMs: summary.updatedAtMs
1640
1401
  };
1641
1402
  }
1642
- function coercePendingAuthState(value) {
1643
- if (!isRecord(value)) {
1644
- return void 0;
1645
- }
1646
- const kind = value.kind;
1647
- const provider = toOptionalString(value.provider);
1648
- const actorId = toOptionalString(value.actorId);
1649
- const authSessionId = toOptionalString(value.authSessionId);
1650
- const scope = toOptionalString(value.scope);
1651
- const sessionId = toOptionalString(value.sessionId);
1652
- const linkSentAtMs = toOptionalNumber(value.linkSentAtMs);
1653
- if (kind !== "mcp" && kind !== "plugin" || !provider || !actorId || kind === "mcp" && !authSessionId || !sessionId || typeof linkSentAtMs !== "number") {
1654
- return void 0;
1655
- }
1656
- const base = {
1657
- provider,
1658
- actorId,
1659
- ...scope ? { scope } : {},
1660
- sessionId,
1661
- linkSentAtMs
1662
- };
1663
- return kind === "mcp" ? { ...base, authSessionId, kind } : { ...base, kind };
1403
+ function sessionLogActor(actor) {
1404
+ return actor?.platform === "slack" ? toStoredSlackActor(actor) : void 0;
1664
1405
  }
1665
- function coerceThreadConversationState(value) {
1666
- if (!isRecord(value)) {
1667
- return defaultConversationState();
1668
- }
1669
- const root = value;
1670
- const rawConversation = isRecord(root.conversation) ? root.conversation : {};
1671
- const base = defaultConversationState();
1672
- const messages = [];
1673
- const compactions = [];
1674
- const rawBackfill = isRecord(rawConversation.backfill) ? rawConversation.backfill : {};
1675
- const backfill = {
1676
- completedAtMs: toOptionalNumber(rawBackfill.completedAtMs),
1677
- source: rawBackfill.source === "recent_messages" || rawBackfill.source === "thread_fetch" ? rawBackfill.source : void 0
1678
- };
1679
- const rawProcessing = isRecord(rawConversation.processing) ? rawConversation.processing : {};
1680
- const processing = {
1681
- activeTurnId: toOptionalString(rawProcessing.activeTurnId),
1682
- lastCompletedAtMs: toOptionalNumber(rawProcessing.lastCompletedAtMs),
1683
- pendingAuth: coercePendingAuthState(rawProcessing.pendingAuth)
1684
- };
1685
- const rawStats = isRecord(rawConversation.stats) ? rawConversation.stats : {};
1686
- const stats = {
1687
- estimatedContextTokens: toOptionalNumber(rawStats.estimatedContextTokens) ?? base.stats.estimatedContextTokens,
1688
- totalMessageCount: toOptionalNumber(rawStats.totalMessageCount) ?? messages.length,
1689
- compactedMessageCount: toOptionalNumber(rawStats.compactedMessageCount) ?? 0,
1690
- updatedAtMs: toOptionalNumber(rawStats.updatedAtMs) ?? base.stats.updatedAtMs
1691
- };
1692
- const rawVision = isRecord(rawConversation.vision) ? rawConversation.vision : {};
1693
- const rawVisionByFileId = isRecord(rawVision.byFileId) ? rawVision.byFileId : {};
1694
- const byFileId = {};
1695
- for (const [fileId, value2] of Object.entries(rawVisionByFileId)) {
1696
- if (typeof fileId !== "string" || fileId.trim().length === 0) continue;
1697
- if (!isRecord(value2)) continue;
1698
- const summary = toOptionalString(value2.summary);
1699
- const analyzedAtMs = toOptionalNumber(value2.analyzedAtMs);
1700
- if (!summary || !analyzedAtMs) continue;
1701
- byFileId[fileId] = {
1406
+ function parseAgentTurnSessionRecord(value) {
1407
+ return storedAgentTurnSessionRecordSchema.parse(value);
1408
+ }
1409
+ function parseAgentTurnSessionSummary(value) {
1410
+ return agentTurnSessionSummarySchema.parse(value);
1411
+ }
1412
+ async function appendAgentTurnSessionSummary(summary, ttlMs) {
1413
+ const stateAdapter = getStateAdapter();
1414
+ await Promise.all([
1415
+ stateAdapter.appendToList(AGENT_TURN_SESSION_INDEX_KEY, summary, {
1416
+ maxLength: AGENT_TURN_SESSION_INDEX_MAX_LENGTH,
1417
+ ttlMs
1418
+ }),
1419
+ stateAdapter.appendToList(
1420
+ agentTurnSessionConversationIndexKey(summary.conversationId),
1702
1421
  summary,
1703
- analyzedAtMs
1704
- };
1705
- }
1706
- return {
1707
- schemaVersion: 1,
1708
- messages,
1709
- compactions,
1710
- backfill,
1711
- processing,
1712
- stats,
1713
- vision: {
1714
- backfillCompletedAtMs: toOptionalNumber(rawVision.backfillCompletedAtMs),
1715
- byFileId
1716
- }
1717
- };
1422
+ { ttlMs }
1423
+ )
1424
+ ]);
1718
1425
  }
1719
- function buildConversationStatePatch(conversation) {
1720
- const {
1721
- compactions: _compactions,
1722
- messages: _messages,
1723
- ...scratch
1724
- } = conversation;
1426
+ async function recordConversationActivityMetadata(args) {
1427
+ const conversationStore = args.conversationStore ?? getConversationStore();
1428
+ const source = args.summary.destination?.platform === "local" ? "local" : args.summary.surface;
1429
+ await conversationStore.recordActivity({
1430
+ activityAtMs: args.summary.updatedAtMs,
1431
+ channelName: args.summary.channelName,
1432
+ conversationId: args.summary.conversationId,
1433
+ destination: args.summary.destination,
1434
+ nowMs: args.nowMs,
1435
+ actor: sessionLogActor(args.summary.actor),
1436
+ source,
1437
+ visibility: args.destinationVisibility
1438
+ });
1439
+ await conversationStore.recordExecution({
1440
+ channelName: args.summary.channelName,
1441
+ conversationId: args.summary.conversationId,
1442
+ createdAtMs: args.summary.startedAtMs,
1443
+ destination: args.summary.destination,
1444
+ execution: conversationExecutionFromSummary(args.summary),
1445
+ lastActivityAtMs: args.summary.updatedAtMs,
1446
+ metrics: {
1447
+ durationMs: args.summary.cumulativeDurationMs,
1448
+ ...args.summary.cumulativeUsage ? { usage: args.summary.cumulativeUsage } : {}
1449
+ },
1450
+ actor: sessionLogActor(args.summary.actor),
1451
+ source,
1452
+ updatedAtMs: args.nowMs,
1453
+ visibility: args.destinationVisibility
1454
+ });
1455
+ }
1456
+ function materializeAgentTurnSessionRecord(stored, piProjection, turnStartMessageIndex, restoreVolatileContext = true) {
1457
+ const piMessages = restoreVolatileContext && (stored.state === "running" || stored.state === "awaiting_resume") ? restoreRuntimeContext(piProjection.messages, stored.runtimeContext) : piProjection.messages;
1725
1458
  return {
1726
- conversation: {
1727
- ...scratch,
1728
- schemaVersion: 1,
1729
- stats: {
1730
- ...conversation.stats,
1731
- totalMessageCount: conversation.messages.length,
1732
- updatedAtMs: Date.now()
1733
- }
1734
- }
1459
+ version: stored.version,
1460
+ ...stored.channelName ? { channelName: stored.channelName } : {},
1461
+ conversationId: stored.conversationId,
1462
+ sessionId: stored.sessionId,
1463
+ sliceId: stored.sliceId,
1464
+ state: stored.state,
1465
+ startedAtMs: stored.startedAtMs,
1466
+ lastProgressAtMs: stored.lastProgressAtMs,
1467
+ updatedAtMs: stored.updatedAtMs,
1468
+ piMessages,
1469
+ piMessageProvenance: piProjection.provenance,
1470
+ actors: stored.actors ?? instructionActors(piProjection.provenance),
1471
+ cumulativeDurationMs: stored.cumulativeDurationMs,
1472
+ ...stored.destination ? { destination: stored.destination } : {},
1473
+ ...stored.dispatchId ? { dispatchId: stored.dispatchId } : {},
1474
+ ...stored.dispatchOutcome ? { dispatchOutcome: stored.dispatchOutcome } : {},
1475
+ ...stored.resultMessageId ? { resultMessageId: stored.resultMessageId } : {},
1476
+ ...stored.source ? { source: stored.source } : {},
1477
+ ...stored.cumulativeUsage ? { cumulativeUsage: stored.cumulativeUsage } : {},
1478
+ ...stored.resumeReason ? { resumeReason: stored.resumeReason } : {},
1479
+ ...stored.errorMessage ? { errorMessage: stored.errorMessage } : {},
1480
+ ...stored.loadedSkillNames ? { loadedSkillNames: stored.loadedSkillNames } : {},
1481
+ ...stored.modelId ? { modelId: stored.modelId } : {},
1482
+ ...stored.reasoningLevel ? { reasoningLevel: stored.reasoningLevel } : {},
1483
+ ...stored.actor ? { actor: stored.actor } : {},
1484
+ ...stored.resumedFromSliceId !== void 0 ? { resumedFromSliceId: stored.resumedFromSliceId } : {},
1485
+ ...stored.surface ? { surface: stored.surface } : {},
1486
+ ...stored.traceId ? { traceId: stored.traceId } : {},
1487
+ ...turnStartMessageIndex !== void 0 ? { turnStartMessageIndex } : {}
1735
1488
  };
1736
1489
  }
1737
-
1738
- // src/chat/services/context-budget.ts
1739
- var COMPACTION_TRIGGER_RATIO = 0.9;
1740
- var CONTEXT_INPUT_LIMIT_RATIO = 0.95;
1741
- var COMPACTION_TARGET_RATIO = 0.8;
1742
- var FALLBACK_CONTEXT_WINDOW_TOKENS = 4e5;
1743
- function positiveInteger(value, fallback) {
1744
- return Number.isFinite(value) && value > 0 ? Math.floor(value) : fallback;
1490
+ function restoreRuntimeContext(messages, runtimeContext) {
1491
+ if (!runtimeContext || runtimeContext.length === 0) return messages;
1492
+ const restored = [...messages];
1493
+ const unmatchedRuntimeContext = [];
1494
+ for (const runtimeMessage of runtimeContext) {
1495
+ const runtime = runtimeMessage;
1496
+ const targetIndex = restored.findIndex((message) => {
1497
+ const candidate = message;
1498
+ return candidate.role === "user" && candidate.timestamp === runtime.timestamp;
1499
+ });
1500
+ if (targetIndex < 0) {
1501
+ unmatchedRuntimeContext.push(runtimeMessage);
1502
+ continue;
1503
+ }
1504
+ const target = restored[targetIndex];
1505
+ restored[targetIndex] = {
1506
+ ...restored[targetIndex],
1507
+ content: [
1508
+ ...Array.isArray(runtime.content) ? runtime.content : [],
1509
+ ...Array.isArray(target.content) ? target.content : []
1510
+ ]
1511
+ };
1512
+ }
1513
+ return [...unmatchedRuntimeContext, ...restored];
1745
1514
  }
1746
- function estimateTextTokens(text) {
1747
- return Math.ceil(text.length / 4);
1515
+ async function getStoredAgentTurnSessionRecord(conversationId, sessionId) {
1516
+ const stateAdapter = getStateAdapter();
1517
+ await stateAdapter.connect();
1518
+ const value = await stateAdapter.get(
1519
+ agentTurnSessionKey(conversationId, sessionId)
1520
+ );
1521
+ return value == null ? void 0 : parseAgentTurnSessionRecord(value);
1748
1522
  }
1749
- function calculateContextCompactionTriggerTokens(model) {
1750
- const contextWindow = positiveInteger(
1751
- model.contextWindow,
1752
- FALLBACK_CONTEXT_WINDOW_TOKENS
1523
+ async function materializeStoredAgentTurnSessionRecord(conversationId, sessionId, followCurrentReplacement) {
1524
+ const parsed = await getStoredAgentTurnSessionRecord(
1525
+ conversationId,
1526
+ sessionId
1527
+ );
1528
+ if (!parsed) {
1529
+ return void 0;
1530
+ }
1531
+ const pinnedProjection = await loadTurnProjection({
1532
+ conversationId,
1533
+ committedSeq: parsed.committedSeq,
1534
+ // Unfinished records include the current-epoch tail so parked input
1535
+ // appended after the last safe boundary stays model-visible on resume.
1536
+ includeTail: parsed.state === "running" || parsed.state === "awaiting_resume"
1537
+ });
1538
+ if (!pinnedProjection) {
1539
+ return void 0;
1540
+ }
1541
+ const currentHistory = await getConversationEventStore().loadCurrentHistory(conversationId);
1542
+ const currentHistoryVersion = currentHistory.at(-1)?.historyVersion ?? parsed.historyVersion ?? 0;
1543
+ const followsReplacement = followCurrentReplacement && (parsed.state === "running" || parsed.state === "awaiting_resume") && parsed.historyVersion !== void 0 && parsed.historyVersion !== currentHistoryVersion;
1544
+ const piProjection = followsReplacement ? projectConversationEvents(currentHistory) : pinnedProjection;
1545
+ const turnStartMessageIndex = followsReplacement ? 0 : parsed.turnStartSeq === void 0 ? void 0 : piProjection.seqs.filter((seq) => seq <= parsed.turnStartSeq).length;
1546
+ return materializeAgentTurnSessionRecord(
1547
+ parsed,
1548
+ piProjection,
1549
+ turnStartMessageIndex,
1550
+ !followsReplacement && (parsed.historyVersion === void 0 || parsed.historyVersion === currentHistoryVersion)
1753
1551
  );
1754
- return Math.max(1, Math.floor(contextWindow * COMPACTION_TRIGGER_RATIO));
1755
1552
  }
1756
- function calculateContextInputLimitTokens(model) {
1757
- const contextWindow = positiveInteger(
1758
- model.contextWindow,
1759
- FALLBACK_CONTEXT_WINDOW_TOKENS
1553
+ async function getAgentTurnSessionRecord(conversationId, sessionId) {
1554
+ return await materializeStoredAgentTurnSessionRecord(
1555
+ conversationId,
1556
+ sessionId,
1557
+ false
1760
1558
  );
1761
- return Math.max(1, Math.floor(contextWindow * CONTEXT_INPUT_LIMIT_RATIO));
1762
1559
  }
1763
- function calculateContextCompactionTargetTokens(triggerTokens) {
1764
- return Math.max(1, Math.floor(triggerTokens * COMPACTION_TARGET_RATIO));
1765
- }
1766
- function getModelContextBudget(modelId) {
1767
- const model = resolveGatewayModel(modelId);
1768
- const advertisedContextWindow = positiveInteger(
1769
- model.contextWindow,
1770
- FALLBACK_CONTEXT_WINDOW_TOKENS
1560
+ async function getAgentTurnSessionRecordForResume(conversationId, sessionId) {
1561
+ return await materializeStoredAgentTurnSessionRecord(
1562
+ conversationId,
1563
+ sessionId,
1564
+ true
1771
1565
  );
1566
+ }
1567
+ function buildStoredRecord(args) {
1568
+ const nowMs = Date.now();
1772
1569
  return {
1773
- contextWindow: Math.min(
1774
- botConfig.contextWindowTokens,
1775
- advertisedContextWindow
1776
- )
1570
+ version: (args.previousVersion ?? 0) + 1,
1571
+ ...args.channelName ? { channelName: args.channelName } : {},
1572
+ conversationId: args.conversationId,
1573
+ sessionId: args.sessionId,
1574
+ sliceId: args.sliceId,
1575
+ state: args.state,
1576
+ startedAtMs: args.startedAtMs ?? nowMs,
1577
+ lastProgressAtMs: args.lastProgressAtMs ?? nowMs,
1578
+ updatedAtMs: nowMs,
1579
+ committedSeq: args.committedSeq,
1580
+ ...args.historyVersion !== void 0 ? { historyVersion: args.historyVersion } : {},
1581
+ ...args.turnStartSeq !== void 0 ? { turnStartSeq: args.turnStartSeq } : {},
1582
+ ...args.runtimeContext && args.runtimeContext.length > 0 ? { runtimeContext: args.runtimeContext } : {},
1583
+ cumulativeDurationMs: args.cumulativeDurationMs,
1584
+ ...args.cumulativeUsage ? { cumulativeUsage: args.cumulativeUsage } : {},
1585
+ ...args.destination ? { destination: args.destination } : {},
1586
+ ...args.dispatchId ? { dispatchId: args.dispatchId } : {},
1587
+ ...args.dispatchOutcome ? { dispatchOutcome: args.dispatchOutcome } : {},
1588
+ ...args.resultMessageId ? { resultMessageId: args.resultMessageId } : {},
1589
+ ...args.source ? { source: args.source } : {},
1590
+ ...args.actor ? { actor: args.actor } : {},
1591
+ ...args.actors ? { actors: args.actors } : {},
1592
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
1593
+ ...args.modelId ? { modelId: args.modelId } : {},
1594
+ ...args.reasoningLevel ? { reasoningLevel: args.reasoningLevel } : {},
1595
+ ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
1596
+ ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
1597
+ ...args.resumedFromSliceId !== void 0 ? { resumedFromSliceId: args.resumedFromSliceId } : {},
1598
+ ...args.surface ? { surface: args.surface } : {},
1599
+ ...args.traceId ? { traceId: args.traceId } : {}
1777
1600
  };
1778
1601
  }
1779
- function getAgentContextCompactionTriggerTokens(modelId) {
1780
- return calculateContextCompactionTriggerTokens(
1781
- getModelContextBudget(modelId)
1602
+ async function setStoredRecord(args) {
1603
+ const stateAdapter = getStateAdapter();
1604
+ await stateAdapter.connect();
1605
+ await recordConversationActivityMetadata({
1606
+ conversationStore: args.conversationStore,
1607
+ destinationVisibility: args.destinationVisibility,
1608
+ nowMs: Date.now(),
1609
+ summary: args.record
1610
+ });
1611
+ await stateAdapter.set(
1612
+ agentTurnSessionKey(args.record.conversationId, args.record.sessionId),
1613
+ args.record,
1614
+ args.ttlMs
1782
1615
  );
1783
- }
1784
- function getAgentContextInputLimitTokens(modelId) {
1785
- return calculateContextInputLimitTokens(getModelContextBudget(modelId));
1786
- }
1787
- function getConversationContextCompactionTriggerTokens() {
1788
- return calculateContextCompactionTriggerTokens(
1789
- getModelContextBudget(botConfig.fastModelId)
1616
+ const {
1617
+ actors: _actors,
1618
+ committedSeq: _committedSeq,
1619
+ historyVersion: _historyVersion,
1620
+ errorMessage: _errorMessage,
1621
+ turnStartSeq: _turnStartSeq,
1622
+ runtimeContext: _runtimeContext,
1623
+ ...summary
1624
+ } = args.record;
1625
+ await appendAgentTurnSessionSummary(summary, args.ttlMs);
1626
+ return materializeAgentTurnSessionRecord(
1627
+ args.record,
1628
+ {
1629
+ messages: [...args.piMessages],
1630
+ provenance: [...args.piMessageProvenance]
1631
+ },
1632
+ args.turnStartMessageIndex
1790
1633
  );
1791
1634
  }
1792
-
1793
- // src/chat/services/conversation-memory.ts
1794
- var CONTEXT_MIN_LIVE_MESSAGES = 12;
1795
- var CONTEXT_COMPACTION_BATCH_SIZE = 24;
1796
- var CONTEXT_MAX_COMPACTIONS = 16;
1797
- var CONTEXT_MAX_MESSAGE_CHARS = 3200;
1798
- var SLACK_USER_ID_DISPLAY_PATTERN = /^[UW][A-Z0-9]{5,}$/;
1799
- function generateConversationId(prefix) {
1800
- return `${prefix}_${Date.now()}_${Math.random().toString(36).slice(2, 10)}`;
1801
- }
1802
- function normalizeConversationText(text) {
1803
- return text.replace(/\r\n?/g, "\n").trim();
1804
- }
1805
- function buildImageContextSuffix(message, conversation) {
1806
- const byFileId = conversation?.vision.byFileId;
1807
- const imageFileIds = message.meta?.imageFileIds ?? [];
1808
- if (!byFileId || imageFileIds.length === 0) {
1809
- return "";
1635
+ async function updateAgentTurnSessionState(args) {
1636
+ const parsed = await getStoredAgentTurnSessionRecord(
1637
+ args.existing.conversationId,
1638
+ args.existing.sessionId
1639
+ );
1640
+ if (!parsed || parsed.version !== args.existing.version) {
1641
+ return void 0;
1810
1642
  }
1811
- const summaries = imageFileIds.map((fileId) => byFileId[fileId]?.summary?.trim()).filter((summary) => Boolean(summary));
1812
- if (summaries.length === 0) {
1813
- return "";
1643
+ return await setStoredRecord({
1644
+ piMessages: args.existing.piMessages,
1645
+ piMessageProvenance: args.existing.piMessageProvenance,
1646
+ ttlMs: AGENT_TURN_SESSION_TTL_MS,
1647
+ ...args.existing.turnStartMessageIndex !== void 0 ? { turnStartMessageIndex: args.existing.turnStartMessageIndex } : {},
1648
+ record: buildStoredRecord({
1649
+ conversationId: args.existing.conversationId,
1650
+ sessionId: args.existing.sessionId,
1651
+ sliceId: args.existing.sliceId,
1652
+ state: args.state,
1653
+ committedSeq: parsed.committedSeq,
1654
+ ...parsed.historyVersion !== void 0 ? { historyVersion: parsed.historyVersion } : {},
1655
+ ...parsed.turnStartSeq !== void 0 ? { turnStartSeq: parsed.turnStartSeq } : {},
1656
+ ...parsed.runtimeContext ? { runtimeContext: parsed.runtimeContext } : {},
1657
+ ...parsed.channelName ? { channelName: parsed.channelName } : {},
1658
+ startedAtMs: parsed.startedAtMs,
1659
+ lastProgressAtMs: parsed.lastProgressAtMs,
1660
+ previousVersion: parsed.version,
1661
+ cumulativeDurationMs: args.existing.cumulativeDurationMs,
1662
+ ...args.existing.cumulativeUsage ? { cumulativeUsage: args.existing.cumulativeUsage } : {},
1663
+ ...args.existing.destination ? { destination: args.existing.destination } : {},
1664
+ ...args.existing.dispatchId ? { dispatchId: args.existing.dispatchId } : {},
1665
+ ...args.existing.dispatchOutcome ? { dispatchOutcome: args.existing.dispatchOutcome } : {},
1666
+ ...args.existing.resultMessageId ? { resultMessageId: args.existing.resultMessageId } : {},
1667
+ ...args.existing.source ? { source: args.existing.source } : {},
1668
+ ...args.existing.loadedSkillNames ? { loadedSkillNames: args.existing.loadedSkillNames } : {},
1669
+ ...args.existing.modelId ? { modelId: args.existing.modelId } : {},
1670
+ ...args.existing.reasoningLevel ? { reasoningLevel: args.existing.reasoningLevel } : {},
1671
+ ...args.existing.actor ? { actor: args.existing.actor } : {},
1672
+ actors: args.existing.actors,
1673
+ ...args.existing.resumeReason ? { resumeReason: args.existing.resumeReason } : {},
1674
+ ...args.existing.resumedFromSliceId !== void 0 ? { resumedFromSliceId: args.existing.resumedFromSliceId } : {},
1675
+ ...args.existing.surface ? { surface: args.existing.surface } : {},
1676
+ ...args.existing.traceId ? { traceId: args.existing.traceId } : {},
1677
+ ...args.errorMessage ?? args.existing.errorMessage ? { errorMessage: args.errorMessage ?? args.existing.errorMessage } : {}
1678
+ })
1679
+ });
1680
+ }
1681
+ async function upsertAgentTurnSessionRecord(args) {
1682
+ const existingRecord = await getStoredAgentTurnSessionRecord(
1683
+ args.conversationId,
1684
+ args.sessionId
1685
+ );
1686
+ const existingDispatchId = existingRecord?.dispatchId ?? (await listAgentTurnSessionSummariesForConversation(args.conversationId)).find((summary) => summary.sessionId === args.sessionId)?.dispatchId;
1687
+ if (existingDispatchId && args.dispatchId && existingDispatchId !== args.dispatchId) {
1688
+ throw new Error(
1689
+ `Turn session ${args.sessionId} dispatchId cannot be changed`
1690
+ );
1814
1691
  }
1815
- return ` [image context: ${summaries.join(" | ")}]`;
1692
+ const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
1693
+ const instructionActor = args.actor ?? existingRecord?.actor;
1694
+ const commit = await commitMessages({
1695
+ conversationId: args.conversationId,
1696
+ messages: args.piMessages,
1697
+ ...instructionActor ? { newMessageProvenance: instructionProvenanceFor(instructionActor) } : {},
1698
+ ...args.trailingMessageProvenance ? { trailingMessageProvenance: args.trailingMessageProvenance } : {},
1699
+ ...args.turnContexts && args.turnContexts.length > 0 ? {
1700
+ turnContext: {
1701
+ contexts: args.turnContexts,
1702
+ turnId: args.sessionId
1703
+ }
1704
+ } : {}
1705
+ });
1706
+ const durableTurnStartMessageIndex = args.turnStartMessageIndex === void 0 ? void 0 : stripRuntimeTurnContext(
1707
+ args.piMessages.slice(0, args.turnStartMessageIndex)
1708
+ ).length;
1709
+ const runtimeContext = retainRuntimeTurnContext(args.piMessages);
1710
+ const retainedRuntimeContext = runtimeContext.length > 0 ? runtimeContext : existingRecord?.historyVersion === commit.historyVersion ? existingRecord.runtimeContext : void 0;
1711
+ const turnStartSeq = durableTurnStartMessageIndex === void 0 ? existingRecord?.turnStartSeq : durableTurnStartMessageIndex <= 0 ? -1 : commit.messageSeqs[durableTurnStartMessageIndex - 1] ?? commit.committedSeq;
1712
+ const turnStartMessageIndex = durableTurnStartMessageIndex ?? (turnStartSeq === void 0 ? void 0 : commit.messageSeqs.filter((seq) => seq <= turnStartSeq).length);
1713
+ return await setStoredRecord({
1714
+ conversationStore: args.conversationStore,
1715
+ destinationVisibility: args.destinationVisibility,
1716
+ piMessages: commit.messages,
1717
+ piMessageProvenance: commit.provenance,
1718
+ ttlMs,
1719
+ ...turnStartMessageIndex !== void 0 ? { turnStartMessageIndex } : {},
1720
+ record: buildStoredRecord({
1721
+ ...args.channelName ?? existingRecord?.channelName ? { channelName: args.channelName ?? existingRecord?.channelName } : {},
1722
+ conversationId: args.conversationId,
1723
+ sessionId: args.sessionId,
1724
+ sliceId: args.sliceId,
1725
+ state: args.state,
1726
+ ...existingRecord?.startedAtMs !== void 0 ? { startedAtMs: existingRecord.startedAtMs } : {},
1727
+ ...args.lastProgressAtMs !== void 0 ? { lastProgressAtMs: args.lastProgressAtMs } : {},
1728
+ committedSeq: commit.committedSeq,
1729
+ historyVersion: commit.historyVersion,
1730
+ ...turnStartSeq !== void 0 ? { turnStartSeq } : {},
1731
+ ...retainedRuntimeContext ? { runtimeContext: retainedRuntimeContext } : {},
1732
+ previousVersion: existingRecord?.version,
1733
+ cumulativeDurationMs: args.cumulativeDurationMs ?? existingRecord?.cumulativeDurationMs ?? 0,
1734
+ ...args.cumulativeUsage ? { cumulativeUsage: args.cumulativeUsage } : {},
1735
+ ...args.destination ?? existingRecord?.destination ? { destination: args.destination ?? existingRecord?.destination } : {},
1736
+ ...args.dispatchId ?? existingRecord?.dispatchId ? { dispatchId: args.dispatchId ?? existingRecord?.dispatchId } : {},
1737
+ ...args.dispatchOutcome ?? existingRecord?.dispatchOutcome ? {
1738
+ dispatchOutcome: args.dispatchOutcome ?? existingRecord?.dispatchOutcome
1739
+ } : {},
1740
+ ...args.resultMessageId ?? existingRecord?.resultMessageId ? {
1741
+ resultMessageId: args.resultMessageId ?? existingRecord?.resultMessageId
1742
+ } : {},
1743
+ ...args.source ?? existingRecord?.source ? { source: args.source ?? existingRecord?.source } : {},
1744
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
1745
+ modelId: args.modelId,
1746
+ ...args.reasoningLevel ?? existingRecord?.reasoningLevel ? {
1747
+ reasoningLevel: args.reasoningLevel ?? existingRecord?.reasoningLevel
1748
+ } : {},
1749
+ ...args.actor ?? existingRecord?.actor ? { actor: args.actor ?? existingRecord?.actor } : {},
1750
+ actors: instructionActors([
1751
+ ...(existingRecord?.actors ?? []).map(instructionProvenanceFor),
1752
+ ...(args.actors ?? []).map(instructionProvenanceFor),
1753
+ ...commit.provenance
1754
+ ]),
1755
+ ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
1756
+ ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
1757
+ ...args.resumedFromSliceId !== void 0 ? { resumedFromSliceId: args.resumedFromSliceId } : {},
1758
+ ...args.surface ?? existingRecord?.surface ? { surface: args.surface ?? existingRecord?.surface } : {},
1759
+ ...args.traceId ?? existingRecord?.traceId ? { traceId: args.traceId ?? existingRecord?.traceId } : {}
1760
+ })
1761
+ });
1816
1762
  }
1817
- function renderConversationMessageLine(message, conversation) {
1818
- const displayName = conversationAuthorDisplayName(message);
1819
- const text = message.text.replace(/\s+/g, " ").slice(0, CONTEXT_MAX_MESSAGE_CHARS);
1820
- const markers = [];
1821
- if (message.meta?.replied === false) {
1822
- markers.push(
1823
- `assistant skipped: ${message.meta?.skippedReason ?? "no-reply route"}`
1763
+ async function recordAgentTurnSessionSummary(args) {
1764
+ const stored = await getStoredAgentTurnSessionRecord(
1765
+ args.conversationId,
1766
+ args.sessionId
1767
+ );
1768
+ const priorSummary = (await listAgentTurnSessionSummariesForConversation(args.conversationId)).find((summary2) => summary2.sessionId === args.sessionId);
1769
+ const existing = stored ?? priorSummary;
1770
+ const existingDispatchId = existing?.dispatchId;
1771
+ const existingDispatchOutcome = priorSummary?.dispatchOutcome ?? stored?.dispatchOutcome;
1772
+ const existingResultMessageId = priorSummary?.resultMessageId ?? stored?.resultMessageId;
1773
+ if (existingDispatchId && args.dispatchId && existingDispatchId !== args.dispatchId) {
1774
+ throw new Error(
1775
+ `Turn session ${args.sessionId} dispatchId cannot be changed`
1824
1776
  );
1825
1777
  }
1826
- if (message.meta?.explicitMention) {
1827
- markers.push("explicit mention");
1828
- }
1829
- const markerSuffix = markers.length > 0 ? ` (${markers.join("; ")})` : "";
1830
- const imageContext = buildImageContextSuffix(message, conversation);
1831
- return `[${message.role}] ${displayName}: ${text}${imageContext}${markerSuffix}`;
1832
- }
1833
- function conversationAuthorDisplayName(message) {
1834
- const author = message.author;
1835
- const fullName = authorDisplayField(author?.fullName, author?.userId);
1836
- const userName = authorDisplayField(author?.userName, author?.userId);
1837
- return fullName ?? userName ?? (message.role === "assistant" ? botConfig.userName : message.role);
1778
+ const nowMs = Date.now();
1779
+ const ttlMs = Math.max(1, args.ttlMs ?? AGENT_TURN_SESSION_TTL_MS);
1780
+ const summary = {
1781
+ version: existing?.version ?? 0,
1782
+ ...args.channelName ?? existing?.channelName ? { channelName: args.channelName ?? existing?.channelName } : {},
1783
+ conversationId: args.conversationId,
1784
+ sessionId: args.sessionId,
1785
+ sliceId: args.sliceId,
1786
+ startedAtMs: existing?.startedAtMs ?? args.startedAtMs ?? nowMs,
1787
+ lastProgressAtMs: args.lastProgressAtMs ?? nowMs,
1788
+ state: args.state,
1789
+ updatedAtMs: nowMs,
1790
+ cumulativeDurationMs: args.cumulativeDurationMs ?? existing?.cumulativeDurationMs ?? 0,
1791
+ ...args.cumulativeUsage ?? existing?.cumulativeUsage ? { cumulativeUsage: args.cumulativeUsage ?? existing?.cumulativeUsage } : {},
1792
+ ...args.destination ?? existing?.destination ? { destination: args.destination ?? existing?.destination } : {},
1793
+ ...args.dispatchId ?? existingDispatchId ? { dispatchId: args.dispatchId ?? existingDispatchId } : {},
1794
+ ...args.dispatchOutcome ?? existingDispatchOutcome ? { dispatchOutcome: args.dispatchOutcome ?? existingDispatchOutcome } : {},
1795
+ ...args.resultMessageId ?? existingResultMessageId ? { resultMessageId: args.resultMessageId ?? existingResultMessageId } : {},
1796
+ ...args.source ?? existing?.source ? { source: args.source ?? existing?.source } : {},
1797
+ ...args.actor ?? existing?.actor ? { actor: args.actor ?? existing?.actor } : {},
1798
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : existing?.loadedSkillNames ? { loadedSkillNames: existing.loadedSkillNames } : {},
1799
+ ...args.modelId ?? existing?.modelId ? { modelId: args.modelId ?? existing?.modelId } : {},
1800
+ ...args.reasoningLevel ?? existing?.reasoningLevel ? { reasoningLevel: args.reasoningLevel ?? existing?.reasoningLevel } : {},
1801
+ ...args.resumeReason ? { resumeReason: args.resumeReason } : {},
1802
+ ...args.surface ?? existing?.surface ? { surface: args.surface ?? existing?.surface } : {},
1803
+ ...args.traceId ?? existing?.traceId ? { traceId: args.traceId ?? existing?.traceId } : {}
1804
+ };
1805
+ await recordConversationActivityMetadata({
1806
+ conversationStore: args.conversationStore,
1807
+ destinationVisibility: args.destinationVisibility,
1808
+ nowMs,
1809
+ summary
1810
+ });
1811
+ await appendAgentTurnSessionSummary(summary, ttlMs);
1838
1812
  }
1839
- function authorDisplayField(value, userId) {
1840
- if (!value || value === userId || SLACK_USER_ID_DISPLAY_PATTERN.test(value)) {
1841
- return void 0;
1813
+ async function readAgentTurnSessionSummariesFromIndex(key, stateAdapter) {
1814
+ await stateAdapter.connect();
1815
+ const values = await stateAdapter.getList(key);
1816
+ const summaries = /* @__PURE__ */ new Map();
1817
+ for (const value of [...values].reverse()) {
1818
+ let summary;
1819
+ try {
1820
+ summary = parseAgentTurnSessionSummary(value);
1821
+ } catch (error) {
1822
+ logWarn("agent.turn.session_summary_parse.failed", {
1823
+ "app.state.key": key,
1824
+ "exception.message": error instanceof Error ? error.message : String(error)
1825
+ });
1826
+ continue;
1827
+ }
1828
+ const summaryKey = `${summary.conversationId}:${summary.sessionId}`;
1829
+ if (!summaries.has(summaryKey)) {
1830
+ summaries.set(summaryKey, summary);
1831
+ }
1842
1832
  }
1843
- return value;
1844
- }
1845
- function updateConversationStats(conversation) {
1846
- const contextText = buildConversationContext(conversation);
1847
- conversation.stats.estimatedContextTokens = estimateTextTokens(
1848
- contextText ?? ""
1833
+ return [...summaries.values()].sort(
1834
+ (left, right) => right.updatedAtMs - left.updatedAtMs
1849
1835
  );
1850
- conversation.stats.totalMessageCount = conversation.messages.length;
1851
- conversation.stats.updatedAtMs = Date.now();
1852
1836
  }
1853
- function upsertConversationMessage(conversation, message) {
1854
- const existingIndex = conversation.messages.findIndex(
1855
- (entry) => entry.id === message.id
1856
- );
1857
- if (existingIndex >= 0) {
1858
- conversation.messages[existingIndex] = {
1859
- ...conversation.messages[existingIndex],
1860
- ...message,
1861
- meta: {
1862
- ...conversation.messages[existingIndex]?.meta,
1863
- ...message.meta
1864
- }
1865
- };
1866
- updateConversationStats(conversation);
1867
- return message.id;
1837
+ async function listAgentTurnSessionSummariesForConversation(conversationId) {
1838
+ const stateAdapter = getStateAdapter();
1839
+ const summaries = await listBoundedAgentTurnSessionSummariesForConversation(conversationId);
1840
+ if (summaries.length > 0) {
1841
+ return summaries;
1868
1842
  }
1869
- conversation.messages.push(message);
1870
- updateConversationStats(conversation);
1871
- return message.id;
1872
- }
1873
- function recordDeliveredAssistantMessage(args) {
1874
- const prefix = `${args.sessionId}:assistant:`;
1875
- const ordinal = args.conversation.messages.filter(
1876
- (message) => message.id.startsWith(prefix)
1877
- ).length + 1;
1878
- const messageId = `${prefix}${ordinal}`;
1879
- markConversationMessage(args.conversation, args.userMessageId, {
1880
- replied: true,
1881
- skippedReason: void 0
1882
- });
1883
- upsertConversationMessage(args.conversation, {
1884
- id: messageId,
1885
- role: "assistant",
1886
- text: normalizeConversationText(args.text),
1887
- createdAtMs: Date.now(),
1888
- author: {
1889
- userName: botConfig.userName,
1890
- isBot: true
1891
- },
1892
- meta: {
1893
- replied: true
1894
- }
1895
- });
1896
- return messageId;
1843
+ return (await readAgentTurnSessionSummariesFromIndex(
1844
+ AGENT_TURN_SESSION_INDEX_KEY,
1845
+ stateAdapter
1846
+ )).filter((summary) => summary.conversationId === conversationId);
1897
1847
  }
1898
- function turnHasReply(conversation, turnId) {
1899
- const assistantPrefix = `${turnId}:assistant:`;
1900
- return conversation.messages.some(
1901
- (message) => message.role === "assistant" && message.id.startsWith(assistantPrefix)
1848
+ async function listBoundedAgentTurnSessionSummariesForConversation(conversationId) {
1849
+ return readAgentTurnSessionSummariesFromIndex(
1850
+ agentTurnSessionConversationIndexKey(conversationId),
1851
+ getStateAdapter()
1902
1852
  );
1903
1853
  }
1904
- function markConversationMessage(conversation, messageId, patch) {
1905
- if (!messageId) return;
1906
- const messageIndex = conversation.messages.findIndex(
1907
- (entry) => entry.id === messageId
1854
+ async function abandonAgentTurnSessionRecord(args) {
1855
+ const existing = await getAgentTurnSessionRecord(
1856
+ args.conversationId,
1857
+ args.sessionId
1908
1858
  );
1909
- if (messageIndex < 0) return;
1910
- const current = conversation.messages[messageIndex];
1911
- conversation.messages[messageIndex] = {
1912
- ...current,
1913
- meta: {
1914
- ...current.meta ?? {},
1915
- ...patch
1916
- }
1917
- };
1918
- updateConversationStats(conversation);
1859
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned") {
1860
+ return void 0;
1861
+ }
1862
+ return await updateAgentTurnSessionState({
1863
+ existing,
1864
+ state: "abandoned",
1865
+ errorMessage: args.errorMessage ?? existing.errorMessage
1866
+ });
1919
1867
  }
1920
- function buildConversationContext(conversation, options = {}) {
1921
- const messages = conversation.messages.filter(
1922
- (entry) => entry.id !== options.excludeMessageId
1868
+ async function failAgentTurnSessionRecord(args) {
1869
+ const existing = await getAgentTurnSessionRecord(
1870
+ args.conversationId,
1871
+ args.sessionId
1923
1872
  );
1924
- if (messages.length === 0 && conversation.compactions.length === 0) {
1873
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned" || existing.version !== args.expectedVersion) {
1925
1874
  return void 0;
1926
1875
  }
1927
- const lines = [];
1928
- if (conversation.compactions.length > 0) {
1929
- lines.push("<thread-compactions>");
1930
- for (const [index, compaction] of conversation.compactions.entries()) {
1931
- lines.push(
1932
- ` <compaction index="${index + 1}" covered_messages="${compaction.coveredMessageCount}" created_at="${new Date(compaction.createdAtMs).toISOString()}">`,
1933
- compaction.summary,
1934
- " </compaction>"
1935
- );
1936
- }
1937
- lines.push("</thread-compactions>");
1876
+ return await updateAgentTurnSessionState({
1877
+ existing,
1878
+ state: "failed",
1879
+ errorMessage: args.errorMessage ?? existing.errorMessage
1880
+ });
1881
+ }
1882
+
1883
+ // src/chat/services/turn-limit.ts
1884
+ var TurnSliceLimitExceededError = class extends Error {
1885
+ constructor(maxSlices) {
1886
+ super(`Agent turn exceeded execution limit (${maxSlices} slices)`);
1887
+ this.name = "TurnSliceLimitExceededError";
1938
1888
  }
1939
- if (messages.length > 0) {
1940
- if (lines.length > 0) {
1941
- lines.push("");
1942
- }
1943
- lines.push("<thread-transcript>");
1944
- for (const [index, message] of messages.entries()) {
1945
- const author = escapeXml(conversationAuthorDisplayName(message));
1946
- const actorIdAttr = message.author?.userId ? ` actor_id="${escapeXml(message.author.userId)}"` : "";
1947
- const ts = new Date(message.createdAtMs).toISOString();
1948
- const slackTsAttr = message.meta?.slackTs ? ` slack_ts="${escapeXml(message.meta.slackTs)}"` : "";
1949
- lines.push(
1950
- ` <message index="${index + 1}" ts="${ts}" role="${message.role}" author="${author}"${actorIdAttr}${slackTsAttr}>`,
1951
- renderConversationMessageLine(message, conversation),
1952
- " </message>"
1953
- );
1889
+ };
1890
+ function buildTurnLimitResponse(eventId) {
1891
+ return `I couldn't finish this request because this turn reached its execution limit. Please try again with a smaller or more specific request. Reference: \`event_id=${eventId}\`.`;
1892
+ }
1893
+
1894
+ // src/chat/services/turn-session-record.ts
1895
+ function logSessionRecordError(error, eventName, args, attributes) {
1896
+ logException(error, eventName, {
1897
+ "app.ai.resume_conversation_id": args.conversationId,
1898
+ "app.ai.resume_session_id": args.sessionId,
1899
+ ...attributes
1900
+ });
1901
+ }
1902
+ function addDurationMs(prior, current) {
1903
+ const total = [prior, current].reduce((sum, value) => {
1904
+ if (typeof value !== "number" || !Number.isFinite(value)) {
1905
+ return sum;
1954
1906
  }
1955
- lines.push("</thread-transcript>");
1956
- }
1957
- return lines.join("\n");
1907
+ return (sum ?? 0) + Math.max(0, Math.floor(value));
1908
+ }, void 0);
1909
+ return total;
1958
1910
  }
1959
- function pruneCompactions(compactions) {
1960
- if (compactions.length <= CONTEXT_MAX_COMPACTIONS) {
1961
- return compactions;
1911
+ function resumableBoundary(messages, fallbackMessages) {
1912
+ const current = trimTrailingAssistantMessages(messages);
1913
+ if (current.length > 0 && isContinuablePiBoundary(current)) {
1914
+ return current;
1962
1915
  }
1963
- const overflowCount = compactions.length - CONTEXT_MAX_COMPACTIONS + 1;
1964
- const merged = compactions.slice(0, overflowCount);
1965
- const mergedSummary = merged.map((entry) => entry.summary).join("\n").slice(0, 3500);
1966
- const coveredMessageCount = merged.reduce(
1967
- (count, entry) => count + entry.coveredMessageCount,
1968
- 0
1969
- );
1970
- const compacted = {
1971
- id: generateConversationId("compaction"),
1972
- createdAtMs: Date.now(),
1973
- summary: mergedSummary,
1974
- coveredMessageCount
1916
+ return trimTrailingAssistantMessages(fallbackMessages ?? []);
1917
+ }
1918
+ async function loadTurnSessionRecord(ctx) {
1919
+ const existingSessionRecord = await getAgentTurnSessionRecordForResume(
1920
+ ctx.conversationId,
1921
+ ctx.sessionId
1922
+ );
1923
+ const hasAwaitingResumeRecord = Boolean(
1924
+ existingSessionRecord && existingSessionRecord.state === "awaiting_resume"
1925
+ );
1926
+ return {
1927
+ resumedFromSessionRecord: hasAwaitingResumeRecord,
1928
+ currentSliceId: hasAwaitingResumeRecord ? existingSessionRecord.sliceId : 1,
1929
+ existingSessionRecord
1975
1930
  };
1976
- return [compacted, ...compactions.slice(overflowCount)];
1977
1931
  }
1978
- async function summarizeConversationChunk(messages, conversation, context, deps) {
1979
- const transcript = messages.map((message) => renderConversationMessageLine(message, conversation)).join("\n");
1932
+ async function persistRunningSessionRecord(args) {
1933
+ if (args.messages.length === 0 || !isContinuablePiBoundary(args.messages)) {
1934
+ return false;
1935
+ }
1980
1936
  try {
1981
- const result = await deps.completeText({
1982
- modelId: botConfig.fastModelId,
1983
- temperature: 0,
1984
- messages: [
1985
- {
1986
- role: "user",
1987
- content: [
1988
- "Summarize the following older Slack thread transcript segment for future assistant turns.",
1989
- "Keep the summary factual and concise. Do not invent details.",
1990
- "",
1991
- "Output exactly three XML sections in this order:",
1992
- "<active-asks> one bullet per outstanding user ask that has not been narrowed, answered, or replaced by a later turn. Omit the section body if none. </active-asks>",
1993
- "<resolved-or-replaced-asks> one bullet per ask that has been replaced, narrowed, answered, or already acted on in this segment. Include the replacement/outcome inline. Omit the section body if none. </resolved-or-replaced-asks>",
1994
- "<facts> one bullet per durable fact useful regardless of scope: names, ids, URLs, decisions, locations, preferences, constraints that remain true. Omit the section body if none. </facts>",
1995
- "",
1996
- "Do not output any text outside the three sections.",
1997
- "",
1998
- transcript
1999
- ].join("\n"),
2000
- timestamp: Date.now()
2001
- }
2002
- ],
2003
- metadata: {
2004
- modelId: botConfig.fastModelId,
2005
- threadId: context.threadId ?? "",
2006
- channelId: context.channelId ?? "",
2007
- actorId: context.actorId ?? "",
2008
- runId: context.runId ?? ""
2009
- }
1937
+ const latestSessionRecord = await getAgentTurnSessionRecord(
1938
+ args.conversationId,
1939
+ args.sessionId
1940
+ );
1941
+ await upsertAgentTurnSessionRecord({
1942
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
1943
+ conversationId: args.conversationId,
1944
+ cumulativeDurationMs: latestSessionRecord?.cumulativeDurationMs,
1945
+ cumulativeUsage: latestSessionRecord?.cumulativeUsage,
1946
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
1947
+ ...args.dispatchId ?? latestSessionRecord?.dispatchId ? { dispatchId: args.dispatchId ?? latestSessionRecord?.dispatchId } : {},
1948
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
1949
+ sessionId: args.sessionId,
1950
+ sliceId: args.sliceId,
1951
+ state: "running",
1952
+ piMessages: args.messages,
1953
+ ...args.trailingMessageProvenance ? { trailingMessageProvenance: args.trailingMessageProvenance } : {},
1954
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
1955
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
1956
+ modelId: args.modelId,
1957
+ ...args.reasoningLevel ? { reasoningLevel: args.reasoningLevel } : {},
1958
+ ...args.turnContexts ? { turnContexts: args.turnContexts } : {},
1959
+ ...args.actor ?? latestSessionRecord?.actor ? { actor: args.actor ?? latestSessionRecord?.actor } : {},
1960
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {},
1961
+ ...(args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex) !== void 0 ? {
1962
+ turnStartMessageIndex: args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex
1963
+ } : {}
2010
1964
  });
2011
- const summary = result.text.trim();
2012
- if (summary.length > 0) {
2013
- return summary.slice(0, 3500);
2014
- }
2015
- } catch (error) {
2016
- logWarn(
2017
- "conversation_compaction_summary_failed",
2018
- {
2019
- messageConversationId: context.threadId,
2020
- userId: context.actorId,
2021
- destinationName: context.channelId,
2022
- runId: context.runId,
2023
- assistantUserName: botConfig.userName,
2024
- modelId: botConfig.fastModelId
2025
- },
1965
+ return true;
1966
+ } catch (recordError) {
1967
+ logSessionRecordError(
1968
+ recordError,
1969
+ "agent.turn.running_session_record.failed",
1970
+ args,
2026
1971
  {
2027
- "exception.message": error instanceof Error ? error.message : String(error),
2028
- "app.compaction_messages_covered": messages.length
2029
- },
2030
- "Compaction summarization failed; using fallback summary"
1972
+ "app.ai.resume_slice_id": args.sliceId
1973
+ }
2031
1974
  );
1975
+ return false;
2032
1976
  }
2033
- return transcript.slice(0, 2800);
2034
1977
  }
2035
- async function generateThreadTitleWithDeps(sourceText, deps) {
2036
- const result = await deps.completeText({
2037
- modelId: botConfig.fastModelId,
2038
- temperature: 0,
2039
- messages: [
2040
- {
2041
- role: "user",
2042
- content: [
2043
- "Generate a concise 5-8 word Slack conversation title from the first user message below.",
2044
- "Capture the user's main request.",
2045
- "Reply with ONLY the title, with no quotes or trailing punctuation.",
2046
- "",
2047
- `First user message: ${sourceText.slice(0, 500)}`
2048
- ].join("\n"),
2049
- timestamp: Date.now()
2050
- }
2051
- ],
2052
- metadata: {
2053
- modelId: botConfig.fastModelId
2054
- }
1978
+ async function persistCompletedSessionRecord(args) {
1979
+ let latestSessionRecord;
1980
+ await persistWithRetry(async () => {
1981
+ latestSessionRecord = await getAgentTurnSessionRecord(
1982
+ args.conversationId,
1983
+ args.sessionId
1984
+ );
1985
+ });
1986
+ const sliceId = args.sliceId ?? latestSessionRecord?.sliceId;
1987
+ if (sliceId === void 0) {
1988
+ throw new Error(
1989
+ "Completed session record requires a slice id from the caller or the latest stored record"
1990
+ );
1991
+ }
1992
+ const modelId = args.modelId;
1993
+ const reasoningLevel = args.reasoningLevel ?? latestSessionRecord?.reasoningLevel;
1994
+ const target = {
1995
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
1996
+ conversationId: args.conversationId,
1997
+ cumulativeDurationMs: addDurationMs(
1998
+ latestSessionRecord?.cumulativeDurationMs,
1999
+ args.currentDurationMs
2000
+ ),
2001
+ cumulativeUsage: addAgentTurnUsage(
2002
+ latestSessionRecord?.cumulativeUsage,
2003
+ args.currentUsage
2004
+ ),
2005
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
2006
+ ...args.dispatchId ?? latestSessionRecord?.dispatchId ? { dispatchId: args.dispatchId ?? latestSessionRecord?.dispatchId } : {},
2007
+ ...args.dispatchOutcome ?? latestSessionRecord?.dispatchOutcome ? {
2008
+ dispatchOutcome: args.dispatchOutcome ?? latestSessionRecord?.dispatchOutcome
2009
+ } : {},
2010
+ ...args.errorMessage ? { errorMessage: args.errorMessage } : {},
2011
+ ...args.resultMessageId ?? latestSessionRecord?.resultMessageId ? {
2012
+ resultMessageId: args.resultMessageId ?? latestSessionRecord?.resultMessageId
2013
+ } : {},
2014
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
2015
+ ...args.destinationVisibility ? { destinationVisibility: args.destinationVisibility } : {},
2016
+ sessionId: args.sessionId,
2017
+ sliceId,
2018
+ state: "completed",
2019
+ piMessages: args.allMessages,
2020
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
2021
+ ...args.loadedSkillNames ?? latestSessionRecord?.loadedSkillNames ? {
2022
+ loadedSkillNames: args.loadedSkillNames ?? latestSessionRecord?.loadedSkillNames
2023
+ } : {},
2024
+ modelId,
2025
+ ...reasoningLevel ? { reasoningLevel } : {},
2026
+ ...args.actor ?? latestSessionRecord?.actor ? { actor: args.actor ?? latestSessionRecord?.actor } : {},
2027
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {},
2028
+ ...(args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex) !== void 0 ? {
2029
+ turnStartMessageIndex: args.turnStartMessageIndex ?? latestSessionRecord?.turnStartMessageIndex
2030
+ } : {}
2031
+ };
2032
+ await persistWithRetry(async () => {
2033
+ await upsertAgentTurnSessionRecord(target);
2055
2034
  });
2056
- return result.text.trim().slice(0, 60);
2057
2035
  }
2058
- function getThreadTitleSourceMessage(conversation) {
2059
- let firstMessage;
2060
- for (const message of conversation.messages) {
2061
- if (!isHumanConversationMessage(message)) {
2062
- continue;
2063
- }
2064
- if (!firstMessage) {
2065
- firstMessage = message;
2066
- continue;
2067
- }
2068
- if (message.createdAtMs < firstMessage.createdAtMs) {
2069
- firstMessage = message;
2070
- continue;
2071
- }
2072
- if (message.createdAtMs === firstMessage.createdAtMs && message.id < firstMessage.id) {
2073
- firstMessage = message;
2036
+ async function completeDeliveredTurn(args) {
2037
+ await persistCompletedSessionRecord({
2038
+ channelName: args.channelName,
2039
+ conversationId: args.conversationId,
2040
+ currentDurationMs: args.durationMs,
2041
+ currentUsage: args.usage,
2042
+ destination: args.destination,
2043
+ destinationVisibility: args.destinationVisibility,
2044
+ dispatchId: args.dispatchId,
2045
+ dispatchOutcome: args.dispatchOutcome,
2046
+ errorMessage: args.errorMessage,
2047
+ resultMessageId: args.resultMessageId,
2048
+ source: args.source,
2049
+ sessionId: args.sessionId,
2050
+ sliceId: args.sliceId,
2051
+ allMessages: args.messages,
2052
+ loadedSkillNames: args.loadedSkillNames,
2053
+ modelId: args.modelId,
2054
+ actor: args.actor,
2055
+ reasoningLevel: args.reasoningLevel,
2056
+ surface: args.surface,
2057
+ turnStartMessageIndex: args.turnStartMessageIndex
2058
+ });
2059
+ }
2060
+ async function persistAuthPauseSessionRecord(args) {
2061
+ const nextSliceId = args.currentSliceId + 1;
2062
+ try {
2063
+ const latestSessionRecord = await getAgentTurnSessionRecord(
2064
+ args.conversationId,
2065
+ args.sessionId
2066
+ );
2067
+ const piMessages = resumableBoundary(
2068
+ args.messages,
2069
+ latestSessionRecord?.piMessages
2070
+ );
2071
+ if (piMessages.length > 0 && !isContinuablePiBoundary(piMessages)) {
2072
+ return void 0;
2074
2073
  }
2074
+ return await upsertAgentTurnSessionRecord({
2075
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
2076
+ conversationId: args.conversationId,
2077
+ cumulativeDurationMs: addDurationMs(
2078
+ latestSessionRecord?.cumulativeDurationMs,
2079
+ args.currentDurationMs
2080
+ ),
2081
+ cumulativeUsage: addAgentTurnUsage(
2082
+ latestSessionRecord?.cumulativeUsage,
2083
+ args.currentUsage
2084
+ ),
2085
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
2086
+ ...args.dispatchId ?? latestSessionRecord?.dispatchId ? { dispatchId: args.dispatchId ?? latestSessionRecord?.dispatchId } : {},
2087
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
2088
+ sessionId: args.sessionId,
2089
+ sliceId: nextSliceId,
2090
+ state: "awaiting_resume",
2091
+ piMessages,
2092
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
2093
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
2094
+ modelId: args.modelId,
2095
+ ...args.reasoningLevel ?? latestSessionRecord?.reasoningLevel ? {
2096
+ reasoningLevel: args.reasoningLevel ?? latestSessionRecord?.reasoningLevel
2097
+ } : {},
2098
+ resumeReason: "auth",
2099
+ resumedFromSliceId: args.currentSliceId,
2100
+ errorMessage: args.errorMessage,
2101
+ ...args.actor ?? latestSessionRecord?.actor ? { actor: args.actor ?? latestSessionRecord?.actor } : {},
2102
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
2103
+ });
2104
+ } catch (recordError) {
2105
+ logSessionRecordError(
2106
+ recordError,
2107
+ "agent.turn.auth_resume_session_record.failed",
2108
+ args,
2109
+ {
2110
+ "app.ai.resume_from_slice_id": args.currentSliceId,
2111
+ "app.ai.resume_next_slice_id": nextSliceId
2112
+ }
2113
+ );
2075
2114
  }
2076
- return firstMessage;
2115
+ return void 0;
2077
2116
  }
2078
- async function compactConversationIfNeededWithDeps(conversation, context, deps) {
2079
- updateConversationStats(conversation);
2080
- let estimatedTokens = conversation.stats.estimatedContextTokens;
2081
- setSpanAttributes({
2082
- "app.context_tokens_estimated": estimatedTokens
2083
- });
2084
- const triggerTokens = getConversationContextCompactionTriggerTokens();
2085
- const targetTokens = calculateContextCompactionTargetTokens(triggerTokens);
2086
- while (estimatedTokens > triggerTokens && conversation.messages.length > CONTEXT_MIN_LIVE_MESSAGES) {
2087
- const compactCount = Math.min(
2088
- CONTEXT_COMPACTION_BATCH_SIZE,
2089
- conversation.messages.length - CONTEXT_MIN_LIVE_MESSAGES
2117
+ async function persistContinuationSessionRecord(args) {
2118
+ const nextSliceId = args.currentSliceId + 1;
2119
+ try {
2120
+ const latestSessionRecord = await getAgentTurnSessionRecord(
2121
+ args.conversationId,
2122
+ args.sessionId
2090
2123
  );
2091
- if (compactCount <= 0) {
2092
- break;
2093
- }
2094
- const compactedChunk = conversation.messages.slice(0, compactCount);
2095
- const summary = await summarizeConversationChunk(
2096
- compactedChunk,
2097
- conversation,
2098
- context,
2099
- deps
2124
+ const piMessages = resumableBoundary(
2125
+ args.messages,
2126
+ latestSessionRecord?.piMessages
2100
2127
  );
2101
- conversation.compactions.push({
2102
- id: generateConversationId("compaction"),
2103
- createdAtMs: Date.now(),
2104
- summary,
2105
- coveredMessageCount: compactedChunk.length
2106
- });
2107
- conversation.compactions = pruneCompactions(conversation.compactions);
2108
- conversation.messages = conversation.messages.slice(compactCount);
2109
- conversation.stats.compactedMessageCount += compactCount;
2110
- updateConversationStats(conversation);
2111
- estimatedTokens = conversation.stats.estimatedContextTokens;
2112
- setSpanAttributes({
2113
- "app.compaction_messages_covered": compactCount,
2114
- "app.compaction.trigger_tokens": triggerTokens,
2115
- "app.compaction.target_tokens": targetTokens,
2116
- "app.context_tokens_estimated": estimatedTokens
2117
- });
2118
- if (estimatedTokens <= targetTokens) {
2119
- break;
2128
+ if (piMessages.length === 0 || !isContinuablePiBoundary(piMessages)) {
2129
+ return void 0;
2120
2130
  }
2121
- }
2122
- }
2123
- function createConversationMemoryService(deps) {
2124
- return {
2125
- compactConversationIfNeeded: async (conversation, context) => await compactConversationIfNeededWithDeps(conversation, context, deps),
2126
- generateThreadTitle: async (sourceText) => await generateThreadTitleWithDeps(sourceText, deps)
2127
- };
2128
- }
2129
- function isHumanConversationMessage(message) {
2130
- return message.role === "user" && message.author?.isBot !== true;
2131
- }
2132
- function getConversationMessageSlackTs(message) {
2133
- return parseSlackMessageTs(message.meta?.slackTs);
2134
- }
2135
-
2136
- // src/chat/conversations/messages.ts
2137
- import { createHash } from "crypto";
2138
- import { isDeepStrictEqual as isDeepStrictEqual3 } from "util";
2139
-
2140
- // src/chat/conversations/message-summaries.ts
2141
- import { isDeepStrictEqual as isDeepStrictEqual2 } from "util";
2142
- function projectConversationMessageSummaries(events) {
2143
- for (let index = events.length - 1; index >= 0; index -= 1) {
2144
- const data = events[index]?.data;
2145
- if (data?.type === "messages_summarized") {
2146
- return data.compactions;
2131
+ const cumulativeDurationMs = addDurationMs(
2132
+ latestSessionRecord?.cumulativeDurationMs,
2133
+ args.currentDurationMs
2134
+ );
2135
+ const cumulativeUsage = addAgentTurnUsage(
2136
+ latestSessionRecord?.cumulativeUsage,
2137
+ args.currentUsage
2138
+ );
2139
+ if (nextSliceId > botConfig.maxSlicesPerTurn) {
2140
+ return await upsertAgentTurnSessionRecord({
2141
+ ...args.channelName ?? latestSessionRecord?.channelName ? {
2142
+ channelName: args.channelName ?? latestSessionRecord?.channelName
2143
+ } : {},
2144
+ conversationId: args.conversationId,
2145
+ cumulativeDurationMs,
2146
+ cumulativeUsage,
2147
+ ...args.destination ?? latestSessionRecord?.destination ? {
2148
+ destination: args.destination ?? latestSessionRecord?.destination
2149
+ } : {},
2150
+ ...args.dispatchId ?? latestSessionRecord?.dispatchId ? { dispatchId: args.dispatchId ?? latestSessionRecord?.dispatchId } : {},
2151
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
2152
+ sessionId: args.sessionId,
2153
+ sliceId: args.currentSliceId,
2154
+ state: "failed",
2155
+ piMessages,
2156
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
2157
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
2158
+ modelId: args.modelId,
2159
+ ...args.reasoningLevel ?? latestSessionRecord?.reasoningLevel ? {
2160
+ reasoningLevel: args.reasoningLevel ?? latestSessionRecord?.reasoningLevel
2161
+ } : {},
2162
+ resumeReason: args.resumeReason,
2163
+ resumedFromSliceId: latestSessionRecord?.resumedFromSliceId,
2164
+ errorMessage: new TurnSliceLimitExceededError(
2165
+ botConfig.maxSlicesPerTurn
2166
+ ).message,
2167
+ ...args.actor ?? latestSessionRecord?.actor ? { actor: args.actor ?? latestSessionRecord?.actor } : {},
2168
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
2169
+ });
2147
2170
  }
2171
+ return await upsertAgentTurnSessionRecord({
2172
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
2173
+ conversationId: args.conversationId,
2174
+ cumulativeDurationMs,
2175
+ cumulativeUsage,
2176
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
2177
+ ...args.dispatchId ?? latestSessionRecord?.dispatchId ? { dispatchId: args.dispatchId ?? latestSessionRecord?.dispatchId } : {},
2178
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
2179
+ sessionId: args.sessionId,
2180
+ sliceId: nextSliceId,
2181
+ state: "awaiting_resume",
2182
+ piMessages,
2183
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
2184
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
2185
+ modelId: args.modelId,
2186
+ ...args.reasoningLevel ?? latestSessionRecord?.reasoningLevel ? {
2187
+ reasoningLevel: args.reasoningLevel ?? latestSessionRecord?.reasoningLevel
2188
+ } : {},
2189
+ resumeReason: args.resumeReason,
2190
+ resumedFromSliceId: args.currentSliceId,
2191
+ errorMessage: args.errorMessage,
2192
+ ...args.actor ?? latestSessionRecord?.actor ? { actor: args.actor ?? latestSessionRecord?.actor } : {},
2193
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
2194
+ });
2195
+ } catch (recordError) {
2196
+ logSessionRecordError(
2197
+ recordError,
2198
+ "agent.continue.session_record.failed",
2199
+ args,
2200
+ {
2201
+ "app.ai.resume_from_slice_id": args.currentSliceId,
2202
+ "app.ai.resume_next_slice_id": nextSliceId
2203
+ }
2204
+ );
2205
+ return void 0;
2148
2206
  }
2149
- return [];
2150
2207
  }
2151
- async function persistConversationMessageSummaries(args) {
2152
- const eventStore = getConversationEventStore();
2153
- const history = await eventStore.loadMessageHistory(args.conversationId);
2154
- const existing = history.compaction ? projectConversationMessageSummaries([history.compaction]) : [];
2155
- if (isDeepStrictEqual2(existing, args.conversation.compactions)) {
2156
- return;
2157
- }
2158
- const liveMessageIds = new Set(
2159
- args.conversation.messages.map((message) => message.id)
2160
- );
2161
- const historyFromSeq = history.events.find(
2162
- (event) => event.data.type === "message" && liveMessageIds.has(event.data.messageId)
2163
- )?.seq;
2164
- const nextHistoryFromSeq = historyFromSeq ?? Math.max(history.historyFromSeq, (history.events.at(-1)?.seq ?? -1) + 1);
2165
- await eventStore.append(args.conversationId, [
2166
- {
2167
- data: {
2168
- type: "messages_summarized",
2169
- historyFromSeq: nextHistoryFromSeq,
2170
- compactions: args.conversation.compactions
2171
- },
2172
- createdAtMs: args.conversation.compactions.at(-1)?.createdAtMs ?? Date.now()
2208
+ async function persistYieldSessionRecord(args) {
2209
+ try {
2210
+ const piMessages = [...args.messages];
2211
+ if (piMessages.length === 0 || !isContinuablePiBoundary(piMessages)) {
2212
+ return void 0;
2173
2213
  }
2174
- ]);
2175
- }
2176
-
2177
- // src/chat/conversations/message-projection.ts
2178
- function isMessageEvent(event) {
2179
- return event.data.type === "message" || event.data.type === "message_updated" || event.data.type === "message_handled";
2180
- }
2181
- function splitMeta(meta) {
2182
- const rest = { ...meta ?? {} };
2183
- const author = rest.author;
2184
- delete rest.author;
2185
- return {
2186
- ...author ? { author } : {},
2187
- ...Object.keys(rest).length > 0 ? { meta: rest } : {}
2188
- };
2189
- }
2190
- function missingBaselineError(event) {
2191
- return new Error(
2192
- `Message event ${event.data.type} at seq ${event.seq} references ${event.data.messageId} before message`
2193
- );
2194
- }
2195
- function projectConversationMessages(history) {
2196
- const byId = /* @__PURE__ */ new Map();
2197
- for (const event of history.events) {
2198
- if (!isMessageEvent(event)) continue;
2199
- const data = event.data;
2200
- const current = byId.get(data.messageId);
2201
- if (data.type === "message" || data.type === "message_updated") {
2202
- const message = {
2203
- id: data.messageId,
2204
- role: data.role,
2205
- text: data.text,
2206
- createdAtMs: event.createdAtMs,
2207
- ...splitMeta(data.meta)
2208
- };
2209
- if (data.type === "message" && current)
2210
- throw new Error(
2211
- `Duplicate message event for ${data.messageId} at seq ${event.seq}`
2212
- );
2213
- if (data.type === "message_updated" && !current) {
2214
- if (history.historyFromSeq > 0) continue;
2215
- throw missingBaselineError(event);
2214
+ const latestSessionRecord = await getAgentTurnSessionRecord(
2215
+ args.conversationId,
2216
+ args.sessionId
2217
+ );
2218
+ return await upsertAgentTurnSessionRecord({
2219
+ ...args.channelName ?? latestSessionRecord?.channelName ? { channelName: args.channelName ?? latestSessionRecord?.channelName } : {},
2220
+ conversationId: args.conversationId,
2221
+ cumulativeDurationMs: addDurationMs(
2222
+ latestSessionRecord?.cumulativeDurationMs,
2223
+ args.currentDurationMs
2224
+ ),
2225
+ cumulativeUsage: addAgentTurnUsage(
2226
+ latestSessionRecord?.cumulativeUsage,
2227
+ args.currentUsage
2228
+ ),
2229
+ ...args.destination ?? latestSessionRecord?.destination ? { destination: args.destination ?? latestSessionRecord?.destination } : {},
2230
+ ...args.dispatchId ?? latestSessionRecord?.dispatchId ? { dispatchId: args.dispatchId ?? latestSessionRecord?.dispatchId } : {},
2231
+ ...args.source ?? latestSessionRecord?.source ? { source: args.source ?? latestSessionRecord?.source } : {},
2232
+ sessionId: args.sessionId,
2233
+ sliceId: args.currentSliceId,
2234
+ state: "awaiting_resume",
2235
+ piMessages,
2236
+ ...args.surface ?? latestSessionRecord?.surface ? { surface: args.surface ?? latestSessionRecord?.surface } : {},
2237
+ ...args.loadedSkillNames ? { loadedSkillNames: args.loadedSkillNames } : {},
2238
+ modelId: args.modelId,
2239
+ ...args.reasoningLevel ?? latestSessionRecord?.reasoningLevel ? {
2240
+ reasoningLevel: args.reasoningLevel ?? latestSessionRecord?.reasoningLevel
2241
+ } : {},
2242
+ resumeReason: "yield",
2243
+ resumedFromSliceId: latestSessionRecord?.resumedFromSliceId,
2244
+ errorMessage: args.errorMessage,
2245
+ ...args.actor ?? latestSessionRecord?.actor ? { actor: args.actor ?? latestSessionRecord?.actor } : {},
2246
+ ...getActiveTraceId() ?? latestSessionRecord?.traceId ? { traceId: getActiveTraceId() ?? latestSessionRecord?.traceId } : {}
2247
+ });
2248
+ } catch (recordError) {
2249
+ logSessionRecordError(
2250
+ recordError,
2251
+ "agent.turn.yield_session_record.failed",
2252
+ args,
2253
+ {
2254
+ "app.ai.resume_slice_id": args.currentSliceId
2216
2255
  }
2217
- byId.set(data.messageId, {
2218
- message: {
2219
- ...message,
2220
- createdAtMs: current?.message.createdAtMs ?? message.createdAtMs
2221
- },
2222
- ...current?.repliedAtMs === void 0 ? {} : { repliedAtMs: current.repliedAtMs }
2223
- });
2224
- continue;
2225
- }
2226
- if (!current) {
2227
- if (history.historyFromSeq > 0) continue;
2228
- throw missingBaselineError(event);
2229
- }
2230
- current.repliedAtMs ??= event.createdAtMs;
2256
+ );
2257
+ return void 0;
2231
2258
  }
2232
- return [...byId.values()].map(({ message, repliedAtMs }) => {
2233
- if (repliedAtMs === void 0) return message;
2234
- return {
2235
- ...message,
2236
- meta: { ...message.meta ?? {}, replied: true }
2237
- };
2238
- });
2239
2259
  }
2240
2260
 
2241
- // src/chat/conversations/messages.ts
2242
- function toStoredConversationMessage(message) {
2243
- const meta = {};
2244
- if (message.author) meta.author = message.author;
2245
- const { replied, ...restMeta } = message.meta ?? {};
2246
- Object.assign(meta, restMeta);
2247
- if (replied === false) meta.replied = false;
2261
+ // src/chat/state/conversation.ts
2262
+ function defaultConversationState() {
2263
+ const nowMs = Date.now();
2248
2264
  return {
2249
- messageId: message.id,
2250
- role: message.role,
2251
- text: message.text,
2252
- ...Object.keys(meta).length > 0 ? { meta } : {},
2253
- createdAtMs: message.createdAtMs
2265
+ schemaVersion: 1,
2266
+ messages: [],
2267
+ compactions: [],
2268
+ backfill: {},
2269
+ processing: {},
2270
+ stats: {
2271
+ estimatedContextTokens: 0,
2272
+ totalMessageCount: 0,
2273
+ compactedMessageCount: 0,
2274
+ updatedAtMs: nowMs
2275
+ },
2276
+ vision: {
2277
+ byFileId: {}
2278
+ }
2254
2279
  };
2255
2280
  }
2256
- function canonicalJson(value) {
2257
- if (Array.isArray(value)) return value.map(canonicalJson);
2258
- if (!value || typeof value !== "object") return value;
2259
- return Object.fromEntries(
2260
- Object.entries(value).filter(([, item]) => item !== void 0).sort(([left], [right]) => left.localeCompare(right)).map(([key, item]) => [key, canonicalJson(item)])
2261
- );
2262
- }
2263
- function messageStateKey(message) {
2264
- return createHash("sha256").update(JSON.stringify(canonicalJson(message))).digest("hex").slice(0, 32);
2281
+ function coercePendingAuthState(value) {
2282
+ if (!isRecord(value)) {
2283
+ return void 0;
2284
+ }
2285
+ const kind = value.kind;
2286
+ const provider = toOptionalString(value.provider);
2287
+ const actorId = toOptionalString(value.actorId);
2288
+ const authSessionId = toOptionalString(value.authSessionId);
2289
+ const scope = toOptionalString(value.scope);
2290
+ const sessionId = toOptionalString(value.sessionId);
2291
+ const linkSentAtMs = toOptionalNumber(value.linkSentAtMs);
2292
+ if (kind !== "mcp" && kind !== "plugin" || !provider || !actorId || kind === "mcp" && !authSessionId || !sessionId || typeof linkSentAtMs !== "number") {
2293
+ return void 0;
2294
+ }
2295
+ const base = {
2296
+ provider,
2297
+ actorId,
2298
+ ...scope ? { scope } : {},
2299
+ sessionId,
2300
+ linkSentAtMs
2301
+ };
2302
+ return kind === "mcp" ? { ...base, authSessionId, kind } : { ...base, kind };
2265
2303
  }
2266
- async function hydrateConversationMessages(args) {
2267
- if (!args.conversationId) {
2268
- args.conversation.messages = [];
2269
- return;
2304
+ function coerceThreadConversationState(value) {
2305
+ if (!isRecord(value)) {
2306
+ return defaultConversationState();
2270
2307
  }
2271
- const history = await getConversationEventStore().loadMessageHistory(
2272
- args.conversationId
2273
- );
2274
- args.conversation.compactions = history.compaction ? projectConversationMessageSummaries([history.compaction]) : [];
2275
- args.conversation.messages = projectConversationMessages(history);
2276
- updateConversationStats(args.conversation);
2308
+ const root = value;
2309
+ const rawConversation = isRecord(root.conversation) ? root.conversation : {};
2310
+ const base = defaultConversationState();
2311
+ const messages = [];
2312
+ const compactions = [];
2313
+ const rawBackfill = isRecord(rawConversation.backfill) ? rawConversation.backfill : {};
2314
+ const backfill = {
2315
+ completedAtMs: toOptionalNumber(rawBackfill.completedAtMs),
2316
+ source: rawBackfill.source === "recent_messages" || rawBackfill.source === "thread_fetch" ? rawBackfill.source : void 0
2317
+ };
2318
+ const rawProcessing = isRecord(rawConversation.processing) ? rawConversation.processing : {};
2319
+ const processing = {
2320
+ activeTurnId: toOptionalString(rawProcessing.activeTurnId),
2321
+ lastCompletedAtMs: toOptionalNumber(rawProcessing.lastCompletedAtMs),
2322
+ pendingAuth: coercePendingAuthState(rawProcessing.pendingAuth)
2323
+ };
2324
+ const rawStats = isRecord(rawConversation.stats) ? rawConversation.stats : {};
2325
+ const stats = {
2326
+ estimatedContextTokens: toOptionalNumber(rawStats.estimatedContextTokens) ?? base.stats.estimatedContextTokens,
2327
+ totalMessageCount: toOptionalNumber(rawStats.totalMessageCount) ?? messages.length,
2328
+ compactedMessageCount: toOptionalNumber(rawStats.compactedMessageCount) ?? 0,
2329
+ updatedAtMs: toOptionalNumber(rawStats.updatedAtMs) ?? base.stats.updatedAtMs
2330
+ };
2331
+ const rawVision = isRecord(rawConversation.vision) ? rawConversation.vision : {};
2332
+ const rawVisionByFileId = isRecord(rawVision.byFileId) ? rawVision.byFileId : {};
2333
+ const byFileId = {};
2334
+ for (const [fileId, value2] of Object.entries(rawVisionByFileId)) {
2335
+ if (typeof fileId !== "string" || fileId.trim().length === 0) continue;
2336
+ if (!isRecord(value2)) continue;
2337
+ const summary = toOptionalString(value2.summary);
2338
+ const analyzedAtMs = toOptionalNumber(value2.analyzedAtMs);
2339
+ if (!summary || !analyzedAtMs) continue;
2340
+ byFileId[fileId] = {
2341
+ summary,
2342
+ analyzedAtMs
2343
+ };
2344
+ }
2345
+ return {
2346
+ schemaVersion: 1,
2347
+ messages,
2348
+ compactions,
2349
+ backfill,
2350
+ processing,
2351
+ stats,
2352
+ vision: {
2353
+ backfillCompletedAtMs: toOptionalNumber(rawVision.backfillCompletedAtMs),
2354
+ byFileId
2355
+ }
2356
+ };
2277
2357
  }
2278
- async function persistConversationMessages(args) {
2279
- if (!args.conversationId || args.conversation.messages.length === 0) return;
2280
- const history = await getConversationEventStore().loadMessageHistory(
2281
- args.conversationId
2282
- );
2283
- const existingMessages = new Map(
2284
- projectConversationMessages(history).map((message) => [
2285
- message.id,
2286
- message
2287
- ])
2288
- );
2289
- const handledAtMs = args.repliedAtMs ?? Date.now();
2290
- const events = args.conversation.messages.flatMap((message) => {
2291
- const stored = toStoredConversationMessage(message);
2292
- const existing = existingMessages.get(stored.messageId);
2293
- const changed = existing !== void 0 && !isDeepStrictEqual3(toStoredConversationMessage(existing), stored);
2294
- const messageEvent = existing ? changed ? [
2295
- {
2296
- idempotencyKey: `message:${stored.messageId}:update:${messageStateKey(stored)}`,
2297
- createdAtMs: handledAtMs,
2298
- data: {
2299
- type: "message_updated",
2300
- messageId: stored.messageId,
2301
- role: stored.role,
2302
- text: stored.text,
2303
- ...stored.authorIdentityId ? { authorIdentityId: stored.authorIdentityId } : {},
2304
- ...stored.meta ? { meta: stored.meta } : {}
2305
- }
2306
- }
2307
- ] : [] : [
2308
- {
2309
- idempotencyKey: `message:${stored.messageId}`,
2310
- createdAtMs: stored.createdAtMs,
2311
- data: {
2312
- type: "message",
2313
- messageId: stored.messageId,
2314
- role: stored.role,
2315
- text: stored.text,
2316
- ...stored.authorIdentityId ? { authorIdentityId: stored.authorIdentityId } : {},
2317
- ...stored.meta ? { meta: stored.meta } : {}
2318
- }
2358
+ function buildConversationStatePatch(conversation) {
2359
+ const {
2360
+ compactions: _compactions,
2361
+ messages: _messages,
2362
+ ...scratch
2363
+ } = conversation;
2364
+ return {
2365
+ conversation: {
2366
+ ...scratch,
2367
+ schemaVersion: 1,
2368
+ stats: {
2369
+ ...conversation.stats,
2370
+ totalMessageCount: conversation.messages.length,
2371
+ updatedAtMs: Date.now()
2319
2372
  }
2320
- ];
2321
- return [
2322
- ...messageEvent,
2323
- ...message.meta?.replied === true ? [
2324
- {
2325
- idempotencyKey: `message:${stored.messageId}:handled`,
2326
- createdAtMs: handledAtMs,
2327
- data: {
2328
- type: "message_handled",
2329
- messageId: stored.messageId
2330
- }
2331
- }
2332
- ] : []
2333
- ];
2334
- });
2335
- await getConversationEventStore().append(args.conversationId, events);
2373
+ }
2374
+ };
2336
2375
  }
2337
2376
 
2338
2377
  // src/chat/state/artifacts.ts
@@ -2793,11 +2832,29 @@ export {
2793
2832
  stripRuntimeTurnContext,
2794
2833
  historyItemFromPiMessage,
2795
2834
  piMessageFromHistoryItem,
2835
+ persistConversationMessageSummaries,
2836
+ estimateTextTokens,
2837
+ getAgentContextCompactionTriggerTokens,
2838
+ getAgentContextInputLimitTokens,
2839
+ normalizeConversationText,
2840
+ updateConversationStats,
2841
+ upsertConversationMessage,
2842
+ recordDeliveredAssistantMessage,
2843
+ turnHasReply,
2844
+ markConversationMessage,
2845
+ buildConversationContext,
2846
+ getThreadTitleSourceMessage,
2847
+ createConversationMemoryService,
2848
+ isHumanConversationMessage,
2849
+ getConversationMessageSlackTs,
2850
+ hydrateConversationMessages,
2851
+ persistConversationMessages,
2796
2852
  loadProjection,
2797
2853
  loadConversationProjection,
2798
2854
  openConversationProjection,
2799
2855
  loadConnectedMcpProviders,
2800
2856
  commitMessages,
2857
+ commitAcceptedReply,
2801
2858
  recordMcpProviderConnected,
2802
2859
  recordAuthorizationRequested,
2803
2860
  recordAuthorizationCompleted,
@@ -2818,6 +2875,12 @@ export {
2818
2875
  markTurnClosed,
2819
2876
  markTurnCompleted,
2820
2877
  markTurnFailed,
2878
+ NO_REPLY_MARKER,
2879
+ isNoReplyMarker,
2880
+ containsNoReplyMarker,
2881
+ sanitizeAssistantText,
2882
+ decideReply,
2883
+ getAssistantReplyText,
2821
2884
  getAgentTurnSessionRecord,
2822
2885
  getAgentTurnSessionRecordForResume,
2823
2886
  recordAgentTurnSessionSummary,
@@ -2841,23 +2904,6 @@ export {
2841
2904
  abandonReplacedPendingAuth,
2842
2905
  isPendingAuthLatestRequest,
2843
2906
  coerceThreadConversationState,
2844
- persistConversationMessageSummaries,
2845
- estimateTextTokens,
2846
- getAgentContextCompactionTriggerTokens,
2847
- getAgentContextInputLimitTokens,
2848
- normalizeConversationText,
2849
- updateConversationStats,
2850
- upsertConversationMessage,
2851
- recordDeliveredAssistantMessage,
2852
- turnHasReply,
2853
- markConversationMessage,
2854
- buildConversationContext,
2855
- getThreadTitleSourceMessage,
2856
- createConversationMemoryService,
2857
- isHumanConversationMessage,
2858
- getConversationMessageSlackTs,
2859
- hydrateConversationMessages,
2860
- persistConversationMessages,
2861
2907
  coerceThreadArtifactsState,
2862
2908
  mergeArtifactsState,
2863
2909
  getPersistedSandboxState,