@openchambery/web 1.18.7-beta.1 → 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.
- package/dist/assets/AgentAvatar-CVTgW_rx.js +1 -0
- package/dist/assets/AssistantView-CvQ91w2s.js +1 -0
- package/dist/assets/{ChatContainer-Dnxdg6He.js → ChatContainer-CERrtaB-.js} +41 -43
- package/dist/assets/ContextPanel-CLz4r1G-.js +285 -0
- package/dist/assets/{DiagramView-BplbsR8l.js → DiagramView-DhV4Zthj.js} +1 -1
- package/dist/assets/{FadeInOnReveal-By2F9vcI.js → FadeInOnReveal-Cy3sf2M5.js} +1 -1
- package/dist/assets/{Icon-DpjYnXxn.js → Icon-BLWVqBee.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-EP8W7j6_.js → MarkdownRendererImpl-B4qwUu4q.js} +2 -2
- package/dist/assets/{MobileDetailNavigation-CYq_8ZAq.js → MobileDetailNavigation-CuWgEThi.js} +1 -1
- package/dist/assets/{MobileShareBridge-VmA8Qdh0.js → MobileShareBridge-C3nay5Sq.js} +2 -2
- package/dist/assets/MobileSurface-D7v_ZkYO.js +2 -0
- package/dist/assets/{MultiRunWindow-F6NGMY5U.js → MultiRunWindow-Cq8yyiqm.js} +1 -1
- package/dist/assets/{OnboardingScreen-BKmuT7AL.js → OnboardingScreen-CTNi2zuA.js} +1 -1
- package/dist/assets/OverlayScrollbar-GQgJusKL.js +2 -0
- package/dist/assets/SettingsView-CViKCsxJ.js +25 -0
- package/dist/assets/{SettingsWindow-Z4sr2ZBF.js → SettingsWindow-C6vwy4hH.js} +1 -1
- package/dist/assets/{TerminalView-DnOowqR4.js → TerminalView-C1aX22ka.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BKJJN6ZR.js → ToolOutputDialog-BrOqj1sB.js} +4 -4
- package/dist/assets/appThemeRegistry-BbqImxSo.js +2 -0
- package/dist/assets/{button-C7OXr4ae.js → button-CeCkEpvn.js} +1 -1
- package/dist/assets/{clipboard-DWuULPzo.js → clipboard-BXC1vZwE.js} +1 -1
- package/dist/assets/{collapsible-DaEya6b6.js → collapsible-B-aagADh.js} +1 -1
- package/dist/assets/{desktopHosts-Txp4xOVQ.js → desktopHosts-BrrYPqKq.js} +1 -1
- package/dist/assets/{desktopRelayRestore-C_FSEHKM.js → desktopRelayRestore-DR60DATT.js} +1 -1
- package/dist/assets/{diagram-CHfginjS.js → diagram-Cxma_IKI.js} +1 -1
- package/dist/assets/dialog-wraJHZQ2.js +1 -0
- package/dist/assets/es-DQqFAaWT.js +15 -0
- package/dist/assets/{fileQueries-BrUKKgPW.js → fileQueries-BJKLdYos.js} +1 -1
- package/dist/assets/fr-CNfGRpc1.js +13 -0
- package/dist/assets/{gitApi-CtHhG2UX.js → gitApi-B4z4eSZV.js} +1 -1
- package/dist/assets/{input-BzYA603C.js → input-CyG05w-V.js} +1 -1
- package/dist/assets/insertionBoundaries-DCYAK5W1.js +4 -0
- package/dist/assets/ja-CQqehnVn.js +15 -0
- package/dist/assets/ko-kmwi8UJD.js +15 -0
- package/dist/assets/main-A8oe9r34.js +29 -0
- package/dist/assets/{main-YMWxpyqS.js → main-BJBKpmw3.js} +2 -2
- package/dist/assets/{markdown-worker-C3uVXiOS.js → markdown-worker-DDCUZkz4.js} +1 -1
- package/dist/assets/{markdownTheme-B89dbj3N.js → markdownTheme-H7_7lbuh.js} +1 -1
- package/dist/assets/miniChat-BObOyFSV.js +2 -0
- package/dist/assets/mobile-Ba390Cmc.js +2 -0
- package/dist/assets/multirun-CMDYOrmD.js +4 -0
- package/dist/assets/{persistence-6sclr7B6.js → persistence-DE9RnVcA.js} +2 -2
- package/dist/assets/pl-Blv5o2QA.js +15 -0
- package/dist/assets/platform-MjT8iqKB.js +2 -0
- package/dist/assets/projectMeta-uWBWFG5A.js +3 -0
- package/dist/assets/pt-BR-BK74t-Bd.js +15 -0
- package/dist/assets/radio-CEXUd4RC.js +1 -0
- package/dist/assets/renderElectronMiniChatApp-Bpgb2E78.js +2 -0
- package/dist/assets/renderMobileApp-BMwRozEC.js +23 -0
- package/dist/assets/runtimeConfig-hHuyauKZ.js +2 -0
- package/dist/assets/runtimeEndpointReset-CmMI7Xp1.js +4 -0
- package/dist/assets/shikiHighlight-C-kTQvqd.js +1 -0
- package/dist/assets/{sortable-tabs-strip-nLsPC06f.js → sortable-tabs-strip-BdbKDDAP.js} +1 -1
- package/dist/assets/src-CgFGBE_A.css +2 -0
- package/dist/assets/store-UweBeN2y.js +15 -0
- package/dist/assets/{terminalApi-BpPeXWIu.js → terminalApi-Bnx0KvaV.js} +1 -1
- package/dist/assets/{tooltip-D_orJMYd.js → tooltip-B__x-HDJ.js} +1 -1
- package/dist/assets/uk-6I-KXy3R.js +15 -0
- package/dist/assets/{url-BsaBG5-1.js → url-CTuw7Rjf.js} +1 -1
- package/dist/assets/{useAppFontEffects-DPE9SuwC.js → useAppFontEffects-8iAE0Yrj.js} +5 -5
- package/dist/assets/{useAssistantUIStore-BHsHXCKE.js → useAssistantUIStore-D2K7ksGj.js} +1 -1
- package/dist/assets/{useDesktopWindowControlsLayout-evJk53fe.js → useDesktopWindowControlsLayout--oz1VAPt.js} +1 -1
- package/dist/assets/useEffectiveDirectory-DmoqXaa4.js +1 -0
- package/dist/assets/{useFontPreferences-ckzpXQaJ.js → useFontPreferences-CWZnNRjz.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BxbEX-es.js +412 -0
- package/dist/assets/useSessionAutoCleanup-BwlJlhY7.js +1 -0
- package/dist/assets/{useUIStore-DzwCfRqr.js → useUIStore-DOxLVeGb.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-ClEzCydD.js → useWorkerHighlightedLines-B21D9MOD.js} +1 -1
- package/dist/assets/{utils-B1EK5zKa.js → utils-C2R0YoC5.js} +1 -1
- package/dist/assets/vendor-legendapp-list-WE32lwNk.js +1 -0
- package/dist/assets/zh-CN-DwlzS4yK.js +15 -0
- package/dist/assets/zh-TW-p0VriyfT.js +15 -0
- package/dist/index.html +9 -9
- package/dist/mini-chat.html +9 -9
- package/dist/mobile.html +9 -9
- package/package.json +3 -2
- package/server/index.js +23 -20
- package/server/lib/config-sync/DOCUMENTATION.md +6 -7
- package/server/lib/config-sync/engine.js +0 -8
- package/server/lib/config-sync/index.js +0 -7
- package/server/lib/config-sync/receiver.js +0 -35
- package/server/lib/config-sync/routes.js +8 -41
- package/server/lib/config-sync/routes.test.js +1 -38
- package/server/lib/notifications/APNS.md +88 -38
- package/server/lib/notifications/DOCUMENTATION.md +11 -4
- package/server/lib/notifications/apns-runtime.js +793 -78
- package/server/lib/notifications/apns-runtime.test.js +922 -0
- package/server/lib/notifications/apns-titles.js +212 -0
- package/server/lib/notifications/apns-titles.test.js +55 -0
- package/server/lib/notifications/routes.js +57 -1
- package/server/lib/notifications/runtime.js +20 -17
- package/server/lib/notifications/runtime.test.js +108 -0
- package/server/lib/opencode/DOCUMENTATION.md +1 -1
- package/server/lib/opencode/bootstrap-runtime.js +6 -0
- package/server/lib/opencode/bootstrap-runtime.test.js +21 -0
- package/server/lib/opencode/openchamber-routes.js +1 -7
- package/server/lib/opencode/routes.js +24 -5
- package/server/lib/opencode/routes.test.js +78 -1
- package/server/lib/opencode/settings-helpers.js +0 -3
- package/server/lib/opencode/shutdown-runtime.js +0 -2
- package/server/lib/relay/DOCUMENTATION.md +2 -2
- package/server/lib/relay/service.js +45 -3
- package/server/lib/relay/service.test.js +117 -0
- package/server/lib/relay/signing-key.js +1 -1
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +27 -2
- package/server/lib/scheduled-tasks/runtime.js +271 -3
- package/server/lib/scheduled-tasks/runtime.test.js +308 -0
- package/server/lib/session-goal/DOCUMENTATION.md +3 -3
- package/server/lib/session-goal/runtime.js +3 -3
- package/server/lib/session-title/DOCUMENTATION.md +1 -2
- package/server/lib/small-model/DOCUMENTATION.md +2 -2
- package/dist/assets/AgentAvatar-DDcjamcJ.js +0 -1
- package/dist/assets/AssistantView-BsQRWYlz.js +0 -1
- package/dist/assets/MobileSurface-D7aKyOLV.js +0 -2
- package/dist/assets/OverlayScrollbar-Ch3J6ASR.js +0 -2
- package/dist/assets/SettingsView-5b0tAHTL.js +0 -25
- package/dist/assets/appThemeRegistry-Dfucen00.js +0 -2
- package/dist/assets/context-menu-in4KRVok.js +0 -1
- package/dist/assets/dialog-DPS0W9eS.js +0 -1
- package/dist/assets/es-BX0Vm3Ac.js +0 -15
- package/dist/assets/fr-CuPzXTYx.js +0 -13
- package/dist/assets/insertionBoundaries--0yrL837.js +0 -1
- package/dist/assets/ja-DGtzjrFO.js +0 -15
- package/dist/assets/ko-DZ9Di0je.js +0 -15
- package/dist/assets/main-B9klnJvc.js +0 -172
- package/dist/assets/miniChat-D6inDYgY.js +0 -2
- package/dist/assets/mobile-DRu5smYb.js +0 -2
- package/dist/assets/multirun-CVRJ-r1z.js +0 -4
- package/dist/assets/pl-BuCO3qb1.js +0 -15
- package/dist/assets/platform-CbeLpKPk.js +0 -2
- package/dist/assets/projectMeta-Cr4ghXIQ.js +0 -3
- package/dist/assets/pt-BR-Brf4RMsw.js +0 -15
- package/dist/assets/radio-C9Qe2mW1.js +0 -1
- package/dist/assets/renderElectronMiniChatApp-CCpXytpP.js +0 -2
- package/dist/assets/renderMobileApp-BmAEbSuq.js +0 -23
- package/dist/assets/runtimeConfig-JC8_XlMh.js +0 -2
- package/dist/assets/runtimeEndpointReset-Cgefx6dv.js +0 -147
- package/dist/assets/src-CpetTO35.css +0 -2
- package/dist/assets/store-DQY8Fkb0.js +0 -15
- package/dist/assets/uk-C-wmaPTi.js +0 -15
- package/dist/assets/useEffectiveDirectory-Bdc1_8wI.js +0 -1
- package/dist/assets/useMobileNavigationStore-BNyyqZch.js +0 -475
- package/dist/assets/useSessionAutoCleanup-C-NCLDvZ.js +0 -1
- package/dist/assets/zh-CN-DueRrIql.js +0 -15
- package/dist/assets/zh-TW-6875X80V.js +0 -15
- package/server/lib/config-sync/credential-auth.js +0 -39
- package/server/lib/session-assist/DOCUMENTATION.md +0 -62
- package/server/lib/session-assist/runtime.js +0 -376
- package/server/lib/session-assist/runtime.test.js +0 -114
|
@@ -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
|
-
|
|
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
|
});
|
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
Server-side control loop that keeps a session working toward a user-defined
|
|
4
4
|
objective stored under `metadata.openchamber.goal`, with the small model as
|
|
5
5
|
an independent progress auditor. Built on OpenChamber's backend-driven
|
|
6
|
-
architecture
|
|
6
|
+
architecture: the loop lives in
|
|
7
7
|
the web server and survives UI disconnects.
|
|
8
8
|
|
|
9
9
|
## Goal payload (`metadata.openchamber.goal`)
|
|
@@ -69,8 +69,8 @@ before touching the filesystem). Rationale: metadata rides every
|
|
|
69
69
|
|
|
70
70
|
## Flow
|
|
71
71
|
|
|
72
|
-
1. `createSessionGoalRuntime` subscribes to the global SSE hub (
|
|
73
|
-
|
|
72
|
+
1. `createSessionGoalRuntime` subscribes to the global SSE hub (it needs
|
|
73
|
+
the envelope's `directory`).
|
|
74
74
|
2. `session.status: idle` arms a 15s per-session timer; `busy`/`retry` clears
|
|
75
75
|
it. A `session.updated` carrying a fresh active goal (`turnsUsed === 0` or
|
|
76
76
|
`statusReason === 'resumed'`) arms a kickoff timer — 3s for fresh goals,
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
// has no channel to settle its own goal. When the small model is unavailable
|
|
12
12
|
// the loop still terminates via the budget and the continuation cap.
|
|
13
13
|
//
|
|
14
|
-
// Purely event-driven
|
|
14
|
+
// Purely event-driven: no polling, no backfill, no session
|
|
15
15
|
// scans. Only sessions that emit events while the server runs ever tick.
|
|
16
16
|
|
|
17
17
|
import fs from 'fs';
|
|
@@ -110,8 +110,8 @@ const buildAuditSystemPrompt = () => [
|
|
|
110
110
|
].join('\n');
|
|
111
111
|
|
|
112
112
|
// Hard guard against language hallucination (account-side personalization
|
|
113
|
-
// can leak a different language despite the instruction
|
|
114
|
-
//
|
|
113
|
+
// can leak a different language despite the instruction): if the note uses
|
|
114
|
+
// a script absent from the objective
|
|
115
115
|
// and the agent's reply, drop the note but keep the verdict.
|
|
116
116
|
const SCRIPT_RANGES = [
|
|
117
117
|
/[Ѐ-ӿ]/, // Cyrillic
|