@qwen-code/qwen-code 0.15.12-preview.2 → 0.16.0-preview.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (79) hide show
  1. package/bundled/qc-helper/docs/configuration/settings.md +30 -34
  2. package/bundled/qc-helper/docs/features/lsp.md +87 -10
  3. package/bundled/qc-helper/docs/qwen-serve.md +74 -24
  4. package/bundled/qc-helper/docs/reference/keyboard-shortcuts.md +11 -11
  5. package/bundled/stuck/SKILL.md +124 -0
  6. package/chunks/{agent-UQY6A6OS.js → agent-ZNQPH67I.js} +15 -15
  7. package/chunks/{anthropicContentGenerator-4QE6LTVV.js → anthropicContentGenerator-ICBDZ6R2.js} +4 -4
  8. package/chunks/{askUserQuestion-QFSCBTUO.js → askUserQuestion-WQILGUSQ.js} +2 -2
  9. package/chunks/{ca-VQSV6JHA.js → ca-S3XJMT6P.js} +26 -0
  10. package/chunks/{chunk-SQNQIOD5.js → chunk-2B7UBDY5.js} +2 -2
  11. package/chunks/chunk-3MBY4GKN.js +350 -0
  12. package/chunks/{chunk-FSYKVGER.js → chunk-7QXHXMC6.js} +23 -7
  13. package/chunks/{chunk-PCL3EJGY.js → chunk-C3LHPHN2.js} +3924 -3683
  14. package/chunks/{chunk-UXW7MYAW.js → chunk-CW44BRRA.js} +1 -1
  15. package/chunks/{chunk-G27O2LD2.js → chunk-D5NTAHYL.js} +1 -1
  16. package/chunks/{chunk-CBVB66WY.js → chunk-EDYSNFEM.js} +1 -1
  17. package/chunks/{chunk-OCC4MZRS.js → chunk-F23NCRJ2.js} +1 -1
  18. package/chunks/{chunk-FYMSCRHM.js → chunk-FZIUV27X.js} +1 -1
  19. package/chunks/{chunk-SOIEFHIK.js → chunk-G7YTSRES.js} +1 -100
  20. package/chunks/{chunk-MXBWOU2L.js → chunk-JHMX4QTD.js} +15 -15
  21. package/chunks/{chunk-TPGOGCWM.js → chunk-JYQUJ5DS.js} +1 -1
  22. package/chunks/{chunk-FKVKVE6N.js → chunk-KXZ4TJB4.js} +1 -1
  23. package/chunks/{chunk-2WFU3IUH.js → chunk-MNPZ2WO6.js} +4864 -2303
  24. package/chunks/{chunk-BXNCPI75.js → chunk-NAID3ZWF.js} +2 -2
  25. package/chunks/{chunk-JMZQICAL.js → chunk-PPHYLJSS.js} +1 -1
  26. package/chunks/{chunk-CM2IESUE.js → chunk-PR4T27R7.js} +1 -1
  27. package/chunks/{chunk-CAWKL3UC.js → chunk-VTPOO6GV.js} +1 -1
  28. package/chunks/{chunk-GJXIKCKL.js → chunk-XP27SJMH.js} +76 -5
  29. package/chunks/{chunk-B7ZL7HUA.js → chunk-XVHR7ATJ.js} +1 -1
  30. package/chunks/{contextCommand-MQRG6RMG.js → contextCommand-IGBCEXI4.js} +16 -16
  31. package/chunks/{cron-create-WUTD5ZTH.js → cron-create-AVI3Q267.js} +2 -2
  32. package/chunks/{cron-delete-N3UQYCRA.js → cron-delete-ZCEGDXXV.js} +2 -2
  33. package/chunks/{cron-list-Z6RJJ4YH.js → cron-list-VN653OK5.js} +2 -2
  34. package/chunks/{de-M2IPQRBS.js → de-MNR4SMAI.js} +26 -0
  35. package/chunks/{edit-3KCBTA25.js → edit-74Q4AFHQ.js} +20 -16
  36. package/chunks/{en-N5GMPCVT.js → en-FIUWJSZR.js} +28 -0
  37. package/chunks/{enter-worktree-VWS5QZTU.js → enter-worktree-H72HXC7D.js} +15 -15
  38. package/chunks/{exit-worktree-RVXFWAPD.js → exit-worktree-FGIQO3S3.js} +15 -15
  39. package/chunks/{exitPlanMode-UL5DILDG.js → exitPlanMode-NBR2PK2D.js} +15 -15
  40. package/chunks/{fr-BTHRYEXO.js → fr-OFJFHLCR.js} +26 -0
  41. package/chunks/{geminiContentGenerator-O2OPGHJG.js → geminiContentGenerator-33RP4WKD.js} +3 -3
  42. package/chunks/{glob-57BSREPN.js → glob-WEE3CJL6.js} +15 -15
  43. package/chunks/{grep-XO5JOC7T.js → grep-DZKSBFZK.js} +15 -15
  44. package/chunks/{ja-D63TAEBO.js → ja-V6OQ6VL7.js} +26 -0
  45. package/chunks/{keychain-token-storage-DMFP5IJM.js → keychain-token-storage-335UOLJ6.js} +2 -2
  46. package/chunks/{ls-SUILOZZB.js → ls-6F3VSP6S.js} +3 -3
  47. package/chunks/{lsp-6TQBWVMZ.js → lsp-67Y7DJN5.js} +2 -2
  48. package/chunks/{monitor-BECPGO3K.js → monitor-EDZWEZVS.js} +33 -24
  49. package/chunks/{openaiContentGenerator-KEZQHIRM.js → openaiContentGenerator-5NQG3W64.js} +10 -10
  50. package/chunks/{pt-XUV7FSKC.js → pt-ZLE6SA4A.js} +26 -0
  51. package/chunks/{qwenContentGenerator-RPMRXTNH.js → qwenContentGenerator-4DPUUS6R.js} +17 -17
  52. package/chunks/{qwenOAuth2-JSQ7EPR3.js → qwenOAuth2-JE7H47TE.js} +3 -3
  53. package/chunks/{read-file-LGHEIQNH.js → read-file-CQOF7BQ2.js} +7 -7
  54. package/chunks/{ripGrep-6SFSXZ2G.js → ripGrep-KR5LKGTI.js} +15 -15
  55. package/chunks/{ru-7KHWMN3A.js → ru-A4OHIUNN.js} +26 -0
  56. package/chunks/{send-message-Q2JRAC3J.js → send-message-GB4AQZNC.js} +2 -2
  57. package/chunks/{serve-27O2AFE3.js → serve-GAD2PEST.js} +1299 -408
  58. package/chunks/{shell-J7K5KYCH.js → shell-E2HMCBGR.js} +15 -15
  59. package/chunks/{skill-2R7P4ATS.js → skill-KDZH6UZ6.js} +9 -9
  60. package/chunks/{src-CGEDVW67.js → src-LY4RU5AI.js} +96 -24
  61. package/chunks/{syntheticOutput-S4DRGMQM.js → syntheticOutput-HFL3DE7R.js} +3 -3
  62. package/chunks/{task-stop-7THHVAQS.js → task-stop-ZQF26RXS.js} +2 -2
  63. package/chunks/{todoWrite-WKUGUTPX.js → todoWrite-U4SC643O.js} +3 -3
  64. package/chunks/{tool-search-XOH3ZWVS.js → tool-search-U4XQVLFU.js} +7 -7
  65. package/chunks/{web-fetch-OZE6ZQUF.js → web-fetch-BRWZ4WSE.js} +4 -4
  66. package/chunks/{write-file-74NQ27Q2.js → write-file-NBLRMNGB.js} +20 -16
  67. package/chunks/{zh-TW-O36Q4V7E.js → zh-TW-552S24LR.js} +28 -0
  68. package/chunks/{zh-VGHU6XBB.js → zh-V32QONGV.js} +28 -0
  69. package/cli.js +8027 -5104
  70. package/locales/ca.js +40 -0
  71. package/locales/de.js +40 -0
  72. package/locales/en.js +43 -0
  73. package/locales/fr.js +41 -0
  74. package/locales/ja.js +39 -0
  75. package/locales/pt.js +39 -0
  76. package/locales/ru.js +39 -0
  77. package/locales/zh-TW.js +41 -0
  78. package/locales/zh.js +41 -0
  79. package/package.json +2 -2
@@ -6,6 +6,17 @@ import {
6
6
  require_mime_db,
7
7
  require_type
8
8
  } from "./chunk-7RYW5LQV.js";
9
+ import {
10
+ DEFAULT_RING_SIZE,
11
+ EVENT_SCHEMA_VERSION,
12
+ EventBus,
13
+ SERVE_STATUS_EXT_METHODS,
14
+ STATUS_SCHEMA_VERSION,
15
+ SubscriberLimitExceededError,
16
+ createIdleWorkspaceMcpStatus,
17
+ createIdleWorkspaceProvidersStatus,
18
+ createIdleWorkspaceSkillsStatus
19
+ } from "./chunk-3MBY4GKN.js";
9
20
  import {
10
21
  writeStderrLine,
11
22
  writeStdoutLine
@@ -13076,6 +13087,20 @@ function bearerAuth(token) {
13076
13087
  };
13077
13088
  }
13078
13089
  __name(bearerAuth, "bearerAuth");
13090
+ function createMutationGate(deps) {
13091
+ const passthrough = /* @__PURE__ */ __name((_req, _res, next) => next(), "passthrough");
13092
+ if (deps.requireAuth || deps.tokenConfigured) {
13093
+ return () => passthrough;
13094
+ }
13095
+ const strictDenier = /* @__PURE__ */ __name((_req, res) => {
13096
+ res.status(401).json({
13097
+ error: "This route requires the daemon to be configured with a bearer token. Set QWEN_SERVER_TOKEN or pass --token to enable bearer auth.",
13098
+ code: "token_required"
13099
+ });
13100
+ }, "strictDenier");
13101
+ return (opts = {}) => opts.strict ? strictDenier : passthrough;
13102
+ }
13103
+ __name(createMutationGate, "createMutationGate");
13079
13104
 
13080
13105
  // packages/cli/src/serve/httpAcpBridge.ts
13081
13106
  init_esbuild_shims();
@@ -13084,276 +13109,45 @@ import { randomUUID } from "node:crypto";
13084
13109
  import { promises as fs, realpathSync } from "node:fs";
13085
13110
  import * as path from "node:path";
13086
13111
  import { Readable, Writable } from "node:stream";
