@sentry/junior 0.144.0 → 0.145.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 (88) hide show
  1. package/dist/{agent-hooks-GCUTGKY3.js → agent-hooks-L7D2BDVV.js} +10 -10
  2. package/dist/api/conversations/detail.d.ts +1 -0
  3. package/dist/api/schema/conversation.d.ts +2 -0
  4. package/dist/api/schema.js +1 -1
  5. package/dist/api.js +40 -22
  6. package/dist/app.js +335 -409
  7. package/dist/{catalog-runtime-DWVYKDC6.js → catalog-runtime-QLAC4Q3W.js} +5 -5
  8. package/dist/chat/agent/request.d.ts +1 -1
  9. package/dist/chat/agent/resume.d.ts +6 -15
  10. package/dist/chat/agent/tools.d.ts +1 -1
  11. package/dist/chat/agent-dispatch/types.d.ts +1 -1
  12. package/dist/chat/app/conversation-work.d.ts +24 -0
  13. package/dist/chat/conversations/projection.d.ts +11 -3
  14. package/dist/chat/conversations/store.d.ts +1 -1
  15. package/dist/chat/runtime/agent-run-outcome.d.ts +1 -1
  16. package/dist/chat/runtime/reply-executor.d.ts +5 -5
  17. package/dist/chat/runtime/slack-resume.d.ts +12 -4
  18. package/dist/chat/state/locks.d.ts +28 -0
  19. package/dist/chat/task-execution/checkpoint.d.ts +86 -0
  20. package/dist/chat/task-execution/paused-turn.d.ts +29 -0
  21. package/dist/chat/task-execution/queue-signing.d.ts +1 -0
  22. package/dist/chat/task-execution/queue.d.ts +7 -1
  23. package/dist/chat/task-execution/slack-work.d.ts +1 -1
  24. package/dist/chat/task-execution/state.d.ts +4 -4
  25. package/dist/chat/task-execution/store.d.ts +2 -2
  26. package/dist/chat/task-execution/turn-cursor-keys.d.ts +6 -0
  27. package/dist/chat/{state/turn-session.d.ts → task-execution/turn-cursor.d.ts} +52 -42
  28. package/dist/chat/task-execution/turn-wake.d.ts +29 -0
  29. package/dist/chat/tools/types.d.ts +1 -1
  30. package/dist/{chunk-HNO2E5WB.js → chunk-27TEJTZO.js} +1 -1
  31. package/dist/{chunk-RIFJUJSI.js → chunk-2LWPFSB4.js} +1 -1
  32. package/dist/{chunk-SBZP2J5Q.js → chunk-2UH3JI64.js} +10 -9
  33. package/dist/{chunk-QUZJDAQQ.js → chunk-2XET6PKP.js} +93 -89
  34. package/dist/{chunk-SUK7NC5A.js → chunk-5PNPQYTO.js} +2 -2
  35. package/dist/{chunk-SSDTQDYQ.js → chunk-64SBYW2O.js} +132 -127
  36. package/dist/{chunk-DVO5VKYQ.js → chunk-B46ZMVOV.js} +5 -5
  37. package/dist/{chunk-A67GZN73.js → chunk-CD2XFGYQ.js} +1 -1
  38. package/dist/{chunk-WTX2RFJG.js → chunk-DS3J6WGY.js} +1 -1
  39. package/dist/{chunk-PKH6762I.js → chunk-DXEQAIUV.js} +1 -1
  40. package/dist/{chunk-WSOLZJJS.js → chunk-EIX2LQ6L.js} +3 -3
  41. package/dist/{chunk-KQBXT5B7.js → chunk-FUBRJA5F.js} +6 -6
  42. package/dist/{chunk-I44JB7HQ.js → chunk-GDV5LXRA.js} +1 -1
  43. package/dist/{chunk-7KAW7UF4.js → chunk-GUWLDMGF.js} +4 -4
  44. package/dist/{chunk-PELZ2ZSY.js → chunk-HISR3DTV.js} +33 -61
  45. package/dist/{chunk-HD6BHR25.js → chunk-HLOUZC5C.js} +32 -34
  46. package/dist/{chunk-BBCWYXHB.js → chunk-I47UGQPJ.js} +7 -7
  47. package/dist/{chunk-IUFM5JWV.js → chunk-JAL3CH4G.js} +3 -0
  48. package/dist/{chunk-E7HCWOHG.js → chunk-JLMVCNXL.js} +2 -1
  49. package/dist/{chunk-STFCL3K4.js → chunk-JTGS3ZZE.js} +1 -1
  50. package/dist/{chunk-GKA74DRA.js → chunk-LVVSIMBB.js} +1 -1
  51. package/dist/{chunk-SHEEZSLY.js → chunk-PJ4NMVQB.js} +2 -2
  52. package/dist/{chunk-W7JFV5SB.js → chunk-QJBDABQA.js} +14 -17
  53. package/dist/{chunk-UDQMY6RS.js → chunk-QUG4JSZK.js} +43 -1
  54. package/dist/{chunk-4X5KM7Z6.js → chunk-RUSS62EN.js} +14 -14
  55. package/dist/{chunk-ZF3N3JD6.js → chunk-SIN3MXH6.js} +1 -1
  56. package/dist/{chunk-QE3TICD4.js → chunk-TZRXYWRE.js} +2 -2
  57. package/dist/{chunk-CNTWB2SM.js → chunk-VTDMB7XX.js} +1 -1
  58. package/dist/{chunk-LDWNYCZ6.js → chunk-WF5HVH3M.js} +1 -1
  59. package/dist/{chunk-KCBZSJRU.js → chunk-WR76IVA5.js} +5 -2
  60. package/dist/{chunk-3GU4PWA3.js → chunk-XFKIRN3Q.js} +29 -29
  61. package/dist/{chunk-ZZCTGAIT.js → chunk-YD54Q45G.js} +4 -4
  62. package/dist/{chunk-BEGGFVFR.js → chunk-ZIAMN2UW.js} +407 -523
  63. package/dist/{chunk-44RJWAND.js → chunk-ZIMRGFYL.js} +6 -6
  64. package/dist/{chunk-54JMYQYS.js → chunk-ZYAJMESY.js} +2 -2
  65. package/dist/cli/chat.js +32 -32
  66. package/dist/cli/check.js +6 -6
  67. package/dist/cli/plugins.js +13 -13
  68. package/dist/cli/snapshot-create.js +9 -9
  69. package/dist/cli/upgrade.js +7 -7
  70. package/dist/{db-EFLHTBU6.js → db-62PW6LM3.js} +7 -7
  71. package/dist/instrumentation.js +4 -4
  72. package/dist/nitro.js +4 -4
  73. package/dist/{oauth-callback-server-RO3IQKWM.js → oauth-callback-server-6NZNHJAM.js} +25 -25
  74. package/dist/{runner-LOWW7JKA.js → runner-GCRPG3GQ.js} +19 -18
  75. package/dist/{skills-ZLMUG7AE.js → skills-2S3MDFIQ.js} +6 -6
  76. package/dist/spawn-7WK37BTD.js +39 -0
  77. package/dist/{validation-MAKEZNTS.js → validation-HAOLCJ5A.js} +6 -6
  78. package/dist/version.js +1 -1
  79. package/dist/{work-5OAGC3I4.js → work-R4XYKMUW.js} +17 -16
  80. package/dist/worker-U2UCVR7M.js +24 -0
  81. package/package.json +4 -4
  82. package/dist/chat/agent/session.d.ts +0 -19
  83. package/dist/chat/runtime/agent-continue-runner.d.ts +0 -37
  84. package/dist/chat/services/agent-continue.d.ts +0 -29
  85. package/dist/chat/services/turn-session-record.d.ts +0 -164
  86. package/dist/chat/state/turn-session-keys.d.ts +0 -6
  87. package/dist/spawn-R3LUV6RF.js +0 -38
  88. package/dist/worker-KTGGRTTP.js +0 -24
@@ -21,18 +21,21 @@ import {
21
21
  instructionProvenanceFor,
22
22
  sanitizePostgresJson,
23
23
  withConversationEventLock
24
- } from "./chunk-SBZP2J5Q.js";
24
+ } from "./chunk-2UH3JI64.js";
25
25
  import {
26
26
  SANDBOX_WORKSPACE_ROOT,
27
27
  sleep
28
28
  } from "./chunk-EM225ITJ.js";
29
29
  import {
30
- getStateAdapter
31
- } from "./chunk-UDQMY6RS.js";
30
+ MUTATION_LOCK_TTL_MS,
31
+ fenceLock,
32
+ getStateAdapter,
33
+ withLock
34
+ } from "./chunk-QUG4JSZK.js";
32
35
  import {
33
36
  botConfig,
34
37
  unwrapCurrentInstruction
35
- } from "./chunk-3GU4PWA3.js";
38
+ } from "./chunk-XFKIRN3Q.js";
36
39
  import {
37
40
  toStoredSlackActor
38
41
  } from "./chunk-HZF5P5UN.js";
@@ -42,7 +45,7 @@ import {
42
45
  logException,
43
46
  toOptionalNumber,
44
47
  toOptionalString
45
- } from "./chunk-RIFJUJSI.js";
48
+ } from "./chunk-2LWPFSB4.js";
46
49
 
