@hyperdrive.bot/paseo-server 0.3.40 → 0.3.42
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/server/server/agent/agent-manager.d.ts +15 -0
- package/dist/server/server/agent/agent-manager.js +157 -25
- package/dist/server/server/agent/agent-projections.js +3 -0
- package/dist/server/server/agent/agent-sdk-types.d.ts +23 -0
- package/dist/server/server/agent/agent-storage.d.ts +2 -1
- package/dist/server/server/agent/agent-storage.js +4 -0
- package/dist/server/server/agent/mcp-shared.js +5 -2
- package/dist/server/server/agent/providers/claude/agent.d.ts +34 -0
- package/dist/server/server/agent/providers/claude/agent.js +74 -0
- package/dist/server/server/agent/providers/claude/background-task-tracker.d.ts +38 -1
- package/dist/server/server/agent/providers/claude/background-task-tracker.js +114 -9
- package/dist/server/server/agent/providers/claude/background-work-kinds.d.ts +95 -0
- package/dist/server/server/agent/providers/claude/background-work-kinds.js +73 -0
- package/dist/server/server/agent/providers/claude/pty-session-launcher.d.ts +7 -0
- package/dist/server/server/agent/providers/claude/pty-session-launcher.js +9 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist-guard.d.ts +41 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist-guard.js +93 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist.d.ts +68 -0
- package/dist/server/server/agent/providers/claude/tool-allowlist.js +133 -0
- package/dist/server/server/agent/providers/claude/transport/pty.d.ts +17 -0
- package/dist/server/server/agent/providers/claude/transport/pty.js +51 -1
- package/dist/server/server/agent/providers/claude/transport/tmux.d.ts +74 -0
- package/dist/server/server/agent/providers/claude/transport/tmux.js +157 -0
- package/dist/server/server/agent/providers/claude/transport/types.d.ts +6 -0
- package/dist/server/server/agent/providers/opencode-agent.d.ts +7 -0
- package/dist/server/server/agent/providers/opencode-agent.js +51 -1
- package/dist/server/server/agent/tools/paseo-tools.d.ts +19 -0
- package/dist/server/server/agent/tools/paseo-tools.js +213 -38
- package/dist/server/server/agent/tools/read-only-surface.d.ts +1 -0
- package/dist/server/server/agent/tools/read-only-surface.js +1 -0
- package/dist/server/server/persistence-hooks.js +2 -0
- package/dist/server/server/workspace-directory.js +32 -14
- package/dist/server/web-ui/_expo/static/js/web/{index-73ebfe5c6b82437cad59d50a40d2c8ef.js → index-9c3bcdc334cf1c08001b6bea510e0292.js} +17 -17
- package/dist/server/web-ui/_expo/static/js/web/index-9c3bcdc334cf1c08001b6bea510e0292.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-9c3bcdc334cf1c08001b6bea510e0292.js.gz +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-73ebfe5c6b82437cad59d50a40d2c8ef.js.map.br → index-9c3bcdc334cf1c08001b6bea510e0292.js.map.br} +0 -0
- package/dist/server/web-ui/_expo/static/js/web/{index-73ebfe5c6b82437cad59d50a40d2c8ef.js.map.gz → index-9c3bcdc334cf1c08001b6bea510e0292.js.map.gz} +0 -0
- package/dist/server/web-ui/index.html +1 -1
- package/dist/server/web-ui/index.html.br +0 -0
- package/dist/server/web-ui/index.html.gz +0 -0
- package/package.json +6 -6
- package/dist/server/web-ui/_expo/static/js/web/index-73ebfe5c6b82437cad59d50a40d2c8ef.js.br +0 -0
- package/dist/server/web-ui/_expo/static/js/web/index-73ebfe5c6b82437cad59d50a40d2c8ef.js.gz +0 -0
|
@@ -1925,6 +1925,29 @@ function appendOpenCodeSessionError(event, state, events) {
|
|
|
1925
1925
|
});
|
|
1926
1926
|
}
|
|
1927
1927
|
}
|
|
1928
|
+
/**
|
|
1929
|
+
* Some opencode "retry" errors are DETERMINISTIC: the exact request that failed
|
|
1930
|
+
* will fail identically on every retry, so opencode's retry-forever policy turns
|
|
1931
|
+
* them into a permanent wedge (endless "Provider retry" lines, spinner never ends,
|
|
1932
|
+
* the turn never reaches a terminal state). We only match the failure whose
|
|
1933
|
+
* unrecoverability is provable from the request itself.
|
|
1934
|
+
*
|
|
1935
|
+
* Deliberately NARROW. Notably EXCLUDED (kept retryable on purpose):
|
|
1936
|
+
* - "model does not exist" / "not found": ollama and other backends lazy-load a
|
|
1937
|
+
* model on first use, so this can resolve mid-retry once the model finishes
|
|
1938
|
+
* loading. A unit test pins this as non-terminal.
|
|
1939
|
+
* - auth failures (401 / invalid api key): a token can be refreshed out of band
|
|
1940
|
+
* between retries in some provider setups.
|
|
1941
|
+
*
|
|
1942
|
+
* Currently the sole member is the model server rejecting a request that has no
|
|
1943
|
+
* user-role message (e.g. ollama 0.32.x "no user query found in messages"). The
|
|
1944
|
+
* message array cannot gain a user turn on retry, so it never recovers. Add a new
|
|
1945
|
+
* pattern here only when its unrecoverability is likewise provable.
|
|
1946
|
+
*/
|
|
1947
|
+
const TERMINAL_PROVIDER_RETRY_PATTERNS = [/no user query found in messages/i];
|
|
1948
|
+
function isTerminalProviderRetryMessage(message) {
|
|
1949
|
+
return message.length > 0 && TERMINAL_PROVIDER_RETRY_PATTERNS.some((re) => re.test(message));
|
|
1950
|
+
}
|
|
1928
1951
|
function appendOpenCodeSessionStatus(event, state, events) {
|
|
1929
1952
|
if (event.properties.sessionID !== state.sessionId) {
|
|
1930
1953
|
return;
|
|
@@ -1936,12 +1959,32 @@ function appendOpenCodeSessionStatus(event, state, events) {
|
|
|
1936
1959
|
return;
|
|
1937
1960
|
}
|
|
1938
1961
|
if (status.type === "retry") {
|
|
1962
|
+
const message = typeof status.message === "string" ? status.message.trim() : "";
|
|
1963
|
+
// A deterministic provider error fails identically on every retry, and
|
|
1964
|
+
// opencode never gives up on its own. Fail the turn cleanly and ask the agent
|
|
1965
|
+
// to abort the opencode session so it stops backing off forever.
|
|
1966
|
+
if (isTerminalProviderRetryMessage(message)) {
|
|
1967
|
+
events.push({
|
|
1968
|
+
type: "timeline",
|
|
1969
|
+
provider: "opencode",
|
|
1970
|
+
item: {
|
|
1971
|
+
type: "error",
|
|
1972
|
+
message: `Provider gave up (non-retryable after attempt ${status.attempt}): ${message}`,
|
|
1973
|
+
},
|
|
1974
|
+
});
|
|
1975
|
+
state.onTerminalProviderRetry?.(message);
|
|
1976
|
+
events.push({
|
|
1977
|
+
type: "turn_failed",
|
|
1978
|
+
provider: "opencode",
|
|
1979
|
+
error: message,
|
|
1980
|
+
});
|
|
1981
|
+
return;
|
|
1982
|
+
}
|
|
1939
1983
|
// Mirror what opencode's TUI shows: retry attempts are visible activity, not
|
|
1940
1984
|
// terminal. opencode itself never gives up — it backs off and tries again
|
|
1941
1985
|
// forever. If we silently swallow these the user sees a spinner with no
|
|
1942
1986
|
// explanation. Forwarding as a timeline error item is a no-op for old
|
|
1943
1987
|
// clients (the schema already supports it).
|
|
1944
|
-
const message = typeof status.message === "string" ? status.message.trim() : "";
|
|
1945
1988
|
const text = message
|
|
1946
1989
|
? `Provider retry (attempt ${status.attempt}): ${message}`
|
|
1947
1990
|
: `Provider retry (attempt ${status.attempt})`;
|
|
@@ -2801,6 +2844,13 @@ class OpenCodeAgentSession {
|
|
|
2801
2844
|
this.selectedModelContextWindowMaxTokens = contextWindowMaxTokens;
|
|
2802
2845
|
}
|
|
2803
2846
|
},
|
|
2847
|
+
onTerminalProviderRetry: () => {
|
|
2848
|
+
// The translated turn_failed already ends the turn for our clients; this
|
|
2849
|
+
// aborts opencode's session so its own retry-forever loop stops hammering
|
|
2850
|
+
// the provider in the background. Best-effort: an abort failure must not
|
|
2851
|
+
// mask the terminal turn_failed.
|
|
2852
|
+
void this.beginSessionAbort(this.activeForegroundTurnId, "provider_error").catch(() => { });
|
|
2853
|
+
},
|
|
2804
2854
|
});
|
|
2805
2855
|
const events = [];
|
|
2806
2856
|
if (typeof this.accumulatedUsage.totalCostUsd === "number") {
|
|
@@ -8,6 +8,7 @@ import type { TerminalManager } from "../../../terminal/terminal-manager.js";
|
|
|
8
8
|
import type { CreatePaseoWorktreeWorkflowFn } from "../../worktree-session.js";
|
|
9
9
|
import type { JudgeRelayGate } from "../judge-relay-gate.js";
|
|
10
10
|
import type { ScheduleService } from "../../schedule/service.js";
|
|
11
|
+
import { type ScheduleCadence } from "@hyperdrive.bot/paseo-protocol/schedule/types";
|
|
11
12
|
import type { ProviderSnapshotManager } from "../provider-snapshot-manager.js";
|
|
12
13
|
import type { GitHubService } from "../../../services/github-service.js";
|
|
13
14
|
import type { WorkspaceGitService } from "../../workspace-git-service.js";
|
|
@@ -62,5 +63,23 @@ export interface PaseoToolHostDependencies {
|
|
|
62
63
|
judgeRelayGate?: JudgeRelayGate;
|
|
63
64
|
logger: Logger;
|
|
64
65
|
}
|
|
66
|
+
export interface ScheduleCreateCadenceArgs {
|
|
67
|
+
at?: string;
|
|
68
|
+
every?: string;
|
|
69
|
+
cron?: string;
|
|
70
|
+
timezone?: string;
|
|
71
|
+
}
|
|
72
|
+
export interface ResolvedScheduleCreateCadence {
|
|
73
|
+
cadence: ScheduleCadence;
|
|
74
|
+
/**
|
|
75
|
+
* `at` is a one-off. It lowers to an `every` cadence whose interval is the
|
|
76
|
+
* distance to the instant, pinned to a single run. An `every` schedule stores
|
|
77
|
+
* the computed `nextRunAt`, so a daemon that was down at that moment fires it
|
|
78
|
+
* late on the next tick, which is what a reminder wants. A pinned cron would
|
|
79
|
+
* instead skip silently to the next calendar match a year away.
|
|
80
|
+
*/
|
|
81
|
+
oneOff: boolean;
|
|
82
|
+
}
|
|
83
|
+
export declare function resolveScheduleCreateCadence(input: ScheduleCreateCadenceArgs, now: Date): ResolvedScheduleCreateCadence;
|
|
65
84
|
export declare function createPaseoToolCatalog(options: PaseoToolHostDependencies): PaseoToolCatalog;
|
|
66
85
|
//# sourceMappingURL=paseo-tools.d.ts.map
|
|
@@ -141,6 +141,51 @@ function resolveScheduleUpdateCadence(input) {
|
|
|
141
141
|
}
|
|
142
142
|
return undefined;
|
|
143
143
|
}
|
|
144
|
+
// A duration ("90m", "2h30m", "45" seconds) rather than a timestamp. Checked
|
|
145
|
+
// first so `at` can accept both without Date parsing guessing at bare numbers.
|
|
146
|
+
const SCHEDULE_DURATION_ONLY_PATTERN = /^(?:\d+|(?:\d+[smhd])+)$/;
|
|
147
|
+
function resolveScheduleAtInstant(at, now) {
|
|
148
|
+
if (SCHEDULE_DURATION_ONLY_PATTERN.test(at)) {
|
|
149
|
+
return new Date(now.getTime() + parseDurationString(at));
|
|
150
|
+
}
|
|
151
|
+
const parsed = new Date(at);
|
|
152
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
153
|
+
throw new Error(`Invalid at value: ${at}. Use an ISO 8601 instant (2026-08-21T21:30:00-03:00) or a duration from now (90m).`);
|
|
154
|
+
}
|
|
155
|
+
return parsed;
|
|
156
|
+
}
|
|
157
|
+
export function resolveScheduleCreateCadence(input, now) {
|
|
158
|
+
const at = normalizeScheduleCadenceArg(input.at);
|
|
159
|
+
const every = normalizeScheduleCadenceArg(input.every);
|
|
160
|
+
const cron = normalizeScheduleCadenceArg(input.cron);
|
|
161
|
+
const timeZone = normalizeScheduleTimeZoneArg(input.timezone);
|
|
162
|
+
const provided = [at, every, cron].filter((value) => value !== undefined);
|
|
163
|
+
if (provided.length !== 1) {
|
|
164
|
+
throw new Error("Specify exactly one of at, every, or cron");
|
|
165
|
+
}
|
|
166
|
+
if (timeZone !== undefined && cron === undefined) {
|
|
167
|
+
throw new Error("timezone can only be used with cron");
|
|
168
|
+
}
|
|
169
|
+
if (at !== undefined) {
|
|
170
|
+
const instant = resolveScheduleAtInstant(at, now);
|
|
171
|
+
const everyMs = instant.getTime() - now.getTime();
|
|
172
|
+
if (everyMs <= 0) {
|
|
173
|
+
throw new Error(`at must be in the future: ${instant.toISOString()} is not after ${now.toISOString()}`);
|
|
174
|
+
}
|
|
175
|
+
return { cadence: { type: "every", everyMs }, oneOff: true };
|
|
176
|
+
}
|
|
177
|
+
if (every !== undefined) {
|
|
178
|
+
return { cadence: { type: "every", everyMs: parseDurationString(every) }, oneOff: false };
|
|
179
|
+
}
|
|
180
|
+
return {
|
|
181
|
+
cadence: {
|
|
182
|
+
type: "cron",
|
|
183
|
+
expression: cron,
|
|
184
|
+
...(timeZone !== undefined ? { timezone: timeZone } : {}),
|
|
185
|
+
},
|
|
186
|
+
oneOff: false,
|
|
187
|
+
};
|
|
188
|
+
}
|
|
144
189
|
function resolveScheduleUpdateExpiresAt(input) {
|
|
145
190
|
if (input.expiresIn !== undefined && input.clearExpires) {
|
|
146
191
|
throw new Error("Specify at most one of expiresIn or clearExpires");
|
|
@@ -277,18 +322,6 @@ export function createPaseoToolCatalog(options) {
|
|
|
277
322
|
return tool.handler(await parseToolInput(tool, input), context);
|
|
278
323
|
},
|
|
279
324
|
});
|
|
280
|
-
const buildCronScheduleCadence = (input) => {
|
|
281
|
-
const expression = input.cron?.trim() ?? "";
|
|
282
|
-
if (!expression) {
|
|
283
|
-
throw new Error("cron is required");
|
|
284
|
-
}
|
|
285
|
-
const timezone = normalizeScheduleTimeZoneArg(input.timezone);
|
|
286
|
-
return {
|
|
287
|
-
type: "cron",
|
|
288
|
-
expression,
|
|
289
|
-
...(timezone !== undefined ? { timezone } : {}),
|
|
290
|
-
};
|
|
291
|
-
};
|
|
292
325
|
const buildScheduleExpiry = (expiresIn) => {
|
|
293
326
|
return expiresIn === undefined
|
|
294
327
|
? undefined
|
|
@@ -427,6 +460,59 @@ export function createPaseoToolCatalog(options) {
|
|
|
427
460
|
},
|
|
428
461
|
};
|
|
429
462
|
};
|
|
463
|
+
// A schedule bound to an agent that is gone can never fire: the startup sweep
|
|
464
|
+
// completes it on sight. Fail loudly at create time instead of handing back a
|
|
465
|
+
// schedule that reads active and is already dead.
|
|
466
|
+
const assertScheduleTargetAgentExists = async (agentId) => {
|
|
467
|
+
if (agentManager.getAgent(agentId)) {
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
const record = await agentStorage.get(agentId);
|
|
471
|
+
if (!record || record.internal) {
|
|
472
|
+
throw new Error(`Agent ${agentId} not found`);
|
|
473
|
+
}
|
|
474
|
+
if (record.archivedAt) {
|
|
475
|
+
throw new Error(`Agent ${agentId} is archived and cannot be a schedule target`);
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
const resolveScheduleCreateTarget = async (params) => {
|
|
479
|
+
const target = params.target?.trim();
|
|
480
|
+
// Default stays new-agent so callers written against the narrower schema
|
|
481
|
+
// keep the behaviour they had before target existed.
|
|
482
|
+
if (!target || target === "new-agent") {
|
|
483
|
+
return resolveNewAgentScheduleTarget({
|
|
484
|
+
...(params.provider !== undefined ? { provider: params.provider } : {}),
|
|
485
|
+
...(params.cwd !== undefined ? { cwd: params.cwd } : {}),
|
|
486
|
+
});
|
|
487
|
+
}
|
|
488
|
+
if (params.provider !== undefined || params.cwd !== undefined) {
|
|
489
|
+
throw new Error("provider and cwd can only be used with target new-agent");
|
|
490
|
+
}
|
|
491
|
+
if (target === "self") {
|
|
492
|
+
if (!callerAgentId) {
|
|
493
|
+
throw new Error('target "self" requires an agent-scoped session');
|
|
494
|
+
}
|
|
495
|
+
resolveCallerAgent();
|
|
496
|
+
return { type: "agent", agentId: callerAgentId };
|
|
497
|
+
}
|
|
498
|
+
await assertScheduleTargetAgentExists(target);
|
|
499
|
+
return { type: "agent", agentId: target };
|
|
500
|
+
};
|
|
501
|
+
// Recurring creates go through createOrReplace so an agent re-registering the
|
|
502
|
+
// same standing schedule refreshes it instead of minting a twin. A one-off must
|
|
503
|
+
// NOT: createOrReplace carries the existing schedule's `runs` forward, and
|
|
504
|
+
// shouldCompleteSchedule counts them against the incoming maxRuns before tick
|
|
505
|
+
// looks at nextRunAt, so replacing a schedule that has already run with
|
|
506
|
+
// maxRuns 1 completes it on the next tick without ever firing. There is also
|
|
507
|
+
// nothing to make idempotent about a one-off: two calls mean two reminders.
|
|
508
|
+
// Covered by schedule/service.test.ts "replacing a schedule that has run with
|
|
509
|
+
// maxRuns 1 buries it without firing".
|
|
510
|
+
const persistNewSchedule = async (input, oneOff) => {
|
|
511
|
+
if (!scheduleService) {
|
|
512
|
+
throw new Error("Schedule service is not configured");
|
|
513
|
+
}
|
|
514
|
+
return oneOff ? scheduleService.create(input) : scheduleService.createOrReplace(input);
|
|
515
|
+
};
|
|
430
516
|
const ProviderModelInputSchema = AgentProviderEnum.trim()
|
|
431
517
|
.refine((value) => value.includes("/"), {
|
|
432
518
|
message: "provider must be provider/model, for example codex/gpt-5.4",
|
|
@@ -1609,39 +1695,80 @@ export function createPaseoToolCatalog(options) {
|
|
|
1609
1695
|
});
|
|
1610
1696
|
registerTool("create_schedule", {
|
|
1611
1697
|
title: "Create schedule",
|
|
1612
|
-
description: "
|
|
1698
|
+
description: "Schedule a prompt for later: once at a specific time (at), on a repeating interval (every), or on a cron cadence (cron). The prompt can start a fresh agent, come back to you (target self), or go to another agent by id. Prefer this over an in-session loop whenever the work must survive this session ending, since schedules are owned by the daemon.",
|
|
1613
1699
|
inputSchema: {
|
|
1614
1700
|
prompt: z.string().trim().min(1, "prompt is required"),
|
|
1615
|
-
|
|
1701
|
+
at: z
|
|
1702
|
+
.string()
|
|
1703
|
+
.trim()
|
|
1704
|
+
.min(1)
|
|
1705
|
+
.optional()
|
|
1706
|
+
.describe('One-off delivery time. An ISO 8601 instant ("2026-08-21T21:30:00-03:00") or a duration from now ("90m", "2h30m"). Runs exactly once. Mutually exclusive with every and cron.'),
|
|
1707
|
+
every: z
|
|
1708
|
+
.string()
|
|
1709
|
+
.trim()
|
|
1710
|
+
.min(1)
|
|
1711
|
+
.optional()
|
|
1712
|
+
.describe('Repeating interval, for example "5m", "1h", "2h30m", "1d". Mutually exclusive with at and cron.'),
|
|
1713
|
+
cron: z
|
|
1714
|
+
.string()
|
|
1715
|
+
.trim()
|
|
1716
|
+
.min(1)
|
|
1717
|
+
.optional()
|
|
1718
|
+
.describe('Repeating cron expression, for example "0 9 * * 1-5". Mutually exclusive with at and every.'),
|
|
1616
1719
|
timezone: z
|
|
1617
1720
|
.string()
|
|
1618
1721
|
.trim()
|
|
1619
1722
|
.min(1)
|
|
1620
1723
|
.optional()
|
|
1621
|
-
.describe("IANA time zone for the cron cadence. For example: America/New_York."),
|
|
1622
|
-
name: z
|
|
1623
|
-
|
|
1624
|
-
|
|
1724
|
+
.describe("IANA time zone for the cron cadence. Requires cron. For example: America/New_York."),
|
|
1725
|
+
name: z
|
|
1726
|
+
.string()
|
|
1727
|
+
.optional()
|
|
1728
|
+
.describe("Reusing a name with the same target refreshes that schedule in place instead of creating a second one."),
|
|
1729
|
+
target: z
|
|
1730
|
+
.string()
|
|
1731
|
+
.trim()
|
|
1732
|
+
.min(1)
|
|
1733
|
+
.optional()
|
|
1734
|
+
.describe('Who receives the prompt: "new-agent" (default) starts a fresh agent each run, "self" delivers to you, or an agent id delivers to that agent.'),
|
|
1735
|
+
provider: AgentProviderEnum.optional().describe("Provider, or provider/model (for example: codex or codex/gpt-5.4). Only valid with target new-agent."),
|
|
1736
|
+
cwd: z.string().optional().describe("Working directory. Only valid with target new-agent."),
|
|
1625
1737
|
maxRuns: z.number().int().positive().optional(),
|
|
1626
1738
|
expiresIn: z.string().optional(),
|
|
1739
|
+
runNow: z
|
|
1740
|
+
.boolean()
|
|
1741
|
+
.optional()
|
|
1742
|
+
.describe("Also fire once immediately on create. Defaults to true for every, false for cron. Not allowed with at."),
|
|
1627
1743
|
},
|
|
1628
1744
|
outputSchema: ScheduleSummarySchema.shape,
|
|
1629
|
-
}, async ({ prompt, cron, timezone, name, provider, cwd, maxRuns, expiresIn }) => {
|
|
1745
|
+
}, async ({ prompt, at, every, cron, timezone, name, target, provider, cwd, maxRuns, expiresIn, runNow, }) => {
|
|
1630
1746
|
if (!scheduleService) {
|
|
1631
1747
|
throw new Error("Schedule service is not configured");
|
|
1632
1748
|
}
|
|
1749
|
+
const { cadence, oneOff } = resolveScheduleCreateCadence({ at, every, cron, timezone }, new Date());
|
|
1750
|
+
if (oneOff && maxRuns !== undefined && maxRuns !== 1) {
|
|
1751
|
+
throw new Error("maxRuns cannot be combined with at: an at schedule runs exactly once");
|
|
1752
|
+
}
|
|
1753
|
+
if (oneOff && runNow === true) {
|
|
1754
|
+
throw new Error("runNow cannot be combined with at: an at schedule runs only at its time");
|
|
1755
|
+
}
|
|
1756
|
+
const resolvedTarget = await resolveScheduleCreateTarget({ target, provider, cwd });
|
|
1633
1757
|
const expiresAt = buildScheduleExpiry(expiresIn);
|
|
1634
|
-
const
|
|
1758
|
+
const scheduleInput = {
|
|
1635
1759
|
prompt: prompt.trim(),
|
|
1636
|
-
cadence
|
|
1637
|
-
|
|
1638
|
-
...(timezone !== undefined ? { timezone } : {}),
|
|
1639
|
-
}),
|
|
1640
|
-
target: resolveNewAgentScheduleTarget({ provider, cwd }),
|
|
1760
|
+
cadence,
|
|
1761
|
+
target: resolvedTarget,
|
|
1641
1762
|
...(name?.trim() ? { name: name.trim() } : {}),
|
|
1642
|
-
...(
|
|
1763
|
+
...(oneOff
|
|
1764
|
+
? { maxRuns: 1, runOnCreate: false }
|
|
1765
|
+
: {
|
|
1766
|
+
...(maxRuns === undefined ? {} : { maxRuns }),
|
|
1767
|
+
...(runNow === undefined ? {} : { runOnCreate: runNow }),
|
|
1768
|
+
}),
|
|
1643
1769
|
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
1644
|
-
}
|
|
1770
|
+
};
|
|
1771
|
+
const schedule = await persistNewSchedule(scheduleInput, oneOff);
|
|
1645
1772
|
return {
|
|
1646
1773
|
content: [],
|
|
1647
1774
|
structuredContent: ensureValidJson(toScheduleSummary(schedule)),
|
|
@@ -1649,22 +1776,43 @@ export function createPaseoToolCatalog(options) {
|
|
|
1649
1776
|
});
|
|
1650
1777
|
registerTool("create_heartbeat", {
|
|
1651
1778
|
title: "Create heartbeat",
|
|
1652
|
-
description: "
|
|
1779
|
+
description: "Schedule a prompt back to yourself: once at a specific time (at), on a repeating interval (every), or on a cron cadence (cron). Equivalent to create_schedule with target self.",
|
|
1653
1780
|
inputSchema: {
|
|
1654
1781
|
prompt: z.string().trim().min(1, "prompt is required"),
|
|
1655
|
-
|
|
1782
|
+
at: z
|
|
1783
|
+
.string()
|
|
1784
|
+
.trim()
|
|
1785
|
+
.min(1)
|
|
1786
|
+
.optional()
|
|
1787
|
+
.describe('One-off delivery time. An ISO 8601 instant ("2026-08-21T21:30:00-03:00") or a duration from now ("90m"). Runs exactly once. Mutually exclusive with every and cron.'),
|
|
1788
|
+
every: z
|
|
1789
|
+
.string()
|
|
1790
|
+
.trim()
|
|
1791
|
+
.min(1)
|
|
1792
|
+
.optional()
|
|
1793
|
+
.describe('Repeating interval, for example "5m", "1h", "2h30m". Mutually exclusive with at and cron.'),
|
|
1794
|
+
cron: z
|
|
1795
|
+
.string()
|
|
1796
|
+
.trim()
|
|
1797
|
+
.min(1)
|
|
1798
|
+
.optional()
|
|
1799
|
+
.describe('Repeating cron expression, for example "0 9 * * 1-5". Mutually exclusive with at and every.'),
|
|
1656
1800
|
timezone: z
|
|
1657
1801
|
.string()
|
|
1658
1802
|
.trim()
|
|
1659
1803
|
.min(1)
|
|
1660
1804
|
.optional()
|
|
1661
|
-
.describe("IANA time zone for the cron cadence. For example: America/New_York."),
|
|
1805
|
+
.describe("IANA time zone for the cron cadence. Requires cron. For example: America/New_York."),
|
|
1662
1806
|
name: z.string().optional(),
|
|
1663
1807
|
maxRuns: z.number().int().positive().optional(),
|
|
1664
1808
|
expiresIn: z.string().optional(),
|
|
1809
|
+
runNow: z
|
|
1810
|
+
.boolean()
|
|
1811
|
+
.optional()
|
|
1812
|
+
.describe("Also fire once immediately on create. Defaults to true for every, false for cron. Not allowed with at."),
|
|
1665
1813
|
},
|
|
1666
1814
|
outputSchema: ScheduleSummarySchema.shape,
|
|
1667
|
-
}, async ({ prompt, cron, timezone, name, maxRuns, expiresIn }) => {
|
|
1815
|
+
}, async ({ prompt, at, every, cron, timezone, name, maxRuns, expiresIn, runNow }) => {
|
|
1668
1816
|
if (!scheduleService) {
|
|
1669
1817
|
throw new Error("Schedule service is not configured");
|
|
1670
1818
|
}
|
|
@@ -1672,23 +1820,50 @@ export function createPaseoToolCatalog(options) {
|
|
|
1672
1820
|
throw new Error("create_heartbeat requires an agent-scoped session");
|
|
1673
1821
|
}
|
|
1674
1822
|
resolveCallerAgent();
|
|
1823
|
+
const { cadence, oneOff } = resolveScheduleCreateCadence({ at, every, cron, timezone }, new Date());
|
|
1824
|
+
if (oneOff && maxRuns !== undefined && maxRuns !== 1) {
|
|
1825
|
+
throw new Error("maxRuns cannot be combined with at: an at schedule runs exactly once");
|
|
1826
|
+
}
|
|
1827
|
+
if (oneOff && runNow === true) {
|
|
1828
|
+
throw new Error("runNow cannot be combined with at: an at schedule runs only at its time");
|
|
1829
|
+
}
|
|
1675
1830
|
const expiresAt = buildScheduleExpiry(expiresIn);
|
|
1676
|
-
const
|
|
1831
|
+
const scheduleInput = {
|
|
1677
1832
|
prompt: prompt.trim(),
|
|
1678
|
-
cadence
|
|
1679
|
-
cron,
|
|
1680
|
-
...(timezone !== undefined ? { timezone } : {}),
|
|
1681
|
-
}),
|
|
1833
|
+
cadence,
|
|
1682
1834
|
target: { type: "agent", agentId: callerAgentId },
|
|
1683
1835
|
...(name?.trim() ? { name: name.trim() } : {}),
|
|
1684
|
-
...(
|
|
1836
|
+
...(oneOff
|
|
1837
|
+
? { maxRuns: 1, runOnCreate: false }
|
|
1838
|
+
: {
|
|
1839
|
+
...(maxRuns === undefined ? {} : { maxRuns }),
|
|
1840
|
+
...(runNow === undefined ? {} : { runOnCreate: runNow }),
|
|
1841
|
+
}),
|
|
1685
1842
|
...(expiresAt === undefined ? {} : { expiresAt }),
|
|
1686
|
-
}
|
|
1843
|
+
};
|
|
1844
|
+
const schedule = await persistNewSchedule(scheduleInput, oneOff);
|
|
1687
1845
|
return {
|
|
1688
1846
|
content: [],
|
|
1689
1847
|
structuredContent: ensureValidJson(toScheduleSummary(schedule)),
|
|
1690
1848
|
};
|
|
1691
1849
|
});
|
|
1850
|
+
registerTool("run_schedule_once", {
|
|
1851
|
+
title: "Run schedule once",
|
|
1852
|
+
description: "Fire a schedule immediately, out of band. The cadence and the next scheduled run are left untouched, but the run is recorded and counts toward maxRuns.",
|
|
1853
|
+
inputSchema: {
|
|
1854
|
+
id: z.string(),
|
|
1855
|
+
},
|
|
1856
|
+
outputSchema: StoredScheduleSchema.shape,
|
|
1857
|
+
}, async ({ id }) => {
|
|
1858
|
+
if (!scheduleService) {
|
|
1859
|
+
throw new Error("Schedule service is not configured");
|
|
1860
|
+
}
|
|
1861
|
+
const schedule = await scheduleService.runOnce(id);
|
|
1862
|
+
return {
|
|
1863
|
+
content: [],
|
|
1864
|
+
structuredContent: ensureValidJson(schedule),
|
|
1865
|
+
};
|
|
1866
|
+
});
|
|
1692
1867
|
registerTool("list_schedules", {
|
|
1693
1868
|
title: "List schedules",
|
|
1694
1869
|
description: "List all schedules managed by the daemon.",
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* - `create_terminal`, `kill_terminal`, `send_terminal_keys` arbitrary command execution.
|
|
43
43
|
* - `create_schedule`, `update_schedule`, `delete_schedule`,
|
|
44
44
|
* `pause_schedule`, `resume_schedule`, `create_heartbeat` persistent side effects.
|
|
45
|
+
* - `run_schedule_once` fires a schedule now, which starts or prompts an agent.
|
|
45
46
|
* - `rename_workspace` mutates.
|
|
46
47
|
* - `speak` an outward side effect, and voice is out of v1 scope.
|
|
47
48
|
* - `wait_for_agent` not mutating, but it BLOCKS. An event-woken judge
|
|
@@ -42,6 +42,7 @@
|
|
|
42
42
|
* - `create_terminal`, `kill_terminal`, `send_terminal_keys` arbitrary command execution.
|
|
43
43
|
* - `create_schedule`, `update_schedule`, `delete_schedule`,
|
|
44
44
|
* `pause_schedule`, `resume_schedule`, `create_heartbeat` persistent side effects.
|
|
45
|
+
* - `run_schedule_once` fires a schedule now, which starts or prompts an agent.
|
|
45
46
|
* - `rename_workspace` mutates.
|
|
46
47
|
* - `speak` an outward side effect, and voice is out of v1 scope.
|
|
47
48
|
* - `wait_for_agent` not mutating, but it BLOCKS. An event-woken judge
|
|
@@ -41,6 +41,7 @@ export function buildConfigOverrides(record) {
|
|
|
41
41
|
extra: record.config?.extra ?? undefined,
|
|
42
42
|
systemPrompt: record.config?.systemPrompt ?? undefined,
|
|
43
43
|
mcpServers: record.config?.mcpServers ?? undefined,
|
|
44
|
+
allowedTools: record.config?.allowedTools ?? undefined,
|
|
44
45
|
});
|
|
45
46
|
}
|
|
46
47
|
export function buildSessionConfig(record, options) {
|
|
@@ -58,6 +59,7 @@ export function buildSessionConfig(record, options) {
|
|
|
58
59
|
extra: overrides.extra,
|
|
59
60
|
systemPrompt: overrides.systemPrompt,
|
|
60
61
|
mcpServers: overrides.mcpServers,
|
|
62
|
+
allowedTools: overrides.allowedTools,
|
|
61
63
|
});
|
|
62
64
|
}
|
|
63
65
|
export function isStoredAgentProviderAvailable(record, validProviders) {
|
|
@@ -44,6 +44,28 @@ export function workspaceIdsOnCheckout(workspaces, cwd) {
|
|
|
44
44
|
.filter((workspace) => !workspace.archivedAt && resolve(workspace.cwd) === resolvedCwd)
|
|
45
45
|
.map((workspace) => workspace.workspaceId);
|
|
46
46
|
}
|
|
47
|
+
/**
|
|
48
|
+
* Build the bucket-derivation input for one agent snapshot.
|
|
49
|
+
*
|
|
50
|
+
* Exists so every call site in this file feeds `deriveAgentStateBucket` the
|
|
51
|
+
* SAME shape. It previously did not: two call sites hand-built the object and
|
|
52
|
+
* both omitted `activeChildCount`, so server-derived workspace buckets could
|
|
53
|
+
* never see armed background work while the app-side derivation could. The same
|
|
54
|
+
* agent then bucketed as "done" in the workspace list and "running" on its
|
|
55
|
+
* kanban card. One helper, one shape, no drift.
|
|
56
|
+
*/
|
|
57
|
+
function toBucketInput(agent) {
|
|
58
|
+
return {
|
|
59
|
+
status: agent.status,
|
|
60
|
+
pendingPermissionCount: agent.pendingPermissions?.length ?? 0,
|
|
61
|
+
requiresAttention: agent.requiresAttention,
|
|
62
|
+
attentionReason: agent.attentionReason ?? null,
|
|
63
|
+
// Monitors, crons and backgrounded shells still alive on this agent. Live
|
|
64
|
+
// SUBAGENTS are not counted here — they arrive as their own snapshots and
|
|
65
|
+
// are attributed to the delegation root in applyAgentBucketContributions.
|
|
66
|
+
activeChildCount: agent.activeBackgroundTaskCount ?? 0,
|
|
67
|
+
};
|
|
68
|
+
}
|
|
47
69
|
export class WorkspaceDirectory {
|
|
48
70
|
constructor(deps) {
|
|
49
71
|
this.deps = deps;
|
|
@@ -166,15 +188,13 @@ export class WorkspaceDirectory {
|
|
|
166
188
|
continue;
|
|
167
189
|
}
|
|
168
190
|
workspaceAgent = parentAgent;
|
|
169
|
-
|
|
191
|
+
// A live subagent means the ROOT's own turn is not in flight — it is
|
|
192
|
+
// waiting on delegated work that will hand control back by itself.
|
|
193
|
+
// That is "pending", not "running"; see agent-state-bucket.ts.
|
|
194
|
+
bucket = "pending";
|
|
170
195
|
}
|
|
171
196
|
else {
|
|
172
|
-
bucket = deriveAgentStateBucket(
|
|
173
|
-
status: agent.status,
|
|
174
|
-
pendingPermissionCount: agent.pendingPermissions?.length ?? 0,
|
|
175
|
-
requiresAttention: agent.requiresAttention,
|
|
176
|
-
attentionReason: agent.attentionReason ?? null,
|
|
177
|
-
});
|
|
197
|
+
bucket = deriveAgentStateBucket(toBucketInput(agent));
|
|
178
198
|
}
|
|
179
199
|
const workspaceId = workspaceAgent.workspaceId;
|
|
180
200
|
if (!workspaceId) {
|
|
@@ -210,7 +230,10 @@ export class WorkspaceDirectory {
|
|
|
210
230
|
existing.status = bucket;
|
|
211
231
|
}
|
|
212
232
|
const entries = terminalEntriesByWorkspaceId.get(workspaceId) ?? [];
|
|
213
|
-
entries.push({
|
|
233
|
+
entries.push({
|
|
234
|
+
bucket,
|
|
235
|
+
changedAtIso: new Date(activity.changedAt).toISOString(),
|
|
236
|
+
});
|
|
214
237
|
terminalEntriesByWorkspaceId.set(workspaceId, entries);
|
|
215
238
|
}
|
|
216
239
|
return terminalEntriesByWorkspaceId;
|
|
@@ -271,12 +294,7 @@ export class WorkspaceDirectory {
|
|
|
271
294
|
findNewestTimestampInBucket(contributingAgents, terminalEntries, winningBucket) {
|
|
272
295
|
const agentTimestamps = contributingAgents
|
|
273
296
|
.filter((agent) => {
|
|
274
|
-
const derived = deriveAgentStateBucket(
|
|
275
|
-
status: agent.status,
|
|
276
|
-
pendingPermissionCount: agent.pendingPermissions?.length ?? 0,
|
|
277
|
-
requiresAttention: agent.requiresAttention,
|
|
278
|
-
attentionReason: agent.attentionReason ?? null,
|
|
279
|
-
});
|
|
297
|
+
const derived = deriveAgentStateBucket(toBucketInput(agent));
|
|
280
298
|
return derived === winningBucket;
|
|
281
299
|
})
|
|
282
300
|
.map((agent) => {
|