@twelvehart/orcats 0.2.3 → 0.3.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.
- package/README.md +3 -3
- package/dist/backends/claude-run.d.ts.map +1 -1
- package/dist/backends/codex-jsonl.d.ts +2 -1
- package/dist/backends/codex-jsonl.d.ts.map +1 -1
- package/dist/backends/codex-run.d.ts +2 -1
- package/dist/backends/codex-run.d.ts.map +1 -1
- package/dist/backends/codex.d.ts.map +1 -1
- package/dist/backends/pi-run.d.ts.map +1 -1
- package/dist/backends/select.d.ts +1 -1
- package/dist/backends/select.d.ts.map +1 -1
- package/dist/backends/subprocess-run.d.ts +4 -5
- package/dist/backends/subprocess-run.d.ts.map +1 -1
- package/dist/backends/subprocess-termination.d.ts +4 -0
- package/dist/backends/subprocess-termination.d.ts.map +1 -0
- package/dist/cli/version.d.ts +1 -1
- package/dist/conversation/conversation.d.ts +2 -0
- package/dist/conversation/conversation.d.ts.map +1 -1
- package/dist/conversation/settlement-reservation.d.ts +10 -0
- package/dist/conversation/settlement-reservation.d.ts.map +1 -0
- package/dist/model/backend-config.d.ts +2 -0
- package/dist/model/backend-config.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/backends/claude-run.ts +7 -2
- package/src/backends/codex-jsonl.ts +5 -0
- package/src/backends/codex-run.ts +15 -1
- package/src/backends/codex.ts +4 -1
- package/src/backends/pi-run.ts +8 -2
- package/src/backends/select.ts +4 -1
- package/src/backends/subprocess-run.ts +534 -58
- package/src/backends/subprocess-termination.ts +56 -0
- package/src/cli/main.ts +2 -1
- package/src/cli/version.ts +1 -1
- package/src/conversation/conversation.ts +92 -7
- package/src/conversation/settlement-reservation.ts +127 -0
- package/src/model/backend-config.ts +8 -0
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
import type { SubprocessProcess } from "./subprocess-run.ts";
|
|
2
|
+
|
|
3
|
+
const DefaultSubprocessTerminationGraceMs = 1_000;
|
|
4
|
+
const DefaultSubprocessForceGraceMs = 1_000;
|
|
5
|
+
|
|
6
|
+
const exitWaitCancellations = new WeakMap<
|
|
7
|
+
SubprocessProcess,
|
|
8
|
+
(error: unknown) => void
|
|
9
|
+
>();
|
|
10
|
+
|
|
11
|
+
export function registerSubprocessExitWaitCancellation(
|
|
12
|
+
process: SubprocessProcess,
|
|
13
|
+
cancel: (error: unknown) => void
|
|
14
|
+
): void {
|
|
15
|
+
exitWaitCancellations.set(process, cancel);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export async function terminateSubprocess(
|
|
19
|
+
process: SubprocessProcess,
|
|
20
|
+
gracefulMs = DefaultSubprocessTerminationGraceMs,
|
|
21
|
+
forceMs = DefaultSubprocessForceGraceMs
|
|
22
|
+
): Promise<void> {
|
|
23
|
+
try {
|
|
24
|
+
process.kill("SIGTERM");
|
|
25
|
+
if (await exitsWithin(process.exit, gracefulMs)) {
|
|
26
|
+
return;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
process.kill("SIGKILL");
|
|
30
|
+
if (!(await exitsWithin(process.exit, forceMs))) {
|
|
31
|
+
throw new Error("subprocess did not exit after SIGKILL");
|
|
32
|
+
}
|
|
33
|
+
} catch (error) {
|
|
34
|
+
const cancelExitWait = exitWaitCancellations.get(process);
|
|
35
|
+
exitWaitCancellations.delete(process);
|
|
36
|
+
cancelExitWait?.(error);
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function exitsWithin(
|
|
42
|
+
exit: Promise<number | null>,
|
|
43
|
+
timeoutMs: number
|
|
44
|
+
): Promise<boolean> {
|
|
45
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
46
|
+
const timedOut = new Promise<false>((resolve) => {
|
|
47
|
+
timer = setTimeout(() => {
|
|
48
|
+
resolve(false);
|
|
49
|
+
}, Math.max(timeoutMs, 0));
|
|
50
|
+
});
|
|
51
|
+
try {
|
|
52
|
+
return await Promise.race([exit.then(() => true as const), timedOut]);
|
|
53
|
+
} finally {
|
|
54
|
+
clearTimeout(timer);
|
|
55
|
+
}
|
|
56
|
+
}
|
package/src/cli/main.ts
CHANGED
|
@@ -321,7 +321,8 @@ function describeError(error: unknown): string {
|
|
|
321
321
|
return error.reason;
|
|
322
322
|
}
|
|
323
323
|
try {
|
|
324
|
-
|
|
324
|
+
const serialized = JSON.stringify(error) as string | undefined;
|
|
325
|
+
return serialized ?? String(error);
|
|
325
326
|
} catch {
|
|
326
327
|
return String(error);
|
|
327
328
|
}
|
package/src/cli/version.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const ORCA_VERSION = "0.
|
|
1
|
+
export const ORCA_VERSION = "0.3.0";
|
|
@@ -4,9 +4,15 @@ import {
|
|
|
4
4
|
type BackendTag,
|
|
5
5
|
type ConversationEvent,
|
|
6
6
|
type RuntimeError,
|
|
7
|
+
backendFailed,
|
|
7
8
|
unsupportedFeature
|
|
8
9
|
} from "../model/index.ts";
|
|
9
10
|
import { BoundedAsyncQueue } from "./queue.ts";
|
|
11
|
+
import {
|
|
12
|
+
deferConversationSettlement,
|
|
13
|
+
markConversationCancellationComplete,
|
|
14
|
+
registerConversationCancellationFailureHandler
|
|
15
|
+
} from "./settlement-reservation.ts";
|
|
10
16
|
|
|
11
17
|
export type Outcome<B extends BackendTag = BackendTag> =
|
|
12
18
|
| { readonly type: "success"; readonly result: BackendResult<B> }
|
|
@@ -39,6 +45,7 @@ export class StreamConversation<B extends BackendTag> implements Conversation<B>
|
|
|
39
45
|
private readonly outcome: Promise<Outcome<B>>;
|
|
40
46
|
private settle!: (outcome: Outcome<B>) => void;
|
|
41
47
|
private settled = false;
|
|
48
|
+
private cancellation: Promise<void> | undefined;
|
|
42
49
|
|
|
43
50
|
constructor(private readonly options: StreamConversationOptions<B>) {
|
|
44
51
|
this.queue = new BoundedAsyncQueue(options.capacity ?? 32);
|
|
@@ -80,21 +87,80 @@ export class StreamConversation<B extends BackendTag> implements Conversation<B>
|
|
|
80
87
|
}
|
|
81
88
|
|
|
82
89
|
succeed(result: BackendResult<B>): void {
|
|
83
|
-
this.
|
|
90
|
+
if (this.cancellation !== undefined) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
const outcome: Outcome<B> = { type: "success", result };
|
|
94
|
+
if (
|
|
95
|
+
deferConversationSettlement(this, "success", () => {
|
|
96
|
+
if (this.cancellation === undefined) {
|
|
97
|
+
this.complete(outcome);
|
|
98
|
+
}
|
|
99
|
+
})
|
|
100
|
+
) {
|
|
101
|
+
return;
|
|
102
|
+
}
|
|
103
|
+
this.complete(outcome);
|
|
84
104
|
}
|
|
85
105
|
|
|
86
106
|
fail(error: RuntimeError): void {
|
|
87
|
-
this.
|
|
107
|
+
if (this.cancellation !== undefined) {
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
const outcome: Outcome<B> = { type: "failed", error };
|
|
111
|
+
if (
|
|
112
|
+
deferConversationSettlement(this, "failure", () => {
|
|
113
|
+
if (this.cancellation === undefined) {
|
|
114
|
+
this.complete(outcome);
|
|
115
|
+
}
|
|
116
|
+
})
|
|
117
|
+
) {
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
120
|
+
this.complete(outcome);
|
|
88
121
|
}
|
|
89
122
|
|
|
90
|
-
|
|
123
|
+
cancel(reason?: string): Promise<void> {
|
|
91
124
|
if (this.settled) {
|
|
92
|
-
return;
|
|
125
|
+
return Promise.resolve();
|
|
126
|
+
}
|
|
127
|
+
if (this.cancellation !== undefined) {
|
|
128
|
+
return this.cancellation;
|
|
93
129
|
}
|
|
94
130
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
this
|
|
131
|
+
const cancellation = Promise.withResolvers<undefined>();
|
|
132
|
+
this.cancellation = cancellation.promise;
|
|
133
|
+
registerConversationCancellationFailureHandler(this, (error: unknown) => {
|
|
134
|
+
this.completeCancellationFailure(error, cancellation.reject);
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
let cleanup: Promise<void>;
|
|
138
|
+
try {
|
|
139
|
+
this.abortController.abort(reason);
|
|
140
|
+
cleanup = Promise.resolve(this.options.onCancel?.(reason));
|
|
141
|
+
} catch (error) {
|
|
142
|
+
this.completeCancellationFailure(error, cancellation.reject);
|
|
143
|
+
return cancellation.promise;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
void cleanup.then(
|
|
147
|
+
() => {
|
|
148
|
+
const outcome: Outcome<B> =
|
|
149
|
+
reason === undefined ? { type: "cancelled" } : { type: "cancelled", reason };
|
|
150
|
+
const publish = (): void => {
|
|
151
|
+
this.complete(outcome);
|
|
152
|
+
cancellation.resolve(undefined);
|
|
153
|
+
};
|
|
154
|
+
if (!deferConversationSettlement(this, "cancellation", publish)) {
|
|
155
|
+
publish();
|
|
156
|
+
}
|
|
157
|
+
markConversationCancellationComplete(this);
|
|
158
|
+
},
|
|
159
|
+
(error: unknown) => {
|
|
160
|
+
this.completeCancellationFailure(error, cancellation.reject);
|
|
161
|
+
}
|
|
162
|
+
);
|
|
163
|
+
return cancellation.promise;
|
|
98
164
|
}
|
|
99
165
|
|
|
100
166
|
private complete(outcome: Outcome<B>): void {
|
|
@@ -106,4 +172,23 @@ export class StreamConversation<B extends BackendTag> implements Conversation<B>
|
|
|
106
172
|
this.queue.close();
|
|
107
173
|
this.settle(outcome);
|
|
108
174
|
}
|
|
175
|
+
|
|
176
|
+
private completeCancellationFailure(
|
|
177
|
+
error: unknown,
|
|
178
|
+
rejectCancellation: (reason?: unknown) => void
|
|
179
|
+
): void {
|
|
180
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
181
|
+
const outcome: Outcome<B> = {
|
|
182
|
+
type: "failed",
|
|
183
|
+
error: backendFailed(this.backend, `${this.backend} cancellation cleanup failed: ${message}`)
|
|
184
|
+
};
|
|
185
|
+
const publish = (): void => {
|
|
186
|
+
this.complete(outcome);
|
|
187
|
+
rejectCancellation(error);
|
|
188
|
+
};
|
|
189
|
+
if (!deferConversationSettlement(this, "cancellation_failure", publish)) {
|
|
190
|
+
publish();
|
|
191
|
+
}
|
|
192
|
+
markConversationCancellationComplete(this);
|
|
193
|
+
}
|
|
109
194
|
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
type SettlementKind = "success" | "cancellation" | "failure" | "cancellation_failure";
|
|
2
|
+
|
|
3
|
+
const settlementPriority: Readonly<Record<SettlementKind, number>> = {
|
|
4
|
+
success: 0,
|
|
5
|
+
failure: 1,
|
|
6
|
+
cancellation: 2,
|
|
7
|
+
cancellation_failure: 3
|
|
8
|
+
};
|
|
9
|
+
|
|
10
|
+
interface PendingSettlement {
|
|
11
|
+
readonly kind: SettlementKind;
|
|
12
|
+
readonly publish: () => void;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface SettlementReservation {
|
|
16
|
+
depth: number;
|
|
17
|
+
pending?: PendingSettlement;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const reservations = new WeakMap<object, SettlementReservation>();
|
|
21
|
+
|
|
22
|
+
interface CancellationCompletion {
|
|
23
|
+
readonly promise: Promise<void>;
|
|
24
|
+
readonly resolve: () => void;
|
|
25
|
+
completed: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const cancellationCompletions = new WeakMap<object, CancellationCompletion>();
|
|
29
|
+
const cancellationFailureHandlers = new WeakMap<object, (error: unknown) => void>();
|
|
30
|
+
|
|
31
|
+
function cancellationCompletion(conversation: object): CancellationCompletion {
|
|
32
|
+
const existing = cancellationCompletions.get(conversation);
|
|
33
|
+
if (existing !== undefined) {
|
|
34
|
+
return existing;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const deferred = Promise.withResolvers<undefined>();
|
|
38
|
+
const created: CancellationCompletion = {
|
|
39
|
+
promise: deferred.promise,
|
|
40
|
+
resolve: () => {
|
|
41
|
+
deferred.resolve(undefined);
|
|
42
|
+
},
|
|
43
|
+
completed: false
|
|
44
|
+
};
|
|
45
|
+
cancellationCompletions.set(conversation, created);
|
|
46
|
+
return created;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function observeConversationCancellationCompletion(
|
|
50
|
+
conversation: object
|
|
51
|
+
): Promise<void> {
|
|
52
|
+
return cancellationCompletion(conversation).promise;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function markConversationCancellationComplete(conversation: object): void {
|
|
56
|
+
const completion = cancellationCompletion(conversation);
|
|
57
|
+
if (completion.completed) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
completion.completed = true;
|
|
61
|
+
completion.resolve();
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function registerConversationCancellationFailureHandler(
|
|
65
|
+
conversation: object,
|
|
66
|
+
handler: (error: unknown) => void
|
|
67
|
+
): void {
|
|
68
|
+
cancellationFailureHandlers.set(conversation, handler);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export function reportConversationCancellationFailure(
|
|
72
|
+
conversation: object,
|
|
73
|
+
error: unknown
|
|
74
|
+
): boolean {
|
|
75
|
+
const handler = cancellationFailureHandlers.get(conversation);
|
|
76
|
+
if (handler === undefined) {
|
|
77
|
+
return false;
|
|
78
|
+
}
|
|
79
|
+
handler(error);
|
|
80
|
+
return true;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function reserveConversationSettlement(conversation: object): () => void {
|
|
84
|
+
const reservation = reservations.get(conversation) ?? { depth: 0 };
|
|
85
|
+
reservation.depth += 1;
|
|
86
|
+
reservations.set(conversation, reservation);
|
|
87
|
+
let released = false;
|
|
88
|
+
|
|
89
|
+
return () => {
|
|
90
|
+
if (released) {
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
released = true;
|
|
94
|
+
reservation.depth -= 1;
|
|
95
|
+
if (reservation.depth > 0) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
reservations.delete(conversation);
|
|
100
|
+
const pending = reservation.pending;
|
|
101
|
+
delete reservation.pending;
|
|
102
|
+
pending?.publish();
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function isConversationSettlementReserved(conversation: object): boolean {
|
|
107
|
+
return reservations.has(conversation);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function deferConversationSettlement(
|
|
111
|
+
conversation: object,
|
|
112
|
+
kind: SettlementKind,
|
|
113
|
+
publish: () => void
|
|
114
|
+
): boolean {
|
|
115
|
+
const reservation = reservations.get(conversation);
|
|
116
|
+
if (reservation === undefined) {
|
|
117
|
+
return false;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
if (
|
|
121
|
+
reservation.pending === undefined ||
|
|
122
|
+
settlementPriority[kind] > settlementPriority[reservation.pending.kind]
|
|
123
|
+
) {
|
|
124
|
+
reservation.pending = { kind, publish };
|
|
125
|
+
}
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
@@ -4,6 +4,13 @@ import type { SessionId } from "./brand.ts";
|
|
|
4
4
|
|
|
5
5
|
export type BackendApprovalPolicy = "never" | "on-request" | "on-failure" | "granular" | "untrusted";
|
|
6
6
|
export type BackendSandboxMode = "read-only" | "workspace-write" | "danger-full-access";
|
|
7
|
+
export type CodexReasoningEffort =
|
|
8
|
+
| "low"
|
|
9
|
+
| "medium"
|
|
10
|
+
| "high"
|
|
11
|
+
| "xhigh"
|
|
12
|
+
| "max"
|
|
13
|
+
| "ultra";
|
|
7
14
|
|
|
8
15
|
export interface BackendRetryConfig {
|
|
9
16
|
readonly attempts: number;
|
|
@@ -17,6 +24,7 @@ export interface StructuredOutputConfig<Output = unknown> {
|
|
|
17
24
|
|
|
18
25
|
export interface BackendConfig<B extends BackendTag = BackendTag, Output = unknown> {
|
|
19
26
|
readonly model?: string;
|
|
27
|
+
readonly reasoningEffort?: B extends "codex" ? CodexReasoningEffort : never;
|
|
20
28
|
readonly systemPrompt?: string;
|
|
21
29
|
readonly approvalPolicy?: BackendApprovalPolicy;
|
|
22
30
|
readonly readOnly?: boolean;
|