@assuredloop/orca-bot-kit 0.35.0 → 0.36.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/README.md CHANGED
@@ -403,6 +403,18 @@ bot's sessions, set `"auto-mode-setup": "on"` under `skillOverrides` in the bot'
403
403
  `.claude/settings.local.json`, which wins over the kit's entry. The kit never
404
404
  writes that file.
405
405
 
406
+ Under an Orca Claude account, the kit's line for a Claude session also carries
407
+ `--settings` with one key, `autoMemoryDirectory`. It names the physical folder
408
+ behind the account's linked `projects` folder, so a session saves its memory
409
+ with no permission dialog. The memory is the same memory, and the kit writes no
410
+ file for this. A bot, its repository or an account that names its own
411
+ `autoMemoryDirectory` keeps it. A conversation that began before this keeps its old path until the
412
+ session restarts under this version and then begins a new conversation with
413
+ `/clear`. A restart alone or a `/clear` alone does not change the path. After
414
+ Orca brings a tab back by itself, a `/clear` before a
415
+ restart brings the dialog back; `obk health` names such a session
416
+ ([ADR 0054](docs/adr/0054-the-kits-launch-line-names-the-physical-memory-folder.md)).
417
+
406
418
  So: kill a tab, or reboot, and `obk up` brings the session back with the
407
419
  conversation it was having, rather than starting a new one. Clear a session —
408
420
  `/clear` on Claude Code, `/new` on Codex — and the kit writes down the new id,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@assuredloop/orca-bot-kit",
