@cydm/happy-elves 0.1.0-beta.83 → 0.1.0-beta.85

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.
Files changed (68) hide show
  1. package/apps/cli/dist/commands/lib/args.js +4 -0
  2. package/apps/cli/dist/commands/lib/gateway-store.d.ts +0 -4
  3. package/apps/cli/dist/commands/lib/gateway-store.js +0 -8
  4. package/apps/cli/dist/commands/lib/index.d.ts +0 -1
  5. package/apps/cli/dist/commands/lib/index.js +0 -1
  6. package/apps/cli/dist/commands/lib/paths.d.ts +0 -1
  7. package/apps/cli/dist/commands/lib/paths.js +0 -1
  8. package/apps/cli/dist/commands/lib/session-output.d.ts +12 -0
  9. package/apps/cli/dist/commands/lib/session-output.js +49 -0
  10. package/apps/cli/dist/commands/lib/usage.js +4 -2
  11. package/apps/cli/dist/commands/memory.js +48 -1
  12. package/apps/cli/dist/commands/schedule.js +5 -2
  13. package/apps/cli/dist/commands/turn.js +147 -0
  14. package/apps/cli/dist/commands/workspace.js +10 -2
  15. package/apps/cli/package.json +1 -1
  16. package/apps/daemon/dist/gateway/adapters.d.ts +2 -1
  17. package/apps/daemon/dist/gateway/feishu.d.ts +0 -4
  18. package/apps/daemon/dist/gateway/feishu.js +1 -1
  19. package/apps/daemon/dist/gateway/runtime.d.ts +0 -5
  20. package/apps/daemon/dist/gateway/runtime.js +1 -4
  21. package/apps/daemon/dist/gateway/store.d.ts +2 -1
  22. package/apps/daemon/dist/gateway/store.js +146 -3
  23. package/apps/daemon/dist/memory/handler.js +15 -5
  24. package/apps/daemon/dist/memory/manager.d.ts +7 -4
  25. package/apps/daemon/dist/memory/manager.js +51 -11
  26. package/apps/daemon/dist/relay/connection.js +5 -0
  27. package/apps/daemon/dist/relay/send.js +1 -0
  28. package/apps/daemon/dist/runtime/external-provider.js +119 -0
  29. package/apps/daemon/dist/schedule/handler.js +12 -5
  30. package/apps/daemon/dist/schedule/runner.d.ts +9 -1
  31. package/apps/daemon/dist/schedule/runner.js +84 -4
  32. package/apps/daemon/dist/schedule/store.d.ts +1 -2
  33. package/apps/daemon/dist/schedule/store.js +7 -1
  34. package/apps/daemon/dist/session/events.d.ts +1 -1
  35. package/apps/daemon/dist/session/events.js +1 -1
  36. package/apps/daemon/dist/session/prompt.js +18 -0
  37. package/apps/daemon/dist/session/reconcile.d.ts +3 -0
  38. package/apps/daemon/dist/session/reconcile.js +235 -0
  39. package/apps/daemon/dist/turn-coordinator.d.ts +1 -0
  40. package/apps/daemon/dist/turn-coordinator.js +4 -0
  41. package/apps/daemon/package.json +1 -1
  42. package/apps/relay/dist/controller-handlers.d.ts +9 -0
  43. package/apps/relay/dist/controller-handlers.js +53 -3
  44. package/apps/relay/dist/db.js +42 -2
  45. package/apps/relay/dist/http-routes.js +10 -4
  46. package/apps/relay/dist/http-schemas.d.ts +5 -0
  47. package/apps/relay/dist/http-schemas.js +1 -0
  48. package/apps/relay/dist/machine-handlers.js +58 -0
  49. package/package.json +2 -1
  50. package/packages/agent-sdk/dist/index.d.ts +31 -0
  51. package/packages/agent-sdk/dist/index.js +4 -0
  52. package/packages/client/dist/account.js +1 -0
  53. package/packages/client/dist/client.d.ts +2 -1
  54. package/packages/client/dist/client.js +13 -1
  55. package/packages/client/dist/parsers.js +4 -0
  56. package/packages/client/dist/transport.d.ts +5 -0
  57. package/packages/client/dist/transport.js +7 -0
  58. package/packages/client/dist/types.d.ts +11 -2
  59. package/packages/runtime/dist/index.d.ts +26 -0
  60. package/packages/runtime-acpx/dist/index.js +8 -0
  61. package/packages/runtime-cli/dist/index.js +77 -0
  62. package/packages/shared/dist/protocol-schemas.d.ts +96 -3
  63. package/packages/shared/dist/protocol-schemas.js +35 -1
  64. package/packages/shared/dist/protocol-types.d.ts +36 -1
  65. package/packages/shared/dist/protocol.d.ts +29 -4
  66. package/packages/shared/dist/protocol.js +32 -3
  67. package/apps/cli/dist/commands/lib/schedule-store.d.ts +0 -28
  68. package/apps/cli/dist/commands/lib/schedule-store.js +0 -32
