@someonegg/agent-hub 0.0.0-stage → 0.7.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 (185) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +104 -2
  3. package/README.zh-CN.md +99 -0
  4. package/README.zh-TW.md +99 -0
  5. package/dist/client/apple-touch-icon.png +0 -0
  6. package/dist/client/assets/KaTeX_AMS-Regular-BQhdFMY1.woff2 +0 -0
  7. package/dist/client/assets/KaTeX_AMS-Regular-DMm9YOAa.woff +0 -0
  8. package/dist/client/assets/KaTeX_AMS-Regular-DRggAlZN.ttf +0 -0
  9. package/dist/client/assets/KaTeX_Caligraphic-Bold-ATXxdsX0.ttf +0 -0
  10. package/dist/client/assets/KaTeX_Caligraphic-Bold-BEiXGLvX.woff +0 -0
  11. package/dist/client/assets/KaTeX_Caligraphic-Bold-Dq_IR9rO.woff2 +0 -0
  12. package/dist/client/assets/KaTeX_Caligraphic-Regular-CTRA-rTL.woff +0 -0
  13. package/dist/client/assets/KaTeX_Caligraphic-Regular-Di6jR-x-.woff2 +0 -0
  14. package/dist/client/assets/KaTeX_Caligraphic-Regular-wX97UBjC.ttf +0 -0
  15. package/dist/client/assets/KaTeX_Fraktur-Bold-BdnERNNW.ttf +0 -0
  16. package/dist/client/assets/KaTeX_Fraktur-Bold-BsDP51OF.woff +0 -0
  17. package/dist/client/assets/KaTeX_Fraktur-Bold-CL6g_b3V.woff2 +0 -0
  18. package/dist/client/assets/KaTeX_Fraktur-Regular-CB_wures.ttf +0 -0
  19. package/dist/client/assets/KaTeX_Fraktur-Regular-CTYiF6lA.woff2 +0 -0
  20. package/dist/client/assets/KaTeX_Fraktur-Regular-Dxdc4cR9.woff +0 -0
  21. package/dist/client/assets/KaTeX_Main-Bold-Cx986IdX.woff2 +0 -0
  22. package/dist/client/assets/KaTeX_Main-Bold-Jm3AIy58.woff +0 -0
  23. package/dist/client/assets/KaTeX_Main-Bold-waoOVXN0.ttf +0 -0
  24. package/dist/client/assets/KaTeX_Main-BoldItalic-DxDJ3AOS.woff2 +0 -0
  25. package/dist/client/assets/KaTeX_Main-BoldItalic-DzxPMmG6.ttf +0 -0
  26. package/dist/client/assets/KaTeX_Main-BoldItalic-SpSLRI95.woff +0 -0
  27. package/dist/client/assets/KaTeX_Main-Italic-3WenGoN9.ttf +0 -0
  28. package/dist/client/assets/KaTeX_Main-Italic-BMLOBm91.woff +0 -0
  29. package/dist/client/assets/KaTeX_Main-Italic-NWA7e6Wa.woff2 +0 -0
  30. package/dist/client/assets/KaTeX_Main-Regular-B22Nviop.woff2 +0 -0
  31. package/dist/client/assets/KaTeX_Main-Regular-Dr94JaBh.woff +0 -0
  32. package/dist/client/assets/KaTeX_Main-Regular-ypZvNtVU.ttf +0 -0
  33. package/dist/client/assets/KaTeX_Math-BoldItalic-B3XSjfu4.ttf +0 -0
  34. package/dist/client/assets/KaTeX_Math-BoldItalic-CZnvNsCZ.woff2 +0 -0
  35. package/dist/client/assets/KaTeX_Math-BoldItalic-iY-2wyZ7.woff +0 -0
  36. package/dist/client/assets/KaTeX_Math-Italic-DA0__PXp.woff +0 -0
  37. package/dist/client/assets/KaTeX_Math-Italic-flOr_0UB.ttf +0 -0
  38. package/dist/client/assets/KaTeX_Math-Italic-t53AETM-.woff2 +0 -0
  39. package/dist/client/assets/KaTeX_SansSerif-Bold-CFMepnvq.ttf +0 -0
  40. package/dist/client/assets/KaTeX_SansSerif-Bold-D1sUS0GD.woff2 +0 -0
  41. package/dist/client/assets/KaTeX_SansSerif-Bold-DbIhKOiC.woff +0 -0
  42. package/dist/client/assets/KaTeX_SansSerif-Italic-C3H0VqGB.woff2 +0 -0
  43. package/dist/client/assets/KaTeX_SansSerif-Italic-DN2j7dab.woff +0 -0
  44. package/dist/client/assets/KaTeX_SansSerif-Italic-YYjJ1zSn.ttf +0 -0
  45. package/dist/client/assets/KaTeX_SansSerif-Regular-BNo7hRIc.ttf +0 -0
  46. package/dist/client/assets/KaTeX_SansSerif-Regular-CS6fqUqJ.woff +0 -0
  47. package/dist/client/assets/KaTeX_SansSerif-Regular-DDBCnlJ7.woff2 +0 -0
  48. package/dist/client/assets/KaTeX_Script-Regular-C5JkGWo-.ttf +0 -0
  49. package/dist/client/assets/KaTeX_Script-Regular-D3wIWfF6.woff2 +0 -0
  50. package/dist/client/assets/KaTeX_Script-Regular-D5yQViql.woff +0 -0
  51. package/dist/client/assets/KaTeX_Size1-Regular-C195tn64.woff +0 -0
  52. package/dist/client/assets/KaTeX_Size1-Regular-Dbsnue_I.ttf +0 -0
  53. package/dist/client/assets/KaTeX_Size1-Regular-mCD8mA8B.woff2 +0 -0
  54. package/dist/client/assets/KaTeX_Size2-Regular-B7gKUWhC.ttf +0 -0
  55. package/dist/client/assets/KaTeX_Size2-Regular-Dy4dx90m.woff2 +0 -0
  56. package/dist/client/assets/KaTeX_Size2-Regular-oD1tc_U0.woff +0 -0
  57. package/dist/client/assets/KaTeX_Size3-Regular-CTq5MqoE.woff +0 -0
  58. package/dist/client/assets/KaTeX_Size3-Regular-DgpXs0kz.ttf +0 -0
  59. package/dist/client/assets/KaTeX_Size4-Regular-BF-4gkZK.woff +0 -0
  60. package/dist/client/assets/KaTeX_Size4-Regular-DWFBv043.ttf +0 -0
  61. package/dist/client/assets/KaTeX_Size4-Regular-Dl5lxZxV.woff2 +0 -0
  62. package/dist/client/assets/KaTeX_Typewriter-Regular-C0xS9mPB.woff +0 -0
  63. package/dist/client/assets/KaTeX_Typewriter-Regular-CO6r4hn1.woff2 +0 -0
  64. package/dist/client/assets/KaTeX_Typewriter-Regular-D3Ib7_Hf.ttf +0 -0
  65. package/dist/client/assets/index-4ejyszaV.css +1 -0
  66. package/dist/client/assets/index-CKKT4cMK.js +309 -0
  67. package/dist/client/favicon.ico +0 -0
  68. package/dist/client/favicon.svg +6 -0
  69. package/dist/client/icons/icon-192.png +0 -0
  70. package/dist/client/icons/icon-512.png +0 -0
  71. package/dist/client/icons/icon-maskable-512.png +0 -0
  72. package/dist/client/index.html +22 -0
  73. package/dist/client/manifest.webmanifest +21 -0
  74. package/dist/server/server/adapters/claude/claude-session-source.js +220 -0
  75. package/dist/server/server/adapters/claude/identity.js +28 -0
  76. package/dist/server/server/adapters/claude/interaction.js +68 -0
  77. package/dist/server/server/adapters/claude/prompt-readiness.js +12 -0
  78. package/dist/server/server/adapters/claude/ready-script.js +25 -0
  79. package/dist/server/server/adapters/claude/session-manager.js +106 -0
  80. package/dist/server/server/adapters/claude/session-normalizer.js +173 -0
  81. package/dist/server/server/adapters/claude/user-input-normalizer.js +71 -0
  82. package/dist/server/server/adapters/codex/catalog-source.js +2 -0
  83. package/dist/server/server/adapters/codex/codex-session-source.js +250 -0
  84. package/dist/server/server/adapters/codex/identity-resolver.js +104 -0
  85. package/dist/server/server/adapters/codex/interaction-parser.js +70 -0
  86. package/dist/server/server/adapters/codex/io-errors.js +2 -0
  87. package/dist/server/server/adapters/codex/jsonl-catalog-source.js +84 -0
  88. package/dist/server/server/adapters/codex/limits.js +2 -0
  89. package/dist/server/server/adapters/codex/message-normalizer.js +66 -0
  90. package/dist/server/server/adapters/codex/response-item-parser.js +64 -0
  91. package/dist/server/server/adapters/codex/rollout-descriptor.js +2 -0
  92. package/dist/server/server/adapters/codex/session-index-reader.js +101 -0
  93. package/dist/server/server/adapters/codex/session-manager.js +51 -0
  94. package/dist/server/server/adapters/codex/session-normalizer.js +175 -0
  95. package/dist/server/server/adapters/codex/subagent-identity.js +9 -0
  96. package/dist/server/server/adapters/codex/user-input-normalizer.js +121 -0
  97. package/dist/server/server/adapters/codex/value-readers.js +4 -0
  98. package/dist/server/server/adapters/shared/binding-protocol.js +54 -0
  99. package/dist/server/server/adapters/shared/io-errors.js +13 -0
  100. package/dist/server/server/adapters/shared/jsonl-decoder.js +263 -0
  101. package/dist/server/server/adapters/shared/limits.js +4 -0
  102. package/dist/server/server/adapters/shared/session-management.js +43 -0
  103. package/dist/server/server/adapters/shared/source-cache.js +23 -0
  104. package/dist/server/server/api/session-api-mapper.js +65 -0
  105. package/dist/server/server/application/interaction-session-reader.js +17 -0
  106. package/dist/server/server/application/session-read-service.js +86 -0
  107. package/dist/server/server/application/session-reader.js +2 -0
  108. package/dist/server/server/application/session-response.js +11 -0
  109. package/dist/server/server/catalog/config.js +4 -0
  110. package/dist/server/server/catalog/session-catalog-service.js +112 -0
  111. package/dist/server/server/cli/command-error.js +27 -0
  112. package/dist/server/server/cli/main.js +42 -0
  113. package/dist/server/server/cli/output.js +52 -0
  114. package/dist/server/server/cli/resource.js +104 -0
  115. package/dist/server/server/cli/session-client.js +64 -0
  116. package/dist/server/server/cli/session-follow.js +58 -0
  117. package/dist/server/server/cli/session-output.js +184 -0
  118. package/dist/server/server/cli/session-wait.js +44 -0
  119. package/dist/server/server/cli/session.js +318 -0
  120. package/dist/server/server/cli/skill.js +37 -0
  121. package/dist/server/server/config.js +196 -0
  122. package/dist/server/server/create-hub.js +91 -0
  123. package/dist/server/server/directory-lock.js +65 -0
  124. package/dist/server/server/domain/project-directory.js +5 -0
  125. package/dist/server/server/domain/session-domain.js +2 -0
  126. package/dist/server/server/domain/session-text.js +45 -0
  127. package/dist/server/server/hosting/diff-store.js +103 -0
  128. package/dist/server/server/hosting/routes.js +168 -0
  129. package/dist/server/server/hosting/store.js +162 -0
  130. package/dist/server/server/hosting/validation-error.js +4 -0
  131. package/dist/server/server/http/api-route-handler.js +2 -0
  132. package/dist/server/server/http/api-router.js +54 -0
  133. package/dist/server/server/http/create-server.js +218 -0
  134. package/dist/server/server/http/query-params.js +19 -0
  135. package/dist/server/server/http/request-body.js +48 -0
  136. package/dist/server/server/http/request-errors.js +59 -0
  137. package/dist/server/server/http/router.js +16 -0
  138. package/dist/server/server/http/security.js +19 -0
  139. package/dist/server/server/http/session-interaction-routes.js +128 -0
  140. package/dist/server/server/http/session-management-routes.js +105 -0
  141. package/dist/server/server/http/session-path.js +10 -0
  142. package/dist/server/server/http/session-read-routes.js +143 -0
  143. package/dist/server/server/http/terminal-preview-websocket.js +179 -0
  144. package/dist/server/server/http/upload-routes.js +124 -0
  145. package/dist/server/server/interaction/interaction-service.js +276 -0
  146. package/dist/server/server/interaction/message.js +19 -0
  147. package/dist/server/server/interaction/pane-binding-manager.js +62 -0
  148. package/dist/server/server/interaction/pane-observation.js +31 -0
  149. package/dist/server/server/interaction/reconcile-session-state.js +21 -0
  150. package/dist/server/server/interaction/runtime-capabilities.js +2 -0
  151. package/dist/server/server/interaction/session-runtime.js +86 -0
  152. package/dist/server/server/interaction/terminal-activity-service.js +81 -0
  153. package/dist/server/server/interaction/terminal-preview-diff.js +71 -0
  154. package/dist/server/server/interaction/terminal-preview-service.js +184 -0
  155. package/dist/server/server/interaction/tmux-launcher.js +84 -0
  156. package/dist/server/server/interaction/tmux-preview-listener.js +107 -0
  157. package/dist/server/server/interaction/tmux-service.js +334 -0
  158. package/dist/server/server/interaction/tmux-target.js +13 -0
  159. package/dist/server/server/interaction/transport.js +9 -0
  160. package/dist/server/server/main.js +54 -0
  161. package/dist/server/server/management/operation-service.js +307 -0
  162. package/dist/server/server/management/operation-state.js +3 -0
  163. package/dist/server/server/management/session-exit-service.js +50 -0
  164. package/dist/server/server/management/session-launch-executor.js +164 -0
  165. package/dist/server/server/management/session-lifecycle-executor.js +158 -0
  166. package/dist/server/server/management/session-manager.js +64 -0
  167. package/dist/server/server/repository/catalog-snapshot-store.js +318 -0
  168. package/dist/server/server/repository/diagnostic-logger.js +70 -0
  169. package/dist/server/server/repository/refresh-coordinator.js +14 -0
  170. package/dist/server/server/repository/session-queries.js +72 -0
  171. package/dist/server/server/security/opaque-id.js +17 -0
  172. package/dist/server/server/source/session-source.js +2 -0
  173. package/dist/server/server/updates/session-updates-service.js +151 -0
  174. package/dist/server/server/updates/update-revision.js +22 -0
  175. package/dist/server/server/uploads/store.js +304 -0
  176. package/dist/server/server/version.js +21 -0
  177. package/dist/server/shared/api-contract.js +2 -0
  178. package/dist/server/shared/diff.js +158 -0
  179. package/dist/server/shared/domain.js +2 -0
  180. package/dist/server/shared/interaction.js +22 -0
  181. package/dist/server/shared/task-name.js +6 -0
  182. package/dist/server/shared/uploads.js +8 -0
  183. package/package.json +98 -4
  184. package/skills/ahub-control/SKILL.md +93 -0
  185. package/skills/ahub-publish/SKILL.md +54 -0
