@lazyingart/agintiflow 0.20.42 → 0.20.44

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 CHANGED
@@ -138,8 +138,11 @@ aginti update
138
138
  aginti sessions list
139
139
  aginti sessions show <session-id>
140
140
  aginti sessions rename <session-id> "friendly title"
141
+ aginti --remove-empty-sessions
142
+ aginti --remove-sessions
141
143
  aginti storage migrate
142
144
  aginti resume
145
+ aginti resume --all-sessions
143
146
  aginti resume latest
144
147
  aginti resume <session-id> "continue with a short follow-up"
145
148
  aginti queue <session-id> "extra instruction for the running agent"
@@ -148,6 +151,10 @@ aginti --latex "draw a figure, write a short LaTeX report, and compile the PDF"
148
151
  aginti "set up this project and run the tests"
149
152
  ```
150
153
 
154
+ Bare `aginti resume` lists sessions for the current cwd by default. Use `--all-sessions` to browse the global session index; in the interactive selector, type a number to resume, `q` to quit, `/text` to filter the visible list, or `/` to clear the filter.
155
+
156
+ Session cleanup is cwd-scoped by default. `aginti --remove-empty-sessions` shows only empty sessions and preselects them; `aginti --remove-sessions` shows all cwd sessions with nothing preselected. The cleanup selector uses Space to select, Up/Down to move, Tab to switch to OK/Cancel, and a second Yes/Cancel confirmation before deleting the project pointer and central `~/.agintiflow/sessions/<session-id>` data.
157
+
151
158
  Run from a source checkout:
152
159
 
153
160
  ```bash
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyingart/agintiflow",
3
- "version": "0.20.42",
3
+ "version": "0.20.44",
4
4
  "type": "module",
5
5
  "description": "AgInTiFlow is a web-first coding agent and CLI with DeepSeek routing, sandboxed tools, model providers, canvas artifacts, and optional wrappers.",
6
6
  "license": "Apache-2.0",