47
50
  // src/chat/turn-context-tag.ts
48
51
  var TURN_CONTEXT_TAG = "runtime-turn-context";
@@ -517,7 +520,52 @@ function connectedMcpProvidersFromEvents(events) {
517
520
  function isUserMessage(message) {
518
521
  return message.role === "user";
519
522
  }
520
- function countMatchingPrefix(left, right) {
523
+ var AgentHistoryBranchError = class extends Error {
524
+ constructor(conversationId) {
525
+ super(
526
+ `Agent history for ${conversationId} changed before its committed boundary`
527
+ );
528
+ this.name = "AgentHistoryBranchError";
529
+ }
530
+ };
531
+ function normalizeDurableMessage(message) {
532
+ return piMessageSchema.parse(
533
+ JSON.parse(JSON.stringify(sanitizePostgresJson(message)))
534
+ );
535
+ }
536
+ function durableMessageIdentity(message) {
537
+ const record = message;
538
+ if (record.role === "toolResult") {
539
+ return {
540
+ role: "toolResult",
541
+ timestamp: record.timestamp,
542
+ toolCallId: record.toolCallId,
543
+ toolName: record.toolName,
544
+ isError: record.isError,
545
+ content: record.content
546
+ };
547
+ }
548
+ return {
549
+ role: record.role,
550
+ timestamp: record.timestamp,
551
+ content: record.content
552
+ };
553
+ }
554
+ function countDurablePrefix(current, next) {
555
+ if (next.length < current.length) {
556
+ return countMatchingDeepPrefix(current, next);
557
+ }
558
+ for (let index = 0; index < current.length; index += 1) {
559
+ if (!isDeepStrictEqual3(
560
+ durableMessageIdentity(current[index]),
561
+ durableMessageIdentity(next[index])
562
+ )) {
563
+ return index;
564
+ }
565
+ }
566
+ return current.length;
567
+ }
568
+ function countMatchingDeepPrefix(left, right) {
521
569
  const limit = Math.min(left.length, right.length);
522
570
  for (let index = 0; index < limit; index += 1) {
523
571
  if (!isDeepStrictEqual3(left[index], right[index])) {
@@ -526,11 +574,6 @@ function countMatchingPrefix(left, right) {
526
574
  }
527
575
  return limit;
528
576
  }
529
- function normalizeDurableMessage(message) {
530
- return piMessageSchema.parse(
531
- JSON.parse(JSON.stringify(sanitizePostgresJson(message)))
532
- );
533
- }
534
577
  function resolveCommitProvenance(args) {
535
578
  if (args.explicitProvenance) {
536
579
  if (args.explicitProvenance.length !== args.nextMessages.length) {
@@ -672,7 +715,7 @@ async function commitMessagesLocked(args, executor) {
672
715
  const nextLocalMessages = stripRuntimeTurnContext(args.messages).map(
673
716
  normalizeDurableMessage
674
717
  );
675
- const matchingPrefix = countMatchingPrefix(
718
+ const matchingPrefix = countDurablePrefix(
676
719
  current.messages,
677
720
  nextLocalMessages
678
721
  );
@@ -710,9 +753,7 @@ async function commitMessagesLocked(args, executor) {
710
753
  ...turnContextEvents
711
754
  ]);
712
755
  } else {
713
- throw new Error(
714
- `Agent history for ${args.conversationId} changed before its committed boundary`
715
- );
756
+ throw new AgentHistoryBranchError(args.conversationId);
716
757
  }
717
758
  const committedEvents = await eventStore.loadCurrentHistory(
718
759
  args.conversationId
@@ -1056,7 +1097,25 @@ function getAssistantReplyText(message) {
1056
1097
  return decision.kind === "deliver" ? decision.text : void 0;
1057
1098
  }
1058
1099
 
1059
- // src/chat/state/turn-session.ts
1100
+ // src/chat/services/persist-retry.ts
1101
+ var PERSIST_ATTEMPTS = 3;
1102
+ async function persistWithRetry(persist) {
1103
+ let lastError;
1104
+ for (let attempt = 1; attempt <= PERSIST_ATTEMPTS; attempt += 1) {
1105
+ try {
1106
+ await persist();
1107
+ return;
1108
+ } catch (error) {
1109
+ lastError = error;
1110
+ if (attempt < PERSIST_ATTEMPTS) {
1111
+ await sleep(attempt * 100);
1112
+ }
1113
+ }
1114
+ }
1115
+ throw lastError;
1116
+ }
1117
+
1118
+ // src/chat/task-execution/turn-cursor.ts
1060
1119
  import { z as z2 } from "zod";
1061
1120
 
1062
1121
  // src/chat/repository-instructions.ts
@@ -1407,40 +1466,63 @@ function isAgentsInstructionsMessage(message) {
1407
1466
  return Boolean(findVisibleAgentsInstructions([message]));
1408
1467
  }
1409
1468
 
1410
- // src/chat/state/turn-session-keys.ts
1411
- var AGENT_TURN_SESSION_PREFIX = "junior:agent_turn_session";
1412
- function agentTurnSessionKey(conversationId, sessionId) {
1413
- return `${AGENT_TURN_SESSION_PREFIX}:${conversationId}:${sessionId}`;
1469
+ // src/chat/task-execution/turn-cursor-keys.ts
1470
+ var TURN_CURSOR_PREFIX = "junior:turn_cursor:v2";
1471
+ function turnCursorKey(conversationId, turnId) {
1472
+ return `${TURN_CURSOR_PREFIX}:${conversationId}:${turnId}`;
1414
1473
  }
1415
- function agentTurnSessionConversationIndexKey(conversationId) {
1416
- return `${AGENT_TURN_SESSION_PREFIX}:conversation:${conversationId}:index`;
1474
+ function turnCursorIndexKey(conversationId) {
1475
+ return `${TURN_CURSOR_PREFIX}:conversation:${conversationId}:index`;
1476
+ }
1477
+ function turnCursorMutationKey(conversationId, turnId) {
1478
+ return `${TURN_CURSOR_PREFIX}:mutation:${conversationId}:${turnId}`;
1417
1479
  }
1418
1480
 
1419
- // src/chat/state/turn-session.ts
1420
- var AGENT_TURN_SESSION_RESUME_TTL_MS = 24 * 60 * 60 * 1e3;
1421
- var AGENT_TURN_SESSION_TERMINAL_TTL_MS = 60 * 60 * 1e3;
1422
- function executionMetricsForSession(conversation, sessionId) {
1423
- return conversation?.executionMetrics?.runId === sessionId ? conversation.executionMetrics : void 0;
1481
+ // src/chat/task-execution/turn-cursor.ts
1482
+ var TURN_CURSOR_SCHEMA_VERSION = 2;
1483
+ var TURN_CURSOR_RESUME_TTL_MS = 24 * 60 * 60 * 1e3;
1484
+ var TURN_CURSOR_TERMINAL_TTL_MS = 60 * 60 * 1e3;
1485
+ var TURN_CURSOR_MUTATION_LOCK_WAIT_MS = 1e4;
1486
+ async function withTurnCursorMutation(conversationId, turnId, run) {
1487
+ const state = getStateAdapter();
1488
+ await state.connect();
1489
+ const result = await withLock(
1490
+ state,
1491
+ turnCursorMutationKey(conversationId, turnId),
1492
+ async (lock) => await run(async () => await fenceLock(state, lock)),
1493
+ {
1494
+ keepAlive: true,
1495
+ ttlMs: MUTATION_LOCK_TTL_MS,
1496
+ waitMs: TURN_CURSOR_MUTATION_LOCK_WAIT_MS
1497
+ }
1498
+ );
1499
+ if (!result.acquired) {
1500
+ throw new Error(`Could not acquire turn cursor lock for ${turnId}`);
1501
+ }
1502
+ return result.value;
1503
+ }
1504
+ function executionMetricsForTurn(conversation, turnId) {
1505
+ return conversation?.executionMetrics?.runId === turnId ? conversation.executionMetrics : void 0;
1424
1506
  }
1425
1507
  function definedProps(values) {
1426
1508
  return Object.fromEntries(
1427
1509
  Object.entries(values).filter(([, value]) => value !== void 0)
1428
1510
  );
1429
1511
  }
1430
- var agentTurnSessionStatusSchema = z2.enum([
1512
+ var turnStatusSchema = z2.enum([
1431
1513
  "running",
1432
- "awaiting_resume",
1514
+ "paused",
1433
1515
  "completed",
1434
1516
  "failed",
1435
1517
  "abandoned"
1436
1518
  ]);
1437
- var agentTurnSurfaceSchema = z2.enum([
1519
+ var turnSurfaceSchema = z2.enum([
1438
1520
  "slack",
1439
1521
  "api",
1440
1522
  "scheduler",
1441
1523
  "internal"
1442
1524
  ]);
1443
- var agentTurnResumeReasonSchema = z2.enum([
1525
+ var turnPauseReasonSchema = z2.enum([
1444
1526
  "timeout",
1445
1527
  "auth",
1446
1528
  "yield",
@@ -1448,33 +1530,35 @@ var agentTurnResumeReasonSchema = z2.enum([
1448
1530
  ]);
1449
1531
  var nonNegativeNumberSchema = z2.number().finite().nonnegative();
1450
1532
  var seqCursorSchema = z2.number().int().min(-1);
1451
- var agentTurnSessionSummarySchema = z2.object({
1533
+ var storedTurnSummarySchema = z2.object({
1534
+ schemaVersion: z2.literal(TURN_CURSOR_SCHEMA_VERSION),
1452
1535
  version: z2.number().int().nonnegative(),
1453
1536
  conversationId: z2.string().min(1),
1454
1537
  dispatchId: z2.string().min(1).optional(),
1455
1538
  dispatchOutcome: z2.enum(["blocked", "completed", "failed"]).optional(),
1456
1539
  resultMessageId: z2.string().min(1).optional(),
1457
- resumeReason: agentTurnResumeReasonSchema.optional(),
1458
- sessionId: z2.string().min(1),
1540
+ resumeReason: turnPauseReasonSchema.optional(),
1541
+ turnId: z2.string().min(1),
1459
1542
  sliceId: z2.number().int().nonnegative(),
1460
- state: agentTurnSessionStatusSchema,
1461
- surface: agentTurnSurfaceSchema.optional(),
1543
+ state: turnStatusSchema,
1544
+ surface: turnSurfaceSchema.optional(),
1462
1545
  updatedAtMs: nonNegativeNumberSchema
1463
1546
  }).strip();
1464
- var storedAgentTurnSessionRecordSchema = z2.object({
1547
+ var storedTurnRecordSchema = z2.object({
1548
+ schemaVersion: z2.literal(TURN_CURSOR_SCHEMA_VERSION),
1465
1549
  version: z2.number().int().nonnegative(),
1466
1550
  conversationId: z2.string().min(1),
1467
1551
  dispatchId: z2.string().min(1).optional(),
1468
1552
  dispatchOutcome: z2.enum(["blocked", "completed", "failed"]).optional(),
1469
1553
  resultMessageId: z2.string().min(1).optional(),
1470
1554
  lastProgressAtMs: nonNegativeNumberSchema,
1471
- resumeReason: agentTurnResumeReasonSchema.optional(),
1555
+ resumeReason: turnPauseReasonSchema.optional(),
1472
1556
  resumedFromSliceId: z2.number().int().nonnegative().optional(),
1473
- sessionId: z2.string().min(1),
1557
+ turnId: z2.string().min(1),
1474
1558
  sliceId: z2.number().int().nonnegative(),
1475
1559
  startedAtMs: nonNegativeNumberSchema,
1476
- state: agentTurnSessionStatusSchema,
1477
- surface: agentTurnSurfaceSchema.optional(),
1560
+ state: turnStatusSchema,
1561
+ surface: turnSurfaceSchema.optional(),
1478
1562
  traceId: z2.string().optional(),
1479
1563
  updatedAtMs: nonNegativeNumberSchema,
1480
1564
  committedSeq: seqCursorSchema,
@@ -1487,36 +1571,39 @@ function conversationExecutionFromSummary(summary) {
1487
1571
  const status = summary.state === "completed" || summary.state === "abandoned" ? "idle" : summary.state;
1488
1572
  return {
1489
1573
  status,
1490
- runId: summary.sessionId,
1574
+ runId: summary.turnId,
1491
1575
  updatedAtMs: summary.updatedAtMs
1492
1576
  };
1493
1577
  }
1494
1578
  function sessionLogActor(actor) {
1495
1579
  return actor?.platform === "slack" ? toStoredSlackActor(actor) : void 0;
1496
1580
  }
1497
- function parseAgentTurnSessionRecord(value) {
1498
- return storedAgentTurnSessionRecordSchema.parse(value);
1581
+ function parseTurnRecord(value) {
1582
+ return storedTurnRecordSchema.parse(value);
1499
1583
  }
1500
- function parseAgentTurnSessionSummary(value) {
1501
- return agentTurnSessionSummarySchema.parse(value);
1584
+ function parseTurnSummary(value) {
1585
+ return storedTurnSummarySchema.parse(value);
1502
1586
  }
1503
- function agentTurnSessionRecordTtlMs(state, ttlMs) {
1587
+ function storeTurnSummary(summary) {
1588
+ return storedTurnSummarySchema.parse(summary);
1589
+ }
1590
+ function turnCursorTtlMs(state, ttlMs) {
1504
1591
  if (ttlMs !== void 0) {
1505
1592
  return Math.max(1, ttlMs);
1506
1593
  }
1507
- return state === "running" || state === "awaiting_resume" ? AGENT_TURN_SESSION_RESUME_TTL_MS : AGENT_TURN_SESSION_TERMINAL_TTL_MS;
1594
+ return state === "running" || state === "paused" ? TURN_CURSOR_RESUME_TTL_MS : TURN_CURSOR_TERMINAL_TTL_MS;
1508
1595
  }
1509
- function agentTurnSessionIndexTtlMs(ttlMs) {
1510
- return Math.max(1, ttlMs ?? AGENT_TURN_SESSION_RESUME_TTL_MS);
1596
+ function turnCursorIndexTtlMs(ttlMs) {
1597
+ return Math.max(1, ttlMs ?? TURN_CURSOR_RESUME_TTL_MS);
1511
1598
  }
1512
- function projectAgentTurnSessionSummary(record) {
1513
- return agentTurnSessionSummarySchema.parse(record);
1599
+ function projectTurnSummary(record) {
1600
+ return parseTurnSummary(record);
1514
1601
  }
1515
- async function appendAgentTurnSessionSummary(summary, ttlMs) {
1602
+ async function appendTurnSummary(summary, ttlMs) {
1516
1603
  const stateAdapter = getStateAdapter();
1517
1604
  await stateAdapter.appendToList(
1518
- agentTurnSessionConversationIndexKey(summary.conversationId),
1519
- agentTurnSessionSummarySchema.parse(summary),
1605
+ turnCursorIndexKey(summary.conversationId),
1606
+ storeTurnSummary(summary),
1520
1607
  { ttlMs }
1521
1608
  );
1522
1609
  }
@@ -1556,12 +1643,13 @@ async function recordConversationActivityMetadata(args) {
1556
1643
  visibility: isChild ? void 0 : args.destinationVisibility
1557
1644
  });
1558
1645
  }
1559
- function materializeAgentTurnSessionRecord(stored, piProjection, turnStartMessageIndex, restoreVolatileContext = true, runtimeMetadata) {
1560
- const restoredProjection = restoreVolatileContext && (stored.state === "running" || stored.state === "awaiting_resume") ? restoreRuntimeContext(piProjection, stored.runtimeContext) : piProjection;
1646
+ function materializeTurnRecord(stored, piProjection, turnStartMessageIndex, restoreVolatileContext = true, runtimeMetadata) {
1647
+ const restoredProjection = restoreVolatileContext && (stored.state === "running" || stored.state === "paused") ? restoreRuntimeContext(piProjection, stored.runtimeContext) : piProjection;
1561
1648
  return {
1649
+ schemaVersion: stored.schemaVersion,
1562
1650
  version: stored.version,
1563
1651
  conversationId: stored.conversationId,
1564
- sessionId: stored.sessionId,
1652
+ turnId: stored.turnId,
1565
1653
  sliceId: stored.sliceId,
1566
1654
  state: stored.state,
1567
1655
  startedAtMs: stored.startedAtMs,
@@ -1625,19 +1713,14 @@ function restoreRuntimeContext(projection, runtimeContext) {
1625
1713
  ]
1626
1714
  };
1627
1715
  }
1628
- async function getStoredAgentTurnSessionRecord(conversationId, sessionId) {
1716
+ async function getStoredTurnRecord(conversationId, turnId) {
1629
1717
  const stateAdapter = getStateAdapter();
1630
1718
  await stateAdapter.connect();
1631
- const value = await stateAdapter.get(
1632
- agentTurnSessionKey(conversationId, sessionId)
1633
- );
1634
- return value == null ? void 0 : parseAgentTurnSessionRecord(value);
1719
+ const value = await stateAdapter.get(turnCursorKey(conversationId, turnId));
1720
+ return value == null ? void 0 : parseTurnRecord(value);
1635
1721
  }
1636
- async function materializeStoredAgentTurnSessionRecord(conversationId, sessionId, followCurrentReplacement) {
1637
- const parsed = await getStoredAgentTurnSessionRecord(
1638
- conversationId,
1639
- sessionId
1640
- );
1722
+ async function materializeStoredTurnRecord(conversationId, turnId, followCurrentReplacement) {
1723
+ const parsed = await getStoredTurnRecord(conversationId, turnId);
1641
1724
  if (!parsed) {
1642
1725
  return void 0;
1643
1726
  }
@@ -1646,19 +1729,19 @@ async function materializeStoredAgentTurnSessionRecord(conversationId, sessionId
1646
1729
  committedSeq: parsed.committedSeq,
1647
1730
  // Unfinished records include the current-epoch tail so parked input
1648
1731
  // appended after the last safe boundary stays model-visible on resume.
1649
- includeTail: parsed.state === "running" || parsed.state === "awaiting_resume"
1732
+ includeTail: parsed.state === "running" || parsed.state === "paused"
1650
1733
  });
1651
1734
  if (!pinnedProjection) {
1652
1735
  return void 0;
1653
1736
  }
1654
1737
  const currentHistory = await getConversationEventStore().loadCurrentHistory(conversationId);
1655
1738
  const currentHistoryVersion = currentHistory.at(-1)?.historyVersion ?? parsed.historyVersion ?? 0;
1656
- const followsReplacement = followCurrentReplacement && (parsed.state === "running" || parsed.state === "awaiting_resume") && parsed.historyVersion !== void 0 && parsed.historyVersion !== currentHistoryVersion;
1739
+ const followsReplacement = followCurrentReplacement && (parsed.state === "running" || parsed.state === "paused") && parsed.historyVersion !== void 0 && parsed.historyVersion !== currentHistoryVersion;
1657
1740
  const piProjection = followsReplacement ? projectConversationEvents(currentHistory) : pinnedProjection;
1658
1741
  const turnStartMessageIndex = followsReplacement ? 0 : parsed.turnStartSeq === void 0 ? void 0 : piProjection.seqs.filter((seq) => seq <= parsed.turnStartSeq).length;
1659
1742
  const conversation = await getConversationStore().get({ conversationId });
1660
- const executionMetrics = executionMetricsForSession(conversation, sessionId);
1661
- return materializeAgentTurnSessionRecord(
1743
+ const executionMetrics = executionMetricsForTurn(conversation, turnId);
1744
+ return materializeTurnRecord(
1662
1745
  parsed,
1663
1746
  piProjection,
1664
1747
  turnStartMessageIndex,
@@ -1670,26 +1753,19 @@ async function materializeStoredAgentTurnSessionRecord(conversationId, sessionId
1670
1753
  }
1671
1754
  );
1672
1755
  }
1673
- async function getAgentTurnSessionRecord(conversationId, sessionId) {
1674
- return await materializeStoredAgentTurnSessionRecord(
1675
- conversationId,
1676
- sessionId,
1677
- false
1678
- );
1756
+ async function getTurnRecord(conversationId, turnId) {
1757
+ return await materializeStoredTurnRecord(conversationId, turnId, false);
1679
1758
  }
1680
- async function getAgentTurnSessionRecordForResume(conversationId, sessionId) {
1681
- return await materializeStoredAgentTurnSessionRecord(
1682
- conversationId,
1683
- sessionId,
1684
- true
1685
- );
1759
+ async function getTurnRecordForResume(conversationId, turnId) {
1760
+ return await materializeStoredTurnRecord(conversationId, turnId, true);
1686
1761
  }
1687
1762
  function buildStoredRecord(args) {
1688
1763
  const nowMs = Date.now();
1689
1764
  return {
1765
+ schemaVersion: TURN_CURSOR_SCHEMA_VERSION,
1690
1766
  version: (args.previousVersion ?? 0) + 1,
1691
1767
  conversationId: args.conversationId,
1692
- sessionId: args.sessionId,
1768
+ turnId: args.turnId,
1693
1769
  sliceId: args.sliceId,
1694
1770
  state: args.state,
1695
1771
  startedAtMs: args.startedAtMs ?? nowMs,
@@ -1714,7 +1790,8 @@ function buildStoredRecord(args) {
1714
1790
  async function setStoredRecord(args) {
1715
1791
  const stateAdapter = getStateAdapter();
1716
1792
  await stateAdapter.connect();
1717
- const storedRecord = storedAgentTurnSessionRecordSchema.parse(args.record);
1793
+ const storedRecord = storedTurnRecordSchema.parse(args.record);
1794
+ await args.fence();
1718
1795
  await recordConversationActivityMetadata({
1719
1796
  actor: args.actor,
1720
1797
  conversationStore: args.conversationStore,
@@ -1722,18 +1799,24 @@ async function setStoredRecord(args) {
1722
1799
  destinationVisibility: args.destinationVisibility,
1723
1800
  nowMs: Date.now(),
1724
1801
  source: args.source,
1725
- summary: { ...storedRecord, ...args.runtimeMetadata }
1802
+ summary: {
1803
+ ...projectTurnSummary(storedRecord),
1804
+ startedAtMs: storedRecord.startedAtMs,
1805
+ ...args.runtimeMetadata
1806
+ }
1726
1807
  });
1808
+ await args.fence();
1727
1809
  await stateAdapter.set(
1728
- agentTurnSessionKey(storedRecord.conversationId, storedRecord.sessionId),
1810
+ turnCursorKey(storedRecord.conversationId, storedRecord.turnId),
1729
1811
  storedRecord,
1730
1812
  args.ttlMs
1731
1813
  );
1732
- await appendAgentTurnSessionSummary(
1733
- projectAgentTurnSessionSummary(storedRecord),
1734
- agentTurnSessionIndexTtlMs(args.indexTtlMs)
1814
+ await args.fence();
1815
+ await appendTurnSummary(
1816
+ projectTurnSummary(storedRecord),
1817
+ turnCursorIndexTtlMs(args.indexTtlMs)
1735
1818
  );
1736
- return materializeAgentTurnSessionRecord(
1819
+ return materializeTurnRecord(
1737
1820
  storedRecord,
1738
1821
  {
1739
1822
  messages: [...args.piMessages],
@@ -1744,18 +1827,19 @@ async function setStoredRecord(args) {
1744
1827
  args.runtimeMetadata
1745
1828
  );
1746
1829
  }
1747
- async function updateAgentTurnSessionState(args) {
1748
- const parsed = await getStoredAgentTurnSessionRecord(
1830
+ async function updateTurnState(args) {
1831
+ const parsed = await getStoredTurnRecord(
1749
1832
  args.existing.conversationId,
1750
- args.existing.sessionId
1833
+ args.existing.turnId
1751
1834
  );
1752
1835
  if (!parsed || parsed.version !== args.existing.version) {
1753
1836
  return void 0;
1754
1837
  }
1755
1838
  return await setStoredRecord({
1839
+ fence: args.fence,
1756
1840
  piMessages: args.existing.piMessages,
1757
1841
  piMessageProvenance: args.existing.piMessageProvenance,
1758
- ttlMs: agentTurnSessionRecordTtlMs(args.state),
1842
+ ttlMs: turnCursorTtlMs(args.state),
1759
1843
  runtimeMetadata: {
1760
1844
  cumulativeDurationMs: args.existing.cumulativeDurationMs,
1761
1845
  ...definedProps({
@@ -1771,7 +1855,7 @@ async function updateAgentTurnSessionState(args) {
1771
1855
  }),
1772
1856
  record: buildStoredRecord({
1773
1857
  conversationId: args.existing.conversationId,
1774
- sessionId: args.existing.sessionId,
1858
+ turnId: args.existing.turnId,
1775
1859
  sliceId: args.existing.sliceId,
1776
1860
  state: args.state,
1777
1861
  committedSeq: parsed.committedSeq,
@@ -1784,7 +1868,7 @@ async function updateAgentTurnSessionState(args) {
1784
1868
  errorMessage: args.errorMessage ?? args.existing.errorMessage,
1785
1869
  historyVersion: parsed.historyVersion,
1786
1870
  resumeReason: args.existing.resumeReason,
1787
- resultMessageId: args.existing.resultMessageId,
1871
+ resultMessageId: args.resultMessageId ?? args.existing.resultMessageId,
1788
1872
  resumedFromSliceId: args.existing.resumedFromSliceId,
1789
1873
  surface: args.existing.surface,
1790
1874
  traceId: args.existing.traceId,
@@ -1793,25 +1877,29 @@ async function updateAgentTurnSessionState(args) {
1793
1877
  })
1794
1878
  });
1795
1879
  }
1796
- async function upsertAgentTurnSessionRecord(args) {
1797
- const existingRecord = await getStoredAgentTurnSessionRecord(
1880
+ async function upsertTurnRecord(args) {
1881
+ return await withTurnCursorMutation(
1798
1882
  args.conversationId,
1799
- args.sessionId
1883
+ args.turnId,
1884
+ async (fence) => await upsertTurnRecordLocked(args, fence)
1885
+ );
1886
+ }
1887
+ async function upsertTurnRecordLocked(args, fence) {
1888
+ const existingRecord = await getStoredTurnRecord(
1889
+ args.conversationId,
1890
+ args.turnId
1800
1891
  );
1801
1892
  const conversation = await (args.conversationStore ?? getConversationStore()).get({
1802
1893
  conversationId: args.conversationId
1803
1894
  });
1804
- const executionMetrics = executionMetricsForSession(
1805
- conversation,
1806
- args.sessionId
1807
- );
1808
- const existingDispatchId = existingRecord?.dispatchId ?? (await listAgentTurnSessionSummariesForConversation(args.conversationId)).find((summary) => summary.sessionId === args.sessionId)?.dispatchId;
1895
+ const executionMetrics = executionMetricsForTurn(conversation, args.turnId);
1896
+ const existingDispatchId = existingRecord?.dispatchId ?? (await listTurnSummaries(args.conversationId)).find(
1897
+ (summary) => summary.turnId === args.turnId
1898
+ )?.dispatchId;
1809
1899
  if (existingDispatchId && args.dispatchId && existingDispatchId !== args.dispatchId) {
1810
- throw new Error(
1811
- `Turn session ${args.sessionId} dispatchId cannot be changed`
1812
- );
1900
+ throw new Error(`Turn ${args.turnId} dispatchId cannot be changed`);
1813
1901
  }
1814
- const ttlMs = agentTurnSessionRecordTtlMs(args.state, args.ttlMs);
1902
+ const ttlMs = turnCursorTtlMs(args.state, args.ttlMs);
1815
1903
  const instructionActor = args.actor;
1816
1904
  const commit = await commitMessages({
1817
1905
  conversationId: args.conversationId,
@@ -1821,7 +1909,7 @@ async function upsertAgentTurnSessionRecord(args) {
1821
1909
  ...args.turnContexts && args.turnContexts.length > 0 ? {
1822
1910
  turnContext: {
1823
1911
  contexts: args.turnContexts,
1824
- turnId: args.sessionId
1912
+ turnId: args.turnId
1825
1913
  }
1826
1914
  } : {}
1827
1915
  });
@@ -1834,6 +1922,7 @@ async function upsertAgentTurnSessionRecord(args) {
1834
1922
  const turnStartMessageIndex = durableTurnStartMessageIndex ?? (turnStartSeq === void 0 ? void 0 : commit.messageSeqs.filter((seq) => seq <= turnStartSeq).length);
1835
1923
  return await setStoredRecord({
1836
1924
  actor: args.actor,
1925
+ fence,
1837
1926
  conversationStore: args.conversationStore,
1838
1927
  destination: args.destination,
1839
1928
  destinationVisibility: args.destinationVisibility,
@@ -1842,7 +1931,7 @@ async function upsertAgentTurnSessionRecord(args) {
1842
1931
  piMessageProvenance: commit.provenance,
1843
1932
  ttlMs,
1844
1933
  // TODO(#1267): remove the temporary caller-to-SQL metadata write once the
1845
- // SQL-only turn-session readers have completed rollout.
1934
+ // SQL-only turn readers have completed rollout.
1846
1935
  runtimeMetadata: {
1847
1936
  channelName: args.channelName ?? conversation?.channelName,
1848
1937
  cumulativeDurationMs: args.cumulativeDurationMs ?? executionMetrics?.durationMs ?? 0,
@@ -1857,7 +1946,7 @@ async function upsertAgentTurnSessionRecord(args) {
1857
1946
  turnStartMessageIndex,
1858
1947
  record: buildStoredRecord({
1859
1948
  conversationId: args.conversationId,
1860
- sessionId: args.sessionId,
1949
+ turnId: args.turnId,
1861
1950
  sliceId: args.sliceId,
1862
1951
  state: args.state,
1863
1952
  committedSeq: commit.committedSeq,
@@ -1871,7 +1960,7 @@ async function upsertAgentTurnSessionRecord(args) {
1871
1960
  resumeReason: args.resumeReason,
1872
1961
  resultMessageId: args.resultMessageId ?? existingRecord?.resultMessageId,
1873
1962
  resumedFromSliceId: args.resumedFromSliceId,
1874
- runtimeContext: args.state === "running" || args.state === "awaiting_resume" ? retainedRuntimeContext : void 0,
1963
+ runtimeContext: args.state === "running" || args.state === "paused" ? retainedRuntimeContext : void 0,
1875
1964
  startedAtMs: existingRecord?.startedAtMs,
1876
1965
  surface: args.surface ?? existingRecord?.surface,
1877
1966
  traceId: args.traceId ?? existingRecord?.traceId,
@@ -1880,35 +1969,42 @@ async function upsertAgentTurnSessionRecord(args) {
1880
1969
  })
1881
1970
  });
1882
1971
  }
1883
- async function recordAgentTurnSessionSummary(args) {
1884
- const stored = await getStoredAgentTurnSessionRecord(
1972
+ async function recordTurnSummary(args) {
1973
+ await withTurnCursorMutation(
1885
1974
  args.conversationId,
1886
- args.sessionId
1975
+ args.turnId,
1976
+ async (fence) => await recordTurnSummaryOwned(args, fence)
1887
1977
  );
1978
+ }
1979
+ async function recordTurnSummaryOwned(args, fence) {
1980
+ const stored = await getStoredTurnRecord(args.conversationId, args.turnId);
1888
1981
  const conversationStore = args.conversationStore ?? getConversationStore();
1889
1982
  const conversation = await conversationStore.get({
1890
1983
  conversationId: args.conversationId
1891
1984
  });
1892
- const executionMetrics = executionMetricsForSession(
1893
- conversation,
1894
- args.sessionId
1985
+ const executionMetrics = executionMetricsForTurn(conversation, args.turnId);
1986
+ const priorSummary = (await listTurnSummaries(args.conversationId)).find(
1987
+ (summary2) => summary2.turnId === args.turnId
1895
1988
  );
1896
- const priorSummary = (await listAgentTurnSessionSummariesForConversation(args.conversationId)).find((summary2) => summary2.sessionId === args.sessionId);
1897
- const existing = stored ?? priorSummary;
1989
+ const terminalSummary = priorSummary && (priorSummary.state === "completed" || priorSummary.state === "failed" || priorSummary.state === "abandoned") ? priorSummary : void 0;
1990
+ const terminalRecord = stored && (stored.state === "completed" || stored.state === "failed" || stored.state === "abandoned") ? stored : void 0;
1991
+ const existing = terminalSummary ?? terminalRecord ?? stored ?? priorSummary;
1992
+ if ((terminalSummary || terminalRecord) && (args.state === "running" || args.state === "paused")) {
1993
+ return;
1994
+ }
1898
1995
  const existingDispatchId = existing?.dispatchId;
1899
1996
  const existingDispatchOutcome = priorSummary?.dispatchOutcome ?? stored?.dispatchOutcome;
1900
1997
  const existingResultMessageId = priorSummary?.resultMessageId ?? stored?.resultMessageId;
1901
1998
  if (existingDispatchId && args.dispatchId && existingDispatchId !== args.dispatchId) {
1902
- throw new Error(
1903
- `Turn session ${args.sessionId} dispatchId cannot be changed`
1904
- );
1999
+ throw new Error(`Turn ${args.turnId} dispatchId cannot be changed`);
1905
2000
  }
1906
2001
  const nowMs = Date.now();
1907
- const ttlMs = agentTurnSessionIndexTtlMs(args.ttlMs);
2002
+ const ttlMs = turnCursorIndexTtlMs(args.ttlMs);
1908
2003
  const summary = {
2004
+ schemaVersion: TURN_CURSOR_SCHEMA_VERSION,
1909
2005
  version: existing?.version ?? 0,
1910
2006
  conversationId: args.conversationId,
1911
- sessionId: args.sessionId,
2007
+ turnId: args.turnId,
1912
2008
  sliceId: args.sliceId,
1913
2009
  state: args.state,
1914
2010
  updatedAtMs: nowMs,
@@ -1920,6 +2016,7 @@ async function recordAgentTurnSessionSummary(args) {
1920
2016
  surface: args.surface ?? existing?.surface
1921
2017
  })
1922
2018
  };
2019
+ await fence();
1923
2020
  await recordConversationActivityMetadata({
1924
2021
  actor: args.actor,
1925
2022
  conversationStore,
@@ -1935,9 +2032,10 @@ async function recordAgentTurnSessionSummary(args) {
1935
2032
  startedAtMs: stored?.startedAtMs ?? args.startedAtMs ?? nowMs
1936
2033
  }
1937
2034
  });
1938
- await appendAgentTurnSessionSummary(summary, ttlMs);
2035
+ await fence();
2036
+ await appendTurnSummary(summary, ttlMs);
1939
2037
  }
1940
- async function readAgentTurnSessionSummariesFromIndex(key) {
2038
+ async function readTurnSummaries(key) {
1941
2039
  const stateAdapter = getStateAdapter();
1942
2040
  await stateAdapter.connect();
1943
2041
  const values = await stateAdapter.getList(key);
@@ -1945,11 +2043,11 @@ async function readAgentTurnSessionSummariesFromIndex(key) {
1945
2043
  for (const value of [...values].reverse()) {
1946
2044
  let summary;
1947
2045
  try {
1948
- summary = parseAgentTurnSessionSummary(value);
2046
+ summary = parseTurnSummary(value);
1949
2047
  } catch {
1950
2048
  continue;
1951
2049
  }
1952
- const summaryKey = `${summary.conversationId}:${summary.sessionId}`;
2050
+ const summaryKey = `${summary.conversationId}:${summary.turnId}`;
1953
2051
  if (!summaries.has(summaryKey)) {
1954
2052
  summaries.set(summaryKey, summary);
1955
2053
  }
@@ -1958,56 +2056,62 @@ async function readAgentTurnSessionSummariesFromIndex(key) {
1958
2056
  (left, right) => right.updatedAtMs - left.updatedAtMs
1959
2057
  );
1960
2058
  }
1961
- async function listAgentTurnSessionSummariesForConversation(conversationId) {
1962
- return readAgentTurnSessionSummariesFromIndex(
1963
- agentTurnSessionConversationIndexKey(conversationId)
1964
- );
2059
+ async function listTurnSummaries(conversationId) {
2060
+ return readTurnSummaries(turnCursorIndexKey(conversationId));
1965
2061
  }
1966
- async function abandonAgentTurnSessionRecord(args) {
1967
- const existing = await getAgentTurnSessionRecord(
2062
+ async function abandonTurnRecord(args) {
2063
+ return await withTurnCursorMutation(
1968
2064
  args.conversationId,
1969
- args.sessionId
2065
+ args.turnId,
2066
+ async (fence) => {
2067
+ const existing = await getTurnRecord(args.conversationId, args.turnId);
2068
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned") {
2069
+ return void 0;
2070
+ }
2071
+ return await updateTurnState({
2072
+ existing,
2073
+ fence,
2074
+ state: "abandoned",
2075
+ errorMessage: args.errorMessage ?? existing.errorMessage
2076
+ });
2077
+ }
1970
2078
  );
1971
- if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned") {
1972
- return void 0;
1973
- }
1974
- return await updateAgentTurnSessionState({
1975
- existing,
1976
- state: "abandoned",
1977
- errorMessage: args.errorMessage ?? existing.errorMessage
1978
- });
1979
2079
  }
1980
- async function failAgentTurnSessionRecord(args) {
1981
- const existing = await getAgentTurnSessionRecord(
2080
+ async function completeTurnRecord(args) {
2081
+ return await withTurnCursorMutation(
1982
2082
  args.conversationId,
1983
- args.sessionId
2083
+ args.turnId,
2084
+ async (fence) => {
2085
+ const existing = await getTurnRecord(args.conversationId, args.turnId);
2086
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned" || existing.version !== args.expectedVersion) {
2087
+ return void 0;
2088
+ }
2089
+ return await updateTurnState({
2090
+ existing,
2091
+ fence,
2092
+ resultMessageId: args.resultMessageId,
2093
+ state: "completed"
2094
+ });
2095
+ }
1984
2096
  );
1985
- if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned" || existing.version !== args.expectedVersion) {
1986
- return void 0;
1987
- }
1988
- return await updateAgentTurnSessionState({
1989
- existing,
1990
- state: "failed",
1991
- errorMessage: args.errorMessage ?? existing.errorMessage
1992
- });
1993
2097
  }
1994
-
1995
- // src/chat/services/persist-retry.ts
1996
- var PERSIST_ATTEMPTS = 3;
1997
- async function persistWithRetry(persist) {
1998
- let lastError;
1999
- for (let attempt = 1; attempt <= PERSIST_ATTEMPTS; attempt += 1) {
2000
- try {
2001
- await persist();
2002
- return;
2003
- } catch (error) {
2004
- lastError = error;
2005
- if (attempt < PERSIST_ATTEMPTS) {
2006
- await sleep(attempt * 100);
2098
+ async function failTurnRecord(args) {
2099
+ return await withTurnCursorMutation(
2100
+ args.conversationId,
2101
+ args.turnId,
2102
+ async (fence) => {
2103
+ const existing = await getTurnRecord(args.conversationId, args.turnId);
2104
+ if (!existing || existing.state === "completed" || existing.state === "failed" || existing.state === "abandoned" || existing.version !== args.expectedVersion) {
2105
+ return void 0;
2007
2106
  }
2107
+ return await updateTurnState({
2108
+ existing,
2109
+ fence,
2110
+ state: "failed",
2111
+ errorMessage: args.errorMessage ?? existing.errorMessage
2112
+ });
2008
2113
  }
2009
- }
2010
- throw lastError;
2114
+ );
2011
2115
  }
2012
2116
 
2013
2117
  // src/chat/services/turn-limit.ts
@@ -2021,25 +2125,56 @@ function buildTurnLimitResponse(eventId) {
2021
2125
  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}\`.`;
2022
2126
  }
2023
2127
 
2024
- // src/chat/services/turn-session-record.ts
2128
+ // src/chat/task-execution/checkpoint.ts
2025
2129
  function definedProps2(values) {
2026
2130
  return Object.fromEntries(
2027
2131
  Object.entries(values).filter(([, value]) => value !== void 0)
2028
2132
  );
2029
2133
  }
2030
- function sessionWriteContext(args, latest) {
2134
+ function addDurationMs(prior, current) {
2135
+ const total = [prior, current].reduce((sum, value) => {
2136
+ if (typeof value !== "number" || !Number.isFinite(value)) {
2137
+ return sum;
2138
+ }
2139
+ return (sum ?? 0) + Math.max(0, Math.floor(value));
2140
+ }, void 0);
2141
+ return total;
2142
+ }
2143
+ function continuableMessages(messages, fallback) {
2144
+ const current = trimTrailingAssistantMessages(messages);
2145
+ if (current.length > 0 && isContinuablePiBoundary(current)) {
2146
+ return current;
2147
+ }
2148
+ return trimTrailingAssistantMessages(fallback ?? []);
2149
+ }
2150
+ async function loadTurnCheckpoint(args) {
2151
+ const record = await getTurnRecordForResume(args.conversationId, args.turnId);
2152
+ if (record?.state === "paused") {
2153
+ return { resumed: true, sliceId: record.sliceId, record };
2154
+ }
2155
+ return { resumed: false, sliceId: 1, record };
2156
+ }
2157
+ async function saveTurnCheckpoint(args) {
2158
+ if (args.mode === "running") {
2159
+ return await saveRunning(args);
2160
+ }
2161
+ if (args.mode === "paused") {
2162
+ return await savePaused(args);
2163
+ }
2164
+ await saveDone(args);
2165
+ return void 0;
2166
+ }
2167
+ function sharedWrite(args, latest) {
2031
2168
  return {
2032
2169
  conversationId: args.conversationId,
2033
2170
  modelId: args.modelId,
2034
- sessionId: args.sessionId,
2171
+ turnId: args.turnId,
2035
2172
  ...definedProps2({
2036
- // Live only: Redis no longer stores execution actor across resumes.
2037
2173
  actor: args.actor,
2038
2174
  channelName: args.channelName ?? latest?.channelName,
2039
2175
  destination: args.destination,
2040
2176
  destinationVisibility: args.destinationVisibility,
2041
2177
  dispatchId: args.dispatchId ?? latest?.dispatchId,
2042
- // Caller-resolved: some paths inherit these, others stay caller-only.
2043
2178
  loadedSkillNames: args.loadedSkillNames,
2044
2179
  reasoningLevel: args.reasoningLevel,
2045
2180
  source: args.source,
@@ -2049,372 +2184,124 @@ function sessionWriteContext(args, latest) {
2049
2184
  })
2050
2185
  };
2051
2186
  }
2052
- function logSessionRecordError(error, eventName, args, attributes) {
2053
- logException(error, eventName, {
2054
- "app.ai.resume_conversation_id": args.conversationId,
2055
- "app.ai.resume_session_id": args.sessionId,
2056
- ...attributes
2057
- });
2058
- }
2059
- function addDurationMs(prior, current) {
2060
- const total = [prior, current].reduce((sum, value) => {
2061
- if (typeof value !== "number" || !Number.isFinite(value)) {
2062
- return sum;
2063
- }
2064
- return (sum ?? 0) + Math.max(0, Math.floor(value));
2065
- }, void 0);
2066
- return total;
2067
- }
2068
- function resumableBoundary(messages, fallbackMessages) {
2069
- const current = trimTrailingAssistantMessages(messages);
2070
- if (current.length > 0 && isContinuablePiBoundary(current)) {
2071
- return current;
2072
- }
2073
- return trimTrailingAssistantMessages(fallbackMessages ?? []);
2074
- }
2075
- async function loadTurnSessionRecord(ctx) {
2076
- const existingSessionRecord = await getAgentTurnSessionRecordForResume(
2077
- ctx.conversationId,
2078
- ctx.sessionId
2079
- );
2080
- const hasAwaitingResumeRecord = Boolean(
2081
- existingSessionRecord && existingSessionRecord.state === "awaiting_resume"
2082
- );
2083
- return {
2084
- resumedFromSessionRecord: hasAwaitingResumeRecord,
2085
- currentSliceId: hasAwaitingResumeRecord ? existingSessionRecord.sliceId : 1,
2086
- existingSessionRecord
2087
- };
2088
- }
2089
- async function persistRunningSessionRecord(args) {
2187
+ async function saveRunning(args) {
2090
2188
  if (args.messages.length === 0 || !isContinuablePiBoundary(args.messages)) {
2091
- return false;
2189
+ return void 0;
2092
2190
  }
2093
2191
  try {
2094
- const latestSessionRecord = await getAgentTurnSessionRecord(
2095
- args.conversationId,
2096
- args.sessionId
2097
- );
2098
- await upsertAgentTurnSessionRecord({
2099
- ...sessionWriteContext(
2100
- {
2101
- actor: args.actor,
2102
- channelName: args.channelName,
2103
- conversationId: args.conversationId,
2104
- destination: args.destination,
2105
- destinationVisibility: args.destinationVisibility,
2106
- dispatchId: args.dispatchId,
2107
- loadedSkillNames: args.loadedSkillNames,
2108
- modelId: args.modelId,
2109
- reasoningLevel: args.reasoningLevel,
2110
- sessionId: args.sessionId,
2111
- source: args.source,
2112
- surface: args.surface,
2113
- turnStartMessageIndex: args.turnStartMessageIndex
2114
- },
2115
- latestSessionRecord
2116
- ),
2192
+ const latest = await getTurnRecord(args.conversationId, args.turnId);
2193
+ return await upsertTurnRecord({
2194
+ ...sharedWrite(args, latest),
2117
2195
  ...definedProps2({
2118
2196
  trailingMessageProvenance: args.trailingMessageProvenance,
2119
2197
  turnContexts: args.turnContexts
2120
2198
  }),
2121
- cumulativeDurationMs: latestSessionRecord?.cumulativeDurationMs,
2122
- cumulativeUsage: latestSessionRecord?.cumulativeUsage,
2199
+ cumulativeDurationMs: latest?.cumulativeDurationMs,
2200
+ cumulativeUsage: latest?.cumulativeUsage,
2123
2201
  piMessages: args.messages,
2124
2202
  sliceId: args.sliceId,
2125
2203
  state: "running"
2126
2204
  });
2127
- return true;
2128
- } catch (recordError) {
2129
- logSessionRecordError(
2130
- recordError,
2131
- "agent.turn.running_session_record.failed",
2132
- args,
2133
- {
2205
+ } catch (error) {
2206
+ if (!(error instanceof AgentHistoryBranchError)) {
2207
+ logException(error, "agent.turn.checkpoint.running.failed", {
2208
+ "app.ai.resume_conversation_id": args.conversationId,
2209
+ "app.ai.resume_session_id": args.turnId,
2134
2210
  "app.ai.resume_slice_id": args.sliceId
2135
- }
2136
- );
2137
- return false;
2138
- }
2139
- }
2140
- async function persistCompletedSessionRecord(args) {
2141
- let latestSessionRecord;
2142
- await persistWithRetry(async () => {
2143
- latestSessionRecord = await getAgentTurnSessionRecord(
2144
- args.conversationId,
2145
- args.sessionId
2146
- );
2147
- });
2148
- const sliceId = args.sliceId ?? latestSessionRecord?.sliceId;
2149
- if (sliceId === void 0) {
2150
- throw new Error(
2151
- "Completed session record requires a slice id from the caller or the latest stored record"
2152
- );
2211
+ });
2212
+ }
2213
+ return void 0;
2153
2214
  }
2154
- const target = {
2155
- ...sessionWriteContext(
2156
- {
2157
- actor: args.actor,
2158
- channelName: args.channelName,
2159
- conversationId: args.conversationId,
2160
- destination: args.destination,
2161
- destinationVisibility: args.destinationVisibility,
2162
- dispatchId: args.dispatchId,
2163
- loadedSkillNames: args.loadedSkillNames,
2164
- modelId: args.modelId,
2165
- reasoningLevel: args.reasoningLevel,
2166
- sessionId: args.sessionId,
2167
- source: args.source,
2168
- surface: args.surface,
2169
- turnStartMessageIndex: args.turnStartMessageIndex
2170
- },
2171
- latestSessionRecord
2172
- ),
2173
- ...definedProps2({
2174
- cumulativeDurationMs: addDurationMs(
2175
- latestSessionRecord?.cumulativeDurationMs,
2176
- args.currentDurationMs
2177
- ),
2178
- cumulativeUsage: addAgentTurnUsage(
2179
- latestSessionRecord?.cumulativeUsage,
2180
- args.currentUsage
2181
- ),
2182
- dispatchOutcome: args.dispatchOutcome ?? latestSessionRecord?.dispatchOutcome,
2183
- errorMessage: args.errorMessage,
2184
- resultMessageId: args.resultMessageId ?? latestSessionRecord?.resultMessageId
2185
- }),
2186
- piMessages: args.allMessages,
2187
- sliceId,
2188
- state: "completed"
2189
- };
2190
- await persistWithRetry(async () => {
2191
- await upsertAgentTurnSessionRecord(target);
2192
- });
2193
2215
  }
2194
- async function completeDeliveredTurn(args) {
2195
- await persistCompletedSessionRecord({
2196
- channelName: args.channelName,
2197
- conversationId: args.conversationId,
2198
- currentDurationMs: args.durationMs,
2199
- currentUsage: args.usage,
2200
- destination: args.destination,
2201
- destinationVisibility: args.destinationVisibility,
2202
- dispatchId: args.dispatchId,
2203
- dispatchOutcome: args.dispatchOutcome,
2204
- errorMessage: args.errorMessage,
2205
- resultMessageId: args.resultMessageId,
2206
- source: args.source,
2207
- sessionId: args.sessionId,
2208
- sliceId: args.sliceId,
2209
- allMessages: args.messages,
2210
- loadedSkillNames: args.loadedSkillNames,
2211
- modelId: args.modelId,
2212
- actor: args.actor,
2213
- reasoningLevel: args.reasoningLevel,
2214
- surface: args.surface,
2215
- turnStartMessageIndex: args.turnStartMessageIndex
2216
- });
2217
- }
2218
- async function persistAuthPauseSessionRecord(args) {
2219
- const nextSliceId = args.currentSliceId + 1;
2216
+ async function savePaused(args) {
2217
+ const keepSlice = args.reason === "yield";
2218
+ const nextSliceId = keepSlice ? args.sliceId : args.sliceId + 1;
2220
2219
  try {
2221
- const latestSessionRecord = await getAgentTurnSessionRecord(
2222
- args.conversationId,
2223
- args.sessionId
2224
- );
2225
- const piMessages = resumableBoundary(
2226
- args.messages,
2227
- latestSessionRecord?.piMessages
2228
- );
2229
- if (piMessages.length > 0 && !isContinuablePiBoundary(piMessages)) {
2220
+ const latest = await getTurnRecord(args.conversationId, args.turnId);
2221
+ const messages = args.reason === "yield" ? [...args.messages] : continuableMessages(args.messages, latest?.piMessages);
2222
+ if (args.reason === "auth") {
2223
+ if (messages.length > 0 && !isContinuablePiBoundary(messages)) {
2224
+ return void 0;
2225
+ }
2226
+ } else if (messages.length === 0 || !isContinuablePiBoundary(messages)) {
2230
2227
  return void 0;
2231
2228
  }
2232
- return await upsertAgentTurnSessionRecord({
2233
- ...sessionWriteContext(
2234
- {
2235
- actor: args.actor,
2236
- channelName: args.channelName,
2237
- conversationId: args.conversationId,
2238
- destination: args.destination,
2239
- destinationVisibility: args.destinationVisibility,
2240
- dispatchId: args.dispatchId,
2241
- // Auth-pause keeps caller-owned skill names only.
2242
- loadedSkillNames: args.loadedSkillNames,
2243
- modelId: args.modelId,
2244
- reasoningLevel: args.reasoningLevel,
2245
- sessionId: args.sessionId,
2246
- source: args.source,
2247
- surface: args.surface
2248
- },
2249
- latestSessionRecord
2250
- ),
2229
+ const base = {
2230
+ ...sharedWrite(args, latest),
2251
2231
  ...definedProps2({
2252
2232
  cumulativeDurationMs: addDurationMs(
2253
- latestSessionRecord?.cumulativeDurationMs,
2254
- args.currentDurationMs
2233
+ latest?.cumulativeDurationMs,
2234
+ args.durationMs
2255
2235
  ),
2256
- cumulativeUsage: addAgentTurnUsage(
2257
- latestSessionRecord?.cumulativeUsage,
2258
- args.currentUsage
2259
- )
2260
- }),
2261
- errorMessage: args.errorMessage,
2262
- piMessages,
2263
- resumeReason: "auth",
2264
- resumedFromSliceId: args.currentSliceId,
2265
- sliceId: nextSliceId,
2266
- state: "awaiting_resume"
2267
- });
2268
- } catch (recordError) {
2269
- logSessionRecordError(
2270
- recordError,
2271
- "agent.turn.auth_resume_session_record.failed",
2272
- args,
2273
- {
2274
- "app.ai.resume_from_slice_id": args.currentSliceId,
2275
- "app.ai.resume_next_slice_id": nextSliceId
2276
- }
2277
- );
2278
- }
2279
- return void 0;
2280
- }
2281
- async function persistContinuationSessionRecord(args) {
2282
- const nextSliceId = args.currentSliceId + 1;
2283
- try {
2284
- const latestSessionRecord = await getAgentTurnSessionRecord(
2285
- args.conversationId,
2286
- args.sessionId
2287
- );
2288
- const piMessages = resumableBoundary(
2289
- args.messages,
2290
- latestSessionRecord?.piMessages
2291
- );
2292
- if (piMessages.length === 0 || !isContinuablePiBoundary(piMessages)) {
2293
- return void 0;
2294
- }
2295
- const cumulativeDurationMs = addDurationMs(
2296
- latestSessionRecord?.cumulativeDurationMs,
2297
- args.currentDurationMs
2298
- );
2299
- const cumulativeUsage = addAgentTurnUsage(
2300
- latestSessionRecord?.cumulativeUsage,
2301
- args.currentUsage
2302
- );
2303
- const shared = {
2304
- ...sessionWriteContext(
2305
- {
2306
- actor: args.actor,
2307
- channelName: args.channelName,
2308
- conversationId: args.conversationId,
2309
- destination: args.destination,
2310
- destinationVisibility: args.destinationVisibility,
2311
- dispatchId: args.dispatchId,
2312
- // Continuation keeps caller-owned skill names only.
2313
- loadedSkillNames: args.loadedSkillNames,
2314
- modelId: args.modelId,
2315
- reasoningLevel: args.reasoningLevel,
2316
- sessionId: args.sessionId,
2317
- source: args.source,
2318
- surface: args.surface
2319
- },
2320
- latestSessionRecord
2321
- ),
2322
- ...definedProps2({
2323
- cumulativeDurationMs,
2324
- cumulativeUsage
2236
+ cumulativeUsage: addAgentTurnUsage(latest?.cumulativeUsage, args.usage),
2237
+ errorMessage: args.errorMessage
2325
2238
  }),
2326
- piMessages,
2327
- resumeReason: args.resumeReason
2239
+ piMessages: messages,
2240
+ resumeReason: args.reason
2328
2241
  };
2329
- if (nextSliceId > botConfig.maxSlicesPerTurn) {
2330
- return await upsertAgentTurnSessionRecord({
2331
- ...shared,
2332
- ...definedProps2({
2333
- resumedFromSliceId: latestSessionRecord?.resumedFromSliceId
2334
- }),
2335
- errorMessage: new TurnSliceLimitExceededError(
2336
- botConfig.maxSlicesPerTurn
2337
- ).message,
2338
- sliceId: args.currentSliceId,
2242
+ if (!keepSlice && nextSliceId > botConfig.maxSlicesPerTurn) {
2243
+ const error = new TurnSliceLimitExceededError(botConfig.maxSlicesPerTurn);
2244
+ await upsertTurnRecord({
2245
+ ...base,
2246
+ ...definedProps2({ resumedFromSliceId: latest?.resumedFromSliceId }),
2247
+ errorMessage: error.message,
2248
+ sliceId: args.sliceId,
2339
2249
  state: "failed"
2340
2250
  });
2251
+ throw error;
2341
2252
  }
2342
- return await upsertAgentTurnSessionRecord({
2343
- ...shared,
2344
- errorMessage: args.errorMessage,
2345
- resumedFromSliceId: args.currentSliceId,
2253
+ return await upsertTurnRecord({
2254
+ ...base,
2255
+ ...definedProps2({
2256
+ resumedFromSliceId: keepSlice ? latest?.resumedFromSliceId : args.sliceId
2257
+ }),
2346
2258
  sliceId: nextSliceId,
2347
- state: "awaiting_resume"
2259
+ // Stored name is historical; means "paused, may continue".
2260
+ state: "paused"
2261
+ });
2262
+ } catch (error) {
2263
+ if (error instanceof TurnSliceLimitExceededError) {
2264
+ throw error;
2265
+ }
2266
+ logException(error, "agent.turn.checkpoint.paused.failed", {
2267
+ "app.ai.resume_conversation_id": args.conversationId,
2268
+ "app.ai.resume_session_id": args.turnId,
2269
+ "app.ai.resume_from_slice_id": args.sliceId,
2270
+ "app.ai.resume_next_slice_id": nextSliceId,
2271
+ "app.ai.resume_reason": args.reason
2348
2272
  });
2349
- } catch (recordError) {
2350
- logSessionRecordError(
2351
- recordError,
2352
- "agent.continue.session_record.failed",
2353
- args,
2354
- {
2355
- "app.ai.resume_from_slice_id": args.currentSliceId,
2356
- "app.ai.resume_next_slice_id": nextSliceId
2357
- }
2358
- );
2359
2273
  return void 0;
2360
2274
  }
2361
2275
  }
2362
- async function persistYieldSessionRecord(args) {
2363
- try {
2364
- const piMessages = [...args.messages];
2365
- if (piMessages.length === 0 || !isContinuablePiBoundary(piMessages)) {
2366
- return void 0;
2367
- }
2368
- const latestSessionRecord = await getAgentTurnSessionRecord(
2369
- args.conversationId,
2370
- args.sessionId
2276
+ async function saveDone(args) {
2277
+ let latest;
2278
+ await persistWithRetry(async () => {
2279
+ latest = await getTurnRecord(args.conversationId, args.turnId);
2280
+ });
2281
+ const sliceId = args.sliceId ?? latest?.sliceId;
2282
+ if (sliceId === void 0) {
2283
+ throw new Error(
2284
+ "Completed turn checkpoint requires a slice id from the caller or the latest stored record"
2371
2285
  );
2372
- return await upsertAgentTurnSessionRecord({
2373
- ...sessionWriteContext(
2374
- {
2375
- actor: args.actor,
2376
- channelName: args.channelName,
2377
- conversationId: args.conversationId,
2378
- destination: args.destination,
2379
- dispatchId: args.dispatchId,
2380
- // Yield keeps caller-owned skill names only.
2381
- loadedSkillNames: args.loadedSkillNames,
2382
- modelId: args.modelId,
2383
- reasoningLevel: args.reasoningLevel,
2384
- sessionId: args.sessionId,
2385
- source: args.source,
2386
- surface: args.surface
2387
- },
2388
- latestSessionRecord
2389
- ),
2286
+ }
2287
+ await persistWithRetry(async () => {
2288
+ await upsertTurnRecord({
2289
+ ...sharedWrite(args, latest),
2390
2290
  ...definedProps2({
2391
2291
  cumulativeDurationMs: addDurationMs(
2392
- latestSessionRecord?.cumulativeDurationMs,
2393
- args.currentDurationMs
2292
+ latest?.cumulativeDurationMs,
2293
+ args.durationMs
2394
2294
  ),
2395
- cumulativeUsage: addAgentTurnUsage(
2396
- latestSessionRecord?.cumulativeUsage,
2397
- args.currentUsage
2398
- ),
2399
- resumedFromSliceId: latestSessionRecord?.resumedFromSliceId
2295
+ cumulativeUsage: addAgentTurnUsage(latest?.cumulativeUsage, args.usage),
2296
+ dispatchOutcome: args.dispatchOutcome ?? latest?.dispatchOutcome,
2297
+ errorMessage: args.errorMessage,
2298
+ resultMessageId: args.resultMessageId ?? latest?.resultMessageId
2400
2299
  }),
2401
- errorMessage: args.errorMessage,
2402
- piMessages,
2403
- resumeReason: "yield",
2404
- sliceId: args.currentSliceId,
2405
- state: "awaiting_resume"
2300
+ piMessages: args.messages,
2301
+ sliceId,
2302
+ state: args.mode
2406
2303
  });
2407
- } catch (recordError) {
2408
- logSessionRecordError(
2409
- recordError,
2410
- "agent.turn.yield_session_record.failed",
2411
- args,
2412
- {
2413
- "app.ai.resume_slice_id": args.currentSliceId
2414
- }
2415
- );
2416
- return void 0;
2417
- }
2304
+ });
2418
2305
  }
2419
2306
 
2420
2307
  // src/chat/state/conversation.ts
@@ -2923,9 +2810,9 @@ function clearPendingAuth(conversation, sessionId) {
2923
2810
  }
2924
2811
  async function abandonReplacedPendingAuth(args) {
2925
2812
  if (args.previousPendingAuth && args.previousPendingAuth.sessionId !== args.nextPendingAuth.sessionId && args.conversationId) {
2926
- await abandonAgentTurnSessionRecord({
2813
+ await abandonTurnRecord({
2927
2814
  conversationId: args.conversationId,
2928
- sessionId: args.previousPendingAuth.sessionId,
2815
+ turnId: args.previousPendingAuth.sessionId,
2929
2816
  errorMessage: "Abandoned by a newer auth-blocked request in the same conversation."
2930
2817
  });
2931
2818
  }
@@ -3021,22 +2908,19 @@ export {
3021
2908
  sanitizeAssistantText,
3022
2909
  decideReply,
3023
2910
  getAssistantReplyText,
3024
- getAgentTurnSessionRecord,
3025
- getAgentTurnSessionRecordForResume,
3026
- recordAgentTurnSessionSummary,
3027
- listAgentTurnSessionSummariesForConversation,
3028
- abandonAgentTurnSessionRecord,
3029
- failAgentTurnSessionRecord,
3030
- persistWithRetry,
3031
2911
  TurnSliceLimitExceededError,
3032
2912
  buildTurnLimitResponse,
3033
- loadTurnSessionRecord,
3034
- persistRunningSessionRecord,
3035
- persistCompletedSessionRecord,
3036
- completeDeliveredTurn,
3037
- persistAuthPauseSessionRecord,
3038
- persistContinuationSessionRecord,
3039
- persistYieldSessionRecord,
2913
+ persistWithRetry,
2914
+ getTurnRecord,
2915
+ getTurnRecordForResume,
2916
+ recordTurnSummary,
2917
+ listTurnSummaries,
2918
+ abandonTurnRecord,
2919
+ completeTurnRecord,
2920
+ failTurnRecord,
2921
+ continuableMessages,
2922
+ loadTurnCheckpoint,
2923
+ saveTurnCheckpoint,
3040
2924
  canReusePendingAuthLink,
3041
2925
  getConversationPendingAuth,
3042
2926
  clearPendingAuth,