@ganglion/xacpx 0.16.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
@@ -4727,7 +4727,13 @@ function parseConfig(raw, options = {}) {
4727
4727
  workspaces,
4728
4728
  orchestration: orchestrationConfig,
4729
4729
  later: laterConfig,
4730
- ...language ? { language } : {}
4730
+ ...language ? { language } : {},
4731
+ ...raw.terminal && typeof raw.terminal === "object" ? {
4732
+ terminal: {
4733
+ enabled: raw.terminal.enabled === true,
4734
+ ...typeof raw.terminal.idleTimeoutSeconds === "number" ? { idleTimeoutSeconds: raw.terminal.idleTimeoutSeconds } : {}
4735
+ }
4736
+ } : {}
4731
4737
  };
4732
4738
  }
4733
4739
  function parsePluginConfig(raw, index) {
@@ -25571,6 +25577,15 @@ var init_console_agent = __esm(() => {
25571
25577
  init_i18n();
25572
25578
  });
25573
25579
 
25580
+ // src/config/types.ts
25581
+ function terminalEnabled(config4) {
25582
+ return config4.terminal?.enabled === true;
25583
+ }
25584
+ function terminalIdleTimeoutSeconds(config4) {
25585
+ const v = config4.terminal?.idleTimeoutSeconds;
25586
+ return typeof v === "number" && v > 0 ? v : 900;
25587
+ }
25588
+
25574
25589
  // src/orchestration/orchestration-server.ts
25575
25590
  import { chmod as chmod4, rm as rm8 } from "node:fs/promises";
25576
25591
  import { createServer } from "node:net";
@@ -33874,6 +33889,7 @@ var init_workspace_fs = __esm(() => {
33874
33889
 
33875
33890
  // src/control/control-service.ts
33876
33891
  import path15 from "node:path";
33892
+ import { randomUUID as randomUUID3 } from "node:crypto";
33877
33893
 
33878
33894
  class ControlService {
33879
33895
  deps;
@@ -34041,12 +34057,23 @@ class ControlService {
34041
34057
  return task;
34042
34058
  }
34043
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
+ }
34044
34070
  async prompt(input) {
34045
34071
  return this.executeTurn({
34046
34072
  chatKey: input.chatKey,
34047
34073
  sessionAlias: input.sessionAlias,
34048
34074
  text: input.text,
34049
34075
  senderId: input.senderId,
34076
+ queueable: true,
34050
34077
  ...input.isOwner !== undefined ? { isOwner: input.isOwner } : {},
34051
34078
  ...input.accountId !== undefined ? { accountId: input.accountId } : {},
34052
34079
  ...input.media !== undefined ? { media: input.media } : {}
@@ -34066,14 +34093,34 @@ class ControlService {
34066
34093
  }
34067
34094
  async executeTurn(params) {
34068
34095
  const key = turnKey(params.chatKey, params.sessionAlias);
34069
- const existing = this.inFlight.get(key);
34070
- if (existing) {
34071
- 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
+ }
34072
34117
  return { ok: false, errorMessage: "turn-already-running" };
34073
34118
  }
34074
- await raceWithTimeout(existing.settled, CANCEL_DRAIN_TIMEOUT_MS);
34075
- if (this.inFlight.has(key)) {
34076
- 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
+ }
34077
34124
  }
34078
34125
  }
34079
34126
  const controller = new AbortController;
@@ -34088,6 +34135,9 @@ class ControlService {
34088
34135
  resolveSettled = resolve4;
34089
34136
  });
34090
34137
  this.inFlight.set(key, { controller, settled });
34138
+ if (params.drained) {
34139
+ this.draining.delete(key);
34140
+ }
34091
34141
  let internalAlias;
34092
34142
  let wasArchived = false;
34093
34143
  let priorTransportSession;
@@ -34100,8 +34150,8 @@ class ControlService {
34100
34150
  try {
34101
34151
  await this.deps.sessions.useSession(params.chatKey, params.sessionAlias);
34102
34152
  } catch (error2) {
34103
- this.inFlight.delete(key);
34104
34153
  resolveSettled();
34154
+ this.advanceQueue(key, params.chatKey, params.sessionAlias);
34105
34155
  return { ok: false, errorMessage: toErrorMessage(error2) };
34106
34156
  }
34107
34157
  if (wasArchived) {
@@ -34112,7 +34162,8 @@ class ControlService {
34112
34162
  chatKey: params.chatKey,
34113
34163
  sessionAlias: params.sessionAlias,
34114
34164
  ...params.turnStarted?.prompt ? { prompt: params.turnStarted.prompt } : {},
34115
- ...params.turnStarted?.scheduled ? { scheduled: params.turnStarted.scheduled } : {}
34165
+ ...params.turnStarted?.scheduled ? { scheduled: params.turnStarted.scheduled } : {},
34166
+ ...params.turnStarted?.queueItemId ? { queueItemId: params.turnStarted.queueItemId } : {}
34116
34167
  });
34117
34168
  let streamMode = false;
34118
34169
  try {
@@ -34236,8 +34287,9 @@ ${chunk}` : chunk
34236
34287
  });
34237
34288
  return { ok: false, errorMessage };
34238
34289
  } finally {
34239
- this.inFlight.delete(key);
34240
- resolveSettled();
34290
+ if ((this.queues.get(key)?.length ?? 0) > 0) {
34291
+ this.draining.add(key);
34292
+ }
34241
34293
  if (internalAlias && priorTransportSession) {
34242
34294
  try {
34243
34295
  const after = await this.deps.sessions.getSession(internalAlias);
@@ -34246,6 +34298,33 @@ ${chunk}` : chunk
34246
34298
  }
34247
34299
  } catch {}
34248
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);
34249
34328
  }
34250
34329
  }
