@leviyuan/lodestar 0.2.7 → 0.2.8
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/package.json +1 -1
- package/src/cardkit.ts +12 -7
- package/src/session.ts +31 -13
package/package.json
CHANGED
package/src/cardkit.ts
CHANGED
|
@@ -81,7 +81,12 @@ async function call(method: string, path: string, body?: object): Promise<any> {
|
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
function isStreamingClosed(e: unknown): boolean {
|
|
84
|
-
|
|
84
|
+
if (typeof e !== 'object' || e === null) return false
|
|
85
|
+
const code = (e as any).code
|
|
86
|
+
// 300309 "streaming mode is closed" — TTL already fired before our write.
|
|
87
|
+
// 200850 "card streaming timeout" — TTL fired exactly during our write.
|
|
88
|
+
// Both mean the streaming session is gone and a reopen will unstick the card.
|
|
89
|
+
return code === 300309 || code === 200850
|
|
85
90
|
}
|
|
86
91
|
|
|
87
92
|
/** Reopen streaming_mode on a card that Feishu auto-closed after its
|
|
@@ -98,11 +103,11 @@ async function reopenStreaming(cardId: string): Promise<void> {
|
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
/** Run `op` inside the per-card queue. If it fails with code=300309
|
|
101
|
-
* (Feishu auto-closed streaming after the 10-
|
|
102
|
-
* streaming inline and retry `op` exactly once.
|
|
103
|
-
*
|
|
104
|
-
* swallowed, matching the fire-and-forget contract every
|
|
105
|
-
* already has at the call sites. */
|
|
106
|
+
* or 200850 (Feishu auto-closed / timed-out streaming after the 10-
|
|
107
|
+
* minute TTL), reopen streaming inline and retry `op` exactly once.
|
|
108
|
+
* Anything else — other failure, reopen failure, retry failure — is
|
|
109
|
+
* logged and swallowed, matching the fire-and-forget contract every
|
|
110
|
+
* cardkit op already has at the call sites. */
|
|
106
111
|
async function withReopenOnStreamingClosed(
|
|
107
112
|
cardId: string,
|
|
108
113
|
label: string,
|
|
@@ -118,7 +123,7 @@ async function withReopenOnStreamingClosed(
|
|
|
118
123
|
if (onFailure) onFailure()
|
|
119
124
|
return
|
|
120
125
|
}
|
|
121
|
-
log(`cardkit ${label} ${cardId}: streaming closed (
|
|
126
|
+
log(`cardkit ${label} ${cardId}: streaming closed (code=${(e as any).code}) — reopening`)
|
|
122
127
|
}
|
|
123
128
|
try {
|
|
124
129
|
await reopenStreaming(cardId)
|
package/src/session.ts
CHANGED
|
@@ -118,18 +118,25 @@ export class Session {
|
|
|
118
118
|
private currentTurn: TurnState | null = null
|
|
119
119
|
/** Count of user messages we've written to Claude's stdin since the last
|
|
120
120
|
* turn opened on our side. NOT a FIFO of individual messages — the SDK
|
|
121
|
-
* batch-merges every mid-turn user message into a single
|
|
122
|
-
* once the in-flight turn finishes, so
|
|
123
|
-
* **one** init event per batch
|
|
124
|
-
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
*
|
|
131
|
-
*
|
|
132
|
-
*
|
|
121
|
+
* USUALLY batch-merges every mid-turn user message into a single
|
|
122
|
+
* combined turn once the in-flight turn finishes, so most of the time
|
|
123
|
+
* the daemon observes **one** init event per batch. Tracking a count +
|
|
124
|
+
* last-sender (rather than an Array<msg>) keeps the daemon's view
|
|
125
|
+
* loosely in sync with the SDK's dequeue semantics. Caveat verified
|
|
126
|
+
* 2026-05-17 (test1 accumulator, 8-message rapid-fire): when the first
|
|
127
|
+
* write lands in an idle SDK, that single msg gets its own turn and
|
|
128
|
+
* the rest merge into a second turn — i.e. 1+(N-1) split, not always
|
|
129
|
+
* one merge. To stay coherent with this, `drainMidTurnAndOpen` bumps
|
|
130
|
+
* the count by `batch.length` up front (covering both the first solo
|
|
131
|
+
* turn and the eventual merged tail), and the init handler resets to
|
|
132
|
+
* 0 on the first claim. If the SDK takes the merge path, the bail at
|
|
133
|
+
* `currentTurn=yes` in the init handler leaves pendingCount stale (>0)
|
|
134
|
+
* until the GC at the next `onUserMessage`; if it takes the split
|
|
135
|
+
* path, the second init sees pendingCount>0 and correctly classifies
|
|
136
|
+
* the trailing batch as user-batch (not a scheduled wakeup).
|
|
137
|
+
* Distinguishes user-msg turns from cron-fired scheduled
|
|
138
|
+
* wakeups: count > 0 ⇒ user; count === 0 ⇒ scheduled (and
|
|
139
|
+
* `initCount > 1`). */
|
|
133
140
|
private pendingUserMessageCount = 0
|
|
134
141
|
/** Mid-turn user messages buffered DAEMON-SIDE (not yet sendUserText'd
|
|
135
142
|
* to the SDK). Drained in the `result` handler by writing each to SDK
|
|
@@ -1029,7 +1036,17 @@ export class Session {
|
|
|
1029
1036
|
* calls wake the SDK polling loop (priority="now" semantics) and
|
|
1030
1037
|
* comprise the input for the new turn. Opens the card here rather
|
|
1031
1038
|
* than deferring to init because the init for this batch will arrive
|
|
1032
|
-
* with `currentTurn` already set and bail.
|
|
1039
|
+
* with `currentTurn` already set and bail.
|
|
1040
|
+
*
|
|
1041
|
+
* Each sendUserText also bumps `pendingUserMessageCount`. The SDK
|
|
1042
|
+
* USUALLY collapses our N writes into one merged turn, but **not
|
|
1043
|
+
* always** — empirically observed 2026-05-17, test1 accumulator
|
|
1044
|
+
* session: when the first write lands in an idle SDK (turn just
|
|
1045
|
+
* ended), the SDK eagerly starts a turn for that msg alone, then
|
|
1046
|
+
* merges the rest into a second turn. Without the bump here, that
|
|
1047
|
+
* second turn fires an `init` with `pendingUserMessageCount === 0`
|
|
1048
|
+
* and the init handler misclassifies it as a scheduled wakeup,
|
|
1049
|
+
* painting the `⏰ 触发` banner on what is really a user batch. */
|
|
1033
1050
|
private async drainMidTurnAndOpen(): Promise<void> {
|
|
1034
1051
|
if (this.pendingMidTurnMsgs.length === 0) return
|
|
1035
1052
|
const batch = this.pendingMidTurnMsgs
|
|
@@ -1038,6 +1055,7 @@ export class Session {
|
|
|
1038
1055
|
try {
|
|
1039
1056
|
for (const msg of batch) {
|
|
1040
1057
|
this.proc!.sendUserText(msg.wireText, msg.files)
|
|
1058
|
+
this.pendingUserMessageCount++
|
|
1041
1059
|
if (msg.msgId) {
|
|
1042
1060
|
const rid = this.pendingReactionIds.get(msg.msgId) ?? ''
|
|
1043
1061
|
this.currentBatchReactionIds.set(msg.msgId, rid)
|