ompchamber 1.0.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/client/index.html +1 -1
- package/dist/client/static/css/index.b938f4a693.css +2 -0
- package/dist/client/static/js/77.8022a800b5.js +139 -0
- package/dist/client/static/js/async/{1k.5974df3b87.js → 1k.a411fe5b43.js} +2 -2
- package/dist/client/static/js/async/{3y.71f2ccf3b6.js → 3y.3186ecdd63.js} +5 -5
- package/dist/client/static/js/async/5b.d88304bc13.js +1 -0
- package/dist/client/static/js/async/66.86544ad741.js +93 -0
- package/dist/client/static/js/async/7g.db82aca7ba.js +1 -0
- package/dist/client/static/js/async/7l.fadbbc0583.js +1 -0
- package/dist/client/static/js/async/{dc.856fda6560.js → ar.ad5c8822a8.js} +2 -2
- package/dist/client/static/js/async/cx.9c8268d542.js +226 -0
- package/dist/client/static/js/async/e.6443e739a2.js +79 -0
- package/dist/client/static/js/async/f7.46971e9d6d.js +2 -0
- package/dist/client/static/js/async/g.ee41bfed7f.js +2 -0
- package/dist/client/static/js/async/h1.d7091cef78.js +1 -0
- package/dist/client/static/js/async/hs.e7c59d8090.js +41 -0
- package/dist/client/static/js/async/jl.4911fa3383.js +1 -0
- package/dist/client/static/js/async/{k.7466330d1a.js → k.d16cd09ae7.js} +2 -2
- package/dist/client/static/js/async/{m.c038c71b9f.js → m.2e1ea3db9e.js} +2 -2
- package/dist/client/static/js/async/m3.d2b05f9604.js +2 -0
- package/dist/client/static/js/async/mt.853be2b5aa.js +1 -0
- package/dist/client/static/js/async/{oc.9c060c9779.js → oc.eb22093f05.js} +1 -1
- package/dist/client/static/js/async/{og.634fba99ac.js → og.53f068804c.js} +1 -1
- package/dist/client/static/js/async/{5b.0d6ae50fe8.js → q.b453193538.js} +10 -10
- package/dist/client/static/js/async/q5.88b50ffa95.js +286 -0
- package/dist/client/static/js/async/q8.7f1b48e9cd.js +21 -0
- package/dist/client/static/js/async/{sy.d16be843a7.js → s.aad2ecebc6.js} +1 -1
- package/dist/client/static/js/async/{tr.e878658140.js → tr.2a0d2b7600.js} +1 -1
- package/dist/client/static/js/async/vt.20406a5604.js +1 -0
- package/dist/client/static/js/async/x1.fe8af17016.js +90 -0
- package/dist/client/static/js/async/yg.5a94e1fc8a.js +149 -0
- package/dist/client/static/js/async/yx.af2936c368.js +10 -0
- package/dist/client/static/js/index.57b39ffa7f.js +34 -0
- package/package.json +1 -1
- package/src/client/components/common/ComposerPicker.tsx +16 -4
- package/src/client/components/common/ComposerTextarea.tsx +1 -0
- package/src/client/components/common/file-icon/index.tsx +4 -1
- package/src/client/components/common/image-viewer/index.tsx +224 -0
- package/src/client/components/layout/RightActivityBar.tsx +1 -1
- package/src/client/components/layout/desktop-layout/ResizeHandle.tsx +2 -2
- package/src/client/components/layout/desktop-layout/TopNavbar.tsx +13 -2
- package/src/client/components/layout/desktop-layout/WorkspacePanels.tsx +107 -56
- package/src/client/components/layout/desktop-layout/index.tsx +25 -96
- package/src/client/components/layout/desktop-layout/resizer/index.tsx +79 -118
- package/src/client/components/layout/desktop-layout/resizer/separator-drag.ts +140 -0
- package/src/client/components/layout/session-sidebar/Header.tsx +1 -1
- package/src/client/components/layout/session-sidebar/SessionItem.tsx +26 -12
- package/src/client/components/layout/session-sidebar/SubagentList.tsx +6 -4
- package/src/client/components/mobile/mobile-right-sidebar/FullEditor.tsx +57 -43
- package/src/client/components/mobile/mobile-right-sidebar/Header.tsx +31 -24
- package/src/client/components/settings/Modal.tsx +1 -0
- package/src/client/components/settings/categories/ChatSettings.tsx +6 -2
- package/src/client/components/settings/categories/chat-settings/TitleSection.tsx +99 -0
- package/src/client/components/workspace/btw-panel/AskCard.tsx +155 -0
- package/src/client/components/workspace/btw-panel/Composer.tsx +181 -0
- package/src/client/components/workspace/btw-panel/Messages.tsx +100 -0
- package/src/client/components/workspace/btw-panel/TopicMenu.tsx +106 -0
- package/src/client/components/workspace/btw-panel/attachments.ts +80 -0
- package/src/client/components/workspace/btw-panel/index.tsx +83 -0
- package/src/client/components/workspace/chat-timeline/ComposerDock.tsx +39 -20
- package/src/client/components/workspace/chat-timeline/EmptyWorkspacePrompt.tsx +3 -0
- package/src/client/components/workspace/chat-timeline/MinimapShortcuts.tsx +49 -12
- package/src/client/components/workspace/chat-timeline/SubagentView.tsx +3 -1
- package/src/client/components/workspace/chat-timeline/TimelineBody.tsx +10 -6
- package/src/client/components/workspace/chat-timeline/chat-input/AttachmentToolbar.tsx +2 -1
- package/src/client/components/workspace/chat-timeline/chat-input/DropOverlay.tsx +17 -0
- package/src/client/components/workspace/chat-timeline/chat-input/index.tsx +67 -43
- package/src/client/components/workspace/chat-timeline/index.tsx +14 -9
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Goal.tsx +3 -3
- package/src/client/components/workspace/chat-timeline/tool-renderers/panels/Read.tsx +25 -7
- package/src/client/components/workspace/editor/Toolbar.tsx +32 -23
- package/src/client/components/workspace/editor/index.tsx +45 -36
- package/src/client/components/workspace/model-dropdown/groups.ts +35 -2
- package/src/client/components/workspace/model-dropdown/index.tsx +48 -7
- package/src/client/components/workspace/model-dropdown/use-catalog.ts +28 -4
- package/src/client/hooks/chat/btw/index.ts +180 -0
- package/src/client/hooks/chat/btw/intercept.ts +43 -0
- package/src/client/hooks/chat/btw/mode.ts +179 -0
- package/src/client/hooks/chat/composer/attachments.ts +198 -0
- package/src/client/hooks/chat/composer/drop-entries.ts +204 -0
- package/src/client/hooks/chat/composer/drop-payload.test.ts +97 -0
- package/src/client/hooks/chat/composer/drop-payload.ts +130 -0
- package/src/client/hooks/chat/composer/drop-references.ts +106 -0
- package/src/client/hooks/chat/composer/file-drop.test.ts +194 -0
- package/src/client/hooks/chat/composer/file-drop.ts +226 -0
- package/src/client/hooks/chat/composer/file-reads.test.ts +211 -0
- package/src/client/hooks/chat/composer/file-reads.ts +238 -0
- package/src/client/hooks/chat/composer/index.ts +35 -9
- package/src/client/hooks/chat/composer/pipeline.ts +181 -0
- package/src/client/hooks/chat/omp/index.ts +3 -3
- package/src/client/hooks/chat/omp/prompt-send.ts +5 -5
- package/src/client/hooks/chat/timeline/actions.ts +40 -4
- package/src/client/hooks/chat/timeline/deferred-model.test.ts +219 -0
- package/src/client/hooks/chat/timeline/deferred-model.ts +92 -0
- package/src/client/hooks/chat/timeline/index.ts +29 -9
- package/src/client/hooks/chat/timeline/prompt-text.ts +6 -1
- package/src/client/hooks/chat/timeline/send.ts +45 -70
- package/src/client/hooks/chat/timeline/session-load.ts +2 -1
- package/src/client/hooks/chat/timeline/session-pagination.ts +161 -14
- package/src/client/hooks/chat/timeline/user-turns.ts +149 -0
- package/src/client/hooks/editor/file-actions.ts +49 -0
- package/src/client/hooks/editor/use-file-editor.ts +38 -27
- package/src/client/hooks/editor/use-file-transfer.ts +78 -0
- package/src/client/hooks/workspace/available-width.ts +33 -0
- package/src/client/hooks/workspace/panel-widths.ts +34 -37
- package/src/client/tailwind.css +22 -0
- package/src/server/db.server.ts +21 -287
- package/src/server/lib/btw/frames.test.ts +68 -0
- package/src/server/lib/btw/frames.ts +56 -0
- package/src/server/lib/btw/prompt.ts +28 -0
- package/src/server/lib/btw/registry.server.ts +141 -0
- package/src/server/lib/btw/runtime.server.ts +324 -0
- package/src/server/lib/btw/schema.server.ts +44 -0
- package/src/server/lib/btw/service.server.ts +171 -0
- package/src/server/lib/btw/session-copy.server.ts +213 -0
- package/src/server/lib/btw/session-copy.test.ts +166 -0
- package/src/server/lib/btw/store.server.ts +240 -0
- package/src/server/lib/db/schema.ts +110 -0
- package/src/server/lib/db/seed.ts +160 -0
- package/src/server/lib/db/workspace-sync.ts +70 -0
- package/src/server/lib/fs/root.ts +37 -0
- package/src/server/lib/models/preferences.server.ts +110 -0
- package/src/server/lib/models/registry.server.ts +206 -0
- package/src/server/lib/omp/rpc/frame-fold.ts +207 -0
- package/src/server/lib/omp/rpc/manager.ts +27 -123
- package/src/server/lib/omp/rpc/utility.ts +7 -1
- package/src/server/lib/omp/session/auto-title.server.test.ts +137 -0
- package/src/server/lib/omp/session/auto-title.server.ts +152 -0
- package/src/server/lib/omp/session/blobs.server.test.ts +100 -0
- package/src/server/lib/omp/session/blobs.server.ts +76 -0
- package/src/server/lib/omp/session/file-scan.ts +174 -0
- package/src/server/lib/omp/session/files.test.ts +147 -0
- package/src/server/lib/omp/session/files.ts +52 -35
- package/src/server/lib/omp/session/locator.ts +2 -2
- package/src/server/lib/omp/session/merge-stored.test.ts +118 -0
- package/src/server/lib/omp/session/merge-stored.ts +171 -0
- package/src/server/lib/omp/session/messages.ts +34 -0
- package/src/server/lib/omp/session/reader.ts +2 -2
- package/src/server/lib/omp/session/subagent-presence.ts +9 -6
- package/src/server/lib/queue/delivery.server.test.ts +260 -0
- package/src/server/lib/queue/delivery.server.ts +63 -11
- package/src/server/plugins/ssr.ts +29 -2
- package/src/server/routes/btw/events.ts +70 -0
- package/src/server/routes/btw/index.ts +8 -0
- package/src/server/routes/btw/topics.ts +87 -0
- package/src/server/routes/btw/ws.ts +136 -0
- package/src/server/routes/chat/index.ts +2 -0
- package/src/server/routes/chat/session.ts +1 -115
- package/src/server/routes/chat/turns.ts +87 -0
- package/src/server/routes/fs/index.ts +4 -1
- package/src/server/routes/fs/raw.ts +85 -0
- package/src/server/routes/fs/read.ts +58 -3
- package/src/server/routes/index.ts +7 -1
- package/src/server/routes/models/root.ts +88 -215
- package/src/server/routes/settings/commands.ts +59 -18
- package/src/shared/lib/chat/attachments.test.ts +208 -0
- package/src/shared/lib/chat/attachments.ts +200 -15
- package/src/shared/lib/chat/btw/client.ts +94 -0
- package/src/shared/lib/chat/composer/client.test.ts +48 -0
- package/src/shared/lib/chat/composer/client.ts +23 -9
- package/src/shared/lib/chat/composer/commands.ts +290 -0
- package/src/shared/lib/chat/composer/composer.test.ts +195 -0
- package/src/shared/lib/chat/composer/filter.ts +21 -7
- package/src/shared/lib/chat/composer/trigger.ts +154 -21
- package/src/shared/lib/chat/omp/agent-events.ts +33 -80
- package/src/shared/lib/chat/omp/socket.ts +11 -6
- package/src/shared/lib/chat/omp/sse.ts +11 -6
- package/src/shared/lib/chat/omp/tool-results.ts +102 -0
- package/src/shared/lib/chat/omp/transport.ts +19 -3
- package/src/shared/lib/chat/text-file.ts +148 -0
- package/src/shared/lib/chat/timeline/omp-callbacks.ts +18 -32
- package/src/shared/lib/chat/timeline/stream-coalescer.ts +66 -0
- package/src/shared/lib/chat/timeline/turns.ts +7 -0
- package/src/shared/lib/fs/file-kind.ts +50 -0
- package/src/shared/lib/fs/paths.ts +19 -1
- package/src/shared/lib/markdown/math.test.ts +20 -3
- package/src/shared/lib/models/preferences.test.ts +131 -0
- package/src/shared/lib/models/preferences.ts +88 -0
- package/src/shared/lib/models/provider-models.ts +8 -1
- package/src/shared/lib/omp/session/inlined-attachments.test.ts +82 -0
- package/src/shared/lib/omp/session/mapper.ts +24 -4
- package/src/shared/lib/omp/session/messages-map.ts +16 -3
- package/src/shared/lib/omp/session/messages-parse.ts +2 -2
- package/src/shared/lib/omp/session/parse-message-blocks.ts +85 -5
- package/src/shared/lib/omp/subagent/label.test.ts +94 -0
- package/src/shared/lib/omp/subagent/label.ts +84 -0
- package/src/shared/lib/workspace/panel-widths.test.ts +113 -18
- package/src/shared/lib/workspace/panel-widths.ts +161 -30
- package/src/shared/lib/workspace/right-panels.ts +50 -11
- package/src/shared/lib/workspace/session-state/store.ts +3 -0
- package/src/shared/types/btw.ts +65 -0
- package/src/shared/types/chat.ts +77 -66
- package/src/shared/types/index.ts +2 -0
- package/src/shared/types/models.ts +101 -0
- package/src/shared/types/omp/agent.ts +14 -1
- package/src/shared/types/omp/session.ts +4 -1
- package/src/shared/types/omp/subagent.ts +4 -0
- package/src/shared/types/settings/command.ts +13 -0
- package/src/shared/types/settings/state.ts +5 -0
- package/dist/client/static/css/index.e880517fcd.css +0 -2
- package/dist/client/static/js/async/5bh.8b2363424f.js +0 -1
- package/dist/client/static/js/async/66.5dfa29f957.js +0 -93
- package/dist/client/static/js/async/7g.a7814350c3.js +0 -1
- package/dist/client/static/js/async/7l.7a6a377ecc.js +0 -1
- package/dist/client/static/js/async/cx.ca5feec570.js +0 -226
- package/dist/client/static/js/async/e.93214d622b.js +0 -79
- package/dist/client/static/js/async/g.a06ca3b3e9.js +0 -2
- package/dist/client/static/js/async/h1.e8d5aa8e80.js +0 -1
- package/dist/client/static/js/async/hs.6d480d0ded.js +0 -41
- package/dist/client/static/js/async/jl.874d352278.js +0 -1
- package/dist/client/static/js/async/mt.0352e976d8.js +0 -1
- package/dist/client/static/js/async/nam.b5663e919f.js +0 -10
- package/dist/client/static/js/async/q.23af2a358f.js +0 -21
- package/dist/client/static/js/async/q5.e683f7c26e.js +0 -286
- package/dist/client/static/js/async/s.222ee974f6.js +0 -2
- package/dist/client/static/js/async/uf.46ad6209fa.js +0 -2
- package/dist/client/static/js/async/vt.42416b707d.js +0 -1
- package/dist/client/static/js/async/x1.da8687159b.js +0 -90
- package/dist/client/static/js/async/yg.35bebd6d0c.js +0 -149
- package/dist/client/static/js/index.0de15c5e17.js +0 -33
- package/dist/client/static/js/jp.50817896f3.js +0 -139
- /package/dist/client/static/js/{jp.50817896f3.js.LICENSE.txt → 77.8022a800b5.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{1k.5974df3b87.js.LICENSE.txt → 1k.a411fe5b43.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{3y.71f2ccf3b6.js.LICENSE.txt → 3y.3186ecdd63.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{dc.856fda6560.js.LICENSE.txt → ar.ad5c8822a8.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{m.c038c71b9f.js.LICENSE.txt → f7.46971e9d6d.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{5b.0d6ae50fe8.js.LICENSE.txt → g.ee41bfed7f.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{q.23af2a358f.js.LICENSE.txt → m.2e1ea3db9e.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{s.222ee974f6.js.LICENSE.txt → m3.d2b05f9604.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{g.a06ca3b3e9.js.LICENSE.txt → q.b453193538.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{uf.46ad6209fa.js.LICENSE.txt → q8.7f1b48e9cd.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/async/{nam.b5663e919f.js.LICENSE.txt → yx.af2936c368.js.LICENSE.txt} +0 -0
- /package/dist/client/static/js/{index.0de15c5e17.js.LICENSE.txt → index.57b39ffa7f.js.LICENSE.txt} +0 -0
|
@@ -0,0 +1,206 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Assembly of the chat model registry that `GET /api/models` serves: the omp
|
|
8
|
+
* registry (auth providers + models.yml) narrowed by the provider/model hides
|
|
9
|
+
* the operator set, plus the live session's default model.
|
|
10
|
+
*
|
|
11
|
+
* Extracted from `routes/models/root.ts` so the route keeps only its loader and
|
|
12
|
+
* action, and so the preference store can share the same model list.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { getDb } from '@/server/db.server';
|
|
16
|
+
import { PROVIDERS_SETTINGS_KEY } from '@/server/lib/models/provider-registry.server';
|
|
17
|
+
import { readSettingsJson } from '@/server/lib/db/settings-store';
|
|
18
|
+
import { readDisabledProviders } from '@/server/lib/omp/config/roles';
|
|
19
|
+
import { runUtilityCommand, type OmpModel } from '@/server/lib/omp/rpc/utility';
|
|
20
|
+
import { invalidateModelsCaches } from '@/shared/lib/models/server-cache';
|
|
21
|
+
import type { ModelsData, ProviderItem } from '@/shared/types';
|
|
22
|
+
|
|
23
|
+
const MODELS_CACHE_TTL_MS = 60_000;
|
|
24
|
+
export const SAFE_MODEL_LOAD_FAILURE_MESSAGE = 'Model list is temporarily unavailable. Check your configuration and try again.';
|
|
25
|
+
|
|
26
|
+
const modelNameCollator = new Intl.Collator(undefined, { numeric: true, sensitivity: 'base' });
|
|
27
|
+
|
|
28
|
+
function compareModelEntries(a: { id: string; name: string; provider: string }, b: { id: string; name: string; provider: string }): number {
|
|
29
|
+
return modelNameCollator.compare(a.name || a.id, b.name || b.id)
|
|
30
|
+
|| modelNameCollator.compare(a.provider, b.provider)
|
|
31
|
+
|| modelNameCollator.compare(a.id, b.id);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
// "off" is always a valid selector; the concrete efforts come from the model's
|
|
35
|
+
// baked thinking metadata (omp: getSupportedEfforts = reasoning ? efforts : []).
|
|
36
|
+
function thinkingLevelsFor(model: OmpModel): string[] {
|
|
37
|
+
if (!model.reasoning) return ['off'];
|
|
38
|
+
return ['off', ...(model.thinking?.efforts ?? [])];
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function supportsFastMode(model: OmpModel): boolean {
|
|
42
|
+
return model.provider === 'anthropic' || model.provider === 'openai' || model.provider === 'google';
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Disabled providers recorded in the chamber's own provider overlay. Used only
|
|
47
|
+
* by the MOCK path of `/api/models`, which has no config.yml `disabledProviders`
|
|
48
|
+
* to read — real mode gets the same answer natively.
|
|
49
|
+
*/
|
|
50
|
+
function isDisabledStoredProvider(value: unknown): value is ProviderItem {
|
|
51
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
52
|
+
const candidate = value as ProviderItem;
|
|
53
|
+
return typeof candidate.slug === 'string' && candidate.disabled === true;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function readStoredProvidersForModels(): Promise<ProviderItem[]> {
|
|
57
|
+
try {
|
|
58
|
+
const db = await getDb();
|
|
59
|
+
const parsed = await readSettingsJson<unknown>(db, PROVIDERS_SETTINGS_KEY, null);
|
|
60
|
+
if (!Array.isArray(parsed)) return [];
|
|
61
|
+
return parsed.filter(isDisabledStoredProvider);
|
|
62
|
+
} catch {
|
|
63
|
+
return [];
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function hiddenModelKey(provider: string, modelId: string): string {
|
|
68
|
+
return `${provider.trim().toLowerCase()}:${modelId}`;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function readHiddenModelKeys(): Promise<Set<string>> {
|
|
72
|
+
try {
|
|
73
|
+
const db = await getDb();
|
|
74
|
+
const parsed = await readSettingsJson<unknown>(db, PROVIDERS_SETTINGS_KEY, null);
|
|
75
|
+
if (!Array.isArray(parsed)) return new Set();
|
|
76
|
+
const hidden = new Set<string>();
|
|
77
|
+
for (const provider of parsed) {
|
|
78
|
+
if (typeof provider !== 'object' || provider === null) continue;
|
|
79
|
+
const { slug, models } = provider as { slug?: unknown; models?: unknown };
|
|
80
|
+
if (typeof slug !== 'string' || !Array.isArray(models)) continue;
|
|
81
|
+
for (const model of models) {
|
|
82
|
+
if (typeof model !== 'object' || model === null) continue;
|
|
83
|
+
const { id, isVisible } = model as { id?: unknown; isVisible?: unknown };
|
|
84
|
+
if (typeof id === 'string' && isVisible === false) hidden.add(hiddenModelKey(slug, id));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return hidden;
|
|
88
|
+
} catch {
|
|
89
|
+
return new Set();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* A model is offered only when its provider is neither disabled in omp's
|
|
95
|
+
* config.yml (`disabledProviders`) nor carrying a per-model hide in the
|
|
96
|
+
* chamber overlay. Disabling the provider is the coarse switch — Settings →
|
|
97
|
+
* Providers flips it so the whole provider leaves the chat picker.
|
|
98
|
+
*/
|
|
99
|
+
function filterSelectableModels(
|
|
100
|
+
available: OmpModel[],
|
|
101
|
+
disabledProviders: Set<string>,
|
|
102
|
+
hiddenModelKeys: Set<string>,
|
|
103
|
+
): OmpModel[] {
|
|
104
|
+
return available.filter((model) => (
|
|
105
|
+
!disabledProviders.has(model.provider)
|
|
106
|
+
&& !hiddenModelKeys.has(hiddenModelKey(model.provider, model.id))
|
|
107
|
+
));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function isOmpModelEntry(value: unknown): value is OmpModel {
|
|
111
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
112
|
+
const candidate = value as OmpModel;
|
|
113
|
+
return typeof candidate.id === 'string' && typeof candidate.provider === 'string';
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function isLoginProvider(value: unknown): value is { id: string; name: string; authenticated: boolean } {
|
|
117
|
+
if (typeof value !== 'object' || value === null) return false;
|
|
118
|
+
const candidate = value as { id?: unknown; name?: unknown; authenticated?: unknown };
|
|
119
|
+
return typeof candidate.id === 'string'
|
|
120
|
+
&& typeof candidate.name === 'string'
|
|
121
|
+
&& typeof candidate.authenticated === 'boolean';
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
async function loadModels(): Promise<ModelsData> {
|
|
125
|
+
const availableResponse = await runUtilityCommand<{ models?: unknown }>(
|
|
126
|
+
{ type: 'get_available_models' },
|
|
127
|
+
120_000,
|
|
128
|
+
);
|
|
129
|
+
const available = Array.isArray(availableResponse.models)
|
|
130
|
+
? availableResponse.models
|
|
131
|
+
.filter(isOmpModelEntry)
|
|
132
|
+
.map((model) => ({
|
|
133
|
+
...model,
|
|
134
|
+
name: typeof model.name === 'string' && model.name.trim().length > 0 ? model.name : model.id,
|
|
135
|
+
}))
|
|
136
|
+
: [];
|
|
137
|
+
|
|
138
|
+
const disabledProviders = await readDisabledProviders();
|
|
139
|
+
const hiddenModelKeys = await readHiddenModelKeys();
|
|
140
|
+
const nameMap: Record<string, string> = {};
|
|
141
|
+
const thinkingLevels: Record<string, string[]> = {};
|
|
142
|
+
const modelList = filterSelectableModels(available, disabledProviders, hiddenModelKeys)
|
|
143
|
+
.map((m) => ({
|
|
144
|
+
id: m.id,
|
|
145
|
+
name: m.name,
|
|
146
|
+
provider: m.provider,
|
|
147
|
+
thinkingLevels: thinkingLevelsFor(m),
|
|
148
|
+
supportsFastMode: supportsFastMode(m),
|
|
149
|
+
contextWindow: m.contextWindow ?? undefined,
|
|
150
|
+
maxTokens: m.maxTokens ?? undefined,
|
|
151
|
+
cost: m.cost,
|
|
152
|
+
}))
|
|
153
|
+
.sort(compareModelEntries);
|
|
154
|
+
|
|
155
|
+
const loginResponse = await runUtilityCommand<{ providers?: unknown }>(
|
|
156
|
+
{ type: 'get_login_providers' },
|
|
157
|
+
30_000,
|
|
158
|
+
);
|
|
159
|
+
const loginProviders = Array.isArray(loginResponse.providers)
|
|
160
|
+
? loginResponse.providers.filter(isLoginProvider)
|
|
161
|
+
: [];
|
|
162
|
+
const connectedProviders = loginProviders
|
|
163
|
+
.filter((provider) => provider.authenticated)
|
|
164
|
+
.map((provider) => ({ id: provider.id, name: provider.name, disabled: disabledProviders.has(provider.id) }));
|
|
165
|
+
for (const m of available) {
|
|
166
|
+
const key = `${m.provider}:${m.id}`;
|
|
167
|
+
nameMap[key] = m.name;
|
|
168
|
+
thinkingLevels[key] = thinkingLevelsFor(m);
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
let defaultModel: { provider: string; modelId: string } | null = null;
|
|
172
|
+
try {
|
|
173
|
+
const state = await runUtilityCommand<{ model?: { provider?: string; id?: string } }>(
|
|
174
|
+
{ type: 'get_state' },
|
|
175
|
+
30_000,
|
|
176
|
+
);
|
|
177
|
+
const provider = state.model?.provider;
|
|
178
|
+
const modelId = state.model?.id;
|
|
179
|
+
if (provider && modelId && available.some((m) => m.provider === provider && m.id === modelId)) {
|
|
180
|
+
defaultModel = { provider, modelId };
|
|
181
|
+
}
|
|
182
|
+
} catch {
|
|
183
|
+
// Default model is cosmetic — the models list is still useful without it.
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
return { models: nameMap, modelList, defaultModel, thinkingLevels, connectedProviders };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
export const EMPTY_MODELS: ModelsData = {
|
|
190
|
+
models: {},
|
|
191
|
+
modelList: [],
|
|
192
|
+
defaultModel: null,
|
|
193
|
+
thinkingLevels: {},
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
export async function loadModelsWithCache(): Promise<ModelsData> {
|
|
197
|
+
const cached = globalThis.__ompChamberModelsCache;
|
|
198
|
+
if (cached && cached.expiresAt > Date.now()) return cached.data;
|
|
199
|
+
const data = await loadModels();
|
|
200
|
+
globalThis.__ompChamberModelsCache = { data, expiresAt: Date.now() + MODELS_CACHE_TTL_MS };
|
|
201
|
+
return data;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
export function invalidateModelsCache(): void {
|
|
205
|
+
invalidateModelsCaches();
|
|
206
|
+
}
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Folds one omp RPC frame into the session wrapper's runtime state, and runs
|
|
8
|
+
* the settle-time side effects a terminal run triggers.
|
|
9
|
+
*
|
|
10
|
+
* Mirrors the client's `foldAgentEvent` (shared/lib/chat/omp/agent-events.ts):
|
|
11
|
+
* the same shape of problem — one frame, many state flags — solved the same way
|
|
12
|
+
* so both ends of the transport read alike. Split out of rpc-manager.ts so the
|
|
13
|
+
* wrapper owns only process lifecycle.
|
|
14
|
+
*
|
|
15
|
+
* The state machine is the load-bearing part. `turn_end` fires for EVERY turn
|
|
16
|
+
* in a multi-turn run, `agent_end` is the only frame that knows whether the run
|
|
17
|
+
* is terminal, and a `prompt` response that failed must clear the same flags a
|
|
18
|
+
* successful settle does — a missed clear strands the session as "running" and
|
|
19
|
+
* the idle reclaim can never take it back.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { clearSessionFileCaches } from '@/server/lib/omp/session/files';
|
|
23
|
+
import {
|
|
24
|
+
consumeAutoTitleOutput,
|
|
25
|
+
triggerAutoSessionTitle,
|
|
26
|
+
type AutoTitleHost,
|
|
27
|
+
} from '@/server/lib/omp/session/auto-title.server';
|
|
28
|
+
import { scheduleQueueDelivery, type QueueDeliveryHost } from '@/server/lib/queue/delivery.server';
|
|
29
|
+
import { loadStreamStatuses, markStreamStatus } from '@/shared/lib/omp/session/stream-state.server';
|
|
30
|
+
import { NON_TERMINAL_CONTINUATION_GRACE_MS, type AgentEvent } from '@/server/lib/omp/rpc/constants';
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Wrapper surface the fold reads and writes. Every field is owned by the
|
|
34
|
+
* wrapper; the fold mutates them in place exactly as the inline switch did.
|
|
35
|
+
* Composes the two host contracts the settle path hands the wrapper to
|
|
36
|
+
* (auto-title, queue delivery) so the wrapper itself satisfies this whole type.
|
|
37
|
+
*/
|
|
38
|
+
export interface SessionFrameHost extends AutoTitleHost, QueueDeliveryHost {
|
|
39
|
+
promptRunning: boolean;
|
|
40
|
+
streaming: boolean;
|
|
41
|
+
compacting: boolean;
|
|
42
|
+
awaitingAgentStart: boolean;
|
|
43
|
+
awaitingAgentStartDeadline: number;
|
|
44
|
+
continuationGraceUntil: number;
|
|
45
|
+
emit(event: AgentEvent): void;
|
|
46
|
+
/** Remember a blocking ask/approval dialog so a reattaching client can answer it. */
|
|
47
|
+
trackUiDialog(frame: AgentEvent): void;
|
|
48
|
+
/** Fold a subagent frame into the liveness roster. */
|
|
49
|
+
observeSubagent(frame: AgentEvent, now: number): void;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** What the caller must do after the fold. */
|
|
53
|
+
export interface FrameFoldResult {
|
|
54
|
+
/** The session list is stale and needs a rebuild. */
|
|
55
|
+
refreshSessionList: boolean;
|
|
56
|
+
/** The fold already handled this frame's user-visible effect and the caller
|
|
57
|
+
* must NOT forward it: a failed `prompt` response has emitted its own
|
|
58
|
+
* `prompt_error`, and a `command_output` belonging to the chamber's own
|
|
59
|
+
* background rename must stay out of the timeline. */
|
|
60
|
+
suppressForward: boolean;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** True when the ending turn was a user abort rather than a natural stop. */
|
|
64
|
+
function endedAborted(messages: unknown): boolean {
|
|
65
|
+
return (
|
|
66
|
+
Array.isArray(messages) &&
|
|
67
|
+
messages.some((entry) => {
|
|
68
|
+
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;
|
|
69
|
+
return (entry as Record<string, unknown>).stopReason === 'aborted';
|
|
70
|
+
})
|
|
71
|
+
);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Write the terminal stream badge from the ending turn's own stopReason.
|
|
76
|
+
*
|
|
77
|
+
* The `abort` command already wrote `abort` at dispatch time, but the
|
|
78
|
+
* `agent_end` frame arrives later and previously flattened it to `finish`.
|
|
79
|
+
* Re-check the current row and only upgrade `stream` rows, so a `finish`
|
|
80
|
+
* written here can never clobber a newer run's live `stream`/`abort`.
|
|
81
|
+
*/
|
|
82
|
+
async function markEndStatus(sessionId: string, messages: unknown): Promise<void> {
|
|
83
|
+
if (!sessionId) return;
|
|
84
|
+
const current = await loadStreamStatuses();
|
|
85
|
+
if (current[sessionId] !== 'stream') return;
|
|
86
|
+
await markStreamStatus(sessionId, endedAborted(messages) ? 'abort' : 'finish');
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** Apply one frame to the wrapper's runtime state. */
|
|
90
|
+
export function foldSessionFrame(host: SessionFrameHost, event: AgentEvent): FrameFoldResult {
|
|
91
|
+
const result: FrameFoldResult = { refreshSessionList: false, suppressForward: false };
|
|
92
|
+
|
|
93
|
+
switch (event.type) {
|
|
94
|
+
case 'agent_start':
|
|
95
|
+
host.promptRunning = true;
|
|
96
|
+
host.streaming = true;
|
|
97
|
+
host.awaitingAgentStart = false;
|
|
98
|
+
host.awaitingAgentStartDeadline = 0;
|
|
99
|
+
host.continuationGraceUntil = 0;
|
|
100
|
+
clearSessionFileCaches();
|
|
101
|
+
result.refreshSessionList = true;
|
|
102
|
+
if (host.sessionId) void markStreamStatus(host.sessionId, 'stream');
|
|
103
|
+
break;
|
|
104
|
+
case 'turn_start':
|
|
105
|
+
// Redundant with agent_start in the happy path, but the run-level
|
|
106
|
+
// counterpart can be missed (e.g. the state was cleared by a command
|
|
107
|
+
// between frames). The turn is proof enough the session is live.
|
|
108
|
+
if (host.sessionId) void markStreamStatus(host.sessionId, 'stream');
|
|
109
|
+
break;
|
|
110
|
+
case 'message_start':
|
|
111
|
+
// Same recovery as turn_start: a message frame is only emitted inside a
|
|
112
|
+
// live run, so its arrival re-arms the `stream` row no matter what stale
|
|
113
|
+
// status sits there (upsert overwrites any terminal badge).
|
|
114
|
+
if (host.sessionId) void markStreamStatus(host.sessionId, 'stream');
|
|
115
|
+
break;
|
|
116
|
+
case 'turn_end': {
|
|
117
|
+
// A multi-turn run emits turn_end for EVERY turn — intermediates end with
|
|
118
|
+
// `toolUse`/`stop` while the run keeps going (verified against omp
|
|
119
|
+
// 18.2.8: agent_start → turn_start → turn_end('toolUse') → turn_start →
|
|
120
|
+
// turn_end('stop') → agent_end, and the frame carries no isTerminal
|
|
121
|
+
// field). Only `aborted` is unambiguous here: the run is over, so the
|
|
122
|
+
// abort badge can be written before agent_end arrives. Everything else
|
|
123
|
+
// waits for agent_end, which knows isTerminal.
|
|
124
|
+
if (!host.sessionId) break;
|
|
125
|
+
const turn = event.message as Record<string, unknown> | undefined;
|
|
126
|
+
if (turn?.stopReason === 'aborted') void markStreamStatus(host.sessionId, 'abort');
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
case 'agent_end':
|
|
130
|
+
if (event.isTerminal !== false) {
|
|
131
|
+
host.streaming = false;
|
|
132
|
+
host.promptRunning = false;
|
|
133
|
+
host.awaitingAgentStart = false;
|
|
134
|
+
host.awaitingAgentStartDeadline = 0;
|
|
135
|
+
host.continuationGraceUntil = 0;
|
|
136
|
+
clearSessionFileCaches();
|
|
137
|
+
const aborted = endedAborted(event.messages);
|
|
138
|
+
if (host.sessionId) void markEndStatus(host.sessionId, event.messages);
|
|
139
|
+
// A settled run is the moment a session becomes nameable: omp's own
|
|
140
|
+
// first-message titling is suppressed under `--mode rpc-ui`
|
|
141
|
+
// (PI_NO_TITLE), so the chamber asks for the same title itself. The
|
|
142
|
+
// trigger is a no-op once the session carries a name — see
|
|
143
|
+
// triggerAutoSessionTitle for why that check is the safety property.
|
|
144
|
+
if (!aborted) void triggerAutoSessionTitle(host);
|
|
145
|
+
// The run truly ended — the server, not the browser, decides whether a
|
|
146
|
+
// queued follow-up goes out next. A user-aborted run holds the queue
|
|
147
|
+
// (stop-all semantics): the next run end or an explicit send picks it up.
|
|
148
|
+
if (!aborted) scheduleQueueDelivery(host);
|
|
149
|
+
} else {
|
|
150
|
+
host.continuationGraceUntil = Date.now() + NON_TERMINAL_CONTINUATION_GRACE_MS;
|
|
151
|
+
}
|
|
152
|
+
break;
|
|
153
|
+
case 'prompt_result':
|
|
154
|
+
host.promptRunning = false;
|
|
155
|
+
host.awaitingAgentStart = false;
|
|
156
|
+
host.awaitingAgentStartDeadline = 0;
|
|
157
|
+
break;
|
|
158
|
+
case 'auto_compaction_start':
|
|
159
|
+
host.compacting = true;
|
|
160
|
+
break;
|
|
161
|
+
case 'auto_compaction_end':
|
|
162
|
+
host.compacting = false;
|
|
163
|
+
clearSessionFileCaches();
|
|
164
|
+
break;
|
|
165
|
+
case 'command_output':
|
|
166
|
+
// A `command_output` for a command the OPERATOR typed renders as a notice
|
|
167
|
+
// row. But the chamber fires its own `/rename` after a settled run, and
|
|
168
|
+
// omp answers it on the same frame — "Session renamed to …", or "Could not
|
|
169
|
+
// generate a session title" for a low-signal first message. Neither was
|
|
170
|
+
// asked for, and the second would be pure noise. The frame carries no
|
|
171
|
+
// correlation id, so attribution is by the request window.
|
|
172
|
+
result.suppressForward = consumeAutoTitleOutput(host);
|
|
173
|
+
break;
|
|
174
|
+
case 'session_info_update':
|
|
175
|
+
// A rename rewrote the fixed-width title slot in place; the scan cache is
|
|
176
|
+
// keyed on file mtime, which that write cannot move, so the list must be
|
|
177
|
+
// rebuilt explicitly.
|
|
178
|
+
clearSessionFileCaches();
|
|
179
|
+
result.refreshSessionList = true;
|
|
180
|
+
break;
|
|
181
|
+
case 'extension_ui_request':
|
|
182
|
+
host.trackUiDialog(event);
|
|
183
|
+
break;
|
|
184
|
+
// Subagent frames carry no turn state, but they are the only proof that
|
|
185
|
+
// work is still running once the parent turn has ended.
|
|
186
|
+
case 'subagent_lifecycle':
|
|
187
|
+
case 'subagent_progress':
|
|
188
|
+
case 'subagent_event':
|
|
189
|
+
host.observeSubagent(event, Date.now());
|
|
190
|
+
break;
|
|
191
|
+
case 'response': {
|
|
192
|
+
if (event.success === false && event.command === 'prompt') {
|
|
193
|
+
host.promptRunning = false;
|
|
194
|
+
host.awaitingAgentStart = false;
|
|
195
|
+
host.awaitingAgentStartDeadline = 0;
|
|
196
|
+
host.emit({ type: 'prompt_error', errorMessage: (event.error as string) ?? 'Prompt failed' });
|
|
197
|
+
// The caller's trailing notify covers this: promptRunning just flipped,
|
|
198
|
+
// so the running snapshot changes and the listeners fire. Suppress only
|
|
199
|
+
// the forward, or the error would land twice.
|
|
200
|
+
result.suppressForward = true;
|
|
201
|
+
}
|
|
202
|
+
break;
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
return result;
|
|
207
|
+
}
|
|
@@ -17,13 +17,12 @@
|
|
|
17
17
|
|
|
18
18
|
import { RpcProcess, type RpcFrame } from '@/server/lib/omp/rpc/process';
|
|
19
19
|
import { PendingUiDialogs } from '@/server/lib/omp/rpc/pending-ui-dialogs';
|
|
20
|
-
import {
|
|
20
|
+
import { foldSessionFrame } from '@/server/lib/omp/rpc/frame-fold';
|
|
21
21
|
import { notifyRunningChange } from '@/server/lib/omp/rpc/session-registry';
|
|
22
22
|
import { dispatchSessionCommand } from '@/server/lib/omp/rpc/session-commands';
|
|
23
|
-
import { scheduleQueueDelivery } from '@/server/lib/queue/delivery.server';
|
|
24
23
|
import { SubagentLiveness } from '@/server/lib/omp/rpc/subagent-liveness';
|
|
25
|
-
import {
|
|
26
|
-
import { GET_STATE_TIMEOUT_MS, IDLE_DESTROY_MS,
|
|
24
|
+
import { markStreamStatus } from '@/shared/lib/omp/session/stream-state.server';
|
|
25
|
+
import { GET_STATE_TIMEOUT_MS, IDLE_DESTROY_MS, READY_TIMEOUT_MS, SUBAGENT_STALE_MS, type AgentEvent, type EventListener, type RpcSessionState } from '@/server/lib/omp/rpc/constants';
|
|
27
26
|
|
|
28
27
|
export type {
|
|
29
28
|
AgentEvent,
|
|
@@ -68,6 +67,13 @@ export class AgentSessionWrapper {
|
|
|
68
67
|
// set in initialize(). No other flag on this wrapper can see them, and an
|
|
69
68
|
// idle reclaim or a spawn-mode reconcile must never kill a running subagent.
|
|
70
69
|
private readonly subagents = new SubagentLiveness();
|
|
70
|
+
// One background `/rename` at a time. A settled run may be followed by
|
|
71
|
+
// another (queue delivery, a second prompt), and two overlapping generations
|
|
72
|
+
// would race to write the same title slot.
|
|
73
|
+
autoTitleInFlight = false;
|
|
74
|
+
// Epoch ms until which `command_output` frames belong to our own background
|
|
75
|
+
// rename and must not reach the timeline. 0 when nothing is outstanding.
|
|
76
|
+
autoTitleWindowUntil = 0;
|
|
71
77
|
readonly idleDestroyMs: number;
|
|
72
78
|
proc: RpcProcess;
|
|
73
79
|
readonly cwd: string;
|
|
@@ -173,130 +179,28 @@ export class AgentSessionWrapper {
|
|
|
173
179
|
this.destroy();
|
|
174
180
|
}
|
|
175
181
|
|
|
176
|
-
/**
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;
|
|
185
|
-
return (entry as Record<string, unknown>).stopReason === 'aborted';
|
|
186
|
-
});
|
|
187
|
-
const current = await loadStreamStatuses();
|
|
188
|
-
if (current[this.sessionId] !== 'stream') return;
|
|
189
|
-
await markStreamStatus(this.sessionId, aborted ? 'abort' : 'finish');
|
|
182
|
+
/** SessionFrameHost adapter: remember a blocking dialog for reattaching clients. */
|
|
183
|
+
trackUiDialog(frame: AgentEvent): void {
|
|
184
|
+
this.pendingUiDialogs.track(frame);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/** SessionFrameHost adapter: fold a subagent frame into the liveness roster. */
|
|
188
|
+
observeSubagent(frame: AgentEvent, now: number): void {
|
|
189
|
+
this.subagents.observe(frame, now);
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
private handleFrame(frame: RpcFrame): void {
|
|
193
193
|
this.resetIdleTimer();
|
|
194
194
|
const event = frame as AgentEvent;
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
clearSessionFileCaches();
|
|
205
|
-
refreshSessionList = true;
|
|
206
|
-
if (this.sessionId) void markStreamStatus(this.sessionId, 'stream');
|
|
207
|
-
break;
|
|
208
|
-
case 'turn_start':
|
|
209
|
-
// Redundant with agent_start in the happy path, but the run-level
|
|
210
|
-
// counterpart can be missed (e.g. the state was cleared by a command
|
|
211
|
-
// between frames). The turn is proof enough the session is live.
|
|
212
|
-
if (this.sessionId) void markStreamStatus(this.sessionId, 'stream');
|
|
213
|
-
break;
|
|
214
|
-
case 'message_start':
|
|
215
|
-
// Same recovery as turn_start: a message frame is only emitted inside
|
|
216
|
-
// a live run, so its arrival re-arms the `stream` row no matter what
|
|
217
|
-
// stale status sits there (upsert overwrites any terminal badge).
|
|
218
|
-
if (this.sessionId) void markStreamStatus(this.sessionId, 'stream');
|
|
219
|
-
break;
|
|
220
|
-
case 'turn_end': {
|
|
221
|
-
// A multi-turn run emits turn_end for EVERY turn — intermediates end
|
|
222
|
-
// with `toolUse`/`stop` while the run keeps going (verified against
|
|
223
|
-
// omp 18.2.8: agent_start → turn_start → turn_end('toolUse') →
|
|
224
|
-
// turn_start → turn_end('stop') → agent_end, and the frame carries no
|
|
225
|
-
// isTerminal field). Only `aborted` is unambiguous here: the run is
|
|
226
|
-
// over, so the abort badge can be written before agent_end arrives.
|
|
227
|
-
// Everything else waits for agent_end, which knows isTerminal.
|
|
228
|
-
if (!this.sessionId) break;
|
|
229
|
-
const turn = event.message as Record<string, unknown> | undefined;
|
|
230
|
-
if (turn?.stopReason === 'aborted') void markStreamStatus(this.sessionId, 'abort');
|
|
231
|
-
break;
|
|
232
|
-
}
|
|
233
|
-
case 'agent_end':
|
|
234
|
-
if (event.isTerminal !== false) {
|
|
235
|
-
this.streaming = false;
|
|
236
|
-
this.promptRunning = false;
|
|
237
|
-
this.awaitingAgentStart = false;
|
|
238
|
-
this.awaitingAgentStartDeadline = 0;
|
|
239
|
-
this.continuationGraceUntil = 0;
|
|
240
|
-
clearSessionFileCaches();
|
|
241
|
-
// The turn's own stopReason is the ground truth for how it ended:
|
|
242
|
-
// `aborted` = user stop, anything else (`stop`, `toolUse`, and the
|
|
243
|
-
// ambiguous `error` that tool failures also produce) completes as
|
|
244
|
-
// `finish`. This must not overwrite a fresh `abort` row from a NEWER
|
|
245
|
-
// run — guard against downgrades by checking the current row first.
|
|
246
|
-
const aborted = Array.isArray(event.messages) && event.messages.some((entry) => {
|
|
247
|
-
if (!entry || typeof entry !== 'object' || Array.isArray(entry)) return false;
|
|
248
|
-
return (entry as Record<string, unknown>).stopReason === 'aborted';
|
|
249
|
-
});
|
|
250
|
-
if (this.sessionId) void this.markEndStatus(event.messages);
|
|
251
|
-
// The run truly ended — the server, not the browser, decides whether
|
|
252
|
-
// a queued follow-up goes out next. A user-aborted run holds the
|
|
253
|
-
// queue (stop-all semantics): the next run end or an explicit send
|
|
254
|
-
// picks it up.
|
|
255
|
-
if (!aborted) scheduleQueueDelivery(this);
|
|
256
|
-
} else {
|
|
257
|
-
this.continuationGraceUntil = Date.now() + NON_TERMINAL_CONTINUATION_GRACE_MS;
|
|
258
|
-
}
|
|
259
|
-
break;
|
|
260
|
-
case 'prompt_result':
|
|
261
|
-
this.promptRunning = false;
|
|
262
|
-
this.awaitingAgentStart = false;
|
|
263
|
-
this.awaitingAgentStartDeadline = 0;
|
|
264
|
-
break;
|
|
265
|
-
case 'auto_compaction_start':
|
|
266
|
-
this.compacting = true;
|
|
267
|
-
break;
|
|
268
|
-
case 'auto_compaction_end':
|
|
269
|
-
this.compacting = false;
|
|
270
|
-
clearSessionFileCaches();
|
|
271
|
-
break;
|
|
272
|
-
case 'session_info_update':
|
|
273
|
-
clearSessionFileCaches();
|
|
274
|
-
refreshSessionList = true;
|
|
275
|
-
break;
|
|
276
|
-
case 'extension_ui_request':
|
|
277
|
-
this.pendingUiDialogs.track(event);
|
|
278
|
-
break;
|
|
279
|
-
// Subagent frames carry no turn state, but they are the only proof that
|
|
280
|
-
// work is still running once the parent turn has ended.
|
|
281
|
-
case 'subagent_lifecycle':
|
|
282
|
-
case 'subagent_progress':
|
|
283
|
-
case 'subagent_event':
|
|
284
|
-
this.subagents.observe(event, Date.now());
|
|
285
|
-
break;
|
|
286
|
-
case 'response': {
|
|
287
|
-
if (event.success === false && event.command === 'prompt') {
|
|
288
|
-
this.promptRunning = false;
|
|
289
|
-
this.awaitingAgentStart = false;
|
|
290
|
-
this.awaitingAgentStartDeadline = 0;
|
|
291
|
-
this.emit({ type: 'prompt_error', errorMessage: (event.error as string) ?? 'Prompt failed' });
|
|
292
|
-
notifyRunningChange();
|
|
293
|
-
return;
|
|
294
|
-
}
|
|
295
|
-
break;
|
|
296
|
-
}
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
this.emit(event);
|
|
195
|
+
// The state machine and its settle-time side effects live in frame-fold.ts;
|
|
196
|
+
// this method owns only the wrapper's own bookkeeping around it.
|
|
197
|
+
const { refreshSessionList, suppressForward } = foldSessionFrame(this, event);
|
|
198
|
+
// `suppressForward` withholds only the FRAME — a failed prompt response
|
|
199
|
+
// already emitted its own `prompt_error`, and the chamber's own background
|
|
200
|
+
// rename must not surface its diagnostics. The running-state notification
|
|
201
|
+
// still fires: the fold just cleared `promptRunning`, and that flip is what
|
|
202
|
+
// releases the sidebar's spinner.
|
|
203
|
+
if (!suppressForward) this.emit(event);
|
|
300
204
|
notifyRunningChange({ refreshSessionList });
|
|
301
205
|
}
|
|
302
206
|
|
|
@@ -22,7 +22,13 @@ import { RpcProcess } from '@/server/lib/omp/rpc/process';
|
|
|
22
22
|
|
|
23
23
|
// Extensions stay ENABLED: they can register models and login providers, and
|
|
24
24
|
// omitting them made the web UI's model/provider lists disagree with the CLI's.
|
|
25
|
-
|
|
25
|
+
//
|
|
26
|
+
// Skills stay ENABLED too. `--no-skills` made `get_available_commands` omit
|
|
27
|
+
// every `skill:<name>` entry (measured: 45 commands without, 52 with), so the
|
|
28
|
+
// composer's `/` popup could not offer the skills omp happily executes — and
|
|
29
|
+
// the chamber's own disk scan missed provider roots omp reads (`~/.agents/skills`).
|
|
30
|
+
// Cost is nil: the ready frame lands at the same ~420ms either way.
|
|
31
|
+
const UTILITY_EXTRA_ARGS = ['--no-session', '--no-lsp'];
|
|
26
32
|
const READY_TIMEOUT_MS = 60_000;
|
|
27
33
|
// Longer than the 60s models-cache TTL on purpose: with idle-kill == TTL every
|
|
28
34
|
// pause past a minute paid a cold multi-second respawn on top of the stale
|