pi-better-background-tasks 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +65 -0
- package/package.json +52 -0
- package/src/conditions.ts +92 -0
- package/src/goal-provider.ts +116 -0
- package/src/index.ts +23 -0
- package/src/logs.ts +30 -0
- package/src/navigator-provider.ts +212 -0
- package/src/process.ts +99 -0
- package/src/registry.ts +77 -0
- package/src/runtime.ts +345 -0
- package/src/tools.ts +476 -0
- package/src/types.ts +84 -0
package/src/registry.ts
ADDED
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { mkdirSync, readFileSync, readdirSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { BackgroundTaskMeta } from "./types.js";
|
|
5
|
+
import { isTerminalStatus } from "./types.js";
|
|
6
|
+
|
|
7
|
+
let seq = 0;
|
|
8
|
+
const metaCache = new Map<string, BackgroundTaskMeta>();
|
|
9
|
+
|
|
10
|
+
export function baseDir(): string {
|
|
11
|
+
return join(tmpdir(), "pi-better-background-tasks");
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function tasksDir(): string {
|
|
15
|
+
return join(baseDir(), "tasks");
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function taskDir(id: string): string {
|
|
19
|
+
return join(tasksDir(), id);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function metaPathFor(id: string): string {
|
|
23
|
+
return join(taskDir(id), "meta.json");
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function logPathFor(id: string): string {
|
|
27
|
+
return join(taskDir(id), "output.log");
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function nextTaskId(): string {
|
|
31
|
+
seq += 1;
|
|
32
|
+
return `bg_${process.pid.toString(36)}_${Date.now().toString(36)}_${seq}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function ensureTaskDir(id: string): void {
|
|
36
|
+
mkdirSync(taskDir(id), { recursive: true });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function writeMeta(meta: BackgroundTaskMeta): void {
|
|
40
|
+
ensureTaskDir(meta.id);
|
|
41
|
+
writeFileSync(metaPathFor(meta.id), JSON.stringify(meta, null, 2));
|
|
42
|
+
metaCache.set(meta.id, meta);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export function readMeta(id: string): BackgroundTaskMeta | undefined {
|
|
46
|
+
try {
|
|
47
|
+
const meta = JSON.parse(readFileSync(metaPathFor(id), "utf8")) as BackgroundTaskMeta;
|
|
48
|
+
metaCache.set(id, meta);
|
|
49
|
+
return meta;
|
|
50
|
+
} catch {
|
|
51
|
+
metaCache.delete(id);
|
|
52
|
+
return undefined;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export function listMetas(): BackgroundTaskMeta[] {
|
|
57
|
+
let ids: string[];
|
|
58
|
+
try {
|
|
59
|
+
ids = readdirSync(tasksDir());
|
|
60
|
+
} catch {
|
|
61
|
+
return [];
|
|
62
|
+
}
|
|
63
|
+
const liveIds = new Set(ids);
|
|
64
|
+
for (const cachedId of metaCache.keys()) {
|
|
65
|
+
if (!liveIds.has(cachedId)) metaCache.delete(cachedId);
|
|
66
|
+
}
|
|
67
|
+
return ids
|
|
68
|
+
.map(readMetaForSweep)
|
|
69
|
+
.filter((meta): meta is BackgroundTaskMeta => meta !== undefined)
|
|
70
|
+
.sort((a, b) => b.startedAt - a.startedAt);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function readMetaForSweep(id: string): BackgroundTaskMeta | undefined {
|
|
74
|
+
const cached = metaCache.get(id);
|
|
75
|
+
if (cached && isTerminalStatus(cached.status)) return cached;
|
|
76
|
+
return readMeta(id);
|
|
77
|
+
}
|
package/src/runtime.ts
ADDED
|
@@ -0,0 +1,345 @@
|
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { appendLine, appendWatchResult } from "./logs.js";
|
|
3
|
+
import { evaluateCondition } from "./conditions.js";
|
|
4
|
+
import { processExists, runCommandOnce, spawnCommand, stopProcessGroup } from "./process.js";
|
|
5
|
+
import { ensureTaskDir, logPathFor, nextTaskId, readMeta, writeMeta } from "./registry.js";
|
|
6
|
+
import type { BackgroundTaskCallbackOrigin, BackgroundTaskMeta, CommandSpec, Condition, TerminalResult } from "./types.js";
|
|
7
|
+
import { isTerminalStatus } from "./types.js";
|
|
8
|
+
|
|
9
|
+
const watcherTimers = new Map<string, ReturnType<typeof setTimeout>>();
|
|
10
|
+
const activePolls = new Set<string>();
|
|
11
|
+
|
|
12
|
+
export const DEFAULT_WATCH_TIMEOUT_SECONDS = 15 * 60;
|
|
13
|
+
|
|
14
|
+
export type ActiveSessionProvider = () => BackgroundTaskCallbackOrigin | undefined;
|
|
15
|
+
|
|
16
|
+
export interface SpawnTaskParams extends CommandSpec {
|
|
17
|
+
name?: string;
|
|
18
|
+
callback?: boolean;
|
|
19
|
+
timeout_seconds?: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface WatchTaskParams extends CommandSpec {
|
|
23
|
+
name?: string;
|
|
24
|
+
callback?: boolean;
|
|
25
|
+
interval_seconds?: number;
|
|
26
|
+
timeout_seconds?: number;
|
|
27
|
+
success_when: Condition;
|
|
28
|
+
failure_when?: Condition;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function spawnTask(
|
|
32
|
+
pi: ExtensionAPI,
|
|
33
|
+
params: SpawnTaskParams,
|
|
34
|
+
defaultCwd: string,
|
|
35
|
+
callbackOrigin?: BackgroundTaskCallbackOrigin,
|
|
36
|
+
getActiveSession?: ActiveSessionProvider,
|
|
37
|
+
): BackgroundTaskMeta {
|
|
38
|
+
const id = nextTaskId();
|
|
39
|
+
const cwd = params.cwd ?? defaultCwd;
|
|
40
|
+
const logPath = logPathFor(id);
|
|
41
|
+
ensureTaskDir(id);
|
|
42
|
+
const spawned = spawnCommand({ ...params, cwd, shell: params.shell ?? true }, logPath, true);
|
|
43
|
+
const now = Date.now();
|
|
44
|
+
const meta: BackgroundTaskMeta = {
|
|
45
|
+
id,
|
|
46
|
+
name: params.name,
|
|
47
|
+
kind: "process",
|
|
48
|
+
status: "running",
|
|
49
|
+
startedAt: now,
|
|
50
|
+
deadlineAt: params.timeout_seconds ? now + params.timeout_seconds * 1000 : undefined,
|
|
51
|
+
logPath,
|
|
52
|
+
callback: params.callback,
|
|
53
|
+
callbackOrigin,
|
|
54
|
+
command: params.command,
|
|
55
|
+
argv: params.argv,
|
|
56
|
+
shell: params.shell ?? true,
|
|
57
|
+
cwd,
|
|
58
|
+
env: params.env,
|
|
59
|
+
pid: spawned.child.pid,
|
|
60
|
+
pgid: spawned.pgid,
|
|
61
|
+
spawnPid: process.pid,
|
|
62
|
+
};
|
|
63
|
+
writeMeta(meta);
|
|
64
|
+
spawned.child.unref();
|
|
65
|
+
spawned.child.on("close", (exitCode, signal) => {
|
|
66
|
+
const latest = readMeta(id);
|
|
67
|
+
if (!latest || isTerminalStatus(latest.status)) return;
|
|
68
|
+
latest.status = exitCode === 0 ? "succeeded" : "failed";
|
|
69
|
+
latest.endedAt = Date.now();
|
|
70
|
+
latest.lastExitCode = exitCode;
|
|
71
|
+
latest.lastSignal = signal;
|
|
72
|
+
latest.result = { exitCode, signal };
|
|
73
|
+
writeMeta(latest);
|
|
74
|
+
void notifyTerminal(pi, latest, getActiveSession);
|
|
75
|
+
});
|
|
76
|
+
if (meta.deadlineAt) scheduleProcessTimeout(pi, id, meta.deadlineAt, getActiveSession);
|
|
77
|
+
return meta;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export function startWatchTask(
|
|
81
|
+
pi: ExtensionAPI,
|
|
82
|
+
params: WatchTaskParams,
|
|
83
|
+
defaultCwd: string,
|
|
84
|
+
callbackOrigin?: BackgroundTaskCallbackOrigin,
|
|
85
|
+
getActiveSession?: ActiveSessionProvider,
|
|
86
|
+
): BackgroundTaskMeta {
|
|
87
|
+
const id = nextTaskId();
|
|
88
|
+
const cwd = params.cwd ?? defaultCwd;
|
|
89
|
+
const now = Date.now();
|
|
90
|
+
const timeoutSeconds = resolveWatchTimeoutSeconds(params.timeout_seconds);
|
|
91
|
+
const meta: BackgroundTaskMeta = {
|
|
92
|
+
id,
|
|
93
|
+
name: params.name,
|
|
94
|
+
kind: "command_watch",
|
|
95
|
+
status: "running",
|
|
96
|
+
startedAt: now,
|
|
97
|
+
deadlineAt: timeoutSeconds ? now + timeoutSeconds * 1000 : undefined,
|
|
98
|
+
intervalMs: Math.max(1, params.interval_seconds ?? 30) * 1000,
|
|
99
|
+
logPath: logPathFor(id),
|
|
100
|
+
callback: params.callback,
|
|
101
|
+
callbackOrigin,
|
|
102
|
+
command: params.command,
|
|
103
|
+
argv: params.argv,
|
|
104
|
+
shell: params.shell ?? true,
|
|
105
|
+
cwd,
|
|
106
|
+
env: params.env,
|
|
107
|
+
spawnPid: process.pid,
|
|
108
|
+
successWhen: params.success_when,
|
|
109
|
+
failureWhen: params.failure_when,
|
|
110
|
+
notifyOn: "terminal",
|
|
111
|
+
};
|
|
112
|
+
ensureTaskDir(id);
|
|
113
|
+
appendLine(meta.logPath, `--- watch ${new Date(now).toISOString()} interval_ms=${meta.intervalMs} ---`);
|
|
114
|
+
writeMeta(meta);
|
|
115
|
+
scheduleWatch(pi, id, 0, getActiveSession);
|
|
116
|
+
return meta;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function resolveWatchTimeoutSeconds(timeoutSeconds: number | undefined): number | undefined {
|
|
120
|
+
if (timeoutSeconds === undefined) return DEFAULT_WATCH_TIMEOUT_SECONDS;
|
|
121
|
+
if (timeoutSeconds <= 0) return undefined;
|
|
122
|
+
return timeoutSeconds;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export function resumeRunningTask(
|
|
126
|
+
pi: ExtensionAPI,
|
|
127
|
+
meta: BackgroundTaskMeta,
|
|
128
|
+
getActiveSession?: ActiveSessionProvider,
|
|
129
|
+
): BackgroundTaskMeta {
|
|
130
|
+
if (meta.status !== "running") {
|
|
131
|
+
void notifyTerminal(pi, meta, getActiveSession);
|
|
132
|
+
return meta;
|
|
133
|
+
}
|
|
134
|
+
if (meta.kind === "command_watch") {
|
|
135
|
+
scheduleWatch(pi, meta.id, 0, getActiveSession);
|
|
136
|
+
return meta;
|
|
137
|
+
}
|
|
138
|
+
if (meta.kind === "process" && meta.pid && !processExists(meta.pid)) {
|
|
139
|
+
meta.status = "failed";
|
|
140
|
+
meta.endedAt = Date.now();
|
|
141
|
+
meta.error = "process is no longer alive; exit result was not captured by this pi session";
|
|
142
|
+
meta.result = { reason: meta.error };
|
|
143
|
+
writeMeta(meta);
|
|
144
|
+
void notifyTerminal(pi, meta, getActiveSession);
|
|
145
|
+
return meta;
|
|
146
|
+
}
|
|
147
|
+
if (meta.kind === "process" && meta.deadlineAt) scheduleProcessTimeout(pi, meta.id, meta.deadlineAt, getActiveSession);
|
|
148
|
+
return meta;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
export function stopTask(pi: ExtensionAPI, id: string, getActiveSession?: ActiveSessionProvider): BackgroundTaskMeta | undefined {
|
|
152
|
+
const meta = readMeta(id);
|
|
153
|
+
if (!meta) return undefined;
|
|
154
|
+
if (isTerminalStatus(meta.status)) return meta;
|
|
155
|
+
meta.status = "cancelled";
|
|
156
|
+
meta.endedAt = Date.now();
|
|
157
|
+
meta.stopRequestedAt = meta.endedAt;
|
|
158
|
+
meta.result = { reason: "cancelled" };
|
|
159
|
+
writeMeta(meta);
|
|
160
|
+
clearWatchTimer(id);
|
|
161
|
+
if (meta.kind === "process" && meta.pid) {
|
|
162
|
+
try {
|
|
163
|
+
stopProcessGroup(meta.pid, meta.pgid);
|
|
164
|
+
} catch (error) {
|
|
165
|
+
meta.error = error instanceof Error ? error.message : String(error);
|
|
166
|
+
writeMeta(meta);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
void notifyTerminal(pi, meta, getActiveSession);
|
|
170
|
+
return meta;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
function scheduleWatch(pi: ExtensionAPI, id: string, delayMs: number, getActiveSession?: ActiveSessionProvider): void {
|
|
174
|
+
clearWatchTimer(id);
|
|
175
|
+
const timer = setTimeout(() => void pollWatch(pi, id, getActiveSession), delayMs);
|
|
176
|
+
timer.unref();
|
|
177
|
+
watcherTimers.set(id, timer);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function clearWatchTimer(id: string): void {
|
|
181
|
+
const timer = watcherTimers.get(id);
|
|
182
|
+
if (timer) clearTimeout(timer);
|
|
183
|
+
watcherTimers.delete(id);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
async function pollWatch(pi: ExtensionAPI, id: string, getActiveSession?: ActiveSessionProvider): Promise<void> {
|
|
187
|
+
if (activePolls.has(id)) return;
|
|
188
|
+
activePolls.add(id);
|
|
189
|
+
try {
|
|
190
|
+
const meta = readMeta(id);
|
|
191
|
+
if (!meta || meta.status !== "running" || meta.kind !== "command_watch") return;
|
|
192
|
+
const now = Date.now();
|
|
193
|
+
if (meta.deadlineAt && now >= meta.deadlineAt) {
|
|
194
|
+
finalize(meta, { status: "timed_out", reason: "timeout" }, pi, getActiveSession);
|
|
195
|
+
return;
|
|
196
|
+
}
|
|
197
|
+
const result = await runCommandOnce(commandSpecFromMeta(meta));
|
|
198
|
+
appendWatchResult(meta.logPath, result);
|
|
199
|
+
const latest = readMeta(id);
|
|
200
|
+
if (!latest || latest.status !== "running") return;
|
|
201
|
+
latest.lastCheckedAt = Date.now();
|
|
202
|
+
latest.lastExitCode = result.exitCode;
|
|
203
|
+
latest.lastSignal = result.signal;
|
|
204
|
+
latest.lastState = extractLastState(result);
|
|
205
|
+
|
|
206
|
+
if (latest.failureWhen) {
|
|
207
|
+
const failure = evaluateCondition(latest.failureWhen, result);
|
|
208
|
+
if (failure.matched) {
|
|
209
|
+
finalize(latest, { status: "failed", reason: "failure condition matched", matchedCondition: latest.failureWhen, commandResult: result }, pi, getActiveSession);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (latest.successWhen) {
|
|
215
|
+
const success = evaluateCondition(latest.successWhen, result);
|
|
216
|
+
if (success.matched) {
|
|
217
|
+
finalize(latest, { status: "succeeded", reason: "success condition matched", matchedCondition: latest.successWhen, commandResult: result }, pi, getActiveSession);
|
|
218
|
+
return;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
writeMeta(latest);
|
|
223
|
+
scheduleWatch(pi, id, latest.intervalMs ?? 30_000, getActiveSession);
|
|
224
|
+
} catch (error) {
|
|
225
|
+
const meta = readMeta(id);
|
|
226
|
+
if (meta && meta.status === "running") {
|
|
227
|
+
finalize(meta, { status: "failed", reason: error instanceof Error ? error.message : String(error) }, pi, getActiveSession);
|
|
228
|
+
}
|
|
229
|
+
} finally {
|
|
230
|
+
activePolls.delete(id);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function finalize(
|
|
235
|
+
meta: BackgroundTaskMeta,
|
|
236
|
+
terminal: TerminalResult,
|
|
237
|
+
pi: ExtensionAPI,
|
|
238
|
+
getActiveSession?: ActiveSessionProvider,
|
|
239
|
+
): void {
|
|
240
|
+
meta.status = terminal.status;
|
|
241
|
+
meta.endedAt = Date.now();
|
|
242
|
+
meta.result = {
|
|
243
|
+
reason: terminal.reason,
|
|
244
|
+
matchedCondition: terminal.matchedCondition,
|
|
245
|
+
exitCode: terminal.commandResult?.exitCode,
|
|
246
|
+
signal: terminal.commandResult?.signal,
|
|
247
|
+
};
|
|
248
|
+
if (terminal.commandResult) {
|
|
249
|
+
meta.lastExitCode = terminal.commandResult.exitCode;
|
|
250
|
+
meta.lastSignal = terminal.commandResult.signal;
|
|
251
|
+
meta.lastCheckedAt = terminal.commandResult.endedAt;
|
|
252
|
+
meta.lastState = extractLastState(terminal.commandResult);
|
|
253
|
+
}
|
|
254
|
+
writeMeta(meta);
|
|
255
|
+
clearWatchTimer(meta.id);
|
|
256
|
+
void notifyTerminal(pi, meta, getActiveSession);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
function scheduleProcessTimeout(
|
|
260
|
+
pi: ExtensionAPI,
|
|
261
|
+
id: string,
|
|
262
|
+
deadlineAt: number,
|
|
263
|
+
getActiveSession?: ActiveSessionProvider,
|
|
264
|
+
): void {
|
|
265
|
+
const delay = Math.max(0, deadlineAt - Date.now());
|
|
266
|
+
const timer = setTimeout(() => {
|
|
267
|
+
const meta = readMeta(id);
|
|
268
|
+
if (!meta || meta.status !== "running" || meta.kind !== "process") return;
|
|
269
|
+
if (meta.pid) {
|
|
270
|
+
try { stopProcessGroup(meta.pid, meta.pgid); } catch { /* ignore */ }
|
|
271
|
+
}
|
|
272
|
+
meta.status = "timed_out";
|
|
273
|
+
meta.endedAt = Date.now();
|
|
274
|
+
meta.result = { reason: "timeout" };
|
|
275
|
+
writeMeta(meta);
|
|
276
|
+
void notifyTerminal(pi, meta, getActiveSession);
|
|
277
|
+
}, delay);
|
|
278
|
+
timer.unref();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
async function notifyTerminal(
|
|
282
|
+
pi: ExtensionAPI,
|
|
283
|
+
meta: BackgroundTaskMeta,
|
|
284
|
+
getActiveSession?: ActiveSessionProvider,
|
|
285
|
+
): Promise<void> {
|
|
286
|
+
if (meta.callback === false || meta.callbackSentAt || meta.callbackSuppressedAt) return;
|
|
287
|
+
const latest = readMeta(meta.id) ?? meta;
|
|
288
|
+
if (latest.callback === false || latest.callbackSentAt || latest.callbackSuppressedAt) return;
|
|
289
|
+
const suppressionReason = getCallbackSuppressionReason(latest, getActiveSession?.());
|
|
290
|
+
if (suppressionReason) {
|
|
291
|
+
latest.callbackSuppressedAt = Date.now();
|
|
292
|
+
latest.callbackSuppressedReason = suppressionReason;
|
|
293
|
+
writeMeta(latest);
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
const label = latest.name ? `${latest.name} (${latest.id})` : latest.id;
|
|
297
|
+
try {
|
|
298
|
+
await pi.sendUserMessage(
|
|
299
|
+
`Background task ${label} reached terminal status ${latest.status}. Inspect the compact result with bg_task_status id=${latest.id}; call bg_task_log only if the status summary is insufficient.`,
|
|
300
|
+
{ deliverAs: "followUp" },
|
|
301
|
+
);
|
|
302
|
+
latest.callbackSentAt = Date.now();
|
|
303
|
+
writeMeta(latest);
|
|
304
|
+
} catch {
|
|
305
|
+
// Leave callbackSentAt unset so the originating session can attempt delivery once.
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
function getCallbackSuppressionReason(
|
|
310
|
+
meta: BackgroundTaskMeta,
|
|
311
|
+
activeSession: BackgroundTaskCallbackOrigin | undefined,
|
|
312
|
+
): string | undefined {
|
|
313
|
+
const origin = meta.callbackOrigin;
|
|
314
|
+
if (origin) {
|
|
315
|
+
if (!activeSession) return "active session identity is unavailable";
|
|
316
|
+
if (origin.cwd !== activeSession.cwd) return `origin cwd ${origin.cwd} does not match active cwd ${activeSession.cwd}`;
|
|
317
|
+
if (origin.sessionId && origin.sessionId !== activeSession.sessionId) {
|
|
318
|
+
return `origin session ${origin.sessionId} does not match active session ${activeSession.sessionId ?? "unknown"}`;
|
|
319
|
+
}
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (activeSession && meta.cwd !== activeSession.cwd) {
|
|
324
|
+
return `legacy task cwd ${meta.cwd} does not match active cwd ${activeSession.cwd}`;
|
|
325
|
+
}
|
|
326
|
+
return undefined;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
function commandSpecFromMeta(meta: BackgroundTaskMeta): CommandSpec {
|
|
330
|
+
return {
|
|
331
|
+
command: meta.command,
|
|
332
|
+
argv: meta.argv,
|
|
333
|
+
shell: meta.shell,
|
|
334
|
+
cwd: meta.cwd,
|
|
335
|
+
env: meta.env,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
function extractLastState(result: { stdout: string }): unknown {
|
|
340
|
+
try {
|
|
341
|
+
return JSON.parse(result.stdout);
|
|
342
|
+
} catch {
|
|
343
|
+
return result.stdout.slice(0, 4000);
|
|
344
|
+
}
|
|
345
|
+
}
|