@ganglion/xacpx 0.19.0 → 0.19.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -23,6 +23,8 @@ English · **[中文](./docs/zh/README_zh.md)**
23
23
 
24
24
  If you need to code or work remotely on a temporary basis, `xacpx` gives you a fast, convenient **remote entry point** so you can get things done from WeChat or Feishu anytime, anywhere.
25
25
 
26
+ Chat isn't the only entry point: xacpx also ships a self-hostable **[relay hub](#self-hosted-relay-hub--web-dashboard)** — a multi-tenant web dashboard that drives every one of your xacpx instances from a single browser tab (installable as a PWA on your phone).
27
+
26
28
  > For everyday use, remember `/ss` first: it creates or reuses an xacpx logical session. If you want to attach to an existing native session of a local agent such as Codex, use `/ssn`; see [native sessions](./docs/native-sessions.md).
27
29
 
28
30
  ## 5-minute quick start
@@ -173,9 +175,17 @@ take a look at today's API timeout issue
173
175
  /ssn 1
174
176
  ```
175
177
 
176
- ## Self-hosted relay hub (optional)
178
+ ## Self-hosted relay hub — web dashboard
179
+
180
+ If you run one or more xacpx instances and want to drive them from a browser instead of (or in addition to) chat, self-host the **relay hub**. Each instance dials out to the hub over WebSocket and registers; you log in to a multi-tenant web dashboard and manage every instance's sessions from one place.
181
+
182
+ What you get:
177
183
 
178
- If you run several xacpx instances and want to drive them all from one browser dashboard, you can self-host the **relay hub**. Each instance dials out to the hub over WebSocket and registers; you log in to a multi-tenant web dashboard and manage every instance's sessions — chat, scheduled tasks, and orchestration from one place. The hub ships as an npm package (`@ganglion/xacpx-relay`) with the dashboard **bundled in**, served on a single port, with a single **access token** for both web login and connector pairing.
184
+ - **A three-pane IM-style dashboard** instance/session tree on the left, live chat in the middle, scheduled tasks & orchestration panel on the right. English + 中文 UI.
185
+ - **Live streaming replies** rendered as markdown, with tool-call and subagent activity inline; cancel running turns from the browser.
186
+ - **Mobile-ready** — installs as a PWA, so it feels like an app on your phone.
187
+ - **One package, one port** — `@ganglion/xacpx-relay` ships the dashboard **bundled in**; HTTP API, web socket, and instance gateway share a single port by default. SQLite storage via the built-in `node:sqlite`/`bun:sqlite` — no native addons to compile.
188
+ - **Multi-tenant & token-based** — every token is a user who only sees their own instances; tokens and credentials are stored hashed. Onboard teammates with **single-use invite links** (`xacpx-relay add invite`) instead of handing out tokens.
179
189
 
180
190
  ```bash
181
191
  npm i -g @ganglion/xacpx-relay
@@ -188,7 +198,7 @@ xacpx channel add relay --url wss://relay.example.com --token <access-token> --n
188
198
  xacpx restart
