@vincemakes/kiso-code 0.45.2 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -1
- package/dist/builtin.d.ts +5 -4
- package/dist/builtin.js +12 -7
- package/dist/chat.js +121 -19
- package/dist/child-result.d.ts +23 -0
- package/dist/child-result.js +59 -0
- package/dist/config.d.ts +13 -6
- package/dist/config.js +30 -16
- package/dist/create-coding-agent.js +3 -1
- package/dist/dispatch.d.ts +2 -0
- package/dist/dispatch.js +163 -31
- package/dist/empty-sessions.d.ts +4 -2
- package/dist/empty-sessions.js +6 -2
- package/dist/external-editor.js +4 -1
- package/dist/floor.js +71 -26
- package/dist/index.js +89 -29
- package/dist/launch.d.ts +29 -0
- package/dist/launch.js +43 -0
- package/dist/mode.d.ts +90 -17
- package/dist/mode.js +135 -32
- package/dist/protected-shell.js +60 -19
- package/dist/protected-writes.d.ts +2 -2
- package/dist/protected-writes.js +2 -2
- package/dist/readonly-shell.js +16 -14
- package/dist/replay.js +3 -0
- package/dist/resume.js +2 -2
- package/dist/settings.d.ts +3 -0
- package/dist/settings.js +41 -11
- package/dist/shell-words.d.ts +34 -1
- package/dist/shell-words.js +84 -4
- package/dist/state.d.ts +31 -0
- package/dist/state.js +88 -2
- package/dist/task-notice.d.ts +22 -0
- package/dist/task-notice.js +86 -0
- package/dist/trust-ui.js +6 -6
- package/package.json +14 -14
package/README.md
CHANGED
|
@@ -32,7 +32,8 @@ kiso
|
|
|
32
32
|
Named model profiles live in `~/.kiso/config.json` (the config stores
|
|
33
33
|
the NAME of the env var holding each key, never the key itself); switch
|
|
34
34
|
in-session with `/model`. Approval tiers: `--mode
|
|
35
|
-
|
|
35
|
+
default|accept-edits|plan|full-access` or `/mode` in-session; `--dont-ask`
|
|
36
|
+
(or `/dont-ask`) never stops for a person.
|
|
36
37
|
|
|
37
38
|
## The commands
|
|
38
39
|
|
package/dist/builtin.d.ts
CHANGED
|
@@ -6,10 +6,11 @@ import type { KisoExtension } from "@vincemakes/kiso-runtime";
|
|
|
6
6
|
* that could only decline them. A LIVE gate, not a load-time one: the
|
|
7
7
|
* registry reads an extension's tools on every request (registerLive) and
|
|
8
8
|
* the run recomposes the tool table, snippet and guidelines included, so
|
|
9
|
-
*
|
|
10
|
-
* brings ask_user back
|
|
11
|
-
* path's; each
|
|
12
|
-
* path (trust-ui) stays for a turn already
|
|
9
|
+
* turning the switch on takes ask_user away from the next run and turning
|
|
10
|
+
* it off brings ask_user back — in every tier. With the switch on the
|
|
11
|
+
* table is byte-identical to the pipe path's; each flip costs one
|
|
12
|
+
* prompt-cache break. The decline path (trust-ui) stays for a turn already
|
|
13
|
+
* in flight when the switch flips.
|
|
13
14
|
*/
|
|
14
15
|
export declare function offInDontAsk(ext: KisoExtension): KisoExtension;
|
|
15
16
|
export declare function builtInLayer(user: readonly KisoExtension[], project: readonly KisoExtension[],
|
package/dist/builtin.js
CHANGED
|
@@ -24,24 +24,26 @@ import createMcp from "@vincemakes/kiso-mcp-ext";
|
|
|
24
24
|
import createSkills from "@vincemakes/kiso-skills-ext";
|
|
25
25
|
import createSubagent from "@vincemakes/kiso-subagent-ext";
|
|
26
26
|
import createAsk, {} from "@vincemakes/kiso-ask-ext";
|
|
27
|
-
import {
|
|
27
|
+
import { getDontAsk } from "./mode.js";
|
|
28
|
+
import { tasksFor } from "./state.js";
|
|
28
29
|
/**
|
|
29
30
|
* 0.40.0 (the owner's dogfood): dontAsk never asks, so its tool table never
|
|
30
31
|
* offers ask_user — a model once put four questions to a dontAsk session
|
|
31
32
|
* that could only decline them. A LIVE gate, not a load-time one: the
|
|
32
33
|
* registry reads an extension's tools on every request (registerLive) and
|
|
33
34
|
* the run recomposes the tool table, snippet and guidelines included, so
|
|
34
|
-
*
|
|
35
|
-
* brings ask_user back
|
|
36
|
-
* path's; each
|
|
37
|
-
* path (trust-ui) stays for a turn already
|
|
35
|
+
* turning the switch on takes ask_user away from the next run and turning
|
|
36
|
+
* it off brings ask_user back — in every tier. With the switch on the
|
|
37
|
+
* table is byte-identical to the pipe path's; each flip costs one
|
|
38
|
+
* prompt-cache break. The decline path (trust-ui) stays for a turn already
|
|
39
|
+
* in flight when the switch flips.
|
|
38
40
|
*/
|
|
39
41
|
export function offInDontAsk(ext) {
|
|
40
42
|
const tools = ext.tools ?? [];
|
|
41
43
|
return {
|
|
42
44
|
...ext,
|
|
43
45
|
get tools() {
|
|
44
|
-
return
|
|
46
|
+
return getDontAsk() ? [] : tools;
|
|
45
47
|
},
|
|
46
48
|
};
|
|
47
49
|
}
|
|
@@ -56,7 +58,10 @@ ask,
|
|
|
56
58
|
* constructed, so the names have to arrive here — after construction is
|
|
57
59
|
* too late. Only the config knows them; nothing else can. */
|
|
58
60
|
secretEnvNames = []) {
|
|
59
|
-
|
|
61
|
+
// ADR-0058 3d: the session's task manager makes `delegate` offer
|
|
62
|
+
// background children (explorer/reviewer; the cap and the turn budget
|
|
63
|
+
// are the extension's defaults)
|
|
64
|
+
const all = await Promise.all([createMcp({ secretEnvNames }), createSkills(), createSubagent({ tasks: tasksFor }), ...(ask === undefined ? [] : [createAsk(ask).then(offInDontAsk)])]);
|
|
60
65
|
const shadowed = all.filter((b) => user.some((u) => u.name === b.name));
|
|
61
66
|
for (const s of shadowed) {
|
|
62
67
|
console.error(`[extensions] user extension "${s.name}" shadows the built-in — the built-in is not loaded`);
|
package/dist/chat.js
CHANGED
|
@@ -5,10 +5,11 @@
|
|
|
5
5
|
* estimates. All bodies moved verbatim from index.ts.
|
|
6
6
|
*/
|
|
7
7
|
import { readFileSync, statSync } from "node:fs";
|
|
8
|
-
import { escapeTerminal, cacheHitPct, decodeRate, idleStatus, palette, renderEvent, renderRecap, runningStatus, toolTarget, STATUS_GLYPHS, kUnit, } from "@vincemakes/kiso-tui";
|
|
8
|
+
import { escapeTerminal, cacheHitPct, decodeRate, idleStatus, palette, renderEvent, renderRecap, runningStatus, toolTarget, STATUS_GLYPHS, kUnit, namedPickView, } from "@vincemakes/kiso-tui";
|
|
9
9
|
import { askView, coldResumeLine, coldResumeView, deletionRiskHint, editFileDiff, writeFileDiff } from "@vincemakes/kiso-tui";
|
|
10
10
|
import { canonicalTargetPath, isProtectedPath, protectedIdentity, shellProgressPath } from "@vincemakes/kiso-tools-node";
|
|
11
|
-
import { queuedSwitchLines } from "./state.js";
|
|
11
|
+
import { liveTasks, mergedConfig, queuedSwitchLines, seenUnknownTasks, setExitTasks, tasksFor } from "./state.js";
|
|
12
|
+
import { taskCounts, taskNoticeRow } from "./task-notice.js";
|
|
12
13
|
import { echoText } from "@vincemakes/kiso-tui-cells/render";
|
|
13
14
|
import { canonicalizeUsage, RunClosedError } from "@vincemakes/kiso-runtime";
|
|
14
15
|
import { canonicalizeUsageForModel, requestBudget } from "@vincemakes/kiso-runtime/internal";
|
|
@@ -21,7 +22,7 @@ import { learnedWindowFor } from "./learned-windows.js";
|
|
|
21
22
|
import { lookupContextWindow, lookupModelMetadata } from "@vincemakes/kiso-runtime/internal";
|
|
22
23
|
import { addDontAskAgainRule, askPanel, fixHintFor, pendingAsk, resolveUncertains } from "./trust-ui.js";
|
|
23
24
|
import { FauxExhaustionError, failOnFauxExhaustion } from "./faux-glue.js";
|
|
24
|
-
import { OFFERED_MODES, getMode, setMode } from "./mode.js";
|
|
25
|
+
import { MODE_LABEL, OFFERED_MODES, getDontAsk, getMode, modeDisplay, setMode } from "./mode.js";
|
|
25
26
|
/** B area: default context window for the ~ctx estimate (config overridable).
|
|
26
27
|
* CW-1 batch 2: 128,000, down from 200,000 — the figure a model nobody
|
|
27
28
|
* states a window for is assumed to hold (the reference implementation's
|
|
@@ -950,6 +951,11 @@ onInputLanded) {
|
|
|
950
951
|
// the log. Since 0.44.0 (the verify offer retired) the CLI
|
|
951
952
|
// starts no such run itself; the branch keeps the rule for
|
|
952
953
|
// any run that carries one, as replay.ts does for old logs.
|
|
954
|
+
// ADR-0058: a task notice is a row, never the person's chip
|
|
955
|
+
if (ev.via?.kind === "tasks") {
|
|
956
|
+
body.notice(taskNoticeRow(ev.via.items));
|
|
957
|
+
break;
|
|
958
|
+
}
|
|
953
959
|
if (ev.source === "system") {
|
|
954
960
|
// R2 (law 1.3): the ◆ said nothing the words did not. What
|
|
955
961
|
// makes this row honest is that it NAMES itself machinery.
|
|
@@ -959,7 +965,7 @@ onInputLanded) {
|
|
|
959
965
|
}
|
|
960
966
|
// 0.40.0: a skill turn's chip is the line the person TYPED — the
|
|
961
967
|
// SKILL.md body is what the model read, not what they said.
|
|
962
|
-
if (ev.via
|
|
968
|
+
if (ev.via?.kind === "skill") {
|
|
963
969
|
body.userLine(ev.via.line);
|
|
964
970
|
break;
|
|
965
971
|
}
|
|
@@ -1072,7 +1078,7 @@ onInputLanded) {
|
|
|
1072
1078
|
// denied HERE, at the one place kiso asks, so every allow the
|
|
1073
1079
|
// chain gave still stands (a tier deny would outrank them). The
|
|
1074
1080
|
// reason is the tool result: the model sees why and goes on.
|
|
1075
|
-
if (
|
|
1081
|
+
if (getDontAsk()) {
|
|
1076
1082
|
body.notice(`[dontAsk] ${escapeTerminal(name)} would ask — denied`);
|
|
1077
1083
|
await session.approve(decisionId, false, `dontAsk: ${name} needs a human's approval, and this session never asks — denied`);
|
|
1078
1084
|
break;
|
|
@@ -1267,9 +1273,39 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1267
1273
|
const exitAtEmptyPrompt = () => {
|
|
1268
1274
|
if (pendingAsk !== null || input.line() !== "")
|
|
1269
1275
|
return;
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1276
|
+
void confirmExit().then((leave) => {
|
|
1277
|
+
if (!leave) {
|
|
1278
|
+
eotSeen = false; // stayed: a later run's end must not ask again
|
|
1279
|
+
input.prompt();
|
|
1280
|
+
return;
|
|
1281
|
+
}
|
|
1282
|
+
cancelled = true;
|
|
1283
|
+
console.log("\n[exit requested]");
|
|
1284
|
+
input.close();
|
|
1285
|
+
});
|
|
1286
|
+
};
|
|
1287
|
+
/** ADR-0058 (3e): a clean exit with live tasks asks first — on a dock
|
|
1288
|
+
* only; a pipe or a task-file run stops them all, as before. The
|
|
1289
|
+
* question says beforehand what each answer does. True: exit. */
|
|
1290
|
+
const confirmExit = async () => {
|
|
1291
|
+
const live = liveTasks();
|
|
1292
|
+
const total = live.durable + live.moved;
|
|
1293
|
+
if (total === 0 || !dock.active)
|
|
1294
|
+
return true;
|
|
1295
|
+
const n = (k, what) => `${k} ${what}${k === 1 ? "" : "s"}`;
|
|
1296
|
+
const options = [{ label: "stop all and exit" }];
|
|
1297
|
+
if (live.durable > 0)
|
|
1298
|
+
options.push({ label: live.moved > 0 ? `leave ${n(live.durable, "background task")} running; ${n(live.moved, "moved command")} will stop` : `leave ${n(live.durable, "background task")} running` });
|
|
1299
|
+
options.push({ label: "stay" });
|
|
1300
|
+
const picked = await new Promise((resolve) => {
|
|
1301
|
+
input.panelAsk(namedPickView("exit", { header: `${n(total, "task")} ${total === 1 ? "is" : "are"} running`, options }, `▸ ${modeDisplay()}`, "stop the running tasks and exit? "), (v) => resolve(v.action === "picked" ? v.result : null));
|
|
1302
|
+
});
|
|
1303
|
+
paintIdle();
|
|
1304
|
+
const label = picked !== null && "index" in picked ? options[picked.index]?.label : undefined;
|
|
1305
|
+
if (label === undefined || label === "stay")
|
|
1306
|
+
return false;
|
|
1307
|
+
setExitTasks(label.startsWith("leave") ? "leave" : "stop");
|
|
1308
|
+
return true;
|
|
1273
1309
|
};
|
|
1274
1310
|
const turn = (text, via) => new Promise((resolve, reject) => {
|
|
1275
1311
|
queued = Math.max(0, queued - 1); // a queued turn starts
|
|
@@ -1284,7 +1320,8 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1284
1320
|
// SKILL.md body, and a body that mentions `diagram.png` must not
|
|
1285
1321
|
// attach a file from the workspace the person never pointed at.
|
|
1286
1322
|
const content = via !== undefined ? text : attachImages(text, input.attachments?.(), protectedFiles());
|
|
1287
|
-
|
|
1323
|
+
// ADR-0058: a task notice is the runtime's input — source "system"
|
|
1324
|
+
const run = via !== undefined ? session.run(content, { via, ...(via.kind === "tasks" ? { source: "system" } : {}) }) : session.run(content);
|
|
1288
1325
|
currentRun = run;
|
|
1289
1326
|
turnNo += 1;
|
|
1290
1327
|
const myTurn = turnNo;
|
|
@@ -1362,9 +1399,15 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1362
1399
|
pendingAsk?.(); // a startup/trust question — cancel it
|
|
1363
1400
|
}
|
|
1364
1401
|
else if (input.line() === "") {
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1402
|
+
void confirmExit().then((leave) => {
|
|
1403
|
+
if (!leave) {
|
|
1404
|
+
input.prompt();
|
|
1405
|
+
return;
|
|
1406
|
+
}
|
|
1407
|
+
cancelled = true;
|
|
1408
|
+
console.log("\n[exit requested]");
|
|
1409
|
+
input.close();
|
|
1410
|
+
});
|
|
1368
1411
|
}
|
|
1369
1412
|
else {
|
|
1370
1413
|
input.clearLine(); // v2c: Ctrl+C on a non-empty line clears it
|
|
@@ -1483,6 +1526,29 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1483
1526
|
// nullish, so the guard is `> 0` rather than `??` (the same shape the
|
|
1484
1527
|
// compositor's own width call uses).
|
|
1485
1528
|
const rowWidth = () => (process.stdout.columns > 0 ? process.stdout.columns : 80);
|
|
1529
|
+
// ADR-0058 (3e): the session's tasks on the rows. Each read walks the
|
|
1530
|
+
// journals, so the running row (repainted many times a second) reads at
|
|
1531
|
+
// most once a second; the idle row (repainted rarely) always reads —
|
|
1532
|
+
// a task promoted or moved to the background makes no transition to
|
|
1533
|
+
// hear, and the row the turn ends on must already count it
|
|
1534
|
+
let taskRow = null;
|
|
1535
|
+
const taskCountsNow = (fresh = false) => {
|
|
1536
|
+
const manager = tasksFor(session.id);
|
|
1537
|
+
if (manager === undefined)
|
|
1538
|
+
return undefined;
|
|
1539
|
+
if (fresh || taskRow === null || Date.now() - taskRow.at > 1_000 || taskRow.seen !== seenUnknownTasks.size) {
|
|
1540
|
+
let list = [];
|
|
1541
|
+
try {
|
|
1542
|
+
list = manager.list();
|
|
1543
|
+
}
|
|
1544
|
+
catch {
|
|
1545
|
+
// a corrupt journal is reported where it is read on purpose
|
|
1546
|
+
}
|
|
1547
|
+
taskRow = { at: Date.now(), seen: seenUnknownTasks.size, counts: taskCounts(list, seenUnknownTasks) };
|
|
1548
|
+
}
|
|
1549
|
+
return taskRow.counts;
|
|
1550
|
+
};
|
|
1551
|
+
const detachableNow = () => (tasksFor(session.id)?.detachable().length ?? 0) > 0;
|
|
1486
1552
|
// KC2 §5: the STATE (the glyph, the run's start, the usage, the dock)
|
|
1487
1553
|
// stays here; the ROW's text is the tui's status formatter.
|
|
1488
1554
|
const paintRunning = () => {
|
|
@@ -1491,7 +1557,7 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1491
1557
|
// composed blind it would be cut from the END — the context figure,
|
|
1492
1558
|
// a fact, going before the gesture hints.
|
|
1493
1559
|
if (dock.active)
|
|
1494
|
-
dock.setStatus(runningStatus(runGlyph, runStart, runUsage.out, displayCtxRatio(session), lastTokPerSec, rowWidth(), retryOnRow()));
|
|
1560
|
+
dock.setStatus(runningStatus(runGlyph, runStart, runUsage.out, displayCtxRatio(session), lastTokPerSec, rowWidth(), retryOnRow(), taskCountsNow(), detachableNow()));
|
|
1495
1561
|
};
|
|
1496
1562
|
// W19: under plan the idle row makes the posture unmistakable — the W4
|
|
1497
1563
|
// parentheses idiom names the read-only constraint. The tier is the
|
|
@@ -1502,7 +1568,7 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1502
1568
|
// TUI2-R1 (E): the meter rides the idle row — both fields omitted
|
|
1503
1569
|
// when unknown, so a session that has not called the model paints
|
|
1504
1570
|
// exactly the pre-round row.
|
|
1505
|
-
dock.setStatus(idleStatus(
|
|
1571
|
+
dock.setStatus(idleStatus(modeDisplay(), statusModelLabel(session), displayCtxRatio(session), {
|
|
1506
1572
|
cacheHitPct: cacheHitPct(runUsage),
|
|
1507
1573
|
costUsd: spentUsd,
|
|
1508
1574
|
tokPerSec: lastTokPerSec,
|
|
@@ -1510,7 +1576,7 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1510
1576
|
// DF-0330-F1: the row's budget. Without it the row is composed
|
|
1511
1577
|
// blind and invariant ① cuts whatever sits last — which is how
|
|
1512
1578
|
// a measured rate went missing at 100 columns.
|
|
1513
|
-
rowWidth(), !floorOn));
|
|
1579
|
+
rowWidth(), !floorOn, taskCountsNow(true)));
|
|
1514
1580
|
};
|
|
1515
1581
|
// TUI2-R2 ⑥ — the BOOT status line. The row is the product's one
|
|
1516
1582
|
// persistent claim about itself (the tier, how to change it, the model,
|
|
@@ -1555,7 +1621,7 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1555
1621
|
spentUsd = (spentUsd ?? 0) + usd;
|
|
1556
1622
|
};
|
|
1557
1623
|
const queueTurn = (line, via) => {
|
|
1558
|
-
const slot = { line: via?.line
|
|
1624
|
+
const slot = { line: via?.kind === "skill" ? via.line : via?.kind === "tasks" ? taskNoticeRow(via.items) : line, content: line, ...(via !== undefined ? { via } : {}), cancelled: false };
|
|
1559
1625
|
pendingTurns.push(slot);
|
|
1560
1626
|
queued += 1;
|
|
1561
1627
|
chainRef.current = chainRef.current.then(async () => {
|
|
@@ -1619,13 +1685,41 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1619
1685
|
return false; // the run is ending: this is the next turn
|
|
1620
1686
|
throw err;
|
|
1621
1687
|
}
|
|
1622
|
-
steering.push({ line: via?.line ?? line, content: line, ...(via !== undefined ? { via } : {}) });
|
|
1688
|
+
steering.push({ line: (via?.kind === "skill" ? via.line : undefined) ?? line, content: line, ...(via !== undefined ? { via } : {}) });
|
|
1623
1689
|
return true;
|
|
1624
1690
|
};
|
|
1625
1691
|
const submitTurn = (line, via) => {
|
|
1626
1692
|
if (!steer(line, via))
|
|
1627
1693
|
queueTurn(line, via);
|
|
1628
1694
|
};
|
|
1695
|
+
// ADR-0058 (3c): the session's task transitions reach the model — into a
|
|
1696
|
+
// live run at its next safe point, or, idle, as one wake turn through
|
|
1697
|
+
// the same chain a person's turn takes (taskWake: false keeps it for
|
|
1698
|
+
// the next message). Released when this chat ends.
|
|
1699
|
+
const taskManager = tasksFor(session.id);
|
|
1700
|
+
// KISO_AUTO_DETACH_MS — the test rigs' knob (like KISO_STREAM_IDLE_MS):
|
|
1701
|
+
// how old a command must be before a steer detaches it (3e D1). The
|
|
1702
|
+
// rigs that hold a steer pending behind a long command raise it.
|
|
1703
|
+
const autoDetachFromEnv = Number.parseInt(process.env.KISO_AUTO_DETACH_MS ?? "", 10);
|
|
1704
|
+
const stopDelivery = taskManager === undefined
|
|
1705
|
+
? () => { }
|
|
1706
|
+
: session.useTasks(taskManager, {
|
|
1707
|
+
wake: mergedConfig.taskWake !== false,
|
|
1708
|
+
onWake: (w) => queueTurn(w.content, w.via),
|
|
1709
|
+
...(Number.isFinite(autoDetachFromEnv) && autoDetachFromEnv >= 0 ? { autoDetachMinAgeMs: autoDetachFromEnv } : {}),
|
|
1710
|
+
});
|
|
1711
|
+
// 3e: a task's change repaints the row it is counted on
|
|
1712
|
+
const stopHearing = taskManager?.subscribe(() => {
|
|
1713
|
+
taskRow = null;
|
|
1714
|
+
if (currentRun !== null)
|
|
1715
|
+
paintRunning();
|
|
1716
|
+
else
|
|
1717
|
+
paintIdle();
|
|
1718
|
+
}) ?? (() => { });
|
|
1719
|
+
const stopTasks = () => {
|
|
1720
|
+
stopHearing();
|
|
1721
|
+
stopDelivery();
|
|
1722
|
+
};
|
|
1629
1723
|
/** The mirror of the last `n` steers the run handed back; clears it. */
|
|
1630
1724
|
const handed = (n) => {
|
|
1631
1725
|
const out = n > 0 ? steering.slice(-n) : [];
|
|
@@ -1677,6 +1771,7 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1677
1771
|
session,
|
|
1678
1772
|
input,
|
|
1679
1773
|
chainRef,
|
|
1774
|
+
confirmExit,
|
|
1680
1775
|
isRunning: () => currentRun !== null,
|
|
1681
1776
|
paintIdle,
|
|
1682
1777
|
modelSwitched,
|
|
@@ -1741,6 +1836,8 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1741
1836
|
// "expand" turn is never intercepted).
|
|
1742
1837
|
input.onExpand(() => dispatch("\x12expand", dispatchCtx));
|
|
1743
1838
|
input.onThink?.(() => dispatch("\x14think", dispatchCtx));
|
|
1839
|
+
// ADR-0058 (3e): ctrl+b — the running command to the background, now
|
|
1840
|
+
input.onBackground?.(() => dispatch("\x02background", dispatchCtx));
|
|
1744
1841
|
input.onEditor?.(() => dispatch("\x07editor", dispatchCtx));
|
|
1745
1842
|
// E1 §3 — ctrl+x and `/copy` are the same action reached two ways, so
|
|
1746
1843
|
// they are the same sentinel: one implementation, one behaviour.
|
|
@@ -1751,9 +1848,13 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1751
1848
|
// row repaints at once with a one-line notice.
|
|
1752
1849
|
input.onModeCycle?.(() => {
|
|
1753
1850
|
const next = OFFERED_MODES[(OFFERED_MODES.indexOf(getMode()) + 1) % OFFERED_MODES.length];
|
|
1851
|
+
const wasOn = getDontAsk();
|
|
1754
1852
|
setMode(next);
|
|
1755
1853
|
paintIdle();
|
|
1756
|
-
body.notice(`mode → ${next} (shift+tab cycles)`);
|
|
1854
|
+
body.notice(`mode → ${MODE_LABEL[next]} (shift+tab cycles)`);
|
|
1855
|
+
// the switch came with the old dontAsk tier, and left with it
|
|
1856
|
+
if (getDontAsk() !== wasOn)
|
|
1857
|
+
body.notice("don't ask → off");
|
|
1757
1858
|
});
|
|
1758
1859
|
// Recovery first: a session with a dangling pause or uncertain
|
|
1759
1860
|
// executions must resolve them BEFORE the REPL accepts new turns —
|
|
@@ -1775,7 +1876,7 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1775
1876
|
// before the run resumes: the run continues on the compacted projection.
|
|
1776
1877
|
const cold = !cancelled && process.stdin.isTTY ? coldResumeOffer(session) : null;
|
|
1777
1878
|
if (cold !== null) {
|
|
1778
|
-
if (
|
|
1879
|
+
if (getDontAsk()) {
|
|
1779
1880
|
body.notice(`[dontAsk] ${coldResumeLine(cold.tokens, cold.minutes)} — compacting first`);
|
|
1780
1881
|
dispatch("/compact", dispatchCtx);
|
|
1781
1882
|
}
|
|
@@ -1835,6 +1936,7 @@ export async function chat(session, faux, input, autoCompact, nav, seed) {
|
|
|
1835
1936
|
// runs before the exit or the chain is already settled. One level is
|
|
1836
1937
|
// enough: the /compact segment appends nothing of its own.
|
|
1837
1938
|
await chainRef.current;
|
|
1939
|
+
stopTasks();
|
|
1838
1940
|
if (switchTo !== null)
|
|
1839
1941
|
return { next: "switch", id: switchTo, lines: queuedLines.slice() };
|
|
1840
1942
|
// a switch beats a reload: /resume and /clear are going somewhere else,
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-0058 3d — how a background child ends: `kiso chat <id> --task-file
|
|
3
|
+
* <f> --max-turns N --result-file <r>`.
|
|
4
|
+
*
|
|
5
|
+
* The budget (D6) is the session's `maxTurns`: the kernel stops the run at
|
|
6
|
+
* a model boundary, after the round's tool results, before the next
|
|
7
|
+
* request — nothing is killed. Such a run usually ends on a tool round with
|
|
8
|
+
* no answer, so the child gets ONE wrap-up request: a system input naming
|
|
9
|
+
* the spent budget, in a run limited to that one request. The outcome is
|
|
10
|
+
* then `incomplete`, never a plain failure.
|
|
11
|
+
*
|
|
12
|
+
* The answer is the child's own — the last assistant text in its session's
|
|
13
|
+
* projection, never its printed output — written atomically to `<r>`
|
|
14
|
+
* (result.md) with `{ outcome, requests, budget }` beside it (result.json),
|
|
15
|
+
* before the process exits: the runner records the exit after both exist.
|
|
16
|
+
*/
|
|
17
|
+
import type { AgentSession } from "@vincemakes/kiso-runtime";
|
|
18
|
+
export interface ChildEnd {
|
|
19
|
+
readonly maxTurns?: number;
|
|
20
|
+
readonly resultFile?: string;
|
|
21
|
+
}
|
|
22
|
+
export declare const wrapUpInput: (budget: number) => string;
|
|
23
|
+
export declare function finishChild(session: AgentSession, end: ChildEnd): Promise<void>;
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ADR-0058 3d — how a background child ends: `kiso chat <id> --task-file
|
|
3
|
+
* <f> --max-turns N --result-file <r>`.
|
|
4
|
+
*
|
|
5
|
+
* The budget (D6) is the session's `maxTurns`: the kernel stops the run at
|
|
6
|
+
* a model boundary, after the round's tool results, before the next
|
|
7
|
+
* request — nothing is killed. Such a run usually ends on a tool round with
|
|
8
|
+
* no answer, so the child gets ONE wrap-up request: a system input naming
|
|
9
|
+
* the spent budget, in a run limited to that one request. The outcome is
|
|
10
|
+
* then `incomplete`, never a plain failure.
|
|
11
|
+
*
|
|
12
|
+
* The answer is the child's own — the last assistant text in its session's
|
|
13
|
+
* projection, never its printed output — written atomically to `<r>`
|
|
14
|
+
* (result.md) with `{ outcome, requests, budget }` beside it (result.json),
|
|
15
|
+
* before the process exits: the runner records the exit after both exist.
|
|
16
|
+
*/
|
|
17
|
+
import { renameSync, writeFileSync } from "node:fs";
|
|
18
|
+
import { dirname, join } from "node:path";
|
|
19
|
+
export const wrapUpInput = (budget) => `Your turn budget of ${budget} model requests is spent. Answer now from what you have found, without further tool calls. End with the UNRESOLVED section, naming what you did not get to.`;
|
|
20
|
+
export async function finishChild(session, end) {
|
|
21
|
+
const lastOutcome = () => {
|
|
22
|
+
const t = [...session.log.all].reverse().find((e) => e.type === "terminal");
|
|
23
|
+
return t === undefined ? undefined : t.outcome.kind;
|
|
24
|
+
};
|
|
25
|
+
let outcome = lastOutcome() === "completed" ? "completed" : "failed";
|
|
26
|
+
if (end.maxTurns !== undefined && lastOutcome() === "max_turns") {
|
|
27
|
+
for await (const _ of session.run(wrapUpInput(end.maxTurns), { source: "system", maxTurns: 1 })) {
|
|
28
|
+
// drained: the wrap-up's events are durable in the session
|
|
29
|
+
}
|
|
30
|
+
outcome = "incomplete";
|
|
31
|
+
}
|
|
32
|
+
if (end.resultFile === undefined)
|
|
33
|
+
return;
|
|
34
|
+
const answer = lastAnswer(session);
|
|
35
|
+
const requests = session.log.all.filter((e) => e.type === "stop").length;
|
|
36
|
+
writeAtomic(end.resultFile, answer === "" ? "" : `${answer}\n`);
|
|
37
|
+
writeAtomic(join(dirname(end.resultFile), "result.json"), `${JSON.stringify({ outcome, requests, ...(end.maxTurns !== undefined ? { budget: end.maxTurns } : {}) })}\n`);
|
|
38
|
+
// the runner's terminal is the exit: a failed child must not read as a clean one
|
|
39
|
+
if (outcome === "failed")
|
|
40
|
+
process.exitCode = 1;
|
|
41
|
+
}
|
|
42
|
+
/** The last assistant message's text in the session's projection. */
|
|
43
|
+
function lastAnswer(session) {
|
|
44
|
+
const messages = session.projected();
|
|
45
|
+
for (let i = messages.length - 1; i >= 0; i--) {
|
|
46
|
+
const m = messages[i];
|
|
47
|
+
if (m.role !== "assistant")
|
|
48
|
+
continue;
|
|
49
|
+
const text = m.blocks.flatMap((b) => (b.type === "text" ? [b.text] : [])).join("");
|
|
50
|
+
if (text.trim() !== "")
|
|
51
|
+
return text.trimEnd();
|
|
52
|
+
}
|
|
53
|
+
return "";
|
|
54
|
+
}
|
|
55
|
+
function writeAtomic(path, content) {
|
|
56
|
+
const tmp = `${path}.tmp`;
|
|
57
|
+
writeFileSync(tmp, content, "utf8");
|
|
58
|
+
renameSync(tmp, path);
|
|
59
|
+
}
|
package/dist/config.d.ts
CHANGED
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
* models?: { [name]: { kind: "openai-compat"|"anthropic"|"openai-responses",
|
|
14
14
|
* baseUrl?: string, model: string, apiKeyEnv: string,
|
|
15
15
|
* upstream?: string } } — upstream: never where requests go
|
|
16
|
-
* mode?: "
|
|
16
|
+
* mode?: "default"|"accept-edits"|"plan"|"full-access" — and every older
|
|
17
|
+
* spelling: "manual", "bypass" (full-access's old name),
|
|
18
|
+
* "dontAsk" (default with the don't-ask switch on)
|
|
19
|
+
* dontAsk?: boolean — the don't-ask switch (mode.ts)
|
|
17
20
|
* contextWindow?: number — tokens
|
|
18
21
|
* autoCompact?: { thresholdRatio: number } — 0<r<1; default off
|
|
19
22
|
* projectTrust?: "ask" | "never" — no "always" (the ruling)
|
|
@@ -28,7 +31,6 @@
|
|
|
28
31
|
* with an invalid value is likewise loud; unknown top-level keys are
|
|
29
32
|
* ignored (forward compatibility).
|
|
30
33
|
*/
|
|
31
|
-
import type { Mode } from "./mode.js";
|
|
32
34
|
/** OR-1: `openai-responses` is a DIALECT, not a vendor — the same kind
|
|
33
35
|
* drives the first-party Responses API and the ChatGPT subscription
|
|
34
36
|
* backend, and which one it is follows the profile's baseUrl (and so
|
|
@@ -92,9 +94,17 @@ export interface AutoCompactConfig {
|
|
|
92
94
|
export interface KisoConfig {
|
|
93
95
|
readonly model?: string;
|
|
94
96
|
readonly models?: Readonly<Record<string, ModelProfile>>;
|
|
95
|
-
|
|
97
|
+
/** As written — an old name stays an old name here, and mode.ts
|
|
98
|
+
* resolves it (resolveModeLayers), so /settings can say which. */
|
|
99
|
+
readonly mode?: string;
|
|
100
|
+
/** The don't-ask switch. Either config may set it: it only ever
|
|
101
|
+
* refuses more, so a project turning it on lowers nothing. */
|
|
102
|
+
readonly dontAsk?: boolean;
|
|
96
103
|
readonly contextWindow?: number;
|
|
97
104
|
readonly autoCompact?: AutoCompactConfig;
|
|
105
|
+
/** ADR-0058 §8.4: false turns every task wake into a notice that waits
|
|
106
|
+
* for your next message. Default on. */
|
|
107
|
+
readonly taskWake?: boolean;
|
|
98
108
|
readonly projectTrust?: "ask" | "never";
|
|
99
109
|
/** DC-3 §3 rung 1, persisted. The terminal's light/dark, for terminals
|
|
100
110
|
* that answer neither `CSI ? 996 n` nor OSC 11. USER-LEVEL ONLY: a
|
|
@@ -194,9 +204,6 @@ export declare function directWriteProfile(value: string): ModelProfile | null;
|
|
|
194
204
|
* Returns null when nothing resolves (faux mode).
|
|
195
205
|
*/
|
|
196
206
|
export declare function resolveModel(modelFlag: string | undefined, merged: KisoConfig): ResolvedModel | null;
|
|
197
|
-
/** Mode: env (KISO_MODE) beats config.mode; the --mode flag is applied by
|
|
198
|
-
* main before this runs (flags are the top of the chain). */
|
|
199
|
-
export declare function resolveModeFromConfig(merged: KisoConfig): Mode | undefined;
|
|
200
207
|
/** Context window: env (KISO_CONTEXT_WINDOW) > config.contextWindow >
|
|
201
208
|
* default (200k — the caller's default). */
|
|
202
209
|
export declare function resolveContextWindow(merged: KisoConfig, profile?: ModelProfile): number | undefined;
|
package/dist/config.js
CHANGED
|
@@ -13,7 +13,10 @@
|
|
|
13
13
|
* models?: { [name]: { kind: "openai-compat"|"anthropic"|"openai-responses",
|
|
14
14
|
* baseUrl?: string, model: string, apiKeyEnv: string,
|
|
15
15
|
* upstream?: string } } — upstream: never where requests go
|
|
16
|
-
* mode?: "
|
|
16
|
+
* mode?: "default"|"accept-edits"|"plan"|"full-access" — and every older
|
|
17
|
+
* spelling: "manual", "bypass" (full-access's old name),
|
|
18
|
+
* "dontAsk" (default with the don't-ask switch on)
|
|
19
|
+
* dontAsk?: boolean — the don't-ask switch (mode.ts)
|
|
17
20
|
* contextWindow?: number — tokens
|
|
18
21
|
* autoCompact?: { thresholdRatio: number } — 0<r<1; default off
|
|
19
22
|
* projectTrust?: "ask" | "never" — no "always" (the ruling)
|
|
@@ -30,8 +33,9 @@
|
|
|
30
33
|
*/
|
|
31
34
|
import { readFileSync, statSync } from "node:fs";
|
|
32
35
|
import { homedir } from "node:os";
|
|
33
|
-
import { join } from "node:path";
|
|
36
|
+
import { join, win32 } from "node:path";
|
|
34
37
|
import { kisoHome } from "./state.js";
|
|
38
|
+
import { MODE_VALUES, parseMode } from "./mode.js";
|
|
35
39
|
import { AuthError, effectiveBaseUrl, endpointCredentialId, getCredential, providerIdOf } from "./auth/credentials.js";
|
|
36
40
|
import { BUILTIN_MANIFESTS } from "@vincemakes/kiso-runtime/internal";
|
|
37
41
|
/** Header names a profile may not set: credentials (a key never lives in the
|
|
@@ -56,7 +60,6 @@ function parseHeaders(field, raw, fail) {
|
|
|
56
60
|
return out;
|
|
57
61
|
}
|
|
58
62
|
const KINDS = ["openai-compat", "anthropic", "openai-responses"];
|
|
59
|
-
const MODES_LIST = ["manual", "default", "accept-edits", "plan", "bypass", "dontAsk"];
|
|
60
63
|
export class ConfigError extends Error {
|
|
61
64
|
}
|
|
62
65
|
/** Parse + validate one config file. Broken JSON / invalid known values →
|
|
@@ -96,6 +99,11 @@ export function parseConfig(text, source) {
|
|
|
96
99
|
fail("floor", "belongs in the USER config — a project must never be able to lower the floor");
|
|
97
100
|
out.floor = obj.floor;
|
|
98
101
|
}
|
|
102
|
+
// Windows P3: absolute for the host — on Windows a drive path with
|
|
103
|
+
// either separator (or a UNC share), never a drive-rooted `\x`, which
|
|
104
|
+
// would mean a different file on every drive
|
|
105
|
+
const windows = process.platform === "win32";
|
|
106
|
+
const absolute = (p) => (windows ? win32.isAbsolute(p) && !/^[\\/](?![\\/])/.test(p) : p.startsWith("/"));
|
|
99
107
|
if (obj.protectedPaths !== undefined) {
|
|
100
108
|
if (source.startsWith("<cwd>"))
|
|
101
109
|
fail("protectedPaths", "belongs in the USER config — a project must never be able to change what kiso guards");
|
|
@@ -104,7 +112,7 @@ export function parseConfig(text, source) {
|
|
|
104
112
|
for (const [i, p] of obj.protectedPaths.entries()) {
|
|
105
113
|
// a relative path would be read against whatever directory kiso
|
|
106
114
|
// runs in — a different file in every project
|
|
107
|
-
if (typeof p !== "string" || !(p
|
|
115
|
+
if (typeof p !== "string" || !(absolute(p) || p.startsWith("~/")))
|
|
108
116
|
fail(`protectedPaths[${i}]`, "expected an absolute path or one starting with ~/");
|
|
109
117
|
// kiso protects FILES. A directory listed here would protect
|
|
110
118
|
// nothing under it — a security setting that silently does
|
|
@@ -113,7 +121,7 @@ export function parseConfig(text, source) {
|
|
|
113
121
|
// caught at the next start or /reload.)
|
|
114
122
|
const path = p;
|
|
115
123
|
const full = path.startsWith("~/") ? join(homedir(), path.slice(2)) : path;
|
|
116
|
-
if (path.endsWith("/") || statSync(full, { throwIfNoEntry: false })?.isDirectory() === true) {
|
|
124
|
+
if (path.endsWith("/") || (windows && path.endsWith("\\")) || statSync(full, { throwIfNoEntry: false })?.isDirectory() === true) {
|
|
117
125
|
fail(`protectedPaths[${i}]`, `${path} is a directory — kiso protects files, so list the files in it (for ~/.aws: "~/.aws/credentials", "~/.aws/config")`);
|
|
118
126
|
}
|
|
119
127
|
}
|
|
@@ -134,7 +142,7 @@ export function parseConfig(text, source) {
|
|
|
134
142
|
if (!Array.isArray(obj.evaluators))
|
|
135
143
|
fail("evaluators", "expected a list of absolute paths to evaluator scripts");
|
|
136
144
|
for (const path of obj.evaluators) {
|
|
137
|
-
if (typeof path !== "string" || !path
|
|
145
|
+
if (typeof path !== "string" || !absolute(path))
|
|
138
146
|
fail("evaluators", `expected an absolute path, got ${JSON.stringify(path)}`);
|
|
139
147
|
}
|
|
140
148
|
out.evaluators = obj.evaluators;
|
|
@@ -195,10 +203,15 @@ export function parseConfig(text, source) {
|
|
|
195
203
|
out.models = models;
|
|
196
204
|
}
|
|
197
205
|
if (obj.mode !== undefined) {
|
|
198
|
-
if (typeof obj.mode !== "string" ||
|
|
199
|
-
fail("mode", `expected one of ${
|
|
206
|
+
if (typeof obj.mode !== "string" || parseMode(obj.mode) === undefined)
|
|
207
|
+
fail("mode", `expected one of ${MODE_VALUES.join(", ")}`);
|
|
200
208
|
out.mode = obj.mode;
|
|
201
209
|
}
|
|
210
|
+
if (obj.dontAsk !== undefined) {
|
|
211
|
+
if (typeof obj.dontAsk !== "boolean")
|
|
212
|
+
fail("dontAsk", "expected true or false");
|
|
213
|
+
out.dontAsk = obj.dontAsk;
|
|
214
|
+
}
|
|
202
215
|
if (obj.contextWindow !== undefined) {
|
|
203
216
|
if (typeof obj.contextWindow !== "number" || !Number.isFinite(obj.contextWindow) || obj.contextWindow <= 0) {
|
|
204
217
|
fail("contextWindow", "expected a positive token count");
|
|
@@ -213,6 +226,11 @@ export function parseConfig(text, source) {
|
|
|
213
226
|
fail("autoCompact.thresholdRatio", "expected a number strictly between 0 and 1");
|
|
214
227
|
out.autoCompact = { thresholdRatio: r };
|
|
215
228
|
}
|
|
229
|
+
if (obj.taskWake !== undefined) {
|
|
230
|
+
if (typeof obj.taskWake !== "boolean")
|
|
231
|
+
fail("taskWake", "expected true or false");
|
|
232
|
+
out.taskWake = obj.taskWake;
|
|
233
|
+
}
|
|
216
234
|
if (obj.projectTrust !== undefined) {
|
|
217
235
|
if (obj.projectTrust !== "ask" && obj.projectTrust !== "never")
|
|
218
236
|
fail("projectTrust", 'expected "ask" or "never" (there is deliberately no "always")');
|
|
@@ -254,10 +272,14 @@ export function mergeConfigs(user, project) {
|
|
|
254
272
|
...(p.models !== undefined ? { models: { ...u.models, ...p.models } } : u.models !== undefined ? { models: u.models } : {}),
|
|
255
273
|
...(u.mode !== undefined ? { mode: u.mode } : {}),
|
|
256
274
|
...(p.mode !== undefined ? { mode: p.mode } : {}),
|
|
275
|
+
...(u.dontAsk !== undefined ? { dontAsk: u.dontAsk } : {}),
|
|
276
|
+
...(p.dontAsk !== undefined ? { dontAsk: p.dontAsk } : {}),
|
|
257
277
|
...(u.contextWindow !== undefined ? { contextWindow: u.contextWindow } : {}),
|
|
258
278
|
...(p.contextWindow !== undefined ? { contextWindow: p.contextWindow } : {}),
|
|
259
279
|
...(u.autoCompact !== undefined ? { autoCompact: u.autoCompact } : {}),
|
|
260
280
|
...(p.autoCompact !== undefined ? { autoCompact: p.autoCompact } : {}),
|
|
281
|
+
...(u.taskWake !== undefined ? { taskWake: u.taskWake } : {}),
|
|
282
|
+
...(p.taskWake !== undefined ? { taskWake: p.taskWake } : {}),
|
|
261
283
|
...(u.projectTrust !== undefined ? { projectTrust: u.projectTrust } : {}),
|
|
262
284
|
...(p.projectTrust !== undefined ? { projectTrust: p.projectTrust } : {}),
|
|
263
285
|
// DT-1a: checks merge per name — a (trusted) project's check wins over the user's
|
|
@@ -474,14 +496,6 @@ function resolveProfile(name, p) {
|
|
|
474
496
|
const auth = authForProfile(name, p);
|
|
475
497
|
return auth.type === "oauth" ? { name, profile: p, oauthProviderId: auth.providerId } : { name, profile: p, apiKey: auth.apiKey };
|
|
476
498
|
}
|
|
477
|
-
/** Mode: env (KISO_MODE) beats config.mode; the --mode flag is applied by
|
|
478
|
-
* main before this runs (flags are the top of the chain). */
|
|
479
|
-
export function resolveModeFromConfig(merged) {
|
|
480
|
-
const fromEnv = process.env.KISO_MODE;
|
|
481
|
-
if (fromEnv !== undefined)
|
|
482
|
-
return fromEnv;
|
|
483
|
-
return merged.mode;
|
|
484
|
-
}
|
|
485
499
|
/** Context window: env (KISO_CONTEXT_WINDOW) > config.contextWindow >
|
|
486
500
|
* default (200k — the caller's default). */
|
|
487
501
|
export function resolveContextWindow(merged, profile) {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* Moved verbatim from index.ts's makeAgent (R1, 2026-09-23) and renamed;
|
|
8
8
|
* the body is not edited beyond the rename and activeStoreDir's setter.
|
|
9
9
|
*/
|
|
10
|
-
import { activeStoreDir, setActiveStoreDir, body, bodyLog, codingToolOptions, extensionsDir, floorOn, kisoHome, loadedExtensions, ownSessionsDir, projectRoot, protectedFiles, secretEnvNamesOf, sessionsDir, setAgentModel, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setCurrentProfileName, setExtensionLists, setFloorOn, setMergedConfig, setModelChoice, setNeverInherited, setRetryShown, setSessionStore, setUserProtectedPaths, settingsLayers, workspaceRoot } from "./state.js";
|
|
10
|
+
import { activeStoreDir, childTurnBudget, setActiveStoreDir, body, bodyLog, codingToolOptions, extensionsDir, floorOn, kisoHome, loadedExtensions, ownSessionsDir, projectRoot, protectedFiles, secretEnvNamesOf, sessionsDir, setAgentModel, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setCurrentProfileName, setExtensionLists, setFloorOn, setMergedConfig, setModelChoice, setNeverInherited, setRetryShown, setSessionStore, setUserProtectedPaths, settingsLayers, workspaceRoot } from "./state.js";
|
|
11
11
|
import { claimProjectDir, projectLayoutActive } from "./projects.js";
|
|
12
12
|
import { migrationNotice, pendingLegacyIds, planMigration, runMigration } from "./session-migration.js";
|
|
13
13
|
import { askUi, resolveProjectTrust } from "./trust-ui.js";
|
|
@@ -314,6 +314,8 @@ export async function createCodingAgent(sessionId, input, modelFlag) {
|
|
|
314
314
|
// Modes: the five tiers join at the CHAIN HEAD, before the user/
|
|
315
315
|
// project extensions (the deny>allow>ask composition keeps a user
|
|
316
316
|
// deny winning over any mode tier — bypass included).
|
|
317
|
+
// ADR-0058 3d (D6): a background child's budget, its only bound.
|
|
318
|
+
...(childTurnBudget !== undefined ? { maxTurns: childTurnBudget } : {}),
|
|
317
319
|
extensions,
|
|
318
320
|
...(resolved !== null
|
|
319
321
|
? {
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -39,6 +39,8 @@ export interface DispatchCtx {
|
|
|
39
39
|
readonly requestSwitch: (id: string) => void;
|
|
40
40
|
/** DC-57: true once this entry has been asked to leave — the guard that keeps a departing session from answering a batch's later lines. */
|
|
41
41
|
readonly leaving: () => boolean;
|
|
42
|
+
/** ADR-0058 (3e): with live tasks, ask before exiting; true = exit. */
|
|
43
|
+
readonly confirmExit: () => Promise<boolean>;
|
|
42
44
|
/** §2.5: end this chat() with a REBUILD of the agent on the SAME
|
|
43
45
|
* session — extensions, skills and config are read again. */
|
|
44
46
|
readonly requestReload: () => void;
|