@openchambery/web 1.18.7-beta.2 → 1.18.7-beta.3

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 (133) hide show
  1. package/dist/assets/AgentAvatar-CVTgW_rx.js +1 -0
  2. package/dist/assets/AssistantView-CvQ91w2s.js +1 -0
  3. package/dist/assets/{ChatContainer-BHDNtLuy.js → ChatContainer-CERrtaB-.js} +41 -43
  4. package/dist/assets/ContextPanel-CLz4r1G-.js +285 -0
  5. package/dist/assets/DiagramView-DhV4Zthj.js +1 -0
  6. package/dist/assets/{FadeInOnReveal-BpIMrqet.js → FadeInOnReveal-Cy3sf2M5.js} +1 -1
  7. package/dist/assets/{Icon-C0G6afNV.js → Icon-BLWVqBee.js} +1 -1
  8. package/dist/assets/{MarkdownRendererImpl-B-SOb7Za.js → MarkdownRendererImpl-B4qwUu4q.js} +1 -1
  9. package/dist/assets/{MobileDetailNavigation-C7OjF65p.js → MobileDetailNavigation-CuWgEThi.js} +1 -1
  10. package/dist/assets/{MobileShareBridge-CRRrlX0l.js → MobileShareBridge-C3nay5Sq.js} +2 -2
  11. package/dist/assets/MobileSurface-D7v_ZkYO.js +2 -0
  12. package/dist/assets/{MultiRunWindow-Dx4tkVFN.js → MultiRunWindow-Cq8yyiqm.js} +1 -1
  13. package/dist/assets/{OnboardingScreen-BpxD5W4Z.js → OnboardingScreen-CTNi2zuA.js} +1 -1
  14. package/dist/assets/{OverlayScrollbar-Bo7MF_jE.js → OverlayScrollbar-GQgJusKL.js} +1 -1
  15. package/dist/assets/SettingsView-CViKCsxJ.js +25 -0
  16. package/dist/assets/{SettingsWindow-BlKxv7c3.js → SettingsWindow-C6vwy4hH.js} +1 -1
  17. package/dist/assets/{TerminalView-xtUbWmdn.js → TerminalView-C1aX22ka.js} +1 -1
  18. package/dist/assets/{ToolOutputDialog-DUYcTYYW.js → ToolOutputDialog-BrOqj1sB.js} +4 -4
  19. package/dist/assets/appThemeRegistry-BbqImxSo.js +2 -0
  20. package/dist/assets/{button-DfF088a8.js → button-CeCkEpvn.js} +1 -1
  21. package/dist/assets/{clipboard-RoOQVnSl.js → clipboard-BXC1vZwE.js} +1 -1
  22. package/dist/assets/{collapsible-CVL-5iEB.js → collapsible-B-aagADh.js} +1 -1
  23. package/dist/assets/{desktopHosts-Dzx3McaD.js → desktopHosts-BrrYPqKq.js} +1 -1
  24. package/dist/assets/{desktopRelayRestore-B9S0Rp5s.js → desktopRelayRestore-DR60DATT.js} +1 -1
  25. package/dist/assets/diagram-Cxma_IKI.js +1 -0
  26. package/dist/assets/dialog-wraJHZQ2.js +1 -0
  27. package/dist/assets/es-DQqFAaWT.js +15 -0
  28. package/dist/assets/fileQueries-BJKLdYos.js +1 -0
  29. package/dist/assets/fr-CNfGRpc1.js +13 -0
  30. package/dist/assets/{gitApi-DsdwefuR.js → gitApi-B4z4eSZV.js} +1 -1
  31. package/dist/assets/{input-Ca2tUhgN.js → input-CyG05w-V.js} +1 -1
  32. package/dist/assets/insertionBoundaries-DCYAK5W1.js +4 -0
  33. package/dist/assets/ja-CQqehnVn.js +15 -0
  34. package/dist/assets/ko-kmwi8UJD.js +15 -0
  35. package/dist/assets/main-A8oe9r34.js +29 -0
  36. package/dist/assets/{main-CKb-YXE6.js → main-BJBKpmw3.js} +2 -2
  37. package/dist/assets/{markdown-worker-ZaHiIw5I.js → markdown-worker-DDCUZkz4.js} +1 -1
  38. package/dist/assets/{markdownTheme-DidS-jC5.js → markdownTheme-H7_7lbuh.js} +1 -1
  39. package/dist/assets/miniChat-BObOyFSV.js +2 -0
  40. package/dist/assets/mobile-Ba390Cmc.js +2 -0
  41. package/dist/assets/multirun-CMDYOrmD.js +4 -0
  42. package/dist/assets/{persistence-aCOlo6ew.js → persistence-DE9RnVcA.js} +2 -2
  43. package/dist/assets/pl-Blv5o2QA.js +15 -0
  44. package/dist/assets/platform-MjT8iqKB.js +2 -0
  45. package/dist/assets/projectMeta-uWBWFG5A.js +3 -0
  46. package/dist/assets/pt-BR-BK74t-Bd.js +15 -0
  47. package/dist/assets/radio-CEXUd4RC.js +1 -0
  48. package/dist/assets/renderElectronMiniChatApp-Bpgb2E78.js +2 -0
  49. package/dist/assets/renderMobileApp-BMwRozEC.js +23 -0
  50. package/dist/assets/runtimeConfig-hHuyauKZ.js +2 -0
  51. package/dist/assets/runtimeEndpointReset-CmMI7Xp1.js +4 -0
  52. package/dist/assets/{shikiHighlight-eMLXb99c.js → shikiHighlight-C-kTQvqd.js} +1 -1
  53. package/dist/assets/{sortable-tabs-strip-CDHNwlp9.js → sortable-tabs-strip-BdbKDDAP.js} +1 -1
  54. package/dist/assets/src-CgFGBE_A.css +2 -0
  55. package/dist/assets/store-UweBeN2y.js +15 -0
  56. package/dist/assets/{terminalApi-B36I-z14.js → terminalApi-Bnx0KvaV.js} +1 -1
  57. package/dist/assets/{tooltip-GC3dg3gO.js → tooltip-B__x-HDJ.js} +1 -1
  58. package/dist/assets/uk-6I-KXy3R.js +15 -0
  59. package/dist/assets/{url-BQHM4bC4.js → url-CTuw7Rjf.js} +1 -1
  60. package/dist/assets/{useAppFontEffects-B9Ozsfua.js → useAppFontEffects-8iAE0Yrj.js} +4 -4
  61. package/dist/assets/{useAssistantUIStore-DxZsjSPQ.js → useAssistantUIStore-D2K7ksGj.js} +1 -1
  62. package/dist/assets/{useDesktopWindowControlsLayout-QxNEOYzY.js → useDesktopWindowControlsLayout--oz1VAPt.js} +1 -1
  63. package/dist/assets/useEffectiveDirectory-DmoqXaa4.js +1 -0
  64. package/dist/assets/{useFontPreferences-D9h4M7x4.js → useFontPreferences-CWZnNRjz.js} +1 -1
  65. package/dist/assets/useMobileNavigationStore-BxbEX-es.js +412 -0
  66. package/dist/assets/{useSessionAutoCleanup-CXqhEUbp.js → useSessionAutoCleanup-BwlJlhY7.js} +1 -1
  67. package/dist/assets/{useUIStore-Wyu09VUF.js → useUIStore-DOxLVeGb.js} +1 -1
  68. package/dist/assets/{useWorkerHighlightedLines-2KOHSbaa.js → useWorkerHighlightedLines-B21D9MOD.js} +1 -1
  69. package/dist/assets/{utils-CJdWZupK.js → utils-C2R0YoC5.js} +1 -1
  70. package/dist/assets/vendor-legendapp-list-WE32lwNk.js +1 -0
  71. package/dist/assets/zh-CN-DwlzS4yK.js +15 -0
  72. package/dist/assets/zh-TW-p0VriyfT.js +15 -0
  73. package/dist/index.html +9 -9
  74. package/dist/mini-chat.html +9 -9
  75. package/dist/mobile.html +9 -9
  76. package/package.json +3 -2
  77. package/server/index.js +19 -6
  78. package/server/lib/notifications/APNS.md +88 -38
  79. package/server/lib/notifications/DOCUMENTATION.md +11 -4
  80. package/server/lib/notifications/apns-runtime.js +793 -78
  81. package/server/lib/notifications/apns-runtime.test.js +922 -0
  82. package/server/lib/notifications/apns-titles.js +212 -0
  83. package/server/lib/notifications/apns-titles.test.js +55 -0
  84. package/server/lib/notifications/routes.js +57 -1
  85. package/server/lib/notifications/runtime.js +20 -17
  86. package/server/lib/notifications/runtime.test.js +108 -0
  87. package/server/lib/opencode/DOCUMENTATION.md +1 -1
  88. package/server/lib/opencode/bootstrap-runtime.js +6 -0
  89. package/server/lib/opencode/bootstrap-runtime.test.js +21 -0
  90. package/server/lib/opencode/routes.js +24 -5
  91. package/server/lib/opencode/routes.test.js +78 -1
  92. package/server/lib/relay/DOCUMENTATION.md +2 -2
  93. package/server/lib/relay/service.js +45 -3
  94. package/server/lib/relay/service.test.js +117 -0
  95. package/server/lib/relay/signing-key.js +1 -1
  96. package/server/lib/scheduled-tasks/DOCUMENTATION.md +27 -2
  97. package/server/lib/scheduled-tasks/runtime.js +271 -3
  98. package/server/lib/scheduled-tasks/runtime.test.js +308 -0
  99. package/server/lib/session-goal/runtime.js +2 -2
  100. package/dist/assets/AgentAvatar-CWn_73qY.js +0 -1
  101. package/dist/assets/AssistantView-BiOO5An0.js +0 -1
  102. package/dist/assets/ContextPanel-BbNBZipF.js +0 -288
  103. package/dist/assets/DiagramView-AMPWVMV0.js +0 -1
  104. package/dist/assets/MobileSurface-DmMgH78x.js +0 -2
  105. package/dist/assets/SettingsView-BDaMrSki.js +0 -25
  106. package/dist/assets/appThemeRegistry-D6qZmouf.js +0 -2
  107. package/dist/assets/dialog-BbHDXPt6.js +0 -1
  108. package/dist/assets/es-BkIvCvwj.js +0 -15
  109. package/dist/assets/fileQueries-_VUJM8AE.js +0 -1
  110. package/dist/assets/fr-tCwQ3odX.js +0 -13
  111. package/dist/assets/insertionBoundaries-M7AXIWfg.js +0 -1
  112. package/dist/assets/ja-C0rD6dwp.js +0 -15
  113. package/dist/assets/ko-CAbtZ462.js +0 -15
  114. package/dist/assets/main-QjOL36Zh.js +0 -29
  115. package/dist/assets/miniChat-C45kGqqH.js +0 -2
  116. package/dist/assets/mobile-DH8nxp1K.js +0 -2
  117. package/dist/assets/multirun-BkKb0iZv.js +0 -4
  118. package/dist/assets/pl-Bb190ba6.js +0 -15
  119. package/dist/assets/platform-DxNH_0Fj.js +0 -2
  120. package/dist/assets/projectMeta-BHtBPaxp.js +0 -3
  121. package/dist/assets/pt-BR-B6p_t3IB.js +0 -15
  122. package/dist/assets/radio-BacnmuU0.js +0 -1
  123. package/dist/assets/renderElectronMiniChatApp-Cn7umSw9.js +0 -2
  124. package/dist/assets/renderMobileApp-DZBR2t3l.js +0 -23
  125. package/dist/assets/runtimeConfig-CSAVS8gd.js +0 -2
  126. package/dist/assets/runtimeEndpointReset-BZUX3z0A.js +0 -4
  127. package/dist/assets/src-DziW4HUU.css +0 -2
  128. package/dist/assets/store-1FOY4G3z.js +0 -15
  129. package/dist/assets/uk-Dy8-6ZEj.js +0 -15
  130. package/dist/assets/useEffectiveDirectory-D3u_oih8.js +0 -1
  131. package/dist/assets/useMobileNavigationStore-CCPTHVO9.js +0 -475
  132. package/dist/assets/zh-CN-gRTE0npc.js +0 -15
  133. package/dist/assets/zh-TW-ClD4BCfW.js +0 -15
