@zq-silk/yui 0.0.0
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/ARCHITECTURE.md +141 -0
- package/LICENSE +21 -0
- package/README.md +211 -0
- package/dist/agent/adapterCatalog.js +10 -0
- package/dist/agent/agent.js +89 -0
- package/dist/agent/agentRegistry.js +10 -0
- package/dist/agent/argumentPolicy.js +80 -0
- package/dist/brief/taskBrief.js +37 -0
- package/dist/cli/commandCatalog.js +647 -0
- package/dist/cli/completion.js +111 -0
- package/dist/cli/completionWizard.js +143 -0
- package/dist/cli/dynamicCompletion.js +48 -0
- package/dist/cli/helpRenderer.js +32 -0
- package/dist/cli/interactionCandidates.js +139 -0
- package/dist/cli/interactionPolicy.js +389 -0
- package/dist/cli/interactiveSelection.js +185 -0
- package/dist/cli/invocationRouter.js +51 -0
- package/dist/cli/roleOptionCatalog.js +67 -0
- package/dist/cli/roleWizard.js +546 -0
- package/dist/cli/selectionPorts.js +1 -0
- package/dist/cli/updateCommand.js +22 -0
- package/dist/cli.js +402 -0
- package/dist/commands/agentCommands.js +196 -0
- package/dist/commands/globalRoleCommands.js +367 -0
- package/dist/commands/jobCommands.js +100 -0
- package/dist/commands/operatorCommands.js +38 -0
- package/dist/commands/repositoryCommands.js +86 -0
- package/dist/commands/roleConfiguration.js +201 -0
- package/dist/commands/taskCommands.js +1344 -0
- package/dist/commands/taskContextCommand.js +215 -0
- package/dist/commands/taskInputCommands.js +423 -0
- package/dist/commands/taskRoleRuntimeStatus.js +152 -0
- package/dist/completion/completionInstaller.js +168 -0
- package/dist/completion/completionPort.js +1 -0
- package/dist/completion/completionState.js +137 -0
- package/dist/completion/completionWizard.js +125 -0
- package/dist/completion/fileCompletionManager.js +51 -0
- package/dist/config/yuiConfig.js +17 -0
- package/dist/context/dispatchContext.js +74 -0
- package/dist/controller/clientRuntime.js +215 -0
- package/dist/controller/controller.js +158 -0
- package/dist/controller/controllerMain.js +37 -0
- package/dist/controller/fileSchedulerStoreAdapter.js +322 -0
- package/dist/controller/runtime.js +31 -0
- package/dist/controller/sessionNotify.js +136 -0
- package/dist/core/controllerClient.js +127 -0
- package/dist/core/controllerServer.js +269 -0
- package/dist/core/protocol.js +169 -0
- package/dist/decision/decision.js +42 -0
- package/dist/doctor/doctor.js +229 -0
- package/dist/errors/cliError.js +38 -0
- package/dist/event/taskEvent.js +44 -0
- package/dist/executor/agentAdapter.js +338 -0
- package/dist/executor/agentExecutor.js +144 -0
- package/dist/executor/executorRegistry.js +101 -0
- package/dist/executor/fileRoleLaunchPlanner.js +156 -0
- package/dist/executor/launchPlan.js +16 -0
- package/dist/input/inputRequest.js +326 -0
- package/dist/message/message.js +69 -0
- package/dist/milestone/milestone.js +27 -0
- package/dist/operator/operatorContext.js +66 -0
- package/dist/output/rolePresentation.js +82 -0
- package/dist/output/table.js +77 -0
- package/dist/output/terminal.js +198 -0
- package/dist/repository/gitWorkspace.js +210 -0
- package/dist/repository/repository.js +55 -0
- package/dist/repository/taskWorkspacePreparer.js +256 -0
- package/dist/role/role.js +246 -0
- package/dist/role/systemRoles.js +20 -0
- package/dist/run/agentRun.js +102 -0
- package/dist/scheduler/activeRoleRunDelivery.js +94 -0
- package/dist/scheduler/archivedTaskRuntime.js +12 -0
- package/dist/scheduler/leaderFailure.js +18 -0
- package/dist/scheduler/leaderWakeupProcessor.js +143 -0
- package/dist/scheduler/operatorInputNotificationProcessor.js +85 -0
- package/dist/scheduler/operatorNotification.js +17 -0
- package/dist/scheduler/pendingWakeup.js +33 -0
- package/dist/scheduler/ports.js +1 -0
- package/dist/scheduler/roleRunLiveness.js +41 -0
- package/dist/scheduler/wakeupQueue.js +13 -0
- package/dist/setup/setupCommand.js +317 -0
- package/dist/storage/durableFile.js +38 -0
- package/dist/storage/storageSchema.js +259 -0
- package/dist/storage/taskStore.js +1032 -0
- package/dist/task/task.js +216 -0
- package/dist/tmux/commandExecutor.js +69 -0
- package/dist/tmux/terminalHandoff.js +17 -0
- package/dist/tmux/tmuxManager.js +408 -0
- package/dist/workItem/workItem.js +45 -0
- package/dist/worktree/roleWorkspace.js +62 -0
- package/i18n/README.zh-CN.md +205 -0
- package/package.json +47 -0
- package/skills/yui-leader/SKILL.md +72 -0
- package/skills/yui-operator/SKILL.md +57 -0
- package/skills/yui-worker/SKILL.md +31 -0
|
@@ -0,0 +1,322 @@
|
|
|
1
|
+
import { createRoleSessionSet, recordRoleAgentSession, updateRoleAgentSessionStatus } from "../executor/agentExecutor.js";
|
|
2
|
+
import { createTaskEvent } from "../event/taskEvent.js";
|
|
3
|
+
import { answerInputRequest } from "../input/inputRequest.js";
|
|
4
|
+
import { activeRoleAgentBinding, updateRoleStatus } from "../role/role.js";
|
|
5
|
+
import { SYSTEM_OPERATOR_ROLE } from "../role/systemRoles.js";
|
|
6
|
+
import { failAgentRun, markAgentRunDelivered } from "../run/agentRun.js";
|
|
7
|
+
import { pendingWakeupsMatch } from "../scheduler/pendingWakeup.js";
|
|
8
|
+
import { queueLeaderWakeup } from "../scheduler/wakeupQueue.js";
|
|
9
|
+
import { updateWorkItemStatus } from "../workItem/workItem.js";
|
|
10
|
+
/** Maps the authoritative FileTaskStore records to the scheduler's narrow port. */
|
|
11
|
+
export class FileSchedulerStoreAdapter {
|
|
12
|
+
store;
|
|
13
|
+
constructor(store) {
|
|
14
|
+
this.store = store;
|
|
15
|
+
}
|
|
16
|
+
listTasks() { return this.store.listTasks(); }
|
|
17
|
+
getTask(taskId) { return this.store.getTask(taskId); }
|
|
18
|
+
getTaskBrief(taskId) { return this.store.getTaskBrief(taskId); }
|
|
19
|
+
listDecisions(taskId) { return this.store.listDecisions(taskId); }
|
|
20
|
+
listMilestones(taskId) { return this.store.listMilestones(taskId); }
|
|
21
|
+
listRoles(taskId) {
|
|
22
|
+
return this.store.listRoles(taskId).map(mapRole);
|
|
23
|
+
}
|
|
24
|
+
getRole(taskId, roleName) {
|
|
25
|
+
const role = this.store.getRole(taskId, roleName);
|
|
26
|
+
return role === null ? null : mapRole(role);
|
|
27
|
+
}
|
|
28
|
+
getActiveAgentRun(taskId, roleName) {
|
|
29
|
+
return this.store.getActiveAgentRun(taskId, roleName);
|
|
30
|
+
}
|
|
31
|
+
hasOpenInputRequest(taskId) {
|
|
32
|
+
return this.store.listInputRequests(taskId).some((request) => request.status === "open");
|
|
33
|
+
}
|
|
34
|
+
listOpenInputRequests() {
|
|
35
|
+
return this.store.listAllInputRequests().filter((request) => request.status === "open");
|
|
36
|
+
}
|
|
37
|
+
getOperatorDeliveryTarget() {
|
|
38
|
+
const role = this.store.getGlobalRole(SYSTEM_OPERATOR_ROLE);
|
|
39
|
+
if (role === null)
|
|
40
|
+
return null;
|
|
41
|
+
return {
|
|
42
|
+
roleName: SYSTEM_OPERATOR_ROLE,
|
|
43
|
+
adapterId: activeRoleAgentBinding(role).adapterId
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
resolveExpiredInputRecommendations(now) {
|
|
47
|
+
return this.store.transaction((store) => {
|
|
48
|
+
const expired = store.listAllInputRequests().filter((request) => (request.status === "open"
|
|
49
|
+
&& request.policy.kind === "recommended"
|
|
50
|
+
&& Date.parse(request.policy.timeoutAt) <= now.getTime()));
|
|
51
|
+
const resolved = [];
|
|
52
|
+
for (const request of expired) {
|
|
53
|
+
const task = store.getTask(request.taskId);
|
|
54
|
+
if (task?.status !== "active" || request.policy.kind !== "recommended")
|
|
55
|
+
continue;
|
|
56
|
+
const choiceKey = request.policy.recommendedChoiceKey;
|
|
57
|
+
const answered = answerInputRequest(request, { choiceKey }, "agent-timeout", now);
|
|
58
|
+
store.saveInputRequest(task.id, answered);
|
|
59
|
+
store.saveEvent(task.id, createTaskEvent(store.nextEventId(task.id), "input.auto-answered", { requestId: request.id, choiceKey }, now));
|
|
60
|
+
queueLeaderWakeup(store, task.id, `input-timeout:${request.id}`, now);
|
|
61
|
+
resolved.push({ inputRequestId: request.id, taskId: task.id, choiceKey });
|
|
62
|
+
}
|
|
63
|
+
return resolved;
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
getRoleSession(taskId, roleName) {
|
|
67
|
+
const session = this.store.getRoleSession(taskId, roleName);
|
|
68
|
+
return session === null ? null : mapSession(session);
|
|
69
|
+
}
|
|
70
|
+
nextAgentRunId(taskId) { return this.store.nextAgentRunId(taskId); }
|
|
71
|
+
getPendingWakeup(taskId) { return this.store.getPendingWakeup(taskId); }
|
|
72
|
+
listPendingWakeups() { return this.store.listPendingWakeups(); }
|
|
73
|
+
savePendingWakeup(wakeup) {
|
|
74
|
+
this.store.savePendingWakeup(wakeup);
|
|
75
|
+
}
|
|
76
|
+
clearPendingWakeup(taskId) { this.store.clearPendingWakeup(taskId); }
|
|
77
|
+
getLeaderFailure(taskId) { return this.store.getLeaderFailure(taskId); }
|
|
78
|
+
getOperatorNotification(taskId) { return this.store.getOperatorNotification(taskId); }
|
|
79
|
+
saveLeaderDispatch(input) {
|
|
80
|
+
return this.store.transaction((store) => {
|
|
81
|
+
const task = store.getTask(input.task.id);
|
|
82
|
+
if (task === null || task.status !== "active") {
|
|
83
|
+
return "unavailable";
|
|
84
|
+
}
|
|
85
|
+
const role = requireRole(store, input.task.id, input.role.name);
|
|
86
|
+
if (role.activeAgentId !== input.role.activeAgentId
|
|
87
|
+
|| activeRoleAgentBinding(role).adapterId !== input.role.adapterId) {
|
|
88
|
+
return "state-changed";
|
|
89
|
+
}
|
|
90
|
+
if (store.getActiveAgentRun(input.task.id, input.role.name) !== null)
|
|
91
|
+
return "busy";
|
|
92
|
+
const pending = store.getPendingWakeup(input.task.id);
|
|
93
|
+
if (pending === null || !pendingWakeupsMatch(pending, input.wakeup)) {
|
|
94
|
+
return "state-changed";
|
|
95
|
+
}
|
|
96
|
+
store.saveActiveAgentRun(input.run);
|
|
97
|
+
store.saveRole(input.task.id, updateRoleStatus(role, "running", input.now));
|
|
98
|
+
if (input.session !== null && input.session.nativeSessionId !== undefined) {
|
|
99
|
+
saveTaskSession(store, role, {
|
|
100
|
+
...input.session,
|
|
101
|
+
nativeSessionId: input.session.nativeSessionId
|
|
102
|
+
}, "running", input.now);
|
|
103
|
+
}
|
|
104
|
+
store.clearLeaderFailure(input.task.id);
|
|
105
|
+
store.clearOperatorNotification(input.task.id);
|
|
106
|
+
store.clearPendingWakeup(input.task.id);
|
|
107
|
+
return "claimed";
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
saveRoleRunDelivery(input) {
|
|
111
|
+
this.store.transaction((store) => {
|
|
112
|
+
const task = store.getTask(input.task.id);
|
|
113
|
+
if (task === null || task.status !== "active") {
|
|
114
|
+
throw new Error(`Task is not active: ${input.task.id}.`);
|
|
115
|
+
}
|
|
116
|
+
const role = requireRole(store, input.task.id, input.role.name);
|
|
117
|
+
const active = store.getActiveAgentRun(input.task.id, input.role.name);
|
|
118
|
+
if (active === null || active.id !== input.run.id) {
|
|
119
|
+
throw new Error(`Active Agent run changed before delivery was persisted: ${input.run.id}.`);
|
|
120
|
+
}
|
|
121
|
+
if (active.deliveredAt === undefined) {
|
|
122
|
+
store.saveAgentRun(markAgentRunDelivered(active, input.now));
|
|
123
|
+
}
|
|
124
|
+
if (role.status !== "running") {
|
|
125
|
+
store.saveRole(input.task.id, updateRoleStatus(role, "running", input.now));
|
|
126
|
+
}
|
|
127
|
+
if (input.session !== null && input.session.nativeSessionId !== undefined) {
|
|
128
|
+
const existing = store.getRoleSession(input.task.id, input.role.name);
|
|
129
|
+
if (existing?.agentId !== input.session.agentId
|
|
130
|
+
|| existing.adapterId !== input.session.adapterId
|
|
131
|
+
|| existing.nativeSessionId !== input.session.nativeSessionId
|
|
132
|
+
|| existing.status !== "running") {
|
|
133
|
+
saveTaskSession(store, role, {
|
|
134
|
+
...input.session,
|
|
135
|
+
nativeSessionId: input.session.nativeSessionId
|
|
136
|
+
}, "running", input.now);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
});
|
|
140
|
+
}
|
|
141
|
+
saveLeaderDispatchFailure(input) {
|
|
142
|
+
this.store.transaction((store) => {
|
|
143
|
+
const task = store.getTask(input.task.id);
|
|
144
|
+
if (task === null || task.status !== "active")
|
|
145
|
+
return;
|
|
146
|
+
const role = requireRole(store, input.task.id, input.role.name);
|
|
147
|
+
if (input.claimed !== undefined) {
|
|
148
|
+
const active = store.getActiveAgentRun(input.task.id, input.role.name);
|
|
149
|
+
if (active?.id === input.claimed.run.id) {
|
|
150
|
+
store.saveAgentRun(failAgentRun(active, input.failure.message, input.now));
|
|
151
|
+
store.clearActiveAgentRun(input.task.id, input.role.name);
|
|
152
|
+
}
|
|
153
|
+
store.savePendingWakeup(restorePendingWakeup(input.claimed.wakeup, store.getPendingWakeup(input.task.id)));
|
|
154
|
+
}
|
|
155
|
+
store.saveRole(input.task.id, updateRoleStatus(role, "failed", input.now));
|
|
156
|
+
breakTaskSessionIfPresent(store, input.task.id, role.name, role.activeAgentId, input.now);
|
|
157
|
+
store.saveLeaderFailure(input.failure);
|
|
158
|
+
store.saveOperatorNotification(input.notification);
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
saveExitedRoleRun(input) {
|
|
162
|
+
this.store.transaction((store) => {
|
|
163
|
+
const role = requireRole(store, input.task.id, input.role.name);
|
|
164
|
+
store.saveAgentRun(failAgentRun(input.run, input.summary, input.now));
|
|
165
|
+
store.clearActiveAgentRun(input.task.id, input.role.name);
|
|
166
|
+
if (input.run.workItemId !== undefined) {
|
|
167
|
+
const workItem = store.getWorkItem(input.task.id, input.run.workItemId);
|
|
168
|
+
if (workItem !== null && !["completed", "failed", "cancelled", "superseded"].includes(workItem.status)) {
|
|
169
|
+
store.saveWorkItem(input.task.id, updateWorkItemStatus(workItem, "failed", input.summary, input.now));
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
store.saveRole(input.task.id, updateRoleStatus(role, "exited", input.now));
|
|
173
|
+
stopTaskSessionIfPresent(store, input.task.id, role.name, role.activeAgentId, input.now);
|
|
174
|
+
});
|
|
175
|
+
}
|
|
176
|
+
saveArchivedTaskStopped(taskId, now) {
|
|
177
|
+
this.store.transaction((store) => {
|
|
178
|
+
const task = store.getTask(taskId);
|
|
179
|
+
if (task === null || task.status !== "archived")
|
|
180
|
+
return;
|
|
181
|
+
for (const role of store.listRoles(taskId)) {
|
|
182
|
+
if (role.status !== "idle") {
|
|
183
|
+
store.saveRole(taskId, updateRoleStatus(role, "idle", now));
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
for (const current of store.listRoleSessionSets(taskId)) {
|
|
187
|
+
if (Object.values(current.sessions).every((session) => session.status === "stopped")) {
|
|
188
|
+
continue;
|
|
189
|
+
}
|
|
190
|
+
let updated = current;
|
|
191
|
+
for (const agentId of Object.keys(current.sessions)) {
|
|
192
|
+
updated = updateRoleAgentSessionStatus(updated, agentId, "stopped", now);
|
|
193
|
+
}
|
|
194
|
+
store.saveRoleSessionSet(updated);
|
|
195
|
+
}
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
/** Called by the internal Codex notify hook, never by an LLM prompt. */
|
|
199
|
+
recordRuntimeNativeSession(input, now = new Date()) {
|
|
200
|
+
return this.store.transaction((store) => {
|
|
201
|
+
const task = store.getTask(input.taskId);
|
|
202
|
+
if (task === null)
|
|
203
|
+
throw new Error(`Task not found: ${input.taskId}.`);
|
|
204
|
+
if (task.status === "archived") {
|
|
205
|
+
throw new Error(`Cannot register a native session for archived Task: ${input.taskId}.`);
|
|
206
|
+
}
|
|
207
|
+
if (task.status !== "active") {
|
|
208
|
+
throw new Error(`Cannot register a native session for a Task that is not active: ${input.taskId}.`);
|
|
209
|
+
}
|
|
210
|
+
const role = requireRole(store, input.taskId, input.roleName);
|
|
211
|
+
const binding = activeRoleAgentBinding(role);
|
|
212
|
+
if (binding.agentId !== input.agentId || binding.adapterId !== input.adapterId) {
|
|
213
|
+
throw new Error("Native session registration does not match the active Role Agent binding.");
|
|
214
|
+
}
|
|
215
|
+
const current = store.getRoleSessionSet(input.taskId, input.roleName)
|
|
216
|
+
?? createRoleSessionSet({ scope: "task", taskId: input.taskId, roleName: input.roleName }, role.activeAgentId, now);
|
|
217
|
+
const existing = current.sessions[input.agentId];
|
|
218
|
+
if (existing !== undefined && existing.nativeSessionId !== input.nativeSessionId) {
|
|
219
|
+
throw new Error("Native session registration conflicts with the fixed Role session.");
|
|
220
|
+
}
|
|
221
|
+
if (existing?.status === "running")
|
|
222
|
+
return existing;
|
|
223
|
+
const updated = recordRoleAgentSession(current, {
|
|
224
|
+
agentId: input.agentId,
|
|
225
|
+
adapterId: input.adapterId,
|
|
226
|
+
nativeSessionId: input.nativeSessionId,
|
|
227
|
+
policy: "fixed",
|
|
228
|
+
status: "running"
|
|
229
|
+
}, now);
|
|
230
|
+
store.saveRoleSessionSet(updated);
|
|
231
|
+
return updated.sessions[input.agentId];
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
recordGlobalRuntimeNativeSession(input, now = new Date()) {
|
|
235
|
+
return this.store.transaction((store) => {
|
|
236
|
+
const role = store.getGlobalRole(input.roleName);
|
|
237
|
+
if (role === null)
|
|
238
|
+
throw new Error(`Global Role not found: ${input.roleName}.`);
|
|
239
|
+
const binding = activeRoleAgentBinding(role);
|
|
240
|
+
if (binding.agentId !== input.agentId || binding.adapterId !== input.adapterId) {
|
|
241
|
+
throw new Error("Native session registration does not match the active global Role Agent binding.");
|
|
242
|
+
}
|
|
243
|
+
const current = store.getGlobalRoleSessionSet(input.roleName)
|
|
244
|
+
?? createRoleSessionSet({ scope: "global", roleName: input.roleName }, role.activeAgentId, now);
|
|
245
|
+
const existing = current.sessions[input.agentId];
|
|
246
|
+
if (existing !== undefined && existing.nativeSessionId !== input.nativeSessionId) {
|
|
247
|
+
throw new Error("Native session registration conflicts with the fixed global Role session.");
|
|
248
|
+
}
|
|
249
|
+
if (existing?.status === "running")
|
|
250
|
+
return existing;
|
|
251
|
+
const updated = recordRoleAgentSession(current, {
|
|
252
|
+
agentId: input.agentId,
|
|
253
|
+
adapterId: input.adapterId,
|
|
254
|
+
nativeSessionId: input.nativeSessionId,
|
|
255
|
+
policy: "fixed",
|
|
256
|
+
status: "running"
|
|
257
|
+
}, now);
|
|
258
|
+
store.saveGlobalRoleSessionSet(updated);
|
|
259
|
+
return updated.sessions[input.agentId];
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
function restorePendingWakeup(claimed, current) {
|
|
264
|
+
if (current === null)
|
|
265
|
+
return claimed;
|
|
266
|
+
return {
|
|
267
|
+
schemaVersion: 1,
|
|
268
|
+
taskId: claimed.taskId,
|
|
269
|
+
reasons: [...new Set([...claimed.reasons, ...current.reasons])],
|
|
270
|
+
requestCount: claimed.requestCount + current.requestCount,
|
|
271
|
+
firstRequestedAt: claimed.firstRequestedAt,
|
|
272
|
+
lastRequestedAt: current.lastRequestedAt
|
|
273
|
+
};
|
|
274
|
+
}
|
|
275
|
+
function mapRole(role) {
|
|
276
|
+
const binding = activeRoleAgentBinding(role);
|
|
277
|
+
return {
|
|
278
|
+
taskId: role.taskId,
|
|
279
|
+
name: role.name,
|
|
280
|
+
activeAgentId: role.activeAgentId,
|
|
281
|
+
adapterId: binding.adapterId,
|
|
282
|
+
status: role.status
|
|
283
|
+
};
|
|
284
|
+
}
|
|
285
|
+
function mapSession(session) {
|
|
286
|
+
return {
|
|
287
|
+
agentId: session.agentId,
|
|
288
|
+
adapterId: session.adapterId,
|
|
289
|
+
nativeSessionId: session.nativeSessionId,
|
|
290
|
+
status: session.status
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
function saveTaskSession(store, role, session, status, now) {
|
|
294
|
+
const current = store.getRoleSessionSet(role.taskId, role.name)
|
|
295
|
+
?? createRoleSessionSet({ scope: "task", taskId: role.taskId, roleName: role.name }, role.activeAgentId, now);
|
|
296
|
+
const updated = recordRoleAgentSession(current, {
|
|
297
|
+
agentId: session.agentId,
|
|
298
|
+
adapterId: session.adapterId,
|
|
299
|
+
nativeSessionId: session.nativeSessionId,
|
|
300
|
+
policy: "fixed",
|
|
301
|
+
status
|
|
302
|
+
}, now);
|
|
303
|
+
store.saveRoleSessionSet(updated);
|
|
304
|
+
}
|
|
305
|
+
function breakTaskSessionIfPresent(store, taskId, roleName, agentId, now) {
|
|
306
|
+
updateTaskSessionStatusIfPresent(store, taskId, roleName, agentId, "broken", now);
|
|
307
|
+
}
|
|
308
|
+
function stopTaskSessionIfPresent(store, taskId, roleName, agentId, now) {
|
|
309
|
+
updateTaskSessionStatusIfPresent(store, taskId, roleName, agentId, "stopped", now);
|
|
310
|
+
}
|
|
311
|
+
function updateTaskSessionStatusIfPresent(store, taskId, roleName, agentId, status, now) {
|
|
312
|
+
const set = store.getRoleSessionSet(taskId, roleName);
|
|
313
|
+
if (set === null || set.sessions[agentId] === undefined)
|
|
314
|
+
return;
|
|
315
|
+
store.saveRoleSessionSet(updateRoleAgentSessionStatus(set, agentId, status, now));
|
|
316
|
+
}
|
|
317
|
+
function requireRole(store, taskId, roleName) {
|
|
318
|
+
const role = store.getRole(taskId, roleName);
|
|
319
|
+
if (role === null)
|
|
320
|
+
throw new Error(`Role not found: ${taskId}/${roleName}.`);
|
|
321
|
+
return role;
|
|
322
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { reconciliationIntervalMilliseconds } from "../config/yuiConfig.js";
|
|
2
|
+
import { ExecutorRegistry } from "../executor/executorRegistry.js";
|
|
3
|
+
import { FileRoleLaunchPlanner } from "../executor/fileRoleLaunchPlanner.js";
|
|
4
|
+
import { FileTaskStore } from "../storage/taskStore.js";
|
|
5
|
+
import { FileTaskWorkspacePreparer } from "../repository/taskWorkspacePreparer.js";
|
|
6
|
+
import { NodeCommandExecutor } from "../tmux/commandExecutor.js";
|
|
7
|
+
import { TmuxManager } from "../tmux/tmuxManager.js";
|
|
8
|
+
import { startFileTaskController } from "./controller.js";
|
|
9
|
+
import { FileSchedulerStoreAdapter } from "./fileSchedulerStoreAdapter.js";
|
|
10
|
+
import { createSessionNotifyDispatcher } from "./sessionNotify.js";
|
|
11
|
+
/** Production composition root for the lean FileTaskStore + tmux Controller. */
|
|
12
|
+
export async function startFileTaskControllerRuntime(home, options = {}) {
|
|
13
|
+
const store = options.store ?? new FileTaskStore(home);
|
|
14
|
+
const schedulerStore = options.schedulerStore ?? new FileSchedulerStoreAdapter(store);
|
|
15
|
+
const planner = options.planner ?? new FileRoleLaunchPlanner(home, store, {
|
|
16
|
+
environment: options.environment
|
|
17
|
+
});
|
|
18
|
+
const tmux = options.tmux ?? new TmuxManager(options.environment?.YUI_TMUX_BIN ?? process.env.YUI_TMUX_BIN ?? "tmux", new NodeCommandExecutor(), { yuiHome: home });
|
|
19
|
+
const delivery = options.delivery ?? new ExecutorRegistry(planner, tmux);
|
|
20
|
+
const workspacePreparer = options.workspacePreparer
|
|
21
|
+
?? new FileTaskWorkspacePreparer(home, store);
|
|
22
|
+
const dispatcher = createSessionNotifyDispatcher(schedulerStore, options.dispatcher);
|
|
23
|
+
const running = await startFileTaskController(home, schedulerStore, delivery, dispatcher, {
|
|
24
|
+
intervalMs: options.intervalMs
|
|
25
|
+
?? reconciliationIntervalMilliseconds(store.getConfig().reconciliationIntervalSeconds),
|
|
26
|
+
now: options.now,
|
|
27
|
+
onError: options.onError,
|
|
28
|
+
workspacePreparer
|
|
29
|
+
});
|
|
30
|
+
return { ...running, store, schedulerStore, planner, tmux, delivery, workspacePreparer };
|
|
31
|
+
}
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
import { callFileTaskController } from "./clientRuntime.js";
|
|
2
|
+
const SESSION_BIND_METHOD = "runtime.session.bind";
|
|
3
|
+
/** Hidden CLI entrypoint used by Codex's structured legacy notify hook. */
|
|
4
|
+
export async function runSessionNotifyCommand(payloadArgument, environment = process.env, call = callFileTaskController) {
|
|
5
|
+
const params = parseCodexSessionNotification(payloadArgument, environment);
|
|
6
|
+
await call(requireText(environment.YUI_HOME, "YUI_HOME"), SESSION_BIND_METHOD, toJson(params));
|
|
7
|
+
}
|
|
8
|
+
export function parseCodexSessionNotification(payloadArgument, environment) {
|
|
9
|
+
const payload = parseObject(payloadArgument, "Codex notify payload");
|
|
10
|
+
if (payload.type !== "agent-turn-complete") {
|
|
11
|
+
throw new Error("Codex notify payload type is invalid.");
|
|
12
|
+
}
|
|
13
|
+
const nativeSessionId = requireText(payload["thread-id"], "Codex thread-id");
|
|
14
|
+
const scope = environment.YUI_SESSION_SCOPE;
|
|
15
|
+
if (scope !== "task" && scope !== "global") {
|
|
16
|
+
throw new Error("YUI_SESSION_SCOPE must be task or global.");
|
|
17
|
+
}
|
|
18
|
+
const common = {
|
|
19
|
+
scope,
|
|
20
|
+
roleName: requireText(environment.YUI_ROLE, "YUI_ROLE"),
|
|
21
|
+
agentId: requireText(environment.YUI_AGENT_ID, "YUI_AGENT_ID"),
|
|
22
|
+
adapterId: requireCodexAdapter(environment.YUI_ADAPTER_ID),
|
|
23
|
+
nativeSessionId
|
|
24
|
+
};
|
|
25
|
+
return scope === "task"
|
|
26
|
+
? {
|
|
27
|
+
...common,
|
|
28
|
+
taskId: requireText(environment.YUI_TASK_ID, "YUI_TASK_ID")
|
|
29
|
+
}
|
|
30
|
+
: common;
|
|
31
|
+
}
|
|
32
|
+
/** Controller-side handler; session identity never travels through an Agent prompt. */
|
|
33
|
+
export function createSessionNotifyDispatcher(store, fallback) {
|
|
34
|
+
return async (method, params) => {
|
|
35
|
+
if (method !== SESSION_BIND_METHOD) {
|
|
36
|
+
if (fallback !== undefined)
|
|
37
|
+
return fallback(method, params);
|
|
38
|
+
throw applicationError("METHOD_NOT_FOUND", "Controller method was not found.");
|
|
39
|
+
}
|
|
40
|
+
const input = parseSessionBindParams(params);
|
|
41
|
+
const recorded = input.scope === "task"
|
|
42
|
+
? store.recordRuntimeNativeSession({
|
|
43
|
+
taskId: input.taskId,
|
|
44
|
+
roleName: input.roleName,
|
|
45
|
+
agentId: input.agentId,
|
|
46
|
+
adapterId: input.adapterId,
|
|
47
|
+
nativeSessionId: input.nativeSessionId
|
|
48
|
+
})
|
|
49
|
+
: store.recordGlobalRuntimeNativeSession({
|
|
50
|
+
roleName: input.roleName,
|
|
51
|
+
agentId: input.agentId,
|
|
52
|
+
adapterId: input.adapterId,
|
|
53
|
+
nativeSessionId: input.nativeSessionId
|
|
54
|
+
});
|
|
55
|
+
return {
|
|
56
|
+
recorded: true,
|
|
57
|
+
scope: input.scope,
|
|
58
|
+
roleName: input.roleName,
|
|
59
|
+
nativeSessionId: recorded.nativeSessionId
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
function parseSessionBindParams(value) {
|
|
64
|
+
if (!isObject(value))
|
|
65
|
+
throw invalidParams();
|
|
66
|
+
const input = value;
|
|
67
|
+
const scope = input.scope;
|
|
68
|
+
const expected = scope === "task"
|
|
69
|
+
? ["scope", "taskId", "roleName", "agentId", "adapterId", "nativeSessionId"]
|
|
70
|
+
: ["scope", "roleName", "agentId", "adapterId", "nativeSessionId"];
|
|
71
|
+
if ((scope !== "task" && scope !== "global") || !hasExactKeys(input, expected)) {
|
|
72
|
+
throw invalidParams();
|
|
73
|
+
}
|
|
74
|
+
try {
|
|
75
|
+
const common = {
|
|
76
|
+
scope,
|
|
77
|
+
roleName: requireText(input.roleName, "Role name"),
|
|
78
|
+
agentId: requireText(input.agentId, "Agent id"),
|
|
79
|
+
adapterId: requireCodexAdapter(input.adapterId),
|
|
80
|
+
nativeSessionId: requireText(input.nativeSessionId, "Native session id")
|
|
81
|
+
};
|
|
82
|
+
return scope === "task"
|
|
83
|
+
? { ...common, taskId: requireText(input.taskId, "Task id") }
|
|
84
|
+
: common;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
throw invalidParams();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
function parseObject(value, label) {
|
|
91
|
+
if (value === undefined || Buffer.byteLength(value) > 1_048_576) {
|
|
92
|
+
throw new Error(`${label} is invalid.`);
|
|
93
|
+
}
|
|
94
|
+
try {
|
|
95
|
+
const parsed = JSON.parse(value);
|
|
96
|
+
if (!isObject(parsed))
|
|
97
|
+
throw new Error("shape");
|
|
98
|
+
return parsed;
|
|
99
|
+
}
|
|
100
|
+
catch {
|
|
101
|
+
throw new Error(`${label} is invalid.`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
function requireCodexAdapter(value) {
|
|
105
|
+
if (value !== "codex")
|
|
106
|
+
throw new Error("Runtime session notify requires the Codex adapter.");
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
function requireText(value, label) {
|
|
110
|
+
if (typeof value !== "string" || value.includes("\0")) {
|
|
111
|
+
throw new Error(`${label} is required.`);
|
|
112
|
+
}
|
|
113
|
+
const text = value.trim();
|
|
114
|
+
if (text.length === 0 || text.length > 1_024)
|
|
115
|
+
throw new Error(`${label} is invalid.`);
|
|
116
|
+
return text;
|
|
117
|
+
}
|
|
118
|
+
function isObject(value) {
|
|
119
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
120
|
+
}
|
|
121
|
+
function hasExactKeys(value, expected) {
|
|
122
|
+
const actual = Object.keys(value).sort();
|
|
123
|
+
return actual.length === expected.length
|
|
124
|
+
&& actual.every((key, index) => key === [...expected].sort()[index]);
|
|
125
|
+
}
|
|
126
|
+
function invalidParams() {
|
|
127
|
+
return applicationError("INVALID_PARAMS", "Runtime session params are invalid.");
|
|
128
|
+
}
|
|
129
|
+
function applicationError(code, message) {
|
|
130
|
+
const error = Object.assign(new Error(message), { code });
|
|
131
|
+
error.name = "CoreApplicationError";
|
|
132
|
+
return error;
|
|
133
|
+
}
|
|
134
|
+
function toJson(value) {
|
|
135
|
+
return JSON.parse(JSON.stringify(value));
|
|
136
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import { lstat, readFile } from "node:fs/promises";
|
|
3
|
+
import { createConnection } from "node:net";
|
|
4
|
+
import { join } from "node:path";
|
|
5
|
+
import { CONTROLLER_DISCOVERY_PATH, CONTROLLER_SOCKET_PATH, MAX_CONTROLLER_MESSAGE_BYTES, ControllerProtocolError, encodeControllerRequest, parseControllerDiscovery, parseControllerResponse } from "./protocol.js";
|
|
6
|
+
export class ControllerClientError extends Error {
|
|
7
|
+
code;
|
|
8
|
+
constructor(code, message) {
|
|
9
|
+
super(message);
|
|
10
|
+
this.code = code;
|
|
11
|
+
this.name = "ControllerClientError";
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export async function readControllerDiscovery(home) {
|
|
15
|
+
const discoveryPath = join(home, CONTROLLER_DISCOVERY_PATH);
|
|
16
|
+
const expectedSocketPath = join(home, CONTROLLER_SOCKET_PATH);
|
|
17
|
+
try {
|
|
18
|
+
const metadata = await lstat(discoveryPath);
|
|
19
|
+
if (!metadata.isFile()
|
|
20
|
+
|| (metadata.mode & 0o077) !== 0
|
|
21
|
+
|| metadata.size > 4_096) {
|
|
22
|
+
throw invalidDiscovery();
|
|
23
|
+
}
|
|
24
|
+
const value = JSON.parse(await readFile(discoveryPath, "utf8"));
|
|
25
|
+
return parseControllerDiscovery(value, expectedSocketPath);
|
|
26
|
+
}
|
|
27
|
+
catch (error) {
|
|
28
|
+
if (error instanceof ControllerClientError)
|
|
29
|
+
throw error;
|
|
30
|
+
if (isNodeError(error) && error.code === "ENOENT") {
|
|
31
|
+
throw new ControllerClientError("CONTROLLER_UNAVAILABLE", "Controller is not running.");
|
|
32
|
+
}
|
|
33
|
+
throw invalidDiscovery();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
export async function callController(home, method, params = {}, options = {}) {
|
|
37
|
+
const discovery = await readControllerDiscovery(home);
|
|
38
|
+
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
39
|
+
if (!Number.isSafeInteger(timeoutMs) || timeoutMs < 1) {
|
|
40
|
+
throw new TypeError("Controller timeout must be a positive integer.");
|
|
41
|
+
}
|
|
42
|
+
const id = options.id ?? randomUUID();
|
|
43
|
+
let requestLine;
|
|
44
|
+
try {
|
|
45
|
+
requestLine = encodeControllerRequest({
|
|
46
|
+
id,
|
|
47
|
+
token: discovery.token,
|
|
48
|
+
method,
|
|
49
|
+
params
|
|
50
|
+
});
|
|
51
|
+
}
|
|
52
|
+
catch (error) {
|
|
53
|
+
if (error instanceof ControllerProtocolError) {
|
|
54
|
+
throw new ControllerClientError(error.code, error.message);
|
|
55
|
+
}
|
|
56
|
+
throw new ControllerClientError("INVALID_REQUEST", "Invalid controller request.");
|
|
57
|
+
}
|
|
58
|
+
return exchange(discovery.socketPath, requestLine, id, timeoutMs);
|
|
59
|
+
}
|
|
60
|
+
function exchange(socketPath, requestLine, expectedId, timeoutMs) {
|
|
61
|
+
return new Promise((resolve, reject) => {
|
|
62
|
+
const socket = createConnection(socketPath);
|
|
63
|
+
let buffer = Buffer.alloc(0);
|
|
64
|
+
let settled = false;
|
|
65
|
+
const timer = setTimeout(() => {
|
|
66
|
+
fail(new ControllerClientError("CONTROLLER_TIMEOUT", "Controller request timed out."));
|
|
67
|
+
}, timeoutMs);
|
|
68
|
+
const finish = (result) => {
|
|
69
|
+
if (settled)
|
|
70
|
+
return;
|
|
71
|
+
settled = true;
|
|
72
|
+
clearTimeout(timer);
|
|
73
|
+
socket.destroy();
|
|
74
|
+
resolve(result);
|
|
75
|
+
};
|
|
76
|
+
const fail = (error) => {
|
|
77
|
+
if (settled)
|
|
78
|
+
return;
|
|
79
|
+
settled = true;
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
socket.destroy();
|
|
82
|
+
reject(error);
|
|
83
|
+
};
|
|
84
|
+
socket.on("connect", () => socket.write(requestLine));
|
|
85
|
+
socket.on("data", (chunk) => {
|
|
86
|
+
if (settled)
|
|
87
|
+
return;
|
|
88
|
+
buffer = Buffer.concat([buffer, chunk]);
|
|
89
|
+
const newline = buffer.indexOf(0x0a);
|
|
90
|
+
if (newline < 0) {
|
|
91
|
+
if (buffer.length > MAX_CONTROLLER_MESSAGE_BYTES)
|
|
92
|
+
fail(invalidResponse());
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
95
|
+
if (newline > MAX_CONTROLLER_MESSAGE_BYTES || buffer.length !== newline + 1) {
|
|
96
|
+
fail(invalidResponse());
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
try {
|
|
100
|
+
const response = parseControllerResponse(buffer.subarray(0, newline).toString("utf8"), expectedId);
|
|
101
|
+
if (response.ok)
|
|
102
|
+
finish(response.result);
|
|
103
|
+
else
|
|
104
|
+
fail(new ControllerClientError(response.error.code, response.error.message));
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
fail(invalidResponse());
|
|
108
|
+
}
|
|
109
|
+
});
|
|
110
|
+
socket.on("end", () => {
|
|
111
|
+
if (!settled)
|
|
112
|
+
fail(invalidResponse());
|
|
113
|
+
});
|
|
114
|
+
socket.on("error", () => {
|
|
115
|
+
fail(new ControllerClientError("CONTROLLER_UNAVAILABLE", "Controller is unavailable."));
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
function invalidDiscovery() {
|
|
120
|
+
return new ControllerClientError("CONTROLLER_DISCOVERY_INVALID", "Controller discovery is invalid.");
|
|
121
|
+
}
|
|
122
|
+
function invalidResponse() {
|
|
123
|
+
return new ControllerClientError("INVALID_RESPONSE", "Controller response is invalid.");
|
|
124
|
+
}
|
|
125
|
+
function isNodeError(error) {
|
|
126
|
+
return error instanceof Error && "code" in error;
|
|
127
|
+
}
|