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,324 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* One BTW topic's live side conversation.
|
|
8
|
+
*
|
|
9
|
+
* The side question runs in a second `omp --mode rpc-ui` child that resumes the
|
|
10
|
+
* topic's own transcript — a snapshot of the parent session taken when the
|
|
11
|
+
* topic first asks. That gives the parent's context as real provider messages
|
|
12
|
+
* (no re-prompting it as text, and the parent's prompt-cache prefix survives),
|
|
13
|
+
* never writes the parent file, and disables the tool surface exactly like
|
|
14
|
+
* omp's TUI `/btw` (`--no-tools`; the prompt itself says "NEVER use tools").
|
|
15
|
+
*
|
|
16
|
+
* The child is disposable by design: history lives in SQLite plus the topic
|
|
17
|
+
* transcript, so an idle reclaim costs a respawn, not the conversation — a
|
|
18
|
+
* follow-up resumes the transcript and still sees every earlier turn.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { RpcProcess, type RpcFrame } from '@/server/lib/omp/rpc/process';
|
|
22
|
+
import { GET_STATE_TIMEOUT_MS, IDLE_DESTROY_MS, PROMPT_ACK_TIMEOUT_MS, READY_TIMEOUT_MS, type RpcSessionState } from '@/server/lib/omp/rpc/constants';
|
|
23
|
+
import { resolveOmpBin } from '@/server/lib/omp/core/cli';
|
|
24
|
+
import { buildBtwPrompt } from '@/server/lib/btw/prompt';
|
|
25
|
+
import { appendBtwTurn, setBtwTopicLeaf, setBtwTopicModel, settleRunningBtwTurns, updateBtwTurn } from '@/server/lib/btw/store.server';
|
|
26
|
+
import { createBtwWorkspace, resolveBtwWorkspacePaths, type BtwWorkspace } from '@/server/lib/btw/session-copy.server';
|
|
27
|
+
import { isRecord } from '@/shared/lib/util/guards';
|
|
28
|
+
import { finalAnswer, finalStatus, isAnswerableUiMethod } from '@/server/lib/btw/frames';
|
|
29
|
+
import type { AgentImage, BtwFrame, BtwModel, BtwTurnStatus } from '@/shared/types';
|
|
30
|
+
|
|
31
|
+
/** Longest an aborted turn may stay unsettled before its child is reclaimed. */
|
|
32
|
+
const ABORT_GRACE_MS = 10_000;
|
|
33
|
+
|
|
34
|
+
/** Prompt images, validated by the route before they reach the child. */
|
|
35
|
+
export type BtwImages = AgentImage[];
|
|
36
|
+
|
|
37
|
+
export interface BtwRuntimeContext {
|
|
38
|
+
topicId: string;
|
|
39
|
+
sessionId: string;
|
|
40
|
+
/** Parent session file the topic's transcript was snapshotted from. */
|
|
41
|
+
parentSessionFile: string;
|
|
42
|
+
cwd: string;
|
|
43
|
+
model?: BtwModel;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Where the runtime reports what happened. */
|
|
47
|
+
export interface BtwRuntimeSink {
|
|
48
|
+
/** One frame for every client watching this session's btw stream. */
|
|
49
|
+
publish(frame: BtwFrame): void;
|
|
50
|
+
/** The session's topics changed — re-read the state and republish it. */
|
|
51
|
+
stateChanged(): void;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export class BtwError extends Error {
|
|
55
|
+
readonly code: string;
|
|
56
|
+
|
|
57
|
+
constructor(message: string, code: string) {
|
|
58
|
+
super(message);
|
|
59
|
+
this.name = 'BtwError';
|
|
60
|
+
this.code = code;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class BtwRuntime {
|
|
65
|
+
private proc: RpcProcess | null = null;
|
|
66
|
+
private starting: Promise<RpcProcess> | null = null;
|
|
67
|
+
private workspace: BtwWorkspace | null = null;
|
|
68
|
+
private idleTimer: NodeJS.Timeout | null = null;
|
|
69
|
+
private abortTimer: NodeJS.Timeout | null = null;
|
|
70
|
+
private lastIdleReset = 0;
|
|
71
|
+
private turnIndex: number | null = null;
|
|
72
|
+
private settling = false;
|
|
73
|
+
private answer = '';
|
|
74
|
+
private disposing: Promise<void> | null = null;
|
|
75
|
+
|
|
76
|
+
constructor(
|
|
77
|
+
private readonly context: BtwRuntimeContext,
|
|
78
|
+
private readonly sink: BtwRuntimeSink,
|
|
79
|
+
) {}
|
|
80
|
+
|
|
81
|
+
get topicId(): string {
|
|
82
|
+
return this.context.topicId;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
get sessionId(): string {
|
|
86
|
+
return this.context.sessionId;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/** A question is in flight in this topic. */
|
|
90
|
+
get running(): boolean {
|
|
91
|
+
return this.turnIndex !== null;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** True from the moment a turn starts settling until its row is written:
|
|
95
|
+
* the state repair must treat this topic as live, or it would overwrite the
|
|
96
|
+
* turn's own status with `interrupted`. */
|
|
97
|
+
get busy(): boolean {
|
|
98
|
+
return this.settling;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Ask (or follow up) — the caller has already refused concurrent turns. */
|
|
102
|
+
async ask(question: string, images?: BtwImages): Promise<void> {
|
|
103
|
+
if (this.running) throw new BtwError('A side question is already running here.', 'btw_busy');
|
|
104
|
+
this.turnIndex = await appendBtwTurn(this.topicId, question);
|
|
105
|
+
this.answer = '';
|
|
106
|
+
this.sink.stateChanged();
|
|
107
|
+
try {
|
|
108
|
+
const proc = await this.start();
|
|
109
|
+
await proc.sendCommand(
|
|
110
|
+
{ type: 'prompt', message: buildBtwPrompt(question), ...(images?.length ? { images } : {}) },
|
|
111
|
+
PROMPT_ACK_TIMEOUT_MS,
|
|
112
|
+
);
|
|
113
|
+
} catch (error) {
|
|
114
|
+
await this.settle('failed');
|
|
115
|
+
this.sink.publish({
|
|
116
|
+
type: 'btw_error',
|
|
117
|
+
topicId: this.topicId,
|
|
118
|
+
message: error instanceof Error ? error.message : String(error),
|
|
119
|
+
});
|
|
120
|
+
}
|
|
121
|
+
this.resetIdleTimer(true);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Cancel the running question, keeping whatever text already arrived. */
|
|
125
|
+
async abort(): Promise<void> {
|
|
126
|
+
if (!this.running) return;
|
|
127
|
+
const proc = this.proc;
|
|
128
|
+
if (!proc?.isAlive) {
|
|
129
|
+
await this.settle('cancelled');
|
|
130
|
+
return;
|
|
131
|
+
}
|
|
132
|
+
try {
|
|
133
|
+
await proc.sendCommand({ type: 'abort' }, GET_STATE_TIMEOUT_MS);
|
|
134
|
+
} catch {
|
|
135
|
+
// An unanswered abort is the escalate-to-cancel path below, not an error.
|
|
136
|
+
}
|
|
137
|
+
if (this.abortTimer) clearTimeout(this.abortTimer);
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
this.abortTimer = null;
|
|
140
|
+
void this.disposeIfUnsettled();
|
|
141
|
+
}, ABORT_GRACE_MS);
|
|
142
|
+
timer.unref?.();
|
|
143
|
+
this.abortTimer = timer;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Reclaim the child; the topic's history survives in SQLite and on disk. */
|
|
147
|
+
async dispose(): Promise<void> {
|
|
148
|
+
if (this.disposing) return this.disposing;
|
|
149
|
+
if (this.running) {
|
|
150
|
+
// A turn would die with the child — settle it rather than lose the answer.
|
|
151
|
+
await this.settle('cancelled');
|
|
152
|
+
}
|
|
153
|
+
this.disposing = this.teardown();
|
|
154
|
+
return this.disposing;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
private async teardown(): Promise<void> {
|
|
158
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
159
|
+
if (this.abortTimer) clearTimeout(this.abortTimer);
|
|
160
|
+
this.idleTimer = null;
|
|
161
|
+
this.abortTimer = null;
|
|
162
|
+
const proc = this.proc;
|
|
163
|
+
this.proc = null;
|
|
164
|
+
if (proc) await proc.dispose().catch(() => {});
|
|
165
|
+
this.disposing = null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
private async disposeIfUnsettled(): Promise<void> {
|
|
169
|
+
if (this.running) await this.settle('cancelled');
|
|
170
|
+
await this.dispose();
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
private ensureWorkspace(): Promise<BtwWorkspace> {
|
|
174
|
+
if (this.workspace) return Promise.resolve(this.workspace);
|
|
175
|
+
return this.openWorkspace();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** The topic's transcript: created from the parent on first use, resumed
|
|
179
|
+
* afterwards so a respawn continues the same side conversation. */
|
|
180
|
+
private async openWorkspace(): Promise<BtwWorkspace> {
|
|
181
|
+
const paths = await resolveBtwWorkspacePaths(this.context.sessionId, this.context.topicId);
|
|
182
|
+
const existing = await Bun.file(paths.sessionFile).exists();
|
|
183
|
+
const workspace = existing
|
|
184
|
+
? { ...paths, leafId: null }
|
|
185
|
+
: await createBtwWorkspace({
|
|
186
|
+
parentSessionId: this.context.sessionId,
|
|
187
|
+
topicId: this.context.topicId,
|
|
188
|
+
parentSessionFile: this.context.parentSessionFile,
|
|
189
|
+
});
|
|
190
|
+
this.workspace = workspace;
|
|
191
|
+
// The snapshot's leaf is what a later promotion must still find in the
|
|
192
|
+
// parent; a topic resumed from disk keeps the leaf it was created with.
|
|
193
|
+
if (workspace.leafId) await setBtwTopicLeaf(this.context.topicId, workspace.leafId);
|
|
194
|
+
return workspace;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
private start(): Promise<RpcProcess> {
|
|
198
|
+
if (this.starting) return this.starting;
|
|
199
|
+
const started = this.spawnChild();
|
|
200
|
+
this.starting = started;
|
|
201
|
+
void started
|
|
202
|
+
.catch(() => {})
|
|
203
|
+
.finally(() => {
|
|
204
|
+
if (this.starting === started) this.starting = null;
|
|
205
|
+
});
|
|
206
|
+
return started;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
private async spawnChild(): Promise<RpcProcess> {
|
|
210
|
+
const bin = resolveOmpBin();
|
|
211
|
+
if (!bin) throw new BtwError('The omp binary could not be found.', 'btw_unavailable');
|
|
212
|
+
const workspace = await this.ensureWorkspace();
|
|
213
|
+
const model = this.context.model;
|
|
214
|
+
const proc = new RpcProcess({
|
|
215
|
+
cwd: this.context.cwd,
|
|
216
|
+
extraArgs: [
|
|
217
|
+
'--resume',
|
|
218
|
+
workspace.sessionFile,
|
|
219
|
+
// Side questions never touch tools, so no approval mode is needed.
|
|
220
|
+
'--no-tools',
|
|
221
|
+
// Auto-titling would spend a model call on a transcript nobody lists.
|
|
222
|
+
'--no-title',
|
|
223
|
+
...(model ? ['--model', `${model.provider}/${model.id}`] : []),
|
|
224
|
+
],
|
|
225
|
+
onFrame: (frame) => this.handleFrame(frame),
|
|
226
|
+
onExit: (info) => this.handleExit(info.stderrTail),
|
|
227
|
+
});
|
|
228
|
+
this.proc = proc;
|
|
229
|
+
const ready = await proc.waitReady(READY_TIMEOUT_MS);
|
|
230
|
+
await proc.negotiateProtocol(ready);
|
|
231
|
+
await this.captureModel(proc);
|
|
232
|
+
return proc;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Record the display name the child reports, for the panel's model chip. */
|
|
236
|
+
private async captureModel(proc: RpcProcess): Promise<void> {
|
|
237
|
+
try {
|
|
238
|
+
const state = await proc.sendCommand<RpcSessionState>({ type: 'get_state' }, GET_STATE_TIMEOUT_MS);
|
|
239
|
+
const reported = state.model;
|
|
240
|
+
if (reported?.provider && reported.id && reported.name) {
|
|
241
|
+
await setBtwTopicModel(this.context.topicId, { provider: reported.provider, id: reported.id, name: reported.name });
|
|
242
|
+
this.sink.stateChanged();
|
|
243
|
+
}
|
|
244
|
+
} catch {
|
|
245
|
+
// A chip without a display name is cosmetic; the topic keeps its model.
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
private handleFrame(frame: RpcFrame): void {
|
|
250
|
+
if (frame.type === 'message_update') {
|
|
251
|
+
const event = frame.assistantMessageEvent;
|
|
252
|
+
if (isRecord(event) && event.type === 'text_delta' && typeof event.delta === 'string' && this.turnIndex !== null) {
|
|
253
|
+
this.answer += event.delta;
|
|
254
|
+
this.sink.publish({ type: 'btw_delta', topicId: this.topicId, turnIndex: this.turnIndex, text: event.delta });
|
|
255
|
+
}
|
|
256
|
+
} else if (frame.type === 'agent_end') {
|
|
257
|
+
// `isTerminal: false` means maintenance scheduled more work; the turn is
|
|
258
|
+
// not done until a terminal end arrives (mirrors the session wrapper).
|
|
259
|
+
if (frame.isTerminal === false) return;
|
|
260
|
+
void this.settle(finalStatus(frame.messages), finalAnswer(frame.messages, this.answer));
|
|
261
|
+
} else if (frame.type === 'extension_ui_request') {
|
|
262
|
+
this.answerExtensionUi(frame);
|
|
263
|
+
}
|
|
264
|
+
this.resetIdleTimer();
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/** A side session has no UI: release any dialog omp parks on, so it can
|
|
268
|
+
* never block a turn that the panel is waiting on. */
|
|
269
|
+
private answerExtensionUi(frame: RpcFrame): void {
|
|
270
|
+
const id = frame.id;
|
|
271
|
+
if (typeof id !== 'string' || !isAnswerableUiMethod(frame.method)) return;
|
|
272
|
+
this.proc?.sendFrame({ type: 'extension_ui_response', id, cancelled: true });
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
private async settle(status: BtwTurnStatus, answer?: string): Promise<void> {
|
|
276
|
+
const turnIndex = this.turnIndex;
|
|
277
|
+
if (turnIndex === null) return;
|
|
278
|
+
this.turnIndex = null;
|
|
279
|
+
const text = answer ?? this.answer;
|
|
280
|
+
this.answer = '';
|
|
281
|
+
// `busy` holds the topic live across the write: a state read that lands in
|
|
282
|
+
// this window must not settle the row as `interrupted` (see `btwStateFor`).
|
|
283
|
+
this.settling = true;
|
|
284
|
+
try {
|
|
285
|
+
await updateBtwTurn(this.topicId, turnIndex, { answer: text, status });
|
|
286
|
+
} finally {
|
|
287
|
+
this.settling = false;
|
|
288
|
+
this.sink.stateChanged();
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
private handleExit(stderrTail: string): void {
|
|
293
|
+
const wasRunning = this.running;
|
|
294
|
+
this.proc = null;
|
|
295
|
+
if (!wasRunning) return;
|
|
296
|
+
void settleRunningBtwTurns(this.topicId, 'interrupted').then(() => {
|
|
297
|
+
this.turnIndex = null;
|
|
298
|
+
this.answer = '';
|
|
299
|
+
this.sink.stateChanged();
|
|
300
|
+
const detail = stderrTail.trim().split('\n').pop();
|
|
301
|
+
this.sink.publish({
|
|
302
|
+
type: 'btw_error',
|
|
303
|
+
topicId: this.topicId,
|
|
304
|
+
message: `The side session exited before answering${detail ? `: ${detail}` : '.'}`,
|
|
305
|
+
});
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
private resetIdleTimer(force = false): void {
|
|
310
|
+
const now = Date.now();
|
|
311
|
+
if (!force && this.idleTimer && now - this.lastIdleReset < 5_000) return;
|
|
312
|
+
this.lastIdleReset = now;
|
|
313
|
+
if (this.idleTimer) clearTimeout(this.idleTimer);
|
|
314
|
+
const timer = setTimeout(() => {
|
|
315
|
+
if (this.running) {
|
|
316
|
+
this.resetIdleTimer(true);
|
|
317
|
+
return;
|
|
318
|
+
}
|
|
319
|
+
void this.dispose();
|
|
320
|
+
}, IDLE_DESTROY_MS);
|
|
321
|
+
timer.unref?.();
|
|
322
|
+
this.idleTimer = timer;
|
|
323
|
+
}
|
|
324
|
+
}
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* BTW schema, kept out of `db.server.ts`'s bootstrap block so that file's
|
|
8
|
+
* growth stays attributable (it was already at the ceiling before this
|
|
9
|
+
* feature). Called once per database open, like the other migrations there.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import type { DbClient } from '@/server/lib/db/client';
|
|
13
|
+
|
|
14
|
+
const BTW_SCHEMA_SQL = `
|
|
15
|
+
CREATE TABLE IF NOT EXISTS btw_topics (
|
|
16
|
+
id TEXT PRIMARY KEY,
|
|
17
|
+
session_id TEXT NOT NULL,
|
|
18
|
+
title TEXT NOT NULL DEFAULT '',
|
|
19
|
+
provider TEXT,
|
|
20
|
+
model_id TEXT,
|
|
21
|
+
model_name TEXT,
|
|
22
|
+
leaf_id TEXT,
|
|
23
|
+
promoted_session_id TEXT,
|
|
24
|
+
created_at INTEGER NOT NULL,
|
|
25
|
+
updated_at INTEGER NOT NULL
|
|
26
|
+
);
|
|
27
|
+
CREATE INDEX IF NOT EXISTS idx_btw_topics_session ON btw_topics (session_id, created_at ASC);
|
|
28
|
+
|
|
29
|
+
CREATE TABLE IF NOT EXISTS btw_turns (
|
|
30
|
+
topic_id TEXT NOT NULL,
|
|
31
|
+
turn_index INTEGER NOT NULL,
|
|
32
|
+
question TEXT NOT NULL DEFAULT '',
|
|
33
|
+
answer TEXT NOT NULL DEFAULT '',
|
|
34
|
+
status TEXT NOT NULL,
|
|
35
|
+
created_at INTEGER NOT NULL,
|
|
36
|
+
updated_at INTEGER NOT NULL,
|
|
37
|
+
PRIMARY KEY (topic_id, turn_index)
|
|
38
|
+
);
|
|
39
|
+
`;
|
|
40
|
+
|
|
41
|
+
/** Idempotent: side questions live beside the session they belong to. */
|
|
42
|
+
export async function ensureBtwSchema(db: DbClient): Promise<void> {
|
|
43
|
+
await db.exec(BTW_SCHEMA_SQL);
|
|
44
|
+
}
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* BTW operations: list, ask (new topic or follow-up), abort, promote, delete.
|
|
8
|
+
*
|
|
9
|
+
* The rules here are omp's own `/btw` rules, expressed against the chamber's
|
|
10
|
+
* storage:
|
|
11
|
+
* - one side question in flight per session (no implicit queue, no implicit
|
|
12
|
+
* cancel — a second question is refused, exactly like the TUI);
|
|
13
|
+
* - a new question is its own topic; only a follow-up continues one, and it
|
|
14
|
+
* sees only that topic's turns (plus the parent's context);
|
|
15
|
+
* - promotion is refused once the parent transcript has moved past the point
|
|
16
|
+
* the topic was snapshotted at, and while a turn is still running.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { isMockMode } from '@/server/mock.server';
|
|
20
|
+
import { findSessionFileById } from '@/server/lib/omp/session/locator';
|
|
21
|
+
import { readRawHeaderLine } from '@/server/lib/omp/session/files';
|
|
22
|
+
import { loadSessionModel } from '@/server/lib/omp/session/messages';
|
|
23
|
+
import { resolveSpawnCwd } from '@/server/lib/omp/rpc/manager';
|
|
24
|
+
import { createBtwTopic, deleteBtwTopic, getBtwTopic, readBtwTopicLeaf, setBtwTopicPromoted } from '@/server/lib/btw/store.server';
|
|
25
|
+
import { promoteBtwTopic as materializePromotedTopic, readTranscriptLeaf, removeBtwWorkspace, resolveBtwWorkspacePaths } from '@/server/lib/btw/session-copy.server';
|
|
26
|
+
import { btwStateFor, ensureBtwRuntime, findRunningBtwRuntime, forgetBtwRuntime, getBtwRuntime, publishBtwState } from '@/server/lib/btw/registry.server';
|
|
27
|
+
import { BtwError, type BtwImages } from '@/server/lib/btw/runtime.server';
|
|
28
|
+
import type { BtwState } from '@/shared/types';
|
|
29
|
+
|
|
30
|
+
/** Longest a topic label may be, matching the sidebar's session-title length. */
|
|
31
|
+
const TOPIC_TITLE_CHARS = 60;
|
|
32
|
+
|
|
33
|
+
function topicTitle(question: string): string {
|
|
34
|
+
const firstLine = question.trim().split('\n')[0].trim();
|
|
35
|
+
return firstLine.slice(0, TOPIC_TITLE_CHARS);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function assertAvailable(): void {
|
|
39
|
+
if (isMockMode()) {
|
|
40
|
+
throw new BtwError('Side questions need a real omp session; the chamber is running in demo mode.', 'btw_unavailable');
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** Resolve the parent session file and the directory its child must run in. */
|
|
45
|
+
async function resolveParent(sessionId: string): Promise<{ sessionFile: string; cwd: string }> {
|
|
46
|
+
const sessionFile = await findSessionFileById(sessionId);
|
|
47
|
+
if (!sessionFile) throw new BtwError('Session not found', 'session_not_found');
|
|
48
|
+
const header = await readRawHeaderLine(sessionFile);
|
|
49
|
+
const recordedCwd = header && typeof header.cwd === 'string' ? header.cwd : null;
|
|
50
|
+
return { sessionFile, cwd: await resolveSpawnCwd(recordedCwd) };
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function getBtwState(sessionId: string): Promise<BtwState> {
|
|
54
|
+
return btwStateFor(sessionId);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface AskBtwInput {
|
|
58
|
+
/** Continue this topic; omitted starts a new one. */
|
|
59
|
+
topicId?: string;
|
|
60
|
+
question: string;
|
|
61
|
+
images?: BtwImages;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function askBtw(sessionId: string, input: AskBtwInput): Promise<BtwState> {
|
|
65
|
+
assertAvailable();
|
|
66
|
+
const question = input.question.trim();
|
|
67
|
+
if (!question) throw new BtwError('A side question needs some text.', 'btw_empty');
|
|
68
|
+
|
|
69
|
+
if (findRunningBtwRuntime(sessionId)) {
|
|
70
|
+
throw new BtwError('A side question is still running — wait for it, or cancel it first.', 'btw_busy');
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const parent = await resolveParent(sessionId);
|
|
74
|
+
const existing = input.topicId ? await getBtwTopic(input.topicId) : undefined;
|
|
75
|
+
if (input.topicId && (!existing || existing.sessionId !== sessionId)) {
|
|
76
|
+
throw new BtwError('Unknown side question.', 'btw_topic_not_found');
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const topic =
|
|
80
|
+
existing ??
|
|
81
|
+
(await createBtwTopic({
|
|
82
|
+
sessionId,
|
|
83
|
+
title: topicTitle(question),
|
|
84
|
+
model: await topicModel(parent.sessionFile),
|
|
85
|
+
}));
|
|
86
|
+
|
|
87
|
+
const runtime = ensureBtwRuntime({
|
|
88
|
+
topicId: topic.id,
|
|
89
|
+
sessionId,
|
|
90
|
+
parentSessionFile: parent.sessionFile,
|
|
91
|
+
cwd: parent.cwd,
|
|
92
|
+
model: topic.model,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
await runtime.ask(question, input.images);
|
|
96
|
+
return btwStateFor(sessionId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/** The parent's model, so the side conversation runs on the visible model. */
|
|
100
|
+
async function topicModel(sessionFile: string) {
|
|
101
|
+
const model = await loadSessionModel(sessionFile);
|
|
102
|
+
return model ? { provider: model.provider, id: model.modelId } : undefined;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
export async function abortBtw(sessionId: string, topicId: string): Promise<BtwState> {
|
|
106
|
+
const topic = await getBtwTopic(topicId);
|
|
107
|
+
if (!topic || topic.sessionId !== sessionId) throw new BtwError('Unknown side question.', 'btw_topic_not_found');
|
|
108
|
+
await getBtwRuntime(topicId)?.abort();
|
|
109
|
+
return btwStateFor(sessionId);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Turn the topic into a session of the chat's own: a branch of the parent
|
|
114
|
+
* conversation whose tip is the side answer, opened by the caller.
|
|
115
|
+
*/
|
|
116
|
+
export async function promoteBtw(sessionId: string, topicId: string): Promise<{ sessionId: string }> {
|
|
117
|
+
assertAvailable();
|
|
118
|
+
const topic = await getBtwTopic(topicId);
|
|
119
|
+
if (!topic || topic.sessionId !== sessionId) throw new BtwError('Unknown side question.', 'btw_topic_not_found');
|
|
120
|
+
// Promotion materializes a file beside the parent, so it happens once: a
|
|
121
|
+
// second promote re-opens the branch it already wrote.
|
|
122
|
+
if (topic.promotedSessionId) return { sessionId: topic.promotedSessionId };
|
|
123
|
+
if (topic.turns.length === 0) throw new BtwError('This side question has no answer to promote yet.', 'btw_empty');
|
|
124
|
+
if (topic.turns.some((turn) => turn.status === 'running')) {
|
|
125
|
+
throw new BtwError('Wait for the side answer to finish, or cancel it, before promoting it.', 'btw_busy');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const parent = await resolveParent(sessionId);
|
|
129
|
+
const snapshotLeaf = await readBtwTopicLeaf(topicId);
|
|
130
|
+
if (snapshotLeaf) {
|
|
131
|
+
const currentLeaf = await readTranscriptLeaf(parent.sessionFile);
|
|
132
|
+
if (currentLeaf !== snapshotLeaf) {
|
|
133
|
+
throw new BtwError(
|
|
134
|
+
'The chat has moved on since this side question started, so it can no longer branch from it.',
|
|
135
|
+
'btw_stale',
|
|
136
|
+
);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// The child would keep appending to the transcript we are about to move.
|
|
141
|
+
const runtime = getBtwRuntime(topicId);
|
|
142
|
+
if (runtime) {
|
|
143
|
+
await runtime.dispose();
|
|
144
|
+
forgetBtwRuntime(topicId);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const paths = await resolveBtwWorkspacePaths(sessionId, topicId);
|
|
148
|
+
const title = topicTitle(topic.turns[0].question);
|
|
149
|
+
const created = await materializePromotedTopic({
|
|
150
|
+
parentSessionFile: parent.sessionFile,
|
|
151
|
+
topicSessionFile: paths.sessionFile,
|
|
152
|
+
title,
|
|
153
|
+
});
|
|
154
|
+
await setBtwTopicPromoted(topicId, created);
|
|
155
|
+
await publishBtwState(sessionId);
|
|
156
|
+
return { sessionId: created };
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
export async function removeBtw(sessionId: string, topicId: string): Promise<BtwState> {
|
|
160
|
+
const topic = await getBtwTopic(topicId);
|
|
161
|
+
if (!topic || topic.sessionId !== sessionId) throw new BtwError('Unknown side question.', 'btw_topic_not_found');
|
|
162
|
+
const runtime = getBtwRuntime(topicId);
|
|
163
|
+
if (runtime) {
|
|
164
|
+
await runtime.dispose();
|
|
165
|
+
forgetBtwRuntime(topicId);
|
|
166
|
+
}
|
|
167
|
+
await deleteBtwTopic(topicId);
|
|
168
|
+
await removeBtwWorkspace((await resolveBtwWorkspacePaths(sessionId, topicId)).dir);
|
|
169
|
+
await publishBtwState(sessionId);
|
|
170
|
+
return btwStateFor(sessionId);
|
|
171
|
+
}
|