@zq-silk/yui 0.2.0 → 0.4.2

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 (208) hide show
  1. package/ARCHITECTURE.md +603 -133
  2. package/README.md +806 -31
  3. package/dist/agent/agent.js +2 -1
  4. package/dist/agent/argumentPolicy.js +3 -1
  5. package/dist/agent/launchEnvironment.js +106 -0
  6. package/dist/agent/managedRuntimeEnvironment.js +34 -0
  7. package/dist/brief/taskBrief.js +11 -1
  8. package/dist/cli/agentConfigurationPicker.js +287 -0
  9. package/dist/cli/commandCatalog.js +488 -60
  10. package/dist/cli/completion.js +146 -22
  11. package/dist/cli/helpRenderer.js +3 -1
  12. package/dist/cli/interactionCandidates.js +53 -15
  13. package/dist/cli/interactionPolicy.js +267 -30
  14. package/dist/cli/interactiveSelection.js +6 -2
  15. package/dist/cli/invocationRouter.js +5 -1
  16. package/dist/cli/operatorWizard.js +87 -0
  17. package/dist/cli/roleOptionCatalog.js +1 -0
  18. package/dist/cli/roleWizard.js +185 -21
  19. package/dist/cli/updateCommand.js +62 -19
  20. package/dist/cli/updateOrchestrator.js +539 -0
  21. package/dist/cli/updatePorts.js +1119 -0
  22. package/dist/cli/upgradeCommand.js +112 -0
  23. package/dist/cli.js +1420 -86
  24. package/dist/commands/agentCommands.js +146 -3
  25. package/dist/commands/configCommands.js +126 -0
  26. package/dist/commands/controllerCommands.js +365 -0
  27. package/dist/commands/globalRoleCommands.js +168 -126
  28. package/dist/commands/jobCommands.js +18 -8
  29. package/dist/commands/operatorCommands.js +159 -9
  30. package/dist/commands/profileCommands.js +203 -0
  31. package/dist/commands/projectCommands.js +650 -0
  32. package/dist/commands/roleConfiguration.js +85 -24
  33. package/dist/commands/roleRuntimeGuard.js +12 -0
  34. package/dist/commands/roleSkillValidation.js +47 -0
  35. package/dist/commands/taskActor.js +127 -0
  36. package/dist/commands/taskCommands.js +4201 -313
  37. package/dist/commands/taskCompletionGate.js +131 -0
  38. package/dist/commands/taskContextCommand.js +244 -30
  39. package/dist/commands/taskInputCommands.js +177 -59
  40. package/dist/commands/taskIntegrationCommands.js +303 -0
  41. package/dist/commands/taskOverviewCommand.js +363 -0
  42. package/dist/commands/taskRoleRuntimeStatus.js +125 -19
  43. package/dist/commands/textInput.js +15 -0
  44. package/dist/completion/completionInstaller.js +26 -22
  45. package/dist/config/yuiConfig.js +4 -3
  46. package/dist/context/dispatchContext.js +90 -38
  47. package/dist/context/roleSessionContext.js +119 -0
  48. package/dist/controller/claudeLifecycleHook.js +203 -0
  49. package/dist/controller/clientRuntime.js +408 -56
  50. package/dist/controller/codexLifecycleHook.js +108 -0
  51. package/dist/controller/controller.js +1089 -32
  52. package/dist/controller/domainIdentity.js +505 -0
  53. package/dist/controller/ephemeralResourceReaper.js +131 -0
  54. package/dist/controller/fileSchedulerStoreAdapter.js +2153 -103
  55. package/dist/controller/providerHookRunFence.js +127 -0
  56. package/dist/controller/resourceCleanupLinux.js +286 -0
  57. package/dist/controller/resourceInventory.js +531 -0
  58. package/dist/controller/resourceInventoryLinux.js +610 -0
  59. package/dist/controller/runtime.js +629 -10
  60. package/dist/controller/runtimeEventInbox.js +564 -0
  61. package/dist/controller/runtimeEventProcessor.js +248 -0
  62. package/dist/controller/runtimeLaunchCoordinator.js +477 -0
  63. package/dist/controller/sessionNotify.js +121 -78
  64. package/dist/coordination/deadlineScheduler.js +15 -0
  65. package/dist/coordination/mailboxScheduler.js +108 -0
  66. package/dist/coordination/workMailbox.js +329 -0
  67. package/dist/coordination/workMailboxQueue.js +86 -0
  68. package/dist/core/controllerClient.js +19 -5
  69. package/dist/core/controllerEndpoint.js +37 -0
  70. package/dist/core/controllerServer.js +218 -10
  71. package/dist/core/protocol.js +6 -2
  72. package/dist/decision/decision.js +2 -1
  73. package/dist/doctor/doctor.js +681 -32
  74. package/dist/domain/validation.js +53 -0
  75. package/dist/errors/cliError.js +5 -3
  76. package/dist/event/taskEvent.js +7 -3
  77. package/dist/execution/codexThreadNaming.js +160 -0
  78. package/dist/execution/executionGroup.js +579 -0
  79. package/dist/executor/agentAdapter.js +255 -40
  80. package/dist/executor/agentConfigurationCatalog.js +326 -0
  81. package/dist/executor/agentConfigurationProbe.js +506 -0
  82. package/dist/executor/agentExecutor.js +625 -10
  83. package/dist/executor/codexConfigConflict.js +290 -0
  84. package/dist/executor/effectiveLaunch.js +340 -0
  85. package/dist/executor/executorRegistry.js +238 -36
  86. package/dist/executor/fileRoleLaunchPlanner.js +550 -40
  87. package/dist/executor/turnCompletion.js +126 -0
  88. package/dist/input/inputRequest.js +30 -9
  89. package/dist/integration/changeSet.js +36 -0
  90. package/dist/integration/checkResult.js +24 -0
  91. package/dist/integration/gitIntegrationService.js +695 -0
  92. package/dist/integration/integrationAttempt.js +142 -0
  93. package/dist/interaction/operatorPresentation.js +96 -0
  94. package/dist/lifecycle/canonicalLifecycleEvent.js +342 -0
  95. package/dist/lifecycle/exactRunTerminalization.js +572 -0
  96. package/dist/lifecycle/providerLifecycleMapping.js +190 -0
  97. package/dist/lifecycle/taskRoleSessionReset.js +124 -0
  98. package/dist/message/message.js +23 -7
  99. package/dist/milestone/milestone.js +2 -1
  100. package/dist/operator/operatorSessionHistory.js +124 -0
  101. package/dist/output/agentConfigurationPresentation.js +43 -0
  102. package/dist/output/rolePresentation.js +34 -10
  103. package/dist/output/terminal.js +8 -0
  104. package/dist/output/timePresentation.js +55 -0
  105. package/dist/profile/agentProfile.js +128 -0
  106. package/dist/repository/gitWorkspace.js +578 -24
  107. package/dist/repository/project.js +213 -0
  108. package/dist/repository/taskWorkspaceCoordinator.js +392 -0
  109. package/dist/repository/taskWorkspacePreparer.js +1688 -191
  110. package/dist/review/reviewConfig.js +11 -0
  111. package/dist/review/reviewRound.js +399 -0
  112. package/dist/review/taskFinalReviewContract.js +90 -0
  113. package/dist/role/role.js +124 -23
  114. package/dist/run/agentRun.js +155 -12
  115. package/dist/run/runIdentity.js +82 -0
  116. package/dist/runtime/exactControlPlane.js +472 -0
  117. package/dist/runtime/index.js +8 -0
  118. package/dist/runtime/lifecycleReservation.js +38 -0
  119. package/dist/runtime/ports.js +11 -0
  120. package/dist/runtime/preallocatedNativeSession.js +13 -0
  121. package/dist/runtime/promptEnvelope.js +30 -0
  122. package/dist/runtime/runtimeBinding.js +31 -0
  123. package/dist/runtime/runtimeOwner.js +14 -0
  124. package/dist/runtime/sessionLaunchRequest.js +62 -0
  125. package/dist/runtime/sessionTitle.js +54 -0
  126. package/dist/runtime/taskRuntimeIsolation.js +643 -0
  127. package/dist/runtime/tmuxAdapters.js +315 -0
  128. package/dist/runtime/turnCompletion.js +3 -0
  129. package/dist/runtime/validation.js +23 -0
  130. package/dist/scheduler/activeRoleRunDelivery.js +342 -32
  131. package/dist/scheduler/activeTaskProgress.js +63 -0
  132. package/dist/scheduler/leaderFailure.js +2 -1
  133. package/dist/scheduler/leaderWakeupProcessor.js +307 -66
  134. package/dist/scheduler/operatorInputNotificationProcessor.js +109 -46
  135. package/dist/scheduler/operatorNotification.js +44 -2
  136. package/dist/scheduler/ports.js +28 -1
  137. package/dist/scheduler/roleRunLiveness.js +131 -25
  138. package/dist/scheduler/roleRunStall.js +951 -0
  139. package/dist/scheduler/taskExecutionProjection.js +544 -0
  140. package/dist/scheduler/wakeupQueue.js +3 -0
  141. package/dist/setup/setupCommand.js +302 -52
  142. package/dist/storage/compatibleTaskStore.js +102 -0
  143. package/dist/storage/migration/baseline.js +78 -0
  144. package/dist/storage/migration/classifier.js +51 -0
  145. package/dist/storage/migration/compatibleCodec.js +53 -0
  146. package/dist/storage/migration/engine.js +147 -0
  147. package/dist/storage/migration/index.js +33 -0
  148. package/dist/storage/migration/planner.js +154 -0
  149. package/dist/storage/migration/productionRegistry.js +486 -0
  150. package/dist/storage/migration/registry.js +169 -0
  151. package/dist/storage/migration/report.js +54 -0
  152. package/dist/storage/migration/types.js +31 -0
  153. package/dist/storage/storageSchema.js +147 -123
  154. package/dist/storage/storageVersions.js +11 -0
  155. package/dist/storage/taskStore.js +1793 -197
  156. package/dist/storage/upgrade/homeClassification.js +156 -0
  157. package/dist/storage/upgrade/homeMigrationTarget.js +595 -0
  158. package/dist/storage/upgrade/offlineUpgradeInventory.js +315 -0
  159. package/dist/storage/upgrade/productionMigrationRegistry.js +6 -0
  160. package/dist/storage/upgrade/recordVersionScan.js +176 -0
  161. package/dist/storage/upgrade/recordVersions.js +159 -0
  162. package/dist/storage/upgrade/switchProgress.js +80 -0
  163. package/dist/storage/upgrade/upgradeOrchestrator.js +948 -0
  164. package/dist/storage/upgrade/upgradeReceipt.js +161 -0
  165. package/dist/storage/upgradeCoordination.js +186 -0
  166. package/dist/storage/upgradeFence.js +366 -0
  167. package/dist/task/task.js +132 -26
  168. package/dist/task/taskRecordReference.js +66 -0
  169. package/dist/tmux/commandExecutor.js +75 -2
  170. package/dist/tmux/tmuxManager.js +747 -49
  171. package/dist/version.js +23 -0
  172. package/dist/web/assets/assetManifest.js +62 -0
  173. package/dist/web/assets/client/app.js +631 -0
  174. package/dist/web/assets/client/components.js +605 -0
  175. package/dist/web/assets/client/dom.js +14 -0
  176. package/dist/web/assets/client/format.js +28 -0
  177. package/dist/web/assets/client/i18n.js +494 -0
  178. package/dist/web/assets/client/markdown.js +114 -0
  179. package/dist/web/assets/client/theme.js +32 -0
  180. package/dist/web/assets/client/view.js +458 -0
  181. package/dist/web/assets/fontData.js +12 -0
  182. package/dist/web/assets/fonts.js +12 -0
  183. package/dist/web/assets/shell.js +114 -0
  184. package/dist/web/assets/styles/cards.js +135 -0
  185. package/dist/web/assets/styles/layout.js +47 -0
  186. package/dist/web/assets/styles/markdown.js +29 -0
  187. package/dist/web/assets/styles/responsive.js +39 -0
  188. package/dist/web/assets/styles/tokens.js +101 -0
  189. package/dist/web/assets/styles/widgets.js +147 -0
  190. package/dist/web/tmuxWebTerminal.js +158 -0
  191. package/dist/web/webServer.js +463 -0
  192. package/dist/web/webSnapshot.js +148 -0
  193. package/dist/workItem/workItem.js +642 -23
  194. package/dist/workspace/gitChangeSetCapture.js +86 -0
  195. package/dist/workspace/workItemChangeSetManager.js +445 -0
  196. package/dist/worktree/managedWorkspace.js +202 -0
  197. package/docs/task-local-identity.md +62 -0
  198. package/i18n/README.zh-CN.md +406 -31
  199. package/package.json +10 -2
  200. package/skills/yui-leader/SKILL.md +601 -39
  201. package/skills/yui-operator/SKILL.md +255 -34
  202. package/skills/yui-reviewer/SKILL.md +57 -0
  203. package/skills/yui-worker/SKILL.md +214 -17
  204. package/dist/commands/repositoryCommands.js +0 -86
  205. package/dist/operator/operatorContext.js +0 -66
  206. package/dist/repository/repository.js +0 -55
  207. package/dist/scheduler/archivedTaskRuntime.js +0 -12
  208. package/dist/worktree/roleWorkspace.js +0 -62
