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.
@@ -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
- task = await this.store.create(base);
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' ||
@@ -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) => writeJson(io, event.type === 'warning' || event.type === 'failed'
3163
- ? {
3164
- ...event,
3165
- message: redactSensitiveText(event.message, sensitiveValues),
3162
+ return (event) => {
3163
+ if (event.type === 'warning') {
3164
+ io.stderr(`Warning: ${redactSensitiveText(event.message, sensitiveValues)}\n`);
3165
+ return;
3166
3166
  }
3167
- : event);
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
- const safeEvent = event.type === 'warning' || event.type === 'failed'
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
- throw new Error('Entry leafUuid does not match transcript tail');
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
- cancelledWakeups,
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
- scheduledFor: wakeup.scheduledFor,
324
- clampedDelaySeconds: wakeup.clampedDelaySeconds,
325
- wasClamped: wakeup.wasClamped,
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
- scheduledFor: 0,
335
- clampedDelaySeconds: 0,
336
- wasClamped: false,
334
+ stopped: false,
335
+ nextWakeupMs: 0,
336
+ delaySeconds: 0,
337
+ reason: String(call.input.reason),
337
338
  },
338
339
  };
339
340
  default:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "praxis-agent",
3
- "version": "0.20.10",
3
+ "version": "0.20.14",
4
4
  "description": "Local-first, single-user general agent for the command line.",
5
5
  "license": "MIT",
6
6
  "author": "wuqisen",