@assuredloop/orca-bot-kit 0.14.0 → 0.16.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/SETUP.md +6 -6
- package/package.json +1 -1
- package/skills/obk-bot-building/SKILL.md +12 -9
- package/skills/obk-debugging/SKILL.md +10 -6
- package/skills/obk-fleet-review/SKILL.md +12 -0
- package/skills/obk-grilling/NOTICE.md +1 -1
- package/skills/obk-grooming/SKILL.md +19 -0
- package/src/bot.js +20 -0
- package/src/cli.js +11 -1
- package/src/orca-reload.applescript +1 -1
- package/src/orca-runtime.cjs +1 -1
- package/src/orca.js +99 -29
- package/src/pause.js +2 -2
- package/src/restart.js +41 -4
- package/src/retire.js +3 -3
- package/src/up.js +67 -4
package/SETUP.md
CHANGED
|
@@ -194,12 +194,12 @@ waiting for:
|
|
|
194
194
|
| On screen | Send | Which is | Proven on |
|
|
195
195
|
|---|---|---|---|
|
|
196
196
|
| Claude Code's folder trust list | `\x1b[B\r` | down, then return: its selection starts on **No, exit** | Claude Code 2.1.283 |
|
|
197
|
-
| Codex's directory trust, `1. Trust and continue` (older: `1. Yes, continue`) | `\r` | return: its selection starts on **Trust and continue** | Codex 0.
|
|
198
|
-
| Codex's `Hooks need review` | `\x1b[B\r` | down, then return, to **Trust all and continue**; if the selection already starts there, return alone | Codex 0.
|
|
199
|
-
| Codex's `/new`: `Where should the new conversation run?` | `\r` | return: its selection starts on **Current checkout** (bot home) | Codex 0.
|
|
200
|
-
| Codex's update offer, `1. Update now` | `\r` | return: its selection starts on **Update now**, which accepts it | Codex 0.156.1; 0.157.1 has had no newer release to offer |
|
|
197
|
+
| Codex's directory trust, `1. Trust and continue` (older: `1. Yes, continue`) | `\r` | return: its selection starts on **Trust and continue** | Codex 0.158.0 |
|
|
198
|
+
| Codex's `Hooks need review` | `\x1b[B\r` | down, then return, to **Trust all and continue**; if the selection already starts there, return alone | Codex 0.158.0 |
|
|
199
|
+
| Codex's `/new`: `Where should the new conversation run?` | `\r` | return: its selection starts on **Current checkout** (bot home) | Codex 0.158.0 |
|
|
200
|
+
| Codex's update offer, `1. Update now` | `\r` | return: its selection starts on **Update now**, which accepts it | Codex 0.156.1; seen on 0.157.1 (offering 0.158.0) with the selection on **Update now**, not pressed; 0.158.0 has had no newer release to offer |
|
|
201
201
|
| `[oh-my-zsh] Would you like to update?` | `n` | they update their own shell | oh-my-zsh bf77e35 |
|
|
202
|
-
| Claude Code's `Teach auto mode about your environment?` | `\x1b` | Esc, which is **
|
|
202
|
+
| Claude Code's `Teach auto mode about your environment?` | `\x1b` | Esc, which cancels it. On 2.1.283 it is a form, not a list: `How you use Claude here`, `Also scan shell history`, `Also scan your other repos`, `Continue`, and `Enter to continue · Esc to cancel`. **Never send a return**: Enter is **Continue**, which starts a scan of the project, recent Claude sessions and, by default, the machine's shell history | Not yet proven: Esc cancels in Claude Code 2.1.283's code, not yet seen live (#370); the form was seen live on 2.1.283 (#416); 2.1.278 showed a numbered list, answered with 2. Not now |
|
|
203
203
|
|
|
204
204
|
Each key counts on where the selection starts, as it did on the version named.
|
|
205
205
|
Read the screen in front of you before you send: if the selection is somewhere
|
|
@@ -217,7 +217,7 @@ empty book and nothing says why.
|
|
|
217
217
|
Codex's trust question applies to the **repository root**, which for a bot means
|
|
218
218
|
the whole bots folder rather than the one bot. That is what they are agreeing to.
|
|
219
219
|
|
|
220
|
-
Codex's `/new` question (seen on 0.156.1 and 0.
|
|
220
|
+
Codex's `/new` question (seen on 0.156.1, 0.157.1 and 0.158.0) is always answered with the current checkout, which is the bot
|
|
221
221
|
home. The kit never makes a git worktree, so
|
|
222
222
|
`2. New worktree` is never the answer.
|
|
223
223
|
|
package/package.json
CHANGED
|
@@ -284,12 +284,12 @@ otherwise `/Applications/Orca.app/Contents/Resources/bin/orca`. A bare
|
|
|
284
284
|
| On screen | Send | Which is | Proven on |
|
|
285
285
|
|---|---|---|---|
|
|
286
286
|
| Claude Code's trust list | `\x1b[B\r` | down, return: off **No, exit** | Claude Code 2.1.283 |
|
|
287
|
-
| Codex's `Trust this folder?` | `\r` | return: it starts on yes | Codex 0.
|
|
288
|
-
| Codex's `Hooks need review` | `\x1b[B\r` | **Trust all and continue** | Codex 0.
|
|
289
|
-
| Codex's `Where should the new conversation run?` | `\r` | **Current checkout** | Codex 0.
|
|
287
|
+
| Codex's `Trust this folder?` | `\r` | return: it starts on yes | Codex 0.158.0 |
|
|
288
|
+
| Codex's `Hooks need review` | `\x1b[B\r` | **Trust all and continue** | Codex 0.158.0 |
|
|
289
|
+
| Codex's `Where should the new conversation run?` | `\r` | **Current checkout** | Codex 0.158.0 |
|
|
290
290
|
| Codex's update offer, `1. Update now` | `\r` | **Update now**, where it starts | Codex 0.156.1 |
|
|
291
291
|
| `[oh-my-zsh] Would you like to update?` | `n` | no: the user updates it | oh-my-zsh bf77e35 |
|
|
292
|
-
| Claude Code's `Teach auto mode…` | `\x1b` | Esc
|
|
292
|
+
| Claude Code's `Teach auto mode…` | `\x1b` | Esc cancels; no return | Not yet proven, below |
|
|
293
293
|
|
|
294
294
|
Each key counts on where the selection starts on the version named. Read the
|
|
295
295
|
screen before you send: if the selection is elsewhere or the options have
|
|
@@ -297,15 +297,18 @@ moved, go to the answer with the arrows, then return. Never a digit and then
|
|
|
297
297
|
return: on Codex's update offer that took the highlighted **Update now**,
|
|
298
298
|
whatever the digit was. Codex's trust said `1. Yes, continue` on older
|
|
299
299
|
versions. Its hooks review sometimes starts on `2` already: then return alone.
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
300
|
+
Its update offer was proven on 0.156.1; 0.157.1 showed it with the selection on
|
|
301
|
+
Update now, not pressed, and 0.158.0 has had no newer release to offer.
|
|
302
|
+
Claude Code's `Teach auto mode` on 2.1.283 is a form, not a list, ending
|
|
303
|
+
`Enter to continue · Esc to cancel`: never send it a return, since
|
|
304
|
+
Enter is Continue, which scans the project, recent sessions and the machine's
|
|
305
|
+
shell history. Esc cancels in its code; that is not yet proven live. On 2.1.278
|
|
306
|
+
it was a numbered list, answered with `2. Not now`. Codex's hooks
|
|
304
307
|
question matters most. The kit's hook is how the book learns which
|
|
305
308
|
conversation the session is running, and until it is answered the
|
|
306
309
|
conversation has not started. Codex's trust applies to the repository root,
|
|
307
310
|
which is the whole bots folder. Codex's `/new` may ask where the new
|
|
308
|
-
conversation runs (0.156.1
|
|
311
|
+
conversation runs (0.156.1, 0.157.1 and 0.158.0 all did); the answer is
|
|
309
312
|
always the current checkout, the bot home, and never `2. New worktree`:
|
|
310
313
|
the kit never makes a git worktree.
|
|
311
314
|
|
|
@@ -379,12 +379,16 @@ Write the regression test before the fix, at a seam where it exercises the bug
|
|
|
379
379
|
the way it actually happened; if there is no correct seam, that is itself the
|
|
380
380
|
finding.
|
|
381
381
|
|
|
382
|
-
The test is the separate author's to write, as it is for any other behaviour
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
382
|
+
The test is the separate author's to write, as it is for any other behaviour:
|
|
383
|
+
someone with a context of their own, such as a fresh subagent, another session
|
|
384
|
+
or a person, never a fork of this conversation. Give them the requirement and
|
|
385
|
+
what the correct result is, the public interface, and your reproduction as the
|
|
386
|
+
worked example; not your fix, your diff or your plan. They return the tests, the
|
|
387
|
+
command, and the failing output, failing because the bug is there rather than
|
|
388
|
+
because of a typo. `obk-tdd` has more on the brief, if you can load it. Two are
|
|
389
|
+
often better than one: one at the interface saying what a caller should have
|
|
390
|
+
got, and the smallest one that shows the fault where it lives. A test that now
|
|
391
|
+
looks wrong goes back to its author; it is not edited to fit the fix.
|
|
388
392
|
|
|
389
393
|
Watch it fail. Fix. Watch it pass. Then run the original, unminimised scenario
|
|
390
394
|
again, on the same surface the bug appeared on. A different surface, or an
|
|
@@ -227,6 +227,18 @@ ask for and you do it.
|
|
|
227
227
|
A grooming report is input to this review, not its end: check what it flags
|
|
228
228
|
against the fleet, and recommend from there.
|
|
229
229
|
|
|
230
|
+
When a grooming report names temporary sessions a bot left behind, offer the
|
|
231
|
+
user one rule for that bot's charter, with the words ready to accept, such as:
|
|
232
|
+
"Retire each temporary session you make as soon as its work is done, and
|
|
233
|
+
before you end a piece of work, check that none of yours is left." On a yes,
|
|
234
|
+
read the bot's charter, add the line, and set the whole with `obk bot change
|
|
235
|
+
--bots <folder> --bot <bot> --charter <text>`, which replaces the charter, so
|
|
236
|
+
what you pass is all of it. On a no, nothing changes, and it is not offered
|
|
237
|
+
again for the same sessions: write the declined offer in grooming's open
|
|
238
|
+
findings, next to those sessions, so it outlasts this conversation. Retire
|
|
239
|
+
none of them yourself, and send nothing to their makers: the hint is for the
|
|
240
|
+
user, and only a maker knows when its work is done.
|
|
241
|
+
|
|
230
242
|
Sort the problem before you write it up. Something the kit does wrong, or
|
|
231
243
|
cannot do, is filed where the kit's problems are filed, as one issue, in one
|
|
232
244
|
step: no draft to approve first. Something a bot is doing wrong goes back to
|
|
@@ -73,7 +73,7 @@ words where they are quoted here:
|
|
|
73
73
|
against the session log (19:12:40, 19:15:38), which is the fix the skill
|
|
74
74
|
gives.
|
|
75
75
|
|
|
76
|
-
|
|
76
|
+
Five deliberate departures from `grilling` and `domain-modeling`, so they are
|
|
77
77
|
not mistaken for consolidation:
|
|
78
78
|
|
|
79
79
|
- **A round is three questions, not the whole frontier.** `grilling` says to
|
|
@@ -185,6 +185,25 @@ Never edit a memory yourself, not even to cut it. The memory is the bot's, the
|
|
|
185
185
|
harness keeps it, and a pass that rewrites what a bot remembers is a pass that
|
|
186
186
|
decides what the bot knows.
|
|
187
187
|
|
|
188
|
+
## Temporary sessions left behind
|
|
189
|
+
|
|
190
|
+
A temporary session is one a bot's own session made for a piece of work with
|
|
191
|
+
`obk temp make`, and its maker retires it when the work is done. Each bot's
|
|
192
|
+
book, `sessions.yaml` in its folder, marks one on its entry as `temporary`,
|
|
193
|
+
with its `maker` and when it was `made`. Read the books every run, a quiet one
|
|
194
|
+
included: a session left behind does no work, so the usage index never shows
|
|
195
|
+
it.
|
|
196
|
+
|
|
197
|
+
Count one as left behind when its maker is no longer one of the bot's
|
|
198
|
+
sessions, or when it was made more than a day ago. The report names each: the
|
|
199
|
+
bot and session, its maker, saying so where the maker is gone, and how old it
|
|
200
|
+
is. Put them in the open findings, so the next run says whether each is still
|
|
201
|
+
there rather than reporting it as new.
|
|
202
|
+
|
|
203
|
+
Retire none of them. Only the maker knows when the work is done: a review
|
|
204
|
+
session can be waiting on an open pull request for a re-review. What to do
|
|
205
|
+
about a bot that leaves them behind is the management session's to offer.
|
|
206
|
+
|
|
188
207
|
## Say whether this run was worth making
|
|
189
208
|
|
|
190
209
|
Every run, record which of three things happened: it found something that
|
package/src/bot.js
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
// writes nothing, rather than leave a bot half made or a bot.yaml half edited.
|
|
9
9
|
|
|
10
10
|
import { existsSync, lstatSync, mkdirSync, readFileSync, readdirSync, readlinkSync, realpathSync, statSync, writeFileSync } from 'node:fs';
|
|
11
|
+
import os from 'node:os';
|
|
11
12
|
import path from 'node:path';
|
|
12
13
|
import { isDeepStrictEqual } from 'node:util';
|
|
13
14
|
import { parse, parseDocument, stringify } from 'yaml';
|
|
@@ -114,6 +115,25 @@ function whereItLeads(target) {
|
|
|
114
115
|
export const displayName = (name) =>
|
|
115
116
|
name.split('-').map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(' ');
|
|
116
117
|
|
|
118
|
+
/**
|
|
119
|
+
* What Orca is asked to call a bot's project. A fleet whose bots folder is under
|
|
120
|
+
* the system's temp folder, as a system test's or a live check's is, gets a mark
|
|
121
|
+
* and the name of the first folder under the temp folder, so its Bot Father is
|
|
122
|
+
* never taken for the owner's in Orca's sidebar (#401). Every other fleet's
|
|
123
|
+
* project is called what it always was. Tab titles keep `displayName` either way.
|
|
124
|
+
*/
|
|
125
|
+
export function projectName(bots, name) {
|
|
126
|
+
let under;
|
|
127
|
+
try {
|
|
128
|
+
under = path.relative(realpathSync(os.tmpdir()), realpathSync(bots));
|
|
129
|
+
} catch {
|
|
130
|
+
return displayName(name);
|
|
131
|
+
}
|
|
132
|
+
// The temp folder itself, or anywhere outside it, is not a throwaway fleet's.
|
|
133
|
+
if (under === '' || under === '..' || under.startsWith(`..${path.sep}`) || path.isAbsolute(under)) return displayName(name);
|
|
134
|
+
return `${displayName(name)} · temp fleet ${under.split(path.sep)[0]}`;
|
|
135
|
+
}
|
|
136
|
+
|
|
117
137
|
/**
|
|
118
138
|
* A bot's own file: which harness its sessions run on unless they say
|
|
119
139
|
* otherwise, and the sessions it keeps.
|
package/src/cli.js
CHANGED
|
@@ -1574,11 +1574,21 @@ function harnessLines(tab, bots) {
|
|
|
1574
1574
|
...ANSWER_IT,
|
|
1575
1575
|
];
|
|
1576
1576
|
|
|
1577
|
-
lines.push(...promptLines(tab));
|
|
1577
|
+
lines.push(...promptLines(tab), ...listLines(tab));
|
|
1578
1578
|
if (tab.unclaimed !== undefined) lines.push(...unclaimedLines(tab, bots));
|
|
1579
1579
|
return lines;
|
|
1580
1580
|
}
|
|
1581
1581
|
|
|
1582
|
+
/**
|
|
1583
|
+
* Whether a resumed Codex session was typed its one line so Orca lists it among
|
|
1584
|
+
* its agents, which it does only from a session's first turn (#226).
|
|
1585
|
+
*/
|
|
1586
|
+
function listLines(tab) {
|
|
1587
|
+
if (tab.listLine === true) return [' it was typed one line so Orca lists it among its agents: Codex tells Orca only at a first turn.'];
|
|
1588
|
+
if (tab.listLine === false) return [` it was not typed the line that makes Orca list it (${tab.listLineTrouble}): Orca lists it at its next turn.`];
|
|
1589
|
+
return [];
|
|
1590
|
+
}
|
|
1591
|
+
|
|
1582
1592
|
/**
|
|
1583
1593
|
* Whether the session got its start prompt, as its own record has it (#274):
|
|
1584
1594
|
* received, or not confirmed yet. Nothing for a session that was told nothing.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
-- Run by the kit as `osascript orca-reload.applescript <Orca.app path>`, after
|
|
2
|
-
-- a project was removed (#343, ADR
|
|
2
|
+
-- a project was removed (#343, ADR 0031).
|
|
3
3
|
--
|
|
4
4
|
-- Orca's window keeps a removed project in its sidebar until the window is
|
|
5
5
|
-- rebuilt (stablyai/orca#23224; once a release fixes it, this can go behind a
|
package/src/orca-runtime.cjs
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
// CLI: `ELECTRON_RUN_AS_NODE=1 <Orca> orca-runtime.cjs <client> <method> <params> <wait>`.
|
|
3
3
|
//
|
|
4
4
|
// It makes one call to Orca's runtime, through Orca's own client, for a method
|
|
5
|
-
// Orca's CLI does not offer (ADR
|
|
5
|
+
// Orca's CLI does not offer (ADR 0031): `project.update`, which makes Orca's
|
|
6
6
|
// window read its projects again (#224), and `terminal.inspectProcess`, which
|
|
7
7
|
// says who is in front of a tab where `ps` cannot (#298). `params` is JSON. It
|
|
8
8
|
// prints the runtime's answer as JSON and exits 0 only when the runtime took
|
package/src/orca.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Every call the kit makes to Orca goes through here (ADR
|
|
1
|
+
// Every call the kit makes to Orca goes through here (ADR 0031). Orca's CLI
|
|
2
2
|
// changes often, so the kit reads `--json` and never the human text, and keeps
|
|
3
3
|
// the parsing in one place.
|
|
4
4
|
//
|
|
@@ -13,6 +13,9 @@ import { homedir } from 'node:os';
|
|
|
13
13
|
import path from 'node:path';
|
|
14
14
|
import { fileURLToPath } from 'node:url';
|
|
15
15
|
|
|
16
|
+
import { SHELL_ENV } from './launch.js';
|
|
17
|
+
import { TAB_ENV } from './record.js';
|
|
18
|
+
|
|
16
19
|
// The Orca that works for a normal user: `/usr/local/bin/orca` is a root-only
|
|
17
20
|
// symlink on this machine (tech notes, section 1). OBK_ORCA overrides it, for a
|
|
18
21
|
// machine that keeps Orca somewhere else.
|
|
@@ -40,10 +43,14 @@ export function orca(args, { timeoutMs } = {}) {
|
|
|
40
43
|
/** What an error carries when Orca was given a time and did not answer in it. */
|
|
41
44
|
export const TIMED_OUT = 'orca_timed_out';
|
|
42
45
|
|
|
43
|
-
/**
|
|
46
|
+
/**
|
|
47
|
+
* Orca's refusal as an error, with its code on it, so a caller can tell a stale
|
|
48
|
+
* handle from the rest (#294), and its own message as `reason`, so a caller
|
|
49
|
+
* that goes by the message reads Orca's words rather than the kit's (#405).
|
|
50
|
+
*/
|
|
44
51
|
const refusal = (args, answer) => Object.assign(
|
|
45
52
|
new Error(`Orca refused ${args.join(' ')}: ${answer.error?.message ?? 'no reason given'}`),
|
|
46
|
-
{ code: answer.error?.code },
|
|
53
|
+
{ code: answer.error?.code, reason: answer.error?.message },
|
|
47
54
|
);
|
|
48
55
|
|
|
49
56
|
/** One Orca command, and the whole envelope back, refusals included. */
|
|
@@ -195,7 +202,7 @@ const CLIENT_KILL_MS = 3000;
|
|
|
195
202
|
*
|
|
196
203
|
* Orca's window re-reads only when its runtime says the projects changed, and
|
|
197
204
|
* `setup-update` and `setup-delete` do not say so. `project.update` with no
|
|
198
|
-
* changes does, and Orca's CLI does not offer it (ADR
|
|
205
|
+
* changes does, and Orca's CLI does not offer it (ADR 0031). The caller prints
|
|
199
206
|
* `RELOAD_LINE` either way. It does not take a removed project out of the
|
|
200
207
|
* sidebar: `reloadWindow` does.
|
|
201
208
|
*/
|
|
@@ -206,7 +213,7 @@ export const tellWindow = (projectId) => askRuntime('project.update', { projectI
|
|
|
206
213
|
* runtime's answer, or undefined when there is none to be had.
|
|
207
214
|
*
|
|
208
215
|
* It goes through Orca's own runtime client out of the installed app, run by
|
|
209
|
-
* Orca's binary the way its `bin/orca` runs its CLI (ADR
|
|
216
|
+
* Orca's binary the way its `bin/orca` runs its CLI (ADR 0031). None of that is
|
|
210
217
|
* Orca's published interface, so anything that goes wrong is a quiet
|
|
211
218
|
* undefined: it never throws and is never tried twice.
|
|
212
219
|
*/
|
|
@@ -243,7 +250,7 @@ const RELOAD_KILL_MS = 5000;
|
|
|
243
250
|
|
|
244
251
|
/**
|
|
245
252
|
* Have Orca's window force-reload itself, after a project was removed: true
|
|
246
|
-
* only when it did (#343, ADR
|
|
253
|
+
* only when it did (#343, ADR 0031).
|
|
247
254
|
*
|
|
248
255
|
* The window keeps a removed project in its sidebar until it is rebuilt, and
|
|
249
256
|
* `tellWindow` only relabels the row "Unknown" (stablyai/orca#23224; once a
|
|
@@ -319,9 +326,11 @@ export const retitleTab = (handle, title) =>
|
|
|
319
326
|
* the screen, as `questionIn` answers, and `screenUnreadable` says why there
|
|
320
327
|
* was none.
|
|
321
328
|
*/
|
|
322
|
-
export function harnessInTab(handle, timeoutMs) {
|
|
329
|
+
export function harnessInTab(handle, timeoutMs, readMs) {
|
|
323
330
|
const args = ['terminal', 'wait', '--terminal', handle, '--for', 'tui-idle', '--timeout-ms', String(timeoutMs)];
|
|
324
|
-
|
|
331
|
+
// With `readMs`, each call to Orca is given that long on top of what it was
|
|
332
|
+
// asked to wait, and ends as TIMED_OUT when it has not answered (#422).
|
|
333
|
+
const answer = ask(args, readMs === undefined ? undefined : timeoutMs + readMs);
|
|
325
334
|
|
|
326
335
|
// Out of time means nothing went idle, busy or absent alike. That is an
|
|
327
336
|
// answer, not a breakdown.
|
|
@@ -329,20 +338,26 @@ export function harnessInTab(handle, timeoutMs) {
|
|
|
329
338
|
throw refusal(args, answer);
|
|
330
339
|
}
|
|
331
340
|
|
|
332
|
-
const shown = orca(['terminal', 'show', '--terminal', handle]).terminal;
|
|
341
|
+
const shown = orca(['terminal', 'show', '--terminal', handle], { timeoutMs: readMs }).terminal;
|
|
333
342
|
const agent = typeof shown?.agentIdentity === 'string' && shown.agentIdentity !== '' ? shown.agentIdentity : undefined;
|
|
334
343
|
return {
|
|
335
344
|
answered: answer.ok === true,
|
|
336
345
|
blockedReason: answer.result?.wait?.blockedReason,
|
|
337
346
|
agent,
|
|
338
|
-
...frontOf(handle, shown?.ptyId),
|
|
339
|
-
...screenOf(handle),
|
|
347
|
+
...frontOf(handle, shown?.ptyId, readMs),
|
|
348
|
+
...screenOf(handle, readMs),
|
|
340
349
|
};
|
|
341
350
|
}
|
|
342
351
|
|
|
343
352
|
/** Orca's reason for a folder-trust screen, which it can go on giving once that is answered (#342). */
|
|
344
353
|
const TRUST_REASON = 'agent-trust-workspace';
|
|
345
354
|
|
|
355
|
+
/**
|
|
356
|
+
* How long each call to Orca the typing gate makes is given, beyond any wait it
|
|
357
|
+
* asked for, before the gate gives up on it and cannot tell (#422).
|
|
358
|
+
*/
|
|
359
|
+
const GATE_READ_MS = 5000;
|
|
360
|
+
|
|
346
361
|
/** The kit's word for a tab whose screen shows a question of its harness's own (#329). */
|
|
347
362
|
export const QUESTION_ON_SCREEN = 'question-on-screen';
|
|
348
363
|
|
|
@@ -353,10 +368,10 @@ export const QUESTION_ON_SCREEN = 'question-on-screen';
|
|
|
353
368
|
* rendered screen: accumulated output comes back in fragments (tech notes,
|
|
354
369
|
* section 1).
|
|
355
370
|
*/
|
|
356
|
-
function screenOf(handle) {
|
|
371
|
+
function screenOf(handle, readMs) {
|
|
357
372
|
let read;
|
|
358
373
|
try {
|
|
359
|
-
read = orca(['terminal', 'read', '--terminal', handle, '--screen']).terminal;
|
|
374
|
+
read = orca(['terminal', 'read', '--terminal', handle, '--screen'], { timeoutMs: readMs }).terminal;
|
|
360
375
|
} catch (error) {
|
|
361
376
|
return { screenUnreadable: `Orca would not read its screen: ${error.message}` };
|
|
362
377
|
}
|
|
@@ -369,6 +384,9 @@ function screenOf(handle) {
|
|
|
369
384
|
/** A row the harness starts with its selection pointer: `›` on Codex, `❯` on Claude Code. */
|
|
370
385
|
const POINTER_ROW = /^ *[›❯]/;
|
|
371
386
|
|
|
387
|
+
/** How a harness's form says it is answered, as Claude Code 2.1.283 draws its foot (#416). */
|
|
388
|
+
const FORM_FOOT = /Enter to (?:continue|confirm|select)\b.*Esc to cancel/i;
|
|
389
|
+
|
|
372
390
|
/** That pointer on a numbered choice, and the column its number starts in. */
|
|
373
391
|
const ON_A_CHOICE = /^( *[›❯] +)\d+\. /;
|
|
374
392
|
|
|
@@ -376,7 +394,7 @@ const ON_A_CHOICE = /^( *[›❯] +)\d+\. /;
|
|
|
376
394
|
* Whether the rows of a rendered screen hold a question of the harness's own.
|
|
377
395
|
*
|
|
378
396
|
* Every one seen is a numbered list of choices with the harness's pointer on
|
|
379
|
-
* one (tech notes, section 1; ADR
|
|
397
|
+
* one (tech notes, section 1; ADR 0031). The same pointer starts the harness's
|
|
380
398
|
* input line and its echo of the user's past turns, and the input line is the
|
|
381
399
|
* lowest of them whenever it is on screen, so only the lowest pointer row is
|
|
382
400
|
* asked about: a question counts while it stands in the input line's place.
|
|
@@ -385,6 +403,13 @@ const ON_A_CHOICE = /^( *[›❯] +)\d+\. /;
|
|
|
385
403
|
*/
|
|
386
404
|
export function questionIn(rows) {
|
|
387
405
|
const at = rows.findLastIndex((row) => POINTER_ROW.test(row));
|
|
406
|
+
// A form of the harness's own has no numbers but a foot that says how to
|
|
407
|
+
// answer it: Claude Code 2.1.283's "Teach auto mode" ends `Enter to continue
|
|
408
|
+
// · Esc to cancel`, and a return there is Continue, which starts a scan
|
|
409
|
+
// (#416). It counts on or below the lowest pointer row, or anywhere when
|
|
410
|
+
// there is none: under old words of one in the history, the input line is
|
|
411
|
+
// the lowest pointer, and nothing below it is a foot.
|
|
412
|
+
if (rows.slice(Math.max(at, 0)).some((row) => FORM_FOOT.test(row))) return true;
|
|
388
413
|
const pointer = at < 0 ? null : ON_A_CHOICE.exec(rows[at]);
|
|
389
414
|
if (pointer === null) return false;
|
|
390
415
|
const column = pointer[1].length;
|
|
@@ -408,10 +433,18 @@ export function questionIn(rows) {
|
|
|
408
433
|
*/
|
|
409
434
|
export function tabToTypeInto(home, tabId, timeoutMs) {
|
|
410
435
|
if (tabId === undefined) return {};
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
436
|
+
// Every call to Orca here has a bound, so one slow reply cannot hold the
|
|
437
|
+
// command that asked: one that runs out is "cannot tell" (#422).
|
|
438
|
+
let live;
|
|
439
|
+
let seen;
|
|
440
|
+
try {
|
|
441
|
+
live = tabs(home, { timeoutMs: GATE_READ_MS }).find((tab) => tab.tabId === tabId);
|
|
442
|
+
if (live === undefined) return {};
|
|
443
|
+
seen = harnessInTab(live.handle, timeoutMs, GATE_READ_MS);
|
|
444
|
+
} catch (error) {
|
|
445
|
+
if (error.code !== TIMED_OUT) throw error;
|
|
446
|
+
return { unsure: `Orca did not answer in time (${error.message}), so the kit could not tell whether a harness is running in it, and nothing was typed` };
|
|
447
|
+
}
|
|
415
448
|
// A line typed into a screen waiting for an answer is that answer: once, it
|
|
416
449
|
// confirmed Claude Code's folder-trust default, `No, exit` (tech notes,
|
|
417
450
|
// section 1).
|
|
@@ -431,9 +464,12 @@ export function tabToTypeInto(home, tabId, timeoutMs) {
|
|
|
431
464
|
// there. The program in front has to be the agent Orca names. With no name
|
|
432
465
|
// it may be a harness a few seconds into its launch; under another name it
|
|
433
466
|
// may be a pager started after the harness quit, under an identity Orca has
|
|
434
|
-
// not let go of (tech notes, section 1). A harness run
|
|
435
|
-
// as `node`
|
|
436
|
-
|
|
467
|
+
// not let go of (tech notes, section 1). A harness run under another name,
|
|
468
|
+
// such as `node` for an npm install, passes on the kit's own mark instead
|
|
469
|
+
// (#261).
|
|
470
|
+
// Either way Orca has to name an agent there: a harness it has not named yet
|
|
471
|
+
// may be seconds into its launch and not ready for a line.
|
|
472
|
+
if (seen.front === undefined || seen.agent === undefined || (seen.command !== seen.agent && !launchedHere(seen.pid, tabId))) {
|
|
437
473
|
const why = seen.front === undefined
|
|
438
474
|
? seen.unreadable
|
|
439
475
|
: `${seen.command} holds its terminal, and Orca names ${seen.agent ?? 'no agent'} in it`;
|
|
@@ -449,6 +485,25 @@ export function tabToTypeInto(home, tabId, timeoutMs) {
|
|
|
449
485
|
return { handle: live.handle, agent: seen.agent };
|
|
450
486
|
}
|
|
451
487
|
|
|
488
|
+
/**
|
|
489
|
+
* Whether the process `pid`, in front of the tab `tabId`, is the harness the
|
|
490
|
+
* kit's launch line started there, whatever it is called (#261). The line sets
|
|
491
|
+
* `OBK_TAB_SHELL` to the tab's shell's pid, for the harness alone and not
|
|
492
|
+
* exported, so the harness carries it, with its parent that shell. A program
|
|
493
|
+
* the shell starts later, such as `less` after the harness quit (#232), has no
|
|
494
|
+
* mark. A program the harness starts carries the mark, and its parent is the
|
|
495
|
+
* harness. A tab Orca restored by itself has no mark at all. Only whole words
|
|
496
|
+
* of the environment count, and anything unread is a no.
|
|
497
|
+
*/
|
|
498
|
+
function launchedHere(pid, tabId) {
|
|
499
|
+
if (pid === undefined) return false;
|
|
500
|
+
const words = wordsOfProcess(pid);
|
|
501
|
+
if (words === undefined || !words.includes(`${TAB_ENV}=${tabId}`)) return false;
|
|
502
|
+
const marks = words.filter((word) => new RegExp(`^${SHELL_ENV}=[0-9]+$`).test(word));
|
|
503
|
+
const own = psLine(pid);
|
|
504
|
+
return marks.length === 1 && own !== undefined && Number(marks[0].slice(SHELL_ENV.length + 1)) === own.ppid;
|
|
505
|
+
}
|
|
506
|
+
|
|
452
507
|
/**
|
|
453
508
|
* Who holds the terminal of the tab `handle`, as `frontOf` answers, without
|
|
454
509
|
* waiting on the tab: for a caller that asks about every tab and has nothing
|
|
@@ -475,10 +530,10 @@ const psCli = () => process.env.OBK_PS || '/bin/ps';
|
|
|
475
530
|
*
|
|
476
531
|
* `ps` is asked first. Where it cannot read the tab, as inside Codex's
|
|
477
532
|
* sandbox, where it does not start at all, Orca's runtime is asked instead
|
|
478
|
-
* (#298, ADR
|
|
533
|
+
* (#298, ADR 0031).
|
|
479
534
|
*/
|
|
480
|
-
function frontOf(handle, ptyId) {
|
|
481
|
-
const read = frontByPs(ptyId);
|
|
535
|
+
function frontOf(handle, ptyId, readMs) {
|
|
536
|
+
const read = frontByPs(ptyId, readMs);
|
|
482
537
|
if (read.unreadable === undefined) return read;
|
|
483
538
|
const asked = frontByOrca(handle);
|
|
484
539
|
return asked.unreadable === undefined ? asked : { unreadable: `${read.unreadable}, and ${asked.unreadable}` };
|
|
@@ -511,19 +566,23 @@ function frontByOrca(handle) {
|
|
|
511
566
|
* Who holds the terminal of the pane `ptyId`, as `ps` says it.
|
|
512
567
|
*
|
|
513
568
|
* Orca gives the pane's pid in `diagnostics memory` and nowhere else, and `ps`
|
|
514
|
-
* gives that pid's terminal's foreground process group (ADR
|
|
569
|
+
* gives that pid's terminal's foreground process group (ADR 0031).
|
|
515
570
|
* On macOS the pane is `login` with the shell as its child, so the shell is in
|
|
516
571
|
* front when the group is the pane's own or that of a child of a `login` pane.
|
|
517
572
|
* `diagnostics memory` is a diagnostics command and may change, so everything
|
|
518
573
|
* here ends in "cannot tell" rather than in an error or a guess.
|
|
519
574
|
*/
|
|
520
|
-
function frontByPs(ptyId) {
|
|
575
|
+
function frontByPs(ptyId, readMs) {
|
|
521
576
|
let pane;
|
|
522
577
|
try {
|
|
523
|
-
pane = orca(['diagnostics', 'memory']).worktrees
|
|
578
|
+
pane = orca(['diagnostics', 'memory'], { timeoutMs: readMs }).worktrees
|
|
524
579
|
.flatMap((worktree) => worktree.sessions ?? [])
|
|
525
580
|
.find((session) => session.sessionId === ptyId)?.pid;
|
|
526
581
|
} catch (error) {
|
|
582
|
+
// Orca not answering within the bound the typing gate set is not a tab
|
|
583
|
+
// `ps` cannot read: it ends the gate as "cannot tell", with no fallback to
|
|
584
|
+
// the runtime (review of PR #424).
|
|
585
|
+
if (error.code === TIMED_OUT) throw error;
|
|
527
586
|
return { unreadable: `Orca would not give the pane's pid: ${error.message}` };
|
|
528
587
|
}
|
|
529
588
|
if (!Number.isInteger(pane) || pane <= 0) return { unreadable: 'Orca gave no pid for its pane' };
|
|
@@ -563,14 +622,25 @@ function psLine(pid) {
|
|
|
563
622
|
/**
|
|
564
623
|
* Close one tab, by the handle Orca issued for it.
|
|
565
624
|
*
|
|
566
|
-
*
|
|
567
|
-
* tab: `--terminal <handle> --tab`. Orca's other
|
|
625
|
+
* With `closeTerminal` below, the only place in the kit that takes anything
|
|
626
|
+
* away, and each takes away one tab: `--terminal <handle> --tab`. Orca's other
|
|
627
|
+
* form, `--worktree <sel> --all`,
|
|
568
628
|
* closes every tab of a project with its layouts and resume records, which are
|
|
569
629
|
* the user's work and not the kit's to end (tech notes, section 1).
|
|
570
630
|
*/
|
|
571
631
|
export const closeTab = (handle) =>
|
|
572
632
|
orca(['terminal', 'close', '--terminal', handle, '--tab']).close;
|
|
573
633
|
|
|
634
|
+
/**
|
|
635
|
+
* Close one terminal without `--tab`: for a tab of one pane, as every tab the
|
|
636
|
+
* kit opens is, Orca then closes that tab by its id and stops its process (read
|
|
637
|
+
* in the 1.4.215 bundle). Only for a tab `--tab` could not find (#405): Orca
|
|
638
|
+
* looks a `--tab` close up in a tab snapshot of its own, which a tab it still
|
|
639
|
+
* lists can be missing from after a restart, and answers `tab_not_found`.
|
|
640
|
+
*/
|
|
641
|
+
export const closeTerminal = (handle) =>
|
|
642
|
+
orca(['terminal', 'close', '--terminal', handle]).close;
|
|
643
|
+
|
|
574
644
|
/**
|
|
575
645
|
* Type `text` into a tab and press return.
|
|
576
646
|
*
|
package/src/pause.js
CHANGED
|
@@ -14,7 +14,7 @@ import { realpathSync } from 'node:fs';
|
|
|
14
14
|
|
|
15
15
|
import { botDir, markPaused, readBot } from './bot.js';
|
|
16
16
|
import { DAILY_SESSION } from './init.js';
|
|
17
|
-
import { closeTabs, tabsToClose } from './restart.js';
|
|
17
|
+
import { closeTabs, commandLine, tabsToClose } from './restart.js';
|
|
18
18
|
import { BOT_FATHER, botsNamed, bringUp, sessionsOf } from './up.js';
|
|
19
19
|
|
|
20
20
|
/**
|
|
@@ -30,7 +30,7 @@ export async function pauseSessions(bots, { bot, session }) {
|
|
|
30
30
|
// they were.
|
|
31
31
|
const going = tabsToClose(bots, bot, home, sessions);
|
|
32
32
|
const changed = markPaused(bots, bot, session, true);
|
|
33
|
-
const closed = await closeTabs(home, going, bots, bot);
|
|
33
|
+
const closed = await closeTabs(home, going, bots, bot, commandLine('pause', bots, bot, session));
|
|
34
34
|
return { bot, session, changed, closed };
|
|
35
35
|
}
|
|
36
36
|
|
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, findProject, frontOfTab, tabs } from './orca.js';
|
|
37
|
+
import { closeTab, closeTerminal, findProject, frontOfTab, orcaCli, tabs } from './orca.js';
|
|
38
38
|
import { botsNamed, bringUp, prepareBots, sessionsOf } from './up.js';
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -68,7 +68,7 @@ export async function restartSessions(bots, { bot: name, session: onlySession }
|
|
|
68
68
|
}
|
|
69
69
|
|
|
70
70
|
const going = tabsToClose(bots, name, home, sessions.filter((session) => session.paused !== true), { quit: true });
|
|
71
|
-
const closed = await closeTabs(home, going, bots, name);
|
|
71
|
+
const closed = await closeTabs(home, going, bots, name, commandLine('restart', bots, name, onlySession));
|
|
72
72
|
return { closed, ...(await bringUp(bots, { bot: name, session: onlySession })) };
|
|
73
73
|
}
|
|
74
74
|
|
|
@@ -113,12 +113,14 @@ export function tabsToClose(bots, name, home, sessions, { keepless = false, quit
|
|
|
113
113
|
/**
|
|
114
114
|
* Close the tabs `tabsToClose` gave, one at a time by their own handles, and
|
|
115
115
|
* wait until Orca's listing agrees they are gone. Returns one entry per tab.
|
|
116
|
+
* `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.
|
|
116
118
|
*/
|
|
117
|
-
export async function closeTabs(home, going, bots, name) {
|
|
119
|
+
export async function closeTabs(home, going, bots, name, again) {
|
|
118
120
|
const closed = [];
|
|
119
121
|
for (const { name: session, tab } of going) {
|
|
120
122
|
try {
|
|
121
|
-
|
|
123
|
+
closeWhole(tab.handle, again);
|
|
122
124
|
} catch (error) {
|
|
123
125
|
// A tab that would not close still holds its session, so nothing is
|
|
124
126
|
// opened after this: two harnesses on one conversation is worse than a
|
|
@@ -132,6 +134,41 @@ export async function closeTabs(home, going, bots, name) {
|
|
|
132
134
|
return closed;
|
|
133
135
|
}
|
|
134
136
|
|
|
137
|
+
/**
|
|
138
|
+
* Close one session's tab. Orca finds a `--tab` close in a tab snapshot of its
|
|
139
|
+
* own, which a tab it still lists can be missing from after a restart, and then
|
|
140
|
+
* answers `tab_not_found` (#405, read in the 1.4.215 bundle). The same terminal
|
|
141
|
+
* closed without `--tab` closes its one-pane tab by id, so that is done instead,
|
|
142
|
+
* and `gone` checks the tab went as it does after any close. Every other
|
|
143
|
+
* refusal stops the restart as it did.
|
|
144
|
+
*/
|
|
145
|
+
function closeWhole(handle, again) {
|
|
146
|
+
try {
|
|
147
|
+
closeTab(handle);
|
|
148
|
+
} catch (error) {
|
|
149
|
+
if (!notFound(error)) throw error;
|
|
150
|
+
try {
|
|
151
|
+
closeTerminal(handle);
|
|
152
|
+
} catch (second) {
|
|
153
|
+
// With the tab closed by hand there is nothing left for the command to
|
|
154
|
+
// close, so running it again finishes it, whichever command it was.
|
|
155
|
+
throw new Error(`${error.message}, and then ${second.message}. Close it by hand, then run the same command again: ${shellWord(orcaCli())} terminal close --terminal ${shellWord(handle)} then ${again}`);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Whether Orca refused a `--tab` close for a tab it could not find: its code,
|
|
162
|
+
* or its 1.4.215 form of code `runtime_error` with exactly that message. Orca's
|
|
163
|
+
* own words, never the kit's wrapped text, and no other form (#405).
|
|
164
|
+
*/
|
|
165
|
+
const notFound = (error) =>
|
|
166
|
+
error.code === 'tab_not_found' || (error.code === 'runtime_error' && error.reason === 'tab_not_found');
|
|
167
|
+
|
|
168
|
+
/** A command of the kit's as the user would type it, for a message to name. */
|
|
169
|
+
export const commandLine = (verb, bots, bot, session) =>
|
|
170
|
+
`${shellWord(ownCli())} ${verb} --bots ${shellWord(bots)} --bot ${bot}${session === undefined ? '' : ` --session ${session}`}`;
|
|
171
|
+
|
|
135
172
|
/** How long Orca is given to stop listing a tab it has closed, and how often it is asked. */
|
|
136
173
|
const SETTLED_MS = 5000;
|
|
137
174
|
const ASK_MS = 100;
|
package/src/retire.js
CHANGED
|
@@ -26,7 +26,7 @@ import { readBook, tabIdsIn, updateBook } from './book.js';
|
|
|
26
26
|
import { botDir, dropSession, readBot } from './bot.js';
|
|
27
27
|
import { deleteProject, findProject, projects, reloadWindow, tabs } from './orca.js';
|
|
28
28
|
import { fleetMember } from './pause.js';
|
|
29
|
-
import { closeTabs, tabsToClose } from './restart.js';
|
|
29
|
+
import { closeTabs, commandLine, tabsToClose } from './restart.js';
|
|
30
30
|
import { unlinkSkills } from './skills.js';
|
|
31
31
|
import { promptPath, sessionsOf } from './up.js';
|
|
32
32
|
|
|
@@ -41,7 +41,7 @@ export async function retireSession(bots, { bot, session }) {
|
|
|
41
41
|
const home = fleetMember(bots, bot, 'retire', session);
|
|
42
42
|
const sessions = sessionsOf(readBot(home, bot), session);
|
|
43
43
|
|
|
44
|
-
const closed = await closeTabs(home, tabsToClose(bots, bot, home, sessions, { keepless: true }), bots, bot);
|
|
44
|
+
const closed = await closeTabs(home, tabsToClose(bots, bot, home, sessions, { keepless: true }), bots, bot, commandLine('retire', bots, bot, session));
|
|
45
45
|
dropSession(bots, bot, session);
|
|
46
46
|
|
|
47
47
|
const at = new Date().toISOString();
|
|
@@ -86,7 +86,7 @@ export async function retireBot(bots, { bot }) {
|
|
|
86
86
|
// taken away with one of them still open. `closeTabs` waits until Orca agrees
|
|
87
87
|
// they are gone.
|
|
88
88
|
const booked = Object.keys(readBook(home).sessions).map((name) => ({ name }));
|
|
89
|
-
const closed = await closeTabs(home, tabsToClose(bots, bot, home, booked, { keepless: true }), bots, bot);
|
|
89
|
+
const closed = await closeTabs(home, tabsToClose(bots, bot, home, booked, { keepless: true }), bots, bot, commandLine('retire', bots, bot));
|
|
90
90
|
let windowReloaded;
|
|
91
91
|
if (project !== undefined) {
|
|
92
92
|
deleteProject(project.id);
|
package/src/up.js
CHANGED
|
@@ -9,12 +9,12 @@ import path from 'node:path';
|
|
|
9
9
|
import { setTimeout as pause } from 'node:timers/promises';
|
|
10
10
|
|
|
11
11
|
import { forgetClaimed, forgetSession, MAILBOX_WAIT_MS, readBook, sessionIdsIn, tabIdsIn, takeMailboxTurn, updateBook, withUnclaimed } from './book.js';
|
|
12
|
-
import { botDir, botNames, displayName, readBot } from './bot.js';
|
|
12
|
+
import { botDir, botNames, displayName, projectName, readBot } from './bot.js';
|
|
13
13
|
import { conversationsIn, hasConversation, heldAsUserTurn, transcriptsIn } from './conversations.js';
|
|
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, TERMINAL_ENV, TIMED_OUT, tellWindow, typeIntoTab, useMailbox } from './orca.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';
|
|
18
18
|
import { TAB_ENV } from './record.js';
|
|
19
19
|
import { buildAgents, rulesStamp } from './rules.js';
|
|
20
20
|
import { linkSkills } from './skills.js';
|
|
@@ -211,7 +211,7 @@ async function bringUpBot(bots, home, bot, onlySession) {
|
|
|
211
211
|
// Orca is asked first and the book is written after: nothing that takes time
|
|
212
212
|
// happens while the book is held, because a session's own hook may be writing
|
|
213
213
|
// its id into that same file at any moment.
|
|
214
|
-
const { change, ...orca } = orcaProject(home,
|
|
214
|
+
const { change, ...orca } = orcaProject(home, projectName(bots, name));
|
|
215
215
|
await updateBook(home, (book) => { book.orca = orca; });
|
|
216
216
|
// Orca's window does not see a project made or renamed on the command line
|
|
217
217
|
// until it is told (#224).
|
|
@@ -369,6 +369,14 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
369
369
|
? undefined
|
|
370
370
|
: await heldInRecord(home, session.name, harness, launch.prompt, { launched, running: tui.running === true });
|
|
371
371
|
|
|
372
|
+
// A Codex session resumed here is not among Orca's agents until its first
|
|
373
|
+
// turn: Codex fires its SessionStart then and not at the resume, seen live on
|
|
374
|
+
// 0.157.1 (#226). So it is given one line, through the same gate as a mail
|
|
375
|
+
// notice, which the owner allowed. A fresh start's first turn is its prompt.
|
|
376
|
+
const list = harness === 'codex' && launch.resume !== undefined && tui.running === true
|
|
377
|
+
? await typeListLine(home, made.tabId)
|
|
378
|
+
: undefined;
|
|
379
|
+
|
|
372
380
|
return entry(made, {
|
|
373
381
|
bot: bot.name,
|
|
374
382
|
name: session.name,
|
|
@@ -382,9 +390,60 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
382
390
|
resumed: launch.resume !== undefined,
|
|
383
391
|
noConversation: which.noConversation,
|
|
384
392
|
unclaimed: which.unclaimed,
|
|
393
|
+
listLine: list?.typed,
|
|
394
|
+
listLineTrouble: list?.why,
|
|
385
395
|
});
|
|
386
396
|
}
|
|
387
397
|
|
|
398
|
+
/** The line a resumed Codex session is typed so Orca lists it, in the architect's words (#226). */
|
|
399
|
+
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
|
+
|
|
401
|
+
/**
|
|
402
|
+
* How long a resumed Codex tab is given to be one the kit may type into: live,
|
|
403
|
+
* Orca named the agent 7 to 11 s after the restart (#226), and the gate waits
|
|
404
|
+
* for that name. How long each look waits on the tab, and the pause between.
|
|
405
|
+
*/
|
|
406
|
+
const LIST_WAIT_MS = 20000;
|
|
407
|
+
const LIST_LOOK_MS = 2000;
|
|
408
|
+
const LIST_ASK_MS = 500;
|
|
409
|
+
|
|
410
|
+
/**
|
|
411
|
+
* Type LIST_LINE into the tab `tabId` once the gate every typed line goes
|
|
412
|
+
* through lets it: `{ typed: true }`, or `{ typed: false, why }` with the
|
|
413
|
+
* gate's last reason once LIST_WAIT_MS is up. Nothing goes into a question, a
|
|
414
|
+
* form or a menu (#329, #416), nor into a tab the kit cannot tell about.
|
|
415
|
+
*/
|
|
416
|
+
async function typeListLine(home, tabId) {
|
|
417
|
+
const until = Date.now() + LIST_WAIT_MS;
|
|
418
|
+
for (;;) {
|
|
419
|
+
let found;
|
|
420
|
+
try {
|
|
421
|
+
// Each look waits no longer than what is left of the wait.
|
|
422
|
+
found = tabToTypeInto(home, tabId, Math.max(1, Math.min(LIST_LOOK_MS, until - Date.now())));
|
|
423
|
+
} catch (error) {
|
|
424
|
+
return { typed: false, why: `Orca would not say whether it may be typed into: ${error.message}` };
|
|
425
|
+
}
|
|
426
|
+
// A yes that comes after the wait is too late: the wait is a cutoff, not a
|
|
427
|
+
// count of looks (review of PR #421).
|
|
428
|
+
if (found.handle !== undefined && Date.now() > until) {
|
|
429
|
+
return { typed: false, why: `the ${LIST_WAIT_MS / 1000} s wait for it ran out before the kit could tell it may be typed into` };
|
|
430
|
+
}
|
|
431
|
+
if (found.handle !== undefined) {
|
|
432
|
+
try {
|
|
433
|
+
typeIntoTab(found.handle, LIST_LINE);
|
|
434
|
+
return { typed: true };
|
|
435
|
+
} catch (error) {
|
|
436
|
+
return { typed: false, why: `Orca refused the line: ${error.message}` };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
if (Date.now() >= until) {
|
|
440
|
+
const last = found.blocked !== undefined ? `it is waiting on ${found.blocked}` : found.unsure ?? 'no harness is running in it';
|
|
441
|
+
return { typed: false, why: `the ${LIST_WAIT_MS / 1000} s wait for it ran out: ${last}` };
|
|
442
|
+
}
|
|
443
|
+
await pause(LIST_ASK_MS);
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
388
447
|
/**
|
|
389
448
|
* The line that starts this session: resuming `resume`, or, with none, a fresh
|
|
390
449
|
* start carrying its duty. Returns `{ resume, prompt, promptFile, command, address }`.
|
|
@@ -714,11 +773,15 @@ const ids = (setup, change) => ({ project: setup.projectId, setup: setup.id, cha
|
|
|
714
773
|
export const promptPath = (bots, bot, session) =>
|
|
715
774
|
path.join(`${bots}.prompts`, `${encodeURIComponent(bot)}.${encodeURIComponent(session)}.txt`);
|
|
716
775
|
|
|
717
|
-
function entry(tab, { bot, name, created, running = false, blockedReason, promptReceived, promptFile, resumed, noConversation, unclaimed, noMailbox = false }) {
|
|
776
|
+
function entry(tab, { bot, name, created, running = false, blockedReason, promptReceived, promptFile, resumed, noConversation, unclaimed, noMailbox = false, listLine, listLineTrouble }) {
|
|
718
777
|
const made = { bot, name, title: tab.title, tabId: tab.tabId, terminal: tab.handle, created, harnessStarted: running };
|
|
719
778
|
// Whether this run picked the session up where it was or started a new one.
|
|
720
779
|
// Only for a tab this run opened: a tab that was already there was left alone.
|
|
721
780
|
if (resumed !== undefined) made.resumed = resumed;
|
|
781
|
+
// Whether a resumed Codex session was typed its one line so Orca lists it,
|
|
782
|
+
// and why not when it was not (#226).
|
|
783
|
+
if (listLine !== undefined) made.listLine = listLine;
|
|
784
|
+
if (listLineTrouble !== undefined) made.listLineTrouble = listLineTrouble;
|
|
722
785
|
// The id the book held that had no conversation behind it, now in the history.
|
|
723
786
|
if (noConversation !== undefined) made.noConversation = noConversation;
|
|
724
787
|
// Orca's own words for what is on screen waiting to be answered, when it
|