@xfey/tutti 0.1.126 → 0.1.128

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 (107) hide show
  1. package/README.md +2 -0
  2. package/dist/approvals/decision-command.d.ts +2 -2
  3. package/dist/chat-assistant/diagnostics.d.ts +11 -3
  4. package/dist/chat-assistant/diagnostics.js +5 -2
  5. package/dist/chat-assistant/failures.d.ts +27 -0
  6. package/dist/chat-assistant/failures.js +102 -0
  7. package/dist/chat-assistant/index.d.ts +14 -2
  8. package/dist/chat-assistant/index.js +241 -54
  9. package/dist/chat-assistant/language-context.d.ts +8 -0
  10. package/dist/chat-assistant/language-context.js +41 -0
  11. package/dist/chat-assistant/requests.d.ts +30 -0
  12. package/dist/chat-assistant/requests.js +58 -0
  13. package/dist/collaboration-ingestion/clarification.js +3 -1
  14. package/dist/collaboration-ingestion/main-chat.js +1 -0
  15. package/dist/collaboration-ingestion/types.d.ts +2 -0
  16. package/dist/collaboration-state/clarification-snapshot.d.ts +5 -5
  17. package/dist/collaboration-state/communication-language.d.ts +31 -0
  18. package/dist/collaboration-state/communication-language.js +36 -0
  19. package/dist/collaboration-state/task-compile-context.d.ts +1 -1
  20. package/dist/collaboration-state/task-compile-context.js +20 -1
  21. package/dist/collaboration-state/task-compile-requests.d.ts +6 -0
  22. package/dist/collaboration-state/task-compile-requests.js +16 -0
  23. package/dist/communication-language/index.d.ts +75 -0
  24. package/dist/communication-language/index.js +122 -0
  25. package/dist/communication-language/live-smoke-cli.d.ts +2 -0
  26. package/dist/communication-language/live-smoke-cli.js +368 -0
  27. package/dist/communication-language/prompt.d.ts +22 -0
  28. package/dist/communication-language/prompt.js +73 -0
  29. package/dist/control-plane/communication-language.d.ts +33 -0
  30. package/dist/control-plane/communication-language.js +72 -0
  31. package/dist/control-plane/context-sync.js +7 -0
  32. package/dist/control-plane/follow-up-output.js +25 -4
  33. package/dist/control-plane/follow-up-start.js +17 -0
  34. package/dist/control-plane/project-brief-refresh.js +10 -1
  35. package/dist/control-plane/project-context-bootstrap.d.ts +2 -0
  36. package/dist/control-plane/project-context-bootstrap.js +8 -0
  37. package/dist/control-plane/reference-summary-refresh.js +7 -0
  38. package/dist/control-plane/run-start.js +14 -6
  39. package/dist/control-plane/scratchpad-refresh-start.d.ts +1 -0
  40. package/dist/control-plane/scratchpad-refresh-start.js +13 -0
  41. package/dist/control-plane/scratchpad-refresh.d.ts +2 -0
  42. package/dist/control-plane/scratchpad-refresh.js +40 -26
  43. package/dist/control-plane/task-compile-continuation.js +16 -1
  44. package/dist/control-plane/task-compile-output.js +38 -14
  45. package/dist/control-plane/task-compile-request-status.d.ts +6 -0
  46. package/dist/control-plane/task-compile-request-status.js +70 -0
  47. package/dist/control-plane/task-compile-start.d.ts +1 -0
  48. package/dist/control-plane/task-compile-start.js +62 -3
  49. package/dist/control-plane/types.d.ts +2 -0
  50. package/dist/control-plane/workflows/openai.js +48 -28
  51. package/dist/control-plane/workflows/types.d.ts +18 -13
  52. package/dist/prompt-templates/index.d.ts +1 -1
  53. package/dist/prompt-templates/index.js +2 -0
  54. package/dist/providers/failure-classification.d.ts +23 -0
  55. package/dist/providers/failure-classification.js +43 -0
  56. package/dist/reference-access/read-evidence.d.ts +2 -0
  57. package/dist/reference-access/read-evidence.js +5 -0
  58. package/dist/reference-access/service.d.ts +2 -0
  59. package/dist/reference-access/service.js +2 -0
  60. package/dist/run-pipeline/task-run-invocation.js +12 -9
  61. package/dist/runtime-failures.js +15 -1
  62. package/dist/server-shell/cli/external-reference-runtime.js +6 -3
  63. package/dist/server-shell/cli/host-server-runtime.js +4 -1
  64. package/dist/server-shell/cli/relay-registration.js +1 -0
  65. package/dist/server-shell/http/routes/project-api/messages-routes.js +22 -1
  66. package/dist/server-shell/http/routes/project-api/openapi-collaboration-routes.d.ts +140 -5
  67. package/dist/server-shell/http/routes/project-api/openapi-collaboration-routes.js +29 -2
  68. package/dist/server-shell/http/routes/project-api/openapi-skills-routes.d.ts +2 -0
  69. package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +6 -0
  70. package/dist/server-shell/http/routes/project-api/openapi.d.ts +148 -5
  71. package/dist/server-shell/http/routes/project-api/schemas.d.ts +6 -0
  72. package/dist/server-shell/http/routes/project-api/schemas.js +1 -0
  73. package/dist/server-shell/http/routes/project-api/types.d.ts +1 -0
  74. package/dist/server-shell/http/routes/project-api/workflow-routes.js +39 -3
  75. package/dist/server-shell/http/routes/project-api/workspace-projections.js +1 -0
  76. package/dist/server-shell/http/validation.js +8 -2
  77. package/dist/server-shell/local-console/managed-project-service.d.ts +1 -1
  78. package/dist/server-shell/managed-access/transport.d.ts +1 -1
  79. package/migrations/0023_communication_language.sql +14 -0
  80. package/migrations/0024_chat_requests.sql +19 -0
  81. package/migrations/0025_task_compile_requests.sql +50 -0
  82. package/migrations/README.md +3 -1
  83. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +1 -0
  84. package/node_modules/@tutti/relay-client/dist/host-control.js +1 -0
  85. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -0
  86. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -0
  87. package/node_modules/@tutti/shared/dist/schemas/api/operation-status.d.ts +174 -0
  88. package/node_modules/@tutti/shared/dist/schemas/api/operation-status.js +75 -0
  89. package/node_modules/@tutti/shared/dist/schemas/api/primitives.d.ts +1 -0
  90. package/node_modules/@tutti/shared/dist/schemas/api/primitives.js +1 -0
  91. package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.d.ts +1 -0
  92. package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.js +2 -1
  93. package/package.json +1 -1
  94. package/prompts/README.md +1 -0
  95. package/prompts/chat-assistant.md +1 -1
  96. package/prompts/communication-language-result.md +27 -0
  97. package/prompts/communication-language.md +50 -0
  98. package/prompts/procedures/follow-up-check.md +1 -1
  99. package/prompts/procedures/scratchpad-refresh.md +2 -2
  100. package/prompts/procedures/task-compile.md +1 -1
  101. package/prompts/prompt-flow-map.md +2 -0
  102. package/web/assets/{homepage-motion-scene-DyEmWwvt.js → homepage-motion-scene-BcMt0AVW.js} +1 -1
  103. package/web/assets/index-2eX37Jx6.js +70 -0
  104. package/web/assets/index-BJvKuUiT.css +1 -0
  105. package/web/index.html +2 -2
  106. package/web/assets/index-By3idT4U.js +0 -70
  107. package/web/assets/index-DSBjOZmJ.css +0 -1
