@mrrlin-dev/mcp 0.3.60 → 0.3.64

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.
Files changed (2) hide show
  1. package/dist/bin.cjs +1053 -159
  2. package/package.json +4 -4
package/dist/bin.cjs CHANGED
@@ -6543,7 +6543,7 @@ var require_truncate = __commonJS({
6543
6543
  var parse5 = require_parse2();
6544
6544
  var constants = require_constants2();
6545
6545
  var SemVer = require_semver();
6546
- var truncate2 = (version2, truncation, options) => {
6546
+ var truncate3 = (version2, truncation, options) => {
6547
6547
  if (!constants.RELEASE_TYPES.includes(truncation)) {
6548
6548
  return null;
6549
6549
  }
@@ -6573,7 +6573,7 @@ var require_truncate = __commonJS({
6573
6573
  var isPrerelease = (type) => {
6574
6574
  return type.startsWith("pre");
6575
6575
  };
6576
- module2.exports = truncate2;
6576
+ module2.exports = truncate3;
6577
6577
  }
6578
6578
  });
6579
6579
 
@@ -7611,7 +7611,7 @@ var require_semver2 = __commonJS({
7611
7611
  var lte = require_lte();
7612
7612
  var cmp = require_cmp();
7613
7613
  var coerce = require_coerce();
7614
- var truncate2 = require_truncate();
7614
+ var truncate3 = require_truncate();
7615
7615
  var Comparator = require_comparator();
7616
7616
  var Range = require_range();
7617
7617
  var satisfies = require_satisfies();
@@ -7650,7 +7650,7 @@ var require_semver2 = __commonJS({
7650
7650
  lte,
7651
7651
  cmp,
7652
7652
  coerce,
7653
- truncate: truncate2,
7653
+ truncate: truncate3,
7654
7654
  Comparator,
7655
7655
  Range,
7656
7656
  satisfies,
@@ -36432,6 +36432,13 @@ var openApiOperationIds = {
36432
36432
  restartExecutionRun: "restartExecutionRun",
36433
36433
  resolveHandoff: "resolveHandoff",
36434
36434
  touchExecutionRun: "touchExecutionRun",
36435
+ getTaskState: "getTaskState",
36436
+ upsertTaskState: "upsertTaskState",
36437
+ retireThread: "retireThread",
36438
+ // Run-scoped, model-facing half of the resume contract (D8). Distinct from upsertTaskState:
36439
+ // that one is the server's own write (whole record, explicit lastRunSeq), this one is the
36440
+ // executor reporting narrative only, with the ordering value derived server-side from the run.
36441
+ reportTaskState: "reportTaskState",
36435
36442
  issueAgentCredential: "issueAgentCredential",
36436
36443
  listAgentCredentials: "listAgentCredentials",
36437
36444
  revokeAgentCredential: "revokeAgentCredential",
@@ -37463,7 +37470,55 @@ var mrrlinExecutionRunSchema = external_exports.object({
37463
37470
  status: mrrlinExecutionRunStatusSchema,
37464
37471
  taskId: mrrlinTaskIdSchema,
37465
37472
  updatedAt: external_exports.string().datetime(),
37466
- worktreeId: external_exports.string().min(1).nullable()
37473
+ worktreeId: external_exports.string().min(1).nullable(),
37474
+ stateSeq: external_exports.number().int(),
37475
+ promptBytes: external_exports.number().int().nullable(),
37476
+ turnCount: external_exports.number().int().nullable(),
37477
+ inputTokens: external_exports.number().int().nullable(),
37478
+ cachedInputTokens: external_exports.number().int().nullable(),
37479
+ outputTokens: external_exports.number().int().nullable(),
37480
+ effort: external_exports.enum(["none", "minimal", "low", "medium", "high", "xhigh"]).nullable(),
37481
+ effortEscalationReason: external_exports.string().min(1).nullable()
37482
+ });
37483
+ var mrrlinTaskStateSchema = external_exports.object({
37484
+ projectSlug: mrrlinProjectSlugSchema,
37485
+ taskId: mrrlinTaskIdSchema,
37486
+ goal: external_exports.string().min(1).nullable(),
37487
+ acceptedRequirements: external_exports.string().min(1).nullable(),
37488
+ decisions: external_exports.string().min(1).nullable(),
37489
+ relevantFiles: external_exports.string().min(1).nullable(),
37490
+ filesChanged: external_exports.string().min(1).nullable(),
37491
+ latestVerification: external_exports.string().min(1).nullable(),
37492
+ openProblems: external_exports.string().min(1).nullable(),
37493
+ nextActions: external_exports.string().min(1).nullable(),
37494
+ lastRunSeq: external_exports.number().int(),
37495
+ threadRunCount: external_exports.number().int(),
37496
+ retiredThreadId: external_exports.string().min(1).nullable(),
37497
+ updatedAt: external_exports.string().datetime()
37498
+ });
37499
+ var mrrlinTaskStateUpsertSchema = external_exports.object({
37500
+ goal: external_exports.string().min(1).nullable(),
37501
+ acceptedRequirements: external_exports.string().min(1).nullable(),
37502
+ decisions: external_exports.string().min(1).nullable(),
37503
+ relevantFiles: external_exports.string().min(1).nullable(),
37504
+ filesChanged: external_exports.string().min(1).nullable(),
37505
+ latestVerification: external_exports.string().min(1).nullable(),
37506
+ openProblems: external_exports.string().min(1).nullable(),
37507
+ nextActions: external_exports.string().min(1).nullable(),
37508
+ lastRunSeq: external_exports.number().int()
37509
+ });
37510
+ var mrrlinTaskStateReportSchema = external_exports.object({
37511
+ goal: external_exports.string().min(1).nullish(),
37512
+ acceptedRequirements: external_exports.string().min(1).nullish(),
37513
+ decisions: external_exports.string().min(1).nullish(),
37514
+ openProblems: external_exports.string().min(1).nullish(),
37515
+ nextActions: external_exports.string().min(1).nullish()
37516
+ });
37517
+ var taskStateResponseSchema = external_exports.object({
37518
+ data: mrrlinTaskStateSchema.nullable()
37519
+ });
37520
+ var retireThreadResponseSchema = external_exports.object({
37521
+ data: external_exports.object({ retiredThreadId: external_exports.string().min(1).nullable() })
37467
37522
  });
37468
37523
  var mrrlinExecutionRunCreateSchema = external_exports.object({
37469
37524
  actor: external_exports.string().min(1),
@@ -37495,7 +37550,18 @@ var mrrlinExecutionRunUpdateSchema = external_exports.object({
37495
37550
  prodWorkflowId: external_exports.number().int().positive().nullable().optional(),
37496
37551
  pullRequestUrl: external_exports.string().min(1).nullable().optional(),
37497
37552
  status: mrrlinExecutionRunStatusSchema.optional(),
37498
- worktreeId: external_exports.string().min(1).nullable().optional()
37553
+ worktreeId: external_exports.string().min(1).nullable().optional(),
37554
+ // Slice 6 (cost telemetry) + Slice 7 (effort). All nullable and optional: per D6, NULL means
37555
+ // "not measured" and must never be confused with a measured zero, so a disabled slice omits its
37556
+ // fields entirely rather than writing 0. `nonnegative` rather than `positive` because a real
37557
+ // measured zero is legitimate here (a turn can genuinely produce 0 output tokens).
37558
+ promptBytes: external_exports.number().int().nonnegative().nullable().optional(),
37559
+ turnCount: external_exports.number().int().nonnegative().nullable().optional(),
37560
+ inputTokens: external_exports.number().int().nonnegative().nullable().optional(),
37561
+ cachedInputTokens: external_exports.number().int().nonnegative().nullable().optional(),
37562
+ outputTokens: external_exports.number().int().nonnegative().nullable().optional(),
37563
+ effort: external_exports.enum(["none", "minimal", "low", "medium", "high", "xhigh"]).nullable().optional(),
37564
+ effortEscalationReason: external_exports.string().min(1).nullable().optional()
37499
37565
  }).refine((value) => Object.keys(value).length > 0, {
37500
37566
  message: "ExecutionRunUpdate requires at least one field"
37501
37567
  });
@@ -38274,6 +38340,117 @@ function applyHistoricalBudgetIfNeeded(input) {
38274
38340
  droppedHandles
38275
38341
  };
38276
38342
  }
38343
+ function renderTaskStateSection(state) {
38344
+ const rows = [
38345
+ ["goal", state.goal],
38346
+ ["accepted requirements", state.acceptedRequirements],
38347
+ ["decisions (and why)", state.decisions],
38348
+ ["relevant files", state.relevantFiles],
38349
+ ["files changed", state.filesChanged],
38350
+ ["latest verification", state.latestVerification],
38351
+ ["open problems", state.openProblems],
38352
+ ["next actions", state.nextActions]
38353
+ ];
38354
+ const present = rows.filter(([, value]) => typeof value === "string" && value.trim().length > 0);
38355
+ if (present.length === 0) {
38356
+ return "## Task state (normative)\n(no state recorded yet \u2014 establish it before ending your turn)";
38357
+ }
38358
+ const lines2 = ["## Task state (normative)"];
38359
+ for (const [label, value] of present) {
38360
+ lines2.push(`${label}: ${value}`);
38361
+ }
38362
+ return lines2.join("\n");
38363
+ }
38364
+ function renderRunDelta(input) {
38365
+ const sections = [
38366
+ renderDeltaTaskStatus(input.task),
38367
+ renderDeltaRuns(input.newlyFinishedRuns),
38368
+ renderDeltaArtifacts(input.newArtifacts, input.omittedArtifactCount ?? 0)
38369
+ ];
38370
+ return sections.join("\n\n");
38371
+ }
38372
+ function renderDeltaArtifacts(artifacts, omittedCount) {
38373
+ if (artifacts.length === 0 && omittedCount === 0)
38374
+ return "## New artifacts since your last turn\n(none)";
38375
+ const lines2 = ["## New artifacts since your last turn"];
38376
+ if (omittedCount > 0) {
38377
+ lines2.push(`(+${omittedCount} older artifact${omittedCount === 1 ? "" : "s"} omitted)`);
38378
+ }
38379
+ for (const artifact of artifacts) {
38380
+ if (artifact.kind === "file") {
38381
+ const p = artifact.payload;
38382
+ if (p.filename && p.stableUrl) {
38383
+ lines2.push(`- ${artifact.createdAt} [file] ${p.filename} (${p.class ?? "?"}, ${p.sizeBytes ?? "?"} bytes) ${p.stableUrl}`);
38384
+ continue;
38385
+ }
38386
+ }
38387
+ lines2.push(`- ${artifact.createdAt} [${artifact.kind}] run=${artifact.runId} ${JSON.stringify(artifact.payload)}`);
38388
+ }
38389
+ return lines2.join("\n");
38390
+ }
38391
+ function renderDeltaTaskStatus(task) {
38392
+ const lines2 = ["## Since your last turn", `task ${task.id} status: ${task.status}`];
38393
+ if (task.latestJudgement) {
38394
+ lines2.push(`latest judgement (${task.latestJudgementKind ?? "unknown"}): ${task.latestJudgement}`);
38395
+ }
38396
+ return lines2.join("\n");
38397
+ }
38398
+ function renderDeltaRuns(runs) {
38399
+ if (runs.length === 0)
38400
+ return "## Runs completed since your last turn\n(none)";
38401
+ const lines2 = ["## Runs completed since your last turn"];
38402
+ for (const run3 of runs) {
38403
+ lines2.push(`- ${run3.id} [${run3.status}] finished ${run3.finishedAt ?? run3.createdAt}`);
38404
+ }
38405
+ return lines2.join("\n");
38406
+ }
38407
+
38408
+ // ../../packages/codex-client/dist/token-usage.js
38409
+ var TOKEN_USAGE_NOTIFICATION_METHOD = "thread/tokenUsage/updated";
38410
+ function readCount(source, key) {
38411
+ const value = source[key];
38412
+ return typeof value === "number" && Number.isFinite(value) ? value : null;
38413
+ }
38414
+ function extractTurnTokenUsage(method, params) {
38415
+ if (method !== TOKEN_USAGE_NOTIFICATION_METHOD)
38416
+ return null;
38417
+ if (params === null || typeof params !== "object")
38418
+ return null;
38419
+ const usage = params.tokenUsage;
38420
+ if (usage === null || typeof usage !== "object")
38421
+ return null;
38422
+ const last = usage.last;
38423
+ if (last === null || typeof last !== "object")
38424
+ return null;
38425
+ const block = last;
38426
+ const extracted = {
38427
+ inputTokens: readCount(block, "inputTokens"),
38428
+ cachedInputTokens: readCount(block, "cachedInputTokens"),
38429
+ outputTokens: readCount(block, "outputTokens")
38430
+ };
38431
+ if (extracted.inputTokens === null && extracted.cachedInputTokens === null && extracted.outputTokens === null) {
38432
+ return null;
38433
+ }
38434
+ return extracted;
38435
+ }
38436
+ function addTurnTokenUsage(total, turn) {
38437
+ if (turn === null)
38438
+ return total;
38439
+ if (total === null)
38440
+ return { ...turn };
38441
+ const add2 = (a, b) => {
38442
+ if (a === null)
38443
+ return b;
38444
+ if (b === null)
38445
+ return a;
38446
+ return a + b;
38447
+ };
38448
+ return {
38449
+ inputTokens: add2(total.inputTokens, turn.inputTokens),
38450
+ cachedInputTokens: add2(total.cachedInputTokens, turn.cachedInputTokens),
38451
+ outputTokens: add2(total.outputTokens, turn.outputTokens)
38452
+ };
38453
+ }
38277
38454
 
38278
38455
  // ../../packages/client/dist/agent-token-provider.js
38279
38456
  var AgentTokenProviderError = class extends Error {
@@ -38998,6 +39175,34 @@ function createMrrlinClient(config2) {
38998
39175
  throw error51;
38999
39176
  }
39000
39177
  },
39178
+ async getTaskState(projectSlug, taskId) {
39179
+ const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/state`);
39180
+ return taskStateResponseSchema.parse(body).data;
39181
+ },
39182
+ async upsertTaskState(projectSlug, taskId, input) {
39183
+ const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/state`, {
39184
+ body: JSON.stringify(mrrlinTaskStateUpsertSchema.parse(input)),
39185
+ headers: { "content-type": "application/json" },
39186
+ method: "PUT"
39187
+ });
39188
+ return taskStateResponseSchema.parse(body).data;
39189
+ },
39190
+ async retireThread(projectSlug, taskId, newThreadId) {
39191
+ const body = await request(`${projectPath(projectSlug)}/tasks/${encodeURIComponent(taskId)}/retire-thread`, {
39192
+ body: JSON.stringify({ newThreadId }),
39193
+ headers: { "content-type": "application/json" },
39194
+ method: "POST"
39195
+ });
39196
+ return retireThreadResponseSchema.parse(body).data.retiredThreadId;
39197
+ },
39198
+ async reportTaskState(projectSlug, runId, report) {
39199
+ const body = await request(`${projectPath(projectSlug)}/execution-runs/${encodeURIComponent(runId)}/task-state`, {
39200
+ body: JSON.stringify(mrrlinTaskStateReportSchema.parse(report)),
39201
+ headers: { "content-type": "application/json" },
39202
+ method: "POST"
39203
+ });
39204
+ return taskStateResponseSchema.parse(body).data;
39205
+ },
39001
39206
  async resolveHandoff(projectSlug, runId, input) {
39002
39207
  const body = await request(`${projectPath(projectSlug)}/execution-runs/${encodeURIComponent(runId)}/resolve-handoff`, {
39003
39208
  body: JSON.stringify(input),
@@ -39201,6 +39406,36 @@ function createMrrlinClient(config2) {
39201
39406
  };
39202
39407
  }
39203
39408
 
39409
+ // src/describe-error.ts
39410
+ var MAX_DETAIL_CHARS = 200;
39411
+ function truncate(value) {
39412
+ return value.length > MAX_DETAIL_CHARS ? value.slice(0, MAX_DETAIL_CHARS) : value;
39413
+ }
39414
+ function codeAndMessage(value) {
39415
+ if (!value || typeof value !== "object") return null;
39416
+ const obj = value;
39417
+ const code = typeof obj.code === "string" ? obj.code : null;
39418
+ const message = typeof obj.message === "string" ? obj.message : null;
39419
+ if (code) return message ? `${code}: ${message}` : code;
39420
+ return message;
39421
+ }
39422
+ function detailFromCause(cause) {
39423
+ if (!cause || typeof cause !== "object") return null;
39424
+ const direct = codeAndMessage(cause);
39425
+ const errors = cause.errors;
39426
+ const hasCode = typeof cause.code === "string";
39427
+ if (!hasCode && Array.isArray(errors) && errors.length > 0) {
39428
+ const fromFirst = codeAndMessage(errors[0]);
39429
+ if (fromFirst) return fromFirst;
39430
+ }
39431
+ return direct;
39432
+ }
39433
+ function describeError(error51) {
39434
+ if (!(error51 instanceof Error)) return String(error51);
39435
+ const detail = detailFromCause(error51.cause);
39436
+ return detail ? `${error51.message} (${truncate(detail)})` : error51.message;
39437
+ }
39438
+
39204
39439
  // src/token-refresher.ts
39205
39440
  function createTokenRefresher(opts) {
39206
39441
  const setIntervalFn = opts.setIntervalFn ?? setInterval;
@@ -39217,7 +39452,7 @@ function createTokenRefresher(opts) {
39217
39452
  opts.onOk?.();
39218
39453
  } catch (error51) {
39219
39454
  process.stderr.write(
39220
- `[mrrlin-mcp director-bridge] token refresh failed (will retry next tick): ${error51 instanceof Error ? error51.message : String(error51)}
39455
+ `[mrrlin-mcp director-bridge] token refresh failed (will retry next tick): ${describeError(error51)}
39221
39456
  `
39222
39457
  );
39223
39458
  opts.onError?.(error51);
@@ -39256,29 +39491,117 @@ function createServiceHealth() {
39256
39491
  }
39257
39492
 
39258
39493
  // src/turn-lock.ts
39494
+ var CHAT_LIMIT = 5;
39495
+ var AUTONOMOUS_LIMIT = 10;
39259
39496
  function createTurnLock() {
39260
- let tail2 = Promise.resolve();
39261
- let active = 0;
39262
- return {
39263
- runExclusive(fn) {
39264
- active += 1;
39265
- const run3 = tail2.then(() => fn());
39266
- tail2 = run3.then(
39267
- () => void 0,
39268
- () => void 0
39269
- );
39270
- run3.then(
39271
- () => {
39272
- active -= 1;
39273
- },
39274
- () => {
39275
- active -= 1;
39497
+ let chatTotalActive = 0;
39498
+ let autonomousTotalActive = 0;
39499
+ let chatRunning = 0;
39500
+ let autonomousRunning = 0;
39501
+ const chatQueue = [];
39502
+ const execQueue = [];
39503
+ const asyncQueue = [];
39504
+ let lastServed = null;
39505
+ let autonomousBound = false;
39506
+ const logBound = (queued) => {
39507
+ console.warn(
39508
+ `[mrrlin-mcp director-bridge] warn autonomous_ceiling_bound {"limit":${AUTONOMOUS_LIMIT},"queued":${queued}}`
39509
+ );
39510
+ };
39511
+ const maybeEnterBound = () => {
39512
+ if (!autonomousBound && autonomousRunning === AUTONOMOUS_LIMIT && execQueue.length + asyncQueue.length > 0) {
39513
+ autonomousBound = true;
39514
+ logBound(execQueue.length + asyncQueue.length);
39515
+ }
39516
+ };
39517
+ const maybeLeaveBound = () => {
39518
+ if (autonomousBound && (autonomousRunning < AUTONOMOUS_LIMIT || execQueue.length + asyncQueue.length === 0)) {
39519
+ autonomousBound = false;
39520
+ logBound(execQueue.length + asyncQueue.length);
39521
+ }
39522
+ };
39523
+ const acquire = (lane, startRunner) => {
39524
+ if (lane === "chat") {
39525
+ if (chatRunning < CHAT_LIMIT) {
39526
+ chatRunning += 1;
39527
+ startRunner();
39528
+ } else {
39529
+ chatQueue.push(startRunner);
39530
+ }
39531
+ } else {
39532
+ if (autonomousRunning < AUTONOMOUS_LIMIT) {
39533
+ autonomousRunning += 1;
39534
+ startRunner();
39535
+ } else {
39536
+ if (lane === "exec") {
39537
+ execQueue.push(startRunner);
39538
+ } else {
39539
+ asyncQueue.push(startRunner);
39276
39540
  }
39277
- );
39278
- return run3;
39279
- },
39280
- activeCount: () => active
39541
+ maybeEnterBound();
39542
+ }
39543
+ }
39544
+ };
39545
+ const release = (lane) => {
39546
+ if (lane === "chat") {
39547
+ chatRunning -= 1;
39548
+ const next = chatQueue.shift();
39549
+ if (next) {
39550
+ chatRunning += 1;
39551
+ next();
39552
+ }
39553
+ } else {
39554
+ autonomousRunning -= 1;
39555
+ const execWait = execQueue.length > 0;
39556
+ const asyncWait = asyncQueue.length > 0;
39557
+ if (execWait || asyncWait) {
39558
+ let serveLane;
39559
+ if (execWait && !asyncWait) {
39560
+ serveLane = "exec";
39561
+ } else if (!execWait && asyncWait) {
39562
+ serveLane = "async";
39563
+ } else {
39564
+ serveLane = lastServed === "exec" ? "async" : "exec";
39565
+ }
39566
+ const queue = serveLane === "exec" ? execQueue : asyncQueue;
39567
+ const next = queue.shift();
39568
+ if (next) {
39569
+ autonomousRunning += 1;
39570
+ lastServed = serveLane;
39571
+ next();
39572
+ }
39573
+ }
39574
+ maybeLeaveBound();
39575
+ }
39576
+ };
39577
+ const run3 = async (lane, fn) => {
39578
+ if (lane === "chat") {
39579
+ chatTotalActive += 1;
39580
+ } else {
39581
+ autonomousTotalActive += 1;
39582
+ }
39583
+ return new Promise((resolve, reject) => {
39584
+ const start = () => {
39585
+ let bodyPromise;
39586
+ try {
39587
+ bodyPromise = fn();
39588
+ } catch (e) {
39589
+ bodyPromise = Promise.reject(e);
39590
+ }
39591
+ bodyPromise.then((v) => resolve(v)).catch((e) => reject(e)).finally(() => {
39592
+ if (lane === "chat") {
39593
+ chatTotalActive -= 1;
39594
+ } else {
39595
+ autonomousTotalActive -= 1;
39596
+ }
39597
+ release(lane);
39598
+ });
39599
+ };
39600
+ acquire(lane, start);
39601
+ });
39281
39602
  };
39603
+ const activeCount = () => chatTotalActive + autonomousTotalActive;
39604
+ return { run: run3, activeCount };
39282
39605
  }
39283
39606
 
39284
39607
  // src/self-update.ts
@@ -39642,7 +39965,7 @@ function startCodexProbe(deps = {}) {
39642
39965
  }
39643
39966
 
39644
39967
  // src/_generated/version.ts
39645
- var PKG_VERSION = "0.3.60";
39968
+ var PKG_VERSION = "0.3.64";
39646
39969
 
39647
39970
  // src/api-base-url-migrate.ts
39648
39971
  var import_node_fs2 = __toESM(require("node:fs"), 1);
@@ -40142,7 +40465,7 @@ async function attemptSchedulerTick(deps) {
40142
40465
  return { nextWakeAt: typeof data.nextWakeAt === "string" ? data.nextWakeAt : null, failed: false };
40143
40466
  } catch (error51) {
40144
40467
  process.stderr.write(
40145
- `[mrrlin-mcp director-bridge] scheduler tick failed: ${error51 instanceof Error ? error51.message : String(error51)}
40468
+ `[mrrlin-mcp director-bridge] scheduler tick failed: ${describeError(error51)}
40146
40469
  `
40147
40470
  );
40148
40471
  bridgeHealth?.recordFailure("schedulerTick", error51);
@@ -41831,6 +42154,11 @@ function renderMilestone(m, ctx) {
41831
42154
  return "Autodeploy is enabled, but this task has nothing deployable \u2014 skipping";
41832
42155
  case "daily_limit_reached":
41833
42156
  return "Daily autonomous operations limit reached \u2014 I will continue tomorrow";
42157
+ case "thread_retired": {
42158
+ if (ctx.reason === "bounded_no_state")
42159
+ return "Started a fresh conversation for this task \u2014 the previous one grew too long and no progress notes were saved, so some earlier reasoning may be lost";
42160
+ return "Started a fresh conversation for this task \u2014 the previous one grew too long, so I carried the saved progress notes over";
42161
+ }
41834
42162
  }
41835
42163
  }
41836
42164
 
@@ -41854,6 +42182,82 @@ async function postRunFeed(client, projectSlug, runId, taskId, milestone, ctx =
41854
42182
  }
41855
42183
  }
41856
42184
 
42185
+ // src/output-projection.ts
42186
+ var FAILURE_PATTERNS = [
42187
+ /^\s*(?:E|FAIL|ERROR|ERR!)\b/i,
42188
+ /\bTraceback \(most recent call last\)/,
42189
+ /^\s*at\s+.+:\d+:\d+/,
42190
+ // stack frames
42191
+ /\bAssertionError\b|\bexpected\b.*\breceived\b|\bexpected:\b/i,
42192
+ /\berror\b[:\s]|\bfailed\b[:\s]|\bfailure\b[:\s]/i,
42193
+ /^\s*not ok\b/,
42194
+ // TAP
42195
+ /\b(?:SyntaxError|TypeError|ReferenceError|RangeError)\b/,
42196
+ /^\s*✕|^\s*✗|^\s*×/,
42197
+ /\bexit(?:ed)? (?:code|status)\s*[:=]?\s*[1-9]/i
42198
+ ];
42199
+ var DEFAULT_OUTPUT_PROJECTION_MAX_CHARS = 2e3;
42200
+ function projectCommandOutput(output3, options = {}) {
42201
+ const maxChars = options.maxChars ?? DEFAULT_OUTPUT_PROJECTION_MAX_CHARS;
42202
+ const exitStatus = options.exitStatus ?? null;
42203
+ const fullOutputRef = options.fullOutputRef ?? null;
42204
+ const label = options.label ?? "command output";
42205
+ const lines2 = output3.split(/\r?\n/);
42206
+ const matched = [];
42207
+ for (let index = 0; index < lines2.length; index += 1) {
42208
+ const line = lines2[index] ?? "";
42209
+ if (FAILURE_PATTERNS.some((pattern) => pattern.test(line))) matched.push(index);
42210
+ }
42211
+ const candidates = matched.length > 0 ? matched : lines2.map((_, index) => index);
42212
+ const kept = [];
42213
+ let used = 0;
42214
+ for (let cursor = candidates.length - 1; cursor >= 0; cursor -= 1) {
42215
+ const index = candidates[cursor];
42216
+ if (index === void 0) continue;
42217
+ let text = lines2[index] ?? "";
42218
+ if (text.trim().length === 0) continue;
42219
+ if (text.length > maxChars) {
42220
+ text = `${text.slice(0, Math.max(0, maxChars - 14))}\u2026[line cut]`;
42221
+ }
42222
+ const cost = text.length + 1;
42223
+ if (used + cost > maxChars) break;
42224
+ kept.push({ index, text });
42225
+ used += cost;
42226
+ }
42227
+ kept.sort((a, b) => a.index - b.index);
42228
+ const failureLines = kept.map((entry) => entry.text);
42229
+ const carried = failureLines.reduce((total, line) => total + line.length + 1, 0);
42230
+ const omittedChars = Math.max(0, output3.length - carried);
42231
+ const parts = [`${label}:`];
42232
+ parts.push(exitStatus === null ? "exit status unknown" : `exit status ${exitStatus}`);
42233
+ parts.push(
42234
+ matched.length > 0 ? `${failureLines.length} of ${matched.length} failure-bearing line(s) shown` : `no failure markers found; showing the tail (${failureLines.length} line(s))`
42235
+ );
42236
+ if (omittedChars > 0) {
42237
+ parts.push(
42238
+ fullOutputRef ? `${omittedChars} chars omitted \u2014 retrieve ${fullOutputRef} for the full output` : `${omittedChars} chars omitted`
42239
+ );
42240
+ }
42241
+ return {
42242
+ exitStatus,
42243
+ failureLines,
42244
+ summary: parts.join(" "),
42245
+ omittedChars,
42246
+ fullOutputRef,
42247
+ usedTailFallback: matched.length === 0
42248
+ };
42249
+ }
42250
+ function renderOutputProjection(projection) {
42251
+ const lines2 = [projection.summary];
42252
+ if (projection.failureLines.length > 0) {
42253
+ lines2.push(...projection.failureLines);
42254
+ }
42255
+ return lines2.join("\n");
42256
+ }
42257
+ function outputProjectionDisabled(env = process.env) {
42258
+ return (env.MRRLIN_EXEC_OUTPUT_PROJECTION_DISABLE ?? "").trim() === "1";
42259
+ }
42260
+
41857
42261
  // src/deploy-gate.ts
41858
42262
  var DEFAULT_DEPLOY_POLL_BUDGET_SECONDS = 1200;
41859
42263
  var DEFAULT_PROD_GRACE_SECONDS = 900;
@@ -42209,10 +42613,48 @@ async function runDeployGateTick(deps, projectSlug, run3, task) {
42209
42613
  task,
42210
42614
  run: run3
42211
42615
  });
42616
+ const verificationAttempt = attemptIndex(artifacts, env, "verification") + 1;
42617
+ let outputRef = null;
42618
+ if (verdict.fullOutput && !outputProjectionDisabled()) {
42619
+ outputRef = `EA-veroutput-${env}-${verificationAttempt}`;
42620
+ const stored = await deps.client.appendExecutionArtifact(projectSlug, run3.id, {
42621
+ id: outputRef,
42622
+ kind: "command_output",
42623
+ payload: { env, source: "verification-turn", output: verdict.fullOutput }
42624
+ }).then(() => true).catch(() => false);
42625
+ if (!stored) outputRef = null;
42626
+ }
42627
+ const projection = verdict.fullOutput && !outputProjectionDisabled() ? projectCommandOutput(verdict.fullOutput, {
42628
+ // A model turn reports no exit code; claiming 0 would assert a success never observed.
42629
+ exitStatus: null,
42630
+ fullOutputRef: outputRef,
42631
+ label: `verification (${env})`
42632
+ }) : null;
42633
+ if (projection?.usedTailFallback) {
42634
+ console.warn(
42635
+ JSON.stringify({
42636
+ event: "output_projection_tail_fallback",
42637
+ projectSlug,
42638
+ runId: run3.id,
42639
+ env,
42640
+ detail: "no failure marker matched; the projection carries the tail instead"
42641
+ })
42642
+ );
42643
+ }
42212
42644
  await deps.client.appendExecutionArtifact(projectSlug, run3.id, {
42213
- id: `EA-verification-${env}-${attemptIndex(artifacts, env, "verification") + 1}`,
42645
+ id: `EA-verification-${env}-${verificationAttempt}`,
42214
42646
  kind: "verification",
42215
- payload: { env, verdict: verdict.verdict, summary: verdict.summary, turnRef: verdict.turnRef }
42647
+ payload: {
42648
+ env,
42649
+ verdict: verdict.verdict,
42650
+ summary: verdict.summary,
42651
+ turnRef: verdict.turnRef,
42652
+ ...projection ? {
42653
+ projection: renderOutputProjection(projection),
42654
+ fullOutputArtifactId: projection.fullOutputRef,
42655
+ omittedChars: projection.omittedChars
42656
+ } : {}
42657
+ }
42216
42658
  });
42217
42659
  if (verdict.verdict === "fail") {
42218
42660
  if (env === "prod" && task.autonomyLevel === "auto") {
@@ -42524,6 +42966,112 @@ function buildPullRequestBody(input) {
42524
42966
  ${lines2.join("\n")}`;
42525
42967
  }
42526
42968
 
42969
+ // src/context-compact/policy.ts
42970
+ var DEFAULT_THRESHOLD_CHARS = 12e3;
42971
+ var DEFAULT_MAX_ITEMS = 25;
42972
+ function readPositiveInt(value, fallback) {
42973
+ const raw = (value ?? "").trim();
42974
+ if (!raw) return fallback;
42975
+ const parsed = Number.parseInt(raw, 10);
42976
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
42977
+ }
42978
+ function readCompactPolicy(env = process.env) {
42979
+ const disabled = (env.MRRLIN_MCP_COMPACT_DISABLE ?? "").trim() === "1";
42980
+ return {
42981
+ enabled: !disabled,
42982
+ thresholdChars: readPositiveInt(env.MRRLIN_MCP_COMPACT_THRESHOLD_CHARS, DEFAULT_THRESHOLD_CHARS),
42983
+ maxItems: readPositiveInt(env.MRRLIN_MCP_COMPACT_MAX_ITEMS, DEFAULT_MAX_ITEMS),
42984
+ debug: (env.MRRLIN_MCP_COMPACT_DEBUG ?? "").trim() === "1",
42985
+ retrievalBudgetDisable: (env.MRRLIN_RETRIEVAL_BUDGET_DISABLE ?? "").trim() === "1"
42986
+ };
42987
+ }
42988
+ function warmDeltaDisabled(env = process.env) {
42989
+ return (env.MRRLIN_EXEC_WARM_DELTA_DISABLE ?? "").trim() === "1";
42990
+ }
42991
+ var DEFAULT_THREAD_MAX_RUNS = 12;
42992
+ var THREAD_MAX_RUNS_MIN = 4;
42993
+ var THREAD_MAX_RUNS_MAX = 50;
42994
+ var DEFAULT_THREAD_RETIRE_GRACE_RUNS = 6;
42995
+ var warnedOutOfRange = /* @__PURE__ */ new Set();
42996
+ function readBoundedInt(name, raw, fallback, min, max) {
42997
+ const trimmed = (raw ?? "").trim();
42998
+ if (!trimmed) return fallback;
42999
+ const parsed = Number.parseInt(trimmed, 10);
43000
+ if (Number.isInteger(parsed) && parsed >= min && parsed <= max) return parsed;
43001
+ if (!warnedOutOfRange.has(name)) {
43002
+ warnedOutOfRange.add(name);
43003
+ console.warn(
43004
+ JSON.stringify({
43005
+ event: "thread_retirement_bound_out_of_range",
43006
+ variable: name,
43007
+ provided: trimmed,
43008
+ accepted: `${min}-${max}`,
43009
+ usingDefault: fallback
43010
+ })
43011
+ );
43012
+ }
43013
+ return fallback;
43014
+ }
43015
+ function threadMaxRuns(env = process.env) {
43016
+ return readBoundedInt(
43017
+ "MRRLIN_EXEC_THREAD_MAX_RUNS",
43018
+ env.MRRLIN_EXEC_THREAD_MAX_RUNS,
43019
+ DEFAULT_THREAD_MAX_RUNS,
43020
+ THREAD_MAX_RUNS_MIN,
43021
+ THREAD_MAX_RUNS_MAX
43022
+ );
43023
+ }
43024
+ function threadRetireGraceRuns(env = process.env) {
43025
+ return readBoundedInt(
43026
+ "MRRLIN_EXEC_THREAD_RETIRE_GRACE_RUNS",
43027
+ env.MRRLIN_EXEC_THREAD_RETIRE_GRACE_RUNS,
43028
+ DEFAULT_THREAD_RETIRE_GRACE_RUNS,
43029
+ 0,
43030
+ THREAD_MAX_RUNS_MAX
43031
+ );
43032
+ }
43033
+ function threadRetireDisabled(env = process.env) {
43034
+ return (env.MRRLIN_EXEC_THREAD_RETIRE_DISABLE ?? "").trim() === "1";
43035
+ }
43036
+
43037
+ // src/thread-retirement.ts
43038
+ function decideThreadRetirement(input) {
43039
+ if (input.disabled) return { retire: false };
43040
+ if (input.threadRunCount < input.maxRuns) return { retire: false };
43041
+ if (input.hasNarrative) return { retire: true, reason: "bounded_run_count" };
43042
+ if (input.threadRunCount >= input.maxRuns + input.graceRuns) {
43043
+ return { retire: true, reason: "bounded_no_state" };
43044
+ }
43045
+ return { retire: false };
43046
+ }
43047
+ function hasNarrativeContent(state) {
43048
+ if (!state) return false;
43049
+ return [state.goal, state.acceptedRequirements, state.decisions, state.openProblems, state.nextActions].some(
43050
+ (field) => typeof field === "string" && field.trim().length > 0
43051
+ );
43052
+ }
43053
+
43054
+ // src/effort-tier.ts
43055
+ var DEFAULT_EFFORT_LEVEL = "medium";
43056
+ var MECHANICAL_EFFORT_LEVEL = "low";
43057
+ var ESCALATED_EFFORT_LEVEL = "high";
43058
+ function deriveEffort(input) {
43059
+ if (input.disabled) return null;
43060
+ if (input.impact === "high") {
43061
+ return {
43062
+ level: ESCALATED_EFFORT_LEVEL,
43063
+ escalationReason: "task impact is high (operator-set)"
43064
+ };
43065
+ }
43066
+ if (input.impact === "low") {
43067
+ return { level: MECHANICAL_EFFORT_LEVEL, escalationReason: null };
43068
+ }
43069
+ return { level: DEFAULT_EFFORT_LEVEL, escalationReason: null };
43070
+ }
43071
+ function effortDisabled(env = process.env) {
43072
+ return (env.MRRLIN_EXEC_EFFORT_DISABLE ?? "").trim() === "1";
43073
+ }
43074
+
42527
43075
  // src/execution-consumer.ts
42528
43076
  var DEFAULT_STALE_SECONDS = 300;
42529
43077
  var DEFAULT_MAX_LEASE_RECLAIMS = 3;
@@ -42654,6 +43202,9 @@ var BASE_INSTRUCTIONS_PREFIX = [
42654
43202
  " if you are blocked and a human must decide before the work can continue, leave it at `handoff_needed`.",
42655
43203
  "",
42656
43204
  "File artifacts:",
43205
+ "- Decide BEFORE writing your result whether the operator will scan it, compare across it,",
43206
+ " re-read it later, keep it, or share it. If so it is a file, not inline text \u2014 judge by use,",
43207
+ " not by topic, domain, or data type.",
42657
43208
  "- When your output is a file (markdown report, HTML, screenshot), upload it with",
42658
43209
  " `upload_artifact` and embed the returned markdown link \u2014 never inline large blobs",
42659
43210
  " into chat, spec, or note text.",
@@ -42661,8 +43212,28 @@ var BASE_INSTRUCTIONS_PREFIX = [
42661
43212
  " or evidence page will reference (180 days). Always pass taskId/runId when known.",
42662
43213
  "- Anything user-facing delivered as a file MUST be uploaded. Files are capped at 5MB \u2014",
42663
43214
  " split or summarize larger outputs.",
43215
+ "- `upload_artifact` needs a real local file path, so it presupposes you can write one. When this",
43216
+ " run path CAN write files, produce the artifact normally \u2014 that is the default. ONLY when it",
43217
+ " cannot write, do NOT attempt the file: put the summary in your result text and state what a",
43218
+ " file would have held. Never fake a path you did not write.",
43219
+ "",
43220
+ "Produced URLs \u2014 the operator's per-page links come only from you:",
43221
+ "- Call `record_produced_url` after publishing EACH web page this task produced, so the task UI",
43222
+ " links the page itself instead of the deployed site root. One call per page, not per run.",
43223
+ "- Pass either a full external https URL, or a path starting with `/` to resolve against the",
43224
+ " run's deployed env URL. An optional `label` (<=40 chars) replaces the default page title.",
43225
+ "- Never record the site root or a bare domain \u2014 the point is a link that opens the page itself.",
43226
+ " If a published page is not serving yet, record it once the deploy completes; never skip it.",
43227
+ " Nothing else populates these links, so a page you do not record has none at all.",
42664
43228
  "",
42665
43229
  "Browser tools (when the run needs to drive a browser):",
43230
+ "- Choose the browser FIRST when the target is a rendered UI, the question is inherently visual,",
43231
+ " the content sits behind a logged-in session, or reconstructing the answer would take many",
43232
+ " chained fetches. A single cheap fetch is still right for a plain one-shot HTTP/HTML check.",
43233
+ "- Escalate from a fetch to the browser in the same run on 401/403, a bot/consent/CAPTCHA wall,",
43234
+ " an empty JS-only shell, or a login gate. A 429 is rate limiting, not a wall \u2014 back off and",
43235
+ " retry once, escalating only if it persists. A failed fetch is a signal to change tool, not",
43236
+ " a finding \u2014 never conclude a public target is unverifiable without having tried the browser.",
42666
43237
  "- Use tools from the `mrrlin-browser` MCP server ONLY \u2014 browser_navigate, browser_click,",
42667
43238
  " browser_snapshot, browser_type, etc. Never invoke the same-named tools from any other",
42668
43239
  " browser MCP (`playwright`, `chrome-devtools`, `puppeteer`, ...) that may also be connected.",
@@ -42688,6 +43259,18 @@ var BASE_INSTRUCTIONS_PREFIX = [
42688
43259
  " ## Result \u2014 what was accomplished, key findings or metrics, follow-up tasks created.",
42689
43260
  " Do NOT remove earlier sections \u2014 they remain useful for context.",
42690
43261
  " Adapt depth to the task: a one-line fix needs one sentence, not four headings.",
43262
+ "",
43263
+ // Slice 4 of token-efficient-execution-runs (D8). This is the ONLY way the reasoning behind a
43264
+ // decision survives past this conversation: a thread is retired once it has accumulated too many
43265
+ // runs, and the next run then starts from this record plus the repository. Everything else the
43266
+ // next run receives (spec, run history, artifacts) is reconstructed from the database anyway —
43267
+ // what only the model holds is why it chose what it chose, and what it intended to do next.
43268
+ "BEFORE you end your turn, call `update_task_state` to record where this task now stands: the",
43269
+ "goal as you understand it, decisions you made AND WHY, problems still open, and the next",
43270
+ "concrete actions. A later run may start from a FRESH conversation that has none of this turn's",
43271
+ "context \u2014 that record is what it will read. A decision without its reason is the thing most",
43272
+ "often lost, so state the reason. Omit any field you learned nothing new about; an omitted field",
43273
+ "keeps its previous value. Do not restate the spec or the file list \u2014 the next run gets those.",
42691
43274
  ""
42692
43275
  ];
42693
43276
  var CONSUMER_BASE_INSTRUCTIONS = [
@@ -42923,13 +43506,15 @@ async function runHasLease(deps, projectSlug, runId, leaseId) {
42923
43506
  return false;
42924
43507
  }
42925
43508
  }
42926
- async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inputText, overallMs, phase) {
43509
+ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inputText, overallMs, phase, effort) {
42927
43510
  const mapper = new CodexBridgeEventMapper();
42928
43511
  mapper.reset();
42929
43512
  let turnError = null;
42930
43513
  let interruptFailed = false;
42931
43514
  let touch = () => {
42932
43515
  };
43516
+ let turnUsage = null;
43517
+ const promptBytes = Buffer.byteLength(inputText, "utf8");
42933
43518
  const turnStartedAt = Date.now();
42934
43519
  let lastActivityAt = turnStartedAt;
42935
43520
  const persist = async (event) => {
@@ -42945,7 +43530,7 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
42945
43530
  } catch {
42946
43531
  }
42947
43532
  };
42948
- return deps.runExclusive(async () => {
43533
+ return deps.run("exec", async () => {
42949
43534
  const pending = [];
42950
43535
  let resolveTurnEnded = null;
42951
43536
  const turnEnded = new Promise((resolve) => {
@@ -42958,6 +43543,8 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
42958
43543
  lastActivityAt = Date.now();
42959
43544
  touch();
42960
43545
  }
43546
+ const usage = !notifThreadId || notifThreadId === threadId ? extractTurnTokenUsage(method, params) : null;
43547
+ if (usage) turnUsage = usage;
42961
43548
  const events = mapper.mapNotification(
42962
43549
  { method, params },
42963
43550
  threadId
@@ -42983,7 +43570,8 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
42983
43570
  touch = activity.touch;
42984
43571
  const accepted = await codex.turn.start({
42985
43572
  threadId,
42986
- input: [{ type: "text", text: inputText }]
43573
+ input: [{ type: "text", text: inputText }],
43574
+ ...effort === void 0 ? {} : { effort }
42987
43575
  });
42988
43576
  acceptedTurnId = accepted?.turn?.id ?? null;
42989
43577
  await turnEnded;
@@ -43018,7 +43606,8 @@ async function driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, inp
43018
43606
  clearInterval(touchTimer);
43019
43607
  unsub();
43020
43608
  }
43021
- return turnError ? { ok: false, error: turnError, interruptFailed } : { ok: true };
43609
+ const telemetry = { promptBytes, usage: turnUsage };
43610
+ return turnError ? { ok: false, error: turnError, interruptFailed, telemetry } : { ok: true, telemetry };
43022
43611
  });
43023
43612
  }
43024
43613
  async function driveRun(deps, projectSlug, runId) {
@@ -43088,6 +43677,47 @@ async function driveRun(deps, projectSlug, runId) {
43088
43677
  } : context
43089
43678
  );
43090
43679
  bundle += await deployWorkflowReverifyNotice(deps.client, projectSlug);
43680
+ const runTelemetry = {
43681
+ promptBytes: 0,
43682
+ turnCount: 0,
43683
+ usage: null
43684
+ };
43685
+ const recordTurn = (outcome) => {
43686
+ runTelemetry.promptBytes += outcome.telemetry.promptBytes;
43687
+ runTelemetry.turnCount += 1;
43688
+ runTelemetry.usage = addTurnTokenUsage(runTelemetry.usage, outcome.telemetry.usage);
43689
+ return outcome;
43690
+ };
43691
+ const effortDecision = deriveEffort({
43692
+ impact: task.impact,
43693
+ type: task.type,
43694
+ disabled: effortDisabled()
43695
+ });
43696
+ const taskState = await deps.client.getTaskState(projectSlug, task.id).catch(() => null);
43697
+ const retirement = decideThreadRetirement({
43698
+ threadRunCount: taskState?.threadRunCount ?? 0,
43699
+ hasNarrative: hasNarrativeContent(taskState),
43700
+ disabled: threadRetireDisabled(),
43701
+ maxRuns: threadMaxRuns(),
43702
+ graceRuns: threadRetireGraceRuns()
43703
+ });
43704
+ const threadIdForAcquire = retirement.retire ? null : task.directorThreadId;
43705
+ if (retirement.retire) {
43706
+ console.warn(
43707
+ JSON.stringify({
43708
+ event: "thread_retirement_triggered",
43709
+ projectSlug,
43710
+ runId,
43711
+ taskId: task.id,
43712
+ reason: retirement.reason,
43713
+ threadRunCount: taskState?.threadRunCount ?? 0,
43714
+ retiringThreadId: task.directorThreadId
43715
+ })
43716
+ );
43717
+ }
43718
+ if (taskState) bundle += `
43719
+
43720
+ ${renderTaskStateSection(taskState)}`;
43091
43721
  const checkout = deps.checkoutRegistry?.get(projectSlug) ?? null;
43092
43722
  let sandbox = "read-only";
43093
43723
  let turnCwd = deps.codexCwd ?? null;
@@ -43163,7 +43793,7 @@ async function driveRun(deps, projectSlug, runId) {
43163
43793
  const pushFn = deps.pushRunBranch ?? pushRunBranch;
43164
43794
  try {
43165
43795
  const { threadId, resumed } = await acquireThread(codex, {
43166
- threadId: task.directorThreadId,
43796
+ threadId: threadIdForAcquire,
43167
43797
  startParams: {
43168
43798
  cwd: turnCwd,
43169
43799
  approvalPolicy: "never",
@@ -43179,9 +43809,36 @@ async function driveRun(deps, projectSlug, runId) {
43179
43809
  },
43180
43810
  onThreadIdChanged: async (newId) => {
43181
43811
  if (!await runHasLease(deps, projectSlug, runId, leaseId)) return;
43812
+ if (retirement.retire) return;
43182
43813
  await deps.client.updateTask(projectSlug, task.id, { directorThreadId: newId });
43183
43814
  }
43184
43815
  });
43816
+ if (retirement.retire && await runHasLease(deps, projectSlug, runId, leaseId)) {
43817
+ let persisted = true;
43818
+ const retiredThreadId = await deps.client.retireThread(projectSlug, task.id, threadId).catch(() => {
43819
+ persisted = false;
43820
+ return null;
43821
+ });
43822
+ if (persisted) {
43823
+ await postRunFeed(deps.client, projectSlug, runId, task.id, "thread_retired", {
43824
+ reason: retirement.reason
43825
+ });
43826
+ }
43827
+ console.warn(
43828
+ JSON.stringify({
43829
+ event: persisted ? "thread_retired" : "thread_retirement_write_failed",
43830
+ projectSlug,
43831
+ runId,
43832
+ taskId: task.id,
43833
+ reason: retirement.reason,
43834
+ retiredThreadId,
43835
+ newThreadId: threadId,
43836
+ // Says out loud what an unpersisted retirement costs, so the log line is actionable
43837
+ // rather than merely present.
43838
+ ...persisted ? {} : { impact: "counter not reset and task still on the old thread; later runs will re-retire until this write succeeds" }
43839
+ })
43840
+ );
43841
+ }
43185
43842
  if (!resumed) {
43186
43843
  const primingBudgetMs = remainingTurnBudgetMs();
43187
43844
  if (primingBudgetMs <= 0) {
@@ -43196,14 +43853,52 @@ async function driveRun(deps, projectSlug, runId) {
43196
43853
  );
43197
43854
  return;
43198
43855
  }
43199
- const primed = await driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, bundle, primingBudgetMs, "priming");
43856
+ const primed = recordTurn(
43857
+ await driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, bundle, primingBudgetMs, "priming", effortDecision?.level)
43858
+ );
43200
43859
  if (!primed.ok) {
43860
+ if (retirement.retire && task.directorThreadId) {
43861
+ await deps.client.updateTask(projectSlug, task.id, { directorThreadId: task.directorThreadId }).catch(() => {
43862
+ });
43863
+ console.warn(
43864
+ JSON.stringify({
43865
+ event: "thread_retirement_reverted",
43866
+ projectSlug,
43867
+ runId,
43868
+ taskId: task.id,
43869
+ reason: retirement.reason,
43870
+ restoredThreadId: task.directorThreadId,
43871
+ detail: "re-prime failed; predecessor thread restored"
43872
+ })
43873
+ );
43874
+ }
43201
43875
  await resetWedgedThread(deps, projectSlug, task.id, primed.interruptFailed);
43202
43876
  await failRun(deps, projectSlug, task, runId, leaseId, primed.error);
43203
43877
  return;
43204
43878
  }
43205
43879
  }
43206
- const executionPrompt = resumed ? `${bundle}
43880
+ const priorRuns = context.runs.filter((r) => r.id !== runId);
43881
+ const priorRun = priorRuns[priorRuns.length - 1] ?? null;
43882
+ const useDelta = resumed && !warmDeltaDisabled() && priorRun !== null;
43883
+ let resumeContent;
43884
+ if (useDelta) {
43885
+ const cutoffTime = priorRun.finishedAt ?? priorRun.createdAt;
43886
+ const newArtifacts = context.artifacts.filter((a) => a.createdAt > cutoffTime);
43887
+ resumeContent = renderRunDelta({
43888
+ task: {
43889
+ id: task.id,
43890
+ status: task.status,
43891
+ latestJudgement: task.latestJudgement,
43892
+ latestJudgementKind: task.latestJudgementKind
43893
+ },
43894
+ newlyFinishedRuns: [priorRun],
43895
+ newArtifacts,
43896
+ omittedArtifactCount: context.omittedArtifactCount
43897
+ });
43898
+ } else {
43899
+ resumeContent = bundle;
43900
+ }
43901
+ const executionPrompt = resumed ? `${resumeContent}
43207
43902
 
43208
43903
  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.`;
43209
43904
  const workBudgetMs = remainingTurnBudgetMs();
@@ -43219,7 +43914,9 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
43219
43914
  );
43220
43915
  return;
43221
43916
  }
43222
- const outcome = await driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, executionPrompt, workBudgetMs, "work");
43917
+ const outcome = recordTurn(
43918
+ await driveTurn(deps, codex, projectSlug, runId, leaseId, threadId, executionPrompt, workBudgetMs, "work", effortDecision?.level)
43919
+ );
43223
43920
  if (!outcome.ok) {
43224
43921
  await resetWedgedThread(deps, projectSlug, task.id, outcome.interruptFailed);
43225
43922
  await failRun(deps, projectSlug, task, runId, leaseId, outcome.error);
@@ -43243,16 +43940,19 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
43243
43940
  })
43244
43941
  );
43245
43942
  const completionPrompt = buildCompletionTurnPrompt(task.id, readiness.task.status);
43246
- const completionOutcome = await driveTurn(
43247
- deps,
43248
- codex,
43249
- projectSlug,
43250
- runId,
43251
- leaseId,
43252
- threadId,
43253
- completionPrompt,
43254
- completionBudget,
43255
- "completion"
43943
+ const completionOutcome = recordTurn(
43944
+ await driveTurn(
43945
+ deps,
43946
+ codex,
43947
+ projectSlug,
43948
+ runId,
43949
+ leaseId,
43950
+ threadId,
43951
+ completionPrompt,
43952
+ completionBudget,
43953
+ "completion",
43954
+ effortDecision?.level
43955
+ )
43256
43956
  );
43257
43957
  if (!completionOutcome.ok) {
43258
43958
  await resetWedgedThread(deps, projectSlug, task.id, completionOutcome.interruptFailed);
@@ -43431,9 +44131,97 @@ Continue executing task ${task.id}. Make the next concrete increment of progress
43431
44131
  error51 instanceof Error ? error51.message : String(error51)
43432
44132
  );
43433
44133
  } finally {
44134
+ await deps.client.updateExecutionRun(projectSlug, runId, {
44135
+ promptBytes: runTelemetry.promptBytes,
44136
+ turnCount: runTelemetry.turnCount,
44137
+ inputTokens: runTelemetry.usage?.inputTokens ?? null,
44138
+ cachedInputTokens: runTelemetry.usage?.cachedInputTokens ?? null,
44139
+ outputTokens: runTelemetry.usage?.outputTokens ?? null,
44140
+ // Slice 7 (7.4): record the level actually used and, when it was an escalation, what caused
44141
+ // it — so "why did this run cost that much?" is answerable after the fact. NULL when the
44142
+ // slice is disabled, which per D6 reads as "not set" rather than a level we chose.
44143
+ effort: effortDecision?.level ?? null,
44144
+ effortEscalationReason: effortDecision?.escalationReason ?? null
44145
+ }).catch((error51) => {
44146
+ console.warn(
44147
+ JSON.stringify({
44148
+ event: "run_telemetry_write_failed",
44149
+ projectSlug,
44150
+ runId,
44151
+ detail: error51 instanceof Error ? error51.message : String(error51)
44152
+ })
44153
+ );
44154
+ });
44155
+ await writeServerOwnedTaskState(deps, projectSlug, task.id, runId, leaseId).catch((error51) => {
44156
+ console.warn(
44157
+ JSON.stringify({
44158
+ event: "task_state_server_write_failed",
44159
+ projectSlug,
44160
+ runId,
44161
+ taskId: task.id,
44162
+ detail: error51 instanceof Error ? error51.message : String(error51)
44163
+ })
44164
+ );
44165
+ });
43434
44166
  disposeCodex();
43435
44167
  }
43436
44168
  }
44169
+ async function writeServerOwnedTaskState(deps, projectSlug, taskId, runId, leaseId) {
44170
+ const run3 = await deps.client.getExecutionRun(projectSlug, runId).catch(() => null);
44171
+ if (!run3) {
44172
+ console.warn(
44173
+ JSON.stringify({
44174
+ event: "task_state_server_write_skipped",
44175
+ projectSlug,
44176
+ runId,
44177
+ taskId,
44178
+ detail: "could not read the run's state_seq; skipped rather than writing a defaulted ordering value"
44179
+ })
44180
+ );
44181
+ return;
44182
+ }
44183
+ if (run3.leaseId !== null && run3.leaseId !== leaseId) {
44184
+ console.warn(
44185
+ JSON.stringify({
44186
+ event: "task_state_server_write_skipped",
44187
+ projectSlug,
44188
+ runId,
44189
+ taskId,
44190
+ detail: "the run is held by another lease (reclaimed); refusing to write state for a run we no longer own"
44191
+ })
44192
+ );
44193
+ return;
44194
+ }
44195
+ let artifacts = null;
44196
+ try {
44197
+ artifacts = await deps.client.listExecutionArtifacts(projectSlug, runId);
44198
+ } catch {
44199
+ artifacts = null;
44200
+ }
44201
+ let changed = null;
44202
+ if (artifacts !== null) {
44203
+ const summaries = artifacts.filter((artifact) => artifact.kind === "diff_summary").map((artifact) => JSON.stringify(artifact.payload));
44204
+ changed = summaries.length > 0 ? summaries.join("\n") : NO_FILE_CHANGES_THIS_RUN;
44205
+ }
44206
+ let latestVerification = null;
44207
+ if (artifacts !== null) {
44208
+ const verifications = artifacts.filter((artifact) => artifact.kind === "verification").map((artifact) => JSON.stringify(artifact.payload));
44209
+ latestVerification = verifications[verifications.length - 1] ?? NO_VERIFICATION_THIS_RUN;
44210
+ }
44211
+ await deps.client.upsertTaskState(projectSlug, taskId, {
44212
+ goal: null,
44213
+ acceptedRequirements: null,
44214
+ decisions: null,
44215
+ openProblems: null,
44216
+ nextActions: null,
44217
+ relevantFiles: null,
44218
+ filesChanged: changed,
44219
+ latestVerification,
44220
+ lastRunSeq: run3.stateSeq
44221
+ });
44222
+ }
44223
+ var NO_FILE_CHANGES_THIS_RUN = "no file changes recorded in this run";
44224
+ var NO_VERIFICATION_THIS_RUN = "not run in this run";
43437
44225
  function buildDeliverableAuthorPrompt(task, specMarkdown) {
43438
44226
  return [
43439
44227
  `Produce the deliverable for the task below. Write the FULL deliverable content as markdown.`,
@@ -43963,7 +44751,8 @@ async function driveAsyncJob(deps, projectSlug, jobId) {
43963
44751
  break;
43964
44752
  }
43965
44753
  try {
43966
- outcome = await deps.runExclusive(
44754
+ outcome = await deps.run(
44755
+ "async",
43967
44756
  () => descriptor2.run(
43968
44757
  { client: deps.client, cwd: deps.cwd, codexExecutable: deps.codexExecutable },
43969
44758
  input
@@ -44038,7 +44827,7 @@ async function runAsyncBackstopSweep(deps, drive = (slug, jobId) => driveAsyncJo
44038
44827
  } catch {
44039
44828
  queued = [];
44040
44829
  }
44041
- for (const job of queued) await drive(slug, job.id);
44830
+ await Promise.all(queued.map((job) => drive(slug, job.id)));
44042
44831
  }
44043
44832
  }
44044
44833
 
@@ -44506,13 +45295,16 @@ function createInactivityWatchdog(opts) {
44506
45295
  const clearT = opts.clearTimeoutFn ?? clearTimeout;
44507
45296
  const now = opts.now ?? Date.now;
44508
45297
  const suspended = opts.suspendedMs ?? (() => 0);
45298
+ const excluded = opts.idleExcludedMs ?? (() => 0);
44509
45299
  let idleTimer = null;
44510
45300
  let maxTimer = null;
44511
45301
  let done = false;
44512
45302
  const startAt = now();
44513
45303
  const suspendedAtStart = suspended();
45304
+ const excludedAtStart = excluded();
44514
45305
  let lastPokeAt = startAt;
44515
45306
  let suspendedAtLastPoke = suspendedAtStart;
45307
+ let excludedAtLastPoke = excludedAtStart;
44516
45308
  const expire = (reason) => {
44517
45309
  if (done) return;
44518
45310
  done = true;
@@ -44530,7 +45322,7 @@ function createInactivityWatchdog(opts) {
44530
45322
  };
44531
45323
  const onIdleDeadline = () => {
44532
45324
  if (done) return;
44533
- const awakeSilence = now() - lastPokeAt - (suspended() - suspendedAtLastPoke);
45325
+ const awakeSilence = now() - lastPokeAt - (suspended() - suspendedAtLastPoke) - (excluded() - excludedAtLastPoke);
44534
45326
  if (awakeSilence < opts.idleMs) {
44535
45327
  armIdle(opts.idleMs - awakeSilence);
44536
45328
  return;
@@ -44554,6 +45346,7 @@ function createInactivityWatchdog(opts) {
44554
45346
  if (idleTimer) clearT(idleTimer);
44555
45347
  lastPokeAt = now();
44556
45348
  suspendedAtLastPoke = suspended();
45349
+ excludedAtLastPoke = excluded();
44557
45350
  armIdle(opts.idleMs);
44558
45351
  },
44559
45352
  cancel: () => {
@@ -44563,8 +45356,8 @@ function createInactivityWatchdog(opts) {
44563
45356
  }
44564
45357
  };
44565
45358
  }
44566
- var DEFAULT_CHAT_TURN_IDLE_SECONDS = 180;
44567
- var DEFAULT_CHAT_TURN_MAX_SECONDS = 1800;
45359
+ var DEFAULT_CHAT_TURN_IDLE_SECONDS = 300;
45360
+ var DEFAULT_CHAT_TURN_MAX_SECONDS = 2700;
44568
45361
  function readPositiveSeconds(name, fallback) {
44569
45362
  const raw = (process.env[name] ?? "").trim();
44570
45363
  const parsed = Number.parseInt(raw, 10);
@@ -44577,6 +45370,64 @@ function chatTurnMaxMs() {
44577
45370
  return readPositiveSeconds("MRRLIN_DIRECTOR_BRIDGE_CHAT_TURN_MAX_SECONDS", DEFAULT_CHAT_TURN_MAX_SECONDS) * 1e3;
44578
45371
  }
44579
45372
 
45373
+ // src/tool-busy.ts
45374
+ function createToolBusyClock(opts) {
45375
+ const now = opts?.now ?? Date.now;
45376
+ const open = /* @__PURE__ */ new Set();
45377
+ let total = 0;
45378
+ let segmentStart = null;
45379
+ function start(itemId) {
45380
+ if (!open.has(itemId)) {
45381
+ open.add(itemId);
45382
+ if (open.size === 1) {
45383
+ segmentStart = now();
45384
+ }
45385
+ }
45386
+ }
45387
+ function end(itemId) {
45388
+ if (open.has(itemId)) {
45389
+ open.delete(itemId);
45390
+ if (open.size === 0) {
45391
+ const nowVal = now();
45392
+ if (segmentStart !== null) {
45393
+ total += nowVal - segmentStart;
45394
+ segmentStart = null;
45395
+ }
45396
+ }
45397
+ }
45398
+ }
45399
+ function endAll() {
45400
+ if (open.size > 0) {
45401
+ const nowVal = now();
45402
+ if (segmentStart !== null) {
45403
+ total += nowVal - segmentStart;
45404
+ segmentStart = null;
45405
+ }
45406
+ open.clear();
45407
+ }
45408
+ }
45409
+ function ms() {
45410
+ const nowVal = now();
45411
+ if (open.size > 0 && segmentStart !== null) {
45412
+ return total + (nowVal - segmentStart);
45413
+ }
45414
+ return total;
45415
+ }
45416
+ return { start, end, endAll, ms };
45417
+ }
45418
+ var TOOL_ITEM_TYPES = /* @__PURE__ */ new Set(["commandExecution", "mcpToolCall", "fileChange"]);
45419
+ function applyToolBusyNotification(clock, method, params) {
45420
+ if (method !== "item/started" && method !== "item/completed") return;
45421
+ if (typeof params !== "object" || params === null) return;
45422
+ const p = params;
45423
+ const item = typeof p.item === "object" && p.item !== null ? p.item : void 0;
45424
+ if (typeof item?.type !== "string" || !TOOL_ITEM_TYPES.has(item.type)) return;
45425
+ const id = typeof item.id === "string" ? item.id : typeof p.itemId === "string" ? p.itemId : void 0;
45426
+ if (id === void 0) return;
45427
+ if (method === "item/started") clock.start(id);
45428
+ else clock.end(id);
45429
+ }
45430
+
44580
45431
  // src/worker-token.ts
44581
45432
  var import_node_crypto7 = require("node:crypto");
44582
45433
  var import_node_fs9 = require("node:fs");
@@ -48581,11 +49432,16 @@ function parseVerdict2(text) {
48581
49432
  let last = null;
48582
49433
  while ((match = re.exec(text)) !== null) last = match;
48583
49434
  if (!last) {
48584
- return { verdict: "fail", summary: "no parseable VERDICT line in verification output", turnRef: null };
49435
+ return {
49436
+ verdict: "fail",
49437
+ summary: "no parseable VERDICT line in verification output",
49438
+ turnRef: null,
49439
+ fullOutput: text
49440
+ };
48585
49441
  }
48586
49442
  const verdict = (last[1] ?? "").toLowerCase() === "pass" ? "pass" : "fail";
48587
49443
  const summary = (last[2] ?? "").trim() || `verification ${verdict}`;
48588
- return { verdict, summary, turnRef: null };
49444
+ return { verdict, summary, turnRef: null, fullOutput: text };
48589
49445
  }
48590
49446
  function isBlockedVerificationHost(hostname3) {
48591
49447
  const h = hostname3.toLowerCase().replace(/^\[|\]$/g, "");
@@ -49482,7 +50338,9 @@ function buildThreadStartParams(msg, config2, desiredModel) {
49482
50338
  "- For public/third-party HTTP/HTML verification through `commandExecution`, prefer built-in",
49483
50339
  " tooling such as `curl`, `python3` stdlib `urllib.request`, and `html.parser`; do not assume",
49484
50340
  " third-party Python packages such as `requests` or `bs4` are installed. This permission covers",
49485
- " third-party sites ONLY \u2014 never call the Mrrlin API from `commandExecution`.",
50341
+ " third-party sites ONLY \u2014 never call the Mrrlin API from `commandExecution`. A cheap fetch is",
50342
+ " the default, not the only option: see the browser-vs-fetch contract below for when a fetch is",
50343
+ " the wrong tool outright and when a failed fetch must be escalated to `mrrlin-browser`.",
49486
50344
  "",
49487
50345
  "Handoff contract \u2014 durable operator action MUST NOT live only in chat:",
49488
50346
  "- If you conclude that a specific task needs human input, account access, manual evidence,",
@@ -49515,11 +50373,49 @@ function buildThreadStartParams(msg, config2, desiredModel) {
49515
50373
  " if it did not (that state REQUIRES a linked Evidence wiki page); or, if the effect still",
49516
50374
  " is not measurable, re-set `verificationDueAt` to a new date with a reason.",
49517
50375
  "",
49518
- "File artifacts: when you produce a file for the operator (report, HTML, screenshot),",
49519
- "upload it with `upload_artifact` (class=temp for one-off downloads, class=durable when a",
49520
- "spec/evidence page references it; pass taskId when known) and paste the returned markdown",
49521
- "link into your reply. For images, embed as an image (`![name](url)`) so the chat previews",
49522
- "it inline. Never inline file contents into chat. 5MB max per file.",
50376
+ "File artifacts contract \u2014 decide the delivery medium BEFORE you compose the reply:",
50377
+ "- First ask what the operator will DO with this output. If they will scan it, compare across",
50378
+ " it, re-read it later, keep it, or share it, it belongs in a FILE. If they need to know one",
50379
+ " thing right now, chat alone is right. Judge by use, not by topic, domain, or data type.",
50380
+ "- When it is a file: write it (`.md` for linear prose, `.html` when structure or layout",
50381
+ " carries meaning), upload with `upload_artifact` (class=temp for one-off downloads,",
50382
+ " class=durable when a spec/evidence page references it; pass taskId when known), and paste",
50383
+ " the returned markdown link into your reply.",
50384
+ "- The chat reply must still stand alone as a complete answer even if the file is never opened:",
50385
+ " the headline finding, the few points that matter, then the link. Never reduce it to",
50386
+ ' "see attached" or "details in the report".',
50387
+ "- For images, embed as an image (`![name](url)`) so the chat previews it inline.",
50388
+ "- Never inline file contents into chat. 5MB max per file.",
50389
+ "",
50390
+ "Browser-vs-fetch contract \u2014 pick whatever reaches ground truth in the fewest steps:",
50391
+ "- Reach for `mrrlin-browser` FIRST, without trying a fetch at all, when the target is a",
50392
+ " rendered UI, the question is inherently visual, the content sits behind a logged-in session,",
50393
+ " or reconstructing the answer would take many chained fetches.",
50394
+ "- A single cheap `curl`/`python3 urllib` fetch stays the right default for a plain one-shot",
50395
+ " HTTP/HTML check. Do not launch a browser for something one fetch already answers.",
50396
+ "- Escalate to `mrrlin-browser` in the SAME turn whenever a fetch cannot establish ground",
50397
+ " truth: 401/403, a bot/consent/CAPTCHA wall, an empty JS-only shell, or a login gate. A 429",
50398
+ " is rate limiting, not a wall \u2014 back off and retry the fetch once, and escalate only if it",
50399
+ " persists (bot detection does sometimes answer 429 to non-browser clients).",
50400
+ "- A failed fetch is a signal to change tool, never a finding. NEVER report that you could not",
50401
+ " check, verify, or reach a publicly reachable target until you have actually tried the",
50402
+ " browser on it in this turn.",
50403
+ "",
50404
+ "Initiative contract \u2014 do the obvious next step instead of offering it:",
50405
+ "- When a next step is cheap, clearly wanted, and finishable in this turn, just do it and",
50406
+ ' report the result. Do not end a reply with "if you want, I can also ..." for work you',
50407
+ " could have finished in the time it took to offer.",
50408
+ "- This never overrides the turn-lifecycle rules above: still split a large batch UP FRONT and",
50409
+ " still end at a clean boundary with an explicit request to continue. Ask when the work is",
50410
+ " genuinely big, needs an operator decision, or would change scope \u2014 never to dodge cheap work.",
50411
+ "",
50412
+ "Context-handle contract \u2014 a `mrrlin://` handle is a pointer, not an answer:",
50413
+ "- When tool output carries a `mrrlin://` handle and your answer depends on detail the compact",
50414
+ " envelope does not contain, call `retrieve_context` on that handle BEFORE answering.",
50415
+ "- Before any irreversible action on a handled record \u2014 closing a task, dispatching a deploy,",
50416
+ " writing a spec \u2014 call `retrieve_context` first. Never act on a compact envelope alone.",
50417
+ "- Never guess the missing detail, and never report a field as absent when it is merely",
50418
+ " un-rehydrated. Do not echo a raw handle to the operator as if it were data.",
49523
50419
  "",
49524
50420
  "Clarifying-question contract \u2014 ask early, never lose the question:",
49525
50421
  "- During a live chat turn, when a SPECIFIC task is ambiguous and the ambiguity affects",
@@ -50685,7 +51581,7 @@ function createBridgeMessageHandler(deps) {
50685
51581
  }
50686
51582
  };
50687
51583
  try {
50688
- await deps.runExclusive(async () => {
51584
+ await deps.run("chat", async () => {
50689
51585
  if (currentReplayBuf.state === "interrupted" || currentReplayBuf.state === "completed") {
50690
51586
  logCtx?.logger.logOut({
50691
51587
  spanId,
@@ -50714,7 +51610,9 @@ function createBridgeMessageHandler(deps) {
50714
51610
  outputEvents += 1;
50715
51611
  sendInTurn({ type: "event", directorSessionId, event });
50716
51612
  };
51613
+ const toolBusy = createToolBusyClock();
50717
51614
  const unsub = codexClient.onNotification((method, params) => {
51615
+ applyToolBusyNotification(toolBusy, method, params);
50718
51616
  pokeActivity();
50719
51617
  if (method === "account/rateLimits/updated") {
50720
51618
  const snapshot = params?.rateLimits ?? null;
@@ -50742,6 +51640,7 @@ function createBridgeMessageHandler(deps) {
50742
51640
  idleMs,
50743
51641
  maxMs,
50744
51642
  ...deps.suspendedMs ? { suspendedMs: deps.suspendedMs } : {},
51643
+ idleExcludedMs: () => toolBusy.ms(),
50745
51644
  onExpire: (reason) => {
50746
51645
  rejectWatchdog?.(
50747
51646
  new Error(
@@ -50845,6 +51744,7 @@ ${effectiveText}` : effectiveText;
50845
51744
  }
50846
51745
  watchdog.cancel();
50847
51746
  unsub();
51747
+ toolBusy.endAll();
50848
51748
  thread.cancel = null;
50849
51749
  }
50850
51750
  });
@@ -50964,8 +51864,8 @@ async function startDirectorBridge() {
50964
51864
  const turnLock = createTurnLock();
50965
51865
  let lastTurnEndedAt = Date.now();
50966
51866
  const wrappedTurnLock = {
50967
- runExclusive(fn) {
50968
- return turnLock.runExclusive(async () => {
51867
+ run(lane, fn) {
51868
+ return turnLock.run(lane, async () => {
50969
51869
  try {
50970
51870
  return await fn();
50971
51871
  } finally {
@@ -51008,7 +51908,11 @@ async function startDirectorBridge() {
51008
51908
  }
51009
51909
  });
51010
51910
  const codexProbeHandle = startCodexProbe();
51011
- const consumerState = { inFlightRunId: null, lastClaimAt: null };
51911
+ const consumerState = {
51912
+ inFlightRunIds: /* @__PURE__ */ new Set(),
51913
+ lastClaimedRunId: null,
51914
+ lastClaimAt: null
51915
+ };
51012
51916
  const health = createServiceHealth();
51013
51917
  try {
51014
51918
  migrateRetiredApiBaseUrls();
@@ -51087,7 +51991,7 @@ async function startDirectorBridge() {
51087
51991
  // handle returned by acquireExecutorCodex (Fix A + G), so there is no shared
51088
51992
  // mutable `activeExecutorClient` slot to race a concurrent chat/bootstrap turn.
51089
51993
  releaseCodex: () => codexKeepalive.release(),
51090
- runExclusive: wrappedTurnLock.runExclusive,
51994
+ run: wrappedTurnLock.run,
51091
51995
  staleSeconds,
51092
51996
  touchIntervalMs: Math.min(DEFAULT_TOUCH_INTERVAL_MS, Math.floor(staleSeconds * 1e3 / 2)),
51093
51997
  codexCwd: config2.codexCwd,
@@ -51114,7 +52018,7 @@ async function startDirectorBridge() {
51114
52018
  // ADR 0007 self-review: critique the committed diff on the executor's own client before
51115
52019
  // push/PR. Reuses the verification turn-runner (acquireThread + collect assistant text).
51116
52020
  selfReviewTurn: createSelfReviewTurn({
51117
- runTurn: (codexClient, cwd, prompt) => runCollectedCodexTurn(codexClient, prompt, cwd, SELF_REVIEW_BASE_INSTRUCTIONS)
52021
+ runTurn: (codexClient, cwd, prompt) => wrappedTurnLock.run("chat", () => runCollectedCodexTurn(codexClient, prompt, cwd, SELF_REVIEW_BASE_INSTRUCTIONS))
51118
52022
  }),
51119
52023
  // ADR 0015 deploy gate: after a pushed run reaches dev_deploy/prod_deploy, the backstop sweep
51120
52024
  // dispatches the deploy + polls + runs a verification turn against the deployed env URL in a
@@ -51153,7 +52057,7 @@ async function startDirectorBridge() {
51153
52057
  }
51154
52058
  };
51155
52059
  },
51156
- runTurn: (codexClient, prompt, cwd) => runCollectedCodexTurn(codexClient, prompt, cwd, VERIFICATION_BASE_INSTRUCTIONS)
52060
+ runTurn: (codexClient, prompt, cwd) => wrappedTurnLock.run("chat", () => runCollectedCodexTurn(codexClient, prompt, cwd, VERIFICATION_BASE_INSTRUCTIONS))
51157
52061
  }),
51158
52062
  budgetSeconds: deployPollBudgetSeconds()
51159
52063
  },
@@ -51178,7 +52082,7 @@ async function startDirectorBridge() {
51178
52082
  const bridgeInstanceId = (0, import_node_crypto8.randomUUID)();
51179
52083
  const asyncConsumerDeps = {
51180
52084
  client,
51181
- runExclusive: wrappedTurnLock.runExclusive,
52085
+ run: wrappedTurnLock.run,
51182
52086
  cwd: config2.codexCwd ?? process.cwd(),
51183
52087
  codexExecutable: config2.codexExecutable,
51184
52088
  staleSeconds,
@@ -51194,40 +52098,34 @@ async function startDirectorBridge() {
51194
52098
  sink({ type: "async-result", directorSessionId, projectSlug: slug, deliveryKind, body });
51195
52099
  }
51196
52100
  };
51197
- let execChain = Promise.resolve();
51198
- const enqueueRun = (job) => {
51199
- execChain = execChain.then(job, job);
51200
- };
51201
- const driveTracked = (slug, runId) => enqueueRun(async () => {
51202
- consumerState.inFlightRunId = runId;
52101
+ const driveTracked = (slug, runId) => {
52102
+ consumerState.inFlightRunIds.add(runId);
52103
+ consumerState.lastClaimedRunId = runId;
51203
52104
  consumerState.lastClaimAt = (/* @__PURE__ */ new Date()).toISOString();
51204
- try {
51205
- await driveRun(consumerDeps, slug, runId);
51206
- } catch (error51) {
52105
+ void driveRun(consumerDeps, slug, runId).catch((error51) => {
51207
52106
  process.stderr.write(`[mrrlin-mcp director-bridge] drive failed for ${slug}/${runId}: ${error51 instanceof Error ? error51.message : String(error51)}
51208
52107
  `);
51209
- } finally {
51210
- consumerState.inFlightRunId = null;
51211
- }
51212
- });
52108
+ }).finally(() => {
52109
+ consumerState.inFlightRunIds.delete(runId);
52110
+ });
52111
+ };
51213
52112
  const driveRunNow = (slug, runId) => {
51214
- consumerState.inFlightRunId = runId;
52113
+ consumerState.inFlightRunIds.add(runId);
52114
+ consumerState.lastClaimedRunId = runId;
51215
52115
  consumerState.lastClaimAt = (/* @__PURE__ */ new Date()).toISOString();
51216
52116
  void driveRun(consumerDeps, slug, runId).catch((error51) => {
51217
52117
  process.stderr.write(`[mrrlin-mcp director-bridge] drive failed for ${slug}/${runId}: ${error51 instanceof Error ? error51.message : String(error51)}
51218
52118
  `);
51219
52119
  }).finally(() => {
51220
- consumerState.inFlightRunId = null;
52120
+ consumerState.inFlightRunIds.delete(runId);
51221
52121
  });
51222
52122
  };
51223
- const driveAsyncTracked = (slug, jobId) => enqueueRun(async () => {
51224
- try {
51225
- await driveAsyncJob(asyncConsumerDeps, slug, jobId);
51226
- } catch (error51) {
52123
+ const driveAsyncTracked = (slug, jobId) => {
52124
+ void driveAsyncJob(asyncConsumerDeps, slug, jobId).catch((error51) => {
51227
52125
  process.stderr.write(`[mrrlin-mcp director-bridge] async drive failed for ${slug}/${jobId}: ${error51 instanceof Error ? error51.message : String(error51)}
51228
52126
  `);
51229
- }
51230
- });
52127
+ });
52128
+ };
51231
52129
  const driveAsyncNow = (slug, jobId) => {
51232
52130
  void driveAsyncJob(asyncConsumerDeps, slug, jobId).catch((error51) => {
51233
52131
  process.stderr.write(`[mrrlin-mcp director-bridge] async drive failed for ${slug}/${jobId}: ${error51 instanceof Error ? error51.message : String(error51)}
@@ -51240,7 +52138,15 @@ async function startDirectorBridge() {
51240
52138
  port: config2.port,
51241
52139
  token: config2.token,
51242
52140
  allowedOrigins: config2.allowedOrigins,
51243
- getHealth: () => ({ ok: true, inFlightRunId: consumerState.inFlightRunId, lastClaimAt: consumerState.lastClaimAt, service: health.snapshot() })
52141
+ getHealth: () => ({
52142
+ ok: true,
52143
+ // Most recently claimed run, or null once nothing is in flight — kept for existing consumers.
52144
+ inFlightRunId: consumerState.inFlightRunIds.size > 0 ? consumerState.lastClaimedRunId : null,
52145
+ inFlightRunIds: Array.from(consumerState.inFlightRunIds),
52146
+ inFlightCount: consumerState.inFlightRunIds.size,
52147
+ lastClaimAt: consumerState.lastClaimAt,
52148
+ service: health.snapshot()
52149
+ })
51244
52150
  })
51245
52151
  );
51246
52152
  const threads = /* @__PURE__ */ new Map();
@@ -51303,7 +52209,7 @@ async function startDirectorBridge() {
51303
52209
  const sharedHandlerDeps = {
51304
52210
  acquireCodex: () => codexKeepalive.acquire(),
51305
52211
  releaseCodex: () => codexKeepalive.release(),
51306
- runExclusive: wrappedTurnLock.runExclusive,
52212
+ run: wrappedTurnLock.run,
51307
52213
  suspendedMs: () => suspensionDetector.suspendedMs(),
51308
52214
  buildStartParams: (msg, desiredModel) => buildThreadStartParams(msg, config2, desiredModel),
51309
52215
  turnInactivity: () => ({ idleMs: chatTurnIdleMs(), maxMs: chatTurnMaxMs() }),
@@ -51449,7 +52355,7 @@ async function startDirectorBridge() {
51449
52355
  await bridgeHealth.recordSuccess();
51450
52356
  } catch (error51) {
51451
52357
  process.stderr.write(
51452
- `[mrrlin-mcp director-bridge] ${label} failed: ${error51 instanceof Error ? error51.message : String(error51)}
52358
+ `[mrrlin-mcp director-bridge] ${label} failed: ${describeError(error51)}
51453
52359
  `
51454
52360
  );
51455
52361
  bridgeHealth.recordFailure(id, error51);
@@ -51461,34 +52367,28 @@ async function startDirectorBridge() {
51461
52367
  const scheduleClaimLoop = () => {
51462
52368
  if (claimLoopStopped) return;
51463
52369
  claimTimer = setTimeout(() => {
51464
- enqueueRun(
51465
- () => runTrackedSweep(
51466
- "claimSweep",
51467
- "claim sweep",
51468
- () => runBackstopSweep(consumerDeps, (slug, runId) => {
51469
- driveTracked(slug, runId);
51470
- return Promise.resolve();
51471
- })
51472
- )
51473
- );
51474
- enqueueRun(
51475
- () => runTrackedSweep(
51476
- "asyncSweep",
51477
- "async sweep",
51478
- () => runAsyncBackstopSweep(asyncConsumerDeps, (slug, jobId) => {
51479
- driveAsyncTracked(slug, jobId);
51480
- return Promise.resolve();
51481
- })
51482
- )
52370
+ void runTrackedSweep(
52371
+ "claimSweep",
52372
+ "claim sweep",
52373
+ () => runBackstopSweep(consumerDeps, (slug, runId) => {
52374
+ driveTracked(slug, runId);
52375
+ return Promise.resolve();
52376
+ })
51483
52377
  );
51484
- enqueueRun(
51485
- () => runOrphanWorktreeSweep(client, {
51486
- checkoutRegistry,
51487
- worktreeRoot: import_node_path18.default.join(config2.stateDir, "worktrees"),
51488
- locksDir: import_node_path18.default.join(config2.stateDir, "locks")
52378
+ void runTrackedSweep(
52379
+ "asyncSweep",
52380
+ "async sweep",
52381
+ () => runAsyncBackstopSweep(asyncConsumerDeps, (slug, jobId) => {
52382
+ driveAsyncTracked(slug, jobId);
52383
+ return Promise.resolve();
51489
52384
  })
51490
52385
  );
51491
- enqueueRun(async () => {
52386
+ void runOrphanWorktreeSweep(client, {
52387
+ checkoutRegistry,
52388
+ worktreeRoot: import_node_path18.default.join(config2.stateDir, "worktrees"),
52389
+ locksDir: import_node_path18.default.join(config2.stateDir, "locks")
52390
+ });
52391
+ {
51492
52392
  const { retainedBuffers, retainedBytes, tombstonesRemoved } = sweepReplayBuffers(threads, tombstones, {
51493
52393
  now: Date.now(),
51494
52394
  suspendedMs: () => suspensionDetector.suspendedMs(),
@@ -51526,7 +52426,7 @@ async function startDirectorBridge() {
51526
52426
  payload: { removed: tombstonesRemoved }
51527
52427
  });
51528
52428
  }
51529
- });
52429
+ }
51530
52430
  scheduleClaimLoop();
51531
52431
  }, claimIntervalMs());
51532
52432
  claimTimer.unref?.();
@@ -51564,7 +52464,7 @@ async function startDirectorBridge() {
51564
52464
  agentPoolLoopHandle = startAgentPoolLoop({
51565
52465
  push: (snapshot) => client.putOperatorAgentPool(snapshot),
51566
52466
  onError: (err) => process.stderr.write(
51567
- `[mrrlin-mcp director-bridge] agent-pool snapshot push failed: ${err instanceof Error ? err.message : String(err)}
52467
+ `[mrrlin-mcp director-bridge] agent-pool snapshot push failed: ${describeError(err)}
51568
52468
  `
51569
52469
  )
51570
52470
  });
@@ -56500,31 +57400,12 @@ var mcpToolNames = {
56500
57400
  restartExecutionRun: "restart_execution_run",
56501
57401
  resolveHandoff: "resolve_handoff",
56502
57402
  recordProducedUrl: "record_produced_url",
57403
+ updateTaskState: "update_task_state",
56503
57404
  registerLocalCheckout: "register_local_checkout",
56504
57405
  /** Compact-context retrieval: rehydrate a mrrlin:// handle into the canonical full record. */
56505
57406
  retrieveContext: "retrieve_context"
56506
57407
  };
56507
57408
 
56508
- // src/context-compact/policy.ts
56509
- var DEFAULT_THRESHOLD_CHARS = 12e3;
56510
- var DEFAULT_MAX_ITEMS = 25;
56511
- function readPositiveInt(value, fallback) {
56512
- const raw = (value ?? "").trim();
56513
- if (!raw) return fallback;
56514
- const parsed = Number.parseInt(raw, 10);
56515
- return Number.isInteger(parsed) && parsed > 0 ? parsed : fallback;
56516
- }
56517
- function readCompactPolicy(env = process.env) {
56518
- const disabled = (env.MRRLIN_MCP_COMPACT_DISABLE ?? "").trim() === "1";
56519
- return {
56520
- enabled: !disabled,
56521
- thresholdChars: readPositiveInt(env.MRRLIN_MCP_COMPACT_THRESHOLD_CHARS, DEFAULT_THRESHOLD_CHARS),
56522
- maxItems: readPositiveInt(env.MRRLIN_MCP_COMPACT_MAX_ITEMS, DEFAULT_MAX_ITEMS),
56523
- debug: (env.MRRLIN_MCP_COMPACT_DEBUG ?? "").trim() === "1",
56524
- retrievalBudgetDisable: (env.MRRLIN_RETRIEVAL_BUDGET_DISABLE ?? "").trim() === "1"
56525
- };
56526
- }
56527
-
56528
57409
  // src/context-compact/handles.ts
56529
57410
  var MRRLIN_HANDLE_SCHEME = "mrrlin://";
56530
57411
  var SLUG_RE = /^[a-z0-9][a-z0-9-]*[a-z0-9]$|^[a-z0-9]$/;
@@ -56599,7 +57480,7 @@ function parseMrrlinHandle(value) {
56599
57480
  var NOTES_EXCERPT_CHARS = 200;
56600
57481
  var TITLE_TRUNCATE_CHARS = 160;
56601
57482
  var EXCERPT_TRUNCATE_CHARS = 240;
56602
- function truncate(value, maxChars) {
57483
+ function truncate2(value, maxChars) {
56603
57484
  if (value.length <= maxChars) return value;
56604
57485
  return `${value.slice(0, Math.max(0, maxChars - 1))}\u2026`;
56605
57486
  }
@@ -56615,7 +57496,7 @@ function compactTaskList(projectSlug, tasks, maxItems) {
56615
57496
  const slice = tasks.slice(0, included);
56616
57497
  const items = slice.map((t) => ({
56617
57498
  id: t.id,
56618
- title: truncate(t.title, TITLE_TRUNCATE_CHARS),
57499
+ title: truncate2(t.title, TITLE_TRUNCATE_CHARS),
56619
57500
  status: t.status,
56620
57501
  type: t.type,
56621
57502
  category: t.category,
@@ -56626,8 +57507,8 @@ function compactTaskList(projectSlug, tasks, maxItems) {
56626
57507
  assignee: t.assignee,
56627
57508
  planId: t.planId,
56628
57509
  specWikiPageId: t.specWikiPageId,
56629
- notesExcerpt: t.notes ? truncate(t.notes, NOTES_EXCERPT_CHARS) : null,
56630
- latestJudgementExcerpt: t.latestJudgement ? truncate(t.latestJudgement, NOTES_EXCERPT_CHARS) : null,
57510
+ notesExcerpt: t.notes ? truncate2(t.notes, NOTES_EXCERPT_CHARS) : null,
57511
+ latestJudgementExcerpt: t.latestJudgement ? truncate2(t.latestJudgement, NOTES_EXCERPT_CHARS) : null,
56631
57512
  handle: formatTaskHandle(projectSlug, t.id)
56632
57513
  }));
56633
57514
  const handles = slice.map((t) => ({
@@ -56648,7 +57529,7 @@ function compactWikiPageList(projectSlug, pages, maxItems) {
56648
57529
  const slice = pages.slice(0, included);
56649
57530
  const items = slice.map((p) => ({
56650
57531
  id: p.id,
56651
- title: truncate(p.title, TITLE_TRUNCATE_CHARS),
57532
+ title: truncate2(p.title, TITLE_TRUNCATE_CHARS),
56652
57533
  folder: p.folder,
56653
57534
  path: p.path,
56654
57535
  updatedAt: p.updatedAt,
@@ -56675,7 +57556,7 @@ function compactWikiSearch(projectSlug, results, maxItems) {
56675
57556
  const slice = results.slice(0, included);
56676
57557
  const items = slice.map((r) => ({
56677
57558
  pageId: r.id,
56678
- title: truncate(r.title, TITLE_TRUNCATE_CHARS),
57559
+ title: truncate2(r.title, TITLE_TRUNCATE_CHARS),
56679
57560
  folder: r.folder,
56680
57561
  path: r.path,
56681
57562
  rank: r.rank,
@@ -56683,7 +57564,7 @@ function compactWikiSearch(projectSlug, results, maxItems) {
56683
57564
  ageDays: r.ageDays,
56684
57565
  memoryClass: r.memoryClass,
56685
57566
  precedence: r.precedence,
56686
- snippet: truncate(r.snippet, EXCERPT_TRUNCATE_CHARS),
57567
+ snippet: truncate2(r.snippet, EXCERPT_TRUNCATE_CHARS),
56687
57568
  handle: formatWikiHandle(projectSlug, r.id)
56688
57569
  }));
56689
57570
  const handles = slice.map((r) => ({
@@ -56712,7 +57593,7 @@ function compactProjectSnapshot(projectSlug, snapshot, maxItems) {
56712
57593
  }
56713
57594
  const compactPlans = snapshot.plans.map((p) => ({
56714
57595
  id: p.id,
56715
- name: truncate(p.name, TITLE_TRUNCATE_CHARS),
57596
+ name: truncate2(p.name, TITLE_TRUNCATE_CHARS),
56716
57597
  status: p.status,
56717
57598
  taskCount: p.taskCount,
56718
57599
  closedTaskCount: p.closedTaskCount,
@@ -56784,7 +57665,7 @@ function compactArtifactPayload(artifact) {
56784
57665
  cwd,
56785
57666
  exitCode,
56786
57667
  firstError,
56787
- tail: truncate(tail2, ARTIFACT_PAYLOAD_BUDGET),
57668
+ tail: truncate2(tail2, ARTIFACT_PAYLOAD_BUDGET),
56788
57669
  totalLines: lines2.length
56789
57670
  };
56790
57671
  }
@@ -56792,7 +57673,7 @@ function compactArtifactPayload(artifact) {
56792
57673
  return {
56793
57674
  status: payload.status ?? null,
56794
57675
  command: payload.command ?? null,
56795
- summary: typeof payload.summary === "string" ? truncate(payload.summary, ARTIFACT_PAYLOAD_BUDGET) : null,
57676
+ summary: typeof payload.summary === "string" ? truncate2(payload.summary, ARTIFACT_PAYLOAD_BUDGET) : null,
56796
57677
  failed: Array.isArray(payload.failedTests) ? payload.failedTests.slice(0, 25) : null
56797
57678
  };
56798
57679
  }
@@ -56810,7 +57691,7 @@ function compactArtifactPayload(artifact) {
56810
57691
  case "checkpoint":
56811
57692
  default: {
56812
57693
  const flattened = JSON.stringify(payload);
56813
- return { excerpt: truncate(flattened, ARTIFACT_PAYLOAD_BUDGET) };
57694
+ return { excerpt: truncate2(flattened, ARTIFACT_PAYLOAD_BUDGET) };
56814
57695
  }
56815
57696
  }
56816
57697
  }
@@ -56849,7 +57730,7 @@ function compactTaskEvents(projectSlug, taskId, events, maxItems) {
56849
57730
  createdAt: e.createdAt,
56850
57731
  autonomyLevel: e.autonomyLevel,
56851
57732
  autonomyReason: e.autonomyReason,
56852
- bodyExcerpt: e.body ? truncate(e.body, NOTES_EXCERPT_CHARS) : null
57733
+ bodyExcerpt: e.body ? truncate2(e.body, NOTES_EXCERPT_CHARS) : null
56853
57734
  }));
56854
57735
  const handles = [
56855
57736
  {
@@ -56871,7 +57752,7 @@ function compactGeneric(value, maxItems, projectSlug) {
56871
57752
  const included = Math.min(total, maxItems);
56872
57753
  const slice = value.slice(0, included).map((entry) => {
56873
57754
  const text2 = typeof entry === "string" ? entry : JSON.stringify(entry);
56874
- return truncate(text2, ARTIFACT_PAYLOAD_BUDGET);
57755
+ return truncate2(text2, ARTIFACT_PAYLOAD_BUDGET);
56875
57756
  });
56876
57757
  return {
56877
57758
  summary: `Compact array view: ${total} entries, ${included} included.`,
@@ -56902,7 +57783,7 @@ function compactGeneric(value, maxItems, projectSlug) {
56902
57783
  const text = typeof value === "string" ? value : JSON.stringify(value);
56903
57784
  return {
56904
57785
  summary: "Compact scalar view.",
56905
- items: [truncate(text, ARTIFACT_PAYLOAD_BUDGET)],
57786
+ items: [truncate2(text, ARTIFACT_PAYLOAD_BUDGET)],
56906
57787
  handles: [],
56907
57788
  counts: { total: 1, included: 1, omitted: 0 }
56908
57789
  };
@@ -57072,6 +57953,7 @@ var updateExecutionRunInputSchema = runScopedSchema.merge(mrrlinExecutionRunUpda
57072
57953
  var listExecutionArtifactsInputSchema = runScopedSchema;
57073
57954
  var appendExecutionArtifactInputSchema = runScopedSchema.merge(mrrlinExecutionArtifactCreateSchema);
57074
57955
  var recordProducedUrlInputSchema = runScopedSchema.merge(mrrlinProducedUrlPayloadSchema);
57956
+ var updateTaskStateInputSchema = runScopedSchema.merge(mrrlinTaskStateReportSchema);
57075
57957
  var getProjectSchedulerPolicyInputSchema = projectScopedSchema;
57076
57958
  var upsertProjectSchedulerPolicyInputSchema = projectScopedSchema.merge(mrrlinProjectSchedulerPolicyUpsertSchema);
57077
57959
  var recordDeployWorkflowInputSchema = projectScopedSchema.extend({
@@ -57184,6 +58066,7 @@ var mcpToolInputSchemas = {
57184
58066
  [mcpToolNames.restartExecutionRun]: restartExecutionRunInputSchema,
57185
58067
  [mcpToolNames.resolveHandoff]: resolveHandoffInputSchema,
57186
58068
  [mcpToolNames.recordProducedUrl]: recordProducedUrlInputSchema,
58069
+ [mcpToolNames.updateTaskState]: updateTaskStateInputSchema,
57187
58070
  [mcpToolNames.registerLocalCheckout]: registerLocalCheckoutInputSchema,
57188
58071
  [mcpToolNames.retrieveContext]: retrieveContextInputSchema
57189
58072
  };
@@ -57235,6 +58118,7 @@ var mcpToolOperationIds = {
57235
58118
  [mcpToolNames.recordDeployWorkflow]: openApiOperationIds.upsertProjectDeployWorkflow,
57236
58119
  [mcpToolNames.updatePlan]: openApiOperationIds.updatePlan,
57237
58120
  [mcpToolNames.updateTask]: openApiOperationIds.updateTask,
58121
+ [mcpToolNames.updateTaskState]: openApiOperationIds.reportTaskState,
57238
58122
  [mcpToolNames.resetDirectorThread]: openApiOperationIds.resetDirectorThread,
57239
58123
  [mcpToolNames.archiveTask]: openApiOperationIds.archiveTask,
57240
58124
  [mcpToolNames.unarchiveTask]: openApiOperationIds.unarchiveTask,
@@ -57295,6 +58179,7 @@ var mcpToolDescriptions = {
57295
58179
  [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).`,
57296
58180
  [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.",
57297
58181
  [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.',
58182
+ [mcpToolNames.updateTaskState]: "Record where this task actually stands, in your own words, BEFORE you end your turn. This is the handoff the next run reads: it survives even when this conversation does not, because a thread is retired once it has accumulated too many runs and the next run then starts from this record plus the repository. Report what only you know \u2014 the goal as you now understand it, decisions you made AND WHY (a decision without its reason is the thing most often lost), problems still open, and the next concrete actions. Omit any field you learned nothing new about; an omitted field keeps its previous value rather than being cleared. Use one entry per line for decisions/next actions. Do not restate the spec or the file list \u2014 the next run gets those anyway.",
57298
58183
  [mcpToolNames.createExecutionRun]: 'Create a durable Director execution run for a task. Persists status, branch/SHA, workflow ids, and checkpoint cursor outside the chat transcript. For tasks that require a browser (login flows, UI verification, scraping), set prerequisites:[{mcp:"mrrlin-browser",reason:"<why a browser is needed>"}] so any resulting handoff offers a one-click "Launch browser & resume". When actually driving the browser inside the run, invoke tools from the `mrrlin-browser` MCP server ONLY (browser_navigate/browser_click/browser_snapshot/browser_type/etc.) \u2014 never from `playwright`, `chrome-devtools`, `puppeteer`, or any other browser MCP that may also be connected on the operator\'s box. Only `mrrlin-browser` uses the dedicated Chromium + persistent operator profile.',
57299
58184
  [mcpToolNames.getExecutionRun]: "Fetch a single execution run by id.",
57300
58185
  [mcpToolNames.listExecutionArtifacts]: "List artifacts captured during an execution run, in chronological order.",
@@ -57892,6 +58777,13 @@ function createMrrlinTools(options) {
57892
58777
  "Unable to append execution artifact.",
57893
58778
  async (c, { projectSlug, runId, ...input }) => await c.appendExecutionArtifact(projectSlug, runId, input)
57894
58779
  ),
58780
+ [mcpToolNames.updateTaskState]: makeTool(
58781
+ client,
58782
+ mcpToolNames.updateTaskState,
58783
+ "TASK_STATE_REPORT_FAILED",
58784
+ "Unable to record task state.",
58785
+ async (c, { projectSlug, runId, ...report }) => await c.reportTaskState(projectSlug, runId, report)
58786
+ ),
57895
58787
  [mcpToolNames.recordProducedUrl]: makeTool(
57896
58788
  client,
57897
58789
  mcpToolNames.recordProducedUrl,
@@ -58553,11 +59445,13 @@ Usage:
58553
59445
  - MRRLIN_WORKER_PORT (dispatch listener, default 8792)
58554
59446
  - MRRLIN_DIRECTOR_BRIDGE_CLAIM_INTERVAL_SECONDS (default 30)
58555
59447
  - MRRLIN_DIRECTOR_BRIDGE_STALE_SECONDS (default 300)
58556
- - MRRLIN_DIRECTOR_BRIDGE_CHAT_TURN_IDLE_SECONDS (default 180;
59448
+ - MRRLIN_DIRECTOR_BRIDGE_CHAT_TURN_IDLE_SECONDS (default 300;
58557
59449
  a chat turn is cut only after this long with NO codex
58558
- signal at all \u2014 long-but-active turns keep running)
58559
- - MRRLIN_DIRECTOR_BRIDGE_CHAT_TURN_MAX_SECONDS (default 1800;
58560
- absolute ceiling for one chat turn regardless of activity)
59450
+ signal at all AND no tool call in flight \u2014 long-but-active
59451
+ turns, and long single tool calls, keep running)
59452
+ - MRRLIN_DIRECTOR_BRIDGE_CHAT_TURN_MAX_SECONDS (default 2700;
59453
+ absolute ceiling for one chat turn regardless of activity;
59454
+ the only guard against a genuinely hung tool call)
58561
59455
  - MRRLIN_DIRECTOR_BRIDGE_CODEX_IDLE_SECONDS (default 300;
58562
59456
  how long the warm codex app-server is kept alive
58563
59457
  between turns before it is released)