@cydm/happy-elves 0.1.0-beta.37 → 0.1.0-beta.39

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.
@@ -61,6 +61,7 @@ const booleanFlags = new Set([
61
61
  "local",
62
62
  "no-open",
63
63
  "no-wait",
64
+ "repair-head",
64
65
  "summary",
65
66
  "verbose",
66
67
  "wait",
@@ -1,6 +1,7 @@
1
1
  import type { ControllerClient, DecodedSessionEvent } from "../../../../../packages/client/dist/index.js";
2
2
  export declare function compactText(value: string, max?: number): string;
3
3
  export declare function lastAssistantOutput(events: DecodedSessionEvent[]): string;
4
+ export declare function compareSessionEventLogicalOrder(left: DecodedSessionEvent, right: DecodedSessionEvent): number;
4
5
  export declare function displayPath(filePath: string): string;
5
6
  export type StoredTurnOutput = {
6
7
  sessionId: string;
@@ -48,6 +48,27 @@ function assistantOutputForTurn(events, turnId) {
48
48
  .join("")
49
49
  .trim();
50
50
  }
51
+ function sessionEventLogicalOrder(event) {
52
+ const messageKey = event.messageId ?? "";
53
+ if (event.occurredAt !== undefined) {
54
+ return { time: event.occurredAt, seq: event.turnSeq ?? event.seq, messageKey, id: event.id };
55
+ }
56
+ if (event.messageId?.startsWith("hist_") && event.turnSeq !== undefined) {
57
+ return { time: 0, seq: event.turnSeq, messageKey, id: event.id };
58
+ }
59
+ return { time: event.createdAt, seq: event.seq, messageKey, id: event.id };
60
+ }
61
+ export function compareSessionEventLogicalOrder(left, right) {
62
+ const leftOrder = sessionEventLogicalOrder(left);
63
+ const rightOrder = sessionEventLogicalOrder(right);
64
+ if (leftOrder.time !== rightOrder.time)
65
+ return leftOrder.time - rightOrder.time;
66
+ if (leftOrder.seq !== rightOrder.seq)
67
+ return leftOrder.seq - rightOrder.seq;
68
+ if (leftOrder.messageKey !== rightOrder.messageKey)
69
+ return leftOrder.messageKey < rightOrder.messageKey ? -1 : 1;
70
+ return leftOrder.id - rightOrder.id;
71
+ }
51
72
  function outputPaths(sessionId, turnId) {
52
73
  const dir = path.join(outputRoot, sessionId);
53
74
  const latest = path.join(dir, "latest.md");
@@ -142,8 +163,9 @@ export async function readStoredTurnOutput(sessionId, turnId) {
142
163
  export function summarizeTurns(events) {
143
164
  const folded = foldAliasedRuntimeEvents(events);
144
165
  const turns = new Map();
145
- const sorted = [...folded.events].sort((left, right) => left.id - right.id);
166
+ const sorted = [...folded.events].sort(compareSessionEventLogicalOrder);
146
167
  for (const event of sorted) {
168
+ const order = sessionEventLogicalOrder(event);
147
169
  const current = turns.get(event.turnId) ?? {
148
170
  turnId: event.turnId,
149
171
  promptChunks: [],
@@ -155,11 +177,13 @@ export function summarizeTurns(events) {
155
177
  firstEventId: event.id,
156
178
  lastEventId: event.id,
157
179
  eventCount: 0,
180
+ order,
158
181
  };
159
182
  current.startedAt ??= event.createdAt;
160
183
  current.updatedAt = event.createdAt;
161
184
  current.lastEventId = event.id;
162
185
  current.eventCount += 1;
186
+ current.order = order;
163
187
  if (event.turnSeq !== undefined)
164
188
  current.turnSeq = event.turnSeq;
165
189
  const payload = event.decoded;
@@ -194,8 +218,14 @@ export function summarizeTurns(events) {
194
218
  firstEventId: turn.firstEventId,
195
219
  lastEventId: turn.lastEventId,
196
220
  eventCount: turn.eventCount,
221
+ order: turn.order,
197
222
  }))
198
- .sort((left, right) => right.updatedAt - left.updatedAt || right.lastEventId - left.lastEventId);
223
+ .sort((left, right) => right.order.time - left.order.time ||
224
+ right.order.seq - left.order.seq ||
225
+ (right.order.messageKey === left.order.messageKey ? 0 : right.order.messageKey < left.order.messageKey ? -1 : 1) ||
226
+ right.order.id - left.order.id ||
227
+ right.lastEventId - left.lastEventId)
228
+ .map(({ order: _order, ...turn }) => turn);
199
229
  }
200
230
  export function structuredOutputForTurn(sessionId, turn) {
201
231
  const finalOutput = turn.output || turn.terminalText || undefined;
@@ -50,7 +50,7 @@ Check local controller config, relay reachability, and daemon state.
50
50
  happy-elves session history --machine <machineId> [--cwd <path>] [--agent <agent>] [--include-archived] [--limit 20] --json
51
51
  happy-elves session continue <runtimeSessionId> --machine <machineId> [--name <name>] --json
52
52
  happy-elves session status <sessionId> [--verbose] [--json]
53
- happy-elves session diagnose <sessionId> [--limit 200] [--json]
53
+ happy-elves session diagnose <sessionId> [--limit 200] [--repair-head] [--json]
54
54
  happy-elves session close <sessionId> [--reason <reason>] --json
55
55
 
56
56
  Session management:
@@ -62,7 +62,8 @@ Session management:
62
62
  existing loaded session if it already exists.
63
63
  diagnose reports the selected session's runtime handle metadata, backfill
64
64
  counters, and latest materialized relay page counts for debugging empty or
65
- stale transcripts.
65
+ stale transcripts. --repair-head advances a stale canonical head to the
66
+ latest completed relay event when the relay already has the event basis.
66
67
  close archives a loaded session. There is no separate archive command.
67
68
  `,
68
69
  turn: `Usage:
@@ -141,7 +142,7 @@ Core commands:
141
142
  session history --machine <machineId> [--cwd <path>] [--limit 20] --json
142
143
  session continue <runtimeSessionId> --machine <machineId> --json
143
144
  session status <sessionId> [--verbose] [--json]
144
- session diagnose <sessionId> [--limit 200] [--json]
145
+ session diagnose <sessionId> [--limit 200] [--repair-head] [--json]
145
146
  session close <sessionId> [--reason <reason>] --json
146
147
 
147
148
  turn list <sessionId> [--limit 10] [--json]
@@ -1,4 +1,4 @@
1
- import { CliError, ControllerClient, compactSessionListItem, compactText, conciseSessionSummary, deriveOrchestration, listWorkspaceHistoricalSessions, ok, parsePermissionMode, readConfig, readStoredTurnOutput, requirePositional, requireString, showSession, structuredSessionProjection, summarizeTurns, wantsJson, wantsVerbose } from "./lib/index.js";
1
+ import { CliError, ControllerClient, compareSessionEventLogicalOrder, compactSessionListItem, compactText, conciseSessionSummary, deriveOrchestration, listWorkspaceHistoricalSessions, ok, parsePermissionMode, readConfig, readStoredTurnOutput, requirePositional, requireString, showSession, structuredSessionProjection, summarizeTurns, wantsJson, wantsVerbose } from "./lib/index.js";
2
2
  function formatTurnTimestamp(value) {
3
3
  return value === undefined ? "-" : new Date(value).toISOString();
4
4
  }
@@ -104,6 +104,35 @@ function compactEventDiagnostic(event) {
104
104
  ...(event.turnSeq !== undefined ? { turnSeq: event.turnSeq } : {}),
105
105
  };
106
106
  }
107
+ function repairHeadInputForLatestCompletedEvent(events) {
108
+ const doneEvent = [...events]
109
+ .sort(compareSessionEventLogicalOrder)
110
+ .reverse()
111
+ .find((event) => event.decoded?.type === "done" && event.decoded.status === "completed");
112
+ const done = doneEvent?.decoded;
113
+ if (!doneEvent || done?.type !== "done") {
114
+ throw new CliError("No completed done event is available for head repair", "SESSION_HEAD_REPAIR_UNAVAILABLE");
115
+ }
116
+ if (!doneEvent.messageId) {
117
+ throw new CliError("Latest completed done event has no messageId basis", "SESSION_HEAD_REPAIR_UNAVAILABLE");
118
+ }
119
+ const turnSeq = doneEvent.turnSeq;
120
+ if (turnSeq === undefined) {
121
+ throw new CliError("Latest completed done event has no turnSeq basis", "SESSION_HEAD_REPAIR_UNAVAILABLE");
122
+ }
123
+ const currentHead = done.currentHead ?? done.lastTurnId ?? doneEvent.turnId;
124
+ const lastTurnId = done.lastTurnId ?? done.currentHead ?? doneEvent.turnId;
125
+ return {
126
+ currentHead,
127
+ lastTurnId,
128
+ basis: {
129
+ turnId: doneEvent.turnId,
130
+ eventMessageId: doneEvent.messageId,
131
+ turnSeq,
132
+ ...(doneEvent.occurredAt === undefined ? {} : { occurredAt: doneEvent.occurredAt }),
133
+ },
134
+ };
135
+ }
107
136
  function eventCollectionDiagnostics(events) {
108
137
  return {
109
138
  decryptErrors: events.filter((event) => event.decryptError).length,
@@ -330,15 +359,24 @@ export async function handleSession({ domain, action, positional, flags }) {
330
359
  const limit = parsePositiveIntFlag(flags, "limit") ?? 200;
331
360
  const client = await makeClient();
332
361
  const snapshot = await client.snapshot();
333
- const session = snapshot.sessions.find((item) => item.id === sessionId);
334
- if (!session)
362
+ const initialSession = snapshot.sessions.find((item) => item.id === sessionId);
363
+ if (!initialSession)
335
364
  throw new CliError("Session not found", "SESSION_NOT_FOUND");
365
+ let session = initialSession;
366
+ if (flags["repair-head"] === true && session.status === "running") {
367
+ throw new CliError("Cannot repair a running session head", "SESSION_HEAD_REPAIR_RUNNING");
368
+ }
336
369
  const machine = snapshot.machines.find((item) => item.id === session.machineId);
337
370
  const metadata = await client.decodeSessionMetadata(session);
338
371
  const latestPage = await client.collectPage(session.id, { limit });
339
372
  const turns = summarizeTurns(latestPage.events);
340
373
  const latestCompletedTurn = turns.find((turn) => turn.status === "completed");
341
374
  const latestTerminalTurn = turns.find((turn) => turn.status !== "running");
375
+ const repair = flags["repair-head"] === true
376
+ ? await client.repairSessionHead(session.id, repairHeadInputForLatestCompletedEvent(latestPage.events))
377
+ : undefined;
378
+ if (repair)
379
+ session = repair.session;
342
380
  const data = {
343
381
  generatedAt: new Date().toISOString(),
344
382
  session: {
@@ -395,6 +433,13 @@ export async function handleSession({ domain, action, positional, flags }) {
395
433
  staleHeadSuspected: staleHeadSuspected(session, latestCompletedTurn),
396
434
  headMatchesLatestCompletedTurn: headMatchesTurn(session, latestCompletedTurn),
397
435
  },
436
+ repair: repair ? {
437
+ advanced: repair.advanced,
438
+ basis: repair.basis,
439
+ currentHead: repair.session.currentHead,
440
+ lastTurnId: repair.session.lastTurnId,
441
+ reason: repair.reason,
442
+ } : undefined,
398
443
  };
399
444
  if (wantsJson(flags) || wantsVerbose(flags)) {
400
445
  ok("session.diagnose", data, { machineId: session.machineId, sessionId: session.id });
@@ -0,0 +1,6 @@
1
+ {
2
+ "name": "@cydm/happy-elves-daemon",
3
+ "version": "0.1.0-beta.39",
4
+ "private": true,
5
+ "type": "module"
6
+ }
@@ -1,6 +1,6 @@
1
1
  import { z } from "zod";
2
2
  import { encryptedEnvelopeSchema, pairingCode, randomId, } from "../../../packages/shared/dist/index.js";
3
- import { controllerDeviceSchema, controllerInviteClaimSchema, controllerInviteCreateSchema, deviceRevokeSchema, pairingClaimSchema, pairingStartSchema, scopedTokenCreateSchema, scopedTokenRevokeSchema, sessionEventsQuerySchema, } from "./http-schemas.js";
3
+ import { controllerDeviceSchema, controllerInviteClaimSchema, controllerInviteCreateSchema, deviceRevokeSchema, pairingClaimSchema, pairingStartSchema, scopedTokenCreateSchema, scopedTokenRevokeSchema, sessionHeadAdvanceSchema, sessionEventsQuerySchema, } from "./http-schemas.js";
4
4
  import { HttpError } from "./errors.js";
5
5
  import { encodePairingCode, isPairingCodeMatch, tokenDigest } from "./security.js";
6
6
  const visibleSessionEventWhere = `
@@ -238,6 +238,65 @@ export function registerHttpRoutes(app, context, controllerInviteTtlMs) {
238
238
  newestEventId: newestRow?.id,
239
239
  };
240
240
  });
241
+ app.post("/api/sessions/:sessionId/head", async (request) => {
242
+ const token = context.requireHttpToken(request.headers.authorization, "controller");
243
+ context.assertHttpScopedAction(token, "session.repairHead");
244
+ const params = z.object({ sessionId: z.string().min(1) }).parse(request.params);
245
+ const body = sessionHeadAdvanceSchema.parse(request.body);
246
+ context.assertScopedSession(context.tokenScope(token), token.account_id, params.sessionId);
247
+ const session = context.db
248
+ .prepare("SELECT * FROM sessions WHERE account_id = ? AND id = ?")
249
+ .get(token.account_id, params.sessionId);
250
+ if (!session)
251
+ throw new HttpError(404, "SESSION_NOT_FOUND", "Session not found");
252
+ if (session.status === "running") {
253
+ throw new HttpError(409, "SESSION_HEAD_REPAIR_RUNNING", "Cannot repair a running session head");
254
+ }
255
+ const basisEvent = context.db
256
+ .prepare(`SELECT * FROM session_events
257
+ WHERE account_id = ? AND session_id = ? AND machine_id = ? AND message_id = ?`)
258
+ .get(token.account_id, params.sessionId, session.machine_id, body.basis.eventMessageId);
259
+ if (!basisEvent)
260
+ throw new HttpError(409, "SESSION_HEAD_BASIS_MISSING", "Head advance basis event is not available");
261
+ if (basisEvent.turn_id !== body.basis.turnId || basisEvent.turn_seq !== body.basis.turnSeq) {
262
+ throw new HttpError(409, "SESSION_HEAD_BASIS_MISMATCH", "Head advance basis does not match the stored event");
263
+ }
264
+ const nextOrder = logicalEventOrderForRow(basisEvent);
265
+ const currentOrder = sessionHeadBasisOrderForRow(context.db, session);
266
+ const rewriteFenced = typeof session.head_basis_message_id === "string" &&
267
+ session.head_basis_message_id.startsWith("rewrite:");
268
+ const advanced = !rewriteFenced && compareHeadOrders(nextOrder, currentOrder) > 0;
269
+ if (advanced) {
270
+ const ts = context.now();
271
+ 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 = ?,
278
+ 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);
280
+ }
281
+ const row = context.db
282
+ .prepare("SELECT * FROM sessions WHERE account_id = ? AND id = ?")
283
+ .get(token.account_id, params.sessionId);
284
+ if (!row)
285
+ throw new HttpError(404, "SESSION_NOT_FOUND", "Session not found");
286
+ const snapshot = context.sessionSnapshot(row);
287
+ context.broadcastControllers(token.account_id, { type: "server:session", session: snapshot });
288
+ return {
289
+ advanced,
290
+ session: snapshot,
291
+ basis: {
292
+ eventId: basisEvent.id,
293
+ messageId: basisEvent.message_id ?? undefined,
294
+ logicalTime: nextOrder.time,
295
+ turnSeq: nextOrder.seq,
296
+ },
297
+ ...(rewriteFenced ? { reason: "rewrite-fenced" } : {}),
298
+ };
299
+ });
241
300
  app.get("/api/devices", async (request) => {
242
301
  const token = context.requireFullControllerToken(request.headers.authorization);
243
302
  const rows = context.db
@@ -392,4 +451,40 @@ export function registerHttpRoutes(app, context, controllerInviteTtlMs) {
392
451
  return { tokenId: body.tokenId, revokedAt: target.revoked_at ?? ts };
393
452
  });
394
453
  }
454
+ function compareHeadOrders(next, current) {
455
+ if (!current)
456
+ return 1;
457
+ if (next.time !== current.time)
458
+ return next.time > current.time ? 1 : -1;
459
+ if (next.seq !== current.seq)
460
+ return next.seq > current.seq ? 1 : -1;
461
+ if (next.id !== current.id)
462
+ return next.id > current.id ? 1 : -1;
463
+ return 0;
464
+ }
465
+ function logicalEventOrderForRow(row) {
466
+ if (row.occurred_at !== null && row.occurred_at !== undefined) {
467
+ return { time: row.occurred_at, seq: row.turn_seq ?? row.seq, id: row.id };
468
+ }
469
+ if (row.message_id?.startsWith("hist_") && row.turn_seq !== null && row.turn_seq !== undefined) {
470
+ return { time: 0, seq: row.turn_seq, id: row.id };
471
+ }
472
+ return { time: row.created_at, seq: row.turn_seq ?? row.seq, id: row.id };
473
+ }
474
+ function sessionHeadBasisOrderForRow(db, row) {
475
+ if (row.head_basis_logical_time !== null && row.head_basis_turn_seq !== null) {
476
+ return {
477
+ time: row.head_basis_logical_time,
478
+ seq: row.head_basis_turn_seq,
479
+ id: row.head_basis_event_id ?? 0,
480
+ };
481
+ }
482
+ const eventId = row.current_head ? /^event:(\d+)$/.exec(row.current_head)?.[1] : undefined;
483
+ if (!eventId)
484
+ return undefined;
485
+ const event = db
486
+ .prepare("SELECT * FROM session_events WHERE account_id = ? AND session_id = ? AND machine_id = ? AND id = ?")
487
+ .get(row.account_id, row.id, row.machine_id, Number(eventId));
488
+ return event ? logicalEventOrderForRow(event) : undefined;
489
+ }
395
490
  //# sourceMappingURL=http-routes.js.map
@@ -46,3 +46,13 @@ export declare const sessionEventsQuerySchema: z.ZodObject<{
46
46
  limit: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
47
47
  since: z.ZodOptional<z.ZodCoercedNumber<unknown>>;
48
48
  }, z.core.$strip>;
49
+ export declare const sessionHeadAdvanceSchema: z.ZodObject<{
50
+ currentHead: z.ZodString;
51
+ lastTurnId: z.ZodString;
52
+ basis: z.ZodObject<{
53
+ turnId: z.ZodString;
54
+ eventMessageId: z.ZodString;
55
+ turnSeq: z.ZodNumber;
56
+ occurredAt: z.ZodOptional<z.ZodNumber>;
57
+ }, z.core.$strip>;
58
+ }, z.core.$strip>;
@@ -53,4 +53,14 @@ export const sessionEventsQuerySchema = z.object({
53
53
  }).refine((query) => query.beforeCursor === undefined || query.since === undefined, {
54
54
  message: "since and beforeCursor cannot both be provided",
55
55
  });
56
+ export const sessionHeadAdvanceSchema = z.object({
57
+ currentHead: z.string().min(1),
58
+ lastTurnId: z.string().min(1),
59
+ basis: z.object({
60
+ turnId: z.string().min(1),
61
+ eventMessageId: z.string().min(1),
62
+ turnSeq: z.number().int().min(0),
63
+ occurredAt: z.number().int().optional(),
64
+ }),
65
+ });
56
66
  //# sourceMappingURL=http-schemas.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.37",
3
+ "version": "0.1.0-beta.39",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -11,6 +11,7 @@
11
11
  "README.md",
12
12
  "apps/cli/dist",
13
13
  "apps/daemon/dist",
14
+ "apps/daemon/package.json",
14
15
  "apps/relay/dist",
15
16
  "packages/shared/dist",
16
17
  "packages/client/dist",
@@ -1,6 +1,6 @@
1
1
  import { type DirectoryListing, type EncryptedSessionEvent, type HistoricalSessionSnapshot, type MachineSnapshot, type PairingStartResponse, type SessionSnapshot } from "../../shared/dist/index.js";
2
2
  import { sendCommand } from "./transport.js";
3
- import type { AddControllerDeviceInput, AddControllerDeviceResult, CancelInput, CollectInput, CommandResult, ControllerClientConfig, ControllerInviteClaimInput, ControllerInviteClaimResult, ControllerInviteCreateInput, ControllerInviteCreateResult, ControllerSnapshot, ControllerSubscriptionHandlers, CreateScopedTokenInput, CreateSessionInput, DecodedSessionEvent, DecodedSessionEventsPage, DeviceListResponse, DirectoryInput, FilePreviewInput, ForkInput, HistoricalSessionListPage, HistoricalSessionsInput, ImportHistoricalSessionInput, MachineMetadata, MachineDevExecInput, MachineDevExecResponse, MachineDiagnosticsResponse, RunOptions, ScopedTokenInfo, ScopedTokenSelf, SessionFilter, SessionMetadata, WaitInput } from "./types.js";
3
+ import type { AddControllerDeviceInput, AddControllerDeviceResult, CancelInput, CollectInput, CommandResult, ControllerClientConfig, ControllerInviteClaimInput, ControllerInviteClaimResult, ControllerInviteCreateInput, ControllerInviteCreateResult, ControllerSnapshot, ControllerSubscriptionHandlers, CreateScopedTokenInput, CreateSessionInput, DecodedSessionEvent, DecodedSessionEventsPage, DeviceListResponse, DirectoryInput, FilePreviewInput, ForkInput, HistoricalSessionListPage, HistoricalSessionsInput, ImportHistoricalSessionInput, MachineMetadata, MachineDevExecInput, MachineDevExecResponse, MachineDiagnosticsResponse, RepairSessionHeadInput, RepairSessionHeadResult, RunOptions, ScopedTokenInfo, ScopedTokenSelf, SessionFilter, SessionMetadata, WaitInput } from "./types.js";
4
4
  export declare class ControllerClient {
5
5
  readonly config: ControllerClientConfig;
6
6
  constructor(config: ControllerClientConfig);
@@ -33,6 +33,7 @@ export declare class ControllerClient {
33
33
  createSession(input: CreateSessionInput): Promise<CommandResult>;
34
34
  collect(sessionId: string, input?: CollectInput): Promise<DecodedSessionEvent[]>;
35
35
  collectPage(sessionId: string, input?: CollectInput): Promise<DecodedSessionEventsPage>;
36
+ repairSessionHead(sessionId: string, input: RepairSessionHeadInput): Promise<RepairSessionHeadResult>;
36
37
  wait(sessionId: string, input?: WaitInput): Promise<SessionSnapshot>;
37
38
  resume(sessionId: string): Promise<CommandResult>;
38
39
  listHistoricalSessionsPage(input: HistoricalSessionsInput): Promise<HistoricalSessionListPage>;
@@ -2,7 +2,7 @@ import { decryptJson, encryptJson, normalizeSessionName, parseServerMessage, ran
2
2
  import { addControllerDevice, claimControllerInvite, createControllerInvite, createPairingCode, createScopedToken, listDevices, listScopedTokens, revokeDevice, revokeScopedToken, tokenSelf, } from "./account.js";
3
3
  import { ControllerClientError, ControllerCommandError } from "./errors.js";
4
4
  import { authenticatedJson, normalizeRelayUrl, readRelayJson, relayHttpError } from "./http.js";
5
- import { parseSessionEventsResponse, } from "./parsers.js";
5
+ import { parseRepairSessionHeadResult, parseSessionEventsResponse, } from "./parsers.js";
6
6
  import { sendAndWaitForDirectoryListing, sendAndWaitForFilePreview, sendAndWaitForHistoricalSessions, sendAndWaitForMachineDevExecResult, sendAndWaitForMachineDiagnostics, sendCommand, subscribe, } from "./transport.js";
7
7
  import { DEFAULT_SESSION_CREATE_WAIT_MS, } from "./types.js";
8
8
  import { isWaitCondition, matchesWaitCondition, parseEventCursor, parseEventLimit, parsePositiveDuration, requireNonEmpty, sleep, } from "./validation.js";
@@ -161,6 +161,27 @@ export class ControllerClient {
161
161
  ...(page.oldestEventId === undefined ? {} : { oldestEventId: page.oldestEventId }),
162
162
  };
163
163
  }
164
+ async repairSessionHead(sessionId, input) {
165
+ const checkedSessionId = requireNonEmpty(sessionId, "sessionId");
166
+ const currentHead = requireNonEmpty(input.currentHead, "currentHead");
167
+ const lastTurnId = requireNonEmpty(input.lastTurnId, "lastTurnId");
168
+ const turnId = requireNonEmpty(input.basis.turnId, "basis.turnId");
169
+ const eventMessageId = requireNonEmpty(input.basis.eventMessageId, "basis.eventMessageId");
170
+ return await authenticatedJson(this.config, `/api/sessions/${encodeURIComponent(checkedSessionId)}/head`, {
171
+ method: "POST",
172
+ headers: { "content-type": "application/json" },
173
+ body: JSON.stringify({
174
+ currentHead,
175
+ lastTurnId,
176
+ basis: {
177
+ turnId,
178
+ eventMessageId,
179
+ turnSeq: input.basis.turnSeq,
180
+ ...(input.basis.occurredAt === undefined ? {} : { occurredAt: input.basis.occurredAt }),
181
+ },
182
+ }),
183
+ }, parseRepairSessionHeadResult);
184
+ }
164
185
  async wait(sessionId, input = {}) {
165
186
  const checkedSessionId = requireNonEmpty(sessionId, "sessionId");
166
187
  const until = input.until ?? "idle";
@@ -1,4 +1,4 @@
1
1
  export { ControllerClient } from "./client.js";
2
2
  export { ControllerClientError, ControllerCommandError } from "./errors.js";
3
- export type { AddControllerDeviceInput, AddControllerDeviceResult, CancelInput, CollectInput, CommandResult, ControllerClientConfig, ControllerDevice, ControllerInviteClaimInput, ControllerInviteClaimResult, ControllerInviteCreateInput, ControllerInviteCreateResult, ControllerSnapshot, ControllerSubscriptionHandlers, CreateScopedTokenInput, CreateSessionInput, DecodedSessionEvent, DecodedSessionEventsPage, DeviceListResponse, DirectoryInput, DirectoryListResponse, FilePreviewInput, FilePreviewResponse, EventCursor, ForkInput, HistoricalSessionListPage, HistoricalSessionListResponse, HistoricalSessionsInput, ImportHistoricalSessionInput, MachineListResponse, MachineMetadata, PermissionMode, RunOptions, ScopedTokenInfo, ScopedTokenScope, ScopedTokenSelf, SessionFilter, SessionMetadata, WaitCondition, WaitInput, } from "./types.js";
3
+ export type { AddControllerDeviceInput, AddControllerDeviceResult, CancelInput, CollectInput, CommandResult, ControllerClientConfig, ControllerDevice, ControllerInviteClaimInput, ControllerInviteClaimResult, ControllerInviteCreateInput, ControllerInviteCreateResult, ControllerSnapshot, ControllerSubscriptionHandlers, CreateScopedTokenInput, CreateSessionInput, DecodedSessionEvent, DecodedSessionEventsPage, DeviceListResponse, DirectoryInput, DirectoryListResponse, FilePreviewInput, FilePreviewResponse, EventCursor, ForkInput, HistoricalSessionListPage, HistoricalSessionListResponse, HistoricalSessionsInput, ImportHistoricalSessionInput, MachineListResponse, MachineMetadata, PermissionMode, RepairSessionHeadInput, RepairSessionHeadResult, RunOptions, ScopedTokenInfo, ScopedTokenScope, ScopedTokenSelf, SessionFilter, SessionMetadata, WaitCondition, WaitInput, } from "./types.js";
4
4
  //# sourceMappingURL=index.d.ts.map
@@ -1,5 +1,5 @@
1
1
  import { type PairingStartResponse } from "../../shared/dist/index.js";
2
- import type { AddControllerDeviceResult, ControllerInviteClaimResult, ControllerInviteCreateResult, DeviceListResponse, ScopedTokenInfo, ScopedTokenSelf, SessionEventsResponse } from "./types.js";
2
+ import type { AddControllerDeviceResult, ControllerInviteClaimResult, ControllerInviteCreateResult, DeviceListResponse, ScopedTokenInfo, ScopedTokenSelf, RepairSessionHeadResult, SessionEventsResponse } from "./types.js";
3
3
  export declare function parsePairingStartResponse(value: unknown): PairingStartResponse;
4
4
  export declare function parseDevicesResponse(value: unknown): DeviceListResponse;
5
5
  export declare function parseAddControllerDeviceResult(value: unknown): AddControllerDeviceResult;
@@ -22,4 +22,5 @@ export declare function parseScopedTokenRevoke(value: unknown): {
22
22
  revokedAt: number;
23
23
  };
24
24
  export declare function parseSessionEventsResponse(value: unknown): SessionEventsResponse;
25
+ export declare function parseRepairSessionHeadResult(value: unknown): RepairSessionHeadResult;
25
26
  //# sourceMappingURL=parsers.d.ts.map
@@ -175,4 +175,22 @@ export function parseSessionEventsResponse(value) {
175
175
  ...(oldestEventId === undefined ? {} : { oldestEventId }),
176
176
  };
177
177
  }
178
+ export function parseRepairSessionHeadResult(value) {
179
+ const record = assertRecord(value, "Session head repair");
180
+ const session = assertRecord(record.session, "Session head repair session");
181
+ const basis = assertRecord(record.basis, "Session head repair basis");
182
+ const messageId = basis.messageId;
183
+ const reason = record.reason;
184
+ return {
185
+ advanced: readBooleanField(record, "advanced", "Session head repair"),
186
+ session: session,
187
+ basis: {
188
+ eventId: readNumberField(basis, "eventId", "Session head repair basis"),
189
+ ...(typeof messageId === "string" ? { messageId } : {}),
190
+ logicalTime: readNumberField(basis, "logicalTime", "Session head repair basis"),
191
+ turnSeq: readNumberField(basis, "turnSeq", "Session head repair basis"),
192
+ },
193
+ ...(typeof reason === "string" ? { reason } : {}),
194
+ };
195
+ }
178
196
  //# sourceMappingURL=parsers.js.map
@@ -202,6 +202,27 @@ export type CollectInput = {
202
202
  since?: EventCursor;
203
203
  limit?: number;
204
204
  };
205
+ export type RepairSessionHeadInput = {
206
+ currentHead: string;
207
+ lastTurnId: string;
208
+ basis: {
209
+ turnId: string;
210
+ eventMessageId: string;
211
+ turnSeq: number;
212
+ occurredAt?: number;
213
+ };
214
+ };
215
+ export type RepairSessionHeadResult = {
216
+ advanced: boolean;
217
+ session: SessionSnapshot;
218
+ basis: {
219
+ eventId: number;
220
+ messageId?: string;
221
+ logicalTime: number;
222
+ turnSeq: number;
223
+ };
224
+ reason?: string;
225
+ };
205
226
  export type ControllerInviteCreateInput = {
206
227
  deviceName?: string;
207
228
  ttlMs?: number;