@akira-tl/forgerelay 0.4.6 → 0.5.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/CHANGELOG.md +24 -0
- package/README.md +8 -7
- package/dist/activity/audit-store.js +209 -0
- package/dist/db/migrations.js +40 -0
- package/dist/db/schema.js +25 -1
- package/dist/hooks.js +43 -14
- package/dist/mcp/server-instructions.js +1 -1
- package/dist/process-sessions.js +119 -14
- package/dist/server.js +165 -103
- package/docs/chatgpt-coding-workflow.md +10 -5
- package/docs/configuration.md +24 -15
- package/docs/roadmap.md +4 -1
- package/docs/versioning.md +9 -7
- package/package.json +3 -3
- package/scripts/debug/accept.mjs +1 -0
- package/scripts/release-proof.mjs +139 -0
- package/scripts/release-proof.test.mjs +104 -0
- package/scripts/release-version.mjs +1 -1
package/dist/process-sessions.js
CHANGED
|
@@ -6,21 +6,33 @@ const DEFAULT_POLL_YIELD_MS = 5_000;
|
|
|
6
6
|
const MAX_START_YIELD_MS = 300_000;
|
|
7
7
|
const MAX_COMMAND_YIELD_MS = 300_000;
|
|
8
8
|
const MAX_POLL_YIELD_MS = 300_000;
|
|
9
|
+
const MAX_EXECUTION_TIMEOUT_MS = 24 * 60 * 60 * 1_000;
|
|
9
10
|
const DEFAULT_MAX_OUTPUT_TOKENS = 10_000;
|
|
10
11
|
const DEFAULT_BUFFER_CHARACTERS = 256_000;
|
|
11
12
|
const DEFAULT_MAX_ACTIVE_PROCESSES = 64;
|
|
12
13
|
const DEFAULT_MAX_COMPLETED_PROCESSES = 128;
|
|
13
14
|
const COMPLETED_PROCESS_TTL_MS = 5 * 60 * 1_000;
|
|
15
|
+
const COMPACT_COMPLETION_TTL_MS = 24 * 60 * 60 * 1_000;
|
|
16
|
+
const COMPACT_COMPLETION_CHARACTERS = 16 * 1024;
|
|
17
|
+
const COMPACT_COMPLETION_OUTPUT_TOKENS = COMPACT_COMPLETION_CHARACTERS / 4;
|
|
14
18
|
const DEFAULT_COLUMNS = 80;
|
|
15
19
|
const DEFAULT_ROWS = 24;
|
|
16
20
|
function boundedInteger(value, fallback, maximum) {
|
|
17
21
|
if (value === undefined)
|
|
18
|
-
return fallback;
|
|
22
|
+
return Math.min(fallback, maximum);
|
|
19
23
|
if (!Number.isFinite(value) || value < 0) {
|
|
20
24
|
throw new Error("Duration and output limits must be non-negative.");
|
|
21
25
|
}
|
|
22
26
|
return Math.min(Math.floor(value), maximum);
|
|
23
27
|
}
|
|
28
|
+
function optionalExecutionTimeout(value) {
|
|
29
|
+
if (value === undefined)
|
|
30
|
+
return undefined;
|
|
31
|
+
if (!Number.isInteger(value) || value < 1 || value > MAX_EXECUTION_TIMEOUT_MS) {
|
|
32
|
+
throw new Error(`Execution timeout must be an integer between 1 and ${MAX_EXECUTION_TIMEOUT_MS}ms.`);
|
|
33
|
+
}
|
|
34
|
+
return value;
|
|
35
|
+
}
|
|
24
36
|
function terminalSize(value, fallback) {
|
|
25
37
|
if (value === undefined)
|
|
26
38
|
return fallback;
|
|
@@ -215,9 +227,11 @@ export class ProcessManager {
|
|
|
215
227
|
this.monotonicNow = options.monotonicNow ?? (() => performance.now());
|
|
216
228
|
}
|
|
217
229
|
async start(input) {
|
|
230
|
+
input.signal?.throwIfAborted();
|
|
218
231
|
if (this.stats().running >= this.maxActiveProcesses) {
|
|
219
232
|
throw new Error(`Active process limit reached (${this.maxActiveProcesses}). Poll, interrupt, or wait for an existing process before starting another.`);
|
|
220
233
|
}
|
|
234
|
+
const executionTimeoutMs = optionalExecutionTimeout(input.timeoutMs);
|
|
221
235
|
const processEntry = this.createProcess(input);
|
|
222
236
|
this.processes.set(processEntry.id, processEntry);
|
|
223
237
|
try {
|
|
@@ -230,8 +244,19 @@ export class ProcessManager {
|
|
|
230
244
|
this.processes.delete(processEntry.id);
|
|
231
245
|
throw error;
|
|
232
246
|
}
|
|
247
|
+
this.armExecutionTimeout(processEntry, executionTimeoutMs);
|
|
233
248
|
const yieldTimeMs = boundedInteger(input.yieldTimeMs, DEFAULT_EXEC_YIELD_MS, this.maxStartYieldMs);
|
|
234
|
-
|
|
249
|
+
try {
|
|
250
|
+
await this.waitForExit(processEntry, yieldTimeMs, input.signal);
|
|
251
|
+
input.signal?.throwIfAborted();
|
|
252
|
+
}
|
|
253
|
+
catch (error) {
|
|
254
|
+
if (input.signal?.aborted) {
|
|
255
|
+
processEntry.discardOnFinish = true;
|
|
256
|
+
this.stopProcess(processEntry, "SIGTERM");
|
|
257
|
+
}
|
|
258
|
+
throw error;
|
|
259
|
+
}
|
|
235
260
|
if (processEntry.running)
|
|
236
261
|
processEntry.background = true;
|
|
237
262
|
const snapshot = this.consume(processEntry, input.maxOutputTokens);
|
|
@@ -263,7 +288,7 @@ export class ProcessManager {
|
|
|
263
288
|
const fallback = interactionRequested ? DEFAULT_INTERACTIVE_YIELD_MS : DEFAULT_POLL_YIELD_MS;
|
|
264
289
|
const maximum = interactionRequested ? MAX_COMMAND_YIELD_MS : MAX_POLL_YIELD_MS;
|
|
265
290
|
const yieldTimeMs = boundedInteger(input.yieldTimeMs, fallback, maximum);
|
|
266
|
-
await this.waitForExit(processEntry, yieldTimeMs);
|
|
291
|
+
await this.waitForExit(processEntry, yieldTimeMs, input.signal);
|
|
267
292
|
}
|
|
268
293
|
const snapshot = this.consume(processEntry, input.maxOutputTokens);
|
|
269
294
|
if (!processEntry.running)
|
|
@@ -271,7 +296,9 @@ export class ProcessManager {
|
|
|
271
296
|
return snapshot;
|
|
272
297
|
}
|
|
273
298
|
activeWorkspaceIds() {
|
|
274
|
-
return new Set([...this.processes.values()]
|
|
299
|
+
return new Set([...this.processes.values()]
|
|
300
|
+
.filter((processEntry) => processEntry.running)
|
|
301
|
+
.map((processEntry) => processEntry.workspaceId));
|
|
275
302
|
}
|
|
276
303
|
stats() {
|
|
277
304
|
let running = 0;
|
|
@@ -311,10 +338,17 @@ export class ProcessManager {
|
|
|
311
338
|
if (processEntry.running)
|
|
312
339
|
processEntry.process?.kill("SIGTERM");
|
|
313
340
|
}
|
|
341
|
+
discardUndelivered(workspaceId, processId) {
|
|
342
|
+
const processEntry = this.getOwnedProcess(workspaceId, processId);
|
|
343
|
+
processEntry.discardOnFinish = true;
|
|
344
|
+
if (processEntry.running)
|
|
345
|
+
this.stopProcess(processEntry, "SIGTERM");
|
|
346
|
+
else
|
|
347
|
+
this.removeProcess(processEntry.id);
|
|
348
|
+
}
|
|
314
349
|
shutdown() {
|
|
315
350
|
for (const processEntry of this.processes.values()) {
|
|
316
|
-
|
|
317
|
-
clearTimeout(processEntry.cleanupTimer);
|
|
351
|
+
this.clearProcessTimers(processEntry);
|
|
318
352
|
if (processEntry.running)
|
|
319
353
|
processEntry.process?.kill("SIGTERM");
|
|
320
354
|
}
|
|
@@ -322,19 +356,32 @@ export class ProcessManager {
|
|
|
322
356
|
this.completedByWorkspace.clear();
|
|
323
357
|
this.completedProcessIds.length = 0;
|
|
324
358
|
}
|
|
325
|
-
async waitForExit(processEntry, yieldTimeMs) {
|
|
359
|
+
async waitForExit(processEntry, yieldTimeMs, signal) {
|
|
360
|
+
signal?.throwIfAborted();
|
|
326
361
|
let timer;
|
|
362
|
+
let abortListener;
|
|
327
363
|
try {
|
|
328
|
-
|
|
364
|
+
const waits = [
|
|
329
365
|
processEntry.exitPromise,
|
|
330
366
|
new Promise((resolve) => {
|
|
331
367
|
timer = setTimeout(resolve, yieldTimeMs);
|
|
332
368
|
}),
|
|
333
|
-
]
|
|
369
|
+
];
|
|
370
|
+
if (signal) {
|
|
371
|
+
waits.push(new Promise((_resolve, reject) => {
|
|
372
|
+
abortListener = () => reject(signal.reason instanceof Error
|
|
373
|
+
? signal.reason
|
|
374
|
+
: Object.assign(new Error("Process wait cancelled by Host."), { name: "AbortError" }));
|
|
375
|
+
signal.addEventListener("abort", abortListener, { once: true });
|
|
376
|
+
}));
|
|
377
|
+
}
|
|
378
|
+
await Promise.race(waits);
|
|
334
379
|
}
|
|
335
380
|
finally {
|
|
336
381
|
if (timer)
|
|
337
382
|
clearTimeout(timer);
|
|
383
|
+
if (signal && abortListener)
|
|
384
|
+
signal.removeEventListener("abort", abortListener);
|
|
338
385
|
}
|
|
339
386
|
}
|
|
340
387
|
createProcess(input) {
|
|
@@ -351,7 +398,10 @@ export class ProcessManager {
|
|
|
351
398
|
rows: terminalSize(input.rows, DEFAULT_ROWS),
|
|
352
399
|
buffer: new HeadTailBuffer(this.maxBufferCharacters),
|
|
353
400
|
running: true,
|
|
401
|
+
timedOut: false,
|
|
402
|
+
outputWasTruncated: false,
|
|
354
403
|
background: false,
|
|
404
|
+
discardOnFinish: false,
|
|
355
405
|
exitPromise,
|
|
356
406
|
resolveExit,
|
|
357
407
|
};
|
|
@@ -423,9 +473,18 @@ export class ProcessManager {
|
|
|
423
473
|
processEntry.running = false;
|
|
424
474
|
processEntry.exitCode = exitCode;
|
|
425
475
|
processEntry.signal = signal;
|
|
476
|
+
processEntry.finishedAtMonotonic = this.monotonicNow();
|
|
426
477
|
processEntry.process = undefined;
|
|
478
|
+
if (processEntry.executionTimeoutTimer)
|
|
479
|
+
clearTimeout(processEntry.executionTimeoutTimer);
|
|
480
|
+
if (processEntry.forceKillTimer)
|
|
481
|
+
clearTimeout(processEntry.forceKillTimer);
|
|
427
482
|
processEntry.resolveExit();
|
|
428
|
-
|
|
483
|
+
if (processEntry.discardOnFinish) {
|
|
484
|
+
this.removeProcess(processEntry.id);
|
|
485
|
+
return;
|
|
486
|
+
}
|
|
487
|
+
processEntry.cleanupTimer = setTimeout(() => this.compactCompletedProcess(processEntry), this.completedProcessTtlMs);
|
|
429
488
|
processEntry.cleanupTimer.unref();
|
|
430
489
|
if (processEntry.background) {
|
|
431
490
|
const completed = this.completedByWorkspace.get(processEntry.workspaceId) ?? [];
|
|
@@ -442,6 +501,17 @@ export class ProcessManager {
|
|
|
442
501
|
}
|
|
443
502
|
}
|
|
444
503
|
}
|
|
504
|
+
compactCompletedProcess(processEntry) {
|
|
505
|
+
if (processEntry.running || !this.processes.has(processEntry.id))
|
|
506
|
+
return;
|
|
507
|
+
const compacted = this.consume(processEntry, COMPACT_COMPLETION_OUTPUT_TOKENS);
|
|
508
|
+
processEntry.buffer = new HeadTailBuffer(COMPACT_COMPLETION_CHARACTERS);
|
|
509
|
+
processEntry.buffer.append(compacted.output);
|
|
510
|
+
processEntry.outputWasTruncated = compacted.outputTruncated;
|
|
511
|
+
const remainingMs = Math.max(1, COMPACT_COMPLETION_TTL_MS - this.completedProcessTtlMs);
|
|
512
|
+
processEntry.cleanupTimer = setTimeout(() => this.removeProcess(processEntry.id), remainingMs);
|
|
513
|
+
processEntry.cleanupTimer.unref();
|
|
514
|
+
}
|
|
445
515
|
append(processEntry, output) {
|
|
446
516
|
processEntry.buffer.append(output);
|
|
447
517
|
}
|
|
@@ -449,18 +519,53 @@ export class ProcessManager {
|
|
|
449
519
|
const limit = boundedInteger(maxOutputTokens, DEFAULT_MAX_OUTPUT_TOKENS, 100_000);
|
|
450
520
|
const maxCharacters = Math.max(256, limit * 4);
|
|
451
521
|
const buffered = processEntry.buffer.drain(maxCharacters);
|
|
522
|
+
if (buffered.truncated)
|
|
523
|
+
processEntry.outputWasTruncated = true;
|
|
452
524
|
const processId = processEntry.running ? processEntry.id : undefined;
|
|
525
|
+
const endedAt = processEntry.finishedAtMonotonic ?? this.monotonicNow();
|
|
453
526
|
return {
|
|
454
527
|
processId,
|
|
455
528
|
sessionId: processId,
|
|
456
529
|
output: buffered.output,
|
|
457
|
-
outputTruncated:
|
|
530
|
+
outputTruncated: processEntry.outputWasTruncated,
|
|
458
531
|
running: processEntry.running,
|
|
459
532
|
exitCode: processEntry.exitCode,
|
|
460
533
|
signal: processEntry.signal,
|
|
461
|
-
|
|
534
|
+
timedOut: processEntry.timedOut,
|
|
535
|
+
wallTimeMs: Math.max(0, Math.round(endedAt - processEntry.startedAtMonotonic)),
|
|
462
536
|
};
|
|
463
537
|
}
|
|
538
|
+
armExecutionTimeout(processEntry, timeoutMs) {
|
|
539
|
+
if (timeoutMs === undefined)
|
|
540
|
+
return;
|
|
541
|
+
processEntry.executionTimeoutTimer = setTimeout(() => {
|
|
542
|
+
if (!processEntry.running)
|
|
543
|
+
return;
|
|
544
|
+
processEntry.timedOut = true;
|
|
545
|
+
this.stopProcess(processEntry, "SIGTERM");
|
|
546
|
+
}, timeoutMs);
|
|
547
|
+
processEntry.executionTimeoutTimer.unref();
|
|
548
|
+
}
|
|
549
|
+
stopProcess(processEntry, signal) {
|
|
550
|
+
if (!processEntry.running)
|
|
551
|
+
return;
|
|
552
|
+
processEntry.process?.kill(signal);
|
|
553
|
+
if (processEntry.forceKillTimer)
|
|
554
|
+
clearTimeout(processEntry.forceKillTimer);
|
|
555
|
+
processEntry.forceKillTimer = setTimeout(() => {
|
|
556
|
+
if (processEntry.running)
|
|
557
|
+
processEntry.process?.kill("SIGKILL");
|
|
558
|
+
}, 500);
|
|
559
|
+
processEntry.forceKillTimer.unref();
|
|
560
|
+
}
|
|
561
|
+
clearProcessTimers(processEntry) {
|
|
562
|
+
if (processEntry.cleanupTimer)
|
|
563
|
+
clearTimeout(processEntry.cleanupTimer);
|
|
564
|
+
if (processEntry.executionTimeoutTimer)
|
|
565
|
+
clearTimeout(processEntry.executionTimeoutTimer);
|
|
566
|
+
if (processEntry.forceKillTimer)
|
|
567
|
+
clearTimeout(processEntry.forceKillTimer);
|
|
568
|
+
}
|
|
464
569
|
getOwnedProcess(workspaceId, processId) {
|
|
465
570
|
const processEntry = this.processes.get(processId);
|
|
466
571
|
if (!processEntry)
|
|
@@ -472,8 +577,8 @@ export class ProcessManager {
|
|
|
472
577
|
}
|
|
473
578
|
removeProcess(processId) {
|
|
474
579
|
const processEntry = this.processes.get(processId);
|
|
475
|
-
if (processEntry
|
|
476
|
-
|
|
580
|
+
if (processEntry)
|
|
581
|
+
this.clearProcessTimers(processEntry);
|
|
477
582
|
this.processes.delete(processId);
|
|
478
583
|
const completedIndex = this.completedProcessIds.indexOf(processId);
|
|
479
584
|
if (completedIndex >= 0)
|