@parall/claude-agent 1.65.0 → 1.66.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.
@@ -89,19 +89,19 @@ export declare class ClaudeCodeAdapter implements DispatchAdapter {
89
89
  abortDispatch(sessionKey: string): void;
90
90
  hasPendingInjections(sessionKey: string): boolean;
91
91
  hasUnsettledInjections(sessionKey: string): boolean;
92
+ isInjectionLive(sessionKey: string, deliveryKey: string): boolean;
92
93
  /**
93
94
  * The gateway discarded the buffered copy of an injection without a
94
- * dispatch(). A settled injection only owes bookkeeping. One that has NOT
95
- * settled is an orphan: nothing will ever drain it, and the CLI does not
96
- * start a queued input on its own once the turn it was steered into has
97
- * ended it works its queue only when the next stdin frame arrives, which
98
- * would be the batch re-delivering this very member (prod 2026-09-03: lane
99
- * dethroned after 10 idle minutes, then "refusing duplicate stdin write"
100
- * on every retry). Withdraw it: kill the process (the session survives via
101
- * --resume), report the input failed so the server releases the member,
102
- * and let the gateway run it as a real turn.
95
+ * dispatch(). A settled injection only owes bookkeeping. A live one never
96
+ * reaches here (the gateway attaches to it via isInjectionLive); if one
97
+ * does, it is left registered byCommand must keep routing its lifecycle
98
+ * frames and its terminal state drops it (see apply/fail) and the
99
+ * anomaly is logged. Killing it is not an option: Claude Code starts a
100
+ * queued input on its own once the turn it was steered into ends, and the
101
+ * server, having recorded that `started` as delivered, would never re-run
102
+ * it (2026-09-17 prod loss).
103
103
  */
104
- acknowledgeDiscardedInjection(sessionKey: string, deliveryKey: string): Promise<DiscardedInjectionOutcome>;
104
+ acknowledgeDiscardedInjection(sessionKey: string, deliveryKey: string): DiscardedInjectionOutcome;
105
105
  dispatch({ event, bodyForAgent, sessionKey, context, inputLifecycle, noteActivity, }: DispatchOpts): AsyncIterable<RuntimeEvent>;
106
106
  getBranchPoint(_sessionKey: string): string | undefined;
107
107
  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,WAAW,EACX,aAAa,EACb,yBAAyB,EACzB,eAAe,EACf,sBAAsB,EACtB,YAAY,EACZ,QAAQ,EACR,aAAa,EACb,oBAAoB,EACpB,gBAAgB,EAChB,YAAY,EACb,MAAM,oBAAoB,CAAC;AAQ5B,OAAO,KAAK,EAAE,iBAAiB,EAAE,MAAM,aAAa,CAAC;AACrD,OAAO,EAA4B,mBAAmB,EAAE,MAAM,sBAAsB,CAAC;AAErF,OAAO,EAAE,iBAAiB,EAAE,MAAM,mBAAmB,CAAC;AACtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,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;;;OAGG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB;;;;;;OAMG;IACH,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B;;;;OAIG;IACH,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,iBAAiB;IACjB,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CACpB,CAAC;AAqBF;;;;;;;;;GASG;AACH,MAAM,MAAM,YAAY,GAAG;IACzB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,IAAI,EAAE,iBAAiB,CAAC;IACxB,IAAI,EAAE,OAAO,CAAC;IACd,YAAY,EAAE,OAAO,CAAC;IACtB,MAAM,EAAE,mBAAmB,CAAC;IAC5B,GAAG,CAAC,EAAE,aAAa,CAAC;CACrB,CAAC;AAEF,qBAAa,iBAAkB,YAAW,eAAe;IAe3C,OAAO,CAAC,QAAQ,CAAC,IAAI;IAdjC,QAAQ,CAAC,kBAAkB,EAAG,UAAU,CAAU;IAClD,QAAQ,CAAC,mBAAmB,EAAG,SAAS,CAAU;IAClD,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAmC;IAC7D,uGAAuG;IACvG,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2B;IACpD,OAAO,CAAC,eAAe,CAAC,CAAgB;IACxC,OAAO,CAAC,qBAAqB,CAAC,CAAsB;IACpD,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAAsB;IAC/C,OAAO,CAAC,MAAM,CAAqB;IACnC,OAAO,CAAC,YAAY,CAAqB;IACzC,OAAO,CAAC,cAAc,CAAqB;IAC3C,OAAO,CAAC,UAAU,CAAqB;gBAEV,IAAI,EAAE,wBAAwB;IAO3D,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;IAQ7B,wBAAwB,CAAC,OAAO,EAAE,CAAC,KAAK,EAAE,oBAAoB,KAAK,IAAI,GAAG,MAAM,IAAI;IAIpF,SAAS,CAAC,GAAG,SAAa,GAAG,gBAAgB;IAU7C,MAAM,CAAC,GAAG,SAAa,GAAG,OAAO;IAIjC,qBAAqB,CACnB,UAAU,EAAE,MAAM,EAClB,IAAI,EAAE,MAAM,EACZ,cAAc,CAAC,EAAE,sBAAsB,GACtC,OAAO;IA2BV,aAAa,CAAC,UAAU,EAAE,MAAM,GAAG,IAAI;IAqBvC,oBAAoB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAMjD,sBAAsB,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO;IAMnD;;;;;;;;;;;OAWG;IACG,6BAA6B,CACjC,UAAU,EAAE,MAAM,EAClB,WAAW,EAAE,MAAM,GAClB,OAAO,CAAC,yBAAyB,CAAC;IA+B9B,QAAQ,CAAC,EACd,KAAK,EACL,YAAY,EACZ,UAAU,EACV,OAAO,EACP,cAAc,EACd,YAAY,GACb,EAAE,YAAY,GAAG,aAAa,CAAC,YAAY,CAAC;IAyD7C,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;IAahB,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAU/B,OAAO,CAAC,GAAG;YAII,OAAO;IA4CtB,sFAAsF;IACtF,OAAO,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,aAAa,CAAC;IAelD,OAAO,CAAC,uBAAuB;IAa/B;;;;;OAKG;YACW,sBAAsB;IAwCpC;;;;;OAKG;YACY,eAAe;IA8C9B;;;;;;OAMG;IACH,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,aAAa;IA0ErB,OAAO,CAAC,YAAY;IA+DpB,gFAAgF;IAChF,OAAO,CAAC,WAAW;IAenB,OAAO,CAAC,QAAQ;IAQhB,OAAO,CAAC,eAAe;IAavB,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
@@ -170,38 +170,34 @@ export class ClaudeCodeAdapter {
170
170
  return false;
171
171
  return state.inputs.hasUnsettledInjections();
172
172
  }
173
+ isInjectionLive(sessionKey, deliveryKey) {
174
+ const state = this.processes.get(sessionKey);
175
+ if (!state || state.done)
176
+ return false;
177
+ const delivery = state.inputs.getByKey(deliveryKey);
178
+ if (!delivery || delivery.terminal)
179
+ return false;
180
+ const { proc } = state.handle;
181
+ return proc.exitCode === null && proc.signalCode === null;
182
+ }
173
183
  /**
174
184
  * The gateway discarded the buffered copy of an injection without a
175
- * dispatch(). A settled injection only owes bookkeeping. One that has NOT
176
- * settled is an orphan: nothing will ever drain it, and the CLI does not
177
- * start a queued input on its own once the turn it was steered into has
178
- * ended it works its queue only when the next stdin frame arrives, which
179
- * would be the batch re-delivering this very member (prod 2026-09-03: lane
180
- * dethroned after 10 idle minutes, then "refusing duplicate stdin write"
181
- * on every retry). Withdraw it: kill the process (the session survives via
182
- * --resume), report the input failed so the server releases the member,
183
- * and let the gateway run it as a real turn.
185
+ * dispatch(). A settled injection only owes bookkeeping. A live one never
186
+ * reaches here (the gateway attaches to it via isInjectionLive); if one
187
+ * does, it is left registered byCommand must keep routing its lifecycle
188
+ * frames and its terminal state drops it (see apply/fail) and the
189
+ * anomaly is logged. Killing it is not an option: Claude Code starts a
190
+ * queued input on its own once the turn it was steered into ends, and the
191
+ * server, having recorded that `started` as delivered, would never re-run
192
+ * it (2026-09-17 prod loss).
184
193
  */
185
- async acknowledgeDiscardedInjection(sessionKey, deliveryKey) {
194
+ acknowledgeDiscardedInjection(sessionKey, deliveryKey) {
186
195
  const state = this.processes.get(sessionKey);
187
196
  if (!state)
188
197
  return 'discarded';
189
198
  const delivery = state.inputs.getByKey(deliveryKey);
190
199
  if (delivery && !delivery.terminal) {
191
- state.log?.warn?.(`withdrawing orphaned Claude input ${delivery.commandUuid} (${delivery.reportedState ?? 'queued'}) on ${sessionKey}: its buffered copy was discarded before the CLI settled it${state.pump.hasOwnWork() ? '; the runtime-initiated turn dies with the process' : ''}`);
192
- await this.killProcess(sessionKey, state);
193
- state.inputs.discardBookkeeping(deliveryKey);
194
- // 'cancelled' only when the server confirmed the failed receipt: the
195
- // member is released and un-folded, so the gateway can re-fold and run
196
- // it locally. A lost receipt (reportedState never reached 'failed') or
197
- // a 'settled' answer (a reply effect already resolved the member)
198
- // leaves nothing to re-run here — the lane complete releases an unread
199
- // member for re-drive instead.
200
- const confirmed = delivery.reportedState === 'failed' && delivery.terminal === 'failed';
201
- if (!confirmed) {
202
- state.log?.warn?.(`withdrawal of Claude input ${delivery.commandUuid} was not confirmed by the server (${delivery.terminal}); leaving the member to the lane complete`);
203
- }
204
- return confirmed ? 'cancelled' : 'discarded';
200
+ state.log?.warn?.(`buffered copy of Claude input ${delivery.commandUuid} (${delivery.reportedState ?? 'queued'}) on ${sessionKey} discarded while still live keeping its registration; the runtime runs it as its own turn`);
205
201
  }
206
202
  state.inputs.discardBookkeeping(deliveryKey);
207
203
  // The discarded copy may have been the last thing a lazy restart was
@@ -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,OAAO,EACL,KAAK,cAAc,EAGnB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAExB,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,kFAAkF;IAClF,IAAI,EAAE,cAAc,CAAC;IACrB,uEAAuE;IACvE,QAAQ,EAAE,YAAY,CAAC;IACvB,6FAA6F;IAC7F,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,8DAA8D;IAC9D,gBAAgB,EAAE,OAAO,CAAC;IAC1B;;;;;;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,EACjB,YAAY,CAAC,EAAE,MAAM,IAAI,EACzB,GAAG,CAAC,EAAE,aAAa,GAClB,mBAAmB;IAsBtB,MAAM,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAY3C;;;;;;;;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,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IA6BnE,cAAc,CAClB,QAAQ,EAAE,mBAAmB,EAC7B,GAAG,CAAC,EAAE,aAAa,EACnB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC;IAUV,iBAAiB,CAAC,GAAG,CAAC,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAO7E"}
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,OAAO,EACL,KAAK,cAAc,EAGnB,KAAK,YAAY,EAClB,MAAM,gBAAgB,CAAC;AAExB,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,kFAAkF;IAClF,IAAI,EAAE,cAAc,CAAC;IACrB,uEAAuE;IACvE,QAAQ,EAAE,YAAY,CAAC;IACvB,6FAA6F;IAC7F,YAAY,CAAC,EAAE,MAAM,IAAI,CAAC;IAC1B,8DAA8D;IAC9D,gBAAgB,EAAE,OAAO,CAAC;IAC1B;;;;;;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,EACjB,YAAY,CAAC,EAAE,MAAM,IAAI,EACzB,GAAG,CAAC,EAAE,aAAa,GAClB,mBAAmB;IAsBtB,MAAM,CAAC,QAAQ,EAAE,mBAAmB,GAAG,IAAI;IAY3C;;;;;;;;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,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAiCnE,cAAc,CAClB,QAAQ,EAAE,mBAAmB,EAC7B,GAAG,CAAC,EAAE,aAAa,EACnB,MAAM,CAAC,EAAE,MAAM,GACd,OAAO,CAAC,IAAI,CAAC;IAUV,iBAAiB,CAAC,GAAG,CAAC,EAAE,aAAa,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;CAO7E"}
@@ -136,7 +136,11 @@ export class ClaudeInputRegistry {
136
136
  detail: detail ?? classified.detail ?? 'Claude input failed',
137
137
  });
138
138
  delivery.reportedState = 'failed';
139
- delivery.terminal = result?.retry === false ? 'settled' : 'failed';
139
+ // 'settled' only when the server answered and released nothing (a
140
+ // delivered or reply-resolved member); an unconfirmed receipt is not
141
+ // a settlement.
142
+ delivery.terminal =
143
+ result && !result.pending && result.retry === false ? 'settled' : 'failed';
140
144
  }
141
145
  }
