@assuredloop/orca-bot-kit 0.27.0 → 0.28.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/README.md +48 -15
- package/SETUP.md +26 -13
- package/package.json +1 -1
- package/rules/mail.md +3 -3
- package/skills/obk-bot-building/SKILL.md +24 -15
- package/skills/obk-grooming/SKILL.md +4 -3
- package/src/book.js +91 -10
- package/src/bot.js +246 -118
- package/src/clear.js +7 -3
- package/src/cli.js +95 -34
- package/src/commands.js +20 -11
- package/src/conversations.js +73 -3
- package/src/groom.js +27 -9
- package/src/health.js +18 -7
- package/src/hooks.js +7 -1
- package/src/init.js +36 -29
- package/src/launch.js +202 -18
- package/src/message.js +42 -2
- package/src/orca-reload.applescript +1 -1
- package/src/orca-runtime.cjs +1 -1
- package/src/orca.js +16 -7
- package/src/permissions.js +30 -16
- package/src/pi.js +55 -0
- package/src/rules.js +5 -2
- package/src/skills.js +50 -34
- package/src/sources.js +73 -4
- package/src/temp.js +65 -20
- package/src/up.js +68 -13
package/src/temp.js
CHANGED
|
@@ -75,6 +75,9 @@ export async function makeTemp(bots, { tab, ...given }) {
|
|
|
75
75
|
|
|
76
76
|
const maker = bot.sessions.find((session) => session.name === caller.session);
|
|
77
77
|
const { settings, chosen } = settingsFor(bot, maker, role?.option ?? {}, given);
|
|
78
|
+
if (chosen.harness.value === 'pi') {
|
|
79
|
+
throw new Error(`${name} would run on Pi, and a temporary session on Pi comes in a later slice of #547. Make it on claude or codex. Nothing was made.`);
|
|
80
|
+
}
|
|
78
81
|
refuseWiderApproval(bots, caller, bot, maker, given.approval);
|
|
79
82
|
refuseApprovalArgs(chosen.harness.value, given.extra_args);
|
|
80
83
|
settings.name = name;
|
|
@@ -83,27 +86,42 @@ export async function makeTemp(bots, { tab, ...given }) {
|
|
|
83
86
|
else settings.prompt_file = given.prompt_file ?? role.prompt_file;
|
|
84
87
|
settings.work_dir = `work/${name}`;
|
|
85
88
|
|
|
86
|
-
const written = bot.sessions.some((session) => session.name === name) ? undefined : writePromptFirst(bots, caller, settings);
|
|
87
89
|
// bot.yaml and the book are written under the book's lock, so that a second
|
|
88
90
|
// make from the same maker waits, and then sees this one (#464). addSession
|
|
89
91
|
// refuses a name the bot already has, and a setting that will not work,
|
|
90
92
|
// before it writes anything.
|
|
91
93
|
const made = new Date().toISOString();
|
|
92
94
|
let locked = false;
|
|
95
|
+
let written;
|
|
93
96
|
let added;
|
|
94
97
|
try {
|
|
95
98
|
await updateBook(caller.home, (book) => {
|
|
96
99
|
locked = true;
|
|
97
100
|
refuseNested(bots, caller, book);
|
|
98
101
|
if (role !== undefined) refuseCap(caller, role, book);
|
|
99
|
-
|
|
102
|
+
// The prompt file is written under the lock too, and only for a name the
|
|
103
|
+
// bot does not have yet: a make of the same name that waited on this one
|
|
104
|
+
// writes nothing, so it has nothing of this one's to remove (#572).
|
|
105
|
+
written = readBot(caller.home, caller.bot).sessions.some((session) => session.name === name) ? undefined : writePromptFirst(bots, caller, settings);
|
|
106
|
+
try {
|
|
107
|
+
added = addSession(bots, caller.bot, settings);
|
|
108
|
+
} catch (error) {
|
|
109
|
+
// Taken back while the lock is held: once it is let go, another make
|
|
110
|
+
// of this name may write the same file.
|
|
111
|
+
const left = removeWritten(written);
|
|
112
|
+
if (left !== undefined) throw promptLeft(error.message.replace(/ ?Nothing was made\.$/, ''), written, left);
|
|
113
|
+
throw error;
|
|
114
|
+
}
|
|
100
115
|
const kind = role === undefined ? {} : { role: role.name, option: role.option.name };
|
|
101
116
|
book.sessions[name] = { ...book.sessions[name], temporary: { maker: caller.session, made, ...kind } };
|
|
102
117
|
});
|
|
103
118
|
} catch (error) {
|
|
104
|
-
// A refusal under the lock has written nothing
|
|
105
|
-
|
|
119
|
+
// A refusal under the lock has written nothing, or taken its prompt file back.
|
|
120
|
+
if (locked && added === undefined) throw error;
|
|
106
121
|
const failed = `${caller.bot}/${name} could not be recorded in the book as temporary (${error.message})`;
|
|
122
|
+
// The prompt file goes while the session is still in bot.yaml, where it
|
|
123
|
+
// keeps any other make of its name from writing that file.
|
|
124
|
+
const left = removeWritten(written);
|
|
107
125
|
// A session in bot.yaml that the book does not call temporary would be a
|
|
108
126
|
// long-lived one to everything that reads it, its maker's retire included,
|
|
109
127
|
// so it is taken back off rather than left that way.
|
|
@@ -114,15 +132,8 @@ export async function makeTemp(bots, { tab, ...given }) {
|
|
|
114
132
|
throw new Error(`${failed}, and taking it back off bot.yaml failed too (${undo.message}). It is in bot.yaml as a session the book does not call temporary. Take it off with ${shellWord(ownCli())} retire --bots ${shellWord(bots)} --bot ${caller.bot} --session ${name}`);
|
|
115
133
|
}
|
|
116
134
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
const why = refused ? error.message.replace(/ ?Nothing was made\.$/, '') : `${failed}${added === undefined ? '.' : ', so it was taken back off bot.yaml.'}`;
|
|
120
|
-
try {
|
|
121
|
-
if (written !== undefined) rmSync(written, { force: true });
|
|
122
|
-
} catch (left) {
|
|
123
|
-
throw new Error(`${why} Nothing was made. Its start prompt ${written} could not be removed (${left.message}), and nothing reads it. Remove it with rm ${shellWord(written)}`);
|
|
124
|
-
}
|
|
125
|
-
if (refused) throw error;
|
|
135
|
+
const why = `${failed}${added === undefined ? '.' : ', so it was taken back off bot.yaml.'}`;
|
|
136
|
+
if (left !== undefined) throw promptLeft(why, written, left);
|
|
126
137
|
throw new Error(`${why} Nothing was made. Run this again once the book can be written.`);
|
|
127
138
|
}
|
|
128
139
|
|
|
@@ -178,7 +189,7 @@ function settingsFor(bot, maker, option, given) {
|
|
|
178
189
|
/**
|
|
179
190
|
* Refuse an approval wider than the widest of the maker's own and the bot's
|
|
180
191
|
* `temp_approval`, which only the user's yes through `permission approval
|
|
181
|
-
* --temps` widens (ADR
|
|
192
|
+
* --temps` widens (ADR 0051).
|
|
182
193
|
*/
|
|
183
194
|
function refuseWiderApproval(bots, caller, bot, maker, asked) {
|
|
184
195
|
if (asked === undefined) return;
|
|
@@ -316,6 +327,24 @@ function writePromptFirst(bots, caller, settings) {
|
|
|
316
327
|
return file;
|
|
317
328
|
}
|
|
318
329
|
|
|
330
|
+
/** Remove the start prompt's file `written`, when there is one, and answer why it could not be. */
|
|
331
|
+
function removeWritten(written) {
|
|
332
|
+
try {
|
|
333
|
+
if (written !== undefined) rmSync(written, { force: true });
|
|
334
|
+
} catch (error) {
|
|
335
|
+
return error;
|
|
336
|
+
}
|
|
337
|
+
return undefined;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
/**
|
|
341
|
+
* The refusal `why` for a make whose start prompt's file `written` could not
|
|
342
|
+
* be removed (`left`). Off bot.yaml, nothing is made; a prompt file that stays
|
|
343
|
+
* is only a file, and nothing reads it (#393).
|
|
344
|
+
*/
|
|
345
|
+
const promptLeft = (why, written, left) =>
|
|
346
|
+
new Error(`${why} Nothing was made. Its start prompt ${written} could not be removed (${left.message}), and nothing reads it. Remove it with rm ${shellWord(written)}`);
|
|
347
|
+
|
|
319
348
|
/**
|
|
320
349
|
* Retire a temporary session the caller made. Returns what `retireSession`
|
|
321
350
|
* does, with the maker.
|
|
@@ -506,10 +535,11 @@ const TEACH_FOOT = /Enter to .*· Esc to cancel/;
|
|
|
506
535
|
* captured live, with what answers it. Rows are compared trimmed, the pointer
|
|
507
536
|
* `❯` taken out, so the pointer may sit on any one of them.
|
|
508
537
|
*
|
|
509
|
-
* Claude Code 2.1.283's form (#261's live test, #416)
|
|
510
|
-
* which cancels
|
|
511
|
-
* 2.1.289's numbered list (#489's live run 1) is
|
|
512
|
-
* never "1. Yes" or "3. Don't show again" (the
|
|
538
|
+
* Claude Code 2.1.283's form (#261's live test, #416) and 2.1.302's (#604) are
|
|
539
|
+
* answered with Esc, which cancels them (Not now), never a return, which is
|
|
540
|
+
* Continue. Claude Code 2.1.289's numbered list (#489's live run 1) is
|
|
541
|
+
* answered with "2. Not now", never "1. Yes" or "3. Don't show again" (the
|
|
542
|
+
* architect's ruling on #489).
|
|
513
543
|
*/
|
|
514
544
|
const TEACH_SHAPES = [
|
|
515
545
|
{
|
|
@@ -524,6 +554,21 @@ const TEACH_SHAPES = [
|
|
|
524
554
|
'Continue',
|
|
525
555
|
'←/→ to change usage · Enter to continue · Esc to cancel',
|
|
526
556
|
],
|
|
557
|
+
escape: true,
|
|
558
|
+
},
|
|
559
|
+
{
|
|
560
|
+
name: 'the 2.1.302 form',
|
|
561
|
+
rows: [
|
|
562
|
+
TEACH_TITLE,
|
|
563
|
+
'Claude Code reads this project, your recent Claude sessions, and optionally your shell history and other',
|
|
564
|
+
'repositories. Claude analyzes this data and customizes auto mode to make better decisions.',
|
|
565
|
+
'How you use Claude here Mixed',
|
|
566
|
+
'Also scan shell history false',
|
|
567
|
+
'Also scan your other repos false',
|
|
568
|
+
'Continue',
|
|
569
|
+
'←/→ to change · Enter to continue · Esc to cancel',
|
|
570
|
+
],
|
|
571
|
+
escape: true,
|
|
527
572
|
},
|
|
528
573
|
{
|
|
529
574
|
name: 'the 2.1.289 list',
|
|
@@ -597,7 +642,7 @@ const wholeScreen = (rows) => rows.filter((row) => row.trim() !== '').map((row)
|
|
|
597
642
|
/**
|
|
598
643
|
* Answer the first-run screen of a temporary session the caller made (#489):
|
|
599
644
|
* Claude Code's Teach auto mode screen, in one of the shapes the kit knows,
|
|
600
|
-
* matched exactly. The 2.1.283
|
|
645
|
+
* matched exactly. The 2.1.283 and 2.1.302 forms get Esc. The 2.1.289 list gets arrows to
|
|
601
646
|
* "2. Not now" where needed, a second look that the pointer is there and the
|
|
602
647
|
* list is otherwise as it was, and only then a return. Anything else is refused with
|
|
603
648
|
* nothing typed. Like `trustHooks`, it is one command a permission rule of its
|
|
@@ -628,7 +673,7 @@ function answerTeach(target, kind) {
|
|
|
628
673
|
const hooks = rows.some((row) => row.includes(HOOKS_REVIEW)) ? ` A Codex session's "${HOOKS_REVIEW}" is answered with ${kind} trust-hooks.` : '';
|
|
629
674
|
throw new Error(`${run}'s screen is not one ${kind} answer answers: ${seen.misfit}.${hooks} ${nothing} It shows:\n${wholeScreen(rows)}`);
|
|
630
675
|
}
|
|
631
|
-
if (seen.shape
|
|
676
|
+
if (seen.shape.escape) {
|
|
632
677
|
send('\x1b');
|
|
633
678
|
return 'Esc (Not now)';
|
|
634
679
|
}
|
package/src/up.js
CHANGED
|
@@ -4,16 +4,18 @@
|
|
|
4
4
|
// conversation and Orca's resume record with it. Run it twice and the second
|
|
5
5
|
// run does nothing.
|
|
6
6
|
|
|
7
|
+
import { randomUUID } from 'node:crypto';
|
|
7
8
|
import { existsSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
8
9
|
import path from 'node:path';
|
|
9
10
|
import { setTimeout as pause } from 'node:timers/promises';
|
|
10
11
|
|
|
11
|
-
import { forgetClaimed, forgetSession, MAILBOX_WAIT_MS, readBook, sessionIdsIn, tabIdsIn, takeLineTurn, takeMailboxTurn, TYPING_HELD, TYPING_WAIT_MS, updateBook, withUnclaimed } from './book.js';
|
|
12
|
+
import { forgetClaimed, forgetSession, MAILBOX_WAIT_MS, readBook, rememberSession, sessionIdsIn, tabIdsIn, takeLineTurn, takeMailboxTurn, TYPING_HELD, TYPING_WAIT_MS, updateBook, withUnclaimed } from './book.js';
|
|
12
13
|
import { botDir, botNames, displayName, projectName, readBot } from './bot.js';
|
|
13
14
|
import { conversationsIn, hasConversation, heldAsUserTurn, transcriptsIn } from './conversations.js';
|
|
14
15
|
import { installHook } from './hooks.js';
|
|
15
16
|
import { writePermissions } from './permissions.js';
|
|
16
|
-
import {
|
|
17
|
+
import { piTrouble } from './pi.js';
|
|
18
|
+
import { addressOf, HARNESSES, harnessOf, isAddressOf, isShortPrompt, kitFolders, launchCommand, mailboxStep, reachesMail, sessionTrouble, startPrompt, workDirOf } from './launch.js';
|
|
17
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';
|
|
18
20
|
import { TAB_ENV } from './record.js';
|
|
19
21
|
import { buildAgents, rulesStamp } from './rules.js';
|
|
@@ -110,6 +112,7 @@ export function prepareBots(bots, names, onlySession) {
|
|
|
110
112
|
return { home, bot: readBot(home, name) };
|
|
111
113
|
});
|
|
112
114
|
for (const { bot, home } of chosen) refuseWhatCannotStart(bot, home, onlySession);
|
|
115
|
+
refuseWhenPiIsNotReady(chosen, onlySession);
|
|
113
116
|
|
|
114
117
|
// Every bot's AGENTS.md is built before Orca is asked for anything, because a
|
|
115
118
|
// session reads that file as it starts: a build landing after the tab was
|
|
@@ -170,7 +173,7 @@ export function prepareBots(bots, names, onlySession) {
|
|
|
170
173
|
export function botsNamed(bots, onlyBot) {
|
|
171
174
|
const names = botNames(bots);
|
|
172
175
|
if (names.length === 0) {
|
|
173
|
-
throw new Error(`there are no bots in ${bots} yet: make one with obk bot create --bots <path> --name <name> --harness claude|codex.`);
|
|
176
|
+
throw new Error(`there are no bots in ${bots} yet: make one with obk bot create --bots <path> --name <name> --harness claude|codex|pi.`);
|
|
174
177
|
}
|
|
175
178
|
if (onlyBot !== undefined && !names.includes(onlyBot)) {
|
|
176
179
|
throw new Error(`there is no bot called ${onlyBot} in ${bots}. The bots there are: ${names.join(', ')}.`);
|
|
@@ -185,6 +188,23 @@ function refuseWhatCannotStart(bot, home, onlySession) {
|
|
|
185
188
|
}
|
|
186
189
|
}
|
|
187
190
|
|
|
191
|
+
/**
|
|
192
|
+
* Refuse the run before Orca is asked for anything when it would start a Pi
|
|
193
|
+
* session and Pi cannot start one: Pi does not run, can use no model, or
|
|
194
|
+
* cannot use the one a session names (#578). The user signs in to Pi on their
|
|
195
|
+
* own. A run with no Pi session does not ask Pi anything.
|
|
196
|
+
*/
|
|
197
|
+
function refuseWhenPiIsNotReady(chosen, onlySession) {
|
|
198
|
+
const onPi = chosen.flatMap(({ bot }) => awake(sessionsOf(bot, onlySession))
|
|
199
|
+
.filter((session) => harnessOf(session, bot.harness) === 'pi')
|
|
200
|
+
.map((session) => ({ bot, session })));
|
|
201
|
+
if (onPi.length === 0) return;
|
|
202
|
+
const trouble = piTrouble(onPi.map(({ session }) => session.model).filter((model) => typeof model === 'string' && model.trim() !== ''));
|
|
203
|
+
if (trouble !== undefined) {
|
|
204
|
+
throw new Error(`${trouble} It would start ${onPi.map(({ bot, session }) => `${bot.name} ${session.name}`).join(', ')}.`);
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
188
208
|
/**
|
|
189
209
|
* The sessions that are not paused. A paused session is left out of a run
|
|
190
210
|
* altogether, so what it is set to asks nothing of the ones that will start;
|
|
@@ -273,7 +293,10 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
273
293
|
// Asked before the book is held, because it reads a folder of the harness's
|
|
274
294
|
// own files: nothing slow happens under the lock.
|
|
275
295
|
let which = whichConversation(book, home, bot, session, was, harness);
|
|
276
|
-
|
|
296
|
+
// A Pi session is told which conversation to be, and a new one is named here:
|
|
297
|
+
// the book is where that name is kept (ADR 0012, tech notes, section 6).
|
|
298
|
+
const fresh = harness === 'pi' ? randomUUID() : undefined;
|
|
299
|
+
const launchOf = (chosen) => launchFor(bots, bot, session, harness, home, workDir, chosen.resume, was?.address, fresh);
|
|
277
300
|
let launch = launchOf(which);
|
|
278
301
|
|
|
279
302
|
// A work dir is a plain folder, made for the session before it is told about
|
|
@@ -317,10 +340,16 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
317
340
|
// gets, and trust-hooks asks what this one got (#506).
|
|
318
341
|
let entry = { ...current.sessions[session.name], tab: made.tabId, launched, launched_with: session.extra_args ?? [], rules };
|
|
319
342
|
if (rules === undefined) delete entry.rules;
|
|
343
|
+
// Only Claude Code answers to an address. One left from a Claude run
|
|
344
|
+
// before a move to another harness would draw mail to nobody (#565).
|
|
345
|
+
if (harness !== 'claude') delete entry.address;
|
|
320
346
|
held = entry.session;
|
|
321
347
|
// Before the line is typed, so the hook finds no id here and takes the one
|
|
322
348
|
// it reports for a start rather than a clear, which would tell the duty twice.
|
|
323
|
-
if (which.noConversation !== undefined && held === which.noConversation) entry =
|
|
349
|
+
if (which.noConversation !== undefined && held === which.noConversation) entry = setAside(entry, which);
|
|
350
|
+
// The id a new Pi conversation is started under, written before the line
|
|
351
|
+
// that starts it, as a hook writes Claude Code's and Codex's.
|
|
352
|
+
if (fresh !== undefined && launch.resume === undefined) entry = rememberSession(entry, fresh);
|
|
324
353
|
current.sessions[session.name] = withUnclaimed(entry, which.unclaimed ?? []);
|
|
325
354
|
forgetClaimed(current);
|
|
326
355
|
});
|
|
@@ -331,13 +360,15 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
331
360
|
// The old tab's hook can name another conversation while the tab is being
|
|
332
361
|
// opened. A fresh start chosen before that is chosen again from the id the
|
|
333
362
|
// book holds now: resumed if the harness has it, and otherwise set aside the
|
|
334
|
-
// same way (review of PR #310).
|
|
335
|
-
|
|
336
|
-
|
|
363
|
+
// same way (review of PR #310). A Pi session has no hook, and the id this run
|
|
364
|
+
// named for it is the one the book now holds.
|
|
365
|
+
if (fresh === undefined && which.noConversation !== undefined && typeof held === 'string' && held !== which.noConversation) {
|
|
366
|
+
which = hasConversation(harness, home, held) ? { resume: held } : notHere(harness, home, held);
|
|
337
367
|
if (which.noConversation !== undefined) {
|
|
368
|
+
const chosen = which;
|
|
338
369
|
await updateBook(home, (current) => {
|
|
339
370
|
const entry = current.sessions[session.name];
|
|
340
|
-
if (entry?.session === held) current.sessions[session.name] =
|
|
371
|
+
if (entry?.session === held) current.sessions[session.name] = setAside(entry, chosen);
|
|
341
372
|
});
|
|
342
373
|
}
|
|
343
374
|
launch = launchOf(which);
|
|
@@ -402,7 +433,9 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
402
433
|
promptReceived,
|
|
403
434
|
promptFile: launch.promptFile,
|
|
404
435
|
resumed: launch.resume !== undefined,
|
|
405
|
-
noConversation: which.noConversation,
|
|
436
|
+
noConversation: which.ranOn === undefined ? which.noConversation : undefined,
|
|
437
|
+
harnessChanged: which.ranOn === undefined ? undefined : which.noConversation,
|
|
438
|
+
ranOn: which.ranOn,
|
|
406
439
|
unclaimed: which.unclaimed,
|
|
407
440
|
listLine: list?.typed,
|
|
408
441
|
listLineTrouble: list?.why,
|
|
@@ -532,7 +565,7 @@ function lookAndType(home, tabId, line, until) {
|
|
|
532
565
|
* The line that starts this session: resuming `resume`, or, with none, a fresh
|
|
533
566
|
* start carrying its duty. Returns `{ resume, prompt, promptFile, command, address }`.
|
|
534
567
|
*/
|
|
535
|
-
function launchFor(bots, bot, session, harness, home, workDir, resume, held) {
|
|
568
|
+
function launchFor(bots, bot, session, harness, home, workDir, resume, held, fresh) {
|
|
536
569
|
const prompt = resume === undefined ? startPrompt(session, { home, workDir }) : undefined;
|
|
537
570
|
// Anything longer than a line goes to the harness out of a file, rather than
|
|
538
571
|
// through the tab's shell a character at a time.
|
|
@@ -554,6 +587,9 @@ function launchFor(bots, bot, session, harness, home, workDir, resume, held) {
|
|
|
554
587
|
resume,
|
|
555
588
|
address,
|
|
556
589
|
kit: kitFolders(bots, bot.name),
|
|
590
|
+
// A new Pi conversation's id, and the name its tab and session list show,
|
|
591
|
+
// `<bot>.<session>` as a Codex thread is named (#480).
|
|
592
|
+
...(harness === 'pi' ? { sessionId: resume === undefined ? fresh : undefined, name: `${bot.name}.${session.name}` } : {}),
|
|
557
593
|
});
|
|
558
594
|
// The session's mailbox first, from inside its own tab (#317), and the
|
|
559
595
|
// harness whatever became of it.
|
|
@@ -831,7 +867,7 @@ function whichConversation(book, home, bot, session, was, harness) {
|
|
|
831
867
|
// it: a session paused before its first turn is one. There is nothing to
|
|
832
868
|
// resume, so it starts again with its duty, and the book keeps the id in the
|
|
833
869
|
// history with the reason (#295).
|
|
834
|
-
return
|
|
870
|
+
return notHere(harness, home, was.session);
|
|
835
871
|
}
|
|
836
872
|
// No tab: nothing has ever run for this session, so there is nothing to find.
|
|
837
873
|
if (typeof was?.tab !== 'string') return {};
|
|
@@ -851,6 +887,22 @@ function whichConversation(book, home, bot, session, was, harness) {
|
|
|
851
887
|
return { unclaimed: known.map((one) => one.id) };
|
|
852
888
|
}
|
|
853
889
|
|
|
890
|
+
/**
|
|
891
|
+
* An id the book holds that this harness has no conversation behind, so the
|
|
892
|
+
* session starts fresh: `{ noConversation: id }`, and `ranOn`, the harness that
|
|
893
|
+
* does have it, when the session has moved to another harness since it ran
|
|
894
|
+
* (#565). That conversation is not lost, only not this harness's to resume.
|
|
895
|
+
*/
|
|
896
|
+
function notHere(harness, home, id) {
|
|
897
|
+
const ranOn = HARNESSES.find((other) => other !== harness && hasConversation(other, home, id));
|
|
898
|
+
return ranOn === undefined ? { noConversation: id } : { noConversation: id, ranOn };
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
/** The entry with the id `which` set aside moved into its history, with why. */
|
|
902
|
+
const setAside = (entry, which) => (which.ranOn === undefined
|
|
903
|
+
? forgetSession(entry, 'no conversation')
|
|
904
|
+
: forgetSession(entry, 'harness changed', { harness: which.ranOn }));
|
|
905
|
+
|
|
854
906
|
/**
|
|
855
907
|
* A bot's Orca project, made if it is not there yet, with `change` saying
|
|
856
908
|
* whether this run `made` it, `renamed` it or left it as it was (undefined).
|
|
@@ -886,7 +938,7 @@ export const promptPath = (bots, bot, session) =>
|
|
|
886
938
|
export const oldPromptPath = (bots, bot, session) =>
|
|
887
939
|
path.join(`${bots}.prompts`, `${encodeURIComponent(bot)}.${encodeURIComponent(session)}.txt`);
|
|
888
940
|
|
|
889
|
-
function entry(tab, { bot, name, created, running = false, blockedReason, promptReceived, promptFile, resumed, noConversation, unclaimed, noMailbox = false, listLine, listLineTrouble, notLaunched }) {
|
|
941
|
+
function entry(tab, { bot, name, created, running = false, blockedReason, promptReceived, promptFile, resumed, noConversation, harnessChanged, ranOn, unclaimed, noMailbox = false, listLine, listLineTrouble, notLaunched }) {
|
|
890
942
|
const made = { bot, name, title: tab.title, tabId: tab.tabId, terminal: tab.handle, created, harnessStarted: running };
|
|
891
943
|
// A tab whose shell was not ready for the launch line, and why: nothing was
|
|
892
944
|
// typed into it (#498).
|
|
@@ -900,6 +952,9 @@ function entry(tab, { bot, name, created, running = false, blockedReason, prompt
|
|
|
900
952
|
if (listLineTrouble !== undefined) made.listLineTrouble = listLineTrouble;
|
|
901
953
|
// The id the book held that had no conversation behind it, now in the history.
|
|
902
954
|
if (noConversation !== undefined) made.noConversation = noConversation;
|
|
955
|
+
// The id the book held that ran on another harness, the one this session
|
|
956
|
+
// has since moved from, now in the history (#565).
|
|
957
|
+
if (harnessChanged !== undefined) Object.assign(made, { harnessChanged, ranOn });
|
|
903
958
|
// Orca's own words for what is on screen waiting to be answered, when it
|
|
904
959
|
// gave any: the caller acts on it, the kit only passes it on.
|
|
905
960
|
if (blockedReason !== undefined) made.blockedReason = blockedReason;
|