@inixiative/agent-session 0.6.7 → 0.6.8

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.
package/README.md CHANGED
@@ -50,6 +50,12 @@ session.kill();
50
50
  refused before a byte is written.
51
51
  - **fork / resume / interrupt** and pre-send hooks (rewrite the outgoing
52
52
  message — e.g. inject just-in-time context per turn).
53
+ - **A resume whose native conversation is not found is typed.** Claude finding no transcript
54
+ ("No conversation found with session ID": cleaned up, deleted, or under another project
55
+ directory or config home) and Codex finding no rollout ("no rollout found for thread id") both
56
+ raise `NativeBindingMissingError` (`nativeBindingMissing(error)` finds it behind a turn error,
57
+ with the `cwd` the lookup ran in). No turn ran, so the attempt settles as a no-send instead of
58
+ uncertain native work, and the caller can start fresh from its own record.
53
59
  - **Transports with declared capabilities.** How a model is reached is a
54
60
  pluggable transport; switching it is a routing change, not a rewrite.
55
61
  - **Zero dependencies.** Just Bun + the agent CLI you're driving. The Agent SDK
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@inixiative/agent-session",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "Drive coding-agent CLIs as persistent, streaming, event-captured sessions over interchangeable transports, with primed decision sessions, subscription pools, routing, limit polling and continuity.",
5
5
  "type": "module",
6
6
  "main": "src/index.ts",
@@ -40,7 +40,7 @@
40
40
  },
