@assuredloop/orca-bot-kit 0.5.0 → 0.5.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 +5 -0
- package/package.json +1 -1
- package/src/book.js +10 -0
- package/src/cli.js +9 -4
- package/src/conversations.js +15 -1
- package/src/message.js +28 -6
- package/src/orca.js +9 -3
- package/src/up.js +77 -33
package/README.md
CHANGED
|
@@ -350,6 +350,11 @@ conversation it was having, rather than starting a new one. Clear a session —
|
|
|
350
350
|
keeps the old one, and gives the session its start prompt again, because that
|
|
351
351
|
prompt is what tells one session's duty from another's.
|
|
352
352
|
|
|
353
|
+
A session paused before its first turn can have an id in the book and no
|
|
354
|
+
conversation behind it: the harness has nothing on record for that id. There is
|
|
355
|
+
nothing to resume, so it starts again with its duty, and the book moves the id
|
|
356
|
+
into its history as `no conversation`.
|
|
357
|
+
|
|
353
358
|
One tab holds one session: the harness the kit started in it. Anything that
|
|
354
359
|
session runs inside the tab — a `codex exec`, a helper, a subagent's own process
|
|
355
360
|
— is not the session, and never becomes the conversation the kit brings back.
|
package/package.json
CHANGED
package/src/book.js
CHANGED
|
@@ -269,3 +269,13 @@ export function rememberSession(entry = {}, id, ended) {
|
|
|
269
269
|
const was = { session: entry.session, ended: ended ?? 'replaced', at: new Date().toISOString() };
|
|
270
270
|
return { ...entry, session: id, history: [...(entry.history ?? []), was] };
|
|
271
271
|
}
|
|
272
|
+
|
|
273
|
+
/**
|
|
274
|
+
* The session entry once the id it held has turned out to have no conversation
|
|
275
|
+
* behind it: the id goes into the history with that reason, and the entry holds
|
|
276
|
+
* none until the harness names the new one.
|
|
277
|
+
*/
|
|
278
|
+
export function forgetSession(entry, ended) {
|
|
279
|
+
const { session, ...rest } = entry;
|
|
280
|
+
return { ...rest, history: [...(entry.history ?? []), { session, ended, at: new Date().toISOString() }] };
|
|
281
|
+
}
|
package/src/cli.js
CHANGED
|
@@ -892,9 +892,14 @@ function refuseWhenOrcaIsDown() {
|
|
|
892
892
|
|
|
893
893
|
/** The same facts as `--json`, as lines, for a person reading along. */
|
|
894
894
|
/** The one line that says which conversation this tab was given, and from where. */
|
|
895
|
-
const
|
|
896
|
-
|
|
897
|
-
|
|
895
|
+
const howLines = (tab) => {
|
|
896
|
+
if (tab.resumed === true) return [' it was told to resume the session the book holds, with its conversation.'];
|
|
897
|
+
if (tab.noConversation === undefined) return [' it was told to start a new session: the book holds none for this one yet.'];
|
|
898
|
+
return [
|
|
899
|
+
` it was told to start a new session: the book's conversation ${tab.noConversation} had nothing on record`,
|
|
900
|
+
" behind it, so it went into the session's history.",
|
|
901
|
+
];
|
|
902
|
+
};
|
|
898
903
|
|
|
899
904
|
/**
|
|
900
905
|
* What the harness still has in this bot's folder that no session claims. The
|
|
@@ -1205,7 +1210,7 @@ function harnessLines(tab, bots) {
|
|
|
1205
1210
|
// What the line that was typed in asked for, and where the kit got it: the
|
|
1206
1211
|
// session the book holds, one the harness itself still had on record, or a new
|
|
1207
1212
|
// one — and if a new one, whether anything was known about an older one.
|
|
1208
|
-
const how =
|
|
1213
|
+
const how = howLines(tab);
|
|
1209
1214
|
|
|
1210
1215
|
if (!tab.harnessStarted) {
|
|
1211
1216
|
return [
|
package/src/conversations.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// directory, and every session starts at its bot home. Read only, never
|
|
11
11
|
// written: these are the harness's files (tech notes, sections 2 and 3).
|
|
12
12
|
|
|
13
|
-
import { readdirSync, readFileSync, statSync } from 'node:fs';
|
|
13
|
+
import { existsSync, readdirSync, readFileSync, statSync } from 'node:fs';
|
|
14
14
|
import { homedir } from 'node:os';
|
|
15
15
|
import path from 'node:path';
|
|
16
16
|
|
|
@@ -39,6 +39,20 @@ export function conversationsIn(harness, home, since) {
|
|
|
39
39
|
.map((one) => ({ id: one.id, at: one.at }));
|
|
40
40
|
}
|
|
41
41
|
|
|
42
|
+
/**
|
|
43
|
+
* Whether the harness has a record of the conversation `id` for this bot home,
|
|
44
|
+
* which is what its own resume looks for. A session paused before its first
|
|
45
|
+
* turn has an id the hook reported and nothing written behind it, and Claude
|
|
46
|
+
* Code answers a resume of it with "No conversation found" (#295).
|
|
47
|
+
*
|
|
48
|
+
* Asked of the file names alone, which both harnesses make from the id: nothing
|
|
49
|
+
* is opened, however many conversations the machine has.
|
|
50
|
+
*/
|
|
51
|
+
export function hasConversation(harness, home, id) {
|
|
52
|
+
if (harness === 'claude') return existsSync(path.join(claudeDir(home), `${id}.jsonl`));
|
|
53
|
+
return rollouts(codexDir()).some((file) => path.basename(file).endsWith(`-${id}.jsonl`));
|
|
54
|
+
}
|
|
55
|
+
|
|
42
56
|
/**
|
|
43
57
|
* The same conversations, each with the file the harness keeps it in, for a
|
|
44
58
|
* caller that has to read what is inside one rather than only know it is there,
|
package/src/message.js
CHANGED
|
@@ -176,8 +176,8 @@ export function sendMessage(bots, { to: target, from: sender, tab, subject, text
|
|
|
176
176
|
* Orca fences a Run to one reader, its coordinator. A session whose tab is live
|
|
177
177
|
* is bound to that tab and read as it; one whose tab is down is read as its
|
|
178
178
|
* Run's coordinator, and nothing is bound (issue #249). A read that
|
|
179
|
-
* is not a peek acknowledges
|
|
180
|
-
* check
|
|
179
|
+
* is not a peek acknowledges every batch waiting, one after another, so one
|
|
180
|
+
* check hands over all of it (issue #299).
|
|
181
181
|
*/
|
|
182
182
|
export function checkMail(bots, { bot: botName, session: sessionName, tab, peek = false }) {
|
|
183
183
|
const who = sessionName === undefined && botName === undefined
|
|
@@ -212,9 +212,16 @@ export function checkMail(bots, { bot: botName, session: sessionName, tab, peek
|
|
|
212
212
|
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.`,
|
|
213
213
|
};
|
|
214
214
|
}
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
215
|
+
// Orca hands mail over a batch at a time and replays the oldest until it is
|
|
216
|
+
// acknowledged; the acknowledgement answers with the next batch. So a read
|
|
217
|
+
// takes every batch in turn, or newer mail waits behind an older one (#299).
|
|
218
|
+
const messages = [];
|
|
219
|
+
let found = readMailbox(who.mailbox, { peek, handle });
|
|
220
|
+
for (;;) {
|
|
221
|
+
messages.push(...(found.messages ?? []).map((message) => asMessage(bots, message)));
|
|
222
|
+
if (peek || !found.deliveryId || (found.messages ?? []).length === 0) break;
|
|
223
|
+
found = ackMailbox(who.mailbox, found.deliveryId, handle);
|
|
224
|
+
}
|
|
218
225
|
|
|
219
226
|
return { bots, bot: who.bot, session: who.session, mailbox: who.mailbox, read: !peek, messages };
|
|
220
227
|
}
|
|
@@ -335,7 +342,7 @@ function nudge(to, from, subject) {
|
|
|
335
342
|
if (to.tab === undefined) return { nudged: false };
|
|
336
343
|
|
|
337
344
|
try {
|
|
338
|
-
const found =
|
|
345
|
+
const found = lookAt(to);
|
|
339
346
|
if (found.blocked !== undefined) return { nudged: false, blocked: found.blocked };
|
|
340
347
|
// A line that lands in a shell is run there, with the sender's subject in it.
|
|
341
348
|
if (found.unsure !== undefined) return { nudged: false, nudgeTrouble: found.unsure };
|
|
@@ -356,6 +363,21 @@ function nudge(to, from, subject) {
|
|
|
356
363
|
}
|
|
357
364
|
}
|
|
358
365
|
|
|
366
|
+
/**
|
|
367
|
+
* The look before the nudge. Orca now and then refuses a handle it has just
|
|
368
|
+
* listed as stale, and a fresh listing hands out one that works (tech notes,
|
|
369
|
+
* section 1), so that refusal earns one more listing and one more look. Only
|
|
370
|
+
* one: a second refusal is reported as it is (#294).
|
|
371
|
+
*/
|
|
372
|
+
function lookAt(to) {
|
|
373
|
+
try {
|
|
374
|
+
return tabToTypeInto(to.home, to.tab, LOOK_MS);
|
|
375
|
+
} catch (error) {
|
|
376
|
+
if (error.code !== 'terminal_handle_stale') throw error;
|
|
377
|
+
return tabToTypeInto(to.home, to.tab, LOOK_MS);
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
|
|
359
381
|
/** `<bot>/<session>`, or a bot on its own when it has only the one session. */
|
|
360
382
|
function splitTarget(target) {
|
|
361
383
|
const parts = target.split('/');
|
package/src/orca.js
CHANGED
|
@@ -30,11 +30,17 @@ export function orca(args) {
|
|
|
30
30
|
if (answer.ok !== true) {
|
|
31
31
|
// Orca's own words: it knows what went wrong, and the caller is an LLM
|
|
32
32
|
// that can act on them.
|
|
33
|
-
throw
|
|
33
|
+
throw refusal(args, answer);
|
|
34
34
|
}
|
|
35
35
|
return answer.result;
|
|
36
36
|
}
|
|
37
37
|
|
|
38
|
+
/** Orca's refusal as an error, with its code on it, so a caller can tell a stale handle from the rest (#294). */
|
|
39
|
+
const refusal = (args, answer) => Object.assign(
|
|
40
|
+
new Error(`Orca refused ${args.join(' ')}: ${answer.error?.message ?? 'no reason given'}`),
|
|
41
|
+
{ code: answer.error?.code },
|
|
42
|
+
);
|
|
43
|
+
|
|
38
44
|
/** One Orca command, and the whole envelope back, refusals included. */
|
|
39
45
|
function ask(args) {
|
|
40
46
|
const asked = spawnSync(orcaCli(), [...args, '--json'], { encoding: 'utf8' });
|
|
@@ -257,7 +263,7 @@ export function harnessInTab(handle, timeoutMs) {
|
|
|
257
263
|
// Out of time means nothing went idle, busy or absent alike. That is an
|
|
258
264
|
// answer, not a breakdown.
|
|
259
265
|
if (answer.ok !== true && answer.error?.code !== 'timeout') {
|
|
260
|
-
throw
|
|
266
|
+
throw refusal(args, answer);
|
|
261
267
|
}
|
|
262
268
|
|
|
263
269
|
const shown = orca(['terminal', 'show', '--terminal', handle]).terminal;
|
|
@@ -461,7 +467,7 @@ export function postMessage({ to, from, subject, body, type = 'status', thread }
|
|
|
461
467
|
export const readMailbox = (id, { peek = false, handle } = {}) =>
|
|
462
468
|
orca(['orchestration', 'check', '--run', id, ...as(handle), ...(peek ? ['--peek'] : [])]);
|
|
463
469
|
|
|
464
|
-
/** Say a batch has been read
|
|
470
|
+
/** Say a batch has been read. Orca answers with the batch after it. */
|
|
465
471
|
export const ackMailbox = (id, delivery, handle) =>
|
|
466
472
|
orca(['orchestration', 'check', '--run', id, ...as(handle), '--ack', delivery]);
|
|
467
473
|
|
package/src/up.js
CHANGED
|
@@ -8,9 +8,9 @@ import { existsSync, mkdirSync, realpathSync, writeFileSync } from 'node:fs';
|
|
|
8
8
|
import path from 'node:path';
|
|
9
9
|
import { setTimeout as pause } from 'node:timers/promises';
|
|
10
10
|
|
|
11
|
-
import { forgetClaimed, readBook, sessionIdsIn, tabIdsIn, updateBook, withUnclaimed } from './book.js';
|
|
11
|
+
import { forgetClaimed, forgetSession, readBook, sessionIdsIn, tabIdsIn, updateBook, withUnclaimed } from './book.js';
|
|
12
12
|
import { botDir, botNames, displayName, readBot } from './bot.js';
|
|
13
|
-
import { conversationsIn, heldAsUserTurn, transcriptsIn } from './conversations.js';
|
|
13
|
+
import { conversationsIn, hasConversation, heldAsUserTurn, transcriptsIn } from './conversations.js';
|
|
14
14
|
import { installHook } from './hooks.js';
|
|
15
15
|
import { addressOf, harnessOf, isShortPrompt, launchCommand, reachesMail, sessionTrouble, startPrompt, workDirOf } from './launch.js';
|
|
16
16
|
import { asFolderProject, findProject, harnessInTab, makeMailbox, makeProject, openTab, retitleTab, tabs, tellWindow, typeIntoTab, useMailbox } from './orca.js';
|
|
@@ -263,33 +263,14 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
263
263
|
// given again (PRD 6.4) — after a clear it is, and that is the hook's work.
|
|
264
264
|
// Asked before the book is held, because it reads a folder of the harness's
|
|
265
265
|
// own files: nothing slow happens under the lock.
|
|
266
|
-
|
|
267
|
-
const
|
|
268
|
-
|
|
269
|
-
// Anything longer than a line goes to the harness out of a file, rather than
|
|
270
|
-
// through the tab's shell a character at a time.
|
|
271
|
-
const promptFile = prompt === undefined || isShortPrompt(prompt) ? undefined : promptPath(bots, bot.name, session.name);
|
|
272
|
-
const command = launchCommand(session, {
|
|
273
|
-
harness,
|
|
274
|
-
home,
|
|
275
|
-
workDir,
|
|
276
|
-
prompt,
|
|
277
|
-
promptFile,
|
|
278
|
-
resume,
|
|
279
|
-
address: harness === 'claude' ? addressOf(bot.name, session.name) : undefined,
|
|
280
|
-
});
|
|
266
|
+
let which = whichConversation(book, home, bot, session, was, harness);
|
|
267
|
+
const launchOf = (chosen) => launchFor(bots, bot, session, harness, home, workDir, chosen.resume);
|
|
268
|
+
let launch = launchOf(which);
|
|
281
269
|
|
|
282
270
|
// A work dir is a plain folder, made for the session before it is told about
|
|
283
271
|
// it (PRD 6.4). Nothing here is a git worktree.
|
|
284
272
|
if (workDir !== undefined) mkdirSync(workDir, { recursive: true });
|
|
285
|
-
|
|
286
|
-
// The prompt is written where the launch line can read it from, before that
|
|
287
|
-
// line is typed. It is the kit's own file, not the user's: theirs stays where
|
|
288
|
-
// they put it, in the bot home.
|
|
289
|
-
if (promptFile !== undefined) {
|
|
290
|
-
mkdirSync(path.dirname(promptFile), { recursive: true });
|
|
291
|
-
writeFileSync(promptFile, prompt);
|
|
292
|
-
}
|
|
273
|
+
writePrompt(launch);
|
|
293
274
|
|
|
294
275
|
const made = openTab(home, tabTitle);
|
|
295
276
|
|
|
@@ -305,20 +286,41 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
305
286
|
// built, noted so that health can say when the file moves on and the session
|
|
306
287
|
// does not (#272).
|
|
307
288
|
const rules = rulesStamp(home);
|
|
289
|
+
let held;
|
|
308
290
|
await updateBook(home, (current) => {
|
|
309
291
|
// What the harness has in this folder that nobody claims goes on the record,
|
|
310
292
|
// for a person or Bot Father to settle — added to whatever was already noted,
|
|
311
293
|
// because this run's scan cannot see what an earlier one found. The kit never
|
|
312
294
|
// settles it itself.
|
|
313
|
-
|
|
295
|
+
let entry = { ...current.sessions[session.name], tab: made.tabId, launched, rules };
|
|
314
296
|
if (rules === undefined) delete entry.rules;
|
|
297
|
+
held = entry.session;
|
|
298
|
+
// Before the line is typed, so the hook finds no id here and takes the one
|
|
299
|
+
// it reports for a start rather than a clear, which would tell the duty twice.
|
|
300
|
+
if (which.noConversation !== undefined && held === which.noConversation) entry = forgetSession(entry, 'no conversation');
|
|
315
301
|
current.sessions[session.name] = withUnclaimed(entry, which.unclaimed ?? []);
|
|
316
302
|
forgetClaimed(current);
|
|
317
303
|
});
|
|
318
304
|
|
|
305
|
+
// The old tab's hook can name another conversation while the tab is being
|
|
306
|
+
// opened. A fresh start chosen before that is chosen again from the id the
|
|
307
|
+
// book holds now: resumed if the harness has it, and otherwise set aside the
|
|
308
|
+
// same way (review of PR #310).
|
|
309
|
+
if (which.noConversation !== undefined && typeof held === 'string' && held !== which.noConversation) {
|
|
310
|
+
which = hasConversation(harness, home, held) ? { resume: held } : { noConversation: held };
|
|
311
|
+
if (which.noConversation !== undefined) {
|
|
312
|
+
await updateBook(home, (current) => {
|
|
313
|
+
const entry = current.sessions[session.name];
|
|
314
|
+
if (entry?.session === held) current.sessions[session.name] = forgetSession(entry, 'no conversation');
|
|
315
|
+
});
|
|
316
|
+
}
|
|
317
|
+
launch = launchOf(which);
|
|
318
|
+
writePrompt(launch);
|
|
319
|
+
}
|
|
320
|
+
|
|
319
321
|
// Typing it in is the way: for a project the kit has just made, giving Orca
|
|
320
322
|
// the harness as the tab's own command times out and leaves a dead tab.
|
|
321
|
-
typeIntoTab(made.handle, command);
|
|
323
|
+
typeIntoTab(made.handle, launch.command);
|
|
322
324
|
|
|
323
325
|
// Now the session has an address, and not before: the mailbox it can be
|
|
324
326
|
// written to, and — on Claude Code — the name that line just gave it, which
|
|
@@ -344,9 +346,9 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
344
346
|
// duty: one held on a first-run screen, or one that refused the argument, has
|
|
345
347
|
// been told nothing. So it is received only when the session's own record
|
|
346
348
|
// holds it as a user turn, and otherwise not confirmed (#274).
|
|
347
|
-
const promptReceived = prompt === undefined
|
|
349
|
+
const promptReceived = launch.prompt === undefined
|
|
348
350
|
? undefined
|
|
349
|
-
: await heldInRecord(home, session.name, harness, prompt, { launched, running: tui.running === true });
|
|
351
|
+
: await heldInRecord(home, session.name, harness, launch.prompt, { launched, running: tui.running === true });
|
|
350
352
|
|
|
351
353
|
return entry(made, {
|
|
352
354
|
bot: bot.name,
|
|
@@ -354,12 +356,45 @@ async function bringUpSession(bots, home, live, session, bot, title) {
|
|
|
354
356
|
created: true,
|
|
355
357
|
...tui,
|
|
356
358
|
promptReceived,
|
|
357
|
-
promptFile,
|
|
358
|
-
resumed: resume !== undefined,
|
|
359
|
+
promptFile: launch.promptFile,
|
|
360
|
+
resumed: launch.resume !== undefined,
|
|
361
|
+
noConversation: which.noConversation,
|
|
359
362
|
unclaimed: which.unclaimed,
|
|
360
363
|
});
|
|
361
364
|
}
|
|
362
365
|
|
|
366
|
+
/**
|
|
367
|
+
* The line that starts this session: resuming `resume`, or, with none, a fresh
|
|
368
|
+
* start carrying its duty. Returns `{ resume, prompt, promptFile, command }`.
|
|
369
|
+
*/
|
|
370
|
+
function launchFor(bots, bot, session, harness, home, workDir, resume) {
|
|
371
|
+
const prompt = resume === undefined ? startPrompt(session, { home, workDir }) : undefined;
|
|
372
|
+
// Anything longer than a line goes to the harness out of a file, rather than
|
|
373
|
+
// through the tab's shell a character at a time.
|
|
374
|
+
const promptFile = prompt === undefined || isShortPrompt(prompt) ? undefined : promptPath(bots, bot.name, session.name);
|
|
375
|
+
const command = launchCommand(session, {
|
|
376
|
+
harness,
|
|
377
|
+
home,
|
|
378
|
+
workDir,
|
|
379
|
+
prompt,
|
|
380
|
+
promptFile,
|
|
381
|
+
resume,
|
|
382
|
+
address: harness === 'claude' ? addressOf(bot.name, session.name) : undefined,
|
|
383
|
+
});
|
|
384
|
+
return { resume, prompt, promptFile, command };
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* The prompt is written where the launch line can read it from, before that
|
|
389
|
+
* line is typed. It is the kit's own file, not the user's: theirs stays where
|
|
390
|
+
* they put it, in the bot home.
|
|
391
|
+
*/
|
|
392
|
+
function writePrompt({ prompt, promptFile }) {
|
|
393
|
+
if (promptFile === undefined) return;
|
|
394
|
+
mkdirSync(path.dirname(promptFile), { recursive: true });
|
|
395
|
+
writeFileSync(promptFile, prompt);
|
|
396
|
+
}
|
|
397
|
+
|
|
363
398
|
/**
|
|
364
399
|
* How long a running harness's own record is given to show the start prompt.
|
|
365
400
|
* Seen live (Claude Code 2.1.282, a trusted folder): the hook's id and the
|
|
@@ -479,7 +514,14 @@ function mailboxFor(book, bot, session, harness, handle) {
|
|
|
479
514
|
* has already started a harness in, the harness's own record is asked.
|
|
480
515
|
*/
|
|
481
516
|
function whichConversation(book, home, bot, session, was, harness) {
|
|
482
|
-
if (typeof was?.session === 'string')
|
|
517
|
+
if (typeof was?.session === 'string') {
|
|
518
|
+
if (hasConversation(harness, home, was.session)) return { resume: was.session };
|
|
519
|
+
// The hook reported an id and the harness never wrote a conversation behind
|
|
520
|
+
// it: a session paused before its first turn is one. There is nothing to
|
|
521
|
+
// resume, so it starts again with its duty, and the book keeps the id in the
|
|
522
|
+
// history with the reason (#295).
|
|
523
|
+
return { noConversation: was.session };
|
|
524
|
+
}
|
|
483
525
|
// No tab: nothing has ever run for this session, so there is nothing to find.
|
|
484
526
|
if (typeof was?.tab !== 'string') return {};
|
|
485
527
|
|
|
@@ -526,11 +568,13 @@ const ids = (setup, change) => ({ project: setup.projectId, setup: setup.id, cha
|
|
|
526
568
|
export const promptPath = (bots, bot, session) =>
|
|
527
569
|
path.join(`${bots}.prompts`, `${encodeURIComponent(bot)}.${encodeURIComponent(session)}.txt`);
|
|
528
570
|
|
|
529
|
-
function entry(tab, { bot, name, created, running = false, blockedReason, promptReceived, promptFile, resumed, unclaimed }) {
|
|
571
|
+
function entry(tab, { bot, name, created, running = false, blockedReason, promptReceived, promptFile, resumed, noConversation, unclaimed }) {
|
|
530
572
|
const made = { bot, name, title: tab.title, tabId: tab.tabId, terminal: tab.handle, created, harnessStarted: running };
|
|
531
573
|
// Whether this run picked the session up where it was or started a new one.
|
|
532
574
|
// Only for a tab this run opened: a tab that was already there was left alone.
|
|
533
575
|
if (resumed !== undefined) made.resumed = resumed;
|
|
576
|
+
// The id the book held that had no conversation behind it, now in the history.
|
|
577
|
+
if (noConversation !== undefined) made.noConversation = noConversation;
|
|
534
578
|
// Orca's own words for what is on screen waiting to be answered, when it
|
|
535
579
|
// gave any: the caller acts on it, the kit only passes it on.
|
|
536
580
|
if (blockedReason !== undefined) made.blockedReason = blockedReason;
|