@parall/claude-agent 1.64.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":"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"}
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
@@ -540,10 +536,9 @@ export class ClaudeCodeAdapter {
540
536
  this.processes.delete(sessionKey);
541
537
  this.retiring.delete(state);
542
538
  },
543
- // Main-session turns go to the gateway (steps + session activity);
544
- // fork-session turns only count as busy (fork-scope invariant).
545
539
  onRuntimeTurnOpened: (turn) => this.activity.surfaceTurn(turn, this.opts.sessionManager.isMain(sessionKey), log),
546
540
  onRuntimeTurnClosed: () => this.maybeApplyRestart(sessionKey, state),
541
+ onObservation: (event) => this.activity.emit({ kind: 'observation', sessionKey, event }, log),
547
542
  },
548
543
  }),
549
544
  done: false,
@@ -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":"output-parser.d.ts","sourceRoot":"","sources":["../src/output-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EACH,0BAA0B,GAC1B,cAAc,GACd,eAAe,GACf,cAAc,GACd,mBAAmB,GACnB,QAAQ,GACR,uBAAuB,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC/F,CAAC;AAYF,MAAM,MAAM,iBAAiB,GACzB,YAAY,GACZ;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,GACpE,sBAAsB,GAItB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAKnC;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,GAC5B;IACE,IAAI,EAAE,mBAAmB,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;CACvE,GAID;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAI5C;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAqBlE;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B,CAAC;AAgFN,wBAAuB,qBAAqB,CAC1C,QAAQ,EAAE,QAAQ,GACjB,cAAc,CAAC,iBAAiB,CAAC,CA6NnC"}
1
+ {"version":3,"file":"output-parser.d.ts","sourceRoot":"","sources":["../src/output-parser.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,aAAa,CAAC;AAC5C,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAGvD;;;;;GAKG;AACH,MAAM,MAAM,gBAAgB,GAAG;IAC7B,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,uEAAuE;IACvE,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,mBAAmB,CAAC,EAAE,MAAM,CAAC;IAC7B,yEAAyE;IACzE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC;AAEF;;;;;;;GAOG;AACH,MAAM,MAAM,sBAAsB,GAAG;IACnC,IAAI,EAAE,cAAc,CAAC;IACrB,OAAO,EACH,0BAA0B,GAC1B,cAAc,GACd,eAAe,GACf,cAAc,GACd,mBAAmB,GACnB,QAAQ,GACR,uBAAuB,CAAC;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,KAAK,CAAC,EAAE,KAAK,CAAC;QAAE,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAAC,WAAW,CAAC,EAAE,MAAM,CAAC;QAAC,OAAO,CAAC,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC/F,CAAC;AAYF,MAAM,MAAM,iBAAiB,GACzB,YAAY,GACZ;IAAE,IAAI,EAAE,cAAc,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAC;IAAC,YAAY,EAAE,MAAM,EAAE,CAAA;CAAE,GACpE,sBAAsB,GAItB;IAAE,IAAI,EAAE,WAAW,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,GAKnC;IAAE,IAAI,EAAE,kBAAkB,CAAA;CAAE,GAC5B;IACE,IAAI,EAAE,mBAAmB,CAAC;IAC1B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,QAAQ,GAAG,SAAS,GAAG,WAAW,GAAG,WAAW,GAAG,WAAW,CAAC;CACvE,GAID;IAAE,IAAI,EAAE,iBAAiB,CAAC;IAAC,OAAO,EAAE,MAAM,CAAA;CAAE,GAI5C;IAAE,IAAI,EAAE,kBAAkB,CAAC;IAAC,OAAO,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAE,GAqBlE;IACE,IAAI,EAAE,UAAU,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,OAAO,EAAE,OAAO,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,UAAU,CAAC,EAAE,gBAAgB,CAAC;CAC/B,CAAC;AAgFN,wBAAuB,qBAAqB,CAC1C,QAAQ,EAAE,QAAQ,GACjB,cAAc,CAAC,iBAAiB,CAAC,CAiOnC"}
@@ -1,3 +1,4 @@
1
+ import { claudeObservation } from './runtime-observations.js';
1
2
  const RUNTIME_TASK_SUBTYPES = new Set([
2
3
  'background_tasks_changed',
3
4
  'task_started',
@@ -100,6 +101,10 @@ export async function* parseClaudeStreamJson(readable) {
100
101
  const now = Date.now();
101
102
  const eventTimestampMs = parseEventTimestampMs(event) ?? now;
102
103
  const eventRecord = event;
104
+ const observation = claudeObservation(eventRecord);
105
+ // Init must announce the native session before its observations drain.
106
+ if (observation && observation.content.event !== 'session_initialized')
107
+ yield observation;
103
108
  if (eventRecord.type === 'system' && eventRecord.subtype === 'compact_boundary') {
104
109
  const meta = eventRecord.compact_metadata && typeof eventRecord.compact_metadata === 'object'
105
110
  ? eventRecord.compact_metadata
@@ -126,6 +131,8 @@ export async function* parseClaudeStreamJson(readable) {
126
131
  ...(sessionId ? { sessionId } : {}),
127
132
  capabilities,
128
133
  };
134
+ if (observation)
135
+ yield observation;
129
136
  continue;
130
137
  }
131
138
  const subtype = asTrimmedString(eventRecord.subtype);
@@ -1,4 +1,4 @@
1
- import { type GatewayLogger, type RuntimeBusyState } from '@parall/agent-core';
1
+ import { type GatewayLogger, type RuntimeBusyState, type RuntimeObservationEvent } from '@parall/agent-core';
2
2
  import { ClaudeBusyTracker } from './busy-state.js';
3
3
  import type { ClaudeInputDelivery, ClaudeInputRegistry } from './input-lifecycle.js';
4
4
  import type { ClaudeParsedEvent } from './output-parser.js';
@@ -17,6 +17,7 @@ export type ClaudeProcessPumpHooks = {
17
17
  onEof(): void;
18
18
  onRuntimeTurnOpened(turn: ClaudeRuntimeTurn): void;
19
19
  onRuntimeTurnClosed(): void;
20
+ onObservation?(event: RuntimeObservationEvent): void;
20
21
  };
21
22
  export type ClaudeProcessPumpOptions = {
22
23
  sessionKey: string;
@@ -59,6 +60,7 @@ export declare class ClaudeProcessPump {
59
60
  private idleTimer;
60
61
  private activeDrain;
61
62
  private readonly now;
63
+ private pendingObservations;
62
64
  constructor(opts: ClaudeProcessPumpOptions);
63
65
  start(): void;
64
66
  /** The dispatch generator currently draining `delivery` (owner preference). */
@@ -92,6 +94,7 @@ export declare class ClaudeProcessPump {
92
94
  private run;
93
95
  private route;
94
96
  private touchAll;
97
+ private routeObservation;
95
98
  private liveDeliveries;
96
99
  private ownerFor;
97
100
  private openRuntimeTurn;
@@ -1 +1 @@
1
- {"version":3,"file":"process-pump.d.ts","sourceRoot":"","sources":["../src/process-pump.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"process-pump.d.ts","sourceRoot":"","sources":["../src/process-pump.ts"],"names":[],"mappings":"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"}
@@ -33,6 +33,7 @@ export class ClaudeProcessPump {
33
33
  idleTimer = null;
34
34
  activeDrain = null;
35
35
  now;
36
+ pendingObservations = [];
36
37
  constructor(opts) {
37
38
  this.opts = opts;
38
39
  this.now = opts.now ?? Date.now;
@@ -153,7 +154,8 @@ export class ClaudeProcessPump {
153
154
  for await (const parsed of parser) {
154
155
  if (this.closed)
155
156
  break;
156
- this.touchAll();
157
+ if (parsed.type !== 'observation')
158
+ this.touchAll();
157
159
  await this.route(parsed);
158
160
  }
159
161
  }
@@ -168,6 +170,9 @@ export class ClaudeProcessPump {
168
170
  switch (parsed.type) {
169
171
  case 'runtime_activity':
170
172
  return;
173
+ case 'observation':
174
+ this.routeObservation(parsed);
175
+ return;
171
176
  case 'runtime_init':
172
177
  this.handleInit(parsed);
173
178
  return;
@@ -222,6 +227,27 @@ export class ClaudeProcessPump {
222
227
  }
223
228
  this.runtimeTurn?.touch();
224
229
  }
230
+ routeObservation(event) {
231
+ if (!this.sessionId) {
232
+ // SessionStart hooks precede init. Keep a bounded startup buffer and
233
+ // release only records whose native session matches the eventual init.
234
+ if (this.pendingObservations.length < 64)
235
+ this.pendingObservations.push(event);
236
+ else
237
+ this.opts.log?.warn('Claude startup observation buffer full; dropping observation');
238
+ return;
239
+ }
240
+ if (event.runtimeSessionId !== this.sessionId) {
241
+ this.opts.log?.warn('Claude observation does not match the initialized session');
242
+ return;
243
+ }
244
+ const owner = this.ownerFor(false) ??
245
+ (this.activeDrain && !this.activeDrain.terminal ? this.activeDrain : null);
246
+ if (owner)
247
+ owner.sink.push({ kind: 'runtime', event });
248
+ else
249
+ this.opts.hooks.onObservation?.(event);
250
+ }
225
251
  liveDeliveries() {
226
252
  const live = [];
227
253
  for (const delivery of this.opts.inputs.values())
@@ -293,6 +319,10 @@ export class ClaudeProcessPump {
293
319
  delivery.sessionAnnounced = true;
294
320
  delivery.sink.push({ kind: 'runtime', event: announce });
295
321
  }
322
+ const pending = this.pendingObservations;
323
+ this.pendingObservations = [];
324
+ for (const event of pending)
325
+ this.routeObservation(event);
296
326
  }
297
327
  async handleLifecycle(parsed) {
298
328
  if (!this.capabilities.has('msg_lifecycle_v1')) {
@@ -0,0 +1,4 @@
1
+ import { type RuntimeObservationEvent } from '@parall/agent-core';
2
+ /** Only facts the CLI emitted. A result UUID is not a native turn identity. */
3
+ export declare function claudeObservation(frame: Record<string, unknown>): RuntimeObservationEvent | undefined;
4
+ //# sourceMappingURL=runtime-observations.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"runtime-observations.d.ts","sourceRoot":"","sources":["../src/runtime-observations.ts"],"names":[],"mappings":"AAAA,OAAO,EAGL,KAAK,uBAAuB,EAC7B,MAAM,oBAAoB,CAAC;AAG5B,+EAA+E;AAC/E,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAC7B,uBAAuB,GAAG,SAAS,CAqErC"}
@@ -0,0 +1,72 @@
1
+ import { observationField, runtimeObservation, } from '@parall/agent-core';
2
+ /** Only facts the CLI emitted. A result UUID is not a native turn identity. */
3
+ export function claudeObservation(frame) {
4
+ const sessionId = observationField(frame.session_id);
5
+ if (!sessionId || frame.parent_tool_use_id)
6
+ return;
7
+ const native = {
8
+ session_id: sessionId,
9
+ event_id: observationField(frame.uuid),
10
+ };
11
+ let event;
12
+ let status = 'unknown';
13
+ if (frame.type === 'result') {
14
+ event = 'request_completed';
15
+ status =
16
+ frame.is_error === true ? 'failed' : frame.is_error === false ? 'completed' : 'unknown';
17
+ native.status = observationField(frame.subtype);
18
+ if (typeof frame.num_turns === 'number' &&
19
+ Number.isSafeInteger(frame.num_turns) &&
20
+ frame.num_turns >= 0) {
21
+ native.num_turns = frame.num_turns;
22
+ }
23
+ }
24
+ else if (frame.type === 'system') {
25
+ switch (frame.subtype) {
26
+ case 'init':
27
+ event = 'session_initialized';
28
+ status = 'initialized';
29
+ break;
30
+ case 'hook_started':
31
+ case 'hook_response':
32
+ event = frame.subtype === 'hook_started' ? 'hook_started' : 'hook_completed';
33
+ status =
34
+ frame.subtype === 'hook_started'
35
+ ? 'running'
36
+ : frame.outcome === 'success'
37
+ ? 'completed'
38
+ : frame.outcome === 'error'
39
+ ? 'failed'
40
+ : frame.outcome === 'cancelled'
41
+ ? 'stopped'
42
+ : 'unknown';
43
+ native.hook_id = observationField(frame.hook_id);
44
+ native.hook_event = observationField(frame.hook_event);
45
+ native.status = observationField(frame.outcome);
46
+ break;
47
+ case 'status':
48
+ case 'session_state_changed':
49
+ event = 'session_status';
50
+ native.status = observationField(frame.status) ?? observationField(frame.state);
51
+ // Null does not prove the runtime is idle; only explicit known states
52
+ // can become public statuses.
53
+ if (native.status === 'compacting' ||
54
+ native.status === 'running' ||
55
+ native.status === 'idle') {
56
+ status = native.status;
57
+ }
58
+ break;
59
+ default:
60
+ return;
61
+ }
62
+ }
63
+ else
64
+ return;
65
+ return runtimeObservation(sessionId, {
66
+ runtime: 'claude-code',
67
+ source: 'stream_json',
68
+ event,
69
+ status,
70
+ native,
71
+ });
72
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"spawn-env.d.ts","sourceRoot":"","sources":["../src/spawn-env.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,CAAC,UAAU,EAC5B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,EAChC,IAAI,EAAE;IACJ,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,GACA,MAAM,CAAC,UAAU,CAiFnB"}
1
+ {"version":3,"file":"spawn-env.d.ts","sourceRoot":"","sources":["../src/spawn-env.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,oBAAoB,CAAC;AAEvD,wBAAgB,aAAa,CAC3B,SAAS,EAAE,MAAM,CAAC,UAAU,EAC5B,UAAU,EAAE,MAAM,EAClB,OAAO,EAAE,YAAY,CAAC,SAAS,CAAC,EAChC,IAAI,EAAE;IACJ,WAAW,EAAE,OAAO,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B,GACA,MAAM,CAAC,UAAU,CAoFnB"}
package/dist/spawn-env.js CHANGED
@@ -8,6 +8,9 @@ export function buildSpawnEnv(parentEnv, claudeHome, context, opts) {
8
8
  const result = {
9
9
  ...env,
10
10
  HOME: claudeHome,
11
+ // Request native state frames for observation without changing user files.
12
+ // Preserve an operator's explicit opt-out; older CLIs can ignore the flag.
13
+ CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: env.CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS ?? '1',
11
14
  PRLL_API_URL: context.apiUrl,
12
15
  PRLL_API_KEY: context.apiKey,
13
16
  PRLL_ORG_ID: context.orgId,
@@ -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.64.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.64.0",
29
- "@parall/cli": "1.64.0",
30
- "@parall/sdk": "1.64.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
@@ -681,11 +674,11 @@ export class ClaudeCodeAdapter implements DispatchAdapter {
681
674
  if (this.processes.get(sessionKey) === state) this.processes.delete(sessionKey);
682
675
  this.retiring.delete(state);
683
676
  },
684
- // Main-session turns go to the gateway (steps + session activity);
685
- // fork-session turns only count as busy (fork-scope invariant).
686
677
  onRuntimeTurnOpened: (turn) =>
687
678
  this.activity.surfaceTurn(turn, this.opts.sessionManager.isMain(sessionKey), log),
688
679
  onRuntimeTurnClosed: () => this.maybeApplyRestart(sessionKey, state),
680
+ onObservation: (event) =>
681
+ this.activity.emit({ kind: 'observation', sessionKey, event }, log),
689
682
  },
690
683
  }),
691
684
  done: false,
@@ -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';
@@ -1,5 +1,6 @@
1
1
  import type { Readable } from 'node:stream';
2
2
  import type { RuntimeEvent } from '@parall/agent-core';
3
+ import { claudeObservation } from './runtime-observations.js';
3
4
 
4
5
  /**
5
6
  * Structured snapshot of the `result` frame's LLM-layer discriminators and
@@ -215,6 +216,9 @@ export async function* parseClaudeStreamJson(
215
216
  const now = Date.now();
216
217
  const eventTimestampMs = parseEventTimestampMs(event) ?? now;
217
218
  const eventRecord = event as Record<string, unknown>;
219
+ const observation = claudeObservation(eventRecord);
220
+ // Init must announce the native session before its observations drain.
221
+ if (observation && observation.content.event !== 'session_initialized') yield observation;
218
222
 
219
223
  if (eventRecord.type === 'system' && eventRecord.subtype === 'compact_boundary') {
220
224
  const meta =
@@ -244,6 +248,7 @@ export async function* parseClaudeStreamJson(
244
248
  ...(sessionId ? { sessionId } : {}),
245
249
  capabilities,
246
250
  };
251
+ if (observation) yield observation;
247
252
  continue;
248
253
  }
249
254
  const subtype = asTrimmedString(eventRecord.subtype) as
@@ -3,6 +3,7 @@ import {
3
3
  type GatewayLogger,
4
4
  type RuntimeBusyState,
5
5
  type RuntimeTurnTrigger,
6
+ type RuntimeObservationEvent,
6
7
  } from '@parall/agent-core';
7
8
  import { ClaudeBusyTracker } from './busy-state.js';
8
9
  import type { ClaudeInputDelivery, ClaudeInputRegistry } from './input-lifecycle.js';
@@ -24,6 +25,7 @@ export type ClaudeProcessPumpHooks = {
24
25
  onEof(): void;
25
26
  onRuntimeTurnOpened(turn: ClaudeRuntimeTurn): void;
26
27
  onRuntimeTurnClosed(): void;
28
+ onObservation?(event: RuntimeObservationEvent): void;
27
29
  };
28
30
 
29
31
  export type ClaudeProcessPumpOptions = {
@@ -67,6 +69,7 @@ export class ClaudeProcessPump {
67
69
  private idleTimer: NodeJS.Timeout | null = null;
68
70
  private activeDrain: ClaudeInputDelivery | null = null;
69
71
  private readonly now: () => number;
72
+ private pendingObservations: RuntimeObservationEvent[] = [];
70
73
 
71
74
  constructor(private readonly opts: ClaudeProcessPumpOptions) {
72
75
  this.now = opts.now ?? Date.now;
@@ -194,7 +197,7 @@ export class ClaudeProcessPump {
194
197
  try {
195
198
  for await (const parsed of parser) {
196
199
  if (this.closed) break;
197
- this.touchAll();
200
+ if (parsed.type !== 'observation') this.touchAll();
198
201
  await this.route(parsed);
199
202
  }
200
203
  } catch (err) {
@@ -208,6 +211,9 @@ export class ClaudeProcessPump {
208
211
  switch (parsed.type) {
209
212
  case 'runtime_activity':
210
213
  return;
214
+ case 'observation':
215
+ this.routeObservation(parsed);
216
+ return;
211
217
  case 'runtime_init':
212
218
  this.handleInit(parsed);
213
219
  return;
@@ -261,6 +267,25 @@ export class ClaudeProcessPump {
261
267
  this.runtimeTurn?.touch();
262
268
  }
263
269
 
270
+ private routeObservation(event: RuntimeObservationEvent): void {
271
+ if (!this.sessionId) {
272
+ // SessionStart hooks precede init. Keep a bounded startup buffer and
273
+ // release only records whose native session matches the eventual init.
274
+ if (this.pendingObservations.length < 64) this.pendingObservations.push(event);
275
+ else this.opts.log?.warn('Claude startup observation buffer full; dropping observation');
276
+ return;
277
+ }
278
+ if (event.runtimeSessionId !== this.sessionId) {
279
+ this.opts.log?.warn('Claude observation does not match the initialized session');
280
+ return;
281
+ }
282
+ const owner =
283
+ this.ownerFor(false) ??
284
+ (this.activeDrain && !this.activeDrain.terminal ? this.activeDrain : null);
285
+ if (owner) owner.sink.push({ kind: 'runtime', event });
286
+ else this.opts.hooks.onObservation?.(event);
287
+ }
288
+
264
289
  private liveDeliveries(): ClaudeInputDelivery[] {
265
290
  const live: ClaudeInputDelivery[] = [];
266
291
  for (const delivery of this.opts.inputs.values()) if (isLive(delivery)) live.push(delivery);
@@ -328,6 +353,9 @@ export class ClaudeProcessPump {
328
353
  delivery.sessionAnnounced = true;
329
354
  delivery.sink.push({ kind: 'runtime', event: announce });
330
355
  }
356
+ const pending = this.pendingObservations;
357
+ this.pendingObservations = [];
358
+ for (const event of pending) this.routeObservation(event);
331
359
  }
332
360
 
333
361
  private async handleLifecycle(parsed: {
@@ -0,0 +1,80 @@
1
+ import {
2
+ observationField,
3
+ runtimeObservation,
4
+ type RuntimeObservationEvent,
5
+ } from '@parall/agent-core';
6
+ import type { RuntimeObservationContent } from '@parall/sdk';
7
+
8
+ /** Only facts the CLI emitted. A result UUID is not a native turn identity. */
9
+ export function claudeObservation(
10
+ frame: Record<string, unknown>,
11
+ ): RuntimeObservationEvent | undefined {
12
+ const sessionId = observationField(frame.session_id);
13
+ if (!sessionId || frame.parent_tool_use_id) return;
14
+ const native: NonNullable<RuntimeObservationContent['native']> = {
15
+ session_id: sessionId,
16
+ event_id: observationField(frame.uuid),
17
+ };
18
+ let event: RuntimeObservationContent['event'];
19
+ let status: RuntimeObservationContent['status'] = 'unknown';
20
+ if (frame.type === 'result') {
21
+ event = 'request_completed';
22
+ status =
23
+ frame.is_error === true ? 'failed' : frame.is_error === false ? 'completed' : 'unknown';
24
+ native.status = observationField(frame.subtype);
25
+ if (
26
+ typeof frame.num_turns === 'number' &&
27
+ Number.isSafeInteger(frame.num_turns) &&
28
+ frame.num_turns >= 0
29
+ ) {
30
+ native.num_turns = frame.num_turns;
31
+ }
32
+ } else if (frame.type === 'system') {
33
+ switch (frame.subtype) {
34
+ case 'init':
35
+ event = 'session_initialized';
36
+ status = 'initialized';
37
+ break;
38
+ case 'hook_started':
39
+ case 'hook_response':
40
+ event = frame.subtype === 'hook_started' ? 'hook_started' : 'hook_completed';
41
+ status =
42
+ frame.subtype === 'hook_started'
43
+ ? 'running'
44
+ : frame.outcome === 'success'
45
+ ? 'completed'
46
+ : frame.outcome === 'error'
47
+ ? 'failed'
48
+ : frame.outcome === 'cancelled'
49
+ ? 'stopped'
50
+ : 'unknown';
51
+ native.hook_id = observationField(frame.hook_id);
52
+ native.hook_event = observationField(frame.hook_event);
53
+ native.status = observationField(frame.outcome);
54
+ break;
55
+ case 'status':
56
+ case 'session_state_changed':
57
+ event = 'session_status';
58
+ native.status = observationField(frame.status) ?? observationField(frame.state);
59
+ // Null does not prove the runtime is idle; only explicit known states
60
+ // can become public statuses.
61
+ if (
62
+ native.status === 'compacting' ||
63
+ native.status === 'running' ||
64
+ native.status === 'idle'
65
+ ) {
66
+ status = native.status;
67
+ }
68
+ break;
69
+ default:
70
+ return;
71
+ }
72
+ } else return;
73
+ return runtimeObservation(sessionId, {
74
+ runtime: 'claude-code',
75
+ source: 'stream_json',
76
+ event,
77
+ status,
78
+ native,
79
+ });
80
+ }
package/src/spawn-env.ts CHANGED
@@ -22,6 +22,9 @@ export function buildSpawnEnv(
22
22
  const result: NodeJS.ProcessEnv = {
23
23
  ...env,
24
24
  HOME: claudeHome,
25
+ // Request native state frames for observation without changing user files.
26
+ // Preserve an operator's explicit opt-out; older CLIs can ignore the flag.
27
+ CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS: env.CLAUDE_CODE_EMIT_SESSION_STATE_EVENTS ?? '1',
25
28
  PRLL_API_URL: context.apiUrl,
26
29
  PRLL_API_KEY: context.apiKey,
27
30
  PRLL_ORG_ID: context.orgId,
@@ -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' };