tarsk 0.4.30 → 0.5.32

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 (91) hide show
  1. package/dist/index.js +768 -289
  2. package/dist/public/assets/{account-view-wqPOnMfW.js → account-view-DjLjq8T6.js} +1 -1
  3. package/dist/public/assets/alert-dialog-BWqb8ENe.js +1 -0
  4. package/dist/public/assets/api-BbUzw5rb.js +1 -0
  5. package/dist/public/assets/{browser-tab-BjqSmZHm.js → browser-tab-BVsBMyo3.js} +1 -1
  6. package/dist/public/assets/chat-input-container-Dh5eJPNP.js +22 -0
  7. package/dist/public/assets/commit-dialog-CfjW1f6P.js +1 -0
  8. package/dist/public/assets/{context-menu-XD6khGRr.js → context-menu-D_gX85jr.js} +1 -1
  9. package/dist/public/assets/conversation-history-view-CKiv8CxD.js +1 -0
  10. package/dist/public/assets/create-repo-dialog-ByvOv7em.js +1 -0
  11. package/dist/public/assets/{dialogs-config-BXDglGdB.js → dialogs-config-CV8HzCCA.js} +3 -3
  12. package/dist/public/assets/diff-view-CuEdnFtI.js +3 -0
  13. package/dist/public/assets/{explorer-tab-view-DbMBihAH.js → explorer-tab-view-CiDUcvE2.js} +2 -2
  14. package/dist/public/assets/explorer-tree-cxPx2HiW.js +1 -0
  15. package/dist/public/assets/{explorer-view-CL_9lhkq.js → explorer-view-BCQNEBOZ.js} +1 -1
  16. package/dist/public/assets/git-history-dialog-DKey5IQa.js +1 -0
  17. package/dist/public/assets/history-view-DDpROJvQ.js +1 -0
  18. package/dist/public/assets/index-BZwvl9X4.js +29 -0
  19. package/dist/public/assets/index-CihB-pLh.css +1 -0
  20. package/dist/public/assets/markdown-renderer-B1teF28W.js +10 -0
  21. package/dist/public/assets/mcp-server-card-CY-VLPhS.js +1 -0
  22. package/dist/public/assets/merged-pr-dialog-BfwdsytN.js +1 -0
  23. package/dist/public/assets/onboarding-BEy4bi0g.js +1 -0
  24. package/dist/public/assets/onboarding-dialog-GjczsXqi.js +1 -0
  25. package/dist/public/assets/page-toolbar-rWocuukR.js +1 -0
  26. package/dist/public/assets/{project-settings-view-CRRJPGNL.js → project-settings-view-wlaXVds9.js} +1 -1
  27. package/dist/public/assets/providers-list-view-BJsp9sta.js +1 -0
  28. package/dist/public/assets/pull-request-dialog-DDxX2W8n.js +1 -0
  29. package/dist/public/assets/pull-with-changes-dialog-BtXHWQ3k.js +1 -0
  30. package/dist/public/assets/push-before-pr-dialog-Cs54Vb76.js +1 -0
  31. package/dist/public/assets/radio-group-D2kNbZM6.js +1 -0
  32. package/dist/public/assets/react-vendor-DV_DP0Qd.js +22 -0
  33. package/dist/public/assets/{resizable-BLhzHL_f.js → resizable-jxPFsLog.js} +1 -1
  34. package/dist/public/assets/{run-stop-button-CIuDOHFg.js → run-stop-button-N1JNjOlD.js} +2 -2
  35. package/dist/public/assets/settings-general-view-BKcgxF5z.js +1 -0
  36. package/dist/public/assets/{settings-instructions-view-B-uTQrNZ.js → settings-instructions-view-DbQ3c3f1.js} +1 -1
  37. package/dist/public/assets/settings-mcp-servers-view-D2_Brac1.js +5 -0
  38. package/dist/public/assets/{settings-models-skeleton-CffyGIvJ.js → settings-models-skeleton-BSV1GmZQ.js} +1 -1
  39. package/dist/public/assets/settings-models-view-BhFuBHAE.js +1 -0
  40. package/dist/public/assets/settings-rules-view-qL__Zcav.js +8 -0
  41. package/dist/public/assets/settings-skills-view-Ccj8tTR9.js +2 -0
  42. package/dist/public/assets/{settings-slash-commands-view-BoT1nCir.js → settings-slash-commands-view-DyXAbQIp.js} +1 -1
  43. package/dist/public/assets/settings-subagents-view-LcaON0Dq.js +2 -0
  44. package/dist/public/assets/{settings-view-CS-RnV6J.js → settings-view-cLrxaSw4.js} +2 -2
  45. package/dist/public/assets/{side-panel-container-B33_hnhc.js → side-panel-container-CCaF3ZtX.js} +2 -2
  46. package/dist/public/assets/skeleton-DNfHG6mv.js +1 -0
  47. package/dist/public/assets/standard-list-item-PWOXi212.js +1 -0
  48. package/dist/public/assets/store-9Wm6Zyve.js +4 -0
  49. package/dist/public/assets/{tab-context-BUUT3x3d.js → tab-context-BdML89mQ.js} +1 -1
  50. package/dist/public/assets/tabs-BVcV8raO.js +1 -0
  51. package/dist/public/assets/{terminal-panel-BCHLDUbD.js → terminal-panel-BG8UFUSX.js} +2 -2
  52. package/dist/public/assets/textarea-CU-XhvgE.js +1 -0
  53. package/dist/public/assets/todos-view-DLC_0wx_.js +1 -0
  54. package/dist/public/assets/use-font-size-Bqn2rvp_.js +1 -0
  55. package/dist/public/assets/{use-toast-C5s-Xnwi.js → use-toast-CGSBJJb0.js} +1 -1
  56. package/dist/public/assets/{utils-CDrGT12s.js → utils-Dt4Hs5eF.js} +1 -1
  57. package/dist/public/assets/{whisper-wasm-C_Ot631g.js → whisper-wasm-BCJQtWoU.js} +2 -2
  58. package/dist/public/busy.svg +3 -0
  59. package/dist/public/index.html +22 -22
  60. package/package.json +1 -1
  61. package/dist/public/assets/alert-dialog-Dj0hDlZC.js +0 -1
  62. package/dist/public/assets/api-RbLAI1bg.js +0 -1
  63. package/dist/public/assets/chat-input-container-BNOl7YtC.js +0 -21
  64. package/dist/public/assets/conversation-history-view-BRSNoBYn.js +0 -1
  65. package/dist/public/assets/diff-view-DiNlWcSj.js +0 -3
  66. package/dist/public/assets/explorer-tree-D0P-HUYS.js +0 -1
  67. package/dist/public/assets/history-view-i7fGKCkc.js +0 -1
  68. package/dist/public/assets/index-BeLCtY82.css +0 -1
  69. package/dist/public/assets/index-CZZ6Jb9i.js +0 -29
  70. package/dist/public/assets/markdown-renderer-CPSKOcqK.js +0 -10
  71. package/dist/public/assets/mcp-server-card-DFATg_Ie.js +0 -1
  72. package/dist/public/assets/onboarding-5CTkrfwe.js +0 -1
  73. package/dist/public/assets/onboarding-dialog-gcZ9aKKp.js +0 -1
  74. package/dist/public/assets/page-toolbar-CbwENcML.js +0 -1
  75. package/dist/public/assets/providers-list-view-D3CIB4ou.js +0 -1
  76. package/dist/public/assets/radio-group-qCcnwXV3.js +0 -1
  77. package/dist/public/assets/react-vendor-Bpg1hd5i.js +0 -22
  78. package/dist/public/assets/settings-general-view-7Y0c9534.js +0 -1
  79. package/dist/public/assets/settings-mcp-servers-view-nAMQQWHb.js +0 -5
  80. package/dist/public/assets/settings-models-view-CMZh5LlR.js +0 -1
  81. package/dist/public/assets/settings-rules-view-BaNccq1v.js +0 -8
  82. package/dist/public/assets/settings-skills-view-DBX7obT7.js +0 -2
  83. package/dist/public/assets/settings-subagents-view-Cy87X2bs.js +0 -2
  84. package/dist/public/assets/skeleton-n47_ST9G.js +0 -1
  85. package/dist/public/assets/standard-list-item-BLTOBGFa.js +0 -1
  86. package/dist/public/assets/store-B9rEO6q-.js +0 -4
  87. package/dist/public/assets/tabs--5y6mYhG.js +0 -1
  88. package/dist/public/assets/textarea-CwXpBxom.js +0 -1
  89. package/dist/public/assets/todos-view-k0k8R9NT.js +0 -1
  90. package/dist/public/assets/use-font-size-BJl84s49.js +0 -1
  91. /package/dist/public/assets/{dist-CAVGqbBm.js → dist-2eORHB3M.js} +0 -0
