@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.
Files changed (65) hide show
  1. package/dist/approvals/decision-command.d.ts +2 -2
  2. package/dist/chat-assistant/diagnostics.d.ts +7 -2
  3. package/dist/chat-assistant/diagnostics.js +5 -2
  4. package/dist/chat-assistant/failures.d.ts +27 -0
  5. package/dist/chat-assistant/failures.js +102 -0
  6. package/dist/chat-assistant/index.d.ts +14 -2
  7. package/dist/chat-assistant/index.js +226 -48
  8. package/dist/chat-assistant/requests.d.ts +30 -0
  9. package/dist/chat-assistant/requests.js +58 -0
  10. package/dist/collaboration-ingestion/clarification.js +3 -1
  11. package/dist/collaboration-ingestion/main-chat.js +1 -0
  12. package/dist/collaboration-ingestion/types.d.ts +2 -0
  13. package/dist/collaboration-state/clarification-snapshot.d.ts +5 -5
  14. package/dist/collaboration-state/task-compile-context.d.ts +1 -1
  15. package/dist/collaboration-state/task-compile-context.js +11 -1
  16. package/dist/collaboration-state/task-compile-requests.d.ts +6 -0
  17. package/dist/collaboration-state/task-compile-requests.js +16 -0
  18. package/dist/control-plane/task-compile-continuation.js +2 -0
  19. package/dist/control-plane/task-compile-request-status.d.ts +6 -0
  20. package/dist/control-plane/task-compile-request-status.js +70 -0
  21. package/dist/control-plane/task-compile-start.js +40 -2
  22. package/dist/providers/failure-classification.d.ts +23 -0
  23. package/dist/providers/failure-classification.js +43 -0
  24. package/dist/reference-access/read-evidence.d.ts +2 -0
  25. package/dist/reference-access/read-evidence.js +5 -0
  26. package/dist/reference-access/service.d.ts +2 -0
  27. package/dist/reference-access/service.js +2 -0
  28. package/dist/runtime-failures.js +15 -1
  29. package/dist/server-shell/cli/external-reference-runtime.js +6 -3
  30. package/dist/server-shell/cli/host-server-runtime.js +4 -1
  31. package/dist/server-shell/cli/relay-registration.js +1 -0
  32. package/dist/server-shell/http/routes/project-api/messages-routes.js +22 -1
  33. package/dist/server-shell/http/routes/project-api/openapi-collaboration-routes.d.ts +140 -5
  34. package/dist/server-shell/http/routes/project-api/openapi-collaboration-routes.js +29 -2
  35. package/dist/server-shell/http/routes/project-api/openapi-skills-routes.d.ts +2 -0
  36. package/dist/server-shell/http/routes/project-api/openapi-workspace-routes.d.ts +6 -0
  37. package/dist/server-shell/http/routes/project-api/openapi.d.ts +148 -5
  38. package/dist/server-shell/http/routes/project-api/schemas.d.ts +6 -0
  39. package/dist/server-shell/http/routes/project-api/schemas.js +1 -0
  40. package/dist/server-shell/http/routes/project-api/types.d.ts +1 -0
  41. package/dist/server-shell/http/routes/project-api/workflow-routes.js +39 -3
  42. package/dist/server-shell/http/routes/project-api/workspace-projections.js +1 -0
  43. package/dist/server-shell/http/validation.js +8 -2
  44. package/dist/server-shell/local-console/managed-project-service.d.ts +1 -1
  45. package/dist/server-shell/managed-access/transport.d.ts +1 -1
  46. package/migrations/0024_chat_requests.sql +19 -0
  47. package/migrations/0025_task_compile_requests.sql +50 -0
  48. package/migrations/README.md +3 -1
  49. package/node_modules/@tutti/relay-client/dist/host-control.d.ts +1 -0
  50. package/node_modules/@tutti/relay-client/dist/host-control.js +1 -0
  51. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -0
  52. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -0
  53. package/node_modules/@tutti/shared/dist/schemas/api/operation-status.d.ts +174 -0
  54. package/node_modules/@tutti/shared/dist/schemas/api/operation-status.js +75 -0
  55. package/node_modules/@tutti/shared/dist/schemas/api/primitives.d.ts +1 -0
  56. package/node_modules/@tutti/shared/dist/schemas/api/primitives.js +1 -0
  57. package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.d.ts +1 -0
  58. package/node_modules/@tutti/shared/dist/schemas/api/workspace-commands.js +2 -1
  59. package/package.json +1 -1
  60. package/web/assets/{homepage-motion-scene-DyEmWwvt.js → homepage-motion-scene-BcMt0AVW.js} +1 -1
  61. package/web/assets/index-2eX37Jx6.js +70 -0
  62. package/web/assets/index-BJvKuUiT.css +1 -0
  63. package/web/index.html +2 -2
  64. package/web/assets/index-By3idT4U.js +0 -70
  65. package/web/assets/index-DSBjOZmJ.css +0 -1
