@cydm/happy-elves 0.1.0-beta.277 → 0.1.0-beta.279

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.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-cli",
3
- "version": "0.1.0-beta.277",
3
+ "version": "0.1.0-beta.279",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -1,6 +1,7 @@
1
1
  import { type MachineCommand } from "../../../../packages/shared/dist/index.js";
2
2
  import type { DaemonConfig, HistoricalBackfillStatus, SessionFallback, SessionState } from "../types.js";
3
3
  import { type HistoricalSessionHead } from "./events.js";
4
+ type HistoricalBackfillDirection = "latest" | "older";
4
5
  export type HistoricalBackfillResult = {
5
6
  availableEvents: number;
6
7
  cursor: number;
@@ -16,7 +17,7 @@ export type HistoricalBackfillResult = {
16
17
  export declare function handleCreateSession(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
17
18
  export declare function handleImportHistoricalSession(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
18
19
  export declare function handleListHistoricalSessions(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
19
- export declare function resumeSession(ws: WebSocket, config: DaemonConfig, sessionId: string, requestId: string, fallback?: SessionFallback, announceSuccess?: boolean): Promise<SessionState | null>;
20
+ export declare function resumeSession(ws: WebSocket, config: DaemonConfig, sessionId: string, requestId: string, fallback?: SessionFallback, announceSuccess?: boolean, syncDirection?: HistoricalBackfillDirection): Promise<SessionState | null>;
20
21
  export declare function usesAuthoritativeTranscript(session: SessionState): boolean;
21
22
  export declare function replaceAuthoritativeTranscript(ws: WebSocket, config: DaemonConfig, session: SessionState, runtimeSessionId?: string | undefined, input?: {
22
23
  currentHead?: string;
@@ -28,3 +29,4 @@ export declare function replaceAuthoritativeTranscript(ws: WebSocket, config: Da
28
29
  }): Promise<HistoricalBackfillResult>;
29
30
  export declare function handleResume(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
30
31
  export declare function handleClose(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
32
+ export {};
@@ -256,12 +256,14 @@ function historicalSessionPage(value) {
256
256
  return { sessions: value };
257
257
  return value;
258
258
  }
259
- export async function resumeSession(ws, config, sessionId, requestId, fallback, announceSuccess = true) {
259
+ export async function resumeSession(ws, config, sessionId, requestId, fallback, announceSuccess = true, syncDirection = "latest") {
260
260
  const existing = sessions.get(sessionId);
261
261
  if (existing) {
262
262
  const refreshed = await refreshRuntimeSessionName(existing);
263
263
  const nameChanged = refreshed.name !== existing.name;
264
- const backfill = await backfillHistoricalEvents(ws, config, refreshed, undefined, refreshed.historicalBackfill ?? fallback?.historicalBackfill);
264
+ const backfill = await backfillHistoricalEvents(ws, config, refreshed, undefined, refreshed.historicalBackfill ?? fallback?.historicalBackfill, {
265
+ direction: syncDirection,
266
+ });
265
267
  const resumed = withHistoricalBackfill(refreshed, backfill);
266
268
  sessions.set(sessionId, resumed);
267
269
  await appendAudit({
@@ -311,7 +313,9 @@ export async function resumeSession(ws, config, sessionId, requestId, fallback,
311
313
  const state = await refreshRuntimeSessionName({ sessionId, agent, cwd, name, handle });
312
314
  sessions.set(sessionId, state);
313
315
  const nameChanged = state.name !== name;
314
- const backfill = await backfillHistoricalEvents(ws, config, state, resumeSessionId, fallback?.historicalBackfill);
316
+ const backfill = await backfillHistoricalEvents(ws, config, state, resumeSessionId, fallback?.historicalBackfill, {
317
+ direction: syncDirection,
318
+ });
315
319
  const resumed = withHistoricalBackfill(state, backfill);
316
320
  sessions.set(sessionId, resumed);
317
321
  await appendAudit({
@@ -558,7 +562,7 @@ async function backfillHistoricalEvents(ws, config, session, runtimeSessionId =
558
562
  if (!runtimeSessionId)
559
563
  return completedBackfillResult(0);
560
564
  if (usesAuthoritativeTranscript(session)) {
561
- if (previousBackfill?.status === "partial" && previousBackfill.providerCursor) {
565
+ if ((options.direction ?? "latest") === "older" && previousBackfill?.status === "partial" && previousBackfill.providerCursor) {
562
566
  return await prependAuthoritativeTranscript(ws, config, session, runtimeSessionId, previousBackfill, { strict: options.strict });
563
567
  }
564
568
  return await replaceAuthoritativeTranscript(ws, config, session, runtimeSessionId, { strict: options.strict });
@@ -731,7 +735,7 @@ export async function handleResume(ws, config, command) {
731
735
  return;
732
736
  if (!claimCommandRequest(ws, command.requestId))
733
737
  return;
734
- await resumeSession(ws, config, command.sessionId, command.requestId, await sessionFallbackFromCommand(config, command));
738
+ await resumeSession(ws, config, command.sessionId, command.requestId, await sessionFallbackFromCommand(config, command), true, command.syncDirection ?? "latest");
735
739
  }
736
740
  export async function handleClose(ws, config, command) {
737
741
  if (command.type !== "machine:close")
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.277",
3
+ "version": "0.1.0-beta.279",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -354,6 +354,7 @@ export function handleControllerMessage(context, connection, message) {
354
354
  type: "machine:resume",
355
355
  requestId: message.requestId,
356
356
  sessionId: message.sessionId,
357
+ ...(message.syncDirection ? { syncDirection: message.syncDirection } : {}),
357
358
  encryptedMetadata: sessionRow.encrypted_metadata ? parseJson(sessionRow.encrypted_metadata, undefined) : undefined,
358
359
  });
359
360
  return;
@@ -784,21 +785,20 @@ export function handleControllerMessage(context, connection, message) {
784
785
  sendAck(connection, { type: "server:ack", requestId: message.requestId, ok: true });
785
786
  return;
786
787
  }
788
+ const closedSession = setSessionStatus(sessionRow.id, "closed");
789
+ finishSessionClaim({
790
+ accountId: connection.accountId,
791
+ sessionId: sessionRow.id,
792
+ terminalStatus: "cancelled",
793
+ errorCode: "SESSION_CLOSED",
794
+ });
795
+ if (closedSession)
796
+ broadcastControllers(connection.accountId, { type: "server:session", session: closedSession });
797
+ sendAck(connection, { type: "server:ack", requestId: message.requestId, ok: true });
787
798
  const machineConnection = connectedMachine(connection.accountId, sessionRow.machine_id);
788
799
  if (!machineConnection) {
789
- const session = setSessionStatus(sessionRow.id, "closed");
790
- finishSessionClaim({
791
- accountId: connection.accountId,
792
- sessionId: sessionRow.id,
793
- terminalStatus: "cancelled",
794
- errorCode: "SESSION_CLOSED",
795
- });
796
- if (session)
797
- broadcastControllers(connection.accountId, { type: "server:session", session });
798
- sendAck(connection, { type: "server:ack", requestId: message.requestId, ok: true });
799
800
  return;
800
801
  }
801
- markCommandPending(connection.accountId, sessionRow.machine_id, message.requestId);
802
802
  send(machineConnection.socket, {
803
803
  type: "machine:close",
804
804
  requestId: message.requestId,
@@ -223,7 +223,7 @@ export function handleMachineMessage(context, connection, message) {
223
223
  current_head = COALESCE(?, current_head),
224
224
  last_turn_id = COALESCE(?, last_turn_id),
225
225
  updated_at = ?
226
- WHERE id = ? AND account_id = ? AND machine_id = ?`).run(message.result.terminalStatus, message.result.currentHead ?? null, message.result.lastTurnId ?? message.result.turnId ?? null, ts, message.result.sessionId, connection.accountId, connection.machineId);
226
+ WHERE id = ? AND account_id = ? AND machine_id = ? AND status != 'closed'`).run(message.result.terminalStatus, message.result.currentHead ?? null, message.result.lastTurnId ?? message.result.turnId ?? null, ts, message.result.sessionId, connection.accountId, connection.machineId);
227
227
  finishSessionClaim({
228
228
  accountId: connection.accountId,
229
229
  sessionId: message.result.sessionId,
@@ -237,6 +237,30 @@ export function handleMachineMessage(context, connection, message) {
237
237
  if (row)
238
238
  broadcastControllers(connection.accountId, { type: "server:session", session: sessionSnapshot(row) });
239
239
  }
240
+ if (message.result.status === "unknown" || message.result.status === "not_found" || message.result.status === "uninspectable") {
241
+ const claim = activePromptClaim(db, connection.accountId, message.result.sessionId, connection.machineId);
242
+ const turnId = message.result.turnId ?? claim?.turn_id;
243
+ const requestId = message.result.requestId ?? claim?.request_id;
244
+ const claimMatches = Boolean(claim) &&
245
+ (!message.result.requestId || message.result.requestId === claim?.request_id) &&
246
+ (!message.result.turnId || message.result.turnId === claim?.turn_id);
247
+ if (claimMatches && turnId) {
248
+ db.prepare(`UPDATE sessions
249
+ SET status = 'failed',
250
+ current_head = COALESCE(current_head, ?),
251
+ last_turn_id = ?,
252
+ updated_at = ?
253
+ WHERE id = ? AND account_id = ? AND machine_id = ? AND status != 'closed'`).run(turnId, turnId, ts, message.result.sessionId, connection.accountId, connection.machineId);
254
+ if (requestId) {
255
+ db.prepare(`UPDATE relay_admissions
256
+ SET last_error_code = COALESCE(last_error_code, ?), updated_at = ?
257
+ WHERE account_id = ? AND session_id = ? AND request_id = ?`).run(message.result.status === "not_found" ? "EXECUTION_NOT_FOUND" : "EXECUTION_UNKNOWN", ts, connection.accountId, message.result.sessionId, requestId);
258
+ }
259
+ const row = db.prepare("SELECT * FROM sessions WHERE id = ?").get(message.result.sessionId);
260
+ if (row)
261
+ broadcastControllers(connection.accountId, { type: "server:session", session: sessionSnapshot(row) });
262
+ }
263
+ }
240
264
  rememberCommandResponse(connection.accountId, message.requestId, response);
241
265
  clearCommandPending(connection.accountId, message.requestId);
242
266
  broadcastControllers(connection.accountId, response);
@@ -371,6 +395,10 @@ export function handleMachineMessage(context, connection, message) {
371
395
  acknowledgeMachineMessage();
372
396
  return;
373
397
  }
398
+ if (sessionRow.status === "closed") {
399
+ acknowledgeMachineMessage();
400
+ return;
401
+ }
374
402
  const replacementBasis = message.basis;
375
403
  const replacementBasisEvent = replacementBasis
376
404
  ? message.events.find((event) => event.messageId === replacementBasis.eventMessageId &&
@@ -508,6 +536,10 @@ export function handleMachineMessage(context, connection, message) {
508
536
  acknowledgeMachineMessage();
509
537
  return;
510
538
  }
539
+ if (sessionRow.status === "closed") {
540
+ acknowledgeMachineMessage();
541
+ return;
542
+ }
511
543
  const ts = now();
512
544
  const inserted = [];
513
545
  db.exec("BEGIN");
@@ -570,6 +602,10 @@ export function handleMachineMessage(context, connection, message) {
570
602
  const sessionBefore = db
571
603
  .prepare("SELECT * FROM sessions WHERE id = ? AND account_id = ? AND machine_id = ?")
572
604
  .get(message.sessionId, connection.accountId, connection.machineId);
605
+ if (sessionBefore?.status === "closed") {
606
+ acknowledgeMachineMessage();
607
+ return;
608
+ }
573
609
  const activeClaim = sessionBefore
574
610
  ? activePromptClaim(db, connection.accountId, message.sessionId, connection.machineId)
575
611
  : undefined;
@@ -709,6 +745,18 @@ export function handleMachineMessage(context, connection, message) {
709
745
  acknowledgeMachineMessage();
710
746
  return;
711
747
  }
748
+ if (sessionRow.status === "closed") {
749
+ finishSessionClaim({
750
+ accountId: connection.accountId,
751
+ sessionId: message.sessionId,
752
+ turnId: message.turnId,
753
+ terminalStatus: status,
754
+ ...(message.error ? { errorCode: "TURN_ERROR" } : {}),
755
+ finishedAt: ts,
756
+ });
757
+ acknowledgeMachineMessage();
758
+ return;
759
+ }
712
760
  const currentOrder = sessionHeadBasisOrder(db, sessionRow);
713
761
  const basisComparison = basisOrder ? compareHeadBasisOrder(basisOrder, currentOrder) : null;
714
762
  const isRuntimeAliasForCurrentRequest = Boolean(runtimeBasisEvent && runtimeTurnId !== message.turnId && (sessionRow.last_turn_id === message.turnId || requestBasisEvent));
@@ -860,6 +908,8 @@ function applySessionHeadAdvance(db, input, ts) {
860
908
  .get(input.sessionId, input.accountId, input.machineId);
861
909
  if (!sessionRow)
862
910
  return { status: "session-not-found" };
911
+ if (sessionRow.status === "closed")
912
+ return { status: "noop", session: sessionRow };
863
913
  const basisEvent = resolveHeadAdvanceBasisEvent(db, input.accountId, input.sessionId, input.machineId, input.basis);
864
914
  if (!basisEvent)
865
915
  return { status: "missing", session: sessionRow };
@@ -366,6 +366,29 @@ export function createRelayContext(options) {
366
366
  if (!machine)
367
367
  return "queued";
368
368
  const command = JSON.parse(row.message);
369
+ if (command.type === "machine:prompt") {
370
+ const session = options.db
371
+ .prepare("SELECT status FROM sessions WHERE account_id = ? AND id = ? AND machine_id = ?")
372
+ .get(accountId, command.sessionId, row.machine_id);
373
+ if (session?.status === "closed") {
374
+ const ts = now();
375
+ options.db
376
+ .prepare(`UPDATE relay_dispatch_outbox
377
+ SET state = 'delivered', delivered_at = ?, updated_at = ?
378
+ WHERE account_id = ? AND request_id = ? AND state = 'queued'`)
379
+ .run(ts, ts, accountId, requestId);
380
+ finishSessionClaim({
381
+ accountId,
382
+ sessionId: command.sessionId,
383
+ requestId,
384
+ turnId: command.turnId,
385
+ terminalStatus: "cancelled",
386
+ errorCode: "SESSION_CLOSED",
387
+ finishedAt: ts,
388
+ });
389
+ return "queued";
390
+ }
391
+ }
369
392
  send(machine.socket, command);
370
393
  const ts = now();
371
394
  options.db
@@ -635,7 +658,9 @@ export function createRelayContext(options) {
635
658
  }
636
659
  function setSessionStatus(sessionId, status) {
637
660
  const ts = now();
638
- options.db.prepare("UPDATE sessions SET status = ?, updated_at = ? WHERE id = ?").run(status, ts, sessionId);
661
+ options.db
662
+ .prepare("UPDATE sessions SET status = ?, updated_at = ? WHERE id = ? AND (status != 'closed' OR ? = 'closed')")
663
+ .run(status, ts, sessionId, status);
639
664
  const row = options.db.prepare("SELECT * FROM sessions WHERE id = ?").get(sessionId);
640
665
  return row ? sessionSnapshot(row) : undefined;
641
666
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.277",
3
+ "version": "0.1.0-beta.279",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -70,7 +70,9 @@ export declare class ControllerClient {
70
70
  collectPage(sessionId: string, input?: CollectInput): Promise<DecodedSessionEventsPage>;
71
71
  repairSessionHead(sessionId: string, input: RepairSessionHeadInput): Promise<RepairSessionHeadResult>;
72
72
  wait(sessionId: string, input?: WaitInput): Promise<SessionSnapshot>;
73
- resume(sessionId: string): Promise<CommandResult>;
73
+ resume(sessionId: string, options?: {
74
+ syncDirection?: "latest" | "older";
75
+ }): Promise<CommandResult>;
74
76
  listHistoricalSessionsPage(input: HistoricalSessionsInput): Promise<HistoricalSessionListPage>;
75
77
  listHistoricalSessions(input: HistoricalSessionsInput): Promise<HistoricalSessionSnapshot[]>;
76
78
  listDirectory(input: DirectoryInput): Promise<DirectoryListing>;
@@ -302,10 +302,15 @@ export class ControllerClient {
302
302
  await sleep(intervalMs);
303
303
  }
304
304
  }
305
- async resume(sessionId) {
305
+ async resume(sessionId, options = {}) {
306
306
  const checkedSessionId = requireNonEmpty(sessionId, "sessionId");
307
307
  const requestId = randomId("req");
308
- return await this.sendCommand({ type: "controller:resume", requestId, sessionId: checkedSessionId });
308
+ return await this.sendCommand({
309
+ type: "controller:resume",
310
+ requestId,
311
+ sessionId: checkedSessionId,
312
+ ...(options.syncDirection ? { syncDirection: options.syncDirection } : {}),
313
+ });
309
314
  }
310
315
  async listHistoricalSessionsPage(input) {
311
316
  const machineId = requireNonEmpty(input.machineId, "machineId");
@@ -50,6 +50,7 @@ export type ControllerClientMessage = {
50
50
  type: "controller:resume";
51
51
  requestId: string;
52
52
  sessionId: string;
53
+ syncDirection?: "latest" | "older";
53
54
  } | {
54
55
  type: "controller:listHistoricalSessions";
55
56
  requestId: string;
@@ -384,6 +385,7 @@ export type MachineCommand = {
384
385
  type: "machine:resume";
385
386
  requestId: string;
386
387
  sessionId: string;
388
+ syncDirection?: "latest" | "older";
387
389
  encryptedMetadata?: EncryptedEnvelope;
388
390
  } | {
389
391
  type: "machine:listHistoricalSessions";
@@ -57,6 +57,7 @@ const controllerMessageSchema = z.discriminatedUnion("type", [
57
57
  type: z.literal("controller:resume"),
58
58
  requestId: z.string().min(1),
59
59
  sessionId: z.string().min(1),
60
+ syncDirection: z.enum(["latest", "older"]).optional(),
60
61
  }),
61
62
  z.object({
62
63
  type: z.literal("controller:listHistoricalSessions"),
@@ -458,6 +459,7 @@ const machineCommandSchema = z.discriminatedUnion("type", [
458
459
  type: z.literal("machine:resume"),
459
460
  requestId: z.string().min(1),
460
461
  sessionId: z.string().min(1),
462
+ syncDirection: z.enum(["latest", "older"]).optional(),
461
463
  encryptedMetadata: encryptedEnvelopeSchema.optional(),
462
464
  }),
463
465
  z.object({