@cydm/happy-elves 0.1.0-beta.49 → 0.1.0-beta.50

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.
@@ -213,6 +213,8 @@ function diagnosticSessionMetadata(metadata) {
213
213
  historicalOlderBackfilledEvents: numberMetadata(metadata, "historicalOlderBackfilledEvents"),
214
214
  historicalTailRepairedEvents: numberMetadata(metadata, "historicalTailRepairedEvents"),
215
215
  truncatedHistoricalBackfill: metadata.truncatedHistoricalBackfill === true,
216
+ runtimeResumeStatus: stringMetadata(metadata, "runtimeResumeStatus"),
217
+ runtimeResumeError: stringMetadata(metadata, "runtimeResumeError"),
216
218
  importRequestId: stringMetadata(metadata, "importRequestId"),
217
219
  currentHead: stringMetadata(metadata, "currentHead"),
218
220
  emptyRuntimeSession: metadata.emptyRuntimeSession === true,
@@ -342,6 +342,27 @@ export function createExternalAgentRuntimeProvider(options) {
342
342
  }), "ensureSession result handle");
343
343
  return toRuntimeHandle(input.sessionKey, handle, input.cwd);
344
344
  },
345
+ async attachSession(input) {
346
+ const client = requireClientForAgent(input.agent);
347
+ if (!client) {
348
+ if (typeof options.fallback.attachSession === "function")
349
+ return await options.fallback.attachSession(input);
350
+ return await options.fallback.ensureSession({
351
+ sessionKey: input.sessionKey,
352
+ agent: input.agent,
353
+ mode: "persistent",
354
+ resumeSessionId: input.runtimeSessionId,
355
+ cwd: input.cwd,
356
+ });
357
+ }
358
+ const handle = validatedProviderHandle(client.id, {
359
+ provider: client.id,
360
+ agent: input.agent,
361
+ sessionId: input.runtimeSessionId,
362
+ cwd: input.cwd,
363
+ }, "historical runtime handle");
364
+ return toRuntimeHandle(input.sessionKey, handle, input.cwd);
365
+ },
345
366
  async listHistoricalSessions(input) {
346
367
  return (await listHistoricalSessionPage(input)).sessions;
347
368
  },