3
- "version": "0.35.0",
3
+ "version": "0.36.1",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -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}`]),
@@ -20,7 +20,7 @@ import { eachLine, firstLine } from './lines.js';
20
20
  const claudeDir = (home) => path.join(homedir(), '.claude', 'projects', slug(home));
21
21
 
22
22
  /** The folder's name is the working directory with everything else turned into a dash. */
23
- const slug = (home) => home.replaceAll(/[^A-Za-z0-9]/g, '-');
23
+ export const slug = (home) => home.replaceAll(/[^A-Za-z0-9]/g, '-');
24
24
 
25
25
  /**
26
26
  * Pi's sessions: one folder per working directory, named from its real path
package/src/launch.js CHANGED
@@ -294,11 +294,12 @@ const NO_DAEMON = '--no-daemon';
294
294
 
295
295
  // A string is shell text, so it is read as the shell will split it: `'--no-daemon'`
296
296
  // is the flag, and `'/tmp/a --no-daemon b'` is one path that merely says it.
297
- const setsNoDaemon = (session) => {
298
- const extra = session.extra_args;
299
- const words = Array.isArray(extra) ? extra.map(String) : set(extra) ? shellWords(String(extra)) : [];
300
- return words.includes(NO_DAEMON);
301
- };
297
+ const extraArgWords = (extra) => (Array.isArray(extra) ? extra.map(String) : set(extra) ? shellWords(String(extra)) : []);
298
+
299
+ const setsNoDaemon = (session) => extraArgWords(session.extra_args).includes(NO_DAEMON);
300
+
301
+ /** Whether the session's own extra arguments hold Claude Code's `--settings`. */
302
+ const setsSettings = (session) => extraArgWords(session.extra_args).some((word) => word === '--settings' || word.startsWith('--settings='));
302
303
 
303
304
  /**
304
305
  * The words a shell makes of `text`: split on blanks outside quotes, quotes
@@ -412,7 +413,7 @@ const where = (session) => (set(session.name) ? `session ${session.name}` : 'thi
412
413
  * send would land on that list and answer it. A prompt given as an argument is
413
414
  * held by the harness until it is ready for it (tech notes, section 1).
414
415
  */
415
- export function launchCommand(session, { harness, home, workDir, prompt, promptFile: fromFile, resume, address, kit, sessionId, name }) {
416
+ export function launchCommand(session, { harness, home, workDir, prompt, promptFile: fromFile, resume, address, kit, sessionId, name, memoryFolder }) {
416
417
  const trouble = sessionTrouble(session, harness, home);
417
418
  if (trouble !== undefined) throw new Error(trouble);
418
419
  // Pi is told which conversation it is, a new one too: the kit names it, and
@@ -440,6 +441,10 @@ export function launchCommand(session, { harness, home, workDir, prompt, promptF
440
441
  // The context window rides on the model name: `sonnet[1m]`.
441
442
  if (set(session.model)) words.push('--model', set(session.context) ? `${session.model}[${session.context}]` : session.model);
442
443
  if (set(session.effort)) words.push('--effort', String(session.effort));
444
+ // This one key and no other: `--settings` can carry rules, folders and
445
+ // hooks, which is why a user's extra argument may not use it (ADR 0054,
446
+ // ADR 0049). A hand-written one in bot.yaml is the user's, and wins.
447
+ if (memoryFolder !== undefined && !setsSettings(session)) words.push('--settings', JSON.stringify({ autoMemoryDirectory: memoryFolder }));
443
448
  } else if (harness === 'pi') {
444
449
  // Pi trusts none of the folder's own files (`.pi/`, `.agents/skills`), so it
445
450
  // shows no trust screen and runs nothing the kit has not looked at (#578).
package/src/memory.js ADDED
@@ -0,0 +1,113 @@
1
+ // The physical memory folder of a Claude session, when the path Claude Code
2
+ // would use leads through a link (#722, ADR 0054).
3
+ //
4
+ // Orca starts a Claude tab with CLAUDE_CONFIG_DIR at a profile of its own,
5
+ // where `projects` is a link to `~/.claude/projects`. Claude Code names its
6
+ // memory folder through that link, and a write there stops on a dialog that
7
+ // only a person can answer (tech notes, section 2). Named by its real path, the
8
+ // same folder has no dialog. Whenever the answer is not sure, there is no
9
+ // answer, and the session runs as it did: a dialog costs a wait, and a wrong
10
+ // folder costs the session its memory.
11
+
12
+ import { spawnSync } from 'node:child_process';
13
+ import { existsSync, lstatSync, readFileSync, realpathSync, statSync } from 'node:fs';
14
+ import path from 'node:path';
15
+
16
+ import { slug } from './conversations.js';
17
+ import { selectedClaudeProfile } from './orca.js';
18
+
19
+ /** The longest slug Claude Code uses as it is. Past this it adds a hash of its own. */
20
+ const LONGEST_SLUG = 200;
21
+
22
+ /**
23
+ * The folder to name for a session of the bot at `home`, or undefined when the
24
+ * kit should name none: no profile is routed, its `projects` is no link, the
25
+ * bot or the profile names a folder of its own, the project root is not sure,
26
+ * or the slug is too long.
27
+ */
28
+ export function physicalMemoryFolder(home) {
29
+ const profile = selectedClaudeProfile();
30
+ if (profile === undefined) return undefined;
31
+ const projects = linkedFolder(path.join(profile, 'projects'));
32
+ if (projects === undefined) return undefined;
33
+ const root = projectRoot(home);
34
+ if (root === undefined) return undefined;
35
+ // Claude Code reads a project's local settings from the top of its repository as well as from the bot's folder.
36
+ const named = [
37
+ path.join(home, '.claude', 'settings.json'),
38
+ path.join(home, '.claude', 'settings.local.json'),
39
+ path.join(root, '.claude', 'settings.local.json'),
40
+ path.join(profile, 'settings.json'),
41
+ ];
42
+ if (named.some(namesMemoryFolder)) return undefined;
43
+ // Claude Code holds its project root in NFC, whatever form the disk has.
44
+ const name = slug(root.normalize('NFC'));
45
+ return name.length > LONGEST_SLUG ? undefined : path.join(projects, name, 'memory');
46
+ }
47
+
48
+ /** The real path of the folder the link `entry` leads to, or undefined when it is not such a link. */
49
+ function linkedFolder(entry) {
50
+ try {
51
+ if (!lstatSync(entry).isSymbolicLink()) return undefined;
52
+ return statSync(entry).isDirectory() ? realpathSync(entry) : undefined;
53
+ } catch {
54
+ return undefined;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * Whether the settings file `file` may name a memory folder of its own: it has
60
+ * the key, or it is there and cannot be read, so the kit cannot tell.
61
+ */
62
+ function namesMemoryFolder(file) {
63
+ let text;
64
+ try {
65
+ text = readFileSync(file, 'utf8');
66
+ } catch (error) {
67
+ return error.code !== 'ENOENT' && error.code !== 'ENOTDIR';
68
+ }
69
+ try {
70
+ const settings = JSON.parse(text);
71
+ return settings !== null && typeof settings === 'object' && Object.hasOwn(settings, 'autoMemoryDirectory');
72
+ } catch {
73
+ return true;
74
+ }
75
+ }
76
+
77
+ /**
78
+ * The folder Claude Code keys memory by: the top of the git repository that
79
+ * holds `home`, the main repository's for a linked worktree, since every
80
+ * worktree of a repository shares one memory (tech notes, section 2). With no
81
+ * repository, it is `home` itself. Undefined when the kit is not sure: a `.git`
82
+ * is on the way and git cannot say what it is, a linked worktree's shared git
83
+ * folder is not named `.git`, or the `.git` at the top is a link.
84
+ */
85
+ function projectRoot(home) {
86
+ const shared = revParse(home, '--git-common-dir');
87
+ if (shared === undefined) return gitOnTheWay(home) ? undefined : realpathSync(home);
88
+ const own = revParse(home, '--git-dir');
89
+ if (own === undefined) return undefined;
90
+ const main = realpathSync(path.resolve(home, shared));
91
+ const linked = realpathSync(path.resolve(home, own)) !== main;
92
+ // Only a linked worktree tells its main repository by the shared git folder:
93
+ // `--separate-git-dir` puts a repository's own git folder away from its top.
94
+ const top = !linked ? revParse(home, '--show-toplevel') : path.basename(main) === '.git' ? path.dirname(main) : undefined;
95
+ if (top === undefined) return undefined;
96
+ return lstatSync(path.join(top, '.git'), { throwIfNoEntry: false })?.isSymbolicLink() ? undefined : top;
97
+ }
98
+
99
+ /** Whether `folder` or a folder above it holds a `.git` entry. */
100
+ function gitOnTheWay(folder) {
101
+ for (let at = realpathSync(folder); ; at = path.dirname(at)) {
102
+ if (existsSync(path.join(at, '.git'))) return true;
103
+ if (path.dirname(at) === at) return false;
104
+ }
105
+ }
106
+
107
+ /** One answer of `git rev-parse` in `folder`, or undefined when git gives none. */
108
+ function revParse(folder, question) {
109
+ const asked = spawnSync('git', ['-C', folder, 'rev-parse', question], { encoding: 'utf8' });
110
+ // Only git's record terminator comes off: trailing whitespace may be part of the folder's name.
111
+ const answer = (asked.stdout ?? '').replace(/\n$/, '');
112
+ return asked.status === 0 && answer !== '' ? answer : undefined;
113
+ }
package/src/message.js CHANGED
@@ -203,19 +203,21 @@ export async function sendMessage(bots, { to: target, from: sender, tab, subject
203
203
  const shown = await loaded(to);
204
204
  // The Escape goes first, so the turn has ended when the letter is in.
205
205
  const interrupted = interrupt ? interruptBusy(to) : {};
206
- const message = postMessage({
206
+ const posted = postMessage({
207
207
  to: road.address,
208
208
  from: `run:${from.mailbox}`,
209
209
  subject,
210
210
  body: written.body,
211
211
  thread,
212
212
  });
213
+ // Orca took it: sent, whatever else its answer says or leaves out (#730).
214
+ const message = posted?.message;
213
215
 
214
216
  return {
215
217
  ...answer,
216
218
  sent: true,
217
- id: message.id,
218
- thread: message.thread_id ?? thread,
219
+ id: message?.id ?? posted?.messageId,
220
+ thread: message?.thread_id ?? thread,
219
221
  file: written.file,
220
222
  ...interrupted,
221
223
  ...shown,
@@ -356,12 +358,14 @@ function mailOf(bots, who, peek, until) {
356
358
  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
359
  };
358
360
  }
359
- // Orca hands mail over a batch at a time and replays the oldest until it is
360
- // acknowledged; the acknowledgement answers with the next batch. So a read
361
- // takes every batch in turn, or newer mail waits behind an older one (#299).
362
- //
363
- // A batch is given back once it is acknowledged, and not before: one this
364
- // check stops short of acknowledging is handed over again to the next.
361
+ // Orca hands mail over a batch at a time, so a read takes every batch in
362
+ // turn, or newer mail waits behind an older one (#299). Up to 1.4.245 it
363
+ // replays the oldest until it is acknowledged, and the acknowledgement
364
+ // answers with the next batch: a batch this check stops short of
365
+ // acknowledging is handed over again to the next. From 1.4.246 a read
366
+ // acknowledges the batch it hands over and says so (`deliveryAcknowledged`),
367
+ // and an acknowledgement answers with no batch, so the next is read again
368
+ // (#729; read in the 1.4.246 and 1.4.255 source).
365
369
  const messages = [];
366
370
  const read = { bots, bot: who.bot, session: who.session, mailbox: who.mailbox, read: !peek, messages };
367
371
  let found = readMailbox(who.mailbox, { peek, handle }, timeLeft(until));
@@ -372,6 +376,24 @@ function mailOf(bots, who, peek, until) {
372
376
  return read;
373
377
  }
374
378
  const wait = timeLeft(until, false);
379
+ if (found.deliveryAcknowledged === true) {
380
+ // Read already, so shown whatever comes after.
381
+ messages.push(...batch);
382
+ if (wait === undefined) {
383
+ return { ...read, trouble: `${who.bot}/${who.session} has more mail than one check reads in ${HOLD_MS / 1000} seconds, so it stopped there. The rest is still waiting: check again.` };
384
+ }
385
+ try {
386
+ found = readMailbox(who.mailbox, { handle }, wait);
387
+ } catch (error) {
388
+ // A read Orca did not answer about may have taken the next batch all
389
+ // the same, and taken is read: that batch is not handed over again.
390
+ if (error.code === TIMED_OUT) {
391
+ 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.` };
392
+ }
393
+ return { ...read, trouble: `${error.message} The mail shown above is read, and the rest waits for the next check.` };
394
+ }
395
+ continue;
396
+ }
375
397
  if (wait === undefined) {
376
398
  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
399
  }
