@assuredloop/orca-bot-kit 0.36.0 → 0.37.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 +12 -0
- package/package.json +1 -1
- package/skills/obk-bot-building/SKILL.md +16 -0
- package/skills/obk-finops/SKILL.md +3 -0
- package/src/cli.js +16 -1
- package/src/message.js +67 -11
- package/src/orca.js +107 -12
- package/src/restart.js +58 -12
- package/src/retire.js +7 -5
- package/src/temp.js +2 -1
- package/src/up.js +39 -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:
|
package/package.json
CHANGED
|
@@ -127,6 +127,22 @@ the bots folder moves. `obk session add` and `obk session change` say so when a
|
|
|
127
127
|
work dir leads out of the bot home; take that back to the user rather than
|
|
128
128
|
past them.
|
|
129
129
|
|
|
130
|
+
## How its sessions choose a subagent's model
|
|
131
|
+
|
|
132
|
+
A session runs on one model of its own, but the model of each subagent it starts
|
|
133
|
+
can be chosen task by task. A bot that should choose carries its way of working
|
|
134
|
+
for this as a rule unit of its own. The bot can decide that way of working, or
|
|
135
|
+
the user can. Write it in their words as a unit in `rules/` of the bots folder,
|
|
136
|
+
name it in that bot's `rules:` list, and run `obk rules build`: every session
|
|
137
|
+
of the bot then reads it in its `AGENTS.md`. Another bot can carry another
|
|
138
|
+
rule, or none. A bot with none works as before, and each subagent runs on what
|
|
139
|
+
the harness gives it.
|
|
140
|
+
|
|
141
|
+
The rule acts where the harness lets a session choose a model for a subagent,
|
|
142
|
+
as Claude Code's Agent tool does with its `model`. Where the harness does not,
|
|
143
|
+
nothing changes. The kit names no model and no table from a task to a model:
|
|
144
|
+
any model in the rule is the bot's or the user's choice.
|
|
145
|
+
|
|
130
146
|
## Offer a line-up rather than a blank page
|
|
131
147
|
|
|
132
148
|
Someone making their first bots does not know what a good set looks like, so
|
|
@@ -119,6 +119,9 @@ A usual shape: high effort for the session that orchestrates, medium for the
|
|
|
119
119
|
ones doing the work, and the top of the range only for deep read-only
|
|
120
120
|
analysis.
|
|
121
121
|
|
|
122
|
+
A bot can also carry its own rule on the model each subagent gets: see "How
|
|
123
|
+
its sessions choose a subagent's model" in the obk-bot-building skill.
|
|
124
|
+
|
|
122
125
|
## When a model is not up to the work
|
|
123
126
|
|
|
124
127
|
The signs are in the work, not in the answers. The same instruction given
|
package/src/cli.js
CHANGED
|
@@ -1177,6 +1177,10 @@ const commands = {
|
|
|
1177
1177
|
answer,
|
|
1178
1178
|
lines: [
|
|
1179
1179
|
`${'sent'.padEnd(9)} ${where.padEnd(24)} ${answer.subject}`,
|
|
1180
|
+
// Orca took it, and its answer named no letter (#730).
|
|
1181
|
+
...(answer.id === undefined
|
|
1182
|
+
? [' Orca queued it, and its answer gave no message id for it.']
|
|
1183
|
+
: []),
|
|
1180
1184
|
...(answer.file === undefined
|
|
1181
1185
|
? []
|
|
1182
1186
|
: [` it was too long to travel as itself, so it went as a file: ${answer.file}`]),
|
|
@@ -1984,6 +1988,7 @@ function harnessLines(tab, bots) {
|
|
|
1984
1988
|
` Look at it: ${lookAt(tab.terminal)}`,
|
|
1985
1989
|
...ANSWER_IT,
|
|
1986
1990
|
...promptLines(tab),
|
|
1991
|
+
...restartLines(tab),
|
|
1987
1992
|
];
|
|
1988
1993
|
}
|
|
1989
1994
|
|
|
@@ -2005,7 +2010,7 @@ function harnessLines(tab, bots) {
|
|
|
2005
2010
|
...ANSWER_IT,
|
|
2006
2011
|
];
|
|
2007
2012
|
|
|
2008
|
-
lines.push(...promptLines(tab), ...listLines(tab));
|
|
2013
|
+
lines.push(...promptLines(tab), ...listLines(tab), ...restartLines(tab));
|
|
2009
2014
|
if (tab.unclaimed !== undefined) lines.push(...unclaimedLines(tab, bots));
|
|
2010
2015
|
return lines;
|
|
2011
2016
|
}
|
|
@@ -2020,6 +2025,16 @@ function listLines(tab) {
|
|
|
2020
2025
|
return [];
|
|
2021
2026
|
}
|
|
2022
2027
|
|
|
2028
|
+
/**
|
|
2029
|
+
* Whether a session `obk restart` resumed was typed the line that tells it what
|
|
2030
|
+
* the restart ended (#726). When it was not, the caller is the one left to tell it.
|
|
2031
|
+
*/
|
|
2032
|
+
function restartLines(tab) {
|
|
2033
|
+
if (tab.restartLine === true) return [' it was typed one line that tells it what the restart ended: its background shells, and to look at its scheduled jobs and its mail.'];
|
|
2034
|
+
if (tab.restartLine === false) return [` it was not told what the restart ended (${tab.restartLineTrouble}): tell it yourself that its background shells ended, and to look at its scheduled jobs and its mail.`];
|
|
2035
|
+
return [];
|
|
2036
|
+
}
|
|
2037
|
+
|
|
2023
2038
|
/**
|
|
2024
2039
|
* Whether the session got its start prompt, as its own record has it (#274):
|
|
2025
2040
|
* 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
|
|
@@ -203,19 +204,21 @@ export async function sendMessage(bots, { to: target, from: sender, tab, subject
|
|
|
203
204
|
const shown = await loaded(to);
|
|
204
205
|
// The Escape goes first, so the turn has ended when the letter is in.
|
|
205
206
|
const interrupted = interrupt ? interruptBusy(to) : {};
|
|
206
|
-
const
|
|
207
|
+
const posted = postMessage({
|
|
207
208
|
to: road.address,
|
|
208
209
|
from: `run:${from.mailbox}`,
|
|
209
210
|
subject,
|
|
210
211
|
body: written.body,
|
|
211
212
|
thread,
|
|
212
213
|
});
|
|
214
|
+
// Orca took it: sent, whatever else its answer says or leaves out (#730).
|
|
215
|
+
const message = posted?.message;
|
|
213
216
|
|
|
214
217
|
return {
|
|
215
218
|
...answer,
|
|
216
219
|
sent: true,
|
|
217
|
-
id: message
|
|
218
|
-
thread: message
|
|
220
|
+
id: message?.id ?? posted?.messageId,
|
|
221
|
+
thread: message?.thread_id ?? thread,
|
|
219
222
|
file: written.file,
|
|
220
223
|
...interrupted,
|
|
221
224
|
...shown,
|
|
@@ -356,15 +359,25 @@ function mailOf(bots, who, peek, until) {
|
|
|
356
359
|
trouble: `${who.bot}/${who.session} is not up, and its mailbox is bound to no tab, so there is nothing to read it as. Its mail waits until its tab is back: \`up\` brings it back, or \`unpause\` when it is paused.`,
|
|
357
360
|
};
|
|
358
361
|
}
|
|
359
|
-
// Orca hands mail over a batch at a time
|
|
360
|
-
//
|
|
361
|
-
//
|
|
362
|
-
//
|
|
363
|
-
//
|
|
364
|
-
//
|
|
362
|
+
// Orca hands mail over a batch at a time, so a read takes every batch in
|
|
363
|
+
// turn, or newer mail waits behind an older one (#299). Up to 1.4.245 it
|
|
364
|
+
// replays the oldest until it is acknowledged, and the acknowledgement
|
|
365
|
+
// answers with the next batch: a batch this check stops short of
|
|
366
|
+
// acknowledging is handed over again to the next. From 1.4.246 a read
|
|
367
|
+
// acknowledges the batch it hands over and says so (`deliveryAcknowledged`),
|
|
368
|
+
// and an acknowledgement answers with no batch, so the next is read again
|
|
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).
|
|
365
372
|
const messages = [];
|
|
366
373
|
const read = { bots, bot: who.bot, session: who.session, mailbox: who.mailbox, read: !peek, messages };
|
|
367
|
-
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
|
+
}
|
|
368
381
|
for (;;) {
|
|
369
382
|
const batch = (found.messages ?? []).map((message) => asMessage(bots, message));
|
|
370
383
|
if (peek || !found.deliveryId || batch.length === 0) {
|
|
@@ -372,6 +385,24 @@ function mailOf(bots, who, peek, until) {
|
|
|
372
385
|
return read;
|
|
373
386
|
}
|
|
374
387
|
const wait = timeLeft(until, false);
|
|
388
|
+
if (found.deliveryAcknowledged === true) {
|
|
389
|
+
// Read already, so shown whatever comes after.
|
|
390
|
+
messages.push(...batch);
|
|
391
|
+
if (wait === undefined) {
|
|
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.` };
|
|
393
|
+
}
|
|
394
|
+
try {
|
|
395
|
+
found = take(who.mailbox, handle, until, wait);
|
|
396
|
+
} catch (error) {
|
|
397
|
+
// A read Orca did not answer about may have taken the next batch all
|
|
398
|
+
// the same, and taken is read: that batch is not handed over again.
|
|
399
|
+
if (error.code === TIMED_OUT) {
|
|
400
|
+
return { ...read, trouble: `${error.message} Orca may have taken the next letters as read without the kit seeing them. The mail shown above is read.` };
|
|
401
|
+
}
|
|
402
|
+
return { ...read, trouble: `${error.message} The mail shown above is read, and the rest waits for the next check.` };
|
|
403
|
+
}
|
|
404
|
+
continue;
|
|
405
|
+
}
|
|
375
406
|
if (wait === undefined) {
|
|
376
407
|
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.` };
|
|
377
408
|
}
|
|
@@ -395,6 +426,31 @@ function mailOf(bots, who, peek, until) {
|
|
|
395
426
|
}
|
|
396
427
|
}
|
|
397
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
|
+
|
|
398
454
|
/**
|
|
399
455
|
* How long a check holds the session's turn, all its Orca calls together. Well
|
|
400
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';
|
|
@@ -1203,6 +1233,33 @@ export const pressEscape = (handle) =>
|
|
|
1203
1233
|
/** Where Orca names the terminal a process runs in, in every pane it opens. */
|
|
1204
1234
|
export const TERMINAL_ENV = 'ORCA_TERMINAL_HANDLE';
|
|
1205
1235
|
|
|
1236
|
+
/**
|
|
1237
|
+
* One `orca orchestration` command, answered in full. From 1.4.246 Orca prints
|
|
1238
|
+
* a short answer for each of these unless it is given `--full`, and the short
|
|
1239
|
+
* one leaves out what the kit reads: a Run's `id` and `coordinator_handle`, a
|
|
1240
|
+
* letter's `from_handle`, `created_at`, `thread_id`, `read` and `delivered_at`,
|
|
1241
|
+
* and a sent letter's `message` (#729, #730; read in the 1.4.246 and 1.4.255
|
|
1242
|
+
* source, and seen live on 1.4.255). Orca 1.4.245 and older do not know the
|
|
1243
|
+
* flag on any of them but `inbox`, and refuse it before they do anything, as
|
|
1244
|
+
* `deleteProject` finds with `--force` (#528). Only that refusal sends the
|
|
1245
|
+
* command again without it: one Orca took, or did not answer, is never sent a
|
|
1246
|
+
* second time. Once refused, the rest of this run sends no `--full`.
|
|
1247
|
+
*/
|
|
1248
|
+
function orchestration(args, options) {
|
|
1249
|
+
if (fullKnown) {
|
|
1250
|
+
try {
|
|
1251
|
+
return orca([...args, '--full'], options);
|
|
1252
|
+
} catch (error) {
|
|
1253
|
+
if (error.code !== 'invalid_argument' || !error.reason?.startsWith('Unknown flag --full for command: ')) throw error;
|
|
1254
|
+
fullKnown = false;
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
return orca(args, options);
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
/** Whether this Orca may know `--full` on its orchestration commands: false once it has refused it. */
|
|
1261
|
+
let fullKnown = true;
|
|
1262
|
+
|
|
1206
1263
|
/**
|
|
1207
1264
|
* A mailbox of one session's own: an Orca Run, which is a name and an inbox and
|
|
1208
1265
|
* nothing else — it schedules nothing and runs nobody. The objective is what a
|
|
@@ -1215,7 +1272,7 @@ export const TERMINAL_ENV = 'ORCA_TERMINAL_HANDLE';
|
|
|
1215
1272
|
* tab and names no `--from` (tech notes, section 1; #317).
|
|
1216
1273
|
*/
|
|
1217
1274
|
export const makeMailbox = (objective, options) =>
|
|
1218
|
-
|
|
1275
|
+
orchestration(['orchestration', 'run-create', '--objective', `obk ${objective}`], options).run.id;
|
|
1219
1276
|
|
|
1220
1277
|
/**
|
|
1221
1278
|
* Bind a mailbox to the terminal `handle`, which makes it the Run's coordinator
|
|
@@ -1226,7 +1283,7 @@ export const makeMailbox = (objective, options) =>
|
|
|
1226
1283
|
* tab may name no other (#317).
|
|
1227
1284
|
*/
|
|
1228
1285
|
export const useMailbox = (id, handle, options) =>
|
|
1229
|
-
|
|
1286
|
+
orchestration(['orchestration', 'run-use', '--id', id, ...(handle === undefined ? [] : ['--from', handle])], options).run;
|
|
1230
1287
|
|
|
1231
1288
|
/**
|
|
1232
1289
|
* The terminal a mailbox is bound to, or undefined when it has none. A closed
|
|
@@ -1234,7 +1291,7 @@ export const useMailbox = (id, handle, options) =>
|
|
|
1234
1291
|
* nothing, only `run-use` does (tech notes, section 1).
|
|
1235
1292
|
*/
|
|
1236
1293
|
export const coordinatorOf = (id, options) =>
|
|
1237
|
-
|
|
1294
|
+
orchestration(['orchestration', 'run-show', '--id', id], options).run.coordinator_handle ?? undefined;
|
|
1238
1295
|
|
|
1239
1296
|
/**
|
|
1240
1297
|
* Whether this Orca has no Run of that id at all, as when the book was written
|
|
@@ -1245,29 +1302,64 @@ export const coordinatorOf = (id, options) =>
|
|
|
1245
1302
|
*/
|
|
1246
1303
|
export function runMissing(id, options) {
|
|
1247
1304
|
try {
|
|
1248
|
-
|
|
1305
|
+
orchestration(['orchestration', 'run-show', '--id', id], options);
|
|
1249
1306
|
return false;
|
|
1250
1307
|
} catch (error) {
|
|
1251
1308
|
return error.code === 'run_not_found';
|
|
1252
1309
|
}
|
|
1253
1310
|
}
|
|
1254
1311
|
|
|
1255
|
-
/**
|
|
1312
|
+
/**
|
|
1313
|
+
* Queue one message. `to` and `from` are mailboxes, written `run:<id>`. Orca's
|
|
1314
|
+
* whole answer comes back: the letter it queued is its `message`, where Orca
|
|
1315
|
+
* says so. Once Orca has taken it, an answer the kit cannot read is no reason
|
|
1316
|
+
* to say it was not sent (#730).
|
|
1317
|
+
*/
|
|
1256
1318
|
export function postMessage({ to, from, subject, body, type = 'status', thread }) {
|
|
1257
1319
|
const args = ['orchestration', 'send', '--to', to, '--subject', subject, '--body', body, '--type', type];
|
|
1258
1320
|
if (from !== undefined) args.push('--from', from);
|
|
1259
1321
|
if (thread !== undefined) args.push('--thread-id', thread);
|
|
1260
|
-
return
|
|
1322
|
+
return orchestration(args);
|
|
1261
1323
|
}
|
|
1262
1324
|
|
|
1263
1325
|
/**
|
|
1264
1326
|
* What is waiting in a mailbox: the oldest batch that has not been
|
|
1265
|
-
* acknowledged, or, peeking, whatever is unread without touching it.
|
|
1327
|
+
* acknowledged, or, peeking, whatever is unread without touching it. From
|
|
1328
|
+
* Orca 1.4.246 a read that is not a peek acknowledges the batch it hands over,
|
|
1329
|
+
* and says so with `deliveryAcknowledged: true` (#729). Read as
|
|
1266
1330
|
* the terminal `handle` when one is given, which is how a read works from a tab
|
|
1267
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`.
|
|
1268
1337
|
*/
|
|
1269
|
-
export
|
|
1270
|
-
|
|
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).
|
|
1361
|
+
*/
|
|
1362
|
+
export const readsCarryRequest = () => retryKnown;
|
|
1271
1363
|
|
|
1272
1364
|
/**
|
|
1273
1365
|
* The newest `limit` messages in a mailbox, newest first, with every column
|
|
@@ -1278,11 +1370,14 @@ export const readMailbox = (id, { peek = false, handle } = {}, options) =>
|
|
|
1278
1370
|
* cap in Orca, so the answer is taken whatever its size.
|
|
1279
1371
|
*/
|
|
1280
1372
|
export const mailIn = (id, limit) =>
|
|
1281
|
-
|
|
1373
|
+
orchestration(['orchestration', 'inbox', '--terminal', `run:${id}`, '--limit', String(limit)], { whole: true }).messages ?? [];
|
|
1282
1374
|
|
|
1283
|
-
/**
|
|
1375
|
+
/**
|
|
1376
|
+
* Say a batch has been read. Up to Orca 1.4.245 Orca answers with the batch
|
|
1377
|
+
* after it; from 1.4.246 with no batch, since its read acknowledged it (#729).
|
|
1378
|
+
*/
|
|
1284
1379
|
export const ackMailbox = (id, delivery, handle, options) =>
|
|
1285
|
-
|
|
1380
|
+
orchestration(['orchestration', 'check', '--run', id, ...as(handle), '--ack', delivery], options);
|
|
1286
1381
|
|
|
1287
1382
|
/** The reader of a `check`: the terminal named, or this process's own. */
|
|
1288
1383
|
const as = (handle) => (handle === undefined ? [] : ['--terminal', handle]);
|
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 } = {}) {
|
|
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);
|
|
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) {
|
|
234
234
|
const name = bot.name;
|
|
235
235
|
const title = displayName(name);
|
|
236
236
|
const sessions = awake(sessionsOf(bot, onlySession));
|
|
@@ -250,7 +250,7 @@ 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
|
-
for (const session of sessions) report.push(await bringUpSession(bots, home, live, session, bot, title));
|
|
253
|
+
for (const session of sessions) report.push(await bringUpSession(bots, home, live, session, bot, title, restarted?.has(session.name) === true));
|
|
254
254
|
|
|
255
255
|
// The ops tab, and the whole of what the kit knows about it: Bot Father's
|
|
256
256
|
// project needs one tab that is not a session, for work across the fleet. Any
|
|
@@ -274,7 +274,7 @@ async function bringUpBot(bots, home, bot, onlySession) {
|
|
|
274
274
|
return { tabs: report, project: change === undefined && windowTrouble === undefined ? undefined : project };
|
|
275
275
|
}
|
|
276
276
|
|
|
277
|
-
async function bringUpSession(bots, home, live, session, bot, title) {
|
|
277
|
+
async function bringUpSession(bots, home, live, session, bot, title, restarted) {
|
|
278
278
|
const harness = harnessOf(session, bot.harness);
|
|
279
279
|
const book = readBook(home);
|
|
280
280
|
const was = book.sessions[session.name];
|
|
@@ -431,11 +431,20 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
431
431
|
? undefined
|
|
432
432
|
: await heldInRecord(home, session.name, harness, launch.prompt, { launched, running: tui.running === true });
|
|
433
433
|
|
|
434
|
+
// A session whose tab `obk restart` closed lost the background shells its
|
|
435
|
+
// harness ran there, and nothing else tells it (#726). So once it is resumed
|
|
436
|
+
// it is typed one line that says so, through the same gate, whatever its
|
|
437
|
+
// harness. For Codex that line is its first turn too, so it takes the place
|
|
438
|
+
// of the list line below.
|
|
439
|
+
const told = !restarted || launch.resume === undefined ? undefined
|
|
440
|
+
: tui.running === true ? await typeListLine(home, session.name, made.tabId, { line: restartLine(bots) })
|
|
441
|
+
: { typed: false, why: 'its harness did not come up in the new tab' };
|
|
442
|
+
|
|
434
443
|
// A Codex session resumed here is not among Orca's agents until its first
|
|
435
444
|
// turn: Codex fires its SessionStart then and not at the resume, seen live on
|
|
436
445
|
// 0.157.1 (#226). So it is given one line, through the same gate as a mail
|
|
437
446
|
// 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
|
|
447
|
+
const list = told === undefined && harness === 'codex' && launch.resume !== undefined && tui.running === true
|
|
439
448
|
? await typeListLine(home, session.name, made.tabId, { line: listLine(bots) })
|
|
440
449
|
: undefined;
|
|
441
450
|
|
|
@@ -456,6 +465,8 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
456
465
|
unclaimed: which.unclaimed,
|
|
457
466
|
listLine: list?.typed,
|
|
458
467
|
listLineTrouble: list?.why,
|
|
468
|
+
restartLine: told?.typed,
|
|
469
|
+
restartLineTrouble: told?.why,
|
|
459
470
|
});
|
|
460
471
|
}
|
|
461
472
|
|
|
@@ -516,6 +527,25 @@ const listLine = (bots) => 'obk: this session was resumed in a new tab, and the
|
|
|
516
527
|
+ ` Read your mail with \`${shellWord(ownCli())} message check --bots ${shellWord(bots)}\`, and do what it asks.`
|
|
517
528
|
+ ' That includes mail you read just before this line. If there is none, reply "ok".';
|
|
518
529
|
|
|
530
|
+
/**
|
|
531
|
+
* The line a session is typed once `obk restart` has resumed it in a new tab,
|
|
532
|
+
* in the architect's ruling (#726). It says only what the kit knows: the old
|
|
533
|
+
* tab's harness ended, and its background shells with it. The kit cannot tell
|
|
534
|
+
* which shells ran, nor whether a scheduled job survived (one was seen to), so
|
|
535
|
+
* the session looks before it makes a job again. The mail part is the list
|
|
536
|
+
* line's as #709 left it, but for its last sentence: "If nothing needs doing"
|
|
537
|
+
* where the list line says "If there is none". This line also asks the session
|
|
538
|
+
* to start its shells again, and a session with no mail must not reply "ok"
|
|
539
|
+
* and stop there (the architect's ruling on PR #731).
|
|
540
|
+
*/
|
|
541
|
+
const restartLine = (bots) => 'obk: obk restart closed this session\'s old tab and resumed the conversation in this new one.'
|
|
542
|
+
+ ' Closing the old tab ended the harness there, and the background shells it ran ended with it.'
|
|
543
|
+
+ ' The kit cannot tell which shells ran, or whether a scheduled job of the harness still runs.'
|
|
544
|
+
+ ' Look at the harness\'s scheduled jobs before you make one again, so that only one of each runs.'
|
|
545
|
+
+ ' Start again the shells you still need.'
|
|
546
|
+
+ ` Read your mail with \`${shellWord(ownCli())} message check --bots ${shellWord(bots)}\`, and do what it asks.`
|
|
547
|
+
+ ' That includes mail you read just before this line. If nothing needs doing, reply "ok".';
|
|
548
|
+
|
|
519
549
|
/**
|
|
520
550
|
* How long a resumed Codex tab is given to be one the kit may type into: live,
|
|
521
551
|
* Orca named the agent 7 to 11 s after the restart (#226), and the gate waits
|
|
@@ -969,7 +999,7 @@ export const promptPath = (bots, bot, session) =>
|
|
|
969
999
|
export const oldPromptPath = (bots, bot, session) =>
|
|
970
1000
|
path.join(`${bots}.prompts`, `${encodeURIComponent(bot)}.${encodeURIComponent(session)}.txt`);
|
|
971
1001
|
|
|
972
|
-
function entry(tab, { bot, name, created, running = false, blockedReason, promptReceived, promptFile, resumed, noConversation, harnessChanged, ranOn, unclaimed, noMailbox = false, listLine, listLineTrouble, notLaunched }) {
|
|
1002
|
+
function entry(tab, { bot, name, created, running = false, blockedReason, promptReceived, promptFile, resumed, noConversation, harnessChanged, ranOn, unclaimed, noMailbox = false, listLine, listLineTrouble, restartLine, restartLineTrouble, notLaunched }) {
|
|
973
1003
|
const made = { bot, name, title: tab.title, tabId: tab.tabId, terminal: tab.handle, created, harnessStarted: running };
|
|
974
1004
|
// A tab whose shell was not ready for the launch line, and why: nothing was
|
|
975
1005
|
// typed into it (#498).
|
|
@@ -981,6 +1011,8 @@ function entry(tab, { bot, name, created, running = false, blockedReason, prompt
|
|
|
981
1011
|
// and why not when it was not (#226).
|
|
982
1012
|
if (listLine !== undefined) made.listLine = listLine;
|
|
983
1013
|
if (listLineTrouble !== undefined) made.listLineTrouble = listLineTrouble;
|
|
1014
|
+
if (restartLine !== undefined) made.restartLine = restartLine;
|
|
1015
|
+
if (restartLineTrouble !== undefined) made.restartLineTrouble = restartLineTrouble;
|
|
984
1016
|
// The id the book held that had no conversation behind it, now in the history.
|
|
985
1017
|
if (noConversation !== undefined) made.noConversation = noConversation;
|
|
986
1018
|
// The id the book held that ran on another harness, the one this session
|