lark-remote 0.1.0

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.
Files changed (201) hide show
  1. package/CHANGELOG.md +52 -0
  2. package/LICENSE +21 -0
  3. package/README.en.md +162 -0
  4. package/README.md +161 -0
  5. package/dist/binder.d.ts +57 -0
  6. package/dist/binder.js +105 -0
  7. package/dist/bridge/index.d.ts +338 -0
  8. package/dist/bridge/index.js +1281 -0
  9. package/dist/bridge/queue-manager.d.ts +277 -0
  10. package/dist/bridge/queue-manager.js +729 -0
  11. package/dist/card/bash-card-session.d.ts +78 -0
  12. package/dist/card/bash-card-session.js +174 -0
  13. package/dist/card/bash-renderer.d.ts +24 -0
  14. package/dist/card/bash-renderer.js +187 -0
  15. package/dist/card/card-budget.d.ts +31 -0
  16. package/dist/card/card-budget.js +280 -0
  17. package/dist/card/card-session.d.ts +83 -0
  18. package/dist/card/card-session.js +185 -0
  19. package/dist/card/card-shared.d.ts +28 -0
  20. package/dist/card/card-shared.js +99 -0
  21. package/dist/card/collapsible.d.ts +26 -0
  22. package/dist/card/collapsible.js +87 -0
  23. package/dist/card/run-card-session.d.ts +78 -0
  24. package/dist/card/run-card-session.js +165 -0
  25. package/dist/card/run-renderer.d.ts +66 -0
  26. package/dist/card/run-renderer.js +841 -0
  27. package/dist/card/run-state.d.ts +103 -0
  28. package/dist/card/run-state.js +313 -0
  29. package/dist/card/text-truncate.d.ts +56 -0
  30. package/dist/card/text-truncate.js +184 -0
  31. package/dist/card/time.d.ts +1 -0
  32. package/dist/card/time.js +14 -0
  33. package/dist/card/tool-render.d.ts +25 -0
  34. package/dist/card/tool-render.js +176 -0
  35. package/dist/cli.d.ts +28 -0
  36. package/dist/cli.js +89 -0
  37. package/dist/common/guards.d.ts +37 -0
  38. package/dist/common/guards.js +65 -0
  39. package/dist/config/codex-config.d.ts +113 -0
  40. package/dist/config/codex-config.js +428 -0
  41. package/dist/config/dir.d.ts +17 -0
  42. package/dist/config/dir.js +81 -0
  43. package/dist/config/index.d.ts +219 -0
  44. package/dist/config/index.js +388 -0
  45. package/dist/config/kimi-config.d.ts +33 -0
  46. package/dist/config/kimi-config.js +111 -0
  47. package/dist/config/opencode-config.d.ts +20 -0
  48. package/dist/config/opencode-config.js +124 -0
  49. package/dist/config/pi-config.d.ts +49 -0
  50. package/dist/config/pi-config.js +151 -0
  51. package/dist/config/wizard.d.ts +12 -0
  52. package/dist/config/wizard.js +84 -0
  53. package/dist/connector/file-limits.d.ts +7 -0
  54. package/dist/connector/file-limits.js +8 -0
  55. package/dist/connector/index.d.ts +86 -0
  56. package/dist/connector/index.js +340 -0
  57. package/dist/error-classification.d.ts +15 -0
  58. package/dist/error-classification.js +24 -0
  59. package/dist/index.d.ts +1 -0
  60. package/dist/index.js +580 -0
  61. package/dist/instance-lock.d.ts +30 -0
  62. package/dist/instance-lock.js +184 -0
  63. package/dist/logger/index.d.ts +57 -0
  64. package/dist/logger/index.js +178 -0
  65. package/dist/order/index.d.ts +27 -0
  66. package/dist/order/index.js +90 -0
  67. package/dist/persistence/atomic-write.d.ts +10 -0
  68. package/dist/persistence/atomic-write.js +65 -0
  69. package/dist/persistence/load-json-file.d.ts +9 -0
  70. package/dist/persistence/load-json-file.js +23 -0
  71. package/dist/restart.d.ts +24 -0
  72. package/dist/restart.js +73 -0
  73. package/dist/router/card-helpers.d.ts +28 -0
  74. package/dist/router/card-helpers.js +65 -0
  75. package/dist/router/config/claude.d.ts +14 -0
  76. package/dist/router/config/claude.js +72 -0
  77. package/dist/router/config/codex.d.ts +22 -0
  78. package/dist/router/config/codex.js +100 -0
  79. package/dist/router/config/common/render.d.ts +16 -0
  80. package/dist/router/config/common/render.js +158 -0
  81. package/dist/router/config/index.d.ts +18 -0
  82. package/dist/router/config/index.js +38 -0
  83. package/dist/router/config/kimi.d.ts +21 -0
  84. package/dist/router/config/kimi.js +63 -0
  85. package/dist/router/config/opencode.d.ts +12 -0
  86. package/dist/router/config/opencode.js +44 -0
  87. package/dist/router/config/pi.d.ts +12 -0
  88. package/dist/router/config/pi.js +50 -0
  89. package/dist/router/config/types.d.ts +43 -0
  90. package/dist/router/config/types.js +8 -0
  91. package/dist/router/index.d.ts +318 -0
  92. package/dist/router/index.js +2715 -0
  93. package/dist/router/order-exec-dispatch.d.ts +30 -0
  94. package/dist/router/order-exec-dispatch.js +48 -0
  95. package/dist/router/utils.d.ts +44 -0
  96. package/dist/router/utils.js +116 -0
  97. package/dist/runner/bash/index.d.ts +4 -0
  98. package/dist/runner/bash/index.js +5 -0
  99. package/dist/runner/bash/runner.d.ts +55 -0
  100. package/dist/runner/bash/runner.js +213 -0
  101. package/dist/runner/claude/index.d.ts +4 -0
  102. package/dist/runner/claude/index.js +5 -0
  103. package/dist/runner/claude/runner.d.ts +29 -0
  104. package/dist/runner/claude/runner.js +83 -0
  105. package/dist/runner/codex/argv.d.ts +33 -0
  106. package/dist/runner/codex/argv.js +40 -0
  107. package/dist/runner/codex/index.d.ts +1 -0
  108. package/dist/runner/codex/index.js +2 -0
  109. package/dist/runner/codex/jsonl.d.ts +37 -0
  110. package/dist/runner/codex/jsonl.js +221 -0
  111. package/dist/runner/codex/runner.d.ts +65 -0
  112. package/dist/runner/codex/runner.js +124 -0
  113. package/dist/runner/common/exec-translator.d.ts +73 -0
  114. package/dist/runner/common/exec-translator.js +105 -0
  115. package/dist/runner/common/jsonl-stream.d.ts +39 -0
  116. package/dist/runner/common/jsonl-stream.js +203 -0
  117. package/dist/runner/common/process-stopper.d.ts +28 -0
  118. package/dist/runner/common/process-stopper.js +79 -0
  119. package/dist/runner/common/runner-utils.d.ts +21 -0
  120. package/dist/runner/common/runner-utils.js +65 -0
  121. package/dist/runner/common/spawn-heartbeat.d.ts +33 -0
  122. package/dist/runner/common/spawn-heartbeat.js +51 -0
  123. package/dist/runner/common/spawning-runner.d.ts +321 -0
  124. package/dist/runner/common/spawning-runner.js +634 -0
  125. package/dist/runner/common/usage.d.ts +31 -0
  126. package/dist/runner/common/usage.js +29 -0
  127. package/dist/runner/index.d.ts +19 -0
  128. package/dist/runner/index.js +24 -0
  129. package/dist/runner/kimi/index.d.ts +1 -0
  130. package/dist/runner/kimi/index.js +2 -0
  131. package/dist/runner/kimi/jsonl.d.ts +28 -0
  132. package/dist/runner/kimi/jsonl.js +143 -0
  133. package/dist/runner/kimi/runner.d.ts +50 -0
  134. package/dist/runner/kimi/runner.js +105 -0
  135. package/dist/runner/opencode/argv.d.ts +32 -0
  136. package/dist/runner/opencode/argv.js +35 -0
  137. package/dist/runner/opencode/index.d.ts +1 -0
  138. package/dist/runner/opencode/index.js +2 -0
  139. package/dist/runner/opencode/jsonl.d.ts +68 -0
  140. package/dist/runner/opencode/jsonl.js +295 -0
  141. package/dist/runner/opencode/runner.d.ts +63 -0
  142. package/dist/runner/opencode/runner.js +93 -0
  143. package/dist/runner/pi/index.d.ts +1 -0
  144. package/dist/runner/pi/index.js +2 -0
  145. package/dist/runner/pi/jsonl.d.ts +128 -0
  146. package/dist/runner/pi/jsonl.js +249 -0
  147. package/dist/runner/pi/runner.d.ts +37 -0
  148. package/dist/runner/pi/runner.js +76 -0
  149. package/dist/runner/registry.d.ts +54 -0
  150. package/dist/runner/registry.js +75 -0
  151. package/dist/runner/resolve-agent-choices.d.ts +7 -0
  152. package/dist/runner/resolve-agent-choices.js +76 -0
  153. package/dist/runner/sync-agent-choices.d.ts +6 -0
  154. package/dist/runner/sync-agent-choices.js +73 -0
  155. package/dist/runner/types.d.ts +250 -0
  156. package/dist/runner/types.js +8 -0
  157. package/dist/session/claude/index.d.ts +1 -0
  158. package/dist/session/claude/index.js +2 -0
  159. package/dist/session/claude/session-reader.d.ts +34 -0
  160. package/dist/session/claude/session-reader.js +46 -0
  161. package/dist/session/claude/sessions.d.ts +53 -0
  162. package/dist/session/claude/sessions.js +528 -0
  163. package/dist/session/codex/index.d.ts +1 -0
  164. package/dist/session/codex/index.js +2 -0
  165. package/dist/session/codex/rollout-reader.d.ts +88 -0
  166. package/dist/session/codex/rollout-reader.js +473 -0
  167. package/dist/session/codex/sessions.d.ts +21 -0
  168. package/dist/session/codex/sessions.js +40 -0
  169. package/dist/session/common/catch-up.d.ts +12 -0
  170. package/dist/session/common/catch-up.js +31 -0
  171. package/dist/session/common/constants.d.ts +11 -0
  172. package/dist/session/common/constants.js +12 -0
  173. package/dist/session/common/content-blocks.d.ts +37 -0
  174. package/dist/session/common/content-blocks.js +68 -0
  175. package/dist/session/common/jsonl.d.ts +77 -0
  176. package/dist/session/common/jsonl.js +351 -0
  177. package/dist/session/common/usage-accumulator.d.ts +58 -0
  178. package/dist/session/common/usage-accumulator.js +72 -0
  179. package/dist/session/index.d.ts +7 -0
  180. package/dist/session/index.js +11 -0
  181. package/dist/session/kimi/index.d.ts +1 -0
  182. package/dist/session/kimi/index.js +2 -0
  183. package/dist/session/kimi/sessions.d.ts +81 -0
  184. package/dist/session/kimi/sessions.js +527 -0
  185. package/dist/session/opencode/index.d.ts +1 -0
  186. package/dist/session/opencode/index.js +2 -0
  187. package/dist/session/opencode/sessions.d.ts +81 -0
  188. package/dist/session/opencode/sessions.js +388 -0
  189. package/dist/session/pi/index.d.ts +1 -0
  190. package/dist/session/pi/index.js +2 -0
  191. package/dist/session/pi/sessions.d.ts +27 -0
  192. package/dist/session/pi/sessions.js +399 -0
  193. package/dist/session/registry.d.ts +12 -0
  194. package/dist/session/registry.js +20 -0
  195. package/dist/session/session-store.d.ts +86 -0
  196. package/dist/session/session-store.js +450 -0
  197. package/dist/startup-contact.d.ts +18 -0
  198. package/dist/startup-contact.js +51 -0
  199. package/dist/workspace/index.d.ts +19 -0
  200. package/dist/workspace/index.js +53 -0
  201. package/package.json +93 -0