@@ -1,10 +1,16 @@
1
+ import { linkChatRequestInvocation } from "./requests.js";
2
+ import { createEventRef, ID_PREFIXES, isPrefixedId, } from "@tutti/shared/ids";
1
3
  import { createChatAssistantDiagnostics } from "./diagnostics.js";
2
4
  import { createClarificationRoundAgentMessage, createMainChatAgentMessage, readClarificationRoundMessagesWindow, readClarificationRoundProjection, readMainChatMessageWindow, readMainChatMessagesBeforeCursor, readMessageProjectionById, } from "../collaboration-state/index.js";
3
- import { renderPromptTemplate } from "../prompt-templates/index.js";
5
+ import { renderLanguageAwarePrompt } from "../communication-language/prompt.js";
6
+ import { languageDiagnosticFields } from "../communication-language/index.js";
7
+ import { chatLanguageContext } from "./language-context.js";
4
8
  import { withHostStoreTransaction, } from "../store/index.js";
5
9
  import { messageTextForModel, messageTextForTuttiMention, } from "../collaboration-state/message-context.js";
6
10
  import { externalReferenceMessageContext } from "../reference-access/message-context.js";
7
11
  import { readExternalReferenceReadSources } from "../reference-access/read-evidence.js";
12
+ import { chatFailure, chatFailureMessage, classifyChatFailure } from "./failures.js";
13
+ import { countQueuedChatRequests, insertChatRequest, listRunningChatRequests, nextQueuedChatRequest, readChatRequest, updateChatRequest, } from "./requests.js";
8
14
  const CHAT_ASSISTANT_CONTEXT_MESSAGE_LIMIT = 3;