@@ -0,0 +1,30 @@
1
+ import { type ChatRequestStatus, type OperationFailure } from "@tutti/shared/schemas/api";
2
+ import type { SqliteDatabase } from "../store/index.js";
3
+ export type ChatRequestRecord = {
4
+ trigger_message_id: string;
5
+ status: "queued" | "running" | "succeeded" | "failed";
6
+ phase: "queue" | "workspace_wait" | "answer" | "settled";
7
+ revision: number;
8
+ attempt_ref: string | null;
9
+ response_message_id: string | null;
10
+ failure_json: string | null;
11
+ created_at: string;
12
+ updated_at: string;
13
+ };
14
+ export declare function readChatRequest(db: SqliteDatabase, messageId: string): ChatRequestRecord | null;
15
+ export declare function chatRequestStatus(row: ChatRequestRecord): ChatRequestStatus;
16
+ export declare function insertChatRequest(db: SqliteDatabase, messageId: string, now: string, failure?: OperationFailure): void;
17
+ export declare function countQueuedChatRequests(db: SqliteDatabase): number;
18
+ export declare function nextQueuedChatRequest(db: SqliteDatabase): ChatRequestRecord | null;
19
+ /** CAS/fencing only. Lifecycle policy belongs to Chat Assistant, not the repository. */
20
+ export declare function updateChatRequest(db: SqliteDatabase, row: ChatRequestRecord, next: {
21
+ status: ChatRequestRecord["status"];
22
+ phase: ChatRequestRecord["phase"];
23
+ attemptRef?: string;
24
+ responseMessageId?: string;
25
+ failure?: OperationFailure;
26
+ }, now: string): ChatRequestRecord | null;
27
+ export declare function listRunningChatRequests(db: SqliteDatabase): ChatRequestRecord[];
28
+ /** Correlation only; never changes lifecycle state based on a progress callback. */
29
+ export declare function linkChatRequestInvocation(db: SqliteDatabase, row: ChatRequestRecord, invocationRef: string): void;
30
+ //# sourceMappingURL=requests.d.ts.map
@@ -0,0 +1,58 @@
1
+ import { Value } from "@sinclair/typebox/value";
2
+ import { ChatRequestStatusSchema, } from "@tutti/shared/schemas/api";
3
+ export function readChatRequest(db, messageId) {
4
+ return (db
5
+ .prepare("SELECT * FROM chat_requests WHERE trigger_message_id = ?")
6
+ .get(messageId) ?? null);
7
+ }
8
+ export function chatRequestStatus(row) {
9
+ const value = {
10
+ operation: { kind: "chat_answer", ref: row.trigger_message_id },
11
+ status: row.status,
12
+ phase: row.phase,
13
+ revision: row.revision,
14
+ created_at: row.created_at,
15
+ updated_at: row.updated_at,
16
+ ...(row.response_message_id === null ? {} : { response_message_id: row.response_message_id }),
17
+ ...(row.failure_json === null ? {} : { failure: JSON.parse(row.failure_json) }),
18
+ };
19
+ if (!Value.Check(ChatRequestStatusSchema, value))
20
+ throw new Error("chat_request_invalid");
21
+ return value;
22
+ }
23
+ export function insertChatRequest(db, messageId, now, failure) {
24
+ db.prepare(`INSERT INTO chat_requests
25
+ (trigger_message_id, status, phase, failure_json, created_at, updated_at)
26
+ VALUES (?, ?, ?, ?, ?, ?) ON CONFLICT(trigger_message_id) DO NOTHING`).run(messageId, failure ? "failed" : "queued", failure ? "settled" : "queue", failure ? JSON.stringify(failure) : null, now, now);
27
+ }
28
+ export function countQueuedChatRequests(db) {
29
+ return db
30
+ .prepare("SELECT count(*) AS count FROM chat_requests WHERE status = 'queued'")
31
+ .get().count;
32
+ }
33
+ export function nextQueuedChatRequest(db) {
34
+ return (db
35
+ .prepare("SELECT * FROM chat_requests WHERE status = 'queued' ORDER BY enqueue_order LIMIT 1")
36
+ .get() ?? null);
37
+ }
38
+ /** CAS/fencing only. Lifecycle policy belongs to Chat Assistant, not the repository. */
39
+ export function updateChatRequest(db, row, next, now) {
40
+ const changed = db
41
+ .prepare(`UPDATE chat_requests SET status = ?, phase = ?,
42
+ attempt_ref = ?, response_message_id = ?, failure_json = ?, updated_at = ?, revision = revision + 1
43
+ WHERE trigger_message_id = ? AND revision = ?`)
44
+ .run(next.status, next.phase, next.attemptRef ?? row.attempt_ref, next.responseMessageId ?? null, next.failure ? JSON.stringify(next.failure) : null, now, row.trigger_message_id, row.revision).changes;
45
+ return changed === 0 ? null : readChatRequest(db, row.trigger_message_id);
46
+ }
47
+ export function listRunningChatRequests(db) {
48
+ return db
49
+ .prepare("SELECT * FROM chat_requests WHERE status = 'running' ORDER BY created_at, trigger_message_id")
50
+ .all();
51
+ }
52
+ /** Correlation only; never changes lifecycle state based on a progress callback. */
53
+ export function linkChatRequestInvocation(db, row, invocationRef) {
54
+ db.prepare(`INSERT OR IGNORE INTO chat_request_invocations(message_id, attempt_ref, invocation_ref)
55
+ SELECT trigger_message_id, attempt_ref, ? FROM chat_requests
56
+ WHERE trigger_message_id = ? AND attempt_ref = ? AND status = 'running'`).run(invocationRef, row.trigger_message_id, row.attempt_ref);
57
+ }
58
+ //# sourceMappingURL=requests.js.map
@@ -21,8 +21,10 @@ export function ingestClarificationMessage(options) {
21
21
  ...(options.now === undefined ? {} : { now: options.now }),
22
22
  execute: (tx) => {
23
23
  const result = options.controlPlane.sendClarificationRoundMessage(options.roundId, options.payload, options.author);
24
- if (result.result?.message !== undefined)
24
+ if (result.result?.message !== undefined) {
25
25
  options.afterCreate?.(tx, result.result.message);
26
+ options.chatAssistant?.register(tx, result.result.message);
27
+ }
26
28
  return result;
27
29
  },
28
30
  });
