kritya 0.8.23-beta → 0.8.24-beta
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 +2 -1
- package/dist/agent/loop.js +11 -0
- package/dist/agent/toolExecutor.js +16 -2
- package/dist/audit/audit.js +2 -2
- package/dist/config/config.js +7 -0
- package/dist/config/debug.js +33 -0
- package/dist/engine.js +10 -5
- package/dist/headless.js +11 -6
- package/dist/index.js +19 -9
- package/dist/provider/switchyardClient.js +1 -0
- package/dist/session/store.js +4 -4
- package/dist/telemetry/metrics.js +7 -4
- package/dist/telemetry/otlp.js +4 -4
- package/dist/telemetry/tracer.js +2 -2
- package/dist/ui/App.js +5 -4
- package/dist/ui/StatusLine.js +10 -5
- package/dist/ui/useAgent.js +37 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -50,7 +50,8 @@ review → fix` for building something new end-to-end
|
|
|
50
50
|
state
|
|
51
51
|
- **Office docs & notebooks** — read/write Word, Excel, PowerPoint, PDF,
|
|
52
52
|
Jupyter
|
|
53
|
-
- **Headless/CI mode** — scriptable, no-TTY runs for automation
|
|
53
|
+
- **Headless/CI mode** — scriptable, no-TTY runs for automation
|
|
54
|
+
- **Privacy mode** — `--privacy`, `KRITYA_PRIVACY=1`, or `"privacyMode": true` disables transcript, audit, and telemetry persistence
|
|
54
55
|
|
|
55
56
|
## Setup
|
|
56
57
|
|
package/dist/agent/loop.js
CHANGED
|
@@ -78,6 +78,14 @@ export class Agent {
|
|
|
78
78
|
audit;
|
|
79
79
|
/** OpenTelemetry-shaped tracer for the tool loop. No-op unless enabled. */
|
|
80
80
|
tracer = NOOP_TRACER;
|
|
81
|
+
/**
|
|
82
|
+
* Fired around compact() so the UI can show a live "compacting…" indicator
|
|
83
|
+
* for the summarization call, which otherwise runs silently until it
|
|
84
|
+
* either finishes or fails (see onToolEnd's "compact" record for the
|
|
85
|
+
* after-the-fact summary).
|
|
86
|
+
*/
|
|
87
|
+
onCompactStart;
|
|
88
|
+
onCompactEnd;
|
|
81
89
|
/** OTLP metrics recorder for tool/turn durations. No-op unless enabled. */
|
|
82
90
|
meter = NOOP_METER;
|
|
83
91
|
/**
|
|
@@ -266,6 +274,7 @@ export class Agent {
|
|
|
266
274
|
this.kill.assertLive();
|
|
267
275
|
const compactSpan = this.tracer.startSpan("agent.compact", { parent: this.currentTurnSpan });
|
|
268
276
|
const tokensBefore = this.lastPromptTokens;
|
|
277
|
+
this.onCompactStart?.();
|
|
269
278
|
try {
|
|
270
279
|
const note = await this.doCompact(signal, compactSpan);
|
|
271
280
|
compactSpan.setStatus("OK");
|
|
@@ -279,6 +288,7 @@ export class Agent {
|
|
|
279
288
|
compactSpan.setAttribute("kritya.prompt_tokens_before", tokensBefore);
|
|
280
289
|
compactSpan.setAttribute("kritya.prompt_tokens_after", this.lastPromptTokens);
|
|
281
290
|
compactSpan.end();
|
|
291
|
+
this.onCompactEnd?.();
|
|
282
292
|
}
|
|
283
293
|
}
|
|
284
294
|
async doCompact(signal, compactSpan) {
|
|
@@ -444,6 +454,7 @@ export class Agent {
|
|
|
444
454
|
onTextDelta: handlers.onTextDelta,
|
|
445
455
|
onReasoningDelta: handlers.onReasoningDelta,
|
|
446
456
|
onRetry: handlers.onRetry,
|
|
457
|
+
onFallback: handlers.onFallback,
|
|
447
458
|
}, signal, { tracer: this.tracer, parent: this.currentTurnSpan });
|
|
448
459
|
let result;
|
|
449
460
|
try {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { classifyDanger } from "../permissions/danger.js";
|
|
2
|
-
import { acknowledgeUnsandboxedFallback, sandboxFallbackWarning } from "../shell/sandbox.js";
|
|
2
|
+
import { acknowledgeUnsandboxedFallback, sandboxAvailable, sandboxFallbackWarning, } from "../shell/sandbox.js";
|
|
3
3
|
import { isPlanningDocWrite, loadProjectState } from "./workflow.js";
|
|
4
4
|
import { redactSecrets } from "../tools/secretScan.js";
|
|
5
5
|
/** How much tool output to hand the UI (it shows a preview and expands on toggle). */
|
|
@@ -251,7 +251,21 @@ export class ToolExecutor {
|
|
|
251
251
|
diff = undefined;
|
|
252
252
|
}
|
|
253
253
|
}
|
|
254
|
-
const
|
|
254
|
+
const permissionDetails = [
|
|
255
|
+
summary,
|
|
256
|
+
`workspace: ${host.ctx.workspace}`,
|
|
257
|
+
typeof args.path === "string" ? `affected path: ${args.path}` : undefined,
|
|
258
|
+
tool.name === "shell"
|
|
259
|
+
? `command: ${shellCommand}\nsandbox: ${host.ctx.sandboxMode === "off"
|
|
260
|
+
? "off"
|
|
261
|
+
: sandboxAvailable()
|
|
262
|
+
? `${host.ctx.sandboxMode ?? "auto"} (available)`
|
|
263
|
+
: `${host.ctx.sandboxMode ?? "auto"} (unavailable; unsandboxed)`}`
|
|
264
|
+
: undefined,
|
|
265
|
+
]
|
|
266
|
+
.filter(Boolean)
|
|
267
|
+
.join("\n");
|
|
268
|
+
const decision = await handlers.requestPermission(tool.name, permissionDetails, diff, danger ?? undefined);
|
|
255
269
|
// A forced (danger) prompt does not grant a lasting allowance.
|
|
256
270
|
if (danger === null)
|
|
257
271
|
host.permissions.record(tool.name, decision, args);
|
package/dist/audit/audit.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { CONFIG_DIR } from "../config/config.js";
|
|
5
5
|
import { hardenWindowsDir } from "../config/winAcl.js";
|
|
6
|
-
import { debugLog,
|
|
6
|
+
import { debugLog, warnPersistenceFailure } from "../config/debug.js";
|
|
7
7
|
const GENESIS = "0".repeat(64);
|
|
8
8
|
/**
|
|
9
9
|
* Where all audit logs live, across every workspace (not scoped per-project).
|
|
@@ -108,7 +108,7 @@ export class AuditLog {
|
|
|
108
108
|
}
|
|
109
109
|
catch (err) {
|
|
110
110
|
// best-effort
|
|
111
|
-
|
|
111
|
+
warnPersistenceFailure(`AuditLog.write(${this.file})`, err);
|
|
112
112
|
}
|
|
113
113
|
}
|
|
114
114
|
ensureDir() {
|
package/dist/config/config.js
CHANGED
|
@@ -5,6 +5,13 @@ import { hardenWindowsDir } from "./winAcl.js";
|
|
|
5
5
|
import { debugLog, warnUser } from "./debug.js";
|
|
6
6
|
import { writeFileAtomicSync } from "../atomicWrite.js";
|
|
7
7
|
import { assertJsonWithinLimits } from "./jsonSafety.js";
|
|
8
|
+
/** KRITYA_PRIVACY wins over config.json; truthy values enable privacy mode. */
|
|
9
|
+
export function privacyModeFor(config) {
|
|
10
|
+
const value = process.env.KRITYA_PRIVACY;
|
|
11
|
+
if (value !== undefined)
|
|
12
|
+
return /^(1|true|yes|on)$/i.test(value.trim());
|
|
13
|
+
return config.privacyMode === true;
|
|
14
|
+
}
|
|
8
15
|
/**
|
|
9
16
|
* Bounds on `mcpServers` entries (from config.json or a workspace's
|
|
10
17
|
* .mcp.json) — without these, an oversized or malicious server list could
|
package/dist/config/debug.js
CHANGED
|
@@ -47,3 +47,36 @@ export function warnUser(context, err) {
|
|
|
47
47
|
}
|
|
48
48
|
debugLog(context, err);
|
|
49
49
|
}
|
|
50
|
+
/** Contexts currently warning via warnPersistenceFailure, in the order first seen. */
|
|
51
|
+
const persistenceWarnings = [];
|
|
52
|
+
const persistenceListeners = new Set();
|
|
53
|
+
function notifyPersistenceListeners() {
|
|
54
|
+
const snapshot = [...persistenceWarnings];
|
|
55
|
+
for (const listener of persistenceListeners)
|
|
56
|
+
listener(snapshot);
|
|
57
|
+
}
|
|
58
|
+
/** Current persistence-failure warnings (session/audit/telemetry writes that failed this process). */
|
|
59
|
+
export function activePersistenceWarnings() {
|
|
60
|
+
return [...persistenceWarnings];
|
|
61
|
+
}
|
|
62
|
+
/** Subscribe to persistence-warning changes. Returns an unsubscribe function. */
|
|
63
|
+
export function onPersistenceWarning(listener) {
|
|
64
|
+
persistenceListeners.add(listener);
|
|
65
|
+
return () => persistenceListeners.delete(listener);
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Like warnUser, but for the specific best-effort writes whose failure means
|
|
69
|
+
* silent data loss (session transcripts, audit log, telemetry) — a stderr
|
|
70
|
+
* line alone is easy to miss since Ink's full-screen redraws can immediately
|
|
71
|
+
* paint over it. Recorded once per context, same dedup as warnUser, so the
|
|
72
|
+
* UI badge doesn't grow without bound from a write that fails every turn.
|
|
73
|
+
*/
|
|
74
|
+
export function warnPersistenceFailure(context, err) {
|
|
75
|
+
const isNewContext = !warnedContexts.has(context);
|
|
76
|
+
warnUser(context, err);
|
|
77
|
+
if (isNewContext) {
|
|
78
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
79
|
+
persistenceWarnings.push({ context, message });
|
|
80
|
+
notifyPersistenceListeners();
|
|
81
|
+
}
|
|
82
|
+
}
|
package/dist/engine.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { Agent } from "./agent/loop.js";
|
|
3
|
-
import { CONFIG_DIR, legacyGlobalModel, loadConfig, loadDotEnv, resolveProvider, } from "./config/config.js";
|
|
3
|
+
import { CONFIG_DIR, legacyGlobalModel, loadConfig, loadDotEnv, privacyModeFor, resolveProvider, } from "./config/config.js";
|
|
4
4
|
import { DEFAULT_MODEL, contextWindowFor } from "./config/models.js";
|
|
5
5
|
import { PermissionManager } from "./permissions/permissions.js";
|
|
6
6
|
import { loadRules } from "./permissions/rules.js";
|
|
@@ -35,6 +35,7 @@ export async function createEngineSession(dir, opts = {}) {
|
|
|
35
35
|
if (trustWorkspace)
|
|
36
36
|
loadDotEnv([path.join(workspace, ".env")]);
|
|
37
37
|
const config = loadConfig();
|
|
38
|
+
const privacyMode = privacyModeFor(config);
|
|
38
39
|
const provider = resolveProvider(config, opts.provider);
|
|
39
40
|
if (!provider.apiKey) {
|
|
40
41
|
throw new Error(`No API key found for provider "${provider.name}". Set it via env var, a .env file, or ~/.kritya/config.json.`);
|
|
@@ -49,9 +50,11 @@ export async function createEngineSession(dir, opts = {}) {
|
|
|
49
50
|
const client = provider.name === "switchyard"
|
|
50
51
|
? await createSwitchyardClient(provider.apiKey, sampling)
|
|
51
52
|
: new ProviderClient(provider.apiKey, provider.baseUrl, sampling);
|
|
52
|
-
const session = new SessionStore(workspace);
|
|
53
|
+
const session = new SessionStore(workspace, privacyMode);
|
|
53
54
|
session.start([]);
|
|
54
|
-
const sessionMeter =
|
|
55
|
+
const sessionMeter = privacyMode
|
|
56
|
+
? createMeter(session.id, "off")
|
|
57
|
+
: createMeter(session.id, config.otel);
|
|
55
58
|
// The crash path is fire-and-forget best-effort by Node's own constraints
|
|
56
59
|
// (a crash handler can't reliably await async work), so this uses the
|
|
57
60
|
// synchronous flush() rather than flushAndWait().
|
|
@@ -79,8 +82,10 @@ export async function createEngineSession(dir, opts = {}) {
|
|
|
79
82
|
if (trustWorkspace) {
|
|
80
83
|
agent.hooks = new HookRunner(loadHooks(workspace, trustWorkspace), workspace);
|
|
81
84
|
}
|
|
82
|
-
agent.audit = AuditLog.forSession(session.id, config.audit);
|
|
83
|
-
agent.tracer =
|
|
85
|
+
agent.audit = privacyMode ? undefined : AuditLog.forSession(session.id, config.audit);
|
|
86
|
+
agent.tracer = privacyMode
|
|
87
|
+
? createTracer(session.id, "off")
|
|
88
|
+
: createTracer(session.id, config.otel);
|
|
84
89
|
agent.meter = sessionMeter;
|
|
85
90
|
if (agent.hooks)
|
|
86
91
|
agent.hooks.tracer = agent.tracer;
|
package/dist/headless.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from "node:path";
|
|
2
2
|
import { Agent } from "./agent/loop.js";
|
|
3
3
|
import { KillSwitchError } from "./agent/killSwitch.js";
|
|
4
|
-
import { CONFIG_DIR, legacyGlobalModel, listProviders, loadConfig, loadDotEnv, resolveProvider, } from "./config/config.js";
|
|
4
|
+
import { CONFIG_DIR, legacyGlobalModel, listProviders, loadConfig, loadDotEnv, privacyModeFor, resolveProvider, } from "./config/config.js";
|
|
5
5
|
import { DEFAULT_MODEL, contextWindowFor } from "./config/models.js";
|
|
6
6
|
import { PermissionManager } from "./permissions/permissions.js";
|
|
7
7
|
import { loadRules } from "./permissions/rules.js";
|
|
@@ -78,6 +78,7 @@ export async function runHeadless(args) {
|
|
|
78
78
|
if (trustWorkspace)
|
|
79
79
|
loadDotEnv([path.join(workspace, ".env")]);
|
|
80
80
|
const config = loadConfig();
|
|
81
|
+
const privacyMode = args.privacy === true || privacyModeFor(config);
|
|
81
82
|
const provider = resolveProvider(config, args.provider || undefined);
|
|
82
83
|
if (!provider.apiKey) {
|
|
83
84
|
return finish(args, startedAt, {
|
|
@@ -102,12 +103,16 @@ export async function runHeadless(args) {
|
|
|
102
103
|
const client = provider.name === "switchyard"
|
|
103
104
|
? await createSwitchyardClient(provider.apiKey, sampling)
|
|
104
105
|
: new ProviderClient(provider.apiKey, provider.baseUrl, sampling);
|
|
105
|
-
const session = new SessionStore(workspace);
|
|
106
|
-
const initialHistory = args.continue ? (SessionStore.loadLatest(workspace) ?? []) : [];
|
|
106
|
+
const session = new SessionStore(workspace, privacyMode);
|
|
107
|
+
const initialHistory = !privacyMode && args.continue ? (SessionStore.loadLatest(workspace) ?? []) : [];
|
|
107
108
|
session.start(initialHistory);
|
|
108
|
-
const sessionAudit = AuditLog.forSession(session.id, config.audit);
|
|
109
|
-
const sessionTracer =
|
|
110
|
-
|
|
109
|
+
const sessionAudit = privacyMode ? undefined : AuditLog.forSession(session.id, config.audit);
|
|
110
|
+
const sessionTracer = privacyMode
|
|
111
|
+
? createTracer(session.id, "off")
|
|
112
|
+
: createTracer(session.id, config.otel);
|
|
113
|
+
const sessionMeter = privacyMode
|
|
114
|
+
? createMeter(session.id, "off")
|
|
115
|
+
: createMeter(session.id, config.otel);
|
|
111
116
|
// A crash here orphans MCP children and background processes onto a CI
|
|
112
117
|
// runner, where nothing will ever reap them. No terminal to restore. The
|
|
113
118
|
// crash path is fire-and-forget best-effort by Node's own constraints (a
|
package/dist/index.js
CHANGED
|
@@ -4,7 +4,7 @@ import fs from "node:fs";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { render } from "ink";
|
|
6
6
|
import { Agent } from "./agent/loop.js";
|
|
7
|
-
import { CONFIG_DIR, legacyGlobalModel, loadConfig, loadDotEnv, resolveProvider, } from "./config/config.js";
|
|
7
|
+
import { CONFIG_DIR, legacyGlobalModel, loadConfig, loadDotEnv, privacyModeFor, resolveProvider, } from "./config/config.js";
|
|
8
8
|
import { DEFAULT_MODEL, contextWindowFor } from "./config/models.js";
|
|
9
9
|
import { PermissionManager } from "./permissions/permissions.js";
|
|
10
10
|
import { loadRules } from "./permissions/rules.js";
|
|
@@ -65,6 +65,7 @@ Headless / CI mode (no terminal UI, exits with 0 on success / 1 on failure):
|
|
|
65
65
|
since CI often checks out untrusted branches/PRs)
|
|
66
66
|
--timeout <seconds> hard wall-clock cap for the whole run (default 1800)
|
|
67
67
|
--non-interactive accepted for compatibility; implied by --prompt
|
|
68
|
+
--privacy do not persist transcripts, audit logs, or telemetry
|
|
68
69
|
|
|
69
70
|
Inspect the local audit log:
|
|
70
71
|
kritya audit --list | --verify [file] | --show [file]
|
|
@@ -93,6 +94,7 @@ function parseArgs(argv) {
|
|
|
93
94
|
allowAll: false,
|
|
94
95
|
trust: false,
|
|
95
96
|
timeoutSeconds: 1800,
|
|
97
|
+
privacy: false,
|
|
96
98
|
};
|
|
97
99
|
for (let i = 0; i < argv.length; i++) {
|
|
98
100
|
const a = argv[i];
|
|
@@ -124,6 +126,8 @@ function parseArgs(argv) {
|
|
|
124
126
|
args.trust = true;
|
|
125
127
|
else if (a === "--timeout")
|
|
126
128
|
args.timeoutSeconds = Number(argv[++i]) || args.timeoutSeconds;
|
|
129
|
+
else if (a === "--privacy")
|
|
130
|
+
args.privacy = true;
|
|
127
131
|
else if (a === "--non-interactive") {
|
|
128
132
|
// implied by --prompt; accepted so scripts can pass it explicitly
|
|
129
133
|
}
|
|
@@ -176,6 +180,7 @@ if (args.prompt) {
|
|
|
176
180
|
allowAll: args.allowAll,
|
|
177
181
|
trust: args.trust,
|
|
178
182
|
timeoutSeconds: args.timeoutSeconds,
|
|
183
|
+
privacy: args.privacy,
|
|
179
184
|
}).then((code) => process.exit(code));
|
|
180
185
|
}
|
|
181
186
|
else {
|
|
@@ -261,6 +266,7 @@ async function showAiDisclosureNotice() {
|
|
|
261
266
|
}
|
|
262
267
|
async function main() {
|
|
263
268
|
const config = loadConfig();
|
|
269
|
+
const privacyMode = args.privacy || privacyModeFor(config);
|
|
264
270
|
if (!isAiDisclosureShown(workspace)) {
|
|
265
271
|
await showAiDisclosureNotice();
|
|
266
272
|
markAiDisclosureShown(workspace);
|
|
@@ -312,18 +318,22 @@ async function main() {
|
|
|
312
318
|
let client = provider.name === "switchyard"
|
|
313
319
|
? await createSwitchyardClient(apiKey, sampling)
|
|
314
320
|
: new ProviderClient(apiKey, provider.baseUrl, sampling);
|
|
315
|
-
const session = new SessionStore(workspace);
|
|
321
|
+
const session = new SessionStore(workspace, privacyMode);
|
|
316
322
|
// Shared by the main agent and every subagent it spawns, so a write
|
|
317
323
|
// subagent's commits and a read-only subagent's tool calls land in the same
|
|
318
324
|
// audit trail and trace tree as the turn that spawned them — an agent that
|
|
319
325
|
// edits the repo should never do so off the record.
|
|
320
|
-
const sessionAudit = AuditLog.forSession(session.id, config.audit);
|
|
321
|
-
const sessionTracer =
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
const
|
|
326
|
+
const sessionAudit = privacyMode ? undefined : AuditLog.forSession(session.id, config.audit);
|
|
327
|
+
const sessionTracer = privacyMode
|
|
328
|
+
? createTracer(session.id, "off")
|
|
329
|
+
: createTracer(session.id, config.otel);
|
|
330
|
+
const sessionMeter = privacyMode
|
|
331
|
+
? createMeter(session.id, "off")
|
|
332
|
+
: createMeter(session.id, config.otel);
|
|
333
|
+
const initialHistory = !privacyMode && args.continue ? (SessionStore.loadLatest(workspace) ?? []) : [];
|
|
334
|
+
const initialTasks = !privacyMode && args.continue ? SessionStore.loadLatestTasks(workspace) : [];
|
|
325
335
|
session.start(initialHistory);
|
|
326
|
-
const resumeSessions = args.resume ? SessionStore.listSessions(workspace) : [];
|
|
336
|
+
const resumeSessions = !privacyMode && args.resume ? SessionStore.listSessions(workspace) : [];
|
|
327
337
|
// Filled in once the app is mounted, below; the crash handler needs a way to
|
|
328
338
|
// tear the UI down and is installed before there is a UI to tear down.
|
|
329
339
|
const ui = {};
|
|
@@ -677,5 +687,5 @@ async function main() {
|
|
|
677
687
|
permissionRef.current = fn;
|
|
678
688
|
}, onRequestElicitationReady: (fn) => {
|
|
679
689
|
elicitationRef.current = fn;
|
|
680
|
-
} }));
|
|
690
|
+
}, privacyMode: privacyMode }));
|
|
681
691
|
}
|
|
@@ -29,6 +29,7 @@ export class SwitchyardProviderClient extends ProviderClient {
|
|
|
29
29
|
let lastErr = err;
|
|
30
30
|
for (const fb of this.fallbacks) {
|
|
31
31
|
try {
|
|
32
|
+
callbacks.onFallback?.("switchyard", fb.model);
|
|
32
33
|
return await fb.client.chat(fb.model, messages, tools, callbacks, signal, trace);
|
|
33
34
|
}
|
|
34
35
|
catch (fbErr) {
|
package/dist/session/store.js
CHANGED
|
@@ -4,7 +4,7 @@ import path from "node:path";
|
|
|
4
4
|
import { writeFileAtomicSync } from "../atomicWrite.js";
|
|
5
5
|
import { CONFIG_DIR } from "../config/config.js";
|
|
6
6
|
import { hardenWindowsDir } from "../config/winAcl.js";
|
|
7
|
-
import { debugLog,
|
|
7
|
+
import { debugLog, warnPersistenceFailure } from "../config/debug.js";
|
|
8
8
|
/**
|
|
9
9
|
* A session file loaded whole into memory has no upper bound otherwise —
|
|
10
10
|
* `--continue` on a pathologically large transcript (corruption, a runaway
|
|
@@ -127,7 +127,7 @@ export class SessionStore {
|
|
|
127
127
|
writeSessionFile(this.tasksFilePath(), JSON.stringify(tasks));
|
|
128
128
|
}
|
|
129
129
|
catch (err) {
|
|
130
|
-
|
|
130
|
+
warnPersistenceFailure(`SessionStore.saveTasks(${this.tasksFilePath()})`, err);
|
|
131
131
|
}
|
|
132
132
|
}
|
|
133
133
|
/** Loads the task checklist saved alongside a given session file, if any. */
|
|
@@ -178,7 +178,7 @@ export class SessionStore {
|
|
|
178
178
|
}
|
|
179
179
|
catch (err) {
|
|
180
180
|
// Persistence is best-effort; never crash the session over it.
|
|
181
|
-
|
|
181
|
+
warnPersistenceFailure(`SessionStore.append(${this.file})`, err);
|
|
182
182
|
}
|
|
183
183
|
}
|
|
184
184
|
/** Start over with a fresh session file (used by /clear). */
|
|
@@ -201,7 +201,7 @@ export class SessionStore {
|
|
|
201
201
|
}
|
|
202
202
|
catch (err) {
|
|
203
203
|
// Persistence is best-effort; never crash the session over it.
|
|
204
|
-
|
|
204
|
+
warnPersistenceFailure(`SessionStore.overwrite(${this.file})`, err);
|
|
205
205
|
}
|
|
206
206
|
}
|
|
207
207
|
/**
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { VERSION } from "../version.js";
|
|
2
|
+
import { warnPersistenceFailure } from "../config/debug.js";
|
|
2
3
|
import { encodeMetricsSnapshot, postOtlp, postOtlpAndWait, } from "./otlp.js";
|
|
3
4
|
const NOOP_COUNTER = { add() { } };
|
|
4
5
|
const NOOP_HISTOGRAM = { record() { } };
|
|
@@ -113,8 +114,10 @@ class RealMeter {
|
|
|
113
114
|
return;
|
|
114
115
|
postOtlp(this.endpoint, "/v1/metrics", body, this.headers);
|
|
115
116
|
}
|
|
116
|
-
catch {
|
|
117
|
-
// best-effort: metrics must never crash the process
|
|
117
|
+
catch (err) {
|
|
118
|
+
// best-effort: metrics must never crash the process, but a dropped
|
|
119
|
+
// export should still be visible to the user.
|
|
120
|
+
warnPersistenceFailure("metrics.flush", err);
|
|
118
121
|
}
|
|
119
122
|
}
|
|
120
123
|
async flushAndWait() {
|
|
@@ -124,8 +127,8 @@ class RealMeter {
|
|
|
124
127
|
return;
|
|
125
128
|
await postOtlpAndWait(this.endpoint, "/v1/metrics", body, this.headers);
|
|
126
129
|
}
|
|
127
|
-
catch {
|
|
128
|
-
|
|
130
|
+
catch (err) {
|
|
131
|
+
warnPersistenceFailure("metrics.flushAndWait", err);
|
|
129
132
|
}
|
|
130
133
|
}
|
|
131
134
|
stop() {
|
package/dist/telemetry/otlp.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { warnPersistenceFailure } from "../config/debug.js";
|
|
2
2
|
import { assertSafeUrl, pinnedDispatcherAllowLoopback, } from "../net/urlSafety.js";
|
|
3
3
|
function toAnyValue(value) {
|
|
4
4
|
if (typeof value === "string")
|
|
@@ -109,10 +109,10 @@ export function postOtlp(endpoint, path, body, headers) {
|
|
|
109
109
|
body: JSON.stringify(body),
|
|
110
110
|
dispatcher: pinnedDispatcherAllowLoopback,
|
|
111
111
|
};
|
|
112
|
-
fetch(url.href, init).catch((err) =>
|
|
112
|
+
fetch(url.href, init).catch((err) => warnPersistenceFailure(`postOtlp(${path})`, err));
|
|
113
113
|
}
|
|
114
114
|
catch (err) {
|
|
115
|
-
|
|
115
|
+
warnPersistenceFailure(`postOtlp(${path})`, err);
|
|
116
116
|
}
|
|
117
117
|
}
|
|
118
118
|
/**
|
|
@@ -134,6 +134,6 @@ export async function postOtlpAndWait(endpoint, path, body, headers) {
|
|
|
134
134
|
await fetch(url.href, init);
|
|
135
135
|
}
|
|
136
136
|
catch (err) {
|
|
137
|
-
|
|
137
|
+
warnPersistenceFailure(`postOtlpAndWait(${path})`, err);
|
|
138
138
|
}
|
|
139
139
|
}
|
package/dist/telemetry/tracer.js
CHANGED
|
@@ -3,7 +3,7 @@ import fs from "node:fs";
|
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { CONFIG_DIR } from "../config/config.js";
|
|
5
5
|
import { hardenWindowsDir } from "../config/winAcl.js";
|
|
6
|
-
import { debugLog,
|
|
6
|
+
import { debugLog, warnPersistenceFailure } from "../config/debug.js";
|
|
7
7
|
import { VERSION } from "../version.js";
|
|
8
8
|
import { encodeSpan, postOtlp } from "./otlp.js";
|
|
9
9
|
function nowUnixNano() {
|
|
@@ -158,7 +158,7 @@ function fileSink(file) {
|
|
|
158
158
|
}
|
|
159
159
|
catch (err) {
|
|
160
160
|
// best-effort: telemetry must never crash a turn
|
|
161
|
-
|
|
161
|
+
warnPersistenceFailure(`tracer.fileSink(${file})`, err);
|
|
162
162
|
}
|
|
163
163
|
};
|
|
164
164
|
}
|
package/dist/ui/App.js
CHANGED
|
@@ -30,7 +30,7 @@ const IMAGE_RE = /\.(png|jpe?g|gif|webp|bmp)$/i;
|
|
|
30
30
|
// Erase screen + scrollback, then home the cursor (same sequence Ink itself
|
|
31
31
|
// uses for its own "content taller than the terminal" clear path).
|
|
32
32
|
const CLEAR_TERMINAL = "\x1b[2J\x1b[3J\x1b[H";
|
|
33
|
-
export function App({ agent, workspace, modelRef, providerRef, config, resumedCount, initialTasks, undoStack, uiBridge, resumeSessions, customCommands = [], mcpToolCount = 0, onSwitchClient, onRequestPermissionReady, onRequestElicitationReady, }) {
|
|
33
|
+
export function App({ agent, workspace, modelRef, providerRef, config, resumedCount, initialTasks, undoStack, uiBridge, resumeSessions, customCommands = [], mcpToolCount = 0, onSwitchClient, onRequestPermissionReady, onRequestElicitationReady, privacyMode, }) {
|
|
34
34
|
const { exit } = useApp();
|
|
35
35
|
const { stdout } = useStdout();
|
|
36
36
|
const [input, setInput] = useState("");
|
|
@@ -125,7 +125,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
125
125
|
.catch(() => { });
|
|
126
126
|
}, [workspace]);
|
|
127
127
|
useEffect(refreshFileList, [refreshFileList]);
|
|
128
|
-
const { items, addItem, phase, setPhase, stream, thinking, activity, setActivity, runningPhase, setRunningPhase, workflow, refreshWorkflow, permission, elicitation, inFlight, model, provider, servedModel, totalUsage, totalCost, tasks, setTasks, ctxPct, setCtxPct, tokenBudget, budgetPct, budgetUsed, budgetStopped, resetBudget, setBudgetLimit, branch, planMode, setPlanMode, dryRunMode, killed, killReason, engageKill, releaseKill, acceptEdits, setAcceptEdits, autoApprovedCount, cycleMode, onAcceptEditsConfirm, abortRef, setModelEverywhere, setProviderEverywhere, costReport, runAgent, runWebSearch, onPermissionDecision, onElicitationDecision, onResumeSelect, requestPermission, requestElicitation, } = useAgent({
|
|
128
|
+
const { items, addItem, phase, setPhase, stream, thinking, activity, setActivity, runningPhase, setRunningPhase, workflow, refreshWorkflow, permission, elicitation, inFlight, model, provider, servedModel, totalUsage, totalCost, tasks, setTasks, ctxPct, setCtxPct, tokenBudget, budgetPct, budgetUsed, budgetStopped, resetBudget, setBudgetLimit, branch, planMode, setPlanMode, dryRunMode, killed, killReason, engageKill, releaseKill, acceptEdits, setAcceptEdits, autoApprovedCount, cycleMode, onAcceptEditsConfirm, abortRef, setModelEverywhere, setProviderEverywhere, costReport, runAgent, runWebSearch, onPermissionDecision, onElicitationDecision, onResumeSelect, requestPermission, requestElicitation, persistenceWarningCount, } = useAgent({
|
|
129
129
|
agent,
|
|
130
130
|
workspace,
|
|
131
131
|
modelRef,
|
|
@@ -195,6 +195,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
195
195
|
return;
|
|
196
196
|
}
|
|
197
197
|
if (key.escape && phase === "working") {
|
|
198
|
+
setActivity("cancelling…");
|
|
198
199
|
abortRef.current?.abort();
|
|
199
200
|
}
|
|
200
201
|
// Ctrl+O toggles showing full tool output.
|
|
@@ -444,7 +445,7 @@ export function App({ agent, workspace, modelRef, providerRef, config, resumedCo
|
|
|
444
445
|
setInput((prev) => (isCtrlEcho(prev, v) ? prev : v.replace(/\t/g, "")));
|
|
445
446
|
setCmdIndex(0);
|
|
446
447
|
setFileIndex(0);
|
|
447
|
-
}, onSubmit: handleSubmit }, inputKey)] }), suggestions.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [suggestions.map((c, i) => (_jsxs(Text, { color: i === selectedCmd ? "cyan" : undefined, children: [i === selectedCmd ? "❯ " : " ", _jsx(Text, { bold: i === selectedCmd, children: c.name }), _jsxs(Text, { dimColor: true, children: [" \u2014 ", c.description] })] }, c.name))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u00B7 Tab/Enter select \u00B7 Enter again to run" })] })), fileSuggestions.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [fileSuggestions.map((f, i) => (_jsxs(Text, { color: i === selectedFile ? "cyan" : undefined, children: [i === selectedFile ? "❯ " : " ", f] }, f))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u00B7 Tab/Enter attach file" })] }))] })), _jsx(Box, { children: _jsx(StatusLine, { killed: killed, killReason: killReason, dryRunMode: dryRunMode, planMode: planMode, acceptEdits: acceptEdits, autoApprovedCount: autoApprovedCount, model: servedModel && servedModel !== model
|
|
448
|
+
}, onSubmit: handleSubmit }, inputKey)] }), suggestions.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [suggestions.map((c, i) => (_jsxs(Text, { color: i === selectedCmd ? "cyan" : undefined, children: [i === selectedCmd ? "❯ " : " ", _jsx(Text, { bold: i === selectedCmd, children: c.name }), _jsxs(Text, { dimColor: true, children: [" \u2014 ", c.description] })] }, c.name))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u00B7 Tab/Enter select \u00B7 Enter again to run" })] })), fileSuggestions.length > 0 && (_jsxs(Box, { flexDirection: "column", paddingLeft: 2, children: [fileSuggestions.map((f, i) => (_jsxs(Text, { color: i === selectedFile ? "cyan" : undefined, children: [i === selectedFile ? "❯ " : " ", f] }, f))), _jsx(Text, { dimColor: true, children: "\u2191\u2193 select \u00B7 Tab/Enter attach file" })] }))] })), _jsx(Box, { children: _jsx(StatusLine, { killed: killed, killReason: killReason, dryRunMode: dryRunMode, planMode: planMode, acceptEdits: acceptEdits, autoApprovedCount: autoApprovedCount, provider: provider, model: servedModel && servedModel !== model
|
|
448
449
|
? `${model}-${modelDisplaySlug(servedModel)}`
|
|
449
|
-
: model, workflow: workflow, branch: branch, tasks: tasks, ctxPct: ctxPct, budgetPct: budgetPct, budgetStopped: budgetStopped, phase: phase, elapsed: elapsed, totalUsage: totalUsage, totalCost: totalCost, verbose: verbose, workspace: workspace, sandboxActive: (config.sandboxExec ?? defaultSandboxMode()) !== "off" && sandboxAvailable() }) })] }));
|
|
450
|
+
: model, workflow: workflow, branch: branch, tasks: tasks, ctxPct: ctxPct, budgetPct: budgetPct, budgetStopped: budgetStopped, phase: phase, elapsed: elapsed, totalUsage: totalUsage, totalCost: totalCost, verbose: verbose, workspace: workspace, sandboxActive: (config.sandboxExec ?? defaultSandboxMode()) !== "off" && sandboxAvailable(), persistenceWarningCount: persistenceWarningCount, privacyMode: privacyMode }) })] }));
|
|
450
451
|
}
|
package/dist/ui/StatusLine.js
CHANGED
|
@@ -1,11 +1,16 @@
|
|
|
1
1
|
import { jsxs as _jsxs, jsx as _jsx } from "react/jsx-runtime";
|
|
2
2
|
import { Text } from "ink";
|
|
3
|
-
import path from "node:path";
|
|
4
3
|
/** The single dim status bar pinned to the bottom of the screen. */
|
|
5
|
-
export function StatusLine({ killed, killReason, dryRunMode, planMode, acceptEdits, autoApprovedCount, model, workflow, branch, tasks, ctxPct, budgetPct, budgetStopped, phase, elapsed, totalUsage, totalCost, verbose, workspace, sandboxActive, }) {
|
|
6
|
-
return (_jsxs(Text, { dimColor: true, children: [killed ? (_jsxs(Text, { bold: true, color: "red", children: ["\u26D4 KILLED", killReason ? ` (${killReason})` : "", " \u00B7", " "] })) : (""),
|
|
4
|
+
export function StatusLine({ killed, killReason, dryRunMode, planMode, acceptEdits, autoApprovedCount, provider, model, workflow, branch, tasks, ctxPct, budgetPct, budgetStopped, phase, elapsed, totalUsage, totalCost, verbose, workspace, sandboxActive, persistenceWarningCount, privacyMode, }) {
|
|
5
|
+
return (_jsxs(Text, { dimColor: true, children: [killed ? (_jsxs(Text, { bold: true, color: "red", children: ["\u26D4 KILLED", killReason ? ` (${killReason})` : "", " \u00B7", " "] })) : (""), _jsxs(Text, { color: dryRunMode || planMode ? "cyan" : acceptEdits ? "green" : undefined, children: ["mode:", " ", dryRunMode
|
|
6
|
+
? "dry-run"
|
|
7
|
+
: planMode
|
|
8
|
+
? "plan"
|
|
9
|
+
: acceptEdits
|
|
10
|
+
? `accept-edits (${autoApprovedCount} auto)`
|
|
11
|
+
: "default", " · "] }), provider, "/", model, _jsxs(Text, { color: sandboxActive ? "green" : "red", children: [" ", "\u00B7 ", sandboxActive ? "🔒 sandbox:active" : "🔓 sandbox:inactive"] }), workflow ? (_jsxs(Text, { color: "magenta", children: [" ", "\u00B7 \u2691 ", workflow.name, ":", workflow.phase] })) : (""), branch ? ` · ⎇ ${branch}` : "", tasks.length > 0
|
|
7
12
|
? ` · tasks ${tasks.filter((t) => t.status === "done").length}/${tasks.length}`
|
|
8
|
-
: "", ctxPct > 0 ? ` · ctx ${ctxPct}%` : "", budgetPct > 0 ? (_jsxs(Text, { color: budgetStopped ? "red" : budgetPct >= 80 ? "yellow" : undefined, children: [" ", "\u00B7 budget ", budgetPct, "%"] })) : (""), phase === "working" && elapsed > 0 ? ` · ${elapsed}s` : "", " \u00B7", " ", totalUsage.estimated ? "~" : "", totalUsage.promptTokens.toLocaleString(), " in", (totalUsage.cachedPromptTokens ?? 0) > 0
|
|
13
|
+
: "", ctxPct > 0 ? ` · ctx ${ctxPct}%` : "", budgetPct > 0 ? (_jsxs(Text, { color: budgetStopped ? "red" : budgetPct >= 80 ? "yellow" : undefined, children: [" ", "\u00B7 budget ", budgetPct, "%"] })) : (""), phase === "working" && elapsed > 0 ? ` · ${elapsed}s` : "", " \u00B7", " ", totalUsage.estimated ? "~" : "", "tokens: ", totalUsage.promptTokens.toLocaleString(), " in", (totalUsage.cachedPromptTokens ?? 0) > 0
|
|
9
14
|
? ` (${Math.round(((totalUsage.cachedPromptTokens ?? 0) / totalUsage.promptTokens) * 100)}% cached)`
|
|
10
|
-
: "", " ", "/ ", totalUsage.completionTokens.toLocaleString(), " out", totalCost > 0 ? ` · $${totalCost.toFixed(4)}` : "", verbose ? " · verbose" : "", " \u00B7 ",
|
|
15
|
+
: "", " ", "/ ", totalUsage.completionTokens.toLocaleString(), " out", totalCost > 0 ? ` · $${totalCost.toFixed(4)}` : "", persistenceWarningCount > 0 ? (_jsxs(Text, { color: "yellow", children: [" \u00B7 \u26A0 persistence warnings: ", persistenceWarningCount] })) : (""), privacyMode ? _jsx(Text, { color: "cyan", children: " \u00B7 privacy:on" }) : "", verbose ? " · verbose" : "", " \u00B7 workspace: ", workspace] }));
|
|
11
16
|
}
|
package/dist/ui/useAgent.js
CHANGED
|
@@ -12,6 +12,18 @@ import { tavilySearch } from "../tools/webSearch.js";
|
|
|
12
12
|
import { killActiveNotice, useKillSwitch } from "./useKillSwitch.js";
|
|
13
13
|
import { useUsageBudget } from "./useUsageBudget.js";
|
|
14
14
|
import { useSessionResume } from "./useSessionResume.js";
|
|
15
|
+
import { activePersistenceWarnings, onPersistenceWarning } from "../config/debug.js";
|
|
16
|
+
function initialToolStatus(name) {
|
|
17
|
+
if (name.startsWith("mcp_"))
|
|
18
|
+
return "contacting MCP server…";
|
|
19
|
+
if (name.startsWith("lsp_"))
|
|
20
|
+
return "querying language server…";
|
|
21
|
+
if (name.includes("document"))
|
|
22
|
+
return "processing document…";
|
|
23
|
+
if (name.includes("notebook"))
|
|
24
|
+
return "processing notebook…";
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
15
27
|
/**
|
|
16
28
|
* Owns everything tied to running agent turns: the transcript, streaming and
|
|
17
29
|
* permission state, usage/cost tracking, and session resume. Input handling
|
|
@@ -44,6 +56,21 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
44
56
|
const [stream, setStream] = useState("");
|
|
45
57
|
const [thinking, setThinking] = useState(false);
|
|
46
58
|
const [activity, setActivity] = useState(null);
|
|
59
|
+
const [persistenceWarningCount, setPersistenceWarningCount] = useState(() => activePersistenceWarnings().length);
|
|
60
|
+
useEffect(() => onPersistenceWarning((warnings) => setPersistenceWarningCount(warnings.length)), []);
|
|
61
|
+
/**
|
|
62
|
+
* Compaction calls the model directly and can take a while, so without this
|
|
63
|
+
* the spinner would sit frozen on whatever it showed before compaction
|
|
64
|
+
* started until onToolEnd's after-the-fact "compact" record appears.
|
|
65
|
+
*/
|
|
66
|
+
useEffect(() => {
|
|
67
|
+
agent.onCompactStart = () => setActivity("compacting context…");
|
|
68
|
+
agent.onCompactEnd = () => setActivity(null);
|
|
69
|
+
return () => {
|
|
70
|
+
agent.onCompactStart = undefined;
|
|
71
|
+
agent.onCompactEnd = undefined;
|
|
72
|
+
};
|
|
73
|
+
}, [agent]);
|
|
47
74
|
/**
|
|
48
75
|
* Which workflow phase the *current turn* is running. Turn-scoped and cleared
|
|
49
76
|
* when the turn ends, unlike `workflow` below — `activity` can't carry this,
|
|
@@ -355,6 +382,7 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
355
382
|
return;
|
|
356
383
|
}
|
|
357
384
|
setPhase("working");
|
|
385
|
+
setActivity(`Calling ${provider}/${model}…`);
|
|
358
386
|
const ac = new AbortController();
|
|
359
387
|
abortRef.current = ac;
|
|
360
388
|
setInFlight([]);
|
|
@@ -372,7 +400,10 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
372
400
|
},
|
|
373
401
|
onToolStart: (id, name, summary) => {
|
|
374
402
|
setStream("");
|
|
375
|
-
setInFlight((prev) => [
|
|
403
|
+
setInFlight((prev) => [
|
|
404
|
+
...prev,
|
|
405
|
+
{ id, name, summary, status: initialToolStatus(name) },
|
|
406
|
+
]);
|
|
376
407
|
},
|
|
377
408
|
onToolEnd: (id, name, summary, preview, isError, resultSummary) => {
|
|
378
409
|
setInFlight((prev) => prev.filter((t) => t.id !== id));
|
|
@@ -397,6 +428,10 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
397
428
|
setStream("");
|
|
398
429
|
setActivity(`Provider error${status ? ` (${status})` : ""} — retrying (attempt ${attempt})…`);
|
|
399
430
|
},
|
|
431
|
+
onFallback: (from, to) => {
|
|
432
|
+
setStream("");
|
|
433
|
+
setActivity(`${from} unavailable — trying fallback model ${to}…`);
|
|
434
|
+
},
|
|
400
435
|
onUsage: (usage) => {
|
|
401
436
|
if (usage.servedModel)
|
|
402
437
|
setServedModel(usage.servedModel);
|
|
@@ -498,6 +533,7 @@ export function useAgent({ agent, workspace, modelRef, providerRef, config, uiBr
|
|
|
498
533
|
totalCost,
|
|
499
534
|
tasks,
|
|
500
535
|
setTasks,
|
|
536
|
+
persistenceWarningCount,
|
|
501
537
|
ctxPct,
|
|
502
538
|
setCtxPct,
|
|
503
539
|
tokenBudget,
|
package/package.json
CHANGED