@@ -0,0 +1,307 @@
1
+ import { randomUUID } from "node:crypto";
2
+ import { paneBindings } from "../interaction/pane-binding-manager.js";
3
+ import { attachCommand } from "../interaction/tmux-launcher.js";
4
+ import { AssociationError, } from "./operation-state.js";
5
+ import { SessionLaunchExecutor } from "./session-launch-executor.js";
6
+ import { SessionLifecycleExecutor } from "./session-lifecycle-executor.js";
7
+ import { LAUNCH_ATTACH_DELAY_MS, SessionLaunchError, } from "./session-manager.js";
8
+ export class ManagementError extends Error {
9
+ status;
10
+ code;
11
+ constructor(status, message, code = "management_error") {
12
+ super(message);
13
+ this.status = status;
14
+ this.code = code;
15
+ }
16
+ }
17
+ export class OperationService {
18
+ sessions;
19
+ interaction;
20
+ managers;
21
+ now;
22
+ waitMs;
23
+ #entries = new Map();
24
+ #requests = new Map();
25
+ #active = new Map();
26
+ #results = new Map();
27
+ #timer;
28
+ #closed = false;
29
+ #launch;
30
+ #lifecycle;
31
+ constructor(sessions, interaction, managers, options = {}) {
32
+ this.sessions = sessions;
33
+ this.interaction = interaction;
34
+ this.managers = managers;
35
+ const { now = Date.now, waitMs = { create: 60_000, exit: 30_000, archive: 30_000, delete: 30_000 }, bindings = paneBindings, runtime, uploads, } = options;
36
+ this.now = now;
37
+ this.waitMs = waitMs;
38
+ const control = {
39
+ pending: (entry) => this.#pending(entry),
40
+ transition: (entry, context, deadline) => this.#transition(entry, context, deadline),
41
+ finish: (entry, status, error) => this.#finish(entry, status, error),
42
+ error: (entry, error) => this.#error(entry, error),
43
+ timeout: (entry) => this.#timeout(entry),
44
+ observe: (entry) => this.#observe(entry),
45
+ };
46
+ this.#launch = new SessionLaunchExecutor(sessions, interaction, managers, bindings, control, now, waitMs, runtime);
47
+ this.#lifecycle = new SessionLifecycleExecutor(sessions, interaction, managers, control, now, waitMs, uploads);
48
+ this.#timer = setInterval(() => {
49
+ void this.poll();
50
+ }, 1_000);
51
+ this.#timer.unref();
52
+ }
53
+ create(input) {
54
+ if (input.resumeSessionId !== undefined)
55
+ return this.#resume(input);
56
+ let agent;
57
+ try {
58
+ agent = this.managers.resolveCreateAgent(input.agent);
59
+ }
60
+ catch (error) {
61
+ throw new ManagementError(400, error.message);
62
+ }
63
+ const fingerprint = JSON.stringify([
64
+ agent,
65
+ "create",
66
+ input.projectDirectory,
67
+ input.taskName?.trim() || null,
68
+ input.workingDirectory ?? input.projectDirectory,
69
+ input.worktree,
70
+ input.yolo ?? false,
71
+ ]);
72
+ const duplicate = this.#duplicate(input.requestId, fingerprint);
73
+ if (duplicate)
74
+ return { ...duplicate };
75
+ const entry = this.#new("create", null);
76
+ entry.metadata.mode = "new";
77
+ entry.agent = agent;
78
+ this.#requests.set(input.requestId, { fingerprint, operationId: entry.metadata.operationId });
79
+ void this.#launch.create(entry, input);
80
+ return this.#project(entry);
81
+ }
82
+ #resume(input) {
83
+ if (Object.keys(input).some((key) => !["resumeSessionId", "yolo", "requestId"].includes(key)) ||
84
+ !input.resumeSessionId.trim())
85
+ throw new ManagementError(400, "Resume accepts only resumeSessionId, yolo and requestId");
86
+ const sessionId = input.resumeSessionId;
87
+ const fingerprint = JSON.stringify(["resume", sessionId, input.yolo ?? false]);
88
+ const duplicate = this.#duplicate(input.requestId, fingerprint);
89
+ if (duplicate)
90
+ return { ...duplicate };
91
+ if (this.#active.has(sessionId))
92
+ throw new ManagementError(409, "Another management operation is running for this session");
93
+ const entry = this.#new("create", sessionId);
94
+ entry.metadata.mode = "resume";
95
+ this.#active.set(sessionId, entry);
96
+ this.interaction.setBusy(sessionId, true);
97
+ this.#requests.set(input.requestId, { fingerprint, operationId: entry.metadata.operationId });
98
+ void this.#launch.resume(entry, input);
99
+ return this.#project(entry);
100
+ }
101
+ async assertSupported(type, sessionId) {
102
+ if (type !== "archive")
103
+ return;
104
+ const session = (await this.sessions.managementSnapshot()).byId.get(sessionId);
105
+ if (session && typeof this.managers.get(session.origin.agent).archive !== "function")
106
+ throw new ManagementError(409, "Operation is not supported by this session source", "operation_not_supported");
107
+ }
108
+ manage(type, sessionId) {
109
+ const active = this.#active.get(sessionId);
110
+ if (active) {
111
+ if (active.metadata.type !== type)
112
+ throw new ManagementError(409, "Another management operation is running for this session");
113
+ return this.#project(active);
114
+ }
115
+ const entry = this.#new(type, sessionId);
116
+ this.#active.set(sessionId, entry);
117
+ this.interaction.setBusy(sessionId, true);
118
+ this.#transition(entry, { kind: "manage", phase: "resolving" }, this.now() +
119
+ (type === "exit" ? (this.waitMs.exit ?? this.waitMs.archive) : this.waitMs[type]));
120
+ void this.#lifecycle.manage(entry, type, sessionId);
121
+ return this.#project(entry);
122
+ }
123
+ async get(id) {
124
+ this.#prune();
125
+ const operation = this.#results.get(id) ?? this.#lookup(id);
126
+ return operation ? { ...operation } : null;
127
+ }
128
+ async poll() {
129
+ if (this.#closed)
130
+ return;
131
+ this.#prune();
132
+ const checks = [];
133
+ for (const entry of this.#entries.values()) {
134
+ this.#timeout(entry);
135
+ if (!this.#pending(entry))
136
+ continue;
137
+ const context = entry.stage;
138
+ if (context.kind === "create" &&
139
+ context.phase === "observing" &&
140
+ this.now() - context.startedAt >= LAUNCH_ATTACH_DELAY_MS) {
141
+ this.#attach(entry, attachCommand(context.target.socketPath, `ahub-${entry.metadata.operationId}`));
142
+ }
143
+ if ((context.kind === "create" && context.phase === "observing") ||
144
+ context.kind === "exiting" ||
145
+ ((context.kind === "archive" || context.kind === "delete") &&
146
+ context.phase === "confirming")) {
147
+ checks.push(this.#observe(entry));
148
+ }
149
+ }
150
+ await Promise.all(checks);
151
+ }
152
+ close() {
153
+ clearInterval(this.#timer);
154
+ for (const entry of this.#entries.values())
155
+ this.#finish(entry, "unknown", "Server stopped tracking the operation");
156
+ this.#closed = true;
157
+ }
158
+ #observe(entry) {
159
+ if (entry.checking)
160
+ return entry.checking;
161
+ entry.checking = this.#check(entry).finally(() => {
162
+ entry.checking = undefined;
163
+ });
164
+ return entry.checking;
165
+ }
166
+ async #check(entry) {
167
+ if (!this.#pending(entry))
168
+ return;
169
+ try {
170
+ if (entry.stage.kind === "create")
171
+ await this.#launch.observe(entry);
172
+ else
173
+ await this.#lifecycle.observe(entry);
174
+ }
175
+ catch (error) {
176
+ // Read failures never prove disappearance or justify another command.
177
+ if (error instanceof AssociationError)
178
+ this.#finish(entry, "unknown", error.message);
179
+ }
180
+ this.#timeout(entry);
181
+ }
182
+ #transition(entry, context, deadline = entry.stage?.deadline ?? null) {
183
+ if (!this.#pending(entry))
184
+ return;
185
+ const previous = operationPhase(entry.stage);
186
+ entry.stage = { ...context, deadline };
187
+ if (context.kind === "create" && context.phase === "observing")
188
+ entry.metadata.target = context.target;
189
+ if (previous !== operationPhase(entry.stage))
190
+ entry.metadata.updatedAt = new Date(this.now()).toISOString();
191
+ }
192
+ #lookup(id) {
193
+ const entry = this.#entries.get(id);
194
+ return entry ? this.#project(entry) : undefined;
195
+ }
196
+ #project(entry) {
197
+ if (entry.result)
198
+ return { ...entry.result };
199
+ const context = entry.stage;
200
+ if (!context)
201
+ throw new Error("Pending operation has no execution stage");
202
+ return { ...entry.metadata, phase: operationPhase(context), status: "pending", error: null };
203
+ }
204
+ #attach(entry, command) {
205
+ if (!this.#pending(entry) || entry.metadata.attachCommand === command)
206
+ return;
207
+ entry.metadata.attachCommand = command;
208
+ entry.metadata.updatedAt = new Date(this.now()).toISOString();
209
+ }
210
+ #pending(entry) {
211
+ return !this.#closed && !entry.result && entry.stage !== null;
212
+ }
213
+ #timeout(entry) {
214
+ if (this.#pending(entry) &&
215
+ entry.stage.deadline !== null &&
216
+ this.now() >= entry.stage.deadline) {
217
+ this.#finish(entry, "unknown", entry.metadata.type === "create"
218
+ ? "No agent session detected. Tracking has stopped. Attach to tmux or refresh the session list to check the result; creating again may start another session."
219
+ : "Timed out; result is unknown. Refresh the session list before submitting again.");
220
+ }
221
+ }
222
+ #error(entry, error) {
223
+ if (error instanceof SessionLaunchError) {
224
+ this.#attach(entry, error.attachCommand);
225
+ if (error.target)
226
+ entry.metadata.target = error.target;
227
+ this.#finish(entry, error.outcome === "unknown" ? "unknown" : "failed", error.message);
228
+ return;
229
+ }
230
+ const timedOut = !!error &&
231
+ typeof error === "object" &&
232
+ (("killed" in error && error.killed) || ("code" in error && error.code === "timeout"));
233
+ this.#finish(entry, timedOut ? "unknown" : "failed", error instanceof Error ? error.message : String(error));
234
+ }
235
+ #finish(entry, status, error) {
236
+ if (entry.result)
237
+ return;
238
+ const context = entry.stage;
239
+ if (context?.kind === "create" && context.phase === "observing" && status === "unknown") {
240
+ entry.metadata.attachCommand = attachCommand(context.target.socketPath, `ahub-${entry.metadata.operationId}`);
241
+ }
242
+ entry.metadata.updatedAt = new Date(this.now()).toISOString();
243
+ entry.result = Object.freeze({ ...this.#project(entry), status, error });
244
+ entry.stage = null;
245
+ this.#results.set(entry.metadata.operationId, entry.result);
246
+ this.#entries.delete(entry.metadata.operationId);
247
+ const sessionId = entry.metadata.sessionId;
248
+ if (sessionId && this.#active.get(sessionId) === entry) {
249
+ this.#active.delete(sessionId);
250
+ this.interaction.setBusy(sessionId, false);
251
+ }
252
+ }
253
+ #new(type, sessionId) {
254
+ if (this.#closed)
255
+ throw new ManagementError(503, "Server stopped tracking operations");
256
+ this.#prune();
257
+ const timestamp = new Date(this.now()).toISOString();
258
+ const entry = {
259
+ metadata: {
260
+ attachCommand: null,
261
+ operationId: randomUUID(),
262
+ type,
263
+ sessionId,
264
+ createdAt: timestamp,
265
+ updatedAt: timestamp,
266
+ },
267
+ agent: this.managers.defaultAgent,
268
+ stage: type === "create"
269
+ ? { kind: "create", phase: "resolving", deadline: null }
270
+ : { kind: "manage", phase: "resolving", deadline: null },
271
+ };
272
+ this.#entries.set(entry.metadata.operationId, entry);
273
+ return entry;
274
+ }
275
+ #duplicate(requestId, fingerprint) {
276
+ if (!/^[A-Za-z0-9_-]{8,128}$/.test(requestId))
277
+ throw new ManagementError(400, "requestId must contain 8–128 letters, digits, underscores or hyphens");
278
+ this.#prune();
279
+ const record = this.#requests.get(requestId);
280
+ if (record && record.fingerprint !== fingerprint)
281
+ throw new ManagementError(409, "requestId was already used with different parameters");
282
+ return record
283
+ ? (this.#results.get(record.operationId) ?? this.#lookup(record.operationId))
284
+ : undefined;
285
+ }
286
+ #prune() {
287
+ const now = this.now();
288
+ for (const [id, operation] of this.#results) {
289
+ if (now - Date.parse(operation.updatedAt) >= 30 * 60_000)
290
+ this.#results.delete(id);
291
+ }
292
+ for (const [requestId, record] of this.#requests) {
293
+ if (!this.#entries.has(record.operationId) && !this.#results.has(record.operationId))
294
+ this.#requests.delete(requestId);
295
+ }
296
+ }
297
+ }
298
+ function operationPhase(context) {
299
+ if (context.kind === "exiting")
300
+ return context.task.phase;
301
+ if (context.kind === "archive")
302
+ return context.phase === "confirming" ? "confirming_archive" : "archiving";
303
+ if (context.kind === "delete")
304
+ return context.phase === "confirming" ? "confirming_delete" : "deleting";
305
+ return null;
306
+ }
307
+ //# sourceMappingURL=operation-service.js.map
@@ -0,0 +1,3 @@
1
+ export class AssociationError extends Error {
2
+ }
3
+ //# sourceMappingURL=operation-state.js.map
@@ -0,0 +1,50 @@
1
+ export class SessionExitService {
2
+ interaction;
3
+ now;
4
+ timing;
5
+ constructor(interaction, now = Date.now, timing = { eofDelayMs: 3_200, killDelayMs: 9_000 }) {
6
+ this.interaction = interaction;
7
+ this.now = now;
8
+ this.timing = timing;
9
+ }
10
+ start(sessionId, target, capability, assertActive) {
11
+ const steps = [
12
+ ...capability.exitSteps().map((step) => ({ ...step, keys: [...step.keys] })),
13
+ { keys: ["C-d"], delayMs: this.timing.eofDelayMs },
14
+ ];
15
+ let index = 0;
16
+ let due = this.now() + steps[0].delayMs;
17
+ let killed = false;
18
+ let phase = "sending_exit";
19
+ return {
20
+ get phase() {
21
+ return phase;
22
+ },
23
+ advance: async () => {
24
+ assertActive();
25
+ const state = await this.interaction.inspectTarget(target, sessionId);
26
+ assertActive();
27
+ if (state === "exited")
28
+ return true;
29
+ if (this.now() < due || killed)
30
+ return false;
31
+ const step = steps[index];
32
+ if (step) {
33
+ await this.interaction.sendExitKeys(target, sessionId, step.keys, assertActive);
34
+ assertActive();
35
+ index++;
36
+ phase = index < steps.length - 1 ? "sending_exit" : "waiting_exit";
37
+ due = this.now() + (steps[index]?.delayMs ?? this.timing.killDelayMs);
38
+ }
39
+ else {
40
+ // Claim before awaiting: an uncertain kill is never retried.
41
+ killed = true;
42
+ await this.interaction.killTarget(target, sessionId, assertActive);
43
+ assertActive();
44
+ }
45
+ return false;
46
+ },
47
+ };
48
+ }
49
+ }
50
+ //# sourceMappingURL=session-exit-service.js.map
@@ -0,0 +1,164 @@
1
+ import { AssociationError, } from "./operation-state.js";
2
+ /** Adapter launch and association checks; OperationService owns idempotency and occupancy. */
3
+ export class SessionLaunchExecutor {
4
+ sessions;
5
+ interaction;
6
+ managers;
7
+ bindings;
8
+ control;
9
+ now;
10
+ waitMs;
11
+ runtime;
12
+ constructor(sessions, interaction, managers, bindings, control, now, waitMs, runtime) {
13
+ this.sessions = sessions;
14
+ this.interaction = interaction;
15
+ this.managers = managers;
16
+ this.bindings = bindings;
17
+ this.control = control;
18
+ this.now = now;
19
+ this.waitMs = waitMs;
20
+ this.runtime = runtime;
21
+ }
22
+ async create(entry, input) {
23
+ try {
24
+ const manager = this.managers.get(entry.agent);
25
+ const project = await manager.resolveDirectory(input.projectDirectory);
26
+ const working = input.workingDirectory === undefined || input.workingDirectory === input.projectDirectory
27
+ ? project
28
+ : await manager.resolveDirectory(input.workingDirectory);
29
+ if (!this.control.pending(entry))
30
+ return;
31
+ this.control.transition(entry, { kind: "create", phase: "launching" });
32
+ const target = await manager.launch({
33
+ operationId: entry.metadata.operationId,
34
+ workingDirectory: working.directory,
35
+ worktree: input.worktree,
36
+ yolo: input.yolo ?? false,
37
+ projectDirectory: project.directory,
38
+ ...(input.taskName?.trim() ? { taskName: input.taskName.trim() } : {}),
39
+ });
40
+ if (!this.control.pending(entry))
41
+ return;
42
+ this.control.transition(entry, {
43
+ kind: "create",
44
+ phase: "observing",
45
+ project: project.directory,
46
+ target,
47
+ startedAt: this.now(),
48
+ }, this.now() + this.waitMs.create);
49
+ }
50
+ catch (error) {
51
+ this.control.error(entry, error);
52
+ }
53
+ }
54
+ async resume(entry, input) {
55
+ const sessionId = input.resumeSessionId;
56
+ try {
57
+ if (!this.runtime)
58
+ throw new Error("Resume is unavailable");
59
+ const snapshot = await this.sessions.managementSnapshot();
60
+ const session = snapshot.byId.get(sessionId);
61
+ if (!session)
62
+ throw new Error("Session not found");
63
+ const attempt = this.bindings.latest(session.id);
64
+ const replacement = this.bindings.replacement(session.id);
65
+ const capability = await this.runtime.evaluate(session);
66
+ if (!capability.resume.allowed)
67
+ throw new Error(capability.resume.reason);
68
+ entry.agent = session.origin.agent;
69
+ entry.resumeNativeId = session.sourceId ?? undefined;
70
+ const manager = this.managers.get(entry.agent);
71
+ const directory = capability.directory;
72
+ if (attempt !== this.bindings.latest(session.id) ||
73
+ replacement !== this.bindings.replacement(session.id))
74
+ throw new Error("Session binding changed during validation");
75
+ if (!this.control.pending(entry))
76
+ return;
77
+ this.control.transition(entry, { kind: "create", phase: "launching" });
78
+ const target = await manager.launch({
79
+ operationId: entry.metadata.operationId,
80
+ workingDirectory: directory,
81
+ worktree: false,
82
+ yolo: input.yolo ?? false,
83
+ resumeNativeId: session.sourceId,
84
+ projectDirectory: session.projectDirectory,
85
+ ...(session.taskName ? { taskName: session.taskName } : {}),
86
+ });
87
+ if (!this.control.pending(entry))
88
+ return;
89
+ this.control.transition(entry, {
90
+ kind: "create",
91
+ phase: "observing",
92
+ target,
93
+ project: session.projectDirectory ?? directory,
94
+ startedAt: this.now(),
95
+ }, this.now() + this.waitMs.create);
96
+ }
97
+ catch (error) {
98
+ this.control.error(entry, error);
99
+ }
100
+ }
101
+ async observe(entry) {
102
+ const snapshot = await this.sessions.managementSnapshot();
103
+ if (!this.control.pending(entry))
104
+ return;
105
+ const context = entry.stage;
106
+ if (context.kind !== "create" || context.phase !== "observing")
107
+ return;
108
+ const matches = snapshot.sessions.filter((session) => {
109
+ const attempt = this.bindings.latest(session.id);
110
+ return (session.origin.agent === entry.agent &&
111
+ attempt?.valid &&
112
+ attempt.operationId === entry.metadata.operationId);
113
+ });
114
+ if (matches.length > 1)
115
+ throw new AssociationError("Multiple sessions claimed this operation ID");
116
+ const match = matches[0];
117
+ if (!match)
118
+ return;
119
+ if (entry.metadata.mode === "resume" &&
120
+ (match.id !== entry.metadata.sessionId ||
121
+ match.archived ||
122
+ match.sourceId !== entry.resumeNativeId ||
123
+ !match.sourceId))
124
+ throw new AssociationError("Resume did not bind the original session");
125
+ const attempt = this.bindings.latest(match.id);
126
+ if (!attempt?.valid ||
127
+ attempt.socketPath !== context.target.socketPath ||
128
+ attempt.sessionName !== context.target.sessionName ||
129
+ match.projectDirectory !== context.project) {
130
+ throw new AssociationError("Binding does not match the launched target/project");
131
+ }
132
+ const binding = await this.interaction.describe(match.id);
133
+ if (!this.control.pending(entry))
134
+ return;
135
+ if (binding?.supported && binding.state === "unknown")
136
+ return;
137
+ if (!binding?.supported || binding.state !== "connected")
138
+ throw new AssociationError("The new session binding failed validation");
139
+ const matchesTarget = await this.interaction.matchesTarget(match.id, context.target);
140
+ if (!this.control.pending(entry))
141
+ return;
142
+ if (!matchesTarget)
143
+ throw new AssociationError("Binding does not match the launched target");
144
+ entry.metadata.sessionId = match.id;
145
+ if (this.managers.get(entry.agent).interruptAfterBinding) {
146
+ try {
147
+ await this.interaction.interruptLaunch(context.target, match.id, () => {
148
+ if (!this.control.pending(entry))
149
+ throw new Error("Launch operation is no longer active");
150
+ if (this.bindings.latest(match.id) !== attempt)
151
+ throw new Error("Launch binding changed before interrupt");
152
+ });
153
+ }
154
+ catch (error) {
155
+ this.control.finish(entry, "unknown", error instanceof Error ? error.message : String(error));
156
+ return;
157
+ }
158
+ if (!this.control.pending(entry))
159
+ return;
160
+ }
161
+ this.control.finish(entry, "succeeded", null);
162
+ }
163
+ }
164
+ //# sourceMappingURL=session-launch-executor.js.map
@@ -0,0 +1,158 @@
1
+ import { SessionExitService } from "./session-exit-service.js";
2
+ /** Exit, archive and delete execution, including confirmation of native effects. */
3
+ export class SessionLifecycleExecutor {
4
+ sessions;
5
+ interaction;
6
+ managers;
7
+ control;
8
+ now;
9
+ waitMs;
10
+ uploads;
11
+ #exit;
12
+ constructor(sessions, interaction, managers, control, now, waitMs, uploads) {
13
+ this.sessions = sessions;
14
+ this.interaction = interaction;
15
+ this.managers = managers;
16
+ this.control = control;
17
+ this.now = now;
18
+ this.waitMs = waitMs;
19
+ this.uploads = uploads;
20
+ this.#exit = new SessionExitService(interaction, now);
21
+ }
22
+ async manage(entry, type, sessionId) {
23
+ try {
24
+ const snapshot = await this.sessions.managementSnapshot();
25
+ if (!this.control.pending(entry))
26
+ return;
27
+ const session = snapshot.byId.get(sessionId);
28
+ if (!session) {
29
+ if (type === "delete") {
30
+ this.control.finish(entry, "succeeded", null);
31
+ this.#cleanup(sessionId);
32
+ return;
33
+ }
34
+ throw new Error("Session not found");
35
+ }
36
+ entry.agent = session.origin.agent;
37
+ const manager = this.managers.get(entry.agent);
38
+ if (type === "archive" && typeof manager.archive !== "function")
39
+ throw new Error("operation_not_supported: Operation is not supported by this session source");
40
+ if (type === "archive" && session.archived) {
41
+ this.control.finish(entry, "succeeded", null);
42
+ return;
43
+ }
44
+ if (type === "exit") {
45
+ await this.#startExit(entry, sessionId, null);
46
+ return;
47
+ }
48
+ const nativeId = session.sourceId;
49
+ if (!nativeId?.trim())
50
+ throw new Error("Session has no valid native identity");
51
+ await this.#startExit(entry, sessionId, { type, nativeId });
52
+ if (this.control.pending(entry))
53
+ await this.control.observe(entry);
54
+ }
55
+ catch (error) {
56
+ this.control.error(entry, error);
57
+ }
58
+ }
59
+ async observe(entry) {
60
+ if (!this.control.pending(entry))
61
+ return;
62
+ let context = entry.stage;
63
+ if (context.kind === "exiting") {
64
+ try {
65
+ // Refresh adapter bindings before observing or acting on the fixed target.
66
+ await this.sessions.managementSnapshot();
67
+ if (!this.control.pending(entry))
68
+ return;
69
+ const phase = context.task.phase;
70
+ const exited = await context.task.advance();
71
+ if (!this.control.pending(entry))
72
+ return;
73
+ if (phase !== context.task.phase)
74
+ entry.metadata.updatedAt = new Date(this.now()).toISOString();
75
+ if (!exited)
76
+ return;
77
+ if (context.next === null) {
78
+ this.control.finish(entry, "succeeded", null);
79
+ return;
80
+ }
81
+ await this.#nativeOperation(entry, context.next);
82
+ if (!this.control.pending(entry))
83
+ return;
84
+ }
85
+ catch (error) {
86
+ if (this.control.pending(entry))
87
+ this.control.finish(entry, "unknown", error instanceof Error ? error.message : String(error));
88
+ return;
89
+ }
90
+ }
91
+ const snapshot = await this.sessions.managementSnapshot();
92
+ if (!this.control.pending(entry))
93
+ return;
94
+ context = entry.stage;
95
+ if ((context.kind === "archive" || context.kind === "delete") &&
96
+ context.phase === "confirming") {
97
+ const matches = this.#matches(snapshot, entry, context.nativeId);
98
+ if ((context.kind === "archive" && matches.length === 1 && matches[0]?.archived) ||
99
+ (context.kind === "delete" && matches.length === 0)) {
100
+ this.control.finish(entry, "succeeded", null);
101
+ if (context.kind === "delete" && entry.metadata.sessionId)
102
+ this.#cleanup(entry.metadata.sessionId);
103
+ }
104
+ }
105
+ }
106
+ #cleanup(id) {
107
+ void Promise.resolve()
108
+ .then(() => this.uploads?.cleanup(id))
109
+ .catch((error) => {
110
+ console.error("Session attachment cleanup failed", { sessionId: id, error });
111
+ });
112
+ }
113
+ async #startExit(entry, sessionId, next) {
114
+ const target = await this.interaction.resolveTarget(sessionId);
115
+ if (!this.control.pending(entry))
116
+ return;
117
+ if (!target) {
118
+ if (next === null)
119
+ this.control.finish(entry, "succeeded", null);
120
+ else
121
+ await this.#nativeOperation(entry, next);
122
+ return;
123
+ }
124
+ const task = this.#exit.start(sessionId, target, this.managers.get(entry.agent), () => {
125
+ this.control.timeout(entry);
126
+ if (!this.control.pending(entry))
127
+ throw new Error("Exit operation is no longer active");
128
+ });
129
+ this.control.transition(entry, { kind: "exiting", task, next }, this.now() + (this.waitMs.exit ?? this.waitMs.archive));
130
+ await this.control.observe(entry);
131
+ }
132
+ async #nativeOperation(entry, { type, nativeId }) {
133
+ if (!this.control.pending(entry))
134
+ return;
135
+ this.control.transition(entry, type === "archive"
136
+ ? { kind: type, phase: "archiving", nativeId }
137
+ : { kind: type, phase: "deleting", nativeId }, this.now() + 25_000);
138
+ try {
139
+ const manager = this.managers.get(entry.agent);
140
+ if (type === "archive")
141
+ await manager.archive(nativeId);
142
+ else
143
+ await manager.delete(nativeId);
144
+ if (!this.control.pending(entry))
145
+ return;
146
+ this.control.transition(entry, { kind: type, phase: "confirming", nativeId }, this.now() + this.waitMs[type]);
147
+ }
148
+ catch (error) {
149
+ if (!this.control.pending(entry))
150
+ return;
151
+ this.control.error(entry, error);
152
+ }
153
+ }
154
+ #matches(snapshot, entry, nativeId) {
155
+ return snapshot.sessions.filter((session) => session.origin.agent === entry.agent && session.sourceId === nativeId);
156
+ }
157
+ }
158
+ //# sourceMappingURL=session-lifecycle-executor.js.map