@xfey/tutti 0.1.20 → 0.1.22

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 (42) hide show
  1. package/dist/collaboration-state/index.d.ts +1 -1
  2. package/dist/collaboration-state/index.js +1 -1
  3. package/dist/collaboration-state/messages.js +50 -1
  4. package/dist/collaboration-state/scratchpad.d.ts +2 -0
  5. package/dist/collaboration-state/scratchpad.js +63 -0
  6. package/dist/collaboration-state/storage-types.d.ts +5 -0
  7. package/dist/collaboration-state/types.d.ts +8 -1
  8. package/dist/control-plane/event-publishers.js +2 -2
  9. package/dist/control-plane/invalidations.d.ts +2 -1
  10. package/dist/control-plane/invalidations.js +25 -0
  11. package/dist/control-plane/scratchpad-refresh-start.d.ts +2 -0
  12. package/dist/control-plane/scratchpad-refresh-start.js +10 -0
  13. package/dist/control-plane/scratchpad-refresh.d.ts +2 -0
  14. package/dist/control-plane/scratchpad-refresh.js +1 -0
  15. package/dist/project-timeline/index.d.ts +17 -3
  16. package/dist/server-shell/http/routes/project-api/openapi-collaboration-routes.d.ts +1 -0
  17. package/dist/server-shell/http/routes/project-api/openapi-timeline-routes.d.ts +94 -5
  18. package/dist/server-shell/http/routes/project-api/openapi.d.ts +95 -5
  19. package/dist/server-shell/http/routes/project-api/payload-validation.d.ts +6 -2
  20. package/dist/server-shell/http/routes/project-api/payload-validation.js +3 -0
  21. package/dist/server-shell/http/routes/project-api/project-timeline-projection.js +694 -502
  22. package/migrations/0013_scratchpad_receipt_sources.sql +24 -0
  23. package/migrations/README.md +3 -2
  24. package/node_modules/@tutti/shared/dist/schemas/api/index.d.ts +1 -1
  25. package/node_modules/@tutti/shared/dist/schemas/api/index.js +1 -1
  26. package/node_modules/@tutti/shared/dist/schemas/api/messages.d.ts +1 -0
  27. package/node_modules/@tutti/shared/dist/schemas/api/messages.js +1 -0
  28. package/node_modules/@tutti/shared/dist/schemas/api/project-timeline.d.ts +313 -12
  29. package/node_modules/@tutti/shared/dist/schemas/api/project-timeline.js +131 -14
  30. package/node_modules/@tutti/shared/dist/schemas/api/types.d.ts +5 -1
  31. package/package.json +1 -1
  32. package/prompts/procedures/README.md +1 -1
  33. package/prompts/procedures/task-compile.md +9 -2
  34. package/prompts/runs/README.md +2 -2
  35. package/prompts/runs/task-continuation.md +11 -4
  36. package/prompts/runs/task-retry.md +11 -4
  37. package/prompts/runs/task-run.md +11 -4
  38. package/web/assets/index-BfFYfzeO.js +29 -0
  39. package/web/assets/index-De6mK5Q2.css +1 -0
  40. package/web/index.html +2 -2
  41. package/web/assets/index-BtneECwp.js +0 -29
  42. package/web/assets/index-CM9mbbeW.css +0 -1
@@ -1,22 +1,24 @@
1
- import { spawnSync } from "node:child_process";
2
- import { providerUsageSourceMetadata, readProviderUsageBreakdownForAnchors, readProviderUsageModelSummariesForAnchors, readProviderUsageProjection, readProviderUsageSourceSummariesForAnchors, } from "../../../../provider-usage/index.js";
3
- import { readProjectTimelineEventItems } from "../../../../project-timeline/index.js";
4
- import { HOST_MAINLINE_BRANCH } from "./constants.js";
1
+ import { readProviderUsageBreakdownForAnchors, readProviderUsageModelSummariesForAnchors, readProviderUsageProjection, readProviderUsageSourceSummariesForAnchors, } from "../../../../provider-usage/index.js";
2
+ import { runGitRaw } from "../../../../workspace-ops/git.js";
5
3
  const TIMELINE_LIMIT = 50;
6
- const COMMIT_TIMELINE_LIMIT = 30;
4
+ const TASK_ACCEPTED_TIMELINE_LIMIT = 50;
7
5
  const RUN_RESULT_TIMELINE_LIMIT = 50;
8
- const TASK_TIMELINE_LIMIT = 50;
9
- const CLARIFICATION_TIMELINE_LIMIT = 50;
10
- const RECOVERY_TIMELINE_LIMIT = 30;
11
- const DURABLE_EVENT_TIMELINE_LIMIT = 50;
6
+ const CHECKPOINT_TIMELINE_LIMIT = 30;
12
7
  const RUN_USAGE_SOURCES = [
13
8
  "workspace_write_run",
14
9
  "follow_up_run",
15
10
  "pipeline_self_correction",
16
11
  ];
