praxis-agent 0.20.11 → 0.20.16
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/dist/application/scheduled-prompt-manager.d.ts +5 -0
- package/dist/application/scheduled-prompt-manager.js +19 -2
- package/dist/cli/interactive.js +1 -1
- package/dist/cli/tui/claude-style.d.ts +3 -2
- package/dist/cli/tui/claude-style.js +13 -7
- package/dist/cli-runtime.d.ts +9 -0
- package/dist/cli-runtime.js +72 -20
- package/dist/persistence/claude-scheduled-task-store.d.ts +8 -1
- package/dist/persistence/claude-scheduled-task-store.js +13 -1
- package/dist/persistence/claude-transcript-store.js +6 -1
- package/dist/tools/claude-scheduled-tools.js +11 -10
- package/package.json +1 -1
|
@@ -1,4 +1,9 @@
|
|
|
1
1
|
import { type ClaudeScheduledTask } from '../persistence/claude-scheduled-task-store.js';
|
|
2
|
+
export declare const MAX_JOBS = 50;
|
|
3
|
+
export declare class ScheduledJobLimitError extends Error {
|
|
4
|
+
readonly maxJobs: number;
|
|
5
|
+
constructor(maxJobs: number);
|
|
6
|
+
}
|
|
2
7
|
export interface ScheduledPromptManagerOptions {
|
|
3
8
|
filePath: string;
|
|
4
9
|
lockFile: string;
|
|
@@ -2,7 +2,7 @@ import { execFile } from 'node:child_process';
|
|
|
2
2
|
import { randomBytes } from 'node:crypto';
|
|
3
3
|
import { promisify } from 'node:util';
|
|
4
4
|
import { CronExpressionParser } from 'cron-parser';
|
|
5
|
-
import { ClaudeScheduledTaskStore, } from '../persistence/claude-scheduled-task-store.js';
|
|
5
|
+
import { ClaudeScheduledTaskLimitError, ClaudeScheduledTaskStore, } from '../persistence/claude-scheduled-task-store.js';
|
|
6
6
|
const execFileAsync = promisify(execFile);
|
|
7
7
|
const RECURRING_LIFETIME_MS = 7 * 24 * 60 * 60 * 1_000;
|
|
8
8
|
const MIN_DYNAMIC_WAKEUP_SECONDS = 60;
|
|
@@ -11,6 +11,15 @@ const DYNAMIC_CACHE_TTL_MS = 300_000;
|
|
|
11
11
|
const DEFAULT_DYNAMIC_CACHE_LEAD_MS = 15_000;
|
|
12
12
|
const MAX_TIMER_MS = 2_147_000_000;
|
|
13
13
|
const DURABLE_REFRESH_MS = 5_000;
|
|
14
|
+
export const MAX_JOBS = 50;
|
|
15
|
+
export class ScheduledJobLimitError extends Error {
|
|
16
|
+
maxJobs;
|
|
17
|
+
constructor(maxJobs) {
|
|
18
|
+
super(`Scheduled job limit reached: at most ${maxJobs} active scheduled jobs. Delete or wait for an existing job before scheduling another.`);
|
|
19
|
+
this.maxJobs = maxJobs;
|
|
20
|
+
this.name = 'ScheduledJobLimitError';
|
|
21
|
+
}
|
|
22
|
+
}
|
|
14
23
|
function nextOccurrence(cron, after) {
|
|
15
24
|
return CronExpressionParser.parse(cron, {
|
|
16
25
|
currentDate: new Date(after),
|
|
@@ -131,7 +140,15 @@ export class ScheduledPromptManager {
|
|
|
131
140
|
};
|
|
132
141
|
let task;
|
|
133
142
|
if (input.durable) {
|
|
134
|
-
|
|
143
|
+
try {
|
|
144
|
+
task = await this.store.create(base, { maxJobs: MAX_JOBS });
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
if (error instanceof ClaudeScheduledTaskLimitError) {
|
|
148
|
+
throw new ScheduledJobLimitError(MAX_JOBS);
|
|
149
|
+
}
|
|
150
|
+
throw error;
|
|
151
|
+
}
|
|
135
152
|
}
|
|
136
153
|
else {
|
|
137
154
|
const occupied = new Set((await this.list()).map(({ id }) => id));
|
package/dist/cli/interactive.js
CHANGED
|
@@ -5626,7 +5626,7 @@ export function InteractiveApp({ factory, initialSessions, initialPrompt, initia
|
|
|
5626
5626
|
}
|
|
5627
5627
|
editComposer();
|
|
5628
5628
|
});
|
|
5629
|
-
return (_jsx(TuiThemeProvider, { settings: themeSettings, children: _jsx(Box, { flexDirection: "column", children: selectingSession ? (_jsx(SessionPicker, { sessions: filteredPickerChoices, selectedIndex: selectedIndex, screenReader: axScreenReader, query: sessionSearch })) : (_jsxs(_Fragment, { children: [!axScreenReader && history.length === 0 && !sessionId ? (_jsx(WelcomePanel, { display: runtimeDisplay, width: width })) : null, sessionId ? (_jsxs(Text, { dimColor: true, children: ["Session ", sessionId.slice(0, 8)] })) : null, _jsx(Transcript, { items: history, activeText: activeText, activeThinking: activeThinking, thinkingExpanded: thinkingExpanded, detailedTranscript: thinkingExpanded || runtimeSettings.verbose, screenReader: axScreenReader }), externalEditorRequest !== null ||
|
|
5629
|
+
return (_jsx(TuiThemeProvider, { settings: themeSettings, children: _jsx(Box, { flexDirection: "column", children: selectingSession ? (_jsx(SessionPicker, { sessions: filteredPickerChoices, selectedIndex: selectedIndex, screenReader: axScreenReader, query: sessionSearch })) : (_jsxs(_Fragment, { children: [!axScreenReader && history.length === 0 && !sessionId ? (_jsx(WelcomePanel, { display: runtimeDisplay, width: width, showTips: runtimeSettings.tips })) : null, sessionId ? (_jsxs(Text, { dimColor: true, children: ["Session ", sessionId.slice(0, 8)] })) : null, _jsx(Transcript, { items: history, activeText: activeText, activeThinking: activeThinking, thinkingExpanded: thinkingExpanded, detailedTranscript: thinkingExpanded || runtimeSettings.verbose, screenReader: axScreenReader }), externalEditorRequest !== null ||
|
|
5630
5630
|
keybindingsEditing ||
|
|
5631
5631
|
memoryEditorRequest !== null ? (_jsx(ExternalEditorWait, { screenReader: axScreenReader })) : permission ? (permission.kind === 'tool' && toolPermissionModel ? (_jsx(ToolPermissionDialog, { model: toolPermissionModel, selection: permissionSelection, feedbackMode: permissionFeedbackMode, feedback: input, ruleEditor: permissionRuleEditor, screenReader: axScreenReader })) : (_jsxs(DialogFrame, { title: `Retry interrupted ${permission.call.name}?`, screenReader: axScreenReader, children: [_jsx(Box, { flexDirection: "column", paddingX: 1, paddingY: 1, children: _jsx(Text, { bold: true, children: describeTool(permission.call, sensitiveValues) }) }), _jsx(Text, { children: "Do you want to proceed?" }), _jsxs(Text, { inverse: !axScreenReader && permissionSelection === 0, children: [selectionPrefix(permissionSelection === 0, axScreenReader), "1. Yes"] }), _jsxs(Text, { inverse: !axScreenReader && permissionSelection === 1, children: [selectionPrefix(permissionSelection === 1, axScreenReader), "2. No"] }), permissionFeedbackMode ? (_jsxs(Text, { children: ["\u203A", ' ', input ||
|
|
5632
5632
|
(permissionSelection === 0
|
|
@@ -71,10 +71,11 @@ export interface TuiBtwEntry {
|
|
|
71
71
|
error?: string;
|
|
72
72
|
}
|
|
73
73
|
export declare function useTerminalWidth(override?: number): number;
|
|
74
|
-
export declare function WelcomePanel({ display, width, }: {
|
|
74
|
+
export declare function WelcomePanel({ display, width, showTips, }: {
|
|
75
75
|
display: TuiDisplayMetadata;
|
|
76
76
|
width: number;
|
|
77
|
-
|
|
77
|
+
showTips: boolean;
|
|
78
|
+
}): import("react").JSX.Element | null;
|
|
78
79
|
export declare function MarkdownText({ text }: {
|
|
79
80
|
text: string;
|
|
80
81
|
}): ReactElement<unknown, string | import("react").JSXElementConstructor<any>>;
|
|
@@ -49,17 +49,23 @@ function selectionPrefix(selected, screenReader) {
|
|
|
49
49
|
return screenReader ? 'Selected: ' : ' ❯ ';
|
|
50
50
|
return screenReader ? '' : ' ';
|
|
51
51
|
}
|
|
52
|
-
export function WelcomePanel({ display, width, }) {
|
|
52
|
+
export function WelcomePanel({ display, width, showTips, }) {
|
|
53
53
|
const palette = useTuiPalette();
|
|
54
|
+
if (!showTips)
|
|
55
|
+
return null;
|
|
54
56
|
const panelWidth = Math.min(100, Math.max(32, width));
|
|
55
57
|
const wide = panelWidth >= 68;
|
|
56
58
|
const brand = 'Praxis';
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
59
|
+
const model = display.model ?? 'provider default';
|
|
60
|
+
const effort = display.effort ? ` · ${display.effort} effort` : '';
|
|
61
|
+
const cwd = compactPath(display.cwd);
|
|
62
|
+
// Title lives in the top border row:
|
|
63
|
+
// ╭───Praxis Code vX.Y.Z ───...───╮
|
|
64
|
+
// Fixed prefix/suffix = "╭───"(4) + "Praxis"(6) + " Code vX.Y.Z "(8 + version)
|
|
65
|
+
// + "╮"(1), so the fill keeps the row exactly at panelWidth.
|
|
66
|
+
const fill = Math.max(1, panelWidth - display.version.length - 19);
|
|
67
|
+
const identity = `Welcome to ${brand} · ${model}${effort} · ${cwd}`;
|
|
68
|
+
return (_jsxs(Box, { flexDirection: "column", width: panelWidth, children: [_jsxs(Text, { color: palette.muted, children: ['╭───', _jsx(Text, { color: palette.brand, bold: true, children: brand }), ` Code v${display.version} `, _jsx(Text, { dimColor: true, children: '─'.repeat(fill) }), '╮'] }), wide ? (_jsxs(_Fragment, { children: [_jsx(Text, { children: identity }), _jsx(Text, { children: "/init to create CLAUDE.md \u00B7 /config to open settings" })] })) : (_jsxs(_Fragment, { children: [_jsxs(Text, { children: ["Welcome to ", brand] }), _jsxs(Text, { children: [model, effort] }), _jsx(Text, { children: cwd }), _jsx(Text, { children: "/init to create CLAUDE.md" }), _jsx(Text, { children: "/config to open settings" })] }))] }));
|
|
63
69
|
}
|
|
64
70
|
const INLINE_SEGMENT_CACHE_MAX = 4096;
|
|
65
71
|
const inlineSegmentCache = new Map();
|
package/dist/cli-runtime.d.ts
CHANGED
|
@@ -7,6 +7,7 @@ import { type ModelDocument, type ModelImage, type ModelProvider, type ModelTool
|
|
|
7
7
|
import type { InteractiveResumeOptions, InteractiveServiceFactory } from './cli/interactive.js';
|
|
8
8
|
import type { TuiSlashCommand } from './cli/tui/slash-commands.js';
|
|
9
9
|
import { type TuiHookConfiguration } from './cli/tui/hook-settings.js';
|
|
10
|
+
import { type PraxisRuntimeSettings } from './cli/tui/runtime-settings.js';
|
|
10
11
|
import { type ClaudePermissionMode } from './permissions/claude-permission-resolver.js';
|
|
11
12
|
import { type ClaudeMcpServerStatus, type ClaudeMcpToolInspection } from './mcp/claude-mcp-tools.js';
|
|
12
13
|
import { authenticateMcpServer } from './mcp/claude-mcp-oauth.js';
|
|
@@ -164,6 +165,14 @@ export interface CliDependencies extends InteractiveServiceFactory {
|
|
|
164
165
|
force?: boolean;
|
|
165
166
|
}) => Promise<SelfUpdateResult>;
|
|
166
167
|
}
|
|
168
|
+
/**
|
|
169
|
+
* Shared runtime model precedence used by every consumer (provider
|
|
170
|
+
* construction, status/doctor output, and the interactive display):
|
|
171
|
+
* explicit CLI selection > non-empty PRAXIS_MODEL > Praxis settings model,
|
|
172
|
+
* with the configured default (settings.model === 'default') falling through
|
|
173
|
+
* to the existing default behavior (undefined).
|
|
174
|
+
*/
|
|
175
|
+
export declare function resolveRuntimeModel(explicitModel: string | undefined, environment: NodeJS.ProcessEnv, settings: PraxisRuntimeSettings | undefined): string | undefined;
|
|
167
176
|
export declare function createDefaultDependencies(entrypoint?: string): CliDependencies;
|
|
168
177
|
export declare function createBackgroundWorkerRuntime(workerSink: RuntimeEventSink, dispatch: {
|
|
169
178
|
argv: string[];
|
package/dist/cli-runtime.js
CHANGED
|
@@ -695,6 +695,20 @@ const consoleIO = {
|
|
|
695
695
|
isTTY: Boolean(process.stdin.isTTY && process.stdout.isTTY),
|
|
696
696
|
readStdinLines: () => process.stdin,
|
|
697
697
|
};
|
|
698
|
+
/**
|
|
699
|
+
* Shared runtime model precedence used by every consumer (provider
|
|
700
|
+
* construction, status/doctor output, and the interactive display):
|
|
701
|
+
* explicit CLI selection > non-empty PRAXIS_MODEL > Praxis settings model,
|
|
702
|
+
* with the configured default (settings.model === 'default') falling through
|
|
703
|
+
* to the existing default behavior (undefined).
|
|
704
|
+
*/
|
|
705
|
+
export function resolveRuntimeModel(explicitModel, environment, settings) {
|
|
706
|
+
const envModel = environment.PRAXIS_MODEL;
|
|
707
|
+
const settingsModel = settings && settings.model !== 'default' ? settings.model : undefined;
|
|
708
|
+
return (explicitModel ??
|
|
709
|
+
(envModel !== undefined && envModel.trim() !== '' ? envModel : undefined) ??
|
|
710
|
+
settingsModel);
|
|
711
|
+
}
|
|
698
712
|
const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = false, approveRecovery, approveTool, agent, model: interactiveModel, effort: interactiveEffort, permissionMode: interactivePermissionMode, isSessionActionApproved, controls = DEFAULT_CLI_CONTROLS, interactive = false, sessionKind, signal, exposeToolRegistry = false, onElicitation, askUser, approvePlan, emitToolUseSummaries = false, cwd: requestedCwd, sandboxOriginalCwd, configRoot: requestedConfigRoot, environment, providerEnvironment: requestedProviderEnvironment, }) => {
|
|
699
713
|
const runtimeEnvironment = requestedProviderEnvironment ?? process.env;
|
|
700
714
|
const sandboxEnvironment = { ...runtimeEnvironment, ...environment };
|
|
@@ -740,7 +754,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
740
754
|
let providerForMainModel;
|
|
741
755
|
const context = parseContextEnvironment(runtimeEnvironment);
|
|
742
756
|
const apiKey = runtimeEnvironment.PRAXIS_API_KEY;
|
|
743
|
-
const model =
|
|
757
|
+
const model = resolveRuntimeModel(interactiveModel ?? controls.model, runtimeEnvironment, runtimeSettings);
|
|
744
758
|
const providerEnvironment = apiKey && model
|
|
745
759
|
? parseProviderEnvironment(runtimeEnvironment)
|
|
746
760
|
: cli.fileResources.length > 0
|
|
@@ -1300,7 +1314,9 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1300
1314
|
];
|
|
1301
1315
|
const runtimeInfo = {
|
|
1302
1316
|
cwd: workspace.cwd(),
|
|
1303
|
-
model: provider?.model ??
|
|
1317
|
+
model: provider?.model ??
|
|
1318
|
+
resolveRuntimeModel(interactiveModel ?? controls.model, runtimeEnvironment, runtimeSettings) ??
|
|
1319
|
+
'unknown',
|
|
1304
1320
|
...(provider?.capabilities.contextWindowTokens === undefined
|
|
1305
1321
|
? {}
|
|
1306
1322
|
: { contextWindowTokens: provider.capabilities.contextWindowTokens }),
|
|
@@ -1457,7 +1473,7 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1457
1473
|
cwd: workspace.cwd(),
|
|
1458
1474
|
model: service.model() ??
|
|
1459
1475
|
provider?.model ??
|
|
1460
|
-
|
|
1476
|
+
resolveRuntimeModel(interactiveModel ?? controls.model, runtimeEnvironment, runtimeSettings) ??
|
|
1461
1477
|
'unknown',
|
|
1462
1478
|
};
|
|
1463
1479
|
delete staticInfo.contextWindowTokens;
|
|
@@ -1498,7 +1514,12 @@ const createDefaultService = async ({ eventSink, requireProvider, hooksOnly = fa
|
|
|
1498
1514
|
};
|
|
1499
1515
|
const createDefaultAutoModeCritic = async ({ model }) => {
|
|
1500
1516
|
const apiKey = process.env.PRAXIS_API_KEY;
|
|
1501
|
-
const
|
|
1517
|
+
const configRoot = resolve(process.env.CLAUDE_CONFIG_DIR ?? resolve(homedir(), '.claude'));
|
|
1518
|
+
const runtimeSettings = await loadRuntimeSettings({
|
|
1519
|
+
configRoot,
|
|
1520
|
+
statePath: join(configRoot, '.claude.json'),
|
|
1521
|
+
});
|
|
1522
|
+
const selectedModel = resolveRuntimeModel(model, process.env, runtimeSettings);
|
|
1502
1523
|
if (!apiKey || !selectedModel) {
|
|
1503
1524
|
throw new Error('PRAXIS_API_KEY and a model (--model or PRAXIS_MODEL) are required');
|
|
1504
1525
|
}
|
|
@@ -1657,6 +1678,18 @@ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta
|
|
|
1657
1678
|
const { runInteractive } = await import('./cli/interactive.js');
|
|
1658
1679
|
const interactiveControls = controls ?? DEFAULT_CLI_CONTROLS;
|
|
1659
1680
|
const initialAdditionalDirectories = interactiveControls.addDirectories.map((directory) => realpathSync(resolve(process.cwd(), directory)));
|
|
1681
|
+
const configuredRoot = process.env.CLAUDE_CONFIG_DIR || undefined;
|
|
1682
|
+
const interactiveConfigRoot = resolve(configuredRoot ?? resolve(homedir(), '.claude'));
|
|
1683
|
+
const interactiveStatePath = configuredRoot
|
|
1684
|
+
? join(interactiveConfigRoot, '.claude.json')
|
|
1685
|
+
: resolve(homedir(), '.claude.json');
|
|
1686
|
+
const interactiveRuntimeSettings = interactiveControls.safeMode || interactiveControls.bare
|
|
1687
|
+
? undefined
|
|
1688
|
+
: await loadRuntimeSettings({
|
|
1689
|
+
configRoot: interactiveConfigRoot,
|
|
1690
|
+
statePath: interactiveStatePath,
|
|
1691
|
+
});
|
|
1692
|
+
const effectiveModel = resolveRuntimeModel(interactiveControls.model, process.env, interactiveRuntimeSettings);
|
|
1660
1693
|
return runInteractive({
|
|
1661
1694
|
factory: {
|
|
1662
1695
|
createService: ({ additionalDirectories, cwd, ...options }) => createDefaultService({
|
|
@@ -1670,8 +1703,7 @@ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta
|
|
|
1670
1703
|
},
|
|
1671
1704
|
interactive: true,
|
|
1672
1705
|
}),
|
|
1673
|
-
scheduledPrompts: Boolean(process.env.PRAXIS_API_KEY &&
|
|
1674
|
-
(interactiveControls.model ?? process.env.PRAXIS_MODEL)),
|
|
1706
|
+
scheduledPrompts: Boolean(process.env.PRAXIS_API_KEY && effectiveModel),
|
|
1675
1707
|
},
|
|
1676
1708
|
...(signal ? { signal } : {}),
|
|
1677
1709
|
...(initialPrompt === undefined ? {} : { initialPrompt }),
|
|
@@ -1686,11 +1718,7 @@ export function createDefaultDependencies(entrypoint = fileURLToPath(import.meta
|
|
|
1686
1718
|
display: {
|
|
1687
1719
|
version: VERSION,
|
|
1688
1720
|
cwd: process.cwd(),
|
|
1689
|
-
...(
|
|
1690
|
-
? { model: controls.model }
|
|
1691
|
-
: process.env.PRAXIS_MODEL
|
|
1692
|
-
? { model: process.env.PRAXIS_MODEL }
|
|
1693
|
-
: {}),
|
|
1721
|
+
...(effectiveModel === undefined ? {} : { model: effectiveModel }),
|
|
1694
1722
|
effort: controls?.effort ?? 'high',
|
|
1695
1723
|
permissionMode: controls?.dangerouslySkipPermissions
|
|
1696
1724
|
? 'bypassPermissions'
|
|
@@ -2090,6 +2118,7 @@ async function executeDoctorCommand(args, invocation, io) {
|
|
|
2090
2118
|
configRoot,
|
|
2091
2119
|
statePath: claudeStatePath,
|
|
2092
2120
|
});
|
|
2121
|
+
const effectiveModel = resolveRuntimeModel(invocation.model, process.env, runtimeSettings);
|
|
2093
2122
|
const report = await runDoctor({
|
|
2094
2123
|
version: VERSION,
|
|
2095
2124
|
executablePath: fileURLToPath(import.meta.url),
|
|
@@ -2098,7 +2127,9 @@ async function executeDoctorCommand(args, invocation, io) {
|
|
|
2098
2127
|
configRoot,
|
|
2099
2128
|
claudeStatePath,
|
|
2100
2129
|
cwd: process.cwd(),
|
|
2101
|
-
environment:
|
|
2130
|
+
environment: effectiveModel === undefined
|
|
2131
|
+
? process.env
|
|
2132
|
+
: { ...process.env, PRAXIS_MODEL: effectiveModel },
|
|
2102
2133
|
autoUpdateChannel: runtimeSettings.autoUpdatesChannel,
|
|
2103
2134
|
...(process.argv[1] === undefined
|
|
2104
2135
|
? {}
|
|
@@ -3159,12 +3190,18 @@ async function executeMcpCommand(args, invocation, io, dependencies, signal) {
|
|
|
3159
3190
|
function eventSink(io, outputFormat, legacyJson = false) {
|
|
3160
3191
|
const sensitiveValues = sensitiveEnvironmentValues(process.env);
|
|
3161
3192
|
if (legacyJson) {
|
|
3162
|
-
return (event) =>
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3193
|
+
return (event) => {
|
|
3194
|
+
if (event.type === 'warning') {
|
|
3195
|
+
io.stderr(`Warning: ${redactSensitiveText(event.message, sensitiveValues)}\n`);
|
|
3196
|
+
return;
|
|
3166
3197
|
}
|
|
3167
|
-
|
|
3198
|
+
writeJson(io, event.type === 'failed'
|
|
3199
|
+
? {
|
|
3200
|
+
...event,
|
|
3201
|
+
message: redactSensitiveText(event.message, sensitiveValues),
|
|
3202
|
+
}
|
|
3203
|
+
: event);
|
|
3204
|
+
};
|
|
3168
3205
|
}
|
|
3169
3206
|
if (outputFormat !== 'text')
|
|
3170
3207
|
return () => undefined;
|
|
@@ -4047,6 +4084,10 @@ async function execute(argv, io, dependencies, signal) {
|
|
|
4047
4084
|
const startedAt = Date.now();
|
|
4048
4085
|
const sessionId = invocation.sessionId ?? randomUUID();
|
|
4049
4086
|
const configRoot = resolve(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'));
|
|
4087
|
+
const runtimeSettings = await loadRuntimeSettings({
|
|
4088
|
+
configRoot,
|
|
4089
|
+
statePath: join(configRoot, '.claude.json'),
|
|
4090
|
+
});
|
|
4050
4091
|
const text = dependencies.loadReleaseNotes
|
|
4051
4092
|
? await dependencies.loadReleaseNotes(configRoot)
|
|
4052
4093
|
: await loadClaudeReleaseNotes({ configRoot });
|
|
@@ -4057,7 +4098,8 @@ async function execute(argv, io, dependencies, signal) {
|
|
|
4057
4098
|
};
|
|
4058
4099
|
const info = {
|
|
4059
4100
|
cwd: process.cwd(),
|
|
4060
|
-
model: invocation.model
|
|
4101
|
+
model: resolveRuntimeModel(invocation.model, process.env, runtimeSettings) ??
|
|
4102
|
+
'unknown',
|
|
4061
4103
|
tools: [],
|
|
4062
4104
|
mcpServers: [],
|
|
4063
4105
|
permissionMode: invocation.permissionMode,
|
|
@@ -4100,7 +4142,11 @@ async function execute(argv, io, dependencies, signal) {
|
|
|
4100
4142
|
const service = await dependencies.createService({
|
|
4101
4143
|
eventSink: outputFormat === 'stream-json' && !invocation.legacyJson
|
|
4102
4144
|
? (event) => {
|
|
4103
|
-
|
|
4145
|
+
if (event.type === 'warning') {
|
|
4146
|
+
io.stderr(`Warning: ${redactSensitiveText(event.message, sensitiveEnvironmentValues(process.env))}\n`);
|
|
4147
|
+
return;
|
|
4148
|
+
}
|
|
4149
|
+
const safeEvent = event.type === 'failed'
|
|
4104
4150
|
? {
|
|
4105
4151
|
...event,
|
|
4106
4152
|
message: redactSensitiveText(event.message, sensitiveEnvironmentValues(process.env)),
|
|
@@ -4208,9 +4254,15 @@ async function execute(argv, io, dependencies, signal) {
|
|
|
4208
4254
|
io.stdout(transcript);
|
|
4209
4255
|
return 0;
|
|
4210
4256
|
}
|
|
4257
|
+
const configRoot = resolve(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), '.claude'));
|
|
4258
|
+
const runtimeSettings = await loadRuntimeSettings({
|
|
4259
|
+
configRoot,
|
|
4260
|
+
statePath: join(configRoot, '.claude.json'),
|
|
4261
|
+
});
|
|
4211
4262
|
const runtimeInfo = service.runtimeInfo?.() ?? {
|
|
4212
4263
|
cwd: process.cwd(),
|
|
4213
|
-
model: process.env
|
|
4264
|
+
model: resolveRuntimeModel(invocation.model, process.env, runtimeSettings) ??
|
|
4265
|
+
'unknown',
|
|
4214
4266
|
tools: [],
|
|
4215
4267
|
mcpServers: [],
|
|
4216
4268
|
permissionMode: 'default',
|
|
@@ -14,12 +14,19 @@ export interface ClaudeScheduledTaskStoreOptions {
|
|
|
14
14
|
lockFile?: string;
|
|
15
15
|
}
|
|
16
16
|
export type ClaudeScheduledTaskCreateInput = Pick<ClaudeScheduledTask, 'cron' | 'prompt' | 'createdAt' | 'recurring' | 'createdBySessionId' | 'createdByPid' | 'createdByProcStart'>;
|
|
17
|
+
export interface ClaudeScheduledTaskCreateOptions {
|
|
18
|
+
maxJobs?: number;
|
|
19
|
+
}
|
|
20
|
+
export declare class ClaudeScheduledTaskLimitError extends Error {
|
|
21
|
+
readonly maxJobs: number;
|
|
22
|
+
constructor(maxJobs: number);
|
|
23
|
+
}
|
|
17
24
|
export declare class ClaudeScheduledTaskStore {
|
|
18
25
|
private readonly filePath;
|
|
19
26
|
private readonly lease;
|
|
20
27
|
constructor(options: ClaudeScheduledTaskStoreOptions);
|
|
21
28
|
list(): Promise<ClaudeScheduledTask[]>;
|
|
22
|
-
create(input: ClaudeScheduledTaskCreateInput): Promise<ClaudeScheduledTask>;
|
|
29
|
+
create(input: ClaudeScheduledTaskCreateInput, options?: ClaudeScheduledTaskCreateOptions): Promise<ClaudeScheduledTask>;
|
|
23
30
|
delete(id: string): Promise<boolean>;
|
|
24
31
|
private readRecord;
|
|
25
32
|
private writeDocument;
|
|
@@ -3,6 +3,14 @@ import { readFile, stat } from 'node:fs/promises';
|
|
|
3
3
|
import { dirname, join } from 'node:path';
|
|
4
4
|
import { writeFileAtomically } from '../platform/atomic-write.js';
|
|
5
5
|
import { ExclusiveFileLease } from '../platform/exclusive-file-lease.js';
|
|
6
|
+
export class ClaudeScheduledTaskLimitError extends Error {
|
|
7
|
+
maxJobs;
|
|
8
|
+
constructor(maxJobs) {
|
|
9
|
+
super(`Scheduled job limit reached: at most ${maxJobs} active scheduled jobs. Delete or wait for an existing job before scheduling another.`);
|
|
10
|
+
this.maxJobs = maxJobs;
|
|
11
|
+
this.name = 'ClaudeScheduledTaskLimitError';
|
|
12
|
+
}
|
|
13
|
+
}
|
|
6
14
|
const JOB_ID_PATTERN = /^[0-9a-f]{8}$/u;
|
|
7
15
|
const LOCK_WAIT_MS = 5_000;
|
|
8
16
|
const MAX_MUTATION_RETRIES = 16;
|
|
@@ -76,10 +84,14 @@ export class ClaudeScheduledTaskStore {
|
|
|
76
84
|
async list() {
|
|
77
85
|
return (await this.readRecord()).document.tasks.map((task) => ({ ...task }));
|
|
78
86
|
}
|
|
79
|
-
async create(input) {
|
|
87
|
+
async create(input, options = {}) {
|
|
80
88
|
return this.withLock(async () => {
|
|
81
89
|
for (let attempt = 0; attempt < MAX_MUTATION_RETRIES; attempt += 1) {
|
|
82
90
|
const { document, fingerprint: expected } = await this.readRecord();
|
|
91
|
+
if (options.maxJobs !== undefined &&
|
|
92
|
+
document.tasks.length >= options.maxJobs) {
|
|
93
|
+
throw new ClaudeScheduledTaskLimitError(options.maxJobs);
|
|
94
|
+
}
|
|
83
95
|
const ids = new Set(document.tasks.map(({ id }) => id));
|
|
84
96
|
let id = randomBytes(4).toString('hex');
|
|
85
97
|
while (ids.has(id))
|
|
@@ -299,6 +299,7 @@ export class ClaudeTranscriptStore {
|
|
|
299
299
|
let logicalTailUuid = tailLogicalUuid(expectedTail);
|
|
300
300
|
const branchParentUuid = expectedTail.branchParentUuid;
|
|
301
301
|
let advancedLogicalTail = false;
|
|
302
|
+
let staleLastPromptLeaf = false;
|
|
302
303
|
const lines = [];
|
|
303
304
|
for (const entry of entries) {
|
|
304
305
|
if (this.writeProfile === 'sidechain') {
|
|
@@ -314,7 +315,8 @@ export class ClaudeTranscriptStore {
|
|
|
314
315
|
}
|
|
315
316
|
else if (entry.type === 'last-prompt') {
|
|
316
317
|
if (entry.leafUuid !== logicalTailUuid) {
|
|
317
|
-
|
|
318
|
+
staleLastPromptLeaf = true;
|
|
319
|
+
continue;
|
|
318
320
|
}
|
|
319
321
|
}
|
|
320
322
|
else if (entry.type === 'system' &&
|
|
@@ -347,6 +349,9 @@ export class ClaudeTranscriptStore {
|
|
|
347
349
|
advancedLogicalTail = true;
|
|
348
350
|
}
|
|
349
351
|
}
|
|
352
|
+
if (staleLastPromptLeaf) {
|
|
353
|
+
return { status: 'conflict', reason: 'tail-changed' };
|
|
354
|
+
}
|
|
350
355
|
const encodedLine = Buffer.from(`${lines.join('\n')}\n`);
|
|
351
356
|
await mkdir(dirname(this.sessionFile), { recursive: true });
|
|
352
357
|
const sessionHandle = await open(this.sessionFile, 'a');
|
|
@@ -295,11 +295,10 @@ export class ClaudeScheduledToolRegistry {
|
|
|
295
295
|
: `Loop stopped — cancelled ${cancelledWakeups} pending wakeup(s); no further dynamic-loop wakeups scheduled. If you armed a Monitor for this loop, TaskStop it now; otherwise nothing more to do this turn.`,
|
|
296
296
|
isError: false,
|
|
297
297
|
nativeToolUseResult: {
|
|
298
|
-
scheduledFor: 0,
|
|
299
|
-
clampedDelaySeconds: 0,
|
|
300
|
-
wasClamped: false,
|
|
301
298
|
stopped: true,
|
|
302
|
-
|
|
299
|
+
nextWakeupMs: 0,
|
|
300
|
+
delaySeconds: 0,
|
|
301
|
+
reason: '',
|
|
303
302
|
},
|
|
304
303
|
};
|
|
305
304
|
}
|
|
@@ -320,9 +319,10 @@ export class ClaudeScheduledToolRegistry {
|
|
|
320
319
|
content: `Next wakeup scheduled for ${scheduledTime} (in ${secondsUntilWakeup}s)${clampNotice}. Nothing more to do this turn — the harness re-invokes you when the wakeup fires or a task-notification arrives.`,
|
|
321
320
|
isError: false,
|
|
322
321
|
nativeToolUseResult: {
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
322
|
+
stopped: false,
|
|
323
|
+
nextWakeupMs: wakeup.scheduledFor,
|
|
324
|
+
delaySeconds: wakeup.clampedDelaySeconds,
|
|
325
|
+
reason: String(call.input.reason),
|
|
326
326
|
},
|
|
327
327
|
};
|
|
328
328
|
}
|
|
@@ -331,9 +331,10 @@ export class ClaudeScheduledToolRegistry {
|
|
|
331
331
|
content: 'Wakeup not scheduled. Either the /loop dynamic runtime gate is off or the loop reached its maximum duration — the loop has ended; do not re-issue.',
|
|
332
332
|
isError: false,
|
|
333
333
|
nativeToolUseResult: {
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
334
|
+
stopped: false,
|
|
335
|
+
nextWakeupMs: 0,
|
|
336
|
+
delaySeconds: 0,
|
|
337
|
+
reason: String(call.input.reason),
|
|
337
338
|
},
|
|
338
339
|
};
|
|
339
340
|
default:
|