13087
-
13088
- // packages/cli/src/serve/eventBus.ts
13089
- init_esbuild_shims();
13090
- var EVENT_SCHEMA_VERSION = 1;
13091
- var DEFAULT_MAX_QUEUED = 256;
13092
- var DEFAULT_RING_SIZE = 4e3;
13093
- var DEFAULT_MAX_SUBSCRIBERS = 64;
13094
- var SubscriberLimitExceededError = class extends Error {
13095
- static {
13096
- __name(this, "SubscriberLimitExceededError");
13097
- }
13098
- limit;
13099
- constructor(limit) {
13100
- super(`EventBus subscriber limit reached (${limit})`);
13101
- this.name = "SubscriberLimitExceededError";
13102
- this.limit = limit;
13103
- }
13104
- };
13105
- var EventBus = class {
13106
- constructor(ringSize = DEFAULT_RING_SIZE, maxSubscribers = DEFAULT_MAX_SUBSCRIBERS) {
13107
- this.ringSize = ringSize;
13108
- this.maxSubscribers = maxSubscribers;
13109
- }
13112
+ var SessionNotFoundError = class extends Error {
13110
13113
  static {
13111
- __name(this, "EventBus");
13112
- }
13113
- nextId = 1;
13114
- ring = [];
13115
- subs = /* @__PURE__ */ new Set();
13116
- closed = false;
13117
- /** Most recent id ever assigned by `publish`. 0 if no events published. */
13118
- get lastEventId() {
13119
- return this.nextId - 1;
13120
- }
13121
- /** Snapshot of the live subscriber count. */
13122
- get subscriberCount() {
13123
- return this.subs.size;
13124
- }
13125
- /**
13126
- * Publish an event to the bus. Returns the constructed `BridgeEvent`
13127
- * (with `id` + `v` assigned) on success, or `undefined` when the
13128
- * bus is closed.
13129
- *
13130
- * **Never throws** (BX9_p contract). Closing the bus mid-publish
13131
- * is the only abnormal path and is handled as a return-undefined
13132
- * no-op; subscriber-enqueue failures are caught internally and
13133
- * translated to per-subscriber eviction. Call sites can rely on
13134
- * this — the historical `try { publish(...) } catch {}` blocks in
13135
- * `httpAcpBridge.ts` are defense-in-depth, not load-bearing, and
13136
- * may be removed in a future cleanup pass without changing
13137
- * behavior. Don't add new try/catch wrappers around `publish()`.
13138
- */
13139
- publish(input) {
13140
- if (this.closed) return void 0;
13141
- const event = {
13142
- id: this.nextId++,
13143
- v: EVENT_SCHEMA_VERSION,
13144
- ...input
13145
- };
13146
- this.ring.push(event);
13147
- if (this.ring.length > this.ringSize) this.ring.shift();
13148
- for (const sub of Array.from(this.subs)) {
13149
- if (sub.evicted) continue;
13150
- if (!sub.queue.push(event)) {
13151
- sub.evicted = true;
13152
- const evictionFrame = {
13153
- v: EVENT_SCHEMA_VERSION,
13154
- type: "client_evicted",
13155
- data: { reason: "queue_overflow", droppedAfter: event.id }
13156
- };
13157
- sub.queue.forcePush(evictionFrame);
13158
- sub.queue.close();
13159
- sub.dispose();
13160
- }
13161
- }
13162
- return event;
13163
- }
13164
- /**
13165
- * Note: registration is synchronous — by the time `subscribe()` returns,
13166
- * the subscriber is already attached and will receive any subsequent
13167
- * `publish()` even if the consumer hasn't started iterating yet. (A
13168
- * generator-style implementation would defer registration to the first
13169
- * `next()` call, which races with publishes that happen before the
13170
- * consumer's first await.)
13171
- *
13172
- * The returned iterator is NOT safe to drive from concurrent callers —
13173
- * two simultaneous `.next()` calls would race for the same event from
13174
- * the underlying queue. Daemon usage is sequential (`for await ... of`
13175
- * inside the SSE route), so this is safe in production. Callers that
13176
- * fan an iterator out to multiple consumers must serialize themselves.
13177
- */
13178
- subscribe(opts = {}) {
13179
- if (this.closed) {
13180
- return emptyAsyncIterable();
13181
- }
13182
- if (this.subs.size >= this.maxSubscribers) {
13183
- throw new SubscriberLimitExceededError(this.maxSubscribers);
13184
- }
13185
- const queue = new BoundedAsyncQueue(
13186
- opts.maxQueued ?? DEFAULT_MAX_QUEUED
13187
- );
13188
- const sub = { queue, evicted: false, dispose: /* @__PURE__ */ __name(() => {
13189
- }, "dispose") };
13190
- this.subs.add(sub);
13191
- if (opts.lastEventId !== void 0) {
13192
- for (const e of this.ring) {
13193
- if (e.id !== void 0 && e.id > opts.lastEventId) {
13194
- queue.forcePush(e);
13195
- }
13196
- }
13197
- }
13198
- let disposed = false;
13199
- const dispose = /* @__PURE__ */ __name(() => {
13200
- if (disposed) return;
13201
- disposed = true;
13202
- this.subs.delete(sub);
13203
- opts.signal?.removeEventListener("abort", onAbort);
13204
- }, "dispose");
13205
- sub.dispose = dispose;
13206
- const onAbort = /* @__PURE__ */ __name(() => {
13207
- queue.close({ drain: false });
13208
- dispose();
13209
- }, "onAbort");
13210
- if (opts.signal) {
13211
- if (opts.signal.aborted) {
13212
- onAbort();
13213
- } else {
13214
- opts.signal.addEventListener("abort", onAbort, { once: true });
13215
- }
13216
- }
13217
- return {
13218
- [Symbol.asyncIterator]: () => ({
13219
- async next() {
13220
- const r = await queue.next();
13221
- if (r.done) dispose();
13222
- return r;
13223
- },
13224
- async return() {
13225
- queue.close();
13226
- dispose();
13227
- return { value: void 0, done: true };
13228
- }
13229
- })
13230
- };
13114
+ __name(this, "SessionNotFoundError");
13231
13115
  }
13232
- /** Close all live subscribers and prevent further `publish`/`subscribe`. */
13233
- close() {
13234
- if (this.closed) return;
13235
- this.closed = true;
13236
- for (const sub of this.subs) sub.queue.close();
13237
- this.subs.clear();
13116
+ sessionId;
13117
+ constructor(sessionId, extra) {
13118
+ super(`No session with id "${sessionId}"` + (extra ? `. ${extra}` : ""));
13119
+ this.name = "SessionNotFoundError";
13120
+ this.sessionId = sessionId;
13238
13121
  }
13239
13122
  };
13240
- function emptyAsyncIterable() {
13241
- return {
13242
- [Symbol.asyncIterator]: () => ({
13243
- async next() {
13244
- return { value: void 0, done: true };
13245
- }
13246
- })
13247
- };
13248
- }
13249
- __name(emptyAsyncIterable, "emptyAsyncIterable");
13250
- var BoundedAsyncQueue = class {
13251
- constructor(maxSize) {
13252
- this.maxSize = maxSize;
13253
- }
13123
+ var RestoreInProgressError = class extends Error {
13254
13124
  static {
13255
- __name(this, "BoundedAsyncQueue");
13256
- }
13257
- buf = [];
13258
- resolvers = [];
13259
- closed = false;
13260
- /**
13261
- * Number of force-pushed items still in `buf`. The cap check in
13262
- * `push()` only applies to LIVE items; this counter tells us how
13263
- * many slots in `buf` are replay-injected and shouldn't count.
13264
- *
13265
- * Position invariant: under the bus's two callers,
13266
- * 1. subscribe-time replay (`Last-Event-ID` resume) — forcePush
13267
- * fires BEFORE any live `push()`, so replay items are at the
13268
- * front of `buf`;
13269
- * 2. eviction terminal frame — forcePush fires AFTER `push()`
13270
- * rejection, then `close()` is called immediately, so the
13271
- * eviction frame is at the BACK of `buf`.
13272
- *
13273
- * `next()` decrements `forcedInBuf` whenever the counter is > 0 on
13274
- * shift, which is correct for case (1). For case (2) it slightly
13275
- * misaccounts (decrements on the first live shift), but that's
13276
- * harmless: the queue is closed so no `push()` runs the cap check
13277
- * again. The counter only matters for live cap enforcement.
13278
- */
13279
- forcedInBuf = 0;
13280
- /** Returns true if accepted, false if dropped due to overflow. */
13281
- push(value) {
13282
- if (this.closed) return false;
13283
- const r = this.resolvers.shift();
13284
- if (r) {
13285
- r({ value, done: false });
13286
- return true;
13287
- }
13288
- if (this.buf.length - this.forcedInBuf >= this.maxSize) return false;
13289
- this.buf.push(value);
13290
- return true;
13125
+ __name(this, "RestoreInProgressError");
13291
13126
  }
13292
- /** Bypasses the size cap. Used for replay frames and terminal eviction. */
13293
- forcePush(value) {
13294
- if (this.closed) return;
13295
- const r = this.resolvers.shift();
13296
- if (r) {
13297
- r({ value, done: false });
13298
- return;
13299
- }
13300
- this.buf.push(value);
13301
- this.forcedInBuf += 1;
13302
- }
13303
- /**
13304
- * Mark the queue closed. By default `next()` continues to drain
13305
- * any items already in `buf` before returning `done: true` —
13306
- * that's what the eviction path relies on (the synthetic
13307
- * `client_evicted` frame is force-pushed THEN close is called,
13308
- * and we want the consumer to see the terminal frame before the
13309
- * iterator unwinds).
13310
- *
13311
- * Pass `{ drain: false }` to drop buffered items immediately
13312
- * (the AbortSignal-driven unsubscribe path uses this — the
13313
- * subscribe docstring says abort should close the iterator
13314
- * promptly, but draining hundreds of queued events first
13315
- * contradicts that and adds post-abort work to the SSE route).
13316
- */
13317
- close(opts = {}) {
13318
- if (this.closed) return;
13319
- this.closed = true;
13320
- if (opts.drain === false) {
13321
- this.buf.length = 0;
13322
- this.forcedInBuf = 0;
13323
- }
13324
- while (this.resolvers.length > 0) {
13325
- this.resolvers.shift()({
13326
- value: void 0,
13327
- done: true
13328
- });
13329
- }
13330
- }
13331
- next() {
13332
- if (this.buf.length > 0) {
13333
- const value = this.buf.shift();
13334
- if (this.forcedInBuf > 0) this.forcedInBuf -= 1;
13335
- return Promise.resolve({ value, done: false });
13336
- }
13337
- if (this.closed) {
13338
- return Promise.resolve({
13339
- value: void 0,
13340
- done: true
13341
- });
13342
- }
13343
- return new Promise((resolve2) => this.resolvers.push(resolve2));
13127
+ sessionId;
13128
+ activeAction;
13129
+ requestedAction;
13130
+ constructor(sessionId, activeAction, requestedAction) {
13131
+ super(
13132
+ `Session "${sessionId}" is already being restored via session/${activeAction}; retry session/${requestedAction} after it completes`
13133
+ );
13134
+ this.name = "RestoreInProgressError";
13135
+ this.sessionId = sessionId;
13136
+ this.activeAction = activeAction;
13137
+ this.requestedAction = requestedAction;
13344
13138
  }
13345
13139
  };
13346
-
13347
- // packages/cli/src/serve/httpAcpBridge.ts
13348
- var SessionNotFoundError = class extends Error {
13140
+ var InvalidSessionScopeError = class extends Error {
13349
13141
  static {
13350
- __name(this, "SessionNotFoundError");
13142
+ __name(this, "InvalidSessionScopeError");
13351
13143
  }
13352
- sessionId;
13353
- constructor(sessionId, extra) {
13354
- super(`No session with id "${sessionId}"` + (extra ? `. ${extra}` : ""));
13355
- this.name = "SessionNotFoundError";
13356
- this.sessionId = sessionId;
13144
+ sessionScope;
13145
+ constructor(sessionScope) {
13146
+ super(
13147
+ `Invalid sessionScope: ${JSON.stringify(sessionScope)}. Expected 'single' or 'thread'.`
13148
+ );
13149
+ this.name = "InvalidSessionScopeError";
13150
+ this.sessionScope = sessionScope;
13357
13151
  }
13358
13152
  };
13359
13153
  var SessionLimitExceededError = class extends Error {
@@ -13383,7 +13177,32 @@ var WorkspaceMismatchError = class extends Error {
13383
13177
  this.requested = safeRequested;
13384
13178
  }
13385
13179
  };
13180
+ var InvalidClientIdError = class extends Error {
13181
+ static {
13182
+ __name(this, "InvalidClientIdError");
13183
+ }
13184
+ sessionId;
13185
+ clientId;
13186
+ constructor(sessionId, clientId) {
13187
+ super(`Client id "${clientId}" is not registered for session ${sessionId}`);
13188
+ this.name = "InvalidClientIdError";
13189
+ this.sessionId = sessionId;
13190
+ this.clientId = clientId;
13191
+ }
13192
+ };
13386
13193
  var MAX_WORKSPACE_PATH_LENGTH = 4096;
13194
+ var MAX_RESOLVED_PERMISSION_RECORDS = 512;
13195
+ function isServeDebugLoggingEnabled() {
13196
+ const value = process.env["QWEN_SERVE_DEBUG"];
13197
+ if (!value) return false;
13198
+ return !["0", "false", "off", "no"].includes(value.trim().toLowerCase());
13199
+ }
13200
+ __name(isServeDebugLoggingEnabled, "isServeDebugLoggingEnabled");
13201
+ function writeServeDebugLine(message) {
13202
+ if (!isServeDebugLoggingEnabled()) return;
13203
+ writeStderrLine(`qwen serve debug: ${message}`);
13204
+ }
13205
+ __name(writeServeDebugLine, "writeServeDebugLine");
13387
13206
  var InvalidPermissionOptionError = class extends Error {
13388
13207
  static {
13389
13208
  __name(this, "InvalidPermissionOptionError");
@@ -13399,9 +13218,32 @@ var InvalidPermissionOptionError = class extends Error {
13399
13218
  this.optionId = optionId;
13400
13219
  }
13401
13220
  };
13221
+ var MAX_DISPLAY_NAME_LENGTH = 256;
13222
+ function hasControlCharacter(value) {
13223
+ for (let i = 0; i < value.length; i += 1) {
13224
+ const code = value.charCodeAt(i);
13225
+ if (code <= 31 || code === 127) {
13226
+ return true;
13227
+ }
13228
+ }
13229
+ return false;
13230
+ }
13231
+ __name(hasControlCharacter, "hasControlCharacter");
13232
+ var InvalidSessionMetadataError = class extends Error {
13233
+ static {
13234
+ __name(this, "InvalidSessionMetadataError");
13235
+ }
13236
+ field;
13237
+ constructor(field, reason) {
13238
+ super(`Invalid session metadata: ${field} ${reason}`);
13239
+ this.name = "InvalidSessionMetadataError";
13240
+ this.field = field;
13241
+ }
13242
+ };
13402
13243
  var BridgeClient = class {
13403
- constructor(resolveEntry, registerPending, rollbackPending, permissionTimeoutMs, maxPendingPerSession) {
13244
+ constructor(resolveEntry, resolvePendingRestoreEvents, registerPending, rollbackPending, permissionTimeoutMs, maxPendingPerSession) {
13404
13245
  this.resolveEntry = resolveEntry;
13246
+ this.resolvePendingRestoreEvents = resolvePendingRestoreEvents;
13405
13247
  this.registerPending = registerPending;
13406
13248
  this.rollbackPending = rollbackPending;
13407
13249
  this.permissionTimeoutMs = permissionTimeoutMs;
@@ -13459,7 +13301,8 @@ var BridgeClient = class {
13459
13301
  sessionId: entry.sessionId,
13460
13302
  toolCall: params.toolCall,
13461
13303
  options: params.options
13462
- }
13304
+ },
13305
+ ...entry.activePromptOriginatorClientId ? { originatorClientId: entry.activePromptOriginatorClientId } : {}
13463
13306
  });
13464
13307
  if (!published) {
13465
13308
  this.rollbackPending(requestId);
@@ -13481,8 +13324,13 @@ var BridgeClient = class {
13481
13324
  }
13482
13325
  async sessionUpdate(params) {
13483
13326
  const entry = this.resolveEntry(params.sessionId);
13484
- if (!entry) return;
13485
- entry.events.publish({ type: "session_update", data: params });
13327
+ const events = entry?.events ?? this.resolvePendingRestoreEvents(params.sessionId);
13328
+ if (!events) return;
13329
+ events.publish({
13330
+ type: "session_update",
13331
+ data: params,
13332
+ ...entry?.activePromptOriginatorClientId ? { originatorClientId: entry.activePromptOriginatorClientId } : {}
13333
+ });
13486
13334
  }
13487
13335
  async writeTextFile(params) {
13488
13336
  let realTarget = params.path;
@@ -13558,10 +13406,11 @@ var BridgeClient = class {
13558
13406
  };
13559
13407
  var DEFAULT_INIT_TIMEOUT_MS = 1e4;
13560
13408
  var DEFAULT_MAX_SESSIONS = 20;
13409
+ var MAX_EVENT_RING_SIZE = 1e6;
13561
13410
  var DEFAULT_PERMISSION_TIMEOUT_MS = 5 * 60 * 1e3;
13562
13411
  var DEFAULT_MAX_PENDING_PER_SESSION = 64;
13563
13412
  function createHttpAcpBridge(opts) {
13564
- const sessionScope = opts.sessionScope ?? "single";
13413
+ const defaultSessionScope = opts.sessionScope ?? "single";
13565
13414
  let maxSessions;
13566
13415
  if (opts.maxSessions === void 0) {
13567
13416
  maxSessions = DEFAULT_MAX_SESSIONS;
@@ -13578,9 +13427,15 @@ function createHttpAcpBridge(opts) {
13578
13427
  } else {
13579
13428
  maxSessions = opts.maxSessions;
13580
13429
  }
13581
- if (sessionScope !== "single" && sessionScope !== "thread") {
13430
+ if (defaultSessionScope !== "single" && defaultSessionScope !== "thread") {
13582
13431
  throw new TypeError(
13583
- `Invalid sessionScope: ${JSON.stringify(sessionScope)}. Expected 'single' or 'thread'.`
13432
+ `Invalid sessionScope: ${JSON.stringify(defaultSessionScope)}. Expected 'single' or 'thread'.`
13433
+ );
13434
+ }
13435
+ const eventRingSize = opts.eventRingSize ?? DEFAULT_RING_SIZE;
13436
+ if (!Number.isInteger(eventRingSize) || eventRingSize < 1 || eventRingSize > MAX_EVENT_RING_SIZE) {
13437
+ throw new TypeError(
13438
+ `Invalid eventRingSize: ${opts.eventRingSize}. Must be a positive integer in [1, ${MAX_EVENT_RING_SIZE}].`
13584
13439
  );
13585
13440
  }
13586
13441
  const channelFactory = opts.channelFactory ?? defaultSpawnChannelFactory;
@@ -13606,8 +13461,49 @@ function createHttpAcpBridge(opts) {
13606
13461
  let inFlightChannelSpawn;
13607
13462
  const byId = /* @__PURE__ */ new Map();
13608
13463
  const pendingPermissions = /* @__PURE__ */ new Map();
13464
+ const resolvedPermissions = /* @__PURE__ */ new Map();
13465
+ const resolvedPermissionOrder = [];
13609
13466
  let shuttingDown = false;
13610
13467
  const inFlightSpawns = /* @__PURE__ */ new Map();
13468
+ const inFlightRestores = /* @__PURE__ */ new Map();
13469
+ const pendingRestoreEvents = /* @__PURE__ */ new Map();
13470
+ const createClientId = /* @__PURE__ */ __name(() => `client_${randomUUID()}`, "createClientId");
13471
+ const registerClient = /* @__PURE__ */ __name((entry, requestedClientId) => {
13472
+ if (requestedClientId && entry.clientIds.has(requestedClientId)) {
13473
+ entry.clientIds.set(
13474
+ requestedClientId,
13475
+ (entry.clientIds.get(requestedClientId) ?? 0) + 1
13476
+ );
13477
+ return requestedClientId;
13478
+ }
13479
+ const clientId = createClientId();
13480
+ entry.clientIds.set(clientId, 1);
13481
+ return clientId;
13482
+ }, "registerClient");
13483
+ const unregisterClient = /* @__PURE__ */ __name((entry, clientId) => {
13484
+ if (clientId === void 0) return;
13485
+ const count = entry.clientIds.get(clientId);
13486
+ if (count === void 0) return;
13487
+ if (count <= 1) {
13488
+ entry.clientIds.delete(clientId);
13489
+ entry.clientLastSeenAt.delete(clientId);
13490
+ } else {
13491
+ entry.clientIds.set(clientId, count - 1);
13492
+ }
13493
+ }, "unregisterClient");
13494
+ const resolveTrustedClientId = /* @__PURE__ */ __name((entry, clientId) => {
13495
+ if (clientId === void 0) return void 0;
13496
+ if (!entry.clientIds.has(clientId)) {
13497
+ throw new InvalidClientIdError(entry.sessionId, clientId);
13498
+ }
13499
+ return clientId;
13500
+ }, "resolveTrustedClientId");
13501
+ const resolveAnyTrustedClientId = /* @__PURE__ */ __name((clientId) => {
13502
+ for (const entry of byId.values()) {
13503
+ if (entry.clientIds.has(clientId)) return clientId;
13504
+ }
13505
+ throw new InvalidClientIdError("unknown", clientId);
13506
+ }, "resolveAnyTrustedClientId");
13611
13507
  const registerPending = /* @__PURE__ */ __name((p) => {
13612
13508
  const entry = byId.get(p.sessionId);
13613
13509
  if (!entry) {
@@ -13617,7 +13513,38 @@ function createHttpAcpBridge(opts) {
13617
13513
  pendingPermissions.set(p.requestId, p);
13618
13514
  entry.pendingPermissionIds.add(p.requestId);
13619
13515
  }, "registerPending");
13620
- const resolvePending = /* @__PURE__ */ __name((requestId, response) => {
13516
+ const rememberResolvedPermission = /* @__PURE__ */ __name((record) => {
13517
+ if (!resolvedPermissions.has(record.requestId)) {
13518
+ resolvedPermissionOrder.push(record.requestId);
13519
+ }
13520
+ resolvedPermissions.set(record.requestId, record);
13521
+ while (resolvedPermissionOrder.length > MAX_RESOLVED_PERMISSION_RECORDS) {
13522
+ const oldest = resolvedPermissionOrder.shift();
13523
+ if (oldest !== void 0) resolvedPermissions.delete(oldest);
13524
+ }
13525
+ }, "rememberResolvedPermission");
13526
+ const publishPermissionAlreadyResolved = /* @__PURE__ */ __name((record) => {
13527
+ const entry = byId.get(record.sessionId);
13528
+ if (!entry) return;
13529
+ try {
13530
+ writeServeDebugLine(
13531
+ `permission ${JSON.stringify(record.requestId)} for session ${JSON.stringify(record.sessionId)} was already resolved; publishing duplicate-vote notification.`
13532
+ );
13533
+ entry.events.publish({
13534
+ type: "permission_already_resolved",
13535
+ data: {
13536
+ requestId: record.requestId,
13537
+ sessionId: record.sessionId,
13538
+ outcome: record.outcome
13539
+ }
13540
+ });
13541
+ } catch {
13542
+ writeServeDebugLine(
13543
+ `skipped duplicate-vote notification for permission ${JSON.stringify(record.requestId)} during shutdown.`
13544
+ );
13545
+ }
13546
+ }, "publishPermissionAlreadyResolved");
13547
+ const resolvePending = /* @__PURE__ */ __name((requestId, response, originatorClientId) => {
13621
13548
  const pending = pendingPermissions.get(requestId);
13622
13549
  if (!pending) return false;
13623
13550
  pendingPermissions.delete(requestId);
@@ -13627,11 +13554,17 @@ function createHttpAcpBridge(opts) {
13627
13554
  try {
13628
13555
  entry.events.publish({
13629
13556
  type: "permission_resolved",
13630
- data: { requestId, outcome: response.outcome }
13557
+ data: { requestId, outcome: response.outcome },
13558
+ ...originatorClientId ? { originatorClientId } : {}
13631
13559
  });
13632
13560
  } catch {
13633
13561
  }
13634
13562
  }
13563
+ rememberResolvedPermission({
13564
+ requestId,
13565
+ sessionId: pending.sessionId,
13566
+ outcome: response.outcome
13567
+ });
13635
13568
  pending.resolve(response);
13636
13569
  return true;
13637
13570
  }, "resolvePending");
@@ -13656,6 +13589,7 @@ function createHttpAcpBridge(opts) {
13656
13589
  }
13657
13590
  return void 0;
13658
13591
  },
13592
+ (sessionId) => sessionId ? pendingRestoreEvents.get(sessionId) : void 0,
13659
13593
  registerPending,
13660
13594
  (rid) => (
13661
13595
  // Roll back a register-then-publish-failed pending so the agent
@@ -13671,6 +13605,7 @@ function createHttpAcpBridge(opts) {
13671
13605
  connection,
13672
13606
  client,
13673
13607
  sessionIds: /* @__PURE__ */ new Set(),
13608
+ pendingRestoreIds: /* @__PURE__ */ new Set(),
13674
13609
  isDying: false
13675
13610
  };
13676
13611
  aliveChannels.add(info);
@@ -13746,7 +13681,7 @@ function createHttpAcpBridge(opts) {
13746
13681
  }
13747
13682
  }
13748
13683
  __name(ensureChannel, "ensureChannel");
13749
- async function doSpawn(modelServiceId) {
13684
+ async function doSpawn(modelServiceId, effectiveScope, requestedClientId) {
13750
13685
  const ci = await ensureChannel();
13751
13686
  let newSessionResp;
13752
13687
  try {
@@ -13769,26 +13704,21 @@ function createHttpAcpBridge(opts) {
13769
13704
  if (shuttingDown) {
13770
13705
  throw new Error("HttpAcpBridge is shutting down");
13771
13706
  }
13772
- const entry = {
13773
- sessionId: newSessionResp.sessionId,
13774
- workspaceCwd: boundWorkspace,
13775
- channel: ci.channel,
13776
- connection: ci.connection,
13777
- events: new EventBus(),
13778
- promptQueue: Promise.resolve(),
13779
- modelChangeQueue: Promise.resolve(),
13780
- pendingPermissionIds: /* @__PURE__ */ new Set(),
13781
- attachCount: 0,
13782
- spawnOwnerWantedKill: false
13783
- };
13784
- ci.sessionIds.add(entry.sessionId);
13785
- byId.set(entry.sessionId, entry);
13786
- if (!defaultEntry) defaultEntry = entry;
13707
+ const entry = createSessionEntry(
13708
+ ci,
13709
+ newSessionResp.sessionId,
13710
+ boundWorkspace
13711
+ );
13712
+ const clientId = registerClient(entry, requestedClientId);
13713
+ if (effectiveScope === "single" && !defaultEntry) defaultEntry = entry;
13787
13714
  if (modelServiceId) {
13788
- await applyModelServiceId(entry, modelServiceId, initTimeoutMs).catch(
13789
- () => {
13790
- }
13791
- );
13715
+ await applyModelServiceId(
13716
+ entry,
13717
+ modelServiceId,
13718
+ initTimeoutMs,
13719
+ clientId
13720
+ ).catch(() => {
13721
+ });
13792
13722
  }
13793
13723
  if (!byId.has(entry.sessionId)) {
13794
13724
  throw new Error(
@@ -13798,11 +13728,13 @@ function createHttpAcpBridge(opts) {
13798
13728
  return {
13799
13729
  sessionId: entry.sessionId,
13800
13730
  workspaceCwd: entry.workspaceCwd,
13801
- attached: false
13731
+ attached: false,
13732
+ clientId,
13733
+ createdAt: entry.createdAt
13802
13734
  };
13803
13735
  }
13804
13736
  __name(doSpawn, "doSpawn");
13805
- async function applyModelServiceId(entry, modelId, timeoutMs) {
13737
+ async function applyModelServiceId(entry, modelId, timeoutMs, originatorClientId) {
13806
13738
  const conn = entry.connection;
13807
13739
  const transportClosed = getTransportClosedReject(entry);
13808
13740
  const work = entry.modelChangeQueue.then(async () => {
@@ -13820,7 +13752,8 @@ function createHttpAcpBridge(opts) {
13820
13752
  ]);
13821
13753
  entry.events.publish({
13822
13754
  type: "model_switched",
13823
- data: { sessionId: entry.sessionId, modelId }
13755
+ data: { sessionId: entry.sessionId, modelId },
13756
+ ...originatorClientId ? { originatorClientId } : {}
13824
13757
  });
13825
13758
  } catch (err) {
13826
13759
  entry.events.publish({
@@ -13829,7 +13762,8 @@ function createHttpAcpBridge(opts) {
13829
13762
  sessionId: entry.sessionId,
13830
13763
  requestedModelId: modelId,
13831
13764
  error: err instanceof Error ? err.message : String(err)
13832
- }
13765
+ },
13766
+ ...originatorClientId ? { originatorClientId } : {}
13833
13767
  });
13834
13768
  throw err;
13835
13769
  }
@@ -13859,6 +13793,266 @@ function createHttpAcpBridge(opts) {
13859
13793
  }
13860
13794
  return entry.transportClosedReject;
13861
13795
  }, "getTransportClosedReject");
13796
+ const resolveWorkspaceKey = /* @__PURE__ */ __name((workspaceCwd) => {
13797
+ if (!path.isAbsolute(workspaceCwd)) {
13798
+ throw new Error(
13799
+ `workspaceCwd must be an absolute path; got "${workspaceCwd}"`
13800
+ );
13801
+ }
13802
+ const workspaceKey = workspaceCwd === boundWorkspace ? boundWorkspace : canonicalizeWorkspace(workspaceCwd);
13803
+ if (workspaceKey !== boundWorkspace) {
13804
+ throw new WorkspaceMismatchError(boundWorkspace, workspaceKey);
13805
+ }
13806
+ return workspaceKey;
13807
+ }, "resolveWorkspaceKey");
13808
+ const liveChannelInfo = /* @__PURE__ */ __name(() => {
13809
+ if (!channelInfo || channelInfo.isDying) return void 0;
13810
+ return channelInfo;
13811
+ }, "liveChannelInfo");
13812
+ const channelInfoForEntry = /* @__PURE__ */ __name((entry) => {
13813
+ if (channelInfo?.channel === entry.channel) return channelInfo;
13814
+ for (const info of aliveChannels) {
13815
+ if (info.channel === entry.channel) return info;
13816
+ }
13817
+ return void 0;
13818
+ }, "channelInfoForEntry");
13819
+ const getChannelClosedReject = /* @__PURE__ */ __name((info) => {
13820
+ if (!info.statusClosedReject) {
13821
+ info.statusClosedReject = info.channel.exited.then(() => {
13822
+ throw new Error("agent channel closed mid-request (workspace status)");
13823
+ });
13824
+ }
13825
+ return info.statusClosedReject;
13826
+ }, "getChannelClosedReject");
13827
+ const requestWorkspaceStatus = /* @__PURE__ */ __name(async (method, idle) => {
13828
+ const info = liveChannelInfo();
13829
+ if (!info) return idle();
13830
+ const response = await withTimeout(
13831
+ Promise.race([
13832
+ info.connection.extMethod(method, { cwd: boundWorkspace }),
13833
+ getChannelClosedReject(info)
13834
+ ]),
13835
+ initTimeoutMs,
13836
+ method
13837
+ );
13838
+ return response;
13839
+ }, "requestWorkspaceStatus");
13840
+ const requestSessionStatus = /* @__PURE__ */ __name(async (sessionId, method) => {
13841
+ const entry = byId.get(sessionId);
13842
+ if (!entry) throw new SessionNotFoundError(sessionId);
13843
+ const info = channelInfoForEntry(entry);
13844
+ if (!info || info.isDying) throw new SessionNotFoundError(sessionId);
13845
+ const response = await Promise.race([
13846
+ withTimeout(
13847
+ entry.connection.extMethod(method, { sessionId }),
13848
+ initTimeoutMs,
13849
+ method
13850
+ ),
13851
+ getTransportClosedReject(entry)
13852
+ ]);
13853
+ return response;
13854
+ }, "requestSessionStatus");
13855
+ const createSessionEntry = /* @__PURE__ */ __name((ci, sessionId, workspaceCwd, events = new EventBus(eventRingSize)) => {
13856
+ const entry = {
13857
+ sessionId,
13858
+ workspaceCwd,
13859
+ createdAt: (/* @__PURE__ */ new Date()).toISOString(),
13860
+ channel: ci.channel,
13861
+ connection: ci.connection,
13862
+ events,
13863
+ promptQueue: Promise.resolve(),
13864
+ modelChangeQueue: Promise.resolve(),
13865
+ pendingPermissionIds: /* @__PURE__ */ new Set(),
13866
+ clientIds: /* @__PURE__ */ new Map(),
13867
+ clientLastSeenAt: /* @__PURE__ */ new Map(),
13868
+ attachCount: 0,
13869
+ spawnOwnerWantedKill: false
13870
+ };
13871
+ ci.sessionIds.add(entry.sessionId);
13872
+ byId.set(entry.sessionId, entry);
13873
+ return entry;
13874
+ }, "createSessionEntry");
13875
+ const isAcpSessionResourceNotFound = /* @__PURE__ */ __name((err, sessionId) => {
13876
+ if (!err || typeof err !== "object") return false;
13877
+ const maybe = err;
13878
+ if (maybe.code !== -32002) return false;
13879
+ const expectedUri = `session:${sessionId}`;
13880
+ if (maybe.data && typeof maybe.data === "object" && maybe.data.uri === expectedUri) {
13881
+ return true;
13882
+ }
13883
+ return typeof maybe.message === "string" && maybe.message === `Resource not found: ${expectedUri}`;
13884
+ }, "isAcpSessionResourceNotFound");
13885
+ async function restoreSession(action, req) {
13886
+ if (shuttingDown) {
13887
+ throw new Error("HttpAcpBridge is shutting down");
13888
+ }
13889
+ const workspaceKey = resolveWorkspaceKey(req.workspaceCwd);
13890
+ const existing = byId.get(req.sessionId);
13891
+ if (existing) {
13892
+ existing.attachCount++;
13893
+ const clientId = registerClient(existing, req.clientId);
13894
+ return {
13895
+ sessionId: existing.sessionId,
13896
+ workspaceCwd: existing.workspaceCwd,
13897
+ attached: true,
13898
+ clientId,
13899
+ createdAt: existing.createdAt,
13900
+ // Late attachers get the same ACP state the original restore
13901
+ // caller saw; spawn-only sessions don't carry a state payload.
13902
+ state: existing.restoreState ?? {}
13903
+ };
13904
+ }
13905
+ const inFlight = inFlightRestores.get(req.sessionId);
13906
+ if (inFlight) {
13907
+ if (action !== inFlight.action) {
13908
+ throw new RestoreInProgressError(
13909
+ req.sessionId,
13910
+ inFlight.action,
13911
+ action
13912
+ );
13913
+ }
13914
+ inFlight.coalesceState.count++;
13915
+ let restored;
13916
+ try {
13917
+ restored = await inFlight.promise;
13918
+ } catch (err) {
13919
+ inFlight.coalesceState.count--;
13920
+ throw err;
13921
+ }
13922
+ const entry = byId.get(restored.sessionId);
13923
+ if (!entry) {
13924
+ inFlight.coalesceState.count--;
13925
+ throw new SessionNotFoundError(
13926
+ restored.sessionId,
13927
+ "the agent child likely crashed during session restore \u2014 retry to restore the session"
13928
+ );
13929
+ }
13930
+ return {
13931
+ ...restored,
13932
+ attached: true,
13933
+ clientId: registerClient(entry, req.clientId),
13934
+ createdAt: entry.createdAt
13935
+ };
13936
+ }
13937
+ if (byId.size + inFlightSpawns.size + inFlightRestores.size >= maxSessions) {
13938
+ throw new SessionLimitExceededError(maxSessions);
13939
+ }
13940
+ const restoreEvents = new EventBus(eventRingSize);
13941
+ let registeredEntry;
13942
+ let ci;
13943
+ const coalesceState = { count: 0 };
13944
+ const promise = (async () => {
13945
+ pendingRestoreEvents.set(req.sessionId, restoreEvents);
13946
+ ci = await ensureChannel();
13947
+ ci.pendingRestoreIds.add(req.sessionId);
13948
+ const transportClosed = ci.channel.exited.then(() => {
13949
+ throw new Error(`agent channel closed during session/${action}`);
13950
+ });
13951
+ transportClosed.catch(() => {
13952
+ });
13953
+ let state;
13954
+ try {
13955
+ if (action === "load") {
13956
+ state = await Promise.race([
13957
+ withTimeout(
13958
+ ci.connection.loadSession({
13959
+ sessionId: req.sessionId,
13960
+ cwd: workspaceKey,
13961
+ // Restore path drops per-request `mcpServers` (matches
13962
+ // `doSpawn`); daemon-wide MCP comes from settings on
13963
+ // the agent side. The SDK's `RestoreSessionRequest`
13964
+ // intentionally has no `mcpServers` field for the
13965
+ // same reason.
13966
+ mcpServers: []
13967
+ }),
13968
+ initTimeoutMs,
13969
+ "loadSession"
13970
+ ),
13971
+ transportClosed
13972
+ ]);
13973
+ } else {
13974
+ state = await Promise.race([
13975
+ withTimeout(
13976
+ ci.connection.unstable_resumeSession({
13977
+ sessionId: req.sessionId,
13978
+ cwd: workspaceKey,
13979
+ mcpServers: []
13980
+ }),
13981
+ initTimeoutMs,
13982
+ "resumeSession"
13983
+ ),
13984
+ transportClosed
13985
+ ]);
13986
+ }
13987
+ } catch (err) {
13988
+ restoreEvents.close();
13989
+ if (isAcpSessionResourceNotFound(err, req.sessionId)) {
13990
+ throw new SessionNotFoundError(req.sessionId);
13991
+ }
13992
+ if (ci.sessionIds.size === 0 && ci.pendingRestoreIds.size === 1 && ci.pendingRestoreIds.has(req.sessionId)) {
13993
+ ci.isDying = true;
13994
+ await ci.channel.kill().catch(() => {
13995
+ });
13996
+ }
13997
+ throw err;
13998
+ }
13999
+ if (shuttingDown) {
14000
+ restoreEvents.close();
14001
+ throw new Error("HttpAcpBridge is shutting down");
14002
+ }
14003
+ if (ci.isDying || !aliveChannels.has(ci)) {
14004
+ restoreEvents.close();
14005
+ throw new Error(
14006
+ `Session ${req.sessionId} restored on a closed agent channel`
14007
+ );
14008
+ }
14009
+ const racedEntry = byId.get(req.sessionId);
14010
+ if (racedEntry) {
14011
+ restoreEvents.close();
14012
+ racedEntry.attachCount += 1 + coalesceState.count;
14013
+ const clientId2 = registerClient(racedEntry, req.clientId);
14014
+ return {
14015
+ sessionId: racedEntry.sessionId,
14016
+ workspaceCwd: racedEntry.workspaceCwd,
14017
+ attached: true,
14018
+ clientId: clientId2,
14019
+ createdAt: racedEntry.createdAt,
14020
+ state: racedEntry.restoreState ?? {}
14021
+ };
14022
+ }
14023
+ const entry = createSessionEntry(
14024
+ ci,
14025
+ req.sessionId,
14026
+ workspaceKey,
14027
+ restoreEvents
14028
+ );
14029
+ entry.restoreState = state;
14030
+ const clientId = registerClient(entry, req.clientId);
14031
+ entry.attachCount = coalesceState.count;
14032
+ registeredEntry = entry;
14033
+ return {
14034
+ sessionId: entry.sessionId,
14035
+ workspaceCwd: entry.workspaceCwd,
14036
+ attached: false,
14037
+ clientId,
14038
+ createdAt: entry.createdAt,
14039
+ state
14040
+ };
14041
+ })().finally(() => {
14042
+ ci?.pendingRestoreIds.delete(req.sessionId);
14043
+ pendingRestoreEvents.delete(req.sessionId);
14044
+ if (!registeredEntry) {
14045
+ restoreEvents.close();
14046
+ }
14047
+ });
14048
+ inFlightRestores.set(req.sessionId, { action, promise, coalesceState });
14049
+ try {
14050
+ return await promise;
14051
+ } finally {
14052
+ inFlightRestores.delete(req.sessionId);
14053
+ }
14054
+ }
14055
+ __name(restoreSession, "restoreSession");
13862
14056
  return {
13863
14057
  get sessionCount() {
13864
14058
  return byId.size;
@@ -13866,35 +14060,41 @@ function createHttpAcpBridge(opts) {
13866
14060
  get pendingPermissionCount() {
13867
14061
  return pendingPermissions.size;
13868
14062
  },
14063
+ async loadSession(req) {
14064
+ return restoreSession("load", req);
14065
+ },
14066
+ async resumeSession(req) {
14067
+ return restoreSession("resume", req);
14068
+ },
13869
14069
  async spawnOrAttach(req) {
13870
14070
  if (shuttingDown) {
13871
14071
  throw new Error("HttpAcpBridge is shutting down");
13872
14072
  }
13873
- if (!path.isAbsolute(req.workspaceCwd)) {
13874
- throw new Error(
13875
- `workspaceCwd must be an absolute path; got "${req.workspaceCwd}"`
13876
- );
13877
- }
13878
- const workspaceKey = req.workspaceCwd === boundWorkspace ? boundWorkspace : canonicalizeWorkspace(req.workspaceCwd);
13879
- if (workspaceKey !== boundWorkspace) {
13880
- throw new WorkspaceMismatchError(boundWorkspace, workspaceKey);
14073
+ const workspaceKey = resolveWorkspaceKey(req.workspaceCwd);
14074
+ if (req.sessionScope !== void 0 && req.sessionScope !== "single" && req.sessionScope !== "thread") {
14075
+ throw new InvalidSessionScopeError(req.sessionScope);
13881
14076
  }
13882
- if (sessionScope === "single") {
14077
+ const effectiveScope = req.sessionScope ?? defaultSessionScope;
14078
+ if (effectiveScope === "single") {
13883
14079
  const existing = defaultEntry;
13884
14080
  if (existing) {
13885
14081
  existing.attachCount++;
14082
+ const clientId = registerClient(existing, req.clientId);
13886
14083
  if (req.modelServiceId) {
13887
14084
  await applyModelServiceId(
13888
14085
  existing,
13889
14086
  req.modelServiceId,
13890
- initTimeoutMs
14087
+ initTimeoutMs,
14088
+ clientId
13891
14089
  ).catch(() => {
13892
14090
  });
13893
14091
  }
13894
14092
  return {
13895
14093
  sessionId: existing.sessionId,
13896
14094
  workspaceCwd: existing.workspaceCwd,
13897
- attached: true
14095
+ attached: true,
14096
+ clientId,
14097
+ createdAt: existing.createdAt
13898
14098
  };
13899
14099
  }
13900
14100
  const inFlight = inFlightSpawns.get(workspaceKey);
@@ -13908,22 +14108,24 @@ function createHttpAcpBridge(opts) {
13908
14108
  "the agent child likely crashed during initialization \u2014 retry to spawn a new session"
13909
14109
  );
13910
14110
  }
14111
+ const clientId = registerClient(attachedEntry, req.clientId);
13911
14112
  if (req.modelServiceId) {
13912
14113
  await applyModelServiceId(
13913
14114
  attachedEntry,
13914
14115
  req.modelServiceId,
13915
- initTimeoutMs
14116
+ initTimeoutMs,
14117
+ clientId
13916
14118
  ).catch(() => {
13917
14119
  });
13918
14120
  }
13919
- return { ...session, attached: true };
14121
+ return { ...session, attached: true, clientId };
13920
14122
  }
13921
14123
  }
13922
- if (byId.size + inFlightSpawns.size >= maxSessions) {
14124
+ if (byId.size + inFlightSpawns.size + inFlightRestores.size >= maxSessions) {
13923
14125
  throw new SessionLimitExceededError(maxSessions);
13924
14126
  }
13925
- const promise = doSpawn(req.modelServiceId);
13926
- const tracker = sessionScope === "single" ? workspaceKey : `${workspaceKey}#${randomUUID()}`;
14127
+ const promise = doSpawn(req.modelServiceId, effectiveScope, req.clientId);
14128
+ const tracker = effectiveScope === "single" ? workspaceKey : `${workspaceKey}#${randomUUID()}`;
13927
14129
  inFlightSpawns.set(tracker, promise);
13928
14130
  try {
13929
14131
  return await promise;
@@ -13931,9 +14133,13 @@ function createHttpAcpBridge(opts) {
13931
14133
  inFlightSpawns.delete(tracker);
13932
14134
  }
13933
14135
  },
13934
- async sendPrompt(sessionId, req, signal) {
14136
+ async sendPrompt(sessionId, req, signal, context) {
13935
14137
  const entry = byId.get(sessionId);
13936
14138
  if (!entry) throw new SessionNotFoundError(sessionId);
14139
+ const originatorClientId = resolveTrustedClientId(
14140
+ entry,
14141
+ context?.clientId
14142
+ );
13937
14143
  if (signal?.aborted) {
13938
14144
  throw new DOMException("Prompt aborted", "AbortError");
13939
14145
  }
@@ -13942,7 +14148,14 @@ function createHttpAcpBridge(opts) {
13942
14148
  if (signal?.aborted) {
13943
14149
  throw new DOMException("Prompt aborted", "AbortError");
13944
14150
  }
13945
- const promptPromise = entry.connection.prompt(normalized);
14151
+ if (originatorClientId === void 0) {
14152
+ delete entry.activePromptOriginatorClientId;
14153
+ } else {
14154
+ entry.activePromptOriginatorClientId = originatorClientId;
14155
+ }
14156
+ const promptPromise = entry.connection.prompt(normalized).finally(() => {
14157
+ delete entry.activePromptOriginatorClientId;
14158
+ });
13946
14159
  const racedPromise = Promise.race([
13947
14160
  promptPromise,
13948
14161
  getTransportClosedReject(entry)
@@ -13969,9 +14182,10 @@ function createHttpAcpBridge(opts) {
13969
14182
  );
13970
14183
  return result;
13971
14184
  },
13972
- async cancelSession(sessionId, req) {
14185
+ async cancelSession(sessionId, req, context) {
13973
14186
  const entry = byId.get(sessionId);
13974
14187
  if (!entry) throw new SessionNotFoundError(sessionId);
14188
+ resolveTrustedClientId(entry, context?.clientId);
13975
14189
  cancelPendingForSession(sessionId);
13976
14190
  const notif = req ? { ...req, sessionId } : { sessionId };
13977
14191
  await entry.connection.cancel(notif);
@@ -13981,65 +14195,259 @@ function createHttpAcpBridge(opts) {
13981
14195
  if (!entry) throw new SessionNotFoundError(sessionId);
13982
14196
  return entry.events.subscribe(subOpts);
13983
14197
  },
13984
- respondToPermission(requestId, response) {
14198
+ respondToPermission(requestId, response, context) {
14199
+ const pending = pendingPermissions.get(requestId);
14200
+ let originatorClientId;
14201
+ if (context?.clientId !== void 0 && !pending) {
14202
+ resolveAnyTrustedClientId(context.clientId);
14203
+ } else if (pending && context?.clientId !== void 0) {
14204
+ const entry = byId.get(pending.sessionId);
14205
+ if (entry) {
14206
+ originatorClientId = resolveTrustedClientId(entry, context.clientId);
14207
+ } else {
14208
+ resolveAnyTrustedClientId(context.clientId);
14209
+ }
14210
+ }
14211
+ if (!pending) {
14212
+ const record = resolvedPermissions.get(requestId);
14213
+ if (record) {
14214
+ publishPermissionAlreadyResolved(record);
14215
+ }
14216
+ return false;
14217
+ }
13985
14218
  if (response.outcome.outcome === "selected") {
13986
- const pending = pendingPermissions.get(requestId);
13987
- if (pending && !pending.allowedOptionIds.has(response.outcome.optionId)) {
14219
+ if (!pending.allowedOptionIds.has(response.outcome.optionId)) {
13988
14220
  throw new InvalidPermissionOptionError(
13989
14221
  requestId,
13990
14222
  response.outcome.optionId
13991
14223
  );
13992
14224
  }
13993
14225
  }
13994
- return resolvePending(requestId, response);
14226
+ return resolvePending(requestId, response, originatorClientId);
13995
14227
  },
13996
- listWorkspaceSessions(workspaceCwd) {
13997
- if (!path.isAbsolute(workspaceCwd)) return [];
13998
- const key = workspaceCwd === boundWorkspace ? boundWorkspace : canonicalizeWorkspace(workspaceCwd);
13999
- if (key !== boundWorkspace) return [];
14000
- const out = [];
14001
- for (const entry of byId.values()) {
14002
- if (entry.workspaceCwd === key) {
14003
- out.push({
14004
- sessionId: entry.sessionId,
14005
- workspaceCwd: entry.workspaceCwd
14006
- });
14228
+ respondToSessionPermission(sessionId, requestId, response, context) {
14229
+ const entry = byId.get(sessionId);
14230
+ if (!entry) throw new SessionNotFoundError(sessionId);
14231
+ const pending = pendingPermissions.get(requestId);
14232
+ if (!pending) {
14233
+ const record = resolvedPermissions.get(requestId);
14234
+ if (record?.sessionId === sessionId) {
14235
+ resolveTrustedClientId(entry, context?.clientId);
14236
+ publishPermissionAlreadyResolved(record);
14237
+ } else if (record) {
14238
+ writeServeDebugLine(
14239
+ `rejected permission vote ${JSON.stringify(requestId)} for session ${JSON.stringify(sessionId)}; request belongs to session ${JSON.stringify(record.sessionId)}.`
14240
+ );
14007
14241
  }
14242
+ return false;
14008
14243
  }
14009
- return out;
14244
+ if (pending.sessionId !== sessionId) {
14245
+ writeServeDebugLine(
14246
+ `rejected permission vote ${JSON.stringify(requestId)} for session ${JSON.stringify(sessionId)}; request belongs to session ${JSON.stringify(pending.sessionId)}.`
14247
+ );
14248
+ return false;
14249
+ }
14250
+ const originatorClientId = resolveTrustedClientId(
14251
+ entry,
14252
+ context?.clientId
14253
+ );
14254
+ if (response.outcome.outcome === "selected" && !pending.allowedOptionIds.has(response.outcome.optionId)) {
14255
+ throw new InvalidPermissionOptionError(
14256
+ requestId,
14257
+ response.outcome.optionId
14258
+ );
14259
+ }
14260
+ return resolvePending(requestId, response, originatorClientId);
14010
14261
  },
14011
- async setSessionModel(sessionId, req) {
14262
+ async closeSession(sessionId, context) {
14012
14263
  const entry = byId.get(sessionId);
14013
14264
  if (!entry) throw new SessionNotFoundError(sessionId);
14014
- const normalized = { ...req, sessionId };
14015
- const conn = entry.connection;
14016
- const transportClosed = getTransportClosedReject(entry);
14017
- const work = entry.modelChangeQueue.then(
14018
- () => Promise.race([
14019
- withTimeout(
14020
- conn.unstable_setSessionModel(normalized),
14021
- initTimeoutMs,
14022
- "setSessionModel"
14023
- ),
14024
- transportClosed
14025
- ])
14026
- );
14027
- entry.modelChangeQueue = work.then(
14028
- () => void 0,
14029
- () => void 0
14265
+ let originatorClientId;
14266
+ if (context?.clientId !== void 0) {
14267
+ originatorClientId = resolveTrustedClientId(entry, context.clientId);
14268
+ }
14269
+ writeStderrLine(
14270
+ `qwen serve: closing session ${JSON.stringify(sessionId)}` + (originatorClientId ? ` by client ${JSON.stringify(originatorClientId)}` : "")
14030
14271
  );
14031
- let response;
14032
- try {
14033
- response = await work;
14034
- } catch (err) {
14035
- try {
14036
- entry.events.publish({
14037
- type: "model_switch_failed",
14038
- data: {
14272
+ if (defaultEntry === entry) defaultEntry = void 0;
14273
+ const ci = channelInfo;
14274
+ if (ci && ci.channel === entry.channel) {
14275
+ ci.sessionIds.delete(sessionId);
14276
+ }
14277
+ for (const id of Array.from(entry.pendingPermissionIds)) {
14278
+ resolvePending(id, { outcome: { outcome: "cancelled" } });
14279
+ }
14280
+ byId.delete(sessionId);
14281
+ try {
14282
+ entry.events.publish({
14283
+ type: "session_closed",
14284
+ data: {
14285
+ sessionId,
14286
+ reason: "client_close",
14287
+ ...originatorClientId ? { closedBy: originatorClientId } : {}
14288
+ }
14289
+ });
14290
+ } catch {
14291
+ }
14292
+ entry.events.close();
14293
+ try {
14294
+ await entry.connection.cancel({ sessionId });
14295
+ } catch {
14296
+ }
14297
+ if (ci && ci.sessionIds.size === 0 && ci.pendingRestoreIds.size === 0) {
14298
+ ci.isDying = true;
14299
+ await ci.channel.kill().catch((err) => {
14300
+ writeStderrLine(
14301
+ `qwen serve: closeSession channel kill failed for session ${JSON.stringify(sessionId)}: ${String(err)}`
14302
+ );
14303
+ });
14304
+ }
14305
+ },
14306
+ updateSessionMetadata(sessionId, metadata, context) {
14307
+ const entry = byId.get(sessionId);
14308
+ if (!entry) throw new SessionNotFoundError(sessionId);
14309
+ if (context?.clientId !== void 0) {
14310
+ resolveTrustedClientId(entry, context.clientId);
14311
+ }
14312
+ if (metadata.displayName !== void 0) {
14313
+ if (typeof metadata.displayName !== "string" || metadata.displayName.length > MAX_DISPLAY_NAME_LENGTH) {
14314
+ throw new InvalidSessionMetadataError(
14315
+ "displayName",
14316
+ `must be a string of at most ${MAX_DISPLAY_NAME_LENGTH} characters`
14317
+ );
14318
+ }
14319
+ if (hasControlCharacter(metadata.displayName)) {
14320
+ throw new InvalidSessionMetadataError(
14321
+ "displayName",
14322
+ "must not contain control characters"
14323
+ );
14324
+ }
14325
+ const nextDisplayName = metadata.displayName || void 0;
14326
+ if (entry.displayName !== nextDisplayName) {
14327
+ entry.displayName = nextDisplayName;
14328
+ writeStderrLine(
14329
+ `qwen serve: updated session metadata ${JSON.stringify(sessionId)} displayName=${entry.displayName === void 0 ? "cleared" : "set"}` + (context?.clientId ? ` by client ${JSON.stringify(context.clientId)}` : "")
14330
+ );
14331
+ try {
14332
+ entry.events.publish({
14333
+ type: "session_metadata_updated",
14334
+ data: { sessionId, displayName: entry.displayName }
14335
+ });
14336
+ } catch {
14337
+ }
14338
+ }
14339
+ }
14340
+ return { displayName: entry.displayName };
14341
+ },
14342
+ listWorkspaceSessions(workspaceCwd) {
14343
+ if (!path.isAbsolute(workspaceCwd)) return [];
14344
+ const key = workspaceCwd === boundWorkspace ? boundWorkspace : canonicalizeWorkspace(workspaceCwd);
14345
+ if (key !== boundWorkspace) return [];
14346
+ const out = [];
14347
+ for (const entry of byId.values()) {
14348
+ if (entry.workspaceCwd === key) {
14349
+ out.push({
14350
+ sessionId: entry.sessionId,
14351
+ workspaceCwd: entry.workspaceCwd,
14352
+ createdAt: entry.createdAt,
14353
+ displayName: entry.displayName,
14354
+ clientCount: entry.clientIds.size,
14355
+ hasActivePrompt: entry.activePromptOriginatorClientId !== void 0
14356
+ });
14357
+ }
14358
+ }
14359
+ return out;
14360
+ },
14361
+ recordHeartbeat(sessionId, context) {
14362
+ const entry = byId.get(sessionId);
14363
+ if (!entry) throw new SessionNotFoundError(sessionId);
14364
+ const clientId = resolveTrustedClientId(entry, context?.clientId);
14365
+ const lastSeenAt = Date.now();
14366
+ entry.sessionLastSeenAt = lastSeenAt;
14367
+ if (clientId !== void 0) {
14368
+ entry.clientLastSeenAt.set(clientId, lastSeenAt);
14369
+ }
14370
+ return {
14371
+ sessionId: entry.sessionId,
14372
+ ...clientId !== void 0 ? { clientId } : {},
14373
+ lastSeenAt
14374
+ };
14375
+ },
14376
+ getHeartbeatState(sessionId) {
14377
+ const entry = byId.get(sessionId);
14378
+ if (!entry) return void 0;
14379
+ return {
14380
+ ...entry.sessionLastSeenAt !== void 0 ? { sessionLastSeenAt: entry.sessionLastSeenAt } : {},
14381
+ clientLastSeenAt: new Map(entry.clientLastSeenAt)
14382
+ };
14383
+ },
14384
+ async getWorkspaceMcpStatus() {
14385
+ return requestWorkspaceStatus(
14386
+ SERVE_STATUS_EXT_METHODS.workspaceMcp,
14387
+ () => createIdleWorkspaceMcpStatus(boundWorkspace)
14388
+ );
14389
+ },
14390
+ async getWorkspaceSkillsStatus() {
14391
+ return requestWorkspaceStatus(
14392
+ SERVE_STATUS_EXT_METHODS.workspaceSkills,
14393
+ () => createIdleWorkspaceSkillsStatus(boundWorkspace)
14394
+ );
14395
+ },
14396
+ async getWorkspaceProvidersStatus() {
14397
+ return requestWorkspaceStatus(
14398
+ SERVE_STATUS_EXT_METHODS.workspaceProviders,
14399
+ () => createIdleWorkspaceProvidersStatus(boundWorkspace)
14400
+ );
14401
+ },
14402
+ async getSessionContextStatus(sessionId) {
14403
+ return requestSessionStatus(
14404
+ sessionId,
14405
+ SERVE_STATUS_EXT_METHODS.sessionContext
14406
+ );
14407
+ },
14408
+ async getSessionSupportedCommandsStatus(sessionId) {
14409
+ return requestSessionStatus(
14410
+ sessionId,
14411
+ SERVE_STATUS_EXT_METHODS.sessionSupportedCommands
14412
+ );
14413
+ },
14414
+ async setSessionModel(sessionId, req, context) {
14415
+ const entry = byId.get(sessionId);
14416
+ if (!entry) throw new SessionNotFoundError(sessionId);
14417
+ const originatorClientId = resolveTrustedClientId(
14418
+ entry,
14419
+ context?.clientId
14420
+ );
14421
+ const normalized = { ...req, sessionId };
14422
+ const conn = entry.connection;
14423
+ const transportClosed = getTransportClosedReject(entry);
14424
+ const work = entry.modelChangeQueue.then(
14425
+ () => Promise.race([
14426
+ withTimeout(
14427
+ conn.unstable_setSessionModel(normalized),
14428
+ initTimeoutMs,
14429
+ "setSessionModel"
14430
+ ),
14431
+ transportClosed
14432
+ ])
14433
+ );
14434
+ entry.modelChangeQueue = work.then(
14435
+ () => void 0,
14436
+ () => void 0
14437
+ );
14438
+ let response;
14439
+ try {
14440
+ response = await work;
14441
+ } catch (err) {
14442
+ try {
14443
+ entry.events.publish({
14444
+ type: "model_switch_failed",
14445
+ data: {
14039
14446
  sessionId: entry.sessionId,
14040
14447
  requestedModelId: req.modelId,
14041
14448
  error: err instanceof Error ? err.message : String(err)
14042
- }
14449
+ },
14450
+ ...originatorClientId ? { originatorClientId } : {}
14043
14451
  });
14044
14452
  } catch {
14045
14453
  }
@@ -14048,7 +14456,8 @@ function createHttpAcpBridge(opts) {
14048
14456
  try {
14049
14457
  entry.events.publish({
14050
14458
  type: "model_switched",
14051
- data: { sessionId: entry.sessionId, modelId: req.modelId }
14459
+ data: { sessionId: entry.sessionId, modelId: req.modelId },
14460
+ ...originatorClientId ? { originatorClientId } : {}
14052
14461
  });
14053
14462
  } catch {
14054
14463
  }
@@ -14078,16 +14487,17 @@ function createHttpAcpBridge(opts) {
14078
14487
  } catch {
14079
14488
  }
14080
14489
  entry.events.close();
14081
- if (ci && ci.sessionIds.size === 0) {
14490
+ if (ci && ci.sessionIds.size === 0 && ci.pendingRestoreIds.size === 0) {
14082
14491
  ci.isDying = true;
14083
14492
  await ci.channel.kill().catch(() => {
14084
14493
  });
14085
14494
  }
14086
14495
  },
14087
- async detachClient(sessionId) {
14496
+ async detachClient(sessionId, clientId) {
14088
14497
  const entry = byId.get(sessionId);
14089
14498
  if (!entry) return;
14090
14499
  if (entry.attachCount > 0) entry.attachCount--;
14500
+ unregisterClient(entry, clientId);
14091
14501
  if (entry.spawnOwnerWantedKill && entry.attachCount === 0 && entry.events.subscriberCount === 0) {
14092
14502
  await this.killSession(sessionId).catch(() => {
14093
14503
  });
@@ -14135,6 +14545,12 @@ function createHttpAcpBridge(opts) {
14135
14545
  () => void 0
14136
14546
  )
14137
14547
  );
14548
+ const inFlightRestoreAwaits = Array.from(inFlightRestores.values()).map(
14549
+ (restore) => restore.promise.then(
14550
+ () => void 0,
14551
+ () => void 0
14552
+ )
14553
+ );
14138
14554
  const inFlightChannelAwait = inFlightChannelSpawn ? inFlightChannelSpawn.then(
14139
14555
  () => void 0,
14140
14556
  () => void 0
@@ -14143,6 +14559,7 @@ function createHttpAcpBridge(opts) {
14143
14559
  ...channels.map((ci) => ci.channel.kill().catch(() => {
14144
14560
  })),
14145
14561
  ...inFlightSessionAwaits,
14562
+ ...inFlightRestoreAwaits,
14146
14563
  inFlightChannelAwait
14147
14564
  ]);
14148
14565
  }
@@ -14328,20 +14745,101 @@ function killChild(child) {
14328
14745
  }
14329
14746
  __name(killChild, "killChild");
14330
14747
 
14748
+ // packages/cli/src/serve/capabilities.ts
14749
+ init_esbuild_shims();
14750
+ var SERVE_PROTOCOL_VERSION = "v1";
14751
+ var SUPPORTED_SERVE_PROTOCOL_VERSIONS = [
14752
+ SERVE_PROTOCOL_VERSION
14753
+ ];
14754
+ var SERVE_CAPABILITY_REGISTRY = {
14755
+ health: { since: "v1" },
14756
+ capabilities: { since: "v1" },
14757
+ session_create: { since: "v1" },
14758
+ session_scope_override: { since: "v1" },
14759
+ session_load: { since: "v1" },
14760
+ // ACP backs this with `connection.unstable_resumeSession`. Surface
14761
+ // the unstable prefix so clients don't pin against a `v1` shape that
14762
+ // the underlying ACP method may still change.
14763
+ unstable_session_resume: { since: "v1" },
14764
+ session_list: { since: "v1" },
14765
+ session_prompt: { since: "v1" },
14766
+ session_cancel: { since: "v1" },
14767
+ session_events: { since: "v1" },
14768
+ // Daemon emits `slow_client_warning` synthetic frames at 75% queue
14769
+ // fill and honors `?maxQueued=N` (range [16, 2048]) on
14770
+ // `GET /session/:id/events`. Old daemons silently lack both — SDK
14771
+ // clients pre-flight this tag before opting in.
14772
+ slow_client_warning: { since: "v1" },
14773
+ // SDK consumers can detect `KnownDaemonEvent` schema support without
14774
+ // pinning against this SDK release — `narrowDaemonEvent` falls back
14775
+ // to `kind: 'unknown'` for daemons that don't advertise the tag,
14776
+ // so the tag is purely informational.
14777
+ typed_event_schema: { since: "v1" },
14778
+ session_set_model: { since: "v1" },
14779
+ client_identity: { since: "v1" },
14780
+ client_heartbeat: { since: "v1" },
14781
+ session_permission_vote: { since: "v1" },
14782
+ permission_vote: { since: "v1" },
14783
+ workspace_mcp: { since: "v1" },
14784
+ workspace_skills: { since: "v1" },
14785
+ workspace_providers: { since: "v1" },
14786
+ session_context: { since: "v1" },
14787
+ session_supported_commands: { since: "v1" },
14788
+ session_close: { since: "v1" },
14789
+ session_metadata: { since: "v1" },
14790
+ // Issue #4175 PR 15. Daemon was booted with `--require-auth` (or
14791
+ // `requireAuth: true`), so even loopback callers must carry a bearer
14792
+ // token. Advertised CONDITIONALLY — only when the flag is on — so
14793
+ // SDK clients can branch on its presence to surface a clear "this
14794
+ // deployment requires auth" hint instead of speculatively trying
14795
+ // requests and parsing the resulting 401 body. Loopback developer
14796
+ // defaults (no flag) omit the tag, preserving the bit-for-bit shape
14797
+ // older clients expect.
14798
+ require_auth: { since: "v1" }
14799
+ };
14800
+ var CONDITIONAL_SERVE_FEATURES = /* @__PURE__ */ new Map([
14801
+ ["require_auth", (toggles) => toggles.requireAuth === true]
14802
+ ]);
14803
+ var SERVE_FEATURES = Object.freeze(
14804
+ Object.keys(SERVE_CAPABILITY_REGISTRY)
14805
+ );
14806
+ function serveProtocolVersionIndex(version) {
14807
+ return SUPPORTED_SERVE_PROTOCOL_VERSIONS.indexOf(version);
14808
+ }
14809
+ __name(serveProtocolVersionIndex, "serveProtocolVersionIndex");
14810
+ function isFeatureAvailableInProtocol(feature, protocolVersion) {
14811
+ return serveProtocolVersionIndex(SERVE_CAPABILITY_REGISTRY[feature].since) <= serveProtocolVersionIndex(protocolVersion);
14812
+ }
14813
+ __name(isFeatureAvailableInProtocol, "isFeatureAvailableInProtocol");
14814
+ function getRegisteredServeFeatures() {
14815
+ return [...SERVE_FEATURES];
14816
+ }
14817
+ __name(getRegisteredServeFeatures, "getRegisteredServeFeatures");
14818
+ function getAdvertisedServeFeatures(protocolVersion = SERVE_PROTOCOL_VERSION, toggles = {}) {
14819
+ return SERVE_FEATURES.filter((feature) => {
14820
+ if (!isFeatureAvailableInProtocol(feature, protocolVersion)) return false;
14821
+ const predicate = CONDITIONAL_SERVE_FEATURES.get(feature);
14822
+ if (predicate !== void 0) return predicate(toggles);
14823
+ return true;
14824
+ });
14825
+ }
14826
+ __name(getAdvertisedServeFeatures, "getAdvertisedServeFeatures");
14827
+ function getServeFeatures() {
14828
+ return getAdvertisedServeFeatures();
14829
+ }
14830
+ __name(getServeFeatures, "getServeFeatures");
14831
+ function getServeProtocolVersions() {
14832
+ return {
14833
+ current: SERVE_PROTOCOL_VERSION,
14834
+ supported: [...SUPPORTED_SERVE_PROTOCOL_VERSIONS]
14835
+ };
14836
+ }
14837
+ __name(getServeProtocolVersions, "getServeProtocolVersions");
14838
+
14331
14839
  // packages/cli/src/serve/types.ts
14332
14840
  init_esbuild_shims();
14333
14841
  var CAPABILITIES_SCHEMA_VERSION = 1;
14334
- var STAGE1_FEATURES = [
14335
- "health",
14336
- "capabilities",
14337
- "session_create",
14338
- "session_list",
14339
- "session_prompt",
14340
- "session_cancel",
14341
- "session_events",
14342
- "session_set_model",
14343
- "permission_vote"
14344
- ];
14842
+ var STAGE1_FEATURES = SERVE_FEATURES;
14345
14843
 
14346
14844
  // packages/cli/src/serve/server.ts
14347
14845
  function createServeApp(opts, getPort = () => opts.port, deps = {}) {
@@ -14349,6 +14847,11 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14349
14847
  const boundWorkspace = deps.boundWorkspace ?? canonicalizeWorkspace(opts.workspace ?? process.cwd());
14350
14848
  const bridge = deps.bridge ?? createHttpAcpBridge({
14351
14849
  maxSessions: opts.maxSessions,
14850
+ // Symmetric with `runQwenServe.ts` — direct embeds / tests that
14851
+ // call `createServeApp` without supplying their own bridge and
14852
+ // pass `ServeOptions.eventRingSize` would otherwise silently
14853
+ // get the default 8000 ring instead of their configured value.
14854
+ ...opts.eventRingSize !== void 0 ? { eventRingSize: opts.eventRingSize } : {},
14352
14855
  boundWorkspace
14353
14856
  });
14354
14857
  app.use(denyBrowserOriginCors);
@@ -14374,19 +14877,31 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14374
14877
  }
14375
14878
  }, "healthHandler");
14376
14879
  const loopback = isLoopbackBind(opts.hostname);
14377
- if (loopback) {
14880
+ const exposeHealthPreAuth = loopback && !opts.requireAuth;
14881
+ if (exposeHealthPreAuth) {
14378
14882
  app.get("/health", healthHandler);
14379
14883
  }
14380
14884
  app.use(bearerAuth(opts.token));
14381
14885
  app.use(import_express.default.json({ limit: "10mb" }));
14382
- if (!loopback) {
14886
+ if (!exposeHealthPreAuth) {
14383
14887
  app.get("/health", healthHandler);
14384
14888
  }
14889
+ const mutate = createMutationGate({
14890
+ tokenConfigured: opts.token !== void 0,
14891
+ requireAuth: opts.requireAuth === true
14892
+ });
14385
14893
  app.get("/capabilities", (_req, res) => {
14386
14894
  const envelope = {
14387
14895
  v: CAPABILITIES_SCHEMA_VERSION,
14896
+ protocolVersions: getServeProtocolVersions(),
14388
14897
  mode: opts.mode,
14389
- features: [...STAGE1_FEATURES],
14898
+ // PR 15. Pass `requireAuth` so the `require_auth` tag appears
14899
+ // ONLY when the operator opted in. Tag presence = behavior is
14900
+ // on; older daemons without this PR omit the tag and SDKs that
14901
+ // post-PR feature-detect on it stay backward compatible.
14902
+ features: getAdvertisedServeFeatures(void 0, {
14903
+ requireAuth: opts.requireAuth === true
14904
+ }),
14390
14905
  modelServices: [],
14391
14906
  // #3803 §02: surface the bound workspace so clients can detect
14392
14907
  // mismatch pre-flight and omit `cwd` on `POST /session`.
@@ -14394,7 +14909,28 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14394
14909
  };
14395
14910
  res.status(200).json(envelope);
14396
14911
  });
14397
- app.post("/session", async (req, res) => {
14912
+ app.get("/workspace/mcp", async (_req, res) => {
14913
+ try {
14914
+ res.status(200).json(await bridge.getWorkspaceMcpStatus());
14915
+ } catch (err) {
14916
+ sendBridgeError(res, err, { route: "GET /workspace/mcp" });
14917
+ }
14918
+ });
14919
+ app.get("/workspace/skills", async (_req, res) => {
14920
+ try {
14921
+ res.status(200).json(await bridge.getWorkspaceSkillsStatus());
14922
+ } catch (err) {
14923
+ sendBridgeError(res, err, { route: "GET /workspace/skills" });
14924
+ }
14925
+ });
14926
+ app.get("/workspace/providers", async (_req, res) => {
14927
+ try {
14928
+ res.status(200).json(await bridge.getWorkspaceProvidersStatus());
14929
+ } catch (err) {
14930
+ sendBridgeError(res, err, { route: "GET /workspace/providers" });
14931
+ }
14932
+ });
14933
+ app.post("/session", mutate(), async (req, res) => {
14398
14934
  const body = safeBody(req);
14399
14935
  const hasCwd = "cwd" in body;
14400
14936
  if (hasCwd && typeof body["cwd"] !== "string") {
@@ -14413,17 +14949,33 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14413
14949
  return;
14414
14950
  }
14415
14951
  const modelServiceId = typeof body["modelServiceId"] === "string" ? body["modelServiceId"] : void 0;
14952
+ const rawSessionScope = body["sessionScope"];
14953
+ let sessionScope;
14954
+ if (rawSessionScope !== void 0) {
14955
+ if (rawSessionScope !== "single" && rawSessionScope !== "thread") {
14956
+ res.status(400).json({
14957
+ error: '`sessionScope` must be "single" or "thread" when provided',
14958
+ code: "invalid_session_scope"
14959
+ });
14960
+ return;
14961
+ }
14962
+ sessionScope = rawSessionScope;
14963
+ }
14964
+ const clientId = parseClientIdHeader(req, res);
14965
+ if (clientId === null) return;
14416
14966
  try {
14417
14967
  const session = await bridge.spawnOrAttach({
14418
14968
  workspaceCwd: cwd,
14419
- modelServiceId
14969
+ modelServiceId,
14970
+ ...clientId !== void 0 ? { clientId } : {},
14971
+ ...sessionScope !== void 0 ? { sessionScope } : {}
14420
14972
  });
14421
14973
  if (!res.writable) {
14422
14974
  if (!session.attached) {
14423
14975
  bridge.killSession(session.sessionId, { requireZeroAttaches: true }).catch(() => {
14424
14976
  });
14425
14977
  } else {
14426
- bridge.detachClient(session.sessionId).catch(() => {
14978
+ bridge.detachClient(session.sessionId, session.clientId).catch(() => {
14427
14979
  });
14428
14980
  }
14429
14981
  return;
@@ -14433,7 +14985,78 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14433
14985
  sendBridgeError(res, err, { route: "POST /session" });
14434
14986
  }
14435
14987
  });
14436
- app.post("/session/:id/prompt", async (req, res) => {
14988
+ const restoreSessionHandler = /* @__PURE__ */ __name((action) => async (req, res) => {
14989
+ const sessionId = req.params["id"];
14990
+ if (!sessionId) {
14991
+ res.status(400).json({ error: "`sessionId` route parameter is required" });
14992
+ return;
14993
+ }
14994
+ const body = safeBody(req);
14995
+ const cwd = parseOptionalWorkspaceCwd(body, boundWorkspace, res);
14996
+ if (cwd === void 0) return;
14997
+ const clientId = parseClientIdHeader(req, res);
14998
+ if (clientId === null) return;
14999
+ try {
15000
+ const session = action === "load" ? await bridge.loadSession({
15001
+ sessionId,
15002
+ workspaceCwd: cwd,
15003
+ ...clientId !== void 0 ? { clientId } : {}
15004
+ }) : await bridge.resumeSession({
15005
+ sessionId,
15006
+ workspaceCwd: cwd,
15007
+ ...clientId !== void 0 ? { clientId } : {}
15008
+ });
15009
+ if (!res.writable) {
15010
+ if (!session.attached) {
15011
+ bridge.killSession(session.sessionId, { requireZeroAttaches: true }).catch(() => {
15012
+ });
15013
+ } else {
15014
+ bridge.detachClient(session.sessionId, session.clientId).catch(() => {
15015
+ });
15016
+ }
15017
+ return;
15018
+ }
15019
+ res.status(200).json(session);
15020
+ } catch (err) {
15021
+ sendBridgeError(res, err, {
15022
+ route: `POST /session/:id/${action}`,
15023
+ sessionId
15024
+ });
15025
+ }
15026
+ }, "restoreSessionHandler");
15027
+ app.post("/session/:id/load", mutate(), restoreSessionHandler("load"));
15028
+ app.post("/session/:id/resume", mutate(), restoreSessionHandler("resume"));
15029
+ app.get("/session/:id/context", async (req, res) => {
15030
+ const sessionId = req.params["id"];
15031
+ if (!sessionId) {
15032
+ res.status(400).json({ error: "`sessionId` route parameter is required" });
15033
+ return;
15034
+ }
15035
+ try {
15036
+ res.status(200).json(await bridge.getSessionContextStatus(sessionId));
15037
+ } catch (err) {
15038
+ sendBridgeError(res, err, {
15039
+ route: "GET /session/:id/context",
15040
+ sessionId
15041
+ });
15042
+ }
15043
+ });
15044
+ app.get("/session/:id/supported-commands", async (req, res) => {
15045
+ const sessionId = req.params["id"];
15046
+ if (!sessionId) {
15047
+ res.status(400).json({ error: "`sessionId` route parameter is required" });
15048
+ return;
15049
+ }
15050
+ try {
15051
+ res.status(200).json(await bridge.getSessionSupportedCommandsStatus(sessionId));
15052
+ } catch (err) {
15053
+ sendBridgeError(res, err, {
15054
+ route: "GET /session/:id/supported-commands",
15055
+ sessionId
15056
+ });
15057
+ }
15058
+ });
15059
+ app.post("/session/:id/prompt", mutate(), async (req, res) => {
14437
15060
  const sessionId = req.params["id"];
14438
15061
  const body = safeBody(req);
14439
15062
  const prompt = body["prompt"];
@@ -14464,6 +15087,11 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14464
15087
  if (!res.writableEnded) abort.abort();
14465
15088
  }, "onResClose");
14466
15089
  res.once("close", onResClose);
15090
+ const clientId = parseClientIdHeader(req, res);
15091
+ if (clientId === null) {
15092
+ res.off("close", onResClose);
15093
+ return;
15094
+ }
14467
15095
  try {
14468
15096
  const result = await bridge.sendPrompt(
14469
15097
  sessionId,
@@ -14472,7 +15100,8 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14472
15100
  sessionId,
14473
15101
  prompt
14474
15102
  },
14475
- abort.signal
15103
+ abort.signal,
15104
+ clientId !== void 0 ? { clientId } : void 0
14476
15105
  );
14477
15106
  res.status(200).json(result);
14478
15107
  } catch (err) {
@@ -14487,14 +15116,41 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14487
15116
  res.off("close", onResClose);
14488
15117
  }
14489
15118
  });
14490
- app.post("/session/:id/cancel", async (req, res) => {
15119
+ app.post("/session/:id/heartbeat", mutate(), (req, res) => {
14491
15120
  const sessionId = req.params["id"];
14492
- const body = safeBody(req);
15121
+ if (!sessionId) {
15122
+ res.status(400).json({ error: "`sessionId` route parameter is required" });
15123
+ return;
15124
+ }
15125
+ const clientId = parseClientIdHeader(req, res);
15126
+ if (clientId === null) return;
14493
15127
  try {
14494
- await bridge.cancelSession(sessionId, {
14495
- ...body,
15128
+ const result = bridge.recordHeartbeat(
15129
+ sessionId,
15130
+ clientId !== void 0 ? { clientId } : void 0
15131
+ );
15132
+ res.status(200).json(result);
15133
+ } catch (err) {
15134
+ sendBridgeError(res, err, {
15135
+ route: "POST /session/:id/heartbeat",
14496
15136
  sessionId
14497
15137
  });
15138
+ }
15139
+ });
15140
+ app.post("/session/:id/cancel", mutate(), async (req, res) => {
15141
+ const sessionId = req.params["id"];
15142
+ const body = safeBody(req);
15143
+ const clientId = parseClientIdHeader(req, res);
15144
+ if (clientId === null) return;
15145
+ try {
15146
+ await bridge.cancelSession(
15147
+ sessionId,
15148
+ {
15149
+ ...body,
15150
+ sessionId
15151
+ },
15152
+ clientId !== void 0 ? { clientId } : void 0
15153
+ );
14498
15154
  res.status(204).end();
14499
15155
  } catch (err) {
14500
15156
  sendBridgeError(res, err, {
@@ -14503,6 +15159,51 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14503
15159
  });
14504
15160
  }
14505
15161
  });
15162
+ app.delete("/session/:id", async (req, res) => {
15163
+ const sessionId = req.params["id"];
15164
+ const clientId = parseClientIdHeader(req, res);
15165
+ if (clientId === null) return;
15166
+ try {
15167
+ await bridge.closeSession(
15168
+ sessionId,
15169
+ clientId !== void 0 ? { clientId } : void 0
15170
+ );
15171
+ res.status(204).end();
15172
+ } catch (err) {
15173
+ sendBridgeError(res, err, {
15174
+ route: "DELETE /session/:id",
15175
+ sessionId
15176
+ });
15177
+ }
15178
+ });
15179
+ app.patch("/session/:id/metadata", (req, res) => {
15180
+ const sessionId = req.params["id"];
15181
+ const body = safeBody(req);
15182
+ const clientId = parseClientIdHeader(req, res);
15183
+ if (clientId === null) return;
15184
+ const displayName = body["displayName"];
15185
+ if (displayName !== void 0 && typeof displayName !== "string") {
15186
+ res.status(400).json({
15187
+ error: "`displayName` must be a string",
15188
+ code: "invalid_metadata",
15189
+ field: "displayName"
15190
+ });
15191
+ return;
15192
+ }
15193
+ try {
15194
+ const effective = bridge.updateSessionMetadata(
15195
+ sessionId,
15196
+ { displayName },
15197
+ clientId !== void 0 ? { clientId } : void 0
15198
+ );
15199
+ res.status(200).json({ sessionId, ...effective });
15200
+ } catch (err) {
15201
+ sendBridgeError(res, err, {
15202
+ route: "PATCH /session/:id/metadata",
15203
+ sessionId
15204
+ });
15205
+ }
15206
+ });
14506
15207
  app.get("/workspace/:id/sessions", (req, res) => {
14507
15208
  const workspaceCwd = req.params["id"] ?? "";
14508
15209
  if (!path2.isAbsolute(workspaceCwd)) {
@@ -14522,7 +15223,7 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14522
15223
  const sessions = bridge.listWorkspaceSessions(workspaceCwd);
14523
15224
  res.status(200).json({ sessions });
14524
15225
  });
14525
- app.post("/session/:id/model", async (req, res) => {
15226
+ app.post("/session/:id/model", mutate(), async (req, res) => {
14526
15227
  const sessionId = req.params["id"];
14527
15228
  const body = safeBody(req);
14528
15229
  const modelId = body["modelId"];
@@ -14532,12 +15233,18 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14532
15233
  });
14533
15234
  return;
14534
15235
  }
15236
+ const clientId = parseClientIdHeader(req, res);
15237
+ if (clientId === null) return;
14535
15238
  try {
14536
- const response = await bridge.setSessionModel(sessionId, {
14537
- ...body,
15239
+ const response = await bridge.setSessionModel(
14538
15240
  sessionId,
14539
- modelId
14540
- });
15241
+ {
15242
+ ...body,
15243
+ sessionId,
15244
+ modelId
15245
+ },
15246
+ clientId !== void 0 ? { clientId } : void 0
15247
+ );
14541
15248
  res.status(200).json(response);
14542
15249
  } catch (err) {
14543
15250
  sendBridgeError(res, err, {
@@ -14546,33 +15253,56 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14546
15253
  });
14547
15254
  }
14548
15255
  });
14549
- app.post("/permission/:requestId", (req, res) => {
15256
+ app.post("/session/:id/permission/:requestId", mutate(), (req, res) => {
15257
+ const sessionId = req.params["id"];
14550
15258
  const requestId = req.params["requestId"];
14551
- const body = safeBody(req);
14552
- const outcome = body["outcome"];
14553
- if (!isValidOutcome(outcome)) {
14554
- res.status(400).json({
14555
- error: '`outcome` must be `{ outcome: "cancelled" }` or `{ outcome: "selected", optionId: string }`'
15259
+ const response = parsePermissionVoteBody(req, res);
15260
+ if (response === void 0) return;
15261
+ const clientId = parseClientIdHeader(req, res);
15262
+ if (clientId === null) return;
15263
+ let accepted;
15264
+ try {
15265
+ accepted = bridge.respondToSessionPermission(
15266
+ sessionId,
15267
+ requestId,
15268
+ response,
15269
+ clientId !== void 0 ? { clientId } : void 0
15270
+ );
15271
+ } catch (err) {
15272
+ sendPermissionVoteError(res, err, {
15273
+ route: "POST /session/:id/permission/:requestId",
15274
+ sessionId
15275
+ });
15276
+ return;
15277
+ }
15278
+ if (!accepted) {
15279
+ res.status(404).json({
15280
+ error: "No pending permission request for session",
15281
+ sessionId,
15282
+ requestId
14556
15283
  });
14557
15284
  return;
14558
15285
  }
15286
+ res.status(200).json({});
15287
+ });
15288
+ app.post("/permission/:requestId", mutate(), (req, res) => {
15289
+ const requestId = req.params["requestId"];
15290
+ const response = parsePermissionVoteBody(req, res);
15291
+ if (response === void 0) return;
15292
+ const clientId = parseClientIdHeader(req, res);
15293
+ if (clientId === null) return;
14559
15294
  let accepted;
14560
15295
  try {
14561
- accepted = bridge.respondToPermission(requestId, {
14562
- ...body,
14563
- outcome
14564
- });
15296
+ accepted = bridge.respondToPermission(
15297
+ requestId,
15298
+ response,
15299
+ clientId !== void 0 ? { clientId } : void 0
15300
+ );
14565
15301
  } catch (err) {
14566
- if (err instanceof InvalidPermissionOptionError) {
14567
- res.status(400).json({
14568
- error: err.message,
14569
- code: "invalid_option_id",
14570
- requestId: err.requestId,
14571
- optionId: err.optionId
14572
- });
14573
- return;
14574
- }
14575
- throw err;
15302
+ sendPermissionVoteError(res, err, {
15303
+ route: "POST /permission/:requestId"
15304
+ });
15305
+ return;
14576
15306
  }
14577
15307
  if (!accepted) {
14578
15308
  res.status(404).json({ error: "No pending permission request", requestId });
@@ -14583,12 +15313,15 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14583
15313
  app.get("/session/:id/events", (req, res) => {
14584
15314
  const sessionId = req.params["id"];
14585
15315
  const lastEventId = parseLastEventId(req.headers["last-event-id"]);
15316
+ const maxQueued = parseMaxQueuedQuery(req.query["maxQueued"], res);
15317
+ if (maxQueued === null) return;
14586
15318
  let iter;
14587
15319
  const abort = new AbortController();
14588
15320
  try {
14589
15321
  const iterable = bridge.subscribeEvents(sessionId, {
14590
15322
  signal: abort.signal,
14591
- lastEventId
15323
+ lastEventId,
15324
+ ...maxQueued !== void 0 ? { maxQueued } : {}
14592
15325
  });
14593
15326
  iter = iterable[Symbol.asyncIterator]();
14594
15327
  } catch (err) {
@@ -14728,6 +15461,10 @@ var PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
14728
15461
  "constructor",
14729
15462
  "prototype"
14730
15463
  ]);
15464
+ var CLIENT_ID_HEADER = "x-qwen-client-id";
15465
+ var MAX_CLIENT_ID_LENGTH = 128;
15466
+ var CLIENT_ID_RE = /^[A-Za-z0-9._:-]+$/;
15467
+ var INVALID_PERMISSION_OUTCOME_ERROR = '`outcome` must be `{ outcome: "cancelled" }` or `{ outcome: "selected", optionId: string }`';
14731
15468
  function safeBody(req) {
14732
15469
  const raw = req.body;
14733
15470
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
@@ -14741,6 +15478,52 @@ function safeBody(req) {
14741
15478
  return out;
14742
15479
  }
14743
15480
  __name(safeBody, "safeBody");
15481
+ function parseOptionalWorkspaceCwd(body, boundWorkspace, res) {
15482
+ const hasCwd = "cwd" in body;
15483
+ if (hasCwd && typeof body["cwd"] !== "string") {
15484
+ res.status(400).json({ error: "`cwd` must be a string absolute path when provided" });
15485
+ return void 0;
15486
+ }
15487
+ if (hasCwd && body["cwd"].length > MAX_WORKSPACE_PATH_LENGTH) {
15488
+ res.status(400).json({
15489
+ error: `\`cwd\` exceeds the ${MAX_WORKSPACE_PATH_LENGTH}-character limit`
15490
+ });
15491
+ return void 0;
15492
+ }
15493
+ const cwd = hasCwd ? body["cwd"] : boundWorkspace;
15494
+ if (!path2.isAbsolute(cwd)) {
15495
+ res.status(400).json({ error: "`cwd` must be an absolute path when provided" });
15496
+ return void 0;
15497
+ }
15498
+ return cwd;
15499
+ }
15500
+ __name(parseOptionalWorkspaceCwd, "parseOptionalWorkspaceCwd");
15501
+ function parseClientIdHeader(req, res) {
15502
+ const raw = req.get(CLIENT_ID_HEADER);
15503
+ if (raw === void 0 || raw === "") return void 0;
15504
+ if (raw.length > MAX_CLIENT_ID_LENGTH || !CLIENT_ID_RE.test(raw)) {
15505
+ res.status(400).json({
15506
+ error: "`X-Qwen-Client-Id` must be a non-empty token of 128 characters or fewer",
15507
+ code: "invalid_client_id"
15508
+ });
15509
+ return null;
15510
+ }
15511
+ return raw;
15512
+ }
15513
+ __name(parseClientIdHeader, "parseClientIdHeader");
15514
+ function parsePermissionVoteBody(req, res) {
15515
+ const body = safeBody(req);
15516
+ const outcome = body["outcome"];
15517
+ if (!isValidOutcome(outcome)) {
15518
+ res.status(400).json({ error: INVALID_PERMISSION_OUTCOME_ERROR });
15519
+ return void 0;
15520
+ }
15521
+ return {
15522
+ ...body,
15523
+ outcome
15524
+ };
15525
+ }
15526
+ __name(parsePermissionVoteBody, "parsePermissionVoteBody");
14744
15527
  function isValidOutcome(raw) {
14745
15528
  if (typeof raw !== "object" || raw === null) return false;
14746
15529
  const obj = raw;
@@ -14748,11 +15531,43 @@ function isValidOutcome(raw) {
14748
15531
  return obj["outcome"] === "selected" && typeof obj["optionId"] === "string" && obj["optionId"].length > 0;
14749
15532
  }
14750
15533
  __name(isValidOutcome, "isValidOutcome");
15534
+ var MIN_QUERY_MAX_QUEUED = 16;
15535
+ var MAX_QUERY_MAX_QUEUED = 2048;
15536
+ function parseMaxQueuedQuery(raw, res) {
15537
+ if (raw === void 0) return void 0;
15538
+ if (typeof raw !== "string" || !/^\d+$/.test(raw)) {
15539
+ writeStderrLine(
15540
+ `qwen serve: rejected ?maxQueued ${safeLogValue(raw)} (not a decimal integer)`
15541
+ );
15542
+ res.status(400).json({
15543
+ error: "`maxQueued` must be a decimal integer",
15544
+ code: "invalid_max_queued"
15545
+ });
15546
+ return null;
15547
+ }
15548
+ const n = Number.parseInt(raw, 10);
15549
+ if (!Number.isFinite(n) || n < MIN_QUERY_MAX_QUEUED || n > MAX_QUERY_MAX_QUEUED) {
15550
+ writeStderrLine(
15551
+ `qwen serve: rejected ?maxQueued ${safeLogValue(raw)} (outside [${MIN_QUERY_MAX_QUEUED}, ${MAX_QUERY_MAX_QUEUED}])`
15552
+ );
15553
+ res.status(400).json({
15554
+ error: `\`maxQueued\` must be in [${MIN_QUERY_MAX_QUEUED}, ${MAX_QUERY_MAX_QUEUED}]`,
15555
+ code: "invalid_max_queued"
15556
+ });
15557
+ return null;
15558
+ }
15559
+ return n;
15560
+ }
15561
+ __name(parseMaxQueuedQuery, "parseMaxQueuedQuery");
15562
+ function safeLogValue(raw) {
15563
+ return JSON.stringify(String(raw)).slice(0, 82);
15564
+ }
15565
+ __name(safeLogValue, "safeLogValue");
14751
15566
  function parseLastEventId(raw) {
14752
15567
  if (typeof raw !== "string" || !/^\d+$/.test(raw)) {
14753
15568
  if (typeof raw === "string" && raw.length > 0) {
14754
15569
  writeStderrLine(
14755
- `qwen serve: rejected Last-Event-ID "${raw.slice(0, 80)}" (not a decimal integer)`
15570
+ `qwen serve: rejected Last-Event-ID ${safeLogValue(raw)} (not a decimal integer)`
14756
15571
  );
14757
15572
  }
14758
15573
  return void 0;
@@ -14760,13 +15575,26 @@ function parseLastEventId(raw) {
14760
15575
  const n = Number.parseInt(raw, 10);
14761
15576
  if (!Number.isFinite(n) || n > Number.MAX_SAFE_INTEGER) {
14762
15577
  writeStderrLine(
14763
- `qwen serve: rejected Last-Event-ID "${raw.slice(0, 80)}" (exceeds Number.MAX_SAFE_INTEGER)`
15578
+ `qwen serve: rejected Last-Event-ID ${safeLogValue(raw)} (exceeds Number.MAX_SAFE_INTEGER)`
14764
15579
  );
14765
15580
  return void 0;
14766
15581
  }
14767
15582
  return n;
14768
15583
  }
14769
15584
  __name(parseLastEventId, "parseLastEventId");
15585
+ function sendPermissionVoteError(res, err, ctx) {
15586
+ if (err instanceof InvalidPermissionOptionError) {
15587
+ res.status(400).json({
15588
+ error: err.message,
15589
+ code: "invalid_option_id",
15590
+ requestId: err.requestId,
15591
+ optionId: err.optionId
15592
+ });
15593
+ return;
15594
+ }
15595
+ sendBridgeError(res, err, ctx);
15596
+ }
15597
+ __name(sendPermissionVoteError, "sendPermissionVoteError");
14770
15598
  function formatSseFrame(event) {
14771
15599
  const dataJson = JSON.stringify(event);
14772
15600
  const idLine = "id" in event && event.id !== void 0 ? `id: ${event.id}
@@ -14782,6 +15610,15 @@ function sendBridgeError(res, err, ctx) {
14782
15610
  res.status(404).json({ error: err.message, sessionId: err.sessionId });
14783
15611
  return;
14784
15612
  }
15613
+ if (err instanceof InvalidClientIdError) {
15614
+ res.status(400).json({
15615
+ error: err.message,
15616
+ code: "invalid_client_id",
15617
+ sessionId: err.sessionId,
15618
+ clientId: err.clientId
15619
+ });
15620
+ return;
15621
+ }
14785
15622
  if (err instanceof WorkspaceMismatchError) {
14786
15623
  writeStderrLine(
14787
15624
  `qwen serve: workspace_mismatch (POST /session): daemon bound to ${JSON.stringify(err.bound)}, rejected ${JSON.stringify(err.requested)}`
@@ -14794,6 +15631,21 @@ function sendBridgeError(res, err, ctx) {
14794
15631
  });
14795
15632
  return;
14796
15633
  }
15634
+ if (err instanceof InvalidSessionScopeError) {
15635
+ res.status(400).json({
15636
+ error: err.message,
15637
+ code: "invalid_session_scope"
15638
+ });
15639
+ return;
15640
+ }
15641
+ if (err instanceof InvalidSessionMetadataError) {
15642
+ res.status(400).json({
15643
+ error: err.message,
15644
+ code: "invalid_metadata",
15645
+ field: err.field
15646
+ });
15647
+ return;
15648
+ }
14797
15649
  if (err instanceof SessionLimitExceededError) {
14798
15650
  res.set("Retry-After", "5");
14799
15651
  res.status(503).json({
@@ -14803,6 +15655,17 @@ function sendBridgeError(res, err, ctx) {
14803
15655
  });
14804
15656
  return;
14805
15657
  }
15658
+ if (err instanceof RestoreInProgressError) {
15659
+ res.set("Retry-After", "5");
15660
+ res.status(409).json({
15661
+ error: err.message,
15662
+ code: "restore_in_progress",
15663
+ sessionId: err.sessionId,
15664
+ activeAction: err.activeAction,
15665
+ requestedAction: err.requestedAction
15666
+ });
15667
+ return;
15668
+ }
14806
15669
  const ctxParts = [
14807
15670
  ctx?.route,
14808
15671
  ctx?.sessionId ? `session=${ctx.sessionId}` : void 0
@@ -14870,6 +15733,11 @@ async function runQwenServe(optsIn, deps = {}) {
14870
15733
  `Refusing to bind ${opts.hostname}:${opts.port} without a bearer token. Set ${QWEN_SERVER_TOKEN_ENV} or pass --token, or rebind to loopback (127.0.0.1, localhost, ::1, or [::1]).`
14871
15734
  );
14872
15735
  }
15736
+ if (opts.requireAuth && !token) {
15737
+ throw new Error(
15738
+ `Refusing to start with --require-auth set but no bearer token configured. Set ${QWEN_SERVER_TOKEN_ENV} or pass --token, or omit --require-auth to keep the loopback developer default.`
15739
+ );
15740
+ }
14873
15741
  const rawWorkspace = opts.workspace ?? process.cwd();
14874
15742
  if (!path3.isAbsolute(rawWorkspace)) {
14875
15743
  throw new Error(
@@ -14902,6 +15770,7 @@ async function runQwenServe(optsIn, deps = {}) {
14902
15770
  const boundWorkspace = canonicalizeWorkspace(rawWorkspace);
14903
15771
  const bridge = deps.bridge ?? createHttpAcpBridge({
14904
15772
  maxSessions: opts.maxSessions,
15773
+ ...opts.eventRingSize !== void 0 ? { eventRingSize: opts.eventRingSize } : {},
14905
15774
  boundWorkspace
14906
15775
  });
14907
15776
  let actualPort = opts.port;
@@ -14950,6 +15819,10 @@ async function runQwenServe(optsIn, deps = {}) {
14950
15819
  writeStderrLine(
14951
15820
  `qwen serve: bearer auth disabled (loopback default). Set ${QWEN_SERVER_TOKEN_ENV} to enable.`
14952
15821
  );
15822
+ } else if (opts.requireAuth) {
15823
+ writeStderrLine(
15824
+ "qwen serve: --require-auth enabled (bearer token mandatory on every route, including loopback /health)."
15825
+ );
14953
15826
  }
14954
15827
  let shuttingDown = false;
14955
15828
  let closePromise;
@@ -15064,14 +15937,32 @@ function createInMemoryChannel() {
15064
15937
  __name(createInMemoryChannel, "createInMemoryChannel");
15065
15938
  export {
15066
15939
  CAPABILITIES_SCHEMA_VERSION,
15940
+ CONDITIONAL_SERVE_FEATURES,
15067
15941
  EVENT_SCHEMA_VERSION,
15068
15942
  EventBus,
15943
+ SERVE_CAPABILITY_REGISTRY,
15944
+ SERVE_FEATURES,
15945
+ SERVE_PROTOCOL_VERSION,
15946
+ SERVE_STATUS_EXT_METHODS,
15069
15947
  STAGE1_FEATURES,
15948
+ STATUS_SCHEMA_VERSION,
15949
+ SUPPORTED_SERVE_PROTOCOL_VERSIONS,
15070
15950
  SessionNotFoundError,
15951
+ bearerAuth,
15071
15952
  createHttpAcpBridge,
15953
+ createIdleWorkspaceMcpStatus,
15954
+ createIdleWorkspaceProvidersStatus,
15955
+ createIdleWorkspaceSkillsStatus,
15072
15956
  createInMemoryChannel,
15957
+ createMutationGate,
15073
15958
  createServeApp,
15074
15959
  defaultSpawnChannelFactory,
15960
+ denyBrowserOriginCors,
15961
+ getAdvertisedServeFeatures,
15962
+ getRegisteredServeFeatures,
15963
+ getServeFeatures,
15964
+ getServeProtocolVersions,
15965
+ hostAllowlist,
15075
15966
  runQwenServe
15076
15967
  };
15077
15968
  /**