@@ -305,6 +305,113 @@ const extractGoalFromSession = (session) => {
305
305
  };
306
306
  };
307
307
 
308
+ /**
309
+ * Single-shot session outcome for post-run continuation (no polling).
310
+ * Goal-enabled: terminal complete → success; blocked/budgetLimited → error;
311
+ * otherwise not-yet-success. Non-goal: busy/retry, assistant error, or
312
+ * completed assistant; incomplete/empty tails are not treated as success.
313
+ */
314
+ const snapshotSessionOutcome = async ({
315
+ client,
316
+ sessionID,
317
+ projectPath,
318
+ goalEnabled,
319
+ signal,
320
+ }) => {
321
+ const requestOptions = signal ? { signal } : undefined;
322
+
323
+ if (goalEnabled && typeof client?.session?.get === 'function') {
324
+ try {
325
+ const sessionResult = await client.session.get({
326
+ sessionID,
327
+ directory: projectPath,
328
+ }, requestOptions);
329
+ if (!sessionResult?.error) {
330
+ const goal = extractGoalFromSession(sessionResult?.data);
331
+ if (goal && GOAL_TERMINAL_STATUSES.has(goal.status)) {
332
+ if (goal.status === 'complete') {
333
+ return { outcome: 'success' };
334
+ }
335
+ return {
336
+ outcome: 'error',
337
+ error: goal.note || `goal ${goal.status}`,
338
+ };
339
+ }
340
+ }
341
+ } catch (error) {
342
+ if (signal?.aborted) throw error;
343
+ }
344
+ return { outcome: 'busy' };
345
+ }
346
+
347
+ if (typeof client?.session?.status === 'function') {
348
+ try {
349
+ const statusResult = await client.session.status({
350
+ directory: projectPath,
351
+ }, requestOptions);
352
+ if (!statusResult?.error && statusResult?.data && typeof statusResult.data === 'object') {
353
+ const statusValue = statusResult.data[sessionID];
354
+ const type = statusValue?.type ?? statusValue?.status;
355
+ if (type === 'busy' || type === 'retry') {
356
+ return { outcome: 'busy' };
357
+ }
358
+ }
359
+ } catch (error) {
360
+ if (signal?.aborted) throw error;
361
+ }
362
+ }
363
+
364
+ if (typeof client?.session?.messages === 'function') {
365
+ try {
366
+ const messagesResult = await client.session.messages({
367
+ sessionID,
368
+ directory: projectPath,
369
+ limit: 50,
370
+ }, requestOptions);
371
+ if (!messagesResult?.error && Array.isArray(messagesResult?.data)) {
372
+ const lastInfo = readMessageInfo(messagesResult.data.at(-1));
373
+ if (lastInfo?.role === 'assistant') {
374
+ if (lastInfo.error) {
375
+ return {
376
+ outcome: 'error',
377
+ error: formatAssistantError(lastInfo.error),
378
+ };
379
+ }
380
+ if (lastInfo.time?.completed) {
381
+ return { outcome: 'success' };
382
+ }
383
+ }
384
+ }
385
+ } catch (error) {
386
+ if (signal?.aborted) throw error;
387
+ }
388
+ }
389
+
390
+ return { outcome: 'busy' };
391
+ };
392
+
393
+ const parseSessionEventPhase = (event) => {
394
+ const payload = event?.payload?.payload ?? event?.payload;
395
+ const properties = payload?.properties;
396
+ const sessionID = typeof properties?.sessionID === 'string' ? properties.sessionID : '';
397
+ if (!sessionID) {
398
+ return { phase: '', sessionID: '' };
399
+ }
400
+ if (payload?.type === 'session.idle') {
401
+ return { phase: 'idle', sessionID };
402
+ }
403
+ if (payload?.type === 'session.status') {
404
+ const type = properties?.status?.type ?? properties?.info?.type;
405
+ if (type === 'idle') {
406
+ return { phase: 'idle', sessionID };
407
+ }
408
+ if (type === 'busy' || type === 'retry') {
409
+ return { phase: 'busy', sessionID };
410
+ }
411
+ }
412
+ return { phase: '', sessionID };
413
+ };
414
+
308
415
  export const createScheduledTasksRuntime = (deps) => {
309
416
  const {
310
417
  projectConfigRuntime,
@@ -331,6 +438,34 @@ export const createScheduledTasksRuntime = (deps) => {
331
438
  const runningCountByProject = new Map();
332
439
  let runningGlobalCount = 0;
333
440
  const queue = [];
441
+ /** sessionID → { projectID, taskID } for post-run continuation correction. */
442
+ const lastSessionOwners = new Map();
443
+
444
+ const rememberLastSession = (projectID, task) => {
445
+ const sessionID = task?.state?.lastSessionId;
446
+ if (typeof sessionID !== 'string' || !sessionID || !task?.id) {
447
+ return;
448
+ }
449
+ lastSessionOwners.set(sessionID, { projectID, taskID: task.id });
450
+ };
451
+
452
+ const forgetLastSessionsForProject = (projectID) => {
453
+ for (const [sessionID, owner] of [...lastSessionOwners.entries()]) {
454
+ if (owner.projectID === projectID) {
455
+ lastSessionOwners.delete(sessionID);
456
+ }
457
+ }
458
+ };
459
+
460
+ const forgetLastSessionIfOwned = (projectID, taskID, sessionID) => {
461
+ if (typeof sessionID !== 'string' || !sessionID) {
462
+ return;
463
+ }
464
+ const owner = lastSessionOwners.get(sessionID);
465
+ if (owner?.projectID === projectID && owner?.taskID === taskID) {
466
+ lastSessionOwners.delete(sessionID);
467
+ }
468
+ };
334
469
 
335
470
  const clearTimerForKey = (taskKey) => {
336
471
  const timer = timersByTaskKey.get(taskKey);
@@ -361,9 +496,11 @@ export const createScheduledTasksRuntime = (deps) => {
361
496
 
362
497
  const setProjectTasks = (projectID, tasks) => {
363
498
  clearProjectTimers(projectID);
499
+ forgetLastSessionsForProject(projectID);
364
500
  const taskMap = new Map();
365
501
  for (const task of tasks) {
366
502
  taskMap.set(task.id, task);
503
+ rememberLastSession(projectID, task);
367
504
  }
368
505
  tasksByProject.set(projectID, taskMap);
369
506
  };
@@ -412,7 +549,14 @@ export const createScheduledTasksRuntime = (deps) => {
412
549
  if (!taskMap) {
413
550
  return;
414
551
  }
552
+ const previous = taskMap.get(nextTask.id);
553
+ const previousSessionID = previous?.state?.lastSessionId;
554
+ const nextSessionID = nextTask.state?.lastSessionId;
555
+ if (previousSessionID && previousSessionID !== nextSessionID) {
556
+ forgetLastSessionIfOwned(projectID, nextTask.id, previousSessionID);
557
+ }
415
558
  taskMap.set(nextTask.id, nextTask);
559
+ rememberLastSession(projectID, nextTask);
416
560
  };
417
561
 
418
562
  const syncTaskSchedule = async (projectID, task) => {
@@ -1152,6 +1296,15 @@ export const createScheduledTasksRuntime = (deps) => {
1152
1296
  if (runningState.task) {
1153
1297
  updateInMemoryTask(projectID, runningState.task);
1154
1298
  }
1299
+ try {
1300
+ emitTaskRunEvent?.({
1301
+ projectID,
1302
+ taskID,
1303
+ ranAt: runStartedAt,
1304
+ status: 'running',
1305
+ });
1306
+ } catch {
1307
+ }
1155
1308
  } catch (error) {
1156
1309
  errorMessage = safeErrorMessage(error);
1157
1310
  logger.warn?.('[ScheduledTasks] failed to persist running state', {
@@ -1278,14 +1431,17 @@ export const createScheduledTasksRuntime = (deps) => {
1278
1431
 
1279
1432
  const nextRunAt = computeNextRunAt(latestTask, finishedAt);
1280
1433
  try {
1281
- stateResult = await projectConfigRuntime.updateScheduledTaskState(projectID, taskID, {
1434
+ const statePatch = {
1282
1435
  lastStatus: status,
1283
1436
  lastDurationMs: durationMs,
1284
1437
  lastError: status === 'error' ? errorMessage : undefined,
1285
- lastSessionId: status === 'success' ? sessionID : undefined,
1286
1438
  nextRunAt: Number.isFinite(nextRunAt) ? nextRunAt : undefined,
1287
1439
  updatedAt: finishedAt,
1288
- });
1440
+ };
1441
+ if (sessionID) {
1442
+ statePatch.lastSessionId = sessionID;
1443
+ }
1444
+ stateResult = await projectConfigRuntime.updateScheduledTaskState(projectID, taskID, statePatch);
1289
1445
  if (stateResult.task) {
1290
1446
  updateInMemoryTask(projectID, stateResult.task);
1291
1447
  if (stateResult.task.enabled && Number.isFinite(stateResult.task.state?.nextRunAt)) {
@@ -1301,6 +1457,9 @@ export const createScheduledTasksRuntime = (deps) => {
1301
1457
  error: errorMessage,
1302
1458
  });
1303
1459
  }
1460
+ if (sessionID) {
1461
+ lastSessionOwners.set(sessionID, { projectID, taskID });
1462
+ }
1304
1463
 
1305
1464
  // History final status reflects the ultimate run outcome, including
1306
1465
  // state-persistence failures. A failed finalize leaves the row running
@@ -1441,6 +1600,114 @@ export const createScheduledTasksRuntime = (deps) => {
1441
1600
  };
1442
1601
  };
1443
1602
 
1603
+ const emitContinuationStatus = (projectID, taskID, sessionID, status) => {
1604
+ try {
1605
+ emitTaskRunEvent?.({
1606
+ projectID,
1607
+ taskID,
1608
+ ranAt: Date.now(),
1609
+ status,
1610
+ sessionID,
1611
+ });
1612
+ } catch {
1613
+ }
1614
+ };
1615
+
1616
+ const handleSessionContinuation = async (event) => {
1617
+ const { phase, sessionID } = parseSessionEventPhase(event);
1618
+ if (!phase || !sessionID) {
1619
+ return;
1620
+ }
1621
+
1622
+ const owner = lastSessionOwners.get(sessionID);
1623
+ if (!owner) {
1624
+ return;
1625
+ }
1626
+
1627
+ const { projectID, taskID } = owner;
1628
+ const taskKey = buildTaskKey(projectID, taskID);
1629
+ if (runningTaskKeys.has(taskKey)) {
1630
+ return;
1631
+ }
1632
+
1633
+ const task = tasksByProject.get(projectID)?.get(taskID);
1634
+ const lastStatus = task?.state?.lastStatus;
1635
+
1636
+ if (phase === 'busy') {
1637
+ if (lastStatus !== 'error' && lastStatus !== 'success') {
1638
+ return;
1639
+ }
1640
+ const stateResult = await projectConfigRuntime.updateScheduledTaskState(projectID, taskID, {
1641
+ lastStatus: 'running',
1642
+ lastError: undefined,
1643
+ lastSessionId: sessionID,
1644
+ updatedAt: Date.now(),
1645
+ });
1646
+ if (stateResult.task) {
1647
+ updateInMemoryTask(projectID, stateResult.task);
1648
+ }
1649
+ emitContinuationStatus(projectID, taskID, sessionID, 'running');
1650
+ return;
1651
+ }
1652
+
1653
+ if (lastStatus === 'success') {
1654
+ return;
1655
+ }
1656
+ if (lastStatus !== 'error' && lastStatus !== 'running') {
1657
+ return;
1658
+ }
1659
+
1660
+ const projectPath = projectPathByID.get(projectID) || await ensureProjectPath(projectID);
1661
+ if (!projectPath) {
1662
+ return;
1663
+ }
1664
+
1665
+ const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
1666
+ const authHeaders = getOpenCodeAuthHeaders();
1667
+ const client = createOpencodeClient({
1668
+ baseUrl,
1669
+ headers: authHeaders,
1670
+ });
1671
+ const snapshot = await snapshotSessionOutcome({
1672
+ client,
1673
+ sessionID,
1674
+ projectPath,
1675
+ goalEnabled: Boolean(task.execution?.goalEnabled),
1676
+ });
1677
+ if (snapshot.outcome !== 'success') {
1678
+ return;
1679
+ }
1680
+
1681
+ if (runningTaskKeys.has(taskKey)) {
1682
+ return;
1683
+ }
1684
+ const latest = tasksByProject.get(projectID)?.get(taskID);
1685
+ const latestStatus = latest?.state?.lastStatus;
1686
+ if (latestStatus !== 'error' && latestStatus !== 'running') {
1687
+ return;
1688
+ }
1689
+
1690
+ const stateResult = await projectConfigRuntime.updateScheduledTaskState(projectID, taskID, {
1691
+ lastStatus: 'success',
1692
+ lastError: undefined,
1693
+ lastSessionId: sessionID,
1694
+ updatedAt: Date.now(),
1695
+ });
1696
+ if (stateResult.task) {
1697
+ updateInMemoryTask(projectID, stateResult.task);
1698
+ }
1699
+
1700
+ emitContinuationStatus(projectID, taskID, sessionID, 'success');
1701
+ };
1702
+
1703
+ const observeSessionEvent = (event) => {
1704
+ return handleSessionContinuation(event).catch((error) => {
1705
+ logger.warn?.('[ScheduledTasks] observeSessionEvent failed', {
1706
+ error: safeErrorMessage(error),
1707
+ });
1708
+ });
1709
+ };
1710
+
1444
1711
  return {
1445
1712
  start,
1446
1713
  stop,
@@ -1448,5 +1715,6 @@ export const createScheduledTasksRuntime = (deps) => {
1448
1715
  syncProject,
1449
1716
  runNow,
1450
1717
  getStatus,
1718
+ observeSessionEvent,
1451
1719
  };
1452
1720
  };
@@ -993,4 +993,312 @@ describe('scheduled-tasks run history and session lifecycle', () => {
993
993
 
994
994
  vi.unstubAllGlobals();
995
995
  });
996
+
997
+ it('persists lastSessionId when an error run still created a session', async () => {
998
+ const history = createHistoryStore();
999
+ createSuccessfulClient({ settlement: 'assistant-error' });
1000
+ const fetchMock = vi.fn(async (url) => {
1001
+ if (String(url).includes('prompt_async')) {
1002
+ return { ok: true, text: async () => '' };
1003
+ }
1004
+ throw new Error(`unexpected fetch ${url}`);
1005
+ });
1006
+ vi.stubGlobal('fetch', fetchMock);
1007
+
1008
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
1009
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
1010
+ }));
1011
+ const runtime = createRuntime(updateScheduledTaskState, {
1012
+ runHistoryStore: history,
1013
+ waitForOpenCodeReady: vi.fn(async () => {}),
1014
+ });
1015
+ await runtime.syncProject('project-1');
1016
+ const result = await runtime.runNow('project-1', 'task-1');
1017
+
1018
+ expect(result.ok).toBe(false);
1019
+ expect(result.status).toBe('error');
1020
+ const finalPatch = updateScheduledTaskState.mock.calls.at(-1)[2];
1021
+ expect(finalPatch.lastStatus).toBe('error');
1022
+ expect(finalPatch.lastSessionId).toBe('ses_1');
1023
+
1024
+ vi.unstubAllGlobals();
1025
+ });
1026
+
1027
+ it('omits lastSessionId from the final patch when the failed run has no session', async () => {
1028
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
1029
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
1030
+ }));
1031
+ const runtime = createRuntime(updateScheduledTaskState);
1032
+ await runtime.syncProject('project-1');
1033
+ const result = await runtime.runNow('project-1', 'task-1');
1034
+
1035
+ expect(result.ok).toBe(false);
1036
+ expect(result.status).toBe('error');
1037
+ const finalPatch = updateScheduledTaskState.mock.calls.at(-1)[2];
1038
+ expect(finalPatch.lastStatus).toBe('error');
1039
+ expect(Object.hasOwn(finalPatch, 'lastSessionId')).toBe(false);
1040
+ });
1041
+
1042
+ it('marks lastStatus running when a history session continues, then success on idle', async () => {
1043
+ const history = createHistoryStore();
1044
+ const client = createSuccessfulClient({ settlement: 'assistant-error' });
1045
+ const fetchMock = vi.fn(async (url) => {
1046
+ if (String(url).includes('prompt_async')) {
1047
+ return { ok: true, text: async () => '' };
1048
+ }
1049
+ throw new Error(`unexpected fetch ${url}`);
1050
+ });
1051
+ vi.stubGlobal('fetch', fetchMock);
1052
+
1053
+ const emitTaskRunEvent = vi.fn();
1054
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
1055
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
1056
+ }));
1057
+ const runtime = createRuntime(updateScheduledTaskState, {
1058
+ runHistoryStore: history,
1059
+ waitForOpenCodeReady: vi.fn(async () => {}),
1060
+ emitTaskRunEvent,
1061
+ });
1062
+ await runtime.syncProject('project-1');
1063
+ const result = await runtime.runNow('project-1', 'task-1');
1064
+ expect(result.status).toBe('error');
1065
+ expect(history.finishRun).toHaveBeenCalledTimes(1);
1066
+
1067
+ await runtime.observeSessionEvent({
1068
+ payload: {
1069
+ type: 'session.status',
1070
+ properties: { sessionID: 'ses_1', status: { type: 'busy' } },
1071
+ },
1072
+ });
1073
+
1074
+ expect(history.finishRun).toHaveBeenCalledTimes(1);
1075
+ const runningPatch = updateScheduledTaskState.mock.calls.at(-1)[2];
1076
+ expect(runningPatch).toEqual(expect.objectContaining({
1077
+ lastStatus: 'running',
1078
+ lastError: undefined,
1079
+ lastSessionId: 'ses_1',
1080
+ }));
1081
+ expect(Object.hasOwn(runningPatch, 'lastRunAt')).toBe(false);
1082
+ expect(emitTaskRunEvent).toHaveBeenCalledWith(expect.objectContaining({
1083
+ projectID: 'project-1',
1084
+ taskID: 'task-1',
1085
+ status: 'running',
1086
+ sessionID: 'ses_1',
1087
+ }));
1088
+
1089
+ client.messages.mockImplementation(async () => ({
1090
+ data: [{
1091
+ info: {
1092
+ id: 'msg_ok',
1093
+ role: 'assistant',
1094
+ time: { completed: Date.now() },
1095
+ },
1096
+ }],
1097
+ }));
1098
+
1099
+ await runtime.observeSessionEvent({
1100
+ payload: {
1101
+ type: 'session.idle',
1102
+ properties: { sessionID: 'ses_1' },
1103
+ },
1104
+ });
1105
+
1106
+ expect(updateScheduledTaskState.mock.calls.at(-1)[2]).toEqual(expect.objectContaining({
1107
+ lastStatus: 'success',
1108
+ lastSessionId: 'ses_1',
1109
+ }));
1110
+ expect(history.finishRun).toHaveBeenCalledTimes(1);
1111
+
1112
+ vi.unstubAllGlobals();
1113
+ });
1114
+
1115
+ it('corrects lastStatus error → success on idle after a continuation, without rewriting history', async () => {
1116
+ const history = createHistoryStore();
1117
+ const client = createSuccessfulClient({ settlement: 'assistant-error' });
1118
+ const fetchMock = vi.fn(async (url) => {
1119
+ if (String(url).includes('prompt_async')) {
1120
+ return { ok: true, text: async () => '' };
1121
+ }
1122
+ throw new Error(`unexpected fetch ${url}`);
1123
+ });
1124
+ vi.stubGlobal('fetch', fetchMock);
1125
+
1126
+ const emitTaskRunEvent = vi.fn();
1127
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
1128
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
1129
+ }));
1130
+ const runtime = createRuntime(updateScheduledTaskState, {
1131
+ runHistoryStore: history,
1132
+ waitForOpenCodeReady: vi.fn(async () => {}),
1133
+ emitTaskRunEvent,
1134
+ });
1135
+ await runtime.syncProject('project-1');
1136
+ const result = await runtime.runNow('project-1', 'task-1');
1137
+ expect(result.status).toBe('error');
1138
+ expect(history.finishRun).toHaveBeenCalledTimes(1);
1139
+
1140
+ client.messages.mockImplementation(async () => ({
1141
+ data: [{
1142
+ info: {
1143
+ id: 'msg_ok',
1144
+ role: 'assistant',
1145
+ time: { completed: Date.now() },
1146
+ },
1147
+ }],
1148
+ }));
1149
+
1150
+ await runtime.observeSessionEvent({
1151
+ payload: {
1152
+ type: 'session.idle',
1153
+ properties: { sessionID: 'ses_1' },
1154
+ },
1155
+ });
1156
+
1157
+ expect(history.finishRun).toHaveBeenCalledTimes(1);
1158
+ const correctionPatch = updateScheduledTaskState.mock.calls.at(-1)[2];
1159
+ expect(correctionPatch).toEqual(expect.objectContaining({
1160
+ lastStatus: 'success',
1161
+ lastError: undefined,
1162
+ lastSessionId: 'ses_1',
1163
+ }));
1164
+ expect(Object.hasOwn(correctionPatch, 'lastRunAt')).toBe(false);
1165
+ expect(Object.hasOwn(correctionPatch, 'lastDurationMs')).toBe(false);
1166
+ expect(Object.hasOwn(correctionPatch, 'nextRunAt')).toBe(false);
1167
+ expect(emitTaskRunEvent).toHaveBeenCalledWith(expect.objectContaining({
1168
+ projectID: 'project-1',
1169
+ taskID: 'task-1',
1170
+ status: 'success',
1171
+ sessionID: 'ses_1',
1172
+ }));
1173
+
1174
+ vi.unstubAllGlobals();
1175
+ });
1176
+
1177
+ it('does not correct lastStatus while the task is still running', async () => {
1178
+ vi.useFakeTimers();
1179
+ const history = createHistoryStore();
1180
+ const erroredTask = {
1181
+ ...scheduledTask,
1182
+ state: {
1183
+ ...scheduledTask.state,
1184
+ lastStatus: 'error',
1185
+ lastSessionId: 'ses_1',
1186
+ lastError: 'ProviderError',
1187
+ },
1188
+ };
1189
+ createSuccessfulClient({ settlement: 'busy-then-success' });
1190
+ const fetchMock = vi.fn(async (url) => {
1191
+ if (String(url).includes('prompt_async')) {
1192
+ return { ok: true, text: async () => '' };
1193
+ }
1194
+ throw new Error(`unexpected fetch ${url}`);
1195
+ });
1196
+ vi.stubGlobal('fetch', fetchMock);
1197
+
1198
+ const emitTaskRunEvent = vi.fn();
1199
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
1200
+ task: { ...erroredTask, state: { ...erroredTask.state, ...state } },
1201
+ }));
1202
+ const runtime = createRuntime(updateScheduledTaskState, {
1203
+ projectConfigRuntime: {
1204
+ listScheduledTasks: vi.fn(async () => [erroredTask]),
1205
+ updateScheduledTaskState,
1206
+ upsertScheduledTask: vi.fn(),
1207
+ },
1208
+ runHistoryStore: history,
1209
+ waitForOpenCodeReady: vi.fn(async () => {}),
1210
+ emitTaskRunEvent,
1211
+ });
1212
+ await runtime.syncProject('project-1');
1213
+
1214
+ const runPromise = runtime.runNow('project-1', 'task-1');
1215
+ await vi.advanceTimersByTimeAsync(0);
1216
+ expect(history.finishRun).not.toHaveBeenCalled();
1217
+
1218
+ const successEmitsBefore = emitTaskRunEvent.mock.calls.filter((call) => call[0].status === 'success').length;
1219
+ const continuationRunningBefore = updateScheduledTaskState.mock.calls.filter((call) => (
1220
+ call[2].lastStatus === 'running' && !Object.hasOwn(call[2], 'lastRunAt')
1221
+ )).length;
1222
+ await runtime.observeSessionEvent({
1223
+ payload: {
1224
+ type: 'session.status',
1225
+ properties: { sessionID: 'ses_1', status: { type: 'busy' } },
1226
+ },
1227
+ });
1228
+ await runtime.observeSessionEvent({
1229
+ payload: {
1230
+ type: 'session.idle',
1231
+ properties: { sessionID: 'ses_1' },
1232
+ },
1233
+ });
1234
+ expect(history.finishRun).not.toHaveBeenCalled();
1235
+ expect(updateScheduledTaskState.mock.calls.map((call) => call[2].lastStatus)).not.toContain('success');
1236
+ expect(emitTaskRunEvent.mock.calls.filter((call) => call[0].status === 'success')).toHaveLength(successEmitsBefore);
1237
+ expect(updateScheduledTaskState.mock.calls.filter((call) => (
1238
+ call[2].lastStatus === 'running' && !Object.hasOwn(call[2], 'lastRunAt')
1239
+ ))).toHaveLength(continuationRunningBefore);
1240
+
1241
+ await vi.advanceTimersByTimeAsync(1_000);
1242
+ const result = await runPromise;
1243
+ expect(result.ok).toBe(true);
1244
+
1245
+ vi.unstubAllGlobals();
1246
+ vi.useRealTimers();
1247
+ });
1248
+
1249
+ it('does not correct lastStatus when it is already success', async () => {
1250
+ const history = createHistoryStore();
1251
+ const client = createSuccessfulClient();
1252
+ const fetchMock = vi.fn(async (url) => {
1253
+ if (String(url).includes('prompt_async')) {
1254
+ return { ok: true, text: async () => '' };
1255
+ }
1256
+ throw new Error(`unexpected fetch ${url}`);
1257
+ });
1258
+ vi.stubGlobal('fetch', fetchMock);
1259
+
1260
+ const emitTaskRunEvent = vi.fn();
1261
+ const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
1262
+ task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
1263
+ }));
1264
+ const runtime = createRuntime(updateScheduledTaskState, {
1265
+ runHistoryStore: history,
1266
+ waitForOpenCodeReady: vi.fn(async () => {}),
1267
+ emitTaskRunEvent,
1268
+ });
1269
+ await runtime.syncProject('project-1');
1270
+ const result = await runtime.runNow('project-1', 'task-1');
1271
+
1272
+ expect(result.ok).toBe(true);
1273
+ expect(emitTaskRunEvent.mock.calls.map((call) => call[0].status)[0]).toBe('running');
1274
+ expect(history.finishRun).toHaveBeenCalledTimes(1);
1275
+ const stateWritesAfterRun = updateScheduledTaskState.mock.calls.length;
1276
+ const successEmitsAfterRun = emitTaskRunEvent.mock.calls.filter((call) => call[0].status === 'success').length;
1277
+
1278
+ client.messages.mockImplementation(async () => ({
1279
+ data: [{
1280
+ info: {
1281
+ id: 'msg_ok',
1282
+ role: 'assistant',
1283
+ time: { completed: Date.now() },
1284
+ },
1285
+ }],
1286
+ }));
1287
+
1288
+ await runtime.observeSessionEvent({
1289
+ payload: {
1290
+ type: 'session.status',
1291
+ properties: {
1292
+ sessionID: 'ses_1',
1293
+ status: { type: 'idle' },
1294
+ },
1295
+ },
1296
+ });
1297
+
1298
+ expect(history.finishRun).toHaveBeenCalledTimes(1);
1299
+ expect(updateScheduledTaskState).toHaveBeenCalledTimes(stateWritesAfterRun);
1300
+ expect(emitTaskRunEvent.mock.calls.filter((call) => call[0].status === 'success')).toHaveLength(successEmitsAfterRun);
1301
+
1302
+ vi.unstubAllGlobals();
1303
+ });
996
1304
  });