@@ -0,0 +1,103 @@
1
+ import type { AgentEvent } from '../runner/index.js';
2
+ export type RunTerminal = 'running' | 'finalizing' | 'done' | 'error' | 'interrupted' | 'idle_timeout';
3
+ export type RunFooter = 'thinking' | 'tool_running' | 'streaming' | null;
4
+ type ToolStatus = 'running' | 'ok' | 'error';
5
+ export interface ToolEntry {
6
+ id: string;
7
+ name: string;
8
+ input: unknown;
9
+ /**
10
+ * P3-6: cached parse of `input` (the truncated string) into a record, so
11
+ * `tool-render.ts`'s `asRecord` does not re-`JSON.parse` on every render.
12
+ * Computed once at store time in `reduceAssistantEvent` from the SAME
13
+ * truncated string that the old per-render `asRecord` parsed — so a
14
+ * truncation-broken JSON still yields `null` here (preserving the "over-cap
15
+ * input renders no summary" behavior). `undefined` means "not computed"
16
+ * (ToolEntries built outside the reduce path fall back to parsing `input`).
17
+ */
18
+ parsedInput?: Record<string, unknown> | null;
19
+ output?: string;
20
+ status: ToolStatus;
21
+ startedAt?: string;
22
+ completedAt?: string;
23
+ }
24
+ export type RunBlock = {
25
+ kind: 'thinking';
26
+ content: string;
27
+ active: boolean;
28
+ timestamp?: string;
29
+ } | {
30
+ kind: 'text';
31
+ content: string;
32
+ timestamp?: string;
33
+ } | {
34
+ kind: 'tool';
35
+ tool: ToolEntry;
36
+ } | {
37
+ kind: 'plan';
38
+ content: string;
39
+ active: boolean;
40
+ timestamp?: string;
41
+ } | {
42
+ kind: 'file_change';
43
+ path: string;
44
+ operation: 'create' | 'edit' | 'delete' | 'read';
45
+ diff?: string;
46
+ timestamp?: string;
47
+ };
48
+ export interface RunState {
49
+ runId: string;
50
+ terminal: RunTerminal;
51
+ footer: RunFooter;
52
+ blocks: RunBlock[];
53
+ sessionId?: string;
54
+ resultSubtype?: 'success' | 'error';
55
+ contextLength?: number;
56
+ errorMsg?: string;
57
+ idleTimeoutMinutes?: number;
58
+ compactCount?: number;
59
+ cacheReadTokens?: number;
60
+ cacheCreationTokens?: number;
61
+ /** Agent-declared total; the done card uses max(total, sum of parts). */
62
+ totalTokens?: number;
63
+ /** Real input tokens from the agent's result event (codex/opencode).
64
+ * When present, the done card shows the real value instead of the
65
+ * contextLength-based estimate. */
66
+ inputTokens?: number;
67
+ /** Real output tokens from the agent's result event. */
68
+ outputTokens?: number;
69
+ /** 会话累计 total/input/output(所有 run 之和),Run 卡片展示"累计"。 */
70
+ cumulativeTotalTokens?: number;
71
+ cumulativeInputTokens?: number;
72
+ cumulativeOutputTokens?: number;
73
+ /** 会话累计 cache read token(所有 run 之和)。 */
74
+ cumulativeCacheReadTokens?: number;
75
+ /** 会话累计 cache creation token(所有 run 之和)。 */
76
+ cumulativeCacheCreationTokens?: number;
77
+ plan?: string;
78
+ }
79
+ export interface FinishMeta {
80
+ resultSubtype?: 'success' | 'error';
81
+ contextLength?: number;
82
+ errorMsg?: string;
83
+ idleTimeoutMinutes?: number;
84
+ compactCount?: number;
85
+ cacheReadTokens?: number;
86
+ cacheCreationTokens?: number;
87
+ /** Agent-declared total; threaded to RunState for the done card. */
88
+ totalTokens?: number;
89
+ /** Real input/output tokens from the agent's result event. Threaded through
90
+ * to RunState so the done card can display real values instead of estimates. */
91
+ inputTokens?: number;
92
+ outputTokens?: number;
93
+ /** 会话累计 total/input/output(所有 run 之和),从 session jsonl 读取。 */
94
+ cumulativeTotalTokens?: number;
95
+ cumulativeInputTokens?: number;
96
+ cumulativeOutputTokens?: number;
97
+ cumulativeCacheReadTokens?: number;
98
+ cumulativeCacheCreationTokens?: number;
99
+ }
100
+ export declare function createInitialRunState(runId: string): RunState;
101
+ export declare function reduceRunState(state: RunState, event: AgentEvent): RunState;
102
+ export declare function finishRun(state: RunState, terminal: Exclude<RunTerminal, 'running' | 'finalizing'>, meta?: FinishMeta): RunState;
103
+ export {};
@@ -0,0 +1,313 @@
1
+ const MAX_REASONING_CHARS = 4_000;
2
+ const MAX_TEXT_CHARS = 12_000;
3
+ const MAX_TOOL_DETAIL_CHARS = 3_000;
4
+ const MAX_BLOCKS = 24;
5
+ export function createInitialRunState(runId) {
6
+ return {
7
+ runId,
8
+ terminal: 'running',
9
+ footer: 'thinking',
10
+ blocks: [],
11
+ };
12
+ }
13
+ export function reduceRunState(state, event) {
14
+ // 只有终态才阻止事件处理。finalizing 是非终态,
15
+ // 仍需累积 assistant 内容(thinking、text、tool_use、后台任务输出)。
16
+ const terminal = state.terminal;
17
+ if (terminal === 'done' ||
18
+ terminal === 'error' ||
19
+ terminal === 'interrupted' ||
20
+ terminal === 'idle_timeout') {
21
+ return state;
22
+ }
23
+ if (event.type === 'system')
24
+ return reduceSystemEvent(state, event);
25
+ if (event.type === 'result')
26
+ return reduceResultEvent(state, event);
27
+ if (event.type === 'assistant')
28
+ return reduceAssistantEvent(state, event);
29
+ if (event.type === 'user')
30
+ return reduceToolResultEvent(state, event);
31
+ if (event.type === 'plan')
32
+ return reducePlanEvent(state, event);
33
+ if (event.type === 'file_change')
34
+ return reduceFileChangeEvent(state, event);
35
+ return state;
36
+ }
37
+ /** Handle system init and compact_boundary events. */
38
+ function reduceSystemEvent(state, event) {
39
+ if (event.type !== 'system')
40
+ return state;
41
+ if (event.subtype === 'init') {
42
+ return { ...state, sessionId: event.session_id, footer: 'thinking' };
43
+ }
44
+ if (event.subtype === 'compact_boundary' && event.compactMetadata) {
45
+ return {
46
+ ...state,
47
+ contextLength: event.compactMetadata.postTokens,
48
+ compactCount: (state.compactCount ?? 0) + 1,
49
+ };
50
+ }
51
+ return state;
52
+ }
53
+ /** Handle result event - transition to finalizing (non-terminal).
54
+ *
55
+ * 进程退出是唯一终态触发源。result 事件只暂存 resultSubtype +
56
+ * errorMsg(终态判定必需),不暂存 usage(由 bridge 局部变量捕获)。
57
+ * 合并语义:多个 result 事件(agent 自身 + SpawningRunner.buildResultEvent 合成)
58
+ * - resultSubtype: error 优先(任一为 error 即 error)
59
+ * - errorMsg: 首个非 undefined 胜出(agent 的更具体)
60
+ */
61
+ function reduceResultEvent(state, event) {
62
+ if (event.type !== 'result')
63
+ return state;
64
+ const resultEvent = event;
65
+ const incomingSubtype = event.subtype;
66
+ const incomingErrorMsg = incomingSubtype === 'error' ? (resultEvent.errorMessage ?? 'Agent 返回错误结果') : undefined;
67
+ // 合并 subtype:error 优先(任一为 error 即 error)
68
+ const mergedSubtype = state.resultSubtype === 'error' || incomingSubtype === 'error' ? 'error' : 'success';
69
+ // 合并 errorMsg:首个非 undefined 胜出(existing ?? incoming)
70
+ const mergedErrorMsg = state.errorMsg ?? incomingErrorMsg;
71
+ return {
72
+ ...state,
73
+ terminal: 'finalizing',
74
+ footer: null,
75
+ blocks: markThinkingInactive(state.blocks),
76
+ resultSubtype: mergedSubtype,
77
+ errorMsg: mergedErrorMsg,
78
+ };
79
+ }
80
+ /** Handle assistant thinking/text/tool_use content blocks. */
81
+ function reduceAssistantEvent(state, event) {
82
+ if (event.type !== 'assistant')
83
+ return state;
84
+ let next = state;
85
+ for (const content of event.message.content) {
86
+ if (content.type === 'thinking') {
87
+ const last = next.blocks[next.blocks.length - 1];
88
+ const newBlocks = last?.kind === 'thinking'
89
+ ? [
90
+ ...next.blocks.slice(0, -1),
91
+ {
92
+ kind: 'thinking',
93
+ content: keepLatest(last.content + '\n' + content.thinking, MAX_REASONING_CHARS),
94
+ active: true,
95
+ timestamp: last.timestamp ?? event.timestamp,
96
+ },
97
+ ]
98
+ : [
99
+ ...next.blocks,
100
+ {
101
+ kind: 'thinking',
102
+ content: keepLatest(content.thinking, MAX_REASONING_CHARS),
103
+ active: true,
104
+ timestamp: event.timestamp,
105
+ },
106
+ ];
107
+ next = {
108
+ ...next,
109
+ blocks: keepLatestBlocks(newBlocks),
110
+ footer: 'thinking',
111
+ };
112
+ }
113
+ else if (content.type === 'text') {
114
+ const blocks = markThinkingInactive(next.blocks);
115
+ const last = blocks[blocks.length - 1];
116
+ const newBlocks = last?.kind === 'text'
117
+ ? [
118
+ ...blocks.slice(0, -1),
119
+ {
120
+ kind: 'text',
121
+ content: keepLatest(last.content + content.text, MAX_TEXT_CHARS),
122
+ timestamp: last.timestamp ?? event.timestamp,
123
+ },
124
+ ]
125
+ : [
126
+ ...blocks,
127
+ {
128
+ kind: 'text',
129
+ content: keepLatest(content.text, MAX_TEXT_CHARS),
130
+ timestamp: event.timestamp,
131
+ },
132
+ ];
133
+ next = {
134
+ ...next,
135
+ blocks: keepLatestBlocks(newBlocks),
136
+ footer: 'streaming',
137
+ };
138
+ }
139
+ else {
140
+ // P3-6: parse the truncated stored string ONCE here (not on every render)
141
+ // and cache the result on `parsedInput`. tool-render's asRecord then hits
142
+ // the object branch with zero parsing. The parse uses the SAME truncated
143
+ // string as the old per-render parse, so truncation-broken JSON still
144
+ // yields null (over-cap input renders no summary — behavior preserved).
145
+ const inputStr = truncateDetail(stringifyUnknown(content.input));
146
+ next = {
147
+ ...next,
148
+ blocks: keepLatestBlocks([
149
+ ...markThinkingInactive(next.blocks),
150
+ {
151
+ kind: 'tool',
152
+ tool: {
153
+ id: content.id,
154
+ name: content.name,
155
+ input: inputStr,
156
+ parsedInput: tryParseRecord(inputStr),
157
+ status: 'running',
158
+ startedAt: event.timestamp,
159
+ },
160
+ },
161
+ ]),
162
+ footer: 'tool_running',
163
+ };
164
+ }
165
+ }
166
+ return next;
167
+ }
168
+ /** Handle tool_result content blocks (user message with tool results).
169
+ * P3-4: rebuild ONLY the block matching `tool_use_id`, copying every other
170
+ * block by reference. The old `.map()` recreated all block objects per
171
+ * tool_result (O(N) allocations on the run-card hot path). Footer still
172
+ * advances to 'streaming' on every content block (parity with old behavior);
173
+ * only the blocks array is preserved by reference when nothing matches. */
174
+ function reduceToolResultEvent(state, event) {
175
+ if (event.type !== 'user')
176
+ return state;
177
+ let next = state;
178
+ for (const content of event.message.content) {
179
+ const targetId = content.tool_use_id;
180
+ const matchIndex = next.blocks.findIndex((block) => block.kind === 'tool' && block.tool.id === targetId);
181
+ if (matchIndex === -1) {
182
+ if (next.footer !== 'streaming')
183
+ next = { ...next, footer: 'streaming' };
184
+ continue;
185
+ }
186
+ const matched = next.blocks[matchIndex];
187
+ const updated = {
188
+ kind: 'tool',
189
+ tool: {
190
+ ...matched.tool,
191
+ output: truncateDetail(stringifyUnknown(content.content)),
192
+ status: content.is_error ? 'error' : 'ok',
193
+ completedAt: event.timestamp,
194
+ },
195
+ };
196
+ const blocks = next.blocks.slice();
197
+ blocks[matchIndex] = updated;
198
+ next = { ...next, blocks, footer: 'streaming' };
199
+ }
200
+ return next;
201
+ }
202
+ /** Handle plan event — accumulate plan text for Codex-style agents. */
203
+ function reducePlanEvent(state, event) {
204
+ if (event.type !== 'plan')
205
+ return state;
206
+ const currentPlan = state.plan ?? '';
207
+ const newPlan = keepLatest(currentPlan + '\n' + event.plan, MAX_REASONING_CHARS * 2);
208
+ // Plan is a single evolving document — replace the existing plan block
209
+ // instead of appending a new one (avoids N overlapping blocks after N events).
210
+ const withoutOldPlan = state.blocks.filter((b) => b.kind !== 'plan');
211
+ const newBlocks = [
212
+ ...withoutOldPlan,
213
+ { kind: 'plan', content: newPlan, active: true, timestamp: event.timestamp },
214
+ ];
215
+ return {
216
+ ...state,
217
+ plan: newPlan,
218
+ blocks: keepLatestBlocks(newBlocks),
219
+ };
220
+ }
221
+ /** Handle file_change event — emit a file_change block for rendering (Codex-style agents). */
222
+ function reduceFileChangeEvent(state, event) {
223
+ if (event.type !== 'file_change')
224
+ return state;
225
+ // Add file_change block for rendering (collapsed by default)
226
+ const newBlocks = [
227
+ ...state.blocks,
228
+ {
229
+ kind: 'file_change',
230
+ path: event.path,
231
+ operation: event.operation,
232
+ diff: event.diff,
233
+ timestamp: event.timestamp,
234
+ },
235
+ ];
236
+ return {
237
+ ...state,
238
+ blocks: keepLatestBlocks(newBlocks),
239
+ };
240
+ }
241
+ export function finishRun(state, terminal, meta = {}) {
242
+ // 允许从 running 或 finalizing finish(/stop、idle watchdog、进程退出 finally)。
243
+ // finalizing 是非终态(result 已收到,进程未退出),可被 finish 转终态。
244
+ // 如果已经是终态,仍然允许补充 meta 信息(如 usage 等,bridge finally 的
245
+ // else-if-sawResult 分支用此路径补充 token 统计)。
246
+ if (state.terminal !== 'running' && state.terminal !== 'finalizing') {
247
+ // Already terminal: apply meta fields if provided (e.g. usage from jsonl)
248
+ const hasMeta = Object.keys(meta).some((k) => meta[k] !== undefined);
249
+ if (!hasMeta)
250
+ return state;
251
+ return { ...state, ...sanitizeMeta(meta) };
252
+ }
253
+ return {
254
+ ...state,
255
+ terminal,
256
+ footer: null,
257
+ blocks: markThinkingInactive(state.blocks),
258
+ ...sanitizeMeta(meta),
259
+ };
260
+ }
261
+ /** Remove undefined/null meta fields to avoid overwriting existing state with nullish values. */
262
+ function sanitizeMeta(meta) {
263
+ const result = {};
264
+ for (const [k, v] of Object.entries(meta)) {
265
+ if (v !== undefined && v !== null)
266
+ result[k] = v;
267
+ }
268
+ return result;
269
+ }
270
+ /** Mark all thinking blocks in the array as inactive. */
271
+ function markThinkingInactive(blocks) {
272
+ return blocks.map((b) => (b.kind === 'thinking' && b.active ? { ...b, active: false } : b));
273
+ }
274
+ function stringifyUnknown(value) {
275
+ if (typeof value === 'string')
276
+ return value;
277
+ try {
278
+ return JSON.stringify(value);
279
+ }
280
+ catch {
281
+ return String(value);
282
+ }
283
+ }
284
+ /**
285
+ * P3-6: parse a (possibly truncated) string into a record, mirroring what
286
+ * `tool-render.ts`'s `asRecord` did on every render. Returns `null` when the
287
+ * string is empty, non-JSON, or parses to a non-object — so a truncation-broken
288
+ * JSON yields `null` exactly as the old per-render parse did. Computed once at
289
+ * store time and cached on `ToolEntry.parsedInput`.
290
+ */
291
+ function tryParseRecord(input) {
292
+ if (!input)
293
+ return null;
294
+ try {
295
+ const p = JSON.parse(input);
296
+ return p && typeof p === 'object' ? p : null;
297
+ }
298
+ catch {
299
+ return null;
300
+ }
301
+ }
302
+ function keepLatest(value, maxChars) {
303
+ return value.length <= maxChars ? value : value.slice(-maxChars);
304
+ }
305
+ function keepLatestBlocks(blocks) {
306
+ return blocks.length <= MAX_BLOCKS ? blocks : blocks.slice(-MAX_BLOCKS);
307
+ }
308
+ function truncateDetail(value) {
309
+ return value.length <= MAX_TOOL_DETAIL_CHARS
310
+ ? value
311
+ : `${value.slice(0, MAX_TOOL_DETAIL_CHARS)}…(已截断)`;
312
+ }
313
+ //# sourceMappingURL=run-state.js.map
@@ -0,0 +1,56 @@
1
+ /**
2
+ * UTF-8 byte-safe text truncation primitives.
3
+ *
4
+ * Shared by the live run renderer (`run-renderer.ts`), the static card budget
5
+ * enforcer (`card-budget.ts`), and the opencode session reader
6
+ * (`src/session/opencode/sessions.ts`) so that huge tool outputs are folded at a single
7
+ * place instead of being reimplemented per consumer.
8
+ *
9
+ * Why byte-safe: Feishu card size limits are measured in UTF-8 bytes, and a
10
+ * naive `str.slice(0, n)` can split a multi-byte CJK/emoji codepoint, yielding
11
+ * invalid UTF-8. `fitUtf8` iterates by Unicode codepoint (via `Array.from` /
12
+ * `for...of`) so it never breaks a surrogate pair or a multi-byte sequence.
13
+ */
14
+ /** Feishu 11310 limit: max markdown tables per card. */
15
+ export declare const FEISHU_MAX_TABLES = 5;
16
+ /** Default truncation suffix, shared across all consumers for UX consistency. */
17
+ export declare const DEFAULT_TRUNCATE_SUFFIX = "\u2026\uFF08\u5DF2\u622A\u65AD\uFF09";
18
+ /**
19
+ * Max byte budget for a single session `tool_result` event's content when
20
+ * replaying an opencode session (L2 pre-fold). The static card budget enforcer
21
+ * (`enforceCardBudget`, maxPanelContentBytes = 2000) would truncate it again at
22
+ * render time anyway; pre-folding here bounds the in-memory `events[]` array
23
+ * and the intermediate card JSON so a single pathological tool_result (e.g. a
24
+ * 500KB file listing) never inflates the replay payload to megabytes.
25
+ */
26
+ export declare const TOOL_RESULT_MAX_BYTES = 4000;
27
+ /**
28
+ * Truncate a UTF-8 string to fit within `maxBytes`, appending `suffix`
29
+ * (default `…(已截断)`) when truncation occurs. When `fromEnd` is true, keeps
30
+ * the tail and prepends the suffix (useful for "show the most recent N bytes").
31
+ */
32
+ export declare function truncateUtf8(value: string, maxBytes: number, fromEnd?: boolean, suffix?: string): string;
33
+ /**
34
+ * Count the number of markdown tables in a string.
35
+ *
36
+ * A markdown table is identified by a separator line matching `/^\|[-: |]+\$/m`
37
+ * (i.e. a line that starts with `|` and contains only `|`, `-`, `:`, spaces).
38
+ * Each separator line corresponds to exactly one table.
39
+ */
40
+ export declare function countMarkdownTables(text: string): number;
41
+ /**
42
+ * Truncate markdown tables in a string to at most `maxTables`, keeping the
43
+ * **newest** (last) tables and removing the oldest ones.
44
+ *
45
+ * Strategy: find all table separator lines, determine which tables to remove
46
+ * (those whose separator is among the oldest `count - maxTables`), then remove
47
+ * each such table entirely (header row + separator row + data rows).
48
+ *
49
+ * A "table block" is a contiguous run of lines where the first line is the
50
+ * header row, the second is the separator row (`|---|`), and subsequent lines
51
+ * are data rows. We remove entire table blocks from the text.
52
+ *
53
+ * When tables are removed, a hint is inserted at the removal point indicating
54
+ * how many earlier tables were omitted.
55
+ */
56
+ export declare function truncateMarkdownTables(text: string, maxTables?: number): string;
@@ -0,0 +1,184 @@
1
+ /**
2
+ * UTF-8 byte-safe text truncation primitives.
3
+ *
4
+ * Shared by the live run renderer (`run-renderer.ts`), the static card budget
5
+ * enforcer (`card-budget.ts`), and the opencode session reader
6
+ * (`src/session/opencode/sessions.ts`) so that huge tool outputs are folded at a single
7
+ * place instead of being reimplemented per consumer.
8
+ *
9
+ * Why byte-safe: Feishu card size limits are measured in UTF-8 bytes, and a
10
+ * naive `str.slice(0, n)` can split a multi-byte CJK/emoji codepoint, yielding
11
+ * invalid UTF-8. `fitUtf8` iterates by Unicode codepoint (via `Array.from` /
12
+ * `for...of`) so it never breaks a surrogate pair or a multi-byte sequence.
13
+ */
14
+ /** Feishu 11310 limit: max markdown tables per card. */
15
+ export const FEISHU_MAX_TABLES = 5;
16
+ /** Default truncation suffix, shared across all consumers for UX consistency. */
17
+ export const DEFAULT_TRUNCATE_SUFFIX = '…(已截断)';
18
+ /**
19
+ * Max byte budget for a single session `tool_result` event's content when
20
+ * replaying an opencode session (L2 pre-fold). The static card budget enforcer
21
+ * (`enforceCardBudget`, maxPanelContentBytes = 2000) would truncate it again at
22
+ * render time anyway; pre-folding here bounds the in-memory `events[]` array
23
+ * and the intermediate card JSON so a single pathological tool_result (e.g. a
24
+ * 500KB file listing) never inflates the replay payload to megabytes.
25
+ */
26
+ export const TOOL_RESULT_MAX_BYTES = 4000;
27
+ /**
28
+ * Fit a UTF-8 string into `maxBytes` by iterating Unicode codepoints, never
29
+ * splitting a multi-byte sequence. When `fromEnd` is true, keeps the TAIL
30
+ * (useful for logs/tails); otherwise keeps the HEAD.
31
+ *
32
+ * P1-2 (2026-08-02): 单遍累计单字符字节数,替代逐字符 `Buffer.byteLength(result +
33
+ * char)` + `result += char`(后者每轮新建并扫描不断增长的字符串,O(budget²))。
34
+ * 字节长度对拼接可加(byteLength(a+b) = byteLength(a) + byteLength(b)),故累计
35
+ * 与旧实现的逐字符判断逐字节等价;最后一次 slice/join 物化结果。混合 CJK/emoji
36
+ * 样本实测 ~150× 提速(33.7ms/次 → 0.2ms/次,见 tests/anchor/priority/p0-fit-utf8.test.ts)。
37
+ */
38
+ function fitUtf8(value, maxBytes, fromEnd = false) {
39
+ if (fromEnd) {
40
+ // tail 分支:需要从末尾找截断起点,Array.from 一次物化(旧实现同样 Array.from().reverse(),
41
+ // 无内存回归);数组仅用于定位,之后 slice+join。
42
+ const codepoints = Array.from(value);
43
+ let bytes = 0;
44
+ let start = codepoints.length;
45
+ for (let i = codepoints.length - 1; i >= 0; i--) {
46
+ const cb = Buffer.byteLength(codepoints[i], 'utf8');
47
+ if (bytes + cb > maxBytes)
48
+ break;
49
+ bytes += cb;
50
+ start = i;
51
+ }
52
+ return codepoints.slice(start).join('');
53
+ }
54
+ // head 分支:惰性 for-of 迭代 + 字符串索引累计(旧实现同 O(1) 内存)。
55
+ // 不物化数组——opencode-sessions 会对 MB 级 tool_result 做 head 截断,
56
+ // Array.from 全量分配会引入不必要的瞬态内存。
57
+ let bytes = 0;
58
+ let end = 0;
59
+ for (const char of value) {
60
+ const cb = Buffer.byteLength(char, 'utf8');
61
+ if (bytes + cb > maxBytes)
62
+ break;
63
+ bytes += cb;
64
+ end += char.length; // for-of 产出完整码点;char.length 为 1 或 2(代理对),
65
+ // end 落在码点边界上,value.slice(0, end) 不会切分代理对
66
+ }
67
+ return value.slice(0, end);
68
+ }
69
+ /**
70
+ * Truncate a UTF-8 string to fit within `maxBytes`, appending `suffix`
71
+ * (default `…(已截断)`) when truncation occurs. When `fromEnd` is true, keeps
72
+ * the tail and prepends the suffix (useful for "show the most recent N bytes").
73
+ */
74
+ export function truncateUtf8(value, maxBytes, fromEnd = false, suffix = DEFAULT_TRUNCATE_SUFFIX) {
75
+ if (Buffer.byteLength(value, 'utf8') <= maxBytes)
76
+ return value;
77
+ const suffixBytes = Buffer.byteLength(suffix, 'utf8');
78
+ const budget = maxBytes - suffixBytes;
79
+ if (budget <= 0)
80
+ return suffix;
81
+ const truncated = fitUtf8(value, budget, fromEnd);
82
+ return fromEnd ? `${suffix}\n${truncated}` : `${truncated}${suffix}`;
83
+ }
84
+ /**
85
+ * Count the number of markdown tables in a string.
86
+ *
87
+ * A markdown table is identified by a separator line matching `/^\|[-: |]+\$/m`
88
+ * (i.e. a line that starts with `|` and contains only `|`, `-`, `:`, spaces).
89
+ * Each separator line corresponds to exactly one table.
90
+ */
91
+ export function countMarkdownTables(text) {
92
+ let count = 0;
93
+ for (const line of text.split('\n')) {
94
+ if (/^\|[-: |]+$/.test(line.trim())) {
95
+ count++;
96
+ }
97
+ }
98
+ return count;
99
+ }
100
+ /**
101
+ * Truncate markdown tables in a string to at most `maxTables`, keeping the
102
+ * **newest** (last) tables and removing the oldest ones.
103
+ *
104
+ * Strategy: find all table separator lines, determine which tables to remove
105
+ * (those whose separator is among the oldest `count - maxTables`), then remove
106
+ * each such table entirely (header row + separator row + data rows).
107
+ *
108
+ * A "table block" is a contiguous run of lines where the first line is the
109
+ * header row, the second is the separator row (`|---|`), and subsequent lines
110
+ * are data rows. We remove entire table blocks from the text.
111
+ *
112
+ * When tables are removed, a hint is inserted at the removal point indicating
113
+ * how many earlier tables were omitted.
114
+ */
115
+ export function truncateMarkdownTables(text, maxTables = FEISHU_MAX_TABLES) {
116
+ if (maxTables <= 0)
117
+ return text;
118
+ const tableCount = countMarkdownTables(text);
119
+ if (tableCount <= maxTables)
120
+ return text;
121
+ const lines = text.split('\n');
122
+ // Step 1: Find all table separator line indices
123
+ const separatorIndices = [];
124
+ for (let i = 0; i < lines.length; i++) {
125
+ if (/^\|[-: |]+$/.test(lines[i].trim())) {
126
+ separatorIndices.push(i);
127
+ }
128
+ }
129
+ const blocks = [];
130
+ for (const sepIdx of separatorIndices) {
131
+ // Header is the line just before the separator
132
+ let start = sepIdx - 1;
133
+ if (start < 0)
134
+ continue; // malformed table, skip
135
+ // Expand start upward to include preceding heading (### ...) and blank lines
136
+ // that belong to this table block. Stop at the first non-blank, non-heading line
137
+ // or at the start of the text.
138
+ while (start > 0) {
139
+ const prevLine = lines[start - 1].trim();
140
+ if (prevLine === '' || /^#{1,6}\s/.test(prevLine)) {
141
+ start--;
142
+ }
143
+ else {
144
+ break;
145
+ }
146
+ }
147
+ // Data rows: lines after separator that start with '|'
148
+ let end = sepIdx + 1;
149
+ while (end < lines.length && /^\|/.test(lines[end].trim())) {
150
+ end++;
151
+ }
152
+ blocks.push({ start, end, separatorIdx: sepIdx });
153
+ }
154
+ // Step 3: Determine which blocks to remove (oldest first)
155
+ const toRemoveCount = blocks.length - maxTables;
156
+ // blocks are in document order (oldest first), so remove from the front
157
+ const blocksToRemove = new Set(blocks.slice(0, toRemoveCount));
158
+ // Map: start line index → block (for lookup when processing lines)
159
+ const blockByStart = new Map();
160
+ for (const b of blocks) {
161
+ blockByStart.set(b.start, b);
162
+ }
163
+ // Step 4: Rebuild the text, skipping removed table blocks and inserting hints
164
+ const result = [];
165
+ let i = 0;
166
+ let removedSoFar = 0;
167
+ while (i < lines.length) {
168
+ const block = blockByStart.get(i);
169
+ if (block && blocksToRemove.has(block)) {
170
+ // Skip the entire table block
171
+ removedSoFar++;
172
+ i = block.end;
173
+ // Insert hint at the removal point
174
+ if (removedSoFar === toRemoveCount) {
175
+ result.push(`_💡 前 ${toRemoveCount} 个表格已省略_`);
176
+ }
177
+ continue;
178
+ }
179
+ result.push(lines[i]);
180
+ i++;
181
+ }
182
+ return result.join('\n');
183
+ }
184
+ //# sourceMappingURL=text-truncate.js.map
@@ -0,0 +1 @@
1
+ export declare function formatTimestamp(ts: string | number | undefined): string;
@@ -0,0 +1,14 @@
1
+ export function formatTimestamp(ts) {
2
+ if (!ts)
3
+ return '';
4
+ const d = new Date(ts);
5
+ if (isNaN(d.getTime()))
6
+ return '';
7
+ const yyyy = d.getFullYear();
8
+ const mm = String(d.getMonth() + 1).padStart(2, '0');
9
+ const dd = String(d.getDate()).padStart(2, '0');
10
+ const hh = String(d.getHours()).padStart(2, '0');
11
+ const mi = String(d.getMinutes()).padStart(2, '0');
12
+ return `${yyyy}-${mm}-${dd} ${hh}:${mi}`;
13
+ }
14
+ //# sourceMappingURL=time.js.map