@cydm/happy-elves 0.1.0-beta.48 → 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.
- package/apps/cli/dist/commands/app.js +5 -1
- package/apps/cli/dist/commands/config.js +2 -2
- package/apps/cli/dist/commands/lib/args.js +7 -0
- package/apps/cli/dist/commands/lib/config.d.ts +1 -0
- package/apps/cli/dist/commands/lib/config.js +15 -2
- package/apps/cli/dist/commands/lib/doctor.js +3 -3
- package/apps/cli/dist/commands/lib/exit.js +2 -1
- package/apps/cli/dist/commands/lib/index.d.ts +1 -0
- package/apps/cli/dist/commands/lib/index.js +1 -0
- package/apps/cli/dist/commands/lib/json.d.ts +1 -1
- package/apps/cli/dist/commands/lib/json.js +2 -2
- package/apps/cli/dist/commands/lib/orchestrator.d.ts +30 -0
- package/apps/cli/dist/commands/lib/orchestrator.js +146 -0
- package/apps/cli/dist/commands/lib/relay-http.js +15 -4
- package/apps/cli/dist/commands/lib/session-output.d.ts +1 -0
- package/apps/cli/dist/commands/lib/session-output.js +42 -10
- package/apps/cli/dist/commands/lib/status.js +3 -3
- package/apps/cli/dist/commands/lib/types.d.ts +1 -0
- package/apps/cli/dist/commands/lib/usage.js +46 -16
- package/apps/cli/dist/commands/orchestrator.d.ts +2 -0
- package/apps/cli/dist/commands/orchestrator.js +411 -0
- package/apps/cli/dist/commands/relay.js +5 -5
- package/apps/cli/dist/commands/remote.js +2 -2
- package/apps/cli/dist/commands/skill.d.ts +2 -0
- package/apps/cli/dist/commands/skill.js +214 -0
- package/apps/cli/dist/commands/turn.js +5 -2
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/connections.d.ts +1 -0
- package/apps/relay/dist/controller-handlers.js +4 -1
- package/apps/relay/dist/http-routes.js +32 -1
- package/apps/relay/dist/http-schemas.d.ts +16 -0
- package/apps/relay/dist/http-schemas.js +5 -0
- package/apps/relay/dist/relay-context.js +2 -0
- package/apps/relay/dist/session-projection-reducer.js +1 -1
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
- package/packages/client/dist/client.d.ts +5 -0
- package/packages/client/dist/client.js +35 -7
- package/packages/client/dist/http.d.ts +1 -0
- package/packages/client/dist/http.js +15 -3
- package/packages/client/dist/live-session-state.d.ts +15 -0
- package/packages/client/dist/live-session-state.js +60 -0
- package/packages/client/dist/live-state.d.ts +8 -0
- package/packages/client/dist/live-state.js +33 -0
- package/packages/client/dist/live-types.d.ts +113 -0
- package/packages/client/dist/live-types.js +1 -0
- package/packages/client/dist/live.d.ts +2 -99
- package/packages/client/dist/live.js +16 -77
- package/packages/client/dist/types.d.ts +4 -1
- package/packages/client/dist/validation.d.ts +1 -0
- package/packages/client/dist/validation.js +11 -0
- package/packages/shared/dist/protocol.d.ts +3 -0
- 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"
|
|
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
|
-
|
|
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
|
|
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
|
|
94
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
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: ${
|
|
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
|
-
|
|
177
|
-
|
|
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.
|
|
203
|
-
2.
|
|
204
|
-
3.
|
|
205
|
-
4.
|
|
206
|
-
5.
|
|
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
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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.
|