@vascend/talos 0.1.4 → 0.1.5

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.
Files changed (55) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +37 -12
  3. package/dist/core/agent.js +58 -38
  4. package/dist/core/session-naming.d.ts +3 -0
  5. package/dist/core/session-naming.js +43 -0
  6. package/dist/core/session-usage.js +4 -1
  7. package/dist/core/session.d.ts +1 -0
  8. package/dist/core/session.js +9 -3
  9. package/dist/core/storage-health.d.ts +24 -0
  10. package/dist/core/storage-health.js +72 -0
  11. package/dist/core/types.d.ts +17 -0
  12. package/dist/tui/agent-sessions-screen.d.ts +25 -0
  13. package/dist/tui/agent-sessions-screen.js +132 -0
  14. package/dist/tui/agent-sessions.d.ts +39 -0
  15. package/dist/tui/agent-sessions.js +175 -0
  16. package/dist/tui/bottom-input.d.ts +11 -0
  17. package/dist/tui/bottom-input.js +106 -10
  18. package/dist/tui/commands.js +2 -1
  19. package/dist/tui/i18n-catalog.js +39 -1
  20. package/dist/tui/logo.js +3 -0
  21. package/dist/tui/model-picker.d.ts +2 -0
  22. package/dist/tui/model-picker.js +49 -0
  23. package/dist/tui/native-scrollback.d.ts +2 -1
  24. package/dist/tui/native-scrollback.js +26 -6
  25. package/dist/tui/prompt-queue-store.d.ts +1 -0
  26. package/dist/tui/prompt-queue-store.js +4 -0
  27. package/dist/tui/queued-questions.js +2 -4
  28. package/dist/tui/quick-help.d.ts +1 -1
  29. package/dist/tui/quick-help.js +3 -0
  30. package/dist/tui/repl.d.ts +3 -0
  31. package/dist/tui/repl.js +110 -24
  32. package/dist/tui/review-store.d.ts +7 -1
  33. package/dist/tui/review-store.js +36 -4
  34. package/dist/tui/scrollback-archive.d.ts +16 -0
  35. package/dist/tui/scrollback-archive.js +90 -0
  36. package/dist/tui/selector.d.ts +4 -0
  37. package/dist/tui/selector.js +9 -2
  38. package/dist/tui/session-naming-notice.d.ts +14 -0
  39. package/dist/tui/session-naming-notice.js +57 -0
  40. package/dist/tui/session-picker.d.ts +2 -0
  41. package/dist/tui/session-picker.js +32 -0
  42. package/dist/tui/storage-warning.d.ts +2 -0
  43. package/dist/tui/storage-warning.js +35 -0
  44. package/dist/vascend/context-feedback-loop.js +2 -0
  45. package/dist/vascend/context-snapshot.d.ts +1 -0
  46. package/dist/vascend/context-snapshot.js +4 -3
  47. package/dist/vascend/context-window-compiler.d.ts +1 -0
  48. package/dist/vascend/context-window-compiler.js +1 -0
  49. package/dist/vascend/granular-kingdom.js +32 -4
  50. package/dist/vascend/kingdom.js +4 -0
  51. package/dist/vascend/memory-store.d.ts +1 -0
  52. package/dist/vascend/memory-store.js +12 -4
  53. package/dist/vascend/room-memory.d.ts +3 -1
  54. package/dist/vascend/room-memory.js +6 -4
  55. package/package.json +2 -2
