@xfey/tutti 0.1.59 → 0.1.61

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 (45) hide show
  1. package/dist/collaboration-state/clarification-records.d.ts +0 -9
  2. package/dist/collaboration-state/clarification-records.js +0 -57
  3. package/dist/collaboration-state/index.d.ts +6 -5
  4. package/dist/collaboration-state/index.js +5 -4
  5. package/dist/collaboration-state/messages.d.ts +4 -6
  6. package/dist/collaboration-state/messages.js +22 -29
  7. package/dist/collaboration-state/scratchpad-source-state.d.ts +4 -12
  8. package/dist/collaboration-state/scratchpad-source-state.js +24 -58
  9. package/dist/collaboration-state/scratchpad.d.ts +1 -2
  10. package/dist/collaboration-state/scratchpad.js +0 -59
  11. package/dist/collaboration-state/storage-types.d.ts +25 -5
  12. package/dist/collaboration-state/task-compile-context.d.ts +40 -0
  13. package/dist/collaboration-state/task-compile-context.js +273 -0
  14. package/dist/collaboration-state/types.d.ts +39 -9
  15. package/dist/control-plane/clarification-commands.d.ts +0 -1
  16. package/dist/control-plane/clarification-commands.js +0 -1
  17. package/dist/control-plane/index.js +16 -3
  18. package/dist/control-plane/project-context-bootstrap.d.ts +4 -4
  19. package/dist/control-plane/project-context-bootstrap.js +7 -12
  20. package/dist/control-plane/scratchpad-auto-refresh.js +12 -1
  21. package/dist/control-plane/scratchpad-refresh-start.js +77 -94
  22. package/dist/control-plane/scratchpad-refresh.js +3 -1
  23. package/dist/control-plane/scratchpad-source-messages.d.ts +15 -7
  24. package/dist/control-plane/scratchpad-source-messages.js +53 -69
  25. package/dist/control-plane/task-compile-clarification-context.d.ts +9 -0
  26. package/dist/control-plane/task-compile-clarification-context.js +70 -0
  27. package/dist/control-plane/task-compile-continuation.js +28 -56
  28. package/dist/control-plane/task-compile-output.js +50 -14
  29. package/dist/control-plane/task-compile-start.js +119 -55
  30. package/dist/control-plane/workflows/openai.d.ts +2 -1
  31. package/dist/control-plane/workflows/openai.js +13 -27
  32. package/dist/control-plane/workflows/types.d.ts +27 -17
  33. package/migrations/0014_scratchpad_task_compile_context.sql +147 -0
  34. package/migrations/README.md +2 -1
  35. package/package.json +1 -1
  36. package/prompts/procedures/README.md +3 -3
  37. package/prompts/procedures/scratchpad-refresh.md +17 -9
  38. package/prompts/procedures/task-compile.md +6 -3
  39. package/prompts/prompt-flow-map.md +21 -14
  40. package/web/assets/{homepage-motion-scene-BJF0AKlJ.js → homepage-motion-scene-CY7o4hnR.js} +1 -1
  41. package/web/assets/index-B08r3x8o.js +69 -0
  42. package/web/assets/index-DpabiRgp.css +1 -0
  43. package/web/index.html +2 -2
  44. package/web/assets/index-8lX8GhfZ.css +0 -1
  45. package/web/assets/index-BHGh4UOg.js +0 -69
@@ -1,16 +1,11 @@
1
1
  import { createWorkflowInvocationRef, } from "@tutti/shared/ids";
2
- import { readClarificationRoundMessagesWindow, readMainChatMessageWindow, readMainChatMessagesAfter, readMainChatMessagesAfterCursor, readMainChatMessagesBeforeCursor, markScratchpadRefreshFailed, markScratchpadRefreshStarted, readScratchpadProjection, readScratchpadSourceState, readSubmittedTaskCompileClarificationRounds, readSubmittedTaskCompileClarificationRoundsAfterCursor, } from "../collaboration-state/index.js";
2
+ import { readActiveTaskCompileContext, readMainChatMessagesForScratchpadCycle, readScratchpadSourceState, readWorklistProjection, markScratchpadRefreshFailed, markScratchpadRefreshStarted, } from "../collaboration-state/index.js";
3
3
  import { readProjectBriefProjection, readProjectBriefSourceDocuments, } from "../project-brief/index.js";
4
4
  import { logProcedureExecutionError } from "./procedure-logging.js";
5
5
  import { applyScratchpadRefreshOutput } from "./scratchpad-refresh.js";
