talos-code 0.2.0 → 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +25 -0
- package/dist/runtime/repo.js +11 -0
- package/dist/runtime/talos-composition.js +2 -1
- package/dist/version.js +1 -1
- package/dist/workspace/checkpoint-store.js +32 -1
- package/dist/workspace/checkpoint.js +40 -12
- package/package.json +2 -1
- package/vendor/harness-ui/src/kernel/talosHarness.mjs +149 -2
- package/vendor/manifest.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,31 @@ Format: [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/). Versions
|
|
|
5
5
|
`src/version.ts`, and in the `talos-cli-vX.Y.Z` tag. The package is published on npm as
|
|
6
6
|
[`talos-code`](https://www.npmjs.com/package/talos-code); the command is `talos`.
|
|
7
7
|
|
|
8
|
+
## talos-cli-v0.2.1 — 2026-09-26
|
|
9
|
+
|
|
10
|
+
`npm i -g talos-code@0.2.1`, or `talos update` to see it.
|
|
11
|
+
|
|
12
|
+
### Changed
|
|
13
|
+
- Faster turns in large folders. The workspace snapshot that makes `/undo` possible no longer reads, rewrites and syncs
|
|
14
|
+
every file at every turn: a file that has not changed since the last turn reuses what was stored. A turn in a plain
|
|
15
|
+
folder of 6,000 files went from 39 s to 1.8 s, and from 2.6 s to 1.4 s in a git repository of the same size. What
|
|
16
|
+
`/undo` covers is unchanged.
|
|
17
|
+
- Faster and complete project search. `cerca` runs ripgrep, shipped inside the package for your platform (no download),
|
|
18
|
+
and each match now comes with its line: `path:line:text`. Four searches over 6,000 files took 9.3 s; now about 0.2 s.
|
|
19
|
+
Before, a search stopped after reading 5,000 files and could miss matches in a bigger project. Several searches asked
|
|
20
|
+
in the same answer run together. Without ripgrep, the previous search still works.
|
|
21
|
+
|
|
22
|
+
### Known limits
|
|
23
|
+
- Tested on Windows 11 x64 only; Node.js 24 (24.18 or later in the 24 line).
|
|
24
|
+
- Shell commands still run one at a time, each in its sandbox; a start of about one second remains.
|
|
25
|
+
|
|
26
|
+
### Verification
|
|
27
|
+
- Measured on a hermetic bench (`scripts/bench-harness.ts`): a local fake provider, no network, TALOS against Pi, Codex
|
|
28
|
+
and OpenCode on the same turns.
|
|
29
|
+
- CLI unit suite: 1876 tests, 1872 pass, 4 skip, 0 fail. Acceptance on the built CLI in a synthetic terminal: 263/263.
|
|
30
|
+
- The package proof (`scripts/prove-standalone.ts`), 16/16 on `talos-code-0.2.1.tgz` (1.76 MB); the installed package
|
|
31
|
+
resolves its own ripgrep.
|
|
32
|
+
|
|
8
33
|
## talos-cli-v0.2.0 — 2026-09-26
|
|
9
34
|
|
|
10
35
|
`npm i -g talos-code@0.2.0`, or `talos update` to see it from 0.1.0.
|
package/dist/runtime/repo.js
CHANGED
|
@@ -89,7 +89,18 @@ export function findTalosRepoRoot(start = process.cwd(), deps = {}) {
|
|
|
89
89
|
return cwd;
|
|
90
90
|
throw new Error('TALOS_RUNTIME_NOT_FOUND');
|
|
91
91
|
}
|
|
92
|
+
/* P18 phase 3 (owner 2026-09-26: ripgrep inside the npm package, no network): the kernel's `cerca` runs ripgrep from
|
|
93
|
+
TALOS_RG_PATH. `@vscode/ripgrep` ships the binary for this platform as an optional dependency of the package (no
|
|
94
|
+
install script, no download); a path already set, or a package without it, is left alone and `cerca` keeps its JS walk. */
|
|
95
|
+
let bundledRipgrep = null;
|
|
96
|
+
export function provideBundledRipgrep(env = process.env) {
|
|
97
|
+
if (env.TALOS_RG_PATH)
|
|
98
|
+
return Promise.resolve();
|
|
99
|
+
return bundledRipgrep ??= import('@vscode/ripgrep').then((m) => { if (typeof m?.rgPath === 'string' && existsSync(m.rgPath))
|
|
100
|
+
env.TALOS_RG_PATH = m.rgPath; }, () => { });
|
|
101
|
+
}
|
|
92
102
|
export async function importTalosModule(repoRoot, relativeFromHarnessSrc) {
|
|
103
|
+
await provideBundledRipgrep();
|
|
93
104
|
const file = join(repoRoot, 'harness-ui', 'src', relativeFromHarnessSrc);
|
|
94
105
|
return import(__rewriteRelativeImportExtension(pathToFileURL(file).href));
|
|
95
106
|
}
|
|
@@ -12,6 +12,7 @@ import { secretValuesFromEnvironment } from "../diagnostics/redact.js";
|
|
|
12
12
|
import { isTestProfile } from "../provider/store.js";
|
|
13
13
|
import { compactionEvent, createCliEventHub, createRunObserver, RETRY_AFTER_MAX_MS, summaryRequestedEvent } from "./provider-attempts.js";
|
|
14
14
|
import { createToolOutputStore, pruneToolOutput } from "./output-store.js";
|
|
15
|
+
import { provideBundledRipgrep } from "./repo.js";
|
|
15
16
|
import { withArchivableCapture } from "./context-archive.js";
|
|
16
17
|
import { CONTEXT_ENGINE_MIN_WINDOW, configureModelProfiles, createModelsDevLoader, modelProfile, modelProfileEvidence, ollamaWindow, rememberOllamaWindow, setModelOverrides } from "./model-profile.js";
|
|
17
18
|
import { AsyncLocalStorage } from 'node:async_hooks';
|
|
@@ -246,7 +247,7 @@ export function createCliContextBridge({ repoRoot, paths, registry, providerRegi
|
|
|
246
247
|
export function scopeCliKeyring(raw) { if (!raw)
|
|
247
248
|
return null; return { get: (s, a) => raw.get(remapService(s), a), set: (s, a, v) => raw.set(remapService(s), a, v), remove: (s, a) => raw.remove(remapService(s), a) }; }
|
|
248
249
|
/** The kernel modules the product composes with. Exported so a test can observe one of them while every other stays the production one. */
|
|
249
|
-
export async function loadModules(repoRoot) { const src = join(repoRoot, 'harness-ui', 'src'); const [cred, owner, agent, sessions, plugins, search, duck, providerRegistry, probe, destination, readiness, chatImages] = await Promise.all([import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'provider-credential-store.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'runtime-owner-adapter.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'agent-service.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'session-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'plugin-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'search-source-store.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'duckduckgo-search.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'provider-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'provider-probe.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'model-destination.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'sessione-pronta.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'chat-image-attachments.mjs')).href))]); return { createProviderCredentialStore: cred.createProviderCredentialStore, createOwnerRuntimeAdapter: owner.createOwnerRuntimeAdapter, creaFetchMultiProvider: owner.creaFetchMultiProvider, compattaSessione: agent.compattaSessione, chiediAlModelloUnaVolta: agent.chiediAlModelloUnaVolta, avviaSessione: agent.avviaSessione, eseguiComandoDiretto: agent.eseguiComandoDiretto, createSessionRegistry: sessions.createSessionRegistry, verificaTrustPlugin: plugins.verificaTrustPlugin, createSearchSourceStore: search.createSearchSourceStore, creaTrasportoSenzaChiave: duck.creaTrasportoSenzaChiave, ENDPOINT_SENTINELLA_DUCKDUCKGO: duck.ENDPOINT_SENTINELLA_DUCKDUCKGO, REGISTRO_FORNITORI: providerRegistry.REGISTRO_FORNITORI, createProviderProbe: probe.createProviderProbe, separaFonteModello: destination.separaFonteModello, creaProntoFn: readiness.creaProntoFn, createChatImageStore: chatImages.createChatImageStore }; }
|
|
250
|
+
export async function loadModules(repoRoot) { await provideBundledRipgrep(); const src = join(repoRoot, 'harness-ui', 'src'); const [cred, owner, agent, sessions, plugins, search, duck, providerRegistry, probe, destination, readiness, chatImages] = await Promise.all([import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'provider-credential-store.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'runtime-owner-adapter.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'agent-service.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'session-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'plugin-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'search-source-store.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'duckduckgo-search.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'provider-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'provider-probe.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'model-destination.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'sessione-pronta.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'chat-image-attachments.mjs')).href))]); return { createProviderCredentialStore: cred.createProviderCredentialStore, createOwnerRuntimeAdapter: owner.createOwnerRuntimeAdapter, creaFetchMultiProvider: owner.creaFetchMultiProvider, compattaSessione: agent.compattaSessione, chiediAlModelloUnaVolta: agent.chiediAlModelloUnaVolta, avviaSessione: agent.avviaSessione, eseguiComandoDiretto: agent.eseguiComandoDiretto, createSessionRegistry: sessions.createSessionRegistry, verificaTrustPlugin: plugins.verificaTrustPlugin, createSearchSourceStore: search.createSearchSourceStore, creaTrasportoSenzaChiave: duck.creaTrasportoSenzaChiave, ENDPOINT_SENTINELLA_DUCKDUCKGO: duck.ENDPOINT_SENTINELLA_DUCKDUCKGO, REGISTRO_FORNITORI: providerRegistry.REGISTRO_FORNITORI, createProviderProbe: probe.createProviderProbe, separaFonteModello: destination.separaFonteModello, creaProntoFn: readiness.creaProntoFn, createChatImageStore: chatImages.createChatImageStore }; }
|
|
250
251
|
async function loadTrustSupport(repoRoot) {
|
|
251
252
|
const src = join(repoRoot, 'harness-ui', 'src');
|
|
252
253
|
const [plugins, hooks, mcp, mcpSession, pluginSession] = await Promise.all([import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'plugin-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'hook-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'mcp-registry.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'mcp-session.mjs')).href)), import(__rewriteRelativeImportExtension(pathToFileURL(join(src, 'plugin-session.mjs')).href))]);
|
package/dist/version.js
CHANGED
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
export const CLI_VERSION = '0.2.
|
|
1
|
+
export const CLI_VERSION = '0.2.1';
|
|
2
2
|
export const TALOS_BASE_COMMIT = '0c432153a288f64237e98403869d02d20d8fabc7';
|
|
@@ -60,6 +60,24 @@ function parseRecord(text, expectedRoot) {
|
|
|
60
60
|
throw coded('CHECKPOINT_STORE_INVALID');
|
|
61
61
|
return value;
|
|
62
62
|
}
|
|
63
|
+
const STAT_INDEX_SCHEMA = 'talos.cli.checkpoint-stat-index.v1';
|
|
64
|
+
async function loadStatIndex(indexPath) {
|
|
65
|
+
const rows = new Map();
|
|
66
|
+
try {
|
|
67
|
+
const value = JSON.parse(await readFile(indexPath, 'utf8'));
|
|
68
|
+
if (value?.schema !== STAT_INDEX_SCHEMA || !Array.isArray(value.rows))
|
|
69
|
+
return rows;
|
|
70
|
+
for (const row of value.rows) {
|
|
71
|
+
if (!Array.isArray(row) || row.length !== 6)
|
|
72
|
+
continue;
|
|
73
|
+
const [p, size, mtimeMs, ctimeMs, ref, verifiedAtMs] = row;
|
|
74
|
+
if (typeof p === 'string' && typeof ref === 'string' && /^[0-9a-f]{64}$/u.test(ref) && [size, mtimeMs, ctimeMs, verifiedAtMs].every(n => typeof n === 'number' && Number.isFinite(n)))
|
|
75
|
+
rows.set(p, { size, mtimeMs, ctimeMs, ref, verifiedAtMs });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
catch { /* missing or torn: an empty index, every file is read again */ }
|
|
79
|
+
return rows;
|
|
80
|
+
}
|
|
63
81
|
function dedupeEvidence(rows) { const byKey = new Map(); for (const row of rows)
|
|
64
82
|
byKey.set(`${row.path}\0${row.beforeHash ?? ''}\0${row.afterHash ?? ''}`, row); return [...byKey.values()]; }
|
|
65
83
|
export function createCheckpointStore({ rootDir, projectRoot, limits = CHECKPOINT_LIMITS }) {
|
|
@@ -85,7 +103,20 @@ export function createCheckpointStore({ rootDir, projectRoot, limits = CHECKPOIN
|
|
|
85
103
|
} }
|
|
86
104
|
async function replace(record) { const x = await init(); if (record.canonicalRoot !== x.canonicalRoot)
|
|
87
105
|
throw coded('CHECKPOINT_WORKSPACE_MISMATCH'); await atomicWrite(await recordPath(record.id), `${JSON.stringify(record)}\n`); }
|
|
88
|
-
|
|
106
|
+
/* P18: the stat index of the last capture (checkpoint.ts StatCache), kept in memory and in the workspace's folder so a new
|
|
107
|
+
`talos -p` process starts from it too. It is only a cache: unreadable means empty (every file read again), and a
|
|
108
|
+
failed save costs speed on the next turn, never a checkpoint. */
|
|
109
|
+
let statRows = null;
|
|
110
|
+
async function capture() {
|
|
111
|
+
const x = await init();
|
|
112
|
+
const indexPath = path.join(x.workspaceDir, 'stat-index.json');
|
|
113
|
+
statRows ??= await loadStatIndex(indexPath);
|
|
114
|
+
const next = new Map();
|
|
115
|
+
const snapshot = await captureWorkspaceSnapshot({ projectRoot: x.canonicalRoot, blobRoot, limits, statCache: { previous: statRows, next } });
|
|
116
|
+
statRows = next;
|
|
117
|
+
await atomicWrite(indexPath, `${JSON.stringify({ schema: STAT_INDEX_SCHEMA, rows: [...next].map(([p, r]) => [p, r.size, r.mtimeMs, r.ctimeMs, r.ref, r.verifiedAtMs]) })}\n`).catch(() => { });
|
|
118
|
+
return snapshot;
|
|
119
|
+
}
|
|
89
120
|
async function list() { const x = await init(); let names = []; try {
|
|
90
121
|
names = await readdir(x.recordsDir);
|
|
91
122
|
}
|
|
@@ -5,6 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
import { spawn } from 'node:child_process';
|
|
6
6
|
import { resolveWorkspaceIdentity } from "../security/workspace-identity.js";
|
|
7
7
|
export const CHECKPOINT_LIMITS = { maxEntries: 50_000, maxStoredBytes: 256 * 1024 * 1024, maxStoredFileBytes: 64 * 1024 * 1024 };
|
|
8
|
+
const RACY_MARGIN_MS = 2_000, BLOB_SYNC_BATCH = 16;
|
|
8
9
|
function coded(code, message = code, details = {}) { return Object.assign(new Error(message === code ? code : `${code}: ${message}`), { code, details }); }
|
|
9
10
|
function sha256(data) { return createHash('sha256').update(data).digest('hex'); }
|
|
10
11
|
function slash(value) { return value.split(path.sep).join('/'); }
|
|
@@ -59,15 +60,15 @@ catch (error) {
|
|
|
59
60
|
if (process.platform !== 'win32')
|
|
60
61
|
throw error;
|
|
61
62
|
} }
|
|
62
|
-
async function storeBlob(blobRoot, data) {
|
|
63
|
+
async function storeBlob(blobRoot, data, budget) {
|
|
63
64
|
const hash = sha256(data), target = path.join(blobRoot, hash);
|
|
64
|
-
|
|
65
|
+
if (budget.blobs.has(hash))
|
|
66
|
+
return hash;
|
|
65
67
|
const temp = path.join(blobRoot, `.${hash}.${process.pid}.tmp`);
|
|
66
68
|
try {
|
|
67
69
|
const handle = await open(temp, 'wx', 0o600);
|
|
68
70
|
try {
|
|
69
71
|
await handle.writeFile(data);
|
|
70
|
-
await handle.sync();
|
|
71
72
|
}
|
|
72
73
|
finally {
|
|
73
74
|
await handle.close();
|
|
@@ -76,14 +77,35 @@ async function storeBlob(blobRoot, data) {
|
|
|
76
77
|
await rm(temp, { force: true });
|
|
77
78
|
return;
|
|
78
79
|
} throw error; });
|
|
79
|
-
await durableDir(blobRoot);
|
|
80
80
|
}
|
|
81
81
|
catch (error) {
|
|
82
82
|
if (error?.code !== 'EEXIST')
|
|
83
83
|
throw error;
|
|
84
84
|
}
|
|
85
|
+
budget.blobs.add(hash);
|
|
86
|
+
budget.pending.push(target);
|
|
85
87
|
return hash;
|
|
86
88
|
}
|
|
89
|
+
/* Every blob written by this capture is durable before the capture returns (and so before its record is written); a sync
|
|
90
|
+
error is never swallowed (gate E8/E9). */
|
|
91
|
+
async function syncNewBlobs(blobRoot, budget) {
|
|
92
|
+
if (budget.pending.length === 0)
|
|
93
|
+
return;
|
|
94
|
+
for (let i = 0; i < budget.pending.length; i += BLOB_SYNC_BATCH)
|
|
95
|
+
await Promise.all(budget.pending.slice(i, i + BLOB_SYNC_BATCH).map(durableFile));
|
|
96
|
+
budget.pending = [];
|
|
97
|
+
await durableDir(blobRoot);
|
|
98
|
+
}
|
|
99
|
+
async function assertFileContained(workspace, absolute, budget) {
|
|
100
|
+
const dir = path.dirname(absolute);
|
|
101
|
+
let canonical = budget.dirs.get(dir);
|
|
102
|
+
if (canonical === undefined) {
|
|
103
|
+
canonical = await realpath(dir);
|
|
104
|
+
budget.dirs.set(dir, canonical);
|
|
105
|
+
}
|
|
106
|
+
if (!inside(workspace.canonicalRoot, canonical))
|
|
107
|
+
throw coded('CHECKPOINT_PATH_ESCAPE', `Checkpoint path escapes workspace: ${absolute}`);
|
|
108
|
+
}
|
|
87
109
|
function consumeFileBudget(budget, size) {
|
|
88
110
|
if (size > budget.limits.maxStoredFileBytes)
|
|
89
111
|
throw coded('CHECKPOINT_FILE_TOO_LARGE');
|
|
@@ -178,10 +200,13 @@ async function captureActualFile(workspace, absolute, relative, blobRoot, budget
|
|
|
178
200
|
}
|
|
179
201
|
if (!stat.isFile())
|
|
180
202
|
throw coded('CHECKPOINT_SPECIAL_FILE_UNSUPPORTED', `Unsupported workspace entry: ${relative}`);
|
|
181
|
-
await
|
|
203
|
+
await assertFileContained(workspace, absolute, budget);
|
|
182
204
|
consumeFileBudget(budget, stat.size);
|
|
183
|
-
const
|
|
184
|
-
const
|
|
205
|
+
const seen = budget.cache?.previous.get(relative);
|
|
206
|
+
const unchanged = Boolean(seen && seen.size === stat.size && seen.mtimeMs === stat.mtimeMs && seen.ctimeMs === stat.ctimeMs
|
|
207
|
+
&& stat.mtimeMs < seen.verifiedAtMs - RACY_MARGIN_MS && stat.ctimeMs < seen.verifiedAtMs - RACY_MARGIN_MS && budget.blobs.has(seen.ref));
|
|
208
|
+
const ref = unchanged ? seen.ref : await storeBlob(blobRoot, await readFile(absolute), budget);
|
|
209
|
+
budget.cache?.next.set(relative, { size: stat.size, mtimeMs: stat.mtimeMs, ctimeMs: stat.ctimeMs, ref, verifiedAtMs: budget.startedAtMs });
|
|
185
210
|
const executable = process.platform === 'win32' ? indexExecutable : Boolean(stat.mode & 0o111);
|
|
186
211
|
return { kind: 'file', source: 'blob', ref, size: stat.size, mode: stat.mode & 0o777, executable };
|
|
187
212
|
}
|
|
@@ -249,7 +274,7 @@ async function captureGit(workspace, blobRoot, budget) {
|
|
|
249
274
|
continue;
|
|
250
275
|
}
|
|
251
276
|
consumeEntry(budget);
|
|
252
|
-
await
|
|
277
|
+
await assertFileContained(workspace, absolute, budget);
|
|
253
278
|
entries[relative] = { kind: 'file', source: 'git', ref: oid, size: stat.size, mode: stat.mode & 0o777, executable: mode === '100755', gitPath };
|
|
254
279
|
}
|
|
255
280
|
for (const gitPath of nulList(untrackedRaw)) {
|
|
@@ -302,7 +327,7 @@ async function captureNonGit(workspace, blobRoot, budget) {
|
|
|
302
327
|
}
|
|
303
328
|
if (!stat.isFile())
|
|
304
329
|
throw coded('CHECKPOINT_SPECIAL_FILE_UNSUPPORTED', `Unsupported workspace entry: ${relative}`);
|
|
305
|
-
await
|
|
330
|
+
await assertFileContained(workspace, absolute, budget);
|
|
306
331
|
consumeFileBudget(budget, stat.size);
|
|
307
332
|
}
|
|
308
333
|
}
|
|
@@ -328,14 +353,17 @@ async function captureNonGit(workspace, blobRoot, budget) {
|
|
|
328
353
|
await walk(workspace.canonicalRoot, '');
|
|
329
354
|
return { capturedAt: new Date().toISOString(), workspace: { canonicalRoot: workspace.canonicalRoot, identityHash: workspace.identityHash, gitRoot: null }, entries, git: null, coverage: { tracked: false, untrackedNonIgnored: true, ignoredUntracked: true, gitMetadataRestored: false }, exclusions: ['Filesystem metadata other than file content, executable bit and symlink target is outside rollback coverage.', 'Empty-directory existence is outside rollback coverage.'] };
|
|
330
355
|
}
|
|
331
|
-
export async function captureWorkspaceSnapshot({ projectRoot, blobRoot, limits = CHECKPOINT_LIMITS }) {
|
|
356
|
+
export async function captureWorkspaceSnapshot({ projectRoot, blobRoot, limits = CHECKPOINT_LIMITS, statCache }) {
|
|
357
|
+
const startedAtMs = Date.now();
|
|
332
358
|
const workspace = await resolveWorkspaceIdentity({ projectRoot });
|
|
333
359
|
await ensurePrivateDir(blobRoot);
|
|
334
360
|
const canonicalBlobRoot = await realpath(blobRoot);
|
|
335
361
|
if (inside(workspace.canonicalRoot, canonicalBlobRoot))
|
|
336
362
|
throw coded('CHECKPOINT_STORE_INSIDE_WORKSPACE');
|
|
337
|
-
const budget = { entries: 0, storedBytes: 0, limits };
|
|
338
|
-
|
|
363
|
+
const budget = { entries: 0, storedBytes: 0, limits, startedAtMs, blobs: new Set(await readdir(canonicalBlobRoot)), pending: [], dirs: new Map(), cache: statCache ?? null };
|
|
364
|
+
const snapshot = workspace.gitRoot ? await captureGit(workspace, canonicalBlobRoot, budget) : await captureNonGit(workspace, canonicalBlobRoot, budget);
|
|
365
|
+
await syncNewBlobs(canonicalBlobRoot, budget);
|
|
366
|
+
return snapshot;
|
|
339
367
|
}
|
|
340
368
|
export function checkpointMutationAllowed(mode) { return mode !== 'plan'; }
|
|
341
369
|
export function extractWorkspaceWriteEvidence(raw) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "talos-code",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.1",
|
|
4
4
|
"description": "TALOS in the terminal: a coding agent with per-tool permissions, file checkpoints and resumable sessions, on the model you choose.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"coding-agent",
|
|
@@ -34,6 +34,7 @@
|
|
|
34
34
|
"@microsoft/mxc-sdk": "0.8.0",
|
|
35
35
|
"@modelcontextprotocol/client": "2.0.0",
|
|
36
36
|
"@napi-rs/keyring": "1.3.0",
|
|
37
|
+
"@vscode/ripgrep": "1.18.0",
|
|
37
38
|
"ai": "7.0.93",
|
|
38
39
|
"diff": "9.0.0",
|
|
39
40
|
"docx": "9.5.1",
|
|
@@ -3662,6 +3662,123 @@ async function tuttiIPercorsi(disco, { filtro = null } = {}) {
|
|
|
3662
3662
|
return { percorsi, dimensioni, stato }
|
|
3663
3663
|
}
|
|
3664
3664
|
|
|
3665
|
+
/**
|
|
3666
|
+
* ⭐⭐⭐ P18 fase 3 (owner 26/09/2026: «TALOS più veloce di tutti»; decisioni: ripgrep DENTRO il pacchetto,
|
|
3667
|
+
* righe trovate con un tetto) — `cerca` nel contenuto con ripgrep.
|
|
3668
|
+
*
|
|
3669
|
+
* Misurato sul banco ermetico (`cli/scripts/bench-harness.ts`, 26/09): quattro `cerca` su un albero di 6.000 file
|
|
3670
|
+
* costavano **9,3 s** (una lettura JS alla volta, fino a `MAX_FILE_LETTI`), e su questo monorepo (6.405 file visibili)
|
|
3671
|
+
* la ricerca si fermava a 5.000 letture: **incompleta**, e una parola assente risultava «non c'e'». Pi fa `rg --json`
|
|
3672
|
+
* (`coding-agent/src/core/tools/grep.ts:119-168`), Claude Code ha ripgrep incluso (`USE_BUILTIN_RIPGREP`), Codex lo usa
|
|
3673
|
+
* dalla shell. ⇒ Stessa semantica di prima (sottostringa, maiuscole indifferenti, `.gitignore` anche fuori da un repo,
|
|
3674
|
+
* `.git`/`node_modules` sempre fuori, i binari saltati), ma ogni risultato porta **riga e testo** (`percorso:riga:testo`):
|
|
3675
|
+
* il modello non deve riaprire il file per sapere dove.
|
|
3676
|
+
*
|
|
3677
|
+
* Il binario: `TALOS_RG_PATH` (la CLI lo mette da `@vscode/ripgrep`, che e' nel suo pacchetto, senza rete), altrimenti
|
|
3678
|
+
* `@vscode/ripgrep` se risolvibile da qui. ⛔ Assente, rotto o in timeout ⇒ `null`, e `cercaNelProgetto` prosegue con la
|
|
3679
|
+
* camminata JS di sempre: nessuna installazione perde la ricerca.
|
|
3680
|
+
*/
|
|
3681
|
+
const MAX_CERCA_INSIEME = 8
|
|
3682
|
+
const MAX_RIGHE_RG = 100
|
|
3683
|
+
const MAX_RIGHE_PER_FILE = 5
|
|
3684
|
+
const MAX_COLONNE_RG = 200
|
|
3685
|
+
const MAX_BYTE_RG = 4_000_000
|
|
3686
|
+
const TEMPO_MAX_RG_MS = 20_000
|
|
3687
|
+
let ripgrepRisolto = null
|
|
3688
|
+
function percorsoRipgrep() {
|
|
3689
|
+
const daAmbiente = process.env.TALOS_RG_PATH
|
|
3690
|
+
if (daAmbiente) return Promise.resolve(existsSync(daAmbiente) ? daAmbiente : null)
|
|
3691
|
+
ripgrepRisolto ??= import('@vscode/ripgrep').then(
|
|
3692
|
+
(m) => (typeof m?.rgPath === 'string' && existsSync(m.rgPath) ? m.rgPath : null),
|
|
3693
|
+
() => null)
|
|
3694
|
+
return ripgrepRisolto
|
|
3695
|
+
}
|
|
3696
|
+
function eseguiRipgrep(rg, argomenti, cartella, segnale) {
|
|
3697
|
+
return new Promise((fine) => {
|
|
3698
|
+
let figlio
|
|
3699
|
+
try { figlio = spawn(rg, argomenti, { cwd: cartella, stdio: ['ignore', 'pipe', 'ignore'], windowsHide: true }) }
|
|
3700
|
+
catch { fine(null); return }
|
|
3701
|
+
const pezzi = []
|
|
3702
|
+
let byte = 0, tagliato = false
|
|
3703
|
+
const orologio = setTimeout(() => { tagliato = true; figlio.kill() }, TEMPO_MAX_RG_MS)
|
|
3704
|
+
/* Lo Stop ferma anche la ricerca IN VOLO (vincolo del desktop, 26/09): si uccide rg e si dice «fermata». */
|
|
3705
|
+
let fermato = false
|
|
3706
|
+
const alloStop = () => { fermato = true; figlio.kill() }
|
|
3707
|
+
if (segnale?.aborted) alloStop(); else segnale?.addEventListener?.('abort', alloStop, { once: true })
|
|
3708
|
+
figlio.stdout.on('data', (pezzo) => {
|
|
3709
|
+
if (tagliato) return
|
|
3710
|
+
byte += pezzo.length
|
|
3711
|
+
pezzi.push(pezzo)
|
|
3712
|
+
if (byte >= MAX_BYTE_RG) { tagliato = true; figlio.kill() }
|
|
3713
|
+
})
|
|
3714
|
+
figlio.on('error', () => { clearTimeout(orologio); fine(null) })
|
|
3715
|
+
figlio.on('close', (codice) => {
|
|
3716
|
+
clearTimeout(orologio)
|
|
3717
|
+
segnale?.removeEventListener?.('abort', alloStop)
|
|
3718
|
+
if (fermato) { fine({ testo: '', tagliato: false, scaduto: false, fermato: true }); return }
|
|
3719
|
+
const testo = Buffer.concat(pezzi).toString('utf8')
|
|
3720
|
+
/* 0 = trovato, 1 = niente; 2 = errori (un file illeggibile) con o senza risultati. Un 2 senza uscita,
|
|
3721
|
+
o un processo ucciso senza niente, non e' «non c'e'»: e' un guasto, e decide il ripiego JS. */
|
|
3722
|
+
if (codice === 0 || codice === 1 || testo) fine({ testo, tagliato: tagliato && byte >= MAX_BYTE_RG, scaduto: tagliato && byte < MAX_BYTE_RG })
|
|
3723
|
+
else fine(null)
|
|
3724
|
+
})
|
|
3725
|
+
})
|
|
3726
|
+
}
|
|
3727
|
+
const percorsoDaRg = (p) => p.replace(/\\/gu, '/').replace(/^\.\//u, '')
|
|
3728
|
+
async function cercaConRipgrep(radice, testo, chiaveNome, segnale) {
|
|
3729
|
+
const rg = await percorsoRipgrep()
|
|
3730
|
+
if (!rg) return null
|
|
3731
|
+
const comuni = ['--no-config', '--hidden', '--no-require-git', '--glob', '!.git', '--glob', '!node_modules']
|
|
3732
|
+
/* Il ripiego delle cartelle potate vale, come prima, solo quando non c'e' un `.gitignore` da cui leggere. */
|
|
3733
|
+
if (!existsSync(join(radice, '.gitignore'))) for (const c of POTATE_SENZA_GITIGNORE) comuni.push('--glob', `!${c}`)
|
|
3734
|
+
const ricerca = await eseguiRipgrep(rg, [...comuni, '--line-number', '--with-filename', '--no-heading', '--color', 'never',
|
|
3735
|
+
'--fixed-strings', '--ignore-case', '--max-count', String(MAX_RIGHE_PER_FILE), '--max-columns', String(MAX_COLONNE_RG),
|
|
3736
|
+
'--max-columns-preview', '--max-filesize', String(MAX_BYTE_FILE), '-e', testo, '--', '.'], radice, segnale)
|
|
3737
|
+
if (ricerca === null) return null
|
|
3738
|
+
if (ricerca.fermato) return 'stopped: the search was interrupted.'
|
|
3739
|
+
/* rg lavora in parallelo e non promette un ordine: si ordina qui, cosi' la stessa domanda da' la stessa risposta. */
|
|
3740
|
+
const perFile = new Map()
|
|
3741
|
+
for (const riga of ricerca.testo.split(/\r?\n/u)) {
|
|
3742
|
+
const m = /^(.+?):(\d+):(.*)$/u.exec(riga)
|
|
3743
|
+
if (!m) continue
|
|
3744
|
+
const percorso = percorsoDaRg(m[1])
|
|
3745
|
+
if (!perFile.has(percorso)) perFile.set(percorso, [])
|
|
3746
|
+
perFile.get(percorso).push(`${percorso}:${m[2]}:${m[3].trim()}`)
|
|
3747
|
+
}
|
|
3748
|
+
let perNome = []
|
|
3749
|
+
if (chiaveNome) {
|
|
3750
|
+
const elenco = await eseguiRipgrep(rg, [...comuni, '--files', '.'], radice, segnale)
|
|
3751
|
+
if (elenco === null) return null
|
|
3752
|
+
if (elenco.fermato) return 'stopped: the search was interrupted.'
|
|
3753
|
+
perNome = elenco.testo.split(/\r?\n/u).filter(Boolean).map(percorsoDaRg).filter((p) => p.toLowerCase().includes(chiaveNome)).sort()
|
|
3754
|
+
}
|
|
3755
|
+
const avvisi = []
|
|
3756
|
+
if (ricerca.scaduto) avvisi.push(`⚠ incomplete scan: the search took longer than ${TEMPO_MAX_RG_MS / 1000} s and was stopped. Search inside a subfolder.`)
|
|
3757
|
+
if (ricerca.tagliato) avvisi.push('⚠ incomplete scan: too much output — the matches below are a part of them. Narrow the search.')
|
|
3758
|
+
const coda = avvisi.length > 0 ? `\n${avvisi.join('\n')}` : ''
|
|
3759
|
+
const file = [...perFile.keys()].sort()
|
|
3760
|
+
if (file.length === 0 && perNome.length === 0) {
|
|
3761
|
+
return `no file matches "${testo}" (searched with ripgrep; .gitignore respected, binaries skipped). Try a shorter or different "testo".${coda}`
|
|
3762
|
+
}
|
|
3763
|
+
const righe = perNome.slice(0, MAX_RISULTATI)
|
|
3764
|
+
let righeTesto = 0, fileMostrati = 0
|
|
3765
|
+
for (const f of file) {
|
|
3766
|
+
if (fileMostrati >= MAX_RISULTATI || righeTesto >= MAX_RIGHE_RG) break
|
|
3767
|
+
const trovate = perFile.get(f).slice(0, MAX_RIGHE_RG - righeTesto)
|
|
3768
|
+
righe.push(...trovate)
|
|
3769
|
+
righeTesto += trovate.length
|
|
3770
|
+
fileMostrati += 1
|
|
3771
|
+
}
|
|
3772
|
+
const fileTagliati = file.length - fileMostrati
|
|
3773
|
+
const nomiTagliati = perNome.length - Math.min(perNome.length, MAX_RISULTATI)
|
|
3774
|
+
return righe.join('\n')
|
|
3775
|
+
+ (nomiTagliati > 0 ? `\n… and ${nomiTagliati} more paths matching "${chiaveNome}" not shown — narrow the search.` : '')
|
|
3776
|
+
// ⛔ Il taglio si DICHIARA, come nella camminata JS: «40 file» non deve leggersi come «sono 40».
|
|
3777
|
+
+ (fileTagliati > 0 ? `\n… and ${fileTagliati} more files with matches not shown — narrow the search.` : '')
|
|
3778
|
+
+ (file.some((f) => perFile.get(f).length >= MAX_RIGHE_PER_FILE) ? `\n(at most ${MAX_RIGHE_PER_FILE} matching lines per file are shown)` : '')
|
|
3779
|
+
+ coda
|
|
3780
|
+
}
|
|
3781
|
+
|
|
3665
3782
|
/**
|
|
3666
3783
|
* ⭐⭐⭐ CERCA — l'attrezzo che toglie la cecita', senza comprare l'inventario.
|
|
3667
3784
|
*
|
|
@@ -3685,10 +3802,16 @@ async function tuttiIPercorsi(disco, { filtro = null } = {}) {
|
|
|
3685
3802
|
* @param {{radice?: string}} [opzioni] — la cartella VERA, per leggere il `.gitignore`.
|
|
3686
3803
|
* Assente (test, ponte del telefono) ⇒ si usa il ripiego `POTATE_SENZA_GITIGNORE`.
|
|
3687
3804
|
*/
|
|
3688
|
-
export async function cercaNelProgetto(disco, { testo, nome }, { radice } = {}) {
|
|
3805
|
+
export async function cercaNelProgetto(disco, { testo, nome }, { radice, segnale } = {}) {
|
|
3689
3806
|
const chiaveTesto = String(testo ?? '').trim().toLowerCase()
|
|
3690
3807
|
const chiaveNome = String(nome ?? '').trim().toLowerCase()
|
|
3691
3808
|
if (!chiaveTesto && !chiaveNome) return 'give at least one of "testo" or "nome".'
|
|
3809
|
+
/* P18 fase 3: con la cartella vera e un ripgrep disponibile, la ricerca nel contenuto la fa `rg`.
|
|
3810
|
+
`null` = rg assente o fallito: si prosegue con la camminata JS qui sotto, invariata. */
|
|
3811
|
+
if (chiaveTesto && radice) {
|
|
3812
|
+
const conRg = await cercaConRipgrep(radice, String(testo).trim(), chiaveNome, segnale)
|
|
3813
|
+
if (conRg !== null) return conRg
|
|
3814
|
+
}
|
|
3692
3815
|
|
|
3693
3816
|
const filtro = await filtroDelProgetto(radice)
|
|
3694
3817
|
const { percorsi, dimensioni, stato } = await tuttiIPercorsi(disco, { filtro })
|
|
@@ -8136,6 +8259,30 @@ export async function talosLavora({
|
|
|
8136
8259
|
break
|
|
8137
8260
|
}
|
|
8138
8261
|
|
|
8262
|
+
/*
|
|
8263
|
+
* ⭐⭐ P18 fase 2 (owner 26/09/2026, patch approvata dalla lane desktop) — le `cerca` della STESSA risposta
|
|
8264
|
+
* partono INSIEME, fino a `MAX_CERCA_INSIEME` (Hermes 8, `_MAX_TOOL_WORKERS`; Claude Code 10, i blocchi
|
|
8265
|
+
* «concurrency-safe» dello `StreamingToolExecutor`). Il ciclo qui sotto resta quello di sempre: hook,
|
|
8266
|
+
* approvazioni, eventi `tool-inizio`/`tool-esito` e risultati nella storia, tutto nell'ordine delle chiamate;
|
|
8267
|
+
* il ramo `cerca` aspetta la SUA ricerca già partita invece di avviarla.
|
|
8268
|
+
* ⛔ Solo `cerca`: e' pura (legge dentro la cartella, non chiede approvazioni) e un rifiuto del pre-hook su una
|
|
8269
|
+
* lettura e' gia' ignorato (vedi FASE A qui sotto). `leggi` no: un percorso fuori dal progetto chiede
|
|
8270
|
+
* l'approvazione, e quella va chiesta in ordine, prima di leggere.
|
|
8271
|
+
* ⛔ Lo Stop ferma anche quelle in volo: il segnale arriva fino a rg, che viene ucciso.
|
|
8272
|
+
* Misura (banco ermetico, 4 `cerca` su 6.000 file): 8.648 ms prima di P18, 377 con ripgrep in fila.
|
|
8273
|
+
*/
|
|
8274
|
+
const ricercheAvviate = new Map()
|
|
8275
|
+
const daCercare = chiamate.filter((c) => c.function?.name === 'cerca')
|
|
8276
|
+
if (daCercare.length > 1 && !segnaleStop?.aborted) {
|
|
8277
|
+
for (const c of daCercare.slice(0, MAX_CERCA_INSIEME)) {
|
|
8278
|
+
let argomentiCerca = {}
|
|
8279
|
+
try { argomentiCerca = JSON.parse(c.function?.arguments || '{}') } catch { /* vuoto: il ramo dira' cosa manca */ }
|
|
8280
|
+
const avviata = cercaNelProgetto(disco, argomentiCerca, { radice: cartella, segnale: segnaleStop })
|
|
8281
|
+
avviata.catch(() => {})
|
|
8282
|
+
ricercheAvviate.set(c, avviata)
|
|
8283
|
+
}
|
|
8284
|
+
}
|
|
8285
|
+
|
|
8139
8286
|
let fermatoDentroIlGiro = false
|
|
8140
8287
|
for (const c of chiamate) {
|
|
8141
8288
|
/*
|
|
@@ -8232,7 +8379,7 @@ export async function talosLavora({
|
|
|
8232
8379
|
* potare. Senza, `cerca` ricade sulla lista fissa — che e' il ripiego per il ponte
|
|
8233
8380
|
* del telefono e per i test, non il caso normale.
|
|
8234
8381
|
*/
|
|
8235
|
-
esito = await cercaNelProgetto(disco, argomenti, { radice: cartella })
|
|
8382
|
+
esito = await (ricercheAvviate.get(c) ?? cercaNelProgetto(disco, argomenti, { radice: cartella, segnale: segnaleStop }))
|
|
8236
8383
|
}
|
|
8237
8384
|
else if (nome === 'leggi') {
|
|
8238
8385
|
/*
|
package/vendor/manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schema": "talos.cli.vendor.v1",
|
|
3
|
-
"sourceCommit": "
|
|
3
|
+
"sourceCommit": "9a10705c95b865e72c431f8d8ad7873bcdd430b5",
|
|
4
4
|
"files": 163,
|
|
5
5
|
"sha256": {
|
|
6
6
|
"context-engine/package.json": "cd416c2ef02241365eca721ce1bed7a702cff34bb804d79e974f9f36731ced8a",
|
|
@@ -73,7 +73,7 @@
|
|
|
73
73
|
"harness-ui/src/image-generator.mjs": "8762739d578f81d1b881d24338518480e59fcae5b8a3ff6a071947652bec8008",
|
|
74
74
|
"harness-ui/src/istruzioni-di-progetto.mjs": "a0c2c9dec9d119799e3bc02b1aee99ffa996d2c141ba679345f7309495a29cff",
|
|
75
75
|
"harness-ui/src/kernel/dist/kernelPerIlBanco.js": "ff75a3e93b6c6d6d838331016c5eb2d1e6acfea85ffc0609d2f531f07b202a54",
|
|
76
|
-
"harness-ui/src/kernel/talosHarness.mjs": "
|
|
76
|
+
"harness-ui/src/kernel/talosHarness.mjs": "c03a8d1e155a8f4f31793e687f7b26e5ee625ff3a3dcfc737dde6f8cb0fca32a",
|
|
77
77
|
"harness-ui/src/library-policy-store.mjs": "b0295e20d7348cc13cb23a8ad2a0b577fe275b4778b94e102ceeb4fb1bb4c70b",
|
|
78
78
|
"harness-ui/src/library-store.mjs": "b7a0333da5783361536509f9108e1293b1e8c22bddfb02fd5581a4e1ed4bcd4f",
|
|
79
79
|
"harness-ui/src/llama-server-supervisor.mjs": "d59cc22a0e7875b4deeb4a4eac1daa55c3f524db7f5f9bd1f5e58f2c4be261e3",
|