34251
34330
  cancelTurn(chatKey, sessionAlias) {
@@ -34256,6 +34335,20 @@ ${chunk}` : chunk
34256
34335
  entry.controller.abort();
34257
34336
  return true;
34258
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
+ }
34259
34352
  async executeCommand(input) {
34260
34353
  const chunks = [];
34261
34354
  const response = await this.deps.agent.chat({
@@ -34273,6 +34366,23 @@ ${chunk}` : chunk
34273
34366
  return chunks.join(`
34274
34367
  `);
34275
34368
  }
34369
+ async createTerminal(chatKey, sessionAlias, cols, rows) {
34370
+ if (!this.deps.terminalEnabled())
34371
+ throw new Error("terminal-disabled");
34372
+ const session3 = await this.resolveControlSession(chatKey, sessionAlias);
34373
+ if (!session3)
34374
+ throw new Error("session-not-found");
34375
+ return this.deps.terminal.create({ cwd: session3.cwd, cols, rows });
34376
+ }
34377
+ writeTerminal(terminalId, data) {
34378
+ this.deps.terminal.write(terminalId, data);
34379
+ }
34380
+ resizeTerminal(terminalId, cols, rows) {
34381
+ this.deps.terminal.resize(terminalId, cols, rows);
34382
+ }
34383
+ closeTerminal(terminalId) {
34384
+ this.deps.terminal.close(terminalId);
34385
+ }
34276
34386
  }
34277
34387
  async function raceWithTimeout(promise2, ms) {
34278
34388
  let timer;
@@ -34300,13 +34410,140 @@ function buildControlMetadata(senderId, isOwner) {
34300
34410
  ...isOwner === undefined ? {} : { isOwner }
34301
34411
  };
34302
34412
  }
34303
- var CANCEL_DRAIN_TIMEOUT_MS = 5000;
34413
+ var CANCEL_DRAIN_TIMEOUT_MS = 5000, QUEUE_PREVIEW_MAX = 120;
34304
34414
  var init_control_service = __esm(() => {
34305
34415
  init_channel_scope();
34306
34416
  init_native_session_history();
34307
34417
  init_workspace_fs();
34308
34418
  });
34309
34419
 