6
- import { compactPreviousScratchpad, compactScratchpadSourceMessages, filterClarificationRoundScratchpadSourceMessages, filterScratchpadSourceMessages, sortScratchpadSourceMessages, } from "./scratchpad-source-messages.js";
7
- const DEFAULT_SCRATCHPAD_SOURCE_MESSAGE_LIMIT = 50;
8
- const INCREMENTAL_SCRATCHPAD_SOURCE_MESSAGE_LIMIT = 200;
9
- const RECENT_CONTEXT_RAW_MESSAGE_LIMIT = 12;
10
- const RECENT_MESSAGE_WINDOW_SIZE = 5;
11
- const DEFAULT_CLARIFICATION_SOURCE_ROUND_LIMIT = 20;
12
- const CLARIFICATION_SOURCE_MESSAGE_LIMIT = 50;
6
+ import { compactScratchpadSourceMessages, compactScratchpadWorklist, filterScratchpadSourceMessages, MAX_SOURCE_MESSAGES_TOTAL_CHARS, MIN_SOURCE_MESSAGE_TEXT_CHARS, ScratchpadSourceBudgetExceededError, } from "./scratchpad-source-messages.js";
13
7
  const SCRATCHPAD_REFRESH_FAILURE_BACKOFF_MS = 60_000;
