@vincemakes/kiso-code 0.39.1 → 0.40.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 +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 +28 -2
- package/dist/chat.js +143 -25
- package/dist/config.d.ts +11 -1
- package/dist/config.js +48 -3
- package/dist/dispatch.d.ts +10 -2
- package/dist/dispatch.js +138 -23
- package/dist/floor.d.ts +80 -0
- package/dist/floor.js +652 -0
- package/dist/index.d.ts +21 -2
- package/dist/index.js +397 -44
- 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/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 +67 -1
- package/dist/state.js +98 -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, setExtensionLists, setUserProtectedPaths, protectedFiles, setMergedConfig, setModelChoice, setSessionStore, setRetryShown, setNeverInherited, secretEnvNamesOf, userExtensions, VERSION, lastBinding, acceptDrift, setAcceptDrift, setFloorOn, floorOn, loadedSkillsCatalog } 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.
|
|
@@ -484,7 +497,11 @@ function bannerExtensionText() {
|
|
|
484
497
|
// terminal layer (extensionsBannerText — a pure function of the three
|
|
485
498
|
// name lists, including the "(connecting…)" in-flight label). Which
|
|
486
499
|
// lists exist is the CLI's fact and stays here.
|
|
487
|
-
|
|
500
|
+
// 0.40.0: in dontAsk the ask extension is loaded but offers no tool
|
|
501
|
+
// (builtin.ts offInDontAsk) — the banner says so beside the tier that
|
|
502
|
+
// turns it off, rather than listing it as if it could ask.
|
|
503
|
+
const builtIn = builtInExtensions.map((e) => (e.name === "ask" && getMode() === "dontAsk" ? { name: e.name, note: "off in dontAsk" } : e));
|
|
504
|
+
return extensionsBannerText(builtIn, userExtensions, projectExtensions);
|
|
488
505
|
}
|
|
489
506
|
/** E1: the startup banner line(s) — TTY: logo + merged extensions + the
|
|
490
507
|
* W5 resume list as a LIVE banner cell (W1: the tier re-derives on
|
|
@@ -619,9 +636,18 @@ const INSTRUCTION_FILES = ["AGENTS.md", "CLAUDE.md"];
|
|
|
619
636
|
* return it as an injected section, or "" when none exists. Truncated at
|
|
620
637
|
* 8KB with an explicit note. Pure — read once per session, so the prompt
|
|
621
638
|
* is byte-stable for the session's lifetime.
|
|
639
|
+
*
|
|
640
|
+
* kiso never serves its own credential store to a model, and this read is
|
|
641
|
+
* the one that needs no model at all: a cloned repository whose AGENTS.md
|
|
642
|
+
* is a symlink to the store would put it in the SYSTEM PROMPT of every
|
|
643
|
+
* request, with no tool call and no trust prompt. A file that resolves to
|
|
644
|
+
* a protected one is skipped as though absent.
|
|
622
645
|
*/
|
|
623
|
-
export function readProjectInstructions(cwd) {
|
|
646
|
+
export function readProjectInstructions(cwd, protectedFiles = []) {
|
|
647
|
+
const id = protectedIdentity(protectedFiles);
|
|
624
648
|
for (const name of INSTRUCTION_FILES) {
|
|
649
|
+
if (isProtectedPath(join(cwd, name), id))
|
|
650
|
+
continue;
|
|
625
651
|
let text;
|
|
626
652
|
try {
|
|
627
653
|
text = readFileSync(join(cwd, name), "utf8");
|
|
@@ -635,8 +661,8 @@ export function readProjectInstructions(cwd) {
|
|
|
635
661
|
}
|
|
636
662
|
/** A area: the session's system prompt — the constant plus any project
|
|
637
663
|
* instructions found in the workspace. Deterministic per cwd. */
|
|
638
|
-
export function composeSystemPrompt(cwd) {
|
|
639
|
-
const injected = readProjectInstructions(cwd);
|
|
664
|
+
export function composeSystemPrompt(cwd, protectedFiles = []) {
|
|
665
|
+
const injected = readProjectInstructions(cwd, protectedFiles);
|
|
640
666
|
return injected === "" ? SYSTEM_PROMPT : `${SYSTEM_PROMPT}\n${injected}`;
|
|
641
667
|
}
|
|
642
668
|
/**
|
|
@@ -700,7 +726,12 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
700
726
|
// the trust record is the first home write, the scaffold the second.
|
|
701
727
|
if (isFirstRun())
|
|
702
728
|
scaffoldFirstRun();
|
|
703
|
-
|
|
729
|
+
// 0.40.0 — one folder per project: the one-time move of the legacy
|
|
730
|
+
// folder, then this project's folder claimed (made, its workspace
|
|
731
|
+
// recorded). Both write, so both sit behind the verdict, with the store.
|
|
732
|
+
prepareSessionFolders();
|
|
733
|
+
activeStoreDir = sessionsDir();
|
|
734
|
+
const store = new SessionStore(activeStoreDir);
|
|
704
735
|
// TUI2-R2 ②/③: the navigation surfaces read through THIS store — one
|
|
705
736
|
// store per process, and the picker/listing never write through it.
|
|
706
737
|
setSessionStore(store);
|
|
@@ -736,7 +767,12 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
736
767
|
// DT-1a: what a delegated task may NAME — the configured checks and the
|
|
737
768
|
// model profiles — handed to the (in-process) subagent extension through
|
|
738
769
|
// the environment. A model never supplies a command; it names a check.
|
|
739
|
-
|
|
770
|
+
// 0.40.0: and the folder this process keeps its sessions in — a child
|
|
771
|
+
// writes beside its parent, whatever directory it runs in. Through this
|
|
772
|
+
// channel and not an exported KISO_SESSIONS_DIR: a variable in
|
|
773
|
+
// process.env would reach every shell child, and a kiso started from a
|
|
774
|
+
// shell tool would then write into this project's folder.
|
|
775
|
+
process.env.KISO_DELEGATION_CONFIG_JSON = JSON.stringify({ checks: merged.checks ?? {}, profiles: Object.keys(merged.models ?? {}), sessionsDir: sessionsDir() });
|
|
740
776
|
setConfigModels(merged.models ?? {});
|
|
741
777
|
const resolved = resolveModel(modelFlag, merged);
|
|
742
778
|
// AFTER the model resolves: the window a PROFILE states is about that
|
|
@@ -762,14 +798,45 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
762
798
|
// re-reads the config's extensions array per run).
|
|
763
799
|
// LT-2: the loop breaker at the chain HEAD — it speaks first when it speaks,
|
|
764
800
|
// so `decidedBy` names it; a deny there beats every tier, bypass included.
|
|
765
|
-
|
|
801
|
+
// 0.40.0: the read-only shell allow sits after the tiers — an allow from
|
|
802
|
+
// it outranks a tier's ask and names itself in decidedBy.
|
|
803
|
+
// 0.40.0: a saved allow never carries a write into .git/ or .kiso/, nor
|
|
804
|
+
// a destructive shell command.
|
|
805
|
+
const workspaceRoot = () => codingToolOptions().workspaceRoot;
|
|
806
|
+
const neverInherited = (call) => isProtectedWrite(call, workspaceRoot()) || isDestructiveCall(call);
|
|
807
|
+
setNeverInherited(neverInherited);
|
|
808
|
+
// 0.40.0: the catastrophe floor, at the chain's HEAD — a deny there
|
|
809
|
+
// names itself in decidedBy and outranks every tier, bypass included.
|
|
810
|
+
// Read per agent, so /reload picks up an edited user config.
|
|
811
|
+
const userConfig = loadUserConfig();
|
|
812
|
+
setFloorOn(userConfig?.floor !== "off");
|
|
813
|
+
// kiso never serves its own credential store to a model: the file tools
|
|
814
|
+
// refuse it through codingToolOptions, and this member denies a shell
|
|
815
|
+
// line naming it — at the HEAD with the floor, in every mode, and NOT
|
|
816
|
+
// switched off with it (`floor: "off"` lowers the catastrophe floor,
|
|
817
|
+
// never this).
|
|
818
|
+
setUserProtectedPaths(userConfig?.protectedPaths);
|
|
819
|
+
const extensions = [
|
|
820
|
+
protectedShellExtension({ files: protectedFiles, workspaceRoot, env: () => ({ home: homedir(), kisoHome: kisoHome() }) }),
|
|
821
|
+
floorExtension(() => floorOn, workspaceRoot),
|
|
822
|
+
breakerExtension(),
|
|
823
|
+
...modeExtensions(workspaceRoot),
|
|
824
|
+
readOnlyShellExtension(codingToolOptions),
|
|
825
|
+
...loadedExtensions.map((e) => guardSavedAllow(e, neverInherited)),
|
|
826
|
+
];
|
|
766
827
|
setCurrentAgentExtensions(extensions);
|
|
767
828
|
// E6: the run-start context policy (captured once — exactOptionalPropertyTypes).
|
|
768
829
|
const contextPolicy = contextPolicyFromEnv();
|
|
769
830
|
const idleFromEnv = streamIdleFromEnv(); // read once: a narrowed const, not a call per spread
|
|
831
|
+
const retriesFromEnv = maxRetriesFromEnv();
|
|
770
832
|
const definition = {
|
|
771
833
|
model,
|
|
772
834
|
store,
|
|
835
|
+
// 0.40.0: a NEW session records where it started; the picker scopes by
|
|
836
|
+
// it. The profile name is recorded only when the model came from a
|
|
837
|
+
// config profile — a direct provider/model or an env key names none.
|
|
838
|
+
workspace: workspaceRoot(),
|
|
839
|
+
...(resolved !== null && merged.models?.[resolved.name] === resolved.profile ? { profileName: resolved.name } : {}),
|
|
773
840
|
// Area 5: the coding tools are bound to the workspace — every path
|
|
774
841
|
// they touch is canonicalized inside cwd, escapes are refused.
|
|
775
842
|
tools: [...createCodingTools(codingToolOptions())], // DC-49 — the options live in state.ts, shared with the `!` command's runner
|
|
@@ -779,19 +846,19 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
779
846
|
// still counts loadedExtensions only — the modes are in-process,
|
|
780
847
|
// never a file extension.
|
|
781
848
|
systemPrompt: (() => {
|
|
782
|
-
const sp = composeSystemPrompt(process.cwd());
|
|
849
|
+
const sp = composeSystemPrompt(process.cwd(), protectedFiles());
|
|
783
850
|
const extra = modeSystemPrompt();
|
|
784
851
|
return extra === undefined ? sp : `${sp}\n\n${extra}`;
|
|
785
852
|
})(),
|
|
786
|
-
//
|
|
787
|
-
//
|
|
788
|
-
//
|
|
789
|
-
//
|
|
790
|
-
//
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
853
|
+
// ADR-0055 Amendment 1 (A1b): compaction is ON by default and runs
|
|
854
|
+
// INSIDE a run, by tiers drawn from the model's window (CTX-1: the
|
|
855
|
+
// binding step moves the window with /model and /resume). The
|
|
856
|
+
// standing microcompact at half the window is gone (A4). Phase rule
|
|
857
|
+
// 1 reads the user's configured checks first, then the runner table.
|
|
858
|
+
contextPolicy: {
|
|
859
|
+
...(contextPolicy ?? {}),
|
|
860
|
+
tiers: { windowTokens: contextWindowTokens(), isCheck: (command) => runsACheck(command, Object.values(merged.checks ?? {})) },
|
|
861
|
+
},
|
|
795
862
|
// R3e (owner ruling, 2026-08-28): NO turn limit on an interactive
|
|
796
863
|
// session. This was `maxTurns: 20`, hardcoded on 2026-08-03 with no
|
|
797
864
|
// stated reason and no way to change it — and it was the thing that
|
|
@@ -823,9 +890,19 @@ async function makeAgent(sessionId, input, modelFlag) {
|
|
|
823
890
|
// LT-1: KISO_STREAM_IDLE_MS (the test rigs' knob) beats the profile —
|
|
824
891
|
// the last spread wins, which is why it sits after the profile's.
|
|
825
892
|
...(idleFromEnv !== undefined ? { streamIdleMs: idleFromEnv } : {}),
|
|
893
|
+
...(retriesFromEnv !== undefined ? { maxRetries: retriesFromEnv } : {}),
|
|
894
|
+
// ADR-0005 Amendment 2: the kernel announces each retry before its
|
|
895
|
+
// wait; the running row shows it. Composed with every extension's
|
|
896
|
+
// hooks by the runtime — an extension observing retries too is heard.
|
|
897
|
+
hooks: {
|
|
898
|
+
onRetry: async (info) => {
|
|
899
|
+
setRetryShown({ attempt: info.attempt, maxRetries: info.maxRetries, code: info.code, until: Date.now() + info.delayMs });
|
|
900
|
+
},
|
|
901
|
+
},
|
|
826
902
|
};
|
|
827
903
|
return createAgent(definition);
|
|
828
904
|
}
|
|
905
|
+
export { workspaceRoot } from "./state.js";
|
|
829
906
|
/** TUI2-R2 ② — the picker's affordance row: the keys, said where the
|
|
830
907
|
* keys are useful. */
|
|
831
908
|
const PICKER_HINT = "↑↓ pick · ⏎ resumes · type filters · esc";
|
|
@@ -834,11 +911,139 @@ const PICKER_HINT = "↑↓ pick · ⏎ resumes · type filters · esc";
|
|
|
834
911
|
* runtime's own accessors (see session-cards.ts); this is only the
|
|
835
912
|
* plumbing that hands it the store's read side.
|
|
836
913
|
*/
|
|
837
|
-
async function sessionCards(
|
|
914
|
+
async function sessionCards(_agent, announce = (l) => bodyLog(l)) {
|
|
838
915
|
const store = sessionStoreRef;
|
|
839
916
|
if (store === null)
|
|
840
917
|
return []; // unreachable: makeAgent builds the store first
|
|
841
|
-
|
|
918
|
+
const folders = listingFolders();
|
|
919
|
+
// 0.40.0 dogfood (item 2, lead's ruling A): the ONE-TIME migration — the
|
|
920
|
+
// first list after the upgrade reads each legacy log exactly once and
|
|
921
|
+
// writes its summary into the sidecar; announced with its count. Per
|
|
922
|
+
// folder: each carries its own marker.
|
|
923
|
+
const pending = folders.filter((f) => summaryMigrationPending(f.dir));
|
|
924
|
+
const count = pending.reduce((n, f) => n + listSessionSidecars(f.dir).filter((l) => l.summary === null).length, 0);
|
|
925
|
+
if (count > 0)
|
|
926
|
+
announce(`recording summaries for ${count} older session${count === 1 ? "" : "s"} — once`);
|
|
927
|
+
for (const f of pending) {
|
|
928
|
+
const reader = f.dir === activeStoreDir ? store : new SessionStore(f.dir);
|
|
929
|
+
migrateSummaries(f.dir, (id) => reader.load(id));
|
|
930
|
+
}
|
|
931
|
+
// …and from then on, the SIDECARS only: no log is opened to draw a row.
|
|
932
|
+
// A project folder's sessions are that project's; one without a recorded
|
|
933
|
+
// workspace was placed by inference and says so.
|
|
934
|
+
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)));
|
|
935
|
+
}
|
|
936
|
+
/**
|
|
937
|
+
* 0.40.0 — the folders a listing reads: every project's under the
|
|
938
|
+
* per-project layout (the picker opens on this one, `tab` shows all), and
|
|
939
|
+
* the one folder otherwise. This process's folder is always first, even
|
|
940
|
+
* before its first session exists.
|
|
941
|
+
*/
|
|
942
|
+
function listingFolders() {
|
|
943
|
+
const here = ownSessionsDir();
|
|
944
|
+
const home = kisoHome();
|
|
945
|
+
if (!projectLayoutActive(home))
|
|
946
|
+
return [{ dir: here, workspace: null, kind: "legacy" }];
|
|
947
|
+
const all = sessionFolders(home);
|
|
948
|
+
const mine = all.find((f) => f.dir === here) ?? { dir: here, workspace: projectRoot(), kind: "project" };
|
|
949
|
+
return [mine, ...all.filter((f) => f.dir !== here)];
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* 0.40.0 — before any session opens: the one-time move of the legacy
|
|
953
|
+
* folder into project folders (announced once, with the undo), then this
|
|
954
|
+
* project's folder claimed. Nothing happens under a pinned folder or after
|
|
955
|
+
* a reversed migration.
|
|
956
|
+
*/
|
|
957
|
+
function prepareSessionFolders() {
|
|
958
|
+
const home = kisoHome();
|
|
959
|
+
if (!projectLayoutActive(home))
|
|
960
|
+
return;
|
|
961
|
+
if (pendingLegacyIds(home).length > 0) {
|
|
962
|
+
const result = runMigration(home, planMigration(home));
|
|
963
|
+
if (result !== null && result.moved > 0) {
|
|
964
|
+
const line = migrationNotice(result);
|
|
965
|
+
if (process.stdout.isTTY)
|
|
966
|
+
bodyLog(line);
|
|
967
|
+
else
|
|
968
|
+
process.stderr.write(`${line}\n`);
|
|
969
|
+
}
|
|
970
|
+
}
|
|
971
|
+
claimProjectDir(ownSessionsDir(), projectRoot());
|
|
972
|
+
}
|
|
973
|
+
/**
|
|
974
|
+
* 0.40.0 (the owner's dogfood: a uooki session resumed from flowpix2) —
|
|
975
|
+
* whether a session id may open in this project. Its own folder: yes. A
|
|
976
|
+
* session another project RECORDED: refused, with where to go. One whose
|
|
977
|
+
* project is unknown, or was only inferred, is never refused and never
|
|
978
|
+
* moved (the lead's ruling: a resume changes no placement) — it opens where
|
|
979
|
+
* it is, and the line says the tools work here. A pure read. `null` = not
|
|
980
|
+
* found anywhere (the caller's own rule applies: a new session, or "no
|
|
981
|
+
* such session").
|
|
982
|
+
*/
|
|
983
|
+
export function routeSession(id) {
|
|
984
|
+
const own = ownSessionsDir();
|
|
985
|
+
if (hasSession(own, id))
|
|
986
|
+
return { kind: "here" };
|
|
987
|
+
const home = kisoHome();
|
|
988
|
+
if (!projectLayoutActive(home))
|
|
989
|
+
return null;
|
|
990
|
+
const found = locateSession(home, id, own);
|
|
991
|
+
if (found === null)
|
|
992
|
+
return null;
|
|
993
|
+
const profile = readProfile(found.dir, id);
|
|
994
|
+
const recorded = profile.kind === "ok" && profile.profile.workspace !== null ? canonicalPath(profile.profile.workspace) : null;
|
|
995
|
+
const cwd = projectRoot();
|
|
996
|
+
const owner = found.kind === "project" ? (found.workspace ?? recorded) : recorded;
|
|
997
|
+
if (recorded !== null && owner !== null && owner !== cwd) {
|
|
998
|
+
return { kind: "refused", line: `this session belongs to ${tildeOf(owner)} — cd there to resume it` };
|
|
999
|
+
}
|
|
1000
|
+
// recorded here, and still in the legacy folder (open elsewhere when the
|
|
1001
|
+
// move ran): nothing to warn about
|
|
1002
|
+
if (recorded !== null)
|
|
1003
|
+
return { kind: "elsewhere", dir: found.dir, line: null };
|
|
1004
|
+
const inferred = found.kind === "project" && owner !== null ? `workspace inferred as ${tildeOf(owner)}, never recorded` : "workspace unknown";
|
|
1005
|
+
return { kind: "elsewhere", dir: found.dir, line: `${inferred} — tools work in ${tildeOf(workspaceRoot())}` };
|
|
1006
|
+
}
|
|
1007
|
+
/** The folder a session opens in: the one the store is on when it holds
|
|
1008
|
+
* it, else this project's, else where the route found it. A new id opens
|
|
1009
|
+
* in this project's folder. */
|
|
1010
|
+
function sessionFolderOf(id) {
|
|
1011
|
+
if (hasSession(activeStoreDir, id))
|
|
1012
|
+
return activeStoreDir;
|
|
1013
|
+
const route = routeSession(id);
|
|
1014
|
+
return route?.kind === "elsewhere" ? route.dir : ownSessionsDir();
|
|
1015
|
+
}
|
|
1016
|
+
/** The folder makeAgent built the store on. */
|
|
1017
|
+
let activeStoreDir = "";
|
|
1018
|
+
/** The workspace a listing is scoped to: a project folder's identity under
|
|
1019
|
+
* the per-project layout, the recorded realpath in one pinned folder. */
|
|
1020
|
+
function scopeRoot() {
|
|
1021
|
+
return projectLayoutActive(kisoHome()) ? projectRoot() : workspaceRoot();
|
|
1022
|
+
}
|
|
1023
|
+
function tildeOf(path) {
|
|
1024
|
+
const home = homedir();
|
|
1025
|
+
return path === home || path.startsWith(`${home}/`) ? `~${path.slice(home.length)}` : path;
|
|
1026
|
+
}
|
|
1027
|
+
/** The explicit-id doors (`chat <id>`, `resume <id>`, `-p … <id>`), BEFORE
|
|
1028
|
+
* makeAgent builds the store: a refusal is the entry's error (the line on
|
|
1029
|
+
* stderr, exit 2); a session in another folder opens there. Returns the
|
|
1030
|
+
* line to say once the session is up. */
|
|
1031
|
+
function enterRouted(id) {
|
|
1032
|
+
const route = routeSession(id);
|
|
1033
|
+
if (route === null || route.kind === "here")
|
|
1034
|
+
return null;
|
|
1035
|
+
if (route.kind === "refused")
|
|
1036
|
+
throw new CliUsageError(route.line);
|
|
1037
|
+
setOpenSessionFolder(route.dir);
|
|
1038
|
+
return route.line;
|
|
1039
|
+
}
|
|
1040
|
+
function sayRouteLine(line) {
|
|
1041
|
+
if (line === null)
|
|
1042
|
+
return;
|
|
1043
|
+
if (process.stdout.isTTY)
|
|
1044
|
+
bodyLog(line);
|
|
1045
|
+
else
|
|
1046
|
+
process.stderr.write(`${line}\n`);
|
|
842
1047
|
}
|
|
843
1048
|
/**
|
|
844
1049
|
* TUI2-R2 ② — the resume picker: the band, the keys, the id.
|
|
@@ -866,7 +1071,8 @@ async function pickSession(agent, input) {
|
|
|
866
1071
|
}
|
|
867
1072
|
dock.setStatus("", PICKER_HINT);
|
|
868
1073
|
const picked = await new Promise((resolve) => {
|
|
869
|
-
|
|
1074
|
+
// 0.40.0: scoped to the running workspace; tab flips to all
|
|
1075
|
+
input.pick(() => cards, resolve, scopeRoot());
|
|
870
1076
|
});
|
|
871
1077
|
dock.setStatus("", null);
|
|
872
1078
|
return picked;
|
|
@@ -892,6 +1098,21 @@ async function pickSession(agent, input) {
|
|
|
892
1098
|
* formatter — never a boot-time copy, which would drift from the real
|
|
893
1099
|
* row the moment either changed.
|
|
894
1100
|
*/
|
|
1101
|
+
/**
|
|
1102
|
+
* ADR-0055 Amendment 1 (the owner, 2026-09-18): `autoCompact` — the
|
|
1103
|
+
* between-turn ratio, from config or KISO_AUTO_COMPACT — is RETIRED. The
|
|
1104
|
+
* in-run tiers compact inside a run and before its first request, which is
|
|
1105
|
+
* everything it did and more. 0.40.0 accepts it, ignores it and says so
|
|
1106
|
+
* once; 0.41.0 removes it.
|
|
1107
|
+
*/
|
|
1108
|
+
let autoCompactNoticed = false;
|
|
1109
|
+
function retiredAutoCompact(merged) {
|
|
1110
|
+
if (!autoCompactNoticed && (process.env.KISO_AUTO_COMPACT !== undefined || resolveAutoCompact(merged) !== undefined)) {
|
|
1111
|
+
autoCompactNoticed = true;
|
|
1112
|
+
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");
|
|
1113
|
+
}
|
|
1114
|
+
return undefined;
|
|
1115
|
+
}
|
|
895
1116
|
/**
|
|
896
1117
|
* CTX-1 (Astra F34-1): EVERY entry point that opens a session must bind the
|
|
897
1118
|
* policy that follows the session's OWN model.
|
|
@@ -913,11 +1134,20 @@ async function pickSession(agent, input) {
|
|
|
913
1134
|
* threshold got stuck in the first place.
|
|
914
1135
|
*/
|
|
915
1136
|
function bindRestoredSession(session) {
|
|
1137
|
+
// 0.40.0: an acknowledged drift says what it did, once — including the
|
|
1138
|
+
// owner-ruled reasoning reset, which was silent until now.
|
|
1139
|
+
const ack = session.driftAcknowledgement ?? null;
|
|
1140
|
+
if (ack !== null) {
|
|
1141
|
+
const was = ack.reasoningReset;
|
|
1142
|
+
const prior = [was.effort !== "default" ? `effort ${was.effort}` : "", was.thinking !== "default" ? `thinking ${was.thinking}` : ""].filter((s) => s !== "").join(", ");
|
|
1143
|
+
bodyLog(`drift acknowledged — now on ${session.model}; reasoning reset to defaults${prior === "" ? "" : ` (was ${prior})`}`);
|
|
1144
|
+
}
|
|
916
1145
|
setAgentModel(session.model, session.baseUrl);
|
|
917
1146
|
session.setMicrocompactThreshold(microcompactThresholdFor({
|
|
918
1147
|
model: session.model,
|
|
919
1148
|
...(session.baseUrl !== undefined ? { baseUrl: session.baseUrl } : {}),
|
|
920
1149
|
}));
|
|
1150
|
+
session.setContextWindow(contextWindowTokens({ model: session.model, ...(session.baseUrl !== undefined ? { baseUrl: session.baseUrl } : {}) }));
|
|
921
1151
|
}
|
|
922
1152
|
function paintBootStatus(session) {
|
|
923
1153
|
if (!dock.active)
|
|
@@ -971,7 +1201,7 @@ function skillCount() {
|
|
|
971
1201
|
* is the durable log on disk. The record never moves: a reload is
|
|
972
1202
|
* invisible in it, because nothing the model said or did changes.
|
|
973
1203
|
*/
|
|
974
|
-
async function reloadAgent(old, id, input) {
|
|
1204
|
+
async function reloadAgent(old, id, input, announce = true) {
|
|
975
1205
|
// Snapshot BEFORE makeAgent: it overwrites every one of these, so the
|
|
976
1206
|
// old set would be unreachable by the time we needed to dispose it.
|
|
977
1207
|
const oldLoaded = loadedExtensions;
|
|
@@ -1051,7 +1281,8 @@ async function reloadAgent(old, id, input) {
|
|
|
1051
1281
|
// best-effort — the temp dir would be reaped by the OS
|
|
1052
1282
|
}
|
|
1053
1283
|
}
|
|
1054
|
-
|
|
1284
|
+
if (announce)
|
|
1285
|
+
bodyLog(`[reload] ${loadedExtensions.length} extensions, ${skillCount()} skills — the conversation is unchanged`);
|
|
1055
1286
|
return next;
|
|
1056
1287
|
}
|
|
1057
1288
|
async function chatLoop(first, firstId, input, autoCompact) {
|
|
@@ -1064,6 +1295,29 @@ async function chatLoop(first, firstId, input, autoCompact) {
|
|
|
1064
1295
|
// as a new session, which is the one thing it is not.
|
|
1065
1296
|
let rebuilt = false;
|
|
1066
1297
|
for (;;) {
|
|
1298
|
+
// 0.40.0 (the lead's ruling): a session opens in the folder that holds
|
|
1299
|
+
// it, never moved — a switch that crosses folders rebuilds the agent
|
|
1300
|
+
// on the other folder's store, and one back rebuilds it on this
|
|
1301
|
+
// project's. A failed rebuild switches nothing.
|
|
1302
|
+
const folder = sessionFolderOf(id);
|
|
1303
|
+
if (folder !== activeStoreDir) {
|
|
1304
|
+
const was = { dir: activeStoreDir, store: sessionStoreRef };
|
|
1305
|
+
setOpenSessionFolder(folder === ownSessionsDir() ? null : folder);
|
|
1306
|
+
const next = await reloadAgent(agent, id, input, false);
|
|
1307
|
+
if (next === agent) {
|
|
1308
|
+
// makeAgent may have built the new store before it failed
|
|
1309
|
+
activeStoreDir = was.dir;
|
|
1310
|
+
if (was.store !== null)
|
|
1311
|
+
setSessionStore(was.store);
|
|
1312
|
+
setOpenSessionFolder(was.dir === ownSessionsDir() ? null : was.dir);
|
|
1313
|
+
if (prev === null)
|
|
1314
|
+
return;
|
|
1315
|
+
id = prev;
|
|
1316
|
+
rebuilt = true;
|
|
1317
|
+
continue;
|
|
1318
|
+
}
|
|
1319
|
+
agent = next;
|
|
1320
|
+
}
|
|
1067
1321
|
const session = await agent.session({ id, ...(acceptDrift() ? { acceptDrift: true } : {}) });
|
|
1068
1322
|
if (rebuilt) {
|
|
1069
1323
|
rebuilt = false;
|
|
@@ -1075,7 +1329,7 @@ async function chatLoop(first, firstId, input, autoCompact) {
|
|
|
1075
1329
|
// prompt inside a conversation with thousands of events — the
|
|
1076
1330
|
// durable log was right there and none of it was shown. Empty for
|
|
1077
1331
|
// a fresh session, so `kiso chat` is byte-identical.
|
|
1078
|
-
|
|
1332
|
+
showResumeTail(session.log.all);
|
|
1079
1333
|
// R2 (owner, 2026-08-27): the resume list is NOT on the opening
|
|
1080
1334
|
// screen. `/resume` is where you go looking for a session; the
|
|
1081
1335
|
// opening's job is to say what THIS one is.
|
|
@@ -1107,7 +1361,7 @@ async function chatLoop(first, firstId, input, autoCompact) {
|
|
|
1107
1361
|
}
|
|
1108
1362
|
else {
|
|
1109
1363
|
bodyLog(`session ${id} (switched — previous: ${prev}, /resume ${prev} returns)\n`);
|
|
1110
|
-
|
|
1364
|
+
showResumeTail(session.log.all);
|
|
1111
1365
|
if (currentFaux)
|
|
1112
1366
|
session.setAdapter(createFauxProvider(readFauxScript().slice(fauxSkip(id))));
|
|
1113
1367
|
}
|
|
@@ -1132,7 +1386,10 @@ async function chatLoop(first, firstId, input, autoCompact) {
|
|
|
1132
1386
|
// be left naming the session the user just left.
|
|
1133
1387
|
paintWindowTitle(session.log.all);
|
|
1134
1388
|
const nav = {
|
|
1135
|
-
|
|
1389
|
+
// 0.40.0 dogfood: the ids only — agent.sessions() read every log whole
|
|
1390
|
+
// (seconds on the owner's 118 sessions) before /resume could open
|
|
1391
|
+
sessions: () => agent.sessionIds(),
|
|
1392
|
+
route: (sessionId) => routeSession(sessionId),
|
|
1136
1393
|
...(process.stdin.isTTY ? { pick: () => pickSession(agent, input) } : {}),
|
|
1137
1394
|
};
|
|
1138
1395
|
const end = await chat(session, currentFaux, input, autoCompact, nav);
|
|
@@ -1197,6 +1454,20 @@ async function readSecret(prompt) {
|
|
|
1197
1454
|
process.stdin.on("data", onData);
|
|
1198
1455
|
});
|
|
1199
1456
|
}
|
|
1457
|
+
/**
|
|
1458
|
+
* What a resumed session shows (REL-0152-D5, 4c). On the compositor: the
|
|
1459
|
+
* durable events replayed into cells — the last two turns in full, the
|
|
1460
|
+
* earlier ones in one fold row the ctrl+r viewer reads. Through a pipe
|
|
1461
|
+
* there is no viewer and no cell renderer, so the plain text tail stays
|
|
1462
|
+
* (DC-51: one call, one cell).
|
|
1463
|
+
*/
|
|
1464
|
+
function showResumeTail(events) {
|
|
1465
|
+
const W = process.stdout.columns ?? 80;
|
|
1466
|
+
if (dock.active)
|
|
1467
|
+
replayInto(body, events, W);
|
|
1468
|
+
else
|
|
1469
|
+
bodyLog(resumeTail(events, W).join("\n"));
|
|
1470
|
+
}
|
|
1200
1471
|
async function main() {
|
|
1201
1472
|
// E group (the graceful-exit gate ③, R-G 0.1.48): a terminal closing
|
|
1202
1473
|
// turns the in-flight stdout/stderr writes into EIO, and node's
|
|
@@ -1273,7 +1544,7 @@ async function main() {
|
|
|
1273
1544
|
if (modeFlag !== -1) {
|
|
1274
1545
|
const m = MODES.find((x) => x === args[modeFlag + 1]);
|
|
1275
1546
|
if (m === undefined) {
|
|
1276
|
-
console.error(`unknown mode: ${args[modeFlag + 1]} (tiers: ${
|
|
1547
|
+
console.error(`unknown mode: ${args[modeFlag + 1]} (tiers: ${OFFERED_MODES.join(", ")})`);
|
|
1277
1548
|
process.exit(2);
|
|
1278
1549
|
}
|
|
1279
1550
|
setMode(m);
|
|
@@ -1292,6 +1563,11 @@ async function main() {
|
|
|
1292
1563
|
const path = args[i + 1];
|
|
1293
1564
|
if (path === undefined)
|
|
1294
1565
|
throw new CliUsageError("--task-file needs a path");
|
|
1566
|
+
// the file becomes the turn a model reads — never the credential
|
|
1567
|
+
// store. The list here is the store's own path: the user config's
|
|
1568
|
+
// protectedPaths is read later, with the agent
|
|
1569
|
+
if (isProtectedPath(path, protectedIdentity(protectedFiles())))
|
|
1570
|
+
throw new CliUsageError(`--task-file: ${path} — ${PROTECTED_REFUSAL}`);
|
|
1295
1571
|
try {
|
|
1296
1572
|
taskFile = readFileSync(path, "utf8");
|
|
1297
1573
|
}
|
|
@@ -1301,6 +1577,30 @@ async function main() {
|
|
|
1301
1577
|
args.splice(i, 2);
|
|
1302
1578
|
}
|
|
1303
1579
|
}
|
|
1580
|
+
// 0.40.0: `kiso sessions --all | --current` — the one command that takes
|
|
1581
|
+
// them. Parsed only there, so neither ever becomes a session id. Absent
|
|
1582
|
+
// = each form's default (TTY: current; pipe: all, today's bytes).
|
|
1583
|
+
let listScope;
|
|
1584
|
+
let reverseManifest;
|
|
1585
|
+
if (args[0] === "sessions") {
|
|
1586
|
+
// 0.40.0: the undo of the per-project move, named by its manifest
|
|
1587
|
+
const r = args.indexOf("--reverse-migration");
|
|
1588
|
+
if (r !== -1) {
|
|
1589
|
+
reverseManifest = args[r + 1];
|
|
1590
|
+
if (reverseManifest === undefined || reverseManifest.startsWith("--"))
|
|
1591
|
+
throw new CliUsageError("kiso sessions --reverse-migration <manifest> — the path the move announced");
|
|
1592
|
+
args.splice(r, 2);
|
|
1593
|
+
}
|
|
1594
|
+
for (const flag of ["--all", "--current"]) {
|
|
1595
|
+
const i = args.indexOf(flag);
|
|
1596
|
+
if (i === -1)
|
|
1597
|
+
continue;
|
|
1598
|
+
args.splice(i, 1);
|
|
1599
|
+
if (listScope !== undefined && listScope !== flag.slice(2))
|
|
1600
|
+
throw new CliUsageError("kiso sessions: --all and --current contradict each other");
|
|
1601
|
+
listScope = flag.slice(2);
|
|
1602
|
+
}
|
|
1603
|
+
}
|
|
1304
1604
|
const [command, arg] = args;
|
|
1305
1605
|
// round 8: faux mode is the keyless demo script — an exhausted script must
|
|
1306
1606
|
// exit non-zero, never masquerade as a successful provider run. The
|
|
@@ -1413,8 +1713,10 @@ async function main() {
|
|
|
1413
1713
|
// the -p flow: recovery-first one-shot, the resume() machinery
|
|
1414
1714
|
// verbatim (a fresh id makes the recovery a no-op)
|
|
1415
1715
|
const id = command ?? newSessionId(sessionsDir());
|
|
1716
|
+
const routeLine = command !== undefined ? enterRouted(id) : null;
|
|
1416
1717
|
agent = await makeAgent(id, input, modelFlag);
|
|
1417
1718
|
applyConfigMode();
|
|
1719
|
+
sayRouteLine(routeLine);
|
|
1418
1720
|
const session = await agent.session({ id, ...(acceptDrift() ? { acceptDrift: true } : {}) });
|
|
1419
1721
|
bindRestoredSession(session); // CTX-1 (F34-1): the -p door, too
|
|
1420
1722
|
faux = currentFaux;
|
|
@@ -1431,10 +1733,12 @@ async function main() {
|
|
|
1431
1733
|
dock.enter();
|
|
1432
1734
|
// E area: a resumed session continues the script at its durable
|
|
1433
1735
|
// position — never restarts it (fauxSkip).
|
|
1736
|
+
const routeLine = arg !== undefined ? enterRouted(id) : null;
|
|
1434
1737
|
agent = await makeAgent(id, input, modelFlag);
|
|
1435
1738
|
applyConfigMode();
|
|
1739
|
+
sayRouteLine(routeLine);
|
|
1436
1740
|
faux = currentFaux;
|
|
1437
|
-
await chatLoop(agent, id, input,
|
|
1741
|
+
await chatLoop(agent, id, input, retiredAutoCompact(mergedConfig));
|
|
1438
1742
|
break;
|
|
1439
1743
|
}
|
|
1440
1744
|
case "resume": {
|
|
@@ -1451,6 +1755,7 @@ async function main() {
|
|
|
1451
1755
|
// (argv = [node, script, resume, id, prompt?]).
|
|
1452
1756
|
const prompt = process.argv[4];
|
|
1453
1757
|
dock.enter();
|
|
1758
|
+
const routeLine = arg !== undefined ? enterRouted(arg) : null;
|
|
1454
1759
|
agent = await makeAgent(arg, input, modelFlag);
|
|
1455
1760
|
applyConfigMode();
|
|
1456
1761
|
let id = arg;
|
|
@@ -1461,15 +1766,25 @@ async function main() {
|
|
|
1461
1766
|
// an error, never a session started behind their back.
|
|
1462
1767
|
if (picked === null)
|
|
1463
1768
|
break;
|
|
1769
|
+
// 0.40.0: `tab` lists every project, and another project's
|
|
1770
|
+
// recorded session is refused here, with where to go
|
|
1771
|
+
const route = routeSession(picked);
|
|
1772
|
+
if (route?.kind === "refused") {
|
|
1773
|
+
bodyLog(route.line);
|
|
1774
|
+
break;
|
|
1775
|
+
}
|
|
1776
|
+
if (route?.kind === "elsewhere" && route.line !== null)
|
|
1777
|
+
bodyLog(route.line);
|
|
1464
1778
|
// the mini-spec (a DECLARED SUPERSESSION of the one-shot
|
|
1465
1779
|
// picker flow): a PICKED session enters the full REPL —
|
|
1466
1780
|
// "resume and keep working" no longer requires knowing to
|
|
1467
1781
|
// type `kiso chat <id>`. The explicit-id one-shot form
|
|
1468
1782
|
// (`kiso resume <id> ["prompt"]`) keeps its exact bytes.
|
|
1469
1783
|
faux = currentFaux;
|
|
1470
|
-
await chatLoop(agent, picked, input,
|
|
1784
|
+
await chatLoop(agent, picked, input, retiredAutoCompact(mergedConfig));
|
|
1471
1785
|
break;
|
|
1472
1786
|
}
|
|
1787
|
+
sayRouteLine(routeLine);
|
|
1473
1788
|
const session = await agent.session({ id, ...(acceptDrift() ? { acceptDrift: true } : {}) });
|
|
1474
1789
|
bindRestoredSession(session); // CTX-1 (F34-1): the explicit-id door
|
|
1475
1790
|
faux = currentFaux;
|
|
@@ -1482,7 +1797,7 @@ async function main() {
|
|
|
1482
1797
|
session.setAdapter(createFauxProvider(readFauxScript().slice(fauxSkip(id))));
|
|
1483
1798
|
// REL-0152-D5 — the same tail on the explicit-id form. NOT on
|
|
1484
1799
|
// the -p path above: that one's stdout is a machine's input.
|
|
1485
|
-
|
|
1800
|
+
showResumeTail(session.log.all);
|
|
1486
1801
|
await resume(session, prompt, faux, input);
|
|
1487
1802
|
break;
|
|
1488
1803
|
}
|
|
@@ -1495,22 +1810,60 @@ async function main() {
|
|
|
1495
1810
|
// "reading 'question'" on the dock-less branch). The input
|
|
1496
1811
|
// exists so the gate's ask can be answered; the listing
|
|
1497
1812
|
// itself never touches it.
|
|
1813
|
+
if (reverseManifest !== undefined) {
|
|
1814
|
+
// before makeAgent: its folder preparation would otherwise run
|
|
1815
|
+
// the very move this undoes
|
|
1816
|
+
const { restored, left } = reverseMigration(kisoHome(), reverseManifest);
|
|
1817
|
+
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` : ""}`);
|
|
1818
|
+
break;
|
|
1819
|
+
}
|
|
1498
1820
|
agent = await makeAgent(undefined, input, modelFlag);
|
|
1499
1821
|
// TUI2-R2 ③ — the same projection the picker renders, printed.
|
|
1500
1822
|
// The PIPE keeps today's bytes exactly: `kiso sessions` is
|
|
1501
1823
|
// something scripts read, and a badge column is a TTY-render
|
|
1502
1824
|
// concern, not a change to a machine interface.
|
|
1825
|
+
// 0.40.0 (lead's ruling): the TTY defaults to THIS workspace and
|
|
1826
|
+
// says so in a header line with both counts; the PIPE defaults to
|
|
1827
|
+
// every session with today's bytes — scripts must not change
|
|
1828
|
+
// under them. An explicit --all or --current applies to both.
|
|
1829
|
+
const here = scopeRoot();
|
|
1503
1830
|
if (process.stdout.isTTY) {
|
|
1504
|
-
const
|
|
1831
|
+
const every = await sessionCards(agent, (l) => console.log(l));
|
|
1832
|
+
const all = (listScope ?? "current") === "all";
|
|
1833
|
+
const inHere = every.filter((c) => c.workspace === here);
|
|
1834
|
+
const cards = all ? every : inHere;
|
|
1505
1835
|
const W = process.stdout.columns ?? 80;
|
|
1506
1836
|
const col = idColumn(cards);
|
|
1507
1837
|
const now = Date.now();
|
|
1838
|
+
console.log(sessionListHeader(inHere.length, every.length, all, W));
|
|
1839
|
+
// 0.40.1: the sessions with no workspace — one counted line, never
|
|
1840
|
+
// listed, unless --all
|
|
1841
|
+
if (!all) {
|
|
1842
|
+
const unknownLine = sessionListUnknownLine(every.filter((c) => c.workspace === null).length, W);
|
|
1843
|
+
if (unknownLine !== "")
|
|
1844
|
+
console.log(unknownLine);
|
|
1845
|
+
}
|
|
1508
1846
|
for (const card of cards)
|
|
1509
|
-
console.log(sessionListRow(card, W, now, col));
|
|
1847
|
+
console.log(sessionListRow(card, W, now, col, all ? here : null));
|
|
1510
1848
|
console.log(sessionListFooter(cards.length, W));
|
|
1511
1849
|
}
|
|
1512
1850
|
else {
|
|
1513
|
-
|
|
1851
|
+
// 0.40.0: every folder's sessions, merged in id order — today's
|
|
1852
|
+
// bytes; --current is this project's folder (in a single pinned
|
|
1853
|
+
// folder, the sessions that recorded this workspace)
|
|
1854
|
+
const folders = listingFolders();
|
|
1855
|
+
const single = folders.length === 1 && folders[0].kind === "legacy";
|
|
1856
|
+
const workspaceOf = (id) => {
|
|
1857
|
+
const r = readProfile(sessionsDir(), id);
|
|
1858
|
+
return r.kind === "ok" ? r.profile.workspace : null;
|
|
1859
|
+
};
|
|
1860
|
+
const metas = folders
|
|
1861
|
+
.filter((f) => listScope !== "current" || single || f.dir === sessionsDir())
|
|
1862
|
+
.flatMap((f) => (f.dir === sessionsDir() ? agent.sessions() : new SessionStore(f.dir).list()))
|
|
1863
|
+
.sort((a, b) => a.id.localeCompare(b.id));
|
|
1864
|
+
for (const meta of metas) {
|
|
1865
|
+
if (listScope === "current" && single && workspaceOf(meta.id) !== here)
|
|
1866
|
+
continue;
|
|
1514
1867
|
console.log(renderSessionLine(meta));
|
|
1515
1868
|
}
|
|
1516
1869
|
}
|
|
@@ -1615,7 +1968,7 @@ async function main() {
|
|
|
1615
1968
|
" kiso chat [sessionId] same as above\n" +
|
|
1616
1969
|
" kiso resume pick a session to continue (TTY picker)\n" +
|
|
1617
1970
|
" kiso resume <id> [prompt] continue a session (one-shot)\n" +
|
|
1618
|
-
" kiso sessions
|
|
1971
|
+
" kiso sessions [--all|--current] list durable sessions (a terminal shows this workspace's by default)\n" +
|
|
1619
1972
|
" kiso login <provider> anthropic|openai|deepseek|zai: store an API key (hidden prompt, or stdin when piped);\n" +
|
|
1620
1973
|
" chatgpt: sign in with a ChatGPT subscription (browser; unofficial third-party flow)\n" +
|
|
1621
1974
|
" kiso logout <provider> remove the stored credential\n" +
|
|
@@ -1624,7 +1977,7 @@ async function main() {
|
|
|
1624
1977
|
" kiso help this help\n\n" +
|
|
1625
1978
|
"flags (any position):\n" +
|
|
1626
1979
|
" --model <profile|provider/model> pick the model (also /model in-session)\n" +
|
|
1627
|
-
" --mode <tier> approval tier:
|
|
1980
|
+
" --mode <tier> approval tier: default|accept-edits|plan|dontAsk|bypass\n" +
|
|
1628
1981
|
" --version print the version\n\n" +
|
|
1629
1982
|
"configuration:\n" +
|
|
1630
1983
|
" no key keyless faux demo (a scripted four-round session)\n" +
|
|
@@ -1654,7 +2007,7 @@ async function main() {
|
|
|
1654
2007
|
// lives inside the resolver. It used to pass the env-only
|
|
1655
2008
|
// reader, so a user's config.json autoCompact worked on
|
|
1656
2009
|
// `kiso chat` and silently vanished on the default entry.
|
|
1657
|
-
await chatLoop(agent, id, input,
|
|
2010
|
+
await chatLoop(agent, id, input, retiredAutoCompact(mergedConfig));
|
|
1658
2011
|
break;
|
|
1659
2012
|
}
|
|
1660
2013
|
}
|