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

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 (55) hide show
  1. package/apps/cli/dist/commands/app.js +5 -1
  2. package/apps/cli/dist/commands/config.js +2 -2
  3. package/apps/cli/dist/commands/lib/args.js +7 -0
  4. package/apps/cli/dist/commands/lib/config.d.ts +1 -0
  5. package/apps/cli/dist/commands/lib/config.js +15 -2
  6. package/apps/cli/dist/commands/lib/doctor.js +3 -3
  7. package/apps/cli/dist/commands/lib/exit.js +2 -1
  8. package/apps/cli/dist/commands/lib/index.d.ts +1 -0
  9. package/apps/cli/dist/commands/lib/index.js +1 -0
  10. package/apps/cli/dist/commands/lib/json.d.ts +1 -1
  11. package/apps/cli/dist/commands/lib/json.js +2 -2
  12. package/apps/cli/dist/commands/lib/orchestrator.d.ts +30 -0
  13. package/apps/cli/dist/commands/lib/orchestrator.js +146 -0
  14. package/apps/cli/dist/commands/lib/relay-http.js +15 -4
  15. package/apps/cli/dist/commands/lib/session-output.d.ts +1 -0
  16. package/apps/cli/dist/commands/lib/session-output.js +42 -10
  17. package/apps/cli/dist/commands/lib/status.js +3 -3
  18. package/apps/cli/dist/commands/lib/types.d.ts +1 -0
  19. package/apps/cli/dist/commands/lib/usage.js +46 -16
  20. package/apps/cli/dist/commands/orchestrator.d.ts +2 -0
  21. package/apps/cli/dist/commands/orchestrator.js +411 -0
  22. package/apps/cli/dist/commands/relay.js +5 -5
  23. package/apps/cli/dist/commands/remote.js +2 -2
  24. package/apps/cli/dist/commands/skill.d.ts +2 -0
  25. package/apps/cli/dist/commands/skill.js +214 -0
  26. package/apps/cli/dist/commands/turn.js +5 -2
  27. package/apps/daemon/dist/session/directory.d.ts +4 -0
  28. package/apps/daemon/dist/session/directory.js +30 -3
  29. package/apps/daemon/package.json +1 -1
  30. package/apps/relay/dist/connections.d.ts +1 -0
  31. package/apps/relay/dist/controller-handlers.js +4 -1
  32. package/apps/relay/dist/http-routes.js +32 -1
  33. package/apps/relay/dist/http-schemas.d.ts +16 -0
  34. package/apps/relay/dist/http-schemas.js +5 -0
  35. package/apps/relay/dist/relay-context.js +2 -0
  36. package/apps/relay/dist/session-projection-reducer.js +1 -1
  37. package/npm-shrinkwrap.json +5 -5
  38. package/package.json +1 -1
  39. package/packages/client/dist/client.d.ts +5 -0
  40. package/packages/client/dist/client.js +35 -7
  41. package/packages/client/dist/http.d.ts +1 -0
  42. package/packages/client/dist/http.js +15 -3
  43. package/packages/client/dist/live-session-state.d.ts +15 -0
  44. package/packages/client/dist/live-session-state.js +60 -0
  45. package/packages/client/dist/live-state.d.ts +8 -0
  46. package/packages/client/dist/live-state.js +33 -0
  47. package/packages/client/dist/live-types.d.ts +113 -0
  48. package/packages/client/dist/live-types.js +1 -0
  49. package/packages/client/dist/live.d.ts +2 -99
  50. package/packages/client/dist/live.js +16 -77
  51. package/packages/client/dist/types.d.ts +4 -1
  52. package/packages/client/dist/validation.d.ts +1 -0
  53. package/packages/client/dist/validation.js +11 -0
  54. package/packages/shared/dist/protocol.d.ts +3 -0
  55. package/packages/shared/dist/protocol.js +1 -0
@@ -6,9 +6,11 @@ import { handleConfig } from "./config.js";
6
6
  import { handleDaemon } from "./daemon.js";
7
7
  import { handleLoop } from "./loop.js";
8
8
  import { handleMachine } from "./machine.js";
9
+ import { handleOrchestrator } from "./orchestrator.js";
9
10
  import { handleRelay } from "./relay.js";
10
11
  import { handleRemote } from "./remote.js";
11
12
  import { handleSession } from "./session.js";
13
+ import { handleSkill } from "./skill.js";
12
14
  import { handleToken } from "./token.js";
13
15
  import { handleTurn } from "./turn.js";
14
16
  import { commandUsage, commandUsageText, handleCliError, parseCliFlags, skillText, startBootstrap, topLevelDoctor, topLevelStatus, usage, usageText } from "./lib/index.js";
@@ -22,7 +24,9 @@ const domainHandlers = [
22
24
  handleDaemon,
23
25
  handleRelay,
24
26
  handleMachine,
27
+ handleOrchestrator,
25
28
  handleSession,
29
+ handleSkill,
26
30
  handleTurn,
27
31
  handleCollect,
28
32
  ];
