@datename/fastcode-termux 0.1.6 → 0.1.8-beta.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/LICENSE +4 -0
- package/README.md +67 -16
- package/dist/agent/approval-json.d.ts +43 -0
- package/dist/agent/approval-json.js +69 -0
- package/dist/agent/approval-native.d.ts +3 -0
- package/dist/agent/approval-native.js +165 -0
- package/dist/agent/approval-store.d.ts +16 -0
- package/dist/agent/approval-store.js +21 -0
- package/dist/agent/approval.d.ts +83 -0
- package/dist/agent/approval.js +42 -0
- package/dist/agent/compaction.d.ts +41 -0
- package/dist/agent/compaction.js +270 -0
- package/dist/agent/context.d.ts +29 -0
- package/dist/agent/context.js +62 -0
- package/dist/agent/guards.d.ts +24 -16
- package/dist/agent/guards.js +1167 -18
- package/dist/agent/ignore.d.ts +11 -0
- package/dist/agent/ignore.js +129 -0
- package/dist/agent/policy/defaults.d.ts +16 -0
- package/dist/agent/policy/defaults.js +17 -0
- package/dist/agent/policy/egress.d.ts +32 -0
- package/dist/agent/policy/egress.js +280 -0
- package/dist/agent/process-tree.d.ts +22 -0
- package/dist/agent/process-tree.js +102 -0
- package/dist/agent/protocol-mode.d.ts +6 -0
- package/dist/agent/protocol-mode.js +51 -0
- package/dist/agent/protocol.d.ts +20 -1
- package/dist/agent/protocol.js +169 -39
- package/dist/agent/registry.d.ts +19 -0
- package/dist/agent/registry.js +247 -0
- package/dist/agent/runner.d.ts +127 -0
- package/dist/agent/runner.js +391 -0
- package/dist/agent/shell-ast.d.ts +11 -0
- package/dist/agent/shell-ast.js +204 -0
- package/dist/agent/shell-session.d.ts +51 -0
- package/dist/agent/shell-session.js +611 -0
- package/dist/agent/stream.d.ts +29 -0
- package/dist/agent/stream.js +103 -0
- package/dist/agent/tool-schemas.d.ts +2 -0
- package/dist/agent/tool-schemas.js +82 -0
- package/dist/agent/tools/append_file.d.ts +2 -0
- package/dist/agent/tools/append_file.js +28 -0
- package/dist/agent/tools/apply_patch.d.ts +10 -0
- package/dist/agent/tools/apply_patch.js +92 -0
- package/dist/agent/tools/copy_file.d.ts +2 -0
- package/dist/agent/tools/copy_file.js +53 -0
- package/dist/agent/tools/delete_file.d.ts +2 -0
- package/dist/agent/tools/delete_file.js +43 -0
- package/dist/agent/tools/grep.d.ts +2 -0
- package/dist/agent/tools/grep.js +54 -0
- package/dist/agent/tools/index.d.ts +2 -0
- package/dist/agent/tools/index.js +32 -0
- package/dist/agent/tools/list_files.d.ts +2 -0
- package/dist/agent/tools/list_files.js +17 -0
- package/dist/agent/tools/make_dir.d.ts +2 -0
- package/dist/agent/tools/make_dir.js +26 -0
- package/dist/agent/tools/move_path.d.ts +9 -0
- package/dist/agent/tools/move_path.js +73 -0
- package/dist/agent/tools/paths.d.ts +7 -0
- package/dist/agent/tools/paths.js +84 -0
- package/dist/agent/tools/pwd.d.ts +2 -0
- package/dist/agent/tools/pwd.js +10 -0
- package/dist/agent/tools/read_file.d.ts +2 -0
- package/dist/agent/tools/read_file.js +81 -0
- package/dist/agent/tools/read_many.d.ts +2 -0
- package/dist/agent/tools/read_many.js +43 -0
- package/dist/agent/tools/readers.d.ts +58 -0
- package/dist/agent/tools/readers.js +426 -0
- package/dist/agent/tools/replace_in_file.d.ts +2 -0
- package/dist/agent/tools/replace_in_file.js +63 -0
- package/dist/agent/tools/shell.d.ts +2 -0
- package/dist/agent/tools/shell.js +48 -0
- package/dist/agent/tools/stat_path.d.ts +2 -0
- package/dist/agent/tools/stat_path.js +31 -0
- package/dist/agent/tools/types.d.ts +65 -0
- package/dist/agent/tools/types.js +1 -0
- package/dist/agent/tools/validate.d.ts +19 -0
- package/dist/agent/tools/validate.js +92 -0
- package/dist/agent/tools/write_file.d.ts +2 -0
- package/dist/agent/tools/write_file.js +28 -0
- package/dist/agent/tools.d.ts +3 -1
- package/dist/agent/tools.js +2 -377
- package/dist/app.d.ts +2 -1
- package/dist/app.js +846 -173
- package/dist/cli-main.js +363 -0
- package/dist/cli.js +444 -333
- package/dist/commands/about.js +1 -1
- package/dist/commands/attach.d.ts +9 -0
- package/dist/commands/attach.js +15 -0
- package/dist/commands/bg.d.ts +7 -0
- package/dist/commands/bg.js +9 -0
- package/dist/commands/cancel.d.ts +10 -0
- package/dist/commands/cancel.js +10 -0
- package/dist/commands/clear.d.ts +1 -0
- package/dist/commands/clear.js +1 -1
- package/dist/commands/compact.d.ts +22 -0
- package/dist/commands/compact.js +34 -0
- package/dist/commands/context.d.ts +7 -0
- package/dist/commands/context.js +25 -0
- package/dist/commands/edit.d.ts +10 -0
- package/dist/commands/edit.js +36 -0
- package/dist/commands/effort.d.ts +25 -0
- package/dist/commands/effort.js +48 -0
- package/dist/commands/expand.d.ts +10 -0
- package/dist/commands/expand.js +8 -0
- package/dist/commands/index.js +24 -0
- package/dist/commands/jobs.d.ts +7 -0
- package/dist/commands/jobs.js +17 -0
- package/dist/commands/kill.d.ts +7 -0
- package/dist/commands/kill.js +13 -0
- package/dist/commands/login.d.ts +6 -4
- package/dist/commands/login.js +24 -22
- package/dist/commands/logout.js +8 -16
- package/dist/commands/model.d.ts +1 -0
- package/dist/commands/model.js +10 -22
- package/dist/commands/provider.d.ts +1 -2
- package/dist/commands/provider.js +4 -11
- package/dist/commands/skills.js +34 -4
- package/dist/commands/steer.d.ts +11 -0
- package/dist/commands/steer.js +13 -0
- package/dist/commands/threads.d.ts +9 -0
- package/dist/commands/threads.js +26 -0
- package/dist/commands/whoami.js +6 -5
- package/dist/config/store.d.ts +53 -12
- package/dist/config/store.js +163 -73
- package/dist/config/trust.d.ts +30 -8
- package/dist/config/trust.js +139 -14
- package/dist/headless.d.ts +7 -0
- package/dist/headless.js +213 -0
- package/dist/index.js +19 -3
- package/dist/lib/debug.d.ts +2 -0
- package/dist/lib/debug.js +22 -0
- package/dist/lib/diagnostics.d.ts +48 -4
- package/dist/lib/diagnostics.js +90 -8
- package/dist/lib/highlight-subset.d.ts +21 -0
- package/dist/lib/highlight-subset.js +88 -0
- package/dist/lib/http.d.ts +11 -1
- package/dist/lib/http.js +62 -4
- package/dist/lib/lifecycle.d.ts +5 -0
- package/dist/lib/lifecycle.js +62 -0
- package/dist/lib/markdown.js +22 -4
- package/dist/lib/model-meta.d.ts +19 -0
- package/dist/lib/model-meta.js +113 -0
- package/dist/lib/native-tools.d.ts +12 -0
- package/dist/lib/native-tools.js +21 -0
- package/dist/lib/net.d.ts +33 -0
- package/dist/lib/net.js +168 -0
- package/dist/patch/apply.d.ts +15 -0
- package/dist/patch/apply.js +172 -0
- package/dist/patch/parse.d.ts +29 -0
- package/dist/patch/parse.js +112 -0
- package/dist/patch/plan.d.ts +49 -0
- package/dist/patch/plan.js +273 -0
- package/dist/providers/anthropic.d.ts +1 -0
- package/dist/providers/anthropic.js +12 -0
- package/dist/providers/custom.d.ts +9 -0
- package/dist/providers/deepseek.d.ts +10 -0
- package/dist/providers/deepseek.js +3 -2
- package/dist/providers/google.d.ts +9 -0
- package/dist/providers/groq.d.ts +10 -0
- package/dist/providers/groq.js +2 -1
- package/dist/providers/index.d.ts +10 -201
- package/dist/providers/index.js +30 -0
- package/dist/providers/mistral.d.ts +10 -0
- package/dist/providers/mistral.js +2 -1
- package/dist/providers/moonshot.d.ts +10 -0
- package/dist/providers/moonshot.js +2 -1
- package/dist/providers/ollama.d.ts +9 -0
- package/dist/providers/openai.d.ts +10 -0
- package/dist/providers/openai.js +2 -1
- package/dist/providers/openaiCompatible.d.ts +33 -2
- package/dist/providers/openaiCompatible.js +78 -16
- package/dist/providers/openrouter.d.ts +10 -0
- package/dist/providers/openrouter.js +2 -1
- package/dist/providers/qwen.d.ts +10 -0
- package/dist/providers/qwen.js +2 -1
- package/dist/providers/spec.d.ts +12 -0
- package/dist/providers/spec.js +29 -0
- package/dist/providers/xai.d.ts +10 -0
- package/dist/providers/xai.js +2 -1
- package/dist/providers/zhipu.d.ts +10 -0
- package/dist/providers/zhipu.js +2 -1
- package/dist/session/log.d.ts +49 -14
- package/dist/session/log.js +236 -11
- package/dist/session/projection.d.ts +76 -0
- package/dist/session/projection.js +526 -0
- package/dist/skills/manager.d.ts +15 -0
- package/dist/skills/manager.js +135 -18
- package/dist/types.d.ts +75 -1
- package/dist/ui/Message.d.ts +1 -7
- package/dist/ui/Message.js +13 -2
- package/dist/ui/Picker.d.ts +8 -0
- package/dist/ui/Picker.js +24 -0
- package/dist/ui/Prompt.d.ts +1 -2
- package/dist/ui/Prompt.js +2 -2
- package/dist/ui/Spinner.js +11 -5
- package/dist/ui/StatusBar.d.ts +2 -1
- package/dist/ui/StatusBar.js +6 -5
- package/package.json +3 -2
- package/src/agent/approval-json.ts +103 -0
- package/src/agent/approval-native.ts +168 -0
- package/src/agent/approval-store.ts +38 -0
- package/src/agent/approval.ts +94 -0
- package/src/agent/compaction.ts +321 -0
- package/src/agent/context.ts +97 -0
- package/src/agent/guards.ts +1239 -19
- package/src/agent/ignore.ts +147 -0
- package/src/agent/policy/defaults.ts +34 -0
- package/src/agent/policy/egress.ts +335 -0
- package/src/agent/process-tree.ts +114 -0
- package/src/agent/protocol-mode.ts +59 -0
- package/src/agent/protocol.ts +191 -40
- package/src/agent/registry.ts +270 -0
- package/src/agent/runner.ts +486 -0
- package/src/agent/shell-ast.ts +227 -0
- package/src/agent/shell-session.ts +673 -0
- package/src/agent/stream.ts +143 -0
- package/src/agent/tool-schemas.ts +86 -0
- package/src/agent/tools/append_file.ts +30 -0
- package/src/agent/tools/apply_patch.ts +95 -0
- package/src/agent/tools/copy_file.ts +56 -0
- package/src/agent/tools/delete_file.ts +45 -0
- package/src/agent/tools/grep.ts +57 -0
- package/src/agent/tools/index.ts +34 -0
- package/src/agent/tools/list_files.ts +19 -0
- package/src/agent/tools/make_dir.ts +28 -0
- package/src/agent/tools/move_path.ts +83 -0
- package/src/agent/tools/paths.ts +92 -0
- package/src/agent/tools/pwd.ts +12 -0
- package/src/agent/tools/read_file.ts +84 -0
- package/src/agent/tools/read_many.ts +45 -0
- package/src/agent/tools/readers.ts +491 -0
- package/src/agent/tools/replace_in_file.ts +66 -0
- package/src/agent/tools/shell.ts +53 -0
- package/src/agent/tools/stat_path.ts +33 -0
- package/src/agent/tools/types.ts +84 -0
- package/src/agent/tools/validate.ts +104 -0
- package/src/agent/tools/write_file.ts +30 -0
- package/src/agent/tools.ts +3 -377
- package/src/app.ts +853 -170
- package/src/{cli.ts → cli-main.ts} +64 -9
- package/src/commands/about.ts +1 -1
- package/src/commands/attach.ts +16 -0
- package/src/commands/bg.ts +9 -0
- package/src/commands/cancel.ts +10 -0
- package/src/commands/clear.ts +1 -1
- package/src/commands/compact.ts +35 -0
- package/src/commands/context.ts +26 -0
- package/src/commands/edit.ts +37 -0
- package/src/commands/effort.ts +49 -0
- package/src/commands/expand.ts +8 -0
- package/src/commands/index.ts +24 -0
- package/src/commands/jobs.ts +18 -0
- package/src/commands/kill.ts +14 -0
- package/src/commands/login.ts +24 -22
- package/src/commands/logout.ts +8 -16
- package/src/commands/model.ts +10 -22
- package/src/commands/provider.ts +4 -11
- package/src/commands/skills.ts +34 -4
- package/src/commands/steer.ts +13 -0
- package/src/commands/threads.ts +28 -0
- package/src/commands/whoami.ts +6 -5
- package/src/config/store.ts +168 -77
- package/src/config/trust.ts +181 -22
- package/src/headless.ts +225 -0
- package/src/index.ts +19 -3
- package/src/lib/debug.ts +24 -0
- package/src/lib/diagnostics.ts +90 -8
- package/src/lib/highlight-subset.ts +93 -0
- package/src/lib/http.ts +66 -4
- package/src/lib/lifecycle.ts +69 -0
- package/src/lib/markdown.ts +22 -4
- package/src/lib/model-meta.ts +138 -0
- package/src/lib/native-tools.ts +32 -0
- package/src/lib/net.ts +204 -0
- package/src/patch/apply.ts +194 -0
- package/src/patch/parse.ts +131 -0
- package/src/patch/plan.ts +317 -0
- package/src/providers/anthropic.ts +13 -1
- package/src/providers/deepseek.ts +3 -2
- package/src/providers/groq.ts +2 -1
- package/src/providers/index.ts +32 -1
- package/src/providers/mistral.ts +2 -1
- package/src/providers/moonshot.ts +2 -1
- package/src/providers/openai.ts +2 -1
- package/src/providers/openaiCompatible.ts +104 -19
- package/src/providers/openrouter.ts +2 -1
- package/src/providers/qwen.ts +2 -1
- package/src/providers/spec.ts +42 -0
- package/src/providers/xai.ts +2 -1
- package/src/providers/zhipu.ts +2 -1
- package/src/session/log.ts +311 -29
- package/src/session/projection.ts +621 -0
- package/src/skills/manager.ts +140 -18
- package/src/types/highlight-shims.d.ts +14 -0
- package/src/types.ts +79 -2
- package/src/ui/Message.ts +13 -2
- package/src/ui/Picker.ts +24 -0
- package/src/ui/Prompt.ts +2 -2
- package/src/ui/Spinner.ts +11 -5
- package/src/ui/StatusBar.ts +6 -5
- package/tsconfig.json +1 -0
- /package/dist/{cli.d.ts → cli-main.d.ts} +0 -0
|
@@ -0,0 +1,673 @@
|
|
|
1
|
+
import { spawn, type ChildProcess, type ChildProcessWithoutNullStreams } from "node:child_process";
|
|
2
|
+
import parse from "bash-parser";
|
|
3
|
+
import { terminateProcessTree } from "./process-tree.js";
|
|
4
|
+
import { normalizeTimeoutMs } from "./tools/validate.js";
|
|
5
|
+
|
|
6
|
+
export type ShellJobStatus = "running" | "done" | "failed" | "killed" | "timeout";
|
|
7
|
+
|
|
8
|
+
export interface ShellExecResult {
|
|
9
|
+
ok: boolean;
|
|
10
|
+
status: ShellJobStatus;
|
|
11
|
+
output: string;
|
|
12
|
+
exitCode?: number | null;
|
|
13
|
+
sessionId?: string;
|
|
14
|
+
nextPollMs?: number;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface ShellJobView {
|
|
18
|
+
id: string;
|
|
19
|
+
command: string;
|
|
20
|
+
cwd: string;
|
|
21
|
+
status: ShellJobStatus;
|
|
22
|
+
exitCode?: number | null;
|
|
23
|
+
bytes: number;
|
|
24
|
+
startedAt: number;
|
|
25
|
+
pid?: number;
|
|
26
|
+
/** Lifecycle visibility for /jobs and regression tests. */
|
|
27
|
+
timeoutArmed?: boolean;
|
|
28
|
+
foregroundAttached?: boolean;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
interface ExecOptions {
|
|
32
|
+
signal?: AbortSignal;
|
|
33
|
+
turn?: number;
|
|
34
|
+
timeoutMs?: number;
|
|
35
|
+
backgroundAfterMs?: number;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const OUTPUT_LIMIT = 1024 * 1024;
|
|
39
|
+
const DEFAULT_BACKGROUND_MS = 2000;
|
|
40
|
+
const DEFAULT_TIMEOUT_MS = 120_000;
|
|
41
|
+
export const DEFAULT_SHELL_POOL_SIZE = 2;
|
|
42
|
+
export const MAX_SHELL_POOL_SIZE = 6;
|
|
43
|
+
let poolLimit = DEFAULT_SHELL_POOL_SIZE;
|
|
44
|
+
const jobs = new Map<string, ShellJob>();
|
|
45
|
+
const pool: ResidentShell[] = [];
|
|
46
|
+
let spawns = 0;
|
|
47
|
+
let spawnTurn = Number.NaN;
|
|
48
|
+
let spawnsThisTurn = 0;
|
|
49
|
+
|
|
50
|
+
class ShellJob {
|
|
51
|
+
readonly id: string;
|
|
52
|
+
readonly key: string;
|
|
53
|
+
readonly command: string;
|
|
54
|
+
readonly cwd: string;
|
|
55
|
+
readonly startedAt = Date.now();
|
|
56
|
+
output = "";
|
|
57
|
+
delivered = 0;
|
|
58
|
+
status: ShellJobStatus = "running";
|
|
59
|
+
exitCode: number | null | undefined;
|
|
60
|
+
nextPollMs = 1000;
|
|
61
|
+
child?: ChildProcess;
|
|
62
|
+
resident?: ResidentShell;
|
|
63
|
+
private readonly donePromise: Promise<void>;
|
|
64
|
+
private resolveDone!: () => void;
|
|
65
|
+
private waiters: Array<() => void> = [];
|
|
66
|
+
private timeoutTimer?: ReturnType<typeof setTimeout>;
|
|
67
|
+
private abortSignal?: AbortSignal;
|
|
68
|
+
private abortListener?: () => void;
|
|
69
|
+
private cleaned = false;
|
|
70
|
+
constructor(command: string, cwd: string) {
|
|
71
|
+
this.id = "sh_" + Date.now().toString(36) + "_" + Math.random().toString(36).slice(2, 7);
|
|
72
|
+
this.key = this.id.replace(/[^a-z0-9]/gi, "");
|
|
73
|
+
this.command = command;
|
|
74
|
+
this.cwd = cwd;
|
|
75
|
+
this.donePromise = new Promise((resolve) => {
|
|
76
|
+
this.resolveDone = resolve;
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
done(): Promise<void> {
|
|
80
|
+
return this.donePromise;
|
|
81
|
+
}
|
|
82
|
+
append(text: string): void {
|
|
83
|
+
if (!text)
|
|
84
|
+
return;
|
|
85
|
+
this.output += text;
|
|
86
|
+
if (this.output.length > OUTPUT_LIMIT) {
|
|
87
|
+
const over = this.output.length - OUTPUT_LIMIT;
|
|
88
|
+
this.output = this.output.slice(over);
|
|
89
|
+
this.delivered = Math.max(0, this.delivered - over);
|
|
90
|
+
}
|
|
91
|
+
this.notify();
|
|
92
|
+
}
|
|
93
|
+
takeOutput(): string {
|
|
94
|
+
const next = this.output.slice(this.delivered);
|
|
95
|
+
this.delivered = this.output.length;
|
|
96
|
+
return next;
|
|
97
|
+
}
|
|
98
|
+
finish(status: ShellJobStatus, exitCode?: number | null): void {
|
|
99
|
+
if (this.status !== "running")
|
|
100
|
+
return;
|
|
101
|
+
this.status = status;
|
|
102
|
+
this.exitCode = exitCode;
|
|
103
|
+
this.cleanupLifecycle();
|
|
104
|
+
this.resolveDone();
|
|
105
|
+
this.notify();
|
|
106
|
+
}
|
|
107
|
+
/**
|
|
108
|
+
* Task-level timeout lives until the job actually finishes. Returning to
|
|
109
|
+
* the foreground only detaches the AbortSignal; it must not disarm this.
|
|
110
|
+
*/
|
|
111
|
+
armTimeout(ms: number): void {
|
|
112
|
+
if (this.timeoutTimer || this.status !== "running")
|
|
113
|
+
return;
|
|
114
|
+
this.timeoutTimer = setTimeout(() => this.kill("timeout"), ms);
|
|
115
|
+
this.timeoutTimer.unref?.();
|
|
116
|
+
}
|
|
117
|
+
attachAbort(signal: AbortSignal | undefined): void {
|
|
118
|
+
if (!signal || this.abortListener)
|
|
119
|
+
return;
|
|
120
|
+
const listener = () => this.kill("killed");
|
|
121
|
+
this.abortSignal = signal;
|
|
122
|
+
this.abortListener = listener;
|
|
123
|
+
signal.addEventListener("abort", listener, { once: true });
|
|
124
|
+
}
|
|
125
|
+
/** Detach Esc/foreground cancellation but keep the timeout armed. */
|
|
126
|
+
detachForeground(): void {
|
|
127
|
+
if (this.abortSignal && this.abortListener)
|
|
128
|
+
this.abortSignal.removeEventListener("abort", this.abortListener);
|
|
129
|
+
this.abortSignal = undefined;
|
|
130
|
+
this.abortListener = undefined;
|
|
131
|
+
}
|
|
132
|
+
private cleanupLifecycle(): void {
|
|
133
|
+
if (this.cleaned)
|
|
134
|
+
return;
|
|
135
|
+
this.cleaned = true;
|
|
136
|
+
if (this.timeoutTimer) {
|
|
137
|
+
clearTimeout(this.timeoutTimer);
|
|
138
|
+
this.timeoutTimer = undefined;
|
|
139
|
+
}
|
|
140
|
+
this.detachForeground();
|
|
141
|
+
}
|
|
142
|
+
waitForChange(ms: number): Promise<void> {
|
|
143
|
+
if (this.status !== "running" || this.delivered < this.output.length)
|
|
144
|
+
return Promise.resolve();
|
|
145
|
+
if (ms <= 0)
|
|
146
|
+
return Promise.resolve();
|
|
147
|
+
return new Promise((resolve) => {
|
|
148
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
149
|
+
const wake = () => {
|
|
150
|
+
if (timer)
|
|
151
|
+
clearTimeout(timer);
|
|
152
|
+
this.waiters = this.waiters.filter((entry) => entry !== wake);
|
|
153
|
+
resolve();
|
|
154
|
+
};
|
|
155
|
+
this.waiters.push(wake);
|
|
156
|
+
timer = setTimeout(wake, ms);
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
notify(): void {
|
|
160
|
+
const waiters = this.waiters;
|
|
161
|
+
this.waiters = [];
|
|
162
|
+
for (const waiter of waiters)
|
|
163
|
+
waiter();
|
|
164
|
+
}
|
|
165
|
+
view(): ShellJobView {
|
|
166
|
+
return {
|
|
167
|
+
id: this.id,
|
|
168
|
+
command: this.command,
|
|
169
|
+
cwd: this.cwd,
|
|
170
|
+
status: this.status,
|
|
171
|
+
exitCode: this.exitCode,
|
|
172
|
+
bytes: this.output.length,
|
|
173
|
+
startedAt: this.startedAt,
|
|
174
|
+
pid: this.child?.pid ?? this.resident?.child.pid,
|
|
175
|
+
timeoutArmed: Boolean(this.timeoutTimer),
|
|
176
|
+
foregroundAttached: Boolean(this.abortListener),
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
kill(reason: "killed" | "timeout"): void {
|
|
180
|
+
if (this.status !== "running")
|
|
181
|
+
return;
|
|
182
|
+
this.append(reason === "timeout"
|
|
183
|
+
? "\n[command timed out: process group killed; setsid descendants may escape]"
|
|
184
|
+
: "\n[command aborted: process group killed; setsid descendants may escape]");
|
|
185
|
+
this.status = reason;
|
|
186
|
+
this.cleanupLifecycle();
|
|
187
|
+
if (this.resident)
|
|
188
|
+
this.resident.kill();
|
|
189
|
+
else if (this.child)
|
|
190
|
+
terminateProcessTree(this.child);
|
|
191
|
+
this.resolveDone();
|
|
192
|
+
this.notify();
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
class ResidentShell {
|
|
197
|
+
readonly child: ChildProcessWithoutNullStreams;
|
|
198
|
+
job?: ShellJob;
|
|
199
|
+
dead = false;
|
|
200
|
+
private buffer = "";
|
|
201
|
+
private background = false;
|
|
202
|
+
private claimed = false;
|
|
203
|
+
constructor(cwd: string) {
|
|
204
|
+
const env = { ...process.env, FASTCODE_SANDBOX: "1", FASTCODE_SANDBOX_ROOT: cwd };
|
|
205
|
+
this.child = spawn("sh", ["-s"], {
|
|
206
|
+
cwd,
|
|
207
|
+
env,
|
|
208
|
+
detached: process.platform !== "win32",
|
|
209
|
+
windowsHide: true,
|
|
210
|
+
});
|
|
211
|
+
this.child.stdout.setEncoding("utf8");
|
|
212
|
+
this.child.stderr.setEncoding("utf8");
|
|
213
|
+
this.child.stdout.on("data", (chunk: string) => this.consume(chunk));
|
|
214
|
+
this.child.stderr.on("data", (chunk: string) => this.consume(chunk));
|
|
215
|
+
this.child.on("error", (err) => {
|
|
216
|
+
this.fail(err.message);
|
|
217
|
+
});
|
|
218
|
+
this.child.on("close", (code) => {
|
|
219
|
+
this.dead = true;
|
|
220
|
+
this.fail(`shell exited (${code ?? "unknown"})`, code);
|
|
221
|
+
const index = pool.indexOf(this);
|
|
222
|
+
if (index >= 0)
|
|
223
|
+
pool.splice(index, 1);
|
|
224
|
+
});
|
|
225
|
+
}
|
|
226
|
+
claimIfIdle(): boolean {
|
|
227
|
+
if (this.dead || this.job || this.claimed)
|
|
228
|
+
return false;
|
|
229
|
+
this.claimed = true;
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
run(job: ShellJob, timeoutMs: number, background = false): void {
|
|
233
|
+
this.claimed = false;
|
|
234
|
+
this.job = job;
|
|
235
|
+
this.background = background;
|
|
236
|
+
this.buffer = "";
|
|
237
|
+
job.resident = this;
|
|
238
|
+
const marker = `__FC_END_${job.key}__`;
|
|
239
|
+
const body = job.command.trim() ? ["(", job.command, ")"] : [":"];
|
|
240
|
+
const script = [
|
|
241
|
+
`if cd ${shellQuote(job.cwd)}; then`,
|
|
242
|
+
...body,
|
|
243
|
+
"__fc_status=$?",
|
|
244
|
+
"else",
|
|
245
|
+
"__fc_status=1",
|
|
246
|
+
"fi",
|
|
247
|
+
`printf '\\n${marker}:%s\\n' "$__fc_status"`,
|
|
248
|
+
"",
|
|
249
|
+
].join("\n");
|
|
250
|
+
try {
|
|
251
|
+
this.child.stdin.write(script);
|
|
252
|
+
}
|
|
253
|
+
catch (err: any) {
|
|
254
|
+
job.append(err?.message ?? String(err));
|
|
255
|
+
job.finish("failed", 1);
|
|
256
|
+
}
|
|
257
|
+
void timeoutMs;
|
|
258
|
+
}
|
|
259
|
+
private consume(chunk: string): void {
|
|
260
|
+
if (this.dead)
|
|
261
|
+
return;
|
|
262
|
+
// Output that arrives while no job owns the shell belongs to an
|
|
263
|
+
// already-finished background child. Never carry it into the next job.
|
|
264
|
+
if (!this.job) {
|
|
265
|
+
this.buffer = "";
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
this.buffer += chunk;
|
|
269
|
+
const marker = `__FC_END_${this.job.key}__:`;
|
|
270
|
+
const index = this.buffer.indexOf(marker);
|
|
271
|
+
if (index < 0) {
|
|
272
|
+
const sentinel = "\n" + marker;
|
|
273
|
+
let keep = 0;
|
|
274
|
+
const max = Math.min(this.buffer.length, sentinel.length);
|
|
275
|
+
for (let length = max; length > 0; length -= 1) {
|
|
276
|
+
if (sentinel.startsWith(this.buffer.slice(this.buffer.length - length))) {
|
|
277
|
+
keep = length;
|
|
278
|
+
break;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
const flush = this.buffer.length - keep;
|
|
282
|
+
if (flush > 0) {
|
|
283
|
+
this.job.append(this.buffer.slice(0, flush));
|
|
284
|
+
this.buffer = this.buffer.slice(flush);
|
|
285
|
+
}
|
|
286
|
+
return;
|
|
287
|
+
}
|
|
288
|
+
const rest = this.buffer.slice(index + marker.length);
|
|
289
|
+
const newline = rest.indexOf("\n");
|
|
290
|
+
if (newline < 0)
|
|
291
|
+
return;
|
|
292
|
+
const job = this.job;
|
|
293
|
+
job.append(this.buffer.slice(0, index));
|
|
294
|
+
const code = Number(rest.slice(0, newline).trim());
|
|
295
|
+
this.buffer = rest.slice(newline + 1);
|
|
296
|
+
this.job = undefined;
|
|
297
|
+
const background = this.background;
|
|
298
|
+
this.background = false;
|
|
299
|
+
job.finish(Number.isFinite(code) && code === 0 ? "done" : "failed", Number.isFinite(code) ? code : 1);
|
|
300
|
+
// A background operator leaves a detached child writing into this
|
|
301
|
+
// shell's stdout pipe. Retire the resident so late output cannot be
|
|
302
|
+
// attributed to the next command.
|
|
303
|
+
if (background)
|
|
304
|
+
this.kill();
|
|
305
|
+
}
|
|
306
|
+
private fail(message: string, code?: number | null): void {
|
|
307
|
+
this.claimed = false;
|
|
308
|
+
if (!this.job)
|
|
309
|
+
return;
|
|
310
|
+
const job = this.job;
|
|
311
|
+
this.job = undefined;
|
|
312
|
+
this.background = false;
|
|
313
|
+
if (this.buffer) {
|
|
314
|
+
job.append(this.buffer);
|
|
315
|
+
this.buffer = "";
|
|
316
|
+
}
|
|
317
|
+
if (code === 0) {
|
|
318
|
+
job.finish("done", 0);
|
|
319
|
+
return;
|
|
320
|
+
}
|
|
321
|
+
job.append(`\n${message}`);
|
|
322
|
+
job.finish("failed", code ?? 1);
|
|
323
|
+
}
|
|
324
|
+
kill(): void {
|
|
325
|
+
this.dead = true;
|
|
326
|
+
this.claimed = false;
|
|
327
|
+
this.background = false;
|
|
328
|
+
this.buffer = "";
|
|
329
|
+
terminateProcessTree(this.child);
|
|
330
|
+
const index = pool.indexOf(this);
|
|
331
|
+
if (index >= 0)
|
|
332
|
+
pool.splice(index, 1);
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
export async function executeShellCommand(command: string, cwd: string, options: ExecOptions = {}): Promise<ShellExecResult> {
|
|
337
|
+
pruneJobs();
|
|
338
|
+
if (options.signal?.aborted)
|
|
339
|
+
return { ok: false, status: "killed", output: "command aborted before start" };
|
|
340
|
+
const timeoutMs = normalizeTimeoutMs(options.timeoutMs, DEFAULT_TIMEOUT_MS);
|
|
341
|
+
const backgroundAfterMs = options.backgroundAfterMs ?? DEFAULT_BACKGROUND_MS;
|
|
342
|
+
const shape = analyzeShellShape(command);
|
|
343
|
+
const useResident = process.platform !== "win32" && !shape.oneShot;
|
|
344
|
+
let job: ShellJob | undefined;
|
|
345
|
+
if (useResident) {
|
|
346
|
+
const resident = await acquireResident(options.turn, cwd, timeoutMs, options.signal);
|
|
347
|
+
if (resident === "limit")
|
|
348
|
+
return { ok: false, status: "failed", output: spawnLimitMessage() };
|
|
349
|
+
if (resident === "timeout")
|
|
350
|
+
return { ok: false, status: "timeout", output: "shell pool is busy; timed out waiting for a free resident shell" };
|
|
351
|
+
if (resident === "killed")
|
|
352
|
+
return { ok: false, status: "killed", output: "command aborted before start" };
|
|
353
|
+
job = new ShellJob(command, cwd);
|
|
354
|
+
resident.run(job, timeoutMs, shape.background);
|
|
355
|
+
}
|
|
356
|
+
else {
|
|
357
|
+
if (!maySpawn(options.turn))
|
|
358
|
+
return { ok: false, status: "failed", output: spawnLimitMessage() };
|
|
359
|
+
job = startOneShot(command, cwd);
|
|
360
|
+
}
|
|
361
|
+
jobs.set(job.id, job);
|
|
362
|
+
job.armTimeout(timeoutMs);
|
|
363
|
+
job.attachAbort(options.signal);
|
|
364
|
+
if (options.signal?.aborted)
|
|
365
|
+
job.kill("killed");
|
|
366
|
+
await Promise.race([job.done(), delay(backgroundAfterMs)]);
|
|
367
|
+
if (job.status === "running") {
|
|
368
|
+
// Task keeps running: only the foreground Esc/abort association is
|
|
369
|
+
// released. The timeout stays armed until the job really finishes.
|
|
370
|
+
job.detachForeground();
|
|
371
|
+
return {
|
|
372
|
+
ok: true,
|
|
373
|
+
status: "running",
|
|
374
|
+
output: job.takeOutput(),
|
|
375
|
+
sessionId: job.id,
|
|
376
|
+
nextPollMs: job.nextPollMs,
|
|
377
|
+
};
|
|
378
|
+
}
|
|
379
|
+
return syncResult(job);
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
export async function pollShellSession(id: string, waitMs = 0): Promise<ShellExecResult | undefined> {
|
|
383
|
+
pruneJobs();
|
|
384
|
+
const job = jobs.get(id);
|
|
385
|
+
if (!job)
|
|
386
|
+
return undefined;
|
|
387
|
+
return await pollOnce(job, waitMs);
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
async function pollOnce(job: ShellJob, waitMs: number): Promise<ShellExecResult> {
|
|
391
|
+
await job.waitForChange(waitMs);
|
|
392
|
+
const output = job.takeOutput();
|
|
393
|
+
if (output)
|
|
394
|
+
job.nextPollMs = 1000;
|
|
395
|
+
else if (job.status === "running")
|
|
396
|
+
job.nextPollMs = Math.min(30_000, job.nextPollMs * 2);
|
|
397
|
+
return {
|
|
398
|
+
ok: job.status === "done" || job.status === "running",
|
|
399
|
+
status: job.status,
|
|
400
|
+
output,
|
|
401
|
+
exitCode: job.exitCode,
|
|
402
|
+
sessionId: job.id,
|
|
403
|
+
nextPollMs: job.nextPollMs,
|
|
404
|
+
};
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
export function killShellSession(id: string): boolean {
|
|
408
|
+
const job = jobs.get(id);
|
|
409
|
+
if (!job || job.status !== "running")
|
|
410
|
+
return false;
|
|
411
|
+
job.kill("killed");
|
|
412
|
+
return true;
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
export function listShellSessions(): ShellJobView[] {
|
|
416
|
+
pruneJobs();
|
|
417
|
+
return Array.from(jobs.values())
|
|
418
|
+
.sort((a, b) => b.startedAt - a.startedAt)
|
|
419
|
+
.map((job) => job.view());
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
export function shutdownShells(): void {
|
|
423
|
+
for (const job of jobs.values()) {
|
|
424
|
+
if (job.status === "running")
|
|
425
|
+
job.kill("killed");
|
|
426
|
+
}
|
|
427
|
+
for (const resident of [...pool])
|
|
428
|
+
resident.kill();
|
|
429
|
+
pool.length = 0;
|
|
430
|
+
jobs.clear();
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
/**
|
|
434
|
+
* Exit-safe stop: send SIGTERM, wait briefly for close, then force SIGKILL.
|
|
435
|
+
* Use this when the process is about to exit and must not leave children.
|
|
436
|
+
*/
|
|
437
|
+
export async function shutdownShellsAsync(graceMs = 400): Promise<void> {
|
|
438
|
+
const running = Array.from(jobs.values()).filter((job) => job.status === "running");
|
|
439
|
+
const residents = pool.filter((resident) => !resident.dead);
|
|
440
|
+
for (const job of running)
|
|
441
|
+
job.kill("killed");
|
|
442
|
+
for (const resident of residents)
|
|
443
|
+
resident.kill();
|
|
444
|
+
pool.length = 0;
|
|
445
|
+
jobs.clear();
|
|
446
|
+
const children: ChildProcess[] = [];
|
|
447
|
+
for (const job of running) {
|
|
448
|
+
if (job.child)
|
|
449
|
+
children.push(job.child);
|
|
450
|
+
}
|
|
451
|
+
for (const resident of residents)
|
|
452
|
+
children.push(resident.child);
|
|
453
|
+
if (children.length === 0)
|
|
454
|
+
return;
|
|
455
|
+
await Promise.race([
|
|
456
|
+
Promise.all(children.map((child) => waitForChildClose(child, graceMs))),
|
|
457
|
+
delay(graceMs + 50),
|
|
458
|
+
]);
|
|
459
|
+
for (const child of children) {
|
|
460
|
+
if (child.exitCode === null && child.signalCode === null)
|
|
461
|
+
terminateProcessTree(child, { graceMs: 0 });
|
|
462
|
+
}
|
|
463
|
+
await delay(50);
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
function waitForChildClose(child: ChildProcess, ms: number): Promise<void> {
|
|
467
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
468
|
+
return Promise.resolve();
|
|
469
|
+
return new Promise((resolve) => {
|
|
470
|
+
let timer: ReturnType<typeof setTimeout> | undefined;
|
|
471
|
+
const done = () => {
|
|
472
|
+
if (timer)
|
|
473
|
+
clearTimeout(timer);
|
|
474
|
+
child.removeListener("close", done);
|
|
475
|
+
resolve();
|
|
476
|
+
};
|
|
477
|
+
timer = setTimeout(done, ms);
|
|
478
|
+
child.once("close", done);
|
|
479
|
+
});
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
export function shellPoolSize(): number {
|
|
483
|
+
return poolLimit;
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
/** Clamp and apply the resident shell concurrency (1..6, default 2). */
|
|
487
|
+
export function setShellPoolSize(size: number): number {
|
|
488
|
+
const parsed = Number(size);
|
|
489
|
+
poolLimit = Number.isFinite(parsed)
|
|
490
|
+
? Math.min(MAX_SHELL_POOL_SIZE, Math.max(1, Math.floor(parsed)))
|
|
491
|
+
: DEFAULT_SHELL_POOL_SIZE;
|
|
492
|
+
return poolLimit;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
export function shellPoolStats(): { spawns: number; active: number; pool: number; maxPool: number; maxSpawnPerTurn: number } {
|
|
496
|
+
return {
|
|
497
|
+
spawns,
|
|
498
|
+
active: Array.from(jobs.values()).filter((job) => job.status === "running").length,
|
|
499
|
+
pool: pool.filter((resident) => !resident.dead).length,
|
|
500
|
+
maxPool: poolLimit,
|
|
501
|
+
maxSpawnPerTurn: maxSpawnPerTurn(),
|
|
502
|
+
};
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
function startOneShot(command: string, cwd: string): ShellJob {
|
|
506
|
+
const job = new ShellJob(command, cwd);
|
|
507
|
+
const child = spawn(command, {
|
|
508
|
+
cwd,
|
|
509
|
+
shell: true,
|
|
510
|
+
env: { ...process.env, FASTCODE_SANDBOX: "1", FASTCODE_SANDBOX_ROOT: cwd },
|
|
511
|
+
windowsHide: true,
|
|
512
|
+
detached: process.platform !== "win32",
|
|
513
|
+
});
|
|
514
|
+
job.child = child;
|
|
515
|
+
child.stdout?.on("data", (chunk) => job.append(chunk.toString("utf8")));
|
|
516
|
+
child.stderr?.on("data", (chunk) => job.append(chunk.toString("utf8")));
|
|
517
|
+
child.on("error", (err) => {
|
|
518
|
+
job.append("\n" + err.message);
|
|
519
|
+
job.finish("failed", 1);
|
|
520
|
+
});
|
|
521
|
+
child.on("close", (code) => {
|
|
522
|
+
if (job.status === "running")
|
|
523
|
+
job.finish(code === 0 ? "done" : "failed", code);
|
|
524
|
+
});
|
|
525
|
+
return job;
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
async function acquireResident(turn: number | undefined, cwd: string, waitMs: number, signal?: AbortSignal): Promise<ResidentShell | "limit" | "timeout" | "killed"> {
|
|
529
|
+
const deadline = Date.now() + waitMs;
|
|
530
|
+
while (true) {
|
|
531
|
+
for (const resident of pool) {
|
|
532
|
+
if (resident.claimIfIdle())
|
|
533
|
+
return resident;
|
|
534
|
+
}
|
|
535
|
+
const live = pool.filter((resident) => !resident.dead);
|
|
536
|
+
if (live.length < poolLimit && maySpawn(turn)) {
|
|
537
|
+
const resident = new ResidentShell(cwd);
|
|
538
|
+
resident.claimIfIdle();
|
|
539
|
+
pool.push(resident);
|
|
540
|
+
return resident;
|
|
541
|
+
}
|
|
542
|
+
// No free shell and no spawn budget: wait for a running command to
|
|
543
|
+
// finish instead of failing the third call with "both shells busy".
|
|
544
|
+
if (live.length === 0)
|
|
545
|
+
return "limit";
|
|
546
|
+
if (signal?.aborted)
|
|
547
|
+
return "killed";
|
|
548
|
+
const remaining = deadline - Date.now();
|
|
549
|
+
if (remaining <= 0)
|
|
550
|
+
return "timeout";
|
|
551
|
+
await delay(Math.min(remaining, 50));
|
|
552
|
+
}
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
const INTERACTIVE_COMMANDS = new Set(["vim", "nano", "less", "more", "ssh", "su", "login", "read", "top", "htop", "tmux", "screen"]);
|
|
556
|
+
const REPL_COMMANDS = new Set(["python", "python3", "node", "bash", "zsh", "fish", "irb", "sqlite3"]);
|
|
557
|
+
const WRAPPER_COMMANDS = new Set(["sudo", "env", "nohup", "command", "time", "exec"]);
|
|
558
|
+
|
|
559
|
+
interface ShellShape {
|
|
560
|
+
oneShot: boolean;
|
|
561
|
+
background: boolean;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
function analyzeShellShape(command: string): ShellShape {
|
|
565
|
+
let ast: any;
|
|
566
|
+
try {
|
|
567
|
+
ast = parse(command, { mode: "bash" });
|
|
568
|
+
}
|
|
569
|
+
catch {
|
|
570
|
+
// Fall back to the historical heuristic only when the parser cannot
|
|
571
|
+
// classify the command at all.
|
|
572
|
+
return {
|
|
573
|
+
oneShot: /(^|\s)(vim|nano|less|more|ssh|su|login|read|top|htop|tmux|screen)(\s|$)/.test(command)
|
|
574
|
+
|| /(^|\s)(python|python3|node|bash|zsh|fish|irb|sqlite3)(\s|$)/.test(command) && !/\s-(c|e|m)\s/.test(command),
|
|
575
|
+
background: /(^|[^&>])&([^&>]|$)/.test(command),
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
let oneShot = false;
|
|
579
|
+
let background = false;
|
|
580
|
+
const walk = (node: any, depth = 0): void => {
|
|
581
|
+
if (!node || typeof node !== "object" || depth > 32)
|
|
582
|
+
return;
|
|
583
|
+
if (Array.isArray(node)) {
|
|
584
|
+
for (const entry of node)
|
|
585
|
+
walk(entry, depth + 1);
|
|
586
|
+
return;
|
|
587
|
+
}
|
|
588
|
+
// bash-parser also marks `cmd &> file` as async; only retire the
|
|
589
|
+
// resident for a real background operator, not a redirect.
|
|
590
|
+
if (node.async === true && /(^|[^&>])&([^&>]|$)/.test(command))
|
|
591
|
+
background = true;
|
|
592
|
+
if (node.type === "Command" && typeof node.name?.text === "string") {
|
|
593
|
+
const name = node.name.text;
|
|
594
|
+
const args = Array.isArray(node.suffix)
|
|
595
|
+
? node.suffix.map((word: any) => typeof word?.text === "string" ? word.text : "").filter(Boolean)
|
|
596
|
+
: [];
|
|
597
|
+
const replOptions = (values: string[]) => values.some((arg: string) => /^(-c|-e|-m|-p|--eval|--print|--command)$/.test(arg));
|
|
598
|
+
if (INTERACTIVE_COMMANDS.has(name) || REPL_COMMANDS.has(name) && !replOptions(args))
|
|
599
|
+
oneShot = true;
|
|
600
|
+
if (WRAPPER_COMMANDS.has(name)) {
|
|
601
|
+
const wrappedIndex = args.findIndex((arg: string) => INTERACTIVE_COMMANDS.has(arg) || REPL_COMMANDS.has(arg));
|
|
602
|
+
if (wrappedIndex >= 0) {
|
|
603
|
+
const wrappedName = args[wrappedIndex];
|
|
604
|
+
if (INTERACTIVE_COMMANDS.has(wrappedName) ||
|
|
605
|
+
REPL_COMMANDS.has(wrappedName) && !replOptions(args.slice(wrappedIndex + 1)))
|
|
606
|
+
oneShot = true;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
for (const value of Object.values(node)) {
|
|
611
|
+
if (value && typeof value === "object")
|
|
612
|
+
walk(value, depth + 1);
|
|
613
|
+
}
|
|
614
|
+
};
|
|
615
|
+
walk(ast);
|
|
616
|
+
return { oneShot, background };
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
function maySpawn(turn: number | undefined): boolean {
|
|
620
|
+
const current = typeof turn === "number" ? turn : 0;
|
|
621
|
+
if (current !== spawnTurn) {
|
|
622
|
+
spawnTurn = current;
|
|
623
|
+
spawnsThisTurn = 0;
|
|
624
|
+
}
|
|
625
|
+
if (spawnsThisTurn >= maxSpawnPerTurn())
|
|
626
|
+
return false;
|
|
627
|
+
spawnsThisTurn += 1;
|
|
628
|
+
spawns += 1;
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
function maxSpawnPerTurn(): number {
|
|
633
|
+
const raw = Number(process.env["FASTCODE_MAX_SPAWN_PER_TURN"]);
|
|
634
|
+
return Number.isFinite(raw) && raw > 0 ? Math.floor(raw) : 8;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
function spawnLimitMessage(): string {
|
|
638
|
+
return `spawn limit reached (FASTCODE_MAX_SPAWN_PER_TURN=${maxSpawnPerTurn()}); batch commands or poll an existing session`;
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
function syncResult(job: ShellJob): ShellExecResult {
|
|
642
|
+
return {
|
|
643
|
+
ok: job.status === "done",
|
|
644
|
+
status: job.status,
|
|
645
|
+
output: job.takeOutput(),
|
|
646
|
+
exitCode: job.exitCode,
|
|
647
|
+
sessionId: job.id,
|
|
648
|
+
nextPollMs: job.nextPollMs,
|
|
649
|
+
};
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
function pruneJobs(): void {
|
|
653
|
+
const now = Date.now();
|
|
654
|
+
for (const [id, job] of jobs) {
|
|
655
|
+
if (job.status !== "running" && now - job.startedAt > 10 * 60 * 1000)
|
|
656
|
+
jobs.delete(id);
|
|
657
|
+
}
|
|
658
|
+
if (jobs.size > 50) {
|
|
659
|
+
const finished = Array.from(jobs.values())
|
|
660
|
+
.filter((job) => job.status !== "running")
|
|
661
|
+
.sort((a, b) => a.startedAt - b.startedAt);
|
|
662
|
+
for (const job of finished.slice(0, jobs.size - 50))
|
|
663
|
+
jobs.delete(job.id);
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
function shellQuote(value: string): string {
|
|
668
|
+
return "'" + value.replace(/'/g, "'\\''") + "'";
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
function delay(ms: number): Promise<void> {
|
|
672
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
673
|
+
}
|