@vincemakes/kiso-code 0.15.0 → 0.15.2
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/dist/chat.d.ts +17 -1
- package/dist/chat.js +31 -4
- package/dist/dispatch.d.ts +7 -0
- package/dist/dispatch.js +70 -2
- package/dist/index.js +112 -21
- package/dist/session-id.d.ts +54 -0
- package/dist/session-id.js +74 -0
- package/dist/state.d.ts +5 -0
- package/dist/trust-ui.js +22 -0
- package/package.json +54 -54
package/dist/chat.d.ts
CHANGED
|
@@ -158,4 +158,20 @@ export declare function consumeRun(session: AgentSession, run: Run, input: LineI
|
|
|
158
158
|
* (resume) where a dropped amend is noticed instead. */
|
|
159
159
|
submitTurn?: (line: string) => void): Promise<import("@vincemakes/kiso-core").Event | undefined>;
|
|
160
160
|
/** Interactive REPL: stream events, pause for approvals, Ctrl+C aborts. */
|
|
161
|
-
|
|
161
|
+
/** The chat loop's ENDING: exit closes the process's REPL for good; a
|
|
162
|
+
* switch hands main another session id to re-enter chat with — the
|
|
163
|
+
* editor survives, the durable law is untouched (the /resume+/clear
|
|
164
|
+
* mini-spec). */
|
|
165
|
+
export type ChatEnd = {
|
|
166
|
+
readonly next: "exit";
|
|
167
|
+
} | {
|
|
168
|
+
readonly next: "switch";
|
|
169
|
+
readonly id: string;
|
|
170
|
+
};
|
|
171
|
+
/** The session-navigation seam main provides: the OTHER sessions'
|
|
172
|
+
* ids, and (when a dock is up) the existing picker. */
|
|
173
|
+
export interface ChatNav {
|
|
174
|
+
readonly sessions: () => readonly string[];
|
|
175
|
+
readonly pick?: () => Promise<string | null>;
|
|
176
|
+
}
|
|
177
|
+
export declare function chat(session: AgentSession, faux: boolean, input: LineInput, autoCompact?: AutoCompact, nav?: ChatNav): Promise<ChatEnd>;
|
package/dist/chat.js
CHANGED
|
@@ -793,8 +793,14 @@ submitTurn) {
|
|
|
793
793
|
}
|
|
794
794
|
return last;
|
|
795
795
|
}
|
|
796
|
-
|
|
797
|
-
|
|
796
|
+
export async function chat(session, faux, input, autoCompact, nav) {
|
|
797
|
+
// the switch directive — set once by dispatch's /clear or /resume,
|
|
798
|
+
// resolved through the end signal so the final awaits still run
|
|
799
|
+
let switchTo = null;
|
|
800
|
+
let resolveEnd = () => { };
|
|
801
|
+
const endSignal = new Promise((r) => {
|
|
802
|
+
resolveEnd = r;
|
|
803
|
+
});
|
|
798
804
|
let currentRun = null;
|
|
799
805
|
let cancelled = false;
|
|
800
806
|
// E group (the graceful-exit gate ③, R-G 0.1.48): the terminal can
|
|
@@ -1114,6 +1120,15 @@ export async function chat(session, faux, input, autoCompact) {
|
|
|
1114
1120
|
submitTurn,
|
|
1115
1121
|
estimateCtx: () => estimateCtxRatio(session),
|
|
1116
1122
|
contextWindow: () => contextWindowTokens(),
|
|
1123
|
+
// the /resume+/clear mini-spec: the switch directive and the
|
|
1124
|
+
// session-navigation seam (absent nav = the commands degrade to
|
|
1125
|
+
// an honest refusal in dispatch)
|
|
1126
|
+
requestSwitch: (id) => {
|
|
1127
|
+
switchTo = id;
|
|
1128
|
+
resolveEnd();
|
|
1129
|
+
},
|
|
1130
|
+
sessions: () => nav?.sessions() ?? [],
|
|
1131
|
+
...(nav?.pick !== undefined ? { pickSession: nav.pick } : {}),
|
|
1117
1132
|
};
|
|
1118
1133
|
// the ergonomics batch C8: the auto-compact check — the /compact FULL path via the
|
|
1119
1134
|
// shared dispatch (same notices, same chain ordering, same mid-run
|
|
@@ -1143,6 +1158,15 @@ export async function chat(session, faux, input, autoCompact) {
|
|
|
1143
1158
|
// chain action (the sentinel's control char marks the key, so a typed
|
|
1144
1159
|
// "expand" turn is never intercepted).
|
|
1145
1160
|
input.onExpand(() => dispatch("\x12expand", dispatchCtx));
|
|
1161
|
+
// R3a — Shift+Tab: the approval-tier cycle (the /mode ring, in the
|
|
1162
|
+
// MODES order). The switch is the SAME live-extension flip /mode
|
|
1163
|
+
// performs; the status row repaints at once with a one-line notice.
|
|
1164
|
+
input.onModeCycle?.(() => {
|
|
1165
|
+
const next = MODES[(MODES.indexOf(getMode()) + 1) % MODES.length];
|
|
1166
|
+
setMode(next);
|
|
1167
|
+
paintIdle();
|
|
1168
|
+
body.notice(`mode → ${next} (shift+tab cycles)`);
|
|
1169
|
+
});
|
|
1146
1170
|
// Recovery first: a session with a dangling pause or uncertain
|
|
1147
1171
|
// executions must resolve them BEFORE the REPL accepts new turns —
|
|
1148
1172
|
// otherwise the interrupted run dangles while a new one starts.
|
|
@@ -1174,7 +1198,7 @@ export async function chat(session, faux, input, autoCompact) {
|
|
|
1174
1198
|
if (cancelled) {
|
|
1175
1199
|
input.close();
|
|
1176
1200
|
await input.closed;
|
|
1177
|
-
return;
|
|
1201
|
+
return { next: "exit" };
|
|
1178
1202
|
}
|
|
1179
1203
|
// The REPL is ready: replay anything that arrived during recovery.
|
|
1180
1204
|
replReady = true;
|
|
@@ -1188,7 +1212,9 @@ export async function chat(session, faux, input, autoCompact) {
|
|
|
1188
1212
|
}
|
|
1189
1213
|
queuedLines.length = 0;
|
|
1190
1214
|
input.prompt();
|
|
1191
|
-
|
|
1215
|
+
// the REPL ends by CLOSE (exit) or by SWITCH (/clear, /resume) — the
|
|
1216
|
+
// switch leaves the editor alive for the next chat() entry
|
|
1217
|
+
await Promise.race([input.closed, endSignal]);
|
|
1192
1218
|
await chainRef.current; // never exit while a turn is in flight
|
|
1193
1219
|
// the ergonomics batch C8: the auto-compact may have appended ITS segment inside the
|
|
1194
1220
|
// turn (the check runs at the turn's end, after the exit-await above
|
|
@@ -1196,4 +1222,5 @@ export async function chat(session, faux, input, autoCompact) {
|
|
|
1196
1222
|
// runs before the exit or the chain is already settled. One level is
|
|
1197
1223
|
// enough: the /compact segment appends nothing of its own.
|
|
1198
1224
|
await chainRef.current;
|
|
1225
|
+
return switchTo === null ? { next: "exit" } : { next: "switch", id: switchTo };
|
|
1199
1226
|
}
|
package/dist/dispatch.d.ts
CHANGED
|
@@ -25,6 +25,13 @@ export interface DispatchCtx {
|
|
|
25
25
|
/** TUI2-R1 (E): the model's context window, as the session is
|
|
26
26
|
* configured — the /context ledger's denominator. */
|
|
27
27
|
readonly contextWindow: () => number;
|
|
28
|
+
/** the /resume+/clear mini-spec: end this chat() with a switch to
|
|
29
|
+
* another session — main re-enters chat there; the editor survives. */
|
|
30
|
+
readonly requestSwitch: (id: string) => void;
|
|
31
|
+
/** every durable session id (the /resume validation + listing). */
|
|
32
|
+
readonly sessions: () => readonly string[];
|
|
33
|
+
/** the dock's session picker, when one exists (bare /resume). */
|
|
34
|
+
readonly pickSession?: () => Promise<string | null>;
|
|
28
35
|
}
|
|
29
36
|
/** The ONE dispatcher — slash commands, exit, and turns. The recovery
|
|
30
37
|
* replay routes through it too — a queued "/last" must never become a
|
package/dist/dispatch.js
CHANGED
|
@@ -4,9 +4,10 @@
|
|
|
4
4
|
* the context (the chain, the run state, the prompt arming).
|
|
5
5
|
*/
|
|
6
6
|
import { contextRows, contextUnavailableRows, displayVerb, escapeTerminal, helpRows, kUnit, modelPickView, palette } from "@vincemakes/kiso-tui";
|
|
7
|
+
import { newSessionId } from "./session-id.js";
|
|
7
8
|
import { buildAdapter } from "@vincemakes/kiso-runtime/internal";
|
|
8
9
|
import { MODES, getMode, setMode } from "./mode.js";
|
|
9
|
-
import { agentModel, body, bodyLog, configModels, dock, readContextLedger, setAgentModel, setCurrentModelName } from "./state.js";
|
|
10
|
+
import { agentModel, body, bodyLog, configModels, dock, readContextLedger, sessionsDir, setAgentModel, setCurrentModelName } from "./state.js";
|
|
10
11
|
import { directWriteProfile, profileAvailable } from "./config.js";
|
|
11
12
|
/** The ONE dispatcher — slash commands, exit, and turns. The recovery
|
|
12
13
|
* replay routes through it too — a queued "/last" must never become a
|
|
@@ -263,7 +264,7 @@ export function dispatch(line, ctx) {
|
|
|
263
264
|
});
|
|
264
265
|
return;
|
|
265
266
|
}
|
|
266
|
-
if (trimmed === "/compact") {
|
|
267
|
+
if (trimmed === "/compact" || trimmed.startsWith("/compact ")) {
|
|
267
268
|
// /compact (ADR-0044): the older conversation becomes one
|
|
268
269
|
// model summary — an OFF-LOOP call through the session's own
|
|
269
270
|
// adapter, so it must never race a running turn: refused
|
|
@@ -306,8 +307,13 @@ export function dispatch(line, ctx) {
|
|
|
306
307
|
}, 1000);
|
|
307
308
|
};
|
|
308
309
|
try {
|
|
310
|
+
// R3a: /compact <focus> — the words after the command steer
|
|
311
|
+
// the summary ("keep the auth details"); bare /compact is
|
|
312
|
+
// byte-identical to the pre-round call.
|
|
313
|
+
const focus = trimmed.slice(8).trim();
|
|
309
314
|
const result = await ctx.session.summarize({
|
|
310
315
|
signal: abort.signal,
|
|
316
|
+
...(focus !== "" ? { focus } : {}),
|
|
311
317
|
onStart: (info) => {
|
|
312
318
|
compactInfo = info;
|
|
313
319
|
compacting(info);
|
|
@@ -369,6 +375,68 @@ export function dispatch(line, ctx) {
|
|
|
369
375
|
ctx.input.close();
|
|
370
376
|
return;
|
|
371
377
|
}
|
|
378
|
+
if (trimmed === "/clear") {
|
|
379
|
+
// the mini-spec: /clear = a FRESH conversation. The old session
|
|
380
|
+
// stays on disk, resumable — the append-only law does not move;
|
|
381
|
+
// what clears is the CONTEXT, never the history.
|
|
382
|
+
if (ctx.isRunning()) {
|
|
383
|
+
body.notice("[/clear] a run is in flight — let it finish (esc stops it), then clear");
|
|
384
|
+
ctx.input.prompt();
|
|
385
|
+
return;
|
|
386
|
+
}
|
|
387
|
+
ctx.requestSwitch(newSessionId(sessionsDir()));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (trimmed === "/resume" || trimmed.startsWith("/resume ")) {
|
|
391
|
+
// the mini-spec: the in-session door to the durable sessions —
|
|
392
|
+
// /resume <id> switches directly; bare /resume opens the SAME
|
|
393
|
+
// picker `kiso resume` owns (dock), or lists ids (no dock).
|
|
394
|
+
if (ctx.isRunning()) {
|
|
395
|
+
body.notice("[/resume] a run is in flight — let it finish (esc stops it), then switch");
|
|
396
|
+
ctx.input.prompt();
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const arg = trimmed.slice(7).trim();
|
|
400
|
+
if (arg !== "") {
|
|
401
|
+
// a switch never silently CREATES a session — agent.session()
|
|
402
|
+
// would; the validation is the difference
|
|
403
|
+
if (!ctx.sessions().includes(arg)) {
|
|
404
|
+
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
405
|
+
bodyLog(`no such session: ${escapeTerminal(arg)} — /resume lists them`);
|
|
406
|
+
ctx.input.prompt();
|
|
407
|
+
});
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
410
|
+
ctx.requestSwitch(arg);
|
|
411
|
+
return;
|
|
412
|
+
}
|
|
413
|
+
const others = ctx.sessions().filter((id) => id !== ctx.session.id);
|
|
414
|
+
if (others.length === 0) {
|
|
415
|
+
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
416
|
+
bodyLog("no other sessions — /clear starts a fresh one");
|
|
417
|
+
ctx.input.prompt();
|
|
418
|
+
});
|
|
419
|
+
return;
|
|
420
|
+
}
|
|
421
|
+
if (ctx.pickSession !== undefined && dock.active) {
|
|
422
|
+
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
423
|
+
const picked = await ctx.pickSession();
|
|
424
|
+
if (picked === null) {
|
|
425
|
+
ctx.input.prompt(); // esc — nothing switched, nothing said
|
|
426
|
+
return;
|
|
427
|
+
}
|
|
428
|
+
ctx.requestSwitch(picked);
|
|
429
|
+
});
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
ctx.chainRef.current = ctx.chainRef.current.then(async () => {
|
|
433
|
+
for (const id of others)
|
|
434
|
+
bodyLog(` ${escapeTerminal(id)}`);
|
|
435
|
+
bodyLog("switch with /resume <id>");
|
|
436
|
+
ctx.input.prompt();
|
|
437
|
+
});
|
|
438
|
+
return;
|
|
439
|
+
}
|
|
372
440
|
// PH-1a (finding PH-F1): an unrecognized slash command is an ERROR,
|
|
373
441
|
// never a turn — the fallthrough used to hand "/clear", "/exit", or a
|
|
374
442
|
// typo to the model, burning a request on text the user meant as a
|
package/dist/index.js
CHANGED
|
@@ -23,7 +23,8 @@
|
|
|
23
23
|
* index.ts keeps the entry: banner, input sources, the A area prompt,
|
|
24
24
|
* makeAgent, and main.
|
|
25
25
|
*/
|
|
26
|
-
import { readFileSync, realpathSync, rmSync } from "node:fs";
|
|
26
|
+
import { appendFileSync, mkdirSync, readFileSync, realpathSync, rmSync, writeFileSync } from "node:fs";
|
|
27
|
+
import { newSessionId } from "./session-id.js";
|
|
27
28
|
import { createInterface } from "node:readline";
|
|
28
29
|
import { fileURLToPath } from "node:url";
|
|
29
30
|
import { join } from "node:path";
|
|
@@ -33,7 +34,7 @@ import { createFauxProvider } from "@vincemakes/kiso-evals";
|
|
|
33
34
|
import { createCodingTools } from "@vincemakes/kiso-tools-node";
|
|
34
35
|
import { MODES, getMode, modeExtensions, modeFromEnv, modeSystemPrompt, setMode } from "./mode.js";
|
|
35
36
|
import { builtInLayer } from "./builtin.js";
|
|
36
|
-
import { agentModel, atFiles, body, bodyLog, builtInExtensions, currentFaux, dock, extensionsDir, loadedExtensions, mergedConfig, mergedTempPaths, projectExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setExtensionLists, setMergedConfig, setSessionStore, userExtensions, VERSION } from "./state.js";
|
|
37
|
+
import { agentModel, atFiles, body, bodyLog, kisoHome, builtInExtensions, currentFaux, dock, extensionsDir, loadedExtensions, mergedConfig, mergedTempPaths, projectExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setExtensionLists, setMergedConfig, setSessionStore, userExtensions, VERSION } from "./state.js";
|
|
37
38
|
import { askUi, resolveProjectTrust } from "./trust-ui.js";
|
|
38
39
|
import { isFirstRun, scaffoldFirstRun } from "./first-run.js";
|
|
39
40
|
import { fauxSkip, readFauxScript } from "./faux-glue.js";
|
|
@@ -188,6 +189,13 @@ function editorInput(editor) {
|
|
|
188
189
|
bindQueue(state, pop) {
|
|
189
190
|
editor.bindQueue(state, pop);
|
|
190
191
|
},
|
|
192
|
+
// R3a: cross-session history — the CLI owns the file I/O.
|
|
193
|
+
bindHistory(seed, persist) {
|
|
194
|
+
editor.bindHistory(seed, persist);
|
|
195
|
+
},
|
|
196
|
+
onModeCycle(cb) {
|
|
197
|
+
editor.onModeCycle(cb);
|
|
198
|
+
},
|
|
191
199
|
emitLine() {
|
|
192
200
|
/* the editor's buffer survives a cancelled question — its text
|
|
193
201
|
* becomes the next turn on Enter (the readline re-emit
|
|
@@ -570,6 +578,38 @@ function paintBootStatus(session) {
|
|
|
570
578
|
class CliUsageError extends Error {
|
|
571
579
|
exitCode = 2;
|
|
572
580
|
}
|
|
581
|
+
/** The /resume+/clear mini-spec — the chat LOOP: chat() ends with a
|
|
582
|
+
* directive; a switch re-enters it on another session with the SAME
|
|
583
|
+
* editor. First entry paints the banner; a switch paints one notice
|
|
584
|
+
* line (the previous conversation stays resumable — clear/switch
|
|
585
|
+
* never erase history). Faux sessions re-arm the scripted adapter at
|
|
586
|
+
* the NEW session's durable position, exactly like the picker path. */
|
|
587
|
+
async function chatLoop(agent, firstId, input, autoCompact) {
|
|
588
|
+
let id = firstId;
|
|
589
|
+
let prev = null;
|
|
590
|
+
for (;;) {
|
|
591
|
+
const session = await agent.session({ id });
|
|
592
|
+
if (prev === null) {
|
|
593
|
+
bodyLog(`session ${id}\n`);
|
|
594
|
+
extensionsBanner(await recentSessions(id, agent));
|
|
595
|
+
}
|
|
596
|
+
else {
|
|
597
|
+
bodyLog(`session ${id} (switched — previous: ${prev}, /resume ${prev} returns)\n`);
|
|
598
|
+
if (currentFaux)
|
|
599
|
+
session.setAdapter(createFauxProvider(readFauxScript().slice(fauxSkip(id))));
|
|
600
|
+
}
|
|
601
|
+
paintBootStatus(session);
|
|
602
|
+
const nav = {
|
|
603
|
+
sessions: () => agent.sessions().map((m) => m.id),
|
|
604
|
+
...(process.stdin.isTTY ? { pick: () => pickSession(agent, input) } : {}),
|
|
605
|
+
};
|
|
606
|
+
const end = await chat(session, currentFaux, input, autoCompact, nav);
|
|
607
|
+
if (end.next === "exit")
|
|
608
|
+
return;
|
|
609
|
+
prev = id;
|
|
610
|
+
id = end.id;
|
|
611
|
+
}
|
|
612
|
+
}
|
|
573
613
|
async function main() {
|
|
574
614
|
// E group (the graceful-exit gate ③, R-G 0.1.48): a terminal closing
|
|
575
615
|
// turns the in-flight stdout/stderr writes into EIO, and node's
|
|
@@ -598,6 +638,22 @@ async function main() {
|
|
|
598
638
|
console.log(VERSION);
|
|
599
639
|
return;
|
|
600
640
|
}
|
|
641
|
+
// R3a: -p/--print — the one-shot prompt mode (the F3 adjudication's
|
|
642
|
+
// forward path: a bare quoted argument stays a session id; the
|
|
643
|
+
// PROMPT is explicit). `kiso -p "fix the bug"` runs one turn on a
|
|
644
|
+
// fresh session and exits; an optional trailing session id continues
|
|
645
|
+
// that session one-shot instead. Exit code: 0 only when the turn's
|
|
646
|
+
// terminal is `completed` — scripts can trust it.
|
|
647
|
+
let printPrompt;
|
|
648
|
+
const printIdx = args.findIndex((a) => a === "-p" || a === "--print");
|
|
649
|
+
if (printIdx !== -1) {
|
|
650
|
+
printPrompt = args[printIdx + 1];
|
|
651
|
+
if (printPrompt === undefined) {
|
|
652
|
+
console.error('usage: kiso -p "prompt" [sessionId]');
|
|
653
|
+
process.exit(2);
|
|
654
|
+
}
|
|
655
|
+
args.splice(printIdx, 2);
|
|
656
|
+
}
|
|
601
657
|
// merge round B: --model <profile|provider/model> — the top of the model
|
|
602
658
|
// precedence chain; the value flows into makeAgent's config resolution.
|
|
603
659
|
let modelFlag;
|
|
@@ -638,6 +694,33 @@ async function main() {
|
|
|
638
694
|
// readline elsewhere. The trust question, chat, and resume all read
|
|
639
695
|
// through it; main's finally closes it on every exit path.
|
|
640
696
|
const input = makeLineInput();
|
|
697
|
+
// R3a — cross-session input history: ~/.kiso/history, one line per
|
|
698
|
+
// entry, appended on submit, tail-500 at load (truncated by REWRITE
|
|
699
|
+
// at startup so the file never grows unbounded). Unreadable file =
|
|
700
|
+
// an empty history, silently — recall is a convenience, never a
|
|
701
|
+
// startup risk. Control-character lines never enter the file (the
|
|
702
|
+
// editor's own recall excludes them by construction: a submitted
|
|
703
|
+
// line is printable input).
|
|
704
|
+
if (input.bindHistory !== undefined) {
|
|
705
|
+
const historyPath = join(kisoHome(), "history");
|
|
706
|
+
let seed = [];
|
|
707
|
+
try {
|
|
708
|
+
seed = readFileSync(historyPath, "utf8").split("\n").filter((l) => l !== "").slice(-500);
|
|
709
|
+
writeFileSync(historyPath, seed.length > 0 ? seed.join("\n") + "\n" : "");
|
|
710
|
+
}
|
|
711
|
+
catch {
|
|
712
|
+
// no file yet, or unreadable — start empty
|
|
713
|
+
}
|
|
714
|
+
input.bindHistory(seed, (line) => {
|
|
715
|
+
try {
|
|
716
|
+
mkdirSync(kisoHome(), { recursive: true });
|
|
717
|
+
appendFileSync(historyPath, line.replaceAll("\n", " ") + "\n");
|
|
718
|
+
}
|
|
719
|
+
catch {
|
|
720
|
+
// best-effort — a full disk never breaks a submit
|
|
721
|
+
}
|
|
722
|
+
});
|
|
723
|
+
}
|
|
641
724
|
// PH-1a (finding PH-F6, RESOLVED AS WON'T-FIX-IN-JS — the tcsetattr
|
|
642
725
|
// ruling): SIGTERM/SIGHUP deliberately keep their DEFAULT disposition.
|
|
643
726
|
// A JS handler that restored the terminal was built and then reverted
|
|
@@ -670,9 +753,22 @@ async function main() {
|
|
|
670
753
|
if (modeFlag === -1 && process.env.KISO_MODE === undefined && mergedConfig.mode !== undefined)
|
|
671
754
|
setMode(mergedConfig.mode);
|
|
672
755
|
};
|
|
756
|
+
if (printPrompt !== undefined) {
|
|
757
|
+
// the -p flow: recovery-first one-shot, the resume() machinery
|
|
758
|
+
// verbatim (a fresh id makes the recovery a no-op)
|
|
759
|
+
const id = command ?? newSessionId(sessionsDir());
|
|
760
|
+
agent = await makeAgent(id, input, modelFlag);
|
|
761
|
+
applyConfigMode();
|
|
762
|
+
const session = await agent.session({ id });
|
|
763
|
+
faux = currentFaux;
|
|
764
|
+
await resume(session, printPrompt, faux, input);
|
|
765
|
+
const last = [...session.log.all].reverse().find((e) => e.type === "terminal");
|
|
766
|
+
process.exitCode = last !== undefined && last.outcome.kind === "completed" ? 0 : 1;
|
|
767
|
+
return;
|
|
768
|
+
}
|
|
673
769
|
switch (command) {
|
|
674
770
|
case "chat": {
|
|
675
|
-
const id = arg ??
|
|
771
|
+
const id = arg ?? newSessionId(sessionsDir());
|
|
676
772
|
// v2b: the dock (TTY only) wraps the whole session — the
|
|
677
773
|
// trust question, the banner, the body, and the input line.
|
|
678
774
|
dock.enter();
|
|
@@ -680,12 +776,8 @@ async function main() {
|
|
|
680
776
|
// position — never restarts it (fauxSkip).
|
|
681
777
|
agent = await makeAgent(id, input, modelFlag);
|
|
682
778
|
applyConfigMode();
|
|
683
|
-
const session = await agent.session({ id });
|
|
684
|
-
bodyLog(`session ${id}\n`);
|
|
685
|
-
extensionsBanner(await recentSessions(id, agent));
|
|
686
779
|
faux = currentFaux;
|
|
687
|
-
|
|
688
|
-
await chat(session, faux, input, resolveAutoCompact(mergedConfig));
|
|
780
|
+
await chatLoop(agent, id, input, resolveAutoCompact(mergedConfig));
|
|
689
781
|
break;
|
|
690
782
|
}
|
|
691
783
|
case "resume": {
|
|
@@ -712,7 +804,14 @@ async function main() {
|
|
|
712
804
|
// an error, never a session started behind their back.
|
|
713
805
|
if (picked === null)
|
|
714
806
|
break;
|
|
715
|
-
|
|
807
|
+
// the mini-spec (a DECLARED SUPERSESSION of the one-shot
|
|
808
|
+
// picker flow): a PICKED session enters the full REPL —
|
|
809
|
+
// "resume and keep working" no longer requires knowing to
|
|
810
|
+
// type `kiso chat <id>`. The explicit-id one-shot form
|
|
811
|
+
// (`kiso resume <id> ["prompt"]`) keeps its exact bytes.
|
|
812
|
+
faux = currentFaux;
|
|
813
|
+
await chatLoop(agent, picked, input, resolveAutoCompact(mergedConfig));
|
|
814
|
+
break;
|
|
716
815
|
}
|
|
717
816
|
const session = await agent.session({ id });
|
|
718
817
|
faux = currentFaux;
|
|
@@ -786,7 +885,7 @@ async function main() {
|
|
|
786
885
|
default: {
|
|
787
886
|
// A area: no subcommand (or any non-command first argument) IS
|
|
788
887
|
// chat — the first argument is the session id.
|
|
789
|
-
const id = command ??
|
|
888
|
+
const id = command ?? newSessionId(sessionsDir());
|
|
790
889
|
dock.enter();
|
|
791
890
|
// R-I-p2 (finding R-I-p-2): the bare command passes the SAME
|
|
792
891
|
// input source and model flag as chat/resume — the pre-patch
|
|
@@ -795,17 +894,9 @@ async function main() {
|
|
|
795
894
|
// undefined" at the ask (panelAsk with the dock, question on
|
|
796
895
|
// the dock-less fallback).
|
|
797
896
|
agent = await makeAgent(id, input, modelFlag);
|
|
798
|
-
|
|
799
|
-
bodyLog(`session ${id}\n`);
|
|
800
|
-
extensionsBanner(await recentSessions(id, agent));
|
|
801
|
-
// finding E4-1: the same faux resolution the chat/resume cases
|
|
802
|
-
// carry (faux = currentFaux) — pre-patch the initial faux=true
|
|
803
|
-
// was passed here, so ANY provider failure in a bare-command
|
|
804
|
-
// session surfaced as "[faux mode] the scripted model failed:
|
|
805
|
-
// <real error>" (a false accusation of the keyless demo).
|
|
897
|
+
// finding E4-1's faux resolution rides chatLoop (currentFaux).
|
|
806
898
|
faux = currentFaux;
|
|
807
|
-
|
|
808
|
-
await chat(session, faux, input, autoCompactFromEnv());
|
|
899
|
+
await chatLoop(agent, id, input, autoCompactFromEnv());
|
|
809
900
|
break;
|
|
810
901
|
}
|
|
811
902
|
}
|
|
@@ -869,7 +960,7 @@ function exitFlushed(code) {
|
|
|
869
960
|
}
|
|
870
961
|
if (process.argv[1] !== undefined && realpathSync(process.argv[1]) === fileURLToPath(import.meta.url)) {
|
|
871
962
|
main()
|
|
872
|
-
.then(() => exitFlushed(0))
|
|
963
|
+
.then(() => exitFlushed(typeof process.exitCode === "number" ? process.exitCode : 0))
|
|
873
964
|
.catch((err) => {
|
|
874
965
|
// round 10: top-level errors are terminal-escaped. v2a: the exit is EXPLICIT
|
|
875
966
|
// — natural drain is racy on a TTY (readline leaves the stdio handles
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RD1B-F9 — the auto-generated session id, in ONE place.
|
|
3
|
+
*
|
|
4
|
+
* It used to be this expression, copied at four call sites:
|
|
5
|
+
*
|
|
6
|
+
* new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16)
|
|
7
|
+
*
|
|
8
|
+
* which stops at the MINUTE and carries no entropy. `SessionStore` is one
|
|
9
|
+
* file per id, so two sessions started in the same minute were the same
|
|
10
|
+
* session: the second launch presented as fresh and transparently appended
|
|
11
|
+
* to the first one's durable log (`tests/session-id-identity.test.ts`).
|
|
12
|
+
*
|
|
13
|
+
* WHAT THE GUARANTEE IS, precisely — because the first version of this
|
|
14
|
+
* comment claimed "collision-safe at any launch rate a human or a script
|
|
15
|
+
* produces" and that was an unmeasured claim that is false. A 16-bit
|
|
16
|
+
* suffix collides at script rates: 100 launches inside one second carry a
|
|
17
|
+
* 7.3% chance of at least one collision, and 1,000 produce a handful every
|
|
18
|
+
* time. Measured, not modelled.
|
|
19
|
+
*
|
|
20
|
+
* So the id does not rest on entropy at all:
|
|
21
|
+
*
|
|
22
|
+
* - SEQUENTIAL collision is eliminated BY CONSTRUCTION. `newSessionId`
|
|
23
|
+
* is handed the sessions directory and will not return an id whose
|
|
24
|
+
* durable log or lock already exists; it draws again. Entropy only
|
|
25
|
+
* decides how often it has to draw.
|
|
26
|
+
* - CONCURRENT collision — two processes drawing the same id before
|
|
27
|
+
* either has written — remains possible and is already handled
|
|
28
|
+
* correctly one layer down: the store's single-writer link lock
|
|
29
|
+
* (ADR-0050) fails the second writer loudly, and `storage.test.ts`
|
|
30
|
+
* pins that. Loud failure is the right outcome there; silent sharing
|
|
31
|
+
* was the defect.
|
|
32
|
+
*
|
|
33
|
+
* The id keeps the one property anything depends on: **lexicographic order
|
|
34
|
+
* is time order**, because `listSessions` sorts with `id.localeCompare`
|
|
35
|
+
* and nothing anywhere parses an id back into a date. Seconds extend the
|
|
36
|
+
* stamp monotonically; the suffix only breaks ties inside one second.
|
|
37
|
+
*
|
|
38
|
+
* It also stays 24 characters — exactly the session picker's id column cap
|
|
39
|
+
* (`packages/tui/src/session-picker.ts:112`). Widening the suffix instead
|
|
40
|
+
* of checking for collisions would have pushed the distinguishing tail out
|
|
41
|
+
* of the column, hiding the very bytes that make two ids different.
|
|
42
|
+
*
|
|
43
|
+
* Old ids are untouched — no rename, no migration. They still resume by id
|
|
44
|
+
* and still sort before same-minute new ids.
|
|
45
|
+
*/
|
|
46
|
+
/**
|
|
47
|
+
* A fresh session id: `YYYY-MM-DDTHH-MM-SS-xxxx`, sortable, and — when
|
|
48
|
+
* `dir` is given — guaranteed not to name a session that already exists
|
|
49
|
+
* there.
|
|
50
|
+
*
|
|
51
|
+
* `rand` is injectable so the collision path can be tested; production
|
|
52
|
+
* never passes it.
|
|
53
|
+
*/
|
|
54
|
+
export declare function newSessionId(dir?: string, now?: Date, rand?: () => string): string;
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RD1B-F9 — the auto-generated session id, in ONE place.
|
|
3
|
+
*
|
|
4
|
+
* It used to be this expression, copied at four call sites:
|
|
5
|
+
*
|
|
6
|
+
* new Date().toISOString().replace(/[:.]/g, "-").slice(0, 16)
|
|
7
|
+
*
|
|
8
|
+
* which stops at the MINUTE and carries no entropy. `SessionStore` is one
|
|
9
|
+
* file per id, so two sessions started in the same minute were the same
|
|
10
|
+
* session: the second launch presented as fresh and transparently appended
|
|
11
|
+
* to the first one's durable log (`tests/session-id-identity.test.ts`).
|
|
12
|
+
*
|
|
13
|
+
* WHAT THE GUARANTEE IS, precisely — because the first version of this
|
|
14
|
+
* comment claimed "collision-safe at any launch rate a human or a script
|
|
15
|
+
* produces" and that was an unmeasured claim that is false. A 16-bit
|
|
16
|
+
* suffix collides at script rates: 100 launches inside one second carry a
|
|
17
|
+
* 7.3% chance of at least one collision, and 1,000 produce a handful every
|
|
18
|
+
* time. Measured, not modelled.
|
|
19
|
+
*
|
|
20
|
+
* So the id does not rest on entropy at all:
|
|
21
|
+
*
|
|
22
|
+
* - SEQUENTIAL collision is eliminated BY CONSTRUCTION. `newSessionId`
|
|
23
|
+
* is handed the sessions directory and will not return an id whose
|
|
24
|
+
* durable log or lock already exists; it draws again. Entropy only
|
|
25
|
+
* decides how often it has to draw.
|
|
26
|
+
* - CONCURRENT collision — two processes drawing the same id before
|
|
27
|
+
* either has written — remains possible and is already handled
|
|
28
|
+
* correctly one layer down: the store's single-writer link lock
|
|
29
|
+
* (ADR-0050) fails the second writer loudly, and `storage.test.ts`
|
|
30
|
+
* pins that. Loud failure is the right outcome there; silent sharing
|
|
31
|
+
* was the defect.
|
|
32
|
+
*
|
|
33
|
+
* The id keeps the one property anything depends on: **lexicographic order
|
|
34
|
+
* is time order**, because `listSessions` sorts with `id.localeCompare`
|
|
35
|
+
* and nothing anywhere parses an id back into a date. Seconds extend the
|
|
36
|
+
* stamp monotonically; the suffix only breaks ties inside one second.
|
|
37
|
+
*
|
|
38
|
+
* It also stays 24 characters — exactly the session picker's id column cap
|
|
39
|
+
* (`packages/tui/src/session-picker.ts:112`). Widening the suffix instead
|
|
40
|
+
* of checking for collisions would have pushed the distinguishing tail out
|
|
41
|
+
* of the column, hiding the very bytes that make two ids different.
|
|
42
|
+
*
|
|
43
|
+
* Old ids are untouched — no rename, no migration. They still resume by id
|
|
44
|
+
* and still sort before same-minute new ids.
|
|
45
|
+
*/
|
|
46
|
+
import { randomBytes } from "node:crypto";
|
|
47
|
+
import { existsSync } from "node:fs";
|
|
48
|
+
import { join } from "node:path";
|
|
49
|
+
/** How many draws before giving up. Reaching this means either the clock
|
|
50
|
+
* is frozen or the directory holds ~every suffix for this second; both
|
|
51
|
+
* are worth failing loudly over rather than returning a colliding id. */
|
|
52
|
+
const MAX_DRAWS = 50;
|
|
53
|
+
const stampOf = (now, suffix) => `${now.toISOString().replace(/[:.]/g, "-").slice(0, 19)}-${suffix}`;
|
|
54
|
+
/**
|
|
55
|
+
* A fresh session id: `YYYY-MM-DDTHH-MM-SS-xxxx`, sortable, and — when
|
|
56
|
+
* `dir` is given — guaranteed not to name a session that already exists
|
|
57
|
+
* there.
|
|
58
|
+
*
|
|
59
|
+
* `rand` is injectable so the collision path can be tested; production
|
|
60
|
+
* never passes it.
|
|
61
|
+
*/
|
|
62
|
+
export function newSessionId(dir, now = new Date(), rand = () => randomBytes(2).toString("hex")) {
|
|
63
|
+
if (dir === undefined)
|
|
64
|
+
return stampOf(now, rand());
|
|
65
|
+
for (let draw = 0; draw < MAX_DRAWS; draw += 1) {
|
|
66
|
+
const id = stampOf(now, rand());
|
|
67
|
+
// The store writes `<id>.jsonl` and takes `<id>.lock`; either one
|
|
68
|
+
// present means the id is spoken for, including by a session that
|
|
69
|
+
// has locked but not yet appended.
|
|
70
|
+
if (!existsSync(join(dir, `${id}.jsonl`)) && !existsSync(join(dir, `${id}.lock`)))
|
|
71
|
+
return id;
|
|
72
|
+
}
|
|
73
|
+
throw new Error(`could not draw an unused session id in ${dir} after ${MAX_DRAWS} attempts`);
|
|
74
|
+
}
|
package/dist/state.d.ts
CHANGED
|
@@ -100,6 +100,11 @@ export interface LineInput {
|
|
|
100
100
|
* live slots (each pop cancels the turn), esc ends the walk after
|
|
101
101
|
* one more pop. The chips are the compositor's own bindQueue. */
|
|
102
102
|
bindQueue(state: () => readonly string[], pop: () => string | null): void;
|
|
103
|
+
/** R3a: cross-session history — seed the recall buffer, register the
|
|
104
|
+
* append sink. The pipe path has no recall keys; optional. */
|
|
105
|
+
bindHistory?(seed: readonly string[], persist: (line: string) => void): void;
|
|
106
|
+
/** R3a: Shift+Tab cycles the approval tier (TTY editor only). */
|
|
107
|
+
onModeCycle?(cb: () => void): void;
|
|
103
108
|
emitLine(line: string): void;
|
|
104
109
|
line(): string;
|
|
105
110
|
clearLine(): void;
|
package/dist/trust-ui.js
CHANGED
|
@@ -70,6 +70,28 @@ opts) {
|
|
|
70
70
|
resolve(verdict);
|
|
71
71
|
}, opts);
|
|
72
72
|
}
|
|
73
|
+
else if (view.ask) {
|
|
74
|
+
// RD1B-F6: a multiple-choice ask has NO dock-less form. The
|
|
75
|
+
// fallback said so — "the question is declined" — and then
|
|
76
|
+
// waited for a line anyway, forever, on input nothing in the
|
|
77
|
+
// environment knows to send: the surface has just announced
|
|
78
|
+
// the interaction is over. An unattended run did not fail
|
|
79
|
+
// there, it stopped, silently (RD-1B c9-r2).
|
|
80
|
+
//
|
|
81
|
+
// So the sentence becomes true. There is no panel, therefore
|
|
82
|
+
// no answer is obtainable, therefore the ask declines NOW and
|
|
83
|
+
// the model gets an honest refusal to act on. Waiting could
|
|
84
|
+
// only ever have produced the same decline, later, and every
|
|
85
|
+
// line typed at it was discarded anyway.
|
|
86
|
+
//
|
|
87
|
+
// The y/n fallback below still serves the views that really
|
|
88
|
+
// do take a yes or no — the uncertainty gate, the trust
|
|
89
|
+
// prompt, the verify offer. Those have a dock-less form.
|
|
90
|
+
settled = true;
|
|
91
|
+
pendingAsk = null;
|
|
92
|
+
bodyLog(view.fallbackQuestion);
|
|
93
|
+
resolve({ action: "deny", reason: "no option panel in this terminal" });
|
|
94
|
+
}
|
|
73
95
|
else {
|
|
74
96
|
// v2c: a TTY without a dock (rows < 4) — the fallback question
|
|
75
97
|
// in the body; the y/n line answer maps to the verdicts.
|
package/package.json
CHANGED
|
@@ -1,56 +1,56 @@
|
|
|
1
1
|
{
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
2
|
+
"name": "@vincemakes/kiso-code",
|
|
3
|
+
"version": "0.15.2",
|
|
4
|
+
"description": "kiso CLI \u2014 the durable coding agent that survives kill -9: kiso chat / kiso resume / kiso sessions.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"bin": {
|
|
8
|
+
"kiso": "dist/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"dist",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"scripts": {
|
|
16
|
+
"build": "tsc -p tsconfig.build.json",
|
|
17
|
+
"typecheck": "tsc -p tsconfig.json",
|
|
18
|
+
"test": "vitest run"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"@vincemakes/kiso-ask-ext": "0.15.2",
|
|
22
|
+
"@vincemakes/kiso-core": "0.15.2",
|
|
23
|
+
"@vincemakes/kiso-evals": "0.15.2",
|
|
24
|
+
"@vincemakes/kiso-mcp-ext": "0.15.2",
|
|
25
|
+
"@vincemakes/kiso-provider-anthropic": "0.15.2",
|
|
26
|
+
"@vincemakes/kiso-provider-openai": "0.15.2",
|
|
27
|
+
"@vincemakes/kiso-runtime": "0.15.2",
|
|
28
|
+
"@vincemakes/kiso-skills-ext": "0.15.2",
|
|
29
|
+
"@vincemakes/kiso-subagent-ext": "0.15.2",
|
|
30
|
+
"@vincemakes/kiso-task-ext": "0.15.2",
|
|
31
|
+
"@vincemakes/kiso-tools-node": "0.15.2",
|
|
32
|
+
"@vincemakes/kiso-tui": "0.15.2",
|
|
33
|
+
"@vincemakes/kiso-tui-cells": "0.15.2"
|
|
34
|
+
},
|
|
35
|
+
"devDependencies": {
|
|
36
|
+
"@types/node": "^26.1.2",
|
|
37
|
+
"typescript": "^5.7.2",
|
|
38
|
+
"vitest": "^3.0.0"
|
|
39
|
+
},
|
|
40
|
+
"engines": {
|
|
41
|
+
"node": ">=22"
|
|
42
|
+
},
|
|
43
|
+
"os": [
|
|
44
|
+
"darwin",
|
|
45
|
+
"linux"
|
|
46
|
+
],
|
|
47
|
+
"repository": {
|
|
48
|
+
"type": "git",
|
|
49
|
+
"url": "https://github.com/vincemakes/kiso.git",
|
|
50
|
+
"directory": "apps/cli"
|
|
51
|
+
},
|
|
52
|
+
"bugs": {
|
|
53
|
+
"url": "https://github.com/vincemakes/kiso/issues"
|
|
54
|
+
},
|
|
55
|
+
"homepage": "https://github.com/vincemakes/kiso/tree/main/apps/cli#readme"
|
|
56
56
|
}
|