@xfey/tutti 0.1.127 → 0.1.129
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/dist/approvals/decision-command.d.ts +2 -2
- package/dist/chat-assistant/diagnostics.d.ts +17 -3
- package/dist/chat-assistant/diagnostics.js +5 -2
- package/dist/chat-assistant/failure-messages.d.ts +4 -0
- package/dist/chat-assistant/failure-messages.js +57 -0
- package/dist/chat-assistant/failures.d.ts +27 -0
- package/dist/chat-assistant/failures.js +46 -0
- package/dist/chat-assistant/index.d.ts +18 -4
- package/dist/chat-assistant/index.js +254 -51
- package/dist/chat-assistant/language-context.d.ts +2 -0
- package/dist/chat-assistant/language-context.js +17 -14
- package/dist/chat-assistant/requests.d.ts +34 -0
- package/dist/chat-assistant/requests.js +64 -0
- package/dist/collaboration-ingestion/clarification.js +3 -1
- package/dist/collaboration-ingestion/main-chat.js +1 -0
- package/dist/collaboration-ingestion/types.d.ts +2 -0
- package/dist/collaboration-state/clarification-snapshot.d.ts +5 -5
- package/dist/collaboration-state/context-sync-attempts.d.ts +9 -0
- package/dist/collaboration-state/context-sync-attempts.js +13 -0
- package/dist/collaboration-state/task-compile-context.d.ts +1 -1
- package/dist/collaboration-state/task-compile-context.js +11 -1
- package/dist/collaboration-state/task-compile-requests.d.ts +6 -0
- package/dist/collaboration-state/task-compile-requests.js +16 -0
- package/dist/communication-language/notice-locale.d.ts +4 -0
- package/dist/communication-language/notice-locale.js +14 -0
- package/dist/control-plane/context-sync-completion.d.ts +7 -0
- package/dist/control-plane/context-sync-completion.js +27 -0
- package/dist/control-plane/context-sync.d.ts +1 -0
- package/dist/control-plane/context-sync.js +23 -0
- package/dist/control-plane/index.d.ts +13 -4
- package/dist/control-plane/index.js +30 -23
- package/dist/control-plane/task-compile-continuation.js +2 -0
- package/dist/control-plane/task-compile-request-status.d.ts +6 -0
- package/dist/control-plane/task-compile-request-status.js +70 -0
- package/dist/control-plane/task-compile-start.js +40 -2
- package/dist/control-plane/types.d.ts +2 -0
- package/dist/providers/failure-classification.d.ts +23 -0
- package/dist/providers/failure-classification.js +43 -0
- package/dist/providers/invocation-progress.d.ts +6 -0
- package/dist/providers/invocation-progress.js +24 -0
- package/dist/providers/openai/app-server/read-only-procedure.js +1 -0
- package/dist/providers/openai/chat-assistant.js +2 -1
- package/dist/reference-access/read-evidence.d.ts +2 -0
- package/dist/reference-access/read-evidence.js +5 -0
- package/dist/reference-access/service.d.ts +2 -0
- package/dist/reference-access/service.js +2 -0
- package/dist/runtime-failures.js +15 -1
- package/dist/server-shell/cli/external-reference-runtime.js +6 -3
- package/dist/server-shell/cli/host-server-runtime.js +8 -2
- package/dist/server-shell/cli/relay-registration.js +1 -0
- package/dist/server-shell/http/routes/project-api/messages-routes.js +22 -1
- package/dist/server-shell/http/routes/project-api/openapi-collaboration-routes.d.ts +140 -5
- package/dist/server-shell/http/routes/project-api/openapi-collaboration-routes.js +29 -2
- package/dist/server-shell/http/routes/project-api/openapi-skills-routes.d.ts +2 -0
- package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +6 -0
- package/dist/server-shell/http/routes/project-api/openapi.d.ts +148 -5
- package/dist/server-shell/http/routes/project-api/schemas.d.ts +6 -0
- package/dist/server-shell/http/routes/project-api/schemas.js +1 -0
- package/dist/server-shell/http/routes/project-api/types.d.ts +1 -0
- package/dist/server-shell/http/routes/project-api/workflow-routes.js +39 -3
- package/dist/server-shell/http/routes/project-api/workspace-projections.js +1 -0
- package/dist/server-shell/http/validation.js +8 -2
- package/dist/server-shell/local-console/managed-project-service.d.ts +1 -1
- package/dist/server-shell/managed-access/transport.d.ts +1 -1
- package/migrations/0024_chat_requests.sql +19 -0
- package/migrations/0025_task_compile_requests.sql +50 -0
- package/migrations/0026_context_sync_and_chat_locale.sql +12 -0
- package/migrations/README.md +3 -1
- package/node_modules/@tutti/relay-client/dist/host-control.d.ts +1 -0
- package/node_modules/@tutti/relay-client/dist/host-control.js +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/operation-status.d.ts +174 -0
- package/node_modules/@tutti/shared/dist/schemas/api/operation-status.js +75 -0
- package/node_modules/@tutti/shared/dist/schemas/api/primitives.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/primitives.js +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.js +2 -1
- package/package.json +1 -1
- package/web/assets/{homepage-motion-scene-DyEmWwvt.js → homepage-motion-scene-BHhw4CfM.js} +1 -1
- package/web/assets/index-5COc1Yku.js +70 -0
- package/web/assets/index-BJvKuUiT.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-By3idT4U.js +0 -70
- package/web/assets/index-DSBjOZmJ.css +0 -1
|
@@ -1,12 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { linkChatRequestInvocation } from "./requests.js";
|
|
2
|
+
import { createEventRef, ID_PREFIXES, isPrefixedId, } from "@tutti/shared/ids";
|
|
3
|
+
import { createChatAssistantDiagnostics, } from "./diagnostics.js";
|
|
2
4
|
import { createClarificationRoundAgentMessage, createMainChatAgentMessage, readClarificationRoundMessagesWindow, readClarificationRoundProjection, readMainChatMessageWindow, readMainChatMessagesBeforeCursor, readMessageProjectionById, } from "../collaboration-state/index.js";
|
|
3
5
|
import { renderLanguageAwarePrompt } from "../communication-language/prompt.js";
|
|
4
6
|
import { languageDiagnosticFields } from "../communication-language/index.js";
|
|
5
|
-
import { chatLanguageContext } from "./language-context.js";
|
|
7
|
+
import { chatLanguageContext, chatLanguagePreference } from "./language-context.js";
|
|
8
|
+
import { noticeLocale } from "../communication-language/notice-locale.js";
|
|
6
9
|
import { withHostStoreTransaction, } from "../store/index.js";
|
|
7
10
|
import { messageTextForModel, messageTextForTuttiMention, } from "../collaboration-state/message-context.js";
|
|
8
11
|
import { externalReferenceMessageContext } from "../reference-access/message-context.js";
|
|
9
12
|
import { readExternalReferenceReadSources } from "../reference-access/read-evidence.js";
|
|
13
|
+
import { chatFailure, chatFailureMessage, classifyChatFailure } from "./failures.js";
|
|
14
|
+
import { countQueuedChatRequests, insertChatRequest, listRunningChatRequests, nextQueuedChatRequest, readChatRequest, updateChatRequest, } from "./requests.js";
|
|
10
15
|
const CHAT_ASSISTANT_CONTEXT_MESSAGE_LIMIT = 3;
|
|
11
16
|
const CHAT_ASSISTANT_ROUND_MESSAGE_LIMIT = 50;
|
|
12
17
|
const CHAT_ASSISTANT_ROUND_MESSAGE_MAX_CHARS = 2_000;
|
|
@@ -45,9 +50,6 @@ function normalizeAnswer(answer) {
|
|
|
45
50
|
}
|
|
46
51
|
return `${trimmed.slice(0, CHAT_ASSISTANT_MAX_ANSWER_CHARS - 3)}...`;
|
|
47
52
|
}
|
|
48
|
-
function assistantFailureMessage() {
|
|
49
|
-
return "Tutti could not complete this read-only answer. Try again later, or add more context in chat.";
|
|
50
|
-
}
|
|
51
53
|
function messageToContext(message) {
|
|
52
54
|
const authorDisplayName = message.author.display_name?.trim();
|
|
53
55
|
return {
|
|
@@ -105,46 +107,113 @@ function roundMessagesToContext(messages) {
|
|
|
105
107
|
}
|
|
106
108
|
export class ReadOnlyChatAssistant {
|
|
107
109
|
options;
|
|
108
|
-
pending = [];
|
|
109
110
|
idleWaiters = new Set();
|
|
110
111
|
active = null;
|
|
111
112
|
accepting = true;
|
|
112
113
|
maxQueuedMentions;
|
|
114
|
+
recoveryTimer;
|
|
115
|
+
started = false;
|
|
116
|
+
activeAttemptRef;
|
|
117
|
+
storageFailureReported = false;
|
|
113
118
|
constructor(options) {
|
|
114
119
|
this.options = options;
|
|
115
|
-
this.maxQueuedMentions = options.maxQueuedMentions ?? 8;
|
|
120
|
+
this.maxQueuedMentions = Math.max(1, options.maxQueuedMentions ?? 8);
|
|
116
121
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
122
|
+
/** Call once after Host recovery/transport wiring. No old messages are backfilled. */
|
|
123
|
+
start() {
|
|
124
|
+
if (this.started || !this.accepting)
|
|
125
|
+
return;
|
|
126
|
+
this.started = true;
|
|
127
|
+
for (const row of listRunningChatRequests(this.options.store.db)) {
|
|
128
|
+
if (row.attempt_ref === this.activeAttemptRef)
|
|
129
|
+
continue;
|
|
130
|
+
if (row.phase === "workspace_wait") {
|
|
131
|
+
updateChatRequest(this.options.store.db, row, { status: "queued", phase: "queue" }, this.now());
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const failure = chatFailure("host_restarted");
|
|
135
|
+
const recovered = updateChatRequest(this.options.store.db, row, {
|
|
136
|
+
status: "failed",
|
|
137
|
+
phase: "settled",
|
|
138
|
+
failure,
|
|
139
|
+
}, this.now());
|
|
140
|
+
const message = readMessageProjectionById(this.options.store.db, row.trigger_message_id);
|
|
141
|
+
if (recovered && message) {
|
|
142
|
+
this.report(message, recovered)({ stage: "failed", error_code: "host_restarted", diagnostic_id: failure.diagnostic_id });
|
|
143
|
+
try {
|
|
144
|
+
this.appendAssistantMessageForTrigger(message, chatFailureMessage(failure, recovered.notice_locale), recovered, failure);
|
|
145
|
+
}
|
|
146
|
+
catch {
|
|
147
|
+
this.report(message, recovered)({ stage: "failed", reason_code: "reply_failed" });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
127
150
|
}
|
|
128
|
-
|
|
151
|
+
// A lost post-commit wake must not strand a durable queued request.
|
|
152
|
+
this.recoveryTimer = setInterval(() => this.pump(), 1_000);
|
|
153
|
+
this.recoveryTimer.unref?.();
|
|
154
|
+
this.notifyRequestsChanged();
|
|
155
|
+
this.pump();
|
|
156
|
+
}
|
|
157
|
+
register(tx, message) {
|
|
158
|
+
if (!this.accepting ||
|
|
159
|
+
(message.author.kind !== "human" &&
|
|
160
|
+
!(message.author.kind === "system" && message.refs?.development_operator === true)) ||
|
|
161
|
+
!containsTuttiMention(messageTextForTuttiMention(message)) ||
|
|
162
|
+
readChatRequest(tx, message.id) !== null)
|
|
163
|
+
return;
|
|
164
|
+
insertChatRequest(tx, message.id, this.now(), countQueuedChatRequests(tx) >= this.maxQueuedMentions
|
|
165
|
+
? chatFailure("chat_queue_full")
|
|
166
|
+
: undefined, noticeLocale(chatLanguagePreference(tx, message)?.locale));
|
|
167
|
+
}
|
|
168
|
+
now() {
|
|
169
|
+
return (this.options.now?.() ?? new Date()).toISOString();
|
|
170
|
+
}
|
|
171
|
+
report(message, request) {
|
|
172
|
+
return createChatAssistantDiagnostics({
|
|
129
173
|
...(this.options.logger === undefined ? {} : { logger: this.options.logger }),
|
|
130
174
|
projectId: this.options.project.project_id,
|
|
131
|
-
triggerMessageId:
|
|
132
|
-
scope:
|
|
175
|
+
triggerMessageId: message.id,
|
|
176
|
+
scope: message.scope.kind,
|
|
177
|
+
requestRef: message.id,
|
|
178
|
+
...(request.attempt_ref === null ? {} : { attemptRef: request.attempt_ref }),
|
|
133
179
|
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
180
|
+
}
|
|
181
|
+
enqueue(input) {
|
|
182
|
+
if (!this.accepting)
|
|
183
|
+
return { triggered: false };
|
|
184
|
+
withHostStoreTransaction(this.options.store.db, (tx) => this.register(tx, input.message));
|
|
185
|
+
const request = readChatRequest(this.options.store.db, input.message.id);
|
|
186
|
+
if (request === null)
|
|
187
|
+
return { triggered: false };
|
|
188
|
+
if (request.status === "failed" &&
|
|
189
|
+
request.response_message_id === null &&
|
|
190
|
+
request.failure_json !== null) {
|
|
191
|
+
const report = this.report(input.message, request);
|
|
192
|
+
const failure = JSON.parse(request.failure_json);
|
|
193
|
+
report({
|
|
194
|
+
stage: "failed",
|
|
195
|
+
reason_code: failure.code === "chat_queue_full" ? "queue_full" : "reply_failed",
|
|
196
|
+
diagnostic_id: failure.diagnostic_id,
|
|
138
197
|
});
|
|
198
|
+
try {
|
|
199
|
+
this.appendAssistantMessageForTrigger(input.message, chatFailureMessage(failure, request.notice_locale), request, failure);
|
|
200
|
+
}
|
|
201
|
+
catch {
|
|
202
|
+
// The original message and failed request are already accepted.
|
|
203
|
+
report({
|
|
204
|
+
stage: "notification_failed",
|
|
205
|
+
reason_code: "reply_failed",
|
|
206
|
+
diagnostic_id: failure.diagnostic_id,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
139
209
|
return { triggered: true };
|
|
140
210
|
}
|
|
141
|
-
report({ stage: "queued" });
|
|
142
|
-
this.pending.push({ message: input.message, report });
|
|
143
211
|
this.pump();
|
|
144
212
|
return { triggered: true };
|
|
145
213
|
}
|
|
146
214
|
async waitForIdle() {
|
|
147
|
-
if (this.active === null &&
|
|
215
|
+
if (this.active === null &&
|
|
216
|
+
(!this.accepting || nextQueuedChatRequest(this.options.store.db) === null)) {
|
|
148
217
|
return;
|
|
149
218
|
}
|
|
150
219
|
await new Promise((resolve) => this.idleWaiters.add(resolve));
|
|
@@ -154,36 +223,125 @@ export class ReadOnlyChatAssistant {
|
|
|
154
223
|
return;
|
|
155
224
|
}
|
|
156
225
|
this.accepting = false;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this.appendAssistantMessageForTrigger(message, assistantFailureMessage(), {
|
|
160
|
-
scratchpad_source: "exclude",
|
|
161
|
-
});
|
|
162
|
-
}
|
|
226
|
+
if (this.recoveryTimer !== undefined)
|
|
227
|
+
clearInterval(this.recoveryTimer);
|
|
163
228
|
this.resolveIdleIfNeeded();
|
|
164
229
|
}
|
|
165
230
|
pump() {
|
|
166
|
-
|
|
231
|
+
try {
|
|
232
|
+
this.pumpNext();
|
|
233
|
+
this.storageFailureReported = false;
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
this.reportStorageFailure();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
reportStorageFailure() {
|
|
240
|
+
if (this.storageFailureReported)
|
|
241
|
+
return;
|
|
242
|
+
this.storageFailureReported = true;
|
|
243
|
+
try {
|
|
244
|
+
this.options.logger?.warn({
|
|
245
|
+
project_id: this.options.project.project_id,
|
|
246
|
+
stage: "queue_storage_failed",
|
|
247
|
+
diagnostic_id: createEventRef(),
|
|
248
|
+
}, "Chat request queue storage unavailable");
|
|
249
|
+
}
|
|
250
|
+
catch {
|
|
251
|
+
/* Diagnostics cannot replace durable recovery. */
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
pumpNext() {
|
|
255
|
+
if (this.active !== null)
|
|
256
|
+
return;
|
|
257
|
+
if (!this.accepting) {
|
|
258
|
+
this.resolveIdleIfNeeded();
|
|
167
259
|
return;
|
|
168
260
|
}
|
|
169
|
-
const pending = this.
|
|
170
|
-
if (pending ===
|
|
261
|
+
const pending = nextQueuedChatRequest(this.options.store.db);
|
|
262
|
+
if (pending === null) {
|
|
171
263
|
this.resolveIdleIfNeeded();
|
|
172
264
|
return;
|
|
173
265
|
}
|
|
174
|
-
const
|
|
175
|
-
|
|
266
|
+
const request = updateChatRequest(this.options.store.db, pending, {
|
|
267
|
+
status: "running",
|
|
268
|
+
phase: "workspace_wait",
|
|
269
|
+
attemptRef: createEventRef(),
|
|
270
|
+
}, this.now());
|
|
271
|
+
if (request === null)
|
|
272
|
+
return;
|
|
273
|
+
this.notifyRequestsChanged();
|
|
274
|
+
this.activeAttemptRef = request.attempt_ref;
|
|
275
|
+
const message = readMessageProjectionById(this.options.store.db, request.trigger_message_id);
|
|
276
|
+
if (message === null)
|
|
277
|
+
throw new Error("chat_request_source_missing");
|
|
278
|
+
const report = this.report(message, request);
|
|
279
|
+
report({
|
|
280
|
+
stage: "queued",
|
|
281
|
+
queue_wait_ms: Math.max(0, Date.parse(this.now()) - Date.parse(request.created_at)),
|
|
282
|
+
});
|
|
283
|
+
const task = this.respond(message, request, report).catch(() => {
|
|
284
|
+
// Do not overwrite a committed success when only notification delivery failed.
|
|
285
|
+
try {
|
|
286
|
+
const current = readChatRequest(this.options.store.db, message.id);
|
|
287
|
+
if (current?.status === "running" && current.attempt_ref === request.attempt_ref) {
|
|
288
|
+
const failure = chatFailure("chat_reply_persistence_failed", request.attempt_ref);
|
|
289
|
+
updateChatRequest(this.options.store.db, current, {
|
|
290
|
+
status: "failed",
|
|
291
|
+
phase: "settled",
|
|
292
|
+
failure,
|
|
293
|
+
}, this.now());
|
|
294
|
+
report({
|
|
295
|
+
stage: "failed",
|
|
296
|
+
reason_code: "reply_failed",
|
|
297
|
+
error_code: "chat_reply_persistence_failed",
|
|
298
|
+
diagnostic_id: failure.diagnostic_id,
|
|
299
|
+
});
|
|
300
|
+
}
|
|
301
|
+
else if (current !== null &&
|
|
302
|
+
current.response_message_id !== null &&
|
|
303
|
+
current.attempt_ref === request.attempt_ref) {
|
|
304
|
+
report({ stage: "notification_failed", reason_code: "reply_failed" });
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
report({ stage: "skipped", reason_code: "request_changed" });
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
catch {
|
|
311
|
+
this.reportStorageFailure();
|
|
312
|
+
}
|
|
176
313
|
});
|
|
177
314
|
this.active = task;
|
|
178
315
|
void task.finally(() => {
|
|
179
316
|
if (this.active === task) {
|
|
180
317
|
this.active = null;
|
|
318
|
+
this.activeAttemptRef = undefined;
|
|
319
|
+
this.notifyRequestsChanged();
|
|
181
320
|
this.pump();
|
|
182
321
|
}
|
|
183
322
|
});
|
|
184
323
|
}
|
|
324
|
+
notifyRequestsChanged() {
|
|
325
|
+
try {
|
|
326
|
+
this.options.onRequestsChanged?.();
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
// Maintenance wake failure cannot change a committed Chat request/reply.
|
|
330
|
+
try {
|
|
331
|
+
this.options.logger?.warn({
|
|
332
|
+
project_id: this.options.project.project_id,
|
|
333
|
+
stage: "maintenance_notification_failed",
|
|
334
|
+
diagnostic_id: createEventRef(),
|
|
335
|
+
}, "Chat maintenance notification failed");
|
|
336
|
+
}
|
|
337
|
+
catch {
|
|
338
|
+
/* Diagnostics cannot replace durable recovery. */
|
|
339
|
+
}
|
|
340
|
+
}
|
|
341
|
+
}
|
|
185
342
|
resolveIdleIfNeeded() {
|
|
186
|
-
if (this.active !== null ||
|
|
343
|
+
if (this.active !== null ||
|
|
344
|
+
(this.accepting && nextQueuedChatRequest(this.options.store.db) !== null)) {
|
|
187
345
|
return;
|
|
188
346
|
}
|
|
189
347
|
for (const resolve of this.idleWaiters) {
|
|
@@ -263,7 +421,7 @@ export class ReadOnlyChatAssistant {
|
|
|
263
421
|
},
|
|
264
422
|
}, language);
|
|
265
423
|
}
|
|
266
|
-
appendMainChatAssistantMessage(triggerMessage, body, refs) {
|
|
424
|
+
appendMainChatAssistantMessage(triggerMessage, body, request, failure, refs) {
|
|
267
425
|
let afterCommit;
|
|
268
426
|
const message = withHostStoreTransaction(this.options.store.db, (tx) => {
|
|
269
427
|
const responseMessage = createMainChatAgentMessage(tx, {
|
|
@@ -279,6 +437,7 @@ export class ReadOnlyChatAssistant {
|
|
|
279
437
|
if (typeof callback === "function") {
|
|
280
438
|
afterCommit = callback;
|
|
281
439
|
}
|
|
440
|
+
this.recordReply(tx, request, responseMessage.id, failure);
|
|
282
441
|
return responseMessage;
|
|
283
442
|
});
|
|
284
443
|
afterCommit?.();
|
|
@@ -310,6 +469,7 @@ export class ReadOnlyChatAssistant {
|
|
|
310
469
|
});
|
|
311
470
|
if (typeof callback === "function")
|
|
312
471
|
afterCommit = callback;
|
|
472
|
+
this.recordReply(tx, input.request, responseMessage.id, input.failure);
|
|
313
473
|
return responseMessage;
|
|
314
474
|
});
|
|
315
475
|
afterCommit?.();
|
|
@@ -332,36 +492,75 @@ export class ReadOnlyChatAssistant {
|
|
|
332
492
|
invalidates,
|
|
333
493
|
});
|
|
334
494
|
}
|
|
335
|
-
appendAssistantMessageForTrigger(triggerMessage, body, refs) {
|
|
495
|
+
appendAssistantMessageForTrigger(triggerMessage, body, request, failure, refs) {
|
|
496
|
+
if (failure !== undefined)
|
|
497
|
+
refs = { ...refs, scratchpad_source: "exclude" };
|
|
336
498
|
if (triggerMessage.scope.kind === "clarification_round") {
|
|
337
499
|
this.appendClarificationRoundAssistantMessage({
|
|
338
500
|
triggerMessage,
|
|
339
501
|
roundId: triggerMessage.scope.clarification_round_ref,
|
|
340
502
|
body,
|
|
503
|
+
request,
|
|
504
|
+
...(failure === undefined ? {} : { failure }),
|
|
341
505
|
...(refs === undefined ? {} : { refs }),
|
|
342
506
|
});
|
|
343
507
|
return;
|
|
344
508
|
}
|
|
345
|
-
this.appendMainChatAssistantMessage(triggerMessage, body, refs);
|
|
509
|
+
this.appendMainChatAssistantMessage(triggerMessage, body, request, failure, refs);
|
|
510
|
+
}
|
|
511
|
+
recordReply(tx, request, messageId, failure) {
|
|
512
|
+
if (!updateChatRequest(tx, request, {
|
|
513
|
+
status: failure ? "failed" : "succeeded",
|
|
514
|
+
phase: "settled",
|
|
515
|
+
responseMessageId: messageId,
|
|
516
|
+
...(failure === undefined ? {} : { failure }),
|
|
517
|
+
}, this.now()))
|
|
518
|
+
throw new Error("chat_request_changed");
|
|
346
519
|
}
|
|
347
|
-
async respond(triggerMessage, report) {
|
|
520
|
+
async respond(triggerMessage, request, report) {
|
|
348
521
|
let answer;
|
|
349
522
|
let invocationRef;
|
|
350
523
|
let readSources = [];
|
|
351
524
|
try {
|
|
352
525
|
report({ stage: "inspection_wait" });
|
|
353
|
-
await this.options.waitForInspectionAvailability?.();
|
|
526
|
+
await this.options.waitForInspectionAvailability?.((blocker) => report({ stage: "inspection_blocked", ...blocker }));
|
|
527
|
+
if (!this.accepting) {
|
|
528
|
+
updateChatRequest(this.options.store.db, request, { status: "queued", phase: "queue" }, this.now());
|
|
529
|
+
return;
|
|
530
|
+
}
|
|
354
531
|
const prompt = this.renderPrompt(triggerMessage, report);
|
|
355
532
|
if (prompt === null) {
|
|
356
533
|
report({ stage: "skipped", reason_code: "context_unavailable" });
|
|
534
|
+
updateChatRequest(this.options.store.db, request, {
|
|
535
|
+
status: "failed",
|
|
536
|
+
phase: "settled",
|
|
537
|
+
failure: chatFailure("chat_context_unavailable", request.attempt_ref),
|
|
538
|
+
}, this.now());
|
|
357
539
|
return;
|
|
358
540
|
}
|
|
541
|
+
const executing = updateChatRequest(this.options.store.db, request, {
|
|
542
|
+
status: "running",
|
|
543
|
+
phase: "answer",
|
|
544
|
+
}, this.now());
|
|
545
|
+
if (executing === null)
|
|
546
|
+
return;
|
|
547
|
+
request = executing;
|
|
359
548
|
report({ stage: "model_requested" });
|
|
360
549
|
const output = await this.options.model.answer({
|
|
361
550
|
prompt,
|
|
362
551
|
onProgress: (event) => {
|
|
363
|
-
if (event.stage === "context_ready")
|
|
364
|
-
|
|
552
|
+
if (event.stage === "context_ready") {
|
|
553
|
+
if (typeof event.invocation_ref === "string" &&
|
|
554
|
+
isPrefixedId(event.invocation_ref, ID_PREFIXES.event)) {
|
|
555
|
+
invocationRef = event.invocation_ref;
|
|
556
|
+
try {
|
|
557
|
+
linkChatRequestInvocation(this.options.store.db, request, invocationRef);
|
|
558
|
+
}
|
|
559
|
+
catch {
|
|
560
|
+
report({ stage: "notification_failed" });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
365
564
|
report(event);
|
|
366
565
|
},
|
|
367
566
|
});
|
|
@@ -370,16 +569,20 @@ export class ReadOnlyChatAssistant {
|
|
|
370
569
|
readSources = readExternalReferenceReadSources(this.options.store.db, invocationRef);
|
|
371
570
|
}
|
|
372
571
|
}
|
|
373
|
-
catch {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
572
|
+
catch (error) {
|
|
573
|
+
const failure = classifyChatFailure(error, request.attempt_ref);
|
|
574
|
+
report({
|
|
575
|
+
stage: "failed",
|
|
576
|
+
reason_code: "answer_failed",
|
|
577
|
+
error_code: failure.code,
|
|
578
|
+
diagnostic_id: failure.diagnostic_id,
|
|
377
579
|
});
|
|
580
|
+
this.appendAssistantMessageForTrigger(triggerMessage, chatFailureMessage(failure, request.notice_locale), request, failure);
|
|
378
581
|
report({ stage: "reply_persisted" });
|
|
379
582
|
return;
|
|
380
583
|
}
|
|
381
584
|
// Persistence/outbound-hook failure must not retry by creating a second reply.
|
|
382
|
-
this.appendAssistantMessageForTrigger(triggerMessage, answer, readSources.length === 0 ? undefined : { external_reference_reads: readSources });
|
|
585
|
+
this.appendAssistantMessageForTrigger(triggerMessage, answer, request, undefined, readSources.length === 0 ? undefined : { external_reference_reads: readSources });
|
|
383
586
|
report({ stage: "reply_persisted", reference_source_count: readSources.length });
|
|
384
587
|
}
|
|
385
588
|
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { MessageProjection } from "@tutti/shared/schemas/api";
|
|
2
2
|
import type { SqliteDatabase } from "../store/index.js";
|
|
3
|
+
/** Shared scope boundary for model inheritance and pre-call static notices. */
|
|
4
|
+
export declare function chatLanguagePreference(db: SqliteDatabase, trigger: MessageProjection): import("../collaboration-state/communication-language.js").StoredCommunicationLanguage | null;
|
|
3
5
|
/** Same compact scope as Q&A, never boundary-after main chat for a round. */
|
|
4
6
|
export declare function chatLanguageContext(db: SqliteDatabase, trigger: MessageProjection, limits: {
|
|
5
7
|
mainChat: number;
|
|
@@ -1,9 +1,25 @@
|
|
|
1
1
|
import { readActiveClarificationProjection, readClarificationRoundMessagesWindow, readMainChatMessageWindow, readTaskProjection, } from "../collaboration-state/index.js";
|
|
2
2
|
import { readCommunicationLanguage } from "../collaboration-state/communication-language.js";
|
|
3
3
|
import { createLanguageContext, messageLanguageEvidence } from "../communication-language/index.js";
|
|
4
|
+
/** Shared scope boundary for model inheritance and pre-call static notices. */
|
|
5
|
+
export function chatLanguagePreference(db, trigger) {
|
|
6
|
+
if (trigger.scope.kind === "main_chat")
|
|
7
|
+
return readCommunicationLanguage(db);
|
|
8
|
+
const active = readActiveClarificationProjection(db);
|
|
9
|
+
// Never borrow a newer project-wide preference for a task/compile round.
|
|
10
|
+
if (active.state !== "active" || active.round.id !== trigger.scope.clarification_round_ref)
|
|
11
|
+
return null;
|
|
12
|
+
const owner = active.thread.owner;
|
|
13
|
+
const task = owner.kind === "task" ? readTaskProjection(db, owner.task_id) : null;
|
|
14
|
+
const workflowRef = owner.kind === "task_compile" ? owner.workflow_ref : task?.created_from_workflow_ref;
|
|
15
|
+
return ((task === null ? null : readCommunicationLanguage(db, { kind: "task", ref: task.id })) ??
|
|
16
|
+
(workflowRef === undefined
|
|
17
|
+
? null
|
|
18
|
+
: readCommunicationLanguage(db, { kind: "workflow", ref: workflowRef })));
|
|
19
|
+
}
|
|
4
20
|
/** Same compact scope as Q&A, never boundary-after main chat for a round. */
|
|
5
21
|
export function chatLanguageContext(db, trigger, limits) {
|
|
6
|
-
|
|
22
|
+
const inherited = chatLanguagePreference(db, trigger);
|
|
7
23
|
let messages;
|
|
8
24
|
if (trigger.scope.kind === "main_chat") {
|
|
9
25
|
messages = readMainChatMessageWindow(db, { limit: limits.mainChat }).items;
|
|
@@ -14,19 +30,6 @@ export function chatLanguageContext(db, trigger, limits) {
|
|
|
14
30
|
anchor: "start",
|
|
15
31
|
limit: limits.round,
|
|
16
32
|
})?.items ?? [];
|
|
17
|
-
const active = readActiveClarificationProjection(db);
|
|
18
|
-
// A task/compile round must not inherit a newer project-wide language choice.
|
|
19
|
-
inherited = null;
|
|
20
|
-
if (active.state === "active" && active.round.id === trigger.scope.clarification_round_ref) {
|
|
21
|
-
const owner = active.thread.owner;
|
|
22
|
-
const task = owner.kind === "task" ? readTaskProjection(db, owner.task_id) : null;
|
|
23
|
-
const workflowRef = owner.kind === "task_compile" ? owner.workflow_ref : task?.created_from_workflow_ref;
|
|
24
|
-
inherited =
|
|
25
|
-
(task === null ? null : readCommunicationLanguage(db, { kind: "task", ref: task.id })) ??
|
|
26
|
-
(workflowRef === undefined
|
|
27
|
-
? null
|
|
28
|
-
: readCommunicationLanguage(db, { kind: "workflow", ref: workflowRef }));
|
|
29
|
-
}
|
|
30
33
|
}
|
|
31
34
|
return createLanguageContext({
|
|
32
35
|
scope: "reply",
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type ChatRequestStatus, type OperationFailure } from "@tutti/shared/schemas/api";
|
|
2
|
+
import type { SqliteDatabase } from "../store/index.js";
|
|
3
|
+
import type { NoticeLocale } from "../communication-language/notice-locale.js";
|
|
4
|
+
export type ChatRequestRecord = {
|
|
5
|
+
trigger_message_id: string;
|
|
6
|
+
status: "queued" | "running" | "succeeded" | "failed";
|
|
7
|
+
phase: "queue" | "workspace_wait" | "answer" | "settled";
|
|
8
|
+
revision: number;
|
|
9
|
+
attempt_ref: string | null;
|
|
10
|
+
response_message_id: string | null;
|
|
11
|
+
failure_json: string | null;
|
|
12
|
+
created_at: string;
|
|
13
|
+
updated_at: string;
|
|
14
|
+
notice_locale: NoticeLocale;
|
|
15
|
+
};
|
|
16
|
+
export declare function readChatRequest(db: SqliteDatabase, messageId: string): ChatRequestRecord | null;
|
|
17
|
+
export declare function chatRequestStatus(row: ChatRequestRecord): ChatRequestStatus;
|
|
18
|
+
export declare function insertChatRequest(db: SqliteDatabase, messageId: string, now: string, failure?: OperationFailure, noticeLocale?: NoticeLocale): void;
|
|
19
|
+
/** A narrow read-only signal for deferring automatic maintenance, not Chat lifecycle ownership. */
|
|
20
|
+
export declare function hasPendingChatRequests(db: SqliteDatabase): boolean;
|
|
21
|
+
export declare function countQueuedChatRequests(db: SqliteDatabase): number;
|
|
22
|
+
export declare function nextQueuedChatRequest(db: SqliteDatabase): ChatRequestRecord | null;
|
|
23
|
+
/** CAS/fencing only. Lifecycle policy belongs to Chat Assistant, not the repository. */
|
|
24
|
+
export declare function updateChatRequest(db: SqliteDatabase, row: ChatRequestRecord, next: {
|
|
25
|
+
status: ChatRequestRecord["status"];
|
|
26
|
+
phase: ChatRequestRecord["phase"];
|
|
27
|
+
attemptRef?: string;
|
|
28
|
+
responseMessageId?: string;
|
|
29
|
+
failure?: OperationFailure;
|
|
30
|
+
}, now: string): ChatRequestRecord | null;
|
|
31
|
+
export declare function listRunningChatRequests(db: SqliteDatabase): ChatRequestRecord[];
|
|
32
|
+
/** Correlation only; never changes lifecycle state based on a progress callback. */
|
|
33
|
+
export declare function linkChatRequestInvocation(db: SqliteDatabase, row: ChatRequestRecord, invocationRef: string): void;
|
|
34
|
+
//# sourceMappingURL=requests.d.ts.map
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
import { Value } from "@sinclair/typebox/value";
|
|
2
|
+
import { ChatRequestStatusSchema, } from "@tutti/shared/schemas/api";
|
|
3
|
+
export function readChatRequest(db, messageId) {
|
|
4
|
+
return (db
|
|
5
|
+
.prepare("SELECT * FROM chat_requests WHERE trigger_message_id = ?")
|
|
6
|
+
.get(messageId) ?? null);
|
|
7
|
+
}
|
|
8
|
+
export function chatRequestStatus(row) {
|
|
9
|
+
const value = {
|
|
10
|
+
operation: { kind: "chat_answer", ref: row.trigger_message_id },
|
|
11
|
+
status: row.status,
|
|
12
|
+
phase: row.phase,
|
|
13
|
+
revision: row.revision,
|
|
14
|
+
created_at: row.created_at,
|
|
15
|
+
updated_at: row.updated_at,
|
|
16
|
+
...(row.response_message_id === null ? {} : { response_message_id: row.response_message_id }),
|
|
17
|
+
...(row.failure_json === null ? {} : { failure: JSON.parse(row.failure_json) }),
|
|
18
|
+
};
|
|
19
|
+
if (!Value.Check(ChatRequestStatusSchema, value))
|
|
20
|
+
throw new Error("chat_request_invalid");
|
|
21
|
+
return value;
|
|
22
|
+
}
|
|
23
|
+
export function insertChatRequest(db, messageId, now, failure, noticeLocale = "en") {
|
|
24
|
+
db.prepare(`INSERT INTO chat_requests
|
|
25
|
+
(trigger_message_id, status, phase, failure_json, created_at, updated_at, notice_locale)
|
|
26
|
+
VALUES (?, ?, ?, ?, ?, ?, ?) ON CONFLICT(trigger_message_id) DO NOTHING`).run(messageId, failure ? "failed" : "queued", failure ? "settled" : "queue", failure ? JSON.stringify(failure) : null, now, now, noticeLocale);
|
|
27
|
+
}
|
|
28
|
+
/** A narrow read-only signal for deferring automatic maintenance, not Chat lifecycle ownership. */
|
|
29
|
+
export function hasPendingChatRequests(db) {
|
|
30
|
+
return (db
|
|
31
|
+
.prepare("SELECT 1 AS present FROM chat_requests WHERE status IN ('queued', 'running') LIMIT 1")
|
|
32
|
+
.get() !== undefined);
|
|
33
|
+
}
|
|
34
|
+
export function countQueuedChatRequests(db) {
|
|
35
|
+
return db
|
|
36
|
+
.prepare("SELECT count(*) AS count FROM chat_requests WHERE status = 'queued'")
|
|
37
|
+
.get().count;
|
|
38
|
+
}
|
|
39
|
+
export function nextQueuedChatRequest(db) {
|
|
40
|
+
return (db
|
|
41
|
+
.prepare("SELECT * FROM chat_requests WHERE status = 'queued' ORDER BY enqueue_order LIMIT 1")
|
|
42
|
+
.get() ?? null);
|
|
43
|
+
}
|
|
44
|
+
/** CAS/fencing only. Lifecycle policy belongs to Chat Assistant, not the repository. */
|
|
45
|
+
export function updateChatRequest(db, row, next, now) {
|
|
46
|
+
const changed = db
|
|
47
|
+
.prepare(`UPDATE chat_requests SET status = ?, phase = ?,
|
|
48
|
+
attempt_ref = ?, response_message_id = ?, failure_json = ?, updated_at = ?, revision = revision + 1
|
|
49
|
+
WHERE trigger_message_id = ? AND revision = ?`)
|
|
50
|
+
.run(next.status, next.phase, next.attemptRef ?? row.attempt_ref, next.responseMessageId ?? null, next.failure ? JSON.stringify(next.failure) : null, now, row.trigger_message_id, row.revision).changes;
|
|
51
|
+
return changed === 0 ? null : readChatRequest(db, row.trigger_message_id);
|
|
52
|
+
}
|
|
53
|
+
export function listRunningChatRequests(db) {
|
|
54
|
+
return db
|
|
55
|
+
.prepare("SELECT * FROM chat_requests WHERE status = 'running' ORDER BY created_at, trigger_message_id")
|
|
56
|
+
.all();
|
|
57
|
+
}
|
|
58
|
+
/** Correlation only; never changes lifecycle state based on a progress callback. */
|
|
59
|
+
export function linkChatRequestInvocation(db, row, invocationRef) {
|
|
60
|
+
db.prepare(`INSERT OR IGNORE INTO chat_request_invocations(message_id, attempt_ref, invocation_ref)
|
|
61
|
+
SELECT trigger_message_id, attempt_ref, ? FROM chat_requests
|
|
62
|
+
WHERE trigger_message_id = ? AND attempt_ref = ? AND status = 'running'`).run(invocationRef, row.trigger_message_id, row.attempt_ref);
|
|
63
|
+
}
|
|
64
|
+
//# sourceMappingURL=requests.js.map
|
|
@@ -21,8 +21,10 @@ export function ingestClarificationMessage(options) {
|
|
|
21
21
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
22
22
|
execute: (tx) => {
|
|
23
23
|
const result = options.controlPlane.sendClarificationRoundMessage(options.roundId, options.payload, options.author);
|
|
24
|
-
if (result.result?.message !== undefined)
|
|
24
|
+
if (result.result?.message !== undefined) {
|
|
25
25
|
options.afterCreate?.(tx, result.result.message);
|
|
26
|
+
options.chatAssistant?.register(tx, result.result.message);
|
|
27
|
+
}
|
|
26
28
|
return result;
|
|
27
29
|
},
|
|
28
30
|
});
|
|
@@ -39,6 +39,7 @@ export function ingestMainChatMessage(options) {
|
|
|
39
39
|
...(options.now === undefined ? {} : { now: options.now }),
|
|
40
40
|
});
|
|
41
41
|
options.afterCreate?.(tx, message);
|
|
42
|
+
options.chatAssistant?.register(tx, message);
|
|
42
43
|
return {
|
|
43
44
|
disposition: { kind: "created", message_id: message.id },
|
|
44
45
|
result: { message },
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { AccountAvatarProjection } from "@tutti/shared/domain";
|
|
2
2
|
import type { MessageProjection } from "@tutti/shared/schemas/api";
|
|
3
|
+
import type { SqliteDatabase } from "../store/index.js";
|
|
3
4
|
export type TrustedCollaborationAuthor = {
|
|
4
5
|
kind?: "human" | "development_operator";
|
|
5
6
|
account_ref: string;
|
|
@@ -10,6 +11,7 @@ export type ScratchpadSourceNotifier = {
|
|
|
10
11
|
notify: (message: MessageProjection) => void;
|
|
11
12
|
};
|
|
12
13
|
export type ChatAssistantEnqueuer = {
|
|
14
|
+
register: (tx: SqliteDatabase, message: MessageProjection) => void;
|
|
13
15
|
enqueue: (input: {
|
|
14
16
|
message: MessageProjection;
|
|
15
17
|
}) => unknown;
|