@prismshadow/penguin-server 0.2.11 → 0.2.13
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1 -1
- package/dist/api/types.d.ts +1014 -12
- package/dist/api/types.js +7 -0
- package/dist/auth-token.js +6 -4
- package/dist/auth-token.js.map +1 -1
- package/dist/{chunk-Q65VPAQS.js → chunk-3I2XOPJB.js} +102 -55
- package/dist/chunk-3I2XOPJB.js.map +1 -0
- package/dist/{chunk-ADZJYDNC.js → chunk-A3QANMU5.js} +293 -3
- package/dist/chunk-A3QANMU5.js.map +1 -0
- package/dist/chunk-B5IP37ZQ.js +56 -0
- package/dist/chunk-L2H3K4DT.js +7 -0
- package/dist/chunk-L2H3K4DT.js.map +1 -0
- package/dist/chunk-LPPVGHYD.js +83 -0
- package/dist/chunk-LPPVGHYD.js.map +1 -0
- package/dist/{chunk-TTGGJDNA.js → chunk-S3BI6R7I.js} +2 -2
- package/dist/{chunk-HOS3W3UP.js → chunk-TOOTEEM4.js} +50 -8
- package/dist/chunk-TOOTEEM4.js.map +1 -0
- package/dist/hmr/manifest.d.ts +1 -77
- package/dist/hmr/manifest.js +6 -1
- package/dist/index.js +50757 -20174
- package/dist/index.js.map +1 -1
- package/dist/initial-password.js +1 -0
- package/dist/lock.js +1 -0
- package/dist/machine-status.js +2 -1
- package/dist/machine-status.js.map +1 -1
- package/dist/plugin/index.d.ts +2595 -0
- package/dist/plugin/index.js.map +1 -0
- package/dist/reset-admin-password.js +7 -5
- package/dist/reset-admin-password.js.map +1 -1
- package/dist/secret-file.js +2 -0
- package/dist/secret-file.js.map +1 -1
- package/dist/version-report.js +3 -2
- package/package.json +11 -9
- package/web-dist/assets/{addon-clipboard-1cARCaK9.js → addon-clipboard-DrZY90Ks.js} +1 -1
- package/web-dist/assets/{addon-fit-BWdzl8Er.js → addon-fit-CtEAdAax.js} +1 -1
- package/web-dist/assets/{addon-web-links-DjXuvn4Z.js → addon-web-links-DJL13cYY.js} +1 -1
- package/web-dist/assets/c-BG-tZi7o.js +1 -0
- package/web-dist/assets/cpp-CaeG3uDQ.js +1 -0
- package/web-dist/assets/css-Br67jR8j.js +1 -0
- package/web-dist/assets/diff-jW5kmiru.js +1 -0
- package/web-dist/assets/github-dark-BQV5ugrc.js +1 -0
- package/web-dist/assets/github-light-Rcb2q3L5.js +1 -0
- package/web-dist/assets/go-B7VTfpnm.js +1 -0
- package/web-dist/assets/highlighter-CUb4XKtQ.js +2 -0
- package/web-dist/assets/{highlighter-Cu43iYr8.js → highlighter-core-CrBdruEx.js} +2 -2
- package/web-dist/assets/highlighter.worker-CL6f9Fzw.js +152 -0
- package/web-dist/assets/html-BW_6VhB7.js +1 -0
- package/web-dist/assets/index-CmfumtXy.js +570 -0
- package/web-dist/assets/index-Ywc2uM5u.css +1 -0
- package/web-dist/assets/ini-Dix3pTmg.js +1 -0
- package/web-dist/assets/java-BTDmaRNO.js +1 -0
- package/web-dist/assets/javascript-WyGyM1wV.js +1 -0
- package/web-dist/assets/json-B8JryHcM.js +1 -0
- package/web-dist/assets/jsx-8cLv2Six.js +1 -0
- package/web-dist/assets/log-CcaF_xNv.js +1 -0
- package/web-dist/assets/markdown-DX-TNHnc.js +1 -0
- package/web-dist/assets/php-CNeTOyEV.js +1 -0
- package/web-dist/assets/python-B6Ihcp-E.js +1 -0
- package/web-dist/assets/ruby-CYiNE6ps.js +1 -0
- package/web-dist/assets/rust-CxuBE0J8.js +1 -0
- package/web-dist/assets/shellscript-DgJyDXW6.js +1 -0
- package/web-dist/assets/sql-Dq2MiB0T.js +1 -0
- package/web-dist/assets/toml-Ms459DBw.js +1 -0
- package/web-dist/assets/tsx-D6116Ycy.js +1 -0
- package/web-dist/assets/typescript-BqvgUolh.js +1 -0
- package/web-dist/assets/xml-BkAJuBCc.js +1 -0
- package/web-dist/assets/{xterm-DKcY6q-I.js → xterm-B2oS8Mg8.js} +1 -1
- package/web-dist/assets/yaml-Bopit2L3.js +1 -0
- package/web-dist/index.html +2 -2
- package/dist/chunk-4SXGU7RO.js +0 -50
- package/dist/chunk-4SXGU7RO.js.map +0 -1
- package/dist/chunk-ADZJYDNC.js.map +0 -1
- package/dist/chunk-HOS3W3UP.js.map +0 -1
- package/dist/chunk-Q65VPAQS.js.map +0 -1
- package/dist/extension/index.d.ts +0 -212
- package/web-dist/assets/index-Bm9n7ZV-.js +0 -457
- package/web-dist/assets/index-DR4PrRzS.css +0 -1
- /package/dist/{extension/index.js.map → chunk-B5IP37ZQ.js.map} +0 -0
- /package/dist/{chunk-TTGGJDNA.js.map → chunk-S3BI6R7I.js.map} +0 -0
- /package/dist/{extension → plugin}/index.js +0 -0
|
@@ -0,0 +1,2595 @@
|
|
|
1
|
+
import { SpawnConfiner, ThinkingLevelName, OmniMessage, ToolCallPayload, ProjectConfig, ModelRef, ListEndpointModelsOptions, ProxyEnvPolicy, ControlEnvContext, ApproveFn, CompactAvailability, SessionTitleResult, BackgroundCommandInfo, ToolDetachResult, BackgroundSubagentInfo, SubagentMessageOptions, SubagentMessageOutcome } from '@prismshadow/penguin-core';
|
|
2
|
+
import { SandboxProviderSource, SandboxSettings, SandboxDimension } from '@prismshadow/penguin-core/plugin';
|
|
3
|
+
import { Slot, Opaque } from '@prismshadow/penguin-core/kernel';
|
|
4
|
+
import { ApprovalMode, SessionSource, ModelRefDto, ChatDefaultsDto, CommandPolicyDto, ModelVisionDetectRequest, ModelVisionDetectResponse, ModelTestRequest, ModelTestResponse, ModelProtocolDetectRequest, ModelProtocolDetectResponse, EndpointModelListRequest, EndpointModelListResponse, ModelsResponse, ModelsUpdateRequest, MessagingChannel as MessagingChannel$1, SessionInfo, SessionCategory, SessionCategoryCounts, ServerEvent, SessionStatus, SessionBackgroundTasks, PendingSteeringInfo, PendingFollowUpInfo, AgentConfigDto, AgentConfigUpdateRequest, AgentKernelUpdateResponse, VaultResponse, VaultUpdateRequest, PluginUpdateRef, MessagingRuntimeStatus, ContributionsResponse, RendererRef } from '../api/types.js';
|
|
5
|
+
import { ThinkingLevelName as ThinkingLevelName$1 } from '@prismshadow/penguin-core/interfaces';
|
|
6
|
+
import { Hono } from 'hono';
|
|
7
|
+
import '@prismshadow/penguin-core/omnimessage';
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* The sandbox service: owns the registered backends, routes each spawn's policy to a
|
|
11
|
+
* backend that implements the dimensions it requires, and produces the SpawnConfiner
|
|
12
|
+
* closure the runtime transports into core's command-session seam.
|
|
13
|
+
*
|
|
14
|
+
* Routing is by CAPABILITY, not by registration order alone: a policy requiring only
|
|
15
|
+
* `fs-write` goes to the first backend that covers it (the DSH adaptor, which works on
|
|
16
|
+
* Linux/macOS/Windows alike), while a policy also requiring `network` or `mask-paths`
|
|
17
|
+
* goes to the first backend implementing those (penguin-bwrap). Registration order
|
|
18
|
+
* only breaks ties between backends that both cover the request. A request nothing
|
|
19
|
+
* covers fails closed — never a silent unconfined run, and never a silently dropped
|
|
20
|
+
* dimension.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
declare class SandboxService {
|
|
24
|
+
private readonly mounted;
|
|
25
|
+
/** name → why it failed to load; surfaced in the fail-closed message. */
|
|
26
|
+
private readonly loadErrors;
|
|
27
|
+
/**
|
|
28
|
+
* Ships with confinement OFF (`danger-full-access`): the default flips to
|
|
29
|
+
* workspace-write together with the deployment-facing config surface, so a
|
|
30
|
+
* deployment gains the switch before the enforcement — otherwise every host without
|
|
31
|
+
* a usable backend would fail every command the moment this code arrives.
|
|
32
|
+
*/
|
|
33
|
+
private settings;
|
|
34
|
+
private readonly ready;
|
|
35
|
+
/**
|
|
36
|
+
* @param registrations - the backends plugins registered (see the sandbox registry on
|
|
37
|
+
* PenguinInterface). Loading is async, but create() and the confiner are sync: a
|
|
38
|
+
* confining policy resolved before loading settles fails closed rather than
|
|
39
|
+
* waiting. The default settings never consult a backend, so that window only
|
|
40
|
+
* exists for deployments flipping the mode in the first milliseconds after boot.
|
|
41
|
+
*/
|
|
42
|
+
constructor(registrations?: Iterable<[string, SandboxProviderSource]>);
|
|
43
|
+
/** Resolves when every registered backend has loaded (or failed to). */
|
|
44
|
+
whenReady(): Promise<void>;
|
|
45
|
+
/** Replaces the active settings (the config surface's write path). */
|
|
46
|
+
configure(settings: SandboxSettings): void;
|
|
47
|
+
/** The active settings as a plain copy (the config surface's read side). */
|
|
48
|
+
currentSettings(): SandboxSettings;
|
|
49
|
+
/**
|
|
50
|
+
* The active settings as parked state, or undefined for the pristine default. The
|
|
51
|
+
* omission is load-bearing compatibility: a default deployment keeps parking
|
|
52
|
+
* `{ motd }`, which any platform bundle's context schema accepts. Once confinement
|
|
53
|
+
* is configured the field enters the document, and from then on pushing a
|
|
54
|
+
* sandbox-ignorant bundle is BLOCKED by schema validation — the right failure:
|
|
55
|
+
* refusing the swap beats a swap that silently un-confines the deployment.
|
|
56
|
+
*/
|
|
57
|
+
parkedSettings(): SandboxSettings | undefined;
|
|
58
|
+
/** The mounted backends and what each implements (diagnostics / the config surface). */
|
|
59
|
+
backends(): Array<{
|
|
60
|
+
name: string;
|
|
61
|
+
dimensions: readonly SandboxDimension[];
|
|
62
|
+
}>;
|
|
63
|
+
/**
|
|
64
|
+
* The closure handed to the runtime: rewrites each spawn's argv under the active
|
|
65
|
+
* settings. Pure and self-contained, so the previous platform's confiner keeps
|
|
66
|
+
* serving during a hot-swap freeze window.
|
|
67
|
+
*/
|
|
68
|
+
confiner(): SpawnConfiner;
|
|
69
|
+
/** The first mounted backend implementing every required dimension, or a fail-closed throw. */
|
|
70
|
+
private pick;
|
|
71
|
+
private failedSuffix;
|
|
72
|
+
}
|
|
73
|
+
declare const Sandbox_base: (abstract new () => Pick<SandboxService, "configure" | "currentSettings" | "parkedSettings" | "backends" | "confiner" | "whenReady">) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
74
|
+
/** Confinement: settings, the active confiner, and which backends are mounted. */
|
|
75
|
+
declare abstract class Sandbox extends Sandbox_base {
|
|
76
|
+
}
|
|
77
|
+
interface SandboxSlots {
|
|
78
|
+
/**
|
|
79
|
+
* A backend: its static half here (name, the dimensions it implements), its code half
|
|
80
|
+
* bound by the contributor — a provider, or a promise of one for backends that probe.
|
|
81
|
+
*/
|
|
82
|
+
providers: Slot<{
|
|
83
|
+
name: string;
|
|
84
|
+
dimensions: SandboxDimension[];
|
|
85
|
+
}, SandboxProviderSource>;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Screen snapshot -> ANSI restore stream, plus plain-text capture.
|
|
90
|
+
*
|
|
91
|
+
* This is what makes a browser refresh (or a reconnect from a second device) show the
|
|
92
|
+
* terminal exactly as it was: the daemon keeps an authoritative headless xterm per pty, and
|
|
93
|
+
* on attach it *re-renders* that grid as an escape-sequence stream the client's real xterm
|
|
94
|
+
* replays into an identical screen. That beats the two obvious alternatives:
|
|
95
|
+
*
|
|
96
|
+
* - Replaying the raw pty byte log: unbounded, and full-screen programs (vim, top) replay
|
|
97
|
+
* as a flicker of stale frames.
|
|
98
|
+
* - Sending a JSON cell grid: a 200k-object payload per attach, parsed on the UI thread.
|
|
99
|
+
*
|
|
100
|
+
* Cursor alignment is the subtle part. The restore writes `scrollback + rows` lines, so the
|
|
101
|
+
* client's viewport ends up holding exactly the server's viewport, and only then is the
|
|
102
|
+
* cursor placed with a viewport-relative CUP. Trailing blank rows are therefore written on
|
|
103
|
+
* purpose — dropping them would shift every subsequent row and land the cursor in the wrong
|
|
104
|
+
* place.
|
|
105
|
+
*/
|
|
106
|
+
|
|
107
|
+
interface CaptureLinesOptions {
|
|
108
|
+
/** Line index into scrollback+viewport; negative counts from the end (-1 = last line). */
|
|
109
|
+
start?: number;
|
|
110
|
+
end?: number;
|
|
111
|
+
}
|
|
112
|
+
interface CaptureLinesResult {
|
|
113
|
+
lines: string[];
|
|
114
|
+
totalLines: number;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
interface TerminalExitInfo {
|
|
118
|
+
exitCode: number;
|
|
119
|
+
signal: number | null;
|
|
120
|
+
lastOutputLines: string[];
|
|
121
|
+
}
|
|
122
|
+
interface CreateTerminalSessionOptions {
|
|
123
|
+
id?: string;
|
|
124
|
+
/** Absolute working directory (already resolved and validated by the manager). */
|
|
125
|
+
cwd: string;
|
|
126
|
+
/** Owning user id — a shell is the most privileged thing this server hands out. */
|
|
127
|
+
ownerUserId: string;
|
|
128
|
+
/**
|
|
129
|
+
* Stable per-user display number (tmux-style), assigned by the manager at creation. It
|
|
130
|
+
* travels with the terminal for its whole life — UI numbering must never depend on list
|
|
131
|
+
* position, which changes under user-reordered tabs.
|
|
132
|
+
*/
|
|
133
|
+
seq?: number;
|
|
134
|
+
name?: string;
|
|
135
|
+
shell?: string;
|
|
136
|
+
cols?: number;
|
|
137
|
+
rows?: number;
|
|
138
|
+
env?: Record<string, string>;
|
|
139
|
+
/**
|
|
140
|
+
* Where a pushed bundle's node-pty assets live (the hmr capability's assetsDir);
|
|
141
|
+
* absent falls back to the packaged require (see pty-module.ts).
|
|
142
|
+
*/
|
|
143
|
+
assets?: () => string | null;
|
|
144
|
+
}
|
|
145
|
+
interface TerminalSessionInfo {
|
|
146
|
+
id: string;
|
|
147
|
+
seq: number;
|
|
148
|
+
name: string;
|
|
149
|
+
cwd: string;
|
|
150
|
+
ownerUserId: string;
|
|
151
|
+
title: string | null;
|
|
152
|
+
cols: number;
|
|
153
|
+
rows: number;
|
|
154
|
+
pid: number;
|
|
155
|
+
createdAt: string;
|
|
156
|
+
alive: boolean;
|
|
157
|
+
exit: TerminalExitInfo | null;
|
|
158
|
+
}
|
|
159
|
+
type TerminalOutputListener = (data: string) => void;
|
|
160
|
+
type TerminalExitListener = (info: TerminalExitInfo) => void;
|
|
161
|
+
type TerminalTitleListener = (title: string | null) => void;
|
|
162
|
+
declare class TerminalSession {
|
|
163
|
+
readonly id: string;
|
|
164
|
+
readonly seq: number;
|
|
165
|
+
readonly cwd: string;
|
|
166
|
+
readonly ownerUserId: string;
|
|
167
|
+
readonly createdAt: Date;
|
|
168
|
+
private name;
|
|
169
|
+
private readonly terminal;
|
|
170
|
+
private readonly ptyProcess;
|
|
171
|
+
private readonly inputMode;
|
|
172
|
+
private readonly outputListeners;
|
|
173
|
+
private readonly exitListeners;
|
|
174
|
+
private readonly titleListeners;
|
|
175
|
+
private title;
|
|
176
|
+
private pendingTitle;
|
|
177
|
+
private titleTimer;
|
|
178
|
+
private sizeState;
|
|
179
|
+
private exitInfo;
|
|
180
|
+
private disposed;
|
|
181
|
+
constructor(options: CreateTerminalSessionOptions);
|
|
182
|
+
/** DA1 / DSR / OSC colour queries — see OSC_COLOR_QUERY_RESPONSES. */
|
|
183
|
+
private registerCapabilityResponders;
|
|
184
|
+
get alive(): boolean;
|
|
185
|
+
get exit(): TerminalExitInfo | null;
|
|
186
|
+
info(): TerminalSessionInfo;
|
|
187
|
+
rename(name: string): void;
|
|
188
|
+
write(data: string): void;
|
|
189
|
+
/**
|
|
190
|
+
* Applies a size request under the claim/update ownership rule, resizing both the pty and
|
|
191
|
+
* the daemon's emulator so snapshots keep matching what the shell laid out.
|
|
192
|
+
*/
|
|
193
|
+
resize(request: {
|
|
194
|
+
connectionId: string;
|
|
195
|
+
cols: number;
|
|
196
|
+
rows: number;
|
|
197
|
+
intent: "claim" | "update";
|
|
198
|
+
}): boolean;
|
|
199
|
+
releaseSize(connectionId: string): void;
|
|
200
|
+
get size(): {
|
|
201
|
+
cols: number;
|
|
202
|
+
rows: number;
|
|
203
|
+
};
|
|
204
|
+
/** The attach payload: the whole screen as an ANSI stream (see snapshot.ts). */
|
|
205
|
+
restoreStream(options?: {
|
|
206
|
+
scrollbackLines?: number;
|
|
207
|
+
}): string;
|
|
208
|
+
capture(options?: CaptureLinesOptions): CaptureLinesResult;
|
|
209
|
+
onOutput(listener: TerminalOutputListener): () => void;
|
|
210
|
+
onExit(listener: TerminalExitListener): () => void;
|
|
211
|
+
onTitleChange(listener: TerminalTitleListener): () => void;
|
|
212
|
+
kill(signal?: string): void;
|
|
213
|
+
dispose(): void;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
interface CreateTerminalRequest {
|
|
217
|
+
cwd: string;
|
|
218
|
+
ownerUserId: string;
|
|
219
|
+
name?: string;
|
|
220
|
+
cols?: number;
|
|
221
|
+
rows?: number;
|
|
222
|
+
shell?: string;
|
|
223
|
+
}
|
|
224
|
+
/** A live pty: a host-like object (it owns a process), compared by name. */
|
|
225
|
+
type Terminal = Opaque<"TerminalSession", TerminalSession>;
|
|
226
|
+
declare const Terminals_base: (abstract new () => {
|
|
227
|
+
create(request: CreateTerminalRequest): Promise<Terminal>;
|
|
228
|
+
adopt(ids: readonly string[]): void;
|
|
229
|
+
quiesce(): void;
|
|
230
|
+
handleIds(): string[];
|
|
231
|
+
require(id: string, userId: string): Terminal;
|
|
232
|
+
get(id: string): Terminal | undefined;
|
|
233
|
+
list(userId: string): Terminal[];
|
|
234
|
+
listInfo(userId: string): TerminalSessionInfo[];
|
|
235
|
+
kill(id: string, userId: string): void;
|
|
236
|
+
disposeAll(): void;
|
|
237
|
+
}) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
238
|
+
/** Live ptys: the spawn primitive behind /api/terminals and the terminal WebSocket. */
|
|
239
|
+
declare abstract class Terminals extends Terminals_base {
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* The resolved, in-force limits for one request, in bytes. Read fresh per request from the
|
|
244
|
+
* settings repo rather than snapshotted at boot, so an admin's change applies to the very next
|
|
245
|
+
* upload without a restart.
|
|
246
|
+
*/
|
|
247
|
+
interface AttachmentLimits {
|
|
248
|
+
/** Per-file cap. */
|
|
249
|
+
maxBytes: number;
|
|
250
|
+
/** Per-request total of decoded attachment bytes. */
|
|
251
|
+
totalBytes: number;
|
|
252
|
+
/** Per-request file count. */
|
|
253
|
+
maxCount: number;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* A queued message's file attachment as held for recall: the scratchpad path it was written
|
|
258
|
+
* to, plus what is needed to rebuild the composer-shaped part — the submitted name and media
|
|
259
|
+
* type (the bytes stay on disk; queued entries never hold file payloads in memory).
|
|
260
|
+
*/
|
|
261
|
+
interface RecallableFile {
|
|
262
|
+
fileName: string;
|
|
263
|
+
path: string;
|
|
264
|
+
mime: string;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
declare const SessionIndex_base: (abstract new () => {
|
|
268
|
+
insert(row: SessionRow): void;
|
|
269
|
+
insertOrIgnore(row: SessionRow): void;
|
|
270
|
+
insertFork(sourceSessionId: string, row: SessionRow): SessionRow;
|
|
271
|
+
markOrgClient(sessionIds: readonly string[]): void;
|
|
272
|
+
markHasTrace(sessionId: string): void;
|
|
273
|
+
markDriven(sessionId: string, at: string): void;
|
|
274
|
+
touchLastActive(sessionId: string, at: string): void;
|
|
275
|
+
findById(sessionId: string): SessionRow | null;
|
|
276
|
+
listByAgent(projectId: string, agentId: string): SessionRow[];
|
|
277
|
+
listByProject(projectId: string): SessionRow[];
|
|
278
|
+
updateApprovalMode(sessionId: string, mode: ApprovalMode): void;
|
|
279
|
+
updateThinkingLevel(sessionId: string, level: ThinkingLevelName): void;
|
|
280
|
+
updateTitle(sessionId: string, title: string): void;
|
|
281
|
+
updateTitleIfNull(sessionId: string, title: string): void;
|
|
282
|
+
setArchived(sessionId: string, archivedAt: string | null): void;
|
|
283
|
+
replaceId(oldSessionId: string, newSessionId: string): void;
|
|
284
|
+
deleteByAgent(projectId: string, agentId: string): void;
|
|
285
|
+
deleteByProject(projectId: string): void;
|
|
286
|
+
deleteById(sessionId: string): void;
|
|
287
|
+
}) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
288
|
+
/** SessionIndex: the mechanism SessionsRepo implements. */
|
|
289
|
+
declare abstract class SessionIndex extends SessionIndex_base {
|
|
290
|
+
}
|
|
291
|
+
declare const SessionOrigins_base: (abstract new () => {
|
|
292
|
+
set(sessionId: string, source: SessionSource | null): void;
|
|
293
|
+
get(sessionId: string): SessionSource | null | undefined;
|
|
294
|
+
delete(sessionId: string): void;
|
|
295
|
+
}) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
296
|
+
/** SessionOrigins: the mechanism SessionSources implements. */
|
|
297
|
+
declare abstract class SessionOrigins extends SessionOrigins_base {
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
/**
|
|
301
|
+
* sessions table repo:
|
|
302
|
+
* Session index, approval mode, and auto-generated title; Session-level routes use this to look up project ownership.
|
|
303
|
+
*/
|
|
304
|
+
|
|
305
|
+
interface SessionRow {
|
|
306
|
+
sessionId: string;
|
|
307
|
+
projectId: string;
|
|
308
|
+
agentId: string;
|
|
309
|
+
/** Provider group of the session's model (pairs with `modelId` to form the model reference). */
|
|
310
|
+
provider: string;
|
|
311
|
+
/** Upstream model_id of the session's model (sent as-is to AgentHub; never concatenated). */
|
|
312
|
+
modelId: string;
|
|
313
|
+
workspace: string;
|
|
314
|
+
approvalMode: ApprovalMode;
|
|
315
|
+
/**
|
|
316
|
+
* Thinking level pinned for THIS session (PATCH /api/sessions/:id); NULL = not pinned,
|
|
317
|
+
* so runs that carry no level of their own follow the Agent config as before. Sessions
|
|
318
|
+
* are never inserted with one — pinning is an explicit act inside a live conversation.
|
|
319
|
+
*/
|
|
320
|
+
thinkingLevel?: ThinkingLevelName$1 | null;
|
|
321
|
+
/** Auto-generated session title; NULL = not yet generated (frontend shows "New Conversation"). */
|
|
322
|
+
title: string | null;
|
|
323
|
+
/** Archive timestamp, ISO; NULL = not archived (omitting on insert defaults to NULL). */
|
|
324
|
+
archivedAt?: string | null;
|
|
325
|
+
/**
|
|
326
|
+
* Creating client: "web" (created via the Web App), "cli" (created through the API by
|
|
327
|
+
* the CLI, or adopted from a Trace a legacy CLI-direct run left behind), "org" (opened by
|
|
328
|
+
* the organization runtime — a desk or a ticket session); NULL = legacy row from before
|
|
329
|
+
* the column existed, treated as web. Free text in SQLite, so a new value needs no
|
|
330
|
+
* migration. Provenance that outlives its organization: development mode's list is the
|
|
331
|
+
* one reader that filters on it, and it hides "org" rows whether or not the organization
|
|
332
|
+
* still exists. The schedule/subagent SOURCE is deliberately NOT a row field — core
|
|
333
|
+
* session_meta in the Trace stays the single source of truth for it
|
|
334
|
+
* (runtime/session-sources.ts); `client` is a separate, DB-only axis that meta never
|
|
335
|
+
* records.
|
|
336
|
+
*/
|
|
337
|
+
client?: "web" | "cli" | "org" | null;
|
|
338
|
+
/** Cache: a Trace record exists (set at task start / adoption / subagent registration; backfilled by list hydration). */
|
|
339
|
+
hasTrace?: boolean;
|
|
340
|
+
/**
|
|
341
|
+
* Last activity this server drove for the session, ISO: stamped once when a run starts
|
|
342
|
+
* and once when it ends (see SessionManager.drive — a goal run is ONE drive over all its
|
|
343
|
+
* rounds, so it stamps twice, not per round). Set to createdAt at insert, and only ever
|
|
344
|
+
* moves forward (markDriven/touchLastActive clamp with MAX, so a backwards clock step
|
|
345
|
+
* cannot regress it). Rows this server never drives — CLI-adopted Sessions, subagent
|
|
346
|
+
* rows (their activity is driven through the parent's entry) — keep createdAt.
|
|
347
|
+
*/
|
|
348
|
+
lastActiveAt: string;
|
|
349
|
+
createdAt: string;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Tool call approval: ApproveFn factory + pending
|
|
354
|
+
* approval registry.
|
|
355
|
+
*
|
|
356
|
+
* Approval mode semantics match the CLI (packages/cli/src/approval.ts):
|
|
357
|
+
* allow-all auto-approves; deny-all auto-denies; read-only allows read-only tools
|
|
358
|
+
* (permission==="r") and routes the rest to manual approval; always-ask routes
|
|
359
|
+
* everything to manual approval. Routing to manual approval registers a pending entry
|
|
360
|
+
* and pushes an `approval_request` via SSE, suspending until the frontend decides via
|
|
361
|
+
* `POST /approvals/:toolCallId`; no timeout — pending approvals are resolved to deny
|
|
362
|
+
* when the Task is interrupted (then proceeds through the abort flow).
|
|
363
|
+
*
|
|
364
|
+
* Every approval decision re-reads the current approval_mode (`getMode` reads the DB),
|
|
365
|
+
* so mode changes take effect immediately.
|
|
366
|
+
* Docs: /docs/tools § "Approval".
|
|
367
|
+
*/
|
|
368
|
+
|
|
369
|
+
interface PendingApproval {
|
|
370
|
+
toolCall: OmniMessage<ToolCallPayload>;
|
|
371
|
+
origin?: string[];
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
/** A numbered channel event; `id` is `<epoch>-<seq>`, `data` is serialized single-line JSON. */
|
|
375
|
+
interface ChannelEvent {
|
|
376
|
+
id: string;
|
|
377
|
+
/** SSE event name; omitted (OmniMessage) means no `event:` line. */
|
|
378
|
+
event?: string;
|
|
379
|
+
data: string;
|
|
380
|
+
}
|
|
381
|
+
type ChannelListener = (evt: ChannelEvent) => void;
|
|
382
|
+
interface ChannelOptions {
|
|
383
|
+
maxBufferCount?: number;
|
|
384
|
+
maxBufferBytes?: number;
|
|
385
|
+
}
|
|
386
|
+
declare class Channel {
|
|
387
|
+
/** Channel epoch: generated at instance creation, prefixed onto event ids (necessarily changes after recycle/recreate or restart). */
|
|
388
|
+
readonly epoch: string;
|
|
389
|
+
private nextSeq;
|
|
390
|
+
private buffer;
|
|
391
|
+
private bufferBytes;
|
|
392
|
+
/** Max seq among evicted events (0 means never evicted): lower bound for hit checks. */
|
|
393
|
+
private lastEvictedSeq;
|
|
394
|
+
private readonly listeners;
|
|
395
|
+
private readonly maxCount;
|
|
396
|
+
private readonly maxBytes;
|
|
397
|
+
/** Timestamp of last activity (publish/subscription change), used for idle-reclaim checks. */
|
|
398
|
+
lastActivityMs: number;
|
|
399
|
+
constructor(opts?: ChannelOptions);
|
|
400
|
+
get subscriberCount(): number;
|
|
401
|
+
/**
|
|
402
|
+
* Id of the most recently assigned event (`<epoch>-<seq>`; seq 0 when none was assigned
|
|
403
|
+
* yet). Unicast (sendTo) seqs count too — the value is a position marker, not a buffer
|
|
404
|
+
* lookup key. Used as the live-tail cursor on GET /messages: captured in the same
|
|
405
|
+
* synchronous tick as the fragment snapshot, it tells the client which buffered events
|
|
406
|
+
* the snapshot already covers.
|
|
407
|
+
*/
|
|
408
|
+
get lastEventId(): string;
|
|
409
|
+
/** Broadcast an event: number it, buffer it (evicting the oldest), notify all subscribers. */
|
|
410
|
+
publish(data: unknown, event?: string): ChannelEvent;
|
|
411
|
+
/** Unicast an event to a single subscriber: consumes a seq but doesn't buffer or broadcast (used for replay at subscribe time). */
|
|
412
|
+
sendTo(listener: ChannelListener, data: unknown, event?: string): ChannelEvent;
|
|
413
|
+
subscribe(listener: ChannelListener): () => void;
|
|
414
|
+
/**
|
|
415
|
+
* Compute replay from a Last-Event-ID (`<epoch>-<seq>`): a mismatched epoch (channel
|
|
416
|
+
* recycled/recreated, process restarted, or malformed id) always misses; a matching
|
|
417
|
+
* epoch hits the buffer (if no events after that seq have been evicted and the seq was
|
|
418
|
+
* indeed assigned by this channel) and returns the buffered events after it; otherwise
|
|
419
|
+
* miss (the caller should send `resync_required` first).
|
|
420
|
+
*/
|
|
421
|
+
replayAfter(lastEventId: string): {
|
|
422
|
+
hit: boolean;
|
|
423
|
+
events: ChannelEvent[];
|
|
424
|
+
};
|
|
425
|
+
private makeEvent;
|
|
426
|
+
}
|
|
427
|
+
interface ChannelHubOptions {
|
|
428
|
+
idleMs?: number;
|
|
429
|
+
/**
|
|
430
|
+
* Active check: keys for which this returns true are excluded from idle reclaim (app
|
|
431
|
+
* assembly injects `manager.statusOf(key) !== "idle"`, so a running/compacting
|
|
432
|
+
* Session channel is never reclaimed no matter how long since its last publish; a
|
|
433
|
+
* user channel key looks like `user:<id>` and is always considered active).
|
|
434
|
+
*/
|
|
435
|
+
isActive?: (key: string) => boolean;
|
|
436
|
+
}
|
|
437
|
+
/**
|
|
438
|
+
* Channel collection: lazily created by key (Session id or `user:<user_id>`);
|
|
439
|
+
* a channel whose Session is idle and has had no subscribers for over 30 minutes is
|
|
440
|
+
* reclaimed, releasing its buffer as well.
|
|
441
|
+
*/
|
|
442
|
+
declare class ChannelHub {
|
|
443
|
+
private readonly channels;
|
|
444
|
+
private readonly timer;
|
|
445
|
+
private readonly idleMs;
|
|
446
|
+
private isActive;
|
|
447
|
+
constructor(opts?: ChannelHubOptions);
|
|
448
|
+
/**
|
|
449
|
+
* Replaces the activity probe. "Is this session busy" is a business question the hub
|
|
450
|
+
* (runtime mechanism) cannot answer itself; the App installs its answer at create over
|
|
451
|
+
* the claimed hub — ordinary capability use, not a registry entry. Overwrite-only, the
|
|
452
|
+
* cross-generation discipline: a successor installs its own probe and a dead
|
|
453
|
+
* generation's is simply left behind (its manager has drained, so it reports idle —
|
|
454
|
+
* never wrong, only replaced).
|
|
455
|
+
*/
|
|
456
|
+
setActivityProbe(probe: (key: string) => boolean): void;
|
|
457
|
+
get(key: string): Channel;
|
|
458
|
+
peek(key: string): Channel | undefined;
|
|
459
|
+
/**
|
|
460
|
+
* Publishes to every live channel whose key starts with `prefix` — global
|
|
461
|
+
* notices (e.g. `user:` for "the web assets were hot-swapped, reload").
|
|
462
|
+
* Only reaches channels that exist (i.e. someone connected at least once);
|
|
463
|
+
* that is the right scope for a notice that only matters to live clients.
|
|
464
|
+
*/
|
|
465
|
+
broadcast(prefix: string, data: unknown, event?: string): void;
|
|
466
|
+
/** Reclaim idle channels (skips active Sessions: no reclaim even without a publish while awaiting approval); `now` is injectable for tests. */
|
|
467
|
+
sweep(now?: number): void;
|
|
468
|
+
dispose(): void;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Attribution context for one record (top-level Session scope). */
|
|
472
|
+
interface UsageContext {
|
|
473
|
+
projectId: string;
|
|
474
|
+
agentId: string;
|
|
475
|
+
/** Top-level Session id (the current actual id after self-heal). */
|
|
476
|
+
sessionId: string;
|
|
477
|
+
/** Vendor grouping for the top-level Session's model (paired with modelId; the fallback attribution when the origin mapping has no hit). */
|
|
478
|
+
provider: string;
|
|
479
|
+
/** Upstream model_id of the top-level Session (paired with provider). */
|
|
480
|
+
modelId: string;
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
/**
|
|
484
|
+
* Usage statistics query.
|
|
485
|
+
*
|
|
486
|
+
* Cost is **computed in real time**: usage_records only stores Tokens (pricing may
|
|
487
|
+
* be added later), so at query time each Model's cost is converted using the
|
|
488
|
+
* current Project's configured pricing — the repo returns raw Token totals broken
|
|
489
|
+
* down by `(provider, model_id)` paired reference, and this service looks up each
|
|
490
|
+
* reference's price once and folds it into cost / hasUncosted (if a Model has no
|
|
491
|
+
* pricing, its consumption is excluded from cost and hasUncosted is flagged).
|
|
492
|
+
* Summary cards (today / last 7 days / cumulative), grouped aggregation (date /
|
|
493
|
+
* agent / model / session, with the session dimension supporting agentId drill-down
|
|
494
|
+
* filtering), and the zero-filled time series the cost center's charts draw.
|
|
495
|
+
* Server-side error statistics (error_records) ride along on the same response:
|
|
496
|
+
* the statistics center fetches everything in one request, and filters are
|
|
497
|
+
* naturally shared; unattributed errors (login failures, process crashes, and other
|
|
498
|
+
* errors with no Project context) are visible only to admins, see the ErrorsRepo
|
|
499
|
+
* file header.
|
|
500
|
+
*/
|
|
501
|
+
|
|
502
|
+
/** The three pricing buckets (usd_per_mtok convention), returned by the pricing lookup callback. */
|
|
503
|
+
interface PricingRates {
|
|
504
|
+
cacheRead: number;
|
|
505
|
+
cacheWrite: number;
|
|
506
|
+
output: number;
|
|
507
|
+
}
|
|
508
|
+
/**
|
|
509
|
+
* What one Model costs, in the tiers it can be billed in.
|
|
510
|
+
*
|
|
511
|
+
* `offPeak` differs from `peak` only for a catalog row on a time-based schedule whose stored
|
|
512
|
+
* price is still the catalog's own. Both are returned together because a query spans time: a
|
|
513
|
+
* week that straddles the boundary contains Tokens of both kinds, and each half is priced at
|
|
514
|
+
* the rate it actually ran at rather than at whichever tier is in force when it is read.
|
|
515
|
+
*/
|
|
516
|
+
interface TieredRates {
|
|
517
|
+
peak: PricingRates;
|
|
518
|
+
offPeak: PricingRates;
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
declare const Errors_base: (abstract new () => {
|
|
522
|
+
record(args: ErrorRecordArgs): void;
|
|
523
|
+
}) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
524
|
+
/** Errors: the mechanism ErrorRecorder implements. */
|
|
525
|
+
declare abstract class Errors extends Errors_base {
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/** Capture-site source (maps one-to-one to error_records.source). */
|
|
529
|
+
type ErrorSource = "http" | "organization" | "session" | "llm" | "environment" | "compaction" | "usage" | "title" | "subagent" | "process" | "schedule" | "messaging";
|
|
530
|
+
/** Error classification: see file header — the criterion is "does a human need to step in". */
|
|
531
|
+
type ErrorKind = "expected" | "unexpected";
|
|
532
|
+
/** Attribution context (all optional: the login endpoint has no Project, and process-level fallback has no request at all). */
|
|
533
|
+
interface ErrorContext {
|
|
534
|
+
projectId?: string;
|
|
535
|
+
agentId?: string;
|
|
536
|
+
sessionId?: string;
|
|
537
|
+
}
|
|
538
|
+
interface ErrorRecordArgs {
|
|
539
|
+
source: ErrorSource;
|
|
540
|
+
/** The caught error (unknown: the value caught may not be an Error; failures from the message stream pass the reason text directly). */
|
|
541
|
+
err: unknown;
|
|
542
|
+
ctx?: ErrorContext;
|
|
543
|
+
/** Semantic code (required for non-HTTP sources, e.g. session_run_failed); defaults to HttpError.code. */
|
|
544
|
+
code?: string;
|
|
545
|
+
/** HTTP status code; leave empty for non-HTTP sources. */
|
|
546
|
+
status?: number;
|
|
547
|
+
/** Explicit classification (see file header); defaults to inferring from `HttpError` — HTTP sources rely on this, other sources should pass it explicitly. */
|
|
548
|
+
kind?: ErrorKind;
|
|
549
|
+
}
|
|
550
|
+
declare class ErrorRecorder implements Errors {
|
|
551
|
+
/** Dedup table: `source \0 code \0 projectId` → timestamp of the last **persist** (see file header). */
|
|
552
|
+
private readonly lastSeen;
|
|
553
|
+
private readonly errors;
|
|
554
|
+
private readonly clock;
|
|
555
|
+
/** Record an error (synchronous, fails silently; same-window duplicates are dropped outright, see file header). */
|
|
556
|
+
record(args: ErrorRecordArgs): void;
|
|
557
|
+
/** true if a same-kind error was already recorded within the window (drop it); otherwise register this persist timestamp and keep the dedup table bounded. */
|
|
558
|
+
private deduped;
|
|
559
|
+
/** Keep the dedup table bounded (see file header): clear expired entries first; if still over the limit (hundreds/thousands of distinct error codes erupting at once), clear it entirely. */
|
|
560
|
+
private evict;
|
|
561
|
+
}
|
|
562
|
+
/** Minimal dependency a capture site needs on the recorder (tests inject a fake; structurally matches SessionManager's UsageRecorderLike). */
|
|
563
|
+
type ErrorSink = Pick<ErrorRecorder, "record">;
|
|
564
|
+
|
|
565
|
+
/** Host-side parameters for one title-generation request. */
|
|
566
|
+
interface TitleRequest {
|
|
567
|
+
/** The user input this run started from: its first words become the immediate fallback title. */
|
|
568
|
+
fallbackText: string;
|
|
569
|
+
/** Material override (the main session passes its input text, a subagent the prompt that spawned it — assistant material stays empty either way, so the request never waits on model output); defaults to the material self-collected by the core Session. */
|
|
570
|
+
material?: {
|
|
571
|
+
userText: string;
|
|
572
|
+
assistantText: string;
|
|
573
|
+
};
|
|
574
|
+
/** The channel to push the `session_title` event to; defaults to `ctx.sessionId`. A
|
|
575
|
+
* subagent has no SSE channel of its own, so its title must reach the frontend via
|
|
576
|
+
* the **parent Session's** channel (the list updates in place by sessionId). */
|
|
577
|
+
notifyOn?: string;
|
|
578
|
+
}
|
|
579
|
+
/** session-manager's minimal dependency on the title generator (tests inject a fake implementation). */
|
|
580
|
+
interface TitleNotifier {
|
|
581
|
+
maybeGenerate(ctx: UsageContext, session: Pick<RuntimeSession, "generateTitle">, req: TitleRequest): void;
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
/**
|
|
585
|
+
* trace_files / trace_sessions repo: the Trace-tree derived cache (see schema.ts and
|
|
586
|
+
* services/trace-index.ts for the cache rules). Pure row access — reconciliation
|
|
587
|
+
* policy (mtime gates, head-reads) lives in the service.
|
|
588
|
+
*/
|
|
589
|
+
|
|
590
|
+
/** One indexed Trace shard. */
|
|
591
|
+
interface TraceFileRow {
|
|
592
|
+
projectId: string;
|
|
593
|
+
agentId: string;
|
|
594
|
+
sessionId: string;
|
|
595
|
+
fileIndex: number;
|
|
596
|
+
date: string;
|
|
597
|
+
sizeBytes: number;
|
|
598
|
+
}
|
|
599
|
+
/** One Session's registration-time facts (earliest shard's head). */
|
|
600
|
+
interface TraceSessionRow {
|
|
601
|
+
sessionId: string;
|
|
602
|
+
projectId: string;
|
|
603
|
+
agentId: string;
|
|
604
|
+
source: SessionSource | null;
|
|
605
|
+
workspace: string;
|
|
606
|
+
title: string | null;
|
|
607
|
+
provider: string | null;
|
|
608
|
+
modelId: string | null;
|
|
609
|
+
firstTs: string | null;
|
|
610
|
+
metaRead: boolean;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
declare const TraceIndexStore_base: (abstract new () => {
|
|
614
|
+
upsertFile(row: TraceFileRow): void;
|
|
615
|
+
updateFileSize(projectId: string, agentId: string, sessionId: string, fileIndex: number, sizeBytes: number): void;
|
|
616
|
+
getPageStats(projectId: string, agentId: string, sessionId: string, fileIndex: number): string | null;
|
|
617
|
+
setPageStats(projectId: string, agentId: string, sessionId: string, fileIndex: number, sizeBytes: number, pageStats: string): void;
|
|
618
|
+
deleteFile(projectId: string, agentId: string, sessionId: string, fileIndex: number): void;
|
|
619
|
+
listFilesByAgent(projectId: string, agentId: string): TraceFileRow[];
|
|
620
|
+
listFilesBySession(projectId: string, agentId: string, sessionId: string): TraceFileRow[];
|
|
621
|
+
findAgentBySession(projectId: string, sessionId: string): string | null;
|
|
622
|
+
upsertSession(row: TraceSessionRow): void;
|
|
623
|
+
getSession(sessionId: string): TraceSessionRow | null;
|
|
624
|
+
listSessionsByAgent(projectId: string, agentId: string): TraceSessionRow[];
|
|
625
|
+
deleteBySession(sessionId: string): void;
|
|
626
|
+
deleteByAgent(projectId: string, agentId: string): void;
|
|
627
|
+
deleteByProject(projectId: string): void;
|
|
628
|
+
}) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
629
|
+
/** TraceIndexStore: the mechanism TraceIndexRepo implements. */
|
|
630
|
+
declare abstract class TraceIndexStore extends TraceIndexStore_base {
|
|
631
|
+
}
|
|
632
|
+
declare const TraceIndex_base: (abstract new () => {
|
|
633
|
+
readonly counters: {
|
|
634
|
+
gateStats: number;
|
|
635
|
+
dirScans: number;
|
|
636
|
+
headReads: number;
|
|
637
|
+
};
|
|
638
|
+
reconcileAgent(projectId: string, agentId: string, opts?: {
|
|
639
|
+
force?: boolean;
|
|
640
|
+
}): Promise<void>;
|
|
641
|
+
reconcileProject(projectId: string, opts?: {
|
|
642
|
+
force?: boolean;
|
|
643
|
+
}): Promise<void>;
|
|
644
|
+
registerImportedFile(args: {
|
|
645
|
+
projectId: string;
|
|
646
|
+
agentId: string;
|
|
647
|
+
sessionId: string;
|
|
648
|
+
fileIndex: number;
|
|
649
|
+
date: string;
|
|
650
|
+
sizeBytes: number;
|
|
651
|
+
records: OmniMessage[];
|
|
652
|
+
}): void;
|
|
653
|
+
removeSession(projectId: string, agentId: string, sessionId: string): void;
|
|
654
|
+
removeAgent(projectId: string, agentId: string): void;
|
|
655
|
+
removeProject(projectId: string): void;
|
|
656
|
+
}) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
657
|
+
/** TraceIndex: the mechanism TraceIndexService implements. */
|
|
658
|
+
declare abstract class TraceIndex extends TraceIndex_base {
|
|
659
|
+
}
|
|
660
|
+
|
|
661
|
+
/**
|
|
662
|
+
* "desktop" and "setup" may set a password without the old one (theirs is random and was never
|
|
663
|
+
* shown); desktop-only routes open to "desktop" alone. "token" is the local API token's Bearer
|
|
664
|
+
* header — per request, never a stored row. Anything else reads as "password".
|
|
665
|
+
*/
|
|
666
|
+
type SessionVia = "password" | "desktop" | "setup" | "token";
|
|
667
|
+
|
|
668
|
+
interface UserRow {
|
|
669
|
+
userId: string;
|
|
670
|
+
passwordHash: string;
|
|
671
|
+
isAdmin: boolean;
|
|
672
|
+
/** Still using the initial password (seeded / set by an admin); cleared to 0 once the user changes it. */
|
|
673
|
+
passwordIsInitial: boolean;
|
|
674
|
+
/** Nickname shown in place of the id; null = never set. */
|
|
675
|
+
displayName: string | null;
|
|
676
|
+
/** Avatar as a data URL; null = never set. */
|
|
677
|
+
avatar: string | null;
|
|
678
|
+
createdAt: string;
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
type RawTable = Record<string, unknown>;
|
|
682
|
+
/**
|
|
683
|
+
* What one utility completion produced: the model's text, or why there is none. A bare null
|
|
684
|
+
* would say "no answer" and nothing else — and every one of these failures (no model
|
|
685
|
+
* configured, a missing credential, a provider rejection, a timeout, an answer that was all
|
|
686
|
+
* thinking) is one the user can act on, so the reason travels to the caller, which records it.
|
|
687
|
+
*/
|
|
688
|
+
type UtilityCompletion = {
|
|
689
|
+
ok: true;
|
|
690
|
+
text: string;
|
|
691
|
+
} | {
|
|
692
|
+
ok: false;
|
|
693
|
+
/** `no_model`: the Project names no default model, or names one it has no entry for — nothing was asked. `failed`: the model was asked and produced no text. */
|
|
694
|
+
cause: "no_model" | "failed";
|
|
695
|
+
error: string;
|
|
696
|
+
};
|
|
697
|
+
|
|
698
|
+
declare const ProjectConfigStore_base: (abstract new () => {
|
|
699
|
+
readRaw(projectId: string): Promise<RawTable>;
|
|
700
|
+
loadConfig(projectId: string): Promise<ProjectConfig>;
|
|
701
|
+
writeRaw(projectId: string, data: RawTable): Promise<void>;
|
|
702
|
+
writeInitialConfig(projectId: string, name: string): Promise<void>;
|
|
703
|
+
ensurePresetModels(projectId: string): Promise<void>;
|
|
704
|
+
getName(projectId: string): Promise<string | undefined>;
|
|
705
|
+
setName(projectId: string, name: string): Promise<void>;
|
|
706
|
+
getDefaultModelRef(projectId: string): Promise<ModelRef | undefined>;
|
|
707
|
+
setDefaultModelRef(projectId: string, ref: ModelRefDto): Promise<ModelRefDto>;
|
|
708
|
+
getChatDefaults(projectId: string): Promise<ChatDefaultsDto>;
|
|
709
|
+
setChatDefaults(projectId: string, req: ChatDefaultsDto): Promise<ChatDefaultsDto>;
|
|
710
|
+
getCommandPolicy(projectId: string): Promise<CommandPolicyDto>;
|
|
711
|
+
setCommandPolicy(projectId: string, req: {
|
|
712
|
+
enabled?: boolean;
|
|
713
|
+
rules: {
|
|
714
|
+
name: string;
|
|
715
|
+
pattern: string;
|
|
716
|
+
description?: string;
|
|
717
|
+
enabled?: boolean;
|
|
718
|
+
}[];
|
|
719
|
+
}): Promise<CommandPolicyDto>;
|
|
720
|
+
getPricing(projectId: string, provider: string, modelId: string): Promise<TieredRates | undefined>;
|
|
721
|
+
detectVision(projectId: string, req: ModelVisionDetectRequest): Promise<ModelVisionDetectResponse>;
|
|
722
|
+
testModel(projectId: string, req: ModelTestRequest): Promise<ModelTestResponse>;
|
|
723
|
+
detectProtocol(projectId: string, req: ModelProtocolDetectRequest): Promise<ModelProtocolDetectResponse>;
|
|
724
|
+
listEndpointModels(req: EndpointModelListRequest, listImpl?: (options: ListEndpointModelsOptions) => Promise<string[]>, timeoutMs?: number): Promise<EndpointModelListResponse>;
|
|
725
|
+
getModels(projectId: string): Promise<ModelsResponse>;
|
|
726
|
+
updateModels(projectId: string, req: ModelsUpdateRequest): Promise<ModelsResponse>;
|
|
727
|
+
setGroupApiKey(projectId: string, provider: string, apiKey: string): Promise<number>;
|
|
728
|
+
completeOnce(projectId: string, prompt: string): Promise<UtilityCompletion>;
|
|
729
|
+
}) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
730
|
+
/** ProjectConfigStore: the mechanism ProjectConfigService implements. */
|
|
731
|
+
declare abstract class ProjectConfigStore extends ProjectConfigStore_base {
|
|
732
|
+
}
|
|
733
|
+
|
|
734
|
+
interface SessionServiceDeps {
|
|
735
|
+
root: string;
|
|
736
|
+
sessions: SessionIndex;
|
|
737
|
+
manager: SessionManager;
|
|
738
|
+
projectConfig: ProjectConfigStore;
|
|
739
|
+
/** In-process origin registry derived from session_meta (the DB stores no source column). */
|
|
740
|
+
sources: SessionOrigins;
|
|
741
|
+
/** Trace-file index: discovery / adoption / stats serve from it (mtime-gated reconciler; no per-request walks). */
|
|
742
|
+
traceIndex: TraceIndex;
|
|
743
|
+
/** The index rows themselves (files and sessions), for the reads the service does directly. */
|
|
744
|
+
traceStore: TraceIndexStore;
|
|
745
|
+
/**
|
|
746
|
+
* Admin proxy-settings threading (same getter the session loader passes, see
|
|
747
|
+
* createCoreSessionLoader): the runtime created here is adopted by the manager and
|
|
748
|
+
* runs the Session's first Task, so it needs the command-subprocess proxy policy too.
|
|
749
|
+
*/
|
|
750
|
+
proxyEnv?: () => ProxyEnvPolicy | null;
|
|
751
|
+
/**
|
|
752
|
+
* Harness-control env threading (same policy the session loader passes): the server's
|
|
753
|
+
* API URL/token plus the Session coordinates, injected into command subprocesses so
|
|
754
|
+
* agents can drive the harness back through the CLI/API.
|
|
755
|
+
*/
|
|
756
|
+
controlEnv?: (ctx: ControlEnvContext) => Record<string, string>;
|
|
757
|
+
/**
|
|
758
|
+
* PATH threading (same getter the session loader passes): the shim directory holding
|
|
759
|
+
* this harness's own `penguin`, put in front of every command an Agent runs.
|
|
760
|
+
*/
|
|
761
|
+
pathPrepend?: () => string[];
|
|
762
|
+
/**
|
|
763
|
+
* The channel of the Session's ENABLED messaging binding, or null when none is enabled
|
|
764
|
+
* (SessionInfo.messagingChannel, the sidebar row's per-channel indicator — saved-but-
|
|
765
|
+
* disabled configs stay off the row). A lookup lambda rather than the repo, so the
|
|
766
|
+
* service stays decoupled from the bindings table; absent (older assemblies/tests)
|
|
767
|
+
* means the field is never set.
|
|
768
|
+
*/
|
|
769
|
+
messagingChannel?: (sessionId: string) => MessagingChannel$1 | null;
|
|
770
|
+
/**
|
|
771
|
+
* Company mode: the organization owning a Session (a desk session, or a session
|
|
772
|
+
* contributing to a ticket), for `SessionInfo.orgId` — development mode's list hides
|
|
773
|
+
* those rows, the company sidebar groups them. Two shapes because the two flows cost
|
|
774
|
+
* differently: the single-Session GET asks about one id, a list asks once for the whole
|
|
775
|
+
* Project and looks its rows up in the returned map, so a long list never costs a query
|
|
776
|
+
* per row. Lambdas rather than the repo, so the service stays decoupled from the
|
|
777
|
+
* company-mode caches; absent (older assemblies/tests) means the field is never set.
|
|
778
|
+
*/
|
|
779
|
+
orgIdOfSession?: (sessionId: string) => string | undefined;
|
|
780
|
+
orgIdsOfProject?: (projectId: string) => ReadonlyMap<string, string>;
|
|
781
|
+
/** Spawn-confinement getter (the sandbox module's), forwarded into core beside proxyEnv. */
|
|
782
|
+
confineSpawn?: () => SpawnConfiner | null;
|
|
783
|
+
}
|
|
784
|
+
declare class SessionService {
|
|
785
|
+
private readonly deps;
|
|
786
|
+
constructor(deps: SessionServiceDeps);
|
|
787
|
+
/**
|
|
788
|
+
* DB row -> SessionInfo (run status and pending approval count come from session-manager).
|
|
789
|
+
* Async because `source` is derived from session_meta: a registry miss (Session predating
|
|
790
|
+
* this process) falls back to reading the Trace head once (see sourceOf). `traces` is the
|
|
791
|
+
* list flow's one-walk discovery result; without it a miss locates the shard itself.
|
|
792
|
+
*
|
|
793
|
+
* `orgIds` is the list flow's one-query organization map (see listSessions); without it the
|
|
794
|
+
* organization is a point lookup, which is what the single-Session paths want.
|
|
795
|
+
*/
|
|
796
|
+
toInfo(row: SessionRow, hasTrace: boolean, orgIds?: ReadonlyMap<string, string>): Promise<SessionInfo>;
|
|
797
|
+
/**
|
|
798
|
+
* A Session's origin, with session_meta as the single source of truth: the in-process
|
|
799
|
+
* registry answers first (populated at creation / subagent registration / adoption /
|
|
800
|
+
* index registration); on a miss (a Session created before this process started) the
|
|
801
|
+
* trace index's registration-time facts answer — the reconciler head-read the earliest
|
|
802
|
+
* shard once when the file first appeared, so no file is touched here. A Session with
|
|
803
|
+
* no Trace yet stays unknown and is NOT cached negatively — its meta may appear with
|
|
804
|
+
* the first run.
|
|
805
|
+
*/
|
|
806
|
+
private sourceOf;
|
|
807
|
+
/** Whether this Session already has a Trace record (a Task has been run): answered by the index (reconciled first). */
|
|
808
|
+
hasTrace(row: SessionRow): Promise<boolean>;
|
|
809
|
+
/**
|
|
810
|
+
* The list category of a row: archived wins (an explicit user action), then the
|
|
811
|
+
* origin's bucket, and no/unknown source is `active` — the same precedence the
|
|
812
|
+
* sidebar's partition applies to loaded rows, so server filtering and client
|
|
813
|
+
* rendering can never disagree.
|
|
814
|
+
*/
|
|
815
|
+
private categoryOf;
|
|
816
|
+
/**
|
|
817
|
+
* List, sorted by createdAt descending. Every row is served **straight from the DB**,
|
|
818
|
+
* whichever client created it, with no Trace directory scanning — the answer to
|
|
819
|
+
* many-session sidebar reloads re-walking the filesystem on every request (#139).
|
|
820
|
+
* Sessions living only in the Trace directory were adopted into the index by the
|
|
821
|
+
* boot-time sweep (`adoptUnmanagedTraceSessions`), so listing never discovers. One
|
|
822
|
+
* lazy discovery walk still runs for a list call that contains rows this process has
|
|
823
|
+
* not classified yet (no in-process source entry): it supplies the Trace locations for
|
|
824
|
+
* the one-time head reads and backfills the `has_trace` cache; once every row is
|
|
825
|
+
* classified, list calls touch only the DB.
|
|
826
|
+
*
|
|
827
|
+
* Optional `paging` returns just that slice (the sidebar pages with limit+1 to detect
|
|
828
|
+
* "has more"); slicing happens before toInfo, so per-request source derivation (lazy
|
|
829
|
+
* Trace-head reads) stays bounded by the page size.
|
|
830
|
+
*
|
|
831
|
+
* `category` filters to one sidebar bucket **before** paging, so offset/limit page
|
|
832
|
+
* within the category. Filtering needs each walked row's category (a possible
|
|
833
|
+
* Trace-head read per row, cached in the sources registry); without `withCounts`
|
|
834
|
+
* the walk stops as soon as the requested page is complete. `withCounts` classifies
|
|
835
|
+
* every row and returns per-category totals over the whole list — plus the same
|
|
836
|
+
* totals broken down by Workspace path, and each path's newest Session's `createdAt` —
|
|
837
|
+
* so the sidebar can label the collapsed folders, list every Workspace that holds
|
|
838
|
+
* Sessions (not only the ones its loaded pages happen to touch) and place the groups
|
|
839
|
+
* by recency, all without loading them.
|
|
840
|
+
*
|
|
841
|
+
* `workspaceGroup` filters the same way, to one Workspace group (see workspace-group.ts),
|
|
842
|
+
* so a sidebar grouped by Workspace pages each group down its OWN stream instead of
|
|
843
|
+
* sharing one per-Agent cursor — without it, one group's "load more" consumes the page
|
|
844
|
+
* its siblings were about to read, and their rows move on screen untouched. The two
|
|
845
|
+
* filters compose; the returned counts stay whole-Agent either way.
|
|
846
|
+
*/
|
|
847
|
+
listSessions(projectId: string, agentId: string, opts?: {
|
|
848
|
+
paging?: {
|
|
849
|
+
offset: number;
|
|
850
|
+
limit: number;
|
|
851
|
+
};
|
|
852
|
+
category?: SessionCategory;
|
|
853
|
+
workspaceGroup?: string;
|
|
854
|
+
withCounts?: boolean;
|
|
855
|
+
}): Promise<{
|
|
856
|
+
sessions: SessionInfo[];
|
|
857
|
+
counts?: SessionCategoryCounts;
|
|
858
|
+
workspaceCounts?: Record<string, SessionCategoryCounts>;
|
|
859
|
+
workspaceLatest?: Record<string, string>;
|
|
860
|
+
}>;
|
|
861
|
+
/**
|
|
862
|
+
* Session stats (Agents list card): total count = size of the union of DB index
|
|
863
|
+
* ∪ Trace directory discovery; activity = number of active Sessions per day over
|
|
864
|
+
* the last `days` days (deduplicated count of Sessions created that day or with a
|
|
865
|
+
* Trace record that day; index 0 = earliest, last index = today). Counts only —
|
|
866
|
+
* does not backfill index rows.
|
|
867
|
+
*/
|
|
868
|
+
sessionStats(projectId: string, agentId: string, days: number): Promise<{
|
|
869
|
+
sessionCount: number;
|
|
870
|
+
activity: number[];
|
|
871
|
+
}>;
|
|
872
|
+
/**
|
|
873
|
+
* Create a Session: the model reference is a complete `(provider, modelId)` pair.
|
|
874
|
+
* Half a reference is a client error, never something to resolve — the missing half
|
|
875
|
+
* is never guessed, since a guessed provider would send an entry's credential to a
|
|
876
|
+
* vendor nobody named. Omitting both falls back to the Project's default reference
|
|
877
|
+
* (400 prompting to configure a model first if there is none). `workspace` is already
|
|
878
|
+
* validated by the route guard. The new Session is added to the active table
|
|
879
|
+
* (idle).
|
|
880
|
+
*/
|
|
881
|
+
createSession(args: {
|
|
882
|
+
projectId: string;
|
|
883
|
+
agentId: string;
|
|
884
|
+
/** Upstream id of the session's model; always paired with provider. Omit both for the Project's default reference. */
|
|
885
|
+
modelId?: string;
|
|
886
|
+
/** The provider group for `modelId`; always paired with modelId, never inferred. */
|
|
887
|
+
provider?: string;
|
|
888
|
+
workspace?: string;
|
|
889
|
+
approvalMode?: ApprovalMode;
|
|
890
|
+
/**
|
|
891
|
+
* Session source marker: `schedule` when triggered by a scheduled task, `benchmark` when
|
|
892
|
+
* created by a Benchmark evaluation or optimization (the only value a client may send);
|
|
893
|
+
* defaults to user-created.
|
|
894
|
+
*/
|
|
895
|
+
source?: "schedule" | "benchmark";
|
|
896
|
+
/**
|
|
897
|
+
* Creating-client hint stored on the index row (`POST .../sessions` body `client`):
|
|
898
|
+
* "cli" from the CLI, defaulting to "web". "org" is not accepted over HTTP — the
|
|
899
|
+
* organization runtime calls this method directly and is the only caller that passes
|
|
900
|
+
* it, so no request can claim an organization's provenance for itself.
|
|
901
|
+
*/
|
|
902
|
+
client?: "web" | "cli" | "org";
|
|
903
|
+
}): Promise<SessionInfo>;
|
|
904
|
+
/**
|
|
905
|
+
* Absolute path of a Session's **latest** Trace file (the current context shard);
|
|
906
|
+
* undefined when no Trace exists. Costs a directory walk, so only the single-session
|
|
907
|
+
* GET includes it in the DTO (see SessionInfo.tracePath) — the web's `/model` switch
|
|
908
|
+
* hands it to the new session's `[model_switch_from]` block so the model can read the
|
|
909
|
+
* source history itself when it needs it.
|
|
910
|
+
*/
|
|
911
|
+
latestTracePath(row: SessionRow): Promise<string | undefined>;
|
|
912
|
+
/**
|
|
913
|
+
* One walk over the Trace directory: session_id → its **earliest** shard (the shard
|
|
914
|
+
* whose head carries the original session_meta). Discovery (which Sessions have
|
|
915
|
+
* records) and the meta-read location come out of a single pass, so classifying every
|
|
916
|
+
* row (`counts=1`) costs one directory walk total instead of one per Session.
|
|
917
|
+
*/
|
|
918
|
+
private discoverTraces;
|
|
919
|
+
/**
|
|
920
|
+
* Startup adoption sweep: walks the whole trace tree once per boot and adopts every
|
|
921
|
+
* unmanaged Session (a Trace with no index row — legacy CLI-direct runs) as a
|
|
922
|
+
* `client: "cli"` row, so lists stay pure-SQLite afterwards. Enumerates
|
|
923
|
+
* `<root>/<project>/agents/<agent>/` directories directly — legacy Traces can live
|
|
924
|
+
* under Projects the DB has never seen — and reuses the existing discovery/adoption
|
|
925
|
+
* path per Agent (mtime-gated TraceIndexService reconcile, then registration-time
|
|
926
|
+
* facts; adoption reads no file itself). Idempotent (insertOrIgnore), so re-running —
|
|
927
|
+
* e.g. after a hot swap re-assembles the business surface — only costs the gated
|
|
928
|
+
* reconcile. Returns the number of rows adopted.
|
|
929
|
+
*/
|
|
930
|
+
adoptUnmanagedTraceSessions(): Promise<number>;
|
|
931
|
+
/**
|
|
932
|
+
* Adopts a Session that exists only in the Trace directory, from the index's
|
|
933
|
+
* registration-time facts (the reconciler head-read the earliest shard's session_meta
|
|
934
|
+
* once when the file first appeared — adoption itself reads no file).
|
|
935
|
+
*/
|
|
936
|
+
private adoptTraceSession;
|
|
937
|
+
}
|
|
938
|
+
|
|
939
|
+
/** Minimal interface for a runtime Session (satisfied by core Session; tests may inject a fake implementation). */
|
|
940
|
+
interface RuntimeSession {
|
|
941
|
+
readonly sessionId: string;
|
|
942
|
+
run(newMessages: OmniMessage[], opts: {
|
|
943
|
+
approve: ApproveFn;
|
|
944
|
+
signal: AbortSignal;
|
|
945
|
+
}): AsyncGenerator<OmniMessage>;
|
|
946
|
+
compact(opts: {
|
|
947
|
+
signal: AbortSignal;
|
|
948
|
+
}): AsyncGenerator<OmniMessage>;
|
|
949
|
+
/**
|
|
950
|
+
* The Session's thinking level (core `Session.thinkingLevel`, plain state): soft-limited —
|
|
951
|
+
* an assignment applies from the Session's very next LLM request. Optional: test fakes may
|
|
952
|
+
* omit it.
|
|
953
|
+
*/
|
|
954
|
+
thinkingLevel?: ThinkingLevelName;
|
|
955
|
+
/** Whether compaction is possible and why; when not ok, compact() yields no messages (see core ContextEngine.compactability). */
|
|
956
|
+
compactability(): CompactAvailability;
|
|
957
|
+
/** Queues a mid-run steering input (core `Session.steer`); false when no Task is running. */
|
|
958
|
+
steer(input: OmniMessage[]): boolean;
|
|
959
|
+
/**
|
|
960
|
+
* Withdraws a steering input queued via `steer` before core delivers it (core
|
|
961
|
+
* `Session.unsteer`, matched by input-list identity); false when it is no longer queued —
|
|
962
|
+
* already delivered, or the run exited. Optional: test fakes may omit it, in which case
|
|
963
|
+
* every recall reports "already delivered".
|
|
964
|
+
*/
|
|
965
|
+
unsteer?(input: OmniMessage[]): boolean;
|
|
966
|
+
/** Skips the in-progress reconnect backoff, firing the next retry immediately (core `Session.skipReconnectWait`); false when no wait is in progress. */
|
|
967
|
+
skipReconnectWait(): boolean;
|
|
968
|
+
/**
|
|
969
|
+
* Runs the named package's `user_prompt` hook (core `Session.runUserPromptHook` — hooks
|
|
970
|
+
* run in core and nowhere else); null = the package is not installed or names no such
|
|
971
|
+
* command. Optional: test fakes may omit it, reading as not installed.
|
|
972
|
+
*/
|
|
973
|
+
runUserPromptHook?(name: string, prompt: string, extras?: Record<string, string | number | boolean>): Promise<{
|
|
974
|
+
context?: string;
|
|
975
|
+
} | null>;
|
|
976
|
+
/** A tool's permission level from the running context's toolset (core `Session.toolPermission`; strict-tier — rebuilt at rotation). */
|
|
977
|
+
toolPermission(name: string): "r" | "rw" | undefined;
|
|
978
|
+
/**
|
|
979
|
+
* Out-of-band one-shot request for title generation (core `Session.generateTitle`,
|
|
980
|
+
* writes no history/Trace). Material defaults to what the Session collects itself
|
|
981
|
+
* (the first Task's text gathered during run); `material` overrides this for
|
|
982
|
+
* subagents.
|
|
983
|
+
*/
|
|
984
|
+
generateTitle(args?: {
|
|
985
|
+
material?: {
|
|
986
|
+
userText: string;
|
|
987
|
+
assistantText: string;
|
|
988
|
+
};
|
|
989
|
+
signal?: AbortSignal;
|
|
990
|
+
}): Promise<SessionTitleResult>;
|
|
991
|
+
/**
|
|
992
|
+
* Subscribes the idle-arrival signal for background-task completion notices (core
|
|
993
|
+
* `Session.onBackgroundNotice`): fired when a `run_in_background` launch settles while no
|
|
994
|
+
* Task is running — the manager then takes the queue and submits it as an ordinary task.
|
|
995
|
+
* Optional: test fakes may omit it.
|
|
996
|
+
*/
|
|
997
|
+
onBackgroundNotice?(listener: () => void): void;
|
|
998
|
+
/** Takes the queued background completion notices as task input (core `Session.takeBackgroundNotices`). Optional, like onBackgroundNotice. */
|
|
999
|
+
takeBackgroundNotices?(): OmniMessage[];
|
|
1000
|
+
/** Whether completion notices are still queued (core `Session.hasPendingBackgroundNotices`); pins the entry against idle eviction. Optional, like onBackgroundNotice. */
|
|
1001
|
+
hasPendingBackgroundNotices?(): boolean;
|
|
1002
|
+
/** Refreshes the listen-port probes behind the process list's `serviceUrl` (core `Session.probeBackgroundCommandServices`). Optional: test fakes may omit it. */
|
|
1003
|
+
probeBackgroundCommandServices?(): Promise<void>;
|
|
1004
|
+
/** Subscribes live-forwarded background-subagent messages (core `Session.onBackgroundMessage`); the manager publishes them to the session channel. Optional: test fakes may omit it. */
|
|
1005
|
+
onBackgroundMessage?(listener: (msg: OmniMessage) => void): void;
|
|
1006
|
+
/** Background command processes owned by the Session's environment (core `Session.listBackgroundCommands`). Optional: test fakes may omit it. */
|
|
1007
|
+
listBackgroundCommands?(): BackgroundCommandInfo[];
|
|
1008
|
+
/** Kills one background command process (core `Session.killBackgroundCommand`); false when the id is unknown. Optional, like listBackgroundCommands. */
|
|
1009
|
+
killBackgroundCommand?(processId: string): boolean;
|
|
1010
|
+
/** Hands one EXECUTING tool call back as a background task (core `Session.detachToolCall`). Optional: test fakes may omit it, reading as "not_running". */
|
|
1011
|
+
detachToolCall?(toolCallId: string): ToolDetachResult;
|
|
1012
|
+
/** Whether a background subagent is mid-round (core `Session.hasRunningBackgroundSubagents`); pins the entry against idle eviction. Optional, like listBackgroundCommands. */
|
|
1013
|
+
hasRunningBackgroundSubagents?(): boolean;
|
|
1014
|
+
/** All live subagent child sessions of the Session's environment (core `Session.listBackgroundSubagents`). Optional, like listBackgroundCommands. */
|
|
1015
|
+
listBackgroundSubagents?(): BackgroundSubagentInfo[];
|
|
1016
|
+
/** Host message to one child session — steering mid-run, a follow-up run when idle, a revival when released (core `Session.sendToBackgroundSubagent`). Optional. */
|
|
1017
|
+
sendToBackgroundSubagent?(childSessionId: string, messages: OmniMessage[], opts?: SubagentMessageOptions): Promise<SubagentMessageOutcome>;
|
|
1018
|
+
/** Subscribes the host's session-lifetime fallback approval sink for child sessions (core `Session.setSubagentApprovalFallback`). Optional. */
|
|
1019
|
+
setSubagentApprovalFallback?(approve: ApproveFn): void;
|
|
1020
|
+
/** Aborts one child session's current run, keeping the session (core `Session.abortBackgroundSubagentRun`); false when unknown or idle. Optional. */
|
|
1021
|
+
abortBackgroundSubagentRun?(childSessionId: string): boolean;
|
|
1022
|
+
/** Pins one live child session's thinking level (core `Session.setBackgroundSubagentThinkingLevel`); false when the child is not live. Optional. */
|
|
1023
|
+
setBackgroundSubagentThinkingLevel?(childSessionId: string, level: ThinkingLevelName): boolean;
|
|
1024
|
+
/** Subscribes subagent run-state changes (core `Session.onSubagentState`): the manager republishes `task_state` with the fresh live listing. Optional. */
|
|
1025
|
+
onSubagentState?(listener: () => void): void;
|
|
1026
|
+
/** Subscribes background-task state changes (core `Session.onBackgroundState`): the manager re-counts the live registries and publishes `session_background` when the counts moved. Optional. */
|
|
1027
|
+
onBackgroundState?(listener: () => void): void;
|
|
1028
|
+
/** Releases environment resources — kills the remaining background processes (core `Session.dispose`). Optional, idempotent. */
|
|
1029
|
+
dispose?(): void;
|
|
1030
|
+
}
|
|
1031
|
+
/** The underlying loader behind get-or-resume-or-heal. */
|
|
1032
|
+
interface SessionLoader {
|
|
1033
|
+
/**
|
|
1034
|
+
* Load a runtime Session from an index row: recover (with a Trace) or self-heal
|
|
1035
|
+
* rebuild (no Trace, session_id will change). Throws HttpError(409) for unrecoverable
|
|
1036
|
+
* cases such as a missing Workspace.
|
|
1037
|
+
*/
|
|
1038
|
+
load(row: SessionRow): Promise<RuntimeSession>;
|
|
1039
|
+
}
|
|
1040
|
+
interface UsageRecorderLike {
|
|
1041
|
+
record(ctx: UsageContext, msg: OmniMessage): Promise<void>;
|
|
1042
|
+
}
|
|
1043
|
+
interface SessionManagerDeps {
|
|
1044
|
+
sessions: SessionIndex;
|
|
1045
|
+
channels: ChannelHub;
|
|
1046
|
+
loader: SessionLoader;
|
|
1047
|
+
/** Session-origin registry (session_meta is the single source of truth; subagent registration records the forwarded meta's source here). */
|
|
1048
|
+
sources: SessionOrigins;
|
|
1049
|
+
recorder: UsageRecorderLike;
|
|
1050
|
+
/** Automatic Session title generation (optional: not injected in tests or when disabled). */
|
|
1051
|
+
titles?: TitleNotifier;
|
|
1052
|
+
/** Error persistence (optional: without it, only logs — same as before this was wired up). */
|
|
1053
|
+
errors?: ErrorSink;
|
|
1054
|
+
log?: (line: string) => void;
|
|
1055
|
+
/** Goal run-state persistence (optional like `titles`: without it, goals run but leave no restorable record). */
|
|
1056
|
+
/**
|
|
1057
|
+
* Publishes a Session-scoped event on the user-level channel of everyone who can see the
|
|
1058
|
+
* Project. The audience lookup (owner + members) and the `user:<id>` channel binding stay in
|
|
1059
|
+
* the app layer, the same split the scheduler's `notify` uses — this class holds no Project
|
|
1060
|
+
* membership repos. Optional: without it only the per-Session channel is served, which is
|
|
1061
|
+
* what unit tests that don't wire it get.
|
|
1062
|
+
*/
|
|
1063
|
+
notifyProjectUsers?: (projectId: string, event: ServerEvent) => void;
|
|
1064
|
+
/**
|
|
1065
|
+
* Clock for persisted timestamps (last_active_at). Injected like the other services' so a
|
|
1066
|
+
* stubbed clock moves this and `usage_records.ts` together — the pairing the legacy
|
|
1067
|
+
* backfill assumes when it reads MAX(ts) as a session's last activity.
|
|
1068
|
+
*/
|
|
1069
|
+
now?: () => Date;
|
|
1070
|
+
/**
|
|
1071
|
+
* Called when a Task starts (or is queued) from a PERSON's input — a message typed in the
|
|
1072
|
+
* Web App, the CLI or a bound messaging bot, as opposed to one the harness injected
|
|
1073
|
+
* (`sender: "server"` / `"harness"`). Company mode uses it to reset the desk's @-chain
|
|
1074
|
+
* accounting: a person's message opens a fresh chain (hop 0), whatever mention last woke
|
|
1075
|
+
* the desk. Optional; unit tests that do not wire it get nothing.
|
|
1076
|
+
*/
|
|
1077
|
+
onHumanInput?: (sessionId: string) => void;
|
|
1078
|
+
}
|
|
1079
|
+
/**
|
|
1080
|
+
* A queued message's original content, held while it waits in a queue so a recall (DELETE
|
|
1081
|
+
* /steer/:id, DELETE /follow-ups/:id) can hand it back to the composer for editing (#287).
|
|
1082
|
+
* `text` and `images` are the same strings the queued input already references (no extra
|
|
1083
|
+
* copy); files stay on disk in the Session scratchpad and are read back only at recall.
|
|
1084
|
+
*/
|
|
1085
|
+
interface RecallStore {
|
|
1086
|
+
text: string;
|
|
1087
|
+
images: string[];
|
|
1088
|
+
files: RecallableFile[];
|
|
1089
|
+
}
|
|
1090
|
+
declare class SessionManager {
|
|
1091
|
+
private readonly deps;
|
|
1092
|
+
private readonly entries;
|
|
1093
|
+
/** Per-Session mutex (serializes get-or-load and status flips); auto-cleaned once the chain drains. */
|
|
1094
|
+
private readonly locks;
|
|
1095
|
+
private readonly log;
|
|
1096
|
+
/** Graceful-shutdown flag: once set, new Tasks/compactions are rejected (503). */
|
|
1097
|
+
private closed;
|
|
1098
|
+
/** Agents currently being deleted (key = agentKey): new Tasks/compactions are always rejected with 409 during this window. */
|
|
1099
|
+
private readonly deletingAgents;
|
|
1100
|
+
/** Sessions currently being deleted (guards against the entry/Trace file being rebuilt and reviving it inside the deletion race window). */
|
|
1101
|
+
private readonly deletingSessions;
|
|
1102
|
+
/** Per-Agent config generation (key = agentKey), bumped by invalidateAgentRuntimes when a Project's credentials change. */
|
|
1103
|
+
private readonly agentGenerations;
|
|
1104
|
+
/** Open streaming fragments of running sessions (fed by drive, served to GET /messages; see live-tail.ts). */
|
|
1105
|
+
private readonly liveTail;
|
|
1106
|
+
private readonly sweepTimer;
|
|
1107
|
+
/** Clock for persisted timestamps (see SessionManagerDeps.now); wall clock unless injected. */
|
|
1108
|
+
private readonly now;
|
|
1109
|
+
constructor(deps: SessionManagerDeps);
|
|
1110
|
+
statusOf(sessionId: string): SessionStatus;
|
|
1111
|
+
pendingApprovalCount(sessionId: string): number;
|
|
1112
|
+
pendingApprovals(sessionId: string): PendingApproval[];
|
|
1113
|
+
/** Number of queued follow-up tasks (`queueIfBusy`) awaiting auto-start. */
|
|
1114
|
+
pendingFollowUpCount(sessionId: string): number;
|
|
1115
|
+
/**
|
|
1116
|
+
* Background-task counts of a LOADED session, read live (see SessionInfo.backgroundTasks);
|
|
1117
|
+
* undefined when the session is not in the active table or nothing is running — the list
|
|
1118
|
+
* row and the single GET omit the field in both cases.
|
|
1119
|
+
*/
|
|
1120
|
+
backgroundTasksOf(sessionId: string): SessionBackgroundTasks | undefined;
|
|
1121
|
+
/** Steering messages queued but not yet delivered to the model (display mirror; see RuntimeEntry.pendingSteering). */
|
|
1122
|
+
pendingSteeringOf(sessionId: string): PendingSteeringInfo[];
|
|
1123
|
+
/** Steering a finished run never delivered, waiting for the composer to take it back (see RuntimeEntry.returnedSteering). */
|
|
1124
|
+
returnedSteeringOf(sessionId: string): PendingSteeringInfo[];
|
|
1125
|
+
/**
|
|
1126
|
+
* Live subagent children of an ACTIVE runtime entry (empty when the session is not loaded
|
|
1127
|
+
* — after a restart there is no in-process child left to report, so empty is the truth).
|
|
1128
|
+
* Rides `task_state` events and the SSE subscribe snapshot; the panel renders child
|
|
1129
|
+
* running marks from this instead of parsing tool-output text.
|
|
1130
|
+
*/
|
|
1131
|
+
subagentsOf(sessionId: string): BackgroundSubagentInfo[];
|
|
1132
|
+
/**
|
|
1133
|
+
* Host message to one child session — a user input on the child, whatever its state:
|
|
1134
|
+
* steering while it runs, a follow-up run while it is idle, a revival (resume-session
|
|
1135
|
+
* semantics) when it is no longer live (core `Session.sendToBackgroundSubagent`). The
|
|
1136
|
+
* parent runtime itself is loaded on demand (ensureEntry — the same get-or-resume path a
|
|
1137
|
+
* task uses), so messaging a child of a long-idle conversation works after a restart too;
|
|
1138
|
+
* the resume fallback names the child's owning Agent from its own session row, which must
|
|
1139
|
+
* belong to the parent's project.
|
|
1140
|
+
*/
|
|
1141
|
+
sendToSubagent(sessionId: string, childSessionId: string, messages: OmniMessage[]): Promise<SubagentMessageOutcome>;
|
|
1142
|
+
/**
|
|
1143
|
+
* Sets a Session's thinking level on its loaded runtime (core `Session.thinkingLevel`,
|
|
1144
|
+
* plain state): the row's value is what the loader applies at load, so this only needs to
|
|
1145
|
+
* reach a runtime that is already in the active table — or a live child session, which
|
|
1146
|
+
* has an index row and a panel picker of its own but runs inside its parent's runtime.
|
|
1147
|
+
* The level is soft-limited: it applies from the Session's very next LLM request
|
|
1148
|
+
* (mid-context — the UI advises compacting first, since the change invalidates the
|
|
1149
|
+
* provider's cached context). No-op when nothing is loaded.
|
|
1150
|
+
*/
|
|
1151
|
+
setThinkingLevel(sessionId: string, level: ThinkingLevelName): void;
|
|
1152
|
+
/** Host abort of one child session's current run (core `Session.abortBackgroundSubagentRun`); false when the parent runtime is not loaded, the child is unknown, or it is idle. */
|
|
1153
|
+
abortSubagentRun(sessionId: string, childSessionId: string): boolean;
|
|
1154
|
+
/** Queued follow-up tasks awaiting auto-start, as their display/recall info (id + content summary). */
|
|
1155
|
+
pendingFollowUpsOf(sessionId: string): PendingFollowUpInfo[];
|
|
1156
|
+
/**
|
|
1157
|
+
* Live tail of a running session: one synthetic `partial_* start` OmniMessage per open
|
|
1158
|
+
* streaming fragment, carrying the full accumulated content so far (see live-tail.ts).
|
|
1159
|
+
* Empty when idle or when nothing is streaming. GET /messages attaches this (with a
|
|
1160
|
+
* channel cursor) so a client joining mid-stream can render the in-progress message.
|
|
1161
|
+
*/
|
|
1162
|
+
liveFragments(sessionId: string): OmniMessage[];
|
|
1163
|
+
/**
|
|
1164
|
+
* The running Task's input messages as published at launch; empty when idle. The engine
|
|
1165
|
+
* writes these exact envelopes to the Trace only after the first run's bootstrap (MCP
|
|
1166
|
+
* connect + discovery), so GET /messages appends whichever of them the Trace read has
|
|
1167
|
+
* not caught up to yet — without this, a client rebuilding history during the connect
|
|
1168
|
+
* (the draft flow subscribes only after the input publish) loses the user's own message.
|
|
1169
|
+
*/
|
|
1170
|
+
pendingInputs(sessionId: string): OmniMessage[];
|
|
1171
|
+
/** The running Task's streamed bootstrap records (see RuntimeEntry.pendingBootstrap); empty when idle. */
|
|
1172
|
+
pendingBootstrap(sessionId: string): OmniMessage[];
|
|
1173
|
+
/** Number of Sessions for this Agent that are currently running / compacting. */
|
|
1174
|
+
activeCountForAgent(projectId: string, agentId: string): number;
|
|
1175
|
+
/** Add a newly created Session to the active table (status idle), avoiding a redundant load on the next Task. */
|
|
1176
|
+
adopt(row: SessionRow, session: RuntimeSession): void;
|
|
1177
|
+
/**
|
|
1178
|
+
* Subscribes the background hooks on a runtime Session entering the active table — every
|
|
1179
|
+
* insertion path must call it (ensureEntry's loads, adopt's fresh creations):
|
|
1180
|
+
* - the idle-arrival signal for completion notices (mid-run arrivals are delivered inside
|
|
1181
|
+
* the run by core; this signal is the only trigger left when the session sits idle);
|
|
1182
|
+
* - live-forwarded background-subagent messages, published to the session channel (the
|
|
1183
|
+
* same feed SSE relays) and recorded for usage — a background child streams to the
|
|
1184
|
+
* frontend in real time past the launching turn's end, until its terminal state;
|
|
1185
|
+
* - subagent run-state changes, republished as `task_state` so the panel's running
|
|
1186
|
+
* marks track child rounds structurally instead of parsing tool-output text;
|
|
1187
|
+
* - background-task state changes (a command promoted or exited, a subagent promoted,
|
|
1188
|
+
* settling or released), re-counted and published as `session_background` on the user
|
|
1189
|
+
* channel so every Session list shows which rows still own background work.
|
|
1190
|
+
*/
|
|
1191
|
+
private registerNoticeListener;
|
|
1192
|
+
/**
|
|
1193
|
+
* The entry-lifetime approval callback: the registry, the per-decision approval-mode
|
|
1194
|
+
* re-read, and the SSE escalation are all entry-scoped, so one instance serves every run
|
|
1195
|
+
* of the entry and the children's session-lifetime fallback sink alike.
|
|
1196
|
+
*/
|
|
1197
|
+
private entryApprove;
|
|
1198
|
+
/** Publishes one live background-subagent message and records its usage (fire-and-forget; the child's own Trace is the durable record). */
|
|
1199
|
+
private forwardBackgroundMessage;
|
|
1200
|
+
/**
|
|
1201
|
+
* Bumps one Agent's config generation so every runtime built before it is discarded on
|
|
1202
|
+
* its next idle access and re-resumed via the loader (history is preserved through the
|
|
1203
|
+
* Trace). What warrants it is a Session-fixed fact changing under the runtime — the
|
|
1204
|
+
* Project's credentials (see invalidateProjectRuntimes). Agent State changes never do:
|
|
1205
|
+
* core reads them into the next model context itself. A Task already in flight is neither
|
|
1206
|
+
* aborted nor hot-swapped: it keeps the values it started with, and its entry is rebuilt
|
|
1207
|
+
* on the first access after it returns to idle (see ensureEntry).
|
|
1208
|
+
*/
|
|
1209
|
+
invalidateAgentRuntimes(projectId: string, agentId: string): void;
|
|
1210
|
+
/**
|
|
1211
|
+
* After a Project's models/credentials change: invalidate every cached runtime in this
|
|
1212
|
+
* Project, so the next Task re-resumes with the new api_key / base_url. Same
|
|
1213
|
+
* effective-value semantics as invalidateAgentRuntimes — no hot swap into a Task already
|
|
1214
|
+
* in flight. Iterating the active table is complete here, not a shortcut: the generation
|
|
1215
|
+
* map only matters for runtimes that are already cached, and an Agent with no cached
|
|
1216
|
+
* entry builds fresh through the loader anyway.
|
|
1217
|
+
*/
|
|
1218
|
+
invalidateProjectRuntimes(projectId: string): void;
|
|
1219
|
+
/**
|
|
1220
|
+
* Cheap, lock-free rehearsal of the 409/503 conditions startTask checks, throwing exactly the
|
|
1221
|
+
* same HttpErrors. **Advisory only**: it neither takes the Session lock nor loads an entry, so
|
|
1222
|
+
* a session that isn't in the active table reads as acceptable and a status change racing this
|
|
1223
|
+
* call is not caught — the authoritative check is still the one inside startTask.
|
|
1224
|
+
*
|
|
1225
|
+
* It exists so a caller that has irreversible work to do first (POST /tasks writes the
|
|
1226
|
+
* message's file attachments to disk) can find out about the ordinary "a Task is already
|
|
1227
|
+
* running" rejection before doing it, instead of undoing it afterwards.
|
|
1228
|
+
*/
|
|
1229
|
+
assertCanAcceptTask(sessionId: string, opts?: {
|
|
1230
|
+
queueIfBusy?: boolean;
|
|
1231
|
+
}): void;
|
|
1232
|
+
/**
|
|
1233
|
+
* Run a metadata operation at an idle Session boundary under the same mutex as Task and
|
|
1234
|
+
* compaction starts. Session fork uses this to snapshot an append-only Trace without a Task
|
|
1235
|
+
* beginning between its status check and final read. An uncached Session is idle by definition;
|
|
1236
|
+
* unlike startTask this path deliberately does not load a heavyweight runtime.
|
|
1237
|
+
*/
|
|
1238
|
+
atIdleBoundary<T>(sessionId: string, operation: () => Promise<T>): Promise<T>;
|
|
1239
|
+
/**
|
|
1240
|
+
* Start a Task: get-or-load → 409
|
|
1241
|
+
* mutual-exclusion check → publish the input messages first → drive run in the
|
|
1242
|
+
* background. Returns the current actual session_id (the new id after self-heal).
|
|
1243
|
+
* With `queueIfBusy`, a busy session (running/compacting) enqueues the input as a
|
|
1244
|
+
* follow-up instead of 409: it auto-starts as an ordinary next task once the session
|
|
1245
|
+
* returns to idle (`queued: true` in the result; see startQueuedFollowUp).
|
|
1246
|
+
* `opts.recall` is that queued message's original
|
|
1247
|
+
* content — optional, because only the HTTP route knows the parts of it the input does
|
|
1248
|
+
* not carry (the pre-attachment text and where each file landed on disk); every other
|
|
1249
|
+
* caller's store is read off the input, so a queued follow-up is recallable and shows
|
|
1250
|
+
* its content whichever path queued it.
|
|
1251
|
+
*/
|
|
1252
|
+
startTask(sessionId: string, input: OmniMessage[], opts?: {
|
|
1253
|
+
queueIfBusy?: boolean;
|
|
1254
|
+
recall?: RecallStore;
|
|
1255
|
+
}): Promise<{
|
|
1256
|
+
sessionId: string;
|
|
1257
|
+
queued: boolean;
|
|
1258
|
+
}>;
|
|
1259
|
+
/**
|
|
1260
|
+
* Start a goal run: like startTask, but the goal plugin composes the input and its stop
|
|
1261
|
+
* hook drives every later round inside this one `session.run` call — so the Session
|
|
1262
|
+
* stays `running` for the whole goal (every round), the existing abort endpoint
|
|
1263
|
+
* interrupts the entire loop and schedules queue behind it as usual. The plugin's
|
|
1264
|
+
* `user_prompt` hook (hooks run in core and nowhere else — `Session.runUserPromptHook`)
|
|
1265
|
+
* writes the Session's goal file and answers the round-1 protocol message; it runs under
|
|
1266
|
+
* the session lock AFTER the idle check, since it overwrites the goal file a running goal
|
|
1267
|
+
* is reading. Not installed (null) is the route's 409; an empty answer a 500. Round inputs
|
|
1268
|
+
* are yielded by core and published like any streamed message; progress additionally goes
|
|
1269
|
+
* out as goal_* server events (the run state itself is the goal file the plugin maintains).
|
|
1270
|
+
*/
|
|
1271
|
+
startGoal(sessionId: string, args: {
|
|
1272
|
+
/** The user's own message(s) exactly as submitted — text and images, no wrapping. */
|
|
1273
|
+
messages: OmniMessage[];
|
|
1274
|
+
/** The user's own objective text (leading marker blocks stripped): what the plugin records, the goal_started event and the title material. */
|
|
1275
|
+
objective: string;
|
|
1276
|
+
budget: number;
|
|
1277
|
+
}): Promise<{
|
|
1278
|
+
sessionId: string;
|
|
1279
|
+
}>;
|
|
1280
|
+
/**
|
|
1281
|
+
* Taps a goal run's stream for `drive`: round boundaries become goal_round events —
|
|
1282
|
+
* round 1 from the seeded input (core never yields a run's initial input, so it is
|
|
1283
|
+
* counted here, where startGoal published it), later rounds from the stop-hook continues
|
|
1284
|
+
* core yields — and the goal hook's `stop` event becomes the
|
|
1285
|
+
* goal_finished server event. `used` is what the hook last recorded in its event's
|
|
1286
|
+
* `output` — the same number its budget check used — so the UI never shows a different
|
|
1287
|
+
* figure. A stream that ends without the hook's terminal event (a cut-off run, an
|
|
1288
|
+
* infrastructure failure) closes as `aborted`.
|
|
1289
|
+
*/
|
|
1290
|
+
private goalStream;
|
|
1291
|
+
/** Closes a goal as aborted for the UI (stream cut off / infrastructure failure). */
|
|
1292
|
+
private finishAborted;
|
|
1293
|
+
/** Shared task launch (fresh tasks and auto-started follow-ups): flips to running, publishes the input, and drives the run. Caller holds the session lock and has verified idle. */
|
|
1294
|
+
private launchTask;
|
|
1295
|
+
/**
|
|
1296
|
+
* Auto-start of queued follow-ups: called from drive's finally whenever a Task or
|
|
1297
|
+
* compaction finishes (abort included — follow-ups are future tasks the user queued, so
|
|
1298
|
+
* an abort of the current run does not discard them). Re-validates everything under the
|
|
1299
|
+
* session lock (another task may have snuck in, the server may be shutting down, the
|
|
1300
|
+
* session may be getting deleted) and launches exactly one queued input — the next
|
|
1301
|
+
* finish picks up the one after, keeping order and one-at-a-time semantics.
|
|
1302
|
+
*/
|
|
1303
|
+
private startQueuedFollowUp;
|
|
1304
|
+
/** Manually compact the context: 409 if already running; compaction output also flows into the SSE channel. */
|
|
1305
|
+
startCompact(sessionId: string): Promise<{
|
|
1306
|
+
sessionId: string;
|
|
1307
|
+
}>;
|
|
1308
|
+
/** Submit an approval decision; returns false if the pending approval doesn't exist (already decided/unknown). */
|
|
1309
|
+
decideApproval(sessionId: string, toolCallId: string, decision: "allow" | "deny"): boolean;
|
|
1310
|
+
/**
|
|
1311
|
+
* Mid-run steering: forward the message to the running Session (core delivers it between
|
|
1312
|
+
* turns as a standalone `[user_steering]` user message followed by its images — no SSE
|
|
1313
|
+
* event of its own; the messages arrive through the stream the drive loop already
|
|
1314
|
+
* publishes). 409 when the Session isn't running a Task (idle / compacting / not loaded)
|
|
1315
|
+
* or the run finished in the race window — the caller falls back to submitting a normal
|
|
1316
|
+
* task, which carries the same text and images.
|
|
1317
|
+
*
|
|
1318
|
+
* `recall` is the queued message's original content: its summary (with a fresh id) is
|
|
1319
|
+
* mirrored on the entry and broadcast via `task_state` (and the SSE subscribe snapshot)
|
|
1320
|
+
* until the delivered `[user_steering]` message is observed on the stream — that is what
|
|
1321
|
+
* keeps the composer's "steering queued" hint, content included, alive across reloads —
|
|
1322
|
+
* and the content itself is what a recall (recallSteering) hands back to the composer.
|
|
1323
|
+
*/
|
|
1324
|
+
steer(sessionId: string, input: OmniMessage[], recall: RecallStore): void;
|
|
1325
|
+
/**
|
|
1326
|
+
* Recall an undelivered steering message (#287): withdraw it from core's queue and the
|
|
1327
|
+
* display mirror, re-broadcast state, and hand back its original content for the composer
|
|
1328
|
+
* to restore. 409 `not_pending` when the entry is no longer queued — already delivered to
|
|
1329
|
+
* the model (core's `unsteer` says so authoritatively; the mirror can lag behind delivery
|
|
1330
|
+
* until the `[user_steering]` message is observed on the stream), the run exited, or the
|
|
1331
|
+
* id is unknown — all the same outcome for the caller: nothing left to take back.
|
|
1332
|
+
*/
|
|
1333
|
+
recallSteering(sessionId: string, steerId: string): RecallStore;
|
|
1334
|
+
/**
|
|
1335
|
+
* Recall a queued follow-up task (#287): remove it from the queue before it auto-starts,
|
|
1336
|
+
* re-broadcast state, and hand back its original content (plus the thinking level it was
|
|
1337
|
+
* queued with). Being in the queue is the whole condition — every queued entry carries a
|
|
1338
|
+
* recall store (see QueuedFollowUp.recall), so no queued message is ever refused. 409
|
|
1339
|
+
* `follow_up_started` when the id is not in the queue: it already auto-started, or the
|
|
1340
|
+
* runtime was released and took the queue with it. Its own code, distinct from steering's
|
|
1341
|
+
* `not_pending`, because the two mean different things to the user — a follow-up became a
|
|
1342
|
+
* task of its own, a steering message reached the model mid-run.
|
|
1343
|
+
*/
|
|
1344
|
+
recallFollowUp(sessionId: string, followUpId: string): {
|
|
1345
|
+
recall: RecallStore;
|
|
1346
|
+
};
|
|
1347
|
+
/**
|
|
1348
|
+
* "Retry now" on the reconnect countdown: skip the in-progress backoff wait and fire
|
|
1349
|
+
* the next retry immediately (the attempt counter is unchanged — the skipped wait does
|
|
1350
|
+
* not consume an extra attempt). Returns false as a benign no-op when the session has
|
|
1351
|
+
* no active runtime or no reconnect wait is in progress — timing races (the wait
|
|
1352
|
+
* elapsed right before the click) must not surface as errors. Mirrors the steer seam:
|
|
1353
|
+
* manager → RuntimeSession → core Session → engine.
|
|
1354
|
+
*/
|
|
1355
|
+
retryNow(sessionId: string): boolean;
|
|
1356
|
+
/**
|
|
1357
|
+
* Interrupt the current Task/compaction: pending approvals converge to deny first,
|
|
1358
|
+
* then the AbortSignal fires. Returns false if nothing is in progress (the route
|
|
1359
|
+
* treats this as a 204 no-op).
|
|
1360
|
+
*/
|
|
1361
|
+
abortTask(sessionId: string): boolean;
|
|
1362
|
+
/**
|
|
1363
|
+
* Background command processes of a LOADED session (empty when the entry is not in
|
|
1364
|
+
* the active table — a resumed entry starts with a fresh environment and can only
|
|
1365
|
+
* ever report an empty list, so nothing is resurrected just to answer a poll).
|
|
1366
|
+
*/
|
|
1367
|
+
listProcesses(sessionId: string): BackgroundCommandInfo[];
|
|
1368
|
+
/**
|
|
1369
|
+
* Refreshes the listen-port probes behind the process list's `serviceUrl` (core
|
|
1370
|
+
* `Session.probeBackgroundCommandServices`): called by the processes route before
|
|
1371
|
+
* listing, so the first fetch already carries probed URLs. Bounded by core's own probe
|
|
1372
|
+
* timeout and TTL; a session that isn't loaded has no processes to probe.
|
|
1373
|
+
*/
|
|
1374
|
+
probeProcessServices(sessionId: string): Promise<void>;
|
|
1375
|
+
/**
|
|
1376
|
+
* Hands one EXECUTING tool call of a loaded session back as a background task (core
|
|
1377
|
+
* `Session.detachToolCall`): the call closes with a registry handle, its work keeps
|
|
1378
|
+
* running, and the turn carries on. An unloaded session has nothing executing, so it
|
|
1379
|
+
* answers "not_running" like an unknown id.
|
|
1380
|
+
*/
|
|
1381
|
+
detachToolCall(sessionId: string, toolCallId: string): ToolDetachResult;
|
|
1382
|
+
/** Kills one background command process of a loaded session; false when the session isn't loaded or the id is unknown. */
|
|
1383
|
+
killProcess(sessionId: string, processId: string): boolean;
|
|
1384
|
+
/**
|
|
1385
|
+
* Removes one EXITED background command entry from a loaded session's process list.
|
|
1386
|
+
* A running process is refused ("running") — stopping is killProcess's job, and a
|
|
1387
|
+
* bare removal must never surprise-signal a live process group. "not_found" covers
|
|
1388
|
+
* an unloaded session and an unknown id alike: the entry is gone either way. The
|
|
1389
|
+
* removal itself reuses the kill path — core's registry removal signals the (dead)
|
|
1390
|
+
* group defensively and drops the row, exactly like input_command's post-exit reap.
|
|
1391
|
+
* The only race, an entry observed running that exits before a retry, is benign:
|
|
1392
|
+
* the caller gets "running", refreshes, and sees the row exited on the next look.
|
|
1393
|
+
*
|
|
1394
|
+
* Note what leaves with the row: the registry entry owns the ManagedSession holding
|
|
1395
|
+
* that process's captured output, so after a removal input_command on the same
|
|
1396
|
+
* process_id answers "unknown process_id". Removal is a deliberate discard, not just
|
|
1397
|
+
* a list tidy-up — the Web App says so at the button and in the docs.
|
|
1398
|
+
*/
|
|
1399
|
+
removeProcess(sessionId: string, processId: string): "removed" | "running" | "not_found";
|
|
1400
|
+
/**
|
|
1401
|
+
* Disposes a just-removed entry's runtime — after its in-flight drive (if any)
|
|
1402
|
+
* settles, so interrupt cleanup never races a dying environment. Deleting a Session /
|
|
1403
|
+
* Agent / Project is the one intent that must also end the background processes the
|
|
1404
|
+
* conversation started (a dev server surviving its deleted conversation is
|
|
1405
|
+
* unreachable from every UI, running forever).
|
|
1406
|
+
*/
|
|
1407
|
+
private disposeRemoved;
|
|
1408
|
+
/**
|
|
1409
|
+
* Before deleting a Project, converge all its active runs and clear them out of the
|
|
1410
|
+
* active table. Returns the in-flight drive Promises of the affected entries: the
|
|
1411
|
+
* caller (deleteProject) should await them before removing the directory, so that
|
|
1412
|
+
* interrupt-cleanup Trace writes don't recreate the directory after deletion.
|
|
1413
|
+
*/
|
|
1414
|
+
abortProject(projectId: string): Promise<void>[];
|
|
1415
|
+
/**
|
|
1416
|
+
* Before deleting an Agent, converge all its active runs and clear them out of the
|
|
1417
|
+
* active table (same semantics as abortProject). Also marks this Agent as "being
|
|
1418
|
+
* deleted": new Tasks/compactions entering during the deletion process are always
|
|
1419
|
+
* rejected with 409 (assertAgentNotDeleting), closing the race window where a new
|
|
1420
|
+
* task recreates the directory and revives an already-deleted Agent between the
|
|
1421
|
+
* abortAgent snapshot and the directory removal. The caller must call
|
|
1422
|
+
* endAgentDeletion once deletion finishes (success or failure).
|
|
1423
|
+
*/
|
|
1424
|
+
beginAgentDeletion(projectId: string, agentId: string): Promise<void>[];
|
|
1425
|
+
endAgentDeletion(projectId: string, agentId: string): void;
|
|
1426
|
+
/**
|
|
1427
|
+
* Before deleting a single Session, converge its active run and clear it out of the
|
|
1428
|
+
* active table (same semantics as beginAgentDeletion). Also marks this Session as
|
|
1429
|
+
* "being deleted": new Tasks/compactions entering during the deletion process are
|
|
1430
|
+
* always rejected with 409 (assertSessionNotDeleting), closing the race window where
|
|
1431
|
+
* a new task recreates the entry and Trace file, reviving an already-deleted Session
|
|
1432
|
+
* between the abort snapshot and the file removal. The caller must call
|
|
1433
|
+
* endSessionDeletion once deletion finishes (success or failure). Returns the
|
|
1434
|
+
* in-flight drive Promise: the caller should await it before deleting the Trace file,
|
|
1435
|
+
* so cleanup writes don't recreate the file.
|
|
1436
|
+
*/
|
|
1437
|
+
beginSessionDeletion(sessionId: string): Promise<void>[];
|
|
1438
|
+
endSessionDeletion(sessionId: string): void;
|
|
1439
|
+
/** Graceful shutdown: reject new tasks (503), interrupt all active runs, and wait for them to finish (default ≤5s). */
|
|
1440
|
+
shutdown(timeoutMs?: number): Promise<void>;
|
|
1441
|
+
/**
|
|
1442
|
+
* Active-table idle eviction: removes entries that are idle (idle status, no pending
|
|
1443
|
+
* approvals, no in-flight drive) and have been inactive past the timeout, releasing
|
|
1444
|
+
* the core Session's full in-memory history. This is purely memory reclamation: the
|
|
1445
|
+
* next access re-resumes via the loader, so correctness is unaffected. Lock-table
|
|
1446
|
+
* entries are auto-cleaned by withLock once their chain drains (including leftover
|
|
1447
|
+
* entries under the old id after self-heal). `now` / `idleMs` are injectable for
|
|
1448
|
+
* tests and timers.
|
|
1449
|
+
*/
|
|
1450
|
+
sweepIdle(now?: number, idleMs?: number): void;
|
|
1451
|
+
private assertOpen;
|
|
1452
|
+
/** The Agent owning this Session is being deleted → 409 (guards against directory recreation inside the deletion race window). */
|
|
1453
|
+
private assertAgentNotDeleting;
|
|
1454
|
+
/** This Session is being deleted → 409 (guards against the entry/Trace being rebuilt and reviving it inside the deletion race window). */
|
|
1455
|
+
private assertSessionNotDeleting;
|
|
1456
|
+
private assertIdle;
|
|
1457
|
+
private generationOf;
|
|
1458
|
+
/** get-or-resume-or-heal: use directly on an active-table hit; otherwise load via the loader, updating the index's primary key on self-heal. */
|
|
1459
|
+
private ensureEntry;
|
|
1460
|
+
/**
|
|
1461
|
+
* Auto-start of queued background completion notices: called when core signals an
|
|
1462
|
+
* idle-session arrival, and from drive's finally for notices that raced a run's exit.
|
|
1463
|
+
* Revalidates under the session lock; only an idle entry launches, and only when the
|
|
1464
|
+
* queue is non-empty — a busy entry skips, because the running/queued task's engine
|
|
1465
|
+
* drains the same queue at its own boundaries (each notice is delivered exactly once).
|
|
1466
|
+
*/
|
|
1467
|
+
private startBackgroundNoticeTask;
|
|
1468
|
+
/**
|
|
1469
|
+
* Drive the output stream in the background: publish each message + persist usage +
|
|
1470
|
+
* persist LLM/tool errors; on completion (including errors) resets to idle and pushes
|
|
1471
|
+
* the status. `titleSource` is passed only for Task runs (compaction doesn't generate
|
|
1472
|
+
* a title): its user text drives automatic title generation at run start.
|
|
1473
|
+
*/
|
|
1474
|
+
private drive;
|
|
1475
|
+
/**
|
|
1476
|
+
* Register a subagent: persisted only when the origin message is session_meta
|
|
1477
|
+
* (agentId is derived from the agent_state path: `<…>/<agentId>/agent_state`).
|
|
1478
|
+
* **The title is left blank here** — the registration site in drive generates it
|
|
1479
|
+
* right away from the spawning prompt (fallback first words + LLM replacement), so
|
|
1480
|
+
* it never waits for the subagent's own output. Idempotent (children dedup +
|
|
1481
|
+
* insertOrIgnore); a subagent has its own Trace, so it's visible in both the list
|
|
1482
|
+
* and the trace view. On successful registration, the entry is put into `children`
|
|
1483
|
+
* and returned; a duplicate session_meta returns null.
|
|
1484
|
+
*/
|
|
1485
|
+
private registerChildSession;
|
|
1486
|
+
/**
|
|
1487
|
+
* Runs one `sessions` row write for a driven run's bookkeeping (has_trace /
|
|
1488
|
+
* last_active_at), swallowing failures the same way the per-message `recorder.record`
|
|
1489
|
+
* call does: bookkeeping must never take down a run's lifecycle. The two call sites both
|
|
1490
|
+
* sit outside a covering try — the run-start write precedes drive's try block, the
|
|
1491
|
+
* run-end one lives in its finally — and the realistic failure (the DatabaseSync handle
|
|
1492
|
+
* closed by shutdown while a run outlived its drain window) throws ERR_INVALID_STATE.
|
|
1493
|
+
*/
|
|
1494
|
+
private touchRow;
|
|
1495
|
+
private publishState;
|
|
1496
|
+
private publishEvent;
|
|
1497
|
+
/**
|
|
1498
|
+
* Re-counts a loaded session's background tasks and, when the counts moved, publishes
|
|
1499
|
+
* `session_background` on the user channel (the list's mark is a user-channel affair like
|
|
1500
|
+
* `session_state`; the open conversation reads the same row). A change is Session
|
|
1501
|
+
* activity for the idle sweep: the entry stays loaded for the usual idle window after its
|
|
1502
|
+
* last background task ends, so the finished process list can still be read. An entry
|
|
1503
|
+
* that already left the table (a delete disposing its runtime) publishes nothing — its
|
|
1504
|
+
* row is gone as well.
|
|
1505
|
+
*/
|
|
1506
|
+
private publishBackgroundTasks;
|
|
1507
|
+
/** Serialize (mutually exclude) execution by sessionId; cleans up the lock-table entry once its chain drains (avoids unbounded growth). */
|
|
1508
|
+
private withLock;
|
|
1509
|
+
}
|
|
1510
|
+
declare const Sessions_base: (abstract new () => Pick<SessionManager, "steer" | "pendingApprovalCount" | "pendingFollowUpCount" | "statusOf" | "pendingApprovals" | "pendingSteeringOf" | "subagentsOf" | "sendToSubagent" | "abortSubagentRun" | "pendingFollowUpsOf" | "liveFragments" | "pendingInputs" | "pendingBootstrap" | "activeCountForAgent" | "invalidateAgentRuntimes" | "invalidateProjectRuntimes" | "assertCanAcceptTask" | "startTask" | "startGoal" | "startCompact" | "decideApproval" | "recallSteering" | "recallFollowUp" | "retryNow" | "abortTask" | "listProcesses" | "probeProcessServices" | "killProcess" | "removeProcess" | "abortProject" | "beginAgentDeletion" | "endAgentDeletion" | "beginSessionDeletion" | "endSessionDeletion" | "atIdleBoundary" | "shutdown" | "sweepIdle">) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
1511
|
+
/** The session runtime: one entry per live Session, task mutex, approvals, streaming. */
|
|
1512
|
+
declare abstract class Sessions extends Sessions_base {
|
|
1513
|
+
}
|
|
1514
|
+
declare const SessionServiceIface_base: (abstract new () => Pick<SessionService, "hasTrace" | "toInfo" | "listSessions" | "sessionStats" | "createSession" | "latestTracePath" | "adoptUnmanagedTraceSessions">) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
1515
|
+
declare abstract class SessionServiceIface extends SessionServiceIface_base {
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1518
|
+
interface AgentConfigView {
|
|
1519
|
+
agentsMd: string;
|
|
1520
|
+
systemConfigYaml: string;
|
|
1521
|
+
config: AgentConfigDto;
|
|
1522
|
+
stateDir: string;
|
|
1523
|
+
}
|
|
1524
|
+
declare class AgentConfigService implements AgentConfig {
|
|
1525
|
+
private readonly paths;
|
|
1526
|
+
private get root();
|
|
1527
|
+
/** Whether the Agent exists (determined by the presence of system_config.yaml, matching the CLI's convention). */
|
|
1528
|
+
exists(projectId: string, agentId: string): Promise<boolean>;
|
|
1529
|
+
requireExists(projectId: string, agentId: string): Promise<void>;
|
|
1530
|
+
/**
|
|
1531
|
+
* Read list-card metadata: name / description + tool count (sum of tools.builtin
|
|
1532
|
+
* and tools.mcpServers entries; MCP counted per server). Silently falls back to
|
|
1533
|
+
* empty / 0 if the file is corrupt.
|
|
1534
|
+
*/
|
|
1535
|
+
readCardMeta(projectId: string, agentId: string): Promise<{
|
|
1536
|
+
name?: string;
|
|
1537
|
+
description?: string;
|
|
1538
|
+
toolCount: number;
|
|
1539
|
+
version: number;
|
|
1540
|
+
kernelOutdated: boolean;
|
|
1541
|
+
}>;
|
|
1542
|
+
/** Structured config view (matching the edit form's shape) + raw text + AGENTS.md + State path. */
|
|
1543
|
+
getConfig(projectId: string, agentId: string): Promise<AgentConfigView>;
|
|
1544
|
+
/**
|
|
1545
|
+
* PUT accepts any subset: only the provided keys are updated (parseDocument
|
|
1546
|
+
* preserves comments and untouched content); agentsMd is overwritten in full.
|
|
1547
|
+
* Numeric validation: >0 or -1; thinkingLevel / mode are validated as enums.
|
|
1548
|
+
*/
|
|
1549
|
+
updateConfig(projectId: string, agentId: string, req: AgentConfigUpdateRequest): Promise<void>;
|
|
1550
|
+
/**
|
|
1551
|
+
* Overwrites system_config.yaml with the current code defaults (core's
|
|
1552
|
+
* resetSystemConfigToDefaults) — same semantics as updating an installed skill:
|
|
1553
|
+
* only the identity fields (name / description / version) survive; the system
|
|
1554
|
+
* prompt, max_turns, model/compaction settings and the tool list (incl. MCP
|
|
1555
|
+
* servers) become the current defaults. AGENTS.md, skills and the vault are
|
|
1556
|
+
* untouched.
|
|
1557
|
+
*/
|
|
1558
|
+
resetConfig(projectId: string, agentId: string): Promise<void>;
|
|
1559
|
+
/**
|
|
1560
|
+
* Smart-merges the config up to the current defaults generation (core's applyKernelUpdate):
|
|
1561
|
+
* a settings tab still carrying a recorded generation's default follows the new defaults,
|
|
1562
|
+
* a tab the user changed is kept whole and reported; the config is stamped with the new
|
|
1563
|
+
* kernel version.
|
|
1564
|
+
* The destructive full-refresh alternative stays resetConfig.
|
|
1565
|
+
*/
|
|
1566
|
+
kernelUpdate(projectId: string, agentId: string): Promise<AgentKernelUpdateResponse>;
|
|
1567
|
+
private applyConfigUpdate;
|
|
1568
|
+
/**
|
|
1569
|
+
* Inserts a feature's section placeholder into the Agent's prompt template — the explicit
|
|
1570
|
+
* adoption path mirroring the Memory tab's endpoint; nothing ever inserts automatically.
|
|
1571
|
+
* For skills/vault the insert is migration-first (core's helpers): a stored template still
|
|
1572
|
+
* carrying the legacy hardcoded section verbatim gets it replaced in place by the
|
|
1573
|
+
* placeholder, otherwise the placeholder is inserted before `# Environment`. Idempotent;
|
|
1574
|
+
* returns the refreshed config view (the route picks out the feature's DTO).
|
|
1575
|
+
*/
|
|
1576
|
+
insertTemplatePlaceholder(projectId: string, agentId: string, feature: "vault" | "skills" | "schedules"): Promise<AgentConfigView>;
|
|
1577
|
+
/** Read the Agent vault (agent_state/.vault.toml): values are always masked, plaintext is never sent to the client. */
|
|
1578
|
+
getVault(projectId: string, agentId: string): Promise<VaultResponse>;
|
|
1579
|
+
/**
|
|
1580
|
+
* PUT replaces the whole vault table (same semantics as models): keys absent from
|
|
1581
|
+
* the body are deleted; omitting value keeps the existing value (a new key must
|
|
1582
|
+
* provide a value). Key names are validated against shell environment variable
|
|
1583
|
+
* naming rules (same rule as core); deleting everything removes the whole
|
|
1584
|
+
* .vault.toml file.
|
|
1585
|
+
*/
|
|
1586
|
+
updateVault(projectId: string, agentId: string, req: VaultUpdateRequest): Promise<VaultResponse>;
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
declare const AgentConfig_base: (abstract new () => {
|
|
1590
|
+
exists(projectId: string, agentId: string): Promise<boolean>;
|
|
1591
|
+
requireExists(projectId: string, agentId: string): Promise<void>;
|
|
1592
|
+
readCardMeta(projectId: string, agentId: string): Promise<{
|
|
1593
|
+
name?: string;
|
|
1594
|
+
description?: string;
|
|
1595
|
+
toolCount: number;
|
|
1596
|
+
version: number;
|
|
1597
|
+
kernelOutdated: boolean;
|
|
1598
|
+
}>;
|
|
1599
|
+
getConfig(projectId: string, agentId: string): Promise<AgentConfigView>;
|
|
1600
|
+
updateConfig(projectId: string, agentId: string, req: AgentConfigUpdateRequest): Promise<void>;
|
|
1601
|
+
resetConfig(projectId: string, agentId: string): Promise<void>;
|
|
1602
|
+
kernelUpdate(projectId: string, agentId: string): Promise<AgentKernelUpdateResponse>;
|
|
1603
|
+
insertTemplatePlaceholder(projectId: string, agentId: string, feature: "vault" | "skills" | "schedules"): Promise<AgentConfigView>;
|
|
1604
|
+
getVault(projectId: string, agentId: string): Promise<VaultResponse>;
|
|
1605
|
+
updateVault(projectId: string, agentId: string, req: VaultUpdateRequest): Promise<VaultResponse>;
|
|
1606
|
+
}) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
1607
|
+
/** AgentConfig: the mechanism AgentConfigService implements. */
|
|
1608
|
+
declare abstract class AgentConfig extends AgentConfig_base {
|
|
1609
|
+
}
|
|
1610
|
+
declare const AgentLifecycle_base: (abstract new () => {
|
|
1611
|
+
listAgents(projectId: string): Promise<AgentListItem[]>;
|
|
1612
|
+
deleteAgent(projectId: string, agentId: string): Promise<void>;
|
|
1613
|
+
createAgent(projectId: string, agentId: string, name?: string, description?: string, skillNames?: readonly string[], directory?: {
|
|
1614
|
+
path: string;
|
|
1615
|
+
names: readonly string[];
|
|
1616
|
+
}, archive?: Buffer<ArrayBufferLike>): Promise<AgentListItem>;
|
|
1617
|
+
pluginVersion(projectId: string, agentId: string, pluginName: string): Promise<{
|
|
1618
|
+
installed: string | null;
|
|
1619
|
+
library: string | null;
|
|
1620
|
+
}>;
|
|
1621
|
+
updatePlugin(projectId: string, agentId: string, pluginName: string): Promise<void>;
|
|
1622
|
+
}) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
1623
|
+
/** AgentLifecycle: the mechanism AgentService implements. */
|
|
1624
|
+
declare abstract class AgentLifecycle extends AgentLifecycle_base {
|
|
1625
|
+
}
|
|
1626
|
+
|
|
1627
|
+
interface AgentListItem {
|
|
1628
|
+
agentId: string;
|
|
1629
|
+
name?: string;
|
|
1630
|
+
description?: string;
|
|
1631
|
+
createdAt?: string;
|
|
1632
|
+
/** Last config modification time: the later of system_config.yaml / AGENTS.md mtime. */
|
|
1633
|
+
updatedAt?: string;
|
|
1634
|
+
/** Tool count: number of tools.builtin + tools.mcpServers entries (MCP counted per server). */
|
|
1635
|
+
toolCount: number;
|
|
1636
|
+
/** Agent State version number (missing field treated as 1). */
|
|
1637
|
+
version: number;
|
|
1638
|
+
/** Whether the config's kernel stamp is behind the current defaults generation (missing stamp = outdated) — the list card's update hint. */
|
|
1639
|
+
kernelOutdated: boolean;
|
|
1640
|
+
/** Number of vault keys. */
|
|
1641
|
+
vaultKeyCount: number;
|
|
1642
|
+
/** Number of scheduled tasks (count of .toml files under schedule/, including invalid ones). */
|
|
1643
|
+
scheduleCount: number;
|
|
1644
|
+
/** Number of installed Skills (count of skills/<name>/ directories that contain a SKILL.md). */
|
|
1645
|
+
skillCount: number;
|
|
1646
|
+
/** Number of installed hook packages (hooks/<name>/ directories with a hooks.json). */
|
|
1647
|
+
hookCount: number;
|
|
1648
|
+
/** Installed skills and hook packages the library has a newer version of, by plugin, each carrying that library version. */
|
|
1649
|
+
pluginUpdates: PluginUpdateRef[];
|
|
1650
|
+
/** Number of memory topic files across every scope directory under memory/ (independent of the memory switch, like skillCount). */
|
|
1651
|
+
memoryCount: number;
|
|
1652
|
+
}
|
|
1653
|
+
declare class AgentService implements AgentLifecycle {
|
|
1654
|
+
private readonly paths;
|
|
1655
|
+
private get root();
|
|
1656
|
+
private readonly agents;
|
|
1657
|
+
private readonly agentConfig;
|
|
1658
|
+
private readonly snapshots;
|
|
1659
|
+
/** Union of DB index ∪ directory scan; unmanaged directory Agents are backfilled into the DB. */
|
|
1660
|
+
listAgents(projectId: string): Promise<AgentListItem[]>;
|
|
1661
|
+
/** Number of vault keys (falls back to 0 on read failure). */
|
|
1662
|
+
private vaultKeyCount;
|
|
1663
|
+
/** Number of scheduled tasks: count of .toml files under schedule/ (0 if the directory doesn't exist). */
|
|
1664
|
+
private scheduleCount;
|
|
1665
|
+
/**
|
|
1666
|
+
* One pass over `skills/` and one over `hooks/`, answering the card's questions: how many
|
|
1667
|
+
* skills and hook packages are installed, and which of them the plugin library has moved
|
|
1668
|
+
* past — by plugin, since a plugin is what gets reinstalled to catch up.
|
|
1669
|
+
*
|
|
1670
|
+
* Only the HEAD of each SKILL.md is read, and that bound is load-bearing rather than a
|
|
1671
|
+
* micro-optimization: a preinstalled library is ~180 KB of SKILL.md across seventeen files
|
|
1672
|
+
* per Agent, so a Project of ten Agents would move ~2 MB through every `GET /agents` — and
|
|
1673
|
+
* the plugin page reloads that list after each update. The frontmatter block is at the head
|
|
1674
|
+
* by definition (the parser's regex is anchored there) and the largest one in the library is
|
|
1675
|
+
* under 1 KB, so {@link SKILL_HEAD_BYTES} covers it many times over; a block that somehow
|
|
1676
|
+
* overran it would be truncated, fail to parse, and read as an empty version — the same
|
|
1677
|
+
* fallback an unparseable file already takes, and one that sorts before every real version.
|
|
1678
|
+
*
|
|
1679
|
+
* The directory name is the identity (core's listings say so, and install/uninstall address
|
|
1680
|
+
* by it), so that — not a frontmatter `name` — is what the library is looked up by: a skill
|
|
1681
|
+
* by the plugin that ships it, a hook package by the plugin of the same name. A skill the
|
|
1682
|
+
* library does not carry (installed from a zip or a picked directory) has no library version
|
|
1683
|
+
* to be behind and is never an update. An unreadable skill degrades to "not installed" / "no
|
|
1684
|
+
* update": a card hint must not be the thing that makes the Agent list fail. The library is
|
|
1685
|
+
* the caller's, read once per listing rather than once per installed skill.
|
|
1686
|
+
*/
|
|
1687
|
+
private installedPlugins;
|
|
1688
|
+
/**
|
|
1689
|
+
* Where one library plugin stands on one Agent: `installed` is the OLDEST version among the
|
|
1690
|
+
* plugin's installed skills and its installed hook package — the same "any component behind
|
|
1691
|
+
* the library is an update" rule {@link installedPlugins} draws the list cards' hints from —
|
|
1692
|
+
* and null when the Agent carries none of them; `library` is the version the library offers
|
|
1693
|
+
* now, null when the library has no such plugin. Only the head of each SKILL.md is read
|
|
1694
|
+
* (see SKILL_HEAD_BYTES), so the answer costs one small read per skill the plugin ships.
|
|
1695
|
+
*
|
|
1696
|
+
* The organization runtime asks this per employee per pass to keep company plugins current.
|
|
1697
|
+
* A skill the Agent does not carry — never installed, or removed by hand — counts for
|
|
1698
|
+
* nothing: what is compared is the oldest version the Agent actually has, so a plugin whose
|
|
1699
|
+
* skills are all current reads as current even where one of them is missing. Restoring a
|
|
1700
|
+
* deleted skill is the Agents page's business, not a pass's.
|
|
1701
|
+
*/
|
|
1702
|
+
pluginVersion(projectId: string, agentId: string, pluginName: string): Promise<{
|
|
1703
|
+
installed: string | null;
|
|
1704
|
+
library: string | null;
|
|
1705
|
+
}>;
|
|
1706
|
+
/**
|
|
1707
|
+
* Reinstall one library plugin over an Agent's copy: the same whole-plugin update the
|
|
1708
|
+
* Agents page performs, through the same `installPlugin` writer, which overwrites every
|
|
1709
|
+
* skill the plugin ships and its hook package with library content. Throws 404
|
|
1710
|
+
* `unknown_plugin` when the library does not carry the name. Callers that may hold a cached
|
|
1711
|
+
* runtime for the Agent invalidate it afterwards — hook packages are bound when a core
|
|
1712
|
+
* Session is built, unlike skills, which are read from disk on demand.
|
|
1713
|
+
*/
|
|
1714
|
+
updatePlugin(projectId: string, agentId: string, pluginName: string): Promise<void>;
|
|
1715
|
+
/** Number of memory topic files: regular `*.md` files (minus each scope's MEMORY.md index) summed over the scope directories under memory/ (0 if the directory doesn't exist). */
|
|
1716
|
+
private memoryCount;
|
|
1717
|
+
/** Last config modification time: the later of system_config.yaml and AGENTS.md mtime; omitted if neither is readable. */
|
|
1718
|
+
private configUpdatedAt;
|
|
1719
|
+
/**
|
|
1720
|
+
* Delete an Agent: the sole built-in Agent
|
|
1721
|
+
* default_agent (shared with the CLI, the default conversation Agent) cannot be
|
|
1722
|
+
* deleted; callers must first drain any active run via manager.abortAgent.
|
|
1723
|
+
* The directory is deleted recursively (including Trace), and the DB's
|
|
1724
|
+
* agents/sessions index rows are removed along with it; usage records are kept
|
|
1725
|
+
* (historical stats are unaffected).
|
|
1726
|
+
*/
|
|
1727
|
+
deleteAgent(projectId: string, agentId: string): Promise<void>;
|
|
1728
|
+
/**
|
|
1729
|
+
* Create an Agent: the id is chosen by the creator (a semantic id, checked for
|
|
1730
|
+
* duplicates against both the DB and the directory within the Project — a 409
|
|
1731
|
+
* if taken, which naturally also blocks built-in Agent ids) → initialize State →
|
|
1732
|
+
* write name/description (name defaults to the id) → install the seed plugins.
|
|
1733
|
+
*
|
|
1734
|
+
* `pluginNames` are library plugin names picked at creation; they go through the same
|
|
1735
|
+
* `installPlugin` writer the plugin routes use, inside the same cleanup window as the rest
|
|
1736
|
+
* of initialization, so an Agent never survives with only part of its picked set. They are
|
|
1737
|
+
* resolved before anything is created, so an unknown name creates no directory at all.
|
|
1738
|
+
*
|
|
1739
|
+
* `archive` is an exported Agent State snapshot package: the fresh Agent is seeded from
|
|
1740
|
+
* it instead of the default template (explicit name/description override the package's
|
|
1741
|
+
* values, absent ones keep them — no id fallback). Mutually exclusive with seeding: the
|
|
1742
|
+
* package carries its own skills and hooks. An invalid package fails the whole creation
|
|
1743
|
+
* inside the same cleanup window, leaving no empty Agent behind.
|
|
1744
|
+
*/
|
|
1745
|
+
createAgent(projectId: string, agentId: string, name?: string, description?: string, pluginNames?: readonly string[], directory?: {
|
|
1746
|
+
path: string;
|
|
1747
|
+
names: readonly string[];
|
|
1748
|
+
}, archive?: Buffer): Promise<AgentListItem>;
|
|
1749
|
+
}
|
|
1750
|
+
|
|
1751
|
+
declare const MessagingBindings_base: (abstract new () => {
|
|
1752
|
+
find(sessionId: string, channel: string): MessagingBindingRow | null;
|
|
1753
|
+
findEnabled(sessionId: string): MessagingBindingRow | null;
|
|
1754
|
+
listForSession(sessionId: string): MessagingBindingRow[];
|
|
1755
|
+
findEnabledByAccount(channel: string, accountId: string): MessagingBindingRow | null;
|
|
1756
|
+
listAll(): MessagingBindingRow[];
|
|
1757
|
+
upsert(args: {
|
|
1758
|
+
sessionId: string;
|
|
1759
|
+
channel: string;
|
|
1760
|
+
accountId: string;
|
|
1761
|
+
config: Record<string, unknown>;
|
|
1762
|
+
linePerMessage?: boolean;
|
|
1763
|
+
finalReplyOnly?: boolean;
|
|
1764
|
+
renderMarkdown?: boolean;
|
|
1765
|
+
}): MessagingBindingRow;
|
|
1766
|
+
setEnabled(sessionId: string, channel: string, enabled: boolean): void;
|
|
1767
|
+
recordChat(sessionId: string, channel: string, chatId: string, isDirect: boolean): void;
|
|
1768
|
+
recordInboundWatermark(sessionId: string, channel: string, messageId: string | null): void;
|
|
1769
|
+
delete(sessionId: string, channel: string): void;
|
|
1770
|
+
deleteSession(sessionId: string): void;
|
|
1771
|
+
}) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
1772
|
+
/** MessagingBindings: the mechanism MessagingBindingsRepo implements. */
|
|
1773
|
+
declare abstract class MessagingBindings extends MessagingBindings_base {
|
|
1774
|
+
}
|
|
1775
|
+
|
|
1776
|
+
interface MessagingBindingRow {
|
|
1777
|
+
sessionId: string;
|
|
1778
|
+
/** Messaging channel discriminator (`feishu` | `telegram` | `qq` | `wechat`). */
|
|
1779
|
+
channel: string;
|
|
1780
|
+
/** Channel-scoped bot/app identity (feishu: the app_id; telegram: the bot token's numeric id; qq: the App ID; wechat: the scanned bot id); never secret. */
|
|
1781
|
+
accountId: string;
|
|
1782
|
+
/** Channel-specific config document (feishu: appId/appSecret/baseDomain; telegram: botToken; qq: appId/appSecret; wechat: botId/botToken/baseUrl/userId). */
|
|
1783
|
+
config: Record<string, unknown>;
|
|
1784
|
+
/**
|
|
1785
|
+
* INTENT state: whether the binding should hold a live connection (the connection's
|
|
1786
|
+
* runtime status stays in memory). Saving credentials never flips it — only the
|
|
1787
|
+
* explicit state toggle does — and new bindings start disabled. Flipping it on is what
|
|
1788
|
+
* binds the account to this Session: at most one of a Session's rows is enabled, and at
|
|
1789
|
+
* most one row per `(channel, accountId)` is enabled across all Sessions (both
|
|
1790
|
+
* route-enforced).
|
|
1791
|
+
*/
|
|
1792
|
+
enabled: boolean;
|
|
1793
|
+
/**
|
|
1794
|
+
* DELIVERY preference: send each non-blank line of a relayed assistant reply as its own
|
|
1795
|
+
* message instead of one message per reply. Off by default, and off reproduces the
|
|
1796
|
+
* original behaviour exactly. Ordinary saved state — the PUT owns it, like the config.
|
|
1797
|
+
*/
|
|
1798
|
+
linePerMessage: boolean;
|
|
1799
|
+
/**
|
|
1800
|
+
* DELIVERY preference: relay only the LAST completed assistant message of a run, sent when
|
|
1801
|
+
* the run ends, instead of mirroring each of them as it completes. Off by default, and off
|
|
1802
|
+
* reproduces the original behaviour exactly. Independent of `linePerMessage`: with both set
|
|
1803
|
+
* that one final message is what gets split per line. Ordinary saved state, like the field
|
|
1804
|
+
* above.
|
|
1805
|
+
*/
|
|
1806
|
+
finalReplyOnly: boolean;
|
|
1807
|
+
/**
|
|
1808
|
+
* DELIVERY preference: render a relayed assistant reply's Markdown in the channel's own
|
|
1809
|
+
* markup rather than sending its characters as written. ON by default — the model writes
|
|
1810
|
+
* Markdown, and relaying it raw is what this replaced. What survives differs per channel
|
|
1811
|
+
* (Telegram has no headings, lists or tables; QQ has no code or tables; Feishu has all of
|
|
1812
|
+
* them) and a rendering the channel refuses falls back to the plain source, so the setting
|
|
1813
|
+
* can cost formatting and never a message.
|
|
1814
|
+
*/
|
|
1815
|
+
renderMarkdown: boolean;
|
|
1816
|
+
/**
|
|
1817
|
+
* Most recent inbound chat (null until the bot is messaged once). The channel's connector
|
|
1818
|
+
* mints this string and is the only thing that may read it: Telegram encodes the forum
|
|
1819
|
+
* topic into it, so it is not always a bare chat id. Pass it to `MessagingClient.sendText`,
|
|
1820
|
+
* never to a channel API directly.
|
|
1821
|
+
*/
|
|
1822
|
+
lastChatId: string | null;
|
|
1823
|
+
/** Whether that chat is a direct chat; group chats prefer reply-to-message. */
|
|
1824
|
+
lastChatIsDirect: boolean;
|
|
1825
|
+
/**
|
|
1826
|
+
* Channel id of the most recently processed inbound message (null until one arrives).
|
|
1827
|
+
* The bridge's redelivery guard is in memory and dies with the process; this is the one
|
|
1828
|
+
* fact that survives it, so a channel replaying an already-processed event across a
|
|
1829
|
+
* restart is recognized rather than run again (see MessagingBridge's recentInbound).
|
|
1830
|
+
*/
|
|
1831
|
+
lastInboundMessageId: string | null;
|
|
1832
|
+
createdAt: string;
|
|
1833
|
+
updatedAt: string;
|
|
1834
|
+
}
|
|
1835
|
+
|
|
1836
|
+
/**
|
|
1837
|
+
* The messaging-channel connector seam: what the MessagingBridge needs from one chat
|
|
1838
|
+
* platform (Feishu, Telegram, QQ and WeChat today; further channels implement the same interface
|
|
1839
|
+
* and register in app assembly). A connector owns everything channel-specific — credential
|
|
1840
|
+
* shape, wire protocol, event normalization — and hands the bridge a channel-neutral
|
|
1841
|
+
* view: a client for outbound sends and credential checks, and a long-lived event
|
|
1842
|
+
* connection delivering normalized inbound messages (text, images and files, each as far as
|
|
1843
|
+
* the channel actually carries them).
|
|
1844
|
+
*
|
|
1845
|
+
* Config documents are the repo's stored per-channel JSON (`MessagingBindingRow.config`);
|
|
1846
|
+
* every connector method validates its own shape and throws a readable error on a
|
|
1847
|
+
* malformed one — the bridge treats that like any other channel failure.
|
|
1848
|
+
*/
|
|
1849
|
+
/** Known messaging channels (the DB stores the discriminator as text; unknown values are skipped defensively). */
|
|
1850
|
+
type MessagingChannel = "feishu" | "telegram" | "qq" | "wechat";
|
|
1851
|
+
/** One inbound image's bytes, once fetched, with the MIME type the bridge needs for its data URL. */
|
|
1852
|
+
interface MessagingInboundImageData {
|
|
1853
|
+
data: Buffer;
|
|
1854
|
+
/** e.g. `image/png` — the connector's best answer, from the channel's own type or the bytes. */
|
|
1855
|
+
mimeType: string;
|
|
1856
|
+
}
|
|
1857
|
+
/**
|
|
1858
|
+
* An image attached to an inbound message: a handle, not the bytes.
|
|
1859
|
+
*
|
|
1860
|
+
* The bytes are deliberately NOT eagerly downloaded by the connector. Most of what makes
|
|
1861
|
+
* an inbound message uninteresting is decided after the event is normalized — a
|
|
1862
|
+
* redelivery is dropped before anything else happens — and a channel that downloaded
|
|
1863
|
+
* first would pay a full image transfer for every replay. Fetching lazily also puts the
|
|
1864
|
+
* cap where the transfer is: `fetch` refuses anything over `maxBytes` rather than handing
|
|
1865
|
+
* the bridge something it would only discard, which is what keeps a 100MB attachment from
|
|
1866
|
+
* ever being resident in this process.
|
|
1867
|
+
*/
|
|
1868
|
+
interface MessagingInboundImage {
|
|
1869
|
+
/**
|
|
1870
|
+
* Downloads the image. Two distinct failures, and the bridge answers them differently:
|
|
1871
|
+
* `MessagingMediaTooLargeError` (see media.ts) for anything past `maxBytes`, which the
|
|
1872
|
+
* user fixes by sending something smaller, and any other Error — carrying the channel's
|
|
1873
|
+
* OWN reason, since a permission the bot lacks and a network blip are not the same
|
|
1874
|
+
* problem — for a transfer that could not be made at all. That reason reaches the chat,
|
|
1875
|
+
* so a connector must keep credentials out of it (see telegram-api's fetchErrorText).
|
|
1876
|
+
*/
|
|
1877
|
+
fetch(maxBytes: number): Promise<MessagingInboundImageData>;
|
|
1878
|
+
}
|
|
1879
|
+
/**
|
|
1880
|
+
* A non-image file attached to an inbound message: a handle, not the bytes, for exactly the
|
|
1881
|
+
* reasons MessagingInboundImage is one — a redelivery is dropped before anything is
|
|
1882
|
+
* transferred, and the cap rides into `fetch` so an oversized attachment is refused at the
|
|
1883
|
+
* byte that crosses it rather than buffered whole and measured afterwards. A file's ceiling
|
|
1884
|
+
* is the server's per-file attachment limit, which is far larger than an image's, so the
|
|
1885
|
+
* difference between refusing during and after the transfer is correspondingly larger.
|
|
1886
|
+
*
|
|
1887
|
+
* There is no MIME type on this seam. An image needs one because its bytes become a `data:`
|
|
1888
|
+
* URL inside the conversation; a file's bytes go to the Session scratchpad and reach the
|
|
1889
|
+
* model as a PATH, so the name's extension is the whole of what says what it is — and a
|
|
1890
|
+
* channel's declared media type is the sender's claim about a file nothing here parses.
|
|
1891
|
+
*/
|
|
1892
|
+
interface MessagingInboundFile {
|
|
1893
|
+
/**
|
|
1894
|
+
* The sender's own file name, extension included — what the model sees at the end of the
|
|
1895
|
+
* `[attached file: …]` path, and what a refusal notice names. Channel text, so it is
|
|
1896
|
+
* neither trusted nor pre-sanitized here: the attachment writer maps it onto a name that
|
|
1897
|
+
* is safe on disk (see services/task-attachments.ts). A channel with no name for a file
|
|
1898
|
+
* says so with a plain fallback rather than inventing one, since a made-up extension
|
|
1899
|
+
* would tell the model the file is something it is not.
|
|
1900
|
+
*/
|
|
1901
|
+
fileName: string;
|
|
1902
|
+
/** Downloads the file; the same two failure shapes MessagingInboundImage.fetch documents. */
|
|
1903
|
+
fetch(maxBytes: number): Promise<Buffer>;
|
|
1904
|
+
}
|
|
1905
|
+
/** One inbound chat message, normalized across channels. */
|
|
1906
|
+
interface MessagingInboundMessage {
|
|
1907
|
+
/**
|
|
1908
|
+
* Channel-scoped chat id, and the reply target for direct chats. Opaque to the bridge, for
|
|
1909
|
+
* the same reason `messageId` is: the connector both mints it here and consumes it in
|
|
1910
|
+
* `sendText`, so a channel whose replies need more routing context than a chat identity
|
|
1911
|
+
* encodes it (Telegram appends the forum topic). It is stored verbatim as the binding's
|
|
1912
|
+
* last known chat, so whatever is encoded here is what survives a restart.
|
|
1913
|
+
*/
|
|
1914
|
+
chatId: string;
|
|
1915
|
+
/** Direct chat with the bot, or a group chat (groups prefer reply-to-message). */
|
|
1916
|
+
chatKind: "direct" | "group";
|
|
1917
|
+
/**
|
|
1918
|
+
* Channel-scoped id of the inbound message itself (the group reply target). Opaque to
|
|
1919
|
+
* the bridge: the connector both mints it here and consumes it in `replyText`, so a
|
|
1920
|
+
* channel whose native message ids are not globally unique encodes whatever context a
|
|
1921
|
+
* reply needs (Telegram packs `chatId:messageId`). It is also the bridge's inbound
|
|
1922
|
+
* dedupe key, so it must identify the MESSAGE and not the delivery: a channel that
|
|
1923
|
+
* redelivers one message must mint the same id both times. `""` opts the channel out of
|
|
1924
|
+
* deduplication entirely — the honest answer for a connector with no message identity,
|
|
1925
|
+
* and better than every message after the first reading as a duplicate.
|
|
1926
|
+
*/
|
|
1927
|
+
messageId: string;
|
|
1928
|
+
/**
|
|
1929
|
+
* The message's plain text; null for anything that carries none (stickers, voice, …) — a
|
|
1930
|
+
* message with no text, no image and no file gets the not-supported notice. An image or a
|
|
1931
|
+
* file sent with a caption puts the caption here: the caption IS that message's text, so
|
|
1932
|
+
* a channel needs no second field for it and the bridge no second rule. A channel that
|
|
1933
|
+
* carries a caption on media it does NOT deliver must still report null, or the model
|
|
1934
|
+
* answers a question about an attachment it never received.
|
|
1935
|
+
*/
|
|
1936
|
+
text: string | null;
|
|
1937
|
+
/**
|
|
1938
|
+
* Images attached to this message (absent or empty when there are none). Handles rather
|
|
1939
|
+
* than bytes — see MessagingInboundImage. A channel that carries several images in one
|
|
1940
|
+
* message lists them in the order the user sees them.
|
|
1941
|
+
*/
|
|
1942
|
+
images?: readonly MessagingInboundImage[];
|
|
1943
|
+
/**
|
|
1944
|
+
* Non-image files attached to this message, in the order the user sees them (absent or
|
|
1945
|
+
* empty when there are none) — see MessagingInboundFile.
|
|
1946
|
+
*
|
|
1947
|
+
* Optional, like `images`, so a channel that delivers no files simply never populates it
|
|
1948
|
+
* rather than carrying an empty list through its whole normalizer. A message may carry
|
|
1949
|
+
* text, images and files together; the bridge composes one composer input out of whatever
|
|
1950
|
+
* is there.
|
|
1951
|
+
*/
|
|
1952
|
+
files?: readonly MessagingInboundFile[];
|
|
1953
|
+
/** Sender display name when the channel's event carries one. */
|
|
1954
|
+
senderName?: string;
|
|
1955
|
+
}
|
|
1956
|
+
interface MessagingConnectorHandlers {
|
|
1957
|
+
onMessage(msg: MessagingInboundMessage): void | Promise<void>;
|
|
1958
|
+
/** The connection completed a handshake (may fire again after an automatic reconnect). */
|
|
1959
|
+
onReady?(): void;
|
|
1960
|
+
/** The connection failed and the channel gave up (or the initial connect failed). */
|
|
1961
|
+
onError?(err: unknown): void;
|
|
1962
|
+
}
|
|
1963
|
+
/** A live inbound event stream; `close` ends it (idempotent). */
|
|
1964
|
+
interface MessagingConnection {
|
|
1965
|
+
close(): void;
|
|
1966
|
+
}
|
|
1967
|
+
/**
|
|
1968
|
+
* A successful credential check's optional payload: whatever the probe learned that the test
|
|
1969
|
+
* endpoint's feedback can act on. Every member is independently optional — a channel reports
|
|
1970
|
+
* the ones its check happens to answer and omits the rest, so a check that learned nothing
|
|
1971
|
+
* beyond "these credentials work" returns an empty object.
|
|
1972
|
+
*/
|
|
1973
|
+
interface MessagingAccountInfo {
|
|
1974
|
+
/** A short human-readable label of the account the credentials sign in as (Telegram: the bot's `@username`). */
|
|
1975
|
+
accountLabel?: string;
|
|
1976
|
+
/**
|
|
1977
|
+
* Whether this ACCOUNT is set up to receive ordinary messages in the groups it belongs to
|
|
1978
|
+
* — as opposed to only the ones a platform hands a bot by default.
|
|
1979
|
+
*
|
|
1980
|
+
* Telegram's privacy mode is the case that needs saying: it is on for every bot whose
|
|
1981
|
+
* owner has not turned it off in @BotFather, and under it `getUpdates` simply omits
|
|
1982
|
+
* everything in a group that is not a command addressed to this bot or a reply to one of
|
|
1983
|
+
* its messages. Nothing errors — the messages are never delivered, so a binding that works
|
|
1984
|
+
* perfectly in a direct chat looks dead in a group. It is one account-wide setting and
|
|
1985
|
+
* Telegram overrides it in any group the bot administers, so `false` says the account is
|
|
1986
|
+
* muted where it is an ordinary member, never that some particular group is silent. Absent
|
|
1987
|
+
* when the channel has no such notion, or reports nothing about it: unknown must never be
|
|
1988
|
+
* reported as a problem.
|
|
1989
|
+
*/
|
|
1990
|
+
readsGroupMessages?: boolean;
|
|
1991
|
+
}
|
|
1992
|
+
/**
|
|
1993
|
+
* What a send had to give up in order to land, as a short readable phrase. The message DID
|
|
1994
|
+
* arrive — a send that did not throws — so this is not a failure: it says the message is
|
|
1995
|
+
* somewhere less right than it was addressed to, which is otherwise invisible from the
|
|
1996
|
+
* outside. A channel that always delivers as addressed returns nothing at all; Telegram is
|
|
1997
|
+
* the one that reports, a forum topic deleted under a live conversation sending the reply to
|
|
1998
|
+
* General instead.
|
|
1999
|
+
*/
|
|
2000
|
+
type MessagingSendNote = string;
|
|
2001
|
+
/** One file on its way out to a chat: the bytes, plus the name the chat should show. */
|
|
2002
|
+
interface MessagingOutboundFile {
|
|
2003
|
+
/** Display name — the base name of the Workspace-relative path the reply mentioned. */
|
|
2004
|
+
fileName: string;
|
|
2005
|
+
data: Buffer;
|
|
2006
|
+
}
|
|
2007
|
+
/** How one outbound text should be rendered. Absent throughout means plain text. */
|
|
2008
|
+
interface MessagingSendOptions {
|
|
2009
|
+
/**
|
|
2010
|
+
* Read the text as Markdown and render it in whatever this channel's markup is.
|
|
2011
|
+
*
|
|
2012
|
+
* A REQUEST, not an instruction: the connector owns both what its channel can show and
|
|
2013
|
+
* what to do when the channel refuses the rendered form, and it must fall back to sending
|
|
2014
|
+
* this same text plainly rather than let a formatting failure cost the message. What
|
|
2015
|
+
* survives the conversion differs per channel and is documented at each renderer
|
|
2016
|
+
* (telegram-html.ts, feishu-card.ts, qq-markdown.ts).
|
|
2017
|
+
*
|
|
2018
|
+
* Only a relayed assistant message ever sets it. The fixed notices and the test message
|
|
2019
|
+
* carry no markup and are sent as they are written, so a bug in a renderer can never
|
|
2020
|
+
* reach the one message a user sends to check whether the binding works at all.
|
|
2021
|
+
*/
|
|
2022
|
+
markdown?: boolean;
|
|
2023
|
+
}
|
|
2024
|
+
/** Outbound half of one bound account. Every method throws on failure with a readable reason. */
|
|
2025
|
+
interface MessagingClient {
|
|
2026
|
+
/** Credential check (used by the test endpoint); resolving means the config signs in. */
|
|
2027
|
+
checkCredentials(): Promise<MessagingAccountInfo | null>;
|
|
2028
|
+
/** Sends a text message into a chat by chat id; resolves a MessagingSendNote when it degraded. */
|
|
2029
|
+
sendText(chatId: string, text: string, opts?: MessagingSendOptions): Promise<MessagingSendNote | void>;
|
|
2030
|
+
/** Replies a text message to a specific inbound message (threads correctly in group chats). */
|
|
2031
|
+
replyText(messageId: string, text: string, opts?: MessagingSendOptions): Promise<MessagingSendNote | void>;
|
|
2032
|
+
/**
|
|
2033
|
+
* Sends a picture into a chat, so a chart the Agent drew arrives as something the reader
|
|
2034
|
+
* can see rather than as a download. Channels that need an upload step first do it here —
|
|
2035
|
+
* the bridge holds bytes and a name, never a channel's file handle.
|
|
2036
|
+
*/
|
|
2037
|
+
sendImage(chatId: string, file: MessagingOutboundFile): Promise<void>;
|
|
2038
|
+
/** Sends any other file into a chat as an attachment. */
|
|
2039
|
+
sendFile(chatId: string, file: MessagingOutboundFile): Promise<void>;
|
|
2040
|
+
}
|
|
2041
|
+
interface MessagingChannelConnector {
|
|
2042
|
+
readonly channel: MessagingChannel;
|
|
2043
|
+
/**
|
|
2044
|
+
* How many outbound messages this channel will accept in answer to ONE inbound message,
|
|
2045
|
+
* or undefined where no such limit exists (Feishu and Telegram both send freely).
|
|
2046
|
+
*
|
|
2047
|
+
* A channel that declares one enforces it itself — the connector is the only place that
|
|
2048
|
+
* knows what the platform rejects and how to combine messages so nothing is lost. What
|
|
2049
|
+
* the bridge does with the number is narrower: it caps the one-message-per-line split at
|
|
2050
|
+
* it, because that option's own ceiling is sized for a channel with no such budget and
|
|
2051
|
+
* would otherwise ask for more messages than the channel can ever deliver.
|
|
2052
|
+
*/
|
|
2053
|
+
readonly replyBudget?: number;
|
|
2054
|
+
/** Builds the outbound client for one stored config (throws on a malformed document). */
|
|
2055
|
+
createClient(config: Record<string, unknown>): Promise<MessagingClient>;
|
|
2056
|
+
/**
|
|
2057
|
+
* Opens the inbound event stream for one stored config. Resolves as soon as the
|
|
2058
|
+
* connection is constructed and connecting — lifecycle arrives via the handlers
|
|
2059
|
+
* (`onReady` / `onError`), because channels reconnect on their own and a single promise
|
|
2060
|
+
* cannot carry a lifecycle.
|
|
2061
|
+
*/
|
|
2062
|
+
connect(config: Record<string, unknown>, handlers: MessagingConnectorHandlers): Promise<MessagingConnection>;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
/**
|
|
2066
|
+
* Auth middleware: `Authorization: Bearer <local API token>` -> the built-in admin, or
|
|
2067
|
+
* session cookie -> auth_sessions row -> user; either way the user is injected into c.var.
|
|
2068
|
+
*
|
|
2069
|
+
* The Bearer path authenticates the boot's local API token (`<root>/api-token`, see
|
|
2070
|
+
* auth/api-token.ts) as the admin — the CLI's and agents' machine-local credential; it
|
|
2071
|
+
* applies to every route behind this middleware, SSE endpoints included (the CLI
|
|
2072
|
+
* consumes SSE via fetch with headers). A Bearer header that does not match fails the
|
|
2073
|
+
* request rather than falling back to the cookie: silently downgrading a wrong explicit
|
|
2074
|
+
* credential would mask misconfiguration.
|
|
2075
|
+
*
|
|
2076
|
+
* Accessing a protected API while logged out -> 401 `{error:{code:"unauthorized"}}`.
|
|
2077
|
+
* CSRF: SameSite=Lax plus a Content-Type an HTML form cannot forge (see jsonOnlyWrites).
|
|
2078
|
+
* That guard stays for Bearer requests too — the CLI always sends application/json.
|
|
2079
|
+
*/
|
|
2080
|
+
|
|
2081
|
+
/** Hono env: variables injected by the auth middleware. */
|
|
2082
|
+
type AppEnv = {
|
|
2083
|
+
Variables: {
|
|
2084
|
+
user: UserRow;
|
|
2085
|
+
/** How the current session was established — see {@link SessionVia}. */
|
|
2086
|
+
sessionVia: SessionVia;
|
|
2087
|
+
};
|
|
2088
|
+
};
|
|
2089
|
+
|
|
2090
|
+
/** Minimal dependency on SessionManager (eases test doubles; mirrors ScheduleTaskRunner). */
|
|
2091
|
+
interface MessagingTaskRunnerShape {
|
|
2092
|
+
statusOf(sessionId: string): string;
|
|
2093
|
+
startTask(sessionId: string, input: OmniMessage[],
|
|
2094
|
+
/**
|
|
2095
|
+
* `recall` is what a queued follow-up hands back when the user withdraws it in the Web
|
|
2096
|
+
* App. It is optional because the manager derives one from the input for every caller
|
|
2097
|
+
* that has nothing to add; the inbound path supplies its own only when it wrote file
|
|
2098
|
+
* attachments, whose on-disk paths the input does not carry (see inboundRecallStore).
|
|
2099
|
+
*/
|
|
2100
|
+
opts: {
|
|
2101
|
+
queueIfBusy: boolean;
|
|
2102
|
+
recall?: RecallStore;
|
|
2103
|
+
}): Promise<{
|
|
2104
|
+
sessionId: string;
|
|
2105
|
+
queued: boolean;
|
|
2106
|
+
}>;
|
|
2107
|
+
}
|
|
2108
|
+
/**
|
|
2109
|
+
* Minimal dependency on the sessions index: existence checks for reconcile/cascade, the
|
|
2110
|
+
* Session's Project/Agent so a messaging failure can be filed under them (see recordError),
|
|
2111
|
+
* and the one field an outbound file needs — the Workspace a mentioned path resolves against.
|
|
2112
|
+
*/
|
|
2113
|
+
interface MessagingSessionIndex {
|
|
2114
|
+
findById(sessionId: string): {
|
|
2115
|
+
projectId: string;
|
|
2116
|
+
agentId: string;
|
|
2117
|
+
workspace: string;
|
|
2118
|
+
} | null;
|
|
2119
|
+
}
|
|
2120
|
+
/**
|
|
2121
|
+
* Minimal dependency on the Workspace file service. Narrow, but deliberately NOT a
|
|
2122
|
+
* reimplementation: both methods carry that service's containment rules (a path that leaves
|
|
2123
|
+
* the Workspace — by `..`, by an absolute path, or through a symlink — is refused there,
|
|
2124
|
+
* and reads happen on the canonical path). A second resolver living in the bridge is
|
|
2125
|
+
* exactly the kind of thing that ends up one security fix behind.
|
|
2126
|
+
*/
|
|
2127
|
+
interface MessagingWorkspaceFiles {
|
|
2128
|
+
/**
|
|
2129
|
+
* The subset of `rels` that exist as regular files inside the Workspace, each with the
|
|
2130
|
+
* time it was last written — which is what separates a file this run produced from one
|
|
2131
|
+
* the reply merely named (see deliverFiles).
|
|
2132
|
+
*/
|
|
2133
|
+
statExistingWithMtime(workspace: string, rels: string[]): Promise<{
|
|
2134
|
+
rel: string;
|
|
2135
|
+
mtimeMs: number;
|
|
2136
|
+
}[]>;
|
|
2137
|
+
/** Reads a file, at most `maxBytes` of it. */
|
|
2138
|
+
read(workspace: string, rel: string, options?: {
|
|
2139
|
+
maxBytes?: number;
|
|
2140
|
+
}): Promise<{
|
|
2141
|
+
data: Buffer;
|
|
2142
|
+
fileName: string;
|
|
2143
|
+
}>;
|
|
2144
|
+
}
|
|
2145
|
+
interface MessagingBridgeDeps {
|
|
2146
|
+
repo: MessagingBindings;
|
|
2147
|
+
sessions: MessagingSessionIndex;
|
|
2148
|
+
files: MessagingWorkspaceFiles;
|
|
2149
|
+
/**
|
|
2150
|
+
* The data root, so an inbound file can be written into the Session scratchpad the web
|
|
2151
|
+
* composer's uploads land in (`scratchpadDir(root, projectId, agentId)/<sessionId>`,
|
|
2152
|
+
* deleted with the Session). The bridge holds the root rather than a pre-resolved
|
|
2153
|
+
* directory because the Project and Agent are per Session, not per binding.
|
|
2154
|
+
*/
|
|
2155
|
+
root: string;
|
|
2156
|
+
/**
|
|
2157
|
+
* The attachment limits in force right now. A function, not a snapshot: the per-file and
|
|
2158
|
+
* per-message ceilings are admin-settable and read fresh per request everywhere else, so
|
|
2159
|
+
* a change has to reach the next inbound message without a restart.
|
|
2160
|
+
*/
|
|
2161
|
+
attachmentLimits: () => AttachmentLimits;
|
|
2162
|
+
channels: ChannelHub;
|
|
2163
|
+
runner: MessagingTaskRunner;
|
|
2164
|
+
/** One connector per channel; a stored binding whose channel has no connector is skipped with an error record. */
|
|
2165
|
+
connectors: readonly MessagingChannelConnector[];
|
|
2166
|
+
errors: ErrorSink;
|
|
2167
|
+
log?: (line: string) => void;
|
|
2168
|
+
now?: () => number;
|
|
2169
|
+
/** Test hook: the pace between a per-line reply's messages (default MESSAGING_LINE_DELAY_MS; tests collapse it to zero). */
|
|
2170
|
+
lineDelayMs?: number;
|
|
2171
|
+
/** Test hook: one binding's inbound image budget (default MESSAGING_INBOUND_IMAGE_BUDGET_BYTES). */
|
|
2172
|
+
inboundImageBudgetBytes?: number;
|
|
2173
|
+
}
|
|
2174
|
+
declare class MessagingBridge {
|
|
2175
|
+
private readonly deps;
|
|
2176
|
+
private readonly entries;
|
|
2177
|
+
/**
|
|
2178
|
+
* Processed inbound ids per BINDING (`sessionId:channel`), deliberately not per
|
|
2179
|
+
* connection: re-saving an enabled binding restarts its connector, and a message
|
|
2180
|
+
* already turned into a Task must not run again because the connector is new.
|
|
2181
|
+
*
|
|
2182
|
+
* This map dies with the process; `messaging_bindings.last_inbound_message_id` is the
|
|
2183
|
+
* half that does not, and `connect` folds it back in here so the two are one memory
|
|
2184
|
+
* rather than two guards. The row holds ONE id — the last message the bridge finished
|
|
2185
|
+
* with — which is what Feishu's WebSocket resume needs and all it needs: that stream
|
|
2186
|
+
* replays events it never saw acknowledged, and the SDK acknowledges each one only
|
|
2187
|
+
* after our handler returns, so at most the one in flight when the process ended is
|
|
2188
|
+
* still owed. Telegram redelivers nothing across a restart — its poller advances
|
|
2189
|
+
* `offset` past an update before handing it over, and a fresh connection drains with
|
|
2190
|
+
* `getUpdates({offset: -1})`, discarding whatever was sent while nobody was connected —
|
|
2191
|
+
* so on that channel the row is still written and still seeded, and simply never has a
|
|
2192
|
+
* replay to catch.
|
|
2193
|
+
*/
|
|
2194
|
+
private readonly recentInbound;
|
|
2195
|
+
/** Inbound image budgets, keyed like recentInbound: per BINDING, surviving a reconnect. */
|
|
2196
|
+
private readonly imageBudgets;
|
|
2197
|
+
private readonly connectors;
|
|
2198
|
+
private readonly now;
|
|
2199
|
+
private readonly log;
|
|
2200
|
+
private readonly lineDelayMs;
|
|
2201
|
+
private readonly inboundImageBudgetBytes;
|
|
2202
|
+
constructor(deps: MessagingBridgeDeps);
|
|
2203
|
+
/**
|
|
2204
|
+
* Server startup: connect every ENABLED binding (disabled ones keep their credentials
|
|
2205
|
+
* and stay dark; at most one per Session is enabled). A binding whose Session no longer
|
|
2206
|
+
* exists (deleted while this server was down, or by a bulk Agent/Project delete that
|
|
2207
|
+
* bypassed the per-session cascade) is reconciled away instead of connected.
|
|
2208
|
+
*/
|
|
2209
|
+
start(): Promise<void>;
|
|
2210
|
+
/** App dispose: close every connection; bindings persist for the successor's start(). */
|
|
2211
|
+
stop(): void;
|
|
2212
|
+
/**
|
|
2213
|
+
* Align the live connection with the stored intent: an enabled binding exists →
|
|
2214
|
+
* (re)connect with its CURRENT config, none → disconnect. The state toggle calls this
|
|
2215
|
+
* after flipping intent, and a credential save calls it only while the saved binding is
|
|
2216
|
+
* enabled — the restart that keeps stored config and live connection from diverging.
|
|
2217
|
+
*/
|
|
2218
|
+
sync(sessionId: string): Promise<void>;
|
|
2219
|
+
/** Route DELETE: disconnect (when this channel holds the connection) and drop the row. No-op when unbound. */
|
|
2220
|
+
unbind(sessionId: string, channel: string): void;
|
|
2221
|
+
/** Session-delete cascade: disconnect and drop every channel's config. */
|
|
2222
|
+
unbindSession(sessionId: string): void;
|
|
2223
|
+
/**
|
|
2224
|
+
* One channel's runtime status (only the connected channel is ever anything but
|
|
2225
|
+
* disconnected), plus what this connection has actually seen: when a message last arrived,
|
|
2226
|
+
* what last failed after one did, and the last connection failure even after it recovered.
|
|
2227
|
+
* A binding can be `connected` with nothing wrong and still never receive anything, and
|
|
2228
|
+
* that combination is invisible without them.
|
|
2229
|
+
*/
|
|
2230
|
+
statusOf(sessionId: string, channel: string): MessagingRuntimeStatus;
|
|
2231
|
+
/**
|
|
2232
|
+
* Credential probe for the test endpoints: ok/error with latency, never a throw. A
|
|
2233
|
+
* channel whose check identifies the account (Telegram: the bot's @username) passes
|
|
2234
|
+
* that label through for the route's success feedback, along with anything else the
|
|
2235
|
+
* probe learned that the user should act on (see MessagingAccountInfo).
|
|
2236
|
+
*/
|
|
2237
|
+
testCredentials(channel: string, config: Record<string, unknown>): Promise<{
|
|
2238
|
+
ok: boolean;
|
|
2239
|
+
latencyMs?: number;
|
|
2240
|
+
accountLabel?: string;
|
|
2241
|
+
readsGroupMessages?: boolean;
|
|
2242
|
+
error?: string;
|
|
2243
|
+
}>;
|
|
2244
|
+
/**
|
|
2245
|
+
* Test-message endpoints: a short fixed text to the binding's last known chat. The
|
|
2246
|
+
* route has already established that the binding and its chat exist; a race that
|
|
2247
|
+
* removed either since surfaces as the send failing.
|
|
2248
|
+
*/
|
|
2249
|
+
sendTestMessage(row: MessagingBindingRow): Promise<void>;
|
|
2250
|
+
private connectorFor;
|
|
2251
|
+
private connect;
|
|
2252
|
+
private disconnect;
|
|
2253
|
+
/** Every timestamp this bridge writes comes from the injected clock, never from `Date` directly. */
|
|
2254
|
+
private nowIso;
|
|
2255
|
+
/** Status write, guarded against a stale entry (replaced by a newer connect). */
|
|
2256
|
+
private setStatus;
|
|
2257
|
+
/**
|
|
2258
|
+
* A connection failure, put in the three places one belongs: the live `error` state, the
|
|
2259
|
+
* copy kept across recovery, and the error table.
|
|
2260
|
+
*
|
|
2261
|
+
* Behind the same stale-entry guard `setStatus` uses, applied to all three — a connector
|
|
2262
|
+
* callback firing after a newer connect replaced this entry is reporting an attempt that was
|
|
2263
|
+
* already abandoned, and neither its status writes (which nothing reads) nor its record
|
|
2264
|
+
* (which would be filed against a binding since reconfigured) belong to the binding as it
|
|
2265
|
+
* now stands.
|
|
2266
|
+
*/
|
|
2267
|
+
private recordConnectionFailure;
|
|
2268
|
+
/**
|
|
2269
|
+
* One error record for a messaging failure, filed under the Session's Project and Agent.
|
|
2270
|
+
*
|
|
2271
|
+
* The attribution is load-bearing, not decoration: `GET /api/projects/:id/usage/errors`
|
|
2272
|
+
* selects by project, and a record with none is "unattributed" — served only to admins
|
|
2273
|
+
* (`includeGlobalErrors`), so an ordinary member could never see a failure of their own
|
|
2274
|
+
* binding anywhere. The lookup uses the sessions index the bridge already holds; a row
|
|
2275
|
+
* deleted since leaves the record unattributed rather than dropping it.
|
|
2276
|
+
*/
|
|
2277
|
+
private recordError;
|
|
2278
|
+
/**
|
|
2279
|
+
* A failure AFTER an inbound message was accepted: recorded like any other, and also put
|
|
2280
|
+
* on the binding's runtime status so the panel can say it. The error table is a Project-wide
|
|
2281
|
+
* dashboard nobody visits to debug one bot; the binding's own panel is where the question
|
|
2282
|
+
* is asked.
|
|
2283
|
+
*
|
|
2284
|
+
* A later success does not clear it. An intermittent failure — a group admin who revokes
|
|
2285
|
+
* send rights for a minute, a rate limit — would otherwise be erased by the next ordinary
|
|
2286
|
+
* message and never be seen at all, which is the same blind spot `lastConnectionError`
|
|
2287
|
+
* exists to close. The panel states `at` so a stale one reads as stale.
|
|
2288
|
+
*/
|
|
2289
|
+
private recordDeliveryFailure;
|
|
2290
|
+
/**
|
|
2291
|
+
* A send that landed somewhere less right than it was addressed to. Not a failure — the
|
|
2292
|
+
* message arrived — so it stays out of `lastDeliveryError` and off the panel, and goes to
|
|
2293
|
+
* the log, which is where "the replies moved to General a while ago" is answerable at all.
|
|
2294
|
+
*/
|
|
2295
|
+
private noteSend;
|
|
2296
|
+
private clientFor;
|
|
2297
|
+
/**
|
|
2298
|
+
* Has this binding already processed this message? A duplicate is a complete no-op —
|
|
2299
|
+
* ahead of the chat record and the text-only reply, not just the Task start, because a
|
|
2300
|
+
* replayed sticker would otherwise answer with the notice twice.
|
|
2301
|
+
*
|
|
2302
|
+
* Nothing downstream is idempotent: `startTask` with `queueIfBusy` appends to the
|
|
2303
|
+
* follow-up queue unconditionally, and a queued input is published to the Session
|
|
2304
|
+
* channel, so one redelivery becomes two runs in the chat AND two messages in the Web
|
|
2305
|
+
* App. A connector that mints no id opts out rather than having every message after the
|
|
2306
|
+
* first read as a duplicate.
|
|
2307
|
+
*/
|
|
2308
|
+
private isRedelivery;
|
|
2309
|
+
/** Get-or-create one binding's processed-id memory (see recentInbound). */
|
|
2310
|
+
private inboundMemoryOf;
|
|
2311
|
+
private onInbound;
|
|
2312
|
+
/**
|
|
2313
|
+
* Turns a message carrying attachments into one Task, or resolves the notice to answer
|
|
2314
|
+
* the chat with when it could not be made whole.
|
|
2315
|
+
*
|
|
2316
|
+
* All-or-nothing across BOTH kinds, and in this order: the images are downloaded first,
|
|
2317
|
+
* then the files, and only then is anything written to disk. A refusal on either side
|
|
2318
|
+
* stops the message — a message is one thing the user sent, and half of it reaching the
|
|
2319
|
+
* Agent is not a partial success but a misleading one — and refusing before the write
|
|
2320
|
+
* means a rejected message leaves nothing behind in the scratchpad to clean up.
|
|
2321
|
+
*/
|
|
2322
|
+
private startAttachedTask;
|
|
2323
|
+
/**
|
|
2324
|
+
* Lands this message's files in the Session scratchpad and folds their path lines into the
|
|
2325
|
+
* input. Returns the input untouched when there are none, so a message with no files
|
|
2326
|
+
* creates no directory.
|
|
2327
|
+
*
|
|
2328
|
+
* A failure here is NOT answered in the chat, unlike a refused download: a write that
|
|
2329
|
+
* fails is a fault of this server (a full disk, a scratchpad that is not writable), the
|
|
2330
|
+
* same class as a Session that cannot load, and it reaches the user through
|
|
2331
|
+
* recordDeliveryFailure and the binding's status line. The chat is told about the things
|
|
2332
|
+
* the person in it can act on.
|
|
2333
|
+
*/
|
|
2334
|
+
private writeInboundFiles;
|
|
2335
|
+
/**
|
|
2336
|
+
* Downloads the message's files into composer attachments, or the notice to answer with
|
|
2337
|
+
* when one of them could not be delivered.
|
|
2338
|
+
*
|
|
2339
|
+
* The caps are the server's attachment limits — the very numbers an authenticated
|
|
2340
|
+
* composer upload answers to — because these files land in exactly the same place, the
|
|
2341
|
+
* Session scratchpad, and are read back the same way, through the model's bounded file
|
|
2342
|
+
* tools. Both byte budgets apply:
|
|
2343
|
+
*
|
|
2344
|
+
* - the per-file ceiling rides into each `fetch`, so an oversized attachment is refused at
|
|
2345
|
+
* the byte that crosses it and is never resident in this process;
|
|
2346
|
+
* - the per-message total is spent as the batch is collected, each later file's cap
|
|
2347
|
+
* shrinking to what is left, so a message carrying several cannot add up past it either.
|
|
2348
|
+
*
|
|
2349
|
+
* The per-message file COUNT is deliberately not enforced: attachment-limits.ts calls it a
|
|
2350
|
+
* composer-usability bound rather than a resource one, the byte budgets being what actually
|
|
2351
|
+
* bound memory and disk — and a chat message carries one file today on both channels.
|
|
2352
|
+
*
|
|
2353
|
+
* There is no rolling per-binding budget like the image one, because the cost it exists to
|
|
2354
|
+
* prevent is not paid here: an inline image is written into the Trace and re-read whole on
|
|
2355
|
+
* every history page and every resume, while a file is bytes on disk that nothing re-reads
|
|
2356
|
+
* and that are deleted with the Session.
|
|
2357
|
+
*
|
|
2358
|
+
* A refusal for size is the user's to fix and is not an error record — they sent something
|
|
2359
|
+
* too big and the chat says so. A failure is somebody's fault (a scope the bot was never
|
|
2360
|
+
* granted, most likely) and lands in the error log with the channel's reason, so it is
|
|
2361
|
+
* diagnosable from the dashboard and not only from a chat bubble.
|
|
2362
|
+
*/
|
|
2363
|
+
private inboundFileAttachments;
|
|
2364
|
+
/**
|
|
2365
|
+
* Downloads the message's images into `image_url` input parts, or the notice to answer
|
|
2366
|
+
* with when one of them could not be delivered.
|
|
2367
|
+
*
|
|
2368
|
+
* The cap is the server's inline-image ceiling — the same number the web composer's
|
|
2369
|
+
* uploads answer to, because these images land in exactly the same place (the
|
|
2370
|
+
* conversation, and from there the Trace, which is read back whole on every resume). It
|
|
2371
|
+
* happens to match Telegram's own 20MB download ceiling for bots.
|
|
2372
|
+
*
|
|
2373
|
+
* A refusal for size is the user's to fix and is not an error record — they sent
|
|
2374
|
+
* something too big and the chat says so. A failure is somebody's fault (a scope the bot
|
|
2375
|
+
* was never granted, most likely) and lands in the error log with the channel's reason,
|
|
2376
|
+
* so it is diagnosable from the dashboard and not only from a chat bubble.
|
|
2377
|
+
*/
|
|
2378
|
+
private inboundImageParts;
|
|
2379
|
+
/** This binding's rolling image budget, created on first use (see InboundImageBudget). */
|
|
2380
|
+
private imageBudgetFor;
|
|
2381
|
+
/** Reply to the inbound message itself: threaded reply in groups, plain send in direct chats. */
|
|
2382
|
+
private replyInbound;
|
|
2383
|
+
/** Channel tap: each main-conversation completed assistant message + the run-state flips. */
|
|
2384
|
+
private observe;
|
|
2385
|
+
/**
|
|
2386
|
+
* Queues one completed assistant message for the bound chat — or holds it back.
|
|
2387
|
+
*
|
|
2388
|
+
* Appended to the entry's send chain rather than started here: a run can complete several
|
|
2389
|
+
* messages within milliseconds, and two sends racing would reach the chat in the wrong
|
|
2390
|
+
* order. Returns immediately either way, so a slow channel never blocks the Session's
|
|
2391
|
+
* stream handling.
|
|
2392
|
+
*
|
|
2393
|
+
* A binding with `finalReplyOnly` set sends nothing from here: the message replaces
|
|
2394
|
+
* whatever this run was already holding, and the last one standing is delivered at the
|
|
2395
|
+
* run's idle edge (see onTaskState). The preference is read per message off the stored row,
|
|
2396
|
+
* as every delivery preference is, and no run ever sees it change: each of the three PUT
|
|
2397
|
+
* handlers calls `sync` on an enabled binding, `sync` reconnects, and `connect` opens with a
|
|
2398
|
+
* disconnect and a fresh entry — so a save made mid-run discards whatever was held at that
|
|
2399
|
+
* moment along with the rest of this entry's memory (see heldReply).
|
|
2400
|
+
*/
|
|
2401
|
+
private relay;
|
|
2402
|
+
/**
|
|
2403
|
+
* Whether this binding holds a run's intermediate messages back. A row that cannot be read
|
|
2404
|
+
* at all is deliberately not decided here: relaying is the default, and the same read
|
|
2405
|
+
* inside deliverReply then records the failure rather than letting it pass as a preference.
|
|
2406
|
+
*/
|
|
2407
|
+
private finalReplyOnly;
|
|
2408
|
+
/**
|
|
2409
|
+
* Where this entry's traffic would go right now: null before any chat is known, and null
|
|
2410
|
+
* when the row cannot be read at all — a caller holding null falls back to reading it again
|
|
2411
|
+
* at delivery time, which is where a failure to read is recorded rather than swallowed.
|
|
2412
|
+
*/
|
|
2413
|
+
private replyTargetOf;
|
|
2414
|
+
private onTaskState;
|
|
2415
|
+
/**
|
|
2416
|
+
* Sends one relayed assistant message to the bound chat, in chunks under the channel's cap.
|
|
2417
|
+
* Which messages arrive here is the caller's business — every completed message of the run,
|
|
2418
|
+
* or its last one alone when the binding holds the rest (`finalReplyOnly`) — and nothing
|
|
2419
|
+
* below is affected by the difference.
|
|
2420
|
+
*
|
|
2421
|
+
* `at` is where to put it. A message relayed as it completes passes none and goes to the
|
|
2422
|
+
* chat the row names now, which is where the conversation is. A held reply passes the target
|
|
2423
|
+
* captured when its run started (see onTaskState), because it leaves at the run's END: a
|
|
2424
|
+
* second user writing in another forum topic meanwhile would otherwise take delivery of a
|
|
2425
|
+
* whole answer to a question they never asked, quoted onto their own message.
|
|
2426
|
+
*
|
|
2427
|
+
* A send failure is recorded, never thrown, so the chain behind it keeps
|
|
2428
|
+
* moving — and so do the messages behind it in THIS reply: one refused message (a 429 on
|
|
2429
|
+
* the third line of twelve) must not abandon the rest, which would leave a reply stopping
|
|
2430
|
+
* mid-sentence with nothing in the chat to say why. A binding with `linePerMessage` set
|
|
2431
|
+
* sends one message per non-blank line instead of one per reply (see splitReplyLines,
|
|
2432
|
+
* capped at the channel's own `replyBudget` where it declares one), paced
|
|
2433
|
+
* MESSAGING_LINE_DELAY_MS apart so the burst stays inside the channel's per-chat
|
|
2434
|
+
* allowance — chunking, threading and ordering are identical either way, and with the flag
|
|
2435
|
+
* off the send sequence is unchanged. In a group the run's
|
|
2436
|
+
* first outbound chunk threads onto the inbound message and
|
|
2437
|
+
* everything after it is a plain send: the reply-to relation names which message is being
|
|
2438
|
+
* answered, and one of them says it — repeating it per message and per chunk stacks quote
|
|
2439
|
+
* headers over the whole conversation.
|
|
2440
|
+
*
|
|
2441
|
+
* A binding with `renderMarkdown` set relays the model's Markdown as formatting rather
|
|
2442
|
+
* than as characters. The bridge only carries the REQUEST: the conversion is the
|
|
2443
|
+
* connector's, because what a channel can show and what it does with a rendering the
|
|
2444
|
+
* channel refuses are both channel-shaped (see MessagingSendOptions). What is not
|
|
2445
|
+
* channel-shaped is where a reply too long for one message may be cut, and that is why the
|
|
2446
|
+
* chunker changes with the flag — a cut through a code fence or an emphasis run costs the
|
|
2447
|
+
* construct everywhere, and on a strict parser costs the whole message's formatting.
|
|
2448
|
+
*/
|
|
2449
|
+
private deliverReply;
|
|
2450
|
+
/**
|
|
2451
|
+
* Sends the files THIS RUN produced and its reply mentioned, after its text.
|
|
2452
|
+
*
|
|
2453
|
+
* `replyText` is what the run actually SENT, not everything it said: a binding with
|
|
2454
|
+
* `finalReplyOnly` set delivered one message, so that message is the whole of what this
|
|
2455
|
+
* scans (see onTaskState). A file named only in a held-back working note has nothing in
|
|
2456
|
+
* the chat to explain its arrival.
|
|
2457
|
+
*
|
|
2458
|
+
* `at` is where those words went, for the one caller whose text is not addressed to the live
|
|
2459
|
+
* chat (again the held reply). Files follow their words: a picture landing in a forum topic
|
|
2460
|
+
* where nothing named it is the same defect as one arriving unmentioned.
|
|
2461
|
+
*
|
|
2462
|
+
* Two filters, and both are load-bearing. The reply having said the name is what picks
|
|
2463
|
+
* the one output that was the point out of the dozen a run writes. The file being newer
|
|
2464
|
+
* than the run's start is what keeps that from becoming an exfiltration primitive: the
|
|
2465
|
+
* reply is steerable by whoever is in the chat, and on a mention-only rule a group member
|
|
2466
|
+
* who asks "what is in secrets.json?" gets the file uploaded by the very refusal that
|
|
2467
|
+
* declined to paste it. Containment (the Workspace and nothing narrower) lives in the
|
|
2468
|
+
* file service either way — see MessagingWorkspaceFiles.
|
|
2469
|
+
*
|
|
2470
|
+
* A file the run did not write is dropped SILENTLY, and so is a name matching no file at
|
|
2471
|
+
* all: the rule reads prose, so a reply that mentions the config it read — or describes a
|
|
2472
|
+
* `hello-world.md` it never wrote — is the ordinary case, and announcing those puts an
|
|
2473
|
+
* error-shaped line under correct answers. Both are logged instead. What IS named in the
|
|
2474
|
+
* chat is a file that exists and whose upload failed: there the reply promised something
|
|
2475
|
+
* that then did not arrive, which is the one case where silence reads as broken.
|
|
2476
|
+
*
|
|
2477
|
+
* Always plain sends, never a threaded reply: a run that mentions a file has by
|
|
2478
|
+
* definition already sent the text that mentions it, and that message took the group's
|
|
2479
|
+
* one reply-to. Nothing here throws — a batch that fails is recorded, and a single file
|
|
2480
|
+
* that fails does not stop the ones behind it.
|
|
2481
|
+
*/
|
|
2482
|
+
private deliverFiles;
|
|
2483
|
+
/**
|
|
2484
|
+
* Names one file that did not make it, in the chat.
|
|
2485
|
+
*
|
|
2486
|
+
* Every other way a file does not arrive is already visible there — over the byte cap,
|
|
2487
|
+
* past the count cap, an inbound refusal. A failed upload reached `error_records` alone,
|
|
2488
|
+
* which the person in the chat cannot open, so the feature just looked broken. A missing
|
|
2489
|
+
* permission gets its own wording: it is the one failure they can fix themselves, in
|
|
2490
|
+
* about ten seconds, given the scope name and the console link.
|
|
2491
|
+
*
|
|
2492
|
+
* Its own try/catch: a chat that will not take the notice must not take the rest of the
|
|
2493
|
+
* batch down with it, and the upload failure behind it is already recorded.
|
|
2494
|
+
*/
|
|
2495
|
+
private noteFileFailure;
|
|
2496
|
+
/** One mirrored file: read under the outer ceiling, then sent as a picture or an attachment. */
|
|
2497
|
+
private deliverOneFile;
|
|
2498
|
+
/** One-line notice (approval waiting) to the last known chat; silent before one exists. */
|
|
2499
|
+
private deliverNotice;
|
|
2500
|
+
/**
|
|
2501
|
+
* Where this entry's outbound traffic goes, with a client for it: null when no chat is
|
|
2502
|
+
* known yet (nothing mirrors before the first inbound message) and null when the binding
|
|
2503
|
+
* or its client could not be read at all, which is recorded like the send it stands in for.
|
|
2504
|
+
*/
|
|
2505
|
+
private sendTarget;
|
|
2506
|
+
/**
|
|
2507
|
+
* Whether this entry still holds its Session's connection — asked at the head of every
|
|
2508
|
+
* step of the send chain, the way `observe` asks it of every event.
|
|
2509
|
+
*
|
|
2510
|
+
* The chain outlives what fed it. A run's last messages and the files behind them are
|
|
2511
|
+
* queued on it, and `stop()` — App dispose, a hot swap — drops the entry and takes the
|
|
2512
|
+
* database with it while they are still waiting their turn. A step that ran anyway would
|
|
2513
|
+
* read a closed database, and it would fail again inside its own error recording, leaving
|
|
2514
|
+
* the chain rejected with nothing left to catch it.
|
|
2515
|
+
*/
|
|
2516
|
+
private live;
|
|
2517
|
+
/** The pace between a per-line reply's messages; zero (tests) waits for nothing. */
|
|
2518
|
+
private pace;
|
|
2519
|
+
}
|
|
2520
|
+
declare const MessagingTaskRunner_base: (abstract new () => MessagingTaskRunnerShape) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
2521
|
+
/** What the bridge needs of the session runtime — declared at the consumer. */
|
|
2522
|
+
declare abstract class MessagingTaskRunner extends MessagingTaskRunner_base {
|
|
2523
|
+
}
|
|
2524
|
+
declare const Messaging_base: (abstract new () => Pick<MessagingBridge, "start" | "stop" | "statusOf" | "sync" | "unbind" | "unbindSession" | "testCredentials" | "sendTestMessage">) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
2525
|
+
/** The chat-channel bridge: bindings, delivery, status. */
|
|
2526
|
+
declare abstract class Messaging extends Messaging_base {
|
|
2527
|
+
}
|
|
2528
|
+
interface MessagingSlots {
|
|
2529
|
+
/** A channel connector: which channel it speaks (static), and the connector (code). */
|
|
2530
|
+
connectors: Slot<{
|
|
2531
|
+
channel: "feishu" | "telegram" | "qq" | "wechat";
|
|
2532
|
+
}, MessagingChannelConnector>;
|
|
2533
|
+
}
|
|
2534
|
+
|
|
2535
|
+
declare const Http_base: (abstract new () => {
|
|
2536
|
+
fetch(request: Opaque<"Request", Request>): Promise<Opaque<"Response", Response>>;
|
|
2537
|
+
}) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
2538
|
+
/** The assembled business surface: one request in, one response (or a decline) out. */
|
|
2539
|
+
declare abstract class Http extends Http_base {
|
|
2540
|
+
}
|
|
2541
|
+
interface HttpSlots {
|
|
2542
|
+
/**
|
|
2543
|
+
* A route group. `auth: "user"` mounts it behind the cookie gate, `"none"` in front of
|
|
2544
|
+
* it; `order` is the mount position (a stable number, since Hono matches in order). The
|
|
2545
|
+
* code half is the group's Hono app.
|
|
2546
|
+
*/
|
|
2547
|
+
routes: Slot<{
|
|
2548
|
+
prefix: string;
|
|
2549
|
+
auth: "user" | "none";
|
|
2550
|
+
order: number;
|
|
2551
|
+
}, Opaque<"Hono", Hono<AppEnv>>>;
|
|
2552
|
+
}
|
|
2553
|
+
|
|
2554
|
+
/**
|
|
2555
|
+
* GET /api/contributions — the web slots' contributions, as data. What a pushed platform
|
|
2556
|
+
* or an installed plugin adds to the web app arrives here; the app merges it with its
|
|
2557
|
+
* own module.json and renders what it has a renderer for.
|
|
2558
|
+
*/
|
|
2559
|
+
|
|
2560
|
+
declare const WebShell_base: (abstract new () => {
|
|
2561
|
+
/** Every contribution to the web slots, by slot, in module order. */
|
|
2562
|
+
contributions(): ContributionsResponse;
|
|
2563
|
+
}) & (<C>(target: C, context: ClassDecoratorContext) => C);
|
|
2564
|
+
/**
|
|
2565
|
+
* The frontend's slots, declared on the SERVER so a server module (or a plugin) can
|
|
2566
|
+
* contribute a page, an Agent settings tab or a Session tab as manifest data. The web
|
|
2567
|
+
* app reads them back through GET /api/contributions and renders the ones whose
|
|
2568
|
+
* renderer it knows: a `builtin` name from its own registry, or an `iframe`. No code
|
|
2569
|
+
* crosses this boundary — only data.
|
|
2570
|
+
*/
|
|
2571
|
+
declare abstract class WebShell extends WebShell_base {
|
|
2572
|
+
}
|
|
2573
|
+
interface WebShellSlots {
|
|
2574
|
+
/** A page: its route, whether it sits in the main nav, whether it is admin-only. */
|
|
2575
|
+
pages: {
|
|
2576
|
+
key: string;
|
|
2577
|
+
path: string;
|
|
2578
|
+
nav: "main" | "none";
|
|
2579
|
+
admin: boolean;
|
|
2580
|
+
renderer: RendererRef;
|
|
2581
|
+
};
|
|
2582
|
+
/** A tab on the Agent settings page. */
|
|
2583
|
+
agentTabs: {
|
|
2584
|
+
key: string;
|
|
2585
|
+
order: number;
|
|
2586
|
+
renderer: RendererRef;
|
|
2587
|
+
};
|
|
2588
|
+
/** A tab beside a Session's chat (a workflow UI lives here). */
|
|
2589
|
+
sessionTabs: {
|
|
2590
|
+
key: string;
|
|
2591
|
+
renderer: RendererRef;
|
|
2592
|
+
};
|
|
2593
|
+
}
|
|
2594
|
+
|
|
2595
|
+
export { AgentConfigService, AgentService, Http, type HttpSlots, Messaging, type MessagingSlots, Sandbox, type SandboxSlots, SessionServiceIface, Sessions, Terminals, WebShell, type WebShellSlots };
|