@cydm/happy-elves 0.1.0-beta.297 → 0.1.0-beta.299

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.
@@ -315,7 +315,8 @@ function foldAliasedRuntimeEvents(events) {
315
315
  const profiles = new Map();
316
316
  const runtimeToRequest = new Map();
317
317
  for (const event of events) {
318
- const profile = profiles.get(event.turnId) ?? { output: "" };
318
+ const profile = profiles.get(event.turnId) ?? { output: "", hasDone: false, firstEventId: event.id };
319
+ profile.firstEventId = Math.min(profile.firstEventId, event.id);
319
320
  const payload = event.decoded;
320
321
  if (payload?.type === "user_prompt" && profile.prompt === undefined) {
321
322
  profile.prompt = payload.text.trim();
@@ -324,6 +325,7 @@ function foldAliasedRuntimeEvents(events) {
324
325
  profile.output += payload.text;
325
326
  }
326
327
  else if (payload?.type === "done") {
328
+ profile.hasDone = true;
327
329
  const runtimeTurnId = payload.lastTurnId ?? payload.currentHead;
328
330
  if (runtimeTurnId && runtimeTurnId !== event.turnId && !runtimeTurnId.startsWith("event:")) {
329
331
  runtimeToRequest.set(runtimeTurnId, event.turnId);
@@ -345,11 +347,83 @@ function foldAliasedRuntimeEvents(events) {
345
347
  duplicateRuntimeToRequest.set(runtimeTurnId, requestTurnId);
346
348
  }
347
349
  }
350
+ const inferredRuntimeToRequest = inferRuntimeToRequestAliases(profiles, runtimeToRequest);
351
+ const inferredRequestToRuntime = new Map();
352
+ for (const [runtimeTurnId, requestTurnId] of inferredRuntimeToRequest) {
353
+ runtimeToRequest.set(runtimeTurnId, requestTurnId);
354
+ inferredRequestToRuntime.set(requestTurnId, runtimeTurnId);
355
+ }
356
+ const foldedEvents = [];
357
+ for (const event of events) {
358
+ if (duplicateRuntimeTurns.has(event.turnId))
359
+ continue;
360
+ const inferredRequestTurnId = inferredRuntimeToRequest.get(event.turnId);
361
+ if (inferredRequestTurnId) {
362
+ foldedEvents.push({ ...event, turnId: inferredRequestTurnId });
363
+ continue;
364
+ }
365
+ const inferredRuntimeTurnId = inferredRequestToRuntime.get(event.turnId);
366
+ if (inferredRuntimeTurnId && shouldDropRequestSideDuplicateEvent(event, profiles.get(inferredRuntimeTurnId))) {
367
+ continue;
368
+ }
369
+ foldedEvents.push(event);
370
+ }
348
371
  return {
349
- events: duplicateRuntimeTurns.size === 0 ? events : events.filter((event) => !duplicateRuntimeTurns.has(event.turnId)),
350
- runtimeToRequest: duplicateRuntimeToRequest,
372
+ events: foldedEvents,
373
+ runtimeToRequest: new Map([...duplicateRuntimeToRequest, ...inferredRuntimeToRequest]),
351
374
  };
352
375
  }
376
+ function inferRuntimeToRequestAliases(profiles, explicitRuntimeToRequest) {
377
+ const requestsByPrompt = new Map();
378
+ const runtimesByPrompt = new Map();
379
+ for (const [turnId, profile] of profiles) {
380
+ const promptKey = comparableOutput(profile.prompt ?? "");
381
+ if (!promptKey)
382
+ continue;
383
+ if (turnId.startsWith("turn_") && !profile.hasDone) {
384
+ requestsByPrompt.set(promptKey, [...(requestsByPrompt.get(promptKey) ?? []), { turnId, output: profile.output, firstEventId: profile.firstEventId }]);
385
+ }
386
+ else if (!turnId.startsWith("turn_") && profile.hasDone && !explicitRuntimeToRequest.has(turnId)) {
387
+ runtimesByPrompt.set(promptKey, [...(runtimesByPrompt.get(promptKey) ?? []), { turnId, output: profile.output, firstEventId: profile.firstEventId }]);
388
+ }
389
+ }
390
+ const inferred = new Map();
391
+ for (const [promptKey, requests] of requestsByPrompt) {
392
+ const runtimes = runtimesByPrompt.get(promptKey) ?? [];
393
+ if (requests.length !== 1 || runtimes.length !== 1)
394
+ continue;
395
+ const request = requests[0];
396
+ const runtime = runtimes[0];
397
+ if (!request || !runtime)
398
+ continue;
399
+ if (runtime.firstEventId <= request.firstEventId)
400
+ continue;
401
+ if (!outputsAreCompatible(request.output, runtime.output))
402
+ continue;
403
+ inferred.set(runtime.turnId, request.turnId);
404
+ }
405
+ return inferred;
406
+ }
407
+ function outputsAreCompatible(requestOutput, runtimeOutput) {
408
+ const request = comparableOutput(requestOutput);
409
+ const runtime = comparableOutput(runtimeOutput);
410
+ if (!request || !runtime)
411
+ return true;
412
+ return request.includes(runtime) || runtime.includes(request);
413
+ }
414
+ function shouldDropRequestSideDuplicateEvent(event, runtimeProfile) {
415
+ if (!runtimeProfile)
416
+ return false;
417
+ const payload = event.decoded;
418
+ if (payload?.type === "user_prompt") {
419
+ return comparableOutput(payload.text) === comparableOutput(runtimeProfile.prompt ?? "");
420
+ }
421
+ if (payload?.type === "text_delta" && payload.stream !== "thought" && payload.text) {
422
+ const chunk = comparableOutput(payload.text);
423
+ return Boolean(chunk) && comparableOutput(runtimeProfile.output).includes(chunk);
424
+ }
425
+ return false;
426
+ }
353
427
  function dedupeSyntheticHistoricalEvents(events) {
354
428
  const groups = new Map();
355
429
  for (const event of events) {
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-cli",
3
- "version": "0.1.0-beta.297",
3
+ "version": "0.1.0-beta.299",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -1,4 +1,4 @@
1
- import { normalizeSessionName } from "../../../../packages/shared/dist/index.js";
1
+ import { normalizeSessionName, normalizeSessionTags } from "../../../../packages/shared/dist/index.js";
2
2
  import { appendAudit } from "../audit.js";
3
3
  import { getRuntime } from "../runtime.js";
4
4
  import { sessions } from "../state.js";
@@ -16,6 +16,7 @@ export async function handleCreateSession(ws, config, command) {
16
16
  try {
17
17
  const metadata = await commandSessionMetadata(config, command.encryptedMetadata);
18
18
  const model = modelFromCommand(command.model ?? metadata?.model);
19
+ const tags = normalizeSessionTags(metadata?.tags);
19
20
  const runtime = getRuntime(cwd);
20
21
  let handle = await runtime.ensureSession({
21
22
  sessionKey: command.sessionId,
@@ -42,6 +43,7 @@ export async function handleCreateSession(ws, config, command) {
42
43
  cwd,
43
44
  name: providerName ?? command.name,
44
45
  ...(model ? { model } : {}),
46
+ ...(tags.length > 0 ? { tags } : {}),
45
47
  handle,
46
48
  };
47
49
  sessions.set(command.sessionId, state);
@@ -257,7 +259,12 @@ function historicalSessionPage(value) {
257
259
  export async function resumeSession(ws, config, sessionId, requestId, fallback, announceSuccess = true, syncDirection = "auto") {
258
260
  const existing = sessions.get(sessionId);
259
261
  if (existing) {
260
- const refreshed = await refreshRuntimeSessionName(existing);
262
+ const hydratedExisting = fallback?.tags && fallback.tags.length > 0
263
+ ? { ...existing, tags: fallback.tags }
264
+ : existing;
265
+ if (hydratedExisting !== existing)
266
+ sessions.set(sessionId, hydratedExisting);
267
+ const refreshed = await refreshRuntimeSessionName(hydratedExisting);
261
268
  const nameChanged = refreshed.name !== existing.name;
262
269
  const backfill = await backfillHistoricalEvents(ws, config, refreshed, undefined, refreshed.historicalBackfill ?? fallback?.historicalBackfill, {
263
270
  direction: syncDirection,
@@ -308,7 +315,14 @@ export async function resumeSession(ws, config, sessionId, requestId, fallback,
308
315
  cwd,
309
316
  restartableEmptySession: fallback?.emptyRuntimeSession === true,
310
317
  });
311
- const state = await refreshRuntimeSessionName({ sessionId, agent, cwd, name, handle });
318
+ const state = await refreshRuntimeSessionName({
319
+ sessionId,
320
+ agent,
321
+ cwd,
322
+ name,
323
+ ...(fallback?.tags && fallback.tags.length > 0 ? { tags: fallback.tags } : {}),
324
+ handle,
325
+ });
312
326
  sessions.set(sessionId, state);
313
327
  const nameChanged = state.name !== name;
314
328
  const backfill = await backfillHistoricalEvents(ws, config, state, resumeSessionId, fallback?.historicalBackfill, {
@@ -1,6 +1,6 @@
1
1
  import { type EncryptedEnvelope, type MachineCommand } from "../../../../packages/shared/dist/index.js";
2
2
  import type { DaemonConfig, SessionFallback, SessionMetadata, SessionState } from "../types.js";
3
- export declare function encryptedSessionMetadata(config: DaemonConfig, sessionId: string, state: Pick<SessionState, "agent" | "cwd" | "name" | "handle" | "model">, extra: Record<string, unknown>): Promise<EncryptedEnvelope>;
3
+ export declare function encryptedSessionMetadata(config: DaemonConfig, sessionId: string, state: Pick<SessionState, "agent" | "cwd" | "name" | "handle" | "model" | "tags">, extra: Record<string, unknown>): Promise<EncryptedEnvelope>;
4
4
  export declare function commandSessionMetadata(config: DaemonConfig, encryptedMetadata?: EncryptedEnvelope): Promise<SessionMetadata | undefined>;
5
5
  export declare function sessionFallbackFromCommand(config: DaemonConfig, command: Extract<MachineCommand, {
6
6
  type: "machine:prompt" | "machine:resume" | "machine:rewind" | "machine:fork" | "machine:renameSession" | "machine:close";
@@ -1,10 +1,11 @@
1
- import { decryptJson, encryptJson } from "../../../../packages/shared/dist/index.js";
1
+ import { decryptJson, encryptJson, normalizeSessionTags, sessionTagKeys } from "../../../../packages/shared/dist/index.js";
2
2
  export function encryptedSessionMetadata(config, sessionId, state, extra) {
3
3
  return encryptJson(config.accountSecret, `session:${sessionId}:metadata`, {
4
4
  agent: state.agent,
5
5
  cwd: state.cwd,
6
6
  name: state.name,
7
7
  ...(state.model ? { model: state.model } : {}),
8
+ ...(state.tags && state.tags.length > 0 ? { tags: state.tags, tagKeys: sessionTagKeys(state.tags) } : {}),
8
9
  runtime: {
9
10
  backend: state.handle.backend,
10
11
  runtimeSessionName: state.handle.runtimeSessionName,
@@ -33,11 +34,13 @@ export async function sessionFallbackFromCommand(config, command) {
33
34
  return undefined;
34
35
  try {
35
36
  const metadata = await decryptJson(config.accountSecret, encryptedMetadata);
37
+ const tags = safeSessionTags(metadata.tags);
36
38
  return {
37
39
  agent: metadata.agent ?? "codex",
38
40
  cwd: metadata.cwd || process.cwd(),
39
41
  name: metadata.name ?? command.sessionId,
40
42
  ...(typeof metadata.model === "string" && metadata.model.trim() ? { model: metadata.model.trim() } : {}),
43
+ ...(tags.length > 0 ? { tags } : {}),
41
44
  emptyRuntimeSession: metadata.emptyRuntimeSession === true,
42
45
  historicalBackfill: historicalBackfillStateFromMetadata(metadata),
43
46
  runtime: metadata.runtime,
@@ -47,6 +50,14 @@ export async function sessionFallbackFromCommand(config, command) {
47
50
  return undefined;
48
51
  }
49
52
  }
53
+ function safeSessionTags(value) {
54
+ try {
55
+ return normalizeSessionTags(Array.isArray(value) ? value : undefined);
56
+ }
57
+ catch {
58
+ return [];
59
+ }
60
+ }
50
61
  function historicalBackfillStateFromMetadata(metadata) {
51
62
  const status = historicalBackfillStatus(metadata.historicalBackfillStatus ?? metadata.historicalBackfill);
52
63
  const totalEvents = nonNegativeInteger(metadata.historicalBackfillTotalEvents ?? metadata.availableHistoricalEvents);
@@ -54,9 +54,10 @@ export async function handleRewind(ws, config, command) {
54
54
  return;
55
55
  if (!claimCommandRequest(ws, command.requestId))
56
56
  return;
57
- const session = sessions.get(command.sessionId) ?? (await resumeSession(ws, config, command.sessionId, command.requestId, await sessionFallbackFromCommand(config, command), false));
57
+ let session = sessions.get(command.sessionId) ?? (await resumeSession(ws, config, command.sessionId, command.requestId, await sessionFallbackFromCommand(config, command), false));
58
58
  if (!session)
59
59
  return;
60
+ session = await hydrateSessionTagsFromCommand(config, command, session);
60
61
  const runtimeWithPrimitives = getRuntime(session.cwd);
61
62
  if (typeof runtimeWithPrimitives.rewindSession !== "function") {
62
63
  await emitCapabilityUnavailable(ws, config, command, "session.rewind", true);
@@ -222,14 +223,16 @@ export async function handleFork(ws, config, command) {
222
223
  });
223
224
  return;
224
225
  }
226
+ const sourceMetadata = await commandSessionMetadata(config, command.sourceEncryptedMetadata);
227
+ const targetMetadata = await commandSessionMetadata(config, command.encryptedMetadata);
225
228
  const forkState = {
226
229
  sessionId: command.targetSessionId,
227
230
  agent: source.agent,
228
231
  cwd: source.cwd,
229
232
  name: command.name,
233
+ ...(targetMetadata?.tags && targetMetadata.tags.length > 0 ? { tags: targetMetadata.tags } : {}),
230
234
  handle: forkHandle,
231
235
  };
232
- const sourceMetadata = await commandSessionMetadata(config, command.sourceEncryptedMetadata);
233
236
  sessions.set(command.targetSessionId, forkState);
234
237
  await appendAudit({
235
238
  sessionId: command.sessionId,
@@ -323,9 +326,10 @@ export async function handleRename(ws, config, command) {
323
326
  });
324
327
  return;
325
328
  }
326
- const session = sessions.get(command.sessionId) ?? (await resumeSession(ws, config, command.sessionId, command.requestId, await sessionFallbackFromCommand(config, command), false));
329
+ let session = sessions.get(command.sessionId) ?? (await resumeSession(ws, config, command.sessionId, command.requestId, await sessionFallbackFromCommand(config, command), false));
327
330
  if (!session)
328
331
  return;
332
+ session = await hydrateSessionTagsFromCommand(config, command, session);
329
333
  const runtimeWithPrimitives = getRuntime(session.cwd);
330
334
  if (typeof runtimeWithPrimitives.renameSession !== "function") {
331
335
  await emitCapabilityUnavailable(ws, config, command, "session.rename", true);
@@ -389,6 +393,14 @@ function capabilityUnavailableEvidence(command) {
389
393
  return { requestId: command.requestId, nameLength: command.name.length };
390
394
  return { requestId: command.requestId, from: command.from, targetSessionId: command.targetSessionId, name: command.name };
391
395
  }
396
+ async function hydrateSessionTagsFromCommand(config, command, session) {
397
+ const metadata = await commandSessionMetadata(config, command.encryptedMetadata);
398
+ if (!metadata?.tags || metadata.tags.length === 0)
399
+ return session;
400
+ const next = { ...session, tags: metadata.tags };
401
+ sessions.set(session.sessionId, next);
402
+ return next;
403
+ }
392
404
  function primitiveRuntimeEvidence(previous, next) {
393
405
  return {
394
406
  runtime: {
@@ -25,14 +25,19 @@ export async function handlePrompt(ws, config, command) {
25
25
  }
26
26
  if (activePromptRequests.has(command.requestId))
27
27
  return;
28
- const session = sessions.get(command.sessionId);
28
+ const fallback = await sessionFallbackFromCommand(config, command);
29
+ let session = sessions.get(command.sessionId);
29
30
  if (!session) {
30
- const resumed = await resumeSession(ws, config, command.sessionId, command.requestId, await sessionFallbackFromCommand(config, command), false);
31
+ const resumed = await resumeSession(ws, config, command.sessionId, command.requestId, fallback, false);
31
32
  if (!resumed)
32
33
  return;
33
34
  await handlePrompt(ws, config, command);
34
35
  return;
35
36
  }
37
+ if (fallback?.tags && fallback.tags.length > 0) {
38
+ session = { ...session, tags: fallback.tags };
39
+ sessions.set(command.sessionId, session);
40
+ }
36
41
  if (!claimSessionTurn(command.sessionId, command.requestId, command.turnId, "prompt")) {
37
42
  await sendCommandResponse(ws, {
38
43
  type: "machine:error",
@@ -32,6 +32,7 @@ export type SessionState = {
32
32
  cwd: string;
33
33
  name: string;
34
34
  model?: string;
35
+ tags?: string[];
35
36
  handle: RuntimeHandle;
36
37
  historicalBackfill?: HistoricalBackfillState;
37
38
  };
@@ -55,6 +56,8 @@ export type SessionMetadata = {
55
56
  name?: string;
56
57
  providerName?: string;
57
58
  model?: string;
59
+ tags?: string[];
60
+ tagKeys?: string[];
58
61
  requestedRuntimeName?: string;
59
62
  permissionMode?: PermissionMode;
60
63
  importedFromRuntimeSessionId?: string;
@@ -86,6 +89,7 @@ export type SessionMetadata = {
86
89
  };
87
90
  export type SessionFallback = Pick<SessionState, "agent" | "cwd" | "name"> & {
88
91
  model?: string;
92
+ tags?: string[];
89
93
  emptyRuntimeSession?: boolean;
90
94
  historicalBackfill?: HistoricalBackfillState;
91
95
  runtime?: SessionMetadata["runtime"];
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-daemon",
3
- "version": "0.1.0-beta.297",
3
+ "version": "0.1.0-beta.299",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -588,7 +588,7 @@ export function handleMachineMessage(context, connection, message) {
588
588
  return;
589
589
  }
590
590
  const row = result.session ?? db.prepare("SELECT * FROM sessions WHERE id = ?").get(message.sessionId);
591
- if (row)
591
+ if (row && row.status !== "closed")
592
592
  broadcastControllers(connection.accountId, { type: "server:session", session: sessionSnapshot(row) });
593
593
  acknowledgeMachineMessage();
594
594
  return;
@@ -813,6 +813,8 @@ function applySessionHeadAdvance(db, input, ts) {
813
813
  .get(input.sessionId, input.accountId, input.machineId);
814
814
  if (!sessionRow)
815
815
  return { status: "session-not-found" };
816
+ if (sessionRow.status === "closed")
817
+ return { status: "noop", session: sessionRow };
816
818
  const basisEvent = resolveHeadAdvanceBasisEvent(db, input.accountId, input.sessionId, input.machineId, input.basis);
817
819
  if (!basisEvent)
818
820
  return { status: "missing", session: sessionRow };
@@ -834,7 +836,10 @@ function applySessionHeadAdvance(db, input, ts) {
834
836
  sessionRow.status === "running" &&
835
837
  comparison === 0 &&
836
838
  currentHeadAlreadySettled;
837
- if (!shouldAdvance && !shouldSettleStatus)
839
+ const shouldRefreshMetadata = !rewriteFenced &&
840
+ input.encryptedMetadata !== undefined &&
841
+ currentHeadAlreadySettled;
842
+ if (!shouldAdvance && !shouldSettleStatus && !shouldRefreshMetadata)
838
843
  return { status: "noop", session: sessionRow };
839
844
  db.prepare(`UPDATE sessions
840
845
  SET status = COALESCE(?, status),
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves",
3
- "version": "0.1.0-beta.297",
3
+ "version": "0.1.0-beta.299",
4
4
  "description": "Remote controller for local coding agents with hosted or self-hosted relay support.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -7,6 +7,7 @@ import { sendAndWaitForDirectoryListing, sendAndWaitForFilePreview, sendAndWaitF
7
7
  import { DEFAULT_SESSION_CREATE_WAIT_MS, } from "./types.js";
8
8
  import { isWaitCondition, matchesWaitCondition, parseEventCursor, parseEventLimit, parsePositiveDuration, requireNonEmpty, sleep, } from "./validation.js";
9
9
  const DEFAULT_BLOCKING_RUN_WAIT_MS = 30 * 60 * 1000;
10
+ const RESUME_COMMAND_TIMEOUT_MS = 60_000;
10
11
  function addRuntimeSessionId(ids, value) {
11
12
  if (typeof value === "string" && value.trim())
12
13
  ids.add(value.trim());
@@ -368,7 +369,7 @@ export class ControllerClient {
368
369
  requestId,
369
370
  sessionId: checkedSessionId,
370
371
  ...(options.syncDirection ? { syncDirection: options.syncDirection } : {}),
371
- });
372
+ }, false, RESUME_COMMAND_TIMEOUT_MS);
372
373
  }
373
374
  async listHistoricalSessionsPage(input) {
374
375
  const machineId = requireNonEmpty(input.machineId, "machineId");