@ganglion/xacpx 0.17.0-beta.0 → 0.17.0-beta.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.
package/dist/cli.js CHANGED
@@ -33889,6 +33889,7 @@ var init_workspace_fs = __esm(() => {
33889
33889
 
33890
33890
  // src/control/control-service.ts
33891
33891
  import path15 from "node:path";
33892
+ import { randomUUID as randomUUID3 } from "node:crypto";
33892
33893
 
33893
33894
  class ControlService {
33894
33895
  deps;
@@ -34056,12 +34057,23 @@ class ControlService {
34056
34057
  return task;
34057
34058
  }
34058
34059
  inFlight = new Map;
34060
+ queues = new Map;
34061
+ draining = new Set;
34062
+ emitQueueUpdated(chatKey, sessionAlias) {
34063
+ const items = (this.queues.get(turnKey(chatKey, sessionAlias)) ?? []).map((q) => ({
34064
+ id: q.id,
34065
+ textPreview: q.text.length > QUEUE_PREVIEW_MAX ? q.text.slice(0, QUEUE_PREVIEW_MAX) : q.text,
34066
+ enqueuedAt: q.enqueuedAt
34067
+ }));
34068
+ this.deps.events.emit({ type: "queue-updated", chatKey, sessionAlias, items });
34069
+ }
34059
34070
  async prompt(input) {
34060
34071
  return this.executeTurn({
34061
34072
  chatKey: input.chatKey,
34062
34073
  sessionAlias: input.sessionAlias,
34063
34074
  text: input.text,
34064
34075
  senderId: input.senderId,
34076
+ queueable: true,
34065
34077
  ...input.isOwner !== undefined ? { isOwner: input.isOwner } : {},
34066
34078
  ...input.accountId !== undefined ? { accountId: input.accountId } : {},
34067
34079
  ...input.media !== undefined ? { media: input.media } : {}
@@ -34081,14 +34093,34 @@ class ControlService {
34081
34093
  }
34082
34094
  async executeTurn(params) {
34083
34095
  const key = turnKey(params.chatKey, params.sessionAlias);
34084
- const existing = this.inFlight.get(key);
34085
- if (existing) {
34086
- if (!existing.controller.signal.aborted) {
34096
+ if (!params.drained) {
34097
+ const existing = this.inFlight.get(key);
34098
+ const busy = this.draining.has(key) || existing !== undefined && !existing.controller.signal.aborted;
34099
+ if (busy) {
34100
+ if (params.queueable) {
34101
+ const id = randomUUID3();
34102
+ const item = {
34103
+ id,
34104
+ text: params.text,
34105
+ enqueuedAt: new Date().toISOString(),
34106
+ senderId: params.senderId,
34107
+ ...params.isOwner !== undefined ? { isOwner: params.isOwner } : {},
34108
+ ...params.accountId !== undefined ? { accountId: params.accountId } : {},
34109
+ ...params.media !== undefined ? { media: params.media } : {}
34110
+ };
34111
+ const q = this.queues.get(key) ?? [];
34112
+ q.push(item);
34113
+ this.queues.set(key, q);
34114
+ this.emitQueueUpdated(params.chatKey, params.sessionAlias);
34115
+ return { ok: true, queued: true, queueItemId: id };
34116
+ }
34087
34117
  return { ok: false, errorMessage: "turn-already-running" };
34088
34118
  }
34089
- await raceWithTimeout(existing.settled, CANCEL_DRAIN_TIMEOUT_MS);
34090
- if (this.inFlight.has(key)) {
34091
- return { ok: false, errorMessage: "turn-already-running" };
34119
+ if (existing) {
34120
+ await raceWithTimeout(existing.settled, CANCEL_DRAIN_TIMEOUT_MS);
34121
+ if (this.inFlight.has(key)) {
34122
+ return { ok: false, errorMessage: "turn-already-running" };
34123
+ }
34092
34124
  }
34093
34125
  }
34094
34126
  const controller = new AbortController;
@@ -34103,6 +34135,9 @@ class ControlService {
34103
34135
  resolveSettled = resolve4;
34104
34136
  });
34105
34137
  this.inFlight.set(key, { controller, settled });
34138
+ if (params.drained) {
34139
+ this.draining.delete(key);
34140
+ }
34106
34141
  let internalAlias;
34107
34142
  let wasArchived = false;
34108
34143
  let priorTransportSession;
@@ -34115,8 +34150,8 @@ class ControlService {
34115
34150
  try {
34116
34151
  await this.deps.sessions.useSession(params.chatKey, params.sessionAlias);
34117
34152
  } catch (error2) {
34118
- this.inFlight.delete(key);
34119
34153
  resolveSettled();
34154
+ this.advanceQueue(key, params.chatKey, params.sessionAlias);
34120
34155
  return { ok: false, errorMessage: toErrorMessage(error2) };
34121
34156
  }
34122
34157
  if (wasArchived) {
@@ -34127,7 +34162,8 @@ class ControlService {
34127
34162
  chatKey: params.chatKey,
34128
34163
  sessionAlias: params.sessionAlias,
34129
34164
  ...params.turnStarted?.prompt ? { prompt: params.turnStarted.prompt } : {},
34130
- ...params.turnStarted?.scheduled ? { scheduled: params.turnStarted.scheduled } : {}
34165
+ ...params.turnStarted?.scheduled ? { scheduled: params.turnStarted.scheduled } : {},
34166
+ ...params.turnStarted?.queueItemId ? { queueItemId: params.turnStarted.queueItemId } : {}
34131
34167
  });
34132
34168
  let streamMode = false;
34133
34169
  try {
@@ -34251,8 +34287,9 @@ ${chunk}` : chunk
34251
34287
  });
34252
34288
  return { ok: false, errorMessage };
34253
34289
  } finally {
34254
- this.inFlight.delete(key);
34255
- resolveSettled();
34290
+ if ((this.queues.get(key)?.length ?? 0) > 0) {
34291
+ this.draining.add(key);
34292
+ }
34256
34293
  if (internalAlias && priorTransportSession) {
34257
34294
  try {
34258
34295
  const after = await this.deps.sessions.getSession(internalAlias);
@@ -34261,6 +34298,33 @@ ${chunk}` : chunk
34261
34298
  }
34262
34299
  } catch {}
34263
34300
  }
34301
+ resolveSettled();
34302
+ this.advanceQueue(key, params.chatKey, params.sessionAlias);
34303
+ }
34304
+ }
34305
+ advanceQueue(key, chatKey, sessionAlias) {
34306
+ const q = this.queues.get(key);
34307
+ const next = q?.shift();
34308
+ if (q && q.length === 0)
34309
+ this.queues.delete(key);
34310
+ if (next) {
34311
+ this.draining.add(key);
34312
+ this.emitQueueUpdated(chatKey, sessionAlias);
34313
+ this.executeTurn({
34314
+ chatKey,
34315
+ sessionAlias,
34316
+ text: next.text,
34317
+ senderId: next.senderId,
34318
+ queueable: true,
34319
+ drained: true,
34320
+ ...next.isOwner !== undefined ? { isOwner: next.isOwner } : {},
34321
+ ...next.accountId !== undefined ? { accountId: next.accountId } : {},
34322
+ ...next.media !== undefined ? { media: next.media } : {},
34323
+ turnStarted: { queueItemId: next.id }
34324
+ });
34325
+ } else {
34326
+ this.draining.delete(key);
34327
+ this.inFlight.delete(key);
34264
34328
  }
34265
34329
  }
34266
34330
  cancelTurn(chatKey, sessionAlias) {
@@ -34271,6 +34335,20 @@ ${chunk}` : chunk
34271
34335
  entry.controller.abort();
34272
34336
  return true;
34273
34337
  }
34338
+ cancelQueuedItem(chatKey, sessionAlias, itemId) {
34339
+ const key = turnKey(chatKey, sessionAlias);
34340
+ const q = this.queues.get(key);
34341
+ if (!q)
34342
+ return { cancelled: false };
34343
+ const i = q.findIndex((x) => x.id === itemId);
34344
+ if (i < 0)
34345
+ return { cancelled: false };
34346
+ q.splice(i, 1);
34347
+ if (q.length === 0)
34348
+ this.queues.delete(key);
34349
+ this.emitQueueUpdated(chatKey, sessionAlias);
34350
+ return { cancelled: true };
34351
+ }
34274
34352
  async executeCommand(input) {
34275
34353
  const chunks = [];
34276
34354
  const response = await this.deps.agent.chat({
@@ -34332,7 +34410,7 @@ function buildControlMetadata(senderId, isOwner) {
34332
34410
  ...isOwner === undefined ? {} : { isOwner }
34333
34411
  };
34334
34412
  }
34335
- var CANCEL_DRAIN_TIMEOUT_MS = 5000;
34413
+ var CANCEL_DRAIN_TIMEOUT_MS = 5000, QUEUE_PREVIEW_MAX = 120;
34336
34414
  var init_control_service = __esm(() => {
34337
34415
  init_channel_scope();
34338
34416
  init_native_session_history();
@@ -34340,7 +34418,7 @@ var init_control_service = __esm(() => {
34340
34418
  });
34341
34419
 
34342
34420
  // src/control/terminal-service.ts
34343
- import { randomUUID as randomUUID3 } from "node:crypto";
34421
+ import { randomUUID as randomUUID4 } from "node:crypto";
34344
34422
  import { createRequire as createRequire6 } from "node:module";
34345
34423
  import { spawn as spawnPty2 } from "node-pty";
34346
34424
  function scrubEnv() {
@@ -34392,7 +34470,7 @@ function createTerminalService(deps) {
34392
34470
  create({ cwd, cols, rows }) {
34393
34471
  if (platform === "win32")
34394
34472
  throw new Error("terminal-unsupported-platform");
34395
- const terminalId = randomUUID3();
34473
+ const terminalId = randomUUID4();
34396
34474
  const handle = spawn11(defaultShell(platform), [], { name: "xterm-256color", cols, rows, cwd, env: scrubEnv() });
34397
34475
  const session3 = { handle, seq: 0, idleTimer: null };
34398
34476
  sessions.set(terminalId, session3);
@@ -34646,7 +34724,7 @@ __export(exports_main, {
34646
34724
  main: () => main,
34647
34725
  buildApp: () => buildApp
34648
34726
  });
34649
- import { randomUUID as randomUUID4 } from "node:crypto";
34727
+ import { randomUUID as randomUUID5 } from "node:crypto";
34650
34728
  import { homedir as homedir14 } from "node:os";
34651
34729
  import { dirname as dirname13, join as join23 } from "node:path";
34652
34730
  import { fileURLToPath as fileURLToPath5 } from "node:url";
@@ -35026,7 +35104,7 @@ async function buildApp(paths, deps = {}) {
35026
35104
  };
35027
35105
  orchestration3 = new OrchestrationService({
35028
35106
  now: deps.loggerNow ?? (() => new Date),
35029
- createId: () => randomUUID4(),
35107
+ createId: () => randomUUID5(),
35030
35108
  config: config4,
35031
35109
  loadState: async () => JSON.parse(JSON.stringify(state)),
35032
35110
  saveState: async (nextState) => {
@@ -37130,7 +37208,7 @@ var init_doctor2 = __esm(async () => {
37130
37208
 
37131
37209
  // src/cli.ts
37132
37210
  init_core_home();
37133
- import { randomUUID as randomUUID5 } from "node:crypto";
37211
+ import { randomUUID as randomUUID6 } from "node:crypto";
37134
37212
  import { homedir as homedir20 } from "node:os";
37135
37213
  import { dirname as dirname15, join as join27, sep as sep2 } from "node:path";
37136
37214
  import { fileURLToPath as fileURLToPath7 } from "node:url";
@@ -52806,7 +52884,7 @@ async function prepareMcpCoordinatorStartup(input) {
52806
52884
  return { kind: "external-coordinator" };
52807
52885
  }
52808
52886
  function createMcpStdioIdentityResolver(input) {
52809
- const instanceId = randomUUID5().slice(0, 8);
52887
+ const instanceId = randomUUID6().slice(0, 8);
52810
52888
  return async (context) => {
52811
52889
  const parsedCoordinatorSession = input.parsedCoordinatorSession?.trim() || null;
52812
52890
  const workspace3 = input.workspace?.trim() || null;
@@ -6,6 +6,13 @@ export interface ScheduledOrigin {
6
6
  taskId: string;
7
7
  executeAt: string;
8
8
  }
9
+ /** A single pending item in the per-session server-side prompt queue, as surfaced on
10
+ * the wire by `queue-updated`. `textPreview` is truncated server-side (~120 chars). */
11
+ export interface QueuedItemInfo {
12
+ id: string;
13
+ textPreview: string;
14
+ enqueuedAt: string;
15
+ }
9
16
  export type ControlEvent = {
10
17
  type: "turn-output";
11
18
  chatKey: string;
@@ -17,6 +24,12 @@ export type ControlEvent = {
17
24
  sessionAlias: string;
18
25
  prompt?: string;
19
26
  scheduled?: ScheduledOrigin;
27
+ queueItemId?: string;
28
+ } | {
29
+ type: "queue-updated";
30
+ chatKey: string;
31
+ sessionAlias: string;
32
+ items: QueuedItemInfo[];
20
33
  } | {
21
34
  type: "tool-event";
22
35
  chatKey: string;
@@ -93,6 +93,23 @@ export interface ControlPromptResult {
93
93
  ok: boolean;
94
94
  text?: string;
95
95
  errorMessage?: string;
96
+ /** True when this prompt did not run immediately and was instead appended to the
97
+ * per-session server-side queue (a turn was already in flight). */
98
+ queued?: boolean;
99
+ /** Id of the queued item, present only when `queued` is true. Used to cancel it via
100
+ * `cancelQueuedItem` before it drains. */
101
+ queueItemId?: string;
102
+ }
103
+ /** A prompt held in the per-session server-side queue while a turn is in flight.
104
+ * Runs through the same `executeTurn` machinery as a manual prompt once drained. */
105
+ export interface QueuedPrompt {
106
+ id: string;
107
+ text: string;
108
+ enqueuedAt: string;
109
+ senderId: string;
110
+ isOwner?: boolean;
111
+ accountId?: string;
112
+ media?: PromptAttachmentRef[];
96
113
  }
97
114
  /** A turn started by a fired scheduled task. Runs through the same agent + turn-event
98
115
  * machinery as a normal prompt, so it streams live and persists to history — but it
@@ -173,6 +190,9 @@ export declare class ControlService {
173
190
  getOrchestrationTask(taskId: string): Promise<OrchestrationTaskRecord | null>;
174
191
  cancelOrchestrationTask(input: CancelTaskInput): Promise<OrchestrationTaskRecord>;
175
192
  private readonly inFlight;
193
+ private readonly queues;
194
+ private readonly draining;
195
+ private emitQueueUpdated;
176
196
  prompt(input: ControlPromptInput): Promise<ControlPromptResult>;
177
197
  /** Run a fired scheduled task as a real turn through the same machinery as a manual
178
198
  * prompt — so it streams live and persists to history — while tagging turn-started
@@ -180,7 +200,15 @@ export declare class ControlService {
180
200
  * the web can badge it. Owner-authorized: the task was owner-gated at creation. */
181
201
  runScheduledTurn(input: ControlScheduledTurnInput): Promise<ControlPromptResult>;
182
202
  private executeTurn;
203
+ private advanceQueue;
183
204
  cancelTurn(chatKey: string, sessionAlias: string): boolean;
205
+ /** Remove a pending queued prompt (by id) before it drains. No-ops (returns
206
+ * `{ cancelled: false }`) when the queue or the id is absent/already drained —
207
+ * e.g. a race where the item drained into a running turn just before the cancel
208
+ * arrived. Does NOT touch a turn that is already running (use `cancelTurn`). */
209
+ cancelQueuedItem(chatKey: string, sessionAlias: string, itemId: string): {
210
+ cancelled: boolean;
211
+ };
184
212
  executeCommand(input: ControlExecuteCommandInput): Promise<string>;
185
213
  /** Open an interactive terminal in the session's workspace cwd. Rejected when terminal is disabled. */
186
214
  createTerminal(chatKey: string, sessionAlias: string, cols: number, rows: number): Promise<{
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.17.0-beta.0",
3
+ "version": "0.17.0-beta.1",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",