@lucascouts/claude-agent-acp-plus 0.24.0 → 0.25.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,4 +1,5 @@
1
1
  import { applyClaudePermissionSelection, parseClaudePermissionSelection } from "./effects.js";
2
+ import { PERMISSION_OPTION_ID } from "./options/shared.js";
2
3
  /** Decode, validate, and interpret an ACP response exactly once. */
3
4
  export function decodeClaudePermissionResponse(response, toolName, input, toolUseID, offeredOptions, durableChangeSet) {
4
5
  const selection = parseClaudePermissionSelection(response, toolName);
@@ -17,3 +18,21 @@ export function decodeClaudePermissionResponse(response, toolName, input, toolUs
17
18
  ...(selection.contextResetMode ? { contextResetMode: selection.contextResetMode } : {}),
18
19
  };
19
20
  }
21
+ /**
22
+ * Map the answer to a classifier-denial request. Only the three ids that
23
+ * request offers are accepted; anything else is an error, never an approval.
24
+ */
25
+ export function decodeClassifierEscalationResponse(response) {
26
+ if (response.outcome.outcome === "cancelled")
27
+ return "cancelled";
28
+ switch (response.outcome.optionId) {
29
+ case PERMISSION_OPTION_ID.classifierAllowOnce:
30
+ return "once";
31
+ case PERMISSION_OPTION_ID.classifierAllowSession:
32
+ return "session";
33
+ case PERMISSION_OPTION_ID.reject:
34
+ return "reject";
35
+ default:
36
+ throw new Error(`Classifier escalation option was not offered: ${response.outcome.optionId}`);
37
+ }
38
+ }
@@ -19,6 +19,12 @@
19
19
  * progress, at once on an ending, and once more after a burst so the latest
20
20
  * values are not lost.
21
21
  *
22
+ * A foreground shell task (`local_bash`, not backgrounded) is held back for
23
+ * `FOREGROUND_SHELL_HOLD_MS`: Claude Code registers every shell command as a
24
+ * task, so only one still running after the hold, or moved to the background,
25
+ * is published. One that ends while held is dropped without a trace. The feed
26
+ * stays clock-driven: the publisher's timer calls `promoteDue()`.
27
+ *
22
28
  * Neither class ever copies a command line (`SDKTaskStartedMessage.prompt`)
23
29
  * into an entry or a log line.
24
30
  */
@@ -50,7 +56,9 @@ export type TasksSnapshot = {
50
56
  tasks: TaskEntry[];
51
57
  };
52
58
  /** What a message did to the table; the publisher decides timing from it. */
53
- export type TaskChange = "none" | "progress" | "terminal";
59
+ export type TaskChange = "none" | "progress" | "terminal" | "held";
60
+ /** How long a foreground shell task is held back before it is published. */
61
+ export declare const FOREGROUND_SHELL_HOLD_MS = 5000;
54
62
  /** How long a running background task may be missing from the level before
55
63
  * it is ended as `interrupted`. The level and the bookends race (the level
56
64
  * may arrive first), so a real ending gets this long to land. */
