@assuredloop/orca-bot-kit 0.26.1 → 0.27.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 +20 -26
- package/package.json +1 -1
- package/rules/mail.md +5 -5
- package/src/book.js +55 -16
- package/src/cli.js +40 -102
- package/src/commands.js +1 -3
- package/src/conversations.js +0 -37
- package/src/health.js +62 -5
- package/src/hooks.js +35 -63
- package/src/launch.js +20 -3
- package/src/message.js +42 -334
- package/src/name.js +1 -1
- package/src/orca.js +19 -35
- package/src/processes.js +392 -0
- package/src/retire.js +43 -40
- package/src/skills.js +1 -1
- package/src/up.js +13 -2
- package/src/unread.js +0 -160
package/README.md
CHANGED
|
@@ -365,19 +365,6 @@ address that is none of your bots folder's sessions, the hook tells it so, and
|
|
|
365
365
|
names the address `obk message to` would have given. The message goes either
|
|
366
366
|
way ([ADR 0032](docs/adr/0032-a-native-message-outside-the-fleet-is-warned-about.md)).
|
|
367
367
|
|
|
368
|
-
And a third: at the end of each turn, it tells the session once about fleet
|
|
369
|
-
mail the kit sent it that is still unread, without typing into its tab
|
|
370
|
-
([ADR 0035](docs/adr/0035-messaging-transport.md)). It asks Orca only when the kit
|
|
371
|
-
knows of mail for that session.
|
|
372
|
-
|
|
373
|
-
A Codex bot gets one more hook too. Inside Codex's sandbox the kit cannot always
|
|
374
|
-
tell whether the receiver of a message has its harness in front, as for a Claude
|
|
375
|
-
session busy running a command, so `obk message send` leaves that nudge, and the
|
|
376
|
-
hook, which Codex runs outside its sandbox right after the command, decides it and
|
|
377
|
-
types it, or says why not ([ADR 0034](docs/adr/0034-orca-is-the-host.md)). Codex
|
|
378
|
-
asks you to trust a hook it has not seen before, so a Codex bot made before this
|
|
379
|
-
hook asks again once.
|
|
380
|
-
|
|
381
368
|
So: kill a tab, or reboot, and `obk up` brings the session back with the
|
|
382
369
|
conversation it was having, rather than starting a new one. Clear a session —
|
|
383
370
|
`/clear` on Claude Code, `/new` on Codex — and the kit writes down the new id,
|
|
@@ -477,7 +464,12 @@ same refusals, and one more: it also refuses a tab with only its shell in front
|
|
|
477
464
|
whose conversation the book does not name. `obk up` leaves what is paused
|
|
478
465
|
closed until `obk unpause` brings it back with its conversations. Retiring a
|
|
479
466
|
session takes it off the bot and keeps its conversations in the book under
|
|
480
|
-
`retired`.
|
|
467
|
+
`retired`. Once its tab is closed, it stops what the session left running in
|
|
468
|
+
its work dir: each of your processes whose working folder is there, and what
|
|
469
|
+
they started, with SIGTERM and, after up to 3 seconds, SIGKILL. It names each
|
|
470
|
+
process it stopped, and each it left: one that only shares a process group with
|
|
471
|
+
them, or one it could not stop. Where it cannot read the processes, as inside
|
|
472
|
+
Codex's sandbox, it says so and stops nothing. Retiring a bot closes
|
|
481
473
|
its tabs, removes its Orca project and moves its folder to `retired/`; it will
|
|
482
474
|
not touch a bot whose Orca project holds a tab your book does not name. The
|
|
483
475
|
folder moves only once Orca no longer lists the project; if Orca still lists it,
|
|
@@ -557,6 +549,8 @@ What it looks for:
|
|
|
557
549
|
`<bot>.<session>` every fleet's sessions had before 0.10.0, which a session of
|
|
558
550
|
another fleet can write to. Its next start through the kit gives it one of its
|
|
559
551
|
own, so the finding names the `obk restart … --session` that does it;
|
|
552
|
+
- **a process still running in a retired session's work dir**: one from before
|
|
553
|
+
retire stopped them, or one it could not stop, with its pid and command;
|
|
560
554
|
- **leftovers no book owns**: an Orca project inside your bots folder with no
|
|
561
555
|
bot in it, a tab in a bot's project that the book does not name, a start
|
|
562
556
|
prompt written for a session that has gone, a clone of a skills source you no
|
|
@@ -580,7 +574,7 @@ verdict: which of these matter, and in what order, is for you or for Bot Father.
|
|
|
580
574
|
## Sessions and bots that talk
|
|
581
575
|
|
|
582
576
|
Ask the kit how to reach a session, and it answers with the road and the
|
|
583
|
-
address ([ADR
|
|
577
|
+
address ([ADR 0042](docs/adr/0042-messaging-transport.md)):
|
|
584
578
|
|
|
585
579
|
```sh
|
|
586
580
|
obk message to --bots /path/to/my-bots --to api-bot/daily
|
|
@@ -623,17 +617,17 @@ mailbox where it was and says so, and the harness starts all the same. A check
|
|
|
623
617
|
holds its turn for 40 seconds at most: with more mail than it reads in that
|
|
624
618
|
time, it shows what it read and says the rest is still waiting.
|
|
625
619
|
|
|
626
|
-
Nothing in a mailbox wakes anybody, so each message gets one signal
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
receiver's
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
([ADR
|
|
620
|
+
Nothing in a mailbox wakes anybody, so each message gets one signal: Orca's
|
|
621
|
+
own notice, which Orca types into the receiver's tab once it is idle. A busy
|
|
622
|
+
receiver gets it a minute or a few after its turn ends. The kit types nothing
|
|
623
|
+
into the receiver's tab, and the send says which road it took. For urgent mail,
|
|
624
|
+
`obk message send --interrupt` presses Escape once in a busy receiver's tab to
|
|
625
|
+
end its turn, and then sends the message, so the notice comes at once. Codex
|
|
626
|
+
keeps a command it was running as a background terminal after an Escape. A
|
|
627
|
+
receiver reads its mail with `obk message check`, which marks it read: a bare
|
|
628
|
+
`orca orchestration check` leaves it unread, and Orca then gives no more
|
|
629
|
+
notices for that mailbox
|
|
630
|
+
([ADR 0042](docs/adr/0042-messaging-transport.md)).
|
|
637
631
|
|
|
638
632
|
A message longer than 4 KiB is written to a file beside your bots folder and
|
|
639
633
|
named in the message, so a long one arrives whole without landing a document in
|
package/package.json
CHANGED
package/rules/mail.md
CHANGED
|
@@ -12,12 +12,12 @@ If your harness refuses a message, do not send it again by another road. Tell
|
|
|
12
12
|
your maker or your user, with the harness's reason. A refusal can mean a missing
|
|
13
13
|
permission rule, which they can add through the kit after the user's yes.
|
|
14
14
|
|
|
15
|
-
Mail is queued, not an interruption.
|
|
15
|
+
Mail is queued, not an interruption. When Orca says you have mail, read it
|
|
16
|
+
with `"${OBK_CLI:-obk}" message check --bots <bots>`, never a bare `orca
|
|
17
|
+
orchestration check`: that leaves it unread, and Orca stops telling you of mail.
|
|
16
18
|
|
|
17
19
|
Ask for a reply when you need one, and send one when you were asked for one.
|
|
18
20
|
|
|
19
|
-
A message
|
|
20
|
-
is what shows it arrived. Look for one before you send it again.
|
|
21
|
+
A sent message is only queued. Look for the reply or the work before you resend.
|
|
21
22
|
|
|
22
|
-
Write to the session that needs it, not to several
|
|
23
|
-
them is right.
|
|
23
|
+
Write only to the session that needs it, not to several at once.
|
package/src/book.js
CHANGED
|
@@ -6,12 +6,14 @@
|
|
|
6
6
|
// theirs. Beside each session's tab it holds the harness session id that
|
|
7
7
|
// session is running under, and every id it ran under before.
|
|
8
8
|
|
|
9
|
-
import { existsSync, mkdirSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
|
|
9
|
+
import { closeSync, existsSync, mkdirSync, openSync, readFileSync, realpathSync, renameSync, writeFileSync } from 'node:fs';
|
|
10
10
|
import path from 'node:path';
|
|
11
11
|
import { DatabaseSync } from 'node:sqlite';
|
|
12
12
|
import { isDeepStrictEqual } from 'node:util';
|
|
13
13
|
import { parse, stringify } from 'yaml';
|
|
14
14
|
|
|
15
|
+
import { kitFolders } from './launch.js';
|
|
16
|
+
|
|
15
17
|
const HEADER = `# What Orca calls this bot on this machine, and where each of its sessions
|
|
16
18
|
# lives. \`obk up\` writes this file; it is committed with the rest of the repo.
|
|
17
19
|
#
|
|
@@ -151,7 +153,7 @@ function takeLock(home) {
|
|
|
151
153
|
try {
|
|
152
154
|
return lockOn(lockFile(home), WAIT_MS);
|
|
153
155
|
} catch (error) {
|
|
154
|
-
throw waitedTooLong(home, error);
|
|
156
|
+
throw error instanceof LockNotWritable ? error : waitedTooLong(home, error);
|
|
155
157
|
}
|
|
156
158
|
}
|
|
157
159
|
|
|
@@ -162,7 +164,21 @@ function takeLock(home) {
|
|
|
162
164
|
* `EXCLUSIVE`, alone, once every `SHARED` holder has let go.
|
|
163
165
|
*/
|
|
164
166
|
function lockOn(file, waitMs, how = 'IMMEDIATE') {
|
|
165
|
-
|
|
167
|
+
// SQLite opens a file it cannot write read-only, without a word, and a
|
|
168
|
+
// read-only connection's BEGIN passes while another process holds the lock:
|
|
169
|
+
// seen in Codex's sandbox (#534). So the file is opened for writing first,
|
|
170
|
+
// and a lock that cannot be held is an error, never a turn that is not one.
|
|
171
|
+
// Only while this process holds no turn on it: closing any descriptor of a
|
|
172
|
+
// file lets go of every lock the process holds on that file, and SQLite's
|
|
173
|
+
// own are among them. A file a turn is held on was written already.
|
|
174
|
+
if (!heldHere.has(file)) {
|
|
175
|
+
try {
|
|
176
|
+
mkdirSync(path.dirname(file), { recursive: true });
|
|
177
|
+
closeSync(openSync(file, 'a'));
|
|
178
|
+
} catch (error) {
|
|
179
|
+
throw new LockNotWritable(file, error);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
166
182
|
|
|
167
183
|
const db = new DatabaseSync(file);
|
|
168
184
|
try {
|
|
@@ -181,17 +197,24 @@ function lockOn(file, waitMs, how = 'IMMEDIATE') {
|
|
|
181
197
|
throw error;
|
|
182
198
|
}
|
|
183
199
|
|
|
200
|
+
heldHere.set(file, (heldHere.get(file) ?? 0) + 1);
|
|
184
201
|
return {
|
|
185
202
|
release() {
|
|
186
203
|
try {
|
|
187
204
|
db.exec('COMMIT');
|
|
188
205
|
} finally {
|
|
189
206
|
db.close();
|
|
207
|
+
const left = heldHere.get(file) - 1;
|
|
208
|
+
if (left > 0) heldHere.set(file, left);
|
|
209
|
+
else heldHere.delete(file);
|
|
190
210
|
}
|
|
191
211
|
},
|
|
192
212
|
};
|
|
193
213
|
}
|
|
194
214
|
|
|
215
|
+
/** How many turns this process holds on each lock file, by its path. */
|
|
216
|
+
const heldHere = new Map();
|
|
217
|
+
|
|
195
218
|
/**
|
|
196
219
|
* Where a writer takes its turn: a folder of the kit's own beside the bots repo,
|
|
197
220
|
* carrying that folder's own name, the way start-prompt files sit beside it
|
|
@@ -213,14 +236,31 @@ function lockOn(file, waitMs, how = 'IMMEDIATE') {
|
|
|
213
236
|
* the bots folder through a link and another by its real path, and two paths
|
|
214
237
|
* would be two locks for one book (#375).
|
|
215
238
|
*/
|
|
216
|
-
const lockFile = (home) => {
|
|
239
|
+
const lockFile = (home) => path.join(botLocks(home), `${encodeURIComponent(path.basename(realpathSync(home)))}.lock`);
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* The bot's own folder of locks, `<bots>.locks/<bot>/`: the one a Codex
|
|
243
|
+
* session of the bot is given to write, beside its mail and its start prompts
|
|
244
|
+
* (#534). One folder per bot, so a bot's sessions can take each other's turns
|
|
245
|
+
* and no other bot's.
|
|
246
|
+
*/
|
|
247
|
+
const botLocks = (home) => {
|
|
217
248
|
const real = realpathSync(home);
|
|
218
|
-
return path.
|
|
219
|
-
`${path.dirname(path.dirname(real))}.locks`,
|
|
220
|
-
`${encodeURIComponent(path.basename(real))}.lock`,
|
|
221
|
-
);
|
|
249
|
+
return kitFolders(path.dirname(path.dirname(real)), path.basename(real)).locks;
|
|
222
250
|
};
|
|
223
251
|
|
|
252
|
+
/**
|
|
253
|
+
* A lock the kit cannot write. Inside Codex's sandbox that is a session
|
|
254
|
+
* started before the kit gave its bot a folder of locks, so it says what puts
|
|
255
|
+
* it right.
|
|
256
|
+
*/
|
|
257
|
+
class LockNotWritable extends Error {
|
|
258
|
+
constructor(file, why) {
|
|
259
|
+
super(`the kit cannot write its lock ${file} (${why.code ?? why.message}), so it did not take the turn, and nothing was done. A Codex session started before this version of the kit cannot write its bot's folder of locks: restart this session with the kit. Elsewhere, make ${path.dirname(file)} writable.`);
|
|
260
|
+
this.code = why.code;
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
|
|
224
264
|
/**
|
|
225
265
|
* One session's turn at its mailbox (#321): held by `session mailbox` for its
|
|
226
266
|
* whole step, by `message check` in the session's own tab for its bind, read and
|
|
@@ -286,11 +326,11 @@ export function takeTypingTurn(home, session, waitMs) {
|
|
|
286
326
|
|
|
287
327
|
/**
|
|
288
328
|
* A session's turn for one whole line, sent with its return in one go, as the
|
|
289
|
-
* mail
|
|
329
|
+
* mail interrupt is: through the typing turn's gate, waited for `waitMs` at most,
|
|
290
330
|
* and then held beside any other line, so whole lines never wait on each
|
|
291
331
|
* other, only on typing one key at a time. Returns `{ release }`, or undefined
|
|
292
332
|
* when the gate did not come within `waitMs`. Every whole line the kit types
|
|
293
|
-
* takes it: the
|
|
333
|
+
* takes it: the mail interrupt, the list line, the skills reload and the grooming line (#482).
|
|
294
334
|
*/
|
|
295
335
|
export function takeLineTurn(home, session, waitMs) {
|
|
296
336
|
const gate = turnOn(home, session, 'typing', waitMs);
|
|
@@ -312,8 +352,9 @@ export const TYPING_WAIT_MS = 5000;
|
|
|
312
352
|
export const TYPING_HELD = `the kit is typing into it, and it was still at it after ${TYPING_WAIT_MS / 1000} s, so nothing was typed`;
|
|
313
353
|
|
|
314
354
|
/**
|
|
315
|
-
* How long a typing turn waits for the lines already on their way: a
|
|
316
|
-
* holds its
|
|
355
|
+
* How long a typing turn waits for the lines already on their way: a line
|
|
356
|
+
* re-issued to Orca holds its turn while Orca waits up to 5 s to see it start
|
|
357
|
+
* a turn.
|
|
317
358
|
*/
|
|
318
359
|
const LINES_WAIT_MS = 10_000;
|
|
319
360
|
|
|
@@ -344,10 +385,8 @@ const SQLITE_BUSY = 5;
|
|
|
344
385
|
* link by one command and by its real path by another, and two paths would be
|
|
345
386
|
* two turns.
|
|
346
387
|
*/
|
|
347
|
-
const mailboxLockFile = (home, session) =>
|
|
348
|
-
|
|
349
|
-
return path.join(`${path.dirname(path.dirname(real))}.locks`, `${path.basename(real)}.${session}.mailbox.lock`);
|
|
350
|
-
};
|
|
388
|
+
const mailboxLockFile = (home, session) =>
|
|
389
|
+
path.join(botLocks(home), `${path.basename(realpathSync(home))}.${session}.mailbox.lock`);
|
|
351
390
|
|
|
352
391
|
/**
|
|
353
392
|
* Something else has been writing the book for longer than this run is prepared
|
package/src/cli.js
CHANGED
|
@@ -19,14 +19,14 @@ import { checkHealth, orcaSettingFindings } from './health.js';
|
|
|
19
19
|
import { initBots } from './init.js';
|
|
20
20
|
import { nameSession } from './name.js';
|
|
21
21
|
import { APPROVALS, harnessOf, HARNESSES, ownCli, refuseApprovalArgs, shellWord, workDirOf } from './launch.js';
|
|
22
|
-
import { checkMail,
|
|
22
|
+
import { checkMail, lookUp, noMailboxYet, sendMessage, sessionInTab } from './message.js';
|
|
23
23
|
import { orcaCli, orcaTrouble, RELOAD_LINE, TERMINAL_ENV } from './orca.js';
|
|
24
24
|
import { allowIn, beyondDefaults, refuseBroad, refuseNoCodexForm, runsOnClaude, runsOnCodex, takeBack, writePermissions } from './permissions.js';
|
|
25
25
|
import { pauseSessions, unpauseSessions } from './pause.js';
|
|
26
26
|
import { recordSession, SHELL_ENV, TAB_ENV } from './record.js';
|
|
27
27
|
import { restartSessions } from './restart.js';
|
|
28
28
|
import { clearSession, compactSession } from './clear.js';
|
|
29
|
-
import { retireBot, retireSession
|
|
29
|
+
import { retireBot, retireSession } from './retire.js';
|
|
30
30
|
import { sentWarning } from './sent.js';
|
|
31
31
|
import { readRoster } from './roster.js';
|
|
32
32
|
import { buildAgents, buildRules, CODEX_CAP } from './rules.js';
|
|
@@ -109,7 +109,9 @@ Usage:
|
|
|
109
109
|
End a session: close its tab and take it off the
|
|
110
110
|
bot, keeping its conversations in the book, and
|
|
111
111
|
first do the same for the temporary sessions it
|
|
112
|
-
made, and theirs.
|
|
112
|
+
made, and theirs. It stops what the session left
|
|
113
|
+
running in its work dir, and names each process.
|
|
114
|
+
Or end a bot: close its tabs,
|
|
113
115
|
remove its Orca project and move its folder to
|
|
114
116
|
retired/. It will not retire a bot whose Orca
|
|
115
117
|
project holds a tab your book does not name, and
|
|
@@ -207,9 +209,12 @@ Usage:
|
|
|
207
209
|
the Orca mailbox. Nothing is sent.
|
|
208
210
|
obk message send --bots <path> --to <bot>[/<session>] [--from <bot>/<session>]
|
|
209
211
|
--subject <text> [--text <text> | --text-file <path>]
|
|
210
|
-
[--thread <id>]
|
|
211
|
-
Put a message in that session's Orca mailbox
|
|
212
|
-
|
|
212
|
+
[--thread <id>] [--interrupt]
|
|
213
|
+
Put a message in that session's Orca mailbox. Orca
|
|
214
|
+
tells it to look once it is idle. --interrupt is
|
|
215
|
+
for urgent mail: when the session is busy, the kit
|
|
216
|
+
presses Escape once in its tab first, to end its
|
|
217
|
+
turn, so it looks now. A message too long to travel
|
|
213
218
|
as itself is written to a file beside your bots
|
|
214
219
|
folder and named in the message. A pair the
|
|
215
220
|
harness's own messaging reaches is not carried:
|
|
@@ -305,17 +310,6 @@ Usage:
|
|
|
305
310
|
Claude session's native message, it warns the
|
|
306
311
|
session when the address was none of this bots
|
|
307
312
|
folder's own. It never stops a message.
|
|
308
|
-
obk session nudge --bots <path> --bot <bot>
|
|
309
|
-
For the kit's own hook, not for typing: after a
|
|
310
|
-
Codex session's shell command, it decides the mail
|
|
311
|
-
nudges a send there left because it could not
|
|
312
|
-
tell from inside Codex's sandbox, and types each
|
|
313
|
-
one or says why not.
|
|
314
|
-
obk session mail --bots <path> --bot <bot>
|
|
315
|
-
For the kit's own hook, not for typing: at the end
|
|
316
|
-
of a Claude session's turn, it tells the session
|
|
317
|
-
once about each fleet mail the kit sent it that is
|
|
318
|
-
still unread. It types nothing into the tab.
|
|
319
313
|
obk session name --bots <path> --bot <bot>
|
|
320
314
|
For the kit's own hook, not for typing: after a
|
|
321
315
|
Codex session's turn, it names the session's thread
|
|
@@ -409,6 +403,7 @@ async function run(argv) {
|
|
|
409
403
|
'text-file': { type: 'string' },
|
|
410
404
|
thread: { type: 'string' },
|
|
411
405
|
peek: { type: 'boolean' },
|
|
406
|
+
interrupt: { type: 'boolean' },
|
|
412
407
|
charter: { type: 'string' },
|
|
413
408
|
allow: { type: 'string', multiple: true },
|
|
414
409
|
disallow: { type: 'string', multiple: true },
|
|
@@ -476,8 +471,6 @@ async function run(argv) {
|
|
|
476
471
|
// The send hook is quiet whatever it is given, a bots folder that is not
|
|
477
472
|
// there included, so it goes before anything that can complain (ADR 0032).
|
|
478
473
|
if (command === SENT) return sent(path.resolve(values.bots));
|
|
479
|
-
if (command === NUDGE) return nudgeLeft();
|
|
480
|
-
if (command === MAIL) return mailHook(path.resolve(values.bots), values.bot);
|
|
481
474
|
if (command === NAME) return nameThread(path.resolve(values.bots), values.bot);
|
|
482
475
|
|
|
483
476
|
// One fleet, one identity, whatever spelling of its path was given (#164).
|
|
@@ -548,8 +541,6 @@ const ANSWER_IT = [
|
|
|
548
541
|
/** The commands a harness runs rather than a person: the kit's hooks. */
|
|
549
542
|
const RECORD = 'session record';
|
|
550
543
|
const SENT = 'session sent';
|
|
551
|
-
const NUDGE = 'session nudge';
|
|
552
|
-
const MAIL = 'session mail';
|
|
553
544
|
const NAME = 'session name';
|
|
554
545
|
|
|
555
546
|
/**
|
|
@@ -569,45 +560,6 @@ function sent(bots) {
|
|
|
569
560
|
return 0;
|
|
570
561
|
}
|
|
571
562
|
|
|
572
|
-
/**
|
|
573
|
-
* What the kit's Codex hook does after a shell command: decide the nudges a
|
|
574
|
-
* send in this tab left for it, outside Codex's sandbox, and say to the session
|
|
575
|
-
* what became of each, as Codex's `additionalContext` (#350, ADR 0034). Silent
|
|
576
|
-
* when nothing was left, never a decision, and never anything but exit 0.
|
|
577
|
-
*/
|
|
578
|
-
async function nudgeLeft() {
|
|
579
|
-
try {
|
|
580
|
-
const said = JSON.parse(readFileSync(0, 'utf8'));
|
|
581
|
-
if (said?.hook_event_name !== 'PostToolUse') return 0;
|
|
582
|
-
const decided = await decideLeftNudges(process.env[TAB_ENV]);
|
|
583
|
-
if (decided.length > 0) {
|
|
584
|
-
const words = decided.map((one) => `Your mail "${one.subject}" to ${one.to}: ${one.nudged ? "the kit's hook nudged its tab, from outside Codex's sandbox:" : "the kit's hook looked at its tab from outside Codex's sandbox, and"} ${nudgeLine(one, one.to).trim()}`);
|
|
585
|
-
process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'PostToolUse', additionalContext: words.join('\n') } })}\n`);
|
|
586
|
-
}
|
|
587
|
-
} catch {
|
|
588
|
-
// Nothing: a hook does not disturb the session it runs in.
|
|
589
|
-
}
|
|
590
|
-
return 0;
|
|
591
|
-
}
|
|
592
|
-
|
|
593
|
-
/**
|
|
594
|
-
* What the kit's Claude Code Stop hook does at a turn end (#509, ADR 0035): tell
|
|
595
|
-
* the session once about fleet mail still unread, as the Stop event's
|
|
596
|
-
* additional context, which the session goes on with. Not a block: Claude Code
|
|
597
|
-
* 2.1.296 draws a block's reason in red as "Stop hook error", and its additional
|
|
598
|
-
* context as "Stop hook feedback". Nothing typed into the tab; never anything
|
|
599
|
-
* but exit 0; silent whenever it cannot be sure.
|
|
600
|
-
*/
|
|
601
|
-
function mailHook(bots, bot) {
|
|
602
|
-
try {
|
|
603
|
-
const reason = stillUnread(sameFleet(bots), bot, JSON.parse(readFileSync(0, 'utf8')), process.env[TAB_ENV], process.env[TERMINAL_ENV]);
|
|
604
|
-
if (reason !== undefined) process.stdout.write(`${JSON.stringify({ hookSpecificOutput: { hookEventName: 'Stop', additionalContext: reason } })}\n`);
|
|
605
|
-
} catch {
|
|
606
|
-
// Nothing: a hook does not disturb the session it runs in.
|
|
607
|
-
}
|
|
608
|
-
return 0;
|
|
609
|
-
}
|
|
610
|
-
|
|
611
563
|
/**
|
|
612
564
|
* What the kit's Codex Stop hook does at a turn end: name the session's thread
|
|
613
565
|
* after its bot and session, once it is idle (#480). Silent, never a decision,
|
|
@@ -737,7 +689,7 @@ const commands = {
|
|
|
737
689
|
...withLines(retired.retiredWith, retired.session),
|
|
738
690
|
...closedLines(retired.closed),
|
|
739
691
|
`retired ${retired.bot} ${retired.session}: off ${path.join('bots', retired.bot, 'bot.yaml')}, and its conversations kept in the book under retired`,
|
|
740
|
-
...
|
|
692
|
+
...processLines(retired.processes),
|
|
741
693
|
...leftLines(retired.promptsLeft),
|
|
742
694
|
],
|
|
743
695
|
};
|
|
@@ -749,6 +701,7 @@ const commands = {
|
|
|
749
701
|
answer: { bots, ...retired },
|
|
750
702
|
lines: [
|
|
751
703
|
...closedLines(retired.closed),
|
|
704
|
+
...processLines(retired.processes),
|
|
752
705
|
`${'trouble'.padEnd(9)} Orca project ${retired.project}`,
|
|
753
706
|
` ${retired.trouble}`,
|
|
754
707
|
`Retire it again: ${shellWord(ownCli())} retire --bots ${shellWord(bots)} --bot ${retired.bot}`,
|
|
@@ -767,7 +720,7 @@ const commands = {
|
|
|
767
720
|
retired.windowReloaded ? "reloaded Orca's window, so its sidebar no longer shows the project" : RELOAD_LINE,
|
|
768
721
|
]),
|
|
769
722
|
`retired ${retired.bot}: moved to ${path.relative(bots, retired.moved)}, with its book, charter and memory`,
|
|
770
|
-
...(retired.
|
|
723
|
+
...processLines(retired.processes),
|
|
771
724
|
...leftLines(retired.promptsLeft),
|
|
772
725
|
],
|
|
773
726
|
};
|
|
@@ -824,7 +777,7 @@ const commands = {
|
|
|
824
777
|
...withLines(retired.retiredWith, retired.session),
|
|
825
778
|
...closedLines(retired.closed),
|
|
826
779
|
`retired ${retired.bot} ${retired.session}, a temporary session of ${retired.maker}'s: off ${path.join('bots', retired.bot, 'bot.yaml')}, and its conversations kept in the book under retired`,
|
|
827
|
-
...
|
|
780
|
+
...processLines(retired.processes),
|
|
828
781
|
...leftLines(retired.promptsLeft),
|
|
829
782
|
],
|
|
830
783
|
};
|
|
@@ -1189,6 +1142,7 @@ const commands = {
|
|
|
1189
1142
|
text: values.text,
|
|
1190
1143
|
textFile: values['text-file'],
|
|
1191
1144
|
thread: values.thread,
|
|
1145
|
+
interrupt: values.interrupt === true,
|
|
1192
1146
|
});
|
|
1193
1147
|
const where = `${answer.to.bot}/${answer.to.session}`;
|
|
1194
1148
|
|
|
@@ -1207,7 +1161,7 @@ const commands = {
|
|
|
1207
1161
|
...(answer.file === undefined
|
|
1208
1162
|
? []
|
|
1209
1163
|
: [` it was too long to travel as itself, so it went as a file: ${answer.file}`]),
|
|
1210
|
-
...
|
|
1164
|
+
...sentLines(answer, where),
|
|
1211
1165
|
],
|
|
1212
1166
|
};
|
|
1213
1167
|
},
|
|
@@ -1348,46 +1302,23 @@ function toLines(answer, bots, where) {
|
|
|
1348
1302
|
}
|
|
1349
1303
|
|
|
1350
1304
|
/**
|
|
1351
|
-
* What
|
|
1352
|
-
*
|
|
1353
|
-
* about the message.
|
|
1305
|
+
* What the send says after `sent`: the road it took, and what the interrupt
|
|
1306
|
+
* came to when one was asked for (#555).
|
|
1354
1307
|
*/
|
|
1355
|
-
function
|
|
1356
|
-
const
|
|
1357
|
-
if (answer.
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
}
|
|
1363
|
-
const why = {
|
|
1364
|
-
'no-turn': `no turn started in its tab in ${WATCH_SECONDS} s, so the kit's line was typed`,
|
|
1365
|
-
busy: "it is busy with a turn, and Codex takes a line into the turn it is having, so the kit's line was typed",
|
|
1366
|
-
'other-turn': "a turn of other work started in its tab, not Orca's notice, so the kit's line was typed",
|
|
1367
|
-
}[answer.because];
|
|
1368
|
-
const head = why === undefined ? ' ' : ` ${why}: `;
|
|
1369
|
-
if (answer.nudged && answer.nudgeWatched === false) {
|
|
1370
|
-
return `${head}its tab was typed into, and Orca could not say whether it was taken: ${answer.nudgeUnseen} The message waits in its mailbox for its next check either way.`;
|
|
1371
|
-
}
|
|
1372
|
-
if (answer.nudged && answer.nudgeUnseen !== undefined) {
|
|
1373
|
-
return `${head}its tab was typed into, but Orca did not see the line start a turn: it may be queued behind the work in hand, or lost. The message waits in its mailbox for its next check either way.`;
|
|
1374
|
-
}
|
|
1375
|
-
if (answer.nudged) return `${head}its tab was told to look; it will read it when it is done with what it is doing.`;
|
|
1376
|
-
if (answer.blocked !== undefined) {
|
|
1377
|
-
return ` its tab has something waiting to be answered (${answer.blocked}), so nothing was typed into it. Settle that, and the mail is there.`;
|
|
1378
|
-
}
|
|
1379
|
-
if (answer.nudgeLeft) {
|
|
1380
|
-
return ` it is queued, and its tab could not be told to look from here: ${answer.nudgeTrouble}. So the nudge is left for this session's hook, which looks at the tab from outside Codex's sandbox when this command is done, and says what it did.`;
|
|
1308
|
+
function sentLines(answer, where) {
|
|
1309
|
+
const road = " it is in its Orca mailbox, and Orca's own notice tells it to look once it is idle.";
|
|
1310
|
+
if (answer.interrupted === true) {
|
|
1311
|
+
return [
|
|
1312
|
+
`${road.replace('once it is idle', 'now')}`,
|
|
1313
|
+
` it was busy, so the kit pressed Escape once in its tab to end its turn.${answer.to.harness === 'codex' ? ' Codex keeps a command it was running as a background terminal after an Escape.' : ''}`,
|
|
1314
|
+
];
|
|
1381
1315
|
}
|
|
1382
|
-
if (answer.
|
|
1383
|
-
return ` it is
|
|
1316
|
+
if (answer.interrupted === false) {
|
|
1317
|
+
return [road, answer.idle ? ` it is idle, so it was not interrupted.` : ` it was not interrupted: ${answer.interruptTrouble}.`];
|
|
1384
1318
|
}
|
|
1385
|
-
return
|
|
1319
|
+
return [road];
|
|
1386
1320
|
}
|
|
1387
1321
|
|
|
1388
|
-
/** The watch before the kit's line, as the send says it (#509, ADR 0035). */
|
|
1389
|
-
const WATCH_SECONDS = WATCH_MS / 1000;
|
|
1390
|
-
|
|
1391
1322
|
/**
|
|
1392
1323
|
* What there is to say about the work dir this command was given: one finding
|
|
1393
1324
|
* when it leads out of the bot home, through `..` or a link or as a path
|
|
@@ -1459,12 +1390,19 @@ const closedLines = (closed) => closed.map((tab) => `closed ${tab.bot} ${tab
|
|
|
1459
1390
|
const withLines = (retiredWith, session) => retiredWith.flatMap((gone) => [
|
|
1460
1391
|
...closedLines(gone.closed),
|
|
1461
1392
|
`retired ${gone.bot} ${gone.session}, a temporary session of ${gone.maker}'s, along with ${session}: off ${path.join('bots', gone.bot, 'bot.yaml')}, and its conversations kept in the book under retired`,
|
|
1462
|
-
...
|
|
1393
|
+
...processLines(gone.processes),
|
|
1463
1394
|
...leftLines(gone.promptsLeft),
|
|
1464
1395
|
]);
|
|
1465
1396
|
|
|
1466
|
-
/**
|
|
1467
|
-
|
|
1397
|
+
/** What a retire stopped of what its sessions left running, and what it named and left (#537). */
|
|
1398
|
+
function processLines(processes) {
|
|
1399
|
+
if (processes === undefined) return [];
|
|
1400
|
+
if (processes.unreadable !== undefined) return [`processes the kit cannot tell which processes were left running in the work dir, so it stopped none: ${processes.unreadable}`];
|
|
1401
|
+
return [
|
|
1402
|
+
...processes.stopped.map((one) => `stopped pid ${one.pid} of ${one.session}, with ${one.signal}: ${one.command}${one.cwd === null ? '' : ` in ${one.cwd}`}`),
|
|
1403
|
+
...processes.left.map((one) => `left pid ${one.pid} in ${one.cwd ?? 'a folder lsof did not name'}: ${one.command}. Not stopped: ${one.why}.`),
|
|
1404
|
+
];
|
|
1405
|
+
}
|
|
1468
1406
|
|
|
1469
1407
|
/** What a retire could not remove, and how to: nothing reads these files now (#393). */
|
|
1470
1408
|
const leftLines = (left = []) => left.flatMap(({ file, reason }) => [
|
package/src/commands.js
CHANGED
|
@@ -37,8 +37,6 @@ export const COMMANDS = {
|
|
|
37
37
|
'message check': ['bots'],
|
|
38
38
|
'session record': ['bots', 'bot'],
|
|
39
39
|
'session sent': ['bots', 'bot'],
|
|
40
|
-
'session nudge': ['bots', 'bot'],
|
|
41
|
-
'session mail': ['bots', 'bot'],
|
|
42
40
|
'session name': ['bots', 'bot'],
|
|
43
41
|
'session mailbox': ['bots', 'bot', 'session'],
|
|
44
42
|
'session trust-hooks': ['bots', 'bot', 'session'],
|
|
@@ -61,7 +59,7 @@ export const DEFAULT_COMMANDS = [
|
|
|
61
59
|
'skills add', 'skills remove', 'skills build', 'skills fetch', 'skills update', 'source add',
|
|
62
60
|
'session add', 'session change', 'session clear', 'session compact',
|
|
63
61
|
'message to', 'message send', 'message check',
|
|
64
|
-
'session record', 'session sent', 'session
|
|
62
|
+
'session record', 'session sent', 'session name', 'session mailbox',
|
|
65
63
|
'session trust-hooks', 'session answer',
|
|
66
64
|
'temp make', 'temp roles', 'temp retire', 'temp trust-hooks', 'temp answer',
|
|
67
65
|
];
|
package/src/conversations.js
CHANGED
|
@@ -159,43 +159,6 @@ export function heldAsUserTurn(harness, file, text) {
|
|
|
159
159
|
return held;
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
/**
|
|
163
|
-
* Where the harness keeps the conversation `id` of this bot home, and how long
|
|
164
|
-
* that record is now: `{ harness, file, size }`, so a later look reads only
|
|
165
|
-
* what was written after this moment. Undefined when there is no id, no
|
|
166
|
-
* record, or it cannot be read (#509).
|
|
167
|
-
*/
|
|
168
|
-
export function recordMark(harness, home, id) {
|
|
169
|
-
if (typeof id !== 'string' || id === '') return undefined;
|
|
170
|
-
try {
|
|
171
|
-
const file = harness === 'codex' ? codexRollout(id) : claudeTranscript(home, id);
|
|
172
|
-
if (file === undefined) return undefined;
|
|
173
|
-
const found = statSync(file);
|
|
174
|
-
return found.isFile() ? { harness, file, size: found.size } : undefined;
|
|
175
|
-
} catch {
|
|
176
|
-
return undefined;
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
/**
|
|
181
|
-
* Whether a turn of the user's written after `mark` holds `text` anywhere in
|
|
182
|
-
* it (#509). A record that cannot be read says no: not seeing a thing is not
|
|
183
|
-
* seeing it.
|
|
184
|
-
*/
|
|
185
|
-
export function userTurnSince(mark, text) {
|
|
186
|
-
if (mark === undefined || mark === null) return false;
|
|
187
|
-
let held = false;
|
|
188
|
-
try {
|
|
189
|
-
eachLine(mark.file, (line) => {
|
|
190
|
-
held = line !== undefined && userTexts(mark.harness, line).some((said) => said.includes(text));
|
|
191
|
-
return held;
|
|
192
|
-
}, { from: mark.size });
|
|
193
|
-
} catch {
|
|
194
|
-
return false;
|
|
195
|
-
}
|
|
196
|
-
return held;
|
|
197
|
-
}
|
|
198
|
-
|
|
199
162
|
/** The texts of one record line, when it is a turn of the user's. */
|
|
200
163
|
function userTexts(harness, line) {
|
|
201
164
|
let parsed;
|