minimal-agent 0.11.0 → 0.12.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/package.json +4 -1
- package/plugins/workflow-runner/plugin.js +27 -15
- package/plugins/workflow-runner/plugin.ts +34 -20
- package/plugins/workflow-runner/src/agentCommon.js +13 -12
- package/plugins/workflow-runner/src/agentCommon.ts +22 -13
- package/plugins/workflow-runner/src/index.js +5 -3
- package/plugins/workflow-runner/src/index.ts +31 -13
- package/plugins/workflow-runner/src/multimodal.js +2 -1
- package/plugins/workflow-runner/src/multimodal.ts +2 -1
- package/plugins/workflow-runner/src/stepExecutors/skill.js +5 -6
- package/plugins/workflow-runner/src/stepExecutors/skill.ts +12 -7
- package/plugins/workflow-runner/src/stepExecutors/tool.js +68 -57
- package/plugins/workflow-runner/src/stepExecutors/tool.ts +88 -66
- package/plugins/workflow-runner/src/toolRuntime.js +48 -0
- package/plugins/workflow-runner/src/toolRuntime.ts +64 -0
- package/plugins/workflow-runner/src/types.ts +26 -7
- package/plugins/workflow-runner/test/context-files.test.ts +47 -16
- package/plugins/workflow-runner/test/hostCapability.test.ts +9 -3
- package/plugins/workflow-runner/test/multimodal.test.ts +61 -10
- package/plugins/workflow-runner/test/toolRuntimeIntegration.test.ts +168 -0
- package/src/cli/print.js +19 -4
- package/src/cli/print.ts +17 -4
- package/src/cli/streamJson.js +19 -4
- package/src/cli/streamJson.ts +17 -4
- package/src/config/configDefaults.js +25 -0
- package/src/config/configDefaults.ts +33 -0
- package/src/config/configFile.js +2 -21
- package/src/config/configFile.ts +11 -24
- package/src/config/configManagement.js +1 -1
- package/src/config/configManagement.ts +2 -2
- package/src/context/compact.js +4 -0
- package/src/context/compact.ts +4 -0
- package/src/context/contextBundle.js +38 -13
- package/src/context/contextBundle.ts +47 -13
- package/src/harness/stallGuard.js +19 -4
- package/src/harness/stallGuard.ts +30 -7
- package/src/loop.js +95 -11
- package/src/loop.ts +125 -16
- package/src/main.js +8 -1
- package/src/main.tsx +7 -1
- package/src/memory/constants.js +10 -0
- package/src/memory/constants.ts +13 -0
- package/src/memory/episodicMemory.js +274 -2
- package/src/memory/episodicMemory.ts +306 -1
- package/src/memory/fileStore.js +24 -4
- package/src/memory/fileStore.ts +22 -2
- package/src/memory/memoryService.js +79 -12
- package/src/memory/memoryService.ts +108 -9
- package/src/memory/repository.js +415 -85
- package/src/memory/repository.ts +437 -86
- package/src/memory/turnJournal.js +95 -0
- package/src/memory/turnJournal.ts +120 -0
- package/src/memory/types.ts +16 -0
- package/src/plugins/pluginApi.ts +30 -6
- package/src/plugins/pluginRunner.js +6 -2
- package/src/plugins/pluginRunner.ts +12 -7
- package/src/prompts/policyRegistry.js +27 -0
- package/src/prompts/policyRegistry.ts +27 -0
- package/src/prompts/promptAssembly.js +39 -7
- package/src/prompts/promptAssembly.ts +145 -59
- package/src/session/runAgentTurn.js +43 -21
- package/src/session/runAgentTurn.ts +68 -26
- package/src/session/runTurn.js +11 -46
- package/src/session/runTurn.ts +12 -46
- package/src/session/runtimeStore.js +149 -0
- package/src/session/runtimeStore.ts +197 -0
- package/src/session/sessionState.js +5 -0
- package/src/session/sessionState.ts +14 -0
- package/src/tools/agent/agent.js +143 -14
- package/src/tools/agent/agent.ts +149 -14
- package/src/tools/agent/agentTaskService.js +60 -0
- package/src/tools/agent/agentTaskService.ts +113 -0
- package/src/tools/agent/agentTypes.js +5 -0
- package/src/tools/agent/agentTypes.ts +5 -0
- package/src/tools/bash/bash.js +31 -11
- package/src/tools/bash/bash.ts +29 -10
- package/src/tools/capabilities.js +5 -0
- package/src/tools/capabilities.ts +11 -1
- package/src/tools/edit/edit.js +7 -4
- package/src/tools/edit/edit.ts +7 -4
- package/src/tools/edit/multi-edit.js +4 -2
- package/src/tools/edit/multi-edit.ts +4 -2
- package/src/tools/glob/glob.js +156 -40
- package/src/tools/glob/glob.ts +185 -46
- package/src/tools/index.js +35 -17
- package/src/tools/index.ts +81 -20
- package/src/tools/lsp/lsp.js +165 -22
- package/src/tools/lsp/lsp.ts +206 -36
- package/src/tools/powershell/powershell.js +116 -26
- package/src/tools/powershell/powershell.ts +155 -30
- package/src/tools/presentresult/presentresult.js +10 -0
- package/src/tools/presentresult/presentresult.ts +10 -0
- package/src/tools/read/read.js +244 -29
- package/src/tools/read/read.ts +391 -59
- package/src/tools/shared/diffSummary.js +44 -0
- package/src/tools/shared/diffSummary.ts +63 -0
- package/src/tools/shared/fileActivity.js +2 -1
- package/src/tools/shared/fileActivity.ts +3 -0
- package/src/tools/shared/permanentDeleteGuard.js +26 -0
- package/src/tools/shared/permanentDeleteGuard.ts +34 -0
- package/src/tools/shared/toolOutputStore.js +97 -0
- package/src/tools/shared/toolOutputStore.ts +110 -0
- package/src/tools/skill/skill.js +86 -0
- package/src/tools/skill/skill.ts +94 -0
- package/src/tools/snip/snip.js +67 -0
- package/src/tools/snip/snip.ts +80 -0
- package/src/tools/tasks/backgroundTaskService.js +79 -28
- package/src/tools/tasks/backgroundTaskService.ts +165 -49
- package/src/tools/tasks/taskManagement.js +163 -0
- package/src/tools/tasks/taskManagement.ts +180 -0
- package/src/tools/tasks/taskOutput.js +64 -14
- package/src/tools/tasks/taskOutput.ts +102 -23
- package/src/tools/tasks/taskStop.js +48 -8
- package/src/tools/tasks/taskStop.ts +69 -9
- package/src/tools/tasks/taskStore.js +46 -0
- package/src/tools/tasks/taskStore.ts +77 -0
- package/src/tools/terminal/terminalCapture.js +167 -0
- package/src/tools/terminal/terminalCapture.ts +179 -0
- package/src/tools/trash/trash.js +233 -0
- package/src/tools/trash/trash.ts +283 -0
- package/src/tools/worktree/sessionWorktree.js +128 -0
- package/src/tools/worktree/sessionWorktree.ts +158 -0
- package/src/tools/write/write.js +6 -2
- package/src/tools/write/write.ts +6 -2
- package/src/types.ts +12 -0
- package/src/ui/hooks/useChat.js +31 -11
- package/src/ui/hooks/useChat.ts +31 -13
- package/src/web/protocol.ts +78 -3
|
@@ -1,17 +1,20 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { spawn } from 'node:child_process';
|
|
3
3
|
import { EventEmitter } from 'node:events';
|
|
4
|
-
import { closeSync, createWriteStream, existsSync, openSync, readSync, statSync } from 'node:fs';
|
|
4
|
+
import { closeSync, createWriteStream, existsSync, openSync, readSync, statSync, } from 'node:fs';
|
|
5
5
|
import { mkdir, readdir, rm, stat } from 'node:fs/promises';
|
|
6
6
|
import { homedir } from 'node:os';
|
|
7
7
|
import { basename, join } from 'node:path';
|
|
8
8
|
import { killTree } from '../shared/killTree.js';
|
|
9
|
-
|
|
9
|
+
import { pruneRuntimeOutputs, registerRuntimeOutput, sessionRuntimeDir, unregisterRuntimeOutput, } from '../../session/runtimeStore.js';
|
|
10
|
+
export function createBackgroundTaskState() {
|
|
11
|
+
return { tasks: new Map() };
|
|
12
|
+
}
|
|
10
13
|
const fallbackState = createBackgroundTaskState();
|
|
11
14
|
/** Contains only states that currently own at least one running child. */
|
|
12
15
|
const liveStates = new Set();
|
|
13
16
|
const TASK_ROOT = join(homedir(), '.minimal-agent', 'tasks');
|
|
14
|
-
const MAX_LOG_BYTES =
|
|
17
|
+
const MAX_LOG_BYTES = 512 * 1024 * 1024;
|
|
15
18
|
const MAX_COMPLETED_PER_STATE = 50;
|
|
16
19
|
const MAX_GLOBAL_LOGS = 500;
|
|
17
20
|
const LOG_RETENTION_MS = 7 * 24 * 60 * 60_000;
|
|
@@ -19,22 +22,32 @@ function safeSegment(value) {
|
|
|
19
22
|
return value.replace(/[^a-zA-Z0-9._-]/g, '_').slice(0, 80) || 'local';
|
|
20
23
|
}
|
|
21
24
|
export async function startBackgroundTask(spec, state = fallbackState) {
|
|
22
|
-
const sessionDir = join(
|
|
25
|
+
const sessionDir = join(sessionRuntimeDir(spec), 'outputs');
|
|
23
26
|
await mkdir(sessionDir, { recursive: true });
|
|
24
|
-
const id = randomUUID()
|
|
27
|
+
const id = `proc_${randomUUID()}`;
|
|
25
28
|
const logPath = join(sessionDir, `${id}.log`);
|
|
29
|
+
registerRuntimeOutput(logPath);
|
|
26
30
|
const log = createWriteStream(logPath, { flags: 'wx', encoding: 'utf8' });
|
|
27
31
|
const child = spec.executable
|
|
28
32
|
? spawn(spec.executable, spec.args ?? [], {
|
|
29
|
-
cwd: spec.cwd,
|
|
30
|
-
|
|
33
|
+
cwd: spec.cwd,
|
|
34
|
+
env: spec.env,
|
|
35
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
36
|
+
windowsHide: true,
|
|
37
|
+
detached: process.platform !== 'win32',
|
|
31
38
|
})
|
|
32
39
|
: spawn(spec.command, {
|
|
33
|
-
cwd: spec.cwd,
|
|
34
|
-
|
|
40
|
+
cwd: spec.cwd,
|
|
41
|
+
env: spec.env,
|
|
42
|
+
shell: spec.shell ?? true,
|
|
43
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
44
|
+
windowsHide: true,
|
|
45
|
+
detached: process.platform !== 'win32',
|
|
35
46
|
});
|
|
36
47
|
let settle;
|
|
37
|
-
const completion = new Promise((resolvePromise) => {
|
|
48
|
+
const completion = new Promise((resolvePromise) => {
|
|
49
|
+
settle = resolvePromise;
|
|
50
|
+
});
|
|
38
51
|
const task = {
|
|
39
52
|
id,
|
|
40
53
|
command: spec.command,
|
|
@@ -50,9 +63,16 @@ export async function startBackgroundTask(spec, state = fallbackState) {
|
|
|
50
63
|
state.tasks.set(id, task);
|
|
51
64
|
liveStates.add(state);
|
|
52
65
|
let written = 0;
|
|
66
|
+
let truncated = false;
|
|
67
|
+
const outputHash = createHash('sha256');
|
|
53
68
|
const writeChunk = (source, value) => {
|
|
54
|
-
if (written >= MAX_LOG_BYTES)
|
|
55
|
-
|
|
69
|
+
if (written >= MAX_LOG_BYTES) {
|
|
70
|
+
if (!truncated) {
|
|
71
|
+
truncated = true;
|
|
72
|
+
log.write('\n[log truncated at 512 MiB; complete=false]\n');
|
|
73
|
+
}
|
|
74
|
+
return true;
|
|
75
|
+
}
|
|
56
76
|
const raw = Buffer.from(value);
|
|
57
77
|
const chunk = source === 'stderr'
|
|
58
78
|
? Buffer.concat([Buffer.from('[stderr] '), raw])
|
|
@@ -60,12 +80,26 @@ export async function startBackgroundTask(spec, state = fallbackState) {
|
|
|
60
80
|
const remaining = MAX_LOG_BYTES - written;
|
|
61
81
|
const selected = chunk.subarray(0, remaining);
|
|
62
82
|
written += selected.length;
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
83
|
+
outputHash.update(selected);
|
|
84
|
+
const accepted = log.write(selected, () => task.events.emit('change'));
|
|
85
|
+
if (selected.length < chunk.length) {
|
|
86
|
+
truncated = true;
|
|
87
|
+
log.write('\n[log truncated at 512 MiB; complete=false]\n');
|
|
88
|
+
}
|
|
89
|
+
return accepted;
|
|
66
90
|
};
|
|
67
|
-
child.stdout?.on('data', (chunk) =>
|
|
68
|
-
|
|
91
|
+
child.stdout?.on('data', (chunk) => {
|
|
92
|
+
if (!writeChunk('stdout', chunk)) {
|
|
93
|
+
child.stdout?.pause();
|
|
94
|
+
log.once('drain', () => child.stdout?.resume());
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
child.stderr?.on('data', (chunk) => {
|
|
98
|
+
if (!writeChunk('stderr', chunk)) {
|
|
99
|
+
child.stderr?.pause();
|
|
100
|
+
log.once('drain', () => child.stderr?.resume());
|
|
101
|
+
}
|
|
102
|
+
});
|
|
69
103
|
let finalized = false;
|
|
70
104
|
const finalize = (status, code, error) => {
|
|
71
105
|
if (finalized)
|
|
@@ -75,7 +109,12 @@ export async function startBackgroundTask(spec, state = fallbackState) {
|
|
|
75
109
|
task.finishedAt = Date.now();
|
|
76
110
|
task.exitCode = code;
|
|
77
111
|
task.child = undefined;
|
|
112
|
+
task.outputBytes = written;
|
|
113
|
+
task.outputSha256 = outputHash.digest('hex');
|
|
114
|
+
task.complete = !truncated;
|
|
78
115
|
log.end(error ? `[stderr] Failed to start: ${error}\n` : undefined, () => {
|
|
116
|
+
unregisterRuntimeOutput(logPath);
|
|
117
|
+
void pruneRuntimeOutputs();
|
|
79
118
|
task.events.emit('change');
|
|
80
119
|
settle();
|
|
81
120
|
pruneCompletedTasks(state);
|
|
@@ -87,16 +126,21 @@ export async function startBackgroundTask(spec, state = fallbackState) {
|
|
|
87
126
|
finalize('failed', null, error.message);
|
|
88
127
|
});
|
|
89
128
|
child.once('close', (code) => {
|
|
90
|
-
finalize(task.status === 'stopped'
|
|
129
|
+
finalize(task.status === 'stopped'
|
|
130
|
+
? 'stopped'
|
|
131
|
+
: code === 0
|
|
132
|
+
? 'completed'
|
|
133
|
+
: 'failed', code);
|
|
91
134
|
});
|
|
92
135
|
void pruneTaskLogs();
|
|
93
136
|
return task;
|
|
94
137
|
}
|
|
95
138
|
export function getBackgroundTask(id, state = fallbackState) {
|
|
96
|
-
return state.tasks.get(id)
|
|
139
|
+
return (state.tasks.get(id) ??
|
|
140
|
+
state.tasks.get(id.startsWith('proc_') ? id.slice(5) : `proc_${id}`));
|
|
97
141
|
}
|
|
98
142
|
export function stopBackgroundTask(id, state = fallbackState) {
|
|
99
|
-
const task =
|
|
143
|
+
const task = getBackgroundTask(id, state);
|
|
100
144
|
if (!task)
|
|
101
145
|
return undefined;
|
|
102
146
|
if (task.status === 'running' && task.child) {
|
|
@@ -148,9 +192,12 @@ function completeUtf8PrefixLength(bytes) {
|
|
|
148
192
|
if (index < 0)
|
|
149
193
|
return bytes.length;
|
|
150
194
|
const lead = bytes[index];
|
|
151
|
-
const expectedLength = lead >= 0xf0 && lead <= 0xf7
|
|
152
|
-
|
|
153
|
-
|
|
195
|
+
const expectedLength = lead >= 0xf0 && lead <= 0xf7
|
|
196
|
+
? 4
|
|
197
|
+
: lead >= 0xe0 && lead <= 0xef
|
|
198
|
+
? 3
|
|
199
|
+
: lead >= 0xc0 && lead <= 0xdf
|
|
200
|
+
? 2
|
|
154
201
|
: 1;
|
|
155
202
|
return expectedLength > continuationBytes + 1 ? index : bytes.length;
|
|
156
203
|
}
|
|
@@ -216,9 +263,13 @@ async function pruneTaskLogsInner() {
|
|
|
216
263
|
catch { }
|
|
217
264
|
}
|
|
218
265
|
function pruneTaskLogs() {
|
|
219
|
-
prunePromise ??= pruneTaskLogsInner().finally(() => {
|
|
266
|
+
prunePromise ??= pruneTaskLogsInner().finally(() => {
|
|
267
|
+
prunePromise = undefined;
|
|
268
|
+
});
|
|
220
269
|
return prunePromise;
|
|
221
270
|
}
|
|
222
|
-
process.once('exit', () => {
|
|
223
|
-
for (const
|
|
224
|
-
|
|
271
|
+
process.once('exit', () => {
|
|
272
|
+
for (const state of liveStates)
|
|
273
|
+
for (const task of state.tasks.values())
|
|
274
|
+
stopBackgroundTask(task.id, state);
|
|
275
|
+
});
|
|
@@ -1,14 +1,28 @@
|
|
|
1
|
-
import { randomUUID } from 'node:crypto';
|
|
1
|
+
import { createHash, randomUUID } from 'node:crypto';
|
|
2
2
|
import { spawn, type ChildProcess } from 'node:child_process';
|
|
3
3
|
import { EventEmitter } from 'node:events';
|
|
4
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
closeSync,
|
|
6
|
+
createWriteStream,
|
|
7
|
+
existsSync,
|
|
8
|
+
openSync,
|
|
9
|
+
readSync,
|
|
10
|
+
statSync,
|
|
11
|
+
} from 'node:fs';
|
|
5
12
|
import { mkdir, readdir, rm, stat } from 'node:fs/promises';
|
|
6
13
|
import { homedir } from 'node:os';
|
|
7
14
|
import { basename, join } from 'node:path';
|
|
8
15
|
|
|
9
16
|
import { killTree } from '../shared/killTree.js';
|
|
17
|
+
import {
|
|
18
|
+
pruneRuntimeOutputs,
|
|
19
|
+
registerRuntimeOutput,
|
|
20
|
+
sessionRuntimeDir,
|
|
21
|
+
unregisterRuntimeOutput,
|
|
22
|
+
} from '../../session/runtimeStore.js';
|
|
10
23
|
|
|
11
|
-
export type BackgroundTaskStatus =
|
|
24
|
+
export type BackgroundTaskStatus =
|
|
25
|
+
'running' | 'completed' | 'failed' | 'stopped';
|
|
12
26
|
export interface BackgroundTask {
|
|
13
27
|
id: string;
|
|
14
28
|
command: string;
|
|
@@ -18,19 +32,26 @@ export interface BackgroundTask {
|
|
|
18
32
|
finishedAt?: number;
|
|
19
33
|
status: BackgroundTaskStatus;
|
|
20
34
|
exitCode?: number | null;
|
|
35
|
+
outputBytes?: number;
|
|
36
|
+
outputSha256?: string;
|
|
37
|
+
complete?: boolean;
|
|
21
38
|
child?: ChildProcess;
|
|
22
39
|
completion: Promise<void>;
|
|
23
40
|
events: EventEmitter;
|
|
24
41
|
shell: 'bash' | 'powershell';
|
|
25
42
|
}
|
|
26
|
-
export interface BackgroundTaskState {
|
|
27
|
-
|
|
43
|
+
export interface BackgroundTaskState {
|
|
44
|
+
tasks: Map<string, BackgroundTask>;
|
|
45
|
+
}
|
|
46
|
+
export function createBackgroundTaskState(): BackgroundTaskState {
|
|
47
|
+
return { tasks: new Map() };
|
|
48
|
+
}
|
|
28
49
|
|
|
29
50
|
const fallbackState = createBackgroundTaskState();
|
|
30
51
|
/** Contains only states that currently own at least one running child. */
|
|
31
52
|
const liveStates = new Set<BackgroundTaskState>();
|
|
32
53
|
const TASK_ROOT = join(homedir(), '.minimal-agent', 'tasks');
|
|
33
|
-
const MAX_LOG_BYTES =
|
|
54
|
+
const MAX_LOG_BYTES = 512 * 1024 * 1024;
|
|
34
55
|
const MAX_COMPLETED_PER_STATE = 50;
|
|
35
56
|
const MAX_GLOBAL_LOGS = 500;
|
|
36
57
|
const LOG_RETENTION_MS = 7 * 24 * 60 * 60_000;
|
|
@@ -47,6 +68,7 @@ export interface BackgroundSpawnSpec {
|
|
|
47
68
|
args?: string[];
|
|
48
69
|
shell?: boolean;
|
|
49
70
|
sessionId?: string;
|
|
71
|
+
workspaceId?: string;
|
|
50
72
|
shellKind?: 'bash' | 'powershell';
|
|
51
73
|
}
|
|
52
74
|
|
|
@@ -54,23 +76,33 @@ export async function startBackgroundTask(
|
|
|
54
76
|
spec: BackgroundSpawnSpec,
|
|
55
77
|
state: BackgroundTaskState = fallbackState,
|
|
56
78
|
): Promise<BackgroundTask> {
|
|
57
|
-
const sessionDir = join(
|
|
79
|
+
const sessionDir = join(sessionRuntimeDir(spec), 'outputs');
|
|
58
80
|
await mkdir(sessionDir, { recursive: true });
|
|
59
|
-
const id = randomUUID()
|
|
81
|
+
const id = `proc_${randomUUID()}`;
|
|
60
82
|
const logPath = join(sessionDir, `${id}.log`);
|
|
83
|
+
registerRuntimeOutput(logPath);
|
|
61
84
|
const log = createWriteStream(logPath, { flags: 'wx', encoding: 'utf8' });
|
|
62
85
|
const child = spec.executable
|
|
63
86
|
? spawn(spec.executable, spec.args ?? [], {
|
|
64
|
-
cwd: spec.cwd,
|
|
65
|
-
|
|
87
|
+
cwd: spec.cwd,
|
|
88
|
+
env: spec.env,
|
|
89
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
90
|
+
windowsHide: true,
|
|
91
|
+
detached: process.platform !== 'win32',
|
|
66
92
|
})
|
|
67
93
|
: spawn(spec.command, {
|
|
68
|
-
cwd: spec.cwd,
|
|
69
|
-
|
|
94
|
+
cwd: spec.cwd,
|
|
95
|
+
env: spec.env,
|
|
96
|
+
shell: spec.shell ?? true,
|
|
97
|
+
stdio: ['ignore', 'pipe', 'pipe'],
|
|
98
|
+
windowsHide: true,
|
|
99
|
+
detached: process.platform !== 'win32',
|
|
70
100
|
});
|
|
71
101
|
|
|
72
102
|
let settle!: () => void;
|
|
73
|
-
const completion = new Promise<void>((resolvePromise) => {
|
|
103
|
+
const completion = new Promise<void>((resolvePromise) => {
|
|
104
|
+
settle = resolvePromise;
|
|
105
|
+
});
|
|
74
106
|
const task: BackgroundTask = {
|
|
75
107
|
id,
|
|
76
108
|
command: spec.command,
|
|
@@ -87,58 +119,120 @@ export async function startBackgroundTask(
|
|
|
87
119
|
liveStates.add(state);
|
|
88
120
|
|
|
89
121
|
let written = 0;
|
|
90
|
-
|
|
91
|
-
|
|
122
|
+
let truncated = false;
|
|
123
|
+
const outputHash = createHash('sha256');
|
|
124
|
+
const writeChunk = (
|
|
125
|
+
source: 'stdout' | 'stderr',
|
|
126
|
+
value: Buffer | string,
|
|
127
|
+
): boolean => {
|
|
128
|
+
if (written >= MAX_LOG_BYTES) {
|
|
129
|
+
if (!truncated) {
|
|
130
|
+
truncated = true;
|
|
131
|
+
log.write('\n[log truncated at 512 MiB; complete=false]\n');
|
|
132
|
+
}
|
|
133
|
+
return true;
|
|
134
|
+
}
|
|
92
135
|
const raw = Buffer.from(value);
|
|
93
|
-
const chunk =
|
|
94
|
-
|
|
95
|
-
|
|
136
|
+
const chunk =
|
|
137
|
+
source === 'stderr'
|
|
138
|
+
? Buffer.concat([Buffer.from('[stderr] '), raw])
|
|
139
|
+
: raw;
|
|
96
140
|
const remaining = MAX_LOG_BYTES - written;
|
|
97
141
|
const selected = chunk.subarray(0, remaining);
|
|
98
142
|
written += selected.length;
|
|
99
|
-
|
|
100
|
-
|
|
143
|
+
outputHash.update(selected);
|
|
144
|
+
const accepted = log.write(selected, () => task.events.emit('change'));
|
|
145
|
+
if (selected.length < chunk.length) {
|
|
146
|
+
truncated = true;
|
|
147
|
+
log.write('\n[log truncated at 512 MiB; complete=false]\n');
|
|
148
|
+
}
|
|
149
|
+
return accepted;
|
|
101
150
|
};
|
|
102
|
-
child.stdout?.on('data', (chunk) =>
|
|
103
|
-
|
|
151
|
+
child.stdout?.on('data', (chunk) => {
|
|
152
|
+
if (!writeChunk('stdout', chunk)) {
|
|
153
|
+
child.stdout?.pause();
|
|
154
|
+
log.once('drain', () => child.stdout?.resume());
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
child.stderr?.on('data', (chunk) => {
|
|
158
|
+
if (!writeChunk('stderr', chunk)) {
|
|
159
|
+
child.stderr?.pause();
|
|
160
|
+
log.once('drain', () => child.stderr?.resume());
|
|
161
|
+
}
|
|
162
|
+
});
|
|
104
163
|
let finalized = false;
|
|
105
|
-
const finalize = (
|
|
164
|
+
const finalize = (
|
|
165
|
+
status: BackgroundTaskStatus,
|
|
166
|
+
code: number | null,
|
|
167
|
+
error?: string,
|
|
168
|
+
) => {
|
|
106
169
|
if (finalized) return;
|
|
107
170
|
finalized = true;
|
|
108
|
-
task.status = status;
|
|
171
|
+
task.status = status;
|
|
172
|
+
task.finishedAt = Date.now();
|
|
173
|
+
task.exitCode = code;
|
|
174
|
+
task.child = undefined;
|
|
175
|
+
task.outputBytes = written;
|
|
176
|
+
task.outputSha256 = outputHash.digest('hex');
|
|
177
|
+
task.complete = !truncated;
|
|
109
178
|
log.end(error ? `[stderr] Failed to start: ${error}\n` : undefined, () => {
|
|
179
|
+
unregisterRuntimeOutput(logPath);
|
|
180
|
+
void pruneRuntimeOutputs();
|
|
110
181
|
task.events.emit('change');
|
|
111
182
|
settle();
|
|
112
183
|
pruneCompletedTasks(state);
|
|
113
|
-
if (![...state.tasks.values()].some((item) => item.status === 'running'))
|
|
184
|
+
if (![...state.tasks.values()].some((item) => item.status === 'running'))
|
|
185
|
+
liveStates.delete(state);
|
|
114
186
|
});
|
|
115
187
|
};
|
|
116
188
|
child.once('error', (error) => {
|
|
117
189
|
finalize('failed', null, error.message);
|
|
118
190
|
});
|
|
119
191
|
child.once('close', (code) => {
|
|
120
|
-
finalize(
|
|
192
|
+
finalize(
|
|
193
|
+
task.status === 'stopped'
|
|
194
|
+
? 'stopped'
|
|
195
|
+
: code === 0
|
|
196
|
+
? 'completed'
|
|
197
|
+
: 'failed',
|
|
198
|
+
code,
|
|
199
|
+
);
|
|
121
200
|
});
|
|
122
201
|
void pruneTaskLogs();
|
|
123
202
|
return task;
|
|
124
203
|
}
|
|
125
204
|
|
|
126
|
-
export function getBackgroundTask(
|
|
127
|
-
|
|
205
|
+
export function getBackgroundTask(
|
|
206
|
+
id: string,
|
|
207
|
+
state: BackgroundTaskState = fallbackState,
|
|
208
|
+
): BackgroundTask | undefined {
|
|
209
|
+
return (
|
|
210
|
+
state.tasks.get(id) ??
|
|
211
|
+
state.tasks.get(id.startsWith('proc_') ? id.slice(5) : `proc_${id}`)
|
|
212
|
+
);
|
|
128
213
|
}
|
|
129
214
|
|
|
130
|
-
export function stopBackgroundTask(
|
|
131
|
-
|
|
215
|
+
export function stopBackgroundTask(
|
|
216
|
+
id: string,
|
|
217
|
+
state: BackgroundTaskState = fallbackState,
|
|
218
|
+
): BackgroundTask | undefined {
|
|
219
|
+
const task = getBackgroundTask(id, state);
|
|
132
220
|
if (!task) return undefined;
|
|
133
221
|
if (task.status === 'running' && task.child) {
|
|
134
|
-
task.status = 'stopped';
|
|
222
|
+
task.status = 'stopped';
|
|
223
|
+
task.finishedAt = Date.now();
|
|
135
224
|
killTree(task.child);
|
|
136
225
|
}
|
|
137
226
|
return task;
|
|
138
227
|
}
|
|
139
228
|
|
|
140
|
-
export function readTaskLog(
|
|
141
|
-
|
|
229
|
+
export function readTaskLog(
|
|
230
|
+
task: BackgroundTask,
|
|
231
|
+
offset = 0,
|
|
232
|
+
limit = 30_000,
|
|
233
|
+
): { text: string; nextOffset: number; totalBytes: number } {
|
|
234
|
+
if (!existsSync(task.logPath))
|
|
235
|
+
return { text: '', nextOffset: offset, totalBytes: 0 };
|
|
142
236
|
const totalBytes = statSync(task.logPath).size;
|
|
143
237
|
const start = Math.max(0, Math.min(offset, totalBytes));
|
|
144
238
|
const requestedLength = Math.max(0, Math.min(limit, totalBytes - start));
|
|
@@ -155,7 +249,8 @@ export function readTaskLog(task: BackgroundTask, offset = 0, limit = 30_000): {
|
|
|
155
249
|
closeSync(descriptor);
|
|
156
250
|
}
|
|
157
251
|
let pageLength = Math.min(requestedLength, bytesRead);
|
|
158
|
-
while (pageLength < bytesRead && (bytes[pageLength]! & 0xc0) === 0x80)
|
|
252
|
+
while (pageLength < bytesRead && (bytes[pageLength]! & 0xc0) === 0x80)
|
|
253
|
+
pageLength += 1;
|
|
159
254
|
const validLength = completeUtf8PrefixLength(bytes.subarray(0, pageLength));
|
|
160
255
|
return {
|
|
161
256
|
text: bytes.subarray(0, validLength).toString('utf8'),
|
|
@@ -174,14 +269,20 @@ function completeUtf8PrefixLength(bytes: Uint8Array): number {
|
|
|
174
269
|
}
|
|
175
270
|
if (index < 0) return bytes.length;
|
|
176
271
|
const lead = bytes[index]!;
|
|
177
|
-
const expectedLength =
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
272
|
+
const expectedLength =
|
|
273
|
+
lead >= 0xf0 && lead <= 0xf7
|
|
274
|
+
? 4
|
|
275
|
+
: lead >= 0xe0 && lead <= 0xef
|
|
276
|
+
? 3
|
|
277
|
+
: lead >= 0xc0 && lead <= 0xdf
|
|
278
|
+
? 2
|
|
279
|
+
: 1;
|
|
181
280
|
return expectedLength > continuationBytes + 1 ? index : bytes.length;
|
|
182
281
|
}
|
|
183
282
|
|
|
184
|
-
export async function stopAllBackgroundTasks(
|
|
283
|
+
export async function stopAllBackgroundTasks(
|
|
284
|
+
state?: BackgroundTaskState,
|
|
285
|
+
): Promise<void> {
|
|
185
286
|
const states = state ? [state] : [...liveStates];
|
|
186
287
|
const completions: Promise<void>[] = [];
|
|
187
288
|
for (const item of states) {
|
|
@@ -202,7 +303,9 @@ export async function stopAllBackgroundTasks(state?: BackgroundTaskState): Promi
|
|
|
202
303
|
}
|
|
203
304
|
|
|
204
305
|
/** Release all process/task resources owned by a disposed host session. */
|
|
205
|
-
export async function disposeBackgroundTaskState(
|
|
306
|
+
export async function disposeBackgroundTaskState(
|
|
307
|
+
state: BackgroundTaskState,
|
|
308
|
+
): Promise<void> {
|
|
206
309
|
await stopAllBackgroundTasks(state);
|
|
207
310
|
state.tasks.clear();
|
|
208
311
|
liveStates.delete(state);
|
|
@@ -211,7 +314,9 @@ export async function disposeBackgroundTaskState(state: BackgroundTaskState): Pr
|
|
|
211
314
|
function pruneCompletedTasks(state: BackgroundTaskState): void {
|
|
212
315
|
const completed = [...state.tasks.values()]
|
|
213
316
|
.filter((task) => task.status !== 'running')
|
|
214
|
-
.sort(
|
|
317
|
+
.sort(
|
|
318
|
+
(a, b) => (b.finishedAt ?? b.startedAt) - (a.finishedAt ?? a.startedAt),
|
|
319
|
+
);
|
|
215
320
|
for (const task of completed.slice(MAX_COMPLETED_PER_STATE)) {
|
|
216
321
|
state.tasks.delete(task.id);
|
|
217
322
|
task.events.removeAllListeners();
|
|
@@ -233,21 +338,32 @@ async function pruneTaskLogsInner(): Promise<void> {
|
|
|
233
338
|
}
|
|
234
339
|
files.sort((a, b) => b.mtime - a.mtime);
|
|
235
340
|
const activeLogs = new Set(
|
|
236
|
-
[...liveStates].flatMap((state) =>
|
|
237
|
-
.
|
|
238
|
-
|
|
341
|
+
[...liveStates].flatMap((state) =>
|
|
342
|
+
[...state.tasks.values()]
|
|
343
|
+
.filter((task) => task.status === 'running')
|
|
344
|
+
.map((task) => task.logPath),
|
|
345
|
+
),
|
|
239
346
|
);
|
|
240
347
|
const completedFiles = files.filter((item) => !activeLogs.has(item.path));
|
|
241
348
|
const cutoff = Date.now() - LOG_RETENTION_MS;
|
|
242
|
-
await Promise.all(
|
|
243
|
-
|
|
244
|
-
|
|
349
|
+
await Promise.all(
|
|
350
|
+
completedFiles
|
|
351
|
+
.filter(
|
|
352
|
+
(item, index) => index >= MAX_GLOBAL_LOGS || item.mtime < cutoff,
|
|
353
|
+
)
|
|
354
|
+
.map((item) => rm(item.path, { force: true })),
|
|
355
|
+
);
|
|
245
356
|
} catch {}
|
|
246
357
|
}
|
|
247
358
|
|
|
248
359
|
function pruneTaskLogs(): Promise<void> {
|
|
249
|
-
prunePromise ??= pruneTaskLogsInner().finally(() => {
|
|
360
|
+
prunePromise ??= pruneTaskLogsInner().finally(() => {
|
|
361
|
+
prunePromise = undefined;
|
|
362
|
+
});
|
|
250
363
|
return prunePromise;
|
|
251
364
|
}
|
|
252
365
|
|
|
253
|
-
process.once('exit', () => {
|
|
366
|
+
process.once('exit', () => {
|
|
367
|
+
for (const state of liveStates)
|
|
368
|
+
for (const task of state.tasks.values()) stopBackgroundTask(task.id, state);
|
|
369
|
+
});
|