@xfey/tutti 0.1.128 → 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.
Files changed (33) hide show
  1. package/dist/chat-assistant/diagnostics.d.ts +10 -1
  2. package/dist/chat-assistant/failure-messages.d.ts +4 -0
  3. package/dist/chat-assistant/failure-messages.js +57 -0
  4. package/dist/chat-assistant/failures.d.ts +16 -16
  5. package/dist/chat-assistant/failures.js +24 -80
  6. package/dist/chat-assistant/index.d.ts +4 -2
  7. package/dist/chat-assistant/index.js +33 -8
  8. package/dist/chat-assistant/language-context.d.ts +2 -0
  9. package/dist/chat-assistant/language-context.js +17 -14
  10. package/dist/chat-assistant/requests.d.ts +5 -1
  11. package/dist/chat-assistant/requests.js +9 -3
  12. package/dist/collaboration-state/context-sync-attempts.d.ts +9 -0
  13. package/dist/collaboration-state/context-sync-attempts.js +13 -0
  14. package/dist/communication-language/notice-locale.d.ts +4 -0
  15. package/dist/communication-language/notice-locale.js +14 -0
  16. package/dist/control-plane/context-sync-completion.d.ts +7 -0
  17. package/dist/control-plane/context-sync-completion.js +27 -0
  18. package/dist/control-plane/context-sync.d.ts +1 -0
  19. package/dist/control-plane/context-sync.js +23 -0
  20. package/dist/control-plane/index.d.ts +13 -4
  21. package/dist/control-plane/index.js +30 -23
  22. package/dist/control-plane/types.d.ts +2 -0
  23. package/dist/providers/invocation-progress.d.ts +6 -0
  24. package/dist/providers/invocation-progress.js +24 -0
  25. package/dist/providers/openai/app-server/read-only-procedure.js +1 -0
  26. package/dist/providers/openai/chat-assistant.js +2 -1
  27. package/dist/server-shell/cli/host-server-runtime.js +4 -1
  28. package/migrations/0026_context_sync_and_chat_locale.sql +12 -0
  29. package/migrations/README.md +2 -2
  30. package/package.json +1 -1
  31. package/web/assets/{homepage-motion-scene-BcMt0AVW.js → homepage-motion-scene-BHhw4CfM.js} +1 -1
  32. package/web/assets/{index-2eX37Jx6.js → index-5COc1Yku.js} +2 -2
  33. package/web/index.html +1 -1
@@ -1,18 +1,27 @@
1
1
  import type { ProviderInvocationProgress } from "../providers/invocation-progress.js";
2
2
  import type { languageDiagnosticFields } from "../communication-language/index.js";
3
3
  import type { ChatFailureCode } from "./failures.js";
4
+ import type { WorkflowKind } from "@tutti/shared/domain";
5
+ export type ChatInspectionBlocker = {
6
+ activity_ref: string;
7
+ activity_kind: "run" | "procedure";
8
+ workflow_kind?: WorkflowKind;
9
+ };
4
10
  export type ChatAssistantLogger = {
5
11
  info: (fields: Record<string, unknown>, message: string) => void;
6
12
  warn: (fields: Record<string, unknown>, message: string) => void;
7
13
  };
8
14
  type ChatAssistantProgress = ProviderInvocationProgress | ({
9
15
  stage: "language_context";
10
- } & ReturnType<typeof languageDiagnosticFields>) | {
16
+ } & ReturnType<typeof languageDiagnosticFields>) | ({
17
+ stage: "inspection_blocked";
18
+ } & ChatInspectionBlocker) | {
11
19
  stage: "queued" | "inspection_wait" | "model_requested" | "reply_persisted" | "notification_failed" | "failed" | "skipped";
12
20
  reason_code?: "queue_full" | "stopping" | "context_unavailable" | "answer_failed" | "reply_failed" | "request_changed";
13
21
  error_code?: ChatFailureCode;
14
22
  diagnostic_id?: string;
15
23
  reference_source_count?: number;
24
+ queue_wait_ms?: number;
16
25
  };