8
+ const MAX_SCRATCHPAD_WORKFLOW_INPUT_CHARS = 30_000;
14
9
  function readScratchpadProjectBrief(input) {
15
10
  if (input.projectContext === undefined) {
16
11
  return undefined;
@@ -35,105 +30,93 @@ function readScratchpadProjectBrief(input) {
35
30
  return undefined;
36
31
  }
37
32
  }
38
- export function readControlPlaneScratchpadSourceBatch(store, options = {}) {
39
- const sourceState = readScratchpadSourceState(store.db);
40
- const scratchpad = readScratchpadProjection(store.db);
41
- const sourceFallbackSince = scratchpad.state === "ready" ? scratchpad.updated_at : undefined;
42
- const isInitialMainChatRefresh = sourceState.main_chat_cursor === undefined && sourceFallbackSince === undefined;
43
- const messages = sourceState.main_chat_cursor === undefined
44
- ? sourceFallbackSince === undefined
45
- ? readMainChatMessageWindow(store.db, {
46
- limit: DEFAULT_SCRATCHPAD_SOURCE_MESSAGE_LIMIT,
47
- }).items
48
- : readMainChatMessagesAfter(store.db, {
49
- after_created_at: sourceFallbackSince,
50
- limit: INCREMENTAL_SCRATCHPAD_SOURCE_MESSAGE_LIMIT,
51
- })
52
- : readMainChatMessagesAfterCursor(store.db, {
53
- after_created_at: sourceState.main_chat_cursor.created_at,
54
- after_id: sourceState.main_chat_cursor.message_id,
55
- limit: INCREMENTAL_SCRATCHPAD_SOURCE_MESSAGE_LIMIT,
56
- });
57
- const mainChatSourceMessages = filterScratchpadSourceMessages(messages);
58
- const newMainChatSourceMessages = isInitialMainChatRefresh ? [] : mainChatSourceMessages;
59
- const recentMessageLimit = Math.max(0, RECENT_MESSAGE_WINDOW_SIZE - newMainChatSourceMessages.length);
60
- const firstNewMainChatSource = newMainChatSourceMessages[0];
61
- const recentMessages = recentMessageLimit === 0
62
- ? []
63
- : isInitialMainChatRefresh
64
- ? mainChatSourceMessages.slice(-recentMessageLimit)
65
- : firstNewMainChatSource === undefined
66
- ? filterScratchpadSourceMessages(readMainChatMessageWindow(store.db, {
67
- limit: RECENT_CONTEXT_RAW_MESSAGE_LIMIT,
68
- }).items).slice(-recentMessageLimit)
69
- : filterScratchpadSourceMessages(readMainChatMessagesBeforeCursor(store.db, {
70
- before_created_at: firstNewMainChatSource.created_at,
71
- before_id: firstNewMainChatSource.id,
72
- limit: RECENT_CONTEXT_RAW_MESSAGE_LIMIT,
73
- })).slice(-recentMessageLimit);
74
- const clarificationRounds = sourceState.task_compile_cursor === undefined
75
- ? readSubmittedTaskCompileClarificationRounds(store.db, sourceFallbackSince === undefined
76
- ? {
77
- limit: DEFAULT_CLARIFICATION_SOURCE_ROUND_LIMIT,
78
- }
79
- : {
80
- afterSubmittedAt: sourceFallbackSince,
81
- limit: DEFAULT_CLARIFICATION_SOURCE_ROUND_LIMIT,
82
- })
83
- : readSubmittedTaskCompileClarificationRoundsAfterCursor(store.db, {
84
- afterSubmittedAt: sourceState.task_compile_cursor.submitted_at,
85
- afterRoundId: sourceState.task_compile_cursor.round_id,
86
- limit: DEFAULT_CLARIFICATION_SOURCE_ROUND_LIMIT,
87
- });
88
- const clarificationSourceMessages = clarificationRounds.flatMap((round) => {
89
- const window = readClarificationRoundMessagesWindow(store.db, round.id, {
90
- anchor: "start",
91
- limit: CLARIFICATION_SOURCE_MESSAGE_LIMIT,
92
- });
93
- return filterClarificationRoundScratchpadSourceMessages(window?.items ?? []);
94
- });
95
- const sourceMessages = sortScratchpadSourceMessages([
96
- ...newMainChatSourceMessages,
97
- ...clarificationSourceMessages,
98
- ]);
99
- const sourceRefMessages = sortScratchpadSourceMessages([
100
- ...(isInitialMainChatRefresh ? recentMessages : []),
101
- ...sourceMessages,
102
- ]);
103
- const latestMainChatSource = mainChatSourceMessages.at(-1);
104
- const latestClarificationRound = clarificationRounds.at(-1);
105
- const cursorUpdate = {};
106
- if (latestMainChatSource !== undefined) {
107
- cursorUpdate.main_chat_cursor = {
108
- created_at: latestMainChatSource.created_at,
109
- message_id: latestMainChatSource.id,
33
+ function workflowInputSize(input) {
34
+ return JSON.stringify(input).length;
35
+ }
36
+ function fitScratchpadRefreshWorkflowInput(input) {
37
+ const base = {
38
+ ...(input.projectBrief === undefined ? {} : { project_brief: input.projectBrief }),
39
+ ...(input.activeTaskCompile === undefined
40
+ ? {}
41
+ : { active_task_compile: input.activeTaskCompile }),
42
+ };
43
+ const worklistVariants = [
44
+ compactScratchpadWorklist(input.worklist),
45
+ compactScratchpadWorklist(input.worklist, { summaryChars: 320 }),
46
+ compactScratchpadWorklist(input.worklist, { includeSummaries: false }),
47
+ ];
48
+ for (const worklist of worklistVariants) {
49
+ const candidate = {
50
+ ...base,
51
+ worklist,
52
+ messages: compactScratchpadSourceMessages(input.sourceMessages),
110
53
  };
54
+ if (workflowInputSize(candidate) <= MAX_SCRATCHPAD_WORKFLOW_INPUT_CHARS) {
55
+ return candidate;
56
+ }
111
57
  }
112
- if (latestClarificationRound?.submitted_at !== undefined) {
113
- cursorUpdate.task_compile_cursor = {
114
- submitted_at: latestClarificationRound.submitted_at,
115
- round_id: latestClarificationRound.id,
58
+ const worklist = worklistVariants.at(-1);
59
+ if (worklist === undefined) {
60
+ throw new ScratchpadSourceBudgetExceededError();
61
+ }
62
+ const minimumMessageBudget = input.sourceMessages.length * MIN_SOURCE_MESSAGE_TEXT_CHARS;
63
+ for (let messageBudget = MAX_SOURCE_MESSAGES_TOTAL_CHARS - 1_000; messageBudget >= minimumMessageBudget; messageBudget -= 1_000) {
64
+ const candidate = {
65
+ ...base,
66
+ worklist,
67
+ messages: compactScratchpadSourceMessages(input.sourceMessages, {
68
+ totalTextChars: messageBudget,
69
+ }),
116
70
  };
71
+ if (workflowInputSize(candidate) <= MAX_SCRATCHPAD_WORKFLOW_INPUT_CHARS) {
72
+ return candidate;
73
+ }
117
74
  }
75
+ throw new ScratchpadSourceBudgetExceededError();
76
+ }
77
+ export function readControlPlaneScratchpadSourceBatch(store, options = {}) {
78
+ const sourceState = readScratchpadSourceState(store.db);
79
+ const mainChatMessages = readMainChatMessagesForScratchpadCycle(store.db, sourceState.cycle_start_cursor);
80
+ const sourceMessages = filterScratchpadSourceMessages(mainChatMessages);
81
+ const latestSource = sourceMessages.at(-1);
82
+ const unrefreshedSourceCount = sourceMessages.filter((message) => {
83
+ const refreshedThrough = sourceState.refreshed_through_cursor;
84
+ if (refreshedThrough === undefined) {
85
+ return true;
86
+ }
87
+ const timeOrder = message.created_at.localeCompare(refreshedThrough.created_at);
88
+ return timeOrder > 0 || (timeOrder === 0 && message.id > refreshedThrough.message_id);
89
+ }).length;
90
+ const activeContext = readActiveTaskCompileContext(store.db);
118
91
  const projectBrief = readScratchpadProjectBrief({
119
92
  store,
120
93
  ...(options.projectContext === undefined ? {} : { projectContext: options.projectContext }),
121
94
  ...(options.now === undefined ? {} : { now: options.now }),
122
95
  });
96
+ const workflowInput = fitScratchpadRefreshWorkflowInput({
97
+ ...(projectBrief === undefined ? {} : { projectBrief }),
98
+ ...(activeContext === null
99
+ ? {}
100
+ : { activeTaskCompile: activeContext.scratchpad_snapshot }),
101
+ worklist: readWorklistProjection(store.db),
102
+ sourceMessages,
103
+ });
123
104
  return {
124
- workflow_input: {
125
- ...(projectBrief === undefined ? {} : { project_brief: projectBrief }),
126
- previous: compactPreviousScratchpad(scratchpad),
127
- recent_messages: compactScratchpadSourceMessages(recentMessages),
128
- new_messages: compactScratchpadSourceMessages(sourceMessages),
129
- },
130
- cursor_update: cursorUpdate,
131
- source_refs: sourceRefMessages.map((message) => ({
105
+ workflow_input: workflowInput,
106
+ cursor_update: latestSource === undefined
107
+ ? {}
108
+ : {
109
+ refreshed_through_cursor: {
110
+ created_at: latestSource.created_at,
111
+ message_id: latestSource.id,
112
+ },
113
+ },
114
+ source_refs: sourceMessages.map((message) => ({
132
115
  message_id: message.id,
133
- source_scope: message.scope.kind,
116
+ source_scope: "main_chat",
134
117
  created_at: message.created_at,
135
118
  })),
136
- source_message_count: sourceMessages.length + (isInitialMainChatRefresh ? recentMessages.length : 0),
119
+ source_message_count: unrefreshedSourceCount,
137
120
  };
138
121
  }
139
122
  export async function runControlPlaneScratchpadRefreshProcedure(options) {
@@ -1,4 +1,4 @@
1
- import { createMainChatAgentMessage, markScratchpadRefreshSucceeded, upsertScratchpadProjection, } from "../collaboration-state/index.js";
1
+ import { createMainChatAgentMessage, markScratchpadRefreshSucceeded, markScratchpadSourceDirty, upsertScratchpadProjection, } from "../collaboration-state/index.js";
2
2
  import { mainChatInvalidates, scratchpadRefreshInvalidates, } from "./invalidations.js";
3
3
  export function applyScratchpadRefreshOutput(input) {
4
4
  if (input.output.decision === "needs_human") {
@@ -17,8 +17,10 @@ export function applyScratchpadRefreshOutput(input) {
17
17
  invalidates: mainChatInvalidates(),
18
18
  });
19
19
  markScratchpadRefreshSucceeded(input.store.db, {
20
+ ...input.cursorUpdate,
20
21
  now: input.now,
21
22
  });
23
+ markScratchpadSourceDirty(input.store.db, { now: input.now });
22
24
  return {
23
25
  kind: "needs_human",
24
26
  summary: input.output.request_payload.summary,
@@ -1,10 +1,18 @@
1
- import type { MessageProjection, ScratchpadProjection } from "@tutti/shared/schemas/api";
2
- import type { ScratchpadRefreshSourceMessageInput, ScratchpadRefreshWorkflowInput } from "./workflows/index.js";
1
+ import type { MessageProjection, WorklistProjection } from "@tutti/shared/schemas/api";
2
+ import type { ScratchpadRefreshSourceMessageInput, ScratchpadRefreshWorklistInput } from "./workflows/index.js";
3
+ export declare const MAX_SOURCE_MESSAGES_TOTAL_CHARS = 10000;
4
+ export declare const MIN_SOURCE_MESSAGE_TEXT_CHARS = 160;
5
+ export declare class ScratchpadSourceBudgetExceededError extends Error {
6
+ constructor();
7
+ }
3
8
  export declare function shouldUseMessageAsScratchpadSource(message: MessageProjection): boolean;
4
- export declare function shouldUseClarificationRoundMessageAsScratchpadSource(message: MessageProjection): boolean;
5
9
  export declare function filterScratchpadSourceMessages(messages: MessageProjection[]): MessageProjection[];
6
- export declare function filterClarificationRoundScratchpadSourceMessages(messages: MessageProjection[]): MessageProjection[];
7
- export declare function sortScratchpadSourceMessages(messages: MessageProjection[]): MessageProjection[];
8
- export declare function compactScratchpadSourceMessages(messages: MessageProjection[]): ScratchpadRefreshSourceMessageInput[];
9
- export declare function compactPreviousScratchpad(scratchpad: ScratchpadProjection): ScratchpadRefreshWorkflowInput["previous"];
10
+ export declare function truncateTextMiddle(text: string, maxChars: number): string;
11
+ export declare function compactScratchpadSourceMessages(messages: MessageProjection[], options?: {
12
+ totalTextChars?: number;
13
+ }): ScratchpadRefreshSourceMessageInput[];
14
+ export declare function compactScratchpadWorklist(worklist: WorklistProjection, options?: {
15
+ summaryChars?: number;
16
+ includeSummaries?: boolean;
17
+ }): ScratchpadRefreshWorklistInput;
10
18
  //# sourceMappingURL=scratchpad-source-messages.d.ts.map
@@ -1,10 +1,19 @@
1
- const MAX_COMPACT_SOURCE_MESSAGES = 20;
2
1
  const MAX_SOURCE_MESSAGE_TEXT_CHARS = 2_000;
3
- const MAX_SOURCE_MESSAGES_TOTAL_CHARS = 10_000;
2
+ export const MAX_SOURCE_MESSAGES_TOTAL_CHARS = 10_000;
3
+ export const MIN_SOURCE_MESSAGE_TEXT_CHARS = 160;
4
+ const MESSAGE_BOUNDARY = "\n\n--- next message ---\n\n";
5
+ const MIDDLE_OMISSION = "\n...[middle omitted by Tutti]...\n";
6
+ const MAX_WORKLIST_SUMMARY_CHARS = 800;
7
+ export class ScratchpadSourceBudgetExceededError extends Error {
8
+ constructor() {
9
+ super("Scratchpad source messages exceed the minimum deterministic context budget");
10
+ this.name = "ScratchpadSourceBudgetExceededError";
11
+ }
12
+ }
4
13
  export function shouldUseMessageAsScratchpadSource(message) {
5
14
  const explicitSource = message.refs?.scratchpad_source;
6
15
  if (explicitSource !== undefined) {
7
- return explicitSource === "include";
16
+ return message.scope.kind === "main_chat" && explicitSource === "include";
8
17
  }
9
18
  if (message.scope.kind !== "main_chat") {
10
19
  return false;
@@ -17,47 +26,31 @@ export function shouldUseMessageAsScratchpadSource(message) {
17
26
  }
18
27
  return false;
19
28
  }
20
- export function shouldUseClarificationRoundMessageAsScratchpadSource(message) {
21
- const explicitSource = message.refs?.scratchpad_source;
22
- if (explicitSource !== undefined) {
23
- return explicitSource === "include";
24
- }
25
- if (message.scope.kind !== "clarification_round") {
26
- return false;
27
- }
28
- if (message.message_kind === "clarification_request") {
29
- return true;
30
- }
31
- return message.message_kind === "user_text" && message.author.kind === "human";
32
- }
33
29
  export function filterScratchpadSourceMessages(messages) {
34
30
  return messages.filter(shouldUseMessageAsScratchpadSource);
35
31
  }
36
- export function filterClarificationRoundScratchpadSourceMessages(messages) {
37
- return messages.filter(shouldUseClarificationRoundMessageAsScratchpadSource);
38
- }
39
- export function sortScratchpadSourceMessages(messages) {
40
- return [...messages].sort((left, right) => {
41
- const timeOrder = left.created_at.localeCompare(right.created_at);
42
- return timeOrder === 0 ? left.id.localeCompare(right.id) : timeOrder;
43
- });
44
- }
45
32
  function compactRole(message) {
46
33
  return message.author.kind === "agent" ? "tutti" : "human";
47
34
  }
48
- function truncateSourceText(text) {
35
+ export function truncateTextMiddle(text, maxChars) {
49
36
  const trimmed = text.trim();
50
- if (trimmed.length <= MAX_SOURCE_MESSAGE_TEXT_CHARS) {
37
+ if (trimmed.length <= maxChars) {
51
38
  return trimmed;
52
39
  }
53
- return `${trimmed.slice(0, MAX_SOURCE_MESSAGE_TEXT_CHARS - 3)}...`;
40
+ if (maxChars < MIDDLE_OMISSION.length + 2) {
41
+ throw new ScratchpadSourceBudgetExceededError();
42
+ }
43
+ const remaining = maxChars - MIDDLE_OMISSION.length;
44
+ const headLength = Math.ceil(remaining / 2);
45
+ const tailLength = Math.floor(remaining / 2);
46
+ return `${trimmed.slice(0, headLength)}${MIDDLE_OMISSION}${trimmed.slice(-tailLength)}`;
54
47
  }
55
- function sourceReferenceLabel(message) {
48
+ function sourceText(message) {
56
49
  const reference = message.refs?.reference_file;
57
50
  if (reference === undefined) {
58
- return undefined;
51
+ return message.body;
59
52
  }
60
- return `${reference.category}:${reference.name}`;
53
+ return `Reference file (${reference.category}:${reference.name})\n${message.body}`;
61
54
  }
62
55
  function sourceAuthorDisplayName(message) {
63
56
  if (message.author.kind !== "human") {
@@ -66,14 +59,12 @@ function sourceAuthorDisplayName(message) {
66
59
  const displayName = message.author.display_name?.trim();
67
60
  return displayName === "" ? undefined : displayName;
68
61
  }
69
- function compactSourceMessage(message) {
70
- const ref = sourceReferenceLabel(message);
62
+ function compactSourceMessage(message, maxChars) {
71
63
  const authorDisplayName = sourceAuthorDisplayName(message);
72
64
  return {
73
65
  role: compactRole(message),
74
66
  ...(authorDisplayName === undefined ? {} : { author_display_name: authorDisplayName }),
75
- text: truncateSourceText(message.body),
76
- ...(ref === undefined ? {} : { ref }),
67
+ text: truncateTextMiddle(sourceText(message), maxChars),
77
68
  };
78
69
  }
79
70
  function mergeCompactSourceMessages(messages) {
@@ -82,50 +73,43 @@ function mergeCompactSourceMessages(messages) {
82
73
  const previous = merged.at(-1);
83
74
  if (previous !== undefined &&
84
75
  previous.role === message.role &&
85
- previous.ref === message.ref &&
86
76
  previous.author_display_name === message.author_display_name) {
87
- previous.text = truncateSourceText(`${previous.text}\n${message.text}`);
77
+ previous.text = `${previous.text}${MESSAGE_BOUNDARY}${message.text}`;
88
78
  continue;
89
79
  }
90
80
  merged.push({ ...message });
91
81
  }
92
82
  return merged;
93
83
  }
94
- function enforceTotalSourceTextLimit(messages) {
95
- const result = [];
96
- let remaining = MAX_SOURCE_MESSAGES_TOTAL_CHARS;
97
- for (const message of messages.toReversed()) {
98
- if (remaining <= 0) {
99
- break;
100
- }
101
- const text = message.text.length <= remaining
102
- ? message.text
103
- : `${message.text.slice(0, Math.max(0, remaining - 3))}...`;
104
- result.push({
105
- ...message,
106
- text,
107
- });
108
- remaining -= text.length;
84
+ export function compactScratchpadSourceMessages(messages, options = {}) {
85
+ if (messages.length === 0) {
86
+ return [];
109
87
  }
110
- return result.reverse();
111
- }
112
- export function compactScratchpadSourceMessages(messages) {
113
- return enforceTotalSourceTextLimit(mergeCompactSourceMessages(messages.map(compactSourceMessage)).slice(-MAX_COMPACT_SOURCE_MESSAGES));
114
- }
115
- export function compactPreviousScratchpad(scratchpad) {
116
- if (scratchpad.state !== "ready") {
117
- return {};
88
+ const totalTextChars = options.totalTextChars ?? MAX_SOURCE_MESSAGES_TOTAL_CHARS;
89
+ const boundaryBudget = MESSAGE_BOUNDARY.length * Math.max(0, messages.length - 1);
90
+ const availableTextChars = totalTextChars - boundaryBudget;
91
+ if (availableTextChars < messages.length * MIN_SOURCE_MESSAGE_TEXT_CHARS) {
92
+ throw new ScratchpadSourceBudgetExceededError();
118
93
  }
94
+ const perMessageChars = Math.min(MAX_SOURCE_MESSAGE_TEXT_CHARS, Math.floor(availableTextChars / messages.length));
95
+ return mergeCompactSourceMessages(messages.map((message) => compactSourceMessage(message, perMessageChars)));
96
+ }
97
+ export function compactScratchpadWorklist(worklist, options = {}) {
98
+ const summaryChars = options.summaryChars ?? MAX_WORKLIST_SUMMARY_CHARS;
99
+ const includeSummaries = options.includeSummaries ?? true;
119
100
  return {
120
- ...(scratchpad.topic.trim() === "" ? {} : { topic: scratchpad.topic }),
121
- ...(scratchpad.background_summary.trim() === ""
122
- ? {}
123
- : { background: scratchpad.background_summary }),
124
- ...(scratchpad.current_consensus.length === 0
125
- ? {}
126
- : { consensus: scratchpad.current_consensus }),
127
- ...(scratchpad.open_questions.length === 0 ? {} : { questions: scratchpad.open_questions }),
128
- ...(scratchpad.task_changes.length === 0 ? {} : { changes: scratchpad.task_changes }),
101
+ modules: worklist.modules
102
+ .filter((entry) => !entry.module.archived)
103
+ .map((entry) => ({
104
+ name: entry.module.name,
105
+ tasks: entry.tasks.map((task) => ({
106
+ title: task.title,
107
+ ...(includeSummaries && task.summary.trim() !== ""
108
+ ? { summary: truncateTextMiddle(task.summary, summaryChars) }
109
+ : {}),
110
+ status: task.status,
111
+ })),
112
+ })),
129
113
  };
130
114
  }
131
115
  //# sourceMappingURL=scratchpad-source-messages.js.map
@@ -0,0 +1,9 @@
1
+ import type { ClarificationRoundRef, WorkflowInvocationRef } from "@tutti/shared/ids";
2
+ import type { HostProjectStore } from "../store/index.js";
3
+ import type { TaskCompileWorkflowInput } from "./workflows/index.js";
4
+ export declare function readTaskCompileClarificationRounds(input: {
5
+ store: HostProjectStore;
6
+ workflowRef: WorkflowInvocationRef;
7
+ throughRoundId: ClarificationRoundRef;
8
+ }): NonNullable<TaskCompileWorkflowInput["clarification_rounds"]>;
9
+ //# sourceMappingURL=task-compile-clarification-context.d.ts.map
@@ -0,0 +1,70 @@
1
+ import { readActiveClarificationProjection, readClarificationRoundProjection, readClarificationRoundsForThread, readMessageProjectionById, } from "../collaboration-state/index.js";
2
+ import { ScratchpadSourceBudgetExceededError, truncateTextMiddle } from "./scratchpad-source-messages.js";
3
+ const MAX_CLARIFICATION_MESSAGE_CHARS = 4_000;
4
+ const MAX_CLARIFICATION_MESSAGES_TOTAL_CHARS = 16_000;
5
+ const MIN_CLARIFICATION_MESSAGE_CHARS = 160;
6
+ function answerMessage(store, messageId, roundId) {
7
+ const message = readMessageProjectionById(store.db, messageId);
8
+ if (message === null ||
9
+ message.scope.kind !== "clarification_round" ||
10
+ message.scope.clarification_round_ref !== roundId ||
11
+ (message.message_kind !== "user_text" && message.message_kind !== "agent_text")) {
12
+ return null;
13
+ }
14
+ const role = message.author.kind === "agent" ? "tutti" : "human";
15
+ const displayName = message.author.display_name?.trim();
16
+ return {
17
+ role,
18
+ ...(role === "human" && displayName !== undefined && displayName !== ""
19
+ ? { author_display_name: displayName }
20
+ : {}),
21
+ text: message.body,
22
+ };
23
+ }
24
+ export function readTaskCompileClarificationRounds(input) {
25
+ const active = readActiveClarificationProjection(input.store.db);
26
+ if (active.state !== "active" ||
27
+ active.thread.owner.kind !== "task_compile" ||
28
+ active.thread.owner.workflow_ref !== input.workflowRef ||
29
+ active.round.id !== input.throughRoundId) {
30
+ throw new Error("Task Compile clarification owner does not match its pinned context");
31
+ }
32
+ const throughRound = readClarificationRoundProjection(input.store.db, input.throughRoundId);
33
+ if (throughRound === null || throughRound.status !== "sealed") {
34
+ throw new Error("Task Compile continuation requires a sealed clarification round");
35
+ }
36
+ const rounds = readClarificationRoundsForThread(input.store.db, throughRound.thread_id, {
37
+ throughRoundIndex: throughRound.round_index,
38
+ });
39
+ const contextRounds = rounds.map((round) => {
40
+ if (round.status !== "sealed") {
41
+ throw new Error("Task Compile clarification context cannot include a writable round");
42
+ }
43
+ const answerMessages = (round.submitted_message_refs ?? [])
44
+ .filter((messageId) => messageId !== round.opening_message_id && messageId !== round.card_message_id)
45
+ .flatMap((messageId) => {
46
+ const mapped = answerMessage(input.store, messageId, round.id);
47
+ return mapped === null ? [] : [mapped];
48
+ });
49
+ return {
50
+ request_payload: round.request_payload,
51
+ answer_messages: answerMessages,
52
+ };
53
+ });
54
+ const answerCount = contextRounds.reduce((count, round) => count + round.answer_messages.length, 0);
55
+ if (answerCount === 0) {
56
+ return contextRounds;
57
+ }
58
+ if (answerCount * MIN_CLARIFICATION_MESSAGE_CHARS > MAX_CLARIFICATION_MESSAGES_TOTAL_CHARS) {
59
+ throw new ScratchpadSourceBudgetExceededError();
60
+ }
61
+ const perMessageChars = Math.min(MAX_CLARIFICATION_MESSAGE_CHARS, Math.floor(MAX_CLARIFICATION_MESSAGES_TOTAL_CHARS / answerCount));
62
+ return contextRounds.map((round) => ({
63
+ request_payload: round.request_payload,
64
+ answer_messages: round.answer_messages.map((message) => ({
65
+ ...message,
66
+ text: truncateTextMiddle(message.text, perMessageChars),
67
+ })),
68
+ }));
69
+ }
70
+ //# sourceMappingURL=task-compile-clarification-context.js.map
@@ -1,12 +1,17 @@
1
- import { advanceScratchpadTaskCompileSourceCursor, materializeClarificationSuccessor, readClarificationRoundMessagesWindow, readClarificationRoundProjection, readScratchpadProjection, readWorklistProjection, } from "../collaboration-state/index.js";
1
+ import { materializeClarificationSuccessor, readActiveTaskCompileContext, readClarificationRoundProjection, readWorklistProjection, rollbackTaskCompileContext, } from "../collaboration-state/index.js";
2
+ import { withHostStoreTransaction } from "../store/index.js";
2
3
  import { clarificationInvalidates } from "./invalidations.js";
3
4
  import { applyTaskCompileProcedureOutput } from "./task-compile-output.js";
5
+ import { readTaskCompileClarificationRounds } from "./task-compile-clarification-context.js";
4
6
  import { logProcedureExecutionError } from "./procedure-logging.js";
5
7
  export function startControlPlaneTaskCompileContinuation(options) {
6
8
  if (options.owner.kind !== "task_compile") {
7
9
  return false;
8
10
  }
9
11
  const workflowRef = options.owner.workflow_ref;
12
+ if (readActiveTaskCompileContext(options.store.db, workflowRef) === null) {
13
+ return false;
14
+ }
10
15
  const runnerResolution = options.resolveWorkflowRunner();
11
16
  if (runnerResolution.kind !== "ready") {
12
17
  return false;
@@ -29,28 +34,26 @@ export function startControlPlaneTaskCompileContinuation(options) {
29
34
  execute: async (context) => {
30
35
  const round = readClarificationRoundProjection(options.store.db, options.roundId);
31
36
  try {
32
- const messagesWindow = round === null
33
- ? null
34
- : readClarificationRoundMessagesWindow(options.store.db, options.roundId, {
35
- anchor: "start",
36
- });
37
- const clarification = round === null
38
- ? undefined
39
- : {
40
- request_payload: round.request_payload,
41
- messages: messagesWindow?.items ?? [],
42
- };
37
+ const taskCompileContext = readActiveTaskCompileContext(options.store.db, workflowRef);
38
+ if (taskCompileContext === null) {
39
+ throw new Error("Active Task Compile Context is unavailable");
40
+ }
41
+ const clarificationRounds = readTaskCompileClarificationRounds({
42
+ store: options.store,
43
+ workflowRef,
44
+ throughRoundId: options.roundId,
45
+ });
43
46
  const output = await runnerResolution.runner.runTaskCompile({
44
47
  workflow_ref: workflowRef,
45
- scratchpad: readScratchpadProjection(options.store.db),
48
+ scratchpad: taskCompileContext.scratchpad_snapshot,
46
49
  worklist: readWorklistProjection(options.store.db),
47
- ...(clarification === undefined ? {} : { clarification }),
50
+ clarification_rounds: clarificationRounds,
48
51
  }, {
49
52
  workflow_ref: workflowRef,
50
53
  activity_ref: context.activity_ref,
51
54
  reportProgress: context.reportProgress,
52
55
  });
53
- const conclusion = applyTaskCompileProcedureOutput({
56
+ return applyTaskCompileProcedureOutput({
54
57
  store: options.store,
55
58
  events: options.events,
56
59
  output,
@@ -60,52 +63,21 @@ export function startControlPlaneTaskCompileContinuation(options) {
60
63
  continueFromRoundId: options.roundId,
61
64
  onTasksAdded: options.onTasksAdded,
62
65
  });
63
- if (output.decision === "ready" &&
64
- conclusion.kind === "completed" &&
65
- round?.submitted_at !== undefined) {
66
- advanceScratchpadTaskCompileSourceCursor(options.store.db, {
67
- submitted_at: round.submitted_at,
66
+ }
67
+ catch (error) {
68
+ const threadId = round?.thread_id;
69
+ withHostStoreTransaction(options.store.db, (tx) => {
70
+ materializeClarificationSuccessor(tx, {
68
71
  round_id: options.roundId,
72
+ successor_ref: { kind: "chain_closed" },
73
+ close_thread: true,
74
+ closed_reason: "cancelled",
69
75
  now: options.now,
70
76
  });
71
- }
72
- if (output.decision === "ready") {
73
- const threadId = round?.thread_id;
74
- materializeClarificationSuccessor(options.store.db, {
75
- round_id: options.roundId,
76
- successor_ref: {
77
- kind: "proposal_applied",
78
- workflow_ref: workflowRef,
79
- },
80
- close_thread: true,
77
+ rollbackTaskCompileContext(tx, {
78
+ workflow_ref: workflowRef,
81
79
  now: options.now,
82
80
  });
83
- if (threadId !== undefined) {
84
- options.events.publish({
85
- event_type: "clarification.changed",
86
- payload: {
87
- reason: "successor_materialized",
88
- thread_id: threadId,
89
- round_id: options.roundId,
90
- successor_ref: {
91
- kind: "proposal_applied",
92
- workflow_ref: workflowRef,
93
- },
94
- },
95
- invalidates: clarificationInvalidates(options.roundId, threadId),
96
- });
97
- }
98
- }
99
- return conclusion;
100
- }
101
- catch (error) {
102
- const threadId = round?.thread_id;
103
- materializeClarificationSuccessor(options.store.db, {
104
- round_id: options.roundId,
105
- successor_ref: { kind: "chain_closed" },
106
- close_thread: true,
107
- closed_reason: "cancelled",
108
- now: options.now,
109
81
  });
110
82
  if (threadId !== undefined) {
111
83
  options.events.publish({