@vincemakes/kiso-code 0.39.2 → 0.40.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 +1 -1
- package/dist/attachments.d.ts +1 -1
- package/dist/attachments.js +11 -4
- package/dist/builtin.d.ts +12 -0
- package/dist/builtin.js +22 -1
- package/dist/chat.d.ts +31 -3
- package/dist/chat.js +169 -30
- package/dist/config.d.ts +11 -1
- package/dist/config.js +34 -3
- package/dist/dispatch.d.ts +12 -2
- package/dist/dispatch.js +165 -24
- package/dist/floor.d.ts +80 -0
- package/dist/floor.js +652 -0
- package/dist/index.d.ts +21 -2
- package/dist/index.js +463 -46
- package/dist/mode.d.ts +12 -4
- package/dist/mode.js +32 -10
- package/dist/projects.d.ts +106 -0
- package/dist/projects.js +212 -0
- package/dist/protected-shell.d.ts +88 -0
- package/dist/protected-shell.js +406 -0
- package/dist/protected-writes.d.ts +29 -0
- package/dist/protected-writes.js +54 -0
- package/dist/provider-label.d.ts +32 -0
- package/dist/provider-label.js +53 -0
- package/dist/readonly-shell.d.ts +58 -0
- package/dist/readonly-shell.js +590 -0
- package/dist/replay.d.ts +53 -0
- package/dist/replay.js +158 -0
- package/dist/resume-tail.js +4 -1
- package/dist/resume.js +3 -3
- package/dist/retries.d.ts +9 -0
- package/dist/retries.js +16 -0
- package/dist/session-cards.d.ts +33 -4
- package/dist/session-cards.js +44 -3
- package/dist/session-migration.d.ts +104 -0
- package/dist/session-migration.js +319 -0
- package/dist/shell-words.d.ts +134 -0
- package/dist/shell-words.js +628 -0
- package/dist/skill-invoke.d.ts +58 -0
- package/dist/skill-invoke.js +139 -0
- package/dist/state.d.ts +80 -1
- package/dist/state.js +113 -2
- package/dist/trust-ui.js +29 -5
- package/package.json +15 -15
package/dist/index.js
CHANGED
|
@@ -30,18 +30,27 @@ import { createInterface } from "node:readline";
|
|
|
30
30
|
import { Readable } from "node:stream";
|
|
31
31
|
import { fileURLToPath } from "node:url";
|
|
32
32
|
import { join } from "node:path";
|
|
33
|
-
import { Body, Editor, PROMPT, bannerLines, currentGround, resolveGround, setGround, escapeTerminal, extensionsBannerText, idColumn, idleStatus, interactivePrompt, palette, renderSessionLine, sessionListFooter, sessionListRow } from "@vincemakes/kiso-tui";
|
|
33
|
+
import { Body, Editor, PROMPT, bannerLines, currentGround, resolveGround, setGround, escapeTerminal, extensionsBannerText, idColumn, idleStatus, interactivePrompt, palette, renderSessionLine, sessionListFooter, sessionListHeader, sessionListRow, sessionListUnknownLine, slashCommandNames } from "@vincemakes/kiso-tui";
|
|
34
34
|
import { createAgent, disposeExtensions, loadExtensions, loadProjectExtensions, SessionStore, } from "@vincemakes/kiso-runtime";
|
|
35
|
+
import { listSessionSidecars, migrateSummaries, readProfile, runsACheck, summaryMigrationPending } from "@vincemakes/kiso-runtime/internal";
|
|
36
|
+
import { skillMenuItems } from "./skill-invoke.js";
|
|
37
|
+
import { canonicalPath, claimProjectDir, hasSession, locateSession, projectLayoutActive, sessionFolders } from "./projects.js";
|
|
38
|
+
import { migrationNotice, pendingLegacyIds, planMigration, reverseMigration, runMigration } from "./session-migration.js";
|
|
35
39
|
import { createFauxProvider } from "@vincemakes/kiso-evals";
|
|
36
|
-
import { createCodingTools } from "@vincemakes/kiso-tools-node";
|
|
37
|
-
import { MODES, getMode, modeExtensions, modeFromEnv, modeSystemPrompt, setMode } from "./mode.js";
|
|
40
|
+
import { createCodingTools, isProtectedPath, protectedIdentity, PROTECTED_REFUSAL } from "@vincemakes/kiso-tools-node";
|
|
41
|
+
import { MODES, OFFERED_MODES, getMode, modeExtensions, modeFromEnv, modeSystemPrompt, setMode } from "./mode.js";
|
|
42
|
+
import { readOnlyShellExtension } from "./readonly-shell.js";
|
|
43
|
+
import { guardSavedAllow, isProtectedWrite } from "./protected-writes.js";
|
|
44
|
+
import { floorExtension, isDestructiveCall } from "./floor.js";
|
|
45
|
+
import { protectedShellExtension } from "./protected-shell.js";
|
|
38
46
|
import { breakerExtension } from "./breaker.js";
|
|
39
47
|
import { builtInLayer } from "./builtin.js";
|
|
40
|
-
import { agentModel, atFiles, body, bodyLog, codingToolOptions, kisoHome, builtInExtensions, currentFaux, dock, extensionsDir, loadedExtensions, mergedConfig, mergedTempPaths, modelChoice, projectExtensions, configModels, configuredWindow, agentBaseUrl, currentModelName, currentAgentExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setExtensionLists, setMergedConfig, setModelChoice, setSessionStore, secretEnvNamesOf, userExtensions, VERSION, lastBinding, acceptDrift, setAcceptDrift } from "./state.js";
|
|
48
|
+
import { agentModel, atFiles, body, bodyLog, codingToolOptions, kisoHome, workspaceRoot, projectRoot, ownSessionsDir, setOpenSessionFolder, builtInExtensions, currentFaux, dock, extensionsDir, loadedExtensions, mergedConfig, mergedTempPaths, modelChoice, projectExtensions, configModels, configuredWindow, agentBaseUrl, currentModelName, currentAgentExtensions, sessionStoreRef, sessionsDir, setAgentModel, setBody, setConfigModels, setConfiguredWindow, setCurrentAgentExtensions, setCurrentFaux, setCurrentModelName, setCurrentProfileName, setExtensionLists, setUserProtectedPaths, protectedFiles, setMergedConfig, setModelChoice, setSessionStore, setRetryShown, setNeverInherited, secretEnvNamesOf, userExtensions, VERSION, lastBinding, acceptDrift, setAcceptDrift, setFloorOn, floorOn, loadedSkillsCatalog, queuedSwitchLines } from "./state.js";
|
|
49
|
+
import { maxRetriesFromEnv } from "./retries.js";
|
|
41
50
|
import { askUi, resolveProjectTrust } from "./trust-ui.js";
|
|
42
51
|
import { isFirstRun, scaffoldFirstRun } from "./first-run.js";
|
|
43
52
|
import { fauxSkip, readFauxScript } from "./faux-glue.js";
|
|
44
|
-
import { chat, displayCtxRatio, microcompactThresholdFor, statusModelLabel } from "./chat.js";
|
|
53
|
+
import { chat, contextWindowTokens, displayCtxRatio, microcompactThresholdFor, statusModelLabel } from "./chat.js";
|
|
45
54
|
import { adapterOptionsFor } from "./auth/adapter-options.js";
|
|
46
55
|
import { loadProjectConfig, loadUserConfig, mergeConfigs, resolveAutoCompact, resolveContextWindow, resolveModel } from "./config.js";
|
|
47
56
|
import { checkForUpdate, knownUpdate, updateCardLines } from "./update-check.js";
|
|
@@ -49,10 +58,11 @@ import { tmuxMouseHint } from "./tmux-hint.js";
|
|
|
49
58
|
import { resume } from "./resume.js";
|
|
50
59
|
import { paintWindowTitle } from "./window-title.js";
|
|
51
60
|
import { resumeTail } from "./resume-tail.js";
|
|
61
|
+
import { replayInto } from "./replay.js";
|
|
52
62
|
import { armByteTrace } from "./byte-trace.js";
|
|
53
63
|
import { tmpdir, homedir } from "node:os";
|
|
54
64
|
import { clipboardImage } from "./clipboard.js";
|
|
55
|
-
import {
|
|
65
|
+
import { cardsFromListings } from "./session-cards.js";
|
|
56
66
|
// The moved exports stay reachable from this entry — the test imports
|
|
57
67
|
// (project-trust, coding-agent) never change (B4: zero assertion changes).
|
|
58
68
|
export { applyProjectMerges } from "./trust-ui.js";
|
|
@@ -316,8 +326,8 @@ function editorInput(editor) {
|
|
|
316
326
|
// TUI2-R2 ②: the session picker — the editor owns the keys (the
|
|
317
327
|
// selection walk, the filter, enter/esc), the compositor draws the
|
|
318
328
|
// band, and the id comes back here.
|
|
319
|
-
pick(cards, onPick) {
|
|
320
|
-
editor.beginPick(cards, onPick);
|
|
329
|
+
pick(cards, onPick, here) {
|
|
330
|
+
editor.beginPick(cards, onPick, here);
|
|
321
331
|
},
|
|
322
332
|
// W22: the pending-turn queue — the ↑ pop walk (the keys); the
|
|
323
333
|
// chips are the compositor's bindQueue (the dock side).
|
|
@@ -372,7 +382,10 @@ function userThemeSetting() {
|
|
|
372
382
|
function makeLineInput() {
|
|
373
383
|
const userTheme = userThemeSetting();
|
|
374
384
|
if (process.stdin.isTTY) {
|
|
375
|
-
const editor = new Editor(() => (dock.active ? dock.redraw() : editor.selfRender()));
|
|
385
|
+
const editor = new Editor((fromKey) => (dock.active ? dock.redraw(fromKey) : editor.selfRender()));
|
|
386
|
+
// 0.40.1: the installed skills join the `/` menu — read live, so a
|
|
387
|
+
// skill /reload adds is offered on the next keystroke
|
|
388
|
+
editor.bindMenuExtras(() => skillMenuItems(loadedSkillsCatalog(), slashCommandNames()));
|
|
376
389
|
editor.enter();
|
|
377
390
|
// DC-3: ask the terminal what its background is, and paint the
|
|
378
391
|
// first frame without waiting for the answer.
|
|
@@ -455,6 +468,7 @@ function makeLineInput() {
|
|
|
455
468
|
// the panel's option rows, so the compositor is what the editor asks
|
|
456
469
|
// where they are. Neither side computes the other's geometry.
|
|
457
470
|
editor.bindPanelRows(() => dock.panelOptionRows());
|
|
471
|
+
editor.bindPickWindow(() => dock.visiblePickWindow());
|
|
458
472
|
dock.bindMenu(() => editor.menuState()); // v3 §04: the slash-command menu
|
|
459
473
|
editor.bindAtItems(atFiles); // KC3 §5: the file source — listed per OPEN
|
|
460
474
|
dock.bindAt(() => editor.atState()); // KC3 §4: the picker's band
|
|
@@ -484,7 +498,11 @@ function bannerExtensionText() {
|
|
|
484
498
|
// terminal layer (extensionsBannerText — a pure function of the three
|
|
485
499
|
// name lists, including the "(connecting…)" in-flight label). Which
|
|
486
500
|
// lists exist is the CLI's fact and stays here.
|
|
487
|
-
|
|
501
|
+
// 0.40.0: in dontAsk the ask extension is loaded but offers no tool
|
|
502
|
+
// (builtin.ts offInDontAsk) — the banner says so beside the tier that
|
|
503
|
+
// turns it off, rather than listing it as if it could ask.
|
|
504
|
+
const builtIn = builtInExtensions.map((e) => (e.name === "ask" && getMode() === "dontAsk" ? { name: e.name, note: "off in dontAsk" } : e));
|
|
505
|
+
return extensionsBannerText(builtIn, userExtensions, projectExtensions);
|
|
488
506
|
}
|
|
489
507
|
/** E1: the startup banner line(s) — TTY: logo + merged extensions + the
|
|
490
508
|
* W5 resume list as a LIVE banner cell (W1: the tier re-derives on
|
|
@@ -619,9 +637,18 @@ const INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
|
619
637
|
* return it as an injected section, or "" when none exists. Truncated at
|
|
620
638
|
* 8KB with an explicit note. Pure — read once per session, so the prompt
|
|
621
639
|
* is byte-stable for the session's lifetime.
|
|
640
|
+
*
|
|
641
|
+
* kiso never serves its own credential store to a model, and this read is
|
|
642
|
+
* the one that needs no model at all: a cloned repository whose AGENTS.md
|
|
643
|
+
* is a symlink to the store would put it in the SYSTEM PROMPT of every
|
|
644
|
+
* request, with no tool call and no trust prompt. A file that resolves to
|
|
645
|
+
* a protected one is skipped as though absent.
|
|
622
646
|
*/
|
|
623
|
-
export function readProjectInstructions(cwd) {
|
|
647
|
+
export function readProjectInstructions(cwd, protectedFiles = []) {
|
|
648
|
+
const id = protectedIdentity(protectedFiles);
|
|
624
649
|
for (const name of INSTRUCTION_FILES) {
|
|
650
|
+
if (isProtectedPath(join(cwd, name), id))
|
|
651
|
+
continue;
|
|
625
652
|
let text;
|
|
626
653
|
try {
|
|
627
654
|
text = readFileSync(join(cwd, name), "utf8");
|
|
@@ -635,8 +662,8 @@ export function readProjectInstructions(cwd) {
|
|
|
635
662
|
}
|
|
636
663
|
/** A area: the session's system prompt — the constant plus any project
|
|
637
664
|
* instructions found in the workspace. Deterministic per cwd. */
|
|
638
|
-
export function composeSystemPrompt(cwd) {
|
|
639
|
-
const injected = readProjectInstructions(cwd);
|
|
665
|
+
export function composeSystemPrompt(cwd, protectedFiles = []) {
|
|
666
|
+
const injected = readProjectInstructions(cwd, protectedFiles);
|
|
640
667
|
return injected === "" ? SYSTEM_PROMPT : `${SYSTEM_PROMPT}\n${injected}`;
|
|
641
668
|
}
|
|
642
669
|
/**
|
|
@@ -700,7 +727,12 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
700
727
|
// the trust record is the first home write, the scaffold the second.
|
|
701
728
|
if (isFirstRun())
|
|
702
729
|
scaffoldFirstRun();
|
|
703
|
-
|
|
730
|
+
// 0.40.0 — one folder per project: the one-time move of the legacy
|
|
731
|
+
// folder, then this project's folder claimed (made, its workspace
|
|
732
|
+
// recorded). Both write, so both sit behind the verdict, with the store.
|
|
733
|
+
prepareSessionFolders();
|
|
734
|
+
activeStoreDir = sessionsDir();
|
|
735
|
+
const store = new SessionStore(activeStoreDir);
|
|
704
736
|
// TUI2-R2 ②/③: the navigation surfaces read through THIS store — one
|
|
705
737
|
// store per process, and the picker/listing never write through it.
|
|
706
738
|
setSessionStore(store);
|
|
@@ -736,7 +768,12 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
736
768
|
// DT-1a: what a delegated task may NAME — the configured checks and the
|
|
737
769
|
// model profiles — handed to the (in-process) subagent extension through
|
|
738
770
|
// the environment. A model never supplies a command; it names a check.
|
|
739
|
-
|
|
771
|
+
// 0.40.0: and the folder this process keeps its sessions in — a child
|
|
772
|
+
// writes beside its parent, whatever directory it runs in. Through this
|
|
773
|
+
// channel and not an exported KISO_SESSIONS_DIR: a variable in
|
|
774
|
+
// process.env would reach every shell child, and a kiso started from a
|
|
775
|
+
// shell tool would then write into this project's folder.
|
|
776
|
+
process.env.KISO_DELEGATION_CONFIG_JSON = JSON.stringify({ checks: merged.checks ?? {}, profiles: Object.keys(merged.models ?? {}), sessionsDir: sessionsDir() });
|
|
740
777
|
setConfigModels(merged.models ?? {});
|
|
741
778
|
const resolved = resolveModel(modelFlag, merged);
|
|
742
779
|
// AFTER the model resolves: the window a PROFILE states is about that
|
|
@@ -749,10 +786,12 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
749
786
|
console.log("[faux mode — set ANTHROPIC_API_KEY or OPENAI_API_KEY, or configure models in ~/.kiso/config.json]\n");
|
|
750
787
|
setCurrentFaux(true);
|
|
751
788
|
setCurrentModelName("faux");
|
|
789
|
+
setCurrentProfileName(null);
|
|
752
790
|
}
|
|
753
791
|
else {
|
|
754
792
|
setCurrentFaux(false);
|
|
755
793
|
setCurrentModelName(resolved.name);
|
|
794
|
+
setCurrentProfileName(resolved.name);
|
|
756
795
|
}
|
|
757
796
|
setAgentModel(model, resolved?.profile.baseUrl); // v2b: the status bar shows it; OR-1: the endpoint rides along
|
|
758
797
|
// W21: the extensions array is built ONCE per agent and shared with
|
|
@@ -762,14 +801,45 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
762
801
|
// re-reads the config's extensions array per run).
|
|
763
802
|
// LT-2: the loop breaker at the chain HEAD — it speaks first when it speaks,
|
|
764
803
|
// so `decidedBy` names it; a deny there beats every tier, bypass included.
|
|
765
|
-
|
|
804
|
+
// 0.40.0: the read-only shell allow sits after the tiers — an allow from
|
|
805
|
+
// it outranks a tier's ask and names itself in decidedBy.
|
|
806
|
+
// 0.40.0: a saved allow never carries a write into .git/ or .kiso/, nor
|
|
807
|
+
// a destructive shell command.
|
|
808
|
+
const workspaceRoot = () => codingToolOptions().workspaceRoot;
|
|
809
|
+
const neverInherited = (call) => isProtectedWrite(call, workspaceRoot()) || isDestructiveCall(call);
|
|
810
|
+
setNeverInherited(neverInherited);
|
|
811
|
+
// 0.40.0: the catastrophe floor, at the chain's HEAD — a deny there
|
|
812
|
+
// names itself in decidedBy and outranks every tier, bypass included.
|
|
813
|
+
// Read per agent, so /reload picks up an edited user config.
|
|
814
|
+
const userConfig = loadUserConfig();
|
|
815
|
+
setFloorOn(userConfig?.floor !== "off");
|
|
816
|
+
// kiso never serves its own credential store to a model: the file tools
|
|
817
|
+
// refuse it through codingToolOptions, and this member denies a shell
|
|
818
|
+
// line naming it — at the HEAD with the floor, in every mode, and NOT
|
|
819
|
+
// switched off with it (`floor: "off"` lowers the catastrophe floor,
|
|
820
|
+
// never this).
|
|
821
|
+
setUserProtectedPaths(userConfig?.protectedPaths);
|
|
822
|
+
const extensions = [
|
|
823
|
+
protectedShellExtension({ files: protectedFiles, workspaceRoot, env: () => ({ home: homedir(), kisoHome: kisoHome() }) }),
|
|
824
|
+
floorExtension(() => floorOn, workspaceRoot),
|
|
825
|
+
breakerExtension(),
|
|
826
|
+
...modeExtensions(workspaceRoot),
|
|
827
|
+
readOnlyShellExtension(codingToolOptions),
|
|
828
|
+
...loadedExtensions.map((e) => guardSavedAllow(e, neverInherited)),
|
|
829
|
+
];
|
|
766
830
|
setCurrentAgentExtensions(extensions);
|
|
767
831
|
// E6: the run-start context policy (captured once — exactOptionalPropertyTypes).
|
|
768
832
|
const contextPolicy = contextPolicyFromEnv();
|
|
769
833
|
const idleFromEnv = streamIdleFromEnv(); // read once: a narrowed const, not a call per spread
|
|
834
|
+
const retriesFromEnv = maxRetriesFromEnv();
|
|
770
835
|
const definition = {
|
|
771
836
|
model,
|
|
772
837
|
store,
|
|
838
|
+
// 0.40.0: a NEW session records where it started; the picker scopes by
|
|
839
|
+
// it. The profile name is recorded only when the model came from a
|
|
840
|
+
// config profile — a direct provider/model or an env key names none.
|
|
841
|
+
workspace: workspaceRoot(),
|
|
842
|
+
...(resolved !== null && merged.models?.[resolved.name] === resolved.profile ? { profileName: resolved.name } : {}),
|
|
773
843
|
// Area 5: the coding tools are bound to the workspace — every path
|
|
774
844
|
// they touch is canonicalized inside cwd, escapes are refused.
|
|
775
845
|
tools: [...createCodingTools(codingToolOptions())], // DC-49 — the options live in state.ts, shared with the `!` command's runner
|
|
@@ -779,19 +849,19 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
779
849
|
// still counts loadedExtensions only — the modes are in-process,
|
|
780
850
|
// never a file extension.
|
|
781
851
|
systemPrompt: (() => {
|
|
782
|
-
const sp = composeSystemPrompt(process.cwd());
|
|
852
|
+
const sp = composeSystemPrompt(process.cwd(), protectedFiles());
|
|
783
853
|
const extra = modeSystemPrompt();
|
|
784
854
|
return extra === undefined ? sp : `${sp}\n\n${extra}`;
|
|
785
855
|
})(),
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
856
|
+
// ADR-0055 Amendment 1 (A1b): compaction is ON by default and runs
|
|
857
|
+
// INSIDE a run, by tiers drawn from the model's window (CTX-1: the
|
|
858
|
+
// binding step moves the window with /model and /resume). The
|
|
859
|
+
// standing microcompact at half the window is gone (A4). Phase rule
|
|
860
|
+
// 1 reads the user's configured checks first, then the runner table.
|
|
861
|
+
contextPolicy: {
|
|
862
|
+
...(contextPolicy ?? {}),
|
|
863
|
+
tiers: { windowTokens: contextWindowTokens(), isCheck: (command) => runsACheck(command, Object.values(merged.checks ?? {})) },
|
|
864
|
+
},
|
|
795
865
|
// R3e (owner ruling, 2026-08-28): NO turn limit on an interactive
|
|
796
866
|
// session. This was `maxTurns: 20`, hardcoded on 2026-08-03 with no
|
|
797
867
|
// stated reason and no way to change it — and it was the thing that
|
|
@@ -823,9 +893,19 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
823
893
|
// LT-1: KISO_STREAM_IDLE_MS (the test rigs' knob) beats the profile —
|
|
824
894
|
// the last spread wins, which is why it sits after the profile's.
|
|
825
895
|
...(idleFromEnv !== undefined ? { streamIdleMs: idleFromEnv } : {}),
|
|
896
|
+
...(retriesFromEnv !== undefined ? { maxRetries: retriesFromEnv } : {}),
|
|
897
|
+
// ADR-0005 Amendment 2: the kernel announces each retry before its
|
|
898
|
+
// wait; the running row shows it. Composed with every extension's
|
|
899
|
+
// hooks by the runtime — an extension observing retries too is heard.
|
|
900
|
+
hooks: {
|
|
901
|
+
onRetry: async (info) => {
|
|
902
|
+
setRetryShown({ attempt: info.attempt, maxRetries: info.maxRetries, code: info.code, until: Date.now() + info.delayMs });
|
|
903
|
+
},
|
|
904
|
+
},
|
|
826
905
|
};
|
|
827
906
|
return createAgent(definition);
|
|
828
907
|
}
|
|
908
|
+
export { workspaceRoot } from "./state.js";
|
|
829
909
|
/** TUI2-R2 ② — the picker's affordance row: the keys, said where the
|
|
830
910
|
* keys are useful. */
|
|
831
911
|
const PICKER_HINT = "↑↓ pick · ⏎ resumes · type filters · esc";
|
|
@@ -834,11 +914,139 @@ const PICKER_HINT = "↑↓ pick · ⏎ resumes · type filters · esc";
|
|
|
834
914
|
* runtime's own accessors (see session-cards.ts); this is only the
|
|
835
915
|
* plumbing that hands it the store's read side.
|
|
836
916
|
*/
|
|
837
|
-
async function sessionCards(
|
|
917
|
+
async function sessionCards(_agent, announce = (l) => bodyLog(l)) {
|
|
838
918
|
const store = sessionStoreRef;
|
|
839
919
|
if (store === null)
|
|
840
920
|
return []; // unreachable: makeAgent builds the store first
|
|
841
|
-
|
|
921
|
+
const folders = listingFolders();
|
|
922
|
+
// 0.40.0 dogfood (item 2, lead's ruling A): the ONE-TIME migration — the
|
|
923
|
+
// first list after the upgrade reads each legacy log exactly once and
|
|
924
|
+
// writes its summary into the sidecar; announced with its count. Per
|
|
925
|
+
// folder: each carries its own marker.
|
|
926
|
+
const pending = folders.filter((f) => summaryMigrationPending(f.dir));
|
|
927
|
+
const count = pending.reduce((n, f) => n + listSessionSidecars(f.dir).filter((l) => l.summary === null).length, 0);
|
|
928
|
+
if (count > 0)
|
|
929
|
+
announce(`recording summaries for ${count} older session${count === 1 ? "" : "s"} — once`);
|
|
930
|
+
for (const f of pending) {
|
|
931
|
+
const reader = f.dir === activeStoreDir ? store : new SessionStore(f.dir);
|
|
932
|
+
migrateSummaries(f.dir, (id) => reader.load(id));
|
|
933
|
+
}
|
|
934
|
+
// …and from then on, the SIDECARS only: no log is opened to draw a row.
|
|
935
|
+
// A project folder's sessions are that project's; one without a recorded
|
|
936
|
+
// workspace was placed by inference and says so.
|
|
937
|
+
return cardsFromListings(folders.flatMap((f) => listSessionSidecars(f.dir).map((l) => f.kind === "project" ? { ...l, workspace: f.workspace ?? l.workspace, inferred: l.workspace === null } : f.kind === "unknown" ? { ...l, workspace: null } : l)));
|
|
938
|
+
}
|
|
939
|
+
/**
|
|
940
|
+
* 0.40.0 — the folders a listing reads: every project's under the
|
|
941
|
+
* per-project layout (the picker opens on this one, `tab` shows all), and
|
|
942
|
+
* the one folder otherwise. This process's folder is always first, even
|
|
943
|
+
* before its first session exists.
|
|
944
|
+
*/
|
|
945
|
+
function listingFolders() {
|
|
946
|
+
const here = ownSessionsDir();
|
|
947
|
+
const home = kisoHome();
|
|
948
|
+
if (!projectLayoutActive(home))
|
|
949
|
+
return [{ dir: here, workspace: null, kind: "legacy" }];
|
|
950
|
+
const all = sessionFolders(home);
|
|
951
|
+
const mine = all.find((f) => f.dir === here) ?? { dir: here, workspace: projectRoot(), kind: "project" };
|
|
952
|
+
return [mine, ...all.filter((f) => f.dir !== here)];
|
|
953
|
+
}
|
|
954
|
+
/**
|
|
955
|
+
* 0.40.0 — before any session opens: the one-time move of the legacy
|
|
956
|
+
* folder into project folders (announced once, with the undo), then this
|
|
957
|
+
* project's folder claimed. Nothing happens under a pinned folder or after
|
|
958
|
+
* a reversed migration.
|
|
959
|
+
*/
|
|
960
|
+
function prepareSessionFolders() {
|
|
961
|
+
const home = kisoHome();
|
|
962
|
+
if (!projectLayoutActive(home))
|
|
963
|
+
return;
|
|
964
|
+
if (pendingLegacyIds(home).length > 0) {
|
|
965
|
+
const result = runMigration(home, planMigration(home));
|
|
966
|
+
if (result !== null && result.moved > 0) {
|
|
967
|
+
const line = migrationNotice(result);
|
|
968
|
+
if (process.stdout.isTTY)
|
|
969
|
+
bodyLog(line);
|
|
970
|
+
else
|
|
971
|
+
process.stderr.write(`${line}\n`);
|
|
972
|
+
}
|
|
973
|
+
}
|
|
974
|
+
claimProjectDir(ownSessionsDir(), projectRoot());
|
|
975
|
+
}
|
|
976
|
+
/**
|
|
977
|
+
* 0.40.0 (the owner's dogfood: a uooki session resumed from flowpix2) —
|
|
978
|
+
* whether a session id may open in this project. Its own folder: yes. A
|
|
979
|
+
* session another project RECORDED: refused, with where to go. One whose
|
|
980
|
+
* project is unknown, or was only inferred, is never refused and never
|
|
981
|
+
* moved (the lead's ruling: a resume changes no placement) — it opens where
|
|
982
|
+
* it is, and the line says the tools work here. A pure read. `null` = not
|
|
983
|
+
* found anywhere (the caller's own rule applies: a new session, or "no
|
|
984
|
+
* such session").
|
|
985
|
+
*/
|
|
986
|
+
export function routeSession(id) {
|
|
987
|
+
const own = ownSessionsDir();
|
|
988
|
+
if (hasSession(own, id))
|
|
989
|
+
return { kind: "here" };
|
|
990
|
+
const home = kisoHome();
|
|
991
|
+
if (!projectLayoutActive(home))
|
|
992
|
+
return null;
|
|
993
|
+
const found = locateSession(home, id, own);
|
|
994
|
+
if (found === null)
|
|
995
|
+
return null;
|
|
996
|
+
const profile = readProfile(found.dir, id);
|
|
997
|
+
const recorded = profile.kind === "ok" && profile.profile.workspace !== null ? canonicalPath(profile.profile.workspace) : null;
|
|
998
|
+
const cwd = projectRoot();
|
|
999
|
+
const owner = found.kind === "project" ? (found.workspace ?? recorded) : recorded;
|
|
1000
|
+
if (recorded !== null && owner !== null && owner !== cwd) {
|
|
1001
|
+
return { kind: "refused", line: `this session belongs to ${tildeOf(owner)} — cd there to resume it` };
|
|
1002
|
+
}
|
|
1003
|
+
// recorded here, and still in the legacy folder (open elsewhere when the
|
|
1004
|
+
// move ran): nothing to warn about
|
|
1005
|
+
if (recorded !== null)
|
|
1006
|
+
return { kind: "elsewhere", dir: found.dir, line: null };
|
|
1007
|
+
const inferred = found.kind === "project" && owner !== null ? `workspace inferred as ${tildeOf(owner)}, never recorded` : "workspace unknown";
|
|
1008
|
+
return { kind: "elsewhere", dir: found.dir, line: `${inferred} — tools work in ${tildeOf(workspaceRoot())}` };
|
|
1009
|
+
}
|
|
1010
|
+
/** The folder a session opens in: the one the store is on when it holds
|
|
1011
|
+
* it, else this project's, else where the route found it. A new id opens
|
|
1012
|
+
* in this project's folder. */
|
|
1013
|
+
function sessionFolderOf(id) {
|
|
1014
|
+
if (hasSession(activeStoreDir, id))
|
|
1015
|
+
return activeStoreDir;
|
|
1016
|
+
const route = routeSession(id);
|
|
1017
|
+
return route?.kind === "elsewhere" ? route.dir : ownSessionsDir();
|
|
1018
|
+
}
|
|
1019
|
+
/** The folder makeAgent built the store on. */
|
|
1020
|
+
let activeStoreDir = "";
|
|
1021
|
+
/** The workspace a listing is scoped to: a project folder's identity under
|
|
1022
|
+
* the per-project layout, the recorded realpath in one pinned folder. */
|
|
1023
|
+
function scopeRoot() {
|
|
1024
|
+
return projectLayoutActive(kisoHome()) ? projectRoot() : workspaceRoot();
|
|
1025
|
+
}
|
|
1026
|
+
function tildeOf(path) {
|
|
1027
|
+
const home = homedir();
|
|
1028
|
+
return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
|
|
1029
|
+
}
|
|
1030
|
+
/** The explicit-id doors (`chat <id>`, `resume <id>`, `-p … <id>`), BEFORE
|
|
1031
|
+
* makeAgent builds the store: a refusal is the entry's error (the line on
|
|
1032
|
+
* stderr, exit 2); a session in another folder opens there. Returns the
|
|
1033
|
+
* line to say once the session is up. */
|
|
1034
|
+
function enterRouted(id) {
|
|
1035
|
+
const route = routeSession(id);
|
|
1036
|
+
if (route === null || route.kind === "here")
|
|
1037
|
+
return null;
|
|
1038
|
+
if (route.kind === "refused")
|
|
1039
|
+
throw new CliUsageError(route.line);
|
|
1040
|
+
setOpenSessionFolder(route.dir);
|
|
1041
|
+
return route.line;
|
|
1042
|
+
}
|
|
1043
|
+
function sayRouteLine(line) {
|
|
1044
|
+
if (line === null)
|
|
1045
|
+
return;
|
|
1046
|
+
if (process.stdout.isTTY)
|
|
1047
|
+
bodyLog(line);
|
|
1048
|
+
else
|
|
1049
|
+
process.stderr.write(`${line}\n`);
|
|
842
1050
|
}
|
|
843
1051
|
/**
|
|
844
1052
|
* TUI2-R2 ② — the resume picker: the band, the keys, the id.
|
|
@@ -866,7 +1074,8 @@ async function pickSession(agent, input) {
|
|
|
866
1074
|
}
|
|
867
1075
|
dock.setStatus("", PICKER_HINT);
|
|
868
1076
|
const picked = await new Promise((resolve) => {
|
|
869
|
-
|
|
1077
|
+
// 0.40.0: scoped to the running workspace; tab flips to all
|
|
1078
|
+
input.pick(() => cards, resolve, scopeRoot());
|
|
870
1079
|
});
|
|
871
1080
|
dock.setStatus("", null);
|
|
872
1081
|
return picked;
|
|
@@ -892,6 +1101,21 @@ async function pickSession(agent, input) {
|
|
|
892
1101
|
* formatter — never a boot-time copy, which would drift from the real
|
|
893
1102
|
* row the moment either changed.
|
|
894
1103
|
*/
|
|
1104
|
+
/**
|
|
1105
|
+
* ADR-0055 Amendment 1 (the owner, 2026-09-18): `autoCompact` — the
|
|
1106
|
+
* between-turn ratio, from config or KISO_AUTO_COMPACT — is RETIRED. The
|
|
1107
|
+
* in-run tiers compact inside a run and before its first request, which is
|
|
1108
|
+
* everything it did and more. 0.40.0 accepts it, ignores it and says so
|
|
1109
|
+
* once; 0.41.0 removes it.
|
|
1110
|
+
*/
|
|
1111
|
+
let autoCompactNoticed = false;
|
|
1112
|
+
function retiredAutoCompact(merged) {
|
|
1113
|
+
if (!autoCompactNoticed && (process.env.KISO_AUTO_COMPACT !== undefined || resolveAutoCompact(merged) !== undefined)) {
|
|
1114
|
+
autoCompactNoticed = true;
|
|
1115
|
+
console.error("[autoCompact] retired in 0.40.0 — compaction now runs inside a run, by tiers; this setting is ignored and goes in 0.41.0");
|
|
1116
|
+
}
|
|
1117
|
+
return undefined;
|
|
1118
|
+
}
|
|
895
1119
|
/**
|
|
896
1120
|
* CTX-1 (Astra F34-1): EVERY entry point that opens a session must bind the
|
|
897
1121
|
* policy that follows the session's OWN model.
|
|
@@ -913,11 +1137,22 @@ async function pickSession(agent, input) {
|
|
|
913
1137
|
* threshold got stuck in the first place.
|
|
914
1138
|
*/
|
|
915
1139
|
function bindRestoredSession(session) {
|
|
1140
|
+
// 0.40.1 (the owner's ruling of 2026-09-21): a changed binding is neither
|
|
1141
|
+
// blocked nor "acknowledged" — it is recorded as the next revision under
|
|
1142
|
+
// the CURRENT configuration, and this ONE line says which binding and why
|
|
1143
|
+
// (the owner-ruled reasoning reset rides with it, silently until 0.40.0).
|
|
1144
|
+
const ack = session.driftAcknowledgement ?? null;
|
|
1145
|
+
if (ack !== null) {
|
|
1146
|
+
const was = ack.reasoningReset;
|
|
1147
|
+
const prior = [was.effort !== "default" ? `effort ${was.effort}` : "", was.thinking !== "default" ? `thinking ${was.thinking}` : ""].filter((s) => s !== "").join(", ");
|
|
1148
|
+
bodyLog(`binding changed — now on ${session.model}: ${ack.reasons.join("; ")}; reasoning reset to defaults${prior === "" ? "" : ` (was ${prior})`}`);
|
|
1149
|
+
}
|
|
916
1150
|
setAgentModel(session.model, session.baseUrl);
|
|
917
1151
|
session.setMicrocompactThreshold(microcompactThresholdFor({
|
|
918
1152
|
model: session.model,
|
|
919
1153
|
...(session.baseUrl !== undefined ? { baseUrl: session.baseUrl } : {}),
|
|
920
1154
|
}));
|
|
1155
|
+
session.setContextWindow(contextWindowTokens({ model: session.model, ...(session.baseUrl !== undefined ? { baseUrl: session.baseUrl } : {}) }));
|
|
921
1156
|
}
|
|
922
1157
|
function paintBootStatus(session) {
|
|
923
1158
|
if (!dock.active)
|
|
@@ -971,7 +1206,7 @@ function skillCount() {
|
|
|
971
1206
|
* is the durable log on disk. The record never moves: a reload is
|
|
972
1207
|
* invisible in it, because nothing the model said or did changes.
|
|
973
1208
|
*/
|
|
974
|
-
async function reloadAgent(old, id, input) {
|
|
1209
|
+
async function reloadAgent(old, id, input, announce = true) {
|
|
975
1210
|
// Snapshot BEFORE makeAgent: it overwrites every one of these, so the
|
|
976
1211
|
// old set would be unreachable by the time we needed to dispose it.
|
|
977
1212
|
const oldLoaded = loadedExtensions;
|
|
@@ -1031,6 +1266,7 @@ async function reloadAgent(old, id, input) {
|
|
|
1031
1266
|
setConfiguredWindow(oldCfg.window);
|
|
1032
1267
|
setCurrentFaux(oldCfg.faux);
|
|
1033
1268
|
setCurrentModelName(oldCfg.modelName);
|
|
1269
|
+
setCurrentProfileName(null); // the reload snapshot carries no profile name: no mark beats a guessed one
|
|
1034
1270
|
setModelChoice(oldCfg.choice);
|
|
1035
1271
|
setAgentModel(oldCfg.agent, oldCfg.endpoint);
|
|
1036
1272
|
if (oldCfg.delegation === undefined)
|
|
@@ -1051,7 +1287,8 @@ async function reloadAgent(old, id, input) {
|
|
|
1051
1287
|
// best-effort — the temp dir would be reaped by the OS
|
|
1052
1288
|
}
|
|
1053
1289
|
}
|
|
1054
|
-
|
|
1290
|
+
if (announce)
|
|
1291
|
+
bodyLog(`[reload] ${loadedExtensions.length} extensions, ${skillCount()} skills — the conversation is unchanged`);
|
|
1055
1292
|
return next;
|
|
1056
1293
|
}
|
|
1057
1294
|
async function chatLoop(first, firstId, input, autoCompact) {
|
|
@@ -1063,11 +1300,83 @@ async function chatLoop(first, firstId, input, autoCompact) {
|
|
|
1063
1300
|
// reload said what it did; repeating the session's opening would read
|
|
1064
1301
|
// as a new session, which is the one thing it is not.
|
|
1065
1302
|
let rebuilt = false;
|
|
1303
|
+
// XP-1: the id of the session that IS open — null until the first open
|
|
1304
|
+
// succeeds, and cleared by a refused switch, so one refusal can never
|
|
1305
|
+
// recurse into another.
|
|
1306
|
+
let opened = null;
|
|
1307
|
+
// XP-1: the re-open that follows a refused switch. The session is the one
|
|
1308
|
+
// already on screen, so this step prints neither banner nor tail.
|
|
1309
|
+
let refused = false;
|
|
1310
|
+
/** DC-57: lines that arrived with a switch command belong to the session being asked for. */
|
|
1311
|
+
let seed = [];
|
|
1066
1312
|
for (;;) {
|
|
1067
|
-
|
|
1313
|
+
// 0.40.0 (the lead's ruling): a session opens in the folder that holds
|
|
1314
|
+
// it, never moved — a switch that crosses folders rebuilds the agent
|
|
1315
|
+
// on the other folder's store, and one back rebuilds it on this
|
|
1316
|
+
// project's. A failed rebuild switches nothing.
|
|
1317
|
+
const folder = sessionFolderOf(id);
|
|
1318
|
+
if (folder !== activeStoreDir) {
|
|
1319
|
+
const was = { dir: activeStoreDir, store: sessionStoreRef };
|
|
1320
|
+
setOpenSessionFolder(folder === ownSessionsDir() ? null : folder);
|
|
1321
|
+
const next = await reloadAgent(agent, id, input, false);
|
|
1322
|
+
if (next === agent) {
|
|
1323
|
+
// makeAgent may have built the new store before it failed
|
|
1324
|
+
activeStoreDir = was.dir;
|
|
1325
|
+
if (was.store !== null)
|
|
1326
|
+
setSessionStore(was.store);
|
|
1327
|
+
setOpenSessionFolder(was.dir === ownSessionsDir() ? null : was.dir);
|
|
1328
|
+
if (prev === null)
|
|
1329
|
+
return;
|
|
1330
|
+
id = prev;
|
|
1331
|
+
rebuilt = true;
|
|
1332
|
+
continue;
|
|
1333
|
+
}
|
|
1334
|
+
agent = next;
|
|
1335
|
+
}
|
|
1336
|
+
let session;
|
|
1337
|
+
try {
|
|
1338
|
+
session = await agent.session({ id, ...(acceptDrift() ? { acceptDrift: true } : {}) });
|
|
1339
|
+
}
|
|
1340
|
+
catch (err) {
|
|
1341
|
+
// The profile contract's BLOCKED cases — a sidecar that cannot be
|
|
1342
|
+
// read, an XP-era log without one — belong to the ENTRY: nothing is
|
|
1343
|
+
// on screen yet, and the message says what to do. An in-session
|
|
1344
|
+
// SWITCH is a LINE: it switches NOTHING and the session you were in
|
|
1345
|
+
// stays open. The throw used to escape chatLoop, which killed the
|
|
1346
|
+
// REPL and left the session that was open behind with no log at all
|
|
1347
|
+
// (announced once, then empty and unopenable). Every refusal this
|
|
1348
|
+
// open can throw is treated the same way — the message itself is
|
|
1349
|
+
// what says what to do about it.
|
|
1350
|
+
if (opened === null)
|
|
1351
|
+
throw err;
|
|
1352
|
+
bodyLog(escapeTerminal(err.message));
|
|
1353
|
+
// DC-57's other half + DC-55 (the owner's ruling of 2026-09-21,
|
|
1354
|
+
// option ①): the lines that arrived WITH the refused switch were aimed
|
|
1355
|
+
// at a session that EXISTS and could not be opened. They are NOT run
|
|
1356
|
+
// here — a departing session never answers lines meant for the target —
|
|
1357
|
+
// and not dropped in silence either: what was held is printed, so
|
|
1358
|
+
// nothing vanishes without the person seeing it. (Leaving them in the
|
|
1359
|
+
// queue would hand them to the old session on its next entry, which is
|
|
1360
|
+
// the thing this ruling forbids.)
|
|
1361
|
+
seed = [];
|
|
1362
|
+
const held = queuedSwitchLines.splice(0);
|
|
1363
|
+
if (held.length > 0) {
|
|
1364
|
+
bodyLog(`[${held.length} line${held.length === 1 ? "" : "s"} held back — the switch was refused, so nothing was run]`);
|
|
1365
|
+
for (const line of held)
|
|
1366
|
+
bodyLog(` ${escapeTerminal(line)}`);
|
|
1367
|
+
}
|
|
1368
|
+
id = opened; // the switch is undone
|
|
1369
|
+
opened = null;
|
|
1370
|
+
refused = true;
|
|
1371
|
+
continue;
|
|
1372
|
+
}
|
|
1373
|
+
opened = id;
|
|
1068
1374
|
if (rebuilt) {
|
|
1069
1375
|
rebuilt = false;
|
|
1070
1376
|
}
|
|
1377
|
+
else if (refused) {
|
|
1378
|
+
refused = false; // back in the session that was already on screen
|
|
1379
|
+
}
|
|
1071
1380
|
else if (prev === null) {
|
|
1072
1381
|
bodyLog(`session ${id}\n`);
|
|
1073
1382
|
// REL-0152-D5: a session with history says what that history WAS.
|
|
@@ -1075,7 +1384,7 @@ async function chatLoop(first, firstId, input, autoCompact) {
|
|
|
1075
1384
|
// prompt inside a conversation with thousands of events — the
|
|
1076
1385
|
// durable log was right there and none of it was shown. Empty for
|
|
1077
1386
|
// a fresh session, so `kiso chat` is byte-identical.
|
|
1078
|
-
|
|
1387
|
+
showResumeTail(session.log.all);
|
|
1079
1388
|
// R2 (owner, 2026-08-27): the resume list is NOT on the opening
|
|
1080
1389
|
// screen. `/resume` is where you go looking for a session; the
|
|
1081
1390
|
// opening's job is to say what THIS one is.
|
|
@@ -1107,7 +1416,7 @@ async function chatLoop(first, firstId, input, autoCompact) {
|
|
|
1107
1416
|
}
|
|
1108
1417
|
else {
|
|
1109
1418
|
bodyLog(`session ${id} (switched — previous: ${prev}, /resume ${prev} returns)\n`);
|
|
1110
|
-
|
|
1419
|
+
showResumeTail(session.log.all);
|
|
1111
1420
|
if (currentFaux)
|
|
1112
1421
|
session.setAdapter(createFauxProvider(readFauxScript().slice(fauxSkip(id))));
|
|
1113
1422
|
}
|
|
@@ -1125,6 +1434,12 @@ async function chatLoop(first, firstId, input, autoCompact) {
|
|
|
1125
1434
|
// the threshold. One step, shared by all three entry points.
|
|
1126
1435
|
bindRestoredSession(session);
|
|
1127
1436
|
setCurrentModelName(session.model);
|
|
1437
|
+
// A SWITCH knows only a model id here — nothing that names the profile —
|
|
1438
|
+
// so the mark is cleared rather than guessed. A FRESH start keeps what
|
|
1439
|
+
// makeAgent resolved from `--model <profile>`, which is the one place
|
|
1440
|
+
// the name is known (prev is null only on the first entry).
|
|
1441
|
+
if (prev !== null)
|
|
1442
|
+
setCurrentProfileName(null);
|
|
1128
1443
|
paintBootStatus(session);
|
|
1129
1444
|
// The terminal's window title, HERE for the same reason the three
|
|
1130
1445
|
// lines above are here: this is the one step all three entry points
|
|
@@ -1132,12 +1447,18 @@ async function chatLoop(first, firstId, input, autoCompact) {
|
|
|
1132
1447
|
// be left naming the session the user just left.
|
|
1133
1448
|
paintWindowTitle(session.log.all);
|
|
1134
1449
|
const nav = {
|
|
1135
|
-
|
|
1450
|
+
// 0.40.0 dogfood: the ids only — agent.sessions() read every log whole
|
|
1451
|
+
// (seconds on the owner's 118 sessions) before /resume could open
|
|
1452
|
+
sessions: () => agent.sessionIds(),
|
|
1453
|
+
route: (sessionId) => routeSession(sessionId),
|
|
1136
1454
|
...(process.stdin.isTTY ? { pick: () => pickSession(agent, input) } : {}),
|
|
1137
1455
|
};
|
|
1138
|
-
const end = await chat(session, currentFaux, input, autoCompact, nav);
|
|
1456
|
+
const end = await chat(session, currentFaux, input, autoCompact, nav, seed);
|
|
1139
1457
|
if (end.next === "exit")
|
|
1140
1458
|
return;
|
|
1459
|
+
// DC-57: the lines that arrived with the switch command ride INTO the
|
|
1460
|
+
// next entry — they were aimed at the session being asked for.
|
|
1461
|
+
seed = end.lines ?? [];
|
|
1141
1462
|
if (end.next === "reload") {
|
|
1142
1463
|
agent = await reloadAgent(agent, id, input);
|
|
1143
1464
|
rebuilt = true;
|
|
@@ -1197,6 +1518,20 @@ async function readSecret(prompt) {
|
|
|
1197
1518
|
process.stdin.on("data", onData);
|
|
1198
1519
|
});
|
|
1199
1520
|
}
|
|
1521
|
+
/**
|
|
1522
|
+
* What a resumed session shows (REL-0152-D5, 4c). On the compositor: the
|
|
1523
|
+
* durable events replayed into cells — the last two turns in full, the
|
|
1524
|
+
* earlier ones in one fold row the ctrl+r viewer reads. Through a pipe
|
|
1525
|
+
* there is no viewer and no cell renderer, so the plain text tail stays
|
|
1526
|
+
* (DC-51: one call, one cell).
|
|
1527
|
+
*/
|
|
1528
|
+
function showResumeTail(events) {
|
|
1529
|
+
const W = process.stdout.columns ?? 80;
|
|
1530
|
+
if (dock.active)
|
|
1531
|
+
replayInto(body, events, W);
|
|
1532
|
+
else
|
|
1533
|
+
bodyLog(resumeTail(events, W).join("\n"));
|
|
1534
|
+
}
|
|
1200
1535
|
async function main() {
|
|
1201
1536
|
// E group (the graceful-exit gate ③, R-G 0.1.48): a terminal closing
|
|
1202
1537
|
// turns the in-flight stdout/stderr writes into EIO, and node's
|
|
@@ -1273,7 +1608,7 @@ async function main() {
|
|
|
1273
1608
|
if (modeFlag !== -1) {
|
|
1274
1609
|
const m = MODES.find((x) => x === args[modeFlag + 1]);
|
|
1275
1610
|
if (m === undefined) {
|
|
1276
|
-
console.error(`unknown mode: ${args[modeFlag + 1]} (tiers: ${
|
|
1611
|
+
console.error(`unknown mode: ${args[modeFlag + 1]} (tiers: ${OFFERED_MODES.join(", ")})`);
|
|
1277
1612
|
process.exit(2);
|
|
1278
1613
|
}
|
|
1279
1614
|
setMode(m);
|
|
@@ -1292,6 +1627,11 @@ async function main() {
|
|
|
1292
1627
|
const path = args[i + 1];
|
|
1293
1628
|
if (path === undefined)
|
|
1294
1629
|
throw new CliUsageError("--task-file needs a path");
|
|
1630
|
+
// the file becomes the turn a model reads — never the credential
|
|
1631
|
+
// store. The list here is the store's own path: the user config's
|
|
1632
|
+
// protectedPaths is read later, with the agent
|
|
1633
|
+
if (isProtectedPath(path, protectedIdentity(protectedFiles())))
|
|
1634
|
+
throw new CliUsageError(`--task-file: ${path} — ${PROTECTED_REFUSAL}`);
|
|
1295
1635
|
try {
|
|
1296
1636
|
taskFile = readFileSync(path, "utf8");
|
|
1297
1637
|
}
|
|
@@ -1301,6 +1641,30 @@ async function main() {
|
|
|
1301
1641
|
args.splice(i, 2);
|
|
1302
1642
|
}
|
|
1303
1643
|
}
|
|
1644
|
+
// 0.40.0: `kiso sessions --all | --current` — the one command that takes
|
|
1645
|
+
// them. Parsed only there, so neither ever becomes a session id. Absent
|
|
1646
|
+
// = each form's default (TTY: current; pipe: all, today's bytes).
|
|
1647
|
+
let listScope;
|
|
1648
|
+
let reverseManifest;
|
|
1649
|
+
if (args[0] === "sessions") {
|
|
1650
|
+
// 0.40.0: the undo of the per-project move, named by its manifest
|
|
1651
|
+
const r = args.indexOf("--reverse-migration");
|
|
1652
|
+
if (r !== -1) {
|
|
1653
|
+
reverseManifest = args[r + 1];
|
|
1654
|
+
if (reverseManifest === undefined || reverseManifest.startsWith("--"))
|
|
1655
|
+
throw new CliUsageError("kiso sessions --reverse-migration <manifest> — the path the move announced");
|
|
1656
|
+
args.splice(r, 2);
|
|
1657
|
+
}
|
|
1658
|
+
for (const flag of ["--all", "--current"]) {
|
|
1659
|
+
const i = args.indexOf(flag);
|
|
1660
|
+
if (i === -1)
|
|
1661
|
+
continue;
|
|
1662
|
+
args.splice(i, 1);
|
|
1663
|
+
if (listScope !== undefined && listScope !== flag.slice(2))
|
|
1664
|
+
throw new CliUsageError("kiso sessions: --all and --current contradict each other");
|
|
1665
|
+
listScope = flag.slice(2);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1304
1668
|
const [command, arg] = args;
|
|
1305
1669
|
// round 8: faux mode is the keyless demo script — an exhausted script must
|
|
1306
1670
|
// exit non-zero, never masquerade as a successful provider run. The
|
|
@@ -1413,8 +1777,10 @@ async function main() {
|
|
|
1413
1777
|
// the -p flow: recovery-first one-shot, the resume() machinery
|
|
1414
1778
|
// verbatim (a fresh id makes the recovery a no-op)
|
|
1415
1779
|
const id = command ?? newSessionId(sessionsDir());
|
|
1780
|
+
const routeLine = command !== undefined ? enterRouted(id) : null;
|
|
1416
1781
|
agent = await makeAgent(id, input, modelFlag);
|
|
1417
1782
|
applyConfigMode();
|
|
1783
|
+
sayRouteLine(routeLine);
|
|
1418
1784
|
const session = await agent.session({ id, ...(acceptDrift() ? { acceptDrift: true } : {}) });
|
|
1419
1785
|
bindRestoredSession(session); // CTX-1 (F34-1): the -p door, too
|
|
1420
1786
|
faux = currentFaux;
|
|
@@ -1431,10 +1797,12 @@ async function main() {
|
|
|
1431
1797
|
dock.enter();
|
|
1432
1798
|
// E area: a resumed session continues the script at its durable
|
|
1433
1799
|
// position — never restarts it (fauxSkip).
|
|
1800
|
+
const routeLine = arg !== undefined ? enterRouted(id) : null;
|
|
1434
1801
|
agent = await makeAgent(id, input, modelFlag);
|
|
1435
1802
|
applyConfigMode();
|
|
1803
|
+
sayRouteLine(routeLine);
|
|
1436
1804
|
faux = currentFaux;
|
|
1437
|
-
await chatLoop(agent, id, input,
|
|
1805
|
+
await chatLoop(agent, id, input, retiredAutoCompact(mergedConfig));
|
|
1438
1806
|
break;
|
|
1439
1807
|
}
|
|
1440
1808
|
case "resume": {
|
|
@@ -1451,6 +1819,7 @@ async function main() {
|
|
|
1451
1819
|
// (argv = [node, script, resume, id, prompt?]).
|
|
1452
1820
|
const prompt = process.argv[4];
|
|
1453
1821
|
dock.enter();
|
|
1822
|
+
const routeLine = arg !== undefined ? enterRouted(arg) : null;
|
|
1454
1823
|
agent = await makeAgent(arg, input, modelFlag);
|
|
1455
1824
|
applyConfigMode();
|
|
1456
1825
|
let id = arg;
|
|
@@ -1461,15 +1830,25 @@ async function main() {
|
|
|
1461
1830
|
// an error, never a session started behind their back.
|
|
1462
1831
|
if (picked === null)
|
|
1463
1832
|
break;
|
|
1833
|
+
// 0.40.0: `tab` lists every project, and another project's
|
|
1834
|
+
// recorded session is refused here, with where to go
|
|
1835
|
+
const route = routeSession(picked);
|
|
1836
|
+
if (route?.kind === "refused") {
|
|
1837
|
+
bodyLog(route.line);
|
|
1838
|
+
break;
|
|
1839
|
+
}
|
|
1840
|
+
if (route?.kind === "elsewhere" && route.line !== null)
|
|
1841
|
+
bodyLog(route.line);
|
|
1464
1842
|
// the mini-spec (a DECLARED SUPERSESSION of the one-shot
|
|
1465
1843
|
// picker flow): a PICKED session enters the full REPL —
|
|
1466
1844
|
// "resume and keep working" no longer requires knowing to
|
|
1467
1845
|
// type `kiso chat <id>`. The explicit-id one-shot form
|
|
1468
1846
|
// (`kiso resume <id> ["prompt"]`) keeps its exact bytes.
|
|
1469
1847
|
faux = currentFaux;
|
|
1470
|
-
await chatLoop(agent, picked, input,
|
|
1848
|
+
await chatLoop(agent, picked, input, retiredAutoCompact(mergedConfig));
|
|
1471
1849
|
break;
|
|
1472
1850
|
}
|
|
1851
|
+
sayRouteLine(routeLine);
|
|
1473
1852
|
const session = await agent.session({ id, ...(acceptDrift() ? { acceptDrift: true } : {}) });
|
|
1474
1853
|
bindRestoredSession(session); // CTX-1 (F34-1): the explicit-id door
|
|
1475
1854
|
faux = currentFaux;
|
|
@@ -1482,7 +1861,7 @@ async function main() {
|
|
|
1482
1861
|
session.setAdapter(createFauxProvider(readFauxScript().slice(fauxSkip(id))));
|
|
1483
1862
|
// REL-0152-D5 — the same tail on the explicit-id form. NOT on
|
|
1484
1863
|
// the -p path above: that one's stdout is a machine's input.
|
|
1485
|
-
|
|
1864
|
+
showResumeTail(session.log.all);
|
|
1486
1865
|
await resume(session, prompt, faux, input);
|
|
1487
1866
|
break;
|
|
1488
1867
|
}
|
|
@@ -1495,22 +1874,60 @@ async function main() {
|
|
|
1495
1874
|
// "reading 'question'" on the dock-less branch). The input
|
|
1496
1875
|
// exists so the gate's ask can be answered; the listing
|
|
1497
1876
|
// itself never touches it.
|
|
1877
|
+
if (reverseManifest !== undefined) {
|
|
1878
|
+
// before makeAgent: its folder preparation would otherwise run
|
|
1879
|
+
// the very move this undoes
|
|
1880
|
+
const { restored, left } = reverseMigration(kisoHome(), reverseManifest);
|
|
1881
|
+
console.log(`moved ${restored} session${restored === 1 ? "" : "s"} back to ${join(kisoHome(), "sessions")}; one folder per project is off until ${join(kisoHome(), "projects", ".migration-reversed")} is removed${left > 0 ? ` — ${left} created since the move stay in their project folders` : ""}`);
|
|
1882
|
+
break;
|
|
1883
|
+
}
|
|
1498
1884
|
agent = await makeAgent(undefined, input, modelFlag);
|
|
1499
1885
|
// TUI2-R2 ③ — the same projection the picker renders, printed.
|
|
1500
1886
|
// The PIPE keeps today's bytes exactly: `kiso sessions` is
|
|
1501
1887
|
// something scripts read, and a badge column is a TTY-render
|
|
1502
1888
|
// concern, not a change to a machine interface.
|
|
1889
|
+
// 0.40.0 (lead's ruling): the TTY defaults to THIS workspace and
|
|
1890
|
+
// says so in a header line with both counts; the PIPE defaults to
|
|
1891
|
+
// every session with today's bytes — scripts must not change
|
|
1892
|
+
// under them. An explicit --all or --current applies to both.
|
|
1893
|
+
const here = scopeRoot();
|
|
1503
1894
|
if (process.stdout.isTTY) {
|
|
1504
|
-
const
|
|
1895
|
+
const every = await sessionCards(agent, (l) => console.log(l));
|
|
1896
|
+
const all = (listScope ?? "current") === "all";
|
|
1897
|
+
const inHere = every.filter((c) => c.workspace === here);
|
|
1898
|
+
const cards = all ? every : inHere;
|
|
1505
1899
|
const W = process.stdout.columns ?? 80;
|
|
1506
1900
|
const col = idColumn(cards);
|
|
1507
1901
|
const now = Date.now();
|
|
1902
|
+
console.log(sessionListHeader(inHere.length, every.length, all, W));
|
|
1903
|
+
// 0.40.1: the sessions with no workspace — one counted line, never
|
|
1904
|
+
// listed, unless --all
|
|
1905
|
+
if (!all) {
|
|
1906
|
+
const unknownLine = sessionListUnknownLine(every.filter((c) => c.workspace === null).length, W);
|
|
1907
|
+
if (unknownLine !== "")
|
|
1908
|
+
console.log(unknownLine);
|
|
1909
|
+
}
|
|
1508
1910
|
for (const card of cards)
|
|
1509
|
-
console.log(sessionListRow(card, W, now, col));
|
|
1911
|
+
console.log(sessionListRow(card, W, now, col, all ? here : null));
|
|
1510
1912
|
console.log(sessionListFooter(cards.length, W));
|
|
1511
1913
|
}
|
|
1512
1914
|
else {
|
|
1513
|
-
|
|
1915
|
+
// 0.40.0: every folder's sessions, merged in id order — today's
|
|
1916
|
+
// bytes; --current is this project's folder (in a single pinned
|
|
1917
|
+
// folder, the sessions that recorded this workspace)
|
|
1918
|
+
const folders = listingFolders();
|
|
1919
|
+
const single = folders.length === 1 && folders[0].kind === "legacy";
|
|
1920
|
+
const workspaceOf = (id) => {
|
|
1921
|
+
const r = readProfile(sessionsDir(), id);
|
|
1922
|
+
return r.kind === "ok" ? r.profile.workspace : null;
|
|
1923
|
+
};
|
|
1924
|
+
const metas = folders
|
|
1925
|
+
.filter((f) => listScope !== "current" || single || f.dir === sessionsDir())
|
|
1926
|
+
.flatMap((f) => (f.dir === sessionsDir() ? agent.sessions() : new SessionStore(f.dir).list()))
|
|
1927
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
1928
|
+
for (const meta of metas) {
|
|
1929
|
+
if (listScope === "current" && single && workspaceOf(meta.id) !== here)
|
|
1930
|
+
continue;
|
|
1514
1931
|
console.log(renderSessionLine(meta));
|
|
1515
1932
|
}
|
|
1516
1933
|
}
|
|
@@ -1615,7 +2032,7 @@ async function main() {
|
|
|
1615
2032
|
" kiso chat [sessionId] same as above\n" +
|
|
1616
2033
|
" kiso resume pick a session to continue (TTY picker)\n" +
|
|
1617
2034
|
" kiso resume <id> [prompt] continue a session (one-shot)\n" +
|
|
1618
|
-
" kiso sessions
|
|
2035
|
+
" kiso sessions [--all|--current] list durable sessions (a terminal shows this workspace's by default)\n" +
|
|
1619
2036
|
" kiso login <provider> anthropic|openai|deepseek|zai: store an API key (hidden prompt, or stdin when piped);\n" +
|
|
1620
2037
|
" chatgpt: sign in with a ChatGPT subscription (browser; unofficial third-party flow)\n" +
|
|
1621
2038
|
" kiso logout <provider> remove the stored credential\n" +
|
|
@@ -1624,7 +2041,7 @@ async function main() {
|
|
|
1624
2041
|
" kiso help this help\n\n" +
|
|
1625
2042
|
"flags (any position):\n" +
|
|
1626
2043
|
" --model <profile|provider/model> pick the model (also /model in-session)\n" +
|
|
1627
|
-
" --mode <tier> approval tier:
|
|
2044
|
+
" --mode <tier> approval tier: default|accept-edits|plan|dontAsk|bypass\n" +
|
|
1628
2045
|
" --version print the version\n\n" +
|
|
1629
2046
|
"configuration:\n" +
|
|
1630
2047
|
" no key keyless faux demo (a scripted four-round session)\n" +
|
|
@@ -1654,7 +2071,7 @@ async function main() {
|
|
|
1654
2071
|
// lives inside the resolver. It used to pass the env-only
|
|
1655
2072
|
// reader, so a user's config.json autoCompact worked on
|
|
1656
2073
|
// `kiso chat` and silently vanished on the default entry.
|
|
1657
|
-
await chatLoop(agent, id, input,
|
|
2074
|
+
await chatLoop(agent, id, input, retiredAutoCompact(mergedConfig));
|
|
1658
2075
|
break;
|
|
1659
2076
|
}
|
|
1660
2077
|
}
|