@clawos-dev/clawd 0.2.401 → 0.2.402

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
@@ -6692,6 +6692,21 @@ var init_topic = __esm({
6692
6692
  TopicLineMetaSchema = external_exports.object({
6693
6693
  rejectCount: external_exports.number().int().min(0),
6694
6694
  accepted: external_exports.boolean().default(false),
6695
+ /**
6696
+ * 该线在本话题内的**序号**,daemon 按建线先后顺序发,从 1 起。
6697
+ *
6698
+ * 存在的理由只有一个:老板说「第 2 条线」和 Master 说「线2」得指同一条。所以它必须
6699
+ * **稳定且不复用**——删掉第 2 条后新建的线拿 4 而不是 2,否则昨天说的号今天指向别的线,
6700
+ * 序号就没有价值了。发号点见 topic-store.claimLineOrdinal。
6701
+ *
6702
+ * 早先这个号是 UI 从 lineId 字符串里 parse 的(`line-<N>-<slug>`),等于把序号语义
6703
+ * 寄生在 Master 随手起的名字上:Master 用语义命名(`line-channel-fix`)就 parse 不出,
6704
+ * sidebar 整个不显示号。序号的产生端必须是 daemon。
6705
+ *
6706
+ * optional 而非 default:store 不按 schema parse(见 topic-store),存量 lineMeta
6707
+ * 里没有这个字段,读侧一律 `?? null`,回填由 `topic:get` 的发号 pass 幂等补上。
6708
+ */
6709
+ ordinal: external_exports.number().int().min(1).optional(),
6695
6710
  /**
6696
6711
  * Master 在 watcher 判 fail 的情况下仍判合格的次数。
6697
6712
  *
@@ -6716,6 +6731,19 @@ var init_topic = __esm({
6716
6731
  activity: external_exports.array(TopicActivityEntrySchema),
6717
6732
  /** lineId -> 线级元数据 */
6718
6733
  lineMeta: external_exports.record(TopicLineMetaSchema),
6734
+ /**
6735
+ * 线号水位:下一条线该拿的号。发一个加一,**只增不减**。
6736
+ *
6737
+ * 为什么是话题上的一个独立字段,而不是「现有 lineMeta 里的最大号 + 1」现算:现算的水位
6738
+ * 跟着现存线数走,删掉最大号那条线水位就退,下一条新线会**捡回那个号**。老板原话:
6739
+ * 「删了线,已经标记的线的序号不能变吧,要不然留档的记录不都坏了么」——号对不上只是难用,
6740
+ * 号能对上、却对到了另一条线(流水里「#3 判合格」后来指向一条新线)是把留档直接读坏。
6741
+ * 所以不变式有两条:**已发的号永不改**、**号永不复用**,后者必须与现存线数彻底脱钩。
6742
+ *
6743
+ * optional 而非 default:store 不按 schema parse(见 topic-store),存量话题没有这个
6744
+ * 字段,读盘时按「现有最大号 + 1」一次性归一(不能从 1 重开,否则现有的 #1 #2 会被复用)。
6745
+ */
6746
+ nextLineOrdinal: external_exports.number().int().min(1).optional(),
6719
6747
  state: TopicStateSchema,
6720
6748
  createdAt: external_exports.string().min(1),
6721
6749
  updatedAt: external_exports.string().min(1)
@@ -6730,7 +6758,14 @@ var init_topic = __esm({
6730
6758
  acceptance: external_exports.array(external_exports.string()),
6731
6759
  rejectCount: external_exports.number().int().min(0),
6732
6760
  /** 已被 Master 判定合格(`topic:accept`)。UI 的「合格」态靠它,不是猜的 */
6733
- accepted: external_exports.boolean()
6761
+ accepted: external_exports.boolean(),
6762
+ /**
6763
+ * 线号(见 TopicLineMetaSchema.ordinal)。UI 直接渲染,**不再从 lineId 猜**。
6764
+ *
6765
+ * nullable 而非必填:joinLines 是纯函数,拿到的 lineMeta 里可能还没发过号(发号 pass
6766
+ * 在 handler 侧)。走 `topic:get` 出去的线一律已发号,null 只在纯函数被单独调用时出现。
6767
+ */
6768
+ ordinal: external_exports.number().int().min(1).nullable()
6734
6769
  });
6735
6770
  TopicListArgsSchema = external_exports.object({ channelId: external_exports.string().min(1) });
6736
6771
  TopicGetArgsSchema = external_exports.object({
@@ -52657,6 +52692,10 @@ var PersonaDispatchManager = class {
52657
52692
  ...args.taskBrief ? { taskBrief: args.taskBrief } : {},
52658
52693
  ...args.meta ? { meta: args.meta } : {}
52659
52694
  });
52695
+ const lineId = args.meta?.["lineId"];
52696
+ if (typeof lineId === "string" && lineId) {
52697
+ this.deps.onLineClaimed?.({ sourceSessionId: args.sourceSessionId, lineId });
52698
+ }
52660
52699
  return { dispatchId };
52661
52700
  }
52662
52701
  get(dispatchId) {
@@ -52908,19 +52947,40 @@ function createTopicStore(deps) {
52908
52947
  t.updatedAt = new Date(deps.now()).toISOString();
52909
52948
  };
52910
52949
  const lineMetaOf = (t, lineId) => t.lineMeta[lineId] ??= { rejectCount: 0, accepted: false, overrideCount: 0 };
52950
+ const ensureOrdinalWatermark = (t) => {
52951
+ const floor = Object.values(t.lineMeta).reduce((max, m2) => Math.max(max, m2.ordinal ?? 0), 0) + 1;
52952
+ const w2 = t.nextLineOrdinal;
52953
+ const normalized = typeof w2 === "number" && Number.isSafeInteger(w2) && w2 >= floor ? w2 : floor;
52954
+ t.nextLineOrdinal = normalized;
52955
+ return normalized;
52956
+ };
52911
52957
  return {
52912
52958
  /**
52913
- * 读盘后把存量 `state: 'archived'` 归一成 `'done'`。
52959
+ * 读盘后两件归一,**落盘口径不同**:一只改内存(下一次任何写操作自然带出去),
52960
+ * 二补过就立刻回写。理由见各自那段。
52961
+ *
52962
+ * 一、存量 `state: 'archived'` 归成 `'done'`。归档已改由该话题 Master 会话的 `archivedAt`
52963
+ * 承载(spec 2026-08-25-topic-archive-and-pin),`TOPIC_STATES` 随之退掉那一档。json-store
52964
+ * 读盘本身不过 zod,但 `topic:list` 的响应要过 `TopicListResultSchema.parse` —— 留着这个
52965
+ * 值会让**整个频道**的话题列表 parse 失败。
52914
52966
  *
52915
- * 归档已改由该话题 Master 会话的 `archivedAt` 承载(spec 2026-08-25-topic-archive-and-pin),
52916
- * `TOPIC_STATES` 随之退掉那一档。json-store 读盘本身不过 zod,但 `topic:list` 的响应要过
52917
- * `TopicListResultSchema.parse` —— 留着这个值会让**整个频道**的话题列表 parse 失败。
52967
+ * 二、补/纠线号水位。放在读盘而不是发号时:水位一旦落后于已发出的号,下一条新线就会捡回
52968
+ * 一个已经写进流水的号,那是「留档被读坏」而不是「少个号」,越早接上越好。
52969
+ *
52970
+ * **补过就立刻固化**(这一条与 archived 那条不同,不能只留在内存里):水位不落盘的话,
52971
+ * 下次启动还得靠 lineMeta 重算,而 lineMeta 一旦丢了最大号那条,重算出来的水位就退,
52972
+ * 退了就把已经发出去的号又发一遍。不变式不能挂在「lineMeta 一直完好」这个外部事实上。
52973
+ * 水位已经合法的话题不算改动,不会因此每次启动都写一次盘。
52918
52974
  */
52919
52975
  async load() {
52920
52976
  await file.load();
52977
+ let normalized = false;
52921
52978
  for (const t of file.all()) {
52922
52979
  if (t.state === "archived") t.state = "done";
52980
+ const before = t.nextLineOrdinal;
52981
+ if (ensureOrdinalWatermark(t) !== before) normalized = true;
52923
52982
  }
52983
+ if (normalized) file.scheduleFlush();
52924
52984
  },
52925
52985
  flushNow: file.flushNow,
52926
52986
  get,
@@ -52938,6 +52998,8 @@ function createTopicStore(deps) {
52938
52998
  acceptanceCriteria: [],
52939
52999
  activity: [],
52940
53000
  lineMeta: {},
53001
+ // 新话题的第一条线拿 1
53002
+ nextLineOrdinal: 1,
52941
53003
  state: "planning",
52942
53004
  createdAt: iso,
52943
53005
  updatedAt: iso
@@ -52974,6 +53036,19 @@ function createTopicStore(deps) {
52974
53036
  file.scheduleFlush();
52975
53037
  return t;
52976
53038
  },
53039
+ claimLineOrdinal(topicId, lineId) {
53040
+ const t = get(topicId);
53041
+ if (!t) return void 0;
53042
+ const existing = t.lineMeta[lineId]?.ordinal;
53043
+ if (existing !== void 0) return existing;
53044
+ const ordinal = ensureOrdinalWatermark(t);
53045
+ t.nextLineOrdinal = ordinal + 1;
53046
+ const meta = lineMetaOf(t, lineId);
53047
+ meta.ordinal = ordinal;
53048
+ touch(t);
53049
+ file.scheduleFlush();
53050
+ return ordinal;
53051
+ },
52977
53052
  bumpReject(topicId, lineId) {
52978
53053
  const t = get(topicId);
52979
53054
  if (!t) return void 0;
@@ -62113,7 +62188,7 @@ function computeMethodAccess(args) {
62113
62188
  }
62114
62189
 
62115
62190
  // src/version.ts
62116
- var version = "0.2.401".length > 0 ? "0.2.401" : "dev";
62191
+ var version = "0.2.402".length > 0 ? "0.2.402" : "dev";
62117
62192
 
62118
62193
  // src/cli-probe/probe.ts
62119
62194
  var fs66 = __toESM(require("fs"), 1);
@@ -64322,7 +64397,9 @@ function joinLines(records, lineMeta) {
64322
64397
  status: r.status,
64323
64398
  acceptance: Array.isArray(meta.acceptance) ? meta.acceptance.map(String) : [],
64324
64399
  rejectCount: lineMeta[meta.lineId]?.rejectCount ?? 0,
64325
- accepted: lineMeta[meta.lineId]?.accepted ?? false
64400
+ accepted: lineMeta[meta.lineId]?.accepted ?? false,
64401
+ // 号只读不发:这里是纯函数,发号在 linesOf 那一趟(store 是唯一发号点)
64402
+ ordinal: lineMeta[meta.lineId]?.ordinal ?? null
64326
64403
  });
64327
64404
  }
64328
64405
  return out;
@@ -64333,7 +64410,15 @@ function buildTopicHandlers(deps) {
64333
64410
  if (!t) throw new ClawdError(ERROR_CODES.INVALID_PARAM, `topic not found: ${topicId}`);
64334
64411
  return t;
64335
64412
  };
64336
- const linesOf = (t) => joinLines(deps.dispatchStore.list({ sourceSessionId: t.masterSessionId }), t.lineMeta);
64413
+ const linesOf = (t) => {
64414
+ const records = deps.dispatchStore.list({ sourceSessionId: t.masterSessionId });
64415
+ const inCreationOrder = records.slice().sort((a, b2) => a.createdAt < b2.createdAt ? -1 : a.createdAt > b2.createdAt ? 1 : 0);
64416
+ for (const r of inCreationOrder) {
64417
+ const lineId = r.meta?.lineId;
64418
+ if (typeof lineId === "string" && lineId) deps.topicStore.claimLineOrdinal(t.id, lineId);
64419
+ }
64420
+ return joinLines(records, t.lineMeta);
64421
+ };
64337
64422
  return {
64338
64423
  "topic:list": async (frame) => {
64339
64424
  const { channelId } = TopicListArgsSchema.parse(frame);
@@ -66089,7 +66174,13 @@ async function startDaemon(config) {
66089
66174
  genId: () => v4_default(),
66090
66175
  store: dispatchStore,
66091
66176
  now: () => Date.now(),
66092
- onCompleted: (sid) => deliverPendingRef?.(sid)
66177
+ onCompleted: (sid) => deliverPendingRef?.(sid),
66178
+ // 建线即发号:dispatch 的 sourceSessionId 就是该话题的 Master session(1:1),
66179
+ // 反查得到话题后按建线先后领一个稳定的线号。派的不是话题线(反查不到话题)时 no-op。
66180
+ onLineClaimed: ({ sourceSessionId, lineId }) => {
66181
+ const topic = topicStore.getByMasterSession(sourceSessionId);
66182
+ if (topic) topicStore.claimLineOrdinal(topic.id, lineId);
66183
+ }
66093
66184
  });
66094
66185
  const here = typeof __dirname === "string" ? __dirname : import_node_path71.default.dirname((0, import_node_url4.fileURLToPath)(import_meta7.url));
66095
66186
  const mcpConfigs = [];
@@ -21880,6 +21880,21 @@ var TopicActivityEntrySchema = external_exports.object({
21880
21880
  var TopicLineMetaSchema = external_exports.object({
21881
21881
  rejectCount: external_exports.number().int().min(0),
21882
21882
  accepted: external_exports.boolean().default(false),
21883
+ /**
21884
+ * 该线在本话题内的**序号**,daemon 按建线先后顺序发,从 1 起。
21885
+ *
21886
+ * 存在的理由只有一个:老板说「第 2 条线」和 Master 说「线2」得指同一条。所以它必须
21887
+ * **稳定且不复用**——删掉第 2 条后新建的线拿 4 而不是 2,否则昨天说的号今天指向别的线,
21888
+ * 序号就没有价值了。发号点见 topic-store.claimLineOrdinal。
21889
+ *
21890
+ * 早先这个号是 UI 从 lineId 字符串里 parse 的(`line-<N>-<slug>`),等于把序号语义
21891
+ * 寄生在 Master 随手起的名字上:Master 用语义命名(`line-channel-fix`)就 parse 不出,
21892
+ * sidebar 整个不显示号。序号的产生端必须是 daemon。
21893
+ *
21894
+ * optional 而非 default:store 不按 schema parse(见 topic-store),存量 lineMeta
21895
+ * 里没有这个字段,读侧一律 `?? null`,回填由 `topic:get` 的发号 pass 幂等补上。
21896
+ */
21897
+ ordinal: external_exports.number().int().min(1).optional(),
21883
21898
  /**
21884
21899
  * Master 在 watcher 判 fail 的情况下仍判合格的次数。
21885
21900
  *
@@ -21904,6 +21919,19 @@ var TopicSchema = external_exports.object({
21904
21919
  activity: external_exports.array(TopicActivityEntrySchema),
21905
21920
  /** lineId -> 线级元数据 */
21906
21921
  lineMeta: external_exports.record(TopicLineMetaSchema),
21922
+ /**
21923
+ * 线号水位:下一条线该拿的号。发一个加一,**只增不减**。
21924
+ *
21925
+ * 为什么是话题上的一个独立字段,而不是「现有 lineMeta 里的最大号 + 1」现算:现算的水位
21926
+ * 跟着现存线数走,删掉最大号那条线水位就退,下一条新线会**捡回那个号**。老板原话:
21927
+ * 「删了线,已经标记的线的序号不能变吧,要不然留档的记录不都坏了么」——号对不上只是难用,
21928
+ * 号能对上、却对到了另一条线(流水里「#3 判合格」后来指向一条新线)是把留档直接读坏。
21929
+ * 所以不变式有两条:**已发的号永不改**、**号永不复用**,后者必须与现存线数彻底脱钩。
21930
+ *
21931
+ * optional 而非 default:store 不按 schema parse(见 topic-store),存量话题没有这个
21932
+ * 字段,读盘时按「现有最大号 + 1」一次性归一(不能从 1 重开,否则现有的 #1 #2 会被复用)。
21933
+ */
21934
+ nextLineOrdinal: external_exports.number().int().min(1).optional(),
21907
21935
  state: TopicStateSchema,
21908
21936
  createdAt: external_exports.string().min(1),
21909
21937
  updatedAt: external_exports.string().min(1)
@@ -21918,7 +21946,14 @@ var TopicLineViewSchema = external_exports.object({
21918
21946
  acceptance: external_exports.array(external_exports.string()),
21919
21947
  rejectCount: external_exports.number().int().min(0),
21920
21948
  /** 已被 Master 判定合格(`topic:accept`)。UI 的「合格」态靠它,不是猜的 */
21921
- accepted: external_exports.boolean()
21949
+ accepted: external_exports.boolean(),
21950
+ /**
21951
+ * 线号(见 TopicLineMetaSchema.ordinal)。UI 直接渲染,**不再从 lineId 猜**。
21952
+ *
21953
+ * nullable 而非必填:joinLines 是纯函数,拿到的 lineMeta 里可能还没发过号(发号 pass
21954
+ * 在 handler 侧)。走 `topic:get` 出去的线一律已发号,null 只在纯函数被单独调用时出现。
21955
+ */
21956
+ ordinal: external_exports.number().int().min(1).nullable()
21922
21957
  });
21923
21958
  var TopicListArgsSchema = external_exports.object({ channelId: external_exports.string().min(1) });
21924
21959
  var TopicGetArgsSchema = external_exports.object({