@@ -31,7 +35,7 @@ export async function main() {
31
35
  const [domain] = argv;
32
36
  if (!domain)
33
37
  usage();
34
- if (domain === "--skill" || domain === "skill") {
38
+ if (domain === "--skill") {
35
39
  console.log(skillText());
36
40
  return;
37
41
  }
@@ -1,4 +1,4 @@
1
- import { configPath, ok, readConfig, redactedControllerConfig, requireRelayUrl, requireString, wantsJson, writeConfig } from "./lib/index.js";
1
+ import { configPath, ok, readConfig, redactedControllerConfig, redactedRelayUrl, requireRelayUrl, requireString, wantsJson, writeConfig } from "./lib/index.js";
2
2
  export async function handleConfig({ domain, action, flags }) {
3
3
  if (domain === "config" && action === "init") {
4
4
  const config = {
@@ -7,7 +7,7 @@ export async function handleConfig({ domain, action, flags }) {
7
7
  accountSecret: requireString(flags, "secret"),
8
8
  };
9
9
  await writeConfig(config);
10
- ok("config.init", { path: configPath, relayUrl: config.relayUrl });
10
+ ok("config.init", { path: configPath, relayUrl: redactedRelayUrl(config.relayUrl) });
11
11
  return true;
12
12
  }
13
13
  if (domain === "config" && action === "show") {
@@ -23,6 +23,8 @@ const valueFlags = new Set([
23
23
  "permission-mode",
24
24
  "prompt",
25
25
  "prompt-file",
26
+ "query",
27
+ "request-id",
26
28
  "every",
27
29
  "reason",
28
30
  "relay",
@@ -35,11 +37,14 @@ const valueFlags = new Set([
35
37
  "format",
36
38
  "loops",
37
39
  "machines",
40
+ "messages",
38
41
  "sessions",
39
42
  "secret",
40
43
  "since",
41
44
  "stop-on",
42
45
  "tail",
46
+ "tag",
47
+ "tags",
43
48
  "text",
44
49
  "text-file",
45
50
  "timeout",
@@ -56,8 +61,10 @@ const valueFlags = new Set([
56
61
  const booleanFlags = new Set([
57
62
  "all",
58
63
  "detach",
64
+ "force",
59
65
  "help",
60
66
  "h",
67
+ "include-output",
61
68
  "include-archived",
62
69
  "json",
63
70
  "keep-source",
@@ -2,6 +2,7 @@ import type { CliConfig, DaemonConfig } from "./types.js";
2
2
  export declare function requireString(flags: Record<string, string | boolean>, key: string): string;
3
3
  export declare function requirePositional(value: string | undefined, name: string): string;
4
4
  export declare function normalizeRelayUrl(value: string): string;
5
+ export declare function redactedRelayUrl(value: string): string;
5
6
  export declare function requireRelayUrl(flags: Record<string, string | boolean>, key?: string): string;
6
7
  export declare function readConfig(flags: Record<string, string | boolean>): Promise<CliConfig>;
7
8
  export declare function readDaemonConfig(): Promise<DaemonConfig>;
@@ -17,6 +17,19 @@ export function requirePositional(value, name) {
17
17
  export function normalizeRelayUrl(value) {
18
18
  return value.replace(/\/+$/, "");
19
19
  }
20
+ export function redactedRelayUrl(value) {
21
+ try {
22
+ const url = new URL(value);
23
+ if (url.username || url.password) {
24
+ url.username = "";
25
+ url.password = "";
26
+ }
27
+ return normalizeRelayUrl(url.toString());
28
+ }
29
+ catch {
30
+ return normalizeRelayUrl(value.replace(/\/\/[^/@]+@/u, "//"));
31
+ }
32
+ }
20
33
  function assertStringField(candidate, field, label) {
21
34
  const value = candidate[field];
22
35
  if (typeof value !== "string" || value.length === 0) {
@@ -125,7 +138,7 @@ export async function writeDaemonConfig(config) {
125
138
  }
126
139
  export function redactedDaemonConfig(config) {
127
140
  return {
128
- relayUrl: config.relayUrl,
141
+ relayUrl: redactedRelayUrl(config.relayUrl),
129
142
  accountId: config.accountId,
130
143
  machineId: config.machineId,
131
144
  machineName: config.machineName,
@@ -136,7 +149,7 @@ export function redactedDaemonConfig(config) {
136
149
  }
137
150
  export function redactedControllerConfig(config) {
138
151
  return {
139
- relayUrl: config.relayUrl,
152
+ relayUrl: redactedRelayUrl(config.relayUrl),
140
153
  hasControllerToken: Boolean(config.controllerToken),
141
154
  hasAccountSecret: Boolean(config.accountSecret),
142
155
  };
@@ -1,6 +1,6 @@
1
1
  import path from "node:path";
2
2
  import { configDir, configPath } from "./paths.js";
3
- import { redactedControllerConfig, redactedDaemonConfig } from "./config.js";
3
+ import { redactedControllerConfig, redactedDaemonConfig, redactedRelayUrl } from "./config.js";
4
4
  import { ok, wantsJson } from "./json.js";
5
5
  import { localDaemonStatus } from "./local-daemon.js";
6
6
  import { readExistingControllerConfig, readExistingDaemonConfig, relayHealth } from "./relay-http.js";
@@ -21,11 +21,11 @@ export async function topLevelDoctor(flags) {
21
21
  name: "relay",
22
22
  ok: health.ok,
23
23
  message: health.ok ? "reachable" : `HTTP ${health.status}`,
24
- details: { relayUrl: relay.relayUrl, body: health.body },
24
+ details: { relayUrl: redactedRelayUrl(relay.relayUrl), body: health.body },
25
25
  });
26
26
  }
27
27
  catch (error) {
28
- checks.push({ name: "relay", ok: false, message: error instanceof Error ? error.message : String(error), details: { relayUrl: relay.relayUrl } });
28
+ checks.push({ name: "relay", ok: false, message: error instanceof Error ? error.message : String(error), details: { relayUrl: redactedRelayUrl(relay.relayUrl) } });
29
29
  }
30
30
  const existingDaemon = await readExistingDaemonConfig().catch((error) => {
31
31
  checks.push({ name: "daemon-config", ok: false, message: error instanceof Error ? error.message : String(error) });
@@ -126,6 +126,7 @@ export function handleCliError(error) {
126
126
  activeTurnId: enriched.activeTurnId,
127
127
  latestCompletedTurnId: enriched.latestCompletedTurnId,
128
128
  commandAcked: enriched.commandAcked,
129
- }, enriched.capability);
129
+ rawStatus: enriched.rawStatus,
130
+ }, enriched.capability, enriched.retryable);
130
131
  process.exit(1);
131
132
  }
@@ -13,6 +13,7 @@ export * from "./exit.js";
13
13
  export * from "./json.js";
14
14
  export * from "./local-daemon.js";
15
15
  export * from "./loop-store.js";
16
+ export * from "./orchestrator.js";
16
17
  export * from "./paths.js";
17
18
  export * from "./relay-http.js";
18
19
  export * from "./scope.js";
@@ -13,6 +13,7 @@ export * from "./exit.js";
13
13
  export * from "./json.js";
14
14
  export * from "./local-daemon.js";
15
15
  export * from "./loop-store.js";
16
+ export * from "./orchestrator.js";
16
17
  export * from "./paths.js";
17
18
  export * from "./relay-http.js";
18
19
  export * from "./scope.js";
@@ -1,5 +1,5 @@
1
1
  import type { JsonEnvelope } from "../../../../../packages/shared/dist/index.js";
2
2
  export declare function ok<T>(type: string, data: T, meta?: JsonEnvelope["meta"]): void;
3
- export declare function writeError(code: string, message: string, meta?: JsonEnvelope["meta"], capability?: string): void;
3
+ export declare function writeError(code: string, message: string, meta?: JsonEnvelope["meta"], capability?: string, retryable?: boolean): void;
4
4
  export declare function wantsJson(flags: Record<string, string | boolean>): boolean;
5
5
  export declare function wantsVerbose(flags: Record<string, string | boolean>): boolean;
@@ -1,10 +1,10 @@
1
1
  export function ok(type, data, meta = {}) {
2
2
  console.log(JSON.stringify({ ok: true, type, data, meta }, null, 2));
3
3
  }
4
- export function writeError(code, message, meta = {}, capability) {
4
+ export function writeError(code, message, meta = {}, capability, retryable) {
5
5
  console.error(JSON.stringify({
6
6
  ok: false,
7
- error: { code, message, ...(capability ? { capability } : {}) },
7
+ error: { code, message, ...(capability ? { capability } : {}), ...(retryable !== undefined ? { retryable } : {}) },
8
8
  meta,
9
9
  }, null, 2));
10
10
  }
@@ -0,0 +1,30 @@
1
+ import type { SessionSnapshot } from "../../../../../packages/shared/dist/index.js";
2
+ import type { OrchestrationSummary } from "./types.js";
3
+ import type { TurnSummary } from "./session-output.js";
4
+ export declare const ORCHESTRATOR_CONTRACT_VERSION = 1;
5
+ export type OrchestratorStatus = "idle" | "inProgress" | "completed" | "needsInput" | "blocked" | "failed" | "cancelled" | "offline" | "closed";
6
+ export type OrchestratorRequestRecord = {
7
+ turnId: string;
8
+ promptSha256: string;
9
+ createdAt: number;
10
+ };
11
+ export type OrchestratorMetadata = {
12
+ tags?: string[];
13
+ requests?: Record<string, OrchestratorRequestRecord>;
14
+ };
15
+ export type MetadataWithOrchestrator = Record<string, unknown> & {
16
+ orchestrator?: Record<string, unknown> & {
17
+ tags?: string[];
18
+ requests?: Record<string, unknown>;
19
+ };
20
+ };
21
+ export declare function promptSha256(text: string): string;
22
+ export declare function deterministicTurnId(threadId: string, requestId: string): string;
23
+ export declare function deterministicRelayRequestId(threadId: string, requestId: string): string;
24
+ export declare function normalizeOrchestratorStatus(session: Pick<SessionSnapshot, "status">, orchestration: OrchestrationSummary, latestTurn?: Pick<TurnSummary, "status">): OrchestratorStatus;
25
+ export declare function normalizeOrchestratorTag(value: string): string;
26
+ export declare function normalizeOrchestratorTags(values: readonly string[]): string[];
27
+ export declare function readOrchestratorMetadata(metadata: Record<string, unknown>): OrchestratorMetadata;
28
+ export declare function withOrchestratorTags(metadata: Record<string, unknown>, tags: string[]): MetadataWithOrchestrator;
29
+ export declare function withOrchestratorRequest(metadata: Record<string, unknown>, requestId: string, request: OrchestratorRequestRecord): MetadataWithOrchestrator;
30
+ export declare function withoutOrchestratorRequest(metadata: Record<string, unknown>, requestId: string): MetadataWithOrchestrator;
@@ -0,0 +1,146 @@
1
+ import { createHash } from "node:crypto";
2
+ import { CliError } from "../../errors.js";
3
+ export const ORCHESTRATOR_CONTRACT_VERSION = 1;
4
+ export function promptSha256(text) {
5
+ return createHash("sha256").update(text, "utf8").digest("hex");
6
+ }
7
+ export function deterministicTurnId(threadId, requestId) {
8
+ const digest = createHash("sha256").update(threadId).update("\0").update(requestId).digest("base64url");
9
+ return `turn_${digest.slice(0, 28)}`;
10
+ }
11
+ export function deterministicRelayRequestId(threadId, requestId) {
12
+ const digest = createHash("sha256").update("orchestrator").update("\0").update(threadId).update("\0").update(requestId).digest("base64url");
13
+ return `req_${digest.slice(0, 28)}`;
14
+ }
15
+ export function normalizeOrchestratorStatus(session, orchestration, latestTurn) {
16
+ if (session.status === "closed")
17
+ return "closed";
18
+ if (session.status === "offline")
19
+ return "offline";
20
+ if (orchestration.needsUser)
21
+ return "needsInput";
22
+ if (orchestration.blocked)
23
+ return "blocked";
24
+ if (latestTurn?.status === "running" || session.status === "running" || session.status === "new")
25
+ return "inProgress";
26
+ if (latestTurn?.status === "failed" || session.status === "failed")
27
+ return "failed";
28
+ if (latestTurn?.status === "cancelled" || session.status === "cancelled")
29
+ return "cancelled";
30
+ if (latestTurn?.status === "completed" || orchestration.completed || session.status === "completed")
31
+ return "completed";
32
+ return "idle";
33
+ }
34
+ export function normalizeOrchestratorTag(value) {
35
+ const tag = value.trim().toLowerCase();
36
+ if (!tag)
37
+ throw new CliError("Empty orchestrator tag is not allowed", "INVALID_ARGUMENT");
38
+ if (tag.length > 64)
39
+ throw new CliError(`Orchestrator tag is too long: ${tag}`, "INVALID_ARGUMENT");
40
+ if (!/^[a-z0-9._-]+$/u.test(tag)) {
41
+ throw new CliError(`Invalid orchestrator tag: ${value}`, "INVALID_ARGUMENT");
42
+ }
43
+ return tag;
44
+ }
45
+ export function normalizeOrchestratorTags(values) {
46
+ const tags = [...new Set(values.map(normalizeOrchestratorTag))];
47
+ if (tags.length > 20)
48
+ throw new CliError("A thread can have at most 20 orchestrator tags", "INVALID_ARGUMENT");
49
+ return tags;
50
+ }
51
+ export function readOrchestratorMetadata(metadata) {
52
+ const raw = metadata.orchestrator;
53
+ if (!raw || typeof raw !== "object" || Array.isArray(raw))
54
+ return {};
55
+ const record = raw;
56
+ const tags = Array.isArray(record.tags)
57
+ ? [...new Set(record.tags.flatMap((item) => {
58
+ if (typeof item !== "string")
59
+ return [];
60
+ try {
61
+ return [normalizeOrchestratorTag(item)];
62
+ }
63
+ catch {
64
+ return [];
65
+ }
66
+ }))]
67
+ : undefined;
68
+ const requests = {};
69
+ if (record.requests && typeof record.requests === "object" && !Array.isArray(record.requests)) {
70
+ for (const [requestId, value] of Object.entries(record.requests)) {
71
+ if (!value || typeof value !== "object" || Array.isArray(value))
72
+ continue;
73
+ const candidate = value;
74
+ if (typeof candidate.turnId === "string" &&
75
+ typeof candidate.promptSha256 === "string" &&
76
+ typeof candidate.createdAt === "number") {
77
+ requests[requestId] = {
78
+ turnId: candidate.turnId,
79
+ promptSha256: candidate.promptSha256,
80
+ createdAt: candidate.createdAt,
81
+ };
82
+ }
83
+ }
84
+ }
85
+ return {
86
+ ...(tags ? { tags } : {}),
87
+ ...(Object.keys(requests).length > 0 ? { requests } : {}),
88
+ };
89
+ }
90
+ function rawOrchestratorMetadata(metadata) {
91
+ return metadata.orchestrator && typeof metadata.orchestrator === "object" && !Array.isArray(metadata.orchestrator)
92
+ ? { ...metadata.orchestrator }
93
+ : {};
94
+ }
95
+ function rawOrchestratorRequests(rawOrchestrator) {
96
+ return rawOrchestrator.requests && typeof rawOrchestrator.requests === "object" && !Array.isArray(rawOrchestrator.requests)
97
+ ? { ...rawOrchestrator.requests }
98
+ : {};
99
+ }
100
+ export function withOrchestratorTags(metadata, tags) {
101
+ const rawOrchestrator = rawOrchestratorMetadata(metadata);
102
+ return {
103
+ ...metadata,
104
+ orchestrator: {
105
+ ...rawOrchestrator,
106
+ tags: normalizeOrchestratorTags(tags),
107
+ },
108
+ };
109
+ }
110
+ export function withOrchestratorRequest(metadata, requestId, request) {
111
+ const rawOrchestrator = rawOrchestratorMetadata(metadata);
112
+ const rawRequests = rawOrchestratorRequests(rawOrchestrator);
113
+ const existingRequest = rawRequests[requestId] && typeof rawRequests[requestId] === "object" && !Array.isArray(rawRequests[requestId])
114
+ ? { ...rawRequests[requestId] }
115
+ : {};
116
+ return {
117
+ ...metadata,
118
+ orchestrator: {
119
+ ...rawOrchestrator,
120
+ requests: {
121
+ ...rawRequests,
122
+ [requestId]: {
123
+ ...existingRequest,
124
+ ...request,
125
+ },
126
+ },
127
+ },
128
+ };
129
+ }
130
+ export function withoutOrchestratorRequest(metadata, requestId) {
131
+ const rawOrchestrator = rawOrchestratorMetadata(metadata);
132
+ const orchestrator = readOrchestratorMetadata(metadata);
133
+ const requests = rawOrchestratorRequests(rawOrchestrator);
134
+ delete requests[requestId];
135
+ const nextOrchestrator = {
136
+ ...rawOrchestrator,
137
+ ...(orchestrator.tags ? { tags: orchestrator.tags } : {}),
138
+ ...(Object.keys(requests).length > 0 ? { requests } : {}),
139
+ };
140
+ if (Object.keys(requests).length === 0)
141
+ delete nextOrchestrator.requests;
142
+ return {
143
+ ...metadata,
144
+ ...(Object.keys(nextOrchestrator).length > 0 ? { orchestrator: nextOrchestrator } : { orchestrator: undefined }),
145
+ };
146
+ }
@@ -1,7 +1,14 @@
1
1
  import { CliError } from "../../errors.js";
2
- import { normalizeRelayUrl, readConfig, readDaemonConfig } from "./config.js";
2
+ import { normalizeRelayUrl, readConfig, readDaemonConfig, redactedRelayUrl } from "./config.js";
3
3
  export async function relayHealth(relayUrl) {
4
- const response = await fetch(`${normalizeRelayUrl(relayUrl)}/health`);
4
+ const normalizedRelayUrl = normalizeRelayUrl(relayUrl);
5
+ let response;
6
+ try {
7
+ response = await fetch(`${normalizedRelayUrl}/health`);
8
+ }
9
+ catch (error) {
10
+ throw cliErrorFromFetch(error, "RELAY_UNREACHABLE", `Could not reach ${redactedRelayUrl(normalizedRelayUrl)}`);
11
+ }
5
12
  const text = await response.text();
6
13
  let body = text;
7
14
  try {
@@ -90,8 +97,12 @@ export async function readExistingDaemonConfig() {
90
97
  export function cliErrorFromFetch(error, code, message) {
91
98
  if (error instanceof CliError)
92
99
  return error;
93
- const suffix = error instanceof Error ? `: ${error.message}` : "";
94
- return new CliError(`${message}${suffix}`, code);
100
+ const redactedMessage = redactUrlCredentialsInText(message);
101
+ const suffix = error instanceof Error ? `: ${redactUrlCredentialsInText(error.message)}` : "";
102
+ return new CliError(`${redactedMessage}${suffix}`, code);
103
+ }
104
+ function redactUrlCredentialsInText(value) {
105
+ return value.replace(/(https?:\/\/)([^/@\s]+)@/gu, "$1");
95
106
  }
96
107
  export function isAuthTokenInvalid(error) {
97
108
  if (!error || typeof error !== "object")
@@ -50,6 +50,7 @@ export declare function summarizeTurns(events: DecodedSessionEvent[]): TurnSumma
50
50
  export declare function structuredOutputForTurn(sessionId: string, turn: TurnSummary): StructuredCommandOutput;
51
51
  export declare function structuredOutputForStoredTurn(output: StoredTurnOutput): StructuredCommandOutput;
52
52
  export declare function structuredOutputForRunningTurn(sessionId: string, turnId: string): StructuredCommandOutput;
53
+ export declare function collectExactTurnEvents(client: ControllerClient, sessionId: string, turnId: string, limit?: number): Promise<DecodedSessionEvent[]>;
53
54
  export declare function rebuildTurnOutput(client: ControllerClient, sessionId: string, turnId?: string): Promise<StoredTurnOutput>;
54
55
  export declare function conciseEvents(events: DecodedSessionEvent[]): Array<{
55
56
  id: number;
@@ -280,7 +280,7 @@ function foldAliasedRuntimeEvents(events) {
280
280
  profile.output += payload.text;
281
281
  }
282
282
  else if (payload?.type === "done") {
283
- const runtimeTurnId = payload.lastTurnId ?? payload.currentHead;
283
+ const runtimeTurnId = payload.lastTurnId;
284
284
  if (runtimeTurnId && runtimeTurnId !== event.turnId && !runtimeTurnId.startsWith("event:")) {
285
285
  runtimeToRequest.set(runtimeTurnId, event.turnId);
286
286
  }
@@ -288,7 +288,6 @@ function foldAliasedRuntimeEvents(events) {
288
288
  profiles.set(event.turnId, profile);
289
289
  }
290
290
  const duplicateRuntimeTurns = new Set();
291
- const duplicateRuntimeToRequest = new Map();
292
291
  for (const [runtimeTurnId, requestTurnId] of runtimeToRequest) {
293
292
  const request = profiles.get(requestTurnId);
294
293
  const runtime = profiles.get(runtimeTurnId);
@@ -298,12 +297,11 @@ function foldAliasedRuntimeEvents(events) {
298
297
  continue;
299
298
  if (isDuplicateRuntimeOutput(request.output, runtime.output)) {
300
299
  duplicateRuntimeTurns.add(runtimeTurnId);
301
- duplicateRuntimeToRequest.set(runtimeTurnId, requestTurnId);
302
300
  }
303
301
  }
304
302
  return {
305
303
  events: duplicateRuntimeTurns.size === 0 ? events : events.filter((event) => !duplicateRuntimeTurns.has(event.turnId)),
306
- runtimeToRequest: duplicateRuntimeToRequest,
304
+ runtimeToRequest,
307
305
  };
308
306
  }
309
307
  function isDuplicateRuntimeOutput(requestOutput, runtimeOutput) {
@@ -324,13 +322,47 @@ function turnInProgressError(sessionId, turn, latestCompleted) {
324
322
  latestCompletedTurnId: latestCompleted?.turnId,
325
323
  });
326
324
  }
325
+ export async function collectExactTurnEvents(client, sessionId, turnId, limit = 1000) {
326
+ const events = [];
327
+ const seen = new Set();
328
+ let since;
329
+ for (let pageCount = 0; pageCount < 200; pageCount += 1) {
330
+ const page = await client.collectPage(sessionId, {
331
+ turnId,
332
+ limit,
333
+ ...(since === undefined ? {} : { since }),
334
+ });
335
+ for (const event of page.events) {
336
+ if (seen.has(event.id))
337
+ continue;
338
+ seen.add(event.id);
339
+ events.push(event);
340
+ }
341
+ const nextSince = page.newestCursor
342
+ ?? page.newestEventId
343
+ ?? (page.events.length > 0 ? Math.max(...page.events.map((event) => event.id)) : undefined);
344
+ if (page.events.length < limit || nextSince === undefined || nextSince === since)
345
+ break;
346
+ since = nextSince;
347
+ }
348
+ return events;
349
+ }
327
350
  export async function rebuildTurnOutput(client, sessionId, turnId) {
328
- const events = await client.history(sessionId, 1000);
329
- const folded = foldAliasedRuntimeEvents(events);
330
- const resolvedTurnId = turnId ? folded.runtimeToRequest.get(turnId) ?? turnId : undefined;
331
- const turns = summarizeTurns(events);
332
- const latestCompleted = turns.find((turn) => turn.status === "completed");
333
- const summary = resolvedTurnId ? turns.find((turn) => turn.turnId === resolvedTurnId) : turns[0];
351
+ let events = await client.history(sessionId, 1000);
352
+ let folded = foldAliasedRuntimeEvents(events);
353
+ let resolvedTurnId = turnId ? folded.runtimeToRequest.get(turnId) ?? turnId : undefined;
354
+ let turns = summarizeTurns(events);
355
+ let latestCompleted = turns.find((turn) => turn.status === "completed");
356
+ let summary = resolvedTurnId ? turns.find((turn) => turn.turnId === resolvedTurnId) : turns[0];
357
+ const exactTurnId = turnId ? (resolvedTurnId ?? turnId) : summary?.status === "running" ? undefined : summary?.turnId;
358
+ if (exactTurnId) {
359
+ events = await collectExactTurnEvents(client, sessionId, exactTurnId, 1000);
360
+ folded = foldAliasedRuntimeEvents(events);
361
+ resolvedTurnId = turnId ? (folded.runtimeToRequest.get(turnId) ?? resolvedTurnId) : exactTurnId;
362
+ turns = summarizeTurns(events);
363
+ latestCompleted = turns.find((turn) => turn.status === "completed") ?? latestCompleted;
364
+ summary = resolvedTurnId ? turns.find((turn) => turn.turnId === resolvedTurnId) : turns[0];
365
+ }
334
366
  if (!summary) {
335
367
  throw new CliError(turnId ? `Turn ${turnId} was not found in the latest relay history.` : "No turn output found for session", turnId ? "TURN_NOT_FOUND" : "RESULT_NOT_FOUND");
336
368
  }
@@ -1,5 +1,5 @@
1
1
  import { ControllerClient } from "../../../../../packages/client/dist/index.js";
2
- import { readConfig } from "./config.js";
2
+ import { readConfig, redactedRelayUrl } from "./config.js";
3
3
  import { ok, wantsJson } from "./json.js";
4
4
  import { localDaemonStatus } from "./local-daemon.js";
5
5
  import { displayPath, lastAssistantOutput, readStoredTurnOutput } from "./session-output.js";
@@ -20,7 +20,7 @@ export async function topLevelStatus(flags) {
20
20
  ? lastAssistantOutput(await client.history(latestSession.id, 200).catch(() => []))
21
21
  : "";
22
22
  const data = {
23
- relayUrl: config.relayUrl,
23
+ relayUrl: redactedRelayUrl(config.relayUrl),
24
24
  relay: {
25
25
  ok: Boolean(snapshot),
26
26
  ...(relayError ? { error: relayError } : {}),
@@ -46,7 +46,7 @@ export async function topLevelStatus(flags) {
46
46
  ok("status", data);
47
47
  return;
48
48
  }
49
- console.log(`Relay: ${config.relayUrl}${relayError ? ` (unreachable: ${relayError})` : ""}`);
49
+ console.log(`Relay: ${data.relayUrl}${relayError ? ` (unreachable: ${relayError})` : ""}`);
50
50
  console.log(`Daemon: ${localStatus.running ? `running${localStatus.pid ? ` pid ${localStatus.pid}` : ""}` : "not running"}`);
51
51
  console.log(`Machines: ${data.machines.onlineCount}/${data.machines.count} online`);
52
52
  console.log(`Sessions: ${data.sessions.count} total, ${data.sessions.runningCount} running`);
@@ -30,6 +30,7 @@ export type SessionMetadata = {
30
30
  name?: string;
31
31
  requestedRuntimeName?: string;
32
32
  runtime?: unknown;
33
+ [key: string]: unknown;
33
34
  };
34
35
  export declare const idleSessionStatuses: SessionSnapshot["status"][];
35
36
  export type OrchestrationState = "pending" | "active" | "idle" | "completed" | "blocked" | "needs-user" | "closed";
@@ -43,6 +43,18 @@ Check local controller config, relay reachability, and daemon state.
43
43
  diagnose/exec are LAN/dev-only. exec requires the daemon to advertise devTools.
44
44
  --timeout-ms limits the remote process. --wait-timeout-ms limits how long this CLI waits for the result.
45
45
  file preview is read-only and limited to files inside --root.
46
+ `,
47
+ orchestrator: `Usage:
48
+ happy-elves orchestrator list-threads [--query <q>] [--tag <tag>] [--machine <id>] [--limit n] [--all] --json
49
+ happy-elves orchestrator read-thread <threadId> [--messages latest|all] [--limit n] [--include-output] --json
50
+ happy-elves orchestrator send-message <threadId> (--text <prompt> | --text-file <path>) --request-id <id> [--permission-mode <mode>] [--runtime-timeout 1000s] --json
51
+ happy-elves orchestrator wait-thread <threadId> --turn <turnId> [--timeout 1000s] --json
52
+ happy-elves orchestrator set-thread-tags <threadId> --tags worker,reviewer --json
53
+
54
+ Codex-like orchestration:
55
+ threadId is the existing sessionId. send-message is detached by default and
56
+ returns a deterministic turnId for the supplied request id. wait-thread waits
57
+ for that exact turn id instead of following the latest turn.
46
58
  `,
47
59
  session: `Usage:
48
60
  happy-elves session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] [--permission-mode <mode>] --json
@@ -92,6 +104,11 @@ Turn management:
92
104
  happy-elves token create [--name <name>] [--machines a,b] [--sessions a,b] [--loops a,b] --actions a,b [--expires-in 24h] --json
93
105
  happy-elves token list [--verbose] [--json]
94
106
  happy-elves token revoke <tokenId> --json
107
+ `,
108
+ skill: `Usage:
109
+ happy-elves skill print [--name happy-elves-orchestrator]
110
+ happy-elves skill install --to <skills-dir> [--name happy-elves-orchestrator] [--force] --json
111
+ happy-elves skill doctor [--to <skills-dir>] [--name happy-elves-orchestrator] [--json]
95
112
  `,
96
113
  loop: `Usage:
97
114
  happy-elves loop create <sessionId> [--every 15m] (--prompt <text> | --prompt-file <path>) [--stop-on blocked] --json
@@ -158,6 +175,11 @@ Core commands:
158
175
  turn rewind <sessionId> <turn-or-checkpoint-id> --json
159
176
  turn fork <sessionId> <turn-or-checkpoint-id> --name <name> --json
160
177
 
178
+ orchestrator list-threads --json
179
+ orchestrator send-message <threadId> --text <prompt> --request-id <id> --json
180
+ orchestrator read-thread <threadId> --json
181
+ orchestrator wait-thread <threadId> --turn <turnId> --json
182
+
161
183
  remote status --json
162
184
  remote devices [--json]
163
185
  remote invite-controller [--device-name <name>] [--ttl 10m] --json
@@ -173,15 +195,15 @@ Usage:
173
195
  happy-elves --help
174
196
 
175
197
  Common flows:
176
- Discover: machine list --json; session list --machine <machineId> --json
177
- Continue: session history --machine <machineId> --json; session continue <runtimeSessionId> --machine <machineId> --json
178
- Execute: turn run <sessionId> --text <prompt> --json; turn result <sessionId> <turnId> --json
198
+ Orchestrate: orchestrator list-threads --json; orchestrator send-message <threadId> --text <prompt> --request-id <id> --json; orchestrator wait-thread <threadId> --turn <turnId> --json
199
+ Debug: machine list --json; session list --machine <machineId> --json; turn result <sessionId> <turnId> --json
179
200
  Branch: turn rewind <sessionId> <turnId> --json; turn fork <sessionId> <turnId> --name <name> --json
180
201
 
181
202
  More:
182
203
  happy-elves --skill
183
204
  happy-elves <domain> --help
184
205
  start, status, doctor, collect, config, account, daemon, relay, token, loop.
206
+ orchestrator, skill.
185
207
  `;
186
208
  }
187
209
  export function skillText() {
@@ -199,17 +221,17 @@ Do not:
199
221
  returns a sessionId.
200
222
 
201
223
  Recommended agent workflow:
202
- 1. machine list --json
203
- 2. session list --machine <machineId> --limit 20 --json
204
- 3. session history --machine <machineId> --limit 20 --json
205
- 4. session continue <runtimeSessionId> --machine <machineId> --json
206
- 5. turn run <sessionId> --text <prompt> --json
207
- 6. turn wait <sessionId> <turnId> --json
208
- 7. turn result <sessionId> <turnId> --json
224
+ 1. orchestrator list-threads --json
225
+ 2. orchestrator read-thread <threadId> --json
226
+ 3. orchestrator send-message <threadId> --text <prompt> --request-id <stable-id> --json
227
+ 4. orchestrator read-thread <threadId> --json
228
+ 5. orchestrator wait-thread <threadId> --turn <turnId> --json
209
229
 
210
230
  Concepts:
211
231
  Loaded session: a Happy Elves control-plane session that can be operated by
212
232
  this CLI.
233
+ Thread: an orchestrator-facing alias for a loaded session. threadId is the
234
+ same value as sessionId.
213
235
  Runtime history: read-only provider history discovered by session history.
214
236
  Turn: one execution within a loaded session.
215
237
  Relay: encrypted transport between controllers and daemons. Use relay
@@ -230,12 +252,20 @@ Session management:
230
252
  Output modes:
231
253
  Prefer --json when another agent or program will parse the result.
232
254
  Use human output for direct terminal inspection.
233
- Default JSON is compact: list/status/turn answer with bounded projections,
234
- not raw metadata or event streams.
235
- Add --verbose to status, list, history, turn list, or collect when internal
236
- details are needed for debugging.
237
- turn run waits for completion by default with a 1000s timeout. Use --detach
238
- only when you intentionally want background execution.
255
+ Orchestrator JSON is compact by default. read-thread returns only a short
256
+ latest-turn projection unless --messages latest|all or --include-output is
257
+ explicitly requested.
258
+ list-threads returns loaded sessions, including known offline/closed sessions;
259
+ --all is an explicit alias for that loaded-session scope. If a running
260
+ thread's history is unavailable, the item reports historyStatus/historyError
261
+ instead of pretending to be healthy inProgress.
262
+ send-message requires --request-id and is detached by default. Reusing the
263
+ same request id with the same prompt returns the same turn; reusing it with a
264
+ different prompt returns REQUEST_ID_CONFLICT.
265
+
266
+ Debug commands:
267
+ machine/session/turn commands remain available for lower-level inspection,
268
+ runtime history import, rewind, fork, and result repair.
239
269
 
240
270
  Security notes:
241
271
  Account secrets stay on controllers and daemons.