@yadsh/dsh-qa-surface 0.10.0 → 0.11.1

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.
Files changed (169) hide show
  1. package/README.md +310 -48
  2. package/lib/access/service.js +49 -10
  3. package/lib/access/service.js.map +1 -1
  4. package/lib/account-remotes.js +41 -0
  5. package/lib/account-remotes.js.map +1 -1
  6. package/lib/accounts/database.js +140 -0
  7. package/lib/accounts/database.js.map +1 -1
  8. package/lib/accounts/errors.js.map +1 -1
  9. package/lib/accounts/service-token.js +103 -0
  10. package/lib/accounts/service-token.js.map +1 -0
  11. package/lib/accounts/store.js +291 -5
  12. package/lib/accounts/store.js.map +1 -1
  13. package/lib/admin/conversation-log.js +14 -8
  14. package/lib/admin/conversation-log.js.map +1 -1
  15. package/lib/admin/permissions.js +5 -0
  16. package/lib/admin/permissions.js.map +1 -1
  17. package/lib/admin/quality-store.js +4 -0
  18. package/lib/admin/quality-store.js.map +1 -1
  19. package/lib/admin/service.js +173 -0
  20. package/lib/admin/service.js.map +1 -1
  21. package/lib/admin/session-log.js +39 -8
  22. package/lib/admin/session-log.js.map +1 -1
  23. package/lib/attachment-rules.js +39 -6
  24. package/lib/attachment-rules.js.map +1 -1
  25. package/lib/cli.js +83 -1
  26. package/lib/cli.js.map +1 -1
  27. package/lib/client/QaAccountsController.js +213 -4
  28. package/lib/client/QaAccountsController.js.map +1 -1
  29. package/lib/client/QaSessionController.js +37 -21
  30. package/lib/client/QaSessionController.js.map +1 -1
  31. package/lib/client/QaSurface.js +28 -4
  32. package/lib/client/QaSurface.js.map +1 -1
  33. package/lib/client/admin/QaAdmin.js +9 -1
  34. package/lib/client/admin/QaAdmin.js.map +1 -1
  35. package/lib/client/admin/pages/SkillFiles.js +78 -0
  36. package/lib/client/admin/pages/SkillFiles.js.map +1 -0
  37. package/lib/client/admin/pages/Users.js +65 -1
  38. package/lib/client/admin/pages/Users.js.map +1 -1
  39. package/lib/client/admin/routes.js +8 -1
  40. package/lib/client/admin/routes.js.map +1 -1
  41. package/lib/client/admin/styles.js +2 -0
  42. package/lib/client/admin/styles.js.map +1 -1
  43. package/lib/client/attachments.js +41 -11
  44. package/lib/client/attachments.js.map +1 -1
  45. package/lib/client/components/QaAuthGate.js +19 -10
  46. package/lib/client/components/QaAuthGate.js.map +1 -1
  47. package/lib/client/components/QaChangelog.js +43 -0
  48. package/lib/client/components/QaChangelog.js.map +1 -1
  49. package/lib/client/index.js +13 -0
  50. package/lib/client/index.js.map +1 -1
  51. package/lib/client/lineage.js +64 -0
  52. package/lib/client/lineage.js.map +1 -1
  53. package/lib/client/settings/sections/attachments.js +6 -5
  54. package/lib/client/settings/sections/attachments.js.map +1 -1
  55. package/lib/client/settings/sections/notes.js +22 -4
  56. package/lib/client/settings/sections/notes.js.map +1 -1
  57. package/lib/client/styles.js +9 -0
  58. package/lib/client/styles.js.map +1 -1
  59. package/lib/client/types.js.map +1 -1
  60. package/lib/client/user-settings/IntegrationTokensPage.js +150 -0
  61. package/lib/client/user-settings/IntegrationTokensPage.js.map +1 -0
  62. package/lib/client/user-settings/PasswordSettingsPage.js +48 -0
  63. package/lib/client/user-settings/PasswordSettingsPage.js.map +1 -0
  64. package/lib/client/user-settings/SkillCatalog.js +2 -2
  65. package/lib/client/user-settings/SkillCatalog.js.map +1 -1
  66. package/lib/client/user-settings/UserSettingsDialog.js +17 -2
  67. package/lib/client/user-settings/UserSettingsDialog.js.map +1 -1
  68. package/lib/client/user-settings/copy.js +20 -0
  69. package/lib/client/user-settings/copy.js.map +1 -1
  70. package/lib/client.js +2480 -137
  71. package/lib/client.js.map +1 -1
  72. package/lib/config-resolvers/attachments.js +6 -5
  73. package/lib/config-resolvers/attachments.js.map +1 -1
  74. package/lib/config-resolvers/defaults.js +23 -2
  75. package/lib/config-resolvers/defaults.js.map +1 -1
  76. package/lib/config-resolvers/integration.js +94 -0
  77. package/lib/config-resolvers/integration.js.map +1 -0
  78. package/lib/config-resolvers/notes.js +2 -0
  79. package/lib/config-resolvers/notes.js.map +1 -1
  80. package/lib/config.js +72 -0
  81. package/lib/config.js.map +1 -1
  82. package/lib/index.js +197 -2
  83. package/lib/index.js.map +1 -1
  84. package/lib/integration/answer.js +100 -0
  85. package/lib/integration/answer.js.map +1 -0
  86. package/lib/integration/attachments.js +246 -0
  87. package/lib/integration/attachments.js.map +1 -0
  88. package/lib/integration/contract.js +53 -0
  89. package/lib/integration/contract.js.map +1 -0
  90. package/lib/integration/host-runner.js +243 -0
  91. package/lib/integration/host-runner.js.map +1 -0
  92. package/lib/integration/http.js +576 -0
  93. package/lib/integration/http.js.map +1 -0
  94. package/lib/integration/service.js +373 -0
  95. package/lib/integration/service.js.map +1 -0
  96. package/lib/integration/transcript-reader.js +202 -0
  97. package/lib/integration/transcript-reader.js.map +1 -0
  98. package/lib/integration/transcript.js +148 -0
  99. package/lib/integration/transcript.js.map +1 -0
  100. package/lib/personal-skills/index.js +2 -2
  101. package/lib/personal-skills/index.js.map +1 -1
  102. package/lib/personal-skills/paths.js +14 -8
  103. package/lib/personal-skills/paths.js.map +1 -1
  104. package/lib/personal-skills/provider.js +16 -4
  105. package/lib/personal-skills/provider.js.map +1 -1
  106. package/lib/personal-skills/service.js +240 -30
  107. package/lib/personal-skills/service.js.map +1 -1
  108. package/lib/prompt-notes.js +58 -0
  109. package/lib/prompt-notes.js.map +1 -1
  110. package/lib/provenance/document-preview.js +10 -1
  111. package/lib/provenance/document-preview.js.map +1 -1
  112. package/lib/qa-tools/catalog.js +12 -1
  113. package/lib/qa-tools/catalog.js.map +1 -1
  114. package/lib/qa-tools/docs-tools.js +563 -0
  115. package/lib/qa-tools/docs-tools.js.map +1 -0
  116. package/lib/resolve-config.js +4 -0
  117. package/lib/resolve-config.js.map +1 -1
  118. package/lib/secure-session.js +83 -3
  119. package/lib/secure-session.js.map +1 -1
  120. package/lib/shared/integration-tokens.js +23 -0
  121. package/lib/shared/integration-tokens.js.map +1 -0
  122. package/lib/typert.host.js +1176 -213
  123. package/lib/typert.remote-client.d.ts +27 -1
  124. package/lib/typert.remote-client.js +911 -69
  125. package/lib/types/access/service.d.ts +33 -2
  126. package/lib/types/account-remotes.d.ts +30 -1
  127. package/lib/types/accounts/database.d.ts +53 -0
  128. package/lib/types/accounts/errors.d.ts +5 -1
  129. package/lib/types/accounts/service-token.d.ts +70 -0
  130. package/lib/types/accounts/store.d.ts +131 -1
  131. package/lib/types/admin/conversation-log.d.ts +12 -0
  132. package/lib/types/admin/service.d.ts +56 -1
  133. package/lib/types/admin/session-log.d.ts +21 -0
  134. package/lib/types/attachment-rules.d.ts +26 -3
  135. package/lib/types/client/QaAccountsController.d.ts +64 -5
  136. package/lib/types/client/QaSessionController.d.ts +24 -2
  137. package/lib/types/client/admin/pages/SkillFiles.d.ts +17 -0
  138. package/lib/types/client/admin/routes.d.ts +2 -0
  139. package/lib/types/client/attachments.d.ts +21 -4
  140. package/lib/types/client/lineage.d.ts +42 -0
  141. package/lib/types/client/settings/sections/notes.d.ts +5 -3
  142. package/lib/types/client/types.d.ts +71 -1
  143. package/lib/types/client/user-settings/IntegrationTokensPage.d.ts +21 -0
  144. package/lib/types/client/user-settings/PasswordSettingsPage.d.ts +19 -0
  145. package/lib/types/client/user-settings/UserSettingsDialog.d.ts +11 -2
  146. package/lib/types/client/user-settings/copy.d.ts +10 -1
  147. package/lib/types/config-resolvers/integration.d.ts +47 -0
  148. package/lib/types/config.d.ts +10 -0
  149. package/lib/types/index.d.ts +73 -1
  150. package/lib/types/integration/answer.d.ts +65 -0
  151. package/lib/types/integration/attachments.d.ts +85 -0
  152. package/lib/types/integration/contract.d.ts +196 -0
  153. package/lib/types/integration/host-runner.d.ts +44 -0
  154. package/lib/types/integration/http.d.ts +79 -0
  155. package/lib/types/integration/service.d.ts +120 -0
  156. package/lib/types/integration/transcript-reader.d.ts +100 -0
  157. package/lib/types/integration/transcript.d.ts +59 -0
  158. package/lib/types/personal-skills/index.d.ts +2 -2
  159. package/lib/types/personal-skills/paths.d.ts +15 -3
  160. package/lib/types/personal-skills/provider.d.ts +14 -2
  161. package/lib/types/personal-skills/service.d.ts +89 -14
  162. package/lib/types/prompt-notes.d.ts +28 -0
  163. package/lib/types/provenance/document-preview.d.ts +4 -0
  164. package/lib/types/qa-tools/catalog.d.ts +1 -1
  165. package/lib/types/qa-tools/docs-tools.d.ts +160 -0
  166. package/lib/types/secure-session.d.ts +52 -1
  167. package/lib/types/shared/integration-tokens.d.ts +24 -0
  168. package/lib/types/types.d.ts +170 -2
  169. package/package.json +2 -2
