@mrrlin-dev/mcp 0.3.14 → 0.3.20
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/bin.cjs
CHANGED
|
@@ -20799,9 +20799,8 @@ function renderBlock(block) {
|
|
|
20799
20799
|
lines2.push("");
|
|
20800
20800
|
return lines2.join("\n");
|
|
20801
20801
|
}
|
|
20802
|
-
|
|
20803
|
-
|
|
20804
|
-
const candidate = actual;
|
|
20802
|
+
var KNOWN_MRRLIN_ENV_KEYS = /* @__PURE__ */ new Set(["MRRLIN_STAGING", "MRRLIN_API_BASE_URL", "MRRLIN_API_ALLOW_REMOTE"]);
|
|
20803
|
+
function commandArgsTimeoutEqual(candidate, expected) {
|
|
20805
20804
|
if (candidate.command !== expected.command) return false;
|
|
20806
20805
|
if (!Array.isArray(candidate.args)) return false;
|
|
20807
20806
|
if (candidate.args.length !== expected.args.length) return false;
|
|
@@ -20811,6 +20810,12 @@ function tablesEqual(actual, expected) {
|
|
|
20811
20810
|
if (candidate.startup_timeout_sec !== void 0 && candidate.startup_timeout_sec !== expected.startup_timeout_sec) {
|
|
20812
20811
|
return false;
|
|
20813
20812
|
}
|
|
20813
|
+
return true;
|
|
20814
|
+
}
|
|
20815
|
+
function tablesEqual(actual, expected) {
|
|
20816
|
+
if (typeof actual !== "object" || actual === null) return false;
|
|
20817
|
+
const candidate = actual;
|
|
20818
|
+
if (!commandArgsTimeoutEqual(candidate, expected)) return false;
|
|
20814
20819
|
const expectedEnv = expected.env ?? {};
|
|
20815
20820
|
const actualEnv = candidate.env && typeof candidate.env === "object" ? candidate.env : {};
|
|
20816
20821
|
const expectedKeys = Object.keys(expectedEnv);
|
|
@@ -20820,6 +20825,34 @@ function tablesEqual(actual, expected) {
|
|
|
20820
20825
|
}
|
|
20821
20826
|
return true;
|
|
20822
20827
|
}
|
|
20828
|
+
function isCoherentMrrlinEnv(env) {
|
|
20829
|
+
const keys = Object.keys(env);
|
|
20830
|
+
const staging = env.MRRLIN_STAGING;
|
|
20831
|
+
const url2 = env.MRRLIN_API_BASE_URL;
|
|
20832
|
+
const allowRemote = env.MRRLIN_API_ALLOW_REMOTE;
|
|
20833
|
+
const hasUrl = typeof url2 === "string" && url2.trim() !== "";
|
|
20834
|
+
if (keys.length === 1 && staging === "1") return true;
|
|
20835
|
+
if (keys.length === 1 && hasUrl) return true;
|
|
20836
|
+
if (keys.length === 2 && hasUrl && allowRemote === "1") return true;
|
|
20837
|
+
return false;
|
|
20838
|
+
}
|
|
20839
|
+
function classifyExistingBlock(actual, expected) {
|
|
20840
|
+
if (tablesEqual(actual, expected)) return "equal";
|
|
20841
|
+
if (typeof actual !== "object" || actual === null) return "conflict";
|
|
20842
|
+
const candidate = actual;
|
|
20843
|
+
if (expected.env && Object.keys(expected.env).length > 0) return "conflict";
|
|
20844
|
+
if (!commandArgsTimeoutEqual(candidate, expected)) return "conflict";
|
|
20845
|
+
const env = candidate.env && typeof candidate.env === "object" ? candidate.env : {};
|
|
20846
|
+
const keys = Object.keys(env);
|
|
20847
|
+
if (keys.length === 0) return "conflict";
|
|
20848
|
+
if (!keys.every((key) => KNOWN_MRRLIN_ENV_KEYS.has(key))) return "conflict";
|
|
20849
|
+
if (!isCoherentMrrlinEnv(env)) return "conflict";
|
|
20850
|
+
return "preserve-env-noop";
|
|
20851
|
+
}
|
|
20852
|
+
function renderEnvPreservedNotice(blockId, envKeys) {
|
|
20853
|
+
const keys = [...envKeys].sort().join(", ");
|
|
20854
|
+
return `[mrrlin-mcp install-codex] kept your existing [mcp_servers.${blockId}] env keys (${keys}) so your API target is preserved. Re-run with --force to replace or remove it.`;
|
|
20855
|
+
}
|
|
20823
20856
|
async function pathExists(target) {
|
|
20824
20857
|
try {
|
|
20825
20858
|
await import_promises.default.access(target);
|
|
@@ -20845,7 +20878,7 @@ async function installCodex(options) {
|
|
|
20845
20878
|
if (!await pathExists(configPath)) {
|
|
20846
20879
|
const content2 = blocks.map(renderBlock).join("").replace(/^\n/, "");
|
|
20847
20880
|
await import_promises.default.writeFile(configPath, content2, { mode: 384 });
|
|
20848
|
-
return { action: "created", configPath, prompts: await writePrompts() };
|
|
20881
|
+
return { action: "created", configPath, prompts: await writePrompts(), preservedEnvBlocks: [] };
|
|
20849
20882
|
}
|
|
20850
20883
|
const realPath = await resolveRealConfigPath(configPath);
|
|
20851
20884
|
let content = await import_promises.default.readFile(realPath, "utf8");
|
|
@@ -20862,10 +20895,19 @@ async function installCodex(options) {
|
|
|
20862
20895
|
throw Object.assign(new Error(err.detail), err);
|
|
20863
20896
|
}
|
|
20864
20897
|
let action = "noop";
|
|
20898
|
+
let mutated = false;
|
|
20899
|
+
const preservedEnvBlocks = [];
|
|
20865
20900
|
for (const block of blocks) {
|
|
20866
20901
|
const existingTable = parsed?.mcp_servers?.[block.id];
|
|
20867
20902
|
if (existingTable !== void 0) {
|
|
20868
|
-
|
|
20903
|
+
const classification = classifyExistingBlock(existingTable, block.expected);
|
|
20904
|
+
if (classification === "equal") continue;
|
|
20905
|
+
if (classification === "preserve-env-noop") {
|
|
20906
|
+
const env = existingTable.env ?? {};
|
|
20907
|
+
preservedEnvBlocks.push({ blockId: block.id, envKeys: Object.keys(env).sort() });
|
|
20908
|
+
if (action === "noop") action = "env-preserved";
|
|
20909
|
+
continue;
|
|
20910
|
+
}
|
|
20869
20911
|
if (!options.force) {
|
|
20870
20912
|
const err = {
|
|
20871
20913
|
code: "CODEX_CONFIG_CONFLICT",
|
|
@@ -20876,15 +20918,17 @@ async function installCodex(options) {
|
|
|
20876
20918
|
}
|
|
20877
20919
|
content = replaceBlock(content, block.blockRe, renderBlock(block));
|
|
20878
20920
|
action = "replaced";
|
|
20921
|
+
mutated = true;
|
|
20879
20922
|
} else {
|
|
20880
20923
|
content = appendBlock(content, renderBlock(block));
|
|
20881
20924
|
if (action !== "replaced") action = "appended";
|
|
20925
|
+
mutated = true;
|
|
20882
20926
|
}
|
|
20883
20927
|
}
|
|
20884
|
-
if (
|
|
20928
|
+
if (mutated) {
|
|
20885
20929
|
await import_promises.default.writeFile(realPath, content, { mode: 384 });
|
|
20886
20930
|
}
|
|
20887
|
-
return { action, configPath: realPath, prompts: await writePrompts() };
|
|
20931
|
+
return { action, configPath: realPath, prompts: await writePrompts(), preservedEnvBlocks };
|
|
20888
20932
|
}
|
|
20889
20933
|
async function installPrompts(codexHome, prompts, forcePrompts) {
|
|
20890
20934
|
if (prompts.length === 0) return [];
|
|
@@ -20941,10 +20985,15 @@ var import_node_child_process4 = require("node:child_process");
|
|
|
20941
20985
|
var import_node_crypto5 = __toESM(require("node:crypto"), 1);
|
|
20942
20986
|
|
|
20943
20987
|
// src/turn-replay-buffer.ts
|
|
20944
|
-
var DEFAULT_GRACE_MS = 3e4;
|
|
20945
20988
|
var DEFAULT_MAX_EVENTS = 2e3;
|
|
20946
20989
|
var DEFAULT_MAX_BYTES = 4e6;
|
|
20947
|
-
var
|
|
20990
|
+
var DEFAULT_ACK_LINGER_MS = 1e4;
|
|
20991
|
+
var DEFAULT_RESULT_RETENTION_MS = 36e5;
|
|
20992
|
+
function resultRetentionMs() {
|
|
20993
|
+
const raw = (process.env.MRRLIN_DIRECTOR_BRIDGE_RESULT_RETENTION_SECONDS ?? "").trim();
|
|
20994
|
+
const parsed = Number.parseInt(raw, 10);
|
|
20995
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed * 1e3 : DEFAULT_RESULT_RETENTION_MS;
|
|
20996
|
+
}
|
|
20948
20997
|
function createReplayBuffer(opts) {
|
|
20949
20998
|
return {
|
|
20950
20999
|
turnId: opts.turnId,
|
|
@@ -20956,14 +21005,15 @@ function createReplayBuffer(opts) {
|
|
|
20956
21005
|
terminalDelivered: false,
|
|
20957
21006
|
replaying: false,
|
|
20958
21007
|
outboundQueue: [],
|
|
20959
|
-
graceTimer: null,
|
|
20960
21008
|
finishedAt: null,
|
|
21009
|
+
finishedSuspendedMs: null,
|
|
21010
|
+
ackedAt: null,
|
|
20961
21011
|
bytesUsed: 0,
|
|
20962
|
-
graceMs: opts.graceMs ?? DEFAULT_GRACE_MS,
|
|
20963
21012
|
maxEvents: opts.maxEvents ?? DEFAULT_MAX_EVENTS,
|
|
20964
21013
|
maxBytes: opts.maxBytes ?? DEFAULT_MAX_BYTES,
|
|
20965
|
-
|
|
20966
|
-
|
|
21014
|
+
ackLingerMs: opts.ackLingerMs ?? DEFAULT_ACK_LINGER_MS,
|
|
21015
|
+
retentionMs: opts.retentionMs ?? resultRetentionMs(),
|
|
21016
|
+
triggerDetach: null
|
|
20967
21017
|
};
|
|
20968
21018
|
}
|
|
20969
21019
|
function recordOutgoing(buf, msg) {
|
|
@@ -20972,9 +21022,10 @@ function recordOutgoing(buf, msg) {
|
|
|
20972
21022
|
buf.events.push(entry);
|
|
20973
21023
|
buf.bytesUsed += approxByteSize(msg);
|
|
20974
21024
|
while ((buf.events.length > buf.maxEvents || buf.bytesUsed > buf.maxBytes) && buf.events.length > 0) {
|
|
20975
|
-
const
|
|
20976
|
-
if (!
|
|
20977
|
-
buf.
|
|
21025
|
+
const head = buf.events[0];
|
|
21026
|
+
if (!head || isTerminalEvent(head.msg)) break;
|
|
21027
|
+
buf.events.shift();
|
|
21028
|
+
buf.bytesUsed -= approxByteSize(head.msg);
|
|
20978
21029
|
}
|
|
20979
21030
|
return entry;
|
|
20980
21031
|
}
|
|
@@ -20983,19 +21034,7 @@ function eventsAfter(buf, lastSeq) {
|
|
|
20983
21034
|
if (first && lastSeq < first.seq - 1) return null;
|
|
20984
21035
|
return buf.events.filter((e) => e.seq > lastSeq);
|
|
20985
21036
|
}
|
|
20986
|
-
function
|
|
20987
|
-
if (buf.graceTimer !== null) return;
|
|
20988
|
-
buf.state = "grace";
|
|
20989
|
-
buf.graceTimer = setTimeout(onExpire, buf.graceMs);
|
|
20990
|
-
}
|
|
20991
|
-
function cancelGrace(buf) {
|
|
20992
|
-
if (buf.graceTimer !== null) {
|
|
20993
|
-
clearTimeout(buf.graceTimer);
|
|
20994
|
-
buf.graceTimer = null;
|
|
20995
|
-
}
|
|
20996
|
-
if (buf.state === "grace") buf.state = "live";
|
|
20997
|
-
}
|
|
20998
|
-
function recordInterrupted(buf) {
|
|
21037
|
+
function recordInterrupted(buf, now = Date.now(), suspendedMsNow = 0) {
|
|
20999
21038
|
if (buf.terminalDelivered) return null;
|
|
21000
21039
|
const msg = {
|
|
21001
21040
|
type: "event",
|
|
@@ -21003,16 +21042,31 @@ function recordInterrupted(buf) {
|
|
|
21003
21042
|
};
|
|
21004
21043
|
const entry = recordOutgoing(buf, msg);
|
|
21005
21044
|
buf.terminalDelivered = true;
|
|
21045
|
+
if (buf.finishedAt === null) {
|
|
21046
|
+
buf.finishedAt = now;
|
|
21047
|
+
buf.finishedSuspendedMs = suspendedMsNow;
|
|
21048
|
+
}
|
|
21006
21049
|
return entry;
|
|
21007
21050
|
}
|
|
21008
|
-
function markCompleted(buf, now) {
|
|
21051
|
+
function markCompleted(buf, now, suspendedMsNow = 0) {
|
|
21009
21052
|
buf.state = "completed";
|
|
21010
21053
|
buf.finishedAt = now;
|
|
21054
|
+
buf.finishedSuspendedMs = suspendedMsNow;
|
|
21055
|
+
}
|
|
21056
|
+
function recordAck(buf, turnId, lastSeq, now) {
|
|
21057
|
+
if (buf.turnId !== turnId) return false;
|
|
21058
|
+
if (lastSeq !== buf.lastSeq) return false;
|
|
21059
|
+
if (buf.state !== "completed" && buf.state !== "interrupted") return false;
|
|
21060
|
+
if (buf.ackedAt === null) buf.ackedAt = now;
|
|
21061
|
+
return true;
|
|
21011
21062
|
}
|
|
21012
|
-
function shouldGc(buf, now) {
|
|
21013
|
-
if (buf.state !== "completed") return false;
|
|
21063
|
+
function shouldGc(buf, now, suspendedMsNow = 0) {
|
|
21064
|
+
if (buf.state !== "completed" && buf.state !== "interrupted") return false;
|
|
21014
21065
|
if (buf.finishedAt === null) return false;
|
|
21015
|
-
return now - buf.
|
|
21066
|
+
if (buf.ackedAt !== null) return now - buf.ackedAt > buf.ackLingerMs;
|
|
21067
|
+
const suspendedSinceFinish = suspendedMsNow - (buf.finishedSuspendedMs ?? suspendedMsNow);
|
|
21068
|
+
const awakeElapsed = now - buf.finishedAt - Math.max(0, suspendedSinceFinish);
|
|
21069
|
+
return awakeElapsed > buf.retentionMs;
|
|
21016
21070
|
}
|
|
21017
21071
|
function incrementBindEpoch(buf) {
|
|
21018
21072
|
return ++buf.bindEpoch;
|
|
@@ -21033,6 +21087,41 @@ function approxByteSize(msg) {
|
|
|
21033
21087
|
}
|
|
21034
21088
|
}
|
|
21035
21089
|
|
|
21090
|
+
// src/suspension-detector.ts
|
|
21091
|
+
var DEFAULT_SUSPEND_TICK_MS = 15e3;
|
|
21092
|
+
var DEFAULT_SUSPEND_GAP_MS = 45e3;
|
|
21093
|
+
function readPositiveMs(name, fallback) {
|
|
21094
|
+
const raw = (process.env[name] ?? "").trim();
|
|
21095
|
+
const parsed = Number.parseInt(raw, 10);
|
|
21096
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
21097
|
+
}
|
|
21098
|
+
function suspendTickMs() {
|
|
21099
|
+
return readPositiveMs("MRRLIN_DIRECTOR_BRIDGE_SUSPEND_TICK_MS", DEFAULT_SUSPEND_TICK_MS);
|
|
21100
|
+
}
|
|
21101
|
+
function suspendGapMs() {
|
|
21102
|
+
return readPositiveMs("MRRLIN_DIRECTOR_BRIDGE_SUSPEND_GAP_MS", DEFAULT_SUSPEND_GAP_MS);
|
|
21103
|
+
}
|
|
21104
|
+
function createSuspensionDetector(opts) {
|
|
21105
|
+
const tickMs = opts?.tickMs ?? suspendTickMs();
|
|
21106
|
+
const gapMs = opts?.gapMs ?? suspendGapMs();
|
|
21107
|
+
const now = opts?.now ?? Date.now;
|
|
21108
|
+
const setI = opts?.setIntervalFn ?? setInterval;
|
|
21109
|
+
const clearI = opts?.clearIntervalFn ?? clearInterval;
|
|
21110
|
+
let last = now();
|
|
21111
|
+
let suspended = 0;
|
|
21112
|
+
const timer = setI(() => {
|
|
21113
|
+
const t = now();
|
|
21114
|
+
const gap = t - last;
|
|
21115
|
+
last = t;
|
|
21116
|
+
if (gap >= gapMs) suspended += gap - tickMs;
|
|
21117
|
+
}, tickMs);
|
|
21118
|
+
timer.unref?.();
|
|
21119
|
+
return {
|
|
21120
|
+
suspendedMs: () => suspended,
|
|
21121
|
+
dispose: () => clearI(timer)
|
|
21122
|
+
};
|
|
21123
|
+
}
|
|
21124
|
+
|
|
21036
21125
|
// src/replay-state-machine.ts
|
|
21037
21126
|
function transition(state, event) {
|
|
21038
21127
|
if (event.kind === "resume" && !event.epochMatch) return { next: "invalid" };
|
|
@@ -21040,7 +21129,12 @@ function transition(state, event) {
|
|
|
21040
21129
|
case "live":
|
|
21041
21130
|
switch (event.kind) {
|
|
21042
21131
|
case "socket_close":
|
|
21043
|
-
return { next: "
|
|
21132
|
+
return { next: "detached", sideEffects: [] };
|
|
21133
|
+
case "resume":
|
|
21134
|
+
return {
|
|
21135
|
+
next: "live",
|
|
21136
|
+
sideEffects: ["rebind_sink_increment_epoch", "replay_drain"]
|
|
21137
|
+
};
|
|
21044
21138
|
case "codex_terminal":
|
|
21045
21139
|
return { next: "completed", sideEffects: ["mark_finished"] };
|
|
21046
21140
|
case "stop":
|
|
@@ -21051,35 +21145,22 @@ function transition(state, event) {
|
|
|
21051
21145
|
default:
|
|
21052
21146
|
return { next: "invalid" };
|
|
21053
21147
|
}
|
|
21054
|
-
case "
|
|
21148
|
+
case "detached":
|
|
21055
21149
|
switch (event.kind) {
|
|
21150
|
+
case "socket_close":
|
|
21151
|
+
return { next: "detached", sideEffects: [] };
|
|
21152
|
+
// idempotent
|
|
21056
21153
|
case "resume":
|
|
21057
21154
|
return {
|
|
21058
21155
|
next: "live",
|
|
21059
|
-
sideEffects: [
|
|
21060
|
-
"cancel_grace_timer",
|
|
21061
|
-
"rebind_sink_increment_epoch",
|
|
21062
|
-
"replay_drain"
|
|
21063
|
-
]
|
|
21064
|
-
};
|
|
21065
|
-
case "grace_expire":
|
|
21066
|
-
return {
|
|
21067
|
-
next: "interrupted",
|
|
21068
|
-
sideEffects: ["record_interrupted", "fire_turn_interrupt"]
|
|
21156
|
+
sideEffects: ["rebind_sink_increment_epoch", "replay_drain"]
|
|
21069
21157
|
};
|
|
21158
|
+
case "codex_terminal":
|
|
21159
|
+
return { next: "completed", sideEffects: ["mark_finished"] };
|
|
21070
21160
|
case "stop":
|
|
21071
21161
|
return {
|
|
21072
21162
|
next: "interrupted",
|
|
21073
|
-
sideEffects: [
|
|
21074
|
-
"cancel_grace_timer",
|
|
21075
|
-
"record_interrupted",
|
|
21076
|
-
"fire_turn_interrupt"
|
|
21077
|
-
]
|
|
21078
|
-
};
|
|
21079
|
-
case "codex_terminal":
|
|
21080
|
-
return {
|
|
21081
|
-
next: "completed",
|
|
21082
|
-
sideEffects: ["cancel_grace_timer", "mark_finished"]
|
|
21163
|
+
sideEffects: ["record_interrupted", "fire_turn_interrupt"]
|
|
21083
21164
|
};
|
|
21084
21165
|
default:
|
|
21085
21166
|
return { next: "invalid" };
|
|
@@ -36437,10 +36518,12 @@ var mrrlinTaskStatusSchema = external_exports.enum([
|
|
|
36437
36518
|
"closed_done",
|
|
36438
36519
|
"closed_no_result"
|
|
36439
36520
|
]);
|
|
36521
|
+
var mrrlinTerminalTaskStatuses = ["closed_done", "closed_no_result"];
|
|
36440
36522
|
var mrrlinTaskTypeSchema = external_exports.enum(["task", "bug"]);
|
|
36441
36523
|
var mrrlinTaskImpactSchema = external_exports.enum(["low", "medium", "high"]);
|
|
36442
36524
|
var mrrlinAutonomyLevelSchema = external_exports.enum(["auto", "minimal_human", "human_only"]);
|
|
36443
36525
|
var mrrlinAutoDeploySchema = external_exports.enum(["off", "dev", "prod"]);
|
|
36526
|
+
var mrrlinTaskDomainSchema = external_exports.enum(["code", "seo", "marketing", "content"]);
|
|
36444
36527
|
var mrrlinSpecApprovedSchema = external_exports.enum(["pending", "approved", "skipped", "skipped_legacy"]);
|
|
36445
36528
|
var mrrlinTaskSourceSchema = external_exports.enum(["fixture-import", "api"]);
|
|
36446
36529
|
var mrrlinPlanStatusSchema = external_exports.enum(["active", "paused", "done", "archived"]);
|
|
@@ -36472,6 +36555,27 @@ function normalizeTaskTags(tags) {
|
|
|
36472
36555
|
}
|
|
36473
36556
|
return [...normalized].sort();
|
|
36474
36557
|
}
|
|
36558
|
+
var terminalTaskStatusSet = new Set(mrrlinTerminalTaskStatuses);
|
|
36559
|
+
function isTaskTerminalStatus(status) {
|
|
36560
|
+
return terminalTaskStatusSet.has(status);
|
|
36561
|
+
}
|
|
36562
|
+
function getActiveBlockedByIds(blockedBy, blockers) {
|
|
36563
|
+
const blockersById = /* @__PURE__ */ new Map();
|
|
36564
|
+
for (const blocker of blockers) {
|
|
36565
|
+
if (!blocker)
|
|
36566
|
+
continue;
|
|
36567
|
+
blockersById.set(blocker.id, blocker.status);
|
|
36568
|
+
}
|
|
36569
|
+
const active = [];
|
|
36570
|
+
for (const blockerId of blockedBy ?? []) {
|
|
36571
|
+
const status = blockersById.get(blockerId);
|
|
36572
|
+
if (!status)
|
|
36573
|
+
continue;
|
|
36574
|
+
if (!isTaskTerminalStatus(status))
|
|
36575
|
+
active.push(blockerId);
|
|
36576
|
+
}
|
|
36577
|
+
return active;
|
|
36578
|
+
}
|
|
36475
36579
|
var mrrlinTaskTagSchema = external_exports.string().min(1).max(40).transform(normalizeTaskTagInput).pipe(external_exports.string().min(1).max(40));
|
|
36476
36580
|
var mrrlinTaskTagsSchema = external_exports.array(mrrlinTaskTagSchema).default([]).transform(normalizeTaskTags);
|
|
36477
36581
|
var mrrlinExecutionRunStatusSchema = external_exports.enum([
|
|
@@ -36525,6 +36629,10 @@ var mrrlinTaskAssigneeSchema = external_exports.union([external_exports.string()
|
|
|
36525
36629
|
var mrrlinTaskSchema = external_exports.object({
|
|
36526
36630
|
assignee: mrrlinTaskAssigneeSchema,
|
|
36527
36631
|
autoDeploy: mrrlinAutoDeploySchema,
|
|
36632
|
+
// ADR 0020. OUTPUT-optional (not defaulted-required) so hardcoded task fixtures/mocks across the
|
|
36633
|
+
// codebase need no churn; the DB column is NOT NULL default 'code', so real rows always carry it,
|
|
36634
|
+
// and resolveHeads() treats an absent domain as `code`. Persisted create/update use their own forms.
|
|
36635
|
+
domain: mrrlinTaskDomainSchema.optional(),
|
|
36528
36636
|
autonomyLevel: mrrlinAutonomyLevelSchema,
|
|
36529
36637
|
autonomyReason: external_exports.string().min(1).max(240),
|
|
36530
36638
|
blockedBy: external_exports.array(mrrlinTaskIdSchema),
|
|
@@ -36642,7 +36750,10 @@ var mrrlinProjectSummarySchema = external_exports.object({
|
|
|
36642
36750
|
emoji: mrrlinProjectEmojiSchema,
|
|
36643
36751
|
github: mrrlinGithubBindingSchema,
|
|
36644
36752
|
onboardingKickoffState: external_exports.enum(["pending", "sent"]).nullable().optional(),
|
|
36645
|
-
slug: mrrlinProjectSlugSchema
|
|
36753
|
+
slug: mrrlinProjectSlugSchema,
|
|
36754
|
+
// ADR 0020: default consensus domain for this project's plan/project-scoped specs. Output-optional
|
|
36755
|
+
// (DB column is NOT NULL default 'code') so existing project fixtures/upsert callers need no churn.
|
|
36756
|
+
defaultDomain: mrrlinTaskDomainSchema.optional()
|
|
36646
36757
|
});
|
|
36647
36758
|
var mrrlinProjectUpsertSchema = mrrlinProjectSummarySchema.omit({
|
|
36648
36759
|
onboardingKickoffState: true,
|
|
@@ -36691,6 +36802,9 @@ var mrrlinProjectSnapshotSchema = external_exports.object({
|
|
|
36691
36802
|
});
|
|
36692
36803
|
var mrrlinTaskCreateSchema = external_exports.object({
|
|
36693
36804
|
autoDeploy: mrrlinAutoDeploySchema.default("off"),
|
|
36805
|
+
// Input-optional so existing createTask call sites need no churn; createTask fills 'code' for the
|
|
36806
|
+
// INSERT (the pg column is NOT NULL, so a concrete value is required, not undefined→NULL).
|
|
36807
|
+
domain: mrrlinTaskDomainSchema.optional(),
|
|
36694
36808
|
autonomyLevel: mrrlinAutonomyLevelSchema.default("minimal_human"),
|
|
36695
36809
|
autonomyReason: external_exports.string().min(1).max(240).default("Needs human review before automation."),
|
|
36696
36810
|
category: taxonomyCategorySchema,
|
|
@@ -36714,6 +36828,7 @@ var mrrlinTaskUpdateSchema = external_exports.object({
|
|
|
36714
36828
|
actor: external_exports.string().min(1).optional(),
|
|
36715
36829
|
assignee: mrrlinTaskAssigneeSchema.optional(),
|
|
36716
36830
|
autoDeploy: mrrlinAutoDeploySchema.optional(),
|
|
36831
|
+
domain: mrrlinTaskDomainSchema.optional(),
|
|
36717
36832
|
autonomyLevel: mrrlinAutonomyLevelSchema.optional(),
|
|
36718
36833
|
autonomyReason: external_exports.string().min(1).max(240).optional(),
|
|
36719
36834
|
category: taxonomyCategorySchema.optional(),
|
|
@@ -37347,6 +37462,43 @@ var mrrlinInboxItemActionPayloadSchema = external_exports.object({
|
|
|
37347
37462
|
// (worktree + push/PR path). Scheduler proposals set it; legacy payloads omit it.
|
|
37348
37463
|
codeExecution: external_exports.boolean().optional()
|
|
37349
37464
|
});
|
|
37465
|
+
var mrrlinInboxQuestionOptionSchema = external_exports.object({
|
|
37466
|
+
answer: external_exports.string().trim().min(1),
|
|
37467
|
+
description: external_exports.string().trim().min(1),
|
|
37468
|
+
label: external_exports.string().trim().min(1),
|
|
37469
|
+
recommended: external_exports.boolean().optional()
|
|
37470
|
+
});
|
|
37471
|
+
var mrrlinInboxQuestionOptionsSchema = external_exports.array(mrrlinInboxQuestionOptionSchema).min(1).max(3).superRefine((options, context) => {
|
|
37472
|
+
const labels = /* @__PURE__ */ new Set();
|
|
37473
|
+
const answers = /* @__PURE__ */ new Set();
|
|
37474
|
+
let recommendedCount = 0;
|
|
37475
|
+
for (const [index, option] of options.entries()) {
|
|
37476
|
+
if (labels.has(option.label)) {
|
|
37477
|
+
context.addIssue({
|
|
37478
|
+
code: external_exports.ZodIssueCode.custom,
|
|
37479
|
+
message: "questionOptions labels must be unique after trimming",
|
|
37480
|
+
path: [index, "label"]
|
|
37481
|
+
});
|
|
37482
|
+
}
|
|
37483
|
+
labels.add(option.label);
|
|
37484
|
+
if (answers.has(option.answer)) {
|
|
37485
|
+
context.addIssue({
|
|
37486
|
+
code: external_exports.ZodIssueCode.custom,
|
|
37487
|
+
message: "questionOptions answers must be unique after trimming",
|
|
37488
|
+
path: [index, "answer"]
|
|
37489
|
+
});
|
|
37490
|
+
}
|
|
37491
|
+
answers.add(option.answer);
|
|
37492
|
+
if (option.recommended === true)
|
|
37493
|
+
recommendedCount += 1;
|
|
37494
|
+
}
|
|
37495
|
+
if (recommendedCount > 1) {
|
|
37496
|
+
context.addIssue({
|
|
37497
|
+
code: external_exports.ZodIssueCode.custom,
|
|
37498
|
+
message: "questionOptions may mark at most one option as recommended"
|
|
37499
|
+
});
|
|
37500
|
+
}
|
|
37501
|
+
});
|
|
37350
37502
|
var mrrlinInboxItemSchema = external_exports.object({
|
|
37351
37503
|
actionPayload: mrrlinInboxItemActionPayloadSchema.nullable(),
|
|
37352
37504
|
answer: external_exports.string().nullable(),
|
|
@@ -37362,9 +37514,18 @@ var mrrlinInboxItemSchema = external_exports.object({
|
|
|
37362
37514
|
origin: external_exports.string().nullable().optional(),
|
|
37363
37515
|
planId: mrrlinPlanIdSchema.nullable().default(null),
|
|
37364
37516
|
projectSlug: mrrlinProjectSlugSchema,
|
|
37517
|
+
questionOptions: mrrlinInboxQuestionOptionsSchema.nullable().default(null),
|
|
37365
37518
|
readAt: external_exports.string().datetime().nullable(),
|
|
37366
37519
|
status: mrrlinInboxItemStatusSchema,
|
|
37367
37520
|
taskId: mrrlinTaskIdSchema.nullable()
|
|
37521
|
+
}).superRefine((value, context) => {
|
|
37522
|
+
if (value.kind !== "question" && value.questionOptions !== null) {
|
|
37523
|
+
context.addIssue({
|
|
37524
|
+
code: external_exports.ZodIssueCode.custom,
|
|
37525
|
+
message: "questionOptions are allowed only on question inbox items",
|
|
37526
|
+
path: ["questionOptions"]
|
|
37527
|
+
});
|
|
37528
|
+
}
|
|
37368
37529
|
});
|
|
37369
37530
|
var mrrlinInboxItemCreateSchema = external_exports.object({
|
|
37370
37531
|
actionPayload: mrrlinInboxItemActionPayloadSchema.nullable().optional(),
|
|
@@ -37374,7 +37535,16 @@ var mrrlinInboxItemCreateSchema = external_exports.object({
|
|
|
37374
37535
|
kind: mrrlinInboxItemKindSchema,
|
|
37375
37536
|
origin: external_exports.string().min(1).optional(),
|
|
37376
37537
|
planId: mrrlinPlanIdSchema.nullable().optional(),
|
|
37538
|
+
questionOptions: mrrlinInboxQuestionOptionsSchema.nullable().optional(),
|
|
37377
37539
|
taskId: mrrlinTaskIdSchema.nullable().optional()
|
|
37540
|
+
}).superRefine((value, context) => {
|
|
37541
|
+
if (value.kind !== "question" && value.questionOptions != null) {
|
|
37542
|
+
context.addIssue({
|
|
37543
|
+
code: external_exports.ZodIssueCode.custom,
|
|
37544
|
+
message: "questionOptions are allowed only on question inbox items",
|
|
37545
|
+
path: ["questionOptions"]
|
|
37546
|
+
});
|
|
37547
|
+
}
|
|
37378
37548
|
});
|
|
37379
37549
|
var mrrlinInboxItemDecideSchema = external_exports.object({
|
|
37380
37550
|
answer: external_exports.string().min(1).optional(),
|
|
@@ -38564,7 +38734,7 @@ Do NOT derail the current task - if the operator has an in-flight question, answ
|
|
|
38564
38734
|
}
|
|
38565
38735
|
|
|
38566
38736
|
// src/_generated/version.ts
|
|
38567
|
-
var PKG_VERSION = "0.3.
|
|
38737
|
+
var PKG_VERSION = "0.3.20";
|
|
38568
38738
|
|
|
38569
38739
|
// src/api-base-url.ts
|
|
38570
38740
|
var DEFAULT_API_BASE_URL = "http://127.0.0.1:8787";
|
|
@@ -38778,10 +38948,42 @@ function withTimeout(promise2, ms, message) {
|
|
|
38778
38948
|
});
|
|
38779
38949
|
}
|
|
38780
38950
|
async function awaitTurnWithDeadline(opts) {
|
|
38951
|
+
const hasIdle = opts.idleTimeoutMs != null && opts.idleTimeoutMessage != null;
|
|
38952
|
+
const overallDeadlineAt = Date.now() + opts.timeoutMs;
|
|
38953
|
+
let overallTimer = null;
|
|
38954
|
+
let idleTimer = null;
|
|
38955
|
+
let settled = false;
|
|
38956
|
+
const clearTimers = () => {
|
|
38957
|
+
settled = true;
|
|
38958
|
+
if (overallTimer) clearTimeout(overallTimer);
|
|
38959
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
38960
|
+
overallTimer = null;
|
|
38961
|
+
idleTimer = null;
|
|
38962
|
+
};
|
|
38963
|
+
const activity = { touch: () => {
|
|
38964
|
+
} };
|
|
38965
|
+
const idleTimeoutMs = opts.idleTimeoutMs;
|
|
38966
|
+
const idleTimeoutMessage = opts.idleTimeoutMessage;
|
|
38967
|
+
const deadline = new Promise((_, reject) => {
|
|
38968
|
+
overallTimer = setTimeout(() => reject(new Error(opts.timeoutMessage)), opts.timeoutMs);
|
|
38969
|
+
if (idleTimeoutMs != null && idleTimeoutMessage != null) {
|
|
38970
|
+
const armIdle = () => {
|
|
38971
|
+
if (settled) return;
|
|
38972
|
+
if (idleTimer) clearTimeout(idleTimer);
|
|
38973
|
+
idleTimer = setTimeout(() => {
|
|
38974
|
+
reject(new Error(Date.now() >= overallDeadlineAt ? opts.timeoutMessage : idleTimeoutMessage));
|
|
38975
|
+
}, idleTimeoutMs);
|
|
38976
|
+
};
|
|
38977
|
+
activity.touch = armIdle;
|
|
38978
|
+
armIdle();
|
|
38979
|
+
}
|
|
38980
|
+
});
|
|
38781
38981
|
try {
|
|
38782
|
-
await
|
|
38982
|
+
await Promise.race([opts.run(activity), deadline]);
|
|
38783
38983
|
} catch (error51) {
|
|
38784
|
-
|
|
38984
|
+
clearTimers();
|
|
38985
|
+
const message = error51 instanceof Error ? error51.message : String(error51);
|
|
38986
|
+
const isDeadline = message === opts.timeoutMessage || hasIdle && message === opts.idleTimeoutMessage;
|
|
38785
38987
|
if (isDeadline) {
|
|
38786
38988
|
try {
|
|
38787
38989
|
await withTimeout(
|
|
@@ -38795,6 +38997,7 @@ async function awaitTurnWithDeadline(opts) {
|
|
|
38795
38997
|
}
|
|
38796
38998
|
throw error51;
|
|
38797
38999
|
}
|
|
39000
|
+
clearTimers();
|
|
38798
39001
|
}
|
|
38799
39002
|
|
|
38800
39003
|
// src/worktree.ts
|
|
@@ -38987,9 +39190,12 @@ ${lines2.join("\n")}`;
|
|
|
38987
39190
|
var DEFAULT_STALE_SECONDS = 300;
|
|
38988
39191
|
var DEFAULT_TOUCH_INTERVAL_MS = 6e4;
|
|
38989
39192
|
var DEFAULT_CLAIM_INTERVAL_SECONDS = 30;
|
|
38990
|
-
var
|
|
39193
|
+
var DEFAULT_EXEC_TURN_IDLE_TIMEOUT_SECONDS = 600;
|
|
39194
|
+
var DEFAULT_EXEC_RUN_MAX_SECONDS = 3600;
|
|
38991
39195
|
var DEFAULT_CONTEXT_MAX_ARTIFACTS = 50;
|
|
38992
39196
|
var EXECUTION_TURN_TIMEOUT_PREFIX = "Execution turn did not complete within";
|
|
39197
|
+
var EXECUTION_TURN_STALL_PREFIX = "Execution turn stalled: no Codex activity within";
|
|
39198
|
+
var EXECUTION_TURN_BUDGET_PREFIX = "Execution turn exceeded the run wall-clock budget";
|
|
38993
39199
|
var EXECUTION_TURN_TIMEOUT_RETRY_CODE = "execution_turn_timeout_retry";
|
|
38994
39200
|
var EXECUTION_TURN_TIMEOUT_RETRY_SUMMARY = "Execution turn timed out; requeued for one automatic retry.";
|
|
38995
39201
|
var EXECUTION_TURN_TIMEOUT_RETRY_CURSOR = "execution_turn_timeout_retry:1";
|
|
@@ -38999,14 +39205,26 @@ function readPositiveIntEnv(name, fallback) {
|
|
|
38999
39205
|
const parsed = Number.parseInt(raw, 10);
|
|
39000
39206
|
return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
|
|
39001
39207
|
}
|
|
39002
|
-
function
|
|
39003
|
-
return readPositiveIntEnv("
|
|
39208
|
+
function execTurnIdleTimeoutMs() {
|
|
39209
|
+
return readPositiveIntEnv("MRRLIN_DIRECTOR_BRIDGE_EXEC_TURN_IDLE_TIMEOUT_SECONDS", DEFAULT_EXEC_TURN_IDLE_TIMEOUT_SECONDS) * 1e3;
|
|
39210
|
+
}
|
|
39211
|
+
function execRunMaxMs() {
|
|
39212
|
+
return readPositiveIntEnv("MRRLIN_DIRECTOR_BRIDGE_EXEC_RUN_MAX_SECONDS", DEFAULT_EXEC_RUN_MAX_SECONDS) * 1e3;
|
|
39213
|
+
}
|
|
39214
|
+
function explicitLegacyTurnTimeoutMs() {
|
|
39215
|
+
const raw = (process.env.MRRLIN_DIRECTOR_BRIDGE_EXEC_TURN_TIMEOUT_SECONDS ?? "").trim();
|
|
39216
|
+
if (!raw) return null;
|
|
39217
|
+
const parsed = Number.parseInt(raw, 10);
|
|
39218
|
+
return Number.isInteger(parsed) && parsed > 0 ? parsed * 1e3 : null;
|
|
39219
|
+
}
|
|
39220
|
+
function executionTurnStallMessage(idleMs) {
|
|
39221
|
+
return `${EXECUTION_TURN_STALL_PREFIX} ${idleMs}ms.`;
|
|
39004
39222
|
}
|
|
39005
|
-
function
|
|
39006
|
-
return `${
|
|
39223
|
+
function executionTurnBudgetMessage(budgetMs) {
|
|
39224
|
+
return `${EXECUTION_TURN_BUDGET_PREFIX} (${budgetMs}ms).`;
|
|
39007
39225
|
}
|
|
39008
39226
|
function isExecutionTurnTimeoutReason(reason) {
|
|
39009
|
-
return reason.startsWith(EXECUTION_TURN_TIMEOUT_PREFIX);
|
|
39227
|
+
return reason.startsWith(EXECUTION_TURN_TIMEOUT_PREFIX) || reason.startsWith(EXECUTION_TURN_STALL_PREFIX);
|
|
39010
39228
|
}
|
|
39011
39229
|
function contextMaxArtifacts() {
|
|
39012
39230
|
return readPositiveIntEnv("MRRLIN_DIRECTOR_BRIDGE_CONTEXT_MAX_ARTIFACTS", DEFAULT_CONTEXT_MAX_ARTIFACTS);
|
|
@@ -39159,11 +39377,15 @@ async function runHasLease(deps, projectSlug, runId, leaseId) {
|
|
|
39159
39377
|
return false;
|
|
39160
39378
|
}
|
|
39161
39379
|
}
|
|
39162
|
-
async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inputText) {
|
|
39380
|
+
async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inputText, overallMs, phase) {
|
|
39163
39381
|
const mapper = new CodexBridgeEventMapper();
|
|
39164
39382
|
mapper.reset();
|
|
39165
39383
|
let turnError = null;
|
|
39166
39384
|
let interruptFailed = false;
|
|
39385
|
+
let touch = () => {
|
|
39386
|
+
};
|
|
39387
|
+
const turnStartedAt = Date.now();
|
|
39388
|
+
let lastActivityAt = turnStartedAt;
|
|
39167
39389
|
const persist = async (event) => {
|
|
39168
39390
|
if (event.type === "error") turnError = event.content;
|
|
39169
39391
|
const kind = artifactKindForEvent(event);
|
|
@@ -39185,6 +39407,11 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
|
|
|
39185
39407
|
});
|
|
39186
39408
|
let acceptedTurnId = null;
|
|
39187
39409
|
const unsub = codex.onNotification((method, params) => {
|
|
39410
|
+
const notifThreadId = params?.threadId;
|
|
39411
|
+
if (!notifThreadId || notifThreadId === threadId) {
|
|
39412
|
+
lastActivityAt = Date.now();
|
|
39413
|
+
touch();
|
|
39414
|
+
}
|
|
39188
39415
|
const events = mapper.mapNotification(
|
|
39189
39416
|
{ method, params },
|
|
39190
39417
|
threadId
|
|
@@ -39192,9 +39419,8 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
|
|
|
39192
39419
|
for (const event of events) pending.push(persist(event));
|
|
39193
39420
|
if (method === "turn/completed") {
|
|
39194
39421
|
const p = params;
|
|
39195
|
-
const notifThreadId = p?.threadId;
|
|
39196
39422
|
const notifTurnId = p?.turn?.id;
|
|
39197
|
-
const threadMatches = !
|
|
39423
|
+
const threadMatches = !p?.threadId || p.threadId === threadId;
|
|
39198
39424
|
const turnMatches = !acceptedTurnId || !notifTurnId || notifTurnId === acceptedTurnId;
|
|
39199
39425
|
if (threadMatches && turnMatches) resolveTurnEnded?.();
|
|
39200
39426
|
}
|
|
@@ -39204,10 +39430,11 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
|
|
|
39204
39430
|
});
|
|
39205
39431
|
}, deps.touchIntervalMs);
|
|
39206
39432
|
touchTimer.unref?.();
|
|
39433
|
+
const idleMs = execTurnIdleTimeoutMs();
|
|
39207
39434
|
try {
|
|
39208
|
-
const timeoutMs = execTurnTimeoutMs();
|
|
39209
39435
|
await awaitTurnWithDeadline({
|
|
39210
|
-
run: async () => {
|
|
39436
|
+
run: async (activity) => {
|
|
39437
|
+
touch = activity.touch;
|
|
39211
39438
|
const accepted = await codex.turn.start({
|
|
39212
39439
|
threadId,
|
|
39213
39440
|
input: [{ type: "text", text: inputText }]
|
|
@@ -39216,8 +39443,10 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
|
|
|
39216
39443
|
await turnEnded;
|
|
39217
39444
|
},
|
|
39218
39445
|
interrupt: () => codex.turn.interrupt({ threadId }),
|
|
39219
|
-
timeoutMs,
|
|
39220
|
-
timeoutMessage:
|
|
39446
|
+
timeoutMs: overallMs,
|
|
39447
|
+
timeoutMessage: executionTurnBudgetMessage(overallMs),
|
|
39448
|
+
idleTimeoutMs: idleMs,
|
|
39449
|
+
idleTimeoutMessage: executionTurnStallMessage(idleMs),
|
|
39221
39450
|
onInterruptFailed: () => {
|
|
39222
39451
|
interruptFailed = true;
|
|
39223
39452
|
}
|
|
@@ -39226,6 +39455,19 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
|
|
|
39226
39455
|
await Promise.all(pending);
|
|
39227
39456
|
} catch (error51) {
|
|
39228
39457
|
turnError = error51 instanceof Error ? error51.message : String(error51);
|
|
39458
|
+
if (turnError.startsWith(EXECUTION_TURN_STALL_PREFIX) || turnError.startsWith(EXECUTION_TURN_BUDGET_PREFIX)) {
|
|
39459
|
+
console.warn(
|
|
39460
|
+
JSON.stringify({
|
|
39461
|
+
event: "execution_turn_deadline",
|
|
39462
|
+
projectSlug,
|
|
39463
|
+
runId,
|
|
39464
|
+
phase,
|
|
39465
|
+
kind: turnError.startsWith(EXECUTION_TURN_BUDGET_PREFIX) ? "budget" : "stall",
|
|
39466
|
+
elapsedMs: Date.now() - turnStartedAt,
|
|
39467
|
+
sinceLastActivityMs: Date.now() - lastActivityAt
|
|
39468
|
+
})
|
|
39469
|
+
);
|
|
39470
|
+
}
|
|
39229
39471
|
} finally {
|
|
39230
39472
|
clearInterval(touchTimer);
|
|
39231
39473
|
unsub();
|
|
@@ -39247,7 +39489,38 @@ async function driveRun(deps, projectSlug, runId) {
|
|
|
39247
39489
|
return;
|
|
39248
39490
|
}
|
|
39249
39491
|
const leaseId = claimed.leaseId;
|
|
39492
|
+
const attemptDeadlineMs = Date.now() + execRunMaxMs();
|
|
39493
|
+
const remainingTurnBudgetMs = () => {
|
|
39494
|
+
const remaining = attemptDeadlineMs - Date.now();
|
|
39495
|
+
const legacy = explicitLegacyTurnTimeoutMs();
|
|
39496
|
+
return legacy != null ? Math.min(legacy, remaining) : remaining;
|
|
39497
|
+
};
|
|
39250
39498
|
const task = await deps.client.getTask(projectSlug, claimed.taskId);
|
|
39499
|
+
const blockerTasks = task.blockedBy.length ? await Promise.all(task.blockedBy.map((blockerTaskId) => deps.client.getTask(projectSlug, blockerTaskId))) : [];
|
|
39500
|
+
const missingBlockers = task.blockedBy.filter((_, index) => !blockerTasks[index]);
|
|
39501
|
+
if (missingBlockers.length > 0) {
|
|
39502
|
+
await failRun(
|
|
39503
|
+
deps,
|
|
39504
|
+
projectSlug,
|
|
39505
|
+
task,
|
|
39506
|
+
runId,
|
|
39507
|
+
leaseId,
|
|
39508
|
+
`task_blocked: run ${runId} targets ${task.id} but blocker tasks could not be loaded: ${missingBlockers.join(", ")}`
|
|
39509
|
+
);
|
|
39510
|
+
return;
|
|
39511
|
+
}
|
|
39512
|
+
const activeBlockers = getActiveBlockedByIds(task.blockedBy, blockerTasks);
|
|
39513
|
+
if (activeBlockers.length > 0) {
|
|
39514
|
+
await failRun(
|
|
39515
|
+
deps,
|
|
39516
|
+
projectSlug,
|
|
39517
|
+
task,
|
|
39518
|
+
runId,
|
|
39519
|
+
leaseId,
|
|
39520
|
+
`task_blocked: run ${runId} targets ${task.id} but active blockers remain open: ${activeBlockers.join(", ")}`
|
|
39521
|
+
);
|
|
39522
|
+
return;
|
|
39523
|
+
}
|
|
39251
39524
|
const compactEnabled = (process.env.MRRLIN_MCP_COMPACT_DISABLE ?? "").trim() !== "1";
|
|
39252
39525
|
const context = await gatherContext(deps.client, projectSlug, task, runId);
|
|
39253
39526
|
const bundle = renderContextBundle(
|
|
@@ -39269,7 +39542,11 @@ async function driveRun(deps, projectSlug, runId) {
|
|
|
39269
39542
|
};
|
|
39270
39543
|
if (claimed.codeExecution) {
|
|
39271
39544
|
if (!checkout) {
|
|
39272
|
-
const reason = `local_checkout_missing: run ${runId}
|
|
39545
|
+
const reason = `local_checkout_missing: run ${runId} needs code execution, but project ${projectSlug} has no local git checkout registered on this machine, so there is nowhere to make the changes. Explain this to the operator in plain, non-technical language, then proactively help register one:
|
|
39546
|
+
1. Search the usual locations (~/dev, ~/code, ~/work, ~/projects) for a clone of this project's GitHub repo, matching by its \`git remote origin\` URL.
|
|
39547
|
+
2. If you find a likely match, tell the operator where you think it is and ask them to confirm \u2014 e.g. "For code changes I need this project's repo checked out here. I think it's at <path> \u2014 should I register that one, or do you want to point me at a different path?"
|
|
39548
|
+
3. When the operator confirms a path, call register_local_checkout({ projectSlug: "${projectSlug}", path: "<absolute path>", repo: "<this project's GitHub repo>" }).
|
|
39549
|
+
4. If no candidate is found, ask the operator for the absolute path to the checkout (or to clone the repo first), then register it. Once a checkout is registered, the run can be re-queued.`;
|
|
39273
39550
|
await failRun(deps, projectSlug, task, runId, leaseId, reason);
|
|
39274
39551
|
return;
|
|
39275
39552
|
}
|
|
@@ -39284,6 +39561,7 @@ async function driveRun(deps, projectSlug, runId) {
|
|
|
39284
39561
|
runId,
|
|
39285
39562
|
defaultBranch: await getDefaultBranch(projectSlug)
|
|
39286
39563
|
});
|
|
39564
|
+
if (!await runHasLease(deps, projectSlug, runId, leaseId)) return;
|
|
39287
39565
|
await deps.client.updateExecutionRun(projectSlug, runId, {
|
|
39288
39566
|
worktreeId: worktreePath,
|
|
39289
39567
|
branch: worktreeBranch(runId)
|
|
@@ -39334,11 +39612,17 @@ async function driveRun(deps, projectSlug, runId) {
|
|
|
39334
39612
|
ephemeral: null
|
|
39335
39613
|
},
|
|
39336
39614
|
onThreadIdChanged: async (newId) => {
|
|
39615
|
+
if (!await runHasLease(deps, projectSlug, runId, leaseId)) return;
|
|
39337
39616
|
await deps.client.updateTask(projectSlug, task.id, { directorThreadId: newId });
|
|
39338
39617
|
}
|
|
39339
39618
|
});
|
|
39340
39619
|
if (!resumed) {
|
|
39341
|
-
const
|
|
39620
|
+
const primingBudgetMs = remainingTurnBudgetMs();
|
|
39621
|
+
if (primingBudgetMs <= 0) {
|
|
39622
|
+
await failRun(deps, projectSlug, task, runId, leaseId, executionTurnBudgetMessage(execRunMaxMs()));
|
|
39623
|
+
return;
|
|
39624
|
+
}
|
|
39625
|
+
const primed = await driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, bundle, primingBudgetMs, "priming");
|
|
39342
39626
|
if (!primed.ok) {
|
|
39343
39627
|
await resetWedgedThread(deps, projectSlug, task.id, primed.interruptFailed);
|
|
39344
39628
|
await failRun(deps, projectSlug, task, runId, leaseId, primed.error);
|
|
@@ -39348,7 +39632,12 @@ async function driveRun(deps, projectSlug, runId) {
|
|
|
39348
39632
|
const executionPrompt = resumed ? `${bundle}
|
|
39349
39633
|
|
|
39350
39634
|
Continue executing task ${task.id}. Make the next concrete increment of progress and record artifacts/judgements via MCP tools.` : `Continue executing task ${task.id}. Make the next concrete increment of progress and record artifacts/judgements via MCP tools.`;
|
|
39351
|
-
const
|
|
39635
|
+
const workBudgetMs = remainingTurnBudgetMs();
|
|
39636
|
+
if (workBudgetMs <= 0) {
|
|
39637
|
+
await failRun(deps, projectSlug, task, runId, leaseId, executionTurnBudgetMessage(execRunMaxMs()));
|
|
39638
|
+
return;
|
|
39639
|
+
}
|
|
39640
|
+
const outcome = await driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, executionPrompt, workBudgetMs, "work");
|
|
39352
39641
|
if (!outcome.ok) {
|
|
39353
39642
|
await resetWedgedThread(deps, projectSlug, task.id, outcome.interruptFailed);
|
|
39354
39643
|
await failRun(deps, projectSlug, task, runId, leaseId, outcome.error);
|
|
@@ -39903,9 +40192,15 @@ function saveThreadIndex(stateDir, entries) {
|
|
|
39903
40192
|
function createInactivityWatchdog(opts) {
|
|
39904
40193
|
const setT = opts.setTimeoutFn ?? setTimeout;
|
|
39905
40194
|
const clearT = opts.clearTimeoutFn ?? clearTimeout;
|
|
40195
|
+
const now = opts.now ?? Date.now;
|
|
40196
|
+
const suspended = opts.suspendedMs ?? (() => 0);
|
|
39906
40197
|
let idleTimer = null;
|
|
39907
40198
|
let maxTimer = null;
|
|
39908
40199
|
let done = false;
|
|
40200
|
+
const startAt = now();
|
|
40201
|
+
const suspendedAtStart = suspended();
|
|
40202
|
+
let lastPokeAt = startAt;
|
|
40203
|
+
let suspendedAtLastPoke = suspendedAtStart;
|
|
39909
40204
|
const expire = (reason) => {
|
|
39910
40205
|
if (done) return;
|
|
39911
40206
|
done = true;
|
|
@@ -39913,16 +40208,41 @@ function createInactivityWatchdog(opts) {
|
|
|
39913
40208
|
if (maxTimer) clearT(maxTimer);
|
|
39914
40209
|
opts.onExpire(reason);
|
|
39915
40210
|
};
|
|
39916
|
-
|
|
39917
|
-
|
|
39918
|
-
|
|
39919
|
-
|
|
40211
|
+
const armIdle = (ms) => {
|
|
40212
|
+
idleTimer = setT(onIdleDeadline, Math.max(1, ms));
|
|
40213
|
+
idleTimer.unref?.();
|
|
40214
|
+
};
|
|
40215
|
+
const armMax = (ms) => {
|
|
40216
|
+
maxTimer = setT(onMaxDeadline, Math.max(1, ms));
|
|
40217
|
+
maxTimer.unref?.();
|
|
40218
|
+
};
|
|
40219
|
+
const onIdleDeadline = () => {
|
|
40220
|
+
if (done) return;
|
|
40221
|
+
const awakeSilence = now() - lastPokeAt - (suspended() - suspendedAtLastPoke);
|
|
40222
|
+
if (awakeSilence < opts.idleMs) {
|
|
40223
|
+
armIdle(opts.idleMs - awakeSilence);
|
|
40224
|
+
return;
|
|
40225
|
+
}
|
|
40226
|
+
expire("idle");
|
|
40227
|
+
};
|
|
40228
|
+
const onMaxDeadline = () => {
|
|
40229
|
+
if (done) return;
|
|
40230
|
+
const awakeElapsed = now() - startAt - (suspended() - suspendedAtStart);
|
|
40231
|
+
if (awakeElapsed < opts.maxMs) {
|
|
40232
|
+
armMax(opts.maxMs - awakeElapsed);
|
|
40233
|
+
return;
|
|
40234
|
+
}
|
|
40235
|
+
expire("max");
|
|
40236
|
+
};
|
|
40237
|
+
armIdle(opts.idleMs);
|
|
40238
|
+
armMax(opts.maxMs);
|
|
39920
40239
|
return {
|
|
39921
40240
|
poke: () => {
|
|
39922
40241
|
if (done) return;
|
|
39923
40242
|
if (idleTimer) clearT(idleTimer);
|
|
39924
|
-
|
|
39925
|
-
|
|
40243
|
+
lastPokeAt = now();
|
|
40244
|
+
suspendedAtLastPoke = suspended();
|
|
40245
|
+
armIdle(opts.idleMs);
|
|
39926
40246
|
},
|
|
39927
40247
|
cancel: () => {
|
|
39928
40248
|
done = true;
|
|
@@ -44098,6 +44418,7 @@ function createBridgeLogger(stateDir, env = process.env) {
|
|
|
44098
44418
|
}
|
|
44099
44419
|
|
|
44100
44420
|
// src/director-bridge.ts
|
|
44421
|
+
var JSON_GPT_MODEL_REF_RE = /"(?:slug|model)"\s*:\s*"(gpt-[a-z0-9.-]+)"/gi;
|
|
44101
44422
|
var DEFAULT_DIRECTOR_BRIDGE_ALLOWED_ORIGINS = [
|
|
44102
44423
|
"http://localhost:*",
|
|
44103
44424
|
"http://127.0.0.1:*",
|
|
@@ -44118,6 +44439,139 @@ function normalizeModel(value) {
|
|
|
44118
44439
|
const trimmed = value.trim();
|
|
44119
44440
|
return trimmed ? trimmed : null;
|
|
44120
44441
|
}
|
|
44442
|
+
function isSelectableModelSlug(value) {
|
|
44443
|
+
return typeof value === "string" && value.startsWith("gpt-");
|
|
44444
|
+
}
|
|
44445
|
+
function humanizeModelSlug(slug) {
|
|
44446
|
+
if (!slug.startsWith("gpt-")) return slug;
|
|
44447
|
+
const rest = slug.slice(4);
|
|
44448
|
+
if (!rest) return "GPT";
|
|
44449
|
+
const [version2, ...variants] = rest.split("-").filter(Boolean);
|
|
44450
|
+
if (variants.length === 0) return `GPT-${version2}`;
|
|
44451
|
+
return `GPT-${version2} ${variants.map((part) => part.charAt(0).toUpperCase() + part.slice(1)).join(" ")}`;
|
|
44452
|
+
}
|
|
44453
|
+
function mapBridgeModel(raw) {
|
|
44454
|
+
const model = (raw.model ?? raw.id ?? "").trim();
|
|
44455
|
+
if (!model) return null;
|
|
44456
|
+
return {
|
|
44457
|
+
id: (raw.id ?? model).trim() || model,
|
|
44458
|
+
model,
|
|
44459
|
+
displayName: (raw.displayName ?? model).trim() || model,
|
|
44460
|
+
description: raw.description ?? "",
|
|
44461
|
+
supportedReasoningEfforts: (raw.supportedReasoningEfforts ?? []).map((entry) => ({
|
|
44462
|
+
reasoningEffort: entry.reasoningEffort,
|
|
44463
|
+
description: entry.description ?? ""
|
|
44464
|
+
})),
|
|
44465
|
+
defaultReasoningEffort: raw.defaultReasoningEffort ?? null,
|
|
44466
|
+
isDefault: raw.isDefault === true
|
|
44467
|
+
};
|
|
44468
|
+
}
|
|
44469
|
+
function addBridgeModel(target, model) {
|
|
44470
|
+
if (!model) return;
|
|
44471
|
+
if (!target.has(model.model)) target.set(model.model, model);
|
|
44472
|
+
}
|
|
44473
|
+
function readTextIfExists(filePath) {
|
|
44474
|
+
try {
|
|
44475
|
+
return import_node_fs10.default.readFileSync(filePath, "utf8");
|
|
44476
|
+
} catch {
|
|
44477
|
+
return null;
|
|
44478
|
+
}
|
|
44479
|
+
}
|
|
44480
|
+
function readKnownGptSlugsFromTomlFile(filePath) {
|
|
44481
|
+
const raw = readTextIfExists(filePath);
|
|
44482
|
+
if (!raw) return [];
|
|
44483
|
+
return Array.from(raw.matchAll(/^\s*model\s*=\s*"([^"]+)"/gm)).map((match) => match[1]?.trim() ?? "").filter(isSelectableModelSlug);
|
|
44484
|
+
}
|
|
44485
|
+
function readCodexFallbackModels() {
|
|
44486
|
+
const codexHome = process.env.CODEX_HOME?.trim() || import_node_path11.default.join(import_node_os7.default.homedir(), ".codex");
|
|
44487
|
+
const models = /* @__PURE__ */ new Map();
|
|
44488
|
+
const cachePath = import_node_path11.default.join(codexHome, "models_cache.json");
|
|
44489
|
+
const cacheRaw = readTextIfExists(cachePath);
|
|
44490
|
+
if (cacheRaw) {
|
|
44491
|
+
try {
|
|
44492
|
+
const parsed = JSON.parse(cacheRaw);
|
|
44493
|
+
if (Array.isArray(parsed.models)) {
|
|
44494
|
+
for (const entry of parsed.models) {
|
|
44495
|
+
if (!entry || typeof entry !== "object") continue;
|
|
44496
|
+
const cached2 = entry;
|
|
44497
|
+
const slug = typeof cached2.slug === "string" ? cached2.slug.trim() : "";
|
|
44498
|
+
if (!isSelectableModelSlug(slug)) continue;
|
|
44499
|
+
const supportedReasoningEfforts = Array.isArray(cached2.supported_reasoning_levels) ? cached2.supported_reasoning_levels.flatMap((level) => {
|
|
44500
|
+
if (!level || typeof level !== "object") return [];
|
|
44501
|
+
const option = level;
|
|
44502
|
+
if (typeof option.effort !== "string" || !option.effort.trim()) return [];
|
|
44503
|
+
return [{ reasoningEffort: option.effort, description: typeof option.description === "string" ? option.description : "" }];
|
|
44504
|
+
}) : [];
|
|
44505
|
+
addBridgeModel(
|
|
44506
|
+
models,
|
|
44507
|
+
mapBridgeModel({
|
|
44508
|
+
id: slug,
|
|
44509
|
+
model: slug,
|
|
44510
|
+
displayName: typeof cached2.display_name === "string" ? cached2.display_name : humanizeModelSlug(slug),
|
|
44511
|
+
description: typeof cached2.description === "string" ? cached2.description : "",
|
|
44512
|
+
supportedReasoningEfforts,
|
|
44513
|
+
defaultReasoningEffort: typeof cached2.default_reasoning_level === "string" ? cached2.default_reasoning_level : null,
|
|
44514
|
+
isDefault: false
|
|
44515
|
+
})
|
|
44516
|
+
);
|
|
44517
|
+
}
|
|
44518
|
+
}
|
|
44519
|
+
} catch {
|
|
44520
|
+
}
|
|
44521
|
+
for (const match of cacheRaw.matchAll(JSON_GPT_MODEL_REF_RE)) {
|
|
44522
|
+
const slug = match[1]?.trim().toLowerCase() ?? "";
|
|
44523
|
+
if (!isSelectableModelSlug(slug)) continue;
|
|
44524
|
+
addBridgeModel(
|
|
44525
|
+
models,
|
|
44526
|
+
mapBridgeModel({
|
|
44527
|
+
id: slug,
|
|
44528
|
+
model: slug,
|
|
44529
|
+
displayName: humanizeModelSlug(slug),
|
|
44530
|
+
description: "",
|
|
44531
|
+
supportedReasoningEfforts: [],
|
|
44532
|
+
defaultReasoningEffort: null,
|
|
44533
|
+
isDefault: false
|
|
44534
|
+
})
|
|
44535
|
+
);
|
|
44536
|
+
}
|
|
44537
|
+
}
|
|
44538
|
+
for (const slug of readKnownGptSlugsFromTomlFile(import_node_path11.default.join(codexHome, "config.toml"))) {
|
|
44539
|
+
addBridgeModel(
|
|
44540
|
+
models,
|
|
44541
|
+
mapBridgeModel({
|
|
44542
|
+
id: slug,
|
|
44543
|
+
model: slug,
|
|
44544
|
+
displayName: humanizeModelSlug(slug),
|
|
44545
|
+
description: "",
|
|
44546
|
+
supportedReasoningEfforts: [],
|
|
44547
|
+
defaultReasoningEffort: null,
|
|
44548
|
+
isDefault: false
|
|
44549
|
+
})
|
|
44550
|
+
);
|
|
44551
|
+
}
|
|
44552
|
+
const automationsDir = import_node_path11.default.join(codexHome, "automations");
|
|
44553
|
+
try {
|
|
44554
|
+
for (const entry of import_node_fs10.default.readdirSync(automationsDir, { withFileTypes: true })) {
|
|
44555
|
+
if (!entry.isDirectory()) continue;
|
|
44556
|
+
for (const slug of readKnownGptSlugsFromTomlFile(import_node_path11.default.join(automationsDir, entry.name, "automation.toml"))) {
|
|
44557
|
+
addBridgeModel(
|
|
44558
|
+
models,
|
|
44559
|
+
mapBridgeModel({
|
|
44560
|
+
id: slug,
|
|
44561
|
+
model: slug,
|
|
44562
|
+
displayName: humanizeModelSlug(slug),
|
|
44563
|
+
description: "",
|
|
44564
|
+
supportedReasoningEfforts: [],
|
|
44565
|
+
defaultReasoningEffort: null,
|
|
44566
|
+
isDefault: false
|
|
44567
|
+
})
|
|
44568
|
+
);
|
|
44569
|
+
}
|
|
44570
|
+
}
|
|
44571
|
+
} catch {
|
|
44572
|
+
}
|
|
44573
|
+
return [...models.values()];
|
|
44574
|
+
}
|
|
44121
44575
|
function neutralCheckoutCwd(stateDir) {
|
|
44122
44576
|
const dir = import_node_path11.default.join(stateDir, "no-checkout");
|
|
44123
44577
|
try {
|
|
@@ -44145,13 +44599,16 @@ function buildThreadStartParams(msg, config2, desiredModel) {
|
|
|
44145
44599
|
"request behind the task (pure system bookkeeping) \u2014 note that omitting it makes the gate fail-closed.",
|
|
44146
44600
|
"",
|
|
44147
44601
|
"Spec authoring contract \u2014 when creating or updating a `Specs/{id}` wiki page:",
|
|
44602
|
+
"- BEGIN the page with YAML frontmatter naming the gated task: a first line `---`, then",
|
|
44603
|
+
" `task: {taskId}` (the task this spec gates, e.g. `task: GT-1023`), then `---`. Never change it",
|
|
44604
|
+
" to another task id \u2014 it is the machine-readable identity the consensus gate validates against.",
|
|
44148
44605
|
"- In `## Specification`, write each requirement as `### Requirement: <name>` followed by one",
|
|
44149
44606
|
" SHALL sentence stating the externally visible behavior.",
|
|
44150
44607
|
"- Give every requirement at least one `#### Scenario: <name>` with `- WHEN ...` / `- THEN ...`",
|
|
44151
44608
|
" bullets (`- AND ...` as needed). Scenarios make the spec verifiable.",
|
|
44152
44609
|
"- Keep behavior contracts in the spec; implementation details belong in `## Implementation Plan`.",
|
|
44153
44610
|
"- Apply this format ONLY to Specs/{id} pages; do not retro-fit it onto other wiki page types.",
|
|
44154
|
-
"- Spec consensus runs ASYNCHRONOUSLY. Call `run_spec_consensus` for a `Specs/{id}` page; it returns IMMEDIATELY. On `already_converged`, the spec is already marked ready \u2014 proceed straight to `create_execution_run`; do NOT call `mark_spec_ready` and do NOT re-run consensus. On `queued` or `already_running`, tell the user it was launched and END your turn \u2014 do NOT poll or call it again; the result arrives in the inbox (an `info` item on CONVERGED, a `handoff` item if it was blocked/failed). When a consensus `info` item says CONVERGED, proceed to `create_execution_run`. When a `handoff` says it was blocked, address the recorded issues in the spec (any edit re-arms the gate) and call `run_spec_consensus` once more.",
|
|
44611
|
+
"- Spec consensus runs ASYNCHRONOUSLY. Call `run_spec_consensus` for a `Specs/{id}` page; it returns IMMEDIATELY. On `already_converged`, the spec is already marked ready \u2014 proceed straight to `create_execution_run`; do NOT call `mark_spec_ready` and do NOT re-run consensus. On `queued` or `already_running`, tell the user it was launched and END your turn \u2014 do NOT poll or call it again; the result arrives in the inbox (an `info` item on CONVERGED, a `handoff` item if it was blocked/failed). When a consensus `info` item says CONVERGED, proceed to `create_execution_run`. When a `handoff` says it was blocked, address the recorded issues in the spec (any edit re-arms the gate) and call `run_spec_consensus` once more. For a NON-code task (domain seo/marketing/content, ADR 0020) the spec is review-only \u2014 do NOT call `create_execution_run` (non-code execution is not yet supported and fail-closes); tell the operator the spec was reviewed.",
|
|
44155
44612
|
'- When a task needs a browser (login flows, UI verification/clicks, scraping), pass `prerequisites:[{mcp:"mrrlin-browser",reason:"..."}]` to `create_execution_run`. If that run later fails into a handoff, the operator gets a one-click "Launch browser & resume" that brings up the dedicated browser as the turn starts.',
|
|
44156
44613
|
"- When you actually drive a browser (calling `browser_navigate`, `browser_click`, `browser_snapshot`, `browser_type`, etc.), invoke those tools from the **`mrrlin-browser`** MCP server ONLY. Do NOT use tools from any other browser MCP (`playwright`, `chrome-devtools`, `puppeteer`, etc.) even if they are also connected on this operator's box \u2014 those use a shared/default Playwright profile that is fragile across runs. Only `mrrlin-browser` uses this project's dedicated Chromium + persistent operator profile (`~/.mrrlin/browser/profile`). If your host exposes multiple browser MCPs with overlapping tool names, prefer the `mrrlin-browser` server unconditionally.",
|
|
44157
44614
|
"",
|
|
@@ -44220,8 +44677,12 @@ function buildThreadStartParams(msg, config2, desiredModel) {
|
|
|
44220
44677
|
" assumption on the spec page.",
|
|
44221
44678
|
"- For any such task-affecting question, ALSO bank it durably before your final reply:",
|
|
44222
44679
|
' `create_inbox_item` with `kind="question"`, `taskId` set, `directorSessionId` from the',
|
|
44223
|
-
" Context JSON,
|
|
44224
|
-
"
|
|
44680
|
+
" Context JSON, `body` = the exact question, and `questionOptions` = 2-3 concrete answer",
|
|
44681
|
+
" options. Each option MUST include a short `label`, a concise `description`, and a",
|
|
44682
|
+
" self-contained durable `answer`; you MAY mark at most one option as `recommended` when",
|
|
44683
|
+
" you explicitly prefer a path. The operator always has a custom-answer path in the UI, so",
|
|
44684
|
+
" your options should guide, not trap. (The server dedups identical open questions; do not",
|
|
44685
|
+
" supply a dedup key.)",
|
|
44225
44686
|
"- When the operator answers in chat, write the answer into the Specs/{taskId}",
|
|
44226
44687
|
' Clarifications section, then `decide_inbox_item` the question with `verdict="acknowledged"`',
|
|
44227
44688
|
" and `answer` = the resolved answer.",
|
|
@@ -44552,6 +45013,24 @@ function attachBridgeWss(server, opts) {
|
|
|
44552
45013
|
return;
|
|
44553
45014
|
}
|
|
44554
45015
|
const span = { id: "conn", at: 0 };
|
|
45016
|
+
let isAlive = true;
|
|
45017
|
+
socket.on("pong", () => {
|
|
45018
|
+
isAlive = true;
|
|
45019
|
+
});
|
|
45020
|
+
const pingTimer = setInterval(() => {
|
|
45021
|
+
if (socket.readyState !== socket.OPEN) return;
|
|
45022
|
+
if (!isAlive) {
|
|
45023
|
+
socket.terminate();
|
|
45024
|
+
return;
|
|
45025
|
+
}
|
|
45026
|
+
isAlive = false;
|
|
45027
|
+
try {
|
|
45028
|
+
socket.ping();
|
|
45029
|
+
} catch {
|
|
45030
|
+
}
|
|
45031
|
+
}, wsPingMs());
|
|
45032
|
+
pingTimer.unref?.();
|
|
45033
|
+
socket.on("close", () => clearInterval(pingTimer));
|
|
44555
45034
|
const send = (msg) => {
|
|
44556
45035
|
if (socket.readyState !== socket.OPEN) return;
|
|
44557
45036
|
logger?.logOut({
|
|
@@ -44595,6 +45074,20 @@ function attachBridgeWss(server, opts) {
|
|
|
44595
45074
|
}
|
|
44596
45075
|
var TurnInterruptedError = class extends Error {
|
|
44597
45076
|
};
|
|
45077
|
+
var DEFAULT_WS_PING_SECONDS = 30;
|
|
45078
|
+
function wsPingMs() {
|
|
45079
|
+
const raw = (process.env.MRRLIN_DIRECTOR_BRIDGE_WS_PING_SECONDS ?? "").trim();
|
|
45080
|
+
const parsed = Number.parseInt(raw, 10);
|
|
45081
|
+
return (Number.isInteger(parsed) && parsed > 0 ? parsed : DEFAULT_WS_PING_SECONDS) * 1e3;
|
|
45082
|
+
}
|
|
45083
|
+
async function waitFor(pred, timeoutMs) {
|
|
45084
|
+
const deadline = Date.now() + timeoutMs;
|
|
45085
|
+
while (Date.now() < deadline) {
|
|
45086
|
+
if (pred()) return true;
|
|
45087
|
+
await new Promise((r) => setTimeout(r, 25));
|
|
45088
|
+
}
|
|
45089
|
+
return pred();
|
|
45090
|
+
}
|
|
44598
45091
|
function attachSeqMetadata(msg, seq, turnId) {
|
|
44599
45092
|
if (msg.type === "event" || msg.type === "rateLimits" || msg.type === "error") {
|
|
44600
45093
|
return { ...msg, seq, turnId };
|
|
@@ -44653,11 +45146,11 @@ function createBridgeMessageHandler(deps) {
|
|
|
44653
45146
|
logResumeRejected(req.directorSessionId, "superseded");
|
|
44654
45147
|
return;
|
|
44655
45148
|
}
|
|
44656
|
-
|
|
45149
|
+
let tail2 = eventsAfter(buf, req.lastSeq);
|
|
45150
|
+
let truncated = false;
|
|
44657
45151
|
if (tail2 === null) {
|
|
44658
|
-
|
|
44659
|
-
|
|
44660
|
-
return;
|
|
45152
|
+
truncated = true;
|
|
45153
|
+
tail2 = [...buf.events];
|
|
44661
45154
|
}
|
|
44662
45155
|
const result = transition(buf.state, { kind: "resume", epochMatch: true });
|
|
44663
45156
|
if (result.next === "invalid") {
|
|
@@ -44669,7 +45162,14 @@ function createBridgeMessageHandler(deps) {
|
|
|
44669
45162
|
buf.replaying = true;
|
|
44670
45163
|
deps.bindSessionSink?.(req.directorSessionId, send);
|
|
44671
45164
|
boundSessions.add(req.directorSessionId);
|
|
44672
|
-
if (buf.
|
|
45165
|
+
if (buf.triggerDetach) {
|
|
45166
|
+
const detach = buf.triggerDetach;
|
|
45167
|
+
const epochAtBind = buf.bindEpoch;
|
|
45168
|
+
socket.once("close", () => {
|
|
45169
|
+
if (buf.bindEpoch !== epochAtBind) return;
|
|
45170
|
+
detach();
|
|
45171
|
+
});
|
|
45172
|
+
}
|
|
44673
45173
|
const willStreamLive = result.next === "live";
|
|
44674
45174
|
sendMsg({
|
|
44675
45175
|
type: "resume",
|
|
@@ -44682,6 +45182,14 @@ function createBridgeMessageHandler(deps) {
|
|
|
44682
45182
|
eventCount: tail2.length,
|
|
44683
45183
|
willStreamLive
|
|
44684
45184
|
});
|
|
45185
|
+
if (truncated) {
|
|
45186
|
+
sendMsg({
|
|
45187
|
+
type: "replay-truncated",
|
|
45188
|
+
directorSessionId: req.directorSessionId,
|
|
45189
|
+
turnId: buf.turnId,
|
|
45190
|
+
droppedBelowSeq: tail2[0]?.seq ?? buf.lastSeq
|
|
45191
|
+
});
|
|
45192
|
+
}
|
|
44685
45193
|
for (const { msg: bufferedMsg } of tail2) sendMsg(bufferedMsg);
|
|
44686
45194
|
for (const m of buf.outboundQueue) sendMsg(m);
|
|
44687
45195
|
buf.outboundQueue.length = 0;
|
|
@@ -44760,26 +45268,36 @@ function createBridgeMessageHandler(deps) {
|
|
|
44760
45268
|
let models = null;
|
|
44761
45269
|
let rateLimits = null;
|
|
44762
45270
|
let bootstrapError;
|
|
45271
|
+
const fallbackModels = readCodexFallbackModels();
|
|
44763
45272
|
try {
|
|
44764
45273
|
const codexClient2 = await deps.acquireCodex();
|
|
44765
45274
|
try {
|
|
44766
45275
|
try {
|
|
44767
|
-
const res = await codexClient2.model.list({});
|
|
45276
|
+
const res = await codexClient2.model.list({ includeHidden: true });
|
|
44768
45277
|
const raw = res.data ?? res.models ?? [];
|
|
44769
|
-
|
|
44770
|
-
|
|
44771
|
-
|
|
44772
|
-
|
|
44773
|
-
|
|
44774
|
-
|
|
44775
|
-
|
|
44776
|
-
|
|
44777
|
-
|
|
44778
|
-
|
|
44779
|
-
|
|
44780
|
-
|
|
45278
|
+
const mergedModels = /* @__PURE__ */ new Map();
|
|
45279
|
+
for (const m of raw) {
|
|
45280
|
+
const modelSlug = (m.model ?? m.id ?? "").trim();
|
|
45281
|
+
if (!modelSlug) continue;
|
|
45282
|
+
if (m.hidden === true && (modelSlug.startsWith("codex-") || !modelSlug.startsWith("gpt-"))) continue;
|
|
45283
|
+
addBridgeModel(
|
|
45284
|
+
mergedModels,
|
|
45285
|
+
mapBridgeModel({
|
|
45286
|
+
id: m.id,
|
|
45287
|
+
model: m.model ?? m.id,
|
|
45288
|
+
displayName: m.displayName ?? m.model ?? m.id,
|
|
45289
|
+
description: m.description ?? "",
|
|
45290
|
+
supportedReasoningEfforts: m.supportedReasoningEfforts ?? [],
|
|
45291
|
+
defaultReasoningEffort: m.defaultReasoningEffort ?? null,
|
|
45292
|
+
isDefault: m.isDefault === true
|
|
45293
|
+
})
|
|
45294
|
+
);
|
|
45295
|
+
}
|
|
45296
|
+
for (const fallbackModel of fallbackModels) addBridgeModel(mergedModels, fallbackModel);
|
|
45297
|
+
models = [...mergedModels.values()];
|
|
44781
45298
|
} catch (error51) {
|
|
44782
45299
|
bootstrapError = error51 instanceof Error ? error51.message : String(error51);
|
|
45300
|
+
models = fallbackModels.length > 0 ? fallbackModels : null;
|
|
44783
45301
|
}
|
|
44784
45302
|
try {
|
|
44785
45303
|
const res = await codexClient2.account.readRateLimits();
|
|
@@ -44792,6 +45310,7 @@ function createBridgeMessageHandler(deps) {
|
|
|
44792
45310
|
}
|
|
44793
45311
|
} catch (error51) {
|
|
44794
45312
|
bootstrapError = error51 instanceof Error ? error51.message : String(error51);
|
|
45313
|
+
models = fallbackModels.length > 0 ? fallbackModels : null;
|
|
44795
45314
|
}
|
|
44796
45315
|
sendForSpan({ type: "bootstrap", models, rateLimits, ...deps.bridgeInstanceId ? { bridgeInstanceId: deps.bridgeInstanceId } : {}, ...bootstrapError ? { error: bootstrapError } : {} });
|
|
44797
45316
|
return;
|
|
@@ -44800,6 +45319,20 @@ function createBridgeMessageHandler(deps) {
|
|
|
44800
45319
|
handleResumeRequest(msg, sendForSpan);
|
|
44801
45320
|
return;
|
|
44802
45321
|
}
|
|
45322
|
+
if (msg.type === "ack") {
|
|
45323
|
+
const ack = msg;
|
|
45324
|
+
const sessionId = typeof ack.directorSessionId === "string" ? ack.directorSessionId.trim() : "";
|
|
45325
|
+
const ackThread = sessionId ? deps.threads.get(sessionId) : void 0;
|
|
45326
|
+
if (ackThread?.replay && typeof ack.turnId === "string" && typeof ack.lastSeq === "number" && recordAck(ackThread.replay, ack.turnId, ack.lastSeq, Date.now())) {
|
|
45327
|
+
logCtx?.logger.logOut({
|
|
45328
|
+
spanId,
|
|
45329
|
+
sessionId,
|
|
45330
|
+
type: "<result-acked>",
|
|
45331
|
+
payload: { sessionId, turnId: ack.turnId, lastSeq: ack.lastSeq }
|
|
45332
|
+
});
|
|
45333
|
+
}
|
|
45334
|
+
return;
|
|
45335
|
+
}
|
|
44803
45336
|
if (msg.type === "interrupt") {
|
|
44804
45337
|
const directorSessionId2 = (msg.directorSessionId ?? "").trim();
|
|
44805
45338
|
const existing = directorSessionId2 ? deps.threads.get(directorSessionId2) : void 0;
|
|
@@ -44808,12 +45341,11 @@ function createBridgeMessageHandler(deps) {
|
|
|
44808
45341
|
return;
|
|
44809
45342
|
}
|
|
44810
45343
|
const interruptBuf = existing.replay;
|
|
44811
|
-
if (interruptBuf && interruptBuf.state === "
|
|
44812
|
-
const stopResult = transition("
|
|
45344
|
+
if (interruptBuf && interruptBuf.state === "detached") {
|
|
45345
|
+
const stopResult = transition("detached", { kind: "stop" });
|
|
44813
45346
|
if (stopResult.next !== "invalid") {
|
|
44814
45347
|
interruptBuf.state = stopResult.next;
|
|
44815
|
-
|
|
44816
|
-
recordInterrupted(interruptBuf);
|
|
45348
|
+
recordInterrupted(interruptBuf, Date.now(), deps.suspendedMs?.() ?? 0);
|
|
44817
45349
|
}
|
|
44818
45350
|
}
|
|
44819
45351
|
const codexClient2 = await deps.acquireCodex();
|
|
@@ -44875,6 +45407,36 @@ function createBridgeMessageHandler(deps) {
|
|
|
44875
45407
|
}
|
|
44876
45408
|
const codexClient = await deps.acquireCodex();
|
|
44877
45409
|
let thread = deps.threads.get(directorSessionId);
|
|
45410
|
+
if (thread?.inFlight && msg.takeover === true) {
|
|
45411
|
+
const takenOver = thread;
|
|
45412
|
+
const oldBuf = takenOver.replay;
|
|
45413
|
+
if (oldBuf && (oldBuf.state === "live" || oldBuf.state === "detached")) {
|
|
45414
|
+
const r = transition(oldBuf.state, { kind: "stop" });
|
|
45415
|
+
if (r.next !== "invalid") {
|
|
45416
|
+
oldBuf.state = r.next;
|
|
45417
|
+
recordInterrupted(oldBuf, Date.now(), deps.suspendedMs?.() ?? 0);
|
|
45418
|
+
}
|
|
45419
|
+
}
|
|
45420
|
+
takenOver.cancel?.();
|
|
45421
|
+
void withTimeout(
|
|
45422
|
+
codexClient.turn.interrupt({ threadId: takenOver.threadId }),
|
|
45423
|
+
DEFAULT_TURN_INTERRUPT_TIMEOUT_MS,
|
|
45424
|
+
"interrupt timed out"
|
|
45425
|
+
).catch(() => {
|
|
45426
|
+
});
|
|
45427
|
+
const cleared = await waitFor(() => !takenOver.inFlight, DEFAULT_TURN_INTERRUPT_TIMEOUT_MS);
|
|
45428
|
+
if (!cleared) {
|
|
45429
|
+
deps.threads.delete(directorSessionId);
|
|
45430
|
+
deps.persistThreads();
|
|
45431
|
+
thread = void 0;
|
|
45432
|
+
}
|
|
45433
|
+
logCtx?.logger.logOut({
|
|
45434
|
+
spanId,
|
|
45435
|
+
sessionId: directorSessionId,
|
|
45436
|
+
type: "<turn-takeover>",
|
|
45437
|
+
payload: { sessionId: directorSessionId, clean: cleared, oldTurnId: oldBuf?.turnId ?? null }
|
|
45438
|
+
});
|
|
45439
|
+
}
|
|
44878
45440
|
const desiredModel = normalizeModel(msg.model);
|
|
44879
45441
|
const desiredEffort = normalizeReasoningEffort(msg.reasoningEffort);
|
|
44880
45442
|
try {
|
|
@@ -44932,7 +45494,13 @@ function createBridgeMessageHandler(deps) {
|
|
|
44932
45494
|
return;
|
|
44933
45495
|
}
|
|
44934
45496
|
if (thread.inFlight) {
|
|
44935
|
-
sendForSpan({
|
|
45497
|
+
sendForSpan({
|
|
45498
|
+
type: "error",
|
|
45499
|
+
directorSessionId,
|
|
45500
|
+
code: "busy",
|
|
45501
|
+
...thread.replay ? { inFlightTurnId: thread.replay.turnId } : {},
|
|
45502
|
+
error: "Director session is busy."
|
|
45503
|
+
});
|
|
44936
45504
|
deps.releaseCodex();
|
|
44937
45505
|
return;
|
|
44938
45506
|
}
|
|
@@ -44940,7 +45508,8 @@ function createBridgeMessageHandler(deps) {
|
|
|
44940
45508
|
thread.replay = createReplayBuffer({
|
|
44941
45509
|
turnId: (0, import_node_crypto5.randomUUID)(),
|
|
44942
45510
|
spanId,
|
|
44943
|
-
...deps.
|
|
45511
|
+
...deps.retentionMs !== void 0 ? { retentionMs: deps.retentionMs } : {},
|
|
45512
|
+
...deps.ackLingerMs !== void 0 ? { ackLingerMs: deps.ackLingerMs } : {}
|
|
44944
45513
|
});
|
|
44945
45514
|
thread.lastUsedAt = Date.now();
|
|
44946
45515
|
const threadId = thread.threadId;
|
|
@@ -45031,6 +45600,7 @@ function createBridgeMessageHandler(deps) {
|
|
|
45031
45600
|
const watchdog = createInactivityWatchdog({
|
|
45032
45601
|
idleMs,
|
|
45033
45602
|
maxMs,
|
|
45603
|
+
...deps.suspendedMs ? { suspendedMs: deps.suspendedMs } : {},
|
|
45034
45604
|
onExpire: (reason) => {
|
|
45035
45605
|
rejectWatchdog?.(
|
|
45036
45606
|
new Error(
|
|
@@ -45039,48 +45609,36 @@ function createBridgeMessageHandler(deps) {
|
|
|
45039
45609
|
);
|
|
45040
45610
|
void withTimeout(codexClient.turn.interrupt({ threadId }), DEFAULT_TURN_INTERRUPT_TIMEOUT_MS, "interrupt timed out").catch(
|
|
45041
45611
|
() => {
|
|
45042
|
-
deps.threads.
|
|
45043
|
-
|
|
45612
|
+
if (deps.threads.get(directorSessionId) === thread) {
|
|
45613
|
+
deps.threads.delete(directorSessionId);
|
|
45614
|
+
deps.persistThreads();
|
|
45615
|
+
}
|
|
45044
45616
|
}
|
|
45045
45617
|
);
|
|
45046
45618
|
}
|
|
45047
45619
|
});
|
|
45620
|
+
const turnBuf = thread.replay;
|
|
45048
45621
|
const onSocketClose = () => {
|
|
45049
45622
|
const buf = thread.replay;
|
|
45050
|
-
if (!buf || buf
|
|
45051
|
-
const r = transition(
|
|
45052
|
-
if (r.next === "invalid") return;
|
|
45623
|
+
if (!buf || buf !== turnBuf) return;
|
|
45624
|
+
const r = transition(buf.state, { kind: "socket_close" });
|
|
45625
|
+
if (r.next === "invalid" || r.next === buf.state) return;
|
|
45053
45626
|
buf.state = r.next;
|
|
45054
45627
|
logCtx?.logger.logOut({
|
|
45055
45628
|
spanId,
|
|
45056
45629
|
sessionId: directorSessionId,
|
|
45057
|
-
type: "<
|
|
45058
|
-
payload: { sessionId: directorSessionId, turnId: buf.turnId,
|
|
45059
|
-
});
|
|
45060
|
-
enterGrace(buf, () => {
|
|
45061
|
-
const expired = transition("grace", { kind: "grace_expire" });
|
|
45062
|
-
if (expired.next === "invalid") return;
|
|
45063
|
-
buf.state = expired.next;
|
|
45064
|
-
recordInterrupted(buf);
|
|
45065
|
-
logCtx?.logger.logOut({
|
|
45066
|
-
spanId,
|
|
45067
|
-
sessionId: directorSessionId,
|
|
45068
|
-
type: "<grace-expired>",
|
|
45069
|
-
payload: { sessionId: directorSessionId, turnId: buf.turnId, lastSeq: buf.lastSeq }
|
|
45070
|
-
});
|
|
45071
|
-
rejectWatchdog?.(new TurnInterruptedError());
|
|
45072
|
-
void withTimeout(
|
|
45073
|
-
codexClient.turn.interrupt({ threadId }),
|
|
45074
|
-
DEFAULT_TURN_INTERRUPT_TIMEOUT_MS,
|
|
45075
|
-
"interrupt timed out"
|
|
45076
|
-
).catch(() => {
|
|
45077
|
-
deps.threads.delete(directorSessionId);
|
|
45078
|
-
deps.persistThreads();
|
|
45079
|
-
});
|
|
45630
|
+
type: "<buffer-detached>",
|
|
45631
|
+
payload: { sessionId: directorSessionId, turnId: buf.turnId, lastSeq: buf.lastSeq }
|
|
45080
45632
|
});
|
|
45081
45633
|
};
|
|
45082
|
-
|
|
45083
|
-
|
|
45634
|
+
{
|
|
45635
|
+
const epochAtBind = turnBuf?.bindEpoch ?? 0;
|
|
45636
|
+
socket.once("close", () => {
|
|
45637
|
+
if (turnBuf && turnBuf.bindEpoch !== epochAtBind) return;
|
|
45638
|
+
onSocketClose();
|
|
45639
|
+
});
|
|
45640
|
+
}
|
|
45641
|
+
if (thread.replay) thread.replay.triggerDetach = onSocketClose;
|
|
45084
45642
|
pokeActivity = () => watchdog.poke();
|
|
45085
45643
|
const runSequence = (async () => {
|
|
45086
45644
|
const turnInput = [];
|
|
@@ -45129,10 +45687,20 @@ ${effectiveText}` : effectiveText;
|
|
|
45129
45687
|
sendInTurn({ type: "event", directorSessionId, event });
|
|
45130
45688
|
}
|
|
45131
45689
|
} finally {
|
|
45132
|
-
if (thread.replay && thread.replay.state === "live") {
|
|
45133
|
-
markCompleted(thread.replay, Date.now());
|
|
45690
|
+
if (thread.replay && (thread.replay.state === "live" || thread.replay.state === "detached")) {
|
|
45691
|
+
markCompleted(thread.replay, Date.now(), deps.suspendedMs?.() ?? 0);
|
|
45692
|
+
logCtx?.logger.logOut({
|
|
45693
|
+
spanId,
|
|
45694
|
+
sessionId: directorSessionId,
|
|
45695
|
+
type: "<result-retained>",
|
|
45696
|
+
payload: {
|
|
45697
|
+
sessionId: directorSessionId,
|
|
45698
|
+
turnId: thread.replay.turnId,
|
|
45699
|
+
lastSeq: thread.replay.lastSeq,
|
|
45700
|
+
bytes: thread.replay.bytesUsed
|
|
45701
|
+
}
|
|
45702
|
+
});
|
|
45134
45703
|
}
|
|
45135
|
-
socket.off("close", onSocketClose);
|
|
45136
45704
|
watchdog.cancel();
|
|
45137
45705
|
unsub();
|
|
45138
45706
|
thread.cancel = null;
|
|
@@ -45439,6 +46007,7 @@ async function startDirectorBridge() {
|
|
|
45439
46007
|
})
|
|
45440
46008
|
);
|
|
45441
46009
|
const threads = /* @__PURE__ */ new Map();
|
|
46010
|
+
const suspensionDetector = createSuspensionDetector();
|
|
45442
46011
|
for (const [directorSessionId, entry] of loadThreadIndex(config2.stateDir, config2.idleThreadTtlMs, Date.now())) {
|
|
45443
46012
|
threads.set(directorSessionId, {
|
|
45444
46013
|
threadId: entry.threadId,
|
|
@@ -45476,6 +46045,7 @@ async function startDirectorBridge() {
|
|
|
45476
46045
|
let changed = false;
|
|
45477
46046
|
for (const [directorSessionId, thread] of threads.entries()) {
|
|
45478
46047
|
if (thread.inFlight) continue;
|
|
46048
|
+
if (thread.replay && !shouldGc(thread.replay, now, suspensionDetector.suspendedMs())) continue;
|
|
45479
46049
|
if (now - thread.lastUsedAt > config2.idleThreadTtlMs) {
|
|
45480
46050
|
threads.delete(directorSessionId);
|
|
45481
46051
|
changed = true;
|
|
@@ -45491,6 +46061,7 @@ async function startDirectorBridge() {
|
|
|
45491
46061
|
acquireCodex: () => codexKeepalive.acquire(),
|
|
45492
46062
|
releaseCodex: () => codexKeepalive.release(),
|
|
45493
46063
|
runExclusive: wrappedTurnLock.runExclusive,
|
|
46064
|
+
suspendedMs: () => suspensionDetector.suspendedMs(),
|
|
45494
46065
|
buildStartParams: (msg, desiredModel) => buildThreadStartParams(msg, config2, desiredModel),
|
|
45495
46066
|
turnInactivity: () => ({ idleMs: chatTurnIdleMs(), maxMs: chatTurnMaxMs() }),
|
|
45496
46067
|
checkoutRegistry,
|
|
@@ -45653,20 +46224,35 @@ async function startDirectorBridge() {
|
|
|
45653
46224
|
);
|
|
45654
46225
|
enqueueRun(async () => {
|
|
45655
46226
|
const now = Date.now();
|
|
46227
|
+
const suspendedNow = suspensionDetector.suspendedMs();
|
|
46228
|
+
let retainedBuffers = 0;
|
|
46229
|
+
let retainedBytes = 0;
|
|
45656
46230
|
for (const [sid, t] of threads) {
|
|
45657
|
-
if (t.replay && shouldGc(t.replay, now)) {
|
|
46231
|
+
if (t.replay && shouldGc(t.replay, now, suspendedNow)) {
|
|
45658
46232
|
const turnId = t.replay.turnId;
|
|
45659
46233
|
const lastSeq = t.replay.lastSeq;
|
|
46234
|
+
const expiredUnacked = t.replay.ackedAt === null;
|
|
45660
46235
|
writeTombstone(tombstones, sid, t.replay, now);
|
|
45661
46236
|
bridgeLogger.logOut({
|
|
45662
46237
|
spanId: "gc",
|
|
45663
46238
|
sessionId: sid,
|
|
45664
|
-
type: "<tombstone-written>",
|
|
46239
|
+
type: expiredUnacked ? "<result-expired>" : "<tombstone-written>",
|
|
45665
46240
|
payload: { sessionId: sid, turnId, lastSeq }
|
|
45666
46241
|
});
|
|
45667
46242
|
t.replay = null;
|
|
46243
|
+
} else if (t.replay) {
|
|
46244
|
+
retainedBuffers += 1;
|
|
46245
|
+
retainedBytes += t.replay.bytesUsed;
|
|
45668
46246
|
}
|
|
45669
46247
|
}
|
|
46248
|
+
if (retainedBuffers > 0) {
|
|
46249
|
+
bridgeLogger.logOut({
|
|
46250
|
+
spanId: "gc",
|
|
46251
|
+
sessionId: null,
|
|
46252
|
+
type: "<buffer-stats>",
|
|
46253
|
+
payload: { retainedBuffers, retainedBytes }
|
|
46254
|
+
});
|
|
46255
|
+
}
|
|
45670
46256
|
const removed = sweepExpiredTombstones(tombstones, now);
|
|
45671
46257
|
if (removed > 0) {
|
|
45672
46258
|
bridgeLogger.logOut({
|
|
@@ -45721,6 +46307,7 @@ async function startDirectorBridge() {
|
|
|
45721
46307
|
() => server.close(),
|
|
45722
46308
|
() => dispatchServer.close(),
|
|
45723
46309
|
() => clearInterval(cleanupTimer),
|
|
46310
|
+
() => suspensionDetector.dispose(),
|
|
45724
46311
|
() => remoteRelay?.stop(),
|
|
45725
46312
|
() => tokenRefresher?.stop(),
|
|
45726
46313
|
() => {
|
|
@@ -49624,8 +50211,91 @@ ${dismissedFwd.slice(-20).join("\n")}` : "";
|
|
|
49624
50211
|
return { outcome: "CONSENSUS_BLOCKED", rounds: input.rounds, finalArtifact: artifact, history };
|
|
49625
50212
|
}
|
|
49626
50213
|
|
|
50214
|
+
// src/consensus/spec-identity.ts
|
|
50215
|
+
function validateSpecIdentity(markdown, taskId) {
|
|
50216
|
+
const target = taskId.toUpperCase();
|
|
50217
|
+
const fieldToken = frontmatterTaskToken(markdown);
|
|
50218
|
+
if (fieldToken !== null && fieldToken.toUpperCase() !== target) return false;
|
|
50219
|
+
const headingToken = firstHeadingToken(markdown);
|
|
50220
|
+
if (headingToken !== null && headingToken.toUpperCase() !== target) return false;
|
|
50221
|
+
return true;
|
|
50222
|
+
}
|
|
50223
|
+
var TASK_ID_TOKEN = /\b[A-Za-z]{1,12}-\d{1,10}\b/;
|
|
50224
|
+
var ATX_HEADING = /^ {0,3}#{1,6}\s+(.*)$/;
|
|
50225
|
+
var FRONTMATTER_TASK = /^task(?:_id)?:\s*(.+)$/i;
|
|
50226
|
+
function frontmatterTaskToken(markdown) {
|
|
50227
|
+
const lines2 = markdown.split("\n");
|
|
50228
|
+
const firstContent = lines2.findIndex((l) => l.trim() !== "");
|
|
50229
|
+
if (firstContent === -1 || lines2[firstContent]?.trim() !== "---") return null;
|
|
50230
|
+
for (let i = firstContent + 1; i < lines2.length; i++) {
|
|
50231
|
+
const trimmed = (lines2[i] ?? "").trim();
|
|
50232
|
+
if (trimmed === "---") break;
|
|
50233
|
+
const m = FRONTMATTER_TASK.exec(trimmed);
|
|
50234
|
+
if (m) return firstTaskIdToken(m[1] ?? "");
|
|
50235
|
+
}
|
|
50236
|
+
return null;
|
|
50237
|
+
}
|
|
50238
|
+
function firstHeadingToken(markdown) {
|
|
50239
|
+
const heading = firstHeading(markdown);
|
|
50240
|
+
return heading === null ? null : firstTaskIdToken(heading);
|
|
50241
|
+
}
|
|
50242
|
+
function firstHeading(markdown) {
|
|
50243
|
+
const lines2 = markdown.split("\n");
|
|
50244
|
+
let i = 0;
|
|
50245
|
+
const firstContent = lines2.findIndex((l) => l.trim() !== "");
|
|
50246
|
+
if (firstContent !== -1 && lines2[firstContent]?.trim() === "---") {
|
|
50247
|
+
i = firstContent + 1;
|
|
50248
|
+
while (i < lines2.length && lines2[i]?.trim() !== "---") i++;
|
|
50249
|
+
i++;
|
|
50250
|
+
}
|
|
50251
|
+
let inFence = false;
|
|
50252
|
+
let inComment = false;
|
|
50253
|
+
for (; i < lines2.length; i++) {
|
|
50254
|
+
const line = lines2[i] ?? "";
|
|
50255
|
+
const trimmed = line.trim();
|
|
50256
|
+
if (inComment) {
|
|
50257
|
+
if (trimmed.includes("-->")) inComment = false;
|
|
50258
|
+
continue;
|
|
50259
|
+
}
|
|
50260
|
+
if (trimmed.startsWith("```") || trimmed.startsWith("~~~")) {
|
|
50261
|
+
inFence = !inFence;
|
|
50262
|
+
continue;
|
|
50263
|
+
}
|
|
50264
|
+
if (inFence) continue;
|
|
50265
|
+
if (trimmed.startsWith("<!--")) {
|
|
50266
|
+
if (!trimmed.includes("-->")) inComment = true;
|
|
50267
|
+
continue;
|
|
50268
|
+
}
|
|
50269
|
+
const m = ATX_HEADING.exec(line);
|
|
50270
|
+
if (m) return m[1] ?? "";
|
|
50271
|
+
}
|
|
50272
|
+
return null;
|
|
50273
|
+
}
|
|
50274
|
+
function firstTaskIdToken(text) {
|
|
50275
|
+
const m = TASK_ID_TOKEN.exec(text);
|
|
50276
|
+
return m ? m[0] : null;
|
|
50277
|
+
}
|
|
50278
|
+
|
|
50279
|
+
// src/consensus/heads.ts
|
|
50280
|
+
var DEFAULT_DOMAIN = "code";
|
|
50281
|
+
var DOMAINS = ["code", "seo", "marketing", "content"];
|
|
50282
|
+
var HEADS_BY_DOMAIN = {
|
|
50283
|
+
code: ["plan-reviewer", "architect", "security-analyst", "scope-analyst"],
|
|
50284
|
+
seo: ["plan-reviewer", "scope-analyst", "seo-analyst"],
|
|
50285
|
+
marketing: ["plan-reviewer", "scope-analyst", "content-editor"],
|
|
50286
|
+
content: ["plan-reviewer", "scope-analyst", "content-editor"]
|
|
50287
|
+
};
|
|
50288
|
+
var SPEC_HEADS = HEADS_BY_DOMAIN.code;
|
|
50289
|
+
function normalizeDomain(value) {
|
|
50290
|
+
if (typeof value !== "string") return DEFAULT_DOMAIN;
|
|
50291
|
+
const v = value.trim().toLowerCase();
|
|
50292
|
+
return DOMAINS.includes(v) ? v : DEFAULT_DOMAIN;
|
|
50293
|
+
}
|
|
50294
|
+
function resolveHeads(domain2) {
|
|
50295
|
+
return [...HEADS_BY_DOMAIN[normalizeDomain(domain2)]];
|
|
50296
|
+
}
|
|
50297
|
+
|
|
49627
50298
|
// src/consensus/spec-gate.ts
|
|
49628
|
-
var SPEC_HEADS = ["plan-reviewer", "architect", "security-analyst", "scope-analyst"];
|
|
49629
50299
|
async function runSpecGate(_target, deps, opts) {
|
|
49630
50300
|
if (!await deps.consensusEnabled()) return { outcome: "SKIPPED" };
|
|
49631
50301
|
const intent = await deps.readIntent();
|
|
@@ -49641,10 +50311,12 @@ async function runSpecGate(_target, deps, opts) {
|
|
|
49641
50311
|
]
|
|
49642
50312
|
};
|
|
49643
50313
|
}
|
|
50314
|
+
const baseHeads = resolveHeads(intent.domain);
|
|
49644
50315
|
const extraHeads = opts?.extraHeads ?? [];
|
|
49645
|
-
const heads = [...
|
|
50316
|
+
const heads = [...baseHeads, ...extraHeads.filter((h) => !baseHeads.includes(h))];
|
|
49646
50317
|
const spec = await deps.readSpec();
|
|
49647
50318
|
let currentHash = spec.contentHash;
|
|
50319
|
+
let currentMarkdown = spec.markdownBody;
|
|
49648
50320
|
let conflicted = false;
|
|
49649
50321
|
const result = await runConsensus({
|
|
49650
50322
|
artifact: spec.markdownBody,
|
|
@@ -49656,12 +50328,20 @@ async function runSpecGate(_target, deps, opts) {
|
|
|
49656
50328
|
revise: async (artifact, accepted) => {
|
|
49657
50329
|
const revised = await deps.runReviser(artifact, accepted);
|
|
49658
50330
|
if (!revised.ok) return artifact;
|
|
50331
|
+
if (intent.taskId !== null && !validateSpecIdentity(revised.text, intent.taskId)) {
|
|
50332
|
+
process.stderr.write(
|
|
50333
|
+
`[spec-gate] discarding drifted revision for ${spec.pageId}: heading does not identify ${intent.taskId}
|
|
50334
|
+
`
|
|
50335
|
+
);
|
|
50336
|
+
return artifact;
|
|
50337
|
+
}
|
|
49659
50338
|
const cas = await deps.casUpdateSpec(revised.text, currentHash);
|
|
49660
50339
|
if (!cas.ok) {
|
|
49661
50340
|
conflicted = true;
|
|
49662
50341
|
throw new Error("SPEC_CHANGED_DURING_REVIEW");
|
|
49663
50342
|
}
|
|
49664
50343
|
currentHash = cas.contentHash;
|
|
50344
|
+
currentMarkdown = revised.text;
|
|
49665
50345
|
return revised.text;
|
|
49666
50346
|
}
|
|
49667
50347
|
// Only OUR conflict throw is swallowed (→ null). Anything else is a real
|
|
@@ -49671,6 +50351,22 @@ async function runSpecGate(_target, deps, opts) {
|
|
|
49671
50351
|
await deps.writeEvidence({ specPageId: spec.pageId, outcome: "SPEC_CHANGED_DURING_REVIEW" });
|
|
49672
50352
|
return { outcome: "SPEC_CHANGED_DURING_REVIEW" };
|
|
49673
50353
|
}
|
|
50354
|
+
if (result.outcome === "CONVERGED" && intent.taskId !== null && !validateSpecIdentity(currentMarkdown, intent.taskId)) {
|
|
50355
|
+
process.stderr.write(
|
|
50356
|
+
`[spec-gate] blocking promotion for ${spec.pageId}: converged spec heading does not identify ${intent.taskId}
|
|
50357
|
+
`
|
|
50358
|
+
);
|
|
50359
|
+
return {
|
|
50360
|
+
outcome: "CONSENSUS_BLOCKED",
|
|
50361
|
+
clarifications: [
|
|
50362
|
+
{
|
|
50363
|
+
question: `SPEC_IDENTITY_MISMATCH: this spec page gates ${intent.taskId}, but its heading identifies a different task \u2014 rewrite the spec heading and content for ${intent.taskId}, or re-link the task.`,
|
|
50364
|
+
whyBlocking: "converged spec content belongs to a different task than the one it gates",
|
|
50365
|
+
category: "scope"
|
|
50366
|
+
}
|
|
50367
|
+
]
|
|
50368
|
+
};
|
|
50369
|
+
}
|
|
49674
50370
|
await deps.writeEvidence({
|
|
49675
50371
|
specPageId: spec.pageId,
|
|
49676
50372
|
outcome: result.outcome,
|
|
@@ -49888,6 +50584,40 @@ var import_node_fs11 = __toESM(require("node:fs"), 1);
|
|
|
49888
50584
|
var import_node_path13 = __toESM(require("node:path"), 1);
|
|
49889
50585
|
var import_node_url2 = require("node:url");
|
|
49890
50586
|
|
|
50587
|
+
// src/consensus/resolve-gated-task.ts
|
|
50588
|
+
async function resolveGatedTaskId(client, projectSlug, page) {
|
|
50589
|
+
const tasks = await client.listTasks(projectSlug);
|
|
50590
|
+
const linking = tasks.find((t) => t.specWikiPageId === page.id);
|
|
50591
|
+
if (linking) {
|
|
50592
|
+
if (page.taskId !== linking.id) {
|
|
50593
|
+
throw new Error(
|
|
50594
|
+
`spec page ${page.id} is linked by task ${linking.id} (tasks.spec_wiki_page_id) but its wiki_pages.task_id back-pointer is ${page.taskId === null ? "null" : page.taskId} \u2014 refusing to gate with a drifted identity`
|
|
50595
|
+
);
|
|
50596
|
+
}
|
|
50597
|
+
return linking.id;
|
|
50598
|
+
}
|
|
50599
|
+
return page.taskId;
|
|
50600
|
+
}
|
|
50601
|
+
|
|
50602
|
+
// src/consensus/review-identity.ts
|
|
50603
|
+
function domainScopedReviewHash(rawHash, domain2) {
|
|
50604
|
+
const d = normalizeDomain(domain2);
|
|
50605
|
+
return d === "code" ? rawHash : `${rawHash}@${d}`;
|
|
50606
|
+
}
|
|
50607
|
+
async function resolveSpecDomain(client, projectSlug, page) {
|
|
50608
|
+
try {
|
|
50609
|
+
const taskId = await resolveGatedTaskId(client, projectSlug, page);
|
|
50610
|
+
if (taskId !== null) {
|
|
50611
|
+
const task = await client.getTask(projectSlug, taskId);
|
|
50612
|
+
return normalizeDomain(task?.domain);
|
|
50613
|
+
}
|
|
50614
|
+
const project = await client.getProject(projectSlug);
|
|
50615
|
+
return normalizeDomain(project?.defaultDomain);
|
|
50616
|
+
} catch {
|
|
50617
|
+
return "code";
|
|
50618
|
+
}
|
|
50619
|
+
}
|
|
50620
|
+
|
|
49891
50621
|
// src/consensus/code-gate-git.ts
|
|
49892
50622
|
var import_node_child_process6 = require("node:child_process");
|
|
49893
50623
|
var DIFF_MAX_BYTES = 2e5;
|
|
@@ -50132,6 +50862,23 @@ function loadPersona(name) {
|
|
|
50132
50862
|
personaCache.set(name, content);
|
|
50133
50863
|
return content;
|
|
50134
50864
|
}
|
|
50865
|
+
function specHeadIntentBlock(taskId, title, seed) {
|
|
50866
|
+
const identity = taskId ? `--- TASK IDENTITY (the spec MUST be about THIS task; if its scope or direction contradicts this identity, raise a \`scope\` issue instead of steering the spec elsewhere) ---
|
|
50867
|
+
TASK ${taskId}: ${title ?? ""}
|
|
50868
|
+
|
|
50869
|
+
` : "";
|
|
50870
|
+
const intent = seed ? `--- OPERATOR INTENT (the operator's own words \u2014 judge the spec against THIS) ---
|
|
50871
|
+
${seed}
|
|
50872
|
+
|
|
50873
|
+
` : "";
|
|
50874
|
+
return `${identity}${intent}`;
|
|
50875
|
+
}
|
|
50876
|
+
function reviserIdentityInstructions(taskId, title) {
|
|
50877
|
+
const lock = taskId ? `This spec gates task ${taskId}${title ? ` ("${title}")` : ""}. You MUST NOT change the spec's task identity or heading, retarget it to a different task id, or invert its direction (e.g. live-submission vs preparation-only). Preserve the leading \`task: ${taskId}\` frontmatter EXACTLY; never change it to another task id. Address ONLY the accepted issues.
|
|
50878
|
+
|
|
50879
|
+
` : "";
|
|
50880
|
+
return `${lock}You revise a spec to address accepted issues. Return ONLY the full revised markdown, no commentary.`;
|
|
50881
|
+
}
|
|
50135
50882
|
function buildSpecGateDeps(args) {
|
|
50136
50883
|
const { client, projectSlug, specPageId, cwd, codexExecutable, extraHeadCount = 0 } = args;
|
|
50137
50884
|
const rounds = Number(process.env["MRRLIN_CONSENSUS_ROUNDS"] ?? 3);
|
|
@@ -50142,10 +50889,19 @@ function buildSpecGateDeps(args) {
|
|
|
50142
50889
|
if (!intentPromise) {
|
|
50143
50890
|
intentPromise = (async () => {
|
|
50144
50891
|
const page = await client.getWikiPage(projectSlug, specPageId);
|
|
50145
|
-
|
|
50146
|
-
|
|
50892
|
+
if (!page) {
|
|
50893
|
+
throw new Error(`spec page ${projectSlug}/${specPageId} not found \u2014 cannot gate`);
|
|
50894
|
+
}
|
|
50895
|
+
const taskId = await resolveGatedTaskId(client, projectSlug, { id: page.id, taskId: page.taskId ?? null });
|
|
50896
|
+
if (taskId === null) {
|
|
50897
|
+
const project = await client.getProject(projectSlug);
|
|
50898
|
+
return { taskId: null, seed: null, title: null, domain: project.defaultDomain };
|
|
50899
|
+
}
|
|
50147
50900
|
const task = await client.getTask(projectSlug, taskId);
|
|
50148
|
-
|
|
50901
|
+
if (!task) {
|
|
50902
|
+
throw new Error(`gated task ${projectSlug}/${taskId} not found for spec ${specPageId} \u2014 cannot gate`);
|
|
50903
|
+
}
|
|
50904
|
+
return { taskId, seed: task.operatorSeedRequest ?? null, title: task.title ?? "", domain: task.domain };
|
|
50149
50905
|
})();
|
|
50150
50906
|
}
|
|
50151
50907
|
return intentPromise;
|
|
@@ -50170,11 +50926,8 @@ function buildSpecGateDeps(args) {
|
|
|
50170
50926
|
},
|
|
50171
50927
|
async runHead(personaName, artifact, dismissed) {
|
|
50172
50928
|
const persona = loadPersona(personaName);
|
|
50173
|
-
const { seed } = await loadIntent();
|
|
50174
|
-
const intentBlock =
|
|
50175
|
-
${seed}
|
|
50176
|
-
|
|
50177
|
-
` : "";
|
|
50929
|
+
const { taskId, title, seed } = await loadIntent();
|
|
50930
|
+
const intentBlock = specHeadIntentBlock(taskId, title, seed);
|
|
50178
50931
|
return runCodexExec(
|
|
50179
50932
|
{
|
|
50180
50933
|
prompt: `Review this spec against the operator's intent.
|
|
@@ -50205,6 +50958,7 @@ ${artifact}`,
|
|
|
50205
50958
|
},
|
|
50206
50959
|
async runReviser(artifact, accepted) {
|
|
50207
50960
|
const acceptedList = accepted.map((a) => `- (${a.category}) ${a.text}`).join("\n");
|
|
50961
|
+
const { taskId, title } = await loadIntent();
|
|
50208
50962
|
return runCodexExec(
|
|
50209
50963
|
{
|
|
50210
50964
|
prompt: `Accepted issues:
|
|
@@ -50212,7 +50966,7 @@ ${acceptedList}
|
|
|
50212
50966
|
|
|
50213
50967
|
--- SPEC ---
|
|
50214
50968
|
${artifact}`,
|
|
50215
|
-
developerInstructions:
|
|
50969
|
+
developerInstructions: reviserIdentityInstructions(taskId, title),
|
|
50216
50970
|
cwd,
|
|
50217
50971
|
timeoutMs,
|
|
50218
50972
|
sandbox: "read-only",
|
|
@@ -50263,9 +51017,10 @@ ${artifact}`,
|
|
|
50263
51017
|
}
|
|
50264
51018
|
const outcome = r["outcome"];
|
|
50265
51019
|
const rounds2 = r["rounds"];
|
|
51020
|
+
const { domain: domain2 } = await loadIntent();
|
|
50266
51021
|
try {
|
|
50267
51022
|
await client.recordSpecConsensusReview(projectSlug, specPageId, {
|
|
50268
|
-
contentHash,
|
|
51023
|
+
contentHash: domainScopedReviewHash(contentHash, domain2),
|
|
50269
51024
|
outcome: String(outcome),
|
|
50270
51025
|
rounds: typeof rounds2 === "number" ? rounds2 : 0
|
|
50271
51026
|
});
|
|
@@ -50358,11 +51113,51 @@ ${artifact}`,
|
|
|
50358
51113
|
|
|
50359
51114
|
// src/async-tools/consensus.ts
|
|
50360
51115
|
var import_node_crypto6 = require("node:crypto");
|
|
51116
|
+
function buildGateQuestionOptions(category) {
|
|
51117
|
+
if (category === "scope") {
|
|
51118
|
+
return [
|
|
51119
|
+
{
|
|
51120
|
+
answer: "Use the narrower scope only. Update the spec to keep the original limited scope and do not add the broader interpretation unless explicitly required.",
|
|
51121
|
+
description: "Do only the originally requested narrower scope and leave the broader bucket out unless explicitly required.",
|
|
51122
|
+
label: "Narrower"
|
|
51123
|
+
},
|
|
51124
|
+
{
|
|
51125
|
+
answer: "Use the broader scope. Update the spec to explicitly include the broader interpretation and any required acceptance criteria.",
|
|
51126
|
+
description: "Treat the broader interpretation as in scope and spell out the extra acceptance criteria it adds.",
|
|
51127
|
+
label: "Broader"
|
|
51128
|
+
}
|
|
51129
|
+
];
|
|
51130
|
+
}
|
|
51131
|
+
return [
|
|
51132
|
+
{
|
|
51133
|
+
answer: "Use the safer/default interpretation. Update the spec to document that assumption explicitly.",
|
|
51134
|
+
description: "Choose the lower-risk default interpretation and record it explicitly so implementation is unblocked.",
|
|
51135
|
+
label: "Safer default"
|
|
51136
|
+
},
|
|
51137
|
+
{
|
|
51138
|
+
answer: "Use the broader/stronger interpretation. Update the spec to make that interpretation explicit.",
|
|
51139
|
+
description: "Commit to the stronger interpretation instead of a conservative default, and update the spec to match.",
|
|
51140
|
+
label: "Stronger interpretation"
|
|
51141
|
+
}
|
|
51142
|
+
];
|
|
51143
|
+
}
|
|
50361
51144
|
async function cachedResult(client, input) {
|
|
50362
51145
|
const page = await client.getWikiPage(input.projectSlug, input.pageId);
|
|
50363
51146
|
if (!page) return null;
|
|
50364
|
-
const
|
|
51147
|
+
const domain2 = await resolveSpecDomain(client, input.projectSlug, { id: page.id, taskId: page.taskId ?? null });
|
|
51148
|
+
const review = await client.getSpecConsensusReview(
|
|
51149
|
+
input.projectSlug,
|
|
51150
|
+
input.pageId,
|
|
51151
|
+
domainScopedReviewHash(page.contentHash, domain2)
|
|
51152
|
+
);
|
|
50365
51153
|
if (review?.outcome === "CONVERGED") {
|
|
51154
|
+
const gatedTaskId = await resolveGatedTaskId(client, input.projectSlug, {
|
|
51155
|
+
id: page.id,
|
|
51156
|
+
taskId: page.taskId ?? null
|
|
51157
|
+
});
|
|
51158
|
+
if (gatedTaskId !== null && !validateSpecIdentity(page.markdownBody, gatedTaskId)) {
|
|
51159
|
+
return null;
|
|
51160
|
+
}
|
|
50366
51161
|
return { status: "already_converged", payload: { outcome: "CONVERGED", contentHash: page.contentHash } };
|
|
50367
51162
|
}
|
|
50368
51163
|
if (review?.outcome === "CONSENSUS_BLOCKED") {
|
|
@@ -50404,7 +51199,8 @@ async function run(ctx, input) {
|
|
|
50404
51199
|
kind: "question",
|
|
50405
51200
|
taskId: scopeId,
|
|
50406
51201
|
body: clarification.question,
|
|
50407
|
-
origin: "gate"
|
|
51202
|
+
origin: "gate",
|
|
51203
|
+
questionOptions: buildGateQuestionOptions(clarification.category)
|
|
50408
51204
|
});
|
|
50409
51205
|
} catch {
|
|
50410
51206
|
}
|
|
@@ -50419,7 +51215,8 @@ async function run(ctx, input) {
|
|
|
50419
51215
|
kind: "question",
|
|
50420
51216
|
planId: scopeId,
|
|
50421
51217
|
body: clarification.question,
|
|
50422
|
-
origin: "gate"
|
|
51218
|
+
origin: "gate",
|
|
51219
|
+
questionOptions: buildGateQuestionOptions(clarification.category)
|
|
50423
51220
|
});
|
|
50424
51221
|
} catch {
|
|
50425
51222
|
}
|
|
@@ -51353,7 +52150,7 @@ var mcpToolDescriptions = {
|
|
|
51353
52150
|
[mcpToolNames.resetDirectorThread]: "Clear a task's persisted Director execution thread handle so the next execution run starts a fresh provider thread. Records a director_thread_reset task event.",
|
|
51354
52151
|
[mcpToolNames.updateWikiPage]: "Update a markdown Wiki page.",
|
|
51355
52152
|
[mcpToolNames.markSpecReady]: "Mark a wiki page (folder=Specs) as ready (or not) for execution-run gating. The Director sets this true after validating the spec; any edit to the page's markdown auto-resets it to false.",
|
|
51356
|
-
[mcpToolNames.runSpecConsensus]: 'Enqueue the consensus review gate over a Specs/{id} page. Returns IMMEDIATELY with one of: already_converged (a CONVERGED review already exists for the current content \u2014 proceed straight to create_execution_run, do NOT call mark_spec_ready, it is already set), already_reviewed (a terminal CONSENSUS_BLOCKED for this content), already_running (a consensus job is in flight), or queued (a new job was launched). The review runs in the background; its result/blocker arrives in the inbox (info on CONVERGED, handoff on failure) and is surfaced into the open chat. Do NOT call again for the same spec and do NOT poll. On queued/already_running, tell the user it was launched and end the turn. Pass extraHeads: ["researcher"] for specs referencing external libraries/APIs, or ["debugger"] for root-cause/investigation specs to add opt-in specialist reviewers on top of the fixed four.',
|
|
52153
|
+
[mcpToolNames.runSpecConsensus]: 'Enqueue the consensus review gate over a Specs/{id} page. Returns IMMEDIATELY with one of: already_converged (a CONVERGED review already exists for the current content \u2014 proceed straight to create_execution_run, do NOT call mark_spec_ready, it is already set), already_reviewed (a terminal CONSENSUS_BLOCKED for this content), already_running (a consensus job is in flight), or queued (a new job was launched). The review runs in the background; its result/blocker arrives in the inbox (info on CONVERGED, handoff on failure) and is surfaced into the open chat. Do NOT call again for the same spec and do NOT poll. On queued/already_running, tell the user it was launched and end the turn. Pass extraHeads: ["researcher"] for specs referencing external libraries/APIs, or ["debugger"] for root-cause/investigation specs to add opt-in specialist reviewers on top of the fixed four. For a NON-code task (domain seo/marketing/content, ADR 0020) the spec is REVIEW-ONLY: do NOT call create_execution_run (it fail-closes with non_code_execution_unsupported until Phase 2) \u2014 report that the spec was reviewed and end the turn.',
|
|
51357
52154
|
[mcpToolNames.runCodeReviewerGate]: `Review the current branch's code diff (vs baseRef) with a Code Reviewer gate \u2014 independent reviewers iterate review\u2192fix\u2192re-check (checkpoint, scope-limited reviser edits, typecheck/build fast-gate, rollback) until the diff converges or is blocked. Run before opening a PR. Pass extraHeads: ["security-analyst"] for sensitive diffs (auth, secrets, input handling, new endpoints).`,
|
|
51358
52155
|
[mcpToolNames.appendExecutionArtifact]: "Append a durable artifact (plan, checkpoint, command output, test result, diff summary, self-review, error, deploy attempt, or handoff judgement) to an execution run.",
|
|
51359
52156
|
[mcpToolNames.recordProducedUrl]: 'Call this after publishing each web page produced by the task, so the UI link opens the page itself instead of the deployed site root. Pass the page\'s full https URL (for external pages, e.g. "https://dev.to/<slug>") OR a path starting with "/" for the deploy environment (e.g. "/blog/<slug>") \u2014 paths are resolved against the run\'s deployed env URL at render time. Optional `label` (\u226440 chars) is shown next to the link; omit and the UI defaults to the page title. Call once per page; multiple calls dedupe by resolved URL in the UI.',
|
|
@@ -51810,7 +52607,12 @@ function createMrrlinTools(options) {
|
|
|
51810
52607
|
if (!page || page.folder !== "Specs") {
|
|
51811
52608
|
return await c.markSpecReady({ projectSlug, pageId, ready });
|
|
51812
52609
|
}
|
|
51813
|
-
const
|
|
52610
|
+
const reviewDomain = await resolveSpecDomain(c, projectSlug, { id: page.id, taskId: page.taskId ?? null });
|
|
52611
|
+
const review = await c.getSpecConsensusReview(
|
|
52612
|
+
projectSlug,
|
|
52613
|
+
pageId,
|
|
52614
|
+
domainScopedReviewHash(page.contentHash, reviewDomain)
|
|
52615
|
+
);
|
|
51814
52616
|
if (review && review.outcome === "CONVERGED") {
|
|
51815
52617
|
try {
|
|
51816
52618
|
const promotion = await c.promoteSpecReadyCas(projectSlug, pageId, {
|
|
@@ -52585,6 +53387,13 @@ Usage:
|
|
|
52585
53387
|
non-loopback URL (see \`mrrlin-mcp serve\` above).
|
|
52586
53388
|
URL and MRRLIN_STAGING are mutually exclusive at
|
|
52587
53389
|
bake time \u2014 URL wins, mirrors serve's resolver.
|
|
53390
|
+
Re-running BARE (no env vars) over a block you
|
|
53391
|
+
already pinned to staging/a remote target is safe:
|
|
53392
|
+
it keeps your env verbatim (reported as
|
|
53393
|
+
'env-preserved') instead of conflicting, so the
|
|
53394
|
+
same setup command doubles as an update command.
|
|
53395
|
+
To CHANGE or REMOVE the baked target, re-run with
|
|
53396
|
+
--force (or the new env vars).
|
|
52588
53397
|
--force replaces an existing conflicting block.
|
|
52589
53398
|
--force-prompts overwrites prompt files that
|
|
52590
53399
|
already exist with different content. WITHOUT it,
|
|
@@ -52631,8 +53440,18 @@ Usage:
|
|
|
52631
53440
|
- MRRLIN_DIRECTOR_BRIDGE_CODEX_IDLE_SECONDS (default 300;
|
|
52632
53441
|
how long the warm codex app-server is kept alive
|
|
52633
53442
|
between turns before it is released)
|
|
52634
|
-
-
|
|
52635
|
-
|
|
53443
|
+
- MRRLIN_DIRECTOR_BRIDGE_EXEC_TURN_IDLE_TIMEOUT_SECONDS (default 600;
|
|
53444
|
+
an execution turn is cut only after this long with NO codex
|
|
53445
|
+
signal at all \u2014 long-but-active turns keep running. A stall
|
|
53446
|
+
earns one automatic retry)
|
|
53447
|
+
- MRRLIN_DIRECTOR_BRIDGE_EXEC_RUN_MAX_SECONDS (default 3600;
|
|
53448
|
+
overall wall-clock budget for the active-turn portion of one
|
|
53449
|
+
claim attempt, measured from claim. Exceeding it hands off with
|
|
53450
|
+
NO retry. Setup/post-turn hangs are bounded by the stale reaper,
|
|
53451
|
+
not by this budget)
|
|
53452
|
+
- MRRLIN_DIRECTOR_BRIDGE_EXEC_TURN_TIMEOUT_SECONDS (DEPRECATED;
|
|
53453
|
+
unset by default. When set, an extra per-turn hard cap: the
|
|
53454
|
+
effective turn deadline becomes min(this, remaining run budget))
|
|
52636
53455
|
- MRRLIN_DIRECTOR_BRIDGE_CONTEXT_MAX_ARTIFACTS (default 50;
|
|
52637
53456
|
most-recent artifacts kept in the execution context bundle)
|
|
52638
53457
|
|
|
@@ -52747,6 +53566,10 @@ async function main() {
|
|
|
52747
53566
|
});
|
|
52748
53567
|
process.stderr.write(`[mrrlin-mcp install-codex] ${result.action} ${result.configPath}
|
|
52749
53568
|
`);
|
|
53569
|
+
for (const preserved of result.preservedEnvBlocks) {
|
|
53570
|
+
process.stderr.write(`${renderEnvPreservedNotice(preserved.blockId, preserved.envKeys)}
|
|
53571
|
+
`);
|
|
53572
|
+
}
|
|
52750
53573
|
for (const p of result.prompts) {
|
|
52751
53574
|
process.stderr.write(`[mrrlin-mcp install-codex] prompt ${p.name}: ${p.action} ${p.promptPath}
|
|
52752
53575
|
`);
|