@leviyuan/lodestar 0.2.1 → 0.2.2

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
@@ -24,7 +24,6 @@ AI 不是帮手,是倍率。它放大的不是体力,是你——你的直觉、
24
24
  - ⌨️ **Type-ahead 不打断**:连珠炮全收,排队下一轮合并处理
25
25
  - 🔢 **合并消息加序号**:`[#N]\n` 前缀让模型看清独立边界
26
26
  - ⏳ **排队反应可见**:消息进队列加 ⏳,消化/取消自动清/换 ❌
27
- - 📨 **mid-turn 切新卡**:中途新消息 → 下一 tool 边界切新卡续写
28
27
  - ⏰ **定时唤醒可见化**:Cron / ScheduleWakeup 到点自开新卡
29
28
  - 📊 **footer 实时指标**:`✅ ⏱时长 · 📊上下文% · 💰本轮成本`
30
29
  - 📦 **`hi` 弹控制台**:跨群项目、上下文%、订阅额度一屏看完
@@ -65,7 +64,7 @@ AI 不是帮手,是倍率。它放大的不是体力,是你——你的直觉、
65
64
 
66
65
  **运行时**:[Bun](https://bun.sh) ≥ 1.0。
67
66
 
68
- **Claude Code**:装好且能跑 —— 详见[官方文档](https://docs.anthropic.com/en/docs/claude-code)。**强烈建议用 claude.ai 账号 OAuth 登录**(`claude auth login`),而不是 `ANTHROPIC_API_KEY`:Cron / ScheduleWakeup / `/schedule` 等定时唤醒工具只在 OAuth 模式下注册。
67
+ **Claude Code**:装好且能跑 —— 详见[官方文档](https://docs.anthropic.com/en/docs/claude-code)
69
68
 
70
69
  **飞书自建应用**:去[飞书开放平台](https://open.feishu.cn/app)→ 创建企业自建应用,然后:
71
70
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@leviyuan/lodestar",
3
- "version": "0.2.1",
3
+ "version": "0.2.2",
4
4
  "publishConfig": {
5
5
  "access": "public"
6
6
  },
package/src/cards.ts CHANGED
@@ -668,7 +668,7 @@ export function consoleCard(opts: ConsoleOpts): object {
668
668
  lines.push(` · ${dot} \`${p.name}\`${up}${mark}`)
669
669
  }
670
670
  }
671
- if (contextTokens != null) {
671
+ if (contextTokens != null && contextTokens > 0) {
672
672
  const limit = contextLimit ?? 1_000_000
673
673
  const pct = limit > 0 ? Math.round((contextTokens / limit) * 100) : 0
674
674
  lines.push(`**📦 上下文** ${fmtTokens(contextTokens)} / ${fmtTokens(limit)} (${pct}%)`)
package/src/session.ts CHANGED
@@ -137,25 +137,6 @@ export class Session {
137
137
  * to clear (via deleteReaction). Empty for eager-opened solo turns
138
138
  * and for scheduled wakeups (no user messages went into those). */
139
139
  private currentBatchReactionIds = new Map<string, string>()
140
- /** Set the moment a mid-turn user message lands. Tells the next
141
- * content-adding event (assistant text delta or fresh tool_use) to
142
- * rotate the card before applying its update — closes the in-flight
143
- * card with a `📨 转交新卡` footer and opens a fresh card, so the
144
- * continuation has a visible boundary instead of piling up under
145
- * one card. Reset to false after the rotation fires (or on
146
- * stop/restart/exit). User feedback (2026-05-15): the prior
147
- * everything-in-one-card behavior made the order feel jumbled. */
148
- private wantsRotation = false
149
- /** Holds assistant / thinking / tool_use events that arrive while a
150
- * card rotation is mid-flight (close-old → open-new straddles a
151
- * Feishu API await window during which `currentTurn` is transiently
152
- * null). Replayed onto the new card the moment rotation completes
153
- * so no streamed token is lost across the boundary. */
154
- private rotationBuffer: Array<
155
- | { kind: 'assistant'; delta: string }
156
- | { kind: 'thinking'; delta: string }
157
- | { kind: 'tool_use'; id: string; name: string; input: any }
158
- > = []
159
140
  /** Count of `system/init` events seen this subprocess. The first one is
160
141
  * the boot init (claimed by whichever user message lands first); all
161
142
  * subsequent ones mark the start of an SDK-initiated turn (queued
@@ -311,8 +292,6 @@ export class Session {
311
292
  this.lastUserOpenId = ''
312
293
  this.pendingReactionIds = new Map()
313
294
  this.currentBatchReactionIds = new Map()
314
- this.wantsRotation = false
315
- this.rotationBuffer = []
316
295
  this.initCount = 0
317
296
  this.openingTurn = false
318
297
  this.pendingPermissions.clear()
@@ -333,8 +312,6 @@ export class Session {
333
312
  this.lastUserOpenId = ''
334
313
  this.pendingReactionIds = new Map()
335
314
  this.currentBatchReactionIds = new Map()
336
- this.wantsRotation = false
337
- this.rotationBuffer = []
338
315
  this.initCount = 0
339
316
  this.openingTurn = false
340
317
  this.pendingPermissions.clear()
@@ -416,7 +393,6 @@ export class Session {
416
393
  this.lastUserOpenId = ''
417
394
  this.pendingReactionIds = new Map()
418
395
  this.currentBatchReactionIds = new Map()
419
- this.wantsRotation = false
420
396
  this.interrupt()
421
397
  return true
422
398
  case 'kill':
@@ -453,6 +429,7 @@ export class Session {
453
429
  // ~5s; not worth blocking the panel on it).
454
430
  usage: undefined,
455
431
  contextTokens: this.currentContextTokens(),
432
+ contextLimit: this.contextWindowMax(),
456
433
  cumStats: this.cumStats,
457
434
  lastTurn: this.lastTurnDelta
458
435
  ? {
@@ -536,9 +513,6 @@ export class Session {
536
513
  this.pendingReactionIds.set(msgId, rid)
537
514
  }
538
515
  })()
539
- // Rotation hint: a mid-turn user msg means the next assistant /
540
- // tool event should split the visual into a new card.
541
- this.wantsRotation = true
542
516
  }
543
517
  if (!this.currentTurn && !this.openingTurn && this.initCount >= 1) {
544
518
  // Eager open: this message is going to be processed solo (no current
@@ -853,7 +827,6 @@ export class Session {
853
827
  this.lastUserOpenId = ''
854
828
  this.pendingReactionIds = new Map()
855
829
  this.currentBatchReactionIds = new Map()
856
- this.wantsRotation = false
857
830
  this.initCount = 0
858
831
  this.openingTurn = false
859
832
  this.status = 'stopped'
@@ -884,14 +857,17 @@ export class Session {
884
857
 
885
858
  /** Current context-window occupancy estimate — uses the most recent
886
859
  * assistant `usage` (input + caches), since each assistant reply replays
887
- * the full conversation. Falls back to the last-turn delta when no
888
- * assistant message has streamed yet this process. */
860
+ * the full conversation. Returns 0 when no per-call usage is available
861
+ * (process dead, or fresh spawn before first assistant message);
862
+ * `lastTurnDelta.inputTokens` is the CUMULATIVE turn input across all
863
+ * API calls in the turn (sum of cache_read across N steps) — using it
864
+ * here would inflate the percentage by Nx after a heavy multi-step
865
+ * turn (observed bug 2026-05-16: 417% in the `hi` panel after killing
866
+ * the proc with a long turn's delta still on file). */
889
867
  private currentContextTokens(): number {
890
868
  const u = this.proc?.lastUsage as ClaudeUsage | null | undefined
891
- if (u) {
892
- return (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0)
893
- }
894
- return this.lastTurnDelta?.inputTokens ?? 0
869
+ if (!u) return 0
870
+ return (u.input_tokens ?? 0) + (u.cache_creation_input_tokens ?? 0) + (u.cache_read_input_tokens ?? 0)
895
871
  }
896
872
 
897
873
  /** Context-window capacity for the model the subprocess is currently
@@ -940,44 +916,8 @@ export class Session {
940
916
  // forget here and rely on enqueue source order — that way no `await`
941
917
  // can yield mid-handler and let `closeTurnCard` (or another event) race
942
918
  // and mutate `this.currentTurn` underfoot.
943
- /** Rotate to a fresh card mid-turn: close the in-flight card with a
944
- * `📨 转交新卡` footer (distinct from `✅ done` and `🛑 打断`) and
945
- * open a new card so the post-user-message continuation has a
946
- * visible boundary. Streams that land during the rotation's await
947
- * windows are buffered in `rotationBuffer` and replayed onto the
948
- * new card the moment it's ready, so no tokens are lost across the
949
- * cut. Caller guarantees `wantsRotation` was true sync-immediately
950
- * before. */
951
- private async rotateCard(): Promise<void> {
952
- this.openingTurn = true
953
- try {
954
- await this.closeTurnCard('📨 转交新卡')
955
- await this.openTurnCard('', this.lastUserOpenId, 'user_message')
956
- } finally {
957
- this.openingTurn = false
958
- }
959
- if (this.rotationBuffer.length === 0) return
960
- const buf = this.rotationBuffer
961
- this.rotationBuffer = []
962
- for (const e of buf) {
963
- if (e.kind === 'assistant') this.appendAssistant(e.delta)
964
- else if (e.kind === 'thinking') this.appendThinking(e.delta)
965
- else if (e.kind === 'tool_use') this.addTool(e.id, e.name, e.input)
966
- }
967
- }
968
-
969
919
  private appendAssistant(delta: string): void {
970
- if (!this.currentTurn) {
971
- if (this.openingTurn) this.rotationBuffer.push({ kind: 'assistant', delta })
972
- return
973
- }
974
- // Note: assistant text DOES NOT trigger rotation, even if a mid-turn
975
- // user message landed and set `wantsRotation`. Rotating mid-segment
976
- // would chop the model's in-progress reply (often a response to the
977
- // ORIGINAL prompt that started this card) onto a fresh card,
978
- // visually associating it with the queued msg — which is the bug
979
- // the user surfaced 2026-05-16. The rotation defers to the next
980
- // tool_use, which is a clean section boundary.
920
+ if (!this.currentTurn) return
981
921
  if (!this.currentTurn.currentAssistantSegmentId) {
982
922
  const i = this.currentTurn.assistantSegmentCount++
983
923
  const segId = cards.ELEMENTS.assistant(i)
@@ -1003,12 +943,7 @@ export class Session {
1003
943
  }
1004
944
 
1005
945
  private appendThinking(delta: string): void {
1006
- if (!this.currentTurn) {
1007
- if (this.openingTurn) this.rotationBuffer.push({ kind: 'thinking', delta })
1008
- return
1009
- }
1010
- // Thinking, like assistant text, doesn't trigger rotation — it's
1011
- // preamble to the same response, not a section break.
946
+ if (!this.currentTurn) return
1012
947
  this.currentTurn.thinkingText += delta
1013
948
  cardkit.streamTextThrottled(
1014
949
  this.currentTurn.cardId,
@@ -1026,16 +961,7 @@ export class Session {
1026
961
  }
1027
962
 
1028
963
  private addTool(toolUseId: string, name: string, input: any): void {
1029
- if (!this.currentTurn) {
1030
- if (this.openingTurn) this.rotationBuffer.push({ kind: 'tool_use', id: toolUseId, name, input })
1031
- return
1032
- }
1033
- if (this.wantsRotation) {
1034
- this.wantsRotation = false
1035
- this.rotationBuffer.push({ kind: 'tool_use', id: toolUseId, name, input })
1036
- void this.rotateCard()
1037
- return
1038
- }
964
+ if (!this.currentTurn) return
1039
965
  // Close current assistant segment (if any) so the tool panel renders
1040
966
  // AFTER it in card body order. Flush queues the segment's last
1041
967
  // buffered delta before the tool element is inserted.
@@ -1322,15 +1248,15 @@ export class Session {
1322
1248
  }
1323
1249
  const sendNote = sendPaths.length ? ` · 📎 ${sendPaths.length}` : ''
1324
1250
  // State marker leads the footer (✅ for natural completion, or the
1325
- // suffix verbatim for non-natural states like `📨 转交新卡`). The
1251
+ // suffix verbatim for non-natural states like `🛑 打断`). The
1326
1252
  // trailing "done" word is gone — the ✅ already carries that
1327
1253
  // meaning. User-confirmed footer order 2026-05-16.
1328
1254
  const stateMark = suffix ? suffix : '✅'
1329
1255
  // Per-turn metrics: context-window occupancy (as a real percentage,
1330
1256
  // not a token count) and dollar cost. Only meaningful on a clean
1331
- // close — suffix-tagged turns (rotation / interrupt) didn't fire
1332
- // the `result` event that populates `lastTurnDelta`, so these
1333
- // numbers would be stale and misleading.
1257
+ // close — suffix-tagged turns (interrupt) didn't fire the `result`
1258
+ // event that populates `lastTurnDelta`, so these numbers would be
1259
+ // stale and misleading.
1334
1260
  let metrics = ''
1335
1261
  if (!suffix) {
1336
1262
  const ctxTokens = this.currentContextTokens()