@@ -52,6 +52,8 @@ const valueFlags = new Set([
52
52
  "runtime-session",
53
53
  "shell",
54
54
  "actions",
55
+ "allow-chat",
56
+ "allow-sender",
55
57
  "expires-in",
56
58
  "format",
57
59
  "loops",
@@ -60,6 +62,7 @@ const valueFlags = new Set([
60
62
  "session",
61
63
  "secret",
62
64
  "since",
65
+ "source",
63
66
  "stop-on",
64
67
  "tail",
65
68
  "tags",
@@ -83,6 +86,7 @@ const booleanFlags = new Set([
83
86
  "all",
84
87
  "allow-any-sender",
85
88
  "app-secret-stdin",
89
+ "confirm-risk",
86
90
  "detach",
87
91
  "disable-reaction",
88
92
  "help",
@@ -36,7 +36,3 @@ export type GatewayStore = {
36
36
  };
37
37
  export declare function readGatewayStore(): Promise<GatewayStore>;
38
38
  export declare function updateGatewayStore(update: (store: GatewayStore) => GatewayStore | Promise<GatewayStore>): Promise<GatewayStore>;
39
- export declare function redactedGatewayChannel(channel: GatewayChannelConfig): Omit<GatewayChannelConfig, "localToken" | "localSecret"> & {
40
- hasLocalToken: boolean;
41
- hasLocalSecret: boolean;
42
- };
@@ -30,12 +30,4 @@ export async function updateGatewayStore(update) {
30
30
  await writeGatewayStore(next);
31
31
  return next;
32
32
  }
33
- export function redactedGatewayChannel(channel) {
34
- const { localToken, localSecret, ...rest } = channel;
35
- return {
36
- ...rest,
37
- hasLocalToken: Boolean(localToken),
38
- hasLocalSecret: Boolean(localSecret),
39
- };
40
- }
41
33
  //# sourceMappingURL=gateway-store.js.map
@@ -17,7 +17,6 @@ export * from "./loop-store.js";
17
17
  export * from "./paths.js";
18
18
  export * from "./relay-db-audit.js";
19
19
  export * from "./relay-http.js";
20
- export * from "./schedule-store.js";
21
20
  export * from "./scope.js";
22
21
  export * from "./session-output.js";
23
22
  export * from "./session-view.js";
@@ -17,7 +17,6 @@ export * from "./loop-store.js";
17
17
  export * from "./paths.js";
18
18
  export * from "./relay-db-audit.js";
19
19
  export * from "./relay-http.js";
20
- export * from "./schedule-store.js";
21
20
  export * from "./scope.js";
22
21
  export * from "./session-output.js";
23
22
  export * from "./session-view.js";
@@ -8,4 +8,3 @@ export declare const daemonPidPath: string;
8
8
  export declare const loopStorePath: string;
9
9
  export declare const loopStoreLockPath: string;
10
10
  export declare const gatewayStorePath: string;
11
- export declare const scheduleStorePath: string;
@@ -12,5 +12,4 @@ export const daemonPidPath = path.join(configDir, "daemon.pid");
12
12
  export const loopStorePath = path.join(configDir, "loops.json");
13
13
  export const loopStoreLockPath = path.join(configDir, "loops.json.lock");
14
14
  export const gatewayStorePath = path.join(configDir, "gateway.json");
15
- export const scheduleStorePath = path.join(configDir, "schedules.json");
16
15
  //# sourceMappingURL=paths.js.map
@@ -1,4 +1,5 @@
1
1
  import type { ControllerClient, DecodedSessionEvent } from "../../../../../packages/client/dist/index.js";
2
+ import type { MemoryCitation, MemoryLayer } from "../../../../../packages/shared/dist/index.js";
2
3
  export declare function compactText(value: string, max?: number): string;
3
4
  export declare function lastAssistantOutput(events: DecodedSessionEvent[]): string;
4
5
  export declare function compareSessionEventLogicalOrder(left: DecodedSessionEvent, right: DecodedSessionEvent): number;
@@ -14,6 +15,14 @@ export type StoredTurnOutput = {
14
15
  startedAt?: number;
15
16
  completedAt: number;
16
17
  eventCount: number;
18
+ usedMemories?: UsedMemoryReference[];
19
+ };
20
+ type UsedMemoryReference = {
21
+ id: string;
22
+ layer: MemoryLayer;
23
+ path: string;
24
+ title?: string;
25
+ citation: MemoryCitation;
17
26
  };
18
27
  export type TurnSummary = {
19
28
  turnId: string;
@@ -30,6 +39,7 @@ export type TurnSummary = {
30
39
  firstEventId: number;
31
40
  lastEventId: number;
32
41
  eventCount: number;
42
+ usedMemories?: UsedMemoryReference[];
33
43
  };
34
44
  export type StructuredCommandOutput = {
35
45
  state: string;
@@ -43,6 +53,7 @@ export type StructuredCommandOutput = {
43
53
  latest?: string;
44
54
  metadata?: string;
45
55
  };
56
+ usedMemories?: UsedMemoryReference[];
46
57
  };
47
58
  export declare function writeTurnOutput(sessionId: string, turnId: string, events: DecodedSessionEvent[]): Promise<StoredTurnOutput>;
48
59
  export declare function readStoredTurnOutput(sessionId: string, turnId?: string): Promise<StoredTurnOutput | undefined>;
@@ -59,3 +70,4 @@ export declare function conciseEvents(events: DecodedSessionEvent[]): Array<{
59
70
  }>;
60
71
  export declare function printConciseEvents(events: DecodedSessionEvent[]): void;
61
72
  export declare function readPromptTextFile(filePath: string): Promise<string>;
73
+ export {};
@@ -48,6 +48,45 @@ function assistantOutputForTurn(events, turnId) {
48
48
  .join("")
49
49
  .trim();
50
50
  }
51
+ function usedMemoriesForTurn(events, turnId) {
52
+ return dedupeUsedMemories(events
53
+ .filter((event) => event.turnId === turnId && event.decoded?.type === "memory_used")
54
+ .flatMap((event) => event.decoded?.type === "memory_used" ? event.decoded.memories : []));
55
+ }
56
+ function dedupeUsedMemories(memories) {
57
+ const seen = new Set();
58
+ const result = [];
59
+ for (const memory of memories) {
60
+ const key = [
61
+ memory.id,
62
+ memory.path,
63
+ memory.citation.path,
64
+ memory.citation.startLine,
65
+ memory.citation.endLine,
66
+ ].join(":");
67
+ if (seen.has(key))
68
+ continue;
69
+ seen.add(key);
70
+ result.push(memory);
71
+ }
72
+ return result;
73
+ }
74
+ function isUsedMemoryArray(value) {
75
+ return Array.isArray(value) && value.every((item) => {
76
+ if (!item || typeof item !== "object" || Array.isArray(item))
77
+ return false;
78
+ const record = item;
79
+ const citation = record.citation;
80
+ return typeof record.id === "string" &&
81
+ (record.layer === "user" || record.layer === "curated" || record.layer === "daily") &&
82
+ typeof record.path === "string" &&
83
+ (!record.title || typeof record.title === "string") &&
84
+ Boolean(citation) &&
85
+ typeof citation?.path === "string" &&
86
+ typeof citation?.startLine === "number" &&
87
+ typeof citation?.endLine === "number";
88
+ });
89
+ }
51
90
  function sessionEventLogicalOrder(event) {
52
91
  const messageKey = event.messageId ?? "";
53
92
  if (event.occurredAt !== undefined) {
@@ -99,6 +138,7 @@ export async function writeTurnOutput(sessionId, turnId, events) {
99
138
  await fs.mkdir(paths.dir, { recursive: true });
100
139
  const output = assistantOutputForTurn(events, turnId) || terminalTextForDone(events, turnId);
101
140
  const turnEvents = events.filter((event) => event.turnId === turnId);
141
+ const usedMemories = usedMemoriesForTurn(events, turnId);
102
142
  const doneEvent = [...turnEvents].reverse().find((event) => event.decoded?.type === "done");
103
143
  const metadata = {
104
144
  sessionId,
@@ -107,6 +147,7 @@ export async function writeTurnOutput(sessionId, turnId, events) {
107
147
  startedAt: turnEvents[0]?.createdAt,
108
148
  completedAt: doneEvent?.createdAt ?? Date.now(),
109
149
  eventCount: turnEvents.length,
150
+ ...(usedMemories.length > 0 ? { usedMemories } : {}),
110
151
  outputPath: paths.turn,
111
152
  latestPath: paths.latest,
112
153
  };
@@ -158,6 +199,7 @@ export async function readStoredTurnOutput(sessionId, turnId) {
158
199
  startedAt: typeof metadata.startedAt === "number" ? metadata.startedAt : undefined,
159
200
  completedAt: typeof metadata.completedAt === "number" ? metadata.completedAt : 0,
160
201
  eventCount: typeof metadata.eventCount === "number" ? metadata.eventCount : 0,
202
+ ...(isUsedMemoryArray(metadata.usedMemories) ? { usedMemories: metadata.usedMemories } : {}),
161
203
  };
162
204
  }
163
205
  export function summarizeTurns(events) {
@@ -177,6 +219,7 @@ export function summarizeTurns(events) {
177
219
  firstEventId: event.id,
178
220
  lastEventId: event.id,
179
221
  eventCount: 0,
222
+ usedMemories: [],
180
223
  order,
181
224
  };
182
225
  current.startedAt ??= event.createdAt;
@@ -200,6 +243,9 @@ export function summarizeTurns(events) {
200
243
  current.checkpointId = payload.currentHead;
201
244
  current.runtimeTurnId = payload.lastTurnId;
202
245
  }
246
+ else if (payload?.type === "memory_used") {
247
+ current.usedMemories = dedupeUsedMemories([...current.usedMemories, ...payload.memories]);
248
+ }
203
249
  turns.set(event.turnId, current);
204
250
  }
205
251
  return [...turns.values()]
@@ -218,6 +264,7 @@ export function summarizeTurns(events) {
218
264
  firstEventId: turn.firstEventId,
219
265
  lastEventId: turn.lastEventId,
220
266
  eventCount: turn.eventCount,
267
+ ...(turn.usedMemories.length > 0 ? { usedMemories: turn.usedMemories } : {}),
221
268
  order: turn.order,
222
269
  }))
223
270
  .sort((left, right) => right.order.time - left.order.time ||
@@ -236,6 +283,7 @@ export function structuredOutputForTurn(sessionId, turn) {
236
283
  status: turn.status,
237
284
  finalOutput,
238
285
  ...(turn.status === "failed" && finalOutput ? { error: finalOutput } : {}),
286
+ ...(turn.usedMemories?.length ? { usedMemories: turn.usedMemories } : {}),
239
287
  };
240
288
  }
241
289
  export function structuredOutputForStoredTurn(output) {
@@ -246,6 +294,7 @@ export function structuredOutputForStoredTurn(output) {
246
294
  status: output.status,
247
295
  finalOutput: output.output || undefined,
248
296
  ...(output.status === "failed" && output.output ? { error: output.output } : {}),
297
+ ...(output.usedMemories?.length ? { usedMemories: output.usedMemories } : {}),
249
298
  artifactPaths: {
250
299
  output: output.outputPath,
251
300
  latest: output.latestPath,
@@ -74,7 +74,7 @@ Session management:
74
74
  happy-elves turn list <sessionId> [--limit 10] [--verbose] [--json]
75
75
  happy-elves turn status <sessionId> <turnId> [--verbose] [--json]
76
76
  happy-elves turn result <sessionId> [<turnId>] [--verbose] [--json]
77
- happy-elves turn run <sessionId> (--text <prompt> | --text-file <path>) [--detach] [--timeout 1000s] [--inactivity-timeout 1000s] [--permission-mode approve-reads|approve-all|deny-all] [--json]
77
+ happy-elves turn run <sessionId> (--text <prompt> | --text-file <path>) [--detach] [--timeout 1000s] [--inactivity-timeout 1000s] [--permission-mode approve-reads|approve-all|deny-all] [--memory off|readonly|readwrite] [--memory-project <key>] [--memory-top-n 3] [--json]
78
78
  happy-elves turn wait <sessionId> <turnId> [--timeout 1000s] --json
79
79
  happy-elves turn cancel <sessionId> <turnId> [--reason <reason>] --json
80
80
  happy-elves turn rewind <sessionId> <turn-or-checkpoint-id> --json
@@ -108,7 +108,7 @@ session stars, recent sessions, pinned workspaces, and current project defaults.
108
108
  `,
109
109
  gateway: `Usage:
110
110
  happy-elves gateway enable [<channelId>] --channel lark --machine <machineId> (--cwd <cwd> | --session <sessionId>) [--agent codex] [--model <model>] --json
111
- happy-elves gateway enable lark-main --channel lark --machine <machineId> (--cwd <cwd> | --session <sessionId>) --app-id <appId> --app-secret-stdin [--domain feishu|lark] [--inbound mention|all] --json
111
+ happy-elves gateway enable lark-main --channel lark --machine <machineId> (--cwd <cwd> | --session <sessionId>) --app-id <appId> --app-secret-stdin [--domain feishu|lark] [--inbound mention|all] (--allow-any-sender | --allow-chat <id[,id]> | --allow-sender <id[,id]>) --json
112
112
  happy-elves gateway disable [<channelId>] [--machine <machineId>] --json
113
113
  happy-elves gateway status [--machine <machineId>] [--include-debug] [--json]
114
114
  happy-elves gateway test [<channelId>] [--machine <machineId>] --conversation <externalConversationId> --message-id <externalMessageId> --text <prompt> --json
@@ -116,6 +116,7 @@ session stars, recent sessions, pinned workspaces, and current project defaults.
116
116
 
117
117
  Gateway v1 official Feishu/Lark path is daemon-backed SDK long connection.
118
118
  App secrets are read from stdin and stored only in the target daemon config.
119
+ Lark/Feishu product gateways require an explicit inbound sender/chat policy.
119
120
  gateway serve is kept for fake/local debugging only; lark-cli is not a product dependency.
120
121
  `,
121
122
  memory: `Usage:
@@ -123,6 +124,7 @@ gateway serve is kept for fake/local debugging only; lark-cli is not a product d
123
124
  happy-elves memory save [--machine <machineId>] [--project <projectKey>] [--layer user|curated|daily] (--text <text> | --text-file <path>) [--title <title>] [--date YYYY-MM-DD] --json
124
125
  happy-elves memory read <USER.md|MEMORY.md|memory/YYYY-MM-DD.md> [--project <projectKey>] --json
125
126
  happy-elves memory search --query <text> [--project <projectKey>] [--limit 5] --json
127
+ happy-elves memory promote --source <path:start-end[@contentHash]> --layer user|curated [--project <projectKey>] [--title <title>] --json
126
128
  happy-elves memory reindex [--project <projectKey>] --json
127
129
  happy-elves memory sync [--project <projectKey>] --json
128
130
 
@@ -72,7 +72,9 @@ export async function handleMemory({ domain, action, positional, flags }) {
72
72
  console.log("No matching memory.");
73
73
  else
74
74
  for (const item of results) {
75
- console.log(`${item.path}:${item.citation.startLine}-${item.citation.endLine} ${item.snippet}`);
75
+ const hash = item.citation.contentHash ? ` hash=${item.citation.contentHash.slice(0, 10)}` : "";
76
+ const stale = item.citation.stale ? " stale" : "";
77
+ console.log(`${item.path}:${item.citation.startLine}-${item.citation.endLine}${hash}${stale} ${item.snippet}`);
76
78
  if (wantsVerbose(flags))
77
79
  console.log(` score=${item.score}`);
78
80
  }
@@ -81,6 +83,27 @@ export async function handleMemory({ domain, action, positional, flags }) {
81
83
  ok("memory.search", result);
82
84
  return true;
83
85
  }
86
+ if (action === "promote") {
87
+ const machineId = await resolveMemoryMachineId(client, flags);
88
+ const projectKey = await resolveMemoryProjectKey(client, flags, machineId);
89
+ const source = typeof flags.source === "string" ? parseMemoryCitation(flags.source) : undefined;
90
+ if (!source)
91
+ throw new CliError("Missing --source <path:start-end>", "MISSING_ARGUMENT");
92
+ const targetLayer = parsePromotionLayer(flags.layer) ?? "curated";
93
+ const result = await client.memory({
94
+ machineId,
95
+ action: "promote",
96
+ projectKey,
97
+ promote: {
98
+ projectKey,
99
+ source,
100
+ targetLayer,
101
+ ...(typeof flags.title === "string" ? { title: flags.title } : {}),
102
+ },
103
+ });
104
+ ok("memory.promote", result);
105
+ return true;
106
+ }
84
107
  if (action === "reindex" || action === "sync") {
85
108
  const machineId = await resolveMemoryMachineId(client, flags);
86
109
  const projectKey = await resolveMemoryProjectKey(client, flags, machineId);
@@ -128,4 +151,28 @@ function parseMemoryMode(value) {
128
151
  return value;
129
152
  throw new CliError("Invalid --mode; expected off|readonly|readwrite", "INVALID_ARGUMENT");
130
153
  }
154
+ function parsePromotionLayer(value) {
155
+ const layer = parseMemoryLayer(value);
156
+ if (!layer)
157
+ return undefined;
158
+ if (layer === "user" || layer === "curated")
159
+ return layer;
160
+ throw new CliError("Invalid --layer for promotion; expected user|curated", "INVALID_ARGUMENT");
161
+ }
162
+ function parseMemoryCitation(value) {
163
+ const match = value.match(/^(.+):(\d+)-(\d+)(?:@([A-Za-z0-9._-]+))?$/);
164
+ if (!match)
165
+ throw new CliError("Invalid --source; expected path:start-end or path:start-end@contentHash", "INVALID_ARGUMENT");
166
+ const startLine = Number(match[2]);
167
+ const endLine = Number(match[3]);
168
+ if (!Number.isInteger(startLine) || !Number.isInteger(endLine) || startLine < 1 || endLine < startLine) {
169
+ throw new CliError("Invalid --source line range", "INVALID_ARGUMENT");
170
+ }
171
+ return {
172
+ path: match[1],
173
+ startLine,
174
+ endLine,
175
+ ...(match[4] ? { contentHash: match[4] } : {}),
176
+ };
177
+ }
131
178
  //# sourceMappingURL=memory.js.map
@@ -48,8 +48,10 @@ export async function handleSchedule({ domain, action, positional, flags }) {
48
48
  if (schedules.length === 0)
49
49
  console.log("No schedules.");
50
50
  else
51
- for (const schedule of schedules)
52
- console.log(`${schedule.id} ${schedule.status} ${cadenceLabel(schedule)} next:${schedule.nextRunAt ?? "manual"} source:${schedule.sourceSessionId ?? schedule.sessionId ?? "pending"}`);
51
+ for (const schedule of schedules) {
52
+ const preview = schedule.previewRunAt?.length ? ` preview:${schedule.previewRunAt.join(",")}` : "";
53
+ console.log(`${schedule.id} ${schedule.status} ${cadenceLabel(schedule)} next:${schedule.nextRunAt ?? "manual"} source:${schedule.sourceSessionId ?? schedule.sessionId ?? "pending"}${preview}`);
54
+ }
53
55
  return true;
54
56
  }
55
57
  ok("schedule.list", { machineId, schedules: schedules.map(compactSchedule) });
@@ -216,6 +218,7 @@ function compactSchedule(schedule) {
216
218
  stopOn: schedule.stopOn,
217
219
  dueAt: schedule.dueAt,
218
220
  nextRunAt: schedule.nextRunAt,
221
+ previewRunAt: schedule.previewRunAt,
219
222
  status: schedule.status,
220
223
  runCount: schedule.runCount ?? schedule.runs.length,
221
224
  lastRun: schedule.lastRun ?? schedule.runs[0],
@@ -70,6 +70,42 @@ function findTurnSummaryByAnyId(turns, id) {
70
70
  turn.runtimeTurnId === id ||
71
71
  turn.checkpointId === id);
72
72
  }
73
+ function recoveryPromptWithContext(turns, inputTurnId, continuation) {
74
+ const chronological = [...turns].reverse();
75
+ const selected = [];
76
+ for (const turn of chronological) {
77
+ if (inputTurnId && (turn.turnId === inputTurnId || turn.runtimeTurnId === inputTurnId || turn.checkpointId === inputTurnId))
78
+ break;
79
+ if (turn.status === "running")
80
+ break;
81
+ selected.push(turn);
82
+ }
83
+ const blocks = selected.map((turn) => {
84
+ const lines = [`Turn ${turn.turnId} (${turn.status})`];
85
+ if (turn.prompt.trim())
86
+ lines.push(`User:\n${turn.prompt.trim()}`);
87
+ const output = (turn.output || turn.terminalText).trim();
88
+ if (output)
89
+ lines.push(`Assistant:\n${output}`);
90
+ return lines.join("\n");
91
+ });
92
+ const context = clampEnd(blocks.join("\n\n---\n\n"), 32 * 1024);
93
+ if (!context.trim())
94
+ return continuation;
95
+ return [
96
+ "Recovered context from the original Happy Elves session. The previous turn could not be confirmed, so do not assume it completed and do not replay its original prompt.",
97
+ "",
98
+ context,
99
+ "",
100
+ "User continuation:",
101
+ continuation,
102
+ ].join("\n");
103
+ }
104
+ function clampEnd(value, maxChars) {
105
+ if (value.length <= maxChars)
106
+ return value;
107
+ return value.slice(value.length - maxChars);
108
+ }
73
109
  async function waitForTurn(client, sessionId, turnId, timeoutMs) {
74
110
  const startedAt = Date.now();
75
111
  while (true) {
@@ -127,6 +163,117 @@ export async function handleTurn({ domain, action, positional, flags }) {
127
163
  ok("turn.status", wantsVerbose(flags) ? { sessionId, inputTurnId: turnId, turn } : { sessionId, inputTurnId: turnId, ...compactTurn(turn) }, { sessionId, turnId: turn.turnId });
128
164
  return true;
129
165
  }
166
+ if (action === "reconcile") {
167
+ const sessionId = requirePositional(positional[0], "sessionId");
168
+ const turnId = positional[1] ?? (typeof flags.turn === "string" ? flags.turn : undefined);
169
+ if (turnId)
170
+ rejectEventPrimitiveId(turnId);
171
+ const result = await client.reconcileTurn(sessionId, turnId);
172
+ if (wantsJson(flags)) {
173
+ ok("turn.reconcile", result, { requestId: result.requestId, sessionId, turnId: result.turnId ?? turnId });
174
+ }
175
+ else {
176
+ console.log(`Reconcile: ${result.status}${result.terminalStatus ? ` (${result.terminalStatus})` : ""}`);
177
+ if (result.turnId)
178
+ console.log(`Turn: ${result.turnId}`);
179
+ if (result.providerTurnId)
180
+ console.log(`Provider turn: ${result.providerTurnId}`);
181
+ if (result.reason)
182
+ console.log(`Reason: ${result.reason}`);
183
+ if (result.status !== "terminal" && result.status !== "active") {
184
+ console.log("Unable to confirm whether the previous execution finished. Use turn recover with --confirm-risk before sending a continuation.");
185
+ }
186
+ }
187
+ return true;
188
+ }
189
+ if (action === "recover") {
190
+ const sessionId = requirePositional(positional[0], "sessionId");
191
+ const inputTurnId = positional[1] ?? (typeof flags.turn === "string" ? flags.turn : undefined);
192
+ if (inputTurnId)
193
+ rejectEventPrimitiveId(inputTurnId);
194
+ if (flags["confirm-risk"] !== true) {
195
+ throw new CliError("Recovery can duplicate external side effects if the original turn later completes. Re-run with --confirm-risk to create a Recovery Session.", "RECOVERY_CONFIRMATION_REQUIRED");
196
+ }
197
+ const continuation = typeof flags.text === "string"
198
+ ? flags.text
199
+ : typeof flags["text-file"] === "string"
200
+ ? await readPromptTextFile(flags["text-file"])
201
+ : "";
202
+ if (!continuation.trim())
203
+ throw new CliError("Missing --text or --text-file", "MISSING_ARGUMENT");
204
+ const sourceSession = await requireProjectedSession(client, sessionId, "turn.recover");
205
+ const sourceMetadata = await client.decodeSessionMetadata(sourceSession);
206
+ const sourceName = sourceMetadata.name ?? sourceSession.name;
207
+ const recoveryName = `${sourceName} Recovery`;
208
+ const turns = summarizeTurns(await client.history(sessionId, 1000));
209
+ const targetTurn = inputTurnId ? findTurnSummaryByAnyId(turns, inputTurnId) : turns[0];
210
+ let recoverySessionId;
211
+ let recoveryMode = "new-session-context";
212
+ let forkError;
213
+ const checkpoint = targetTurn?.checkpointId && !targetTurn.checkpointId.startsWith("event:") ? targetTurn.checkpointId : undefined;
214
+ if (checkpoint) {
215
+ try {
216
+ const fork = await client.fork(sessionId, { from: checkpoint, name: recoveryName });
217
+ recoverySessionId = fork.sessionId;
218
+ recoveryMode = "checkpoint-fork";
219
+ }
220
+ catch (error) {
221
+ forkError = error instanceof Error ? error.message : String(error);
222
+ }
223
+ }
224
+ let promptText = continuation;
225
+ if (!recoverySessionId) {
226
+ const created = await client.createSession({
227
+ machineId: sourceSession.machineId,
228
+ agent: sourceMetadata.agent ?? sourceSession.agent,
229
+ cwd: sourceMetadata.cwd ?? sourceSession.cwd,
230
+ name: recoveryName,
231
+ createdFrom: "turn.recover",
232
+ permissionMode: parsePermissionMode(flags["permission-mode"] ?? flags.permission),
233
+ });
234
+ if (!created.sessionId)
235
+ throw new CliError("Recovery session create was acknowledged without sessionId", "RELAY_RESPONSE_INVALID");
236
+ recoverySessionId = created.sessionId;
237
+ promptText = recoveryPromptWithContext(turns, inputTurnId, continuation);
238
+ }
239
+ await client.wait(recoverySessionId, { timeoutMs: 120_000 });
240
+ const permissionMode = parsePermissionMode(flags["permission-mode"] ?? flags.permission);
241
+ const wait = flags.detach !== true && flags["no-wait"] !== true;
242
+ const memoryMode = parseMemoryModeFlag(flags["memory-mode"] ?? flags.memory) ?? "readonly";
243
+ const recoverySession = await client.getSession(recoverySessionId);
244
+ const memoryProject = typeof flags["memory-project"] === "string"
245
+ ? flags["memory-project"]
246
+ : recoverySession
247
+ ? `${recoverySession.machineId}:${recoverySession.cwd}`
248
+ : undefined;
249
+ const result = await client.run(recoverySessionId, promptText, {
250
+ wait,
251
+ permissionMode,
252
+ memoryMode,
253
+ ...(memoryProject ? { memoryProjectKey: memoryProject } : {}),
254
+ memoryTopN: 3,
255
+ waitTimeoutMs: parseDurationMs(flags.timeout, DEFAULT_COMMAND_TIMEOUT_MS),
256
+ ackTimeoutMs: 15_000,
257
+ });
258
+ if (wantsJson(flags)) {
259
+ ok("turn.recover", {
260
+ sourceSessionId: sessionId,
261
+ sourceTurnId: targetTurn?.turnId ?? inputTurnId,
262
+ recoverySessionId,
263
+ recoveryTurnId: result.turnId,
264
+ recoveryMode,
265
+ ...(forkError ? { forkError } : {}),
266
+ }, { requestId: result.requestId, sessionId: recoverySessionId, turnId: result.turnId });
267
+ }
268
+ else {
269
+ console.log(`Recovery session: ${recoverySessionId}`);
270
+ console.log(`Recovery mode: ${recoveryMode}`);
271
+ if (forkError)
272
+ console.log(`Checkpoint fork fallback: ${forkError}`);
273
+ console.log(`Turn: ${result.turnId}`);
274
+ }
275
+ return true;
276
+ }
130
277
  if (action === "run") {
131
278
  const sessionId = requirePositional(positional[0], "sessionId");
132
279
  const text = typeof flags.text === "string"
@@ -1,4 +1,4 @@
1
- import { ControllerClient, ok, readConfig, requirePositional, requireString, wantsJson } from "./lib/index.js";
1
+ import { CliError, ControllerClient, ok, readConfig, requirePositional, requireString, wantsJson } from "./lib/index.js";
2
2
  export async function handleWorkspace({ domain, action, positional, flags }) {
3
3
  if (domain !== "workspace")
4
4
  return false;
@@ -49,9 +49,17 @@ export async function handleWorkspace({ domain, action, positional, flags }) {
49
49
  const machineId = typeof flags.machine === "string" ? flags.machine : undefined;
50
50
  const cwd = typeof flags.cwd === "string" ? flags.cwd : undefined;
51
51
  const agent = typeof flags.agent === "string" ? flags.agent : undefined;
52
- ok("workspace.prefer", await client.setWorkspaceProjectPreference({ projectKey, machineId, cwd, agent }));
52
+ const memoryMode = parseMemoryMode(flags["memory-mode"] ?? flags.memory);
53
+ ok("workspace.prefer", await client.setWorkspaceProjectPreference({ projectKey, machineId, cwd, agent, ...(memoryMode ? { memoryMode } : {}) }));
53
54
  return true;
54
55
  }
55
56
  return false;
56
57
  }
58
+ function parseMemoryMode(value) {
59
+ if (value === undefined || value === false)
60
+ return undefined;
61
+ if (value === "off" || value === "readonly" || value === "readwrite")
62
+ return value;
63
+ throw new CliError("Invalid --memory-mode; expected off|readonly|readwrite", "INVALID_ARGUMENT");
64
+ }
57
65
  //# sourceMappingURL=workspace.js.map
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cydm/happy-elves-cli",
3
- "version": "0.1.0-beta.83",
3
+ "version": "0.1.0-beta.85",
4
4
  "private": true,
5
5
  "type": "module"
6
6
  }
@@ -14,7 +14,7 @@ export type GatewayInboundResult = {
14
14
  duplicate?: boolean;
15
15
  queued?: boolean;
16
16
  };
17
- export type GatewayInboundSender = (input: {
17
+ type GatewayInboundSender = (input: {
18
18
  gatewayId: string;
19
19
  platform: "feishu" | "fake";
20
20
  platformEventId: string;
@@ -35,3 +35,4 @@ export type GatewayAdapterContext = {
35
35
  readChannel: () => Promise<GatewayChannelConfig | undefined>;
36
36
  sendInbound: GatewayInboundSender;
37
37
  };
38
+ export {};
@@ -56,10 +56,6 @@ export declare function createFeishuProcessingReactionTracker(client: LarkClient
56
56
  clear(): Promise<void>;
57
57
  pendingCount(): number;
58
58
  };
59
- export declare function createFeishuBotOpenIdResolver(client: LarkClient): {
60
- get(): Promise<FeishuBotIdentity | undefined>;
61
- cached(): FeishuBotIdentity | undefined;
62
- };
63
59
  export declare function normalizeFeishuMessage(data: unknown): NormalizedFeishuInbound | undefined;
64
60
  export declare function shouldHandleFeishuInbound(profile: {
65
61
  inboundMode: "mention" | "all";
@@ -144,7 +144,7 @@ export function createFeishuProcessingReactionTracker(client, input = {}) {
144
144
  }
145
145
  return { start, complete, clear, pendingCount: () => pending.size };
146
146
  }
147
- export function createFeishuBotOpenIdResolver(client) {
147
+ function createFeishuBotOpenIdResolver(client) {
148
148
  let cached;
149
149
  let inflight;
150
150
  async function get() {
@@ -18,11 +18,6 @@ type GatewayTerminalInput = {
18
18
  error?: string;
19
19
  outputPreview?: string;
20
20
  };
21
- export declare function gatewayRuntimeIsListening(): boolean;
22
- export declare function gatewayRuntimeEndpoint(): {
23
- host: string;
24
- port: number;
25
- };
26
21
  export declare function startGatewayRuntime(config: DaemonConfig): Promise<void>;
27
22
  export declare function stopGatewayRuntime(): Promise<void>;
28
23
  export declare function syncGatewayRuntime(config?: DaemonConfig | undefined): Promise<void>;
@@ -10,10 +10,7 @@ let gatewayServer;
10
10
  let gatewayRuntimeConfig;
11
11
  const gatewayAdapters = new Map();
12
12
  let gatewaySyncChain = Promise.resolve();
13
- export function gatewayRuntimeIsListening() {
14
- return Boolean(gatewayServer?.listening) || gatewayAdapters.size > 0;
15
- }
16
- export function gatewayRuntimeEndpoint() {
13
+ function gatewayRuntimeEndpoint() {
17
14
  return {
18
15
  host: process.env.HAPPY_ELVES_GATEWAY_HOST || "127.0.0.1",
19
16
  port: parseGatewayPort(process.env.HAPPY_ELVES_GATEWAY_PORT),
@@ -66,7 +66,7 @@ export type GatewayPendingReply = {
66
66
  createdAt: string;
67
67
  updatedAt: string;
68
68
  };
69
- export type GatewayReplyHistory = {
69
+ type GatewayReplyHistory = {
70
70
  channelId: string;
71
71
  externalConversationId: string;
72
72
  externalMessageId: string;
@@ -89,3 +89,4 @@ export declare function readGatewayStore(): Promise<GatewayStore>;
89
89
  export declare function updateGatewayStore(update: (store: GatewayStore) => GatewayStore | Promise<GatewayStore>): Promise<GatewayStore>;
90
90
  export declare function publicGatewayChannel(channel: GatewayChannelConfig, listening?: boolean): GatewayChannelPublic;
91
91
  export declare function upsertGatewayChannel(current: GatewayStore, input: GatewayChannelInput, defaultMachineId: string): GatewayStore;
92
+ export {};