@assuredloop/orca-bot-kit 0.25.0 → 0.25.1
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/SETUP.md +10 -5
- package/package.json +1 -1
- package/src/cli.js +17 -2
- package/src/orca.js +120 -20
- package/src/up.js +60 -2
package/SETUP.md
CHANGED
|
@@ -147,14 +147,19 @@ their own edits included.
|
|
|
147
147
|
## 5. Answer what the tabs ask
|
|
148
148
|
|
|
149
149
|
**How the tab got into this state**, because it decides what you do about it. A
|
|
150
|
-
tab the kit has just made has no harness in it yet
|
|
151
|
-
the
|
|
152
|
-
whether a harness came up.
|
|
153
|
-
|
|
154
|
-
different answers.
|
|
150
|
+
tab the kit has just made has no harness in it yet. The kit waits up to 15 s for
|
|
151
|
+
the tab's shell to come to a ready prompt, types the launch line, and only then
|
|
152
|
+
asks whether a harness came up. A shell that asks a question of its own as it
|
|
153
|
+
starts gets nothing typed. So different things can be on the screen, and they
|
|
154
|
+
need different answers.
|
|
155
155
|
|
|
156
156
|
`obk init` tells you which, for every tab it opened:
|
|
157
157
|
|
|
158
|
+
- *not launched: the shell is asking: …* — the shell asked a question as it
|
|
159
|
+
started, and the kit typed nothing. Answer the shell (the oh-my-zsh question
|
|
160
|
+
in the table below is the usual one), then run the `obk restart` command the
|
|
161
|
+
output gives for that session. It closes that tab and launches the session in
|
|
162
|
+
a new one. The command exits 1 while a session is not launched.
|
|
158
163
|
- *the harness was typed in and is running. The kit cannot see whether a screen
|
|
159
164
|
in it is waiting for an answer* — look at it: Orca does not flag every first-run
|
|
160
165
|
screen, so it may still be asking something of its own. That is the table below.
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -472,7 +472,9 @@ async function run(argv) {
|
|
|
472
472
|
const { answer, lines, code = 0 } = await commands[command](bots, values);
|
|
473
473
|
|
|
474
474
|
process.stdout.write(values.json ? `${JSON.stringify(answer, null, 2)}\n` : `${lines.join('\n')}\n`);
|
|
475
|
-
|
|
475
|
+
// A session whose tab was opened and not launched is a command that stopped
|
|
476
|
+
// short, whichever command opened it (#498).
|
|
477
|
+
return answer?.tabs?.some((tab) => tab.launched === false) ? 1 : code;
|
|
476
478
|
}
|
|
477
479
|
|
|
478
480
|
/**
|
|
@@ -756,7 +758,9 @@ const commands = {
|
|
|
756
758
|
lines: [
|
|
757
759
|
...(made.role === undefined ? [] : [`role ${made.role.name}:${made.role.option}${made.role.for === undefined ? '' : `, for ${made.role.for}`}. Its options: ${optionsLine(made.role.options)}`]),
|
|
758
760
|
...Object.entries(made.chosen).map(([setting, one]) => `${setting.padEnd(9)} ${one.value === undefined ? `the ${one.from}` : `${one.value}, from ${one.from === 'flag' ? `--${setting}` : `the ${one.from}`}`}`),
|
|
759
|
-
...tabLines(answer,
|
|
761
|
+
...tabLines(answer, tabs.some((one) => one.launched === false)
|
|
762
|
+
? `${made.bot}/${made.session}, a temporary session of ${made.maker}'s, is in bot.yaml and the book, and was not launched. Restart it once its tab is ready, or retire it: ${retire}`
|
|
763
|
+
: `Made ${made.bot}/${made.session}, a temporary session of ${made.maker}'s, working in work/${made.session}. Retire it when its work is done: ${retire}`),
|
|
760
764
|
],
|
|
761
765
|
};
|
|
762
766
|
},
|
|
@@ -1771,6 +1775,17 @@ function tabLines({ bots, created, completed, rules, skills, permissions = [], t
|
|
|
1771
1775
|
function harnessLines(tab, bots) {
|
|
1772
1776
|
if (!tab.created || tab.name === null) return [];
|
|
1773
1777
|
|
|
1778
|
+
// Nothing was typed into it, so there is nothing more to say about a harness
|
|
1779
|
+
// (#498).
|
|
1780
|
+
if (tab.launched === false) {
|
|
1781
|
+
return [
|
|
1782
|
+
` not launched: ${tab.notLaunched}. Nothing was typed into it.`,
|
|
1783
|
+
` Look at it: ${lookAt(tab.terminal)}`,
|
|
1784
|
+
' Answer it in the tab, then start the session again in a new tab:',
|
|
1785
|
+
` ${shellWord(ownCli())} restart --bots ${shellWord(bots)} --bot ${tab.bot} --session ${tab.name}`,
|
|
1786
|
+
];
|
|
1787
|
+
}
|
|
1788
|
+
|
|
1774
1789
|
// What the line that was typed in asked for, and where the kit got it: the
|
|
1775
1790
|
// session the book holds, one the harness itself still had on record, or a new
|
|
1776
1791
|
// one — and if a new one, whether anything was known about an older one.
|
package/src/orca.js
CHANGED
|
@@ -231,7 +231,7 @@ export const tellWindow = (projectId) => askRuntime('project.update', { projectI
|
|
|
231
231
|
* Orca's published interface, so anything that goes wrong is a quiet
|
|
232
232
|
* undefined: it never throws and is never tried twice.
|
|
233
233
|
*/
|
|
234
|
-
function askRuntime(method, params) {
|
|
234
|
+
function askRuntime(method, params, killMs = CLIENT_KILL_MS) {
|
|
235
235
|
try {
|
|
236
236
|
// The CLI sits at <Orca.app>/Contents/Resources/bin/orca, often reached
|
|
237
237
|
// through a link.
|
|
@@ -244,8 +244,8 @@ function askRuntime(method, params) {
|
|
|
244
244
|
|
|
245
245
|
const asked = spawnSync(
|
|
246
246
|
path.join(contents, 'MacOS', 'Orca'),
|
|
247
|
-
[RUNTIME_SCRIPT, client, method, JSON.stringify(params), String(CLIENT_WAIT_MS)],
|
|
248
|
-
{ env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: CLIENT_KILL_MS },
|
|
247
|
+
[RUNTIME_SCRIPT, client, method, JSON.stringify(params), String(Math.min(CLIENT_WAIT_MS, killMs))],
|
|
248
|
+
{ env, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'], timeout: Math.min(CLIENT_KILL_MS, killMs) },
|
|
249
249
|
);
|
|
250
250
|
if (asked.error !== undefined || asked.status !== 0) return undefined;
|
|
251
251
|
return parse(asked.stdout) ?? undefined;
|
|
@@ -586,17 +586,21 @@ const psCli = () => process.env.OBK_PS || '/bin/ps';
|
|
|
586
586
|
* Who holds the terminal of the tab `handle`, whose pane is `ptyId`:
|
|
587
587
|
* `{ front: 'shell' }`, `{ front: 'program', command, pid }` with the name and,
|
|
588
588
|
* where `ps` gave it, the pid of the process leading the group in front, or
|
|
589
|
-
* `{ unreadable: <why> }
|
|
589
|
+
* `{ unreadable: <why> }`; each with `pane`, the pane's pid, where Orca gave it.
|
|
590
590
|
*
|
|
591
591
|
* `ps` is asked first. Where it cannot read the tab, as inside Codex's
|
|
592
592
|
* sandbox, where it does not start at all, Orca's runtime is asked instead
|
|
593
|
-
* (#298, ADR 0034).
|
|
593
|
+
* (#298, ADR 0034). `readMs` bounds each call: a number, or a function that
|
|
594
|
+
* says what is left of a wait, asked before each one (#498). A `ps` that runs
|
|
595
|
+
* out of it ends the look, `late`, with no runtime asked after it.
|
|
594
596
|
*/
|
|
595
597
|
function frontOf(handle, ptyId, readMs) {
|
|
596
598
|
const read = frontByPs(ptyId, readMs);
|
|
597
|
-
if (read.unreadable === undefined) return read;
|
|
598
|
-
const asked = frontByOrca(handle);
|
|
599
|
-
|
|
599
|
+
if (read.unreadable === undefined || read.late === true) return read;
|
|
600
|
+
const asked = frontByOrca(handle, msOf(readMs));
|
|
601
|
+
// The pane's pid, where `diagnostics memory` gave it, goes on with the answer.
|
|
602
|
+
const pane = read.pane === undefined ? {} : { pane: read.pane };
|
|
603
|
+
return { ...(asked.unreadable === undefined ? asked : { unreadable: `${read.unreadable}, and ${asked.unreadable}` }), psUnread: true, ...pane };
|
|
600
604
|
}
|
|
601
605
|
|
|
602
606
|
/**
|
|
@@ -608,8 +612,8 @@ function frontOf(handle, ptyId, readMs) {
|
|
|
608
612
|
* else is "cannot tell": for a moment after a harness quits, Orca can name
|
|
609
613
|
* nothing and still see a child in front (read in the Orca 1.4.212 bundle).
|
|
610
614
|
*/
|
|
611
|
-
function frontByOrca(handle) {
|
|
612
|
-
const seen = askRuntime('terminal.inspectProcess', { terminal: handle })?.result?.process;
|
|
615
|
+
function frontByOrca(handle, readMs) {
|
|
616
|
+
const seen = askRuntime('terminal.inspectProcess', { terminal: handle }, readMs)?.result?.process;
|
|
613
617
|
const evidence = seen?.foregroundProcessEvidence;
|
|
614
618
|
if (evidence?.verdict !== 'live') {
|
|
615
619
|
const why = typeof evidence?.reason === 'string' ? `: ${evidence.reason}` : '';
|
|
@@ -633,6 +637,29 @@ function frontByOrca(handle) {
|
|
|
633
637
|
* here ends in "cannot tell" rather than in an error or a guess.
|
|
634
638
|
*/
|
|
635
639
|
function frontByPs(ptyId, readMs) {
|
|
640
|
+
const { pane, unreadable } = panePid(ptyId, msOf(readMs));
|
|
641
|
+
if (unreadable !== undefined) return { unreadable };
|
|
642
|
+
|
|
643
|
+
const own = psLine(pane, msOf(readMs));
|
|
644
|
+
if (own?.late === true) return { unreadable: `ps did not answer in time for its pane, pid ${pane}`, late: true };
|
|
645
|
+
if (own === undefined) return { unreadable: `ps could not read its pane, pid ${pane}`, pane };
|
|
646
|
+
if (own.tpgid === pane) return { front: 'shell', pane };
|
|
647
|
+
|
|
648
|
+
const front = psLine(own.tpgid, msOf(readMs));
|
|
649
|
+
if (front?.late === true) return { unreadable: `ps did not answer in time for the process in front, pid ${own.tpgid}`, late: true };
|
|
650
|
+
if (front === undefined) return { unreadable: `ps could not read the process in front, pid ${own.tpgid}`, pane };
|
|
651
|
+
const shell = front.ppid === pane && path.basename(own.comm) === 'login';
|
|
652
|
+
return shell ? { front: 'shell', pane } : { front: 'program', command: path.basename(front.comm), pid: own.tpgid, pane };
|
|
653
|
+
}
|
|
654
|
+
|
|
655
|
+
/** A bound `frontOf` was given, as a number of milliseconds: asked now when it is a function. */
|
|
656
|
+
const msOf = (readMs) => (typeof readMs === 'function' ? readMs() : readMs);
|
|
657
|
+
|
|
658
|
+
/**
|
|
659
|
+
* The pid of the pane `ptyId`, `{ pane }`, from `diagnostics memory`, or
|
|
660
|
+
* `{ unreadable: <why> }`.
|
|
661
|
+
*/
|
|
662
|
+
function panePid(ptyId, readMs) {
|
|
636
663
|
let pane;
|
|
637
664
|
try {
|
|
638
665
|
pane = orca(['diagnostics', 'memory'], { timeoutMs: readMs }).worktrees
|
|
@@ -646,15 +673,87 @@ function frontByPs(ptyId, readMs) {
|
|
|
646
673
|
return { unreadable: `Orca would not give the pane's pid: ${error.message}` };
|
|
647
674
|
}
|
|
648
675
|
if (!Number.isInteger(pane) || pane <= 0) return { unreadable: 'Orca gave no pid for its pane' };
|
|
676
|
+
return { pane };
|
|
677
|
+
}
|
|
649
678
|
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
679
|
+
/** The `lsof` and `stty` this run asks. OBK_LSOF and OBK_STTY override them, as OBK_PS does `ps`. */
|
|
680
|
+
const lsofCli = () => process.env.OBK_LSOF || '/usr/sbin/lsof';
|
|
681
|
+
const sttyCli = () => process.env.OBK_STTY || '/bin/stty';
|
|
653
682
|
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
683
|
+
/**
|
|
684
|
+
* One look at the shell of a tab the kit has just opened, before anything is
|
|
685
|
+
* typed into it (#498): `{ ready: true }` when the shell is at a prompt that
|
|
686
|
+
* takes a line, `{ asking: true }` when the shell is in front and reading the
|
|
687
|
+
* tty some other way, `{ program }` when something else is in front, and
|
|
688
|
+
* `{ unsure: <why> }` when the kit cannot tell, with `late` when a call ran out
|
|
689
|
+
* of the time left until `until`.
|
|
690
|
+
*
|
|
691
|
+
* A shell's line editor (zsh's, bash's readline) puts the tty in non-canonical
|
|
692
|
+
* mode with no echo and no literal-next key while it waits at a prompt. A
|
|
693
|
+
* question asked in a start-up file (`read -k 1`, `read -p`, `read -s -k 1`)
|
|
694
|
+
* leaves echo or the literal-next key on, and so does a shell still busy with
|
|
695
|
+
* its start-up files. Orca's daemon tells a ready shell
|
|
696
|
+
* the same way when its own mark does not come (tech notes, section 1). The
|
|
697
|
+
* shell's prompt itself is never read: it is the user's.
|
|
698
|
+
*/
|
|
699
|
+
export function shellInTab(handle, until) {
|
|
700
|
+
// Each call is given what is left until `until`, so a reader that hangs
|
|
701
|
+
// cannot hold the launch past its wait.
|
|
702
|
+
const left = () => Math.max(1, until - Date.now());
|
|
703
|
+
try {
|
|
704
|
+
let shown;
|
|
705
|
+
try {
|
|
706
|
+
shown = orca(['terminal', 'show', '--terminal', handle], { timeoutMs: left() }).terminal;
|
|
707
|
+
} catch (error) {
|
|
708
|
+
if (error.code === TIMED_OUT) throw error;
|
|
709
|
+
return { unsure: `Orca would not show the tab: ${error.message}` };
|
|
710
|
+
}
|
|
711
|
+
const front = frontOf(handle, shown?.ptyId, left);
|
|
712
|
+
if (front.unreadable !== undefined) return { unsure: front.unreadable, ...(front.late ? { late: true } : {}) };
|
|
713
|
+
if (front.front === 'program') return { program: front.command };
|
|
714
|
+
const { pane, unreadable } = front.pane === undefined ? panePid(shown?.ptyId, left()) : front;
|
|
715
|
+
if (unreadable !== undefined) return { unsure: unreadable };
|
|
716
|
+
const mode = ttyMode(pane, left);
|
|
717
|
+
if (mode.unreadable !== undefined) return { unsure: mode.unreadable, ...(mode.late ? { late: true } : {}) };
|
|
718
|
+
return mode.lineEditor ? { ready: true } : { asking: true };
|
|
719
|
+
} catch (error) {
|
|
720
|
+
if (error.code !== TIMED_OUT) throw error;
|
|
721
|
+
return { unsure: `Orca did not answer in time: ${error.message}`, late: true };
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
|
|
725
|
+
/**
|
|
726
|
+
* Whether the tty of the pane `pid` is in a line editor's mode, `{ lineEditor }`,
|
|
727
|
+
* or `{ unreadable: <why> }`. `lsof` names the tty on the standard input of the
|
|
728
|
+
* shell, and `stty` reads its flags; both run inside Codex's sandbox, where
|
|
729
|
+
* `ps` does not. The pane is `login`, which runs as root and whose files `lsof`
|
|
730
|
+
* cannot read, so the shell is found as the user's process whose parent it is,
|
|
731
|
+
* or as the pane itself where the pane is the shell.
|
|
732
|
+
*/
|
|
733
|
+
function ttyMode(pid, left) {
|
|
734
|
+
const listed = spawnSync(lsofCli(), ['-a', '-R', '-d', '0', '-u', String(process.getuid()), '-FpRn'], { encoding: 'utf8', timeout: left() });
|
|
735
|
+
if (listed.error?.code === 'ETIMEDOUT') return { unreadable: 'lsof did not answer in time', late: true };
|
|
736
|
+
if (listed.error || listed.status !== 0) return { unreadable: `lsof could not list the processes' ttys` };
|
|
737
|
+
// One record per process: `p<pid>`, `R<parent pid>`, `f0`, `n<name>`.
|
|
738
|
+
let record = {};
|
|
739
|
+
let tty;
|
|
740
|
+
for (const line of listed.stdout.split('\n')) {
|
|
741
|
+
if (line.startsWith('p')) record = { pid: Number(line.slice(1)) };
|
|
742
|
+
else if (line.startsWith('R')) record.ppid = Number(line.slice(1));
|
|
743
|
+
else if (line.startsWith('n/dev/') && (record.pid === pid || record.ppid === pid)) tty ??= line.slice(1);
|
|
744
|
+
}
|
|
745
|
+
if (tty === undefined) return { unreadable: `lsof named no tty for the shell of its pane, pid ${pid}` };
|
|
746
|
+
|
|
747
|
+
const read = spawnSync(sttyCli(), ['-a', '-f', tty], { encoding: 'utf8', timeout: left() });
|
|
748
|
+
if (read.error?.code === 'ETIMEDOUT') return { unreadable: `stty did not answer in time for ${tty}`, late: true };
|
|
749
|
+
if (read.error || read.status !== 0) return { unreadable: `stty could not read ${tty}` };
|
|
750
|
+
const words = read.stdout.split(/\s+/);
|
|
751
|
+
const flag = (name) => (words.includes(name) ? true : words.includes(`-${name}`) ? false : undefined);
|
|
752
|
+
const [icanon, echo] = [flag('icanon'), flag('echo')];
|
|
753
|
+
if (icanon === undefined || echo === undefined) return { unreadable: `stty gave no icanon and echo flags for ${tty}` };
|
|
754
|
+
// A line editor also turns off the literal-next key, which a question read
|
|
755
|
+
// with echo off (`read -s -k 1`) leaves on, as Orca's own test has it.
|
|
756
|
+
return { lineEditor: !icanon && !echo && /(?:^|[;\s])lnext\s*=\s*<undef>(?:;|\s|$)/.test(read.stdout) };
|
|
658
757
|
}
|
|
659
758
|
|
|
660
759
|
/**
|
|
@@ -670,9 +769,10 @@ export function wordsOfProcess(pid) {
|
|
|
670
769
|
return asked.stdout.trim().split(/\s+/);
|
|
671
770
|
}
|
|
672
771
|
|
|
673
|
-
/** One process as `ps` gives it, read only: `{ ppid, tpgid, comm }`, or undefined. */
|
|
674
|
-
function psLine(pid) {
|
|
675
|
-
const asked = spawnSync(psCli(), ['-o', 'pid=,ppid=,tpgid=,comm=', '-p', String(pid)], { encoding: 'utf8' });
|
|
772
|
+
/** One process as `ps` gives it, read only: `{ ppid, tpgid, comm }`, `{ late: true }` past `readMs`, or undefined. */
|
|
773
|
+
function psLine(pid, readMs) {
|
|
774
|
+
const asked = spawnSync(psCli(), ['-o', 'pid=,ppid=,tpgid=,comm=', '-p', String(pid)], { encoding: 'utf8', ...(readMs === undefined ? {} : { timeout: readMs }) });
|
|
775
|
+
if (asked.error?.code === 'ETIMEDOUT') return { late: true };
|
|
676
776
|
if (asked.error || asked.status !== 0) return undefined;
|
|
677
777
|
const line = /^\s*(\d+)\s+(\d+)\s+(-?\d+)\s+(.+?)\s*$/.exec(asked.stdout);
|
|
678
778
|
if (line === null) return undefined;
|
package/src/up.js
CHANGED
|
@@ -14,7 +14,7 @@ import { conversationsIn, hasConversation, heldAsUserTurn, transcriptsIn } from
|
|
|
14
14
|
import { installHook } from './hooks.js';
|
|
15
15
|
import { writePermissions } from './permissions.js';
|
|
16
16
|
import { addressOf, harnessOf, isAddressOf, isShortPrompt, launchCommand, mailboxStep, reachesMail, sessionTrouble, startPrompt, workDirOf } from './launch.js';
|
|
17
|
-
import { asFolderProject, coordinatorOf, findProject, harnessInTab, makeMailbox, makeProject, openTab, QUESTION_ON_SCREEN, retitleTab, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, tellWindow, typeIntoTab, useMailbox } from './orca.js';
|
|
17
|
+
import { asFolderProject, coordinatorOf, findProject, harnessInTab, makeMailbox, makeProject, openTab, QUESTION_ON_SCREEN, retitleTab, screenRows, shellInTab, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, tellWindow, typeIntoTab, useMailbox } from './orca.js';
|
|
18
18
|
import { TAB_ENV } from './record.js';
|
|
19
19
|
import { buildAgents, rulesStamp } from './rules.js';
|
|
20
20
|
import { linkSkills } from './skills.js';
|
|
@@ -338,6 +338,14 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
338
338
|
writePrompt(launch);
|
|
339
339
|
}
|
|
340
340
|
|
|
341
|
+
// Only into a shell that is ready for it (#498). A question the shell asks as
|
|
342
|
+
// it starts takes the line as its answer: Orca holds a line for a new tab
|
|
343
|
+
// only until its shell is ready or 15 s pass, and then lets it through.
|
|
344
|
+
const notLaunched = await shellNotReady(made.handle);
|
|
345
|
+
if (notLaunched !== undefined) {
|
|
346
|
+
return entry(made, { bot: bot.name, name: session.name, created: true, notLaunched });
|
|
347
|
+
}
|
|
348
|
+
|
|
341
349
|
// Typing it in is the way: for a project the kit has just made, giving Orca
|
|
342
350
|
// the harness as the tab's own command times out and leaves a dead tab.
|
|
343
351
|
typeIntoTab(made.handle, launch.command);
|
|
@@ -395,6 +403,53 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
395
403
|
});
|
|
396
404
|
}
|
|
397
405
|
|
|
406
|
+
/**
|
|
407
|
+
* How long a new tab's shell is given to come to a ready prompt, and the pause
|
|
408
|
+
* between looks. A start-up with oh-my-zsh took about a second here; Orca
|
|
409
|
+
* itself waits 15 s for a new shell.
|
|
410
|
+
*/
|
|
411
|
+
const SHELL_READY_MS = 15000;
|
|
412
|
+
const SHELL_LOOK_MS = 250;
|
|
413
|
+
|
|
414
|
+
/** The least a look is given: a wait with less left than that ends. */
|
|
415
|
+
const LAST_LOOK_MS = 1000;
|
|
416
|
+
|
|
417
|
+
/** How long the screen read for a refusal's words is given. */
|
|
418
|
+
const SCREEN_READ_MS = 5000;
|
|
419
|
+
|
|
420
|
+
/**
|
|
421
|
+
* Undefined once the shell in the new tab `handle` is at a ready prompt, or,
|
|
422
|
+
* when it is not within SHELL_READY_MS, a sentence that says why and names
|
|
423
|
+
* what its screen shows (#498).
|
|
424
|
+
*/
|
|
425
|
+
async function shellNotReady(handle) {
|
|
426
|
+
const until = Date.now() + SHELL_READY_MS;
|
|
427
|
+
let seen;
|
|
428
|
+
// The last look that was not cut short: what the refusal is about, when the
|
|
429
|
+
// last one ran out of time.
|
|
430
|
+
let answered;
|
|
431
|
+
for (;;) {
|
|
432
|
+
const look = shellInTab(handle, until);
|
|
433
|
+
seen = look.late === true ? answered ?? look : look;
|
|
434
|
+
if (look.late !== true) answered = look;
|
|
435
|
+
// The calls that decide a yes, lsof and stty last, end by `until`, so a
|
|
436
|
+
// yes is never late.
|
|
437
|
+
if (seen.ready === true) return undefined;
|
|
438
|
+
// The last look ends the wait: one more would have no time to answer in.
|
|
439
|
+
if (until - Date.now() < SHELL_LOOK_MS + LAST_LOOK_MS) break;
|
|
440
|
+
await pause(SHELL_LOOK_MS);
|
|
441
|
+
}
|
|
442
|
+
const wait = `it did not come to a ready prompt in ${SHELL_READY_MS / 1000} s`;
|
|
443
|
+
if (seen.program !== undefined) return `${seen.program} is running in front of the shell, and ${wait}`;
|
|
444
|
+
if (seen.unsure !== undefined) return `the kit could not tell whether the shell is ready (${seen.unsure})`;
|
|
445
|
+
const read = screenRows(handle, SCREEN_READ_MS);
|
|
446
|
+
if (read.rows === undefined) return `the shell is asking something, and ${wait}; its screen could not be read (${read.unreadable})`;
|
|
447
|
+
const last = read.rows.findLast((row) => row.trim() !== '');
|
|
448
|
+
return last === undefined
|
|
449
|
+
? `the shell is asking something, and ${wait}; its screen is blank`
|
|
450
|
+
: `the shell is asking: ${last.trim()} (${wait})`;
|
|
451
|
+
}
|
|
452
|
+
|
|
398
453
|
/** The line a resumed Codex session is typed so Orca lists it, in the architect's words (#226). */
|
|
399
454
|
const LIST_LINE = 'obk: this session was resumed in a new tab, and this line is only so Orca lists it. Reply "ok"; nothing else is asked.';
|
|
400
455
|
|
|
@@ -796,8 +851,11 @@ const ids = (setup, change) => ({ project: setup.projectId, setup: setup.id, cha
|
|
|
796
851
|
export const promptPath = (bots, bot, session) =>
|
|
797
852
|
path.join(`${bots}.prompts`, `${encodeURIComponent(bot)}.${encodeURIComponent(session)}.txt`);
|
|
798
853
|
|
|
799
|
-
function entry(tab, { bot, name, created, running = false, blockedReason, promptReceived, promptFile, resumed, noConversation, unclaimed, noMailbox = false, listLine, listLineTrouble }) {
|
|
854
|
+
function entry(tab, { bot, name, created, running = false, blockedReason, promptReceived, promptFile, resumed, noConversation, unclaimed, noMailbox = false, listLine, listLineTrouble, notLaunched }) {
|
|
800
855
|
const made = { bot, name, title: tab.title, tabId: tab.tabId, terminal: tab.handle, created, harnessStarted: running };
|
|
856
|
+
// A tab whose shell was not ready for the launch line, and why: nothing was
|
|
857
|
+
// typed into it (#498).
|
|
858
|
+
if (notLaunched !== undefined) Object.assign(made, { launched: false, notLaunched });
|
|
801
859
|
// Whether this run picked the session up where it was or started a new one.
|
|
802
860
|
// Only for a tab this run opened: a tab that was already there was left alone.
|
|
803
861
|
if (resumed !== undefined) made.resumed = resumed;
|