@aiworker/web 1.22.2 → 1.23.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/bin/cli.js +7 -0
- package/dist/assets/{CodeMirrorCommentWidgets-CSICpWQD.js → CodeMirrorCommentWidgets-teB3HmBT.js} +1 -1
- package/dist/assets/DiffView-DC9RX3MF.js +4 -0
- package/dist/assets/{DirtyBranchSwitchDialog-C22HdWj4.js → DirtyBranchSwitchDialog-DwPkzXsS.js} +1 -1
- package/dist/assets/FilesView-BFZkuu4O.js +13 -0
- package/dist/assets/{GitView-VQQkYuUw.js → GitView-B_x52MHY.js} +4 -4
- package/dist/assets/{JsonTreeView-6lrhwD3q.js → JsonTreeView-cmAzUTiw.js} +1 -1
- package/dist/assets/{LinearIssuesView-Ba6Q9569.js → LinearIssuesView-Dehh5afm.js} +1 -1
- package/dist/assets/MarkdownImageGallery-DgNOqqhE.js +1 -0
- package/dist/assets/MarkdownRendererImpl-DusmMZ_p.js +12 -0
- package/dist/assets/OnboardingScreen-B8fm1H4m.js +1 -0
- package/dist/assets/{PatchDiff-BzcZPlOq.js → PatchDiff-BVr7gJU0.js} +1 -1
- package/dist/assets/PierreDiffViewer-DzLt-nel.js +161 -0
- package/dist/assets/PlanView-BmQUYT_z.js +5 -0
- package/dist/assets/SettingsView-ChWmAcZT.js +28 -0
- package/dist/assets/{SettingsWindow-DDQYaKWQ.js → SettingsWindow-Dy3M6ZB9.js} +1 -1
- package/dist/assets/SymbolsNerdFontMono-Regular-aK5vsLov.woff2 +0 -0
- package/dist/assets/{ToolOutputDialog-iYF8Y2bz.js → ToolOutputDialog-cTl_eLIH.js} +5 -5
- package/dist/assets/{ToolPartDiffPreview-CdjbOVMM.js → ToolPartDiffPreview-C_3iYNJQ.js} +1 -1
- package/dist/assets/{VirtualizedFileDiff-fFFm2QgH.js → VirtualizedFileDiff-CAWthkoy.js} +4 -4
- package/dist/assets/WalkthroughView-C7AHOpCA.js +4 -0
- package/dist/assets/{computeVirtualFileMetrics-CzgLum_K.js → computeVirtualFileMetrics-BkiDSlTW.js} +2 -2
- package/dist/assets/contextPanelEmbeddedChat-BdA6bdky.js +1 -0
- package/dist/assets/de-DJsQr-p7.js +10 -0
- package/dist/assets/es-BuYMlA9g.js +16 -0
- package/dist/assets/fr-ePDay33C.js +14 -0
- package/dist/assets/ghostty-vt-DdA0Zryv.wasm +0 -0
- package/dist/assets/index-BD8ILbxF.js +40 -0
- package/dist/assets/{index-Bl3LSbjn.js → index-n6O3hklM.js} +1 -1
- package/dist/assets/index-zhgg-SDz.css +1 -0
- package/dist/assets/{isDiffPlainText-Dx-dB_8l.js → isDiffPlainText-GMdPz-71.js} +1 -1
- package/dist/assets/{isFilePlainText-CWFPwmnU.js → isFilePlainText-BVuH1zH9.js} +1 -1
- package/dist/assets/ja-BzR9xOY_.js +16 -0
- package/dist/assets/ko-BEjHAm14.js +16 -0
- package/dist/assets/main-BydQ1tr_.js +930 -0
- package/dist/assets/{main-DnBAoktx.js → main-D3ZDQglc.js} +2 -2
- package/dist/assets/markdownCore-Cb2MdZnV.js +5 -0
- package/dist/assets/{miniChat-BKyvLseN.js → miniChat-d01k801U.js} +2 -2
- package/dist/assets/{mobile-DKjeFgMN.js → mobile-D7VV-Ufz.js} +2 -2
- package/dist/assets/patchFileDiff-CdztVMz0.js +13 -0
- package/dist/assets/pl-rQmpxNIF.js +16 -0
- package/dist/assets/pt-BR-CorbsSW-.js +16 -0
- package/dist/assets/renderElectronMiniChatApp-BGG9UIlh.js +2 -0
- package/dist/assets/renderMobileApp-BRP3J1iv.js +25 -0
- package/dist/assets/runtimeEndpointReset-BhT_-c52.js +41 -0
- package/dist/assets/scheduledTasksApi-BEuUF_cj.js +1 -0
- package/dist/assets/{shikiHighlight-TEQCmzW8.js → shikiHighlight-BzXUjlby.js} +1 -1
- package/dist/assets/surface-COKb77Ua.js +1 -0
- package/dist/assets/tr-CoAMXPtI.js +7 -0
- package/dist/assets/uk-YlAX0rRU.js +16 -0
- package/dist/assets/{useAppFontEffects-D7sNpuL2.js → useAppFontEffects-Cwyhjfl5.js} +141 -140
- package/dist/assets/useGitComparison-BCZVefdE.js +1 -0
- package/dist/assets/useInlineCommentController-DDkpBEU8.js +2 -0
- package/dist/assets/useRootScrollLock-CwjoWuC4.js +1 -0
- package/dist/assets/{vendor-base-ui-BeMD8X_w.js → vendor-base-ui-BY_5qla_.js} +1 -1
- package/dist/assets/{vendor-codemirror-language-data-Dp3KMRQG.js → vendor-codemirror-language-data-Cb1YnICj.js} +2 -2
- package/dist/assets/vendor-legendapp-list-BuBPBC3R.js +1 -0
- package/dist/assets/vendor-zod-B6551dwG.js +39 -0
- package/dist/assets/{web-components-_mNEMjFY.js → web-components-LV3vjTxy.js} +1 -1
- package/dist/assets/zh-CN-DDYg0Jl7.js +16 -0
- package/dist/assets/zh-TW-CQKaCayr.js +16 -0
- package/dist/index.html +5 -43
- package/dist/mini-chat.html +4 -4
- package/dist/mobile.html +4 -4
- package/package.json +2 -3
- package/server/index.d.ts +15 -0
- package/server/index.js +48 -22
- package/server/lib/agent-memory/runtime.js +7 -2
- package/server/lib/agent-memory/runtime.test.js +22 -0
- package/server/lib/data-dir-migration.js +40 -0
- package/server/lib/data-dir-migration.test.js +59 -0
- package/server/lib/event-stream/DOCUMENTATION.md +3 -0
- package/server/lib/event-stream/global-hub.js +29 -4
- package/server/lib/event-stream/global-hub.test.js +22 -0
- package/server/lib/event-stream/global-ws-bridge.js +15 -18
- package/server/lib/event-stream/protocol.js +21 -7
- package/server/lib/event-stream/runtime.js +9 -6
- package/server/lib/event-stream/runtime.test.js +72 -12
- package/server/lib/fs/DOCUMENTATION.md +3 -0
- package/server/lib/fs/routes.js +31 -0
- package/server/lib/fs/routes.test.js +110 -1
- package/server/lib/git/DOCUMENTATION.md +39 -6
- package/server/lib/git/routes.js +45 -12
- package/server/lib/git/routes.test.js +78 -0
- package/server/lib/git/service.js +343 -202
- package/server/lib/git/service.test.js +496 -25
- package/server/lib/github/DOCUMENTATION.md +5 -0
- package/server/lib/github/routes.js +2 -2
- package/server/lib/inherited-env.js +16 -9
- package/server/lib/inherited-env.test.js +8 -9
- package/server/lib/message-queue/DOCUMENTATION.md +11 -1
- package/server/lib/message-queue/runtime.js +13 -0
- package/server/lib/message-queue/runtime.test.js +31 -1
- package/server/lib/network-defaults.js +23 -0
- package/server/lib/network-defaults.test.js +52 -0
- package/server/lib/notifications/DOCUMENTATION.md +1 -1
- package/server/lib/notifications/emitter-runtime.js +4 -3
- package/server/lib/opencode/DOCUMENTATION.md +35 -5
- package/server/lib/opencode/bootstrap-runtime.js +2 -0
- package/server/lib/opencode/env-runtime.js +5 -1
- package/server/lib/opencode/env-runtime.test.js +72 -0
- package/server/lib/opencode/feature-routes-runtime.js +17 -1
- package/server/lib/opencode/lifecycle.js +4 -3
- package/server/lib/opencode/lifecycle.test.js +29 -0
- package/server/lib/opencode/managed-plugin-config.js +1 -1
- package/server/lib/opencode/openchamber-routes.js +64 -4
- package/server/lib/opencode/openchamber-routes.test.js +128 -1
- package/server/lib/opencode/proxy.js +33 -0
- package/server/lib/opencode/routes.js +5 -3
- package/server/lib/opencode/settings-files.js +214 -0
- package/server/lib/opencode/settings-helpers.js +88 -41
- package/server/lib/opencode/settings-helpers.test.js +176 -1
- package/server/lib/opencode/settings-registry.json +750 -0
- package/server/lib/opencode/settings-runtime.js +175 -27
- package/server/lib/opencode/settings-runtime.test.js +321 -7
- package/server/lib/project-context/DOCUMENTATION.md +38 -4
- package/server/lib/project-context/routes.http.test.js +18 -0
- package/server/lib/project-context/routes.js +15 -0
- package/server/lib/project-context/runtime.js +256 -25
- package/server/lib/project-context/runtime.test.js +160 -3
- package/server/lib/projects/DOCUMENTATION.md +77 -0
- package/server/lib/projects/project-config.js +179 -17
- package/server/lib/projects/project-config.test.js +97 -0
- package/server/lib/projects/project-id.js +43 -0
- package/server/lib/projects/project-setup.js +409 -0
- package/server/lib/projects/project-setup.test.js +557 -0
- package/server/lib/projects/routes.js +59 -0
- package/server/lib/quota/DOCUMENTATION.md +31 -1
- package/server/lib/quota/providers/cline-pass.js +125 -0
- package/server/lib/quota/providers/cline-pass.test.js +103 -0
- package/server/lib/quota/providers/index.js +7 -0
- package/server/lib/quota/providers/neuralwatt.js +3 -6
- package/server/lib/quota/providers/neuralwatt.test.js +7 -7
- package/server/lib/quota/providers/ollama-cloud.js +31 -0
- package/server/lib/quota/providers/ollama-cloud.test.js +33 -1
- package/server/lib/quota/providers/opencode-go.js +1 -1
- package/server/lib/quota/providers/opencode-go.test.js +26 -1
- package/server/lib/quota/providers/openrouter.js +119 -24
- package/server/lib/quota/providers/openrouter.test.js +330 -0
- package/server/lib/relay/DOCUMENTATION.md +75 -0
- package/server/lib/relay/cross-compat.test.js +26 -0
- package/server/lib/relay/downstream-scheduler.js +154 -0
- package/server/lib/relay/downstream-scheduler.test.js +162 -0
- package/server/lib/relay/e2ee.js +10 -8
- package/server/lib/relay/flow-control.test.js +177 -0
- package/server/lib/relay/host-client.js +44 -8
- package/server/lib/relay/host-client.test.js +14 -0
- package/server/lib/relay/tunnel-codec.js +9 -0
- package/server/lib/relay/tunnel-host.js +66 -17
- package/server/lib/scheduled-tasks/DOCUMENTATION.md +9 -1
- package/server/lib/scheduled-tasks/issue-2710-double-execution.test.js +28 -0
- package/server/lib/scheduled-tasks/runtime.js +12 -2
- package/server/lib/scheduled-tasks/runtime.test.js +53 -0
- package/server/lib/session-assist/DOCUMENTATION.md +131 -66
- package/server/lib/session-assist/context.js +127 -0
- package/server/lib/session-assist/context.test.js +132 -0
- package/server/lib/session-assist/prompt.js +54 -0
- package/server/lib/session-assist/runtime.js +117 -267
- package/server/lib/session-assist/runtime.test.js +203 -0
- package/server/lib/session-goal/runtime.js +4 -9
- package/server/lib/small-model/DOCUMENTATION.md +3 -2
- package/server/lib/small-model/call.js +9 -4
- package/server/lib/small-model/call.test.js +29 -0
- package/server/lib/small-model/index.js +5 -10
- package/server/lib/small-model/runtime-providers.js +15 -7
- package/server/lib/small-model/runtime-providers.test.js +7 -1
- package/server/lib/terminal/DOCUMENTATION.md +4 -4
- package/server/lib/terminal/runtime.js +11 -6
- package/server/lib/terminal/runtime.test.js +20 -13
- package/server/lib/walkthrough/DOCUMENTATION.md +49 -20
- package/server/lib/walkthrough/model-settings.js +8 -12
- package/server/lib/walkthrough/pr-source.test.js +21 -0
- package/server/lib/walkthrough/prompt.js +3 -1
- package/server/lib/walkthrough/pull-request.js +7 -4
- package/server/lib/walkthrough/pull-request.test.js +16 -0
- package/server/lib/walkthrough/routes.js +19 -0
- package/server/lib/walkthrough/routes.test.js +26 -0
- package/server/lib/walkthrough/sources.js +36 -4
- package/server/opencode-worktree-readiness.test.js +130 -0
- package/dist/assets/DiffView-hDBKesA_.js +0 -4
- package/dist/assets/FilesView-CdWzeOp9.js +0 -17
- package/dist/assets/MarkdownImageGallery-Czv_mhvS.js +0 -1
- package/dist/assets/MarkdownRendererImpl-BrMcswiy.js +0 -12
- package/dist/assets/OnboardingScreen-ChxQoAkx.js +0 -1
- package/dist/assets/PierreDiffViewer-BchBtZlp.js +0 -157
- package/dist/assets/PlanView-DyDt_nZ_.js +0 -5
- package/dist/assets/SettingsView-BKf5xBsP.js +0 -28
- package/dist/assets/WalkthroughView-DOeCsMju.js +0 -4
- package/dist/assets/contextPanelEmbeddedChat-C0ZLAepL.js +0 -1
- package/dist/assets/de-B5JaA3SA.js +0 -10
- package/dist/assets/es-EPIH88ii.js +0 -16
- package/dist/assets/fr-CAMfxUdZ.js +0 -14
- package/dist/assets/index-DF5kMP-i.js +0 -39
- package/dist/assets/index-Dl3xbAB_.css +0 -1
- package/dist/assets/ja-CifOE1VN.js +0 -16
- package/dist/assets/ko-BBDyAyYQ.js +0 -16
- package/dist/assets/main-MhSaOZoB.js +0 -930
- package/dist/assets/markdownCore-BrBwOOMn.js +0 -3
- package/dist/assets/patchFileDiff-o8ms1me8.js +0 -10
- package/dist/assets/pl-DgEHYfFM.js +0 -16
- package/dist/assets/pt-BR-DuWHojUw.js +0 -16
- package/dist/assets/renderElectronMiniChatApp-CDgBwwCS.js +0 -2
- package/dist/assets/renderMobileApp-DJIKIK1r.js +0 -25
- package/dist/assets/runtimeEndpointReset-CYJ9O_xX.js +0 -17
- package/dist/assets/scheduledTasksApi-D8yamh1b.js +0 -1
- package/dist/assets/tr-D5m2yIWL.js +0 -7
- package/dist/assets/uk-D_q60oog.js +0 -16
- package/dist/assets/useInlineCommentController-Brz_cLcQ.js +0 -2
- package/dist/assets/useRootScrollLock-lXEnWlaU.js +0 -1
- package/dist/assets/vendor-ghostty-web-CrxinGGH.js +0 -13
- package/dist/assets/vendor-legendapp-list-gg7YLRW7.js +0 -1
- package/dist/assets/vendor-zod-CK21ihK-.js +0 -39
- package/dist/assets/zh-CN-D8QvsVeg.js +0 -16
- package/dist/assets/zh-TW-B9t7rQQO.js +0 -16
- package/server/lib/mcp-reconnect/DOCUMENTATION.md +0 -55
- package/server/lib/mcp-reconnect/runtime.js +0 -122
- package/server/lib/mcp-reconnect/runtime.test.js +0 -140
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
3
|
-
// small model, and store both on the session's metadata
|
|
4
|
-
// (metadata.aiworker.assist). Clients decide visibility from
|
|
5
|
-
// assist.forMessageID — a new message makes the payload stale everywhere
|
|
6
|
-
// without any extra writes.
|
|
7
|
-
//
|
|
8
|
-
// Purely event-driven: only sessions that transition busy→idle while the
|
|
9
|
-
// server is running ever generate anything. No backfill, no session scans.
|
|
10
|
-
|
|
1
|
+
// Background session assistance. Only live idle events arm generation; there
|
|
2
|
+
// is no backfill. Clients hide results whose forMessageID is no longer current.
|
|
11
3
|
import fs from 'fs';
|
|
12
4
|
import os from 'os';
|
|
13
5
|
import path from 'path';
|
|
6
|
+
import { createOpencodeClient } from '@opencode-ai/sdk/v2';
|
|
7
|
+
import { readMergedSettingsSync } from '../opencode/settings-files.js';
|
|
8
|
+
import { loadAssistContext } from './context.js';
|
|
9
|
+
import { buildAssistPrompt, buildAssistSystemPrompt } from './prompt.js';
|
|
14
10
|
|
|
15
11
|
const AIWORKER_SETTINGS_FILE = path.join(
|
|
16
12
|
process.env.AIWORKER_DATA_DIR
|
|
@@ -19,75 +15,20 @@ const AIWORKER_SETTINGS_FILE = path.join(
|
|
|
19
15
|
'settings.json',
|
|
20
16
|
);
|
|
21
17
|
|
|
22
|
-
// The Chat settings are hard generation switches (default on): when both are
|
|
23
|
-
// off, no small-model calls and no metadata writes happen at all. Existing
|
|
24
|
-
// payloads stay untouched — clients keep showing them and dismissal still works.
|
|
25
18
|
const getSessionAssistTargets = () => {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
suggestion: settings?.sessionSuggestionEnabled !== false,
|
|
32
|
-
};
|
|
33
|
-
} catch {
|
|
34
|
-
return { recap: true, suggestion: true };
|
|
35
|
-
}
|
|
19
|
+
const settings = readMergedSettingsSync({ fs, path, settingsFilePath: AIWORKER_SETTINGS_FILE });
|
|
20
|
+
return {
|
|
21
|
+
recap: settings.sessionRecapEnabled !== false,
|
|
22
|
+
suggestion: settings.sessionSuggestionEnabled !== false,
|
|
23
|
+
};
|
|
36
24
|
};
|
|
37
25
|
|
|
38
26
|
const IDLE_QUIET_MS = 60_000;
|
|
39
|
-
const TRANSCRIPT_MESSAGE_LIMIT = 12;
|
|
40
|
-
const TRANSCRIPT_PART_CHAR_LIMIT = 6_000;
|
|
41
27
|
const RECAP_CHAR_LIMIT = 320;
|
|
42
28
|
const SUGGESTION_CHAR_LIMIT = 500;
|
|
43
29
|
const FETCH_TIMEOUT_MS = 5_000;
|
|
44
|
-
|
|
45
|
-
const
|
|
46
|
-
'You assist a user who chats with a coding agent. Based on the conversation transcript, return exactly one JSON object and nothing else — no prose, no markdown, no code fences.',
|
|
47
|
-
`Shape: {${[recap ? '"recap": string' : '', suggestion ? '"suggestion": string' : ''].filter(Boolean).join(', ')}}`,
|
|
48
|
-
recap
|
|
49
|
-
? 'recap: at most 20 words. State the substance directly — the facts, result, or conclusion, plus the next move if there is one. NEVER narrate ("The assistant explained…", "The agent did…") — write the content itself, like a note the user jotted down.'
|
|
50
|
-
: '',
|
|
51
|
-
suggestion ? 'suggestion: write ONE immediately sendable next user message addressed TO the coding agent.' : '',
|
|
52
|
-
suggestion ? 'The suggestion should be the most useful next step after the assistant\'s latest reply. It should help the user continue productively, not inspect already-known details.' : '',
|
|
53
|
-
suggestion ? 'Prefer suggestions that ask the agent to make a concrete improvement, implement something specific, validate the latest change, explain tradeoffs, improve the current approach, or continue from the current result.' : '',
|
|
54
|
-
suggestion ? 'Rules for suggestion:' : '',
|
|
55
|
-
suggestion ? '- Output exactly one message the user could click and send without editing.' : '',
|
|
56
|
-
suggestion ? '- Pick one best next action yourself.' : '',
|
|
57
|
-
suggestion ? '- Do not include alternatives, choices, slash-separated options, or "or".' : '',
|
|
58
|
-
suggestion ? '- Do not write "Do X or Y", "Ask whether...", "Maybe...", or "You could...".' : '',
|
|
59
|
-
suggestion ? '- Do not ask for information the assistant already provided.' : '',
|
|
60
|
-
suggestion ? '- Do not ask to see exact code, file paths, prompt locations, or implementation internals unless the assistant did not provide them and they are necessary for the next step.' : '',
|
|
61
|
-
suggestion ? '- Do not produce generic workflow commands like "Run tests" unless testing is clearly the next unresolved step.' : '',
|
|
62
|
-
suggestion ? '- Do not produce meta/debug requests that merely inspect the implementation.' : '',
|
|
63
|
-
suggestion ? '- Use imperative or question form.' : '',
|
|
64
|
-
suggestion ? '- Keep it concise.' : '',
|
|
65
|
-
suggestion ? 'Use these examples to understand how to choose the suggestion. Do not copy their topic or wording unless the current conversation is about the same thing.' : '',
|
|
66
|
-
suggestion ? 'Example 1:' : '',
|
|
67
|
-
suggestion ? 'Assistant reply summary:' : '',
|
|
68
|
-
suggestion ? 'The assistant already identified the file where the feature is implemented, explained what context is sent to the small model, and summarized the current prompt.' : '',
|
|
69
|
-
suggestion ? 'Bad suggestion:' : '',
|
|
70
|
-
suggestion ? '"Show me the exact runtime.js code and where the prompt is built."' : '',
|
|
71
|
-
suggestion ? 'Why bad:' : '',
|
|
72
|
-
suggestion ? 'It asks for information the assistant already provided. It repeats inspection instead of moving to an improvement or decision.' : '',
|
|
73
|
-
suggestion ? 'Good suggestion:' : '',
|
|
74
|
-
suggestion ? '"Suggest how to improve the prompt and context so the generated suggestion is more useful."' : '',
|
|
75
|
-
suggestion ? 'Why good:' : '',
|
|
76
|
-
suggestion ? 'It naturally continues from the analysis and asks for a concrete improvement.' : '',
|
|
77
|
-
suggestion ? 'Example 2:' : '',
|
|
78
|
-
suggestion ? 'Assistant reply summary:' : '',
|
|
79
|
-
suggestion ? 'The assistant implemented a timeline dialog redesign, listed concrete UI changes, and reported that type-check and lint passed.' : '',
|
|
80
|
-
suggestion ? 'Bad suggestion:' : '',
|
|
81
|
-
suggestion ? '"Check whether scrolling or loading older messages works without jumps."' : '',
|
|
82
|
-
suggestion ? 'Why bad:' : '',
|
|
83
|
-
suggestion ? 'It contains an alternative. A suggestion chip must be one sendable message, not a choice the user has to edit.' : '',
|
|
84
|
-
suggestion ? 'Good suggestion:' : '',
|
|
85
|
-
suggestion ? '"Check whether scrolling and loading older messages work without jumps."' : '',
|
|
86
|
-
suggestion ? 'Why good:' : '',
|
|
87
|
-
suggestion ? 'It picks a single validation request that the user can send immediately.' : '',
|
|
88
|
-
'All requested values MUST be written in the same language as the conversation text itself. Ignore any other language preferences or personalization you may have — only the conversation text decides the language.',
|
|
89
|
-
'Use double quotes for JSON strings, no trailing commas.',
|
|
90
|
-
].filter(Boolean).join('\n');
|
|
30
|
+
const GENERATION_TIMEOUT_MS = 120_000;
|
|
31
|
+
const QUIET_FAILURE_CODES = new Set(['context-too-small', 'output-exhausted']);
|
|
91
32
|
|
|
92
33
|
const extractJsonObject = (value) => {
|
|
93
34
|
const text = String(value ?? '').trim();
|
|
@@ -136,23 +77,16 @@ const extractUserMessage = (payload) => {
|
|
|
136
77
|
};
|
|
137
78
|
};
|
|
138
79
|
|
|
139
|
-
const messagePartsToText = (message) => {
|
|
140
|
-
const parts = Array.isArray(message?.parts) ? message.parts : [];
|
|
141
|
-
return parts
|
|
142
|
-
.map((part) => (part?.type === 'text' && typeof part.text === 'string' ? part.text : ''))
|
|
143
|
-
.filter(Boolean)
|
|
144
|
-
.join('\n')
|
|
145
|
-
.slice(0, TRANSCRIPT_PART_CHAR_LIMIT);
|
|
146
|
-
};
|
|
147
|
-
|
|
148
80
|
export const createSessionAssistRuntime = ({
|
|
149
81
|
buildOpenCodeUrl,
|
|
150
82
|
getOpenCodeAuthHeaders,
|
|
151
83
|
getSmallModelService,
|
|
84
|
+
getTargets = getSessionAssistTargets,
|
|
152
85
|
quietMs = IDLE_QUIET_MS,
|
|
153
86
|
}) => {
|
|
154
87
|
const timers = new Map();
|
|
155
|
-
const inflight = new
|
|
88
|
+
const inflight = new Map();
|
|
89
|
+
const ready = new Map();
|
|
156
90
|
let stopped = false;
|
|
157
91
|
|
|
158
92
|
const clearTimer = (sessionId) => {
|
|
@@ -163,233 +97,149 @@ export const createSessionAssistRuntime = ({
|
|
|
163
97
|
}
|
|
164
98
|
};
|
|
165
99
|
|
|
166
|
-
const
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
method,
|
|
171
|
-
headers: {
|
|
172
|
-
Accept: 'application/json',
|
|
173
|
-
...(body ? { 'Content-Type': 'application/json' } : {}),
|
|
174
|
-
...getOpenCodeAuthHeaders(),
|
|
175
|
-
},
|
|
176
|
-
...(body ? { body: JSON.stringify(body) } : {}),
|
|
177
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
178
|
-
});
|
|
179
|
-
if (!response.ok) {
|
|
180
|
-
throw new Error(`OpenCode ${method} ${path} failed with ${response.status}`);
|
|
181
|
-
}
|
|
182
|
-
return response.json().catch(() => null);
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
const fetchRecentMessages = async (sessionId, directory) => {
|
|
186
|
-
const base = buildOpenCodeUrl(`/session/${encodeURIComponent(sessionId)}/message`, '');
|
|
187
|
-
const params = new URLSearchParams({ limit: String(TRANSCRIPT_MESSAGE_LIMIT) });
|
|
188
|
-
if (directory) params.set('directory', directory);
|
|
189
|
-
const response = await fetch(`${base}?${params.toString()}`, {
|
|
190
|
-
method: 'GET',
|
|
191
|
-
headers: { Accept: 'application/json', ...getOpenCodeAuthHeaders() },
|
|
192
|
-
signal: AbortSignal.timeout(FETCH_TIMEOUT_MS),
|
|
193
|
-
});
|
|
194
|
-
if (!response.ok) return null;
|
|
195
|
-
const messages = await response.json().catch(() => null);
|
|
196
|
-
return Array.isArray(messages) ? messages : null;
|
|
100
|
+
const invalidate = (sessionId) => {
|
|
101
|
+
clearTimer(sessionId);
|
|
102
|
+
ready.delete(sessionId);
|
|
103
|
+
inflight.get(sessionId)?.controller.abort();
|
|
197
104
|
};
|
|
198
105
|
|
|
199
|
-
const generateAssist = async (sessionId, directory) => {
|
|
200
|
-
const targets =
|
|
106
|
+
const generateAssist = async (sessionId, directory, signal) => {
|
|
107
|
+
const targets = getTargets();
|
|
201
108
|
if (!targets.recap && !targets.suggestion) return;
|
|
202
|
-
const
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (!
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
// token waste for a one-line recap and a single suggestion.
|
|
231
|
-
const parentUserMessage = typeof lastAssistantInfo.parentID === 'string' && lastAssistantInfo.parentID
|
|
232
|
-
? messages.find((message) => message?.info?.id === lastAssistantInfo.parentID && message?.info?.role === 'user')
|
|
233
|
-
: null;
|
|
234
|
-
const userText = parentUserMessage ? messagePartsToText(parentUserMessage) : '';
|
|
235
|
-
const assistantText = messagePartsToText(lastAssistant);
|
|
236
|
-
const transcript = [
|
|
237
|
-
userText ? `User:\n${userText}` : '',
|
|
238
|
-
assistantText ? `Assistant:\n${assistantText}` : '',
|
|
239
|
-
].filter(Boolean).join('\n\n');
|
|
240
|
-
if (!transcript) return;
|
|
241
|
-
|
|
242
|
-
const { generateSmallModelText } = await getSmallModelService();
|
|
243
|
-
const requestedFields = [targets.recap ? 'recap' : '', targets.suggestion ? 'suggestion' : '']
|
|
244
|
-
.filter(Boolean)
|
|
245
|
-
.join(' and ');
|
|
246
|
-
// Instruct the language by example, not by description — account-side
|
|
247
|
-
// personalization (e.g. the ChatGPT backend knowing the user's locale)
|
|
248
|
-
// otherwise leaks a different language into the output.
|
|
249
|
-
const languageSample = (userText || assistantText).slice(0, 200).replace(/\s+/g, ' ').trim();
|
|
109
|
+
const baseUrl = buildOpenCodeUrl('/', '').replace(/\/$/, '');
|
|
110
|
+
const client = createOpencodeClient({ baseUrl, headers: getOpenCodeAuthHeaders(), throwOnError: true });
|
|
111
|
+
const requestOptions = () => ({ signal: AbortSignal.any([signal, AbortSignal.timeout(FETCH_TIMEOUT_MS)]) });
|
|
112
|
+
const checkCurrent = () => {
|
|
113
|
+
signal.throwIfAborted();
|
|
114
|
+
if (buildOpenCodeUrl('/', '').replace(/\/$/, '') !== baseUrl) throw new Error('Session assist runtime changed');
|
|
115
|
+
};
|
|
116
|
+
const { data: session } = await client.session.get({ sessionID: sessionId, directory }, requestOptions());
|
|
117
|
+
checkCurrent();
|
|
118
|
+
// Reverted history is not the active conversation. A new prompt clears
|
|
119
|
+
// the revert boundary before its next idle event.
|
|
120
|
+
if (session?.id !== sessionId || session.parentID || session.revert?.messageID || session.time?.archived) return;
|
|
121
|
+
const context = await loadAssistContext({
|
|
122
|
+
signal,
|
|
123
|
+
readPage: (page) => client.session.messages({ sessionID: sessionId, directory, ...page }, requestOptions()),
|
|
124
|
+
});
|
|
125
|
+
checkCurrent();
|
|
126
|
+
if (!context) return;
|
|
127
|
+
const { last, turns } = context;
|
|
128
|
+
const { describeSmallModel, generateSmallModelText } = await getSmallModelService();
|
|
129
|
+
const preferredProviderID = last.providerID;
|
|
130
|
+
const preferredModelID = last.modelID;
|
|
131
|
+
const described = await describeSmallModel({ directory, preferredProviderID, preferredModelID });
|
|
132
|
+
checkCurrent();
|
|
133
|
+
if (!described) return;
|
|
134
|
+
const system = buildAssistSystemPrompt(targets);
|
|
135
|
+
const prompt = buildAssistPrompt(turns, targets, described.inputCharBudget - system.length - 512);
|
|
136
|
+
if (!prompt) return;
|
|
250
137
|
let generated;
|
|
251
138
|
try {
|
|
252
139
|
generated = await generateSmallModelText({
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
restrictToPreferredProvider: true,
|
|
257
|
-
prompt: `The latest exchange in the conversation:\n\n${transcript}\n\nWrite ${requestedFields} in the SAME language as this sample from the conversation: "${languageSample}"`,
|
|
258
|
-
system: buildAssistSystemPrompt(targets),
|
|
259
|
-
directory,
|
|
260
|
-
sessionID: sessionId,
|
|
261
|
-
preferredProviderID: typeof lastAssistantInfo.providerID === 'string' ? lastAssistantInfo.providerID : undefined,
|
|
262
|
-
preferredModelID: typeof lastAssistantInfo.modelID === 'string' ? lastAssistantInfo.modelID : undefined,
|
|
140
|
+
prompt: prompt.text, system, directory, sessionID: sessionId,
|
|
141
|
+
preferredProviderID, preferredModelID, restrictToPreferredProvider: true,
|
|
142
|
+
onOverflow: 'error', timeoutMs: GENERATION_TIMEOUT_MS, signal,
|
|
263
143
|
});
|
|
264
144
|
} catch (error) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (Number(error?.statusCode) !== 404) {
|
|
268
|
-
console.warn('[session-assist] generation failed:', error?.message || error);
|
|
145
|
+
if (!signal.aborted && Number(error?.statusCode) !== 404 && !QUIET_FAILURE_CODES.has(error?.code)) {
|
|
146
|
+
console.warn('[session-assist] generation failed');
|
|
269
147
|
}
|
|
270
148
|
return;
|
|
271
149
|
}
|
|
272
|
-
|
|
150
|
+
checkCurrent();
|
|
273
151
|
const structured = extractJsonObject(generated?.text);
|
|
274
152
|
let recap = targets.recap && typeof structured?.recap === 'string' ? structured.recap.trim().slice(0, RECAP_CHAR_LIMIT) : '';
|
|
275
153
|
let suggestion = targets.suggestion && typeof structured?.suggestion === 'string' ? structured.suggestion.trim().slice(0, SUGGESTION_CHAR_LIMIT) : '';
|
|
276
|
-
|
|
277
|
-
// Hard guard against language hallucination: if the conversation contains
|
|
278
|
-
// no Cyrillic/CJK at all, the output must not either (and drop per-field,
|
|
279
|
-
// so one hallucinated field doesn't kill the other).
|
|
280
154
|
const hasCyrillic = (text) => /[\u0400-\u04FF]/.test(text);
|
|
281
155
|
const hasCjk = (text) => /[\u3040-\u30FF\u4E00-\u9FFF\uAC00-\uD7AF]/.test(text);
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
156
|
+
// Quoted source and assistant replies cannot authorize a different script.
|
|
157
|
+
// With no authored language sample, leave the decision to the prompt.
|
|
158
|
+
const scriptMismatch = (text) => prompt.language && ((hasCyrillic(text) && !hasCyrillic(prompt.language))
|
|
159
|
+
|| (hasCjk(text) && !hasCjk(prompt.language)));
|
|
160
|
+
if (recap && scriptMismatch(recap)) recap = '';
|
|
161
|
+
if (suggestion && scriptMismatch(suggestion)) suggestion = '';
|
|
162
|
+
if (!recap && !suggestion) return;
|
|
163
|
+
const { data: latest } = await client.session.messages({ sessionID: sessionId, directory, limit: 1 }, requestOptions());
|
|
164
|
+
checkCurrent();
|
|
165
|
+
if (latest?.at(-1)?.info.id !== last.id) return;
|
|
166
|
+
// Never fall back to the pre-generation metadata snapshot after a failed
|
|
167
|
+
// fresh read: doing so overwrites dismissals and unrelated metadata.
|
|
168
|
+
const { data: freshSession } = await client.session.get({ sessionID: sessionId, directory }, requestOptions());
|
|
169
|
+
checkCurrent();
|
|
170
|
+
if (freshSession?.id !== sessionId || freshSession.revert?.messageID || freshSession.time?.archived || freshSession.directory !== session.directory) return;
|
|
171
|
+
const enabled = getTargets();
|
|
172
|
+
if (!enabled.recap) recap = '';
|
|
173
|
+
if (!enabled.suggestion) suggestion = '';
|
|
293
174
|
if (!recap && !suggestion) return;
|
|
175
|
+
const currentMetadata = freshSession.metadata ?? {};
|
|
176
|
+
const currentNamespace = currentMetadata.aiworker ?? {};
|
|
177
|
+
await client.session.update({
|
|
178
|
+
sessionID: sessionId, directory,
|
|
179
|
+
metadata: {
|
|
180
|
+
...currentMetadata,
|
|
181
|
+
aiworker: {
|
|
182
|
+
...currentNamespace,
|
|
183
|
+
assist: { recap, suggestion, forMessageID: last.id, generatedAt: Date.now() },
|
|
184
|
+
},
|
|
185
|
+
},
|
|
186
|
+
}, requestOptions());
|
|
187
|
+
};
|
|
294
188
|
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
if (!latest) return null;
|
|
300
|
-
for (let i = latest.length - 1; i >= 0; i -= 1) {
|
|
301
|
-
const info = latest[i]?.info;
|
|
302
|
-
if (info?.role === 'assistant') return info.id;
|
|
303
|
-
if (info?.role === 'user') return null;
|
|
304
|
-
}
|
|
305
|
-
return null;
|
|
306
|
-
})();
|
|
307
|
-
if (latestAssistantId !== lastAssistantInfo.id) {
|
|
308
|
-
console.log('[session-assist] tail moved on, dropping result');
|
|
189
|
+
const startGeneration = (sessionId, directory, armedAt) => {
|
|
190
|
+
if (stopped) return;
|
|
191
|
+
if (inflight.has(sessionId)) {
|
|
192
|
+
ready.set(sessionId, { directory, armedAt });
|
|
309
193
|
return;
|
|
310
194
|
}
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
await openCodeFetch(`/session/${encodeURIComponent(sessionId)}`, {
|
|
326
|
-
directory,
|
|
327
|
-
method: 'PATCH',
|
|
328
|
-
body: {
|
|
329
|
-
metadata: {
|
|
330
|
-
...currentMetadata,
|
|
331
|
-
aiworker: {
|
|
332
|
-
...currentNamespace,
|
|
333
|
-
assist: {
|
|
334
|
-
recap,
|
|
335
|
-
suggestion,
|
|
336
|
-
forMessageID: lastAssistantInfo.id,
|
|
337
|
-
generatedAt: Date.now(),
|
|
338
|
-
},
|
|
339
|
-
},
|
|
340
|
-
},
|
|
341
|
-
},
|
|
342
|
-
});
|
|
195
|
+
const controller = new AbortController();
|
|
196
|
+
inflight.set(sessionId, { controller, armedAt });
|
|
197
|
+
generateAssist(sessionId, directory, controller.signal)
|
|
198
|
+
.catch(() => {
|
|
199
|
+
if (!controller.signal.aborted) console.warn('[session-assist] failed to read or save assistance');
|
|
200
|
+
})
|
|
201
|
+
.finally(() => {
|
|
202
|
+
inflight.delete(sessionId);
|
|
203
|
+
if (ready.has(sessionId)) {
|
|
204
|
+
const next = ready.get(sessionId);
|
|
205
|
+
ready.delete(sessionId);
|
|
206
|
+
startGeneration(sessionId, next.directory, next.armedAt);
|
|
207
|
+
}
|
|
208
|
+
});
|
|
343
209
|
};
|
|
344
210
|
|
|
345
211
|
const armTimer = (sessionId, directory) => {
|
|
346
212
|
clearTimer(sessionId);
|
|
213
|
+
const armedAt = Date.now();
|
|
347
214
|
const timer = setTimeout(() => {
|
|
348
215
|
timers.delete(sessionId);
|
|
349
|
-
|
|
350
|
-
inflight.add(sessionId);
|
|
351
|
-
generateAssist(sessionId, directory)
|
|
352
|
-
.catch((error) => {
|
|
353
|
-
console.warn('[session-assist] failed:', error?.message || error);
|
|
354
|
-
})
|
|
355
|
-
.finally(() => {
|
|
356
|
-
inflight.delete(sessionId);
|
|
357
|
-
});
|
|
216
|
+
startGeneration(sessionId, directory, armedAt);
|
|
358
217
|
}, quietMs);
|
|
359
|
-
|
|
360
|
-
timers.set(sessionId, { timer, armedAt
|
|
218
|
+
timer.unref?.();
|
|
219
|
+
timers.set(sessionId, { timer, armedAt });
|
|
361
220
|
};
|
|
362
221
|
|
|
363
222
|
const processPayload = (payload, directoryHint = '') => {
|
|
364
223
|
if (stopped) return;
|
|
365
224
|
const status = extractSessionStatus(payload);
|
|
366
225
|
if (status) {
|
|
367
|
-
if (status.type === 'idle')
|
|
368
|
-
|
|
369
|
-
} else {
|
|
370
|
-
clearTimer(status.sessionId);
|
|
371
|
-
}
|
|
226
|
+
if (status.type === 'idle') armTimer(status.sessionId, status.directory || directoryHint);
|
|
227
|
+
else invalidate(status.sessionId);
|
|
372
228
|
return;
|
|
373
229
|
}
|
|
374
230
|
const userMessage = extractUserMessage(payload);
|
|
375
231
|
if (userMessage) {
|
|
376
|
-
//
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
const armed = timers.get(userMessage.sessionId);
|
|
380
|
-
if (armed && userMessage.createdAt >= armed.armedAt) {
|
|
381
|
-
clearTimer(userMessage.sessionId);
|
|
382
|
-
}
|
|
232
|
+
// Ignore old message.updated events re-emitted after completion.
|
|
233
|
+
const since = timers.get(userMessage.sessionId)?.armedAt ?? inflight.get(userMessage.sessionId)?.armedAt;
|
|
234
|
+
if (since !== undefined && userMessage.createdAt >= since) invalidate(userMessage.sessionId);
|
|
383
235
|
}
|
|
384
236
|
};
|
|
385
237
|
|
|
386
238
|
const stop = () => {
|
|
387
239
|
stopped = true;
|
|
388
|
-
for (const
|
|
389
|
-
|
|
390
|
-
}
|
|
391
|
-
timers.clear();
|
|
240
|
+
for (const sessionId of timers.keys()) clearTimer(sessionId);
|
|
241
|
+
ready.clear();
|
|
242
|
+
for (const { controller } of inflight.values()) controller.abort();
|
|
392
243
|
};
|
|
393
|
-
|
|
394
244
|
return { processPayload, stop };
|
|
395
245
|
};
|