@@ -0,0 +1,57 @@
1
+ // A short, non-scrolling transition from the first prompt to its saved title.
2
+ export class SessionNamingNotice {
3
+ refresh;
4
+ title = "";
5
+ startedAt = 0;
6
+ pending = false;
7
+ timer;
8
+ constructor(refresh) {
9
+ this.refresh = refresh;
10
+ }
11
+ show(title) {
12
+ this.clear();
13
+ this.title = title;
14
+ this.startedAt = Date.now();
15
+ let showingTitle = false;
16
+ this.timer = setInterval(() => {
17
+ if (!this.pending && Date.now() - this.startedAt >= 5900)
18
+ this.clear();
19
+ else if (this.pending || Date.now() - this.startedAt < 900 || !showingTitle) {
20
+ showingTitle = Date.now() - this.startedAt >= 900;
21
+ this.refresh();
22
+ }
23
+ }, 100);
24
+ this.timer.unref?.();
25
+ this.refresh();
26
+ }
27
+ start() {
28
+ this.show("Renaming");
29
+ this.pending = true;
30
+ }
31
+ finish(title) {
32
+ this.show(title);
33
+ this.startedAt = Date.now() - 900;
34
+ this.refresh();
35
+ }
36
+ lines(now = Date.now()) {
37
+ if (!this.title)
38
+ return [];
39
+ const elapsed = Math.max(0, now - this.startedAt);
40
+ if (!this.pending && elapsed >= 5900)
41
+ return [];
42
+ if (!this.pending && elapsed >= 900)
43
+ return [this.title];
44
+ const frame = Math.floor(elapsed / 100) % 9;
45
+ return Array.from({ length: 3 }, (_, row) => Array.from({ length: 3 }, (_, col) => row * 3 + col === frame ? "\u25cf" : "\u00b7").join(" ")
46
+ + (row === 1 ? " Renaming" : ""));
47
+ }
48
+ line(now = Date.now()) { return this.lines(now).join("\n"); }
49
+ clear() {
50
+ if (this.timer)
51
+ clearInterval(this.timer);
52
+ this.timer = undefined;
53
+ this.title = "";
54
+ this.pending = false;
55
+ this.refresh();
56
+ }
57
+ }
@@ -0,0 +1,2 @@
1
+ import type { SelectorRenderOptions } from "./selector.js";
2
+ export declare function renderSessionPicker<T>(opts: SelectorRenderOptions<T>): string[];
@@ -0,0 +1,32 @@
1
+ import { fitLine, oneLine, padLine, terminalWidth } from "./terminal-text.js";
2
+ // A dense session list leaves the conversation title most of the screen width.
3
+ export function renderSessionPicker(opts) {
4
+ const width = Math.max(1, opts.columns - 2);
5
+ const t = opts.theme;
6
+ const row = (text) => " " + fitLine(text, width);
7
+ const page = Math.max(1, opts.pageSize);
8
+ const index = Math.max(0, Math.min(opts.index, opts.options.length - 1));
9
+ const start = Math.max(0, Math.min(index - Math.floor(page / 2), opts.options.length - page));
10
+ const visible = opts.options.slice(start, start + page);
11
+ const filter = width >= 40 ? "[Cwd] Sort:[Updated]" : "";
12
+ const searchWidth = Math.max(1, width - (filter ? filter.length + 2 : 0));
13
+ const search = padLine(oneLine(opts.query || "Type to search"), searchWidth);
14
+ const lines = [row(t.accent(opts.title)), "", row(t.muted(search) + " " + t.silver(filter)), ""];
15
+ const ageWidth = Math.min(10, Math.max(3, Math.floor(width / 5)));
16
+ for (const [offset, option] of visible.entries()) {
17
+ const active = start + offset === index;
18
+ const text = (active ? "› " : " ") + padLine(option.age || "", ageWidth)
19
+ + " " + oneLine(option.label);
20
+ lines.push(row(active ? t.selected(padLine(text, width)) : t.silver(text)));
21
+ }
22
+ if (!visible.length)
23
+ lines.push(row(t.muted("No matching sessions")));
24
+ while (lines.length < page + 4)
25
+ lines.push("");
26
+ const count = `${opts.options.length ? index + 1 : 0} / ${opts.options.length}`
27
+ + (opts.totalCount !== undefined && opts.totalCount !== opts.options.length ? ` (${opts.totalCount} total)` : "");
28
+ lines.push(row(t.muted("─".repeat(Math.max(0, width - terminalWidth(count) - 1)) + " " + count)));
29
+ lines.push(row(t.silver("enter resume esc back tab details")));
30
+ lines.push(row(t.muted("↑/↓ browse PgUp/PgDn page ctrl+u clear search")));
31
+ return lines;
32
+ }
@@ -0,0 +1,2 @@
1
+ import type { StorageIssue } from "../core/storage-health.js";
2
+ export declare function storageWarning(issues: readonly StorageIssue[], columns: number, paint?: (text: string) => string): string[];
@@ -0,0 +1,35 @@
1
+ import { tr } from "./i18n.js";
2
+ import { wrapText } from "./terminal-text.js";
3
+ function reason(code) {
4
+ switch (code) {
5
+ case "ENOSPC": return tr("disco pieno", "disk full");
6
+ case "EDQUOT": return tr("quota disco esaurita", "disk quota exceeded");
7
+ case "EACCES":
8
+ case "EPERM": return tr("permesso negato", "permission denied");
9
+ case "EROFS": return tr("disco in sola lettura", "read-only filesystem");
10
+ case "EIO": return tr("errore di lettura/scrittura", "I/O error");
11
+ case "ENOENT":
12
+ case "ENOTDIR":
13
+ case "EISDIR": return tr("percorso non disponibile", "path unavailable");
14
+ case "EMFILE":
15
+ case "ENFILE": return tr("troppi file aperti", "too many open files");
16
+ case "EBUSY": return tr("file occupato", "file busy");
17
+ default: return tr("errore di salvataggio", "storage error");
18
+ }
19
+ }
20
+ export function storageWarning(issues, columns, paint = (text) => text) {
21
+ // A still-failing store takes precedence over the short recovery notice.
22
+ const issue = issues.find(item => !item.recovered && item.kind === "transcript")
23
+ ?? issues.find(item => !item.recovered) ?? issues[0];
24
+ if (!issue)
25
+ return [];
26
+ const target = issue.kind === "transcript" ? tr("conversazione", "conversation")
27
+ : issue.kind === "queue" ? tr("coda", "queue") : tr("consumi token", "token usage");
28
+ const detail = reason(issue.code) + (issue.code === "UNKNOWN" ? "" : ` - ${issue.code}`);
29
+ const text = issue.recovered
30
+ ? tr("Salvataggio {target} ripreso · alcuni dati precedenti potrebbero mancare", "Saving {target} resumed · some earlier data may be missing", { target })
31
+ : columns < 65
32
+ ? tr("{target}: dati non salvati ({code})", "{target}: data not saved ({code})", { target, code: issue.code === "UNKNOWN" ? detail : issue.code })
33
+ : tr("Salvataggio {target} fallito ({reason}) · i dati recenti potrebbero non essere salvati", "Saving {target} failed ({reason}) · recent data may not be saved", { target, reason: detail });
34
+ return wrapText("! " + text, Math.max(1, columns - 3), true).map(row => paint(" " + row));
35
+ }
@@ -111,6 +111,8 @@ function snapshotFor(input) {
111
111
  return input.snapshot ?? loadContextStoreSnapshot({
112
112
  sessionDir: input.sessionDir,
113
113
  scopeId: input.scopeId,
114
+ sharedProjectMemory: input.sharedProjectMemory,
115
+ ...(input.planScopeId ? { planScopeId: input.planScopeId } : {}),
114
116
  ...(input.projectKey ? { projectKey: input.projectKey } : {}),
115
117
  });
116
118
  }
