lark-remote 0.5.14 → 0.5.16

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.
@@ -157,6 +157,11 @@ function findSessionFile(sessionId, cwd, sessionsDir) {
157
157
  */
158
158
  function piScalarScan(filePath) {
159
159
  const acc = new UsageAccumulator();
160
+ // 末个真人 turn 的 flow:user 消息开启新 turn,assistant 用量同时进两个
161
+ // accumulator。带工具往返的 turn 有多个 assistant 消息(一次模型请求一条),
162
+ // 只取末条会低报整轮消耗。
163
+ let turnAcc = new UsageAccumulator();
164
+ let sawUser = false;
160
165
  let tailOffset = -1;
161
166
  let lastUserText;
162
167
  scanJsonlOnce(filePath, (rawObj, line, offset) => {
@@ -168,18 +173,22 @@ function piScalarScan(filePath) {
168
173
  // usage aggregation (assistant only)
169
174
  if (msg.role === 'assistant' && msg.usage) {
170
175
  const u = msg.usage;
171
- acc.add({
176
+ const record = {
172
177
  input: u.input ?? 0,
173
178
  output: u.output ?? 0,
174
179
  cacheRead: u.cacheRead ?? 0,
175
180
  cacheCreation: u.cacheWrite ?? 0,
176
181
  total: u.totalTokens ?? 0,
177
- });
182
+ };
183
+ acc.add(record);
184
+ turnAcc.add(record);
178
185
  }
179
186
  // last user message offset + displayTitle. Record the byte offset
180
187
  // where the tail should start: the beginning of the NEXT line after
181
188
  // this user message (see tailOffsetAfter).
182
189
  if (msg.role === 'user') {
190
+ sawUser = true;
191
+ turnAcc = new UsageAccumulator();
183
192
  tailOffset = tailOffsetAfter(line, offset);
184
193
  const text = extractPiText(msg.content);
185
194
  // Compress skill-injection bodies so the "最近输入" label shows what
@@ -195,7 +204,7 @@ function piScalarScan(filePath) {
195
204
  });
196
205
  return {
197
206
  tailOffset,
198
- usage: buildPiUsage(acc),
207
+ usage: buildPiUsage(acc, turnAcc, sawUser),
199
208
  // Whitespace-normalized single-line display title (no length truncation —
200
209
  // the consumer truncates). Normalization collapses the skill-injection
201
210
  // newlines so the title stays on one line.
@@ -203,27 +212,38 @@ function piScalarScan(filePath) {
203
212
  };
204
213
  }
205
214
  /**
206
- * Build the AgentSessionUsage from an accumulator, preserving pi's
207
- * "last turn" semantics: inputTokens/outputTokens use the LAST turn's
208
- * values, contextLength = last turn's context-window occupancy
209
- * (input + cacheRead + cacheCreation, excludes output —unified
210
- * contract across all five readers), cumulative = session-wide sum.
215
+ * Build the AgentSessionUsage: flow (input/output/cache/total) is the sum of
216
+ * every assistant message in the LAST real-user turn; contextLength is the
217
+ * last assistant message's window occupancy; cumulative is the whole session.
218
+ * With no real-user turn (legacy/truncated file), flow falls back to the last
219
+ * assistant sample, matching dsh/codex's no-turn-marker fallback.
211
220
  * Returns undefined when no usage data was observed.
212
221
  */
213
- function buildPiUsage(acc) {
222
+ function buildPiUsage(acc, turnAcc, sawUser) {
214
223
  const t = acc.totals;
215
- if (t.input + t.output === 0) {
224
+ if (t.input + t.output + t.cacheRead + t.cacheCreation === 0) {
216
225
  return undefined;
217
226
  }
218
227
  const l = acc.last;
228
+ // 没有真人 user prompt(旧数据/截断文件)→ 无法划分 turn,回退末条采样,
229
+ // 与 dsh/codex「无 turn 标记退回末条」一致;不能把整份文件当成一轮。
230
+ const f = sawUser
231
+ ? turnAcc.totals
232
+ : {
233
+ input: l.input,
234
+ output: l.output,
235
+ cacheRead: l.cacheRead,
236
+ cacheCreation: l.cacheCreation,
237
+ total: l.total,
238
+ };
219
239
  return {
220
- inputTokens: l.input,
221
- outputTokens: l.output,
240
+ inputTokens: f.input,
241
+ outputTokens: f.output,
222
242
  contextLength: contextWindowOccupancy(l),
223
243
  compactCount: acc.compactCount > 0 ? acc.compactCount : undefined,
224
- cacheReadTokens: l.cacheRead > 0 ? l.cacheRead : 0,
225
- cacheCreationTokens: l.cacheCreation > 0 ? l.cacheCreation : 0,
226
- totalTokens: l.total > 0 ? l.total : undefined,
244
+ cacheReadTokens: f.cacheRead > 0 ? f.cacheRead : 0,
245
+ cacheCreationTokens: f.cacheCreation > 0 ? f.cacheCreation : 0,
246
+ totalTokens: f.total > 0 ? f.total : undefined,
227
247
  // Cumulative (session-wide): sum of ALL runs in the jsonl file.
228
248
  ...cumulativeUsageFields(t),
229
249
  };
@@ -10,4 +10,6 @@ export declare class SessionReaderRegistry {
10
10
  private readonly readers;
11
11
  register(kind: AgentKind, reader: AgentSessionReader): void;
12
12
  get(kind: AgentKind): AgentSessionReader;
13
+ /** 可选能力探测用:不抛错,交给调用方决定降级行为。 */
14
+ tryGet(kind: AgentKind): AgentSessionReader | undefined;
13
15
  }
@@ -16,5 +16,9 @@ export class SessionReaderRegistry {
16
16
  }
17
17
  return reader;
18
18
  }
19
+ /** 可选能力探测用:不抛错,交给调用方决定降级行为。 */
20
+ tryGet(kind) {
21
+ return this.readers.get(kind);
22
+ }
19
23
  }
20
24
  //# sourceMappingURL=registry.js.map
@@ -175,4 +175,10 @@ export interface AgentSessionReader {
175
175
  * 未实现 = 该 agent 没有这个概念,调用方按「不在途」处理。
176
176
  */
177
177
  readCompactionState?(sessionId: string, cwd: string): CompactionState;
178
+ /**
179
+ * `readSessionContent` 是否产出 `compactCount`。opencode 的 export 没有
180
+ * 压缩计数;compact 收尾的重试不能把「不支持」误判成「还没落盘」。
181
+ * 未声明 = 支持(保持既有 reader 行为)。
182
+ */
183
+ supportsCompactCount?: boolean;
178
184
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "lark-remote",
3
- "version": "0.5.14",
3
+ "version": "0.5.16",
4
4
  "description": "Feishu (Lark) private-chat bridge to local coding agents — Claude Code / Codex / opencode / pi / kimi, with live-streaming CardKit 2.0 cards",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",