@openchambery/web 1.19.0 → 1.19.2-beta.1
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/{AssistantView-WZ8qXOm6.js → AssistantView-DGqRio2e.js} +1 -1
- package/dist/assets/{ChatContainer-CPOlhHJn.js → ChatContainer-CM43Pxln.js} +6 -6
- package/dist/assets/ContextPanel-Bp3ZKAJE.js +288 -0
- package/dist/assets/{DiagramView-BFE0XxUH.js → DiagramView-BH5YxFAL.js} +1 -1
- package/dist/assets/{MarkdownRendererImpl-BisXjTdJ.js → MarkdownRendererImpl-Cmm9i5Vj.js} +1 -1
- package/dist/assets/{MobileDetailNavigation-DRiFdQXq.js → MobileDetailNavigation-nITHp7W3.js} +1 -1
- package/dist/assets/{MobileShareBridge-D-kwfbug.js → MobileShareBridge-C_QaKaB6.js} +1 -1
- package/dist/assets/MobileSurface-CjpCviv6.js +2 -0
- package/dist/assets/{MultiRunWindow-C_Tbyfco.js → MultiRunWindow-B1ALyR36.js} +1 -1
- package/dist/assets/{SettingsView-BOrARuHr.js → SettingsView-BW8d_8pw.js} +1 -1
- package/dist/assets/{SettingsWindow-BmegFGj-.js → SettingsWindow-7SmQV5GW.js} +1 -1
- package/dist/assets/{TerminalView-BcFMX_mg.js → TerminalView-C7EFF7ZS.js} +1 -1
- package/dist/assets/{ToolOutputDialog-DN2IYmqU.js → ToolOutputDialog-Cba_60bU.js} +1 -1
- package/dist/assets/appThemeRegistry-Co5dwWx_.js +2 -0
- package/dist/assets/{gitApi-CxPLKmZ2.js → gitApi-DaTTo7St.js} +1 -1
- package/dist/assets/{insertionBoundaries-2Mzsx_VV.js → insertionBoundaries-BoIHXczc.js} +1 -1
- package/dist/assets/{main-dGakBRsd.js → main-IyYkGXkX.js} +2 -2
- package/dist/assets/{main-C1IdVCQf.js → main-WVLfE2Df.js} +5 -5
- package/dist/assets/{miniChat-C8rpOcNP.js → miniChat-CAAQzm9R.js} +2 -2
- package/dist/assets/{mobile-Dm22ebC9.js → mobile-BiCmfs5Y.js} +2 -2
- package/dist/assets/{multirun-ColPhFw1.js → multirun-DPXG-TWE.js} +1 -1
- package/dist/assets/{projectMeta-CKI_msIR.js → projectMeta-B0YA9FxC.js} +1 -1
- package/dist/assets/{radio-DKNOd1MW.js → radio-CUea8K8R.js} +1 -1
- package/dist/assets/{renderElectronMiniChatApp-B8ly-dZn.js → renderElectronMiniChatApp-CGA16ZyN.js} +2 -2
- package/dist/assets/{renderMobileApp-OAlYU54b.js → renderMobileApp-D9fgFt6E.js} +4 -4
- package/dist/assets/{runtimeConfig-QFdp6Itq.js → runtimeConfig-BJ3S27Ah.js} +1 -1
- package/dist/assets/runtimeEndpointReset-C9pWVmjf.js +4 -0
- package/dist/assets/{shikiHighlight-C7KkxQ9i.js → shikiHighlight-_cgskSv2.js} +1 -1
- package/dist/assets/{useAppFontEffects-D0FmY4C_.js → useAppFontEffects-B_a7MLYy.js} +5 -5
- package/dist/assets/{useDesktopWindowControlsLayout-Ce9FSwxa.js → useDesktopWindowControlsLayout-D-74BaI3.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-C5CrdVlr.js → useEffectiveDirectory-DWwrRiBF.js} +1 -1
- package/dist/assets/{useMobileNavigationStore-jKG6OU5F.js → useMobileNavigationStore-7LBI6idi.js} +1 -1
- package/dist/assets/{useSessionAutoCleanup-C6GD9W2z.js → useSessionAutoCleanup-Buvg8rem.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-mc9J5erB.js → useWorkerHighlightedLines-BdlVg3Sj.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/mini-chat.html +2 -2
- package/dist/mobile.html +2 -2
- package/package.json +1 -1
- package/server/index.js +4 -0
- package/server/lib/opencode/DOCUMENTATION.md +1 -1
- package/server/lib/opencode/routes.js +24 -5
- package/server/lib/opencode/routes.test.js +78 -1
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +25 -2
- package/server/lib/scheduled-tasks/runtime.js +234 -3
- package/server/lib/scheduled-tasks/runtime.test.js +221 -0
- package/dist/assets/ContextPanel-ClzJl6RQ.js +0 -288
- package/dist/assets/MobileSurface-NsB1sKld.js +0 -2
- package/dist/assets/appThemeRegistry-7v5TIiNK.js +0 -2
- package/dist/assets/runtimeEndpointReset-MzR_-tYR.js +0 -4
|
@@ -305,6 +305,108 @@ 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 parseSessionEventIdle = (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 { idle: false, sessionID: '' };
|
|
399
|
+
}
|
|
400
|
+
if (payload?.type === 'session.idle') {
|
|
401
|
+
return { idle: true, sessionID };
|
|
402
|
+
}
|
|
403
|
+
if (payload?.type === 'session.status') {
|
|
404
|
+
const type = properties?.status?.type ?? properties?.info?.type;
|
|
405
|
+
return { idle: type === 'idle', sessionID };
|
|
406
|
+
}
|
|
407
|
+
return { idle: false, sessionID };
|
|
408
|
+
};
|
|
409
|
+
|
|
308
410
|
export const createScheduledTasksRuntime = (deps) => {
|
|
309
411
|
const {
|
|
310
412
|
projectConfigRuntime,
|
|
@@ -331,6 +433,34 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
331
433
|
const runningCountByProject = new Map();
|
|
332
434
|
let runningGlobalCount = 0;
|
|
333
435
|
const queue = [];
|
|
436
|
+
/** sessionID → { projectID, taskID } for post-run continuation correction. */
|
|
437
|
+
const lastSessionOwners = new Map();
|
|
438
|
+
|
|
439
|
+
const rememberLastSession = (projectID, task) => {
|
|
440
|
+
const sessionID = task?.state?.lastSessionId;
|
|
441
|
+
if (typeof sessionID !== 'string' || !sessionID || !task?.id) {
|
|
442
|
+
return;
|
|
443
|
+
}
|
|
444
|
+
lastSessionOwners.set(sessionID, { projectID, taskID: task.id });
|
|
445
|
+
};
|
|
446
|
+
|
|
447
|
+
const forgetLastSessionsForProject = (projectID) => {
|
|
448
|
+
for (const [sessionID, owner] of [...lastSessionOwners.entries()]) {
|
|
449
|
+
if (owner.projectID === projectID) {
|
|
450
|
+
lastSessionOwners.delete(sessionID);
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
|
|
455
|
+
const forgetLastSessionIfOwned = (projectID, taskID, sessionID) => {
|
|
456
|
+
if (typeof sessionID !== 'string' || !sessionID) {
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
const owner = lastSessionOwners.get(sessionID);
|
|
460
|
+
if (owner?.projectID === projectID && owner?.taskID === taskID) {
|
|
461
|
+
lastSessionOwners.delete(sessionID);
|
|
462
|
+
}
|
|
463
|
+
};
|
|
334
464
|
|
|
335
465
|
const clearTimerForKey = (taskKey) => {
|
|
336
466
|
const timer = timersByTaskKey.get(taskKey);
|
|
@@ -361,9 +491,11 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
361
491
|
|
|
362
492
|
const setProjectTasks = (projectID, tasks) => {
|
|
363
493
|
clearProjectTimers(projectID);
|
|
494
|
+
forgetLastSessionsForProject(projectID);
|
|
364
495
|
const taskMap = new Map();
|
|
365
496
|
for (const task of tasks) {
|
|
366
497
|
taskMap.set(task.id, task);
|
|
498
|
+
rememberLastSession(projectID, task);
|
|
367
499
|
}
|
|
368
500
|
tasksByProject.set(projectID, taskMap);
|
|
369
501
|
};
|
|
@@ -412,7 +544,14 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
412
544
|
if (!taskMap) {
|
|
413
545
|
return;
|
|
414
546
|
}
|
|
547
|
+
const previous = taskMap.get(nextTask.id);
|
|
548
|
+
const previousSessionID = previous?.state?.lastSessionId;
|
|
549
|
+
const nextSessionID = nextTask.state?.lastSessionId;
|
|
550
|
+
if (previousSessionID && previousSessionID !== nextSessionID) {
|
|
551
|
+
forgetLastSessionIfOwned(projectID, nextTask.id, previousSessionID);
|
|
552
|
+
}
|
|
415
553
|
taskMap.set(nextTask.id, nextTask);
|
|
554
|
+
rememberLastSession(projectID, nextTask);
|
|
416
555
|
};
|
|
417
556
|
|
|
418
557
|
const syncTaskSchedule = async (projectID, task) => {
|
|
@@ -1278,14 +1417,17 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1278
1417
|
|
|
1279
1418
|
const nextRunAt = computeNextRunAt(latestTask, finishedAt);
|
|
1280
1419
|
try {
|
|
1281
|
-
|
|
1420
|
+
const statePatch = {
|
|
1282
1421
|
lastStatus: status,
|
|
1283
1422
|
lastDurationMs: durationMs,
|
|
1284
1423
|
lastError: status === 'error' ? errorMessage : undefined,
|
|
1285
|
-
lastSessionId: status === 'success' ? sessionID : undefined,
|
|
1286
1424
|
nextRunAt: Number.isFinite(nextRunAt) ? nextRunAt : undefined,
|
|
1287
1425
|
updatedAt: finishedAt,
|
|
1288
|
-
}
|
|
1426
|
+
};
|
|
1427
|
+
if (sessionID) {
|
|
1428
|
+
statePatch.lastSessionId = sessionID;
|
|
1429
|
+
}
|
|
1430
|
+
stateResult = await projectConfigRuntime.updateScheduledTaskState(projectID, taskID, statePatch);
|
|
1289
1431
|
if (stateResult.task) {
|
|
1290
1432
|
updateInMemoryTask(projectID, stateResult.task);
|
|
1291
1433
|
if (stateResult.task.enabled && Number.isFinite(stateResult.task.state?.nextRunAt)) {
|
|
@@ -1301,6 +1443,9 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1301
1443
|
error: errorMessage,
|
|
1302
1444
|
});
|
|
1303
1445
|
}
|
|
1446
|
+
if (sessionID) {
|
|
1447
|
+
lastSessionOwners.set(sessionID, { projectID, taskID });
|
|
1448
|
+
}
|
|
1304
1449
|
|
|
1305
1450
|
// History final status reflects the ultimate run outcome, including
|
|
1306
1451
|
// state-persistence failures. A failed finalize leaves the row running
|
|
@@ -1441,6 +1586,91 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1441
1586
|
};
|
|
1442
1587
|
};
|
|
1443
1588
|
|
|
1589
|
+
const handleSessionIdleCorrection = async (event) => {
|
|
1590
|
+
const { idle, sessionID } = parseSessionEventIdle(event);
|
|
1591
|
+
if (!idle || !sessionID) {
|
|
1592
|
+
return;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
const owner = lastSessionOwners.get(sessionID);
|
|
1596
|
+
if (!owner) {
|
|
1597
|
+
return;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
const { projectID, taskID } = owner;
|
|
1601
|
+
const taskKey = buildTaskKey(projectID, taskID);
|
|
1602
|
+
if (runningTaskKeys.has(taskKey)) {
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
const task = tasksByProject.get(projectID)?.get(taskID);
|
|
1607
|
+
const lastStatus = task?.state?.lastStatus;
|
|
1608
|
+
if (lastStatus === 'success' || lastStatus === 'running') {
|
|
1609
|
+
return;
|
|
1610
|
+
}
|
|
1611
|
+
if (lastStatus !== 'error') {
|
|
1612
|
+
return;
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
const projectPath = projectPathByID.get(projectID) || await ensureProjectPath(projectID);
|
|
1616
|
+
if (!projectPath) {
|
|
1617
|
+
return;
|
|
1618
|
+
}
|
|
1619
|
+
|
|
1620
|
+
const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
|
|
1621
|
+
const authHeaders = getOpenCodeAuthHeaders();
|
|
1622
|
+
const client = createOpencodeClient({
|
|
1623
|
+
baseUrl,
|
|
1624
|
+
headers: authHeaders,
|
|
1625
|
+
});
|
|
1626
|
+
const snapshot = await snapshotSessionOutcome({
|
|
1627
|
+
client,
|
|
1628
|
+
sessionID,
|
|
1629
|
+
projectPath,
|
|
1630
|
+
goalEnabled: Boolean(task.execution?.goalEnabled),
|
|
1631
|
+
});
|
|
1632
|
+
if (snapshot.outcome !== 'success') {
|
|
1633
|
+
return;
|
|
1634
|
+
}
|
|
1635
|
+
|
|
1636
|
+
if (runningTaskKeys.has(taskKey)) {
|
|
1637
|
+
return;
|
|
1638
|
+
}
|
|
1639
|
+
const latest = tasksByProject.get(projectID)?.get(taskID);
|
|
1640
|
+
if (latest?.state?.lastStatus !== 'error') {
|
|
1641
|
+
return;
|
|
1642
|
+
}
|
|
1643
|
+
|
|
1644
|
+
const stateResult = await projectConfigRuntime.updateScheduledTaskState(projectID, taskID, {
|
|
1645
|
+
lastStatus: 'success',
|
|
1646
|
+
lastError: undefined,
|
|
1647
|
+
lastSessionId: sessionID,
|
|
1648
|
+
updatedAt: Date.now(),
|
|
1649
|
+
});
|
|
1650
|
+
if (stateResult.task) {
|
|
1651
|
+
updateInMemoryTask(projectID, stateResult.task);
|
|
1652
|
+
}
|
|
1653
|
+
|
|
1654
|
+
try {
|
|
1655
|
+
emitTaskRunEvent?.({
|
|
1656
|
+
projectID,
|
|
1657
|
+
taskID,
|
|
1658
|
+
ranAt: Date.now(),
|
|
1659
|
+
status: 'success',
|
|
1660
|
+
sessionID,
|
|
1661
|
+
});
|
|
1662
|
+
} catch {
|
|
1663
|
+
}
|
|
1664
|
+
};
|
|
1665
|
+
|
|
1666
|
+
const observeSessionEvent = (event) => {
|
|
1667
|
+
return handleSessionIdleCorrection(event).catch((error) => {
|
|
1668
|
+
logger.warn?.('[ScheduledTasks] observeSessionEvent failed', {
|
|
1669
|
+
error: safeErrorMessage(error),
|
|
1670
|
+
});
|
|
1671
|
+
});
|
|
1672
|
+
};
|
|
1673
|
+
|
|
1444
1674
|
return {
|
|
1445
1675
|
start,
|
|
1446
1676
|
stop,
|
|
@@ -1448,5 +1678,6 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1448
1678
|
syncProject,
|
|
1449
1679
|
runNow,
|
|
1450
1680
|
getStatus,
|
|
1681
|
+
observeSessionEvent,
|
|
1451
1682
|
};
|
|
1452
1683
|
};
|
|
@@ -993,4 +993,225 @@ 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('corrects lastStatus error → success on idle after a continuation, without rewriting history', 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
|
+
client.messages.mockImplementation(async () => ({
|
|
1068
|
+
data: [{
|
|
1069
|
+
info: {
|
|
1070
|
+
id: 'msg_ok',
|
|
1071
|
+
role: 'assistant',
|
|
1072
|
+
time: { completed: Date.now() },
|
|
1073
|
+
},
|
|
1074
|
+
}],
|
|
1075
|
+
}));
|
|
1076
|
+
|
|
1077
|
+
await runtime.observeSessionEvent({
|
|
1078
|
+
payload: {
|
|
1079
|
+
type: 'session.idle',
|
|
1080
|
+
properties: { sessionID: 'ses_1' },
|
|
1081
|
+
},
|
|
1082
|
+
});
|
|
1083
|
+
|
|
1084
|
+
expect(history.finishRun).toHaveBeenCalledTimes(1);
|
|
1085
|
+
const correctionPatch = updateScheduledTaskState.mock.calls.at(-1)[2];
|
|
1086
|
+
expect(correctionPatch).toEqual(expect.objectContaining({
|
|
1087
|
+
lastStatus: 'success',
|
|
1088
|
+
lastError: undefined,
|
|
1089
|
+
lastSessionId: 'ses_1',
|
|
1090
|
+
}));
|
|
1091
|
+
expect(Object.hasOwn(correctionPatch, 'lastRunAt')).toBe(false);
|
|
1092
|
+
expect(Object.hasOwn(correctionPatch, 'lastDurationMs')).toBe(false);
|
|
1093
|
+
expect(Object.hasOwn(correctionPatch, 'nextRunAt')).toBe(false);
|
|
1094
|
+
expect(emitTaskRunEvent).toHaveBeenCalledWith(expect.objectContaining({
|
|
1095
|
+
projectID: 'project-1',
|
|
1096
|
+
taskID: 'task-1',
|
|
1097
|
+
status: 'success',
|
|
1098
|
+
sessionID: 'ses_1',
|
|
1099
|
+
}));
|
|
1100
|
+
|
|
1101
|
+
vi.unstubAllGlobals();
|
|
1102
|
+
});
|
|
1103
|
+
|
|
1104
|
+
it('does not correct lastStatus while the task is still running', async () => {
|
|
1105
|
+
vi.useFakeTimers();
|
|
1106
|
+
const history = createHistoryStore();
|
|
1107
|
+
const erroredTask = {
|
|
1108
|
+
...scheduledTask,
|
|
1109
|
+
state: {
|
|
1110
|
+
...scheduledTask.state,
|
|
1111
|
+
lastStatus: 'error',
|
|
1112
|
+
lastSessionId: 'ses_1',
|
|
1113
|
+
lastError: 'ProviderError',
|
|
1114
|
+
},
|
|
1115
|
+
};
|
|
1116
|
+
createSuccessfulClient({ settlement: 'busy-then-success' });
|
|
1117
|
+
const fetchMock = vi.fn(async (url) => {
|
|
1118
|
+
if (String(url).includes('prompt_async')) {
|
|
1119
|
+
return { ok: true, text: async () => '' };
|
|
1120
|
+
}
|
|
1121
|
+
throw new Error(`unexpected fetch ${url}`);
|
|
1122
|
+
});
|
|
1123
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1124
|
+
|
|
1125
|
+
const emitTaskRunEvent = vi.fn();
|
|
1126
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1127
|
+
task: { ...erroredTask, state: { ...erroredTask.state, ...state } },
|
|
1128
|
+
}));
|
|
1129
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1130
|
+
projectConfigRuntime: {
|
|
1131
|
+
listScheduledTasks: vi.fn(async () => [erroredTask]),
|
|
1132
|
+
updateScheduledTaskState,
|
|
1133
|
+
upsertScheduledTask: vi.fn(),
|
|
1134
|
+
},
|
|
1135
|
+
runHistoryStore: history,
|
|
1136
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1137
|
+
emitTaskRunEvent,
|
|
1138
|
+
});
|
|
1139
|
+
await runtime.syncProject('project-1');
|
|
1140
|
+
|
|
1141
|
+
const runPromise = runtime.runNow('project-1', 'task-1');
|
|
1142
|
+
await vi.advanceTimersByTimeAsync(0);
|
|
1143
|
+
expect(history.finishRun).not.toHaveBeenCalled();
|
|
1144
|
+
|
|
1145
|
+
const successEmitsBefore = emitTaskRunEvent.mock.calls.filter((call) => call[0].status === 'success').length;
|
|
1146
|
+
await runtime.observeSessionEvent({
|
|
1147
|
+
payload: {
|
|
1148
|
+
type: 'session.idle',
|
|
1149
|
+
properties: { sessionID: 'ses_1' },
|
|
1150
|
+
},
|
|
1151
|
+
});
|
|
1152
|
+
expect(history.finishRun).not.toHaveBeenCalled();
|
|
1153
|
+
expect(updateScheduledTaskState.mock.calls.map((call) => call[2].lastStatus)).not.toContain('success');
|
|
1154
|
+
expect(emitTaskRunEvent.mock.calls.filter((call) => call[0].status === 'success')).toHaveLength(successEmitsBefore);
|
|
1155
|
+
|
|
1156
|
+
await vi.advanceTimersByTimeAsync(1_000);
|
|
1157
|
+
const result = await runPromise;
|
|
1158
|
+
expect(result.ok).toBe(true);
|
|
1159
|
+
|
|
1160
|
+
vi.unstubAllGlobals();
|
|
1161
|
+
vi.useRealTimers();
|
|
1162
|
+
});
|
|
1163
|
+
|
|
1164
|
+
it('does not correct lastStatus when it is already success', async () => {
|
|
1165
|
+
const history = createHistoryStore();
|
|
1166
|
+
const client = createSuccessfulClient();
|
|
1167
|
+
const fetchMock = vi.fn(async (url) => {
|
|
1168
|
+
if (String(url).includes('prompt_async')) {
|
|
1169
|
+
return { ok: true, text: async () => '' };
|
|
1170
|
+
}
|
|
1171
|
+
throw new Error(`unexpected fetch ${url}`);
|
|
1172
|
+
});
|
|
1173
|
+
vi.stubGlobal('fetch', fetchMock);
|
|
1174
|
+
|
|
1175
|
+
const emitTaskRunEvent = vi.fn();
|
|
1176
|
+
const updateScheduledTaskState = vi.fn(async (_projectID, _taskID, state) => ({
|
|
1177
|
+
task: { ...scheduledTask, state: { ...scheduledTask.state, ...state } },
|
|
1178
|
+
}));
|
|
1179
|
+
const runtime = createRuntime(updateScheduledTaskState, {
|
|
1180
|
+
runHistoryStore: history,
|
|
1181
|
+
waitForOpenCodeReady: vi.fn(async () => {}),
|
|
1182
|
+
emitTaskRunEvent,
|
|
1183
|
+
});
|
|
1184
|
+
await runtime.syncProject('project-1');
|
|
1185
|
+
const result = await runtime.runNow('project-1', 'task-1');
|
|
1186
|
+
expect(result.ok).toBe(true);
|
|
1187
|
+
expect(history.finishRun).toHaveBeenCalledTimes(1);
|
|
1188
|
+
const stateWritesAfterRun = updateScheduledTaskState.mock.calls.length;
|
|
1189
|
+
const successEmitsAfterRun = emitTaskRunEvent.mock.calls.filter((call) => call[0].status === 'success').length;
|
|
1190
|
+
|
|
1191
|
+
client.messages.mockImplementation(async () => ({
|
|
1192
|
+
data: [{
|
|
1193
|
+
info: {
|
|
1194
|
+
id: 'msg_ok',
|
|
1195
|
+
role: 'assistant',
|
|
1196
|
+
time: { completed: Date.now() },
|
|
1197
|
+
},
|
|
1198
|
+
}],
|
|
1199
|
+
}));
|
|
1200
|
+
|
|
1201
|
+
await runtime.observeSessionEvent({
|
|
1202
|
+
payload: {
|
|
1203
|
+
type: 'session.status',
|
|
1204
|
+
properties: {
|
|
1205
|
+
sessionID: 'ses_1',
|
|
1206
|
+
status: { type: 'idle' },
|
|
1207
|
+
},
|
|
1208
|
+
},
|
|
1209
|
+
});
|
|
1210
|
+
|
|
1211
|
+
expect(history.finishRun).toHaveBeenCalledTimes(1);
|
|
1212
|
+
expect(updateScheduledTaskState).toHaveBeenCalledTimes(stateWritesAfterRun);
|
|
1213
|
+
expect(emitTaskRunEvent.mock.calls.filter((call) => call[0].status === 'success')).toHaveLength(successEmitsAfterRun);
|
|
1214
|
+
|
|
1215
|
+
vi.unstubAllGlobals();
|
|
1216
|
+
});
|
|
996
1217
|
});
|