package/src/orca.js CHANGED
@@ -146,10 +146,25 @@ export function findProject(home) {
146
146
  * names its CLI: a build's folder is written only inside its own CLI, so the
147
147
  * kit cannot tell it from the app (#551).
148
148
  */
149
- export const profilesDir = () => path.join(
150
- process.env.OBK_ORCA_DATA || path.join(homedir(), 'Library', 'Application Support', 'orca'),
151
- 'profiles',
152
- );
149
+ export const profilesDir = () => path.join(orcaData(), 'profiles');
150
+
151
+ const orcaData = () => process.env.OBK_ORCA_DATA || path.join(homedir(), 'Library', 'Application Support', 'orca');
152
+
153
+ /**
154
+ * The home of the Claude profile Orca routes a new Claude tab to, or undefined
155
+ * when it routes none. Orca writes the path to `selected-host`, with nothing
156
+ * in it when no profile is routed, and removes the file when it has no accounts
157
+ * (read in the bundle of Orca 1.4.245, tech notes, section 2).
158
+ */
159
+ export function selectedClaudeProfile() {
160
+ let home;
161
+ try {
162
+ home = readFileSync(path.join(orcaData(), 'claude-profiles', 'selected-host'), 'utf8').replace(/\r?\n$/, '');
163
+ } catch {
164
+ return undefined;
165
+ }
166
+ return path.isAbsolute(home) ? home : undefined;
167
+ }
153
168
 
154
169
  /**
155
170
  * Orca's own default launch arguments, per profile it keeps:
@@ -1188,6 +1203,33 @@ export const pressEscape = (handle) =>
1188
1203
  /** Where Orca names the terminal a process runs in, in every pane it opens. */
1189
1204
  export const TERMINAL_ENV = 'ORCA_TERMINAL_HANDLE';
1190
1205
 
1206
+ /**
1207
+ * One `orca orchestration` command, answered in full. From 1.4.246 Orca prints
1208
+ * a short answer for each of these unless it is given `--full`, and the short
1209
+ * one leaves out what the kit reads: a Run's `id` and `coordinator_handle`, a
1210
+ * letter's `from_handle`, `created_at`, `thread_id`, `read` and `delivered_at`,
1211
+ * and a sent letter's `message` (#729, #730; read in the 1.4.246 and 1.4.255
1212
+ * source, and seen live on 1.4.255). Orca 1.4.245 and older do not know the
1213
+ * flag on any of them but `inbox`, and refuse it before they do anything, as
1214
+ * `deleteProject` finds with `--force` (#528). Only that refusal sends the
1215
+ * command again without it: one Orca took, or did not answer, is never sent a
1216
+ * second time. Once refused, the rest of this run sends no `--full`.
1217
+ */
1218
+ function orchestration(args, options) {
1219
+ if (fullKnown) {
1220
+ try {
1221
+ return orca([...args, '--full'], options);
1222
+ } catch (error) {
1223
+ if (error.code !== 'invalid_argument' || !error.reason?.startsWith('Unknown flag --full for command: ')) throw error;
1224
+ fullKnown = false;
1225
+ }
1226
+ }
1227
+ return orca(args, options);
1228
+ }
1229
+
1230
+ /** Whether this Orca may know `--full` on its orchestration commands: false once it has refused it. */
1231
+ let fullKnown = true;
1232
+
1191
1233
  /**
1192
1234
  * A mailbox of one session's own: an Orca Run, which is a name and an inbox and
1193
1235
  * nothing else — it schedules nothing and runs nobody. The objective is what a
@@ -1200,7 +1242,7 @@ export const TERMINAL_ENV = 'ORCA_TERMINAL_HANDLE';
1200
1242
  * tab and names no `--from` (tech notes, section 1; #317).
1201
1243
  */
1202
1244
  export const makeMailbox = (objective, options) =>
1203
- orca(['orchestration', 'run-create', '--objective', `obk ${objective}`], options).run.id;
1245
+ orchestration(['orchestration', 'run-create', '--objective', `obk ${objective}`], options).run.id;
1204
1246
 
1205
1247
  /**
1206
1248
  * Bind a mailbox to the terminal `handle`, which makes it the Run's coordinator
@@ -1211,7 +1253,7 @@ export const makeMailbox = (objective, options) =>
1211
1253
  * tab may name no other (#317).
1212
1254
  */
1213
1255
  export const useMailbox = (id, handle, options) =>
1214
- orca(['orchestration', 'run-use', '--id', id, ...(handle === undefined ? [] : ['--from', handle])], options).run;
1256
+ orchestration(['orchestration', 'run-use', '--id', id, ...(handle === undefined ? [] : ['--from', handle])], options).run;
1215
1257
 
1216
1258
  /**
1217
1259
  * The terminal a mailbox is bound to, or undefined when it has none. A closed
@@ -1219,7 +1261,7 @@ export const useMailbox = (id, handle, options) =>
1219
1261
  * nothing, only `run-use` does (tech notes, section 1).
1220
1262
  */
1221
1263
  export const coordinatorOf = (id, options) =>
1222
- orca(['orchestration', 'run-show', '--id', id], options).run.coordinator_handle ?? undefined;
1264
+ orchestration(['orchestration', 'run-show', '--id', id], options).run.coordinator_handle ?? undefined;
1223
1265
 
1224
1266
  /**
1225
1267
  * Whether this Orca has no Run of that id at all, as when the book was written
@@ -1230,29 +1272,36 @@ export const coordinatorOf = (id, options) =>
1230
1272
  */
1231
1273
  export function runMissing(id, options) {
1232
1274
  try {
1233
- orca(['orchestration', 'run-show', '--id', id], options);
1275
+ orchestration(['orchestration', 'run-show', '--id', id], options);
1234
1276
  return false;
1235
1277
  } catch (error) {
1236
1278
  return error.code === 'run_not_found';
1237
1279
  }
1238
1280
  }
1239
1281
 
1240
- /** Queue one message. `to` and `from` are mailboxes, written `run:<id>`. */
1282
+ /**
1283
+ * Queue one message. `to` and `from` are mailboxes, written `run:<id>`. Orca's
1284
+ * whole answer comes back: the letter it queued is its `message`, where Orca
1285
+ * says so. Once Orca has taken it, an answer the kit cannot read is no reason
1286
+ * to say it was not sent (#730).
1287
+ */
1241
1288
  export function postMessage({ to, from, subject, body, type = 'status', thread }) {
1242
1289
  const args = ['orchestration', 'send', '--to', to, '--subject', subject, '--body', body, '--type', type];
1243
1290
  if (from !== undefined) args.push('--from', from);
1244
1291
  if (thread !== undefined) args.push('--thread-id', thread);
1245
- return orca(args).message;
1292
+ return orchestration(args);
1246
1293
  }
1247
1294
 
1248
1295
  /**
1249
1296
  * What is waiting in a mailbox: the oldest batch that has not been
1250
- * acknowledged, or, peeking, whatever is unread without touching it. Read as
1297
+ * acknowledged, or, peeking, whatever is unread without touching it. From
1298
+ * Orca 1.4.246 a read that is not a peek acknowledges the batch it hands over,
1299
+ * and says so with `deliveryAcknowledged: true` (#729). Read as
1251
1300
  * the terminal `handle` when one is given, which is how a read works from a tab
1252
1301
  * bound to another Run; otherwise as this process's own terminal.
1253
1302
  */
1254
1303
  export const readMailbox = (id, { peek = false, handle } = {}, options) =>
1255
- orca(['orchestration', 'check', '--run', id, ...as(handle), ...(peek ? ['--peek'] : [])], options);
1304
+ orchestration(['orchestration', 'check', '--run', id, ...as(handle), ...(peek ? ['--peek'] : [])], options);
1256
1305
 
1257
1306
  /**
1258
1307
  * The newest `limit` messages in a mailbox, newest first, with every column
@@ -1263,11 +1312,14 @@ export const readMailbox = (id, { peek = false, handle } = {}, options) =>
1263
1312
  * cap in Orca, so the answer is taken whatever its size.
1264
1313
  */
1265
1314
  export const mailIn = (id, limit) =>
1266
- orca(['orchestration', 'inbox', '--terminal', `run:${id}`, '--limit', String(limit)], { whole: true }).messages ?? [];
1315
+ orchestration(['orchestration', 'inbox', '--terminal', `run:${id}`, '--limit', String(limit)], { whole: true }).messages ?? [];
1267
1316
 
1268
- /** Say a batch has been read. Orca answers with the batch after it. */
1317
+ /**
1318
+ * Say a batch has been read. Up to Orca 1.4.245 Orca answers with the batch
1319
+ * after it; from 1.4.246 with no batch, since its read acknowledged it (#729).
1320
+ */
1269
1321
  export const ackMailbox = (id, delivery, handle, options) =>
1270
- orca(['orchestration', 'check', '--run', id, ...as(handle), '--ack', delivery], options);
1322
+ orchestration(['orchestration', 'check', '--run', id, ...as(handle), '--ack', delivery], options);
1271
1323
 
1272
1324
  /** The reader of a `check`: the terminal named, or this process's own. */
1273
1325
  const as = (handle) => (handle === undefined ? [] : ['--terminal', handle]);
package/src/up.js CHANGED
@@ -14,6 +14,7 @@ import { botDir, botNames, displayName, readBot } from './bot.js';
14
14
  import { conversationsIn, hasConversation, heldAsUserTurn, transcriptsIn } from './conversations.js';
15
15
  import { dropRemovedHooks, installHook } from './hooks.js';
16
16
  import { writePermissions } from './permissions.js';
17
+ import { physicalMemoryFolder } from './memory.js';
17
18
  import { piTrouble } from './pi.js';
18
19
  import { addressOf, HARNESSES, harnessOf, isAddressOf, isShortPrompt, kitFolders, launchCommand, mailboxStep, ownCli, reachesMail, sessionTrouble, shellWord, startPrompt, workDirOf } from './launch.js';
19
20
  import { asFolderProject, coordinatorOf, dropLeftGit, findProject, harnessInTab, loadInWindow, makeMailbox, makeProject, openTab, QUESTION_ON_SCREEN, retitleTab, runMissing, screenRows, shellInTab, tabs, tabToTypeInto, TERMINAL_ENV, TIMED_OUT, tellWindow, typeIntoTab, useMailbox } from './orca.js';
@@ -610,6 +611,7 @@ function launchFor(bots, bot, session, harness, home, workDir, resume, held, fre
610
611
  resume,
611
612
  address,
612
613
  kit: kitFolders(bots, bot.name),
614
+ ...(harness === 'claude' ? { memoryFolder: physicalMemoryFolder(home) } : {}),
613
615
  // A new Pi conversation's id, and the name its tab and session list show,
614
616
  // `<bot>.<session>` as a Codex thread is named (#480).
615
617
  ...(harness === 'pi' ? { sessionId: resume === undefined ? fresh : undefined, name: `${bot.name}.${session.name}` } : {}),