17
- const TASK_ADDED_USAGE_SOURCES = ["task_compile"];
18
- const CONTEXT_SYNC_USAGE_SOURCES = ["context_sync"];
19
- const REFERENCE_SUMMARY_USAGE_SOURCES = ["reference_summary_refresh"];
12
+ const TASK_ACCEPTED_USAGE_SOURCES = ["task_compile"];
13
+ const TRACE_CONSENSUS_LIMIT = 3;
14
+ const TRACE_TASK_CHANGE_LIMIT = 2;
15
+ const TRACE_CONTRACT_SCOPE_LIMIT = 5;
16
+ const TRACE_CHANGED_PATH_LIMIT = 6;
17
+ const TRACE_CHECKPOINT_ITEM_LIMIT = 5;
18
+ const TRACE_GENERATED_FILE_LIMIT = 5;
19
+ const TRACE_SOURCE_SNIPPET_LIMIT = 3;
20
+ const TRACE_SOURCE_SNIPPET_BODY_LIMIT = 280;
21
+ const TRACE_CHANGE_LINK_LIMIT = 3;
20
22
  function compareTimelineItemsByTimeDesc(left, right) {
21
23
  const leftTime = Date.parse(left.occurred_at);
22
24
  const rightTime = Date.parse(right.occurred_at);
@@ -25,62 +27,26 @@ function compareTimelineItemsByTimeDesc(left, right) {
25
27
  : right.occurred_at.localeCompare(left.occurred_at);
26
28
  return byTime === 0 ? right.id.localeCompare(left.id) : byTime;
27
29
  }
28
- function attachProviderUsageToItem(db, item, anchors, options = {}) {
29
- const tokenUsage = readProviderUsageBreakdownForAnchors(db, anchors, options);
30
- if (tokenUsage.event_count <= 0) {
31
- return false;
32
- }
33
- item.token_usage = tokenUsage;
34
- item.usage_sources = readProviderUsageSourceSummariesForAnchors(db, anchors, options);
35
- item.usage_models = readProviderUsageModelSummariesForAnchors(db, anchors, options);
36
- return true;
37
- }
38
- function pushConsumedProcedureUsageAnchors(consumed, sources, anchors) {
39
- const hasAnchor = anchors.activity_ref !== undefined ||
40
- anchors.workflow_ref !== undefined ||
41
- anchors.task_id !== undefined;
42
- if (!hasAnchor) {
43
- return;
44
- }
45
- for (const source of sources) {
46
- if (anchors.activity_ref !== undefined) {
47
- consumed.push({ source, activity_ref: anchors.activity_ref });
48
- }
49
- if (anchors.workflow_ref !== undefined) {
50
- consumed.push({ source, workflow_ref: anchors.workflow_ref });
51
- }
52
- if (anchors.task_id !== undefined) {
53
- consumed.push({ source, task_id: anchors.task_id });
54
- }
55
- }
56
- }
57
- function isProcedureUsageConsumed(row, consumed) {
58
- return consumed.some((anchor) => {
59
- if (anchor.source !== row.source) {
60
- return false;
61
- }
62
- if (anchor.activity_ref !== undefined && anchor.activity_ref !== row.activity_ref) {
63
- return false;
64
- }
65
- if (anchor.workflow_ref !== undefined && anchor.workflow_ref !== row.workflow_ref) {
66
- return false;
67
- }
68
- if (anchor.task_id !== undefined && anchor.task_id !== row.task_id) {
69
- return false;
70
- }
71
- return true;
72
- });
73
- }
74
- function runGit(args, cwd) {
75
- const result = spawnSync("git", [...args], {
76
- cwd,
77
- encoding: "utf8",
78
- shell: false,
79
- });
80
- if (result.status !== 0) {
81
- return null;
82
- }
83
- return (result.stdout ?? "").trim();
30
+ function emptyUsageOverview() {
31
+ const emptyBreakdown = {
32
+ input_tokens: 0,
33
+ cached_input_tokens: 0,
34
+ output_tokens: 0,
35
+ reasoning_output_tokens: 0,
36
+ total_tokens: 0,
37
+ event_count: 0,
38
+ };
39
+ return {
40
+ total_tokens: 0,
41
+ totals: { ...emptyBreakdown },
42
+ by_source: [],
43
+ by_model: [],
44
+ recent_windows: {
45
+ today: { ...emptyBreakdown },
46
+ last_7_days: { ...emptyBreakdown },
47
+ last_30_days: { ...emptyBreakdown },
48
+ },
49
+ };
84
50
  }
85
51
  function parseJsonObject(value) {
86
52
  try {
@@ -95,6 +61,9 @@ function parseJsonObject(value) {
95
61
  return {};
96
62
  }
97
63
  function parseJsonStringArray(value) {
64
+ if (value === null) {
65
+ return [];
66
+ }
98
67
  try {
99
68
  const parsed = JSON.parse(value);
100
69
  if (!Array.isArray(parsed)) {
@@ -106,89 +75,324 @@ function parseJsonStringArray(value) {
106
75
  return [];
107
76
  }
108
77
  }
109
- function readCommitTimelineItems(workspaceRoot) {
110
- if (workspaceRoot === undefined) {
111
- return [];
112
- }
113
- const output = runGit([
114
- "log",
115
- HOST_MAINLINE_BRANCH,
116
- `--max-count=${COMMIT_TIMELINE_LIMIT}`,
117
- "--date=iso-strict",
118
- "--pretty=format:%H%x00%cI%x00%s",
119
- ], workspaceRoot);
120
- if (output === null || output.length === 0) {
121
- return [];
122
- }
123
- return output
124
- .split("\n")
125
- .map((line) => {
126
- const [oid, committedAt, subject] = line.split("\0");
127
- if (oid === undefined || committedAt === undefined || subject === undefined) {
128
- return null;
129
- }
130
- return {
131
- id: `commit:${oid}`,
132
- kind: "commit",
133
- occurred_at: committedAt,
134
- title: subject.trim() === "" ? "Mainline commit" : subject,
135
- commit_oid: oid,
136
- commit_short_oid: oid.slice(0, 12),
137
- status: "completed",
138
- };
139
- })
140
- .filter((item) => item !== null);
78
+ function stringValue(record, key) {
79
+ const value = record[key];
80
+ return typeof value === "string" && value.trim() !== "" ? value : undefined;
141
81
  }
142
- function runResultStatus(row, result) {
143
- if (row.result_kind === "needs_human") {
144
- return "needs_human";
145
- }
146
- if (row.result_kind === "failed" || result.outcome === "failed") {
147
- return "failed";
82
+ function stringArrayValue(record, key) {
83
+ const value = record[key];
84
+ if (!Array.isArray(value)) {
85
+ return undefined;
148
86
  }
149
- return "completed";
87
+ return value.filter((item) => typeof item === "string" && item.trim() !== "");
150
88
  }
151
- function runResultTitle(row, status) {
152
- const taskTitle = row.task_title ?? "Task";
153
- if (status === "needs_human") {
154
- return `Needs input: ${taskTitle}`;
89
+ function parseTaskContract(value) {
90
+ if (value === null) {
91
+ return { goal: "", scope: [] };
155
92
  }
156
- if (status === "failed") {
157
- return `Run failed: ${taskTitle}`;
93
+ const parsed = parseJsonObject(value);
94
+ return {
95
+ goal: stringValue(parsed, "goal") ?? "",
96
+ scope: stringArrayValue(parsed, "scope") ?? [],
97
+ };
98
+ }
99
+ function attachProviderUsageToItem(db, item, anchors, options = {}) {
100
+ const tokenUsage = readProviderUsageBreakdownForAnchors(db, anchors, options);
101
+ if (tokenUsage.event_count <= 0) {
102
+ return;
158
103
  }
159
- return `Run completed: ${taskTitle}`;
104
+ item.token_usage = tokenUsage;
105
+ item.usage_sources = readProviderUsageSourceSummariesForAnchors(db, anchors, options);
106
+ item.usage_models = readProviderUsageModelSummariesForAnchors(db, anchors, options);
160
107
  }
161
- function stringValue(record, key) {
162
- const value = record[key];
163
- return typeof value === "string" && value.trim() !== "" ? value : undefined;
108
+ function worklistTaskLink(taskId) {
109
+ return {
110
+ kind: "worklist_task",
111
+ label: "Open Worklist Task",
112
+ task_id: taskId,
113
+ };
164
114
  }
165
- function promotionCommit(result) {
166
- const promotion = result.promotion;
167
- if (typeof promotion !== "object" || promotion === null || Array.isArray(promotion)) {
168
- return undefined;
169
- }
170
- return stringValue(promotion, "commit_oid");
115
+ function chatMessageLink(messageId) {
116
+ return {
117
+ kind: "chat_message",
118
+ label: "Open in Chat",
119
+ message_id: messageId,
120
+ };
171
121
  }
172
- function runResultMeta(result) {
173
- const meta = [];
174
- const checks = stringValue(result, "checks");
175
- const docs = stringValue(result, "docs");
176
- const promotion = result.promotion;
177
- if (checks !== undefined) {
178
- meta.push(`checks: ${checks}`);
122
+ function authorLabel(row) {
123
+ if (row.author_display_name !== null && row.author_display_name.trim() !== "") {
124
+ return row.author_display_name;
179
125
  }
180
- if (docs !== undefined) {
181
- meta.push(`docs: ${docs}`);
126
+ if (row.author_kind === "agent") {
127
+ return "Tutti";
182
128
  }
183
- if (typeof promotion === "object" && promotion !== null && !Array.isArray(promotion)) {
184
- const promotionStatus = stringValue(promotion, "status");
185
- if (promotionStatus !== undefined) {
186
- meta.push(`promotion: ${promotionStatus}`);
187
- }
129
+ if (row.author_kind === "system") {
130
+ return "System";
131
+ }
132
+ return "Member";
133
+ }
134
+ function truncateSnippetBody(value) {
135
+ const trimmed = value.trim();
136
+ if (trimmed.length <= TRACE_SOURCE_SNIPPET_BODY_LIMIT) {
137
+ return trimmed;
188
138
  }
189
- return meta;
139
+ return `${trimmed.slice(0, TRACE_SOURCE_SNIPPET_BODY_LIMIT - 3)}...`;
140
+ }
141
+ function readReceiptSourceCount(db, workflowRef) {
142
+ const row = db
143
+ .prepare(`
144
+ SELECT COUNT(*) AS source_count
145
+ FROM scratchpad_receipt_sources
146
+ WHERE workflow_ref = ?
147
+ `)
148
+ .get(workflowRef);
149
+ return row?.source_count ?? 0;
150
+ }
151
+ function readReceiptSourceSnippets(db, workflowRef) {
152
+ const rows = db
153
+ .prepare(`
154
+ SELECT
155
+ messages.id AS message_id,
156
+ messages.author_kind,
157
+ messages.author_display_name,
158
+ messages.body,
159
+ messages.created_at
160
+ FROM scratchpad_receipt_sources
161
+ JOIN messages ON messages.id = scratchpad_receipt_sources.message_id
162
+ WHERE scratchpad_receipt_sources.workflow_ref = ?
163
+ ORDER BY scratchpad_receipt_sources.created_at ASC, scratchpad_receipt_sources.message_id ASC
164
+ LIMIT ${TRACE_SOURCE_SNIPPET_LIMIT}
165
+ `)
166
+ .all(workflowRef);
167
+ return rows.map((source) => ({
168
+ message_id: source.message_id,
169
+ author: authorLabel(source),
170
+ body: truncateSnippetBody(source.body),
171
+ created_at: source.created_at,
172
+ }));
173
+ }
174
+ function clarificationChatLink(messageId) {
175
+ return {
176
+ kind: "chat_message",
177
+ label: "Open clarification in Chat",
178
+ message_id: messageId,
179
+ };
180
+ }
181
+ function readClarificationAnswerSnippets(db, submittedRefsJson) {
182
+ const submittedRefs = parseJsonStringArray(submittedRefsJson);
183
+ if (submittedRefs.length === 0) {
184
+ return { answer_count: 0, answers: [] };
185
+ }
186
+ const readMessage = db.prepare(`
187
+ SELECT
188
+ id AS message_id,
189
+ author_kind,
190
+ author_display_name,
191
+ body,
192
+ created_at
193
+ FROM messages
194
+ WHERE id = ?
195
+ AND message_kind = 'user_text'
196
+ AND author_kind = 'human'
197
+ LIMIT 1
198
+ `);
199
+ const answers = submittedRefs.flatMap((messageId) => {
200
+ const row = readMessage.get(messageId);
201
+ return row === undefined ? [] : [row];
202
+ });
203
+ const visibleAnswers = answers.slice(0, TRACE_SOURCE_SNIPPET_LIMIT).map((answer) => ({
204
+ message_id: answer.message_id,
205
+ author: authorLabel(answer),
206
+ body: truncateSnippetBody(answer.body),
207
+ created_at: answer.created_at,
208
+ }));
209
+ return {
210
+ answer_count: answers.length,
211
+ answers: visibleAnswers,
212
+ };
213
+ }
214
+ function clarificationTraceFromRow(db, row) {
215
+ const payload = parseJsonObject(row.request_payload_json);
216
+ const answers = readClarificationAnswerSnippets(db, row.submitted_message_refs_json);
217
+ const visibleAnswers = answers.answers;
218
+ const title = stringValue(payload, "title");
219
+ const summary = stringValue(payload, "summary");
220
+ const request = stringValue(payload, "request");
221
+ const clarification = {
222
+ ...(title === undefined ? {} : { title }),
223
+ ...(summary === undefined ? {} : { summary }),
224
+ ...(request === undefined ? {} : { request }),
225
+ status: row.round_status === "sealed" ? "resolved" : "active",
226
+ answer_count: answers.answer_count,
227
+ ...(visibleAnswers.length === 0 ? {} : { answers: visibleAnswers }),
228
+ };
229
+ const firstAnswer = visibleAnswers[0]?.body;
230
+ return {
231
+ clarification,
232
+ links: [clarificationChatLink(row.card_message_id)],
233
+ ...(firstAnswer === undefined
234
+ ? summary === undefined
235
+ ? {}
236
+ : { decision_summary: summary }
237
+ : { decision_summary: firstAnswer }),
238
+ };
239
+ }
240
+ function readLatestTaskCompileClarificationTrace(db, workflowRef) {
241
+ const row = db
242
+ .prepare(`
243
+ SELECT
244
+ clarification_threads.id AS thread_id,
245
+ clarification_threads.status AS thread_status,
246
+ clarification_rounds.id AS round_id,
247
+ clarification_rounds.status AS round_status,
248
+ clarification_rounds.request_payload_json,
249
+ clarification_rounds.card_message_id,
250
+ clarification_rounds.submitted_message_refs_json,
251
+ clarification_rounds.submitted_at,
252
+ clarification_rounds.updated_at
253
+ FROM clarification_rounds
254
+ JOIN clarification_threads
255
+ ON clarification_threads.id = clarification_rounds.clarification_thread_id
256
+ WHERE clarification_threads.owner_kind = 'task_compile'
257
+ AND clarification_threads.owner_workflow_ref = ?
258
+ AND clarification_rounds.status = 'sealed'
259
+ AND clarification_rounds.submitted_at IS NOT NULL
260
+ ORDER BY clarification_rounds.round_index DESC, clarification_rounds.submitted_at DESC
261
+ LIMIT 1
262
+ `)
263
+ .get(workflowRef);
264
+ return row === undefined ? undefined : clarificationTraceFromRow(db, row);
190
265
  }
191
- function readTaskTimelineItems(db, consumedProcedureUsageAnchors) {
266
+ function readLatestClarificationTraceByThread(db, threadId) {
267
+ const row = db
268
+ .prepare(`
269
+ SELECT
270
+ clarification_threads.id AS thread_id,
271
+ clarification_threads.status AS thread_status,
272
+ clarification_rounds.id AS round_id,
273
+ clarification_rounds.status AS round_status,
274
+ clarification_rounds.request_payload_json,
275
+ clarification_rounds.card_message_id,
276
+ clarification_rounds.submitted_message_refs_json,
277
+ clarification_rounds.submitted_at,
278
+ clarification_rounds.updated_at
279
+ FROM clarification_threads
280
+ JOIN clarification_rounds
281
+ ON clarification_rounds.clarification_thread_id = clarification_threads.id
282
+ WHERE clarification_threads.id = ?
283
+ ORDER BY
284
+ CASE
285
+ WHEN clarification_rounds.id = clarification_threads.current_round_id THEN 0
286
+ ELSE 1
287
+ END,
288
+ clarification_rounds.round_index DESC,
289
+ clarification_rounds.updated_at DESC
290
+ LIMIT 1
291
+ `)
292
+ .get(threadId);
293
+ return row === undefined ? undefined : clarificationTraceFromRow(db, row);
294
+ }
295
+ function readLatestResolvedTaskClarificationTrace(db, taskId, throughCreatedAt) {
296
+ const row = db
297
+ .prepare(`
298
+ SELECT
299
+ clarification_threads.id AS thread_id,
300
+ clarification_threads.status AS thread_status,
301
+ clarification_rounds.id AS round_id,
302
+ clarification_rounds.status AS round_status,
303
+ clarification_rounds.request_payload_json,
304
+ clarification_rounds.card_message_id,
305
+ clarification_rounds.submitted_message_refs_json,
306
+ clarification_rounds.submitted_at,
307
+ clarification_rounds.updated_at
308
+ FROM clarification_rounds
309
+ JOIN clarification_threads
310
+ ON clarification_threads.id = clarification_rounds.clarification_thread_id
311
+ WHERE clarification_threads.owner_kind = 'task'
312
+ AND clarification_threads.owner_task_id = ?
313
+ AND clarification_threads.status = 'closed'
314
+ AND clarification_rounds.status = 'sealed'
315
+ AND clarification_rounds.submitted_at IS NOT NULL
316
+ AND clarification_rounds.submitted_at <= ?
317
+ ORDER BY clarification_rounds.submitted_at DESC, clarification_rounds.round_index DESC
318
+ LIMIT 1
319
+ `)
320
+ .get(taskId, throughCreatedAt);
321
+ return row === undefined ? undefined : clarificationTraceFromRow(db, row);
322
+ }
323
+ function taskAcceptedTraceCard(db, row) {
324
+ const consensus = parseJsonStringArray(row.receipt_current_consensus_json);
325
+ const taskChanges = parseJsonStringArray(row.receipt_task_changes_json);
326
+ const contract = parseTaskContract(row.contract_json);
327
+ const sourceCount = readReceiptSourceCount(db, row.created_from_workflow_ref);
328
+ const sourceSnippets = sourceCount === 0 ? [] : readReceiptSourceSnippets(db, row.created_from_workflow_ref);
329
+ const clarification = readLatestTaskCompileClarificationTrace(db, row.created_from_workflow_ref);
330
+ const decisions = [
331
+ ...consensus.slice(0, TRACE_CONSENSUS_LIMIT).map((summary) => ({
332
+ title: "Consensus",
333
+ summary,
334
+ source: "scratchpad",
335
+ })),
336
+ ...taskChanges.slice(0, TRACE_TASK_CHANGE_LIMIT).map((summary) => ({
337
+ title: "Task change",
338
+ summary,
339
+ source: "scratchpad",
340
+ })),
341
+ ...(clarification?.decision_summary === undefined
342
+ ? []
343
+ : [
344
+ {
345
+ title: "Clarified decision",
346
+ summary: clarification.decision_summary,
347
+ source: "clarification",
348
+ },
349
+ ]),
350
+ ];
351
+ const scope = contract.scope.slice(0, TRACE_CONTRACT_SCOPE_LIMIT);
352
+ const hasReceipt = row.receipt_topic !== null ||
353
+ row.receipt_background_summary !== null ||
354
+ consensus.length > 0 ||
355
+ taskChanges.length > 0;
356
+ return {
357
+ tone: "success",
358
+ summary: row.summary,
359
+ origin: {
360
+ title: row.receipt_topic !== null && row.receipt_topic.trim() !== ""
361
+ ? row.receipt_topic
362
+ : row.title,
363
+ ...(row.receipt_background_summary === null
364
+ ? {}
365
+ : { summary: row.receipt_background_summary }),
366
+ ...(sourceCount === 0
367
+ ? {
368
+ unavailable_reason: hasReceipt
369
+ ? "Source message refs unavailable; showing Scratchpad summary."
370
+ : "Scratchpad receipt unavailable.",
371
+ }
372
+ : {
373
+ source_count: sourceCount,
374
+ snippets: sourceSnippets,
375
+ }),
376
+ },
377
+ ...(decisions.length === 0 ? {} : { decisions }),
378
+ ...(clarification === undefined ? {} : { clarification: clarification.clarification }),
379
+ contract: {
380
+ goal: contract.goal,
381
+ scope,
382
+ ...(contract.scope.length > scope.length ? { truncated: true } : {}),
383
+ },
384
+ links: [
385
+ worklistTaskLink(row.id),
386
+ ...sourceSnippets.map((snippet, index) => ({
387
+ kind: "chat_message",
388
+ label: index === 0 ? "Open source in Chat" : `Open source ${index + 1} in Chat`,
389
+ message_id: snippet.message_id,
390
+ })),
391
+ ...(clarification?.links ?? []),
392
+ ],
393
+ };
394
+ }
395
+ function readTaskAcceptedTimelineItems(db) {
192
396
  if (db === undefined) {
193
397
  return [];
194
398
  }
@@ -198,39 +402,37 @@ function readTaskTimelineItems(db, consumedProcedureUsageAnchors) {
198
402
  tasks.id,
199
403
  tasks.title,
200
404
  tasks.summary,
201
- tasks.status,
202
405
  tasks.created_from_workflow_ref,
203
406
  tasks.created_at,
204
- task_modules.name AS module_name
407
+ task_modules.name AS module_name,
408
+ task_details.contract_json,
409
+ scratchpad_receipts.topic AS receipt_topic,
410
+ scratchpad_receipts.background_summary AS receipt_background_summary,
411
+ scratchpad_receipts.current_consensus_json AS receipt_current_consensus_json,
412
+ scratchpad_receipts.task_changes_json AS receipt_task_changes_json
205
413
  FROM tasks
206
414
  LEFT JOIN task_modules ON task_modules.id = tasks.module_id
415
+ LEFT JOIN task_details ON task_details.task_id = tasks.id
416
+ LEFT JOIN scratchpad_receipts
417
+ ON scratchpad_receipts.workflow_ref = tasks.created_from_workflow_ref
207
418
  ORDER BY tasks.created_at DESC, tasks.id DESC
208
- LIMIT ${TASK_TIMELINE_LIMIT}
419
+ LIMIT ${TASK_ACCEPTED_TIMELINE_LIMIT}
209
420
  `)
210
421
  .all();
211
422
  return rows.map((row) => {
212
- const meta = [`status: ${row.status}`];
213
- if (row.module_name !== null && row.module_name.trim() !== "") {
214
- meta.unshift(`module: ${row.module_name}`);
215
- }
216
423
  const item = {
217
- id: `task:${row.id}`,
218
- kind: "procedure",
424
+ id: `task_accepted:${row.id}`,
425
+ kind: "task_accepted",
219
426
  occurred_at: row.created_at,
220
- title: `Task added: ${row.title}`,
427
+ title: `Task accepted: ${row.title}`,
221
428
  summary: row.summary,
222
429
  task_id: row.id,
223
430
  task_title: row.title,
224
431
  workflow_ref: row.created_from_workflow_ref,
225
432
  status: "completed",
226
- meta,
433
+ trace_card: taskAcceptedTraceCard(db, row),
227
434
  };
228
- const anchors = { workflow_ref: row.created_from_workflow_ref };
229
- if (attachProviderUsageToItem(db, item, anchors, {
230
- sources: TASK_ADDED_USAGE_SOURCES,
231
- })) {
232
- pushConsumedProcedureUsageAnchors(consumedProcedureUsageAnchors, TASK_ADDED_USAGE_SOURCES, anchors);
233
- }
435
+ attachProviderUsageToItem(db, item, { workflow_ref: row.created_from_workflow_ref }, { sources: TASK_ACCEPTED_USAGE_SOURCES });
234
436
  return item;
235
437
  });
236
438
  }
@@ -244,414 +446,404 @@ function readRunResultTimelineRows(db) {
244
446
  run_results.result_kind,
245
447
  run_results.result_json,
246
448
  run_results.created_at,
247
- tasks.title AS task_title
449
+ tasks.title AS task_title,
450
+ task_details.contract_json
248
451
  FROM run_results
249
452
  LEFT JOIN tasks ON tasks.id = run_results.task_id
453
+ LEFT JOIN task_details ON task_details.task_id = run_results.task_id
250
454
  ORDER BY run_results.created_at DESC, run_results.id DESC
251
455
  LIMIT ${RUN_RESULT_TIMELINE_LIMIT}
252
456
  `)
253
457
  .all();
254
458
  }
255
- function readRunTimelineItems(db, rows) {
256
- if (db === undefined) {
257
- return [];
459
+ function runUpdateState(row, result) {
460
+ if (row.result_kind === "needs_human") {
461
+ return "needs_input";
258
462
  }
259
- return rows.map((row) => {
260
- const result = parseJsonObject(row.result_json);
261
- const status = runResultStatus(row, result);
262
- const commitOid = promotionCommit(result);
263
- const item = {
264
- id: `run:${row.id}`,
265
- kind: "run",
266
- occurred_at: row.created_at,
267
- title: runResultTitle(row, status),
268
- task_id: row.task_id,
269
- activity_ref: row.activity_ref,
270
- status,
271
- meta: runResultMeta(result),
272
- };
273
- const summary = stringValue(result, "summary");
274
- if (summary !== undefined) {
275
- item.summary = summary;
463
+ if (row.result_kind === "failed" || result.outcome === "failed") {
464
+ return "failed";
465
+ }
466
+ return "completed";
467
+ }
468
+ function timelineStatusForUpdate(updateState) {
469
+ if (updateState === "needs_input") {
470
+ return "needs_human";
471
+ }
472
+ if (updateState === "failed") {
473
+ return "failed";
474
+ }
475
+ return "completed";
476
+ }
477
+ function runUpdateTitle(row, updateState) {
478
+ const taskTitle = row.task_title ?? "Task";
479
+ if (updateState === "needs_input") {
480
+ return `Task needs input: ${taskTitle}`;
481
+ }
482
+ if (updateState === "failed") {
483
+ return `Task failed: ${taskTitle}`;
484
+ }
485
+ return `Task completed: ${taskTitle}`;
486
+ }
487
+ function traceToneForUpdate(updateState) {
488
+ if (updateState === "needs_input") {
489
+ return "warning";
490
+ }
491
+ if (updateState === "failed") {
492
+ return "error";
493
+ }
494
+ return "success";
495
+ }
496
+ function gitChangeStatus(status) {
497
+ if (status.startsWith("R")) {
498
+ return "renamed";
499
+ }
500
+ switch (status) {
501
+ case "A":
502
+ return "added";
503
+ case "M":
504
+ return "modified";
505
+ case "D":
506
+ return "deleted";
507
+ default:
508
+ return "unknown";
509
+ }
510
+ }
511
+ function isSafeRepoRelativePath(path) {
512
+ return (path.trim() !== "" &&
513
+ !path.startsWith("/") &&
514
+ !path.includes("\0") &&
515
+ !path.split("/").includes(".."));
516
+ }
517
+ function promotedCommitOid(result) {
518
+ const promotion = result.promotion;
519
+ if (typeof promotion !== "object" || promotion === null || Array.isArray(promotion)) {
520
+ return undefined;
521
+ }
522
+ const promotionRecord = promotion;
523
+ if (promotionRecord.status !== "promoted") {
524
+ return undefined;
525
+ }
526
+ return stringValue(promotionRecord, "commit_oid");
527
+ }
528
+ function readPromotedCommitChanges(workspaceRoot, commitOid) {
529
+ if (workspaceRoot === undefined || commitOid === undefined) {
530
+ return undefined;
531
+ }
532
+ const result = runGitRaw(["show", "--name-status", "--find-renames", "-z", "--format=", commitOid, "--"], workspaceRoot, 1024 * 1024);
533
+ if (result.status !== 0) {
534
+ return undefined;
535
+ }
536
+ const fields = result.stdout.toString("utf8").split("\0").filter((field) => field !== "");
537
+ const changes = [];
538
+ for (let index = 0; index < fields.length;) {
539
+ const status = fields[index];
540
+ if (status === undefined) {
541
+ break;
276
542
  }
277
- if (row.task_title !== null && row.task_title.trim() !== "") {
278
- item.task_title = row.task_title;
543
+ if (status.startsWith("R")) {
544
+ const previousPath = fields[index + 1];
545
+ const nextPath = fields[index + 2];
546
+ index += 3;
547
+ if (previousPath !== undefined &&
548
+ nextPath !== undefined &&
549
+ isSafeRepoRelativePath(previousPath) &&
550
+ isSafeRepoRelativePath(nextPath)) {
551
+ changes.push({
552
+ status: "renamed",
553
+ path: nextPath,
554
+ previous_path: previousPath,
555
+ });
556
+ }
557
+ continue;
279
558
  }
280
- if (commitOid !== undefined) {
281
- item.commit_oid = commitOid;
282
- item.commit_short_oid = commitOid.slice(0, 12);
559
+ const path = fields[index + 1];
560
+ index += 2;
561
+ if (path === undefined || !isSafeRepoRelativePath(path)) {
562
+ continue;
283
563
  }
284
- const tokenUsage = readProviderUsageBreakdownForAnchors(db, {
285
- activity_ref: row.activity_ref,
286
- run_result_id: row.id,
287
- }, {
288
- sources: RUN_USAGE_SOURCES,
564
+ changes.push({
565
+ status: gitChangeStatus(status),
566
+ path,
289
567
  });
290
- if (tokenUsage.event_count > 0) {
291
- item.token_usage = tokenUsage;
292
- item.usage_sources = readProviderUsageSourceSummariesForAnchors(db, {
293
- activity_ref: row.activity_ref,
294
- run_result_id: row.id,
295
- }, {
296
- sources: RUN_USAGE_SOURCES,
297
- });
298
- item.usage_models = readProviderUsageModelSummariesForAnchors(db, {
299
- activity_ref: row.activity_ref,
300
- run_result_id: row.id,
301
- }, {
302
- sources: RUN_USAGE_SOURCES,
303
- });
304
- }
305
- return item;
306
- });
568
+ }
569
+ return changes;
307
570
  }
308
- function checksStatus(value) {
309
- if (value === "passed") {
310
- return "completed";
571
+ function changesTrace(result, workspaceRoot) {
572
+ const changedPaths = stringArrayValue(result, "changed_paths");
573
+ const commitChanges = readPromotedCommitChanges(workspaceRoot, promotedCommitOid(result));
574
+ const changeItems = commitChanges ??
575
+ changedPaths?.filter(isSafeRepoRelativePath).map((path) => ({
576
+ status: "unknown",
577
+ path,
578
+ }));
579
+ if (changeItems === undefined) {
580
+ return undefined;
311
581
  }
312
- if (value === "failed") {
313
- return "failed";
582
+ const visibleChanges = changeItems.slice(0, TRACE_CHANGED_PATH_LIMIT);
583
+ return {
584
+ items: visibleChanges,
585
+ total_count: changeItems.length,
586
+ truncated: changeItems.length > visibleChanges.length,
587
+ };
588
+ }
589
+ function changeLinks(changes) {
590
+ if (changes === undefined) {
591
+ return [];
314
592
  }
315
- if (value === "skipped") {
316
- return "skipped";
593
+ return changes.items
594
+ .filter((change) => change.status !== "deleted")
595
+ .slice(0, TRACE_CHANGE_LINK_LIMIT)
596
+ .map((change, index) => ({
597
+ kind: "repo_path",
598
+ label: index === 0 ? "Open changed file" : `Open changed file ${index + 1}`,
599
+ path: change.path,
600
+ }));
601
+ }
602
+ function verificationTrace(result) {
603
+ const checks = result.checks;
604
+ if (checks !== "passed" && checks !== "failed" && checks !== "skipped") {
605
+ return undefined;
317
606
  }
318
- return undefined;
607
+ const skipReasonCode = stringValue(result, "skip_reason_code");
608
+ const skipReasonSummary = stringValue(result, "skip_reason_summary");
609
+ return {
610
+ checks,
611
+ ...(skipReasonCode === undefined ? {} : { skip_reason_code: skipReasonCode }),
612
+ ...(skipReasonSummary === undefined ? {} : { skip_reason_summary: skipReasonSummary }),
613
+ };
319
614
  }
320
- function readChecksTimelineItems(rows) {
321
- if (rows.length === 0) {
615
+ function promotionTrace(result) {
616
+ const promotion = result.promotion;
617
+ if (typeof promotion !== "object" || promotion === null || Array.isArray(promotion)) {
618
+ return undefined;
619
+ }
620
+ const promotionRecord = promotion;
621
+ const status = promotionRecord.status;
622
+ if (status !== "promoted" && status !== "not_promoted" && status !== "not_attempted") {
623
+ return undefined;
624
+ }
625
+ const commitOid = stringValue(promotionRecord, "commit_oid");
626
+ const summary = stringValue(promotionRecord, "summary");
627
+ const reasonCode = stringValue(promotionRecord, "reason_code");
628
+ return {
629
+ status,
630
+ ...(commitOid === undefined
631
+ ? {}
632
+ : { commit_oid: commitOid, commit_short_oid: commitOid.slice(0, 12) }),
633
+ ...(summary === undefined ? {} : { summary }),
634
+ ...(reasonCode === undefined ? {} : { reason_code: reasonCode }),
635
+ };
636
+ }
637
+ function runUpdateTraceCard(db, row, result, updateState, workspaceRoot) {
638
+ const contract = parseTaskContract(row.contract_json);
639
+ const summary = stringValue(result, "summary") ?? "";
640
+ const errorCode = stringValue(result, "error_code");
641
+ const changes = changesTrace(result, workspaceRoot);
642
+ const verification = verificationTrace(result);
643
+ const promotion = promotionTrace(result);
644
+ const clarificationThreadRef = stringValue(result, "clarification_thread_ref");
645
+ const clarification = updateState === "needs_input" && clarificationThreadRef !== undefined
646
+ ? readLatestClarificationTraceByThread(db, clarificationThreadRef)
647
+ : updateState === "completed" || updateState === "failed"
648
+ ? readLatestResolvedTaskClarificationTrace(db, row.task_id, row.created_at)
649
+ : undefined;
650
+ return {
651
+ tone: traceToneForUpdate(updateState),
652
+ summary,
653
+ result: {
654
+ summary,
655
+ ...(errorCode === undefined ? {} : { error_code: errorCode }),
656
+ },
657
+ contract: {
658
+ goal: contract.goal,
659
+ scope: contract.scope.slice(0, TRACE_CONTRACT_SCOPE_LIMIT),
660
+ ...(contract.scope.length > TRACE_CONTRACT_SCOPE_LIMIT ? { truncated: true } : {}),
661
+ },
662
+ ...(changes === undefined ? {} : { changes }),
663
+ ...(verification === undefined ? {} : { verification }),
664
+ ...(promotion === undefined ? {} : { promotion }),
665
+ ...(clarification === undefined ? {} : { clarification: clarification.clarification }),
666
+ links: [
667
+ worklistTaskLink(row.task_id),
668
+ ...(clarification?.links ?? []),
669
+ ...changeLinks(changes),
670
+ ],
671
+ };
672
+ }
673
+ function readTaskUpdateTimelineItems(db, rows, workspaceRoot) {
674
+ if (db === undefined) {
322
675
  return [];
323
676
  }
324
- return rows.flatMap((row) => {
677
+ return rows.map((row) => {
325
678
  const result = parseJsonObject(row.result_json);
326
- const checks = stringValue(result, "checks");
327
- const status = checksStatus(checks);
328
- if (checks === undefined || status === undefined) {
329
- return [];
330
- }
331
- const taskTitle = row.task_title ?? "Task";
679
+ const updateState = runUpdateState(row, result);
680
+ const status = timelineStatusForUpdate(updateState);
332
681
  const item = {
333
- id: `checks:${row.id}`,
334
- kind: "checks",
682
+ id: `task_update:${row.id}`,
683
+ kind: "task_update",
335
684
  occurred_at: row.created_at,
336
- title: `Checks ${checks}: ${taskTitle}`,
685
+ title: runUpdateTitle(row, updateState),
337
686
  task_id: row.task_id,
338
687
  activity_ref: row.activity_ref,
688
+ run_result_id: row.id,
339
689
  status,
340
- meta: [`run result: ${row.id}`],
690
+ update_state: updateState,
691
+ trace_card: runUpdateTraceCard(db, row, result, updateState, workspaceRoot),
341
692
  };
342
693
  if (row.task_title !== null && row.task_title.trim() !== "") {
343
694
  item.task_title = row.task_title;
344
695
  }
345
- const summary = stringValue(result, "skip_reason_summary") ??
346
- (checks === "skipped" ? stringValue(result, "skip_reason_code") : undefined);
696
+ const summary = stringValue(result, "summary");
347
697
  if (summary !== undefined) {
348
698
  item.summary = summary;
349
699
  }
350
- return [item];
700
+ attachProviderUsageToItem(db, item, { activity_ref: row.activity_ref, run_result_id: row.id }, { sources: RUN_USAGE_SOURCES });
701
+ return item;
351
702
  });
352
703
  }
353
- function usageBreakdownFromTimelineRow(row) {
354
- return {
355
- input_tokens: row.input_tokens ?? 0,
356
- cached_input_tokens: row.cached_input_tokens ?? 0,
357
- output_tokens: row.output_tokens ?? 0,
358
- reasoning_output_tokens: row.reasoning_output_tokens ?? 0,
359
- total_tokens: row.total_tokens ?? 0,
360
- event_count: row.event_count ?? 0,
361
- };
362
- }
363
- function readProcedureUsageTimelineItems(db, consumedProcedureUsageAnchors) {
364
- if (db === undefined) {
365
- return [];
704
+ function parseCheckpointFeedback(refsJson) {
705
+ const refs = parseJsonObject(refsJson);
706
+ const feedback = refs.worklist_feedback;
707
+ if (typeof feedback !== "object" || feedback === null || Array.isArray(feedback)) {
708
+ return null;
366
709
  }
367
- const rows = db
368
- .prepare(`
369
- SELECT
370
- source,
371
- activity_ref,
372
- workflow_ref,
373
- task_id,
374
- MAX(recorded_at) AS occurred_at,
375
- COALESCE(SUM(input_tokens), 0) AS input_tokens,
376
- COALESCE(SUM(cached_input_tokens), 0) AS cached_input_tokens,
377
- COALESCE(SUM(output_tokens), 0) AS output_tokens,
378
- COALESCE(SUM(reasoning_output_tokens), 0) AS reasoning_output_tokens,
379
- COALESCE(SUM(total_tokens), 0) AS total_tokens,
380
- COUNT(*) AS event_count
381
- FROM provider_usage_events
382
- WHERE
383
- (activity_ref IS NOT NULL OR workflow_ref IS NOT NULL) AND
384
- source NOT IN ('workspace_write_run', 'follow_up_run', 'pipeline_self_correction')
385
- GROUP BY source, activity_ref, workflow_ref, task_id
386
- ORDER BY occurred_at DESC
387
- LIMIT 50
388
- `)
389
- .all();
390
- return rows
391
- .filter((row) => !isProcedureUsageConsumed(row, consumedProcedureUsageAnchors))
392
- .map((row) => {
393
- const source = providerUsageSourceMetadata(row.source);
394
- const anchor = row.activity_ref ?? row.workflow_ref ?? row.source;
395
- const item = {
396
- id: `procedure:${anchor}:${row.source}`,
397
- kind: "procedure",
398
- occurred_at: row.occurred_at,
399
- title: source.label,
400
- status: "completed",
401
- token_usage: usageBreakdownFromTimelineRow(row),
402
- usage_sources: [
403
- {
404
- source: row.source,
405
- ...source,
406
- ...usageBreakdownFromTimelineRow(row),
407
- },
408
- ],
409
- meta: [`category: ${source.category}`],
410
- };
411
- if (row.activity_ref !== null) {
412
- item.activity_ref = row.activity_ref;
413
- }
414
- if (row.workflow_ref !== null) {
415
- item.workflow_ref = row.workflow_ref;
710
+ const record = feedback;
711
+ const status = record.status;
712
+ const counters = record.counters;
713
+ const rawItems = record.items;
714
+ if ((status !== "completed" && status !== "failed") ||
715
+ typeof counters !== "object" ||
716
+ counters === null ||
717
+ Array.isArray(counters) ||
718
+ !Array.isArray(rawItems)) {
719
+ return null;
720
+ }
721
+ const counterRecord = counters;
722
+ const parsedItems = rawItems.flatMap((item) => {
723
+ if (typeof item !== "object" || item === null || Array.isArray(item)) {
724
+ return [];
416
725
  }
417
- if (row.task_id !== null) {
418
- item.task_id = row.task_id;
726
+ const itemRecord = item;
727
+ if (typeof itemRecord.task_id !== "string" ||
728
+ typeof itemRecord.title !== "string" ||
729
+ (itemRecord.status !== "done" && itemRecord.status !== "failed")) {
730
+ return [];
419
731
  }
420
- item.usage_models = readProviderUsageModelSummariesForAnchors(db, {
421
- ...(row.activity_ref === null ? {} : { activity_ref: row.activity_ref }),
422
- ...(row.workflow_ref === null ? {} : { workflow_ref: row.workflow_ref }),
423
- ...(row.task_id === null ? {} : { task_id: row.task_id }),
424
- }, { sources: [row.source] });
425
- return item;
732
+ return [
733
+ {
734
+ task_id: itemRecord.task_id,
735
+ title: itemRecord.title,
736
+ status: itemRecord.status,
737
+ summary: typeof itemRecord.summary === "string" ? itemRecord.summary : "",
738
+ },
739
+ ];
426
740
  });
741
+ const rawGeneratedFiles = record.generated_files;
742
+ const generatedFiles = Array.isArray(rawGeneratedFiles)
743
+ ? rawGeneratedFiles.flatMap((file) => {
744
+ if (typeof file !== "object" || file === null || Array.isArray(file)) {
745
+ return [];
746
+ }
747
+ const fileRecord = file;
748
+ if (typeof fileRecord.path !== "string" || typeof fileRecord.name !== "string") {
749
+ return [];
750
+ }
751
+ return [{ path: fileRecord.path, name: fileRecord.name }];
752
+ })
753
+ : undefined;
754
+ const finalNote = stringValue(record, "final_note");
755
+ return {
756
+ status,
757
+ title: stringValue(record, "title") ?? "Worklist checkpoint",
758
+ counters: {
759
+ completed: typeof counterRecord.completed === "number" ? counterRecord.completed : 0,
760
+ failed: typeof counterRecord.failed === "number" ? counterRecord.failed : 0,
761
+ remaining: typeof counterRecord.remaining === "number" ? counterRecord.remaining : 0,
762
+ },
763
+ items: parsedItems,
764
+ ...(finalNote === undefined ? {} : { final_note: finalNote }),
765
+ ...(generatedFiles === undefined || generatedFiles.length === 0
766
+ ? {}
767
+ : { generated_files: generatedFiles }),
768
+ ...(typeof record.primary_task_id === "string"
769
+ ? { primary_task_id: record.primary_task_id }
770
+ : {}),
771
+ };
427
772
  }
428
- function clarificationOwnerLabel(row) {
429
- if (row.owner_kind === "task") {
430
- return row.task_title ?? "Task";
431
- }
432
- return "Worklist planning";
433
- }
434
- function clarificationSummary(row) {
435
- return stringValue(parseJsonObject(row.request_payload_json), "summary");
436
- }
437
- function clarificationMeta(row, phase) {
438
- const meta = [
439
- row.owner_kind === "task" ? "owner: task" : "owner: task_compile",
440
- `round: ${row.round_index}`,
441
- ];
442
- if (phase === "answered") {
443
- meta.push(`successor: ${row.successor_status}`);
444
- }
445
- return meta;
773
+ function firstNonEmptyLine(value) {
774
+ return value
775
+ .split(/\r?\n/u)
776
+ .map((line) => line.trim())
777
+ .find((line) => line.length > 0);
446
778
  }
447
- function assignClarificationAnchors(item, row) {
448
- if (row.owner_task_id !== null) {
449
- item.task_id = row.owner_task_id;
450
- }
451
- if (row.task_title !== null && row.task_title.trim() !== "") {
452
- item.task_title = row.task_title;
453
- }
454
- if (row.owner_workflow_ref !== null) {
455
- item.workflow_ref = row.owner_workflow_ref;
456
- }
457
- if (row.created_from_activity_ref !== null) {
458
- item.activity_ref = row.created_from_activity_ref;
459
- }
779
+ function checkpointTraceCard(feedback, messageId, body) {
780
+ const visibleItems = feedback.items.slice(0, TRACE_CHECKPOINT_ITEM_LIMIT);
781
+ const visibleGeneratedFiles = feedback.generated_files?.slice(0, TRACE_GENERATED_FILE_LIMIT);
782
+ return {
783
+ tone: feedback.status === "completed" ? "success" : "error",
784
+ summary: feedback.final_note ?? firstNonEmptyLine(body) ?? feedback.title,
785
+ checkpoint: {
786
+ counters: feedback.counters,
787
+ items: visibleItems,
788
+ ...(visibleGeneratedFiles === undefined ? {} : { generated_files: visibleGeneratedFiles }),
789
+ ...(feedback.items.length > visibleItems.length ||
790
+ (feedback.generated_files?.length ?? 0) > (visibleGeneratedFiles?.length ?? 0)
791
+ ? { truncated: true }
792
+ : {}),
793
+ },
794
+ links: [chatMessageLink(messageId)],
795
+ };
460
796
  }
461
- function readClarificationTimelineItems(db) {
797
+ function readCheckpointTimelineItems(db) {
462
798
  if (db === undefined) {
463
799
  return [];
464
800
  }
465
801
  const rows = db
466
802
  .prepare(`
467
- SELECT
468
- clarification_rounds.id,
469
- clarification_rounds.round_index,
470
- clarification_rounds.status,
471
- clarification_rounds.request_payload_json,
472
- clarification_rounds.submitted_at,
473
- clarification_rounds.successor_status,
474
- clarification_rounds.created_at,
475
- clarification_threads.owner_kind,
476
- clarification_threads.owner_workflow_ref,
477
- clarification_threads.owner_task_id,
478
- clarification_threads.created_from_activity_ref,
479
- tasks.title AS task_title
480
- FROM clarification_rounds
481
- JOIN clarification_threads
482
- ON clarification_threads.id = clarification_rounds.clarification_thread_id
483
- LEFT JOIN tasks ON tasks.id = clarification_threads.owner_task_id
484
- ORDER BY clarification_rounds.created_at DESC, clarification_rounds.id DESC
485
- LIMIT ${CLARIFICATION_TIMELINE_LIMIT}
803
+ SELECT id, body, refs_json, created_at
804
+ FROM messages
805
+ WHERE scope_kind = 'main_chat'
806
+ AND refs_json IS NOT NULL
807
+ AND json_type(refs_json, '$.worklist_feedback') = 'object'
808
+ ORDER BY created_at DESC, id DESC
809
+ LIMIT ${CHECKPOINT_TIMELINE_LIMIT}
486
810
  `)
487
811
  .all();
488
812
  return rows.flatMap((row) => {
489
- const ownerLabel = clarificationOwnerLabel(row);
490
- const summary = clarificationSummary(row);
491
- const requested = {
492
- id: `clarification:${row.id}:requested`,
493
- kind: "clarification",
494
- occurred_at: row.created_at,
495
- title: `Clarification requested: ${ownerLabel}`,
496
- status: row.status === "writable" ? "needs_human" : "completed",
497
- meta: clarificationMeta(row, "requested"),
498
- };
499
- if (summary !== undefined) {
500
- requested.summary = summary;
501
- }
502
- assignClarificationAnchors(requested, row);
503
- if (row.submitted_at === null) {
504
- return [requested];
813
+ const feedback = parseCheckpointFeedback(row.refs_json);
814
+ if (feedback === null) {
815
+ return [];
505
816
  }
506
- const answered = {
507
- id: `clarification:${row.id}:answered`,
508
- kind: "clarification",
509
- occurred_at: row.submitted_at,
510
- title: `Clarification answered: ${ownerLabel}`,
511
- summary: "Human input submitted.",
512
- status: "completed",
513
- meta: clarificationMeta(row, "answered"),
514
- };
515
- assignClarificationAnchors(answered, row);
516
- return [requested, answered];
517
- });
518
- }
519
- function readRecoveryTimelineItems(db) {
520
- if (db === undefined) {
521
- return [];
522
- }
523
- const rows = db
524
- .prepare(`
525
- SELECT
526
- id,
527
- affected_activities_json,
528
- summary,
529
- created_at
530
- FROM recovery_summaries
531
- ORDER BY created_at DESC, id DESC
532
- LIMIT ${RECOVERY_TIMELINE_LIMIT}
533
- `)
534
- .all();
535
- return rows.map((row) => {
536
- const affectedActivities = parseJsonStringArray(row.affected_activities_json);
537
817
  const item = {
538
- id: `recovery:${row.id}`,
539
- kind: "recovery",
818
+ id: `checkpoint:${row.id}`,
819
+ kind: "checkpoint",
540
820
  occurred_at: row.created_at,
541
- title: "Workspace recovery completed",
542
- summary: row.summary,
543
- status: "completed",
544
- meta: [`affected activities: ${affectedActivities.length}`],
821
+ title: feedback.title,
822
+ status: feedback.status === "completed" ? "completed" : "failed",
823
+ message_id: row.id,
824
+ trace_card: checkpointTraceCard(feedback, row.id, row.body),
545
825
  };
546
- if (affectedActivities.length === 1) {
547
- item.activity_ref = affectedActivities[0];
826
+ const summary = feedback.final_note ?? firstNonEmptyLine(row.body);
827
+ if (summary !== undefined) {
828
+ item.summary = summary;
548
829
  }
549
- return item;
830
+ if (feedback.primary_task_id !== undefined) {
831
+ item.task_id = feedback.primary_task_id;
832
+ }
833
+ return [item];
550
834
  });
551
835
  }
552
- function usageSourcesForDurableEventItem(item) {
553
- if (item.title === "Docs sync completed" || item.title === "Docs sync failed") {
554
- return CONTEXT_SYNC_USAGE_SOURCES;
555
- }
556
- if (item.title === "Reference summaries refreshed" ||
557
- item.title === "Reference summary refresh failed") {
558
- return REFERENCE_SUMMARY_USAGE_SOURCES;
559
- }
560
- return [];
561
- }
562
- function attachDurableEventUsage(db, item, consumedProcedureUsageAnchors) {
563
- if (db === undefined) {
564
- return item;
565
- }
566
- const sources = usageSourcesForDurableEventItem(item);
567
- if (sources.length === 0) {
568
- return item;
569
- }
570
- const anchors = {};
571
- if (item.activity_ref !== undefined) {
572
- anchors.activity_ref = item.activity_ref;
573
- }
574
- if (item.workflow_ref !== undefined) {
575
- anchors.workflow_ref = item.workflow_ref;
576
- }
577
- if (item.task_id !== undefined) {
578
- anchors.task_id = item.task_id;
579
- }
580
- if (attachProviderUsageToItem(db, item, anchors, { sources })) {
581
- pushConsumedProcedureUsageAnchors(consumedProcedureUsageAnchors, sources, anchors);
582
- }
583
- return item;
584
- }
585
- function shouldDisplayDurableEventItem(item) {
586
- if (item.kind === "promotion") {
587
- return false;
588
- }
589
- return item.title !== "Docs sync completed" && item.title !== "Reference summaries refreshed";
590
- }
591
836
  export function readProjectTimelineProjection(input) {
592
837
  const now = input.now ?? (() => new Date());
593
838
  const runRows = input.db === undefined ? [] : readRunResultTimelineRows(input.db);
594
- const consumedProcedureUsageAnchors = [];
595
- const durableEventItems = input.db === undefined
596
- ? []
597
- : readProjectTimelineEventItems(input.db, { limit: DURABLE_EVENT_TIMELINE_LIMIT })
598
- .filter(shouldDisplayDurableEventItem)
599
- .map((item) => attachDurableEventUsage(input.db, item, consumedProcedureUsageAnchors));
600
839
  const items = [
601
- ...readTaskTimelineItems(input.db, consumedProcedureUsageAnchors),
602
- ...readRunTimelineItems(input.db, runRows),
603
- ...readChecksTimelineItems(runRows),
604
- ...readProcedureUsageTimelineItems(input.db, consumedProcedureUsageAnchors),
605
- ...readClarificationTimelineItems(input.db),
606
- ...readRecoveryTimelineItems(input.db),
607
- ...durableEventItems,
608
- ...readCommitTimelineItems(input.workspaceRoot),
609
- ]
610
- .filter((item) => item.status !== "skipped")
611
- .sort(compareTimelineItemsByTimeDesc);
840
+ ...readTaskAcceptedTimelineItems(input.db),
841
+ ...readTaskUpdateTimelineItems(input.db, runRows, input.workspaceRoot),
842
+ ...readCheckpointTimelineItems(input.db),
843
+ ].sort(compareTimelineItemsByTimeDesc);
612
844
  return {
613
845
  generated_at: now().toISOString(),
614
- usage_overview: input.db === undefined
615
- ? {
616
- total_tokens: 0,
617
- totals: {
618
- input_tokens: 0,
619
- cached_input_tokens: 0,
620
- output_tokens: 0,
621
- reasoning_output_tokens: 0,
622
- total_tokens: 0,
623
- event_count: 0,
624
- },
625
- by_source: [],
626
- by_model: [],
627
- recent_windows: {
628
- today: {
629
- input_tokens: 0,
630
- cached_input_tokens: 0,
631
- output_tokens: 0,
632
- reasoning_output_tokens: 0,
633
- total_tokens: 0,
634
- event_count: 0,
635
- },
636
- last_7_days: {
637
- input_tokens: 0,
638
- cached_input_tokens: 0,
639
- output_tokens: 0,
640
- reasoning_output_tokens: 0,
641
- total_tokens: 0,
642
- event_count: 0,
643
- },
644
- last_30_days: {
645
- input_tokens: 0,
646
- cached_input_tokens: 0,
647
- output_tokens: 0,
648
- reasoning_output_tokens: 0,
649
- total_tokens: 0,
650
- event_count: 0,
651
- },
652
- },
653
- }
654
- : readProviderUsageProjection(input.db, { now }),
846
+ usage_overview: input.db === undefined ? emptyUsageOverview() : readProviderUsageProjection(input.db, { now }),
655
847
  items: items.slice(0, TIMELINE_LIMIT),
656
848
  };
657
849
  }