openmausbot 0.1.82 → 0.1.83
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-BLLsDK2F.js → index-DSvqfBOf.js} +71 -71
- package/dist/assets/{index-5HVwp5m2.js → index-nqTFZTxg.js} +1 -1
- package/dist/index.html +1 -1
- package/dist-server/container-mcp.js +20 -4
- package/dist-server/drivers/agents-proxy.js +4 -4
- package/dist-server/index.js +1063 -493
- package/dist-server/mcp-gate.js +2 -1
- package/dist-server/openmausbot.js +187 -55
- package/dist-server/pair-cli.js +187 -55
- package/dist-server/server/auto-approve.js +15 -1
- package/dist-server/server/auto-vm-claims.js +22 -0
- package/dist-server/server/browser-bundle-release.js +8 -7
- package/dist-server/server/browser-engine.js +1 -1
- package/dist-server/server/channel-queue.js +58 -0
- package/dist-server/server/chief-of-staff.js +2 -2
- package/dist-server/server/config.js +33 -2
- package/dist-server/server/contracts.js +0 -7
- package/dist-server/server/drivers/acp/gemini.js +11 -1
- package/dist-server/server/drivers/acp/qwen.js +16 -1
- package/dist-server/server/drivers/agents-proxy.js +4 -4
- package/dist-server/server/drivers/claude.js +4 -3
- package/dist-server/server/drivers/codex.js +66 -1
- package/dist-server/server/drivers/pi.js +2 -1
- package/dist-server/server/env-path.js +6 -1
- package/dist-server/server/index.js +534 -84
- package/dist-server/server/local-routing.js +10 -4
- package/dist-server/server/peer-roster.js +37 -1
- package/dist-server/server/recent-work.js +4 -1
- package/dist-server/server/redact.js +5 -48
- package/dist-server/server/room-handoffs.js +79 -16
- package/dist-server/server/skill-fetch.js +1 -1
- package/dist-server/server/steer-queue.js +29 -0
- package/dist-server/server/store.js +26 -3
- package/dist-server/server/thread-retention.js +50 -0
- package/dist-server/server/turn-context.js +7 -0
- package/dist-server/server/turn-resources.js +6 -0
- package/dist-server/shared/approval-mode.js +8 -5
- package/dist-server/shared/inspector.js +1 -0
- package/dist-server/shared/json.js +1 -0
- package/dist-server/shared/notification.js +1 -0
- package/dist-server/shared/redact.js +61 -0
- package/dist-server/shared/routines.js +1 -0
- package/dist-server/shared/runtime-events.js +1 -0
- package/dist-server/shared/webhooks.js +1 -0
- package/dist-server/shared/wire.js +8 -0
- package/dist-server/vps-container-mcp.js +20 -4
- package/package.json +1 -1
|
@@ -7,7 +7,8 @@ import { join } from "node:path";
|
|
|
7
7
|
import { z } from "zod";
|
|
8
8
|
import { normalizeImageGenerationUrl } from "../shared/image-generation.js";
|
|
9
9
|
import { writeFileAtomic } from "./atomic.js";
|
|
10
|
-
import { EFFORT_LEVELS
|
|
10
|
+
import { EFFORT_LEVELS } from "../shared/wire.js";
|
|
11
|
+
import { isModelVariant } from "./contracts.js";
|
|
11
12
|
import { isRemoteMcpServer, parseStoredMcpServer } from "./mcp-registry.js";
|
|
12
13
|
import { parseJson, schemaIssue } from "./schema.js";
|
|
13
14
|
const optionalText = z.string().optional();
|
|
@@ -17,6 +18,9 @@ const BROWSER_PROFILE_ID = /^[a-z0-9_-]{1,40}$/;
|
|
|
17
18
|
export const DEFAULT_ROOM_TURN_TIMEOUT_MINUTES = 5;
|
|
18
19
|
export const MIN_ROOM_TURN_TIMEOUT_MINUTES = 1;
|
|
19
20
|
export const MAX_ROOM_TURN_TIMEOUT_MINUTES = 1_440;
|
|
21
|
+
export const DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES = 30;
|
|
22
|
+
export const DEFAULT_ROOM_HANDOFF_MIN_RUNWAY_MINUTES = 10;
|
|
23
|
+
export const DEFAULT_ROOM_HANDOFF_HARD_CAP_MINUTES = 240;
|
|
20
24
|
export const DEFAULT_MAX_CONCURRENT_BOT_THREADS = 3;
|
|
21
25
|
export const MAX_CONCURRENT_BOT_THREADS = 10;
|
|
22
26
|
/** Bounds for threads.eventLogMaxBytes: the floor keeps the kept tail large
|
|
@@ -57,7 +61,15 @@ const roomConfigSchema = z.object({
|
|
|
57
61
|
.int()
|
|
58
62
|
.min(MIN_ROOM_TURN_TIMEOUT_MINUTES)
|
|
59
63
|
.max(MAX_ROOM_TURN_TIMEOUT_MINUTES),
|
|
60
|
-
|
|
64
|
+
/** Room handoff tree lifetime. Active execution pauses this clock; the
|
|
65
|
+
* hard cap is wall-clock and bounds trees that never stop executing. */
|
|
66
|
+
handoffLifetimeMinutes: z.number().int().min(1).max(MAX_ROOM_TURN_TIMEOUT_MINUTES).optional(),
|
|
67
|
+
handoffMinRunwayMinutes: z.number().int().min(1).max(MAX_ROOM_TURN_TIMEOUT_MINUTES).optional(),
|
|
68
|
+
handoffHardCapMinutes: z.number().int().min(1).max(7 * MAX_ROOM_TURN_TIMEOUT_MINUTES).optional(),
|
|
69
|
+
}).refine((rooms) => (rooms.handoffMinRunwayMinutes ?? DEFAULT_ROOM_HANDOFF_MIN_RUNWAY_MINUTES) <=
|
|
70
|
+
(rooms.handoffLifetimeMinutes ?? DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES) &&
|
|
71
|
+
(rooms.handoffLifetimeMinutes ?? DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES) <=
|
|
72
|
+
(rooms.handoffHardCapMinutes ?? DEFAULT_ROOM_HANDOFF_HARD_CAP_MINUTES), { message: "rooms handoff bounds must satisfy handoffMinRunwayMinutes <= handoffLifetimeMinutes <= handoffHardCapMinutes" });
|
|
61
73
|
const localVmConfigSchema = z.object({
|
|
62
74
|
mode: z.enum(["shared", "per-bot"]).optional(),
|
|
63
75
|
maxInstances: z
|
|
@@ -349,6 +361,9 @@ const appConfigSchema = z.object({
|
|
|
349
361
|
/** Cap each per-thread events/ and native/ NDJSON log at this many
|
|
350
362
|
* bytes; absent (the default) keeps today's unbounded growth (#1280). */
|
|
351
363
|
eventLogMaxBytes: z.number().int().min(MIN_THREAD_EVENT_LOG_BYTES).max(MAX_THREAD_EVENT_LOG_BYTES).optional(),
|
|
364
|
+
/** Days a closed or archived thread's event logs survive (#1280).
|
|
365
|
+
* Absent keeps them forever. */
|
|
366
|
+
eventLogRetentionDays: z.number().int().min(1).max(3650).optional(),
|
|
352
367
|
}).strict().optional(),
|
|
353
368
|
localVm: localVmConfigSchema.optional(),
|
|
354
369
|
features: featureConfigSchema.optional(),
|
|
@@ -447,6 +462,16 @@ export function vpsSshAlias(cfg) {
|
|
|
447
462
|
export function roomTurnTimeoutMinutes(cfg) {
|
|
448
463
|
return cfg.rooms?.turnTimeoutMinutes ?? DEFAULT_ROOM_TURN_TIMEOUT_MINUTES;
|
|
449
464
|
}
|
|
465
|
+
/** Room handoff tree budgets in milliseconds. The tree lifetime pauses
|
|
466
|
+
* while a node is actively executing; the hard cap is wall-clock and bounds
|
|
467
|
+
* trees that never stop. Read when the server starts. */
|
|
468
|
+
export function roomHandoffLimits(cfg) {
|
|
469
|
+
return {
|
|
470
|
+
lifetimeMs: (cfg.rooms?.handoffLifetimeMinutes ?? DEFAULT_ROOM_HANDOFF_LIFETIME_MINUTES) * 60_000,
|
|
471
|
+
minRunwayMs: (cfg.rooms?.handoffMinRunwayMinutes ?? DEFAULT_ROOM_HANDOFF_MIN_RUNWAY_MINUTES) * 60_000,
|
|
472
|
+
hardCapMs: (cfg.rooms?.handoffHardCapMinutes ?? DEFAULT_ROOM_HANDOFF_HARD_CAP_MINUTES) * 60_000,
|
|
473
|
+
};
|
|
474
|
+
}
|
|
450
475
|
export function maxConcurrentBotThreads(cfg) {
|
|
451
476
|
return cfg.threads?.maxConcurrentPerBot ?? DEFAULT_MAX_CONCURRENT_BOT_THREADS;
|
|
452
477
|
}
|
|
@@ -456,6 +481,12 @@ export function threadEventLogMaxBytes(cfg) {
|
|
|
456
481
|
const cap = cfg.threads?.eventLogMaxBytes;
|
|
457
482
|
return typeof cap === "number" && Number.isFinite(cap) && cap > 0 ? cap : null;
|
|
458
483
|
}
|
|
484
|
+
/** Days a closed or archived bot thread's event logs survive before the
|
|
485
|
+
* retention sweep removes them (#1280). Null — the default — keeps them
|
|
486
|
+
* forever. */
|
|
487
|
+
export function threadEventLogRetentionDays(cfg) {
|
|
488
|
+
return cfg.threads?.eventLogRetentionDays ?? null;
|
|
489
|
+
}
|
|
459
490
|
export function localVmMode(cfg) {
|
|
460
491
|
return cfg.localVm?.mode ?? DEFAULT_LOCAL_VM_MODE;
|
|
461
492
|
}
|
|
@@ -12,13 +12,6 @@ export class ProviderError extends Error {
|
|
|
12
12
|
this.code = code;
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
|
-
/** Reasoning-effort levels, ascending. A union of everything any engine
|
|
16
|
-
* accepts; each driver declares the subset its CLI will take. */
|
|
17
|
-
export const EFFORT_LEVELS = ["none", "low", "medium", "high", "xhigh", "max"];
|
|
18
|
-
/** Narrow untrusted API/config input before it becomes a model selection. */
|
|
19
|
-
export function isEffortLevel(value) {
|
|
20
|
-
return typeof value === "string" && EFFORT_LEVELS.includes(value);
|
|
21
|
-
}
|
|
22
15
|
/** Variants are opaque provider IDs, not the cross-engine effort enum. */
|
|
23
16
|
export function isModelVariant(value) {
|
|
24
17
|
return typeof value === "string" && value.length > 0 && value.length <= 256 &&
|
|
@@ -64,6 +64,16 @@ export function geminiIsAuthenticated(env) {
|
|
|
64
64
|
nonBlank(env.GOOGLE_API_KEY) ||
|
|
65
65
|
(existsSync(join(homedir(), ".gemini", "oauth_creds.json")) && liveOauthCredential()));
|
|
66
66
|
}
|
|
67
|
+
/** Gemini CLI's approval modes, passed through: `--approval-mode auto_edit`
|
|
68
|
+
* approves file edits, `--yolo` approves everything; Gemini has no
|
|
69
|
+
* automatic reviewer, so Auto stays Ask. Ask sends nothing. */
|
|
70
|
+
export function geminiApprovalArgs(fullAuto, approvalMode) {
|
|
71
|
+
if (fullAuto)
|
|
72
|
+
return ["--yolo"];
|
|
73
|
+
if (approvalMode === "edits")
|
|
74
|
+
return ["--approval-mode", "auto_edit"];
|
|
75
|
+
return [];
|
|
76
|
+
}
|
|
67
77
|
const support = {
|
|
68
78
|
driverKind: "geminiAgent",
|
|
69
79
|
displayName: "Gemini",
|
|
@@ -79,7 +89,7 @@ const support = {
|
|
|
79
89
|
loginNote: "Gemini CLI needs a GEMINI_API_KEY, a Vertex AI setup, or an enterprise Code Assist login — consumer Google logins stopped working on 2026-06-18 (use the Antigravity engine for those accounts)",
|
|
80
90
|
// --acp is the stable Gemini CLI surface. --experimental-acp remains an
|
|
81
91
|
// alias for older releases, but using it now emits a deprecation warning.
|
|
82
|
-
spawnArgs: (
|
|
92
|
+
spawnArgs: (config, turn) => ["--acp", ...geminiApprovalArgs(config.fullAuto, turn.approvalMode), ...(turn.model ? ["-m", turn.model] : [])],
|
|
83
93
|
credentialEnv: ["GEMINI_API_KEY", "GOOGLE_API_KEY"],
|
|
84
94
|
pickAuthMethod: (methods) => {
|
|
85
95
|
const ids = methods.map((m) => m.id).filter((id) => typeof id === "string");
|
|
@@ -205,6 +205,21 @@ export function resolveQwenTurnModel(model, env) {
|
|
|
205
205
|
throw new Error("This Qwen model is no longer configured. Refresh models and select it again.");
|
|
206
206
|
return matches[0].id;
|
|
207
207
|
}
|
|
208
|
+
/** Qwen Code's own approval ladder, passed through (qwen --help, 0.24):
|
|
209
|
+
* `--approval-mode default` asks, `auto-edit` approves file edits, `auto`
|
|
210
|
+
* runs Qwen's LLM classifier that approves safe actions and blocks risky
|
|
211
|
+
* ones, and `--yolo` approves everything. Ask sends nothing, so an older
|
|
212
|
+
* CLI without the flag keeps working at the level it always had; the ACP
|
|
213
|
+
* client still answers residual permission asks itself under Full. */
|
|
214
|
+
export function qwenApprovalArgs(fullAuto, approvalMode) {
|
|
215
|
+
if (fullAuto)
|
|
216
|
+
return ["--yolo"];
|
|
217
|
+
if (approvalMode === "auto")
|
|
218
|
+
return ["--approval-mode", "auto"];
|
|
219
|
+
if (approvalMode === "edits")
|
|
220
|
+
return ["--approval-mode", "auto-edit"];
|
|
221
|
+
return [];
|
|
222
|
+
}
|
|
208
223
|
const support = {
|
|
209
224
|
driverKind: "qwenAgent",
|
|
210
225
|
displayName: "Qwen",
|
|
@@ -225,7 +240,7 @@ const support = {
|
|
|
225
240
|
},
|
|
226
241
|
// A raw -m only changes the model within the saved provider. ACP switches
|
|
227
242
|
// the complete route and confirms it before any prompt leaves OMB.
|
|
228
|
-
spawnArgs: () => ["--acp"],
|
|
243
|
+
spawnArgs: (config, turn) => ["--acp", ...qwenApprovalArgs(config.fullAuto, turn.approvalMode)],
|
|
229
244
|
selectModel: { configId: "model" },
|
|
230
245
|
pickAuthMethod: () => null,
|
|
231
246
|
authFailure: "continue",
|
|
@@ -390,10 +390,10 @@ const TOOLS = [
|
|
|
390
390
|
},
|
|
391
391
|
{
|
|
392
392
|
name: "coordinate_bots",
|
|
393
|
-
description: "Ask existing OpenMausBot teammates for advice or assign concrete work. From normal chat every assignment you send a teammate continues your one standing conversation with that teammate, so they keep the context of what you asked before; from a room it defaults to this room. Use group_id from list_room_targets for a specific room.
|
|
393
|
+
description: "Ask existing OpenMausBot teammates for advice or assign concrete work. From normal chat every assignment you send a teammate continues your one standing conversation with that teammate, so they keep the context of what you asked before; from a room it defaults to this room. Use group_id from list_room_targets for a specific room. Give 1-4 bot_ids — teammate ids as list_bots or your roster prints them; a unique teammate name also resolves: they receive only your brief and use their own model, tools and permissions. Busy bots queue. They can consult their specialists; all results return here and resume you automatically. Include exact file paths, constraints and what must be verified. After sending all assignments, END your turn; do not poll or wait. On return, resolve tradeoffs, verify the requested outcome and request concrete corrections if necessary before giving one final answer. Do not send acknowledgements as new work.",
|
|
394
394
|
inputSchema: { type: "object", additionalProperties: false, properties: {
|
|
395
395
|
group_id: { type: "string", description: "Optional destination room. Omit for this room, or your standing conversation with each teammate when chatting directly." },
|
|
396
|
-
bot_ids: { type: "array", items: { type: "string" }, minItems: 1, maxItems: 4, uniqueItems: true },
|
|
396
|
+
bot_ids: { type: "array", items: { type: "string", description: "A teammate's id exactly as list_bots or your roster prints it ([id: …]). A teammate's unique display name also resolves; a name shared by two reachable teammates is refused." }, minItems: 1, maxItems: 4, uniqueItems: true },
|
|
397
397
|
message: { type: "string", minLength: 1, maxLength: 4000, description: "Self-contained question or task for these teammates. Send separate requests when responsibilities differ." },
|
|
398
398
|
request_key: { type: "string", description: "A short unique assignment key. Reuse for an identical retry." },
|
|
399
399
|
rework: { type: "boolean", description: "True only for concrete additional work from someone who already completed a request." },
|
|
@@ -416,7 +416,7 @@ const TOOLS = [
|
|
|
416
416
|
inputSchema: {
|
|
417
417
|
type: "object",
|
|
418
418
|
properties: {
|
|
419
|
-
bot_id: { type: "string", description: "The target bot's id (from list_bots)." },
|
|
419
|
+
bot_id: { type: "string", description: "The target bot's id (from list_bots or your roster); a unique teammate name also resolves." },
|
|
420
420
|
message: { type: "string", description: "What to say / ask the bot." },
|
|
421
421
|
},
|
|
422
422
|
required: ["bot_id", "message"],
|
|
@@ -428,7 +428,7 @@ const TOOLS = [
|
|
|
428
428
|
inputSchema: {
|
|
429
429
|
type: "object",
|
|
430
430
|
properties: {
|
|
431
|
-
bot_id: { type: "string", description: "The target bot's id (from list_bots)." },
|
|
431
|
+
bot_id: { type: "string", description: "The target bot's id (from list_bots or your roster); a unique teammate name also resolves." },
|
|
432
432
|
message: { type: "string", description: "What the peer should do / answer." },
|
|
433
433
|
reason: { type: "string", description: "Optional one-line reason for the delegation (shown to the user as a chip)." },
|
|
434
434
|
},
|
|
@@ -1754,12 +1754,13 @@ export const ClaudeDriver = {
|
|
|
1754
1754
|
return { turnId };
|
|
1755
1755
|
};
|
|
1756
1756
|
/** A user message into the running turn: the CLI delivers it before its
|
|
1757
|
-
* next model call.
|
|
1757
|
+
* next model call. "refused" when nothing is running here to steer or
|
|
1758
|
+
* the stdin write provably failed; the caller queues those words. */
|
|
1758
1759
|
const steer = async (threadId, text) => {
|
|
1759
1760
|
const s = sessions.get(threadId);
|
|
1760
1761
|
if (!s || !s.turn || s.turn.settled || s.closing || s.child.exitCode !== null)
|
|
1761
|
-
return
|
|
1762
|
-
return writeUser(s, threadId, claudeUserMessage(text, undefined));
|
|
1762
|
+
return "refused";
|
|
1763
|
+
return (await writeUser(s, threadId, claudeUserMessage(text, undefined))) ? "steered" : "refused";
|
|
1763
1764
|
};
|
|
1764
1765
|
// Sign in from Settings: the unmodified CLI's own login, driven over pipes
|
|
1765
1766
|
// (server/drivers/claude-login-auth.ts). Same environment as every turn.
|
|
@@ -703,9 +703,33 @@ export const CodexDriver = {
|
|
|
703
703
|
return stopped;
|
|
704
704
|
});
|
|
705
705
|
let completeStoppedTurn;
|
|
706
|
+
// Stop asks the app-server to end the turn itself before any process
|
|
707
|
+
// signal. Killing first surfaced routine stops as "codex exited null
|
|
708
|
+
// (signal SIGTERM) before turn/completed"; the protocol interrupt keeps
|
|
709
|
+
// the session the authority, and the kill below is only escalation for
|
|
710
|
+
// a server that will not answer. settle() runs with state.settled
|
|
711
|
+
// already true, so ordinary completion still tears down immediately.
|
|
712
|
+
let interruptRequested = false;
|
|
706
713
|
const stop = async () => {
|
|
707
714
|
stopRequested = true;
|
|
708
715
|
stopSignal.abort();
|
|
716
|
+
if (!state.settled && !interruptRequested && codexThreadId && codexTurnId &&
|
|
717
|
+
child.exitCode === null && child.signalCode === null) {
|
|
718
|
+
interruptRequested = true;
|
|
719
|
+
const graceMs = Math.max(1, Number(process.env.FAKE_CODEX_INTERRUPT_GRACE_MS ?? 750) || 750);
|
|
720
|
+
try {
|
|
721
|
+
await request("turn/interrupt", { threadId: codexThreadId, turnId: codexTurnId }, graceMs);
|
|
722
|
+
}
|
|
723
|
+
catch {
|
|
724
|
+
// Old CLI without the method, or a wedged server: escalate below.
|
|
725
|
+
}
|
|
726
|
+
const deadline = Date.now() + graceMs;
|
|
727
|
+
while (!state.settled && Date.now() < deadline) {
|
|
728
|
+
await new Promise((wake) => setTimeout(wake, 15));
|
|
729
|
+
}
|
|
730
|
+
if (state.settled)
|
|
731
|
+
return true;
|
|
732
|
+
}
|
|
709
733
|
const stopped = await terminate();
|
|
710
734
|
if (stopped)
|
|
711
735
|
completeStoppedTurn?.();
|
|
@@ -731,6 +755,35 @@ export const CodexDriver = {
|
|
|
731
755
|
emit({ ...base(threadId, turnId), type: "runtime.error", message: "codex did not shut down after termination was requested" });
|
|
732
756
|
}
|
|
733
757
|
};
|
|
758
|
+
// Live steering folds new input into the running turn without ending
|
|
759
|
+
// it. expectedTurnId is the protocol's precondition: a turn that moved
|
|
760
|
+
// on (or a CLI without turn/steer) answers with an explicit RPC error,
|
|
761
|
+
// which becomes "refused" here so the caller queues for the next turn —
|
|
762
|
+
// the child is never killed to steer. A timeout after delivery, a dead
|
|
763
|
+
// transport, or a turn that settles while the answer is in flight is
|
|
764
|
+
// "indeterminate": the words may already be running, so the caller must
|
|
765
|
+
// not re-queue them.
|
|
766
|
+
const steerActiveTurn = async (text) => {
|
|
767
|
+
if (state.settled || abandoned || stopRequested || !codexThreadId || !codexTurnId)
|
|
768
|
+
return "refused";
|
|
769
|
+
if (child.exitCode !== null || child.signalCode !== null)
|
|
770
|
+
return "refused";
|
|
771
|
+
try {
|
|
772
|
+
const steerTimeoutMs = Math.max(1, Number(process.env.FAKE_CODEX_STEER_TIMEOUT_MS ?? 10_000) || 10_000);
|
|
773
|
+
await request("turn/steer", {
|
|
774
|
+
threadId: codexThreadId,
|
|
775
|
+
input: [{ type: "text", text }],
|
|
776
|
+
expectedTurnId: codexTurnId,
|
|
777
|
+
}, steerTimeoutMs);
|
|
778
|
+
return "steered";
|
|
779
|
+
}
|
|
780
|
+
catch (error) {
|
|
781
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
782
|
+
if (state.settled || message.includes("timed out"))
|
|
783
|
+
return "indeterminate";
|
|
784
|
+
return "refused";
|
|
785
|
+
}
|
|
786
|
+
};
|
|
734
787
|
// server→client approval request → canonical request.opened
|
|
735
788
|
// Host-scope tagging mirrors claude.ts: when this turn mounts the real
|
|
736
789
|
// Mac (not a VM), every card carries approvalScope so the harness's
|
|
@@ -1134,6 +1187,13 @@ export const CodexDriver = {
|
|
|
1134
1187
|
void stop();
|
|
1135
1188
|
return;
|
|
1136
1189
|
}
|
|
1190
|
+
// An intentional stop killed (or outlived) the child before the
|
|
1191
|
+
// turn acknowledged its own end. That is the stop doing its job, not
|
|
1192
|
+
// a crash: settle quietly so Stop never reports the raw signal.
|
|
1193
|
+
if (stopRequested) {
|
|
1194
|
+
void settle(false, "interrupted");
|
|
1195
|
+
return;
|
|
1196
|
+
}
|
|
1137
1197
|
// The child died before the turn completed. Attribute the exit
|
|
1138
1198
|
// honestly: name the signal when it was killed, and only quote
|
|
1139
1199
|
// stderr that arrived after the last protocol message. A stale
|
|
@@ -1194,7 +1254,7 @@ export const CodexDriver = {
|
|
|
1194
1254
|
});
|
|
1195
1255
|
void settle(false, "exit_before_result");
|
|
1196
1256
|
});
|
|
1197
|
-
active.set(threadId, { stop, turnId, asks });
|
|
1257
|
+
active.set(threadId, { stop, turnId, asks, steer: steerActiveTurn });
|
|
1198
1258
|
// Relaunching the app-server is still the same logical turn. Keep the
|
|
1199
1259
|
// active process current on every attempt, but announce the turn once.
|
|
1200
1260
|
if (attempt === 0)
|
|
@@ -1441,6 +1501,7 @@ export const CodexDriver = {
|
|
|
1441
1501
|
provider: DRIVER_KIND,
|
|
1442
1502
|
capabilities: {
|
|
1443
1503
|
sessionModelSwitch: "unsupported",
|
|
1504
|
+
queueing: true,
|
|
1444
1505
|
computerMcp: true,
|
|
1445
1506
|
localComputerMcp: true,
|
|
1446
1507
|
composioMcp: true,
|
|
@@ -1456,6 +1517,10 @@ export const CodexDriver = {
|
|
|
1456
1517
|
interruptTurn: async (threadId) => {
|
|
1457
1518
|
await active.get(threadId)?.stop();
|
|
1458
1519
|
},
|
|
1520
|
+
steer: async (threadId, text) => {
|
|
1521
|
+
const turn = active.get(threadId);
|
|
1522
|
+
return turn?.steer ? await turn.steer(text) : "refused";
|
|
1523
|
+
},
|
|
1459
1524
|
respondToRequest: async (threadId, requestId, decision) => {
|
|
1460
1525
|
const turn = active.get(threadId);
|
|
1461
1526
|
const finish = turn?.asks.get(requestId);
|
|
@@ -28,7 +28,8 @@ import { augmentedPath } from "../env-path.js";
|
|
|
28
28
|
import { describeSpawnFailure, execCli, killCliTree, spawnCli } from "../procs.js";
|
|
29
29
|
import { SPAWNED_PROXIES } from "../proxy-paths.js";
|
|
30
30
|
import { commandSummary, toolDetailPreview } from "../tool-summary.js";
|
|
31
|
-
import { EFFORT_LEVELS
|
|
31
|
+
import { EFFORT_LEVELS } from "../../shared/wire.js";
|
|
32
|
+
import { newEventId, newId } from "../contracts.js";
|
|
32
33
|
import { decodeInjectId, encodeInjectId, hostApiKey, localHost, mergeLocalInject, } from "./local-inject.js";
|
|
33
34
|
import { appendNative } from "./native.js";
|
|
34
35
|
const DRIVER_KIND = "piAgent";
|
|
@@ -252,7 +252,12 @@ function parseCmdShim(shim, env) {
|
|
|
252
252
|
return null;
|
|
253
253
|
}
|
|
254
254
|
const dir = dirname(shim);
|
|
255
|
-
|
|
255
|
+
// npm's own npm.cmd / npx.cmd, installed beside node.exe, name their entry
|
|
256
|
+
// in a variable next to a helper script that is not the CLI:
|
|
257
|
+
// SET "NPX_CLI_JS=%~dp0\node_modules\npm\bin\npx-cli.js". The launcher's
|
|
258
|
+
// switch to a globally upgraded npm is not followed; this node's npm runs.
|
|
259
|
+
const npmEntry = /^SET "NP[MX]_CLI_JS=%~dp0\\([^"]+)"/im.exec(text);
|
|
260
|
+
const targets = [...(npmEntry ? [npmEntry] : []), ...text.matchAll(/"%~?dp0%?\\?([^"]+)"/g)]
|
|
256
261
|
.map((m) => join(dir, m[1]))
|
|
257
262
|
.filter((p) => isFile(p) && basename(p).toLowerCase() !== "node.exe");
|
|
258
263
|
const script = targets.find((p) => /\.[cm]?js$/i.test(p));
|