larkway 0.3.28 → 0.3.30

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/README.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  You @ the bot in a Feishu thread. It runs on your machine — reading your real codebase, executing commands, opening MRs — and posts the result back. You define what the agent knows and what it can do. Larkway just carries the messages.
10
10
 
11
- **Current release: v0.3.28**
11
+ **Current release: v0.3.30**
12
12
 
13
13
  ---
14
14
 
package/README.zh.md CHANGED
@@ -8,7 +8,7 @@
8
8
 
9
9
  你在飞书话题里 @ bot,它在你的机器上运行——读真实代码库、执行命令、开 MR——把结果贴回飞书。你定义 agent 知道什么、能做什么。Larkway 只负责传递消息。
10
10
 
11
- **当前版本:v0.3.28**
11
+ **当前版本:v0.3.30**
12
12
 
13
13
  ---
14
14
 
package/dist/cli/index.js CHANGED
@@ -124829,6 +124829,9 @@ import path from "node:path";
124829
124829
 
124830
124830
  // src/responseSurface.ts
124831
124831
  var NonEmptyString = external_exports.string().trim().min(1);
124832
+ var CardKitMentionUserIdSchema = NonEmptyString.max(128).regex(/^[A-Za-z0-9_:-]+$/, {
124833
+ message: "mention user_id must contain only letters, numbers, underscore, colon, or hyphen"
124834
+ });
124832
124835
  function defaultResponseSurfacePrototypeConfig() {
124833
124836
  return {
124834
124837
  enabled: true,
@@ -124838,6 +124841,7 @@ function defaultResponseSurfacePrototypeConfig() {
124838
124841
  post_outbound_enabled: true,
124839
124842
  cardkit_streaming_enabled: true,
124840
124843
  allow_agent_mentions: true,
124844
+ denied_mention_open_ids: [],
124841
124845
  allowed_mention_open_ids: []
124842
124846
  };
124843
124847
  }
@@ -124850,6 +124854,7 @@ var responseSurfacePrototypeConfigDefaults = () => ({
124850
124854
  post_outbound_enabled: true,
124851
124855
  cardkit_streaming_enabled: true,
124852
124856
  allow_agent_mentions: true,
124857
+ denied_mention_open_ids: [],
124853
124858
  allowed_mention_open_ids: []
124854
124859
  });
124855
124860
  var ResponseSurfaceModeSchema = external_exports.enum(["card", "post", "hybrid"]);
@@ -124862,7 +124867,7 @@ var ResponseSurfaceCapabilitySchema = external_exports.enum([
124862
124867
  "audit"
124863
124868
  ]);
124864
124869
  var MentionTargetSchema = external_exports.object({
124865
- user_id: NonEmptyString.max(128),
124870
+ user_id: CardKitMentionUserIdSchema,
124866
124871
  label: external_exports.string().trim().min(1).max(80).optional()
124867
124872
  });
124868
124873
  var ResponseSurfacePostSchema = external_exports.object({
@@ -124873,7 +124878,7 @@ var ResponseSurfaceCardSchema = external_exports.object({
124873
124878
  capabilities: external_exports.array(ResponseSurfaceCapabilitySchema).max(8).default([])
124874
124879
  });
124875
124880
  var StrictResponseSurfaceStateSchema = external_exports.object({
124876
- mode: ResponseSurfaceModeSchema,
124881
+ mode: ResponseSurfaceModeSchema.optional().default("card"),
124877
124882
  primary: ResponseSurfacePrimarySchema.optional(),
124878
124883
  post: ResponseSurfacePostSchema.optional(),
124879
124884
  card: ResponseSurfaceCardSchema.optional()
@@ -124888,9 +124893,18 @@ var StrictResponseSurfaceStateSchema = external_exports.object({
124888
124893
  });
124889
124894
  var ResponseSurfaceStateSchema = external_exports.preprocess((value) => {
124890
124895
  if (value === void 0) return void 0;
124891
- const result = StrictResponseSurfaceStateSchema.safeParse(value);
124892
- return result.success ? result.data : void 0;
124896
+ return parseResponseSurfaceState(value).state;
124893
124897
  }, StrictResponseSurfaceStateSchema.optional());
124898
+ function parseResponseSurfaceState(value) {
124899
+ const result = StrictResponseSurfaceStateSchema.safeParse(value);
124900
+ if (result.success) return { state: result.data, diagnostics: [] };
124901
+ return {
124902
+ diagnostics: result.error.issues.map((issue) => {
124903
+ const path25 = issue.path.length > 0 ? issue.path.join(".") : "<root>";
124904
+ return `${path25}: ${issue.message}`;
124905
+ })
124906
+ };
124907
+ }
124894
124908
  var ResponseSurfacePrototypeConfigSchema = external_exports.object({
124895
124909
  /**
124896
124910
  * Master gate. Default true makes response surfaces available unless the
@@ -124931,10 +124945,16 @@ var ResponseSurfacePrototypeConfigSchema = external_exports.object({
124931
124945
  */
124932
124946
  allow_agent_mentions: external_exports.boolean().default(true),
124933
124947
  /**
124934
- * Optional target allowlist for real post @ mentions. Empty means the Agent
124935
- * may choose mention targets; non-empty narrows mentions to this exact set.
124948
+ * Optional deny-list for Agent-authored @ mentions. Empty means the Agent
124949
+ * may choose mention targets, except broadcast aliases such as @all.
124936
124950
  * Keep real IDs in private bot config, never in public docs/tests.
124937
124951
  */
124952
+ denied_mention_open_ids: external_exports.array(external_exports.string().min(1)).default([]),
124953
+ /**
124954
+ * Deprecated compatibility field. It is still parsed so older private
124955
+ * configs do not fail, but mention policy is deny-list/default-allow.
124956
+ * Use denied_mention_open_ids for new restrictions.
124957
+ */
124938
124958
  allowed_mention_open_ids: external_exports.array(external_exports.string().min(1)).default([])
124939
124959
  }).default(responseSurfacePrototypeConfigDefaults);
124940
124960
 
package/dist/main.js CHANGED
@@ -115875,6 +115875,10 @@ function resolveAgentWorkspacePath(agentId) {
115875
115875
  function resolveAgentWorkspaceSessionsDir(agentId) {
115876
115876
  return join(resolveAgentWorkspacePath(agentId), "sessions");
115877
115877
  }
115878
+ function resolveAgentSessionPath(agentId, threadId) {
115879
+ assertSafePathSegment("threadId", threadId);
115880
+ return join(resolveAgentWorkspaceSessionsDir(agentId), threadId);
115881
+ }
115878
115882
  function resolveAgentWorkspaceReposDir(agentId) {
115879
115883
  return join(resolveAgentWorkspacePath(agentId), "repos");
115880
115884
  }
@@ -118683,7 +118687,7 @@ function renderStateContract(stateFilePath) {
118683
118687
  "- card_title/card_color: \u517C\u5BB9\u5B57\u6BB5; \u9ED8\u8BA4 CardKit \u4E0D\u6E32\u67D3\u9876\u90E8\u6807\u9898\u8272\u6761,legacy/fallback \u5361\u7247\u8DEF\u5F84\u53EF\u80FD\u4F7F\u7528",
118684
118688
  "- image_blocks: \u53EF\u9009\u56FE\u7247\u9884\u89C8\u5757\u6570\u7EC4,\u6700\u591A 4 \u4E2A\u3002\u6BCF\u9879 `{img_key, alt?, title?, mode?, preview?}`; `img_key` \u5FC5\u987B\u662F\u5DF2\u4E0A\u4F20/\u53EF\u7528\u4E8E\u5361\u7247\u7684 Feishu \u56FE\u7247 key,`alt` \u7701\u7565\u65F6 bridge \u9ED8\u8BA4\u201C\u56FE\u7247\u9884\u89C8\u201D,`mode` \u53EA\u5141\u8BB8 `crop_center`/`fit_horizontal` \u5E76\u6620\u5C04\u5230 Card JSON 2.0 `scale_type`,`preview` \u9ED8\u8BA4 true\u3002bridge \u4E0D\u8D1F\u8D23\u4E0B\u8F7D/\u4E0A\u4F20/\u9009\u62E9\u56FE\u7247;\u8FD9\u4E9B\u7531\u4F60\u7528 lark-cli \u7B49\u5DE5\u5177\u5148\u5B8C\u6210\u3002",
118685
118689
  '- content_blocks: \u53EF\u9009\u6709\u5E8F\u6B63\u6587\u5757\u6570\u7EC4,\u6700\u591A 12 \u4E2A block\u3001\u6700\u591A 4 \u4E2A image block\u3002\u53EA\u652F\u6301\u7A84 union:`{type:"markdown", content}` \u548C `{type:"image", img_key, alt?, title?, mode?, preview?}`;\u4E0D\u652F\u6301 raw card JSON\u3002\u7528\u4E8E\u6B63\u6587\u4E0E\u56FE\u7247\u4EA4\u9519\u6392\u7248,\u4F8B\u5982 markdown -> image -> markdown -> image\u3002\u82E5 `content_blocks` \u975E\u7A7A,bridge \u4EE5\u5B83\u4F5C\u4E3A\u4E3B\u6B63\u6587\u5E76\u5FFD\u7565 `last_message` + `image_blocks` \u7684\u6B63\u6587\u6E32\u67D3,\u907F\u514D\u91CD\u590D;\u82E5\u7701\u7565\u5219\u4FDD\u6301\u65E7 `last_message` + `image_blocks` \u884C\u4E3A\u3002',
118686
- "- response_surface: \u53EF\u9009\u8986\u76D6\u5B57\u6BB5,\u4E3B\u8981\u7528\u4E8E `{post:{mentions:[{user_id,label?}]}}` late peer @\u3002\u9ED8\u8BA4\u53EF\u4E0D\u5199;bridge \u6309 CardKit \u6D41\u5F0F\u5361\u7247\u5904\u7406,\u6700\u7EC8\u6536\u6210\u5E72\u51C0\u603B\u7ED3\u5361\u3002\u65E7 `mode`/`primary` \u4EC5\u517C\u5BB9\u89E3\u6790,\u4E0D\u518D\u9009\u62E9 post-only/hybrid \u4E3B\u54CD\u5E94\u9762\u3002\u9700\u8981\u771F\u5B9E @ \u65F6\u628A\u76EE\u6807\u5199\u5165 `post.mentions`;\u4E0D\u8981\u5199 raw Feishu post/card JSON\u3002",
118690
+ "- response_surface: \u53EF\u9009\u8986\u76D6\u5B57\u6BB5,\u4E3B\u8981\u7528\u4E8E `{post:{mentions:[{user_id,label?}]}}` late peer @\u3002\u9ED8\u8BA4\u53EF\u4E0D\u5199;bridge \u6309 CardKit \u6D41\u5F0F\u5361\u7247\u5904\u7406,\u6700\u7EC8\u6536\u6210\u5E72\u51C0\u603B\u7ED3\u5361\u3002\u65E7 `mode`/`primary` \u4EC5\u517C\u5BB9\u89E3\u6790,\u4E0D\u518D\u9009\u62E9 post-only/hybrid \u4E3B\u54CD\u5E94\u9762\u3002\u8FD9\u91CC\u7684 late @ \u53EA\u662F\u6700\u7EC8\u5361\u7247\u91CC\u7684\u89C6\u89C9\u63D0\u793A;\u9700\u8981 peer bot \u6D88\u8D39\u6B63\u6587\u7684 handoff \u5FC5\u987B\u7531 Agent/\u56E2\u961F\u5DE5\u4F5C\u6D41\u53D1\u9001\u771F\u5B9E Feishu post + at \u6807\u7B7E\u3002\u4E0D\u8981\u5199 raw Feishu post/card JSON\u3002",
118687
118691
  "- scheduled reply / daily social ops review card \u7B49\u9700\u8981\u201C\u5E73\u53F0\u6B63\u6587 + \u5339\u914D\u56FE\u7247\u201D\u540C\u6BB5\u76F8\u90BB\u5C55\u793A\u7684\u573A\u666F,\u5E94\u5148\u53D6\u5F97\u5404\u56FE\u7247 `img_key`,\u518D\u5199 `content_blocks` \u4E3A `\u5E73\u53F0 markdown -> \u5BF9\u5E94 image -> \u4E0B\u4E2A\u5E73\u53F0 markdown -> \u5BF9\u5E94 image`;\u4E0D\u8981\u7528\u5355\u72EC\u8BDD\u9898\u56FE\u7247\u6D88\u606F\u6216\u5C3E\u90E8 `image_blocks` \u4EE3\u66FF\u9A8C\u6536\u9762\u3002",
118688
118692
  "- dev_url / mr_url / \u5176\u4F59\u4E1A\u52A1\u5B57\u6BB5:\u81EA\u7531\u5199\u5165,bridge \u4E0D\u611F\u77E5\u5176\u4E1A\u52A1\u542B\u4E49;\u8981\u8BA9\u8FD0\u8425\u770B\u5230,\u8BF7\u5199\u8FDB last_message",
118689
118693
  "- updated_at: ISO 8601 timestamp",
@@ -118734,6 +118738,9 @@ function renderPeersBlock(peers) {
118734
118738
  "- \u4E0D\u8981\u628A\u540C\u4E00\u4EFB\u52A1\u540C\u65F6\u8F6C\u53D1\u7ED9\u591A\u4E2A peer",
118735
118739
  '- @ peer \u5FC5\u987B\u7528 **post \u6D88\u606F** + at \u6807\u7B7E `{"tag":"at","user_id":"ou_xxx"}`(\u7528\u4E0A\u9762\u7684 open_id),',
118736
118740
  " **\u4E25\u7981\u7528\u7EAF text \u7684 @xxx**(\u7EAF\u6587\u672C @ \u4E0D\u4F1A\u771F\u6B63\u89E6\u8FBE\u5BF9\u65B9 bot)",
118741
+ "- \u53D1\u8D77 peer handoff \u540E,\u5728\u4F60\u7684\u534F\u8C03\u5C42/\u5DE5\u4F5C\u533A\u53F0\u8D26\u8BB0\u5F55 task_id\u3001assignee\u3001\u6765\u6E90\u3001\u671F\u671B\u4EA7\u51FA\u3001deadline \u548C\u5347\u7EA7\u4EBA;\u6CA1\u6709\u4E13\u7528 skill \u65F6\u81F3\u5C11\u5199\u5165\u672C session summary\u3002",
118742
+ "- \u6536\u5230 peer handoff \u540E,\u5148\u7528\u771F\u5B9E post \u8F7B\u91CF ack(\u6536\u5230/\u5F00\u59CB)\u518D\u505A\u957F\u4EFB\u52A1;\u5B8C\u6210\u3001\u5931\u8D25\u6216\u963B\u585E\u65F6\u5FC5\u987B\u7528\u771F\u5B9E post \u56DE\u62A5\u7EC8\u6001,\u4E0D\u8981\u8BA9\u94FE\u8DEF\u9759\u9ED8\u505C\u5728\u4F60\u8FD9\u91CC\u3002",
118743
+ "- \u9ED8\u8BA4 deadline \u53EF\u6309\u56E2\u961F\u5DE5\u4F5C\u6D41\u8BBE\u7F6E(\u5E38\u89C1\u9ED8\u8BA4 15 \u5206\u949F);\u8D85\u65F6\u68C0\u6D4B\u3001\u91CD\u8BD5/\u91CD\u6D3E/\u5347\u7EA7\u5C5E\u4E8E\u534F\u8C03\u5C42 skill/workspace \u903B\u8F91,\u4E0D\u8981\u671F\u5F85 bridge \u66FF\u4F60\u7F16\u6392\u3002",
118737
118744
  "</peer-bots>"
118738
118745
  ];
118739
118746
  }
@@ -119490,6 +119497,9 @@ import path6 from "node:path";
119490
119497
 
119491
119498
  // src/responseSurface.ts
119492
119499
  var NonEmptyString = external_exports.string().trim().min(1);
119500
+ var CardKitMentionUserIdSchema = NonEmptyString.max(128).regex(/^[A-Za-z0-9_:-]+$/, {
119501
+ message: "mention user_id must contain only letters, numbers, underscore, colon, or hyphen"
119502
+ });
119493
119503
  function defaultResponseSurfacePrototypeConfig() {
119494
119504
  return {
119495
119505
  enabled: true,
@@ -119499,6 +119509,7 @@ function defaultResponseSurfacePrototypeConfig() {
119499
119509
  post_outbound_enabled: true,
119500
119510
  cardkit_streaming_enabled: true,
119501
119511
  allow_agent_mentions: true,
119512
+ denied_mention_open_ids: [],
119502
119513
  allowed_mention_open_ids: []
119503
119514
  };
119504
119515
  }
@@ -119511,6 +119522,7 @@ var responseSurfacePrototypeConfigDefaults = () => ({
119511
119522
  post_outbound_enabled: true,
119512
119523
  cardkit_streaming_enabled: true,
119513
119524
  allow_agent_mentions: true,
119525
+ denied_mention_open_ids: [],
119514
119526
  allowed_mention_open_ids: []
119515
119527
  });
119516
119528
  var ResponseSurfaceModeSchema = external_exports.enum(["card", "post", "hybrid"]);
@@ -119523,7 +119535,7 @@ var ResponseSurfaceCapabilitySchema = external_exports.enum([
119523
119535
  "audit"
119524
119536
  ]);
119525
119537
  var MentionTargetSchema = external_exports.object({
119526
- user_id: NonEmptyString.max(128),
119538
+ user_id: CardKitMentionUserIdSchema,
119527
119539
  label: external_exports.string().trim().min(1).max(80).optional()
119528
119540
  });
119529
119541
  var ResponseSurfacePostSchema = external_exports.object({
@@ -119534,7 +119546,7 @@ var ResponseSurfaceCardSchema = external_exports.object({
119534
119546
  capabilities: external_exports.array(ResponseSurfaceCapabilitySchema).max(8).default([])
119535
119547
  });
119536
119548
  var StrictResponseSurfaceStateSchema = external_exports.object({
119537
- mode: ResponseSurfaceModeSchema,
119549
+ mode: ResponseSurfaceModeSchema.optional().default("card"),
119538
119550
  primary: ResponseSurfacePrimarySchema.optional(),
119539
119551
  post: ResponseSurfacePostSchema.optional(),
119540
119552
  card: ResponseSurfaceCardSchema.optional()
@@ -119549,9 +119561,18 @@ var StrictResponseSurfaceStateSchema = external_exports.object({
119549
119561
  });
119550
119562
  var ResponseSurfaceStateSchema = external_exports.preprocess((value) => {
119551
119563
  if (value === void 0) return void 0;
119552
- const result = StrictResponseSurfaceStateSchema.safeParse(value);
119553
- return result.success ? result.data : void 0;
119564
+ return parseResponseSurfaceState(value).state;
119554
119565
  }, StrictResponseSurfaceStateSchema.optional());
119566
+ function parseResponseSurfaceState(value) {
119567
+ const result = StrictResponseSurfaceStateSchema.safeParse(value);
119568
+ if (result.success) return { state: result.data, diagnostics: [] };
119569
+ return {
119570
+ diagnostics: result.error.issues.map((issue) => {
119571
+ const path16 = issue.path.length > 0 ? issue.path.join(".") : "<root>";
119572
+ return `${path16}: ${issue.message}`;
119573
+ })
119574
+ };
119575
+ }
119555
119576
  var ResponseSurfacePrototypeConfigSchema = external_exports.object({
119556
119577
  /**
119557
119578
  * Master gate. Default true makes response surfaces available unless the
@@ -119592,10 +119613,16 @@ var ResponseSurfacePrototypeConfigSchema = external_exports.object({
119592
119613
  */
119593
119614
  allow_agent_mentions: external_exports.boolean().default(true),
119594
119615
  /**
119595
- * Optional target allowlist for real post @ mentions. Empty means the Agent
119596
- * may choose mention targets; non-empty narrows mentions to this exact set.
119616
+ * Optional deny-list for Agent-authored @ mentions. Empty means the Agent
119617
+ * may choose mention targets, except broadcast aliases such as @all.
119597
119618
  * Keep real IDs in private bot config, never in public docs/tests.
119598
119619
  */
119620
+ denied_mention_open_ids: external_exports.array(external_exports.string().min(1)).default([]),
119621
+ /**
119622
+ * Deprecated compatibility field. It is still parsed so older private
119623
+ * configs do not fail, but mention policy is deny-list/default-allow.
119624
+ * Use denied_mention_open_ids for new restrictions.
119625
+ */
119599
119626
  allowed_mention_open_ids: external_exports.array(external_exports.string().min(1)).default([])
119600
119627
  }).default(responseSurfacePrototypeConfigDefaults);
119601
119628
  function isResponseSurfacePrototypeAllowlisted(config, facts) {
@@ -119607,12 +119634,30 @@ function isResponseSurfacePrototypeAllowlisted(config, facts) {
119607
119634
  const threadAllowed = config.allowed_threads.length > 0 && config.allowed_threads.includes(facts.threadId);
119608
119635
  return chatAllowed || threadAllowed;
119609
119636
  }
119610
- function isResponseSurfaceMentionAllowed(config, userId) {
119611
- if (!config?.allow_agent_mentions) return false;
119637
+ function evaluateResponseSurfaceMentionPolicy(config, userId) {
119638
+ if (!config?.allow_agent_mentions) {
119639
+ return {
119640
+ allowed: false,
119641
+ rule: "agent_mentions_disabled",
119642
+ reason: "Agent-authored mentions are disabled by allow_agent_mentions=false."
119643
+ };
119644
+ }
119612
119645
  const normalized = userId.trim().toLowerCase();
119613
- if (normalized === "all" || normalized === "@all") return false;
119614
- if (config.allowed_mention_open_ids.length === 0) return true;
119615
- return config.allowed_mention_open_ids.includes(userId);
119646
+ if (normalized === "all" || normalized === "@all") {
119647
+ return {
119648
+ allowed: false,
119649
+ rule: "broadcast_blocked",
119650
+ reason: "Broadcast mentions such as @all are blocked."
119651
+ };
119652
+ }
119653
+ if (config.denied_mention_open_ids.includes(userId)) {
119654
+ return {
119655
+ allowed: false,
119656
+ rule: "denied_target",
119657
+ reason: "Mention target is denied by denied_mention_open_ids."
119658
+ };
119659
+ }
119660
+ return { allowed: true, rule: "allowed" };
119616
119661
  }
119617
119662
  function shouldProvideResponseSurfacePostClient(config) {
119618
119663
  return !!(config?.enabled && !config.kill_switch && config.post_outbound_enabled);
@@ -119797,14 +119842,19 @@ async function ensureStateFile(worktreePath) {
119797
119842
  await fs3.writeFile(file, JSON.stringify(initial, null, 2), "utf8");
119798
119843
  }
119799
119844
  async function readStateFile(worktreePath) {
119845
+ return (await readStateFileDetailed(worktreePath)).state;
119846
+ }
119847
+ async function readStateFileDetailed(worktreePath) {
119800
119848
  const file = stateFilePathOf(worktreePath);
119801
119849
  let raw;
119802
119850
  try {
119803
119851
  raw = await fs3.readFile(file, "utf8");
119804
119852
  } catch (err) {
119805
- if (err.code === "ENOENT") return null;
119853
+ if (err.code === "ENOENT") {
119854
+ return { state: null, diagnostics: [] };
119855
+ }
119806
119856
  console.warn(`[stateFile] read ${file} failed:`, err);
119807
- return null;
119857
+ return { state: null, diagnostics: [] };
119808
119858
  }
119809
119859
  let parsed;
119810
119860
  try {
@@ -119822,11 +119872,11 @@ async function readStateFile(worktreePath) {
119822
119872
  `[stateFile] ${file} not valid JSON (repair also failed):`,
119823
119873
  err2
119824
119874
  );
119825
- return null;
119875
+ return { state: null, diagnostics: [] };
119826
119876
  }
119827
119877
  } else {
119828
119878
  console.warn(`[stateFile] ${file} not valid JSON:`, err);
119829
- return null;
119879
+ return { state: null, diagnostics: [] };
119830
119880
  }
119831
119881
  }
119832
119882
  const result = StateFileSchema.safeParse(parsed);
@@ -119835,9 +119885,22 @@ async function readStateFile(worktreePath) {
119835
119885
  `[stateFile] ${file} failed schema validation:`,
119836
119886
  result.error.issues
119837
119887
  );
119838
- return null;
119888
+ return { state: null, diagnostics: [] };
119839
119889
  }
119840
- return result.data;
119890
+ const diagnostics = diagnoseStateFile(parsed, result.data);
119891
+ for (const diagnostic of diagnostics) {
119892
+ console.warn(`[stateFile] ${file}: ${diagnostic}`);
119893
+ }
119894
+ return { state: result.data, diagnostics };
119895
+ }
119896
+ function diagnoseStateFile(parsed, state) {
119897
+ if (parsed === null || typeof parsed !== "object") return [];
119898
+ if (!Object.prototype.hasOwnProperty.call(parsed, "response_surface")) return [];
119899
+ const rawSurface = parsed.response_surface;
119900
+ if (rawSurface === void 0 || state.response_surface !== void 0) return [];
119901
+ const parsedSurface = parseResponseSurfaceState(rawSurface);
119902
+ const details = parsedSurface.diagnostics.length > 0 ? parsedSurface.diagnostics.join("; ") : "unknown parse failure";
119903
+ return [`response_surface ignored: ${details}`];
119841
119904
  }
119842
119905
  function tryRepairBareQuotesInLastMessage(raw) {
119843
119906
  const startRe = /"last_message"\s*:\s*"/;
@@ -120660,6 +120723,13 @@ function derivePostIdempotencyKey(input) {
120660
120723
 
120661
120724
  // src/bridge/handler.ts
120662
120725
  var DEFAULT_CARDKIT_RESPONSE_SURFACE_TIMEOUT_MS = 20 * 60 * 1e3;
120726
+ function summarizeMentionPolicyRules(rules) {
120727
+ const counts = /* @__PURE__ */ new Map();
120728
+ for (const rule of rules) {
120729
+ counts.set(rule, (counts.get(rule) ?? 0) + 1);
120730
+ }
120731
+ return Array.from(counts.entries()).map(([rule, count]) => `${rule}=${count}`).join(", ");
120732
+ }
120663
120733
  function execGit(cwd, args) {
120664
120734
  return new Promise((resolve2, reject) => {
120665
120735
  const child = child_process.spawn("git", args, {
@@ -121398,6 +121468,13 @@ var BridgeHandler = class {
121398
121468
  botGitIdentity: this.deps.botConfig?.git_identity,
121399
121469
  gitlabToken: this.deps.gitlabToken
121400
121470
  });
121471
+ if (isAgentWorkspace && handle.pid != null) {
121472
+ const sessionPidFile = path9.join(worktreePath, ".larkway", "runner.pid");
121473
+ void fs6.mkdir(path9.dirname(sessionPidFile), { recursive: true }).then(
121474
+ () => fs6.writeFile(sessionPidFile, JSON.stringify({ pid: handle.pid }), "utf8")
121475
+ ).catch(() => {
121476
+ });
121477
+ }
121401
121478
  let sessionId;
121402
121479
  let trustedAnswerText = "";
121403
121480
  try {
@@ -121415,8 +121492,23 @@ var BridgeHandler = class {
121415
121492
  }
121416
121493
  const result = await handle.done;
121417
121494
  const cardKitTurnTimedOut = cardKitProgress != null && result.exitCode !== 0 && Date.now() - runnerStartedAt >= timeoutMs;
121418
- const rawReportedState = await readStateFile(worktreePath);
121495
+ const reportedStateRead = await readStateFileDetailed(worktreePath);
121496
+ const rawReportedState = reportedStateRead.state;
121419
121497
  const reportedState = rawReportedState?.updated_at != null && rawReportedState.updated_at !== preRunUpdatedAt ? rawReportedState : null;
121498
+ if (reportedStateRead.diagnostics.length > 0 && reportedState !== null) {
121499
+ await recordEvent({
121500
+ status: "running",
121501
+ appendPath: "state \u8BCA\u65AD",
121502
+ reason: reportedStateRead.diagnostics.join("; ")
121503
+ });
121504
+ }
121505
+ if (reportedState === null) {
121506
+ await recordEvent({
121507
+ status: "running",
121508
+ appendPath: "\u672A\u66F4\u65B0 state.json",
121509
+ reason: rawReportedState === null ? "\u672C\u8F6E\u7ED3\u675F\u65F6\u672A\u8BFB\u53D6\u5230 state.json\u3002" : "\u672C\u8F6E state.json \u6CA1\u6709 fresh updated_at\uFF0C\u5DF2\u5FFD\u7565\u65E7\u72B6\u6001\u3002"
121510
+ });
121511
+ }
121420
121512
  const now = Date.now();
121421
121513
  if (sessionId !== void 0 && currentExisting === void 0) {
121422
121514
  await this.deps.sessionStore.put({
@@ -121455,11 +121547,21 @@ var BridgeHandler = class {
121455
121547
  } else if (cardKitTimeoutFailure) {
121456
121548
  success = false;
121457
121549
  failureReason = `agent turn timed out after ${timeoutMs}ms; CardKit running card was finalized as interrupted`;
121550
+ await recordEvent({
121551
+ status: "running",
121552
+ appendPath: "Agent \u8D85\u65F6",
121553
+ reason: failureReason
121554
+ });
121458
121555
  } else if (result.exitCode === 0) {
121459
121556
  success = true;
121460
121557
  } else {
121461
121558
  success = false;
121462
121559
  failureReason = `claude exited ${result.exitCode} \u4E14 bot \u672A\u66F4\u65B0 state.json status \u2014 \u53EF\u80FD\u5D29\u6E83`;
121560
+ await recordEvent({
121561
+ status: "running",
121562
+ appendPath: "Agent \u5F02\u5E38\u9000\u51FA",
121563
+ reason: failureReason
121564
+ });
121463
121565
  }
121464
121566
  const fallbackAnswer = trustedAnswerText.trim() || cardKitProgress?.answerText.trim() || "";
121465
121567
  const cardBody = cardKitTimeoutFailure ? "\u26A0\uFE0F \u672C\u8F6E\u5904\u7406\u8D85\u65F6\uFF0C\u5DF2\u4E2D\u65AD\u3002\u8BF7\u518D @ \u6211\u4E00\u6B21\u91CD\u8BD5\u3002" : reportedState?.last_message ?? (fallbackAnswer ? fallbackAnswer : "\u26A0\uFE0F \u672C\u8F6E\u6CA1\u6709\u62FF\u5230 agent \u7684\u65B0\u56DE\u590D(\u53EF\u80FD\u88AB\u4E2D\u65AD\u6216\u672A\u66F4\u65B0\u72B6\u6001),\u518D @ \u6211\u4E00\u6B21\u91CD\u8BD5\u3002");
@@ -121480,9 +121582,31 @@ var BridgeHandler = class {
121480
121582
  contentBlocks: reportedState?.content_blocks
121481
121583
  };
121482
121584
  if (cardKitProgress) {
121483
- const mentions = (reportedState?.response_surface?.post?.mentions ?? []).filter(
121484
- (mention) => isResponseSurfaceMentionAllowed(prototypeConfig, mention.user_id)
121485
- );
121585
+ const declaredMentions = reportedState?.response_surface?.post?.mentions ?? [];
121586
+ const responseSurfacePostDeclared = reportedState?.response_surface?.post !== void 0;
121587
+ const mentionPolicyResults = declaredMentions.map((mention) => ({
121588
+ mention,
121589
+ policy: evaluateResponseSurfaceMentionPolicy(prototypeConfig, mention.user_id)
121590
+ }));
121591
+ const mentions = mentionPolicyResults.filter(({ policy }) => policy.allowed).map(({ mention }) => mention);
121592
+ const blockedMentionRules = mentionPolicyResults.filter(({ policy }) => !policy.allowed).map(({ policy }) => policy.rule);
121593
+ if (responseSurfacePostDeclared && declaredMentions.length === 0) {
121594
+ const reason = "response_surface.post was declared with an empty mentions array.";
121595
+ console.warn("[bridge.handler] response_surface post has no mentions");
121596
+ await recordEvent({
121597
+ status: "running",
121598
+ appendPath: "mention \u8BCA\u65AD",
121599
+ reason
121600
+ });
121601
+ } else if (declaredMentions.length > mentions.length) {
121602
+ const reason = `response_surface mentions filtered by policy: ${mentions.length}/${declaredMentions.length} allowed; blocked rules: ${summarizeMentionPolicyRules(blockedMentionRules)}.`;
121603
+ console.warn("[bridge.handler] response_surface mention policy filtered targets");
121604
+ await recordEvent({
121605
+ status: "running",
121606
+ appendPath: "mention \u8BCA\u65AD",
121607
+ reason
121608
+ });
121609
+ }
121486
121610
  try {
121487
121611
  await cardKitProgress.finalize({
121488
121612
  title: baseCardPayload.titleOverride,
@@ -121793,7 +121917,7 @@ function isRuntimeEventRecord(value) {
121793
121917
 
121794
121918
  // src/housekeeping/gc.ts
121795
121919
  import { spawn } from "node:child_process";
121796
- import { readFile as readFile4, readdir, stat } from "node:fs/promises";
121920
+ import { readFile as readFile4, readdir, rm, stat } from "node:fs/promises";
121797
121921
  import { join as pathJoin } from "node:path";
121798
121922
  var DEFAULT_SCAN_INTERVAL_MS = 30 * 60 * 1e3;
121799
121923
  var DEFAULT_IDLE_NOTIFY_MS = 4 * 60 * 60 * 1e3;
@@ -121805,12 +121929,20 @@ var Housekeeping = class {
121805
121929
  #idleCleanupMs;
121806
121930
  /** This housekeeping's bot scope — resolves which worktrees dir to sweep. */
121807
121931
  #botId;
121932
+ /**
121933
+ * Bot runtime. "agent_workspace" reclaims per-thread session dirs under
121934
+ * agents/<id>/workspace/sessions/ via rm -rf (they are plain dirs / full
121935
+ * clones, not git worktrees). Anything else (legacy) reclaims git worktrees
121936
+ * under <botId>/worktrees/ via `git worktree remove`.
121937
+ */
121938
+ #runtime;
121808
121939
  /** thread_ids that have already received an idle-notify warn this session */
121809
121940
  #notified = /* @__PURE__ */ new Set();
121810
121941
  #timer;
121811
121942
  constructor(deps, opts) {
121812
121943
  this.#sessionStore = deps.sessionStore;
121813
121944
  this.#botId = deps.botId;
121945
+ this.#runtime = deps.runtime;
121814
121946
  this.#scanIntervalMs = opts?.scanIntervalMs ?? DEFAULT_SCAN_INTERVAL_MS;
121815
121947
  this.#idleNotifyMs = opts?.idleNotifyMs ?? DEFAULT_IDLE_NOTIFY_MS;
121816
121948
  this.#idleCleanupMs = opts?.idleCleanupMs ?? DEFAULT_IDLE_CLEANUP_MS;
@@ -121855,9 +121987,9 @@ var Housekeeping = class {
121855
121987
  if (idleMs >= this.#idleCleanupMs) {
121856
121988
  const idleHours = Math.floor(idleMs / (60 * 60 * 1e3));
121857
121989
  console.warn(
121858
- `[housekeeping] \u8BDD\u9898 ${tid} idle ${idleHours}h+,worktree \u53EF\u6E05\u7406`
121990
+ `[housekeeping] \u8BDD\u9898 ${tid} idle ${idleHours}h+,\u5DE5\u4F5C\u76EE\u5F55\u53EF\u6E05\u7406`
121859
121991
  );
121860
- void cleanupWorktree(tid, record.botId, dryRun);
121992
+ void this.#cleanupThread(tid, record.botId, dryRun);
121861
121993
  this.#notified.delete(tid);
121862
121994
  continue;
121863
121995
  }
@@ -121878,32 +122010,46 @@ var Housekeeping = class {
121878
122010
  * cleanupWorktree (kill PIDs → git worktree remove --force).
121879
122011
  */
121880
122012
  async #sweepOrphans(liveThreadIds, now, dryRun) {
121881
- const worktreesDir = resolveWorktreesDir(this.#botId);
122013
+ const reclaimDir = this.#runtime === "agent_workspace" ? resolveAgentWorkspaceSessionsDir(this.#botId ?? "") : resolveWorktreesDir(this.#botId);
121882
122014
  let dirNames;
121883
122015
  try {
121884
- const entries = await readdir(worktreesDir, { withFileTypes: true });
122016
+ const entries = await readdir(reclaimDir, { withFileTypes: true });
121885
122017
  dirNames = entries.filter((e) => e.isDirectory()).map((e) => e.name);
121886
122018
  } catch (err) {
121887
122019
  if (err.code !== "ENOENT") {
121888
- console.error(`[gc] orphan sweep: cannot read ${worktreesDir}:`, err);
122020
+ console.error(`[gc] orphan sweep: cannot read ${reclaimDir}:`, err);
121889
122021
  }
121890
122022
  return;
121891
122023
  }
121892
122024
  for (const name of selectOrphanWorktreeNames(dirNames, liveThreadIds)) {
121893
- const wtPath = pathJoin(worktreesDir, name);
122025
+ if (name.startsWith("_")) continue;
122026
+ const dirPath = pathJoin(reclaimDir, name);
121894
122027
  let ageMs;
121895
122028
  try {
121896
- ageMs = now - (await stat(wtPath)).mtimeMs;
122029
+ ageMs = now - (await stat(dirPath)).mtimeMs;
121897
122030
  } catch {
121898
122031
  continue;
121899
122032
  }
121900
122033
  if (ageMs < this.#idleCleanupMs) continue;
121901
122034
  const ageHours = Math.floor(ageMs / (60 * 60 * 1e3));
121902
122035
  console.warn(
121903
- `[housekeeping] orphan worktree ${name}(\u65E0 session \u8BB0\u5F55, idle ${ageHours}h+)\u2014 \u6E05\u7406`
122036
+ `[housekeeping] orphan ${name}(\u65E0 session \u8BB0\u5F55, idle ${ageHours}h+)\u2014 \u6E05\u7406`
121904
122037
  );
121905
- await cleanupWorktree(name, this.#botId, dryRun);
122038
+ await this.#cleanupThread(name, this.#botId, dryRun);
122039
+ }
122040
+ }
122041
+ /**
122042
+ * Runtime-aware reclaim of one thread's working dir.
122043
+ * - agent_workspace: rm -rf agents/<id>/workspace/sessions/<tid> (plain dir /
122044
+ * full clones — `git worktree remove` cannot reclaim these).
122045
+ * - legacy: git worktree remove --force <botId>/worktrees/<tid>.
122046
+ * Both kill any lingering runner PIDs first (idle >24h → normally dead).
122047
+ */
122048
+ #cleanupThread(threadId, botId, dryRun) {
122049
+ if (this.#runtime === "agent_workspace") {
122050
+ return cleanupAgentSession(threadId, botId ?? this.#botId, dryRun);
121906
122051
  }
122052
+ return cleanupWorktree(threadId, botId, dryRun);
121907
122053
  }
121908
122054
  };
121909
122055
  function selectOrphanWorktreeNames(dirNames, liveThreadIds) {
@@ -122034,6 +122180,61 @@ async function cleanupWorktree(threadId, botId, dryRun) {
122034
122180
  console.error(`[gc] worktree remove failed for path=${worktreePath}:`, err);
122035
122181
  }
122036
122182
  }
122183
+ function isReclaimableSessionPath(p) {
122184
+ const m = /[/\\]workspace[/\\]sessions[/\\]([^/\\]+)[/\\]?$/.exec(p);
122185
+ if (m === null) return false;
122186
+ const seg = m[1];
122187
+ return seg !== "." && seg !== "..";
122188
+ }
122189
+ async function removeSessionDir(sessionPath, dryRun) {
122190
+ if (!isReclaimableSessionPath(sessionPath)) {
122191
+ console.error(`[gc] refusing to rm -rf non-session path: ${sessionPath}`);
122192
+ return;
122193
+ }
122194
+ if (dryRun) {
122195
+ console.log(`[gc] dry-run: would rm -rf ${sessionPath}`);
122196
+ return;
122197
+ }
122198
+ console.log(`[gc] rm -rf ${sessionPath}`);
122199
+ try {
122200
+ await rm(sessionPath, { recursive: true, force: true });
122201
+ } catch (err) {
122202
+ console.error(`[gc] rm -rf failed for ${sessionPath}:`, err);
122203
+ }
122204
+ }
122205
+ async function cleanupAgentSession(threadId, agentId, dryRun) {
122206
+ if (!agentId) {
122207
+ console.error(
122208
+ `[gc] cleanupAgentSession: missing agentId for thread=${threadId}`
122209
+ );
122210
+ return;
122211
+ }
122212
+ let sessionPath;
122213
+ try {
122214
+ sessionPath = resolveAgentSessionPath(agentId, threadId);
122215
+ } catch (err) {
122216
+ console.error(`[gc] invalid session threadId=${threadId}:`, err);
122217
+ return;
122218
+ }
122219
+ console.log(
122220
+ `[gc] cleanup session thread=${threadId} path=${sessionPath} dryRun=${dryRun}`
122221
+ );
122222
+ let pids;
122223
+ try {
122224
+ pids = await findPidsByWorktree(sessionPath);
122225
+ } catch (err) {
122226
+ console.error(`[gc] pid lookup failed for path=${sessionPath}:`, err);
122227
+ pids = [];
122228
+ }
122229
+ const alivePids = pids.filter(isPidAlive);
122230
+ if (alivePids.length > 0) {
122231
+ console.warn(
122232
+ `[gc] skip live session thread=${threadId} path=${sessionPath}: runner pid(s) [${alivePids.join(", ")}] still alive \u2014 not reclaiming in-flight work`
122233
+ );
122234
+ return;
122235
+ }
122236
+ await removeSessionDir(sessionPath, dryRun);
122237
+ }
122037
122238
 
122038
122239
  // src/config/botLoader.ts
122039
122240
  import { readdir as readdir2, readFile as readFile5 } from "node:fs/promises";
@@ -126008,7 +126209,8 @@ stderr: ${stderr}` : "")
126008
126209
  return {
126009
126210
  events: generateEvents(),
126010
126211
  done,
126011
- kill: doKill
126212
+ kill: doKill,
126213
+ pid: child.pid ?? void 0
126012
126214
  };
126013
126215
  }
126014
126216
  var ClaudeRunner = class {
@@ -126365,7 +126567,8 @@ stderr: ${stderr}` : "")
126365
126567
  return {
126366
126568
  events: generateEvents(),
126367
126569
  done,
126368
- kill: doKill
126570
+ kill: doKill,
126571
+ pid: child.pid ?? void 0
126369
126572
  };
126370
126573
  }
126371
126574
  var CodexRunner = class {
@@ -126839,7 +127042,11 @@ async function runV2Mode({
126839
127042
  await upsertRuntimeEvent(larkwayHome(), bot.id, patch);
126840
127043
  }
126841
127044
  });
126842
- const housekeeping = new Housekeeping({ sessionStore, botId: bot.id });
127045
+ const housekeeping = new Housekeeping({
127046
+ sessionStore,
127047
+ botId: bot.id,
127048
+ runtime: bot.runtime
127049
+ });
126843
127050
  const inst = {
126844
127051
  bot,
126845
127052
  client,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkway",
3
- "version": "0.3.28",
3
+ "version": "0.3.30",
4
4
  "description": "Thin bridge: Feishu thread to local Claude Code CLI",
5
5
  "license": "MIT",
6
6
  "author": "Chuck Wu (chuckwu0)",