@yadsh/dsh-qa-surface 0.10.0 → 0.11.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/README.md +251 -12
- package/lib/access/service.js +49 -10
- package/lib/access/service.js.map +1 -1
- package/lib/account-remotes.js +41 -0
- package/lib/account-remotes.js.map +1 -1
- package/lib/accounts/database.js +140 -0
- package/lib/accounts/database.js.map +1 -1
- package/lib/accounts/errors.js.map +1 -1
- package/lib/accounts/service-token.js +103 -0
- package/lib/accounts/service-token.js.map +1 -0
- package/lib/accounts/store.js +291 -5
- package/lib/accounts/store.js.map +1 -1
- package/lib/admin/conversation-log.js +14 -8
- package/lib/admin/conversation-log.js.map +1 -1
- package/lib/admin/permissions.js +5 -0
- package/lib/admin/permissions.js.map +1 -1
- package/lib/admin/quality-store.js +4 -0
- package/lib/admin/quality-store.js.map +1 -1
- package/lib/admin/service.js +173 -0
- package/lib/admin/service.js.map +1 -1
- package/lib/admin/session-log.js +39 -8
- package/lib/admin/session-log.js.map +1 -1
- package/lib/attachment-rules.js +39 -6
- package/lib/attachment-rules.js.map +1 -1
- package/lib/cli.js +83 -1
- package/lib/cli.js.map +1 -1
- package/lib/client/QaAccountsController.js +213 -4
- package/lib/client/QaAccountsController.js.map +1 -1
- package/lib/client/QaSessionController.js +37 -21
- package/lib/client/QaSessionController.js.map +1 -1
- package/lib/client/QaSurface.js +28 -4
- package/lib/client/QaSurface.js.map +1 -1
- package/lib/client/admin/QaAdmin.js +9 -1
- package/lib/client/admin/QaAdmin.js.map +1 -1
- package/lib/client/admin/pages/SkillFiles.js +78 -0
- package/lib/client/admin/pages/SkillFiles.js.map +1 -0
- package/lib/client/admin/pages/Users.js +65 -1
- package/lib/client/admin/pages/Users.js.map +1 -1
- package/lib/client/admin/routes.js +8 -1
- package/lib/client/admin/routes.js.map +1 -1
- package/lib/client/admin/styles.js +2 -0
- package/lib/client/admin/styles.js.map +1 -1
- package/lib/client/attachments.js +41 -11
- package/lib/client/attachments.js.map +1 -1
- package/lib/client/components/QaAuthGate.js +19 -10
- package/lib/client/components/QaAuthGate.js.map +1 -1
- package/lib/client/components/QaChangelog.js +27 -0
- package/lib/client/components/QaChangelog.js.map +1 -1
- package/lib/client/index.js +13 -0
- package/lib/client/index.js.map +1 -1
- package/lib/client/lineage.js +64 -0
- package/lib/client/lineage.js.map +1 -1
- package/lib/client/settings/sections/attachments.js +6 -5
- package/lib/client/settings/sections/attachments.js.map +1 -1
- package/lib/client/settings/sections/notes.js +22 -4
- package/lib/client/settings/sections/notes.js.map +1 -1
- package/lib/client/styles.js +9 -0
- package/lib/client/styles.js.map +1 -1
- package/lib/client/types.js.map +1 -1
- package/lib/client/user-settings/IntegrationTokensPage.js +150 -0
- package/lib/client/user-settings/IntegrationTokensPage.js.map +1 -0
- package/lib/client/user-settings/PasswordSettingsPage.js +48 -0
- package/lib/client/user-settings/PasswordSettingsPage.js.map +1 -0
- package/lib/client/user-settings/SkillCatalog.js +2 -2
- package/lib/client/user-settings/SkillCatalog.js.map +1 -1
- package/lib/client/user-settings/UserSettingsDialog.js +17 -2
- package/lib/client/user-settings/UserSettingsDialog.js.map +1 -1
- package/lib/client/user-settings/copy.js +20 -0
- package/lib/client/user-settings/copy.js.map +1 -1
- package/lib/client.js +2466 -137
- package/lib/client.js.map +1 -1
- package/lib/config-resolvers/attachments.js +6 -5
- package/lib/config-resolvers/attachments.js.map +1 -1
- package/lib/config-resolvers/defaults.js +23 -2
- package/lib/config-resolvers/defaults.js.map +1 -1
- package/lib/config-resolvers/integration.js +94 -0
- package/lib/config-resolvers/integration.js.map +1 -0
- package/lib/config-resolvers/notes.js +2 -0
- package/lib/config-resolvers/notes.js.map +1 -1
- package/lib/config.js +72 -0
- package/lib/config.js.map +1 -1
- package/lib/index.js +197 -2
- package/lib/index.js.map +1 -1
- package/lib/integration/answer.js +100 -0
- package/lib/integration/answer.js.map +1 -0
- package/lib/integration/attachments.js +231 -0
- package/lib/integration/attachments.js.map +1 -0
- package/lib/integration/contract.js +53 -0
- package/lib/integration/contract.js.map +1 -0
- package/lib/integration/host-runner.js +243 -0
- package/lib/integration/host-runner.js.map +1 -0
- package/lib/integration/http.js +538 -0
- package/lib/integration/http.js.map +1 -0
- package/lib/integration/service.js +331 -0
- package/lib/integration/service.js.map +1 -0
- package/lib/integration/transcript-reader.js +202 -0
- package/lib/integration/transcript-reader.js.map +1 -0
- package/lib/integration/transcript.js +148 -0
- package/lib/integration/transcript.js.map +1 -0
- package/lib/personal-skills/index.js +2 -2
- package/lib/personal-skills/index.js.map +1 -1
- package/lib/personal-skills/paths.js +14 -8
- package/lib/personal-skills/paths.js.map +1 -1
- package/lib/personal-skills/provider.js +16 -4
- package/lib/personal-skills/provider.js.map +1 -1
- package/lib/personal-skills/service.js +240 -30
- package/lib/personal-skills/service.js.map +1 -1
- package/lib/prompt-notes.js +58 -0
- package/lib/prompt-notes.js.map +1 -1
- package/lib/provenance/document-preview.js +10 -1
- package/lib/provenance/document-preview.js.map +1 -1
- package/lib/qa-tools/catalog.js +12 -1
- package/lib/qa-tools/catalog.js.map +1 -1
- package/lib/qa-tools/docs-tools.js +563 -0
- package/lib/qa-tools/docs-tools.js.map +1 -0
- package/lib/resolve-config.js +4 -0
- package/lib/resolve-config.js.map +1 -1
- package/lib/secure-session.js +83 -3
- package/lib/secure-session.js.map +1 -1
- package/lib/shared/integration-tokens.js +23 -0
- package/lib/shared/integration-tokens.js.map +1 -0
- package/lib/typert.host.js +1176 -213
- package/lib/typert.remote-client.d.ts +27 -1
- package/lib/typert.remote-client.js +911 -69
- package/lib/types/access/service.d.ts +33 -2
- package/lib/types/account-remotes.d.ts +30 -1
- package/lib/types/accounts/database.d.ts +53 -0
- package/lib/types/accounts/errors.d.ts +5 -1
- package/lib/types/accounts/service-token.d.ts +70 -0
- package/lib/types/accounts/store.d.ts +131 -1
- package/lib/types/admin/conversation-log.d.ts +12 -0
- package/lib/types/admin/service.d.ts +56 -1
- package/lib/types/admin/session-log.d.ts +21 -0
- package/lib/types/attachment-rules.d.ts +26 -3
- package/lib/types/client/QaAccountsController.d.ts +64 -5
- package/lib/types/client/QaSessionController.d.ts +24 -2
- package/lib/types/client/admin/pages/SkillFiles.d.ts +17 -0
- package/lib/types/client/admin/routes.d.ts +2 -0
- package/lib/types/client/attachments.d.ts +21 -4
- package/lib/types/client/lineage.d.ts +42 -0
- package/lib/types/client/settings/sections/notes.d.ts +5 -3
- package/lib/types/client/types.d.ts +71 -1
- package/lib/types/client/user-settings/IntegrationTokensPage.d.ts +21 -0
- package/lib/types/client/user-settings/PasswordSettingsPage.d.ts +19 -0
- package/lib/types/client/user-settings/UserSettingsDialog.d.ts +11 -2
- package/lib/types/client/user-settings/copy.d.ts +10 -1
- package/lib/types/config-resolvers/integration.d.ts +47 -0
- package/lib/types/config.d.ts +10 -0
- package/lib/types/index.d.ts +73 -1
- package/lib/types/integration/answer.d.ts +65 -0
- package/lib/types/integration/attachments.d.ts +85 -0
- package/lib/types/integration/contract.d.ts +196 -0
- package/lib/types/integration/host-runner.d.ts +44 -0
- package/lib/types/integration/http.d.ts +79 -0
- package/lib/types/integration/service.d.ts +120 -0
- package/lib/types/integration/transcript-reader.d.ts +100 -0
- package/lib/types/integration/transcript.d.ts +59 -0
- package/lib/types/personal-skills/index.d.ts +2 -2
- package/lib/types/personal-skills/paths.d.ts +15 -3
- package/lib/types/personal-skills/provider.d.ts +14 -2
- package/lib/types/personal-skills/service.d.ts +89 -14
- package/lib/types/prompt-notes.d.ts +28 -0
- package/lib/types/provenance/document-preview.d.ts +4 -0
- package/lib/types/qa-tools/catalog.d.ts +1 -1
- package/lib/types/qa-tools/docs-tools.d.ts +160 -0
- package/lib/types/secure-session.d.ts +52 -1
- package/lib/types/shared/integration-tokens.d.ts +24 -0
- package/lib/types/types.d.ts +170 -2
- package/package.json +4 -4
|
@@ -0,0 +1,331 @@
|
|
|
1
|
+
import { parseServiceToken, } from "../accounts/service-token.js";
|
|
2
|
+
import { boundAnswer } from "./answer.js";
|
|
3
|
+
import { QaIntegrationAttachmentError } from "./attachments.js";
|
|
4
|
+
import { QaIntegrationError, } from "./contract.js";
|
|
5
|
+
/**
|
|
6
|
+
* The integration API's brain: credential, budget, and one answered turn.
|
|
7
|
+
*
|
|
8
|
+
* The service owns everything that must be identical for every transport —
|
|
9
|
+
* who is allowed to ask, how often, how many at once, what a refusal means and
|
|
10
|
+
* how an answered turn is shaped into the contract. The HTTP layer owns bytes
|
|
11
|
+
* and status codes, the runner owns sessions and agents. That split is what
|
|
12
|
+
* lets the mechanics be tested without a web server, a model or a database.
|
|
13
|
+
*
|
|
14
|
+
* Two limits, deliberately different in kind. Concurrency (`maxConcurrent`)
|
|
15
|
+
* protects the deployment: a bridge that fires a hundred questions must not
|
|
16
|
+
* open a hundred agent turns. Rate (`requestsPerMinute`) protects the account:
|
|
17
|
+
* one token cannot spend the deployment's whole budget. Both refuse with 429,
|
|
18
|
+
* which the bridge already retries.
|
|
19
|
+
*/
|
|
20
|
+
/** The bridge publishes a handful of citations; more is noise in a Jira note. */
|
|
21
|
+
export const QA_INTEGRATION_MAX_CITATIONS = 5;
|
|
22
|
+
const MINUTE_MS = 60_000;
|
|
23
|
+
/**
|
|
24
|
+
* One citation string per source, in the bridge's own style: the document plus
|
|
25
|
+
* where in it the evidence sits, so a reader can open the right page.
|
|
26
|
+
*
|
|
27
|
+
* @param sources - the turn's evidence, best first.
|
|
28
|
+
* @returns bounded citation strings.
|
|
29
|
+
*/
|
|
30
|
+
export function sourceCitations(sources) {
|
|
31
|
+
const citations = [];
|
|
32
|
+
for (const source of sources) {
|
|
33
|
+
if (citations.length >= QA_INTEGRATION_MAX_CITATIONS)
|
|
34
|
+
break;
|
|
35
|
+
const location = source.locations?.[0];
|
|
36
|
+
const base = source.uri ?? source.path ?? source.title;
|
|
37
|
+
let citation = base;
|
|
38
|
+
if (location !== undefined) {
|
|
39
|
+
if (location.anchor !== undefined && location.anchor !== "") {
|
|
40
|
+
citation = `${base}#${location.anchor}`;
|
|
41
|
+
}
|
|
42
|
+
else if (location.lineStart !== undefined) {
|
|
43
|
+
citation =
|
|
44
|
+
location.lineEnd === undefined ||
|
|
45
|
+
location.lineEnd === location.lineStart
|
|
46
|
+
? `${base}:${String(location.lineStart)}`
|
|
47
|
+
: `${base}:${String(location.lineStart)}-${String(location.lineEnd)}`;
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
if (citation === "" || citations.includes(citation))
|
|
51
|
+
continue;
|
|
52
|
+
citations.push(citation);
|
|
53
|
+
}
|
|
54
|
+
return Object.freeze(citations);
|
|
55
|
+
}
|
|
56
|
+
export class QaIntegrationService {
|
|
57
|
+
deps;
|
|
58
|
+
now;
|
|
59
|
+
startedAt;
|
|
60
|
+
/** Recent request timestamps per token id, pruned to the rolling minute. */
|
|
61
|
+
recent = new Map();
|
|
62
|
+
inFlight = 0;
|
|
63
|
+
constructor(deps) {
|
|
64
|
+
this.deps = deps;
|
|
65
|
+
this.now = deps.now ?? Date.now;
|
|
66
|
+
this.startedAt = this.now();
|
|
67
|
+
}
|
|
68
|
+
/** Requests currently inside `ask`; the console shows this as load. */
|
|
69
|
+
get busy() {
|
|
70
|
+
return this.inFlight;
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Resolve a bearer credential to its token, or refuse.
|
|
74
|
+
*
|
|
75
|
+
* A malformed, unknown, expired, revoked or disabled-account token is one
|
|
76
|
+
* answer — 401 — because telling them apart would let an unauthenticated
|
|
77
|
+
* caller enumerate token ids. A valid token missing the scope is 403: the
|
|
78
|
+
* caller proved who it is, and the refusal is about rights, not identity.
|
|
79
|
+
*/
|
|
80
|
+
authenticate(authorization, required) {
|
|
81
|
+
const token = bearerToken(authorization);
|
|
82
|
+
if (token === null) {
|
|
83
|
+
throw new QaIntegrationError("unauthorized", "a Bearer integration token is required");
|
|
84
|
+
}
|
|
85
|
+
const accounts = this.deps.accounts();
|
|
86
|
+
if (accounts === undefined) {
|
|
87
|
+
throw new QaIntegrationError("unavailable", "QA accounts are not enabled on this deployment");
|
|
88
|
+
}
|
|
89
|
+
// The shape check happens before the store: a foreign credential must not
|
|
90
|
+
// even reach a database lookup.
|
|
91
|
+
if (parseServiceToken(token) === null) {
|
|
92
|
+
throw new QaIntegrationError("unauthorized", "the token is not accepted");
|
|
93
|
+
}
|
|
94
|
+
const verified = accounts.verifyServiceToken(token);
|
|
95
|
+
if (verified === null) {
|
|
96
|
+
throw new QaIntegrationError("unauthorized", "the token is expired, revoked or unknown");
|
|
97
|
+
}
|
|
98
|
+
if (required !== null && !verified.scopes.includes(required)) {
|
|
99
|
+
throw new QaIntegrationError("forbidden", `the token lacks the ${required} scope`);
|
|
100
|
+
}
|
|
101
|
+
return Object.freeze({
|
|
102
|
+
tokenId: verified.tokenId,
|
|
103
|
+
userId: verified.userId,
|
|
104
|
+
scopes: verified.scopes,
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Answer one question, or refuse before a turn is ever opened.
|
|
109
|
+
*
|
|
110
|
+
* @param authorization - the raw `Authorization` header value.
|
|
111
|
+
* @param request - the parsed body.
|
|
112
|
+
* @param signal - the request's own lifetime; a dropped connection stops the wait.
|
|
113
|
+
* @returns the answer the bridge publishes.
|
|
114
|
+
*/
|
|
115
|
+
async ask(authorization, request, signal) {
|
|
116
|
+
const config = this.deps.getConfig();
|
|
117
|
+
if (!config.integration.enabled) {
|
|
118
|
+
throw new QaIntegrationError("unavailable", "the QA integration API is disabled on this deployment");
|
|
119
|
+
}
|
|
120
|
+
const identity = this.authenticate(authorization, "ask");
|
|
121
|
+
this.assertRate(identity.tokenId);
|
|
122
|
+
if (this.inFlight >= config.integration.maxConcurrent) {
|
|
123
|
+
throw new QaIntegrationError("busy", "too many integration requests are being answered right now");
|
|
124
|
+
}
|
|
125
|
+
const started = this.now();
|
|
126
|
+
const deadline = new AbortController();
|
|
127
|
+
const timer = setTimeout(() => {
|
|
128
|
+
deadline.abort(new Error("integration request timed out"));
|
|
129
|
+
}, config.integration.requestTimeoutMs);
|
|
130
|
+
const combined = AbortSignal.any([signal, deadline.signal]);
|
|
131
|
+
let chatId = request.sessionId;
|
|
132
|
+
this.inFlight += 1;
|
|
133
|
+
try {
|
|
134
|
+
const turn = await this.deps.runner.run({
|
|
135
|
+
userId: identity.userId,
|
|
136
|
+
chatId: request.sessionId,
|
|
137
|
+
message: request.message,
|
|
138
|
+
attachments: request.attachments,
|
|
139
|
+
ticketKey: request.context.ticketKey,
|
|
140
|
+
signal: combined,
|
|
141
|
+
onChat: (id) => {
|
|
142
|
+
chatId = id;
|
|
143
|
+
},
|
|
144
|
+
});
|
|
145
|
+
this.deps.logger.info("integration.answered", {
|
|
146
|
+
tokenId: identity.tokenId,
|
|
147
|
+
chatId: turn.chatId,
|
|
148
|
+
ticketKey: request.context.ticketKey,
|
|
149
|
+
attachments: request.attachments.length,
|
|
150
|
+
ms: this.now() - started,
|
|
151
|
+
});
|
|
152
|
+
if (turn.interrupted) {
|
|
153
|
+
// A cancelled or failed turn does commit a partial message. Publishing
|
|
154
|
+
// it as "the answer" would put a truncated answer in front of a
|
|
155
|
+
// customer; the bridge's escalation path is exactly for this.
|
|
156
|
+
return this.escalated(turn.chatId, "turn interrupted");
|
|
157
|
+
}
|
|
158
|
+
const answer = boundAnswer(turn.answer.trim(), config.integration.maxAnswerCharacters);
|
|
159
|
+
if (answer === "") {
|
|
160
|
+
return this.escalated(turn.chatId, "the assistant produced no answer");
|
|
161
|
+
}
|
|
162
|
+
if (answer.length < turn.answer.trim().length) {
|
|
163
|
+
// The caller publishes into a comment with its own budget, so an
|
|
164
|
+
// over-long answer is cut here, at a boundary it can read, instead of
|
|
165
|
+
// there, mid-sentence and unannounced.
|
|
166
|
+
this.deps.logger.warn("integration.answer_truncated", {
|
|
167
|
+
tokenId: identity.tokenId,
|
|
168
|
+
chatId: turn.chatId,
|
|
169
|
+
published: answer.length,
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
return Object.freeze({
|
|
173
|
+
chatId: turn.chatId,
|
|
174
|
+
answer,
|
|
175
|
+
sources: sourceCitations(turn.sources),
|
|
176
|
+
// "high" is deliberately never claimed: no model-side judgment backs
|
|
177
|
+
// it, and a field that always says high teaches the operator to
|
|
178
|
+
// ignore it.
|
|
179
|
+
confidence: "medium",
|
|
180
|
+
escalate: false,
|
|
181
|
+
reason: "",
|
|
182
|
+
});
|
|
183
|
+
}
|
|
184
|
+
catch (error) {
|
|
185
|
+
if (deadline.signal.aborted) {
|
|
186
|
+
// The bridge's contract is a 90-second answer. Escalating with the
|
|
187
|
+
// chat id keeps the still-running turn reachable: a retry that
|
|
188
|
+
// started a new chat would orphan the answer being written.
|
|
189
|
+
this.deps.logger.warn("integration.timeout", {
|
|
190
|
+
tokenId: identity.tokenId,
|
|
191
|
+
chatId,
|
|
192
|
+
ms: this.now() - started,
|
|
193
|
+
});
|
|
194
|
+
return this.escalated(chatId ?? "", "the request timed out");
|
|
195
|
+
}
|
|
196
|
+
if (signal.aborted) {
|
|
197
|
+
throw new QaIntegrationError("unavailable", "the request was dropped");
|
|
198
|
+
}
|
|
199
|
+
if (error instanceof QaIntegrationAttachmentError) {
|
|
200
|
+
// The caller's own fallback signal: repeat the question without the
|
|
201
|
+
// attachment it sent. Answering anyway would publish an answer to a
|
|
202
|
+
// question the model never saw the material for.
|
|
203
|
+
this.deps.logger.warn("integration.attachment-refused", {
|
|
204
|
+
tokenId: identity.tokenId,
|
|
205
|
+
chatId,
|
|
206
|
+
name: error.attachment,
|
|
207
|
+
reason: error.reason,
|
|
208
|
+
});
|
|
209
|
+
throw new QaIntegrationError("unsupported-media", error.message, {
|
|
210
|
+
cause: error,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
this.deps.logger.error("integration.failed", {
|
|
214
|
+
tokenId: identity.tokenId,
|
|
215
|
+
chatId,
|
|
216
|
+
message: error instanceof Error ? error.message : String(error),
|
|
217
|
+
});
|
|
218
|
+
// No answer was produced and no chat may even exist: this is a Host
|
|
219
|
+
// failure, not an answer, so the bridge retries rather than escalating.
|
|
220
|
+
throw new QaIntegrationError("unavailable", "the QA assistant failed", {
|
|
221
|
+
cause: error,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
finally {
|
|
225
|
+
clearTimeout(timer);
|
|
226
|
+
this.inFlight -= 1;
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* Read one conversation back for the application that started it.
|
|
231
|
+
*
|
|
232
|
+
* This is what `sessions:read` is for. A bridge that escalated a question,
|
|
233
|
+
* or that has to show a specialist what was already said, reads the chat it
|
|
234
|
+
* owns instead of asking the same question again — which would spend a turn
|
|
235
|
+
* and produce a second answer to a question that already has one.
|
|
236
|
+
*
|
|
237
|
+
* The ownership rule is the same one `ask` applies when it continues a chat:
|
|
238
|
+
* the account the token belongs to has to own the conversation. A chat that
|
|
239
|
+
* does not exist and a chat that belongs to somebody else get one answer, so
|
|
240
|
+
* an id alone never confirms that another account's conversation exists.
|
|
241
|
+
*
|
|
242
|
+
* @param authorization - the raw `Authorization` header value.
|
|
243
|
+
* @param query - the chat, the caller's cursor and the page size.
|
|
244
|
+
* @returns the window of messages, with the caller's new cursor.
|
|
245
|
+
*/
|
|
246
|
+
async session(authorization, query) {
|
|
247
|
+
const config = this.deps.getConfig();
|
|
248
|
+
if (!config.integration.enabled) {
|
|
249
|
+
throw new QaIntegrationError("unavailable", "the QA integration API is disabled on this deployment");
|
|
250
|
+
}
|
|
251
|
+
const identity = this.authenticate(authorization, "sessions:read");
|
|
252
|
+
this.assertRate(identity.tokenId);
|
|
253
|
+
const owner = this.deps.accounts()?.ownerIdOf(query.chatId);
|
|
254
|
+
if (owner === undefined || owner !== identity.userId) {
|
|
255
|
+
throw new QaIntegrationError("not-found", "this account has no such conversation");
|
|
256
|
+
}
|
|
257
|
+
const transcript = await this.deps.runner.transcript(query);
|
|
258
|
+
this.deps.logger.info("integration.transcript-read", {
|
|
259
|
+
tokenId: identity.tokenId,
|
|
260
|
+
chatId: query.chatId,
|
|
261
|
+
after: query.after,
|
|
262
|
+
messages: transcript.messages.length,
|
|
263
|
+
});
|
|
264
|
+
return transcript;
|
|
265
|
+
}
|
|
266
|
+
/**
|
|
267
|
+
* Liveness and routing facts for the bridge's own UI.
|
|
268
|
+
* @param authorization - the raw `Authorization` header value.
|
|
269
|
+
* @returns the health payload; `ok` is true whenever it is answered at all.
|
|
270
|
+
*/
|
|
271
|
+
async health(authorization) {
|
|
272
|
+
this.authenticate(authorization, null);
|
|
273
|
+
let models = [];
|
|
274
|
+
try {
|
|
275
|
+
models = await this.deps.runner.models();
|
|
276
|
+
}
|
|
277
|
+
catch (error) {
|
|
278
|
+
// A deployment with no routable provider is still a live deployment:
|
|
279
|
+
// the health answer is owed, and the missing catalog is not a failure.
|
|
280
|
+
this.deps.logger.warn("integration.models-unavailable", {
|
|
281
|
+
message: error instanceof Error ? error.message : String(error),
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
return Object.freeze({
|
|
285
|
+
ok: true,
|
|
286
|
+
version: this.deps.version,
|
|
287
|
+
models: Object.freeze([...models]),
|
|
288
|
+
uptimeS: Math.max(0, Math.floor((this.now() - this.startedAt) / 1000)),
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
/** One refusal shaped as an answer: empty text, escalated, no citations. */
|
|
292
|
+
escalated(chatId, reason) {
|
|
293
|
+
return Object.freeze({
|
|
294
|
+
chatId,
|
|
295
|
+
answer: "",
|
|
296
|
+
sources: Object.freeze([]),
|
|
297
|
+
confidence: "low",
|
|
298
|
+
escalate: true,
|
|
299
|
+
reason,
|
|
300
|
+
});
|
|
301
|
+
}
|
|
302
|
+
/** Refuse with `rate-limited` once a token spends its minute's budget. */
|
|
303
|
+
assertRate(tokenId) {
|
|
304
|
+
const limit = this.deps.getConfig().integration.requestsPerMinute;
|
|
305
|
+
const cutoff = this.now() - MINUTE_MS;
|
|
306
|
+
const kept = (this.recent.get(tokenId) ?? []).filter((at) => at > cutoff);
|
|
307
|
+
if (kept.length >= limit) {
|
|
308
|
+
this.recent.set(tokenId, kept);
|
|
309
|
+
throw new QaIntegrationError("rate-limited", "this token exceeded its requests-per-minute budget");
|
|
310
|
+
}
|
|
311
|
+
kept.push(this.now());
|
|
312
|
+
this.recent.set(tokenId, kept);
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
/**
|
|
316
|
+
* The credential in one `Authorization` value, or null.
|
|
317
|
+
*
|
|
318
|
+
* `Bearer` is matched case-insensitively, as HTTP authentication schemes are,
|
|
319
|
+
* and the token itself is never trimmed: whitespace inside a credential is
|
|
320
|
+
* part of it, and accepting a padded variant would accept a token the store
|
|
321
|
+
* never minted.
|
|
322
|
+
* @param authorization - the raw header, when the caller sent one.
|
|
323
|
+
* @returns the token, or null.
|
|
324
|
+
*/
|
|
325
|
+
export function bearerToken(authorization) {
|
|
326
|
+
if (typeof authorization !== "string")
|
|
327
|
+
return null;
|
|
328
|
+
const match = /^Bearer[ \t]+(\S+)$/iu.exec(authorization.trim());
|
|
329
|
+
return match?.[1] ?? null;
|
|
330
|
+
}
|
|
331
|
+
//# 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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"}
|