openmausbot 0.1.72 → 0.1.74
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/assets/index-BRLHsQ5g.js +298 -0
- package/dist/assets/index-ByLQ7a3F.css +1 -0
- package/dist/assets/{index-Bpai86ua.js → index-CwoQdM0z.js} +1 -1
- package/dist/index.html +2 -2
- package/dist-server/container-mcp.js +9 -0
- package/dist-server/drivers/agents-proxy.js +2 -2
- package/dist-server/index.js +721 -484
- package/dist-server/mcp-gate.js +58 -16
- package/dist-server/openmausbot.js +236 -80
- package/dist-server/pair-cli.js +236 -80
- package/dist-server/server/auto-approve.js +30 -206
- package/dist-server/server/channel-queue.js +20 -2
- package/dist-server/server/config.js +36 -1
- package/dist-server/server/drivers/acp/core.js +56 -3
- package/dist-server/server/drivers/acp/grok.js +3 -1
- package/dist-server/server/drivers/agents-proxy.js +2 -2
- package/dist-server/server/drivers/antigravity-acp.js +5 -4
- package/dist-server/server/drivers/antigravity.js +9 -4
- package/dist-server/server/drivers/claude.js +54 -11
- package/dist-server/server/drivers/codex.js +86 -27
- package/dist-server/server/engine-install.js +9 -5
- package/dist-server/server/index.js +210 -231
- package/dist-server/server/message-db.js +70 -3
- package/dist-server/server/procs.js +82 -23
- package/dist-server/server/routines.js +67 -8
- package/dist-server/server/steer-queue.js +27 -7
- package/dist-server/server/team-manifest.js +1 -1
- package/dist-server/server/vps-computer.js +81 -12
- package/dist-server/server/webhooks.js +4 -1
- package/dist-server/server/workspace-backup.js +24 -9
- package/dist-server/shared/approval-mode.js +11 -10
- package/dist-server/vps-container-mcp.js +9 -0
- package/package.json +1 -1
- package/dist/assets/index-BFPYw6P-.js +0 -296
- package/dist/assets/index-D1odUdTi.css +0 -1
- package/dist-server/server/auto-review.js +0 -79
|
@@ -482,12 +482,15 @@ export async function createPermissionBroker(opts) {
|
|
|
482
482
|
continue;
|
|
483
483
|
}
|
|
484
484
|
const ask = { id: askId, kind, tool: msg.tool ?? "tool", input: msg.input ?? {}, at: Date.now() };
|
|
485
|
-
const finish = (behavior, message, source) => {
|
|
485
|
+
const finish = (behavior, message, source, always) => {
|
|
486
486
|
if (!pending.delete(askId))
|
|
487
487
|
return;
|
|
488
488
|
clearTimeout(timer);
|
|
489
489
|
try {
|
|
490
|
-
|
|
490
|
+
// `always` rides to the proxy, which hands the CLI's own suggested
|
|
491
|
+
// permission rules back as updatedPermissions: Claude remembers
|
|
492
|
+
// the allow for the session, the harness remembers nothing.
|
|
493
|
+
conn.write(JSON.stringify({ t: "answer", id: askId, behavior, message, ...(always ? { always: true } : {}) }) + "\n");
|
|
491
494
|
}
|
|
492
495
|
catch { }
|
|
493
496
|
opts.onResolve({ ...ask, behavior, source });
|
|
@@ -570,13 +573,13 @@ export async function createPermissionBroker(opts) {
|
|
|
570
573
|
}
|
|
571
574
|
};
|
|
572
575
|
return {
|
|
573
|
-
answer(askId, behavior, message) {
|
|
576
|
+
answer(askId, behavior, message, always) {
|
|
574
577
|
const p = pending.get(askId);
|
|
575
578
|
if (!p)
|
|
576
579
|
return false;
|
|
577
580
|
if (p.ask.kind === "question" ? behavior !== "answer" : behavior === "answer")
|
|
578
581
|
return false;
|
|
579
|
-
p.finish(behavior, message, "user");
|
|
582
|
+
p.finish(behavior, message, "user", always && behavior === "allow");
|
|
580
583
|
return true;
|
|
581
584
|
},
|
|
582
585
|
pause() {
|
|
@@ -752,6 +755,12 @@ export const ClaudeDriver = {
|
|
|
752
755
|
? configuredIdleMinimum
|
|
753
756
|
: 10_000;
|
|
754
757
|
const SESSION_IDLE_MS = Math.max(sessionIdleMinimum, Number(process.env.OMB_CLAUDE_SESSION_IDLE_MS) || 10 * 60_000);
|
|
758
|
+
const stopSession = (session) => {
|
|
759
|
+
void killCliTree(session.child).then((stopped) => {
|
|
760
|
+
if (stopped)
|
|
761
|
+
void session.finishClose?.();
|
|
762
|
+
});
|
|
763
|
+
};
|
|
755
764
|
const closeSession = (threadId, why) => {
|
|
756
765
|
const s = sessions.get(threadId);
|
|
757
766
|
if (!s || s.closing)
|
|
@@ -772,8 +781,7 @@ export const ClaudeDriver = {
|
|
|
772
781
|
}
|
|
773
782
|
catch { }
|
|
774
783
|
const kill = setTimeout(() => {
|
|
775
|
-
|
|
776
|
-
killCliTree(s.child);
|
|
784
|
+
stopSession(s);
|
|
777
785
|
}, 5_000);
|
|
778
786
|
kill.unref?.();
|
|
779
787
|
};
|
|
@@ -833,7 +841,8 @@ export const ClaudeDriver = {
|
|
|
833
841
|
const permissionMode = turn.approvalMode === undefined
|
|
834
842
|
? config.permissionMode
|
|
835
843
|
: turn.approvalMode === "full" ? "bypassPermissions"
|
|
836
|
-
: turn.approvalMode === "auto" ? "auto"
|
|
844
|
+
: turn.approvalMode === "auto" ? "auto"
|
|
845
|
+
: turn.approvalMode === "edits" ? "acceptEdits" : "default";
|
|
837
846
|
const controlsHost = turn.integrations?.localComputer?.scope === "local-computer";
|
|
838
847
|
if (controlsHost && permissionMode === "bypassPermissions" && turn.approvalMode !== "full") {
|
|
839
848
|
throw new Error("local computer control requires the interactive approval broker");
|
|
@@ -1049,7 +1058,7 @@ export const ClaudeDriver = {
|
|
|
1049
1058
|
live.turn = { turnId, settled: false, sawStreamDelta: false };
|
|
1050
1059
|
active.set(threadId, { stop: () => {
|
|
1051
1060
|
closeSession(threadId, "interrupted");
|
|
1052
|
-
|
|
1061
|
+
stopSession(live);
|
|
1053
1062
|
}, turnId, broker: live.broker });
|
|
1054
1063
|
emit({ ...base(threadId, turnId), type: "turn.started" });
|
|
1055
1064
|
const volatile = turn.systemVolatile ?? "";
|
|
@@ -1121,6 +1130,13 @@ export const ClaudeDriver = {
|
|
|
1121
1130
|
onAsk: (ask) => {
|
|
1122
1131
|
const eventTurnId = sessions.get(threadId)?.turn?.turnId ?? turnId;
|
|
1123
1132
|
askTools.set(ask.id, typeof ask.tool === "string" ? ask.tool : undefined);
|
|
1133
|
+
// Auto was requested: say whether the CLI's reviewer is actually
|
|
1134
|
+
// running, from init, so the harness can tell a classifier's
|
|
1135
|
+
// verdict from a Manual session asking about everything.
|
|
1136
|
+
const nativeMode = sessions.get(threadId)?.nativePermissionMode ?? null;
|
|
1137
|
+
const nativeReview = permissionMode === "auto" && nativeMode !== null
|
|
1138
|
+
? nativeMode === "auto" ? "active" : "inactive"
|
|
1139
|
+
: undefined;
|
|
1124
1140
|
emit({
|
|
1125
1141
|
...base(threadId, eventTurnId),
|
|
1126
1142
|
type: "request.opened",
|
|
@@ -1128,6 +1144,10 @@ export const ClaudeDriver = {
|
|
|
1128
1144
|
requestType: ask.kind,
|
|
1129
1145
|
tool: ask.tool,
|
|
1130
1146
|
summary: askSummary(ask),
|
|
1147
|
+
nativeReview,
|
|
1148
|
+
// the proxy hands Claude its own suggested rules on `always`;
|
|
1149
|
+
// host control stays one action at a time
|
|
1150
|
+
allowSession: ask.kind === "permission" && !(controlsHost && typeof ask.tool === "string" && ask.tool.startsWith("mcp__computer")) ? true : undefined,
|
|
1131
1151
|
approvalScope: typeof ask.tool === "string" && controlsHost && ask.tool.startsWith("mcp__computer")
|
|
1132
1152
|
? "local-computer"
|
|
1133
1153
|
: undefined,
|
|
@@ -1190,6 +1210,7 @@ export const ClaudeDriver = {
|
|
|
1190
1210
|
volatile: turn.systemVolatile ?? "",
|
|
1191
1211
|
sessionId: sessionId ?? newSessionId,
|
|
1192
1212
|
sawInit: false,
|
|
1213
|
+
nativePermissionMode: null,
|
|
1193
1214
|
turn: { turnId, settled: false, sawStreamDelta: false },
|
|
1194
1215
|
idleTimer: null,
|
|
1195
1216
|
closing: false,
|
|
@@ -1231,6 +1252,8 @@ export const ClaudeDriver = {
|
|
|
1231
1252
|
};
|
|
1232
1253
|
const currentTurnId = () => session.turn?.turnId ?? turnId;
|
|
1233
1254
|
const handleLine = (line) => {
|
|
1255
|
+
if (session.closing)
|
|
1256
|
+
return;
|
|
1234
1257
|
let o;
|
|
1235
1258
|
try {
|
|
1236
1259
|
o = JSON.parse(line);
|
|
@@ -1243,6 +1266,7 @@ export const ClaudeDriver = {
|
|
|
1243
1266
|
case "system":
|
|
1244
1267
|
if (o.subtype === "init") {
|
|
1245
1268
|
session.sawInit = true;
|
|
1269
|
+
session.nativePermissionMode = typeof o.permissionMode === "string" ? o.permissionMode : null;
|
|
1246
1270
|
if (typeof o.session_id === "string")
|
|
1247
1271
|
session.sessionId = o.session_id;
|
|
1248
1272
|
emit({ ...base(threadId, currentTurnId()), type: "session.started", sessionId: o.session_id, model: o.model });
|
|
@@ -1371,7 +1395,22 @@ export const ClaudeDriver = {
|
|
|
1371
1395
|
emit({ ...base(threadId, currentTurnId()), type: "runtime.error", ...describeSpawnFailure(e, config.cli) });
|
|
1372
1396
|
settle(false, "spawn_error");
|
|
1373
1397
|
});
|
|
1374
|
-
|
|
1398
|
+
let closeFinalized = false;
|
|
1399
|
+
const finalizeClose = async (code) => {
|
|
1400
|
+
if (closeFinalized)
|
|
1401
|
+
return;
|
|
1402
|
+
// The root can close while its MCP helpers are still running. Join
|
|
1403
|
+
// an in-flight stop (or reap its remaining group) before releasing
|
|
1404
|
+
// the turn so a replacement cannot overlap the old helpers.
|
|
1405
|
+
if (!(await killCliTree(child, 0))) {
|
|
1406
|
+
session.broker?.close();
|
|
1407
|
+
session.broker = undefined;
|
|
1408
|
+
emit({ ...base(threadId, currentTurnId()), type: "runtime.error", message: "Claude could not be confirmed stopped; its helper processes may still be running." });
|
|
1409
|
+
return;
|
|
1410
|
+
}
|
|
1411
|
+
if (closeFinalized)
|
|
1412
|
+
return;
|
|
1413
|
+
closeFinalized = true;
|
|
1375
1414
|
// a turn still running when the process died is a failed turn; a
|
|
1376
1415
|
// process that exited between turns (idle close, contract change)
|
|
1377
1416
|
// is just a session ending
|
|
@@ -1536,6 +1575,10 @@ export const ClaudeDriver = {
|
|
|
1536
1575
|
removePrivateTempDir(session.systemPromptPath);
|
|
1537
1576
|
if (sessions.get(threadId) === session)
|
|
1538
1577
|
sessions.delete(threadId);
|
|
1578
|
+
};
|
|
1579
|
+
child.on("close", (code) => {
|
|
1580
|
+
session.finishClose = () => finalizeClose(code);
|
|
1581
|
+
void session.finishClose();
|
|
1539
1582
|
});
|
|
1540
1583
|
const stop = () => {
|
|
1541
1584
|
// taskkill is asynchronous on Windows. Retire steering and approvals
|
|
@@ -1543,7 +1586,7 @@ export const ClaudeDriver = {
|
|
|
1543
1586
|
closeSession(threadId, "interrupted");
|
|
1544
1587
|
retry.cancelled = true;
|
|
1545
1588
|
retryAbort.abort();
|
|
1546
|
-
|
|
1589
|
+
stopSession(session);
|
|
1547
1590
|
};
|
|
1548
1591
|
active.set(threadId, { stop, turnId, broker });
|
|
1549
1592
|
emit({ ...base(threadId, turnId), type: "turn.started" });
|
|
@@ -1683,7 +1726,7 @@ export const ClaudeDriver = {
|
|
|
1683
1726
|
if (!broker)
|
|
1684
1727
|
return "unavailable";
|
|
1685
1728
|
const behavior = decision.behavior === "answer" ? "answer" : decision.behavior;
|
|
1686
|
-
if (!broker.answer(requestId, behavior, decision.message))
|
|
1729
|
+
if (!broker.answer(requestId, behavior, decision.message, decision.always))
|
|
1687
1730
|
return "unavailable";
|
|
1688
1731
|
return behavior === "allow" ? "allowed-once" : behavior === "answer" ? "answered" : "rejected";
|
|
1689
1732
|
},
|
|
@@ -181,6 +181,9 @@ function mcpAppApprovalForm(params) {
|
|
|
181
181
|
/** Codex persists these values on its native thread. Keep them explicit on
|
|
182
182
|
* start, resume, and every turn so switching modes cannot leave a more
|
|
183
183
|
* permissive sandbox/reviewer stuck to the next request. */
|
|
184
|
+
/** Ask and Edits both run Codex's workspace-write sandbox with the person as
|
|
185
|
+
* reviewer: Codex has no narrower "edits only" mode, so the selector never
|
|
186
|
+
* offers Edits for it (supportsApprovalMode) and a stray value asks. */
|
|
184
187
|
function namedApprovalParams(mode) {
|
|
185
188
|
if (mode === "full") {
|
|
186
189
|
return {
|
|
@@ -515,6 +518,10 @@ export const CodexDriver = {
|
|
|
515
518
|
stdio: ["pipe", "pipe", "pipe"],
|
|
516
519
|
});
|
|
517
520
|
let abandoned = false;
|
|
521
|
+
let codexThreadId = null;
|
|
522
|
+
let codexTurnId = null;
|
|
523
|
+
let startingNativeTurn = false;
|
|
524
|
+
const earlyNotifications = [];
|
|
518
525
|
const state = {
|
|
519
526
|
settled: false,
|
|
520
527
|
lastText: "",
|
|
@@ -553,6 +560,21 @@ export const CodexDriver = {
|
|
|
553
560
|
rpcPending.set(id, {
|
|
554
561
|
resolve: (v) => {
|
|
555
562
|
clearTimeout(timer);
|
|
563
|
+
if (method === "turn/start") {
|
|
564
|
+
if (typeof v?.turn?.id !== "string" || !v.turn.id) {
|
|
565
|
+
reject(new Error("Codex did not return a native turn id"));
|
|
566
|
+
return;
|
|
567
|
+
}
|
|
568
|
+
// Bind synchronously: a single stdout chunk can contain the
|
|
569
|
+
// response, streamed events, completion and a late request.
|
|
570
|
+
codexTurnId = v.turn.id;
|
|
571
|
+
startingNativeTurn = false;
|
|
572
|
+
for (const notification of earlyNotifications.splice(0)) {
|
|
573
|
+
if (state.settled)
|
|
574
|
+
break;
|
|
575
|
+
handleNotification(notification);
|
|
576
|
+
}
|
|
577
|
+
}
|
|
556
578
|
resolve(v);
|
|
557
579
|
},
|
|
558
580
|
reject: (e) => {
|
|
@@ -563,10 +585,18 @@ export const CodexDriver = {
|
|
|
563
585
|
send({ jsonrpc: "2.0", id, method, params });
|
|
564
586
|
});
|
|
565
587
|
let stopping;
|
|
566
|
-
const terminate = () => stopping ??= killCliTree(child)
|
|
567
|
-
|
|
588
|
+
const terminate = () => stopping ??= killCliTree(child).then((stopped) => {
|
|
589
|
+
if (!stopped)
|
|
590
|
+
stopping = undefined;
|
|
591
|
+
return stopped;
|
|
592
|
+
});
|
|
593
|
+
let completeStoppedTurn;
|
|
594
|
+
const stop = async () => {
|
|
568
595
|
stopRequested = true;
|
|
569
|
-
|
|
596
|
+
const stopped = await terminate();
|
|
597
|
+
if (stopped)
|
|
598
|
+
completeStoppedTurn?.();
|
|
599
|
+
return stopped;
|
|
570
600
|
};
|
|
571
601
|
const settle = async (ok, stopReason) => {
|
|
572
602
|
if (state.settled)
|
|
@@ -583,15 +613,9 @@ export const CodexDriver = {
|
|
|
583
613
|
active.delete(threadId);
|
|
584
614
|
emit({ ...base(threadId, turnId), type: "turn.completed", ok, stopReason, cost: null, ...(state.usage ? { usage: state.usage } : {}) });
|
|
585
615
|
};
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
}
|
|
589
|
-
else {
|
|
616
|
+
completeStoppedTurn = complete;
|
|
617
|
+
if (!(await stop())) {
|
|
590
618
|
emit({ ...base(threadId, turnId), type: "runtime.error", message: "codex did not shut down after termination was requested" });
|
|
591
|
-
if (child.exitCode !== null || child.signalCode !== null)
|
|
592
|
-
complete();
|
|
593
|
-
else
|
|
594
|
-
child.once("close", complete);
|
|
595
619
|
}
|
|
596
620
|
};
|
|
597
621
|
// server→client approval request → canonical request.opened
|
|
@@ -710,6 +734,33 @@ export const CodexDriver = {
|
|
|
710
734
|
};
|
|
711
735
|
const handleNotification = (msg) => {
|
|
712
736
|
const p = msg.params ?? {};
|
|
737
|
+
// An app-server also emits notifications for native helper threads.
|
|
738
|
+
// Only this request's parent may write its transcript/usage or settle
|
|
739
|
+
// its run. Requests still use the approval broker above, including
|
|
740
|
+
// helper requests; ignoring child *notifications* must not grant tools.
|
|
741
|
+
const connectionError = msg.method === "error" &&
|
|
742
|
+
!("threadId" in p) && !("turnId" in p);
|
|
743
|
+
if (!connectionError) {
|
|
744
|
+
if (!codexThreadId || p.threadId !== codexThreadId)
|
|
745
|
+
return;
|
|
746
|
+
if (!codexTurnId) {
|
|
747
|
+
// Some servers stream before acknowledging turn/start. Retain a
|
|
748
|
+
// bounded prefix, then filter against the authoritative response.
|
|
749
|
+
if (startingNativeTurn) {
|
|
750
|
+
if (earlyNotifications.length >= 1024) {
|
|
751
|
+
void settle(false, "too_many_events_before_turn_start");
|
|
752
|
+
}
|
|
753
|
+
else {
|
|
754
|
+
earlyNotifications.push(msg);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
return;
|
|
758
|
+
}
|
|
759
|
+
const eventTurnId = msg.method === "turn/started" || msg.method === "turn/completed"
|
|
760
|
+
? p.turn?.id : p.turnId;
|
|
761
|
+
if (eventTurnId !== codexTurnId)
|
|
762
|
+
return;
|
|
763
|
+
}
|
|
713
764
|
switch (msg.method) {
|
|
714
765
|
// token-level chat text; the item/completed frame follows with the
|
|
715
766
|
// whole message, so its delta is only a fallback when none streamed
|
|
@@ -897,6 +948,12 @@ export const CodexDriver = {
|
|
|
897
948
|
child.on("close", (code) => {
|
|
898
949
|
if (abandoned)
|
|
899
950
|
return;
|
|
951
|
+
if (state.settled) {
|
|
952
|
+
// Root exit alone cannot release a turn after an uncertain stop.
|
|
953
|
+
// Recheck its group; an explicit later Stop can also retry this.
|
|
954
|
+
void stop();
|
|
955
|
+
return;
|
|
956
|
+
}
|
|
900
957
|
if (!state.settled) {
|
|
901
958
|
emit({
|
|
902
959
|
...base(threadId, turnId),
|
|
@@ -959,7 +1016,6 @@ export const CodexDriver = {
|
|
|
959
1016
|
// on start AND resume so Codex owns their lifetime through compaction.
|
|
960
1017
|
// Removed bot rules are cleared without dropping native configured rules.
|
|
961
1018
|
const cursor = typeof turn.resumeCursor === "string" ? turn.resumeCursor : null;
|
|
962
|
-
let codexThreadId = null;
|
|
963
1019
|
let startedModel = null;
|
|
964
1020
|
if (cursor) {
|
|
965
1021
|
try {
|
|
@@ -1020,21 +1076,24 @@ export const CodexDriver = {
|
|
|
1020
1076
|
...(promptText ? [{ type: "text", text: promptText }] : []),
|
|
1021
1077
|
...(turn.images ?? []).map((image) => ({ type: "localImage", path: image.path })),
|
|
1022
1078
|
];
|
|
1023
|
-
const startTurn = () =>
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1079
|
+
const startTurn = () => {
|
|
1080
|
+
startingNativeTurn = true;
|
|
1081
|
+
return request("turn/start", {
|
|
1082
|
+
threadId: codexThreadId,
|
|
1083
|
+
input: turnInput,
|
|
1084
|
+
...approvalParams.turn,
|
|
1085
|
+
// Spread, not `effort: turn.effort ?? null`. Probed against
|
|
1086
|
+
// codex-cli 0.146.0: null is indistinguishable from an absent key
|
|
1087
|
+
// — both leave the thread's current effort alone, emitting no
|
|
1088
|
+
// thread/settings/updated, and thread/resume reads the old value
|
|
1089
|
+
// back. The app-server offers no way to clear a level either:
|
|
1090
|
+
// "" is rejected outright and thread/start takes no effort at
|
|
1091
|
+
// all. So a thread keeps the last level it was sent until it is
|
|
1092
|
+
// sent another, and choosing Default lands on the bot's next new
|
|
1093
|
+
// thread rather than the current one.
|
|
1094
|
+
...(turn.effort ? { effort: turn.effort } : {}),
|
|
1095
|
+
});
|
|
1096
|
+
};
|
|
1038
1097
|
try {
|
|
1039
1098
|
await startTurn();
|
|
1040
1099
|
}
|
|
@@ -112,10 +112,16 @@ function runNpm(args, env, cwd, timeoutMs) {
|
|
|
112
112
|
let timedOut = false;
|
|
113
113
|
const timer = setTimeout(() => {
|
|
114
114
|
timedOut = true;
|
|
115
|
-
void killCliTree(child)
|
|
115
|
+
void killCliTree(child).then((stopped) => {
|
|
116
|
+
rejectRun(new Error(stopped
|
|
117
|
+
? "The install took too long and was stopped. Check the server's network connection and try again."
|
|
118
|
+
: "The install took too long, but npm could not be confirmed stopped. Ask the server administrator to stop the install process before trying again."));
|
|
119
|
+
});
|
|
116
120
|
}, timeoutMs);
|
|
117
121
|
timer.unref();
|
|
118
122
|
child.once("error", (error) => {
|
|
123
|
+
if (timedOut)
|
|
124
|
+
return; // A failed kill is not a failed npm launch.
|
|
119
125
|
clearTimeout(timer);
|
|
120
126
|
rejectRun(new Error(error.code === "ENOENT"
|
|
121
127
|
? "npm is not installed on this server. Install Node.js with npm for the user running OpenMausBot, then try again."
|
|
@@ -123,10 +129,8 @@ function runNpm(args, env, cwd, timeoutMs) {
|
|
|
123
129
|
});
|
|
124
130
|
child.once("close", (code) => {
|
|
125
131
|
clearTimeout(timer);
|
|
126
|
-
if (timedOut)
|
|
127
|
-
|
|
128
|
-
return;
|
|
129
|
-
}
|
|
132
|
+
if (timedOut)
|
|
133
|
+
return; // The whole group must stop, not just npm's root.
|
|
130
134
|
resolveRun({ code, output });
|
|
131
135
|
});
|
|
132
136
|
});
|