codeksei 0.1.0 → 0.1.1

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 (68) hide show
  1. package/LICENSE +661 -661
  2. package/README.en.md +109 -47
  3. package/README.md +79 -58
  4. package/bin/cyberboss.js +1 -1
  5. package/package.json +86 -86
  6. package/scripts/open_shared_wechat_thread.sh +77 -77
  7. package/scripts/open_wechat_thread.sh +108 -108
  8. package/scripts/shared-common.js +144 -144
  9. package/scripts/shared-open.js +14 -14
  10. package/scripts/shared-start.js +5 -5
  11. package/scripts/shared-status.js +27 -27
  12. package/scripts/show_shared_status.sh +45 -45
  13. package/scripts/start_shared_app_server.sh +52 -52
  14. package/scripts/start_shared_wechat.sh +94 -94
  15. package/scripts/timeline-screenshot.sh +14 -14
  16. package/src/adapters/channel/weixin/account-store.js +99 -99
  17. package/src/adapters/channel/weixin/api-v2.js +50 -50
  18. package/src/adapters/channel/weixin/api.js +169 -169
  19. package/src/adapters/channel/weixin/context-token-store.js +84 -84
  20. package/src/adapters/channel/weixin/index.js +618 -604
  21. package/src/adapters/channel/weixin/legacy.js +579 -566
  22. package/src/adapters/channel/weixin/media-mime.js +22 -22
  23. package/src/adapters/channel/weixin/media-receive.js +370 -370
  24. package/src/adapters/channel/weixin/media-send.js +102 -102
  25. package/src/adapters/channel/weixin/message-utils-v2.js +282 -282
  26. package/src/adapters/channel/weixin/message-utils.js +199 -199
  27. package/src/adapters/channel/weixin/redact.js +41 -41
  28. package/src/adapters/channel/weixin/reminder-queue-store.js +101 -101
  29. package/src/adapters/channel/weixin/sync-buffer-store.js +35 -35
  30. package/src/adapters/runtime/codex/events.js +215 -215
  31. package/src/adapters/runtime/codex/index.js +109 -104
  32. package/src/adapters/runtime/codex/message-utils.js +95 -95
  33. package/src/adapters/runtime/codex/model-catalog.js +106 -106
  34. package/src/adapters/runtime/codex/protocol-leak-monitor.js +75 -75
  35. package/src/adapters/runtime/codex/rpc-client.js +339 -339
  36. package/src/adapters/runtime/codex/session-store.js +286 -286
  37. package/src/app/channel-send-file-cli.js +57 -57
  38. package/src/app/diary-write-cli.js +236 -88
  39. package/src/app/note-sync-cli.js +2 -2
  40. package/src/app/reminder-write-cli.js +215 -210
  41. package/src/app/review-cli.js +7 -5
  42. package/src/app/system-checkin-poller.js +64 -64
  43. package/src/app/system-send-cli.js +129 -129
  44. package/src/app/timeline-event-cli.js +28 -25
  45. package/src/app/timeline-screenshot-cli.js +103 -100
  46. package/src/core/app.js +1763 -1763
  47. package/src/core/branding.js +2 -1
  48. package/src/core/command-registry.js +381 -369
  49. package/src/core/config.js +30 -14
  50. package/src/core/default-targets.js +163 -163
  51. package/src/core/durable-note-schema.js +9 -8
  52. package/src/core/instructions-template.js +17 -16
  53. package/src/core/note-sync.js +8 -7
  54. package/src/core/path-utils.js +54 -0
  55. package/src/core/project-radar.js +11 -10
  56. package/src/core/review.js +48 -50
  57. package/src/core/stream-delivery.js +1162 -983
  58. package/src/core/system-message-dispatcher.js +68 -68
  59. package/src/core/system-message-queue-store.js +128 -128
  60. package/src/core/thread-state-store.js +96 -96
  61. package/src/core/timeline-screenshot-queue-store.js +134 -134
  62. package/src/core/timezone.js +436 -0
  63. package/src/core/workspace-bootstrap.js +9 -1
  64. package/src/index.js +148 -146
  65. package/src/integrations/timeline/index.js +130 -74
  66. package/src/integrations/timeline/state-sync.js +240 -0
  67. package/templates/weixin-instructions.md +12 -38
  68. package/templates/weixin-operations.md +29 -31