package/dist/index.js CHANGED
@@ -265,6 +265,9 @@ async function initializeSchema(db) {
265
265
  request_model TEXT NOT NULL,
266
266
  request_attachments TEXT,
267
267
  request_planMode INTEGER,
268
+ request_ralphMode INTEGER NOT NULL DEFAULT 0,
269
+ request_imageMode INTEGER NOT NULL DEFAULT 0,
270
+ request_checkpointRef TEXT,
268
271
  response_content TEXT,
269
272
  response_events TEXT,
270
273
  response_completedAt TEXT,
@@ -307,6 +310,16 @@ async function initializeSchema(db) {
307
310
  cachedAt TEXT NOT NULL
308
311
  )
309
312
  `);
313
+ await db.execute(`
314
+ CREATE TABLE IF NOT EXISTS user_tasks (
315
+ id TEXT PRIMARY KEY,
316
+ threadId TEXT NOT NULL,
317
+ content TEXT NOT NULL,
318
+ createdAt TEXT NOT NULL,
319
+ updatedAt TEXT NOT NULL,
320
+ FOREIGN KEY (threadId) REFERENCES threads(id) ON DELETE CASCADE
321
+ )
322
+ `);
310
323
  await db.execute(`
311
324
  CREATE TABLE IF NOT EXISTS code_files (
312
325
  id INTEGER PRIMARY KEY,
@@ -356,6 +369,9 @@ async function initializeSchema(db) {
356
369
  await db.execute(`
357
370
  CREATE INDEX IF NOT EXISTS idx_project_todos_projectId ON project_todos(projectId)
358
371
  `);
372
+ await db.execute(`
373
+ CREATE INDEX IF NOT EXISTS idx_user_tasks_threadId ON user_tasks(threadId)
374
+ `);
359
375
  await db.execute(`
360
376
  CREATE INDEX IF NOT EXISTS idx_conversation_history_threadId ON conversation_history(threadId)
361
377
  `);
@@ -674,6 +690,15 @@ async function runMigrations(db) {
674
690
  );
675
691
  await db.execute(`ALTER TABLE conversation_history ADD COLUMN response_imageUrl TEXT`);
676
692
  }
693
+ const hasCheckpointRef = convInfo.rows.some(
694
+ (col) => col.name === "request_checkpointRef"
695
+ );
696
+ if (!hasCheckpointRef) {
697
+ tarskDebugLog(
698
+ "[db] Running migration: Adding request_checkpointRef column to conversation_history"
699
+ );
700
+ await db.execute(`ALTER TABLE conversation_history ADD COLUMN request_checkpointRef TEXT`);
701
+ }
677
702
  const projectTodosInfo = await db.execute(`PRAGMA table_info(project_todos)`);
678
703
  const hasThreadIdColumn = projectTodosInfo.rows.some(
679
704
  (col) => col.name === "threadId"
@@ -695,6 +720,25 @@ async function runMigrations(db) {
695
720
  )
696
721
  `);
697
722
  }
723
+ const userTasksExists = await db.execute(
724
+ `SELECT name FROM sqlite_master WHERE type='table' AND name='user_tasks'`
725
+ );
726
+ if (userTasksExists.rows.length === 0) {
727
+ tarskDebugLog("[db] Running migration: Creating user_tasks table");
728
+ await db.execute(`
729
+ CREATE TABLE user_tasks (
730
+ id TEXT PRIMARY KEY,
731
+ threadId TEXT NOT NULL,
732
+ content TEXT NOT NULL,
733
+ createdAt TEXT NOT NULL,
734
+ updatedAt TEXT NOT NULL,
735
+ FOREIGN KEY (threadId) REFERENCES threads(id) ON DELETE CASCADE
736
+ )
737
+ `);
738
+ await db.execute(`
739
+ CREATE INDEX idx_user_tasks_threadId ON user_tasks(threadId)
740
+ `);
741
+ }
698
742
  const codeFilesExists = await db.execute(
699
743
  `SELECT name FROM sqlite_master WHERE type='table' AND name='code_files'`
700
744
  );
@@ -930,7 +974,7 @@ function estimateTokenCount(text) {
930
974
 
931
975
  // src/server.ts
932
976
  import fs3 from "fs";
933
- import { Hono as Hono23 } from "hono";
977
+ import { Hono as Hono24 } from "hono";
934
978
  import { cors } from "hono/cors";
935
979
  import open3 from "open";
936
980
  import path4 from "path";
@@ -5077,10 +5121,12 @@ async function createCodingTools(cwd, options) {
5077
5121
  );
5078
5122
  }
5079
5123
  let mcpTools = [];
5080
- try {
5081
- mcpTools = await createMCPTools(cwd);
5082
- } catch (error) {
5083
- console.warn(`[Tools] Failed to load MCP tools for ${cwd}:`, error);
5124
+ if (options?.loadMcpTools !== false) {
5125
+ try {
5126
+ mcpTools = await createMCPTools(cwd);
5127
+ } catch (error) {
5128
+ console.warn(`[Tools] Failed to load MCP tools for ${cwd}:`, error);
5129
+ }
5084
5130
  }
5085
5131
  const deferredTools = /* @__PURE__ */ new Map();
5086
5132
  if (deferOption === false) {
@@ -6874,6 +6920,114 @@ async function readEnvFile() {
6874
6920
  // src/features/ask-user/ask-user.routes.ts
6875
6921
  import { Hono } from "hono";
6876
6922
 
6923
+ // src/agent/agent-run-guard.ts
6924
+ import { randomUUID as randomUUID2 } from "crypto";
6925
+ var LONG_RUNNING_TURN_LIMIT = 50;
6926
+ var LONG_RUNNING_DURATION_MS = 10 * 60 * 1e3;
6927
+ var LONG_RUNNING_CONFIRMATION_QUESTION = "The agent has been working for a long time. Do you want to continue?";
6928
+ var LONG_RUNNING_CONFIRMATION_OPTIONS = ["Yes", "No"];
6929
+ function getLongRunningResolvers() {
6930
+ const pendingQuestions2 = globalThis;
6931
+ if (!pendingQuestions2.__tarskLongRunningResolvers) {
6932
+ pendingQuestions2.__tarskLongRunningResolvers = /* @__PURE__ */ new Map();
6933
+ }
6934
+ return pendingQuestions2.__tarskLongRunningResolvers;
6935
+ }
6936
+ function createLongRunningGuardState(now = Date.now()) {
6937
+ return {
6938
+ startedAt: now,
6939
+ turnCount: 0,
6940
+ prompted: false
6941
+ };
6942
+ }
6943
+ function incrementLongRunningTurn(state) {
6944
+ state.turnCount += 1;
6945
+ }
6946
+ function resetLongRunningGuard(state, now = Date.now()) {
6947
+ state.startedAt = now;
6948
+ state.turnCount = 0;
6949
+ state.prompted = false;
6950
+ }
6951
+ function shouldPromptForLongRunningConfirmation(state, now = Date.now()) {
6952
+ if (state.prompted) return false;
6953
+ return state.turnCount > LONG_RUNNING_TURN_LIMIT || now - state.startedAt > LONG_RUNNING_DURATION_MS;
6954
+ }
6955
+ function createLongRunningConfirmationRequest() {
6956
+ const toolCallId = `long-running-confirmation-${randomUUID2()}`;
6957
+ const event = {
6958
+ type: "long_running_confirmation",
6959
+ prompt: {
6960
+ toolCallId,
6961
+ question: LONG_RUNNING_CONFIRMATION_QUESTION,
6962
+ options: [...LONG_RUNNING_CONFIRMATION_OPTIONS],
6963
+ context: "The current agent run has exceeded 50 turns or 10 minutes. Choose Yes to continue and reset the limit, or No to stop."
6964
+ },
6965
+ content: JSON.stringify([
6966
+ {
6967
+ type: "tool_use",
6968
+ name: "ask_user",
6969
+ id: toolCallId,
6970
+ input: {
6971
+ question: LONG_RUNNING_CONFIRMATION_QUESTION,
6972
+ options: [...LONG_RUNNING_CONFIRMATION_OPTIONS],
6973
+ context: "The current agent run has exceeded 50 turns or 10 minutes. Choose Yes to continue and reset the limit, or No to stop."
6974
+ }
6975
+ }
6976
+ ])
6977
+ };
6978
+ return {
6979
+ toolCallId,
6980
+ event,
6981
+ waitForAnswer: (signal) => {
6982
+ return new Promise((resolve6, reject) => {
6983
+ const resolvers = getLongRunningResolvers();
6984
+ const cleanup = () => {
6985
+ resolvers.delete(toolCallId);
6986
+ if (signal) {
6987
+ signal.removeEventListener("abort", onAbort);
6988
+ }
6989
+ };
6990
+ const onAbort = () => {
6991
+ cleanup();
6992
+ reject(new Error("Operation aborted"));
6993
+ };
6994
+ if (signal?.aborted) {
6995
+ cleanup();
6996
+ reject(new Error("Operation aborted"));
6997
+ return;
6998
+ }
6999
+ if (signal) {
7000
+ signal.addEventListener("abort", onAbort, { once: true });
7001
+ }
7002
+ resolvers.set(toolCallId, {
7003
+ resolve: (answer) => {
7004
+ cleanup();
7005
+ resolve6(answer);
7006
+ },
7007
+ reject: (error) => {
7008
+ cleanup();
7009
+ reject(error);
7010
+ }
7011
+ });
7012
+ });
7013
+ }
7014
+ };
7015
+ }
7016
+ function resolveLongRunningConfirmation(toolCallId, answer) {
7017
+ const resolver = getLongRunningResolvers().get(toolCallId);
7018
+ if (!resolver) {
7019
+ return false;
7020
+ }
7021
+ resolver.resolve(answer);
7022
+ return true;
7023
+ }
7024
+ function normalizeLongRunningConfirmationAnswer(answer) {
7025
+ const normalized = answer.trim().toLowerCase();
7026
+ if (normalized === "yes") return "yes";
7027
+ if (normalized === "no") return "no";
7028
+ return null;
7029
+ }
7030
+
6877
7031
  // src/core/response-builder.ts
6878
7032
  var ResponseBuilder = class {
6879
7033
  /**
@@ -7014,7 +7168,7 @@ function createAskUserRoutes() {
7014
7168
  400
7015
7169
  );
7016
7170
  }
7017
- const resolved = submitAnswer(toolCallId, answer);
7171
+ const resolved = submitAnswer(toolCallId, answer) || resolveLongRunningConfirmation(toolCallId, answer);
7018
7172
  if (!resolved) {
7019
7173
  return errorResponse(
7020
7174
  c,
@@ -7041,7 +7195,7 @@ function createAskUserRoutes() {
7041
7195
  import { Hono as Hono2 } from "hono";
7042
7196
 
7043
7197
  // src/features/chat/chat-post.route.ts
7044
- import { randomUUID as randomUUID4 } from "crypto";
7198
+ import { randomUUID as randomUUID5 } from "crypto";
7045
7199
 
7046
7200
  // src/features/skills/skills.manager.ts
7047
7201
  import { readdir as readdir4, readFile as readFile6 } from "fs/promises";
@@ -7615,7 +7769,7 @@ import { join as join15 } from "path";
7615
7769
 
7616
7770
  // src/features/project-todos/project-todos.database.ts
7617
7771
  init_database();
7618
- import { randomUUID as randomUUID2 } from "crypto";
7772
+ import { randomUUID as randomUUID3 } from "crypto";
7619
7773
  function serializeTodo(row) {
7620
7774
  return {
7621
7775
  ...row,
@@ -7640,7 +7794,7 @@ async function getTodoById(db, todoId) {
7640
7794
  }
7641
7795
  async function insertTodo(projectId, title, description) {
7642
7796
  const db = await getDatabase();
7643
- const id = randomUUID2();
7797
+ const id = randomUUID3();
7644
7798
  const now = (/* @__PURE__ */ new Date()).toISOString();
7645
7799
  await db.execute({
7646
7800
  sql: `INSERT INTO project_todos (id, projectId, title, description, status, working, createdAt, updatedAt)
@@ -7750,7 +7904,7 @@ async function invalidateGitStatusCache(db, threadId) {
7750
7904
 
7751
7905
  // src/features/account/account-get-info.route.ts
7752
7906
  init_database();
7753
- import { randomUUID as randomUUID3 } from "crypto";
7907
+ import { randomUUID as randomUUID4 } from "crypto";
7754
7908
 
7755
7909
  // src/core/crypto.ts
7756
7910
  init_database();
@@ -7903,7 +8057,7 @@ async function getOrCreateClientReferenceId() {
7903
8057
  if (existing !== null && existing !== void 0) {
7904
8058
  return existing;
7905
8059
  }
7906
- const id = randomUUID3();
8060
+ const id = randomUUID4();
7907
8061
  await setState(db, KEY_CLIENT_REFERENCE_ID, id);
7908
8062
  return id;
7909
8063
  }
@@ -8185,7 +8339,8 @@ async function postChatMessage(c, threadManager, agentExecutor, conversationMana
8185
8339
  provider,
8186
8340
  attachments,
8187
8341
  planMode,
8188
- ralphMode
8342
+ ralphMode,
8343
+ checkpointRef
8189
8344
  } = body;
8190
8345
  let model = baseModel;
8191
8346
  if (provider && typeof provider === "string") {
@@ -8284,7 +8439,7 @@ async function postChatMessage(c, threadManager, agentExecutor, conversationMana
8284
8439
  }
8285
8440
  let conversationId = thread.currentConversationId;
8286
8441
  if (!conversationId) {
8287
- conversationId = randomUUID4();
8442
+ conversationId = randomUUID5();
8288
8443
  const db = await getDatabase();
8289
8444
  await db.execute({
8290
8445
  sql: "DELETE FROM todos WHERE threadId = ?",
@@ -8338,7 +8493,8 @@ async function postChatMessage(c, threadManager, agentExecutor, conversationMana
8338
8493
  attachments,
8339
8494
  planMode,
8340
8495
  void 0,
8341
- ralphMode
8496
+ ralphMode,
8497
+ typeof checkpointRef === "string" ? checkpointRef : void 0
8342
8498
  );
8343
8499
  const history = await conversationManager.getConversationHistoryByConversationId(
8344
8500
  threadId,
@@ -8366,14 +8522,18 @@ async function postChatMessage(c, threadManager, agentExecutor, conversationMana
8366
8522
  }
8367
8523
  processingStateManager.setProcessing(threadId);
8368
8524
  processingStateManager.registerAbortController(threadId, abortController);
8525
+ const longRunningGuard = createLongRunningGuardState();
8369
8526
  async function* executeAgent(prompt, ctx, signal, history2, captured) {
8370
8527
  for await (const event of agentExecutor.execute(prompt, ctx, signal, history2)) {
8371
8528
  captured.events.push(event);
8529
+ if (event.type === "message" || event.type === "toolcall_delta" || event.type === "subagent_event" || event.type === "subagent_start" || event.type === "subagent_complete") {
8530
+ incrementLongRunningTurn(longRunningGuard);
8531
+ }
8372
8532
  if (event.type === "message" && event.content) {
8373
8533
  captured.fullContent += event.content;
8374
8534
  }
8375
- if (event.type === "message" && typeof event.content === "string" && (event.role === "tool" || isToolLikeContent(event.content))) {
8376
- const thinkingEvent = { type: "thinking", content: event.content };
8535
+ if (event.type === "message" && typeof event.content === "string" && (event.role === "tool" || isToolLikeContent(event.content)) || event.type === "long_running_confirmation") {
8536
+ const thinkingEvent = { type: "thinking", content: event.content ?? "" };
8377
8537
  processingStateManager.pushEvent(threadId, thinkingEvent);
8378
8538
  yield thinkingEvent;
8379
8539
  continue;
@@ -8384,6 +8544,44 @@ async function postChatMessage(c, threadManager, agentExecutor, conversationMana
8384
8544
  }
8385
8545
  async function* chatExecutionGenerator() {
8386
8546
  const captured = { events: [], fullContent: "" };
8547
+ async function completeCapturedMessage(completedMessageId, completedCaptured) {
8548
+ const finalContent = extractAssistantContent(
8549
+ completedCaptured.events,
8550
+ completedCaptured.fullContent
8551
+ );
8552
+ await conversationManager.completeMessage(
8553
+ completedMessageId,
8554
+ finalContent,
8555
+ completedCaptured.events
8556
+ );
8557
+ return finalContent;
8558
+ }
8559
+ async function* confirmLongRunningWorkIfNeeded(currentMessageId, currentCaptured) {
8560
+ if (!shouldPromptForLongRunningConfirmation(longRunningGuard)) {
8561
+ return true;
8562
+ }
8563
+ longRunningGuard.prompted = true;
8564
+ const confirmation = createLongRunningConfirmationRequest();
8565
+ currentCaptured.events.push(confirmation.event);
8566
+ processingStateManager.pushEvent(threadId, confirmation.event);
8567
+ yield confirmation.event;
8568
+ const answer = await confirmation.waitForAnswer(abortController.signal).catch(() => "No");
8569
+ const normalizedAnswer = normalizeLongRunningConfirmationAnswer(answer);
8570
+ if (normalizedAnswer === "yes") {
8571
+ resetLongRunningGuard(longRunningGuard);
8572
+ return true;
8573
+ }
8574
+ abortController.abort();
8575
+ const stopEvent = {
8576
+ type: "thinking",
8577
+ content: "[Agent] Stopped long-running execution at user request."
8578
+ };
8579
+ currentCaptured.events.push(stopEvent);
8580
+ processingStateManager.pushEvent(threadId, stopEvent);
8581
+ yield stopEvent;
8582
+ await completeCapturedMessage(currentMessageId, currentCaptured);
8583
+ return false;
8584
+ }
8387
8585
  try {
8388
8586
  yield* executeAgent(
8389
8587
  content,
@@ -8392,8 +8590,16 @@ async function postChatMessage(c, threadManager, agentExecutor, conversationMana
8392
8590
  conversationHistory,
8393
8591
  captured
8394
8592
  );
8395
- const finalContent = extractAssistantContent(captured.events, captured.fullContent);
8396
- await conversationManager.completeMessage(messageId, finalContent, captured.events);
8593
+ const initialConfirmation = confirmLongRunningWorkIfNeeded(messageId, captured);
8594
+ const initialDecision = await initialConfirmation.next();
8595
+ if (!initialDecision.done) {
8596
+ yield initialDecision.value;
8597
+ const postPromptDecision = await initialConfirmation.next();
8598
+ if (postPromptDecision.value === false) {
8599
+ return;
8600
+ }
8601
+ }
8602
+ const finalContent = await completeCapturedMessage(messageId, captured);
8397
8603
  console.log("[ChatRoute] Conversation captured:", {
8398
8604
  messageId,
8399
8605
  eventCount: captured.events.length,
@@ -8421,7 +8627,7 @@ $ ${validationScript}`
8421
8627
  console.log("[ChatRoute] Validation script passed");
8422
8628
  const passEvent = {
8423
8629
  type: "thinking",
8424
- content: `[Validated] Validation passed (exit code 0)${result.output ? `
8630
+ content: `[Validated] Validated (exit code 0)${result.output ? `
8425
8631
  ${result.output}` : ""}`
8426
8632
  };
8427
8633
  processingStateManager.pushEvent(threadId, passEvent);
@@ -8463,14 +8669,21 @@ ${result.output}`;
8463
8669
  updatedHistory,
8464
8670
  validationCaptured
8465
8671
  );
8466
- const validationContent = extractAssistantContent(
8467
- validationCaptured.events,
8468
- validationCaptured.fullContent
8672
+ const validationConfirmation = confirmLongRunningWorkIfNeeded(
8673
+ validationMessageId,
8674
+ validationCaptured
8469
8675
  );
8470
- await conversationManager.completeMessage(
8676
+ const validationDecision = await validationConfirmation.next();
8677
+ if (!validationDecision.done) {
8678
+ yield validationDecision.value;
8679
+ const postValidationDecision = await validationConfirmation.next();
8680
+ if (postValidationDecision.value === false) {
8681
+ return;
8682
+ }
8683
+ }
8684
+ const validationContent = await completeCapturedMessage(
8471
8685
  validationMessageId,
8472
- validationContent,
8473
- validationCaptured.events
8686
+ validationCaptured
8474
8687
  );
8475
8688
  conversationHistory.push({
8476
8689
  userMessage: validationPrompt,
@@ -8513,7 +8726,7 @@ ${result.output}`;
8513
8726
  };
8514
8727
  processingStateManager.pushEvent(threadId, iterationEvent);
8515
8728
  yield iterationEvent;
8516
- const newConversationId = randomUUID4();
8729
+ const newConversationId = randomUUID5();
8517
8730
  await threadManager.updateThread(threadId, {
8518
8731
  currentConversationId: newConversationId
8519
8732
  });
@@ -8539,15 +8752,16 @@ ${result.output}`;
8539
8752
  // Empty history - fresh context
8540
8753
  iterCaptured
8541
8754
  );
8542
- const iterContent = extractAssistantContent(
8543
- iterCaptured.events,
8544
- iterCaptured.fullContent
8545
- );
8546
- await conversationManager.completeMessage(
8547
- iterMessageId,
8548
- iterContent,
8549
- iterCaptured.events
8550
- );
8755
+ const ralphConfirmation = confirmLongRunningWorkIfNeeded(iterMessageId, iterCaptured);
8756
+ const ralphDecision = await ralphConfirmation.next();
8757
+ if (!ralphDecision.done) {
8758
+ yield ralphDecision.value;
8759
+ const postRalphDecision = await ralphConfirmation.next();
8760
+ if (postRalphDecision.value === false) {
8761
+ return;
8762
+ }
8763
+ }
8764
+ await completeCapturedMessage(iterMessageId, iterCaptured);
8551
8765
  decrementBalance().catch((err) => {
8552
8766
  console.error("[ChatRoute] Failed to decrement balance:", err);
8553
8767
  });
@@ -8637,7 +8851,7 @@ ${result.output}`;
8637
8851
  }
8638
8852
 
8639
8853
  // src/features/chat/chat-delete.route.ts
8640
- import { randomUUID as randomUUID5 } from "crypto";
8854
+ import { randomUUID as randomUUID6 } from "crypto";
8641
8855
  init_database();
8642
8856
  async function deleteChat(c, threadManager) {
8643
8857
  try {
@@ -8659,7 +8873,7 @@ async function deleteChat(c, threadManager) {
8659
8873
  sql: "DELETE FROM todos WHERE threadId = ?",
8660
8874
  args: [threadId]
8661
8875
  });
8662
- const newConversationId = randomUUID5();
8876
+ const newConversationId = randomUUID6();
8663
8877
  await threadManager.updateThread(threadId, { currentConversationId: newConversationId });
8664
8878
  return successResponse(
8665
8879
  c,
@@ -8688,13 +8902,10 @@ function subscribeToChatStream(c, processingStateManager) {
8688
8902
  return c.json({ error: "Thread is not currently processing" }, 404);
8689
8903
  }
8690
8904
  return stream2(c, async (writer) => {
8691
- const buffered = processingStateManager.getBufferedEvents(threadId);
8692
- for (const event of buffered) {
8693
- await writer.write(JSON.stringify(event) + "\n");
8694
- }
8695
8905
  let resolve6 = null;
8696
8906
  const queue = [];
8697
8907
  let done = false;
8908
+ const replay = processingStateManager.getBufferedEvents(threadId).slice();
8698
8909
  const unsubscribe = processingStateManager.subscribe(threadId, (event) => {
8699
8910
  if (event.type === "complete") {
8700
8911
  done = true;
@@ -8707,6 +8918,7 @@ function subscribeToChatStream(c, processingStateManager) {
8707
8918
  resolve6 = null;
8708
8919
  }
8709
8920
  });
8921
+ const finishedBeforeSubscribe = !processingStateManager.isProcessing(threadId);
8710
8922
  writer.onAbort(() => {
8711
8923
  done = true;
8712
8924
  unsubscribe();
@@ -8716,6 +8928,16 @@ function subscribeToChatStream(c, processingStateManager) {
8716
8928
  }
8717
8929
  });
8718
8930
  try {
8931
+ for (const event of replay) {
8932
+ await writer.write(JSON.stringify(event) + "\n");
8933
+ if (event.type === "complete") {
8934
+ done = true;
8935
+ }
8936
+ }
8937
+ if (finishedBeforeSubscribe && !done) {
8938
+ await writer.write(JSON.stringify({ type: "complete", content: "" }) + "\n");
8939
+ return;
8940
+ }
8719
8941
  while (!done) {
8720
8942
  while (queue.length > 0) {
8721
8943
  const event = queue.shift();
@@ -8771,18 +8993,18 @@ function createChatRoutes(threadManager, agentExecutor, conversationManager, pro
8771
8993
  init_database();
8772
8994
 
8773
8995
  // src/features/conversations/conversations.database.ts
8774
- import { randomUUID as randomUUID6 } from "crypto";
8775
- async function insertMessage(db, threadId, conversationId, message, model, attachments, planMode, imageMode, ralphMode) {
8996
+ import { randomUUID as randomUUID7 } from "crypto";
8997
+ async function insertMessage(db, threadId, conversationId, message, model, attachments, planMode, imageMode, ralphMode, checkpointRef) {
8776
8998
  try {
8777
- const messageId = randomUUID6();
8999
+ const messageId = randomUUID7();
8778
9000
  const timestamp = (/* @__PURE__ */ new Date()).toISOString();
8779
9001
  await db.execute(
8780
9002
  `
8781
9003
  INSERT INTO conversation_history (
8782
9004
  id, threadId, conversationId, timestamp, status,
8783
- request_message, request_model, request_attachments, request_planMode, request_ralphMode, request_imageMode
9005
+ request_message, request_model, request_attachments, request_planMode, request_ralphMode, request_imageMode, request_checkpointRef
8784
9006
  )
8785
- VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
9007
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
8786
9008
  `,
8787
9009
  [
8788
9010
  messageId,
@@ -8795,7 +9017,8 @@ async function insertMessage(db, threadId, conversationId, message, model, attac
8795
9017
  attachments ? JSON.stringify(attachments) : null,
8796
9018
  planMode ? 1 : 0,
8797
9019
  ralphMode ? 1 : 0,
8798
- imageMode ? 1 : 0
9020
+ imageMode ? 1 : 0,
9021
+ checkpointRef ?? null
8799
9022
  ]
8800
9023
  );
8801
9024
  return messageId;
@@ -8999,7 +9222,8 @@ function deserializeMessage(row) {
8999
9222
  ...row.request_attachments && { attachments: JSON.parse(row.request_attachments) },
9000
9223
  ...row.request_planMode === 1 && { planMode: true },
9001
9224
  ...row.request_ralphMode === 1 && { ralphMode: true },
9002
- ...row.request_imageMode === 1 && { imageMode: true }
9225
+ ...row.request_imageMode === 1 && { imageMode: true },
9226
+ ...row.request_checkpointRef && { checkpointRef: row.request_checkpointRef }
9003
9227
  },
9004
9228
  response: row.response_content != null ? {
9005
9229
  content: row.response_content,
@@ -9078,7 +9302,7 @@ var ConversationManagerImpl = class {
9078
9302
  /**
9079
9303
  * Starts a new conversation message
9080
9304
  */
9081
- async startMessage(threadId, _threadPath, conversationId, message, model, attachments, planMode, imageMode, ralphMode) {
9305
+ async startMessage(threadId, _threadPath, conversationId, message, model, attachments, planMode, imageMode, ralphMode, checkpointRef) {
9082
9306
  try {
9083
9307
  if (!this.db) {
9084
9308
  await this.initialize();
@@ -9092,7 +9316,8 @@ var ConversationManagerImpl = class {
9092
9316
  attachments,
9093
9317
  planMode,
9094
9318
  imageMode,
9095
- ralphMode
9319
+ ralphMode,
9320
+ checkpointRef
9096
9321
  );
9097
9322
  } catch (error) {
9098
9323
  console.error("Failed to start conversation message:", error);
@@ -9978,7 +10203,8 @@ async function computeContextBreakdown(threadId, conversationId, conversationMan
9978
10203
  skills: activatedSkills,
9979
10204
  threadId,
9980
10205
  threadPath,
9981
- metadataManager
10206
+ metadataManager,
10207
+ loadMcpTools: false
9982
10208
  });
9983
10209
  const promptSections = await loadPromptSections(
9984
10210
  threadPath,
@@ -10210,6 +10436,14 @@ async function deleteThread(db, id) {
10210
10436
  throw error;
10211
10437
  }
10212
10438
  }
10439
+ async function getThread(db, id) {
10440
+ const result = await db.execute("SELECT * FROM threads WHERE id = ?", [id]);
10441
+ const row = result.rows[0];
10442
+ if (!row) {
10443
+ return null;
10444
+ }
10445
+ return deserializeThread(row);
10446
+ }
10213
10447
  async function getAllThreads(db) {
10214
10448
  try {
10215
10449
  const result = await db.execute(
@@ -12477,7 +12711,7 @@ var ProcessManager = class extends EventEmitter {
12477
12711
 
12478
12712
  // src/features/projects/projects.creator.ts
12479
12713
  init_utils();
12480
- import { randomUUID as randomUUID7 } from "crypto";
12714
+ import { randomUUID as randomUUID8 } from "crypto";
12481
12715
  import { basename as basename2, join as join20, relative as relative5 } from "path";
12482
12716
  import { access as access3, stat as stat3, readdir as readdir8 } from "fs/promises";
12483
12717
 
@@ -13663,14 +13897,14 @@ var ProjectCreator = class {
13663
13897
  }
13664
13898
  let initialThreadId;
13665
13899
  try {
13666
- const projectId = randomUUID7();
13900
+ const projectId = randomUUID8();
13667
13901
  const existingProjects = await this.metadataManager.loadProjects();
13668
13902
  const existingNames = existingProjects.map((p) => p.name);
13669
13903
  const projectName = this.ensureUniqueProjectName(
13670
13904
  this.deriveProjectName(gitUrl),
13671
13905
  existingNames
13672
13906
  );
13673
- initialThreadId = randomUUID7();
13907
+ initialThreadId = randomUUID8();
13674
13908
  const initialThreadTitle = generateRandomThreadName();
13675
13909
  const firstThreadPath = this.generateThreadPath(projectId, initialThreadId);
13676
13910
  this.processingStateManager?.setProcessing(initialThreadId);
@@ -13810,8 +14044,8 @@ var ProjectCreator = class {
13810
14044
  };
13811
14045
  return;
13812
14046
  }
13813
- const projectId = randomUUID7();
13814
- const initialThreadId = randomUUID7();
14047
+ const projectId = randomUUID8();
14048
+ const initialThreadId = randomUUID8();
13815
14049
  const initialThreadTitle = generateRandomThreadName();
13816
14050
  this.processingStateManager?.setProcessing(initialThreadId);
13817
14051
  try {
@@ -13961,8 +14195,8 @@ var ProjectCreator = class {
13961
14195
  yield { type: "error", message: `Cannot access folder: ${folderPath}` };
13962
14196
  return;
13963
14197
  }
13964
- const projectId = randomUUID7();
13965
- const initialThreadId = randomUUID7();
14198
+ const projectId = randomUUID8();
14199
+ const initialThreadId = randomUUID8();
13966
14200
  const initialThreadTitle = generateRandomThreadName();
13967
14201
  this.processingStateManager?.setProcessing(initialThreadId);
13968
14202
  try {
@@ -14396,7 +14630,7 @@ var ProjectManagerImpl = class {
14396
14630
  throw new Error(`Project not found: ${projectId}`);
14397
14631
  }
14398
14632
  if (setupScript === null || setupScript === "") {
14399
- delete projects[projectIndex].setupScript;
14633
+ Reflect.set(projects[projectIndex], "setupScript", null);
14400
14634
  } else {
14401
14635
  projects[projectIndex].setupScript = setupScript;
14402
14636
  }
@@ -14414,7 +14648,7 @@ var ProjectManagerImpl = class {
14414
14648
  throw new Error(`Project not found: ${projectId}`);
14415
14649
  }
14416
14650
  if (validationScript === null || validationScript === "") {
14417
- delete projects[projectIndex].validationScript;
14651
+ Reflect.set(projects[projectIndex], "validationScript", null);
14418
14652
  } else {
14419
14653
  projects[projectIndex].validationScript = validationScript;
14420
14654
  }
@@ -14536,11 +14770,15 @@ ___CWD___%s
14536
14770
  */
14537
14771
  async updateRunCommand(projectId, runCommand) {
14538
14772
  const projects = await this.metadataManager.loadProjects();
14539
- const project = projects.find((p) => p.id === projectId);
14540
- if (!project) {
14773
+ const projectIndex = projects.findIndex((p) => p.id === projectId);
14774
+ if (projectIndex === -1) {
14541
14775
  throw new Error(`Project not found: ${projectId}`);
14542
14776
  }
14543
- project.runCommand = runCommand ?? void 0;
14777
+ if (runCommand === null || runCommand === "") {
14778
+ Reflect.set(projects[projectIndex], "runCommand", null);
14779
+ } else {
14780
+ projects[projectIndex].runCommand = runCommand;
14781
+ }
14544
14782
  await this.metadataManager.saveProjects(projects);
14545
14783
  }
14546
14784
  /**
@@ -14569,7 +14807,7 @@ ___CWD___%s
14569
14807
  if (!project) {
14570
14808
  throw new Error(`Project not found: ${projectId}`);
14571
14809
  }
14572
- project[field] = value && value.trim() ? value : void 0;
14810
+ Reflect.set(project, field, value && value.trim() ? value : null);
14573
14811
  await this.metadataManager.saveProjects(projects);
14574
14812
  }
14575
14813
  /**
@@ -15946,7 +16184,7 @@ function createRuleRoutes(router, projectManager) {
15946
16184
 
15947
16185
  // src/features/threads/threads.manager.ts
15948
16186
  init_utils();
15949
- import { randomUUID as randomUUID8 } from "crypto";
16187
+ import { randomUUID as randomUUID9 } from "crypto";
15950
16188
  import { join as join23 } from "path";
15951
16189
  import { execSync as execSync3 } from "child_process";
15952
16190
  import { rm as rm4, stat as stat4, mkdir as mkdir4 } from "fs/promises";
@@ -16013,7 +16251,7 @@ var ThreadManagerImpl = class {
16013
16251
  } catch {
16014
16252
  await mkdir4(project.path, { recursive: true });
16015
16253
  }
16016
- threadId = randomUUID8();
16254
+ threadId = randomUUID9();
16017
16255
  const threadPath = this.generateThreadPath(projectId, threadId);
16018
16256
  const existingThreads = await this.metadataManager.loadThreads();
16019
16257
  const existingTitles = new Set(
@@ -16554,7 +16792,7 @@ import { glob as glob2 } from "glob";
16554
16792
 
16555
16793
  // src/features/project-scripts/project-scripts.database.ts
16556
16794
  init_database();
16557
- import { randomUUID as randomUUID9 } from "crypto";
16795
+ import { randomUUID as randomUUID10 } from "crypto";
16558
16796
  async function getScriptsByProject(db, projectId) {
16559
16797
  const result = await db.execute({
16560
16798
  sql: `SELECT id, projectId, workspace, name, command, friendlyName, updatedAt
@@ -16571,7 +16809,7 @@ async function upsertProjectScripts(projectId, scripts) {
16571
16809
  sql: `INSERT OR REPLACE INTO project_scripts (id, projectId, workspace, name, command, friendlyName, updatedAt)
16572
16810
  VALUES (?, ?, ?, ?, ?, ?, ?)`,
16573
16811
  args: [
16574
- randomUUID9(),
16812
+ randomUUID10(),
16575
16813
  projectId,
16576
16814
  script.workspace,
16577
16815
  script.name,
@@ -17078,7 +17316,8 @@ async function handleGetThreadMessages(c, threadManager, conversationManager) {
17078
17316
  fileName: attachment.name,
17079
17317
  content: attachment.content,
17080
17318
  size: attachment.size
17081
- }))
17319
+ })),
17320
+ checkpointRef: message.request.checkpointRef
17082
17321
  };
17083
17322
  if (!message.response) {
17084
17323
  return [userMessage];
@@ -18652,6 +18891,101 @@ import { existsSync as existsSync18 } from "fs";
18652
18891
 
18653
18892
  // src/features/git/git.utils.ts
18654
18893
  init_utils();
18894
+
18895
+ // src/features/git/git-subprocess-timing.ts
18896
+ init_utils();
18897
+ import { AsyncLocalStorage } from "async_hooks";
18898
+ var GITOPS_TIMING_PREFIX = "[gitops-timing]";
18899
+ var timingStorage = new AsyncLocalStorage();
18900
+ function withGitTimingContext(threadId, fn) {
18901
+ return timingStorage.run({ threadId }, fn);
18902
+ }
18903
+ function isGitSubprocessTimingEnabled() {
18904
+ return timingStorage.getStore() !== void 0;
18905
+ }
18906
+ function logSubprocessMark(scope, stepName, label, offsetMs) {
18907
+ console.log(
18908
+ `${GITOPS_TIMING_PREFIX} subprocess ${scope} step=${stepName} ${label} +${offsetMs}ms`
18909
+ );
18910
+ }
18911
+ function runCommandTimed(stepName, command, args2, cwd) {
18912
+ const scope = `thread=${timingStorage.getStore()?.threadId ?? "unknown"}`;
18913
+ const t0 = Date.now();
18914
+ let tSpawnEvent = null;
18915
+ let tFirstIo = null;
18916
+ let tExit = null;
18917
+ logSubprocessMark(scope, stepName, "spawn-call", 0);
18918
+ return new Promise((resolve6, reject) => {
18919
+ const proc = spawnProcess(command, args2, { cwd });
18920
+ proc.on("spawn", () => {
18921
+ tSpawnEvent = Date.now();
18922
+ logSubprocessMark(scope, stepName, "spawn-event", tSpawnEvent - t0);
18923
+ });
18924
+ let stdout = "";
18925
+ let stderr = "";
18926
+ function onIo() {
18927
+ if (tFirstIo === null) {
18928
+ tFirstIo = Date.now();
18929
+ logSubprocessMark(scope, stepName, "first-io", tFirstIo - t0);
18930
+ }
18931
+ }
18932
+ if (proc.stdout) {
18933
+ proc.stdout.on("data", (chunk) => {
18934
+ onIo();
18935
+ stdout += chunk.toString();
18936
+ });
18937
+ }
18938
+ if (proc.stderr) {
18939
+ proc.stderr.on("data", (chunk) => {
18940
+ onIo();
18941
+ stderr += chunk.toString();
18942
+ });
18943
+ }
18944
+ proc.on("exit", () => {
18945
+ tExit = Date.now();
18946
+ logSubprocessMark(scope, stepName, "process-exit", tExit - t0);
18947
+ });
18948
+ proc.on("close", (code) => {
18949
+ const tClose = Date.now();
18950
+ logSubprocessMark(scope, stepName, "close-callback", tClose - t0);
18951
+ const preSpawnMs = tSpawnEvent !== null ? tSpawnEvent - t0 : null;
18952
+ const processActiveMs = tSpawnEvent !== null && tExit !== null ? tExit - tSpawnEvent : null;
18953
+ const exitToCloseMs = tExit !== null ? tClose - tExit : null;
18954
+ console.log(
18955
+ `${GITOPS_TIMING_PREFIX} subprocess ${scope} step=${stepName} summary total=${tClose - t0}ms` + (preSpawnMs !== null ? ` pre-spawn=${preSpawnMs}ms` : "") + (processActiveMs !== null ? ` process-active=${processActiveMs}ms` : "") + (exitToCloseMs !== null ? ` exit-to-close=${exitToCloseMs}ms` : "") + ` cmd=${command} ${args2.join(" ")}`
18956
+ );
18957
+ resolve6({ code, stdout, stderr });
18958
+ });
18959
+ proc.on("error", (error) => {
18960
+ logSubprocessMark(scope, stepName, "spawn-error", Date.now() - t0);
18961
+ reject(error);
18962
+ });
18963
+ });
18964
+ }
18965
+ function runGitCommandTimed(stepName, cwd, args2) {
18966
+ return runCommandTimed(stepName, "git", args2, cwd);
18967
+ }
18968
+ var EVENT_LOOP_LAG_THRESHOLD_MS = 50;
18969
+ var EVENT_LOOP_SAMPLE_INTERVAL_MS = 100;
18970
+ function startEventLoopLagMonitor(threadId) {
18971
+ const requestStart = Date.now();
18972
+ let expectedAt = requestStart + EVENT_LOOP_SAMPLE_INTERVAL_MS;
18973
+ const intervalId = setInterval(() => {
18974
+ const now = Date.now();
18975
+ const lag = now - expectedAt;
18976
+ if (lag >= EVENT_LOOP_LAG_THRESHOLD_MS) {
18977
+ console.log(
18978
+ `${GITOPS_TIMING_PREFIX} event-loop-lag thread=${threadId} lag=${lag}ms at=+${now - requestStart}ms`
18979
+ );
18980
+ }
18981
+ expectedAt = now + EVENT_LOOP_SAMPLE_INTERVAL_MS;
18982
+ }, EVENT_LOOP_SAMPLE_INTERVAL_MS);
18983
+ return () => {
18984
+ clearInterval(intervalId);
18985
+ };
18986
+ }
18987
+
18988
+ // src/features/git/git.utils.ts
18655
18989
  import { existsSync as existsSync17, readFileSync as readFileSync5, statSync as statSync4 } from "fs";
18656
18990
  import { isAbsolute as isAbsolute3, normalize as normalize2, resolve as resolve3, join as join28 } from "path";
18657
18991
  import { completeSimple } from "@mariozechner/pi-ai";
@@ -18796,32 +19130,61 @@ function resolveThreadPath(repoPath) {
18796
19130
  const base = isAbsolute3(repoPath) ? repoPath : resolve3(getDataDir(), repoPath);
18797
19131
  return normalize2(base);
18798
19132
  }
18799
- function getGitRoot(cwd) {
18800
- return new Promise((resolveRoot, reject) => {
18801
- const proc = spawnProcess("git", ["rev-parse", "--show-toplevel"], { cwd });
18802
- let out = "";
18803
- let err = "";
18804
- if (proc.stdout) {
18805
- proc.stdout.on("data", (d) => {
18806
- out += d.toString();
18807
- });
18808
- }
18809
- if (proc.stderr) {
18810
- proc.stderr.on("data", (d) => {
18811
- err += d.toString();
18812
- });
19133
+ var gitRootCache = /* @__PURE__ */ new Map();
19134
+ async function getGitRoot(cwd) {
19135
+ const cacheKey = normalize2(cwd);
19136
+ const cached = gitRootCache.get(cacheKey);
19137
+ if (cached) {
19138
+ return cached;
19139
+ }
19140
+ let gitRoot;
19141
+ if (isGitSubprocessTimingEnabled()) {
19142
+ const result = await runGitCommandTimed("resolve-git-root", cwd, [
19143
+ "rev-parse",
19144
+ "--show-toplevel"
19145
+ ]);
19146
+ if (result.code === 0) {
19147
+ gitRoot = result.stdout.trim();
19148
+ } else {
19149
+ throw new Error(result.stderr.trim() || "Not a git repository");
18813
19150
  }
18814
- proc.on("close", (code) => {
18815
- if (code === 0) {
18816
- resolveRoot(out.trim());
18817
- } else {
18818
- reject(new Error(err.trim() || "Not a git repository"));
19151
+ } else {
19152
+ gitRoot = await new Promise((resolveRoot, reject) => {
19153
+ const proc = spawnProcess("git", ["rev-parse", "--show-toplevel"], { cwd });
19154
+ let out = "";
19155
+ let err = "";
19156
+ if (proc.stdout) {
19157
+ proc.stdout.on("data", (d) => {
19158
+ out += d.toString();
19159
+ });
19160
+ }
19161
+ if (proc.stderr) {
19162
+ proc.stderr.on("data", (d) => {
19163
+ err += d.toString();
19164
+ });
18819
19165
  }
19166
+ proc.on("close", (code) => {
19167
+ if (code === 0) {
19168
+ resolveRoot(out.trim());
19169
+ } else {
19170
+ reject(new Error(err.trim() || "Not a git repository"));
19171
+ }
19172
+ });
19173
+ proc.on("error", reject);
18820
19174
  });
18821
- proc.on("error", reject);
18822
- });
19175
+ }
19176
+ gitRootCache.set(cacheKey, gitRoot);
19177
+ return gitRoot;
18823
19178
  }
18824
- function getCurrentBranch(gitRoot) {
19179
+ async function getCurrentBranch(gitRoot) {
19180
+ if (isGitSubprocessTimingEnabled()) {
19181
+ const result = await runGitCommandTimed("getCurrentBranch", gitRoot, [
19182
+ "rev-parse",
19183
+ "--abbrev-ref",
19184
+ "HEAD"
19185
+ ]);
19186
+ return result.stdout.trim();
19187
+ }
18825
19188
  return new Promise((resolve6) => {
18826
19189
  const proc = spawnProcess("git", ["rev-parse", "--abbrev-ref", "HEAD"], { cwd: gitRoot });
18827
19190
  let out = "";
@@ -18934,7 +19297,15 @@ new file mode 100644
18934
19297
  }
18935
19298
  return parts.join("\n");
18936
19299
  }
18937
- function getGitStatus(gitRoot) {
19300
+ async function getGitStatus(gitRoot) {
19301
+ if (isGitSubprocessTimingEnabled()) {
19302
+ const result = await runGitCommandTimed("getGitStatus", gitRoot, [
19303
+ "status",
19304
+ "--porcelain",
19305
+ "--untracked-files=all"
19306
+ ]);
19307
+ return result.stdout;
19308
+ }
18938
19309
  return new Promise((resolve6) => {
18939
19310
  const proc = spawnProcess("git", ["status", "--porcelain", "--untracked-files=all"], {
18940
19311
  cwd: gitRoot
@@ -18987,7 +19358,18 @@ function hasCommitsAheadOfDefault(gitRoot, defaultBranch) {
18987
19358
  proc.on("error", () => resolve6(false));
18988
19359
  });
18989
19360
  }
18990
- function getRemoteOrigin(gitRoot) {
19361
+ async function getRemoteOrigin(gitRoot) {
19362
+ if (isGitSubprocessTimingEnabled()) {
19363
+ const result = await runGitCommandTimed("getRemoteOrigin", gitRoot, [
19364
+ "remote",
19365
+ "get-url",
19366
+ "origin"
19367
+ ]);
19368
+ if (result.code === 0) {
19369
+ return result.stdout.trim();
19370
+ }
19371
+ return "";
19372
+ }
18991
19373
  return new Promise((resolve6) => {
18992
19374
  const proc = spawnProcess("git", ["remote", "get-url", "origin"], { cwd: gitRoot });
18993
19375
  let out = "";
@@ -19006,54 +19388,22 @@ function getRemoteOrigin(gitRoot) {
19006
19388
  proc.on("error", () => resolve6(""));
19007
19389
  });
19008
19390
  }
19009
- async function getGitSyncStatus(gitRoot, branch) {
19391
+ async function getGitSyncStatus(gitRoot, branch, options) {
19392
+ const fetchRemote = options?.fetchRemote ?? false;
19010
19393
  console.log(`[git-status] Checking sync status for branch: ${branch}`);
19011
- console.log(`[git-status] Fetching from origin...`);
19012
- await new Promise((resolve6) => {
19013
- const fetchProc = spawnProcess("git", ["fetch", "origin"], { cwd: gitRoot });
19014
- fetchProc.on("close", () => {
19015
- console.log(`[git-status] \u2713 Fetch completed`);
19016
- resolve6();
19017
- });
19018
- fetchProc.on("error", () => {
19019
- console.log(`[git-status] Fetch error (continuing anyway)`);
19020
- resolve6();
19021
- });
19022
- });
19023
- console.log(`[git-status] Finding default branch...`);
19024
- const defaultBranch = await findDefaultBranch(gitRoot);
19025
- console.log(`[git-status] Default branch: ${defaultBranch}`);
19026
- if (defaultBranch && defaultBranch !== branch) {
19027
- console.log(`[git-status] Checking if ${branch} is behind ${defaultBranch}...`);
19028
- const isBehindDefault = await new Promise((resolve6) => {
19029
- const proc = spawnProcess(
19030
- "git",
19031
- ["rev-list", "--count", `${branch}..origin/${defaultBranch}`],
19032
- { cwd: gitRoot }
19033
- );
19034
- let out = "";
19035
- if (proc.stdout) {
19036
- proc.stdout.on("data", (d) => {
19037
- out += d.toString();
19038
- });
19039
- }
19040
- proc.on("close", () => {
19041
- const count = parseInt(out.trim(), 10);
19042
- console.log(`[git-status] Commits in ${defaultBranch} but not in ${branch}: ${count}`);
19043
- resolve6(count > 0);
19394
+ if (fetchRemote) {
19395
+ console.log(`[git-status] Fetching from origin...`);
19396
+ await new Promise((resolve6) => {
19397
+ const fetchProc = spawnProcess("git", ["fetch", "origin"], { cwd: gitRoot });
19398
+ fetchProc.on("close", () => {
19399
+ console.log(`[git-status] \u2713 Fetch completed`);
19400
+ resolve6();
19044
19401
  });
19045
- proc.on("error", () => {
19046
- console.log(`[git-status] Error checking commits (assuming not behind)`);
19047
- resolve6(false);
19402
+ fetchProc.on("error", () => {
19403
+ console.log(`[git-status] Fetch error (continuing anyway)`);
19404
+ resolve6();
19048
19405
  });
19049
19406
  });
19050
- if (isBehindDefault) {
19051
- console.log(`[git-status] \u2713 Branch IS behind default branch`);
19052
- return "Behind";
19053
- }
19054
- console.log(`[git-status] Branch is NOT behind default branch`);
19055
- } else {
19056
- console.log(`[git-status] Skipping default branch check (same branch or no default found)`);
19057
19407
  }
19058
19408
  console.log(`[git-status] Checking status against remote tracking branch...`);
19059
19409
  return new Promise((resolve6) => {
@@ -19071,27 +19421,30 @@ async function getGitSyncStatus(gitRoot, branch) {
19071
19421
  });
19072
19422
  }
19073
19423
  statusProc.on("close", () => {
19074
- const output = statusOut + statusErr;
19075
- console.log(`[git-status] git status -sb output: ${output.trim()}`);
19076
- if (!output.includes(branch)) {
19424
+ const output = (statusOut + statusErr).trim();
19425
+ console.log(`[git-status] git status -sb output: ${output}`);
19426
+ const firstLine = output.split("\n")[0] ?? "";
19427
+ if (!firstLine.includes(branch)) {
19077
19428
  console.log(`[git-status] Branch name not found in status output, returning 'Up to date'`);
19078
19429
  resolve6("Up to date");
19079
19430
  return;
19080
19431
  }
19081
- if (output.includes("ahead") && output.includes("behind")) {
19432
+ if (!firstLine.includes("...")) {
19433
+ console.log(`[git-status] No upstream tracking branch detected, returning 'Up to date'`);
19434
+ resolve6("Up to date");
19435
+ return;
19436
+ }
19437
+ if (firstLine.includes("ahead") && firstLine.includes("behind")) {
19082
19438
  console.log(`[git-status] \u2713 Status: Diverged`);
19083
19439
  resolve6("Diverged");
19084
- } else if (output.includes("behind")) {
19440
+ } else if (firstLine.includes("behind")) {
19085
19441
  console.log(`[git-status] \u2713 Status: Behind`);
19086
19442
  resolve6("Behind");
19087
- } else if (output.includes("ahead")) {
19443
+ } else if (firstLine.includes("ahead")) {
19088
19444
  console.log(`[git-status] \u2713 Status: Ahead`);
19089
19445
  resolve6("Ahead");
19090
- } else if (output.includes("#") && !output.includes("ahead") && !output.includes("behind")) {
19091
- console.log(`[git-status] \u2713 Status: Up to date`);
19092
- resolve6("Up to date");
19093
19446
  } else {
19094
- console.log(`[git-status] \u2713 Status: Up to date (default)`);
19447
+ console.log(`[git-status] \u2713 Status: Up to date`);
19095
19448
  resolve6("Up to date");
19096
19449
  }
19097
19450
  });
@@ -19137,18 +19490,13 @@ function getDefaultBranch(gitRoot) {
19137
19490
  });
19138
19491
  }
19139
19492
  async function findDefaultBranch(gitRoot) {
19493
+ const fromOriginHead = await getDefaultBranch(gitRoot);
19494
+ if (fromOriginHead) return fromOriginHead;
19140
19495
  const candidates = ["main", "master", "develop", "staging"];
19141
- for (const branch of candidates) {
19142
- const exists = await new Promise((resolve6) => {
19143
- const proc = spawnProcess("git", ["rev-parse", "--verify", `origin/${branch}`], {
19144
- cwd: gitRoot
19145
- });
19146
- proc.on("close", (code) => resolve6(code === 0));
19147
- proc.on("error", () => resolve6(false));
19148
- });
19149
- if (exists) return branch;
19150
- }
19151
- return null;
19496
+ const checks = await Promise.all(
19497
+ candidates.map(async (branch) => await hasRemoteBranch(gitRoot, branch) ? branch : null)
19498
+ );
19499
+ return checks.find((branch) => branch !== null) ?? null;
19152
19500
  }
19153
19501
  function getCommitLog(gitRoot, baseBranch, currentBranch) {
19154
19502
  return new Promise((resolve6) => {
@@ -19394,7 +19742,31 @@ function createPullRequest(gitRoot, title, description) {
19394
19742
  });
19395
19743
  });
19396
19744
  }
19397
- function getPullRequestStatus(gitRoot) {
19745
+ async function getPullRequestStatus(gitRoot) {
19746
+ if (isGitSubprocessTimingEnabled()) {
19747
+ const result = await runCommandTimed(
19748
+ "getPullRequestStatus",
19749
+ "gh",
19750
+ ["pr", "view", "--json", "url,state"],
19751
+ gitRoot
19752
+ );
19753
+ if (result.code === 0) {
19754
+ try {
19755
+ const prData = JSON.parse(result.stdout.trim());
19756
+ return {
19757
+ exists: true,
19758
+ url: prData.url,
19759
+ state: prData.state
19760
+ };
19761
+ } catch {
19762
+ throw new Error("Failed to parse PR data");
19763
+ }
19764
+ }
19765
+ if (result.stderr.includes("no pull requests") || result.stderr.includes("not found")) {
19766
+ return { exists: false };
19767
+ }
19768
+ throw new Error(result.stderr || "Failed to check PR status");
19769
+ }
19398
19770
  return new Promise((resolve6, reject) => {
19399
19771
  const args2 = ["pr", "view", "--json", "url,state"];
19400
19772
  const proc = spawnProcess("gh", args2, { cwd: gitRoot });
@@ -20160,7 +20532,60 @@ async function gitGithubStatusHandler(c, metadataManager) {
20160
20532
 
20161
20533
  // src/features/git/git-unified-status.route.ts
20162
20534
  import { existsSync as existsSync20 } from "fs";
20163
- async function gitUnifiedStatusHandler(c, metadataManager, db) {
20535
+ function parseChangedFiles(statusOutput) {
20536
+ const lines = statusOutput.trim().split("\n").filter((line) => line.length > 0);
20537
+ return {
20538
+ hasChanges: lines.length > 0,
20539
+ changedFilesCount: lines.length
20540
+ };
20541
+ }
20542
+ function buildRepoUrl(remoteUrl, isOnGitHub) {
20543
+ if (!isOnGitHub) return "";
20544
+ if (remoteUrl.startsWith("https://")) {
20545
+ return remoteUrl.replace(/\.git$/, "");
20546
+ }
20547
+ if (remoteUrl.startsWith("git@")) {
20548
+ const match = remoteUrl.match(/git@github\.com:(.+?)\.git$/);
20549
+ if (match) {
20550
+ return `https://github.com/${match[1]}`;
20551
+ }
20552
+ }
20553
+ return "";
20554
+ }
20555
+ function createServerStepTracker(threadId) {
20556
+ const steps = [];
20557
+ const requestStart = Date.now();
20558
+ async function step(name, fn) {
20559
+ const start = Date.now();
20560
+ try {
20561
+ return await fn();
20562
+ } finally {
20563
+ const ms = Date.now() - start;
20564
+ steps.push({ step: name, ms });
20565
+ console.log(`[gitops-timing] unified-status thread=${threadId} step=${name} ${ms}ms`);
20566
+ }
20567
+ }
20568
+ function record(name, start, extra) {
20569
+ const ms = Date.now() - start;
20570
+ steps.push({ step: name, ms });
20571
+ console.log(
20572
+ `[gitops-timing] unified-status thread=${threadId} step=${name} ${ms}ms${extra ? ` ${extra}` : ""}`
20573
+ );
20574
+ }
20575
+ function summary(extra) {
20576
+ const total = Date.now() - requestStart;
20577
+ const slowest = steps.reduce((best, current) => current.ms >= best.ms ? current : best, {
20578
+ step: "none",
20579
+ ms: 0
20580
+ });
20581
+ const breakdown = steps.map((s) => `${s.step}:${s.ms}`).join(" ");
20582
+ console.log(
20583
+ `[gitops-timing] unified-status thread=${threadId} summary total=${total}ms slowest=${slowest.step}@${slowest.ms}ms breakdown=[${breakdown}]${extra ? ` ${extra}` : ""}`
20584
+ );
20585
+ }
20586
+ return { step, record, summary };
20587
+ }
20588
+ async function gitUnifiedStatusHandler(c, _metadataManager, db) {
20164
20589
  try {
20165
20590
  const threadId = c.req.param("threadId");
20166
20591
  if (!threadId) {
@@ -20172,138 +20597,111 @@ async function gitUnifiedStatusHandler(c, metadataManager, db) {
20172
20597
  return c.json(response, statusCode);
20173
20598
  }
20174
20599
  const fresh = c.req.query("fresh") === "true";
20600
+ const tracker = createServerStepTracker(threadId);
20175
20601
  if (!fresh) {
20602
+ const cacheStart = Date.now();
20176
20603
  const cached = await getGitStatusCache(db, threadId);
20604
+ tracker.record("db-cache-read", cacheStart, `hit=${cached ? "true" : "false"}`);
20177
20605
  if (cached) {
20606
+ tracker.summary("cached=true");
20178
20607
  return c.json({ ...cached, cached: true });
20179
20608
  }
20180
20609
  }
20181
- const thread = await metadataManager.loadThreads().then((threads) => threads.find((t) => t.id === threadId));
20182
- if (!thread) {
20183
- return c.json({ error: "Thread not found" }, 404);
20184
- }
20185
- const repoPath = thread.path;
20186
- if (!repoPath) {
20187
- return c.json({ error: "Thread path not found" }, 404);
20188
- }
20189
- const absolutePath = resolveThreadPath(repoPath);
20190
- if (!existsSync20(absolutePath)) {
20191
- return c.json(
20192
- {
20193
- error: `Thread repo path does not exist: ${absolutePath}. Check that the project folder is present.`
20194
- },
20195
- 400
20196
- );
20197
- }
20198
- let gitRoot;
20199
- try {
20200
- gitRoot = await getGitRoot(absolutePath);
20201
- } catch (e) {
20202
- const msg = e instanceof Error ? e.message : String(e);
20203
- return c.json(
20204
- {
20205
- error: `Path is not a git repository: ${absolutePath}. ${msg}`
20206
- },
20207
- 400
20208
- );
20209
- }
20210
- const { hasChanges, changedFilesCount } = await new Promise((resolve6) => {
20211
- getGitStatus(gitRoot).then((statusOutput) => {
20212
- const lines = statusOutput.trim().split("\n").filter((line) => line.length > 0);
20213
- resolve6({
20214
- hasChanges: lines.length > 0,
20215
- changedFilesCount: lines.length
20216
- });
20217
- }).catch(() => resolve6({ hasChanges: false, changedFilesCount: 0 }));
20218
- });
20219
- const currentBranch = await getCurrentBranch(gitRoot);
20220
- const hasUnpushed = await hasUnpushedCommits(gitRoot, currentBranch);
20221
- const remoteUrl = await getRemoteOrigin(gitRoot);
20222
- const isOnGitHub = remoteUrl.includes("github.com");
20223
- let repoUrl = "";
20224
- if (isOnGitHub) {
20225
- if (remoteUrl.startsWith("https://")) {
20226
- repoUrl = remoteUrl.replace(/\.git$/, "");
20227
- } else if (remoteUrl.startsWith("git@")) {
20228
- const match = remoteUrl.match(/git@github\.com:(.+?)\.git$/);
20229
- if (match) {
20230
- repoUrl = `https://github.com/${match[1]}`;
20231
- }
20232
- }
20233
- }
20234
- let hasRemoteBranchExists = false;
20235
- if (remoteUrl && currentBranch && currentBranch !== "HEAD") {
20236
- try {
20237
- hasRemoteBranchExists = await hasRemoteBranch(gitRoot, currentBranch);
20238
- } catch (error) {
20239
- console.warn(
20240
- `Failed to check remote branch existence: ${error instanceof Error ? error.message : String(error)}`
20241
- );
20242
- hasRemoteBranchExists = false;
20243
- }
20244
- }
20245
- let defaultBranchName = null;
20246
- try {
20247
- defaultBranchName = await findDefaultBranch(gitRoot);
20248
- } catch (error) {
20249
- console.warn(
20250
- `Failed to find default branch: ${error instanceof Error ? error.message : String(error)}`
20251
- );
20252
- }
20253
- let commitsAheadOfDefault = false;
20254
- if (defaultBranchName && currentBranch && currentBranch !== defaultBranchName) {
20255
- try {
20256
- commitsAheadOfDefault = await hasCommitsAheadOfDefault(gitRoot, defaultBranchName);
20257
- } catch {
20258
- commitsAheadOfDefault = false;
20259
- }
20260
- }
20261
- let status = "Up to date";
20262
- if (isOnGitHub && remoteUrl) {
20610
+ return withGitTimingContext(threadId, async () => {
20611
+ const stopLagMonitor = startEventLoopLagMonitor(threadId);
20263
20612
  try {
20264
- if (currentBranch && currentBranch !== "HEAD") {
20265
- status = await getGitSyncStatus(gitRoot, currentBranch);
20613
+ const thread = await tracker.step("thread-lookup", () => getThread(db, threadId));
20614
+ if (!thread) {
20615
+ tracker.summary("error=thread-not-found");
20616
+ return c.json({ error: "Thread not found" }, 404);
20266
20617
  }
20267
- } catch (error) {
20268
- console.warn(
20269
- `Failed to check git sync status: ${error instanceof Error ? error.message : String(error)}`
20270
- );
20271
- status = "Up to date";
20272
- }
20273
- }
20274
- let prStatus = { exists: false };
20275
- if (isOnGitHub) {
20276
- try {
20277
- prStatus = await getPullRequestStatus(gitRoot);
20278
- } catch (error) {
20279
- console.warn(
20280
- `Failed to check PR status: ${error instanceof Error ? error.message : String(error)}`
20618
+ const repoPath = thread.path;
20619
+ if (!repoPath) {
20620
+ tracker.summary("error=thread-path-missing");
20621
+ return c.json({ error: "Thread path not found" }, 404);
20622
+ }
20623
+ const absolutePath = resolveThreadPath(repoPath);
20624
+ if (!existsSync20(absolutePath)) {
20625
+ tracker.summary("error=repo-path-missing");
20626
+ return c.json(
20627
+ {
20628
+ error: `Thread repo path does not exist: ${absolutePath}. Check that the project folder is present.`
20629
+ },
20630
+ 400
20631
+ );
20632
+ }
20633
+ let gitRoot;
20634
+ try {
20635
+ gitRoot = await tracker.step("resolve-git-root", () => getGitRoot(absolutePath));
20636
+ } catch (e) {
20637
+ const msg = e instanceof Error ? e.message : String(e);
20638
+ tracker.summary("error=not-git-repo");
20639
+ return c.json(
20640
+ {
20641
+ error: `Path is not a git repository: ${absolutePath}. ${msg}`
20642
+ },
20643
+ 400
20644
+ );
20645
+ }
20646
+ const [statusOutput, currentBranch, remoteUrl] = await Promise.all([
20647
+ tracker.step("getGitStatus", () => getGitStatus(gitRoot).catch(() => "")),
20648
+ tracker.step("getCurrentBranch", () => getCurrentBranch(gitRoot)),
20649
+ tracker.step("getRemoteOrigin", () => getRemoteOrigin(gitRoot))
20650
+ ]);
20651
+ const { hasChanges, changedFilesCount } = parseChangedFiles(statusOutput);
20652
+ const isOnGitHub = remoteUrl.includes("github.com");
20653
+ const repoUrl = buildRepoUrl(remoteUrl, isOnGitHub);
20654
+ const [hasUnpushed, hasRemoteBranchExists, defaultBranchName] = await Promise.all([
20655
+ tracker.step("hasUnpushedCommits", () => hasUnpushedCommits(gitRoot, currentBranch)),
20656
+ remoteUrl && currentBranch && currentBranch !== "HEAD" ? tracker.step(
20657
+ "hasRemoteBranch",
20658
+ () => hasRemoteBranch(gitRoot, currentBranch).catch(() => false)
20659
+ ) : Promise.resolve(false),
20660
+ tracker.step("findDefaultBranch", () => findDefaultBranch(gitRoot).catch(() => null))
20661
+ ]);
20662
+ const emptyPrStatus = { exists: false };
20663
+ const [commitsAheadOfDefault, status, prStatus] = await Promise.all([
20664
+ defaultBranchName && currentBranch && currentBranch !== defaultBranchName ? tracker.step(
20665
+ "hasCommitsAheadOfDefault",
20666
+ () => hasCommitsAheadOfDefault(gitRoot, defaultBranchName).catch(() => false)
20667
+ ) : Promise.resolve(false),
20668
+ isOnGitHub && remoteUrl && currentBranch && currentBranch !== "HEAD" ? tracker.step(
20669
+ "getGitSyncStatus",
20670
+ () => getGitSyncStatus(gitRoot, currentBranch, { fetchRemote: fresh }).catch(
20671
+ () => "Up to date"
20672
+ )
20673
+ ) : Promise.resolve("Up to date"),
20674
+ isOnGitHub ? tracker.step(
20675
+ "getPullRequestStatus",
20676
+ () => getPullRequestStatus(gitRoot).catch(() => emptyPrStatus)
20677
+ ) : Promise.resolve(emptyPrStatus)
20678
+ ]);
20679
+ const result = {
20680
+ hasChanges,
20681
+ changedFilesCount,
20682
+ currentBranch,
20683
+ hasUnpushedCommits: hasUnpushed,
20684
+ hasRemoteBranch: hasRemoteBranchExists,
20685
+ commitsAheadOfDefault,
20686
+ isOnGitHub,
20687
+ remoteUrl,
20688
+ canCreateRepo: !isOnGitHub && remoteUrl.length === 0,
20689
+ repoUrl,
20690
+ status,
20691
+ defaultBranch: defaultBranchName,
20692
+ prExists: prStatus.exists,
20693
+ prUrl: prStatus.url,
20694
+ prState: prStatus.state
20695
+ };
20696
+ await tracker.step("db-cache-write", () => setGitStatusCache(db, threadId, result));
20697
+ tracker.summary(
20698
+ `cached=false branch=${currentBranch || "none"} changes=${changedFilesCount} prExists=${prStatus.exists} fresh=${fresh}`
20281
20699
  );
20282
- prStatus = { exists: false };
20700
+ return c.json({ ...result, cached: false });
20701
+ } finally {
20702
+ stopLagMonitor();
20283
20703
  }
20284
- }
20285
- const result = {
20286
- // Git status fields
20287
- hasChanges,
20288
- changedFilesCount,
20289
- currentBranch,
20290
- hasUnpushedCommits: hasUnpushed,
20291
- hasRemoteBranch: hasRemoteBranchExists,
20292
- commitsAheadOfDefault,
20293
- // GitHub status fields
20294
- isOnGitHub,
20295
- remoteUrl,
20296
- canCreateRepo: !isOnGitHub && remoteUrl.length === 0,
20297
- repoUrl,
20298
- status,
20299
- defaultBranch: defaultBranchName,
20300
- // PR status fields
20301
- prExists: prStatus.exists,
20302
- prUrl: prStatus.url,
20303
- prState: prStatus.state
20304
- };
20305
- await setGitStatusCache(db, threadId, result);
20306
- return c.json({ ...result, cached: false });
20704
+ });
20307
20705
  } catch (error) {
20308
20706
  const message = error instanceof Error ? error.message : "Failed to get unified git status";
20309
20707
  return c.json({ error: message }, 500);
@@ -22197,12 +22595,23 @@ function createBrowserJsRoutes(threadManager) {
22197
22595
 
22198
22596
  // src/features/voice-model/voice-model.routes.ts
22199
22597
  import { Hono as Hono21 } from "hono";
22200
- var MODEL_URL = "https://install.tarsk.io/voice-models/ggml-tiny.en.bin";
22598
+ var VOICE_MODEL_URLS = {
22599
+ default: "https://install.tarsk.io/voice-models/ggml-tiny.en.bin",
22600
+ tiny: "https://install.tarsk.io/voice-models/ggml-tiny.en-q5_1.bin"
22601
+ };
22602
+ function getVoiceModelUrl(model) {
22603
+ if (model === "tiny") {
22604
+ return VOICE_MODEL_URLS.tiny;
22605
+ }
22606
+ return VOICE_MODEL_URLS.default;
22607
+ }
22201
22608
  function createVoiceModelRoutes() {
22202
22609
  const router = new Hono21();
22203
22610
  router.get("/download", async (c) => {
22611
+ const selectedModel = c.req.query("model");
22612
+ const modelUrl = getVoiceModelUrl(selectedModel);
22204
22613
  try {
22205
- const response = await fetch(MODEL_URL);
22614
+ const response = await fetch(modelUrl);
22206
22615
  if (!response.ok) {
22207
22616
  return errorResponse(
22208
22617
  c,
@@ -22213,7 +22622,8 @@ function createVoiceModelRoutes() {
22213
22622
  }
22214
22623
  const contentLength = response.headers.get("content-length");
22215
22624
  const headers = {
22216
- "Content-Type": "application/octet-stream"
22625
+ "Content-Type": "application/octet-stream",
22626
+ "X-Tarsk-Voice-Model": selectedModel === "tiny" ? "tiny" : "default"
22217
22627
  };
22218
22628
  if (contentLength) {
22219
22629
  headers["Content-Length"] = contentLength;
@@ -22275,13 +22685,82 @@ function createLogsRoutes() {
22275
22685
  return router;
22276
22686
  }
22277
22687
 
22688
+ // src/features/user-tasks/user-tasks.routes.ts
22689
+ init_database();
22690
+ import { Hono as Hono23 } from "hono";
22691
+
22692
+ // src/features/user-tasks/user-tasks.database.ts
22693
+ init_database();
22694
+ import { randomUUID as randomUUID11 } from "crypto";
22695
+ async function getUserTasksByThread(db, threadId) {
22696
+ const result = await db.execute({
22697
+ sql: `SELECT id, threadId, content, createdAt, updatedAt
22698
+ FROM user_tasks
22699
+ WHERE threadId = ?
22700
+ ORDER BY createdAt ASC`,
22701
+ args: [threadId]
22702
+ });
22703
+ return result.rows;
22704
+ }
22705
+ async function insertUserTask(threadId, content) {
22706
+ const db = await getDatabase();
22707
+ const id = randomUUID11();
22708
+ const now = (/* @__PURE__ */ new Date()).toISOString();
22709
+ await db.execute({
22710
+ sql: `INSERT INTO user_tasks (id, threadId, content, createdAt, updatedAt)
22711
+ VALUES (?, ?, ?, ?, ?)`,
22712
+ args: [id, threadId, content, now, now]
22713
+ });
22714
+ return {
22715
+ id,
22716
+ threadId,
22717
+ content,
22718
+ createdAt: now,
22719
+ updatedAt: now
22720
+ };
22721
+ }
22722
+ async function deleteUserTaskById(db, id) {
22723
+ await db.execute({
22724
+ sql: `DELETE FROM user_tasks WHERE id = ?`,
22725
+ args: [id]
22726
+ });
22727
+ }
22728
+
22729
+ // src/features/user-tasks/user-tasks.routes.ts
22730
+ function createUserTaskRoutes() {
22731
+ const router = new Hono23();
22732
+ router.get("/:threadId/user-tasks", async (c) => {
22733
+ const threadId = c.req.param("threadId");
22734
+ const db = await getDatabase();
22735
+ const tasks = await getUserTasksByThread(db, threadId);
22736
+ return c.json({ tasks });
22737
+ });
22738
+ router.post("/:threadId/user-tasks", async (c) => {
22739
+ const threadId = c.req.param("threadId");
22740
+ const body = await c.req.json();
22741
+ const content = body.content?.trim();
22742
+ if (!content) {
22743
+ return c.json({ error: "content is required" }, 400);
22744
+ }
22745
+ const task = await insertUserTask(threadId, content);
22746
+ return c.json(task, 201);
22747
+ });
22748
+ router.delete("/:threadId/user-tasks/:taskId", async (c) => {
22749
+ const taskId = c.req.param("taskId");
22750
+ const db = await getDatabase();
22751
+ await deleteUserTaskById(db, taskId);
22752
+ return c.json({ success: true });
22753
+ });
22754
+ return router;
22755
+ }
22756
+
22278
22757
  // src/server.ts
22279
22758
  var __filename = fileURLToPath(import.meta.url);
22280
22759
  var __dirname = path4.dirname(__filename);
22281
22760
  async function startTarskServer(options) {
22282
22761
  const { isDebug: isDebug2, publicDir: publicDirOverride } = options;
22283
22762
  const port = isDebug2 ? 462 : process.env.PORT ? parseInt(process.env.PORT) : 641;
22284
- const app = new Hono23();
22763
+ const app = new Hono24();
22285
22764
  app.use("/*", cors());
22286
22765
  app.use("/*", async (c, next) => {
22287
22766
  c.header("Cross-Origin-Opener-Policy", "same-origin");
@@ -22341,6 +22820,7 @@ async function startTarskServer(options) {
22341
22820
  app.route("/api/projects", createRunRoutes(projectManager));
22342
22821
  app.route("/api/projects", createProjectTodosRoutes(metadataManager));
22343
22822
  app.route("/api/threads", createThreadRoutes(threadManager, gitManager, conversationManager));
22823
+ app.route("/api/threads", createUserTaskRoutes());
22344
22824
  app.route(
22345
22825
  "/api/chat",
22346
22826
  createChatRoutes(threadManager, agentExecutor, conversationManager, processingStateManager)
@@ -22376,19 +22856,18 @@ async function startTarskServer(options) {
22376
22856
  `No static frontend assets found. Expected one of: ${prodPublicDir}, ${devCopiedPublicDir}, ${appDistDir}. Build the app first with \`cd ../app && bun run build\`.`
22377
22857
  );
22378
22858
  }
22379
- const staticRoot = path4.relative(process.cwd(), resolvedPublicDir);
22380
22859
  app.use("/*", async (c, next) => {
22381
22860
  if (c.req.path.startsWith("/api/")) {
22382
22861
  return next();
22383
22862
  }
22384
- return serveStatic({ root: staticRoot })(c, next);
22863
+ return serveStatic({ root: resolvedPublicDir })(c, next);
22385
22864
  });
22386
22865
  app.get("*", async (c, next) => {
22387
22866
  if (c.req.path.startsWith("/api/")) {
22388
22867
  return next();
22389
22868
  }
22390
22869
  return serveStatic({
22391
- path: path4.relative(process.cwd(), path4.join(resolvedPublicDir, "index.html"))
22870
+ path: path4.join(resolvedPublicDir, "index.html")
22392
22871
  })(c, next);
22393
22872
  });
22394
22873
  app.all("*", (c) => {