openmates 0.14.8-alpha.0 → 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";
@@ -1190,6 +1190,8 @@ var SUB_CHAT_EVENT_TYPES = /* @__PURE__ */ new Set([
1190
1190
  var SUB_CHAT_PARENT_STATUS_MESSAGE = "I've started the sub-chats and will continue once they finish.";
1191
1191
  var SUB_CHAT_COMPLETION_TIMEOUT_MS = 10 * 6e4;
1192
1192
  var CLIENT_UPDATE_REQUIRED_GUIDANCE = "OpenMates CLI update required. Run `openmates upgrade` and retry.";
1193
+ var TASK_STATUSES = /* @__PURE__ */ new Set(["backlog", "todo", "in_progress", "blocked", "done"]);
1194
+ var TASK_ASSIGNEES = /* @__PURE__ */ new Set(["ai", "user"]);
1193
1195
  var WebSocketProtocolError = class extends Error {
1194
1196
  code;
1195
1197
  constructor(code, message) {
@@ -1212,14 +1214,121 @@ function websocketProtocolError(envelope) {
1212
1214
  }
1213
1215
  return null;
1214
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 parseTaskProposals(value) {
1243
+ if (!Array.isArray(value)) return [];
1244
+ return value.flatMap((item) => {
1245
+ if (!item || typeof item !== "object" || Array.isArray(item)) return [];
1246
+ const raw = item;
1247
+ if (typeof raw.title !== "string" || raw.title.trim().length === 0) return [];
1248
+ const proposal = { title: raw.title };
1249
+ if (typeof raw.description === "string" || raw.description === null) proposal.description = raw.description;
1250
+ if (typeof raw.status === "string" && TASK_STATUSES.has(raw.status)) proposal.status = raw.status;
1251
+ if (typeof raw.assignee_type === "string" && TASK_ASSIGNEES.has(raw.assignee_type)) proposal.assignee_type = raw.assignee_type;
1252
+ return [proposal];
1253
+ });
1254
+ }
1255
+ function parseTaskUpdateProposals(value) {
1256
+ if (!Array.isArray(value)) return [];
1257
+ return value.flatMap((item) => {
1258
+ if (!item || typeof item !== "object" || Array.isArray(item)) return [];
1259
+ const raw = item;
1260
+ if (typeof raw.task_id !== "string" || raw.task_id.trim().length === 0) return [];
1261
+ const proposal = { task_id: raw.task_id };
1262
+ if (typeof raw.title === "string" || raw.title === null) proposal.title = raw.title;
1263
+ if (typeof raw.description === "string" || raw.description === null) proposal.description = raw.description;
1264
+ if (typeof raw.status === "string" && TASK_STATUSES.has(raw.status) || raw.status === null) proposal.status = raw.status;
1265
+ if (typeof raw.assignee_type === "string" && TASK_ASSIGNEES.has(raw.assignee_type) || raw.assignee_type === null) proposal.assignee_type = raw.assignee_type;
1266
+ return [proposal];
1267
+ });
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
+ }
1215
1321
  var OpenMatesWsClient = class {
1216
1322
  socket;
1323
+ passiveTaskUpdateJobs = /* @__PURE__ */ new Map();
1324
+ activeResponseCollectors = 0;
1217
1325
  constructor(options) {
1218
1326
  const wsBase = options.apiUrl.replace(/^http/, "ws").replace(/\/$/, "");
1219
1327
  const token = options.wsToken || options.refreshToken || "";
1220
1328
  const query = new URLSearchParams({
1221
1329
  sessionId: options.sessionId,
1222
- token
1330
+ token,
1331
+ client_capabilities: "task_update_jobs"
1223
1332
  });
1224
1333
  const wsHeaders = {};
1225
1334
  if (options.userAgent) {
@@ -1234,6 +1343,10 @@ var OpenMatesWsClient = class {
1234
1343
  this.socket = new WebSocket(`${wsBase}/v1/ws?${query.toString()}`, {
1235
1344
  headers: wsHeaders
1236
1345
  });
1346
+ this.socket.on("message", (rawData) => {
1347
+ if (this.activeResponseCollectors > 0) return;
1348
+ this.bufferPassiveTaskUpdateJobs(rawData);
1349
+ });
1237
1350
  }
1238
1351
  async open(timeoutMs = 1e4) {
1239
1352
  await new Promise((resolve7, reject) => {
@@ -1279,6 +1392,22 @@ var OpenMatesWsClient = class {
1279
1392
  });
1280
1393
  });
1281
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
+ }
1282
1411
  waitForMessage(expectedType, predicate, timeoutMs = 2e4) {
1283
1412
  return new Promise((resolve7, reject) => {
1284
1413
  const seenTypes = /* @__PURE__ */ new Set();
@@ -1412,6 +1541,16 @@ var OpenMatesWsClient = class {
1412
1541
  let recoveryJobId = null;
1413
1542
  let followUpSuggestions = [];
1414
1543
  let newChatSuggestions = [];
1544
+ let taskProposals = [];
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
+ };
1415
1554
  const subChatEvents = [];
1416
1555
  const pendingSubChatHandlers = /* @__PURE__ */ new Set();
1417
1556
  const pendingMemoryRequestHandlers = /* @__PURE__ */ new Set();
@@ -1472,6 +1611,10 @@ var OpenMatesWsClient = class {
1472
1611
  modelName,
1473
1612
  followUpSuggestions,
1474
1613
  newChatSuggestions,
1614
+ taskProposals,
1615
+ taskUpdateProposals,
1616
+ taskEvents,
1617
+ pendingTaskUpdateJobs,
1475
1618
  embeds: [...embeds.values()],
1476
1619
  subChatEvents,
1477
1620
  recoveryJobId
@@ -1479,6 +1622,7 @@ var OpenMatesWsClient = class {
1479
1622
  return;
1480
1623
  }
1481
1624
  if (!aiResponseDone || !postProcessingDone) return;
1625
+ if (options?.recoveryTurnId && !recoveryJobId) return;
1482
1626
  if (pendingSubChatHandlers.size > 0) return;
1483
1627
  if (pendingMemoryRequestHandlers.size > 0) return;
1484
1628
  if (processingEmbedIds.size > 0 && !asyncEmbedTimer) {
@@ -1493,6 +1637,10 @@ var OpenMatesWsClient = class {
1493
1637
  modelName,
1494
1638
  followUpSuggestions,
1495
1639
  newChatSuggestions,
1640
+ taskProposals,
1641
+ taskUpdateProposals,
1642
+ taskEvents,
1643
+ pendingTaskUpdateJobs,
1496
1644
  embeds: [...embeds.values()],
1497
1645
  subChatEvents,
1498
1646
  recoveryJobId
@@ -1511,6 +1659,10 @@ var OpenMatesWsClient = class {
1511
1659
  modelName,
1512
1660
  followUpSuggestions,
1513
1661
  newChatSuggestions,
1662
+ taskProposals,
1663
+ taskUpdateProposals,
1664
+ taskEvents,
1665
+ pendingTaskUpdateJobs,
1514
1666
  embeds: [...embeds.values()],
1515
1667
  subChatEvents,
1516
1668
  recoveryJobId
@@ -1587,6 +1739,9 @@ var OpenMatesWsClient = class {
1587
1739
  const type = parsed.type;
1588
1740
  const protocolError = websocketProtocolError(parsed);
1589
1741
  if (protocolError) {
1742
+ if (!errorFrameBelongsToAiResponse(parsed, userMessageId, chatId, options?.recoveryTurnId)) {
1743
+ return;
1744
+ }
1590
1745
  cleanup();
1591
1746
  reject(protocolError);
1592
1747
  return;
@@ -1617,6 +1772,29 @@ var OpenMatesWsClient = class {
1617
1772
  }
1618
1773
  return;
1619
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
+ }
1620
1798
  if (type === "ai_message_update") {
1621
1799
  const msgId = p.user_message_id ?? p.userMessageId;
1622
1800
  if (msgId !== userMessageId && p.chat_id !== chatId) return;
@@ -1695,6 +1873,8 @@ var OpenMatesWsClient = class {
1695
1873
  (s) => typeof s === "string" && s.length > 0
1696
1874
  );
1697
1875
  }
1876
+ taskProposals = parseTaskProposals(p.task_proposals);
1877
+ taskUpdateProposals = parseTaskUpdateProposals(p.task_update_proposals);
1698
1878
  if (aiResponseDone) {
1699
1879
  if (postProcessingTimer) {
1700
1880
  clearTimeout(postProcessingTimer);
@@ -1723,6 +1903,10 @@ var OpenMatesWsClient = class {
1723
1903
  modelName,
1724
1904
  followUpSuggestions,
1725
1905
  newChatSuggestions,
1906
+ taskProposals,
1907
+ taskUpdateProposals,
1908
+ taskEvents,
1909
+ pendingTaskUpdateJobs,
1726
1910
  embeds: [...embeds.values()],
1727
1911
  subChatEvents,
1728
1912
  recoveryJobId
@@ -1745,6 +1929,7 @@ var OpenMatesWsClient = class {
1745
1929
  this.socket.off("message", onMessage);
1746
1930
  this.socket.off("error", onError);
1747
1931
  this.socket.off("close", onClose);
1932
+ this.activeResponseCollectors = Math.max(0, this.activeResponseCollectors - 1);
1748
1933
  };
1749
1934
  this.socket.on("message", onMessage);
1750
1935
  this.socket.on("error", onError);
@@ -2473,6 +2658,227 @@ function toArrayBuffer3(input) {
2473
2658
  return output;
2474
2659
  }
2475
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
+
2476
2882
  // src/client.ts
2477
2883
  function normalizeUnixSeconds(value, fallback) {
2478
2884
  if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) {
@@ -2640,6 +3046,83 @@ function buildAppSettingsMemoryResponseSystemMessage(params) {
2640
3046
  user_message_id: params.userMessageId
2641
3047
  };
2642
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
+ }
2643
3126
  async function buildSubChatEncryptedMetadataPayloads(params) {
2644
3127
  const createdAt = params.createdAt ?? Math.floor(Date.now() / 1e3);
2645
3128
  const payloads = [];
@@ -3313,11 +3796,11 @@ var OpenMatesClient = class _OpenMatesClient {
3313
3796
  }
3314
3797
  let anonymousId = loadAnonymousId();
3315
3798
  if (!anonymousId) {
3316
- anonymousId = randomUUID3();
3799
+ anonymousId = randomUUID4();
3317
3800
  saveAnonymousId(anonymousId);
3318
3801
  }
3319
- const chatId = `anonymous-${randomUUID3()}`;
3320
- const messageId = `anonymous-message-${randomUUID3()}`;
3802
+ const chatId = `anonymous-${randomUUID4()}`;
3803
+ const messageId = `anonymous-message-${randomUUID4()}`;
3321
3804
  const requestBody = {
3322
3805
  anonymous_id: anonymousId,
3323
3806
  client_chat_id: chatId,
@@ -3352,6 +3835,8 @@ var OpenMatesClient = class _OpenMatesClient {
3352
3835
  modelName: response.data.modelName ?? null,
3353
3836
  mateName: null,
3354
3837
  followUpSuggestions: response.data.followUpSuggestions ?? [],
3838
+ taskProposals: [],
3839
+ taskUpdateProposals: [],
3355
3840
  subChatEvents: [],
3356
3841
  appSettingsMemoryRequests: []
3357
3842
  };
@@ -3509,7 +3994,7 @@ var OpenMatesClient = class _OpenMatesClient {
3509
3994
  pin: pin.trim().toUpperCase(),
3510
3995
  token
3511
3996
  });
3512
- const sessionId = randomUUID3();
3997
+ const sessionId = randomUUID4();
3513
3998
  const login = await this.http.post(
3514
3999
  "/v1/auth/login",
3515
4000
  {
@@ -3684,7 +4169,7 @@ var OpenMatesClient = class _OpenMatesClient {
3684
4169
  }
3685
4170
  const session = {
3686
4171
  apiUrl: this.apiUrl,
3687
- sessionId: randomUUID3(),
4172
+ sessionId: randomUUID4(),
3688
4173
  wsToken: null,
3689
4174
  cookies: this.http.getCookieMap(),
3690
4175
  masterKeyExportedB64: material.masterKeyB64,
@@ -3844,7 +4329,7 @@ var OpenMatesClient = class _OpenMatesClient {
3844
4329
  if (!markdown) throw new Error("Draft markdown must not be empty.");
3845
4330
  const masterKey = this.getMasterKeyBytes();
3846
4331
  const encrypted = await this.saveEncryptedDraft({
3847
- chatId: params.chatId ?? randomUUID3(),
4332
+ chatId: params.chatId ?? randomUUID4(),
3848
4333
  encryptedDraftMd: await encryptWithAesGcmCombined(markdown, masterKey),
3849
4334
  encryptedDraftPreview: params.preview ? await encryptWithAesGcmCombined(params.preview, masterKey) : null
3850
4335
  });
@@ -4400,7 +4885,7 @@ var OpenMatesClient = class _OpenMatesClient {
4400
4885
  async sendMessage(params) {
4401
4886
  let chatId;
4402
4887
  if (!params.chatId) {
4403
- chatId = randomUUID3();
4888
+ chatId = randomUUID4();
4404
4889
  } else if (params.chatId.length < 36) {
4405
4890
  const resolved = await this.resolveFullChatId(params.chatId);
4406
4891
  if (!resolved) {
@@ -4434,7 +4919,7 @@ var OpenMatesClient = class _OpenMatesClient {
4434
4919
  ws.close();
4435
4920
  throw new Error("Authenticated user identity is required for saved chat recovery.");
4436
4921
  }
4437
- const messageId = randomUUID3();
4922
+ const messageId = randomUUID4();
4438
4923
  const createdAt = Math.floor(Date.now() / 1e3);
4439
4924
  const isNewChat = !params.chatId;
4440
4925
  ws.send("set_active_chat", { chat_id: chatId });
@@ -4451,6 +4936,7 @@ var OpenMatesClient = class _OpenMatesClient {
4451
4936
  let chatKeyBytes = null;
4452
4937
  let encryptedChatKey = null;
4453
4938
  let baselineMessagesV = 0;
4939
+ let terminalExpectedMessagesV = 1;
4454
4940
  let savedTurnId = null;
4455
4941
  if (!params.incognito) {
4456
4942
  const masterKey = this.getMasterKeyBytes();
@@ -4484,6 +4970,7 @@ var OpenMatesClient = class _OpenMatesClient {
4484
4970
  }
4485
4971
  const messagePayload = {
4486
4972
  chat_id: chatId,
4973
+ client_capabilities: ["task_update_jobs"],
4487
4974
  is_incognito: Boolean(params.incognito),
4488
4975
  message: {
4489
4976
  message_id: messageId,
@@ -4567,12 +5054,13 @@ var OpenMatesClient = class _OpenMatesClient {
4567
5054
  if (encryptedEmbeds.length > 0) {
4568
5055
  messagePayload.encrypted_embeds = encryptedEmbeds;
4569
5056
  }
4570
- 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;
4571
5058
  if (!params.incognito && chatKeyBytes && encryptedChatKey) {
4572
5059
  const protocolVersion = 1;
4573
5060
  const chatKeyVersion = 1;
4574
- const turnId = randomUUID3();
5061
+ const turnId = randomUUID4();
4575
5062
  savedTurnId = turnId;
5063
+ terminalExpectedMessagesV = baselineMessagesV + 1;
4576
5064
  const recoveryKeypair = await deriveChatCompletionRecoveryKeypair(
4577
5065
  Buffer.from(chatKeyBytes).toString("base64url"),
4578
5066
  chatId,
@@ -4638,10 +5126,15 @@ var OpenMatesClient = class _OpenMatesClient {
4638
5126
  protocol_version: protocolVersion,
4639
5127
  preflight_id: ackPayload.preflight_id
4640
5128
  });
5129
+ if (typeof ackPayload.committed_messages_v === "number" && Number.isSafeInteger(ackPayload.committed_messages_v)) {
5130
+ terminalExpectedMessagesV = ackPayload.committed_messages_v;
5131
+ }
4641
5132
  }
4642
5133
  if (params.precollectResponse && !params.incognito) {
4643
5134
  precollectedResponse = ws.collectAiResponse(messageId, chatId, {
4644
- onStream: params.onStream
5135
+ onStream: params.onStream,
5136
+ timeoutMs: params.responseTimeoutMs,
5137
+ recoveryTurnId: savedTurnId
4645
5138
  });
4646
5139
  }
4647
5140
  const confirmed = ws.waitForMessage(
@@ -4653,12 +5146,19 @@ var OpenMatesClient = class _OpenMatesClient {
4653
5146
  2e4
4654
5147
  );
4655
5148
  await ws.sendAsync("chat_message_added", messagePayload);
4656
- await confirmed;
5149
+ const confirmedPayload = (await confirmed).payload;
5150
+ if (typeof confirmedPayload.new_messages_v === "number" && Number.isSafeInteger(confirmedPayload.new_messages_v)) {
5151
+ terminalExpectedMessagesV = confirmedPayload.new_messages_v + 1;
5152
+ }
4657
5153
  let assistant = "";
4658
5154
  let assistantMessageId = null;
4659
5155
  let category = null;
4660
5156
  let modelName = null;
4661
5157
  let followUpSuggestions = [];
5158
+ let taskProposals = [];
5159
+ let taskUpdateProposals = [];
5160
+ let taskEvents = [];
5161
+ let pendingTaskUpdateJobs = [];
4662
5162
  let subChatEvents = [];
4663
5163
  const appSettingsMemoryRequests = [];
4664
5164
  const numberOrNull = (value) => typeof value === "number" && Number.isFinite(value) ? value : null;
@@ -4821,6 +5321,225 @@ var OpenMatesClient = class _OpenMatesClient {
4821
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.`
4822
5322
  );
4823
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
+ };
4824
5543
  const streamOpts = {
4825
5544
  onStream: params.onStream,
4826
5545
  onSubChatEvent: handleSubChatEvent,
@@ -4828,11 +5547,17 @@ var OpenMatesClient = class _OpenMatesClient {
4828
5547
  };
4829
5548
  if (params.incognito) {
4830
5549
  try {
4831
- 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
+ }));
4832
5555
  assistantMessageId = resp.messageId;
4833
5556
  assistant = resp.content;
4834
5557
  category = resp.category;
4835
5558
  modelName = resp.modelName;
5559
+ taskEvents = resp.taskEvents;
5560
+ pendingTaskUpdateJobs = resp.pendingTaskUpdateJobs;
4836
5561
  subChatEvents = resp.subChatEvents;
4837
5562
  if (resp.status === "waiting_for_user") {
4838
5563
  return {
@@ -4844,6 +5569,10 @@ var OpenMatesClient = class _OpenMatesClient {
4844
5569
  modelName,
4845
5570
  mateName: category ? MATE_NAMES[category] ?? null : null,
4846
5571
  followUpSuggestions,
5572
+ taskProposals,
5573
+ taskUpdateProposals,
5574
+ taskEvents,
5575
+ pendingTaskUpdateJobs,
4847
5576
  subChatEvents,
4848
5577
  appSettingsMemoryRequests
4849
5578
  };
@@ -4853,12 +5582,19 @@ var OpenMatesClient = class _OpenMatesClient {
4853
5582
  }
4854
5583
  } else {
4855
5584
  try {
4856
- 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
+ }));
4857
5589
  assistantMessageId = resp.messageId;
4858
5590
  assistant = resp.content;
4859
5591
  category = resp.category;
4860
5592
  modelName = resp.modelName;
4861
5593
  followUpSuggestions = resp.followUpSuggestions;
5594
+ taskProposals = resp.taskProposals;
5595
+ taskUpdateProposals = resp.taskUpdateProposals;
5596
+ taskEvents = resp.taskEvents;
5597
+ pendingTaskUpdateJobs = resp.pendingTaskUpdateJobs;
4862
5598
  subChatEvents = resp.subChatEvents;
4863
5599
  if (resp.status === "waiting_for_user") {
4864
5600
  return {
@@ -4870,6 +5606,10 @@ var OpenMatesClient = class _OpenMatesClient {
4870
5606
  modelName,
4871
5607
  mateName: category ? MATE_NAMES[category] ?? null : null,
4872
5608
  followUpSuggestions,
5609
+ taskProposals,
5610
+ taskUpdateProposals,
5611
+ taskEvents,
5612
+ pendingTaskUpdateJobs,
4873
5613
  subChatEvents,
4874
5614
  appSettingsMemoryRequests
4875
5615
  };
@@ -4935,12 +5675,15 @@ var OpenMatesClient = class _OpenMatesClient {
4935
5675
  "model_name",
4936
5676
  "turn_id"
4937
5677
  ];
4938
- 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") {
4939
5679
  throw new Error("Recovery job plaintext did not match the terminal completion identity.");
4940
5680
  }
5681
+ assistant = recovered.content;
5682
+ category = recovered.category;
5683
+ modelName = recovered.model_name;
4941
5684
  const completedAt = Math.floor(Date.now() / 1e3);
4942
5685
  const encryptedAssistantContent = await encryptWithAesGcmCombined(
4943
- recovered.content,
5686
+ assistant,
4944
5687
  chatKeyBytes
4945
5688
  );
4946
5689
  const encryptedSenderName = await encryptWithAesGcmCombined("Assistant", chatKeyBytes);
@@ -4956,7 +5699,7 @@ var OpenMatesClient = class _OpenMatesClient {
4956
5699
  job_id: recoveryJobId,
4957
5700
  lease_token: leaseToken,
4958
5701
  lease_generation: leaseGeneration,
4959
- expected_messages_v: baselineMessagesV + 1,
5702
+ expected_messages_v: terminalExpectedMessagesV,
4960
5703
  encrypted_assistant_message: {
4961
5704
  client_message_id: assistantId,
4962
5705
  chat_id: chatId,
@@ -4987,6 +5730,9 @@ var OpenMatesClient = class _OpenMatesClient {
4987
5730
  newChatSuggestions: resp.newChatSuggestions,
4988
5731
  encryptedChatKey
4989
5732
  });
5733
+ const persistedTaskJobIds = await persistPendingTaskUpdateJobs(pendingTaskUpdateJobs, taskEvents);
5734
+ pendingTaskUpdateJobs = pendingTaskUpdateJobs.filter((job) => !persistedTaskJobIds.has(job.job_id));
5735
+ await persistTaskEventSystemMessages(taskEvents);
4990
5736
  clearSyncCache();
4991
5737
  }
4992
5738
  } finally {
@@ -5003,6 +5749,10 @@ var OpenMatesClient = class _OpenMatesClient {
5003
5749
  modelName,
5004
5750
  mateName,
5005
5751
  followUpSuggestions,
5752
+ taskProposals,
5753
+ taskUpdateProposals,
5754
+ taskEvents,
5755
+ pendingTaskUpdateJobs,
5006
5756
  subChatEvents,
5007
5757
  appSettingsMemoryRequests
5008
5758
  };
@@ -5935,6 +6685,22 @@ var OpenMatesClient = class _OpenMatesClient {
5935
6685
  }
5936
6686
  return response.data.task;
5937
6687
  }
6688
+ async extractUserTaskProposals(input) {
6689
+ const response = await this.http.post(
6690
+ "/v1/user-tasks/extract",
6691
+ {
6692
+ corrected_text: input.correctedText,
6693
+ mode: input.mode ?? "create",
6694
+ context_chat_id: input.contextChatId ?? null,
6695
+ project_ids: input.projectIds ?? []
6696
+ },
6697
+ this.getCliRequestHeaders()
6698
+ );
6699
+ if (!response.ok || !Array.isArray(response.data.proposed_tasks)) {
6700
+ throw new Error(`Failed to extract task proposals (HTTP ${response.status})`);
6701
+ }
6702
+ return response.data.proposed_tasks;
6703
+ }
5938
6704
  async updateUserTask(taskId, input) {
5939
6705
  this.requireSession();
5940
6706
  const response = await this.http.patch(
@@ -5947,7 +6713,7 @@ var OpenMatesClient = class _OpenMatesClient {
5947
6713
  }
5948
6714
  return response.data.task;
5949
6715
  }
5950
- async startUserTaskWithAI(taskId, input = {}) {
6716
+ async startUserTaskWithAI(taskId, input) {
5951
6717
  this.requireSession();
5952
6718
  const response = await this.http.post(
5953
6719
  `/v1/user-tasks/${encodeURIComponent(taskId)}/start-ai`,
@@ -5959,10 +6725,10 @@ var OpenMatesClient = class _OpenMatesClient {
5959
6725
  }
5960
6726
  return response.data.task;
5961
6727
  }
5962
- async deleteUserTask(taskId) {
6728
+ async deleteUserTask(taskId, version) {
5963
6729
  this.requireSession();
5964
6730
  const response = await this.http.delete(
5965
- `/v1/user-tasks/${encodeURIComponent(taskId)}`,
6731
+ `/v1/user-tasks/${encodeURIComponent(taskId)}?version=${encodeURIComponent(String(version))}`,
5966
6732
  void 0,
5967
6733
  this.getCliRequestHeaders()
5968
6734
  );
@@ -5971,16 +6737,16 @@ var OpenMatesClient = class _OpenMatesClient {
5971
6737
  }
5972
6738
  return response.data;
5973
6739
  }
5974
- async completeUserTask(taskId, input = {}) {
6740
+ async completeUserTask(taskId, input) {
5975
6741
  return this.postUserTaskAction(taskId, "complete", input);
5976
6742
  }
5977
- async blockUserTask(taskId, input = {}) {
6743
+ async blockUserTask(taskId, input) {
5978
6744
  return this.postUserTaskAction(taskId, "block", input);
5979
6745
  }
5980
- async unblockUserTask(taskId, input = {}) {
6746
+ async unblockUserTask(taskId, input) {
5981
6747
  return this.postUserTaskAction(taskId, "unblock", input);
5982
6748
  }
5983
- async skipUserTask(taskId, input = {}) {
6749
+ async skipUserTask(taskId, input) {
5984
6750
  return this.postUserTaskAction(taskId, "skip", input);
5985
6751
  }
5986
6752
  async reorderUserTasks(input) {
@@ -5995,7 +6761,7 @@ var OpenMatesClient = class _OpenMatesClient {
5995
6761
  }
5996
6762
  return response.data.tasks ?? [];
5997
6763
  }
5998
- async postUserTaskAction(taskId, action, input = {}) {
6764
+ async postUserTaskAction(taskId, action, input) {
5999
6765
  this.requireSession();
6000
6766
  const response = await this.http.post(
6001
6767
  `/v1/user-tasks/${encodeURIComponent(taskId)}/${encodeURIComponent(action)}`,
@@ -6753,7 +7519,7 @@ Required: ${schema.required.join(", ")}`
6753
7519
  );
6754
7520
  }
6755
7521
  }
6756
- const entryId = params.entryId ?? randomUUID3();
7522
+ const entryId = params.entryId ?? randomUUID4();
6757
7523
  const now = Math.floor(Date.now() / 1e3);
6758
7524
  const hashedKey = hashItemKey(params.appId, params.itemType);
6759
7525
  const plaintextPayload = {
@@ -7228,7 +7994,7 @@ Required: ${schema.required.join(", ")}`
7228
7994
  makeWsClient(session) {
7229
7995
  return new OpenMatesWsClient({
7230
7996
  apiUrl: session.apiUrl,
7231
- sessionId: randomUUID3(),
7997
+ sessionId: randomUUID4(),
7232
7998
  wsToken: session.wsToken,
7233
7999
  refreshToken: session.cookies.auth_refresh_token ?? null,
7234
8000
  // Same User-Agent as login so OS-based device fingerprint hash matches.
@@ -8411,7 +9177,7 @@ var OutputRedactor = class {
8411
9177
  import { readFileSync as readFileSync4, statSync, existsSync as existsSync4 } from "fs";
8412
9178
  import { basename, extname, resolve as resolve2 } from "path";
8413
9179
  import { homedir as homedir4 } from "os";
8414
- import { createHash as createHash5 } from "crypto";
9180
+ import { createHash as createHash6 } from "crypto";
8415
9181
  var MAX_PER_FILE_SIZE = 100 * 1024 * 1024;
8416
9182
  var BLOCKED_EXTENSIONS = /* @__PURE__ */ new Set([
8417
9183
  ".pem",
@@ -8721,7 +9487,7 @@ function processCodeFile(filePath, filename, redactor) {
8721
9487
  line_count: lineCount
8722
9488
  });
8723
9489
  const textPreview = `${filename} (${language}, ${lineCount} lines)`;
8724
- const contentHash = createHash5("sha256").update(content).digest("hex");
9490
+ const contentHash = createHash6("sha256").update(content).digest("hex");
8725
9491
  const embed = {
8726
9492
  embedId,
8727
9493
  embedRef,
@@ -10237,7 +11003,7 @@ function formatTs(ts) {
10237
11003
 
10238
11004
  // src/server.ts
10239
11005
  import { execFileSync as execFileSync2, execSync, spawn as nodeSpawn, spawnSync } from "child_process";
10240
- import { createHash as createHash6, randomBytes as randomBytes2 } from "crypto";
11006
+ import { createHash as createHash7, randomBytes as randomBytes3 } from "crypto";
10241
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";
10242
11008
  import { createInterface as createInterface2 } from "readline";
10243
11009
  import { createInterface as createPromptInterface } from "readline/promises";
@@ -10855,10 +11621,10 @@ function resolveTargetImageTag(flags, currentTag, packageVersion) {
10855
11621
  return { tag: getDefaultImageTagForVersion(packageVersion) };
10856
11622
  }
10857
11623
  function randomHex(bytes) {
10858
- return randomBytes2(bytes).toString("hex");
11624
+ return randomBytes3(bytes).toString("hex");
10859
11625
  }
10860
11626
  function generateInviteCode() {
10861
- const digits = Array.from(randomBytes2(12), (byte) => String(byte % 10)).join("");
11627
+ const digits = Array.from(randomBytes3(12), (byte) => String(byte % 10)).join("");
10862
11628
  return `${digits.slice(0, 4)}-${digits.slice(4, 8)}-${digits.slice(8, 12)}`;
10863
11629
  }
10864
11630
  function getEnvVar(content, name) {
@@ -10902,7 +11668,7 @@ function packagedCaddyTemplatePath(role) {
10902
11668
  }
10903
11669
  function fileHash(path) {
10904
11670
  if (!existsSync5(path)) return null;
10905
- return createHash6("sha256").update(readFileSync5(path)).digest("hex");
11671
+ return createHash7("sha256").update(readFileSync5(path)).digest("hex");
10906
11672
  }
10907
11673
  async function loadSelfHostComposeTemplate(templateRef, role) {
10908
11674
  const templateDir = process.env.OPENMATES_SELFHOST_TEMPLATE_DIR;
@@ -11288,7 +12054,7 @@ function formatSecretPreflight(preflight) {
11288
12054
  return parts.join("; ");
11289
12055
  }
11290
12056
  function hashFile(path) {
11291
- const hash = createHash6("sha256");
12057
+ const hash = createHash7("sha256");
11292
12058
  const fd = openSync(path, "r");
11293
12059
  const buffer = Buffer.allocUnsafe(CHECKSUM_BUFFER_BYTES);
11294
12060
  try {
@@ -29309,7 +30075,10 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
29309
30075
  },
29310
30076
  models3d: {
29311
30077
  generate: {
29312
- 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
+ }
29313
30082
  }
29314
30083
  },
29315
30084
  music: {
@@ -30164,6 +30933,12 @@ Only output the final Markdown table. Do NOT include explanations, notes, or any
30164
30933
  text: "Create structured idea maps and brainstorm topic trees."
30165
30934
  }
30166
30935
  },
30936
+ models3d: {
30937
+ text: "3D Models",
30938
+ description: {
30939
+ text: "Generate 3D models from prompts or reference images."
30940
+ }
30941
+ },
30167
30942
  math: {
30168
30943
  text: "Math",
30169
30944
  description: {
@@ -46676,7 +47451,7 @@ function buildAssistantFeedbackDecision(rating) {
46676
47451
  }
46677
47452
 
46678
47453
  // src/benchmark.ts
46679
- import { randomUUID as randomUUID4 } from "crypto";
47454
+ import { randomUUID as randomUUID5 } from "crypto";
46680
47455
  import { existsSync as existsSync6, mkdtempSync as mkdtempSync2, readFileSync as readFileSync6, readdirSync as readdirSync2, writeFileSync as writeFileSync4 } from "fs";
46681
47456
  import { tmpdir } from "os";
46682
47457
  import { dirname as dirname2, join as join4, resolve as resolve5 } from "path";
@@ -46976,7 +47751,7 @@ async function handleBenchmark(client, subcommand, rest, flags) {
46976
47751
  const caseIds = parseCaseIds(flags.case);
46977
47752
  const dryRun = flags["dry-run"] === true;
46978
47753
  const output = typeof flags.output === "string" ? flags.output : void 0;
46979
- const runId = typeof flags["run-id"] === "string" ? flags["run-id"] : randomUUID4();
47754
+ const runId = typeof flags["run-id"] === "string" ? flags["run-id"] : randomUUID5();
46980
47755
  const imagePath = typeof flags.image === "string" ? resolve5(flags.image) : defaultImageFixturePath();
46981
47756
  if (!dryRun && flags["confirm-spend-credits"] !== true) {
46982
47757
  throw new Error(
@@ -47386,7 +48161,7 @@ function buildLongContextHistory() {
47386
48161
  }
47387
48162
  function appendHistory(history, role, content) {
47388
48163
  history.push({
47389
- message_id: randomUUID4(),
48164
+ message_id: randomUUID5(),
47390
48165
  role,
47391
48166
  sender_name: role === "user" ? "User" : "Assistant",
47392
48167
  content,
@@ -47921,7 +48696,7 @@ function renderBody(state, width) {
47921
48696
  case "tasks":
47922
48697
  return renderTasks(state, width);
47923
48698
  case "task":
47924
- return renderTaskDetail(state, width);
48699
+ return renderTaskDetail2(state, width);
47925
48700
  case "start":
47926
48701
  default:
47927
48702
  return renderStart(width);
@@ -47994,7 +48769,7 @@ function renderTasks(state, width) {
47994
48769
  }
47995
48770
  return lines.flatMap((line) => wrap(line, width));
47996
48771
  }
47997
- function renderTaskDetail(state, width) {
48772
+ function renderTaskDetail2(state, width) {
47998
48773
  const task = state.activeTask;
47999
48774
  if (!task) return renderTasks(state, width);
48000
48775
  const assignee = task.assigneeType === "ai" ? "OpenMates" : task.assigneeHash ?? "user";
@@ -48311,221 +49086,6 @@ function boxed(line, width) {
48311
49086
  return `\u2502 ${line.padEnd(width)} \u2502`;
48312
49087
  }
48313
49088
 
48314
- // src/tasksCli.ts
48315
- import { createHash as createHash7, randomBytes as randomBytes3, randomUUID as randomUUID5 } from "crypto";
48316
- var TASK_STATUSES = ["backlog", "todo", "in_progress", "blocked", "done"];
48317
- var DEFAULT_STANDALONE_PREFIX = "TASK";
48318
- function normalizeTaskStatus(value) {
48319
- if (value === void 0) return void 0;
48320
- if (TASK_STATUSES.includes(value)) return value;
48321
- throw new Error(`Unknown task status '${value}'. Expected one of: ${TASK_STATUSES.join(", ")}`);
48322
- }
48323
- function parseAssignee(value) {
48324
- if (!value || value === "user") return { assigneeType: "user", assigneeHash: null };
48325
- if (["ai", "openmates", "OpenMates"].includes(value)) return { assigneeType: "ai", assigneeHash: null };
48326
- return { assigneeType: "user", assigneeHash: value };
48327
- }
48328
- function splitCsvFlag(value) {
48329
- if (typeof value !== "string") return [];
48330
- return value.split(/[,\n]/).map((item) => item.trim()).filter(Boolean);
48331
- }
48332
- function parseDueAt(value) {
48333
- if (value === void 0) return void 0;
48334
- if (value === false || value === true) throw new Error("--due requires a timestamp or date value.");
48335
- const numeric = Number(value);
48336
- if (Number.isFinite(numeric) && numeric > 0) return Math.floor(numeric);
48337
- const parsed = Date.parse(value);
48338
- if (Number.isNaN(parsed)) throw new Error(`Invalid --due value '${value}'.`);
48339
- return Math.floor(parsed / 1e3);
48340
- }
48341
- async function buildCreateUserTaskInput(masterKey, input) {
48342
- const taskKey = randomBytes3(32);
48343
- const encryptedTaskKey = await encryptBytesWithAesGcm(taskKey, masterKey);
48344
- const timestamp = nowSeconds();
48345
- const assignee = parseAssignee(input.assign);
48346
- const linkedProjectIds = input.projectIds ?? [];
48347
- const status = input.status ?? (assignee.assigneeType === "ai" && !input.dueAt ? "in_progress" : "todo");
48348
- return {
48349
- task_id: randomUUIDCompat(),
48350
- short_id: void 0,
48351
- encrypted_task_key: encryptedTaskKey,
48352
- encrypted_title: await encryptWithAesGcmCombined(input.title, taskKey),
48353
- encrypted_description: await encryptWithAesGcmCombined(input.description ?? "", taskKey),
48354
- encrypted_tags: await encryptWithAesGcmCombined("[]", taskKey),
48355
- encrypted_linked_project_ids: await encryptWithAesGcmCombined(JSON.stringify(linkedProjectIds), taskKey),
48356
- status,
48357
- assignee_type: assignee.assigneeType,
48358
- assignee_hash: assignee.assigneeHash,
48359
- primary_chat_id: input.chatId ?? null,
48360
- linked_project_ids: linkedProjectIds,
48361
- plan_id: input.planId ?? null,
48362
- due_at: input.dueAt ?? null,
48363
- priority: 0,
48364
- position: timestamp,
48365
- created_at: timestamp,
48366
- updated_at: timestamp
48367
- };
48368
- }
48369
- async function buildUpdateUserTaskInput(task, masterKey, input) {
48370
- const taskKey = await taskKeyFromRecord(task.encrypted, masterKey);
48371
- const patch = { version: task.version, updated_at: nowSeconds() };
48372
- if (input.title !== void 0) patch.encrypted_title = await encryptWithAesGcmCombined(input.title, taskKey);
48373
- if (input.description !== void 0) patch.encrypted_description = await encryptWithAesGcmCombined(input.description, taskKey);
48374
- if (input.status !== void 0) patch.status = input.status;
48375
- if (input.assign !== void 0) {
48376
- const assignee = parseAssignee(input.assign);
48377
- patch.assignee_type = assignee.assigneeType;
48378
- patch.assignee_hash = assignee.assigneeHash;
48379
- }
48380
- if (input.chatId !== void 0) patch.primary_chat_id = input.chatId;
48381
- if (input.projectIds !== void 0) {
48382
- patch.linked_project_ids = input.projectIds;
48383
- patch.encrypted_linked_project_ids = await encryptWithAesGcmCombined(JSON.stringify(input.projectIds), taskKey);
48384
- }
48385
- if (input.planId !== void 0) patch.plan_id = input.planId;
48386
- return patch;
48387
- }
48388
- async function decryptUserTask(record, masterKey) {
48389
- const taskKey = await taskKeyFromRecord(record, masterKey);
48390
- const tags = parseStringArray(await decryptOptional(record.encrypted_tags, taskKey));
48391
- const linkedProjectIds = parseStringArray(await decryptOptional(record.encrypted_linked_project_ids, taskKey));
48392
- return {
48393
- taskId: record.task_id,
48394
- shortId: record.short_id || deriveShortId(record),
48395
- title: await decryptOptional(record.encrypted_title, taskKey) || "(untitled task)",
48396
- description: await decryptOptional(record.encrypted_description, taskKey),
48397
- tags,
48398
- latestInstruction: await decryptOptional(record.encrypted_latest_instruction, taskKey),
48399
- status: record.status,
48400
- assigneeType: record.assignee_type,
48401
- assigneeHash: record.assignee_hash ?? null,
48402
- primaryChatId: record.primary_chat_id ?? null,
48403
- linkedProjectIds: linkedProjectIds.length > 0 ? linkedProjectIds : record.linked_project_ids ?? [],
48404
- planId: record.plan_id ?? null,
48405
- dueAt: record.due_at ?? null,
48406
- priority: record.priority ?? 0,
48407
- position: record.position ?? 0,
48408
- queueState: record.queue_state ?? "none",
48409
- blockedReasonCode: record.blocked_reason_code ?? null,
48410
- aiExecutionState: record.ai_execution_state ?? null,
48411
- version: record.version ?? 1,
48412
- encrypted: record
48413
- };
48414
- }
48415
- async function decryptUserTasks(records, masterKey) {
48416
- const output = [];
48417
- for (const record of records) output.push(await decryptUserTask(record, masterKey));
48418
- return output;
48419
- }
48420
- function findTask(tasks, id) {
48421
- const task = tasks.find((candidate) => candidate.taskId === id || candidate.shortId === id);
48422
- if (!task) throw new Error(`Task '${id}' was not found in the current task list.`);
48423
- return task;
48424
- }
48425
- function renderTaskList(tasks) {
48426
- if (tasks.length === 0) return "No tasks found.";
48427
- const lines = ["Tasks", "ID Status Assignee Queue Title"];
48428
- for (const task of tasks) {
48429
- lines.push(`${pad(task.shortId, 9)} ${pad(task.status, 12)} ${pad(assigneeLabel(task), 11)} ${pad(task.queueState, 11)} ${task.title}`);
48430
- }
48431
- return lines.join("\n");
48432
- }
48433
- function renderTaskDetail2(task) {
48434
- const lines = [
48435
- `Task ${task.shortId}`,
48436
- `Title: ${task.title}`,
48437
- `Status: ${task.status}`,
48438
- `Assignee: ${assigneeLabel(task)}`,
48439
- `Queue: ${task.queueState}`,
48440
- `Task ID: ${task.taskId}`
48441
- ];
48442
- if (task.description) lines.push(`Description: ${task.description}`);
48443
- if (task.primaryChatId) lines.push(`Chat: ${task.primaryChatId}`);
48444
- if (task.linkedProjectIds.length > 0) lines.push(`Projects: ${task.linkedProjectIds.join(", ")}`);
48445
- if (task.planId) lines.push(`Plan: ${task.planId}`);
48446
- if (task.blockedReasonCode) lines.push(`Blocked reason: ${task.blockedReasonCode}`);
48447
- if (task.aiExecutionState) lines.push(`AI state: ${task.aiExecutionState}`);
48448
- return lines.join("\n");
48449
- }
48450
- function renderTaskBoard(tasks, width = process.stdout.columns || 100) {
48451
- const columns = TASK_STATUSES.map((status) => ({ status, tasks: tasks.filter((task) => task.status === status).sort(compareTasks) }));
48452
- if (width < 96) {
48453
- const lines2 = ["OpenMates Tasks Board"];
48454
- for (const column of columns) {
48455
- lines2.push("", `${columnTitle(column.status)} (${column.tasks.length})`, "-".repeat(24));
48456
- lines2.push(...boardColumnLines(column.tasks, 8));
48457
- }
48458
- return lines2.join("\n");
48459
- }
48460
- const perColumn = 6;
48461
- const columnWidth = 22;
48462
- const lines = ["OpenMates Tasks Board", ""];
48463
- lines.push(columns.map((column) => pad(`${columnTitle(column.status)} (${column.tasks.length})`, columnWidth)).join(" "));
48464
- lines.push(columns.map(() => "-".repeat(columnWidth)).join(" "));
48465
- const renderedColumns = columns.map((column) => boardColumnLines(column.tasks, perColumn).map((line) => truncate(line, columnWidth)));
48466
- const maxRows = Math.max(...renderedColumns.map((column) => column.length));
48467
- for (let row = 0; row < maxRows; row += 1) {
48468
- lines.push(renderedColumns.map((column) => pad(column[row] ?? "", columnWidth)).join(" "));
48469
- }
48470
- return lines.join("\n");
48471
- }
48472
- function boardColumnLines(tasks, limit) {
48473
- if (tasks.length === 0) return ["No tasks here."];
48474
- const visible = tasks.slice(0, limit).flatMap((task) => [
48475
- `[${task.shortId}] ${task.title}`,
48476
- ` ${assigneeLabel(task)} ${task.queueState === "none" ? "" : task.queueState}`.trimEnd()
48477
- ]);
48478
- if (tasks.length > limit) visible.push(`... ${tasks.length - limit} more`);
48479
- return visible;
48480
- }
48481
- function compareTasks(a, b) {
48482
- return a.position - b.position || a.title.localeCompare(b.title);
48483
- }
48484
- function columnTitle(status) {
48485
- if (status === "in_progress") return "In progress";
48486
- return status.slice(0, 1).toUpperCase() + status.slice(1).replace("_", " ");
48487
- }
48488
- function assigneeLabel(task) {
48489
- return task.assigneeType === "ai" ? "OpenMates" : task.assigneeHash ?? "user";
48490
- }
48491
- async function taskKeyFromRecord(record, masterKey) {
48492
- if (!record.encrypted_task_key) throw new Error(`Task ${record.task_id} is missing encrypted task key.`);
48493
- const taskKey = await decryptBytesWithAesGcm(record.encrypted_task_key, masterKey);
48494
- if (!taskKey) throw new Error(`Failed to decrypt task key for ${record.task_id}.`);
48495
- return taskKey;
48496
- }
48497
- async function decryptOptional(value, key) {
48498
- if (!value) return "";
48499
- return await decryptWithAesGcmCombined(value, key) ?? "";
48500
- }
48501
- function parseStringArray(value) {
48502
- if (!value) return [];
48503
- try {
48504
- const parsed = JSON.parse(value);
48505
- return Array.isArray(parsed) ? parsed.filter((item) => typeof item === "string") : [];
48506
- } catch {
48507
- return [];
48508
- }
48509
- }
48510
- function deriveShortId(record) {
48511
- const prefix = record.short_id_prefix || DEFAULT_STANDALONE_PREFIX;
48512
- const source = record.task_id || `${record.created_at ?? ""}-${record.position ?? ""}`;
48513
- const digest = createHash7("sha256").update(source).digest("hex").slice(0, 4).toUpperCase();
48514
- return `${prefix}-${parseInt(digest, 16) % 1e4}`;
48515
- }
48516
- function nowSeconds() {
48517
- return Math.floor(Date.now() / 1e3);
48518
- }
48519
- function randomUUIDCompat() {
48520
- return randomUUID5();
48521
- }
48522
- function pad(value, length) {
48523
- return truncate(value, length).padEnd(length);
48524
- }
48525
- function truncate(value, length) {
48526
- return value.length <= length ? value : `${value.slice(0, Math.max(0, length - 3))}...`;
48527
- }
48528
-
48529
49089
  // src/tui.ts
48530
49090
  function defaultModeForStreams(input, output) {
48531
49091
  return input.isTTY === true && output.isTTY === true ? "tui" : "quickstart";
@@ -48959,7 +49519,7 @@ async function handleActiveTaskAction(params) {
48959
49519
  render();
48960
49520
  return;
48961
49521
  }
48962
- await client.deleteUserTask(task.taskId);
49522
+ await client.deleteUserTask(task.taskId, task.version);
48963
49523
  state.tasks = state.tasks.filter((candidate) => candidate.taskId !== task.taskId);
48964
49524
  state.activeTask = null;
48965
49525
  state.selectedIndex = Math.min(state.selectedIndex, Math.max(0, state.tasks.length - 1));
@@ -48976,7 +49536,7 @@ async function handleActiveTaskAction(params) {
48976
49536
  render();
48977
49537
  return;
48978
49538
  }
48979
- 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 }] });
48980
49540
  const [decrypted2] = await decryptUserTasks(updated2, client.getMasterKeyBytes());
48981
49541
  if (decrypted2) {
48982
49542
  replaceTask(state, decrypted2);
@@ -50068,7 +50628,7 @@ async function handleTasks(client, subcommand, rest, flags) {
50068
50628
  if (subcommand === "delete") {
50069
50629
  const task = await requiredResolvedTask(client, masterKey, rest[0], scope, "delete");
50070
50630
  if (flags.confirm !== true) throw new Error("Deleting a task requires --confirm.");
50071
- const result = await client.deleteUserTask(task.taskId);
50631
+ const result = await client.deleteUserTask(task.taskId, task.version);
50072
50632
  if (flags.json === true) printJson2(result);
50073
50633
  else console.log(`Task deleted: ${task.shortId}`);
50074
50634
  return;
@@ -50099,7 +50659,7 @@ async function handleTasks(client, subcommand, rest, flags) {
50099
50659
  }
50100
50660
  if (subcommand === "reorder") {
50101
50661
  const task = await requiredResolvedTask(client, masterKey, rest[0], scope, "reorder");
50102
- const move = { task_id: task.taskId };
50662
+ const move = { task_id: task.taskId, version: task.version };
50103
50663
  if (typeof flags.before === "string") move.before_task_id = (await resolveTask(client, masterKey, flags.before, scope)).taskId;
50104
50664
  if (typeof flags.after === "string") move.after_task_id = (await resolveTask(client, masterKey, flags.after, scope)).taskId;
50105
50665
  if (typeof flags.status === "string") move.status = normalizeTaskStatus(flags.status);
@@ -50134,7 +50694,7 @@ async function requiredResolvedTask(client, masterKey, id, scope, action) {
50134
50694
  }
50135
50695
  function printTaskOutput(task, flags) {
50136
50696
  if (flags.json === true) printJson2({ task: taskToJson(task) });
50137
- else console.log(renderTaskDetail2(task));
50697
+ else console.log(renderTaskDetail(task));
50138
50698
  }
50139
50699
  function taskToJson(task) {
50140
50700
  return {
@@ -50239,6 +50799,10 @@ function parsePositiveIntegerFlag(value, flagName) {
50239
50799
  }
50240
50800
  return parsed;
50241
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
+ }
50242
50806
  function parseRemoteAccessSourceType(value) {
50243
50807
  if (value === void 0) return "local_folder";
50244
50808
  if (value === "local_folder" || value === "local_git_repository") {
@@ -50380,7 +50944,9 @@ async function handleChats(client, subcommand, rest, flags, redactor) {
50380
50944
  json: flags.json === true,
50381
50945
  autoApproveSubChats: flags["auto-approve"] === true,
50382
50946
  autoApproveMemories: flags["auto-approve-memories"] === true,
50947
+ acceptTaskProposals: flags["accept-task-proposals"] === true,
50383
50948
  piiDetection: flags["no-pii-detection"] !== true,
50949
+ responseTimeoutMs: parseResponseTimeoutMs(flags),
50384
50950
  anonymousLearningMode: client.hasSession() ? void 0 : parseAnonymousLearningModeFlags(flags)
50385
50951
  },
50386
50952
  redactor
@@ -50455,7 +51021,9 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
50455
51021
  json: flags.json === true,
50456
51022
  autoApproveSubChats: flags["auto-approve"] === true,
50457
51023
  autoApproveMemories: flags["auto-approve-memories"] === true,
50458
- piiDetection: flags["no-pii-detection"] !== true
51024
+ acceptTaskProposals: flags["accept-task-proposals"] === true,
51025
+ piiDetection: flags["no-pii-detection"] !== true,
51026
+ responseTimeoutMs: parseResponseTimeoutMs(flags)
50459
51027
  },
50460
51028
  redactor
50461
51029
  );
@@ -50483,7 +51051,9 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
50483
51051
  json: flags.json === true,
50484
51052
  autoApproveSubChats: flags["auto-approve"] === true,
50485
51053
  autoApproveMemories: flags["auto-approve-memories"] === true,
50486
- piiDetection: flags["no-pii-detection"] !== true
51054
+ acceptTaskProposals: flags["accept-task-proposals"] === true,
51055
+ piiDetection: flags["no-pii-detection"] !== true,
51056
+ responseTimeoutMs: parseResponseTimeoutMs(flags)
50487
51057
  },
50488
51058
  redactor
50489
51059
  );
@@ -50504,7 +51074,8 @@ Run 'openmates chats show ` + chatId + "' to check if suggestions have been save
50504
51074
  json: flags.json === true,
50505
51075
  autoApproveSubChats: flags["auto-approve"] === true,
50506
51076
  autoApproveMemories: flags["auto-approve-memories"] === true,
50507
- piiDetection: flags["no-pii-detection"] !== true
51077
+ piiDetection: flags["no-pii-detection"] !== true,
51078
+ responseTimeoutMs: parseResponseTimeoutMs(flags)
50508
51079
  },
50509
51080
  redactor
50510
51081
  );
@@ -53891,7 +54462,7 @@ async function sendMessageStreaming(client, params, redactor) {
53891
54462
  process.stdout.write(`${result2.assistant}
53892
54463
  `);
53893
54464
  }
53894
- return result2;
54465
+ return { ...result2, taskEvents: [], pendingTaskUpdateJobs: [], acceptedTaskProposals: [] };
53895
54466
  }
53896
54467
  const urlResult = prepareUrlEmbeds(finalMessage);
53897
54468
  finalMessage = urlResult.message;
@@ -53906,6 +54477,7 @@ async function sendMessageStreaming(client, params, redactor) {
53906
54477
  onSubChatApprovalRequest,
53907
54478
  autoApproveSubChats: params.autoApproveSubChats,
53908
54479
  autoApproveMemories: params.autoApproveMemories,
54480
+ responseTimeoutMs: params.responseTimeoutMs,
53909
54481
  learningMode,
53910
54482
  preparedEmbeds: preparedEmbeds.length > 0 ? preparedEmbeds : void 0,
53911
54483
  piiMappings: piiResult.mappings.map((mapping) => ({
@@ -53915,6 +54487,9 @@ async function sendMessageStreaming(client, params, redactor) {
53915
54487
  }))
53916
54488
  });
53917
54489
  clearTyping();
54490
+ const acceptedTaskProposals = params.acceptTaskProposals === true && result.status === "completed" ? await acceptChatTaskProposals(client, result.chatId, result.taskProposals, `${finalMessage}
54491
+
54492
+ ${result.assistant}`) : [];
53918
54493
  if (result.status === "waiting_for_user") {
53919
54494
  const question = parseInteractiveQuestionBlock(result.assistant);
53920
54495
  if (params.json && question) {
@@ -53930,7 +54505,7 @@ async function sendMessageStreaming(client, params, redactor) {
53930
54505
  "\x1B[33mOpenMates needs more input to continue.\x1B[0m\nContinue in the web app, or rerun with --json to inspect the structured waiting state.\n"
53931
54506
  );
53932
54507
  }
53933
- return result;
54508
+ return { ...result, acceptedTaskProposals: [] };
53934
54509
  }
53935
54510
  if (!params.json) {
53936
54511
  if (!headerPrinted) {
@@ -53994,13 +54569,87 @@ async function sendMessageStreaming(client, params, redactor) {
53994
54569
  process.stdout.write(`${SEP}
53995
54570
  `);
53996
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
+ }
53997
54590
  process.stdout.write(
53998
54591
  `\x1B[2mContinue: openmates chats send --chat ${shortId} "your message"\x1B[0m
53999
54592
  \x1B[2mHistory: openmates chats show ${shortId}\x1B[0m
54000
54593
  `
54001
54594
  );
54002
54595
  }
54003
- return result;
54596
+ return { ...result, acceptedTaskProposals };
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
+ }
54618
+ async function acceptChatTaskProposals(client, chatId, proposals, fallbackText) {
54619
+ let proposalsToAccept = proposals;
54620
+ if (proposalsToAccept.length === 0 && fallbackText.trim()) {
54621
+ const extractionText = buildTaskProposalFallbackText(fallbackText);
54622
+ proposalsToAccept = await client.extractUserTaskProposals({
54623
+ correctedText: extractionText,
54624
+ contextChatId: chatId
54625
+ });
54626
+ }
54627
+ if (proposalsToAccept.length === 0) return [];
54628
+ const masterKey = client.getMasterKeyBytes();
54629
+ const accepted = [];
54630
+ for (const proposal of proposalsToAccept) {
54631
+ const input = await buildCreateUserTaskInput(masterKey, {
54632
+ title: proposal.title,
54633
+ description: proposal.description ?? "",
54634
+ status: proposal.status,
54635
+ assign: proposal.assignee_type ?? "user",
54636
+ chatId
54637
+ });
54638
+ const created = await client.createUserTask(input);
54639
+ const decrypted = await decryptUserTask(created, masterKey);
54640
+ accepted.push(taskToJson(decrypted));
54641
+ }
54642
+ return accepted;
54643
+ }
54644
+ function buildTaskProposalFallbackText(text) {
54645
+ const seen = /* @__PURE__ */ new Set();
54646
+ const bulletLines = text.split("\n").map((line) => line.trim()).filter((line) => /^[-*•]\s+/.test(line)).map((line) => line.replace(/^([-*•]\s*)\[[ xX]\]\s*/, "$1")).filter((line) => {
54647
+ const key = line.replace(/^[-*•]\s+/, "").trim().toLowerCase();
54648
+ if (seen.has(key)) return false;
54649
+ seen.add(key);
54650
+ return true;
54651
+ });
54652
+ return bulletLines.length > 0 ? bulletLines.join("\n") : text;
54004
54653
  }
54005
54654
  function printIncognitoNoHistoryNotice(json) {
54006
54655
  const message = "Incognito chats are not stored. There is no incognito history to show or clear.";
@@ -55401,10 +56050,10 @@ function printChatsHelp() {
55401
56050
  openmates chats show <chat-id> [--raw] [--json]
55402
56051
  openmates chats open [<n|example-id|slug>] [--json]
55403
56052
  openmates chats search <query> [--json]
55404
- openmates chats new <message> [--json] [--learning-mode --age-group <group>] [--auto-approve] [--auto-approve-memories] [--no-pii-detection]
55405
- openmates chats send [--chat <id>] [--incognito] <message> [--json] [--auto-approve] [--auto-approve-memories] [--no-pii-detection]
56053
+ openmates chats new <message> [--json] [--learning-mode --age-group <group>] [--auto-approve] [--auto-approve-memories] [--accept-task-proposals] [--no-pii-detection]
56054
+ openmates chats send [--chat <id>] [--incognito] <message> [--json] [--auto-approve] [--auto-approve-memories] [--accept-task-proposals] [--no-pii-detection]
55406
56055
  openmates chats send --chat <id> --followup <n> [--json] [--auto-approve] [--auto-approve-memories]
55407
- openmates chats answer-interactive --chat <id> --question-json '<json>' --answer-json '<json>' [--json]
56056
+ openmates chats answer-interactive --chat <id> --question-json '<json>' --answer-json '<json>' [--json] [--accept-task-proposals]
55408
56057
  openmates chats download <chat-id> [--output <path>] [--zip] [--json]
55409
56058
  openmates chats delete <id1> [id2] [id3] ... [--yes]
55410
56059
  openmates chats share [<chat-id>] [--expires <seconds>] [--password <pwd>] [--json]
@@ -55447,6 +56096,11 @@ Options for 'new', 'send', and 'incognito':
55447
56096
  --no-pii-detection Send the message exactly as typed. By default, the CLI
55448
56097
  replaces detected PII with placeholders before send.
55449
56098
 
56099
+ Saved-chat task options for 'new', 'send', and 'answer-interactive':
56100
+ --accept-task-proposals Explicitly save assistant task proposals as encrypted
56101
+ Tasks V1 records scoped to the chat. Without this,
56102
+ proposals remain review-only, like the web app card.
56103
+
55450
56104
  Guest-only options for logged-out 'new':
55451
56105
  --learning-mode Opt anonymous chat into request-scoped Learning Mode.
55452
56106
  --age-group <group> Required with --learning-mode: under_10, 10_12,