agentic-engineering-harness 0.6.13 → 0.6.14
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/agents/outputContracts.d.ts +44 -1
- package/dist/agents/outputContracts.js +12 -2
- package/dist/agents/outputContracts.js.map +1 -1
- package/dist/agents/reviewLifecycle.js +54 -14
- package/dist/agents/reviewLifecycle.js.map +1 -1
- package/dist/agents/waveExecutor.d.ts +2 -13
- package/dist/audit/run.d.ts +7 -0
- package/dist/audit/run.js +198 -32
- package/dist/audit/run.js.map +1 -1
- package/dist/core/run.js +103 -28
- package/dist/core/run.js.map +1 -1
- package/dist/main.js +95 -29
- package/dist/main.js.map +1 -1
- package/dist/operations/artifacts.d.ts +13 -0
- package/dist/operations/artifacts.js +45 -0
- package/dist/operations/artifacts.js.map +1 -0
- package/dist/operations/change.d.ts +11 -0
- package/dist/operations/change.js +144 -0
- package/dist/operations/change.js.map +1 -0
- package/dist/operations/completion.d.ts +4 -1
- package/dist/operations/completion.js +69 -89
- package/dist/operations/completion.js.map +1 -1
- package/dist/operations/controller.d.ts +8 -5
- package/dist/operations/controller.js +270 -131
- package/dist/operations/controller.js.map +1 -1
- package/dist/operations/leadBinding.d.ts +2 -0
- package/dist/operations/leadBinding.js +22 -0
- package/dist/operations/leadBinding.js.map +1 -0
- package/dist/operations/liveness.d.ts +35 -0
- package/dist/operations/liveness.js +308 -0
- package/dist/operations/liveness.js.map +1 -0
- package/dist/operations/mcp.js +72 -55
- package/dist/operations/mcp.js.map +1 -1
- package/dist/operations/monitorProcess.d.ts +8 -0
- package/dist/operations/monitorProcess.js +33 -0
- package/dist/operations/monitorProcess.js.map +1 -0
- package/dist/operations/portfolio.d.ts +35 -0
- package/dist/operations/portfolio.js +158 -0
- package/dist/operations/portfolio.js.map +1 -0
- package/dist/operations/state.d.ts +174 -17
- package/dist/operations/state.js +178 -163
- package/dist/operations/state.js.map +1 -1
- package/dist/operations/supervisor.d.ts +38 -0
- package/dist/operations/supervisor.js +195 -0
- package/dist/operations/supervisor.js.map +1 -0
- package/dist/paseo/launchSpec.d.ts +4 -0
- package/dist/paseo/launchSpec.js +31 -37
- package/dist/paseo/launchSpec.js.map +1 -1
- package/dist/paseo/sdk.d.ts +5 -0
- package/dist/paseo/sdk.js +35 -87
- package/dist/paseo/sdk.js.map +1 -1
- package/dist/paseo/start.d.ts +1 -1
- package/dist/paseo/start.js +38 -87
- package/dist/paseo/start.js.map +1 -1
- package/dist/workers/agentPrompt.d.ts +2 -0
- package/dist/workers/agentPrompt.js +135 -190
- package/dist/workers/agentPrompt.js.map +1 -1
- package/docs/OPERATION_SUPERVISION.md +198 -0
- package/package.json +1 -1
- package/presets/agents/orchestration.jsonc +14 -4
- package/skills/engineering-workflow/SKILL.md +149 -104
|
@@ -1,5 +1,8 @@
|
|
|
1
|
-
export type OperationKind = "audit" | "run";
|
|
1
|
+
export type OperationKind = "audit" | "run" | "change";
|
|
2
2
|
export type OperationStatus = "QUEUED" | "RUNNING" | "SUCCEEDED" | "FAILED" | "CANCELLED";
|
|
3
|
+
export type OperationStageStatus = "PENDING" | "RUNNING" | "COMPLETED" | "FAILED" | "BLOCKED" | "SKIPPED";
|
|
4
|
+
export type OperationParticipantStatus = "REGISTERED" | "IDLE" | "RUNNING" | "COMPLETED" | "FAILED" | "BLOCKED" | "CANCELLED";
|
|
5
|
+
export type SupervisorGenerationStatus = "ACTIVE" | "DRAINING" | "ARCHIVED" | "FAILED";
|
|
3
6
|
export interface AuditOperationPayload {
|
|
4
7
|
request: string;
|
|
5
8
|
files?: string[];
|
|
@@ -10,59 +13,213 @@ export interface AuditOperationPayload {
|
|
|
10
13
|
export interface RunOperationPayload {
|
|
11
14
|
taskId: string;
|
|
12
15
|
profile?: string;
|
|
16
|
+
priority?: number;
|
|
13
17
|
}
|
|
14
|
-
export
|
|
18
|
+
export interface ChangeOperationPayload {
|
|
19
|
+
request: string;
|
|
20
|
+
title?: string;
|
|
21
|
+
taskId?: string;
|
|
22
|
+
files?: string[];
|
|
23
|
+
domains?: string[];
|
|
24
|
+
acceptance?: string[];
|
|
25
|
+
risk?: "low" | "medium" | "high";
|
|
26
|
+
profile?: string;
|
|
27
|
+
priority?: number;
|
|
28
|
+
}
|
|
29
|
+
export type OperationPayload = AuditOperationPayload | RunOperationPayload | ChangeOperationPayload;
|
|
15
30
|
export interface OperationAgentRecord {
|
|
16
31
|
id: string;
|
|
17
32
|
role?: string;
|
|
18
33
|
phase?: string;
|
|
19
34
|
workspaceId?: string;
|
|
20
|
-
transport?:
|
|
35
|
+
transport?: string;
|
|
36
|
+
registeredAt: string;
|
|
37
|
+
}
|
|
38
|
+
export interface OperationLeadBinding {
|
|
39
|
+
agentId: string;
|
|
40
|
+
source?: string;
|
|
41
|
+
generation: number;
|
|
42
|
+
boundAt: string;
|
|
43
|
+
acknowledgedRevision: number;
|
|
44
|
+
acknowledgedAt?: string;
|
|
45
|
+
}
|
|
46
|
+
export interface OperationSupervisorGeneration {
|
|
47
|
+
generation: number;
|
|
48
|
+
agentId?: string;
|
|
49
|
+
status: SupervisorGenerationStatus;
|
|
50
|
+
createdAt: string;
|
|
51
|
+
activatedAt?: string;
|
|
52
|
+
drainingAt?: string;
|
|
53
|
+
archivedAt?: string;
|
|
54
|
+
checkpointArtifact?: string;
|
|
55
|
+
contextRatio?: number;
|
|
56
|
+
error?: string;
|
|
57
|
+
}
|
|
58
|
+
export interface OperationSupervisionState {
|
|
59
|
+
required: boolean;
|
|
60
|
+
materialized: boolean;
|
|
61
|
+
activeGeneration?: number;
|
|
62
|
+
generations: OperationSupervisorGeneration[];
|
|
63
|
+
latestConsolidationRevision?: number;
|
|
64
|
+
latestConsolidationArtifact?: string;
|
|
65
|
+
}
|
|
66
|
+
export interface OperationStageRecord {
|
|
67
|
+
name: string;
|
|
68
|
+
status: OperationStageStatus;
|
|
69
|
+
revision: number;
|
|
70
|
+
startedAt?: string;
|
|
71
|
+
finishedAt?: string;
|
|
72
|
+
message?: string;
|
|
73
|
+
artifact?: string;
|
|
74
|
+
}
|
|
75
|
+
export interface OperationParticipantRecord {
|
|
76
|
+
id: string;
|
|
77
|
+
logicalAgent?: string;
|
|
78
|
+
role?: string;
|
|
79
|
+
stage?: string;
|
|
80
|
+
phase?: string;
|
|
81
|
+
parentSupervisorGeneration?: number;
|
|
82
|
+
parentAgentId?: string;
|
|
83
|
+
workspaceId?: string;
|
|
84
|
+
transport?: string;
|
|
85
|
+
status: OperationParticipantStatus;
|
|
21
86
|
registeredAt: string;
|
|
87
|
+
startedAt?: string;
|
|
88
|
+
finishedAt?: string;
|
|
89
|
+
resultArtifact?: string;
|
|
90
|
+
error?: string;
|
|
91
|
+
}
|
|
92
|
+
export interface OperationProgress {
|
|
93
|
+
expected: number;
|
|
94
|
+
registered: number;
|
|
95
|
+
running: number;
|
|
96
|
+
completed: number;
|
|
97
|
+
failed: number;
|
|
98
|
+
blocked: number;
|
|
22
99
|
}
|
|
23
|
-
export interface
|
|
100
|
+
export interface OperationNotificationState {
|
|
101
|
+
lastLeadWakeRevision: number;
|
|
102
|
+
lastLeadWakeAt?: string;
|
|
103
|
+
lastLeadWakeReason?: string;
|
|
104
|
+
terminalDelivered: boolean;
|
|
105
|
+
attempts: number;
|
|
106
|
+
lastError?: string;
|
|
107
|
+
}
|
|
108
|
+
export interface OperationIntentState {
|
|
109
|
+
request?: string;
|
|
110
|
+
classification?: "AUDIT" | "CHANGE" | "RUN";
|
|
111
|
+
mode?: "quick" | "spec";
|
|
112
|
+
risk?: "low" | "medium" | "high";
|
|
113
|
+
priority?: number;
|
|
114
|
+
}
|
|
115
|
+
export interface OperationRecordV1 {
|
|
24
116
|
version: 1;
|
|
25
117
|
id: string;
|
|
118
|
+
kind: "audit" | "run";
|
|
119
|
+
status: OperationStatus;
|
|
120
|
+
phase: string;
|
|
121
|
+
root: string;
|
|
122
|
+
payload: AuditOperationPayload | RunOperationPayload;
|
|
123
|
+
createdAt: string;
|
|
124
|
+
updatedAt: string;
|
|
125
|
+
startedAt?: string;
|
|
126
|
+
finishedAt?: string;
|
|
127
|
+
pid?: number;
|
|
128
|
+
workspaceId?: string;
|
|
129
|
+
workspaceWarning?: string;
|
|
130
|
+
agents?: OperationAgentRecord[];
|
|
131
|
+
cleanupWarnings?: string[];
|
|
132
|
+
result?: Record<string, unknown>;
|
|
133
|
+
error?: string;
|
|
134
|
+
}
|
|
135
|
+
export interface OperationRecordV2 {
|
|
136
|
+
version: 2;
|
|
137
|
+
id: string;
|
|
26
138
|
kind: OperationKind;
|
|
27
139
|
status: OperationStatus;
|
|
28
140
|
phase: string;
|
|
29
141
|
root: string;
|
|
142
|
+
workspaceRoot?: string;
|
|
30
143
|
payload: OperationPayload;
|
|
144
|
+
revision: number;
|
|
31
145
|
createdAt: string;
|
|
32
146
|
updatedAt: string;
|
|
147
|
+
lastProgressAt: string;
|
|
33
148
|
startedAt?: string;
|
|
34
149
|
finishedAt?: string;
|
|
35
150
|
pid?: number;
|
|
36
151
|
workspaceId?: string;
|
|
37
152
|
workspaceWarning?: string;
|
|
153
|
+
intent?: OperationIntentState;
|
|
154
|
+
lead?: OperationLeadBinding;
|
|
155
|
+
supervision: OperationSupervisionState;
|
|
156
|
+
stages: Record<string, OperationStageRecord>;
|
|
157
|
+
participants: Record<string, OperationParticipantRecord>;
|
|
158
|
+
progress: OperationProgress;
|
|
159
|
+
notification: OperationNotificationState;
|
|
38
160
|
agents?: OperationAgentRecord[];
|
|
39
161
|
cleanupWarnings?: string[];
|
|
40
162
|
result?: Record<string, unknown>;
|
|
41
163
|
error?: string;
|
|
42
164
|
}
|
|
165
|
+
export type OperationRecord = OperationRecordV1 | OperationRecordV2;
|
|
43
166
|
export interface TerminalOperationTransition {
|
|
44
|
-
record:
|
|
167
|
+
record: OperationRecordV2;
|
|
45
168
|
transitioned: boolean;
|
|
46
169
|
}
|
|
170
|
+
export interface OperationEvent {
|
|
171
|
+
version: 1;
|
|
172
|
+
operationId: string;
|
|
173
|
+
revision: number;
|
|
174
|
+
at: string;
|
|
175
|
+
type: string;
|
|
176
|
+
status: OperationStatus;
|
|
177
|
+
phase: string;
|
|
178
|
+
changed?: string[];
|
|
179
|
+
details?: Record<string, unknown>;
|
|
180
|
+
}
|
|
181
|
+
export declare function resolveOperationStateRoot(root: string): string;
|
|
47
182
|
export declare function operationFile(root: string, operationId: string): string;
|
|
48
|
-
export declare function
|
|
183
|
+
export declare function operationArtifactDir(root: string, operationId: string): string;
|
|
184
|
+
export declare function operationEventsFile(root: string, operationId: string): string;
|
|
185
|
+
export declare function loadOperation(root: string, operationId: string): Promise<OperationRecordV2>;
|
|
49
186
|
export declare function saveOperation(root: string, record: OperationRecord): Promise<void>;
|
|
50
|
-
export declare function patchOperation(root: string, operationId: string, patch: Partial<
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
* terminal transition. Callers must perform one-shot terminal side effects
|
|
54
|
-
* (for example the managed-lead completion callback) only when transitioned is
|
|
55
|
-
* true. A concurrent cancel/execute path receives the already-terminal record
|
|
56
|
-
* without becoming a second side-effect owner.
|
|
57
|
-
*/
|
|
58
|
-
export declare function transitionOperationToTerminal(root: string, operationId: string, patch: Partial<OperationRecord> & {
|
|
187
|
+
export declare function patchOperation(root: string, operationId: string, patch: Partial<OperationRecordV2>): Promise<OperationRecordV2>;
|
|
188
|
+
export declare function patchOperationMetadata(root: string, operationId: string, patch: Partial<OperationRecordV2>): Promise<OperationRecordV2>;
|
|
189
|
+
export declare function transitionOperationToTerminal(root: string, operationId: string, patch: Partial<OperationRecordV2> & {
|
|
59
190
|
status: "SUCCEEDED" | "FAILED" | "CANCELLED";
|
|
60
191
|
}): Promise<TerminalOperationTransition>;
|
|
61
|
-
export declare function
|
|
62
|
-
export declare function
|
|
192
|
+
export declare function bindOperationLead(root: string, operationId: string, agentId: string, source?: string): Promise<OperationRecordV2>;
|
|
193
|
+
export declare function acknowledgeOperationLead(root: string, operationId: string, revision: number, reason?: string): Promise<OperationRecordV2>;
|
|
194
|
+
export declare function markTerminalDelivered(root: string, operationId: string, attempts: number, error?: string): Promise<OperationRecordV2>;
|
|
195
|
+
export declare function setOperationStage(root: string, operationId: string, name: string, status: OperationStageStatus, options?: {
|
|
196
|
+
message?: string;
|
|
197
|
+
artifact?: string;
|
|
198
|
+
}): Promise<OperationRecordV2>;
|
|
199
|
+
export declare function registerSupervisorGeneration(root: string, operationId: string, input: {
|
|
200
|
+
agentId?: string;
|
|
201
|
+
materialized: boolean;
|
|
202
|
+
checkpointArtifact?: string;
|
|
203
|
+
}): Promise<OperationRecordV2>;
|
|
204
|
+
export declare function updateSupervisorGeneration(root: string, operationId: string, generation: number, patch: Partial<OperationSupervisorGeneration>): Promise<OperationRecordV2>;
|
|
205
|
+
export declare function activeOperationSupervisor(record: OperationRecordV2): OperationSupervisorGeneration | undefined;
|
|
206
|
+
export declare function registerOperationAgent(root: string, operationId: string, agent: Omit<OperationAgentRecord, "registeredAt"> & {
|
|
207
|
+
logicalAgent?: string;
|
|
208
|
+
parentAgentId?: string;
|
|
209
|
+
parentSupervisorGeneration?: number;
|
|
210
|
+
}): Promise<OperationRecordV2>;
|
|
211
|
+
export declare function updateOperationParticipant(root: string, operationId: string, agentId: string, patch: Partial<Omit<OperationParticipantRecord, "id" | "registeredAt">>): Promise<OperationRecordV2>;
|
|
212
|
+
export declare function registerCurrentOperationAgent(root: string, agent: Omit<OperationAgentRecord, "registeredAt"> & {
|
|
213
|
+
logicalAgent?: string;
|
|
214
|
+
parentAgentId?: string;
|
|
215
|
+
parentSupervisorGeneration?: number;
|
|
216
|
+
}): Promise<void>;
|
|
63
217
|
export declare function currentOperationContext(): {
|
|
64
218
|
id?: string;
|
|
65
219
|
kind?: string;
|
|
66
220
|
workspaceId?: string;
|
|
221
|
+
controlRoot?: string;
|
|
67
222
|
};
|
|
68
223
|
export declare function updateCurrentOperationPhase(root: string, phase: string): Promise<void>;
|
|
224
|
+
export declare function normalizeOperationRecord(record: OperationRecord): OperationRecordV2;
|
|
225
|
+
export declare function isTerminalOperation(status: OperationStatus): boolean;
|
package/dist/operations/state.js
CHANGED
|
@@ -5,205 +5,220 @@ const OPERATIONS_DIR = ".harness/operations";
|
|
|
5
5
|
const LOCK_RETRY_MS = 20;
|
|
6
6
|
const LOCK_TIMEOUT_MS = 10_000;
|
|
7
7
|
const STALE_LOCK_MS = 30_000;
|
|
8
|
-
export function
|
|
9
|
-
|
|
8
|
+
export function resolveOperationStateRoot(root) {
|
|
9
|
+
const configured = process.env.AEH_OPERATION_ID?.trim() ? process.env.AEH_CONTROL_ROOT?.trim() : undefined;
|
|
10
|
+
return path.resolve(configured || root);
|
|
10
11
|
}
|
|
12
|
+
export function operationFile(root, operationId) { return path.resolve(resolveOperationStateRoot(root), OPERATIONS_DIR, `${safeId(operationId)}.json`); }
|
|
13
|
+
export function operationArtifactDir(root, operationId) { return path.resolve(resolveOperationStateRoot(root), OPERATIONS_DIR, safeId(operationId)); }
|
|
14
|
+
export function operationEventsFile(root, operationId) { return path.join(operationArtifactDir(root, operationId), "events.ndjson"); }
|
|
11
15
|
export async function loadOperation(root, operationId) {
|
|
12
|
-
return JSON.parse(await fs.readFile(operationFile(root, operationId), "utf8"));
|
|
16
|
+
return normalizeOperationRecord(JSON.parse(await fs.readFile(operationFile(root, operationId), "utf8")));
|
|
13
17
|
}
|
|
14
18
|
export async function saveOperation(root, record) {
|
|
15
|
-
const
|
|
19
|
+
const normalized = normalizeOperationRecord(record);
|
|
20
|
+
const stateRoot = resolveOperationStateRoot(root);
|
|
21
|
+
const file = operationFile(stateRoot, normalized.id);
|
|
16
22
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
17
|
-
await withOperationLock(file, async () => writeRecord(file,
|
|
23
|
+
await withOperationLock(file, async () => { await writeRecord(file, normalized); await appendOperationEvent(stateRoot, normalized, "operation.created", ["status", "phase"]); });
|
|
18
24
|
}
|
|
19
|
-
export async function patchOperation(root, operationId, patch) {
|
|
20
|
-
|
|
21
|
-
const guardedPatch = guardTerminalTransition(current, patch);
|
|
22
|
-
return { ...current, ...guardedPatch };
|
|
23
|
-
});
|
|
24
|
-
}
|
|
25
|
-
/**
|
|
26
|
-
* Atomically grants exactly one controller path ownership of a non-terminal ->
|
|
27
|
-
* terminal transition. Callers must perform one-shot terminal side effects
|
|
28
|
-
* (for example the managed-lead completion callback) only when transitioned is
|
|
29
|
-
* true. A concurrent cancel/execute path receives the already-terminal record
|
|
30
|
-
* without becoming a second side-effect owner.
|
|
31
|
-
*/
|
|
25
|
+
export async function patchOperation(root, operationId, patch) { return mutateOperation(root, operationId, patch, true, "operation.updated"); }
|
|
26
|
+
export async function patchOperationMetadata(root, operationId, patch) { return mutateOperation(root, operationId, patch, false, "operation.metadata"); }
|
|
32
27
|
export async function transitionOperationToTerminal(root, operationId, patch) {
|
|
33
|
-
const
|
|
28
|
+
const stateRoot = resolveOperationStateRoot(root);
|
|
29
|
+
const file = operationFile(stateRoot, operationId);
|
|
34
30
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
35
31
|
return withOperationLock(file, async () => {
|
|
36
|
-
const current = JSON.parse(await fs.readFile(file, "utf8"));
|
|
32
|
+
const current = normalizeOperationRecord(JSON.parse(await fs.readFile(file, "utf8")));
|
|
37
33
|
if (isTerminal(current.status))
|
|
38
34
|
return { record: current, transitioned: false };
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
id: current.id,
|
|
43
|
-
kind: current.kind,
|
|
44
|
-
version: 1,
|
|
45
|
-
updatedAt: new Date().toISOString()
|
|
46
|
-
};
|
|
35
|
+
const now = new Date().toISOString();
|
|
36
|
+
const revision = current.revision + 1;
|
|
37
|
+
const next = normalizeOperationRecord({ ...current, ...patch, version: 2, id: current.id, kind: current.kind, revision, updatedAt: now, lastProgressAt: now, stages: { ...current.stages, finished: { name: "finished", status: terminalStageStatus(patch.status), revision, startedAt: now, finishedAt: now } } });
|
|
47
38
|
await writeRecord(file, next);
|
|
39
|
+
await appendOperationEvent(stateRoot, next, "operation.terminal", ["status", "phase"]);
|
|
48
40
|
return { record: next, transitioned: true };
|
|
49
41
|
});
|
|
50
42
|
}
|
|
43
|
+
export async function bindOperationLead(root, operationId, agentId, source) {
|
|
44
|
+
return mutateOperation(root, operationId, {}, true, "operation.lead.bound", (current, revision, now) => ({ ...current, revision, updatedAt: now, lastProgressAt: now, lead: { agentId: requiredId(agentId), source, generation: (current.lead?.generation ?? 0) + 1, boundAt: now, acknowledgedRevision: revision, acknowledgedAt: now }, notification: { ...current.notification, lastLeadWakeRevision: revision, lastLeadWakeAt: now, lastLeadWakeReason: "operation-started" } }));
|
|
45
|
+
}
|
|
46
|
+
export async function acknowledgeOperationLead(root, operationId, revision, reason) {
|
|
47
|
+
const current = await loadOperation(root, operationId);
|
|
48
|
+
const now = new Date().toISOString();
|
|
49
|
+
return patchOperationMetadata(root, operationId, { lead: current.lead ? { ...current.lead, acknowledgedRevision: Math.max(current.lead.acknowledgedRevision, revision), acknowledgedAt: now } : undefined, notification: { ...current.notification, lastLeadWakeRevision: Math.max(current.notification.lastLeadWakeRevision, revision), lastLeadWakeAt: now, lastLeadWakeReason: reason ?? current.notification.lastLeadWakeReason } });
|
|
50
|
+
}
|
|
51
|
+
export async function markTerminalDelivered(root, operationId, attempts, error) {
|
|
52
|
+
const current = await loadOperation(root, operationId);
|
|
53
|
+
return patchOperationMetadata(root, operationId, { notification: { ...current.notification, lastLeadWakeRevision: error ? current.notification.lastLeadWakeRevision : current.revision, lastLeadWakeAt: error ? current.notification.lastLeadWakeAt : new Date().toISOString(), lastLeadWakeReason: error ? current.notification.lastLeadWakeReason : "terminal", terminalDelivered: !error, attempts, lastError: error } });
|
|
54
|
+
}
|
|
55
|
+
export async function setOperationStage(root, operationId, name, status, options = {}) {
|
|
56
|
+
return mutateOperation(root, operationId, {}, true, "operation.stage", (current, revision, now) => {
|
|
57
|
+
const previous = current.stages[name];
|
|
58
|
+
const terminal = ["COMPLETED", "FAILED", "BLOCKED", "SKIPPED"].includes(status);
|
|
59
|
+
const stage = { name, status, revision, startedAt: previous?.startedAt ?? (status === "RUNNING" ? now : undefined), finishedAt: terminal ? now : undefined, message: options.message ?? previous?.message, artifact: options.artifact ?? previous?.artifact };
|
|
60
|
+
return { ...current, revision, updatedAt: now, lastProgressAt: now, phase: name, stages: { ...current.stages, [name]: stage } };
|
|
61
|
+
});
|
|
62
|
+
}
|
|
63
|
+
export async function registerSupervisorGeneration(root, operationId, input) {
|
|
64
|
+
return mutateOperation(root, operationId, {}, true, "operation.supervisor.registered", (current, revision, now) => {
|
|
65
|
+
const generations = current.supervision.generations.map((item) => item.status === "ACTIVE" ? { ...item, status: "DRAINING", drainingAt: item.drainingAt ?? now } : item);
|
|
66
|
+
const generation = Math.max(0, ...generations.map((item) => item.generation)) + 1;
|
|
67
|
+
generations.push({ generation, agentId: input.agentId, status: "ACTIVE", createdAt: now, activatedAt: now, checkpointArtifact: input.checkpointArtifact });
|
|
68
|
+
return { ...current, revision, updatedAt: now, lastProgressAt: now, supervision: { ...current.supervision, required: true, materialized: current.supervision.materialized || input.materialized, activeGeneration: generation, generations } };
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
export async function updateSupervisorGeneration(root, operationId, generation, patch) {
|
|
72
|
+
return mutateOperation(root, operationId, {}, true, "operation.supervisor.updated", (current, revision, now) => {
|
|
73
|
+
const generations = current.supervision.generations.map((item) => item.generation === generation ? { ...item, ...patch } : item);
|
|
74
|
+
const active = generations.find((item) => item.status === "ACTIVE");
|
|
75
|
+
return { ...current, revision, updatedAt: now, lastProgressAt: now, supervision: { ...current.supervision, activeGeneration: active?.generation, generations } };
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
export function activeOperationSupervisor(record) { const generation = record.supervision.activeGeneration; return generation === undefined ? undefined : record.supervision.generations.find((item) => item.generation === generation && item.status === "ACTIVE"); }
|
|
51
79
|
export async function registerOperationAgent(root, operationId, agent) {
|
|
52
|
-
return mutateOperation(root, operationId, (current) => {
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
const
|
|
56
|
-
const
|
|
57
|
-
const
|
|
58
|
-
|
|
59
|
-
...
|
|
60
|
-
|
|
61
|
-
};
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
agents: [...existing.filter((item) => item.id !== agent.id), next]
|
|
65
|
-
};
|
|
80
|
+
return mutateOperation(root, operationId, {}, true, "operation.participant.registered", (current, revision, now) => {
|
|
81
|
+
const existingAgents = current.agents ?? [];
|
|
82
|
+
const previousAgent = existingAgents.find((item) => item.id === agent.id);
|
|
83
|
+
const compatibility = { ...previousAgent, id: agent.id, role: agent.role ?? previousAgent?.role, phase: agent.phase ?? previousAgent?.phase, workspaceId: agent.workspaceId ?? previousAgent?.workspaceId, transport: agent.transport ?? previousAgent?.transport, registeredAt: previousAgent?.registeredAt ?? now };
|
|
84
|
+
const agents = [...existingAgents.filter((item) => item.id !== agent.id), compatibility];
|
|
85
|
+
const logicalAgent = agent.logicalAgent ?? agent.role;
|
|
86
|
+
if (logicalAgent === "operation-supervisor")
|
|
87
|
+
return { ...current, revision, updatedAt: now, lastProgressAt: now, agents };
|
|
88
|
+
const previous = current.participants[agent.id];
|
|
89
|
+
const participant = { ...previous, id: agent.id, logicalAgent: logicalAgent ?? previous?.logicalAgent, role: agent.role ?? previous?.role, stage: agent.phase ?? previous?.stage, phase: agent.phase ?? previous?.phase, parentSupervisorGeneration: agent.parentSupervisorGeneration ?? previous?.parentSupervisorGeneration ?? current.supervision.activeGeneration, parentAgentId: agent.parentAgentId ?? previous?.parentAgentId ?? activeOperationSupervisor(current)?.agentId, workspaceId: agent.workspaceId ?? previous?.workspaceId, transport: agent.transport ?? previous?.transport, status: previous?.status ?? "REGISTERED", registeredAt: previous?.registeredAt ?? now };
|
|
90
|
+
const participants = { ...current.participants, [agent.id]: participant };
|
|
91
|
+
return { ...current, revision, updatedAt: now, lastProgressAt: now, agents, participants, progress: deriveProgress(participants) };
|
|
66
92
|
});
|
|
67
93
|
}
|
|
68
|
-
export async function
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
}
|
|
75
|
-
catch {
|
|
76
|
-
// Direct/non-controller execution may carry stale environment state. Agent
|
|
77
|
-
// registration is observability/cleanup metadata, not execution authority.
|
|
78
|
-
}
|
|
94
|
+
export async function updateOperationParticipant(root, operationId, agentId, patch) {
|
|
95
|
+
return mutateOperation(root, operationId, {}, true, "operation.participant.updated", (current, revision, now) => {
|
|
96
|
+
const previous = current.participants[agentId] ?? { id: agentId, status: "REGISTERED", registeredAt: now };
|
|
97
|
+
const next = { ...previous, ...patch, id: agentId, registeredAt: previous.registeredAt, startedAt: patch.status === "RUNNING" ? previous.startedAt ?? now : patch.startedAt ?? previous.startedAt, finishedAt: ["COMPLETED", "FAILED", "CANCELLED"].includes(patch.status ?? "") ? patch.finishedAt ?? now : patch.finishedAt ?? previous.finishedAt };
|
|
98
|
+
const participants = { ...current.participants, [agentId]: next };
|
|
99
|
+
return { ...current, revision, updatedAt: now, lastProgressAt: now, participants, progress: deriveProgress(participants) };
|
|
100
|
+
});
|
|
79
101
|
}
|
|
80
|
-
export function currentOperationContext()
|
|
81
|
-
return {
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
102
|
+
export async function registerCurrentOperationAgent(root, agent) { const operationId = currentOperationContext().id; if (!operationId)
|
|
103
|
+
return; try {
|
|
104
|
+
await registerOperationAgent(resolveOperationStateRoot(root), operationId, agent);
|
|
105
|
+
}
|
|
106
|
+
catch { /* stale direct execution metadata is non-authoritative */ } }
|
|
107
|
+
export function currentOperationContext() { return { id: process.env.AEH_OPERATION_ID?.trim() || undefined, kind: process.env.AEH_OPERATION_KIND?.trim() || undefined, workspaceId: process.env.AEH_OPERATION_WORKSPACE_ID?.trim() || undefined, controlRoot: process.env.AEH_CONTROL_ROOT?.trim() || undefined }; }
|
|
108
|
+
export async function updateCurrentOperationPhase(root, phase) { const operationId = currentOperationContext().id; if (!operationId)
|
|
109
|
+
return; try {
|
|
110
|
+
await setOperationStage(resolveOperationStateRoot(root), operationId, phase, "RUNNING");
|
|
111
|
+
}
|
|
112
|
+
catch { /* direct/non-controller */ } }
|
|
113
|
+
export function normalizeOperationRecord(record) {
|
|
114
|
+
if (record.version === 2) {
|
|
115
|
+
const participants = record.participants ?? {};
|
|
116
|
+
return { ...record, version: 2, revision: Math.max(1, record.revision || 1), lastProgressAt: record.lastProgressAt || record.updatedAt, supervision: record.supervision ?? defaultSupervision(record.kind), stages: record.stages ?? {}, participants, progress: record.progress ?? deriveProgress(participants), notification: record.notification ?? defaultNotification() };
|
|
93
117
|
}
|
|
94
|
-
|
|
95
|
-
|
|
118
|
+
const participants = {};
|
|
119
|
+
for (const agent of record.agents ?? []) {
|
|
120
|
+
if (agent.role === "operation-supervisor")
|
|
121
|
+
continue;
|
|
122
|
+
participants[agent.id] = { id: agent.id, logicalAgent: agent.role, role: agent.role, stage: agent.phase, phase: agent.phase, workspaceId: agent.workspaceId, transport: agent.transport, status: "REGISTERED", registeredAt: agent.registeredAt };
|
|
96
123
|
}
|
|
124
|
+
return { ...record, version: 2, kind: record.kind, payload: record.payload, revision: 1, lastProgressAt: record.updatedAt, intent: inferIntent(record.kind, record.payload), supervision: defaultSupervision(record.kind), stages: record.phase ? { [record.phase]: { name: record.phase, status: isTerminal(record.status) ? terminalStageStatus(record.status) : "RUNNING", revision: 1, startedAt: record.startedAt, finishedAt: record.finishedAt } } : {}, participants, progress: deriveProgress(participants), notification: defaultNotification() };
|
|
97
125
|
}
|
|
98
|
-
async function mutateOperation(root, operationId,
|
|
99
|
-
const
|
|
126
|
+
async function mutateOperation(root, operationId, patch, touchRevision, eventType, custom) {
|
|
127
|
+
const stateRoot = resolveOperationStateRoot(root);
|
|
128
|
+
const file = operationFile(stateRoot, operationId);
|
|
100
129
|
await fs.mkdir(path.dirname(file), { recursive: true });
|
|
101
130
|
return withOperationLock(file, async () => {
|
|
102
|
-
const current = JSON.parse(await fs.readFile(file, "utf8"));
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
version: 1,
|
|
109
|
-
updatedAt: new Date().toISOString()
|
|
110
|
-
};
|
|
131
|
+
const current = normalizeOperationRecord(JSON.parse(await fs.readFile(file, "utf8")));
|
|
132
|
+
const guardedPatch = guardTerminalTransition(current, patch);
|
|
133
|
+
const now = new Date().toISOString();
|
|
134
|
+
const revision = touchRevision ? current.revision + 1 : current.revision;
|
|
135
|
+
const candidate = custom ? custom(current, revision, now) : { ...current, ...guardedPatch, version: 2, id: current.id, kind: current.kind, revision, updatedAt: now, lastProgressAt: touchRevision ? now : current.lastProgressAt };
|
|
136
|
+
const next = normalizeOperationRecord(candidate);
|
|
111
137
|
await writeRecord(file, next);
|
|
138
|
+
await appendOperationEvent(stateRoot, next, eventType, Object.keys(patch));
|
|
112
139
|
return next;
|
|
113
140
|
});
|
|
114
141
|
}
|
|
115
|
-
async function
|
|
116
|
-
|
|
117
|
-
await fs.
|
|
118
|
-
try {
|
|
119
|
-
await fs.rename(temp, file);
|
|
120
|
-
}
|
|
121
|
-
finally {
|
|
122
|
-
await fs.rm(temp, { force: true }).catch(() => undefined);
|
|
123
|
-
}
|
|
142
|
+
async function appendOperationEvent(root, record, type, changed, details) { const file = operationEventsFile(root, record.id); await fs.mkdir(path.dirname(file), { recursive: true }); const event = { version: 1, operationId: record.id, revision: record.revision, at: new Date().toISOString(), type, status: record.status, phase: record.phase, changed, details }; await fs.appendFile(file, `${JSON.stringify(event)}\n`); }
|
|
143
|
+
async function writeRecord(file, record) { const temp = `${file}.${process.pid}.${crypto.randomUUID()}.tmp`; await fs.writeFile(temp, `${JSON.stringify(record, null, 2)}\n`); try {
|
|
144
|
+
await fs.rename(temp, file);
|
|
124
145
|
}
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
146
|
+
finally {
|
|
147
|
+
await fs.rm(temp, { force: true }).catch(() => undefined);
|
|
148
|
+
} }
|
|
149
|
+
async function withOperationLock(file, action) { const lock = `${file}.lock`; const deadline = Date.now() + LOCK_TIMEOUT_MS; for (;;) {
|
|
150
|
+
let handle;
|
|
151
|
+
try {
|
|
152
|
+
handle = await fs.open(lock, "wx");
|
|
130
153
|
try {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
await handle.writeFile(`${process.pid}\n`);
|
|
134
|
-
return await action();
|
|
135
|
-
}
|
|
136
|
-
finally {
|
|
137
|
-
await handle.close().catch(() => undefined);
|
|
138
|
-
await fs.rm(lock, { force: true }).catch(() => undefined);
|
|
139
|
-
}
|
|
154
|
+
await handle.writeFile(`${process.pid}\n`);
|
|
155
|
+
return await action();
|
|
140
156
|
}
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
await fs.rm(lock, { force: true }).catch(() => undefined);
|
|
145
|
-
throw error;
|
|
146
|
-
}
|
|
147
|
-
if (!isAlreadyExists(error))
|
|
148
|
-
throw error;
|
|
149
|
-
if (await canRecoverLock(lock)) {
|
|
150
|
-
await fs.rm(lock, { force: true }).catch(() => undefined);
|
|
151
|
-
continue;
|
|
152
|
-
}
|
|
153
|
-
if (Date.now() >= deadline) {
|
|
154
|
-
throw new Error(`Timed out acquiring operation state lock for ${path.basename(file)}.`);
|
|
155
|
-
}
|
|
156
|
-
await delay(LOCK_RETRY_MS);
|
|
157
|
+
finally {
|
|
158
|
+
await handle.close().catch(() => undefined);
|
|
159
|
+
await fs.rm(lock, { force: true }).catch(() => undefined);
|
|
157
160
|
}
|
|
158
161
|
}
|
|
159
|
-
}
|
|
160
|
-
async function canRecoverLock(lock) {
|
|
161
|
-
try {
|
|
162
|
-
const [rawPid, stat] = await Promise.all([
|
|
163
|
-
fs.readFile(lock, "utf8").catch(() => ""),
|
|
164
|
-
fs.stat(lock)
|
|
165
|
-
]);
|
|
166
|
-
const ownerPid = Number.parseInt(rawPid.trim(), 10);
|
|
167
|
-
if (Number.isInteger(ownerPid) && ownerPid > 0 && !processAlive(ownerPid))
|
|
168
|
-
return true;
|
|
169
|
-
return Date.now() - stat.mtimeMs > STALE_LOCK_MS;
|
|
170
|
-
}
|
|
171
|
-
catch {
|
|
172
|
-
return true;
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
function processAlive(pid) {
|
|
176
|
-
try {
|
|
177
|
-
process.kill(pid, 0);
|
|
178
|
-
return true;
|
|
179
|
-
}
|
|
180
162
|
catch (error) {
|
|
181
|
-
|
|
163
|
+
if (handle) {
|
|
164
|
+
await handle.close().catch(() => undefined);
|
|
165
|
+
await fs.rm(lock, { force: true }).catch(() => undefined);
|
|
166
|
+
throw error;
|
|
167
|
+
}
|
|
168
|
+
if (!isAlreadyExists(error))
|
|
169
|
+
throw error;
|
|
170
|
+
if (await canRecoverLock(lock)) {
|
|
171
|
+
await fs.rm(lock, { force: true }).catch(() => undefined);
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (Date.now() >= deadline)
|
|
175
|
+
throw new Error(`Timed out acquiring operation state lock for ${path.basename(file)}.`);
|
|
176
|
+
await delay(LOCK_RETRY_MS);
|
|
182
177
|
}
|
|
183
|
-
}
|
|
178
|
+
} }
|
|
179
|
+
async function canRecoverLock(lock) { try {
|
|
180
|
+
const [rawPid, stat] = await Promise.all([fs.readFile(lock, "utf8").catch(() => ""), fs.stat(lock)]);
|
|
181
|
+
const ownerPid = Number.parseInt(rawPid.trim(), 10);
|
|
182
|
+
if (Number.isInteger(ownerPid) && ownerPid > 0 && !processAlive(ownerPid))
|
|
183
|
+
return true;
|
|
184
|
+
return Date.now() - stat.mtimeMs > STALE_LOCK_MS;
|
|
185
|
+
}
|
|
186
|
+
catch {
|
|
187
|
+
return true;
|
|
188
|
+
} }
|
|
189
|
+
function processAlive(pid) { try {
|
|
190
|
+
process.kill(pid, 0);
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
catch {
|
|
194
|
+
return false;
|
|
195
|
+
} }
|
|
184
196
|
function guardTerminalTransition(current, patch) {
|
|
185
197
|
if (!isTerminal(current.status))
|
|
186
198
|
return patch;
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
}
|
|
195
|
-
function
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
}
|
|
204
|
-
function
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
return
|
|
208
|
-
|
|
199
|
+
// Once one controller owns terminalization, late execution/cancellation paths
|
|
200
|
+
// may still append cleanup/notification metadata but cannot rewrite the
|
|
201
|
+
// terminal outcome, phase, result, error or finish timestamp.
|
|
202
|
+
const { status: _status, phase: _phase, result: _result, error: _error, finishedAt: _finishedAt, ...metadata } = patch;
|
|
203
|
+
return metadata;
|
|
204
|
+
}
|
|
205
|
+
function deriveProgress(participants) { const values = Object.values(participants); return { expected: values.length, registered: values.filter((item) => item.status === "REGISTERED" || item.status === "IDLE").length, running: values.filter((item) => item.status === "RUNNING").length, completed: values.filter((item) => item.status === "COMPLETED").length, failed: values.filter((item) => item.status === "FAILED" || item.status === "CANCELLED").length, blocked: values.filter((item) => item.status === "BLOCKED").length }; }
|
|
206
|
+
function defaultSupervision(kind) { return { required: kind === "audit" || kind === "change", materialized: false, generations: [] }; }
|
|
207
|
+
function defaultNotification() { return { lastLeadWakeRevision: 0, terminalDelivered: false, attempts: 0 }; }
|
|
208
|
+
function inferIntent(kind, payload) { if (kind === "audit") {
|
|
209
|
+
const audit = payload;
|
|
210
|
+
return { request: audit.request, classification: "AUDIT", risk: audit.risk };
|
|
211
|
+
} if (kind === "change") {
|
|
212
|
+
const change = payload;
|
|
213
|
+
return { request: change.request, classification: "CHANGE", risk: change.risk, priority: change.priority };
|
|
214
|
+
} return { classification: "RUN", priority: payload.priority }; }
|
|
215
|
+
function terminalStageStatus(status) { return status === "SUCCEEDED" ? "COMPLETED" : status === "CANCELLED" ? "SKIPPED" : "FAILED"; }
|
|
216
|
+
export function isTerminalOperation(status) { return isTerminal(status); }
|
|
217
|
+
function isTerminal(status) { return status === "SUCCEEDED" || status === "FAILED" || status === "CANCELLED"; }
|
|
218
|
+
function requiredId(value) { const trimmed = value.trim(); if (!trimmed)
|
|
219
|
+
throw new Error("agent id is required"); return trimmed; }
|
|
220
|
+
function safeId(value) { if (!/^[A-Za-z0-9._-]+$/.test(value))
|
|
221
|
+
throw new Error(`Invalid operation id '${value}'.`); return value; }
|
|
222
|
+
function isAlreadyExists(error) { return Boolean(error && typeof error === "object" && "code" in error && error.code === "EEXIST"); }
|
|
223
|
+
function delay(ms) { return new Promise((resolve) => setTimeout(resolve, ms)); }
|
|
209
224
|
//# sourceMappingURL=state.js.map
|