@openchambery/web 1.19.20 → 2.0.0-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/README.md +13 -11
- package/bin/cli-opencode.test.js +208 -0
- package/bin/cli.js +62 -9
- package/bin/lib/cli-startup.js +105 -2
- package/bin/lib/cli-startup.test.js +165 -0
- package/dist/assets/{AgentAvatar-NT957MuI.js → AgentAvatar-CLWWNu_Q.js} +1 -1
- package/dist/assets/AssistantView-BTCkm8yk.js +3 -0
- package/dist/assets/{DiagramView-C_JfbxNl.js → DiagramView-xuSJTnv7.js} +1 -1
- package/dist/assets/{FadeInOnReveal-CilRl4gc.js → FadeInOnReveal-Ov0D96d0.js} +1 -1
- package/dist/assets/{Icon-i5GiQRLq.js → Icon-DD-BuMfZ.js} +1 -1
- package/dist/assets/MarkdownRenderer-bc_p1H7l.js +2 -0
- package/dist/assets/{MarkdownRendererImpl-B1iUwzOz.js → MarkdownRendererImpl-SKi4oG0c.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-BPtMj3-X.js → MarkstreamRendererImpl-CnkodGJW.js} +1 -1
- package/dist/assets/MobileDetailNavigation-Bnx59U_J.js +1 -0
- package/dist/assets/{MobileShareBridge-CKClHsWN.js → MobileShareBridge-BQlP4J-F.js} +1 -1
- package/dist/assets/MobileSurface-BXLeiZ4d.js +2 -0
- package/dist/assets/{MultiRunWindow-DiA24bnN.js → MultiRunWindow-ChZGob0v.js} +1 -1
- package/dist/assets/{OnboardingScreen-BGRLk4Wi.js → OnboardingScreen-DtQJxZG-.js} +1 -1
- package/dist/assets/{OverlayScrollbar-NuQI58XD.js → OverlayScrollbar-pMwxaCca.js} +1 -1
- package/dist/assets/SettingsView-B5I0KDTZ.js +25 -0
- package/dist/assets/{SettingsWindow-XlzRSS7E.js → SettingsWindow-DfnsFa8y.js} +1 -1
- package/dist/assets/{TerminalView-DdIaaLba.js → TerminalView-Dh9MWCpS.js} +1 -1
- package/dist/assets/{ToolOutputDialog-BbwuLdXU.js → ToolOutputDialog-BejmjrRk.js} +1 -1
- package/dist/assets/{appThemeRegistry-ChzfD7mc.js → appThemeRegistry-COGfxTFV.js} +1 -1
- package/dist/assets/{button-DtzFDvij.js → button-D-rATN7w.js} +1 -1
- package/dist/assets/{collapsible-D-nzhcyF.js → collapsible-BXRd5_Hr.js} +1 -1
- package/dist/assets/{context-menu-BzVSzHLG.js → context-menu-jGtH6fR9.js} +1 -1
- package/dist/assets/{diagram-2YWalSb5.js → diagram-DQf7Y_uR.js} +1 -1
- package/dist/assets/{dialog-DGV7DsZC.js → dialog-bfbvR4VH.js} +1 -1
- package/dist/assets/es-c52LTHHe.js +15 -0
- package/dist/assets/{fileMentionTouchSelection-DyjmxUEN.js → fileMentionTouchSelection-BvrNCroG.js} +1 -1
- package/dist/assets/{fr-DsBZeN8Y.js → fr-PisFbUE8.js} +3 -3
- package/dist/assets/gitApi-ButwzQ0N.js +9 -0
- package/dist/assets/{input-CbB2HdLg.js → input-8r6F_Fza.js} +1 -1
- package/dist/assets/insertionBoundaries-DGs7S5zv.js +1 -0
- package/dist/assets/ja-C8Ikun3Z.js +15 -0
- package/dist/assets/ko-ClFys5o2.js +15 -0
- package/dist/assets/main-CCE_4y5F.js +2 -0
- package/dist/assets/main-Dn9rpsgl.js +169 -0
- package/dist/assets/{markdownHeightCache-C9ym9Tsb.js → markdownHeightCache-1Qo6y6IJ.js} +1 -1
- package/dist/assets/miniChat-BpjxVqgg.js +2 -0
- package/dist/assets/mobile-nMJCaNq1.js +2 -0
- package/dist/assets/{multirun-8fkF0Ji_.js → multirun-BvSG94mW.js} +1 -1
- package/dist/assets/pl-CnaKWN5F.js +15 -0
- package/dist/assets/platform-CWTIp02q.js +4 -0
- package/dist/assets/{projectMeta--y6tyjrv.js → projectMeta-DOe49dM_.js} +2 -2
- package/dist/assets/pt-BR-s8OpfRI6.js +15 -0
- package/dist/assets/renderElectronMiniChatApp-R6dKh79f.js +2 -0
- package/dist/assets/renderMobileApp-y-S8y8v_.js +28 -0
- package/dist/assets/{runtimeConfig-DYWtBrkW.js → runtimeConfig-BMvqa5we.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-BSWg99jc.js → runtimeEndpointReset-B6BxhvR0.js} +5 -5
- package/dist/assets/{sessionChangeSummary-BwupJF-Q.js → sessionChangeSummary-VVBlEXuh.js} +1 -1
- package/dist/assets/{sessionLookup-DP2X1qRR.js → sessionLookup-gh-hVRIc.js} +1 -1
- package/dist/assets/{sortable-tabs-strip-CfgEJfB0.js → sortable-tabs-strip-CudoSgV4.js} +1 -1
- package/dist/assets/{src-DZ60kHBP.css → src-BzX5UI-c.css} +1 -1
- package/dist/assets/{store-B6dImdlY.js → store-DJczmczv.js} +4 -4
- package/dist/assets/{terminalApi-xjqssKke.js → terminalApi-DiF6JmBP.js} +1 -1
- package/dist/assets/{textarea-BnD799LW.js → textarea-3XR8sUfy.js} +1 -1
- package/dist/assets/{tooltip-Djn7MSOX.js → tooltip-CQvpK-UV.js} +1 -1
- package/dist/assets/uk-CaX0iqkB.js +15 -0
- package/dist/assets/{useAppFontEffects-DWLxpWlS.js → useAppFontEffects-Cpmpo7Wb.js} +45 -45
- package/dist/assets/{useDesktopWindowControlsLayout-BGPR_rq6.js → useDesktopWindowControlsLayout-B1GX59-L.js} +1 -1
- package/dist/assets/{useEffectiveDirectory-CbV9_r1s.js → useEffectiveDirectory-CN6JoCFw.js} +1 -1
- package/dist/assets/{useFontPreferences-B1JYyzpK.js → useFontPreferences-DcbXQOkc.js} +1 -1
- package/dist/assets/useMobileNavigationStore-BVjFtXSy.js +414 -0
- package/dist/assets/{useSessionAutoCleanup-Dnors7hB.js → useSessionAutoCleanup-BLjymaFt.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-DQqqrOBU.js → useWorkerHighlightedLines-Bqn4AiBL.js} +1 -1
- package/dist/assets/vendor-opencode-client-BOe2qpQb.js +12 -0
- package/dist/assets/{vendor-yaml-DIcAK3EC.js → vendor-yaml-C9KlJAlj.js} +37 -37
- package/dist/assets/zh-CN-20MYEjha.js +15 -0
- package/dist/assets/{zh-TW-BsOQbrwT.js → zh-TW-DiN04qTw.js} +3 -3
- package/dist/index.html +6 -6
- package/dist/mini-chat.html +6 -6
- package/dist/mobile.html +6 -6
- package/package.json +2 -2
- package/server/index.js +107 -0
- package/server/lib/assistants/DOCUMENTATION.md +18 -18
- package/server/lib/assistants/assign.js +2 -1
- package/server/lib/assistants/contact-store.js +37 -19
- package/server/lib/assistants/projection-history.test.js +207 -0
- package/server/lib/assistants/routes.js +5 -5
- package/server/lib/assistants/service.js +493 -123
- package/server/lib/assistants/service.test.js +203 -133
- package/server/lib/conversations/DOCUMENTATION.md +4 -4
- package/server/lib/conversations/service.js +86 -57
- package/server/lib/conversations/service.test.js +116 -100
- package/server/lib/event-stream/global-hub.js +2 -1
- package/server/lib/llm/DOCUMENTATION.md +71 -84
- package/server/lib/llm/catalog.js +111 -45
- package/server/lib/llm/catalog.test.js +121 -59
- package/server/lib/llm/completions.js +37 -12
- package/server/lib/llm/completions.test.js +45 -4
- package/server/lib/llm/generate.js +421 -450
- package/server/lib/llm/generate.test.js +553 -487
- package/server/lib/llm/routes.js +6 -4
- package/server/lib/message-queue/DOCUMENTATION.md +5 -5
- package/server/lib/message-queue/opencode-adapter.js +191 -104
- package/server/lib/message-queue/opencode-adapter.test.js +328 -58
- package/server/lib/message-queue/runtime.test.js +6 -6
- package/server/lib/message-queue/worker.js +7 -1
- package/server/lib/opencode/DOCUMENTATION.md +28 -5
- package/server/lib/opencode/config-entity-routes.js +84 -13
- package/server/lib/opencode/config-entity-routes.test.js +41 -10
- package/server/lib/opencode/ensure-cli.js +239 -0
- package/server/lib/opencode/ensure-cli.test.js +119 -0
- package/server/lib/opencode/env-runtime.js +193 -110
- package/server/lib/opencode/env-runtime.test.js +221 -17
- package/server/lib/opencode/feature-routes-runtime.js +37 -1
- package/server/lib/opencode/lifecycle.js +204 -70
- package/server/lib/opencode/lifecycle.test.js +376 -6
- package/server/lib/opencode/network-runtime.js +28 -16
- package/server/lib/opencode/network-runtime.test.js +68 -4
- package/server/lib/opencode/opencode2-pin.js +139 -0
- package/server/lib/opencode/opencode2-pin.test.js +87 -0
- package/server/lib/opencode/opencode2-sidecar.e2e.test.js +203 -0
- package/server/lib/opencode/plugin-compatibility.js +15 -0
- package/server/lib/opencode/plugin-compatibility.test.js +21 -0
- package/server/lib/opencode/plugins.js +4 -3
- package/server/lib/opencode/plugins.test.js +12 -0
- package/server/lib/opencode/proxy.js +153 -16
- package/server/lib/opencode/proxy.test.js +120 -0
- package/server/lib/opencode/routes.js +25 -32
- package/server/lib/opencode/routes.test.js +51 -52
- package/server/lib/opencode/server-utils-runtime.js +2 -0
- package/server/lib/opencode/skill-routes.js +6 -7
- package/server/lib/opencode/skill-routes.test.js +36 -5
- package/server/lib/opencode/v1-migration-gate.js +157 -0
- package/server/lib/opencode/v1-migration-gate.test.js +111 -0
- package/server/lib/opencode/v2-client.js +49 -0
- package/server/lib/opencode/watcher.js +2 -1
- package/server/lib/opencode/watcher.test.js +1 -1
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +71 -39
- package/server/lib/scheduled-tasks/managed-tool-route.js +31 -1
- package/server/lib/scheduled-tasks/managed-tool-route.test.js +23 -0
- package/server/lib/scheduled-tasks/routes.js +27 -0
- package/server/lib/scheduled-tasks/routes.test.js +94 -0
- package/server/lib/scheduled-tasks/runtime.js +331 -308
- package/server/lib/scheduled-tasks/runtime.test.js +530 -318
- package/server/lib/session-goal/DOCUMENTATION.md +65 -14
- package/server/lib/session-goal/capability.js +57 -0
- package/server/lib/session-goal/capability.test.js +33 -0
- package/server/lib/session-goal/routes.js +231 -2
- package/server/lib/session-goal/routes.test.js +211 -0
- package/server/lib/session-goal/runtime.js +46 -4
- package/server/lib/session-goal/runtime.test.js +62 -1
- package/server/lib/session-index/sync-runtime.js +20 -17
- package/server/lib/session-index/sync-runtime.test.js +9 -8
- package/server/lib/session-metadata/DOCUMENTATION.md +41 -0
- package/server/lib/session-metadata/routes.js +94 -0
- package/server/lib/session-metadata/routes.test.js +111 -0
- package/server/lib/session-metadata/session-metadata-store.js +235 -0
- package/server/lib/session-metadata/session-metadata-store.test.js +184 -0
- package/server/lib/session-turn-pages/DOCUMENTATION.md +3 -3
- package/server/lib/session-turn-pages/reconcile.service.js +1 -1
- package/server/lib/session-turn-pages/reconcile.service.test.js +16 -0
- package/server/lib/session-turn-pages/routes.js +204 -71
- package/server/lib/session-turn-pages/routes.test.js +285 -0
- package/server/lib/session-turn-pages/service.js +6 -2
- package/server/lib/session-turn-pages/service.test.js +34 -0
- package/server/lib/small-model/DOCUMENTATION.md +5 -4
- package/server/lib/small-model/catalog.js +56 -6
- package/server/lib/small-model/catalog.test.js +47 -0
- package/server/lib/small-model/index.js +3 -3
- package/server/lib/small-model/opencode-session.js +50 -206
- package/server/lib/small-model/opencode-session.test.js +58 -88
- package/server/opencode-proxy.test.js +61 -12
- package/dist/assets/AssistantView-BjvfkSRS.js +0 -3
- package/dist/assets/MarkdownRenderer-Cg82L3fN.js +0 -2
- package/dist/assets/MobileDetailNavigation-s_vvUD_F.js +0 -1
- package/dist/assets/MobileSurface-DHpXHbJp.js +0 -2
- package/dist/assets/SettingsView-l0J7pfYo.js +0 -25
- package/dist/assets/es-rqMiRP7-.js +0 -15
- package/dist/assets/gitApi-CHpibtno.js +0 -7
- package/dist/assets/insertionBoundaries-Yd1Lorjw.js +0 -1
- package/dist/assets/ja-Bgr8-dOg.js +0 -15
- package/dist/assets/ko-B_Gxh-0l.js +0 -15
- package/dist/assets/main-BoRXxmB3.js +0 -2
- package/dist/assets/main-DCDcdV5o.js +0 -169
- package/dist/assets/miniChat-12fxk5KR.js +0 -2
- package/dist/assets/mobile-CkZem9Ad.js +0 -2
- package/dist/assets/pl-DW2XJGC4.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- package/dist/assets/pt-BR-CIsanf4T.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-COFc0ZDJ.js +0 -2
- package/dist/assets/renderMobileApp-BKzx7JL3.js +0 -28
- package/dist/assets/uk-BPmPEUYJ.js +0 -15
- package/dist/assets/useMobileNavigationStore-DrB_awgk.js +0 -413
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- package/dist/assets/zh-CN-R8-mDcIp.js +0 -15
|
@@ -1,8 +1,13 @@
|
|
|
1
1
|
import crypto from 'node:crypto';
|
|
2
|
-
import { createOpencodeClient } from '@opencode-ai/sdk/v2';
|
|
3
2
|
import { DateTime } from 'luxon';
|
|
4
3
|
import parser from 'cron-parser';
|
|
4
|
+
import { makeOpenCodeV2Client } from '../opencode/v2-client.js';
|
|
5
5
|
import { expandSnippets } from '../opencode/snippets.js';
|
|
6
|
+
import {
|
|
7
|
+
getSessionGoalCapability,
|
|
8
|
+
isSessionGoalSupported,
|
|
9
|
+
sessionGoalUnavailableMessage,
|
|
10
|
+
} from '../session-goal/capability.js';
|
|
6
11
|
import { SCHEDULED_SLOT_CLAIMED_CODE } from './run-history-store.js';
|
|
7
12
|
|
|
8
13
|
const DEFAULT_GLOBAL_CONCURRENCY = 4;
|
|
@@ -15,16 +20,20 @@ const TASK_DUE_SLACK_MS = 5_000;
|
|
|
15
20
|
const MAX_TIMER_DELAY_MS = 2_147_483_647;
|
|
16
21
|
const PROJECT_SYNC_RETRY_DELAY_MS = 1_000;
|
|
17
22
|
const MAX_PROJECT_SYNC_RETRIES = 3;
|
|
18
|
-
const ARCHIVE_404_RETRY_DELAY_MS = 250;
|
|
19
23
|
/** Poll interval while waiting for the OpenCode session turn (or goal) to settle. */
|
|
20
24
|
const SESSION_SETTLEMENT_POLL_MS = 1_000;
|
|
21
25
|
/**
|
|
22
|
-
*
|
|
23
|
-
*
|
|
26
|
+
* Latest-side message page for settlement. order=desc + bounded limit — never
|
|
27
|
+
* order=asc page-0 + at(-1), which reads an old page once the session exceeds
|
|
28
|
+
* the limit.
|
|
24
29
|
*/
|
|
25
|
-
const
|
|
30
|
+
const SESSION_SETTLEMENT_MESSAGE_LIMIT = 50;
|
|
26
31
|
/** Goal terminal statuses — active/paused mean the run is still open. */
|
|
27
32
|
const GOAL_TERMINAL_STATUSES = new Set(['complete', 'blocked', 'budgetLimited']);
|
|
33
|
+
/** Assistant finish values that are real terminal success (not tool-calls mid-loop). */
|
|
34
|
+
const ASSISTANT_SUCCESS_FINISH = new Set(['stop']);
|
|
35
|
+
/** Assistant finish values that are terminal failure without an error object. */
|
|
36
|
+
const ASSISTANT_ERROR_FINISH = new Set(['error', 'length', 'content-filter']);
|
|
28
37
|
|
|
29
38
|
const buildTaskKey = (projectID, taskID) => `${projectID}:${taskID}`;
|
|
30
39
|
|
|
@@ -235,22 +244,6 @@ export const formatScheduledSessionTitle = (task, nowMs = Date.now()) => {
|
|
|
235
244
|
return `${SCHEDULED_TITLE_PREFIX}${trimmedName}${suffix}`;
|
|
236
245
|
};
|
|
237
246
|
|
|
238
|
-
const buildScheduledTaskMetadata = ({ projectID, taskID, runID, name }) => ({
|
|
239
|
-
openchamber: {
|
|
240
|
-
scheduledTask: {
|
|
241
|
-
projectID,
|
|
242
|
-
taskID,
|
|
243
|
-
runID,
|
|
244
|
-
name,
|
|
245
|
-
},
|
|
246
|
-
},
|
|
247
|
-
});
|
|
248
|
-
|
|
249
|
-
const isMissingSessionError = (result) => {
|
|
250
|
-
const status = result?.error?.status ?? result?.error?.statusCode ?? result?.status;
|
|
251
|
-
return status === 404 || result?.error?.code === 'not_found';
|
|
252
|
-
};
|
|
253
|
-
|
|
254
247
|
const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
255
248
|
if (signal?.aborted) {
|
|
256
249
|
reject(signal.reason ?? new DOMException('Aborted', 'AbortError'));
|
|
@@ -273,9 +266,11 @@ const sleep = (ms, signal) => new Promise((resolve, reject) => {
|
|
|
273
266
|
|
|
274
267
|
const readMessageInfo = (entry) => {
|
|
275
268
|
if (!entry || typeof entry !== 'object') return null;
|
|
276
|
-
|
|
277
|
-
if (
|
|
278
|
-
|
|
269
|
+
// SessionMessageInfo is the entry itself; tolerate projected { info, parts }.
|
|
270
|
+
if (entry.info && typeof entry.info === 'object') {
|
|
271
|
+
return entry.info;
|
|
272
|
+
}
|
|
273
|
+
return entry;
|
|
279
274
|
};
|
|
280
275
|
|
|
281
276
|
const formatAssistantError = (error) => {
|
|
@@ -291,6 +286,79 @@ const formatAssistantError = (error) => {
|
|
|
291
286
|
return 'assistant error';
|
|
292
287
|
};
|
|
293
288
|
|
|
289
|
+
const isAssistantMessage = (info) => (
|
|
290
|
+
info
|
|
291
|
+
&& typeof info === 'object'
|
|
292
|
+
&& (info.type === 'assistant' || info.role === 'assistant')
|
|
293
|
+
);
|
|
294
|
+
|
|
295
|
+
const isUserMessage = (info) => (
|
|
296
|
+
info
|
|
297
|
+
&& typeof info === 'object'
|
|
298
|
+
&& (info.type === 'user' || info.role === 'user')
|
|
299
|
+
);
|
|
300
|
+
|
|
301
|
+
/**
|
|
302
|
+
* Classify the newest message on a latest-side page.
|
|
303
|
+
* - finish tool-calls is mid-loop, never success
|
|
304
|
+
* - incomplete (no time.completed) is never success
|
|
305
|
+
* - model-switched / non-assistant tails are not success
|
|
306
|
+
* - only a completed assistant with a real terminal finish succeeds
|
|
307
|
+
*/
|
|
308
|
+
const classifyMessageTail = (messages) => {
|
|
309
|
+
if (!Array.isArray(messages) || messages.length === 0) {
|
|
310
|
+
return { outcome: 'unknown' };
|
|
311
|
+
}
|
|
312
|
+
// order=desc contract: index 0 is the newest message on this page.
|
|
313
|
+
// Do not re-rank by time.created — that reintroduces the old-page bug when
|
|
314
|
+
// a bounded page mixes ages (and fixtures / clock skew can invert created).
|
|
315
|
+
const latest = readMessageInfo(messages[0]);
|
|
316
|
+
if (!latest) {
|
|
317
|
+
return { outcome: 'unknown' };
|
|
318
|
+
}
|
|
319
|
+
if (isAssistantMessage(latest)) {
|
|
320
|
+
if (latest.error) {
|
|
321
|
+
return {
|
|
322
|
+
outcome: 'error',
|
|
323
|
+
error: formatAssistantError(latest.error),
|
|
324
|
+
};
|
|
325
|
+
}
|
|
326
|
+
const finish = typeof latest.finish === 'string' ? latest.finish.trim() : '';
|
|
327
|
+
if (finish === 'tool-calls') {
|
|
328
|
+
// Model paused for tools — not a successful run terminal.
|
|
329
|
+
return { outcome: 'busy' };
|
|
330
|
+
}
|
|
331
|
+
if (ASSISTANT_ERROR_FINISH.has(finish)) {
|
|
332
|
+
return {
|
|
333
|
+
outcome: 'error',
|
|
334
|
+
error: finish === 'error' ? 'assistant error' : `assistant finish ${finish}`,
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
if (latest.time?.completed && (!finish || ASSISTANT_SUCCESS_FINISH.has(finish))) {
|
|
338
|
+
return { outcome: 'success' };
|
|
339
|
+
}
|
|
340
|
+
// Incomplete assistant (no completed, or unknown finish) — not success.
|
|
341
|
+
return { outcome: 'busy' };
|
|
342
|
+
}
|
|
343
|
+
if (isUserMessage(latest)) {
|
|
344
|
+
return { outcome: 'awaiting_assistant', lastInfo: latest };
|
|
345
|
+
}
|
|
346
|
+
// model-switched, compaction, system, etc. — not a success terminal.
|
|
347
|
+
return { outcome: 'busy' };
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const listLatestMessages = async (client, sessionID, requestOptions) => {
|
|
351
|
+
if (typeof client?.message?.list !== 'function') {
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
const messagesResult = await client.message.list({
|
|
355
|
+
sessionID,
|
|
356
|
+
limit: SESSION_SETTLEMENT_MESSAGE_LIMIT,
|
|
357
|
+
order: 'desc',
|
|
358
|
+
}, requestOptions);
|
|
359
|
+
return Array.isArray(messagesResult?.data) ? messagesResult.data : null;
|
|
360
|
+
};
|
|
361
|
+
|
|
294
362
|
const extractGoalFromSession = (session) => {
|
|
295
363
|
const metadata = session?.metadata;
|
|
296
364
|
if (!metadata || typeof metadata !== 'object') return null;
|
|
@@ -308,9 +376,13 @@ const extractGoalFromSession = (session) => {
|
|
|
308
376
|
|
|
309
377
|
/**
|
|
310
378
|
* Single-shot session outcome for post-run continuation (no polling).
|
|
311
|
-
* Goal-enabled: terminal complete → success; blocked/budgetLimited
|
|
312
|
-
* otherwise not-yet-success.
|
|
313
|
-
*
|
|
379
|
+
* Goal-enabled + supported: terminal complete → success; blocked/budgetLimited
|
|
380
|
+
* → error; otherwise not-yet-success. Goal unsupported: never invent success.
|
|
381
|
+
* Prefer Host `readSessionMetadata` over OpenCode `session.get` (direct clients
|
|
382
|
+
* do not see the Host store). Store read failure is never empty-goal success.
|
|
383
|
+
* Non-goal: session.active failure stays unknown; latest-side assistant tail
|
|
384
|
+
* must be a real terminal (finish tool-calls / incomplete / model-switch ≠
|
|
385
|
+
* success). History rows are never rewritten here.
|
|
314
386
|
*/
|
|
315
387
|
const snapshotSessionOutcome = async ({
|
|
316
388
|
client,
|
|
@@ -318,76 +390,92 @@ const snapshotSessionOutcome = async ({
|
|
|
318
390
|
projectPath,
|
|
319
391
|
goalEnabled,
|
|
320
392
|
signal,
|
|
393
|
+
readSessionMetadata = null,
|
|
321
394
|
}) => {
|
|
322
395
|
const requestOptions = signal ? { signal } : undefined;
|
|
323
396
|
|
|
324
|
-
if (goalEnabled
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
if (goal
|
|
333
|
-
|
|
334
|
-
return { outcome: 'success' };
|
|
335
|
-
}
|
|
336
|
-
return {
|
|
337
|
-
outcome: 'error',
|
|
338
|
-
error: goal.note || `goal ${goal.status}`,
|
|
339
|
-
};
|
|
397
|
+
if (goalEnabled) {
|
|
398
|
+
if (!isSessionGoalSupported()) {
|
|
399
|
+
// Host goal state unavailable — do not promote assistant-tail idle to success.
|
|
400
|
+
return { outcome: 'busy' };
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const classifyGoal = (goal) => {
|
|
404
|
+
if (goal && GOAL_TERMINAL_STATUSES.has(goal.status)) {
|
|
405
|
+
if (goal.status === 'complete') {
|
|
406
|
+
return { outcome: 'success' };
|
|
340
407
|
}
|
|
408
|
+
return {
|
|
409
|
+
outcome: 'error',
|
|
410
|
+
error: goal.note || `goal ${goal.status}`,
|
|
411
|
+
};
|
|
341
412
|
}
|
|
342
|
-
|
|
343
|
-
|
|
413
|
+
return null;
|
|
414
|
+
};
|
|
415
|
+
|
|
416
|
+
// Prefer Host store: OpenCode session.get cannot see openchamber.goal.
|
|
417
|
+
if (typeof readSessionMetadata === 'function') {
|
|
418
|
+
try {
|
|
419
|
+
const metadata = await readSessionMetadata(sessionID);
|
|
420
|
+
const classified = classifyGoal(extractGoalFromSession({ metadata }));
|
|
421
|
+
if (classified) return classified;
|
|
422
|
+
} catch (error) {
|
|
423
|
+
if (signal?.aborted) throw error;
|
|
424
|
+
// Store read failure must not masquerade as empty goal / success.
|
|
425
|
+
return { outcome: 'busy' };
|
|
426
|
+
}
|
|
427
|
+
return { outcome: 'busy' };
|
|
344
428
|
}
|
|
345
|
-
return { outcome: 'busy' };
|
|
346
|
-
}
|
|
347
429
|
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
430
|
+
if (typeof client?.session?.get === 'function') {
|
|
431
|
+
try {
|
|
432
|
+
const sessionResult = await client.session.get({
|
|
433
|
+
sessionID,
|
|
434
|
+
directory: projectPath,
|
|
435
|
+
}, requestOptions);
|
|
436
|
+
if (!sessionResult?.error) {
|
|
437
|
+
const classified = classifyGoal(
|
|
438
|
+
extractGoalFromSession(sessionResult?.data ?? sessionResult),
|
|
439
|
+
);
|
|
440
|
+
if (classified) return classified;
|
|
358
441
|
}
|
|
442
|
+
} catch (error) {
|
|
443
|
+
if (signal?.aborted) throw error;
|
|
359
444
|
}
|
|
360
|
-
} catch (error) {
|
|
361
|
-
if (signal?.aborted) throw error;
|
|
362
445
|
}
|
|
446
|
+
return { outcome: 'busy' };
|
|
363
447
|
}
|
|
364
448
|
|
|
365
|
-
if (typeof client?.session?.
|
|
449
|
+
if (typeof client?.session?.active === 'function') {
|
|
366
450
|
try {
|
|
367
|
-
const
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
}
|
|
372
|
-
if (
|
|
373
|
-
|
|
374
|
-
if (lastInfo?.role === 'assistant') {
|
|
375
|
-
if (lastInfo.error) {
|
|
376
|
-
return {
|
|
377
|
-
outcome: 'error',
|
|
378
|
-
error: formatAssistantError(lastInfo.error),
|
|
379
|
-
};
|
|
380
|
-
}
|
|
381
|
-
if (lastInfo.time?.completed) {
|
|
382
|
-
return { outcome: 'success' };
|
|
383
|
-
}
|
|
384
|
-
}
|
|
451
|
+
const activeMap = await client.session.active(requestOptions);
|
|
452
|
+
if (!(activeMap && typeof activeMap === 'object' && !Array.isArray(activeMap))) {
|
|
453
|
+
// Malformed active payload is unknown — never idle-success.
|
|
454
|
+
return { outcome: 'busy' };
|
|
455
|
+
}
|
|
456
|
+
if (Object.prototype.hasOwnProperty.call(activeMap, sessionID)) {
|
|
457
|
+
return { outcome: 'busy' };
|
|
385
458
|
}
|
|
386
459
|
} catch (error) {
|
|
387
460
|
if (signal?.aborted) throw error;
|
|
461
|
+
// active failure = unknown; do not fall through to message.list success.
|
|
462
|
+
return { outcome: 'busy' };
|
|
388
463
|
}
|
|
389
464
|
}
|
|
390
465
|
|
|
466
|
+
try {
|
|
467
|
+
const messages = await listLatestMessages(client, sessionID, requestOptions);
|
|
468
|
+
if (!messages) {
|
|
469
|
+
return { outcome: 'busy' };
|
|
470
|
+
}
|
|
471
|
+
const classified = classifyMessageTail(messages);
|
|
472
|
+
if (classified.outcome === 'success' || classified.outcome === 'error') {
|
|
473
|
+
return classified;
|
|
474
|
+
}
|
|
475
|
+
} catch (error) {
|
|
476
|
+
if (signal?.aborted) throw error;
|
|
477
|
+
}
|
|
478
|
+
|
|
391
479
|
return { outcome: 'busy' };
|
|
392
480
|
};
|
|
393
481
|
|
|
@@ -428,6 +516,17 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
428
516
|
maxGlobalConcurrency = DEFAULT_GLOBAL_CONCURRENCY,
|
|
429
517
|
maxProjectConcurrency = DEFAULT_PROJECT_CONCURRENCY,
|
|
430
518
|
maxRunDurationMs = DEFAULT_MAX_RUN_MS,
|
|
519
|
+
/** Host store write: (sessionId, directory, goal) => Promise */
|
|
520
|
+
persistSessionGoal = null,
|
|
521
|
+
/** Host store read: (sessionId) => Promise<metadata|null> */
|
|
522
|
+
readSessionMetadata = null,
|
|
523
|
+
/**
|
|
524
|
+
* After a scheduled goal is first persisted, arm the session-goal loop.
|
|
525
|
+
* (session-goal runtime's own progress writes only broadcast — do not
|
|
526
|
+
* feed those back here.)
|
|
527
|
+
* @type {null | ((event: { sessionID: string, directory: string, metadata: object }) => void)}
|
|
528
|
+
*/
|
|
529
|
+
onGoalPersisted = null,
|
|
431
530
|
} = deps;
|
|
432
531
|
|
|
433
532
|
let started = false;
|
|
@@ -702,44 +801,43 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
702
801
|
+ '\n</system-reminder>';
|
|
703
802
|
};
|
|
704
803
|
|
|
705
|
-
const
|
|
804
|
+
const buildSessionCreateInput = (task, projectPath, title) => ({
|
|
805
|
+
title,
|
|
806
|
+
location: { directory: projectPath },
|
|
807
|
+
...(task.execution.agent ? { agent: task.execution.agent } : {}),
|
|
706
808
|
model: {
|
|
809
|
+
id: task.execution.modelID,
|
|
707
810
|
providerID: task.execution.providerID,
|
|
708
|
-
|
|
811
|
+
...(task.execution.variant ? { variant: task.execution.variant } : {}),
|
|
709
812
|
},
|
|
710
|
-
...(task.execution.agent ? { agent: task.execution.agent } : {}),
|
|
711
|
-
...(task.execution.variant ? { variant: task.execution.variant } : {}),
|
|
712
|
-
parts: [
|
|
713
|
-
{
|
|
714
|
-
type: 'text',
|
|
715
|
-
text: expandSnippets(task.execution.prompt, projectPath),
|
|
716
|
-
},
|
|
717
|
-
...(task.execution.goalEnabled
|
|
718
|
-
? [{ type: 'text', text: buildGoalIntroText(task.execution.goalTokenBudget), synthetic: true }]
|
|
719
|
-
: []),
|
|
720
|
-
],
|
|
721
813
|
});
|
|
722
814
|
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
815
|
+
const buildPromptText = (task, projectPath) => {
|
|
816
|
+
const promptText = expandSnippets(task.execution.prompt, projectPath);
|
|
817
|
+
if (!task.execution.goalEnabled) {
|
|
818
|
+
return promptText;
|
|
819
|
+
}
|
|
820
|
+
return `${promptText}\n${buildGoalIntroText(task.execution.goalTokenBudget)}`;
|
|
821
|
+
};
|
|
822
|
+
|
|
823
|
+
// Scheduled goal runs: write the file-backed objective, then persist a
|
|
824
|
+
// trackable goal record into the Host session-metadata store (same seam as
|
|
825
|
+
// manual UI goals). OpenCode SessionInfo has no openchamber.goal channel.
|
|
726
826
|
const createTaskGoal = async ({
|
|
727
|
-
baseUrl,
|
|
728
|
-
authHeaders,
|
|
729
827
|
sessionID,
|
|
730
828
|
projectPath,
|
|
731
829
|
task,
|
|
732
|
-
scheduledTaskMarker,
|
|
733
830
|
signal,
|
|
734
831
|
}) => {
|
|
735
832
|
signal?.throwIfAborted?.();
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
//
|
|
740
|
-
//
|
|
741
|
-
//
|
|
742
|
-
//
|
|
833
|
+
if (typeof persistSessionGoal !== 'function') {
|
|
834
|
+
throw new Error('session goal persist is not configured');
|
|
835
|
+
}
|
|
836
|
+
// File-backed objective keyed by session id: the full expanded prompt
|
|
837
|
+
// lives under the OpenChamber data dir. If the file write fails, fall
|
|
838
|
+
// back to an inline objective so parseGoalMetadata still accepts the
|
|
839
|
+
// record. Oversized prompts are distilled into audit criteria by the
|
|
840
|
+
// small model; on distill failure a head+tail excerpt keeps intent.
|
|
743
841
|
let objectiveText = expandSnippets(task.execution.prompt, projectPath);
|
|
744
842
|
if (objectiveText.length > 5000) {
|
|
745
843
|
let distilled = null;
|
|
@@ -788,21 +886,29 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
788
886
|
try {
|
|
789
887
|
const { writeObjective } = await import('../session-goal/objectives.js');
|
|
790
888
|
await writeObjective(sessionID, objectiveText);
|
|
791
|
-
signal?.throwIfAborted?.();
|
|
792
889
|
objectiveFile = true;
|
|
890
|
+
signal?.throwIfAborted?.();
|
|
793
891
|
} catch (error) {
|
|
794
892
|
if (signal?.aborted) {
|
|
795
893
|
throw error;
|
|
796
894
|
}
|
|
797
|
-
console.warn('[scheduled-tasks] goal objective file write failed,
|
|
895
|
+
console.warn('[scheduled-tasks] goal objective file write failed, continuing with inline objective:', error?.message || error);
|
|
798
896
|
}
|
|
799
897
|
signal?.throwIfAborted?.();
|
|
898
|
+
|
|
899
|
+
const budgetRaw = task.execution?.goalTokenBudget;
|
|
900
|
+
const tokenBudget = Number.isFinite(budgetRaw) && budgetRaw > 0
|
|
901
|
+
? Math.floor(budgetRaw)
|
|
902
|
+
: null;
|
|
903
|
+
const now = Date.now();
|
|
800
904
|
const goal = {
|
|
801
|
-
id:
|
|
802
|
-
objective: objectiveFile ? '' : objectiveText.slice(0, 5000),
|
|
803
|
-
objectiveFile,
|
|
905
|
+
id: crypto.randomUUID(),
|
|
804
906
|
status: 'active',
|
|
805
|
-
|
|
907
|
+
// File-backed: flag only (text lives under goals/<sessionId>.md).
|
|
908
|
+
// Inline fallback: full objective text when the file write failed.
|
|
909
|
+
objective: objectiveFile ? '' : objectiveText,
|
|
910
|
+
objectiveFile,
|
|
911
|
+
tokenBudget,
|
|
806
912
|
tokensUsed: 0,
|
|
807
913
|
turnsUsed: 0,
|
|
808
914
|
blockedStreak: 0,
|
|
@@ -812,181 +918,109 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
812
918
|
createdAt: now,
|
|
813
919
|
updatedAt: now,
|
|
814
920
|
};
|
|
815
|
-
const url = new URL(`${baseUrl}/session/${encodeURIComponent(sessionID)}`);
|
|
816
|
-
url.searchParams.set('directory', projectPath);
|
|
817
|
-
// Preserve the scheduledTask marker when goal metadata is patched; a full
|
|
818
|
-
// openchamber replace would otherwise drop ownership for history/UI.
|
|
819
|
-
const response = await fetch(url.toString(), {
|
|
820
|
-
method: 'PATCH',
|
|
821
|
-
headers: {
|
|
822
|
-
...authHeaders,
|
|
823
|
-
'content-type': 'application/json',
|
|
824
|
-
accept: 'application/json',
|
|
825
|
-
},
|
|
826
|
-
body: JSON.stringify({
|
|
827
|
-
metadata: {
|
|
828
|
-
openchamber: {
|
|
829
|
-
...(scheduledTaskMarker ? { scheduledTask: scheduledTaskMarker } : {}),
|
|
830
|
-
goal,
|
|
831
|
-
},
|
|
832
|
-
},
|
|
833
|
-
}),
|
|
834
|
-
...(signal ? { signal } : {}),
|
|
835
|
-
});
|
|
836
|
-
if (!response.ok) {
|
|
837
|
-
throw new Error(`goal metadata patch failed (${response.status})`);
|
|
838
|
-
}
|
|
839
|
-
};
|
|
840
921
|
|
|
841
|
-
|
|
842
|
-
const
|
|
843
|
-
|
|
844
|
-
|
|
845
|
-
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
922
|
+
// Persist failure must throw — never prompt as if the goal was registered.
|
|
923
|
+
const persisted = await persistSessionGoal(sessionID, projectPath, goal);
|
|
924
|
+
signal?.throwIfAborted?.();
|
|
925
|
+
|
|
926
|
+
if (typeof onGoalPersisted === 'function') {
|
|
927
|
+
try {
|
|
928
|
+
// Prefer full metadata when the seam returns the store object; otherwise
|
|
929
|
+
// synthesize the openchamber.goal shape the session-goal loop expects.
|
|
930
|
+
const metadata = persisted
|
|
931
|
+
&& typeof persisted === 'object'
|
|
932
|
+
&& !Array.isArray(persisted)
|
|
933
|
+
&& persisted.openchamber
|
|
934
|
+
? persisted
|
|
935
|
+
: { openchamber: { goal } };
|
|
936
|
+
onGoalPersisted({
|
|
937
|
+
sessionID,
|
|
938
|
+
directory: projectPath,
|
|
939
|
+
metadata,
|
|
940
|
+
});
|
|
941
|
+
} catch (error) {
|
|
942
|
+
console.warn('[scheduled-tasks] onGoalPersisted failed:', error?.message || error);
|
|
943
|
+
}
|
|
858
944
|
}
|
|
859
945
|
};
|
|
860
946
|
|
|
861
|
-
const
|
|
947
|
+
const runSessionPrompt = async ({ client, sessionID, projectPath, task, signal }) => {
|
|
862
948
|
signal?.throwIfAborted?.();
|
|
863
|
-
const
|
|
864
|
-
|
|
865
|
-
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
...authHeaders,
|
|
869
|
-
'content-type': 'application/json',
|
|
870
|
-
accept: 'application/json',
|
|
871
|
-
},
|
|
872
|
-
body: JSON.stringify(buildPromptAsyncPayload(task, projectPath)),
|
|
873
|
-
...(signal ? { signal } : {}),
|
|
874
|
-
});
|
|
875
|
-
|
|
876
|
-
if (!response.ok) {
|
|
877
|
-
const body = await response.text().catch(() => '');
|
|
878
|
-
throw new Error(`prompt_async failed (${response.status})${body ? `: ${body}` : ''}`);
|
|
879
|
-
}
|
|
949
|
+
const requestOptions = signal ? { signal } : undefined;
|
|
950
|
+
await client.session.prompt({
|
|
951
|
+
sessionID,
|
|
952
|
+
text: buildPromptText(task, projectPath),
|
|
953
|
+
}, requestOptions);
|
|
880
954
|
};
|
|
881
955
|
|
|
882
956
|
/**
|
|
883
|
-
*
|
|
884
|
-
* agent finishes. History must reflect the real session
|
|
885
|
-
* wall-clock duration — poll
|
|
957
|
+
* session.prompt / command return when the turn is *admitted*, not when the
|
|
958
|
+
* agent finishes. History must reflect the real session outcome and
|
|
959
|
+
* wall-clock duration — poll until idle+settled on a latest-side message
|
|
960
|
+
* page (order=desc). finish tool-calls and incomplete assistants never
|
|
961
|
+
* succeed; two incomplete idle probes must not invent success.
|
|
962
|
+
* session.active / message.list failures stay unknown — never idle or empty
|
|
963
|
+
* success. Goal-enabled runs that passed admission still settle on the
|
|
964
|
+
* assistant tail here when Host goal state is unavailable.
|
|
886
965
|
*/
|
|
887
966
|
const waitForRunOutcome = async ({
|
|
888
967
|
client,
|
|
889
968
|
sessionID,
|
|
890
|
-
projectPath,
|
|
891
|
-
goalEnabled,
|
|
892
969
|
signal,
|
|
893
970
|
}) => {
|
|
894
971
|
const requestOptions = signal ? { signal } : undefined;
|
|
895
|
-
let incompleteAssistantProbes = 0;
|
|
896
972
|
let emptyIdleProbes = 0;
|
|
897
973
|
|
|
898
974
|
for (;;) {
|
|
899
975
|
signal?.throwIfAborted?.();
|
|
900
976
|
|
|
901
|
-
|
|
977
|
+
let sessionBusy = null;
|
|
978
|
+
if (typeof client?.session?.active === 'function') {
|
|
902
979
|
try {
|
|
903
|
-
const
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
}, requestOptions);
|
|
907
|
-
if (!sessionResult?.error) {
|
|
908
|
-
const goal = extractGoalFromSession(sessionResult?.data);
|
|
909
|
-
if (goal && GOAL_TERMINAL_STATUSES.has(goal.status)) {
|
|
910
|
-
if (goal.status === 'complete') {
|
|
911
|
-
return { outcome: 'success' };
|
|
912
|
-
}
|
|
913
|
-
return {
|
|
914
|
-
outcome: 'error',
|
|
915
|
-
error: goal.note || `goal ${goal.status}`,
|
|
916
|
-
};
|
|
917
|
-
}
|
|
980
|
+
const activeMap = await client.session.active(requestOptions);
|
|
981
|
+
if (activeMap && typeof activeMap === 'object' && !Array.isArray(activeMap)) {
|
|
982
|
+
sessionBusy = Object.prototype.hasOwnProperty.call(activeMap, sessionID);
|
|
918
983
|
}
|
|
984
|
+
// Malformed / thrown active stays null → unknown, keep polling.
|
|
919
985
|
} catch (error) {
|
|
920
986
|
if (signal?.aborted) throw error;
|
|
921
|
-
// Transient get failures: keep polling until watchdog aborts.
|
|
922
987
|
}
|
|
923
988
|
}
|
|
924
989
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
const statusResult = await client.session.status({
|
|
929
|
-
directory: projectPath,
|
|
930
|
-
}, requestOptions);
|
|
931
|
-
if (!statusResult?.error && statusResult?.data && typeof statusResult.data === 'object') {
|
|
932
|
-
const statusValue = statusResult.data[sessionID];
|
|
933
|
-
const type = statusValue?.type ?? statusValue?.status;
|
|
934
|
-
sessionBusy = type === 'busy' || type === 'retry';
|
|
935
|
-
}
|
|
936
|
-
} catch (error) {
|
|
937
|
-
if (signal?.aborted) throw error;
|
|
990
|
+
if (sessionBusy !== false) {
|
|
991
|
+
if (sessionBusy === true) {
|
|
992
|
+
emptyIdleProbes = 0;
|
|
938
993
|
}
|
|
939
|
-
}
|
|
940
|
-
|
|
941
|
-
if (sessionBusy) {
|
|
942
|
-
incompleteAssistantProbes = 0;
|
|
943
|
-
emptyIdleProbes = 0;
|
|
944
994
|
await sleep(SESSION_SETTLEMENT_POLL_MS, signal);
|
|
945
995
|
continue;
|
|
946
996
|
}
|
|
947
997
|
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
if (
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
error: formatAssistantError(lastInfo.error),
|
|
965
|
-
};
|
|
966
|
-
}
|
|
967
|
-
if (lastInfo.time?.completed) {
|
|
968
|
-
return { outcome: 'success' };
|
|
969
|
-
}
|
|
970
|
-
incompleteAssistantProbes += 1;
|
|
971
|
-
if (incompleteAssistantProbes >= INCOMPLETE_ASSISTANT_SETTLE_PROBES) {
|
|
972
|
-
return { outcome: 'success' };
|
|
973
|
-
}
|
|
974
|
-
} else {
|
|
975
|
-
incompleteAssistantProbes = 0;
|
|
976
|
-
emptyIdleProbes += 1;
|
|
977
|
-
// Prompt admitted but no assistant yet, or aborted before reply.
|
|
978
|
-
// Allow several idle probes so we don't race the first token.
|
|
979
|
-
if (emptyIdleProbes >= 5 && lastInfo?.role === 'user') {
|
|
980
|
-
return {
|
|
981
|
-
outcome: 'error',
|
|
982
|
-
error: 'session ended without assistant response',
|
|
983
|
-
};
|
|
984
|
-
}
|
|
998
|
+
try {
|
|
999
|
+
const messages = await listLatestMessages(client, sessionID, requestOptions);
|
|
1000
|
+
if (messages) {
|
|
1001
|
+
const classified = classifyMessageTail(messages);
|
|
1002
|
+
if (classified.outcome === 'success' || classified.outcome === 'error') {
|
|
1003
|
+
return classified;
|
|
1004
|
+
}
|
|
1005
|
+
if (classified.outcome === 'awaiting_assistant') {
|
|
1006
|
+
emptyIdleProbes += 1;
|
|
1007
|
+
// Prompt admitted but no assistant yet, or aborted before reply.
|
|
1008
|
+
// Allow several idle probes so we don't race the first token.
|
|
1009
|
+
if (emptyIdleProbes >= 5) {
|
|
1010
|
+
return {
|
|
1011
|
+
outcome: 'error',
|
|
1012
|
+
error: 'session ended without assistant response',
|
|
1013
|
+
};
|
|
985
1014
|
}
|
|
1015
|
+
} else {
|
|
1016
|
+
// busy / unknown tails (tool-calls, incomplete, model-switch, …)
|
|
1017
|
+
// keep polling — never promote incomplete idle to success.
|
|
1018
|
+
emptyIdleProbes = 0;
|
|
986
1019
|
}
|
|
987
|
-
} catch (error) {
|
|
988
|
-
if (signal?.aborted) throw error;
|
|
989
1020
|
}
|
|
1021
|
+
} catch (error) {
|
|
1022
|
+
if (signal?.aborted) throw error;
|
|
1023
|
+
// message.list failure stays unknown — keep polling under the watchdog.
|
|
990
1024
|
}
|
|
991
1025
|
|
|
992
1026
|
await sleep(SESSION_SETTLEMENT_POLL_MS, signal);
|
|
@@ -1003,7 +1037,9 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1003
1037
|
const requestOptions = signal ? { signal } : undefined;
|
|
1004
1038
|
let commands = [];
|
|
1005
1039
|
try {
|
|
1006
|
-
const response = await client.command.list({
|
|
1040
|
+
const response = await client.command.list({
|
|
1041
|
+
location: { directory: projectPath },
|
|
1042
|
+
}, requestOptions);
|
|
1007
1043
|
commands = Array.isArray(response?.data) ? response.data : [];
|
|
1008
1044
|
} catch (error) {
|
|
1009
1045
|
if (signal?.aborted) {
|
|
@@ -1020,28 +1056,30 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1020
1056
|
|
|
1021
1057
|
await client.session.command({
|
|
1022
1058
|
sessionID,
|
|
1023
|
-
directory: projectPath,
|
|
1024
1059
|
command: parsed.command,
|
|
1025
1060
|
arguments: parsed.arguments,
|
|
1026
1061
|
...(task.execution.agent ? { agent: task.execution.agent } : {}),
|
|
1027
|
-
model:
|
|
1028
|
-
|
|
1062
|
+
model: {
|
|
1063
|
+
id: task.execution.modelID,
|
|
1064
|
+
providerID: task.execution.providerID,
|
|
1065
|
+
...(task.execution.variant ? { variant: task.execution.variant } : {}),
|
|
1066
|
+
},
|
|
1029
1067
|
}, requestOptions);
|
|
1030
1068
|
|
|
1031
1069
|
return true;
|
|
1032
1070
|
};
|
|
1033
1071
|
|
|
1034
|
-
const
|
|
1035
|
-
if (!sessionID ||
|
|
1072
|
+
const interruptCreatedSessionBestEffort = async ({ client, sessionID }) => {
|
|
1073
|
+
if (!sessionID || typeof client?.session?.interrupt !== 'function') {
|
|
1036
1074
|
return;
|
|
1037
1075
|
}
|
|
1038
1076
|
try {
|
|
1039
|
-
await client.session.
|
|
1077
|
+
await client.session.interrupt({
|
|
1040
1078
|
sessionID,
|
|
1041
|
-
|
|
1079
|
+
continue: false,
|
|
1042
1080
|
});
|
|
1043
1081
|
} catch {
|
|
1044
|
-
// Never let upstream
|
|
1082
|
+
// Never let upstream interrupt failure replace the watchdog timeout error.
|
|
1045
1083
|
}
|
|
1046
1084
|
};
|
|
1047
1085
|
|
|
@@ -1055,6 +1093,20 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1055
1093
|
|
|
1056
1094
|
signal?.throwIfAborted?.();
|
|
1057
1095
|
|
|
1096
|
+
// Goal-enabled tasks: refuse before session create / objective write /
|
|
1097
|
+
// prompt when Host goal state is unavailable OR persist is not wired.
|
|
1098
|
+
// Config + run-history stay; only this run is recorded as error (no
|
|
1099
|
+
// false first-turn success / no create·distill·prompt side effects).
|
|
1100
|
+
if (task.execution?.goalEnabled) {
|
|
1101
|
+
const capability = getSessionGoalCapability();
|
|
1102
|
+
if (!capability.supported) {
|
|
1103
|
+
throw new Error(sessionGoalUnavailableMessage(capability));
|
|
1104
|
+
}
|
|
1105
|
+
if (typeof persistSessionGoal !== 'function') {
|
|
1106
|
+
throw new Error('session goal persist is not configured');
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1058
1110
|
if (typeof waitForOpenCodeReady === 'function') {
|
|
1059
1111
|
await waitForOpenCodeReady(10_000, 250);
|
|
1060
1112
|
signal?.throwIfAborted?.();
|
|
@@ -1062,26 +1114,16 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1062
1114
|
|
|
1063
1115
|
const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
|
|
1064
1116
|
const authHeaders = getOpenCodeAuthHeaders();
|
|
1065
|
-
const client =
|
|
1117
|
+
const client = makeOpenCodeV2Client({
|
|
1066
1118
|
baseUrl,
|
|
1067
|
-
|
|
1119
|
+
authHeaders,
|
|
1068
1120
|
});
|
|
1069
1121
|
const requestOptions = signal ? { signal } : undefined;
|
|
1070
1122
|
|
|
1071
|
-
const taskName = typeof task?.name === 'string' && task.name.trim().length > 0
|
|
1072
|
-
? task.name.trim()
|
|
1073
|
-
: 'Schedule';
|
|
1074
|
-
const scheduledTaskMarker = {
|
|
1075
|
-
projectID,
|
|
1076
|
-
taskID: task.id,
|
|
1077
|
-
runID,
|
|
1078
|
-
name: taskName,
|
|
1079
|
-
};
|
|
1080
|
-
|
|
1081
1123
|
let sessionID;
|
|
1082
|
-
// Watchdog abort must fire session.
|
|
1083
|
-
// work (small-model distill / writeObjective). Register
|
|
1084
|
-
// never on ordinary non-timeout failures or success.
|
|
1124
|
+
// Watchdog abort must fire session.interrupt even while awaiting
|
|
1125
|
+
// non-cancellable work (small-model distill / writeObjective). Register
|
|
1126
|
+
// once after create; never on ordinary non-timeout failures or success.
|
|
1085
1127
|
let sessionAbortStarted = false;
|
|
1086
1128
|
let onWatchdogAbort = null;
|
|
1087
1129
|
const triggerSessionAbortOnce = () => {
|
|
@@ -1089,7 +1131,7 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1089
1131
|
return;
|
|
1090
1132
|
}
|
|
1091
1133
|
sessionAbortStarted = true;
|
|
1092
|
-
void
|
|
1134
|
+
void interruptCreatedSessionBestEffort({ client, sessionID });
|
|
1093
1135
|
};
|
|
1094
1136
|
const clearWatchdogAbortListener = () => {
|
|
1095
1137
|
if (onWatchdogAbort && signal) {
|
|
@@ -1099,12 +1141,11 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1099
1141
|
};
|
|
1100
1142
|
|
|
1101
1143
|
try {
|
|
1102
|
-
const
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
sessionID = sessionResponse?.data?.id;
|
|
1144
|
+
const session = await client.session.create(
|
|
1145
|
+
buildSessionCreateInput(task, projectPath, title),
|
|
1146
|
+
requestOptions,
|
|
1147
|
+
);
|
|
1148
|
+
sessionID = session?.id;
|
|
1108
1149
|
if (!sessionID) {
|
|
1109
1150
|
throw new Error('failed to create session');
|
|
1110
1151
|
}
|
|
@@ -1130,19 +1171,6 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1130
1171
|
}
|
|
1131
1172
|
}
|
|
1132
1173
|
|
|
1133
|
-
try {
|
|
1134
|
-
// Archive must succeed before any goal/prompt/command work. A 404 gets
|
|
1135
|
-
// one short bounded retry; any other failure aborts without prompting.
|
|
1136
|
-
await archiveSessionBeforePrompt({ client, sessionID, projectPath, signal });
|
|
1137
|
-
} catch (archiveError) {
|
|
1138
|
-
if (attachFailed) {
|
|
1139
|
-
throw new Error(
|
|
1140
|
-
`attachSession failed (${safeErrorMessage(attachFailed)}); archive also failed: ${safeErrorMessage(archiveError)}`,
|
|
1141
|
-
);
|
|
1142
|
-
}
|
|
1143
|
-
throw archiveError;
|
|
1144
|
-
}
|
|
1145
|
-
|
|
1146
1174
|
if (attachFailed) {
|
|
1147
1175
|
throw new Error(`attachSession failed: ${safeErrorMessage(attachFailed)}`);
|
|
1148
1176
|
}
|
|
@@ -1162,12 +1190,9 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1162
1190
|
|
|
1163
1191
|
if (task.execution.goalEnabled) {
|
|
1164
1192
|
await createTaskGoal({
|
|
1165
|
-
baseUrl,
|
|
1166
|
-
authHeaders,
|
|
1167
1193
|
sessionID,
|
|
1168
1194
|
projectPath,
|
|
1169
1195
|
task,
|
|
1170
|
-
scheduledTaskMarker,
|
|
1171
1196
|
signal,
|
|
1172
1197
|
});
|
|
1173
1198
|
}
|
|
@@ -1182,9 +1207,8 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1182
1207
|
signal,
|
|
1183
1208
|
});
|
|
1184
1209
|
if (!executedAsCommand) {
|
|
1185
|
-
await
|
|
1186
|
-
|
|
1187
|
-
authHeaders,
|
|
1210
|
+
await runSessionPrompt({
|
|
1211
|
+
client,
|
|
1188
1212
|
sessionID,
|
|
1189
1213
|
projectPath,
|
|
1190
1214
|
task,
|
|
@@ -1192,13 +1216,11 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1192
1216
|
});
|
|
1193
1217
|
}
|
|
1194
1218
|
|
|
1195
|
-
// Do not finalize on admission alone — wait for the real agent turn
|
|
1196
|
-
//
|
|
1219
|
+
// Do not finalize on admission alone — wait for the real agent turn so
|
|
1220
|
+
// history status/duration match what the user sees in chat.
|
|
1197
1221
|
const settlement = await waitForRunOutcome({
|
|
1198
1222
|
client,
|
|
1199
1223
|
sessionID,
|
|
1200
|
-
projectPath,
|
|
1201
|
-
goalEnabled: Boolean(task.execution?.goalEnabled),
|
|
1202
1224
|
signal,
|
|
1203
1225
|
});
|
|
1204
1226
|
|
|
@@ -1369,8 +1391,8 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1369
1391
|
|
|
1370
1392
|
if (raced.timedOut) {
|
|
1371
1393
|
// Bound: do not await uncancellable steps (distill / writeObjective).
|
|
1372
|
-
// session.
|
|
1373
|
-
// throwIfAborted gates stop goal
|
|
1394
|
+
// session.interrupt is started by the signal listener on create;
|
|
1395
|
+
// later throwIfAborted gates stop goal write / command / prompt.
|
|
1374
1396
|
throw timeoutError;
|
|
1375
1397
|
}
|
|
1376
1398
|
|
|
@@ -1701,15 +1723,16 @@ export const createScheduledTasksRuntime = (deps) => {
|
|
|
1701
1723
|
|
|
1702
1724
|
const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
|
|
1703
1725
|
const authHeaders = getOpenCodeAuthHeaders();
|
|
1704
|
-
const client =
|
|
1726
|
+
const client = makeOpenCodeV2Client({
|
|
1705
1727
|
baseUrl,
|
|
1706
|
-
|
|
1728
|
+
authHeaders,
|
|
1707
1729
|
});
|
|
1708
1730
|
const snapshot = await snapshotSessionOutcome({
|
|
1709
1731
|
client,
|
|
1710
1732
|
sessionID,
|
|
1711
1733
|
projectPath,
|
|
1712
1734
|
goalEnabled: Boolean(task.execution?.goalEnabled),
|
|
1735
|
+
readSessionMetadata,
|
|
1713
1736
|
});
|
|
1714
1737
|
if (snapshot.outcome !== 'success') {
|
|
1715
1738
|
return;
|