@ycodium-ai/agent-acp 0.2.3130 → 0.2.3168

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 (2) hide show
  1. package/dist/index.js +269 -104
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -24862,7 +24862,10 @@ function createAcpConnection(options) {
24862
24862
  }
24863
24863
  pending.delete(key);
24864
24864
  try {
24865
- options.onResponse?.(waiter.method);
24865
+ options.onResponse?.(
24866
+ waiter,
24867
+ Object.hasOwn(message, "error") ? { error: message["error"] } : { result: message["result"] }
24868
+ );
24866
24869
  } catch {
24867
24870
  }
24868
24871
  if (Object.hasOwn(message, "error")) {
@@ -24995,12 +24998,17 @@ function createAcpConnection(options) {
24995
24998
  options.readable.on("end", onEnd);
24996
24999
  options.readable.on("error", onError);
24997
25000
  options.writable.once("error", onWritableError);
24998
- const begin = (method, params) => {
25001
+ const begin = (method, params, promptId) => {
24999
25002
  failIfClosed();
25000
25003
  const id = nextRequestId;
25001
25004
  nextRequestId += 1;
25002
25005
  const { promise: promise2, resolve: resolve2, reject } = Promise.withResolvers();
25003
- pending.set(String(id), { method, resolve: resolve2, reject });
25006
+ pending.set(String(id), {
25007
+ method,
25008
+ ...promptId === void 0 ? {} : { promptId },
25009
+ resolve: resolve2,
25010
+ reject
25011
+ });
25004
25012
  const message = params === void 0 ? { jsonrpc: "2.0", id, method } : { jsonrpc: "2.0", id, method, params };
25005
25013
  const written = Promise.race([writeMessage(message), promise2]).then(
25006
25014
  () => void 0,
@@ -25070,12 +25078,14 @@ function createExecutorLifecycle({
25070
25078
  notifyPromptSettled,
25071
25079
  eventStream,
25072
25080
  onInputDead,
25073
- shared
25081
+ shared,
25082
+ promptResults
25074
25083
  }) {
25075
25084
  const cancel = async (turnId) => {
25076
25085
  failIfDisposed();
25077
25086
  const started = requireStarted();
25078
25087
  const targetTurnId = turnId ?? shared.activeTurnId;
25088
+ promptResults.stop(targetTurnId);
25079
25089
  if (targetTurnId !== void 0) {
25080
25090
  interruptedTurnIds.add(targetTurnId);
25081
25091
  }
@@ -25086,6 +25096,9 @@ function createExecutorLifecycle({
25086
25096
  if (shared.sessionId) {
25087
25097
  await connection.notify("session/cancel", { sessionId: shared.sessionId });
25088
25098
  }
25099
+ await promptResults.wait(targetTurnId);
25100
+ if (targetTurnId !== void 0 && shared.activeTurnId !== targetTurnId && !interruptedTurnIds.has(targetTurnId))
25101
+ return;
25089
25102
  const settled = await waitForPromptIdle(cancelGraceMs);
25090
25103
  if (!settled) {
25091
25104
  throw new AcpExecutorCancelTimeoutError();
@@ -25106,6 +25119,7 @@ function createExecutorLifecycle({
25106
25119
  payload: { state: "cancelled", stopReason: "cancelled" }
25107
25120
  });
25108
25121
  interruptedTurnIds.delete(targetTurnId);
25122
+ promptResults.forget(targetTurnId);
25109
25123
  }
25110
25124
  }
25111
25125
  }
@@ -25180,33 +25194,39 @@ function createExecutorLifecycle({
25180
25194
  if (!shared.disposed) emitSessionState("ready");
25181
25195
  }
25182
25196
  };
25183
- const dispose = async () => {
25184
- if (shared.disposed) {
25185
- return;
25186
- }
25197
+ let disposing;
25198
+ const dispose = () => {
25199
+ if (disposing) return disposing;
25200
+ shared.disposing = true;
25201
+ promptResults.stop();
25187
25202
  settleAllApprovals("cancel");
25188
25203
  settleAllUserInputs();
25189
25204
  abortPendingSettlement("executor disposed");
25190
25205
  notifyPromptSettled();
25191
25206
  shared.abortLocalPrompt?.();
25192
- emit({
25193
- type: "session.exited",
25194
- ...stamp(),
25195
- provider: options.provider,
25196
- threadId: options.threadId,
25197
- payload: { reason: "disposed" }
25198
- });
25199
- shared.disposed = true;
25200
- shared.generation += 1;
25201
- shared.status = "disposed";
25202
- eventStream.close();
25203
- options.readable.off("end", onInputDead);
25204
- options.readable.off("error", onInputDead);
25205
- options.readable.pause();
25206
- if (!shared.connectionClosed) {
25207
- await connection.awaitInboundIdle().catch(() => void 0);
25208
- await connection.close().catch(() => void 0);
25209
- }
25207
+ disposing = (async () => {
25208
+ promptResults.finishUnreceived();
25209
+ await promptResults.wait();
25210
+ emit({
25211
+ type: "session.exited",
25212
+ ...stamp(),
25213
+ provider: options.provider,
25214
+ threadId: options.threadId,
25215
+ payload: { reason: "disposed" }
25216
+ });
25217
+ shared.disposed = true;
25218
+ shared.generation += 1;
25219
+ shared.status = "disposed";
25220
+ eventStream.close();
25221
+ options.readable.off("end", onInputDead);
25222
+ options.readable.off("error", onInputDead);
25223
+ options.readable.pause();
25224
+ if (!shared.connectionClosed) {
25225
+ await connection.awaitInboundIdle().catch(() => void 0);
25226
+ await connection.close().catch(() => void 0);
25227
+ }
25228
+ })();
25229
+ return disposing;
25210
25230
  };
25211
25231
  return { cancel, compact, dispose };
25212
25232
  }
@@ -25310,12 +25330,14 @@ function createExecutorPrompt({
25310
25330
  promptMutex,
25311
25331
  applyRequestedModel,
25312
25332
  notifyPromptSettled,
25313
- shared
25333
+ shared,
25334
+ promptResults
25314
25335
  }) {
25315
25336
  const settleTurn = (input2) => {
25316
- if (!isLive(input2.expectedGeneration)) {
25337
+ if (!isLive(input2.expectedGeneration) || input2.record.finished) {
25317
25338
  return false;
25318
25339
  }
25340
+ promptResults.complete(input2.record);
25319
25341
  if (!acpPromptSettlementBelongsToContext({
25320
25342
  liveAcpSessionId: shared.sessionId ?? "",
25321
25343
  expectedAcpSessionId: input2.expectedSessionId,
@@ -25332,6 +25354,7 @@ function createExecutorPrompt({
25332
25354
  if (shared.status !== "disposed") {
25333
25355
  shared.status = "ready";
25334
25356
  }
25357
+ const interrupted = promptResults.hasInterrupted(input2.turnId);
25335
25358
  emit({
25336
25359
  type: "turn.completed",
25337
25360
  ...stamp(),
@@ -25339,30 +25362,44 @@ function createExecutorPrompt({
25339
25362
  threadId: options.threadId,
25340
25363
  turnId: input2.turnId,
25341
25364
  payload: {
25342
- state: input2.state,
25343
- stopReason: input2.stopReason,
25365
+ state: interrupted ? "cancelled" : input2.state,
25366
+ stopReason: interrupted ? "cancelled" : input2.stopReason,
25344
25367
  ...input2.errorMessage === void 0 ? {} : { errorMessage: input2.errorMessage },
25345
25368
  ...input2.state === "failed" ? failureCategoryField(input2.failureCategory) : {}
25346
25369
  }
25347
25370
  });
25348
25371
  interruptedTurnIds.delete(input2.turnId);
25372
+ promptResults.forget(input2.turnId);
25349
25373
  return true;
25350
25374
  };
25351
- const sendPromptRpc = async (sessionId, promptParts, onWritten) => {
25375
+ const sendPromptRpc = async (sessionId, promptParts, onWritten, record2) => {
25352
25376
  const settlementHooks = hooks.promptSettlement;
25353
25377
  const promptKey = settlementHooks === void 0 ? void 0 : randomId();
25354
- const pending = connection.begin("session/prompt", {
25355
- sessionId,
25356
- prompt: promptParts,
25357
- ...settlementHooks !== void 0 && promptKey !== void 0 ? settlementHooks.requestFields(promptKey) : {}
25358
- });
25378
+ const pending = connection.begin(
25379
+ "session/prompt",
25380
+ {
25381
+ sessionId,
25382
+ prompt: promptParts,
25383
+ ...settlementHooks !== void 0 && promptKey !== void 0 ? settlementHooks.requestFields(promptKey) : {}
25384
+ },
25385
+ record2?.promptId
25386
+ );
25359
25387
  void pending.written.then(onWritten, () => void 0);
25360
25388
  const promptRpc = pending.response;
25361
25389
  if (settlementHooks === void 0 || promptKey === void 0) {
25362
25390
  return await promptRpc;
25363
25391
  } else {
25364
25392
  const { promise: early, resolve: settleEarly } = Promise.withResolvers();
25365
- shared.pendingSettlement = { promptKey, sessionId, settle: settleEarly };
25393
+ shared.pendingSettlement = {
25394
+ promptKey,
25395
+ sessionId,
25396
+ settle: settleEarly,
25397
+ ...record2 === void 0 ? {} : {
25398
+ onResponse: (response) => promptResults.receive(record2.promptId, response),
25399
+ onError: () => promptResults.reject(record2.promptId),
25400
+ hasResult: () => record2.response !== void 0
25401
+ }
25402
+ };
25366
25403
  try {
25367
25404
  const outcome = await Promise.race([
25368
25405
  promptRpc.then((value) => {
@@ -25383,9 +25420,11 @@ function createExecutorPrompt({
25383
25420
  const prompt = async (input2) => {
25384
25421
  failIfDisposed();
25385
25422
  const started = requireStarted();
25423
+ let record2;
25386
25424
  const settle2 = (rest) => settleTurn({
25387
25425
  expectedGeneration: started.generation,
25388
25426
  expectedSessionId: started.sessionId,
25427
+ record: record2,
25389
25428
  ...rest
25390
25429
  });
25391
25430
  let turnId;
@@ -25398,6 +25437,17 @@ function createExecutorPrompt({
25398
25437
  randomId
25399
25438
  );
25400
25439
  shared.promptsInFlight += 1;
25440
+ record2 = promptResults.add(
25441
+ {
25442
+ promptId: randomId(),
25443
+ sessionId: started.sessionId,
25444
+ generation: started.generation,
25445
+ turnId
25446
+ },
25447
+ () => {
25448
+ finishPrompt();
25449
+ }
25450
+ );
25401
25451
  shared.activeTurnId = turnId;
25402
25452
  shared.status = "running";
25403
25453
  if (shared.promptsInFlight === 1) {
@@ -25414,6 +25464,42 @@ function createExecutorPrompt({
25414
25464
  threadMutex.release();
25415
25465
  }
25416
25466
  const promptParts = input2.prompt ?? (input2.text !== void 0 ? [{ type: "text", text: input2.text }] : [{ type: "text", text: "" }]);
25467
+ let contextWindow;
25468
+ const finishPrompt = () => {
25469
+ if (!isLive(started.generation) || record2.finished) throw new AcpExecutorDisposedError();
25470
+ const response2 = record2.response;
25471
+ if (record2.interrupted || response2 === void 0) {
25472
+ settle2({ turnId, stopReason: "cancelled", state: "cancelled" });
25473
+ return { threadId: options.threadId, turnId, stopReason: "cancelled" };
25474
+ }
25475
+ const stopReason = hooks.completedStopReason?.(response2) ?? response2.stopReason;
25476
+ if (contextWindow !== void 0) {
25477
+ emit({
25478
+ type: "thread.token-usage.updated",
25479
+ ...stamp(),
25480
+ provider: options.provider,
25481
+ threadId: options.threadId,
25482
+ turnId,
25483
+ payload: {
25484
+ usage: {
25485
+ usedTokens: contextWindow.usedTokens,
25486
+ ...contextWindow.maxTokens === void 0 ? {} : { maxTokens: contextWindow.maxTokens }
25487
+ }
25488
+ }
25489
+ });
25490
+ }
25491
+ settle2({
25492
+ turnId,
25493
+ stopReason,
25494
+ state: response2.stopReason === "cancelled" ? "cancelled" : "completed"
25495
+ });
25496
+ return {
25497
+ threadId: options.threadId,
25498
+ turnId,
25499
+ stopReason,
25500
+ ...response2.usage !== void 0 && response2.usage !== null ? { usage: response2.usage } : {}
25501
+ };
25502
+ };
25417
25503
  await promptMutex.acquire();
25418
25504
  let response;
25419
25505
  try {
@@ -25428,6 +25514,7 @@ function createExecutorPrompt({
25428
25514
  expectedGeneration: started.generation
25429
25515
  });
25430
25516
  }
25517
+ failIfDisposed();
25431
25518
  if (isStale()) {
25432
25519
  settle2({ turnId, stopReason: "cancelled", state: "cancelled" });
25433
25520
  return { threadId: options.threadId, turnId, stopReason: "cancelled" };
@@ -25435,10 +25522,15 @@ function createExecutorPrompt({
25435
25522
  closeActiveAssistantSegment(turnId);
25436
25523
  let written = false;
25437
25524
  try {
25438
- response = await sendPromptRpc(started.sessionId, promptParts, () => {
25439
- written = true;
25440
- input2.onDispatched?.(turnId);
25441
- });
25525
+ response = await sendPromptRpc(
25526
+ started.sessionId,
25527
+ promptParts,
25528
+ () => {
25529
+ written = true;
25530
+ input2.onDispatched?.(turnId);
25531
+ },
25532
+ record2
25533
+ );
25442
25534
  } catch (error62) {
25443
25535
  if (shared.disposed) {
25444
25536
  throw new AcpExecutorDisposedError();
@@ -25477,8 +25569,7 @@ function createExecutorPrompt({
25477
25569
  if (turnId === void 0) {
25478
25570
  throw new AcpExecutorError("internal", "ACP prompt settled without a turn id");
25479
25571
  }
25480
- const settledTurnId = turnId;
25481
- let contextWindow;
25572
+ if (record2.finished) throw new AcpExecutorDisposedError();
25482
25573
  if (hooks.readContextWindow !== void 0) {
25483
25574
  try {
25484
25575
  contextWindow = await hooks.readContextWindow({
@@ -25489,43 +25580,6 @@ function createExecutorPrompt({
25489
25580
  contextWindow = void 0;
25490
25581
  }
25491
25582
  }
25492
- const finishPrompt = () => {
25493
- if (interruptedTurnIds.has(settledTurnId)) {
25494
- settle2({ turnId: settledTurnId, stopReason: "cancelled", state: "cancelled" });
25495
- return {
25496
- threadId: options.threadId,
25497
- turnId: settledTurnId,
25498
- stopReason: "cancelled"
25499
- };
25500
- }
25501
- const stopReason = hooks.completedStopReason?.(response) ?? response.stopReason;
25502
- if (contextWindow !== void 0) {
25503
- emit({
25504
- type: "thread.token-usage.updated",
25505
- ...stamp(),
25506
- provider: options.provider,
25507
- threadId: options.threadId,
25508
- turnId: settledTurnId,
25509
- payload: {
25510
- usage: {
25511
- usedTokens: contextWindow.usedTokens,
25512
- ...contextWindow.maxTokens === void 0 ? {} : { maxTokens: contextWindow.maxTokens }
25513
- }
25514
- }
25515
- });
25516
- }
25517
- settle2({
25518
- turnId: settledTurnId,
25519
- stopReason,
25520
- state: response.stopReason === "cancelled" ? "cancelled" : "completed"
25521
- });
25522
- return {
25523
- threadId: options.threadId,
25524
- turnId: settledTurnId,
25525
- stopReason,
25526
- ...response.usage !== void 0 && response.usage !== null ? { usage: response.usage } : {}
25527
- };
25528
- };
25529
25583
  if (threadMutex.tryAcquire()) {
25530
25584
  try {
25531
25585
  return finishPrompt();
@@ -25543,6 +25597,91 @@ function createExecutorPrompt({
25543
25597
  return { sendPromptRpc, prompt };
25544
25598
  }
25545
25599
 
25600
+ // ../../packages/ycodium-acp-executor/src/executor/prompt-results.ts
25601
+ function createPromptResults({
25602
+ options,
25603
+ shared
25604
+ }) {
25605
+ const prompts = /* @__PURE__ */ new Map();
25606
+ const add = (identity, finish) => {
25607
+ const { promise: done, resolve: resolve2 } = Promise.withResolvers();
25608
+ const record2 = {
25609
+ ...identity,
25610
+ finish,
25611
+ done,
25612
+ resolve: resolve2,
25613
+ rejected: false,
25614
+ interrupted: false,
25615
+ finished: false
25616
+ };
25617
+ prompts.set(record2.promptId, record2);
25618
+ return record2;
25619
+ };
25620
+ const receive = (promptId, response) => {
25621
+ const record2 = prompts.get(promptId);
25622
+ if (!record2 || record2.finished || record2.rejected || record2.response !== void 0) return;
25623
+ if (shared.disposed || shared.generation !== record2.generation || shared.sessionId !== record2.sessionId)
25624
+ return;
25625
+ record2.response = response;
25626
+ };
25627
+ const reject = (promptId) => {
25628
+ const record2 = prompts.get(promptId);
25629
+ if (record2 && record2.response === void 0) record2.rejected = true;
25630
+ };
25631
+ const stop = (turnId) => {
25632
+ for (const record2 of prompts.values()) {
25633
+ if (!record2.finished && (turnId === void 0 || record2.turnId === turnId) && record2.response === void 0) {
25634
+ record2.interrupted = true;
25635
+ }
25636
+ }
25637
+ };
25638
+ const complete = (record2) => {
25639
+ record2.finished = true;
25640
+ record2.resolve();
25641
+ };
25642
+ const hasInterrupted = (turnId) => [...prompts.values()].some((record2) => record2.turnId === turnId && record2.interrupted);
25643
+ const forget = (turnId) => {
25644
+ for (const [id, record2] of prompts) {
25645
+ if (record2.turnId === turnId) {
25646
+ complete(record2);
25647
+ prompts.delete(id);
25648
+ }
25649
+ }
25650
+ };
25651
+ const wait = async (turnId) => {
25652
+ const claimed = [...prompts.values()].filter(
25653
+ (record2) => !record2.finished && record2.response !== void 0 && (turnId === void 0 || record2.turnId === turnId)
25654
+ );
25655
+ if (claimed.length === 0) return;
25656
+ let timer;
25657
+ try {
25658
+ const drained = await Promise.race([
25659
+ Promise.all(claimed.map((record2) => record2.done)).then(() => true),
25660
+ new Promise((resolve2) => {
25661
+ timer = setTimeout(() => resolve2(false), options.resultDrainTimeoutMs ?? 2e3);
25662
+ })
25663
+ ]);
25664
+ if (drained) return;
25665
+ options.log?.warn?.("ACP result settlement timed out", { threadId: options.threadId });
25666
+ for (const record2 of claimed) if (!record2.finished) record2.finish();
25667
+ } finally {
25668
+ clearTimeout(timer);
25669
+ }
25670
+ };
25671
+ const finishUnreceived = () => {
25672
+ const claimedTurns = new Set(
25673
+ [...prompts.values()].filter((record2) => !record2.finished && record2.response !== void 0).map((record2) => record2.turnId)
25674
+ );
25675
+ for (const record2 of prompts.values()) {
25676
+ if (!record2.finished && record2.response === void 0) {
25677
+ if (claimedTurns.has(record2.turnId)) record2.finish();
25678
+ else complete(record2);
25679
+ }
25680
+ }
25681
+ };
25682
+ return { add, receive, reject, stop, complete, hasInterrupted, forget, wait, finishUnreceived };
25683
+ }
25684
+
25546
25685
  // ../../packages/ycodium-acp-executor/src/user-input-pending.ts
25547
25686
  var userInputAnswerOptionId = (question, answers) => {
25548
25687
  const answer = answers[question.id];
@@ -25849,7 +25988,7 @@ function createExecutorRuntime({
25849
25988
  turnId: () => shared.activeTurnId
25850
25989
  });
25851
25990
  const failIfDisposed = () => {
25852
- if (shared.disposed) {
25991
+ if (shared.disposed || shared.disposing) {
25853
25992
  throw new AcpExecutorDisposedError();
25854
25993
  }
25855
25994
  };
@@ -26441,6 +26580,7 @@ function createExecutorSettlement({
26441
26580
  const abortPendingSettlement = (reason, failureCategory) => {
26442
26581
  const waiting = shared.pendingSettlement;
26443
26582
  if (waiting === void 0) return;
26583
+ if (waiting.hasResult?.()) return;
26444
26584
  shared.pendingSettlement = void 0;
26445
26585
  waiting.settle({
26446
26586
  error: withFailureCategory(
@@ -26462,6 +26602,7 @@ function createExecutorSettlement({
26462
26602
  shared.pendingSettlement = void 0;
26463
26603
  rememberSettledPromptKey(waiting.promptKey);
26464
26604
  if (signal.outcome === "error") {
26605
+ waiting.onError?.();
26465
26606
  waiting.settle({
26466
26607
  error: withFailureCategory(
26467
26608
  new AcpExecutorRequestError(
@@ -26472,8 +26613,10 @@ function createExecutorSettlement({
26472
26613
  )
26473
26614
  });
26474
26615
  } else {
26616
+ const response = { stopReason: signal.stopReason ?? "end_turn" };
26617
+ waiting.onResponse?.(response);
26475
26618
  waiting.settle({
26476
- response: { stopReason: signal.stopReason ?? "end_turn" }
26619
+ response
26477
26620
  });
26478
26621
  }
26479
26622
  return true;
@@ -26803,6 +26946,7 @@ function createAcpExecutor(options) {
26803
26946
  const shared = {
26804
26947
  generation: 0,
26805
26948
  disposed: false,
26949
+ disposing: false,
26806
26950
  status: "idle",
26807
26951
  sessionId: void 0,
26808
26952
  modelSelectionState: {
@@ -26890,6 +27034,7 @@ function createAcpExecutor(options) {
26890
27034
  shared
26891
27035
  });
26892
27036
  const { rememberSettledPromptKey, abortPendingSettlement, settleFromNotification } = createExecutorSettlement({ hooks, shared });
27037
+ const promptResults = createPromptResults({ options, shared });
26893
27038
  const connection = createAcpConnection({
26894
27039
  readable: options.readable,
26895
27040
  writable: options.writable,
@@ -26900,8 +27045,12 @@ function createAcpExecutor(options) {
26900
27045
  });
26901
27046
  },
26902
27047
  // 窗口在应答帧到达的那一刻同步收掉:应答之后的通知是真输出,不能再丢。
26903
- onResponse: (method) => {
27048
+ onResponse: ({ method, promptId }, outcome) => {
26904
27049
  if (method === "session/load") shared.sessionLoadInFlight = false;
27050
+ if (promptId !== void 0) {
27051
+ if ("result" in outcome) promptResults.receive(promptId, outcome.result);
27052
+ else promptResults.reject(promptId);
27053
+ }
26905
27054
  },
26906
27055
  onNotification: (notification) => {
26907
27056
  if (notification.method === "session/update") {
@@ -26966,6 +27115,7 @@ function createAcpExecutor(options) {
26966
27115
  shared
26967
27116
  });
26968
27117
  const { sendPromptRpc, prompt } = createExecutorPrompt({
27118
+ promptResults,
26969
27119
  isLive,
26970
27120
  closeActiveAssistantSegment,
26971
27121
  emit,
@@ -26985,6 +27135,7 @@ function createAcpExecutor(options) {
26985
27135
  shared
26986
27136
  });
26987
27137
  const { cancel, compact, dispose } = createExecutorLifecycle({
27138
+ promptResults,
26988
27139
  failIfDisposed,
26989
27140
  requireStarted,
26990
27141
  interruptedTurnIds,
@@ -27036,6 +27187,27 @@ function createAcpExecutor(options) {
27036
27187
  };
27037
27188
  }
27038
27189
 
27190
+ // ../../packages/ycodium-acp-executor/src/session-registry.ts
27191
+ function createAcpSessionRegistry(threads, stopBooked2) {
27192
+ const requireThread = (threadId) => {
27193
+ const booked = threads.get(threadId);
27194
+ if (!booked) throw new Error(`unknown thread ${threadId}`);
27195
+ if (booked.closing) throw new Error(`thread ${threadId} is closing`);
27196
+ return booked;
27197
+ };
27198
+ const stopSession = (threadId, booked = threads.get(threadId)) => {
27199
+ if (!booked) return Promise.resolve();
27200
+ booked.closing ??= Promise.resolve().then(() => stopBooked2(booked)).then(() => {
27201
+ if (threads.get(threadId) === booked) threads.delete(threadId);
27202
+ });
27203
+ return booked.closing;
27204
+ };
27205
+ const stopAll = () => Promise.all([...threads].map(([threadId, booked]) => stopSession(threadId, booked))).then(
27206
+ () => void 0
27207
+ );
27208
+ return { requireThread, stopSession, stopAll };
27209
+ }
27210
+
27039
27211
  // ../../packages/ycodium-acp-executor/src/prompt-dispatch.ts
27040
27212
  var dispatchAcpPrompt = (executor, input2) => {
27041
27213
  const { promise: dispatched, resolve: resolve2 } = Promise.withResolvers();
@@ -27565,11 +27737,8 @@ var createAcpInstance = (config2, host) => {
27565
27737
  const events = new EventBus();
27566
27738
  const threads = /* @__PURE__ */ new Map();
27567
27739
  let disposed = false;
27568
- const requireThread = (threadId) => {
27569
- const booked = threads.get(threadId);
27570
- if (!booked) throw new Error(`unknown thread ${threadId}`);
27571
- return booked;
27572
- };
27740
+ let disposeTask;
27741
+ const { requireThread, stopSession, stopAll } = createAcpSessionRegistry(threads, stopBooked);
27573
27742
  const instance = {
27574
27743
  probe: async () => {
27575
27744
  if (recipe.binaryPath.length === 0) {
@@ -27634,8 +27803,8 @@ var createAcpInstance = (config2, host) => {
27634
27803
  if (cwd.trim() === "") throw new Error("workspace path missing");
27635
27804
  const existing = threads.get(input2.threadId);
27636
27805
  if (existing !== void 0) {
27637
- await stopBooked(existing);
27638
- threads.delete(input2.threadId);
27806
+ await stopSession(input2.threadId, existing);
27807
+ if (disposed) throw new Error("instance disposed");
27639
27808
  throwIfStartAborted(signal);
27640
27809
  }
27641
27810
  const env = buildSpawnEnv(recipe, recipe.homePath);
@@ -27763,7 +27932,7 @@ var createAcpInstance = (config2, host) => {
27763
27932
  },
27764
27933
  interruptTurn: async (threadId, turnId) => {
27765
27934
  const booked = threads.get(threadId);
27766
- if (booked === void 0) return;
27935
+ if (booked === void 0 || booked.closing) return;
27767
27936
  host.log.info("interrupt turn", { threadId, turnId });
27768
27937
  await booked.executor.cancel(turnId);
27769
27938
  await yieldTick();
@@ -27804,25 +27973,21 @@ var createAcpInstance = (config2, host) => {
27804
27973
  updatedAt: nowIso()
27805
27974
  };
27806
27975
  },
27807
- stopSession: async (threadId) => {
27808
- const booked = threads.get(threadId);
27809
- if (booked === void 0) return;
27810
- threads.delete(threadId);
27811
- await stopBooked(booked);
27812
- },
27976
+ stopSession: (threadId) => stopSession(threadId),
27813
27977
  readThread: async (threadId) => {
27814
27978
  const booked = requireThread(threadId);
27815
27979
  return { threadId, turns: booked.turns };
27816
27980
  },
27817
27981
  events,
27818
27982
  continuationKey: acpContinuationKey(recipe),
27819
- dispose: async () => {
27820
- if (disposed) return;
27983
+ dispose: () => {
27984
+ if (disposeTask) return disposeTask;
27821
27985
  disposed = true;
27822
- const booked = [...threads.values()];
27823
- threads.clear();
27824
- await Promise.all(booked.map((entry) => stopBooked(entry)));
27825
- events.close();
27986
+ disposeTask = (async () => {
27987
+ await stopAll();
27988
+ events.close();
27989
+ })();
27990
+ return disposeTask;
27826
27991
  }
27827
27992
  };
27828
27993
  return instance;
@@ -27840,7 +28005,7 @@ var executors = [
27840
28005
  ];
27841
28006
  var index_default = definePlugin({
27842
28007
  id: "agent-acp",
27843
- apiVersion: 14,
28008
+ apiVersion: 16,
27844
28009
  executors,
27845
28010
  activate(ctx) {
27846
28011
  ctx.log.info("generic ACP executor ready");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ycodium-ai/agent-acp",
3
- "version": "0.2.3130",
3
+ "version": "0.2.3168",
4
4
  "description": "Generic ACP executor: user-supplied CLI over Agent Client Protocol stdio.",
5
5
  "keywords": [
6
6
  "ycodium",
@@ -21,9 +21,9 @@
21
21
  "@types/node": "24.12.4",
22
22
  "esbuild": "0.28.1",
23
23
  "vite-plus": "0.2.2",
24
- "ycodium-acp-executor": "0.2.3130",
25
- "@ycodium-ai/plugin-api": "0.2.3130",
26
- "ycodium-executor-testing": "0.0.0"
24
+ "ycodium-executor-testing": "0.0.0",
25
+ "ycodium-acp-executor": "0.2.3168",
26
+ "@ycodium-ai/plugin-api": "0.2.3168"
27
27
  },
28
28
  "ycodium": {
29
29
  "plugin": "./dist/index.js",