@assuredloop/orca-bot-kit 0.33.0 → 0.34.0

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/SETUP.md CHANGED
@@ -126,7 +126,9 @@ default and the kit will not guess. It is Bot Father's own harness only; each
126
126
  bot they make later chooses its own. If only one harness is installed, say so
127
127
  and confirm it rather than asking as though the choice were open. If they choose
128
128
  `pi`, they sign in to Pi themselves first: the kit never handles Pi's
129
- credentials, and `obk init` starts nothing while Pi can use no model.
129
+ credentials, and `obk init` starts nothing while Pi can use no model. When you
130
+ ask or confirm, tell them: "On Pi, Bot Father acts without asking first, because
131
+ Pi has no approval step."
130
132
 
131
133
  ## 4. Make the fleet
132
134
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assuredloop/orca-bot-kit",
3
- "version": "0.33.0",
3
+ "version": "0.34.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
package/src/cli.js CHANGED
@@ -12,7 +12,7 @@ import path from 'node:path';
12
12
  import { fileURLToPath } from 'node:url';
13
13
  import { parseArgs } from 'node:util';
14
14
 
15
- import { addSession, allowedNow, allowRules, botDir, changeBot, changeSession, createBot, disallowRules, leadsOutside, readBot, refuseCodexOutside, SESSION_FIELDS, setRoleCaps, setTempApproval } from './bot.js';
15
+ import { addSession, allowedNow, allowRules, botDir, changeBot, changeSession, createBot, disallowRules, displayName, leadsOutside, readBot, refuseCodexOutside, SESSION_FIELDS, setRoleCaps, setTempApproval } from './bot.js';
16
16
  import { COMMANDS } from './commands.js';
17
17
  import { addCommand, groomCommand, grooming, upCommand } from './groom.js';
18
18
  import { checkHealth, orcaSettingFindings } from './health.js';
@@ -1932,12 +1932,17 @@ function tabLines({ bots, created, completed, rules, skills, permissions = [], t
1932
1932
  const back = one.session === undefined ? '' : ` --session ${one.session}`;
1933
1933
  lines.push(`${'paused'.padEnd(9)} ${what} left closed. Bring it back: ${shellWord(ownCli())} unpause --bots ${shellWord(bots)} --bot ${one.bot}${back}`);
1934
1934
  }
1935
+ // A window that has not loaded a bot's project gives its tabs no notice of
1936
+ // mail (#676).
1937
+ for (const one of projects.filter((project) => project.windowTrouble !== undefined)) {
1938
+ lines.push(`${'window'.padEnd(9)} ${displayName(one.bot)} its tabs may get no fleet mail until Orca's window loads its project: ${one.windowTrouble}. Try again: ${shellWord(ownCli())} up --bots ${shellWord(bots)} --bot ${one.bot}`);
1939
+ }
1935
1940
 
1936
1941
  // Last before the summary, because what a check found is about the setup the
1937
1942
  // run has just left behind rather than about any one thing it did.
1938
1943
  lines.push(...foundLines(found));
1939
1944
  // Hedged, because nothing here can see whether the window was told (#224).
1940
- if (projects.length > 0) lines.push(RELOAD_LINE);
1945
+ if (projects.some((project) => project.change !== undefined)) lines.push(RELOAD_LINE);
1941
1946
  lines.push(summary);
1942
1947
  return lines;
1943
1948
  }
package/src/message.js CHANGED
@@ -18,11 +18,12 @@
18
18
 
