@parall/claude-agent 1.58.2 → 1.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -57,6 +57,8 @@ export declare class ClaudeCodeAdapter implements DispatchAdapter {
57
57
  enqueueDuringDispatch(sessionKey: string, body: string, inputLifecycle?: DispatchInputLifecycle): boolean;
58
58
  abortDispatch(sessionKey: string): void;
59
59
  hasPendingInjections(sessionKey: string): boolean;
60
+ hasUnsettledInjections(sessionKey: string): boolean;
61
+ acknowledgeDiscardedInjection(sessionKey: string, deliveryKey: string): void;
60
62
  dispatch({ event, bodyForAgent, sessionKey, context, inputLifecycle, noteActivity, }: DispatchOpts): AsyncIterable<RuntimeEvent>;
61
63
  getBranchPoint(_sessionKey: string): string | undefined;
62
64
  getSessionHistoryPath(sessionKey: string): string | undefined;
@@ -1 +1 @@
1
- {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,eAAe,EACf,eAAe,EACf,sBAAsB,EACtB,YAAY,EACZ,QAAQ,EAER,YAAY,EACb,MAAM,oBAAoB,CAAC;AAK5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAOrD,OAAO,KAAK,EAAuB,oBAAoB,EAAE,MAAM,sBAAsB,CAAC;AACtF,OAAO,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AAG/C,OAAO,EAAE,aAAa,EAAE,CAAC;AAEzB,KAAK,wBAAwB,GAAG,IAAI,CAClC,iBAAiB,EACf,gBAAgB,GAChB,aAAa,GACb,cAAc,GACd,oBAAoB,GACpB,WAAW,GACX,YAAY,GACZ,iBAAiB,GACjB,OAAO,GACP,gBAAgB,GAChB,cAAc,CACjB,GAAG;IACF,cAAc,EAAE,oBAAoB,CAAC;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,mDAAmD;IACnD,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,kFAAkF;IAClF,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,yBAAyB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC3D,iDAAiD;IACjD,wBAAwB,CAAC,EAAE,CAAC,UAAU,EAAE,MAAM,KAAK,MAAM,CAAC;IAC1D;;;;;;;OAOG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,CAAC;AAqCF,qBAAa,iBAAkB,YAAW,eAAe;IAW3C,OAAO,CAAC,QAAQ,CAAC,IAAI;IAVjC,QAAQ,CAAC,kBAAkB,EAAG,UAAU,CAAU;IAClD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmC;IAC7D,OAAO,CAAC,eAAe,CAAC,CAAgB;IACxC,OAAO,CAAC,qBAAqB,CAAC,CAAsB;IACpD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,cAAc,CAAqB;IAC3C,OAAO,CAAC,UAAU,CAAqB;gBAEV,IAAI,EAAE,wBAAwB;IAM3D,IAAI,YAAY,IAAI,MAAM,GAAG,SAAS,CAErC;IACD,IAAI,aAAa,IAAI,MAAM,GAAG,SAAS,CAEtC;IACD,YAAY,CAAC,MAAM,EAAE;QACnB,KAAK,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACtB,MAAM,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QACvB,aAAa,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;QAC9B,SAAS,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;KAC3B,GAAG,IAAI;IAqBR,qBAAqB,IAAI,IAAI;IAM7B,qBAAqB,CACnB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,cAAc,CAAC,EAAE,sBAAsB,GACtC,OAAO;IA2BV,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAcvC,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAM1C,QAAQ,CAAC,EACd,KAAK,EACL,YAAY,EACZ,UAAU,EACV,OAAO,EACP,cAAc,EACd,YAAY,GACb,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IAiE7C,cAAc,CAAC,WAAW,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAIvD,qBAAqB,CAAC,UAAU,EAAE,MAAM,GAAG,MAAM,GAAG,SAAS;IAc7D,WAAW,CAAC,EAAE,UAAU,EAAE,EAAE,QAAQ;IAIpC,WAAW,CAAC,EAAE,IAAI,EAAE,EAAE,eAAe;IAQrC,cAAc,IAAI,IAAI;IAOhB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;YAUhB,OAAO;IAuCtB,OAAO,CAAC,uBAAuB;IAa/B;;;;;OAKG;YACW,sBAAsB;IAwCpC;;;;;OAKG;YACY,eAAe;IA4L9B,OAAO,CAAC,aAAa;IAmCrB,OAAO,CAAC,YAAY;IA8DpB,OAAO,CAAC,WAAW;IAYnB,OAAO,CAAC,eAAe;IAiBvB,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,gBAAgB;IAaxB,OAAO,CAAC,SAAS;IAwCjB,OAAO,CAAC,uBAAuB;CAmBhC"}
1
+ {"version":3,"file":"dispatch.d.ts","sourceRoot":"","sources":["../src/dispatch.ts"],"names":[],"mappings":"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"}
package/dist/dispatch.js CHANGED
@@ -131,6 +131,27 @@ export class ClaudeCodeAdapter {
131
131
  return false;
132
132
  return state.inputs.hasPendingInjections();
133
133
  }
134
+ hasUnsettledInjections(sessionKey) {
135
+ const state = this.processes.get(sessionKey);
136
+ if (!state)
137
+ return false;
138
+ return state.inputs.hasUnsettledInjections();
139
+ }
140
+ acknowledgeDiscardedInjection(sessionKey, deliveryKey) {
141
+ const state = this.processes.get(sessionKey);
142
+ if (!state)
143
+ return;
144
+ state.inputs.discardBookkeeping(deliveryKey);
145
+ // The discarded copy may have been the last thing a lazy restart was
146
+ // waiting on. Unlike the consume path's tail, the discarded delivery may
147
+ // still be RUNNING (non-terminal): killing the process now would fail a
148
+ // live input, so the restart also waits for every injection to settle.
149
+ if (state.needsRestart &&
150
+ !state.inputs.hasPendingInjections() &&
151
+ !state.inputs.hasUnsettledInjections()) {
152
+ this.killProcess(sessionKey, state);
153
+ }
154
+ }
134
155
  async *dispatch({ event, bodyForAgent, sessionKey, context, inputLifecycle, noteActivity, }) {
135
156
  const deliveryKey = inputLifecycle?.deliveryKey ?? event.dispatchEventId ?? event.messageId;
136
157
  const existingState = this.processes.get(sessionKey);
@@ -150,7 +171,9 @@ export class ClaudeCodeAdapter {
150
171
  finally {
151
172
  existingState.inputs.remove(injected);
152
173
  }
153
- if (existingState.needsRestart && !existingState.inputs.hasPendingInjections()) {
174
+ if (existingState.needsRestart &&
175
+ !existingState.inputs.hasPendingInjections() &&
176
+ !existingState.inputs.hasUnsettledInjections()) {
154
177
  this.killProcess(sessionKey, existingState);
155
178
  }
156
179
  return;
@@ -475,7 +498,9 @@ export class ClaudeCodeAdapter {
475
498
  if (lastResultMeta) {
476
499
  yield classifyClaudeTurn(lastResultMeta, noticeTexts);
477
500
  }
478
- if (state.needsRestart && !state.inputs.hasPendingInjections()) {
501
+ if (state.needsRestart &&
502
+ !state.inputs.hasPendingInjections() &&
503
+ !state.inputs.hasUnsettledInjections()) {
479
504
  this.killProcess(sessionKey, state);
480
505
  }
481
506
  }
@@ -30,8 +30,29 @@ export declare class ClaudeInputRegistry {
30
30
  findOverlapping(dispatchEventIds: string[]): ClaudeInputDelivery | undefined;
31
31
  values(): IterableIterator<ClaudeInputDelivery>;
32
32
  hasPendingInjections(): boolean;
33
+ /**
34
+ * Injected inputs whose lifecycle has NOT reached a terminal state. A
35
+ * terminal delivery still counts as pending (its buffered copy owes frame
36
+ * boundary bookkeeping) but owes the server nothing — the lane complete
37
+ * defers on this predicate, never on hasPendingInjections, so a bookkeeping
38
+ * copy that never drains cannot hold the lane open past its lease. Drained
39
+ * is deliberately NOT part of this predicate: a discarded bookkeeping copy
40
+ * (drained, non-terminal) is still a running input — completing its lane
41
+ * would release the member for redrive mid-processing.
42
+ */
43
+ hasUnsettledInjections(): boolean;
33
44
  register(deliveryKey: string, lifecycle: DispatchInputLifecycle | undefined, injected: boolean): ClaudeInputDelivery;
34
45
  remove(delivery: ClaudeInputDelivery): void;
46
+ /**
47
+ * The buffered copy backing this delivery was discarded without a
48
+ * dispatch (already rendered into a frame the model saw), so the normal
49
+ * bookkeeping drain never comes. Its frame-boundary obligation is void:
50
+ * mark it drained so nothing waits on it, and drop it once terminal. A
51
+ * still-running input keeps its registration — byCommand must keep routing
52
+ * its lifecycle frames — and is dropped when its terminal state arrives
53
+ * (see apply/fail).
54
+ */
55
+ discardBookkeeping(deliveryKey: string): void;
35
56
  apply(delivery: ClaudeInputDelivery, state: 'queued' | 'started' | 'completed' | 'cancelled' | 'discarded'): Promise<void>;
36
57
  fail(delivery: ClaudeInputDelivery): Promise<void>;
37
58
  failBestEffort(delivery: ClaudeInputDelivery, log?: GatewayLogger): Promise<void>;
@@ -1 +1 @@
1
- {"version":3,"file":"input-lifecycle.d.ts","sourceRoot":"","sources":["../src/input-lifecycle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEnG,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC9C,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,YAAY,EAAE,OAAO,CAAC;IACtB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF;;;;GAIG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0C;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;IAEpE,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAI9D,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAIlE,eAAe,CAAC,gBAAgB,EAAE,MAAM,EAAE,GAAG,mBAAmB,GAAG,SAAS;IAU5E,MAAM,IAAI,gBAAgB,CAAC,mBAAmB,CAAC;IAI/C,oBAAoB,IAAI,OAAO;IAI/B,QAAQ,CACN,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,sBAAsB,GAAG,SAAS,EAC7C,QAAQ,EAAE,OAAO,GAChB,mBAAmB;IAiBtB,MAAM,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IASrC,KAAK,CACT,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACpE,OAAO,CAAC,IAAI,CAAC;IAsBV,IAAI,CAAC,QAAQ,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAiBlD,cAAc,CAAC,QAAQ,EAAE,mBAAmB,EAAE,GAAG,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAUjF,iBAAiB,CAAC,GAAG,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;CAO5D"}
1
+ {"version":3,"file":"input-lifecycle.d.ts","sourceRoot":"","sources":["../src/input-lifecycle.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,sBAAsB,EAAE,aAAa,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEnG,MAAM,MAAM,mBAAmB,GAAG;IAChC,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,SAAS,CAAC,EAAE,sBAAsB,CAAC;IACnC,QAAQ,EAAE,OAAO,CAAC;IAClB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IAC9C,aAAa,CAAC,EAAE,iBAAiB,CAAC;IAClC,YAAY,EAAE,OAAO,CAAC;IACtB;;;;;;OAMG;IACH,kBAAkB,CAAC,EAAE,OAAO,CAAC;CAC9B,CAAC;AAEF;;;;GAIG;AACH,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,KAAK,CAA0C;IAChE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAA0C;IAEpE,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAI9D,YAAY,CAAC,WAAW,EAAE,MAAM,GAAG,mBAAmB,GAAG,SAAS;IAIlE,eAAe,CAAC,gBAAgB,EAAE,MAAM,EAAE,GAAG,mBAAmB,GAAG,SAAS;IAU5E,MAAM,IAAI,gBAAgB,CAAC,mBAAmB,CAAC;IAI/C,oBAAoB,IAAI,OAAO;IAI/B;;;;;;;;;OASG;IACH,sBAAsB,IAAI,OAAO;IAIjC,QAAQ,CACN,WAAW,EAAE,MAAM,EACnB,SAAS,EAAE,sBAAsB,GAAG,SAAS,EAC7C,QAAQ,EAAE,OAAO,GAChB,mBAAmB;IAiBtB,MAAM,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAS3C;;;;;;;;OAQG;IACH,kBAAkB,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI;IAOvC,KAAK,CACT,QAAQ,EAAE,mBAAmB,EAC7B,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,GACpE,OAAO,CAAC,IAAI,CAAC;IA0BV,IAAI,CAAC,QAAQ,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAmBlD,cAAc,CAAC,QAAQ,EAAE,mBAAmB,EAAE,GAAG,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;IAUjF,iBAAiB,CAAC,GAAG,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,IAAI,CAAC;CAO5D"}
@@ -28,6 +28,19 @@ export class ClaudeInputRegistry {
28
28
  hasPendingInjections() {
29
29
  return [...this.byKey.values()].some((delivery) => delivery.injected && !delivery.drained);
30
30
  }
31
+ /**
32
+ * Injected inputs whose lifecycle has NOT reached a terminal state. A
33
+ * terminal delivery still counts as pending (its buffered copy owes frame
34
+ * boundary bookkeeping) but owes the server nothing — the lane complete
35
+ * defers on this predicate, never on hasPendingInjections, so a bookkeeping
36
+ * copy that never drains cannot hold the lane open past its lease. Drained
37
+ * is deliberately NOT part of this predicate: a discarded bookkeeping copy
38
+ * (drained, non-terminal) is still a running input — completing its lane
39
+ * would release the member for redrive mid-processing.
40
+ */
41
+ hasUnsettledInjections() {
42
+ return [...this.byKey.values()].some((delivery) => delivery.injected && !delivery.terminal);
43
+ }
31
44
  register(deliveryKey, lifecycle, injected) {
32
45
  if (this.byKey.has(deliveryKey)) {
33
46
  throw new Error(`duplicate Claude delivery key ${deliveryKey}`);
@@ -52,6 +65,23 @@ export class ClaudeInputRegistry {
52
65
  this.byCommand.delete(delivery.commandUuid);
53
66
  }
54
67
  }
68
+ /**
69
+ * The buffered copy backing this delivery was discarded without a
70
+ * dispatch (already rendered into a frame the model saw), so the normal
71
+ * bookkeeping drain never comes. Its frame-boundary obligation is void:
72
+ * mark it drained so nothing waits on it, and drop it once terminal. A
73
+ * still-running input keeps its registration — byCommand must keep routing
74
+ * its lifecycle frames — and is dropped when its terminal state arrives
75
+ * (see apply/fail).
76
+ */
77
+ discardBookkeeping(deliveryKey) {
78
+ const delivery = this.byKey.get(deliveryKey);
79
+ if (!delivery)
80
+ return;
81
+ delivery.drained = true;
82
+ if (delivery.terminal)
83
+ this.remove(delivery);
84
+ }
55
85
  async apply(delivery, state) {
56
86
  if (delivery.terminal || state === 'queued')
57
87
  return;
@@ -71,12 +101,19 @@ export class ClaudeInputRegistry {
71
101
  delivery.reportedState = 'completed';
72
102
  }
73
103
  delivery.terminal = 'completed';
104
+ // A discarded bookkeeping copy (drained without a dispatch) has no
105
+ // drain left to remove this entry — drop it at its terminal so it can
106
+ // never shadow a redelivered batch in findOverlapping.
107
+ if (delivery.drained)
108
+ this.remove(delivery);
74
109
  }
75
110
  async fail(delivery) {
76
111
  if (delivery.terminal)
77
112
  return;
78
113
  if (delivery.suppressFailReport) {
79
114
  delivery.terminal = 'settled';
115
+ if (delivery.drained)
116
+ this.remove(delivery);
80
117
  return;
81
118
  }
82
119
  try {
@@ -88,6 +125,8 @@ export class ClaudeInputRegistry {
88
125
  }
89
126
  finally {
90
127
  delivery.terminal ??= 'failed';
128
+ if (delivery.drained)
129
+ this.remove(delivery);
91
130
  }
92
131
  }
93
132
  async failBestEffort(delivery, log) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/claude-agent",
3
- "version": "1.58.2",
3
+ "version": "1.59.0",
4
4
  "description": "Claude Code bridge runtime for self-hosted Parall agents",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -25,9 +25,9 @@
25
25
  "src"
26
26
  ],
27
27
  "dependencies": {
28
- "@parall/agent-core": "1.58.2",
29
- "@parall/cli": "1.58.2",
30
- "@parall/sdk": "1.58.2"
28
+ "@parall/agent-core": "1.59.0",
29
+ "@parall/cli": "1.59.0",
30
+ "@parall/sdk": "1.59.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^22.0.0",
package/src/dispatch.ts CHANGED
@@ -212,6 +212,29 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
212
212
  return state.inputs.hasPendingInjections();
213
213
  }
214
214
 
215
+ hasUnsettledInjections(sessionKey: string): boolean {
216
+ const state = this.processes.get(sessionKey);
217
+ if (!state) return false;
218
+ return state.inputs.hasUnsettledInjections();
219
+ }
220
+
221
+ acknowledgeDiscardedInjection(sessionKey: string, deliveryKey: string): void {
222
+ const state = this.processes.get(sessionKey);
223
+ if (!state) return;
224
+ state.inputs.discardBookkeeping(deliveryKey);
225
+ // The discarded copy may have been the last thing a lazy restart was
226
+ // waiting on. Unlike the consume path's tail, the discarded delivery may
227
+ // still be RUNNING (non-terminal): killing the process now would fail a
228
+ // live input, so the restart also waits for every injection to settle.
229
+ if (
230
+ state.needsRestart &&
231
+ !state.inputs.hasPendingInjections() &&
232
+ !state.inputs.hasUnsettledInjections()
233
+ ) {
234
+ this.killProcess(sessionKey, state);
235
+ }
236
+ }
237
+
215
238
  async *dispatch({
216
239
  event,
217
240
  bodyForAgent,
@@ -242,7 +265,11 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
242
265
  } finally {
243
266
  existingState.inputs.remove(injected);
244
267
  }
245
- if (existingState.needsRestart && !existingState.inputs.hasPendingInjections()) {
268
+ if (
269
+ existingState.needsRestart &&
270
+ !existingState.inputs.hasPendingInjections() &&
271
+ !existingState.inputs.hasUnsettledInjections()
272
+ ) {
246
273
  this.killProcess(sessionKey, existingState);
247
274
  }
248
275
  return;
@@ -618,7 +645,11 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
618
645
  yield classifyClaudeTurn(lastResultMeta, noticeTexts);
619
646
  }
620
647
 
621
- if (state.needsRestart && !state.inputs.hasPendingInjections()) {
648
+ if (
649
+ state.needsRestart &&
650
+ !state.inputs.hasPendingInjections() &&
651
+ !state.inputs.hasUnsettledInjections()
652
+ ) {
622
653
  this.killProcess(sessionKey, state);
623
654
  }
624
655
  }
@@ -55,6 +55,20 @@ export class ClaudeInputRegistry {
55
55
  return [...this.byKey.values()].some((delivery) => delivery.injected && !delivery.drained);
56
56
  }
57
57
 
58
+ /**
59
+ * Injected inputs whose lifecycle has NOT reached a terminal state. A
60
+ * terminal delivery still counts as pending (its buffered copy owes frame
61
+ * boundary bookkeeping) but owes the server nothing — the lane complete
62
+ * defers on this predicate, never on hasPendingInjections, so a bookkeeping
63
+ * copy that never drains cannot hold the lane open past its lease. Drained
64
+ * is deliberately NOT part of this predicate: a discarded bookkeeping copy
65
+ * (drained, non-terminal) is still a running input — completing its lane
66
+ * would release the member for redrive mid-processing.
67
+ */
68
+ hasUnsettledInjections(): boolean {
69
+ return [...this.byKey.values()].some((delivery) => delivery.injected && !delivery.terminal);
70
+ }
71
+
58
72
  register(
59
73
  deliveryKey: string,
60
74
  lifecycle: DispatchInputLifecycle | undefined,
@@ -85,6 +99,22 @@ export class ClaudeInputRegistry {
85
99
  }
86
100
  }
87
101
 
102
+ /**
103
+ * The buffered copy backing this delivery was discarded without a
104
+ * dispatch (already rendered into a frame the model saw), so the normal
105
+ * bookkeeping drain never comes. Its frame-boundary obligation is void:
106
+ * mark it drained so nothing waits on it, and drop it once terminal. A
107
+ * still-running input keeps its registration — byCommand must keep routing
108
+ * its lifecycle frames — and is dropped when its terminal state arrives
109
+ * (see apply/fail).
110
+ */
111
+ discardBookkeeping(deliveryKey: string): void {
112
+ const delivery = this.byKey.get(deliveryKey);
113
+ if (!delivery) return;
114
+ delivery.drained = true;
115
+ if (delivery.terminal) this.remove(delivery);
116
+ }
117
+
88
118
  async apply(
89
119
  delivery: ClaudeInputDelivery,
90
120
  state: 'queued' | 'started' | 'completed' | 'cancelled' | 'discarded',
@@ -108,12 +138,17 @@ export class ClaudeInputRegistry {
108
138
  delivery.reportedState = 'completed';
109
139
  }
110
140
  delivery.terminal = 'completed';
141
+ // A discarded bookkeeping copy (drained without a dispatch) has no
142
+ // drain left to remove this entry — drop it at its terminal so it can
143
+ // never shadow a redelivered batch in findOverlapping.
144
+ if (delivery.drained) this.remove(delivery);
111
145
  }
112
146
 
113
147
  async fail(delivery: ClaudeInputDelivery): Promise<void> {
114
148
  if (delivery.terminal) return;
115
149
  if (delivery.suppressFailReport) {
116
150
  delivery.terminal = 'settled';
151
+ if (delivery.drained) this.remove(delivery);
117
152
  return;
118
153
  }
119
154
  try {
@@ -124,6 +159,7 @@ export class ClaudeInputRegistry {
124
159
  }
125
160
  } finally {
126
161
  delivery.terminal ??= 'failed';
162
+ if (delivery.drained) this.remove(delivery);
127
163
  }
128
164
  }
129
165