@@ -1,990 +1,1169 @@
1
- const crypto = require("crypto");
2
- const { sanitizeProtocolLeakText } = require("../adapters/runtime/codex/protocol-leak-monitor");
3
- const { normalizeAssistantPhase } = require("../adapters/runtime/codex/message-utils");
4
-
5
- const RECENT_WEIXIN_DELIVERY_TTL_MS = 30_000;
6
- const STREAM_PROGRESS_MAX_CHARS = 120;
7
- const STREAM_PROGRESS_MAX_LINES = 2;
8
-
9
- class StreamDelivery {
10
- constructor({
11
- channelAdapter,
12
- sessionStore,
13
- weixinReplyMode = "settled",
14
- deliveryTraceEnabled = false,
15
- onDeliveryFailure = null,
16
- }) {
17
- this.channelAdapter = channelAdapter;
18
- this.sessionStore = sessionStore;
19
- this.weixinReplyMode = normalizeWeixinReplyMode(weixinReplyMode);
20
- this.deliveryTraceEnabled = Boolean(deliveryTraceEnabled);
21
- this.onDeliveryFailure = typeof onDeliveryFailure === "function" ? onDeliveryFailure : null;
22
- this.replyTargetByBindingKey = new Map();
23
- this.pendingReplyTargetsByThreadId = new Map();
24
- this.stateByRunKey = new Map();
25
- this.ignoredRunKeys = new Set();
26
- this.recentSettledWeixinDeliveries = new Map();
27
- }
28
-
29
- setReplyTarget(bindingKey, target) {
30
- if (!bindingKey || !target?.userId || !target?.contextToken) {
31
- return;
32
- }
33
- this.replyTargetByBindingKey.set(bindingKey, {
34
- userId: String(target.userId).trim(),
35
- contextToken: String(target.contextToken).trim(),
36
- provider: normalizeText(target.provider),
37
- });
38
- }
39
-
40
- queueReplyTargetForThread(threadId, target) {
41
- const normalizedThreadId = normalizeText(threadId);
42
- if (!normalizedThreadId || !target?.userId || !target?.contextToken) {
43
- return;
44
- }
45
- const queue = this.pendingReplyTargetsByThreadId.get(normalizedThreadId) || [];
46
- queue.push({
47
- userId: String(target.userId).trim(),
48
- contextToken: String(target.contextToken).trim(),
49
- provider: normalizeText(target.provider),
50
- });
51
- this.pendingReplyTargetsByThreadId.set(normalizedThreadId, queue);
52
- }
53
-
54
- async handleRuntimeEvent(event) {
55
- const threadId = normalizeText(event?.payload?.threadId);
56
- const turnId = normalizeText(event?.payload?.turnId);
57
- if (!threadId) {
58
- return;
59
- }
60
- const ignoredRunKey = turnId ? buildRunKey(threadId, turnId) : "";
61
- if (ignoredRunKey && this.ignoredRunKeys.has(ignoredRunKey)) {
62
- if (event.type === "runtime.turn.completed" || event.type === "runtime.turn.failed") {
63
- this.ignoredRunKeys.delete(ignoredRunKey);
64
- }
65
- return;
66
- }
67
-
68
- switch (event.type) {
69
- case "runtime.turn.started": {
70
- const state = this.ensureRunState(threadId, turnId);
71
- state.turnId = turnId || state.turnId;
72
- this.attachReplyTarget(state);
73
- return;
74
- }
75
- case "runtime.reply.delta": {
76
- const state = this.ensureRunState(threadId, turnId);
77
- this.upsertItem(state, {
78
- itemId: normalizeText(event.payload.itemId) || `item-${state.itemOrder.length + 1}`,
79
- text: normalizeLineEndings(event.payload.text),
80
- completed: false,
81
- phase: normalizeAssistantPhase(event.payload.phase),
82
- });
83
- return;
84
- }
85
- case "runtime.reply.completed": {
86
- const state = this.ensureRunState(threadId, turnId);
87
- const itemId = normalizeText(event.payload.itemId) || `item-${state.itemOrder.length + 1}`;
88
- const phase = normalizeAssistantPhase(event.payload.phase);
89
- this.upsertItem(state, {
90
- itemId,
91
- text: normalizeLineEndings(event.payload.text),
92
- completed: true,
93
- phase,
94
- });
95
- await this.flush(state, {
96
- force: false,
97
- trigger: {
98
- source: event.type,
99
- itemId,
100
- phase,
101
- },
102
- });
103
- return;
104
- }
105
- case "runtime.turn.completed": {
106
- const state = this.ensureRunState(threadId, turnId);
107
- state.turnId = turnId || state.turnId;
108
- await this.flush(state, {
109
- force: true,
110
- trigger: { source: event.type },
111
- });
112
- this.disposeRunState(state.runKey);
113
- return;
114
- }
115
- case "runtime.turn.failed":
116
- this.disposeRunState(buildRunKey(threadId, turnId));
117
- return;
118
- default:
119
- return;
120
- }
121
- }
122
-
123
- async finishTurn({ threadId, finalText }) {
124
- const normalizedThreadId = normalizeText(threadId);
125
- const normalizedFinalText = normalizeLineEndings(finalText);
126
- if (!normalizedThreadId || !normalizedFinalText) {
127
- return;
128
- }
129
-
130
- const state = this.ensureRunState(normalizedThreadId, "");
131
- this.attachReplyTarget(state);
132
- if (!state.itemOrder.length) {
133
- this.upsertItem(state, {
134
- itemId: "final",
135
- text: normalizedFinalText,
136
- completed: true,
137
- });
138
- } else {
139
- const itemId = state.itemOrder[state.itemOrder.length - 1] || "final";
140
- this.setItemText(state, itemId, normalizedFinalText, true);
141
- for (const candidateId of state.itemOrder) {
142
- const item = state.items.get(candidateId);
143
- if (item) {
144
- item.currentText = item.completedText || item.currentText;
145
- item.completed = true;
146
- }
147
- }
148
- }
149
-
150
- await this.flush(state, {
151
- force: true,
152
- trigger: {
153
- source: "finishTurn",
154
- itemId: state.itemOrder[state.itemOrder.length - 1] || "final",
155
- },
156
- });
157
- this.disposeRunState(state.runKey);
158
- }
159
-
160
- async finalizeAbandonedTurn({ threadId, turnId = "", trailingText = "" }) {
161
- const normalizedThreadId = normalizeText(threadId);
162
- const normalizedTurnId = normalizeText(turnId);
163
- const normalizedTrailingText = normalizeLineEndings(trailingText).trim();
164
- if (!normalizedThreadId) {
165
- return;
166
- }
167
-
168
- const state = this.findRunState(normalizedThreadId, normalizedTurnId);
169
- if (!state) {
170
- // If this exact turn is already gone, do not resurrect a new pending run
171
- // just to send the watchdog suffix again. That creates duplicate "tail"
172
- // messages after delivery failure or other local terminal states.
173
- if (normalizedTurnId) {
174
- return;
175
- }
176
- if (normalizedTrailingText) {
177
- await this.finishTurn({
178
- threadId: normalizedThreadId,
179
- finalText: normalizedTrailingText,
180
- });
181
- }
182
- return;
183
- }
184
-
185
- this.attachReplyTarget(state);
186
- if (normalizedTrailingText) {
187
- this.upsertItem(state, {
188
- itemId: "__watchdog__",
189
- text: normalizedTrailingText,
190
- completed: true,
191
- });
192
- }
193
- await this.flush(state, {
194
- force: true,
195
- trigger: {
196
- source: "finalizeAbandonedTurn",
197
- itemId: normalizedTrailingText ? "__watchdog__" : "",
198
- },
199
- });
200
- this.ignoredRunKeys.add(state.runKey);
201
- this.disposeRunState(state.runKey);
202
- }
203
-
204
- ensureRunState(threadId, turnId = "") {
205
- const runKey = buildRunKey(threadId, turnId);
206
- const existing = this.stateByRunKey.get(runKey);
207
- if (existing) {
208
- return existing;
209
- }
210
-
211
- const created = {
212
- runKey,
213
- threadId,
214
- bindingKey: "",
215
- replyTarget: null,
216
- turnId: normalizeText(turnId),
217
- itemOrder: [],
218
- items: new Map(),
219
- weixinReplyMode: this.weixinReplyMode,
220
- sentText: "",
221
- sendChain: Promise.resolve(),
222
- flushPromise: null,
223
- };
224
- this.stateByRunKey.set(runKey, created);
225
- this.attachReplyTarget(created);
226
- return created;
227
- }
228
-
229
- findRunState(threadId, turnId = "") {
230
- const normalizedThreadId = normalizeText(threadId);
231
- const normalizedTurnId = normalizeText(turnId);
232
- if (!normalizedThreadId) {
233
- return null;
234
- }
235
- if (normalizedTurnId) {
236
- const exact = this.stateByRunKey.get(buildRunKey(normalizedThreadId, normalizedTurnId));
237
- if (exact) {
238
- return exact;
239
- }
240
- }
241
- const pending = this.stateByRunKey.get(buildRunKey(normalizedThreadId, ""));
242
- if (pending && (!normalizedTurnId || !pending.turnId || pending.turnId === normalizedTurnId)) {
243
- return pending;
244
- }
245
- for (const candidate of this.stateByRunKey.values()) {
246
- if (candidate.threadId !== normalizedThreadId) {
247
- continue;
248
- }
249
- if (!normalizedTurnId || candidate.turnId === normalizedTurnId) {
250
- return candidate;
251
- }
252
- }
253
- return null;
254
- }
255
-
256
- attachReplyTarget(state) {
257
- if (!state.replyTarget) {
258
- const queue = this.pendingReplyTargetsByThreadId.get(state.threadId) || [];
259
- if (queue.length) {
260
- state.replyTarget = queue.shift();
261
- if (queue.length) {
262
- this.pendingReplyTargetsByThreadId.set(state.threadId, queue);
263
- } else {
264
- this.pendingReplyTargetsByThreadId.delete(state.threadId);
265
- }
266
- }
267
- }
268
- const linked = this.sessionStore.findBindingForThreadId(state.threadId);
269
- if (!linked?.bindingKey) {
270
- return;
271
- }
272
- state.bindingKey = linked.bindingKey;
273
- if (!state.replyTarget) {
274
- const target = this.replyTargetByBindingKey.get(linked.bindingKey);
275
- state.replyTarget = target;
276
- }
277
- }
278
-
279
- upsertItem(state, { itemId, text, completed, phase = "" }) {
280
- if (!text) {
281
- return;
282
- }
283
- if (!state.items.has(itemId)) {
284
- state.itemOrder.push(itemId);
285
- state.items.set(itemId, {
286
- currentText: "",
287
- completedText: "",
288
- completed: false,
289
- phase: "",
290
- });
291
- }
292
-
293
- const current = state.items.get(itemId);
294
- const normalizedPhase = normalizeAssistantPhase(phase);
295
- if (normalizedPhase) {
296
- current.phase = normalizedPhase;
297
- }
298
- if (completed) {
299
- const merged = mergeCompletedItemText(current.currentText, text);
300
- current.currentText = merged;
301
- current.completedText = merged;
302
- current.completed = true;
303
- return;
304
- }
305
-
306
- current.currentText = appendStreamingText(current.currentText, text);
307
- }
308
-
309
- setItemText(state, itemId, text, completed) {
310
- if (!text) {
311
- return;
312
- }
313
- if (!state.items.has(itemId)) {
314
- state.itemOrder.push(itemId);
315
- state.items.set(itemId, {
316
- currentText: "",
317
- completedText: "",
318
- completed: false,
319
- phase: "",
320
- });
321
- }
322
-
323
- const current = state.items.get(itemId);
324
- current.currentText = text;
325
- if (completed) {
326
- current.completedText = text;
327
- }
328
- current.completed = Boolean(completed);
329
- }
330
-
331
- async flush(state, { force, trigger = null }) {
332
- const previous = state.flushPromise || Promise.resolve();
333
- const current = previous
334
- .catch(() => {})
335
- .then(() => this.flushNow(state, { force, trigger }));
336
- const tracked = current.finally(() => {
337
- const latestState = this.stateByRunKey.get(state.runKey);
338
- if (latestState && latestState.flushPromise === tracked) {
339
- latestState.flushPromise = null;
340
- }
341
- });
342
- state.flushPromise = tracked;
343
- await tracked;
344
- }
345
-
346
- async flushNow(state, { force, trigger = null }) {
347
- if (!state.replyTarget) {
348
- return;
349
- }
350
- // WeChat reply mode is explicit. `settled` waits for the terminal snapshot;
351
- // `stream` ships user-visible assistant message blocks as they complete.
352
- // Keep the modes distinct here so future "optimize the stream" tweaks do
353
- // not accidentally reintroduce token-level spam or repeated block sends.
354
- if (!force && prefersSettledDelivery(state)) {
355
- return;
356
- }
357
-
358
- const plainText = markdownToPlainText(buildReplyText(state, {
359
- completedOnly: !force,
360
- preferLatestMessage: prefersSettledDelivery(state),
361
- force,
362
- }));
363
- const sanitized = sanitizeReplyText(state.replyTarget, plainText);
364
- if (sanitized.suppress) {
365
- state.sentText = sanitized.text;
366
- console.log(
1
+ const crypto = require("crypto");
2
+ const { sanitizeProtocolLeakText } = require("../adapters/runtime/codex/protocol-leak-monitor");
3
+ const { normalizeAssistantPhase } = require("../adapters/runtime/codex/message-utils");
4
+
5
+ const RECENT_WEIXIN_DELIVERY_TTL_MS = 30_000;
6
+ const STREAM_PROGRESS_MAX_CHARS = 120;
7
+ const STREAM_PROGRESS_MAX_LINES = 2;
8
+ const STREAM_SNAPSHOT_REPLACEMENT_MIN_CHARS = 40;
9
+
10
+ class StreamDelivery {
11
+ constructor({
12
+ channelAdapter,
13
+ sessionStore,
14
+ weixinReplyMode = "settled",
15
+ deliveryTraceEnabled = false,
16
+ onDeliveryFailure = null,
17
+ }) {
18
+ this.channelAdapter = channelAdapter;
19
+ this.sessionStore = sessionStore;
20
+ this.weixinReplyMode = normalizeWeixinReplyMode(weixinReplyMode);
21
+ this.deliveryTraceEnabled = Boolean(deliveryTraceEnabled);
22
+ this.onDeliveryFailure = typeof onDeliveryFailure === "function" ? onDeliveryFailure : null;
23
+ this.replyTargetByBindingKey = new Map();
24
+ this.pendingReplyTargetsByThreadId = new Map();
25
+ this.stateByRunKey = new Map();
26
+ this.ignoredRunKeys = new Set();
27
+ this.recentSettledWeixinDeliveries = new Map();
28
+ }
29
+
30
+ setReplyTarget(bindingKey, target) {
31
+ if (!bindingKey || !target?.userId || !target?.contextToken) {
32
+ return;
33
+ }
34
+ this.replyTargetByBindingKey.set(bindingKey, {
35
+ userId: String(target.userId).trim(),
36
+ contextToken: String(target.contextToken).trim(),
37
+ provider: normalizeText(target.provider),
38
+ });
39
+ }
40
+
41
+ queueReplyTargetForThread(threadId, target) {
42
+ const normalizedThreadId = normalizeText(threadId);
43
+ if (!normalizedThreadId || !target?.userId || !target?.contextToken) {
44
+ return;
45
+ }
46
+ const queue = this.pendingReplyTargetsByThreadId.get(normalizedThreadId) || [];
47
+ queue.push({
48
+ userId: String(target.userId).trim(),
49
+ contextToken: String(target.contextToken).trim(),
50
+ provider: normalizeText(target.provider),
51
+ });
52
+ this.pendingReplyTargetsByThreadId.set(normalizedThreadId, queue);
53
+ }
54
+
55
+ async handleRuntimeEvent(event) {
56
+ const threadId = normalizeText(event?.payload?.threadId);
57
+ const turnId = normalizeText(event?.payload?.turnId);
58
+ if (!threadId) {
59
+ return;
60
+ }
61
+ const ignoredRunKey = turnId ? buildRunKey(threadId, turnId) : "";
62
+ if (ignoredRunKey && this.ignoredRunKeys.has(ignoredRunKey)) {
63
+ if (event.type === "runtime.turn.completed" || event.type === "runtime.turn.failed") {
64
+ this.ignoredRunKeys.delete(ignoredRunKey);
65
+ }
66
+ return;
67
+ }
68
+
69
+ switch (event.type) {
70
+ case "runtime.turn.started": {
71
+ this.disposeSupersededAbandonedRuns(threadId, turnId);
72
+ const state = this.ensureRunState(threadId, turnId);
73
+ state.turnId = turnId || state.turnId;
74
+ state.abandonedAt = 0;
75
+ this.attachReplyTarget(state);
76
+ return;
77
+ }
78
+ case "runtime.reply.delta": {
79
+ const state = this.ensureRunState(threadId, turnId);
80
+ state.abandonedAt = 0;
81
+ this.upsertItem(state, {
82
+ itemId: normalizeText(event.payload.itemId) || `item-${state.itemOrder.length + 1}`,
83
+ text: normalizeLineEndings(event.payload.text),
84
+ completed: false,
85
+ phase: normalizeAssistantPhase(event.payload.phase),
86
+ });
87
+ return;
88
+ }
89
+ case "runtime.reply.completed": {
90
+ const state = this.ensureRunState(threadId, turnId);
91
+ state.abandonedAt = 0;
92
+ const itemId = normalizeText(event.payload.itemId) || `item-${state.itemOrder.length + 1}`;
93
+ const phase = normalizeAssistantPhase(event.payload.phase);
94
+ this.upsertItem(state, {
95
+ itemId,
96
+ text: normalizeLineEndings(event.payload.text),
97
+ completed: true,
98
+ phase,
99
+ });
100
+ await this.flush(state, {
101
+ force: false,
102
+ trigger: {
103
+ source: event.type,
104
+ itemId,
105
+ phase,
106
+ },
107
+ });
108
+ return;
109
+ }
110
+ case "runtime.turn.completed": {
111
+ const state = this.ensureRunState(threadId, turnId);
112
+ state.turnId = turnId || state.turnId;
113
+ state.abandonedAt = 0;
114
+ await this.flush(state, {
115
+ force: true,
116
+ trigger: { source: event.type },
117
+ });
118
+ this.disposeRunState(state.runKey);
119
+ return;
120
+ }
121
+ case "runtime.turn.failed":
122
+ this.disposeRunState(buildRunKey(threadId, turnId));
123
+ return;
124
+ default:
125
+ return;
126
+ }
127
+ }
128
+
129
+ async finishTurn({ threadId, finalText }) {
130
+ const normalizedThreadId = normalizeText(threadId);
131
+ const normalizedFinalText = normalizeLineEndings(finalText);
132
+ if (!normalizedThreadId || !normalizedFinalText) {
133
+ return;
134
+ }
135
+
136
+ const state = this.ensureRunState(normalizedThreadId, "");
137
+ this.attachReplyTarget(state);
138
+ if (!state.itemOrder.length) {
139
+ this.upsertItem(state, {
140
+ itemId: "final",
141
+ text: normalizedFinalText,
142
+ completed: true,
143
+ });
144
+ } else {
145
+ const itemId = state.itemOrder[state.itemOrder.length - 1] || "final";
146
+ this.setItemText(state, itemId, normalizedFinalText, true);
147
+ for (const candidateId of state.itemOrder) {
148
+ const item = state.items.get(candidateId);
149
+ if (item) {
150
+ item.currentText = item.completedText || item.currentText;
151
+ item.completed = true;
152
+ }
153
+ }
154
+ }
155
+
156
+ await this.flush(state, {
157
+ force: true,
158
+ trigger: {
159
+ source: "finishTurn",
160
+ itemId: state.itemOrder[state.itemOrder.length - 1] || "final",
161
+ },
162
+ });
163
+ this.disposeRunState(state.runKey);
164
+ }
165
+
166
+ async finalizeAbandonedTurn({ threadId, turnId = "", trailingText = "" }) {
167
+ const normalizedThreadId = normalizeText(threadId);
168
+ const normalizedTurnId = normalizeText(turnId);
169
+ const normalizedTrailingText = normalizeLineEndings(trailingText).trim();
170
+ if (!normalizedThreadId) {
171
+ return;
172
+ }
173
+
174
+ const state = this.findRunState(normalizedThreadId, normalizedTurnId);
175
+ if (!state) {
176
+ // If this exact turn is already gone, do not resurrect a new pending run
177
+ // just to send the watchdog suffix again. That creates duplicate "tail"
178
+ // messages after delivery failure or other local terminal states.
179
+ if (normalizedTurnId) {
180
+ return;
181
+ }
182
+ if (normalizedTrailingText) {
183
+ await this.finishTurn({
184
+ threadId: normalizedThreadId,
185
+ finalText: normalizedTrailingText,
186
+ });
187
+ }
188
+ return;
189
+ }
190
+
191
+ this.attachReplyTarget(state);
192
+ if (normalizedTrailingText) {
193
+ this.upsertItem(state, {
194
+ itemId: "__watchdog__",
195
+ text: normalizedTrailingText,
196
+ completed: true,
197
+ });
198
+ }
199
+ await this.flush(state, {
200
+ force: true,
201
+ trigger: {
202
+ source: "finalizeAbandonedTurn",
203
+ itemId: normalizedTrailingText ? "__watchdog__" : "",
204
+ },
205
+ });
206
+ removeStateItem(state, "__watchdog__");
207
+ state.sentText = buildCurrentSafeReplyText(state, { force: true });
208
+ state.abandonedAt = Date.now();
209
+ }
210
+
211
+ ensureRunState(threadId, turnId = "") {
212
+ const runKey = buildRunKey(threadId, turnId);
213
+ const existing = this.stateByRunKey.get(runKey);
214
+ if (existing) {
215
+ return existing;
216
+ }
217
+
218
+ const created = {
219
+ runKey,
220
+ threadId,
221
+ bindingKey: "",
222
+ replyTarget: null,
223
+ turnId: normalizeText(turnId),
224
+ itemOrder: [],
225
+ items: new Map(),
226
+ weixinReplyMode: this.weixinReplyMode,
227
+ sentText: "",
228
+ sendChain: Promise.resolve(),
229
+ flushPromise: null,
230
+ abandonedAt: 0,
231
+ };
232
+ this.stateByRunKey.set(runKey, created);
233
+ this.attachReplyTarget(created);
234
+ return created;
235
+ }
236
+
237
+ findRunState(threadId, turnId = "") {
238
+ const normalizedThreadId = normalizeText(threadId);
239
+ const normalizedTurnId = normalizeText(turnId);
240
+ if (!normalizedThreadId) {
241
+ return null;
242
+ }
243
+ if (normalizedTurnId) {
244
+ const exact = this.stateByRunKey.get(buildRunKey(normalizedThreadId, normalizedTurnId));
245
+ if (exact) {
246
+ return exact;
247
+ }
248
+ }
249
+ const pending = this.stateByRunKey.get(buildRunKey(normalizedThreadId, ""));
250
+ if (pending && (!normalizedTurnId || !pending.turnId || pending.turnId === normalizedTurnId)) {
251
+ return pending;
252
+ }
253
+ for (const candidate of this.stateByRunKey.values()) {
254
+ if (candidate.threadId !== normalizedThreadId) {
255
+ continue;
256
+ }
257
+ if (!normalizedTurnId || candidate.turnId === normalizedTurnId) {
258
+ return candidate;
259
+ }
260
+ }
261
+ return null;
262
+ }
263
+
264
+ attachReplyTarget(state) {
265
+ if (!state.replyTarget) {
266
+ const queue = this.pendingReplyTargetsByThreadId.get(state.threadId) || [];
267
+ if (queue.length) {
268
+ state.replyTarget = queue.shift();
269
+ if (queue.length) {
270
+ this.pendingReplyTargetsByThreadId.set(state.threadId, queue);
271
+ } else {
272
+ this.pendingReplyTargetsByThreadId.delete(state.threadId);
273
+ }
274
+ }
275
+ }
276
+ const linked = this.sessionStore.findBindingForThreadId(state.threadId);
277
+ if (!linked?.bindingKey) {
278
+ return;
279
+ }
280
+ state.bindingKey = linked.bindingKey;
281
+ if (!state.replyTarget) {
282
+ const target = this.replyTargetByBindingKey.get(linked.bindingKey);
283
+ state.replyTarget = target;
284
+ }
285
+ }
286
+
287
+ upsertItem(state, { itemId, text, completed, phase = "" }) {
288
+ if (!text) {
289
+ return;
290
+ }
291
+ if (!state.items.has(itemId)) {
292
+ state.itemOrder.push(itemId);
293
+ state.items.set(itemId, {
294
+ currentText: "",
295
+ completedText: "",
296
+ completed: false,
297
+ phase: "",
298
+ });
299
+ }
300
+
301
+ const current = state.items.get(itemId);
302
+ const normalizedPhase = normalizeAssistantPhase(phase);
303
+ if (normalizedPhase) {
304
+ current.phase = normalizedPhase;
305
+ }
306
+ if (completed) {
307
+ const merged = mergeCompletedItemText(current.currentText, text);
308
+ current.currentText = merged;
309
+ current.completedText = merged;
310
+ current.completed = true;
311
+ return;
312
+ }
313
+
314
+ current.currentText = appendStreamingText(current.currentText, text);
315
+ }
316
+
317
+ setItemText(state, itemId, text, completed) {
318
+ if (!text) {
319
+ return;
320
+ }
321
+ if (!state.items.has(itemId)) {
322
+ state.itemOrder.push(itemId);
323
+ state.items.set(itemId, {
324
+ currentText: "",
325
+ completedText: "",
326
+ completed: false,
327
+ phase: "",
328
+ });
329
+ }
330
+
331
+ const current = state.items.get(itemId);
332
+ current.currentText = text;
333
+ if (completed) {
334
+ current.completedText = text;
335
+ }
336
+ current.completed = Boolean(completed);
337
+ }
338
+
339
+ async flush(state, { force, trigger = null }) {
340
+ const previous = state.flushPromise || Promise.resolve();
341
+ const current = previous
342
+ .catch(() => {})
343
+ .then(() => this.flushNow(state, { force, trigger }));
344
+ const tracked = current.finally(() => {
345
+ const latestState = this.stateByRunKey.get(state.runKey);
346
+ if (latestState && latestState.flushPromise === tracked) {
347
+ latestState.flushPromise = null;
348
+ }
349
+ });
350
+ state.flushPromise = tracked;
351
+ await tracked;
352
+ }
353
+
354
+ async flushNow(state, { force, trigger = null }) {
355
+ this.attachReplyTarget(state);
356
+ if (!state.replyTarget) {
357
+ return;
358
+ }
359
+ // WeChat reply mode is explicit. `settled` waits for the terminal snapshot;
360
+ // `stream` ships user-visible assistant message blocks as they complete.
361
+ // Keep the modes distinct here so future "optimize the stream" tweaks do
362
+ // not accidentally reintroduce token-level spam or repeated block sends.
363
+ if (!force && prefersSettledDelivery(state)) {
364
+ return;
365
+ }
366
+
367
+ const plainText = markdownToPlainText(buildReplyText(state, {
368
+ completedOnly: !force,
369
+ preferLatestMessage: prefersSettledDelivery(state),
370
+ force,
371
+ }));
372
+ const sanitized = sanitizeReplyText(state.replyTarget, plainText);
373
+ if (sanitized.suppress) {
374
+ state.sentText = sanitized.text;
375
+ console.log(
367
376
  `[codeksei] suppressed system reply `
368
- + `thread=${state.threadId} turn=${state.turnId || "(pending)"} `
369
- + `preview=${JSON.stringify(plainText.slice(0, 80))}`
370
- );
371
- return;
372
- }
373
- const safeText = sanitized.text;
374
- if (!safeText || safeText === state.sentText) {
375
- return;
376
- }
377
-
378
- if (state.sentText && !safeText.startsWith(state.sentText)) {
377
+ + `thread=${state.threadId} turn=${state.turnId || "(pending)"} `
378
+ + `preview=${JSON.stringify(plainText.slice(0, 80))}`
379
+ );
380
+ return;
381
+ }
382
+ const safeText = sanitized.text;
383
+ if (!safeText || safeText === state.sentText) {
384
+ return;
385
+ }
386
+
387
+ if (state.sentText && !safeText.startsWith(state.sentText)) {
379
388
  console.warn(`[codeksei] skip non-monotonic reply thread=${state.threadId}`);
380
- return;
381
- }
382
-
383
- const delta = normalizeDeliveryDelta(
384
- safeText.slice(state.sentText.length),
385
- { streaming: prefersStreamingDelivery(state) }
386
- );
387
- if (!delta) {
388
- return;
389
- }
390
-
391
- if (!delta.trim()) {
392
- state.sentText = safeText;
393
- return;
394
- }
395
-
396
- const deliveryDedupKey = buildSettledWeixinDeliveryKey(state, safeText);
397
- if (deliveryDedupKey && this.wasRecentlyDelivered(deliveryDedupKey)) {
398
- state.sentText = safeText;
389
+ return;
390
+ }
391
+
392
+ const delta = normalizeDeliveryDelta(
393
+ safeText.slice(state.sentText.length),
394
+ { streaming: prefersStreamingDelivery(state) }
395
+ );
396
+ if (!delta) {
397
+ return;
398
+ }
399
+
400
+ if (!delta.trim()) {
401
+ state.sentText = safeText;
402
+ return;
403
+ }
404
+
405
+ const deliveryDedupKey = buildSettledWeixinDeliveryKey(state, safeText);
406
+ if (deliveryDedupKey && this.wasRecentlyDelivered(deliveryDedupKey)) {
407
+ state.sentText = safeText;
399
408
  console.warn(`[codeksei] suppress duplicate weixin delivery thread=${state.threadId}`);
400
- return;
401
- }
402
-
403
- const settledWechatDelivery = prefersSettledDelivery(state);
404
- const tracePayload = buildDeliveryTracePayload(state, {
405
- force,
406
- trigger,
407
- traceId: this.deliveryTraceEnabled ? crypto.randomUUID().slice(0, 8) : "",
408
- safeText,
409
- delta,
410
- });
411
- state.sendChain = state.sendChain.then(async () => {
412
- this.logDeliveryTrace("attempt", tracePayload);
413
- await this.channelAdapter.sendText({
414
- userId: state.replyTarget.userId,
415
- text: delta,
416
- contextToken: state.replyTarget.contextToken,
417
- preserveBlock: settledWechatDelivery,
418
- trace: this.deliveryTraceEnabled
419
- ? {
420
- ...tracePayload,
421
- origin: "stream-delivery",
422
- preserveBlock: settledWechatDelivery,
423
- }
424
- : null,
425
- });
426
- state.sentText = safeText;
427
- this.logDeliveryTrace("delivered", tracePayload);
428
- if (deliveryDedupKey) {
429
- this.rememberRecentDelivery(deliveryDedupKey);
430
- console.log(
409
+ return;
410
+ }
411
+
412
+ const settledWechatDelivery = prefersSettledDelivery(state);
413
+ const tracePayload = buildDeliveryTracePayload(state, {
414
+ force,
415
+ trigger,
416
+ traceId: this.deliveryTraceEnabled ? crypto.randomUUID().slice(0, 8) : "",
417
+ safeText,
418
+ delta,
419
+ });
420
+ state.sendChain = state.sendChain.then(async () => {
421
+ this.logDeliveryTrace("attempt", tracePayload);
422
+ await this.channelAdapter.sendText({
423
+ userId: state.replyTarget.userId,
424
+ text: delta,
425
+ contextToken: state.replyTarget.contextToken,
426
+ preserveBlock: settledWechatDelivery,
427
+ trace: this.deliveryTraceEnabled
428
+ ? {
429
+ ...tracePayload,
430
+ origin: "stream-delivery",
431
+ preserveBlock: settledWechatDelivery,
432
+ }
433
+ : null,
434
+ });
435
+ state.sentText = safeText;
436
+ this.logDeliveryTrace("delivered", tracePayload);
437
+ if (deliveryDedupKey) {
438
+ this.rememberRecentDelivery(deliveryDedupKey);
439
+ console.log(
431
440
  `[codeksei] delivered weixin reply `
432
- + `thread=${state.threadId} turn=${state.turnId || "(pending)"} `
433
- + `chars=${safeText.length} hash=${hashReplyText(safeText)}`
434
- );
435
- }
436
- }).catch((error) => {
437
- this.logDeliveryTrace("failed", tracePayload, error);
441
+ + `thread=${state.threadId} turn=${state.turnId || "(pending)"} `
442
+ + `chars=${safeText.length} hash=${hashReplyText(safeText)}`
443
+ );
444
+ }
445
+ }).catch((error) => {
446
+ this.logDeliveryTrace("failed", tracePayload, error);
438
447
  console.error(`[codeksei] failed to deliver reply thread=${state.threadId}: ${error.message}`);
439
- this.handleDeliveryFailure(state, error);
440
- });
441
-
442
- await state.sendChain;
443
- }
444
-
445
- handleDeliveryFailure(state, error) {
446
- if (!state?.runKey) {
447
- return;
448
- }
449
- // Once WeChat delivery has exhausted its retries for this run, continuing
450
- // to stream later deltas only creates repeated send failures and fake typing.
451
- this.ignoredRunKeys.add(state.runKey);
452
- this.disposeRunState(state.runKey);
453
- if (!this.onDeliveryFailure) {
454
- return;
455
- }
456
- Promise.resolve(this.onDeliveryFailure({
457
- threadId: state.threadId,
458
- turnId: state.turnId,
459
- bindingKey: state.bindingKey,
460
- error,
461
- sentText: state.sentText,
462
- replyTarget: state.replyTarget ? { ...state.replyTarget } : null,
463
- })).catch((callbackError) => {
448
+ this.handleDeliveryFailure(state, error);
449
+ });
450
+
451
+ await state.sendChain;
452
+ }
453
+
454
+ handleDeliveryFailure(state, error) {
455
+ if (!state?.runKey) {
456
+ return;
457
+ }
458
+ // Once WeChat delivery has exhausted its retries for this run, continuing
459
+ // to stream later deltas only creates repeated send failures and fake typing.
460
+ this.ignoredRunKeys.add(state.runKey);
461
+ this.disposeRunState(state.runKey);
462
+ if (!this.onDeliveryFailure) {
463
+ return;
464
+ }
465
+ Promise.resolve(this.onDeliveryFailure({
466
+ threadId: state.threadId,
467
+ turnId: state.turnId,
468
+ bindingKey: state.bindingKey,
469
+ error,
470
+ sentText: state.sentText,
471
+ replyTarget: state.replyTarget ? { ...state.replyTarget } : null,
472
+ })).catch((callbackError) => {
464
473
  console.error(`[codeksei] delivery failure callback crashed thread=${state.threadId}: ${callbackError.message}`);
465
- });
466
- }
467
-
468
- disposeRunState(runKey) {
469
- const normalizedRunKey = normalizeText(runKey);
470
- if (!normalizedRunKey) {
471
- return;
472
- }
473
- this.stateByRunKey.delete(normalizedRunKey);
474
- }
475
-
476
- wasRecentlyDelivered(key) {
477
- this.pruneRecentDeliveries();
478
- const deliveredAt = this.recentSettledWeixinDeliveries.get(key);
479
- return Number.isFinite(deliveredAt) && (Date.now() - deliveredAt) <= RECENT_WEIXIN_DELIVERY_TTL_MS;
480
- }
481
-
482
- rememberRecentDelivery(key) {
483
- this.pruneRecentDeliveries();
484
- this.recentSettledWeixinDeliveries.set(key, Date.now());
485
- }
486
-
487
- pruneRecentDeliveries() {
488
- const now = Date.now();
489
- for (const [key, deliveredAt] of this.recentSettledWeixinDeliveries.entries()) {
490
- if (!Number.isFinite(deliveredAt) || (now - deliveredAt) > RECENT_WEIXIN_DELIVERY_TTL_MS) {
491
- this.recentSettledWeixinDeliveries.delete(key);
492
- }
493
- }
494
- }
495
-
496
- logDeliveryTrace(stage, payload, error = null) {
497
- if (!this.deliveryTraceEnabled || !payload) {
498
- return;
499
- }
500
- const parts = [
474
+ });
475
+ }
476
+
477
+ disposeSupersededAbandonedRuns(threadId, activeTurnId = "") {
478
+ const normalizedThreadId = normalizeText(threadId);
479
+ const normalizedActiveTurnId = normalizeText(activeTurnId);
480
+ if (!normalizedThreadId) {
481
+ return;
482
+ }
483
+ for (const candidate of this.stateByRunKey.values()) {
484
+ if (candidate.threadId !== normalizedThreadId || !candidate.abandonedAt) {
485
+ continue;
486
+ }
487
+ if (normalizedActiveTurnId && candidate.turnId === normalizedActiveTurnId) {
488
+ continue;
489
+ }
490
+ this.ignoredRunKeys.add(candidate.runKey);
491
+ this.disposeRunState(candidate.runKey);
492
+ }
493
+ }
494
+
495
+ disposeRunState(runKey) {
496
+ const normalizedRunKey = normalizeText(runKey);
497
+ if (!normalizedRunKey) {
498
+ return;
499
+ }
500
+ this.stateByRunKey.delete(normalizedRunKey);
501
+ }
502
+
503
+ wasRecentlyDelivered(key) {
504
+ this.pruneRecentDeliveries();
505
+ const deliveredAt = this.recentSettledWeixinDeliveries.get(key);
506
+ return Number.isFinite(deliveredAt) && (Date.now() - deliveredAt) <= RECENT_WEIXIN_DELIVERY_TTL_MS;
507
+ }
508
+
509
+ rememberRecentDelivery(key) {
510
+ this.pruneRecentDeliveries();
511
+ this.recentSettledWeixinDeliveries.set(key, Date.now());
512
+ }
513
+
514
+ pruneRecentDeliveries() {
515
+ const now = Date.now();
516
+ for (const [key, deliveredAt] of this.recentSettledWeixinDeliveries.entries()) {
517
+ if (!Number.isFinite(deliveredAt) || (now - deliveredAt) > RECENT_WEIXIN_DELIVERY_TTL_MS) {
518
+ this.recentSettledWeixinDeliveries.delete(key);
519
+ }
520
+ }
521
+ }
522
+
523
+ logDeliveryTrace(stage, payload, error = null) {
524
+ if (!this.deliveryTraceEnabled || !payload) {
525
+ return;
526
+ }
527
+ const parts = [
501
528
  `[codeksei] weixin delivery trace stage=${stage}`,
502
- `pid=${process.pid}`,
503
- `trace=${payload.traceId || "(none)"}`,
504
- `thread=${payload.threadId}`,
505
- `turn=${payload.turnId || "(pending)"}`,
506
- `mode=${payload.mode}`,
507
- `force=${payload.force ? "1" : "0"}`,
508
- payload.trigger ? `trigger=${payload.trigger}` : "",
509
- `sentCharsBefore=${payload.sentCharsBefore}`,
510
- `safeChars=${payload.safeChars}`,
511
- `deltaChars=${payload.deltaChars}`,
512
- `safeHash=${payload.safeHash}`,
513
- `deltaHash=${payload.deltaHash}`,
514
- ].filter(Boolean);
515
- if (error) {
516
- parts.push(`error=${JSON.stringify(String(error?.message || error || ""))}`);
517
- console.error(parts.join(" "));
518
- return;
519
- }
520
- console.log(parts.join(" "));
521
- }
522
- }
523
-
524
- function buildRunKey(threadId, turnId = "") {
525
- const normalizedThreadId = normalizeText(threadId);
526
- const normalizedTurnId = normalizeText(turnId);
527
- return normalizedTurnId
528
- ? `${normalizedThreadId}:${normalizedTurnId}`
529
- : `${normalizedThreadId}:pending`;
530
- }
531
-
532
- function buildReplyText(state, { completedOnly, preferLatestMessage = false, force = false }) {
533
- if (force && hasWatchdogTail(state, { completedOnly })) {
534
- return buildSettledReplyText(state, { completedOnly });
535
- }
536
-
537
- if (preferLatestMessage) {
538
- return buildSettledReplyText(state, { completedOnly });
539
- }
540
-
541
- if (prefersStreamingDelivery(state)) {
542
- return buildStreamingReplyText(state, { completedOnly, force });
543
- }
544
-
545
- return buildAllVisibleReplyText(state, { completedOnly });
546
- }
547
-
548
- function buildStreamingReplyText(state, { completedOnly, force }) {
549
- const visibleItems = collectVisibleItems(state, { completedOnly });
550
- const parts = [];
551
- const seenParts = new Set();
552
- const lastVisibleReplyIndex = findLastVisibleReplyIndex(visibleItems);
553
-
554
- for (let index = 0; index < visibleItems.length; index += 1) {
555
- const item = visibleItems[index];
556
- if (!shouldStreamImmediately(item, { isTerminalVisibleItem: index === lastVisibleReplyIndex })) {
557
- continue;
558
- }
559
- rememberVisiblePart(parts, seenParts, item.text);
560
- }
561
-
562
- if (!force) {
563
- return parts.join("\n\n");
564
- }
565
-
566
- const terminal = findStreamingTerminalReplyText(visibleItems);
567
- if (terminal) {
568
- rememberVisiblePart(parts, seenParts, terminal.text);
569
- }
570
- return parts.join("\n\n");
571
- }
572
-
573
- function buildSettledReplyText(state, { completedOnly }) {
574
- const tail = readStateItemText(state, "__watchdog__", { completedOnly });
575
- if (!tail) {
576
- // Codex can emit several assistant messages inside one turn. In settled
577
- // WeChat delivery we only want the latest user-facing reply, otherwise all
578
- // intermediate progress updates get stitched onto the final answer.
579
- return findLatestVisibleReplyText(state, { completedOnly });
580
- }
581
-
582
- // The watchdog message says "目前拿到的内容". When a turn never reaches its
583
- // final assistant item, silently dropping earlier completed items here would
584
- // contradict that promise and hide already-generated user-visible text.
585
- const visible = buildAllVisibleReplyText(state, {
586
- completedOnly,
587
- skipItemIds: new Set(["__watchdog__"]),
588
- });
589
- return [visible, tail].filter(Boolean).join("\n\n");
590
- }
591
-
592
- function buildAllVisibleReplyText(
593
- state,
594
- { completedOnly, skipItemIds = null, collapseDuplicateVisibleItems = false }
595
- ) {
596
- const parts = [];
597
- const seenVisibleParts = collapseDuplicateVisibleItems ? new Set() : null;
598
- for (const item of collectVisibleItems(state, { completedOnly, skipItemIds })) {
599
- if (!seenVisibleParts) {
600
- parts.push(item.text);
601
- continue;
602
- }
603
- rememberVisiblePart(parts, seenVisibleParts, item.text);
604
- }
605
- return parts.join("\n\n");
606
- }
607
-
608
- function findLatestVisibleReplyText(state, { completedOnly }) {
609
- const visibleItems = collectVisibleItems(state, { completedOnly });
610
- for (let index = visibleItems.length - 1; index >= 0; index -= 1) {
611
- if (visibleItems[index].itemId !== "__watchdog__") {
612
- return visibleItems[index].text;
613
- }
614
- }
615
- return "";
616
- }
617
-
618
- function findStreamingTerminalReplyText(visibleItems) {
619
- const lastVisibleReplyIndex = findLastVisibleReplyIndex(visibleItems);
620
- if (lastVisibleReplyIndex < 0) {
621
- return null;
622
- }
623
- for (let index = visibleItems.length - 1; index >= 0; index -= 1) {
624
- const item = visibleItems[index];
625
- if (item.itemId === "__watchdog__") {
626
- continue;
627
- }
628
- if (!shouldStreamImmediately(item, { isTerminalVisibleItem: index === lastVisibleReplyIndex })) {
629
- return item;
630
- }
631
- }
632
- return visibleItems[lastVisibleReplyIndex] || null;
633
- }
634
-
635
- function findLastVisibleReplyIndex(visibleItems) {
636
- for (let index = visibleItems.length - 1; index >= 0; index -= 1) {
637
- if (visibleItems[index]?.itemId !== "__watchdog__") {
638
- return index;
639
- }
640
- }
641
- return -1;
642
- }
643
-
644
- function readStateItemText(state, itemId, { completedOnly }) {
645
- const item = state.items.get(itemId);
646
- if (!item) {
647
- return "";
648
- }
649
- const sourceText = completedOnly
650
- ? (item.completed ? item.completedText : "")
651
- : (item.completed ? item.completedText : item.currentText);
652
- return trimOuterBlankLines(sourceText);
653
- }
654
-
655
- function collectVisibleItems(state, { completedOnly, skipItemIds = null }) {
656
- const items = [];
657
- for (const itemId of state.itemOrder) {
658
- if (skipItemIds?.has(itemId)) {
659
- continue;
660
- }
661
- const text = readStateItemText(state, itemId, { completedOnly });
662
- if (!text) {
663
- continue;
664
- }
665
- const item = state.items.get(itemId);
666
- items.push({
667
- itemId,
668
- text,
669
- phase: normalizeAssistantPhase(item?.phase),
670
- });
671
- }
672
- return items;
673
- }
674
-
675
- function markdownToPlainText(text) {
676
- let result = normalizeLineEndings(text);
677
- result = result.replace(/```([^\n]*)\n?([\s\S]*?)```/g, (_, language, code) => {
678
- const label = String(language || "").trim();
679
- const body = indentBlock(String(code || ""));
680
- return label ? `\n${label}:\n${body}\n` : `\n代码:\n${body}\n`;
681
- });
682
- result = result.replace(/```([^\n]*)\n?([\s\S]*)$/g, (_, language, code) => {
683
- const label = String(language || "").trim();
684
- const body = indentBlock(String(code || ""));
685
- return label ? `\n${label}:\n${body}\n` : `\n代码:\n${body}\n`;
686
- });
687
- result = result.replace(/!\[[^\]]*]\([^)]*\)/g, "");
688
- result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
689
- result = result.replace(/`([^`]+)`/g, "$1");
690
- result = result.replace(/^#{1,6}\s*(.+)$/gm, "$1");
691
- result = result.replace(/\*\*([^*]+)\*\*/g, "$1");
692
- result = result.replace(/\*([^*]+)\*/g, "$1");
693
- result = result.replace(/^>\s?/gm, "> ");
694
- result = result.replace(/^\|[\s:|-]+\|$/gm, "");
695
- result = result.replace(/^\|(.+)\|$/gm, (_, inner) =>
696
- String(inner || "").split("|").map((cell) => cell.trim()).join(" ")
697
- );
698
- result = result.replace(/\n{3,}/g, "\n\n");
699
- return trimOuterBlankLines(result);
700
- }
701
-
702
- function appendStreamingText(current, next) {
703
- const base = String(current || "");
704
- const incoming = String(next || "");
705
- if (!incoming) {
706
- return base;
707
- }
708
- if (!base) {
709
- return incoming;
710
- }
711
- if (base.endsWith(incoming)) {
712
- return base;
713
- }
714
- if (incoming.startsWith(base)) {
715
- return incoming;
716
- }
717
-
718
- const maxOverlap = Math.min(base.length, incoming.length);
719
- for (let size = maxOverlap; size > 0; size -= 1) {
720
- if (base.slice(-size) === incoming.slice(0, size)) {
721
- return `${base}${incoming.slice(size)}`;
722
- }
723
- }
724
-
725
- return `${base}${incoming}`;
726
- }
727
-
728
- function prefersSettledDelivery(state) {
729
- return normalizeText(state?.replyTarget?.provider) === "weixin"
730
- && normalizeWeixinReplyMode(state?.weixinReplyMode) === "settled";
731
- }
732
-
733
- function prefersStreamingDelivery(state) {
734
- return normalizeText(state?.replyTarget?.provider) === "weixin"
735
- && normalizeWeixinReplyMode(state?.weixinReplyMode) === "stream";
736
- }
737
-
738
- function buildSettledWeixinDeliveryKey(state, safeText) {
739
- if (!prefersSettledDelivery(state)) {
740
- return "";
741
- }
742
- const threadId = normalizeText(state?.threadId);
743
- const userId = normalizeText(state?.replyTarget?.userId);
744
- const contextToken = normalizeText(state?.replyTarget?.contextToken);
745
- const text = normalizeLineEndings(safeText).trim();
746
- if (!threadId || !userId || !contextToken || !text) {
747
- return "";
748
- }
749
- // Keep the scope narrow: only suppress the same settled payload on the same
750
- // WeChat thread shortly after a successful send, which is where we see
751
- // accidental duplicate terminal deliveries.
752
- return `${threadId}|${userId}|${contextToken}|${text}`;
753
- }
754
-
755
- function buildDeliveryMode(state) {
756
- if (prefersSettledDelivery(state)) {
757
- return "settled";
758
- }
759
- if (prefersStreamingDelivery(state)) {
760
- return "stream";
761
- }
762
- return normalizeText(state?.replyTarget?.provider) || "unknown";
763
- }
764
-
765
- function buildDeliveryTracePayload(state, {
766
- force = false,
767
- trigger = null,
768
- traceId = "",
769
- safeText = "",
770
- delta = "",
771
- } = {}) {
772
- return {
773
- traceId: normalizeText(traceId),
774
- threadId: normalizeText(state?.threadId),
775
- turnId: normalizeText(state?.turnId),
776
- mode: buildDeliveryMode(state),
777
- force: Boolean(force),
778
- trigger: formatDeliveryTrigger(trigger),
779
- sentCharsBefore: String(state?.sentText || "").length,
780
- safeChars: String(safeText || "").length,
781
- deltaChars: String(delta || "").length,
782
- safeHash: hashReplyText(safeText),
783
- deltaHash: hashReplyText(delta),
784
- };
785
- }
786
-
787
- function formatDeliveryTrigger(trigger) {
788
- if (!trigger || typeof trigger !== "object") {
789
- return "";
790
- }
791
- return [
792
- normalizeText(trigger.source),
793
- normalizeText(trigger.itemId),
794
- normalizeText(trigger.phase),
795
- ].filter(Boolean).join("/");
796
- }
797
-
798
- function hashReplyText(text) {
799
- return crypto.createHash("sha1").update(String(text || ""), "utf8").digest("hex").slice(0, 12);
800
- }
801
-
802
- function mergeCompletedItemText(current, completed) {
803
- const streamed = String(current || "");
804
- const finalized = String(completed || "");
805
- if (!finalized) {
806
- return streamed;
807
- }
808
- if (!streamed) {
809
- return finalized;
810
- }
811
- return appendStreamingText(streamed, finalized);
812
- }
813
-
814
- function indentBlock(text) {
815
- const normalized = trimOuterBlankLines(normalizeLineEndings(text));
816
- if (!normalized) {
817
- return "";
818
- }
819
- return normalized.split("\n").map((line) => ` ${line}`).join("\n");
820
- }
821
-
822
- function normalizeText(value) {
823
- return typeof value === "string" ? value.trim() : "";
824
- }
825
-
826
- function normalizeWeixinReplyMode(value) {
827
- return normalizeText(value).toLowerCase() === "settled" ? "settled" : "stream";
828
- }
829
-
830
- function normalizeLineEndings(value) {
831
- return String(value || "").replace(/\r\n/g, "\n");
832
- }
833
-
834
- function normalizeDeliveryDelta(delta, { streaming = false } = {}) {
835
- const normalized = String(delta || "");
836
- if (!streaming) {
837
- return normalized;
838
- }
839
- // Stream mode ships completed assistant items one message at a time. The
840
- // snapshot diff can therefore start with the joiner's blank lines; trim only
841
- // that transport artifact so the next item lands as a clean standalone send.
842
- return normalized.replace(/^\n+/u, "");
843
- }
844
-
845
- function buildVisibleItemDedupKey(text) {
846
- return trimOuterBlankLines(markdownToPlainText(normalizeLineEndings(text)));
847
- }
848
-
849
- function hasWatchdogTail(state, { completedOnly }) {
850
- return Boolean(readStateItemText(state, "__watchdog__", { completedOnly }));
851
- }
852
-
853
- function rememberVisiblePart(parts, seenParts, text) {
854
- const dedupeKey = buildVisibleItemDedupKey(text);
855
- if (dedupeKey && seenParts.has(dedupeKey)) {
856
- return;
857
- }
858
- if (dedupeKey) {
859
- seenParts.add(dedupeKey);
860
- }
861
- parts.push(text);
862
- }
863
-
864
- function shouldStreamImmediately(item, { isTerminalVisibleItem = false } = {}) {
865
- if (!item?.text || item.itemId === "__watchdog__") {
866
- return false;
867
- }
868
- const phase = normalizeAssistantPhase(item.phase);
869
- if (phase === "final") {
870
- return false;
871
- }
872
- if (!isBriefStreamingProgressText(item.text)) {
873
- return false;
874
- }
875
- if (phase === "commentary") {
876
- return true;
877
- }
878
- // When phase is missing, the current terminal short block could still be the
879
- // user's final answer. Hold only that trailing block until another visible
880
- // item arrives or the turn completes, so we do not leak a short final reply
881
- // early just because upstream omitted phase metadata once.
882
- return !isTerminalVisibleItem;
883
- }
884
-
885
- function isBriefStreamingProgressText(text) {
886
- const raw = trimOuterBlankLines(normalizeLineEndings(text));
887
- if (!raw) {
888
- return false;
889
- }
890
- if (
891
- raw.includes("```")
892
- || raw.includes("\n\n")
893
- || /\[[^\]]+\]\([^)]+\)/u.test(raw)
894
- || /^\s*(?:[-*]|\d+\.)\s/mu.test(raw)
895
- || raw.includes("【继续任务】")
896
- || raw.includes("【当前状态】")
897
- || raw.includes("【执行前检查】")
898
- ) {
899
- return false;
900
- }
901
- const plain = buildVisibleItemDedupKey(raw);
902
- if (!plain || plain.length > STREAM_PROGRESS_MAX_CHARS) {
903
- return false;
904
- }
905
- const lineCount = plain.split("\n").filter(Boolean).length;
906
- return lineCount > 0 && lineCount <= STREAM_PROGRESS_MAX_LINES;
907
- }
908
-
909
- function trimOuterBlankLines(text) {
910
- return String(text || "")
911
- .replace(/^\s*\n+/g, "")
912
- .replace(/\n+\s*$/g, "");
913
- }
914
-
915
- function shouldSuppressSystemReply(replyTarget, plainReplyText) {
916
- if (replyTarget?.provider !== "system") {
917
- return false;
918
- }
919
- const normalized = normalizeLineEndings(String(plainReplyText || ""));
920
- const compact = normalized.trim();
921
- if (!compact) {
922
- return false;
923
- }
924
- const sentinelNormalized = normalizeSilentSentinelText(compact);
925
- if (compact === "CB_SILENT" || compact === "__SILENT__" || compact === "SILENT") {
926
- return true;
927
- }
928
- if (containsStructuredSilentSignal(normalized)) {
929
- return true;
930
- }
931
- if (compact.toUpperCase().includes("CB_SILENT") || compact.toUpperCase().includes("__SILENT__")) {
932
- return true;
933
- }
934
- if (sentinelNormalized.includes("CB_SILENT") || sentinelNormalized.includes("__SILENT__") || sentinelNormalized.includes("SILENT")) {
935
- return true;
936
- }
937
- return normalized
938
- .split("\n")
939
- .map((line) => normalizeSilentSentinelText(line.trim()))
940
- .some((line) => line === "CB_SILENT" || line === "__SILENT__" || line === "SILENT");
941
- }
942
-
943
- function sanitizeReplyText(replyTarget, plainReplyText) {
944
- const normalized = normalizeLineEndings(String(plainReplyText || ""));
945
- if (!normalized) {
946
- return { suppress: false, text: "" };
947
- }
948
- const protocolSanitized = sanitizeProtocolLeakText(normalized);
949
- const safeText = protocolSanitized.text || "";
950
- if (shouldSuppressSystemReply(replyTarget, safeText)) {
951
- return { suppress: true, text: "" };
952
- }
953
- const cleaned = stripSilentSentinelArtifacts(safeText);
954
- return {
955
- suppress: false,
956
- text: trimOuterBlankLines(cleaned),
957
- };
958
- }
959
-
960
- function normalizeSilentSentinelText(value) {
961
- return String(value || "")
962
- .normalize("NFKC")
963
- .toUpperCase()
964
- .replace(/[^A-Z_]/g, "");
965
- }
966
-
967
- function stripSilentSentinelArtifacts(value) {
968
- return normalizeLineEndings(String(value || ""))
969
- .replace(/\{\s*"cyberboss_action"\s*:\s*"silent"\s*\}/gi, "")
970
- .split("\n")
971
- .map((line) => {
972
- const parts = line.split(/\s+/);
973
- const kept = parts.filter((part) => !isSilentSentinelToken(part));
974
- return kept.join(" ").trim();
975
- })
976
- .filter((line, index, lines) => line || (index > 0 && index < lines.length - 1))
977
- .join("\n")
978
- .replace(/\n{3,}/g, "\n\n");
979
- }
980
-
981
- function isSilentSentinelToken(value) {
982
- const normalized = normalizeSilentSentinelText(value);
983
- return normalized === "CB_SILENT" || normalized === "__SILENT__" || normalized === "SILENT";
984
- }
985
-
986
- function containsStructuredSilentSignal(value) {
987
- return /\{\s*"cyberboss_action"\s*:\s*"silent"\s*\}/i.test(String(value || ""));
988
- }
989
-
990
- module.exports = { StreamDelivery };
529
+ `pid=${process.pid}`,
530
+ `trace=${payload.traceId || "(none)"}`,
531
+ `thread=${payload.threadId}`,
532
+ `turn=${payload.turnId || "(pending)"}`,
533
+ `mode=${payload.mode}`,
534
+ `force=${payload.force ? "1" : "0"}`,
535
+ payload.trigger ? `trigger=${payload.trigger}` : "",
536
+ `sentCharsBefore=${payload.sentCharsBefore}`,
537
+ `safeChars=${payload.safeChars}`,
538
+ `deltaChars=${payload.deltaChars}`,
539
+ `safeHash=${payload.safeHash}`,
540
+ `deltaHash=${payload.deltaHash}`,
541
+ ].filter(Boolean);
542
+ if (error) {
543
+ parts.push(`error=${JSON.stringify(String(error?.message || error || ""))}`);
544
+ console.error(parts.join(" "));
545
+ return;
546
+ }
547
+ console.log(parts.join(" "));
548
+ }
549
+ }
550
+
551
+ function buildRunKey(threadId, turnId = "") {
552
+ const normalizedThreadId = normalizeText(threadId);
553
+ const normalizedTurnId = normalizeText(turnId);
554
+ return normalizedTurnId
555
+ ? `${normalizedThreadId}:${normalizedTurnId}`
556
+ : `${normalizedThreadId}:pending`;
557
+ }
558
+
559
+ function buildReplyText(state, { completedOnly, preferLatestMessage = false, force = false }) {
560
+ if (force && hasWatchdogTail(state, { completedOnly })) {
561
+ if (prefersStreamingDelivery(state)) {
562
+ return buildStreamingWatchdogReplyText(state, { completedOnly });
563
+ }
564
+ return buildSettledReplyText(state, { completedOnly });
565
+ }
566
+
567
+ if (preferLatestMessage) {
568
+ return buildSettledReplyText(state, { completedOnly });
569
+ }
570
+
571
+ if (prefersStreamingDelivery(state)) {
572
+ return buildStreamingReplyText(state, { completedOnly, force });
573
+ }
574
+
575
+ return buildAllVisibleReplyText(state, { completedOnly });
576
+ }
577
+
578
+ function buildStreamingReplyText(state, { completedOnly, force }) {
579
+ const visibleItems = collectVisibleItems(state, { completedOnly });
580
+ const parts = [];
581
+ const seenParts = new Set();
582
+ const lastVisibleReplyIndex = findLastVisibleReplyIndex(visibleItems);
583
+
584
+ for (let index = 0; index < visibleItems.length; index += 1) {
585
+ const item = visibleItems[index];
586
+ if (!shouldStreamImmediately(item, { isTerminalVisibleItem: index === lastVisibleReplyIndex })) {
587
+ continue;
588
+ }
589
+ rememberVisiblePart(parts, seenParts, item.text);
590
+ }
591
+
592
+ if (!force) {
593
+ return parts.join("\n\n");
594
+ }
595
+
596
+ const terminal = findStreamingTerminalReplyText(visibleItems);
597
+ if (terminal) {
598
+ // `stream` means "ship completed user-visible blocks early when safe", not
599
+ // "stitch every unseen progress block onto the terminal answer". If no
600
+ // user-visible text has actually gone out yet, collapsing to the terminal
601
+ // block avoids the historical failure mode where several brief progress
602
+ // items get welded onto the final answer as one duplicated mega-bubble.
603
+ const terminalWasHeldBack = !shouldStreamImmediately(terminal, {
604
+ isTerminalVisibleItem: true,
605
+ });
606
+ if (!normalizeVisibleStreamingText(state.sentText) && terminalWasHeldBack) {
607
+ return terminal.text;
608
+ }
609
+ rememberVisiblePart(parts, seenParts, terminal.text);
610
+ }
611
+ return parts.join("\n\n");
612
+ }
613
+
614
+ function buildStreamingWatchdogReplyText(state, { completedOnly }) {
615
+ const visible = buildStreamingReplyText(state, { completedOnly, force: true });
616
+ const tail = readStateItemText(state, "__watchdog__", { completedOnly });
617
+ return [visible, tail].filter(Boolean).join("\n\n");
618
+ }
619
+
620
+ function buildCurrentSafeReplyText(state, { force = false, completedOnly = false } = {}) {
621
+ const plainText = markdownToPlainText(buildReplyText(state, {
622
+ completedOnly,
623
+ preferLatestMessage: prefersSettledDelivery(state),
624
+ force,
625
+ }));
626
+ return sanitizeReplyText(state.replyTarget, plainText).text;
627
+ }
628
+
629
+ function buildSettledReplyText(state, { completedOnly }) {
630
+ const tail = readStateItemText(state, "__watchdog__", { completedOnly });
631
+ if (!tail) {
632
+ // Codex can emit several assistant messages inside one turn. In settled
633
+ // WeChat delivery we only want the latest user-facing reply, otherwise all
634
+ // intermediate progress updates get stitched onto the final answer.
635
+ return findLatestVisibleReplyText(state, { completedOnly });
636
+ }
637
+
638
+ // When the watchdog has to rescue a stalled turn, dumping every visible item
639
+ // back to WeChat recreates the historical failure mode where long internal
640
+ // commentary or task cards leak as one giant assistant bubble. Keep only the
641
+ // latest safe visible block, then append the watchdog tail.
642
+ const visible = findLatestWatchdogVisibleReplyText(state, { completedOnly });
643
+ return [visible, tail].filter(Boolean).join("\n\n");
644
+ }
645
+
646
+ function buildAllVisibleReplyText(
647
+ state,
648
+ { completedOnly, skipItemIds = null, collapseDuplicateVisibleItems = false }
649
+ ) {
650
+ const parts = [];
651
+ const seenVisibleParts = collapseDuplicateVisibleItems ? new Set() : null;
652
+ for (const item of collectVisibleItems(state, { completedOnly, skipItemIds })) {
653
+ if (!seenVisibleParts) {
654
+ parts.push(item.text);
655
+ continue;
656
+ }
657
+ rememberVisiblePart(parts, seenVisibleParts, item.text);
658
+ }
659
+ return parts.join("\n\n");
660
+ }
661
+
662
+ function findLatestVisibleReplyText(state, { completedOnly }) {
663
+ const visibleItems = collectVisibleItems(state, { completedOnly });
664
+ for (let index = visibleItems.length - 1; index >= 0; index -= 1) {
665
+ if (visibleItems[index].itemId !== "__watchdog__") {
666
+ return visibleItems[index].text;
667
+ }
668
+ }
669
+ return "";
670
+ }
671
+
672
+ function findLatestWatchdogVisibleReplyText(state, { completedOnly }) {
673
+ const visibleItems = collectVisibleItems(state, {
674
+ completedOnly,
675
+ skipItemIds: new Set(["__watchdog__"]),
676
+ });
677
+ const candidate = findLatestWatchdogVisibleReply(visibleItems);
678
+ return candidate?.text || "";
679
+ }
680
+
681
+ function findLatestWatchdogVisibleReply(visibleItems) {
682
+ for (let index = visibleItems.length - 1; index >= 0; index -= 1) {
683
+ const item = visibleItems[index];
684
+ if (!item || item.itemId === "__watchdog__") {
685
+ continue;
686
+ }
687
+
688
+ if (item.phase === "final") {
689
+ return item;
690
+ }
691
+
692
+ if (item.phase === "commentary") {
693
+ if (item.completed && isBriefStreamingProgressText(item.text)) {
694
+ return item;
695
+ }
696
+ continue;
697
+ }
698
+
699
+ if (item.completed || isBriefStreamingProgressText(item.text)) {
700
+ return item;
701
+ }
702
+ }
703
+ return null;
704
+ }
705
+
706
+ function findStreamingTerminalReplyText(visibleItems) {
707
+ const lastVisibleReplyIndex = findLastVisibleReplyIndex(visibleItems);
708
+ if (lastVisibleReplyIndex < 0) {
709
+ return null;
710
+ }
711
+ for (let index = visibleItems.length - 1; index >= 0; index -= 1) {
712
+ const item = visibleItems[index];
713
+ if (item.itemId === "__watchdog__") {
714
+ continue;
715
+ }
716
+ if (!shouldStreamImmediately(item, { isTerminalVisibleItem: index === lastVisibleReplyIndex })) {
717
+ return item;
718
+ }
719
+ }
720
+ return visibleItems[lastVisibleReplyIndex] || null;
721
+ }
722
+
723
+ function findLastVisibleReplyIndex(visibleItems) {
724
+ for (let index = visibleItems.length - 1; index >= 0; index -= 1) {
725
+ if (visibleItems[index]?.itemId !== "__watchdog__") {
726
+ return index;
727
+ }
728
+ }
729
+ return -1;
730
+ }
731
+
732
+ function readStateItemText(state, itemId, { completedOnly }) {
733
+ const item = state.items.get(itemId);
734
+ if (!item) {
735
+ return "";
736
+ }
737
+ const sourceText = completedOnly
738
+ ? (item.completed ? item.completedText : "")
739
+ : (item.completed ? item.completedText : item.currentText);
740
+ return trimOuterBlankLines(sourceText);
741
+ }
742
+
743
+ function collectVisibleItems(state, { completedOnly, skipItemIds = null }) {
744
+ const items = [];
745
+ for (const itemId of state.itemOrder) {
746
+ if (skipItemIds?.has(itemId)) {
747
+ continue;
748
+ }
749
+ const text = readStateItemText(state, itemId, { completedOnly });
750
+ if (!text) {
751
+ continue;
752
+ }
753
+ const item = state.items.get(itemId);
754
+ items.push({
755
+ itemId,
756
+ text,
757
+ completed: Boolean(item?.completed),
758
+ phase: normalizeAssistantPhase(item?.phase),
759
+ });
760
+ }
761
+ return items;
762
+ }
763
+
764
+ function removeStateItem(state, itemId) {
765
+ const normalizedItemId = normalizeText(itemId);
766
+ if (!normalizedItemId || !state?.items?.has(normalizedItemId)) {
767
+ return;
768
+ }
769
+ state.items.delete(normalizedItemId);
770
+ state.itemOrder = state.itemOrder.filter((candidateId) => candidateId !== normalizedItemId);
771
+ }
772
+
773
+ function markdownToPlainText(text) {
774
+ let result = normalizeLineEndings(text);
775
+ result = result.replace(/```([^\n]*)\n?([\s\S]*?)```/g, (_, language, code) => {
776
+ const label = String(language || "").trim();
777
+ const body = indentBlock(String(code || ""));
778
+ return label ? `\n${label}:\n${body}\n` : `\n代码:\n${body}\n`;
779
+ });
780
+ result = result.replace(/```([^\n]*)\n?([\s\S]*)$/g, (_, language, code) => {
781
+ const label = String(language || "").trim();
782
+ const body = indentBlock(String(code || ""));
783
+ return label ? `\n${label}:\n${body}\n` : `\n代码:\n${body}\n`;
784
+ });
785
+ result = result.replace(/!\[[^\]]*]\([^)]*\)/g, "");
786
+ result = result.replace(/\[([^\]]+)\]\([^)]*\)/g, "$1");
787
+ result = result.replace(/`([^`]+)`/g, "$1");
788
+ result = result.replace(/^#{1,6}\s*(.+)$/gm, "$1");
789
+ result = result.replace(/\*\*([^*]+)\*\*/g, "$1");
790
+ result = result.replace(/\*([^*]+)\*/g, "$1");
791
+ result = result.replace(/^>\s?/gm, "> ");
792
+ result = result.replace(/^\|[\s:|-]+\|$/gm, "");
793
+ result = result.replace(/^\|(.+)\|$/gm, (_, inner) =>
794
+ String(inner || "").split("|").map((cell) => cell.trim()).join(" ")
795
+ );
796
+ result = result.replace(/\n{3,}/g, "\n\n");
797
+ return trimOuterBlankLines(result);
798
+ }
799
+
800
+ function appendStreamingText(current, next) {
801
+ const base = String(current || "");
802
+ const incoming = String(next || "");
803
+ if (!incoming) {
804
+ return base;
805
+ }
806
+ if (!base) {
807
+ return incoming;
808
+ }
809
+ if (base.endsWith(incoming)) {
810
+ return base;
811
+ }
812
+ if (incoming.startsWith(base)) {
813
+ return incoming;
814
+ }
815
+
816
+ const replacement = chooseStreamingSnapshotReplacement(base, incoming);
817
+ if (replacement) {
818
+ return replacement;
819
+ }
820
+
821
+ const maxOverlap = Math.min(base.length, incoming.length);
822
+ for (let size = maxOverlap; size > 0; size -= 1) {
823
+ if (base.slice(-size) === incoming.slice(0, size)) {
824
+ return `${base}${incoming.slice(size)}`;
825
+ }
826
+ }
827
+
828
+ return `${base}${incoming}`;
829
+ }
830
+
831
+ function prefersSettledDelivery(state) {
832
+ return normalizeText(state?.replyTarget?.provider) === "weixin"
833
+ && normalizeWeixinReplyMode(state?.weixinReplyMode) === "settled";
834
+ }
835
+
836
+ function prefersStreamingDelivery(state) {
837
+ return normalizeText(state?.replyTarget?.provider) === "weixin"
838
+ && normalizeWeixinReplyMode(state?.weixinReplyMode) === "stream";
839
+ }
840
+
841
+ function buildSettledWeixinDeliveryKey(state, safeText) {
842
+ if (!prefersSettledDelivery(state)) {
843
+ return "";
844
+ }
845
+ const threadId = normalizeText(state?.threadId);
846
+ const userId = normalizeText(state?.replyTarget?.userId);
847
+ const contextToken = normalizeText(state?.replyTarget?.contextToken);
848
+ const text = normalizeLineEndings(safeText).trim();
849
+ if (!threadId || !userId || !contextToken || !text) {
850
+ return "";
851
+ }
852
+ // Keep the scope narrow: only suppress the same settled payload on the same
853
+ // WeChat thread shortly after a successful send, which is where we see
854
+ // accidental duplicate terminal deliveries.
855
+ return `${threadId}|${userId}|${contextToken}|${text}`;
856
+ }
857
+
858
+ function buildDeliveryMode(state) {
859
+ if (prefersSettledDelivery(state)) {
860
+ return "settled";
861
+ }
862
+ if (prefersStreamingDelivery(state)) {
863
+ return "stream";
864
+ }
865
+ return normalizeText(state?.replyTarget?.provider) || "unknown";
866
+ }
867
+
868
+ function buildDeliveryTracePayload(state, {
869
+ force = false,
870
+ trigger = null,
871
+ traceId = "",
872
+ safeText = "",
873
+ delta = "",
874
+ } = {}) {
875
+ return {
876
+ traceId: normalizeText(traceId),
877
+ threadId: normalizeText(state?.threadId),
878
+ turnId: normalizeText(state?.turnId),
879
+ mode: buildDeliveryMode(state),
880
+ force: Boolean(force),
881
+ trigger: formatDeliveryTrigger(trigger),
882
+ sentCharsBefore: String(state?.sentText || "").length,
883
+ safeChars: String(safeText || "").length,
884
+ deltaChars: String(delta || "").length,
885
+ safeHash: hashReplyText(safeText),
886
+ deltaHash: hashReplyText(delta),
887
+ };
888
+ }
889
+
890
+ function formatDeliveryTrigger(trigger) {
891
+ if (!trigger || typeof trigger !== "object") {
892
+ return "";
893
+ }
894
+ return [
895
+ normalizeText(trigger.source),
896
+ normalizeText(trigger.itemId),
897
+ normalizeText(trigger.phase),
898
+ ].filter(Boolean).join("/");
899
+ }
900
+
901
+ function hashReplyText(text) {
902
+ return crypto.createHash("sha1").update(String(text || ""), "utf8").digest("hex").slice(0, 12);
903
+ }
904
+
905
+ function mergeCompletedItemText(current, completed) {
906
+ const streamed = String(current || "");
907
+ const finalized = String(completed || "");
908
+ if (!finalized) {
909
+ return streamed;
910
+ }
911
+ if (!streamed) {
912
+ return finalized;
913
+ }
914
+ // Upstream can resend the same final item with only whitespace / formatting
915
+ // differences between delta and completed snapshots. Treat those as the same
916
+ // semantic block so we do not concatenate two copies of the same answer.
917
+ if (normalizeVisibleStreamingText(streamed) === normalizeVisibleStreamingText(finalized)) {
918
+ return finalized;
919
+ }
920
+ const finalizedReplacement = chooseCompletedSnapshotReplacement(streamed, finalized);
921
+ if (finalizedReplacement) {
922
+ return finalizedReplacement;
923
+ }
924
+ return appendStreamingText(streamed, finalized);
925
+ }
926
+
927
+ function indentBlock(text) {
928
+ const normalized = trimOuterBlankLines(normalizeLineEndings(text));
929
+ if (!normalized) {
930
+ return "";
931
+ }
932
+ return normalized.split("\n").map((line) => ` ${line}`).join("\n");
933
+ }
934
+
935
+ function normalizeText(value) {
936
+ return typeof value === "string" ? value.trim() : "";
937
+ }
938
+
939
+ function normalizeWeixinReplyMode(value) {
940
+ return normalizeText(value).toLowerCase() === "settled" ? "settled" : "stream";
941
+ }
942
+
943
+ function normalizeLineEndings(value) {
944
+ return String(value || "").replace(/\r\n/g, "\n");
945
+ }
946
+
947
+ function normalizeDeliveryDelta(delta, { streaming = false } = {}) {
948
+ const normalized = String(delta || "");
949
+ if (!streaming) {
950
+ return normalized;
951
+ }
952
+ // Stream mode ships completed assistant items one message at a time. The
953
+ // snapshot diff can therefore start with the joiner's blank lines; trim only
954
+ // that transport artifact so the next item lands as a clean standalone send.
955
+ return normalized.replace(/^\n+/u, "");
956
+ }
957
+
958
+ function buildVisibleItemDedupKey(text) {
959
+ return trimOuterBlankLines(markdownToPlainText(normalizeLineEndings(text)));
960
+ }
961
+
962
+ function normalizeVisibleStreamingText(text) {
963
+ return buildVisibleItemDedupKey(text).replace(/\s+/gu, " ").trim();
964
+ }
965
+
966
+ function normalizeStreamingSnapshotSemanticText(text) {
967
+ return normalizeVisibleStreamingText(text)
968
+ .replace(/[\p{P}\p{S}]+/gu, "")
969
+ .replace(/\s+/gu, " ")
970
+ .trim();
971
+ }
972
+
973
+ function chooseStreamingSnapshotReplacement(base, incoming) {
974
+ const baseVisible = normalizeStreamingSnapshotSemanticText(base);
975
+ const incomingVisible = normalizeStreamingSnapshotSemanticText(incoming);
976
+ if (
977
+ !baseVisible
978
+ || !incomingVisible
979
+ || baseVisible.length < STREAM_SNAPSHOT_REPLACEMENT_MIN_CHARS
980
+ || incomingVisible.length < STREAM_SNAPSHOT_REPLACEMENT_MIN_CHARS
981
+ ) {
982
+ return "";
983
+ }
984
+
985
+ // Codex can resend the whole in-flight assistant item after reformatting an
986
+ // earlier region (for example, once a fenced code block becomes plain text in
987
+ // a later snapshot). Raw prefix checks then fail even though the newer
988
+ // snapshot semantically contains the older one. Prefer the newer snapshot so
989
+ // we do not weld two whole copies of the same answer together.
990
+ if (incomingVisible.startsWith(baseVisible)) {
991
+ return incoming;
992
+ }
993
+ // Some upstream snapshots briefly regress to a shorter normalized view while
994
+ // the same item is still being built. Keep the richer text we already have
995
+ // instead of treating that shorter resend as a new block to append.
996
+ if (baseVisible.startsWith(incomingVisible)) {
997
+ return base;
998
+ }
999
+ return "";
1000
+ }
1001
+
1002
+ function chooseCompletedSnapshotReplacement(streamed, finalized) {
1003
+ const streamedVisible = normalizeStreamingSnapshotSemanticText(streamed);
1004
+ const finalizedVisible = normalizeStreamingSnapshotSemanticText(finalized);
1005
+ if (
1006
+ !streamedVisible
1007
+ || !finalizedVisible
1008
+ || streamedVisible.length < STREAM_SNAPSHOT_REPLACEMENT_MIN_CHARS
1009
+ || finalizedVisible.length < STREAM_SNAPSHOT_REPLACEMENT_MIN_CHARS
1010
+ ) {
1011
+ return "";
1012
+ }
1013
+
1014
+ // The completed item is the authoritative final snapshot. If the streamed
1015
+ // buffer already contains the same normalized answer as a prefix/substring,
1016
+ // it usually means earlier delta snapshots were stitched together after a
1017
+ // formatting rewrite. Collapse back to the completed text instead of sending
1018
+ // a duplicated mega-bubble.
1019
+ if (streamedVisible.startsWith(finalizedVisible) || streamedVisible.includes(finalizedVisible)) {
1020
+ return finalized;
1021
+ }
1022
+ if (finalizedVisible.startsWith(streamedVisible)) {
1023
+ return finalized;
1024
+ }
1025
+ return "";
1026
+ }
1027
+
1028
+ function hasWatchdogTail(state, { completedOnly }) {
1029
+ return Boolean(readStateItemText(state, "__watchdog__", { completedOnly }));
1030
+ }
1031
+
1032
+ function rememberVisiblePart(parts, seenParts, text) {
1033
+ const dedupeKey = buildVisibleItemDedupKey(text);
1034
+ if (dedupeKey && seenParts.has(dedupeKey)) {
1035
+ return;
1036
+ }
1037
+ if (dedupeKey) {
1038
+ seenParts.add(dedupeKey);
1039
+ }
1040
+ parts.push(text);
1041
+ }
1042
+
1043
+ function shouldStreamImmediately(item, { isTerminalVisibleItem = false } = {}) {
1044
+ if (!item?.text || item.itemId === "__watchdog__") {
1045
+ return false;
1046
+ }
1047
+ const phase = normalizeAssistantPhase(item.phase);
1048
+ if (phase === "final") {
1049
+ return false;
1050
+ }
1051
+ if (!isBriefStreamingProgressText(item.text)) {
1052
+ return false;
1053
+ }
1054
+ if (phase === "commentary") {
1055
+ return true;
1056
+ }
1057
+ // When phase is missing, the current terminal short block could still be the
1058
+ // user's final answer. Hold only that trailing block until another visible
1059
+ // item arrives or the turn completes, so we do not leak a short final reply
1060
+ // early just because upstream omitted phase metadata once.
1061
+ return !isTerminalVisibleItem;
1062
+ }
1063
+
1064
+ function isBriefStreamingProgressText(text) {
1065
+ const raw = trimOuterBlankLines(normalizeLineEndings(text));
1066
+ if (!raw) {
1067
+ return false;
1068
+ }
1069
+ if (
1070
+ raw.includes("```")
1071
+ || raw.includes("\n\n")
1072
+ || /\[[^\]]+\]\([^)]+\)/u.test(raw)
1073
+ || /^\s*(?:[-*]|\d+\.)\s/mu.test(raw)
1074
+ || raw.includes("【继续任务】")
1075
+ || raw.includes("【当前状态】")
1076
+ || raw.includes("【执行前检查】")
1077
+ ) {
1078
+ return false;
1079
+ }
1080
+ const plain = buildVisibleItemDedupKey(raw);
1081
+ if (!plain || plain.length > STREAM_PROGRESS_MAX_CHARS) {
1082
+ return false;
1083
+ }
1084
+ const lineCount = plain.split("\n").filter(Boolean).length;
1085
+ return lineCount > 0 && lineCount <= STREAM_PROGRESS_MAX_LINES;
1086
+ }
1087
+
1088
+ function trimOuterBlankLines(text) {
1089
+ return String(text || "")
1090
+ .replace(/^\s*\n+/g, "")
1091
+ .replace(/\n+\s*$/g, "");
1092
+ }
1093
+
1094
+ function shouldSuppressSystemReply(replyTarget, plainReplyText) {
1095
+ if (replyTarget?.provider !== "system") {
1096
+ return false;
1097
+ }
1098
+ const normalized = normalizeLineEndings(String(plainReplyText || ""));
1099
+ const compact = normalized.trim();
1100
+ if (!compact) {
1101
+ return false;
1102
+ }
1103
+ const sentinelNormalized = normalizeSilentSentinelText(compact);
1104
+ if (compact === "CB_SILENT" || compact === "__SILENT__" || compact === "SILENT") {
1105
+ return true;
1106
+ }
1107
+ if (containsStructuredSilentSignal(normalized)) {
1108
+ return true;
1109
+ }
1110
+ if (compact.toUpperCase().includes("CB_SILENT") || compact.toUpperCase().includes("__SILENT__")) {
1111
+ return true;
1112
+ }
1113
+ if (sentinelNormalized.includes("CB_SILENT") || sentinelNormalized.includes("__SILENT__") || sentinelNormalized.includes("SILENT")) {
1114
+ return true;
1115
+ }
1116
+ return normalized
1117
+ .split("\n")
1118
+ .map((line) => normalizeSilentSentinelText(line.trim()))
1119
+ .some((line) => line === "CB_SILENT" || line === "__SILENT__" || line === "SILENT");
1120
+ }
1121
+
1122
+ function sanitizeReplyText(replyTarget, plainReplyText) {
1123
+ const normalized = normalizeLineEndings(String(plainReplyText || ""));
1124
+ if (!normalized) {
1125
+ return { suppress: false, text: "" };
1126
+ }
1127
+ const protocolSanitized = sanitizeProtocolLeakText(normalized);
1128
+ const safeText = protocolSanitized.text || "";
1129
+ if (shouldSuppressSystemReply(replyTarget, safeText)) {
1130
+ return { suppress: true, text: "" };
1131
+ }
1132
+ const cleaned = stripSilentSentinelArtifacts(safeText);
1133
+ return {
1134
+ suppress: false,
1135
+ text: trimOuterBlankLines(cleaned),
1136
+ };
1137
+ }
1138
+
1139
+ function normalizeSilentSentinelText(value) {
1140
+ return String(value || "")
1141
+ .normalize("NFKC")
1142
+ .toUpperCase()
1143
+ .replace(/[^A-Z_]/g, "");
1144
+ }
1145
+
1146
+ function stripSilentSentinelArtifacts(value) {
1147
+ return normalizeLineEndings(String(value || ""))
1148
+ .replace(/\{\s*"cyberboss_action"\s*:\s*"silent"\s*\}/gi, "")
1149
+ .split("\n")
1150
+ .map((line) => {
1151
+ const parts = line.split(/\s+/);
1152
+ const kept = parts.filter((part) => !isSilentSentinelToken(part));
1153
+ return kept.join(" ").trim();
1154
+ })
1155
+ .filter((line, index, lines) => line || (index > 0 && index < lines.length - 1))
1156
+ .join("\n")
1157
+ .replace(/\n{3,}/g, "\n\n");
1158
+ }
1159
+
1160
+ function isSilentSentinelToken(value) {
1161
+ const normalized = normalizeSilentSentinelText(value);
1162
+ return normalized === "CB_SILENT" || normalized === "__SILENT__" || normalized === "SILENT";
1163
+ }
1164
+
1165
+ function containsStructuredSilentSignal(value) {
1166
+ return /\{\s*"cyberboss_action"\s*:\s*"silent"\s*\}/i.test(String(value || ""));
1167
+ }
1168
+
1169
+ module.exports = { StreamDelivery };