@@ -235,6 +235,11 @@ try {
235
235
  if (classifyEscapeAction({ active: true, pendingAsap: [] }) !== "abort") {
236
236
  throw new Error("active Esc should abort when no ASAP pipe messages are pending");
237
237
  }
238
+ await runChat("/exit\n");
239
+ const idleSessionEntries = await fs.readdir(path.join(agintiflowHome, "sessions"), { withFileTypes: true }).catch(() => []);
240
+ if (idleSessionEntries.some((entry) => entry.isDirectory())) {
241
+ throw new Error("idle interactive chat created a session before any user task");
242
+ }
238
243
  if (
239
244
  canonicalSlashPromptBuffer("/ve") !== "/venice" ||
240
245
  canonicalSlashPromptBuffer("/v") !== "/venice" ||
@@ -2,7 +2,13 @@
2
2
  import fs from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
- import { ensureProjectSessionStorage, listProjectSessions } from "../src/project.js";
5
+ import {
6
+ ensureProjectSessionStorage,
7
+ listProjectSessionRemovalCandidates,
8
+ listProjectSessions,
9
+ removeProjectSessions,
10
+ sessionStoreOptions,
11
+ } from "../src/project.js";
6
12
  import { SessionStore } from "../src/session-store.js";
7
13
 
8
14
  const tempRoot = await fs.mkdtemp(path.join(os.tmpdir(), "agintiflow-inbox-"));
@@ -58,6 +64,67 @@ try {
58
64
  "project session pointer was not created"
59
65
  );
60
66
 
67
+ const nestedCwd = path.join(legacyProject, "nested");
68
+ await fs.mkdir(nestedCwd, { recursive: true });
69
+ const otherCwdStore = new SessionStore(paths.globalSessionsDir, "other-cwd-smoke", sessionStoreOptions(legacyProject, "other-cwd-smoke"));
70
+ await otherCwdStore.saveState({
71
+ sessionId: "other-cwd-smoke",
72
+ createdAt: "2026-01-01T00:02:00.000Z",
73
+ updatedAt: "2026-01-01T00:03:00.000Z",
74
+ provider: "mock",
75
+ model: "mock-agent",
76
+ goal: "nested cwd smoke",
77
+ projectRoot: legacyProject,
78
+ commandCwd: nestedCwd,
79
+ chat: [],
80
+ });
81
+ const cwdFiltered = await listProjectSessions(legacyProject, { limit: 10, commandCwd: legacyProject });
82
+ assert(!cwdFiltered.some((session) => session.sessionId === "other-cwd-smoke"), "default cwd filtering included a different cwd session");
83
+ const allSessions = await listProjectSessions(legacyProject, { limit: 10, allSessions: true });
84
+ assert(allSessions.some((session) => session.sessionId === "other-cwd-smoke"), "--all-sessions mode did not include a different cwd session");
85
+
86
+ const emptyStore = new SessionStore(paths.globalSessionsDir, "empty-session-smoke", sessionStoreOptions(legacyProject, "empty-session-smoke"));
87
+ await emptyStore.saveState({
88
+ sessionId: "empty-session-smoke",
89
+ createdAt: "2026-01-01T00:04:00.000Z",
90
+ updatedAt: "2026-01-01T00:04:00.000Z",
91
+ provider: "mock",
92
+ model: "mock-agent",
93
+ projectRoot: legacyProject,
94
+ commandCwd: legacyProject,
95
+ chat: [],
96
+ stepsCompleted: 0,
97
+ });
98
+ const nonEmptyStore = new SessionStore(paths.globalSessionsDir, "nonempty-session-smoke", sessionStoreOptions(legacyProject, "nonempty-session-smoke"));
99
+ await nonEmptyStore.saveState({
100
+ sessionId: "nonempty-session-smoke",
101
+ createdAt: "2026-01-01T00:05:00.000Z",
102
+ updatedAt: "2026-01-01T00:05:00.000Z",
103
+ provider: "mock",
104
+ model: "mock-agent",
105
+ goal: "keep this non-empty session",
106
+ projectRoot: legacyProject,
107
+ commandCwd: legacyProject,
108
+ chat: [{ role: "user", content: "hello" }],
109
+ stepsCompleted: 1,
110
+ });
111
+ const removalCandidates = await listProjectSessionRemovalCandidates(legacyProject, { limit: 20, commandCwd: legacyProject });
112
+ assert(removalCandidates.find((session) => session.sessionId === "empty-session-smoke")?.isEmpty, "empty session was not detected");
113
+ assert(removalCandidates.find((session) => session.sessionId === "nonempty-session-smoke")?.isEmpty === false, "non-empty session was classified as empty");
114
+ const emptyOnly = await listProjectSessionRemovalCandidates(legacyProject, { limit: 20, commandCwd: legacyProject, emptyOnly: true });
115
+ assert(emptyOnly.some((session) => session.sessionId === "empty-session-smoke"), "empty-only removal list omitted the empty session");
116
+ assert(!emptyOnly.some((session) => session.sessionId === "nonempty-session-smoke"), "empty-only removal list included a non-empty session");
117
+ const removed = await removeProjectSessions(legacyProject, ["empty-session-smoke"]);
118
+ assert(removed.removed.length === 1, "empty session removal did not report one removed session");
119
+ assert(
120
+ !(await fs.stat(path.join(paths.globalSessionsDir, "empty-session-smoke", "state.json")).then((stat) => stat.isFile()).catch(() => false)),
121
+ "empty session global state was not removed"
122
+ );
123
+ assert(
124
+ !(await fs.stat(path.join(paths.sessionsDir, "empty-session-smoke", "session.json")).then((stat) => stat.isFile()).catch(() => false)),
125
+ "empty session project pointer was not removed"
126
+ );
127
+
61
128
  console.log(
62
129
  JSON.stringify(
63
130
  {
@@ -68,6 +135,10 @@ try {
68
135
  "session-inbox-asap-priority",
69
136
  "legacy-session-migration",
70
137
  "global-session-store",
138
+ "cwd-session-filter",
139
+ "all-sessions-list",
140
+ "empty-session-detection",
141
+ "empty-session-removal",
71
142
  ],
72
143
  },
73
144
  null,
package/src/cli.js CHANGED
@@ -17,8 +17,10 @@ import {
17
17
  doctorReport,
18
18
  ensureProjectSessionStorage,
19
19
  initProject,
20
+ listProjectSessionRemovalCandidates,
20
21
  listProjectSessions,
21
22
  renameProjectSession,
23
+ removeProjectSessions,
22
24
  providerKeyStatus,
23
25
  setProviderKey,
24
26
  showProjectSession,
@@ -34,6 +36,7 @@ import fs from "node:fs/promises";
34
36
  import path from "node:path";
35
37
  import { fileURLToPath } from "node:url";
36
38
  import readline from "node:readline/promises";
39
+ import * as readlineRaw from "node:readline";
37
40
 
38
41
  const packageDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
39
42
  const packageJson = JSON.parse(await fs.readFile(path.join(packageDir, "package.json"), "utf8"));
@@ -378,7 +381,7 @@ export function parseArgs(argv) {
378
381
 
379
382
  function printUsage() {
380
383
  console.log(
381
- 'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti update OR aginti models OR aginti skills [query] OR aginti auth [deepseek|openai|qwen|venice|grsai] OR aginti resume [latest|<session-id>] ["prompt"] OR aginti queue <session-id> "message" OR aginti [--no-auto-update] [--language en|ja|zh-Hans|zh-Hant|ko|fr|es|ar|vi|de|ru] [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|qwen|venice|mock] [--model MODEL] [--route-model MODEL] [--main-model MODEL] [--spare-model MODEL --spare-reasoning medium] [--aux-provider grsai|venice --aux-model MODEL] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex --wrapper-model gpt-5.5] [--list-models|--list-routes] "your task"'
384
+ 'Usage: aginti [chat] OR aginti web [--port 3210] OR aginti update OR aginti models OR aginti skills [query] OR aginti auth [deepseek|openai|qwen|venice|grsai] OR aginti resume [--all-sessions] [latest|<session-id>] ["prompt"] OR aginti --remove-empty-sessions OR aginti --remove-sessions OR aginti queue <session-id> "message" OR aginti [--no-auto-update] [--language en|ja|zh-Hans|zh-Hant|ko|fr|es|ar|vi|de|ru] [--image] [--latex] [--routing smart|fast|complex|manual] [--provider deepseek|openai|qwen|venice|mock] [--model MODEL] [--route-model MODEL] [--main-model MODEL] [--spare-model MODEL --spare-reasoning medium] [--aux-provider grsai|venice --aux-model MODEL] [--sandbox-mode host|docker-readonly|docker-workspace] [--package-install-policy block|prompt|allow] [--approve-package-installs] [--allow-shell|--no-shell] [--allow-file-tools|--no-file-tools] [--web-search|--no-web-search] [--parallel-scouts|--no-parallel-scouts --scout-count 1..10] [--allow-auxiliary-tools|--no-auxiliary-tools] [--allow-wrappers --wrapper codex --wrapper-model gpt-5.5] [--list-models|--list-routes] "your task"'
382
385
  );
383
386
  console.log(`Languages: ${["en", "ja", "zh-Hans", "zh-Hant", "ko", "fr", "es", "ar", "vi", "de", "ru"].map((code) => `${code}=${languageLabel(code)}`).join(", ")}`);
384
387
  }
@@ -600,12 +603,219 @@ function printAuthWizardResult(result) {
600
603
  }
601
604
  }
602
605
 
606
+ const removeSessionAnsi = {
607
+ reset: "\x1b[0m",
608
+ bold: "\x1b[1m",
609
+ dim: "\x1b[2m",
610
+ inverse: "\x1b[7m",
611
+ };
612
+
613
+ function removeSessionColor(text, ...codes) {
614
+ return `${codes.join("")}${text}${removeSessionAnsi.reset}`;
615
+ }
616
+
617
+ function stripAnsi(text) {
618
+ return String(text || "").replace(/\x1b\[[0-9;]*m/g, "");
619
+ }
620
+
621
+ function ellipsize(text, width) {
622
+ const value = String(text || "").replace(/\s+/g, " ").trim();
623
+ if (value.length <= width) return value.padEnd(width, " ");
624
+ return `${value.slice(0, Math.max(0, width - 1))}…`;
625
+ }
626
+
627
+ function buttonLabel(label, focused, disabled = false) {
628
+ const text = ` ${label} `;
629
+ if (disabled) return removeSessionColor(text, removeSessionAnsi.dim);
630
+ return focused ? removeSessionColor(text, removeSessionAnsi.inverse, removeSessionAnsi.bold) : text;
631
+ }
632
+
633
+ function renderSessionRemovalWizard(state) {
634
+ const width = Math.min(Math.max(process.stdout.columns || 100, 80), 128);
635
+ const rows = process.stdout.rows || 28;
636
+ const bodyWidth = width - 4;
637
+ const visibleCount = Math.max(5, Math.min(state.items.length || 1, rows - 11));
638
+ if (state.cursor < state.scroll) state.scroll = state.cursor;
639
+ if (state.cursor >= state.scroll + visibleCount) state.scroll = state.cursor - visibleCount + 1;
640
+ const shown = state.items.slice(state.scroll, state.scroll + visibleCount);
641
+ const selectedCount = state.selected.size;
642
+ const border = "─".repeat(width - 2);
643
+ const line = (content = "") => {
644
+ const value = String(content || "");
645
+ const clipped = stripAnsi(value).length > bodyWidth ? ellipsize(stripAnsi(value), bodyWidth) : value;
646
+ return `│ ${clipped}${" ".repeat(Math.max(0, bodyWidth - stripAnsi(clipped).length))} │`;
647
+ };
648
+ const listRows = shown.map((session, offset) => {
649
+ const index = state.scroll + offset;
650
+ const checked = state.selected.has(session.sessionId) ? "[x]" : "[ ]";
651
+ const cursor = index === state.cursor ? ">" : " ";
652
+ const badge = session.isEmpty ? "empty" : "work";
653
+ const title = session.title || session.goal || "(no title)";
654
+ const meta = `${session.provider || "unknown"}/${session.model || "unknown"} chat=${session.chatCount} steps=${session.stepsCompleted} files=${session.artifactFileCount}`;
655
+ const row = `${cursor} ${checked} ${badge.padEnd(5)} ${session.sessionId} ${meta} ${title}`;
656
+ const clipped = ellipsize(row, bodyWidth);
657
+ return state.focus === "list" && index === state.cursor ? line(removeSessionColor(clipped, removeSessionAnsi.inverse)) : line(clipped);
658
+ });
659
+ const footer =
660
+ state.phase === "confirm"
661
+ ? `Confirm deletion: ${buttonLabel("Yes, delete", state.confirmFocus === "yes")} ${buttonLabel("Cancel", state.confirmFocus === "cancel")}`
662
+ : `Actions: ${buttonLabel(`OK delete ${selectedCount}`, state.focus === "ok", selectedCount === 0)} ${buttonLabel("Cancel", state.focus === "cancel")}`;
663
+ const guidance =
664
+ state.phase === "confirm"
665
+ ? "Left/Right switches choice. Enter confirms. Esc/q cancels."
666
+ : "Space toggles. Up/Down moves. Tab changes focus. Enter opens confirm. Esc/q cancels.";
667
+ const lines = [
668
+ `╭${border}╮`,
669
+ line(state.title),
670
+ line(state.subtitle),
671
+ line(`Showing ${state.items.length === 0 ? 0 : state.scroll + 1}-${Math.min(state.items.length, state.scroll + visibleCount)} of ${state.items.length}; selected ${selectedCount}`),
672
+ `├${border}┤`,
673
+ ...listRows,
674
+ `├${border}┤`,
675
+ line(footer),
676
+ line(state.message || guidance),
677
+ `╰${border}╯`,
678
+ ];
679
+ process.stdout.write(`\x1b[H\x1b[2J${lines.join("\n")}`);
680
+ }
681
+
682
+ async function promptRemoveSessions(candidates, { defaultSelectedIds = [], title = "Remove sessions", subtitle = "" } = {}) {
683
+ if (!process.stdin.isTTY || !process.stdout.isTTY || typeof process.stdin.setRawMode !== "function") {
684
+ console.log("Interactive terminal required; no sessions removed.");
685
+ return null;
686
+ }
687
+ const state = {
688
+ items: candidates,
689
+ selected: new Set(defaultSelectedIds),
690
+ cursor: 0,
691
+ scroll: 0,
692
+ focus: "list",
693
+ phase: "select",
694
+ confirmFocus: "cancel",
695
+ message: "",
696
+ title,
697
+ subtitle,
698
+ };
699
+ return await new Promise((resolve) => {
700
+ const input = process.stdin;
701
+ const output = process.stdout;
702
+ const cleanup = (value) => {
703
+ input.off("keypress", onKeypress);
704
+ if (input.isTTY) input.setRawMode(false);
705
+ input.pause();
706
+ output.write("\x1b[?25h\x1b[?1049l");
707
+ resolve(value);
708
+ };
709
+ const moveCursor = (delta) => {
710
+ state.focus = "list";
711
+ state.cursor = Math.min(Math.max(state.cursor + delta, 0), Math.max(0, state.items.length - 1));
712
+ state.message = "";
713
+ };
714
+ const toggleCurrent = () => {
715
+ const session = state.items[state.cursor];
716
+ if (!session) return;
717
+ if (state.selected.has(session.sessionId)) state.selected.delete(session.sessionId);
718
+ else state.selected.add(session.sessionId);
719
+ state.message = `${state.selected.size} session(s) selected.`;
720
+ };
721
+ const openConfirm = () => {
722
+ if (state.selected.size === 0) {
723
+ state.message = "Select at least one session before confirming.";
724
+ return;
725
+ }
726
+ state.phase = "confirm";
727
+ state.confirmFocus = "cancel";
728
+ state.message = "Second confirmation required before deleting session data.";
729
+ };
730
+ function onKeypress(char, key = {}) {
731
+ if (key.ctrl && key.name === "c") return cleanup(null);
732
+ const name = key.name || char;
733
+ if (name === "escape" || name === "q") return cleanup(null);
734
+ if (state.phase === "confirm") {
735
+ if (name === "left" || name === "right" || name === "tab") state.confirmFocus = state.confirmFocus === "yes" ? "cancel" : "yes";
736
+ else if (name === "return" || name === "enter") return cleanup(state.confirmFocus === "yes" ? [...state.selected] : null);
737
+ renderSessionRemovalWizard(state);
738
+ return;
739
+ }
740
+ if (name === "up") moveCursor(-1);
741
+ else if (name === "down") moveCursor(1);
742
+ else if (name === "pageup") moveCursor(-8);
743
+ else if (name === "pagedown") moveCursor(8);
744
+ else if (name === "space") toggleCurrent();
745
+ else if (name === "tab") state.focus = state.focus === "list" ? "ok" : state.focus === "ok" ? "cancel" : "list";
746
+ else if (name === "left" || name === "right") state.focus = state.focus === "cancel" ? "ok" : "cancel";
747
+ else if (name === "return" || name === "enter") {
748
+ if (state.focus === "cancel") return cleanup(null);
749
+ openConfirm();
750
+ }
751
+ renderSessionRemovalWizard(state);
752
+ }
753
+ readlineRaw.emitKeypressEvents(input);
754
+ input.setRawMode(true);
755
+ input.resume();
756
+ output.write("\x1b[?1049h\x1b[?25l");
757
+ input.on("keypress", onKeypress);
758
+ renderSessionRemovalWizard(state);
759
+ });
760
+ }
761
+
762
+ function printRemovalPreview(candidates) {
763
+ for (const session of candidates) {
764
+ const title = session.title || session.goal || "(no title)";
765
+ const status = session.isEmpty ? "empty" : "work";
766
+ console.log(`${status.padEnd(5)} ${session.sessionId} ${session.updatedAt || ""} ${title.slice(0, 90)}`);
767
+ }
768
+ }
769
+
770
+ async function handleRemoveSessionsCommand({ emptyOnly = false } = {}) {
771
+ const candidates = await listProjectSessionRemovalCandidates(process.cwd(), {
772
+ limit: 1000,
773
+ emptyOnly,
774
+ });
775
+ if (candidates.length === 0) {
776
+ console.log(emptyOnly ? "No empty sessions found for this cwd." : "No sessions found for this cwd.");
777
+ return;
778
+ }
779
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
780
+ printRemovalPreview(candidates);
781
+ console.log("No sessions removed because this command needs an interactive terminal.");
782
+ return;
783
+ }
784
+ const defaultSelectedIds = emptyOnly ? candidates.map((session) => session.sessionId) : [];
785
+ const selected = await promptRemoveSessions(candidates, {
786
+ defaultSelectedIds,
787
+ title: emptyOnly ? "Remove empty AgInTiFlow sessions in this cwd" : "Remove AgInTiFlow sessions in this cwd",
788
+ subtitle: emptyOnly
789
+ ? "Only empty sessions are shown and selected by default."
790
+ : "All cwd sessions are shown; nothing is selected by default.",
791
+ });
792
+ if (!selected || selected.length === 0) {
793
+ console.log("No sessions removed.");
794
+ return;
795
+ }
796
+ const allowedIds = new Set(candidates.map((session) => session.sessionId));
797
+ const safeSelected = selected.filter((sessionId) => allowedIds.has(sessionId));
798
+ const result = await removeProjectSessions(process.cwd(), safeSelected);
799
+ console.log(`Removed ${result.removed.length} session(s) from this cwd:`);
800
+ for (const item of result.removed) console.log(`- ${item.sessionId}`);
801
+ }
802
+
603
803
  async function handleSessionsCommand(argv) {
604
- const [verb = "list", sessionId = "", ...rest] = argv;
804
+ const normalizedArgv = argv[0] === "--all-sessions" ? ["list", ...argv] : argv;
805
+ const [verb = "list", sessionId = "", ...rest] = normalizedArgv;
806
+ if (verb === "remove-empty" || verb === "delete-empty") {
807
+ await handleRemoveSessionsCommand({ emptyOnly: true });
808
+ return;
809
+ }
810
+ if (verb === "remove" || verb === "delete") {
811
+ await handleRemoveSessionsCommand({ emptyOnly: false });
812
+ return;
813
+ }
605
814
  if (verb === "list") {
606
- const sessions = await listProjectSessions(process.cwd(), 80);
815
+ const allSessions = normalizedArgv.includes("--all-sessions");
816
+ const sessions = await listProjectSessions(process.cwd(), { limit: 80, allSessions });
607
817
  if (sessions.length === 0) {
608
- console.log("No project-local sessions found.");
818
+ console.log(allSessions ? "No sessions found." : "No sessions found for this cwd.");
609
819
  return;
610
820
  }
611
821
  for (const session of sessions) {
@@ -636,39 +846,86 @@ async function handleSessionsCommand(argv) {
636
846
  return;
637
847
  }
638
848
 
639
- console.error('Usage: aginti sessions list OR aginti sessions show <session-id> OR aginti sessions rename <session-id> "title"');
849
+ console.error('Usage: aginti sessions list OR aginti sessions show <session-id> OR aginti sessions rename <session-id> "title" OR aginti sessions remove-empty OR aginti sessions remove');
640
850
  process.exit(1);
641
851
  }
642
852
 
643
- async function promptSelectSession(sessions) {
644
- if (!process.stdin.isTTY || !process.stdout.isTTY) return sessions[0]?.sessionId || "";
645
- console.log("Select a session to resume:");
646
- sessions.slice(0, 20).forEach((session, index) => {
853
+ function sessionSearchText(session) {
854
+ return [
855
+ session.sessionId,
856
+ session.provider,
857
+ session.model,
858
+ session.updatedAt,
859
+ session.title,
860
+ session.goal,
861
+ session.projectRoot,
862
+ session.commandCwd,
863
+ ]
864
+ .filter(Boolean)
865
+ .join(" ")
866
+ .toLowerCase();
867
+ }
868
+
869
+ function filterSessions(sessions, filterText = "") {
870
+ const needle = String(filterText || "").trim().toLowerCase();
871
+ if (!needle) return sessions;
872
+ return sessions.filter((session) => sessionSearchText(session).includes(needle));
873
+ }
874
+
875
+ function printSessionChoices(sessions, { filterText = "", allSessions = false, maxShown = 20 } = {}) {
876
+ const scope = allSessions ? "all sessions" : `cwd ${process.cwd()}`;
877
+ const shown = sessions.slice(0, maxShown);
878
+ console.log(`Select a session to resume (${scope}${filterText ? `, filter="${filterText}"` : ""}):`);
879
+ if (shown.length === 0) {
880
+ console.log("No matching sessions. Type /text to change the filter, / to clear it, or q to quit.");
881
+ return;
882
+ }
883
+ shown.forEach((session, index) => {
647
884
  const title = session.title || session.goal || "(untitled)";
648
885
  console.log(
649
886
  `${index + 1}. ${session.sessionId} ${session.provider || "unknown"}/${session.model || "unknown"} ${session.updatedAt || ""} ${title.slice(0, 90)}`
650
887
  );
651
888
  });
889
+ if (sessions.length > shown.length) console.log(`... ${sessions.length - shown.length} more hidden by display limit; type /text to narrow.`);
890
+ console.log("Type a number to select, /text to filter, / to clear, or q to quit.");
891
+ }
892
+
893
+ async function promptSelectSession(sessions, { allSessions = false } = {}) {
894
+ if (!process.stdin.isTTY || !process.stdout.isTTY) return sessions[0]?.sessionId || "";
652
895
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
896
+ let filterText = "";
653
897
  try {
654
- const answer = await rl.question("Session number: ");
655
- const index = Number(answer.trim()) - 1;
656
- return sessions[index]?.sessionId || "";
898
+ while (true) {
899
+ const filtered = filterSessions(sessions, filterText);
900
+ printSessionChoices(filtered, { filterText, allSessions });
901
+ const answer = (await rl.question("Session number/filter: ")).trim();
902
+ if (!answer || answer.toLowerCase() === "q" || answer.toLowerCase() === "quit") return "";
903
+ if (answer.startsWith("/")) {
904
+ filterText = answer.slice(1).trim();
905
+ continue;
906
+ }
907
+ const index = Number(answer) - 1;
908
+ if (Number.isInteger(index) && index >= 0 && index < Math.min(filtered.length, 20)) {
909
+ return filtered[index]?.sessionId || "";
910
+ }
911
+ console.log("Invalid selection. Type a shown number, /text to filter, or q to quit.");
912
+ }
657
913
  } finally {
658
914
  rl.close();
659
915
  }
660
916
  }
661
917
 
662
- async function resolveResumeSessionId(sessionId) {
918
+ async function resolveResumeSessionId(sessionId, { allSessions = false } = {}) {
663
919
  if (sessionId && sessionId !== "latest") return sessionId;
664
- const sessions = await listProjectSessions(process.cwd(), 50);
920
+ const sessions = await listProjectSessions(process.cwd(), { limit: 1000, allSessions });
665
921
  if (sessionId === "latest" || sessions.length <= 1 || !process.stdin.isTTY || !process.stdout.isTTY) {
666
922
  if (sessions[0]?.sessionId) return sessions[0].sessionId;
667
923
  } else {
668
- const selected = await promptSelectSession(sessions);
924
+ const selected = await promptSelectSession(sessions, { allSessions });
669
925
  if (selected) return selected;
926
+ return "";
670
927
  }
671
- throw new Error("No project-local sessions found. Run `aginti sessions list` to check this folder.");
928
+ throw new Error(allSessions ? "No sessions found." : "No sessions found for this cwd. Use `aginti resume --all-sessions` to browse all sessions.");
672
929
  }
673
930
 
674
931
  async function handleQueueCommand(argv) {
@@ -739,6 +996,16 @@ export async function main(argv = process.argv.slice(2)) {
739
996
  });
740
997
  if (autoUpdateResult.restarted) process.exit(autoUpdateResult.exitCode ?? 0);
741
998
 
999
+ if (argv.includes("--remove-empty-sessions") || argv[0] === "remove-empty-sessions") {
1000
+ await handleRemoveSessionsCommand({ emptyOnly: true });
1001
+ return;
1002
+ }
1003
+
1004
+ if (argv.includes("--remove-sessions") || argv[0] === "remove-sessions") {
1005
+ await handleRemoveSessionsCommand({ emptyOnly: false });
1006
+ return;
1007
+ }
1008
+
742
1009
  if (argv[0] === "init") {
743
1010
  printInitResult(await initProject(process.cwd()));
744
1011
  return;
@@ -837,14 +1104,18 @@ export async function main(argv = process.argv.slice(2)) {
837
1104
  }
838
1105
 
839
1106
  if (argv[0] === "resume") {
840
- let sessionId = argv[1] || "";
841
- const prompt = argv.slice(2).join(" ").trim();
1107
+ const resumeArgv = argv.slice(1);
1108
+ const allSessions = resumeArgv.includes("--all-sessions");
1109
+ const positional = resumeArgv.filter((arg) => arg !== "--all-sessions");
1110
+ let sessionId = positional[0] || "";
1111
+ const prompt = positional.slice(1).join(" ").trim();
842
1112
  try {
843
- sessionId = await resolveResumeSessionId(sessionId);
1113
+ sessionId = await resolveResumeSessionId(sessionId, { allSessions });
844
1114
  } catch (error) {
845
1115
  console.error(error instanceof Error ? error.message : String(error));
846
1116
  process.exit(1);
847
1117
  }
1118
+ if (!sessionId) return;
848
1119
  if (!prompt) {
849
1120
  await startInteractiveCli(agentDefaults({ ...parseArgs([]), resume: sessionId }), {
850
1121
  packageDir,
package/src/project.js CHANGED
@@ -9,6 +9,7 @@ import { platformInfo, platformLabel, platformSetupHints } from "./platform.js";
9
9
  import {
10
10
  LEGACY_PROJECT_SESSIONS_DIR_NAME,
11
11
  PROJECT_SESSIONS_DIR_NAME,
12
+ deleteSessionIndex,
12
13
  globalSessionPaths,
13
14
  isSafeSessionId,
14
15
  listSessionIndex,
@@ -484,11 +485,34 @@ export async function setProviderKey(projectRoot, provider, value) {
484
485
  };
485
486
  }
486
487
 
487
- export async function listProjectSessions(projectRoot = process.cwd(), limit = 50) {
488
+ function normalizeSessionListOptions(projectRoot, limitOrOptions = 50) {
489
+ const options = typeof limitOrOptions === "object" && limitOrOptions !== null ? limitOrOptions : { limit: limitOrOptions };
490
+ const root = resolveProjectRoot(projectRoot);
491
+ const commandCwd = options.commandCwd === false ? "" : path.resolve(options.commandCwd || root);
492
+ return {
493
+ limit: Math.min(Math.max(Number(options.limit) || 50, 1), 1000),
494
+ commandCwd,
495
+ allSessions: Boolean(options.allSessions),
496
+ };
497
+ }
498
+
499
+ function sessionMatchesCommandCwd(session, commandCwd = "") {
500
+ if (!commandCwd) return true;
501
+ const value = session.commandCwd || session.projectRoot || "";
502
+ if (!value) return false;
503
+ return path.resolve(value) === path.resolve(commandCwd);
504
+ }
505
+
506
+ export async function listProjectSessions(projectRoot = process.cwd(), limitOrOptions = 50) {
507
+ const options = normalizeSessionListOptions(projectRoot, limitOrOptions);
488
508
  const paths = await ensureProjectSessionStorage(projectRoot);
489
509
  const indexed = (() => {
490
510
  try {
491
- return listSessionIndex({ projectRoot: paths.root, limit: Math.max(limit, 100) });
511
+ return listSessionIndex({
512
+ projectRoot: options.allSessions ? "" : paths.root,
513
+ commandCwd: options.allSessions ? "" : options.commandCwd,
514
+ limit: Math.max(options.limit, 100),
515
+ });
492
516
  } catch {
493
517
  return [];
494
518
  }
@@ -521,6 +545,7 @@ export async function listProjectSessions(projectRoot = process.cwd(), limit = 5
521
545
  updatedAt: state?.updatedAt || pointer.updatedAt || byId.get(sessionId)?.updatedAt || state?.createdAt || "",
522
546
  stepsCompleted: state?.stepsCompleted || 0,
523
547
  };
548
+ if (!options.allSessions && !sessionMatchesCommandCwd(record, options.commandCwd)) continue;
524
549
  byId.set(sessionId, record);
525
550
  try {
526
551
  upsertSessionIndex({
@@ -534,7 +559,119 @@ export async function listProjectSessions(projectRoot = process.cwd(), limit = 5
534
559
  }
535
560
 
536
561
  const sessions = [...byId.values()];
537
- return sessions.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt))).slice(0, limit);
562
+ return sessions.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt))).slice(0, options.limit);
563
+ }
564
+
565
+ async function readJsonFile(filePath, fallback = {}) {
566
+ try {
567
+ return JSON.parse(await fsp.readFile(filePath, "utf8"));
568
+ } catch {
569
+ return fallback;
570
+ }
571
+ }
572
+
573
+ async function readJsonLines(filePath) {
574
+ try {
575
+ const raw = await fsp.readFile(filePath, "utf8");
576
+ return raw
577
+ .split("\n")
578
+ .map((line) => line.trim())
579
+ .filter(Boolean)
580
+ .map((line) => JSON.parse(line));
581
+ } catch {
582
+ return [];
583
+ }
584
+ }
585
+
586
+ async function countFilesRecursive(dirPath, limit = 200) {
587
+ let count = 0;
588
+ async function walk(current) {
589
+ if (count >= limit) return;
590
+ const entries = await fsp.readdir(current, { withFileTypes: true }).catch(() => []);
591
+ for (const entry of entries) {
592
+ if (count >= limit) return;
593
+ const child = path.join(current, entry.name);
594
+ if (entry.isDirectory()) await walk(child);
595
+ else if (entry.isFile()) count += 1;
596
+ }
597
+ }
598
+ await walk(dirPath);
599
+ return count;
600
+ }
601
+
602
+ function isMeaningfulSessionEvent(event = {}) {
603
+ const type = String(event.type || "");
604
+ if (!type) return false;
605
+ return /^(agent|assistant|browser|canvas|conversation|file|image|model|patch|plan|run|shell|tool|workspace|write)[.:_-]/.test(type);
606
+ }
607
+
608
+ export async function listProjectSessionRemovalCandidates(projectRoot = process.cwd(), options = {}) {
609
+ const paths = await ensureProjectSessionStorage(projectRoot);
610
+ const sessions = await listProjectSessions(projectRoot, {
611
+ limit: options.limit || 1000,
612
+ commandCwd: options.commandCwd,
613
+ allSessions: options.allSessions,
614
+ });
615
+ const candidates = [];
616
+ for (const session of sessions) {
617
+ const safeId = String(session.sessionId || "");
618
+ if (!isSafeSessionId(safeId)) continue;
619
+ const pointerPath = path.join(paths.sessionsDir, safeId, "session.json");
620
+ const pointer = await readJsonFile(pointerPath, {});
621
+ const sessionDir = session.sessionDir || pointer.sessionDir || path.join(paths.globalSessionsDir, safeId);
622
+ const state = await readJsonFile(path.join(sessionDir, "state.json"), {});
623
+ const events = await readJsonLines(path.join(sessionDir, "events.jsonl"));
624
+ const chat = Array.isArray(state.chat) ? state.chat : [];
625
+ const goal = String(state.goal || pointer.goal || session.goal || "").trim();
626
+ const title = String(state.title || pointer.title || session.title || "").trim();
627
+ const stepsCompleted = Number(state.stepsCompleted || 0);
628
+ const artifactsDir = state.artifactsDir || pointer.artifactsDir || path.join(sessionDir, "artifacts");
629
+ const artifactFileCount = await countFilesRecursive(artifactsDir);
630
+ const meaningfulEventCount = events.filter(isMeaningfulSessionEvent).length;
631
+ const isEmpty =
632
+ !goal &&
633
+ !title &&
634
+ chat.length === 0 &&
635
+ stepsCompleted === 0 &&
636
+ artifactFileCount === 0 &&
637
+ meaningfulEventCount === 0;
638
+ candidates.push({
639
+ ...session,
640
+ sessionId: safeId,
641
+ sessionDir,
642
+ pointerPath,
643
+ chatCount: chat.length,
644
+ eventCount: events.length,
645
+ meaningfulEventCount,
646
+ artifactFileCount,
647
+ stepsCompleted,
648
+ isEmpty,
649
+ });
650
+ }
651
+ return options.emptyOnly ? candidates.filter((session) => session.isEmpty) : candidates;
652
+ }
653
+
654
+ export async function removeProjectSessions(projectRoot = process.cwd(), sessionIds = []) {
655
+ const paths = await ensureProjectSessionStorage(projectRoot);
656
+ const removed = [];
657
+ for (const rawId of sessionIds) {
658
+ const safeId = String(rawId || "");
659
+ if (!isSafeSessionId(safeId)) throw new Error(`Invalid session id: ${rawId}`);
660
+ const pointerDir = path.join(paths.sessionsDir, safeId);
661
+ const pointer = await readJsonFile(path.join(pointerDir, "session.json"), {});
662
+ const sessionDir = pointer.sessionDir || path.join(paths.globalSessionsDir, safeId);
663
+ const legacyDir = path.join(paths.legacySessionsDir, safeId);
664
+ await fsp.rm(sessionDir, { recursive: true, force: true });
665
+ await fsp.rm(pointerDir, { recursive: true, force: true });
666
+ await fsp.rm(legacyDir, { recursive: true, force: true }).catch(() => {});
667
+ try {
668
+ deleteSessionIndex(safeId);
669
+ } catch {
670
+ // The on-disk session and pointer are already removed; stale index cleanup can be retried later.
671
+ }
672
+ removed.push({ sessionId: safeId, sessionDir, pointerDir });
673
+ }
674
+ return { ok: true, removed };
538
675
  }
539
676
 
540
677
  export async function showProjectSession(projectRoot, sessionId) {
@@ -120,16 +120,26 @@ export function deleteSessionIndex(sessionId) {
120
120
  return result.changes > 0;
121
121
  }
122
122
 
123
- export function listSessionIndex({ projectRoot = "", limit = 100 } = {}) {
123
+ export function listSessionIndex({ projectRoot = "", commandCwd = "", limit = 100 } = {}) {
124
124
  const db = ensureIndexDb();
125
125
  const maxRows = Math.min(Math.max(Number(limit) || 100, 1), 1000);
126
126
  const columns = `session_id AS sessionId, project_root AS projectRoot, command_cwd AS commandCwd, project_sessions_dir AS projectSessionsDir,
127
127
  session_dir AS sessionDir, provider, model, goal, title, status,
128
128
  created_at AS createdAt, updated_at AS updatedAt, ended_at AS endedAt, result, error`;
129
+ const clauses = [];
130
+ const params = [];
129
131
  if (projectRoot) {
132
+ clauses.push("project_root = ?");
133
+ params.push(path.resolve(projectRoot));
134
+ }
135
+ if (commandCwd) {
136
+ clauses.push("command_cwd = ?");
137
+ params.push(path.resolve(commandCwd));
138
+ }
139
+ if (clauses.length > 0) {
130
140
  return db
131
- .prepare(`SELECT ${columns} FROM sessions WHERE project_root = ? ORDER BY updated_at DESC LIMIT ?`)
132
- .all(path.resolve(projectRoot), maxRows);
141
+ .prepare(`SELECT ${columns} FROM sessions WHERE ${clauses.join(" AND ")} ORDER BY updated_at DESC LIMIT ?`)
142
+ .all(...params, maxRows);
133
143
  }
134
144
  return db.prepare(`SELECT ${columns} FROM sessions ORDER BY updated_at DESC LIMIT ?`).all(maxRows);
135
145
  }