@lumi.ai/runner 0.5.4 → 0.5.5

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/cli.js +91 -24
  2. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -10,14 +10,14 @@ import {
10
10
  deleteField,
11
11
  doc as doc7,
12
12
  getDoc as getDoc5,
13
- getDocs as getDocs3,
13
+ getDocs as getDocs4,
14
14
  onSnapshot as onSnapshot2,
15
- orderBy as orderBy3,
16
- query as query3,
15
+ orderBy as orderBy4,
16
+ query as query4,
17
17
  runTransaction as runTransaction2,
18
18
  setDoc as setDoc2,
19
19
  updateDoc as updateDoc2,
20
- where as where3
20
+ where as where4
21
21
  } from "firebase/firestore";
22
22
 
23
23
  // ../shared/dist/engines/claude.js
@@ -117,6 +117,7 @@ function effectiveAgentTools(agent) {
117
117
  }
118
118
 
119
119
  // ../shared/dist/chat.js
120
+ var MAX_CHAT_MESSAGE_CHARS = 8e3;
120
121
  var MAX_CHAT_MESSAGES_IN_PROMPT = 40;
121
122
 
122
123
  // ../shared/dist/collections.js
@@ -580,7 +581,7 @@ function mcpUrl(config2) {
580
581
  }
581
582
 
582
583
  // src/version.ts
583
- var RUNNER_VERSION = true ? "0.5.4" : "0.0.0-dev";
584
+ var RUNNER_VERSION = true ? "0.5.5" : "0.0.0-dev";
584
585
 
585
586
  // src/auth.ts
586
587
  import { signInWithCustomToken } from "firebase/auth";
@@ -1864,8 +1865,13 @@ import {
1864
1865
  addDoc,
1865
1866
  collection as collection4,
1866
1867
  doc as doc6,
1868
+ getDocs as getDocs3,
1869
+ limit as fsLimit,
1870
+ orderBy as orderBy3,
1871
+ query as query3,
1867
1872
  runTransaction,
1868
- updateDoc
1873
+ updateDoc,
1874
+ where as where3
1869
1875
  } from "firebase/firestore";
1870
1876
  import { ref as storageRef, uploadBytes } from "firebase/storage";
