@xfey/tutti 0.1.78 → 0.1.79
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/dist/chat-assistant/index.d.ts +11 -1
- package/dist/chat-assistant/index.js +61 -7
- package/dist/checks/index.d.ts +22 -0
- package/dist/checks/index.js +110 -4
- package/dist/control-plane/follow-up-start.d.ts +1 -0
- package/dist/control-plane/follow-up-start.js +3 -1
- package/dist/control-plane/index.d.ts +6 -1
- package/dist/control-plane/index.js +72 -11
- package/dist/control-plane/procedure-logging.js +24 -67
- package/dist/control-plane/project-context-bootstrap.js +7 -1
- package/dist/control-plane/reference-summary-refresh.js +1 -1
- package/dist/control-plane/run-result-recording.js +2 -1
- package/dist/control-plane/task-compile-start.d.ts +1 -0
- package/dist/control-plane/task-compile-start.js +11 -1
- package/dist/control-plane/workflows/openai.d.ts +6 -0
- package/dist/control-plane/workflows/openai.js +16 -3
- package/dist/procedure-engine/index.d.ts +1 -0
- package/dist/procedure-engine/index.js +7 -1
- package/dist/process-tree.d.ts +25 -0
- package/dist/process-tree.js +121 -0
- package/dist/providers/openai/app-server/invocation-coordinator.d.ts +28 -0
- package/dist/providers/openai/app-server/invocation-coordinator.js +104 -0
- package/dist/providers/openai/app-server/invocation-lifecycle.d.ts +45 -0
- package/dist/providers/openai/app-server/invocation-lifecycle.js +104 -0
- package/dist/providers/openai/app-server/json-rpc.d.ts +1 -1
- package/dist/providers/openai/app-server/json-rpc.js +9 -16
- package/dist/providers/openai/app-server/read-only-procedure.d.ts +8 -2
- package/dist/providers/openai/app-server/read-only-procedure.js +63 -11
- package/dist/providers/openai/app-server/smoke-types.d.ts +4 -2
- package/dist/providers/openai/app-server/smoke.js +41 -12
- package/dist/providers/openai/app-server/stale-temp-cleanup.d.ts +17 -0
- package/dist/providers/openai/app-server/stale-temp-cleanup.js +130 -0
- package/dist/providers/openai/app-server/workspace-write-run.d.ts +8 -2
- package/dist/providers/openai/app-server/workspace-write-run.js +63 -11
- package/dist/providers/openai/chat-assistant.d.ts +6 -0
- package/dist/providers/openai/chat-assistant.js +12 -0
- package/dist/run-engine/index.js +2 -1
- package/dist/run-pipeline/artifact-applicability.js +2 -2
- package/dist/run-pipeline/openai.d.ts +9 -0
- package/dist/run-pipeline/openai.js +26 -26
- package/dist/run-pipeline/task-run-invocation.js +6 -0
- package/dist/runtime-failures.d.ts +3 -0
- package/dist/runtime-failures.js +22 -0
- package/dist/server-shell/cli/host-lifecycle.js +1 -1
- package/dist/server-shell/cli/host-project-runtime-lifecycle.d.ts +43 -0
- package/dist/server-shell/cli/host-project-runtime-lifecycle.js +54 -0
- package/dist/server-shell/cli/host-server-runtime.js +77 -14
- package/dist/server-shell/http/create-server.d.ts +3 -0
- package/dist/server-shell/http/create-server.js +10 -4
- package/package.json +1 -1
- package/web/assets/{homepage-motion-scene-sNI8fBH3.js → homepage-motion-scene-BFQnHtRi.js} +1 -1
- package/web/assets/index-C4cdSaSb.js +69 -0
- package/web/index.html +1 -1
- package/web/assets/index-d-q9yZ09.js +0 -69
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { recordRunNeedsHumanResult, recordRunResult, } from "../collaboration-state/index.js";
|
|
2
|
+
import { safeRuntimeFailureSummary } from "../runtime-failures.js";
|
|
2
3
|
export function recordRunPipelineResult(input) {
|
|
3
4
|
if (input.pipelineResult.result_kind === "needs_human") {
|
|
4
5
|
const recorded = recordRunNeedsHumanResult(input.store.db, {
|
|
@@ -35,7 +36,7 @@ export function recordRunPipelineResult(input) {
|
|
|
35
36
|
};
|
|
36
37
|
}
|
|
37
38
|
export function recordRunPipelineFailure(input) {
|
|
38
|
-
const summary =
|
|
39
|
+
const summary = safeRuntimeFailureSummary("Run", input.error, "run_failed");
|
|
39
40
|
const recorded = recordRunResult(input.store.db, {
|
|
40
41
|
activity_ref: input.activityRef,
|
|
41
42
|
task_id: input.task.id,
|
|
@@ -17,6 +17,7 @@ export declare function startControlPlaneTaskCompile(options: {
|
|
|
17
17
|
publishProcedureTransition: (state: ProcedureEngineState) => void;
|
|
18
18
|
registerReferenceSummaryWaiter: (activityRef: ActivityRef, waiter: (state: ProcedureEngineState) => void) => void;
|
|
19
19
|
onTasksAdded: () => void;
|
|
20
|
+
shouldContinue?: () => boolean;
|
|
20
21
|
}): ControlPlaneCommandResult<SubmitWorklistDisposition, SubmitWorklistResult>;
|
|
21
22
|
export declare function runControlPlaneTaskCompileProcedure(options: {
|
|
22
23
|
runner: ProcedureWorkflowRunner;
|
|
@@ -137,6 +137,9 @@ function startScratchpadRefreshBeforeTaskCompileActivity(options) {
|
|
|
137
137
|
if (state.transition_kind !== "terminal" || state.activity.status !== "finished") {
|
|
138
138
|
return;
|
|
139
139
|
}
|
|
140
|
+
if (options.shouldContinue?.() === false) {
|
|
141
|
+
return;
|
|
142
|
+
}
|
|
140
143
|
const scratchpad = readScratchpadProjection(options.store.db);
|
|
141
144
|
if (!scratchpadHasSubmittableContent(scratchpad)) {
|
|
142
145
|
return;
|
|
@@ -257,7 +260,7 @@ function startReferenceSummaryGateOrNextStage(options) {
|
|
|
257
260
|
}
|
|
258
261
|
function registerReferenceSummaryGateWaiter(options, activityRef) {
|
|
259
262
|
options.registerReferenceSummaryWaiter(activityRef, (state) => {
|
|
260
|
-
if (state.activity.status !== "finished") {
|
|
263
|
+
if (state.activity.status !== "finished" || options.shouldContinue?.() === false) {
|
|
261
264
|
rollbackTaskCompileContext(options.store.db, {
|
|
262
265
|
workflow_ref: options.taskCompileWorkflowRef,
|
|
263
266
|
now: options.now,
|
|
@@ -300,6 +303,13 @@ function startProjectContextBootstrapActivity(options) {
|
|
|
300
303
|
if (state.transition_kind !== "terminal" || state.activity.status !== "finished") {
|
|
301
304
|
return;
|
|
302
305
|
}
|
|
306
|
+
if (options.shouldContinue?.() === false) {
|
|
307
|
+
rollbackTaskCompileContext(options.store.db, {
|
|
308
|
+
workflow_ref: options.taskCompileWorkflowRef,
|
|
309
|
+
now: options.now,
|
|
310
|
+
});
|
|
311
|
+
return;
|
|
312
|
+
}
|
|
303
313
|
if (shouldInitializeControlPlaneProjectContext(options)) {
|
|
304
314
|
return;
|
|
305
315
|
}
|
|
@@ -4,6 +4,8 @@ import { type OpenAiStructuredOutputClient } from "../../providers/openai/sdk-pr
|
|
|
4
4
|
import { type OpenAiProviderConfig } from "../../providers/openai/provider-config.js";
|
|
5
5
|
import type { ProcedureWorkflowRunner, ProcedureWorkflowRunnerResolution, TaskCompilePromptInput, TaskCompileWorkflowInput } from "./types.js";
|
|
6
6
|
import type { CodexAppServerAgentContextRuntime } from "../../providers/openai/app-server/skills.js";
|
|
7
|
+
import type { CodexAppServerInvocationCoordinator } from "../../providers/openai/app-server/invocation-coordinator.js";
|
|
8
|
+
import type { CodexAppServerLifecycleIncident } from "../../providers/openai/app-server/invocation-lifecycle.js";
|
|
7
9
|
export type OpenAiProcedureWorkflowRunnerOptions = {
|
|
8
10
|
config: OpenAiProviderConfig;
|
|
9
11
|
client: OpenAiStructuredOutputClient;
|
|
@@ -20,6 +22,8 @@ export type OpenAiProcedureWorkflowRunnerOptions = {
|
|
|
20
22
|
userSkillsRoot?: string;
|
|
21
23
|
builtInSkillsRoot?: string;
|
|
22
24
|
agentContext?: CodexAppServerAgentContextRuntime;
|
|
25
|
+
invocationCoordinator?: CodexAppServerInvocationCoordinator;
|
|
26
|
+
onLifecycleIncident?: (incident: CodexAppServerLifecycleIncident) => void;
|
|
23
27
|
};
|
|
24
28
|
};
|
|
25
29
|
export type ProjectOpenAiProcedureWorkflowRunnerOptions = {
|
|
@@ -29,6 +33,8 @@ export type ProjectOpenAiProcedureWorkflowRunnerOptions = {
|
|
|
29
33
|
promptsRoot?: string;
|
|
30
34
|
agentContext?: CodexAppServerAgentContextRuntime;
|
|
31
35
|
recordUsage?: ProviderUsageRecorder;
|
|
36
|
+
invocationCoordinator?: CodexAppServerInvocationCoordinator;
|
|
37
|
+
onLifecycleIncident?: (incident: CodexAppServerLifecycleIncident) => void;
|
|
32
38
|
};
|
|
33
39
|
export declare function buildTaskCompilePromptInput(input: TaskCompileWorkflowInput): TaskCompilePromptInput;
|
|
34
40
|
export declare function createOpenAiProcedureWorkflowRunner(options: OpenAiProcedureWorkflowRunnerOptions): ProcedureWorkflowRunner;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import { copyFileSync, mkdtempSync
|
|
1
|
+
import { copyFileSync, mkdtempSync } from "node:fs";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
2
3
|
import { tmpdir } from "node:os";
|
|
3
4
|
import { extname, isAbsolute, join, relative, resolve } from "node:path";
|
|
4
5
|
import { renderPromptTemplate } from "../../prompt-templates/index.js";
|
|
@@ -46,6 +47,12 @@ function readOnlyAppServerOptions(options, prompt, outputSchema, skillSelectionN
|
|
|
46
47
|
...(options.readOnlyAppServer.agentContext === undefined
|
|
47
48
|
? {}
|
|
48
49
|
: { agentContext: options.readOnlyAppServer.agentContext }),
|
|
50
|
+
...(options.readOnlyAppServer.invocationCoordinator === undefined
|
|
51
|
+
? {}
|
|
52
|
+
: { invocationCoordinator: options.readOnlyAppServer.invocationCoordinator }),
|
|
53
|
+
...(options.readOnlyAppServer.onLifecycleIncident === undefined
|
|
54
|
+
? {}
|
|
55
|
+
: { onLifecycleIncident: options.readOnlyAppServer.onLifecycleIncident }),
|
|
49
56
|
};
|
|
50
57
|
}
|
|
51
58
|
function readOnlyRuntimeTelemetry(context, stageId) {
|
|
@@ -81,7 +88,7 @@ function createReferenceSummaryWorkspace(input) {
|
|
|
81
88
|
copyFileSync(input.sourcePath, join(root, `target${extension}`));
|
|
82
89
|
return {
|
|
83
90
|
cwd: root,
|
|
84
|
-
cleanup: () =>
|
|
91
|
+
cleanup: () => rm(root, { recursive: true, force: true, maxRetries: 4, retryDelay: 50 }),
|
|
85
92
|
};
|
|
86
93
|
}
|
|
87
94
|
function referenceFileSummaryWorkflowOutput(output, originalPath) {
|
|
@@ -379,7 +386,7 @@ export function createOpenAiProcedureWorkflowRunner(options) {
|
|
|
379
386
|
return referenceFileSummaryWorkflowOutput(result.output, input.file.path);
|
|
380
387
|
}
|
|
381
388
|
finally {
|
|
382
|
-
workspace.cleanup();
|
|
389
|
+
await workspace.cleanup();
|
|
383
390
|
}
|
|
384
391
|
};
|
|
385
392
|
}
|
|
@@ -420,6 +427,12 @@ export function resolveProjectOpenAiProcedureWorkflowRunner(options) {
|
|
|
420
427
|
tuttiHome: options.tuttiHome,
|
|
421
428
|
userSkillsRoot: getProjectUserSkillsRoot(options.tuttiHome, options.projectId),
|
|
422
429
|
...(options.agentContext === undefined ? {} : { agentContext: options.agentContext }),
|
|
430
|
+
...(options.invocationCoordinator === undefined
|
|
431
|
+
? {}
|
|
432
|
+
: { invocationCoordinator: options.invocationCoordinator }),
|
|
433
|
+
...(options.onLifecycleIncident === undefined
|
|
434
|
+
? {}
|
|
435
|
+
: { onLifecycleIncident: options.onLifecycleIncident }),
|
|
423
436
|
},
|
|
424
437
|
...(options.recordUsage === undefined ? {} : { recordUsage: options.recordUsage }),
|
|
425
438
|
}),
|
|
@@ -56,6 +56,7 @@ export declare class ProcedureEngine {
|
|
|
56
56
|
getActiveProcedures(): ActivityProjection[];
|
|
57
57
|
getExecutionStatus(): ExecutionStatusProjection;
|
|
58
58
|
waitForIdle(): Promise<void>;
|
|
59
|
+
waitForLaneIdle(lane: ProcedureLane): Promise<void>;
|
|
59
60
|
private stateForActivity;
|
|
60
61
|
private updateActiveProgress;
|
|
61
62
|
private finishActive;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { createActivityRef } from "@tutti/shared/ids";
|
|
2
|
+
import { safeRuntimeFailureSummary } from "../runtime-failures.js";
|
|
2
3
|
const DEFAULT_PROCEDURE_LANE = "foreground";
|
|
3
4
|
const PROCEDURE_LANE_ORDER = ["foreground", "scratchpad_background"];
|
|
4
5
|
export class ProcedureEngine {
|
|
@@ -55,7 +56,7 @@ export class ProcedureEngine {
|
|
|
55
56
|
}
|
|
56
57
|
this.finishActive(activityRef, lane, {
|
|
57
58
|
kind: "failed",
|
|
58
|
-
summary:
|
|
59
|
+
summary: safeRuntimeFailureSummary("Procedure", error, "procedure_failed"),
|
|
59
60
|
}, options.onTransition);
|
|
60
61
|
}
|
|
61
62
|
};
|
|
@@ -101,6 +102,11 @@ export class ProcedureEngine {
|
|
|
101
102
|
await Promise.all(this.orderedActiveProcedures().map((active) => active.promise));
|
|
102
103
|
}
|
|
103
104
|
}
|
|
105
|
+
async waitForLaneIdle(lane) {
|
|
106
|
+
while (this.activeByLane.has(lane)) {
|
|
107
|
+
await this.activeByLane.get(lane)?.promise;
|
|
108
|
+
}
|
|
109
|
+
}
|
|
104
110
|
stateForActivity(activity, transitionKind, progress) {
|
|
105
111
|
const state = {
|
|
106
112
|
activity,
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
import type { ChildProcess } from "node:child_process";
|
|
2
|
+
export type ManagedChildProcess<TChild extends ChildProcess = ChildProcess> = {
|
|
3
|
+
child: TChild;
|
|
4
|
+
processGroupId?: number;
|
|
5
|
+
hasClosed: () => boolean;
|
|
6
|
+
closed: Promise<{
|
|
7
|
+
code: number | null;
|
|
8
|
+
signal: NodeJS.Signals | null;
|
|
9
|
+
}>;
|
|
10
|
+
};
|
|
11
|
+
export type ProcessTreeTerminationOptions = {
|
|
12
|
+
termGraceMs?: number;
|
|
13
|
+
killGraceMs?: number;
|
|
14
|
+
pollIntervalMs?: number;
|
|
15
|
+
};
|
|
16
|
+
export declare class ProcessTreeTerminationError extends Error {
|
|
17
|
+
readonly code: "process_tree_not_terminated";
|
|
18
|
+
constructor(code: "process_tree_not_terminated", message: string);
|
|
19
|
+
}
|
|
20
|
+
export declare function systemErrorCode(error: unknown): string;
|
|
21
|
+
export declare function createManagedChildProcess<TChild extends ChildProcess>(child: TChild): ManagedChildProcess<TChild>;
|
|
22
|
+
export declare function terminateManagedProcessTree(managedProcess: ManagedChildProcess, options?: ProcessTreeTerminationOptions): Promise<{
|
|
23
|
+
forced: boolean;
|
|
24
|
+
}>;
|
|
25
|
+
//# sourceMappingURL=process-tree.d.ts.map
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
export class ProcessTreeTerminationError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = "ProcessTreeTerminationError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
const DEFAULT_TERM_GRACE_MS = 3_000;
|
|
10
|
+
const DEFAULT_KILL_GRACE_MS = 2_000;
|
|
11
|
+
const DEFAULT_POLL_INTERVAL_MS = 25;
|
|
12
|
+
export function systemErrorCode(error) {
|
|
13
|
+
if (typeof error === "object" &&
|
|
14
|
+
error !== null &&
|
|
15
|
+
"code" in error &&
|
|
16
|
+
(typeof error.code === "string" || typeof error.code === "number")) {
|
|
17
|
+
return String(error.code);
|
|
18
|
+
}
|
|
19
|
+
return error instanceof Error && error.name !== "" ? error.name : "UnknownError";
|
|
20
|
+
}
|
|
21
|
+
export function createManagedChildProcess(child) {
|
|
22
|
+
let closedState = false;
|
|
23
|
+
const closed = new Promise((resolve) => {
|
|
24
|
+
let settled = false;
|
|
25
|
+
const settle = (code, signal) => {
|
|
26
|
+
if (settled) {
|
|
27
|
+
return;
|
|
28
|
+
}
|
|
29
|
+
settled = true;
|
|
30
|
+
closedState = true;
|
|
31
|
+
resolve({ code, signal });
|
|
32
|
+
};
|
|
33
|
+
if (child.exitCode !== null || child.signalCode !== null) {
|
|
34
|
+
settle(child.exitCode, child.signalCode);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
child.once("close", settle);
|
|
38
|
+
child.once("error", () => settle(child.exitCode, child.signalCode));
|
|
39
|
+
});
|
|
40
|
+
return {
|
|
41
|
+
child,
|
|
42
|
+
...(process.platform === "win32" || child.pid === undefined
|
|
43
|
+
? {}
|
|
44
|
+
: { processGroupId: child.pid }),
|
|
45
|
+
hasClosed: () => closedState,
|
|
46
|
+
closed,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function processGroupIsAlive(processGroupId) {
|
|
50
|
+
try {
|
|
51
|
+
process.kill(-processGroupId, 0);
|
|
52
|
+
return true;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
return systemErrorCode(error) !== "ESRCH";
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
function childIsAlive(managedProcess) {
|
|
59
|
+
return (!managedProcess.hasClosed() &&
|
|
60
|
+
managedProcess.child.exitCode === null &&
|
|
61
|
+
managedProcess.child.signalCode === null);
|
|
62
|
+
}
|
|
63
|
+
function processTreeIsAlive(managedProcess) {
|
|
64
|
+
if (managedProcess.processGroupId !== undefined) {
|
|
65
|
+
return processGroupIsAlive(managedProcess.processGroupId);
|
|
66
|
+
}
|
|
67
|
+
return childIsAlive(managedProcess);
|
|
68
|
+
}
|
|
69
|
+
function sendProcessTreeSignal(managedProcess, signal) {
|
|
70
|
+
if (managedProcess.processGroupId !== undefined) {
|
|
71
|
+
try {
|
|
72
|
+
process.kill(-managedProcess.processGroupId, signal);
|
|
73
|
+
return;
|
|
74
|
+
}
|
|
75
|
+
catch (error) {
|
|
76
|
+
if (systemErrorCode(error) === "ESRCH") {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (childIsAlive(managedProcess)) {
|
|
82
|
+
try {
|
|
83
|
+
managedProcess.child.kill(signal);
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// The bounded wait below decides whether termination actually succeeded.
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function wait(delayMs) {
|
|
91
|
+
return new Promise((resolve) => setTimeout(resolve, delayMs));
|
|
92
|
+
}
|
|
93
|
+
async function waitForProcessTreeExit(managedProcess, timeoutMs, pollIntervalMs) {
|
|
94
|
+
const deadline = Date.now() + timeoutMs;
|
|
95
|
+
while (processTreeIsAlive(managedProcess)) {
|
|
96
|
+
const remainingMs = deadline - Date.now();
|
|
97
|
+
if (remainingMs <= 0) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
await wait(Math.min(pollIntervalMs, remainingMs));
|
|
101
|
+
}
|
|
102
|
+
await Promise.race([managedProcess.closed.then(() => undefined), wait(pollIntervalMs)]);
|
|
103
|
+
return !processTreeIsAlive(managedProcess);
|
|
104
|
+
}
|
|
105
|
+
export async function terminateManagedProcessTree(managedProcess, options = {}) {
|
|
106
|
+
if (!processTreeIsAlive(managedProcess)) {
|
|
107
|
+
await managedProcess.closed;
|
|
108
|
+
return { forced: false };
|
|
109
|
+
}
|
|
110
|
+
const pollIntervalMs = options.pollIntervalMs ?? DEFAULT_POLL_INTERVAL_MS;
|
|
111
|
+
sendProcessTreeSignal(managedProcess, "SIGTERM");
|
|
112
|
+
if (await waitForProcessTreeExit(managedProcess, options.termGraceMs ?? DEFAULT_TERM_GRACE_MS, pollIntervalMs)) {
|
|
113
|
+
return { forced: false };
|
|
114
|
+
}
|
|
115
|
+
sendProcessTreeSignal(managedProcess, "SIGKILL");
|
|
116
|
+
if (await waitForProcessTreeExit(managedProcess, options.killGraceMs ?? DEFAULT_KILL_GRACE_MS, pollIntervalMs)) {
|
|
117
|
+
return { forced: true };
|
|
118
|
+
}
|
|
119
|
+
throw new ProcessTreeTerminationError("process_tree_not_terminated", "Child process tree did not terminate within the bounded shutdown window.");
|
|
120
|
+
}
|
|
121
|
+
//# sourceMappingURL=process-tree.js.map
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
export type CodexAppServerInvocationLease = {
|
|
2
|
+
setCloser: (closer: () => Promise<unknown>) => void;
|
|
3
|
+
release: () => void;
|
|
4
|
+
};
|
|
5
|
+
export declare class CodexAppServerAdmissionError extends Error {
|
|
6
|
+
readonly code: "admission_closed" | "queue_full";
|
|
7
|
+
constructor(code: "admission_closed" | "queue_full", message: string);
|
|
8
|
+
}
|
|
9
|
+
export declare class CodexAppServerInvocationCoordinator {
|
|
10
|
+
private accepting;
|
|
11
|
+
private active;
|
|
12
|
+
private readonly queue;
|
|
13
|
+
private readonly idleWaiters;
|
|
14
|
+
private readonly maxQueued;
|
|
15
|
+
constructor(options?: {
|
|
16
|
+
maxQueued?: number;
|
|
17
|
+
});
|
|
18
|
+
get activeCount(): number;
|
|
19
|
+
get queuedCount(): number;
|
|
20
|
+
acquire(): Promise<CodexAppServerInvocationLease>;
|
|
21
|
+
stopAccepting(): void;
|
|
22
|
+
closeActive(): Promise<void>;
|
|
23
|
+
waitForIdle(): Promise<void>;
|
|
24
|
+
private activate;
|
|
25
|
+
private startNext;
|
|
26
|
+
private resolveIdleIfNeeded;
|
|
27
|
+
}
|
|
28
|
+
//# sourceMappingURL=invocation-coordinator.d.ts.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
export class CodexAppServerAdmissionError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
constructor(code, message) {
|
|
4
|
+
super(message);
|
|
5
|
+
this.code = code;
|
|
6
|
+
this.name = "CodexAppServerAdmissionError";
|
|
7
|
+
}
|
|
8
|
+
}
|
|
9
|
+
export class CodexAppServerInvocationCoordinator {
|
|
10
|
+
accepting = true;
|
|
11
|
+
active = null;
|
|
12
|
+
queue = [];
|
|
13
|
+
idleWaiters = new Set();
|
|
14
|
+
maxQueued;
|
|
15
|
+
constructor(options = {}) {
|
|
16
|
+
this.maxQueued = options.maxQueued ?? 32;
|
|
17
|
+
}
|
|
18
|
+
get activeCount() {
|
|
19
|
+
return this.active === null ? 0 : 1;
|
|
20
|
+
}
|
|
21
|
+
get queuedCount() {
|
|
22
|
+
return this.queue.length;
|
|
23
|
+
}
|
|
24
|
+
acquire() {
|
|
25
|
+
if (!this.accepting) {
|
|
26
|
+
return Promise.reject(new CodexAppServerAdmissionError("admission_closed", "Codex app-server admission is closed."));
|
|
27
|
+
}
|
|
28
|
+
if (this.active === null) {
|
|
29
|
+
return Promise.resolve(this.activate());
|
|
30
|
+
}
|
|
31
|
+
if (this.queue.length >= this.maxQueued) {
|
|
32
|
+
return Promise.reject(new CodexAppServerAdmissionError("queue_full", "Codex app-server admission queue is full."));
|
|
33
|
+
}
|
|
34
|
+
return new Promise((resolve, reject) => {
|
|
35
|
+
this.queue.push({ resolve, reject });
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
stopAccepting() {
|
|
39
|
+
if (!this.accepting) {
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
this.accepting = false;
|
|
43
|
+
const error = new CodexAppServerAdmissionError("admission_closed", "Codex app-server admission is closed.");
|
|
44
|
+
for (const pending of this.queue.splice(0)) {
|
|
45
|
+
pending.reject(error);
|
|
46
|
+
}
|
|
47
|
+
this.resolveIdleIfNeeded();
|
|
48
|
+
}
|
|
49
|
+
async closeActive() {
|
|
50
|
+
const active = this.active;
|
|
51
|
+
if (active?.closer !== undefined) {
|
|
52
|
+
await active.closer();
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
waitForIdle() {
|
|
56
|
+
if (this.active === null && this.queue.length === 0) {
|
|
57
|
+
return Promise.resolve();
|
|
58
|
+
}
|
|
59
|
+
return new Promise((resolve) => this.idleWaiters.add(resolve));
|
|
60
|
+
}
|
|
61
|
+
activate() {
|
|
62
|
+
const admission = { released: false };
|
|
63
|
+
this.active = admission;
|
|
64
|
+
return {
|
|
65
|
+
setCloser: (closer) => {
|
|
66
|
+
if (!admission.released) {
|
|
67
|
+
admission.closer = closer;
|
|
68
|
+
}
|
|
69
|
+
},
|
|
70
|
+
release: () => {
|
|
71
|
+
if (admission.released) {
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
admission.released = true;
|
|
75
|
+
if (this.active === admission) {
|
|
76
|
+
this.active = null;
|
|
77
|
+
this.startNext();
|
|
78
|
+
}
|
|
79
|
+
},
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
startNext() {
|
|
83
|
+
if (!this.accepting) {
|
|
84
|
+
this.resolveIdleIfNeeded();
|
|
85
|
+
return;
|
|
86
|
+
}
|
|
87
|
+
const pending = this.queue.shift();
|
|
88
|
+
if (pending === undefined) {
|
|
89
|
+
this.resolveIdleIfNeeded();
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
pending.resolve(this.activate());
|
|
93
|
+
}
|
|
94
|
+
resolveIdleIfNeeded() {
|
|
95
|
+
if (this.active !== null || this.queue.length !== 0) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
for (const resolve of this.idleWaiters) {
|
|
99
|
+
resolve();
|
|
100
|
+
}
|
|
101
|
+
this.idleWaiters.clear();
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=invocation-coordinator.js.map
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { createManagedChildProcess, type ManagedChildProcess, type ProcessTreeTerminationOptions } from "../../../process-tree.js";
|
|
2
|
+
export type ManagedCodexAppServerProcess = ManagedChildProcess;
|
|
3
|
+
export type CodexAppServerTerminationOptions = ProcessTreeTerminationOptions;
|
|
4
|
+
export declare const createManagedProcess: typeof createManagedChildProcess;
|
|
5
|
+
export type CodexAppServerLifecycleIncident = {
|
|
6
|
+
code: "protocol_close_failed";
|
|
7
|
+
error_code: string;
|
|
8
|
+
} | {
|
|
9
|
+
code: "temp_cleanup_failed";
|
|
10
|
+
error_code: string;
|
|
11
|
+
} | {
|
|
12
|
+
code: "process_termination_failed";
|
|
13
|
+
error_code: string;
|
|
14
|
+
};
|
|
15
|
+
export type CodexAppServerInvocationCloseResult = {
|
|
16
|
+
forced: boolean;
|
|
17
|
+
cleanup_status: "removed" | "kept" | "failed";
|
|
18
|
+
};
|
|
19
|
+
export declare class CodexAppServerLifecycleError extends Error {
|
|
20
|
+
readonly code: "process_tree_not_terminated";
|
|
21
|
+
constructor(code: "process_tree_not_terminated", message: string);
|
|
22
|
+
}
|
|
23
|
+
export declare const CODEX_APP_SERVER_OWNER_MARKER_FILE = ".tutti-app-server-owner.json";
|
|
24
|
+
export declare const CODEX_APP_SERVER_TEMP_PREFIXES: readonly ["tutti-codex-readonly-procedure-", "tutti-codex-workspace-write-run-", "tutti-codex-app-server-smoke-"];
|
|
25
|
+
export declare function terminateCodexAppServerProcessTree(managedProcess: ManagedCodexAppServerProcess, options?: CodexAppServerTerminationOptions): Promise<{
|
|
26
|
+
forced: boolean;
|
|
27
|
+
}>;
|
|
28
|
+
export declare class CodexAppServerInvocation {
|
|
29
|
+
private readonly options;
|
|
30
|
+
private closePromise;
|
|
31
|
+
constructor(options: {
|
|
32
|
+
managedProcess: ManagedCodexAppServerProcess;
|
|
33
|
+
tempRoot: string;
|
|
34
|
+
closeProtocol: () => void | Promise<void>;
|
|
35
|
+
keepTemp?: boolean;
|
|
36
|
+
termination?: CodexAppServerTerminationOptions;
|
|
37
|
+
removeTempRoot?: (tempRoot: string) => Promise<void>;
|
|
38
|
+
onIncident?: (incident: CodexAppServerLifecycleIncident) => void;
|
|
39
|
+
});
|
|
40
|
+
close(): Promise<CodexAppServerInvocationCloseResult>;
|
|
41
|
+
reportProcessTerminationFailure(error: unknown): void;
|
|
42
|
+
private emitIncident;
|
|
43
|
+
private closeOnce;
|
|
44
|
+
}
|
|
45
|
+
//# sourceMappingURL=invocation-lifecycle.d.ts.map
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { writeFileSync } from "node:fs";
|
|
2
|
+
import { rm } from "node:fs/promises";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { createManagedChildProcess, ProcessTreeTerminationError, systemErrorCode, terminateManagedProcessTree, } from "../../../process-tree.js";
|
|
5
|
+
export const createManagedProcess = createManagedChildProcess;
|
|
6
|
+
export class CodexAppServerLifecycleError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.name = "CodexAppServerLifecycleError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
const TEMP_ROOT_CLEANUP_MAX_RETRIES = 4;
|
|
15
|
+
const TEMP_ROOT_CLEANUP_RETRY_DELAY_MS = 50;
|
|
16
|
+
export const CODEX_APP_SERVER_OWNER_MARKER_FILE = ".tutti-app-server-owner.json";
|
|
17
|
+
export const CODEX_APP_SERVER_TEMP_PREFIXES = [
|
|
18
|
+
"tutti-codex-readonly-procedure-",
|
|
19
|
+
"tutti-codex-workspace-write-run-",
|
|
20
|
+
"tutti-codex-app-server-smoke-",
|
|
21
|
+
];
|
|
22
|
+
export async function terminateCodexAppServerProcessTree(managedProcess, options = {}) {
|
|
23
|
+
try {
|
|
24
|
+
return await terminateManagedProcessTree(managedProcess, options);
|
|
25
|
+
}
|
|
26
|
+
catch (error) {
|
|
27
|
+
if (error instanceof ProcessTreeTerminationError) {
|
|
28
|
+
throw new CodexAppServerLifecycleError("process_tree_not_terminated", "Codex app-server process tree did not terminate within the bounded shutdown window.");
|
|
29
|
+
}
|
|
30
|
+
throw error;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
async function removeTempRootWithRetries(tempRoot) {
|
|
34
|
+
await rm(tempRoot, {
|
|
35
|
+
recursive: true,
|
|
36
|
+
force: true,
|
|
37
|
+
maxRetries: TEMP_ROOT_CLEANUP_MAX_RETRIES,
|
|
38
|
+
retryDelay: TEMP_ROOT_CLEANUP_RETRY_DELAY_MS,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
export class CodexAppServerInvocation {
|
|
42
|
+
options;
|
|
43
|
+
closePromise;
|
|
44
|
+
constructor(options) {
|
|
45
|
+
this.options = options;
|
|
46
|
+
try {
|
|
47
|
+
writeFileSync(join(options.tempRoot, CODEX_APP_SERVER_OWNER_MARKER_FILE), JSON.stringify({
|
|
48
|
+
version: 1,
|
|
49
|
+
owner_pid: process.pid,
|
|
50
|
+
process_group_id: options.managedProcess.processGroupId ?? null,
|
|
51
|
+
created_at: new Date().toISOString(),
|
|
52
|
+
keep_temp: options.keepTemp === true,
|
|
53
|
+
}), { encoding: "utf8", mode: 0o600 });
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// The marker is defense-in-depth for later stale cleanup, not an invocation prerequisite.
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
close() {
|
|
60
|
+
this.closePromise ??= this.closeOnce();
|
|
61
|
+
return this.closePromise;
|
|
62
|
+
}
|
|
63
|
+
reportProcessTerminationFailure(error) {
|
|
64
|
+
this.emitIncident({
|
|
65
|
+
code: "process_termination_failed",
|
|
66
|
+
error_code: systemErrorCode(error),
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
emitIncident(incident) {
|
|
70
|
+
try {
|
|
71
|
+
this.options.onIncident?.(incident);
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Diagnostic hooks must never change provider lifecycle semantics.
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
async closeOnce() {
|
|
78
|
+
try {
|
|
79
|
+
await this.options.closeProtocol();
|
|
80
|
+
}
|
|
81
|
+
catch (error) {
|
|
82
|
+
this.emitIncident({
|
|
83
|
+
code: "protocol_close_failed",
|
|
84
|
+
error_code: systemErrorCode(error),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
const termination = await terminateCodexAppServerProcessTree(this.options.managedProcess, this.options.termination);
|
|
88
|
+
if (this.options.keepTemp === true) {
|
|
89
|
+
return { forced: termination.forced, cleanup_status: "kept" };
|
|
90
|
+
}
|
|
91
|
+
try {
|
|
92
|
+
await (this.options.removeTempRoot ?? removeTempRootWithRetries)(this.options.tempRoot);
|
|
93
|
+
return { forced: termination.forced, cleanup_status: "removed" };
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
this.emitIncident({
|
|
97
|
+
code: "temp_cleanup_failed",
|
|
98
|
+
error_code: systemErrorCode(error),
|
|
99
|
+
});
|
|
100
|
+
return { forced: termination.forced, cleanup_status: "failed" };
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
//# sourceMappingURL=invocation-lifecycle.js.map
|
|
@@ -64,7 +64,7 @@ export declare class CodexAppServerJsonRpcClient {
|
|
|
64
64
|
waitForServerRequest(timeoutMs: number): Promise<CodexAppServerJsonRpcServerRequest>;
|
|
65
65
|
respondToServerRequest(id: CodexAppServerRequestId, result: unknown): void;
|
|
66
66
|
rejectServerRequest(id: CodexAppServerRequestId, error: CodexAppServerJsonRpcError): void;
|
|
67
|
-
|
|
67
|
+
close(): void;
|
|
68
68
|
private rejectAll;
|
|
69
69
|
private writeServerRequestResponse;
|
|
70
70
|
private handleStdout;
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import { StringDecoder } from "node:string_decoder";
|
|
2
2
|
import { redactText } from "@tutti/shared/utils";
|
|
3
|
-
const PROCESS_EXIT_STDERR_EXCERPT_MAX_LENGTH = 500;
|
|
4
3
|
export class CodexAppServerProtocolError extends Error {
|
|
5
4
|
code;
|
|
6
5
|
constructor(message, code) {
|
|
@@ -22,16 +21,6 @@ function normalizeProtocolError(method, error) {
|
|
|
22
21
|
const message = typeof rawMessage === "string" ? redactText(rawMessage) : "protocol_error";
|
|
23
22
|
return new CodexAppServerProtocolError(`Codex app-server request failed: ${method}: ${code}: ${message}`, "request_failed");
|
|
24
23
|
}
|
|
25
|
-
function processExitStderrExcerpt(value) {
|
|
26
|
-
const normalized = redactText(value).replace(/\s+/gu, " ").trim();
|
|
27
|
-
if (normalized.length === 0) {
|
|
28
|
-
return "";
|
|
29
|
-
}
|
|
30
|
-
if (normalized.length <= PROCESS_EXIT_STDERR_EXCERPT_MAX_LENGTH) {
|
|
31
|
-
return normalized;
|
|
32
|
-
}
|
|
33
|
-
return `${normalized.slice(0, PROCESS_EXIT_STDERR_EXCERPT_MAX_LENGTH)}...`;
|
|
34
|
-
}
|
|
35
24
|
export class CodexAppServerJsonRpcClient {
|
|
36
25
|
child;
|
|
37
26
|
options;
|
|
@@ -58,8 +47,7 @@ export class CodexAppServerJsonRpcClient {
|
|
|
58
47
|
});
|
|
59
48
|
child.on("exit", (code, signal) => {
|
|
60
49
|
this.closed = true;
|
|
61
|
-
|
|
62
|
-
this.rejectAll(new CodexAppServerProtocolError(`Codex app-server exited before request completed: code=${code ?? "null"} signal=${signal ?? "null"}${stderrExcerpt === "" ? "" : ` stderr=${stderrExcerpt}`}`, "process_exited"));
|
|
50
|
+
this.rejectAll(new CodexAppServerProtocolError(`Codex app-server exited before request completed: code=${code ?? "null"} signal=${signal ?? "null"}`, "process_exited"));
|
|
63
51
|
});
|
|
64
52
|
child.on("error", (error) => {
|
|
65
53
|
this.closed = true;
|
|
@@ -185,9 +173,14 @@ export class CodexAppServerJsonRpcClient {
|
|
|
185
173
|
rejectServerRequest(id, error) {
|
|
186
174
|
this.writeServerRequestResponse(id, { id, error });
|
|
187
175
|
}
|
|
188
|
-
|
|
189
|
-
if (
|
|
190
|
-
|
|
176
|
+
close() {
|
|
177
|
+
if (this.closed) {
|
|
178
|
+
return;
|
|
179
|
+
}
|
|
180
|
+
this.closed = true;
|
|
181
|
+
this.rejectAll(new CodexAppServerProtocolError("Codex app-server protocol client is closed", "process_closed"));
|
|
182
|
+
if (!this.child.stdin.destroyed) {
|
|
183
|
+
this.child.stdin.end();
|
|
191
184
|
}
|
|
192
185
|
}
|
|
193
186
|
rejectAll(error) {
|