142
146
  finally {
@@ -1 +1 @@
1
- {"version":3,"file":"turn-outcome.d.ts","sourceRoot":"","sources":["../src/turn-outcome.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAa,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAwC3D,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CA8ChF;AAcD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,gBAAgB,GAAG,SAAS,EAClC,WAAW,EAAE,MAAM,EAAE,EACrB,GAAG,GAAE,IAAiB,GACrB,gBAAgB,CAkElB"}
1
+ {"version":3,"file":"turn-outcome.d.ts","sourceRoot":"","sources":["../src/turn-outcome.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,gBAAgB,EAAa,MAAM,oBAAoB,CAAC;AACtE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,oBAAoB,CAAC;AAgD3D,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,EAAE,GAAG,EAAE,IAAI,GAAG,MAAM,GAAG,SAAS,CA8ChF;AAcD;;;;GAIG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,gBAAgB,GAAG,SAAS,EAClC,WAAW,EAAE,MAAM,EAAE,EACrB,GAAG,GAAE,IAAiB,GACrB,gBAAgB,CA8ElB"}
@@ -5,13 +5,20 @@
5
5
  * (api_error_status / terminal_reason / is_error), behavioral heuristics
6
6
  * second (zero-usage synthetic turns), message regex last — and regex only
7
7
  * ever refines a classification, so a CLI wording change degrades to the
8
- * generic `api_error`, never to a wrong `ok` or a wrong deferral.
8
+ * generic `transient`, never to a wrong `ok` or a wrong deferral.
9
9
  * Classification uncertainty must never defer: deferring a real message is
10
10
  * costlier than losing one automatic recovery.
11
11
  */
12
12
  const LIMIT_TEXT = /you'?ve (hit|reached) your .*limit|usage limit reached|weekly limit/i;
13
- const AUTH_TEXT = /not logged in|please run \/login|authentication_error|invalid api key|oauth token (has )?expired|\[action required\]/i;
13
+ const AUTH_TEXT = /not logged in|please run \/login|authentication_error|authentication_failed|failed to authenticate|invalid api key|oauth (token|session) (has )?expired|\[action required\]/i;
14
14
  const CONTEXT_TEXT = /prompt is too long|context (window|length) exceeded|request too large/i;
15
+ // A positively identified per-minute rate limit (agent-runtime-state-model-
16
+ // design.md §7.1): the API's own error type, never a plan / subscription
17
+ // limit notice. Only this wording moves a 429 off the deferred path.
18
+ const RATE_LIMIT_ERROR_TEXT = /rate_limit_error/i;
19
+ // Exhausted balance / quota (402): the sender must top up — an error, not a
20
+ // wait, so it takes the same path as auth.
21
+ const QUOTA_TEXT = /credit balance is too low|insufficient credits|insufficient_quota|payment required/i;
15
22
  const DETAIL_MAX = 500;
16
23
  /**
17
24
  * Parse the reset time out of a limit notice like
@@ -136,9 +143,23 @@ export function classifyClaudeTurn(meta, noticeTexts, now = new Date()) {
136
143
  // 1) Structured HTTP status.
137
144
  if (meta.apiErrorStatus === 401 || meta.apiErrorStatus === 403)
138
145
  return withDetail('auth');
146
+ if (meta.apiErrorStatus === 402)
147
+ return withDetail('quota_exhausted');
139
148
  if (meta.apiErrorStatus === 413)
140
149
  return withDetail('context_overflow');
141
150
  if (meta.apiErrorStatus === 429) {
151
+ // A provider that answers an exhausted balance with 429 (OpenAI-style
152
+ // `insufficient_quota`) is a top-up problem, never a wait: quota
153
+ // evidence outranks both 429 readings below.
154
+ if (QUOTA_TEXT.test(evidence))
155
+ return withDetail('quota_exhausted');
156
+ // The CLI exposes only the status: a 429 that positively names the
157
+ // API's rate_limit_error is a short-backoff transient; every other 429
158
+ // (plan limits, no discernible text) stays a deferral — a wrong
159
+ // deferral costs a wait, a wrong "transient" on a real plan limit costs
160
+ // three fast retries and a dead letter.
161
+ if (RATE_LIMIT_ERROR_TEXT.test(evidence))
162
+ return withDetail('transient');
142
163
  return withDetail('usage_limit', evidence ? parseClaudeResetTime(evidence, now) : undefined);
143
164
  }
144
165
  // 2) Text families — refine only; they can never turn a failure into ok.
@@ -158,6 +179,8 @@ export function classifyClaudeTurn(meta, noticeTexts, now = new Date()) {
158
179
  if (failed || syntheticQuiet || noticeTexts.length > 0) {
159
180
  if (CONTEXT_TEXT.test(evidence))
160
181
  return withDetail('context_overflow');
182
+ if (QUOTA_TEXT.test(evidence))
183
+ return withDetail('quota_exhausted');
161
184
  if (LIMIT_TEXT.test(evidence)) {
162
185
  return withDetail('usage_limit', parseClaudeResetTime(evidence, now));
163
186
  }
@@ -165,9 +188,9 @@ export function classifyClaudeTurn(meta, noticeTexts, now = new Date()) {
165
188
  return withDetail('auth');
166
189
  // Reached only when this block was entered (failed / syntheticQuiet /
167
190
  // a captured synthetic notice) but no text family matched. A CLI wording
168
- // change must degrade to api_error — never fall through to `ok` and settle
169
- // a refused turn as success (the read-no-reply class this PR closes).
170
- return withDetail('api_error');
191
+ // change must degrade to transient — never fall through to `ok` and
192
+ // settle a refused turn as success (the read-no-reply class).
193
+ return withDetail('transient');
171
194
  }
172
195
  return { ...base, outcome: 'ok' };
173
196
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@parall/claude-agent",
3
- "version": "1.65.0",
3
+ "version": "1.66.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.65.0",
29
- "@parall/cli": "1.65.0",
30
- "@parall/sdk": "1.65.0"
28
+ "@parall/agent-core": "1.66.0",
29
+ "@parall/cli": "1.66.0",
30
+ "@parall/sdk": "1.66.0"
31
31
  },
32
32
  "devDependencies": {
33
33
  "@types/node": "^22.0.0",
package/src/dispatch.ts CHANGED
@@ -262,44 +262,37 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
262
262
  return state.inputs.hasUnsettledInjections();
263
263
  }
264
264
 
265
+ isInjectionLive(sessionKey: string, deliveryKey: string): boolean {
266
+ const state = this.processes.get(sessionKey);
267
+ if (!state || state.done) return false;
268
+ const delivery = state.inputs.getByKey(deliveryKey);
269
+ if (!delivery || delivery.terminal) return false;
270
+ const { proc } = state.handle;
271
+ return proc.exitCode === null && proc.signalCode === null;
272
+ }
273
+
265
274
  /**
266
275
  * The gateway discarded the buffered copy of an injection without a
267
- * dispatch(). A settled injection only owes bookkeeping. One that has NOT
268
- * settled is an orphan: nothing will ever drain it, and the CLI does not
269
- * start a queued input on its own once the turn it was steered into has
270
- * ended it works its queue only when the next stdin frame arrives, which
271
- * would be the batch re-delivering this very member (prod 2026-09-03: lane
272
- * dethroned after 10 idle minutes, then "refusing duplicate stdin write"
273
- * on every retry). Withdraw it: kill the process (the session survives via
274
- * --resume), report the input failed so the server releases the member,
275
- * and let the gateway run it as a real turn.
276
+ * dispatch(). A settled injection only owes bookkeeping. A live one never
277
+ * reaches here (the gateway attaches to it via isInjectionLive); if one
278
+ * does, it is left registered byCommand must keep routing its lifecycle
279
+ * frames and its terminal state drops it (see apply/fail) and the
280
+ * anomaly is logged. Killing it is not an option: Claude Code starts a
281
+ * queued input on its own once the turn it was steered into ends, and the
282
+ * server, having recorded that `started` as delivered, would never re-run
283
+ * it (2026-09-17 prod loss).
276
284
  */
277
- async acknowledgeDiscardedInjection(
285
+ acknowledgeDiscardedInjection(
278
286
  sessionKey: string,
279
287
  deliveryKey: string,
280
- ): Promise<DiscardedInjectionOutcome> {
288
+ ): DiscardedInjectionOutcome {
281
289
  const state = this.processes.get(sessionKey);
282
290
  if (!state) return 'discarded';
283
291
  const delivery = state.inputs.getByKey(deliveryKey);
284
292
  if (delivery && !delivery.terminal) {
285
293
  state.log?.warn?.(
286
- `withdrawing orphaned Claude input ${delivery.commandUuid} (${delivery.reportedState ?? 'queued'}) on ${sessionKey}: its buffered copy was discarded before the CLI settled it${state.pump.hasOwnWork() ? '; the runtime-initiated turn dies with the process' : ''}`,
294
+ `buffered copy of Claude input ${delivery.commandUuid} (${delivery.reportedState ?? 'queued'}) on ${sessionKey} discarded while still live keeping its registration; the runtime runs it as its own turn`,
287
295
  );
288
- await this.killProcess(sessionKey, state);
289
- state.inputs.discardBookkeeping(deliveryKey);
290
- // 'cancelled' only when the server confirmed the failed receipt: the
291
- // member is released and un-folded, so the gateway can re-fold and run
292
- // it locally. A lost receipt (reportedState never reached 'failed') or
293
- // a 'settled' answer (a reply effect already resolved the member)
294
- // leaves nothing to re-run here — the lane complete releases an unread
295
- // member for re-drive instead.
296
- const confirmed = delivery.reportedState === 'failed' && delivery.terminal === 'failed';
297
- if (!confirmed) {
298
- state.log?.warn?.(
299
- `withdrawal of Claude input ${delivery.commandUuid} was not confirmed by the server (${delivery.terminal}); leaving the member to the lane complete`,
300
- );
301
- }
302
- return confirmed ? 'cancelled' : 'discarded';
303
296
  }
304
297
  state.inputs.discardBookkeeping(deliveryKey);
305
298
  // The discarded copy may have been the last thing a lazy restart was
@@ -190,7 +190,11 @@ export class ClaudeInputRegistry {
190
190
  detail: detail ?? classified.detail ?? 'Claude input failed',
191
191
  });
192
192
  delivery.reportedState = 'failed';
193
- delivery.terminal = result?.retry === false ? 'settled' : 'failed';
193
+ // 'settled' only when the server answered and released nothing (a
194
+ // delivered or reply-resolved member); an unconfirmed receipt is not
195
+ // a settlement.
196
+ delivery.terminal =
197
+ result && !result.pending && result.retry === false ? 'settled' : 'failed';
194
198
  }
195
199
  } finally {
196
200
  delivery.terminal ??= 'failed';
@@ -8,15 +8,23 @@ import type { ClaudeResultMeta } from './output-parser.js';
8
8
  * (api_error_status / terminal_reason / is_error), behavioral heuristics
9
9
  * second (zero-usage synthetic turns), message regex last — and regex only
10
10
  * ever refines a classification, so a CLI wording change degrades to the
11
- * generic `api_error`, never to a wrong `ok` or a wrong deferral.
11
+ * generic `transient`, never to a wrong `ok` or a wrong deferral.
12
12
  * Classification uncertainty must never defer: deferring a real message is
13
13
  * costlier than losing one automatic recovery.
14
14
  */
15
15
 
16
16
  const LIMIT_TEXT = /you'?ve (hit|reached) your .*limit|usage limit reached|weekly limit/i;
17
17
  const AUTH_TEXT =
18
- /not logged in|please run \/login|authentication_error|invalid api key|oauth token (has )?expired|\[action required\]/i;
18
+ /not logged in|please run \/login|authentication_error|authentication_failed|failed to authenticate|invalid api key|oauth (token|session) (has )?expired|\[action required\]/i;
19
19
  const CONTEXT_TEXT = /prompt is too long|context (window|length) exceeded|request too large/i;
20
+ // A positively identified per-minute rate limit (agent-runtime-state-model-
21
+ // design.md §7.1): the API's own error type, never a plan / subscription
22
+ // limit notice. Only this wording moves a 429 off the deferred path.
23
+ const RATE_LIMIT_ERROR_TEXT = /rate_limit_error/i;
24
+ // Exhausted balance / quota (402): the sender must top up — an error, not a
25
+ // wait, so it takes the same path as auth.
26
+ const QUOTA_TEXT =
27
+ /credit balance is too low|insufficient credits|insufficient_quota|payment required/i;
20
28
 
21
29
  const DETAIL_MAX = 500;
22
30
 
@@ -139,8 +147,19 @@ export function classifyClaudeTurn(
139
147
 
140
148
  // 1) Structured HTTP status.
141
149
  if (meta.apiErrorStatus === 401 || meta.apiErrorStatus === 403) return withDetail('auth');
150
+ if (meta.apiErrorStatus === 402) return withDetail('quota_exhausted');
142
151
  if (meta.apiErrorStatus === 413) return withDetail('context_overflow');
143
152
  if (meta.apiErrorStatus === 429) {
153
+ // A provider that answers an exhausted balance with 429 (OpenAI-style
154
+ // `insufficient_quota`) is a top-up problem, never a wait: quota
155
+ // evidence outranks both 429 readings below.
156
+ if (QUOTA_TEXT.test(evidence)) return withDetail('quota_exhausted');
157
+ // The CLI exposes only the status: a 429 that positively names the
158
+ // API's rate_limit_error is a short-backoff transient; every other 429
159
+ // (plan limits, no discernible text) stays a deferral — a wrong
160
+ // deferral costs a wait, a wrong "transient" on a real plan limit costs
161
+ // three fast retries and a dead letter.
162
+ if (RATE_LIMIT_ERROR_TEXT.test(evidence)) return withDetail('transient');
144
163
  return withDetail('usage_limit', evidence ? parseClaudeResetTime(evidence, now) : undefined);
145
164
  }
146
165
 
@@ -162,15 +181,16 @@ export function classifyClaudeTurn(
162
181
  (meta.durationApiMs ?? 0) === 0;
163
182
  if (failed || syntheticQuiet || noticeTexts.length > 0) {
164
183
  if (CONTEXT_TEXT.test(evidence)) return withDetail('context_overflow');
184
+ if (QUOTA_TEXT.test(evidence)) return withDetail('quota_exhausted');
165
185
  if (LIMIT_TEXT.test(evidence)) {
166
186
  return withDetail('usage_limit', parseClaudeResetTime(evidence, now));
167
187
  }
168
188
  if (AUTH_TEXT.test(evidence)) return withDetail('auth');
169
189
  // Reached only when this block was entered (failed / syntheticQuiet /
170
190
  // a captured synthetic notice) but no text family matched. A CLI wording
171
- // change must degrade to api_error — never fall through to `ok` and settle
172
- // a refused turn as success (the read-no-reply class this PR closes).
173
- return withDetail('api_error');
191
+ // change must degrade to transient — never fall through to `ok` and
192
+ // settle a refused turn as success (the read-no-reply class).
193
+ return withDetail('transient');
174
194
  }
175
195
 
176
196
  return { ...base, outcome: 'ok' };