@@ -85,8 +93,21 @@ export declare class TaskFeed {
85
93
  private readonly snapshotLives;
86
94
  /** task id → time after which a missing bookend means `interrupted`. */
87
95
  private readonly deadlines;
96
+ /** Foreground shell tasks not published yet: see `promoteDue`. */
97
+ private readonly held;
98
+ /** Shell task ids labelled by their tool call: a patch never relabels them. */
99
+ private readonly toolDescribed;
88
100
  constructor(clock: () => number, log?: TaskFeedLog, sessionId?: string);
89
- onStarted(msg: SDKTaskStartedMessage): TaskChange;
101
+ /**
102
+ * `toolDescription` is the spawning tool call's own `description` input. A
103
+ * shell task carries it when it is non-empty; otherwise the SDK's, which
104
+ * for a shell is the command text. Other task types keep the SDK's.
105
+ */
106
+ onStarted(msg: SDKTaskStartedMessage, toolDescription?: string): TaskChange;
107
+ /** Publishes every held shell task whose hold has run out. */
108
+ promoteDue(): TaskChange;
109
+ /** When the earliest held shell task is due for promotion; null if none is held. */
110
+ nextHoldDue(): number | null;
90
111
  onProgress(msg: SDKTaskProgressMessage): TaskChange;
91
112
  onUpdated(msg: SDKTaskUpdatedMessage): TaskChange;
92
113
  onNotification(msg: SDKTaskNotificationMessage): TaskChange;
@@ -102,7 +123,8 @@ export declare class TaskFeed {
102
123
  nextReconcileAt(): number | null;
103
124
  /** Ends as `interrupted` every task whose deadline passed with no bookend. */
104
125
  reconcile(now: number): TaskChange;
105
- /** The CLI process restarted: whatever was running did not survive it. */
126
+ /** The CLI process restarted: whatever was running did not survive it. A
127
+ * held shell task was never shown, so it is dropped rather than ended. */
106
128
  onProcessRestart(): TaskChange;
107
129
  /** True when the feed holds `taskId` as running — the stop request's gate. */
108
130
  isRunning(taskId: string): boolean;
@@ -116,6 +138,15 @@ export declare class TaskFeed {
116
138
  * dropped; the ids are remembered so a late bookend cannot resurrect them. */
117
139
  markPublished(endedIds: string[]): void;
118
140
  private running;
141
+ /** Puts a running entry into the table. A known id starting again is a new
142
+ * life of the same task — a resumed subagent is re-registered under its
143
+ * agent id. It replaces the ended entry, published or not: the running
144
+ * state supersedes an ending the client has not seen yet, and a published
145
+ * id must be shown again. */
146
+ private admit;
147
+ /** Applies a `task_updated` patch's label and placement; a label taken from
148
+ * the tool call is never replaced. */
149
+ private patch;
119
150
  private end;
120
151
  }
121
152
  export type PublisherTimers = {
@@ -139,6 +170,9 @@ export declare class TaskFeedPublisher {
139
170
  private pending;
140
171
  private reconcileTimer;
141
172
  private reconcileAt;
173
+ /** Promotes held foreground shells: armed at the feed's `nextHoldDue()`. */
174
+ private holdTimer;
175
+ private holdAt;
142
176
  private closed;
143
177
  /** Set by `dispose({ flush: true })`: a publish queued behind an in-flight
144
178
  * send still goes out once — a retirement's `interrupted` endings. */
@@ -152,6 +186,10 @@ export declare class TaskFeedPublisher {
152
186
  private settle;
153
187
  private arm;
154
188
  private cancelPending;
189
+ /** One timer for every held shell, at the earliest due time. A held shell
190
+ * that ended early leaves the timer armed: it fires, promotes nothing, and
191
+ * re-arms for the next one still held. */
192
+ private armHold;
155
193
  private armReconcile;
156
194
  }
157
195
  //# sourceMappingURL=task-feed.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"task-feed.d.ts","sourceRoot":"","sources":["../src/task-feed.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;GAuBG;AAEH,OAAO,KAAK,EACV,0BAA0B,EAC1B,sBAAsB,EACtB,qBAAqB,EACrB,qBAAqB,EACtB,MAAM,gCAAgC,CAAC;AAExC,2EAA2E;AAC3E,eAAO,MAAM,cAAc,kBAAkB,CAAC;AAE9C,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,WAAW,GAAG,QAAQ,GAAG,SAAS,GAAG,aAAa,CAAC;AAExF,MAAM,MAAM,SAAS,GAAG;IAAE,MAAM,EAAE,MAAM,CAAC;IAAC,QAAQ,EAAE,MAAM,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,CAAC;AAEjF,0EAA0E;AAC1E,MAAM,MAAM,SAAS,GAAG;IACtB,EAAE,EAAE,MAAM,CAAC;IACX,UAAU,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,IAAI,EAAE,MAAM,CAAC;IACb,WAAW,EAAE,MAAM,CAAC;IACpB,UAAU,EAAE,OAAO,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,UAAU,CAAC;IACnB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;IACvB,KAAK,EAAE,SAAS,GAAG,IAAI,CAAC;IACxB,QAAQ,EAAE,MAAM,GAAG,IAAI,CAAC;IACxB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB,CAAC;AAEF,MAAM,MAAM,aAAa,GAAG;IAAE,KAAK,EAAE,SAAS,EAAE,CAAA;CAAE,CAAC;AAEnD,6EAA6E;AAC7E,MAAM,MAAM,UAAU,GAAG,MAAM,GAAG,UAAU,GAAG,UAAU,CAAC;AAE1D;;kEAEkE;AAClE,eAAO,MAAM,kBAAkB,OAAQ,CAAC;AAExC,iEAAiE;AACjE,eAAO,MAAM,mBAAmB,OAAQ,CAAC;AAEzC,MAAM,MAAM,WAAW,GAAG;IACxB,0EAA0E;IAC1E,KAAK,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;IAClC,oEAAoE;IACpE,IAAI,CAAC,EAAE,CAAC,OAAO,EAAE,MAAM,KAAK,IAAI,CAAC;CAClC,CAAC;AAeF;;;;;GAKG;AACH,wBAAgB,eAAe,CAC7B,MAAM,EAAE,MAAM,GAAG,SAAS,EAC1B,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,UAAU,EAAE,SAAS,CAAC,GAAG,IAAI,CAavC;AAmBD,qBAAa,QAAQ;IAcjB,OAAO,CAAC,QAAQ,CAAC,KAAK;IACtB,OAAO,CAAC,QAAQ,CAAC,GAAG;IACpB,OAAO,CAAC,QAAQ,CAAC,SAAS;IAf5B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAgC;IACtD,iFAAiF;IACjF,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqB;IAC/C,4EAA4E;IAC5E,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAqB;IAC7C,yDAAyD;IACzD,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA6B;IACnD,+DAA+D;IAC/D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAA6B;IAC3D,wEAAwE;IACxE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA6B;gBAGpC,KAAK,EAAE,MAAM,MAAM,EACnB,GAAG,GAAE,WAAgB,EACrB,SAAS,SAAK;IAGjC,SAAS,CAAC,GAAG,EAAE,qBAAqB,GAAG,UAAU;IA6BjD,UAAU,CAAC,GAAG,EAAE,sBAAsB,GAAG,UAAU;IASnD,SAAS,CAAC,GAAG,EAAE,qBAAqB,GAAG,UAAU;IAajD,cAAc,CAAC,GAAG,EAAE,0BAA0B,GAAG,UAAU;IAW3D;;;;;OAKG;IACH,OAAO,CAAC,KAAK,EAAE;QAAE,OAAO,EAAE,MAAM,CAAA;KAAE,EAAE,GAAG,UAAU;IAWjD,eAAe,IAAI,MAAM,GAAG,IAAI;IAMhC,8EAA8E;IAC9E,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,UAAU;IAiBlC,0EAA0E;IAC1E,gBAAgB,IAAI,UAAU;IAU9B,8EAA8E;IAC9E,SAAS,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO;IAIlC;8CAC0C;IAC1C,QAAQ,IAAI;QAAE,QAAQ,EAAE,aAAa,CAAC;QAAC,QAAQ,EAAE,MAAM,EAAE,CAAA;KAAE;IAU3D;mFAC+E;IAC/E,aAAa,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,IAAI;IAYvC,OAAO,CAAC,OAAO;IASf,OAAO,CAAC,GAAG;CAKZ;AAED,MAAM,MAAM,eAAe,GAAG;IAC5B,GAAG,EAAE,MAAM,MAAM,CAAC;IAClB,UAAU,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,EAAE,EAAE,MAAM,KAAK,OAAO,CAAC;IACpD,YAAY,EAAE,CAAC,MAAM,EAAE,GAAG,KAAK,IAAI,CAAC;CACrC,CAAC;AAQF,qBAAa,iBAAiB;IAkB1B,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,IAAI;IACrB,OAAO,CAAC,QAAQ,CAAC,MAAM;IACvB,OAAO,CAAC,QAAQ,CAAC,WAAW;IApB9B,OAAO,CAAC,UAAU,CAA4B;IAC9C;kEAC8D;IAC9D,OAAO,CAAC,QAAQ,CAAS;IACzB;iCAC6B;IAC7B,OAAO,CAAC,MAAM,CAAsB;IACpC,0EAA0E;IAC1E,OAAO,CAAC,OAAO,CAAiB;IAChC,OAAO,CAAC,cAAc,CAAiB;IACvC,OAAO,CAAC,WAAW,CAAuB;IAC1C,OAAO,CAAC,MAAM,CAAS;IACvB;2EACuE;IACvE,OAAO,CAAC,aAAa,CAAS;gBAGX,IAAI,EAAE,QAAQ,EACd,IAAI,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,OAAO,CAAC,IAAI,CAAC,EAChD,MAAM,GAAE,eAA4B,EACpC,WAAW,GAAE,CAAC,KAAK,EAAE,OAAO,KAAK,IAAe;IAGnE,MAAM,CAAC,MAAM,EAAE,UAAU,GAAG,IAAI;IAYhC,OAAO,CAAC,OAAO,GAAE;QAAE,KAAK,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,IAAI;IAShD,OAAO,CAAC,UAAU;IAgClB,OAAO,CAAC,MAAM;IAsBd,OAAO,CAAC,GAAG;IAOX,OAAO,CAAC,aAAa;IAKrB,OAAO,CAAC,YAAY;CAerB"}
1
+ {"version":3,"file":"task-feed.d.ts","sourceRoot":"","sources":["../src/task-feed.ts"],"names":[],"mappings":"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"}
package/dist/task-feed.js CHANGED
@@ -19,11 +19,19 @@
19
19
  * progress, at once on an ending, and once more after a burst so the latest
20
20
  * values are not lost.
21
21
  *
22
+ * A foreground shell task (`local_bash`, not backgrounded) is held back for
23
+ * `FOREGROUND_SHELL_HOLD_MS`: Claude Code registers every shell command as a
24
+ * task, so only one still running after the hold, or moved to the background,
25
+ * is published. One that ends while held is dropped without a trace. The feed
26
+ * stays clock-driven: the publisher's timer calls `promoteDue()`.
27
+ *
22
28
  * Neither class ever copies a command line (`SDKTaskStartedMessage.prompt`)
23
29
  * into an entry or a log line.
24
30
  */
25
31
  /** The `session_info_update._meta` key the snapshot is published under. */
26
32
  export const TASKS_META_KEY = "_claude/tasks";
33
+ /** How long a foreground shell task is held back before it is published. */
34
+ export const FOREGROUND_SHELL_HOLD_MS = 5000;
27
35
  /** How long a running background task may be missing from the level before
28
36
  * it is ended as `interrupted`. The level and the bookends race (the level
29
37
  * may arrive first), so a real ending gets this long to land. */
@@ -92,29 +100,39 @@ export class TaskFeed {
92
100
  snapshotLives = new Map();
93
101
  /** task id → time after which a missing bookend means `interrupted`. */
94
102
  deadlines = new Map();
103
+ /** Foreground shell tasks not published yet: see `promoteDue`. */
104
+ held = new Map();
105
+ /** Shell task ids labelled by their tool call: a patch never relabels them. */
106
+ toolDescribed = new Set();
95
107
  constructor(clock, log = {}, sessionId = "") {
96
108
  this.clock = clock;
97
109
  this.log = log;
98
110
  this.sessionId = sessionId;
99
111
  }
100
- onStarted(msg) {
112
+ /**
113
+ * `toolDescription` is the spawning tool call's own `description` input. A
114
+ * shell task carries it when it is non-empty; otherwise the SDK's, which
115
+ * for a shell is the command text. Other task types keep the SDK's.
116
+ */
117
+ onStarted(msg, toolDescription) {
101
118
  if (msg.ambient) {
102
119
  this.ambient.add(msg.task_id);
103
120
  return "none";
104
121
  }
105
- if (this.tasks.get(msg.task_id)?.status === "running")
122
+ if (this.tasks.get(msg.task_id)?.status === "running" || this.held.has(msg.task_id)) {
106
123
  return "none";
107
- // A known id starting again is a new life of the same task — a resumed
108
- // subagent is re-registered under its agent id. It replaces the ended
109
- // entry, published or not: the running state supersedes an ending the
110
- // client has not seen yet, and a published id must be shown again.
111
- this.published.delete(msg.task_id);
112
- this.lives.set(msg.task_id, (this.lives.get(msg.task_id) ?? 0) + 1);
113
- this.tasks.set(msg.task_id, {
124
+ }
125
+ const shell = msg.task_type === "local_bash";
126
+ const fromTool = shell && typeof toolDescription === "string" && toolDescription.trim() !== "";
127
+ if (fromTool)
128
+ this.toolDescribed.add(msg.task_id);
129
+ else
130
+ this.toolDescribed.delete(msg.task_id);
131
+ const entry = {
114
132
  id: msg.task_id,
115
133
  toolCallId: msg.tool_use_id ?? null,
116
134
  type: normaliseType(msg.task_type),
117
- description: msg.description,
135
+ description: fromTool ? toolDescription : msg.description,
118
136
  background: msg.is_backgrounded ?? false,
119
137
  depth: msg.spawn_depth ?? 0,
120
138
  status: "running",
@@ -123,27 +141,71 @@ export class TaskFeed {
123
141
  usage: null,
124
142
  lastTool: null,
125
143
  summary: null,
126
- });
144
+ };
145
+ if (shell && msg.is_backgrounded !== true) {
146
+ this.held.set(msg.task_id, entry);
147
+ return "held";
148
+ }
149
+ this.admit(entry);
127
150
  return "progress";
128
151
  }
152
+ /** Publishes every held shell task whose hold has run out. */
153
+ promoteDue() {
154
+ const now = this.clock();
155
+ let change = "none";
156
+ for (const [id, entry] of this.held) {
157
+ if (entry.startedAt + FOREGROUND_SHELL_HOLD_MS > now)
158
+ continue;
159
+ this.held.delete(id);
160
+ this.admit(entry);
161
+ change = "progress";
162
+ }
163
+ return change;
164
+ }
165
+ /** When the earliest held shell task is due for promotion; null if none is held. */
166
+ nextHoldDue() {
167
+ let next = null;
168
+ for (const entry of this.held.values()) {
169
+ const due = entry.startedAt + FOREGROUND_SHELL_HOLD_MS;
170
+ if (next === null || due < next)
171
+ next = due;
172
+ }
173
+ return next;
174
+ }
129
175
  onProgress(msg) {
130
- const task = this.running(msg.task_id, "task_progress");
176
+ const heldEntry = this.held.get(msg.task_id);
177
+ const task = heldEntry ?? this.running(msg.task_id, "task_progress");
131
178
  if (!task)
132
179
  return "none";
133
180
  task.usage = usageOf(msg.usage);
134
181
  task.lastTool = msg.last_tool_name ?? task.lastTool;
135
182
  task.summary = msg.summary ?? task.summary;
136
- return "progress";
183
+ // Kept for the promotion, but nobody has been shown the task yet.
184
+ return heldEntry ? "none" : "progress";
137
185
  }
138
186
  onUpdated(msg) {
187
+ const heldEntry = this.held.get(msg.task_id);
188
+ if (heldEntry) {
189
+ if (msg.patch.is_backgrounded === true) {
190
+ // Moved to the background: published at once, then patched as usual.
191
+ this.held.delete(msg.task_id);
192
+ this.admit(heldEntry);
193
+ }
194
+ else {
195
+ if (normaliseStatus(msg.patch.status)) {
196
+ this.held.delete(msg.task_id);
197
+ }
198
+ else {
199
+ this.patch(heldEntry, msg.patch);
200
+ }
201
+ return "none";
202
+ }
203
+ }
139
204
  const task = this.running(msg.task_id, "task_updated");
140
205
  if (!task)
141
206
  return "none";
142
207
  const ending = normaliseStatus(msg.patch.status);
143
- if (msg.patch.description !== undefined)
144
- task.description = msg.patch.description;
145
- if (msg.patch.is_backgrounded !== undefined)
146
- task.background = msg.patch.is_backgrounded;
208
+ this.patch(task, msg.patch);
147
209
  if (ending) {
148
210
  this.end(task, ending);
149
211
  return "terminal";
@@ -151,6 +213,9 @@ export class TaskFeed {
151
213
  return "progress";
152
214
  }
153
215
  onNotification(msg) {
216
+ // A held shell task that ends was never shown: its ending is not news.
217
+ if (this.held.delete(msg.task_id))
218
+ return "none";
154
219
  const task = this.running(msg.task_id, "task_notification");
155
220
  if (!task)
156
221
  return "none";
@@ -205,8 +270,10 @@ export class TaskFeed {
205
270
  }
206
271
  return change;
207
272
  }
208
- /** The CLI process restarted: whatever was running did not survive it. */
273
+ /** The CLI process restarted: whatever was running did not survive it. A
274
+ * held shell task was never shown, so it is dropped rather than ended. */
209
275
  onProcessRestart() {
276
+ this.held.clear();
210
277
  let change = "none";
211
278
  for (const task of this.tasks.values()) {
212
279
  if (task.status !== "running")
@@ -256,6 +323,25 @@ export class TaskFeed {
256
323
  }
257
324
  return undefined;
258
325
  }
326
+ /** Puts a running entry into the table. A known id starting again is a new
327
+ * life of the same task — a resumed subagent is re-registered under its
328
+ * agent id. It replaces the ended entry, published or not: the running
329
+ * state supersedes an ending the client has not seen yet, and a published
330
+ * id must be shown again. */
331
+ admit(entry) {
332
+ this.published.delete(entry.id);
333
+ this.lives.set(entry.id, (this.lives.get(entry.id) ?? 0) + 1);
334
+ this.tasks.set(entry.id, entry);
335
+ }
336
+ /** Applies a `task_updated` patch's label and placement; a label taken from
337
+ * the tool call is never replaced. */
338
+ patch(task, patch) {
339
+ if (patch.description !== undefined && !this.toolDescribed.has(task.id)) {
340
+ task.description = patch.description;
341
+ }
342
+ if (patch.is_backgrounded !== undefined)
343
+ task.background = patch.is_backgrounded;
344
+ }
259
345
  end(task, status, at = this.clock()) {
260
346
  task.status = status;
261
347
  task.endedAt = at;
@@ -283,6 +369,9 @@ export class TaskFeedPublisher {
283
369
  pending = null;
284
370
  reconcileTimer = null;
285
371
  reconcileAt = null;
372
+ /** Promotes held foreground shells: armed at the feed's `nextHoldDue()`. */
373
+ holdTimer = null;
374
+ holdAt = null;
286
375
  closed = false;
287
376
  /** Set by `dispose({ flush: true })`: a publish queued behind an in-flight
288
377
  * send still goes out once — a retirement's `interrupted` endings. */
@@ -306,6 +395,9 @@ export class TaskFeedPublisher {
306
395
  else if (this.pending === null)
307
396
  this.arm(wait);
308
397
  }
398
+ else if (change === "held") {
399
+ this.armHold();
400
+ }
309
401
  this.armReconcile();
310
402
  }
311
403
  dispose(options = {}) {
@@ -316,6 +408,10 @@ export class TaskFeedPublisher {
316
408
  this.timers.clearTimeout(this.reconcileTimer);
317
409
  this.reconcileTimer = null;
318
410
  this.reconcileAt = null;
411
+ if (this.holdTimer !== null)
412
+ this.timers.clearTimeout(this.holdTimer);
413
+ this.holdTimer = null;
414
+ this.holdAt = null;
319
415
  }
320
416
  publishNow() {
321
417
  this.cancelPending();
@@ -382,6 +478,40 @@ export class TaskFeedPublisher {
382
478
  this.timers.clearTimeout(this.pending);
383
479
  this.pending = null;
384
480
  }
481
+ /** One timer for every held shell, at the earliest due time. A held shell
482
+ * that ended early leaves the timer armed: it fires, promotes nothing, and
483
+ * re-arms for the next one still held. */
484
+ armHold() {
485
+ const at = this.feed.nextHoldDue();
486
+ if (at === null)
487
+ return;
488
+ if (this.holdAt !== null && this.holdAt <= at)
489
+ return;
490
+ if (this.holdTimer !== null)
491
+ this.timers.clearTimeout(this.holdTimer);
492
+ this.holdAt = at;
493
+ this.holdTimer = this.timers.setTimeout(() => {
494
+ this.holdTimer = null;
495
+ this.holdAt = null;
496
+ if (this.closed)
497
+ return;
498
+ // Outside the SDK loop nobody handles an error: a failed send is
499
+ // caught in `publishNow`, and anything else thrown is reported here.
500
+ try {
501
+ this.notify(this.feed.promoteDue());
502
+ }
503
+ catch (error) {
504
+ try {
505
+ this.onSendError(error);
506
+ }
507
+ catch {
508
+ // Re-arming matters more than the log line.
509
+ }
510
+ }
511
+ if (!this.closed)
512
+ this.armHold();
513
+ }, Math.max(0, at - this.timers.now()));
514
+ }
385
515
  armReconcile() {
386
516
  const at = this.feed.nextReconcileAt();
387
517
  if (at === null || at === this.reconcileAt)
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.24.0",
6
+ "version": "0.25.1",
7
7
  "description": "Claude Code in Zed and other ACP clients with the VS Code extension's UX — multi-select questions, refusal consent, live quota windows, background tasks, Ultracode effort and session notices. Fork of claude-agent-acp.",
8
8
  "main": "dist/lib.js",
9
9
  "types": "dist/lib.d.ts",