@@ -111,8 +111,8 @@ const buildAuditSystemPrompt = () => [
111
111
 
112
112
  // Hard guard against language hallucination (account-side personalization
113
113
  // can leak a different language despite the instruction): if the note uses
114
- // a script absent from the objective and the agent's reply, drop the note
115
- // but keep the verdict.
114
+ // a script absent from the objective
115
+ // and the agent's reply, drop the note but keep the verdict.
116
116
  const SCRIPT_RANGES = [
117
117
  /[Ѐ-ӿ]/, // Cyrillic
118
118
  /[぀-ヿ一-鿿가-힯]/, // CJK
@@ -1 +0,0 @@
1
- import"./rolldown-runtime-C0FnF6B9.js";import{nt as e,rt as t}from"./vendor-base-ui-CnHfUmDN.js";import{n}from"./vendor-react-DLPElpoQ.js";import{t as r}from"./utils-CJdWZupK.js";t();var i=n(),a=[{var:`--status-success`,class:`agent-success`},{var:`--syntax-keyword`,class:`agent-keyword`},{var:`--syntax-type`,class:`agent-type`},{var:`--syntax-function`,class:`agent-function`},{var:`--syntax-number`,class:`agent-number`},{var:`--status-info`,class:`agent-info`},{var:`--status-warning`,class:`agent-warning`},{var:`--syntax-variable`,class:`agent-variable`}];function o(e){if(!e||e===`build`)return a[0];let t=0;for(let n=0;n<e.length;n++){let r=e.charCodeAt(n);t=(t<<5)-t+r,t&=t}return a[1+Math.abs(t)%(a.length-1)]}var s=5,c=Math.ceil(s/2);function l(e){let t=0;for(let n=0;n<e.length;n++)t=(t<<5)-t+e.charCodeAt(n),t|=0;return t>>>0}function u(e){let t=Array.from({length:s},()=>Array.from({length:s},()=>!1));if(!e)return t;let n=l(e);for(let e=0;e<s;e++)for(let r=0;r<c;r++){let i=(n&1)==1;n>>>=1,t[e][r]=i,t[e][4-r]=i}return t}var d=e(),f=e=>{let t=(0,i.c)(27),{name:n,emoji:a,size:s,className:c,label:l}=e,f=s===void 0?16:s,m,h,g;if(t[0]!==c||t[1]!==a||t[2]!==n){m=o(n);let e;t[6]===n?e=t[7]:(e=u(n),t[6]=n,t[7]=e),h=e,g=r(`inline-flex aspect-square shrink-0 overflow-hidden rounded-sm`,a&&`items-center justify-center select-none`,m.class,c),t[0]=c,t[1]=a,t[2]=n,t[3]=m,t[4]=h,t[5]=g}else m=t[3],h=t[4],g=t[5];let _=`calc(${f}px * var(--dpt-n, 1))`,v=`calc(${f}px * var(--dpt-n, 1))`,y=`calc(${f}px * var(--dpt-n, 1))`,b=`calc(${f}px * var(--dpt-n, 1))`,x=`calc(${f}px * var(--dpt-n, 1))`,S=`calc(${f}px * var(--dpt-n, 1))`,C=`var(${m.var})`,w;t[8]!==_||t[9]!==v||t[10]!==y||t[11]!==b||t[12]!==x||t[13]!==S||t[14]!==C?(w={width:_,minWidth:v,maxWidth:y,height:b,minHeight:x,maxHeight:S,color:C},t[8]=_,t[9]=v,t[10]=y,t[11]=b,t[12]=x,t[13]=S,t[14]=C,t[15]=w):w=t[15];let T=l?`img`:void 0,E=!l||void 0,D;t[16]!==a||t[17]!==h||t[18]!==f?(D=a?(0,d.jsx)(`span`,{"aria-hidden":`true`,style:{fontSize:`calc(${f*.75}px * var(--dpt-n, 1))`,lineHeight:1},children:a}):(0,d.jsxs)(`svg`,{width:f,height:f,viewBox:`0 0 5 5`,shapeRendering:`crispEdges`,className:`oc-agent-avatar block h-full w-full`,"aria-hidden":`true`,children:[(0,d.jsx)(`rect`,{width:`5`,height:`5`,fill:`currentColor`,opacity:.14}),h.map(p)]}),t[16]=a,t[17]=h,t[18]=f,t[19]=D):D=t[19];let O;return t[20]!==l||t[21]!==w||t[22]!==T||t[23]!==E||t[24]!==D||t[25]!==g?(O=(0,d.jsx)(`span`,{className:g,style:w,role:T,"aria-label":l,"aria-hidden":E,children:D}),t[20]=l,t[21]=w,t[22]=T,t[23]=E,t[24]=D,t[25]=g,t[26]=O):O=t[26],O};function p(e,t){return e.map((e,n)=>e?(0,d.jsx)(`rect`,{x:n,y:t,width:`1`,height:`1`,fill:`currentColor`},`${n}-${t}`):null)}export{o as n,f as t};