@assuredloop/orca-bot-kit 0.20.0 → 0.22.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 +14 -0
- package/package.json +1 -1
- package/rules/temporary.md +7 -8
- package/src/book.js +2 -1
- package/src/bot.js +78 -1
- package/src/cli.js +134 -21
- package/src/health.js +12 -1
- package/src/hooks.js +127 -1
- package/src/message.js +81 -4
- package/src/orca-reload.applescript +1 -1
- package/src/orca-runtime.cjs +1 -1
- package/src/orca.js +12 -10
- package/src/retire.js +45 -13
- package/src/sent.js +111 -0
- package/src/temp.js +196 -48
package/src/message.js
CHANGED
|
@@ -15,7 +15,8 @@
|
|
|
15
15
|
// one line into the receiver's tab. Both harnesses queue a typed line while
|
|
16
16
|
// they are busy, which is what PRD 6.9 means by queued and not interrupting.
|
|
17
17
|
|
|
18
|
-
import { mkdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
18
|
+
import { mkdirSync, readdirSync, readFileSync, realpathSync, renameSync, rmSync, writeFileSync } from 'node:fs';
|
|
19
|
+
import os from 'node:os';
|
|
19
20
|
import path from 'node:path';
|
|
20
21
|
|
|
21
22
|
import { MAILBOX_WAIT_MS, readBook, takeMailboxTurn } from './book.js';
|
|
@@ -172,7 +173,7 @@ export function sendMessage(bots, { to: target, from: sender, tab, subject, text
|
|
|
172
173
|
id: message.id,
|
|
173
174
|
thread: message.thread_id ?? thread,
|
|
174
175
|
file: written.file,
|
|
175
|
-
...nudge(to, from, subject),
|
|
176
|
+
...nudge(to, from, subject, tab),
|
|
176
177
|
};
|
|
177
178
|
}
|
|
178
179
|
|
|
@@ -452,14 +453,14 @@ const stamp = () => new Date().toISOString().replaceAll(':', '-').replace('.', '
|
|
|
452
453
|
* it is nobody sending to a session before its first screens are answered,
|
|
453
454
|
* which is the caller's work either way.
|
|
454
455
|
*/
|
|
455
|
-
function nudge(to, from, subject) {
|
|
456
|
+
function nudge(to, from, subject, tab) {
|
|
456
457
|
if (to.tab === undefined) return { nudged: false };
|
|
457
458
|
|
|
458
459
|
try {
|
|
459
460
|
const found = lookAt(to);
|
|
460
461
|
if (found.blocked !== undefined) return { nudged: false, blocked: found.blocked };
|
|
461
462
|
// A line that lands in a shell is run there, with the sender's subject in it.
|
|
462
|
-
if (found.unsure !== undefined) return { nudged: false, nudgeTrouble: found.unsure };
|
|
463
|
+
if (found.unsure !== undefined) return { nudged: false, nudgeTrouble: found.unsure, ...(found.psUnread ? leftForHook(to, from, subject, tab) : {}) };
|
|
463
464
|
if (found.handle === undefined) return { nudged: false };
|
|
464
465
|
|
|
465
466
|
const sent = typeIntoTab(
|
|
@@ -485,6 +486,82 @@ function nudge(to, from, subject) {
|
|
|
485
486
|
}
|
|
486
487
|
}
|
|
487
488
|
|
|
489
|
+
/**
|
|
490
|
+
* The nudge a Codex sender could not decide, left for its own hook (#350, ADR
|
|
491
|
+
* 0034): `{ nudgeLeft: true }`, or nothing when it is not left.
|
|
492
|
+
*
|
|
493
|
+
* Inside Codex's sandbox `ps` does not start, and for a Claude receiver busy
|
|
494
|
+
* with a command Orca's runtime cannot see past it either. Codex runs its
|
|
495
|
+
* hooks outside the sandbox, and the kit's `PostToolUse` hook runs right after
|
|
496
|
+
* the command that made this send, in the same tab, where `ps` runs. So the
|
|
497
|
+
* send leaves the nudge where that hook looks, the system temp folder under the
|
|
498
|
+
* tab's id, which the sandbox lets it write and the hook reads. Only for a
|
|
499
|
+
* Codex session sending from its own tab, and only where `ps` could not read
|
|
500
|
+
* the receiver's tab: anywhere else no hook would see it, or would see no more
|
|
501
|
+
* than the send did.
|
|
502
|
+
*/
|
|
503
|
+
function leftForHook(to, from, subject, tab) {
|
|
504
|
+
if (tab === undefined || from.tab !== tab || from.harness !== 'codex') return {};
|
|
505
|
+
try {
|
|
506
|
+
const dir = leftDir(tab);
|
|
507
|
+
mkdirSync(dir, { recursive: true });
|
|
508
|
+
const left = { bots: to.bots, to: `${to.bot}/${to.session}`, from: `${from.bot}/${from.session}`, subject };
|
|
509
|
+
// Written under a name the hook does not take, then put in place whole: a
|
|
510
|
+
// hook running beside this send never takes a file still being written.
|
|
511
|
+
const name = path.join(dir, `${stamp()}.${process.pid}`);
|
|
512
|
+
writeFileSync(`${name}.writing`, `${JSON.stringify(left)}\n`);
|
|
513
|
+
renameSync(`${name}.writing`, `${name}.json`);
|
|
514
|
+
return { nudgeLeft: true };
|
|
515
|
+
} catch {
|
|
516
|
+
return {};
|
|
517
|
+
}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/** Where the nudges left from one tab wait for its hook. */
|
|
521
|
+
const leftDir = (tab) => path.join(os.tmpdir(), 'obk-nudges', encodeURIComponent(tab));
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* What the kit's Codex hook does after each shell command (#350): decide every
|
|
525
|
+
* nudge a send in this tab left, oldest first, with the gate the send uses, and
|
|
526
|
+
* type it or not. Each is taken out before it is looked at, so it is decided
|
|
527
|
+
* once, by one hook. Returns `{ to, subject, ...what the nudge came to }` for
|
|
528
|
+
* each, as the send's own answer says it.
|
|
529
|
+
*/
|
|
530
|
+
export function decideLeftNudges(tab) {
|
|
531
|
+
if (tab === undefined) return [];
|
|
532
|
+
const dir = leftDir(tab);
|
|
533
|
+
let names;
|
|
534
|
+
try {
|
|
535
|
+
names = readdirSync(dir).filter((name) => name.endsWith('.json')).sort();
|
|
536
|
+
} catch {
|
|
537
|
+
return [];
|
|
538
|
+
}
|
|
539
|
+
return names.flatMap((name) => {
|
|
540
|
+
const taking = path.join(dir, `${name}.taking`);
|
|
541
|
+
// Another hook took it first: it is that hook's to decide, and to remove.
|
|
542
|
+
try {
|
|
543
|
+
renameSync(path.join(dir, name), taking);
|
|
544
|
+
} catch {
|
|
545
|
+
return [];
|
|
546
|
+
}
|
|
547
|
+
let left;
|
|
548
|
+
try {
|
|
549
|
+
left = JSON.parse(readFileSync(taking, 'utf8'));
|
|
550
|
+
} catch {
|
|
551
|
+
return [];
|
|
552
|
+
} finally {
|
|
553
|
+
rmSync(taking, { force: true });
|
|
554
|
+
}
|
|
555
|
+
try {
|
|
556
|
+
const to = findSession(left.bots, left.to);
|
|
557
|
+
const from = findSession(left.bots, left.from);
|
|
558
|
+
return [{ to: left.to, subject: left.subject, ...nudge(to, from, left.subject) }];
|
|
559
|
+
} catch (error) {
|
|
560
|
+
return [{ to: left.to, subject: left.subject, nudged: false, nudgeTrouble: error.message }];
|
|
561
|
+
}
|
|
562
|
+
});
|
|
563
|
+
}
|
|
564
|
+
|
|
488
565
|
/** Orca's words about a line it did not see start a turn, or what its receipt says when it gives none. */
|
|
489
566
|
function unseen(sent) {
|
|
490
567
|
const warnings = Array.isArray(sent?.warnings) ? sent.warnings.filter((warning) => typeof warning === 'string') : [];
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- Run by the kit as `osascript orca-reload.applescript <Orca.app path>`, after
|
|
2
|
-
-- a project was removed (#343, ADR
|
|
2
|
+
-- a project was removed (#343, ADR 0034).
|
|
3
3
|
--
|
|
4
4
|
-- Orca's window keeps a removed project in its sidebar until the window is
|
|
5
5
|
-- rebuilt (stablyai/orca#23224; once a release fixes it, this can go behind a
|
package/src/orca-runtime.cjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// CLI: `ELECTRON_RUN_AS_NODE=1 <Orca> orca-runtime.cjs <client> <method> <params> <wait>`.
|
|
3
3
|
//
|
|
4
4
|
// It makes one call to Orca's runtime, through Orca's own client, for a method
|
|
5
|
-
// Orca's CLI does not offer (ADR
|
|
5
|
+
// Orca's CLI does not offer (ADR 0034): `project.update`, which makes Orca's
|
|
6
6
|
// window read its projects again (#224), and `terminal.inspectProcess`, which
|
|
7
7
|
// says who is in front of a tab where `ps` cannot (#298). `params` is JSON. It
|
|
8
8
|
// prints the runtime's answer as JSON and exits 0 only when the runtime took
|
package/src/orca.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Every call the kit makes to Orca goes through here (ADR
|
|
1
|
+
// Every call the kit makes to Orca goes through here (ADR 0034). Orca's CLI
|
|
2
2
|
// changes often, so the kit reads `--json` and never the human text, and keeps
|
|
3
3
|
// the parsing in one place.
|
|
4
4
|
//
|
|
@@ -216,7 +216,7 @@ const CLIENT_KILL_MS = 3000;
|
|
|
216
216
|
*
|
|
217
217
|
* Orca's window re-reads only when its runtime says the projects changed, and
|
|
218
218
|
* `setup-update` and `setup-delete` do not say so. `project.update` with no
|
|
219
|
-
* changes does, and Orca's CLI does not offer it (ADR
|
|
219
|
+
* changes does, and Orca's CLI does not offer it (ADR 0034). The caller prints
|
|
220
220
|
* `RELOAD_LINE` either way. It does not take a removed project out of the
|
|
221
221
|
* sidebar: `reloadWindow` does.
|
|
222
222
|
*/
|
|
@@ -227,7 +227,7 @@ export const tellWindow = (projectId) => askRuntime('project.update', { projectI
|
|
|
227
227
|
* runtime's answer, or undefined when there is none to be had.
|
|
228
228
|
*
|
|
229
229
|
* It goes through Orca's own runtime client out of the installed app, run by
|
|
230
|
-
* Orca's binary the way its `bin/orca` runs its CLI (ADR
|
|
230
|
+
* Orca's binary the way its `bin/orca` runs its CLI (ADR 0034). None of that is
|
|
231
231
|
* Orca's published interface, so anything that goes wrong is a quiet
|
|
232
232
|
* undefined: it never throws and is never tried twice.
|
|
233
233
|
*/
|
|
@@ -264,7 +264,7 @@ const RELOAD_KILL_MS = 5000;
|
|
|
264
264
|
|
|
265
265
|
/**
|
|
266
266
|
* Have Orca's window force-reload itself, after a project was removed: true
|
|
267
|
-
* only when it did (#343, ADR
|
|
267
|
+
* only when it did (#343, ADR 0034).
|
|
268
268
|
*
|
|
269
269
|
* The window keeps a removed project in its sidebar until it is rebuilt, and
|
|
270
270
|
* `tellWindow` only relabels the row "Unknown" (stablyai/orca#23224; once a
|
|
@@ -418,7 +418,7 @@ const ON_A_CHOICE = /^( *[›❯] +)\d+\. /;
|
|
|
418
418
|
* Whether the rows of a rendered screen hold a question of the harness's own.
|
|
419
419
|
*
|
|
420
420
|
* Every one seen is a numbered list of choices with the harness's pointer on
|
|
421
|
-
* one (tech notes, section 1; ADR
|
|
421
|
+
* one (tech notes, section 1; ADR 0034). The same pointer starts the harness's
|
|
422
422
|
* input line and its echo of the user's past turns, and the input line is the
|
|
423
423
|
* lowest of them whenever it is on screen, so only the lowest pointer row is
|
|
424
424
|
* asked about: a question counts while it stands in the input line's place.
|
|
@@ -449,7 +449,8 @@ export function questionIn(rows) {
|
|
|
449
449
|
* `{}` for a tab with no harness in it (none in the book, none Orca lists, or
|
|
450
450
|
* the shell in front), `{ blocked }` for one with something on screen waiting
|
|
451
451
|
* to be answered, and `{ unsure }`, a sentence, for one the kit cannot tell
|
|
452
|
-
* about
|
|
452
|
+
* about, with `psUnread` where `ps` could not read the tab (#350). Orca
|
|
453
|
+
* refusing throws, for the caller to report.
|
|
453
454
|
*
|
|
454
455
|
* The one gate for every line the kit types into a running session: the mail
|
|
455
456
|
* nudge and the skills reload. A busy harness passes it, since both harnesses
|
|
@@ -497,7 +498,8 @@ export function tabToTypeInto(home, tabId, timeoutMs) {
|
|
|
497
498
|
const why = seen.front === undefined
|
|
498
499
|
? seen.unreadable
|
|
499
500
|
: `${seen.command} holds its terminal, and Orca names ${seen.agent ?? 'no agent'} in it`;
|
|
500
|
-
|
|
501
|
+
// Where `ps` could not read the tab, a reader that can may yet tell (#350).
|
|
502
|
+
return { unsure: `the kit could not tell whether a harness is running in it (${why}), so nothing was typed`, ...(seen.psUnread ? { psUnread: true } : {}) };
|
|
501
503
|
}
|
|
502
504
|
// Orca's reason does not cover every question: it called Codex's update
|
|
503
505
|
// offer idle, and a return typed into it took "Update now" (#329). So the
|
|
@@ -554,13 +556,13 @@ const psCli = () => process.env.OBK_PS || '/bin/ps';
|
|
|
554
556
|
*
|
|
555
557
|
* `ps` is asked first. Where it cannot read the tab, as inside Codex's
|
|
556
558
|
* sandbox, where it does not start at all, Orca's runtime is asked instead
|
|
557
|
-
* (#298, ADR
|
|
559
|
+
* (#298, ADR 0034).
|
|
558
560
|
*/
|
|
559
561
|
function frontOf(handle, ptyId, readMs) {
|
|
560
562
|
const read = frontByPs(ptyId, readMs);
|
|
561
563
|
if (read.unreadable === undefined) return read;
|
|
562
564
|
const asked = frontByOrca(handle);
|
|
563
|
-
return asked.unreadable === undefined ? asked : { unreadable: `${read.unreadable}, and ${asked.unreadable}` };
|
|
565
|
+
return { ...(asked.unreadable === undefined ? asked : { unreadable: `${read.unreadable}, and ${asked.unreadable}` }), psUnread: true };
|
|
564
566
|
}
|
|
565
567
|
|
|
566
568
|
/**
|
|
@@ -590,7 +592,7 @@ function frontByOrca(handle) {
|
|
|
590
592
|
* Who holds the terminal of the pane `ptyId`, as `ps` says it.
|
|
591
593
|
*
|
|
592
594
|
* Orca gives the pane's pid in `diagnostics memory` and nowhere else, and `ps`
|
|
593
|
-
* gives that pid's terminal's foreground process group (ADR
|
|
595
|
+
* gives that pid's terminal's foreground process group (ADR 0034).
|
|
594
596
|
* On macOS the pane is `login` with the shell as its child, so the shell is in
|
|
595
597
|
* front when the group is the pane's own or that of a child of a `login` pane.
|
|
596
598
|
* `diagnostics memory` is a diagnostics command and may change, so everything
|
package/src/retire.js
CHANGED
|
@@ -25,6 +25,7 @@ import path from 'node:path';
|
|
|
25
25
|
import { readBook, tabIdsIn, updateBook } from './book.js';
|
|
26
26
|
import { botDir, dropSession, readBot } from './bot.js';
|
|
27
27
|
import { deleteProject, findProject, projects, reloadWindow, tabs } from './orca.js';
|
|
28
|
+
import { shellWord } from './launch.js';
|
|
28
29
|
import { fleetMember } from './pause.js';
|
|
29
30
|
import { closeTabs, commandLine, tabsToClose } from './restart.js';
|
|
30
31
|
import { unlinkSkills } from './skills.js';
|
|
@@ -34,26 +35,57 @@ import { promptPath, sessionsOf } from './up.js';
|
|
|
34
35
|
export const retiredDir = (bots) => path.join(bots, 'retired');
|
|
35
36
|
|
|
36
37
|
/**
|
|
37
|
-
* Retire the session `session` of the bot `bot
|
|
38
|
-
*
|
|
38
|
+
* Retire the session `session` of the bot `bot`, and first the temporary
|
|
39
|
+
* sessions it made, and theirs (#464, ADR 0033). Returns `{ bot, session,
|
|
40
|
+
* closed, retiredWith }`, and `promptsLeft` when its prompt file could not be
|
|
41
|
+
* removed. `retiredWith` holds one `{ bot, session, maker, closed }` for each
|
|
42
|
+
* session that went with it, deepest first, with its own `promptsLeft`.
|
|
39
43
|
*/
|
|
40
44
|
export async function retireSession(bots, { bot, session }) {
|
|
41
45
|
const home = fleetMember(bots, bot, 'retire', session);
|
|
42
|
-
const
|
|
46
|
+
const known = readBot(home, bot);
|
|
47
|
+
const sessions = sessionsOf(known, session);
|
|
43
48
|
|
|
44
|
-
const
|
|
45
|
-
|
|
49
|
+
const retiredWith = [];
|
|
50
|
+
const book = readBook(home);
|
|
51
|
+
const made = known.sessions.filter((one) => book.sessions[one.name]?.temporary?.maker === session);
|
|
52
|
+
let closed;
|
|
53
|
+
try {
|
|
54
|
+
for (const { name } of made) {
|
|
55
|
+
const { retiredWith: theirs, ...gone } = await retireSession(bots, { bot, session: name });
|
|
56
|
+
retiredWith.push(...theirs, { ...gone, maker: session });
|
|
57
|
+
}
|
|
46
58
|
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
59
|
+
closed = await closeTabs(home, tabsToClose(bots, bot, home, sessions, { keepless: true }), bots, bot, commandLine('retire', bots, bot, session));
|
|
60
|
+
dropSession(bots, bot, session);
|
|
61
|
+
|
|
62
|
+
const at = new Date().toISOString();
|
|
63
|
+
await updateBook(home, (book) => {
|
|
64
|
+
const entry = book.sessions[session];
|
|
65
|
+
if (entry === undefined) return;
|
|
66
|
+
delete book.sessions[session];
|
|
67
|
+
book.retired = [...(Array.isArray(book.retired) ? book.retired : []), { name: session, ...entry, retired: at }];
|
|
68
|
+
});
|
|
69
|
+
} catch (error) {
|
|
70
|
+
// What already went stays gone, and is named: a retire run again finds
|
|
71
|
+
// nothing of it left to name (#464).
|
|
72
|
+
if (retiredWith.length > 0) error.message = `${error.message} ${goneBefore(session, retiredWith)}`;
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
54
75
|
const left = removePrompts([promptPath(bots, bot, session)]);
|
|
55
76
|
|
|
56
|
-
return { bot, session, closed, ...left };
|
|
77
|
+
return { bot, session, closed, retiredWith, ...left };
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* The sessions retired along with `session` before its own retire failed, in
|
|
82
|
+
* words, with any prompt file of theirs left and how to remove it.
|
|
83
|
+
*/
|
|
84
|
+
function goneBefore(session, retiredWith) {
|
|
85
|
+
const names = retiredWith.map((gone) => `${gone.bot}/${gone.session}, a temporary session of ${gone.maker}'s`);
|
|
86
|
+
const left = retiredWith.flatMap((gone) => gone.promptsLeft ?? [])
|
|
87
|
+
.map(({ file, reason }) => ` ${file} could not be removed (${reason}), and nothing reads it. Remove it with rm ${shellWord(file)}`);
|
|
88
|
+
return `Retired along with ${session} before that, and still retired: ${names.join('; ')}.${left.join('')}`;
|
|
57
89
|
}
|
|
58
90
|
|
|
59
91
|
/**
|
package/src/sent.js
ADDED
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
// `obk session sent`: what the kit's PostToolUse hook says after a session's
|
|
2
|
+
// native message (#451, ADR 0032).
|
|
3
|
+
//
|
|
4
|
+
// Claude Code's own messaging reaches every Claude session on the machine, not
|
|
5
|
+
// only the sessions of one bots folder. After a send to another session, this
|
|
6
|
+
// looks at where it went: to a session address none of the fleet's books name,
|
|
7
|
+
// the session is told so, with the road the kit would have given it. The send
|
|
8
|
+
// has gone either way. Nothing here decides anything about it, and anything it
|
|
9
|
+
// cannot read is silence (ADR 0022: a hook never disturbs the session).
|
|
10
|
+
//
|
|
11
|
+
// It reads the send from its fields, never from its wording. Seen on Claude
|
|
12
|
+
// Code 2.1.283: a send to an in-process teammate answers with a `routing`
|
|
13
|
+
// object, and one to another session without it; the input's `to` names the
|
|
14
|
+
// target, as a session's name with an optional ` [xxxxxx]` reference, or as a
|
|
15
|
+
// `uds:` socket path when answering a message, which no book can map.
|
|
16
|
+
|
|
17
|
+
import { readBook } from './book.js';
|
|
18
|
+
import { botDir, botNames } from './bot.js';
|
|
19
|
+
import { isAddressOf, ownCli, shellWord } from './launch.js';
|
|
20
|
+
import { lookUp } from './message.js';
|
|
21
|
+
import { TAB_ENV } from './record.js';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The warning for a send the hook was told about, or undefined when there is
|
|
25
|
+
* nothing to say. `said` is what Claude Code handed the hook.
|
|
26
|
+
*/
|
|
27
|
+
export function sentWarning(bots, said) {
|
|
28
|
+
if (said?.tool_name !== 'SendMessage') return undefined;
|
|
29
|
+
const to = said.tool_input?.to;
|
|
30
|
+
if (typeof to !== 'string') return undefined;
|
|
31
|
+
const result = resultOf(said);
|
|
32
|
+
// A teammate in the same session, or a send that did not go: not this.
|
|
33
|
+
if (result === undefined || result.success !== true || 'routing' in result) return undefined;
|
|
34
|
+
if (/^(uds|bridge):/.test(to)) return undefined;
|
|
35
|
+
|
|
36
|
+
const name = to.replace(/\s*\[[^\]]*\]\s*$/, '').trim();
|
|
37
|
+
if (name === '') return undefined;
|
|
38
|
+
const fleet = addressesIn(bots);
|
|
39
|
+
if (fleet === undefined || fleet.all.has(name)) return undefined;
|
|
40
|
+
|
|
41
|
+
const meant = fleet.byName.get(name.split('.').slice(0, 2).join('.'));
|
|
42
|
+
const ask = (to) => `\`${shellWord(ownCli())} message to --bots ${shellWord(bots)} --to ${to}\``;
|
|
43
|
+
const road = meant === undefined
|
|
44
|
+
? `${ask('<bot>/<session>')} gives the road to a session of this fleet.`
|
|
45
|
+
: roadTo(bots, meant, ask(`${meant.bot}/${meant.session}`));
|
|
46
|
+
return `That message went to ${name}, which is not one of the sessions of your bots folder, ${bots}: Claude Code's messaging reaches every session on this machine. ${road} Check before you send to it again.`;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* The road to the session the sender probably meant, as `obk message to` would
|
|
51
|
+
* answer it from the sender's own tab: native to its address only between
|
|
52
|
+
* sessions of one approval class, the kit's mailbox otherwise (PRD 6.9). Where
|
|
53
|
+
* the sender cannot be told by its tab, the session's address and the question
|
|
54
|
+
* to ask, without claiming the address is the answer (#451 review).
|
|
55
|
+
*/
|
|
56
|
+
function roadTo(bots, meant, ask) {
|
|
57
|
+
let found;
|
|
58
|
+
try {
|
|
59
|
+
found = lookUp(bots, { to: `${meant.bot}/${meant.session}`, tab: process.env[TAB_ENV] });
|
|
60
|
+
} catch {
|
|
61
|
+
found = undefined;
|
|
62
|
+
}
|
|
63
|
+
const who = `${meant.bot}/${meant.session}`;
|
|
64
|
+
if (found?.transport === 'native' && typeof found.address === 'string') {
|
|
65
|
+
return `If you meant ${who}, ${ask} gives the road from your session: write to ${found.address} with your own messaging.`;
|
|
66
|
+
}
|
|
67
|
+
if (found?.transport === 'orca' && typeof found.address === 'string') {
|
|
68
|
+
return `If you meant ${who}, ${ask} gives the road from your session: its mailbox, ${found.address}, by \`${shellWord(ownCli())} message send\`, not your own messaging.`;
|
|
69
|
+
}
|
|
70
|
+
return `If you meant ${who}, its session address is ${meant.address}; ${ask} says which road to take from your session.`;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/** What the send answered, as an object: Claude Code hands it over as one, or as its JSON. */
|
|
74
|
+
function resultOf(said) {
|
|
75
|
+
const raw = said.tool_output ?? said.tool_response;
|
|
76
|
+
if (raw !== null && typeof raw === 'object' && !Array.isArray(raw)) return raw;
|
|
77
|
+
if (typeof raw !== 'string') return undefined;
|
|
78
|
+
try {
|
|
79
|
+
const parsed = JSON.parse(raw);
|
|
80
|
+
return parsed !== null && typeof parsed === 'object' && !Array.isArray(parsed) ? parsed : undefined;
|
|
81
|
+
} catch {
|
|
82
|
+
return undefined;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Every session address the fleet's books name, retired sessions' included, and
|
|
88
|
+
* each live one by `<bot>.<session>`: `{ all, byName }`. Undefined when a book
|
|
89
|
+
* cannot be read, since an address missing from it would be warned about wrongly.
|
|
90
|
+
*/
|
|
91
|
+
function addressesIn(bots) {
|
|
92
|
+
const all = new Set();
|
|
93
|
+
const byName = new Map();
|
|
94
|
+
try {
|
|
95
|
+
for (const bot of botNames(bots)) {
|
|
96
|
+
const book = readBook(botDir(bots, bot));
|
|
97
|
+
for (const [session, entry] of Object.entries(book.sessions ?? {})) {
|
|
98
|
+
const address = entry?.address;
|
|
99
|
+
if (typeof address !== 'string') continue;
|
|
100
|
+
all.add(address);
|
|
101
|
+
if (isAddressOf(bot, session, address)) byName.set(`${bot}.${session}`, { bot, session, address });
|
|
102
|
+
}
|
|
103
|
+
for (const entry of Array.isArray(book.retired) ? book.retired : []) {
|
|
104
|
+
if (typeof entry?.address === 'string') all.add(entry.address);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
return undefined;
|
|
109
|
+
}
|
|
110
|
+
return { all, byName };
|
|
111
|
+
}
|