@@ -85,14 +85,14 @@ export async function handleImportHistoricalSession(ws, config, command) {
85
85
  return;
86
86
  const cwd = command.cwd || process.cwd();
87
87
  try {
88
- const handle = await getRuntime(cwd).ensureSession({
89
- sessionKey: command.sessionId,
88
+ const runtime = getRuntime(cwd);
89
+ const importedMetadata = await commandSessionMetadata(config, command.encryptedMetadata);
90
+ const handle = await attachHistoricalRuntimeSession(runtime, {
91
+ sessionId: command.sessionId,
90
92
  agent: command.agent,
91
- mode: "persistent",
92
- resumeSessionId: command.runtimeSessionId,
93
93
  cwd,
94
+ runtimeSessionId: command.runtimeSessionId,
94
95
  });
95
- assertImportedRuntimeHandleMatches(command.runtimeSessionId, handle);
96
96
  const state = {
97
97
  sessionId: command.sessionId,
98
98
  agent: command.agent,
@@ -100,7 +100,6 @@ export async function handleImportHistoricalSession(ws, config, command) {
100
100
  name: command.name,
101
101
  handle,
102
102
  };
103
- const importedMetadata = await commandSessionMetadata(config, command.encryptedMetadata);
104
103
  sessions.set(command.sessionId, state);
105
104
  const importedAt = new Date().toISOString();
106
105
  const importedMetadataBase = {
@@ -133,14 +132,41 @@ export async function handleImportHistoricalSession(ws, config, command) {
133
132
  ...historicalBackfillMetadata(backfill),
134
133
  },
135
134
  });
135
+ let readyState = backfilledState;
136
+ let runtimeResumeError;
137
+ try {
138
+ const resumedHandle = await runtime.ensureSession({
139
+ sessionKey: command.sessionId,
140
+ agent: command.agent,
141
+ mode: "persistent",
142
+ resumeSessionId: command.runtimeSessionId,
143
+ cwd,
144
+ });
145
+ assertImportedRuntimeHandleMatches(command.runtimeSessionId, resumedHandle);
146
+ readyState = { ...backfilledState, handle: resumedHandle };
147
+ sessions.set(command.sessionId, readyState);
148
+ }
149
+ catch (error) {
150
+ runtimeResumeError = errorMessage(error);
151
+ await appendAudit({
152
+ sessionId: command.sessionId,
153
+ machineId: config.machineId,
154
+ actor: "system",
155
+ action: "session.import.runtime_resume_unavailable",
156
+ summary: `Imported historical transcript but could not resume runtime session ${command.runtimeSessionId}`,
157
+ evidence: { requestId: command.requestId, runtimeSessionId: command.runtimeSessionId, error: runtimeResumeError },
158
+ });
159
+ }
136
160
  await sendCommandResponse(ws, {
137
161
  type: "machine:sessionReady",
138
162
  requestId: command.requestId,
139
163
  sessionId: command.sessionId,
140
- encryptedMetadata: await encryptedSessionMetadata(config, command.sessionId, backfilledState, {
164
+ encryptedMetadata: await encryptedSessionMetadata(config, command.sessionId, readyState, {
141
165
  ...importedMetadataBase,
142
166
  ...historicalBackfillMetadata(backfill),
143
167
  historicalBackfilledAt: new Date().toISOString(),
168
+ runtimeResumeStatus: runtimeResumeError ? "unavailable" : "available",
169
+ ...(runtimeResumeError ? { runtimeResumeError } : {}),
144
170
  }),
145
171
  });
146
172
  }
@@ -162,6 +188,49 @@ export async function handleImportHistoricalSession(ws, config, command) {
162
188
  });
163
189
  }
164
190
  }
191
+ async function attachHistoricalRuntimeSession(runtime, input) {
192
+ if (typeof runtime.attachSession === "function") {
193
+ const handle = await runtime.attachSession({
194
+ sessionKey: input.sessionId,
195
+ agent: input.agent,
196
+ cwd: input.cwd,
197
+ runtimeSessionId: input.runtimeSessionId,
198
+ });
199
+ assertImportedRuntimeHandleMatches(input.runtimeSessionId, handle);
200
+ return handle;
201
+ }
202
+ const handle = await runtime.ensureSession({
203
+ sessionKey: input.sessionId,
204
+ agent: input.agent,
205
+ mode: "persistent",
206
+ resumeSessionId: input.runtimeSessionId,
207
+ cwd: input.cwd,
208
+ });
209
+ assertImportedRuntimeHandleMatches(input.runtimeSessionId, handle);
210
+ return handle;
211
+ }
212
+ function errorMessage(error) {
213
+ return error instanceof Error ? error.message : String(error);
214
+ }
215
+ function resumeImportMetadata(fallback, runtimeResumeError, runtimeResumeAttempted = false) {
216
+ const metadata = {};
217
+ if (fallback?.importedFromRuntimeSessionId)
218
+ metadata.importedFromRuntimeSessionId = fallback.importedFromRuntimeSessionId;
219
+ if (runtimeResumeError) {
220
+ metadata.runtimeResumeStatus = "unavailable";
221
+ metadata.runtimeResumeError = runtimeResumeError;
222
+ }
223
+ else if (runtimeResumeAttempted && fallback?.importedFromRuntimeSessionId) {
224
+ metadata.runtimeResumeStatus = "available";
225
+ }
226
+ else {
227
+ if (fallback?.runtimeResumeStatus)
228
+ metadata.runtimeResumeStatus = fallback.runtimeResumeStatus;
229
+ if (fallback?.runtimeResumeError)
230
+ metadata.runtimeResumeError = fallback.runtimeResumeError;
231
+ }
232
+ return metadata;
233
+ }
165
234
  export async function handleListHistoricalSessions(ws, config, command) {
166
235
  if (command.type !== "machine:listHistoricalSessions")
167
236
  return;
@@ -252,7 +321,7 @@ export async function resumeSession(ws, config, sessionId, requestId, fallback,
252
321
  if (existing) {
253
322
  const refreshed = await refreshRuntimeSessionName(existing);
254
323
  const nameChanged = refreshed.name !== existing.name;
255
- const backfill = await backfillHistoricalEvents(ws, config, refreshed, undefined, refreshed.historicalBackfill ?? fallback?.historicalBackfill);
324
+ const backfill = await backfillHistoricalEvents(ws, config, refreshed, fallback?.importedFromRuntimeSessionId, refreshed.historicalBackfill ?? fallback?.historicalBackfill);
256
325
  const resumed = withHistoricalBackfill(refreshed, backfill);
257
326
  sessions.set(sessionId, resumed);
258
327
  await appendAudit({
@@ -277,6 +346,7 @@ export async function resumeSession(ws, config, sessionId, requestId, fallback,
277
346
  ...(nameChanged ? { name: resumed.name } : {}),
278
347
  encryptedMetadata: await encryptedSessionMetadata(config, sessionId, resumed, {
279
348
  resumedAt: new Date().toISOString(),
349
+ ...resumeImportMetadata(fallback),
280
350
  ...historicalBackfillMetadata(backfill),
281
351
  ...(nameChanged ? { providerName: resumed.name, runtimeNameSyncedAt: new Date().toISOString() } : {}),
282
352
  }),
@@ -289,20 +359,45 @@ export async function resumeSession(ws, config, sessionId, requestId, fallback,
289
359
  const name = fallback?.name ?? sessionId;
290
360
  const resumeSessionId = fallback?.runtime?.backendSessionId || fallback?.runtime?.agentSessionId;
291
361
  const resumeHandle = runtimeHandleFromFallback(sessionId, agent, cwd, fallback);
362
+ const historicalRuntimeSessionId = fallback?.importedFromRuntimeSessionId ?? resumeSessionId;
292
363
  try {
293
- const handle = await getRuntime(cwd).ensureSession({
294
- sessionKey: sessionId,
295
- agent,
296
- mode: "persistent",
297
- resumeSessionId,
298
- resumeHandle,
299
- cwd,
300
- restartableEmptySession: fallback?.emptyRuntimeSession === true,
301
- });
364
+ const runtime = getRuntime(cwd);
365
+ let runtimeResumeError;
366
+ let handle;
367
+ try {
368
+ handle = await runtime.ensureSession({
369
+ sessionKey: sessionId,
370
+ agent,
371
+ mode: "persistent",
372
+ resumeSessionId,
373
+ resumeHandle,
374
+ cwd,
375
+ restartableEmptySession: fallback?.emptyRuntimeSession === true,
376
+ });
377
+ }
378
+ catch (error) {
379
+ if (!fallback?.importedFromRuntimeSessionId)
380
+ throw error;
381
+ runtimeResumeError = errorMessage(error);
382
+ handle = await attachHistoricalRuntimeSession(runtime, {
383
+ sessionId,
384
+ agent,
385
+ cwd,
386
+ runtimeSessionId: fallback.importedFromRuntimeSessionId,
387
+ });
388
+ await appendAudit({
389
+ sessionId,
390
+ machineId: config.machineId,
391
+ actor: "system",
392
+ action: "session.resume.runtime_resume_unavailable",
393
+ summary: `Resumed historical transcript but could not resume runtime session ${fallback.importedFromRuntimeSessionId}`,
394
+ evidence: { requestId, runtimeSessionId: fallback.importedFromRuntimeSessionId, error: runtimeResumeError },
395
+ });
396
+ }
302
397
  const state = await refreshRuntimeSessionName({ sessionId, agent, cwd, name, handle });
303
398
  sessions.set(sessionId, state);
304
399
  const nameChanged = state.name !== name;
305
- const backfill = await backfillHistoricalEvents(ws, config, state, resumeSessionId, fallback?.historicalBackfill);
400
+ const backfill = await backfillHistoricalEvents(ws, config, state, historicalRuntimeSessionId, fallback?.historicalBackfill);
306
401
  const resumed = withHistoricalBackfill(state, backfill);
307
402
  sessions.set(sessionId, resumed);
308
403
  await appendAudit({
@@ -327,6 +422,7 @@ export async function resumeSession(ws, config, sessionId, requestId, fallback,
327
422
  ...(nameChanged ? { name: resumed.name } : {}),
328
423
  encryptedMetadata: await encryptedSessionMetadata(config, sessionId, resumed, {
329
424
  resumedAt: new Date().toISOString(),
425
+ ...resumeImportMetadata(fallback, runtimeResumeError, true),
330
426
  ...historicalBackfillMetadata(backfill),
331
427
  ...(nameChanged ? { providerName: resumed.name, runtimeNameSyncedAt: new Date().toISOString() } : {}),
332
428
  }),
@@ -37,6 +37,9 @@ export async function sessionFallbackFromCommand(config, command) {
37
37
  cwd: metadata.cwd || process.cwd(),
38
38
  name: metadata.name ?? command.sessionId,
39
39
  emptyRuntimeSession: metadata.emptyRuntimeSession === true,
40
+ importedFromRuntimeSessionId: metadata.importedFromRuntimeSessionId,
41
+ runtimeResumeStatus: metadata.runtimeResumeStatus,
42
+ runtimeResumeError: metadata.runtimeResumeError,
40
43
  historicalBackfill: historicalBackfillStateFromMetadata(metadata),
41
44
  runtime: metadata.runtime,
42
45
  };
@@ -65,6 +65,8 @@ export type SessionMetadata = {
65
65
  rewoundTo?: string;
66
66
  currentHead?: string;
67
67
  emptyRuntimeSession?: boolean;
68
+ runtimeResumeStatus?: "available" | "unavailable";
69
+ runtimeResumeError?: string;
68
70
  historicalBackfill?: "running" | HistoricalBackfillStatus;
69
71
  historicalBackfillCursor?: number;
70
72
  historicalBackfillProviderCursor?: string;
@@ -86,6 +88,9 @@ export type SessionMetadata = {
86
88
  };
87
89
  export type SessionFallback = Pick<SessionState, "agent" | "cwd" | "name"> & {
88
90
  emptyRuntimeSession?: boolean;
91
+ importedFromRuntimeSessionId?: string;
92
+ runtimeResumeStatus?: "available" | "unavailable";
93
+ runtimeResumeError?: string;
89
94
  historicalBackfill?: HistoricalBackfillState;
90
95
  runtime?: SessionMetadata["runtime"];
91
96
  };
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.49",
3
+ "version": "0.1.0-beta.50",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -1,12 +1,12 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.49",
3
+ "version": "0.1.0-beta.50",
4
4
  "lockfileVersion": 3,
5
5
  "requires": true,
6
6
  "packages": {
7
7
  "": {
8
8
  "name": "@cydm/happy-elves",
9
- "version": "0.1.0-beta.49",
9
+ "version": "0.1.0-beta.50",
10
10
  "license": "Apache-2.0",
11
11
  "dependencies": {
12
12
  "@fastify/cors": "11.2.0",
@@ -775,9 +775,9 @@
775
775
  }
776
776
  },
777
777
  "node_modules/bare-fs": {
778
- "version": "4.7.2",
779
- "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.2.tgz",
780
- "integrity": "sha512-aTvMFUWkBmjzKtEQMDGGDNF8bkfpD5N1b/FCwt7A3wrU4t1o/e/85Wzkluh6JlODCjqVESYCkQCdTXqZ9G7VFg==",
778
+ "version": "4.7.3",
779
+ "resolved": "https://registry.npmjs.org/bare-fs/-/bare-fs-4.7.3.tgz",
780
+ "integrity": "sha512-xRgplks8SvcKkdlv2M6Z2LZmRsmqd+x0nXXGXeMEjwdibj1HSDrlnqBRLeYdMvsgCox7Bq0e+DHwfczOfsn6IA==",
781
781
  "license": "Apache-2.0",
782
782
  "dependencies": {
783
783
  "bare-events": "^2.5.4",
@@ -1552,9 +1552,9 @@
1552
1552
  }
1553
1553
  },
1554
1554
  "node_modules/tsx": {
1555
- "version": "4.22.4",
1556
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.4.tgz",
1557
- "integrity": "sha512-X8EX+XV4QR5xCsrgxaED954zTDfY8KqlDtskKEL0cHhyS/P8b4IFOvGDQpsC9Q1XnLq915wEfwwY/zzskCtmhg==",
1555
+ "version": "4.22.5",
1556
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.22.5.tgz",
1557
+ "integrity": "sha512-F7JnSfPl5ASt6LqwWyUQ3T8BwN3q0eQEbFMYa2iRWaVQmmudo0d7fRmwM4O002gsvW1bs0yBYioutsAjqLJMvQ==",
1558
1558
  "license": "MIT",
1559
1559
  "dependencies": {
1560
1560
  "esbuild": "~0.28.0"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.49",
3
+ "version": "0.1.0-beta.50",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -58,6 +58,12 @@ export interface HappyElvesRuntime {
58
58
  permissionMode?: RuntimePermissionMode;
59
59
  restartableEmptySession?: boolean;
60
60
  }): Promise<RuntimeHandle>;
61
+ attachSession?(input: {
62
+ sessionKey: string;
63
+ agent: string;
64
+ cwd?: string;
65
+ runtimeSessionId: string;
66
+ }): RuntimeHandle | Promise<RuntimeHandle>;
61
67
  listHistoricalSessions?(input: {
62
68
  agent?: string;
63
69
  cwd?: string;
@@ -474,40 +474,78 @@ async function findCodexRolloutPathBySessionId(directories, sessionId, fileCache
474
474
  return undefined;
475
475
  }
476
476
  async function readCodexRolloutMeta(path) {
477
- const firstLine = await readFirstLine(path);
478
- const record = firstLine ? parseJsonRecord(firstLine) : undefined;
479
- if (!record || record.type !== "session_meta")
480
- return undefined;
481
- const payload = recordFrom(record.payload);
482
- const id = stringField(payload, "id");
483
- if (!id)
477
+ const fallbackId = codexRolloutIdFromPath(path);
478
+ const lines = await readInitialLines(path, 64, 256 * 1024);
479
+ let createdAt;
480
+ let cwd;
481
+ for (const line of lines) {
482
+ const record = line ? parseJsonRecord(line) : undefined;
483
+ if (!record)
484
+ continue;
485
+ createdAt ??= timestampMs(record.timestamp);
486
+ const payload = recordFrom(record.payload);
487
+ cwd ??= stringField(payload, "cwd");
488
+ if (record.type !== "session_meta")
489
+ continue;
490
+ const id = stringField(payload, "id") ?? fallbackId;
491
+ if (!id)
492
+ return undefined;
493
+ return {
494
+ id,
495
+ cwd,
496
+ createdAt: timestampMs(payload.timestamp ?? record.timestamp) ?? createdAt,
497
+ };
498
+ }
499
+ if (!fallbackId)
484
500
  return undefined;
501
+ const id = fallbackId;
485
502
  return {
486
503
  id,
487
- cwd: stringField(payload, "cwd"),
488
- createdAt: timestampMs(payload.timestamp ?? record.timestamp),
504
+ cwd,
505
+ createdAt,
489
506
  };
490
507
  }
491
- async function readFirstLine(path) {
492
- return await new Promise((resolveLine) => {
508
+ function codexRolloutIdFromPath(path) {
509
+ const match = basename(path).match(/([0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12})\.jsonl$/iu);
510
+ return match?.[1];
511
+ }
512
+ async function readInitialLines(path, maxLines, maxBytes) {
513
+ return await new Promise((resolveLines) => {
493
514
  const stream = createReadStream(path, { encoding: "utf8" });
494
515
  let buffer = "";
516
+ let readBytes = 0;
517
+ const lines = [];
495
518
  let settled = false;
496
- const settle = (line = "") => {
519
+ const settle = () => {
497
520
  if (settled)
498
521
  return;
499
522
  settled = true;
500
523
  stream.destroy();
501
- resolveLine(line);
524
+ resolveLines(lines);
502
525
  };
503
526
  stream.on("data", (chunk) => {
527
+ readBytes += chunk.length;
504
528
  buffer += chunk;
505
- const newlineIndex = buffer.indexOf("\n");
506
- if (newlineIndex >= 0)
507
- settle(buffer.slice(0, newlineIndex));
529
+ let newlineIndex = buffer.indexOf("\n");
530
+ while (newlineIndex >= 0) {
531
+ lines.push(buffer.slice(0, newlineIndex));
532
+ buffer = buffer.slice(newlineIndex + 1);
533
+ if (lines.length >= maxLines)
534
+ return settle();
535
+ newlineIndex = buffer.indexOf("\n");
536
+ }
537
+ if (readBytes >= maxBytes) {
538
+ if (buffer)
539
+ lines.push(buffer);
540
+ settle();
541
+ }
508
542
  });
509
543
  stream.on("error", () => settle());
510
- stream.on("end", () => settle(buffer));
544
+ stream.on("end", () => {
545
+ if (buffer)
546
+ lines.push(buffer);
547
+ settle();
548
+ });
511
549
  });
512
550
  }
513
551
  async function readCodexRolloutPreview(path) {
@@ -81,6 +81,14 @@ export function createCliRuntimeProvider(options) {
81
81
  assertNotExternalProviderHandle(input.resumeHandle);
82
82
  return (await sessions.ensure(input)).handle;
83
83
  },
84
+ attachSession(input) {
85
+ const cwd = input.cwd || options.cwd;
86
+ if (input.agent === "codex")
87
+ return sessions.createLoadedCodex(input.sessionKey, cwd, input.runtimeSessionId).handle;
88
+ if (input.agent === "claude")
89
+ return sessions.createLoadedClaude(input.sessionKey, cwd, input.runtimeSessionId).handle;
90
+ throw new Error(`Unsupported CLI runtime agent: ${input.agent}`);
91
+ },
84
92
  async listHistoricalSessions(input) {
85
93
  return (await listHistoricalSessionPage(input)).sessions;
86
94
  },
@@ -3,6 +3,7 @@ import { CodexAppServerSession } from "./codex-app-server.js";
3
3
  export type CliSession = {
4
4
  handle: RuntimeHandle;
5
5
  codex?: CodexAppServerSession;
6
+ codexEnsured?: boolean;
6
7
  activeClaudeTurn?: RuntimeTurn;
7
8
  claudeStarted?: boolean;
8
9
  pendingClaudeName?: string;
@@ -17,8 +17,14 @@ export class CliSessionStore {
17
17
  }
18
18
  async ensure(input) {
19
19
  const existing = this.sessions.get(input.sessionKey);
20
- if (existing)
20
+ if (existing) {
21
+ if (existing.codex && existing.codexEnsured !== true) {
22
+ const threadId = await existing.codex.ensure(input.permissionMode);
23
+ existing.handle.backendSessionId = threadId;
24
+ existing.codexEnsured = true;
25
+ }
21
26
  return existing;
27
+ }
22
28
  const cwd = input.cwd ?? this.input.cwd;
23
29
  const agent = cliAgent(input.agent);
24
30
  if (agent === "codex") {
@@ -71,6 +77,7 @@ export class CliSessionStore {
71
77
  const threadId = await codex.ensure(permissionMode);
72
78
  session.handle.backendSessionId = threadId;
73
79
  session.codex = codex;
80
+ session.codexEnsured = true;
74
81
  this.sessions.set(sessionKey, session);
75
82
  return session;
76
83
  }