34420
+ // src/control/terminal-service.ts
34421
+ import { randomUUID as randomUUID4 } from "node:crypto";
34422
+ import { createRequire as createRequire6 } from "node:module";
34423
+ import { spawn as spawnPty2 } from "node-pty";
34424
+ function scrubEnv() {
34425
+ const out = {};
34426
+ for (const [k, v] of Object.entries(process.env)) {
34427
+ if (v === undefined)
34428
+ continue;
34429
+ if (SENSITIVE_ENV_KEYS.includes(k) || k.startsWith("XACPX_"))
34430
+ continue;
34431
+ out[k] = v;
34432
+ }
34433
+ out.TERM = "xterm-256color";
34434
+ out.LANG = out.LANG ?? "en_US.UTF-8";
34435
+ return out;
34436
+ }
34437
+ function defaultShell(platform) {
34438
+ if (process.env.SHELL)
34439
+ return process.env.SHELL;
34440
+ return platform === "darwin" ? "/bin/zsh" : "/bin/bash";
34441
+ }
34442
+ function realPtySpawn(file, args, opts) {
34443
+ const helperPath = resolveNodePtyHelperPath(require5.resolve("node-pty/package.json"), process.platform, process.arch);
34444
+ ensureNodePtyHelperExecutable(helperPath);
34445
+ return spawnPty2(file, args, opts);
34446
+ }
34447
+ function createTerminalService(deps) {
34448
+ const spawn11 = deps.spawn ?? realPtySpawn;
34449
+ const platform = deps.platform ?? process.platform;
34450
+ const sessions = new Map;
34451
+ const setTimer = deps.setTimer ?? ((fn, ms) => setTimeout(fn, ms));
34452
+ const clearTimer = deps.clearTimer ?? ((id) => clearTimeout(id));
34453
+ const resetIdle = (terminalId) => {
34454
+ const s = sessions.get(terminalId);
34455
+ if (!s)
34456
+ return;
34457
+ if (s.idleTimer)
34458
+ clearTimer(s.idleTimer);
34459
+ const ms = deps.idleTimeoutSeconds() * 1000;
34460
+ s.idleTimer = setTimer(() => {
34461
+ try {
34462
+ s.handle.kill();
34463
+ } catch {}
34464
+ }, ms);
34465
+ const t2 = s.idleTimer;
34466
+ if (typeof t2.unref === "function")
34467
+ t2.unref();
34468
+ };
34469
+ return {
34470
+ create({ cwd, cols, rows }) {
34471
+ if (platform === "win32")
34472
+ throw new Error("terminal-unsupported-platform");
34473
+ const terminalId = randomUUID4();
34474
+ const handle = spawn11(defaultShell(platform), [], { name: "xterm-256color", cols, rows, cwd, env: scrubEnv() });
34475
+ const session3 = { handle, seq: 0, idleTimer: null };
34476
+ sessions.set(terminalId, session3);
34477
+ handle.onData((data) => {
34478
+ deps.events.emit({ type: "terminal-output", terminalId, seq: session3.seq++, data });
34479
+ });
34480
+ handle.onExit(({ exitCode }) => {
34481
+ if (session3.idleTimer)
34482
+ clearTimer(session3.idleTimer);
34483
+ sessions.delete(terminalId);
34484
+ deps.events.emit({ type: "terminal-exit", terminalId, code: exitCode });
34485
+ });
34486
+ resetIdle(terminalId);
34487
+ return { terminalId };
34488
+ },
34489
+ write(terminalId, data) {
34490
+ const s = sessions.get(terminalId);
34491
+ if (!s)
34492
+ return;
34493
+ try {
34494
+ s.handle.write(data);
34495
+ } catch {}
34496
+ resetIdle(terminalId);
34497
+ },
34498
+ resize(terminalId, cols, rows) {
34499
+ const s = sessions.get(terminalId);
34500
+ if (!s)
34501
+ return;
34502
+ try {
34503
+ s.handle.resize(cols, rows);
34504
+ } catch {}
34505
+ resetIdle(terminalId);
34506
+ },
34507
+ close(terminalId) {
34508
+ const s = sessions.get(terminalId);
34509
+ if (!s)
34510
+ return;
34511
+ try {
34512
+ s.handle.kill();
34513
+ } catch {}
34514
+ },
34515
+ disposeAll() {
34516
+ for (const s of sessions.values()) {
34517
+ if (s.idleTimer)
34518
+ clearTimer(s.idleTimer);
34519
+ try {
34520
+ s.handle.kill();
34521
+ } catch {}
34522
+ }
34523
+ sessions.clear();
34524
+ }
34525
+ };
34526
+ }
34527
+ var require5, SENSITIVE_ENV_KEYS;
34528
+ var init_terminal_service = __esm(() => {
34529
+ init_node_pty_helper();
34530
+ require5 = createRequire6(import.meta.url);
34531
+ SENSITIVE_ENV_KEYS = [
34532
+ "ANTHROPIC_API_KEY",
34533
+ "OPENAI_API_KEY",
34534
+ "OPENROUTER_API_KEY",
34535
+ "GEMINI_API_KEY",
34536
+ "GOOGLE_API_KEY",
34537
+ "DEEPSEEK_API_KEY",
34538
+ "GROQ_API_KEY",
34539
+ "AWS_SECRET_ACCESS_KEY",
34540
+ "AWS_SESSION_TOKEN",
34541
+ "GH_TOKEN",
34542
+ "GITHUB_TOKEN",
34543
+ "NPM_TOKEN"
34544
+ ];
34545
+ });
34546
+
34310
34547
  // src/control/upload-store.ts
