@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.
Files changed (36) hide show
  1. package/apps/cli/package.json +1 -1
  2. package/apps/daemon/dist/orchestration/handler.d.ts +5 -1
  3. package/apps/daemon/dist/orchestration/handler.js +52 -1
  4. package/apps/daemon/dist/paths.d.ts +1 -0
  5. package/apps/daemon/dist/paths.js +1 -0
  6. package/apps/daemon/dist/relay/connection.js +3 -1
  7. package/apps/daemon/dist/relay/remote-command.js +30 -3
  8. package/apps/daemon/dist/relay/send.d.ts +1 -0
  9. package/apps/daemon/dist/relay/send.js +508 -54
  10. package/apps/daemon/dist/runtime/headless-event-projector.d.ts +7 -1
  11. package/apps/daemon/dist/runtime/headless-event-projector.js +52 -8
  12. package/apps/daemon/dist/runtime/headless-history-reader.js +49 -2
  13. package/apps/daemon/dist/session/he-official/common.d.ts +1 -0
  14. package/apps/daemon/dist/session/he-official/orchestration-commands.d.ts +3 -2
  15. package/apps/daemon/dist/session/he-official/orchestration-commands.js +53 -6
  16. package/apps/daemon/dist/session/he-official/session-commands.d.ts +2 -2
  17. package/apps/daemon/dist/session/he-official/session-commands.js +197 -6
  18. package/apps/daemon/dist/session/he-official/turn-commands.d.ts +12 -3
  19. package/apps/daemon/dist/session/he-official/turn-commands.js +78 -3
  20. package/apps/daemon/dist/session/he-official-tools.js +11 -6
  21. package/apps/daemon/dist/session/lifecycle.d.ts +1 -1
  22. package/apps/daemon/dist/session/lifecycle.js +39 -85
  23. package/apps/daemon/dist/session/native-history-refresh.d.ts +12 -0
  24. package/apps/daemon/dist/session/native-history-refresh.js +183 -0
  25. package/apps/daemon/dist/session/runtime-truth.js +8 -2
  26. package/apps/daemon/package.json +1 -1
  27. package/apps/relay/dist/relay-context.js +47 -3
  28. package/apps/relay/dist/websocket.js +37 -1
  29. package/package.json +1 -1
  30. package/packages/headless-runtime/dist/core.js +22 -0
  31. package/packages/shared/dist/he-official-tools.js +5 -5
  32. package/packages/shared/dist/protocol-schemas.d.ts +30 -9
  33. package/packages/shared/dist/protocol-schemas.js +11 -0
  34. package/packages/shared/dist/protocol-types.d.ts +17 -0
  35. package/packages/shared/dist/protocol.d.ts +3 -1
  36. package/packages/shared/dist/protocol.js +3 -1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-cli",
3
- "version": "0.1.0-beta.487",
3
+ "version": "0.1.0-beta.489",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -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
- return sessionMessage ? projectRemoteCrewSession(crewMachineId, sessionMessage) : undefined;
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
- context.send({ type: "subscribe", channelId: controllerProjectionChannel });
81
- context.send(await remoteCommandPacket(config, context.packetId, context.targetMachineId, context.command));
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>;