@xfey/tutti 0.1.127 → 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.
- package/dist/approvals/decision-command.d.ts +2 -2
- package/dist/chat-assistant/diagnostics.d.ts +7 -2
- package/dist/chat-assistant/diagnostics.js +5 -2
- package/dist/chat-assistant/failures.d.ts +27 -0
- package/dist/chat-assistant/failures.js +102 -0
- package/dist/chat-assistant/index.d.ts +14 -2
- package/dist/chat-assistant/index.js +226 -48
- package/dist/chat-assistant/requests.d.ts +30 -0
- package/dist/chat-assistant/requests.js +58 -0
- package/dist/collaboration-ingestion/clarification.js +3 -1
- package/dist/collaboration-ingestion/main-chat.js +1 -0
- package/dist/collaboration-ingestion/types.d.ts +2 -0
- package/dist/collaboration-state/clarification-snapshot.d.ts +5 -5
- package/dist/collaboration-state/task-compile-context.d.ts +1 -1
- package/dist/collaboration-state/task-compile-context.js +11 -1
- package/dist/collaboration-state/task-compile-requests.d.ts +6 -0
- package/dist/collaboration-state/task-compile-requests.js +16 -0
- package/dist/control-plane/task-compile-continuation.js +2 -0
- package/dist/control-plane/task-compile-request-status.d.ts +6 -0
- package/dist/control-plane/task-compile-request-status.js +70 -0
- package/dist/control-plane/task-compile-start.js +40 -2
- package/dist/providers/failure-classification.d.ts +23 -0
- package/dist/providers/failure-classification.js +43 -0
- package/dist/reference-access/read-evidence.d.ts +2 -0
- package/dist/reference-access/read-evidence.js +5 -0
- package/dist/reference-access/service.d.ts +2 -0
- package/dist/reference-access/service.js +2 -0
- package/dist/runtime-failures.js +15 -1
- package/dist/server-shell/cli/external-reference-runtime.js +6 -3
- package/dist/server-shell/cli/host-server-runtime.js +4 -1
- package/dist/server-shell/cli/relay-registration.js +1 -0
- package/dist/server-shell/http/routes/project-api/messages-routes.js +22 -1
- package/dist/server-shell/http/routes/project-api/openapi-collaboration-routes.d.ts +140 -5
- package/dist/server-shell/http/routes/project-api/openapi-collaboration-routes.js +29 -2
- package/dist/server-shell/http/routes/project-api/openapi-skills-routes.d.ts +2 -0
- package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +6 -0
- package/dist/server-shell/http/routes/project-api/openapi.d.ts +148 -5
- package/dist/server-shell/http/routes/project-api/schemas.d.ts +6 -0
- package/dist/server-shell/http/routes/project-api/schemas.js +1 -0
- package/dist/server-shell/http/routes/project-api/types.d.ts +1 -0
- package/dist/server-shell/http/routes/project-api/workflow-routes.js +39 -3
- package/dist/server-shell/http/routes/project-api/workspace-projections.js +1 -0
- package/dist/server-shell/http/validation.js +8 -2
- package/dist/server-shell/local-console/managed-project-service.d.ts +1 -1
- package/dist/server-shell/managed-access/transport.d.ts +1 -1
- package/migrations/0024_chat_requests.sql +19 -0
- package/migrations/0025_task_compile_requests.sql +50 -0
- package/migrations/README.md +3 -1
- package/node_modules/@tutti/relay-client/dist/host-control.d.ts +1 -0
- package/node_modules/@tutti/relay-client/dist/host-control.js +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/operation-status.d.ts +174 -0
- package/node_modules/@tutti/shared/dist/schemas/api/operation-status.js +75 -0
- package/node_modules/@tutti/shared/dist/schemas/api/primitives.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/primitives.js +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.d.ts +1 -0
- package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.js +2 -1
- package/package.json +1 -1
- package/web/assets/{homepage-motion-scene-DyEmWwvt.js → homepage-motion-scene-BcMt0AVW.js} +1 -1
- package/web/assets/index-2eX37Jx6.js +70 -0
- package/web/assets/index-BJvKuUiT.css +1 -0
- package/web/index.html +2 -2
- package/web/assets/index-By3idT4U.js +0 -70
- package/web/assets/index-DSBjOZmJ.css +0 -1
|
@@ -25,8 +25,8 @@ export declare function executeApprovalDecisionCommand(input: {
|
|
|
25
25
|
state: "pending";
|
|
26
26
|
request: {
|
|
27
27
|
expires_at?: string;
|
|
28
|
-
task_id?: string;
|
|
29
28
|
activity_ref?: string;
|
|
29
|
+
task_id?: string;
|
|
30
30
|
reason?: string;
|
|
31
31
|
file_change?: {
|
|
32
32
|
paths?: string[];
|
|
@@ -69,8 +69,8 @@ export declare function executeApprovalDecisionCommand(input: {
|
|
|
69
69
|
state: "resolved";
|
|
70
70
|
request: {
|
|
71
71
|
expires_at?: string;
|
|
72
|
-
task_id?: string;
|
|
73
72
|
activity_ref?: string;
|
|
73
|
+
task_id?: string;
|
|
74
74
|
reason?: string;
|
|
75
75
|
file_change?: {
|
|
76
76
|
paths?: string[];
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ProviderInvocationProgress } from "../providers/invocation-progress.js";
|
|
2
2
|
import type { languageDiagnosticFields } from "../communication-language/index.js";
|
|
3
|
+
import type { ChatFailureCode } from "./failures.js";
|
|
3
4
|
export type ChatAssistantLogger = {
|
|
4
5
|
info: (fields: Record<string, unknown>, message: string) => void;
|
|
5
6
|
warn: (fields: Record<string, unknown>, message: string) => void;
|
|
@@ -7,8 +8,10 @@ export type ChatAssistantLogger = {
|
|
|
7
8
|
type ChatAssistantProgress = ProviderInvocationProgress | ({
|
|
8
9
|
stage: "language_context";
|
|
9
10
|
} & ReturnType<typeof languageDiagnosticFields>) | {
|
|
10
|
-
stage: "queued" | "inspection_wait" | "model_requested" | "reply_persisted" | "failed" | "skipped";
|
|
11
|
-
reason_code?: "queue_full" | "stopping" | "context_unavailable" | "answer_failed" | "reply_failed";
|
|
11
|
+
stage: "queued" | "inspection_wait" | "model_requested" | "reply_persisted" | "notification_failed" | "failed" | "skipped";
|
|
12
|
+
reason_code?: "queue_full" | "stopping" | "context_unavailable" | "answer_failed" | "reply_failed" | "request_changed";
|
|
13
|
+
error_code?: ChatFailureCode;
|
|
14
|
+
diagnostic_id?: string;
|
|
12
15
|
reference_source_count?: number;
|
|
13
16
|
};
|
|
14
17
|
export declare function createChatAssistantDiagnostics(options: {
|
|
@@ -16,6 +19,8 @@ export declare function createChatAssistantDiagnostics(options: {
|
|
|
16
19
|
projectId: string;
|
|
17
20
|
triggerMessageId: string;
|
|
18
21
|
scope: string;
|
|
22
|
+
requestRef?: string;
|
|
23
|
+
attemptRef?: string;
|
|
19
24
|
}): (event: ChatAssistantProgress) => void;
|
|
20
25
|
export {};
|
|
21
26
|
//# sourceMappingURL=diagnostics.d.ts.map
|
|
@@ -1,18 +1,21 @@
|
|
|
1
1
|
import { createEventRef } from "@tutti/shared/ids";
|
|
2
2
|
export function createChatAssistantDiagnostics(options) {
|
|
3
|
-
const requestRef = createEventRef();
|
|
3
|
+
const requestRef = options.requestRef ?? createEventRef();
|
|
4
4
|
const started = performance.now();
|
|
5
5
|
return (event) => {
|
|
6
6
|
try {
|
|
7
7
|
const fields = {
|
|
8
8
|
project_id: options.projectId,
|
|
9
9
|
chat_request_ref: requestRef,
|
|
10
|
+
...(options.attemptRef === undefined ? {} : { attempt_ref: options.attemptRef }),
|
|
10
11
|
trigger_message_ref: options.triggerMessageId,
|
|
11
12
|
scope: options.scope,
|
|
12
13
|
elapsed_ms: Math.round(performance.now() - started),
|
|
13
14
|
...event,
|
|
14
15
|
};
|
|
15
|
-
if (event.stage === "failed" ||
|
|
16
|
+
if (event.stage === "failed" ||
|
|
17
|
+
event.stage === "provider_failed" ||
|
|
18
|
+
event.stage === "notification_failed") {
|
|
16
19
|
options.logger?.warn(fields, "Chat assistant progress");
|
|
17
20
|
}
|
|
18
21
|
else {
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import type { OperationFailure } from "@tutti/shared/schemas/api";
|
|
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."];
|
|
18
|
+
};
|
|
19
|
+
export type ChatFailureCode = keyof typeof failures;
|
|
20
|
+
type ChatFailure = OperationFailure & {
|
|
21
|
+
code: ChatFailureCode;
|
|
22
|
+
};
|
|
23
|
+
export declare function chatFailure(code: ChatFailureCode, diagnosticId?: string): ChatFailure;
|
|
24
|
+
export declare function chatFailureMessage(failure: OperationFailure): string;
|
|
25
|
+
export declare function classifyChatFailure(error: unknown, diagnosticId: string): ChatFailure;
|
|
26
|
+
export {};
|
|
27
|
+
//# sourceMappingURL=failures.d.ts.map
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { createEventRef } from "@tutti/shared/ids";
|
|
2
|
+
import { providerFailureCode } from "../providers/failure-classification.js";
|
|
3
|
+
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
|
+
],
|
|
79
|
+
};
|
|
80
|
+
export function chatFailure(code, diagnosticId = createEventRef()) {
|
|
81
|
+
const [category, recovery] = failures[code];
|
|
82
|
+
return { code, category, recovery, diagnostic_id: diagnosticId };
|
|
83
|
+
}
|
|
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}`;
|
|
89
|
+
}
|
|
90
|
+
// Allowlisted error facts only; neither error.message nor diagnostic callbacks
|
|
91
|
+
// establish business truth. Unknown failures remain non-automatically-retryable.
|
|
92
|
+
export function classifyChatFailure(error, diagnosticId) {
|
|
93
|
+
const value = typeof error === "object" && error !== null
|
|
94
|
+
? error
|
|
95
|
+
: {};
|
|
96
|
+
const code = typeof value.code === "string" ? value.code : value.reason;
|
|
97
|
+
const mapped = typeof code === "string" && Object.hasOwn(failures, code)
|
|
98
|
+
? code
|
|
99
|
+
: (providerFailureCode(error) ?? "internal_error");
|
|
100
|
+
return chatFailure(mapped, diagnosticId);
|
|
101
|
+
}
|
|
102
|
+
//# sourceMappingURL=failures.js.map
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type ProjectId } from "@tutti/shared/ids";
|
|
2
2
|
import type { MessageProjection } from "@tutti/shared/schemas/api";
|
|
3
3
|
import { type ChatAssistantLogger } from "./diagnostics.js";
|
|
4
4
|
import type { ProviderInvocationProgressObserver } from "../providers/invocation-progress.js";
|
|
@@ -33,6 +33,7 @@ export type ReadOnlyChatAssistantEnqueueResult = {
|
|
|
33
33
|
triggered: boolean;
|
|
34
34
|
};
|
|
35
35
|
export type ReadOnlyChatAssistantHandle = {
|
|
36
|
+
register: (tx: SqliteDatabase, message: MessageProjection) => void;
|
|
36
37
|
enqueue: (input: ReadOnlyChatAssistantTriggerInput) => ReadOnlyChatAssistantEnqueueResult;
|
|
37
38
|
waitForIdle: () => Promise<void>;
|
|
38
39
|
stopAccepting: () => void;
|
|
@@ -62,16 +63,26 @@ export declare function isChatAssistantOutput(value: unknown): value is ChatAssi
|
|
|
62
63
|
export declare function containsTuttiMention(value: string): boolean;
|
|
63
64
|
export declare class ReadOnlyChatAssistant implements ReadOnlyChatAssistantHandle {
|
|
64
65
|
private readonly options;
|
|
65
|
-
private readonly pending;
|
|
66
66
|
private readonly idleWaiters;
|
|
67
67
|
private active;
|
|
68
68
|
private accepting;
|
|
69
69
|
private readonly maxQueuedMentions;
|
|
70
|
+
private recoveryTimer;
|
|
71
|
+
private started;
|
|
72
|
+
private activeAttemptRef;
|
|
73
|
+
private storageFailureReported;
|
|
70
74
|
constructor(options: ReadOnlyChatAssistantOptions);
|
|
75
|
+
/** Call once after Host recovery/transport wiring. No old messages are backfilled. */
|
|
76
|
+
start(): void;
|
|
77
|
+
register(tx: SqliteDatabase, message: MessageProjection): void;
|
|
78
|
+
private now;
|
|
79
|
+
private report;
|
|
71
80
|
enqueue(input: ReadOnlyChatAssistantTriggerInput): ReadOnlyChatAssistantEnqueueResult;
|
|
72
81
|
waitForIdle(): Promise<void>;
|
|
73
82
|
stopAccepting(): void;
|
|
74
83
|
private pump;
|
|
84
|
+
private reportStorageFailure;
|
|
85
|
+
private pumpNext;
|
|
75
86
|
private resolveIdleIfNeeded;
|
|
76
87
|
private buildMainChatContext;
|
|
77
88
|
private buildClarificationRoundContext;
|
|
@@ -80,6 +91,7 @@ export declare class ReadOnlyChatAssistant implements ReadOnlyChatAssistantHandl
|
|
|
80
91
|
private appendMainChatAssistantMessage;
|
|
81
92
|
private appendClarificationRoundAssistantMessage;
|
|
82
93
|
private appendAssistantMessageForTrigger;
|
|
94
|
+
private recordReply;
|
|
83
95
|
private respond;
|
|
84
96
|
}
|
|
85
97
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,3 +1,5 @@
|
|
|
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
5
|
import { renderLanguageAwarePrompt } from "../communication-language/prompt.js";
|
|
@@ -7,6 +9,8 @@ import { withHostStoreTransaction, } from "../store/index.js";
|
|
|
7
9
|
import { messageTextForModel, messageTextForTuttiMention, } from "../collaboration-state/message-context.js";
|
|
8
10
|
import { externalReferenceMessageContext } from "../reference-access/message-context.js";
|
|
9
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";
|
|
10
14
|
const CHAT_ASSISTANT_CONTEXT_MESSAGE_LIMIT = 3;
|
|
11
15
|
const CHAT_ASSISTANT_ROUND_MESSAGE_LIMIT = 50;
|
|
12
16
|
const CHAT_ASSISTANT_ROUND_MESSAGE_MAX_CHARS = 2_000;
|
|
@@ -45,9 +49,6 @@ function normalizeAnswer(answer) {
|
|
|
45
49
|
}
|
|
46
50
|
return `${trimmed.slice(0, CHAT_ASSISTANT_MAX_ANSWER_CHARS - 3)}...`;
|
|
47
51
|
}
|
|
48
|
-
function assistantFailureMessage() {
|
|
49
|
-
return "Tutti could not complete this read-only answer. Try again later, or add more context in chat.";
|
|
50
|
-
}
|
|
51
52
|
function messageToContext(message) {
|
|
52
53
|
const authorDisplayName = message.author.display_name?.trim();
|
|
53
54
|
return {
|
|
@@ -105,46 +106,112 @@ function roundMessagesToContext(messages) {
|
|
|
105
106
|
}
|
|
106
107
|
export class ReadOnlyChatAssistant {
|
|
107
108
|
options;
|
|
108
|
-
pending = [];
|
|
109
109
|
idleWaiters = new Set();
|
|
110
110
|
active = null;
|
|
111
111
|
accepting = true;
|
|
112
112
|
maxQueuedMentions;
|
|
113
|
+
recoveryTimer;
|
|
114
|
+
started = false;
|
|
115
|
+
activeAttemptRef;
|
|
116
|
+
storageFailureReported = false;
|
|
113
117
|
constructor(options) {
|
|
114
118
|
this.options = options;
|
|
115
|
-
this.maxQueuedMentions = options.maxQueuedMentions ?? 8;
|
|
119
|
+
this.maxQueuedMentions = Math.max(1, options.maxQueuedMentions ?? 8);
|
|
116
120
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
return
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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
|
+
}
|
|
127
149
|
}
|
|
128
|
-
|
|
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({
|
|
129
171
|
...(this.options.logger === undefined ? {} : { logger: this.options.logger }),
|
|
130
172
|
projectId: this.options.project.project_id,
|
|
131
|
-
triggerMessageId:
|
|
132
|
-
scope:
|
|
173
|
+
triggerMessageId: message.id,
|
|
174
|
+
scope: message.scope.kind,
|
|
175
|
+
requestRef: message.id,
|
|
176
|
+
...(request.attempt_ref === null ? {} : { attemptRef: request.attempt_ref }),
|
|
133
177
|
});
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
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,
|
|
138
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
|
+
}
|
|
139
207
|
return { triggered: true };
|
|
140
208
|
}
|
|
141
|
-
report({ stage: "queued" });
|
|
142
|
-
this.pending.push({ message: input.message, report });
|
|
143
209
|
this.pump();
|
|
144
210
|
return { triggered: true };
|
|
145
211
|
}
|
|
146
212
|
async waitForIdle() {
|
|
147
|
-
if (this.active === null &&
|
|
213
|
+
if (this.active === null &&
|
|
214
|
+
(!this.accepting || nextQueuedChatRequest(this.options.store.db) === null)) {
|
|
148
215
|
return;
|
|
149
216
|
}
|
|
150
217
|
await new Promise((resolve) => this.idleWaiters.add(resolve));
|
|
@@ -154,36 +221,102 @@ export class ReadOnlyChatAssistant {
|
|
|
154
221
|
return;
|
|
155
222
|
}
|
|
156
223
|
this.accepting = false;
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
this.appendAssistantMessageForTrigger(message, assistantFailureMessage(), {
|
|
160
|
-
scratchpad_source: "exclude",
|
|
161
|
-
});
|
|
162
|
-
}
|
|
224
|
+
if (this.recoveryTimer !== undefined)
|
|
225
|
+
clearInterval(this.recoveryTimer);
|
|
163
226
|
this.resolveIdleIfNeeded();
|
|
164
227
|
}
|
|
165
228
|
pump() {
|
|
166
|
-
|
|
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();
|
|
167
257
|
return;
|
|
168
258
|
}
|
|
169
|
-
const pending = this.
|
|
170
|
-
if (pending ===
|
|
259
|
+
const pending = nextQueuedChatRequest(this.options.store.db);
|
|
260
|
+
if (pending === null) {
|
|
171
261
|
this.resolveIdleIfNeeded();
|
|
172
262
|
return;
|
|
173
263
|
}
|
|
174
|
-
const
|
|
175
|
-
|
|
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
|
+
}
|
|
176
307
|
});
|
|
177
308
|
this.active = task;
|
|
178
309
|
void task.finally(() => {
|
|
179
310
|
if (this.active === task) {
|
|
180
311
|
this.active = null;
|
|
312
|
+
this.activeAttemptRef = undefined;
|
|
181
313
|
this.pump();
|
|
182
314
|
}
|
|
183
315
|
});
|
|
184
316
|
}
|
|
185
317
|
resolveIdleIfNeeded() {
|
|
186
|
-
if (this.active !== null ||
|
|
318
|
+
if (this.active !== null ||
|
|
319
|
+
(this.accepting && nextQueuedChatRequest(this.options.store.db) !== null)) {
|
|
187
320
|
return;
|
|
188
321
|
}
|
|
189
322
|
for (const resolve of this.idleWaiters) {
|
|
@@ -263,7 +396,7 @@ export class ReadOnlyChatAssistant {
|
|
|
263
396
|
},
|
|
264
397
|
}, language);
|
|
265
398
|
}
|
|
266
|
-
appendMainChatAssistantMessage(triggerMessage, body, refs) {
|
|
399
|
+
appendMainChatAssistantMessage(triggerMessage, body, request, failure, refs) {
|
|
267
400
|
let afterCommit;
|
|
268
401
|
const message = withHostStoreTransaction(this.options.store.db, (tx) => {
|
|
269
402
|
const responseMessage = createMainChatAgentMessage(tx, {
|
|
@@ -279,6 +412,7 @@ export class ReadOnlyChatAssistant {
|
|
|
279
412
|
if (typeof callback === "function") {
|
|
280
413
|
afterCommit = callback;
|
|
281
414
|
}
|
|
415
|
+
this.recordReply(tx, request, responseMessage.id, failure);
|
|
282
416
|
return responseMessage;
|
|
283
417
|
});
|
|
284
418
|
afterCommit?.();
|
|
@@ -310,6 +444,7 @@ export class ReadOnlyChatAssistant {
|
|
|
310
444
|
});
|
|
311
445
|
if (typeof callback === "function")
|
|
312
446
|
afterCommit = callback;
|
|
447
|
+
this.recordReply(tx, input.request, responseMessage.id, input.failure);
|
|
313
448
|
return responseMessage;
|
|
314
449
|
});
|
|
315
450
|
afterCommit?.();
|
|
@@ -332,36 +467,75 @@ export class ReadOnlyChatAssistant {
|
|
|
332
467
|
invalidates,
|
|
333
468
|
});
|
|
334
469
|
}
|
|
335
|
-
appendAssistantMessageForTrigger(triggerMessage, body, refs) {
|
|
470
|
+
appendAssistantMessageForTrigger(triggerMessage, body, request, failure, refs) {
|
|
471
|
+
if (failure !== undefined)
|
|
472
|
+
refs = { ...refs, scratchpad_source: "exclude" };
|
|
336
473
|
if (triggerMessage.scope.kind === "clarification_round") {
|
|
337
474
|
this.appendClarificationRoundAssistantMessage({
|
|
338
475
|
triggerMessage,
|
|
339
476
|
roundId: triggerMessage.scope.clarification_round_ref,
|
|
340
477
|
body,
|
|
478
|
+
request,
|
|
479
|
+
...(failure === undefined ? {} : { failure }),
|
|
341
480
|
...(refs === undefined ? {} : { refs }),
|
|
342
481
|
});
|
|
343
482
|
return;
|
|
344
483
|
}
|
|
345
|
-
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");
|
|
346
494
|
}
|
|
347
|
-
async respond(triggerMessage, report) {
|
|
495
|
+
async respond(triggerMessage, request, report) {
|
|
348
496
|
let answer;
|
|
349
497
|
let invocationRef;
|
|
350
498
|
let readSources = [];
|
|
351
499
|
try {
|
|
352
500
|
report({ stage: "inspection_wait" });
|
|
353
501
|
await this.options.waitForInspectionAvailability?.();
|
|
502
|
+
if (!this.accepting) {
|
|
503
|
+
updateChatRequest(this.options.store.db, request, { status: "queued", phase: "queue" }, this.now());
|
|
504
|
+
return;
|
|
505
|
+
}
|
|
354
506
|
const prompt = this.renderPrompt(triggerMessage, report);
|
|
355
507
|
if (prompt === null) {
|
|
356
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());
|
|
357
514
|
return;
|
|
358
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;
|
|
359
523
|
report({ stage: "model_requested" });
|
|
360
524
|
const output = await this.options.model.answer({
|
|
361
525
|
prompt,
|
|
362
526
|
onProgress: (event) => {
|
|
363
|
-
if (event.stage === "context_ready")
|
|
364
|
-
|
|
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
|
+
}
|
|
365
539
|
report(event);
|
|
366
540
|
},
|
|
367
541
|
});
|
|
@@ -370,16 +544,20 @@ export class ReadOnlyChatAssistant {
|
|
|
370
544
|
readSources = readExternalReferenceReadSources(this.options.store.db, invocationRef);
|
|
371
545
|
}
|
|
372
546
|
}
|
|
373
|
-
catch {
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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,
|
|
377
554
|
});
|
|
555
|
+
this.appendAssistantMessageForTrigger(triggerMessage, chatFailureMessage(failure), request, failure);
|
|
378
556
|
report({ stage: "reply_persisted" });
|
|
379
557
|
return;
|
|
380
558
|
}
|
|
381
559
|
// Persistence/outbound-hook failure must not retry by creating a second reply.
|
|
382
|
-
this.appendAssistantMessageForTrigger(triggerMessage, answer, readSources.length === 0 ? undefined : { external_reference_reads: readSources });
|
|
560
|
+
this.appendAssistantMessageForTrigger(triggerMessage, answer, request, undefined, readSources.length === 0 ? undefined : { external_reference_reads: readSources });
|
|
383
561
|
report({ stage: "reply_persisted", reference_source_count: readSources.length });
|
|
384
562
|
}
|
|
385
563
|
}
|