17
26
  export declare function createChatAssistantDiagnostics(options: {
18
27
  logger?: ChatAssistantLogger;
@@ -0,0 +1,4 @@
1
+ import type { NoticeLocale } from "../communication-language/notice-locale.js";
2
+ import type { ChatFailureCode } from "./failures.js";
3
+ export declare function chatFailureCopy(code: ChatFailureCode, locale: NoticeLocale): string;
4
+ //# sourceMappingURL=failure-messages.d.ts.map
@@ -0,0 +1,57 @@
1
+ const messages = {
2
+ en: {
3
+ reference_authorization_required: "Authorize document access using the group card, then send your question again. Authorization does not restart this answer.",
4
+ chat_queue_full: "Tutti is busy answering other questions. Try again shortly.",
5
+ chat_context_unavailable: "This question's conversation is no longer available.",
6
+ host_restarted: "The Host restarted before this answer was saved. Send your question again.",
7
+ provider_not_configured: "Ask the project owner to configure model access, then send your question again.",
8
+ provider_auth_invalid: "Model access could not be verified. Ask the project owner to check it.",
9
+ provider_quota_or_billing_required: "Model credit is unavailable. Ask the project owner to check the account.",
10
+ provider_rate_limited: "The model service is busy. Wait a moment, then send your question again.",
11
+ provider_network_error: "The model service could not be reached. Try again when the connection is available.",
12
+ provider_model_unavailable: "The selected model is unavailable. Ask the project owner to check model access.",
13
+ provider_timeout: "The model did not finish answering in time. You can send your question again.",
14
+ provider_output_invalid: "The model did not return a usable answer. You can send your question again.",
15
+ provider_lifecycle_failed: "The model process could not close safely. Restart the Host before trying again.",
16
+ chat_reply_persistence_failed: "The answer could not be saved. Check the Host diagnostics before trying again.",
17
+ internal_error: "Tutti could not complete this read-only answer. Share the diagnostic ID if this persists.",
18
+ },
19
+ "zh-Hans": {
20
+ reference_authorization_required: "请先通过群内卡片授权文档访问,然后重新提问。授权不会自动重启这次回答。",
21
+ chat_queue_full: "Tutti 正在回答其他问题,请稍后再试。",
22
+ chat_context_unavailable: "这条问题所在的对话已不可用。",
23
+ host_restarted: "本机运行时在回答保存前重启了,请重新发送问题。",
24
+ provider_not_configured: "请联系项目负责人配置模型接入,然后重新提问。",
25
+ provider_auth_invalid: "无法验证模型接入,请联系项目负责人检查配置。",
26
+ provider_quota_or_billing_required: "模型额度不可用,请联系项目负责人检查账号。",
27
+ provider_rate_limited: "模型服务繁忙,请稍后重新发送问题。",
28
+ provider_network_error: "无法连接模型服务,请在连接恢复后再试。",
29
+ provider_model_unavailable: "当前模型不可用,请联系项目负责人检查模型接入。",
30
+ provider_timeout: "这次回答超时了,你可以重新发送问题。",
31
+ provider_output_invalid: "模型没有返回可用的回答,你可以重新发送问题。",
32
+ provider_lifecycle_failed: "模型进程未能安全关闭,请重启本机运行时后再试。",
33
+ chat_reply_persistence_failed: "回答未能保存,请先检查本机运行时的诊断信息。",
34
+ internal_error: "Tutti 未能完成这次只读回答。如果问题持续,请提供诊断编号。",
35
+ },
36
+ "zh-Hant": {
37
+ reference_authorization_required: "請先透過群組卡片授權文件存取,然後重新提問。授權不會自動重新啟動這次回答。",
38
+ chat_queue_full: "Tutti 正在回答其他問題,請稍後再試。",
39
+ chat_context_unavailable: "這個問題所在的對話已無法使用。",
40
+ host_restarted: "本機執行環境在回答儲存前重新啟動了,請重新傳送問題。",
41
+ provider_not_configured: "請聯絡專案負責人設定模型連線,然後重新提問。",
42
+ provider_auth_invalid: "無法驗證模型連線,請聯絡專案負責人檢查設定。",
43
+ provider_quota_or_billing_required: "模型額度無法使用,請聯絡專案負責人檢查帳號。",
44
+ provider_rate_limited: "模型服務忙碌,請稍後重新傳送問題。",
45
+ provider_network_error: "無法連線至模型服務,請在連線恢復後再試。",
46
+ provider_model_unavailable: "目前的模型無法使用,請聯絡專案負責人檢查模型連線。",
47
+ provider_timeout: "這次回答逾時了,你可以重新傳送問題。",
48
+ provider_output_invalid: "模型沒有傳回可用的回答,你可以重新傳送問題。",
49
+ provider_lifecycle_failed: "模型程序未能安全關閉,請重新啟動本機執行環境後再試。",
50
+ chat_reply_persistence_failed: "回答未能儲存,請先檢查本機執行環境的診斷資訊。",
51
+ internal_error: "Tutti 未能完成這次唯讀回答。如果問題持續,請提供診斷編號。",
52
+ },
53
+ };
54
+ export function chatFailureCopy(code, locale) {
55
+ return messages[locale][code];
56
+ }
57
+ //# sourceMappingURL=failure-messages.js.map
@@ -1,27 +1,27 @@
1
1
  import type { OperationFailure } from "@tutti/shared/schemas/api";
2
2
  declare const failures: {
3
- readonly reference_authorization_required: readonly ["access", "none", "Authorize document access using the group card, then send your question again. Authorization does not restart this answer."];
4
- readonly chat_queue_full: readonly ["capacity", "resend_request", "Tutti is busy answering other questions. Try again shortly."];
5
- readonly chat_context_unavailable: readonly ["interrupted", "none", "This question's conversation is no longer available."];
6
- readonly host_restarted: readonly ["interrupted", "resend_request", "The Host restarted before this answer was saved. Send your question again."];
7
- readonly provider_not_configured: readonly ["configuration", "check_provider", "Ask the project owner to configure model access, then send your question again."];
8
- readonly provider_auth_invalid: readonly ["access", "check_provider", "Model access could not be verified. Ask the project owner to check it."];
9
- readonly provider_quota_or_billing_required: readonly ["access", "check_provider", "Model credit is unavailable. Ask the project owner to check the account."];
10
- readonly provider_rate_limited: readonly ["capacity", "resend_request", "The model service is busy. Wait a moment, then send your question again."];
11
- readonly provider_network_error: readonly ["dependency", "resend_request", "The model service could not be reached. Try again when the connection is available."];
12
- readonly provider_model_unavailable: readonly ["configuration", "check_provider", "The selected model is unavailable. Ask the project owner to check model access."];
13
- readonly provider_timeout: readonly ["dependency", "resend_request", "The model did not finish answering in time. You can send your question again."];
14
- readonly provider_output_invalid: readonly ["dependency", "resend_request", "The model did not return a usable answer. You can send your question again."];
15
- readonly provider_lifecycle_failed: readonly ["internal", "restart_host", "The model process could not close safely. Restart the Host before trying again."];
16
- readonly chat_reply_persistence_failed: readonly ["internal", "contact_support", "The answer could not be saved. Check the Host diagnostics before trying again."];
17
- readonly internal_error: readonly ["internal", "contact_support", "Tutti could not complete this read-only answer. Share the diagnostic ID if this persists."];
3
+ readonly reference_authorization_required: readonly ["access", "none"];
4
+ readonly chat_queue_full: readonly ["capacity", "resend_request"];
5
+ readonly chat_context_unavailable: readonly ["interrupted", "none"];
6
+ readonly host_restarted: readonly ["interrupted", "resend_request"];
7
+ readonly provider_not_configured: readonly ["configuration", "check_provider"];
8
+ readonly provider_auth_invalid: readonly ["access", "check_provider"];
9
+ readonly provider_quota_or_billing_required: readonly ["access", "check_provider"];
10
+ readonly provider_rate_limited: readonly ["capacity", "resend_request"];
11
+ readonly provider_network_error: readonly ["dependency", "resend_request"];
12
+ readonly provider_model_unavailable: readonly ["configuration", "check_provider"];
13
+ readonly provider_timeout: readonly ["dependency", "resend_request"];
14
+ readonly provider_output_invalid: readonly ["dependency", "resend_request"];
15
+ readonly provider_lifecycle_failed: readonly ["internal", "restart_host"];
16
+ readonly chat_reply_persistence_failed: readonly ["internal", "contact_support"];
17
+ readonly internal_error: readonly ["internal", "contact_support"];
18
18
  };
19
19
  export type ChatFailureCode = keyof typeof failures;
20
20
  type ChatFailure = OperationFailure & {
21
21
  code: ChatFailureCode;
22
22
  };
23
23
  export declare function chatFailure(code: ChatFailureCode, diagnosticId?: string): ChatFailure;
24
- export declare function chatFailureMessage(failure: OperationFailure): string;
24
+ export declare function chatFailureMessage(failure: OperationFailure, locale?: string): string;
25
25
  export declare function classifyChatFailure(error: unknown, diagnosticId: string): ChatFailure;
26
26
  export {};
27
27
  //# sourceMappingURL=failures.d.ts.map
@@ -1,91 +1,35 @@
1
1
  import { createEventRef } from "@tutti/shared/ids";
2
2
  import { providerFailureCode } from "../providers/failure-classification.js";
3
+ import { noticeLocale } from "../communication-language/notice-locale.js";
4
+ import { chatFailureCopy } from "./failure-messages.js";
3
5
  const failures = {
4
- reference_authorization_required: [
5
- "access",
6
- "none",
7
- "Authorize document access using the group card, then send your question again. Authorization does not restart this answer.",
8
- ],
9
- chat_queue_full: [
10
- "capacity",
11
- "resend_request",
12
- "Tutti is busy answering other questions. Try again shortly.",
13
- ],
14
- chat_context_unavailable: [
15
- "interrupted",
16
- "none",
17
- "This question's conversation is no longer available.",
18
- ],
19
- host_restarted: [
20
- "interrupted",
21
- "resend_request",
22
- "The Host restarted before this answer was saved. Send your question again.",
23
- ],
24
- provider_not_configured: [
25
- "configuration",
26
- "check_provider",
27
- "Ask the project owner to configure model access, then send your question again.",
28
- ],
29
- provider_auth_invalid: [
30
- "access",
31
- "check_provider",
32
- "Model access could not be verified. Ask the project owner to check it.",
33
- ],
34
- provider_quota_or_billing_required: [
35
- "access",
36
- "check_provider",
37
- "Model credit is unavailable. Ask the project owner to check the account.",
38
- ],
39
- provider_rate_limited: [
40
- "capacity",
41
- "resend_request",
42
- "The model service is busy. Wait a moment, then send your question again.",
43
- ],
44
- provider_network_error: [
45
- "dependency",
46
- "resend_request",
47
- "The model service could not be reached. Try again when the connection is available.",
48
- ],
49
- provider_model_unavailable: [
50
- "configuration",
51
- "check_provider",
52
- "The selected model is unavailable. Ask the project owner to check model access.",
53
- ],
54
- provider_timeout: [
55
- "dependency",
56
- "resend_request",
57
- "The model did not finish answering in time. You can send your question again.",
58
- ],
59
- provider_output_invalid: [
60
- "dependency",
61
- "resend_request",
62
- "The model did not return a usable answer. You can send your question again.",
63
- ],
64
- provider_lifecycle_failed: [
65
- "internal",
66
- "restart_host",
67
- "The model process could not close safely. Restart the Host before trying again.",
68
- ],
69
- chat_reply_persistence_failed: [
70
- "internal",
71
- "contact_support",
72
- "The answer could not be saved. Check the Host diagnostics before trying again.",
73
- ],
74
- internal_error: [
75
- "internal",
76
- "contact_support",
77
- "Tutti could not complete this read-only answer. Share the diagnostic ID if this persists.",
78
- ],
6
+ reference_authorization_required: ["access", "none"],
7
+ chat_queue_full: ["capacity", "resend_request"],
8
+ chat_context_unavailable: ["interrupted", "none"],
9
+ host_restarted: ["interrupted", "resend_request"],
10
+ provider_not_configured: ["configuration", "check_provider"],
11
+ provider_auth_invalid: ["access", "check_provider"],
12
+ provider_quota_or_billing_required: ["access", "check_provider"],
13
+ provider_rate_limited: ["capacity", "resend_request"],
14
+ provider_network_error: ["dependency", "resend_request"],
15
+ provider_model_unavailable: ["configuration", "check_provider"],
16
+ provider_timeout: ["dependency", "resend_request"],
17
+ provider_output_invalid: ["dependency", "resend_request"],
18
+ provider_lifecycle_failed: ["internal", "restart_host"],
19
+ chat_reply_persistence_failed: ["internal", "contact_support"],
20
+ internal_error: ["internal", "contact_support"],
79
21
  };
80
22
  export function chatFailure(code, diagnosticId = createEventRef()) {
81
23
  const [category, recovery] = failures[code];
82
24
  return { code, category, recovery, diagnostic_id: diagnosticId };
83
25
  }
84
- export function chatFailureMessage(failure) {
85
- const entry = Object.hasOwn(failures, failure.code)
86
- ? failures[failure.code]
87
- : failures.internal_error;
88
- return `${entry[2]} Diagnostic ID: ${failure.diagnostic_id}`;
26
+ export function chatFailureMessage(failure, locale) {
27
+ const language = noticeLocale(locale);
28
+ const code = Object.hasOwn(failures, failure.code)
29
+ ? failure.code
30
+ : "internal_error";
31
+ const label = language === "zh-Hans" ? "诊断编号" : language === "zh-Hant" ? "診斷編號" : "Diagnostic ID";
32
+ return `${chatFailureCopy(code, language)} ${label}: ${failure.diagnostic_id}`;
89
33
  }
90
34
  // Allowlisted error facts only; neither error.message nor diagnostic callbacks
91
35
  // establish business truth. Unknown failures remain non-automatically-retryable.
@@ -1,6 +1,6 @@
1
1
  import { type ProjectId } from "@tutti/shared/ids";
2
2
  import type { MessageProjection } from "@tutti/shared/schemas/api";
3
- import { type ChatAssistantLogger } from "./diagnostics.js";
3
+ import { type ChatAssistantLogger, type ChatInspectionBlocker } from "./diagnostics.js";
4
4
  import type { ProviderInvocationProgressObserver } from "../providers/invocation-progress.js";
5
5
  import { type HostProjectStore, type SqliteDatabase } from "../store/index.js";
6
6
  import type { WorkspaceEventBus } from "../server-shell/http/workspace-events.js";
@@ -55,7 +55,8 @@ export type ReadOnlyChatAssistantOptions = {
55
55
  triggerMessage: MessageProjection;
56
56
  responseMessage: MessageProjection;
57
57
  }) => void | (() => void);
58
- waitForInspectionAvailability?: () => Promise<void>;
58
+ waitForInspectionAvailability?: (onBlocked: (blocker: ChatInspectionBlocker) => void) => Promise<void>;
59
+ onRequestsChanged?: () => void;
59
60
  maxQueuedMentions?: number;
60
61
  now?: () => Date;
61
62
  };
@@ -83,6 +84,7 @@ export declare class ReadOnlyChatAssistant implements ReadOnlyChatAssistantHandl
83
84
  private pump;
84
85
  private reportStorageFailure;
85
86
  private pumpNext;
87
+ private notifyRequestsChanged;
86
88
  private resolveIdleIfNeeded;
87
89
  private buildMainChatContext;
88
90
  private buildClarificationRoundContext;
@@ -1,10 +1,11 @@
1
1
  import { linkChatRequestInvocation } from "./requests.js";
2
2
  import { createEventRef, ID_PREFIXES, isPrefixedId, } from "@tutti/shared/ids";
3
- import { createChatAssistantDiagnostics } from "./diagnostics.js";
3
+ import { createChatAssistantDiagnostics, } from "./diagnostics.js";
4
4
  import { createClarificationRoundAgentMessage, createMainChatAgentMessage, readClarificationRoundMessagesWindow, readClarificationRoundProjection, readMainChatMessageWindow, readMainChatMessagesBeforeCursor, readMessageProjectionById, } from "../collaboration-state/index.js";
5
5
  import { renderLanguageAwarePrompt } from "../communication-language/prompt.js";
6
6
  import { languageDiagnosticFields } from "../communication-language/index.js";
7
- import { chatLanguageContext } from "./language-context.js";
7
+ import { chatLanguageContext, chatLanguagePreference } from "./language-context.js";
8
+ import { noticeLocale } from "../communication-language/notice-locale.js";
8
9
  import { withHostStoreTransaction, } from "../store/index.js";
9
10
  import { messageTextForModel, messageTextForTuttiMention, } from "../collaboration-state/message-context.js";
10
11
  import { externalReferenceMessageContext } from "../reference-access/message-context.js";
@@ -140,7 +141,7 @@ export class ReadOnlyChatAssistant {
140
141
  if (recovered && message) {
141
142
  this.report(message, recovered)({ stage: "failed", error_code: "host_restarted", diagnostic_id: failure.diagnostic_id });
142
143
  try {
143
- this.appendAssistantMessageForTrigger(message, chatFailureMessage(failure), recovered, failure);
144
+ this.appendAssistantMessageForTrigger(message, chatFailureMessage(failure, recovered.notice_locale), recovered, failure);
144
145
  }
145
146
  catch {
146
147
  this.report(message, recovered)({ stage: "failed", reason_code: "reply_failed" });
@@ -150,6 +151,7 @@ export class ReadOnlyChatAssistant {
150
151
  // A lost post-commit wake must not strand a durable queued request.
151
152
  this.recoveryTimer = setInterval(() => this.pump(), 1_000);
152
153
  this.recoveryTimer.unref?.();
154
+ this.notifyRequestsChanged();
153
155
  this.pump();
154
156
  }
155
157
  register(tx, message) {
@@ -161,7 +163,7 @@ export class ReadOnlyChatAssistant {
161
163
  return;
162
164
  insertChatRequest(tx, message.id, this.now(), countQueuedChatRequests(tx) >= this.maxQueuedMentions
163
165
  ? chatFailure("chat_queue_full")
164
- : undefined);
166
+ : undefined, noticeLocale(chatLanguagePreference(tx, message)?.locale));
165
167
  }
166
168
  now() {
167
169
  return (this.options.now?.() ?? new Date()).toISOString();
@@ -194,7 +196,7 @@ export class ReadOnlyChatAssistant {
194
196
  diagnostic_id: failure.diagnostic_id,
195
197
  });
196
198
  try {
197
- this.appendAssistantMessageForTrigger(input.message, chatFailureMessage(failure), request, failure);
199
+ this.appendAssistantMessageForTrigger(input.message, chatFailureMessage(failure, request.notice_locale), request, failure);
198
200
  }
199
201
  catch {
200
202
  // The original message and failed request are already accepted.
@@ -268,12 +270,16 @@ export class ReadOnlyChatAssistant {
268
270
  }, this.now());
269
271
  if (request === null)
270
272
  return;
273
+ this.notifyRequestsChanged();
271
274
  this.activeAttemptRef = request.attempt_ref;
272
275
  const message = readMessageProjectionById(this.options.store.db, request.trigger_message_id);
273
276
  if (message === null)
274
277
  throw new Error("chat_request_source_missing");
275
278
  const report = this.report(message, request);
276
- report({ stage: "queued" });
279
+ report({
280
+ stage: "queued",
281
+ queue_wait_ms: Math.max(0, Date.parse(this.now()) - Date.parse(request.created_at)),
282
+ });
277
283
  const task = this.respond(message, request, report).catch(() => {
278
284
  // Do not overwrite a committed success when only notification delivery failed.
279
285
  try {
@@ -310,10 +316,29 @@ export class ReadOnlyChatAssistant {
310
316
  if (this.active === task) {
311
317
  this.active = null;
312
318
  this.activeAttemptRef = undefined;
319
+ this.notifyRequestsChanged();
313
320
  this.pump();
314
321
  }
315
322
  });
316
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
+ }
317
342
  resolveIdleIfNeeded() {
318
343
  if (this.active !== null ||
319
344
  (this.accepting && nextQueuedChatRequest(this.options.store.db) !== null)) {
@@ -498,7 +523,7 @@ export class ReadOnlyChatAssistant {
498
523
  let readSources = [];
499
524
  try {
500
525
  report({ stage: "inspection_wait" });
501
- await this.options.waitForInspectionAvailability?.();
526
+ await this.options.waitForInspectionAvailability?.((blocker) => report({ stage: "inspection_blocked", ...blocker }));
502
527
  if (!this.accepting) {
503
528
  updateChatRequest(this.options.store.db, request, { status: "queued", phase: "queue" }, this.now());
504
529
  return;
@@ -552,7 +577,7 @@ export class ReadOnlyChatAssistant {
552
577
  error_code: failure.code,
553
578
  diagnostic_id: failure.diagnostic_id,
554
579
  });
555
- this.appendAssistantMessageForTrigger(triggerMessage, chatFailureMessage(failure), request, failure);
580
+ this.appendAssistantMessageForTrigger(triggerMessage, chatFailureMessage(failure, request.notice_locale), request, failure);
556
581
  report({ stage: "reply_persisted" });
557
582
  return;
558
583
  }
@@ -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
- let inherited = readCommunicationLanguage(db);
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",
@@ -1,5 +1,6 @@
1
1
  import { type ChatRequestStatus, type OperationFailure } from "@tutti/shared/schemas/api";
2
2
  import type { SqliteDatabase } from "../store/index.js";
3
+ import type { NoticeLocale } from "../communication-language/notice-locale.js";
3
4
  export type ChatRequestRecord = {
4
5
  trigger_message_id: string;
5
6
  status: "queued" | "running" | "succeeded" | "failed";
@@ -10,10 +11,13 @@ export type ChatRequestRecord = {
10
11
  failure_json: string | null;
11
12
  created_at: string;
12
13
  updated_at: string;
14
+ notice_locale: NoticeLocale;
13
15
  };
14
16
  export declare function readChatRequest(db: SqliteDatabase, messageId: string): ChatRequestRecord | null;
15
17
  export declare function chatRequestStatus(row: ChatRequestRecord): ChatRequestStatus;
16
- export declare function insertChatRequest(db: SqliteDatabase, messageId: string, now: string, failure?: OperationFailure): void;
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;
17
21
  export declare function countQueuedChatRequests(db: SqliteDatabase): number;
18
22
  export declare function nextQueuedChatRequest(db: SqliteDatabase): ChatRequestRecord | null;
19
23
  /** CAS/fencing only. Lifecycle policy belongs to Chat Assistant, not the repository. */
@@ -20,10 +20,16 @@ export function chatRequestStatus(row) {
20
20
  throw new Error("chat_request_invalid");
21
21
  return value;
22
22
  }
23
- export function insertChatRequest(db, messageId, now, failure) {
23
+ export function insertChatRequest(db, messageId, now, failure, noticeLocale = "en") {
24
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);
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);
27
33
  }
28
34
  export function countQueuedChatRequests(db) {
29
35
  return db
@@ -0,0 +1,9 @@
1
+ import type { SqliteDatabase } from "../store/index.js";
2
+ export declare function hasContextSyncCompletionAttempt(db: SqliteDatabase, batchKey: string): boolean;
3
+ /** Persist before invoking the provider; failure/unknown outcome is not a retry permit. */
4
+ export declare function recordContextSyncCompletionAttempt(db: SqliteDatabase, input: {
5
+ batchKey: string;
6
+ workflowRef: string;
7
+ startedAt: string;
8
+ }): boolean;
9
+ //# sourceMappingURL=context-sync-attempts.d.ts.map
@@ -0,0 +1,13 @@
1
+ export function hasContextSyncCompletionAttempt(db, batchKey) {
2
+ return (db
3
+ .prepare("SELECT 1 AS present FROM context_sync_completion_attempts WHERE batch_key = ?")
4
+ .get(batchKey) !== undefined);
5
+ }
6
+ /** Persist before invoking the provider; failure/unknown outcome is not a retry permit. */
7
+ export function recordContextSyncCompletionAttempt(db, input) {
8
+ return (db
9
+ .prepare(`INSERT OR IGNORE INTO context_sync_completion_attempts(batch_key, workflow_ref, started_at)
10
+ VALUES (?, ?, ?)`)
11
+ .run(input.batchKey, input.workflowRef, input.startedAt).changes === 1);
12
+ }
13
+ //# sourceMappingURL=context-sync-attempts.js.map
@@ -0,0 +1,4 @@
1
+ /** Small static notice catalog, not language detection or a UI locale setting. */
2
+ export type NoticeLocale = "en" | "zh-Hans" | "zh-Hant";
3
+ export declare function noticeLocale(locale?: string | null): NoticeLocale;
4
+ //# sourceMappingURL=notice-locale.d.ts.map
@@ -0,0 +1,14 @@
1
+ export function noticeLocale(locale) {
2
+ if (locale === undefined || locale === null)
3
+ return "en";
4
+ try {
5
+ const parsed = new Intl.Locale(locale);
6
+ if (parsed.language !== "zh")
7
+ return "en";
8
+ return parsed.maximize().script === "Hant" ? "zh-Hant" : "zh-Hans";
9
+ }
10
+ catch {
11
+ return "en";
12
+ }
13
+ }
14
+ //# sourceMappingURL=notice-locale.js.map
@@ -0,0 +1,7 @@
1
+ import type { SqliteDatabase } from "../store/index.js";
2
+ import type { ContextSyncWorkflowTrigger } from "./workflows/index.js";
3
+ export declare function pendingContextSyncCompletion(db: SqliteDatabase): {
4
+ key: string;
5
+ trigger: ContextSyncWorkflowTrigger;
6
+ } | undefined;
7
+ //# sourceMappingURL=context-sync-completion.d.ts.map
@@ -0,0 +1,27 @@
1
+ import { createHash } from "node:crypto";
2
+ import { hasContextSyncCompletionAttempt } from "../collaboration-state/context-sync-attempts.js";
3
+ import { readWorklistProjection } from "../collaboration-state/index.js";
4
+ export function pendingContextSyncCompletion(db) {
5
+ const worklist = readWorklistProjection(db);
6
+ const tasks = worklist.modules
7
+ .flatMap((entry) => entry.tasks)
8
+ .sort((a, b) => (a.id < b.id ? -1 : 1));
9
+ if (tasks.length === 0 || tasks.some((task) => task.status !== "done"))
10
+ return undefined;
11
+ // A changed result on the same task is a new completion. Source messages,
12
+ // ordering, labels and context_sync's own Git commit are deliberately absent.
13
+ const key = createHash("sha256")
14
+ .update(JSON.stringify(tasks.map((task) => [task.id, task.latest_run_result_id])))
15
+ .digest("hex");
16
+ if (hasContextSyncCompletionAttempt(db, key))
17
+ return undefined;
18
+ return {
19
+ key,
20
+ trigger: {
21
+ kind: "worklist_all_tasks_completed",
22
+ completed_task_count: tasks.length,
23
+ module_count: worklist.modules.length,
24
+ },
25
+ };
26
+ }
27
+ //# sourceMappingURL=context-sync-completion.js.map
@@ -10,6 +10,7 @@ export declare function startContextSyncAnalysisProcedure(input: {
10
10
  store: HostProjectStore;
11
11
  events: WorkspaceEventBus;
12
12
  trigger: ContextSyncWorkflowTrigger;
13
+ completionBatchKey?: string;
13
14
  resolveWorkflowRunner: ProcedureWorkflowRunnerResolver;
14
15
  readActiveActivity: () => {
15
16
  activity_ref: ActivityRef;
@@ -3,6 +3,7 @@ import { projectLanguageContext, logLanguageContext } from "./communication-lang
3
3
  import { recordProjectTimelineEvent } from "../project-timeline/index.js";
4
4
  import { syncProjectDocs } from "../workspace-ops/index.js";
5
5
  import { logProcedureExecutionError } from "./procedure-logging.js";
6
+ import { recordContextSyncCompletionAttempt } from "../collaboration-state/context-sync-attempts.js";
6
7
  import { projectBriefShouldRefreshForUpdatedPaths, refreshProjectBriefProjection, } from "./project-brief-refresh.js";
7
8
  function publishContextSyncTimelineChanged(input) {
8
9
  input.events.publish({
@@ -63,6 +64,28 @@ export function startContextSyncAnalysisProcedure(input) {
63
64
  error,
64
65
  }),
65
66
  execute: async (context) => {
67
+ if (input.completionBatchKey !== undefined) {
68
+ const recorded = recordContextSyncCompletionAttempt(input.store.db, {
69
+ batchKey: input.completionBatchKey,
70
+ workflowRef,
71
+ startedAt: input.now().toISOString(),
72
+ });
73
+ if (!recorded)
74
+ return {
75
+ kind: "completed",
76
+ summary: "These task results were already checked for document maintenance.",
77
+ };
78
+ try {
79
+ input.logger?.info({
80
+ workflow_ref: workflowRef,
81
+ activity_ref: context.activity_ref,
82
+ trigger: "worklist_all_tasks_completed",
83
+ }, "Automatic context sync attempt recorded");
84
+ }
85
+ catch {
86
+ /* Logging must not prevent the recorded maintenance attempt. */
87
+ }
88
+ }
66
89
  const language = projectLanguageContext(input.store.db, "documents");
67
90
  logLanguageContext(input.logger, language, {
68
91
  workflow_ref: workflowRef,