@@ -1,9 +1,46 @@
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) {
1
+ import { callController } from "../core/controllerClient.js";
2
+ import { FileRuntimeEventInbox } from "./runtimeEventInbox.js";
3
+ import { yuiRunBodyFromInputMessage, yuiRunIdFromInputMessages } from "../run/runIdentity.js";
4
+ import { runtimeLifecycleSignalKey } from "../runtime/lifecycleReservation.js";
5
+ import { setCodexThreadName } from "../execution/codexThreadNaming.js";
6
+ import { openCompatibleFileTaskStore } from "../storage/compatibleTaskStore.js";
7
+ const NO_FINAL_ASSISTANT_MESSAGE_SUMMARY = "Native Turn completed without a final assistant message.";
8
+ /** Hidden CLI entrypoint used by Codex's structured notify hook. */
9
+ export async function runSessionNotifyCommand(payloadArgument, environment = process.env, call, setThreadName = setCodexThreadName) {
5
10
  const params = parseCodexSessionNotification(payloadArgument, environment);
6
- await call(requireText(environment.YUI_HOME, "YUI_HOME"), SESSION_BIND_METHOD, toJson(params));
11
+ const home = requireText(environment.YUI_HOME, "YUI_HOME");
12
+ const enqueued = new FileRuntimeEventInbox(home).enqueueTurnCompleted({
13
+ scope: params.scope,
14
+ ...(params.scope === "task" ? { taskId: params.taskId } : {}),
15
+ roleName: params.roleName,
16
+ agentId: params.agentId,
17
+ adapterId: params.adapterId,
18
+ launchId: params.launchId,
19
+ nativeSessionId: params.nativeSessionId,
20
+ turnId: params.turnId,
21
+ ...(params.runId === undefined ? {} : { runId: params.runId }),
22
+ ...(params.title === undefined ? {} : { title: params.title }),
23
+ summary: params.lastAssistantMessage
24
+ });
25
+ // The durable write is authoritative. This short socket call is only a
26
+ // wake-up hint and never starts or waits for a Controller process.
27
+ await (call ?? callController)(home, "scheduler.signal", {
28
+ key: runtimeLifecycleSignalKey(params.scope === "task"
29
+ ? {
30
+ scope: "task",
31
+ taskId: params.taskId,
32
+ roleName: params.roleName
33
+ }
34
+ : {
35
+ scope: "global",
36
+ roleName: params.roleName
37
+ })
38
+ }, { timeoutMs: 100 }).catch(() => { });
39
+ if (enqueued.created && shouldSetThreadName(home, params)) {
40
+ const request = threadNameRequest(params, environment);
41
+ if (request !== null)
42
+ await setThreadName(request).catch(() => { });
43
+ }
7
44
  }
8
45
  export function parseCodexSessionNotification(payloadArgument, environment) {
9
46
  const payload = parseObject(payloadArgument, "Codex notify payload");
@@ -11,6 +48,12 @@ export function parseCodexSessionNotification(payloadArgument, environment) {
11
48
  throw new Error("Codex notify payload type is invalid.");
12
49
  }
13
50
  const nativeSessionId = requireText(payload["thread-id"], "Codex thread-id");
51
+ const turnId = requireText(payload["turn-id"], "Codex turn-id");
52
+ const lastAssistantMessage = requireAssistantMessage(payload["last-assistant-message"]);
53
+ const runId = yuiRunIdFromInputMessages(payload["input-messages"]);
54
+ const title = environment.YUI_SESSION_TITLE === undefined
55
+ ? sessionTitleFromInputMessages(payload["input-messages"])
56
+ : requireText(environment.YUI_SESSION_TITLE, "YUI_SESSION_TITLE");
14
57
  const scope = environment.YUI_SESSION_SCOPE;
15
58
  if (scope !== "task" && scope !== "global") {
16
59
  throw new Error("YUI_SESSION_SCOPE must be task or global.");
@@ -20,7 +63,12 @@ export function parseCodexSessionNotification(payloadArgument, environment) {
20
63
  roleName: requireText(environment.YUI_ROLE, "YUI_ROLE"),
21
64
  agentId: requireText(environment.YUI_AGENT_ID, "YUI_AGENT_ID"),
22
65
  adapterId: requireCodexAdapter(environment.YUI_ADAPTER_ID),
23
- nativeSessionId
66
+ launchId: requireText(environment.YUI_LAUNCH_ID, "YUI_LAUNCH_ID"),
67
+ nativeSessionId,
68
+ turnId,
69
+ ...(runId === undefined ? {} : { runId }),
70
+ ...(title === undefined ? {} : { title }),
71
+ lastAssistantMessage
24
72
  };
25
73
  return scope === "task"
26
74
  ? {
@@ -29,64 +77,6 @@ export function parseCodexSessionNotification(payloadArgument, environment) {
29
77
  }
30
78
  : common;
31
79
  }
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
80
  function parseObject(value, label) {
91
81
  if (value === undefined || Buffer.byteLength(value) > 1_048_576) {
92
82
  throw new Error(`${label} is invalid.`);
@@ -115,22 +105,75 @@ function requireText(value, label) {
115
105
  throw new Error(`${label} is invalid.`);
116
106
  return text;
117
107
  }
118
- function isObject(value) {
119
- return typeof value === "object" && value !== null && !Array.isArray(value);
108
+ function requireAssistantMessage(value) {
109
+ if (value === null)
110
+ return NO_FINAL_ASSISTANT_MESSAGE_SUMMARY;
111
+ if (typeof value !== "string" || value.includes("\0")) {
112
+ throw new Error("Codex last assistant message is required.");
113
+ }
114
+ const text = value.trim();
115
+ if (text.length === 0 || Buffer.byteLength(text) > 524_288) {
116
+ throw new Error("Codex last assistant message is invalid.");
117
+ }
118
+ return text;
119
+ }
120
+ function sessionTitleFromInputMessages(value) {
121
+ if (!Array.isArray(value))
122
+ return undefined;
123
+ for (const entry of value) {
124
+ if (typeof entry !== "string")
125
+ continue;
126
+ const body = yuiRunBodyFromInputMessage(entry);
127
+ const normalized = body.trim().replaceAll(/\s+/g, " ");
128
+ if (normalized.length > 0)
129
+ return truncateSessionText(normalized);
130
+ }
131
+ return undefined;
120
132
  }
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]);
133
+ function shouldSetThreadName(home, params) {
134
+ if (params.scope !== "task"
135
+ || params.title === undefined
136
+ || params.runId === undefined)
137
+ return false;
138
+ try {
139
+ const store = openCompatibleFileTaskStore(home);
140
+ return store.getAgentRun(params.taskId, params.runId)?.mode === "new";
141
+ }
142
+ catch {
143
+ return false;
144
+ }
125
145
  }
126
- function invalidParams() {
127
- return applicationError("INVALID_PARAMS", "Runtime session params are invalid.");
146
+ function threadNameRequest(params, environment) {
147
+ if (params.title === undefined)
148
+ return null;
149
+ const command = environment.YUI_AGENT_COMMAND;
150
+ const encodedBaseArgs = environment.YUI_AGENT_BASE_ARGS;
151
+ if (command === undefined || encodedBaseArgs === undefined)
152
+ return null;
153
+ try {
154
+ const parsed = JSON.parse(encodedBaseArgs);
155
+ if (!Array.isArray(parsed)
156
+ || parsed.some((value) => typeof value !== "string" || value.includes("\0"))) {
157
+ return null;
158
+ }
159
+ return {
160
+ command: requireText(command, "YUI_AGENT_COMMAND"),
161
+ baseArgs: parsed,
162
+ environment,
163
+ threadId: params.nativeSessionId,
164
+ name: params.title
165
+ };
166
+ }
167
+ catch {
168
+ return null;
169
+ }
128
170
  }
129
- function applicationError(code, message) {
130
- const error = Object.assign(new Error(message), { code });
131
- error.name = "CoreApplicationError";
132
- return error;
171
+ function truncateSessionText(value) {
172
+ const truncated = value.slice(0, 1_024);
173
+ return /[\uD800-\uDBFF]$/.test(truncated)
174
+ ? truncated.slice(0, -1)
175
+ : truncated;
133
176
  }
134
- function toJson(value) {
135
- return JSON.parse(JSON.stringify(value));
177
+ function isObject(value) {
178
+ return typeof value === "object" && value !== null && !Array.isArray(value);
136
179
  }
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Pure nearest-deadline selection shared by business deadlines and runtime
3
+ * recovery deadlines. Invalid timestamps are ignored and equal deadlines are
4
+ * coalesced into one wake batch.
5
+ */
6
+ export function nearestDeadlineBatch(deadlines) {
7
+ const valid = deadlines.filter((deadline) => Number.isFinite(deadline.at));
8
+ if (valid.length === 0)
9
+ return null;
10
+ const at = Math.min(...valid.map((deadline) => deadline.at));
11
+ return {
12
+ at,
13
+ keys: [...new Set(valid.filter((deadline) => deadline.at === at).map((deadline) => deadline.key))]
14
+ };
15
+ }
@@ -0,0 +1,108 @@
1
+ const DEFAULT_WINDOW_MS = 100;
2
+ /**
3
+ * Coalesces dirty mailbox keys over a fixed window. The first signal starts
4
+ * the window; later signals never extend it. A signal received during a batch
5
+ * belongs to a later batch, and batches are never processed concurrently.
6
+ */
7
+ export class MailboxScheduler {
8
+ processBatch;
9
+ #windowMs;
10
+ #setTimer;
11
+ #clearTimer;
12
+ #onError;
13
+ #pendingKeys = new Set();
14
+ #timer;
15
+ #current;
16
+ #drainRequested = false;
17
+ #stopped = false;
18
+ constructor(processBatch, options = {}) {
19
+ this.processBatch = processBatch;
20
+ this.#windowMs = nonNegativeFinite(options.windowMs ?? DEFAULT_WINDOW_MS);
21
+ this.#setTimer = options.setTimer ?? ((callback, delayMs) => setTimeout(callback, delayMs));
22
+ this.#clearTimer = options.clearTimer ?? ((timer) => {
23
+ clearTimeout(timer);
24
+ });
25
+ this.#onError = options.onError ?? (() => { });
26
+ }
27
+ signal(key) {
28
+ if (this.#stopped)
29
+ return;
30
+ this.#pendingKeys.add(key);
31
+ this.schedule();
32
+ }
33
+ schedule() {
34
+ if (this.#stopped
35
+ || this.#pendingKeys.size === 0
36
+ || this.#timer !== undefined) {
37
+ return;
38
+ }
39
+ this.#timer = this.#setTimer(() => {
40
+ this.#timer = undefined;
41
+ if (this.#current !== undefined) {
42
+ this.#drainRequested = true;
43
+ return;
44
+ }
45
+ void this.drain().catch(this.#onError);
46
+ }, this.#windowMs);
47
+ }
48
+ drain() {
49
+ if (this.#stopped)
50
+ return Promise.resolve();
51
+ this.#cancelTimer();
52
+ if (this.#current !== undefined) {
53
+ this.#drainRequested = true;
54
+ return this.#current;
55
+ }
56
+ if (this.#pendingKeys.size === 0)
57
+ return Promise.resolve();
58
+ // Defer invoking the callback until #current is installed. This closes the
59
+ // small reentrancy window where a synchronous callback could call drain()
60
+ // before the scheduler knew a drain was already active.
61
+ const running = Promise.resolve().then(() => this.#drainDueBatches());
62
+ this.#current = running;
63
+ void running.finally(() => {
64
+ if (this.#current !== running)
65
+ return;
66
+ this.#current = undefined;
67
+ if (this.#stopped || this.#pendingKeys.size === 0)
68
+ return;
69
+ if (this.#drainRequested) {
70
+ this.#drainRequested = false;
71
+ void this.drain().catch(this.#onError);
72
+ }
73
+ else {
74
+ this.schedule();
75
+ }
76
+ }).catch(() => { });
77
+ return running;
78
+ }
79
+ stop() {
80
+ this.#stopped = true;
81
+ this.#cancelTimer();
82
+ this.#pendingKeys.clear();
83
+ this.#drainRequested = false;
84
+ }
85
+ async #drainDueBatches() {
86
+ do {
87
+ this.#drainRequested = false;
88
+ const keys = [...this.#pendingKeys];
89
+ this.#pendingKeys.clear();
90
+ if (keys.length > 0)
91
+ await this.processBatch(keys);
92
+ } while (!this.#stopped
93
+ && this.#drainRequested
94
+ && this.#pendingKeys.size > 0);
95
+ }
96
+ #cancelTimer() {
97
+ if (this.#timer === undefined)
98
+ return;
99
+ this.#clearTimer(this.#timer);
100
+ this.#timer = undefined;
101
+ }
102
+ }
103
+ function nonNegativeFinite(value) {
104
+ if (!Number.isFinite(value) || value < 0) {
105
+ throw new Error("Mailbox scheduler window must be a non-negative finite number.");
106
+ }
107
+ return value;
108
+ }
@@ -0,0 +1,329 @@
1
+ import { validateTaskRecordReference } from "../task/taskRecordReference.js";
2
+ function requireText(value, label) {
3
+ const normalized = value.trim();
4
+ if (normalized.length === 0) {
5
+ throw new Error(`${label} must not be empty`);
6
+ }
7
+ return normalized;
8
+ }
9
+ function requireInteger(value, minimum, label) {
10
+ if (!Number.isInteger(value) || value < minimum) {
11
+ throw new Error(`${label} must be an integer greater than or equal to ${minimum}`);
12
+ }
13
+ return value;
14
+ }
15
+ function record(value, label) {
16
+ if (value === null || typeof value !== "object" || Array.isArray(value)) {
17
+ throw new Error(`${label} must be an object`);
18
+ }
19
+ return value;
20
+ }
21
+ function exact(value, keys, label) {
22
+ const expected = new Set(keys);
23
+ const unknown = Object.keys(value).find((key) => !expected.has(key));
24
+ if (unknown !== undefined)
25
+ throw new Error(`${label} has unknown field: ${unknown}`);
26
+ const missing = keys.find((key) => !Object.hasOwn(value, key));
27
+ if (missing !== undefined)
28
+ throw new Error(`${label} is missing field: ${missing}`);
29
+ }
30
+ function requireString(value, label) {
31
+ if (typeof value !== "string")
32
+ throw new Error(`${label} must be a string`);
33
+ return requireText(value, label);
34
+ }
35
+ function requireTimestamp(value, label) {
36
+ const timestamp = requireString(value, label);
37
+ if (!Number.isFinite(Date.parse(timestamp)))
38
+ throw new Error(`${label} must be a timestamp`);
39
+ return timestamp;
40
+ }
41
+ function requireStringArray(value, label) {
42
+ if (!Array.isArray(value))
43
+ throw new Error(`${label} must be an array`);
44
+ const result = value.map((item, index) => requireString(item, `${label}[${index}]`));
45
+ if (new Set(result).size !== result.length)
46
+ throw new Error(`${label} must not contain duplicates`);
47
+ return result;
48
+ }
49
+ function copyTarget(target) {
50
+ switch (target.kind) {
51
+ case "task":
52
+ return { kind: "task", taskId: requireText(target.taskId, "taskId") };
53
+ case "role":
54
+ return {
55
+ kind: "role",
56
+ taskId: requireText(target.taskId, "taskId"),
57
+ roleName: requireText(target.roleName, "roleName")
58
+ };
59
+ case "role-runtime":
60
+ return {
61
+ kind: "role-runtime",
62
+ taskId: requireText(target.taskId, "taskId"),
63
+ roleName: requireText(target.roleName, "roleName")
64
+ };
65
+ case "global-role-runtime":
66
+ return {
67
+ kind: "global-role-runtime",
68
+ roleName: requireText(target.roleName, "roleName")
69
+ };
70
+ case "operator":
71
+ return { kind: "operator" };
72
+ }
73
+ }
74
+ export function mailboxTargetKey(target) {
75
+ const copied = copyTarget(target);
76
+ switch (copied.kind) {
77
+ case "operator": return "operator";
78
+ case "task": return `task/${encodeURIComponent(copied.taskId)}`;
79
+ case "role": return `role/${encodeURIComponent(copied.taskId)}/${encodeURIComponent(copied.roleName)}`;
80
+ case "role-runtime":
81
+ return `role-runtime/${encodeURIComponent(copied.taskId)}/${encodeURIComponent(copied.roleName)}`;
82
+ case "global-role-runtime":
83
+ return `global-role-runtime/${encodeURIComponent(copied.roleName)}`;
84
+ }
85
+ }
86
+ function copyRef(ref) {
87
+ if (!("taskId" in ref)) {
88
+ if (ref.type !== "task" && ref.type !== "session") {
89
+ throw new Error(`entity reference taskId is required for ${ref.type}`);
90
+ }
91
+ return {
92
+ type: ref.type,
93
+ id: requireText(ref.id, "entity reference id")
94
+ };
95
+ }
96
+ const validated = validateTaskRecordReference({
97
+ taskId: ref.taskId,
98
+ localId: ref.id
99
+ }, mailboxTaskRecordKind(ref.type));
100
+ return {
101
+ type: ref.type,
102
+ taskId: validated.taskId,
103
+ id: validated.localId
104
+ };
105
+ }
106
+ export function mailboxEntityRefKey(ref) {
107
+ return !("taskId" in ref)
108
+ ? `${ref.type}\u0000${ref.id}`
109
+ : `${ref.type}\u0000${ref.taskId}\u0000${ref.id}`;
110
+ }
111
+ function appendUnique(existing, incoming, keyOf) {
112
+ const result = [...existing];
113
+ const keys = new Set(existing.map(keyOf));
114
+ for (const value of incoming) {
115
+ const key = keyOf(value);
116
+ if (!keys.has(key)) {
117
+ keys.add(key);
118
+ result.push(value);
119
+ }
120
+ }
121
+ return result;
122
+ }
123
+ function mergeBatches(earlier, later) {
124
+ const merged = {
125
+ fromSequence: earlier.fromSequence,
126
+ toSequence: later.toSequence,
127
+ reasons: appendUnique(earlier.reasons, later.reasons, (reason) => reason),
128
+ refs: appendUnique(earlier.refs, later.refs, mailboxEntityRefKey),
129
+ requestCount: earlier.requestCount + later.requestCount,
130
+ firstQueuedAt: earlier.firstQueuedAt,
131
+ lastQueuedAt: later.lastQueuedAt
132
+ };
133
+ return merged;
134
+ }
135
+ function requireProcessing(mailbox, batchId) {
136
+ const normalizedBatchId = requireText(batchId, "batch id");
137
+ if (mailbox.processing === null) {
138
+ throw new Error("Mailbox has no processing batch");
139
+ }
140
+ if (mailbox.processing.batchId !== normalizedBatchId) {
141
+ throw new Error(`Mailbox processing batch id does not match ${normalizedBatchId}`);
142
+ }
143
+ return mailbox.processing;
144
+ }
145
+ export function createWorkMailbox(target) {
146
+ return {
147
+ schemaVersion: 1,
148
+ target: copyTarget(target),
149
+ nextSequence: 1,
150
+ processing: null,
151
+ pending: null
152
+ };
153
+ }
154
+ export function validateWorkMailbox(value) {
155
+ const mailbox = record(value, "WorkMailbox");
156
+ exact(mailbox, ["schemaVersion", "target", "nextSequence", "processing", "pending"], "WorkMailbox");
157
+ if (mailbox.schemaVersion !== 1)
158
+ throw new Error("WorkMailbox must use schemaVersion 1");
159
+ const target = parseTarget(mailbox.target);
160
+ const nextSequence = requireInteger(mailbox.nextSequence, 1, "WorkMailbox nextSequence");
161
+ const processing = mailbox.processing === null ? null : parseProcessing(mailbox.processing);
162
+ const pending = mailbox.pending === null ? null : parseBatch(mailbox.pending, "WorkMailbox pending");
163
+ const batches = [processing?.batch, pending].filter((batch) => batch !== null && batch !== undefined);
164
+ for (const batch of batches) {
165
+ if (batch.toSequence >= nextSequence) {
166
+ throw new Error("WorkMailbox batch sequence must be lower than nextSequence");
167
+ }
168
+ }
169
+ if (processing !== null && pending !== null && processing.batch.toSequence >= pending.fromSequence) {
170
+ throw new Error("WorkMailbox processing and pending sequences overlap");
171
+ }
172
+ return { schemaVersion: 1, target, nextSequence, processing, pending };
173
+ }
174
+ function parseTarget(value) {
175
+ const target = record(value, "WorkMailbox target");
176
+ switch (target.kind) {
177
+ case "operator":
178
+ exact(target, ["kind"], "WorkMailbox operator target");
179
+ return { kind: "operator" };
180
+ case "task":
181
+ exact(target, ["kind", "taskId"], "WorkMailbox task target");
182
+ return { kind: "task", taskId: requireString(target.taskId, "WorkMailbox target taskId") };
183
+ case "role":
184
+ exact(target, ["kind", "taskId", "roleName"], "WorkMailbox role target");
185
+ return {
186
+ kind: "role",
187
+ taskId: requireString(target.taskId, "WorkMailbox target taskId"),
188
+ roleName: requireString(target.roleName, "WorkMailbox target roleName")
189
+ };
190
+ case "role-runtime":
191
+ exact(target, ["kind", "taskId", "roleName"], "WorkMailbox role runtime target");
192
+ return {
193
+ kind: "role-runtime",
194
+ taskId: requireString(target.taskId, "WorkMailbox target taskId"),
195
+ roleName: requireString(target.roleName, "WorkMailbox target roleName")
196
+ };
197
+ case "global-role-runtime":
198
+ exact(target, ["kind", "roleName"], "WorkMailbox global role runtime target");
199
+ return {
200
+ kind: "global-role-runtime",
201
+ roleName: requireString(target.roleName, "WorkMailbox target roleName")
202
+ };
203
+ default:
204
+ throw new Error("WorkMailbox target kind is invalid");
205
+ }
206
+ }
207
+ function parseRef(value, label) {
208
+ const ref = record(value, label);
209
+ const types = ["task", "run", "work-item", "input", "session", "message"];
210
+ if (!types.includes(ref.type))
211
+ throw new Error(`${label} type is invalid`);
212
+ if (ref.type === "task" || ref.type === "session") {
213
+ exact(ref, ["type", "id"], label);
214
+ return { type: ref.type, id: requireString(ref.id, `${label} id`) };
215
+ }
216
+ exact(ref, ["type", "taskId", "id"], label);
217
+ return copyRef({
218
+ type: ref.type,
219
+ taskId: requireString(ref.taskId, `${label} taskId`),
220
+ id: requireString(ref.id, `${label} id`)
221
+ });
222
+ }
223
+ function parseBatch(value, label) {
224
+ const batch = record(value, label);
225
+ const required = ["fromSequence", "toSequence", "reasons", "refs", "requestCount", "firstQueuedAt", "lastQueuedAt"];
226
+ exact(batch, required, label);
227
+ const fromSequence = requireInteger(batch.fromSequence, 1, `${label} fromSequence`);
228
+ const toSequence = requireInteger(batch.toSequence, fromSequence, `${label} toSequence`);
229
+ const requestCount = requireInteger(batch.requestCount, 1, `${label} requestCount`);
230
+ if (requestCount !== toSequence - fromSequence + 1) {
231
+ throw new Error(`${label} requestCount does not match its sequence range`);
232
+ }
233
+ const reasons = requireStringArray(batch.reasons, `${label} reasons`);
234
+ if (reasons.length === 0)
235
+ throw new Error(`${label} reasons must not be empty`);
236
+ if (!Array.isArray(batch.refs))
237
+ throw new Error(`${label} refs must be an array`);
238
+ const refs = batch.refs.map((ref, index) => parseRef(ref, `${label} refs[${index}]`));
239
+ const refKeys = refs.map(mailboxEntityRefKey);
240
+ if (new Set(refKeys).size !== refKeys.length)
241
+ throw new Error(`${label} refs must not contain duplicates`);
242
+ const result = {
243
+ fromSequence,
244
+ toSequence,
245
+ reasons,
246
+ refs,
247
+ requestCount,
248
+ firstQueuedAt: requireTimestamp(batch.firstQueuedAt, `${label} firstQueuedAt`),
249
+ lastQueuedAt: requireTimestamp(batch.lastQueuedAt, `${label} lastQueuedAt`)
250
+ };
251
+ return result;
252
+ }
253
+ function parseProcessing(value) {
254
+ const processing = record(value, "WorkMailbox processing");
255
+ const required = ["batchId", "batch", "owner", "startedAt"];
256
+ exact(processing, processing.executionRef === undefined ? required : [...required, "executionRef"], "WorkMailbox processing");
257
+ const result = {
258
+ batchId: requireString(processing.batchId, "WorkMailbox processing batchId"),
259
+ batch: parseBatch(processing.batch, "WorkMailbox processing batch"),
260
+ owner: requireString(processing.owner, "WorkMailbox processing owner"),
261
+ startedAt: requireTimestamp(processing.startedAt, "WorkMailbox processing startedAt")
262
+ };
263
+ return processing.executionRef === undefined
264
+ ? result
265
+ : { ...result, executionRef: parseRef(processing.executionRef, "WorkMailbox processing executionRef") };
266
+ }
267
+ export function enqueueSignal(mailbox, signal) {
268
+ const reason = requireText(signal.reason, "signal reason");
269
+ const occurredAt = requireText(signal.occurredAt, "signal occurredAt");
270
+ const refs = signal.refs.map(copyRef);
271
+ const sequence = mailbox.nextSequence;
272
+ const incoming = {
273
+ fromSequence: sequence,
274
+ toSequence: sequence,
275
+ reasons: [reason],
276
+ refs: appendUnique([], refs, mailboxEntityRefKey),
277
+ requestCount: 1,
278
+ firstQueuedAt: occurredAt,
279
+ lastQueuedAt: occurredAt
280
+ };
281
+ return {
282
+ ...mailbox,
283
+ nextSequence: sequence + 1,
284
+ pending: mailbox.pending === null
285
+ ? incoming
286
+ : mergeBatches(mailbox.pending, incoming)
287
+ };
288
+ }
289
+ function mailboxTaskRecordKind(type) {
290
+ switch (type) {
291
+ case "run": return "agentRun";
292
+ case "work-item": return "workItem";
293
+ case "input": return "inputRequest";
294
+ case "message": return "message";
295
+ }
296
+ }
297
+ export function claimPending(mailbox, options) {
298
+ if (mailbox.processing !== null) {
299
+ throw new Error("Mailbox is already processing a batch");
300
+ }
301
+ if (mailbox.pending === null) {
302
+ throw new Error("Mailbox has no pending work to claim");
303
+ }
304
+ const processing = {
305
+ batchId: requireText(options.batchId, "batch id"),
306
+ batch: mailbox.pending,
307
+ owner: requireText(options.owner, "claim owner"),
308
+ startedAt: requireText(options.startedAt, "claim startedAt")
309
+ };
310
+ return { ...mailbox, processing, pending: null };
311
+ }
312
+ export function bindExecution(mailbox, batchId, executionRef) {
313
+ const processing = requireProcessing(mailbox, batchId);
314
+ return {
315
+ ...mailbox,
316
+ processing: { ...processing, executionRef: copyRef(executionRef) }
317
+ };
318
+ }
319
+ export function completeProcessing(mailbox, batchId) {
320
+ requireProcessing(mailbox, batchId);
321
+ return { ...mailbox, processing: null };
322
+ }
323
+ export function releaseProcessing(mailbox, batchId) {
324
+ const processing = requireProcessing(mailbox, batchId);
325
+ const released = mailbox.pending === null
326
+ ? processing.batch
327
+ : mergeBatches(processing.batch, mailbox.pending);
328
+ return { ...mailbox, processing: null, pending: released };
329
+ }