34311
34548
  import { mkdtemp as mkdtemp2, readdir as readdir5, rm as rm10, stat as stat4, writeFile as writeFile8 } from "node:fs/promises";
34312
34549
  import { homedir as homedir13 } from "node:os";
@@ -34487,7 +34724,7 @@ __export(exports_main, {
34487
34724
  main: () => main,
34488
34725
  buildApp: () => buildApp
34489
34726
  });
34490
- import { randomUUID as randomUUID3 } from "node:crypto";
34727
+ import { randomUUID as randomUUID5 } from "node:crypto";
34491
34728
  import { homedir as homedir14 } from "node:os";
34492
34729
  import { dirname as dirname13, join as join23 } from "node:path";
34493
34730
  import { fileURLToPath as fileURLToPath5 } from "node:url";
@@ -34867,7 +35104,7 @@ async function buildApp(paths, deps = {}) {
34867
35104
  };
34868
35105
  orchestration3 = new OrchestrationService({
34869
35106
  now: deps.loggerNow ?? (() => new Date),
34870
- createId: () => randomUUID3(),
35107
+ createId: () => randomUUID5(),
34871
35108
  config: config4,
34872
35109
  loadState: async () => JSON.parse(JSON.stringify(state)),
34873
35110
  saveState: async (nextState) => {
@@ -34980,6 +35217,10 @@ async function buildApp(paths, deps = {}) {
34980
35217
  const router3 = new CommandRouter(sessions, transport, config4, configStore, logger2, undefined, orchestration3, quota, scheduledService, deps.channel?.supportsScheduledMessages ? { supportsScheduledMessages: deps.channel.supportsScheduledMessages.bind(deps.channel) } : undefined, deps.channel?.nativeSessionListFormat ? deps.channel.nativeSessionListFormat.bind(deps.channel) : undefined, activeTurns);
34981
35218
  const agent3 = new ConsoleAgent(router3, logger2);
34982
35219
  const controlEvents = createControlEventBus(logger2);
35220
+ const terminalService = createTerminalService({
35221
+ events: controlEvents,
35222
+ idleTimeoutSeconds: () => terminalIdleTimeoutSeconds(config4)
35223
+ });
34983
35224
  const uploadStore = new UploadStore;
34984
35225
  uploadStore.cleanup();
34985
35226
  const uploadCleanupInterval = setInterval(() => void uploadStore.cleanup().catch(() => {}), 60 * 60 * 1000);
@@ -35028,7 +35269,9 @@ async function buildApp(paths, deps = {}) {
35028
35269
  controlEvents.emit({ type: "workspaces-changed" });
35029
35270
  }
35030
35271
  },
35031
- uploadStore
35272
+ uploadStore,
35273
+ terminal: terminalService,
35274
+ terminalEnabled: () => terminalEnabled(config4)
35032
35275
  });
35033
35276
  const workspaceSignature = (cfg) => JSON.stringify(Object.keys(cfg.workspaces).sort().map((name) => {
35034
35277
  const ws = cfg.workspaces[name];
@@ -35120,6 +35363,7 @@ async function buildApp(paths, deps = {}) {
35120
35363
  scheduledScheduler.stop();
35121
35364
  configWatcher.close();
35122
35365
  clearInterval(uploadCleanupInterval);
35366
+ terminalService.disposeAll();
35123
35367
  if (progressHeartbeatInterval !== undefined) {
35124
35368
  clearInterval(progressHeartbeatInterval);
35125
35369
  }
@@ -35262,6 +35506,7 @@ var init_main = __esm(async () => {
35262
35506
  init_render_text();
35263
35507
  init_quota_manager();
35264
35508
  init_control_service();
35509
+ init_terminal_service();
35265
35510
  init_upload_store();
35266
35511
  init_agent_catalog();
35267
35512
  init_config_watcher();
@@ -36963,7 +37208,7 @@ var init_doctor2 = __esm(async () => {
36963
37208
 
36964
37209
  // src/cli.ts
36965
37210
  init_core_home();
36966
- import { randomUUID as randomUUID4 } from "node:crypto";
37211
+ import { randomUUID as randomUUID6 } from "node:crypto";
36967
37212
  import { homedir as homedir20 } from "node:os";
36968
37213
  import { dirname as dirname15, join as join27, sep as sep2 } from "node:path";
36969
37214
  import { fileURLToPath as fileURLToPath7 } from "node:url";
@@ -52639,7 +52884,7 @@ async function prepareMcpCoordinatorStartup(input) {
52639
52884
  return { kind: "external-coordinator" };
52640
52885
  }
52641
52886
  function createMcpStdioIdentityResolver(input) {
52642
- const instanceId = randomUUID4().slice(0, 8);
52887
+ const instanceId = randomUUID6().slice(0, 8);
52643
52888
  return async (context) => {
52644
52889
  const parsedCoordinatorSession = input.parsedCoordinatorSession?.trim() || null;
52645
52890
  const workspace3 = input.workspace?.trim() || null;
@@ -42,6 +42,12 @@ export interface TransportConfig {
42
42
  */
43
43
  preferLocalAgents?: boolean;
44
44
  }
45
+ export interface TerminalConfig {
46
+ /** Default false. When false, control.terminal.create is rejected before any PTY spawns. */
47
+ enabled: boolean;
48
+ /** Idle seconds before a terminal PTY is auto-killed. Defaults to 900 (15 min). */
49
+ idleTimeoutSeconds?: number;
50
+ }
45
51
  export type LoggingLevel = "error" | "info" | "debug";
46
52
  export interface PerfLogConfig {
47
53
  enabled: boolean;
@@ -103,4 +109,7 @@ export interface AppConfig {
103
109
  orchestration: OrchestrationConfig;
104
110
  later?: LaterConfig;
105
111
  language?: Locale;
112
+ terminal?: TerminalConfig;
106
113
  }
114
+ export declare function terminalEnabled(config: AppConfig): boolean;
115
+ export declare function terminalIdleTimeoutSeconds(config: AppConfig): number;
@@ -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;
@@ -64,6 +77,15 @@ export type ControlEvent = {
64
77
  chatKey: string;
65
78
  sessionAlias: string;
66
79
  messages: NativeHistoryMessage[];
80
+ } | {
81
+ type: "terminal-output";
82
+ terminalId: string;
83
+ seq: number;
84
+ data: string;
85
+ } | {
86
+ type: "terminal-exit";
87
+ terminalId: string;
88
+ code: number;
67
89
  } | {
68
90
  type: "orchestration-changed";
69
91
  };
@@ -77,6 +77,8 @@ export interface ControlServiceDeps {
77
77
  remove(name: string): Promise<void>;
78
78
  };
79
79
  uploadStore: UploadStore;
80
+ terminal: import("./terminal-service").TerminalService;
81
+ terminalEnabled: () => boolean;
80
82
  }
81
83
  export interface ControlPromptInput {
82
84
  chatKey: string;
@@ -91,6 +93,23 @@ export interface ControlPromptResult {
91
93
  ok: boolean;
92
94
  text?: string;
93
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[];
94
113
  }
95
114
  /** A turn started by a fired scheduled task. Runs through the same agent + turn-event
96
115
  * machinery as a normal prompt, so it streams live and persists to history — but it
@@ -171,6 +190,9 @@ export declare class ControlService {
171
190
  getOrchestrationTask(taskId: string): Promise<OrchestrationTaskRecord | null>;
172
191
  cancelOrchestrationTask(input: CancelTaskInput): Promise<OrchestrationTaskRecord>;
173
192
  private readonly inFlight;
193
+ private readonly queues;
194
+ private readonly draining;
195
+ private emitQueueUpdated;
174
196
  prompt(input: ControlPromptInput): Promise<ControlPromptResult>;
175
197
  /** Run a fired scheduled task as a real turn through the same machinery as a manual
176
198
  * prompt — so it streams live and persists to history — while tagging turn-started
@@ -178,6 +200,21 @@ export declare class ControlService {
178
200
  * the web can badge it. Owner-authorized: the task was owner-gated at creation. */
179
201
  runScheduledTurn(input: ControlScheduledTurnInput): Promise<ControlPromptResult>;
180
202
  private executeTurn;
203
+ private advanceQueue;
181
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
+ };
182
212
  executeCommand(input: ControlExecuteCommandInput): Promise<string>;
213
+ /** Open an interactive terminal in the session's workspace cwd. Rejected when terminal is disabled. */
214
+ createTerminal(chatKey: string, sessionAlias: string, cols: number, rows: number): Promise<{
215
+ terminalId: string;
216
+ }>;
217
+ writeTerminal(terminalId: string, data: string): void;
218
+ resizeTerminal(terminalId: string, cols: number, rows: number): void;
219
+ closeTerminal(terminalId: string): void;
183
220
  }
@@ -0,0 +1,48 @@
1
+ import type { ControlEventBus } from "./control-event-bus";
2
+ /**
3
+ * Secret-bearing env keys stripped before handing the shell its environment.
4
+ * Best-effort denylist only — a real shell still inherits the full process env;
5
+ * custom secrets (DATABASE_URL, *_TOKEN, ~/.ssh keys, etc.) not on this list
6
+ * pass through. Do not treat env scrubbing as a security guarantee.
7
+ */
8
+ export declare const SENSITIVE_ENV_KEYS: string[];
9
+ export interface PtyHandle {
10
+ onData(cb: (data: string) => void): void;
11
+ onExit(cb: (e: {
12
+ exitCode: number;
13
+ }) => void): void;
14
+ write(data: string): void;
15
+ resize(cols: number, rows: number): void;
16
+ kill(): void;
17
+ }
18
+ export type PtySpawn = (file: string, args: string[], opts: {
19
+ name: string;
20
+ cols: number;
21
+ rows: number;
22
+ cwd: string;
23
+ env: Record<string, string>;
24
+ }) => PtyHandle;
25
+ export interface TerminalCreateInput {
26
+ cwd: string;
27
+ cols: number;
28
+ rows: number;
29
+ }
30
+ export interface TerminalService {
31
+ create(input: TerminalCreateInput): {
32
+ terminalId: string;
33
+ };
34
+ write(terminalId: string, data: string): void;
35
+ resize(terminalId: string, cols: number, rows: number): void;
36
+ close(terminalId: string): void;
37
+ disposeAll(): void;
38
+ }
39
+ export interface TerminalServiceDeps {
40
+ events: ControlEventBus;
41
+ idleTimeoutSeconds: () => number;
42
+ spawn?: PtySpawn;
43
+ platform?: NodeJS.Platform;
44
+ /** Injectable timer primitives; defaults to global setTimeout/clearTimeout. */
45
+ setTimer?: (fn: () => void, ms: number) => unknown;
46
+ clearTimer?: (id: unknown) => void;
47
+ }
48
+ export declare function createTerminalService(deps: TerminalServiceDeps): TerminalService;
@@ -0,0 +1,4 @@
1
+ type ChmodFunction = (path: string, mode: number) => Promise<void>;
2
+ export declare function resolveNodePtyHelperPath(packageJsonPath: string, platform: NodeJS.Platform, arch: string): string | null;
3
+ export declare function ensureNodePtyHelperExecutable(helperPath: string | null, chmod?: ChmodFunction): Promise<void>;
4
+ export {};
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ganglion/xacpx",
3
- "version": "0.16.0",
3
+ "version": "0.17.0-beta.1",
4
4
  "description": "随时随地通过聊天频道(微信 / 飞书 / 元宝等)远程控制 `acpx` 上的 Claude Code、Codex 等 Agents。",
5
5
  "keywords": [
6
6
  "acpx",