openmates 0.14.8-alpha.1 → 0.14.8-alpha.11

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,42 @@ 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
+ }
1242
+ function scopedErrorMissesPredicate(envelope, predicate) {
1243
+ if (envelope.type !== "error" || !predicate) return false;
1244
+ const payload = envelope.payload && typeof envelope.payload === "object" ? envelope.payload : {};
1245
+ const hasScope = typeof payload.turn_id === "string" || typeof payload.chat_id === "string" || typeof payload.message_id === "string" || typeof payload.user_message_id === "string" || typeof payload.userMessageId === "string" || typeof payload.job_id === "string";
1246
+ if (!hasScope) return false;
1247
+ try {
1248
+ return !predicate(payload);
1249
+ } catch {
1250
+ return false;
1251
+ }
1252
+ }
1217
1253
  function parseTaskProposals(value) {
1218
1254
  if (!Array.isArray(value)) return [];
1219
1255
  return value.flatMap((item) => {
@@ -1241,14 +1277,69 @@ function parseTaskUpdateProposals(value) {
1241
1277
  return [proposal];
1242
1278
  });
1243
1279
  }
1280
+ function parseTaskEvent(value) {
1281
+ if (!value || typeof value !== "object" || Array.isArray(value)) return null;
1282
+ const raw = value;
1283
+ if (typeof raw.event_id !== "string" || typeof raw.chat_id !== "string") return null;
1284
+ if (typeof raw.task_id !== "string" || typeof raw.event_type !== "string") return null;
1285
+ const event = {
1286
+ event_id: raw.event_id,
1287
+ chat_id: raw.chat_id,
1288
+ task_id: raw.task_id,
1289
+ event_type: raw.event_type
1290
+ };
1291
+ if (typeof raw.short_id === "string" || raw.short_id === null) event.short_id = raw.short_id;
1292
+ if (typeof raw.title === "string" || raw.title === null) event.title = raw.title;
1293
+ if (typeof raw.status === "string" || raw.status === null) event.status = raw.status;
1294
+ if (typeof raw.reason === "string" || raw.reason === null) event.reason = raw.reason;
1295
+ if (typeof raw.created_at === "number" || raw.created_at === null) event.created_at = raw.created_at;
1296
+ if (typeof raw.task_update_job_id === "string" || raw.task_update_job_id === null) event.task_update_job_id = raw.task_update_job_id;
1297
+ return event;
1298
+ }
1299
+ function parsePendingTaskUpdateJobs(value) {
1300
+ if (!Array.isArray(value)) return [];
1301
+ return value.flatMap((item) => {
1302
+ if (!item || typeof item !== "object" || Array.isArray(item)) return [];
1303
+ const raw = item;
1304
+ if (typeof raw.job_id !== "string" || typeof raw.task_id !== "string") return [];
1305
+ if (typeof raw.revision !== "number" || typeof raw.task_key_version !== "number" || typeof raw.expires_at !== "number") return [];
1306
+ const job = {
1307
+ job_id: raw.job_id,
1308
+ task_id: raw.task_id,
1309
+ revision: raw.revision,
1310
+ task_key_version: raw.task_key_version,
1311
+ expires_at: raw.expires_at
1312
+ };
1313
+ if (typeof raw.chat_id === "string" || raw.chat_id === null) job.chat_id = raw.chat_id;
1314
+ return [job];
1315
+ });
1316
+ }
1317
+ function parseAvailableRecoveryJobs(value) {
1318
+ if (!Array.isArray(value)) return [];
1319
+ return value.flatMap((item) => {
1320
+ if (!item || typeof item !== "object" || Array.isArray(item)) return [];
1321
+ const raw = item;
1322
+ 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 [];
1323
+ return [{
1324
+ job_id: raw.job_id,
1325
+ chat_id: raw.chat_id,
1326
+ turn_id: raw.turn_id,
1327
+ assistant_message_id: raw.assistant_message_id,
1328
+ chat_key_version: raw.chat_key_version
1329
+ }];
1330
+ });
1331
+ }
1244
1332
  var OpenMatesWsClient = class {
1245
1333
  socket;
1334
+ passiveTaskUpdateJobs = /* @__PURE__ */ new Map();
1335
+ activeResponseCollectors = 0;
1246
1336
  constructor(options) {
1247
1337
  const wsBase = options.apiUrl.replace(/^http/, "ws").replace(/\/$/, "");
1248
1338
  const token = options.wsToken || options.refreshToken || "";
1249
1339
  const query = new URLSearchParams({
1250
1340
  sessionId: options.sessionId,
1251
- token
1341
+ token,
1342
+ client_capabilities: "task_update_jobs"
1252
1343
  });
1253
1344
  const wsHeaders = {};
1254
1345
  if (options.userAgent) {
@@ -1263,6 +1354,10 @@ var OpenMatesWsClient = class {
1263
1354
  this.socket = new WebSocket(`${wsBase}/v1/ws?${query.toString()}`, {
1264
1355
  headers: wsHeaders
1265
1356
  });
1357
+ this.socket.on("message", (rawData) => {
1358
+ if (this.activeResponseCollectors > 0) return;
1359
+ this.bufferPassiveTaskUpdateJobs(rawData);
1360
+ });
1266
1361
  }
1267
1362
  async open(timeoutMs = 1e4) {
1268
1363
  await new Promise((resolve7, reject) => {
@@ -1308,6 +1403,22 @@ var OpenMatesWsClient = class {
1308
1403
  });
1309
1404
  });
1310
1405
  }
1406
+ bufferPassiveTaskUpdateJobs(rawData) {
1407
+ try {
1408
+ const parsed = JSON.parse(rawData.toString());
1409
+ if (parsed.type !== "task_update_jobs_available") return;
1410
+ const payload = parsed.payload ?? {};
1411
+ for (const job of parsePendingTaskUpdateJobs(payload.jobs)) {
1412
+ this.passiveTaskUpdateJobs.set(job.job_id, job);
1413
+ }
1414
+ } catch {
1415
+ }
1416
+ }
1417
+ drainPassiveTaskUpdateJobs() {
1418
+ const jobs = [...this.passiveTaskUpdateJobs.values()];
1419
+ this.passiveTaskUpdateJobs.clear();
1420
+ return jobs;
1421
+ }
1311
1422
  waitForMessage(expectedType, predicate, timeoutMs = 2e4) {
1312
1423
  return new Promise((resolve7, reject) => {
1313
1424
  const seenTypes = /* @__PURE__ */ new Set();
@@ -1318,6 +1429,7 @@ var OpenMatesWsClient = class {
1318
1429
  if (typeof parsed.type === "string") seenTypes.add(parsed.type);
1319
1430
  const protocolError = websocketProtocolError(parsed);
1320
1431
  if (protocolError) {
1432
+ if (scopedErrorMissesPredicate(parsed, predicate)) return;
1321
1433
  cleanup();
1322
1434
  reject(protocolError);
1323
1435
  return;
@@ -1443,6 +1555,14 @@ var OpenMatesWsClient = class {
1443
1555
  let newChatSuggestions = [];
1444
1556
  let taskProposals = [];
1445
1557
  let taskUpdateProposals = [];
1558
+ const taskEvents = [];
1559
+ this.activeResponseCollectors += 1;
1560
+ let pendingTaskUpdateJobs = this.drainPassiveTaskUpdateJobs();
1561
+ const mergePendingTaskUpdateJobs = (jobs) => {
1562
+ const byId = new Map(pendingTaskUpdateJobs.map((job) => [job.job_id, job]));
1563
+ for (const job of jobs) byId.set(job.job_id, job);
1564
+ pendingTaskUpdateJobs = [...byId.values()];
1565
+ };
1446
1566
  const subChatEvents = [];
1447
1567
  const pendingSubChatHandlers = /* @__PURE__ */ new Set();
1448
1568
  const pendingMemoryRequestHandlers = /* @__PURE__ */ new Set();
@@ -1505,6 +1625,8 @@ var OpenMatesWsClient = class {
1505
1625
  newChatSuggestions,
1506
1626
  taskProposals,
1507
1627
  taskUpdateProposals,
1628
+ taskEvents,
1629
+ pendingTaskUpdateJobs,
1508
1630
  embeds: [...embeds.values()],
1509
1631
  subChatEvents,
1510
1632
  recoveryJobId
@@ -1512,6 +1634,7 @@ var OpenMatesWsClient = class {
1512
1634
  return;
1513
1635
  }
1514
1636
  if (!aiResponseDone || !postProcessingDone) return;
1637
+ if (options?.recoveryTurnId && !recoveryJobId) return;
1515
1638
  if (pendingSubChatHandlers.size > 0) return;
1516
1639
  if (pendingMemoryRequestHandlers.size > 0) return;
1517
1640
  if (processingEmbedIds.size > 0 && !asyncEmbedTimer) {
@@ -1528,6 +1651,8 @@ var OpenMatesWsClient = class {
1528
1651
  newChatSuggestions,
1529
1652
  taskProposals,
1530
1653
  taskUpdateProposals,
1654
+ taskEvents,
1655
+ pendingTaskUpdateJobs,
1531
1656
  embeds: [...embeds.values()],
1532
1657
  subChatEvents,
1533
1658
  recoveryJobId
@@ -1548,6 +1673,8 @@ var OpenMatesWsClient = class {
1548
1673
  newChatSuggestions,
1549
1674
  taskProposals,
1550
1675
  taskUpdateProposals,
1676
+ taskEvents,
1677
+ pendingTaskUpdateJobs,
1551
1678
  embeds: [...embeds.values()],
1552
1679
  subChatEvents,
1553
1680
  recoveryJobId
@@ -1624,6 +1751,9 @@ var OpenMatesWsClient = class {
1624
1751
  const type = parsed.type;
1625
1752
  const protocolError = websocketProtocolError(parsed);
1626
1753
  if (protocolError) {
1754
+ if (!errorFrameBelongsToAiResponse(parsed, userMessageId, chatId, options?.recoveryTurnId)) {
1755
+ return;
1756
+ }
1627
1757
  cleanup();
1628
1758
  reject(protocolError);
1629
1759
  return;
@@ -1654,6 +1784,29 @@ var OpenMatesWsClient = class {
1654
1784
  }
1655
1785
  return;
1656
1786
  }
1787
+ if (type === "task_event") {
1788
+ const event = parseTaskEvent(p);
1789
+ if (!event || event.chat_id !== chatId) return;
1790
+ taskEvents.push(event);
1791
+ maybeResolve();
1792
+ return;
1793
+ }
1794
+ if (type === "task_update_jobs_available") {
1795
+ mergePendingTaskUpdateJobs(parsePendingTaskUpdateJobs(p.jobs));
1796
+ maybeResolve();
1797
+ return;
1798
+ }
1799
+ if (type === "recovery_jobs_available") {
1800
+ const job = parseAvailableRecoveryJobs(p.jobs).find(
1801
+ (candidate) => candidate.chat_id === chatId && (!options?.recoveryTurnId || candidate.turn_id === options.recoveryTurnId)
1802
+ );
1803
+ if (!job) return;
1804
+ recoveryJobId = job.job_id;
1805
+ messageId = job.assistant_message_id;
1806
+ if (aiResponseDone) maybeResolve();
1807
+ else scheduleResolve(latestContent);
1808
+ return;
1809
+ }
1657
1810
  if (type === "ai_message_update") {
1658
1811
  const msgId = p.user_message_id ?? p.userMessageId;
1659
1812
  if (msgId !== userMessageId && p.chat_id !== chatId) return;
@@ -1764,6 +1917,8 @@ var OpenMatesWsClient = class {
1764
1917
  newChatSuggestions,
1765
1918
  taskProposals,
1766
1919
  taskUpdateProposals,
1920
+ taskEvents,
1921
+ pendingTaskUpdateJobs,
1767
1922
  embeds: [...embeds.values()],
1768
1923
  subChatEvents,
1769
1924
  recoveryJobId
@@ -1786,6 +1941,7 @@ var OpenMatesWsClient = class {
1786
1941
  this.socket.off("message", onMessage);
1787
1942
  this.socket.off("error", onError);
1788
1943
  this.socket.off("close", onClose);
1944
+ this.activeResponseCollectors = Math.max(0, this.activeResponseCollectors - 1);
1789
1945
  };
1790
1946
  this.socket.on("message", onMessage);
1791
1947
  this.socket.on("error", onError);
@@ -2514,6 +2670,227 @@ function toArrayBuffer3(input) {
2514
2670
  return output;
2515
2671
  }
2516
2672
 
2673
+ // src/tasksCli.ts
2674
+ import { createHash as createHash5, randomBytes as randomBytes2, randomUUID as randomUUID3 } from "crypto";
2675
+ var TASK_STATUSES2 = ["backlog", "todo", "in_progress", "blocked", "done"];
2676
+ var DEFAULT_STANDALONE_PREFIX = "TASK";
2677
+ function normalizeTaskStatus(value) {
2678
+ if (value === void 0) return void 0;
2679
+ if (TASK_STATUSES2.includes(value)) return value;
2680
+ throw new Error(`Unknown task status '${value}'. Expected one of: ${TASK_STATUSES2.join(", ")}`);
2681
+ }
2682
+ function parseAssignee(value) {
2683
+ if (!value || value === "user") return { assigneeType: "user", assigneeHash: null };
2684
+ if (["ai", "openmates", "OpenMates"].includes(value)) return { assigneeType: "ai", assigneeHash: null };
2685
+ return { assigneeType: "user", assigneeHash: value };
2686
+ }
2687
+ function splitCsvFlag(value) {
2688
+ if (typeof value !== "string") return [];
2689
+ return value.split(/[,\n]/).map((item) => item.trim()).filter(Boolean);
2690
+ }
2691
+ function parseDueAt(value) {
2692
+ if (value === void 0) return void 0;
2693
+ if (value === false || value === true) throw new Error("--due requires a timestamp or date value.");
2694
+ const numeric = Number(value);
2695
+ if (Number.isFinite(numeric) && numeric > 0) return Math.floor(numeric);
2696
+ const parsed = Date.parse(value);
2697
+ if (Number.isNaN(parsed)) throw new Error(`Invalid --due value '${value}'.`);
2698
+ return Math.floor(parsed / 1e3);
2699
+ }
2700
+ async function buildCreateUserTaskInput(masterKey, input) {
2701
+ const taskKey = randomBytes2(32);
2702
+ const encryptedTaskKey = await encryptBytesWithAesGcm(taskKey, masterKey);
2703
+ const timestamp = nowSeconds();
2704
+ const assignee = parseAssignee(input.assign);
2705
+ const linkedProjectIds = input.projectIds ?? [];
2706
+ const status = input.status ?? (assignee.assigneeType === "ai" && !input.dueAt ? "in_progress" : "todo");
2707
+ return {
2708
+ task_id: randomUUIDCompat(),
2709
+ short_id: void 0,
2710
+ version: 1,
2711
+ encrypted_task_key: encryptedTaskKey,
2712
+ encrypted_title: await encryptWithAesGcmCombined(input.title, taskKey),
2713
+ encrypted_description: await encryptWithAesGcmCombined(input.description ?? "", taskKey),
2714
+ encrypted_tags: await encryptWithAesGcmCombined("[]", taskKey),
2715
+ encrypted_linked_project_ids: await encryptWithAesGcmCombined(JSON.stringify(linkedProjectIds), taskKey),
2716
+ status,
2717
+ assignee_type: assignee.assigneeType,
2718
+ assignee_hash: assignee.assigneeHash,
2719
+ primary_chat_id: input.chatId ?? null,
2720
+ linked_project_ids: linkedProjectIds,
2721
+ plan_id: input.planId ?? null,
2722
+ due_at: input.dueAt ?? null,
2723
+ priority: 0,
2724
+ position: timestamp,
2725
+ created_at: timestamp,
2726
+ updated_at: timestamp
2727
+ };
2728
+ }
2729
+ async function buildUpdateUserTaskInput(task, masterKey, input) {
2730
+ const taskKey = await taskKeyFromRecord(task.encrypted, masterKey);
2731
+ const patch = { version: task.version, updated_at: nowSeconds() };
2732
+ if (input.title !== void 0) patch.encrypted_title = await encryptWithAesGcmCombined(input.title, taskKey);
2733
+ if (input.description !== void 0) patch.encrypted_description = await encryptWithAesGcmCombined(input.description, taskKey);
2734
+ if (input.status !== void 0) patch.status = input.status;
2735
+ if (input.assign !== void 0) {
2736
+ const assignee = parseAssignee(input.assign);
2737
+ patch.assignee_type = assignee.assigneeType;
2738
+ patch.assignee_hash = assignee.assigneeHash;
2739
+ }
2740
+ if (input.chatId !== void 0) patch.primary_chat_id = input.chatId;
2741
+ if (input.projectIds !== void 0) {
2742
+ patch.linked_project_ids = input.projectIds;
2743
+ patch.encrypted_linked_project_ids = await encryptWithAesGcmCombined(JSON.stringify(input.projectIds), taskKey);
2744
+ }
2745
+ if (input.planId !== void 0) patch.plan_id = input.planId;
2746
+ return patch;
2747
+ }
2748
+ async function decryptUserTask(record, masterKey) {
2749
+ if (typeof record.version !== "number") throw new Error(`Task ${record.task_id} is missing version.`);
2750
+ const taskKey = await taskKeyFromRecord(record, masterKey);
2751
+ const tags = parseStringArray(await decryptOptional(record.encrypted_tags, taskKey));
2752
+ const linkedProjectIds = parseStringArray(await decryptOptional(record.encrypted_linked_project_ids, taskKey));
2753
+ return {
2754
+ taskId: record.task_id,
2755
+ shortId: record.short_id || deriveShortId(record),
2756
+ title: await decryptOptional(record.encrypted_title, taskKey) || "(untitled task)",
2757
+ description: await decryptOptional(record.encrypted_description, taskKey),
2758
+ tags,
2759
+ latestInstruction: await decryptOptional(record.encrypted_latest_instruction, taskKey),
2760
+ status: record.status,
2761
+ assigneeType: record.assignee_type,
2762
+ assigneeHash: record.assignee_hash ?? null,
2763
+ primaryChatId: record.primary_chat_id ?? null,
2764
+ linkedProjectIds: linkedProjectIds.length > 0 ? linkedProjectIds : record.linked_project_ids ?? [],
2765
+ planId: record.plan_id ?? null,
2766
+ dueAt: record.due_at ?? null,
2767
+ priority: record.priority ?? 0,
2768
+ position: record.position ?? 0,
2769
+ queueState: record.queue_state ?? "none",
2770
+ blockedReasonCode: record.blocked_reason_code ?? null,
2771
+ aiExecutionState: record.ai_execution_state ?? null,
2772
+ version: record.version,
2773
+ encrypted: record
2774
+ };
2775
+ }
2776
+ async function decryptUserTasks(records, masterKey) {
2777
+ const output = [];
2778
+ for (const record of records) output.push(await decryptUserTask(record, masterKey));
2779
+ return output;
2780
+ }
2781
+ function findTask(tasks, id) {
2782
+ const taskIdMatch = tasks.find((candidate) => candidate.taskId === id);
2783
+ if (taskIdMatch) return taskIdMatch;
2784
+ const shortIdMatches = tasks.filter((candidate) => candidate.shortId === id);
2785
+ if (shortIdMatches.length > 1) throw new Error(`Task '${id}' is ambiguous in the current task list. Use the full task ID.`);
2786
+ const task = shortIdMatches[0];
2787
+ if (!task) throw new Error(`Task '${id}' was not found in the current task list.`);
2788
+ return task;
2789
+ }
2790
+ function renderTaskList(tasks) {
2791
+ if (tasks.length === 0) return "No tasks found.";
2792
+ const lines = ["Tasks", "ID Status Assignee Queue Title"];
2793
+ for (const task of tasks) {
2794
+ lines.push(`${pad(task.shortId, 9)} ${pad(task.status, 12)} ${pad(assigneeLabel(task), 11)} ${pad(task.queueState, 11)} ${task.title}`);
2795
+ }
2796
+ return lines.join("\n");
2797
+ }
2798
+ function renderTaskDetail(task) {
2799
+ const lines = [
2800
+ `Task ${task.shortId}`,
2801
+ `Title: ${task.title}`,
2802
+ `Status: ${task.status}`,
2803
+ `Assignee: ${assigneeLabel(task)}`,
2804
+ `Queue: ${task.queueState}`,
2805
+ `Task ID: ${task.taskId}`
2806
+ ];
2807
+ if (task.description) lines.push(`Description: ${task.description}`);
2808
+ if (task.primaryChatId) lines.push(`Chat: ${task.primaryChatId}`);
2809
+ if (task.linkedProjectIds.length > 0) lines.push(`Projects: ${task.linkedProjectIds.join(", ")}`);
2810
+ if (task.planId) lines.push(`Plan: ${task.planId}`);
2811
+ if (task.blockedReasonCode) lines.push(`Blocked reason: ${task.blockedReasonCode}`);
2812
+ if (task.aiExecutionState) lines.push(`AI state: ${task.aiExecutionState}`);
2813
+ return lines.join("\n");
2814
+ }
2815
+ function renderTaskBoard(tasks, width = process.stdout.columns || 100) {
2816
+ const columns = TASK_STATUSES2.map((status) => ({ status, tasks: tasks.filter((task) => task.status === status).sort(compareTasks) }));
2817
+ if (width < 96) {
2818
+ const lines2 = ["OpenMates Tasks Board"];
2819
+ for (const column of columns) {
2820
+ lines2.push("", `${columnTitle(column.status)} (${column.tasks.length})`, "-".repeat(24));
2821
+ lines2.push(...boardColumnLines(column.tasks, 8));
2822
+ }
2823
+ return lines2.join("\n");
2824
+ }
2825
+ const perColumn = 6;
2826
+ const columnWidth = 22;
2827
+ const lines = ["OpenMates Tasks Board", ""];
2828
+ lines.push(columns.map((column) => pad(`${columnTitle(column.status)} (${column.tasks.length})`, columnWidth)).join(" "));
2829
+ lines.push(columns.map(() => "-".repeat(columnWidth)).join(" "));
2830
+ const renderedColumns = columns.map((column) => boardColumnLines(column.tasks, perColumn).map((line) => truncate(line, columnWidth)));
2831
+ const maxRows = Math.max(...renderedColumns.map((column) => column.length));
2832
+ for (let row = 0; row < maxRows; row += 1) {
2833
+ lines.push(renderedColumns.map((column) => pad(column[row] ?? "", columnWidth)).join(" "));
2834
+ }
2835
+ return lines.join("\n");
2836
+ }
2837
+ function boardColumnLines(tasks, limit) {
2838
+ if (tasks.length === 0) return ["No tasks here."];
2839
+ const visible = tasks.slice(0, limit).flatMap((task) => [
2840
+ `[${task.shortId}] ${task.title}`,
2841
+ ` ${assigneeLabel(task)} ${task.queueState === "none" ? "" : task.queueState}`.trimEnd()
2842
+ ]);
2843
+ if (tasks.length > limit) visible.push(`... ${tasks.length - limit} more`);
2844
+ return visible;
2845
+ }
2846
+ function compareTasks(a, b) {
2847
+ return a.position - b.position || a.title.localeCompare(b.title);
2848
+ }
2849
+ function columnTitle(status) {
2850
+ if (status === "in_progress") return "In progress";
2851
+ return status.slice(0, 1).toUpperCase() + status.slice(1).replace("_", " ");
2852
+ }
2853
+ function assigneeLabel(task) {
2854
+ return task.assigneeType === "ai" ? "OpenMates" : task.assigneeHash ?? "user";
2855
+ }
2856
+ async function taskKeyFromRecord(record, masterKey) {
2857
+ if (!record.encrypted_task_key) throw new Error(`Task ${record.task_id} is missing encrypted task key.`);
2858
+ const taskKey = await decryptBytesWithAesGcm(record.encrypted_task_key, masterKey);
2859
+ if (!taskKey) throw new Error(`Failed to decrypt task key for ${record.task_id}.`);
2860
+ return taskKey;
2861
+ }
2862
+ async function decryptOptional(value, key) {
2863
+ if (!value) return "";
2864
+ return await decryptWithAesGcmCombined(value, key) ?? "";
2865
+ }
2866
+ function parseStringArray(value) {
2867
+ if (!value) return [];
2868
+ try {
2869
+ const parsed = JSON.parse(value);
2870
+ return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
2871
+ } catch {
2872
+ return [];
2873
+ }
2874
+ }
2875
+ function deriveShortId(record) {
2876
+ const prefix = record.short_id_prefix || DEFAULT_STANDALONE_PREFIX;
2877
+ const source = record.task_id || `${record.created_at ?? ""}-${record.position ?? ""}`;
2878
+ const digest = createHash5("sha256").update(source).digest("hex").slice(0, 4).toUpperCase();
2879
+ return `${prefix}-${parseInt(digest, 16) % 1e4}`;
2880
+ }
2881
+ function nowSeconds() {
2882
+ return Math.floor(Date.now() / 1e3);
2883
+ }
2884
+ function randomUUIDCompat() {
2885
+ return randomUUID3();
2886
+ }
2887
+ function pad(value, length) {
2888
+ return truncate(value, length).padEnd(length);
2889
+ }
2890
+ function truncate(value, length) {
2891
+ return value.length <= length ? value : `${value.slice(0, Math.max(0, length - 3))}...`;
2892
+ }
2893
+
2517
2894
  // src/client.ts
2518
2895
  function normalizeUnixSeconds(value, fallback) {
2519
2896
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
@@ -2681,6 +3058,83 @@ function buildAppSettingsMemoryResponseSystemMessage(params) {
2681
3058
  user_message_id: params.userMessageId
2682
3059
  };
2683
3060
  }
3061
+ async function buildTaskEventSystemMessage(params) {
3062
+ const content = formatTaskEventSystemContent(params.event);
3063
+ const message = {
3064
+ message_id: `task-event-${params.event.event_id}`,
3065
+ role: "system",
3066
+ encrypted_content: await encryptWithAesGcmCombined(content, params.chatKey),
3067
+ created_at: params.event.created_at ?? Math.floor(Date.now() / 1e3),
3068
+ user_message_id: params.userMessageId
3069
+ };
3070
+ if (params.event.task_update_job_id) message.task_update_job_id = params.event.task_update_job_id;
3071
+ return message;
3072
+ }
3073
+ function taskUpdateJobBelongsToActiveTurn(job, activeChatId, taskEvents) {
3074
+ void activeChatId;
3075
+ return taskEvents.some((event) => event.task_update_job_id === job.job_id);
3076
+ }
3077
+ function buildTaskUpdateJobPersistPayload(params) {
3078
+ const encryptedTaskPayload = pruneAbsentTaskPersistFields(params.encryptedTaskPayload);
3079
+ assertTaskPersistPayloadEncrypted(encryptedTaskPayload);
3080
+ return {
3081
+ protocol_version: 1,
3082
+ job_id: params.jobId,
3083
+ lease_token: params.leaseToken,
3084
+ lease_generation: params.leaseGeneration,
3085
+ expected_task_version: params.expectedTaskVersion,
3086
+ encrypted_task_payload: encryptedTaskPayload,
3087
+ encrypted_task_event_message: params.encryptedTaskEventMessage ?? null
3088
+ };
3089
+ }
3090
+ function pruneAbsentTaskPersistFields(payload) {
3091
+ return Object.fromEntries(
3092
+ Object.entries(payload).filter(([, value]) => value !== void 0 && value !== null)
3093
+ );
3094
+ }
3095
+ function formatTaskEventSystemContent(event) {
3096
+ const taskLabel = event.short_id || event.task_id;
3097
+ const title = event.title ? ` "${event.title}"` : "";
3098
+ const status = event.status ? ` (${event.status})` : "";
3099
+ const reason = event.reason ? `: ${event.reason}` : "";
3100
+ switch (event.event_type) {
3101
+ case "created":
3102
+ return `${taskLabel} created${title}${status}`;
3103
+ case "updated":
3104
+ return `${taskLabel} updated${title}${status}`;
3105
+ case "blocked":
3106
+ return `${taskLabel} blocked${reason}`;
3107
+ case "completed":
3108
+ return `${taskLabel} completed${title}`;
3109
+ case "unblocked":
3110
+ return `${taskLabel} unblocked`;
3111
+ default:
3112
+ return `${taskLabel} ${event.event_type}${title}${status}${reason}`;
3113
+ }
3114
+ }
3115
+ function assertTaskPersistPayloadEncrypted(payload) {
3116
+ const allowedSafeKeys = /* @__PURE__ */ new Set([
3117
+ "assignee_hash",
3118
+ "assignee_type",
3119
+ "blocked_reason_code",
3120
+ "created_at",
3121
+ "key_wrappers",
3122
+ "linked_project_ids",
3123
+ "position",
3124
+ "primary_chat_id",
3125
+ "priority",
3126
+ "status",
3127
+ "task_id",
3128
+ "updated_at",
3129
+ "version"
3130
+ ]);
3131
+ for (const key of Object.keys(payload)) {
3132
+ if (key.startsWith("encrypted_") || allowedSafeKeys.has(key)) {
3133
+ continue;
3134
+ }
3135
+ throw new Error("Task update job payload contains plaintext or unsupported field");
3136
+ }
3137
+ }
2684
3138
  async function buildSubChatEncryptedMetadataPayloads(params) {
2685
3139
  const createdAt = params.createdAt ?? Math.floor(Date.now() / 1e3);
2686
3140
  const payloads = [];
@@ -3354,11 +3808,11 @@ var OpenMatesClient = class _OpenMatesClient {
3354
3808
  }
3355
3809
  let anonymousId = loadAnonymousId();
3356
3810
  if (!anonymousId) {
3357
- anonymousId = randomUUID3();
3811
+ anonymousId = randomUUID4();
3358
3812
  saveAnonymousId(anonymousId);
3359
3813
  }
3360
- const chatId = `anonymous-${randomUUID3()}`;
3361
- const messageId = `anonymous-message-${randomUUID3()}`;
3814
+ const chatId = `anonymous-${randomUUID4()}`;
3815
+ const messageId = `anonymous-message-${randomUUID4()}`;
3362
3816
  const requestBody = {
3363
3817
  anonymous_id: anonymousId,
3364
3818
  client_chat_id: chatId,
@@ -3552,7 +4006,7 @@ var OpenMatesClient = class _OpenMatesClient {
3552
4006
  pin: pin.trim().toUpperCase(),
3553
4007
  token
3554
4008
  });
3555
- const sessionId = randomUUID3();
4009
+ const sessionId = randomUUID4();
3556
4010
  const login = await this.http.post(
3557
4011
  "/v1/auth/login",
3558
4012
  {
@@ -3727,7 +4181,7 @@ var OpenMatesClient = class _OpenMatesClient {
3727
4181
  }
3728
4182
  const session = {
3729
4183
  apiUrl: this.apiUrl,
3730
- sessionId: randomUUID3(),
4184
+ sessionId: randomUUID4(),
3731
4185
  wsToken: null,
3732
4186
  cookies: this.http.getCookieMap(),
3733
4187
  masterKeyExportedB64: material.masterKeyB64,
@@ -3887,7 +4341,7 @@ var OpenMatesClient = class _OpenMatesClient {
3887
4341
  if (!markdown) throw new Error("Draft markdown must not be empty.");
3888
4342
  const masterKey = this.getMasterKeyBytes();
3889
4343
  const encrypted = await this.saveEncryptedDraft({
3890
- chatId: params.chatId ?? randomUUID3(),
4344
+ chatId: params.chatId ?? randomUUID4(),
3891
4345
  encryptedDraftMd: await encryptWithAesGcmCombined(markdown, masterKey),
3892
4346
  encryptedDraftPreview: params.preview ? await encryptWithAesGcmCombined(params.preview, masterKey) : null
3893
4347
  });
@@ -4443,7 +4897,7 @@ var OpenMatesClient = class _OpenMatesClient {
4443
4897
  async sendMessage(params) {
4444
4898
  let chatId;
4445
4899
  if (!params.chatId) {
4446
- chatId = randomUUID3();
4900
+ chatId = randomUUID4();
4447
4901
  } else if (params.chatId.length < 36) {
4448
4902
  const resolved = await this.resolveFullChatId(params.chatId);
4449
4903
  if (!resolved) {
@@ -4477,7 +4931,7 @@ var OpenMatesClient = class _OpenMatesClient {
4477
4931
  ws.close();
4478
4932
  throw new Error("Authenticated user identity is required for saved chat recovery.");
4479
4933
  }
4480
- const messageId = randomUUID3();
4934
+ const messageId = randomUUID4();
4481
4935
  const createdAt = Math.floor(Date.now() / 1e3);
4482
4936
  const isNewChat = !params.chatId;
4483
4937
  ws.send("set_active_chat", { chat_id: chatId });
@@ -4528,6 +4982,7 @@ var OpenMatesClient = class _OpenMatesClient {
4528
4982
  }
4529
4983
  const messagePayload = {
4530
4984
  chat_id: chatId,
4985
+ client_capabilities: ["task_update_jobs"],
4531
4986
  is_incognito: Boolean(params.incognito),
4532
4987
  message: {
4533
4988
  message_id: messageId,
@@ -4611,11 +5066,11 @@ var OpenMatesClient = class _OpenMatesClient {
4611
5066
  if (encryptedEmbeds.length > 0) {
4612
5067
  messagePayload.encrypted_embeds = encryptedEmbeds;
4613
5068
  }
4614
- let precollectedResponse = params.precollectResponse && params.incognito ? ws.collectAiResponse(messageId, chatId, { onStream: params.onStream }) : null;
5069
+ let precollectedResponse = params.precollectResponse && params.incognito ? ws.collectAiResponse(messageId, chatId, { onStream: params.onStream, timeoutMs: params.responseTimeoutMs }) : null;
4615
5070
  if (!params.incognito && chatKeyBytes && encryptedChatKey) {
4616
5071
  const protocolVersion = 1;
4617
5072
  const chatKeyVersion = 1;
4618
- const turnId = randomUUID3();
5073
+ const turnId = randomUUID4();
4619
5074
  savedTurnId = turnId;
4620
5075
  terminalExpectedMessagesV = baselineMessagesV + 1;
4621
5076
  const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
@@ -4666,7 +5121,10 @@ var OpenMatesClient = class _OpenMatesClient {
4666
5121
  updated_at: createdAt
4667
5122
  };
4668
5123
  }
4669
- const preflightAck = ws.waitForMessage("chat_turn_preflight_ack");
5124
+ const preflightAck = ws.waitForMessage(
5125
+ "chat_turn_preflight_ack",
5126
+ (payload) => payload.turn_id === turnId
5127
+ );
4670
5128
  let ackPayload;
4671
5129
  try {
4672
5130
  await ws.sendAsync("chat_turn_preflight", preflightPayload);
@@ -4689,7 +5147,9 @@ var OpenMatesClient = class _OpenMatesClient {
4689
5147
  }
4690
5148
  if (params.precollectResponse && !params.incognito) {
4691
5149
  precollectedResponse = ws.collectAiResponse(messageId, chatId, {
4692
- onStream: params.onStream
5150
+ onStream: params.onStream,
5151
+ timeoutMs: params.responseTimeoutMs,
5152
+ recoveryTurnId: savedTurnId
4693
5153
  });
4694
5154
  }
4695
5155
  const confirmed = ws.waitForMessage(
@@ -4712,6 +5172,8 @@ var OpenMatesClient = class _OpenMatesClient {
4712
5172
  let followUpSuggestions = [];
4713
5173
  let taskProposals = [];
4714
5174
  let taskUpdateProposals = [];
5175
+ let taskEvents = [];
5176
+ let pendingTaskUpdateJobs = [];
4715
5177
  let subChatEvents = [];
4716
5178
  const appSettingsMemoryRequests = [];
4717
5179
  const numberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
@@ -4874,6 +5336,225 @@ var OpenMatesClient = class _OpenMatesClient {
4874
5336
  `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
5337
  );
4876
5338
  };
5339
+ const persistEncryptedSystemMessage = async (systemMessage, targetChatId = chatId) => {
5340
+ await ws.sendAsync("chat_system_message_added", {
5341
+ chat_id: targetChatId,
5342
+ message: systemMessage
5343
+ });
5344
+ await ws.waitForMessage(
5345
+ "system_message_confirmed",
5346
+ (payload) => payload.message_id === systemMessage.message_id,
5347
+ 2e4
5348
+ );
5349
+ };
5350
+ const persistedTaskEventIds = /* @__PURE__ */ new Set();
5351
+ const persistTaskEventSystemMessages = async (events) => {
5352
+ if (!chatKeyBytes || events.length === 0) return;
5353
+ for (const event of events) {
5354
+ if (persistedTaskEventIds.has(event.event_id)) continue;
5355
+ await persistEncryptedSystemMessage(await buildTaskEventSystemMessage({
5356
+ chatKey: chatKeyBytes,
5357
+ userMessageId: messageId,
5358
+ event
5359
+ }));
5360
+ persistedTaskEventIds.add(event.event_id);
5361
+ }
5362
+ };
5363
+ const persistPendingTaskUpdateJobs = async (jobs, events) => {
5364
+ const handledJobIds = /* @__PURE__ */ new Set();
5365
+ if (jobs.length === 0) return handledJobIds;
5366
+ const masterKey = this.getMasterKeyBytes();
5367
+ let decryptedTasksCache = null;
5368
+ const eventByJobId = new Map(events.map((event) => [event.task_update_job_id, event]));
5369
+ const buildTaskKeyWrappersForChat = async (task, targetChatId, createdAt2) => {
5370
+ const encryptedTaskKey = task.encrypted.encrypted_task_key;
5371
+ if (!encryptedTaskKey) throw new Error(`Task ${task.taskId} is missing encrypted task key.`);
5372
+ const taskKey = await decryptBytesWithAesGcm(encryptedTaskKey, masterKey);
5373
+ if (!taskKey) throw new Error(`Failed to decrypt task key for ${task.taskId}.`);
5374
+ const targetChatKey = await resolveChatKey(targetChatId);
5375
+ const existingProjectWrappers = await listProjectTaskKeyWrappers(task.taskId, createdAt2);
5376
+ const linkedProjectIds = task.linkedProjectIds ?? [];
5377
+ if (linkedProjectIds.length > existingProjectWrappers.length) {
5378
+ throw new Error(`Task ${task.taskId} is missing project key wrappers required for move persistence.`);
5379
+ }
5380
+ return [
5381
+ {
5382
+ key_type: "master",
5383
+ encrypted_task_key: await encryptBytesWithAesGcm(taskKey, masterKey),
5384
+ created_at: createdAt2
5385
+ },
5386
+ {
5387
+ key_type: "chat",
5388
+ hashed_chat_id: computeSHA256(targetChatId),
5389
+ encrypted_task_key: await encryptBytesWithAesGcm(taskKey, targetChatKey),
5390
+ created_at: createdAt2
5391
+ },
5392
+ ...existingProjectWrappers
5393
+ ];
5394
+ };
5395
+ const listProjectTaskKeyWrappers = async (taskId, createdAt2) => {
5396
+ const response = await this.http.get(
5397
+ `/v1/user-tasks/${encodeURIComponent(taskId)}/key-wrappers`,
5398
+ this.getCliRequestHeaders()
5399
+ );
5400
+ if (!response.ok) {
5401
+ throw new Error(`User task key wrapper list failed with HTTP ${response.status}`);
5402
+ }
5403
+ return (response.data.key_wrappers ?? []).filter((wrapper) => wrapper.key_type === "project").map((wrapper) => ({
5404
+ key_type: "project",
5405
+ hashed_project_id: wrapper.hashed_project_id,
5406
+ encrypted_task_key: wrapper.encrypted_task_key,
5407
+ created_at: typeof wrapper.created_at === "number" ? wrapper.created_at : createdAt2,
5408
+ expires_at: wrapper.expires_at ?? null
5409
+ }));
5410
+ };
5411
+ const resolveChatKey = async (targetChatId) => {
5412
+ if (targetChatId === chatId && chatKeyBytes) return chatKeyBytes;
5413
+ const cache = loadSyncCache() ?? await this.ensureSynced();
5414
+ const targetChat = cache.chats.find((chat) => String(chat.details.id ?? "") === targetChatId);
5415
+ const encryptedTargetChatKey = typeof targetChat?.details.encrypted_chat_key === "string" ? targetChat.details.encrypted_chat_key : null;
5416
+ if (!encryptedTargetChatKey) {
5417
+ throw new Error(`Encrypted chat key not found for task move target '${targetChatId}'. Sync and try again.`);
5418
+ }
5419
+ const targetChatKey = await decryptBytesWithAesGcm(encryptedTargetChatKey, masterKey);
5420
+ if (!targetChatKey) {
5421
+ throw new Error(`Failed to decrypt chat key for task move target '${targetChatId}'.`);
5422
+ }
5423
+ return targetChatKey;
5424
+ };
5425
+ const taskEventTypeForOperation = (operation) => {
5426
+ switch (operation) {
5427
+ case "create":
5428
+ return "created";
5429
+ case "move":
5430
+ return "moved";
5431
+ case "update":
5432
+ return "updated";
5433
+ default:
5434
+ return operation || "updated";
5435
+ }
5436
+ };
5437
+ for (const job of jobs) {
5438
+ if (!taskUpdateJobBelongsToActiveTurn(job, chatId, events)) {
5439
+ handledJobIds.add(job.job_id);
5440
+ continue;
5441
+ }
5442
+ const claimPromise = ws.waitForMessage(
5443
+ "task_update_job_claimed",
5444
+ (payload) => payload.job_id === job.job_id,
5445
+ 2e4
5446
+ );
5447
+ await ws.sendAsync("task_update_job_claim", {
5448
+ protocol_version: 1,
5449
+ job_id: job.job_id
5450
+ });
5451
+ const claim = (await claimPromise).payload;
5452
+ const privatePatch = claim.private_patch ?? {};
5453
+ const safeMetadata = claim.safe_metadata ?? {};
5454
+ const sourceChatId = claim.chat_id ?? job.chat_id ?? chatId;
5455
+ const sourceChatKey = await resolveChatKey(sourceChatId);
5456
+ const event = eventByJobId.get(job.job_id) ?? {
5457
+ event_id: `task-event-${job.job_id}`,
5458
+ chat_id: sourceChatId,
5459
+ task_id: claim.task_id,
5460
+ event_type: taskEventTypeForOperation(claim.operation),
5461
+ title: typeof privatePatch.title === "string" ? privatePatch.title : null,
5462
+ status: typeof safeMetadata.status === "string" ? safeMetadata.status : null,
5463
+ created_at: typeof safeMetadata.updated_at === "number" ? safeMetadata.updated_at : Math.floor(Date.now() / 1e3),
5464
+ task_update_job_id: job.job_id
5465
+ };
5466
+ const eventMessage = await buildTaskEventSystemMessage({
5467
+ chatKey: sourceChatKey,
5468
+ userMessageId: claim.message_id ?? messageId,
5469
+ event
5470
+ });
5471
+ const confirmTaskEventPersisted = async () => {
5472
+ const confirmedPromise = ws.waitForMessage(
5473
+ "task_update_job_event_confirmed",
5474
+ (payload) => payload.job_id === job.job_id,
5475
+ 2e4
5476
+ );
5477
+ await ws.sendAsync("task_update_job_event_confirmed", {
5478
+ protocol_version: 1,
5479
+ job_id: job.job_id,
5480
+ event_system_message_id: eventMessage.message_id
5481
+ });
5482
+ await confirmedPromise;
5483
+ };
5484
+ if (claim.state === "TASK_PERSISTED") {
5485
+ await persistEncryptedSystemMessage(eventMessage, sourceChatId);
5486
+ persistedTaskEventIds.add(event.event_id);
5487
+ await confirmTaskEventPersisted();
5488
+ handledJobIds.add(job.job_id);
5489
+ continue;
5490
+ }
5491
+ if (!claim.lease_token || !Number.isSafeInteger(claim.lease_generation)) {
5492
+ throw new Error("Task update job claim returned an invalid lease.");
5493
+ }
5494
+ let encryptedTaskPayload;
5495
+ if (claim.operation === "create") {
5496
+ const input = await buildCreateUserTaskInput(masterKey, {
5497
+ title: typeof privatePatch.title === "string" ? privatePatch.title : "Untitled task",
5498
+ description: typeof privatePatch.description === "string" ? privatePatch.description : "",
5499
+ status: typeof safeMetadata.status === "string" ? safeMetadata.status : "todo",
5500
+ assign: typeof safeMetadata.assignee_type === "string" ? safeMetadata.assignee_type : "user",
5501
+ chatId: typeof safeMetadata.primary_chat_id === "string" ? safeMetadata.primary_chat_id : claim.chat_id ?? chatId
5502
+ });
5503
+ encryptedTaskPayload = {
5504
+ ...input,
5505
+ task_id: claim.task_id,
5506
+ position: typeof safeMetadata.position === "number" ? safeMetadata.position : input.position,
5507
+ created_at: typeof safeMetadata.created_at === "number" ? safeMetadata.created_at : input.created_at,
5508
+ updated_at: typeof safeMetadata.updated_at === "number" ? safeMetadata.updated_at : input.updated_at
5509
+ };
5510
+ } else {
5511
+ if (!decryptedTasksCache) {
5512
+ decryptedTasksCache = await decryptUserTasks(await this.listUserTasks(), masterKey);
5513
+ }
5514
+ const task = findTask(decryptedTasksCache, claim.task_id);
5515
+ const patch = await buildUpdateUserTaskInput(task, masterKey, {
5516
+ title: typeof privatePatch.title === "string" ? privatePatch.title : void 0,
5517
+ description: typeof privatePatch.description === "string" ? privatePatch.description : void 0,
5518
+ status: typeof safeMetadata.status === "string" ? safeMetadata.status : void 0,
5519
+ assign: typeof safeMetadata.assignee_type === "string" ? safeMetadata.assignee_type : void 0,
5520
+ chatId: typeof safeMetadata.primary_chat_id === "string" ? safeMetadata.primary_chat_id : void 0
5521
+ });
5522
+ encryptedTaskPayload = {
5523
+ ...patch,
5524
+ version: claim.expected_task_version,
5525
+ updated_at: typeof safeMetadata.updated_at === "number" ? safeMetadata.updated_at : patch.updated_at
5526
+ };
5527
+ if (typeof safeMetadata.primary_chat_id === "string") {
5528
+ encryptedTaskPayload.key_wrappers = await buildTaskKeyWrappersForChat(
5529
+ task,
5530
+ safeMetadata.primary_chat_id,
5531
+ typeof safeMetadata.updated_at === "number" ? safeMetadata.updated_at : Math.floor(Date.now() / 1e3)
5532
+ );
5533
+ }
5534
+ }
5535
+ const encryptedEventMessage = eventMessage.encrypted_content;
5536
+ const persistedPromise = ws.waitForMessage(
5537
+ "task_update_job_persisted",
5538
+ (payload) => payload.job_id === job.job_id,
5539
+ 2e4
5540
+ );
5541
+ await ws.sendAsync("task_update_job_persist", buildTaskUpdateJobPersistPayload({
5542
+ jobId: job.job_id,
5543
+ leaseToken: claim.lease_token,
5544
+ leaseGeneration: claim.lease_generation,
5545
+ expectedTaskVersion: claim.expected_task_version,
5546
+ encryptedTaskPayload,
5547
+ encryptedTaskEventMessage: encryptedEventMessage
5548
+ }));
5549
+ await persistedPromise;
5550
+ await persistEncryptedSystemMessage(eventMessage, sourceChatId);
5551
+ persistedTaskEventIds.add(event.event_id);
5552
+ await confirmTaskEventPersisted();
5553
+ handledJobIds.add(job.job_id);
5554
+ }
5555
+ clearSyncCache();
5556
+ return handledJobIds;
5557
+ };
4877
5558
  const streamOpts = {
4878
5559
  onStream: params.onStream,
4879
5560
  onSubChatEvent: handleSubChatEvent,
@@ -4881,11 +5562,17 @@ var OpenMatesClient = class _OpenMatesClient {
4881
5562
  };
4882
5563
  if (params.incognito) {
4883
5564
  try {
4884
- const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId, chatId, streamOpts));
5565
+ const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId, chatId, {
5566
+ ...streamOpts,
5567
+ timeoutMs: params.responseTimeoutMs,
5568
+ recoveryTurnId: savedTurnId
5569
+ }));
4885
5570
  assistantMessageId = resp.messageId;
4886
5571
  assistant = resp.content;
4887
5572
  category = resp.category;
4888
5573
  modelName = resp.modelName;
5574
+ taskEvents = resp.taskEvents;
5575
+ pendingTaskUpdateJobs = resp.pendingTaskUpdateJobs;
4889
5576
  subChatEvents = resp.subChatEvents;
4890
5577
  if (resp.status === "waiting_for_user") {
4891
5578
  return {
@@ -4899,6 +5586,8 @@ var OpenMatesClient = class _OpenMatesClient {
4899
5586
  followUpSuggestions,
4900
5587
  taskProposals,
4901
5588
  taskUpdateProposals,
5589
+ taskEvents,
5590
+ pendingTaskUpdateJobs,
4902
5591
  subChatEvents,
4903
5592
  appSettingsMemoryRequests
4904
5593
  };
@@ -4908,7 +5597,10 @@ var OpenMatesClient = class _OpenMatesClient {
4908
5597
  }
4909
5598
  } else {
4910
5599
  try {
4911
- const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId, chatId, streamOpts));
5600
+ const resp = await (precollectedResponse ?? ws.collectAiResponse(messageId, chatId, {
5601
+ ...streamOpts,
5602
+ timeoutMs: params.responseTimeoutMs
5603
+ }));
4912
5604
  assistantMessageId = resp.messageId;
4913
5605
  assistant = resp.content;
4914
5606
  category = resp.category;
@@ -4916,6 +5608,8 @@ var OpenMatesClient = class _OpenMatesClient {
4916
5608
  followUpSuggestions = resp.followUpSuggestions;
4917
5609
  taskProposals = resp.taskProposals;
4918
5610
  taskUpdateProposals = resp.taskUpdateProposals;
5611
+ taskEvents = resp.taskEvents;
5612
+ pendingTaskUpdateJobs = resp.pendingTaskUpdateJobs;
4919
5613
  subChatEvents = resp.subChatEvents;
4920
5614
  if (resp.status === "waiting_for_user") {
4921
5615
  return {
@@ -4929,6 +5623,8 @@ var OpenMatesClient = class _OpenMatesClient {
4929
5623
  followUpSuggestions,
4930
5624
  taskProposals,
4931
5625
  taskUpdateProposals,
5626
+ taskEvents,
5627
+ pendingTaskUpdateJobs,
4932
5628
  subChatEvents,
4933
5629
  appSettingsMemoryRequests
4934
5630
  };
@@ -4994,12 +5690,15 @@ var OpenMatesClient = class _OpenMatesClient {
4994
5690
  "model_name",
4995
5691
  "turn_id"
4996
5692
  ];
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) {
5693
+ 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
5694
  throw new Error("Recovery job plaintext did not match the terminal completion identity.");
4999
5695
  }
5696
+ assistant = recovered.content;
5697
+ category = recovered.category;
5698
+ modelName = recovered.model_name;
5000
5699
  const completedAt = Math.floor(Date.now() / 1e3);
5001
5700
  const encryptedAssistantContent = await encryptWithAesGcmCombined(
5002
- recovered.content,
5701
+ assistant,
5003
5702
  chatKeyBytes
5004
5703
  );
5005
5704
  const encryptedSenderName = await encryptWithAesGcmCombined("Assistant", chatKeyBytes);
@@ -5046,6 +5745,9 @@ var OpenMatesClient = class _OpenMatesClient {
5046
5745
  newChatSuggestions: resp.newChatSuggestions,
5047
5746
  encryptedChatKey
5048
5747
  });
5748
+ const persistedTaskJobIds = await persistPendingTaskUpdateJobs(pendingTaskUpdateJobs, taskEvents);
5749
+ pendingTaskUpdateJobs = pendingTaskUpdateJobs.filter((job) => !persistedTaskJobIds.has(job.job_id));
5750
+ await persistTaskEventSystemMessages(taskEvents);
5049
5751
  clearSyncCache();
5050
5752
  }
5051
5753
  } finally {
@@ -5064,6 +5766,8 @@ var OpenMatesClient = class _OpenMatesClient {
5064
5766
  followUpSuggestions,
5065
5767
  taskProposals,
5066
5768
  taskUpdateProposals,
5769
+ taskEvents,
5770
+ pendingTaskUpdateJobs,
5067
5771
  subChatEvents,
5068
5772
  appSettingsMemoryRequests
5069
5773
  };
@@ -6024,7 +6728,7 @@ var OpenMatesClient = class _OpenMatesClient {
6024
6728
  }
6025
6729
  return response.data.task;
6026
6730
  }
6027
- async startUserTaskWithAI(taskId, input = {}) {
6731
+ async startUserTaskWithAI(taskId, input) {
6028
6732
  this.requireSession();
6029
6733
  const response = await this.http.post(
6030
6734
  `/v1/user-tasks/${encodeURIComponent(taskId)}/start-ai`,
@@ -6036,10 +6740,10 @@ var OpenMatesClient = class _OpenMatesClient {
6036
6740
  }
6037
6741
  return response.data.task;
6038
6742
  }
6039
- async deleteUserTask(taskId) {
6743
+ async deleteUserTask(taskId, version) {
6040
6744
  this.requireSession();
6041
6745
  const response = await this.http.delete(
6042
- `/v1/user-tasks/${encodeURIComponent(taskId)}`,
6746
+ `/v1/user-tasks/${encodeURIComponent(taskId)}?version=${encodeURIComponent(String(version))}`,
6043
6747
  void 0,
6044
6748
  this.getCliRequestHeaders()
6045
6749
  );
@@ -6048,16 +6752,16 @@ var OpenMatesClient = class _OpenMatesClient {
6048
6752
  }
6049
6753
  return response.data;
6050
6754
  }
6051
- async completeUserTask(taskId, input = {}) {
6755
+ async completeUserTask(taskId, input) {
6052
6756
  return this.postUserTaskAction(taskId, "complete", input);
6053
6757
  }
6054
- async blockUserTask(taskId, input = {}) {
6758
+ async blockUserTask(taskId, input) {
6055
6759
  return this.postUserTaskAction(taskId, "block", input);
6056
6760
  }
6057
- async unblockUserTask(taskId, input = {}) {
6761
+ async unblockUserTask(taskId, input) {
6058
6762
  return this.postUserTaskAction(taskId, "unblock", input);
6059
6763
  }
6060
- async skipUserTask(taskId, input = {}) {
6764
+ async skipUserTask(taskId, input) {
6061
6765
  return this.postUserTaskAction(taskId, "skip", input);
6062
6766
  }
6063
6767
  async reorderUserTasks(input) {
@@ -6072,7 +6776,7 @@ var OpenMatesClient = class _OpenMatesClient {
6072
6776
  }
6073
6777
  return response.data.tasks ?? [];
6074
6778
  }
6075
- async postUserTaskAction(taskId, action, input = {}) {
6779
+ async postUserTaskAction(taskId, action, input) {
6076
6780
  this.requireSession();
6077
6781
  const response = await this.http.post(
6078
6782
  `/v1/user-tasks/${encodeURIComponent(taskId)}/${encodeURIComponent(action)}`,
@@ -6830,7 +7534,7 @@ Required: ${schema.required.join(", ")}`
6830
7534
  );
6831
7535
  }
6832
7536
  }
6833
- const entryId = params.entryId ?? randomUUID3();
7537
+ const entryId = params.entryId ?? randomUUID4();
6834
7538
  const now = Math.floor(Date.now() / 1e3);
6835
7539
  const hashedKey = hashItemKey(params.appId, params.itemType);
6836
7540
  const plaintextPayload = {
@@ -7305,7 +8009,7 @@ Required: ${schema.required.join(", ")}`
7305
8009
  makeWsClient(session) {
7306
8010
  return new OpenMatesWsClient({
7307
8011
  apiUrl: session.apiUrl,
7308
- sessionId: randomUUID3(),
8012
+ sessionId: randomUUID4(),
7309
8013
  wsToken: session.wsToken,
7310
8014
  refreshToken: session.cookies.auth_refresh_token ?? null,
7311
8015
  // Same User-Agent as login so OS-based device fingerprint hash matches.
@@ -8488,7 +9192,7 @@ var OutputRedactor = class {
8488
9192
  import { readFileSync as readFileSync4, statSync, existsSync as existsSync4 } from "fs";
8489
9193
  import { basename, extname, resolve as resolve2 } from "path";
8490
9194
  import { homedir as homedir4 } from "os";
8491
- import { createHash as createHash5 } from "crypto";
9195
+ import { createHash as createHash6 } from "crypto";
8492
9196
  var MAX_PER_FILE_SIZE = 100 * 1024 * 1024;
8493
9197
  var BLOCKED_EXTENSIONS = /* @__PURE__ */ new Set([
8494
9198
  ".pem",
@@ -8798,7 +9502,7 @@ function processCodeFile(filePath, filename, redactor) {
8798
9502
  line_count: lineCount
8799
9503
  });
8800
9504
  const textPreview = `${filename} (${language}, ${lineCount} lines)`;
8801
- const contentHash = createHash5("sha256").update(content).digest("hex");
9505
+ const contentHash = createHash6("sha256").update(content).digest("hex");
8802
9506
  const embed = {
8803
9507
  embedId,
8804
9508
  embedRef,
@@ -10314,7 +11018,7 @@ function formatTs(ts) {
10314
11018
 
10315
11019
  // src/server.ts
10316
11020
  import { execFileSync as execFileSync2, execSync, spawn as nodeSpawn, spawnSync } from "child_process";
10317
- import { createHash as createHash6, randomBytes as randomBytes2 } from "crypto";
11021
+ import { createHash as createHash7, randomBytes as randomBytes3 } from "crypto";
10318
11022
  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
11023
  import { createInterface as createInterface2 } from "readline";
10320
11024
  import { createInterface as createPromptInterface } from "readline/promises";
@@ -10932,10 +11636,10 @@ function resolveTargetImageTag(flags, currentTag, packageVersion) {
10932
11636
  return { tag: getDefaultImageTagForVersion(packageVersion) };
10933
11637
  }
10934
11638
  function randomHex(bytes) {
10935
- return randomBytes2(bytes).toString("hex");
11639
+ return randomBytes3(bytes).toString("hex");
10936
11640
  }
10937
11641
  function generateInviteCode() {
10938
- const digits = Array.from(randomBytes2(12), (byte) => String(byte % 10)).join("");
11642
+ const digits = Array.from(randomBytes3(12), (byte) => String(byte % 10)).join("");
10939
11643
  return `${digits.slice(0, 4)}-${digits.slice(4, 8)}-${digits.slice(8, 12)}`;
10940
11644
  }
10941
11645
  function getEnvVar(content, name) {
@@ -10979,7 +11683,7 @@ function packagedCaddyTemplatePath(role) {
10979
11683
  }
10980
11684
  function fileHash(path) {
10981
11685
  if (!existsSync5(path)) return null;
10982
- return createHash6("sha256").update(readFileSync5(path)).digest("hex");
11686
+ return createHash7("sha256").update(readFileSync5(path)).digest("hex");
10983
11687
  }
10984
11688
  async function loadSelfHostComposeTemplate(templateRef, role) {
10985
11689
  const templateDir = process.env.OPENMATES_SELFHOST_TEMPLATE_DIR;
@@ -11365,7 +12069,7 @@ function formatSecretPreflight(preflight) {
11365
12069
  return parts.join("; ");
11366
12070
  }
11367
12071
  function hashFile(path) {
11368
- const hash = createHash6("sha256");
12072
+ const hash = createHash7("sha256");
11369
12073
  const fd = openSync(path, "r");
11370
12074
  const buffer = Buffer.allocUnsafe(CHECKSUM_BUFFER_BYTES);
11371
12075
  try {
@@ -29386,7 +30090,10 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
29386
30090
  },
29387
30091
  models3d: {
29388
30092
  generate: {
29389
- text: "Generate 3D model"
30093
+ text: "Generate 3D model",
30094
+ description: {
30095
+ text: "Create a 3D model from a text prompt or reference images."
30096
+ }
29390
30097
  }
29391
30098
  },
29392
30099
  music: {
@@ -30241,6 +30948,12 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
30241
30948
  text: "Create structured idea maps and brainstorm topic trees."
30242
30949
  }
30243
30950
  },
30951
+ models3d: {
30952
+ text: "3D Models",
30953
+ description: {
30954
+ text: "Generate 3D models from prompts or reference images."
30955
+ }
30956
+ },
30244
30957
  math: {
30245
30958
  text: "Math",
30246
30959
  description: {
@@ -46753,7 +47466,7 @@ function buildAssistantFeedbackDecision(rating) {
46753
47466
  }
46754
47467
 
46755
47468
  // src/benchmark.ts
46756
- import { randomUUID as randomUUID4 } from "crypto";
47469
+ import { randomUUID as randomUUID5 } from "crypto";
46757
47470
  import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
46758
47471
  import { tmpdir } from "os";
46759
47472
  import { dirname as dirname2, join as join4, resolve as resolve5 } from "path";
@@ -47053,7 +47766,7 @@ async function handleBenchmark(client, subcommand, rest, flags) {
47053
47766
  const caseIds = parseCaseIds(flags.case);
47054
47767
  const dryRun = flags["dry-run"] === true;
47055
47768
  const output = typeof flags.output === "string" ? flags.output : void 0;
47056
- const runId = typeof flags["run-id"] === "string" ? flags["run-id"] : randomUUID4();
47769
+ const runId = typeof flags["run-id"] === "string" ? flags["run-id"] : randomUUID5();
47057
47770
  const imagePath = typeof flags.image === "string" ? resolve5(flags.image) : defaultImageFixturePath();
47058
47771
  if (!dryRun && flags["confirm-spend-credits"] !== true) {
47059
47772
  throw new Error(
@@ -47463,7 +48176,7 @@ function buildLongContextHistory() {
47463
48176
  }
47464
48177
  function appendHistory(history, role, content) {
47465
48178
  history.push({
47466
- message_id: randomUUID4(),
48179
+ message_id: randomUUID5(),
47467
48180
  role,
47468
48181
  sender_name: role === "user" ? "User" : "Assistant",
47469
48182
  content,
@@ -47998,7 +48711,7 @@ function renderBody(state, width) {
47998
48711
  case "tasks":
47999
48712
  return renderTasks(state, width);
48000
48713
  case "task":
48001
- return renderTaskDetail(state, width);
48714
+ return renderTaskDetail2(state, width);
48002
48715
  case "start":
48003
48716
  default:
48004
48717
  return renderStart(width);
@@ -48071,7 +48784,7 @@ function renderTasks(state, width) {
48071
48784
  }
48072
48785
  return lines.flatMap((line) => wrap(line, width));
48073
48786
  }
48074
- function renderTaskDetail(state, width) {
48787
+ function renderTaskDetail2(state, width) {
48075
48788
  const task = state.activeTask;
48076
48789
  if (!task) return renderTasks(state, width);
48077
48790
  const assignee = task.assigneeType === "ai" ? "OpenMates" : task.assigneeHash ?? "user";
@@ -48388,221 +49101,6 @@ function boxed(line, width) {
48388
49101
  return `\u2502 ${line.padEnd(width)} \u2502`;
48389
49102
  }
48390
49103
 
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
49104
  // src/tui.ts
48607
49105
  function defaultModeForStreams(input, output) {
48608
49106
  return input.isTTY === true && output.isTTY === true ? "tui" : "quickstart";
@@ -49036,7 +49534,7 @@ async function handleActiveTaskAction(params) {
49036
49534
  render();
49037
49535
  return;
49038
49536
  }
49039
- await client.deleteUserTask(task.taskId);
49537
+ await client.deleteUserTask(task.taskId, task.version);
49040
49538
  state.tasks = state.tasks.filter((candidate) => candidate.taskId !== task.taskId);
49041
49539
  state.activeTask = null;
49042
49540
  state.selectedIndex = Math.min(state.selectedIndex, Math.max(0, state.tasks.length - 1));
@@ -49053,7 +49551,7 @@ async function handleActiveTaskAction(params) {
49053
49551
  render();
49054
49552
  return;
49055
49553
  }
49056
- const updated2 = await client.reorderUserTasks({ moves: [{ task_id: task.taskId, position }] });
49554
+ const updated2 = await client.reorderUserTasks({ moves: [{ task_id: task.taskId, version: task.version, position }] });
49057
49555
  const [decrypted2] = await decryptUserTasks(updated2, client.getMasterKeyBytes());
49058
49556
  if (decrypted2) {
49059
49557
  replaceTask(state, decrypted2);
@@ -50145,7 +50643,7 @@ async function handleTasks(client, subcommand, rest, flags) {
50145
50643
  if (subcommand === "delete") {
50146
50644
  const task = await requiredResolvedTask(client, masterKey, rest[0], scope, "delete");
50147
50645
  if (flags.confirm !== true) throw new Error("Deleting a task requires --confirm.");
50148
- const result = await client.deleteUserTask(task.taskId);
50646
+ const result = await client.deleteUserTask(task.taskId, task.version);
50149
50647
  if (flags.json === true) printJson2(result);
50150
50648
  else console.log(`Task deleted: ${task.shortId}`);
50151
50649
  return;
@@ -50176,7 +50674,7 @@ async function handleTasks(client, subcommand, rest, flags) {
50176
50674
  }
50177
50675
  if (subcommand === "reorder") {
50178
50676
  const task = await requiredResolvedTask(client, masterKey, rest[0], scope, "reorder");
50179
- const move = { task_id: task.taskId };
50677
+ const move = { task_id: task.taskId, version: task.version };
50180
50678
  if (typeof flags.before === "string") move.before_task_id = (await resolveTask(client, masterKey, flags.before, scope)).taskId;
50181
50679
  if (typeof flags.after === "string") move.after_task_id = (await resolveTask(client, masterKey, flags.after, scope)).taskId;
50182
50680
  if (typeof flags.status === "string") move.status = normalizeTaskStatus(flags.status);
@@ -50211,7 +50709,7 @@ async function requiredResolvedTask(client, masterKey, id, scope, action) {
50211
50709
  }
50212
50710
  function printTaskOutput(task, flags) {
50213
50711
  if (flags.json === true) printJson2({ task: taskToJson(task) });
50214
- else console.log(renderTaskDetail2(task));
50712
+ else console.log(renderTaskDetail(task));
50215
50713
  }
50216
50714
  function taskToJson(task) {
50217
50715
  return {
@@ -50316,6 +50814,10 @@ function parsePositiveIntegerFlag(value, flagName) {
50316
50814
  }
50317
50815
  return parsed;
50318
50816
  }
50817
+ function parseResponseTimeoutMs(flags) {
50818
+ const seconds = parsePositiveIntegerFlag(flags["response-timeout-seconds"], "--response-timeout-seconds");
50819
+ return seconds === void 0 ? void 0 : seconds * 1e3;
50820
+ }
50319
50821
  function parseRemoteAccessSourceType(value) {
50320
50822
  if (value === void 0) return "local_folder";
50321
50823
  if (value === "local_folder" || value === "local_git_repository") {
@@ -50459,6 +50961,7 @@ async function handleChats(client, subcommand, rest, flags, redactor) {
50459
50961
  autoApproveMemories: flags["auto-approve-memories"] === true,
50460
50962
  acceptTaskProposals: flags["accept-task-proposals"] === true,
50461
50963
  piiDetection: flags["no-pii-detection"] !== true,
50964
+ responseTimeoutMs: parseResponseTimeoutMs(flags),
50462
50965
  anonymousLearningMode: client.hasSession() ? void 0 : parseAnonymousLearningModeFlags(flags)
50463
50966
  },
50464
50967
  redactor
@@ -50534,7 +51037,8 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
50534
51037
  autoApproveSubChats: flags["auto-approve"] === true,
50535
51038
  autoApproveMemories: flags["auto-approve-memories"] === true,
50536
51039
  acceptTaskProposals: flags["accept-task-proposals"] === true,
50537
- piiDetection: flags["no-pii-detection"] !== true
51040
+ piiDetection: flags["no-pii-detection"] !== true,
51041
+ responseTimeoutMs: parseResponseTimeoutMs(flags)
50538
51042
  },
50539
51043
  redactor
50540
51044
  );
@@ -50563,7 +51067,8 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
50563
51067
  autoApproveSubChats: flags["auto-approve"] === true,
50564
51068
  autoApproveMemories: flags["auto-approve-memories"] === true,
50565
51069
  acceptTaskProposals: flags["accept-task-proposals"] === true,
50566
- piiDetection: flags["no-pii-detection"] !== true
51070
+ piiDetection: flags["no-pii-detection"] !== true,
51071
+ responseTimeoutMs: parseResponseTimeoutMs(flags)
50567
51072
  },
50568
51073
  redactor
50569
51074
  );
@@ -50584,7 +51089,8 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
50584
51089
  json: flags.json === true,
50585
51090
  autoApproveSubChats: flags["auto-approve"] === true,
50586
51091
  autoApproveMemories: flags["auto-approve-memories"] === true,
50587
- piiDetection: flags["no-pii-detection"] !== true
51092
+ piiDetection: flags["no-pii-detection"] !== true,
51093
+ responseTimeoutMs: parseResponseTimeoutMs(flags)
50588
51094
  },
50589
51095
  redactor
50590
51096
  );
@@ -53971,7 +54477,7 @@ async function sendMessageStreaming(client, params, redactor) {
53971
54477
  process.stdout.write(`${result2.assistant}
53972
54478
  `);
53973
54479
  }
53974
- return { ...result2, acceptedTaskProposals: [] };
54480
+ return { ...result2, taskEvents: [], pendingTaskUpdateJobs: [], acceptedTaskProposals: [] };
53975
54481
  }
53976
54482
  const urlResult = prepareUrlEmbeds(finalMessage);
53977
54483
  finalMessage = urlResult.message;
@@ -53986,6 +54492,7 @@ async function sendMessageStreaming(client, params, redactor) {
53986
54492
  onSubChatApprovalRequest,
53987
54493
  autoApproveSubChats: params.autoApproveSubChats,
53988
54494
  autoApproveMemories: params.autoApproveMemories,
54495
+ responseTimeoutMs: params.responseTimeoutMs,
53989
54496
  learningMode,
53990
54497
  preparedEmbeds: preparedEmbeds.length > 0 ? preparedEmbeds : void 0,
53991
54498
  piiMappings: piiResult.mappings.map((mapping) => ({
@@ -54077,6 +54584,24 @@ ${result.assistant}`) : [];
54077
54584
  process.stdout.write(`${SEP}
54078
54585
  `);
54079
54586
  }
54587
+ if (result.taskEvents.length > 0) {
54588
+ process.stdout.write(`\x1B[2mTask updates:\x1B[0m
54589
+ `);
54590
+ for (const event of result.taskEvents) {
54591
+ process.stdout.write(` \x1B[2m- ${formatTaskEvent(event)}\x1B[0m
54592
+ `);
54593
+ }
54594
+ process.stdout.write(`${SEP}
54595
+ `);
54596
+ }
54597
+ if (result.pendingTaskUpdateJobs.length > 0) {
54598
+ const count = result.pendingTaskUpdateJobs.length;
54599
+ process.stdout.write(
54600
+ `\x1B[2mPending encrypted task update${count === 1 ? "" : "s"}: ${count}\x1B[0m
54601
+ ${SEP}
54602
+ `
54603
+ );
54604
+ }
54080
54605
  process.stdout.write(
54081
54606
  `\x1B[2mContinue: openmates chats send --chat ${shortId} "your message"\x1B[0m
54082
54607
  \x1B[2mHistory: openmates chats show ${shortId}\x1B[0m
@@ -54085,6 +54610,26 @@ ${result.assistant}`) : [];
54085
54610
  }
54086
54611
  return { ...result, acceptedTaskProposals };
54087
54612
  }
54613
+ function formatTaskEvent(event) {
54614
+ const taskLabel = event.short_id || event.task_id;
54615
+ const title = event.title ? ` "${event.title}"` : "";
54616
+ const status = event.status ? ` (${event.status})` : "";
54617
+ const reason = event.reason ? `: ${event.reason}` : "";
54618
+ switch (event.event_type) {
54619
+ case "created":
54620
+ return `${taskLabel} created${title}${status}`;
54621
+ case "updated":
54622
+ return `${taskLabel} updated${title}${status}`;
54623
+ case "blocked":
54624
+ return `${taskLabel} blocked${reason}`;
54625
+ case "completed":
54626
+ return `${taskLabel} completed${title}`;
54627
+ case "unblocked":
54628
+ return `${taskLabel} unblocked`;
54629
+ default:
54630
+ return `${taskLabel} ${event.event_type}${title}${status}${reason}`;
54631
+ }
54632
+ }
54088
54633
  async function acceptChatTaskProposals(client, chatId, proposals, fallbackText) {
54089
54634
  let proposalsToAccept = proposals;
54090
54635
  if (proposalsToAccept.length === 0 && fallbackText.trim()) {