larkway 0.3.23 → 0.3.25

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.23**
11
+ **Current release: v0.3.25**
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.23**
11
+ **当前版本:v0.3.25**
12
12
 
13
13
  ---
14
14
 
package/dist/main.js CHANGED
@@ -117647,6 +117647,7 @@ function deriveTriggerFacts(parsed, isNewThread, larkCliProfile) {
117647
117647
  // src/agent/answerChannel.ts
117648
117648
  var ANSWER_BEGIN_MARKER = "LARKWAY_ANSWER_BEGIN";
117649
117649
  var ANSWER_END_MARKER = "LARKWAY_ANSWER_END";
117650
+ var STREAM_HOLD_CHARS = ANSWER_END_MARKER.length + 2;
117650
117651
  function stripLeadingNewline(text) {
117651
117652
  return text.replace(/^\r?\n/, "");
117652
117653
  }
@@ -117662,6 +117663,9 @@ function markerLineIndex(text, marker) {
117662
117663
  const after = lineEnd + (match[2]?.length ?? 0);
117663
117664
  return { start: lineStart, end: after };
117664
117665
  }
117666
+ function hasUsefulText(text) {
117667
+ return text.length > 0;
117668
+ }
117665
117669
  function splitAnswerChannelText(text, raw) {
117666
117670
  const begin = markerLineIndex(text, ANSWER_BEGIN_MARKER);
117667
117671
  if (!begin) return [{ type: "internal_text", text, raw }];
@@ -117676,6 +117680,71 @@ function splitAnswerChannelText(text, raw) {
117676
117680
  if (trailing.trim()) events.push({ type: "internal_text", text: trailing, raw });
117677
117681
  return events;
117678
117682
  }
117683
+ var AnswerChannelExtractor = class {
117684
+ mode = "waiting";
117685
+ buffer = "";
117686
+ visibleText = "";
117687
+ ingestDelta(text, raw) {
117688
+ if (!text || this.mode === "closed") return [];
117689
+ this.buffer += text;
117690
+ return this.drain(raw);
117691
+ }
117692
+ ingestSnapshot(text, raw) {
117693
+ const events = splitAnswerChannelText(text, raw);
117694
+ const out = [];
117695
+ for (const event of events) {
117696
+ if (event.type !== "answer_snapshot") {
117697
+ out.push(event);
117698
+ continue;
117699
+ }
117700
+ if (event.text === this.visibleText) continue;
117701
+ this.visibleText = event.text;
117702
+ out.push(event);
117703
+ if (text.includes(ANSWER_END_MARKER)) this.mode = "closed";
117704
+ }
117705
+ return out;
117706
+ }
117707
+ drain(raw) {
117708
+ const events = [];
117709
+ if (this.mode === "waiting") {
117710
+ const begin = markerLineIndex(this.buffer, ANSWER_BEGIN_MARKER);
117711
+ if (!begin) {
117712
+ this.trimWaitingBuffer();
117713
+ return events;
117714
+ }
117715
+ const before = stripTrailingNewline(this.buffer.slice(0, begin.start));
117716
+ if (before.trim()) events.push({ type: "internal_text", text: before, raw });
117717
+ this.buffer = this.buffer.slice(begin.end);
117718
+ this.mode = "answer";
117719
+ }
117720
+ if (this.mode !== "answer") return events;
117721
+ const end = markerLineIndex(this.buffer, ANSWER_END_MARKER);
117722
+ if (end) {
117723
+ const answerTail = stripTrailingNewline(this.buffer.slice(0, end.start));
117724
+ if (hasUsefulText(answerTail)) events.push(this.answerDelta(answerTail, raw));
117725
+ const trailing = stripLeadingNewline(this.buffer.slice(end.end));
117726
+ if (trailing.trim()) events.push({ type: "internal_text", text: trailing, raw });
117727
+ this.buffer = "";
117728
+ this.mode = "closed";
117729
+ return events;
117730
+ }
117731
+ if (this.buffer.length <= STREAM_HOLD_CHARS) return events;
117732
+ const emitText = this.buffer.slice(0, this.buffer.length - STREAM_HOLD_CHARS);
117733
+ this.buffer = this.buffer.slice(this.buffer.length - STREAM_HOLD_CHARS);
117734
+ if (hasUsefulText(emitText)) events.push(this.answerDelta(emitText, raw));
117735
+ return events;
117736
+ }
117737
+ answerDelta(text, raw) {
117738
+ this.visibleText += text;
117739
+ return { type: "answer_delta", text, raw };
117740
+ }
117741
+ trimWaitingBuffer() {
117742
+ const max = ANSWER_BEGIN_MARKER.length + 2;
117743
+ if (this.buffer.length > max) {
117744
+ this.buffer = this.buffer.slice(this.buffer.length - max);
117745
+ }
117746
+ }
117747
+ };
117679
117748
 
117680
117749
  // src/claude/prompt.ts
117681
117750
  var MEMORY_CATEGORY_FILE_NAMES = [
@@ -117719,8 +117788,7 @@ function renderStateContract(stateFilePath) {
117719
117788
  "- status: in_progress / ready / failed(bridge \u552F\u4E00\u5F3A\u4F9D\u8D56\u7684\u5B57\u6BB5)",
117720
117789
  "- last_message: \u7ED9\u8FD0\u8425\u770B\u7684\u5361\u7247\u6B63\u6587\u3002\u4F60\u81EA\u884C\u7EC4\u7EC7\u5C55\u793A\u5185\u5BB9,\u4F8B\u5982\u8FDB\u5EA6\u3001\u7ED3\u8BBA\u3001\u8BC1\u636E\u3001MR \u94FE\u63A5\u3001dev \u9884\u89C8\u3001\u9700\u8981\u7528\u6237\u8865\u5145\u7684\u4FE1\u606F",
117721
117790
  "- error: \u5931\u8D25\u539F\u56E0(\u642D\u914D status=failed)",
117722
- "- card_title: \u5361\u7247\u6807\u9898\u8986\u76D6(\u53EF\u9009)",
117723
- "- card_color: \u5361\u7247\u914D\u8272(\u53EF\u9009,success/failure/neutral,\u4E5F\u53EF\u76F4\u63A5\u5199 green/red/grey)",
117791
+ "- 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",
117724
117792
  "- 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",
117725
117793
  '- 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',
117726
117794
  '- response_surface: \u53EF\u9009\u8986\u76D6\u5B57\u6BB5,\u5F62\u5982 `{mode:"card"|"post"|"hybrid", primary?:"card"|"post", post?:{mentions:[{user_id,label?}]}, card?:{compact?:boolean, capabilities?:[...]}}`\u3002\u9ED8\u8BA4\u53EF\u4E0D\u5199;\u65E0\u663E\u5F0F card \u610F\u56FE\u65F6 bridge \u6309 CardKit \u6D41\u5F0F\u5361\u7247\u5904\u7406,\u6700\u7EC8\u6536\u6210\u5E72\u51C0\u603B\u7ED3\u5361\u3002\u9700\u8981\u771F\u5B9E @ \u65F6\u628A\u76EE\u6807\u5199\u5165 `post.mentions`;\u4E0D\u8981\u5199 raw Feishu post/card JSON\u3002',
@@ -119043,6 +119111,20 @@ async function deleteCardFile(worktreePath) {
119043
119111
  // src/bridge/cardkitFile.ts
119044
119112
  import fs5 from "node:fs/promises";
119045
119113
  import path8 from "node:path";
119114
+ var defaultCardKitLiveMetrics = () => ({
119115
+ answerDeltaCount: 0,
119116
+ answerSnapshotCount: 0,
119117
+ firstAnswerAt: null,
119118
+ lastAnswerAt: null,
119119
+ visibleAnswerLength: 0,
119120
+ toolUseCount: 0,
119121
+ lastToolUseAt: null,
119122
+ statusPatchCount: 0,
119123
+ lastStatusPatchAt: null,
119124
+ progressUpdateCount: 0,
119125
+ lastProgressPatchAt: null,
119126
+ lastPatchError: null
119127
+ });
119046
119128
  var CardKitFileSchema = external_exports.object({
119047
119129
  surface: external_exports.literal("cardkit_stream"),
119048
119130
  status: external_exports.enum(["planned", "message_sent", "streaming", "finalized", "fallback_visible", "failed"]).default("planned"),
@@ -119056,6 +119138,20 @@ var CardKitFileSchema = external_exports.object({
119056
119138
  replyInThread: external_exports.boolean().default(true),
119057
119139
  idempotencyKey: external_exports.string().min(1),
119058
119140
  sequence: external_exports.number().int().nonnegative().default(0),
119141
+ live: external_exports.object({
119142
+ answerDeltaCount: external_exports.number().int().nonnegative().default(0),
119143
+ answerSnapshotCount: external_exports.number().int().nonnegative().default(0),
119144
+ firstAnswerAt: external_exports.string().min(1).nullable().default(null),
119145
+ lastAnswerAt: external_exports.string().min(1).nullable().default(null),
119146
+ visibleAnswerLength: external_exports.number().int().nonnegative().default(0),
119147
+ toolUseCount: external_exports.number().int().nonnegative().default(0),
119148
+ lastToolUseAt: external_exports.string().min(1).nullable().default(null),
119149
+ statusPatchCount: external_exports.number().int().nonnegative().default(0),
119150
+ lastStatusPatchAt: external_exports.string().min(1).nullable().default(null),
119151
+ progressUpdateCount: external_exports.number().int().nonnegative().default(0),
119152
+ lastProgressPatchAt: external_exports.string().min(1).nullable().default(null),
119153
+ lastPatchError: external_exports.string().min(1).nullable().default(null)
119154
+ }).default(defaultCardKitLiveMetrics),
119059
119155
  elements: external_exports.object({
119060
119156
  status: external_exports.object({ elementId: external_exports.string().min(1) }).optional(),
119061
119157
  thinking: external_exports.object({ elementId: external_exports.string().min(1) }).optional(),
@@ -119136,6 +119232,9 @@ function markdown(content, elementId) {
119136
119232
  if (elementId) element["element_id"] = elementId;
119137
119233
  return element;
119138
119234
  }
119235
+ function buildCardKitAnswerElement(content = "") {
119236
+ return markdown(content, CARDKIT_FINAL_ELEMENT_ID);
119237
+ }
119139
119238
  function safeAtMention(target) {
119140
119239
  const id = target.user_id.trim();
119141
119240
  if (!/^[A-Za-z0-9_:-]+$/.test(id)) return "";
@@ -119187,10 +119286,7 @@ function buildCardKitInitialCard(opts = {}) {
119187
119286
  }
119188
119287
  },
119189
119288
  body: {
119190
- elements: [
119191
- markdown(" ", CARDKIT_FINAL_ELEMENT_ID),
119192
- markdown(footerText, CARDKIT_FOOTER_ELEMENT_ID)
119193
- ]
119289
+ elements: [markdown(footerText, CARDKIT_FOOTER_ELEMENT_ID)]
119194
119290
  }
119195
119291
  };
119196
119292
  }
@@ -119237,7 +119333,7 @@ function finalMarkdown(opts) {
119237
119333
  return truncateChars([mentionLine, body].filter(Boolean).join("\n\n") || "\u5B8C\u6210\u3002", FINAL_BUDGET_CHARS, "\n\n_\u5185\u5BB9\u8FC7\u957F,\u5DF2\u622A\u65AD\u3002_");
119238
119334
  }
119239
119335
  function buildCardKitFinalCard(opts) {
119240
- const elements = [markdown(finalMarkdown(opts), CARDKIT_FINAL_ELEMENT_ID)];
119336
+ const elements = [buildCardKitAnswerElement(finalMarkdown(opts))];
119241
119337
  const images = [];
119242
119338
  if (opts.contentBlocks?.length) {
119243
119339
  for (const block of opts.contentBlocks) {
@@ -119263,10 +119359,6 @@ function buildCardKitFinalCard(opts) {
119263
119359
  streaming_mode: false,
119264
119360
  summary: { content: truncateChars(opts.finalText.replace(/\s+/g, " ").trim(), 50, "...") }
119265
119361
  },
119266
- header: {
119267
- title: plainText2(opts.title ?? "\u5B8C\u6210"),
119268
- template: "green"
119269
- },
119270
119362
  body: { elements }
119271
119363
  });
119272
119364
  }
@@ -119285,6 +119377,34 @@ function idempotencyKey(facts) {
119285
119377
  function sequenceUuid(cardId, role, sequence) {
119286
119378
  return deriveCardKitUuid([cardId, role, String(sequence)].join("\0"));
119287
119379
  }
119380
+ function isMissingCardKitElementError(err) {
119381
+ if (!(err instanceof Error)) return false;
119382
+ const message = err.message.toLowerCase();
119383
+ return message.includes("element") && (message.includes("not found") || message.includes("not exist") || message.includes("\u4E0D\u5B58\u5728"));
119384
+ }
119385
+ function initialLiveMetrics() {
119386
+ return {
119387
+ answerDeltaCount: 0,
119388
+ answerSnapshotCount: 0,
119389
+ firstAnswerAt: null,
119390
+ lastAnswerAt: null,
119391
+ visibleAnswerLength: 0,
119392
+ toolUseCount: 0,
119393
+ lastToolUseAt: null,
119394
+ statusPatchCount: 0,
119395
+ lastStatusPatchAt: null,
119396
+ progressUpdateCount: 0,
119397
+ lastProgressPatchAt: null,
119398
+ lastPatchError: null
119399
+ };
119400
+ }
119401
+ function summarizeError(err) {
119402
+ const message = err instanceof Error ? err.message : String(err);
119403
+ return message.replace(/\s+/g, " ").trim().slice(0, 240) || "unknown error";
119404
+ }
119405
+ function toolStatusText(toolUseCount) {
119406
+ return toolUseCount > 0 ? `\u52AA\u529B\u56DE\u7B54\u4E2D... \xB7 \u5DF2\u7528 ${toolUseCount} \u4E2A\u5DE5\u5177` : "\u52AA\u529B\u56DE\u7B54\u4E2D...";
119407
+ }
119288
119408
  var LiveCardKitProgressHandle = class {
119289
119409
  cardId;
119290
119410
  messageId;
@@ -119293,11 +119413,14 @@ var LiveCardKitProgressHandle = class {
119293
119413
  patchIntervalMs;
119294
119414
  maxProgressUpdates;
119295
119415
  onSequenceCommitted;
119416
+ onLiveMetricsChanged;
119296
119417
  answerBuffer = "";
119297
119418
  pendingPatch = null;
119298
119419
  inFlight = Promise.resolve();
119299
119420
  closed = false;
119300
- progressUpdates = 0;
119421
+ answerElementCreated = false;
119422
+ immediatePatchStarted = false;
119423
+ metrics = initialLiveMetrics();
119301
119424
  sequence = 0;
119302
119425
  constructor(opts) {
119303
119426
  this.cardKitClient = opts.cardKitClient;
@@ -119307,20 +119430,31 @@ var LiveCardKitProgressHandle = class {
119307
119430
  this.patchIntervalMs = opts.patchIntervalMs;
119308
119431
  this.maxProgressUpdates = opts.maxProgressUpdates;
119309
119432
  this.onSequenceCommitted = opts.onSequenceCommitted;
119433
+ this.onLiveMetricsChanged = opts.onLiveMetricsChanged;
119310
119434
  }
119311
119435
  get answerText() {
119312
119436
  return this.answerBuffer;
119313
119437
  }
119438
+ get liveMetrics() {
119439
+ return { ...this.metrics };
119440
+ }
119314
119441
  handle(event) {
119315
119442
  if (this.closed) return;
119443
+ if (event.type === "tool_use") {
119444
+ this.recordToolUse();
119445
+ this.patchStatus(toolStatusText(this.metrics.toolUseCount));
119446
+ return;
119447
+ }
119316
119448
  if (event.type === "answer_delta") {
119317
119449
  this.answerBuffer += event.text;
119318
- this.schedulePatch();
119450
+ this.recordAnswerEvent("answer_delta");
119451
+ this.schedulePatch({ immediate: !this.immediatePatchStarted });
119319
119452
  return;
119320
119453
  }
119321
119454
  if (event.type === "answer_snapshot") {
119322
119455
  this.answerBuffer = event.text;
119323
- this.schedulePatch();
119456
+ this.recordAnswerEvent("answer_snapshot");
119457
+ this.schedulePatch({ immediate: !this.immediatePatchStarted });
119324
119458
  }
119325
119459
  }
119326
119460
  async drain() {
@@ -119336,6 +119470,7 @@ var LiveCardKitProgressHandle = class {
119336
119470
  this.closed = true;
119337
119471
  const finalMarkdown2 = buildCardKitFinalMarkdown(opts);
119338
119472
  if (finalMarkdown2 !== this.answerBuffer) {
119473
+ await this.withAnswerElement(finalMarkdown2);
119339
119474
  await this.next(
119340
119475
  (sequence) => this.cardKitClient.streamElementContent(
119341
119476
  this.cardId,
@@ -119378,8 +119513,71 @@ var LiveCardKitProgressHandle = class {
119378
119513
  this.pendingPatch = null;
119379
119514
  }
119380
119515
  }
119381
- schedulePatch() {
119382
- if (this.pendingPatch || this.progressUpdates >= this.maxProgressUpdates) return;
119516
+ recordAnswerEvent(type2) {
119517
+ const now = (/* @__PURE__ */ new Date()).toISOString();
119518
+ if (type2 === "answer_delta") {
119519
+ this.metrics.answerDeltaCount += 1;
119520
+ } else {
119521
+ this.metrics.answerSnapshotCount += 1;
119522
+ }
119523
+ this.metrics.firstAnswerAt ??= now;
119524
+ this.metrics.lastAnswerAt = now;
119525
+ this.metrics.visibleAnswerLength = this.answerBuffer.length;
119526
+ this.emitLiveMetrics();
119527
+ console.info(
119528
+ "[cardkit_progress] answer event",
119529
+ `type=${type2}`,
119530
+ `delta_count=${this.metrics.answerDeltaCount}`,
119531
+ `snapshot_count=${this.metrics.answerSnapshotCount}`,
119532
+ `visible_length=${this.metrics.visibleAnswerLength}`,
119533
+ `sequence=${this.sequence}`
119534
+ );
119535
+ }
119536
+ recordToolUse() {
119537
+ this.metrics.toolUseCount += 1;
119538
+ this.metrics.lastToolUseAt = (/* @__PURE__ */ new Date()).toISOString();
119539
+ this.emitLiveMetrics();
119540
+ console.info(
119541
+ "[cardkit_progress] tool event",
119542
+ `tool_use_count=${this.metrics.toolUseCount}`,
119543
+ `sequence=${this.sequence}`
119544
+ );
119545
+ }
119546
+ patchStatus(content) {
119547
+ this.inFlight = this.inFlight.then(
119548
+ () => this.next(
119549
+ (sequence) => this.cardKitClient.updateElement(
119550
+ this.cardId,
119551
+ CARDKIT_FOOTER_ELEMENT_ID,
119552
+ {
119553
+ tag: "markdown",
119554
+ content,
119555
+ element_id: CARDKIT_FOOTER_ELEMENT_ID
119556
+ },
119557
+ {
119558
+ sequence,
119559
+ uuid: sequenceUuid(this.cardId, "status", sequence)
119560
+ }
119561
+ )
119562
+ )
119563
+ ).then(() => {
119564
+ this.metrics.statusPatchCount += 1;
119565
+ this.metrics.lastStatusPatchAt = (/* @__PURE__ */ new Date()).toISOString();
119566
+ this.metrics.lastPatchError = null;
119567
+ this.emitLiveMetrics();
119568
+ }).catch((err) => {
119569
+ this.metrics.lastPatchError = summarizeError(err);
119570
+ this.emitLiveMetrics();
119571
+ console.warn("[cardkit_progress] status update failed (continuing):", err);
119572
+ });
119573
+ }
119574
+ schedulePatch(opts = {}) {
119575
+ if (this.pendingPatch || this.metrics.progressUpdateCount >= this.maxProgressUpdates) return;
119576
+ if (opts.immediate) {
119577
+ this.immediatePatchStarted = true;
119578
+ void this.patchProgress();
119579
+ return;
119580
+ }
119383
119581
  this.pendingPatch = setTimeout(() => {
119384
119582
  this.pendingPatch = null;
119385
119583
  void this.patchProgress();
@@ -119387,26 +119585,60 @@ var LiveCardKitProgressHandle = class {
119387
119585
  this.pendingPatch.unref?.();
119388
119586
  }
119389
119587
  async patchProgress() {
119390
- if (this.closed || this.progressUpdates >= this.maxProgressUpdates) return;
119588
+ if (this.closed || this.metrics.progressUpdateCount >= this.maxProgressUpdates) return;
119391
119589
  if (!this.answerBuffer) return;
119392
- this.progressUpdates += 1;
119393
119590
  this.inFlight = this.inFlight.then(
119394
- () => this.next(
119395
- (sequence) => this.cardKitClient.streamElementContent(this.cardId, CARDKIT_FINAL_ELEMENT_ID, this.answerBuffer, {
119396
- sequence,
119397
- uuid: sequenceUuid(this.cardId, "answer", sequence)
119398
- })
119591
+ () => this.withAnswerElement(this.answerBuffer).then(
119592
+ () => this.next(
119593
+ (sequence) => this.cardKitClient.streamElementContent(this.cardId, CARDKIT_FINAL_ELEMENT_ID, this.answerBuffer, {
119594
+ sequence,
119595
+ uuid: sequenceUuid(this.cardId, "answer", sequence)
119596
+ })
119597
+ )
119399
119598
  )
119400
- ).catch((err) => {
119599
+ ).then(() => {
119600
+ this.metrics.progressUpdateCount += 1;
119601
+ this.metrics.visibleAnswerLength = this.answerBuffer.length;
119602
+ this.metrics.lastProgressPatchAt = (/* @__PURE__ */ new Date()).toISOString();
119603
+ this.metrics.lastPatchError = null;
119604
+ this.emitLiveMetrics();
119605
+ console.info(
119606
+ "[cardkit_progress] progress committed",
119607
+ `progress_update_count=${this.metrics.progressUpdateCount}`,
119608
+ `visible_length=${this.metrics.visibleAnswerLength}`,
119609
+ `sequence=${this.sequence}`
119610
+ );
119611
+ }).catch((err) => {
119612
+ this.metrics.lastPatchError = summarizeError(err);
119613
+ this.emitLiveMetrics();
119401
119614
  console.warn("[cardkit_progress] progress update failed (continuing):", err);
119402
119615
  });
119403
119616
  await this.inFlight;
119404
119617
  }
119618
+ async withAnswerElement(initialContent) {
119619
+ if (this.answerElementCreated) return;
119620
+ await this.next(
119621
+ (sequence) => this.cardKitClient.createElements(
119622
+ this.cardId,
119623
+ [buildCardKitAnswerElement(initialContent)],
119624
+ {
119625
+ sequence,
119626
+ uuid: sequenceUuid(this.cardId, "answer-element", sequence),
119627
+ type: "insert_before",
119628
+ targetElementId: CARDKIT_FOOTER_ELEMENT_ID
119629
+ }
119630
+ )
119631
+ );
119632
+ this.answerElementCreated = true;
119633
+ }
119405
119634
  async next(fn) {
119406
119635
  this.sequence += 1;
119407
119636
  await fn(this.sequence);
119408
119637
  await this.onSequenceCommitted?.(this.sequence);
119409
119638
  }
119639
+ emitLiveMetrics() {
119640
+ this.onLiveMetricsChanged?.({ ...this.metrics, sequence: this.sequence });
119641
+ }
119410
119642
  };
119411
119643
  async function createCardKitProgressHandle(opts) {
119412
119644
  const key = idempotencyKey(opts.facts);
@@ -119440,7 +119672,8 @@ async function createCardKitProgressHandle(opts) {
119440
119672
  idempotencyKey: key,
119441
119673
  patchIntervalMs: opts.patchIntervalMs ?? DEFAULT_PATCH_INTERVAL_MS,
119442
119674
  maxProgressUpdates: opts.maxProgressUpdates ?? DEFAULT_MAX_PROGRESS_UPDATES,
119443
- onSequenceCommitted: opts.onSequenceCommitted
119675
+ onSequenceCommitted: opts.onSequenceCommitted,
119676
+ onLiveMetricsChanged: opts.onLiveMetricsChanged
119444
119677
  });
119445
119678
  }
119446
119679
  async function finalizeExistingCardKitCard(opts) {
@@ -119451,7 +119684,7 @@ async function finalizeExistingCardKitCard(opts) {
119451
119684
  await opts.onSequenceCommitted?.(sequence);
119452
119685
  };
119453
119686
  const finalMarkdown2 = buildCardKitFinalMarkdown(opts.final);
119454
- await next(
119687
+ const streamFinalContent = () => next(
119455
119688
  "reconcile-final-content",
119456
119689
  (seq2, uuid) => opts.cardKitClient.streamElementContent(
119457
119690
  opts.cardId,
@@ -119460,6 +119693,25 @@ async function finalizeExistingCardKitCard(opts) {
119460
119693
  { sequence: seq2, uuid }
119461
119694
  )
119462
119695
  );
119696
+ try {
119697
+ await streamFinalContent();
119698
+ } catch (err) {
119699
+ if (!isMissingCardKitElementError(err)) throw err;
119700
+ await next(
119701
+ "reconcile-final-element",
119702
+ (seq2, uuid) => opts.cardKitClient.createElements(
119703
+ opts.cardId,
119704
+ [buildCardKitAnswerElement(finalMarkdown2)],
119705
+ {
119706
+ sequence: seq2,
119707
+ uuid,
119708
+ type: "insert_before",
119709
+ targetElementId: CARDKIT_FOOTER_ELEMENT_ID
119710
+ }
119711
+ )
119712
+ );
119713
+ await streamFinalContent();
119714
+ }
119463
119715
  await next(
119464
119716
  "reconcile-final-card",
119465
119717
  (seq2, uuid) => opts.cardKitClient.updateCardEntity(
@@ -120495,6 +120747,7 @@ var ResponseSurfacePostBudget = class {
120495
120747
  };
120496
120748
 
120497
120749
  // src/bridge/handler.ts
120750
+ var DEFAULT_CARDKIT_RESPONSE_SURFACE_TIMEOUT_MS = 20 * 60 * 1e3;
120498
120751
  function execGit(cwd, args) {
120499
120752
  return new Promise((resolve2, reject) => {
120500
120753
  const child = child_process.spawn("git", args, {
@@ -121049,6 +121302,7 @@ var BridgeHandler = class {
121049
121302
  } catch (err) {
121050
121303
  console.warn("[bridge.handler] ensureStateFile failed (continuing):", err);
121051
121304
  }
121305
+ let cardKitRecordWrite = Promise.resolve();
121052
121306
  const updateCardKitRecord = async (patch) => {
121053
121307
  if (!cardKitRecord) return;
121054
121308
  cardKitRecord = {
@@ -121056,7 +121310,16 @@ var BridgeHandler = class {
121056
121310
  ...patch,
121057
121311
  updatedAt: (/* @__PURE__ */ new Date()).toISOString()
121058
121312
  };
121059
- await writeCardKitFile(worktreePath, cardKitRecord);
121313
+ const record = cardKitRecord;
121314
+ cardKitRecordWrite = cardKitRecordWrite.catch(() => {
121315
+ }).then(() => writeCardKitFile(worktreePath, record));
121316
+ await cardKitRecordWrite;
121317
+ };
121318
+ const updateCardKitLiveMetrics = (metrics) => {
121319
+ const { sequence, ...live } = metrics;
121320
+ void updateCardKitRecord({ sequence, live }).catch((err) => {
121321
+ console.warn("[bridge.handler] write CardKit live metrics failed:", err);
121322
+ });
121060
121323
  };
121061
121324
  if (!card && cardKitAvailable && this.deps.cardKitClient) {
121062
121325
  try {
@@ -121072,7 +121335,8 @@ var BridgeHandler = class {
121072
121335
  initialStatusText: "\u52AA\u529B\u56DE\u7B54\u4E2D...",
121073
121336
  onSequenceCommitted: async (sequence) => {
121074
121337
  await updateCardKitRecord({ status: "streaming", sequence });
121075
- }
121338
+ },
121339
+ onLiveMetricsChanged: updateCardKitLiveMetrics
121076
121340
  });
121077
121341
  cardKitRecord = {
121078
121342
  surface: "cardkit_stream",
@@ -121087,6 +121351,7 @@ var BridgeHandler = class {
121087
121351
  replyInThread,
121088
121352
  idempotencyKey: cardKitProgress.idempotencyKey,
121089
121353
  sequence: cardKitProgress.sequence,
121354
+ live: cardKitProgress.liveMetrics,
121090
121355
  elements: {
121091
121356
  footer: { elementId: "footer_md" },
121092
121357
  final: { elementId: "final_md" }
@@ -121254,7 +121519,12 @@ var BridgeHandler = class {
121254
121519
  });
121255
121520
  const backend = this.deps.botConfig?.backend ?? "claude";
121256
121521
  const permissionMode = this.deps.permissionMode ?? "bypassPermissions";
121257
- const timeoutMs = this.deps.subprocessTimeoutMs ?? 60 * 60 * 1e3;
121522
+ const baseTimeoutMs = this.deps.subprocessTimeoutMs ?? 60 * 60 * 1e3;
121523
+ const timeoutMs = cardKitProgress ? Math.min(
121524
+ baseTimeoutMs,
121525
+ this.deps.responseSurfaceTimeoutMs ?? DEFAULT_CARDKIT_RESPONSE_SURFACE_TIMEOUT_MS
121526
+ ) : baseTimeoutMs;
121527
+ const runnerStartedAt = Date.now();
121258
121528
  const handle = createRunner(backend).run({
121259
121529
  prompt,
121260
121530
  resumeSessionId: currentExisting?.sessionId,
@@ -121282,6 +121552,7 @@ var BridgeHandler = class {
121282
121552
  }
121283
121553
  }
121284
121554
  const result = await handle.done;
121555
+ const cardKitTurnTimedOut = cardKitProgress != null && result.exitCode !== 0 && Date.now() - runnerStartedAt >= timeoutMs;
121285
121556
  const rawReportedState = await readStateFile(worktreePath);
121286
121557
  const reportedState = rawReportedState?.updated_at != null && rawReportedState.updated_at !== preRunUpdatedAt ? rawReportedState : null;
121287
121558
  const now = Date.now();
@@ -121311,6 +121582,7 @@ var BridgeHandler = class {
121311
121582
  }
121312
121583
  const reportedStatus = reportedState?.status;
121313
121584
  const reportedError = reportedState?.error;
121585
+ const cardKitTimeoutFailure = cardKitTurnTimedOut && reportedStatus !== "ready" && reportedStatus !== "failed";
121314
121586
  let success;
121315
121587
  let failureReason;
121316
121588
  if (reportedStatus === "failed") {
@@ -121318,6 +121590,9 @@ var BridgeHandler = class {
121318
121590
  failureReason = reportedError ?? "bot \u62A5\u544A failed (\u65E0 error \u5B57\u6BB5)";
121319
121591
  } else if (reportedStatus === "ready") {
121320
121592
  success = true;
121593
+ } else if (cardKitTimeoutFailure) {
121594
+ success = false;
121595
+ failureReason = `agent turn timed out after ${timeoutMs}ms; CardKit running card was finalized as interrupted`;
121321
121596
  } else if (result.exitCode === 0) {
121322
121597
  success = true;
121323
121598
  } else {
@@ -121325,14 +121600,14 @@ var BridgeHandler = class {
121325
121600
  failureReason = `claude exited ${result.exitCode} \u4E14 bot \u672A\u66F4\u65B0 state.json status \u2014 \u53EF\u80FD\u5D29\u6E83`;
121326
121601
  }
121327
121602
  const fallbackAnswer = trustedAnswerText.trim() || cardKitProgress?.answerText.trim() || "";
121328
- const cardBody = 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");
121603
+ 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");
121329
121604
  const noReportThisTurn = reportedState === null;
121330
121605
  const neutralTitle = noReportThisTurn && success && !failureReason ? "\u{1F4AC} \u5DF2\u56DE\u590D" : void 0;
121331
121606
  const baseCardPayload = {
121332
121607
  finalText: cardBody,
121333
121608
  success,
121334
121609
  failureReason,
121335
- titleOverride: reportedState?.card_title ?? neutralTitle,
121610
+ titleOverride: reportedState?.card_title ?? (cardKitTimeoutFailure ? "\u5DF2\u4E2D\u65AD" : neutralTitle),
121336
121611
  colorOverride: reportedState?.card_color ?? (neutralTitle ? "neutral" : void 0),
121337
121612
  // V2 dynamic-choice buttons — agent-declared, rendered verbatim.
121338
121613
  // reportedState is null when state.json wasn't freshly written
@@ -125439,7 +125714,7 @@ function buildCommand(opts) {
125439
125714
  args.push("-p", opts.prompt);
125440
125715
  return [bin, args];
125441
125716
  }
125442
- function* parseLinesMulti(line) {
125717
+ function* parseLinesMulti(line, answerExtractor = new AnswerChannelExtractor()) {
125443
125718
  const trimmed = line.trim();
125444
125719
  if (trimmed === "") return;
125445
125720
  let obj;
@@ -125473,7 +125748,7 @@ function* parseLinesMulti(line) {
125473
125748
  if (typeof item !== "object" || item === null) continue;
125474
125749
  const block = item;
125475
125750
  if (block["type"] === "text" && typeof block["text"] === "string") {
125476
- yield* splitAnswerChannelText(block["text"], obj);
125751
+ yield* answerExtractor.ingestSnapshot(block["text"], obj);
125477
125752
  emitted = true;
125478
125753
  } else if (block["type"] === "tool_use") {
125479
125754
  yield {
@@ -125491,6 +125766,22 @@ function* parseLinesMulti(line) {
125491
125766
  yield { type: "raw", raw: obj };
125492
125767
  return;
125493
125768
  }
125769
+ if (eventType === "stream_event") {
125770
+ const event = record["event"];
125771
+ if (typeof event === "object" && event !== null) {
125772
+ const streamEvent = event;
125773
+ const delta = streamEvent["delta"];
125774
+ if (typeof delta === "object" && delta !== null) {
125775
+ const deltaRecord = delta;
125776
+ if (streamEvent["type"] === "content_block_delta" && deltaRecord["type"] === "text_delta" && typeof deltaRecord["text"] === "string") {
125777
+ yield* answerExtractor.ingestDelta(deltaRecord["text"], obj);
125778
+ return;
125779
+ }
125780
+ }
125781
+ }
125782
+ yield { type: "raw", raw: obj };
125783
+ return;
125784
+ }
125494
125785
  if (eventType === "user") {
125495
125786
  const message = record["message"];
125496
125787
  if (typeof message === "object" && message !== null && Array.isArray(message["content"])) {
@@ -125664,9 +125955,10 @@ stderr: ${stderr}` : "")
125664
125955
  // unblock the events loop so handler.ts can reach card.finalize().
125665
125956
  signal: rlAbortController.signal
125666
125957
  });
125958
+ const answerExtractor = new AnswerChannelExtractor();
125667
125959
  try {
125668
125960
  for await (const line of rl) {
125669
- for (const event of parseLinesMulti(line)) {
125961
+ for (const event of parseLinesMulti(line, answerExtractor)) {
125670
125962
  if (event.type === "system_init") {
125671
125963
  discoveredSessionId = event.sessionId;
125672
125964
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "larkway",
3
- "version": "0.3.23",
3
+ "version": "0.3.25",
4
4
  "description": "Thin bridge: Feishu thread to local Claude Code CLI",
5
5
  "license": "MIT",
6
6
  "author": "Chuck Wu (chuckwu0)",