9
15
  const CHAT_ASSISTANT_ROUND_MESSAGE_LIMIT = 50;
10
16
  const CHAT_ASSISTANT_ROUND_MESSAGE_MAX_CHARS = 2_000;
@@ -43,9 +49,6 @@ function normalizeAnswer(answer) {
43
49
  }
44
50
  return `${trimmed.slice(0, CHAT_ASSISTANT_MAX_ANSWER_CHARS - 3)}...`;
45
51
  }
46
- function assistantFailureMessage() {
47
- return "Tutti could not complete this read-only answer. Try again later, or add more context in chat.";
48
- }
49
52
  function messageToContext(message) {
50
53
  const authorDisplayName = message.author.display_name?.trim();
51
54
  return {
@@ -103,46 +106,112 @@ function roundMessagesToContext(messages) {
103
106
  }
104
107
  export class ReadOnlyChatAssistant {
105
108
  options;
106
- pending = [];
107
109
  idleWaiters = new Set();
108
110
  active = null;
109
111
  accepting = true;
110
112
  maxQueuedMentions;
113
+ recoveryTimer;
114
+ started = false;
115
+ activeAttemptRef;
116
+ storageFailureReported = false;
111
117
  constructor(options) {
112
118
  this.options = options;
113
- this.maxQueuedMentions = options.maxQueuedMentions ?? 8;
119
+ this.maxQueuedMentions = Math.max(1, options.maxQueuedMentions ?? 8);
114
120
  }
115
- enqueue(input) {
116
- if (input.message.author.kind !== "human" &&
117
- !(input.message.author.kind === "system" && input.message.refs?.development_operator === true)) {
118
- return { triggered: false };
119
- }
120
- if (!containsTuttiMention(messageTextForTuttiMention(input.message))) {
121
- return { triggered: false };
122
- }
123
- if (!this.accepting) {
124
- return { triggered: false };
121
+ /** Call once after Host recovery/transport wiring. No old messages are backfilled. */
122
+ start() {
123
+ if (this.started || !this.accepting)
124
+ return;
125
+ this.started = true;
126
+ for (const row of listRunningChatRequests(this.options.store.db)) {
127
+ if (row.attempt_ref === this.activeAttemptRef)
128
+ continue;
129
+ if (row.phase === "workspace_wait") {
130
+ updateChatRequest(this.options.store.db, row, { status: "queued", phase: "queue" }, this.now());
131
+ continue;
132
+ }
133
+ const failure = chatFailure("host_restarted");
134
+ const recovered = updateChatRequest(this.options.store.db, row, {
135
+ status: "failed",
136
+ phase: "settled",
137
+ failure,
138
+ }, this.now());
139
+ const message = readMessageProjectionById(this.options.store.db, row.trigger_message_id);
140
+ if (recovered && message) {
141
+ this.report(message, recovered)({ stage: "failed", error_code: "host_restarted", diagnostic_id: failure.diagnostic_id });
142
+ try {
143
+ this.appendAssistantMessageForTrigger(message, chatFailureMessage(failure), recovered, failure);
144
+ }
145
+ catch {
146
+ this.report(message, recovered)({ stage: "failed", reason_code: "reply_failed" });
147
+ }
148
+ }
125
149
  }
126
- const report = createChatAssistantDiagnostics({
150
+ // A lost post-commit wake must not strand a durable queued request.
151
+ this.recoveryTimer = setInterval(() => this.pump(), 1_000);
152
+ this.recoveryTimer.unref?.();
153
+ this.pump();
154
+ }
155
+ register(tx, message) {
156
+ if (!this.accepting ||
157
+ (message.author.kind !== "human" &&
158
+ !(message.author.kind === "system" && message.refs?.development_operator === true)) ||
159
+ !containsTuttiMention(messageTextForTuttiMention(message)) ||
160
+ readChatRequest(tx, message.id) !== null)
161
+ return;
162
+ insertChatRequest(tx, message.id, this.now(), countQueuedChatRequests(tx) >= this.maxQueuedMentions
163
+ ? chatFailure("chat_queue_full")
164
+ : undefined);
165
+ }
166
+ now() {
167
+ return (this.options.now?.() ?? new Date()).toISOString();
168
+ }
169
+ report(message, request) {
170
+ return createChatAssistantDiagnostics({
127
171
  ...(this.options.logger === undefined ? {} : { logger: this.options.logger }),
128
172
  projectId: this.options.project.project_id,
129
- triggerMessageId: input.message.id,
130
- scope: input.message.scope.kind,
173
+ triggerMessageId: message.id,
174
+ scope: message.scope.kind,
175
+ requestRef: message.id,
176
+ ...(request.attempt_ref === null ? {} : { attemptRef: request.attempt_ref }),
131
177
  });
132
- if (this.pending.length >= this.maxQueuedMentions) {
133
- report({ stage: "failed", reason_code: "queue_full" });
134
- this.appendAssistantMessageForTrigger(input.message, assistantFailureMessage(), {
135
- scratchpad_source: "exclude",
178
+ }
179
+ enqueue(input) {
180
+ if (!this.accepting)
181
+ return { triggered: false };
182
+ withHostStoreTransaction(this.options.store.db, (tx) => this.register(tx, input.message));
183
+ const request = readChatRequest(this.options.store.db, input.message.id);
184
+ if (request === null)
185
+ return { triggered: false };
186
+ if (request.status === "failed" &&
187
+ request.response_message_id === null &&
188
+ request.failure_json !== null) {
189
+ const report = this.report(input.message, request);
190
+ const failure = JSON.parse(request.failure_json);
191
+ report({
192
+ stage: "failed",
193
+ reason_code: failure.code === "chat_queue_full" ? "queue_full" : "reply_failed",
194
+ diagnostic_id: failure.diagnostic_id,
136
195
  });
196
+ try {
197
+ this.appendAssistantMessageForTrigger(input.message, chatFailureMessage(failure), request, failure);
198
+ }
199
+ catch {
200
+ // The original message and failed request are already accepted.
201
+ report({
202
+ stage: "notification_failed",
203
+ reason_code: "reply_failed",
204
+ diagnostic_id: failure.diagnostic_id,
205
+ });
206
+ }
137
207
  return { triggered: true };
138
208
  }
139
- report({ stage: "queued" });
140
- this.pending.push({ message: input.message, report });
141
209
  this.pump();
142
210
  return { triggered: true };
143
211
  }
144
212
  async waitForIdle() {
145
- if (this.active === null && this.pending.length === 0) {
213
+ if (this.active === null &&
214
+ (!this.accepting || nextQueuedChatRequest(this.options.store.db) === null)) {
146
215
  return;
147
216
  }
148
217
  await new Promise((resolve) => this.idleWaiters.add(resolve));
@@ -152,36 +221,102 @@ export class ReadOnlyChatAssistant {
152
221
  return;
153
222
  }
154
223
  this.accepting = false;
155
- for (const { message, report } of this.pending.splice(0)) {
156
- report({ stage: "skipped", reason_code: "stopping" });
157
- this.appendAssistantMessageForTrigger(message, assistantFailureMessage(), {
158
- scratchpad_source: "exclude",
159
- });
160
- }
224
+ if (this.recoveryTimer !== undefined)
225
+ clearInterval(this.recoveryTimer);
161
226
  this.resolveIdleIfNeeded();
162
227
  }
163
228
  pump() {
164
- if (this.active !== null) {
229
+ try {
230
+ this.pumpNext();
231
+ this.storageFailureReported = false;
232
+ }
233
+ catch {
234
+ this.reportStorageFailure();
235
+ }
236
+ }
237
+ reportStorageFailure() {
238
+ if (this.storageFailureReported)
239
+ return;
240
+ this.storageFailureReported = true;
241
+ try {
242
+ this.options.logger?.warn({
243
+ project_id: this.options.project.project_id,
244
+ stage: "queue_storage_failed",
245
+ diagnostic_id: createEventRef(),
246
+ }, "Chat request queue storage unavailable");
247
+ }
248
+ catch {
249
+ /* Diagnostics cannot replace durable recovery. */
250
+ }
251
+ }
252
+ pumpNext() {
253
+ if (this.active !== null)
254
+ return;
255
+ if (!this.accepting) {
256
+ this.resolveIdleIfNeeded();
165
257
  return;
166
258
  }
167
- const pending = this.pending.shift();
168
- if (pending === undefined) {
259
+ const pending = nextQueuedChatRequest(this.options.store.db);
260
+ if (pending === null) {
169
261
  this.resolveIdleIfNeeded();
170
262
  return;
171
263
  }
172
- const task = this.respond(pending.message, pending.report).catch(() => {
173
- pending.report({ stage: "failed", reason_code: "reply_failed" });
264
+ const request = updateChatRequest(this.options.store.db, pending, {
265
+ status: "running",
266
+ phase: "workspace_wait",
267
+ attemptRef: createEventRef(),
268
+ }, this.now());
269
+ if (request === null)
270
+ return;
271
+ this.activeAttemptRef = request.attempt_ref;
272
+ const message = readMessageProjectionById(this.options.store.db, request.trigger_message_id);
273
+ if (message === null)
274
+ throw new Error("chat_request_source_missing");
275
+ const report = this.report(message, request);
276
+ report({ stage: "queued" });
277
+ const task = this.respond(message, request, report).catch(() => {
278
+ // Do not overwrite a committed success when only notification delivery failed.
279
+ try {
280
+ const current = readChatRequest(this.options.store.db, message.id);
281
+ if (current?.status === "running" && current.attempt_ref === request.attempt_ref) {
282
+ const failure = chatFailure("chat_reply_persistence_failed", request.attempt_ref);
283
+ updateChatRequest(this.options.store.db, current, {
284
+ status: "failed",
285
+ phase: "settled",
286
+ failure,
287
+ }, this.now());
288
+ report({
289
+ stage: "failed",
290
+ reason_code: "reply_failed",
291
+ error_code: "chat_reply_persistence_failed",
292
+ diagnostic_id: failure.diagnostic_id,
293
+ });
294
+ }
295
+ else if (current !== null &&
296
+ current.response_message_id !== null &&
297
+ current.attempt_ref === request.attempt_ref) {
298
+ report({ stage: "notification_failed", reason_code: "reply_failed" });
299
+ }
300
+ else {
301
+ report({ stage: "skipped", reason_code: "request_changed" });
302
+ }
303
+ }
304
+ catch {
305
+ this.reportStorageFailure();
306
+ }
174
307
  });
175
308
  this.active = task;
176
309
  void task.finally(() => {
177
310
  if (this.active === task) {
178
311
  this.active = null;
312
+ this.activeAttemptRef = undefined;
179
313
  this.pump();
180
314
  }
181
315
  });
182
316
  }
183
317
  resolveIdleIfNeeded() {
184
- if (this.active !== null || this.pending.length !== 0) {
318
+ if (this.active !== null ||
319
+ (this.accepting && nextQueuedChatRequest(this.options.store.db) !== null)) {
185
320
  return;
186
321
  }
187
322
  for (const resolve of this.idleWaiters) {
@@ -241,20 +376,27 @@ export class ReadOnlyChatAssistant {
241
376
  }
242
377
  return this.buildMainChatContext(triggerMessage);
243
378
  }
244
- renderPrompt(triggerMessage) {
379
+ renderPrompt(triggerMessage, report) {
245
380
  const context = this.buildContext(triggerMessage);
246
381
  if (context === null) {
247
382
  return null;
248
383
  }
249
- return renderPromptTemplate({
384
+ const language = chatLanguageContext(this.options.store.db, triggerMessage, {
385
+ mainChat: CHAT_ASSISTANT_CONTEXT_MESSAGE_LIMIT,
386
+ round: CHAT_ASSISTANT_ROUND_MESSAGE_LIMIT,
387
+ });
388
+ report({ stage: "language_context", ...languageDiagnosticFields(language) });
389
+ return renderLanguageAwarePrompt({
250
390
  templateKey: "chat.assistant",
251
- ...(this.options.promptsRoot === undefined ? {} : { promptsRoot: this.options.promptsRoot }),
391
+ ...(this.options.promptsRoot === undefined
392
+ ? {}
393
+ : { promptsRoot: this.options.promptsRoot }),
252
394
  variables: {
253
395
  assistant_context_json: context,
254
396
  },
255
- });
397
+ }, language);
256
398
  }
257
- appendMainChatAssistantMessage(triggerMessage, body, refs) {
399
+ appendMainChatAssistantMessage(triggerMessage, body, request, failure, refs) {
258
400
  let afterCommit;
259
401
  const message = withHostStoreTransaction(this.options.store.db, (tx) => {
260
402
  const responseMessage = createMainChatAgentMessage(tx, {
@@ -270,6 +412,7 @@ export class ReadOnlyChatAssistant {
270
412
  if (typeof callback === "function") {
271
413
  afterCommit = callback;
272
414
  }
415
+ this.recordReply(tx, request, responseMessage.id, failure);
273
416
  return responseMessage;
274
417
  });
275
418
  afterCommit?.();
@@ -301,6 +444,7 @@ export class ReadOnlyChatAssistant {
301
444
  });
302
445
  if (typeof callback === "function")
303
446
  afterCommit = callback;
447
+ this.recordReply(tx, input.request, responseMessage.id, input.failure);
304
448
  return responseMessage;
305
449
  });
306
450
  afterCommit?.();
@@ -323,36 +467,75 @@ export class ReadOnlyChatAssistant {
323
467
  invalidates,
324
468
  });
325
469
  }
326
- appendAssistantMessageForTrigger(triggerMessage, body, refs) {
470
+ appendAssistantMessageForTrigger(triggerMessage, body, request, failure, refs) {
471
+ if (failure !== undefined)
472
+ refs = { ...refs, scratchpad_source: "exclude" };
327
473
  if (triggerMessage.scope.kind === "clarification_round") {
328
474
  this.appendClarificationRoundAssistantMessage({
329
475
  triggerMessage,
330
476
  roundId: triggerMessage.scope.clarification_round_ref,
331
477
  body,
478
+ request,
479
+ ...(failure === undefined ? {} : { failure }),
332
480
  ...(refs === undefined ? {} : { refs }),
333
481
  });
334
482
  return;
335
483
  }
336
- this.appendMainChatAssistantMessage(triggerMessage, body, refs);
484
+ this.appendMainChatAssistantMessage(triggerMessage, body, request, failure, refs);
485
+ }
486
+ recordReply(tx, request, messageId, failure) {
487
+ if (!updateChatRequest(tx, request, {
488
+ status: failure ? "failed" : "succeeded",
489
+ phase: "settled",
490
+ responseMessageId: messageId,
491
+ ...(failure === undefined ? {} : { failure }),
492
+ }, this.now()))
493
+ throw new Error("chat_request_changed");
337
494
  }
338
- async respond(triggerMessage, report) {
495
+ async respond(triggerMessage, request, report) {
339
496
  let answer;
340
497
  let invocationRef;
341
498
  let readSources = [];
342
499
  try {
343
500
  report({ stage: "inspection_wait" });
344
501
  await this.options.waitForInspectionAvailability?.();
345
- const prompt = this.renderPrompt(triggerMessage);
502
+ if (!this.accepting) {
503
+ updateChatRequest(this.options.store.db, request, { status: "queued", phase: "queue" }, this.now());
504
+ return;
505
+ }
506
+ const prompt = this.renderPrompt(triggerMessage, report);
346
507
  if (prompt === null) {
347
508
  report({ stage: "skipped", reason_code: "context_unavailable" });
509
+ updateChatRequest(this.options.store.db, request, {
510
+ status: "failed",
511
+ phase: "settled",
512
+ failure: chatFailure("chat_context_unavailable", request.attempt_ref),
513
+ }, this.now());
348
514
  return;
349
515
  }
516
+ const executing = updateChatRequest(this.options.store.db, request, {
517
+ status: "running",
518
+ phase: "answer",
519
+ }, this.now());
520
+ if (executing === null)
521
+ return;
522
+ request = executing;
350
523
  report({ stage: "model_requested" });
351
524
  const output = await this.options.model.answer({
352
525
  prompt,
353
526
  onProgress: (event) => {
354
- if (event.stage === "context_ready")
355
- invocationRef = event.invocation_ref;
527
+ if (event.stage === "context_ready") {
528
+ if (typeof event.invocation_ref === "string" &&
529
+ isPrefixedId(event.invocation_ref, ID_PREFIXES.event)) {
530
+ invocationRef = event.invocation_ref;
531
+ try {
532
+ linkChatRequestInvocation(this.options.store.db, request, invocationRef);
533
+ }
534
+ catch {
535
+ report({ stage: "notification_failed" });
536
+ }
537
+ }
538
+ }
356
539
  report(event);
357
540
  },
358
541
  });
@@ -361,16 +544,20 @@ export class ReadOnlyChatAssistant {
361
544
  readSources = readExternalReferenceReadSources(this.options.store.db, invocationRef);
362
545
  }
363
546
  }
364
- catch {
365
- report({ stage: "failed", reason_code: "answer_failed" });
366
- this.appendAssistantMessageForTrigger(triggerMessage, assistantFailureMessage(), {
367
- scratchpad_source: "exclude",
547
+ catch (error) {
548
+ const failure = classifyChatFailure(error, request.attempt_ref);
549
+ report({
550
+ stage: "failed",
551
+ reason_code: "answer_failed",
552
+ error_code: failure.code,
553
+ diagnostic_id: failure.diagnostic_id,
368
554
  });
555
+ this.appendAssistantMessageForTrigger(triggerMessage, chatFailureMessage(failure), request, failure);
369
556
  report({ stage: "reply_persisted" });
370
557
  return;
371
558
  }
372
559
  // Persistence/outbound-hook failure must not retry by creating a second reply.
373
- this.appendAssistantMessageForTrigger(triggerMessage, answer, readSources.length === 0 ? undefined : { external_reference_reads: readSources });
560
+ this.appendAssistantMessageForTrigger(triggerMessage, answer, request, undefined, readSources.length === 0 ? undefined : { external_reference_reads: readSources });
374
561
  report({ stage: "reply_persisted", reference_source_count: readSources.length });
375
562
  }
376
563
  }
@@ -0,0 +1,8 @@
1
+ import type { MessageProjection } from "@tutti/shared/schemas/api";
2
+ import type { SqliteDatabase } from "../store/index.js";
3
+ /** Same compact scope as Q&A, never boundary-after main chat for a round. */
4
+ export declare function chatLanguageContext(db: SqliteDatabase, trigger: MessageProjection, limits: {
5
+ mainChat: number;
6
+ round: number;
7
+ }): import("../communication-language/index.js").CommunicationLanguageContext;
8
+ //# sourceMappingURL=language-context.d.ts.map
@@ -0,0 +1,41 @@
1
+ import { readActiveClarificationProjection, readClarificationRoundMessagesWindow, readMainChatMessageWindow, readTaskProjection, } from "../collaboration-state/index.js";
2
+ import { readCommunicationLanguage } from "../collaboration-state/communication-language.js";
3
+ import { createLanguageContext, messageLanguageEvidence } from "../communication-language/index.js";
4
+ /** Same compact scope as Q&A, never boundary-after main chat for a round. */
5
+ export function chatLanguageContext(db, trigger, limits) {
6
+ let inherited = readCommunicationLanguage(db);
7
+ let messages;
8
+ if (trigger.scope.kind === "main_chat") {
9
+ messages = readMainChatMessageWindow(db, { limit: limits.mainChat }).items;
10
+ }
11
+ else {
12
+ messages =
13
+ readClarificationRoundMessagesWindow(db, trigger.scope.clarification_round_ref, {
14
+ anchor: "start",
15
+ limit: limits.round,
16
+ })?.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
+ }
31
+ return createLanguageContext({
32
+ scope: "reply",
33
+ inherited,
34
+ messages: [
35
+ ...messages.filter((message) => message.created_at < trigger.created_at ||
36
+ (message.created_at === trigger.created_at && message.id < trigger.id)),
37
+ trigger,
38
+ ].map(messageLanguageEvidence),
39
+ });
40
+ }
41
+ //# sourceMappingURL=language-context.js.map
@@ -0,0 +1,30 @@
1
+ import { type ChatRequestStatus, type OperationFailure } from "@tutti/shared/schemas/api";
2
+ import type { SqliteDatabase } from "../store/index.js";
3
+ export type ChatRequestRecord = {
4
+ trigger_message_id: string;
5
+ status: "queued" | "running" | "succeeded" | "failed";
6
+ phase: "queue" | "workspace_wait" | "answer" | "settled";
7
+ revision: number;
8
+ attempt_ref: string | null;
9
+ response_message_id: string | null;
10
+ failure_json: string | null;
11
+ created_at: string;
12
+ updated_at: string;
13
+ };
14
+ export declare function readChatRequest(db: SqliteDatabase, messageId: string): ChatRequestRecord | null;
15
+ export declare function chatRequestStatus(row: ChatRequestRecord): ChatRequestStatus;
16
+ export declare function insertChatRequest(db: SqliteDatabase, messageId: string, now: string, failure?: OperationFailure): void;
17
+ export declare function countQueuedChatRequests(db: SqliteDatabase): number;
18
+ export declare function nextQueuedChatRequest(db: SqliteDatabase): ChatRequestRecord | null;
19
+ /** CAS/fencing only. Lifecycle policy belongs to Chat Assistant, not the repository. */
20
+ export declare function updateChatRequest(db: SqliteDatabase, row: ChatRequestRecord, next: {
21
+ status: ChatRequestRecord["status"];
22
+ phase: ChatRequestRecord["phase"];
23
+ attemptRef?: string;
24
+ responseMessageId?: string;
25
+ failure?: OperationFailure;
26
+ }, now: string): ChatRequestRecord | null;
27
+ export declare function listRunningChatRequests(db: SqliteDatabase): ChatRequestRecord[];
28
+ /** Correlation only; never changes lifecycle state based on a progress callback. */
29
+ export declare function linkChatRequestInvocation(db: SqliteDatabase, row: ChatRequestRecord, invocationRef: string): void;
30
+ //# sourceMappingURL=requests.d.ts.map
@@ -0,0 +1,58 @@
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) {
24
+ db.prepare(`INSERT INTO chat_requests
25
+ (trigger_message_id, status, phase, failure_json, created_at, updated_at)
26
+ VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(trigger_message_id) DO NOTHING`).run(messageId, failure ? "failed" : "queued", failure ? "settled" : "queue", failure ? JSON.stringify(failure) : null, now, now);
27
+ }
28
+ export function countQueuedChatRequests(db) {
29
+ return db
30
+ .prepare("SELECT count(*) AS count FROM chat_requests WHERE status = 'queued'")
31
+ .get().count;
32
+ }
33
+ export function nextQueuedChatRequest(db) {
34
+ return (db
35
+ .prepare("SELECT * FROM chat_requests WHERE status = 'queued' ORDER BY enqueue_order LIMIT 1")
36
+ .get() ?? null);
37
+ }
38
+ /** CAS/fencing only. Lifecycle policy belongs to Chat Assistant, not the repository. */
39
+ export function updateChatRequest(db, row, next, now) {
40
+ const changed = db
41
+ .prepare(`UPDATE chat_requests SET status = ?, phase = ?,
42
+ attempt_ref = ?, response_message_id = ?, failure_json = ?, updated_at = ?, revision = revision + 1
43
+ WHERE trigger_message_id = ? AND revision = ?`)
44
+ .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;
45
+ return changed === 0 ? null : readChatRequest(db, row.trigger_message_id);
46
+ }
47
+ export function listRunningChatRequests(db) {
48
+ return db
49
+ .prepare("SELECT * FROM chat_requests WHERE status = 'running' ORDER BY created_at, trigger_message_id")
50
+ .all();
51
+ }
52
+ /** Correlation only; never changes lifecycle state based on a progress callback. */
53
+ export function linkChatRequestInvocation(db, row, invocationRef) {
54
+ db.prepare(`INSERT OR IGNORE INTO chat_request_invocations(message_id, attempt_ref, invocation_ref)
55
+ SELECT trigger_message_id, attempt_ref, ? FROM chat_requests
56
+ WHERE trigger_message_id = ? AND attempt_ref = ? AND status = 'running'`).run(invocationRef, row.trigger_message_id, row.attempt_ref);
57
+ }
58
+ //# 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;
@@ -13,8 +13,9 @@ export declare function readSubmittedClarificationMessages(db: SqliteDatabase, r
13
13
  is_current_round: boolean;
14
14
  };
15
15
  refs?: {
16
- task_id?: string;
17
16
  activity_ref?: string;
17
+ workflow_ref?: string;
18
+ task_id?: string;
18
19
  artifact_feedback?: {
19
20
  summary?: string;
20
21
  kind: "artifact_feedback";
@@ -106,15 +107,14 @@ export declare function readSubmittedClarificationMessages(db: SqliteDatabase, r
106
107
  blob_oid?: string;
107
108
  path: string;
108
109
  name: string;
109
- source: "human_upload" | "tutti_output";
110
110
  category: "file" | "image";
111
+ source: "human_upload" | "tutti_output";
111
112
  size_bytes: number;
112
113
  text_read: {
113
114
  blocked_reason?: "unknown" | "binary" | "too_large" | "unsupported_encoding" | "restricted_path" | "secret_like";
114
115
  can_read: boolean;
115
116
  };
116
117
  };
117
- workflow_ref?: string;
118
118
  clarification_round_ref?: string;
119
119
  clarification_thread_ref?: string;
120
120
  task_result?: {
@@ -141,10 +141,10 @@ export declare function readSubmittedClarificationMessages(db: SqliteDatabase, r
141
141
  };
142
142
  worklist_compile?: {
143
143
  kind: "worklist_compile";
144
- title: string;
145
144
  workflow_ref: string;
145
+ title: string;
146
146
  tasks: {
147
- status: "failed" | "pending" | "done" | "todo" | "running";
147
+ status: "failed" | "pending" | "running" | "done" | "todo";
148
148
  task_id: string;
149
149
  title: string;
150
150
  summary: string;