@cydm/happy-elves 0.1.0-beta.244 → 0.1.0-beta.247
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/apps/cli/dist/commands/lib/usage.js +2 -1
- package/apps/cli/dist/commands/schedule.js +48 -11
- package/apps/cli/package.json +1 -1
- package/apps/daemon/dist/schedule/runner.d.ts +1 -0
- package/apps/daemon/dist/schedule/runner.js +21 -1
- package/apps/daemon/package.json +1 -1
- package/package.json +1 -1
- package/packages/client/dist/transport.js +23 -2
|
@@ -149,7 +149,8 @@ Session prompts can run memory off, readonly, or readwrite; readwrite writes dai
|
|
|
149
149
|
|
|
150
150
|
Schedule v1 is daemon-local repeat automation. Runs use normal Relay admission
|
|
151
151
|
with deterministic requestId/turnId per occurrence. Offline due times are marked
|
|
152
|
-
missed and are never auto-caught-up.
|
|
152
|
+
missed and are never auto-caught-up. Commands that take a scheduleId auto-locate
|
|
153
|
+
the owning online daemon when --machine is omitted.
|
|
153
154
|
`,
|
|
154
155
|
loop: `Usage:
|
|
155
156
|
happy-elves loop create <sessionId> [--every 15m] (--prompt <text> | --prompt-file <path>) [--stop-on blocked] --json
|
|
@@ -60,40 +60,41 @@ export async function handleSchedule({ domain, action, positional, flags }) {
|
|
|
60
60
|
if (action === "show") {
|
|
61
61
|
const config = await readConfig(flags);
|
|
62
62
|
const client = new ControllerClient(config);
|
|
63
|
-
const
|
|
63
|
+
const scheduleId = requirePositional(positional[0], "scheduleId");
|
|
64
|
+
const machineId = await resolveScheduleOwnerMachineId(client, flags, scheduleId);
|
|
64
65
|
const result = await client.schedule({
|
|
65
66
|
machineId,
|
|
66
67
|
action: "show",
|
|
67
|
-
scheduleId
|
|
68
|
+
scheduleId,
|
|
68
69
|
});
|
|
69
|
-
ok("schedule.show", { schedule: result.schedule });
|
|
70
|
+
ok("schedule.show", { machineId, schedule: result.schedule });
|
|
70
71
|
return true;
|
|
71
72
|
}
|
|
72
73
|
if (action === "pause" || action === "resume" || action === "delete") {
|
|
73
74
|
const config = await readConfig(flags);
|
|
74
75
|
const client = new ControllerClient(config);
|
|
75
|
-
const machineId = await resolveScheduleMachineId(client, flags);
|
|
76
76
|
const scheduleId = requirePositional(positional[0], "scheduleId");
|
|
77
|
+
const machineId = await resolveScheduleOwnerMachineId(client, flags, scheduleId);
|
|
77
78
|
const result = await client.schedule({ machineId, action, scheduleId });
|
|
78
|
-
ok(`schedule.${action}`, { schedule: compactSchedule(result.schedule), scheduleId: result.scheduleId ?? scheduleId });
|
|
79
|
+
ok(`schedule.${action}`, { machineId, schedule: compactSchedule(result.schedule), scheduleId: result.scheduleId ?? scheduleId });
|
|
79
80
|
return true;
|
|
80
81
|
}
|
|
81
82
|
if (action === "run" || action === "run-now") {
|
|
82
83
|
const config = await readConfig(flags);
|
|
83
84
|
const client = new ControllerClient(config);
|
|
84
|
-
const machineId = await resolveScheduleMachineId(client, flags);
|
|
85
85
|
const scheduleId = requirePositional(positional[0], "scheduleId");
|
|
86
|
+
const machineId = await resolveScheduleOwnerMachineId(client, flags, scheduleId);
|
|
86
87
|
const result = await client.schedule({ machineId, action: "run", scheduleId });
|
|
87
|
-
ok("schedule.run", { scheduleId: result.scheduleId ?? scheduleId, run: result.run });
|
|
88
|
+
ok("schedule.run", { machineId, scheduleId: result.scheduleId ?? scheduleId, run: result.run });
|
|
88
89
|
return true;
|
|
89
90
|
}
|
|
90
91
|
if (action === "history") {
|
|
91
92
|
const config = await readConfig(flags);
|
|
92
93
|
const client = new ControllerClient(config);
|
|
93
|
-
const machineId = await resolveScheduleMachineId(client, flags);
|
|
94
94
|
const scheduleId = requirePositional(positional[0], "scheduleId");
|
|
95
|
+
const machineId = await resolveScheduleOwnerMachineId(client, flags, scheduleId);
|
|
95
96
|
const result = await client.schedule({ machineId, action: "history", scheduleId });
|
|
96
|
-
ok("schedule.history", { scheduleId: result.scheduleId ?? scheduleId, runs: result.history ?? [] });
|
|
97
|
+
ok("schedule.history", { machineId, scheduleId: result.scheduleId ?? scheduleId, runs: result.history ?? [] });
|
|
97
98
|
return true;
|
|
98
99
|
}
|
|
99
100
|
if (action === "run-due") {
|
|
@@ -107,11 +108,11 @@ export async function handleSchedule({ domain, action, positional, flags }) {
|
|
|
107
108
|
if (action === "run-missed") {
|
|
108
109
|
const config = await readConfig(flags);
|
|
109
110
|
const client = new ControllerClient(config);
|
|
110
|
-
const machineId = await resolveScheduleMachineId(client, flags);
|
|
111
111
|
const scheduleId = requirePositional(positional[0], "scheduleId");
|
|
112
|
+
const machineId = await resolveScheduleOwnerMachineId(client, flags, scheduleId);
|
|
112
113
|
const occurrenceId = requirePositional(positional[1], "occurrenceId");
|
|
113
114
|
const result = await client.schedule({ machineId, action: "runMissed", scheduleId, occurrenceId });
|
|
114
|
-
ok("schedule.run-missed", { scheduleId: result.scheduleId ?? scheduleId, run: result.run });
|
|
115
|
+
ok("schedule.run-missed", { machineId, scheduleId: result.scheduleId ?? scheduleId, run: result.run });
|
|
115
116
|
return true;
|
|
116
117
|
}
|
|
117
118
|
return false;
|
|
@@ -130,6 +131,42 @@ async function resolveScheduleMachineId(client, flags, sessionId) {
|
|
|
130
131
|
throw new CliError("No daemon machine is available; pass --machine <machineId>", "MACHINE_NOT_FOUND");
|
|
131
132
|
return machine.id;
|
|
132
133
|
}
|
|
134
|
+
async function resolveScheduleOwnerMachineId(client, flags, scheduleId) {
|
|
135
|
+
if (typeof flags.machine === "string" && flags.machine.length > 0)
|
|
136
|
+
return flags.machine;
|
|
137
|
+
const snapshot = await client.snapshot();
|
|
138
|
+
const machines = snapshot.machines.filter((machine) => machine.online);
|
|
139
|
+
if (machines.length === 0) {
|
|
140
|
+
throw new CliError(`Schedule ${scheduleId} cannot be located because no daemon machine is online; pass --machine <machineId> after starting the target daemon`, "MACHINE_NOT_FOUND");
|
|
141
|
+
}
|
|
142
|
+
const checks = await Promise.all(machines.map(async (machine) => {
|
|
143
|
+
try {
|
|
144
|
+
const result = await client.schedule({ machineId: machine.id, action: "list" });
|
|
145
|
+
return {
|
|
146
|
+
machineId: machine.id,
|
|
147
|
+
found: (result.schedules ?? []).some((schedule) => schedule.id === scheduleId),
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
catch (error) {
|
|
151
|
+
return {
|
|
152
|
+
machineId: machine.id,
|
|
153
|
+
found: false,
|
|
154
|
+
error,
|
|
155
|
+
};
|
|
156
|
+
}
|
|
157
|
+
}));
|
|
158
|
+
const matches = checks.filter((check) => check.found);
|
|
159
|
+
if (matches.length === 1)
|
|
160
|
+
return matches[0].machineId;
|
|
161
|
+
if (matches.length > 1) {
|
|
162
|
+
throw new CliError(`Schedule ${scheduleId} exists on multiple online daemons (${matches.map((match) => match.machineId).join(", ")}); pass --machine <machineId>`, "SCHEDULE_OWNER_AMBIGUOUS");
|
|
163
|
+
}
|
|
164
|
+
const failedChecks = checks.filter((check) => check.error);
|
|
165
|
+
const unavailable = failedChecks.length > 0
|
|
166
|
+
? ` ${failedChecks.length}/${machines.length} online daemon(s) did not respond to schedule list.`
|
|
167
|
+
: "";
|
|
168
|
+
throw new CliError(`Schedule not found on any online daemon: ${scheduleId}.${unavailable} Pass --machine <machineId> if the owning daemon is offline or not visible.`, "SCHEDULE_NOT_FOUND");
|
|
169
|
+
}
|
|
133
170
|
function parseScheduleDueAt(value) {
|
|
134
171
|
const date = new Date(value);
|
|
135
172
|
if (!Number.isFinite(date.getTime()))
|
package/apps/cli/package.json
CHANGED
|
@@ -8,6 +8,7 @@ export declare function runDueSchedulesNow(config: DaemonConfig): Promise<Array<
|
|
|
8
8
|
run: ScheduleRunRecord;
|
|
9
9
|
}>>;
|
|
10
10
|
export declare function occurrenceIdFor(schedule: Pick<ScheduleRecord, "id" | "dueAt" | "nextRunAt">, dueAt?: string): string;
|
|
11
|
+
export declare function __testWithActiveScheduleOccurrence<T>(scheduleId: string, occurrenceId: string, action: () => Promise<T> | T): Promise<T>;
|
|
11
12
|
export declare function nextRunAtForSchedule(schedule: Pick<ScheduleRecord, "schedule" | "dueAt" | "runs">, afterMs?: number): string | undefined;
|
|
12
13
|
export declare function nextRunPreviewForSchedule(schedule: Pick<ScheduleRecord, "id" | "schedule" | "dueAt" | "runs">, count?: number, afterMs?: number): string[];
|
|
13
14
|
export declare function settleScheduleTurnTerminal(input: {
|
|
@@ -20,6 +20,8 @@ export async function tickSchedules(config, onlineSinceMs) {
|
|
|
20
20
|
const dueMs = Date.parse(dueAt);
|
|
21
21
|
if (!Number.isFinite(dueMs) || dueMs > nowMs)
|
|
22
22
|
continue;
|
|
23
|
+
if (isScheduleOccurrenceActive(schedule.id, occurrenceId))
|
|
24
|
+
continue;
|
|
23
25
|
if (dueMs < onlineSinceMs) {
|
|
24
26
|
const run = scheduleRun(schedule, occurrenceId, dueAt, "missed", "daemon was offline at due time");
|
|
25
27
|
await recordScheduleRun(schedule.id, run);
|
|
@@ -64,6 +66,8 @@ export async function runDueSchedulesNow(config) {
|
|
|
64
66
|
const dueMs = Date.parse(dueAt);
|
|
65
67
|
if (!Number.isFinite(dueMs) || dueMs > nowMs)
|
|
66
68
|
continue;
|
|
69
|
+
if (isScheduleOccurrenceActive(schedule.id, occurrenceId))
|
|
70
|
+
continue;
|
|
67
71
|
const run = await runScheduleOccurrence(config, schedule, occurrenceId, dueAt, "manual");
|
|
68
72
|
results.push({ scheduleId: schedule.id, run });
|
|
69
73
|
}
|
|
@@ -79,6 +83,22 @@ function stableScheduleId(prefix, ...parts) {
|
|
|
79
83
|
const hash = createHash("sha256").update(parts.join("\0")).digest("base64url").slice(0, 32);
|
|
80
84
|
return `${prefix}_${hash}`;
|
|
81
85
|
}
|
|
86
|
+
function activeScheduleRunKey(scheduleId, occurrenceId) {
|
|
87
|
+
return `${scheduleId}:${occurrenceId}`;
|
|
88
|
+
}
|
|
89
|
+
function isScheduleOccurrenceActive(scheduleId, occurrenceId) {
|
|
90
|
+
return activeScheduleRuns.has(activeScheduleRunKey(scheduleId, occurrenceId));
|
|
91
|
+
}
|
|
92
|
+
export async function __testWithActiveScheduleOccurrence(scheduleId, occurrenceId, action) {
|
|
93
|
+
const key = activeScheduleRunKey(scheduleId, occurrenceId);
|
|
94
|
+
activeScheduleRuns.add(key);
|
|
95
|
+
try {
|
|
96
|
+
return await action();
|
|
97
|
+
}
|
|
98
|
+
finally {
|
|
99
|
+
activeScheduleRuns.delete(key);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
82
102
|
export function nextRunAtForSchedule(schedule, afterMs = Date.now()) {
|
|
83
103
|
const spec = normalizedSpec(schedule.schedule, schedule.dueAt);
|
|
84
104
|
if (spec.type === "manual")
|
|
@@ -162,7 +182,7 @@ export async function settleScheduleTurnTerminal(input) {
|
|
|
162
182
|
return settledRun;
|
|
163
183
|
}
|
|
164
184
|
async function runScheduleOccurrence(config, schedule, occurrenceId, dueAt, reason) {
|
|
165
|
-
const key =
|
|
185
|
+
const key = activeScheduleRunKey(schedule.id, occurrenceId);
|
|
166
186
|
if (activeScheduleRuns.has(key)) {
|
|
167
187
|
throw new Error("Schedule occurrence is already running");
|
|
168
188
|
}
|
package/apps/daemon/package.json
CHANGED
package/package.json
CHANGED
|
@@ -101,11 +101,14 @@ export async function sendCommand(context, command, waitForDone = false, timeout
|
|
|
101
101
|
const targetTurnId = "turnId" in command ? command.turnId : undefined;
|
|
102
102
|
return await new Promise((resolve, reject) => {
|
|
103
103
|
let settled = false;
|
|
104
|
+
let retainedPoller;
|
|
104
105
|
const finish = (result) => {
|
|
105
106
|
if (settled)
|
|
106
107
|
return;
|
|
107
108
|
settled = true;
|
|
108
109
|
globalThis.clearTimeout(timer);
|
|
110
|
+
if (retainedPoller)
|
|
111
|
+
globalThis.clearInterval(retainedPoller);
|
|
109
112
|
ws.close();
|
|
110
113
|
resolve(result);
|
|
111
114
|
};
|
|
@@ -114,6 +117,8 @@ export async function sendCommand(context, command, waitForDone = false, timeout
|
|
|
114
117
|
return;
|
|
115
118
|
settled = true;
|
|
116
119
|
globalThis.clearTimeout(timer);
|
|
120
|
+
if (retainedPoller)
|
|
121
|
+
globalThis.clearInterval(retainedPoller);
|
|
117
122
|
ws.close();
|
|
118
123
|
reject(error);
|
|
119
124
|
};
|
|
@@ -125,7 +130,11 @@ export async function sendCommand(context, command, waitForDone = false, timeout
|
|
|
125
130
|
dispatch: ackedDispatch,
|
|
126
131
|
events,
|
|
127
132
|
});
|
|
133
|
+
let retainedLookupInFlight = false;
|
|
128
134
|
const resolveFromRetainedTurnIfDone = (sessionId, turnId) => {
|
|
135
|
+
if (retainedLookupInFlight)
|
|
136
|
+
return;
|
|
137
|
+
retainedLookupInFlight = true;
|
|
129
138
|
void context.sessionEvents(sessionId, { limit: 1000 })
|
|
130
139
|
.then(({ events: retainedEvents }) => retainedEvents.filter((event) => event.turnId === turnId))
|
|
131
140
|
.then(async (turnEvents) => {
|
|
@@ -142,7 +151,19 @@ export async function sendCommand(context, command, waitForDone = false, timeout
|
|
|
142
151
|
events.sort((left, right) => left.id - right.id);
|
|
143
152
|
finish(commandResultFromAck());
|
|
144
153
|
})
|
|
145
|
-
.catch(() => { })
|
|
154
|
+
.catch(() => { })
|
|
155
|
+
.finally(() => {
|
|
156
|
+
retainedLookupInFlight = false;
|
|
157
|
+
});
|
|
158
|
+
};
|
|
159
|
+
const startRetainedTurnPoller = (sessionId, turnId) => {
|
|
160
|
+
if (retainedPoller)
|
|
161
|
+
return;
|
|
162
|
+
resolveFromRetainedTurnIfDone(sessionId, turnId);
|
|
163
|
+
retainedPoller = globalThis.setInterval(() => {
|
|
164
|
+
if (!settled)
|
|
165
|
+
resolveFromRetainedTurnIfDone(sessionId, turnId);
|
|
166
|
+
}, 2_000);
|
|
146
167
|
};
|
|
147
168
|
const timer = globalThis.setTimeout(() => {
|
|
148
169
|
fail(new ControllerClientError("Timed out waiting for command result", "COMMAND_TIMEOUT", {
|
|
@@ -182,7 +203,7 @@ export async function sendCommand(context, command, waitForDone = false, timeout
|
|
|
182
203
|
finish(commandResultFromAck());
|
|
183
204
|
}
|
|
184
205
|
else if (targetSessionId && targetTurnId) {
|
|
185
|
-
|
|
206
|
+
startRetainedTurnPoller(targetSessionId, targetTurnId);
|
|
186
207
|
}
|
|
187
208
|
return;
|
|
188
209
|
}
|