@cydm/happy-elves 0.1.0-beta.486 → 0.1.0-beta.488
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/lib/args.js +4 -0
- package/apps/cli/dist/commands/lib/usage.js +3 -3
- package/apps/cli/dist/commands/lib/workspace.d.ts +6 -1
- package/apps/cli/dist/commands/lib/workspace.js +5 -0
- package/apps/cli/dist/commands/session.js +54 -5
- package/apps/cli/package.json +1 -1
- package/apps/daemon/dist/orchestration/crew-queue.js +33 -2
- package/apps/daemon/dist/orchestration/handler.d.ts +5 -1
- package/apps/daemon/dist/orchestration/handler.js +64 -3
- package/apps/daemon/dist/orchestration/store.d.ts +1 -0
- package/apps/daemon/dist/orchestration/store.js +12 -0
- package/apps/daemon/dist/relay/connection.js +64 -3
- package/apps/daemon/dist/relay/remote-command.js +30 -3
- package/apps/daemon/dist/relay/send.d.ts +1 -0
- package/apps/daemon/dist/relay/send.js +229 -20
- package/apps/daemon/dist/runtime/headless-event-projector.d.ts +6 -1
- package/apps/daemon/dist/runtime/headless-event-projector.js +33 -7
- package/apps/daemon/dist/runtime/headless-manager.js +39 -4
- package/apps/daemon/dist/runtime/headless-session-history.js +20 -1
- package/apps/daemon/dist/session/he-official/artifact-commands.js +9 -7
- package/apps/daemon/dist/session/he-official/common.d.ts +30 -2
- package/apps/daemon/dist/session/he-official/common.js +26 -11
- package/apps/daemon/dist/session/he-official/orchestration-commands.d.ts +3 -2
- package/apps/daemon/dist/session/he-official/orchestration-commands.js +54 -7
- package/apps/daemon/dist/session/he-official/session-commands.d.ts +2 -2
- package/apps/daemon/dist/session/he-official/session-commands.js +197 -6
- package/apps/daemon/dist/session/he-official/turn-commands.d.ts +12 -3
- package/apps/daemon/dist/session/he-official/turn-commands.js +78 -3
- package/apps/daemon/dist/session/he-official-tools.js +11 -6
- package/apps/daemon/dist/session/lifecycle.js +103 -4
- package/apps/daemon/dist/session/native-identity.js +17 -1
- package/apps/daemon/dist/session/prompt.d.ts +1 -0
- package/apps/daemon/dist/session/prompt.js +20 -5
- package/apps/daemon/dist/state.d.ts +5 -1
- package/apps/daemon/dist/state.js +28 -0
- package/apps/daemon/package.json +1 -1
- package/apps/relay/dist/http-routes.js +74 -78
- package/apps/relay/dist/index.js +12 -4
- package/apps/relay/dist/packet-store.js +9 -2
- package/apps/relay/dist/relay-context.d.ts +213 -3
- package/apps/relay/dist/relay-context.js +380 -6
- package/apps/relay/dist/relay-migration.d.ts +25 -2
- package/apps/relay/dist/relay-migration.js +27 -112
- package/apps/relay/dist/retention.d.ts +10 -0
- package/apps/relay/dist/retention.js +20 -0
- package/apps/relay/dist/storage-worker.d.ts +137 -0
- package/apps/relay/dist/storage-worker.js +490 -0
- package/apps/relay/dist/websocket.d.ts +4 -0
- package/apps/relay/dist/websocket.js +259 -106
- package/package.json +1 -1
- package/packages/client/dist/client.js +6 -1
- package/packages/client/dist/types.d.ts +1 -0
- package/packages/client/dist/workspace-shared-config.js +8 -3
- package/packages/headless-runtime/dist/codex-provider.js +76 -18
- package/packages/headless-runtime/dist/core.d.ts +1 -0
- package/packages/headless-runtime/dist/core.js +134 -6
- package/packages/shared/dist/he-official-tools.js +5 -5
- package/packages/shared/dist/packet-relay.d.ts +2 -0
- package/packages/shared/dist/packet-relay.js +1 -0
- package/packages/shared/dist/protocol-schemas.d.ts +30 -9
- package/packages/shared/dist/protocol-schemas.js +11 -0
- package/packages/shared/dist/protocol-types.d.ts +17 -0
- package/packages/shared/dist/protocol.d.ts +4 -1
- package/packages/shared/dist/protocol.js +4 -1
|
@@ -33,12 +33,14 @@ const valueFlags = new Set([
|
|
|
33
33
|
"limit",
|
|
34
34
|
"machine",
|
|
35
35
|
"machine-id",
|
|
36
|
+
"native-identity",
|
|
36
37
|
"memory",
|
|
37
38
|
"memory-mode",
|
|
38
39
|
"message-id",
|
|
39
40
|
"memory-project",
|
|
40
41
|
"memory-top-n",
|
|
41
42
|
"name",
|
|
43
|
+
"observed-at",
|
|
42
44
|
"model",
|
|
43
45
|
"thinking-level",
|
|
44
46
|
"mode",
|
|
@@ -69,6 +71,7 @@ const valueFlags = new Set([
|
|
|
69
71
|
"allow-sender",
|
|
70
72
|
"expires-in",
|
|
71
73
|
"format",
|
|
74
|
+
"history-version",
|
|
72
75
|
"loops",
|
|
73
76
|
"machines",
|
|
74
77
|
"sessions",
|
|
@@ -112,6 +115,7 @@ const booleanFlags = new Set([
|
|
|
112
115
|
"allow-any-sender",
|
|
113
116
|
"app-secret-stdin",
|
|
114
117
|
"confirm-risk",
|
|
118
|
+
"confirm-native-handoff",
|
|
115
119
|
"copy-tags",
|
|
116
120
|
"detach",
|
|
117
121
|
"disable-reaction",
|
|
@@ -53,7 +53,7 @@ Check local controller config, relay reachability, and daemon state.
|
|
|
53
53
|
happy-elves session tag remove <sessionId> --tag <tag> --json
|
|
54
54
|
happy-elves session tag list <sessionId> --json
|
|
55
55
|
happy-elves session history --machine <machineId> [--cwd <path>] [--agent <agent>] [--include-archived] [--limit 20] --json
|
|
56
|
-
happy-elves session continue <runtimeSessionId> --machine <machineId> [--name <name>] --json
|
|
56
|
+
happy-elves session continue <runtimeSessionId> --machine <machineId> [--name <name>] [--confirm-native-handoff] [--observed-at <ms>] [--history-version <version>] --json
|
|
57
57
|
happy-elves session status <sessionId> [--verbose] [--json]
|
|
58
58
|
happy-elves session diagnose <sessionId> [--limit 200] [--repair-head] [--json]
|
|
59
59
|
happy-elves session close <sessionId> [--reason <reason>] --json
|
|
@@ -271,7 +271,7 @@ Core commands:
|
|
|
271
271
|
session list [--machine <machineId>] [--cwd <path>] [--limit 20] [--json]
|
|
272
272
|
session create --machine <machineId> [--agent codex] [--cwd <path>] [--name <name>] --json
|
|
273
273
|
session history --machine <machineId> [--cwd <path>] [--limit 20] --json
|
|
274
|
-
session continue <runtimeSessionId> --machine <machineId> --json
|
|
274
|
+
session continue <runtimeSessionId> --machine <machineId> [--confirm-native-handoff] [--observed-at <ms>] [--history-version <version>] --json
|
|
275
275
|
session status <sessionId> [--verbose] [--json]
|
|
276
276
|
session diagnose <sessionId> [--limit 200] [--repair-head] [--json]
|
|
277
277
|
session close <sessionId> [--reason <reason>] --json
|
|
@@ -326,7 +326,7 @@ Usage:
|
|
|
326
326
|
|
|
327
327
|
Common flows:
|
|
328
328
|
Discover: machine list --json; session list --machine <machineId> --json
|
|
329
|
-
Continue: session history --machine <machineId> --json; session continue <runtimeSessionId> --machine <machineId> --json
|
|
329
|
+
Continue: session history --machine <machineId> --json; session continue <runtimeSessionId> --machine <machineId> --confirm-native-handoff --json
|
|
330
330
|
Execute: turn run <sessionId> --text <prompt> --json; turn result <sessionId> <turnId> --json
|
|
331
331
|
Branch: turn rewind <sessionId> <turnId> --json; turn fork <sessionId> <turnId> --name <name> --json
|
|
332
332
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ControllerClient } from "../../../../../packages/client/dist/index.js";
|
|
2
|
-
import type { HistoricalSessionSnapshot } from "../../../../../packages/shared/dist/index.js";
|
|
2
|
+
import type { HistoricalSessionSnapshot, HistoricalSessionWarning } from "../../../../../packages/shared/dist/index.js";
|
|
3
3
|
export type WorkspaceHistoricalSession = HistoricalSessionSnapshot & {
|
|
4
4
|
machineId: string;
|
|
5
5
|
machineName: string;
|
|
@@ -18,6 +18,11 @@ export declare function listWorkspaceHistoricalSessions(client: ControllerClient
|
|
|
18
18
|
code?: string;
|
|
19
19
|
message: string;
|
|
20
20
|
}>;
|
|
21
|
+
warnings: Array<{
|
|
22
|
+
machineId: string;
|
|
23
|
+
machineName: string;
|
|
24
|
+
warning: HistoricalSessionWarning;
|
|
25
|
+
}>;
|
|
21
26
|
machines: Array<{
|
|
22
27
|
machineId: string;
|
|
23
28
|
machineName: string;
|
|
@@ -18,6 +18,7 @@ export async function listWorkspaceHistoricalSessions(client, input) {
|
|
|
18
18
|
const machineName = projected.metadata.name ?? projected.metadata.hostname ?? machine.name;
|
|
19
19
|
try {
|
|
20
20
|
const sessions = [];
|
|
21
|
+
const warnings = [];
|
|
21
22
|
let cursor;
|
|
22
23
|
const seenCursors = new Set();
|
|
23
24
|
while (sessions.length < input.limit) {
|
|
@@ -30,6 +31,7 @@ export async function listWorkspaceHistoricalSessions(client, input) {
|
|
|
30
31
|
cursor,
|
|
31
32
|
});
|
|
32
33
|
sessions.push(...page.sessions);
|
|
34
|
+
warnings.push(...(page.warnings ?? []));
|
|
33
35
|
if (!page.hasMore || !page.nextCursor)
|
|
34
36
|
break;
|
|
35
37
|
if (seenCursors.has(page.nextCursor))
|
|
@@ -42,6 +44,7 @@ export async function listWorkspaceHistoricalSessions(client, input) {
|
|
|
42
44
|
.filter((session) => input.includeArchived === true || session.runtimeState !== "archived")
|
|
43
45
|
.map((session) => ({ ...session, machineId: machine.id, machineName })),
|
|
44
46
|
error: undefined,
|
|
47
|
+
warnings,
|
|
45
48
|
machine: { machineId: machine.id, machineName },
|
|
46
49
|
};
|
|
47
50
|
}
|
|
@@ -55,6 +58,7 @@ export async function listWorkspaceHistoricalSessions(client, input) {
|
|
|
55
58
|
code: enriched.code,
|
|
56
59
|
message: error instanceof Error ? error.message : String(error),
|
|
57
60
|
},
|
|
61
|
+
warnings: [],
|
|
58
62
|
machine: { machineId: machine.id, machineName },
|
|
59
63
|
};
|
|
60
64
|
}
|
|
@@ -65,6 +69,7 @@ export async function listWorkspaceHistoricalSessions(client, input) {
|
|
|
65
69
|
.sort((left, right) => right.updatedAt - left.updatedAt || left.runtimeSessionId.localeCompare(right.runtimeSessionId))
|
|
66
70
|
.slice(0, input.limit)),
|
|
67
71
|
errors: rows.flatMap((row) => row.error ? [row.error] : []),
|
|
72
|
+
warnings: rows.flatMap((row) => row.warnings.map((warning) => ({ machineId: row.machine.machineId, machineName: row.machine.machineName, warning }))),
|
|
68
73
|
machines: rows.map((row) => row.machine),
|
|
69
74
|
};
|
|
70
75
|
}
|
|
@@ -546,13 +546,23 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
546
546
|
returned: result.sessions.length,
|
|
547
547
|
hasMore: result.sessions.length >= limit,
|
|
548
548
|
},
|
|
549
|
+
warnings: result.warnings,
|
|
549
550
|
...(wantsVerbose(flags) ? { errors: result.errors, machines: result.machines } : {}),
|
|
550
551
|
}, { machineId });
|
|
551
552
|
}
|
|
552
553
|
else if (sessions.length === 0) {
|
|
554
|
+
for (const { machineName, warning } of result.warnings) {
|
|
555
|
+
console.log(`Warning: ${machineName} · ${warning.provider}: ${warning.message}`);
|
|
556
|
+
}
|
|
557
|
+
for (const error of result.errors) {
|
|
558
|
+
console.log(`Warning: ${error.machineName}: ${error.message}`);
|
|
559
|
+
}
|
|
553
560
|
console.log("No runtime historical sessions found.");
|
|
554
561
|
}
|
|
555
562
|
else {
|
|
563
|
+
for (const { machineName, warning } of result.warnings) {
|
|
564
|
+
console.log(`Warning: ${machineName} · ${warning.provider}: ${warning.message}`);
|
|
565
|
+
}
|
|
556
566
|
for (const session of sessions) {
|
|
557
567
|
const loaded = session.sessionId ?? "-";
|
|
558
568
|
const state = session.runtimeState === "archived" ? " archived" : "";
|
|
@@ -576,10 +586,25 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
576
586
|
includeArchived: true,
|
|
577
587
|
limit: 200,
|
|
578
588
|
});
|
|
579
|
-
const historical = result.sessions.find((session) => session.runtimeSessionId === runtimeSessionId)
|
|
580
|
-
|
|
589
|
+
const historical = result.sessions.find((session) => session.runtimeSessionId === runtimeSessionId)
|
|
590
|
+
?? result.sessions.find((session) => session.providerSessionId === runtimeSessionId && session.nativeIdentity);
|
|
591
|
+
if (!historical) {
|
|
592
|
+
const warning = result.warnings[0]?.warning;
|
|
593
|
+
if (warning) {
|
|
594
|
+
const code = warning.code === "permission-denied" ? "PROVIDER_PERMISSION_DENIED"
|
|
595
|
+
: warning.code === "unsupported" ? "PROVIDER_CAPABILITY_UNAVAILABLE"
|
|
596
|
+
: "PROVIDER_UNAVAILABLE";
|
|
597
|
+
throw new CliError(`${warning.provider}: ${warning.message}`, code);
|
|
598
|
+
}
|
|
581
599
|
throw new CliError(`Runtime session not found: ${runtimeSessionId}`, "RUNTIME_SESSION_NOT_FOUND");
|
|
582
|
-
|
|
600
|
+
}
|
|
601
|
+
// Imported native sessions are represented by a stable HE runtime id in
|
|
602
|
+
// the canonical headless row, while the provider UUID remains the
|
|
603
|
+
// durable native provenance. Accept either id, but retain the native
|
|
604
|
+
// handoff path whenever that provenance is present.
|
|
605
|
+
const isNative = historical.source === "native" || Boolean(historical.nativeIdentity && historical.providerSessionId);
|
|
606
|
+
const resolvedRuntimeSessionId = historical.runtimeSessionId;
|
|
607
|
+
if (historical.loadedSessionId && !isNative) {
|
|
583
608
|
ok("session.continue", {
|
|
584
609
|
sessionId: historical.loadedSessionId,
|
|
585
610
|
runtimeSessionId,
|
|
@@ -591,19 +616,43 @@ export async function handleSession({ domain, action, positional, flags }) {
|
|
|
591
616
|
const name = typeof flags.name === "string" && flags.name.trim()
|
|
592
617
|
? flags.name.trim()
|
|
593
618
|
: historical.name?.trim() || historical.summary?.trim() || historical.runtimeSessionName?.trim() || runtimeSessionId;
|
|
619
|
+
if (isNative && flags["confirm-native-handoff"] !== true && flags.manual !== true) {
|
|
620
|
+
throw new CliError("Native session takeover requires explicit confirmation. Re-run with --confirm-native-handoff (and optionally --observed-at/--history-version from this listing).", "NATIVE_HANDOFF_CONFIRMATION_REQUIRED");
|
|
621
|
+
}
|
|
622
|
+
const observedAtValue = flags["observed-at"];
|
|
623
|
+
const observedAt = observedAtValue === undefined || observedAtValue === true ? Date.now() : Number(observedAtValue);
|
|
624
|
+
if (!Number.isSafeInteger(observedAt) || observedAt <= 0)
|
|
625
|
+
throw new CliError("Invalid --observed-at", "INVALID_ARGUMENT");
|
|
626
|
+
const suppliedIdentity = typeof flags["native-identity"] === "string" ? flags["native-identity"].trim() : historical.nativeIdentity;
|
|
627
|
+
const suppliedHistoryVersion = typeof flags["history-version"] === "string" ? flags["history-version"].trim() : historical.nativeHistoryVersion;
|
|
628
|
+
const nativeIdentity = suppliedIdentity;
|
|
629
|
+
if (isNative && !nativeIdentity)
|
|
630
|
+
throw new CliError("Native session listing did not include a handoff identity; refresh the history listing.", "NATIVE_HANDOFF_PROOF_MISSING");
|
|
631
|
+
if (isNative && historical.nativeIdentity && nativeIdentity !== historical.nativeIdentity)
|
|
632
|
+
throw new CliError("--native-identity does not match the freshly listed native session.", "NATIVE_HANDOFF_RECONFIRM_REQUIRED");
|
|
633
|
+
if (isNative && historical.nativeHistoryVersion && suppliedHistoryVersion !== historical.nativeHistoryVersion)
|
|
634
|
+
throw new CliError("--history-version does not match the freshly listed native session.", "NATIVE_HANDOFF_RECONFIRM_REQUIRED");
|
|
635
|
+
const manualHandoffProof = isNative && nativeIdentity
|
|
636
|
+
? {
|
|
637
|
+
identity: nativeIdentity,
|
|
638
|
+
observedAt,
|
|
639
|
+
...(suppliedHistoryVersion ? { historyVersion: suppliedHistoryVersion } : {}),
|
|
640
|
+
}
|
|
641
|
+
: undefined;
|
|
594
642
|
const continued = await client.importHistoricalSession({
|
|
595
643
|
machineId,
|
|
596
644
|
agent: historical.agent,
|
|
597
645
|
cwd: historical.cwd,
|
|
598
|
-
runtimeSessionId,
|
|
646
|
+
runtimeSessionId: resolvedRuntimeSessionId,
|
|
599
647
|
name,
|
|
600
648
|
...(historical.name ? { providerName: historical.name } : {}),
|
|
601
649
|
...(historical.provider ? { provider: historical.provider } : {}),
|
|
602
650
|
...(historical.providerSessionId ? { providerSessionId: historical.providerSessionId } : {}),
|
|
603
|
-
...(historical.source ? { source: historical.source } : {}),
|
|
651
|
+
...(isNative ? { source: "native" } : historical.source ? { source: historical.source } : {}),
|
|
604
652
|
summary: historical.summary,
|
|
605
653
|
runtimeState: historical.runtimeState,
|
|
606
654
|
...(historical.nativeIdentity ? { nativeIdentity: historical.nativeIdentity } : {}),
|
|
655
|
+
...(manualHandoffProof ? { manualHandoff: true, manualHandoffProof } : {}),
|
|
607
656
|
});
|
|
608
657
|
ok("session.continue", {
|
|
609
658
|
sessionId: continued.sessionId,
|
package/apps/cli/package.json
CHANGED
|
@@ -5,7 +5,7 @@ import { publishRemoteControllerCommand } from "../relay/remote-command.js";
|
|
|
5
5
|
import { claimCommandRequest, sendCommandResponse } from "../relay/send.js";
|
|
6
6
|
import { runtimeTurnSummaryForTask } from "../session/runtime-truth.js";
|
|
7
7
|
import { crewSessionRuntimeState, deliverQueuedCrewAdmission, publishRemoteCrewQueueEnqueue } from "./handler.js";
|
|
8
|
-
import { applyConsumerQueueUpdateToStore, claimCrewQueueHead, completeMaterializedQueueAdmission, failQueuedCrewBacklog, findAdmissionByTaskAndRequest, listPendingCrewQueueNotifications, listPendingConsumerQueueEnqueues, listCrewQueueHeads, markCrewQueueNotificationAttempt, markCrewQueueNotificationApplied, releaseCrewQueueBackoff, recordAdmission, retryClaimedQueueAdmission, runtimeTurnIdForCrewTurn, updatePendingConsumerQueueEnqueue, upsertCrewQueueNotification, upsertSubscription, } from "./store.js";
|
|
8
|
+
import { applyConsumerQueueUpdateToStore, claimCrewQueueHead, completeMaterializedQueueAdmission, failQueuedCrewBacklog, findAdmissionByTaskAndRequest, listPendingCrewQueueNotifications, listPendingConsumerQueueEnqueues, listCrewQueueHeads, markCrewQueueNotificationAttempt, markCrewQueueNotificationApplied, markCrewQueueNotificationDeadLetter, releaseCrewQueueBackoff, recordAdmission, retryClaimedQueueAdmission, runtimeTurnIdForCrewTurn, updatePendingConsumerQueueEnqueue, upsertCrewQueueNotification, upsertSubscription, } from "./store.js";
|
|
9
9
|
import { catchUpSubscriptionEvents } from "./watcher.js";
|
|
10
10
|
const activeDrains = new Map();
|
|
11
11
|
const pendingDrains = new Set();
|
|
@@ -16,6 +16,8 @@ const notificationRetryTimers = new Map();
|
|
|
16
16
|
const consumerEnqueueRetryTimers = new Map();
|
|
17
17
|
let notificationFlushRun;
|
|
18
18
|
let notificationFlushRequested = false;
|
|
19
|
+
let consumerEnqueueRetryRun;
|
|
20
|
+
let consumerEnqueueRetryRequested = false;
|
|
19
21
|
let recoveryRun;
|
|
20
22
|
export async function signalCrewQueue(config, signal) {
|
|
21
23
|
const key = crewQueueKey(signal);
|
|
@@ -242,6 +244,7 @@ export async function handleCrewQueueUpdateCommand(ws, config, command) {
|
|
|
242
244
|
ownerRevision: command.ownerRevision,
|
|
243
245
|
currentRevision: applied.currentRevision,
|
|
244
246
|
applied: applied.applied,
|
|
247
|
+
status: applied.applied ? "applied" : "permanent-rejected",
|
|
245
248
|
});
|
|
246
249
|
if (applied.subscription) {
|
|
247
250
|
void catchUpSubscriptionEvents({ config, subscription: applied.subscription }).catch((error) => {
|
|
@@ -341,6 +344,21 @@ async function completeMaterializedCrewQueueHead(config, queue) {
|
|
|
341
344
|
return terminalRuntimeStatus(summary.status) ? "terminal" : "active";
|
|
342
345
|
}
|
|
343
346
|
async function retryPendingConsumerQueueEnqueues(config) {
|
|
347
|
+
if (consumerEnqueueRetryRun) {
|
|
348
|
+
consumerEnqueueRetryRequested = true;
|
|
349
|
+
return await consumerEnqueueRetryRun;
|
|
350
|
+
}
|
|
351
|
+
consumerEnqueueRetryRun = (async () => {
|
|
352
|
+
do {
|
|
353
|
+
consumerEnqueueRetryRequested = false;
|
|
354
|
+
await retryPendingConsumerQueueEnqueuesOnce(config);
|
|
355
|
+
} while (consumerEnqueueRetryRequested);
|
|
356
|
+
})().finally(() => {
|
|
357
|
+
consumerEnqueueRetryRun = undefined;
|
|
358
|
+
});
|
|
359
|
+
await consumerEnqueueRetryRun;
|
|
360
|
+
}
|
|
361
|
+
async function retryPendingConsumerQueueEnqueuesOnce(config) {
|
|
344
362
|
const pending = await listPendingConsumerQueueEnqueues({ consumerMachineId: config.machineId });
|
|
345
363
|
for (const admission of pending) {
|
|
346
364
|
const key = consumerEnqueueRetryKey(admission);
|
|
@@ -611,6 +629,18 @@ async function flushCrewQueueNotificationsOnce(config) {
|
|
|
611
629
|
expectedType: "machine:crewQueueUpdateApplied",
|
|
612
630
|
timeoutMs: 15_000,
|
|
613
631
|
});
|
|
632
|
+
if (response?.type === "machine:crewQueueUpdateApplied" && response.status === "permanent-rejected") {
|
|
633
|
+
await markCrewQueueNotificationDeadLetter(notification.notificationId);
|
|
634
|
+
await appendAudit({
|
|
635
|
+
machineId: config.machineId,
|
|
636
|
+
sessionId: notification.crewSessionId,
|
|
637
|
+
actor: "system",
|
|
638
|
+
action: "orchestration.queue.consumer_update_dead_letter",
|
|
639
|
+
summary: `Consumer permanently rejected queue update for ${notification.taskId}`,
|
|
640
|
+
evidence: { taskId: notification.taskId, dispatchRequestId: notification.dispatchRequestId, ownerRevision: notification.ownerRevision },
|
|
641
|
+
}).catch(() => undefined);
|
|
642
|
+
return;
|
|
643
|
+
}
|
|
614
644
|
if (!crewQueueUpdateAppliedCovers(notification, response)) {
|
|
615
645
|
throw new Error(`Consumer did not apply queue update revision ${notification.ownerRevision} for task ${notification.taskId}`);
|
|
616
646
|
}
|
|
@@ -684,7 +714,8 @@ function crewQueueUpdateAppliedCovers(notification, response) {
|
|
|
684
714
|
response.crewSessionId === notification.crewSessionId &&
|
|
685
715
|
response.ownerRevision === notification.ownerRevision &&
|
|
686
716
|
response.currentRevision >= notification.ownerRevision &&
|
|
687
|
-
response.applied
|
|
717
|
+
response.applied &&
|
|
718
|
+
(response.status === undefined || response.status === "applied" || response.status === "covered");
|
|
688
719
|
}
|
|
689
720
|
function consumerProjectionCoversUpdate(admission, command, currentRevision) {
|
|
690
721
|
if (!admission)
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type DispatchTaskProjection, type MachineCommand, type RuntimeTurnReadSummary } from "../../../../packages/shared/dist/index.js";
|
|
1
|
+
import { type DispatchTaskProjection, type LoadedSessionInventory, type MachineCommand, type RuntimeTurnReadSummary } from "../../../../packages/shared/dist/index.js";
|
|
2
2
|
import type { DaemonConfig } from "../types.js";
|
|
3
3
|
import { type StoredDispatchAdmissionRecord } from "./store.js";
|
|
4
4
|
type DispatchCommand = Extract<MachineCommand, {
|
|
@@ -18,6 +18,10 @@ export type DispatchListResult = {
|
|
|
18
18
|
export declare function handleDispatchCommand(ws: WebSocket, config: DaemonConfig, command: DispatchCommand): Promise<void>;
|
|
19
19
|
export declare function handleQueueCommand(ws: WebSocket, config: DaemonConfig, command: QueueCommand): Promise<void>;
|
|
20
20
|
export declare function dispatchListResultForCommand(config: DaemonConfig, command: DispatchListCommand): Promise<DispatchListResult>;
|
|
21
|
+
export declare function remoteLoadedSessionInventory(config: DaemonConfig, machineId: string, filters?: {
|
|
22
|
+
cwd?: string;
|
|
23
|
+
agent?: string;
|
|
24
|
+
}): Promise<LoadedSessionInventory[]>;
|
|
21
25
|
export declare function publishRemoteCrewQueueEnqueue(config: DaemonConfig, admission: StoredDispatchAdmissionRecord): Promise<void>;
|
|
22
26
|
export declare function deliverQueuedCrewAdmission(config: DaemonConfig, admission: StoredDispatchAdmissionRecord): Promise<StoredDispatchAdmissionRecord>;
|
|
23
27
|
export type CrewSessionRuntimeState = {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createHash } from "node:crypto";
|
|
2
2
|
import { decryptJson, dispatchRouteCandidates, encryptJson, filterDispatchRouteCandidates, parseMachineClientMessage, randomId } from "../../../../packages/shared/dist/index.js";
|
|
3
3
|
import { appendAudit } from "../audit.js";
|
|
4
|
-
import { sessions, sessionTurns } from "../state.js";
|
|
4
|
+
import { completedCommandResponses, sessions, sessionTurns } from "../state.js";
|
|
5
5
|
import { admitMachineCreateSession, admitMachineFork, admitMachinePrompt } from "../relay/admission.js";
|
|
6
6
|
import { publishRemoteControllerCommand, readRetainedMachineMessage, RemoteCommandError, requestRemoteMachineResult } from "../relay/remote-command.js";
|
|
7
7
|
import { claimCommandRequest, sendCommandResponse } from "../relay/send.js";
|
|
@@ -11,6 +11,11 @@ import { listArtifacts } from "../artifact/store.js";
|
|
|
11
11
|
import { ackAttention, completeClaimedQueueAdmission, crewQueueHasBacklog, deadAttention, failClaimedQueueAdmission, findAdmissionByTask, findAdmissionByTaskAndRequest, latestAttentionForTask, listAdmissions, listAttentions, recordAdmission, recordTurnRuntimeBinding, reopenUnresolvedArtifactFeedbackAttentions, retryAttention, retryClaimedQueueAdmission, runtimeTurnIdForCrewTurn, updateAdmission, updatePendingConsumerQueueEnqueue, upsertSubscription, } from "./store.js";
|
|
12
12
|
import { catchUpSubscriptionEvents } from "./watcher.js";
|
|
13
13
|
export async function handleDispatchCommand(ws, config, command) {
|
|
14
|
+
// Status/list are read projections, not mutation retries. A caller may use
|
|
15
|
+
// the same correlation id while the durable admission changes; never replay
|
|
16
|
+
// the 24h mutation response cache for these reads.
|
|
17
|
+
if (command.action === "status" || command.action === "list")
|
|
18
|
+
completedCommandResponses.delete(command.requestId);
|
|
14
19
|
if (!claimCommandRequest(ws, command.requestId))
|
|
15
20
|
return;
|
|
16
21
|
if (command.machineId !== config.machineId) {
|
|
@@ -495,6 +500,7 @@ async function applyFirstAgentRouteGate(config, consumer, command, crewMachineId
|
|
|
495
500
|
const candidates = filterDispatchRouteCandidates([
|
|
496
501
|
...dispatchRouteCandidates(records),
|
|
497
502
|
...await artifactLineageRouteCandidates(config, command, crewMachineId),
|
|
503
|
+
...await remoteLoadedCrewRouteCandidates(config, crewMachineId, command.cwd),
|
|
498
504
|
], {
|
|
499
505
|
cwd: command.cwd,
|
|
500
506
|
crewMachineId,
|
|
@@ -504,6 +510,41 @@ async function applyFirstAgentRouteGate(config, consumer, command, crewMachineId
|
|
|
504
510
|
return { error: routeError };
|
|
505
511
|
return { command: routed };
|
|
506
512
|
}
|
|
513
|
+
export async function remoteLoadedSessionInventory(config, machineId, filters = {}) {
|
|
514
|
+
if (!machineId || machineId === config.machineId)
|
|
515
|
+
return [];
|
|
516
|
+
const message = await requestRemoteMachineResult(config, {
|
|
517
|
+
targetMachineId: machineId,
|
|
518
|
+
expectedType: "machine:historicalSessions",
|
|
519
|
+
command: {
|
|
520
|
+
type: "controller:listHistoricalSessions",
|
|
521
|
+
requestId: randomId("req_session_inventory"),
|
|
522
|
+
machineId,
|
|
523
|
+
...(filters.cwd ? { cwd: filters.cwd } : {}),
|
|
524
|
+
...(filters.agent ? { agent: filters.agent } : {}),
|
|
525
|
+
limit: 200,
|
|
526
|
+
},
|
|
527
|
+
timeoutMs: 20_000,
|
|
528
|
+
});
|
|
529
|
+
return (message.loadedSessions ?? []).filter((session) => session.status !== "closed");
|
|
530
|
+
}
|
|
531
|
+
async function remoteLoadedCrewRouteCandidates(config, crewMachineId, cwd) {
|
|
532
|
+
const loaded = await remoteLoadedSessionInventory(config, crewMachineId, cwd ? { cwd } : {}).catch(() => []);
|
|
533
|
+
return loaded.map((session) => {
|
|
534
|
+
const busy = session.status === "running" || session.status === "waiting_for_option";
|
|
535
|
+
const checkpointId = session.currentHead ?? session.lastTurnId;
|
|
536
|
+
return {
|
|
537
|
+
crewSessionId: session.sessionId,
|
|
538
|
+
crewMachineId,
|
|
539
|
+
agent: session.agent,
|
|
540
|
+
cwd: session.cwd,
|
|
541
|
+
availability: busy ? "busy" : "idle",
|
|
542
|
+
recentTasks: [],
|
|
543
|
+
...(busy ? { queueArgs: { crewSessionId: session.sessionId, ifBusy: "queue" } } : { reuseArgs: { crewSessionId: session.sessionId } }),
|
|
544
|
+
...(checkpointId ? { forkArgs: { forkFromCrewSessionId: session.sessionId, forkCheckpointId: checkpointId } } : {}),
|
|
545
|
+
};
|
|
546
|
+
});
|
|
547
|
+
}
|
|
507
548
|
async function artifactLineageRouteCandidates(config, command, crewMachineId) {
|
|
508
549
|
if (!command.taskId || !command.routeDecision || command.routeDecision.kind === "new")
|
|
509
550
|
return [];
|
|
@@ -597,7 +638,12 @@ function validateRouteDecisionCandidate(decision, candidates) {
|
|
|
597
638
|
};
|
|
598
639
|
}
|
|
599
640
|
if (candidate.availability !== "idle") {
|
|
600
|
-
|
|
641
|
+
// An explicit Queue Reuse decision is a durable admission strategy. A
|
|
642
|
+
// remote runtime snapshot may be temporarily unknown while the owner is
|
|
643
|
+
// reconnecting; do not turn that observation into a route rejection or
|
|
644
|
+
// pretend it is idle. The owner-side enqueue will re-check runtime truth
|
|
645
|
+
// and either retain the inbox item or fail a permanently unavailable Crew.
|
|
646
|
+
if (decision.kind === "reuse" && decision.ifBusy === "queue" && (candidate.availability === "busy" || candidate.availability === "unknown"))
|
|
601
647
|
return undefined;
|
|
602
648
|
if (decision.kind === "fork" && candidate.availability === "unknown") {
|
|
603
649
|
return {
|
|
@@ -1211,7 +1257,22 @@ async function crewSessionProjection(config, crewMachineId, crewSessionId) {
|
|
|
1211
1257
|
return session ? projectCrewSession(session, crewMachineId) : undefined;
|
|
1212
1258
|
}
|
|
1213
1259
|
const sessionMessage = await readRetainedMachineMessage(config, `node:${crewMachineId}:session:${crewSessionId}`).catch(() => undefined);
|
|
1214
|
-
|
|
1260
|
+
if (sessionMessage)
|
|
1261
|
+
return projectRemoteCrewSession(crewMachineId, sessionMessage);
|
|
1262
|
+
const loaded = await remoteLoadedSessionInventory(config, crewMachineId).catch(() => []);
|
|
1263
|
+
const inventory = loaded.find((session) => session.sessionId === crewSessionId);
|
|
1264
|
+
if (!inventory)
|
|
1265
|
+
return undefined;
|
|
1266
|
+
return {
|
|
1267
|
+
crewMachineId,
|
|
1268
|
+
crewSessionId: inventory.sessionId,
|
|
1269
|
+
agent: inventory.agent,
|
|
1270
|
+
cwd: inventory.cwd,
|
|
1271
|
+
name: inventory.name,
|
|
1272
|
+
status: inventory.status === "running" || inventory.status === "waiting_for_option" ? "running" : "ready",
|
|
1273
|
+
...(inventory.currentHead ? { currentHead: inventory.currentHead } : {}),
|
|
1274
|
+
...(inventory.lastTurnId ? { lastTurnId: inventory.lastTurnId } : {}),
|
|
1275
|
+
};
|
|
1215
1276
|
}
|
|
1216
1277
|
async function dispatchTaskProjection(config, admission) {
|
|
1217
1278
|
const crewMachineId = admission.crewMachineId ?? config.machineId;
|
|
@@ -153,6 +153,7 @@ export declare function markCrewQueueNotificationApplied(input: {
|
|
|
153
153
|
ownerRevision: number;
|
|
154
154
|
currentRevision: number;
|
|
155
155
|
}): Promise<boolean>;
|
|
156
|
+
export declare function markCrewQueueNotificationDeadLetter(notificationId: string): Promise<boolean>;
|
|
156
157
|
export declare function markCrewQueueNotificationAttempt(input: {
|
|
157
158
|
notificationId: string;
|
|
158
159
|
error: string;
|
|
@@ -166,6 +166,18 @@ export async function markCrewQueueNotificationApplied(input) {
|
|
|
166
166
|
});
|
|
167
167
|
return removed;
|
|
168
168
|
}
|
|
169
|
+
export async function markCrewQueueNotificationDeadLetter(notificationId) {
|
|
170
|
+
let removed = false;
|
|
171
|
+
await updateOrchestrationStore((current) => {
|
|
172
|
+
const index = current.queueNotifications.findIndex((record) => record.notificationId === notificationId);
|
|
173
|
+
if (index < 0)
|
|
174
|
+
return current;
|
|
175
|
+
current.queueNotifications.splice(index, 1);
|
|
176
|
+
removed = true;
|
|
177
|
+
return current;
|
|
178
|
+
});
|
|
179
|
+
return removed;
|
|
180
|
+
}
|
|
169
181
|
export async function markCrewQueueNotificationAttempt(input) {
|
|
170
182
|
const now = new Date();
|
|
171
183
|
const updatedAt = now.toISOString();
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decryptJson, parseControllerClientMessage, parseInternalControllerMessage, parseRelayServerWireMessage, randomId, } from "../../../../packages/shared/dist/index.js";
|
|
1
|
+
import { decryptJson, parseControllerClientMessage, parseInternalControllerMessage, parseMachineClientMessage, parseRelayServerWireMessage, randomId, } from "../../../../packages/shared/dist/index.js";
|
|
2
2
|
import { appendAudit } from "../audit.js";
|
|
3
3
|
import { drainPendingArtifactFeedbackAttentions, handleArtifactCommand } from "../artifact/handler.js";
|
|
4
4
|
import { handleGatewayCommand } from "../gateway/handler.js";
|
|
@@ -21,11 +21,42 @@ import { nativeImportSessionId } from "../session/native-identity.js";
|
|
|
21
21
|
import { encryptedSessionMetadata } from "../session/metadata.js";
|
|
22
22
|
import { loadSessionProjections } from "../session/store.js";
|
|
23
23
|
import { relayWsUrl } from "../relay-http.js";
|
|
24
|
-
import { sessions, sessionTurns } from "../state.js";
|
|
24
|
+
import { applyRelayMachinePresence, applyRelayMachineSnapshot, reconcileRelayMachinePresence, relayMachineRegistry, sessions, sessionTurns } from "../state.js";
|
|
25
25
|
import { handleDevExec, handleDiagnose } from "./devtools.js";
|
|
26
26
|
import { handleInboundPromptCommand, recoverInboundPromptAdmissions } from "./inbound-prompt.js";
|
|
27
27
|
import { registerMachine } from "./register.js";
|
|
28
|
-
import { clearActiveRelaySocket, configureRelaySender, flushRelayOutbox, handleMachineMessageAck, handleRetainedPacketAck, handleRetainedPacketError, sendCommandResponse, setActiveRelaySocket, } from "./send.js";
|
|
28
|
+
import { clearActiveRelaySocket, configureRelaySender, flushRelayOutbox, handleMachineMessageAck, handleMachineMessageError, handleRetainedPacketAck, handleRetainedPacketError, sendCommandResponse, setActiveRelaySocket, } from "./send.js";
|
|
29
|
+
async function applyMachineManifest(config, retained, presence) {
|
|
30
|
+
try {
|
|
31
|
+
const message = parseMachineClientMessage(await decryptJson(config.accountSecret, retained.encryptedPayload));
|
|
32
|
+
if (message.type !== "machine:register")
|
|
33
|
+
return;
|
|
34
|
+
let name = retained.updatedByNodeId;
|
|
35
|
+
if (message.encryptedMetadata) {
|
|
36
|
+
const metadata = await decryptJson(config.accountSecret, message.encryptedMetadata);
|
|
37
|
+
if (typeof metadata.name === "string" && metadata.name.trim())
|
|
38
|
+
name = metadata.name.trim();
|
|
39
|
+
else if (typeof metadata.hostname === "string" && metadata.hostname.trim())
|
|
40
|
+
name = metadata.hostname.trim();
|
|
41
|
+
}
|
|
42
|
+
const previous = relayMachineRegistry.get(retained.updatedByNodeId);
|
|
43
|
+
applyRelayMachineSnapshot({
|
|
44
|
+
id: retained.updatedByNodeId,
|
|
45
|
+
name,
|
|
46
|
+
// A retained manifest describes identity/capabilities only. Preserve
|
|
47
|
+
// the authoritative presence projection when no live presence record
|
|
48
|
+
// accompanied it; never infer online from a static retained packet.
|
|
49
|
+
online: presence?.online ?? previous?.online ?? false,
|
|
50
|
+
lastSeen: presence?.lastSeen ?? previous?.lastSeen ?? retained.updatedAt,
|
|
51
|
+
capabilities: message.capabilities,
|
|
52
|
+
encryptedMetadata: message.encryptedMetadata,
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
catch {
|
|
56
|
+
// Retained manifests are encrypted account state; malformed/unrelated
|
|
57
|
+
// packets must not prevent discovery of other machines.
|
|
58
|
+
}
|
|
59
|
+
}
|
|
29
60
|
export async function handleServerMessage(ws, config, message) {
|
|
30
61
|
if (message.type === "server:hello") {
|
|
31
62
|
await registerMachine(ws, config);
|
|
@@ -186,6 +217,34 @@ async function handleRelayWireMessage(ws, config, message) {
|
|
|
186
217
|
});
|
|
187
218
|
return;
|
|
188
219
|
}
|
|
220
|
+
if (message.type === "server:snapshot") {
|
|
221
|
+
for (const presence of message.presence)
|
|
222
|
+
applyRelayMachinePresence(presence);
|
|
223
|
+
reconcileRelayMachinePresence(message.presence);
|
|
224
|
+
for (const presence of message.presence) {
|
|
225
|
+
if (presence.manifest)
|
|
226
|
+
await applyMachineManifest(config, presence.manifest, presence);
|
|
227
|
+
}
|
|
228
|
+
for (const retained of message.retainedPackets) {
|
|
229
|
+
try {
|
|
230
|
+
const retainedMessage = parseMachineClientMessage(await decryptJson(config.accountSecret, retained.encryptedPayload));
|
|
231
|
+
if (retainedMessage.type === "machine:register") {
|
|
232
|
+
await applyMachineManifest(config, retained);
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
catch {
|
|
236
|
+
// Retained packets are encrypted per account; an unrelated/invalid
|
|
237
|
+
// retained entry must not prevent presence discovery for other nodes.
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
if (message.type === "server:presence") {
|
|
243
|
+
applyRelayMachinePresence(message.presence);
|
|
244
|
+
if (message.presence.manifest)
|
|
245
|
+
await applyMachineManifest(config, message.presence.manifest, message.presence);
|
|
246
|
+
return;
|
|
247
|
+
}
|
|
189
248
|
if (message.type === "server:ack") {
|
|
190
249
|
if (message.packetId)
|
|
191
250
|
await handleMachineMessageAck(message.packetId);
|
|
@@ -194,6 +253,8 @@ async function handleRelayWireMessage(ws, config, message) {
|
|
|
194
253
|
return;
|
|
195
254
|
}
|
|
196
255
|
if (message.type === "server:error") {
|
|
256
|
+
if (message.packetId)
|
|
257
|
+
await handleMachineMessageError(message.packetId, message.message);
|
|
197
258
|
if (message.retainedKey)
|
|
198
259
|
handleRetainedPacketError(message.retainedKey);
|
|
199
260
|
console.error(`Relay error${message.code ? ` ${message.code}` : ""}: ${message.message}`);
|
|
@@ -25,6 +25,7 @@ export async function publishRemoteControllerCommand(config, input) {
|
|
|
25
25
|
return await new Promise((resolve, reject) => {
|
|
26
26
|
const ws = new WebSocket(relayWsUrl(config.relayUrl));
|
|
27
27
|
let settled = false;
|
|
28
|
+
let commandSent = false;
|
|
28
29
|
const timer = globalThis.setTimeout(() => {
|
|
29
30
|
finish(undefined, new RemoteCommandError(`Timed out waiting for remote command ${input.command.requestId}`, "REMOTE_COMMAND_TIMEOUT"));
|
|
30
31
|
}, timeoutMs);
|
|
@@ -45,6 +46,11 @@ export async function publishRemoteControllerCommand(config, input) {
|
|
|
45
46
|
token: config.machineToken,
|
|
46
47
|
nodeKind: "machine",
|
|
47
48
|
nodeId: config.machineId,
|
|
49
|
+
// This is an ephemeral RPC socket, not the daemon's durable command
|
|
50
|
+
// consumer. Do not replay unrelated toNode packets into it: the
|
|
51
|
+
// durable daemon connection (with its own cursor) must remain the
|
|
52
|
+
// only consumer that can acknowledge those commands.
|
|
53
|
+
cursor: Number.MAX_SAFE_INTEGER,
|
|
48
54
|
}));
|
|
49
55
|
});
|
|
50
56
|
ws.addEventListener("error", () => {
|
|
@@ -57,6 +63,18 @@ export async function publishRemoteControllerCommand(config, input) {
|
|
|
57
63
|
targetMachineId: input.targetMachineId,
|
|
58
64
|
expectedType: input.expectedType,
|
|
59
65
|
send: (message) => ws.send(JSON.stringify(message)),
|
|
66
|
+
sendCommand: async (cursor) => {
|
|
67
|
+
if (commandSent)
|
|
68
|
+
return;
|
|
69
|
+
commandSent = true;
|
|
70
|
+
const subscribe = {
|
|
71
|
+
type: "subscribe",
|
|
72
|
+
channelId: controllerProjectionChannel,
|
|
73
|
+
...(cursor === undefined ? {} : { cursor }),
|
|
74
|
+
};
|
|
75
|
+
ws.send(JSON.stringify(subscribe));
|
|
76
|
+
ws.send(JSON.stringify(await remoteCommandPacket(config, packetId, input.targetMachineId, input.command)));
|
|
77
|
+
},
|
|
60
78
|
finish,
|
|
61
79
|
}).catch((error) => finish(undefined, error));
|
|
62
80
|
});
|
|
@@ -77,8 +95,14 @@ export async function readRetainedMachineMessage(config, retainedKey) {
|
|
|
77
95
|
async function handleRelayMessage(config, raw, context) {
|
|
78
96
|
const wire = parseRelayServerWireMessage(JSON.parse(String(raw)));
|
|
79
97
|
if (wire.type === "server:hello") {
|
|
80
|
-
|
|
81
|
-
|
|
98
|
+
// Wait for the snapshot cursor before subscribing. Short-lived RPC
|
|
99
|
+
// connections must not replay the entire product:controllers history;
|
|
100
|
+
// the snapshot cursor gives the subscription a precise lower bound while
|
|
101
|
+
// still catching packets committed between the snapshot and subscribe.
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
if (wire.type === "server:snapshot") {
|
|
105
|
+
await context.sendCommand(wire.cursor);
|
|
82
106
|
return;
|
|
83
107
|
}
|
|
84
108
|
if (wire.type === "server:error") {
|
|
@@ -92,7 +116,6 @@ async function handleRelayMessage(config, raw, context) {
|
|
|
92
116
|
}
|
|
93
117
|
if (wire.type !== "server:packet")
|
|
94
118
|
return;
|
|
95
|
-
context.send({ type: "ackPacket", packetId: wire.packet.packetId, serverSeq: wire.packet.serverSeq });
|
|
96
119
|
let message;
|
|
97
120
|
try {
|
|
98
121
|
message = parseMachineClientMessage(await decryptJson(config.accountSecret, wire.packet.encryptedPayload));
|
|
@@ -102,6 +125,10 @@ async function handleRelayMessage(config, raw, context) {
|
|
|
102
125
|
}
|
|
103
126
|
if (!("requestId" in message) || message.requestId !== context.command.requestId)
|
|
104
127
|
return;
|
|
128
|
+
// A short-lived RPC socket can share a machine id with the durable daemon
|
|
129
|
+
// connection. Only acknowledge the response that belongs to this request;
|
|
130
|
+
// unrelated packets must remain available for the durable consumer.
|
|
131
|
+
context.send({ type: "ackPacket", packetId: wire.packet.packetId, serverSeq: wire.packet.serverSeq });
|
|
105
132
|
if (message.type === "machine:error") {
|
|
106
133
|
context.finish(undefined, new RemoteCommandError(message.message, message.code ?? "REMOTE_MACHINE_ERROR", false));
|
|
107
134
|
return;
|
|
@@ -9,6 +9,7 @@ export declare function setActiveRelaySocket(ws: WebSocket): void;
|
|
|
9
9
|
export declare function clearActiveRelaySocket(ws: WebSocket): void;
|
|
10
10
|
export declare function flushRelayOutbox(): Promise<void>;
|
|
11
11
|
export declare function handleMachineMessageAck(messageId: string): Promise<void>;
|
|
12
|
+
export declare function handleMachineMessageError(messageId: string, error?: string): Promise<void>;
|
|
12
13
|
export declare function handleRetainedPacketAck(retainedKey: string, version: number | undefined): void;
|
|
13
14
|
export declare function handleRetainedPacketError(retainedKey: string): void;
|
|
14
15
|
export declare function clearRelayOutboxForTests(): Promise<void>;
|