openmates 0.14.8-alpha.1 → 0.14.8-alpha.10

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.
@@ -4,7 +4,7 @@ import {
4
4
  } from "./chunk-AXNRPVLE.js";
5
5
 
6
6
  // src/client.ts
7
- import { randomUUID as randomUUID3 } from "crypto";
7
+ import { randomUUID as randomUUID4 } from "crypto";
8
8
  import { arch, platform as platform2, release } from "os";
9
9
  import { createInterface } from "readline/promises";
10
10
  import { stdin, stdout } from "process";
@@ -1214,6 +1214,31 @@ function websocketProtocolError(envelope) {
1214
1214
  }
1215
1215
  return null;
1216
1216
  }
1217
+ function errorFrameBelongsToAiResponse(envelope, userMessageId, chatId, recoveryTurnId) {
1218
+ if (envelope.type === "client_update_required") return true;
1219
+ if (envelope.type !== "error") return true;
1220
+ const payload = envelope.payload && typeof envelope.payload === "object" ? envelope.payload : {};
1221
+ let scoped = false;
1222
+ const errorChatId = typeof payload.chat_id === "string" ? payload.chat_id : null;
1223
+ if (errorChatId) {
1224
+ scoped = true;
1225
+ if (errorChatId !== chatId) return false;
1226
+ }
1227
+ const errorMessageId = typeof payload.user_message_id === "string" ? payload.user_message_id : typeof payload.userMessageId === "string" ? payload.userMessageId : typeof payload.message_id === "string" ? payload.message_id : null;
1228
+ if (errorMessageId) {
1229
+ scoped = true;
1230
+ if (errorMessageId !== userMessageId) return false;
1231
+ }
1232
+ const errorTurnId = typeof payload.turn_id === "string" ? payload.turn_id : null;
1233
+ if (errorTurnId) {
1234
+ scoped = true;
1235
+ if (recoveryTurnId !== errorTurnId) return false;
1236
+ }
1237
+ if (typeof payload.job_id === "string") {
1238
+ scoped = true;
1239
+ }
1240
+ return !scoped || Boolean(errorChatId || errorMessageId || errorTurnId);
1241
+ }
1217
1242
  function parseTaskProposals(value) {
1218
1243
  if (!Array.isArray(value)) return [];
1219
1244
  return value.flatMap((item) => {
@@ -1241,14 +1266,69 @@ function parseTaskUpdateProposals(value) {
1241
1266
  return [proposal];
1242
1267
  });
1243
1268
  }
1269
+ function parseTaskEvent(value) {
1270
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1271
+ const raw = value;
1272
+ if (typeof raw.event_id !== "string" || typeof raw.chat_id !== "string") return null;
1273
+ if (typeof raw.task_id !== "string" || typeof raw.event_type !== "string") return null;
1274
+ const event = {
1275
+ event_id: raw.event_id,
1276
+ chat_id: raw.chat_id,
1277
+ task_id: raw.task_id,
1278
+ event_type: raw.event_type
1279
+ };
1280
+ if (typeof raw.short_id === "string" || raw.short_id === null) event.short_id = raw.short_id;
1281
+ if (typeof raw.title === "string" || raw.title === null) event.title = raw.title;
1282
+ if (typeof raw.status === "string" || raw.status === null) event.status = raw.status;
1283
+ if (typeof raw.reason === "string" || raw.reason === null) event.reason = raw.reason;
1284
+ if (typeof raw.created_at === "number" || raw.created_at === null) event.created_at = raw.created_at;
1285
+ if (typeof raw.task_update_job_id === "string" || raw.task_update_job_id === null) event.task_update_job_id = raw.task_update_job_id;
1286
+ return event;
1287
+ }
1288
+ function parsePendingTaskUpdateJobs(value) {
1289
+ if (!Array.isArray(value)) return [];
1290
+ return value.flatMap((item) => {
1291
+ if (!item || typeof item !== "object" || Array.isArray(item)) return [];
1292
+ const raw = item;
1293
+ if (typeof raw.job_id !== "string" || typeof raw.task_id !== "string") return [];
1294
+ if (typeof raw.revision !== "number" || typeof raw.task_key_version !== "number" || typeof raw.expires_at !== "number") return [];
1295
+ const job = {
1296
+ job_id: raw.job_id,
1297
+ task_id: raw.task_id,
1298
+ revision: raw.revision,
1299
+ task_key_version: raw.task_key_version,
1300
+ expires_at: raw.expires_at
1301
+ };
1302
+ if (typeof raw.chat_id === "string" || raw.chat_id === null) job.chat_id = raw.chat_id;
1303
+ return [job];
1304
+ });
1305
+ }
1306
+ function parseAvailableRecoveryJobs(value) {
1307
+ if (!Array.isArray(value)) return [];
1308
+ return value.flatMap((item) => {
1309
+ if (!item || typeof item !== "object" || Array.isArray(item)) return [];
1310
+ const raw = item;
1311
+ if (typeof raw.job_id !== "string" || typeof raw.chat_id !== "string" || typeof raw.turn_id !== "string" || typeof raw.assistant_message_id !== "string" || typeof raw.chat_key_version !== "number") return [];
1312
+ return [{
1313
+ job_id: raw.job_id,
1314
+ chat_id: raw.chat_id,
1315
+ turn_id: raw.turn_id,
1316
+ assistant_message_id: raw.assistant_message_id,
1317
+ chat_key_version: raw.chat_key_version
1318
+ }];
1319
+ });
1320
+ }
1244
1321
  var OpenMatesWsClient = class {
1245
1322
  socket;
1323
+ passiveTaskUpdateJobs = /* @__PURE__ */ new Map();
1324
+ activeResponseCollectors = 0;
1246
1325
  constructor(options) {
1247
1326
  const wsBase = options.apiUrl.replace(/^http/, "ws").replace(/\/$/, "");
1248
1327
  const token = options.wsToken || options.refreshToken || "";
1249
1328
  const query = new URLSearchParams({
1250
1329
  sessionId: options.sessionId,
1251
- token
1330
+ token,
1331
+ client_capabilities: "task_update_jobs"
1252
1332
  });
1253
1333
  const wsHeaders = {};
1254
1334
  if (options.userAgent) {
@@ -1263,6 +1343,10 @@ var OpenMatesWsClient = class {
1263
1343
  this.socket = new WebSocket(`${wsBase}/v1/ws?${query.toString()}`, {
1264
1344
  headers: wsHeaders
1265
1345
  });
1346
+ this.socket.on("message", (rawData) => {
1347
+ if (this.activeResponseCollectors > 0) return;
1348
+ this.bufferPassiveTaskUpdateJobs(rawData);
1349
+ });
1266
1350
  }
1267
1351
  async open(timeoutMs = 1e4) {
1268
1352
  await new Promise((resolve7, reject) => {
@@ -1308,6 +1392,22 @@ var OpenMatesWsClient = class {
1308
1392
  });
1309
1393
  });
1310
1394
  }
1395
+ bufferPassiveTaskUpdateJobs(rawData) {
1396
+ try {
1397
+ const parsed = JSON.parse(rawData.toString());
1398
+ if (parsed.type !== "task_update_jobs_available") return;
1399
+ const payload = parsed.payload ?? {};
1400
+ for (const job of parsePendingTaskUpdateJobs(payload.jobs)) {
1401
+ this.passiveTaskUpdateJobs.set(job.job_id, job);
1402
+ }
1403
+ } catch {
1404
+ }
1405
+ }
1406
+ drainPassiveTaskUpdateJobs() {
1407
+ const jobs = [...this.passiveTaskUpdateJobs.values()];
1408
+ this.passiveTaskUpdateJobs.clear();
1409
+ return jobs;
1410
+ }
1311
1411
  waitForMessage(expectedType, predicate, timeoutMs = 2e4) {
1312
1412
  return new Promise((resolve7, reject) => {
1313
1413
  const seenTypes = /* @__PURE__ */ new Set();
@@ -1443,6 +1543,14 @@ var OpenMatesWsClient = class {
1443
1543
  let newChatSuggestions = [];
1444
1544
  let taskProposals = [];
1445
1545
  let taskUpdateProposals = [];
1546
+ const taskEvents = [];
1547
+ this.activeResponseCollectors += 1;
1548
+ let pendingTaskUpdateJobs = this.drainPassiveTaskUpdateJobs();
1549
+ const mergePendingTaskUpdateJobs = (jobs) => {
1550
+ const byId = new Map(pendingTaskUpdateJobs.map((job) => [job.job_id, job]));
1551
+ for (const job of jobs) byId.set(job.job_id, job);
1552
+ pendingTaskUpdateJobs = [...byId.values()];
1553
+ };
1446
1554
  const subChatEvents = [];
1447
1555
  const pendingSubChatHandlers = /* @__PURE__ */ new Set();
1448
1556
  const pendingMemoryRequestHandlers = /* @__PURE__ */ new Set();
@@ -1505,6 +1613,8 @@ var OpenMatesWsClient = class {
1505
1613
  newChatSuggestions,
1506
1614
  taskProposals,
1507
1615
  taskUpdateProposals,
1616
+ taskEvents,
1617
+ pendingTaskUpdateJobs,
1508
1618
  embeds: [...embeds.values()],
1509
1619
  subChatEvents,
1510
1620
  recoveryJobId
@@ -1512,6 +1622,7 @@ var OpenMatesWsClient = class {
1512
1622
  return;
1513
1623
  }
1514
1624
  if (!aiResponseDone || !postProcessingDone) return;
1625
+ if (options?.recoveryTurnId && !recoveryJobId) return;
1515
1626
  if (pendingSubChatHandlers.size > 0) return;
1516
1627
  if (pendingMemoryRequestHandlers.size > 0) return;
1517
1628
  if (processingEmbedIds.size > 0 && !asyncEmbedTimer) {
@@ -1528,6 +1639,8 @@ var OpenMatesWsClient = class {
1528
1639
  newChatSuggestions,
1529
1640
  taskProposals,
1530
1641
  taskUpdateProposals,
1642
+ taskEvents,
1643
+ pendingTaskUpdateJobs,
1531
1644
  embeds: [...embeds.values()],
1532
1645
  subChatEvents,
1533
1646
  recoveryJobId
@@ -1548,6 +1661,8 @@ var OpenMatesWsClient = class {
1548
1661
  newChatSuggestions,
1549
1662
  taskProposals,
1550
1663
  taskUpdateProposals,
1664
+ taskEvents,
1665
+ pendingTaskUpdateJobs,
1551
1666
  embeds: [...embeds.values()],
1552
1667
  subChatEvents,
1553
1668
  recoveryJobId
@@ -1624,6 +1739,9 @@ var OpenMatesWsClient = class {
1624
1739
  const type = parsed.type;
1625
1740
  const protocolError = websocketProtocolError(parsed);
1626
1741
  if (protocolError) {
1742
+ if (!errorFrameBelongsToAiResponse(parsed, userMessageId, chatId, options?.recoveryTurnId)) {
1743
+ return;
1744
+ }
1627
1745
  cleanup();
1628
1746
  reject(protocolError);
1629
1747
  return;
@@ -1654,6 +1772,29 @@ var OpenMatesWsClient = class {
1654
1772
  }
1655
1773
  return;
1656
1774
  }
1775
+ if (type === "task_event") {
1776
+ const event = parseTaskEvent(p);
1777
+ if (!event || event.chat_id !== chatId) return;
1778
+ taskEvents.push(event);
1779
+ maybeResolve();
1780
+ return;
1781
+ }
1782
+ if (type === "task_update_jobs_available") {
1783
+ mergePendingTaskUpdateJobs(parsePendingTaskUpdateJobs(p.jobs));
1784
+ maybeResolve();
1785
+ return;
1786
+ }
1787
+ if (type === "recovery_jobs_available") {
1788
+ const job = parseAvailableRecoveryJobs(p.jobs).find(
1789
+ (candidate) => candidate.chat_id === chatId && (!options?.recoveryTurnId || candidate.turn_id === options.recoveryTurnId)
1790
+ );
1791
+ if (!job) return;
1792
+ recoveryJobId = job.job_id;
1793
+ messageId = job.assistant_message_id;
1794
+ if (aiResponseDone) maybeResolve();
1795
+ else scheduleResolve(latestContent);
1796
+ return;
1797
+ }
1657
1798
  if (type === "ai_message_update") {
1658
1799
  const msgId = p.user_message_id ?? p.userMessageId;
1659
1800
  if (msgId !== userMessageId && p.chat_id !== chatId) return;
@@ -1764,6 +1905,8 @@ var OpenMatesWsClient = class {
1764
1905
  newChatSuggestions,
1765
1906
  taskProposals,
1766
1907
  taskUpdateProposals,
1908
+ taskEvents,
1909
+ pendingTaskUpdateJobs,
1767
1910
  embeds: [...embeds.values()],
1768
1911
  subChatEvents,
1769
1912
  recoveryJobId
@@ -1786,6 +1929,7 @@ var OpenMatesWsClient = class {
1786
1929
  this.socket.off("message", onMessage);
1787
1930
  this.socket.off("error", onError);
1788
1931
  this.socket.off("close", onClose);
1932
+ this.activeResponseCollectors = Math.max(0, this.activeResponseCollectors - 1);
1789
1933
  };
1790
1934
  this.socket.on("message", onMessage);
1791
1935
  this.socket.on("error", onError);
@@ -2514,6 +2658,227 @@ function toArrayBuffer3(input) {
2514
2658
  return output;
2515
2659
  }
2516
2660
 
2661
+ // src/tasksCli.ts
2662
+ import { createHash as createHash5, randomBytes as randomBytes2, randomUUID as randomUUID3 } from "crypto";
2663
+ var TASK_STATUSES2 = ["backlog", "todo", "in_progress", "blocked", "done"];
2664
+ var DEFAULT_STANDALONE_PREFIX = "TASK";
2665
+ function normalizeTaskStatus(value) {
2666
+ if (value === void 0) return void 0;
2667
+ if (TASK_STATUSES2.includes(value)) return value;
2668
+ throw new Error(`Unknown task status '${value}'. Expected one of: ${TASK_STATUSES2.join(", ")}`);
2669
+ }
2670
+ function parseAssignee(value) {
2671
+ if (!value || value === "user") return { assigneeType: "user", assigneeHash: null };
2672
+ if (["ai", "openmates", "OpenMates"].includes(value)) return { assigneeType: "ai", assigneeHash: null };
2673
+ return { assigneeType: "user", assigneeHash: value };
2674
+ }
2675
+ function splitCsvFlag(value) {
2676
+ if (typeof value !== "string") return [];
2677
+ return value.split(/[,\n]/).map((item) => item.trim()).filter(Boolean);
2678
+ }
2679
+ function parseDueAt(value) {
2680
+ if (value === void 0) return void 0;
2681
+ if (value === false || value === true) throw new Error("--due requires a timestamp or date value.");
2682
+ const numeric = Number(value);
2683
+ if (Number.isFinite(numeric) && numeric > 0) return Math.floor(numeric);
2684
+ const parsed = Date.parse(value);
2685
+ if (Number.isNaN(parsed)) throw new Error(`Invalid --due value '${value}'.`);
2686
+ return Math.floor(parsed / 1e3);
2687
+ }
2688
+ async function buildCreateUserTaskInput(masterKey, input) {
2689
+ const taskKey = randomBytes2(32);
2690
+ const encryptedTaskKey = await encryptBytesWithAesGcm(taskKey, masterKey);
2691
+ const timestamp = nowSeconds();
2692
+ const assignee = parseAssignee(input.assign);
2693
+ const linkedProjectIds = input.projectIds ?? [];
2694
+ const status = input.status ?? (assignee.assigneeType === "ai" && !input.dueAt ? "in_progress" : "todo");
2695
+ return {
2696
+ task_id: randomUUIDCompat(),
2697
+ short_id: void 0,
2698
+ version: 1,
2699
+ encrypted_task_key: encryptedTaskKey,
2700
+ encrypted_title: await encryptWithAesGcmCombined(input.title, taskKey),
2701
+ encrypted_description: await encryptWithAesGcmCombined(input.description ?? "", taskKey),
2702
+ encrypted_tags: await encryptWithAesGcmCombined("[]", taskKey),
2703
+ encrypted_linked_project_ids: await encryptWithAesGcmCombined(JSON.stringify(linkedProjectIds), taskKey),
2704
+ status,
2705
+ assignee_type: assignee.assigneeType,
2706
+ assignee_hash: assignee.assigneeHash,
2707
+ primary_chat_id: input.chatId ?? null,
2708
+ linked_project_ids: linkedProjectIds,
2709
+ plan_id: input.planId ?? null,
2710
+ due_at: input.dueAt ?? null,
2711
+ priority: 0,
2712
+ position: timestamp,
2713
+ created_at: timestamp,
2714
+ updated_at: timestamp
2715
+ };
2716
+ }
2717
+ async function buildUpdateUserTaskInput(task, masterKey, input) {
2718
+ const taskKey = await taskKeyFromRecord(task.encrypted, masterKey);
2719
+ const patch = { version: task.version, updated_at: nowSeconds() };
2720
+ if (input.title !== void 0) patch.encrypted_title = await encryptWithAesGcmCombined(input.title, taskKey);
2721
+ if (input.description !== void 0) patch.encrypted_description = await encryptWithAesGcmCombined(input.description, taskKey);
2722
+ if (input.status !== void 0) patch.status = input.status;
2723
+ if (input.assign !== void 0) {
2724
+ const assignee = parseAssignee(input.assign);
2725
+ patch.assignee_type = assignee.assigneeType;
2726
+ patch.assignee_hash = assignee.assigneeHash;
2727
+ }
2728
+ if (input.chatId !== void 0) patch.primary_chat_id = input.chatId;
2729
+ if (input.projectIds !== void 0) {
2730
+ patch.linked_project_ids = input.projectIds;
2731
+ patch.encrypted_linked_project_ids = await encryptWithAesGcmCombined(JSON.stringify(input.projectIds), taskKey);
2732
+ }
2733
+ if (input.planId !== void 0) patch.plan_id = input.planId;
2734
+ return patch;
2735
+ }
2736
+ async function decryptUserTask(record, masterKey) {
2737
+ if (typeof record.version !== "number") throw new Error(`Task ${record.task_id} is missing version.`);
2738
+ const taskKey = await taskKeyFromRecord(record, masterKey);
2739
+ const tags = parseStringArray(await decryptOptional(record.encrypted_tags, taskKey));
2740
+ const linkedProjectIds = parseStringArray(await decryptOptional(record.encrypted_linked_project_ids, taskKey));
2741
+ return {
2742
+ taskId: record.task_id,
2743
+ shortId: record.short_id || deriveShortId(record),
2744
+ title: await decryptOptional(record.encrypted_title, taskKey) || "(untitled task)",
2745
+ description: await decryptOptional(record.encrypted_description, taskKey),
2746
+ tags,
2747
+ latestInstruction: await decryptOptional(record.encrypted_latest_instruction, taskKey),
2748
+ status: record.status,
2749
+ assigneeType: record.assignee_type,
2750
+ assigneeHash: record.assignee_hash ?? null,
2751
+ primaryChatId: record.primary_chat_id ?? null,
2752
+ linkedProjectIds: linkedProjectIds.length > 0 ? linkedProjectIds : record.linked_project_ids ?? [],
2753
+ planId: record.plan_id ?? null,
2754
+ dueAt: record.due_at ?? null,
2755
+ priority: record.priority ?? 0,
2756
+ position: record.position ?? 0,
2757
+ queueState: record.queue_state ?? "none",
2758
+ blockedReasonCode: record.blocked_reason_code ?? null,
2759
+ aiExecutionState: record.ai_execution_state ?? null,
2760
+ version: record.version,
2761
+ encrypted: record
2762
+ };
2763
+ }
2764
+ async function decryptUserTasks(records, masterKey) {
2765
+ const output = [];
2766
+ for (const record of records) output.push(await decryptUserTask(record, masterKey));
2767
+ return output;
2768
+ }
2769
+ function findTask(tasks, id) {
2770
+ const taskIdMatch = tasks.find((candidate) => candidate.taskId === id);
2771
+ if (taskIdMatch) return taskIdMatch;
2772
+ const shortIdMatches = tasks.filter((candidate) => candidate.shortId === id);
2773
+ if (shortIdMatches.length > 1) throw new Error(`Task '${id}' is ambiguous in the current task list. Use the full task ID.`);
2774
+ const task = shortIdMatches[0];
2775
+ if (!task) throw new Error(`Task '${id}' was not found in the current task list.`);
2776
+ return task;
2777
+ }
2778
+ function renderTaskList(tasks) {
2779
+ if (tasks.length === 0) return "No tasks found.";
2780
+ const lines = ["Tasks", "ID Status Assignee Queue Title"];
2781
+ for (const task of tasks) {
2782
+ lines.push(`${pad(task.shortId, 9)} ${pad(task.status, 12)} ${pad(assigneeLabel(task), 11)} ${pad(task.queueState, 11)} ${task.title}`);
2783
+ }
2784
+ return lines.join("\n");
2785
+ }
2786
+ function renderTaskDetail(task) {
2787
+ const lines = [
2788
+ `Task ${task.shortId}`,
2789
+ `Title: ${task.title}`,
2790
+ `Status: ${task.status}`,
2791
+ `Assignee: ${assigneeLabel(task)}`,
2792
+ `Queue: ${task.queueState}`,
2793
+ `Task ID: ${task.taskId}`
2794
+ ];
2795
+ if (task.description) lines.push(`Description: ${task.description}`);
2796
+ if (task.primaryChatId) lines.push(`Chat: ${task.primaryChatId}`);
2797
+ if (task.linkedProjectIds.length > 0) lines.push(`Projects: ${task.linkedProjectIds.join(", ")}`);
2798
+ if (task.planId) lines.push(`Plan: ${task.planId}`);
2799
+ if (task.blockedReasonCode) lines.push(`Blocked reason: ${task.blockedReasonCode}`);
2800
+ if (task.aiExecutionState) lines.push(`AI state: ${task.aiExecutionState}`);
2801
+ return lines.join("\n");
2802
+ }
2803
+ function renderTaskBoard(tasks, width = process.stdout.columns || 100) {
2804
+ const columns = TASK_STATUSES2.map((status) => ({ status, tasks: tasks.filter((task) => task.status === status).sort(compareTasks) }));
2805
+ if (width < 96) {
2806
+ const lines2 = ["OpenMates Tasks Board"];
2807
+ for (const column of columns) {
2808
+ lines2.push("", `${columnTitle(column.status)} (${column.tasks.length})`, "-".repeat(24));
2809
+ lines2.push(...boardColumnLines(column.tasks, 8));
2810
+ }
2811
+ return lines2.join("\n");
2812
+ }
2813
+ const perColumn = 6;
2814
+ const columnWidth = 22;
2815
+ const lines = ["OpenMates Tasks Board", ""];
2816
+ lines.push(columns.map((column) => pad(`${columnTitle(column.status)} (${column.tasks.length})`, columnWidth)).join(" "));
2817
+ lines.push(columns.map(() => "-".repeat(columnWidth)).join(" "));
2818
+ const renderedColumns = columns.map((column) => boardColumnLines(column.tasks, perColumn).map((line) => truncate(line, columnWidth)));
2819
+ const maxRows = Math.max(...renderedColumns.map((column) => column.length));
2820
+ for (let row = 0; row < maxRows; row += 1) {
2821
+ lines.push(renderedColumns.map((column) => pad(column[row] ?? "", columnWidth)).join(" "));
2822
+ }
2823
+ return lines.join("\n");
2824
+ }
2825
+ function boardColumnLines(tasks, limit) {
2826
+ if (tasks.length === 0) return ["No tasks here."];
2827
+ const visible = tasks.slice(0, limit).flatMap((task) => [
2828
+ `[${task.shortId}] ${task.title}`,
2829
+ ` ${assigneeLabel(task)} ${task.queueState === "none" ? "" : task.queueState}`.trimEnd()
2830
+ ]);
2831
+ if (tasks.length > limit) visible.push(`... ${tasks.length - limit} more`);
2832
+ return visible;
2833
+ }
2834
+ function compareTasks(a, b) {
2835
+ return a.position - b.position || a.title.localeCompare(b.title);
2836
+ }
2837
+ function columnTitle(status) {
2838
+ if (status === "in_progress") return "In progress";
2839
+ return status.slice(0, 1).toUpperCase() + status.slice(1).replace("_", " ");
2840
+ }
2841
+ function assigneeLabel(task) {
2842
+ return task.assigneeType === "ai" ? "OpenMates" : task.assigneeHash ?? "user";
2843
+ }
2844
+ async function taskKeyFromRecord(record, masterKey) {
2845
+ if (!record.encrypted_task_key) throw new Error(`Task ${record.task_id} is missing encrypted task key.`);
2846
+ const taskKey = await decryptBytesWithAesGcm(record.encrypted_task_key, masterKey);
2847
+ if (!taskKey) throw new Error(`Failed to decrypt task key for ${record.task_id}.`);
2848
+ return taskKey;
2849
+ }
2850
+ async function decryptOptional(value, key) {
2851
+ if (!value) return "";
2852
+ return await decryptWithAesGcmCombined(value, key) ?? "";
2853
+ }
2854
+ function parseStringArray(value) {
2855
+ if (!value) return [];
2856
+ try {
2857
+ const parsed = JSON.parse(value);
2858
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
2859
+ } catch {
2860
+ return [];
2861
+ }
2862
+ }
2863
+ function deriveShortId(record) {
2864
+ const prefix = record.short_id_prefix || DEFAULT_STANDALONE_PREFIX;
2865
+ const source = record.task_id || `${record.created_at ?? ""}-${record.position ?? ""}`;
2866
+ const digest = createHash5("sha256").update(source).digest("hex").slice(0, 4).toUpperCase();
2867
+ return `${prefix}-${parseInt(digest, 16) % 1e4}`;
2868
+ }
2869
+ function nowSeconds() {
2870
+ return Math.floor(Date.now() / 1e3);
2871
+ }
2872
+ function randomUUIDCompat() {
2873
+ return randomUUID3();
2874
+ }
2875
+ function pad(value, length) {
2876
+ return truncate(value, length).padEnd(length);
2877
+ }
2878
+ function truncate(value, length) {
2879
+ return value.length <= length ? value : `${value.slice(0, Math.max(0, length - 3))}...`;
2880
+ }
2881
+
2517
2882
  // src/client.ts
2518
2883
  function normalizeUnixSeconds(value, fallback) {
2519
2884
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
@@ -2681,6 +3046,83 @@ function buildAppSettingsMemoryResponseSystemMessage(params) {
2681
3046
  user_message_id: params.userMessageId
2682
3047
  };
2683
3048
  }
3049
+ async function buildTaskEventSystemMessage(params) {
3050
+ const content = formatTaskEventSystemContent(params.event);
3051
+ const message = {
3052
+ message_id: `task-event-${params.event.event_id}`,
3053
+ role: "system",
3054
+ encrypted_content: await encryptWithAesGcmCombined(content, params.chatKey),
3055
+ created_at: params.event.created_at ?? Math.floor(Date.now() / 1e3),
3056
+ user_message_id: params.userMessageId
3057
+ };
3058
+ if (params.event.task_update_job_id) message.task_update_job_id = params.event.task_update_job_id;
3059
+ return message;
3060
+ }
3061
+ function taskUpdateJobBelongsToActiveTurn(job, activeChatId, taskEvents) {
3062
+ void activeChatId;
3063
+ return taskEvents.some((event) => event.task_update_job_id === job.job_id);
3064
+ }
3065
+ function buildTaskUpdateJobPersistPayload(params) {
3066
+ const encryptedTaskPayload = pruneAbsentTaskPersistFields(params.encryptedTaskPayload);
3067
+ assertTaskPersistPayloadEncrypted(encryptedTaskPayload);
3068
+ return {
3069
+ protocol_version: 1,
3070
+ job_id: params.jobId,
3071
+ lease_token: params.leaseToken,
3072
+ lease_generation: params.leaseGeneration,
3073
+ expected_task_version: params.expectedTaskVersion,
3074
+ encrypted_task_payload: encryptedTaskPayload,
3075
+ encrypted_task_event_message: params.encryptedTaskEventMessage ?? null
3076
+ };
3077
+ }
3078
+ function pruneAbsentTaskPersistFields(payload) {
3079
+ return Object.fromEntries(
3080
+ Object.entries(payload).filter(([, value]) => value !== void 0 && value !== null)
3081
+ );
3082
+ }
3083
+ function formatTaskEventSystemContent(event) {
3084
+ const taskLabel = event.short_id || event.task_id;
3085
+ const title = event.title ? ` "${event.title}"` : "";
3086
+ const status = event.status ? ` (${event.status})` : "";
3087
+ const reason = event.reason ? `: ${event.reason}` : "";
3088
+ switch (event.event_type) {
3089
+ case "created":
3090
+ return `${taskLabel} created${title}${status}`;
3091
+ case "updated":
3092
+ return `${taskLabel} updated${title}${status}`;
3093
+ case "blocked":
3094
+ return `${taskLabel} blocked${reason}`;
3095
+ case "completed":
3096
+ return `${taskLabel} completed${title}`;
3097
+ case "unblocked":
3098
+ return `${taskLabel} unblocked`;
3099
+ default:
3100
+ return `${taskLabel} ${event.event_type}${title}${status}${reason}`;
3101
+ }
3102
+ }
3103
+ function assertTaskPersistPayloadEncrypted(payload) {
3104
+ const allowedSafeKeys = /* @__PURE__ */ new Set([
3105
+ "assignee_hash",
3106
+ "assignee_type",
3107
+ "blocked_reason_code",
3108
+ "created_at",
3109
+ "key_wrappers",
3110
+ "linked_project_ids",
3111
+ "position",
3112
+ "primary_chat_id",
3113
+ "priority",
3114
+ "status",
3115
+ "task_id",
3116
+ "updated_at",
3117
+ "version"
3118
+ ]);
3119
+ for (const key of Object.keys(payload)) {
3120
+ if (key.startsWith("encrypted_") || allowedSafeKeys.has(key)) {
3121
+ continue;
3122
+ }
3123
+ throw new Error("Task update job payload contains plaintext or unsupported field");
3124
+ }
3125
+ }
2684
3126
  async function buildSubChatEncryptedMetadataPayloads(params) {
2685
3127
  const createdAt = params.createdAt ?? Math.floor(Date.now() / 1e3);
2686
3128
  const payloads = [];
@@ -3354,11 +3796,11 @@ var OpenMatesClient = class _OpenMatesClient {
3354
3796
  }
3355
3797
  let anonymousId = loadAnonymousId();
3356
3798
  if (!anonymousId) {
3357
- anonymousId = randomUUID3();
3799
+ anonymousId = randomUUID4();
3358
3800
  saveAnonymousId(anonymousId);
3359
3801
  }
3360
- const chatId = `anonymous-${randomUUID3()}`;
3361
- const messageId = `anonymous-message-${randomUUID3()}`;
3802
+ const chatId = `anonymous-${randomUUID4()}`;
3803
+ const messageId = `anonymous-message-${randomUUID4()}`;
3362
3804
  const requestBody = {
3363
3805
  anonymous_id: anonymousId,
3364
3806
  client_chat_id: chatId,
@@ -3552,7 +3994,7 @@ var OpenMatesClient = class _OpenMatesClient {
3552
3994
  pin: pin.trim().toUpperCase(),
3553
3995
  token
3554
3996
  });
3555
- const sessionId = randomUUID3();
3997
+ const sessionId = randomUUID4();
3556
3998
  const login = await this.http.post(
3557
3999
  "/v1/auth/login",
3558
4000
  {
@@ -3727,7 +4169,7 @@ var OpenMatesClient = class _OpenMatesClient {
3727
4169
  }
3728
4170
  const session = {
3729
4171
  apiUrl: this.apiUrl,
3730
- sessionId: randomUUID3(),
4172
+ sessionId: randomUUID4(),
3731
4173
  wsToken: null,
3732
4174
  cookies: this.http.getCookieMap(),
3733
4175
  masterKeyExportedB64: material.masterKeyB64,
@@ -3887,7 +4329,7 @@ var OpenMatesClient = class _OpenMatesClient {
3887
4329
  if (!markdown) throw new Error("Draft markdown must not be empty.");
3888
4330
  const masterKey = this.getMasterKeyBytes();
3889
4331
  const encrypted = await this.saveEncryptedDraft({
3890
- chatId: params.chatId ?? randomUUID3(),
4332
+ chatId: params.chatId ?? randomUUID4(),
3891
4333
  encryptedDraftMd: await encryptWithAesGcmCombined(markdown, masterKey),
3892
4334
  encryptedDraftPreview: params.preview ? await encryptWithAesGcmCombined(params.preview, masterKey) : null
3893
4335
  });
@@ -4443,7 +4885,7 @@ var OpenMatesClient = class _OpenMatesClient {
4443
4885
  async sendMessage(params) {
4444
4886
  let chatId;
4445
4887
  if (!params.chatId) {
4446
- chatId = randomUUID3();
4888
+ chatId = randomUUID4();
4447
4889
  } else if (params.chatId.length < 36) {
4448
4890
  const resolved = await this.resolveFullChatId(params.chatId);
4449
4891
  if (!resolved) {
@@ -4477,7 +4919,7 @@ var OpenMatesClient = class _OpenMatesClient {
4477
4919
  ws.close();
4478
4920
  throw new Error("Authenticated user identity is required for saved chat recovery.");
4479
4921
  }
4480
- const messageId = randomUUID3();
4922
+ const messageId = randomUUID4();
4481
4923
  const createdAt = Math.floor(Date.now() / 1e3);
4482
4924
  const isNewChat = !params.chatId;
4483
4925
  ws.send("set_active_chat", { chat_id: chatId });
@@ -4528,6 +4970,7 @@ var OpenMatesClient = class _OpenMatesClient {
4528
4970
  }
4529
4971
  const messagePayload = {
4530
4972
  chat_id: chatId,
4973
+ client_capabilities: ["task_update_jobs"],
4531
4974
  is_incognito: Boolean(params.incognito),
4532
4975
  message: {
4533
4976
  message_id: messageId,
@@ -4611,11 +5054,11 @@ var OpenMatesClient = class _OpenMatesClient {
4611
5054
  if (encryptedEmbeds.length > 0) {
4612
5055
  messagePayload.encrypted_embeds = encryptedEmbeds;
4613
5056
  }
4614
- let precollectedResponse = params.precollectResponse && params.incognito ? ws.collectAiResponse(messageId, chatId, { onStream: params.onStream }) : null;
5057
+ let precollectedResponse = params.precollectResponse && params.incognito ? ws.collectAiResponse(messageId, chatId, { onStream: params.onStream, timeoutMs: params.responseTimeoutMs }) : null;
4615
5058
  if (!params.incognito && chatKeyBytes && encryptedChatKey) {
4616
5059
  const protocolVersion = 1;
4617
5060
  const chatKeyVersion = 1;
4618
- const turnId = randomUUID3();
5061
+ const turnId = randomUUID4();
4619
5062
  savedTurnId = turnId;
4620
5063
  terminalExpectedMessagesV = baselineMessagesV + 1;
4621
5064
  const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
@@ -4689,7 +5132,9 @@ var OpenMatesClient = class _OpenMatesClient {
4689
5132
  }
4690
5133
  if (params.precollectResponse && !params.incognito) {
4691
5134
  precollectedResponse = ws.collectAiResponse(messageId, chatId, {
4692
- onStream: params.onStream
5135
+ onStream: params.onStream,
5136
+ timeoutMs: params.responseTimeoutMs,
5137
+ recoveryTurnId: savedTurnId
4693
5138
  });
4694
5139
  }
4695
5140
  const confirmed = ws.waitForMessage(
@@ -4712,6 +5157,8 @@ var OpenMatesClient = class _OpenMatesClient {
4712
5157
  let followUpSuggestions = [];
4713
5158
  let taskProposals = [];
4714
5159
  let taskUpdateProposals = [];
5160
+ let taskEvents = [];
5161
+ let pendingTaskUpdateJobs = [];
4715
5162
  let subChatEvents = [];
4716
5163
  const appSettingsMemoryRequests = [];
4717
5164
  const numberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
@@ -4874,6 +5321,225 @@ var OpenMatesClient = class _OpenMatesClient {
4874
5321
  `The assistant requested memories (${event.requestedKeys.join(", ")}). Rerun with --auto-approve-memories to explicitly approve requested memory categories from the CLI, or continue this chat in the web app to approve or reject the request.`
4875
5322
  );
4876
5323
  };
5324
+ const persistEncryptedSystemMessage = async (systemMessage, targetChatId = chatId) => {
5325
+ await ws.sendAsync("chat_system_message_added", {
5326
+ chat_id: targetChatId,
5327
+ message: systemMessage
5328
+ });
5329
+ await ws.waitForMessage(
5330
+ "system_message_confirmed",
5331
+ (payload) => payload.message_id === systemMessage.message_id,
5332
+ 2e4
5333
+ );
5334
+ };
5335
+ const persistedTaskEventIds = /* @__PURE__ */ new Set();
5336
+ const persistTaskEventSystemMessages = async (events) => {
5337
+ if (!chatKeyBytes || events.length === 0) return;
5338
+ for (const event of events) {
5339
+ if (persistedTaskEventIds.has(event.event_id)) continue;
5340
+ await persistEncryptedSystemMessage(await buildTaskEventSystemMessage({
5341
+ chatKey: chatKeyBytes,
5342
+ userMessageId: messageId,
5343
+ event
5344
+ }));
5345
+ persistedTaskEventIds.add(event.event_id);
5346
+ }
5347
+ };
5348
+ const persistPendingTaskUpdateJobs = async (jobs, events) => {
5349
+ const handledJobIds = /* @__PURE__ */ new Set();
5350
+ if (jobs.length === 0) return handledJobIds;
5351
+ const masterKey = this.getMasterKeyBytes();
5352
+ let decryptedTasksCache = null;
5353
+ const eventByJobId = new Map(events.map((event) => [event.task_update_job_id, event]));
5354
+ const buildTaskKeyWrappersForChat = async (task, targetChatId, createdAt2) => {
5355
+ const encryptedTaskKey = task.encrypted.encrypted_task_key;
5356
+ if (!encryptedTaskKey) throw new Error(`Task ${task.taskId} is missing encrypted task key.`);
5357
+ const taskKey = await decryptBytesWithAesGcm(encryptedTaskKey, masterKey);
5358
+ if (!taskKey) throw new Error(`Failed to decrypt task key for ${task.taskId}.`);
5359
+ const targetChatKey = await resolveChatKey(targetChatId);
5360
+ const existingProjectWrappers = await listProjectTaskKeyWrappers(task.taskId, createdAt2);
5361
+ const linkedProjectIds = task.linkedProjectIds ?? [];
5362
+ if (linkedProjectIds.length > existingProjectWrappers.length) {
5363
+ throw new Error(`Task ${task.taskId} is missing project key wrappers required for move persistence.`);
5364
+ }
5365
+ return [
5366
+ {
5367
+ key_type: "master",
5368
+ encrypted_task_key: await encryptBytesWithAesGcm(taskKey, masterKey),
5369
+ created_at: createdAt2
5370
+ },
5371
+ {
5372
+ key_type: "chat",
5373
+ hashed_chat_id: computeSHA256(targetChatId),
5374
+ encrypted_task_key: await encryptBytesWithAesGcm(taskKey, targetChatKey),
5375
+ created_at: createdAt2
5376
+ },
5377
+ ...existingProjectWrappers
5378
+ ];
5379
+ };
5380
+ const listProjectTaskKeyWrappers = async (taskId, createdAt2) => {
5381
+ const response = await this.http.get(
5382
+ `/v1/user-tasks/${encodeURIComponent(taskId)}/key-wrappers`,
5383
+ this.getCliRequestHeaders()
5384
+ );
5385
+ if (!response.ok) {
5386
+ throw new Error(`User task key wrapper list failed with HTTP ${response.status}`);
5387
+ }
5388
+ return (response.data.key_wrappers ?? []).filter((wrapper) => wrapper.key_type === "project").map((wrapper) => ({
5389
+ key_type: "project",
5390
+ hashed_project_id: wrapper.hashed_project_id,
5391
+ encrypted_task_key: wrapper.encrypted_task_key,
5392
+ created_at: typeof wrapper.created_at === "number" ? wrapper.created_at : createdAt2,
5393
+ expires_at: wrapper.expires_at ?? null
5394
+ }));
5395
+ };
5396
+ const resolveChatKey = async (targetChatId) => {
5397
+ if (targetChatId === chatId && chatKeyBytes) return chatKeyBytes;
5398
+ const cache = loadSyncCache() ?? await this.ensureSynced();
5399
+ const targetChat = cache.chats.find((chat) => String(chat.details.id ?? "") === targetChatId);
5400
+ const encryptedTargetChatKey = typeof targetChat?.details.encrypted_chat_key === "string" ? targetChat.details.encrypted_chat_key : null;
5401
+ if (!encryptedTargetChatKey) {
5402
+ throw new Error(`Encrypted chat key not found for task move target '${targetChatId}'. Sync and try again.`);
5403
+ }
5404
+ const targetChatKey = await decryptBytesWithAesGcm(encryptedTargetChatKey, masterKey);
5405
+ if (!targetChatKey) {
5406
+ throw new Error(`Failed to decrypt chat key for task move target '${targetChatId}'.`);
5407
+ }
5408
+ return targetChatKey;
5409
+ };
5410
+ const taskEventTypeForOperation = (operation) => {
5411
+ switch (operation) {
5412
+ case "create":
5413
+ return "created";
5414
+ case "move":
5415
+ return "moved";
5416
+ case "update":
5417
+ return "updated";
5418
+ default:
5419
+ return operation || "updated";
5420
+ }
5421
+ };
5422
+ for (const job of jobs) {
5423
+ if (!taskUpdateJobBelongsToActiveTurn(job, chatId, events)) {
5424
+ handledJobIds.add(job.job_id);
5425
+ continue;
5426
+ }
5427
+ const claimPromise = ws.waitForMessage(
5428
+ "task_update_job_claimed",
5429
+ (payload) => payload.job_id === job.job_id,
5430
+ 2e4
5431
+ );
5432
+ await ws.sendAsync("task_update_job_claim", {
5433
+ protocol_version: 1,
5434
+ job_id: job.job_id
5435
+ });
5436
+ const claim = (await claimPromise).payload;
5437
+ const privatePatch = claim.private_patch ?? {};
5438
+ const safeMetadata = claim.safe_metadata ?? {};
5439
+ const sourceChatId = claim.chat_id ?? job.chat_id ?? chatId;
5440
+ const sourceChatKey = await resolveChatKey(sourceChatId);
5441
+ const event = eventByJobId.get(job.job_id) ?? {
5442
+ event_id: `task-event-${job.job_id}`,
5443
+ chat_id: sourceChatId,
5444
+ task_id: claim.task_id,
5445
+ event_type: taskEventTypeForOperation(claim.operation),
5446
+ title: typeof privatePatch.title === "string" ? privatePatch.title : null,
5447
+ status: typeof safeMetadata.status === "string" ? safeMetadata.status : null,
5448
+ created_at: typeof safeMetadata.updated_at === "number" ? safeMetadata.updated_at : Math.floor(Date.now() / 1e3),
5449
+ task_update_job_id: job.job_id
5450
+ };
5451
+ const eventMessage = await buildTaskEventSystemMessage({
5452
+ chatKey: sourceChatKey,
5453
+ userMessageId: claim.message_id ?? messageId,
5454
+ event
5455
+ });
5456
+ const confirmTaskEventPersisted = async () => {
5457
+ const confirmedPromise = ws.waitForMessage(
5458
+ "task_update_job_event_confirmed",
5459
+ (payload) => payload.job_id === job.job_id,
5460
+ 2e4
5461
+ );
5462
+ await ws.sendAsync("task_update_job_event_confirmed", {
5463
+ protocol_version: 1,
5464
+ job_id: job.job_id,
5465
+ event_system_message_id: eventMessage.message_id
5466
+ });
5467
+ await confirmedPromise;
5468
+ };
5469
+ if (claim.state === "TASK_PERSISTED") {
5470
+ await persistEncryptedSystemMessage(eventMessage, sourceChatId);
5471
+ persistedTaskEventIds.add(event.event_id);
5472
+ await confirmTaskEventPersisted();
5473
+ handledJobIds.add(job.job_id);
5474
+ continue;
5475
+ }
5476
+ if (!claim.lease_token || !Number.isSafeInteger(claim.lease_generation)) {
5477
+ throw new Error("Task update job claim returned an invalid lease.");
5478
+ }
5479
+ let encryptedTaskPayload;
5480
+ if (claim.operation === "create") {
5481
+ const input = await buildCreateUserTaskInput(masterKey, {
5482
+ title: typeof privatePatch.title === "string" ? privatePatch.title : "Untitled task",
5483
+ description: typeof privatePatch.description === "string" ? privatePatch.description : "",
5484
+ status: typeof safeMetadata.status === "string" ? safeMetadata.status : "todo",
5485
+ assign: typeof safeMetadata.assignee_type === "string" ? safeMetadata.assignee_type : "user",
5486
+ chatId: typeof safeMetadata.primary_chat_id === "string" ? safeMetadata.primary_chat_id : claim.chat_id ?? chatId
5487
+ });
5488
+ encryptedTaskPayload = {
5489
+ ...input,
5490
+ task_id: claim.task_id,
5491
+ position: typeof safeMetadata.position === "number" ? safeMetadata.position : input.position,
5492
+ created_at: typeof safeMetadata.created_at === "number" ? safeMetadata.created_at : input.created_at,
5493
+ updated_at: typeof safeMetadata.updated_at === "number" ? safeMetadata.updated_at : input.updated_at
5494
+ };
5495
+ } else {
5496
+ if (!decryptedTasksCache) {
5497
+ decryptedTasksCache = await decryptUserTasks(await this.listUserTasks(), masterKey);
5498
+ }
5499
+ const task = findTask(decryptedTasksCache, claim.task_id);
5500
+ const patch = await buildUpdateUserTaskInput(task, masterKey, {
5501
+ title: typeof privatePatch.title === "string" ? privatePatch.title : void 0,
5502
+ description: typeof privatePatch.description === "string" ? privatePatch.description : void 0,
5503
+ status: typeof safeMetadata.status === "string" ? safeMetadata.status : void 0,
5504
+ assign: typeof safeMetadata.assignee_type === "string" ? safeMetadata.assignee_type : void 0,
5505
+ chatId: typeof safeMetadata.primary_chat_id === "string" ? safeMetadata.primary_chat_id : void 0
5506
+ });
5507
+ encryptedTaskPayload = {
5508
+ ...patch,
5509
+ version: claim.expected_task_version,
5510
+ updated_at: typeof safeMetadata.updated_at === "number" ? safeMetadata.updated_at : patch.updated_at
5511
+ };
5512
+ if (typeof safeMetadata.primary_chat_id === "string") {
5513
+ encryptedTaskPayload.key_wrappers = await buildTaskKeyWrappersForChat(
5514
+ task,
5515
+ safeMetadata.primary_chat_id,
5516
+ typeof safeMetadata.updated_at === "number" ? safeMetadata.updated_at : Math.floor(Date.now() / 1e3)
5517
+ );
5518
+ }
5519
+ }
5520
+ const encryptedEventMessage = eventMessage.encrypted_content;
5521
+ const persistedPromise = ws.waitForMessage(
5522
+ "task_update_job_persisted",
5523
+ (payload) => payload.job_id === job.job_id,
5524
+ 2e4
5525
+ );
5526
+ await ws.sendAsync("task_update_job_persist", buildTaskUpdateJobPersistPayload({
5527
+ jobId: job.job_id,
5528
+ leaseToken: claim.lease_token,
5529
+ leaseGeneration: claim.lease_generation,
5530
+ expectedTaskVersion: claim.expected_task_version,
5531
+ encryptedTaskPayload,
5532
+ encryptedTaskEventMessage: encryptedEventMessage
5533
+ }));
5534
+ await persistedPromise;
5535
+ await persistEncryptedSystemMessage(eventMessage, sourceChatId);
5536
+ persistedTaskEventIds.add(event.event_id);
5537
+ await confirmTaskEventPersisted();
5538
+ handledJobIds.add(job.job_id);
5539
+ }
5540
+ clearSyncCache();
5541
+ return handledJobIds;
5542
+ };
4877
5543
  const streamOpts = {
4878
5544
  onStream: params.onStream,
4879
5545
  onSubChatEvent: handleSubChatEvent,
@@ -4881,11 +5547,17 @@ var OpenMatesClient = class _OpenMatesClient {
4881
5547
  };
4882
5548
  if (params.incognito) {
4883
5549
  try {
4884
- const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId, chatId, streamOpts));
5550
+ const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId, chatId, {
5551
+ ...streamOpts,
5552
+ timeoutMs: params.responseTimeoutMs,
5553
+ recoveryTurnId: savedTurnId
5554
+ }));
4885
5555
  assistantMessageId = resp.messageId;
4886
5556
  assistant = resp.content;
4887
5557
  category = resp.category;
4888
5558
  modelName = resp.modelName;
5559
+ taskEvents = resp.taskEvents;
5560
+ pendingTaskUpdateJobs = resp.pendingTaskUpdateJobs;
4889
5561
  subChatEvents = resp.subChatEvents;
4890
5562
  if (resp.status === "waiting_for_user") {
4891
5563
  return {
@@ -4899,6 +5571,8 @@ var OpenMatesClient = class _OpenMatesClient {
4899
5571
  followUpSuggestions,
4900
5572
  taskProposals,
4901
5573
  taskUpdateProposals,
5574
+ taskEvents,
5575
+ pendingTaskUpdateJobs,
4902
5576
  subChatEvents,
4903
5577
  appSettingsMemoryRequests
4904
5578
  };
@@ -4908,7 +5582,10 @@ var OpenMatesClient = class _OpenMatesClient {
4908
5582
  }
4909
5583
  } else {
4910
5584
  try {
4911
- const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId, chatId, streamOpts));
5585
+ const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId, chatId, {
5586
+ ...streamOpts,
5587
+ timeoutMs: params.responseTimeoutMs
5588
+ }));
4912
5589
  assistantMessageId = resp.messageId;
4913
5590
  assistant = resp.content;
4914
5591
  category = resp.category;
@@ -4916,6 +5593,8 @@ var OpenMatesClient = class _OpenMatesClient {
4916
5593
  followUpSuggestions = resp.followUpSuggestions;
4917
5594
  taskProposals = resp.taskProposals;
4918
5595
  taskUpdateProposals = resp.taskUpdateProposals;
5596
+ taskEvents = resp.taskEvents;
5597
+ pendingTaskUpdateJobs = resp.pendingTaskUpdateJobs;
4919
5598
  subChatEvents = resp.subChatEvents;
4920
5599
  if (resp.status === "waiting_for_user") {
4921
5600
  return {
@@ -4929,6 +5608,8 @@ var OpenMatesClient = class _OpenMatesClient {
4929
5608
  followUpSuggestions,
4930
5609
  taskProposals,
4931
5610
  taskUpdateProposals,
5611
+ taskEvents,
5612
+ pendingTaskUpdateJobs,
4932
5613
  subChatEvents,
4933
5614
  appSettingsMemoryRequests
4934
5615
  };
@@ -4994,12 +5675,15 @@ var OpenMatesClient = class _OpenMatesClient {
4994
5675
  "model_name",
4995
5676
  "turn_id"
4996
5677
  ];
4997
- if (Object.keys(recovered).sort().join(",") !== expectedRecoveryFields.join(",") || recovered.assistant_message_id !== assistantId || recovered.chat_id !== chatId || recovered.turn_id !== savedTurnId || recovered.job_id !== recoveryJobId || recovered.key_version !== keyVersion || typeof recovered.content !== "string" || recovered.category !== null && typeof recovered.category !== "string" || recovered.model_name !== null && typeof recovered.model_name !== "string" || recovered.content !== assistant || recovered.category !== category || recovered.model_name !== modelName) {
5678
+ if (Object.keys(recovered).sort().join(",") !== expectedRecoveryFields.join(",") || recovered.assistant_message_id !== assistantId || recovered.chat_id !== chatId || recovered.turn_id !== savedTurnId || recovered.job_id !== recoveryJobId || recovered.key_version !== keyVersion || typeof recovered.content !== "string" || recovered.category !== null && typeof recovered.category !== "string" || recovered.model_name !== null && typeof recovered.model_name !== "string") {
4998
5679
  throw new Error("Recovery job plaintext did not match the terminal completion identity.");
4999
5680
  }
5681
+ assistant = recovered.content;
5682
+ category = recovered.category;
5683
+ modelName = recovered.model_name;
5000
5684
  const completedAt = Math.floor(Date.now() / 1e3);
5001
5685
  const encryptedAssistantContent = await encryptWithAesGcmCombined(
5002
- recovered.content,
5686
+ assistant,
5003
5687
  chatKeyBytes
5004
5688
  );
5005
5689
  const encryptedSenderName = await encryptWithAesGcmCombined("Assistant", chatKeyBytes);
@@ -5046,6 +5730,9 @@ var OpenMatesClient = class _OpenMatesClient {
5046
5730
  newChatSuggestions: resp.newChatSuggestions,
5047
5731
  encryptedChatKey
5048
5732
  });
5733
+ const persistedTaskJobIds = await persistPendingTaskUpdateJobs(pendingTaskUpdateJobs, taskEvents);
5734
+ pendingTaskUpdateJobs = pendingTaskUpdateJobs.filter((job) => !persistedTaskJobIds.has(job.job_id));
5735
+ await persistTaskEventSystemMessages(taskEvents);
5049
5736
  clearSyncCache();
5050
5737
  }
5051
5738
  } finally {
@@ -5064,6 +5751,8 @@ var OpenMatesClient = class _OpenMatesClient {
5064
5751
  followUpSuggestions,
5065
5752
  taskProposals,
5066
5753
  taskUpdateProposals,
5754
+ taskEvents,
5755
+ pendingTaskUpdateJobs,
5067
5756
  subChatEvents,
5068
5757
  appSettingsMemoryRequests
5069
5758
  };
@@ -6024,7 +6713,7 @@ var OpenMatesClient = class _OpenMatesClient {
6024
6713
  }
6025
6714
  return response.data.task;
6026
6715
  }
6027
- async startUserTaskWithAI(taskId, input = {}) {
6716
+ async startUserTaskWithAI(taskId, input) {
6028
6717
  this.requireSession();
6029
6718
  const response = await this.http.post(
6030
6719
  `/v1/user-tasks/${encodeURIComponent(taskId)}/start-ai`,
@@ -6036,10 +6725,10 @@ var OpenMatesClient = class _OpenMatesClient {
6036
6725
  }
6037
6726
  return response.data.task;
6038
6727
  }
6039
- async deleteUserTask(taskId) {
6728
+ async deleteUserTask(taskId, version) {
6040
6729
  this.requireSession();
6041
6730
  const response = await this.http.delete(
6042
- `/v1/user-tasks/${encodeURIComponent(taskId)}`,
6731
+ `/v1/user-tasks/${encodeURIComponent(taskId)}?version=${encodeURIComponent(String(version))}`,
6043
6732
  void 0,
6044
6733
  this.getCliRequestHeaders()
6045
6734
  );
@@ -6048,16 +6737,16 @@ var OpenMatesClient = class _OpenMatesClient {
6048
6737
  }
6049
6738
  return response.data;
6050
6739
  }
6051
- async completeUserTask(taskId, input = {}) {
6740
+ async completeUserTask(taskId, input) {
6052
6741
  return this.postUserTaskAction(taskId, "complete", input);
6053
6742
  }
6054
- async blockUserTask(taskId, input = {}) {
6743
+ async blockUserTask(taskId, input) {
6055
6744
  return this.postUserTaskAction(taskId, "block", input);
6056
6745
  }
6057
- async unblockUserTask(taskId, input = {}) {
6746
+ async unblockUserTask(taskId, input) {
6058
6747
  return this.postUserTaskAction(taskId, "unblock", input);
6059
6748
  }
6060
- async skipUserTask(taskId, input = {}) {
6749
+ async skipUserTask(taskId, input) {
6061
6750
  return this.postUserTaskAction(taskId, "skip", input);
6062
6751
  }
6063
6752
  async reorderUserTasks(input) {
@@ -6072,7 +6761,7 @@ var OpenMatesClient = class _OpenMatesClient {
6072
6761
  }
6073
6762
  return response.data.tasks ?? [];
6074
6763
  }
6075
- async postUserTaskAction(taskId, action, input = {}) {
6764
+ async postUserTaskAction(taskId, action, input) {
6076
6765
  this.requireSession();
6077
6766
  const response = await this.http.post(
6078
6767
  `/v1/user-tasks/${encodeURIComponent(taskId)}/${encodeURIComponent(action)}`,
@@ -6830,7 +7519,7 @@ Required: ${schema.required.join(", ")}`
6830
7519
  );
6831
7520
  }
6832
7521
  }
6833
- const entryId = params.entryId ?? randomUUID3();
7522
+ const entryId = params.entryId ?? randomUUID4();
6834
7523
  const now = Math.floor(Date.now() / 1e3);
6835
7524
  const hashedKey = hashItemKey(params.appId, params.itemType);
6836
7525
  const plaintextPayload = {
@@ -7305,7 +7994,7 @@ Required: ${schema.required.join(", ")}`
7305
7994
  makeWsClient(session) {
7306
7995
  return new OpenMatesWsClient({
7307
7996
  apiUrl: session.apiUrl,
7308
- sessionId: randomUUID3(),
7997
+ sessionId: randomUUID4(),
7309
7998
  wsToken: session.wsToken,
7310
7999
  refreshToken: session.cookies.auth_refresh_token ?? null,
7311
8000
  // Same User-Agent as login so OS-based device fingerprint hash matches.
@@ -8488,7 +9177,7 @@ var OutputRedactor = class {
8488
9177
  import { readFileSync as readFileSync4, statSync, existsSync as existsSync4 } from "fs";
8489
9178
  import { basename, extname, resolve as resolve2 } from "path";
8490
9179
  import { homedir as homedir4 } from "os";
8491
- import { createHash as createHash5 } from "crypto";
9180
+ import { createHash as createHash6 } from "crypto";
8492
9181
  var MAX_PER_FILE_SIZE = 100 * 1024 * 1024;
8493
9182
  var BLOCKED_EXTENSIONS = /* @__PURE__ */ new Set([
8494
9183
  ".pem",
@@ -8798,7 +9487,7 @@ function processCodeFile(filePath, filename, redactor) {
8798
9487
  line_count: lineCount
8799
9488
  });
8800
9489
  const textPreview = `${filename} (${language}, ${lineCount} lines)`;
8801
- const contentHash = createHash5("sha256").update(content).digest("hex");
9490
+ const contentHash = createHash6("sha256").update(content).digest("hex");
8802
9491
  const embed = {
8803
9492
  embedId,
8804
9493
  embedRef,
@@ -10314,7 +11003,7 @@ function formatTs(ts) {
10314
11003
 
10315
11004
  // src/server.ts
10316
11005
  import { execFileSync as execFileSync2, execSync, spawn as nodeSpawn, spawnSync } from "child_process";
10317
- import { createHash as createHash6, randomBytes as randomBytes2 } from "crypto";
11006
+ import { createHash as createHash7, randomBytes as randomBytes3 } from "crypto";
10318
11007
  import { chmodSync as chmodSync2, closeSync, copyFileSync, cpSync, existsSync as existsSync5, mkdirSync as mkdirSync3, mkdtempSync, openSync, readFileSync as readFileSync5, readSync, readdirSync, rmSync as rmSync3, writeFileSync as writeFileSync3 } from "fs";
10319
11008
  import { createInterface as createInterface2 } from "readline";
10320
11009
  import { createInterface as createPromptInterface } from "readline/promises";
@@ -10932,10 +11621,10 @@ function resolveTargetImageTag(flags, currentTag, packageVersion) {
10932
11621
  return { tag: getDefaultImageTagForVersion(packageVersion) };
10933
11622
  }
10934
11623
  function randomHex(bytes) {
10935
- return randomBytes2(bytes).toString("hex");
11624
+ return randomBytes3(bytes).toString("hex");
10936
11625
  }
10937
11626
  function generateInviteCode() {
10938
- const digits = Array.from(randomBytes2(12), (byte) => String(byte % 10)).join("");
11627
+ const digits = Array.from(randomBytes3(12), (byte) => String(byte % 10)).join("");
10939
11628
  return `${digits.slice(0, 4)}-${digits.slice(4, 8)}-${digits.slice(8, 12)}`;
10940
11629
  }
10941
11630
  function getEnvVar(content, name) {
@@ -10979,7 +11668,7 @@ function packagedCaddyTemplatePath(role) {
10979
11668
  }
10980
11669
  function fileHash(path) {
10981
11670
  if (!existsSync5(path)) return null;
10982
- return createHash6("sha256").update(readFileSync5(path)).digest("hex");
11671
+ return createHash7("sha256").update(readFileSync5(path)).digest("hex");
10983
11672
  }
10984
11673
  async function loadSelfHostComposeTemplate(templateRef, role) {
10985
11674
  const templateDir = process.env.OPENMATES_SELFHOST_TEMPLATE_DIR;
@@ -11365,7 +12054,7 @@ function formatSecretPreflight(preflight) {
11365
12054
  return parts.join("; ");
11366
12055
  }
11367
12056
  function hashFile(path) {
11368
- const hash = createHash6("sha256");
12057
+ const hash = createHash7("sha256");
11369
12058
  const fd = openSync(path, "r");
11370
12059
  const buffer = Buffer.allocUnsafe(CHECKSUM_BUFFER_BYTES);
11371
12060
  try {
@@ -29386,7 +30075,10 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
29386
30075
  },
29387
30076
  models3d: {
29388
30077
  generate: {
29389
- text: "Generate 3D model"
30078
+ text: "Generate 3D model",
30079
+ description: {
30080
+ text: "Create a 3D model from a text prompt or reference images."
30081
+ }
29390
30082
  }
29391
30083
  },
29392
30084
  music: {
@@ -30241,6 +30933,12 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
30241
30933
  text: "Create structured idea maps and brainstorm topic trees."
30242
30934
  }
30243
30935
  },
30936
+ models3d: {
30937
+ text: "3D Models",
30938
+ description: {
30939
+ text: "Generate 3D models from prompts or reference images."
30940
+ }
30941
+ },
30244
30942
  math: {
30245
30943
  text: "Math",
30246
30944
  description: {
@@ -46753,7 +47451,7 @@ function buildAssistantFeedbackDecision(rating) {
46753
47451
  }
46754
47452
 
46755
47453
  // src/benchmark.ts
46756
- import { randomUUID as randomUUID4 } from "crypto";
47454
+ import { randomUUID as randomUUID5 } from "crypto";
46757
47455
  import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
46758
47456
  import { tmpdir } from "os";
46759
47457
  import { dirname as dirname2, join as join4, resolve as resolve5 } from "path";
@@ -47053,7 +47751,7 @@ async function handleBenchmark(client, subcommand, rest, flags) {
47053
47751
  const caseIds = parseCaseIds(flags.case);
47054
47752
  const dryRun = flags["dry-run"] === true;
47055
47753
  const output = typeof flags.output === "string" ? flags.output : void 0;
47056
- const runId = typeof flags["run-id"] === "string" ? flags["run-id"] : randomUUID4();
47754
+ const runId = typeof flags["run-id"] === "string" ? flags["run-id"] : randomUUID5();
47057
47755
  const imagePath = typeof flags.image === "string" ? resolve5(flags.image) : defaultImageFixturePath();
47058
47756
  if (!dryRun && flags["confirm-spend-credits"] !== true) {
47059
47757
  throw new Error(
@@ -47463,7 +48161,7 @@ function buildLongContextHistory() {
47463
48161
  }
47464
48162
  function appendHistory(history, role, content) {
47465
48163
  history.push({
47466
- message_id: randomUUID4(),
48164
+ message_id: randomUUID5(),
47467
48165
  role,
47468
48166
  sender_name: role === "user" ? "User" : "Assistant",
47469
48167
  content,
@@ -47998,7 +48696,7 @@ function renderBody(state, width) {
47998
48696
  case "tasks":
47999
48697
  return renderTasks(state, width);
48000
48698
  case "task":
48001
- return renderTaskDetail(state, width);
48699
+ return renderTaskDetail2(state, width);
48002
48700
  case "start":
48003
48701
  default:
48004
48702
  return renderStart(width);
@@ -48071,7 +48769,7 @@ function renderTasks(state, width) {
48071
48769
  }
48072
48770
  return lines.flatMap((line) => wrap(line, width));
48073
48771
  }
48074
- function renderTaskDetail(state, width) {
48772
+ function renderTaskDetail2(state, width) {
48075
48773
  const task = state.activeTask;
48076
48774
  if (!task) return renderTasks(state, width);
48077
48775
  const assignee = task.assigneeType === "ai" ? "OpenMates" : task.assigneeHash ?? "user";
@@ -48388,221 +49086,6 @@ function boxed(line, width) {
48388
49086
  return `\u2502 ${line.padEnd(width)} \u2502`;
48389
49087
  }
48390
49088
 
48391
- // src/tasksCli.ts
48392
- import { createHash as createHash7, randomBytes as randomBytes3, randomUUID as randomUUID5 } from "crypto";
48393
- var TASK_STATUSES2 = ["backlog", "todo", "in_progress", "blocked", "done"];
48394
- var DEFAULT_STANDALONE_PREFIX = "TASK";
48395
- function normalizeTaskStatus(value) {
48396
- if (value === void 0) return void 0;
48397
- if (TASK_STATUSES2.includes(value)) return value;
48398
- throw new Error(`Unknown task status '${value}'. Expected one of: ${TASK_STATUSES2.join(", ")}`);
48399
- }
48400
- function parseAssignee(value) {
48401
- if (!value || value === "user") return { assigneeType: "user", assigneeHash: null };
48402
- if (["ai", "openmates", "OpenMates"].includes(value)) return { assigneeType: "ai", assigneeHash: null };
48403
- return { assigneeType: "user", assigneeHash: value };
48404
- }
48405
- function splitCsvFlag(value) {
48406
- if (typeof value !== "string") return [];
48407
- return value.split(/[,\n]/).map((item) => item.trim()).filter(Boolean);
48408
- }
48409
- function parseDueAt(value) {
48410
- if (value === void 0) return void 0;
48411
- if (value === false || value === true) throw new Error("--due requires a timestamp or date value.");
48412
- const numeric = Number(value);
48413
- if (Number.isFinite(numeric) && numeric > 0) return Math.floor(numeric);
48414
- const parsed = Date.parse(value);
48415
- if (Number.isNaN(parsed)) throw new Error(`Invalid --due value '${value}'.`);
48416
- return Math.floor(parsed / 1e3);
48417
- }
48418
- async function buildCreateUserTaskInput(masterKey, input) {
48419
- const taskKey = randomBytes3(32);
48420
- const encryptedTaskKey = await encryptBytesWithAesGcm(taskKey, masterKey);
48421
- const timestamp = nowSeconds();
48422
- const assignee = parseAssignee(input.assign);
48423
- const linkedProjectIds = input.projectIds ?? [];
48424
- const status = input.status ?? (assignee.assigneeType === "ai" && !input.dueAt ? "in_progress" : "todo");
48425
- return {
48426
- task_id: randomUUIDCompat(),
48427
- short_id: void 0,
48428
- encrypted_task_key: encryptedTaskKey,
48429
- encrypted_title: await encryptWithAesGcmCombined(input.title, taskKey),
48430
- encrypted_description: await encryptWithAesGcmCombined(input.description ?? "", taskKey),
48431
- encrypted_tags: await encryptWithAesGcmCombined("[]", taskKey),
48432
- encrypted_linked_project_ids: await encryptWithAesGcmCombined(JSON.stringify(linkedProjectIds), taskKey),
48433
- status,
48434
- assignee_type: assignee.assigneeType,
48435
- assignee_hash: assignee.assigneeHash,
48436
- primary_chat_id: input.chatId ?? null,
48437
- linked_project_ids: linkedProjectIds,
48438
- plan_id: input.planId ?? null,
48439
- due_at: input.dueAt ?? null,
48440
- priority: 0,
48441
- position: timestamp,
48442
- created_at: timestamp,
48443
- updated_at: timestamp
48444
- };
48445
- }
48446
- async function buildUpdateUserTaskInput(task, masterKey, input) {
48447
- const taskKey = await taskKeyFromRecord(task.encrypted, masterKey);
48448
- const patch = { version: task.version, updated_at: nowSeconds() };
48449
- if (input.title !== void 0) patch.encrypted_title = await encryptWithAesGcmCombined(input.title, taskKey);
48450
- if (input.description !== void 0) patch.encrypted_description = await encryptWithAesGcmCombined(input.description, taskKey);
48451
- if (input.status !== void 0) patch.status = input.status;
48452
- if (input.assign !== void 0) {
48453
- const assignee = parseAssignee(input.assign);
48454
- patch.assignee_type = assignee.assigneeType;
48455
- patch.assignee_hash = assignee.assigneeHash;
48456
- }
48457
- if (input.chatId !== void 0) patch.primary_chat_id = input.chatId;
48458
- if (input.projectIds !== void 0) {
48459
- patch.linked_project_ids = input.projectIds;
48460
- patch.encrypted_linked_project_ids = await encryptWithAesGcmCombined(JSON.stringify(input.projectIds), taskKey);
48461
- }
48462
- if (input.planId !== void 0) patch.plan_id = input.planId;
48463
- return patch;
48464
- }
48465
- async function decryptUserTask(record, masterKey) {
48466
- const taskKey = await taskKeyFromRecord(record, masterKey);
48467
- const tags = parseStringArray(await decryptOptional(record.encrypted_tags, taskKey));
48468
- const linkedProjectIds = parseStringArray(await decryptOptional(record.encrypted_linked_project_ids, taskKey));
48469
- return {
48470
- taskId: record.task_id,
48471
- shortId: record.short_id || deriveShortId(record),
48472
- title: await decryptOptional(record.encrypted_title, taskKey) || "(untitled task)",
48473
- description: await decryptOptional(record.encrypted_description, taskKey),
48474
- tags,
48475
- latestInstruction: await decryptOptional(record.encrypted_latest_instruction, taskKey),
48476
- status: record.status,
48477
- assigneeType: record.assignee_type,
48478
- assigneeHash: record.assignee_hash ?? null,
48479
- primaryChatId: record.primary_chat_id ?? null,
48480
- linkedProjectIds: linkedProjectIds.length > 0 ? linkedProjectIds : record.linked_project_ids ?? [],
48481
- planId: record.plan_id ?? null,
48482
- dueAt: record.due_at ?? null,
48483
- priority: record.priority ?? 0,
48484
- position: record.position ?? 0,
48485
- queueState: record.queue_state ?? "none",
48486
- blockedReasonCode: record.blocked_reason_code ?? null,
48487
- aiExecutionState: record.ai_execution_state ?? null,
48488
- version: record.version ?? 1,
48489
- encrypted: record
48490
- };
48491
- }
48492
- async function decryptUserTasks(records, masterKey) {
48493
- const output = [];
48494
- for (const record of records) output.push(await decryptUserTask(record, masterKey));
48495
- return output;
48496
- }
48497
- function findTask(tasks, id) {
48498
- const task = tasks.find((candidate) => candidate.taskId === id || candidate.shortId === id);
48499
- if (!task) throw new Error(`Task '${id}' was not found in the current task list.`);
48500
- return task;
48501
- }
48502
- function renderTaskList(tasks) {
48503
- if (tasks.length === 0) return "No tasks found.";
48504
- const lines = ["Tasks", "ID Status Assignee Queue Title"];
48505
- for (const task of tasks) {
48506
- lines.push(`${pad(task.shortId, 9)} ${pad(task.status, 12)} ${pad(assigneeLabel(task), 11)} ${pad(task.queueState, 11)} ${task.title}`);
48507
- }
48508
- return lines.join("\n");
48509
- }
48510
- function renderTaskDetail2(task) {
48511
- const lines = [
48512
- `Task ${task.shortId}`,
48513
- `Title: ${task.title}`,
48514
- `Status: ${task.status}`,
48515
- `Assignee: ${assigneeLabel(task)}`,
48516
- `Queue: ${task.queueState}`,
48517
- `Task ID: ${task.taskId}`
48518
- ];
48519
- if (task.description) lines.push(`Description: ${task.description}`);
48520
- if (task.primaryChatId) lines.push(`Chat: ${task.primaryChatId}`);
48521
- if (task.linkedProjectIds.length > 0) lines.push(`Projects: ${task.linkedProjectIds.join(", ")}`);
48522
- if (task.planId) lines.push(`Plan: ${task.planId}`);
48523
- if (task.blockedReasonCode) lines.push(`Blocked reason: ${task.blockedReasonCode}`);
48524
- if (task.aiExecutionState) lines.push(`AI state: ${task.aiExecutionState}`);
48525
- return lines.join("\n");
48526
- }
48527
- function renderTaskBoard(tasks, width = process.stdout.columns || 100) {
48528
- const columns = TASK_STATUSES2.map((status) => ({ status, tasks: tasks.filter((task) => task.status === status).sort(compareTasks) }));
48529
- if (width < 96) {
48530
- const lines2 = ["OpenMates Tasks Board"];
48531
- for (const column of columns) {
48532
- lines2.push("", `${columnTitle(column.status)} (${column.tasks.length})`, "-".repeat(24));
48533
- lines2.push(...boardColumnLines(column.tasks, 8));
48534
- }
48535
- return lines2.join("\n");
48536
- }
48537
- const perColumn = 6;
48538
- const columnWidth = 22;
48539
- const lines = ["OpenMates Tasks Board", ""];
48540
- lines.push(columns.map((column) => pad(`${columnTitle(column.status)} (${column.tasks.length})`, columnWidth)).join(" "));
48541
- lines.push(columns.map(() => "-".repeat(columnWidth)).join(" "));
48542
- const renderedColumns = columns.map((column) => boardColumnLines(column.tasks, perColumn).map((line) => truncate(line, columnWidth)));
48543
- const maxRows = Math.max(...renderedColumns.map((column) => column.length));
48544
- for (let row = 0; row < maxRows; row += 1) {
48545
- lines.push(renderedColumns.map((column) => pad(column[row] ?? "", columnWidth)).join(" "));
48546
- }
48547
- return lines.join("\n");
48548
- }
48549
- function boardColumnLines(tasks, limit) {
48550
- if (tasks.length === 0) return ["No tasks here."];
48551
- const visible = tasks.slice(0, limit).flatMap((task) => [
48552
- `[${task.shortId}] ${task.title}`,
48553
- ` ${assigneeLabel(task)} ${task.queueState === "none" ? "" : task.queueState}`.trimEnd()
48554
- ]);
48555
- if (tasks.length > limit) visible.push(`... ${tasks.length - limit} more`);
48556
- return visible;
48557
- }
48558
- function compareTasks(a, b) {
48559
- return a.position - b.position || a.title.localeCompare(b.title);
48560
- }
48561
- function columnTitle(status) {
48562
- if (status === "in_progress") return "In progress";
48563
- return status.slice(0, 1).toUpperCase() + status.slice(1).replace("_", " ");
48564
- }
48565
- function assigneeLabel(task) {
48566
- return task.assigneeType === "ai" ? "OpenMates" : task.assigneeHash ?? "user";
48567
- }
48568
- async function taskKeyFromRecord(record, masterKey) {
48569
- if (!record.encrypted_task_key) throw new Error(`Task ${record.task_id} is missing encrypted task key.`);
48570
- const taskKey = await decryptBytesWithAesGcm(record.encrypted_task_key, masterKey);
48571
- if (!taskKey) throw new Error(`Failed to decrypt task key for ${record.task_id}.`);
48572
- return taskKey;
48573
- }
48574
- async function decryptOptional(value, key) {
48575
- if (!value) return "";
48576
- return await decryptWithAesGcmCombined(value, key) ?? "";
48577
- }
48578
- function parseStringArray(value) {
48579
- if (!value) return [];
48580
- try {
48581
- const parsed = JSON.parse(value);
48582
- return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
48583
- } catch {
48584
- return [];
48585
- }
48586
- }
48587
- function deriveShortId(record) {
48588
- const prefix = record.short_id_prefix || DEFAULT_STANDALONE_PREFIX;
48589
- const source = record.task_id || `${record.created_at ?? ""}-${record.position ?? ""}`;
48590
- const digest = createHash7("sha256").update(source).digest("hex").slice(0, 4).toUpperCase();
48591
- return `${prefix}-${parseInt(digest, 16) % 1e4}`;
48592
- }
48593
- function nowSeconds() {
48594
- return Math.floor(Date.now() / 1e3);
48595
- }
48596
- function randomUUIDCompat() {
48597
- return randomUUID5();
48598
- }
48599
- function pad(value, length) {
48600
- return truncate(value, length).padEnd(length);
48601
- }
48602
- function truncate(value, length) {
48603
- return value.length <= length ? value : `${value.slice(0, Math.max(0, length - 3))}...`;
48604
- }
48605
-
48606
49089
  // src/tui.ts
48607
49090
  function defaultModeForStreams(input, output) {
48608
49091
  return input.isTTY === true && output.isTTY === true ? "tui" : "quickstart";
@@ -49036,7 +49519,7 @@ async function handleActiveTaskAction(params) {
49036
49519
  render();
49037
49520
  return;
49038
49521
  }
49039
- await client.deleteUserTask(task.taskId);
49522
+ await client.deleteUserTask(task.taskId, task.version);
49040
49523
  state.tasks = state.tasks.filter((candidate) => candidate.taskId !== task.taskId);
49041
49524
  state.activeTask = null;
49042
49525
  state.selectedIndex = Math.min(state.selectedIndex, Math.max(0, state.tasks.length - 1));
@@ -49053,7 +49536,7 @@ async function handleActiveTaskAction(params) {
49053
49536
  render();
49054
49537
  return;
49055
49538
  }
49056
- const updated2 = await client.reorderUserTasks({ moves: [{ task_id: task.taskId, position }] });
49539
+ const updated2 = await client.reorderUserTasks({ moves: [{ task_id: task.taskId, version: task.version, position }] });
49057
49540
  const [decrypted2] = await decryptUserTasks(updated2, client.getMasterKeyBytes());
49058
49541
  if (decrypted2) {
49059
49542
  replaceTask(state, decrypted2);
@@ -50145,7 +50628,7 @@ async function handleTasks(client, subcommand, rest, flags) {
50145
50628
  if (subcommand === "delete") {
50146
50629
  const task = await requiredResolvedTask(client, masterKey, rest[0], scope, "delete");
50147
50630
  if (flags.confirm !== true) throw new Error("Deleting a task requires --confirm.");
50148
- const result = await client.deleteUserTask(task.taskId);
50631
+ const result = await client.deleteUserTask(task.taskId, task.version);
50149
50632
  if (flags.json === true) printJson2(result);
50150
50633
  else console.log(`Task deleted: ${task.shortId}`);
50151
50634
  return;
@@ -50176,7 +50659,7 @@ async function handleTasks(client, subcommand, rest, flags) {
50176
50659
  }
50177
50660
  if (subcommand === "reorder") {
50178
50661
  const task = await requiredResolvedTask(client, masterKey, rest[0], scope, "reorder");
50179
- const move = { task_id: task.taskId };
50662
+ const move = { task_id: task.taskId, version: task.version };
50180
50663
  if (typeof flags.before === "string") move.before_task_id = (await resolveTask(client, masterKey, flags.before, scope)).taskId;
50181
50664
  if (typeof flags.after === "string") move.after_task_id = (await resolveTask(client, masterKey, flags.after, scope)).taskId;
50182
50665
  if (typeof flags.status === "string") move.status = normalizeTaskStatus(flags.status);
@@ -50211,7 +50694,7 @@ async function requiredResolvedTask(client, masterKey, id, scope, action) {
50211
50694
  }
50212
50695
  function printTaskOutput(task, flags) {
50213
50696
  if (flags.json === true) printJson2({ task: taskToJson(task) });
50214
- else console.log(renderTaskDetail2(task));
50697
+ else console.log(renderTaskDetail(task));
50215
50698
  }
50216
50699
  function taskToJson(task) {
50217
50700
  return {
@@ -50316,6 +50799,10 @@ function parsePositiveIntegerFlag(value, flagName) {
50316
50799
  }
50317
50800
  return parsed;
50318
50801
  }
50802
+ function parseResponseTimeoutMs(flags) {
50803
+ const seconds = parsePositiveIntegerFlag(flags["response-timeout-seconds"], "--response-timeout-seconds");
50804
+ return seconds === void 0 ? void 0 : seconds * 1e3;
50805
+ }
50319
50806
  function parseRemoteAccessSourceType(value) {
50320
50807
  if (value === void 0) return "local_folder";
50321
50808
  if (value === "local_folder" || value === "local_git_repository") {
@@ -50459,6 +50946,7 @@ async function handleChats(client, subcommand, rest, flags, redactor) {
50459
50946
  autoApproveMemories: flags["auto-approve-memories"] === true,
50460
50947
  acceptTaskProposals: flags["accept-task-proposals"] === true,
50461
50948
  piiDetection: flags["no-pii-detection"] !== true,
50949
+ responseTimeoutMs: parseResponseTimeoutMs(flags),
50462
50950
  anonymousLearningMode: client.hasSession() ? void 0 : parseAnonymousLearningModeFlags(flags)
50463
50951
  },
50464
50952
  redactor
@@ -50534,7 +51022,8 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
50534
51022
  autoApproveSubChats: flags["auto-approve"] === true,
50535
51023
  autoApproveMemories: flags["auto-approve-memories"] === true,
50536
51024
  acceptTaskProposals: flags["accept-task-proposals"] === true,
50537
- piiDetection: flags["no-pii-detection"] !== true
51025
+ piiDetection: flags["no-pii-detection"] !== true,
51026
+ responseTimeoutMs: parseResponseTimeoutMs(flags)
50538
51027
  },
50539
51028
  redactor
50540
51029
  );
@@ -50563,7 +51052,8 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
50563
51052
  autoApproveSubChats: flags["auto-approve"] === true,
50564
51053
  autoApproveMemories: flags["auto-approve-memories"] === true,
50565
51054
  acceptTaskProposals: flags["accept-task-proposals"] === true,
50566
- piiDetection: flags["no-pii-detection"] !== true
51055
+ piiDetection: flags["no-pii-detection"] !== true,
51056
+ responseTimeoutMs: parseResponseTimeoutMs(flags)
50567
51057
  },
50568
51058
  redactor
50569
51059
  );
@@ -50584,7 +51074,8 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
50584
51074
  json: flags.json === true,
50585
51075
  autoApproveSubChats: flags["auto-approve"] === true,
50586
51076
  autoApproveMemories: flags["auto-approve-memories"] === true,
50587
- piiDetection: flags["no-pii-detection"] !== true
51077
+ piiDetection: flags["no-pii-detection"] !== true,
51078
+ responseTimeoutMs: parseResponseTimeoutMs(flags)
50588
51079
  },
50589
51080
  redactor
50590
51081
  );
@@ -53971,7 +54462,7 @@ async function sendMessageStreaming(client, params, redactor) {
53971
54462
  process.stdout.write(`${result2.assistant}
53972
54463
  `);
53973
54464
  }
53974
- return { ...result2, acceptedTaskProposals: [] };
54465
+ return { ...result2, taskEvents: [], pendingTaskUpdateJobs: [], acceptedTaskProposals: [] };
53975
54466
  }
53976
54467
  const urlResult = prepareUrlEmbeds(finalMessage);
53977
54468
  finalMessage = urlResult.message;
@@ -53986,6 +54477,7 @@ async function sendMessageStreaming(client, params, redactor) {
53986
54477
  onSubChatApprovalRequest,
53987
54478
  autoApproveSubChats: params.autoApproveSubChats,
53988
54479
  autoApproveMemories: params.autoApproveMemories,
54480
+ responseTimeoutMs: params.responseTimeoutMs,
53989
54481
  learningMode,
53990
54482
  preparedEmbeds: preparedEmbeds.length > 0 ? preparedEmbeds : void 0,
53991
54483
  piiMappings: piiResult.mappings.map((mapping) => ({
@@ -54077,6 +54569,24 @@ ${result.assistant}`) : [];
54077
54569
  process.stdout.write(`${SEP}
54078
54570
  `);
54079
54571
  }
54572
+ if (result.taskEvents.length > 0) {
54573
+ process.stdout.write(`\x1B[2mTask updates:\x1B[0m
54574
+ `);
54575
+ for (const event of result.taskEvents) {
54576
+ process.stdout.write(` \x1B[2m- ${formatTaskEvent(event)}\x1B[0m
54577
+ `);
54578
+ }
54579
+ process.stdout.write(`${SEP}
54580
+ `);
54581
+ }
54582
+ if (result.pendingTaskUpdateJobs.length > 0) {
54583
+ const count = result.pendingTaskUpdateJobs.length;
54584
+ process.stdout.write(
54585
+ `\x1B[2mPending encrypted task update${count === 1 ? "" : "s"}: ${count}\x1B[0m
54586
+ ${SEP}
54587
+ `
54588
+ );
54589
+ }
54080
54590
  process.stdout.write(
54081
54591
  `\x1B[2mContinue: openmates chats send --chat ${shortId} "your message"\x1B[0m
54082
54592
  \x1B[2mHistory: openmates chats show ${shortId}\x1B[0m
@@ -54085,6 +54595,26 @@ ${result.assistant}`) : [];
54085
54595
  }
54086
54596
  return { ...result, acceptedTaskProposals };
54087
54597
  }
54598
+ function formatTaskEvent(event) {
54599
+ const taskLabel = event.short_id || event.task_id;
54600
+ const title = event.title ? ` "${event.title}"` : "";
54601
+ const status = event.status ? ` (${event.status})` : "";
54602
+ const reason = event.reason ? `: ${event.reason}` : "";
54603
+ switch (event.event_type) {
54604
+ case "created":
54605
+ return `${taskLabel} created${title}${status}`;
54606
+ case "updated":
54607
+ return `${taskLabel} updated${title}${status}`;
54608
+ case "blocked":
54609
+ return `${taskLabel} blocked${reason}`;
54610
+ case "completed":
54611
+ return `${taskLabel} completed${title}`;
54612
+ case "unblocked":
54613
+ return `${taskLabel} unblocked`;
54614
+ default:
54615
+ return `${taskLabel} ${event.event_type}${title}${status}${reason}`;
54616
+ }
54617
+ }
54088
54618
  async function acceptChatTaskProposals(client, chatId, proposals, fallbackText) {
54089
54619
  let proposalsToAccept = proposals;
54090
54620
  if (proposalsToAccept.length === 0 && fallbackText.trim()) {