@addozhang/dsh-discord 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +68 -0
- package/cordis.patch.yml +14 -0
- package/lib/client/DiscordSettingsCard.d.ts +6 -0
- package/lib/client/PluginCard.d.ts +43 -0
- package/lib/client/card-controller.d.ts +22 -0
- package/lib/client/card-form.d.ts +111 -0
- package/lib/client/fields.d.ts +50 -0
- package/lib/client/index.d.ts +12 -0
- package/lib/client/nav-icon.d.ts +21 -0
- package/lib/client/settings-model.d.ts +49 -0
- package/lib/client/slot-contract.d.ts +57 -0
- package/lib/client/snapshot-store.d.ts +9 -0
- package/lib/client.js +1622 -0
- package/lib/compose.d.ts +158 -0
- package/lib/compose.js +227 -0
- package/lib/credential.d.ts +20 -0
- package/lib/credential.js +24 -0
- package/lib/discord/commands.d.ts +45 -0
- package/lib/discord/commands.js +87 -0
- package/lib/discord/components.d.ts +29 -0
- package/lib/discord/components.js +57 -0
- package/lib/discord/delivery.d.ts +21 -0
- package/lib/discord/delivery.js +31 -0
- package/lib/discord/interaction-lifecycle.d.ts +42 -0
- package/lib/discord/interaction-lifecycle.js +78 -0
- package/lib/discord/rest.d.ts +74 -0
- package/lib/discord/rest.js +149 -0
- package/lib/discord/selector.d.ts +39 -0
- package/lib/discord/selector.js +70 -0
- package/lib/discord/thread-port.d.ts +37 -0
- package/lib/discord/thread-port.js +58 -0
- package/lib/dsh/api-proxy-face.d.ts +253 -0
- package/lib/dsh/api-proxy-face.js +370 -0
- package/lib/features/adapter-status.d.ts +111 -0
- package/lib/features/adapter-status.js +103 -0
- package/lib/features/approval-expiry.d.ts +27 -0
- package/lib/features/approval-expiry.js +36 -0
- package/lib/features/approval-routing.d.ts +50 -0
- package/lib/features/approval-routing.js +57 -0
- package/lib/features/approval-store.d.ts +86 -0
- package/lib/features/approval-store.js +119 -0
- package/lib/features/approval-view.d.ts +47 -0
- package/lib/features/approval-view.js +48 -0
- package/lib/features/ask-wiring.d.ts +74 -0
- package/lib/features/ask-wiring.js +151 -0
- package/lib/features/host-status.d.ts +41 -0
- package/lib/features/host-status.js +24 -0
- package/lib/features/image-collection.d.ts +56 -0
- package/lib/features/image-collection.js +79 -0
- package/lib/features/image-download.d.ts +48 -0
- package/lib/features/image-download.js +77 -0
- package/lib/features/image-submission.d.ts +67 -0
- package/lib/features/image-submission.js +57 -0
- package/lib/features/interaction-router.d.ts +84 -0
- package/lib/features/interaction-router.js +655 -0
- package/lib/features/model-control.d.ts +79 -0
- package/lib/features/model-control.js +51 -0
- package/lib/features/preset-control.d.ts +84 -0
- package/lib/features/preset-control.js +43 -0
- package/lib/features/project-bind.d.ts +69 -0
- package/lib/features/project-bind.js +57 -0
- package/lib/features/project-info.d.ts +25 -0
- package/lib/features/project-info.js +22 -0
- package/lib/features/project-list.d.ts +79 -0
- package/lib/features/project-list.js +93 -0
- package/lib/features/prompt-submission.d.ts +53 -0
- package/lib/features/prompt-submission.js +47 -0
- package/lib/features/question-expiry.d.ts +53 -0
- package/lib/features/question-expiry.js +56 -0
- package/lib/features/question-routing.d.ts +108 -0
- package/lib/features/question-routing.js +162 -0
- package/lib/features/question-store.d.ts +124 -0
- package/lib/features/question-store.js +186 -0
- package/lib/features/question-view.d.ts +58 -0
- package/lib/features/question-view.js +81 -0
- package/lib/features/queue-control.d.ts +63 -0
- package/lib/features/queue-control.js +31 -0
- package/lib/features/reconcile-bindings.d.ts +75 -0
- package/lib/features/reconcile-bindings.js +54 -0
- package/lib/features/reconcile-deliveries.d.ts +29 -0
- package/lib/features/reconcile-deliveries.js +28 -0
- package/lib/features/reconcile-events.d.ts +49 -0
- package/lib/features/reconcile-events.js +50 -0
- package/lib/features/reconcile-intents.d.ts +32 -0
- package/lib/features/reconcile-intents.js +32 -0
- package/lib/features/reconcile-interactions.d.ts +59 -0
- package/lib/features/reconcile-interactions.js +54 -0
- package/lib/features/session-adopt.d.ts +89 -0
- package/lib/features/session-adopt.js +77 -0
- package/lib/features/session-creation.d.ts +49 -0
- package/lib/features/session-creation.js +43 -0
- package/lib/features/session-mainline.d.ts +75 -0
- package/lib/features/session-mainline.js +96 -0
- package/lib/features/session-resume.d.ts +44 -0
- package/lib/features/session-resume.js +40 -0
- package/lib/features/skill-run.d.ts +48 -0
- package/lib/features/skill-run.js +25 -0
- package/lib/features/steer-control.d.ts +37 -0
- package/lib/features/steer-control.js +28 -0
- package/lib/features/stop-control.d.ts +38 -0
- package/lib/features/stop-control.js +31 -0
- package/lib/features/task-admission.d.ts +29 -0
- package/lib/features/task-admission.js +21 -0
- package/lib/features/thread-continuation.d.ts +36 -0
- package/lib/features/thread-continuation.js +18 -0
- package/lib/features/thread-creation.d.ts +81 -0
- package/lib/features/thread-creation.js +81 -0
- package/lib/features/thread-routing.d.ts +49 -0
- package/lib/features/thread-routing.js +34 -0
- package/lib/features/turn-ownership.d.ts +39 -0
- package/lib/features/turn-ownership.js +40 -0
- package/lib/features/unbound-mention.d.ts +49 -0
- package/lib/features/unbound-mention.js +31 -0
- package/lib/features/workspace-channel.d.ts +41 -0
- package/lib/features/workspace-channel.js +47 -0
- package/lib/gateway/gateway.d.ts +50 -0
- package/lib/gateway/gateway.js +229 -0
- package/lib/gateway/inbound.d.ts +85 -0
- package/lib/gateway/inbound.js +186 -0
- package/lib/gateway/ingress.d.ts +15 -0
- package/lib/gateway/ingress.js +22 -0
- package/lib/i18n.d.ts +92 -0
- package/lib/i18n.js +180 -0
- package/lib/index.d.ts +17 -0
- package/lib/index.js +869 -0
- package/lib/lifecycle.d.ts +44 -0
- package/lib/lifecycle.js +84 -0
- package/lib/policy/authorization.d.ts +41 -0
- package/lib/policy/authorization.js +64 -0
- package/lib/policy/disclosure.d.ts +93 -0
- package/lib/policy/disclosure.js +153 -0
- package/lib/policy/guard.d.ts +31 -0
- package/lib/policy/guard.js +46 -0
- package/lib/policy/suppress.d.ts +13 -0
- package/lib/policy/suppress.js +20 -0
- package/lib/settings-namespace.d.ts +7 -0
- package/lib/settings-namespace.js +6 -0
- package/lib/settings.d.ts +37 -0
- package/lib/settings.js +102 -0
- package/lib/startup.d.ts +16 -0
- package/lib/startup.js +60 -0
- package/lib/state/bindings.d.ts +46 -0
- package/lib/state/bindings.js +55 -0
- package/lib/state/channel-bindings.d.ts +37 -0
- package/lib/state/channel-bindings.js +43 -0
- package/lib/state/domain.d.ts +82 -0
- package/lib/state/domain.js +86 -0
- package/lib/state/effect-machine.d.ts +37 -0
- package/lib/state/effect-machine.js +52 -0
- package/lib/state/fail-closed.d.ts +44 -0
- package/lib/state/fail-closed.js +57 -0
- package/lib/state/intents.d.ts +51 -0
- package/lib/state/intents.js +73 -0
- package/lib/state/records.d.ts +37 -0
- package/lib/state/records.js +30 -0
- package/lib/state/retention.d.ts +74 -0
- package/lib/state/retention.js +68 -0
- package/lib/state/session-owners.d.ts +50 -0
- package/lib/state/session-owners.js +50 -0
- package/lib/stream/finalizer.d.ts +49 -0
- package/lib/stream/finalizer.js +67 -0
- package/lib/stream/icons.d.ts +15 -0
- package/lib/stream/icons.js +36 -0
- package/lib/stream/live.d.ts +140 -0
- package/lib/stream/live.js +515 -0
- package/lib/stream/markdown.d.ts +24 -0
- package/lib/stream/markdown.js +95 -0
- package/lib/stream/outbound.d.ts +27 -0
- package/lib/stream/outbound.js +25 -0
- package/lib/stream/render-fence.d.ts +27 -0
- package/lib/stream/render-fence.js +37 -0
- package/lib/stream/render-model.d.ts +48 -0
- package/lib/stream/render-model.js +68 -0
- package/lib/stream/splitter.d.ts +8 -0
- package/lib/stream/splitter.js +58 -0
- package/lib/stream/tool-view.d.ts +40 -0
- package/lib/stream/tool-view.js +39 -0
- package/lib/stream/typing.d.ts +22 -0
- package/lib/stream/typing.js +58 -0
- package/lib/stream/update-scheduler.d.ts +20 -0
- package/lib/stream/update-scheduler.js +65 -0
- package/package.json +86 -0
|
@@ -0,0 +1,253 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The typed face over the Host's in-process `ctx.apiProxy` service
|
|
3
|
+
* (ApiProxyService, the transport-agnostic gateway's direct implementation).
|
|
4
|
+
* Its domain methods speak the narrow RPC signature: `RpcRequest<P>` in,
|
|
5
|
+
* `RpcResponse<T>` out, business errors on `result.ok === false` — they never
|
|
6
|
+
* throw for business outcomes. Two guarantees are added here:
|
|
7
|
+
*
|
|
8
|
+
* 1. Boundedness — a Host that never answers must not wedge an interaction
|
|
9
|
+
* handler (or a Discord ephemeral) forever; every call races a timeout and
|
|
10
|
+
* resolves to an unobservable outcome instead.
|
|
11
|
+
* 2. Observability — every terminal outcome is reported through the injected
|
|
12
|
+
* log sink, so a silent-void call can never again be misread as a hang.
|
|
13
|
+
*/
|
|
14
|
+
import type { ProjectListPort } from '../features/project-list.js';
|
|
15
|
+
import type { WorkspaceResolver } from '../features/project-bind.js';
|
|
16
|
+
/** The workspace rows the catalog port needs (subset of WorkspaceView). */
|
|
17
|
+
export interface WorkspaceCatalogEntry {
|
|
18
|
+
workspaceId: string;
|
|
19
|
+
title: string;
|
|
20
|
+
/** Canonical directory; present in Host responses, rendered only to proven administrators. */
|
|
21
|
+
path?: string | undefined;
|
|
22
|
+
}
|
|
23
|
+
/** The narrow slice of ApiProxy this module speaks. */
|
|
24
|
+
export interface DshApiProxyFace {
|
|
25
|
+
workspace: {
|
|
26
|
+
list(request: RpcRequestShape<Record<string, never>>): Promise<RpcResponseShape<{
|
|
27
|
+
items: WorkspaceCatalogEntry[];
|
|
28
|
+
}>>;
|
|
29
|
+
};
|
|
30
|
+
sessions: {
|
|
31
|
+
prompt(request: RpcRequestShape<{
|
|
32
|
+
sessionId: string;
|
|
33
|
+
mode: 'queue' | 'steer';
|
|
34
|
+
content: Array<{
|
|
35
|
+
type: 'text';
|
|
36
|
+
text: string;
|
|
37
|
+
}>;
|
|
38
|
+
}>): Promise<RpcResponseShape<{
|
|
39
|
+
accepted: true;
|
|
40
|
+
}>>;
|
|
41
|
+
create(request: RpcRequestShape<{
|
|
42
|
+
workspaceId?: string;
|
|
43
|
+
sessionId?: string;
|
|
44
|
+
agentPreset?: string;
|
|
45
|
+
}>): Promise<RpcResponseShape<{
|
|
46
|
+
sessionId: string;
|
|
47
|
+
}>>;
|
|
48
|
+
cancel(request: RpcRequestShape<{
|
|
49
|
+
sessionId: string;
|
|
50
|
+
}>): Promise<RpcResponseShape<{
|
|
51
|
+
accepted: true;
|
|
52
|
+
}>>;
|
|
53
|
+
updateQueue(request: RpcRequestShape<{
|
|
54
|
+
sessionId: string;
|
|
55
|
+
itemId: string;
|
|
56
|
+
action: {
|
|
57
|
+
kind: 'remove';
|
|
58
|
+
};
|
|
59
|
+
}>): Promise<RpcResponseShape<{
|
|
60
|
+
accepted: true;
|
|
61
|
+
}>>;
|
|
62
|
+
list(request: RpcRequestShape<{
|
|
63
|
+
cursor?: string;
|
|
64
|
+
}>): Promise<RpcResponseShape<{
|
|
65
|
+
items: Array<{
|
|
66
|
+
sessionId: string;
|
|
67
|
+
}>;
|
|
68
|
+
}>>;
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/** Signature-layer narrow request form (RpcId brand erased at this seam). */
|
|
72
|
+
export interface RpcRequestShape<P> {
|
|
73
|
+
rpcId: string;
|
|
74
|
+
payload: P;
|
|
75
|
+
}
|
|
76
|
+
/** Signature-layer narrow response form with the business ok/error result. */
|
|
77
|
+
export interface RpcResponseShape<T> {
|
|
78
|
+
rpcId: string;
|
|
79
|
+
result: {
|
|
80
|
+
ok: true;
|
|
81
|
+
value: T;
|
|
82
|
+
} | {
|
|
83
|
+
ok: false;
|
|
84
|
+
error: {
|
|
85
|
+
code: string;
|
|
86
|
+
message: string;
|
|
87
|
+
};
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/** Raised when the Host did not answer within the bounded window. */
|
|
91
|
+
export declare class RpcTimeoutError extends Error {
|
|
92
|
+
constructor(timeoutMs: number);
|
|
93
|
+
}
|
|
94
|
+
/** Race one apiProxy promise against a bounded window. */
|
|
95
|
+
export declare function withRpcTimeout<T>(promise: Promise<T>, timeoutMs: number): Promise<T>;
|
|
96
|
+
/** Diagnostic sink shared by both faces. */
|
|
97
|
+
export type ApiProxyLog = (event: string, detail?: unknown) => void;
|
|
98
|
+
export interface ApiProxyFaceOptions {
|
|
99
|
+
timeoutMs?: number;
|
|
100
|
+
log?: ApiProxyLog;
|
|
101
|
+
}
|
|
102
|
+
/**
|
|
103
|
+
* The bind flow's catalog verifier: resolves an opaque `ws:` reference
|
|
104
|
+
* against the live workspace list. A well-formed reference the catalog no
|
|
105
|
+
* longer knows — and any malformed one — resolve `stale` (fail-closed, no
|
|
106
|
+
* write can follow); a Host error is `failed`; a timeout is `unknown`.
|
|
107
|
+
*/
|
|
108
|
+
export declare function createWorkspaceResolver(dsh: DshApiProxyFace, options?: ApiProxyFaceOptions): WorkspaceResolver;
|
|
109
|
+
/**
|
|
110
|
+
* The `/project list` catalog port satisfied by the in-process apiProxy.
|
|
111
|
+
* Outcomes follow the port contract: a definitive Host error is `failed`
|
|
112
|
+
* (sanitized before Discord), while a timeout or unreadable body is
|
|
113
|
+
* `unknown` — delivery was not observed, so no retry is implied.
|
|
114
|
+
*/
|
|
115
|
+
export declare function createWorkspaceCatalogPort(dsh: DshApiProxyFace, options?: ApiProxyFaceOptions): ProjectListPort;
|
|
116
|
+
export type WorkspaceDetailOutcome = {
|
|
117
|
+
outcome: 'found';
|
|
118
|
+
workspace: {
|
|
119
|
+
id: string;
|
|
120
|
+
title: string;
|
|
121
|
+
path: string | undefined;
|
|
122
|
+
};
|
|
123
|
+
} | {
|
|
124
|
+
outcome: 'stale';
|
|
125
|
+
} | {
|
|
126
|
+
outcome: 'failed';
|
|
127
|
+
} | {
|
|
128
|
+
outcome: 'unknown';
|
|
129
|
+
};
|
|
130
|
+
/**
|
|
131
|
+
* Read one Workspace's full view (title plus canonical path). The path is
|
|
132
|
+
* for the administrator-only ephemeral info response — the disclosure
|
|
133
|
+
* policy owns whether it ever renders; this face only carries it in memory.
|
|
134
|
+
*/
|
|
135
|
+
export declare function readWorkspaceDetail(dsh: DshApiProxyFace, reference: string, options?: ApiProxyFaceOptions): Promise<WorkspaceDetailOutcome>;
|
|
136
|
+
export type PromptOutcome = {
|
|
137
|
+
outcome: 'accepted';
|
|
138
|
+
} | {
|
|
139
|
+
outcome: 'rejected';
|
|
140
|
+
reason: string;
|
|
141
|
+
} | {
|
|
142
|
+
outcome: 'unknown';
|
|
143
|
+
};
|
|
144
|
+
/**
|
|
145
|
+
* Submit one prompt turn through the in-process apiProxy. A definitive Host
|
|
146
|
+
* error is a rejection carrying the sanitized code; a timeout is `unknown` —
|
|
147
|
+
* the turn may or may not have been admitted, so callers must not resubmit.
|
|
148
|
+
* `options.rpcId` pins the adapter-owned stable request id, which the Host
|
|
149
|
+
* records on the durable `user/message` (`source.rpcId`) for reconciliation.
|
|
150
|
+
*/
|
|
151
|
+
export declare function promptSession(dsh: DshApiProxyFace, request: {
|
|
152
|
+
sessionId: string;
|
|
153
|
+
prompt: string;
|
|
154
|
+
}, options?: ApiProxyFaceOptions & {
|
|
155
|
+
rpcId?: string;
|
|
156
|
+
}): Promise<PromptOutcome>;
|
|
157
|
+
/**
|
|
158
|
+
* Steer the session's active turn: `session.prompt` with `mode: 'steer'`,
|
|
159
|
+
* carrying the same stable request-id discipline as the queue path.
|
|
160
|
+
*/
|
|
161
|
+
export declare function steerSession(dsh: DshApiProxyFace, request: {
|
|
162
|
+
sessionId: string;
|
|
163
|
+
prompt: string;
|
|
164
|
+
}, options?: ApiProxyFaceOptions & {
|
|
165
|
+
rpcId?: string;
|
|
166
|
+
}): Promise<PromptOutcome>;
|
|
167
|
+
export type CreateSessionOutcome = {
|
|
168
|
+
outcome: 'completed';
|
|
169
|
+
sessionId: string;
|
|
170
|
+
} | {
|
|
171
|
+
outcome: 'rejected';
|
|
172
|
+
reason: string;
|
|
173
|
+
} | {
|
|
174
|
+
outcome: 'unknown';
|
|
175
|
+
};
|
|
176
|
+
/**
|
|
177
|
+
* Create one DSH Session against a preallocated id (design.md §10): DSH
|
|
178
|
+
* adopts the same session id idempotently, so an uncertain response never
|
|
179
|
+
* forks a second Session. Same outcome discipline as the prompt path.
|
|
180
|
+
*/
|
|
181
|
+
export declare function createSessionViaProxy(dsh: DshApiProxyFace, request: {
|
|
182
|
+
sessionId: string;
|
|
183
|
+
workspaceId: string;
|
|
184
|
+
}, options?: ApiProxyFaceOptions): Promise<CreateSessionOutcome>;
|
|
185
|
+
/** The durable Session-id baseline reconciliation reconciles against. */
|
|
186
|
+
export type SessionIdListOutcome = {
|
|
187
|
+
outcome: 'completed';
|
|
188
|
+
ids: string[];
|
|
189
|
+
} | {
|
|
190
|
+
outcome: 'failed';
|
|
191
|
+
} | {
|
|
192
|
+
outcome: 'unknown';
|
|
193
|
+
};
|
|
194
|
+
/** List durable Session ids (`session.list`, v1 returns everything). */
|
|
195
|
+
export declare function listSessionIds(dsh: DshApiProxyFace, options?: ApiProxyFaceOptions): Promise<SessionIdListOutcome>;
|
|
196
|
+
export type CancelOutcome = {
|
|
197
|
+
outcome: 'accepted';
|
|
198
|
+
} | {
|
|
199
|
+
outcome: 'rejected';
|
|
200
|
+
reason: string;
|
|
201
|
+
} | {
|
|
202
|
+
outcome: 'unknown';
|
|
203
|
+
};
|
|
204
|
+
/** Cancel the session's active turn (`session.cancel`); DSH preserves the pending inbox. */
|
|
205
|
+
export declare function cancelSessionViaProxy(dsh: DshApiProxyFace, request: {
|
|
206
|
+
sessionId: string;
|
|
207
|
+
}, options?: ApiProxyFaceOptions): Promise<CancelOutcome>;
|
|
208
|
+
export type QueueRemoveOutcome = {
|
|
209
|
+
outcome: 'accepted';
|
|
210
|
+
} | {
|
|
211
|
+
outcome: 'rejected';
|
|
212
|
+
reason: string;
|
|
213
|
+
} | {
|
|
214
|
+
outcome: 'unknown';
|
|
215
|
+
};
|
|
216
|
+
/** Remove one pending inbox item (`session.updateQueue`, action remove). */
|
|
217
|
+
export declare function removeQueueItemViaProxy(dsh: DshApiProxyFace, request: {
|
|
218
|
+
sessionId: string;
|
|
219
|
+
itemId: string;
|
|
220
|
+
}, options?: ApiProxyFaceOptions): Promise<QueueRemoveOutcome>;
|
|
221
|
+
/** The carrier verdict apiProxy.respond resolves with (RpcReceipt). */
|
|
222
|
+
export type RespondReceipt = {
|
|
223
|
+
accepted: true;
|
|
224
|
+
} | {
|
|
225
|
+
accepted: false;
|
|
226
|
+
reason: 'not-pending' | 'bad-response';
|
|
227
|
+
} | {
|
|
228
|
+
accepted: unknown;
|
|
229
|
+
};
|
|
230
|
+
export type RespondOutcome = {
|
|
231
|
+
outcome: 'confirmed';
|
|
232
|
+
} | {
|
|
233
|
+
outcome: 'rejected';
|
|
234
|
+
reason: string;
|
|
235
|
+
} | {
|
|
236
|
+
outcome: 'unknown';
|
|
237
|
+
};
|
|
238
|
+
/**
|
|
239
|
+
* The respond face for answerable server-requests (approvals, questions).
|
|
240
|
+
* Builds the full ClientResponse envelope — {type: 'client-response',
|
|
241
|
+
* rpcId, result: {ok: true, value}} is the wire contract; posting the bare
|
|
242
|
+
* payload is silently ignored by the Host (rpcId never resolves) — and
|
|
243
|
+
* maps the RpcReceipt onto the port outcome. Like every call in this module
|
|
244
|
+
* it is bounded (a Host that never resolves the receipt must not wedge an
|
|
245
|
+
* interaction handler or an expiry sweep forever) and never throws: a Host
|
|
246
|
+
* rejection or timeout resolves to `unknown` — the response may or may not
|
|
247
|
+
* have landed, and the callers park the ask `unresolved` on exactly that.
|
|
248
|
+
*/
|
|
249
|
+
export declare function createClientRespondPort(dsh: {
|
|
250
|
+
respond(message: unknown): Promise<unknown>;
|
|
251
|
+
}, options?: ApiProxyFaceOptions): {
|
|
252
|
+
respond(rpcId: string, value: unknown): Promise<RespondOutcome>;
|
|
253
|
+
};
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The typed face over the Host's in-process `ctx.apiProxy` service
|
|
3
|
+
* (ApiProxyService, the transport-agnostic gateway's direct implementation).
|
|
4
|
+
* Its domain methods speak the narrow RPC signature: `RpcRequest<P>` in,
|
|
5
|
+
* `RpcResponse<T>` out, business errors on `result.ok === false` — they never
|
|
6
|
+
* throw for business outcomes. Two guarantees are added here:
|
|
7
|
+
*
|
|
8
|
+
* 1. Boundedness — a Host that never answers must not wedge an interaction
|
|
9
|
+
* handler (or a Discord ephemeral) forever; every call races a timeout and
|
|
10
|
+
* resolves to an unobservable outcome instead.
|
|
11
|
+
* 2. Observability — every terminal outcome is reported through the injected
|
|
12
|
+
* log sink, so a silent-void call can never again be misread as a hang.
|
|
13
|
+
*/
|
|
14
|
+
import { parseWorkspaceReference } from '../policy/disclosure.js';
|
|
15
|
+
/** Raised when the Host did not answer within the bounded window. */
|
|
16
|
+
export class RpcTimeoutError extends Error {
|
|
17
|
+
constructor(timeoutMs) {
|
|
18
|
+
super(`dsh apiProxy call did not answer within ${String(timeoutMs)}ms`);
|
|
19
|
+
this.name = 'RpcTimeoutError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
/** Race one apiProxy promise against a bounded window. */
|
|
23
|
+
export function withRpcTimeout(promise, timeoutMs) {
|
|
24
|
+
let timer;
|
|
25
|
+
const timeout = new Promise((_, reject) => {
|
|
26
|
+
timer = setTimeout(() => { reject(new RpcTimeoutError(timeoutMs)); }, timeoutMs);
|
|
27
|
+
});
|
|
28
|
+
return Promise.race([promise, timeout]).finally(() => {
|
|
29
|
+
if (timer !== undefined)
|
|
30
|
+
clearTimeout(timer);
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
/** Default bounded window for unary catalog reads (local, no model work). */
|
|
34
|
+
const CATALOG_TIMEOUT_MS = 5_000;
|
|
35
|
+
/** Default bounded window for prompt admission (the Host may enqueue). */
|
|
36
|
+
const PROMPT_TIMEOUT_MS = 30_000;
|
|
37
|
+
/** Mint a request envelope the way every client shape does. */
|
|
38
|
+
function mintRequest(payload, rpcId) {
|
|
39
|
+
return { rpcId: rpcId ?? crypto.randomUUID(), payload };
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The bind flow's catalog verifier: resolves an opaque `ws:` reference
|
|
43
|
+
* against the live workspace list. A well-formed reference the catalog no
|
|
44
|
+
* longer knows — and any malformed one — resolve `stale` (fail-closed, no
|
|
45
|
+
* write can follow); a Host error is `failed`; a timeout is `unknown`.
|
|
46
|
+
*/
|
|
47
|
+
export function createWorkspaceResolver(dsh, options = {}) {
|
|
48
|
+
const port = createWorkspaceCatalogPort(dsh, options);
|
|
49
|
+
return {
|
|
50
|
+
async resolve(reference) {
|
|
51
|
+
const catalog = await port.listWorkspaces();
|
|
52
|
+
if (catalog.outcome !== 'completed') {
|
|
53
|
+
return catalog.outcome === 'unknown' ? { outcome: 'unknown' } : { outcome: 'failed' };
|
|
54
|
+
}
|
|
55
|
+
const id = parseWorkspaceReference(reference);
|
|
56
|
+
const found = id === undefined
|
|
57
|
+
? undefined
|
|
58
|
+
: catalog.workspaces.find(workspace => workspace.id === id);
|
|
59
|
+
return found === undefined
|
|
60
|
+
? { outcome: 'stale' }
|
|
61
|
+
: { outcome: 'found', workspace: { id: found.id, title: found.title } };
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* The `/project list` catalog port satisfied by the in-process apiProxy.
|
|
67
|
+
* Outcomes follow the port contract: a definitive Host error is `failed`
|
|
68
|
+
* (sanitized before Discord), while a timeout or unreadable body is
|
|
69
|
+
* `unknown` — delivery was not observed, so no retry is implied.
|
|
70
|
+
*/
|
|
71
|
+
export function createWorkspaceCatalogPort(dsh, options = {}) {
|
|
72
|
+
const timeoutMs = options.timeoutMs ?? CATALOG_TIMEOUT_MS;
|
|
73
|
+
const log = options.log;
|
|
74
|
+
return {
|
|
75
|
+
async listWorkspaces() {
|
|
76
|
+
let response;
|
|
77
|
+
try {
|
|
78
|
+
response = await withRpcTimeout(dsh.workspace.list(mintRequest({})), timeoutMs);
|
|
79
|
+
}
|
|
80
|
+
catch (cause) {
|
|
81
|
+
if (cause instanceof RpcTimeoutError) {
|
|
82
|
+
log?.('discord_workspace_list_timeout', { timeoutMs });
|
|
83
|
+
return { outcome: 'unknown' };
|
|
84
|
+
}
|
|
85
|
+
log?.('discord_workspace_list_threw', { cause: String(cause) });
|
|
86
|
+
return { outcome: 'failed' };
|
|
87
|
+
}
|
|
88
|
+
// A malformed envelope is a definitive, sanitized failure: the Host
|
|
89
|
+
// answered, so a retry is safe and semantics stay observable-free.
|
|
90
|
+
const result = response?.result;
|
|
91
|
+
if (result === undefined) {
|
|
92
|
+
log?.('discord_workspace_list_malformed');
|
|
93
|
+
return { outcome: 'failed' };
|
|
94
|
+
}
|
|
95
|
+
if (result.ok) {
|
|
96
|
+
const items = Array.isArray(result.value.items)
|
|
97
|
+
? result.value.items
|
|
98
|
+
: [];
|
|
99
|
+
return {
|
|
100
|
+
outcome: 'completed',
|
|
101
|
+
workspaces: items.map(workspace => ({
|
|
102
|
+
id: workspace.workspaceId,
|
|
103
|
+
title: workspace.title,
|
|
104
|
+
})),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
log?.('discord_workspace_list_rejected', {
|
|
108
|
+
code: result.error.code,
|
|
109
|
+
message: result.error.message,
|
|
110
|
+
});
|
|
111
|
+
return { outcome: 'failed' };
|
|
112
|
+
},
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Read one Workspace's full view (title plus canonical path). The path is
|
|
117
|
+
* for the administrator-only ephemeral info response — the disclosure
|
|
118
|
+
* policy owns whether it ever renders; this face only carries it in memory.
|
|
119
|
+
*/
|
|
120
|
+
export async function readWorkspaceDetail(dsh, reference, options = {}) {
|
|
121
|
+
const timeoutMs = options.timeoutMs ?? CATALOG_TIMEOUT_MS;
|
|
122
|
+
const log = options.log;
|
|
123
|
+
let response;
|
|
124
|
+
try {
|
|
125
|
+
response = await withRpcTimeout(dsh.workspace.list(mintRequest({})), timeoutMs);
|
|
126
|
+
}
|
|
127
|
+
catch (cause) {
|
|
128
|
+
if (cause instanceof RpcTimeoutError) {
|
|
129
|
+
log?.('discord_workspace_detail_timeout', { timeoutMs });
|
|
130
|
+
return { outcome: 'unknown' };
|
|
131
|
+
}
|
|
132
|
+
log?.('discord_workspace_detail_threw', { cause: String(cause) });
|
|
133
|
+
return { outcome: 'failed' };
|
|
134
|
+
}
|
|
135
|
+
const result = response?.result;
|
|
136
|
+
if (result === undefined) {
|
|
137
|
+
log?.('discord_workspace_detail_malformed');
|
|
138
|
+
return { outcome: 'failed' };
|
|
139
|
+
}
|
|
140
|
+
if (!result.ok) {
|
|
141
|
+
log?.('discord_workspace_detail_rejected', { code: result.error.code });
|
|
142
|
+
return { outcome: 'failed' };
|
|
143
|
+
}
|
|
144
|
+
const id = parseWorkspaceReference(reference) ?? (reference === '' ? undefined : reference);
|
|
145
|
+
const found = Array.isArray(result.value.items)
|
|
146
|
+
? result.value.items.find(workspace => workspace.workspaceId === id)
|
|
147
|
+
: undefined;
|
|
148
|
+
if (found === undefined)
|
|
149
|
+
return { outcome: 'stale' };
|
|
150
|
+
return {
|
|
151
|
+
outcome: 'found',
|
|
152
|
+
workspace: {
|
|
153
|
+
id: found.workspaceId,
|
|
154
|
+
title: found.title,
|
|
155
|
+
path: typeof found.path === 'string' ? found.path : undefined,
|
|
156
|
+
},
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
/**
|
|
160
|
+
* Submit one prompt turn through the in-process apiProxy. A definitive Host
|
|
161
|
+
* error is a rejection carrying the sanitized code; a timeout is `unknown` —
|
|
162
|
+
* the turn may or may not have been admitted, so callers must not resubmit.
|
|
163
|
+
* `options.rpcId` pins the adapter-owned stable request id, which the Host
|
|
164
|
+
* records on the durable `user/message` (`source.rpcId`) for reconciliation.
|
|
165
|
+
*/
|
|
166
|
+
export async function promptSession(dsh, request, options = {}) {
|
|
167
|
+
return submitPromptTurn(dsh, { ...request, mode: 'queue' }, options);
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* Steer the session's active turn: `session.prompt` with `mode: 'steer'`,
|
|
171
|
+
* carrying the same stable request-id discipline as the queue path.
|
|
172
|
+
*/
|
|
173
|
+
export async function steerSession(dsh, request, options = {}) {
|
|
174
|
+
return submitPromptTurn(dsh, { ...request, mode: 'steer' }, options);
|
|
175
|
+
}
|
|
176
|
+
async function submitPromptTurn(dsh, request, options) {
|
|
177
|
+
const timeoutMs = options.timeoutMs ?? PROMPT_TIMEOUT_MS;
|
|
178
|
+
const log = options.log;
|
|
179
|
+
let response;
|
|
180
|
+
try {
|
|
181
|
+
response = await withRpcTimeout(dsh.sessions.prompt(mintRequest({
|
|
182
|
+
sessionId: request.sessionId,
|
|
183
|
+
mode: request.mode,
|
|
184
|
+
content: [{ type: 'text', text: request.prompt }],
|
|
185
|
+
}, options.rpcId)), timeoutMs);
|
|
186
|
+
}
|
|
187
|
+
catch (cause) {
|
|
188
|
+
if (cause instanceof RpcTimeoutError) {
|
|
189
|
+
log?.('discord_prompt_submit_timeout', { timeoutMs, sessionId: request.sessionId });
|
|
190
|
+
return { outcome: 'unknown' };
|
|
191
|
+
}
|
|
192
|
+
log?.('discord_prompt_submit_threw', { cause: String(cause), sessionId: request.sessionId });
|
|
193
|
+
return { outcome: 'unknown' };
|
|
194
|
+
}
|
|
195
|
+
const result = response?.result;
|
|
196
|
+
if (result === undefined) {
|
|
197
|
+
// An unreadable body leaves admission unobservable: the turn MAY have
|
|
198
|
+
// been admitted, so the at-most-once discipline forbids resubmission.
|
|
199
|
+
log?.('discord_prompt_submit_malformed', { sessionId: request.sessionId });
|
|
200
|
+
return { outcome: 'unknown' };
|
|
201
|
+
}
|
|
202
|
+
if (result.ok) {
|
|
203
|
+
// The wire schema pins accepted to literal true; the Host's own zod
|
|
204
|
+
// parse enforces it before this seam ever sees the body.
|
|
205
|
+
return { outcome: 'accepted' };
|
|
206
|
+
}
|
|
207
|
+
log?.('discord_prompt_submit_rejected', {
|
|
208
|
+
code: result.error.code,
|
|
209
|
+
sessionId: request.sessionId,
|
|
210
|
+
});
|
|
211
|
+
return { outcome: 'rejected', reason: result.error.code };
|
|
212
|
+
}
|
|
213
|
+
/**
|
|
214
|
+
* Create one DSH Session against a preallocated id (design.md §10): DSH
|
|
215
|
+
* adopts the same session id idempotently, so an uncertain response never
|
|
216
|
+
* forks a second Session. Same outcome discipline as the prompt path.
|
|
217
|
+
*/
|
|
218
|
+
export async function createSessionViaProxy(dsh, request, options = {}) {
|
|
219
|
+
const timeoutMs = options.timeoutMs ?? PROMPT_TIMEOUT_MS;
|
|
220
|
+
const log = options.log;
|
|
221
|
+
let response;
|
|
222
|
+
try {
|
|
223
|
+
response = await withRpcTimeout(dsh.sessions.create(mintRequest({
|
|
224
|
+
workspaceId: request.workspaceId,
|
|
225
|
+
sessionId: request.sessionId,
|
|
226
|
+
})), timeoutMs);
|
|
227
|
+
}
|
|
228
|
+
catch (cause) {
|
|
229
|
+
if (cause instanceof RpcTimeoutError) {
|
|
230
|
+
log?.('discord_session_create_timeout', { timeoutMs, sessionId: request.sessionId });
|
|
231
|
+
return { outcome: 'unknown' };
|
|
232
|
+
}
|
|
233
|
+
log?.('discord_session_create_threw', { cause: String(cause), sessionId: request.sessionId });
|
|
234
|
+
return { outcome: 'unknown' };
|
|
235
|
+
}
|
|
236
|
+
const result = response?.result;
|
|
237
|
+
if (result === undefined) {
|
|
238
|
+
log?.('discord_session_create_malformed', { sessionId: request.sessionId });
|
|
239
|
+
return { outcome: 'unknown' };
|
|
240
|
+
}
|
|
241
|
+
if (result.ok) {
|
|
242
|
+
return { outcome: 'completed', sessionId: result.value.sessionId };
|
|
243
|
+
}
|
|
244
|
+
log?.('discord_session_create_rejected', { code: result.error.code, sessionId: request.sessionId });
|
|
245
|
+
return { outcome: 'rejected', reason: result.error.code };
|
|
246
|
+
}
|
|
247
|
+
/** List durable Session ids (`session.list`, v1 returns everything). */
|
|
248
|
+
export async function listSessionIds(dsh, options = {}) {
|
|
249
|
+
const timeoutMs = options.timeoutMs ?? CATALOG_TIMEOUT_MS;
|
|
250
|
+
const log = options.log;
|
|
251
|
+
let response;
|
|
252
|
+
try {
|
|
253
|
+
response = await withRpcTimeout(dsh.sessions.list(mintRequest({})), timeoutMs);
|
|
254
|
+
}
|
|
255
|
+
catch (cause) {
|
|
256
|
+
if (cause instanceof RpcTimeoutError) {
|
|
257
|
+
log?.('discord_session_list_timeout', { timeoutMs });
|
|
258
|
+
return { outcome: 'unknown' };
|
|
259
|
+
}
|
|
260
|
+
log?.('discord_session_list_threw', { cause: String(cause) });
|
|
261
|
+
return { outcome: 'unknown' };
|
|
262
|
+
}
|
|
263
|
+
const result = response?.result;
|
|
264
|
+
if (result === undefined) {
|
|
265
|
+
log?.('discord_session_list_malformed');
|
|
266
|
+
return { outcome: 'failed' };
|
|
267
|
+
}
|
|
268
|
+
if (result.ok) {
|
|
269
|
+
return { outcome: 'completed', ids: Array.isArray(result.value.items) ? result.value.items.map(item => item.sessionId) : [] };
|
|
270
|
+
}
|
|
271
|
+
log?.('discord_session_list_rejected', { code: result.error.code });
|
|
272
|
+
return { outcome: 'failed' };
|
|
273
|
+
}
|
|
274
|
+
/** Cancel the session's active turn (`session.cancel`); DSH preserves the pending inbox. */
|
|
275
|
+
export async function cancelSessionViaProxy(dsh, request, options = {}) {
|
|
276
|
+
const timeoutMs = options.timeoutMs ?? CATALOG_TIMEOUT_MS;
|
|
277
|
+
const log = options.log;
|
|
278
|
+
let response;
|
|
279
|
+
try {
|
|
280
|
+
response = await withRpcTimeout(dsh.sessions.cancel(mintRequest({ sessionId: request.sessionId })), timeoutMs);
|
|
281
|
+
}
|
|
282
|
+
catch (cause) {
|
|
283
|
+
if (cause instanceof RpcTimeoutError) {
|
|
284
|
+
log?.('discord_session_cancel_timeout', { timeoutMs, sessionId: request.sessionId });
|
|
285
|
+
return { outcome: 'unknown' };
|
|
286
|
+
}
|
|
287
|
+
log?.('discord_session_cancel_threw', { cause: String(cause), sessionId: request.sessionId });
|
|
288
|
+
return { outcome: 'unknown' };
|
|
289
|
+
}
|
|
290
|
+
const result = response?.result;
|
|
291
|
+
if (result === undefined) {
|
|
292
|
+
log?.('discord_session_cancel_malformed', { sessionId: request.sessionId });
|
|
293
|
+
return { outcome: 'unknown' };
|
|
294
|
+
}
|
|
295
|
+
if (result.ok)
|
|
296
|
+
return { outcome: 'accepted' };
|
|
297
|
+
log?.('discord_session_cancel_rejected', { code: result.error.code, sessionId: request.sessionId });
|
|
298
|
+
return { outcome: 'rejected', reason: result.error.code };
|
|
299
|
+
}
|
|
300
|
+
/** Remove one pending inbox item (`session.updateQueue`, action remove). */
|
|
301
|
+
export async function removeQueueItemViaProxy(dsh, request, options = {}) {
|
|
302
|
+
const timeoutMs = options.timeoutMs ?? CATALOG_TIMEOUT_MS;
|
|
303
|
+
const log = options.log;
|
|
304
|
+
let response;
|
|
305
|
+
try {
|
|
306
|
+
response = await withRpcTimeout(dsh.sessions.updateQueue(mintRequest({
|
|
307
|
+
sessionId: request.sessionId,
|
|
308
|
+
itemId: request.itemId,
|
|
309
|
+
action: { kind: 'remove' },
|
|
310
|
+
})), timeoutMs);
|
|
311
|
+
}
|
|
312
|
+
catch (cause) {
|
|
313
|
+
if (cause instanceof RpcTimeoutError) {
|
|
314
|
+
log?.('discord_queue_remove_timeout', { timeoutMs, sessionId: request.sessionId });
|
|
315
|
+
return { outcome: 'unknown' };
|
|
316
|
+
}
|
|
317
|
+
log?.('discord_queue_remove_threw', { cause: String(cause), sessionId: request.sessionId });
|
|
318
|
+
return { outcome: 'unknown' };
|
|
319
|
+
}
|
|
320
|
+
const result = response?.result;
|
|
321
|
+
if (result === undefined) {
|
|
322
|
+
log?.('discord_queue_remove_malformed', { sessionId: request.sessionId });
|
|
323
|
+
return { outcome: 'unknown' };
|
|
324
|
+
}
|
|
325
|
+
if (result.ok)
|
|
326
|
+
return { outcome: 'accepted' };
|
|
327
|
+
log?.('discord_queue_remove_rejected', { code: result.error.code, sessionId: request.sessionId });
|
|
328
|
+
return { outcome: 'rejected', reason: result.error.code };
|
|
329
|
+
}
|
|
330
|
+
/**
|
|
331
|
+
* The respond face for answerable server-requests (approvals, questions).
|
|
332
|
+
* Builds the full ClientResponse envelope — {type: 'client-response',
|
|
333
|
+
* rpcId, result: {ok: true, value}} is the wire contract; posting the bare
|
|
334
|
+
* payload is silently ignored by the Host (rpcId never resolves) — and
|
|
335
|
+
* maps the RpcReceipt onto the port outcome. Like every call in this module
|
|
336
|
+
* it is bounded (a Host that never resolves the receipt must not wedge an
|
|
337
|
+
* interaction handler or an expiry sweep forever) and never throws: a Host
|
|
338
|
+
* rejection or timeout resolves to `unknown` — the response may or may not
|
|
339
|
+
* have landed, and the callers park the ask `unresolved` on exactly that.
|
|
340
|
+
*/
|
|
341
|
+
export function createClientRespondPort(dsh, options = {}) {
|
|
342
|
+
const log = options.log;
|
|
343
|
+
const timeoutMs = options.timeoutMs ?? PROMPT_TIMEOUT_MS;
|
|
344
|
+
return {
|
|
345
|
+
async respond(rpcId, value) {
|
|
346
|
+
let receipt;
|
|
347
|
+
try {
|
|
348
|
+
const response = await withRpcTimeout(dsh.respond({
|
|
349
|
+
type: 'client-response',
|
|
350
|
+
rpcId,
|
|
351
|
+
result: { ok: true, value },
|
|
352
|
+
}), timeoutMs);
|
|
353
|
+
receipt = response;
|
|
354
|
+
}
|
|
355
|
+
catch (cause) {
|
|
356
|
+
const reason = cause instanceof RpcTimeoutError ? 'discord_client_respond_timeout' : 'discord_client_respond_threw';
|
|
357
|
+
log?.(reason, { rpcId, ...(cause instanceof RpcTimeoutError ? { timeoutMs } : { cause: String(cause) }) });
|
|
358
|
+
return { outcome: 'unknown' };
|
|
359
|
+
}
|
|
360
|
+
const table = receipt;
|
|
361
|
+
log?.('discord_client_respond_receipt', { rpcId, receipt: table ?? null });
|
|
362
|
+
if (table?.accepted === true)
|
|
363
|
+
return { outcome: 'confirmed' };
|
|
364
|
+
if (table?.accepted === false) {
|
|
365
|
+
return { outcome: 'rejected', reason: typeof table.reason === 'string' ? table.reason : 'respond-refused' };
|
|
366
|
+
}
|
|
367
|
+
return { outcome: 'unknown' };
|
|
368
|
+
},
|
|
369
|
+
};
|
|
370
|
+
}
|