@qwen-code/qwen-code 0.15.12-preview.2 → 0.15.12-preview.3

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 (50) hide show
  1. package/bundled/qc-helper/docs/configuration/settings.md +10 -10
  2. package/bundled/qc-helper/docs/features/lsp.md +87 -10
  3. package/bundled/qc-helper/docs/qwen-serve.md +46 -15
  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-LIAWUWAO.js} +3 -3
  7. package/chunks/{ca-VQSV6JHA.js → ca-S3XJMT6P.js} +26 -0
  8. package/chunks/{chunk-SOIEFHIK.js → chunk-5QQ5FGTU.js} +0 -99
  9. package/chunks/{chunk-2WFU3IUH.js → chunk-AJSOD5IR.js} +4417 -2250
  10. package/chunks/{chunk-MXBWOU2L.js → chunk-AOJ3BBY7.js} +12 -12
  11. package/chunks/{chunk-FSYKVGER.js → chunk-GC5RXNL2.js} +1 -1
  12. package/chunks/{chunk-PCL3EJGY.js → chunk-XLQ4E5PS.js} +3918 -3677
  13. package/chunks/{contextCommand-MQRG6RMG.js → contextCommand-SVLAZMQL.js} +4 -4
  14. package/chunks/{de-M2IPQRBS.js → de-MNR4SMAI.js} +26 -0
  15. package/chunks/{edit-3KCBTA25.js → edit-VNAZBIZR.js} +7 -3
  16. package/chunks/{en-N5GMPCVT.js → en-NRN4QBAT.js} +27 -0
  17. package/chunks/{enter-worktree-VWS5QZTU.js → enter-worktree-FOF5YZIV.js} +3 -3
  18. package/chunks/{exit-worktree-RVXFWAPD.js → exit-worktree-Y6QVAO3C.js} +3 -3
  19. package/chunks/{exitPlanMode-UL5DILDG.js → exitPlanMode-QZKO7GH7.js} +3 -3
  20. package/chunks/{fr-BTHRYEXO.js → fr-OFJFHLCR.js} +26 -0
  21. package/chunks/{geminiContentGenerator-O2OPGHJG.js → geminiContentGenerator-DYHZPKJX.js} +1 -1
  22. package/chunks/{glob-57BSREPN.js → glob-G7XATELV.js} +3 -3
  23. package/chunks/{grep-XO5JOC7T.js → grep-4SETMY47.js} +3 -3
  24. package/chunks/{ja-D63TAEBO.js → ja-V6OQ6VL7.js} +26 -0
  25. package/chunks/{monitor-BECPGO3K.js → monitor-JTLJBJ7H.js} +21 -12
  26. package/chunks/{openaiContentGenerator-KEZQHIRM.js → openaiContentGenerator-3H7XOZBW.js} +2 -2
  27. package/chunks/{pt-XUV7FSKC.js → pt-ZLE6SA4A.js} +26 -0
  28. package/chunks/{qwenContentGenerator-RPMRXTNH.js → qwenContentGenerator-FAU3QPYO.js} +4 -4
  29. package/chunks/{read-file-LGHEIQNH.js → read-file-WWUQVNCZ.js} +1 -1
  30. package/chunks/{ripGrep-6SFSXZ2G.js → ripGrep-WCOAIWL6.js} +3 -3
  31. package/chunks/{ru-7KHWMN3A.js → ru-A4OHIUNN.js} +26 -0
  32. package/chunks/{serve-27O2AFE3.js → serve-VJEEEXA6.js} +780 -104
  33. package/chunks/{shell-J7K5KYCH.js → shell-IAOKGIJ6.js} +3 -3
  34. package/chunks/{skill-2R7P4ATS.js → skill-NHW6222K.js} +2 -2
  35. package/chunks/{src-CGEDVW67.js → src-OWV5HVQQ.js} +82 -12
  36. package/chunks/{tool-search-XOH3ZWVS.js → tool-search-MSJ6SXLI.js} +1 -1
  37. package/chunks/{write-file-74NQ27Q2.js → write-file-RKCENFZ5.js} +7 -3
  38. package/chunks/{zh-VGHU6XBB.js → zh-RN3JULHO.js} +27 -0
  39. package/chunks/{zh-TW-O36Q4V7E.js → zh-TW-XZEHEV5S.js} +27 -0
  40. package/cli.js +7440 -5056
  41. package/locales/ca.js +40 -0
  42. package/locales/de.js +40 -0
  43. package/locales/en.js +41 -0
  44. package/locales/fr.js +41 -0
  45. package/locales/ja.js +39 -0
  46. package/locales/pt.js +39 -0
  47. package/locales/ru.js +39 -0
  48. package/locales/zh-TW.js +40 -0
  49. package/locales/zh.js +40 -0
  50. package/package.json +2 -2
@@ -13356,6 +13356,36 @@ var SessionNotFoundError = class extends Error {
13356
13356
  this.sessionId = sessionId;
13357
13357
  }
13358
13358
  };
