@lumi.ai/runner 0.15.19 → 0.15.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +108 -16
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -158,6 +158,9 @@ var ASSISTANT_TOOL_AUTHORITY = Object.freeze({
|
|
|
158
158
|
task_get: "member",
|
|
159
159
|
knowledge_get: "member",
|
|
160
160
|
chat_get: "member",
|
|
161
|
+
// `firestore.rules` says `allow get, list: if isShipReader` for a playbook — a member can
|
|
162
|
+
// already open every one of these in the app, so asking for them changes nothing about who may.
|
|
163
|
+
workflow_read: "member",
|
|
161
164
|
// Writes a member can already make by clicking.
|
|
162
165
|
task_create: "member",
|
|
163
166
|
task_comment: "member",
|
|
@@ -1065,7 +1068,7 @@ function mcpUrl(config2) {
|
|
|
1065
1068
|
}
|
|
1066
1069
|
|
|
1067
1070
|
// src/version.ts
|
|
1068
|
-
var RUNNER_VERSION = true ? "0.15.
|
|
1071
|
+
var RUNNER_VERSION = true ? "0.15.21" : "0.0.0-dev";
|
|
1069
1072
|
|
|
1070
1073
|
// src/auth.ts
|
|
1071
1074
|
import { signInWithCustomToken } from "firebase/auth";
|
|
@@ -2127,16 +2130,40 @@ function detectClaudeLimit(text, now, fallbackMs) {
|
|
|
2127
2130
|
detail: text.trim().slice(0, 300)
|
|
2128
2131
|
};
|
|
2129
2132
|
}
|
|
2130
|
-
function
|
|
2131
|
-
|
|
2133
|
+
function expectedMcpServers(agent, extraServers = []) {
|
|
2134
|
+
const out = [];
|
|
2135
|
+
if (effectiveAgentTools(agent).workspaceMcp) {
|
|
2136
|
+
out.push({ key: WORKSPACE_MCP_KEY, label: "the Workspace MCP" });
|
|
2137
|
+
}
|
|
2138
|
+
for (const s of extraServers) out.push({ key: s.key, label: s.name || s.key });
|
|
2139
|
+
return out;
|
|
2140
|
+
}
|
|
2141
|
+
function mcpConnectionFailures(initEvent, expected) {
|
|
2142
|
+
if (expected.length === 0) return [];
|
|
2143
|
+
const byKey = new Map(expected.map((e) => [e.key, e]));
|
|
2144
|
+
const found = /* @__PURE__ */ new Map();
|
|
2132
2145
|
const errors = initEvent.mcp_server_errors;
|
|
2133
|
-
if (
|
|
2134
|
-
|
|
2135
|
-
|
|
2136
|
-
|
|
2137
|
-
|
|
2138
|
-
|
|
2139
|
-
|
|
2146
|
+
if (Array.isArray(errors)) {
|
|
2147
|
+
for (const e of errors) {
|
|
2148
|
+
if (!e || typeof e !== "object" || typeof e.name !== "string") continue;
|
|
2149
|
+
const want = byKey.get(e.name);
|
|
2150
|
+
if (want) found.set(want.key, { ...want, detail: e.message || e.type || "no reason given" });
|
|
2151
|
+
}
|
|
2152
|
+
}
|
|
2153
|
+
const servers = initEvent.mcp_servers;
|
|
2154
|
+
if (Array.isArray(servers)) {
|
|
2155
|
+
for (const s of servers) {
|
|
2156
|
+
if (!s || typeof s !== "object" || typeof s.name !== "string") continue;
|
|
2157
|
+
const want = byKey.get(s.name);
|
|
2158
|
+
if (want && s.status !== "connected" && !found.has(want.key)) {
|
|
2159
|
+
found.set(want.key, { ...want, detail: `status "${s.status ?? "unknown"}"` });
|
|
2160
|
+
}
|
|
2161
|
+
}
|
|
2162
|
+
}
|
|
2163
|
+
return [...found.values()];
|
|
2164
|
+
}
|
|
2165
|
+
function workspaceRefusal(failure) {
|
|
2166
|
+
return `The Claude CLI did not load ${failure.label} ("${failure.key}"): ${failure.detail}. The session would have had none of this Ship's tools \u2014 no task_comment, no run_report, no chat_send \u2014 so it was stopped instead of run blind. If this keeps happening, update the daemon (\`npm i -g @lumi.ai/runner\`) and the Claude CLI on this machine.`;
|
|
2140
2167
|
}
|
|
2141
2168
|
function allowedTools(agent, extraServers = []) {
|
|
2142
2169
|
const granted = effectiveAgentTools(agent);
|
|
@@ -2244,6 +2271,21 @@ async function runSession(input, bin, dirs) {
|
|
|
2244
2271
|
const claudeToken = input.secrets?.claudeToken;
|
|
2245
2272
|
const env = {
|
|
2246
2273
|
...process.env,
|
|
2274
|
+
// THE INIT EVENT MUST BE TRUTHFUL, and one inherited variable is enough to make it lie.
|
|
2275
|
+
//
|
|
2276
|
+
// `MCP_CONNECTION_NONBLOCKING` lets the CLI start before its MCP servers have settled. The init
|
|
2277
|
+
// event then reports `mcp_servers: []` with no status and no error — indistinguishable from a
|
|
2278
|
+
// server that failed — while the connection completes moments later. `mcpConnectionFailures`
|
|
2279
|
+
// reads that event and can only be as honest as it is, so a daemon started from a shell that
|
|
2280
|
+
// happens to set this would either miss every blind session or refuse every healthy one.
|
|
2281
|
+
//
|
|
2282
|
+
// It is DELETED rather than set to a value, because "off" for these is absence: the CLI reads
|
|
2283
|
+
// the variable's presence. A daemon is a long-lived process started from somebody's terminal,
|
|
2284
|
+
// an npm script, or a launchd unit, and it must not inherit a decision about its own
|
|
2285
|
+
// observability from any of them. Found the hard way — every local session for a day reported
|
|
2286
|
+
// an empty server list because the shell that launched the daemon set this.
|
|
2287
|
+
MCP_CONNECTION_NONBLOCKING: void 0,
|
|
2288
|
+
MCP_SERVER_CONNECTION_BATCH_SIZE: void 0,
|
|
2247
2289
|
...claudeToken ? { CLAUDE_CODE_OAUTH_TOKEN: claudeToken } : {},
|
|
2248
2290
|
...input.githubToken ? {
|
|
2249
2291
|
GH_TOKEN: input.githubToken,
|
|
@@ -2264,6 +2306,8 @@ async function runSession(input, bin, dirs) {
|
|
|
2264
2306
|
let resultEvent = null;
|
|
2265
2307
|
let mcpProblem = null;
|
|
2266
2308
|
const workspaceRequired = effectiveAgentTools(input.agent).workspaceMcp;
|
|
2309
|
+
const expectedServers = expectedMcpServers(input.agent, input.extraMcpServers ?? []);
|
|
2310
|
+
const mcpFailed = [];
|
|
2267
2311
|
const mcpWatch = createWorkspaceMcpWatch();
|
|
2268
2312
|
const exitCode = await new Promise((resolve) => {
|
|
2269
2313
|
const child = spawn3(bin, args, { cwd: dirs.workdir, env, stdio: ["ignore", "pipe", "pipe"] });
|
|
@@ -2301,7 +2345,14 @@ async function runSession(input, bin, dirs) {
|
|
|
2301
2345
|
if (event.type === "result") resultEvent = event;
|
|
2302
2346
|
if (event.type === "assistant") input.log("claude: assistant turn");
|
|
2303
2347
|
if (event.type === "system" && event.subtype === "init" && !mcpProblem) {
|
|
2304
|
-
|
|
2348
|
+
for (const failure of mcpConnectionFailures(event, expectedServers)) {
|
|
2349
|
+
if (failure.key === WORKSPACE_MCP_KEY) {
|
|
2350
|
+
stopBlindSession(workspaceRefusal(failure));
|
|
2351
|
+
continue;
|
|
2352
|
+
}
|
|
2353
|
+
input.log(`MCP connection "${failure.key}" did not load: ${failure.detail}.`);
|
|
2354
|
+
if (!mcpFailed.includes(failure.key)) mcpFailed.push(failure.key);
|
|
2355
|
+
}
|
|
2305
2356
|
}
|
|
2306
2357
|
if (workspaceRequired && !mcpProblem) {
|
|
2307
2358
|
stopBlindSession(mcpWatch.observe(event));
|
|
@@ -2349,7 +2400,8 @@ ${stderrLines.slice(-20).join("\n")}`,
|
|
|
2349
2400
|
`,
|
|
2350
2401
|
usage,
|
|
2351
2402
|
resultText: resultText2,
|
|
2352
|
-
...limit3 ? { limit: limit3 } : {}
|
|
2403
|
+
...limit3 ? { limit: limit3 } : {},
|
|
2404
|
+
...mcpFailed.length ? { mcpFailed } : {}
|
|
2353
2405
|
};
|
|
2354
2406
|
}
|
|
2355
2407
|
var CLAUDE_DRIVER_ID = "claude";
|
|
@@ -3158,8 +3210,45 @@ Use Try again above to run it once more.`;
|
|
|
3158
3210
|
});
|
|
3159
3211
|
}
|
|
3160
3212
|
var REPLY_SCAN_LIMIT = 20;
|
|
3213
|
+
var NARRATION_MARKERS = [
|
|
3214
|
+
// Our own heading and the harness's own word for the block above it — see `chatStandingRules`.
|
|
3215
|
+
/\bsystem prompt\b/i,
|
|
3216
|
+
/\bstanding rules\b/i,
|
|
3217
|
+
// "Based on my available tools, …"
|
|
3218
|
+
/\bmy (?:available |current )?tools?\b/i,
|
|
3219
|
+
// "Let me check what tools I do have access to."
|
|
3220
|
+
/\btools?\b[^.?!]{0,40}\bI (?:do )?have access to\b/i,
|
|
3221
|
+
// Both orders of "<the toolbox> is <missing>", bounded to one sentence so a paragraph that
|
|
3222
|
+
// merely mentions tools and, later, something being unavailable does not match.
|
|
3223
|
+
/\b(?:mcp|tools?)\b[^.?!]{0,60}\b(?:not available|unavailable|(?:are|is)n'?t available|not connected|not loaded|no access)\b/i,
|
|
3224
|
+
/\b(?:not available|unavailable|no access|(?:do not|don'?t|does not|doesn'?t) have access)\b[^.?!]{0,60}\b(?:mcp|tools?)\b/i
|
|
3225
|
+
];
|
|
3226
|
+
function isSessionNarration(paragraph) {
|
|
3227
|
+
if (paragraph.includes("```")) return false;
|
|
3228
|
+
if (!/\b(?:i|i'm|i'll|i've|my|me)\b/i.test(paragraph)) return false;
|
|
3229
|
+
return NARRATION_MARKERS.some((re) => re.test(paragraph));
|
|
3230
|
+
}
|
|
3231
|
+
function paragraphsWithOffsets(text) {
|
|
3232
|
+
const out = [];
|
|
3233
|
+
const breaks = /\n{2,}/g;
|
|
3234
|
+
let start = 0;
|
|
3235
|
+
let m;
|
|
3236
|
+
while ((m = breaks.exec(text)) !== null) {
|
|
3237
|
+
out.push({ text: text.slice(start, m.index), start });
|
|
3238
|
+
start = m.index + m[0].length;
|
|
3239
|
+
}
|
|
3240
|
+
out.push({ text: text.slice(start), start });
|
|
3241
|
+
return out;
|
|
3242
|
+
}
|
|
3243
|
+
function stripSessionNarration(text) {
|
|
3244
|
+
const paragraphs = paragraphsWithOffsets(text);
|
|
3245
|
+
let dropped = 0;
|
|
3246
|
+
while (dropped < paragraphs.length && isSessionNarration(paragraphs[dropped].text)) dropped += 1;
|
|
3247
|
+
if (dropped === 0 || dropped === paragraphs.length) return text;
|
|
3248
|
+
return text.slice(paragraphs[dropped].start).trim();
|
|
3249
|
+
}
|
|
3161
3250
|
function backstopReplyContent(resultText2) {
|
|
3162
|
-
const text = resultText2.trim();
|
|
3251
|
+
const text = stripSessionNarration(resultText2.trim());
|
|
3163
3252
|
if (!text) {
|
|
3164
3253
|
return "I finished that run without writing a reply. Write again to start a fresh one.";
|
|
3165
3254
|
}
|
|
@@ -4444,6 +4533,7 @@ async function startDaemon() {
|
|
|
4444
4533
|
let engineId = DEFAULT_ENGINE_ID;
|
|
4445
4534
|
let transcript = "";
|
|
4446
4535
|
let resultText2 = "";
|
|
4536
|
+
let mcpConnected = [];
|
|
4447
4537
|
let usage = {
|
|
4448
4538
|
engine: DEFAULT_ENGINE_ID,
|
|
4449
4539
|
inputTokens: 0,
|
|
@@ -4616,6 +4706,8 @@ async function startDaemon() {
|
|
|
4616
4706
|
armLimitTimer();
|
|
4617
4707
|
}
|
|
4618
4708
|
resultText2 = session.resultText;
|
|
4709
|
+
const failed = new Set(session.mcpFailed ?? []);
|
|
4710
|
+
mcpConnected = extraMcpServers.map((s) => s.key).filter((k) => !failed.has(k));
|
|
4619
4711
|
if (!session.ok) failure = session.resultText || "Session failed.";
|
|
4620
4712
|
} catch (e) {
|
|
4621
4713
|
failure = e instanceof Error ? e.message : String(e);
|
|
@@ -4639,7 +4731,7 @@ async function startDaemon() {
|
|
|
4639
4731
|
// mid-thought, so it almost certainly never reached `run_report` — and whatever it had
|
|
4640
4732
|
// got to is what the next run on this task would otherwise have to rediscover.
|
|
4641
4733
|
resultText: resultText2,
|
|
4642
|
-
mcpServers:
|
|
4734
|
+
mcpServers: mcpConnected
|
|
4643
4735
|
});
|
|
4644
4736
|
const by = slot.stop.by;
|
|
4645
4737
|
if (target.kind === "chat") {
|
|
@@ -4689,7 +4781,7 @@ async function startDaemon() {
|
|
|
4689
4781
|
// §15.41. Only used when the session never called `run_report` — the ordinary path is
|
|
4690
4782
|
// that it did, and a real report always wins inside the transaction.
|
|
4691
4783
|
resultText: resultText2,
|
|
4692
|
-
mcpServers:
|
|
4784
|
+
mcpServers: mcpConnected
|
|
4693
4785
|
});
|
|
4694
4786
|
log2(`Job ${job.id} done (${usage.inputTokens}in/${usage.outputTokens}out tokens).`);
|
|
4695
4787
|
let delivery = "agent-replied";
|
|
@@ -4746,7 +4838,7 @@ async function startDaemon() {
|
|
|
4746
4838
|
// is spent — starts from the context pack alone. `error` is the tail for a human; this
|
|
4747
4839
|
// is the continuity for the next session, and they are read by different readers.
|
|
4748
4840
|
resultText: resultText2,
|
|
4749
|
-
mcpServers:
|
|
4841
|
+
mcpServers: mcpConnected
|
|
4750
4842
|
});
|
|
4751
4843
|
if (target.kind === "chat") {
|
|
4752
4844
|
await markChatFailed(sess(shipId).fb.db, shipId, { ...job, chatId: target.chatId }, failure);
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lumi.ai/runner",
|
|
3
|
-
"version": "0.15.
|
|
3
|
+
"version": "0.15.21",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Lumi Crew runner daemon — claims jobs from your Ships and executes them as headless Claude sessions on your own machine.",
|
|
6
6
|
"//name": "The ONLY package in this monorepo published to the public registry, so it is the one that does not follow the internal @lumi/crew-* convention: `@lumi` is not a scope we own, `@lumi.ai` is (the npm org). The workspace DIRECTORY stays packages/crew/runner — renaming the package is not renaming the folder.",
|