@@ -5,6 +5,7 @@ export interface ContextSnapshotInput {
5
5
  readonly sessionDir: string;
6
6
  readonly scopeId: string;
7
7
  readonly projectKey?: string;
8
+ readonly sharedProjectMemory?: boolean;
8
9
  readonly indexed?: boolean;
9
10
  readonly planScopeId?: string;
10
11
  }
@@ -76,11 +76,12 @@ function legacyStoreSignature(sessionDir) {
76
76
  }
77
77
  export function loadContextStoreSnapshot(input) {
78
78
  const indexed = input.indexed ?? process.env["TALOS_CONTEXT_INDEX"] !== "0";
79
- const key = JSON.stringify([input.sessionDir, input.scopeId, input.projectKey, input.planScopeId, indexed]);
80
- const projectScopes = indexed
79
+ const shared = input.sharedProjectMemory === true;
80
+ const key = JSON.stringify([input.sessionDir, input.scopeId, input.projectKey, input.planScopeId, indexed, shared]);
81
+ const projectScopes = !shared ? [] : indexed
81
82
  ? (input.projectKey ? projectMemoryScopes(input.sessionDir, input.projectKey) : []).filter((scope) => scope !== input.scopeId).sort()
82
83
  : undefined;
83
- const scopes = indexed ? [input.scopeId, ...(projectScopes ?? []), ...(input.planScopeId ? [input.planScopeId] : [])] : undefined;
84
+ const scopes = indexed || !shared ? [input.scopeId, ...(projectScopes ?? []), ...(input.planScopeId ? [input.planScopeId] : [])] : undefined;
84
85
  const signature = storeSignature(input.sessionDir, scopes);
85
86
  const previous = cache.get(key);
86
87
  if (signature && previous?.signature === signature) {
@@ -12,6 +12,7 @@ export interface CompileInput {
12
12
  readonly sessionDir: string;
13
13
  readonly scopeId: string;
14
14
  readonly projectKey?: string;
15
+ readonly sharedProjectMemory?: boolean;
15
16
  readonly userText: string;
16
17
  readonly retrievalQuery?: string;
17
18
  readonly totalTokens?: number;
@@ -48,6 +48,7 @@ export function compileContext(input) {
48
48
  const snapshot = input.snapshot ?? loadContextStoreSnapshot({
49
49
  sessionDir: input.sessionDir,
50
50
  scopeId: input.scopeId,
51
+ sharedProjectMemory: input.sharedProjectMemory,
51
52
  ...(input.planScopeId ? { planScopeId: input.planScopeId } : {}),
52
53
  ...(input.projectKey ? { projectKey: input.projectKey } : {}),
53
54
  });
@@ -39,6 +39,8 @@ function nestedTaskFields(slug) {
39
39
  function nestedActivity(slug, event) {
40
40
  if (event.type === "kingdom")
41
41
  return event;
42
+ if (event.type === "usage")
43
+ return { ...event, record: { ...event.record, actor: event.record.actor ?? slug } };
42
44
  if (event.type === "turn_start")
43
45
  return {
44
46
  type: "kingdom", phase: "vassal", slug, state: "active", label: "Pianifica il task", ...nestedTaskFields(slug),
@@ -63,9 +65,13 @@ function nestedActivity(slug, event) {
63
65
  };
64
66
  }
65
67
  if (event.type === "tool_call")
66
- return {
67
- type: "kingdom", phase: "vassal", slug, state: "active", label: event.call.name, ...nestedTaskFields(slug),
68
- };
68
+ return { type: "kingdom", phase: "tool", slug,
69
+ callId: event.call.id, tool: event.call.name, args: JSON.stringify(event.call.input).slice(0, 2400),
70
+ state: "start", at: Date.now() };
71
+ if (event.type === "tool_result")
72
+ return { type: "kingdom", phase: "tool", slug,
73
+ callId: event.result.callId, tool: "", output: event.result.output.slice(0, 12_000),
74
+ state: event.result.isError ? "error" : "ok", at: Date.now() };
69
75
  if (event.type === "done")
70
76
  return {
71
77
  type: "kingdom", phase: "vassal", slug, state: "done", ...nestedTaskFields(slug),
@@ -173,6 +179,13 @@ export function makeAgentUnitRunner(opts) {
173
179
  ...(route[1] ? { taskRef: route[1] } : {}),
174
180
  });
175
181
  }
182
+ const textBuffers = new Map();
183
+ const flushText = (slug) => {
184
+ const text = textBuffers.get(slug)?.trim();
185
+ textBuffers.delete(slug);
186
+ if (text)
187
+ ctx.emit({ type: "kingdom", phase: "say", slug, text: text.slice(0, 12_000), at: Date.now() });
188
+ };
176
189
  const report = await launchGranularKingdom({
177
190
  ...opts,
178
191
  harness: { ...harness, config: runConfig, tools: workerTools },
@@ -182,7 +195,18 @@ export function makeAgentUnitRunner(opts) {
182
195
  maxDepth,
183
196
  markCastle: undefined,
184
197
  onEvent: (id, event) => {
185
- const mapped = nestedActivity(`${self}/${id}`, event);
198
+ const slug = `${self}/${id}`;
199
+ if (event.type === "text") {
200
+ const text = (textBuffers.get(slug) ?? "") + event.delta;
201
+ textBuffers.set(slug, text);
202
+ if (text.length >= 480)
203
+ flushText(slug);
204
+ return;
205
+ }
206
+ if (event.type === "tool_call" || event.type === "turn_end" || event.type === "done" || event.type === "error"
207
+ || (event.type === "kingdom" && event.phase === "session" && event.state === "despawned"))
208
+ flushText(slug);
209
+ const mapped = nestedActivity(slug, event);
186
210
  if (mapped)
187
211
  ctx.emit(mapped);
188
212
  },
@@ -228,11 +252,15 @@ export function makeAgentUnitRunner(opts) {
228
252
  + (ctx.prerequisites?.length ? "\nPREREQUISITI_COMPLETATI (risultati da verificare)\n"
229
253
  + ctx.prerequisites.map((result) => `[${result.id}] ${result.summary}`).join("\n").slice(0, 6000) + "\n" : "") + unit.goal;
230
254
  try {
255
+ sink({ type: "kingdom", phase: "session", kingdomId, sessionId: session.id,
256
+ slug: self, state: "spawned", goal: unit.goal, at: Date.now() });
231
257
  await runTurn(deps, input, sink, ctx.signal);
232
258
  }
233
259
  finally {
234
260
  deps.fileCoordination?.hub.clearActor(deps.fileCoordination.actor.id);
235
261
  kingdomLive.release(kingdomId, self);
262
+ sink({ type: "kingdom", phase: "session", kingdomId, sessionId: session.id,
263
+ slug: self, state: "despawned", at: Date.now() });
236
264
  }
237
265
  if (ctx.signal.aborted)
238
266
  throw new Error("interrotta");
@@ -774,6 +774,8 @@ async function runVassal(opts, provider, modelId, displayModel, castle, peers, i
774
774
  };
775
775
  try {
776
776
  const role = castle.role?.trim() || "collaboratore";
777
+ sink({ type: "kingdom", phase: "session", kingdomId, sessionId: session.id,
778
+ slug: castle.slug, state: "spawned", goal: castle.goal, at: Date.now() });
777
779
  const parent = castle.parent?.trim() || "orchestratore";
778
780
  const instructions = (castle.area?.length ? `\nAREA_ASSEGNATA: ${castle.area.join(", ")}. Coordina modifiche fuori area prima di eseguirle.\n` : "") + (castle.instructions?.trim()
779
781
  ? `\nISTRUZIONI_AGGIUNTIVE\n${castle.instructions.trim()}\n\n`
@@ -829,6 +831,8 @@ async function runVassal(opts, provider, modelId, displayModel, castle, peers, i
829
831
  // Il collaboratore non c'e' piu': la UI deve saperlo subito, altrimenti
830
832
  // continuerebbe a offrire un campo di scrittura che non consegna nulla.
831
833
  kingdomLive.release(kingdomId, castle.slug);
834
+ sink({ type: "kingdom", phase: "session", kingdomId, sessionId: session.id,
835
+ slug: castle.slug, state: "despawned", at: Date.now() });
832
836
  }
833
837
  }
834
838
  function mandateCompletion(sessionDir, planStoreId, mandate) {
@@ -59,4 +59,5 @@ export declare class MemoryStore {
59
59
  private reloadTailFromDisk;
60
60
  private resetIndexes;
61
61
  private addEvent;
62
+ private ensureSearchIndex;
62
63
  }
@@ -106,6 +106,8 @@ export class MemoryStore {
106
106
  }
107
107
  const n = pool.length || 1;
108
108
  const unfiltered = !opts.domain && !opts.type && pool.length === this.events.length;
109
+ if (unfiltered)
110
+ this.ensureSearchIndex();
109
111
  const df = unfiltered ? this.globalDf : new Map();
110
112
  const docTokens = unfiltered ? this.docTokens : pool.map((e) => {
111
113
  const toks = tokensForEvent(e);
@@ -218,11 +220,17 @@ export class MemoryStore {
218
220
  const numeric = /^E(\d+)$/i.exec(event.id)?.[1];
219
221
  if (numeric)
220
222
  this.maxNumericId = Math.max(this.maxNumericId, Number(numeric));
221
- const tokens = tokensForEvent(event);
222
223
  this.events.push(event);
223
- this.docTokens.push(tokens);
224
- for (const token of new Set(tokens))
225
- this.globalDf.set(token, (this.globalDf.get(token) ?? 0) + 1);
224
+ }
225
+ // Reading/forwarding events does not need a second full-text index. Build
226
+ // this local index only for search(), and extend it after concurrent appends.
227
+ ensureSearchIndex() {
228
+ for (let i = this.docTokens.length; i < this.events.length; i++) {
229
+ const tokens = tokensForEvent(this.events[i]);
230
+ this.docTokens.push(tokens);
231
+ for (const token of new Set(tokens))
232
+ this.globalDf.set(token, (this.globalDf.get(token) ?? 0) + 1);
233
+ }
226
234
  }
227
235
  }
228
236
  function tokensForEvent(event) {
@@ -22,12 +22,14 @@ export declare const DEFAULT_RECENT_KEEP = 2;
22
22
  export declare class RoomMemory {
23
23
  private readonly file;
24
24
  private readonly fanout;
25
- private readonly memory;
25
+ private readonly openMemory;
26
26
  private records;
27
27
  private synonyms;
28
28
  private tokenCache;
29
29
  private graphCache;
30
30
  private readonly recentKeep;
31
+ private memoryStore?;
32
+ private get memory();
31
33
  private constructor();
32
34
  withSynonyms(groups: readonly (readonly string[])[]): this;
33
35
  private tokensFor;
@@ -18,7 +18,7 @@ export const DEFAULT_RECENT_KEEP = 2;
18
18
  export class RoomMemory {
19
19
  file;
20
20
  fanout;
21
- memory;
21
+ openMemory;
22
22
  records = [];
23
23
  // Indice sinonimi (default; configurabile via withSynonyms) per espandere le
24
24
  // query verso forme equivalenti IT/EN.
@@ -33,10 +33,12 @@ export class RoomMemory {
33
33
  // cambiano mai corpo ne' archi dopo la scrittura).
34
34
  graphCache = null;
35
35
  recentKeep;
36
- constructor(file, fanout, memory) {
36
+ memoryStore;
37
+ get memory() { return this.memoryStore ??= this.openMemory(); }
38
+ constructor(file, fanout, openMemory) {
37
39
  this.file = file;
38
40
  this.fanout = fanout;
39
- this.memory = memory;
41
+ this.openMemory = openMemory;
40
42
  this.recentKeep = Math.max(0, Math.min(this.fanout - 1, DEFAULT_RECENT_KEEP));
41
43
  }
42
44
  // Sostituisce la tabella sinonimi (es. per dominio specifico). Ritorna this
@@ -60,7 +62,7 @@ export class RoomMemory {
60
62
  }
61
63
  static open(sessionDir, sessionId, fanout = DEFAULT_FANOUT) {
62
64
  const file = join(sessionDir, `${sessionId}.rooms.jsonl`);
63
- const mem = new RoomMemory(file, Math.max(2, fanout), MemoryStore.open(sessionDir, sessionId));
65
+ const mem = new RoomMemory(file, Math.max(2, fanout), () => MemoryStore.open(sessionDir, sessionId));
64
66
  if (existsSync(file)) {
65
67
  const raw = readFileSync(file, "utf8");
66
68
  for (const line of raw.split("\n")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vascend/talos",
3
- "version": "0.1.4",
3
+ "version": "0.1.5",
4
4
  "description": "Vascend Talos - extensible coding agent for your terminal",
5
5
  "repository": {
6
6
  "type": "git",
@@ -48,7 +48,7 @@
48
48
  "@xterm/headless": "^6.0.0",
49
49
  "typescript": "^5.7.0"
50
50
  },
51
- "license": "UNLICENSED",
51
+ "license": "MIT",
52
52
  "private": false,
53
53
  "publishConfig": {
54
54
  "access": "public"