@cursor/july 0.1.105 → 0.1.107
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/bin/agent-serve.js +1 -8
- package/dist/channels/bitbucket/types.d.ts +2 -4
- package/dist/channels/bitbucket/types.d.ts.map +1 -1
- package/dist/channels/deployments/types.d.ts +5 -8
- package/dist/channels/deployments/types.d.ts.map +1 -1
- package/dist/channels/github/types.d.ts +2 -4
- package/dist/channels/github/types.d.ts.map +1 -1
- package/dist/channels/gitlab/types.d.ts +2 -5
- package/dist/channels/gitlab/types.d.ts.map +1 -1
- package/dist/channels/origin/types.d.ts +2 -3
- package/dist/channels/origin/types.d.ts.map +1 -1
- package/dist/channels/slack/defaults.d.ts.map +1 -1
- package/dist/channels/slack/defaults.js +3 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +1 -1
- package/dist/channels/slack/live-delivery.d.ts.map +1 -1
- package/dist/channels/slack/live-delivery.js +48 -11
- package/dist/channels/slack/types.d.ts +4 -3
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +3 -3
- package/dist/docs/ab.html +6 -6
- package/dist/docs/assets/{app.BVwaffdK.js → app.CtuckIiL.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DXXZxiMv.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.WuWTZ1x8.js → VPLocalSearchBox.Bkkhnu8K.js} +2 -2
- package/dist/docs/assets/chunks/{theme.BYdI_6sF.js → theme.DhpIe0Pa.js} +2 -2
- package/dist/docs/assets/{guides_opentelemetry.md.COh9jFVg.js → guides_opentelemetry.md.SCRMskno.js} +3 -3
- package/dist/docs/assets/{guides_opentelemetry.md.COh9jFVg.lean.js → guides_opentelemetry.md.SCRMskno.lean.js} +1 -1
- package/dist/docs/assets/{reference_channels.md.DBGcaB7m.js → reference_channels.md.D-qTqwcq.js} +2 -2
- package/dist/docs/assets/{reference_channels.md.DBGcaB7m.lean.js → reference_channels.md.D-qTqwcq.lean.js} +1 -1
- package/dist/docs/assets/{reference_cli.md.C6Lo8SEd.js → reference_cli.md.Dm67hd2D.js} +1 -1
- package/dist/docs/assets/{skills_otel.md.CqhobH3B.js → skills_otel.md.B5Ddv1xH.js} +1 -1
- package/dist/docs/assets/{style.BRuM8477.css → style.DCD_zyg3.css} +1 -1
- package/dist/docs/building-with-agents.html +6 -6
- package/dist/docs/concepts.html +5 -5
- package/dist/docs/deployment.html +6 -6
- package/dist/docs/evals.html +6 -6
- package/dist/docs/guides/agent-to-agent.html +6 -6
- package/dist/docs/guides/cloud-agents.html +6 -6
- package/dist/docs/guides/convert-automation.html +6 -6
- package/dist/docs/guides/github.html +6 -6
- package/dist/docs/guides/grokbot-agents.html +6 -6
- package/dist/docs/guides/human-in-the-loop.html +6 -6
- package/dist/docs/guides/mcp-oauth.html +6 -6
- package/dist/docs/guides/opentelemetry.html +8 -8
- package/dist/docs/guides/opentelemetry.md +3 -4
- package/dist/docs/guides/slack.html +6 -6
- package/dist/docs/guides/webhooks.html +6 -6
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +6 -6
- package/dist/docs/index.html +6 -6
- package/dist/docs/llms-full.txt +22 -28
- package/dist/docs/quickstart.html +6 -6
- package/dist/docs/reference/agent-config.html +6 -6
- package/dist/docs/reference/artifacts.html +6 -6
- package/dist/docs/reference/channels.html +7 -7
- package/dist/docs/reference/channels.md +11 -19
- package/dist/docs/reference/cli.html +8 -8
- package/dist/docs/reference/cli.md +5 -2
- package/dist/docs/reference/connections.html +6 -6
- package/dist/docs/reference/extensions.html +6 -6
- package/dist/docs/reference/hooks.html +6 -6
- package/dist/docs/reference/http-api.html +6 -6
- package/dist/docs/reference/instructions.html +6 -6
- package/dist/docs/reference/playground.html +5 -5
- package/dist/docs/reference/project-layout.html +6 -6
- package/dist/docs/reference/prompt.html +6 -6
- package/dist/docs/reference/result.html +6 -6
- package/dist/docs/reference/schedules.html +6 -6
- package/dist/docs/reference/sessions.html +5 -5
- package/dist/docs/reference/skills.html +6 -6
- package/dist/docs/reference/subagents.html +6 -6
- package/dist/docs/reference/tools.html +6 -6
- package/dist/docs/scaffolding-agents.html +5 -5
- package/dist/docs/skills/ab.html +6 -6
- package/dist/docs/skills/create-agent.html +6 -6
- package/dist/docs/skills/debug.html +5 -5
- package/dist/docs/skills/evals.html +6 -6
- package/dist/docs/skills/framework-map.html +5 -5
- package/dist/docs/skills/github.html +6 -6
- package/dist/docs/skills/hillclimb.html +6 -6
- package/dist/docs/skills/index.html +5 -5
- package/dist/docs/skills/mcp-auth.html +6 -6
- package/dist/docs/skills/otel.html +8 -8
- package/dist/docs/skills/otel.md +3 -3
- package/dist/docs/skills/setup-slack.html +6 -6
- package/dist/docs/storage.html +6 -6
- package/dist/docs/templates/agentic-owners.html +6 -6
- package/dist/docs/templates/agents-md.html +6 -6
- package/dist/docs/templates/code-wiki.html +6 -6
- package/dist/docs/templates/demo.html +6 -6
- package/dist/docs/templates/grokbot-agents.html +6 -6
- package/dist/docs/templates/pr-autofixer.html +6 -6
- package/dist/docs/templates/security-help.html +6 -6
- package/dist/docs/templates/security-reviewer.html +6 -6
- package/dist/docs/templates/triage.html +6 -6
- package/dist/docs/troubleshooting.html +5 -5
- package/dist/index.d.ts +4 -5
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -4
- package/dist/internal/cli-deploy.d.ts.map +1 -1
- package/dist/internal/cli-deploy.js +23 -11
- package/dist/internal/cursor/backend-client.d.ts +25 -0
- package/dist/internal/cursor/backend-client.d.ts.map +1 -1
- package/dist/internal/cursor/backend-client.js +57 -2
- package/dist/internal/deploy-client.d.ts +8 -0
- package/dist/internal/deploy-client.d.ts.map +1 -1
- package/dist/internal/deploy-client.js +14 -0
- package/dist/internal/deploy-manifest.d.ts +3 -0
- package/dist/internal/deploy-manifest.d.ts.map +1 -1
- package/dist/internal/deploy-manifest.js +15 -12
- package/dist/internal/discovery/agent.d.ts.map +1 -1
- package/dist/internal/discovery/agent.js +1 -0
- package/dist/internal/discovery/info.d.ts.map +1 -1
- package/dist/internal/discovery/info.js +7 -3
- package/dist/internal/discovery/modules.d.ts +1 -1
- package/dist/internal/discovery/modules.d.ts.map +1 -1
- package/dist/internal/discovery/modules.js +1 -0
- package/dist/internal/discovery/project.d.ts.map +1 -1
- package/dist/internal/discovery/project.js +3 -0
- package/dist/internal/discovery/reminders.d.ts +7 -0
- package/dist/internal/discovery/reminders.d.ts.map +1 -0
- package/dist/internal/discovery/reminders.js +65 -0
- package/dist/internal/discovery/subagent.js +1 -0
- package/dist/internal/distribution.d.ts +5 -0
- package/dist/internal/distribution.d.ts.map +1 -1
- package/dist/internal/distribution.js +17 -0
- package/dist/internal/framework-storage-selection.d.ts +14 -3
- package/dist/internal/framework-storage-selection.d.ts.map +1 -1
- package/dist/internal/framework-storage-selection.js +36 -6
- package/dist/internal/guest-network.d.ts.map +1 -1
- package/dist/internal/guest-network.js +2 -0
- package/dist/internal/hosted-delivery-protocol.d.ts +48 -0
- package/dist/internal/hosted-delivery-protocol.d.ts.map +1 -1
- package/dist/internal/hosted-delivery-protocol.js +60 -0
- package/dist/internal/hosted-delivery.d.ts +10 -0
- package/dist/internal/hosted-delivery.d.ts.map +1 -1
- package/dist/internal/hosted-delivery.js +83 -18
- package/dist/internal/local-control-plane.d.ts +18 -2
- package/dist/internal/local-control-plane.d.ts.map +1 -1
- package/dist/internal/local-control-plane.js +199 -17
- package/dist/internal/reminder-control-plane-protocol.d.ts +187 -0
- package/dist/internal/reminder-control-plane-protocol.d.ts.map +1 -0
- package/dist/internal/reminder-control-plane-protocol.js +244 -0
- package/dist/internal/reminder-runner.d.ts +48 -6
- package/dist/internal/reminder-runner.d.ts.map +1 -1
- package/dist/internal/reminder-runner.js +336 -48
- package/dist/internal/reminder-schedule.d.ts +5 -2
- package/dist/internal/reminder-schedule.d.ts.map +1 -1
- package/dist/internal/reminder-schedule.js +8 -6
- package/dist/internal/reminder-store.d.ts +24 -7
- package/dist/internal/reminder-store.d.ts.map +1 -1
- package/dist/internal/reminder-store.js +6 -5
- package/dist/internal/request-error-status.d.ts +2 -0
- package/dist/internal/request-error-status.d.ts.map +1 -1
- package/dist/internal/request-error-status.js +22 -3
- package/dist/internal/scm/pr-url.d.ts +40 -0
- package/dist/internal/scm/pr-url.d.ts.map +1 -1
- package/dist/internal/scm/pr-url.js +93 -14
- package/dist/internal/server.d.ts.map +1 -1
- package/dist/internal/server.js +67 -27
- package/dist/internal/session-engine.d.ts +4 -0
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +5 -0
- package/dist/playground/assets/{index-BaQzjaNK.js → index-Bq2HpEQB.js} +23 -23
- package/dist/playground/assets/index-CZKKNlmb.css +1 -0
- package/dist/playground/index.html +2 -2
- package/dist/reminders.d.ts +24 -5
- package/dist/reminders.d.ts.map +1 -1
- package/dist/reminders.js +28 -5
- package/dist/types.d.ts +89 -20
- package/dist/types.d.ts.map +1 -1
- package/docs/guides/opentelemetry.md +3 -4
- package/docs/reference/channels.md +11 -19
- package/docs/reference/cli.md +5 -2
- package/package.json +3 -3
- package/skills/otel/SKILL.md +3 -3
- package/src/bin/agent-serve.ts +1 -9
- package/src/channels/bitbucket/types.ts +2 -4
- package/src/channels/deployments/types.ts +5 -8
- package/src/channels/github/types.ts +2 -4
- package/src/channels/gitlab/types.ts +2 -5
- package/src/channels/origin/types.ts +2 -3
- package/src/channels/slack/defaults.ts +3 -0
- package/src/channels/slack/dispatch.ts +1 -0
- package/src/channels/slack/live-delivery.ts +53 -12
- package/src/channels/slack/types.ts +4 -3
- package/src/index.ts +6 -3
- package/src/internal/cli-deploy.ts +30 -12
- package/src/internal/cursor/backend-client.ts +90 -2
- package/src/internal/deploy-client.ts +16 -0
- package/src/internal/deploy-manifest.ts +16 -11
- package/src/internal/discovery/agent.ts +1 -0
- package/src/internal/discovery/info.ts +6 -0
- package/src/internal/discovery/modules.ts +2 -4
- package/src/internal/discovery/project.ts +3 -0
- package/src/internal/discovery/reminders.ts +65 -0
- package/src/internal/discovery/subagent.ts +1 -0
- package/src/internal/distribution.ts +20 -0
- package/src/internal/framework-storage-selection.ts +43 -6
- package/src/internal/guest-network.ts +3 -0
- package/src/internal/hosted-delivery-protocol.ts +96 -0
- package/src/internal/hosted-delivery.ts +154 -27
- package/src/internal/local-control-plane.ts +281 -22
- package/src/internal/reminder-control-plane-protocol.ts +499 -0
- package/src/internal/reminder-runner.ts +421 -48
- package/src/internal/reminder-schedule.ts +11 -6
- package/src/internal/reminder-store.ts +27 -7
- package/src/internal/request-error-status.ts +27 -3
- package/src/internal/scm/pr-url.ts +167 -20
- package/src/internal/server.ts +69 -16
- package/src/internal/session-engine.ts +5 -0
- package/src/reminders.ts +35 -4
- package/src/types.ts +95 -20
- package/dist/docs/assets/chunks/@localSearchIndexroot.CJxMklAy.js +0 -1
- package/dist/playground/assets/index-CceqnLTX.css +0 -1
- /package/dist/docs/assets/{reference_cli.md.C6Lo8SEd.lean.js → reference_cli.md.Dm67hd2D.lean.js} +0 -0
- /package/dist/docs/assets/{skills_otel.md.CqhobH3B.lean.js → skills_otel.md.B5Ddv1xH.lean.js} +0 -0
|
@@ -1,20 +1,35 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Per-session reminder runner: durable wake subscriptions with
|
|
3
|
-
* prompt XOR run, every/cron/delay/at schedules, and
|
|
4
|
-
* function XOR prompt.
|
|
3
|
+
* prompt XOR named handler XOR run, every/cron/delay/at schedules, and
|
|
4
|
+
* until as function XOR prompt.
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { randomBytes } from "node:crypto";
|
|
8
8
|
import type {
|
|
9
9
|
AuthContext,
|
|
10
|
+
DiscoveredReminder,
|
|
11
|
+
JsonObject,
|
|
12
|
+
JsonValue,
|
|
10
13
|
ReminderCreateInput,
|
|
14
|
+
ReminderDefinition,
|
|
11
15
|
ReminderFireContext,
|
|
12
16
|
ReminderFireResult,
|
|
13
17
|
ReminderInfo,
|
|
18
|
+
ReminderStatus,
|
|
14
19
|
ReminderUntilContext,
|
|
15
20
|
} from "../types.js";
|
|
21
|
+
import { NAME_PATTERN } from "./discovery/names.js";
|
|
16
22
|
import { parseDuration } from "./duration.js";
|
|
17
23
|
import type { PlatformTimers } from "./platform-timers.js";
|
|
24
|
+
import {
|
|
25
|
+
REMINDER_CP_PROTOCOL_VERSION,
|
|
26
|
+
type ReminderControlPlane,
|
|
27
|
+
type ReminderCreateIntent,
|
|
28
|
+
type ReminderExecutionOutcome,
|
|
29
|
+
type ReminderFireDescriptor,
|
|
30
|
+
scheduleSpecFromCreateInput,
|
|
31
|
+
toWireReschedule,
|
|
32
|
+
} from "./reminder-control-plane-protocol.js";
|
|
18
33
|
import {
|
|
19
34
|
defaultBusyPolicy,
|
|
20
35
|
MAX_TIMER_MS,
|
|
@@ -28,6 +43,9 @@ import { SessionBusyError, type SessionEngine } from "./session-engine.js";
|
|
|
28
43
|
|
|
29
44
|
export { ReminderValidationError } from "./reminder-schedule.js";
|
|
30
45
|
|
|
46
|
+
/** Max UTF-8 byte length of `JSON.stringify(args)` on named-handler create. */
|
|
47
|
+
export const REMINDER_ARGS_MAX_BYTES: number = 32 * 1024;
|
|
48
|
+
|
|
31
49
|
export class UnknownReminderError extends Error {
|
|
32
50
|
constructor(
|
|
33
51
|
reminderId: string,
|
|
@@ -43,10 +61,79 @@ interface LiveHandlers {
|
|
|
43
61
|
untilFn?: (ctx: ReminderUntilContext) => Promise<boolean>;
|
|
44
62
|
}
|
|
45
63
|
|
|
64
|
+
export interface ReminderRunnerOptions {
|
|
65
|
+
/** Discovered `agent/reminders/*` handlers, keyed by module stem. */
|
|
66
|
+
namedHandlers?: readonly DiscoveredReminder[];
|
|
67
|
+
/**
|
|
68
|
+
* Architecture v2: reject anonymous `run` and function `until` at create
|
|
69
|
+
* (named `handler` + prompt remain), whether local or hosted.
|
|
70
|
+
*/
|
|
71
|
+
rejectAnonymousHandlers?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* When set (local self-hosted architecture v2), create/cancel/rekey go
|
|
74
|
+
* through the control-plane protocol and fires are guest executions of
|
|
75
|
+
* immutable descriptors.
|
|
76
|
+
*/
|
|
77
|
+
controlPlane?: ReminderControlPlane;
|
|
78
|
+
/** Agent architecture; used with {@link controlPlane} for v2 protocol path. */
|
|
79
|
+
architecture?: "v1" | "v2";
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Server wiring for {@link ReminderRunnerOptions.rejectAnonymousHandlers}:
|
|
84
|
+
* architecture v2 always rejects anonymous run/until (local or hosted).
|
|
85
|
+
*/
|
|
86
|
+
export function rejectAnonymousReminderHandlers(options: {
|
|
87
|
+
noControlPlane?: boolean;
|
|
88
|
+
architecture?: string;
|
|
89
|
+
}): boolean {
|
|
90
|
+
return options.architecture === "v2";
|
|
91
|
+
}
|
|
92
|
+
|
|
46
93
|
function newReminderId(): string {
|
|
47
94
|
return `rem_${randomBytes(8).toString("hex")}`;
|
|
48
95
|
}
|
|
49
96
|
|
|
97
|
+
function isJsonObject(value: unknown): value is JsonObject {
|
|
98
|
+
return (
|
|
99
|
+
typeof value === "object" &&
|
|
100
|
+
value !== null &&
|
|
101
|
+
!Array.isArray(value) &&
|
|
102
|
+
Object.values(value as Record<string, unknown>).every(isJsonValue)
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function isJsonValue(value: unknown): value is JsonValue {
|
|
107
|
+
if (
|
|
108
|
+
value === null ||
|
|
109
|
+
typeof value === "string" ||
|
|
110
|
+
typeof value === "boolean"
|
|
111
|
+
) {
|
|
112
|
+
return true;
|
|
113
|
+
}
|
|
114
|
+
if (typeof value === "number") {
|
|
115
|
+
return Number.isFinite(value);
|
|
116
|
+
}
|
|
117
|
+
if (Array.isArray(value)) {
|
|
118
|
+
return value.every(isJsonValue);
|
|
119
|
+
}
|
|
120
|
+
return isJsonObject(value);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/** Same stem rules as `agent/reminders/<name>.ts` discovery. */
|
|
124
|
+
function isValidReminderHandlerName(name: string): boolean {
|
|
125
|
+
return NAME_PATTERN.test(name) && !name.includes("__");
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
function assertReminderArgsSize(args: JsonObject): void {
|
|
129
|
+
const bytes = Buffer.byteLength(JSON.stringify(args), "utf8");
|
|
130
|
+
if (bytes > REMINDER_ARGS_MAX_BYTES) {
|
|
131
|
+
throw new ReminderValidationError(
|
|
132
|
+
`reminders.create args exceed ${REMINDER_ARGS_MAX_BYTES} bytes (got ${bytes})`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
50
137
|
function toInfo(record: ReminderRecord): ReminderInfo {
|
|
51
138
|
return {
|
|
52
139
|
id: record.id,
|
|
@@ -57,9 +144,11 @@ function toInfo(record: ReminderRecord): ReminderInfo {
|
|
|
57
144
|
scheduleKind: record.scheduleKind,
|
|
58
145
|
scheduleValue: record.scheduleValue,
|
|
59
146
|
prompt: record.prompt,
|
|
147
|
+
handler: record.handler,
|
|
148
|
+
args: record.args,
|
|
60
149
|
untilPrompt: record.untilPrompt,
|
|
61
150
|
busyPolicy: record.busyPolicy,
|
|
62
|
-
status: record.status,
|
|
151
|
+
status: record.status === "firing" ? "armed" : record.status,
|
|
63
152
|
disarmReason: record.disarmReason,
|
|
64
153
|
fireCount: record.fireCount,
|
|
65
154
|
nextFireAt: new Date(record.nextFireAtMs).toISOString(),
|
|
@@ -96,17 +185,36 @@ function buildFirePrompt(record: ReminderRecord): string {
|
|
|
96
185
|
export class ReminderRunner {
|
|
97
186
|
private readonly records = new Map<string, ReminderRecord>();
|
|
98
187
|
private readonly handlers = new Map<string, LiveHandlers>();
|
|
188
|
+
private readonly namedHandlers = new Map<string, ReminderDefinition>();
|
|
189
|
+
private readonly namedHandlerNames = new Map<ReminderDefinition, string>();
|
|
99
190
|
private readonly timers = new Map<string, NodeJS.Timeout>();
|
|
100
191
|
private readonly firing = new Set<string>();
|
|
101
192
|
/** Single-flight idle gates keyed by `channelId:continuationToken`. */
|
|
102
193
|
private readonly idleGates = new Map<string, Promise<void>>();
|
|
103
194
|
private started = false;
|
|
195
|
+
private readonly rejectAnonymousHandlers: boolean;
|
|
196
|
+
private readonly controlPlane: ReminderControlPlane | undefined;
|
|
197
|
+
private readonly architecture: "v1" | "v2" | undefined;
|
|
104
198
|
|
|
105
199
|
constructor(
|
|
106
200
|
private readonly engine: SessionEngine,
|
|
107
201
|
private readonly logger: (line: string) => void = () => {},
|
|
108
|
-
private readonly platformTimers?: PlatformTimers
|
|
109
|
-
|
|
202
|
+
private readonly platformTimers?: PlatformTimers,
|
|
203
|
+
options: ReminderRunnerOptions = {}
|
|
204
|
+
) {
|
|
205
|
+
this.rejectAnonymousHandlers = options.rejectAnonymousHandlers === true;
|
|
206
|
+
this.controlPlane = options.controlPlane;
|
|
207
|
+
this.architecture = options.architecture;
|
|
208
|
+
for (const reminder of options.namedHandlers ?? []) {
|
|
209
|
+
this.namedHandlers.set(reminder.name, reminder.definition);
|
|
210
|
+
this.namedHandlerNames.set(reminder.definition, reminder.name);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/** True when create/cancel/rekey/fire go through the CP protocol. */
|
|
215
|
+
private get usesControlPlaneProtocol(): boolean {
|
|
216
|
+
return this.controlPlane !== undefined && this.architecture === "v2";
|
|
217
|
+
}
|
|
110
218
|
|
|
111
219
|
/**
|
|
112
220
|
* Load reminder documents from the control plane into memory. Does not
|
|
@@ -136,6 +244,13 @@ export class ReminderRunner {
|
|
|
136
244
|
if (record.status !== "armed") {
|
|
137
245
|
continue;
|
|
138
246
|
}
|
|
247
|
+
if (record.handler !== undefined) {
|
|
248
|
+
if (this.namedHandlers.get(record.handler) === undefined) {
|
|
249
|
+
await this.disarm(record, "handler_missing");
|
|
250
|
+
disarmedOrphan += 1;
|
|
251
|
+
}
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
139
254
|
if (
|
|
140
255
|
(record.hasRunHandler === true || record.hasUntilFn === true) &&
|
|
141
256
|
this.handlers.get(record.id) === undefined
|
|
@@ -168,6 +283,16 @@ export class ReminderRunner {
|
|
|
168
283
|
let restored = 0;
|
|
169
284
|
let disarmedOrphan = 0;
|
|
170
285
|
for (const record of armed) {
|
|
286
|
+
if (record.handler !== undefined) {
|
|
287
|
+
if (this.namedHandlers.get(record.handler) === undefined) {
|
|
288
|
+
await this.disarm(record, "handler_missing");
|
|
289
|
+
disarmedOrphan += 1;
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
this.armTimer(record);
|
|
293
|
+
restored += 1;
|
|
294
|
+
continue;
|
|
295
|
+
}
|
|
171
296
|
if (
|
|
172
297
|
(record.hasRunHandler === true || record.hasUntilFn === true) &&
|
|
173
298
|
this.handlers.get(record.id) === undefined
|
|
@@ -249,9 +374,11 @@ export class ReminderRunner {
|
|
|
249
374
|
async create(input: ReminderCreateInput): Promise<ReminderInfo> {
|
|
250
375
|
const hasPrompt = input.prompt !== undefined;
|
|
251
376
|
const hasRun = input.run !== undefined;
|
|
252
|
-
|
|
377
|
+
const hasHandler = input.handler !== undefined;
|
|
378
|
+
const formCount = [hasPrompt, hasRun, hasHandler].filter(Boolean).length;
|
|
379
|
+
if (formCount !== 1) {
|
|
253
380
|
throw new ReminderValidationError(
|
|
254
|
-
"reminders.create requires exactly one of prompt or run"
|
|
381
|
+
"reminders.create requires exactly one of prompt, handler, or run"
|
|
255
382
|
);
|
|
256
383
|
}
|
|
257
384
|
if (input.continuationToken.trim() === "") {
|
|
@@ -264,46 +391,136 @@ export class ReminderRunner {
|
|
|
264
391
|
throw new ReminderValidationError("purpose is required");
|
|
265
392
|
}
|
|
266
393
|
|
|
267
|
-
const schedule = resolveSchedule(input);
|
|
268
|
-
const busyPolicy: ReminderBusyPolicy =
|
|
269
|
-
input.busyPolicy ?? defaultBusyPolicy(schedule.kind);
|
|
270
|
-
const now = new Date().toISOString();
|
|
271
|
-
const id = newReminderId();
|
|
272
|
-
|
|
273
394
|
const untilPrompt =
|
|
274
395
|
typeof input.until === "string" ? input.until : undefined;
|
|
275
396
|
const untilFn = typeof input.until === "function" ? input.until : undefined;
|
|
397
|
+
|
|
398
|
+
if (hasHandler && input.until !== undefined) {
|
|
399
|
+
throw new ReminderValidationError(
|
|
400
|
+
"reminders.create: until is not valid with handler; encode the stop condition in args and the handler body"
|
|
401
|
+
);
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
if (this.rejectAnonymousHandlers) {
|
|
405
|
+
if (hasRun) {
|
|
406
|
+
throw new ReminderValidationError(
|
|
407
|
+
"reminders.create: anonymous run handlers are not durable on architecture v2; author agent/reminders/<name>.ts and pass handler + args"
|
|
408
|
+
);
|
|
409
|
+
}
|
|
410
|
+
if (untilFn !== undefined) {
|
|
411
|
+
throw new ReminderValidationError(
|
|
412
|
+
"reminders.create: function until is not durable on architecture v2; use a string until prompt"
|
|
413
|
+
);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
let handlerName: string | undefined;
|
|
418
|
+
let handlerArgs: JsonObject | undefined;
|
|
419
|
+
if (hasHandler) {
|
|
420
|
+
const handler = input.handler!;
|
|
421
|
+
const name =
|
|
422
|
+
typeof handler === "string"
|
|
423
|
+
? handler.trim()
|
|
424
|
+
: this.namedHandlerNames.get(handler);
|
|
425
|
+
if (name === undefined) {
|
|
426
|
+
throw new ReminderValidationError(
|
|
427
|
+
"Unknown reminder handler definition (expected a default export discovered under agent/reminders/)"
|
|
428
|
+
);
|
|
429
|
+
}
|
|
430
|
+
if (name === "") {
|
|
431
|
+
throw new ReminderValidationError("handler name is required");
|
|
432
|
+
}
|
|
433
|
+
if (!isValidReminderHandlerName(name)) {
|
|
434
|
+
throw new ReminderValidationError(
|
|
435
|
+
name.includes("__")
|
|
436
|
+
? `Invalid reminder handler name "${name}": may not contain "__"`
|
|
437
|
+
: `Invalid reminder handler name "${name}": must match ${NAME_PATTERN}`
|
|
438
|
+
);
|
|
439
|
+
}
|
|
440
|
+
if (this.namedHandlers.get(name) === undefined) {
|
|
441
|
+
throw new ReminderValidationError(
|
|
442
|
+
`Unknown reminder handler "${name}" (expected agent/reminders/${name}.ts)`
|
|
443
|
+
);
|
|
444
|
+
}
|
|
445
|
+
if (input.args !== undefined && !isJsonObject(input.args)) {
|
|
446
|
+
throw new ReminderValidationError(
|
|
447
|
+
"reminders.create args must be a JSON object"
|
|
448
|
+
);
|
|
449
|
+
}
|
|
450
|
+
handlerName = name;
|
|
451
|
+
handlerArgs = input.args ?? {};
|
|
452
|
+
assertReminderArgsSize(handlerArgs);
|
|
453
|
+
} else if (input.args !== undefined) {
|
|
454
|
+
throw new ReminderValidationError(
|
|
455
|
+
"reminders.create args is only valid with handler"
|
|
456
|
+
);
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
const schedule = resolveSchedule(input);
|
|
460
|
+
const busyPolicy: ReminderBusyPolicy =
|
|
461
|
+
input.busyPolicy ?? defaultBusyPolicy(schedule.kind);
|
|
276
462
|
const hasRunHandler = input.run !== undefined;
|
|
277
463
|
const hasUntilFn = untilFn !== undefined;
|
|
278
464
|
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}
|
|
465
|
+
let record: ReminderRecord;
|
|
466
|
+
if (this.usesControlPlaneProtocol) {
|
|
467
|
+
const intent: ReminderCreateIntent = {
|
|
468
|
+
protocolVersion: REMINDER_CP_PROTOCOL_VERSION,
|
|
469
|
+
purpose: input.purpose,
|
|
470
|
+
channelId: input.channelId,
|
|
471
|
+
continuationToken: input.continuationToken,
|
|
472
|
+
target: input.target,
|
|
473
|
+
// CP recalculates nextFireAtMs / intervalMs from the raw schedule.
|
|
474
|
+
schedule: scheduleSpecFromCreateInput(input),
|
|
475
|
+
prompt: input.prompt,
|
|
476
|
+
handler: handlerName,
|
|
477
|
+
args: handlerArgs,
|
|
478
|
+
hasRunHandler,
|
|
479
|
+
hasUntilFn,
|
|
480
|
+
untilPrompt,
|
|
481
|
+
busyPolicy,
|
|
482
|
+
};
|
|
483
|
+
record = await this.controlPlane!.createReminder(intent);
|
|
484
|
+
this.records.set(record.id, record);
|
|
485
|
+
} else {
|
|
486
|
+
const now = new Date().toISOString();
|
|
487
|
+
const id = newReminderId();
|
|
488
|
+
record = {
|
|
489
|
+
version: 1,
|
|
490
|
+
id,
|
|
491
|
+
purpose: input.purpose,
|
|
492
|
+
channelId: input.channelId,
|
|
493
|
+
continuationToken: input.continuationToken,
|
|
494
|
+
target: input.target,
|
|
495
|
+
scheduleKind: schedule.kind,
|
|
496
|
+
scheduleValue: schedule.value,
|
|
497
|
+
intervalMs: schedule.intervalMs,
|
|
498
|
+
prompt: input.prompt,
|
|
499
|
+
handler: handlerName,
|
|
500
|
+
args: handlerArgs,
|
|
501
|
+
hasRunHandler,
|
|
502
|
+
hasUntilFn,
|
|
503
|
+
untilPrompt,
|
|
504
|
+
busyPolicy,
|
|
505
|
+
status: "armed",
|
|
506
|
+
fireCount: 0,
|
|
507
|
+
nextFireAtMs: schedule.nextFireAtMs,
|
|
508
|
+
generation: 0,
|
|
509
|
+
createdAt: now,
|
|
510
|
+
updatedAt: now,
|
|
511
|
+
};
|
|
512
|
+
await this.persist(record);
|
|
513
|
+
}
|
|
300
514
|
|
|
301
|
-
|
|
302
|
-
this.handlers.set(id, {
|
|
515
|
+
this.handlers.set(record.id, {
|
|
303
516
|
run: input.run,
|
|
304
517
|
untilFn,
|
|
305
518
|
});
|
|
306
|
-
if (
|
|
519
|
+
if (
|
|
520
|
+
this.started &&
|
|
521
|
+
this.platformTimers === undefined &&
|
|
522
|
+
!this.usesControlPlaneProtocol
|
|
523
|
+
) {
|
|
307
524
|
this.armTimer(record);
|
|
308
525
|
}
|
|
309
526
|
|
|
@@ -311,15 +528,18 @@ export class ReminderRunner {
|
|
|
311
528
|
}
|
|
312
529
|
|
|
313
530
|
async list(filter?: {
|
|
314
|
-
status?:
|
|
531
|
+
status?: ReminderStatus;
|
|
315
532
|
purpose?: string;
|
|
316
533
|
continuationToken?: string;
|
|
317
534
|
}): Promise<ReminderInfo[]> {
|
|
318
535
|
const all = await this.allRecords();
|
|
319
536
|
return all
|
|
320
537
|
.filter((r) => {
|
|
321
|
-
if (filter?.status !== undefined
|
|
322
|
-
|
|
538
|
+
if (filter?.status !== undefined) {
|
|
539
|
+
const publicStatus = r.status === "firing" ? "armed" : r.status;
|
|
540
|
+
if (publicStatus !== filter.status) {
|
|
541
|
+
return false;
|
|
542
|
+
}
|
|
323
543
|
}
|
|
324
544
|
if (filter?.purpose !== undefined && r.purpose !== filter.purpose) {
|
|
325
545
|
return false;
|
|
@@ -348,11 +568,22 @@ export class ReminderRunner {
|
|
|
348
568
|
id: string,
|
|
349
569
|
reason = "explicit_cancel"
|
|
350
570
|
): Promise<ReminderInfo | undefined> {
|
|
571
|
+
if (this.usesControlPlaneProtocol) {
|
|
572
|
+
const updated = await this.controlPlane!.cancelReminder(id, reason);
|
|
573
|
+
if (updated === undefined) {
|
|
574
|
+
this.records.delete(id);
|
|
575
|
+
this.handlers.delete(id);
|
|
576
|
+
return undefined;
|
|
577
|
+
}
|
|
578
|
+
this.records.set(id, updated);
|
|
579
|
+
this.handlers.delete(id);
|
|
580
|
+
return toInfo(updated);
|
|
581
|
+
}
|
|
351
582
|
const record = await this.getRecord(id);
|
|
352
583
|
if (record === undefined) {
|
|
353
584
|
return undefined;
|
|
354
585
|
}
|
|
355
|
-
if (record.status !== "armed") {
|
|
586
|
+
if (record.status !== "armed" && record.status !== "firing") {
|
|
356
587
|
return toInfo(record);
|
|
357
588
|
}
|
|
358
589
|
this.clearTimer(id);
|
|
@@ -360,6 +591,7 @@ export class ReminderRunner {
|
|
|
360
591
|
...record,
|
|
361
592
|
status: "cancelled",
|
|
362
593
|
disarmReason: reason,
|
|
594
|
+
generation: (record.generation ?? 0) + 1,
|
|
363
595
|
updatedAt: new Date().toISOString(),
|
|
364
596
|
};
|
|
365
597
|
await this.persist(updated);
|
|
@@ -394,7 +626,12 @@ export class ReminderRunner {
|
|
|
394
626
|
oldToken: string,
|
|
395
627
|
newToken: string
|
|
396
628
|
): Promise<number> {
|
|
397
|
-
|
|
629
|
+
if (this.usesControlPlaneProtocol) {
|
|
630
|
+
return this.controlPlane!.rekeyReminders(channelId, oldToken, newToken);
|
|
631
|
+
}
|
|
632
|
+
const armed = (await this.allRecords()).filter(
|
|
633
|
+
(r) => r.status === "armed" || r.status === "firing"
|
|
634
|
+
);
|
|
398
635
|
let n = 0;
|
|
399
636
|
for (const record of armed) {
|
|
400
637
|
if (
|
|
@@ -406,6 +643,7 @@ export class ReminderRunner {
|
|
|
406
643
|
await this.persist({
|
|
407
644
|
...record,
|
|
408
645
|
continuationToken: newToken,
|
|
646
|
+
generation: (record.generation ?? 0) + 1,
|
|
409
647
|
updatedAt: new Date().toISOString(),
|
|
410
648
|
});
|
|
411
649
|
n += 1;
|
|
@@ -441,6 +679,121 @@ export class ReminderRunner {
|
|
|
441
679
|
return { reminderId, ...result };
|
|
442
680
|
}
|
|
443
681
|
|
|
682
|
+
/**
|
|
683
|
+
* Architecture v2 / CP protocol guest entrypoint: execute an immutable fire
|
|
684
|
+
* descriptor and return an outcome. Does not mint ids or mutate
|
|
685
|
+
* authoritative status/fireCount — the control plane applies the outcome.
|
|
686
|
+
*/
|
|
687
|
+
async executeFire(
|
|
688
|
+
descriptor: ReminderFireDescriptor
|
|
689
|
+
): Promise<ReminderExecutionOutcome> {
|
|
690
|
+
const record = this.recordFromDescriptor(descriptor);
|
|
691
|
+
this.records.set(record.id, record);
|
|
692
|
+
|
|
693
|
+
const live = this.handlers.get(record.id) ?? {};
|
|
694
|
+
if (record.handler !== undefined) {
|
|
695
|
+
if (this.resolveNamedHandler(record.handler) === undefined) {
|
|
696
|
+
return { kind: "stop", reason: "handler_missing" };
|
|
697
|
+
}
|
|
698
|
+
} else if (
|
|
699
|
+
(record.hasRunHandler === true || record.hasUntilFn === true) &&
|
|
700
|
+
live.run === undefined &&
|
|
701
|
+
live.untilFn === undefined
|
|
702
|
+
) {
|
|
703
|
+
return { kind: "stop", reason: "handler_lost_on_restart" };
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
if (live.untilFn !== undefined) {
|
|
707
|
+
try {
|
|
708
|
+
const done = await live.untilFn(this.buildUntilContext(record));
|
|
709
|
+
if (done) {
|
|
710
|
+
return { kind: "stop", reason: "until_fn" };
|
|
711
|
+
}
|
|
712
|
+
} catch (error) {
|
|
713
|
+
this.logger(
|
|
714
|
+
`[agent-sdk] reminder "${record.id}" until() failed: ${
|
|
715
|
+
error instanceof Error ? error.message : String(error)
|
|
716
|
+
}`
|
|
717
|
+
);
|
|
718
|
+
}
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
if (await this.isSessionBusy(record.channelId, record.continuationToken)) {
|
|
722
|
+
if (record.busyPolicy === "skipIfBusy") {
|
|
723
|
+
this.logger(
|
|
724
|
+
`[agent-sdk] reminder "${record.id}" (${record.purpose}) skipped: session busy`
|
|
725
|
+
);
|
|
726
|
+
return { kind: "skipped" };
|
|
727
|
+
}
|
|
728
|
+
await this.whenSessionIdle(record.channelId, record.continuationToken);
|
|
729
|
+
}
|
|
730
|
+
|
|
731
|
+
let result: ReminderFireResult;
|
|
732
|
+
if (record.handler !== undefined) {
|
|
733
|
+
const named = this.resolveNamedHandler(record.handler);
|
|
734
|
+
if (named === undefined) {
|
|
735
|
+
return { kind: "stop", reason: "handler_missing" };
|
|
736
|
+
}
|
|
737
|
+
result = await named.run(this.buildFireContext(record));
|
|
738
|
+
} else if (live.run !== undefined) {
|
|
739
|
+
result = await live.run(this.buildFireContext(record));
|
|
740
|
+
} else if (record.prompt !== undefined) {
|
|
741
|
+
result = await this.deliverPrompt(record);
|
|
742
|
+
} else {
|
|
743
|
+
return { kind: "stop", reason: "missing_handler" };
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
if (result.action === "stop") {
|
|
747
|
+
return { kind: "stop", reason: "run_stop" };
|
|
748
|
+
}
|
|
749
|
+
if (result.action === "skip") {
|
|
750
|
+
return {
|
|
751
|
+
kind: "skipped",
|
|
752
|
+
reschedule:
|
|
753
|
+
result.reschedule !== undefined
|
|
754
|
+
? toWireReschedule(result.reschedule)
|
|
755
|
+
: undefined,
|
|
756
|
+
};
|
|
757
|
+
}
|
|
758
|
+
return {
|
|
759
|
+
kind: "delivered",
|
|
760
|
+
reschedule:
|
|
761
|
+
result.reschedule !== undefined
|
|
762
|
+
? toWireReschedule(result.reschedule)
|
|
763
|
+
: undefined,
|
|
764
|
+
};
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
private recordFromDescriptor(
|
|
768
|
+
descriptor: ReminderFireDescriptor
|
|
769
|
+
): ReminderRecord {
|
|
770
|
+
const now = new Date().toISOString();
|
|
771
|
+
return {
|
|
772
|
+
version: 1,
|
|
773
|
+
id: descriptor.id,
|
|
774
|
+
purpose: descriptor.purpose,
|
|
775
|
+
channelId: descriptor.channelId,
|
|
776
|
+
continuationToken: descriptor.continuationToken,
|
|
777
|
+
target: descriptor.target,
|
|
778
|
+
scheduleKind: descriptor.scheduleKind,
|
|
779
|
+
scheduleValue: descriptor.scheduleValue,
|
|
780
|
+
intervalMs: descriptor.intervalMs,
|
|
781
|
+
prompt: descriptor.prompt,
|
|
782
|
+
handler: descriptor.handler,
|
|
783
|
+
args: descriptor.args,
|
|
784
|
+
hasRunHandler: descriptor.hasRunHandler,
|
|
785
|
+
hasUntilFn: descriptor.hasUntilFn,
|
|
786
|
+
untilPrompt: descriptor.untilPrompt,
|
|
787
|
+
busyPolicy: descriptor.busyPolicy,
|
|
788
|
+
status: "firing",
|
|
789
|
+
fireCount: descriptor.fireCount,
|
|
790
|
+
nextFireAtMs: descriptor.nextFireAtMs,
|
|
791
|
+
generation: descriptor.generation,
|
|
792
|
+
createdAt: now,
|
|
793
|
+
updatedAt: now,
|
|
794
|
+
};
|
|
795
|
+
}
|
|
796
|
+
|
|
444
797
|
private armTimer(record: ReminderRecord): void {
|
|
445
798
|
this.clearTimer(record.id);
|
|
446
799
|
if (!this.started || record.status !== "armed") return;
|
|
@@ -493,6 +846,10 @@ export class ReminderRunner {
|
|
|
493
846
|
}
|
|
494
847
|
}
|
|
495
848
|
|
|
849
|
+
private resolveNamedHandler(name: string): ReminderDefinition | undefined {
|
|
850
|
+
return this.namedHandlers.get(name);
|
|
851
|
+
}
|
|
852
|
+
|
|
496
853
|
private async fire(
|
|
497
854
|
id: string,
|
|
498
855
|
opts: { force?: boolean } = {}
|
|
@@ -515,7 +872,12 @@ export class ReminderRunner {
|
|
|
515
872
|
}
|
|
516
873
|
|
|
517
874
|
const live = this.handlers.get(id) ?? {};
|
|
518
|
-
if (
|
|
875
|
+
if (record.handler !== undefined) {
|
|
876
|
+
if (this.resolveNamedHandler(record.handler) === undefined) {
|
|
877
|
+
await this.disarm(record, "handler_missing");
|
|
878
|
+
return { outcome: "noop", reason: "handler_missing" };
|
|
879
|
+
}
|
|
880
|
+
} else if (
|
|
519
881
|
(record.hasRunHandler === true || record.hasUntilFn === true) &&
|
|
520
882
|
live.run === undefined &&
|
|
521
883
|
live.untilFn === undefined
|
|
@@ -560,9 +922,15 @@ export class ReminderRunner {
|
|
|
560
922
|
}
|
|
561
923
|
|
|
562
924
|
let result: ReminderFireResult;
|
|
563
|
-
if (
|
|
564
|
-
const
|
|
565
|
-
|
|
925
|
+
if (record.handler !== undefined) {
|
|
926
|
+
const named = this.resolveNamedHandler(record.handler);
|
|
927
|
+
if (named === undefined) {
|
|
928
|
+
await this.disarm(record, "handler_missing");
|
|
929
|
+
return { outcome: "noop", reason: "handler_missing" };
|
|
930
|
+
}
|
|
931
|
+
result = await named.run(this.buildFireContext(record));
|
|
932
|
+
} else if (live.run !== undefined) {
|
|
933
|
+
result = await live.run(this.buildFireContext(record));
|
|
566
934
|
} else if (record.prompt !== undefined) {
|
|
567
935
|
result = await this.deliverPrompt(record);
|
|
568
936
|
} else {
|
|
@@ -595,7 +963,7 @@ export class ReminderRunner {
|
|
|
595
963
|
return { outcome: "fired", reason: "skipped" };
|
|
596
964
|
}
|
|
597
965
|
await this.afterFire(latest, "delivered", result.reschedule);
|
|
598
|
-
this.logger(`[agent-sdk] reminder "${id}" (${record.purpose})
|
|
966
|
+
this.logger(`[agent-sdk] reminder "${id}" (${record.purpose}) fired`);
|
|
599
967
|
return { outcome: "fired", reason: "delivered" };
|
|
600
968
|
} finally {
|
|
601
969
|
this.firing.delete(id);
|
|
@@ -662,7 +1030,11 @@ export class ReminderRunner {
|
|
|
662
1030
|
record.continuationToken
|
|
663
1031
|
);
|
|
664
1032
|
if (auth === undefined) {
|
|
665
|
-
|
|
1033
|
+
// Under CP protocol, return stop and let the CP apply/CAS; do not
|
|
1034
|
+
// mutate authoritative status from the guest mid-fire.
|
|
1035
|
+
if (!this.usesControlPlaneProtocol) {
|
|
1036
|
+
await this.disarm(record, "session_gone");
|
|
1037
|
+
}
|
|
666
1038
|
return { action: "stop" };
|
|
667
1039
|
}
|
|
668
1040
|
return this.sendFollowup(record, buildFirePrompt(record), auth);
|
|
@@ -721,6 +1093,7 @@ export class ReminderRunner {
|
|
|
721
1093
|
continuationToken: record.continuationToken,
|
|
722
1094
|
...(record.target ?? {}),
|
|
723
1095
|
},
|
|
1096
|
+
args: record.args ?? {},
|
|
724
1097
|
host: this.engine.host,
|
|
725
1098
|
mcp: this.engine.host.mcp,
|
|
726
1099
|
appAuth: APP_AUTH,
|