13359
+ var RestoreInProgressError = class extends Error {
13360
+ static {
13361
+ __name(this, "RestoreInProgressError");
13362
+ }
13363
+ sessionId;
13364
+ activeAction;
13365
+ requestedAction;
13366
+ constructor(sessionId, activeAction, requestedAction) {
13367
+ super(
13368
+ `Session "${sessionId}" is already being restored via session/${activeAction}; retry session/${requestedAction} after it completes`
13369
+ );
13370
+ this.name = "RestoreInProgressError";
13371
+ this.sessionId = sessionId;
13372
+ this.activeAction = activeAction;
13373
+ this.requestedAction = requestedAction;
13374
+ }
13375
+ };
13376
+ var InvalidSessionScopeError = class extends Error {
13377
+ static {
13378
+ __name(this, "InvalidSessionScopeError");
13379
+ }
13380
+ sessionScope;
13381
+ constructor(sessionScope) {
13382
+ super(
13383
+ `Invalid sessionScope: ${JSON.stringify(sessionScope)}. Expected 'single' or 'thread'.`
13384
+ );
13385
+ this.name = "InvalidSessionScopeError";
13386
+ this.sessionScope = sessionScope;
13387
+ }
13388
+ };
13359
13389
  var SessionLimitExceededError = class extends Error {
13360
13390
  static {
13361
13391
  __name(this, "SessionLimitExceededError");
@@ -13383,7 +13413,32 @@ var WorkspaceMismatchError = class extends Error {
13383
13413
  this.requested = safeRequested;
13384
13414
  }
13385
13415
  };
13416
+ var InvalidClientIdError = class extends Error {
13417
+ static {
13418
+ __name(this, "InvalidClientIdError");
13419
+ }
13420
+ sessionId;
13421
+ clientId;
13422
+ constructor(sessionId, clientId) {
13423
+ super(`Client id "${clientId}" is not registered for session ${sessionId}`);
13424
+ this.name = "InvalidClientIdError";
13425
+ this.sessionId = sessionId;
13426
+ this.clientId = clientId;
13427
+ }
13428
+ };
13386
13429
  var MAX_WORKSPACE_PATH_LENGTH = 4096;
13430
+ var MAX_RESOLVED_PERMISSION_RECORDS = 512;
13431
+ function isServeDebugLoggingEnabled() {
13432
+ const value = process.env["QWEN_SERVE_DEBUG"];
13433
+ if (!value) return false;
13434
+ return !["0", "false", "off", "no"].includes(value.trim().toLowerCase());
13435
+ }
13436
+ __name(isServeDebugLoggingEnabled, "isServeDebugLoggingEnabled");
13437
+ function writeServeDebugLine(message) {
13438
+ if (!isServeDebugLoggingEnabled()) return;
13439
+ writeStderrLine(`qwen serve debug: ${message}`);
13440
+ }
13441
+ __name(writeServeDebugLine, "writeServeDebugLine");
13387
13442
  var InvalidPermissionOptionError = class extends Error {
13388
13443
  static {
13389
13444
  __name(this, "InvalidPermissionOptionError");
@@ -13400,8 +13455,9 @@ var InvalidPermissionOptionError = class extends Error {
13400
13455
  }
13401
13456
  };
13402
13457
  var BridgeClient = class {
13403
- constructor(resolveEntry, registerPending, rollbackPending, permissionTimeoutMs, maxPendingPerSession) {
13458
+ constructor(resolveEntry, resolvePendingRestoreEvents, registerPending, rollbackPending, permissionTimeoutMs, maxPendingPerSession) {
13404
13459
  this.resolveEntry = resolveEntry;
13460
+ this.resolvePendingRestoreEvents = resolvePendingRestoreEvents;
13405
13461
  this.registerPending = registerPending;
13406
13462
  this.rollbackPending = rollbackPending;
13407
13463
  this.permissionTimeoutMs = permissionTimeoutMs;
@@ -13459,7 +13515,8 @@ var BridgeClient = class {
13459
13515
  sessionId: entry.sessionId,
13460
13516
  toolCall: params.toolCall,
13461
13517
  options: params.options
13462
- }
13518
+ },
13519
+ ...entry.activePromptOriginatorClientId ? { originatorClientId: entry.activePromptOriginatorClientId } : {}
13463
13520
  });
13464
13521
  if (!published) {
13465
13522
  this.rollbackPending(requestId);
@@ -13481,8 +13538,13 @@ var BridgeClient = class {
13481
13538
  }
13482
13539
  async sessionUpdate(params) {
13483
13540
  const entry = this.resolveEntry(params.sessionId);
13484
- if (!entry) return;
13485
- entry.events.publish({ type: "session_update", data: params });
13541
+ const events = entry?.events ?? this.resolvePendingRestoreEvents(params.sessionId);
13542
+ if (!events) return;
13543
+ events.publish({
13544
+ type: "session_update",
13545
+ data: params,
13546
+ ...entry?.activePromptOriginatorClientId ? { originatorClientId: entry.activePromptOriginatorClientId } : {}
13547
+ });
13486
13548
  }
13487
13549
  async writeTextFile(params) {
13488
13550
  let realTarget = params.path;
@@ -13561,7 +13623,7 @@ var DEFAULT_MAX_SESSIONS = 20;
13561
13623
  var DEFAULT_PERMISSION_TIMEOUT_MS = 5 * 60 * 1e3;
13562
13624
  var DEFAULT_MAX_PENDING_PER_SESSION = 64;
13563
13625
  function createHttpAcpBridge(opts) {
13564
- const sessionScope = opts.sessionScope ?? "single";
13626
+ const defaultSessionScope = opts.sessionScope ?? "single";
13565
13627
  let maxSessions;
13566
13628
  if (opts.maxSessions === void 0) {
13567
13629
  maxSessions = DEFAULT_MAX_SESSIONS;
@@ -13578,9 +13640,9 @@ function createHttpAcpBridge(opts) {
13578
13640
  } else {
13579
13641
  maxSessions = opts.maxSessions;
13580
13642
  }
13581
- if (sessionScope !== "single" && sessionScope !== "thread") {
13643
+ if (defaultSessionScope !== "single" && defaultSessionScope !== "thread") {
13582
13644
  throw new TypeError(
13583
- `Invalid sessionScope: ${JSON.stringify(sessionScope)}. Expected 'single' or 'thread'.`
13645
+ `Invalid sessionScope: ${JSON.stringify(defaultSessionScope)}. Expected 'single' or 'thread'.`
13584
13646
  );
13585
13647
  }
13586
13648
  const channelFactory = opts.channelFactory ?? defaultSpawnChannelFactory;
@@ -13606,8 +13668,48 @@ function createHttpAcpBridge(opts) {
13606
13668
  let inFlightChannelSpawn;
13607
13669
  const byId = /* @__PURE__ */ new Map();
13608
13670
  const pendingPermissions = /* @__PURE__ */ new Map();
13671
+ const resolvedPermissions = /* @__PURE__ */ new Map();
13672
+ const resolvedPermissionOrder = [];
13609
13673
  let shuttingDown = false;
13610
13674
  const inFlightSpawns = /* @__PURE__ */ new Map();
13675
+ const inFlightRestores = /* @__PURE__ */ new Map();
13676
+ const pendingRestoreEvents = /* @__PURE__ */ new Map();
13677
+ const createClientId = /* @__PURE__ */ __name(() => `client_${randomUUID()}`, "createClientId");
13678
+ const registerClient = /* @__PURE__ */ __name((entry, requestedClientId) => {
13679
+ if (requestedClientId && entry.clientIds.has(requestedClientId)) {
13680
+ entry.clientIds.set(
13681
+ requestedClientId,
13682
+ (entry.clientIds.get(requestedClientId) ?? 0) + 1
13683
+ );
13684
+ return requestedClientId;
13685
+ }
13686
+ const clientId = createClientId();
13687
+ entry.clientIds.set(clientId, 1);
13688
+ return clientId;
13689
+ }, "registerClient");
13690
+ const unregisterClient = /* @__PURE__ */ __name((entry, clientId) => {
13691
+ if (clientId === void 0) return;
13692
+ const count = entry.clientIds.get(clientId);
13693
+ if (count === void 0) return;
13694
+ if (count <= 1) {
13695
+ entry.clientIds.delete(clientId);
13696
+ } else {
13697
+ entry.clientIds.set(clientId, count - 1);
13698
+ }
13699
+ }, "unregisterClient");
13700
+ const resolveTrustedClientId = /* @__PURE__ */ __name((entry, clientId) => {
13701
+ if (clientId === void 0) return void 0;
13702
+ if (!entry.clientIds.has(clientId)) {
13703
+ throw new InvalidClientIdError(entry.sessionId, clientId);
13704
+ }
13705
+ return clientId;
13706
+ }, "resolveTrustedClientId");
13707
+ const resolveAnyTrustedClientId = /* @__PURE__ */ __name((clientId) => {
13708
+ for (const entry of byId.values()) {
13709
+ if (entry.clientIds.has(clientId)) return clientId;
13710
+ }
13711
+ throw new InvalidClientIdError("unknown", clientId);
13712
+ }, "resolveAnyTrustedClientId");
13611
13713
  const registerPending = /* @__PURE__ */ __name((p) => {
13612
13714
  const entry = byId.get(p.sessionId);
13613
13715
  if (!entry) {
@@ -13617,7 +13719,38 @@ function createHttpAcpBridge(opts) {
13617
13719
  pendingPermissions.set(p.requestId, p);
13618
13720
  entry.pendingPermissionIds.add(p.requestId);
13619
13721
  }, "registerPending");
13620
- const resolvePending = /* @__PURE__ */ __name((requestId, response) => {
13722
+ const rememberResolvedPermission = /* @__PURE__ */ __name((record) => {
13723
+ if (!resolvedPermissions.has(record.requestId)) {
13724
+ resolvedPermissionOrder.push(record.requestId);
13725
+ }
13726
+ resolvedPermissions.set(record.requestId, record);
13727
+ while (resolvedPermissionOrder.length > MAX_RESOLVED_PERMISSION_RECORDS) {
13728
+ const oldest = resolvedPermissionOrder.shift();
13729
+ if (oldest !== void 0) resolvedPermissions.delete(oldest);
13730
+ }
13731
+ }, "rememberResolvedPermission");
13732
+ const publishPermissionAlreadyResolved = /* @__PURE__ */ __name((record) => {
13733
+ const entry = byId.get(record.sessionId);
13734
+ if (!entry) return;
13735
+ try {
13736
+ writeServeDebugLine(
13737
+ `permission ${JSON.stringify(record.requestId)} for session ${JSON.stringify(record.sessionId)} was already resolved; publishing duplicate-vote notification.`
13738
+ );
13739
+ entry.events.publish({
13740
+ type: "permission_already_resolved",
13741
+ data: {
13742
+ requestId: record.requestId,
13743
+ sessionId: record.sessionId,
13744
+ outcome: record.outcome
13745
+ }
13746
+ });
13747
+ } catch {
13748
+ writeServeDebugLine(
13749
+ `skipped duplicate-vote notification for permission ${JSON.stringify(record.requestId)} during shutdown.`
13750
+ );
13751
+ }
13752
+ }, "publishPermissionAlreadyResolved");
13753
+ const resolvePending = /* @__PURE__ */ __name((requestId, response, originatorClientId) => {
13621
13754
  const pending = pendingPermissions.get(requestId);
13622
13755
  if (!pending) return false;
13623
13756
  pendingPermissions.delete(requestId);
@@ -13627,11 +13760,17 @@ function createHttpAcpBridge(opts) {
13627
13760
  try {
13628
13761
  entry.events.publish({
13629
13762
  type: "permission_resolved",
13630
- data: { requestId, outcome: response.outcome }
13763
+ data: { requestId, outcome: response.outcome },
13764
+ ...originatorClientId ? { originatorClientId } : {}
13631
13765
  });
13632
13766
  } catch {
13633
13767
  }
13634
13768
  }
13769
+ rememberResolvedPermission({
13770
+ requestId,
13771
+ sessionId: pending.sessionId,
13772
+ outcome: response.outcome
13773
+ });
13635
13774
  pending.resolve(response);
13636
13775
  return true;
13637
13776
  }, "resolvePending");
@@ -13656,6 +13795,7 @@ function createHttpAcpBridge(opts) {
13656
13795
  }
13657
13796
  return void 0;
13658
13797
  },
13798
+ (sessionId) => sessionId ? pendingRestoreEvents.get(sessionId) : void 0,
13659
13799
  registerPending,
13660
13800
  (rid) => (
13661
13801
  // Roll back a register-then-publish-failed pending so the agent
@@ -13671,6 +13811,7 @@ function createHttpAcpBridge(opts) {
13671
13811
  connection,
13672
13812
  client,
13673
13813
  sessionIds: /* @__PURE__ */ new Set(),
13814
+ pendingRestoreIds: /* @__PURE__ */ new Set(),
13674
13815
  isDying: false
13675
13816
  };
13676
13817
  aliveChannels.add(info);
@@ -13746,7 +13887,7 @@ function createHttpAcpBridge(opts) {
13746
13887
  }
13747
13888
  }
13748
13889
  __name(ensureChannel, "ensureChannel");
13749
- async function doSpawn(modelServiceId) {
13890
+ async function doSpawn(modelServiceId, effectiveScope, requestedClientId) {
13750
13891
  const ci = await ensureChannel();
13751
13892
  let newSessionResp;
13752
13893
  try {
@@ -13769,26 +13910,21 @@ function createHttpAcpBridge(opts) {
13769
13910
  if (shuttingDown) {
13770
13911
  throw new Error("HttpAcpBridge is shutting down");
13771
13912
  }
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;
13913
+ const entry = createSessionEntry(
13914
+ ci,
13915
+ newSessionResp.sessionId,
13916
+ boundWorkspace
13917
+ );
13918
+ const clientId = registerClient(entry, requestedClientId);
13919
+ if (effectiveScope === "single" && !defaultEntry) defaultEntry = entry;
13787
13920
  if (modelServiceId) {
13788
- await applyModelServiceId(entry, modelServiceId, initTimeoutMs).catch(
13789
- () => {
13790
- }
13791
- );
13921
+ await applyModelServiceId(
13922
+ entry,
13923
+ modelServiceId,
13924
+ initTimeoutMs,
13925
+ clientId
13926
+ ).catch(() => {
13927
+ });
13792
13928
  }
13793
13929
  if (!byId.has(entry.sessionId)) {
13794
13930
  throw new Error(
@@ -13798,11 +13934,12 @@ function createHttpAcpBridge(opts) {
13798
13934
  return {
13799
13935
  sessionId: entry.sessionId,
13800
13936
  workspaceCwd: entry.workspaceCwd,
13801
- attached: false
13937
+ attached: false,
13938
+ clientId
13802
13939
  };
13803
13940
  }
13804
13941
  __name(doSpawn, "doSpawn");
13805
- async function applyModelServiceId(entry, modelId, timeoutMs) {
13942
+ async function applyModelServiceId(entry, modelId, timeoutMs, originatorClientId) {
13806
13943
  const conn = entry.connection;
13807
13944
  const transportClosed = getTransportClosedReject(entry);
13808
13945
  const work = entry.modelChangeQueue.then(async () => {
@@ -13820,7 +13957,8 @@ function createHttpAcpBridge(opts) {
13820
13957
  ]);
13821
13958
  entry.events.publish({
13822
13959
  type: "model_switched",
13823
- data: { sessionId: entry.sessionId, modelId }
13960
+ data: { sessionId: entry.sessionId, modelId },
13961
+ ...originatorClientId ? { originatorClientId } : {}
13824
13962
  });
13825
13963
  } catch (err) {
13826
13964
  entry.events.publish({
@@ -13829,7 +13967,8 @@ function createHttpAcpBridge(opts) {
13829
13967
  sessionId: entry.sessionId,
13830
13968
  requestedModelId: modelId,
13831
13969
  error: err instanceof Error ? err.message : String(err)
13832
- }
13970
+ },
13971
+ ...originatorClientId ? { originatorClientId } : {}
13833
13972
  });
13834
13973
  throw err;
13835
13974
  }
@@ -13859,6 +13998,213 @@ function createHttpAcpBridge(opts) {
13859
13998
  }
13860
13999
  return entry.transportClosedReject;
13861
14000
  }, "getTransportClosedReject");
14001
+ const resolveWorkspaceKey = /* @__PURE__ */ __name((workspaceCwd) => {
14002
+ if (!path.isAbsolute(workspaceCwd)) {
14003
+ throw new Error(
14004
+ `workspaceCwd must be an absolute path; got "${workspaceCwd}"`
14005
+ );
14006
+ }
14007
+ const workspaceKey = workspaceCwd === boundWorkspace ? boundWorkspace : canonicalizeWorkspace(workspaceCwd);
14008
+ if (workspaceKey !== boundWorkspace) {
14009
+ throw new WorkspaceMismatchError(boundWorkspace, workspaceKey);
14010
+ }
14011
+ return workspaceKey;
14012
+ }, "resolveWorkspaceKey");
14013
+ const createSessionEntry = /* @__PURE__ */ __name((ci, sessionId, workspaceCwd, events = new EventBus()) => {
14014
+ const entry = {
14015
+ sessionId,
14016
+ workspaceCwd,
14017
+ channel: ci.channel,
14018
+ connection: ci.connection,
14019
+ events,
14020
+ promptQueue: Promise.resolve(),
14021
+ modelChangeQueue: Promise.resolve(),
14022
+ pendingPermissionIds: /* @__PURE__ */ new Set(),
14023
+ clientIds: /* @__PURE__ */ new Map(),
14024
+ attachCount: 0,
14025
+ spawnOwnerWantedKill: false
14026
+ };
14027
+ ci.sessionIds.add(entry.sessionId);
14028
+ byId.set(entry.sessionId, entry);
14029
+ return entry;
14030
+ }, "createSessionEntry");
14031
+ const isAcpSessionResourceNotFound = /* @__PURE__ */ __name((err, sessionId) => {
14032
+ if (!err || typeof err !== "object") return false;
14033
+ const maybe = err;
14034
+ if (maybe.code !== -32002) return false;
14035
+ const expectedUri = `session:${sessionId}`;
14036
+ if (maybe.data && typeof maybe.data === "object" && maybe.data.uri === expectedUri) {
14037
+ return true;
14038
+ }
14039
+ return typeof maybe.message === "string" && maybe.message === `Resource not found: ${expectedUri}`;
14040
+ }, "isAcpSessionResourceNotFound");
14041
+ async function restoreSession(action, req) {
14042
+ if (shuttingDown) {
14043
+ throw new Error("HttpAcpBridge is shutting down");
14044
+ }
14045
+ const workspaceKey = resolveWorkspaceKey(req.workspaceCwd);
14046
+ const existing = byId.get(req.sessionId);
14047
+ if (existing) {
14048
+ existing.attachCount++;
14049
+ const clientId = registerClient(existing, req.clientId);
14050
+ return {
14051
+ sessionId: existing.sessionId,
14052
+ workspaceCwd: existing.workspaceCwd,
14053
+ attached: true,
14054
+ clientId,
14055
+ // Late attachers get the same ACP state the original restore
14056
+ // caller saw; spawn-only sessions don't carry a state payload.
14057
+ state: existing.restoreState ?? {}
14058
+ };
14059
+ }
14060
+ const inFlight = inFlightRestores.get(req.sessionId);
14061
+ if (inFlight) {
14062
+ if (action !== inFlight.action) {
14063
+ throw new RestoreInProgressError(
14064
+ req.sessionId,
14065
+ inFlight.action,
14066
+ action
14067
+ );
14068
+ }
14069
+ inFlight.coalesceState.count++;
14070
+ let restored;
14071
+ try {
14072
+ restored = await inFlight.promise;
14073
+ } catch (err) {
14074
+ inFlight.coalesceState.count--;
14075
+ throw err;
14076
+ }
14077
+ const entry = byId.get(restored.sessionId);
14078
+ if (!entry) {
14079
+ inFlight.coalesceState.count--;
14080
+ throw new SessionNotFoundError(
14081
+ restored.sessionId,
14082
+ "the agent child likely crashed during session restore \u2014 retry to restore the session"
14083
+ );
14084
+ }
14085
+ return {
14086
+ ...restored,
14087
+ attached: true,
14088
+ clientId: registerClient(entry, req.clientId)
14089
+ };
14090
+ }
14091
+ if (byId.size + inFlightSpawns.size + inFlightRestores.size >= maxSessions) {
14092
+ throw new SessionLimitExceededError(maxSessions);
14093
+ }
14094
+ const restoreEvents = new EventBus();
14095
+ let registeredEntry;
14096
+ let ci;
14097
+ const coalesceState = { count: 0 };
14098
+ const promise = (async () => {
14099
+ pendingRestoreEvents.set(req.sessionId, restoreEvents);
14100
+ ci = await ensureChannel();
14101
+ ci.pendingRestoreIds.add(req.sessionId);
14102
+ const transportClosed = ci.channel.exited.then(() => {
14103
+ throw new Error(`agent channel closed during session/${action}`);
14104
+ });
14105
+ transportClosed.catch(() => {
14106
+ });
14107
+ let state;
14108
+ try {
14109
+ if (action === "load") {
14110
+ state = await Promise.race([
14111
+ withTimeout(
14112
+ ci.connection.loadSession({
14113
+ sessionId: req.sessionId,
14114
+ cwd: workspaceKey,
14115
+ // Restore path drops per-request `mcpServers` (matches
14116
+ // `doSpawn`); daemon-wide MCP comes from settings on
14117
+ // the agent side. The SDK's `RestoreSessionRequest`
14118
+ // intentionally has no `mcpServers` field for the
14119
+ // same reason.
14120
+ mcpServers: []
14121
+ }),
14122
+ initTimeoutMs,
14123
+ "loadSession"
14124
+ ),
14125
+ transportClosed
14126
+ ]);
14127
+ } else {
14128
+ state = await Promise.race([
14129
+ withTimeout(
14130
+ ci.connection.unstable_resumeSession({
14131
+ sessionId: req.sessionId,
14132
+ cwd: workspaceKey,
14133
+ mcpServers: []
14134
+ }),
14135
+ initTimeoutMs,
14136
+ "resumeSession"
14137
+ ),
14138
+ transportClosed
14139
+ ]);
14140
+ }
14141
+ } catch (err) {
14142
+ restoreEvents.close();
14143
+ if (isAcpSessionResourceNotFound(err, req.sessionId)) {
14144
+ throw new SessionNotFoundError(req.sessionId);
14145
+ }
14146
+ if (ci.sessionIds.size === 0 && ci.pendingRestoreIds.size === 1 && ci.pendingRestoreIds.has(req.sessionId)) {
14147
+ ci.isDying = true;
14148
+ await ci.channel.kill().catch(() => {
14149
+ });
14150
+ }
14151
+ throw err;
14152
+ }
14153
+ if (shuttingDown) {
14154
+ restoreEvents.close();
14155
+ throw new Error("HttpAcpBridge is shutting down");
14156
+ }
14157
+ if (ci.isDying || !aliveChannels.has(ci)) {
14158
+ restoreEvents.close();
14159
+ throw new Error(
14160
+ `Session ${req.sessionId} restored on a closed agent channel`
14161
+ );
14162
+ }
14163
+ const racedEntry = byId.get(req.sessionId);
14164
+ if (racedEntry) {
14165
+ restoreEvents.close();
14166
+ racedEntry.attachCount += 1 + coalesceState.count;
14167
+ const clientId2 = registerClient(racedEntry, req.clientId);
14168
+ return {
14169
+ sessionId: racedEntry.sessionId,
14170
+ workspaceCwd: racedEntry.workspaceCwd,
14171
+ attached: true,
14172
+ clientId: clientId2,
14173
+ state: racedEntry.restoreState ?? {}
14174
+ };
14175
+ }
14176
+ const entry = createSessionEntry(
14177
+ ci,
14178
+ req.sessionId,
14179
+ workspaceKey,
14180
+ restoreEvents
14181
+ );
14182
+ entry.restoreState = state;
14183
+ const clientId = registerClient(entry, req.clientId);
14184
+ entry.attachCount = coalesceState.count;
14185
+ registeredEntry = entry;
14186
+ return {
14187
+ sessionId: entry.sessionId,
14188
+ workspaceCwd: entry.workspaceCwd,
14189
+ attached: false,
14190
+ clientId,
14191
+ state
14192
+ };
14193
+ })().finally(() => {
14194
+ ci?.pendingRestoreIds.delete(req.sessionId);
14195
+ pendingRestoreEvents.delete(req.sessionId);
14196
+ if (!registeredEntry) {
14197
+ restoreEvents.close();
14198
+ }
14199
+ });
14200
+ inFlightRestores.set(req.sessionId, { action, promise, coalesceState });
14201
+ try {
14202
+ return await promise;
14203
+ } finally {
14204
+ inFlightRestores.delete(req.sessionId);
14205
+ }
14206
+ }
14207
+ __name(restoreSession, "restoreSession");
13862
14208
  return {
13863
14209
  get sessionCount() {
13864
14210
  return byId.size;
@@ -13866,35 +14212,40 @@ function createHttpAcpBridge(opts) {
13866
14212
  get pendingPermissionCount() {
13867
14213
  return pendingPermissions.size;
13868
14214
  },
14215
+ async loadSession(req) {
14216
+ return restoreSession("load", req);
14217
+ },
14218
+ async resumeSession(req) {
14219
+ return restoreSession("resume", req);
14220
+ },
13869
14221
  async spawnOrAttach(req) {
13870
14222
  if (shuttingDown) {
13871
14223
  throw new Error("HttpAcpBridge is shutting down");
13872
14224
  }
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);
14225
+ const workspaceKey = resolveWorkspaceKey(req.workspaceCwd);
14226
+ if (req.sessionScope !== void 0 && req.sessionScope !== "single" && req.sessionScope !== "thread") {
14227
+ throw new InvalidSessionScopeError(req.sessionScope);
13881
14228
  }
13882
- if (sessionScope === "single") {
14229
+ const effectiveScope = req.sessionScope ?? defaultSessionScope;
14230
+ if (effectiveScope === "single") {
13883
14231
  const existing = defaultEntry;
13884
14232
  if (existing) {
13885
14233
  existing.attachCount++;
14234
+ const clientId = registerClient(existing, req.clientId);
13886
14235
  if (req.modelServiceId) {
13887
14236
  await applyModelServiceId(
13888
14237
  existing,
13889
14238
  req.modelServiceId,
13890
- initTimeoutMs
14239
+ initTimeoutMs,
14240
+ clientId
13891
14241
  ).catch(() => {
13892
14242
  });
13893
14243
  }
13894
14244
  return {
13895
14245
  sessionId: existing.sessionId,
13896
14246
  workspaceCwd: existing.workspaceCwd,
13897
- attached: true
14247
+ attached: true,
14248
+ clientId
13898
14249
  };
13899
14250
  }
13900
14251
  const inFlight = inFlightSpawns.get(workspaceKey);
@@ -13908,22 +14259,24 @@ function createHttpAcpBridge(opts) {
13908
14259
  "the agent child likely crashed during initialization \u2014 retry to spawn a new session"
13909
14260
  );
13910
14261
  }
14262
+ const clientId = registerClient(attachedEntry, req.clientId);
13911
14263
  if (req.modelServiceId) {
13912
14264
  await applyModelServiceId(
13913
14265
  attachedEntry,
13914
14266
  req.modelServiceId,
13915
- initTimeoutMs
14267
+ initTimeoutMs,
14268
+ clientId
13916
14269
  ).catch(() => {
13917
14270
  });
13918
14271
  }
13919
- return { ...session, attached: true };
14272
+ return { ...session, attached: true, clientId };
13920
14273
  }
13921
14274
  }
13922
- if (byId.size + inFlightSpawns.size >= maxSessions) {
14275
+ if (byId.size + inFlightSpawns.size + inFlightRestores.size >= maxSessions) {
13923
14276
  throw new SessionLimitExceededError(maxSessions);
13924
14277
  }
13925
- const promise = doSpawn(req.modelServiceId);
13926
- const tracker = sessionScope === "single" ? workspaceKey : `${workspaceKey}#${randomUUID()}`;
14278
+ const promise = doSpawn(req.modelServiceId, effectiveScope, req.clientId);
14279
+ const tracker = effectiveScope === "single" ? workspaceKey : `${workspaceKey}#${randomUUID()}`;
13927
14280
  inFlightSpawns.set(tracker, promise);
13928
14281
  try {
13929
14282
  return await promise;
@@ -13931,9 +14284,13 @@ function createHttpAcpBridge(opts) {
13931
14284
  inFlightSpawns.delete(tracker);
13932
14285
  }
13933
14286
  },
13934
- async sendPrompt(sessionId, req, signal) {
14287
+ async sendPrompt(sessionId, req, signal, context) {
13935
14288
  const entry = byId.get(sessionId);
13936
14289
  if (!entry) throw new SessionNotFoundError(sessionId);
14290
+ const originatorClientId = resolveTrustedClientId(
14291
+ entry,
14292
+ context?.clientId
14293
+ );
13937
14294
  if (signal?.aborted) {
13938
14295
  throw new DOMException("Prompt aborted", "AbortError");
13939
14296
  }
@@ -13942,7 +14299,14 @@ function createHttpAcpBridge(opts) {
13942
14299
  if (signal?.aborted) {
13943
14300
  throw new DOMException("Prompt aborted", "AbortError");
13944
14301
  }
13945
- const promptPromise = entry.connection.prompt(normalized);
14302
+ if (originatorClientId === void 0) {
14303
+ delete entry.activePromptOriginatorClientId;
14304
+ } else {
14305
+ entry.activePromptOriginatorClientId = originatorClientId;
14306
+ }
14307
+ const promptPromise = entry.connection.prompt(normalized).finally(() => {
14308
+ delete entry.activePromptOriginatorClientId;
14309
+ });
13946
14310
  const racedPromise = Promise.race([
13947
14311
  promptPromise,
13948
14312
  getTransportClosedReject(entry)
@@ -13969,9 +14333,10 @@ function createHttpAcpBridge(opts) {
13969
14333
  );
13970
14334
  return result;
13971
14335
  },
13972
- async cancelSession(sessionId, req) {
14336
+ async cancelSession(sessionId, req, context) {
13973
14337
  const entry = byId.get(sessionId);
13974
14338
  if (!entry) throw new SessionNotFoundError(sessionId);
14339
+ resolveTrustedClientId(entry, context?.clientId);
13975
14340
  cancelPendingForSession(sessionId);
13976
14341
  const notif = req ? { ...req, sessionId } : { sessionId };
13977
14342
  await entry.connection.cancel(notif);
@@ -13981,17 +14346,69 @@ function createHttpAcpBridge(opts) {
13981
14346
  if (!entry) throw new SessionNotFoundError(sessionId);
13982
14347
  return entry.events.subscribe(subOpts);
13983
14348
  },
13984
- respondToPermission(requestId, response) {
14349
+ respondToPermission(requestId, response, context) {
14350
+ const pending = pendingPermissions.get(requestId);
14351
+ let originatorClientId;
14352
+ if (context?.clientId !== void 0 && !pending) {
14353
+ resolveAnyTrustedClientId(context.clientId);
14354
+ } else if (pending && context?.clientId !== void 0) {
14355
+ const entry = byId.get(pending.sessionId);
14356
+ if (entry) {
14357
+ originatorClientId = resolveTrustedClientId(entry, context.clientId);
14358
+ } else {
14359
+ resolveAnyTrustedClientId(context.clientId);
14360
+ }
14361
+ }
14362
+ if (!pending) {
14363
+ const record = resolvedPermissions.get(requestId);
14364
+ if (record) {
14365
+ publishPermissionAlreadyResolved(record);
14366
+ }
14367
+ return false;
14368
+ }
13985
14369
  if (response.outcome.outcome === "selected") {
13986
- const pending = pendingPermissions.get(requestId);
13987
- if (pending && !pending.allowedOptionIds.has(response.outcome.optionId)) {
14370
+ if (!pending.allowedOptionIds.has(response.outcome.optionId)) {
13988
14371
  throw new InvalidPermissionOptionError(
13989
14372
  requestId,
13990
14373
  response.outcome.optionId
13991
14374
  );
13992
14375
  }
13993
14376
  }
13994
- return resolvePending(requestId, response);
14377
+ return resolvePending(requestId, response, originatorClientId);
14378
+ },
14379
+ respondToSessionPermission(sessionId, requestId, response, context) {
14380
+ const entry = byId.get(sessionId);
14381
+ if (!entry) throw new SessionNotFoundError(sessionId);
14382
+ const pending = pendingPermissions.get(requestId);
14383
+ if (!pending) {
14384
+ const record = resolvedPermissions.get(requestId);
14385
+ if (record?.sessionId === sessionId) {
14386
+ resolveTrustedClientId(entry, context?.clientId);
14387
+ publishPermissionAlreadyResolved(record);
14388
+ } else if (record) {
14389
+ writeServeDebugLine(
14390
+ `rejected permission vote ${JSON.stringify(requestId)} for session ${JSON.stringify(sessionId)}; request belongs to session ${JSON.stringify(record.sessionId)}.`
14391
+ );
14392
+ }
14393
+ return false;
14394
+ }
14395
+ if (pending.sessionId !== sessionId) {
14396
+ writeServeDebugLine(
14397
+ `rejected permission vote ${JSON.stringify(requestId)} for session ${JSON.stringify(sessionId)}; request belongs to session ${JSON.stringify(pending.sessionId)}.`
14398
+ );
14399
+ return false;
14400
+ }
14401
+ const originatorClientId = resolveTrustedClientId(
14402
+ entry,
14403
+ context?.clientId
14404
+ );
14405
+ if (response.outcome.outcome === "selected" && !pending.allowedOptionIds.has(response.outcome.optionId)) {
14406
+ throw new InvalidPermissionOptionError(
14407
+ requestId,
14408
+ response.outcome.optionId
14409
+ );
14410
+ }
14411
+ return resolvePending(requestId, response, originatorClientId);
13995
14412
  },
13996
14413
  listWorkspaceSessions(workspaceCwd) {
13997
14414
  if (!path.isAbsolute(workspaceCwd)) return [];
@@ -14008,9 +14425,13 @@ function createHttpAcpBridge(opts) {
14008
14425
  }
14009
14426
  return out;
14010
14427
  },
14011
- async setSessionModel(sessionId, req) {
14428
+ async setSessionModel(sessionId, req, context) {
14012
14429
  const entry = byId.get(sessionId);
14013
14430
  if (!entry) throw new SessionNotFoundError(sessionId);
14431
+ const originatorClientId = resolveTrustedClientId(
14432
+ entry,
14433
+ context?.clientId
14434
+ );
14014
14435
  const normalized = { ...req, sessionId };
14015
14436
  const conn = entry.connection;
14016
14437
  const transportClosed = getTransportClosedReject(entry);
@@ -14039,7 +14460,8 @@ function createHttpAcpBridge(opts) {
14039
14460
  sessionId: entry.sessionId,
14040
14461
  requestedModelId: req.modelId,
14041
14462
  error: err instanceof Error ? err.message : String(err)
14042
- }
14463
+ },
14464
+ ...originatorClientId ? { originatorClientId } : {}
14043
14465
  });
14044
14466
  } catch {
14045
14467
  }
@@ -14048,7 +14470,8 @@ function createHttpAcpBridge(opts) {
14048
14470
  try {
14049
14471
  entry.events.publish({
14050
14472
  type: "model_switched",
14051
- data: { sessionId: entry.sessionId, modelId: req.modelId }
14473
+ data: { sessionId: entry.sessionId, modelId: req.modelId },
14474
+ ...originatorClientId ? { originatorClientId } : {}
14052
14475
  });
14053
14476
  } catch {
14054
14477
  }
@@ -14078,16 +14501,17 @@ function createHttpAcpBridge(opts) {
14078
14501
  } catch {
14079
14502
  }
14080
14503
  entry.events.close();
14081
- if (ci && ci.sessionIds.size === 0) {
14504
+ if (ci && ci.sessionIds.size === 0 && ci.pendingRestoreIds.size === 0) {
14082
14505
  ci.isDying = true;
14083
14506
  await ci.channel.kill().catch(() => {
14084
14507
  });
14085
14508
  }
14086
14509
  },
14087
- async detachClient(sessionId) {
14510
+ async detachClient(sessionId, clientId) {
14088
14511
  const entry = byId.get(sessionId);
14089
14512
  if (!entry) return;
14090
14513
  if (entry.attachCount > 0) entry.attachCount--;
14514
+ unregisterClient(entry, clientId);
14091
14515
  if (entry.spawnOwnerWantedKill && entry.attachCount === 0 && entry.events.subscriberCount === 0) {
14092
14516
  await this.killSession(sessionId).catch(() => {
14093
14517
  });
@@ -14135,6 +14559,12 @@ function createHttpAcpBridge(opts) {
14135
14559
  () => void 0
14136
14560
  )
14137
14561
  );
14562
+ const inFlightRestoreAwaits = Array.from(inFlightRestores.values()).map(
14563
+ (restore) => restore.promise.then(
14564
+ () => void 0,
14565
+ () => void 0
14566
+ )
14567
+ );
14138
14568
  const inFlightChannelAwait = inFlightChannelSpawn ? inFlightChannelSpawn.then(
14139
14569
  () => void 0,
14140
14570
  () => void 0
@@ -14143,6 +14573,7 @@ function createHttpAcpBridge(opts) {
14143
14573
  ...channels.map((ci) => ci.channel.kill().catch(() => {
14144
14574
  })),
14145
14575
  ...inFlightSessionAwaits,
14576
+ ...inFlightRestoreAwaits,
14146
14577
  inFlightChannelAwait
14147
14578
  ]);
14148
14579
  }
@@ -14328,20 +14759,68 @@ function killChild(child) {
14328
14759
  }
14329
14760
  __name(killChild, "killChild");
14330
14761
 
14762
+ // packages/cli/src/serve/capabilities.ts
14763
+ init_esbuild_shims();
14764
+ var SERVE_PROTOCOL_VERSION = "v1";
14765
+ var SUPPORTED_SERVE_PROTOCOL_VERSIONS = [
14766
+ SERVE_PROTOCOL_VERSION
14767
+ ];
14768
+ var SERVE_CAPABILITY_REGISTRY = {
14769
+ health: { since: "v1" },
14770
+ capabilities: { since: "v1" },
14771
+ session_create: { since: "v1" },
14772
+ session_scope_override: { since: "v1" },
14773
+ session_load: { since: "v1" },
14774
+ // ACP backs this with `connection.unstable_resumeSession`. Surface
14775
+ // the unstable prefix so clients don't pin against a `v1` shape that
14776
+ // the underlying ACP method may still change.
14777
+ unstable_session_resume: { since: "v1" },
14778
+ session_list: { since: "v1" },
14779
+ session_prompt: { since: "v1" },
14780
+ session_cancel: { since: "v1" },
14781
+ session_events: { since: "v1" },
14782
+ session_set_model: { since: "v1" },
14783
+ client_identity: { since: "v1" },
14784
+ session_permission_vote: { since: "v1" },
14785
+ permission_vote: { since: "v1" }
14786
+ };
14787
+ var SERVE_FEATURES = Object.freeze(
14788
+ Object.keys(SERVE_CAPABILITY_REGISTRY)
14789
+ );
14790
+ function serveProtocolVersionIndex(version) {
14791
+ return SUPPORTED_SERVE_PROTOCOL_VERSIONS.indexOf(version);
14792
+ }
14793
+ __name(serveProtocolVersionIndex, "serveProtocolVersionIndex");
14794
+ function isFeatureAvailableInProtocol(feature, protocolVersion) {
14795
+ return serveProtocolVersionIndex(SERVE_CAPABILITY_REGISTRY[feature].since) <= serveProtocolVersionIndex(protocolVersion);
14796
+ }
14797
+ __name(isFeatureAvailableInProtocol, "isFeatureAvailableInProtocol");
14798
+ function getRegisteredServeFeatures() {
14799
+ return [...SERVE_FEATURES];
14800
+ }
14801
+ __name(getRegisteredServeFeatures, "getRegisteredServeFeatures");
14802
+ function getAdvertisedServeFeatures(protocolVersion = SERVE_PROTOCOL_VERSION) {
14803
+ return SERVE_FEATURES.filter(
14804
+ (feature) => isFeatureAvailableInProtocol(feature, protocolVersion)
14805
+ );
14806
+ }
14807
+ __name(getAdvertisedServeFeatures, "getAdvertisedServeFeatures");
14808
+ function getServeFeatures() {
14809
+ return getAdvertisedServeFeatures();
14810
+ }
14811
+ __name(getServeFeatures, "getServeFeatures");
14812
+ function getServeProtocolVersions() {
14813
+ return {
14814
+ current: SERVE_PROTOCOL_VERSION,
14815
+ supported: [...SUPPORTED_SERVE_PROTOCOL_VERSIONS]
14816
+ };
14817
+ }
14818
+ __name(getServeProtocolVersions, "getServeProtocolVersions");
14819
+
14331
14820
  // packages/cli/src/serve/types.ts
14332
14821
  init_esbuild_shims();
14333
14822
  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
- ];
14823
+ var STAGE1_FEATURES = SERVE_FEATURES;
14345
14824
 
14346
14825
  // packages/cli/src/serve/server.ts
14347
14826
  function createServeApp(opts, getPort = () => opts.port, deps = {}) {
@@ -14385,8 +14864,9 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14385
14864
  app.get("/capabilities", (_req, res) => {
14386
14865
  const envelope = {
14387
14866
  v: CAPABILITIES_SCHEMA_VERSION,
14867
+ protocolVersions: getServeProtocolVersions(),
14388
14868
  mode: opts.mode,
14389
- features: [...STAGE1_FEATURES],
14869
+ features: getAdvertisedServeFeatures(),
14390
14870
  modelServices: [],
14391
14871
  // #3803 §02: surface the bound workspace so clients can detect
14392
14872
  // mismatch pre-flight and omit `cwd` on `POST /session`.
@@ -14413,17 +14893,33 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14413
14893
  return;
14414
14894
  }
14415
14895
  const modelServiceId = typeof body["modelServiceId"] === "string" ? body["modelServiceId"] : void 0;
14896
+ const rawSessionScope = body["sessionScope"];
14897
+ let sessionScope;
14898
+ if (rawSessionScope !== void 0) {
14899
+ if (rawSessionScope !== "single" && rawSessionScope !== "thread") {
14900
+ res.status(400).json({
14901
+ error: '`sessionScope` must be "single" or "thread" when provided',
14902
+ code: "invalid_session_scope"
14903
+ });
14904
+ return;
14905
+ }
14906
+ sessionScope = rawSessionScope;
14907
+ }
14908
+ const clientId = parseClientIdHeader(req, res);
14909
+ if (clientId === null) return;
14416
14910
  try {
14417
14911
  const session = await bridge.spawnOrAttach({
14418
14912
  workspaceCwd: cwd,
14419
- modelServiceId
14913
+ modelServiceId,
14914
+ ...clientId !== void 0 ? { clientId } : {},
14915
+ ...sessionScope !== void 0 ? { sessionScope } : {}
14420
14916
  });
14421
14917
  if (!res.writable) {
14422
14918
  if (!session.attached) {
14423
14919
  bridge.killSession(session.sessionId, { requireZeroAttaches: true }).catch(() => {
14424
14920
  });
14425
14921
  } else {
14426
- bridge.detachClient(session.sessionId).catch(() => {
14922
+ bridge.detachClient(session.sessionId, session.clientId).catch(() => {
14427
14923
  });
14428
14924
  }
14429
14925
  return;
@@ -14433,6 +14929,47 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14433
14929
  sendBridgeError(res, err, { route: "POST /session" });
14434
14930
  }
14435
14931
  });
14932
+ const restoreSessionHandler = /* @__PURE__ */ __name((action) => async (req, res) => {
14933
+ const sessionId = req.params["id"];
14934
+ if (!sessionId) {
14935
+ res.status(400).json({ error: "`sessionId` route parameter is required" });
14936
+ return;
14937
+ }
14938
+ const body = safeBody(req);
14939
+ const cwd = parseOptionalWorkspaceCwd(body, boundWorkspace, res);
14940
+ if (cwd === void 0) return;
14941
+ const clientId = parseClientIdHeader(req, res);
14942
+ if (clientId === null) return;
14943
+ try {
14944
+ const session = action === "load" ? await bridge.loadSession({
14945
+ sessionId,
14946
+ workspaceCwd: cwd,
14947
+ ...clientId !== void 0 ? { clientId } : {}
14948
+ }) : await bridge.resumeSession({
14949
+ sessionId,
14950
+ workspaceCwd: cwd,
14951
+ ...clientId !== void 0 ? { clientId } : {}
14952
+ });
14953
+ if (!res.writable) {
14954
+ if (!session.attached) {
14955
+ bridge.killSession(session.sessionId, { requireZeroAttaches: true }).catch(() => {
14956
+ });
14957
+ } else {
14958
+ bridge.detachClient(session.sessionId, session.clientId).catch(() => {
14959
+ });
14960
+ }
14961
+ return;
14962
+ }
14963
+ res.status(200).json(session);
14964
+ } catch (err) {
14965
+ sendBridgeError(res, err, {
14966
+ route: `POST /session/:id/${action}`,
14967
+ sessionId
14968
+ });
14969
+ }
14970
+ }, "restoreSessionHandler");
14971
+ app.post("/session/:id/load", restoreSessionHandler("load"));
14972
+ app.post("/session/:id/resume", restoreSessionHandler("resume"));
14436
14973
  app.post("/session/:id/prompt", async (req, res) => {
14437
14974
  const sessionId = req.params["id"];
14438
14975
  const body = safeBody(req);
@@ -14464,6 +15001,11 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14464
15001
  if (!res.writableEnded) abort.abort();
14465
15002
  }, "onResClose");
14466
15003
  res.once("close", onResClose);
15004
+ const clientId = parseClientIdHeader(req, res);
15005
+ if (clientId === null) {
15006
+ res.off("close", onResClose);
15007
+ return;
15008
+ }
14467
15009
  try {
14468
15010
  const result = await bridge.sendPrompt(
14469
15011
  sessionId,
@@ -14472,7 +15014,8 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14472
15014
  sessionId,
14473
15015
  prompt
14474
15016
  },
14475
- abort.signal
15017
+ abort.signal,
15018
+ clientId !== void 0 ? { clientId } : void 0
14476
15019
  );
14477
15020
  res.status(200).json(result);
14478
15021
  } catch (err) {
@@ -14490,11 +15033,17 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14490
15033
  app.post("/session/:id/cancel", async (req, res) => {
14491
15034
  const sessionId = req.params["id"];
14492
15035
  const body = safeBody(req);
15036
+ const clientId = parseClientIdHeader(req, res);
15037
+ if (clientId === null) return;
14493
15038
  try {
14494
- await bridge.cancelSession(sessionId, {
14495
- ...body,
14496
- sessionId
14497
- });
15039
+ await bridge.cancelSession(
15040
+ sessionId,
15041
+ {
15042
+ ...body,
15043
+ sessionId
15044
+ },
15045
+ clientId !== void 0 ? { clientId } : void 0
15046
+ );
14498
15047
  res.status(204).end();
14499
15048
  } catch (err) {
14500
15049
  sendBridgeError(res, err, {
@@ -14532,12 +15081,18 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14532
15081
  });
14533
15082
  return;
14534
15083
  }
15084
+ const clientId = parseClientIdHeader(req, res);
15085
+ if (clientId === null) return;
14535
15086
  try {
14536
- const response = await bridge.setSessionModel(sessionId, {
14537
- ...body,
15087
+ const response = await bridge.setSessionModel(
14538
15088
  sessionId,
14539
- modelId
14540
- });
15089
+ {
15090
+ ...body,
15091
+ sessionId,
15092
+ modelId
15093
+ },
15094
+ clientId !== void 0 ? { clientId } : void 0
15095
+ );
14541
15096
  res.status(200).json(response);
14542
15097
  } catch (err) {
14543
15098
  sendBridgeError(res, err, {
@@ -14546,33 +15101,56 @@ function createServeApp(opts, getPort = () => opts.port, deps = {}) {
14546
15101
  });
14547
15102
  }
14548
15103
  });
14549
- app.post("/permission/:requestId", (req, res) => {
15104
+ app.post("/session/:id/permission/:requestId", (req, res) => {
15105
+ const sessionId = req.params["id"];
14550
15106
  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 }`'
15107
+ const response = parsePermissionVoteBody(req, res);
15108
+ if (response === void 0) return;
15109
+ const clientId = parseClientIdHeader(req, res);
15110
+ if (clientId === null) return;
15111
+ let accepted;
15112
+ try {
15113
+ accepted = bridge.respondToSessionPermission(
15114
+ sessionId,
15115
+ requestId,
15116
+ response,
15117
+ clientId !== void 0 ? { clientId } : void 0
15118
+ );
15119
+ } catch (err) {
15120
+ sendPermissionVoteError(res, err, {
15121
+ route: "POST /session/:id/permission/:requestId",
15122
+ sessionId
15123
+ });
15124
+ return;
15125
+ }
15126
+ if (!accepted) {
15127
+ res.status(404).json({
15128
+ error: "No pending permission request for session",
15129
+ sessionId,
15130
+ requestId
14556
15131
  });
14557
15132
  return;
14558
15133
  }
15134
+ res.status(200).json({});
15135
+ });
15136
+ app.post("/permission/:requestId", (req, res) => {
15137
+ const requestId = req.params["requestId"];
15138
+ const response = parsePermissionVoteBody(req, res);
15139
+ if (response === void 0) return;
15140
+ const clientId = parseClientIdHeader(req, res);
15141
+ if (clientId === null) return;
14559
15142
  let accepted;
14560
15143
  try {
14561
- accepted = bridge.respondToPermission(requestId, {
14562
- ...body,
14563
- outcome
14564
- });
15144
+ accepted = bridge.respondToPermission(
15145
+ requestId,
15146
+ response,
15147
+ clientId !== void 0 ? { clientId } : void 0
15148
+ );
14565
15149
  } 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;
15150
+ sendPermissionVoteError(res, err, {
15151
+ route: "POST /permission/:requestId"
15152
+ });
15153
+ return;
14576
15154
  }
14577
15155
  if (!accepted) {
14578
15156
  res.status(404).json({ error: "No pending permission request", requestId });
@@ -14728,6 +15306,10 @@ var PROTOTYPE_POLLUTION_KEYS = /* @__PURE__ */ new Set([
14728
15306
  "constructor",
14729
15307
  "prototype"
14730
15308
  ]);
15309
+ var CLIENT_ID_HEADER = "x-qwen-client-id";
15310
+ var MAX_CLIENT_ID_LENGTH = 128;
15311
+ var CLIENT_ID_RE = /^[A-Za-z0-9._:-]+$/;
15312
+ var INVALID_PERMISSION_OUTCOME_ERROR = '`outcome` must be `{ outcome: "cancelled" }` or `{ outcome: "selected", optionId: string }`';
14731
15313
  function safeBody(req) {
14732
15314
  const raw = req.body;
14733
15315
  if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
@@ -14741,6 +15323,52 @@ function safeBody(req) {
14741
15323
  return out;
14742
15324
  }
14743
15325
  __name(safeBody, "safeBody");
15326
+ function parseOptionalWorkspaceCwd(body, boundWorkspace, res) {
15327
+ const hasCwd = "cwd" in body;
15328
+ if (hasCwd && typeof body["cwd"] !== "string") {
15329
+ res.status(400).json({ error: "`cwd` must be a string absolute path when provided" });
15330
+ return void 0;
15331
+ }
15332
+ if (hasCwd && body["cwd"].length > MAX_WORKSPACE_PATH_LENGTH) {
15333
+ res.status(400).json({
15334
+ error: `\`cwd\` exceeds the ${MAX_WORKSPACE_PATH_LENGTH}-character limit`
15335
+ });
15336
+ return void 0;
15337
+ }
15338
+ const cwd = hasCwd ? body["cwd"] : boundWorkspace;
15339
+ if (!path2.isAbsolute(cwd)) {
15340
+ res.status(400).json({ error: "`cwd` must be an absolute path when provided" });
15341
+ return void 0;
15342
+ }
15343
+ return cwd;
15344
+ }
15345
+ __name(parseOptionalWorkspaceCwd, "parseOptionalWorkspaceCwd");
15346
+ function parseClientIdHeader(req, res) {
15347
+ const raw = req.get(CLIENT_ID_HEADER);
15348
+ if (raw === void 0 || raw === "") return void 0;
15349
+ if (raw.length > MAX_CLIENT_ID_LENGTH || !CLIENT_ID_RE.test(raw)) {
15350
+ res.status(400).json({
15351
+ error: "`X-Qwen-Client-Id` must be a non-empty token of 128 characters or fewer",
15352
+ code: "invalid_client_id"
15353
+ });
15354
+ return null;
15355
+ }
15356
+ return raw;
15357
+ }
15358
+ __name(parseClientIdHeader, "parseClientIdHeader");
15359
+ function parsePermissionVoteBody(req, res) {
15360
+ const body = safeBody(req);
15361
+ const outcome = body["outcome"];
15362
+ if (!isValidOutcome(outcome)) {
15363
+ res.status(400).json({ error: INVALID_PERMISSION_OUTCOME_ERROR });
15364
+ return void 0;
15365
+ }
15366
+ return {
15367
+ ...body,
15368
+ outcome
15369
+ };
15370
+ }
15371
+ __name(parsePermissionVoteBody, "parsePermissionVoteBody");
14744
15372
  function isValidOutcome(raw) {
14745
15373
  if (typeof raw !== "object" || raw === null) return false;
14746
15374
  const obj = raw;
@@ -14767,6 +15395,19 @@ function parseLastEventId(raw) {
14767
15395
  return n;
14768
15396
  }
14769
15397
  __name(parseLastEventId, "parseLastEventId");
15398
+ function sendPermissionVoteError(res, err, ctx) {
15399
+ if (err instanceof InvalidPermissionOptionError) {
15400
+ res.status(400).json({
15401
+ error: err.message,
15402
+ code: "invalid_option_id",
15403
+ requestId: err.requestId,
15404
+ optionId: err.optionId
15405
+ });
15406
+ return;
15407
+ }
15408
+ sendBridgeError(res, err, ctx);
15409
+ }
15410
+ __name(sendPermissionVoteError, "sendPermissionVoteError");
14770
15411
  function formatSseFrame(event) {
14771
15412
  const dataJson = JSON.stringify(event);
14772
15413
  const idLine = "id" in event && event.id !== void 0 ? `id: ${event.id}
@@ -14782,6 +15423,15 @@ function sendBridgeError(res, err, ctx) {
14782
15423
  res.status(404).json({ error: err.message, sessionId: err.sessionId });
14783
15424
  return;
14784
15425
  }
15426
+ if (err instanceof InvalidClientIdError) {
15427
+ res.status(400).json({
15428
+ error: err.message,
15429
+ code: "invalid_client_id",
15430
+ sessionId: err.sessionId,
15431
+ clientId: err.clientId
15432
+ });
15433
+ return;
15434
+ }
14785
15435
  if (err instanceof WorkspaceMismatchError) {
14786
15436
  writeStderrLine(
14787
15437
  `qwen serve: workspace_mismatch (POST /session): daemon bound to ${JSON.stringify(err.bound)}, rejected ${JSON.stringify(err.requested)}`
@@ -14794,6 +15444,13 @@ function sendBridgeError(res, err, ctx) {
14794
15444
  });
14795
15445
  return;
14796
15446
  }
15447
+ if (err instanceof InvalidSessionScopeError) {
15448
+ res.status(400).json({
15449
+ error: err.message,
15450
+ code: "invalid_session_scope"
15451
+ });
15452
+ return;
15453
+ }
14797
15454
  if (err instanceof SessionLimitExceededError) {
14798
15455
  res.set("Retry-After", "5");
14799
15456
  res.status(503).json({
@@ -14803,6 +15460,17 @@ function sendBridgeError(res, err, ctx) {
14803
15460
  });
14804
15461
  return;
14805
15462
  }
15463
+ if (err instanceof RestoreInProgressError) {
15464
+ res.set("Retry-After", "5");
15465
+ res.status(409).json({
15466
+ error: err.message,
15467
+ code: "restore_in_progress",
15468
+ sessionId: err.sessionId,
15469
+ activeAction: err.activeAction,
15470
+ requestedAction: err.requestedAction
15471
+ });
15472
+ return;
15473
+ }
14806
15474
  const ctxParts = [
14807
15475
  ctx?.route,
14808
15476
  ctx?.sessionId ? `session=${ctx.sessionId}` : void 0
@@ -15066,12 +15734,20 @@ export {
15066
15734
  CAPABILITIES_SCHEMA_VERSION,
15067
15735
  EVENT_SCHEMA_VERSION,
15068
15736
  EventBus,
15737
+ SERVE_CAPABILITY_REGISTRY,
15738
+ SERVE_FEATURES,
15739
+ SERVE_PROTOCOL_VERSION,
15069
15740
  STAGE1_FEATURES,
15741
+ SUPPORTED_SERVE_PROTOCOL_VERSIONS,
15070
15742
  SessionNotFoundError,
15071
15743
  createHttpAcpBridge,
15072
15744
  createInMemoryChannel,
15073
15745
  createServeApp,
15074
15746
  defaultSpawnChannelFactory,
15747
+ getAdvertisedServeFeatures,
15748
+ getRegisteredServeFeatures,
15749
+ getServeFeatures,
15750
+ getServeProtocolVersions,
15075
15751
  runQwenServe
15076
15752
  };
15077
15753
  /**