@clawos-dev/clawd 0.2.245 → 0.2.247

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.
package/dist/cli.cjs CHANGED
@@ -67,6 +67,8 @@ var init_methods = __esm({
67
67
  "session:subscribe",
68
68
  "session:unsubscribe",
69
69
  "session:pin",
70
+ // 清共享未读标记(SessionFile.unreadAt → null)+ 广播 session:info;幂等
71
+ "session:markRead",
70
72
  "session:reorderPins",
71
73
  // mirror peer sessions (2026-05-26): owner-only RPC,写/删本机 mirror SessionFile —
72
74
  // mirror 是「**我**在朋友 daemon 上自己创建的 session」的本机持久化副本(不是朋友的会话),
@@ -4781,6 +4783,11 @@ var init_schemas = __esm({
4781
4783
  // pinned 行手动拖拽顺序;越小越靠前。session:reorderPins 整体重写所有 pinned 的此字段。
4782
4784
  // 缺失时 view-state 排序回退到 pinnedAt desc(旧数据兼容)
4783
4785
  pinSortOrder: external_exports.number().int().nullable().optional(),
4786
+ // daemon 权威的共享未读标记(spec 2026-07-20-shared-session-unread):
4787
+ // session wire status 从 running 跃迁为非 running 时 daemon 置为当时 ISO 时间戳;
4788
+ // session:markRead RPC 或新 turn 开始(→ running)时清为 null。
4789
+ // 所有客户端(desktop / mobile)共享同一份,任一端标读全端清。
4790
+ unreadAt: external_exports.string().min(1).nullable().optional(),
4784
4791
  // 用户在 NewSessionDialog 或 Edit modal 选择的 icon 标识;UI 端做 enum 兜底
4785
4792
  iconKey: external_exports.string().optional(),
4786
4793
  // 由 session:fork 派生的 session 在 create 时记录源 session 的 sessionId(稳定 key,
@@ -5126,7 +5133,13 @@ var init_schemas = __esm({
5126
5133
  /** true = 只返回 mirror(originOwnerPrincipalId 非空,即「我在朋友 daemon 上创建」的本机存证)。 */
5127
5134
  originOnly: external_exports.boolean().optional(),
5128
5135
  /** 按 updatedAt desc(list 的既有排序)截断前 N 条。 */
5129
- limit: external_exports.number().int().positive().optional()
5136
+ limit: external_exports.number().int().positive().optional(),
5137
+ /**
5138
+ * 分页起点(2026-07-18 android-tasks-tab 迭代追加):originOnly 过滤后、limit 截断前跳过
5139
+ * 前 N 条。与 limit 组合 = updatedAt desc 序上的一页。响应带 total(过滤后总数,见 handler)
5140
+ * 供客户端判 hasMore。老 daemon 不识别时忽略 → 回全量,客户端按全量兼容。
5141
+ */
5142
+ offset: external_exports.number().int().nonnegative().optional()
5130
5143
  });
5131
5144
  SessionIdArgs = external_exports.object({ sessionId: external_exports.string().min(1) });
5132
5145
  SessionUpdateArgs = external_exports.object({
@@ -5228,7 +5241,16 @@ var init_schemas = __esm({
5228
5241
  sessionId: external_exports.string().min(1),
5229
5242
  limit: external_exports.number().int().positive().max(5e3).optional(),
5230
5243
  /** 不传时走 tail 语义:daemon 返回最后 limit 条(offset = max(0, total - limit)) */
5231
- offset: external_exports.number().int().nonnegative().optional()
5244
+ offset: external_exports.number().int().nonnegative().optional(),
5245
+ /**
5246
+ * 瘦身投影(2026-07-18 android-tasks-tab 迭代,v2 语义):true = daemon 裁掉窄消费面客户端
5247
+ * (mobile)不渲染的大字段(input / output / raw / toolResultExtra / memories /
5248
+ * sourceToolAssistantUUID),meta 注入行 text 截 500 字符。**例外:出错的工具调用全量保留**——
5249
+ * 本页内 error 非空的 tool_result 保留 output,且其同 toolUseId 的 tool_call 保留 input
5250
+ * (用户要能看清「啥错了」;老板 2026-07-18 拍板:普通工具调用手机不渲染,错误全量展示)。
5251
+ * desktop 渲染 tool card 需全文,不用此参数。老 daemon 不识别 → 回全量,客户端向后兼容。
5252
+ */
5253
+ slim: external_exports.boolean().optional()
5232
5254
  });
5233
5255
  HistorySubagentsArgs = external_exports.object({
5234
5256
  cwd: external_exports.string().min(1),
@@ -11595,6 +11617,10 @@ function messageFromJsonl(obj) {
11595
11617
  ts,
11596
11618
  raw: obj
11597
11619
  };
11620
+ if (toolResult.is_error === true && typeof toolResult.content === "string") {
11621
+ msg.error = toolResult.content;
11622
+ delete msg.output;
11623
+ }
11598
11624
  if (toolResultExtra) msg.toolResultExtra = toolResultExtra;
11599
11625
  if (sourceToolAssistantUUID) msg.sourceToolAssistantUUID = sourceToolAssistantUUID;
11600
11626
  const answers = extractAskQuestionAnswers(o.toolUseResult);
@@ -41862,6 +41888,25 @@ deviceId \u76F4\u63A5\u53D6\u81EA token\uFF0C\u4E0D\u8981\u6539\u5199\u6216\u731
41862
41888
  - \u5931\u8D25\uFF1A\`{ isFailure: true, reason: "..." }\`
41863
41889
  - \u62A5\u544A\u5B8C\u540E\u4F60\u7684 session \u5C31\u7ED3\u675F\u4E86\uFF0C\u4E0D\u8981\u7EE7\u7EED\u8F93\u51FA`;
41864
41890
 
41891
+ // src/session/reducer-types.ts
41892
+ function compressStatus(status) {
41893
+ switch (status) {
41894
+ case "spawning":
41895
+ case "running":
41896
+ return "running";
41897
+ case "running-idle":
41898
+ return "running-idle";
41899
+ case "stopping":
41900
+ case "stopped":
41901
+ return "stopped";
41902
+ case "error":
41903
+ return "error";
41904
+ case "idle":
41905
+ default:
41906
+ return "idle";
41907
+ }
41908
+ }
41909
+
41865
41910
  // src/session/reducer.ts
41866
41911
  function cloneState(s) {
41867
41912
  return {
@@ -42171,7 +42216,7 @@ var RUNTIME_PATCH_KEYS = [
42171
42216
  "effort",
42172
42217
  "cwd"
42173
42218
  ];
42174
- var MARKER_PATCH_KEYS = ["pinnedAt", "pinSortOrder"];
42219
+ var MARKER_PATCH_KEYS = ["pinnedAt", "pinSortOrder", "unreadAt"];
42175
42220
  function isMarkerOnlyPatch(patch) {
42176
42221
  const keys = Object.keys(patch).filter(
42177
42222
  (k2) => patch[k2] !== void 0
@@ -42416,6 +42461,24 @@ function assertNever(x) {
42416
42461
  throw new Error(`unexpected command: ${JSON.stringify(x)}`);
42417
42462
  }
42418
42463
  function reduceSession(state, input, deps) {
42464
+ const result = reduceSessionInner(state, input, deps);
42465
+ const prevWire = compressStatus(state.status);
42466
+ const nextWire = compressStatus(result.state.status);
42467
+ if (prevWire === nextWire) return result;
42468
+ let file;
42469
+ if (prevWire === "running") {
42470
+ file = { ...result.state.file, unreadAt: nowIso(deps) };
42471
+ } else if (nextWire === "running" && result.state.file.unreadAt != null) {
42472
+ file = { ...result.state.file, unreadAt: null };
42473
+ } else {
42474
+ return result;
42475
+ }
42476
+ return {
42477
+ state: { ...result.state, file },
42478
+ effects: [...result.effects, { kind: "persist-file", file }, sessionInfoFrame(file)]
42479
+ };
42480
+ }
42481
+ function reduceSessionInner(state, input, deps) {
42419
42482
  switch (input.kind) {
42420
42483
  case "stdout-line": {
42421
42484
  const events = deps.parseLine(input.line);
@@ -43197,53 +43260,13 @@ function probePtyDataThrottled(tsid, len, tail) {
43197
43260
  function compressFrameForWire(frame) {
43198
43261
  if (frame.type !== "session:status") return frame;
43199
43262
  const raw = frame;
43200
- const status = raw.status;
43201
- let compressed;
43202
- switch (status) {
43203
- case "spawning":
43204
- case "running":
43205
- compressed = "running";
43206
- break;
43207
- case "running-idle":
43208
- compressed = "running-idle";
43209
- break;
43210
- case "stopping":
43211
- case "stopped":
43212
- compressed = "stopped";
43213
- break;
43214
- case "error":
43215
- compressed = "error";
43216
- break;
43217
- case "idle":
43218
- compressed = "idle";
43219
- break;
43220
- default:
43221
- compressed = "idle";
43222
- break;
43223
- }
43263
+ const compressed = compressStatus(raw.status);
43224
43264
  return {
43225
43265
  type: "session:status",
43226
43266
  sessionId: raw.sessionId,
43227
43267
  status: compressed
43228
43268
  };
43229
43269
  }
43230
- function compressStatus(status) {
43231
- switch (status) {
43232
- case "spawning":
43233
- case "running":
43234
- return "running";
43235
- case "running-idle":
43236
- return "running-idle";
43237
- case "stopping":
43238
- case "stopped":
43239
- return "stopped";
43240
- case "error":
43241
- return "error";
43242
- case "idle":
43243
- default:
43244
- return "idle";
43245
- }
43246
- }
43247
43270
  function nowIso2(deps) {
43248
43271
  return new Date((deps.now ?? Date.now)()).toISOString();
43249
43272
  }
@@ -43789,6 +43812,29 @@ var SessionManager = class {
43789
43812
  this.writeOwned(updated);
43790
43813
  return { response: updated, broadcast: [] };
43791
43814
  }
43815
+ // 清共享未读(spec 2026-07-20-shared-session-unread)。幂等:已读直接 no-op 不广播。
43816
+ // 与 pin 的差异:no-runner 路径也要广播 session:info——跨端实时清红点是本 RPC 的核心
43817
+ // 语义,不能等下次 session:list。
43818
+ markRead(args) {
43819
+ const existing = this.getFile(args.sessionId);
43820
+ if (existing.unreadAt == null) return { response: existing, broadcast: [] };
43821
+ const runner = this.runners.get(args.sessionId);
43822
+ if (runner) {
43823
+ const { value, broadcast } = this.withCollector(() => {
43824
+ runner.input({ kind: "command", command: { kind: "update", patch: { unreadAt: null } } });
43825
+ return runner.getState().file;
43826
+ });
43827
+ return { response: value, broadcast };
43828
+ }
43829
+ const updated = { ...existing, unreadAt: null };
43830
+ this.writeOwned(updated);
43831
+ return {
43832
+ response: updated,
43833
+ broadcast: [
43834
+ { frame: { type: "session:info", ...updated }, target: "all" }
43835
+ ]
43836
+ };
43837
+ }
43792
43838
  // sidebar 拖拽完成 → 整体重写 pinned root 的 pinSortOrder(按 orderedIds 下标 0,1,2...)
43793
43839
  // orderedIds 必须正好覆盖所有当前 pinned root(既不能漏,也不能含 unpinned id)
43794
43840
  reorderPins(args) {
@@ -53925,21 +53971,28 @@ function buildSessionHandlers(deps) {
53925
53971
  const narrow = (sessions) => {
53926
53972
  let out = sessions;
53927
53973
  if (args.originOnly) out = out.filter((s) => s.originOwnerPrincipalId != null);
53928
- if (args.limit != null) out = out.slice(0, args.limit);
53929
- return out;
53974
+ const total = out.length;
53975
+ const offset = args.offset ?? 0;
53976
+ if (offset > 0 || args.limit != null) {
53977
+ out = out.slice(offset, args.limit != null ? offset + args.limit : void 0);
53978
+ }
53979
+ return { sessions: out, total };
53930
53980
  };
53931
53981
  const { response } = manager.list();
53932
53982
  if (ctx && ctx.principal.kind === "guest") {
53933
53983
  const sessions = response.sessions ?? [];
53934
53984
  const guestDeviceId = ctx.principal.id;
53935
53985
  const filtered = sessions.filter((s) => canAccessPersona(ctx.grants, s.ownerPersonaId, "read")).filter((s) => s.creatorPrincipalId === guestDeviceId);
53936
- return { response: { type: "session:list", sessions: narrow(filtered) } };
53986
+ const page2 = narrow(filtered);
53987
+ return { response: { type: "session:list", sessions: page2.sessions, total: page2.total } };
53937
53988
  }
53989
+ const page = narrow(response.sessions ?? []);
53938
53990
  return {
53939
53991
  response: {
53940
53992
  type: "session:list",
53941
53993
  ...response,
53942
- sessions: narrow(response.sessions ?? [])
53994
+ sessions: page.sessions,
53995
+ total: page.total
53943
53996
  }
53944
53997
  };
53945
53998
  };
@@ -54110,6 +54163,12 @@ function buildSessionHandlers(deps) {
54110
54163
  const { response, broadcast } = manager.pin(args);
54111
54164
  return { response: { type: "session:info", ...response }, broadcast };
54112
54165
  };
54166
+ const markRead = async (frame, _client, ctx) => {
54167
+ const args = SessionIdArgs.parse(frame);
54168
+ ensureSessionAccess(ctx, args.sessionId, "send");
54169
+ const { response, broadcast } = manager.markRead(args);
54170
+ return { response: { type: "session:info", ...response }, broadcast };
54171
+ };
54113
54172
  const reorderPins = async (frame) => {
54114
54173
  const args = SessionReorderPinsArgs.parse(frame);
54115
54174
  const { response, broadcast } = manager.reorderPins(args);
@@ -54225,6 +54284,7 @@ function buildSessionHandlers(deps) {
54225
54284
  "session:subscribe": subscribe,
54226
54285
  "session:unsubscribe": unsubscribe,
54227
54286
  "session:pin": pin,
54287
+ "session:markRead": markRead,
54228
54288
  "session:reorderPins": reorderPins,
54229
54289
  "session:answerQuestion": answerQuestion,
54230
54290
  "session:cancelQuestion": cancelQuestion,
@@ -54322,6 +54382,32 @@ function isPathInGuestBoundary(personaRoot, personaId, userWorkDir, absPath) {
54322
54382
 
54323
54383
  // src/handlers/history.ts
54324
54384
  init_protocol();
54385
+ var SLIM_INJECTED_TEXT_MAX = 500;
54386
+ function slimHistoryMessages(messages) {
54387
+ const errorToolUseIds = new Set(
54388
+ messages.filter((m2) => m2.kind === "tool_result" && m2.error !== void 0 && m2.toolUseId !== void 0).map((m2) => m2.toolUseId)
54389
+ );
54390
+ return messages.map((m2) => {
54391
+ const out = { kind: m2.kind };
54392
+ if (m2.text !== void 0) {
54393
+ out.text = m2.meta !== void 0 && m2.text.length > SLIM_INJECTED_TEXT_MAX ? m2.text.slice(0, SLIM_INJECTED_TEXT_MAX) : m2.text;
54394
+ }
54395
+ if (m2.tool !== void 0) out.tool = m2.tool;
54396
+ if (m2.toolUseId !== void 0) out.toolUseId = m2.toolUseId;
54397
+ if (m2.uuid !== void 0) out.uuid = m2.uuid;
54398
+ if (m2.ts !== void 0) out.ts = m2.ts;
54399
+ if (m2.meta !== void 0) out.meta = m2.meta;
54400
+ if (m2.error !== void 0) out.error = m2.error;
54401
+ if (m2.taskId !== void 0) out.taskId = m2.taskId;
54402
+ if (m2.parentToolUseId !== void 0) out.parentToolUseId = m2.parentToolUseId;
54403
+ if (m2.isSidechain !== void 0) out.isSidechain = m2.isSidechain;
54404
+ if (m2.askQuestionAnswers !== void 0) out.askQuestionAnswers = m2.askQuestionAnswers;
54405
+ const isErrorPair = m2.toolUseId !== void 0 && errorToolUseIds.has(m2.toolUseId);
54406
+ if (m2.kind === "tool_call" && isErrorPair && m2.input !== void 0) out.input = m2.input;
54407
+ if (m2.kind === "tool_result" && m2.error !== void 0 && m2.output !== void 0) out.output = m2.output;
54408
+ return out;
54409
+ });
54410
+ }
54325
54411
  function assertGuestPath(ctx, absPath, personaRoot, method, usersRoot) {
54326
54412
  if (!ctx || ctx.principal.kind !== "guest" || !personaRoot) return;
54327
54413
  const userWorkDir = usersRoot ? deriveUserWorkDir(ctx.principal.id, usersRoot) : void 0;
@@ -54383,7 +54469,8 @@ function buildHistoryHandlers(deps) {
54383
54469
  limit: args.limit,
54384
54470
  offset: args.offset
54385
54471
  });
54386
- return { response: { type: "history:read", ...res } };
54472
+ const messages = args.slim ? slimHistoryMessages(res.messages) : res.messages;
54473
+ return { response: { type: "history:read", ...res, messages } };
54387
54474
  };
54388
54475
  const subagents = async (frame, _client, ctx) => {
54389
54476
  const args = HistorySubagentsArgs.parse(frame);
@@ -57954,6 +58041,7 @@ var METHOD_GRANT_MAP = {
57954
58041
  "session:subscribe": CAPABILITY_SCOPED,
57955
58042
  "session:unsubscribe": CAPABILITY_SCOPED,
57956
58043
  "session:pin": CAPABILITY_SCOPED,
58044
+ "session:markRead": CAPABILITY_SCOPED,
57957
58045
  "session:reorderPins": ADMIN_ANY,
57958
58046
  // owner 全局操作,无 personaId 维度
57959
58047
  // mirror peer sessions (2026-05-26):owner-only — guest 不应能写/删本机 mirror SessionFile
@@ -39833,6 +39833,11 @@ var SessionFileSchema = external_exports.object({
39833
39833
  // pinned 行手动拖拽顺序;越小越靠前。session:reorderPins 整体重写所有 pinned 的此字段。
39834
39834
  // 缺失时 view-state 排序回退到 pinnedAt desc(旧数据兼容)
39835
39835
  pinSortOrder: external_exports.number().int().nullable().optional(),
39836
+ // daemon 权威的共享未读标记(spec 2026-07-20-shared-session-unread):
39837
+ // session wire status 从 running 跃迁为非 running 时 daemon 置为当时 ISO 时间戳;
39838
+ // session:markRead RPC 或新 turn 开始(→ running)时清为 null。
39839
+ // 所有客户端(desktop / mobile)共享同一份,任一端标读全端清。
39840
+ unreadAt: external_exports.string().min(1).nullable().optional(),
39836
39841
  // 用户在 NewSessionDialog 或 Edit modal 选择的 icon 标识;UI 端做 enum 兜底
39837
39842
  iconKey: external_exports.string().optional(),
39838
39843
  // 由 session:fork 派生的 session 在 create 时记录源 session 的 sessionId(稳定 key,
@@ -40178,7 +40183,13 @@ var SessionListArgs = external_exports.object({
40178
40183
  /** true = 只返回 mirror(originOwnerPrincipalId 非空,即「我在朋友 daemon 上创建」的本机存证)。 */
40179
40184
  originOnly: external_exports.boolean().optional(),
40180
40185
  /** 按 updatedAt desc(list 的既有排序)截断前 N 条。 */
40181
- limit: external_exports.number().int().positive().optional()
40186
+ limit: external_exports.number().int().positive().optional(),
40187
+ /**
40188
+ * 分页起点(2026-07-18 android-tasks-tab 迭代追加):originOnly 过滤后、limit 截断前跳过
40189
+ * 前 N 条。与 limit 组合 = updatedAt desc 序上的一页。响应带 total(过滤后总数,见 handler)
40190
+ * 供客户端判 hasMore。老 daemon 不识别时忽略 → 回全量,客户端按全量兼容。
40191
+ */
40192
+ offset: external_exports.number().int().nonnegative().optional()
40182
40193
  });
40183
40194
  var SessionIdArgs = external_exports.object({ sessionId: external_exports.string().min(1) });
40184
40195
  var SessionUpdateArgs = external_exports.object({
@@ -40279,7 +40290,16 @@ var HistoryReadArgs = external_exports.object({
40279
40290
  sessionId: external_exports.string().min(1),
40280
40291
  limit: external_exports.number().int().positive().max(5e3).optional(),
40281
40292
  /** 不传时走 tail 语义:daemon 返回最后 limit 条(offset = max(0, total - limit)) */
40282
- offset: external_exports.number().int().nonnegative().optional()
40293
+ offset: external_exports.number().int().nonnegative().optional(),
40294
+ /**
40295
+ * 瘦身投影(2026-07-18 android-tasks-tab 迭代,v2 语义):true = daemon 裁掉窄消费面客户端
40296
+ * (mobile)不渲染的大字段(input / output / raw / toolResultExtra / memories /
40297
+ * sourceToolAssistantUUID),meta 注入行 text 截 500 字符。**例外:出错的工具调用全量保留**——
40298
+ * 本页内 error 非空的 tool_result 保留 output,且其同 toolUseId 的 tool_call 保留 input
40299
+ * (用户要能看清「啥错了」;老板 2026-07-18 拍板:普通工具调用手机不渲染,错误全量展示)。
40300
+ * desktop 渲染 tool card 需全文,不用此参数。老 daemon 不识别 → 回全量,客户端向后兼容。
40301
+ */
40302
+ slim: external_exports.boolean().optional()
40283
40303
  });
40284
40304
  var HistorySubagentsArgs = external_exports.object({
40285
40305
  cwd: external_exports.string().min(1),