@cydm/happy-elves 0.1.0-beta.41 → 0.1.0-beta.42

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.
@@ -125,6 +125,7 @@ function repairHeadInputForLatestCompletedEvent(events) {
125
125
  return {
126
126
  currentHead,
127
127
  lastTurnId,
128
+ status: "completed",
128
129
  basis: {
129
130
  turnId: doneEvent.turnId,
130
131
  eventMessageId: doneEvent.messageId,
@@ -487,9 +488,6 @@ export async function handleSession({ domain, action, positional, flags }) {
487
488
  if (!initialSession)
488
489
  throw new CliError("Session not found", "SESSION_NOT_FOUND");
489
490
  let session = initialSession;
490
- if (flags["repair-head"] === true && session.status === "running") {
491
- throw new CliError("Cannot repair a running session head", "SESSION_HEAD_REPAIR_RUNNING");
492
- }
493
491
  const machine = snapshot.machines.find((item) => item.id === session.machineId);
494
492
  const metadata = await client.decodeSessionMetadata(session);
495
493
  const latestPage = await client.collectPage(session.id, { limit });
@@ -573,7 +571,9 @@ export async function handleSession({ domain, action, positional, flags }) {
573
571
  },
574
572
  repair: repair ? {
575
573
  advanced: repair.advanced,
574
+ settled: repair.settled,
576
575
  basis: repair.basis,
576
+ status: repair.session.status,
577
577
  currentHead: repair.session.currentHead,
578
578
  lastTurnId: repair.session.lastTurnId,
579
579
  reason: repair.reason,
@@ -4,7 +4,7 @@ import type { DaemonConfig } from "../types.js";
4
4
  export declare function emitEvent(ws: WebSocket, config: DaemonConfig, sessionId: string, turnId: string, payload: SessionEventPayload): Promise<void>;
5
5
  export declare function emitHistoricalEvents(ws: WebSocket, config: DaemonConfig, sessionId: string, events: RuntimeHistoricalEvent[], startIndex?: number): Promise<void>;
6
6
  export declare function emitRuntimeTailEvents(ws: WebSocket, config: DaemonConfig, sessionId: string, events: RuntimeHistoricalEvent[], startIndex?: number): Promise<void>;
7
- export declare function emitSessionHeadAdvanced(ws: WebSocket, sessionId: string, head: HistoricalSessionHead, encryptedMetadata?: EncryptedEnvelope): Promise<void>;
7
+ export declare function emitSessionHeadAdvanced(ws: WebSocket, sessionId: string, head: HistoricalSessionHead, encryptedMetadata?: EncryptedEnvelope, status?: "completed" | "cancelled" | "failed"): Promise<void>;
8
8
  export declare function emitTranscriptReplacement(ws: WebSocket, config: DaemonConfig, sessionId: string, events: RuntimeHistoricalEvent[], input?: {
9
9
  requestId?: string;
10
10
  currentHead?: string;
@@ -32,13 +32,14 @@ export async function emitRuntimeTailEvents(ws, config, sessionId, events, start
32
32
  }, ws);
33
33
  }
34
34
  }
35
- export async function emitSessionHeadAdvanced(ws, sessionId, head, encryptedMetadata) {
35
+ export async function emitSessionHeadAdvanced(ws, sessionId, head, encryptedMetadata, status) {
36
36
  await sendReliable({
37
37
  type: "machine:sessionHeadAdvanced",
38
38
  sessionId,
39
39
  currentHead: head.currentHead,
40
40
  lastTurnId: head.lastTurnId,
41
41
  basis: head.basis,
42
+ status,
42
43
  encryptedMetadata,
43
44
  }, ws);
44
45
  }
@@ -605,7 +605,7 @@ async function backfillHistoricalEvents(ws, config, session, runtimeSessionId =
605
605
  await emitSessionHeadAdvanced(ws, session.sessionId, head, await encryptedSessionMetadata(config, session.sessionId, backfilledSession, {
606
606
  ...historicalBackfillMetadata(result),
607
607
  historicalBackfilledAt: new Date().toISOString(),
608
- }));
608
+ }), "completed");
609
609
  }
610
610
  return result;
611
611
  }
@@ -121,7 +121,7 @@ export async function handlePrompt(ws, config, command) {
121
121
  currentHead: result.currentHead,
122
122
  lastTurnId: result.lastTurnId,
123
123
  });