189
199
  ```
190
200
 
191
- Full walkthrough — pairing, TLS/reverse-proxy, systemd, backups, troubleshooting: **[Self-Hosting the Relay Hub](https://gadzan.github.io/xacpx/guide/relay-self-hosting)** (or [relay-deployment.md](./docs/relay-deployment.md) for the terse runbook).
201
+ The same access token works for both web login and connector pairing. Full walkthrough — pairing, invites, TLS/reverse-proxy, systemd, backups, troubleshooting: **[Self-Hosting the Relay Hub](https://gadzan.github.io/xacpx/guide/relay-self-hosting)** (or [relay-deployment.md](./docs/relay-deployment.md) for the terse runbook).
192
202
 
193
203
  ## Config and runtime files
194
204
 
@@ -3223,6 +3223,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
3223
3223
  let onCommands;
3224
3224
  let rawStream = false;
3225
3225
  let driver;
3226
+ let onBeforeActivityEvent;
3226
3227
  if (options === undefined) {
3227
3228
  toolEventMode = "text";
3228
3229
  onToolEvent = undefined;
@@ -3237,6 +3238,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
3237
3238
  onCommands = options.onCommands;
3238
3239
  rawStream = options.rawStream ?? false;
3239
3240
  driver = options.driver?.trim().toLowerCase() || undefined;
3241
+ onBeforeActivityEvent = options.onBeforeActivityEvent;
3240
3242
  toolEventMode = resolveToolEventMode({
3241
3243
  toolEventMode: options.mode,
3242
3244
  onToolEvent
@@ -3249,10 +3251,14 @@ function createStreamingPromptState(formatToolCalls = false, options) {
3249
3251
  pendingLine: "",
3250
3252
  formatToolCalls,
3251
3253
  emittedToolCallIds: new Set,
3254
+ positionedToolCallIds: new Set,
3252
3255
  toolCalls: new Map,
3253
3256
  toolEventMode,
3254
3257
  driver,
3255
3258
  rawStream,
3259
+ lastTextTail: "",
3260
+ activitySinceLastText: false,
3261
+ onBeforeActivityEvent,
3256
3262
  onToolEvent,
3257
3263
  onThought,
3258
3264
  onPlan,
@@ -3295,6 +3301,14 @@ function parseStreamingChunks(state, line) {
3295
3301
  if (!update)
3296
3302
  return;
3297
3303
  if (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") {
3304
+ const isInitialToolEvent = typeof update.toolCallId === "string" ? !state.positionedToolCallIds.has(update.toolCallId) : update.sessionUpdate === "tool_call";
3305
+ if (isInitialToolEvent) {
3306
+ if (update.toolCallId)
3307
+ state.positionedToolCallIds.add(update.toolCallId);
3308
+ markActivityBoundary(state);
3309
+ } else {
3310
+ flushBeforeActivityEvent(state);
3311
+ }
3298
3312
  const wantsStructured = state.toolEventMode === "structured" || state.toolEventMode === "both";
3299
3313
  const wantsText = (state.toolEventMode === "text" || state.toolEventMode === "both") && state.formatToolCalls;
3300
3314
  if (wantsStructured && state.onToolEvent) {
@@ -3343,6 +3357,7 @@ function parseStreamingChunks(state, line) {
3343
3357
  if (isThoughtChunk) {
3344
3358
  const chunk2 = update.content.text;
3345
3359
  if (chunk2.length > 0) {
3360
+ markActivityBoundary(state);
3346
3361
  state.onThought?.(chunk2);
3347
3362
  }
3348
3363
  return;
@@ -3351,10 +3366,22 @@ function parseStreamingChunks(state, line) {
3351
3366
  if (!isMessageChunk)
3352
3367
  return;
3353
3368
  state.hasAgentMessage = true;
3354
- const chunk = update.content.text ?? "";
3369
+ let chunk = update.content.text ?? "";
3355
3370
  if (chunk.length === 0)
3356
3371
  return;
3372
+ const messageId = typeof update.messageId === "string" && update.messageId.length > 0 ? update.messageId : undefined;
3373
+ const messageIdChanged = state.lastMessageId !== undefined && messageId !== undefined && state.lastMessageId !== messageId;
3374
+ const fallbackBoundary = state.activitySinceLastText && (state.lastMessageId === undefined || messageId === undefined) && endsWithSentenceTerminal(state.lastTextTail);
3375
+ if ((messageIdChanged || fallbackBoundary) && !hasParagraphBoundaryAtJoin(state.lastTextTail, chunk)) {
3376
+ chunk = `
3377
+
3378
+ ${chunk}`;
3379
+ state.lastTextTail = "";
3380
+ }
3357
3381
  state.buffer += chunk;
3382
+ state.lastMessageId = messageId;
3383
+ state.activitySinceLastText = false;
3384
+ state.lastTextTail = `${state.lastTextTail}${chunk}`.slice(-256);
3358
3385
  if (state.rawStream)
3359
3386
  return;
3360
3387
  let boundary;
@@ -3368,6 +3395,24 @@ function parseStreamingChunks(state, line) {
3368
3395
  }
3369
3396
  }
3370
3397
  }
3398
+ function endsWithSentenceTerminal(text) {
3399
+ return SENTENCE_TERMINAL_AT_END.test(text.trimEnd());
3400
+ }
3401
+ function hasParagraphBoundaryAtJoin(left, right) {
3402
+ const leftHasBoundary = PARAGRAPH_BOUNDARY_AT_END.test(left);
3403
+ const rightHasBoundary = PARAGRAPH_BOUNDARY_AT_START.test(right);
3404
+ const boundarySpansJoin = LINE_BREAK_AT_END.test(left) && LINE_BREAK_AT_START.test(right);
3405
+ const crlfBoundarySpansJoin = PARTIAL_CRLF_PARAGRAPH_BOUNDARY_AT_END.test(left) && right.startsWith(`
3406
+ `);
3407
+ return leftHasBoundary || rightHasBoundary || boundarySpansJoin || crlfBoundarySpansJoin;
3408
+ }
3409
+ function markActivityBoundary(state) {
3410
+ flushBeforeActivityEvent(state);
3411
+ state.activitySinceLastText = state.hasAgentMessage;
3412
+ }
3413
+ function flushBeforeActivityEvent(state) {
3414
+ state.onBeforeActivityEvent?.();
3415
+ }
3371
3416
  function formatToolCallEvent(update, sessionUpdate) {
3372
3417
  if (!update)
3373
3418
  return null;
@@ -3629,10 +3674,16 @@ function isGenericToolTitle(kind, title) {
3629
3674
  }
3630
3675
  return false;
3631
3676
  }
3632
- var USAGE_BREAKDOWN_FIELDS;
3677
+ var SENTENCE_TERMINAL_AT_END, PARAGRAPH_BOUNDARY_AT_END, PARAGRAPH_BOUNDARY_AT_START, LINE_BREAK_AT_END, LINE_BREAK_AT_START, PARTIAL_CRLF_PARAGRAPH_BOUNDARY_AT_END, USAGE_BREAKDOWN_FIELDS;
3633
3678
  var init_streaming_prompt = __esm(() => {
3634
3679
  init_background_followup();
3635
3680
  init_tool_kind_emoji();
3681
+ SENTENCE_TERMINAL_AT_END = /(?:\p{Sentence_Terminal}|…|⋯)[\p{Close_Punctuation}\p{Final_Punctuation}"“”‘’*_~`]*$/u;
3682
+ PARAGRAPH_BOUNDARY_AT_END = /\r?\n[\t ]*\r?\n[\t ]*$/;
3683
+ PARAGRAPH_BOUNDARY_AT_START = /^[\t ]*\r?\n[\t ]*\r?\n/;
3684
+ LINE_BREAK_AT_END = /\r?\n[\t ]*$/;
3685
+ LINE_BREAK_AT_START = /^[\t ]*\r?\n/;
3686
+ PARTIAL_CRLF_PARAGRAPH_BOUNDARY_AT_END = /\r?\n[\t ]*\r$/;
3636
3687
  USAGE_BREAKDOWN_FIELDS = [
3637
3688
  ["inputTokens", ["inputTokens", "input_tokens"]],
3638
3689
  ["outputTokens", ["outputTokens", "output_tokens"]],
@@ -7534,10 +7585,14 @@ async function runStreamingPrompt(command, args, onEvent, options = {}) {
7534
7585
  let stdout = "";
7535
7586
  let stderr = "";
7536
7587
  const toolEventMode = options.toolEventMode ?? "text";
7588
+ let flushPendingText = () => {};
7537
7589
  const state = createStreamingPromptState(options.formatToolCalls ?? false, {
7538
7590
  mode: toolEventMode,
7539
7591
  driver: options.driver,
7540
7592
  rawStream,
7593
+ onBeforeActivityEvent: () => {
7594
+ flushPendingText();
7595
+ },
7541
7596
  ...onEvent && (toolEventMode === "structured" || toolEventMode === "both") ? { onToolEvent: (toolEvent) => onEvent({ type: "prompt.tool_event", event: toolEvent }) } : {},
7542
7597
  ...onEvent ? { onThought: (chunk) => onEvent({ type: "prompt.thought", text: chunk }) } : {},
7543
7598
  ...onEvent ? { onPlan: (entries) => onEvent({ type: "prompt.plan", entries }) } : {},
@@ -7553,6 +7608,13 @@ async function runStreamingPrompt(command, args, onEvent, options = {}) {
7553
7608
  lastReplyAt = now();
7554
7609
  }
7555
7610
  };
7611
+ flushPendingText = () => {
7612
+ for (const segment of state.segments.splice(0)) {
7613
+ onEvent?.({ type: "prompt.segment", text: segment });
7614
+ lastReplyAt = now();
7615
+ }
7616
+ flushBuffer();
7617
+ };
7556
7618
  const timer = setIntervalFn(() => {
7557
7619
  if (state.buffer.trim().length > 0 && now() - lastReplyAt >= maxSegmentWaitMs) {
7558
7620
  flushBuffer();
package/dist/cli.js CHANGED
@@ -5762,6 +5762,10 @@ var init_agent_templates = __esm(() => {
5762
5762
  "grok-build": {
5763
5763
  driver: "grok-build"
5764
5764
  },
5765
+ hermes: {
5766
+ driver: "hermes",
5767
+ command: "hermes acp"
5768
+ },
5765
5769
  iflow: {
5766
5770
  driver: "iflow"
5767
5771
  },
@@ -32775,6 +32779,25 @@ var init_quota_gated_reply_sink = __esm(() => {
32775
32779
  ADAPTIVE_WINDOW_SCHEDULE_MS = [3000, 6000, 12000, 24000, 48000, 60000];
32776
32780
  });
32777
32781
 
32782
+ // src/transport/serialized-callback-queue.ts
32783
+ function createSerializedCallbackQueue() {
32784
+ let chain = Promise.resolve();
32785
+ let firstError;
32786
+ return {
32787
+ enqueue(callback) {
32788
+ chain = chain.then(callback).catch((error2) => {
32789
+ firstError ??= error2;
32790
+ });
32791
+ },
32792
+ async drain() {
32793
+ await chain;
32794
+ },
32795
+ getError() {
32796
+ return firstError;
32797
+ }
32798
+ };
32799
+ }
32800
+
32778
32801
  // src/transport/tool-event-mode.ts
32779
32802
  function resolveToolEventMode(input) {
32780
32803
  if (input?.toolEventMode !== undefined) {
@@ -32827,12 +32850,7 @@ class AcpxBridgeTransport {
32827
32850
  reply,
32828
32851
  ...replyContext ? { replyContext } : {}
32829
32852
  }) : null;
32830
- let segmentError;
32831
- let segmentChain = Promise.resolve();
32832
- let toolEventError;
32833
- let toolEventChain = Promise.resolve();
32834
- let thoughtError;
32835
- let thoughtChain = Promise.resolve();
32853
+ const transcriptEvents = createSerializedCallbackQueue();
32836
32854
  let planError;
32837
32855
  let planChain = Promise.resolve();
32838
32856
  let usageError;
@@ -32852,22 +32870,19 @@ class AcpxBridgeTransport {
32852
32870
  }, (event) => {
32853
32871
  if (event.type === "prompt.segment") {
32854
32872
  const onSegment = options?.onSegment;
32855
- if (onSegment) {
32856
- const segmentText = event.text;
32857
- segmentChain = segmentChain.then(() => onSegment(segmentText)).catch((error2) => {
32858
- segmentError ??= error2;
32859
- });
32860
- }
32861
- sink2?.feedSegment(event.text);
32873
+ const segmentText = event.text;
32874
+ transcriptEvents.enqueue(async () => {
32875
+ const segmentResult = onSegment?.(segmentText);
32876
+ sink2?.feedSegment(segmentText);
32877
+ await segmentResult;
32878
+ });
32862
32879
  return;
32863
32880
  }
32864
32881
  if (event.type === "prompt.tool_event") {
32865
32882
  const onToolEvent = options?.onToolEvent;
32866
32883
  if (onToolEvent) {
32867
32884
  const toolEvent = event.event;
32868
- toolEventChain = toolEventChain.then(() => onToolEvent(toolEvent)).catch((error2) => {
32869
- toolEventError ??= error2;
32870
- });
32885
+ transcriptEvents.enqueue(() => onToolEvent(toolEvent));
32871
32886
  }
32872
32887
  return;
32873
32888
  }
@@ -32875,9 +32890,7 @@ class AcpxBridgeTransport {
32875
32890
  const onThought = options?.onThought;
32876
32891
  if (onThought) {
32877
32892
  const thoughtText = event.text;
32878
- thoughtChain = thoughtChain.then(() => onThought(thoughtText)).catch((error2) => {
32879
- thoughtError ??= error2;
32880
- });
32893
+ transcriptEvents.enqueue(() => onThought(thoughtText));
32881
32894
  }
32882
32895
  return;
32883
32896
  }
@@ -32912,9 +32925,7 @@ class AcpxBridgeTransport {
32912
32925
  return;
32913
32926
  }
32914
32927
  });
32915
- await segmentChain;
32916
- await toolEventChain;
32917
- await thoughtChain;
32928
+ await transcriptEvents.drain();
32918
32929
  await planChain;
32919
32930
  await usageChain;
32920
32931
  await commandsChain;
@@ -32926,14 +32937,9 @@ class AcpxBridgeTransport {
32926
32937
  throw deferred;
32927
32938
  }
32928
32939
  const summary = buildOverflowSummary(overflowCount);
32929
- if (segmentError) {
32930
- throw segmentError;
32931
- }
32932
- if (toolEventError) {
32933
- throw toolEventError;
32934
- }
32935
- if (thoughtError) {
32936
- throw thoughtError;
32940
+ const transcriptError2 = transcriptEvents.getError();
32941
+ if (transcriptError2) {
32942
+ throw transcriptError2;
32937
32943
  }
32938
32944
  if (planError) {
32939
32945
  throw planError;
@@ -32948,14 +32954,9 @@ class AcpxBridgeTransport {
32948
32954
 
32949
32955
  ${result.text}` : "" };
32950
32956
  }
32951
- if (segmentError) {
32952
- throw segmentError;
32953
- }
32954
- if (toolEventError) {
32955
- throw toolEventError;
32956
- }
32957
- if (thoughtError) {
32958
- throw thoughtError;
32957
+ const transcriptError = transcriptEvents.getError();
32958
+ if (transcriptError) {
32959
+ throw transcriptError;
32959
32960
  }
32960
32961
  if (planError) {
32961
32962
  throw planError;
@@ -33756,6 +33757,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
33756
33757
  let onCommands;
33757
33758
  let rawStream = false;
33758
33759
  let driver;
33760
+ let onBeforeActivityEvent;
33759
33761
  if (options === undefined) {
33760
33762
  toolEventMode = "text";
33761
33763
  onToolEvent = undefined;
@@ -33770,6 +33772,7 @@ function createStreamingPromptState(formatToolCalls = false, options) {
33770
33772
  onCommands = options.onCommands;
33771
33773
  rawStream = options.rawStream ?? false;
33772
33774
  driver = options.driver?.trim().toLowerCase() || undefined;
33775
+ onBeforeActivityEvent = options.onBeforeActivityEvent;
33773
33776
  toolEventMode = resolveToolEventMode({
33774
33777
  toolEventMode: options.mode,
33775
33778
  onToolEvent
@@ -33782,10 +33785,14 @@ function createStreamingPromptState(formatToolCalls = false, options) {
33782
33785
  pendingLine: "",
33783
33786
  formatToolCalls,
33784
33787
  emittedToolCallIds: new Set,
33788
+ positionedToolCallIds: new Set,
33785
33789
  toolCalls: new Map,
33786
33790
  toolEventMode,
33787
33791
  driver,
33788
33792
  rawStream,
33793
+ lastTextTail: "",
33794
+ activitySinceLastText: false,
33795
+ onBeforeActivityEvent,
33789
33796
  onToolEvent,
33790
33797
  onThought,
33791
33798
  onPlan,
@@ -33828,6 +33835,14 @@ function parseStreamingChunks(state, line) {
33828
33835
  if (!update)
33829
33836
  return;
33830
33837
  if (update.sessionUpdate === "tool_call" || update.sessionUpdate === "tool_call_update") {
33838
+ const isInitialToolEvent = typeof update.toolCallId === "string" ? !state.positionedToolCallIds.has(update.toolCallId) : update.sessionUpdate === "tool_call";
33839
+ if (isInitialToolEvent) {
33840
+ if (update.toolCallId)
33841
+ state.positionedToolCallIds.add(update.toolCallId);
33842
+ markActivityBoundary(state);
33843
+ } else {
33844
+ flushBeforeActivityEvent(state);
33845
+ }
33831
33846
  const wantsStructured = state.toolEventMode === "structured" || state.toolEventMode === "both";
33832
33847
  const wantsText = (state.toolEventMode === "text" || state.toolEventMode === "both") && state.formatToolCalls;
33833
33848
  if (wantsStructured && state.onToolEvent) {
@@ -33876,6 +33891,7 @@ function parseStreamingChunks(state, line) {
33876
33891
  if (isThoughtChunk) {
33877
33892
  const chunk2 = update.content.text;
33878
33893
  if (chunk2.length > 0) {
33894
+ markActivityBoundary(state);
33879
33895
  state.onThought?.(chunk2);
33880
33896
  }
33881
33897
  return;
@@ -33884,10 +33900,22 @@ function parseStreamingChunks(state, line) {
33884
33900
  if (!isMessageChunk)
33885
33901
  return;
33886
33902
  state.hasAgentMessage = true;
33887
- const chunk = update.content.text ?? "";
33903
+ let chunk = update.content.text ?? "";
33888
33904
  if (chunk.length === 0)
33889
33905
  return;
33906
+ const messageId = typeof update.messageId === "string" && update.messageId.length > 0 ? update.messageId : undefined;
33907
+ const messageIdChanged = state.lastMessageId !== undefined && messageId !== undefined && state.lastMessageId !== messageId;
33908
+ const fallbackBoundary = state.activitySinceLastText && (state.lastMessageId === undefined || messageId === undefined) && endsWithSentenceTerminal(state.lastTextTail);
33909
+ if ((messageIdChanged || fallbackBoundary) && !hasParagraphBoundaryAtJoin(state.lastTextTail, chunk)) {
33910
+ chunk = `
33911
+
33912
+ ${chunk}`;
33913
+ state.lastTextTail = "";
33914
+ }
33890
33915
  state.buffer += chunk;
33916
+ state.lastMessageId = messageId;
33917
+ state.activitySinceLastText = false;
33918
+ state.lastTextTail = `${state.lastTextTail}${chunk}`.slice(-256);
33891
33919
  if (state.rawStream)
33892
33920
  return;
33893
33921
  let boundary;
@@ -33901,6 +33929,24 @@ function parseStreamingChunks(state, line) {
33901
33929
  }
33902
33930
  }
33903
33931
  }
33932
+ function endsWithSentenceTerminal(text) {
33933
+ return SENTENCE_TERMINAL_AT_END.test(text.trimEnd());
33934
+ }
33935
+ function hasParagraphBoundaryAtJoin(left, right) {
33936
+ const leftHasBoundary = PARAGRAPH_BOUNDARY_AT_END.test(left);
33937
+ const rightHasBoundary = PARAGRAPH_BOUNDARY_AT_START.test(right);
33938
+ const boundarySpansJoin = LINE_BREAK_AT_END.test(left) && LINE_BREAK_AT_START.test(right);
33939
+ const crlfBoundarySpansJoin = PARTIAL_CRLF_PARAGRAPH_BOUNDARY_AT_END.test(left) && right.startsWith(`
33940
+ `);
33941
+ return leftHasBoundary || rightHasBoundary || boundarySpansJoin || crlfBoundarySpansJoin;
33942
+ }
33943
+ function markActivityBoundary(state) {
33944
+ flushBeforeActivityEvent(state);
33945
+ state.activitySinceLastText = state.hasAgentMessage;
33946
+ }
33947
+ function flushBeforeActivityEvent(state) {
33948
+ state.onBeforeActivityEvent?.();
33949
+ }
33904
33950
  function formatToolCallEvent(update, sessionUpdate) {
33905
33951
  if (!update)
33906
33952
  return null;
@@ -34162,10 +34208,16 @@ function isGenericToolTitle(kind, title) {
34162
34208
  }
34163
34209
  return false;
34164
34210
  }
34165
- var USAGE_BREAKDOWN_FIELDS;
34211
+ var SENTENCE_TERMINAL_AT_END, PARAGRAPH_BOUNDARY_AT_END, PARAGRAPH_BOUNDARY_AT_START, LINE_BREAK_AT_END, LINE_BREAK_AT_START, PARTIAL_CRLF_PARAGRAPH_BOUNDARY_AT_END, USAGE_BREAKDOWN_FIELDS;
34166
34212
  var init_streaming_prompt = __esm(() => {
34167
34213
  init_background_followup();
34168
34214
  init_tool_kind_emoji();
34215
+ SENTENCE_TERMINAL_AT_END = /(?:\p{Sentence_Terminal}|…|⋯)[\p{Close_Punctuation}\p{Final_Punctuation}"“”‘’*_~`]*$/u;
34216
+ PARAGRAPH_BOUNDARY_AT_END = /\r?\n[\t ]*\r?\n[\t ]*$/;
34217
+ PARAGRAPH_BOUNDARY_AT_START = /^[\t ]*\r?\n[\t ]*\r?\n/;
34218
+ LINE_BREAK_AT_END = /\r?\n[\t ]*$/;
34219
+ LINE_BREAK_AT_START = /^[\t ]*\r?\n/;
34220
+ PARTIAL_CRLF_PARAGRAPH_BOUNDARY_AT_END = /\r?\n[\t ]*\r$/;
34169
34221
  USAGE_BREAKDOWN_FIELDS = [
34170
34222
  ["inputTokens", ["inputTokens", "input_tokens"]],
34171
34223
  ["outputTokens", ["outputTokens", "output_tokens"]],
@@ -35074,12 +35126,7 @@ ${baseText}` : "" };
35074
35126
  let stdout2 = "";
35075
35127
  let stderr = "";
35076
35128
  let lastReplyAt = now();
35077
- let segmentChain = Promise.resolve();
35078
- let segmentError;
35079
- let toolEventChain = Promise.resolve();
35080
- let toolEventError;
35081
- let thoughtChain = Promise.resolve();
35082
- let thoughtError;
35129
+ const transcriptEvents = createSerializedCallbackQueue();
35083
35130
  let planChain = Promise.resolve();
35084
35131
  let planError;
35085
35132
  let usageChain = Promise.resolve();
@@ -35091,22 +35138,22 @@ ${baseText}` : "" };
35091
35138
  const userOnPlan = onPlan;
35092
35139
  const userOnUsage = onUsage;
35093
35140
  const userOnCommands = onCommands;
35141
+ let flushPendingText = () => {};
35094
35142
  const state = createStreamingPromptState(formatToolCalls, {
35095
35143
  mode: toolEventMode,
35096
35144
  driver,
35097
35145
  rawStream,
35146
+ onBeforeActivityEvent: () => {
35147
+ flushPendingText();
35148
+ },
35098
35149
  ...userOnToolEvent ? {
35099
35150
  onToolEvent: (event) => {
35100
- toolEventChain = toolEventChain.then(() => userOnToolEvent(event)).catch((error2) => {
35101
- toolEventError ??= error2;
35102
- });
35151
+ transcriptEvents.enqueue(() => userOnToolEvent(event));
35103
35152
  }
35104
35153
  } : {},
35105
35154
  ...userOnThought ? {
35106
35155
  onThought: (chunk) => {
35107
- thoughtChain = thoughtChain.then(() => userOnThought(chunk)).catch((error2) => {
35108
- thoughtError ??= error2;
35109
- });
35156
+ transcriptEvents.enqueue(() => userOnThought(chunk));
35110
35157
  }
35111
35158
  } : {},
35112
35159
  ...userOnPlan ? {
@@ -35136,12 +35183,11 @@ ${baseText}` : "" };
35136
35183
  ...replyContext ? { replyContext } : {}
35137
35184
  }) : null;
35138
35185
  const feedSegment = (segment) => {
35139
- if (onSegment) {
35140
- segmentChain = segmentChain.then(() => onSegment(segment)).catch((error2) => {
35141
- segmentError ??= error2;
35142
- });
35143
- }
35144
- sink2?.feedSegment(segment);
35186
+ transcriptEvents.enqueue(async () => {
35187
+ const segmentResult = onSegment?.(segment);
35188
+ sink2?.feedSegment(segment);
35189
+ await segmentResult;
35190
+ });
35145
35191
  lastReplyAt = now();
35146
35192
  };
35147
35193
  const flushBuffer = () => {
@@ -35151,6 +35197,12 @@ ${baseText}` : "" };
35151
35197
  feedSegment(remaining);
35152
35198
  }
35153
35199
  };
35200
+ flushPendingText = () => {
35201
+ for (const segment of state.segments.splice(0)) {
35202
+ feedSegment(segment);
35203
+ }
35204
+ flushBuffer();
35205
+ };
35154
35206
  const timer = setIntervalFn(() => {
35155
35207
  if (state.buffer.trim().length > 0 && now() - lastReplyAt >= maxSegmentWaitMs) {
35156
35208
  flushBuffer();
@@ -35177,31 +35229,23 @@ ${baseText}` : "" };
35177
35229
  if (remaining.length > 0) {
35178
35230
  feedSegment(remaining);
35179
35231
  }
35180
- const { overflowCount } = sink2?.finalize() ?? { overflowCount: 0 };
35181
- Promise.all([
35182
- sink2?.drain({ timeoutMs: 30000 }) ?? Promise.resolve(),
35183
- segmentChain,
35184
- toolEventChain,
35185
- thoughtChain,
35186
- planChain,
35187
- usageChain,
35188
- commandsChain
35189
- ]).then(() => {
35232
+ (async () => {
35233
+ await Promise.all([
35234
+ transcriptEvents.drain(),
35235
+ planChain,
35236
+ usageChain,
35237
+ commandsChain
35238
+ ]);
35239
+ const { overflowCount } = sink2?.finalize() ?? { overflowCount: 0 };
35240
+ await (sink2?.drain({ timeoutMs: 30000 }) ?? Promise.resolve());
35190
35241
  const deferred = sink2?.getPendingError();
35191
35242
  if (deferred) {
35192
35243
  reject(deferred);
35193
35244
  return;
35194
35245
  }
35195
- if (segmentError) {
35196
- reject(segmentError);
35197
- return;
35198
- }
35199
- if (toolEventError) {
35200
- reject(toolEventError);
35201
- return;
35202
- }
35203
- if (thoughtError) {
35204
- reject(thoughtError);
35246
+ const transcriptError = transcriptEvents.getError();
35247
+ if (transcriptError) {
35248
+ reject(transcriptError);
35205
35249
  return;
35206
35250
  }
35207
35251
  if (planError) {
@@ -35220,7 +35264,7 @@ ${baseText}` : "" };
35220
35264
  result: { code: code ?? 1, stdout: stdout2, stderr },
35221
35265
  overflowCount
35222
35266
  });
35223
- }).catch((error2) => {
35267
+ })().catch((error2) => {
35224
35268
  reject(error2);
35225
35269
  });
35226
35270
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.19.0",
3
+ "version": "0.19.2",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",