@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.
- 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/dist/session/directory.d.ts +4 -0
- package/apps/daemon/dist/session/directory.js +30 -3
- 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 +5 -5
- 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
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { CliError, ControllerClient, DEFAULT_COMMAND_TIMEOUT_MS, compactText, displayPath, ok, parseDurationMs, parsePermissionMode, printConciseEvents, readConfig, readPromptTextFile, rebuildTurnOutput, requirePositional, requireProjectedSession, requireString, structuredOutputForRunningTurn, structuredOutputForStoredTurn, structuredOutputForTurn, summarizeTurns, wantsJson, wantsVerbose, writeTurnOutput } from "./lib/index.js";
|
|
1
|
+
import { CliError, ControllerClient, DEFAULT_COMMAND_TIMEOUT_MS, collectExactTurnEvents, compactText, displayPath, ok, parseDurationMs, parsePermissionMode, printConciseEvents, readConfig, readPromptTextFile, rebuildTurnOutput, requirePositional, requireProjectedSession, requireString, structuredOutputForRunningTurn, structuredOutputForStoredTurn, structuredOutputForTurn, summarizeTurns, wantsJson, wantsVerbose, writeTurnOutput } from "./lib/index.js";
|
|
2
2
|
function formatTurnTimestamp(value) {
|
|
3
3
|
return value === undefined ? "-" : new Date(value).toISOString();
|
|
4
4
|
}
|
|
@@ -46,6 +46,9 @@ function compactTurn(turn) {
|
|
|
46
46
|
async function readTurnSummary(client, sessionId, turnId) {
|
|
47
47
|
return summarizeTurns(await client.history(sessionId, 1000)).find((turn) => turn.turnId === turnId);
|
|
48
48
|
}
|
|
49
|
+
async function readExactTurnSummary(client, sessionId, turnId) {
|
|
50
|
+
return summarizeTurns(await collectExactTurnEvents(client, sessionId, turnId, 1000)).find((turn) => turn.turnId === turnId);
|
|
51
|
+
}
|
|
49
52
|
async function resolveTurnOrCheckpointId(client, sessionId, turnOrCheckpointId) {
|
|
50
53
|
const turn = await readTurnSummary(client, sessionId, turnOrCheckpointId);
|
|
51
54
|
return turn?.checkpointId ?? turn?.runtimeTurnId ?? turnOrCheckpointId;
|
|
@@ -53,7 +56,7 @@ async function resolveTurnOrCheckpointId(client, sessionId, turnOrCheckpointId)
|
|
|
53
56
|
async function waitForTurn(client, sessionId, turnId, timeoutMs) {
|
|
54
57
|
const startedAt = Date.now();
|
|
55
58
|
while (true) {
|
|
56
|
-
const summary = await
|
|
59
|
+
const summary = await readExactTurnSummary(client, sessionId, turnId);
|
|
57
60
|
if (summary && summary.status !== "running")
|
|
58
61
|
return summary;
|
|
59
62
|
if (Date.now() - startedAt >= timeoutMs) {
|
|
@@ -1,4 +1,8 @@
|
|
|
1
1
|
import type { MachineCommand } from "../../../../packages/shared/dist/index.js";
|
|
2
2
|
import type { DaemonConfig, DirectoryListResult } from "../types.js";
|
|
3
|
+
export declare function availableWindowsDriveRoots(options?: {
|
|
4
|
+
platform?: NodeJS.Platform;
|
|
5
|
+
access?: (target: string) => Promise<unknown>;
|
|
6
|
+
}): Promise<string[]>;
|
|
3
7
|
export declare function buildDirectoryList(config: DaemonConfig, requestedPath?: string): Promise<DirectoryListResult>;
|
|
4
8
|
export declare function handleListDirectory(ws: WebSocket, config: DaemonConfig, command: MachineCommand): Promise<void>;
|
|
@@ -5,6 +5,7 @@ import { claimCommandRequest, sendCommandResponse } from "../relay/send.js";
|
|
|
5
5
|
import { isUncPath, workspaceRealRoots } from "./workspace-paths.js";
|
|
6
6
|
const directoryEntryLimit = 500;
|
|
7
7
|
const directoryStatConcurrency = 16;
|
|
8
|
+
const windowsDriveLetters = "ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
8
9
|
async function findRepoRoot(startPath) {
|
|
9
10
|
let current = startPath;
|
|
10
11
|
while (true) {
|
|
@@ -40,12 +41,38 @@ async function directoryRoots(workspaceRoots, currentPath) {
|
|
|
40
41
|
for (const root of workspaceRoots)
|
|
41
42
|
await add(root);
|
|
42
43
|
await add(os.homedir(), "home");
|
|
43
|
-
|
|
44
|
+
const currentRoot = path.parse(currentPath).root;
|
|
45
|
+
await add(currentRoot, filesystemRootName(currentRoot));
|
|
44
46
|
const homeRoot = path.parse(os.homedir()).root;
|
|
45
|
-
if (homeRoot !==
|
|
46
|
-
await add(homeRoot);
|
|
47
|
+
if (homeRoot !== currentRoot)
|
|
48
|
+
await add(homeRoot, filesystemRootName(homeRoot));
|
|
49
|
+
for (const driveRoot of await availableWindowsDriveRoots())
|
|
50
|
+
await add(driveRoot, filesystemRootName(driveRoot));
|
|
47
51
|
return roots;
|
|
48
52
|
}
|
|
53
|
+
export async function availableWindowsDriveRoots(options = {}) {
|
|
54
|
+
const platform = options.platform ?? process.platform;
|
|
55
|
+
if (platform !== "win32")
|
|
56
|
+
return [];
|
|
57
|
+
const access = options.access ?? ((target) => fs.access(target));
|
|
58
|
+
const roots = await Promise.all([...windowsDriveLetters].map(async (letter) => {
|
|
59
|
+
const root = `${letter}:\\`;
|
|
60
|
+
try {
|
|
61
|
+
await access(root);
|
|
62
|
+
return root;
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return undefined;
|
|
66
|
+
}
|
|
67
|
+
}));
|
|
68
|
+
return roots.filter((root) => Boolean(root));
|
|
69
|
+
}
|
|
70
|
+
function filesystemRootName(root) {
|
|
71
|
+
const match = /^([A-Za-z]:)[\\/]*$/u.exec(root);
|
|
72
|
+
if (match)
|
|
73
|
+
return match[1];
|
|
74
|
+
return root;
|
|
75
|
+
}
|
|
49
76
|
export async function buildDirectoryList(config, requestedPath) {
|
|
50
77
|
if (requestedPath && isUncPath(requestedPath)) {
|
|
51
78
|
throw Object.assign(new Error("UNC paths are not supported for directory browsing."), { code: "DIRECTORY_UNSUPPORTED_PATH" });
|
package/apps/daemon/package.json
CHANGED
|
@@ -172,6 +172,7 @@ export function handleControllerMessage(context, connection, message) {
|
|
|
172
172
|
markCommandPending(connection.accountId, sessionRow.machine_id, message.requestId, {
|
|
173
173
|
action: "session.run",
|
|
174
174
|
blocksSessionTurn: true,
|
|
175
|
+
promptHash: message.promptHash,
|
|
175
176
|
recipientDeviceId: connection.deviceId ?? undefined,
|
|
176
177
|
sessionId: sessionRow.id,
|
|
177
178
|
turnId: message.turnId,
|
|
@@ -729,7 +730,7 @@ function controllerMessageCommandContext(message, targetMachineId) {
|
|
|
729
730
|
case "controller:createSession":
|
|
730
731
|
return { action: "session.create", agent: message.agent, cwd: message.cwd, machineId: message.machineId, name: message.name };
|
|
731
732
|
case "controller:prompt":
|
|
732
|
-
return { action: "session.run", machineId: targetMachineId, sessionId: message.sessionId, turnId: message.turnId };
|
|
733
|
+
return { action: "session.run", machineId: targetMachineId, promptHash: message.promptHash, sessionId: message.sessionId, turnId: message.turnId };
|
|
733
734
|
case "controller:cancel":
|
|
734
735
|
return { action: "session.cancel", machineId: targetMachineId, sessionId: message.sessionId, turnId: message.turnId };
|
|
735
736
|
case "controller:resume":
|
|
@@ -790,6 +791,8 @@ function pendingCommandMatchesMessage(pending, expected) {
|
|
|
790
791
|
return false;
|
|
791
792
|
if (!matchesDefined(pending.name, expected.name))
|
|
792
793
|
return false;
|
|
794
|
+
if (!matchesDefined(pending.promptHash, expected.promptHash))
|
|
795
|
+
return false;
|
|
793
796
|
if (!matchesDefined(pending.runtimeSessionId, expected.runtimeSessionId))
|
|
794
797
|
return false;
|
|
795
798
|
if (!commandSessionMatches(pending, expected))
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
2
|
import { encryptedEnvelopeSchema, pairingCode, randomId, } from "../../../packages/shared/dist/index.js";
|
|
3
|
-
import { controllerDeviceSchema, controllerInviteClaimSchema, controllerInviteCreateSchema, deviceRevokeSchema, pairingClaimSchema, pairingStartSchema, scopedTokenCreateSchema, scopedTokenRevokeSchema, sessionHeadAdvanceSchema, sessionEventsQuerySchema, } from "./http-schemas.js";
|
|
3
|
+
import { controllerDeviceSchema, controllerInviteClaimSchema, controllerInviteCreateSchema, deviceRevokeSchema, pairingClaimSchema, pairingStartSchema, scopedTokenCreateSchema, scopedTokenRevokeSchema, sessionHeadAdvanceSchema, sessionMetadataUpdateSchema, sessionEventsQuerySchema, } from "./http-schemas.js";
|
|
4
4
|
import { HttpError } from "./errors.js";
|
|
5
5
|
import { sessionEventLogicalMessageKey, sessionEventLogicalSeq, sessionEventLogicalSeqValue, sessionEventLogicalTime, sessionEventLogicalTimeValue, visibleSessionEventWhere, } from "./event-visibility.js";
|
|
6
6
|
import { encodePairingCode, isPairingCodeMatch, tokenDigest } from "./security.js";
|
|
@@ -360,6 +360,37 @@ export function registerHttpRoutes(app, context, controllerInviteTtlMs) {
|
|
|
360
360
|
...(rewriteFenced ? { reason: "rewrite-fenced" } : {}),
|
|
361
361
|
};
|
|
362
362
|
});
|
|
363
|
+
app.patch("/api/sessions/:sessionId/metadata", async (request) => {
|
|
364
|
+
const token = context.requireFullControllerToken(request.headers.authorization);
|
|
365
|
+
const params = z.object({ sessionId: z.string().min(1) }).parse(request.params);
|
|
366
|
+
const body = sessionMetadataUpdateSchema.parse(request.body);
|
|
367
|
+
const session = context.db
|
|
368
|
+
.prepare("SELECT * FROM sessions WHERE account_id = ? AND id = ?")
|
|
369
|
+
.get(token.account_id, params.sessionId);
|
|
370
|
+
if (!session)
|
|
371
|
+
throw new HttpError(404, "SESSION_NOT_FOUND", "Session not found");
|
|
372
|
+
const ts = context.now();
|
|
373
|
+
const result = body.expectedUpdatedAt === undefined
|
|
374
|
+
? context.db.prepare(`UPDATE sessions
|
|
375
|
+
SET encrypted_metadata = ?,
|
|
376
|
+
updated_at = ?
|
|
377
|
+
WHERE account_id = ? AND id = ?`).run(JSON.stringify(body.encryptedMetadata), ts, token.account_id, params.sessionId)
|
|
378
|
+
: context.db.prepare(`UPDATE sessions
|
|
379
|
+
SET encrypted_metadata = ?,
|
|
380
|
+
updated_at = ?
|
|
381
|
+
WHERE account_id = ? AND id = ? AND updated_at = ?`).run(JSON.stringify(body.encryptedMetadata), ts, token.account_id, params.sessionId, body.expectedUpdatedAt);
|
|
382
|
+
if (result.changes === 0) {
|
|
383
|
+
throw new HttpError(409, "SESSION_METADATA_CONFLICT", "Session metadata changed; retry with the latest session");
|
|
384
|
+
}
|
|
385
|
+
const row = context.db
|
|
386
|
+
.prepare("SELECT * FROM sessions WHERE account_id = ? AND id = ?")
|
|
387
|
+
.get(token.account_id, params.sessionId);
|
|
388
|
+
if (!row)
|
|
389
|
+
throw new HttpError(404, "SESSION_NOT_FOUND", "Session not found");
|
|
390
|
+
const snapshot = context.sessionSnapshot(row);
|
|
391
|
+
context.broadcastControllers(token.account_id, { type: "server:session", session: snapshot });
|
|
392
|
+
return { session: snapshot };
|
|
393
|
+
});
|
|
363
394
|
app.get("/api/devices", async (request) => {
|
|
364
395
|
const token = context.requireFullControllerToken(request.headers.authorization);
|
|
365
396
|
const rows = context.db
|
|
@@ -69,3 +69,19 @@ export declare const sessionHeadAdvanceSchema: z.ZodObject<{
|
|
|
69
69
|
occurredAt: z.ZodOptional<z.ZodNumber>;
|
|
70
70
|
}, z.core.$strip>;
|
|
71
71
|
}, z.core.$strip>;
|
|
72
|
+
export declare const sessionMetadataUpdateSchema: z.ZodObject<{
|
|
73
|
+
encryptedMetadata: z.ZodObject<{
|
|
74
|
+
v: z.ZodLiteral<1>;
|
|
75
|
+
alg: z.ZodLiteral<"A256GCM">;
|
|
76
|
+
scope: z.ZodString;
|
|
77
|
+
key: z.ZodObject<{
|
|
78
|
+
nonce: z.ZodString;
|
|
79
|
+
ciphertext: z.ZodString;
|
|
80
|
+
}, z.core.$strip>;
|
|
81
|
+
data: z.ZodObject<{
|
|
82
|
+
nonce: z.ZodString;
|
|
83
|
+
ciphertext: z.ZodString;
|
|
84
|
+
}, z.core.$strip>;
|
|
85
|
+
}, z.core.$strip>;
|
|
86
|
+
expectedUpdatedAt: z.ZodOptional<z.ZodNumber>;
|
|
87
|
+
}, z.core.$strip>;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { z } from "zod";
|
|
2
|
+
import { encryptedEnvelopeSchema } from "../../../packages/shared/dist/index.js";
|
|
2
3
|
export const pairingStartSchema = z.object({
|
|
3
4
|
deviceId: z.string().min(1),
|
|
4
5
|
deviceName: z.string().min(1),
|
|
@@ -67,3 +68,7 @@ export const sessionHeadAdvanceSchema = z.object({
|
|
|
67
68
|
occurredAt: z.number().int().optional(),
|
|
68
69
|
}),
|
|
69
70
|
});
|
|
71
|
+
export const sessionMetadataUpdateSchema = z.object({
|
|
72
|
+
encryptedMetadata: encryptedEnvelopeSchema,
|
|
73
|
+
expectedUpdatedAt: z.number().int().positive().optional(),
|
|
74
|
+
});
|
|
@@ -286,6 +286,7 @@ export function createRelayContext(options) {
|
|
|
286
286
|
...(pending.cwd !== undefined ? { cwd: pending.cwd } : {}),
|
|
287
287
|
machineId: pending.machineId,
|
|
288
288
|
...(pending.name !== undefined ? { name: pending.name } : {}),
|
|
289
|
+
...(pending.promptHash !== undefined ? { promptHash: pending.promptHash } : {}),
|
|
289
290
|
...(pending.runtimeSessionId !== undefined ? { runtimeSessionId: pending.runtimeSessionId } : {}),
|
|
290
291
|
...(pending.sessionId !== undefined ? { sessionId: pending.sessionId } : {}),
|
|
291
292
|
...(pending.shell !== undefined ? { shell: pending.shell } : {}),
|
|
@@ -319,6 +320,7 @@ export function createRelayContext(options) {
|
|
|
319
320
|
...(typeof parsed.cwd === "string" ? { cwd: parsed.cwd } : {}),
|
|
320
321
|
machineId: parsed.machineId,
|
|
321
322
|
...(typeof parsed.name === "string" ? { name: parsed.name } : {}),
|
|
323
|
+
...(typeof parsed.promptHash === "string" ? { promptHash: parsed.promptHash } : {}),
|
|
322
324
|
requestId: "",
|
|
323
325
|
...(typeof parsed.runtimeSessionId === "string" ? { runtimeSessionId: parsed.runtimeSessionId } : {}),
|
|
324
326
|
...(typeof parsed.sessionId === "string" ? { sessionId: parsed.sessionId } : {}),
|
|
@@ -338,7 +338,7 @@ function handleTurnDone(context, connection, message, acknowledgeMachineMessage)
|
|
|
338
338
|
sessionId: message.sessionId,
|
|
339
339
|
machineId: connection.machineId,
|
|
340
340
|
requestTurnId: message.turnId,
|
|
341
|
-
runtimeTurnId: message.lastTurnId
|
|
341
|
+
runtimeTurnId: message.lastTurnId,
|
|
342
342
|
});
|
|
343
343
|
}
|
|
344
344
|
const runtimeTurnId = message.lastTurnId ?? message.currentHead ?? message.turnId;
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cydm/happy-elves",
|
|
3
|
-
"version": "0.1.0-beta.
|
|
3
|
+
"version": "0.1.0-beta.49",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@cydm/happy-elves",
|
|
9
|
-
"version": "0.1.0-beta.
|
|
9
|
+
"version": "0.1.0-beta.49",
|
|
10
10
|
"license": "Apache-2.0",
|
|
11
11
|
"dependencies": {
|
|
12
12
|
"@fastify/cors": "11.2.0",
|
|
@@ -1021,9 +1021,9 @@
|
|
|
1021
1021
|
}
|
|
1022
1022
|
},
|
|
1023
1023
|
"node_modules/fast-uri": {
|
|
1024
|
-
"version": "3.1.
|
|
1025
|
-
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.
|
|
1026
|
-
"integrity": "sha512-
|
|
1024
|
+
"version": "3.1.3",
|
|
1025
|
+
"resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.3.tgz",
|
|
1026
|
+
"integrity": "sha512-i70LwGWUduXqzicKXWshooq+sWL1K3WUU5rKZNG/0i3a1OSoX3HqhH5WbWwTmqWfor4urUakGPiRQcleRZTwOg==",
|
|
1027
1027
|
"funding": [
|
|
1028
1028
|
{
|
|
1029
1029
|
"type": "github",
|
package/package.json
CHANGED
|
@@ -2,6 +2,9 @@ import { type DirectoryListing, type EncryptedSessionEvent, type HistoricalSessi
|
|
|
2
2
|
import { sendCommand } from "./transport.js";
|
|
3
3
|
import { type ControllerLiveOptions, type ControllerLiveProjection } from "./live.js";
|
|
4
4
|
import type { AddControllerDeviceInput, AddControllerDeviceResult, CancelInput, CollectInput, CommandResult, ControllerClientConfig, ControllerInviteClaimInput, ControllerInviteClaimResult, ControllerInviteCreateInput, ControllerInviteCreateResult, ControllerSnapshot, CreateScopedTokenInput, CreateSessionInput, DecodedSessionEvent, DecodedSessionEventsPage, DeviceListResponse, DirectoryInput, FilePreviewInput, ForkInput, HistoricalSessionListPage, HistoricalSessionsInput, ImportHistoricalSessionInput, MachineMetadata, MachineDevExecInput, MachineDevExecResponse, MachineDiagnosticsResponse, RepairSessionHeadInput, RepairSessionHeadResult, RunOptions, ScopedTokenInfo, ScopedTokenSelf, SessionFilter, SessionMetadata, WaitInput } from "./types.js";
|
|
5
|
+
type UpdateSessionMetadataOptions = {
|
|
6
|
+
expectedUpdatedAt?: number;
|
|
7
|
+
};
|
|
5
8
|
export declare class ControllerClient {
|
|
6
9
|
readonly config: ControllerClientConfig;
|
|
7
10
|
constructor(config: ControllerClientConfig);
|
|
@@ -30,6 +33,7 @@ export declare class ControllerClient {
|
|
|
30
33
|
}>;
|
|
31
34
|
listSessions(filter?: SessionFilter): Promise<SessionSnapshot[]>;
|
|
32
35
|
getSession(sessionId: string): Promise<SessionSnapshot | undefined>;
|
|
36
|
+
updateSessionMetadata(sessionId: string, metadata: SessionMetadata, options?: UpdateSessionMetadataOptions): Promise<SessionSnapshot>;
|
|
33
37
|
history(sessionId: string, limit?: number): Promise<DecodedSessionEvent[]>;
|
|
34
38
|
createSession(input: CreateSessionInput): Promise<CommandResult>;
|
|
35
39
|
collect(sessionId: string, input?: CollectInput): Promise<DecodedSessionEvent[]>;
|
|
@@ -62,3 +66,4 @@ export declare class ControllerClient {
|
|
|
62
66
|
private mergeReplayedTurnEvents;
|
|
63
67
|
private sessionEvents;
|
|
64
68
|
}
|
|
69
|
+
export {};
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { decryptJson, encryptJson, normalizeSessionName, parseServerMessage, randomId, } from "../../shared/dist/index.js";
|
|
2
2
|
import { addControllerDevice, claimControllerInvite, createControllerInvite, createPairingCode, createScopedToken, listDevices, listScopedTokens, revokeDevice, revokeScopedToken, tokenSelf, } from "./account.js";
|
|
3
3
|
import { ControllerClientError, ControllerCommandError } from "./errors.js";
|
|
4
|
-
import { authenticatedJson, normalizeRelayUrl, readRelayJson, relayHttpError } from "./http.js";
|
|
4
|
+
import { authenticatedJson, normalizeRelayUrl, readRelayJson, relayFetch, relayHttpError } from "./http.js";
|
|
5
5
|
import { parseRepairSessionHeadResult, parseSessionEventsResponse, } from "./parsers.js";
|
|
6
6
|
import { sendAndWaitForDirectoryListing, sendAndWaitForFilePreview, sendAndWaitForHistoricalSessions, sendAndWaitForMachineDevExecResult, sendAndWaitForMachineDiagnostics, sendCommand, } from "./transport.js";
|
|
7
7
|
import { ControllerLiveProjectionImpl } from "./live.js";
|
|
8
8
|
import { DEFAULT_SESSION_CREATE_WAIT_MS, } from "./types.js";
|
|
9
|
-
import { isWaitCondition, matchesWaitCondition, parseEventCursor, parseEventLimit, parsePositiveDuration, requireNonEmpty, sleep, } from "./validation.js";
|
|
9
|
+
import { isWaitCondition, matchesWaitCondition, parseEventCursor, parseEventLimit, parsePositiveDuration, parseSinceEventCursor, requireNonEmpty, sleep, } from "./validation.js";
|
|
10
10
|
const DEFAULT_RUN_SETTLEMENT_WAIT_MS = 15_000;
|
|
11
11
|
const DEFAULT_BLOCKING_RUN_WAIT_MS = 30 * 60 * 1000;
|
|
12
12
|
function runSettlementTimeoutMs(options) {
|
|
@@ -16,6 +16,17 @@ function runSettlementTimeoutMs(options) {
|
|
|
16
16
|
return options.runtimeTimeoutMs + DEFAULT_RUN_SETTLEMENT_WAIT_MS;
|
|
17
17
|
return DEFAULT_BLOCKING_RUN_WAIT_MS;
|
|
18
18
|
}
|
|
19
|
+
function parseSessionMetadataUpdateResult(value) {
|
|
20
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
21
|
+
throw new ControllerClientError("Session metadata response is invalid", "RELAY_RESPONSE_INVALID");
|
|
22
|
+
}
|
|
23
|
+
const session = value.session;
|
|
24
|
+
const message = parseServerMessage({ type: "server:session", session });
|
|
25
|
+
if (message.type !== "server:session") {
|
|
26
|
+
throw new ControllerClientError("Session metadata response is invalid", "RELAY_RESPONSE_INVALID");
|
|
27
|
+
}
|
|
28
|
+
return message.session;
|
|
29
|
+
}
|
|
19
30
|
export class ControllerClient {
|
|
20
31
|
config;
|
|
21
32
|
constructor(config) {
|
|
@@ -27,9 +38,9 @@ export class ControllerClient {
|
|
|
27
38
|
};
|
|
28
39
|
}
|
|
29
40
|
async snapshot() {
|
|
30
|
-
const response = await
|
|
41
|
+
const response = await relayFetch(`${this.config.relayUrl}/api/bootstrap`, {
|
|
31
42
|
headers: { authorization: `Bearer ${this.config.controllerToken}` },
|
|
32
|
-
});
|
|
43
|
+
}, "Could not reach relay");
|
|
33
44
|
if (!response.ok) {
|
|
34
45
|
throw await relayHttpError(response, "Bootstrap failed");
|
|
35
46
|
}
|
|
@@ -99,6 +110,19 @@ export class ControllerClient {
|
|
|
99
110
|
const checkedSessionId = requireNonEmpty(sessionId, "sessionId");
|
|
100
111
|
return (await this.snapshot()).sessions.find((session) => session.id === checkedSessionId);
|
|
101
112
|
}
|
|
113
|
+
async updateSessionMetadata(sessionId, metadata, options = {}) {
|
|
114
|
+
const checkedSessionId = requireNonEmpty(sessionId, "sessionId");
|
|
115
|
+
const requestId = randomId("req");
|
|
116
|
+
const encryptedMetadata = await encryptJson(this.config.accountSecret, `session:metadata:${checkedSessionId}:${requestId}`, metadata);
|
|
117
|
+
return await authenticatedJson(this.config, `/api/sessions/${encodeURIComponent(checkedSessionId)}/metadata`, {
|
|
118
|
+
method: "PATCH",
|
|
119
|
+
headers: { "content-type": "application/json" },
|
|
120
|
+
body: JSON.stringify({
|
|
121
|
+
encryptedMetadata,
|
|
122
|
+
...(options.expectedUpdatedAt === undefined ? {} : { expectedUpdatedAt: options.expectedUpdatedAt }),
|
|
123
|
+
}),
|
|
124
|
+
}, parseSessionMetadataUpdateResult);
|
|
125
|
+
}
|
|
102
126
|
async history(sessionId, limit = 20) {
|
|
103
127
|
const checkedSessionId = requireNonEmpty(sessionId, "sessionId");
|
|
104
128
|
const { events } = await this.sessionEvents(checkedSessionId, { limit: parseEventLimit(limit) });
|
|
@@ -142,7 +166,7 @@ export class ControllerClient {
|
|
|
142
166
|
const checkedSessionId = requireNonEmpty(sessionId, "sessionId");
|
|
143
167
|
const before = parseEventCursor(input.before);
|
|
144
168
|
const beforeCursor = input.beforeCursor;
|
|
145
|
-
const since =
|
|
169
|
+
const since = parseSinceEventCursor(input.since);
|
|
146
170
|
const limit = parseEventLimit(input.limit);
|
|
147
171
|
const turnId = input.turnId === undefined ? undefined : requireNonEmpty(input.turnId, "turnId");
|
|
148
172
|
if (before !== undefined && beforeCursor !== undefined) {
|
|
@@ -154,6 +178,9 @@ export class ControllerClient {
|
|
|
154
178
|
if (turnId !== undefined && (before !== undefined || beforeCursor !== undefined)) {
|
|
155
179
|
throw new ControllerClientError("turnId cannot be combined with before cursors", "INVALID_ARGUMENT");
|
|
156
180
|
}
|
|
181
|
+
if (turnId === undefined && typeof since === "string") {
|
|
182
|
+
throw new ControllerClientError("Logical since cursor requires turnId", "INVALID_ARGUMENT");
|
|
183
|
+
}
|
|
157
184
|
const page = await this.sessionEvents(checkedSessionId, { before, beforeCursor, since, limit, turnId });
|
|
158
185
|
const requestedLimit = limit ?? 20;
|
|
159
186
|
const hasMoreBefore = page.hasMoreBefore ?? (since === undefined && page.events.length >= requestedLimit);
|
|
@@ -374,8 +401,8 @@ export class ControllerClient {
|
|
|
374
401
|
async run(sessionId, text, options = {}) {
|
|
375
402
|
const checkedSessionId = requireNonEmpty(sessionId, "sessionId");
|
|
376
403
|
const promptText = requireNonEmpty(text, "text");
|
|
377
|
-
const requestId = randomId("req");
|
|
378
|
-
const turnId = randomId("turn");
|
|
404
|
+
const requestId = options.requestId === undefined ? randomId("req") : requireNonEmpty(options.requestId, "requestId");
|
|
405
|
+
const turnId = options.turnId === undefined ? randomId("turn") : requireNonEmpty(options.turnId, "turnId");
|
|
379
406
|
const wait = options.wait ?? true;
|
|
380
407
|
const timeoutMs = wait ? options.waitTimeoutMs : options.ackTimeoutMs;
|
|
381
408
|
const result = await this.sendCommand({
|
|
@@ -387,6 +414,7 @@ export class ControllerClient {
|
|
|
387
414
|
type: "user_prompt",
|
|
388
415
|
text: promptText,
|
|
389
416
|
}),
|
|
417
|
+
...(options.promptHash === undefined ? {} : { promptHash: requireNonEmpty(options.promptHash, "promptHash") }),
|
|
390
418
|
permissionMode: options.permissionMode ?? "approve-reads",
|
|
391
419
|
timeoutMs: options.runtimeTimeoutMs,
|
|
392
420
|
}, wait, timeoutMs);
|
|
@@ -2,6 +2,7 @@ import { ControllerClientError } from "./errors.js";
|
|
|
2
2
|
import type { ControllerClientConfig } from "./types.js";
|
|
3
3
|
export declare function normalizeRelayUrl(value: string): string;
|
|
4
4
|
export declare function wsUrl(relayUrl: string): string;
|
|
5
|
+
export declare function relayFetch(input: string, init?: RequestInit, fallback?: string): Promise<Response>;
|
|
5
6
|
export declare function relayHttpError(response: Response, fallback: string): Promise<ControllerClientError>;
|
|
6
7
|
export declare function readRelayJson(response: Response, fallback: string): Promise<unknown>;
|
|
7
8
|
export declare function authenticatedJson<T>(config: ControllerClientConfig, pathname: string, init: RequestInit, parse: (value: unknown) => T): Promise<T>;
|
|
@@ -24,6 +24,18 @@ export function wsUrl(relayUrl) {
|
|
|
24
24
|
url.search = "";
|
|
25
25
|
return url.toString();
|
|
26
26
|
}
|
|
27
|
+
function networkErrorMessage(error) {
|
|
28
|
+
const message = error instanceof Error && error.message ? error.message : String(error);
|
|
29
|
+
return message.replace(/(https?:\/\/)([^/@\s]+)@/gu, "$1");
|
|
30
|
+
}
|
|
31
|
+
export async function relayFetch(input, init = {}, fallback = "Could not reach relay") {
|
|
32
|
+
try {
|
|
33
|
+
return await fetch(input, init);
|
|
34
|
+
}
|
|
35
|
+
catch (error) {
|
|
36
|
+
throw new ControllerClientError(`${fallback}: ${networkErrorMessage(error)}`, "RELAY_UNREACHABLE");
|
|
37
|
+
}
|
|
38
|
+
}
|
|
27
39
|
export async function relayHttpError(response, fallback) {
|
|
28
40
|
const text = await response.text();
|
|
29
41
|
try {
|
|
@@ -60,20 +72,20 @@ export async function authenticatedJson(config, pathname, init, parse) {
|
|
|
60
72
|
}
|
|
61
73
|
}
|
|
62
74
|
export async function authenticatedJsonRaw(config, pathname, init) {
|
|
63
|
-
const response = await
|
|
75
|
+
const response = await relayFetch(`${config.relayUrl}${pathname}`, {
|
|
64
76
|
...init,
|
|
65
77
|
headers: {
|
|
66
78
|
...init.headers,
|
|
67
79
|
authorization: `Bearer ${config.controllerToken}`,
|
|
68
80
|
},
|
|
69
|
-
});
|
|
81
|
+
}, "Could not reach relay");
|
|
70
82
|
if (!response.ok) {
|
|
71
83
|
throw await relayHttpError(response, `${pathname} failed`);
|
|
72
84
|
}
|
|
73
85
|
return await readRelayJson(response, `${pathname} response is not valid JSON`);
|
|
74
86
|
}
|
|
75
87
|
export async function publicJsonRaw(config, pathname, init) {
|
|
76
|
-
const response = await
|
|
88
|
+
const response = await relayFetch(`${config.relayUrl}${pathname}`, init, "Could not reach relay");
|
|
77
89
|
if (!response.ok) {
|
|
78
90
|
throw await relayHttpError(response, `${pathname} failed`);
|
|
79
91
|
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { DecodedSessionEvent, SessionMetadata } from "./types.js";
|
|
2
|
+
import type { SessionSnapshot } from "../../shared/dist/index.js";
|
|
3
|
+
export declare function isAuthoritativeSessionModel(session: SessionSnapshot | undefined): boolean;
|
|
4
|
+
export declare function selectedSessionHistoryHead(session: SessionSnapshot): string;
|
|
5
|
+
export declare function selectedSessionProjectionHead(session: SessionSnapshot): string;
|
|
6
|
+
export declare function hasVisibleTranscriptPayload(event: DecodedSessionEvent): boolean;
|
|
7
|
+
export declare function isSelectedSessionHeadMaterialized(session: SessionSnapshot, events: readonly DecodedSessionEvent[]): boolean;
|
|
8
|
+
export declare function oldestEventIdForSession(events: readonly DecodedSessionEvent[], sessionId: string): number | undefined;
|
|
9
|
+
export declare function newestEventIdForSession(events: readonly DecodedSessionEvent[], sessionId: string): number;
|
|
10
|
+
export declare function sessionEventCount(events: readonly DecodedSessionEvent[], sessionId: string): number;
|
|
11
|
+
export declare function sessionHasHistoricalBackfillEvents(events: readonly DecodedSessionEvent[], sessionId: string): boolean;
|
|
12
|
+
export declare function sessionHasMissingHistoricalPrefix(session: SessionSnapshot | undefined, events: readonly DecodedSessionEvent[]): boolean;
|
|
13
|
+
export declare function runtimeHistoricalBackfillCanContinue(metadata: SessionMetadata | undefined, hasMissingHistoricalPrefix: boolean): boolean;
|
|
14
|
+
export declare function runtimeHistoricalBackfillKnown(metadata: SessionMetadata | undefined): boolean;
|
|
15
|
+
export declare function runtimeHistoricalBackfillCompleted(metadata: SessionMetadata | undefined): boolean;
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { oldestSessionEventId } from "./session-event-order.js";
|
|
2
|
+
export function isAuthoritativeSessionModel(session) {
|
|
3
|
+
return session?.historyModel === "authoritative-linear";
|
|
4
|
+
}
|
|
5
|
+
export function selectedSessionHistoryHead(session) {
|
|
6
|
+
return [session.currentHead ?? "", session.lastTurnId ?? "", session.status, String(session.updatedAt)].join(":");
|
|
7
|
+
}
|
|
8
|
+
export function selectedSessionProjectionHead(session) {
|
|
9
|
+
return [session.currentHead ?? "", session.lastTurnId ?? ""].join(":");
|
|
10
|
+
}
|
|
11
|
+
export function hasVisibleTranscriptPayload(event) {
|
|
12
|
+
const type = event.decoded?.type;
|
|
13
|
+
return type === "user_prompt" || type === "text_delta" || type === "tool_call";
|
|
14
|
+
}
|
|
15
|
+
export function isSelectedSessionHeadMaterialized(session, events) {
|
|
16
|
+
const currentHead = session.currentHead || session.lastTurnId;
|
|
17
|
+
if (!currentHead)
|
|
18
|
+
return true;
|
|
19
|
+
const localEvents = events.filter((event) => event.sessionId === session.id);
|
|
20
|
+
const eventIdMatch = /^event:(\d+)$/.exec(currentHead);
|
|
21
|
+
if (eventIdMatch)
|
|
22
|
+
return localEvents.some((event) => event.id === Number(eventIdMatch[1]));
|
|
23
|
+
return localEvents.some((event) => event.turnId === currentHead ||
|
|
24
|
+
(event.decoded?.type === "done" && (event.decoded.currentHead === currentHead || event.decoded.lastTurnId === currentHead)));
|
|
25
|
+
}
|
|
26
|
+
export function oldestEventIdForSession(events, sessionId) {
|
|
27
|
+
return oldestSessionEventId(events, sessionId);
|
|
28
|
+
}
|
|
29
|
+
export function newestEventIdForSession(events, sessionId) {
|
|
30
|
+
return Math.max(0, ...events.filter((event) => event.sessionId === sessionId).map((event) => event.id));
|
|
31
|
+
}
|
|
32
|
+
export function sessionEventCount(events, sessionId) {
|
|
33
|
+
return events.filter((event) => event.sessionId === sessionId).length;
|
|
34
|
+
}
|
|
35
|
+
export function sessionHasHistoricalBackfillEvents(events, sessionId) {
|
|
36
|
+
return events.some((event) => event.sessionId === sessionId && event.messageId?.startsWith("hist_"));
|
|
37
|
+
}
|
|
38
|
+
export function sessionHasMissingHistoricalPrefix(session, events) {
|
|
39
|
+
if (!session?.currentHead && !session?.lastTurnId)
|
|
40
|
+
return false;
|
|
41
|
+
const turnSeqs = events
|
|
42
|
+
.filter((event) => event.sessionId === session.id && event.messageId?.startsWith("hist_") && typeof event.turnSeq === "number")
|
|
43
|
+
.map((event) => event.turnSeq);
|
|
44
|
+
return turnSeqs.length > 0 && Math.min(...turnSeqs) > 0;
|
|
45
|
+
}
|
|
46
|
+
export function runtimeHistoricalBackfillCanContinue(metadata, hasMissingHistoricalPrefix) {
|
|
47
|
+
const status = metadata?.historicalBackfillStatus ?? metadata?.historicalBackfill;
|
|
48
|
+
return status === "partial" || metadata?.truncatedHistoricalBackfill === true || hasMissingHistoricalPrefix;
|
|
49
|
+
}
|
|
50
|
+
export function runtimeHistoricalBackfillKnown(metadata) {
|
|
51
|
+
return Boolean(metadata &&
|
|
52
|
+
(metadata.historicalBackfillStatus !== undefined ||
|
|
53
|
+
metadata.historicalBackfill !== undefined ||
|
|
54
|
+
metadata.historicalBackfillCursor !== undefined ||
|
|
55
|
+
metadata.truncatedHistoricalBackfill !== undefined));
|
|
56
|
+
}
|
|
57
|
+
export function runtimeHistoricalBackfillCompleted(metadata) {
|
|
58
|
+
const status = metadata?.historicalBackfillStatus ?? metadata?.historicalBackfill;
|
|
59
|
+
return status === "completed" || metadata?.historicalBackfillCursor === 0;
|
|
60
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import type { ControllerLiveState } from "./live-types.js";
|
|
2
|
+
export declare function cloneLiveState(state: ControllerLiveState): ControllerLiveState;
|
|
3
|
+
export declare function upsertById<T extends {
|
|
4
|
+
id: string;
|
|
5
|
+
}>(items: readonly T[], next: T): T[];
|
|
6
|
+
export declare function sleep(ms: number): Promise<void>;
|
|
7
|
+
export declare function nonEmptyLiveString(value: string, label: string): string;
|
|
8
|
+
export declare function errorMessage(error: unknown): string;
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ControllerClientError } from "./errors.js";
|
|
2
|
+
export function cloneLiveState(state) {
|
|
3
|
+
return {
|
|
4
|
+
...state,
|
|
5
|
+
events: [...state.events],
|
|
6
|
+
machineMetadata: { ...state.machineMetadata },
|
|
7
|
+
machines: [...state.machines],
|
|
8
|
+
sessionMetadata: { ...state.sessionMetadata },
|
|
9
|
+
sessionPages: Object.fromEntries(Object.entries(state.sessionPages).map(([key, value]) => [key, { ...value }])),
|
|
10
|
+
sessions: [...state.sessions],
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
export function upsertById(items, next) {
|
|
14
|
+
const copy = [...items];
|
|
15
|
+
const index = copy.findIndex((item) => item.id === next.id);
|
|
16
|
+
if (index >= 0)
|
|
17
|
+
copy[index] = next;
|
|
18
|
+
else
|
|
19
|
+
copy.unshift(next);
|
|
20
|
+
return copy;
|
|
21
|
+
}
|
|
22
|
+
export function sleep(ms) {
|
|
23
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
24
|
+
}
|
|
25
|
+
export function nonEmptyLiveString(value, label) {
|
|
26
|
+
const trimmed = value.trim();
|
|
27
|
+
if (!trimmed)
|
|
28
|
+
throw new ControllerClientError(`${label} must be a non-empty string`, "INVALID_ARGUMENT");
|
|
29
|
+
return trimmed;
|
|
30
|
+
}
|
|
31
|
+
export function errorMessage(error) {
|
|
32
|
+
return error instanceof Error ? error.message : String(error);
|
|
33
|
+
}
|