trantor 0.18.94 → 0.18.96

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.94",
3
+ "version": "0.18.96",
4
4
  "description": "Trantor — the hub-world for AI agent crews: live message bus, presence, project Kanban/flow board + crew orchestration for independent AI coding agents (Claude, Codex, Gemini, Kimi, DeepSeek)",
5
5
  "mcpServers": {
6
6
  "relay": {
@@ -29,7 +29,7 @@ import {
29
29
  } from "../lib/turn-policy.mjs";
30
30
  import { snapshotBeforeCut } from "./cut-snapshot.mjs";
31
31
  import { ocTurnUsage, dshTurnUsage, usageTotal } from "../lib/turn-usage.mjs";
32
- import { ocCompactionContent } from "../lib/seat-compaction.mjs";
32
+ import { ocCompactionContent, ocCompactionLogLine } from "../lib/seat-compaction.mjs";
33
33
  import { listContainers, newContainers, recordContainers, statePathFor as dockerStatePathFor, sweep as dockerSweep } from "../lib/docker-janitor.mjs";
34
34
  import {
35
35
  auditDutyNudges, claimDutyNudges, claudeTranscriptDir, dutyEscalations, dutyNudgeDirective,
@@ -37,7 +37,7 @@ import {
37
37
  } from "../lib/duty-nudges.mjs";
38
38
  import { reserveNudgeIds, releaseNudgeIds } from "../lib/nudge-ledger.mjs";
39
39
  import { dutyRecipientResolver } from "../lib/duty-recipient.mjs";
40
- import { cliEffortFlag } from "../lib/model-catalog.mjs";
40
+ import { cliEffortFlag, contextWindow } from "../lib/model-catalog.mjs";
41
41
  import {
42
42
  BREAKER_WINDOW, STATE_ENV, TURN_RESULT_SCHEMA,
43
43
  breakerVerdict, describeTurn, hasJsonSchemaFlag, parseEnvelope, renderCardTail, runStep,
@@ -758,10 +758,12 @@ let cardStartedAt = releaseState?.cardStartedAt || 0;
758
758
  let budgetStop = releaseState?.budgetStop || null; // { card, tokens } once the card crossed
759
759
  let budgetRetryAt = releaseState?.budgetRetryAt || 0; // paces the paused card's OWN redeliveries
760
760
 
761
- // #11559: the compaction block injected to opencode-family seats via OPENCODE_CONFIG_CONTENT.
762
- // "" injects nothing (the operator set TRANTOR_OC_COMPACTION=off, or its JSON is invalid — the
763
- // seat then behaves exactly as before, never with a half-armed config).
764
- const OC_COMPACTION = ocCompactionContent(process.env);
761
+ // #11559: the compaction block injected to opencode-family seats via OPENCODE_CONFIG_CONTENT (""
762
+ // injects nothing: TRANTOR_OC_COMPACTION=off or invalid JSON). opencode has no absolute-threshold
763
+ // knob — auto-compact fires at window − reserved (opencode.ai/config.json; calculateCeiling in
764
+ // core/src/session/compaction.ts) — so reserved is sized from the seat model's catalog window.
765
+ const OC_COMPACTION_WINDOW = contextWindow(MODEL);
766
+ const OC_COMPACTION = ocCompactionContent(process.env, OC_COMPACTION_WINDOW);
765
767
 
766
768
  let sid = releaseState?.sid || "";
767
769
  // #6206: the watchdog is DETACHED, so a runner that dies without ending it leaves an orphan
@@ -836,6 +838,8 @@ async function runTurn(prompt, isFirst, trigger = "kickoff", opts = {}) {
836
838
  log(`turn starting (${isFirst ? "fresh session" : "resume"})${MODEL ? ` · model=${MODEL}` : ""}`);
837
839
  // #7777: the one effort line — which effort parameters were set, or that the model is uncatalogued.
838
840
  if (EFFORT && EFFORT_FLAG.text) log(EFFORT_FLAG.text);
841
+ // #11559: the one compaction line — only where the block actually rides the spawn.
842
+ if (cli.pinned && OC_COMPACTION) log(ocCompactionLogLine(OC_COMPACTION_WINDOW));
839
843
  cmuxStatus("building", "#4a90d9", "hammer", { priority: 50 }); herdrAgent("working");
840
844
  // inherit stdio so the window shows the agent working live; also capture for sid-parsing.
841
845
  // Tee stderr to ERRF (still shown live in the window) so a failed turn can be classified.
@@ -4,6 +4,8 @@ import { isMainThread, parentPort, Worker, workerData } from "node:worker_thread
4
4
  import { gitCheckoutRoot } from "./git-checkout.mjs";
5
5
 
6
6
  const SLOW = "pin check skipped (slow), run trantor memory check";
7
+ // Tests on a loaded CI runner raise this; the hook itself keeps 500 ms.
8
+ const BUDGET_MS = Number(process.env.TRANTOR_PIN_CHECK_BUDGET_MS) || 500;
7
9
 
8
10
  export function memoryStartLine(project) {
9
11
  return new Promise(resolveLine => {
@@ -17,7 +19,7 @@ export function memoryStartLine(project) {
17
19
  if (worker) void worker.terminate().catch(() => {});
18
20
  };
19
21
  // Keep synchronous graph parsing and file reads off the hook's deadline timer.
20
- const timer = setTimeout(() => finish(SLOW), 500);
22
+ const timer = setTimeout(() => finish(SLOW), BUDGET_MS);
21
23
  try {
22
24
  worker = new Worker(new URL(import.meta.url), { workerData: { project } });
23
25
  worker.once("message", finish);
@@ -37,7 +39,7 @@ async function cachedMemoryLine(directory) {
37
39
  const graph = JSON.parse(readFileSync(graphPath, "utf8"));
38
40
  const nodes = new Map(graph.nodes.map(node => [node.id, node]));
39
41
  const graphTime = statSync(graphPath).mtimeMs;
40
- let unchecked = 0;
42
+ const unchecked = new Set();
41
43
  const stale = new Set(), broken = new Set();
42
44
  const notes = readdirSync(memoryDir, { withFileTypes: true })
43
45
  .filter(entry => entry.isFile() && entry.name.endsWith(".md") && entry.name !== "MEMORY.md");
@@ -45,7 +47,7 @@ async function cachedMemoryLine(directory) {
45
47
  try {
46
48
  const pins = notePins(readFileSync(join(memoryDir, note.name), "utf8"));
47
49
  for (const pin of pins) {
48
- if (pinFileNewer(project, pin, graphTime)) { unchecked++; continue; }
50
+ if (pinFileNewer(project, pin, graphTime)) { unchecked.add(note.name); continue; }
49
51
  const { status } = checkPin(pin, project, nodes);
50
52
  if (status === "stale") stale.add(note.name);
51
53
  if (status === "broken") broken.add(note.name);
@@ -53,8 +55,11 @@ async function cachedMemoryLine(directory) {
53
55
  } catch { /* A concurrent edit or invalid span must not suppress other notes. */ }
54
56
  }
55
57
  const names = [...new Set([...stale, ...broken])].sort().map(name => name.replace(/[\s\x00-\x1f\x7f]+/g, " "));
56
- const suffix = unchecked ? ` (${unchecked} unchecked: graph older than code, run trantor memory check)` : "";
57
- return names.length || unchecked ? `${stale.size} stale, ${broken.size} broken notes: ${names.join(", ")}; run trantor memory check${suffix}` : "";
58
+ const parts = [];
59
+ if (names.length) parts.push(`${stale.size} stale, ${broken.size} broken notes: ${names.join(", ")}`);
60
+ if (unchecked.size) parts.push(`${unchecked.size} pinned notes unchecked (graph older than code)`);
61
+ if (!parts.length) return "";
62
+ return `${parts.join("; ")}${unchecked.size ? "," : ";"} run trantor memory check`;
58
63
  }
59
64
 
60
65
  if (!isMainThread) {
@@ -405,6 +405,11 @@ try {
405
405
  // A RESUMED session must never claim a handoff: it already carries its history, so the takeover banner
406
406
  // would land in the same maxed-out context and look right while the window never reset.
407
407
  const isResume = source === "resume";
408
+ // A print-mode one-shot (`claude -p`) is never the successor either (#11577): SessionStart fires
409
+ // for it with source "startup" — the hook INPUT is indistinguishable from a human start — but
410
+ // the CLI marks the hook ENV (CLAUDE_CODE_ENTRYPOINT=sdk-cli; interactive "cli", SDKs sdk-*).
411
+ // Unguarded, it claimed the waiting handoff and superseded the live orchestrator.
412
+ const isPrintRun = String(process.env.CLAUDE_CODE_ENTRYPOINT || "cli").startsWith("sdk-");
408
413
  const orchEnv = process.env.TRANTOR_ORCH || "";
409
414
  const isOrchPane = !!orchEnv && (orchEnv === "1" || orchEnv === project); // project-matched: a child claude in another dir must not inherit the badge
410
415
  const orchSid = readOrchSession(project);
@@ -413,19 +418,24 @@ try {
413
418
  const orchOrigin = !!(peek && orchSid && peek.session_id && peek.session_id === orchSid);
414
419
  const ageMs = peek ? Date.now() - (Number(peek.stamp) || 0) * 1000 : 0;
415
420
  const held = orchOrigin && !isOrchPane && !isCompact && ageMs < holdMs;
416
- const handoff = !peek ? null
421
+ const handoff = !peek || isPrintRun ? null
417
422
  : (isCompact || held || isResume) ? peek
418
423
  : loadPendingHandoff(basename(projectDir), {
419
424
  claim: true,
420
425
  freshSession: { session_id: stdinObj.session_id || "", transcript_path: stdinObj.transcript_path || "" },
421
426
  });
422
427
  const claimed = !!handoff && !isCompact && !held && !isResume;
428
+ // The handoff banner never rides a one-shot either: it is not the successor and nobody asked it
429
+ // to recap, so a print start with a pending handoff gets one quiet stderr line and nothing else.
430
+ if (isPrintRun && peek && !isCompact) {
431
+ process.stderr.write(`[trantor] pending handoff ${peek.id} NOT claimed — print-mode one-shot (claude -p) is never the successor\n`);
432
+ }
423
433
  // Follow the thread: claiming the orchestrator's baton makes THIS session the orchestrator
424
434
  // thread, so the map `trantor open` resumes and the app's chat reads moves with it. The pane
425
435
  // also records itself on every fresh start — that keeps the map honest even if a future
426
436
  // claude forks the session id on resume.
427
437
  if (claimed && orchOrigin && stdinObj.session_id) writeOrchSession(project, String(stdinObj.session_id), "sessionstart-claim");
428
- if (isOrchPane && !isCompact && stdinObj.session_id) writeOrchSession(project, String(stdinObj.session_id), "orch-pane-start");
438
+ if (isOrchPane && !isCompact && !isPrintRun && stdinObj.session_id) writeOrchSession(project, String(stdinObj.session_id), "orch-pane-start");
429
439
  if (handoff && held) {
430
440
  process.stderr.write(`[trantor] pending handoff ${handoff.id} HELD for the orch pane (${Math.round(ageMs / 60000)}m old)\n`);
431
441
  const mins = Math.max(1, Math.round((holdMs - ageMs) / 60000));
@@ -40,6 +40,15 @@ export function lookup(modelId, cat = loadCatalog()) {
40
40
  return uncatalogued(id);
41
41
  }
42
42
 
43
+ // The model's context window from its catalog entry: a positive finite number, or undefined
44
+ // when the id is uncatalogued or its context is absent/invalid — callers keep their own
45
+ // default instead of guessing a window.
46
+ export function contextWindow(modelId, cat = loadCatalog()) {
47
+ const entry = lookup(modelId, cat);
48
+ const context = entry.found ? entry.context : undefined;
49
+ return Number.isFinite(context) && context > 0 ? context : undefined;
50
+ }
51
+
43
52
  // The request parameters one difficulty level maps to for one API kind. Returns null when the
44
53
  // model is uncatalogued (or the level is missing); an EMPTY object means catalogued but the
45
54
  // model takes no effort parameters (provider default — e.g. K2.7 Code thinks always).
package/lib/project.mjs CHANGED
@@ -1,17 +1,23 @@
1
1
  // trantor — canonical project identity (client side): one repo = one lane, keyed by the id recorded
2
2
  // in the checkout (.trantor/project.json) or, unmarked, by the git repo root basename; an explicit
3
3
  // RELAY_PROJECT always wins and the hub folds aliases on top.
4
- import { execSync } from "node:child_process";
4
+ import { execSync, execFileSync } from "node:child_process";
5
5
  import { readFileSync, writeFileSync, appendFileSync, existsSync, mkdirSync, readdirSync, statSync, realpathSync } from "node:fs";
6
6
  import { basename, join, dirname, resolve, sep } from "node:path";
7
7
  import { homedir, hostname } from "node:os";
8
8
  import { asRecord, asString } from "./decode.mjs";
9
9
 
10
10
  export function gitRoot(dir) {
11
- try {
12
- return execSync(`git -C ${JSON.stringify(dir)} rev-parse --show-toplevel`,
13
- { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout: 2000 }).trim();
14
- } catch { return ""; }
11
+ // A loaded runner can miss the short deadline; retry timeouts, not non-repository errors (#11580).
12
+ for (const timeout of [2000, 10000]) {
13
+ try {
14
+ return execFileSync("git", ["-C", dir, "rev-parse", "--show-toplevel"],
15
+ { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"], timeout }).trim();
16
+ } catch (error) {
17
+ if (error.code !== "ETIMEDOUT") return "";
18
+ }
19
+ }
20
+ return "";
15
21
  }
16
22
 
17
23
  // A BUS SEAT WORKTREE is named for its project BY CONSTRUCTION — the bus creates
@@ -1,29 +1,45 @@
1
- // #11559: seats run long cards in ONE opencode session (#6134's design), and opencode only
2
- // auto-compacts when the context is FULL - far past 100K on big-limit models. The knob is the
3
- // top-level `compaction` block (schema: opencode.ai/config.json), injected per seat via
4
- // OPENCODE_CONFIG_CONTENT (override: TRANTOR_OC_COMPACTION), which MERGES with the operator's global config.
1
+ // #11559: seats run long cards in one opencode session (#6134), and opencode auto-compacts only at
2
+ // window − reserved — there is NO absolute-threshold knob (opencode.ai/config.json,
3
+ // additionalProperties:false; calculateCeiling in packages/core/src/session/compaction.ts). So the
4
+ // seat target "compact near 100K" is reached by sizing reserved from the model's window.
5
5
 
6
6
  export const OC_COMPACTION_ENV = "TRANTOR_OC_COMPACTION";
7
+ // The context level a seat card should stay near: on a 1M-window model reserved becomes 900K
8
+ // so auto-compact fires at ~100K, not the ~936K a fixed 64K buffer leaves.
9
+ export const OC_COMPACT_AT = 100_000;
10
+ // The buffer floor: kept when the model's window is unknown, or smaller than would still
11
+ // leave OC_COMPACT_AT free.
12
+ export const OC_RESERVED_FLOOR = 64_000;
7
13
 
8
14
  // The seat default: compaction on, old tool outputs pruned, a small recent window preserved.
9
- // `reserved` is the buffer subtracted from the model's window before opencode calls the context
10
- // full, so a larger value fires compaction earlier. Override the whole block with
11
- // TRANTOR_OC_COMPACTION=<json>, or set it to "off" to ship no config content at all.
15
+ // Override the whole block with TRANTOR_OC_COMPACTION=<json>, or set it to "off" to ship no
16
+ // config content at all.
12
17
  export const OC_COMPACTION_DEFAULT = {
13
18
  auto: true,
14
19
  prune: true,
15
20
  tail_turns: 4,
16
21
  preserve_recent_tokens: 24_000,
17
- reserved: 64_000,
22
+ reserved: OC_RESERVED_FLOOR,
18
23
  };
19
24
 
25
+ // reservedForContext(window) — the buffer that makes opencode compact near OC_COMPACT_AT, or
26
+ // undefined when the window is unknown/invalid (the caller keeps the floor default).
27
+ export function reservedForContext(window) {
28
+ if (!Number.isFinite(window) || window <= 0) return undefined;
29
+ return Math.max(OC_RESERVED_FLOOR, window - OC_COMPACT_AT);
30
+ }
31
+
20
32
  /// A JSON string for OPENCODE_CONFIG_CONTENT, or "" when nothing should be injected: "off" opts
21
33
  /// out, invalid JSON opts out loudly at the caller (never arms a half-parsed block), custom JSON
22
- /// is wrapped as { compaction: <the value> } so the env stays one knob.
23
- export function ocCompactionContent(env = process.env) {
34
+ /// is wrapped as { compaction: <the value> } so the env stays one knob. Given the model's catalog
35
+ /// window the DEFAULT block's reserved is sized; a custom TRANTOR_OC_COMPACTION wins verbatim.
36
+ export function ocCompactionContent(env = process.env, window = undefined) {
24
37
  const raw = String(env[OC_COMPACTION_ENV] || "").trim();
25
38
  if (raw === "off") return "";
26
- if (!raw) return JSON.stringify({ compaction: OC_COMPACTION_DEFAULT });
39
+ if (!raw) {
40
+ const reserved = reservedForContext(window) ?? OC_COMPACTION_DEFAULT.reserved;
41
+ return JSON.stringify({ compaction: { ...OC_COMPACTION_DEFAULT, reserved } });
42
+ }
27
43
  let parsed;
28
44
  try { parsed = JSON.parse(raw); } catch { return ""; }
29
45
  // SAFETY: the operator's raw JSON arrives here untyped — a bare number, string or null must
@@ -32,3 +48,10 @@ export function ocCompactionContent(env = process.env) {
32
48
  if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return "";
33
49
  return JSON.stringify({ compaction: parsed });
34
50
  }
51
+
52
+ // The one per-turn runner line: what the injected block will actually do for this seat.
53
+ export function ocCompactionLogLine(window = undefined) {
54
+ const reserved = reservedForContext(window);
55
+ if (reserved === undefined) return `compaction: reserved=${OC_RESERVED_FLOOR} (floor — model window unknown)`;
56
+ return `compaction: reserved=${reserved} — auto-compact fires near ${OC_COMPACT_AT} tokens (window ${window} − reserved; opencode has no absolute-threshold knob)`;
57
+ }
package/mcp.mjs CHANGED
@@ -4,12 +4,10 @@
4
4
  // via the agent's MCP config. Identity + hub URL come from env (RELAY_SESSION, RELAY_URL).
5
5
  // Loading this server AUTO-REGISTERS the session — so presence works on every agent.
6
6
  import { writeFileSync, readFileSync, existsSync, mkdirSync } from "node:fs";
7
- import { join, basename, dirname } from "node:path";
7
+ import { join, basename } from "node:path";
8
8
  import { homedir, hostname } from "node:os";
9
9
  import { execSync, spawnSync } from "node:child_process";
10
10
  import { randomUUID } from "node:crypto";
11
- import { createRequire } from "node:module";
12
- import { fileURLToPath, pathToFileURL } from "node:url";
13
11
  import { advise, loadWorld } from "./bin/advise.mjs";
14
12
  import { loadSeatRecord } from "./lib/seat-record.mjs";
15
13
  import { resolveProject, hostId, resolveHubInfo, nonSeatReason, handoffDir, orchWriterSid } from "./lib/project.mjs";
@@ -20,43 +18,7 @@ import { assertNoSecrets } from "./lib/scrub.mjs";
20
18
  import { isMessageCardTitle, OPEN_CARD_STATUSES } from "./lib/turn-policy.mjs";
21
19
  import { markDoing, hollowVerdict, blastRadius, blastLine } from "./hooks/lib/hollow-move.mjs";
22
20
 
23
- // Runtime dep resolution: `claude plugin install` snapshots the repo, not an npm tarball, so a
24
- // GitHub-sourced plugin ships no node_modules and a static SDK import dies with
25
- // ERR_MODULE_NOT_FOUND before any line runs — silently: every relay tool just disappears.
26
- // Resolve ourselves: plain import(spec) first; on NOT_FOUND borrow the global trantor's tree.
27
- const HERE = dirname(fileURLToPath(import.meta.url));
28
- const req = createRequire(import.meta.url);
29
-
30
- let fallbackRoots = null;
31
- function borrowRoots() {
32
- if (fallbackRoots) return fallbackRoots;
33
- const roots = [];
34
- const add = (p) => { if (p && !roots.includes(p)) roots.push(p); };
35
- // Cheap guesses first — every one of these is a string join, no process spawn.
36
- if (process.env.npm_config_prefix) add(join(process.env.npm_config_prefix, "lib", "node_modules"));
37
- add(join(dirname(process.execPath), "..", "lib", "node_modules")); // homebrew, nvm, volta, asdf
38
- // Only shell out if the guesses missed — `npm root -g` costs ~0.5s of MCP startup.
39
- if (!roots.some((r) => existsSync(join(r, "trantor")))) {
40
- try { add(execSync("npm root -g", { encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] }).trim()); } catch {}
41
- }
42
- fallbackRoots = roots.flatMap((r) => [join(r, "trantor"), r]);
43
- return fallbackRoots;
44
- }
45
-
46
- async function dep(spec) {
47
- try {
48
- return await import(spec);
49
- } catch (err) {
50
- if (err?.code !== "ERR_MODULE_NOT_FOUND" && err?.code !== "ERR_PACKAGE_PATH_NOT_EXPORTED") throw err;
51
- }
52
- for (const path of [HERE, ...borrowRoots()]) {
53
- try { return await import(pathToFileURL(req.resolve(spec, { paths: [path] })).href); } catch {}
54
- }
55
- throw new Error(
56
- `[trantor-mcp] cannot resolve '${spec}'. This plugin snapshot has no node_modules and no global ` +
57
- `trantor install was found to borrow from. Fix: npm i -g trantor (or: cd ${HERE} && npm install --omit=dev)`,
58
- );
59
- }
21
+ import { dep } from "./lib/dep.mjs";
60
22
 
61
23
  const { McpServer } = await dep("@modelcontextprotocol/sdk/server/mcp.js");
62
24
  const { StdioServerTransport } = await dep("@modelcontextprotocol/sdk/server/stdio.js");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "trantor",
3
- "version": "0.18.94",
3
+ "version": "0.18.96",
4
4
  "type": "module",
5
5
  "bin": {
6
6
  "trantor": "bin/cli.mjs"