@@ -0,0 +1,373 @@
1
+ import { remoteErrorOf } from "@deepseek-ai/dsh-typert-protocol";
2
+ import { parseServiceToken, } from "../accounts/service-token.js";
3
+ import { boundAnswer } from "./answer.js";
4
+ import { QaIntegrationAttachmentError } from "./attachments.js";
5
+ import { QaIntegrationError, } from "./contract.js";
6
+ /**
7
+ * The integration API's brain: credential, budget, and one answered turn.
8
+ *
9
+ * The service owns everything that must be identical for every transport —
10
+ * who is allowed to ask, how often, how many at once, what a refusal means and
11
+ * how an answered turn is shaped into the contract. The HTTP layer owns bytes
12
+ * and status codes, the runner owns sessions and agents. That split is what
13
+ * lets the mechanics be tested without a web server, a model or a database.
14
+ *
15
+ * Two limits, deliberately different in kind. Concurrency (`maxConcurrent`)
16
+ * protects the deployment: a bridge that fires a hundred questions must not
17
+ * open a hundred agent turns. Rate (`requestsPerMinute`) protects the account:
18
+ * one token cannot spend the deployment's whole budget. Both refuse with 429,
19
+ * which the bridge already retries.
20
+ */
21
+ /** The bridge publishes a handful of citations; more is noise in a Jira note. */
22
+ export const QA_INTEGRATION_MAX_CITATIONS = 5;
23
+ const MINUTE_MS = 60_000;
24
+ /**
25
+ * One citation string per source, in the bridge's own style: the document plus
26
+ * where in it the evidence sits, so a reader can open the right page.
27
+ *
28
+ * @param sources - the turn's evidence, best first.
29
+ * @returns bounded citation strings.
30
+ */
31
+ export function sourceCitations(sources) {
32
+ const citations = [];
33
+ for (const source of sources) {
34
+ if (citations.length >= QA_INTEGRATION_MAX_CITATIONS)
35
+ break;
36
+ const location = source.locations?.[0];
37
+ const base = source.uri ?? source.path ?? source.title;
38
+ let citation = base;
39
+ if (location !== undefined) {
40
+ if (location.anchor !== undefined && location.anchor !== "") {
41
+ citation = `${base}#${location.anchor}`;
42
+ }
43
+ else if (location.lineStart !== undefined) {
44
+ citation =
45
+ location.lineEnd === undefined ||
46
+ location.lineEnd === location.lineStart
47
+ ? `${base}:${String(location.lineStart)}`
48
+ : `${base}:${String(location.lineStart)}-${String(location.lineEnd)}`;
49
+ }
50
+ }
51
+ if (citation === "" || citations.includes(citation))
52
+ continue;
53
+ citations.push(citation);
54
+ }
55
+ return Object.freeze(citations);
56
+ }
57
+ export class QaIntegrationService {
58
+ deps;
59
+ now;
60
+ startedAt;
61
+ /** Recent request timestamps per token id, pruned to the rolling minute. */
62
+ recent = new Map();
63
+ inFlight = 0;
64
+ constructor(deps) {
65
+ this.deps = deps;
66
+ this.now = deps.now ?? Date.now;
67
+ this.startedAt = this.now();
68
+ }
69
+ /** Requests currently inside `ask`; the console shows this as load. */
70
+ get busy() {
71
+ return this.inFlight;
72
+ }
73
+ /**
74
+ * Resolve a bearer credential to its token, or refuse.
75
+ *
76
+ * A malformed, unknown, expired, revoked or disabled-account token is one
77
+ * answer — 401 — because telling them apart would let an unauthenticated
78
+ * caller enumerate token ids. A valid token missing the scope is 403: the
79
+ * caller proved who it is, and the refusal is about rights, not identity.
80
+ */
81
+ authenticate(authorization, required) {
82
+ const token = bearerToken(authorization);
83
+ if (token === null) {
84
+ throw new QaIntegrationError("unauthorized", "a Bearer integration token is required");
85
+ }
86
+ const accounts = this.deps.accounts();
87
+ if (accounts === undefined) {
88
+ throw new QaIntegrationError("unavailable", "QA accounts are not enabled on this deployment");
89
+ }
90
+ // The shape check happens before the store: a foreign credential must not
91
+ // even reach a database lookup.
92
+ if (parseServiceToken(token) === null) {
93
+ throw new QaIntegrationError("unauthorized", "the token is not accepted");
94
+ }
95
+ const verified = accounts.verifyServiceToken(token);
96
+ if (verified === null) {
97
+ throw new QaIntegrationError("unauthorized", "the token is expired, revoked or unknown");
98
+ }
99
+ if (required !== null && !verified.scopes.includes(required)) {
100
+ throw new QaIntegrationError("forbidden", `the token lacks the ${required} scope`);
101
+ }
102
+ return Object.freeze({
103
+ tokenId: verified.tokenId,
104
+ userId: verified.userId,
105
+ scopes: verified.scopes,
106
+ });
107
+ }
108
+ /**
109
+ * Answer one question, or refuse before a turn is ever opened.
110
+ *
111
+ * @param authorization - the raw `Authorization` header value.
112
+ * @param request - the parsed body.
113
+ * @param signal - the request's own lifetime; a dropped connection stops the wait.
114
+ * @returns the answer the bridge publishes.
115
+ */
116
+ async ask(authorization, request, signal) {
117
+ const config = this.deps.getConfig();
118
+ if (!config.integration.enabled) {
119
+ throw new QaIntegrationError("unavailable", "the QA integration API is disabled on this deployment");
120
+ }
121
+ const identity = this.authenticate(authorization, "ask");
122
+ this.assertRate(identity.tokenId);
123
+ if (this.inFlight >= config.integration.maxConcurrent) {
124
+ throw new QaIntegrationError("busy", "too many integration requests are being answered right now");
125
+ }
126
+ const started = this.now();
127
+ const deadline = new AbortController();
128
+ const timer = setTimeout(() => {
129
+ deadline.abort(new Error("integration request timed out"));
130
+ }, config.integration.requestTimeoutMs);
131
+ const combined = AbortSignal.any([signal, deadline.signal]);
132
+ let chatId = request.sessionId;
133
+ this.inFlight += 1;
134
+ try {
135
+ const turn = await this.deps.runner.run({
136
+ userId: identity.userId,
137
+ chatId: request.sessionId,
138
+ message: request.message,
139
+ attachments: request.attachments,
140
+ ticketKey: request.context.ticketKey,
141
+ signal: combined,
142
+ onChat: (id) => {
143
+ chatId = id;
144
+ },
145
+ });
146
+ this.deps.logger.info("integration.answered", {
147
+ tokenId: identity.tokenId,
148
+ chatId: turn.chatId,
149
+ ticketKey: request.context.ticketKey,
150
+ attachments: request.attachments.length,
151
+ ms: this.now() - started,
152
+ });
153
+ if (turn.interrupted) {
154
+ // A cancelled or failed turn does commit a partial message. Publishing
155
+ // it as "the answer" would put a truncated answer in front of a
156
+ // customer; the bridge's escalation path is exactly for this.
157
+ return this.escalated(turn.chatId, "turn interrupted");
158
+ }
159
+ const answer = boundAnswer(turn.answer.trim(), config.integration.maxAnswerCharacters);
160
+ if (answer === "") {
161
+ return this.escalated(turn.chatId, "the assistant produced no answer");
162
+ }
163
+ if (answer.length < turn.answer.trim().length) {
164
+ // The caller publishes into a comment with its own budget, so an
165
+ // over-long answer is cut here, at a boundary it can read, instead of
166
+ // there, mid-sentence and unannounced.
167
+ this.deps.logger.warn("integration.answer_truncated", {
168
+ tokenId: identity.tokenId,
169
+ chatId: turn.chatId,
170
+ published: answer.length,
171
+ });
172
+ }
173
+ return Object.freeze({
174
+ chatId: turn.chatId,
175
+ answer,
176
+ sources: sourceCitations(turn.sources),
177
+ // "high" is deliberately never claimed: no model-side judgment backs
178
+ // it, and a field that always says high teaches the operator to
179
+ // ignore it.
180
+ confidence: "medium",
181
+ escalate: false,
182
+ reason: "",
183
+ });
184
+ }
185
+ catch (error) {
186
+ if (deadline.signal.aborted) {
187
+ // The bridge's contract is a 90-second answer. Escalating with the
188
+ // chat id keeps the still-running turn reachable: a retry that
189
+ // started a new chat would orphan the answer being written.
190
+ this.deps.logger.warn("integration.timeout", {
191
+ tokenId: identity.tokenId,
192
+ chatId,
193
+ ms: this.now() - started,
194
+ });
195
+ return this.escalated(chatId ?? "", "the request timed out");
196
+ }
197
+ if (signal.aborted) {
198
+ throw new QaIntegrationError("unavailable", "the request was dropped");
199
+ }
200
+ if (error instanceof QaIntegrationAttachmentError) {
201
+ // The caller's own fallback signal: repeat the question without the
202
+ // attachment it sent. Answering anyway would publish an answer to a
203
+ // question the model never saw the material for.
204
+ this.deps.logger.warn("integration.attachment-refused", {
205
+ tokenId: identity.tokenId,
206
+ chatId,
207
+ name: error.attachment,
208
+ reason: error.reason,
209
+ });
210
+ throw new QaIntegrationError("unsupported-media", error.message, {
211
+ cause: error,
212
+ });
213
+ }
214
+ const refusal = admittedAttachmentRefusal(error);
215
+ if (refusal !== null) {
216
+ // The Harness refuses the prompt itself when it will not admit an
217
+ // attachment: an image its admission rejects (over its byte, pixel or
218
+ // dimension budget, bytes that are not the type the caller declared)
219
+ // or one the selected model cannot see, which is the same refusal for
220
+ // a deployment whose model has no vision. The caller can correct all
221
+ // of them by sending the question again without attachments, which is
222
+ // what the bridge's own 415 fallback does — a 503 would instead set a
223
+ // bridge whose tickets carry screenshots retrying the same refusal.
224
+ const code = /^[A-Z][A-Z0-9_]{2,63}$/u.test(refusal)
225
+ ? ` (${refusal})`
226
+ : "";
227
+ this.deps.logger.warn("integration.attachment-refused", {
228
+ tokenId: identity.tokenId,
229
+ chatId,
230
+ reason: refusal,
231
+ });
232
+ throw new QaIntegrationError("unsupported-media", `the attachment was refused${code}; repeat the request without attachments`, { cause: error });
233
+ }
234
+ this.deps.logger.error("integration.failed", {
235
+ tokenId: identity.tokenId,
236
+ chatId,
237
+ message: error instanceof Error ? error.message : String(error),
238
+ });
239
+ // No answer was produced and no chat may even exist: this is a Host
240
+ // failure, not an answer, so the bridge retries rather than escalating.
241
+ throw new QaIntegrationError("unavailable", "the QA assistant failed", {
242
+ cause: error,
243
+ });
244
+ }
245
+ finally {
246
+ clearTimeout(timer);
247
+ this.inFlight -= 1;
248
+ }
249
+ }
250
+ /**
251
+ * Read one conversation back for the application that started it.
252
+ *
253
+ * This is what `sessions:read` is for. A bridge that escalated a question,
254
+ * or that has to show a specialist what was already said, reads the chat it
255
+ * owns instead of asking the same question again — which would spend a turn
256
+ * and produce a second answer to a question that already has one.
257
+ *
258
+ * The ownership rule is the same one `ask` applies when it continues a chat:
259
+ * the account the token belongs to has to own the conversation. A chat that
260
+ * does not exist and a chat that belongs to somebody else get one answer, so
261
+ * an id alone never confirms that another account's conversation exists.
262
+ *
263
+ * @param authorization - the raw `Authorization` header value.
264
+ * @param query - the chat, the caller's cursor and the page size.
265
+ * @returns the window of messages, with the caller's new cursor.
266
+ */
267
+ async session(authorization, query) {
268
+ const config = this.deps.getConfig();
269
+ if (!config.integration.enabled) {
270
+ throw new QaIntegrationError("unavailable", "the QA integration API is disabled on this deployment");
271
+ }
272
+ const identity = this.authenticate(authorization, "sessions:read");
273
+ this.assertRate(identity.tokenId);
274
+ const owner = this.deps.accounts()?.ownerIdOf(query.chatId);
275
+ if (owner === undefined || owner !== identity.userId) {
276
+ throw new QaIntegrationError("not-found", "this account has no such conversation");
277
+ }
278
+ const transcript = await this.deps.runner.transcript(query);
279
+ this.deps.logger.info("integration.transcript-read", {
280
+ tokenId: identity.tokenId,
281
+ chatId: query.chatId,
282
+ after: query.after,
283
+ messages: transcript.messages.length,
284
+ });
285
+ return transcript;
286
+ }
287
+ /**
288
+ * Liveness and routing facts for the bridge's own UI.
289
+ * @param authorization - the raw `Authorization` header value.
290
+ * @returns the health payload; `ok` is true whenever it is answered at all.
291
+ */
292
+ async health(authorization) {
293
+ this.authenticate(authorization, null);
294
+ let models = [];
295
+ try {
296
+ models = await this.deps.runner.models();
297
+ }
298
+ catch (error) {
299
+ // A deployment with no routable provider is still a live deployment:
300
+ // the health answer is owed, and the missing catalog is not a failure.
301
+ this.deps.logger.warn("integration.models-unavailable", {
302
+ message: error instanceof Error ? error.message : String(error),
303
+ });
304
+ }
305
+ return Object.freeze({
306
+ ok: true,
307
+ version: this.deps.version,
308
+ models: Object.freeze([...models]),
309
+ uptimeS: Math.max(0, Math.floor((this.now() - this.startedAt) / 1000)),
310
+ });
311
+ }
312
+ /** One refusal shaped as an answer: empty text, escalated, no citations. */
313
+ escalated(chatId, reason) {
314
+ return Object.freeze({
315
+ chatId,
316
+ answer: "",
317
+ sources: Object.freeze([]),
318
+ confidence: "low",
319
+ escalate: true,
320
+ reason,
321
+ });
322
+ }
323
+ /** Refuse with `rate-limited` once a token spends its minute's budget. */
324
+ assertRate(tokenId) {
325
+ const limit = this.deps.getConfig().integration.requestsPerMinute;
326
+ const cutoff = this.now() - MINUTE_MS;
327
+ const kept = (this.recent.get(tokenId) ?? []).filter((at) => at > cutoff);
328
+ if (kept.length >= limit) {
329
+ this.recent.set(tokenId, kept);
330
+ throw new QaIntegrationError("rate-limited", "this token exceeded its requests-per-minute budget");
331
+ }
332
+ kept.push(this.now());
333
+ this.recent.set(tokenId, kept);
334
+ }
335
+ }
336
+ /**
337
+ * Why the Harness refused one prompt for its attachment, or null.
338
+ *
339
+ * The session controller folds every attachment failure — its own admission
340
+ * codes (`IMAGE_TOO_LARGE`, `IMAGE_TOO_MANY_PIXELS`, `IMAGE_TYPE_MISMATCH`, …)
341
+ * and a model that cannot take images at all — into one Remote failure,
342
+ * `session/attachment-invalid`, whose `reason` carries the code. The failure is
343
+ * identified structurally rather than with `instanceof`: it crosses the session
344
+ * controller's bundle boundary, which is exactly what `remoteErrorOf` exists
345
+ * for.
346
+ *
347
+ * @param error - the failure raised by the turn.
348
+ * @returns the Harness's reason code, or null when this is some other failure.
349
+ */
350
+ function admittedAttachmentRefusal(error) {
351
+ const failure = remoteErrorOf(error);
352
+ if (failure?.code !== "session/attachment-invalid")
353
+ return null;
354
+ const reason = failure.details?.reason;
355
+ return typeof reason === "string" ? reason : "attachment-refused";
356
+ }
357
+ /**
358
+ * The credential in one `Authorization` value, or null.
359
+ *
360
+ * `Bearer` is matched case-insensitively, as HTTP authentication schemes are,
361
+ * and the token itself is never trimmed: whitespace inside a credential is
362
+ * part of it, and accepting a padded variant would accept a token the store
363
+ * never minted.
364
+ * @param authorization - the raw header, when the caller sent one.
365
+ * @returns the token, or null.
366
+ */
367
+ export function bearerToken(authorization) {
368
+ if (typeof authorization !== "string")
369
+ return null;
370
+ const match = /^Bearer[ \t]+(\S+)$/iu.exec(authorization.trim());
371
+ return match?.[1] ?? null;
372
+ }
373
+ //# sourceMappingURL=service.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"service.js","sourceRoot":"","sources":["../../src/integration/service.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,202 @@
1
+ import { integrationMessageWindow, QA_INTEGRATION_MAX_TRANSCRIPT_MESSAGES, } from "./transcript.js";
2
+ /**
3
+ * A conversation read warm, for an application that pages through it.
4
+ *
5
+ * The projection itself (`transcript.ts`) answers one page from a log. That is
6
+ * the right shape for a review page opened once and the wrong one for a caller
7
+ * that follows a cursor: every read of a long chat would load the stored log
8
+ * and flatten every message in it to hand back the newest fifty, so a bridge
9
+ * paging through a two-year-old conversation pays for the whole conversation
10
+ * per page, and pays it again on the next poll.
11
+ *
12
+ * What is kept here is the window itself — the newest messages, already
13
+ * flattened — plus the newest seq below it, which is all the arithmetic a page
14
+ * needs to be exact. A page is then either served from that window or, when the
15
+ * window cannot answer for the caller's cursor, read from the log.
16
+ *
17
+ * Two things decide whether the window may be trusted:
18
+ *
19
+ * - **A chat this process holds is probed in memory.** The Harness is the
20
+ * writer, so its in-memory events answer \"has anything been appended?\" with
21
+ * no stored read at all, and only the appended events are projected. This is
22
+ * the common case: the bridge reads back the chat it just asked in.
23
+ * - **A chat this process does not hold is trusted for a bounded time.** Nobody
24
+ * in this process can append to it, so the window can only be wrong about a
25
+ * write from somewhere else sharing the same storage — which no page read can
26
+ * distinguish either. The bound is deliberately the same budget the review
27
+ * console gives its own transcript cache, so the two answer a reviewer the
28
+ * same way.
29
+ *
30
+ * A window is never *replaced* by an in-memory snapshot, only extended from it:
31
+ * a session that outlived a process restart, or a very long one, may hold only
32
+ * part of its history in memory, and a read that trusted it as the whole log
33
+ * would answer a page from half a conversation.
34
+ *
35
+ * What it costs: at most one window per conversation, and a window is at most
36
+ * one maximal page — so the memory this keeps is bounded by
37
+ * {@link QA_INTEGRATION_TRANSCRIPT_CACHE_CHATS} pages of conversation, and a
38
+ * page a caller would have received anyway. An old chat's *first* read is still
39
+ * a stored read, and its projection walks back only as far as the window it is
40
+ * building: the whole log of a long conversation is never flattened to answer a
41
+ * page of it.
42
+ */
43
+ /** How long a window is trusted for a chat this process does not hold. */
44
+ export const QA_INTEGRATION_TRANSCRIPT_TTL_MS = 30_000;
45
+ /** How many conversations keep a warm window. */
46
+ export const QA_INTEGRATION_TRANSCRIPT_CACHE_CHATS = 128;
47
+ const EMPTY_MESSAGES = Object.freeze([]);
48
+ export class QaIntegrationTranscriptReader {
49
+ windows = new Map();
50
+ log;
51
+ logger;
52
+ now;
53
+ ttlMs;
54
+ maxChats;
55
+ maxMessages;
56
+ constructor(options) {
57
+ this.log = options.log;
58
+ this.logger = options.logger;
59
+ this.now = options.now ?? (() => Date.now());
60
+ this.ttlMs = options.ttlMs ?? QA_INTEGRATION_TRANSCRIPT_TTL_MS;
61
+ this.maxChats = options.maxChats ?? QA_INTEGRATION_TRANSCRIPT_CACHE_CHATS;
62
+ this.maxMessages =
63
+ options.maxMessages ?? QA_INTEGRATION_MAX_TRANSCRIPT_MESSAGES;
64
+ }
65
+ /**
66
+ * One page of a conversation, from the window when the window can answer it.
67
+ * @param chatId - the conversation; the caller's ownership of it is the
68
+ * service's business, and this reader only reads it.
69
+ * @param query - the caller's exclusive cursor and the page size it wants.
70
+ * @returns the newest page after the cursor, with its new cursor.
71
+ */
72
+ async page(chatId, query) {
73
+ const window = this.windows.get(chatId);
74
+ if (window !== undefined) {
75
+ const held = this.log.snapshot(chatId, { after: this.covered(window) });
76
+ if (held === undefined) {
77
+ if (this.now() - window.at < this.ttlMs) {
78
+ return this.pageOf(chatId, window, query);
79
+ }
80
+ }
81
+ else if (this.merge(chatId, window, held)) {
82
+ return this.pageOf(chatId, window, query);
83
+ }
84
+ // Either the window aged out, or it met a chat whose new events it cannot
85
+ // absorb: both are answered from the log rather than from memory.
86
+ }
87
+ return await this.readLog(chatId, query);
88
+ }
89
+ /** One page out of a window: exact, whatever the caller's cursor is. */
90
+ pageOf(chatId, window, query) {
91
+ const limit = Math.min(query.limit, this.maxMessages);
92
+ const inside = window.messages.filter((message) => message.seq > query.after);
93
+ const messages = inside.length > limit ? inside.slice(inside.length - limit) : inside;
94
+ // A caller whose cursor fell below the window still gets the newest page —
95
+ // its answer is the window's tail — but there are messages it has not seen
96
+ // below the window, and that is what truncation reports.
97
+ const older = window.below !== undefined && window.below > query.after;
98
+ return Object.freeze({
99
+ chatId,
100
+ messages: Object.freeze([...messages]),
101
+ lastSeq: messages.at(-1)?.seq ?? query.after,
102
+ truncated: older || inside.length > limit,
103
+ });
104
+ }
105
+ /**
106
+ * Fold what the Harness is holding into a window, projecting only the events
107
+ * the window has not covered yet.
108
+ * @returns whether the window absorbed the update; false means the delta is
109
+ * larger than a whole window, so the log has to answer instead.
110
+ */
111
+ merge(chatId, window, events) {
112
+ const delta = integrationMessageWindow(chatId, events, {
113
+ after: this.covered(window),
114
+ cover: this.maxMessages,
115
+ });
116
+ if (delta.messages.length === 0) {
117
+ // Nothing was appended, and the window is current as of now.
118
+ window.at = this.now();
119
+ return true;
120
+ }
121
+ const combined = [...window.messages, ...delta.messages];
122
+ if (combined.length > this.maxMessages)
123
+ return false;
124
+ window.messages = Object.freeze(combined);
125
+ if (delta.below !== undefined) {
126
+ // The newest seq left below the *window*: the delta's own cut is newer
127
+ // than anything the previous window had already left behind.
128
+ window.below = delta.below;
129
+ }
130
+ window.at = this.now();
131
+ return true;
132
+ }
133
+ /**
134
+ * The newest published seq a window has covered, which is also the cursor a
135
+ * probe is asked from: `undefined` while the window holds nothing, where the
136
+ * question is "what has this chat published at all".
137
+ */
138
+ covered(window) {
139
+ return window.messages.at(-1)?.seq;
140
+ }
141
+ /** Read the log and remember what it says, for the pages that follow. */
142
+ async readLog(chatId, query) {
143
+ const read = await this.log.read(chatId);
144
+ if (!read.ok) {
145
+ this.windows.delete(chatId);
146
+ if (read.reason === "not-found") {
147
+ // The ownership record is written before the first message, so a chat
148
+ // that exists but has written nothing is an empty conversation rather
149
+ // than a failure. Nothing is remembered: it may write any moment.
150
+ return Object.freeze({
151
+ chatId,
152
+ messages: EMPTY_MESSAGES,
153
+ lastSeq: query.after,
154
+ truncated: false,
155
+ });
156
+ }
157
+ throw new Error(`the session log is unavailable (${read.reason})`);
158
+ }
159
+ const base = integrationMessageWindow(chatId, read.events, {
160
+ after: undefined,
161
+ cover: this.maxMessages,
162
+ });
163
+ const window = {
164
+ at: this.now(),
165
+ messages: base.messages,
166
+ ...(base.below === undefined ? {} : { below: base.below }),
167
+ };
168
+ // Storage lags the session it belongs to, so a chat this process holds is
169
+ // extended with what memory has already: the alternative is answering a
170
+ // page with a turn the Harness has written but not flushed.
171
+ const held = this.log.snapshot(chatId, { after: this.covered(window) });
172
+ if (held !== undefined && !this.merge(chatId, window, held)) {
173
+ // A window cannot hold more than a page, and a chat that wrote more than
174
+ // that between two reads is read rather than merged. The window is still
175
+ // exact for its own messages; the newest ones arrive with the next read.
176
+ this.logger?.warn("integration.transcript-window-behind", {
177
+ chatId,
178
+ below: window.below,
179
+ });
180
+ }
181
+ this.remember(chatId, window);
182
+ return this.pageOf(chatId, window, query);
183
+ }
184
+ /** Keep a bounded number of conversations warm, oldest window first. */
185
+ remember(chatId, window) {
186
+ this.windows.set(chatId, window);
187
+ while (this.windows.size > this.maxChats) {
188
+ let oldestId;
189
+ let oldestAt = Number.POSITIVE_INFINITY;
190
+ for (const [id, entry] of this.windows) {
191
+ if (entry.at < oldestAt) {
192
+ oldestAt = entry.at;
193
+ oldestId = id;
194
+ }
195
+ }
196
+ if (oldestId === undefined)
197
+ return;
198
+ this.windows.delete(oldestId);
199
+ }
200
+ }
201
+ }
202
+ //# sourceMappingURL=transcript-reader.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"transcript-reader.js","sourceRoot":"","sources":["../../src/integration/transcript-reader.ts"],"names":[],"mappings":"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"}