41
41
  "devDependencies": {
42
42
  "@biomejs/biome": "2.5.2",
43
- "@inixiative/config": "^0.5.105",
43
+ "@inixiative/config": "^0.5.118",
44
44
  "@types/bun": "1.4.2",
45
45
  "lefthook": "2.1.9",
46
46
  "typescript": "6.0.3"
@@ -63,7 +63,7 @@ import type {
63
63
  ToolGate,
64
64
  ToolRequest,
65
65
  } from './harness-session';
66
- import { sendableImages } from './harness-session';
66
+ import { NativeBindingMissingError, sendableImages } from './harness-session';
67
67
 
68
68
  // Re-export types so existing import paths keep working
69
69
  export type {
@@ -354,6 +354,8 @@ export class ClaudeCodeSession implements HarnessSession {
354
354
  // so we know the concrete types at runtime.
355
355
  private _proc: PipedSubprocess | null = null;
356
356
  private _stderr = '';
357
+ /** The stderr reader of the running process; awaited (capped) before stderr is interpreted. */
358
+ private _stderrDone: Promise<void> = Promise.resolve();
357
359
 
358
360
  // -- Session state --
359
361
  /**
@@ -560,7 +562,7 @@ export class ClaudeCodeSession implements HarnessSession {
560
562
  // Background readers — run for session lifetime (don't await)
561
563
  const proc = this._proc;
562
564
  const stdoutDone = this._readStdout(proc);
563
- this._readStderr();
565
+ this._stderrDone = this._readStderr();
564
566
 
565
567
  // Monitor process exit for cleanup
566
568
  // The process read its base context file at startup; it goes when the process does.
@@ -577,7 +579,7 @@ export class ClaudeCodeSession implements HarnessSession {
577
579
  const errMsg = this._stderr.trim()
578
580
  ? `Process exited (code ${code}): ${this._stderr.trim().slice(0, 500)}`
579
581
  : `Process exited with code ${code}`;
580
- this._rejectInflight(new Error(errMsg));
582
+ this._rejectInflight((await this._settleMissing(errMsg)) ?? new Error(errMsg));
581
583
  this._rejectQueue(new Error('Session ended'));
582
584
  this._rejectControl(new Error('Session ended'));
583
585
  this._emit({ kind: 'session_end', timestamp: Date.now(), transportOutcome: 'failed' });
@@ -616,6 +618,8 @@ export class ClaudeCodeSession implements HarnessSession {
616
618
  );
617
619
  } catch (error) {
618
620
  this.kill();
621
+ const missing = this._missingBinding((error as Error).message);
622
+ if (missing) throw missing;
619
623
  throw new Error(
620
624
  `${this._toolGate ? 'Tool gate' : 'Extension'} registration failed: ${(error as Error).message}`,
621
625
  );
@@ -630,6 +634,41 @@ export class ClaudeCodeSession implements HarnessSession {
630
634
  }
631
635
  }
632
636
 
637
+ /**
638
+ * The CLI refused to resume because the transcript is gone (Claude Code's cleanup of
639
+ * `~/.claude/projects`, or a deleted file): it says so and exits before reading a turn.
640
+ */
641
+ private _missingBinding(detail: string): NativeBindingMissingError | undefined {
642
+ const binding = this._externalSessionId;
643
+ if (!binding) return undefined;
644
+ const marker = `No conversation found with session ID: ${binding}`;
645
+ return this._stderr.includes(marker) || detail.includes(marker)
646
+ ? new NativeBindingMissingError('claude-code', binding, marker, this._cwd)
647
+ : undefined;
648
+ }
649
+
650
+ /**
651
+ * After the process ended: was the binding missing? stderr is read first (capped), since the
652
+ * marker is there. A turn written to a CLI that never loaded the conversation (no `system/init`,
653
+ * no terminal) could not run: it settles as a no-send rather than uncertain native work.
654
+ */
655
+ private async _settleMissing(detail: string): Promise<NativeBindingMissingError | undefined> {
656
+ await Promise.race([this._stderrDone, Bun.sleep(250)]);
657
+ const missing = this._missingBinding(detail);
658
+ const a = this._inflight?.evidence;
659
+ if (
660
+ missing &&
661
+ a &&
662
+ a.nativeOutcome === 'unknown' &&
663
+ !a.events.some((event) => {
664
+ const raw = event.raw as { type?: unknown; subtype?: unknown } | undefined;
665
+ return raw?.type === 'system' && raw.subtype === 'init';
666
+ })
667
+ )
668
+ a.dispatch = 'not-dispatched';
669
+ return missing;
670
+ }
671
+
633
672
  private _detachExtension(reason: string): void {
634
673
  try {
635
674
  this._extension?.detach(reason);
@@ -1065,14 +1104,12 @@ export class ClaudeCodeSession implements HarnessSession {
1065
1104
  if (buffer.trim() && (this._proc === proc || !this._proc)) {
1066
1105
  this._processLine(buffer);
1067
1106
  }
1068
- if (this._proc === proc && this._alive)
1069
- this._transportFailure(
1070
- new Error(
1071
- this._stderr.trim()
1072
- ? `Native stdout closed: ${this._stderr.trim().slice(0, 500)}`
1073
- : 'Native stdout closed',
1074
- ),
1075
- );
1107
+ if (this._proc === proc && this._alive) {
1108
+ const detail = this._stderr.trim()
1109
+ ? `Native stdout closed: ${this._stderr.trim().slice(0, 500)}`
1110
+ : 'Native stdout closed';
1111
+ this._transportFailure((await this._settleMissing(detail)) ?? new Error(detail));
1112
+ }
1076
1113
  } catch (err) {
1077
1114
  if (this._proc === proc) this._transportFailure(err as Error);
1078
1115
  }
@@ -27,7 +27,7 @@ import type {
27
27
  SessionResult,
28
28
  ToolGate,
29
29
  } from './harness-session';
30
- import { sendableImages } from './harness-session';
30
+ import { NativeBindingMissingError, sendableImages } from './harness-session';
31
31
 
32
32
  // Re-export types so importers can stay on one path (mirrors claude-code-session).
33
33
  export type {
@@ -592,8 +592,11 @@ export class CodexAppServerSession implements HarnessSession {
592
592
 
593
593
  private _releaseKnownTurn(): void {
594
594
  const a = this._inflight?.evidence;
595
- if (!a || !this._canReleaseAttempt(a) || a.nativeOutcome === 'unknown') return;
596
- this._turns++;
595
+ if (!a) return;
596
+ // Refused before `turn/start` was written: nothing native is outstanding.
597
+ const noSend = a.dispatch === 'not-dispatched' && a.localOutcome === 'rejected';
598
+ if (!noSend && (!this._canReleaseAttempt(a) || a.nativeOutcome === 'unknown')) return;
599
+ if (!noSend) this._turns++;
597
600
  this._inflight = null;
598
601
  this._processNextTurn();
599
602
  }
@@ -1222,8 +1225,9 @@ export class CodexAppServerSession implements HarnessSession {
1222
1225
  if (!this._threadReady) {
1223
1226
  const binding = this._externalSessionId;
1224
1227
  const method = binding ? 'thread/resume' : 'thread/start';
1225
- const response = object(
1226
- await this._rpcRequest(method, {
1228
+ let resumed: unknown;
1229
+ try {
1230
+ resumed = await this._rpcRequest(method, {
1227
1231
  ...(binding ? { threadId: binding } : {}),
1228
1232
  model: this._model,
1229
1233
  cwd: this._cwd,
@@ -1232,8 +1236,22 @@ export class CodexAppServerSession implements HarnessSession {
1232
1236
  ...(this._gated ? { approvalsReviewer: 'user' } : {}),
1233
1237
  sandbox: this._sandbox,
1234
1238
  ...(this._options.config ? { config: this._options.config } : {}),
1235
- }),
1236
- );
1239
+ });
1240
+ } catch (error) {
1241
+ // The rollout is gone (Codex's own cleanup, or deleted). Codex answered that no thread
1242
+ // exists, so no turn could run and none was written: a settled no-send, not uncertain
1243
+ // native work. Any other failure here keeps its outcome unknown.
1244
+ if (binding && /^no rollout found for thread id\b/i.test((error as Error).message ?? '')) {
1245
+ a.dispatch = 'not-dispatched';
1246
+ throw new NativeBindingMissingError(
1247
+ 'codex-app-server',
1248
+ binding,
1249
+ (error as Error).message,
1250
+ );
1251
+ }
1252
+ throw error;
1253
+ }
1254
+ const response = object(resumed);
1237
1255
  const thread = object(response?.thread),
1238
1256
  id = thread?.id;
1239
1257
  if (typeof id !== 'string' || !id || (binding && id !== binding))
@@ -79,6 +79,35 @@ export class SessionTurnError extends Error {
79
79
  }
80
80
  }
81
81
 
82
+ /**
83
+ * The native binding a session was told to resume was not found by the runtime, from this
84
+ * session's cwd and config home: Claude found no transcript ("No conversation found with session
85
+ * ID": cleaned up, deleted, or kept under another project directory or `CLAUDE_CONFIG_DIR`), or
86
+ * Codex found no rollout ("no rollout found for thread id": deleted, or under another
87
+ * `CODEX_HOME`). No turn ran. The caller decides what comes next, typically a fresh session seeded
88
+ * from its own record of the conversation; `cwd` says where the lookup was made.
89
+ */
90
+ export class NativeBindingMissingError extends Error {
91
+ constructor(
92
+ readonly runtime: 'claude-code' | 'codex-app-server',
93
+ readonly binding: string,
94
+ message: string,
95
+ readonly cwd?: string,
96
+ ) {
97
+ super(message);
98
+ this.name = 'NativeBindingMissingError';
99
+ }
100
+ }
101
+
102
+ /** The missing-binding error behind a session error, if any (it may ride as a turn error's cause). */
103
+ export function nativeBindingMissing(error: unknown): NativeBindingMissingError | undefined {
104
+ for (let current = error, depth = 0; current && depth < 5; depth++) {
105
+ if (current instanceof NativeBindingMissingError) return current;
106
+ current = (current as { cause?: unknown }).cause;
107
+ }
108
+ return undefined;
109
+ }
110
+
82
111
  export type SessionEventKind =
83
112
  | 'text_delta'
84
113
  | 'native_status'