@cydm/happy-elves 0.1.0-beta.487 → 0.1.0-beta.489
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/package.json +1 -1
- package/apps/daemon/dist/orchestration/handler.d.ts +5 -1
- package/apps/daemon/dist/orchestration/handler.js +52 -1
- package/apps/daemon/dist/paths.d.ts +1 -0
- package/apps/daemon/dist/paths.js +1 -0
- package/apps/daemon/dist/relay/connection.js +3 -1
- package/apps/daemon/dist/relay/remote-command.js +30 -3
- package/apps/daemon/dist/relay/send.d.ts +1 -0
- package/apps/daemon/dist/relay/send.js +508 -54
- package/apps/daemon/dist/runtime/headless-event-projector.d.ts +7 -1
- package/apps/daemon/dist/runtime/headless-event-projector.js +52 -8
- package/apps/daemon/dist/runtime/headless-history-reader.js +49 -2
- package/apps/daemon/dist/session/he-official/common.d.ts +1 -0
- package/apps/daemon/dist/session/he-official/orchestration-commands.d.ts +3 -2
- package/apps/daemon/dist/session/he-official/orchestration-commands.js +53 -6
- package/apps/daemon/dist/session/he-official/session-commands.d.ts +2 -2
- package/apps/daemon/dist/session/he-official/session-commands.js +197 -6
- package/apps/daemon/dist/session/he-official/turn-commands.d.ts +12 -3
- package/apps/daemon/dist/session/he-official/turn-commands.js +78 -3
- package/apps/daemon/dist/session/he-official-tools.js +11 -6
- package/apps/daemon/dist/session/lifecycle.d.ts +1 -1
- package/apps/daemon/dist/session/lifecycle.js +39 -85
- package/apps/daemon/dist/session/native-history-refresh.d.ts +12 -0
- package/apps/daemon/dist/session/native-history-refresh.js +183 -0
- package/apps/daemon/dist/session/runtime-truth.js +8 -2
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/relay-context.js +47 -3
- package/apps/relay/dist/websocket.js +37 -1
- package/package.json +1 -1
- package/packages/headless-runtime/dist/core.js +22 -0
- package/packages/shared/dist/he-official-tools.js +5 -5
- package/packages/shared/dist/protocol-schemas.d.ts +30 -9
- package/packages/shared/dist/protocol-schemas.js +11 -0
- package/packages/shared/dist/protocol-types.d.ts +17 -0
- package/packages/shared/dist/protocol.d.ts +3 -1
- package/packages/shared/dist/protocol.js +3 -1
package/apps/cli/package.json
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DispatchTaskProjection, type MachineCommand, type RuntimeTurnReadSummary } from "../../../../packages/shared/dist/index.js";
|
|
1
|
+
import { type DispatchTaskProjection, type LoadedSessionInventory, type MachineCommand, type RuntimeTurnReadSummary } from "../../../../packages/shared/dist/index.js";
|
|
2
2
|
import type { DaemonConfig } from "../types.js";
|
|
3
3
|
import { type StoredDispatchAdmissionRecord } from "./store.js";
|
|
4
4
|
type DispatchCommand = Extract<MachineCommand, {
|
|
@@ -18,6 +18,10 @@ export type DispatchListResult = {
|
|
|
18
18
|
export declare function handleDispatchCommand(ws: WebSocket, config: DaemonConfig, command: DispatchCommand): Promise<void>;
|
|
19
19
|
export declare function handleQueueCommand(ws: WebSocket, config: DaemonConfig, command: QueueCommand): Promise<void>;
|
|
20
20
|
export declare function dispatchListResultForCommand(config: DaemonConfig, command: DispatchListCommand): Promise<DispatchListResult>;
|
|
21
|
+
export declare function remoteLoadedSessionInventory(config: DaemonConfig, machineId: string, filters?: {
|
|
22
|
+
cwd?: string;
|
|
23
|
+
agent?: string;
|
|
24
|
+
}): Promise<LoadedSessionInventory[]>;
|
|
21
25
|
export declare function publishRemoteCrewQueueEnqueue(config: DaemonConfig, admission: StoredDispatchAdmissionRecord): Promise<void>;
|
|
22
26
|
export declare function deliverQueuedCrewAdmission(config: DaemonConfig, admission: StoredDispatchAdmissionRecord): Promise<StoredDispatchAdmissionRecord>;
|
|
23
27
|
export type CrewSessionRuntimeState = {
|
|
@@ -500,6 +500,7 @@ async function applyFirstAgentRouteGate(config, consumer, command, crewMachineId
|
|
|
500
500
|
const candidates = filterDispatchRouteCandidates([
|
|
501
501
|
...dispatchRouteCandidates(records),
|
|
502
502
|
...await artifactLineageRouteCandidates(config, command, crewMachineId),
|
|
503
|
+
...await remoteLoadedCrewRouteCandidates(config, crewMachineId, command.cwd),
|
|
503
504
|
], {
|
|
504
505
|
cwd: command.cwd,
|
|
505
506
|
crewMachineId,
|
|
@@ -509,6 +510,41 @@ async function applyFirstAgentRouteGate(config, consumer, command, crewMachineId
|
|
|
509
510
|
return { error: routeError };
|
|
510
511
|
return { command: routed };
|
|
511
512
|
}
|
|
513
|
+
export async function remoteLoadedSessionInventory(config, machineId, filters = {}) {
|
|
514
|
+
if (!machineId || machineId === config.machineId)
|
|
515
|
+
return [];
|
|
516
|
+
const message = await requestRemoteMachineResult(config, {
|
|
517
|
+
targetMachineId: machineId,
|
|
518
|
+
expectedType: "machine:historicalSessions",
|
|
519
|
+
command: {
|
|
520
|
+
type: "controller:listHistoricalSessions",
|
|
521
|
+
requestId: randomId("req_session_inventory"),
|
|
522
|
+
machineId,
|
|
523
|
+
...(filters.cwd ? { cwd: filters.cwd } : {}),
|
|
524
|
+
...(filters.agent ? { agent: filters.agent } : {}),
|
|
525
|
+
limit: 200,
|
|
526
|
+
},
|
|
527
|
+
timeoutMs: 20_000,
|
|
528
|
+
});
|
|
529
|
+
return (message.loadedSessions ?? []).filter((session) => session.status !== "closed");
|
|
530
|
+
}
|
|
531
|
+
async function remoteLoadedCrewRouteCandidates(config, crewMachineId, cwd) {
|
|
532
|
+
const loaded = await remoteLoadedSessionInventory(config, crewMachineId, cwd ? { cwd } : {}).catch(() => []);
|
|
533
|
+
return loaded.map((session) => {
|
|
534
|
+
const busy = session.status === "running" || session.status === "waiting_for_option";
|
|
535
|
+
const checkpointId = session.currentHead ?? session.lastTurnId;
|
|
536
|
+
return {
|
|
537
|
+
crewSessionId: session.sessionId,
|
|
538
|
+
crewMachineId,
|
|
539
|
+
agent: session.agent,
|
|
540
|
+
cwd: session.cwd,
|
|
541
|
+
availability: busy ? "busy" : "idle",
|
|
542
|
+
recentTasks: [],
|
|
543
|
+
...(busy ? { queueArgs: { crewSessionId: session.sessionId, ifBusy: "queue" } } : { reuseArgs: { crewSessionId: session.sessionId } }),
|
|
544
|
+
...(checkpointId ? { forkArgs: { forkFromCrewSessionId: session.sessionId, forkCheckpointId: checkpointId } } : {}),
|
|
545
|
+
};
|
|
546
|
+
});
|
|
547
|
+
}
|
|
512
548
|
async function artifactLineageRouteCandidates(config, command, crewMachineId) {
|
|
513
549
|
if (!command.taskId || !command.routeDecision || command.routeDecision.kind === "new")
|
|
514
550
|
return [];
|
|
@@ -1221,7 +1257,22 @@ async function crewSessionProjection(config, crewMachineId, crewSessionId) {
|
|
|
1221
1257
|
return session ? projectCrewSession(session, crewMachineId) : undefined;
|
|
1222
1258
|
}
|
|
1223
1259
|
const sessionMessage = await readRetainedMachineMessage(config, `node:${crewMachineId}:session:${crewSessionId}`).catch(() => undefined);
|
|
1224
|
-
|
|
1260
|
+
if (sessionMessage)
|
|
1261
|
+
return projectRemoteCrewSession(crewMachineId, sessionMessage);
|
|
1262
|
+
const loaded = await remoteLoadedSessionInventory(config, crewMachineId).catch(() => []);
|
|
1263
|
+
const inventory = loaded.find((session) => session.sessionId === crewSessionId);
|
|
1264
|
+
if (!inventory)
|
|
1265
|
+
return undefined;
|
|
1266
|
+
return {
|
|
1267
|
+
crewMachineId,
|
|
1268
|
+
crewSessionId: inventory.sessionId,
|
|
1269
|
+
agent: inventory.agent,
|
|
1270
|
+
cwd: inventory.cwd,
|
|
1271
|
+
name: inventory.name,
|
|
1272
|
+
status: inventory.status === "running" || inventory.status === "waiting_for_option" ? "running" : "ready",
|
|
1273
|
+
...(inventory.currentHead ? { currentHead: inventory.currentHead } : {}),
|
|
1274
|
+
...(inventory.lastTurnId ? { lastTurnId: inventory.lastTurnId } : {}),
|
|
1275
|
+
};
|
|
1225
1276
|
}
|
|
1226
1277
|
async function dispatchTaskProjection(config, admission) {
|
|
1227
1278
|
const crewMachineId = admission.crewMachineId ?? config.machineId;
|
|
@@ -2,6 +2,7 @@ export declare const configDir: string;
|
|
|
2
2
|
export declare const configPath: string;
|
|
3
3
|
export declare const auditLogPath: string;
|
|
4
4
|
export declare const relayOutboxPath: string;
|
|
5
|
+
export declare const relayRetainedOutboxPath: string;
|
|
5
6
|
export declare const inboundPromptAdmissionStorePath: string;
|
|
6
7
|
export declare const sessionProjectionStorePath: string;
|
|
7
8
|
export declare const loopStorePath: string;
|
|
@@ -7,6 +7,7 @@ export const configDir = process.env.HAPPY_ELVES_HOME
|
|
|
7
7
|
export const configPath = path.join(configDir, "daemon.json");
|
|
8
8
|
export const auditLogPath = path.join(configDir, "audit.jsonl");
|
|
9
9
|
export const relayOutboxPath = path.join(configDir, "outbox.jsonl");
|
|
10
|
+
export const relayRetainedOutboxPath = path.join(configDir, "retained-outbox.jsonl");
|
|
10
11
|
export const inboundPromptAdmissionStorePath = path.join(configDir, "inbound-prompt-admissions.json");
|
|
11
12
|
export const sessionProjectionStorePath = path.join(configDir, "sessions.json");
|
|
12
13
|
export const loopStorePath = path.join(configDir, "loops.json");
|
|
@@ -25,7 +25,7 @@ import { applyRelayMachinePresence, applyRelayMachineSnapshot, reconcileRelayMac
|
|
|
25
25
|
import { handleDevExec, handleDiagnose } from "./devtools.js";
|
|
26
26
|
import { handleInboundPromptCommand, recoverInboundPromptAdmissions } from "./inbound-prompt.js";
|
|
27
27
|
import { registerMachine } from "./register.js";
|
|
28
|
-
import { clearActiveRelaySocket, configureRelaySender, flushRelayOutbox, handleMachineMessageAck, handleRetainedPacketAck, handleRetainedPacketError, sendCommandResponse, setActiveRelaySocket, } from "./send.js";
|
|
28
|
+
import { clearActiveRelaySocket, configureRelaySender, flushRelayOutbox, handleMachineMessageAck, handleMachineMessageError, handleRetainedPacketAck, handleRetainedPacketError, sendCommandResponse, setActiveRelaySocket, } from "./send.js";
|
|
29
29
|
async function applyMachineManifest(config, retained, presence) {
|
|
30
30
|
try {
|
|
31
31
|
const message = parseMachineClientMessage(await decryptJson(config.accountSecret, retained.encryptedPayload));
|
|
@@ -253,6 +253,8 @@ async function handleRelayWireMessage(ws, config, message) {
|
|
|
253
253
|
return;
|
|
254
254
|
}
|
|
255
255
|
if (message.type === "server:error") {
|
|
256
|
+
if (message.packetId)
|
|
257
|
+
await handleMachineMessageError(message.packetId, message.message);
|
|
256
258
|
if (message.retainedKey)
|
|
257
259
|
handleRetainedPacketError(message.retainedKey);
|
|
258
260
|
console.error(`Relay error${message.code ? ` ${message.code}` : ""}: ${message.message}`);
|
|
@@ -25,6 +25,7 @@ export async function publishRemoteControllerCommand(config, input) {
|
|
|
25
25
|
return await new Promise((resolve, reject) => {
|
|
26
26
|
const ws = new WebSocket(relayWsUrl(config.relayUrl));
|
|
27
27
|
let settled = false;
|
|
28
|
+
let commandSent = false;
|
|
28
29
|
const timer = globalThis.setTimeout(() => {
|
|
29
30
|
finish(undefined, new RemoteCommandError(`Timed out waiting for remote command ${input.command.requestId}`, "REMOTE_COMMAND_TIMEOUT"));
|
|
30
31
|
}, timeoutMs);
|
|
@@ -45,6 +46,11 @@ export async function publishRemoteControllerCommand(config, input) {
|
|
|
45
46
|
token: config.machineToken,
|
|
46
47
|
nodeKind: "machine",
|
|
47
48
|
nodeId: config.machineId,
|
|
49
|
+
// This is an ephemeral RPC socket, not the daemon's durable command
|
|
50
|
+
// consumer. Do not replay unrelated toNode packets into it: the
|
|
51
|
+
// durable daemon connection (with its own cursor) must remain the
|
|
52
|
+
// only consumer that can acknowledge those commands.
|
|
53
|
+
cursor: Number.MAX_SAFE_INTEGER,
|
|
48
54
|
}));
|
|
49
55
|
});
|
|
50
56
|
ws.addEventListener("error", () => {
|
|
@@ -57,6 +63,18 @@ export async function publishRemoteControllerCommand(config, input) {
|
|
|
57
63
|
targetMachineId: input.targetMachineId,
|
|
58
64
|
expectedType: input.expectedType,
|
|
59
65
|
send: (message) => ws.send(JSON.stringify(message)),
|
|
66
|
+
sendCommand: async (cursor) => {
|
|
67
|
+
if (commandSent)
|
|
68
|
+
return;
|
|
69
|
+
commandSent = true;
|
|
70
|
+
const subscribe = {
|
|
71
|
+
type: "subscribe",
|
|
72
|
+
channelId: controllerProjectionChannel,
|
|
73
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
74
|
+
};
|
|
75
|
+
ws.send(JSON.stringify(subscribe));
|
|
76
|
+
ws.send(JSON.stringify(await remoteCommandPacket(config, packetId, input.targetMachineId, input.command)));
|
|
77
|
+
},
|
|
60
78
|
finish,
|
|
61
79
|
}).catch((error) => finish(undefined, error));
|
|
62
80
|
});
|
|
@@ -77,8 +95,14 @@ export async function readRetainedMachineMessage(config, retainedKey) {
|
|
|
77
95
|
async function handleRelayMessage(config, raw, context) {
|
|
78
96
|
const wire = parseRelayServerWireMessage(JSON.parse(String(raw)));
|
|
79
97
|
if (wire.type === "server:hello") {
|
|
80
|
-
|
|
81
|
-
|
|
98
|
+
// Wait for the snapshot cursor before subscribing. Short-lived RPC
|
|
99
|
+
// connections must not replay the entire product:controllers history;
|
|
100
|
+
// the snapshot cursor gives the subscription a precise lower bound while
|
|
101
|
+
// still catching packets committed between the snapshot and subscribe.
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (wire.type === "server:snapshot") {
|
|
105
|
+
await context.sendCommand(wire.cursor);
|
|
82
106
|
return;
|
|
83
107
|
}
|
|
84
108
|
if (wire.type === "server:error") {
|
|
@@ -92,7 +116,6 @@ async function handleRelayMessage(config, raw, context) {
|
|
|
92
116
|
}
|
|
93
117
|
if (wire.type !== "server:packet")
|
|
94
118
|
return;
|
|
95
|
-
context.send({ type: "ackPacket", packetId: wire.packet.packetId, serverSeq: wire.packet.serverSeq });
|
|
96
119
|
let message;
|
|
97
120
|
try {
|
|
98
121
|
message = parseMachineClientMessage(await decryptJson(config.accountSecret, wire.packet.encryptedPayload));
|
|
@@ -102,6 +125,10 @@ async function handleRelayMessage(config, raw, context) {
|
|
|
102
125
|
}
|
|
103
126
|
if (!("requestId" in message) || message.requestId !== context.command.requestId)
|
|
104
127
|
return;
|
|
128
|
+
// A short-lived RPC socket can share a machine id with the durable daemon
|
|
129
|
+
// connection. Only acknowledge the response that belongs to this request;
|
|
130
|
+
// unrelated packets must remain available for the durable consumer.
|
|
131
|
+
context.send({ type: "ackPacket", packetId: wire.packet.packetId, serverSeq: wire.packet.serverSeq });
|
|
105
132
|
if (message.type === "machine:error") {
|
|
106
133
|
context.finish(undefined, new RemoteCommandError(message.message, message.code ?? "REMOTE_MACHINE_ERROR", false));
|
|
107
134
|
return;
|
|
@@ -9,6 +9,7 @@ export declare function setActiveRelaySocket(ws: WebSocket): void;
|
|
|
9
9
|
export declare function clearActiveRelaySocket(ws: WebSocket): void;
|
|
10
10
|
export declare function flushRelayOutbox(): Promise<void>;
|
|
11
11
|
export declare function handleMachineMessageAck(messageId: string): Promise<void>;
|
|
12
|
+
export declare function handleMachineMessageError(messageId: string, error?: string): Promise<void>;
|
|
12
13
|
export declare function handleRetainedPacketAck(retainedKey: string, version: number | undefined): void;
|
|
13
14
|
export declare function handleRetainedPacketError(retainedKey: string): void;
|
|
14
15
|
export declare function clearRelayOutboxForTests(): Promise<void>;
|