124
- await refreshAuthoritativeTranscriptAfterTurn(ws, config, session, command.requestId, result.currentHead, result.lastTurnId);
124
+ await refreshAuthoritativeTranscriptAfterTurn(ws, config, session, command.requestId, result.currentHead, result.lastTurnId, result.status);
125
125
  releasePromptTurn();
126
126
  await sendReliable({
127
127
  type: "machine:turnDone",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.41",
3
+ "version": "0.1.0-beta.42",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -249,7 +249,7 @@ export function registerHttpRoutes(app, context, controllerInviteTtlMs) {
249
249
  .get(token.account_id, params.sessionId);
250
250
  if (!session)
251
251
  throw new HttpError(404, "SESSION_NOT_FOUND", "Session not found");
252
- if (session.status === "running") {
252
+ if (session.status === "running" && !body.status) {
253
253
  throw new HttpError(409, "SESSION_HEAD_REPAIR_RUNNING", "Cannot repair a running session head");
254
254
  }
255
255
  const basisEvent = context.db
@@ -265,18 +265,28 @@ export function registerHttpRoutes(app, context, controllerInviteTtlMs) {
265
265
  const currentOrder = sessionHeadBasisOrderForRow(context.db, session);
266
266
  const rewriteFenced = typeof session.head_basis_message_id === "string" &&
267
267
  session.head_basis_message_id.startsWith("rewrite:");
268
- const advanced = !rewriteFenced && compareHeadOrders(nextOrder, currentOrder) > 0;
269
- if (advanced) {
268
+ const comparison = compareHeadOrders(nextOrder, currentOrder);
269
+ const currentHeadAlreadySettled = session.current_head === `event:${basisEvent.id}` ||
270
+ session.current_head === body.currentHead ||
271
+ session.last_turn_id === body.lastTurnId;
272
+ const advanced = !rewriteFenced && comparison > 0;
273
+ const statusChanged = !rewriteFenced &&
274
+ body.status !== undefined &&
275
+ session.status === "running" &&
276
+ comparison === 0 &&
277
+ currentHeadAlreadySettled;
278
+ if (advanced || statusChanged) {
270
279
  const ts = context.now();
271
280
  context.db.prepare(`UPDATE sessions
272
- SET current_head = ?,
273
- last_turn_id = ?,
274
- head_basis_event_id = ?,
275
- head_basis_message_id = ?,
276
- head_basis_logical_time = ?,
277
- head_basis_turn_seq = ?,
281
+ SET status = COALESCE(?, status),
282
+ current_head = COALESCE(?, current_head),
283
+ last_turn_id = COALESCE(?, last_turn_id),
284
+ head_basis_event_id = COALESCE(?, head_basis_event_id),
285
+ head_basis_message_id = COALESCE(?, head_basis_message_id),
286
+ head_basis_logical_time = COALESCE(?, head_basis_logical_time),
287
+ head_basis_turn_seq = COALESCE(?, head_basis_turn_seq),
278
288
  updated_at = ?
279
- WHERE id = ? AND account_id = ?`).run(body.currentHead, body.lastTurnId, basisEvent.id, basisEvent.message_id, nextOrder.time, nextOrder.seq, ts, params.sessionId, token.account_id);
289
+ WHERE id = ? AND account_id = ?`).run(body.status && session.status === "running" ? body.status : null, advanced ? body.currentHead : null, advanced ? body.lastTurnId : null, advanced ? basisEvent.id : null, advanced ? basisEvent.message_id : null, advanced ? nextOrder.time : null, advanced ? nextOrder.seq : null, ts, params.sessionId, token.account_id);
280
290
  }
281
291
  const row = context.db
282
292
  .prepare("SELECT * FROM sessions WHERE account_id = ? AND id = ?")
@@ -294,6 +304,7 @@ export function registerHttpRoutes(app, context, controllerInviteTtlMs) {
294
304
  logicalTime: nextOrder.time,
295
305
  turnSeq: nextOrder.seq,
296
306
  },
307
+ ...(statusChanged ? { settled: true } : {}),
297
308
  ...(rewriteFenced ? { reason: "rewrite-fenced" } : {}),
298
309
  };
299
310
  });
@@ -49,6 +49,11 @@ export declare const sessionEventsQuerySchema: z.ZodObject<{
49
49
  export declare const sessionHeadAdvanceSchema: z.ZodObject<{
50
50
  currentHead: z.ZodString;
51
51
  lastTurnId: z.ZodString;
52
+ status: z.ZodOptional<z.ZodEnum<{
53
+ completed: "completed";
54
+ failed: "failed";
55
+ cancelled: "cancelled";
56
+ }>>;
52
57
  basis: z.ZodObject<{
53
58
  turnId: z.ZodString;
54
59
  eventMessageId: z.ZodString;
@@ -56,6 +56,7 @@ export const sessionEventsQuerySchema = z.object({
56
56
  export const sessionHeadAdvanceSchema = z.object({
57
57
  currentHead: z.string().min(1),
58
58
  lastTurnId: z.string().min(1),
59
+ status: z.enum(["completed", "cancelled", "failed"]).optional(),
59
60
  basis: z.object({
60
61
  turnId: z.string().min(1),
61
62
  eventMessageId: z.string().min(1),
@@ -418,6 +418,7 @@ export function handleMachineMessage(context, connection, message) {
418
418
  currentHead: message.currentHead,
419
419
  lastTurnId: message.lastTurnId,
420
420
  basis: message.basis,
421
+ status: message.status,
421
422
  ...(message.encryptedMetadata ? { encryptedMetadata: message.encryptedMetadata } : {}),
422
423
  };
423
424
  const result = applySessionHeadAdvance(db, input, ts);
@@ -634,6 +635,7 @@ function pendingRowToHeadAdvanceInput(row) {
634
635
  turnSeq: row.basis_turn_seq,
635
636
  ...(row.basis_occurred_at !== null ? { occurredAt: row.basis_occurred_at } : {}),
636
637
  },
638
+ status: "completed",
637
639
  ...(row.encrypted_metadata ? { encryptedMetadata: JSON.parse(row.encrypted_metadata) } : {}),
638
640
  };
639
641
  }
@@ -655,19 +657,28 @@ function applySessionHeadAdvance(db, input, ts) {
655
657
  sessionRow.head_basis_message_id.startsWith(REWRITE_HEAD_BASIS_PREFIX);
656
658
  const comparison = compareHeadBasisOrder(nextOrder, currentOrder);
657
659
  const currentHeadIsBasisEvent = sessionRow.current_head === `event:${basisEvent.id}`;
660
+ const currentHeadAlreadySettled = currentHeadIsBasisEvent ||
661
+ sessionRow.current_head === input.currentHead ||
662
+ sessionRow.last_turn_id === input.lastTurnId;
658
663
  const shouldAdvance = !rewriteFenced && (comparison > 0 || (comparison === 0 && currentHeadIsBasisEvent));
659
- if (!shouldAdvance)
664
+ const shouldSettleStatus = !rewriteFenced &&
665
+ input.status !== undefined &&
666
+ sessionRow.status === "running" &&
667
+ comparison === 0 &&
668
+ currentHeadAlreadySettled;
669
+ if (!shouldAdvance && !shouldSettleStatus)
660
670
  return { status: "noop", session: sessionRow };
661
671
  db.prepare(`UPDATE sessions
662
- SET current_head = ?,
663
- last_turn_id = ?,
664
- head_basis_event_id = ?,
665
- head_basis_message_id = ?,
666
- head_basis_logical_time = ?,
667
- head_basis_turn_seq = ?,
672
+ SET status = COALESCE(?, status),
673
+ current_head = COALESCE(?, current_head),
674
+ last_turn_id = COALESCE(?, last_turn_id),
675
+ head_basis_event_id = COALESCE(?, head_basis_event_id),
676
+ head_basis_message_id = COALESCE(?, head_basis_message_id),
677
+ head_basis_logical_time = COALESCE(?, head_basis_logical_time),
678
+ head_basis_turn_seq = COALESCE(?, head_basis_turn_seq),
668
679
  encrypted_metadata = COALESCE(?, encrypted_metadata),
669
680
  updated_at = ?
670
- WHERE id = ? AND account_id = ? AND machine_id = ?`).run(input.currentHead, input.lastTurnId, basisEvent.id, basisEvent.message_id, nextOrder.time, nextOrder.seq, input.encryptedMetadata ? JSON.stringify(input.encryptedMetadata) : null, ts, input.sessionId, input.accountId, input.machineId);
681
+ WHERE id = ? AND account_id = ? AND machine_id = ?`).run(input.status && sessionRow.status === "running" ? input.status : null, shouldAdvance ? input.currentHead : null, shouldAdvance ? input.lastTurnId : null, shouldAdvance ? basisEvent.id : null, shouldAdvance ? basisEvent.message_id : null, shouldAdvance ? nextOrder.time : null, shouldAdvance ? nextOrder.seq : null, input.encryptedMetadata ? JSON.stringify(input.encryptedMetadata) : null, ts, input.sessionId, input.accountId, input.machineId);
671
682
  const session = db.prepare("SELECT * FROM sessions WHERE id = ?").get(input.sessionId);
672
683
  return { status: "applied", session };
673
684
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.41",
3
+ "version": "0.1.0-beta.42",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -165,6 +165,7 @@ export class ControllerClient {
165
165
  const checkedSessionId = requireNonEmpty(sessionId, "sessionId");
166
166
  const currentHead = requireNonEmpty(input.currentHead, "currentHead");
167
167
  const lastTurnId = requireNonEmpty(input.lastTurnId, "lastTurnId");
168
+ const status = input.status;
168
169
  const turnId = requireNonEmpty(input.basis.turnId, "basis.turnId");
169
170
  const eventMessageId = requireNonEmpty(input.basis.eventMessageId, "basis.eventMessageId");
170
171
  return await authenticatedJson(this.config, `/api/sessions/${encodeURIComponent(checkedSessionId)}/head`, {
@@ -173,6 +174,7 @@ export class ControllerClient {
173
174
  body: JSON.stringify({
174
175
  currentHead,
175
176
  lastTurnId,
177
+ ...(status ? { status } : {}),
176
178
  basis: {
177
179
  turnId,
178
180
  eventMessageId,
@@ -183,6 +183,7 @@ export function parseRepairSessionHeadResult(value) {
183
183
  const reason = record.reason;
184
184
  return {
185
185
  advanced: readBooleanField(record, "advanced", "Session head repair"),
186
+ ...(typeof record.settled === "boolean" ? { settled: record.settled } : {}),
186
187
  session: session,
187
188
  basis: {
188
189
  eventId: readNumberField(basis, "eventId", "Session head repair basis"),
@@ -205,6 +205,7 @@ export type CollectInput = {
205
205
  export type RepairSessionHeadInput = {
206
206
  currentHead: string;
207
207
  lastTurnId: string;
208
+ status?: "completed" | "cancelled" | "failed";
208
209
  basis: {
209
210
  turnId: string;
210
211
  eventMessageId: string;
@@ -214,6 +215,7 @@ export type RepairSessionHeadInput = {
214
215
  };
215
216
  export type RepairSessionHeadResult = {
216
217
  advanced: boolean;
218
+ settled?: boolean;
217
219
  session: SessionSnapshot;
218
220
  basis: {
219
221
  eventId: number;
@@ -144,6 +144,7 @@ export type MachineClientMessage = {
144
144
  currentHead: string;
145
145
  lastTurnId: string;
146
146
  basis: SessionHeadAdvanceBasis;
147
+ status?: "completed" | "cancelled" | "failed";
147
148
  encryptedMetadata?: EncryptedEnvelope;
148
149
  } | {
149
150
  type: "machine:turnDone";
@@ -169,6 +169,7 @@ const machineMessageSchema = z.discriminatedUnion("type", [
169
169
  messageId: z.string().min(1).optional(),
170
170
  currentHead: z.string().min(1),
171
171
  lastTurnId: z.string().min(1),
172
+ status: z.enum(["completed", "cancelled", "failed"]).optional(),
172
173
  basis: z.object({
173
174
  turnId: z.string().min(1),
174
175
  eventMessageId: z.string().min(1),