@assuredloop/orca-bot-kit 0.36.1 → 0.38.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 +21 -1
- package/package.json +1 -1
- package/src/cli.js +18 -5
- package/src/message.js +38 -4
- package/src/orca.js +60 -2
- package/src/pause.js +6 -2
- package/src/restart.js +58 -12
- package/src/retire.js +7 -5
- package/src/temp.js +2 -1
- package/src/up.js +60 -7
package/README.md
CHANGED
|
@@ -474,6 +474,18 @@ It closes the session's tab — one tab, by its own handle — and opens a new o
|
|
|
474
474
|
with the conversation the book holds, so the session comes back as itself. With
|
|
475
475
|
no `--session` it does that for every session of the bot.
|
|
476
476
|
|
|
477
|
+
Closing the old tab ends the harness there, and the background shells it ran
|
|
478
|
+
end with it: a timer, a watch, a long build. So once a session whose tab the
|
|
479
|
+
restart closed is back with its conversation, the kit types one line into its
|
|
480
|
+
new tab. A session whose tab was already closed, or that starts fresh, gets no
|
|
481
|
+
line. The line says that its background shells
|
|
482
|
+
ended, that the kit cannot tell which ones ran or whether a scheduled job still
|
|
483
|
+
runs, and that it should look at its scheduled jobs before it makes one again.
|
|
484
|
+
It also asks the session to read its mail and do what it asks. The line goes in
|
|
485
|
+
only when the tab shows no question, form or menu, within 20 seconds. The
|
|
486
|
+
report says under each tab whether the line went in. When it did not, it says
|
|
487
|
+
why, and you tell the session yourself.
|
|
488
|
+
|
|
477
489
|
What it will not do, in order of how much it would cost you. It closes only a
|
|
478
490
|
tab your book names, so Bot Father's ops tab and anything you opened yourself
|
|
479
491
|
are left alone. It will not close a tab whose conversation the book cannot name:
|
|
@@ -516,7 +528,15 @@ effort and context go back to the new harness's defaults unless you give them
|
|
|
516
528
|
too. A pause closes the tabs the way a restart does, with the
|
|
517
529
|
same refusals, and one more: it also refuses a tab with only its shell in front
|
|
518
530
|
whose conversation the book does not name. `obk up` leaves what is paused
|
|
519
|
-
closed until `obk unpause` brings it back with its conversations.
|
|
531
|
+
closed until `obk unpause` brings it back with its conversations. The closed
|
|
532
|
+
tab ended the background shells of each paused session, as a restart does. So
|
|
533
|
+
the unpause tells each session it took off pause and resumed, in one typed
|
|
534
|
+
line, as a restart does: its old tab is closed and its background shells
|
|
535
|
+
ended, and it looks at its scheduled jobs and its mail. The report says under
|
|
536
|
+
each tab whether the line went in. In the `--json` answer, `restartLine` means
|
|
537
|
+
the same after an unpause as after a restart: `true` when the line went in, or
|
|
538
|
+
`false` with the reason in `restartLineTrouble`, and then you tell the session
|
|
539
|
+
yourself. Retiring a
|
|
520
540
|
session takes it off the bot and keeps its conversations in the book under
|
|
521
541
|
`retired`. Once its tab is closed, it stops what the session left running in
|
|
522
542
|
its work dir: each of your processes whose working folder is there, and what
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -690,7 +690,7 @@ const commands = {
|
|
|
690
690
|
const answer = { bots, bot, session, changed, created: [], completed: [], ...up };
|
|
691
691
|
return {
|
|
692
692
|
answer,
|
|
693
|
-
lines: tabLines(answer, `${changed ? 'Unpaused' : 'Not paused, so brought up as it is'}: ${what}. Your bots folder: ${bots}
|
|
693
|
+
lines: tabLines(answer, `${changed ? 'Unpaused' : 'Not paused, so brought up as it is'}: ${what}. Your bots folder: ${bots}`, 'pause'),
|
|
694
694
|
};
|
|
695
695
|
},
|
|
696
696
|
|
|
@@ -1918,7 +1918,7 @@ function settingsLine({ bot, session, running, settings, rules }) {
|
|
|
1918
1918
|
return `${word.padEnd(9)} ${who} ${each.join(' ')} rules ${rules.state}`;
|
|
1919
1919
|
}
|
|
1920
1920
|
|
|
1921
|
-
function tabLines({ bots, created, completed, rules, skills, permissions = [], tabs, paused = [], projects = [], found = [] }, summary) {
|
|
1921
|
+
function tabLines({ bots, created, completed, rules, skills, permissions = [], tabs, paused = [], projects = [], found = [] }, summary, closedBy = 'restart') {
|
|
1922
1922
|
const lines = [
|
|
1923
1923
|
...created.map((entry) => `created ${entry}`),
|
|
1924
1924
|
...completed.map((entry) => `completed ${entry}`),
|
|
@@ -1929,7 +1929,7 @@ function tabLines({ bots, created, completed, rules, skills, permissions = [], t
|
|
|
1929
1929
|
|
|
1930
1930
|
for (const tab of tabs) {
|
|
1931
1931
|
lines.push(`${tab.created ? 'opened' : 'found '} ${tab.title} tab ${tab.tabId} terminal ${tab.terminal}`);
|
|
1932
|
-
lines.push(...harnessLines(tab, bots));
|
|
1932
|
+
lines.push(...harnessLines(tab, bots, closedBy));
|
|
1933
1933
|
if (tab.noMailbox) {
|
|
1934
1934
|
lines.push(` it has no mailbox: ${noMailboxYet({ bots, bot: tab.bot, session: tab.name })}`);
|
|
1935
1935
|
}
|
|
@@ -1962,7 +1962,7 @@ function tabLines({ bots, created, completed, rules, skills, permissions = [], t
|
|
|
1962
1962
|
* The caller decides what to do next from these lines, so they say both what
|
|
1963
1963
|
* was typed and what was seen afterwards, and never one in place of the other.
|
|
1964
1964
|
*/
|
|
1965
|
-
function harnessLines(tab, bots) {
|
|
1965
|
+
function harnessLines(tab, bots, closedBy) {
|
|
1966
1966
|
if (!tab.created || tab.name === null) return [];
|
|
1967
1967
|
|
|
1968
1968
|
// Nothing was typed into it, so there is nothing more to say about a harness
|
|
@@ -1988,6 +1988,7 @@ function harnessLines(tab, bots) {
|
|
|
1988
1988
|
` Look at it: ${lookAt(tab.terminal)}`,
|
|
1989
1989
|
...ANSWER_IT,
|
|
1990
1990
|
...promptLines(tab),
|
|
1991
|
+
...restartLines(tab, closedBy),
|
|
1991
1992
|
];
|
|
1992
1993
|
}
|
|
1993
1994
|
|
|
@@ -2009,7 +2010,7 @@ function harnessLines(tab, bots) {
|
|
|
2009
2010
|
...ANSWER_IT,
|
|
2010
2011
|
];
|
|
2011
2012
|
|
|
2012
|
-
lines.push(...promptLines(tab), ...listLines(tab));
|
|
2013
|
+
lines.push(...promptLines(tab), ...listLines(tab), ...restartLines(tab, closedBy));
|
|
2013
2014
|
if (tab.unclaimed !== undefined) lines.push(...unclaimedLines(tab, bots));
|
|
2014
2015
|
return lines;
|
|
2015
2016
|
}
|
|
@@ -2024,6 +2025,18 @@ function listLines(tab) {
|
|
|
2024
2025
|
return [];
|
|
2025
2026
|
}
|
|
2026
2027
|
|
|
2028
|
+
/**
|
|
2029
|
+
* Whether a session `obk restart` resumed was typed the line that tells it what
|
|
2030
|
+
* the restart ended (#726), or one `obk unpause` resumed, what the pause ended
|
|
2031
|
+
* (#732): `closedBy` names which. When it was not, the caller is the one left
|
|
2032
|
+
* to tell it.
|
|
2033
|
+
*/
|
|
2034
|
+
function restartLines(tab, closedBy) {
|
|
2035
|
+
if (tab.restartLine === true) return [` it was typed one line that tells it what the ${closedBy} ended: its background shells, and to look at its scheduled jobs and its mail.`];
|
|
2036
|
+
if (tab.restartLine === false) return [` it was not told what the ${closedBy} ended (${tab.restartLineTrouble}): tell it yourself that its background shells ended, and to look at its scheduled jobs and its mail.`];
|
|
2037
|
+
return [];
|
|
2038
|
+
}
|
|
2039
|
+
|
|
2027
2040
|
/**
|
|
2028
2041
|
* Whether the session got its start prompt, as its own record has it (#274):
|
|
2029
2042
|
* received, or not confirmed yet. Nothing for a session that was told nothing.
|
package/src/message.js
CHANGED
|
@@ -16,6 +16,7 @@
|
|
|
16
16
|
// Escape a sender asks for with --interrupt. Orca gives no notice to a tab its
|
|
17
17
|
// window has not loaded, so the send says when the receiver's is one (#579).
|
|
18
18
|
|
|
19
|
+
import { randomUUID } from 'node:crypto';
|
|
19
20
|
import { mkdirSync, readdirSync, readFileSync, realpathSync, writeFileSync } from 'node:fs';
|
|
20
21
|
import { homedir } from 'node:os';
|
|
21
22
|
import path from 'node:path';
|
|
@@ -24,7 +25,7 @@ import { setTimeout as pause } from 'node:timers/promises';
|
|
|
24
25
|
import { MAILBOX_WAIT_MS, readBook, takeLineTurn, takeMailboxTurn, TYPING_HELD, TYPING_WAIT_MS } from './book.js';
|
|
25
26
|
import { botDir, botNames, readBot } from './bot.js';
|
|
26
27
|
import { harnessOf, isAddressOf, kitFolders, ownCli, reachesMail, SHELL_ENV, shellWord } from './launch.js';
|
|
27
|
-
import { ackMailbox, coordinatorOf, isRunning, listedOrphaned, loadInWindow, postMessage, pressEscape, readMailbox, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, useMailbox } from './orca.js';
|
|
28
|
+
import { ackMailbox, coordinatorOf, isRunning, listedOrphaned, loadInWindow, postMessage, pressEscape, readMailbox, readsCarryRequest, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, useMailbox } from './orca.js';
|
|
28
29
|
|
|
29
30
|
/**
|
|
30
31
|
* How much of a message travels as itself. Above this it is written to a file
|
|
@@ -365,10 +366,18 @@ function mailOf(bots, who, peek, until) {
|
|
|
365
366
|
// acknowledging is handed over again to the next. From 1.4.246 a read
|
|
366
367
|
// acknowledges the batch it hands over and says so (`deliveryAcknowledged`),
|
|
367
368
|
// and an acknowledgement answers with no batch, so the next is read again
|
|
368
|
-
// (#729; read in the 1.4.246 and 1.4.255 source).
|
|
369
|
+
// (#729; read in the 1.4.246 and 1.4.255 source). So a read that is not a
|
|
370
|
+
// peek is asked again, under the same request id, when Orca does not answer
|
|
371
|
+
// it in time (#735).
|
|
369
372
|
const messages = [];
|
|
370
373
|
const read = { bots, bot: who.bot, session: who.session, mailbox: who.mailbox, read: !peek, messages };
|
|
371
|
-
let found
|
|
374
|
+
let found;
|
|
375
|
+
try {
|
|
376
|
+
found = peek ? readMailbox(who.mailbox, { peek, handle }, timeLeft(until)) : take(who.mailbox, handle, until, timeLeft(until));
|
|
377
|
+
} catch (error) {
|
|
378
|
+
if (peek || error.code !== TIMED_OUT) throw error;
|
|
379
|
+
return { ...read, trouble: `${error.message} Orca may have taken letters as read without the kit seeing them. Other mail waits for the next check.` };
|
|
380
|
+
}
|
|
372
381
|
for (;;) {
|
|
373
382
|
const batch = (found.messages ?? []).map((message) => asMessage(bots, message));
|
|
374
383
|
if (peek || !found.deliveryId || batch.length === 0) {
|
|
@@ -383,7 +392,7 @@ function mailOf(bots, who, peek, until) {
|
|
|
383
392
|
return { ...read, trouble: `${who.bot}/${who.session} has more mail than one check reads in ${HOLD_MS / 1000} seconds, so it stopped there. The rest is still waiting: check again.` };
|
|
384
393
|
}
|
|
385
394
|
try {
|
|
386
|
-
found =
|
|
395
|
+
found = take(who.mailbox, handle, until, wait);
|
|
387
396
|
} catch (error) {
|
|
388
397
|
// A read Orca did not answer about may have taken the next batch all
|
|
389
398
|
// the same, and taken is read: that batch is not handed over again.
|
|
@@ -417,6 +426,31 @@ function mailOf(bots, who, peek, until) {
|
|
|
417
426
|
}
|
|
418
427
|
}
|
|
419
428
|
|
|
429
|
+
/**
|
|
430
|
+
* Take the next batch of `mailbox`, read as `handle`, under a request id of
|
|
431
|
+
* its own, the first call given `options` (#735). Orca keeps the answer of a
|
|
432
|
+
* read under its id: the same read asked again with that id answers the same
|
|
433
|
+
* batch and takes nothing new, waits for the first if it is still running,
|
|
434
|
+
* and runs as new if the first never reached Orca (read in the 1.4.246 and
|
|
435
|
+
* 1.4.255 source). So a read Orca does not answer in time is asked again with
|
|
436
|
+
* the same id while the check's hold lasts, and a batch Orca took as read is
|
|
437
|
+
* shown once. No other call is asked again: a send, a bind or an ack Orca did
|
|
438
|
+
* not answer about is not sent twice (#734).
|
|
439
|
+
*/
|
|
440
|
+
function take(mailbox, handle, until, options) {
|
|
441
|
+
const request = randomUUID();
|
|
442
|
+
for (;;) {
|
|
443
|
+
try {
|
|
444
|
+
return readMailbox(mailbox, { handle, request }, options);
|
|
445
|
+
} catch (error) {
|
|
446
|
+
// A read that went without the id is not asked again (#734).
|
|
447
|
+
if (error.code !== TIMED_OUT || !readsCarryRequest()) throw error;
|
|
448
|
+
options = timeLeft(until, false);
|
|
449
|
+
if (options === undefined) throw error;
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
}
|
|
453
|
+
|
|
420
454
|
/**
|
|
421
455
|
* How long a check holds the session's turn, all its Orca calls together. Well
|
|
422
456
|
* inside the minute `up` waits for the turn before it writes a new tab, so a
|
package/src/orca.js
CHANGED
|
@@ -1021,6 +1021,36 @@ function panePid(ptyId, readMs) {
|
|
|
1021
1021
|
return { pane };
|
|
1022
1022
|
}
|
|
1023
1023
|
|
|
1024
|
+
/**
|
|
1025
|
+
* The ids of the ptys Orca holds a process for, from `diagnostics memory`, as
|
|
1026
|
+
* a set, or undefined when the kit cannot tell (#733). Each session there is a
|
|
1027
|
+
* pty of Orca's own registry, which drops a pty once its process has ended
|
|
1028
|
+
* (read in the 1.4.255 bundle; seen live, the pty went from the answer as its
|
|
1029
|
+
* pane process and shell ended). An Orca that refuses the command, or an
|
|
1030
|
+
* answer in another shape, cannot tell. Nor can an answer with no worktree at
|
|
1031
|
+
* all: Orca answers a snapshot that failed with an empty list (read in the
|
|
1032
|
+
* 1.4.255 bundle), and the kit cannot tell that from a machine with no
|
|
1033
|
+
* terminal.
|
|
1034
|
+
*/
|
|
1035
|
+
export function livePtys() {
|
|
1036
|
+
let worktrees;
|
|
1037
|
+
try {
|
|
1038
|
+
worktrees = orca(['diagnostics', 'memory']).worktrees;
|
|
1039
|
+
} catch {
|
|
1040
|
+
return undefined;
|
|
1041
|
+
}
|
|
1042
|
+
if (!Array.isArray(worktrees) || worktrees.length === 0) return undefined;
|
|
1043
|
+
const ids = new Set();
|
|
1044
|
+
for (const worktree of worktrees) {
|
|
1045
|
+
if (!Array.isArray(worktree?.sessions)) return undefined;
|
|
1046
|
+
for (const session of worktree.sessions) {
|
|
1047
|
+
if (typeof session?.sessionId !== 'string') return undefined;
|
|
1048
|
+
ids.add(session.sessionId);
|
|
1049
|
+
}
|
|
1050
|
+
}
|
|
1051
|
+
return ids;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1024
1054
|
/** The `lsof` and `stty` the kit asks, by their fixed paths, as `ps`. */
|
|
1025
1055
|
export const LSOF = '/usr/sbin/lsof';
|
|
1026
1056
|
const STTY = '/bin/stty';
|
|
@@ -1299,9 +1329,37 @@ export function postMessage({ to, from, subject, body, type = 'status', thread }
|
|
|
1299
1329
|
* and says so with `deliveryAcknowledged: true` (#729). Read as
|
|
1300
1330
|
* the terminal `handle` when one is given, which is how a read works from a tab
|
|
1301
1331
|
* bound to another Run; otherwise as this process's own terminal.
|
|
1332
|
+
*
|
|
1333
|
+
* A read given a `request` id carries it as `--retry-request`: Orca keeps its
|
|
1334
|
+
* answer under that id, and the same read asked again with it answers the
|
|
1335
|
+
* same batch and takes nothing new (#735). An Orca that refuses the flag as
|
|
1336
|
+
* unknown did nothing, and is asked again without it, as for `--full`.
|
|
1337
|
+
*/
|
|
1338
|
+
export function readMailbox(id, { peek = false, handle, request } = {}, options) {
|
|
1339
|
+
const args = ['orchestration', 'check', '--run', id, ...as(handle), ...(peek ? ['--peek'] : [])];
|
|
1340
|
+
if (request !== undefined && retryKnown) {
|
|
1341
|
+
try {
|
|
1342
|
+
return orchestration([...args, '--retry-request', request], options);
|
|
1343
|
+
} catch (error) {
|
|
1344
|
+
if (error.code !== 'invalid_argument' || error.reason !== RETRY_UNKNOWN) throw error;
|
|
1345
|
+
retryKnown = false;
|
|
1346
|
+
}
|
|
1347
|
+
}
|
|
1348
|
+
return orchestration(args, options);
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
/** Orca's own words when its CLI does not know `--retry-request` on `check`: nothing was done. */
|
|
1352
|
+
const RETRY_UNKNOWN = 'Unknown flag --retry-request for command: orchestration check';
|
|
1353
|
+
|
|
1354
|
+
/** Whether this Orca may know `--retry-request` on `check`: false once it has refused it. */
|
|
1355
|
+
let retryKnown = true;
|
|
1356
|
+
|
|
1357
|
+
/**
|
|
1358
|
+
* Whether a read given a request id still carries it. Once Orca has refused
|
|
1359
|
+
* the flag, a read goes without it, and a read with no id is never asked again
|
|
1360
|
+
* after a time-out: it would take another batch, not answer the first (#735).
|
|
1302
1361
|
*/
|
|
1303
|
-
export const
|
|
1304
|
-
orchestration(['orchestration', 'check', '--run', id, ...as(handle), ...(peek ? ['--peek'] : [])], options);
|
|
1362
|
+
export const readsCarryRequest = () => retryKnown;
|
|
1305
1363
|
|
|
1306
1364
|
/**
|
|
1307
1365
|
* The newest `limit` messages in a mailbox, newest first, with every column
|
package/src/pause.js
CHANGED
|
@@ -40,10 +40,14 @@ export async function pauseSessions(bots, { bot, session }) {
|
|
|
40
40
|
*/
|
|
41
41
|
export async function unpauseSessions(bots, { bot, session }) {
|
|
42
42
|
const home = fleetMember(bots, bot, 'unpause', session);
|
|
43
|
-
sessionsOf(readBot(home, bot), session);
|
|
43
|
+
const sessions = sessionsOf(readBot(home, bot), session);
|
|
44
44
|
|
|
45
45
|
const changed = markPaused(bots, bot, session, false);
|
|
46
|
-
|
|
46
|
+
// The sessions this took the pause off are told, once they are resumed, what
|
|
47
|
+
// their closed tab ended (#732). One still paused on its own is not brought
|
|
48
|
+
// up, so it is told nothing; nothing is told when nothing was paused.
|
|
49
|
+
const unpaused = changed ? new Set(sessions.map((one) => one.name)) : undefined;
|
|
50
|
+
return { bot, session, changed, ...(await bringUp(bots, { bot, session, unpaused })) };
|
|
47
51
|
}
|
|
48
52
|
|
|
49
53
|
/**
|
package/src/restart.js
CHANGED
|
@@ -34,7 +34,7 @@ import { setTimeout as pause } from 'node:timers/promises';
|
|
|
34
34
|
import { bookFile, readBook } from './book.js';
|
|
35
35
|
import { botDir, readBot } from './bot.js';
|
|
36
36
|
import { ownCli, shellWord } from './launch.js';
|
|
37
|
-
import { closeTab, closeTerminal, findProject, frontOfTab, orcaCli, tabs } from './orca.js';
|
|
37
|
+
import { closeTab, closeTerminal, findProject, frontOfTab, livePtys, orcaCli, tabs } from './orca.js';
|
|
38
38
|
import { botsNamed, bringUp, prepareBots, sessionsOf } from './up.js';
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -69,7 +69,9 @@ export async function restartSessions(bots, { bot: name, session: onlySession }
|
|
|
69
69
|
|
|
70
70
|
const going = tabsToClose(bots, name, home, sessions.filter((session) => session.paused !== true), { quit: true });
|
|
71
71
|
const closed = await closeTabs(home, going, bots, name, commandLine('restart', bots, name, onlySession));
|
|
72
|
-
|
|
72
|
+
// The sessions whose tabs this closed are the ones the restart ended
|
|
73
|
+
// something of, and the ones told so once they are back (#726).
|
|
74
|
+
return { closed, ...(await bringUp(bots, { bot: name, session: onlySession, restarted: new Set(closed.map((tab) => tab.name)) })) };
|
|
73
75
|
}
|
|
74
76
|
|
|
75
77
|
/**
|
|
@@ -114,9 +116,11 @@ export function tabsToClose(bots, name, home, sessions, { keepless = false, quit
|
|
|
114
116
|
* Close the tabs `tabsToClose` gave, one at a time by their own handles, and
|
|
115
117
|
* wait until Orca's listing agrees they are gone. Returns one entry per tab.
|
|
116
118
|
* `again` is the command that asked, as the user would run it again: what to do
|
|
117
|
-
* once a tab Orca would not close has been closed by hand.
|
|
119
|
+
* once a tab Orca would not close has been closed by hand. With `retiring`, the
|
|
120
|
+
* caller starts no harness in the tabs' place, so a tab Orca still lists after
|
|
121
|
+
* the wait may count as gone once its process is shown to have ended (#733).
|
|
118
122
|
*/
|
|
119
|
-
export async function closeTabs(home, going, bots, name, again) {
|
|
123
|
+
export async function closeTabs(home, going, bots, name, again, { retiring = false } = {}) {
|
|
120
124
|
const closed = [];
|
|
121
125
|
for (const { name: session, tab } of going) {
|
|
122
126
|
try {
|
|
@@ -125,12 +129,12 @@ export async function closeTabs(home, going, bots, name, again) {
|
|
|
125
129
|
// A tab that would not close still holds its session, so nothing is
|
|
126
130
|
// opened after this: two harnesses on one conversation is worse than a
|
|
127
131
|
// restart that did not happen.
|
|
128
|
-
throw new Error(alsoClosed(error.message, closed, bots, name));
|
|
132
|
+
throw new Error(alsoClosed(error.message, closed, bots, name, retiring ? again : undefined));
|
|
129
133
|
}
|
|
130
134
|
closed.push({ bot: name, name: session, tabId: tab.tabId, terminal: tab.handle });
|
|
131
135
|
}
|
|
132
136
|
|
|
133
|
-
await gone(home, closed, bots, name);
|
|
137
|
+
await gone(home, closed, bots, name, retiring ? { again, ptys: new Map(going.map(({ tab }) => [tab.tabId, tab.ptyId])) } : undefined);
|
|
134
138
|
return closed;
|
|
135
139
|
}
|
|
136
140
|
|
|
@@ -185,8 +189,21 @@ const ASK_MS = 100;
|
|
|
185
189
|
* A listing that never catches up is not something the kit can put right, so it
|
|
186
190
|
* stops and says which tab it is waiting on. The conversation is in the book,
|
|
187
191
|
* which is the way back to it.
|
|
192
|
+
*
|
|
193
|
+
* A retire, given as `retire: { again, ptys }`, starts nothing in the tabs'
|
|
194
|
+
* place, and what it must not leave behind is a live harness (#733). Orca
|
|
195
|
+
* 1.4.255 can go on listing a closed tab for minutes, `connected: false`,
|
|
196
|
+
* after its process has ended: a pane its window still holds (tech notes,
|
|
197
|
+
* section 1). That row alone does not prove the process ended, since a pane
|
|
198
|
+
* can say it is not connected while its pty is live, and so Orca's own list
|
|
199
|
+
* of live ptys is read too. Once the wait is over, a tab of the retire's
|
|
200
|
+
* counts as gone when every row Orca lists for it says `connected: false`
|
|
201
|
+
* with the pty the kit closed, and Orca holds no process for that pty.
|
|
202
|
+
* Anything the kit cannot read is "cannot tell", and the retire stops as
|
|
203
|
+
* before, with the retire to run again: the user wants the session gone, not
|
|
204
|
+
* back.
|
|
188
205
|
*/
|
|
189
|
-
async function gone(home, closed, bots, bot) {
|
|
206
|
+
async function gone(home, closed, bots, bot, retire) {
|
|
190
207
|
// Nothing closed is nothing to wait for, and a bot that has never been up has
|
|
191
208
|
// no Orca project to ask: Orca refuses the listing (tech notes, section 1).
|
|
192
209
|
if (closed.length === 0) return;
|
|
@@ -194,18 +211,40 @@ async function gone(home, closed, bots, bot) {
|
|
|
194
211
|
const ids = closed.map((tab) => tab.tabId);
|
|
195
212
|
|
|
196
213
|
for (;;) {
|
|
197
|
-
const
|
|
198
|
-
const
|
|
214
|
+
const listed = tabs(home);
|
|
215
|
+
const there = new Set(listed.map((tab) => tab.tabId));
|
|
216
|
+
let left = ids.filter((id) => there.has(id));
|
|
199
217
|
if (left.length === 0) return;
|
|
200
218
|
|
|
201
219
|
if (Date.now() >= until) {
|
|
202
220
|
const one = left.length === 1;
|
|
203
|
-
|
|
221
|
+
if (retire === undefined) {
|
|
222
|
+
throw new Error(`Orca answered the close for ${one ? 'this tab' : 'these tabs'} and is still listing ${one ? 'it' : 'them'} ${SETTLED_MS / 1000} seconds later: ${left.join(', ')}. Nothing was opened in ${one ? 'its' : 'their'} place, because a tab that is on the way out is not a tab to start a harness in. The conversations are in the book: ${shellWord(ownCli())} up --bots ${shellWord(bots)} --bot ${bot} brings them back once Orca has caught up.`);
|
|
223
|
+
}
|
|
224
|
+
left = stillRunning(listed, left, retire.ptys);
|
|
225
|
+
if (left.length === 0) return;
|
|
226
|
+
const few = left.length === 1;
|
|
227
|
+
throw new Error(`Orca answered the close for ${few ? 'this tab' : 'these tabs'} and is still listing ${few ? 'it' : 'them'} ${SETTLED_MS / 1000} seconds later, and the kit cannot tell that ${few ? 'its harness has' : 'their harnesses have'} stopped: ${left.join(', ')}. The retire stopped there. Once Orca has caught up, run the same retire again: ${retire.again}`);
|
|
204
228
|
}
|
|
205
229
|
await pause(ASK_MS);
|
|
206
230
|
}
|
|
207
231
|
}
|
|
208
232
|
|
|
233
|
+
/**
|
|
234
|
+
* Of the tab ids `left`, the ones Orca's listing `listed` does not show to have
|
|
235
|
+
* ended: a tab ended when each row for it says `connected: false` with the pty
|
|
236
|
+
* `ptys` gives it, and `livePtys` reads and lacks that pty.
|
|
237
|
+
*/
|
|
238
|
+
function stillRunning(listed, left, ptys) {
|
|
239
|
+
const live = livePtys();
|
|
240
|
+
if (live === undefined) return left;
|
|
241
|
+
return left.filter((id) => {
|
|
242
|
+
const pty = ptys.get(id);
|
|
243
|
+
const ended = (tab) => tab.connected === false && tab.ptyId === pty;
|
|
244
|
+
return typeof pty !== 'string' || live.has(pty) || !listed.filter((tab) => tab.tabId === id).every(ended);
|
|
245
|
+
});
|
|
246
|
+
}
|
|
247
|
+
|
|
209
248
|
/**
|
|
210
249
|
* A session the kit will not close a tab on: it is live, and the book does not
|
|
211
250
|
* say which conversation is in it — a Codex hooks file trusted too late is
|
|
@@ -225,10 +264,17 @@ function cannotComeBack(bots, bot, name, was) {
|
|
|
225
264
|
].join(' ');
|
|
226
265
|
}
|
|
227
266
|
|
|
228
|
-
/**
|
|
229
|
-
|
|
267
|
+
/**
|
|
268
|
+
* What a failed close owes whoever is reading: the tabs that went before it.
|
|
269
|
+
* For a retire, given as the command to run again, the way on is that retire,
|
|
270
|
+
* not `up`: the user wants those sessions gone, not back (#733).
|
|
271
|
+
*/
|
|
272
|
+
function alsoClosed(why, closed, bots, bot, retireAgain) {
|
|
230
273
|
if (closed.length === 0) return why;
|
|
231
274
|
|
|
232
275
|
const one = closed.length === 1;
|
|
276
|
+
if (retireAgain !== undefined) {
|
|
277
|
+
return `${why} ${closed.map((tab) => tab.name).join(', ')} ${one ? 'was' : 'were'} closed before it, and nothing more was retired. Once that tab can be closed, run the same retire again: ${retireAgain}`;
|
|
278
|
+
}
|
|
233
279
|
return `${why} ${closed.map((tab) => tab.name).join(', ')} ${one ? 'was' : 'were'} closed before it and nothing was opened in ${one ? 'its' : 'their'} place: ${shellWord(ownCli())} up --bots ${shellWord(bots)} --bot ${bot} brings ${one ? 'it' : 'them'} back with ${one ? 'its' : 'their'} conversation.`;
|
|
234
280
|
}
|
package/src/retire.js
CHANGED
|
@@ -45,9 +45,11 @@ export const retiredDir = (bots) => path.join(bots, 'retired');
|
|
|
45
45
|
* removed, and `processes` when it has a work dir: what `stopProcesses`
|
|
46
46
|
* stopped and left of what it started there (#537). `retiredWith` holds one
|
|
47
47
|
* `{ bot, session, maker, closed }` for each session that went with it,
|
|
48
|
-
* deepest first, with its own `promptsLeft` and `processes`.
|
|
48
|
+
* deepest first, with its own `promptsLeft` and `processes`. `again` is the
|
|
49
|
+
* command the user ran, as they would run it again: what a refusal names, for
|
|
50
|
+
* this session and each that goes with it.
|
|
49
51
|
*/
|
|
50
|
-
export async function retireSession(bots, { bot, session }) {
|
|
52
|
+
export async function retireSession(bots, { bot, session }, again = commandLine('retire', bots, bot, session)) {
|
|
51
53
|
const home = fleetMember(bots, bot, 'retire', session);
|
|
52
54
|
const known = readBot(home, bot);
|
|
53
55
|
const sessions = sessionsOf(known, session);
|
|
@@ -59,11 +61,11 @@ export async function retireSession(bots, { bot, session }) {
|
|
|
59
61
|
let processes;
|
|
60
62
|
try {
|
|
61
63
|
for (const { name } of made) {
|
|
62
|
-
const { retiredWith: theirs, ...gone } = await retireSession(bots, { bot, session: name });
|
|
64
|
+
const { retiredWith: theirs, ...gone } = await retireSession(bots, { bot, session: name }, again);
|
|
63
65
|
retiredWith.push(...theirs, { ...gone, maker: session });
|
|
64
66
|
}
|
|
65
67
|
|
|
66
|
-
closed = await closeTabs(home, tabsToClose(bots, bot, home, sessions, { keepless: true }), bots, bot,
|
|
68
|
+
closed = await closeTabs(home, tabsToClose(bots, bot, home, sessions, { keepless: true }), bots, bot, again, { retiring: true });
|
|
67
69
|
// Only once its tab is closed: its harness starts nothing more.
|
|
68
70
|
const dir = workDirOf(sessions[0], home);
|
|
69
71
|
if (dir !== undefined) processes = await stopProcesses([{ session, dir }]);
|
|
@@ -137,7 +139,7 @@ export async function retireBot(bots, { bot }) {
|
|
|
137
139
|
// taken away with one of them still open. `closeTabs` waits until Orca agrees
|
|
138
140
|
// they are gone.
|
|
139
141
|
const booked = Object.keys(readBook(home).sessions).map((name) => ({ name }));
|
|
140
|
-
const closed = await closeTabs(home, tabsToClose(bots, bot, home, booked, { keepless: true }), bots, bot, commandLine('retire', bots, bot));
|
|
142
|
+
const closed = await closeTabs(home, tabsToClose(bots, bot, home, booked, { keepless: true }), bots, bot, commandLine('retire', bots, bot), { retiring: true });
|
|
141
143
|
// Once their tabs are closed, and before the folder moves (#537).
|
|
142
144
|
const dirs = known.sessions.flatMap((session) => {
|
|
143
145
|
const dir = workDirOf(session, home);
|
package/src/temp.js
CHANGED
|
@@ -367,7 +367,8 @@ const promptLeft = (why, written, left) =>
|
|
|
367
367
|
export async function retireTemp(bots, { tab, name }) {
|
|
368
368
|
const caller = callerIn(bots, tab, 'retire');
|
|
369
369
|
ownTemp(caller, name, 'retire', 'Nothing was retired.');
|
|
370
|
-
|
|
370
|
+
const again = `${shellWord(ownCli())} temp retire --bots ${shellWord(bots)} --name ${name}`;
|
|
371
|
+
return { ...(await retireSession(bots, { bot: caller.bot, session: name }, again)), maker: caller.session };
|
|
371
372
|
}
|
|
372
373
|
|
|
373
374
|
/**
|
package/src/up.js
CHANGED
|
@@ -49,7 +49,7 @@ const SECOND_LOOK_MS = 2000;
|
|
|
49
49
|
* one bot and `session` one of its sessions, for a caller that wants one thing
|
|
50
50
|
* back rather than the fleet.
|
|
51
51
|
*/
|
|
52
|
-
export async function bringUp(bots, { bot: onlyBot, session: onlySession } = {}) {
|
|
52
|
+
export async function bringUp(bots, { bot: onlyBot, session: onlySession, restarted, unpaused } = {}) {
|
|
53
53
|
const named = botsNamed(bots, onlyBot);
|
|
54
54
|
if (onlySession !== undefined && onlyBot === undefined) {
|
|
55
55
|
throw new Error('--session needs --bot: say which bot the session belongs to.');
|
|
@@ -72,7 +72,7 @@ export async function bringUp(bots, { bot: onlyBot, session: onlySession } = {})
|
|
|
72
72
|
for (const session of sessionsOf(bot, onlySession)) {
|
|
73
73
|
if (session.paused === true) paused.push({ bot: bot.name, session: session.name });
|
|
74
74
|
}
|
|
75
|
-
const up = await bringUpBot(bots, home, bot, onlySession);
|
|
75
|
+
const up = await bringUpBot(bots, home, bot, onlySession, { restarted, unpaused });
|
|
76
76
|
report.push(...up.tabs);
|
|
77
77
|
if (up.project !== undefined) projects.push(up.project);
|
|
78
78
|
}
|
|
@@ -230,7 +230,7 @@ export function sessionsOf(bot, onlySession) {
|
|
|
230
230
|
return named;
|
|
231
231
|
}
|
|
232
232
|
|
|
233
|
-
async function bringUpBot(bots, home, bot, onlySession) {
|
|
233
|
+
async function bringUpBot(bots, home, bot, onlySession, { restarted, unpaused } = {}) {
|
|
234
234
|
const name = bot.name;
|
|
235
235
|
const title = displayName(name);
|
|
236
236
|
const sessions = awake(sessionsOf(bot, onlySession));
|
|
@@ -250,7 +250,10 @@ async function bringUpBot(bots, home, bot, onlySession) {
|
|
|
250
250
|
|
|
251
251
|
const live = new Map(tabs(home).map((tab) => [tab.tabId, tab]));
|
|
252
252
|
const report = [];
|
|
253
|
-
|
|
253
|
+
// Which command closed the session's old tab, when one did and the session
|
|
254
|
+
// is to be told what that ended (#726, #732).
|
|
255
|
+
const endedBy = (name) => (restarted?.has(name) ? 'restart' : unpaused?.has(name) ? 'unpause' : undefined);
|
|
256
|
+
for (const session of sessions) report.push(await bringUpSession(bots, home, live, session, bot, title, endedBy(session.name)));
|
|
254
257
|
|
|
255
258
|
// The ops tab, and the whole of what the kit knows about it: Bot Father's
|
|
256
259
|
// project needs one tab that is not a session, for work across the fleet. Any
|
|
@@ -274,7 +277,7 @@ async function bringUpBot(bots, home, bot, onlySession) {
|
|
|
274
277
|
return { tabs: report, project: change === undefined && windowTrouble === undefined ? undefined : project };
|
|
275
278
|
}
|
|
276
279
|
|
|
277
|
-
async function bringUpSession(bots, home, live, session, bot, title) {
|
|
280
|
+
async function bringUpSession(bots, home, live, session, bot, title, endedBy) {
|
|
278
281
|
const harness = harnessOf(session, bot.harness);
|
|
279
282
|
const book = readBook(home);
|
|
280
283
|
const was = book.sessions[session.name];
|
|
@@ -431,11 +434,21 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
431
434
|
? undefined
|
|
432
435
|
: await heldInRecord(home, session.name, harness, launch.prompt, { launched, running: tui.running === true });
|
|
433
436
|
|
|
437
|
+
// A session whose tab `obk restart` closed lost the background shells its
|
|
438
|
+
// harness ran there, and nothing else tells it (#726). So does one that
|
|
439
|
+
// `obk unpause` took off pause: its old tab is closed (#732). So once it is
|
|
440
|
+
// resumed it is typed one line that says so, through the same gate, whatever
|
|
441
|
+
// its harness. For Codex that line is its first turn too, so it takes the
|
|
442
|
+
// place of the list line below.
|
|
443
|
+
const told = endedBy === undefined || launch.resume === undefined ? undefined
|
|
444
|
+
: tui.running === true ? await typeListLine(home, session.name, made.tabId, { line: (endedBy === 'unpause' ? unpauseLine : restartLine)(bots) })
|
|
445
|
+
: { typed: false, why: 'its harness did not come up in the new tab' };
|
|
446
|
+
|
|
434
447
|
// A Codex session resumed here is not among Orca's agents until its first
|
|
435
448
|
// turn: Codex fires its SessionStart then and not at the resume, seen live on
|
|
436
449
|
// 0.157.1 (#226). So it is given one line, through the same gate as a mail
|
|
437
450
|
// notice, which the owner allowed. A fresh start's first turn is its prompt.
|
|
438
|
-
const list = harness === 'codex' && launch.resume !== undefined && tui.running === true
|
|
451
|
+
const list = told === undefined && harness === 'codex' && launch.resume !== undefined && tui.running === true
|
|
439
452
|
? await typeListLine(home, session.name, made.tabId, { line: listLine(bots) })
|
|
440
453
|
: undefined;
|
|
441
454
|
|
|
@@ -456,6 +469,8 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
456
469
|
unclaimed: which.unclaimed,
|
|
457
470
|
listLine: list?.typed,
|
|
458
471
|
listLineTrouble: list?.why,
|
|
472
|
+
restartLine: told?.typed,
|
|
473
|
+
restartLineTrouble: told?.why,
|
|
459
474
|
});
|
|
460
475
|
}
|
|
461
476
|
|
|
@@ -516,6 +531,42 @@ const listLine = (bots) => 'obk: this session was resumed in a new tab, and the
|
|
|
516
531
|
+ ` Read your mail with \`${shellWord(ownCli())} message check --bots ${shellWord(bots)}\`, and do what it asks.`
|
|
517
532
|
+ ' That includes mail you read just before this line. If there is none, reply "ok".';
|
|
518
533
|
|
|
534
|
+
/**
|
|
535
|
+
* The line a session is typed once `obk restart` has resumed it in a new tab,
|
|
536
|
+
* in the architect's ruling (#726). It says only what the kit knows: the old
|
|
537
|
+
* tab's harness ended, and its background shells with it. The kit cannot tell
|
|
538
|
+
* which shells ran, nor whether a scheduled job survived (one was seen to), so
|
|
539
|
+
* the session looks before it makes a job again. The mail part is the list
|
|
540
|
+
* line's as #709 left it, but for its last sentence: "If nothing needs doing"
|
|
541
|
+
* where the list line says "If there is none". This line also asks the session
|
|
542
|
+
* to start its shells again, and a session with no mail must not reply "ok"
|
|
543
|
+
* and stop there (the architect's ruling on PR #731).
|
|
544
|
+
*/
|
|
545
|
+
const restartLine = (bots) => 'obk: obk restart closed this session\'s old tab and resumed the conversation in this new one.'
|
|
546
|
+
+ ' Closing the old tab ended the harness there, and the background shells it ran ended with it.'
|
|
547
|
+
+ whatToDoAfterTheClose(bots);
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* What the restart line and the unpause line both say after their first two
|
|
551
|
+
* sentences: what the kit cannot tell, and what the session does about it.
|
|
552
|
+
*/
|
|
553
|
+
const whatToDoAfterTheClose = (bots) => ' The kit cannot tell which shells ran, or whether a scheduled job of the harness still runs.'
|
|
554
|
+
+ ' Look at the harness\'s scheduled jobs before you make one again, so that only one of each runs.'
|
|
555
|
+
+ ' Start again the shells you still need.'
|
|
556
|
+
+ ` Read your mail with \`${shellWord(ownCli())} message check --bots ${shellWord(bots)}\`, and do what it asks.`
|
|
557
|
+
+ ' That includes mail you read just before this line. If nothing needs doing, reply "ok".';
|
|
558
|
+
|
|
559
|
+
/**
|
|
560
|
+
* The line a session is typed once `obk unpause` has taken it off pause and
|
|
561
|
+
* resumed it in a new tab, in the architect's ruling (#732). Its first two
|
|
562
|
+
* sentences take the place of the restart line's: the kit cannot tell whether
|
|
563
|
+
* the pause closed the old tab or it was closed before, and the words are true
|
|
564
|
+
* either way. The rest is the restart line's, word for word.
|
|
565
|
+
*/
|
|
566
|
+
const unpauseLine = (bots) => 'obk: obk unpause took this session off pause and resumed its conversation in this new tab.'
|
|
567
|
+
+ ' Its old tab is closed, which ended the harness there, and the background shells it ran ended with it.'
|
|
568
|
+
+ whatToDoAfterTheClose(bots);
|
|
569
|
+
|
|
519
570
|
/**
|
|
520
571
|
* How long a resumed Codex tab is given to be one the kit may type into: live,
|
|
521
572
|
* Orca named the agent 7 to 11 s after the restart (#226), and the gate waits
|
|
@@ -969,7 +1020,7 @@ export const promptPath = (bots, bot, session) =>
|
|
|
969
1020
|
export const oldPromptPath = (bots, bot, session) =>
|
|
970
1021
|
path.join(`${bots}.prompts`, `${encodeURIComponent(bot)}.${encodeURIComponent(session)}.txt`);
|
|
971
1022
|
|
|
972
|
-
function entry(tab, { bot, name, created, running = false, blockedReason, promptReceived, promptFile, resumed, noConversation, harnessChanged, ranOn, unclaimed, noMailbox = false, listLine, listLineTrouble, notLaunched }) {
|
|
1023
|
+
function entry(tab, { bot, name, created, running = false, blockedReason, promptReceived, promptFile, resumed, noConversation, harnessChanged, ranOn, unclaimed, noMailbox = false, listLine, listLineTrouble, restartLine, restartLineTrouble, notLaunched }) {
|
|
973
1024
|
const made = { bot, name, title: tab.title, tabId: tab.tabId, terminal: tab.handle, created, harnessStarted: running };
|
|
974
1025
|
// A tab whose shell was not ready for the launch line, and why: nothing was
|
|
975
1026
|
// typed into it (#498).
|
|
@@ -981,6 +1032,8 @@ function entry(tab, { bot, name, created, running = false, blockedReason, prompt
|
|
|
981
1032
|
// and why not when it was not (#226).
|
|
982
1033
|
if (listLine !== undefined) made.listLine = listLine;
|
|
983
1034
|
if (listLineTrouble !== undefined) made.listLineTrouble = listLineTrouble;
|
|
1035
|
+
if (restartLine !== undefined) made.restartLine = restartLine;
|
|
1036
|
+
if (restartLineTrouble !== undefined) made.restartLineTrouble = restartLineTrouble;
|
|
984
1037
|
// The id the book held that had no conversation behind it, now in the history.
|
|
985
1038
|
if (noConversation !== undefined) made.noConversation = noConversation;
|
|
986
1039
|
// The id the book held that ran on another harness, the one this session
|