@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 +6 -0
- package/package.json +2 -2
- package/src/claude-code-session.ts +48 -11
- package/src/codex-session.ts +25 -7
- package/src/harness-session.ts +29 -0
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.
|
|
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.
|
|
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.
|
|
1070
|
-
|
|
1071
|
-
|
|
1072
|
-
|
|
1073
|
-
|
|
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
|
}
|
package/src/codex-session.ts
CHANGED
|
@@ -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
|
|
596
|
-
|
|
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
|
-
|
|
1226
|
-
|
|
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))
|
package/src/harness-session.ts
CHANGED
|
@@ -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'
|