19
19
  import { mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
20
20
  import path from 'node:path';
21
+ import { setTimeout as pause } from 'node:timers/promises';
21
22
 
22
23
  import { MAILBOX_WAIT_MS, readBook, takeLineTurn, takeMailboxTurn, TYPING_HELD, TYPING_WAIT_MS } from './book.js';
23
24
  import { botDir, botNames, readBot } from './bot.js';
24
25
  import { harnessOf, isAddressOf, kitFolders, ownCli, reachesMail, SHELL_ENV, shellWord } from './launch.js';
25
- import { ackMailbox, coordinatorOf, postMessage, pressEscape, readMailbox, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, useMailbox } from './orca.js';
26
+ import { ackMailbox, coordinatorOf, listedOrphaned, loadInWindow, postMessage, pressEscape, readMailbox, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, useMailbox } from './orca.js';
26
27
 
27
28
  /**
28
29
  * How much of a message travels as itself. Above this it is written to a file
@@ -161,7 +162,7 @@ export async function sendMessage(bots, { to: target, from: sender, tab, subject
161
162
 
162
163
  const written = bodyOf(bots, { from, to, subject, text, textFile });
163
164
  // Looked at before the post, which is the send's last call to Orca (PRD 6.9).
164
- const shown = loaded(to);
165
+ const shown = await loaded(to);
165
166
  // The Escape goes first, so the turn has ended when the letter is in.
166
167
  const interrupted = interrupt ? interruptBusy(to) : {};
167
168
  const message = postMessage({
@@ -189,20 +190,50 @@ export async function sendMessage(bots, { to: target, from: sender, tab, subject
189
190
  * and lists one it has not as orphaned, as after a window restart (#579; tech
190
191
  * notes, section 1). So an orphaned tab is said, with its handle, and a
191
192
  * listing that cannot be read is said too, rather than taken for a loaded tab.
192
- * The look is made once: a tab orphaned after the send is not seen here.
193
+ * An orphaned tab of a project the window has never fetched is loaded by
194
+ * `loadInWindow`, which changes nothing on screen, so the kit asks for that
195
+ * and looks again until LOOK_MS after the ask (#676). A tab it does not load,
196
+ * as after a window restart (#579), is said as before. This is all before the
197
+ * send: a tab orphaned after it is not seen here.
193
198
  */
194
- function loaded(to) {
199
+ async function loaded(to) {
195
200
  if (to.tab === undefined) return {};
196
- let listed;
201
+ let tab;
197
202
  try {
198
- listed = tabs(to.home, { timeoutMs: LOOK_MS });
203
+ tab = tabs(to.home, { timeoutMs: LOOK_MS }).find((one) => one.tabId === to.tab);
199
204
  } catch (error) {
200
205
  return { loadTrouble: error.message };
201
206
  }
202
- const tab = listed.find((one) => one.tabId === to.tab);
203
- return tab?.orphaned === true ? { orphaned: true, handle: tab.handle } : {};
207
+ if (tab?.orphaned !== true) return {};
208
+ return (await loadedNow(to.home, tab.handle)) ? {} : { orphaned: true, handle: tab.handle };
204
209
  }
205
210
 
211
+ /**
212
+ * Whether the window loads the orphaned tab `handle` at `home` once asked to.
213
+ * The ask and every look after it keep to one deadline, LOOK_MS from the ask,
214
+ * and each Orca call gets only the time that is left, so a slow Orca cannot
215
+ * hold the send up for longer (#676). A look that fails or runs out counts as
216
+ * a tab still orphaned.
217
+ */
218
+ async function loadedNow(home, handle) {
219
+ const until = Date.now() + LOOK_MS;
220
+ const left = () => until - Date.now();
221
+ loadInWindow(home, { timeoutMs: LOOK_MS });
222
+ try {
223
+ for (let ms = left(); ms > 0; ms = left()) {
224
+ if (!listedOrphaned(home, handle, { timeoutMs: ms })) return true;
225
+ const wait = Math.min(RELOOK_MS, left());
226
+ if (wait > 0) await pause(wait);
227
+ }
228
+ } catch {
229
+ // A look that fails or runs out leaves the tab said as orphaned.
230
+ }
231
+ return false;
232
+ }
233
+
234
+ /** How long the kit waits between two looks at a tab it asked the window to load: it was loaded within 0.4 s, live (#676). */
235
+ const RELOOK_MS = 200;
236
+
206
237
  /**
207
238
  * What is waiting for a session, and the reading of it.
208
239
  *
package/src/orca.js CHANGED
@@ -347,6 +347,29 @@ const CLIENT_KILL_MS = 3000;
347
347
  */
348
348
  export const tellWindow = (projectId) => askRuntime('project.update', { projectId, updates: {} }) !== undefined;
349
349
 
350
+ /**
351
+ * Have Orca's window load the tabs of the Orca project at `home`: undefined
352
+ * when Orca took the call, or what went wrong.
353
+ *
354
+ * The window loads a project's tabs only once it has fetched the project's
355
+ * worktrees, and nothing fetches them for a project the kit made, so its tabs
356
+ * stay orphaned and get no notice of mail (#676, tech notes). `worktree set`
357
+ * with no field writes an empty change and has Orca send the window the event
358
+ * that fetches them: nothing the user sees changes. What went wrong is said
359
+ * without Orca's own words, which name worktrees (PRD KIT-22: the kit says
360
+ * "Orca project"). `options` are `orca`'s.
361
+ */
362
+ export function loadInWindow(home, options) {
363
+ try {
364
+ orca(['worktree', 'set', '--worktree', `path:${home}`], options);
365
+ return undefined;
366
+ } catch (error) {
367
+ return typeof error.code === 'string' && !/worktree/i.test(error.code)
368
+ ? `Orca refused the call (${error.code})`
369
+ : 'Orca gave no answer the kit could read';
370
+ }
371
+ }
372
+
350
373
  /**
351
374
  * One call to Orca's runtime for a method its CLI does not offer, and the
352
375
  * runtime's answer, or undefined when there is none to be had.
@@ -433,6 +456,14 @@ export const tabs = (home, options) => orca(['terminal', 'list', '--worktree', `
433
456
  ? { ...tab, tabId: orca(['terminal', 'show', '--terminal', tab.handle], options).terminal.tabId }
434
457
  : tab));
435
458
 
459
+ /**
460
+ * Whether Orca lists the terminal `handle` at `home` as orphaned, from one
461
+ * listing and no `terminal show`, so a caller with a deadline can give the one
462
+ * call all the time it has left (#676).
463
+ */
464
+ export const listedOrphaned = (home, handle, options) => orca(['terminal', 'list', '--worktree', `path:${home}`], options).terminals
465
+ .some((tab) => tab.handle === handle && tab.orphaned === true);
466
+
436
467
  /**
437
468
  * Open a tab in the Orca project at `home`, titled `title`.
438
469
  *
package/src/up.js CHANGED
@@ -16,7 +16,7 @@ import { dropRemovedHooks, installHook } from './hooks.js';
16
16
  import { writePermissions } from './permissions.js';
17
17
  import { piTrouble } from './pi.js';
18
18
  import { addressOf, HARNESSES, harnessOf, isAddressOf, isShortPrompt, kitFolders, launchCommand, mailboxStep, reachesMail, sessionTrouble, startPrompt, workDirOf } from './launch.js';
19
- import { asFolderProject, coordinatorOf, findProject, harnessInTab, makeMailbox, makeProject, openTab, QUESTION_ON_SCREEN, retitleTab, runMissing, screenRows, shellInTab, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, tellWindow, typeIntoTab, useMailbox } from './orca.js';
19
+ import { asFolderProject, coordinatorOf, findProject, harnessInTab, loadInWindow, makeMailbox, makeProject, openTab, QUESTION_ON_SCREEN, retitleTab, runMissing, screenRows, shellInTab, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, tellWindow, typeIntoTab, useMailbox } from './orca.js';
20
20
  import { TAB_ENV } from './record.js';
21
21
  import { buildAgents, rulesStamp } from './rules.js';
22
22
  import { linkSkills } from './skills.js';
@@ -40,7 +40,9 @@ const SECOND_LOOK_MS = 2000;
40
40
  * Bring bots up in Orca. Returns `{ tabs, rules }`: one tab entry per tab it
41
41
  * looked at — the sessions the book knows, and, for Bot Father, whatever else
42
42
  * is open in its project — and one rules entry and one skills entry per bot.
43
- * `projects` has one entry per Orca project this run made or renamed.
43
+ * `projects` has one entry per Orca project this run made or renamed, and per
44
+ * project Orca's window may not have loaded, with what went wrong as
45
+ * `windowTrouble` (#676).
44
46
  *
45
47
  * With no name it is every bot in the folder, in name order. `bot` brings up
46
48
  * one bot and `session` one of its sessions, for a caller that wants one thing
@@ -238,6 +240,10 @@ async function bringUpBot(bots, home, bot, onlySession) {
238
240
  // Orca's window does not see a project made or renamed on the command line
239
241
  // until it is told (#224).
240
242
  if (change !== undefined) tellWindow(orca.project);
243
+ // Its tabs get no notice of mail until the window loads them, and the window
244
+ // does not load a project it has never fetched (#676). Every run asks, so a
245
+ // project made before this is loaded too; the call changes nothing on screen.
246
+ const windowTrouble = loadInWindow(home);
241
247
 
242
248
  const live = new Map(tabs(home).map((tab) => [tab.tabId, tab]));
243
249
  const report = [];
@@ -256,7 +262,13 @@ async function bringUpBot(bots, home, bot, onlySession) {
256
262
  : spare.map((tab) => entry(tab, { bot: name, name: null, created: false }))));
257
263
  }
258
264
 
259
- return { tabs: report, project: change === undefined ? undefined : { bot: name, project: orca.project, change } };
265
+ const project = {
266
+ bot: name,
267
+ project: orca.project,
268
+ ...(change === undefined ? {} : { change }),
269
+ ...(windowTrouble === undefined ? {} : { windowTrouble }),
270
+ };
271
+ return { tabs: report, project: change === undefined && windowTrouble === undefined ? undefined : project };
260
272
  }
261
273
 
262
274
  async function bringUpSession(bots, home, live, session, bot, title) {