1871
1877
  function redactTranscript(transcript, knownSecrets) {
@@ -1991,6 +1997,41 @@ Write again to start a fresh run.`;
1991
1997
  createdAt: now
1992
1998
  });
1993
1999
  }
2000
+ var REPLY_SCAN_LIMIT = 20;
2001
+ function backstopReplyContent(resultText) {
2002
+ const text = resultText.trim();
2003
+ if (!text) {
2004
+ return "I finished that run without writing a reply. Write again to start a fresh one.";
2005
+ }
2006
+ if (text.length <= MAX_CHAT_MESSAGE_CHARS) return text;
2007
+ const marker = "\n\n\u2026(truncated)";
2008
+ return `${text.slice(0, MAX_CHAT_MESSAGE_CHARS - marker.length)}${marker}`;
2009
+ }
2010
+ async function ensureChatReply(db, shipId, job, resultText) {
2011
+ const chatRef = doc6(db, COLLECTIONS.ships, shipId, COLLECTIONS.chats, job.chatId);
2012
+ const messagesCol = collection4(chatRef, COLLECTIONS.chatMessages);
2013
+ const snap = await getDocs3(
2014
+ query3(
2015
+ messagesCol,
2016
+ where3("createdAt", ">=", job.startedAt || 0),
2017
+ orderBy3("createdAt", "desc"),
2018
+ fsLimit(REPLY_SCAN_LIMIT)
2019
+ )
2020
+ );
2021
+ const replied = snap.docs.some((d) => {
2022
+ const author = d.data().author;
2023
+ return author?.type === "agent" && author.id === job.agentId;
2024
+ });
2025
+ if (replied) return "agent-replied";
2026
+ const content = backstopReplyContent(resultText);
2027
+ await addDoc(messagesCol, {
2028
+ author: { type: "agent", id: job.agentId },
2029
+ content,
2030
+ chars: content.length,
2031
+ createdAt: Date.now()
2032
+ });
2033
+ return resultText.trim() ? "posted-final-text" : "posted-silence-note";
2034
+ }
1994
2035
 
1995
2036
  // src/daemon.ts
1996
2037
  var HEARTBEAT_MS = 3e4;
@@ -2110,11 +2151,11 @@ async function startDaemon() {
2110
2151
  needsRefill.delete(shipId);
2111
2152
  if (!serving.has(shipId) || approved.get(shipId) !== true) continue;
2112
2153
  try {
2113
- const snap = await getDocs3(
2114
- query3(
2154
+ const snap = await getDocs4(
2155
+ query4(
2115
2156
  collection5(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs),
2116
- where3("status", "==", "queued"),
2117
- orderBy3("createdAt", "asc")
2157
+ where4("status", "==", "queued"),
2158
+ orderBy4("createdAt", "asc")
2118
2159
  )
2119
2160
  );
2120
2161
  for (const d of snap.docs) {
@@ -2142,10 +2183,10 @@ async function startDaemon() {
2142
2183
  console.error(`${what} listener error (${shipId}):`, e.message);
2143
2184
  };
2144
2185
  for (const shipId of serving) {
2145
- const q = query3(
2186
+ const q = query4(
2146
2187
  collection5(sess(shipId).fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.jobs),
2147
- where3("status", "==", "queued"),
2148
- orderBy3("createdAt", "asc")
2188
+ where4("status", "==", "queued"),
2189
+ orderBy4("createdAt", "asc")
2149
2190
  );
2150
2191
  unsubsByShip.set(shipId, [
2151
2192
  onSnapshot2(
@@ -2396,6 +2437,7 @@ async function startDaemon() {
2396
2437
  let sessionLimit = null;
2397
2438
  let engineId = DEFAULT_ENGINE_ID;
2398
2439
  let transcript = "";
2440
+ let resultText = "";
2399
2441
  let usage = {
2400
2442
  engine: DEFAULT_ENGINE_ID,
2401
2443
  inputTokens: 0,
@@ -2496,6 +2538,7 @@ async function startDaemon() {
2496
2538
  });
2497
2539
  armLimitTimer();
2498
2540
  }
2541
+ resultText = session.resultText;
2499
2542
  if (!session.ok) failure = session.resultText || "Session failed.";
2500
2543
  } catch (e) {
2501
2544
  failure = e instanceof Error ? e.message : String(e);
@@ -2541,9 +2584,33 @@ async function startDaemon() {
2541
2584
  mcpServers: extraMcpServers.map((s) => s.key)
2542
2585
  });
2543
2586
  log2(`Job ${job.id} done (${usage.inputTokens}in/${usage.outputTokens}out tokens).`);
2587
+ let delivery = "agent-replied";
2588
+ if (target.kind === "chat") {
2589
+ try {
2590
+ delivery = await ensureChatReply(
2591
+ sess(shipId).fb.db,
2592
+ shipId,
2593
+ { ...job, chatId: target.chatId },
2594
+ resultText
2595
+ );
2596
+ if (delivery === "posted-final-text") {
2597
+ log2(
2598
+ `Job ${job.id}: the agent never called chat_send \u2014 posted its final text to chat ${target.chatId} instead.`
2599
+ );
2600
+ } else if (delivery === "posted-silence-note") {
2601
+ log2(
2602
+ `Job ${job.id}: the agent neither called chat_send nor produced any text \u2014 posted a note to chat ${target.chatId} so the thread is not silent.`
2603
+ );
2604
+ }
2605
+ } catch (e) {
2606
+ log2(
2607
+ `Job ${job.id}: could not check whether the agent replied in chat ${target.chatId} \u2014 ${e instanceof Error ? e.message : e}`
2608
+ );
2609
+ }
2610
+ }
2544
2611
  notify(
2545
2612
  "Crew job finished",
2546
- target.kind === "task" ? `Task ${target.taskId} is done.` : "Agent replied in a chat."
2613
+ target.kind === "task" ? `Task ${target.taskId} is done.` : delivery === "agent-replied" ? "Agent replied in a chat." : "Agent finished a chat run without replying \u2014 its answer was posted for it."
2547
2614
  );
2548
2615
  } else if (transient) {
2549
2616
  await releaseJob(
@@ -2884,7 +2951,7 @@ function setParallel(config2, value, ship2) {
2884
2951
 
2885
2952
  // src/cli/commands/doctor.ts
2886
2953
  import { spawnSync as spawnSync2 } from "node:child_process";
2887
- import { collection as collection6, doc as doc8, getDoc as getDoc6, getDocs as getDocs4 } from "firebase/firestore";
2954
+ import { collection as collection6, doc as doc8, getDoc as getDoc6, getDocs as getDocs5 } from "firebase/firestore";
2888
2955
 
2889
2956
  // src/service.ts
2890
2957
  import { spawnSync } from "node:child_process";
@@ -3099,10 +3166,10 @@ function serviceStatus() {
3099
3166
  };
3100
3167
  }
3101
3168
  if (process.platform === "win32") {
3102
- const query5 = run("schtasks", ["/Query", "/TN", WINDOWS_TASK]);
3103
- if (!query5.ok) return { state: "not-installed", detail: "No scheduled task installed." };
3169
+ const query6 = run("schtasks", ["/Query", "/TN", WINDOWS_TASK]);
3170
+ if (!query6.ok) return { state: "not-installed", detail: "No scheduled task installed." };
3104
3171
  return {
3105
- state: /\bRunning\b/i.test(query5.out) ? "running" : "installed",
3172
+ state: /\bRunning\b/i.test(query6.out) ? "running" : "installed",
3106
3173
  detail: "Scheduled task installed (runs at logon).",
3107
3174
  unitPath: WINDOWS_TASK
3108
3175
  };
@@ -3339,7 +3406,7 @@ async function checkShips(config2) {
3339
3406
  }
3340
3407
  let agents = [];
3341
3408
  try {
3342
- const snap = await getDocs4(collection6(fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.agents));
3409
+ const snap = await getDocs5(collection6(fb.db, COLLECTIONS.ships, shipId, COLLECTIONS.agents));
3343
3410
  agents = snap.docs.map((d) => ({ id: d.id, ...d.data() }));
3344
3411
  } catch {
3345
3412
  }
@@ -3864,9 +3931,9 @@ import {
3864
3931
  doc as doc10,
3865
3932
  getCountFromServer as getCountFromServer2,
3866
3933
  getDoc as getDoc8,
3867
- getDocs as getDocs5,
3868
- query as query4,
3869
- where as where4
3934
+ getDocs as getDocs6,
3935
+ query as query5,
3936
+ where as where5
3870
3937
  } from "firebase/firestore";
3871
3938
  async function runStatus() {
3872
3939
  const config2 = loadConfig();
@@ -3906,7 +3973,7 @@ async function runStatus() {
3906
3973
  let queued = 0;
3907
3974
  try {
3908
3975
  const counted = await getCountFromServer2(
3909
- query4(collection7(shipRef, COLLECTIONS.jobs), where4("status", "==", "queued"))
3976
+ query5(collection7(shipRef, COLLECTIONS.jobs), where5("status", "==", "queued"))
3910
3977
  );
3911
3978
  queued = counted.data().count;
3912
3979
  } catch {
@@ -3915,7 +3982,7 @@ async function runStatus() {
3915
3982
  const usageSnap = await getDoc8(doc10(shipRef, COLLECTIONS.usageDaily, utcDay(Date.now())));
3916
3983
  const today = { ...EMPTY_USAGE_TOTALS, ...usageSnap.data()?.totals ?? {} };
3917
3984
  const now = Date.now();
3918
- const limitsSnap = await getDocs5(collection7(shipRef, COLLECTIONS.engineLimits));
3985
+ const limitsSnap = await getDocs6(collection7(shipRef, COLLECTIONS.engineLimits));
3919
3986
  const engineLimits = limitsSnap.docs.map((d) => ({ id: d.id, ...d.data() })).filter((l) => isEngineLimited(l, now));
3920
3987
  ships.push({
3921
3988
  shipId,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lumi.ai/runner",
3
- "version": "0.5.4",
3
+ "version": "0.5.5",
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.",