praxis-agent 0.20.10 → 0.20.14
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/application/top-level-agent-manager.d.ts +2 -2
- package/dist/application/top-level-agent-manager.js +9 -0
- package/dist/cli-runtime.js +16 -6
- 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));
|
|
@@ -5,11 +5,11 @@ export interface TopLevelAgentSummary {
|
|
|
5
5
|
pid?: number;
|
|
6
6
|
id?: string;
|
|
7
7
|
cwd: string;
|
|
8
|
-
kind: 'background' | 'interactive';
|
|
8
|
+
kind: 'background' | 'interactive' | 'bg' | 'daemon' | 'daemon-worker';
|
|
9
9
|
startedAt: number;
|
|
10
10
|
sessionId: string;
|
|
11
11
|
name: string;
|
|
12
|
-
status?: 'active' | 'idle';
|
|
12
|
+
status?: 'active' | 'idle' | 'busy' | 'waiting';
|
|
13
13
|
tempo?: ClaudeJobTempo;
|
|
14
14
|
needs?: string;
|
|
15
15
|
state?: 'working' | 'stopped' | 'failed' | 'done';
|
|
@@ -87,6 +87,15 @@ function nativeClaudeSession(value) {
|
|
|
87
87
|
['active', 'idle'].includes(String(record.status));
|
|
88
88
|
if (interactive)
|
|
89
89
|
return record;
|
|
90
|
+
const daemon = Number.isSafeInteger(record.pid) &&
|
|
91
|
+
typeof record.cwd === 'string' &&
|
|
92
|
+
['bg', 'daemon', 'daemon-worker'].includes(String(record.kind)) &&
|
|
93
|
+
Number.isSafeInteger(record.startedAt) &&
|
|
94
|
+
typeof record.sessionId === 'string' &&
|
|
95
|
+
typeof record.name === 'string' &&
|
|
96
|
+
['busy', 'idle', 'waiting'].includes(String(record.status));
|
|
97
|
+
if (daemon)
|
|
98
|
+
return record;
|
|
90
99
|
if (typeof record.id !== 'string' ||
|
|
91
100
|
typeof record.cwd !== 'string' ||
|
|
92
101
|
record.kind !== 'background' ||
|
package/dist/cli-runtime.js
CHANGED
|
@@ -3159,12 +3159,18 @@ async function executeMcpCommand(args, invocation, io, dependencies, signal) {
|
|
|
3159
3159
|
function eventSink(io, outputFormat, legacyJson = false) {
|
|
3160
3160
|
const sensitiveValues = sensitiveEnvironmentValues(process.env);
|
|
3161
3161
|
if (legacyJson) {
|
|
3162
|
-
return (event) =>
|
|
3163
|
-
|
|
3164
|
-
|
|
3165
|
-
|
|
3162
|
+
return (event) => {
|
|
3163
|
+
if (event.type === 'warning') {
|
|
3164
|
+
io.stderr(`Warning: ${redactSensitiveText(event.message, sensitiveValues)}\n`);
|
|
3165
|
+
return;
|
|
3166
3166
|
}
|
|
3167
|
-
|
|
3167
|
+
writeJson(io, event.type === 'failed'
|
|
3168
|
+
? {
|
|
3169
|
+
...event,
|
|
3170
|
+
message: redactSensitiveText(event.message, sensitiveValues),
|
|
3171
|
+
}
|
|
3172
|
+
: event);
|
|
3173
|
+
};
|
|
3168
3174
|
}
|
|
3169
3175
|
if (outputFormat !== 'text')
|
|
3170
3176
|
return () => undefined;
|
|
@@ -4100,7 +4106,11 @@ async function execute(argv, io, dependencies, signal) {
|
|
|
4100
4106
|
const service = await dependencies.createService({
|
|
4101
4107
|
eventSink: outputFormat === 'stream-json' && !invocation.legacyJson
|
|
4102
4108
|
? (event) => {
|
|
4103
|
-
|
|
4109
|
+
if (event.type === 'warning') {
|
|
4110
|
+
io.stderr(`Warning: ${redactSensitiveText(event.message, sensitiveEnvironmentValues(process.env))}\n`);
|
|
4111
|
+
return;
|
|
4112
|
+
}
|
|
4113
|
+
const safeEvent = event.type === 'failed'
|
|
4104
4114
|
? {
|
|
4105
4115
|
...event,
|
|
4106
4116
|
message: redactSensitiveText(event.message, sensitiveEnvironmentValues(process.env)),
|
|
@@ -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:
|