@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.
- package/dist/chat-assistant/diagnostics.d.ts +10 -1
- 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 +16 -16
- package/dist/chat-assistant/failures.js +24 -80
- package/dist/chat-assistant/index.d.ts +4 -2
- package/dist/chat-assistant/index.js +33 -8
- 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 +5 -1
- package/dist/chat-assistant/requests.js +9 -3
- package/dist/collaboration-state/context-sync-attempts.d.ts +9 -0
- package/dist/collaboration-state/context-sync-attempts.js +13 -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/types.d.ts +2 -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/server-shell/cli/host-server-runtime.js +4 -1
- package/migrations/0026_context_sync_and_chat_locale.sql +12 -0
- package/migrations/README.md +2 -2
- package/package.json +1 -1
- package/web/assets/{homepage-motion-scene-BcMt0AVW.js → homepage-motion-scene-BHhw4CfM.js} +1 -1
- package/web/assets/{index-2eX37Jx6.js → index-5COc1Yku.js} +2 -2
- 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"
|
|
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"
|
|
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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
],
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
],
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
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({
|
|
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
|
-
|
|
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,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,
|