@clawos-dev/clawd 0.2.29 → 0.2.31-beta.46.76a266e

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/cli.cjs +158 -112
  2. package/package.json +1 -1
package/dist/cli.cjs CHANGED
@@ -8583,7 +8583,7 @@ var init_zod = __esm({
8583
8583
  });
8584
8584
 
8585
8585
  // ../protocol/src/persona-schemas.ts
8586
- var PersonaTokenEntrySchema, PersonaFileSchema, PersonaHelloFrameSchema, PersonaSendFrameSchema, PersonaResetFrameSchema, PersonaPingFrameSchema, PersonaReadyFrameSchema, PersonaHistoryEventFrameSchema, PersonaHistoryDoneFrameSchema, PersonaEventFrameSchema, PersonaErrorFrameSchema, PersonaPongFrameSchema, PersonaClientFrameSchema, PersonaServerFrameSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema, PersonaIssueTokenArgsSchema, PersonaRevokeTokenArgsSchema, PersonaAppendOwnerMessageArgsSchema;
8586
+ var PersonaTokenEntrySchema, PersonaFileSchema, PersonaCreateArgsSchema, PersonaIdArgsSchema, PersonaUpdateArgsSchema, PersonaIssueTokenArgsSchema, PersonaRevokeTokenArgsSchema, PersonaAppendOwnerMessageArgsSchema;
8587
8587
  var init_persona_schemas = __esm({
8588
8588
  "../protocol/src/persona-schemas.ts"() {
8589
8589
  "use strict";
@@ -8602,66 +8602,6 @@ var init_persona_schemas = __esm({
8602
8602
  createdAt: external_exports.number(),
8603
8603
  updatedAt: external_exports.number()
8604
8604
  }).strict();
8605
- PersonaHelloFrameSchema = external_exports.object({
8606
- kind: external_exports.literal("persona-hello"),
8607
- token: external_exports.string().min(1)
8608
- });
8609
- PersonaSendFrameSchema = external_exports.object({
8610
- kind: external_exports.literal("send"),
8611
- text: external_exports.string()
8612
- });
8613
- PersonaResetFrameSchema = external_exports.object({
8614
- kind: external_exports.literal("reset")
8615
- });
8616
- PersonaPingFrameSchema = external_exports.object({
8617
- kind: external_exports.literal("ping")
8618
- });
8619
- PersonaReadyFrameSchema = external_exports.object({
8620
- kind: external_exports.literal("persona-ready"),
8621
- subSessionId: external_exports.string(),
8622
- personaLabel: external_exports.string(),
8623
- cwd: external_exports.string(),
8624
- model: external_exports.string().optional()
8625
- });
8626
- PersonaHistoryEventFrameSchema = external_exports.object({
8627
- kind: external_exports.literal("history-event"),
8628
- event: external_exports.unknown()
8629
- });
8630
- PersonaHistoryDoneFrameSchema = external_exports.object({
8631
- kind: external_exports.literal("history-done")
8632
- });
8633
- PersonaEventFrameSchema = external_exports.object({
8634
- kind: external_exports.literal("event"),
8635
- event: external_exports.unknown()
8636
- });
8637
- PersonaErrorFrameSchema = external_exports.object({
8638
- kind: external_exports.literal("error"),
8639
- code: external_exports.enum([
8640
- "TOKEN_INVALID",
8641
- "TOKEN_REVOKED",
8642
- "PERSONA_DELETED",
8643
- "PERSONA_NOT_PUBLIC",
8644
- "INTERNAL"
8645
- ]),
8646
- message: external_exports.string()
8647
- });
8648
- PersonaPongFrameSchema = external_exports.object({
8649
- kind: external_exports.literal("pong")
8650
- });
8651
- PersonaClientFrameSchema = external_exports.discriminatedUnion("kind", [
8652
- PersonaHelloFrameSchema,
8653
- PersonaSendFrameSchema,
8654
- PersonaResetFrameSchema,
8655
- PersonaPingFrameSchema
8656
- ]);
8657
- PersonaServerFrameSchema = external_exports.discriminatedUnion("kind", [
8658
- PersonaReadyFrameSchema,
8659
- PersonaHistoryEventFrameSchema,
8660
- PersonaHistoryDoneFrameSchema,
8661
- PersonaEventFrameSchema,
8662
- PersonaErrorFrameSchema,
8663
- PersonaPongFrameSchema
8664
- ]);
8665
8605
  PersonaCreateArgsSchema = external_exports.object({
8666
8606
  label: external_exports.string().min(1),
8667
8607
  personality: external_exports.string(),
@@ -16644,8 +16584,9 @@ var SessionManager = class {
16644
16584
  // 不进 SessionFile schema(避免破坏现有 zod parse),仅运行时缓存
16645
16585
  subSessionMetaBySid = /* @__PURE__ */ new Map();
16646
16586
  // persona-bound transport 订阅器:sessionId → Set<listener>。
16647
- // 仅推 reducer 产出的 'session:event' 帧里的 ParsedEvent,其他帧(status / info / cleared)
16648
- // 由调用方按需自行处理(persona-bound 简化协议只暴露 event 流,不需要这些)
16587
+ // 透传 owner 路径白名单 EventFrame(routeFromRunner 决定哪些 type 进入),listener 端按
16588
+ // `frame.type` narrow 出 'session:event'(ParsedEvent)/ 'session:status'(procAlive 同步)等。
16589
+ // 这样 owner / listener 共用同一组事件字面量,新增跨端信号时只需在 fan-out 白名单里加 type。
16649
16590
  eventSubscribers = /* @__PURE__ */ new Map();
16650
16591
  // 按 agentId 拿对应的 SessionStore;persona-* agentId 使用 personaRoot 路径。
16651
16592
  // 'default' 直接复用 deps.store;其它 agentId 第一次访问时按需创建并缓存。
@@ -16700,15 +16641,14 @@ var SessionManager = class {
16700
16641
  routeFromRunner(frame, target) {
16701
16642
  const compressed = compressFrameForWire(frame);
16702
16643
  if (!compressed) return;
16703
- if (compressed.type === "session:event") {
16644
+ if (compressed.type === "session:event" || compressed.type === "session:status") {
16704
16645
  const sid = compressed.sessionId;
16705
- const ev = compressed.event;
16706
- if (sid && ev) {
16646
+ if (sid) {
16707
16647
  const subs = this.eventSubscribers.get(sid);
16708
16648
  if (subs && subs.size > 0) {
16709
16649
  for (const fn of subs) {
16710
16650
  try {
16711
- fn(ev);
16651
+ fn(compressed);
16712
16652
  } catch {
16713
16653
  }
16714
16654
  }
@@ -17153,6 +17093,20 @@ var SessionManager = class {
17153
17093
  getActive(sessionId) {
17154
17094
  return this.runners.get(sessionId);
17155
17095
  }
17096
+ /**
17097
+ * 拿当前 sub-session 的 wire 形态 status(已 compress:内部 reducer 'spawning' / 'running-idle' /
17098
+ * 'stopping' 等都压平)。无 runner 返回 'idle'(没 spawn 过)。
17099
+ *
17100
+ * 用途:listener 端 subscribe 时补推一次当前 status —— sessionManager.subscribe 注册的
17101
+ * callback 只接未来 emit,不重放当前 status;不补推则 listener 进入时如果 sub-session
17102
+ * 已 running,永远拿不到 procAlive 信号。owner 路径在 LocalWsServer.onSubscribe hook
17103
+ * 重放 pendingQuestions(同一思路)。
17104
+ */
17105
+ getCurrentStatus(sessionId) {
17106
+ const runner = this.runners.get(sessionId);
17107
+ if (!runner) return "idle";
17108
+ return compressStatus(runner.getState().status);
17109
+ }
17156
17110
  // 给 observer 用:保证有 runner 但不 spawn(仅创建 state 容器)
17157
17111
  ensureSession(file) {
17158
17112
  let r = this.runners.get(file.sessionId);
@@ -17239,7 +17193,35 @@ var SessionManager = class {
17239
17193
  return runner.getState().buffer.map((e) => e.event);
17240
17194
  }
17241
17195
  /**
17242
- * persona-bound transport 用:订阅 sub-session 实时 ParsedEvent。
17196
+ * persona-bound transport 用:基于 readHistoryEvents buffer 切片视图,支持分页。
17197
+ *
17198
+ * Why a new method instead of reusing `history:read` RPC handler:
17199
+ * - `history:read` 走 HandlerDeps.history.read(),读 CC 写盘的 jsonl 文件
17200
+ * - 本方法走 reducer buffer(与实时 `subscribe` 同一数据源)
17201
+ * 两条路径数据形态、时序、字段不同,不能共享 helper。persona-bound 客户端必须
17202
+ * 走 buffer 路径,才能保证首屏回放和实时帧无 gap、无重复。
17203
+ *
17204
+ * Why not 改 readHistoryEvents 加 limit/offset:现有 readHistoryEvents 的语义是
17205
+ * "返回 buffer 全部",被其它路径(rewind uuid 转译等)依赖;保留它不动,新加分页方法。
17206
+ *
17207
+ * 切片语义:clip 安全边界,response.offset 保留请求值便于客户端识别越界请求。
17208
+ * - offset >= total → events: [], offset: 请求值, total: 当前 buffer 长度
17209
+ * - offset + limit > total → 返回 tail [offset, total),长度 = total - offset
17210
+ * - 正常 → 返回 [offset, offset + limit)
17211
+ *
17212
+ * SESSION_NOT_FOUND 行为继承 readHistoryEvents(store.read 返回 null 即抛)。
17213
+ */
17214
+ readHistoryPage(sessionId, agentId, limit, offset) {
17215
+ const all = this.readHistoryEvents(sessionId, agentId);
17216
+ const total = all.length;
17217
+ const start = Math.min(offset, total);
17218
+ const end = Math.min(start + limit, total);
17219
+ return { events: all.slice(start, end), offset, total };
17220
+ }
17221
+ /**
17222
+ * persona-bound transport 用:订阅 sub-session 的 owner-side EventFrame(白名单透传)。
17223
+ * 与 owner 路径共享同一组字面量,listener callback 拿到的 frame.type 当前可能是
17224
+ * 'session:event'(ParsedEvent 流)或 'session:status'(procAlive)。
17243
17225
  * 返回 unsubscribe;不破坏现有 wire 广播路径——routeFromRunner 同时 fan-out 到
17244
17226
  * eventSubscribers 和 deps.broadcastFrame
17245
17227
  */
@@ -18410,6 +18392,11 @@ var PersonaBoundHandler = class {
18410
18392
  this.deps.logger.warn(`persona ws send failed: ${err.message}`);
18411
18393
  }
18412
18394
  };
18395
+ const sendError = (requestId, code, message) => {
18396
+ const errFrame = { type: "error", code, message };
18397
+ if (requestId) errFrame.requestId = requestId;
18398
+ send(errFrame);
18399
+ };
18413
18400
  ws.on("message", (raw) => {
18414
18401
  let parsedRaw;
18415
18402
  try {
@@ -18418,18 +18405,19 @@ var PersonaBoundHandler = class {
18418
18405
  ws.close(4400, "PROTOCOL_VIOLATION");
18419
18406
  return;
18420
18407
  }
18421
- const parseResult = PersonaClientFrameSchema.safeParse(parsedRaw);
18422
- if (!parseResult.success) {
18408
+ if (typeof parsedRaw !== "object" || parsedRaw === null) {
18423
18409
  ws.close(4400, "PROTOCOL_VIOLATION");
18424
18410
  return;
18425
18411
  }
18426
- const frame = parseResult.data;
18412
+ const frame = parsedRaw;
18427
18413
  if (!scope) {
18428
- if (frame.kind !== "persona-hello") {
18414
+ const authParse = AuthRequestFrameSchema.safeParse(frame);
18415
+ if (!authParse.success) {
18429
18416
  ws.close(4400, "PROTOCOL_VIOLATION");
18430
18417
  return;
18431
18418
  }
18432
- const verify = this.deps.registry.verifyToken(personaId, frame.token);
18419
+ const { token } = authParse.data;
18420
+ const verify = this.deps.registry.verifyToken(personaId, token);
18433
18421
  if (!verify.ok) {
18434
18422
  const code = verify.code === "PERSONA_DELETED" ? 4404 : verify.code === "PERSONA_NOT_PUBLIC" ? 4403 : 4401;
18435
18423
  ws.close(code, verify.code);
@@ -18444,7 +18432,7 @@ var PersonaBoundHandler = class {
18444
18432
  try {
18445
18433
  const { sessionFile } = this.deps.personaManager.getOrCreateSubSession(
18446
18434
  personaId,
18447
- frame.token
18435
+ token
18448
18436
  );
18449
18437
  subSessionFile = sessionFile;
18450
18438
  } catch (err) {
@@ -18455,69 +18443,127 @@ var PersonaBoundHandler = class {
18455
18443
  return;
18456
18444
  }
18457
18445
  scope = { personaId, subSessionId: subSessionFile.sessionId };
18446
+ send({ type: "auth:ok" });
18458
18447
  send({
18459
- kind: "persona-ready",
18460
- subSessionId: subSessionFile.sessionId,
18461
- personaLabel: persona.label,
18448
+ type: "session:info",
18449
+ sessionId: subSessionFile.sessionId,
18450
+ label: persona.label,
18462
18451
  cwd: this.deps.personaStore.personaDirPath(personaId),
18463
- model: persona.model
18452
+ ...persona.model ? { model: persona.model } : {}
18464
18453
  });
18465
- unsubscribe = this.deps.sessionManager.subscribe(
18466
- subSessionFile.sessionId,
18467
- personaId,
18468
- (event) => {
18469
- send({ kind: "event", event });
18470
- }
18471
- );
18472
- try {
18473
- const history = this.deps.sessionManager.readHistoryEvents(
18474
- subSessionFile.sessionId,
18475
- personaId
18476
- );
18477
- for (const event of history) {
18478
- send({ kind: "history-event", event });
18479
- }
18480
- } catch (err) {
18481
- this.deps.logger.warn(
18482
- `persona history read failed: ${err.message}`
18483
- );
18484
- }
18485
- send({ kind: "history-done" });
18486
18454
  return;
18487
18455
  }
18488
- switch (frame.kind) {
18489
- case "persona-hello":
18490
- ws.close(4400, "PROTOCOL_VIOLATION");
18456
+ const type = frame.type;
18457
+ const requestId = typeof frame.requestId === "string" ? frame.requestId : void 0;
18458
+ if (type === "auth") {
18459
+ ws.close(4400, "PROTOCOL_VIOLATION");
18460
+ return;
18461
+ }
18462
+ if (type === "ping") {
18463
+ send({ type: "pong", at: Date.now() });
18464
+ return;
18465
+ }
18466
+ const requireScopedSession = () => {
18467
+ if (frame.sessionId !== scope.subSessionId) {
18468
+ sendError(requestId, "FORBIDDEN", "sessionId out of scope");
18469
+ return false;
18470
+ }
18471
+ return true;
18472
+ };
18473
+ switch (type) {
18474
+ case "session:subscribe": {
18475
+ if (!requireScopedSession()) return;
18476
+ if (unsubscribe) unsubscribe();
18477
+ unsubscribe = this.deps.sessionManager.subscribe(
18478
+ scope.subSessionId,
18479
+ scope.personaId,
18480
+ (eventFrame) => send(eventFrame)
18481
+ );
18482
+ if (requestId)
18483
+ send({ type: "subscribed", requestId, sessionId: scope.subSessionId });
18484
+ const currentStatus = this.deps.sessionManager.getCurrentStatus(scope.subSessionId);
18485
+ send({
18486
+ type: "session:status",
18487
+ sessionId: scope.subSessionId,
18488
+ status: currentStatus
18489
+ });
18491
18490
  return;
18492
- case "send": {
18491
+ }
18492
+ case "session:unsubscribe": {
18493
+ if (!requireScopedSession()) return;
18494
+ if (unsubscribe) {
18495
+ unsubscribe();
18496
+ unsubscribe = null;
18497
+ }
18498
+ if (requestId) send({ type: "unsubscribed", requestId, sessionId: scope.subSessionId });
18499
+ return;
18500
+ }
18501
+ case "session:send": {
18502
+ if (!requireScopedSession()) return;
18503
+ const text = frame.text;
18504
+ if (typeof text !== "string") {
18505
+ sendError(requestId, "VALIDATION_ERROR", "text must be string");
18506
+ return;
18507
+ }
18493
18508
  try {
18494
18509
  this.deps.sessionManager.sendForAgent({
18495
18510
  sessionId: scope.subSessionId,
18496
18511
  agentId: scope.personaId,
18497
- text: frame.text
18512
+ text
18498
18513
  });
18514
+ if (requestId) send({ type: "session:send", ok: true, requestId });
18499
18515
  } catch (err) {
18500
- this.deps.logger.warn(
18501
- `persona sendForAgent failed: ${err.message}`
18502
- );
18516
+ const e = err;
18517
+ sendError(requestId, e.code ?? "INTERNAL", e.message ?? String(err));
18503
18518
  }
18504
18519
  return;
18505
18520
  }
18506
- case "reset": {
18521
+ case "session:new": {
18522
+ if (!requireScopedSession()) return;
18507
18523
  try {
18508
18524
  this.deps.sessionManager.resetForAgent({
18509
18525
  sessionId: scope.subSessionId,
18510
18526
  agentId: scope.personaId
18511
18527
  });
18528
+ if (requestId) send({ type: "session:info", sessionId: scope.subSessionId, requestId });
18512
18529
  } catch (err) {
18513
- this.deps.logger.warn(
18514
- `persona resetForAgent failed: ${err.message}`
18530
+ const e = err;
18531
+ sendError(requestId, e.code ?? "INTERNAL", e.message ?? String(err));
18532
+ }
18533
+ return;
18534
+ }
18535
+ case "history:read": {
18536
+ if (!requireScopedSession()) return;
18537
+ const limit = typeof frame.limit === "number" && frame.limit > 0 ? frame.limit : 50;
18538
+ const offset = typeof frame.offset === "number" && frame.offset >= 0 ? frame.offset : 0;
18539
+ try {
18540
+ const page = this.deps.sessionManager.readHistoryPage(
18541
+ scope.subSessionId,
18542
+ scope.personaId,
18543
+ limit,
18544
+ offset
18515
18545
  );
18546
+ if (requestId) {
18547
+ send({
18548
+ type: "history:read",
18549
+ requestId,
18550
+ events: page.events,
18551
+ offset,
18552
+ total: page.total
18553
+ });
18554
+ }
18555
+ } catch (err) {
18556
+ const e = err;
18557
+ sendError(requestId, e.code ?? "INTERNAL", e.message ?? String(err));
18516
18558
  }
18517
18559
  return;
18518
18560
  }
18519
- case "ping":
18520
- send({ kind: "pong" });
18561
+ default:
18562
+ sendError(
18563
+ requestId,
18564
+ "METHOD_NOT_ALLOWED",
18565
+ `${typeof type === "string" ? type : "unknown"} not allowed for listener`
18566
+ );
18521
18567
  return;
18522
18568
  }
18523
18569
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@clawos-dev/clawd",
3
- "version": "0.2.29",
3
+ "version": "0.2.31-beta.46.76a266e",
4
4
  "description": "Standalone clawd daemon — Claude Code (and future Codex) session server over WebSocket",
5
5
  "type": "module",
6
6
  "license": "MIT",