@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.
Files changed (95) hide show
  1. package/ARCHITECTURE.md +141 -0
  2. package/LICENSE +21 -0
  3. package/README.md +211 -0
  4. package/dist/agent/adapterCatalog.js +10 -0
  5. package/dist/agent/agent.js +89 -0
  6. package/dist/agent/agentRegistry.js +10 -0
  7. package/dist/agent/argumentPolicy.js +80 -0
  8. package/dist/brief/taskBrief.js +37 -0
  9. package/dist/cli/commandCatalog.js +647 -0
  10. package/dist/cli/completion.js +111 -0
  11. package/dist/cli/completionWizard.js +143 -0
  12. package/dist/cli/dynamicCompletion.js +48 -0
  13. package/dist/cli/helpRenderer.js +32 -0
  14. package/dist/cli/interactionCandidates.js +139 -0
  15. package/dist/cli/interactionPolicy.js +389 -0
  16. package/dist/cli/interactiveSelection.js +185 -0
  17. package/dist/cli/invocationRouter.js +51 -0
  18. package/dist/cli/roleOptionCatalog.js +67 -0
  19. package/dist/cli/roleWizard.js +546 -0
  20. package/dist/cli/selectionPorts.js +1 -0
  21. package/dist/cli/updateCommand.js +22 -0
  22. package/dist/cli.js +402 -0
  23. package/dist/commands/agentCommands.js +196 -0
  24. package/dist/commands/globalRoleCommands.js +367 -0
  25. package/dist/commands/jobCommands.js +100 -0
  26. package/dist/commands/operatorCommands.js +38 -0
  27. package/dist/commands/repositoryCommands.js +86 -0
  28. package/dist/commands/roleConfiguration.js +201 -0
  29. package/dist/commands/taskCommands.js +1344 -0
  30. package/dist/commands/taskContextCommand.js +215 -0
  31. package/dist/commands/taskInputCommands.js +423 -0
  32. package/dist/commands/taskRoleRuntimeStatus.js +152 -0
  33. package/dist/completion/completionInstaller.js +168 -0
  34. package/dist/completion/completionPort.js +1 -0
  35. package/dist/completion/completionState.js +137 -0
  36. package/dist/completion/completionWizard.js +125 -0
  37. package/dist/completion/fileCompletionManager.js +51 -0
  38. package/dist/config/yuiConfig.js +17 -0
  39. package/dist/context/dispatchContext.js +74 -0
  40. package/dist/controller/clientRuntime.js +215 -0
  41. package/dist/controller/controller.js +158 -0
  42. package/dist/controller/controllerMain.js +37 -0
  43. package/dist/controller/fileSchedulerStoreAdapter.js +322 -0
  44. package/dist/controller/runtime.js +31 -0
  45. package/dist/controller/sessionNotify.js +136 -0
  46. package/dist/core/controllerClient.js +127 -0
  47. package/dist/core/controllerServer.js +269 -0
  48. package/dist/core/protocol.js +169 -0
  49. package/dist/decision/decision.js +42 -0
  50. package/dist/doctor/doctor.js +229 -0
  51. package/dist/errors/cliError.js +38 -0
  52. package/dist/event/taskEvent.js +44 -0
  53. package/dist/executor/agentAdapter.js +338 -0
  54. package/dist/executor/agentExecutor.js +144 -0
  55. package/dist/executor/executorRegistry.js +101 -0
  56. package/dist/executor/fileRoleLaunchPlanner.js +156 -0
  57. package/dist/executor/launchPlan.js +16 -0
  58. package/dist/input/inputRequest.js +326 -0
  59. package/dist/message/message.js +69 -0
  60. package/dist/milestone/milestone.js +27 -0
  61. package/dist/operator/operatorContext.js +66 -0
  62. package/dist/output/rolePresentation.js +82 -0
  63. package/dist/output/table.js +77 -0
  64. package/dist/output/terminal.js +198 -0
  65. package/dist/repository/gitWorkspace.js +210 -0
  66. package/dist/repository/repository.js +55 -0
  67. package/dist/repository/taskWorkspacePreparer.js +256 -0
  68. package/dist/role/role.js +246 -0
  69. package/dist/role/systemRoles.js +20 -0
  70. package/dist/run/agentRun.js +102 -0
  71. package/dist/scheduler/activeRoleRunDelivery.js +94 -0
  72. package/dist/scheduler/archivedTaskRuntime.js +12 -0
  73. package/dist/scheduler/leaderFailure.js +18 -0
  74. package/dist/scheduler/leaderWakeupProcessor.js +143 -0
  75. package/dist/scheduler/operatorInputNotificationProcessor.js +85 -0
  76. package/dist/scheduler/operatorNotification.js +17 -0
  77. package/dist/scheduler/pendingWakeup.js +33 -0
  78. package/dist/scheduler/ports.js +1 -0
  79. package/dist/scheduler/roleRunLiveness.js +41 -0
  80. package/dist/scheduler/wakeupQueue.js +13 -0
  81. package/dist/setup/setupCommand.js +317 -0
  82. package/dist/storage/durableFile.js +38 -0
  83. package/dist/storage/storageSchema.js +259 -0
  84. package/dist/storage/taskStore.js +1032 -0
  85. package/dist/task/task.js +216 -0
  86. package/dist/tmux/commandExecutor.js +69 -0
  87. package/dist/tmux/terminalHandoff.js +17 -0
  88. package/dist/tmux/tmuxManager.js +408 -0
  89. package/dist/workItem/workItem.js +45 -0
  90. package/dist/worktree/roleWorkspace.js +62 -0
  91. package/i18n/README.zh-CN.md +205 -0
  92. package/package.json +47 -0
  93. package/skills/yui-leader/SKILL.md +72 -0
  94. package/skills/yui-operator/SKILL.md +57 -0
  95. 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
+ }