@addozhang/dsh-discord 0.4.0 → 0.5.0-alpha.2

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.
@@ -0,0 +1,279 @@
1
+ /**
2
+ * The 0.1.6 host event bridge: fans the per-session `session/follow`
3
+ * journals plus the host-wide `session/control` stream into the single
4
+ * frame stream the live renderer consumes (the rc.2 `apiProxy.events.mux`
5
+ * global stream no longer exists).
6
+ *
7
+ * Frame vocabulary is preserved verbatim (`session/event`,
8
+ * `session/subscribed`, `session/queue`), so `src/stream/live.ts` and its
9
+ * tests are untouched by the host-side rebase. Two deliberate exclusions:
10
+ *
11
+ * - `snapshot` opening windows ARE translated through the same seq
12
+ * watermark as live events: the turn often completes between prompt
13
+ * admission and our follow subscription, so the opening window is the
14
+ * ONLY carrier of those records. The watermark (last delivered seq per
15
+ * session) keeps re-subscription replays idempotent.
16
+ * - approval/question request frames are NOT synthesized here: the 0.1.6
17
+ * host routes asks through the composed-approval model, which the
18
+ * ask-wiring migration wires separately.
19
+ */
20
+ /** Build the push-side of the single consumer queue. */
21
+ function createFrameQueue() {
22
+ const buffered = [];
23
+ let notify;
24
+ let closed = false;
25
+ const queue = {
26
+ push(frame) {
27
+ if (closed)
28
+ return;
29
+ buffered.push(frame);
30
+ const wake = notify;
31
+ notify = undefined;
32
+ wake?.();
33
+ },
34
+ close() {
35
+ closed = true;
36
+ const wake = notify;
37
+ notify = undefined;
38
+ wake?.();
39
+ },
40
+ };
41
+ return {
42
+ ...queue,
43
+ iterate(signal) {
44
+ return {
45
+ async *[Symbol.asyncIterator]() {
46
+ try {
47
+ for (;;) {
48
+ if (signal.aborted)
49
+ return;
50
+ while (buffered.length > 0) {
51
+ // shift() cannot miss: the length guard ran first.
52
+ yield buffered.shift();
53
+ }
54
+ if (closed)
55
+ return;
56
+ await new Promise(resolve => {
57
+ notify = resolve;
58
+ const abort = () => {
59
+ notify = undefined;
60
+ resolve();
61
+ };
62
+ signal.addEventListener('abort', abort, { once: true });
63
+ });
64
+ }
65
+ }
66
+ finally {
67
+ queue.close();
68
+ }
69
+ },
70
+ };
71
+ },
72
+ };
73
+ }
74
+ /** Defensive record probe: the wire is untrusted regardless of declared types. */
75
+ function isRecord(value) {
76
+ return typeof value === 'object' && value !== null;
77
+ }
78
+ /** Defensive text extraction from a queued message's JSON content parts. */
79
+ function queueItemSummary(content) {
80
+ if (!Array.isArray(content))
81
+ return '';
82
+ const texts = [];
83
+ for (const part of content) {
84
+ if (typeof part !== 'object' || part === null)
85
+ continue;
86
+ const typed = part;
87
+ if (typed.type === 'text' && typeof typed.text === 'string')
88
+ texts.push(typed.text);
89
+ }
90
+ return texts.join(' ').slice(0, 200);
91
+ }
92
+ /**
93
+ * The fan-in router. `track(sessionId)` is idempotent and safe to call from
94
+ * every session-acquisition site (create, adopt, resume, prompt); each
95
+ * tracked session owns one follow loop whose frames join the shared queue.
96
+ */
97
+ export function createHostEventRouter(services, options = {}) {
98
+ const log = options.log;
99
+ const tracked = new Map();
100
+ /** Last delivered durable seq per session: the replay-dedupe watermark. */
101
+ const watermark = new Map();
102
+ let consumer;
103
+ let rootSignal;
104
+ const startLoop = (sessionId, attempt = 0) => {
105
+ const per = new AbortController();
106
+ tracked.set(sessionId, per);
107
+ void (async () => {
108
+ if (process.env['DSH_DISCORD_TRACE'] === '1')
109
+ console.error(`[dsh-discord:trace] follow-start session=${sessionId.slice(0, 8)} attempt=${String(attempt)}`);
110
+ try {
111
+ consumer?.push({ type: 'session/subscribed', sessionId });
112
+ // The follow request MUST ask for assistant streaming: without the
113
+ // flag the host (alpha.2) delivers only the opening snapshot and
114
+ // never pushes later journal records — the live tail stays dead
115
+ // (diagnosis.md §D, run 2 vs run 3).
116
+ for await (const raw of services.follow({ address: { kind: 'session', sessionId }, assistantStream: true }, per.signal)) {
117
+ if (process.env['DSH_DISCORD_TRACE'] === '1') {
118
+ const dumped = JSON.stringify(raw);
119
+ console.error(`[dsh-discord:trace] raw-follow-frame type=${String(isRecord(raw) ? raw['type'] : typeof raw)} frame=${dumped.length > 300 ? `${dumped.slice(0, 300)}…` : dumped}`);
120
+ }
121
+ if (!isRecord(raw))
122
+ continue;
123
+ const frame = raw;
124
+ // Both the opening snapshot and live journal entries carry the
125
+ // same {type, data, seq} record envelope the rc.2 mux delivered;
126
+ // the seq watermark keeps replayed windows idempotent.
127
+ if (frame['type'] === 'event' || frame['type'] === 'snapshot') {
128
+ // Two carriers, both real-machine verified (alpha.2): snapshot
129
+ // windows batch their records under `records`, while live records
130
+ // arrive as single-record frames — the record rides the `event`
131
+ // key of the frame itself, no array. Snapshots without a records
132
+ // array stay drops (malformed), live frames become their own
133
+ // one-element batch.
134
+ const records = raw.records;
135
+ const batch = Array.isArray(records)
136
+ ? records
137
+ : frame['type'] === 'event'
138
+ ? [raw]
139
+ : undefined;
140
+ if (batch === undefined)
141
+ continue;
142
+ const through = watermark.get(sessionId) ?? 0;
143
+ let delivered = through;
144
+ for (const record of batch) {
145
+ // Journal records arrive double-wrapped: {type:'event',
146
+ // event:{type, seq, time, data}} — the wire event rides the
147
+ // `event` key. Accept the flat shape defensively too.
148
+ const candidate = record;
149
+ if (candidate === null || typeof candidate !== 'object')
150
+ continue;
151
+ const inner = typeof candidate.event === 'object' && candidate.event !== null
152
+ ? candidate.event
153
+ : candidate;
154
+ if (typeof inner.type !== 'string')
155
+ continue;
156
+ if (typeof inner.seq === 'number') {
157
+ if (inner.seq <= through)
158
+ continue;
159
+ if (inner.seq > delivered)
160
+ delivered = inner.seq;
161
+ }
162
+ if (process.env['DSH_DISCORD_TRACE'] === '1') {
163
+ // Full data dump (truncated): diagnosis needs the field
164
+ // shapes, not just the key names — keys hide nesting.
165
+ const dumped = JSON.stringify(inner.data);
166
+ console.error(`[dsh-discord:trace] record type=${inner.type} seq=${String(inner.seq)} data=${dumped.length > 800 ? `${dumped.slice(0, 800)}…` : dumped}`);
167
+ }
168
+ consumer?.push({
169
+ type: 'session/event',
170
+ sessionId,
171
+ event: { type: inner.type, data: (typeof inner.data === 'object' && inner.data !== null ? inner.data : {}) },
172
+ });
173
+ }
174
+ if (delivered > through)
175
+ watermark.set(sessionId, delivered);
176
+ }
177
+ else if (frame['type'] === 'assistant-stream') {
178
+ // Live assistant deltas ride the dedicated stream frames; the
179
+ // renderer's durable events already carry the message texts.
180
+ if (process.env['DSH_DISCORD_TRACE'] === '1') {
181
+ const dumped = JSON.stringify(frame);
182
+ console.error(`[dsh-discord:trace] assistant-stream frame=${dumped.length > 300 ? `${dumped.slice(0, 300)}…` : dumped}`);
183
+ }
184
+ continue;
185
+ }
186
+ else if (process.env['DSH_DISCORD_TRACE'] === '1') {
187
+ const dumped = JSON.stringify(frame);
188
+ console.error(`[dsh-discord:trace] follow-frame UNHANDLED type=${String(frame['type'])} frame=${dumped.length > 400 ? `${dumped.slice(0, 400)}…` : dumped}`);
189
+ }
190
+ }
191
+ }
192
+ catch (cause) {
193
+ if (!per.signal.aborted) {
194
+ if (process.env['DSH_DISCORD_TRACE'] === '1')
195
+ console.error(`[dsh-discord:trace] follow-threw session=${sessionId.slice(0, 8)} cause=${String(cause).slice(0, 300)}`);
196
+ log?.('discord_host_follow_threw', { sessionId, cause: String(cause) });
197
+ }
198
+ }
199
+ finally {
200
+ if (process.env['DSH_DISCORD_TRACE'] === '1')
201
+ console.error(`[dsh-discord:trace] follow-end session=${sessionId.slice(0, 8)} aborted=${String(per.signal.aborted)} tracked=${String(tracked.get(sessionId) === per)}`);
202
+ if (tracked.get(sessionId) === per)
203
+ tracked.delete(sessionId);
204
+ // A follow stream may END normally once its snapshot is delivered
205
+ // (a cold session has no live agent to follow): without a
206
+ // re-subscribe, every later turn is invisible until the next
207
+ // process restart replays the journal. Re-arm with backoff.
208
+ if (!per.signal.aborted) {
209
+ const delayMs = Math.min(30_000, 1_000 * 2 ** attempt);
210
+ setTimeout(() => {
211
+ if (!per.signal.aborted && !tracked.has(sessionId)) {
212
+ log?.('discord_host_follow_resubscribed', { sessionId, delayMs });
213
+ startLoop(sessionId, attempt + 1);
214
+ }
215
+ }, delayMs);
216
+ }
217
+ }
218
+ })();
219
+ };
220
+ const startControlLoop = (signal) => {
221
+ void (async () => {
222
+ try {
223
+ for await (const raw of services.control(signal)) {
224
+ if (!isRecord(raw))
225
+ continue;
226
+ const frame = raw;
227
+ if (process.env['DSH_DISCORD_TRACE'] === '1') {
228
+ const dumped = JSON.stringify(frame);
229
+ console.error(`[dsh-discord:trace] control type=${String(frame['type'])} frame=${dumped.length > 400 ? `${dumped.slice(0, 400)}…` : dumped}`);
230
+ }
231
+ if (frame['type'] === 'queue' && typeof frame['sessionId'] === 'string') {
232
+ const rawItems = Array.isArray(frame['items']) ? frame['items'] : [];
233
+ const items = rawItems
234
+ .filter((item) => isRecord(item) && typeof item['id'] === 'string')
235
+ .map(item => ({
236
+ id: item['id'],
237
+ summary: queueItemSummary(isRecord(item['message']) ? item['message']['content'] : undefined),
238
+ }));
239
+ consumer?.push({
240
+ type: 'session/queue',
241
+ sessionId: frame['sessionId'],
242
+ items,
243
+ });
244
+ }
245
+ // baseline / jobs / projection frames carry no live-render state.
246
+ }
247
+ }
248
+ catch (cause) {
249
+ if (!signal.aborted) {
250
+ log?.('discord_host_control_threw', { cause: String(cause) });
251
+ }
252
+ }
253
+ })();
254
+ };
255
+ return {
256
+ track(sessionId) {
257
+ if (sessionId === '' || tracked.has(sessionId))
258
+ return;
259
+ if (rootSignal?.aborted)
260
+ return;
261
+ startLoop(sessionId);
262
+ },
263
+ stream(signal) {
264
+ if (process.env['DSH_DISCORD_TRACE'] === '1')
265
+ console.error(`[dsh-discord:trace] router-stream-open tracked=${String(tracked.size)} rearm=${String(tracked.size)}`);
266
+ rootSignal = signal;
267
+ const queue = createFrameQueue();
268
+ consumer = queue;
269
+ // Re-arm every already-tracked session against the new consumer.
270
+ for (const [sessionId, per] of [...tracked.entries()]) {
271
+ per.abort();
272
+ tracked.delete(sessionId);
273
+ startLoop(sessionId);
274
+ }
275
+ startControlLoop(signal);
276
+ return queue.iterate(signal);
277
+ },
278
+ };
279
+ }
@@ -0,0 +1,418 @@
1
+ /**
2
+ * The typed防腐层 over the Host's 0.1.6 controller services — the cordis
3
+ * services `sessionController`, `workspaceController`, and `sessionQuery`
4
+ * that replaced the 0.1.1-rc.2 `apiProxy` gateway. Domain methods speak the
5
+ * typert faces directly: plain request objects in, plain values out, and
6
+ * business rejections carried as thrown `RemoteError`s (`{code, message}`).
7
+ * Two guarantees are preserved from the rc.2 seam:
8
+ *
9
+ * 1. Boundedness — a Host that never answers must not wedge an interaction
10
+ * handler (or a Discord ephemeral) forever; every call races a timeout and
11
+ * resolves to an unobservable outcome instead.
12
+ * 2. Observability — every terminal outcome is reported through the injected
13
+ * log sink, so a silent-void call can never again be misread as a hang.
14
+ *
15
+ * The exported port-level API (function names, outcome unions) is unchanged
16
+ * from the rc.2 face: the Discord features and their tests keep their
17
+ * vocabulary while this module alone absorbs the host-side rebase.
18
+ */
19
+ import type { ProjectListPort } from '../features/project-list.js';
20
+ import type { WorkspaceResolver } from '../features/project-bind.js';
21
+ import type { DshModelPort } from '../features/model-control.js';
22
+ /** The workspace rows the catalog port needs (subset of WorkspaceView). */
23
+ export interface WorkspaceCatalogEntry {
24
+ workspaceId: string;
25
+ title: string;
26
+ /** Canonical directory; present in Host responses, rendered only to proven administrators. */
27
+ path?: string | undefined;
28
+ }
29
+ /**
30
+ * Narrow slice of the 0.1.6 `sessionController` cordis service. Method
31
+ * shapes mirror the typert descriptors in @deepseek-ai/dsh-api-session-controller.
32
+ */
33
+ export interface DshSessionControllerFace {
34
+ prompt(request: {
35
+ requestId: string;
36
+ sessionId: string;
37
+ mode: 'queue' | 'steer';
38
+ content: Array<{
39
+ type: 'text';
40
+ text: string;
41
+ } | {
42
+ type: 'image';
43
+ mediaType: string;
44
+ data: string;
45
+ }>;
46
+ clientTimeZone?: string;
47
+ }, signal: AbortSignal): Promise<{
48
+ accepted: true;
49
+ }>;
50
+ create(request: {
51
+ workspaceId?: string;
52
+ cwd?: string;
53
+ sessionId?: string;
54
+ agentPreset?: string;
55
+ }): Promise<{
56
+ sessionId: string;
57
+ agentPreset?: string;
58
+ }>;
59
+ /** The durable Session list; the host takes ONLY an optional cancellation signal. */
60
+ list(signal: AbortSignal | undefined): Promise<{
61
+ items: unknown[];
62
+ }>;
63
+ cancel(request: {
64
+ sessionId: string;
65
+ }): Promise<{
66
+ accepted: true;
67
+ }>;
68
+ updateQueue(request: {
69
+ sessionId: string;
70
+ itemId: string;
71
+ action: {
72
+ kind: 'remove';
73
+ };
74
+ }): Promise<{
75
+ accepted: true;
76
+ }>;
77
+ selectModel(request: {
78
+ sessionId: string;
79
+ provider: string;
80
+ model: string;
81
+ reasoningEffort?: string;
82
+ }): Promise<{
83
+ selected: ModelSelectionShape;
84
+ }>;
85
+ modelCatalog(): Promise<ModelCatalogWireShape>;
86
+ /** Per-session durable journal stream (live frames after one opening snapshot). */
87
+ follow(request: {
88
+ address: {
89
+ kind: 'session';
90
+ sessionId: string;
91
+ };
92
+ assistantStream?: true;
93
+ }, signal: AbortSignal): AsyncIterable<unknown>;
94
+ /** Host-wide live state stream (queue/jobs/projection frames over one baseline). */
95
+ control(signal: AbortSignal): AsyncIterable<unknown>;
96
+ }
97
+ /**
98
+ * Narrow slice of the 0.1.6 `workspaceController` cordis service. The
99
+ * registry baseline (`baseline()`) is the synchronous successor of the rc.2
100
+ * unary `workspace.list` RPC.
101
+ */
102
+ export interface DshWorkspaceControllerFace {
103
+ /** Workspace state stream; every generation starts with exactly one baseline frame. */
104
+ follow(signal: AbortSignal): AsyncIterable<unknown>;
105
+ }
106
+ /**
107
+ * Narrow slice of `sessionQuery` — the per-session projection read the /model
108
+ * surface needs for the session's live selection (the rc.2 `sessions.models`
109
+ * RPC split into the global `modelCatalog` plus this projection).
110
+ */
111
+ export interface DshSessionQueryFace {
112
+ observeSession(sessionId: string): Promise<{
113
+ header?: {
114
+ cwd?: string;
115
+ };
116
+ projections?: {
117
+ values?: Record<string, unknown>;
118
+ };
119
+ } & Partial<AsyncDisposable>>;
120
+ }
121
+ /** The host-generation model catalog (`session/modelCatalog`, host-wide). */
122
+ export interface ModelCatalogWireShape {
123
+ default: ModelSelectionShape;
124
+ routableProviders: string[];
125
+ groups: ModelProviderGroupShape[];
126
+ failures: Array<{
127
+ id: string;
128
+ name: string;
129
+ message: string;
130
+ }>;
131
+ }
132
+ /** The composite host face every port factory in this module consumes. */
133
+ export interface DshHostFace {
134
+ session: DshSessionControllerFace;
135
+ workspace: DshWorkspaceControllerFace;
136
+ sessionQuery: DshSessionQueryFace;
137
+ }
138
+ /**
139
+ * Resolve the 0.1.6 controller services off the Cordis context. Throws one
140
+ * actionable TypeError naming every absent service — the composition root
141
+ * treats that as a fail-loud startup boundary, never a silent half-mount.
142
+ */
143
+ export declare function resolveHostFace(ctx: {
144
+ get(name: string): unknown;
145
+ }): DshHostFace;
146
+ /** Defensive read of the title projection in a list row's values. */
147
+ export interface SessionProjectionsShape {
148
+ values?: {
149
+ title?: unknown;
150
+ };
151
+ }
152
+ /** The per-session summary `session.list` returns (rich rows, untrusted wire). */
153
+ export interface SessionSummaryShape {
154
+ sessionId: string;
155
+ updatedAt: number;
156
+ running: boolean;
157
+ blank: boolean;
158
+ cwd?: string;
159
+ agentPreset?: string;
160
+ origin?: 'subagent';
161
+ projections?: SessionProjectionsShape;
162
+ }
163
+ /** The complete provider/model/reasoning selection (dsh-agent ModelSelection). */
164
+ export interface ModelSelectionShape {
165
+ provider: string;
166
+ model: string;
167
+ reasoningEffort?: string;
168
+ }
169
+ /** One reasoning effort a model's adapter advertises (sessions.d.ts). */
170
+ export interface ModelReasoningEffortShape {
171
+ id: string;
172
+ name: string;
173
+ description?: string;
174
+ }
175
+ /** Exact-route reasoning metadata for one catalog model. */
176
+ export interface ModelReasoningShape {
177
+ efforts: ModelReasoningEffortShape[];
178
+ defaultEffort?: string;
179
+ }
180
+ /** One model inside a provider group (sessions.d.ts ModelCatalogModel). */
181
+ export interface ModelCatalogModelShape {
182
+ id: string;
183
+ name: string;
184
+ description?: string;
185
+ reasoning?: ModelReasoningShape;
186
+ }
187
+ /** One provider group and its models (sessions.d.ts ModelProviderGroup). */
188
+ export interface ModelProviderGroupShape {
189
+ id: string;
190
+ name: string;
191
+ models: ModelCatalogModelShape[];
192
+ }
193
+ /**
194
+ * The detached model directory the /model cascade browses (port shape,
195
+ * unchanged from rc.2): the session's live selection, whether its route
196
+ * still serves, and the per-provider catalog groups.
197
+ */
198
+ export interface SessionModelsShape {
199
+ current: ModelSelectionShape;
200
+ routable: boolean;
201
+ groups: ModelProviderGroupShape[];
202
+ failures: Array<{
203
+ id: string;
204
+ name: string;
205
+ message: string;
206
+ }>;
207
+ }
208
+ /** Raised when the Host did not answer within the bounded window. */
209
+ export declare class RpcTimeoutError extends Error {
210
+ constructor(timeoutMs: number);
211
+ }
212
+ /** Race one host promise against a bounded window. */
213
+ export declare function withRpcTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T>;
214
+ /** Diagnostic sink shared by every face function. */
215
+ export type ApiProxyLog = (event: string, detail?: unknown) => void;
216
+ export interface ApiProxyFaceOptions {
217
+ timeoutMs?: number;
218
+ log?: ApiProxyLog;
219
+ }
220
+ /**
221
+ * The bind flow's catalog verifier: resolves an opaque `ws:` reference
222
+ * against the live workspace baseline. A well-formed reference the registry
223
+ * no longer knows — and any malformed one — resolve `stale` (fail-closed, no
224
+ * write can follow); a Host error is `failed`; a timeout is `unknown`.
225
+ */
226
+ export declare function createWorkspaceResolver(dsh: DshHostFace, options?: ApiProxyFaceOptions): WorkspaceResolver;
227
+ /**
228
+ * The `/project list` catalog port over the workspace registry baseline.
229
+ * Outcomes follow the port contract: a definitive Host error is `failed`
230
+ * (sanitized before Discord), while a timeout or unreadable body is
231
+ * `unknown` — delivery was not observed, so no retry is implied.
232
+ */
233
+ export declare function createWorkspaceCatalogPort(dsh: DshHostFace, options?: ApiProxyFaceOptions): ProjectListPort;
234
+ export type WorkspaceDetailOutcome = {
235
+ outcome: 'found';
236
+ workspace: {
237
+ id: string;
238
+ title: string;
239
+ path: string | undefined;
240
+ };
241
+ } | {
242
+ outcome: 'stale';
243
+ } | {
244
+ outcome: 'failed';
245
+ } | {
246
+ outcome: 'unknown';
247
+ };
248
+ /**
249
+ * Read one Workspace's full view (title plus canonical path). The path is
250
+ * for the administrator-only ephemeral info response — the disclosure
251
+ * policy owns whether it ever renders; this face only carries it in memory.
252
+ */
253
+ export declare function readWorkspaceDetail(dsh: DshHostFace, reference: string, options?: ApiProxyFaceOptions): Promise<WorkspaceDetailOutcome>;
254
+ export type PromptOutcome = {
255
+ outcome: 'accepted';
256
+ } | {
257
+ outcome: 'rejected';
258
+ reason: string;
259
+ } | {
260
+ outcome: 'unknown';
261
+ };
262
+ /**
263
+ * Submit one prompt turn through the session controller. A definitive Host
264
+ * rejection (thrown RemoteError) is a rejection carrying the sanitized
265
+ * reason; a timeout or host fault is `unknown` — the turn may or may not
266
+ * have been admitted, so callers must not resubmit. `options.rpcId` pins
267
+ * the adapter-owned stable request id, which the Host records on the
268
+ * durable user message (`source.rpcId`) and de-duplicates on — the 0.1.6
269
+ * admission layer replays `{accepted: true}` for a repeated id, keeping the
270
+ * at-most-once discipline observable. Images (16.50) encode as ordered
271
+ * `image` parts after the text part.
272
+ */
273
+ export declare function promptSession(dsh: DshHostFace, request: {
274
+ sessionId: string;
275
+ prompt: string;
276
+ images?: ReadonlyArray<{
277
+ mediaType: string;
278
+ base64: string;
279
+ }>;
280
+ }, options?: ApiProxyFaceOptions & {
281
+ rpcId?: string;
282
+ }): Promise<PromptOutcome>;
283
+ /**
284
+ * Steer the session's active turn: `session.prompt` with `mode: 'steer'`,
285
+ * carrying the same stable request-id discipline as the queue path.
286
+ */
287
+ export declare function steerSession(dsh: DshHostFace, request: {
288
+ sessionId: string;
289
+ prompt: string;
290
+ }, options?: ApiProxyFaceOptions & {
291
+ rpcId?: string;
292
+ }): Promise<PromptOutcome>;
293
+ export type CreateSessionOutcome = {
294
+ outcome: 'completed';
295
+ sessionId: string;
296
+ } | {
297
+ outcome: 'rejected';
298
+ reason: string;
299
+ } | {
300
+ outcome: 'unknown';
301
+ };
302
+ /**
303
+ * Create one DSH Session against a preallocated id (design.md §10): the 0.1.6
304
+ * controller adopts the same session id idempotently, so an uncertain
305
+ * response never forks a second Session. Same outcome discipline as the
306
+ * prompt path.
307
+ */
308
+ export declare function createSessionViaProxy(dsh: DshHostFace, request: {
309
+ sessionId: string;
310
+ workspaceId: string;
311
+ }, options?: ApiProxyFaceOptions): Promise<CreateSessionOutcome>;
312
+ /** The durable Session-id baseline reconciliation reconciles against. */
313
+ export type SessionIdListOutcome = {
314
+ outcome: 'completed';
315
+ ids: string[];
316
+ } | {
317
+ outcome: 'failed';
318
+ } | {
319
+ outcome: 'unknown';
320
+ };
321
+ /** List durable Session ids (`session.list` returns everything in one page). */
322
+ export declare function listSessionIds(dsh: DshHostFace, options?: ApiProxyFaceOptions): Promise<SessionIdListOutcome>;
323
+ /** A list row narrowed to what the /session resume surface renders. */
324
+ export interface SessionResumeRow {
325
+ sessionId: string;
326
+ title: string | undefined;
327
+ updatedAt: number;
328
+ running: boolean;
329
+ blank: boolean;
330
+ cwd: string | undefined;
331
+ origin: 'subagent' | undefined;
332
+ }
333
+ export type SessionSummariesOutcome = {
334
+ outcome: 'completed';
335
+ sessions: SessionResumeRow[];
336
+ } | {
337
+ outcome: 'failed';
338
+ } | {
339
+ outcome: 'unknown';
340
+ };
341
+ /**
342
+ * The rich `session.list` for the /session resume surface: titles ride each
343
+ * row's projection values (absence = the session has no title yet), blank
344
+ * sessions are flagged, and rows arrive updatedAt-descending. Defensive
345
+ * narrowing: the wire is untrusted, extra/missing fields never throw.
346
+ */
347
+ export declare function listSessionSummaries(dsh: DshHostFace, options?: ApiProxyFaceOptions): Promise<SessionSummariesOutcome>;
348
+ export type CancelOutcome = {
349
+ outcome: 'accepted';
350
+ } | {
351
+ outcome: 'rejected';
352
+ reason: string;
353
+ } | {
354
+ outcome: 'unknown';
355
+ };
356
+ /** Cancel the session's active turn (`session.cancel`); DSH preserves the pending inbox. */
357
+ export declare function cancelSessionViaProxy(dsh: DshHostFace, request: {
358
+ sessionId: string;
359
+ }, options?: ApiProxyFaceOptions): Promise<CancelOutcome>;
360
+ export type QueueRemoveOutcome = {
361
+ outcome: 'accepted';
362
+ } | {
363
+ outcome: 'rejected';
364
+ reason: string;
365
+ } | {
366
+ outcome: 'unknown';
367
+ };
368
+ /** Remove one pending inbox item (`session.updateQueue`, action remove). */
369
+ export declare function removeQueueItemViaProxy(dsh: DshHostFace, request: {
370
+ sessionId: string;
371
+ itemId: string;
372
+ }, options?: ApiProxyFaceOptions): Promise<QueueRemoveOutcome>;
373
+ export type SessionModelsOutcome = {
374
+ outcome: 'completed';
375
+ models: SessionModelsShape;
376
+ } | {
377
+ outcome: 'failed';
378
+ } | {
379
+ outcome: 'unknown';
380
+ };
381
+ /**
382
+ * The session's detached model directory: the live selection, whether its
383
+ * route still serves, and the per-provider catalog groups the /model
384
+ * cascade browses. Composed from the 0.1.6 global `modelCatalog` plus the
385
+ * session's `modelSelection` projection — the rc.2 per-session
386
+ * `sessions.models` RPC no longer exists.
387
+ */
388
+ export declare function sessionModels(dsh: DshHostFace, request: {
389
+ sessionId: string;
390
+ }, options?: ApiProxyFaceOptions): Promise<SessionModelsOutcome>;
391
+ export type SelectModelOutcome = {
392
+ outcome: 'completed';
393
+ selected: ModelSelectionShape;
394
+ } | {
395
+ outcome: 'rejected';
396
+ reason: string;
397
+ } | {
398
+ outcome: 'unknown';
399
+ };
400
+ /**
401
+ * Select the complete model selection for one session (session.selectModel):
402
+ * the session switches immediately and the Host records the choice as the
403
+ * default for sessions that have not logged their own — the response only
404
+ * proves the session switch, so callers must not claim the persistence
405
+ * outcome (design.md §7).
406
+ */
407
+ export declare function selectSessionModel(dsh: DshHostFace, request: {
408
+ sessionId: string;
409
+ provider: string;
410
+ model: string;
411
+ reasoningEffort?: string;
412
+ }, options?: ApiProxyFaceOptions): Promise<SelectModelOutcome>;
413
+ /**
414
+ * The /model surface over the session controller: the composed per-session
415
+ * directory and the guarded selection mutation — the shapes model-control
416
+ * reasons about.
417
+ */
418
+ export declare function createModelPort(dsh: DshHostFace, options?: ApiProxyFaceOptions): DshModelPort;