@pinet/broker-core 0.2.4 → 0.2.7
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/dist/agent-messaging.d.ts +27 -4
- package/dist/hibernation-commands.d.ts +123 -0
- package/dist/hibernation-commands.js +287 -0
- package/dist/hibernation-orchestrator.d.ts +327 -0
- package/dist/hibernation-orchestrator.js +1096 -0
- package/dist/hibernation-projection.d.ts +20 -0
- package/dist/hibernation-projection.js +60 -0
- package/dist/hibernation-status.d.ts +141 -0
- package/dist/hibernation-status.js +390 -0
- package/dist/hibernation-telemetry.d.ts +54 -0
- package/dist/hibernation-telemetry.js +119 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +6 -0
- package/dist/leader.d.ts +134 -5
- package/dist/leader.js +359 -26
- package/dist/lifecycle.d.ts +5 -0
- package/dist/lifecycle.js +59 -0
- package/dist/mail-classification.d.ts +6 -1
- package/dist/message-send.d.ts +4 -3
- package/dist/router.d.ts +14 -1
- package/dist/router.js +15 -0
- package/dist/schema.d.ts +181 -2
- package/dist/schema.js +1243 -17
- package/dist/types.d.ts +288 -1
- package/dist/types.js +6 -0
- package/package.json +5 -5
|
@@ -1,11 +1,33 @@
|
|
|
1
1
|
import type { AgentInfo, BrokerMessage } from "./types.js";
|
|
2
|
+
type AgentMessageMetadataObject = Record<string, unknown>;
|
|
3
|
+
interface AgentCapabilities {
|
|
4
|
+
repo?: string;
|
|
5
|
+
role?: string;
|
|
6
|
+
tools?: string[];
|
|
7
|
+
tags?: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface AgentMessageMetadata extends AgentMessageMetadataObject {
|
|
10
|
+
senderAgent?: string;
|
|
11
|
+
a2a?: boolean;
|
|
12
|
+
broadcast?: boolean;
|
|
13
|
+
broadcastChannel?: string;
|
|
14
|
+
trustedBrokerAgentId?: string;
|
|
15
|
+
emergency?: boolean;
|
|
16
|
+
targetScope?: string;
|
|
17
|
+
capabilities?: AgentCapabilities;
|
|
18
|
+
repo?: string;
|
|
19
|
+
role?: string;
|
|
20
|
+
broadcastChannels?: string[];
|
|
21
|
+
channels?: string[];
|
|
22
|
+
topics?: string[];
|
|
23
|
+
}
|
|
2
24
|
export interface AgentMessageStorage {
|
|
3
25
|
getAgents(): AgentInfo[];
|
|
4
26
|
getThread(threadId: string): {
|
|
5
27
|
threadId: string;
|
|
6
28
|
} | null;
|
|
7
29
|
createThread(threadId: string, source: string, channel: string, ownerAgent: string | null): void;
|
|
8
|
-
insertMessage(threadId: string, source: string, direction: "inbound" | "outbound", sender: string, body: string, targetAgentIds: string[], metadata?:
|
|
30
|
+
insertMessage(threadId: string, source: string, direction: "inbound" | "outbound", sender: string, body: string, targetAgentIds: string[], metadata?: AgentMessageMetadata): BrokerMessage;
|
|
9
31
|
}
|
|
10
32
|
export interface AgentDispatchTarget {
|
|
11
33
|
id: string;
|
|
@@ -16,7 +38,7 @@ export interface DirectAgentDispatchInput {
|
|
|
16
38
|
senderAgentName: string;
|
|
17
39
|
target: string;
|
|
18
40
|
body: string;
|
|
19
|
-
metadata?:
|
|
41
|
+
metadata?: AgentMessageMetadata;
|
|
20
42
|
trustedBrokerAgentId?: string;
|
|
21
43
|
}
|
|
22
44
|
export interface BroadcastAgentDispatchInput {
|
|
@@ -24,7 +46,7 @@ export interface BroadcastAgentDispatchInput {
|
|
|
24
46
|
senderAgentName: string;
|
|
25
47
|
channel: string;
|
|
26
48
|
body: string;
|
|
27
|
-
metadata?:
|
|
49
|
+
metadata?: AgentMessageMetadata;
|
|
28
50
|
}
|
|
29
51
|
export interface DirectAgentDispatchResult {
|
|
30
52
|
target: AgentDispatchTarget;
|
|
@@ -37,7 +59,7 @@ export interface BroadcastAgentDispatchResult {
|
|
|
37
59
|
messageIds: number[];
|
|
38
60
|
threadIds: string[];
|
|
39
61
|
}
|
|
40
|
-
export type AgentDispatchCallback = (target: AgentDispatchTarget, message: BrokerMessage, metadata:
|
|
62
|
+
export type AgentDispatchCallback = (target: AgentDispatchTarget, message: BrokerMessage, metadata: AgentMessageMetadata) => void;
|
|
41
63
|
export declare function isBroadcastChannelTarget(target: string): boolean;
|
|
42
64
|
export declare function normalizeBroadcastChannel(channel: string): string | null;
|
|
43
65
|
export declare function getAgentBroadcastChannels(agent: Pick<AgentInfo, "metadata">): string[];
|
|
@@ -46,3 +68,4 @@ export declare function resolveDirectAgentTarget(agents: AgentInfo[], target: st
|
|
|
46
68
|
export declare function resolveBroadcastTargets(agents: AgentInfo[], senderAgentId: string, channel: string): AgentInfo[];
|
|
47
69
|
export declare function dispatchDirectAgentMessage(storage: AgentMessageStorage, input: DirectAgentDispatchInput, onDispatch?: AgentDispatchCallback): DirectAgentDispatchResult;
|
|
48
70
|
export declare function dispatchBroadcastAgentMessage(storage: AgentMessageStorage, input: BroadcastAgentDispatchInput, onDispatch?: AgentDispatchCallback): BroadcastAgentDispatchResult;
|
|
71
|
+
export {};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type { AgentLifecycleState } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* Broker-managed hibernate/wake operator commands.
|
|
4
|
+
*
|
|
5
|
+
* These are the safe, default-off command primitives behind `pinet hibernate`
|
|
6
|
+
* and `pinet wake`. They add config-policy, repo-allowlist, and coarse
|
|
7
|
+
* lifecycle gating on top of the authoritative checkpoint/fence/eligibility
|
|
8
|
+
* enforcement in {@link HibernationOrchestrator}. Every result is sanitized:
|
|
9
|
+
* machine `reason` + static human `detail` only, never prompts, message bodies,
|
|
10
|
+
* argv, env values, or filesystem/socket paths.
|
|
11
|
+
*/
|
|
12
|
+
export interface HibernationCommandPolicy {
|
|
13
|
+
/** Master switch. Defaults false; false still permits waking hibernated rows. */
|
|
14
|
+
enabled: boolean;
|
|
15
|
+
mode: "observe" | "manual" | "auto";
|
|
16
|
+
/** Positive allowlist of repo identifiers (slug or basename). Empty = none. */
|
|
17
|
+
allowedRepos: string[];
|
|
18
|
+
}
|
|
19
|
+
export interface HibernationCommandRefusal {
|
|
20
|
+
reason: string;
|
|
21
|
+
detail: string;
|
|
22
|
+
retryable: boolean;
|
|
23
|
+
}
|
|
24
|
+
/** Coarse command gate outcome, before any orchestrator execution. */
|
|
25
|
+
export type HibernationCommandGate = {
|
|
26
|
+
outcome: "proceed";
|
|
27
|
+
} | {
|
|
28
|
+
outcome: "noop";
|
|
29
|
+
reason: string;
|
|
30
|
+
detail: string;
|
|
31
|
+
} | {
|
|
32
|
+
outcome: "refused";
|
|
33
|
+
refusal: HibernationCommandRefusal;
|
|
34
|
+
};
|
|
35
|
+
export interface HibernationCommandResult {
|
|
36
|
+
command: "hibernate" | "wake";
|
|
37
|
+
agentId: string;
|
|
38
|
+
/** executed = state changed; noop = already in target state; refused = gated. */
|
|
39
|
+
outcome: "executed" | "noop" | "refused";
|
|
40
|
+
state: AgentLifecycleState | string;
|
|
41
|
+
reason: string;
|
|
42
|
+
detail: string;
|
|
43
|
+
runtimeGeneration?: number | null;
|
|
44
|
+
attempts?: number;
|
|
45
|
+
durationMs?: number;
|
|
46
|
+
retryable?: boolean;
|
|
47
|
+
}
|
|
48
|
+
export interface HibernateCommandGateInput {
|
|
49
|
+
state: AgentLifecycleState;
|
|
50
|
+
repoIdentifier: string | null;
|
|
51
|
+
policy: HibernationCommandPolicy;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Coarse policy/lifecycle gate for a hibernate command. Deep eligibility
|
|
55
|
+
* (policy=never, working, pending inbox, broker-managed metadata) is enforced
|
|
56
|
+
* authoritatively by the orchestrator's prepare/hibernate path.
|
|
57
|
+
*/
|
|
58
|
+
export declare function evaluateHibernateCommandGate(input: HibernateCommandGateInput): HibernationCommandGate;
|
|
59
|
+
export interface WakeCommandGateInput {
|
|
60
|
+
state: AgentLifecycleState;
|
|
61
|
+
policy: HibernationCommandPolicy;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Coarse gate for a wake command. Waking a durable hibernation identity is a
|
|
65
|
+
* drain/recovery operation and is permitted even when `enabled=false`, per the
|
|
66
|
+
* safety model (disabling hibernation must never strand hibernated identities).
|
|
67
|
+
*/
|
|
68
|
+
export declare function evaluateWakeCommandGate(input: WakeCommandGateInput): HibernationCommandGate;
|
|
69
|
+
export interface HibernateCommandExecutor {
|
|
70
|
+
prepareHibernation(agentId: string, opts?: {
|
|
71
|
+
reason?: string;
|
|
72
|
+
actor?: string;
|
|
73
|
+
correlationId?: string;
|
|
74
|
+
}): {
|
|
75
|
+
ready: boolean;
|
|
76
|
+
state: string;
|
|
77
|
+
reason: string;
|
|
78
|
+
};
|
|
79
|
+
hibernate(agentId: string, opts?: {
|
|
80
|
+
reason?: string;
|
|
81
|
+
actor?: string;
|
|
82
|
+
correlationId?: string;
|
|
83
|
+
}): Promise<{
|
|
84
|
+
ok: boolean;
|
|
85
|
+
state: string;
|
|
86
|
+
reason: string;
|
|
87
|
+
durationMs?: number;
|
|
88
|
+
}>;
|
|
89
|
+
}
|
|
90
|
+
export interface WakeCommandExecutor {
|
|
91
|
+
wake(agentId: string, opts?: {
|
|
92
|
+
reason?: string;
|
|
93
|
+
actor?: string;
|
|
94
|
+
correlationId?: string;
|
|
95
|
+
}): Promise<{
|
|
96
|
+
ok: boolean;
|
|
97
|
+
state: string;
|
|
98
|
+
reason: string;
|
|
99
|
+
runtimeGeneration?: number;
|
|
100
|
+
attempts?: number;
|
|
101
|
+
durationMs?: number;
|
|
102
|
+
}>;
|
|
103
|
+
}
|
|
104
|
+
/** Build a sanitized "unknown target" refusal for a command whose target didn't resolve. */
|
|
105
|
+
export declare function unknownHibernationTarget(command: "hibernate" | "wake", target: string): HibernationCommandResult;
|
|
106
|
+
export interface ExecuteHibernateCommandInput extends HibernateCommandGateInput {
|
|
107
|
+
executor: HibernateCommandExecutor;
|
|
108
|
+
agentId: string;
|
|
109
|
+
actor?: string;
|
|
110
|
+
reason?: string;
|
|
111
|
+
correlationId?: string;
|
|
112
|
+
}
|
|
113
|
+
export declare function executeHibernateCommand(input: ExecuteHibernateCommandInput): Promise<HibernationCommandResult>;
|
|
114
|
+
export interface ExecuteWakeCommandInput extends WakeCommandGateInput {
|
|
115
|
+
executor: WakeCommandExecutor;
|
|
116
|
+
agentId: string;
|
|
117
|
+
actor?: string;
|
|
118
|
+
reason?: string;
|
|
119
|
+
correlationId?: string;
|
|
120
|
+
}
|
|
121
|
+
export declare function executeWakeCommand(input: ExecuteWakeCommandInput): Promise<HibernationCommandResult>;
|
|
122
|
+
/** Render a sanitized, compact operator line for a command result. */
|
|
123
|
+
export declare function formatHibernationCommandResult(result: HibernationCommandResult): string;
|
|
@@ -0,0 +1,287 @@
|
|
|
1
|
+
import { fingerprintToken, sanitizeOperatorReason } from "./hibernation-status.js";
|
|
2
|
+
/**
|
|
3
|
+
* Reduce an executor/adapter-produced reason to an operator-safe string.
|
|
4
|
+
*
|
|
5
|
+
* Executor results (e.g. an aborted checkpoint) may fold runtime-authored text
|
|
6
|
+
* into their `reason`, which can carry a filesystem/socket path. Machine reason
|
|
7
|
+
* codes pass through unchanged; anything path-like is redacted. Falls back to a
|
|
8
|
+
* static code so the result always carries a non-empty reason.
|
|
9
|
+
*/
|
|
10
|
+
function safeResultReason(reason) {
|
|
11
|
+
return sanitizeOperatorReason(reason) ?? "unspecified";
|
|
12
|
+
}
|
|
13
|
+
function refusal(reason, detail, retryable) {
|
|
14
|
+
return { outcome: "refused", refusal: { reason, detail, retryable } };
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Coarse policy/lifecycle gate for a hibernate command. Deep eligibility
|
|
18
|
+
* (policy=never, working, pending inbox, broker-managed metadata) is enforced
|
|
19
|
+
* authoritatively by the orchestrator's prepare/hibernate path.
|
|
20
|
+
*/
|
|
21
|
+
export function evaluateHibernateCommandGate(input) {
|
|
22
|
+
const { state, repoIdentifier, policy } = input;
|
|
23
|
+
if (!policy.enabled) {
|
|
24
|
+
return refusal("hibernation_disabled", "Hibernation is disabled (enabled=false). Waking already-hibernated identities is still permitted.", false);
|
|
25
|
+
}
|
|
26
|
+
if (policy.mode === "observe") {
|
|
27
|
+
return refusal("observe_only", "Hibernation is in observe-only mode; no state changes are performed.", false);
|
|
28
|
+
}
|
|
29
|
+
switch (state) {
|
|
30
|
+
case "hibernated":
|
|
31
|
+
case "hibernating":
|
|
32
|
+
return {
|
|
33
|
+
outcome: "noop",
|
|
34
|
+
reason: "already_hibernating",
|
|
35
|
+
detail: "Agent is already hibernated or hibernating.",
|
|
36
|
+
};
|
|
37
|
+
case "waking":
|
|
38
|
+
return refusal("wake_in_progress", "Agent is currently waking; retry once it settles.", true);
|
|
39
|
+
case "reap-candidate":
|
|
40
|
+
return refusal("quarantined", "Agent is quarantined as a reap-candidate and needs manual review, not hibernation.", false);
|
|
41
|
+
case "terminated":
|
|
42
|
+
return refusal("terminated", "Agent is terminated and cannot be hibernated.", false);
|
|
43
|
+
default:
|
|
44
|
+
break;
|
|
45
|
+
}
|
|
46
|
+
// Fail-closed allowlist with EXACT identity matching and NO basename collapse
|
|
47
|
+
// (C2): an allowlist entry must equal the broker-derived repo identifier
|
|
48
|
+
// exactly. A bare "pinet" entry therefore never admits a *different* root
|
|
49
|
+
// that merely shares a basename (e.g. "gugu91/pinet" or "evil/pinet"),
|
|
50
|
+
// and an "owner/repo" slug entry never admits a different owner. Blank
|
|
51
|
+
// identifiers/entries never match. Windows backslash separators are normalized
|
|
52
|
+
// to "/" on both sides so matching is OS-agnostic, and trailing slashes are
|
|
53
|
+
// stripped so "owner/repo/" and "owner/repo" compare equal.
|
|
54
|
+
const repo = (repoIdentifier ?? "").trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
55
|
+
const repoAllowlisted = repo.length > 0 &&
|
|
56
|
+
policy.allowedRepos.some((raw) => {
|
|
57
|
+
const entry = raw.trim().replace(/\\/g, "/").replace(/\/+$/, "");
|
|
58
|
+
if (entry.length === 0)
|
|
59
|
+
return false;
|
|
60
|
+
return entry === repo;
|
|
61
|
+
});
|
|
62
|
+
if (!repoAllowlisted) {
|
|
63
|
+
return refusal("repo_not_allowlisted", "Agent's repository is not in the hibernation allowlist.", false);
|
|
64
|
+
}
|
|
65
|
+
return { outcome: "proceed" };
|
|
66
|
+
}
|
|
67
|
+
/**
|
|
68
|
+
* Coarse gate for a wake command. Waking a durable hibernation identity is a
|
|
69
|
+
* drain/recovery operation and is permitted even when `enabled=false`, per the
|
|
70
|
+
* safety model (disabling hibernation must never strand hibernated identities).
|
|
71
|
+
*/
|
|
72
|
+
export function evaluateWakeCommandGate(input) {
|
|
73
|
+
switch (input.state) {
|
|
74
|
+
case "hibernated":
|
|
75
|
+
return { outcome: "proceed" };
|
|
76
|
+
case "waking":
|
|
77
|
+
// A wake is already in flight; there is nothing more for this command to
|
|
78
|
+
// do. Surfacing this as a noop (rather than proceeding to an executor
|
|
79
|
+
// that would refuse) keeps the operator signal accurate.
|
|
80
|
+
return {
|
|
81
|
+
outcome: "noop",
|
|
82
|
+
reason: "wake_in_progress",
|
|
83
|
+
detail: "Agent is already waking; the in-flight wake will deliver queued messages.",
|
|
84
|
+
};
|
|
85
|
+
case "live":
|
|
86
|
+
case "active":
|
|
87
|
+
case "grace":
|
|
88
|
+
case "idle":
|
|
89
|
+
return {
|
|
90
|
+
outcome: "noop",
|
|
91
|
+
reason: "already_awake",
|
|
92
|
+
detail: "Agent already has a live runtime; nothing to wake.",
|
|
93
|
+
};
|
|
94
|
+
case "hibernating":
|
|
95
|
+
return refusal("hibernate_in_progress", "Agent is mid-hibernation; retry once it reaches a hibernated state.", true);
|
|
96
|
+
case "reap-candidate":
|
|
97
|
+
return refusal("quarantined", "Agent is quarantined as a reap-candidate and needs manual review, not a wake.", false);
|
|
98
|
+
case "terminated":
|
|
99
|
+
return refusal("terminated", "Agent is terminated and cannot be woken.", false);
|
|
100
|
+
default:
|
|
101
|
+
return refusal("unknown_state", "Agent lifecycle state is not wakeable.", false);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
/** Build a sanitized "unknown target" refusal for a command whose target didn't resolve. */
|
|
105
|
+
export function unknownHibernationTarget(command, target) {
|
|
106
|
+
// The target is arbitrary, unresolved operator input (it may be a paste, a
|
|
107
|
+
// path, or carry secret material). Never echo it — not even redacted. Emit a
|
|
108
|
+
// stable non-reversible fingerprint so an operator can still correlate repeated
|
|
109
|
+
// failures of the *same* input without any content reaching an operator surface.
|
|
110
|
+
const controlStripped = Array.from(target)
|
|
111
|
+
.map((ch) => {
|
|
112
|
+
const code = ch.codePointAt(0) ?? 0;
|
|
113
|
+
return code <= 0x1f || code === 0x7f ? " " : ch;
|
|
114
|
+
})
|
|
115
|
+
.join("")
|
|
116
|
+
.replace(/\s+/g, " ")
|
|
117
|
+
.trim();
|
|
118
|
+
const safeTarget = controlStripped.length > 0 ? `target:#${fingerprintToken(controlStripped)}` : "(unnamed)";
|
|
119
|
+
return {
|
|
120
|
+
command,
|
|
121
|
+
agentId: safeTarget,
|
|
122
|
+
outcome: "refused",
|
|
123
|
+
state: "unknown",
|
|
124
|
+
reason: "unknown_target",
|
|
125
|
+
detail: "No broker-managed agent matched the requested target.",
|
|
126
|
+
retryable: false,
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
export async function executeHibernateCommand(input) {
|
|
130
|
+
const gate = evaluateHibernateCommandGate(input);
|
|
131
|
+
if (gate.outcome === "refused") {
|
|
132
|
+
return {
|
|
133
|
+
command: "hibernate",
|
|
134
|
+
agentId: input.agentId,
|
|
135
|
+
outcome: "refused",
|
|
136
|
+
state: input.state,
|
|
137
|
+
reason: gate.refusal.reason,
|
|
138
|
+
detail: gate.refusal.detail,
|
|
139
|
+
retryable: gate.refusal.retryable,
|
|
140
|
+
};
|
|
141
|
+
}
|
|
142
|
+
if (gate.outcome === "noop") {
|
|
143
|
+
return {
|
|
144
|
+
command: "hibernate",
|
|
145
|
+
agentId: input.agentId,
|
|
146
|
+
outcome: "noop",
|
|
147
|
+
state: input.state,
|
|
148
|
+
reason: gate.reason,
|
|
149
|
+
detail: gate.detail,
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
const opts = { actor: input.actor, reason: input.reason, correlationId: input.correlationId };
|
|
153
|
+
const prep = input.executor.prepareHibernation(input.agentId, opts);
|
|
154
|
+
if (!prep.ready) {
|
|
155
|
+
return {
|
|
156
|
+
command: "hibernate",
|
|
157
|
+
agentId: input.agentId,
|
|
158
|
+
outcome: "refused",
|
|
159
|
+
state: prep.state,
|
|
160
|
+
reason: prep.reason,
|
|
161
|
+
detail: "Agent is not eligible for hibernation right now.",
|
|
162
|
+
retryable: prep.reason === "agent_working" || prep.reason === "pending_inbox",
|
|
163
|
+
};
|
|
164
|
+
}
|
|
165
|
+
const result = await input.executor.hibernate(input.agentId, opts);
|
|
166
|
+
return {
|
|
167
|
+
command: "hibernate",
|
|
168
|
+
agentId: input.agentId,
|
|
169
|
+
outcome: result.ok ? "executed" : "refused",
|
|
170
|
+
state: result.state,
|
|
171
|
+
reason: safeResultReason(result.reason),
|
|
172
|
+
detail: result.ok
|
|
173
|
+
? "Agent runtime checkpointed and hibernated."
|
|
174
|
+
: result.state === "reap-candidate"
|
|
175
|
+
? "Hibernation could not complete cleanly; agent quarantined as reap-candidate for manual review."
|
|
176
|
+
: "Hibernation aborted; the runtime was left running.",
|
|
177
|
+
durationMs: result.durationMs,
|
|
178
|
+
// Quarantine needs manual review, not a blind retry; an abort-to-active is
|
|
179
|
+
// safe to retry once the transient condition clears.
|
|
180
|
+
retryable: result.ok ? undefined : result.state !== "reap-candidate",
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
export async function executeWakeCommand(input) {
|
|
184
|
+
const gate = evaluateWakeCommandGate(input);
|
|
185
|
+
if (gate.outcome === "refused") {
|
|
186
|
+
return {
|
|
187
|
+
command: "wake",
|
|
188
|
+
agentId: input.agentId,
|
|
189
|
+
outcome: "refused",
|
|
190
|
+
state: input.state,
|
|
191
|
+
reason: gate.refusal.reason,
|
|
192
|
+
detail: gate.refusal.detail,
|
|
193
|
+
retryable: gate.refusal.retryable,
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
if (gate.outcome === "noop") {
|
|
197
|
+
return {
|
|
198
|
+
command: "wake",
|
|
199
|
+
agentId: input.agentId,
|
|
200
|
+
outcome: "noop",
|
|
201
|
+
state: input.state,
|
|
202
|
+
reason: gate.reason,
|
|
203
|
+
detail: gate.detail,
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
const result = await input.executor.wake(input.agentId, {
|
|
207
|
+
actor: input.actor,
|
|
208
|
+
reason: input.reason,
|
|
209
|
+
correlationId: input.correlationId,
|
|
210
|
+
});
|
|
211
|
+
if (!result.ok && result.reason === "wake_in_progress") {
|
|
212
|
+
// Another lease owner is already waking this agent. That is the single
|
|
213
|
+
// winner and it will deliver queued work — this is a benign no-op, not a
|
|
214
|
+
// retryable failure (mirrors an already-`waking` target at the gate).
|
|
215
|
+
return {
|
|
216
|
+
command: "wake",
|
|
217
|
+
agentId: input.agentId,
|
|
218
|
+
outcome: "noop",
|
|
219
|
+
state: result.state,
|
|
220
|
+
reason: result.reason,
|
|
221
|
+
detail: "A wake is already in progress for this agent; the in-flight wake will deliver queued work.",
|
|
222
|
+
};
|
|
223
|
+
}
|
|
224
|
+
if (!result.ok && result.reason === "wake_lease_contended") {
|
|
225
|
+
// A non-wake lifecycle lease (e.g. a lingering hibernate around a crash) is
|
|
226
|
+
// transiently holding the agent. Unlike an in-flight wake, nothing will
|
|
227
|
+
// drain the inbox, so this is a distinct *retryable* refusal — the queued
|
|
228
|
+
// trigger is preserved (requeued) rather than consumed as a no-op.
|
|
229
|
+
return {
|
|
230
|
+
command: "wake",
|
|
231
|
+
agentId: input.agentId,
|
|
232
|
+
outcome: "refused",
|
|
233
|
+
state: result.state,
|
|
234
|
+
reason: result.reason,
|
|
235
|
+
detail: "A non-wake lifecycle operation is transiently holding this agent; no wake is in flight. The trigger was requeued — retry shortly.",
|
|
236
|
+
retryable: true,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
// Classify the failure by REASON, not just state. Some non-quarantined
|
|
240
|
+
// failures are terminal: a bare retry cannot change the outcome because the
|
|
241
|
+
// target is not a wakeable hibernated identity, or its durable launch manifest
|
|
242
|
+
// is gone. Marking those retryable would send an operator into a futile retry
|
|
243
|
+
// loop instead of the corrective action (investigate / re-spawn the worker).
|
|
244
|
+
const quarantined = result.state === "reap-candidate";
|
|
245
|
+
const notHibernated = result.reason.startsWith("not_hibernated");
|
|
246
|
+
const terminalReason = quarantined ||
|
|
247
|
+
notHibernated ||
|
|
248
|
+
result.reason === "unknown_agent" ||
|
|
249
|
+
result.reason === "missing_runtime_spec";
|
|
250
|
+
const failDetail = quarantined
|
|
251
|
+
? "Wake failed and the agent was quarantined as reap-candidate for manual review."
|
|
252
|
+
: result.reason === "missing_runtime_spec"
|
|
253
|
+
? "Wake failed: no durable runtime manifest exists for this identity, so it cannot be relaunched. Re-spawn the worker instead of retrying."
|
|
254
|
+
: result.reason === "unknown_agent"
|
|
255
|
+
? "No such agent is known to the broker; there is nothing to wake."
|
|
256
|
+
: notHibernated
|
|
257
|
+
? `Agent is not in a wakeable hibernated state (${result.state}); nothing to wake.`
|
|
258
|
+
: "Wake failed; agent left in a safe state — a retry may succeed.";
|
|
259
|
+
return {
|
|
260
|
+
command: "wake",
|
|
261
|
+
agentId: input.agentId,
|
|
262
|
+
outcome: result.ok ? "executed" : "refused",
|
|
263
|
+
state: result.state,
|
|
264
|
+
reason: safeResultReason(result.reason),
|
|
265
|
+
detail: result.ok ? "Agent runtime woken; queued messages will drain in order." : failDetail,
|
|
266
|
+
runtimeGeneration: result.ok ? (result.runtimeGeneration ?? null) : null,
|
|
267
|
+
attempts: result.attempts,
|
|
268
|
+
durationMs: result.durationMs,
|
|
269
|
+
// Quarantine and terminal-reason failures need operator action, not a retry;
|
|
270
|
+
// only genuinely transient safe-state failures are retryable.
|
|
271
|
+
retryable: result.ok ? undefined : !terminalReason,
|
|
272
|
+
};
|
|
273
|
+
}
|
|
274
|
+
/** Render a sanitized, compact operator line for a command result. */
|
|
275
|
+
export function formatHibernationCommandResult(result) {
|
|
276
|
+
const marker = result.outcome === "executed" ? "\u2713" : result.outcome === "noop" ? "\u2014" : "\u2717";
|
|
277
|
+
const parts = [
|
|
278
|
+
`${marker} ${result.command} ${result.agentId}: ${result.outcome} (${result.reason})`,
|
|
279
|
+
];
|
|
280
|
+
parts.push(` ${result.detail}`);
|
|
281
|
+
parts.push(` state=${result.state}`);
|
|
282
|
+
if (result.runtimeGeneration != null)
|
|
283
|
+
parts.push(` runtime_generation=${result.runtimeGeneration}`);
|
|
284
|
+
if (result.attempts != null)
|
|
285
|
+
parts.push(` attempts=${result.attempts}`);
|
|
286
|
+
return parts.join("\n");
|
|
287
|
+
}
|