@@ -39,6 +39,7 @@ export function ingestMainChatMessage(options) {
39
39
  ...(options.now === undefined ? {} : { now: options.now }),
40
40
  });
41
41
  options.afterCreate?.(tx, message);
42
+ options.chatAssistant?.register(tx, message);
42
43
  return {
43
44
  disposition: { kind: "created", message_id: message.id },
44
45
  result: { message },
@@ -1,5 +1,6 @@
1
1
  import type { AccountAvatarProjection } from "@tutti/shared/domain";
2
2
  import type { MessageProjection } from "@tutti/shared/schemas/api";
3
+ import type { SqliteDatabase } from "../store/index.js";
3
4
  export type TrustedCollaborationAuthor = {
4
5
  kind?: "human" | "development_operator";
5
6
  account_ref: string;
@@ -10,6 +11,7 @@ export type ScratchpadSourceNotifier = {
10
11
  notify: (message: MessageProjection) => void;
11
12
  };
12
13
  export type ChatAssistantEnqueuer = {
14
+ register: (tx: SqliteDatabase, message: MessageProjection) => void;
13
15
  enqueue: (input: {
14
16
  message: MessageProjection;
15
17
  }) => unknown;
@@ -13,8 +13,9 @@ export declare function readSubmittedClarificationMessages(db: SqliteDatabase, r
13
13
  is_current_round: boolean;
14
14
  };
15
15
  refs?: {
16
- task_id?: string;
17
16
  activity_ref?: string;
17
+ workflow_ref?: string;
18
+ task_id?: string;
18
19
  artifact_feedback?: {
19
20
  summary?: string;
20
21
  kind: "artifact_feedback";
@@ -106,15 +107,14 @@ export declare function readSubmittedClarificationMessages(db: SqliteDatabase, r
106
107
  blob_oid?: string;
107
108
  path: string;
108
109
  name: string;
109
- source: "human_upload" | "tutti_output";
110
110
  category: "file" | "image";
111
+ source: "human_upload" | "tutti_output";
111
112
  size_bytes: number;
112
113
  text_read: {
113
114
  blocked_reason?: "unknown" | "binary" | "too_large" | "unsupported_encoding" | "restricted_path" | "secret_like";
114
115
  can_read: boolean;
115
116
  };
116
117
  };
117
- workflow_ref?: string;
118
118
  clarification_round_ref?: string;
119
119
  clarification_thread_ref?: string;
120
120
  task_result?: {
@@ -141,10 +141,10 @@ export declare function readSubmittedClarificationMessages(db: SqliteDatabase, r
141
141
  };
142
142
  worklist_compile?: {
143
143
  kind: "worklist_compile";
144
- title: string;
145
144
  workflow_ref: string;
145
+ title: string;
146
146
  tasks: {
147
- status: "failed" | "pending" | "done" | "todo" | "running";
147
+ status: "failed" | "pending" | "running" | "done" | "todo";
148
148
  task_id: string;
149
149
  title: string;
150
150
  summary: string;
@@ -24,7 +24,7 @@ export declare function completeTaskCompileContext(db: SqliteDatabase, input: Co
24
24
  task_changes: string[];
25
25
  receipts: {
26
26
  compiled_tasks?: {
27
- status: "failed" | "pending" | "done" | "todo" | "running";
27
+ status: "failed" | "pending" | "running" | "done" | "todo";
28
28
  task_id: string;
29
29
  title: string;
30
30
  summary: string;
@@ -3,6 +3,7 @@ import { readScratchpadProjection } from "./scratchpad.js";
3
3
  import { readCommunicationLanguage, copyCommunicationLanguage } from "./communication-language.js";
4
4
  import { readScratchpadSourceState } from "./scratchpad-source-state.js";
5
5
  import { nowIso, stringifyJson } from "./serialization.js";
6
+ import { registerTaskCompileRequest, closeFailedTaskCompileRequest, } from "./task-compile-requests.js";
6
7
  function cursorIsAfter(left, right) {
7
8
  if (left === undefined) {
8
9
  return false;
@@ -145,6 +146,7 @@ export function recordTaskCompileSubmission(db, input) {
145
146
  if (readActiveTaskCompileContext(db) !== null) {
146
147
  throw new Error("An active Task Compile Context already exists");
147
148
  }
149
+ registerTaskCompileRequest(db, input.workflow_ref, input.accepted_activity_ref, nowIso(input.now));
148
150
  db.prepare(`
149
151
  INSERT INTO pending_task_compile_submissions (
150
152
  id,
@@ -226,6 +228,7 @@ export function captureTaskCompileContext(db, input) {
226
228
  : { created_at: lastSource.created_at, message_id: lastSource.message_id });
227
229
  const after = submission?.source_window.after ?? sourceState.cycle_start_cursor;
228
230
  const submittedAt = nowIso(input.now);
231
+ registerTaskCompileRequest(tx, input.workflow_ref, input.captured_by_activity_ref, submittedAt);
229
232
  tx.prepare(`
230
233
  INSERT INTO active_task_compile_context (
231
234
  id,
@@ -337,9 +340,16 @@ export function rollbackTaskCompileContext(db, input) {
337
340
  return withHostStoreTransaction(db, (tx) => {
338
341
  const context = readActiveTaskCompileContext(tx, input.workflow_ref);
339
342
  if (context === null) {
340
- return false;
343
+ const pending = readPendingTaskCompileSubmission(tx, input.workflow_ref);
344
+ if (pending === null)
345
+ return false;
346
+ // Before capture the source cursor has not moved: release only this admission.
347
+ closeFailedTaskCompileRequest(tx, input.workflow_ref, nowIso(input.now));
348
+ tx.prepare("DELETE FROM pending_task_compile_submissions WHERE workflow_ref = ?").run(input.workflow_ref);
349
+ return true;
341
350
  }
342
351
  const rolledBackAt = nowIso(input.now);
352
+ closeFailedTaskCompileRequest(tx, input.workflow_ref, rolledBackAt);
343
353
  const sourceState = readScratchpadSourceState(tx);
344
354
  const after = context.source_window.after;
345
355
  const through = context.source_window.through;
@@ -0,0 +1,6 @@
1
+ import type { SqliteDatabase } from "../store/index.js";
2
+ /** Persistence only: callers own acceptance, stage selection and rollback. */
3
+ export declare function registerTaskCompileRequest(db: SqliteDatabase, workflowRef: string, activityRef: string, createdAt: string): void;
4
+ export declare function linkTaskCompileActivity(db: SqliteDatabase, workflowRef: string, activityRef: string): void;
5
+ export declare function closeFailedTaskCompileRequest(db: SqliteDatabase, workflowRef: string, now: string, code?: "task_compile_failed" | "scratchpad_empty_after_refresh"): void;
6
+ //# sourceMappingURL=task-compile-requests.d.ts.map
@@ -0,0 +1,16 @@
1
+ /** Persistence only: callers own acceptance, stage selection and rollback. */
2
+ export function registerTaskCompileRequest(db, workflowRef, activityRef, createdAt) {
3
+ db.prepare(`INSERT OR IGNORE INTO task_compile_requests
4
+ (workflow_ref, accepted_activity_ref, current_activity_ref, created_at)
5
+ VALUES (?, ?, ?, ?)`).run(workflowRef, activityRef, activityRef, createdAt);
6
+ }
7
+ export function linkTaskCompileActivity(db, workflowRef, activityRef) {
8
+ db.prepare(`UPDATE task_compile_requests SET current_activity_ref = ?
9
+ WHERE workflow_ref = ? AND closed_at IS NULL`).run(activityRef, workflowRef);
10
+ }
11
+ export function closeFailedTaskCompileRequest(db, workflowRef, now, code = "task_compile_failed") {
12
+ db.prepare(`UPDATE task_compile_requests SET failure_code = ?, closed_at = ?
13
+ WHERE workflow_ref = ? AND closed_at IS NULL
14
+ AND NOT EXISTS (SELECT 1 FROM scratchpad_receipts WHERE workflow_ref = ?)`).run(code, now, workflowRef, workflowRef);
15
+ }
16
+ //# sourceMappingURL=task-compile-requests.js.map
@@ -1,5 +1,6 @@
1
1
  import { materializeClarificationSuccessor, readActiveTaskCompileContext, readClarificationRoundProjection, readWorklistProjection, rollbackTaskCompileContext, } from "../collaboration-state/index.js";
2
2
  import { withHostStoreTransaction } from "../store/index.js";
3
+ import { linkTaskCompileActivity } from "../collaboration-state/task-compile-requests.js";
3
4
  import { clarificationInvalidates } from "./invalidations.js";
4
5
  import { applyTaskCompileProcedureOutput } from "./task-compile-output.js";
5
6
  import { readTaskCompileClarificationRounds } from "./task-compile-clarification-context.js";
@@ -27,6 +28,7 @@ export function startControlPlaneTaskCompileContinuation(options) {
27
28
  summary: "Continuing task_compile from clarification input.",
28
29
  now: options.now,
29
30
  onTransition: (state) => {
31
+ linkTaskCompileActivity(options.store.db, workflowRef, state.activity.activity_ref);
30
32
  options.publishProcedureTransition(state);
31
33
  if (state.transition_kind === "terminal") {
32
34
  options.onTerminal?.(state.activity.status === "failed"
@@ -0,0 +1,6 @@
1
+ import type { ExecutionStatusProjection, WorklistSubmissionStatus } from "@tutti/shared/schemas/api";
2
+ import type { SqliteDatabase } from "../store/index.js";
3
+ /** Pure projection of owned records. A missing receipt is never proof of failure. */
4
+ export declare function readTaskCompileRequestStatus(db: SqliteDatabase, execution: ExecutionStatusProjection, workflowRef?: string): WorklistSubmissionStatus | null;
5
+ export declare function readTaskCompileCommandRef(db: SqliteDatabase, commandId: string): string | undefined;
6
+ //# sourceMappingURL=task-compile-request-status.d.ts.map
@@ -0,0 +1,70 @@
1
+ import { readActiveClarificationProjection, readActiveTaskCompileContext, readPendingTaskCompileSubmission, } from "../collaboration-state/index.js";
2
+ /** Pure projection of owned records. A missing receipt is never proof of failure. */
3
+ export function readTaskCompileRequestStatus(db, execution, workflowRef) {
4
+ const row = workflowRef === undefined
5
+ ? db
6
+ .prepare("SELECT * FROM task_compile_requests ORDER BY rowid DESC LIMIT 1")
7
+ .get()
8
+ : db
9
+ .prepare("SELECT * FROM task_compile_requests WHERE workflow_ref = ?")
10
+ .get(workflowRef);
11
+ if (row === undefined)
12
+ return null;
13
+ const common = {
14
+ operation: { kind: "worklist_submit", ref: row.workflow_ref },
15
+ created_at: row.created_at,
16
+ activity_ref: row.current_activity_ref,
17
+ };
18
+ if (db.prepare("SELECT 1 FROM scratchpad_receipts WHERE workflow_ref = ?").get(row.workflow_ref)) {
19
+ return { ...common, status: "succeeded", phase: "settled" };
20
+ }
21
+ if (row.failure_code !== null) {
22
+ return {
23
+ ...common,
24
+ status: "failed",
25
+ phase: "settled",
26
+ failure: {
27
+ code: row.failure_code,
28
+ category: "internal",
29
+ recovery: "none",
30
+ diagnostic_id: row.workflow_ref,
31
+ },
32
+ };
33
+ }
34
+ const clarification = readActiveClarificationProjection(db);
35
+ if (clarification.state === "active" &&
36
+ clarification.thread.owner.kind === "task_compile" &&
37
+ clarification.thread.owner.workflow_ref === row.workflow_ref &&
38
+ clarification.round.status === "writable") {
39
+ return {
40
+ ...common,
41
+ status: "waiting_input",
42
+ phase: "clarification",
43
+ round_id: clarification.round.id,
44
+ };
45
+ }
46
+ const activities = execution.active_activities ?? (execution.active_activity ? [execution.active_activity] : []);
47
+ const active = activities.find((entry) => entry.activity_ref === row.current_activity_ref || entry.workflow_ref === row.workflow_ref);
48
+ if (active !== undefined) {
49
+ const kind = active.workflow_kind;
50
+ const phase = kind === "scratchpad_refresh" ||
51
+ kind === "reference_summary_refresh" ||
52
+ kind === "project_context_bootstrap" ||
53
+ kind === "task_compile"
54
+ ? kind
55
+ : "preparing";
56
+ return { ...common, activity_ref: active.activity_ref, status: "running", phase };
57
+ }
58
+ const ref = row.workflow_ref;
59
+ if (readPendingTaskCompileSubmission(db, ref) !== null ||
60
+ readActiveTaskCompileContext(db, ref) !== null) {
61
+ return { ...common, status: "running", phase: "preparing" };
62
+ }
63
+ return { ...common, status: "unknown", phase: "unconfirmed" };
64
+ }
65
+ export function readTaskCompileCommandRef(db, commandId) {
66
+ return db
67
+ .prepare("SELECT workflow_ref FROM task_compile_request_commands WHERE command_id = ?")
68
+ .get(commandId)?.workflow_ref;
69
+ }
70
+ //# sourceMappingURL=task-compile-request-status.js.map
@@ -12,10 +12,15 @@ import { enqueueControlPlaneIntent } from "./dispatch-intents.js";
12
12
  import { materializePromptSourceMessages } from "./task-source-context.js";
13
13
  import { materializeRecentReferences, readPendingRecentReferenceSummaryPaths, readRecentReferenceSources, } from "./recent-references.js";
14
14
  import { startReferenceSummaryRefreshProcedure } from "./reference-summary-refresh.js";
15
+ import { linkTaskCompileActivity, closeFailedTaskCompileRequest, } from "../collaboration-state/task-compile-requests.js";
15
16
  export function startControlPlaneTaskCompile(options) {
16
17
  const activeContext = readActiveTaskCompileContext(options.store.db);
17
18
  if (activeContext !== null) {
18
19
  return {
20
+ result: {
21
+ submission_ref: activeContext.workflow_ref,
22
+ activity_ref: activeContext.captured_by_activity_ref,
23
+ },
19
24
  disposition: {
20
25
  kind: "already_running",
21
26
  activity_ref: activeContext.captured_by_activity_ref,
@@ -25,6 +30,10 @@ export function startControlPlaneTaskCompile(options) {
25
30
  const pendingSubmission = readPendingTaskCompileSubmission(options.store.db);
26
31
  if (pendingSubmission !== null) {
27
32
  return {
33
+ result: {
34
+ submission_ref: pendingSubmission.workflow_ref,
35
+ activity_ref: pendingSubmission.accepted_activity_ref,
36
+ },
28
37
  disposition: {
29
38
  kind: "already_running",
30
39
  activity_ref: pendingSubmission.accepted_activity_ref,
@@ -72,7 +81,7 @@ export function startControlPlaneTaskCompile(options) {
72
81
  },
73
82
  };
74
83
  }
75
- return startScratchpadRefreshBeforeTaskCompileActivity({
84
+ const started = startScratchpadRefreshBeforeTaskCompileActivity({
76
85
  ...options,
77
86
  runner: runnerResolution.runner,
78
87
  sourceBatch,
@@ -86,6 +95,7 @@ export function startControlPlaneTaskCompile(options) {
86
95
  now: options.now,
87
96
  }),
88
97
  });
98
+ return withSubmissionRef(started, taskCompileWorkflowRef);
89
99
  }
90
100
  if (!scratchpadHasSubmittableContent(scratchpad)) {
91
101
  return { disposition: { kind: "scratchpad_empty" } };
@@ -108,7 +118,12 @@ export function startControlPlaneTaskCompile(options) {
108
118
  now: options.now,
109
119
  });
110
120
  }
111
- return started;
121
+ return withSubmissionRef(started, taskCompileWorkflowRef);
122
+ }
123
+ function withSubmissionRef(result, submissionRef) {
124
+ return result.disposition.kind === "accepted"
125
+ ? { ...result, result: { ...result.result, submission_ref: submissionRef } }
126
+ : result;
112
127
  }
113
128
  function recordTaskCompileSubmissionIntent(options) {
114
129
  return withHostStoreTransaction(options.store.db, (tx) => {
@@ -152,6 +167,7 @@ function startControlPlaneTaskCompileActivity(options) {
152
167
  summary: "Compiling Scratchpad into Worklist tasks.",
153
168
  now: options.now,
154
169
  onTransition: (state) => {
170
+ linkTaskCompileActivity(options.store.db, options.taskCompileWorkflowRef, state.activity.activity_ref);
155
171
  options.publishProcedureTransition(state);
156
172
  if (state.transition_kind === "terminal") {
157
173
  options.onChainTerminal?.(state.activity.status === "failed"
@@ -206,11 +222,16 @@ function startScratchpadRefreshBeforeTaskCompileActivity(options) {
206
222
  summary: "Refreshing Scratchpad before submitting to Worklist.",
207
223
  now: options.now,
208
224
  onTransition: (state) => {
225
+ linkTaskCompileActivity(options.store.db, options.taskCompileWorkflowRef, state.activity.activity_ref);
209
226
  options.publishProcedureTransition(state);
210
227
  if (state.transition_kind !== "terminal") {
211
228
  return;
212
229
  }
213
230
  if (state.activity.status !== "finished") {
231
+ rollbackTaskCompileContext(options.store.db, {
232
+ workflow_ref: options.taskCompileWorkflowRef,
233
+ now: options.now,
234
+ });
214
235
  options.onChainTerminal?.({
215
236
  state: "failed",
216
237
  errorCode: "scratchpad_refresh_failed",
@@ -222,6 +243,10 @@ function startScratchpadRefreshBeforeTaskCompileActivity(options) {
222
243
  }
223
244
  const scratchpad = readScratchpadProjection(options.store.db);
224
245
  if (!scratchpadHasSubmittableContent(scratchpad)) {
246
+ withHostStoreTransaction(options.store.db, (tx) => {
247
+ closeFailedTaskCompileRequest(tx, options.taskCompileWorkflowRef, options.now().toISOString(), "scratchpad_empty_after_refresh");
248
+ tx.prepare("DELETE FROM pending_task_compile_submissions WHERE workflow_ref = ?").run(options.taskCompileWorkflowRef);
249
+ });
225
250
  options.onChainTerminal?.({
226
251
  state: "failed",
227
252
  errorCode: "scratchpad_empty_after_refresh",
@@ -230,6 +255,10 @@ function startScratchpadRefreshBeforeTaskCompileActivity(options) {
230
255
  }
231
256
  const next = startReferenceSummaryGateOrNextStage(options);
232
257
  if (next.disposition.kind !== "accepted" && next.disposition.kind !== "already_running") {
258
+ rollbackTaskCompileContext(options.store.db, {
259
+ workflow_ref: options.taskCompileWorkflowRef,
260
+ now: options.now,
261
+ });
233
262
  options.onChainTerminal?.({
234
263
  state: "failed",
235
264
  errorCode: "task_compile_stage_unavailable",
@@ -413,6 +442,10 @@ function startReferenceSummaryGateOrNextStage(options) {
413
442
  });
414
443
  registerReferenceSummaryGateWaiter(options, activeReferenceSummary.activity_ref);
415
444
  return {
445
+ result: {
446
+ submission_ref: options.taskCompileWorkflowRef,
447
+ activity_ref: activeReferenceSummary.activity_ref,
448
+ },
416
449
  disposition: {
417
450
  kind: "already_running",
418
451
  activity_ref: activeReferenceSummary.activity_ref,
@@ -514,6 +547,10 @@ function startProjectContextBootstrapActivity(options) {
514
547
  return;
515
548
  }
516
549
  if (state.activity.status !== "finished") {
550
+ rollbackTaskCompileContext(options.store.db, {
551
+ workflow_ref: options.taskCompileWorkflowRef,
552
+ now: options.now,
553
+ });
517
554
  options.onChainTerminal?.({ state: "failed", errorCode: "project_context_failed" });
518
555
  return;
519
556
  }
@@ -697,6 +734,7 @@ function readOrCaptureTaskCompileContext(input) {
697
734
  captured_by_activity_ref: input.activityRef,
698
735
  now: input.now,
699
736
  });
737
+ linkTaskCompileActivity(tx, input.workflowRef, input.activityRef);
700
738
  enqueueControlPlaneIntent(tx, {
701
739
  kind: "task_compile_start",
702
740
  ownerKind: "task_compile",
@@ -0,0 +1,23 @@
1
+ declare const aliases: {
2
+ readonly provider_not_configured: "provider_not_configured";
3
+ readonly provider_auth_invalid: "provider_auth_invalid";
4
+ readonly provider_quota_or_billing_required: "provider_quota_or_billing_required";
5
+ readonly provider_rate_limited: "provider_rate_limited";
6
+ readonly provider_model_unavailable: "provider_model_unavailable";
7
+ readonly provider_network_error: "provider_network_error";
8
+ readonly auth_failed: "provider_auth_invalid";
9
+ readonly host_delegation_revoked: "provider_auth_invalid";
10
+ readonly authority_invalid: "provider_auth_invalid";
11
+ readonly gateway_rate_limited: "provider_rate_limited";
12
+ readonly control_queue_full: "provider_rate_limited";
13
+ readonly turn_timeout: "provider_timeout";
14
+ readonly output_parse_failed: "provider_output_invalid";
15
+ readonly output_validation_failed: "provider_output_invalid";
16
+ readonly process_lifecycle_failed: "provider_lifecycle_failed";
17
+ readonly process_tree_not_terminated: "provider_lifecycle_failed";
18
+ };
19
+ export type ProviderFailureCode = (typeof aliases)[keyof typeof aliases];
20
+ /** Facts only, never a retry policy. Do not parse provider prose or raw stderr. */
21
+ export declare function providerFailureCode(error: unknown): ProviderFailureCode | undefined;
22
+ export {};
23
+ //# sourceMappingURL=failure-classification.d.ts.map
@@ -0,0 +1,43 @@
1
+ const aliases = {
2
+ provider_not_configured: "provider_not_configured",
3
+ provider_auth_invalid: "provider_auth_invalid",
4
+ provider_quota_or_billing_required: "provider_quota_or_billing_required",
5
+ provider_rate_limited: "provider_rate_limited",
6
+ provider_model_unavailable: "provider_model_unavailable",
7
+ provider_network_error: "provider_network_error",
8
+ auth_failed: "provider_auth_invalid",
9
+ host_delegation_revoked: "provider_auth_invalid",
10
+ authority_invalid: "provider_auth_invalid",
11
+ gateway_rate_limited: "provider_rate_limited",
12
+ control_queue_full: "provider_rate_limited",
13
+ turn_timeout: "provider_timeout",
14
+ output_parse_failed: "provider_output_invalid",
15
+ output_validation_failed: "provider_output_invalid",
16
+ process_lifecycle_failed: "provider_lifecycle_failed",
17
+ process_tree_not_terminated: "provider_lifecycle_failed",
18
+ };
19
+ /** Facts only, never a retry policy. Do not parse provider prose or raw stderr. */
20
+ export function providerFailureCode(error) {
21
+ if (typeof error !== "object" || error === null)
22
+ return undefined;
23
+ const value = error;
24
+ const code = typeof value.code === "string" ? value.code : value.reason;
25
+ if (typeof code === "string" && Object.hasOwn(aliases, code))
26
+ return aliases[code];
27
+ if (code !== "turn_failed" || typeof value.details !== "object" || value.details === null)
28
+ return undefined;
29
+ const details = value.details;
30
+ // Quota can also carry HTTP 429; the specific bounded code takes precedence.
31
+ if (details.turn_error_code === "insufficient_quota")
32
+ return "provider_quota_or_billing_required";
33
+ if (details.turn_error_code === "invalid_api_key")
34
+ return "provider_auth_invalid";
35
+ if (details.turn_error_code === "model_not_found")
36
+ return "provider_model_unavailable";
37
+ if (details.turn_error_code === "rate_limit_exceeded" || details.turn_error_http_status === 429)
38
+ return "provider_rate_limited";
39
+ if (details.turn_error_http_status === 401)
40
+ return "provider_auth_invalid";
41
+ return undefined;
42
+ }
43
+ //# sourceMappingURL=failure-classification.js.map
@@ -1,6 +1,8 @@
1
1
  import type { SqliteDatabase } from "../store/index.js";
2
2
  import type { ReferenceCopy } from "./cache.js";
3
3
  import type { ReferenceInvocation } from "./service.js";
4
+ import type { ReferenceAuthorization } from "@tutti/shared/schemas/api";
5
+ export declare function recordExternalReferenceAccessRequirement(db: SqliteDatabase, invocation: ReferenceInvocation, requestRef: string, authorization: ReferenceAuthorization, now?: () => Date): void;
4
6
  /** Prior answer context only: a source reference is not proof of freshness in a later turn. */
5
7
  export declare function readExternalReferenceReadSources(db: SqliteDatabase, invocationRef: string): string[];
6
8
  /** Durable observation, separate from evictable cache metadata; no content or locator. */
@@ -1,3 +1,8 @@
1
+ export function recordExternalReferenceAccessRequirement(db, invocation, requestRef, authorization, now = () => new Date()) {
2
+ db.prepare(`INSERT OR IGNORE INTO external_reference_access_requirements
3
+ (invocation_ref, request_ref, activity_ref, authorization_ref, profile, observed_at)
4
+ VALUES (?, ?, ?, ?, ?, ?)`).run(invocation.reference_invocation_ref, requestRef, invocation.activity_ref ?? null, authorization.authorization_ref, authorization.profile, now().toISOString());
5
+ }
1
6
  /** Prior answer context only: a source reference is not proof of freshness in a later turn. */
2
7
  export function readExternalReferenceReadSources(db, invocationRef) {
3
8
  return db
@@ -8,6 +8,8 @@ export type ReferenceInvocation = {
8
8
  };
9
9
  export declare class ReferenceAuthorizationRequiredError extends Error {
10
10
  readonly result: ReferenceAuthorizationRequired;
11
+ readonly code = "reference_authorization_required";
12
+ readonly continuation = "explicit_request";
11
13
  constructor(result: ReferenceAuthorizationRequired);
12
14
  }
13
15
  export declare class ExternalReferenceAccess {
@@ -1,6 +1,8 @@
1
1
  import { ReferenceCacheError } from "./cache.js";
2
2
  export class ReferenceAuthorizationRequiredError extends Error {
3
3
  result;
4
+ code = "reference_authorization_required";
5
+ continuation = "explicit_request";
4
6
  constructor(result) {
5
7
  super("reference_authorization_required");
6
8
  this.result = result;
@@ -1,3 +1,4 @@
1
+ import { providerFailureCode } from "./providers/failure-classification.js";
1
2
  const STABLE_FAILURE_CODE = /^[a-z][a-z0-9_]{0,63}$/u;
2
3
  export function stableRuntimeFailureCode(error, fallback) {
3
4
  if (typeof error === "object" &&
@@ -17,6 +18,19 @@ export function safeRuntimeFailureSummary(subject, error, fallbackCode) {
17
18
  if (code === "admission_unavailable" || code === "admission_closed") {
18
19
  return `${subject} stopped because provider execution is unavailable while the Host is closing. Retry after the Host is ready. (${code})`;
19
20
  }
20
- return `${subject} failed because the runtime returned an internal error. Retry the action. (${code})`;
21
+ if (code === "reference_authorization_required") {
22
+ return `${subject} needs document access. Authorize using the group card, then review the task or request before continuing. (${code})`;
23
+ }
24
+ const providerCode = providerFailureCode(error);
25
+ if (providerCode === "provider_auth_invalid" ||
26
+ providerCode === "provider_quota_or_billing_required" ||
27
+ providerCode === "provider_model_unavailable" ||
28
+ providerCode === "provider_not_configured") {
29
+ return `${subject} could not use model access. Ask the project owner to check the provider or account, then review available recovery actions. (${providerCode})`;
30
+ }
31
+ if (providerCode === "provider_rate_limited") {
32
+ return `${subject} stopped because the model service is busy. Wait, then review available recovery actions. (${providerCode})`;
33
+ }
34
+ return `${subject} could not complete. Review its status and available recovery actions before continuing. (${code})`;
21
35
  }
22
36
  //# sourceMappingURL=runtime-failures.js.map
@@ -4,7 +4,7 @@ import { requestExternalReferences } from "@tutti/relay-client";
4
4
  import { isExternalReferenceList, isExternalReferenceReadResult, isExternalReferenceDescriptor, isReferenceAuthorizationRequired, } from "@tutti/shared/schemas/internal";
5
5
  import { ExternalReferenceCache } from "../../reference-access/cache.js";
6
6
  import { ExternalReferenceAccess, ReferenceAuthorizationRequiredError, } from "../../reference-access/service.js";
7
- import { recordExternalReferenceRead } from "../../reference-access/read-evidence.js";
7
+ import { recordExternalReferenceRead, recordExternalReferenceAccessRequirement, } from "../../reference-access/read-evidence.js";
8
8
  import { downloadStagedUploadWithResolver, cleanupDownloadedStagedUpload, } from "../http/routes/project-api/staged-uploads.js";
9
9
  import { readHostRegistrationSecret } from "./machine-local.js";
10
10
  export function createExternalReferenceRuntime(options) {
@@ -14,6 +14,7 @@ export function createExternalReferenceRuntime(options) {
14
14
  if (!relay)
15
15
  throw new Error("reference_host_offline");
16
16
  const secret = readHostRegistrationSecret(preparation.tutti_home, preparation.project_id);
17
+ const requestRef = `eref_${randomUUID().replaceAll("-", "")}`;
17
18
  const result = await requestExternalReferences({
18
19
  relayUrl: preparation.relay_url,
19
20
  body: {
@@ -22,15 +23,17 @@ export function createExternalReferenceRuntime(options) {
22
23
  host_auth: { kind: "host_registration_secret_v1", secret: secret.secret },
23
24
  relay_project_ref: relay.relay_project_ref,
24
25
  host_connection_ref: relay.host_connection_ref,
25
- request_ref: `eref_${randomUUID().replaceAll("-", "")}`,
26
+ request_ref: requestRef,
26
27
  invocation_ref: invocation.reference_invocation_ref,
27
28
  action,
28
29
  ...(sourceRef === undefined ? {} : { source_ref: sourceRef }),
29
30
  ...(documentUrl === undefined ? {} : { document_url: documentUrl }),
30
31
  },
31
32
  });
32
- if (isReferenceAuthorizationRequired(result))
33
+ if (isReferenceAuthorizationRequired(result)) {
34
+ recordExternalReferenceAccessRequirement(options.store.db, invocation, requestRef, result.authorization);
33
35
  throw new ReferenceAuthorizationRequiredError(result);
36
+ }
34
37
  return result;
35
38
  }
36
39
  return new ExternalReferenceAccess({