@openchambery/web 1.19.21-beta.1 → 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-Bh06xinl.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-f2agCc4q.js → MarkdownRendererImpl-SKi4oG0c.js} +1 -1
- package/dist/assets/{MarkstreamRendererImpl-ChD4WD2v.js → MarkstreamRendererImpl-CnkodGJW.js} +1 -1
- package/dist/assets/MobileDetailNavigation-Bnx59U_J.js +1 -0
- package/dist/assets/{MobileShareBridge-D7SG0ueZ.js → MobileShareBridge-BQlP4J-F.js} +1 -1
- package/dist/assets/MobileSurface-BXLeiZ4d.js +2 -0
- package/dist/assets/{MultiRunWindow-CjCw1X4n.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-C2DgbXJ3.js → SettingsWindow-DfnsFa8y.js} +1 -1
- package/dist/assets/{TerminalView-lubDPKMs.js → TerminalView-Dh9MWCpS.js} +1 -1
- package/dist/assets/{ToolOutputDialog-CzgW3-Cb.js → ToolOutputDialog-BejmjrRk.js} +1 -1
- package/dist/assets/{appThemeRegistry-BKc-qXbQ.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-BTkP7aYI.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-BvSG94mW.js +4 -0
- package/dist/assets/pl-CnaKWN5F.js +15 -0
- package/dist/assets/platform-CWTIp02q.js +4 -0
- package/dist/assets/projectMeta-DOe49dM_.js +3 -0
- 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-PpnZiwcW.js → runtimeConfig-BMvqa5we.js} +1 -1
- package/dist/assets/{runtimeEndpointReset-BGjzNuQl.js → runtimeEndpointReset-B6BxhvR0.js} +6 -6
- package/dist/assets/sessionChangeSummary-VVBlEXuh.js +2 -0
- package/dist/assets/{sessionLookup-CbbzalcH.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-BzX5UI-c.css +2 -0
- 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-3XR8sUfy.js +1 -0
- package/dist/assets/{tooltip-Djn7MSOX.js → tooltip-CQvpK-UV.js} +1 -1
- package/dist/assets/uk-CaX0iqkB.js +15 -0
- package/dist/assets/{useAppFontEffects-BJBcy-84.js → useAppFontEffects-Cpmpo7Wb.js} +45 -45
- package/dist/assets/useDesktopWindowControlsLayout-B1GX59-L.js +1 -0
- package/dist/assets/{useEffectiveDirectory-CzHynoi_.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-DzV9N6G-.js → useSessionAutoCleanup-BLjymaFt.js} +1 -1
- package/dist/assets/{useWorkerHighlightedLines-Bi9ASKr_.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-BtGYjSW5.js +0 -3
- package/dist/assets/MarkdownRenderer-AwDKOCvD.js +0 -2
- package/dist/assets/MobileDetailNavigation-B6fGRYsg.js +0 -1
- package/dist/assets/MobileSurface-BY7eErHN.js +0 -2
- package/dist/assets/SettingsView-lcRi35Y3.js +0 -25
- package/dist/assets/es-rqMiRP7-.js +0 -15
- package/dist/assets/gitApi-CcIuekQ3.js +0 -7
- package/dist/assets/insertionBoundaries-BocNNP9Q.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-CFLmpNC5.js +0 -169
- package/dist/assets/main-Ckj_IpUy.js +0 -2
- package/dist/assets/miniChat-Dq1CTKNs.js +0 -2
- package/dist/assets/mobile-DlJ0Yr7h.js +0 -2
- package/dist/assets/multirun-BGUKIJys.js +0 -4
- package/dist/assets/pl-DW2XJGC4.js +0 -15
- package/dist/assets/platform-0w_Q9_ji.js +0 -2
- package/dist/assets/projectMeta-Bft2t6GF.js +0 -3
- package/dist/assets/pt-BR-CIsanf4T.js +0 -15
- package/dist/assets/renderElectronMiniChatApp-CTbXbB0Y.js +0 -2
- package/dist/assets/renderMobileApp-DLewUqoE.js +0 -28
- package/dist/assets/sessionChangeSummary-DiqwX3vZ.js +0 -2
- package/dist/assets/src-rS3zCcsC.css +0 -2
- package/dist/assets/textarea-DpK2Z0Gp.js +0 -1
- package/dist/assets/uk-BPmPEUYJ.js +0 -15
- package/dist/assets/useDesktopWindowControlsLayout-3Bm0rXCQ.js +0 -1
- package/dist/assets/useMobileNavigationStore-BIyCYi_z.js +0 -413
- package/dist/assets/vendor-opencode-sdk-C07HQypa.js +0 -7
- package/dist/assets/zh-CN-R8-mDcIp.js +0 -15
|
@@ -21,6 +21,7 @@ export const createServerUtilsRuntime = (dependencies) => {
|
|
|
21
21
|
setOpenCodeNotReadySince,
|
|
22
22
|
clearLastOpenCodeError,
|
|
23
23
|
getLoginShellPath,
|
|
24
|
+
getStoredSessionMetadata = null,
|
|
24
25
|
} = dependencies;
|
|
25
26
|
|
|
26
27
|
const setOpenCodePort = (port) => {
|
|
@@ -215,6 +216,7 @@ export const createServerUtilsRuntime = (dependencies) => {
|
|
|
215
216
|
getUiNotificationClients,
|
|
216
217
|
onInteractiveSessionRequest: options.onInteractiveSessionRequest,
|
|
217
218
|
onSessionTurnAdmission: options.onSessionTurnAdmission,
|
|
219
|
+
getStoredSessionMetadata,
|
|
218
220
|
});
|
|
219
221
|
};
|
|
220
222
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { OpenCode } from '@opencode/client';
|
|
2
2
|
|
|
3
3
|
const summarizeDescription = (value) => {
|
|
4
4
|
const normalized = typeof value === 'string' ? value.replace(/\s+/gu, ' ').trim() : '';
|
|
@@ -127,19 +127,18 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
127
127
|
}
|
|
128
128
|
|
|
129
129
|
try {
|
|
130
|
-
const client =
|
|
130
|
+
const client = OpenCode.make({
|
|
131
131
|
baseUrl: buildOpenCodeUrl('/', '').replace(/\/$/, ''),
|
|
132
|
-
directory: workingDirectory || undefined,
|
|
133
132
|
headers: getOpenCodeAuthHeaders(),
|
|
134
133
|
fetch: (request) => fetch(request, { signal: AbortSignal.timeout(8_000) }),
|
|
135
134
|
});
|
|
136
135
|
|
|
137
|
-
const response = await client.
|
|
138
|
-
workingDirectory ? { directory: workingDirectory } : undefined,
|
|
136
|
+
const response = await client.skill.list(
|
|
137
|
+
workingDirectory ? { location: { directory: workingDirectory } } : undefined,
|
|
139
138
|
);
|
|
140
139
|
const payload = response?.data;
|
|
141
140
|
if (!Array.isArray(payload)) {
|
|
142
|
-
|
|
141
|
+
throw new Error('OpenCode skill catalog is unavailable');
|
|
143
142
|
}
|
|
144
143
|
|
|
145
144
|
return payload
|
|
@@ -177,7 +176,7 @@ export const registerSkillRoutes = (app, dependencies) => {
|
|
|
177
176
|
.filter(Boolean);
|
|
178
177
|
} catch (error) {
|
|
179
178
|
console.error('Failed to list OpenCode skills:', error);
|
|
180
|
-
|
|
179
|
+
throw error;
|
|
181
180
|
}
|
|
182
181
|
};
|
|
183
182
|
|
|
@@ -2,16 +2,16 @@ import { describe, expect, it, vi } from 'vitest';
|
|
|
2
2
|
import express from 'express';
|
|
3
3
|
import request from 'supertest';
|
|
4
4
|
|
|
5
|
-
vi.mock('@opencode
|
|
5
|
+
vi.mock('@opencode/client', () => ({ OpenCode: { make: vi.fn() } }));
|
|
6
6
|
|
|
7
|
-
const {
|
|
7
|
+
const { OpenCode } = await import('@opencode/client');
|
|
8
8
|
const { registerSkillRoutes } = await import('./skill-routes.js');
|
|
9
9
|
|
|
10
10
|
describe('skill summary route', () => {
|
|
11
11
|
it('returns compact normalized skill data without content or sources', async () => {
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
12
|
+
OpenCode.make.mockReturnValue({
|
|
13
|
+
skill: {
|
|
14
|
+
list: vi.fn(async () => ({ data: [{
|
|
15
15
|
name: 'skill',
|
|
16
16
|
location: '/repo/.opencode/skills/skill/SKILL.md',
|
|
17
17
|
description: ` ${'😀'.repeat(161)}\nnext `,
|
|
@@ -47,5 +47,36 @@ describe('skill summary route', () => {
|
|
|
47
47
|
});
|
|
48
48
|
expect(response.body.skills[0]).not.toHaveProperty('content');
|
|
49
49
|
expect(response.body.skills[0]).not.toHaveProperty('sources');
|
|
50
|
+
expect(OpenCode.make).toHaveBeenCalledWith(expect.objectContaining({
|
|
51
|
+
baseUrl: 'http://opencode-upstream:4096',
|
|
52
|
+
headers: { Authorization: 'Basic example' },
|
|
53
|
+
}));
|
|
54
|
+
expect(OpenCode.make.mock.calls[0][0].directory).toBeUndefined();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
it('returns 500 when OpenCode skill list fails instead of an empty success', async () => {
|
|
58
|
+
OpenCode.make.mockReturnValue({
|
|
59
|
+
skill: {
|
|
60
|
+
list: vi.fn(async () => { throw new Error('upstream sentinel'); }),
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
const app = express();
|
|
64
|
+
registerSkillRoutes(app, {
|
|
65
|
+
fs: { existsSync: () => false },
|
|
66
|
+
path: await import('node:path'),
|
|
67
|
+
os: await import('node:os'),
|
|
68
|
+
resolveProjectDirectory: async () => ({ directory: '/repo' }),
|
|
69
|
+
resolveOptionalProjectDirectory: async () => ({ directory: '/repo' }),
|
|
70
|
+
buildOpenCodeUrl: () => 'http://opencode-upstream:4096/',
|
|
71
|
+
getOpenCodeAuthHeaders: () => ({ Authorization: 'Basic example' }),
|
|
72
|
+
getOpenCodePort: () => 4096,
|
|
73
|
+
SKILL_SCOPE: { PROJECT: 'project', USER: 'user' },
|
|
74
|
+
discoverSkills: () => [],
|
|
75
|
+
mergeDiscoveredSkills: (primary) => primary,
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
const response = await request(app).get('/api/config/skills?summary=true&directory=%2Frepo').expect(500);
|
|
79
|
+
expect(JSON.stringify(response.body)).not.toContain('sentinel');
|
|
80
|
+
expect(response.body).toEqual({ error: 'Failed to list skills' });
|
|
50
81
|
});
|
|
51
82
|
});
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
export const OPENCODE_V1_MIGRATION_PATH = '/api/experimental/migration/v1';
|
|
2
|
+
|
|
3
|
+
// Backfill notes must stay readable by UI and docs. Do not drop them when the
|
|
4
|
+
// gate blocks or later admits a migrated library.
|
|
5
|
+
export const V1_MIGRATION_USER_NOTICE = [
|
|
6
|
+
'V1 history is backfilled in the opencode2 process.',
|
|
7
|
+
'Message ids are reused.',
|
|
8
|
+
'In-progress tools become interrupted.',
|
|
9
|
+
'V1 subtasks do not appear in v2.',
|
|
10
|
+
].join(' ');
|
|
11
|
+
|
|
12
|
+
const MIGRATION_PHASES = new Set(['required', 'running', 'completed', 'error']);
|
|
13
|
+
|
|
14
|
+
const stringifyError = (error) => {
|
|
15
|
+
if (error instanceof Error && error.message) {
|
|
16
|
+
return error.message;
|
|
17
|
+
}
|
|
18
|
+
if (typeof error === 'string' && error.trim()) {
|
|
19
|
+
return error;
|
|
20
|
+
}
|
|
21
|
+
return 'V1 migration status request failed';
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const blocked = (phase, extra = {}) => ({
|
|
25
|
+
admitTranscript: false,
|
|
26
|
+
phase,
|
|
27
|
+
userNotice: V1_MIGRATION_USER_NOTICE,
|
|
28
|
+
...extra,
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
const admitted = (phase, extra = {}) => ({
|
|
32
|
+
admitTranscript: true,
|
|
33
|
+
phase,
|
|
34
|
+
...extra,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
const normalizeProgress = (progress) => {
|
|
38
|
+
if (!progress || typeof progress !== 'object' || Array.isArray(progress)) {
|
|
39
|
+
return undefined;
|
|
40
|
+
}
|
|
41
|
+
const result = {
|
|
42
|
+
label: typeof progress.label === 'string' ? progress.label : '',
|
|
43
|
+
};
|
|
44
|
+
if (Number.isFinite(progress.numerator) && progress.numerator >= 0) {
|
|
45
|
+
result.numerator = progress.numerator;
|
|
46
|
+
}
|
|
47
|
+
if (Number.isFinite(progress.denominator) && progress.denominator >= 0) {
|
|
48
|
+
result.denominator = progress.denominator;
|
|
49
|
+
}
|
|
50
|
+
return result;
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const resolveHttpStatus = (input) => {
|
|
54
|
+
if (typeof input.httpStatus === 'number') {
|
|
55
|
+
return input.httpStatus;
|
|
56
|
+
}
|
|
57
|
+
if (typeof input.status === 'number') {
|
|
58
|
+
return input.status;
|
|
59
|
+
}
|
|
60
|
+
return undefined;
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const resolveBody = (input) => {
|
|
64
|
+
if (input.body !== undefined) {
|
|
65
|
+
return input.body;
|
|
66
|
+
}
|
|
67
|
+
if (typeof input.status === 'string') {
|
|
68
|
+
return input;
|
|
69
|
+
}
|
|
70
|
+
return null;
|
|
71
|
+
};
|
|
72
|
+
|
|
73
|
+
const resolveTransportError = (input) => {
|
|
74
|
+
if (input instanceof Error || typeof input === 'string') {
|
|
75
|
+
return input;
|
|
76
|
+
}
|
|
77
|
+
if (input?.error instanceof Error || typeof input?.error === 'string') {
|
|
78
|
+
if (input.body === undefined && typeof input.status !== 'string') {
|
|
79
|
+
return input.error;
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
return null;
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
// Pure admission decision for GET /api/experimental/migration/v1.
|
|
86
|
+
// required/running/error never become an empty-list success.
|
|
87
|
+
export const evaluateV1MigrationGate = (input = {}) => {
|
|
88
|
+
if (input == null) {
|
|
89
|
+
return blocked('error', { error: 'V1 migration status is missing or invalid' });
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const transportError = resolveTransportError(input);
|
|
93
|
+
if (transportError) {
|
|
94
|
+
return blocked('error', { error: stringifyError(transportError) });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const httpStatus = resolveHttpStatus(input);
|
|
98
|
+
if (httpStatus === 404) {
|
|
99
|
+
return admitted('absent');
|
|
100
|
+
}
|
|
101
|
+
if (Number.isInteger(httpStatus) && httpStatus >= 400) {
|
|
102
|
+
return blocked('error', { error: `V1 migration status HTTP ${httpStatus}` });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const body = resolveBody(input);
|
|
106
|
+
const phase = body && typeof body === 'object' ? body.status : undefined;
|
|
107
|
+
|
|
108
|
+
if (phase === 'required') {
|
|
109
|
+
return blocked('required');
|
|
110
|
+
}
|
|
111
|
+
if (phase === 'running') {
|
|
112
|
+
return blocked('running', { progress: normalizeProgress(body.progress) });
|
|
113
|
+
}
|
|
114
|
+
if (phase === 'completed') {
|
|
115
|
+
return admitted('completed', { userNotice: V1_MIGRATION_USER_NOTICE });
|
|
116
|
+
}
|
|
117
|
+
if (phase === 'error') {
|
|
118
|
+
return blocked('error', {
|
|
119
|
+
error: typeof body.error === 'string' && body.error.trim()
|
|
120
|
+
? body.error
|
|
121
|
+
: 'V1 migration failed',
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (MIGRATION_PHASES.has(phase) === false) {
|
|
126
|
+
return blocked('error', { error: 'V1 migration status is missing or invalid' });
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return blocked('error', { error: 'V1 migration status is missing or invalid' });
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
// Official protocol only publishes GET status. Backfill is owned by opencode2;
|
|
133
|
+
// clients poll this helper and do not POST unless a later API requires it.
|
|
134
|
+
export const fetchV1MigrationGate = async ({
|
|
135
|
+
url,
|
|
136
|
+
headers = {},
|
|
137
|
+
signal,
|
|
138
|
+
fetchImpl = globalThis.fetch,
|
|
139
|
+
} = {}) => {
|
|
140
|
+
try {
|
|
141
|
+
const response = await fetchImpl(url, {
|
|
142
|
+
method: 'GET',
|
|
143
|
+
headers: {
|
|
144
|
+
Accept: 'application/json',
|
|
145
|
+
...headers,
|
|
146
|
+
},
|
|
147
|
+
signal,
|
|
148
|
+
});
|
|
149
|
+
const body = await response.json().catch(() => null);
|
|
150
|
+
return evaluateV1MigrationGate({
|
|
151
|
+
httpStatus: response.status,
|
|
152
|
+
body,
|
|
153
|
+
});
|
|
154
|
+
} catch (error) {
|
|
155
|
+
return evaluateV1MigrationGate({ error });
|
|
156
|
+
}
|
|
157
|
+
};
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
OPENCODE_V1_MIGRATION_PATH,
|
|
5
|
+
V1_MIGRATION_USER_NOTICE,
|
|
6
|
+
evaluateV1MigrationGate,
|
|
7
|
+
fetchV1MigrationGate,
|
|
8
|
+
} from './v1-migration-gate.js';
|
|
9
|
+
|
|
10
|
+
describe('V1 migration gate', () => {
|
|
11
|
+
it('denies transcript while V1 migration is required', () => {
|
|
12
|
+
expect(evaluateV1MigrationGate({ status: 'required' })).toMatchObject({
|
|
13
|
+
admitTranscript: false,
|
|
14
|
+
phase: 'required',
|
|
15
|
+
userNotice: V1_MIGRATION_USER_NOTICE,
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it('denies transcript while V1 migration is running and keeps progress', () => {
|
|
20
|
+
expect(evaluateV1MigrationGate({
|
|
21
|
+
status: 'running',
|
|
22
|
+
progress: { label: 'Sessions', numerator: 3, denominator: 10 },
|
|
23
|
+
})).toEqual({
|
|
24
|
+
admitTranscript: false,
|
|
25
|
+
phase: 'running',
|
|
26
|
+
progress: { label: 'Sessions', numerator: 3, denominator: 10 },
|
|
27
|
+
userNotice: V1_MIGRATION_USER_NOTICE,
|
|
28
|
+
});
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it('admits transcript when V1 migration is completed', () => {
|
|
32
|
+
expect(evaluateV1MigrationGate({ status: 'completed' })).toMatchObject({
|
|
33
|
+
admitTranscript: true,
|
|
34
|
+
phase: 'completed',
|
|
35
|
+
});
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
it('admits transcript when V1 library is absent (HTTP 404)', () => {
|
|
39
|
+
expect(evaluateV1MigrationGate({ httpStatus: 404 })).toEqual({
|
|
40
|
+
admitTranscript: true,
|
|
41
|
+
phase: 'absent',
|
|
42
|
+
});
|
|
43
|
+
expect(evaluateV1MigrationGate({ status: 404 })).toEqual({
|
|
44
|
+
admitTranscript: true,
|
|
45
|
+
phase: 'absent',
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
it('admits transcript when completed with no remaining work', () => {
|
|
50
|
+
expect(evaluateV1MigrationGate({
|
|
51
|
+
body: { status: 'completed' },
|
|
52
|
+
httpStatus: 200,
|
|
53
|
+
})).toMatchObject({
|
|
54
|
+
admitTranscript: true,
|
|
55
|
+
phase: 'completed',
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('denies transcript on V1 migration error instead of treating an empty list as success', () => {
|
|
60
|
+
expect(evaluateV1MigrationGate({
|
|
61
|
+
status: 'error',
|
|
62
|
+
error: 'disk full',
|
|
63
|
+
})).toEqual({
|
|
64
|
+
admitTranscript: false,
|
|
65
|
+
phase: 'error',
|
|
66
|
+
error: 'disk full',
|
|
67
|
+
userNotice: V1_MIGRATION_USER_NOTICE,
|
|
68
|
+
});
|
|
69
|
+
expect(evaluateV1MigrationGate({})).toMatchObject({
|
|
70
|
+
admitTranscript: false,
|
|
71
|
+
phase: 'error',
|
|
72
|
+
});
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('denies transcript on network failure instead of treating an empty list as success', () => {
|
|
76
|
+
expect(evaluateV1MigrationGate({ error: new Error('offline') })).toMatchObject({
|
|
77
|
+
admitTranscript: false,
|
|
78
|
+
phase: 'error',
|
|
79
|
+
error: 'offline',
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('exposes backfill userNotice for UI and docs', () => {
|
|
84
|
+
expect(V1_MIGRATION_USER_NOTICE).toContain('Message ids are reused');
|
|
85
|
+
expect(V1_MIGRATION_USER_NOTICE).toContain('interrupted');
|
|
86
|
+
expect(V1_MIGRATION_USER_NOTICE).toContain('V1 subtasks do not appear in v2');
|
|
87
|
+
expect(evaluateV1MigrationGate({ status: 'required' }).userNotice).toBe(V1_MIGRATION_USER_NOTICE);
|
|
88
|
+
expect(evaluateV1MigrationGate({ status: 'completed' }).userNotice).toBe(V1_MIGRATION_USER_NOTICE);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('polls GET migration status and does not POST', async () => {
|
|
92
|
+
const fetchImpl = vi.fn(async () => new Response(JSON.stringify({ status: 'completed' }), {
|
|
93
|
+
status: 200,
|
|
94
|
+
headers: { 'content-type': 'application/json' },
|
|
95
|
+
}));
|
|
96
|
+
|
|
97
|
+
await expect(fetchV1MigrationGate({
|
|
98
|
+
url: `http://127.0.0.1:4096${OPENCODE_V1_MIGRATION_PATH}`,
|
|
99
|
+
headers: { Authorization: 'Basic test' },
|
|
100
|
+
fetchImpl,
|
|
101
|
+
})).resolves.toMatchObject({
|
|
102
|
+
admitTranscript: true,
|
|
103
|
+
phase: 'completed',
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
expect(fetchImpl).toHaveBeenCalledTimes(1);
|
|
107
|
+
expect(fetchImpl.mock.calls[0][0]).toContain(OPENCODE_V1_MIGRATION_PATH);
|
|
108
|
+
expect(fetchImpl.mock.calls[0][1].method).toBe('GET');
|
|
109
|
+
expect(fetchImpl.mock.calls[0][1].headers.Authorization).toBe('Basic test');
|
|
110
|
+
});
|
|
111
|
+
});
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
import { OpenCode } from '@opencode/client';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* @typedef {import('@opencode/client').OpenCodeClient} OpenCodeV2ClientRaw
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Compatibility client: official 2.x nests exact message lookup under
|
|
9
|
+
* `session.message.get`, while OpenChamber callers still use
|
|
10
|
+
* `session.message(input, requestOptions)`.
|
|
11
|
+
*
|
|
12
|
+
* @typedef {Omit<OpenCodeV2ClientRaw, 'session'> & {
|
|
13
|
+
* session: Omit<OpenCodeV2ClientRaw['session'], 'message'> & {
|
|
14
|
+
* message: ((input: Parameters<OpenCodeV2ClientRaw['session']['message']['get']>[0], requestOptions?: Parameters<OpenCodeV2ClientRaw['session']['message']['get']>[1]) => ReturnType<OpenCodeV2ClientRaw['session']['message']['get']>)
|
|
15
|
+
* & OpenCodeV2ClientRaw['session']['message']
|
|
16
|
+
* }
|
|
17
|
+
* }} OpenCodeV2Client
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Create the single server-side client for official OpenCode v2 APIs.
|
|
22
|
+
*
|
|
23
|
+
* @param {{ baseUrl: string, authHeaders?: Record<string, string>, fetchImpl?: typeof fetch }} input
|
|
24
|
+
* @returns {OpenCodeV2Client}
|
|
25
|
+
*/
|
|
26
|
+
export function makeOpenCodeV2Client({ baseUrl, authHeaders, fetchImpl }) {
|
|
27
|
+
const client = OpenCode.make({
|
|
28
|
+
baseUrl: baseUrl.replace(/\/$/, ''),
|
|
29
|
+
...(authHeaders ? { headers: authHeaders } : {}),
|
|
30
|
+
...(fetchImpl ? { fetch: fetchImpl } : {}),
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
const messageGet = client.session.message?.get?.bind(client.session.message);
|
|
34
|
+
if (typeof messageGet === 'function') {
|
|
35
|
+
const messageCompat = Object.assign(
|
|
36
|
+
(input, requestOptions) => messageGet(input, requestOptions),
|
|
37
|
+
client.session.message,
|
|
38
|
+
);
|
|
39
|
+
return {
|
|
40
|
+
...client,
|
|
41
|
+
session: {
|
|
42
|
+
...client.session,
|
|
43
|
+
message: messageCompat,
|
|
44
|
+
},
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return client;
|
|
49
|
+
}
|
|
@@ -65,7 +65,8 @@ export const createOpenCodeWatcherRuntime = (deps) => {
|
|
|
65
65
|
|
|
66
66
|
reader = createUpstreamSseReader({
|
|
67
67
|
signal,
|
|
68
|
-
|
|
68
|
+
// v2 serves the event stream at `/api/event`; `/api/global/event` is gone.
|
|
69
|
+
buildUrl: () => buildOpenCodeUrl('/event', ''),
|
|
69
70
|
getHeaders: getOpenCodeAuthHeaders,
|
|
70
71
|
fetchImpl,
|
|
71
72
|
stallTimeoutMs: upstreamStallTimeoutMs,
|
|
@@ -15,7 +15,7 @@ Server-owned scheduled task runtime and routes for OpenChamber-only automation.
|
|
|
15
15
|
- Next-run computation (daily/weekly/cron compatibility)
|
|
16
16
|
- Timer scheduling and queueing
|
|
17
17
|
- Concurrency controls
|
|
18
|
-
- Session create →
|
|
18
|
+
- Session create → goal/prompt/command execution
|
|
19
19
|
- Emits OpenChamber task-run events
|
|
20
20
|
- Isolates project sync failures, retries each failed project up to three times,
|
|
21
21
|
and clears pending retries during shutdown
|
|
@@ -186,44 +186,55 @@ Every actual run (timer or manual):
|
|
|
186
186
|
4. Create an OpenCode session with:
|
|
187
187
|
- title: `[Scheduled] ${taskName} yyyy-LL-dd HH:mm` (total length ≤ 120)
|
|
188
188
|
- metadata: `openchamber.scheduledTask = { projectID, taskID, runID, name }`
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
5. `attachSession(runID, sessionID)` immediately after create.
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
**Admission is not completion:** `
|
|
189
|
+
- SDK/HTTP calls receive `{ signal }` as the second request-options argument
|
|
190
|
+
(`client.session.create(params, { signal })`, same for `prompt` /
|
|
191
|
+
`command` / `command.list`; raw `fetch` for goal PATCH
|
|
192
|
+
pass `signal` in `RequestInit`).
|
|
193
|
+
5. `attachSession(runID, sessionID)` immediately after create.
|
|
194
|
+
- If attach fails, the runtime fails the run without prompting.
|
|
195
|
+
6. When `goalEnabled`: refuse before OpenCode ready / session create if
|
|
196
|
+
`persistSessionGoal` is not injected; otherwise write objective file,
|
|
197
|
+
persist an active goal into the Host session-metadata store, optionally
|
|
198
|
+
notify `onGoalPersisted` to arm the goal loop, then command or
|
|
199
|
+
`session.prompt`. Non-cancellable async gaps (small-model distill,
|
|
200
|
+
objective file write) check `signal.throwIfAborted()` before continuing so
|
|
201
|
+
a timed-out run never prompts.
|
|
202
|
+
**Admission is not completion:** `session.prompt` / command return when the
|
|
203
203
|
turn is accepted, not when the agent finishes.
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
204
|
+
7. Wait for the real session outcome (bounded by the same watchdog):
|
|
205
|
+
- Poll `session.active` + `message.list` until the session is idle and
|
|
206
|
+
the **latest-side** message page settles. Settlement always uses
|
|
207
|
+
`message.list({ limit: 50, order: 'desc' })` (newest first) — never
|
|
208
|
+
`order: 'asc'` page-0 + `at(-1)`, which reads an old page once the
|
|
209
|
+
session exceeds the limit.
|
|
210
|
+
- Real terminal success requires an assistant tail with `time.completed`
|
|
211
|
+
and finish not `tool-calls` (prefer `finish: 'stop'`). `finish:
|
|
212
|
+
'tool-calls'`, incomplete assistants (no `time.completed`), and
|
|
213
|
+
non-assistant tails such as `model-switched` never succeed — including
|
|
214
|
+
after multiple incomplete idle probes.
|
|
215
|
+
- `session.active` / `message.list` failures stay **unknown** (keep
|
|
216
|
+
polling under the watchdog); they never masquerade as idle success.
|
|
217
|
+
- Assistant `error` (and terminal finish `error` / `length` /
|
|
218
|
+
`content-filter`) finalizes as `error`.
|
|
219
|
+
- Goal-enabled runs: settle via Host goal terminal status when the store
|
|
220
|
+
seam is available (`readSessionMetadata` preferred over OpenCode
|
|
221
|
+
`session.get`). Store read failure is never empty-goal success.
|
|
222
|
+
- `durationMs` / `finishedAt` are wall-clock from run start through this
|
|
223
|
+
settlement — not the prompt admission latency.
|
|
224
|
+
8. On watchdog timeout after a session exists: immediately throw the canonical
|
|
214
225
|
`schedule run timed out` without awaiting non-cancellable helper work. A
|
|
215
226
|
once-only abort listener registered after session create starts best-effort
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
227
|
+
`client.session.interrupt({ sessionID, continue: false })` as soon as the watchdog
|
|
228
|
+
aborts the signal (or right after create if the signal already aborted).
|
|
229
|
+
Interrupt failures must not replace the timeout error. Successful runs and
|
|
230
|
+
ordinary non-timeout failures never call session interrupt.
|
|
231
|
+
9. Finalize history with the ultimate run status (timeout → `error` with
|
|
221
232
|
`schedule run timed out`; assistant/goal failure → `error` with the
|
|
222
233
|
recorded reason). An already-attached `session_id` remains on the history
|
|
223
234
|
row so the session stays openable from history. Task state persistence
|
|
224
235
|
failures also finalize history as `error`. A failed history finalize
|
|
225
236
|
returns error and may leave a `running` row for next-start convergence.
|
|
226
|
-
|
|
237
|
+
10. `lastSessionId` is written on the task state whenever the run has a
|
|
227
238
|
session (success or error). If the run has no session, omit
|
|
228
239
|
`lastSessionId` from the patch so a previous value is preserved. Full
|
|
229
240
|
association still lives in the history table; history rows are not
|
|
@@ -239,14 +250,35 @@ that same history session. `observeSessionEvent` corrects **only task state**
|
|
|
239
250
|
|
|
240
251
|
- `session.status` busy/retry while `lastStatus` is `error` or `success` →
|
|
241
252
|
`lastStatus` `running`, clear `lastError`, keep/set `lastSessionId`
|
|
242
|
-
- later `session.idle` (or idle `session.status`) with a success snapshot
|
|
243
|
-
`lastStatus` `success`
|
|
253
|
+
- later `session.idle` (or idle `session.status`) with a success snapshot from
|
|
254
|
+
`session.active` + latest-side `message.list` → `lastStatus` `success`
|
|
244
255
|
- do not change `lastRunAt` / `lastDurationMs` / `nextRunAt`
|
|
256
|
+
- history rows are never rewritten; `finishRun` is not called again
|
|
245
257
|
|
|
246
258
|
A live run (`runningTaskKeys`) owns settlement; the observer is a no-op while
|
|
247
259
|
the task is in `runningTaskKeys`. Idle correction is a no-op if `lastStatus`
|
|
248
|
-
is already `success`, or if the snapshot is still error/busy/unknown
|
|
249
|
-
|
|
260
|
+
is already `success`, or if the snapshot is still error/busy/unknown
|
|
261
|
+
(`session.active` failure and goal-unsupported goal tasks stay unknown).
|
|
262
|
+
Observer failures are logged and must not throw out of the event bus.
|
|
263
|
+
|
|
264
|
+
## Goal safety gating
|
|
265
|
+
|
|
266
|
+
Session goal Host state is advertised by
|
|
267
|
+
`packages/web/server/lib/session-goal/capability.js` as `{ supported: true }`.
|
|
268
|
+
Goal records live in the OpenChamber session-metadata store (same seam as
|
|
269
|
+
manual UI / `session-goal` runtime). Scheduled runs inject
|
|
270
|
+
`persistSessionGoal`, `readSessionMetadata`, and optional `onGoalPersisted`
|
|
271
|
+
from `server/index.js`.
|
|
272
|
+
|
|
273
|
+
| Path | Behavior |
|
|
274
|
+
|---|---|
|
|
275
|
+
| `PUT` scheduled task / managed-tool create·update with `execution.goalEnabled` | Allowed when capability supported |
|
|
276
|
+
| Existing `goalEnabled` task due or manual run | Requires `persistSessionGoal`; without it fail **before** OpenCode ready / session create / objective write / prompt; with it write objective + active goal then prompt |
|
|
277
|
+
| Ordinary non-goal task | Unchanged |
|
|
278
|
+
| Post-run observer on a goal-enabled task | Prefer Host store goal terminal status; never invent success from assistant-tail idle alone when goal is active |
|
|
279
|
+
|
|
280
|
+
Do not treat objective-file hints or first-turn assistant idle as a registered
|
|
281
|
+
goal success without a store-backed goal record.
|
|
250
282
|
|
|
251
283
|
Run start also emits `scheduled-task-ran` with `status: running` as soon as
|
|
252
284
|
task state is persisted, so the outer task list can show in-progress before
|
|
@@ -255,21 +287,21 @@ the session is attached.
|
|
|
255
287
|
## Watchdog cancel / upstream abort
|
|
256
288
|
|
|
257
289
|
- Timeout path: abort the per-run signal → cancel in-flight cancellable
|
|
258
|
-
SDK/HTTP → start best-effort OpenCode `session.
|
|
290
|
+
SDK/HTTP → start best-effort OpenCode `session.interrupt` via the create-time
|
|
259
291
|
abort listener (independent of whether the current await is cancellable) →
|
|
260
292
|
**do not wait** for non-cancellable helpers (small-model distill /
|
|
261
293
|
`writeObjective`) to finish → history and task state finalize as `error`
|
|
262
294
|
with `schedule run timed out` within the watchdog bound. The detached
|
|
263
295
|
`runPromise` is settled with a catch so late rejections are not unhandled.
|
|
264
296
|
- When uncancellable helper work eventually finishes, post-await
|
|
265
|
-
`throwIfAborted` gates stop goal PATCH / command / `
|
|
266
|
-
- Non-timeout failures: no timeout abort; no forced `session.
|
|
297
|
+
`throwIfAborted` gates stop goal PATCH / command / `session.prompt`.
|
|
298
|
+
- Non-timeout failures: no timeout abort; no forced `session.interrupt` from the
|
|
267
299
|
watchdog.
|
|
268
300
|
- Runtime `stop()` clears timers, project-sync retries, and the queue only; it
|
|
269
301
|
does not abort in-flight run controllers.
|
|
270
302
|
- Remaining non-cancellable edges: work already fully admitted and completed
|
|
271
303
|
HTTP-wise before timeout cannot be rolled back by aborting the HTTP request
|
|
272
|
-
alone; `session.
|
|
304
|
+
alone; `session.interrupt` is the upstream stop for admitted command/prompt
|
|
273
305
|
execution. Small-model distillation and objective file writes are not passed
|
|
274
306
|
a signal; the watchdog does not await them, and abort gates block later
|
|
275
307
|
stages after they complete.
|