@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,276 @@
1
+ import { normalizeMessage } from "./message.js";
2
+ import { paneBindings } from "./pane-binding-manager.js";
3
+ import { isCurrentObservation, observePaneBinding } from "./pane-observation.js";
4
+ import { TerminalActivityService } from "./terminal-activity-service.js";
5
+ import { TerminalPreviewService } from "./terminal-preview-service.js";
6
+ import { attachCommand } from "./tmux-launcher.js";
7
+ import { createTmuxPreviewListener } from "./tmux-preview-listener.js";
8
+ import { validTmuxSessionName } from "./tmux-target.js";
9
+ import { InteractionTransportError, } from "./transport.js";
10
+ export class SessionInteractionError extends Error {
11
+ code;
12
+ constructor(code, message) {
13
+ super(message);
14
+ this.code = code;
15
+ this.name = "SessionInteractionError";
16
+ }
17
+ }
18
+ export class SessionInteractionService {
19
+ sessions;
20
+ tmux;
21
+ runtime;
22
+ enabled;
23
+ bindings;
24
+ #busy = new Set();
25
+ #activity;
26
+ #previews;
27
+ #unsubscribe;
28
+ close() {
29
+ this.#unsubscribe();
30
+ this.#previews.close();
31
+ this.#activity.close();
32
+ }
33
+ constructor(sessions, tmux, options) {
34
+ this.sessions = sessions;
35
+ this.tmux = tmux;
36
+ const { enabled, bindings = paneBindings, listener = createTmuxPreviewListener(), runtime, } = options;
37
+ this.enabled = enabled;
38
+ this.bindings = bindings;
39
+ this.runtime = runtime;
40
+ this.#activity = new TerminalActivityService(listener);
41
+ this.#previews = new TerminalPreviewService((binding, assertOwner) => this.#perform(() => this.tmux.captureTerminal(binding, assertOwner)), listener);
42
+ this.#unsubscribe = bindings.subscribe(() => {
43
+ this.#previews.invalidate();
44
+ this.#activity.invalidate((id, binding) => bindings.owns(id, binding));
45
+ });
46
+ }
47
+ setBusy(sessionId, busy) {
48
+ if (busy)
49
+ this.#busy.add(sessionId);
50
+ else
51
+ this.#busy.delete(sessionId);
52
+ }
53
+ async resolveTarget(sessionId) {
54
+ const session = await this.sessions.getInteractionSession(sessionId);
55
+ if (!session)
56
+ throw new Error("Session not found");
57
+ const attempt = this.bindings.latest(sessionId);
58
+ if (this.bindings.replacement(sessionId) !== null)
59
+ return null;
60
+ if (!attempt?.valid)
61
+ return null;
62
+ const target = await this.tmux.resolveTarget(attempt);
63
+ return target && this.bindings.owns(sessionId, target) ? target : null;
64
+ }
65
+ async interruptLaunch(target, sessionId, assertActive) {
66
+ const { binding, interruptSteps } = await this.#prepare(sessionId);
67
+ if (binding.socketPath !== target.socketPath || binding.sessionName !== target.sessionName)
68
+ throw new Error("Launch binding changed before interrupt");
69
+ await this.tmux.sendKeys(target, interruptSteps(), () => {
70
+ assertActive();
71
+ this.#assertOwner(sessionId, target);
72
+ });
73
+ }
74
+ sendExitKeys(target, sessionId, keys, assertActive) {
75
+ return this.tmux.sendKeys(target, [{ keys, delayMs: 0 }], () => {
76
+ assertActive();
77
+ this.#assertOwner(sessionId, target);
78
+ });
79
+ }
80
+ killTarget(target, sessionId, assertActive) {
81
+ return this.tmux.killSession(target, () => {
82
+ assertActive();
83
+ this.#assertOwner(sessionId, target);
84
+ });
85
+ }
86
+ async inspectTarget(target, sessionId) {
87
+ this.#assertOwner(sessionId, target);
88
+ const state = await this.tmux.inspect(target);
89
+ this.#assertOwner(sessionId, target);
90
+ return state;
91
+ }
92
+ #assertAvailable(sessionId) {
93
+ if (this.#busy.has(sessionId))
94
+ throw new SessionInteractionError("session_busy", "Session management is in progress; input is temporarily unavailable");
95
+ }
96
+ async describe(sessionId) {
97
+ const session = await this.sessions.getInteractionSession(sessionId);
98
+ if (session === null)
99
+ return null;
100
+ return this.describeSnapshot(sessionId, session);
101
+ }
102
+ async describeSnapshot(sessionId, session) {
103
+ if (!this.#supported(session)) {
104
+ this.#activity.remove(sessionId);
105
+ return { supported: false, tmux: null, attachCommand: null };
106
+ }
107
+ const observed = session.observation ??
108
+ (await observePaneBinding(sessionId, this.bindings, (attempt) => this.#check(attempt)));
109
+ if (observed.state !== "alive" || observed.replacement !== null)
110
+ this.#activity.remove(sessionId);
111
+ if (observed.sessionId !== sessionId || !isCurrentObservation(observed, this.bindings)) {
112
+ this.#activity.remove(sessionId);
113
+ return response("unknown");
114
+ }
115
+ switch (observed.state) {
116
+ case "unbound":
117
+ return response("unbound");
118
+ case "invalid":
119
+ return response("disconnected");
120
+ case "unknown":
121
+ return response("unknown");
122
+ case "exited":
123
+ return response("disconnected");
124
+ case "alive": {
125
+ if (observed.replacement === null)
126
+ return {
127
+ ...response("connected", observed.target),
128
+ activity: this.#activity.read(sessionId, observed.target),
129
+ };
130
+ try {
131
+ const replacement = await this.sessions.getInteractionSession(observed.replacement);
132
+ if (!isCurrentObservation(observed, this.bindings))
133
+ return response("unknown");
134
+ return {
135
+ ...response("superseded"),
136
+ supersededBySessionId: replacement ? observed.replacement : null,
137
+ };
138
+ }
139
+ catch {
140
+ return response("unknown");
141
+ }
142
+ }
143
+ }
144
+ }
145
+ #check(attempt) {
146
+ const connection = this.runtime ? this.runtime.probe(attempt) : this.tmux.connect(attempt);
147
+ return connection.catch((error) => {
148
+ if (error instanceof InteractionTransportError && error.code === "disconnected")
149
+ return null;
150
+ throw error;
151
+ });
152
+ }
153
+ async matchesTarget(sessionId, target) {
154
+ const resolved = await this.#prepare(sessionId);
155
+ return (resolved.binding.socketPath === target.socketPath &&
156
+ resolved.binding.sessionName === target.sessionName);
157
+ }
158
+ async sendMessage(sessionId, message, requireIdle = false) {
159
+ message = normalizeMessage(message);
160
+ this.#assertAvailable(sessionId);
161
+ const { binding } = await this.#prepare(sessionId);
162
+ this.#assertAvailable(sessionId);
163
+ this.#assertInputAllowed(sessionId, binding, requireIdle);
164
+ await this.#perform(() => this.tmux.sendMessage(binding, message, this.#inputGuard(sessionId, binding, requireIdle)));
165
+ }
166
+ async handoffPaneBinding(sessionId) {
167
+ this.#assertAvailable(sessionId);
168
+ const { binding, bindingCommand } = await this.#prepare(sessionId);
169
+ this.#assertAvailable(sessionId);
170
+ await this.#perform(() => this.tmux.sendMessage(binding, bindingCommand, () => this.#assertInputAllowed(sessionId, binding)));
171
+ }
172
+ async sendKeys(sessionId, keys, requireIdle = false) {
173
+ this.#assertAvailable(sessionId);
174
+ const { binding, keyBindings, interruptSteps } = await this.#prepare(sessionId);
175
+ this.#assertAvailable(sessionId);
176
+ this.#assertInputAllowed(sessionId, binding, requireIdle);
177
+ const mapped = keys.flatMap((key) => key === "interrupt" ? interruptSteps() : [{ keys: [keyBindings[key]], delayMs: 0 }]);
178
+ await this.#perform(() => this.tmux.sendKeys(binding, mapped, this.#inputGuard(sessionId, binding, requireIdle)));
179
+ }
180
+ async captureTerminal(sessionId) {
181
+ this.#assertAvailable(sessionId);
182
+ const { binding } = await this.#prepare(sessionId);
183
+ this.#assertAvailable(sessionId);
184
+ return this.#previews.captureFresh(binding, () => {
185
+ this.#assertAvailable(sessionId);
186
+ this.#assertOwner(sessionId, binding);
187
+ });
188
+ }
189
+ async subscribePreview(sessionId, frame, fail, signal) {
190
+ const { binding } = await this.#prepare(sessionId);
191
+ if (signal.aborted)
192
+ throw new DOMException("Aborted", "AbortError");
193
+ return this.#previews.subscribe(binding, () => this.#assertOwner(sessionId, binding), frame, fail, signal);
194
+ }
195
+ async #prepare(sessionId) {
196
+ const session = await this.sessions.getInteractionSession(sessionId);
197
+ if (session === null) {
198
+ throw new SessionInteractionError("session_not_found", "Session not found");
199
+ }
200
+ if (!this.#supported(session)) {
201
+ throw new SessionInteractionError("interaction_not_supported", "Interaction is not supported for this session");
202
+ }
203
+ const interaction = session.interaction;
204
+ const attempt = this.bindings.latest(sessionId);
205
+ if (attempt === null || !attempt.valid) {
206
+ throw new SessionInteractionError("interaction_not_connected", "The session is not connected to a tmux pane");
207
+ }
208
+ this.#assertOwner(sessionId, attempt);
209
+ if (!attempt.socketPath.startsWith("/") || !validTmuxSessionName(attempt.sessionName))
210
+ throw new SessionInteractionError("interaction_not_connected", "Invalid tmux binding");
211
+ return {
212
+ binding: { socketPath: attempt.socketPath, sessionName: attempt.sessionName },
213
+ interruptSteps: () => interaction.interruptSteps(),
214
+ keyBindings: interaction.keyBindings,
215
+ bindingCommand: interaction.bindingCommand,
216
+ };
217
+ }
218
+ #inputGuard(sessionId, binding, requireIdle) {
219
+ let started = false;
220
+ return () => {
221
+ // Check idle at serialized transaction entry. Echo from our own paste or keys
222
+ // must not prevent Enter or the remaining keys; ownership stays checked throughout.
223
+ this.#assertInputAllowed(sessionId, binding, requireIdle && !started);
224
+ started = true;
225
+ };
226
+ }
227
+ #assertInputAllowed(sessionId, binding, requireIdle = false) {
228
+ this.#assertAvailable(sessionId);
229
+ this.#assertOwner(sessionId, binding);
230
+ if (requireIdle && this.#activity.read(sessionId, binding) !== "idle")
231
+ throw new SessionInteractionError("session_busy", "Session must be live and idle for input");
232
+ }
233
+ #assertOwner(sessionId, target) {
234
+ if (!this.bindings.owns(sessionId, target)) {
235
+ throw new SessionInteractionError("interaction_not_connected", "The pane binding has been superseded");
236
+ }
237
+ }
238
+ async #perform(operation) {
239
+ try {
240
+ return await operation();
241
+ }
242
+ catch (error) {
243
+ if (error instanceof InteractionTransportError) {
244
+ if (error.code === "disconnected") {
245
+ throw new SessionInteractionError("interaction_not_connected", "The tmux target is no longer connected");
246
+ }
247
+ if (error.code === "timeout") {
248
+ throw new SessionInteractionError("operation_result_unknown", "The tmux operation timed out; its result is unknown and was not retried");
249
+ }
250
+ if (error.code === "command_failed") {
251
+ throw new SessionInteractionError("interaction_failed", "tmux could not complete the interaction operation");
252
+ }
253
+ }
254
+ throw error;
255
+ }
256
+ }
257
+ #supported(session) {
258
+ return this.enabled && !session.archived && session.interaction !== null;
259
+ }
260
+ }
261
+ function response(state, binding) {
262
+ return {
263
+ supported: true,
264
+ tmux: binding
265
+ ? {
266
+ socketPath: binding.socketPath,
267
+ sessionName: binding.sessionName,
268
+ }
269
+ : null,
270
+ state,
271
+ activity: null,
272
+ supersededBySessionId: null,
273
+ attachCommand: binding ? attachCommand(binding.socketPath, binding.sessionName) : null,
274
+ };
275
+ }
276
+ //# sourceMappingURL=interaction-service.js.map
@@ -0,0 +1,19 @@
1
+ import { MAX_INTERACTION_MESSAGE_BYTES } from "../../shared/interaction.js";
2
+ export class MessageValidationError extends Error {
3
+ code = "invalid_message";
4
+ constructor(message) {
5
+ super(message);
6
+ }
7
+ }
8
+ export function normalizeMessage(message) {
9
+ const normalized = message.replace(/\r\n?/g, "\n");
10
+ const bytes = Buffer.byteLength(normalized, "utf8");
11
+ if (normalized.trim().length === 0) {
12
+ throw new MessageValidationError("message must not be blank");
13
+ }
14
+ if (bytes > MAX_INTERACTION_MESSAGE_BYTES) {
15
+ throw new MessageValidationError(`message must not exceed ${MAX_INTERACTION_MESSAGE_BYTES} UTF-8 bytes`);
16
+ }
17
+ return normalized;
18
+ }
19
+ //# sourceMappingURL=message.js.map
@@ -0,0 +1,62 @@
1
+ /** Retains only the latest session event and latest claim on each tmux target. */
2
+ export class PaneBindingManager {
3
+ #sessions = new Map();
4
+ #targets = new Map();
5
+ #listeners = new Set();
6
+ subscribe(listener) {
7
+ this.#listeners.add(listener);
8
+ return () => {
9
+ this.#listeners.delete(listener);
10
+ };
11
+ }
12
+ submit(event) {
13
+ const latest = this.#sessions.get(event.sessionId);
14
+ let changed = false;
15
+ if (!latest || compare(event, latest) > 0) {
16
+ this.#sessions.set(event.sessionId, event);
17
+ changed = true;
18
+ }
19
+ if (!event.attempt.valid) {
20
+ if (changed)
21
+ for (const listener of this.#listeners)
22
+ listener();
23
+ return;
24
+ }
25
+ const key = targetKey(event.attempt);
26
+ const owner = this.#targets.get(key);
27
+ if (!owner || compare(event, owner) > 0) {
28
+ this.#targets.set(key, event);
29
+ changed = true;
30
+ }
31
+ if (changed)
32
+ for (const listener of this.#listeners)
33
+ listener();
34
+ }
35
+ latest(sessionId) {
36
+ return this.#sessions.get(sessionId)?.attempt ?? null;
37
+ }
38
+ replacement(sessionId) {
39
+ const event = this.#sessions.get(sessionId);
40
+ if (!event?.attempt.valid)
41
+ return null;
42
+ const owner = this.#targets.get(targetKey(event.attempt));
43
+ return owner && compare(owner, event) !== 0 ? owner.sessionId : null;
44
+ }
45
+ owns(sessionId, target) {
46
+ const attempt = this.latest(sessionId);
47
+ return (!!attempt?.valid &&
48
+ targetKey(attempt) === targetKey(target) &&
49
+ this.replacement(sessionId) === null);
50
+ }
51
+ }
52
+ function targetKey(target) {
53
+ return JSON.stringify([target.socketPath, target.sessionName]);
54
+ }
55
+ function compare(left, right) {
56
+ return (left.time - right.time ||
57
+ (left.sessionId < right.sessionId ? -1 : left.sessionId > right.sessionId ? 1 : 0) ||
58
+ left.ordinal - right.ordinal ||
59
+ left.position - right.position);
60
+ }
61
+ export const paneBindings = new PaneBindingManager();
62
+ //# sourceMappingURL=pane-binding-manager.js.map
@@ -0,0 +1,31 @@
1
+ export function isCurrentObservation(observation, bindings) {
2
+ return (observation.attempt === bindings.latest(observation.sessionId) &&
3
+ observation.replacement === bindings.replacement(observation.sessionId));
4
+ }
5
+ export async function observePaneBinding(sessionId, bindings, probe) {
6
+ const attempt = bindings.latest(sessionId);
7
+ const replacement = bindings.replacement(sessionId);
8
+ const result = (state) => ({
9
+ sessionId,
10
+ attempt,
11
+ replacement,
12
+ state,
13
+ target: null,
14
+ });
15
+ if (!attempt)
16
+ return result("unbound");
17
+ if (!attempt.valid)
18
+ return result("invalid");
19
+ try {
20
+ const target = await probe(attempt);
21
+ const observation = target
22
+ ? { sessionId, attempt, replacement, state: "alive", target }
23
+ : result("exited");
24
+ return isCurrentObservation(observation, bindings) ? observation : result("unknown");
25
+ }
26
+ catch {
27
+ // A failed check is never evidence of pane death.
28
+ return result("unknown");
29
+ }
30
+ }
31
+ //# sourceMappingURL=pane-observation.js.map
@@ -0,0 +1,21 @@
1
+ /** Reconcile the read result with the later interaction observation. */
2
+ export function reconcileSessionState(value) {
3
+ if (value.session.status !== "archived" && value.interaction.supported) {
4
+ const connected = value.interaction.state === "connected";
5
+ value = {
6
+ ...value,
7
+ session: {
8
+ ...value.session,
9
+ status: connected ? "live" : "normal",
10
+ resume: connected || value.interaction.state === "unknown"
11
+ ? {
12
+ allowed: false,
13
+ reason: connected ? "Session is already live" : "Connection validation is unknown",
14
+ }
15
+ : value.session.resume,
16
+ },
17
+ };
18
+ }
19
+ return value;
20
+ }
21
+ //# sourceMappingURL=reconcile-session-state.js.map
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=runtime-capabilities.js.map
@@ -0,0 +1,86 @@
1
+ import { resolveResumeDirectory, } from "../management/session-manager.js";
2
+ import { paneBindings } from "./pane-binding-manager.js";
3
+ import { isCurrentObservation, observePaneBinding, } from "./pane-observation.js";
4
+ /** Shared, bounded observations. Errors never prove that a pane exited. */
5
+ export class SessionRuntime {
6
+ transport;
7
+ managers;
8
+ bindings;
9
+ #pending = new Map();
10
+ #waiting = [];
11
+ #activeSockets = new Map();
12
+ constructor(transport, managers, bindings = paneBindings) {
13
+ this.transport = transport;
14
+ this.managers = managers;
15
+ this.bindings = bindings;
16
+ }
17
+ probe(attempt) {
18
+ if (!attempt.valid)
19
+ return Promise.reject(new Error("Invalid binding"));
20
+ const key = JSON.stringify([attempt.socketPath, attempt.sessionName]);
21
+ const existing = this.#pending.get(key);
22
+ if (existing)
23
+ return existing;
24
+ const query = (async () => {
25
+ // Different sessions on one socket must reach the transport together so they
26
+ // can share one list-sessions query, even when there are more than eight sessions.
27
+ while (!this.#activeSockets.has(attempt.socketPath) && this.#activeSockets.size >= 8)
28
+ await new Promise((resolve) => this.#waiting.push(resolve));
29
+ this.#activeSockets.set(attempt.socketPath, (this.#activeSockets.get(attempt.socketPath) ?? 0) + 1);
30
+ try {
31
+ return await this.transport.resolveTarget(attempt);
32
+ }
33
+ finally {
34
+ const remaining = this.#activeSockets.get(attempt.socketPath) - 1;
35
+ if (remaining > 0)
36
+ this.#activeSockets.set(attempt.socketPath, remaining);
37
+ else {
38
+ this.#activeSockets.delete(attempt.socketPath);
39
+ this.#waiting.splice(0).forEach((resume) => resume());
40
+ }
41
+ }
42
+ })();
43
+ this.#pending.set(key, query);
44
+ void query.then(() => this.#pending.delete(key), () => this.#pending.delete(key));
45
+ return query;
46
+ }
47
+ observe(sessionId) {
48
+ return observePaneBinding(sessionId, this.bindings, (attempt) => this.probe(attempt));
49
+ }
50
+ isCurrent(observation) {
51
+ return isCurrentObservation(observation, this.bindings);
52
+ }
53
+ async evaluate(session, detail = true, observation) {
54
+ let status = session.archived ? "archived" : "normal";
55
+ const deny = (reason) => ({ status, resume: { allowed: false, reason } });
56
+ if (session.archived)
57
+ return deny("Archived sessions cannot be resumed");
58
+ const observed = observation ?? (await this.observe(session.id));
59
+ if (observed.sessionId !== session.id || !this.isCurrent(observed))
60
+ return deny("Pane binding changed during validation");
61
+ if (observed.state === "invalid")
62
+ return deny("The latest pane binding is invalid");
63
+ if (observed.state === "unknown")
64
+ return deny("Runtime or working directory validation failed");
65
+ try {
66
+ if (observed.state === "alive" && observed.replacement === null)
67
+ status = "live";
68
+ if (!detail)
69
+ return deny("Resume validation required");
70
+ if (status === "live")
71
+ return deny("Session is already live");
72
+ const manager = this.managers.get(session.origin.agent);
73
+ if (!session.sourceId?.trim() || !manager.isValidNativeId(session.sourceId))
74
+ return deny("Session has no valid native identity");
75
+ const resolved = await resolveResumeDirectory(manager, session);
76
+ if (!this.isCurrent(observed))
77
+ return deny("Pane binding changed during validation");
78
+ return { status, resume: { allowed: true, reason: null }, directory: resolved.directory };
79
+ }
80
+ catch {
81
+ status = "normal";
82
+ return deny("Runtime or working directory validation failed");
83
+ }
84
+ }
85
+ }
86
+ //# sourceMappingURL=session-runtime.js.map
@@ -0,0 +1,81 @@
1
+ const WINDOW_MS = 5000;
2
+ const LEASE_MS = 30 * 60_000;
3
+ /** Output activity is independent of terminal captures and connection health. */
4
+ export class TerminalActivityService {
5
+ listen;
6
+ now;
7
+ #observations = new Map();
8
+ #timer;
9
+ #closed = false;
10
+ constructor(listen, now = () => performance.now()) {
11
+ this.listen = listen;
12
+ this.now = now;
13
+ this.#timer = setInterval(() => this.invalidate(() => true), 1000);
14
+ this.#timer.unref();
15
+ }
16
+ read(sessionId, binding) {
17
+ if (this.#closed)
18
+ return "unknown";
19
+ let observation = this.#observations.get(sessionId);
20
+ if (observation &&
21
+ (observation.failed ||
22
+ observation.expiresAt <= this.now() ||
23
+ observation.binding.socketPath !== binding.socketPath ||
24
+ observation.binding.sessionName !== binding.sessionName)) {
25
+ this.remove(sessionId);
26
+ observation = undefined;
27
+ }
28
+ if (!observation) {
29
+ observation = { binding, expiresAt: this.now() + LEASE_MS, failed: false };
30
+ this.#observations.set(sessionId, observation);
31
+ this.#subscribe(observation);
32
+ }
33
+ observation.expiresAt = this.now() + LEASE_MS;
34
+ if (observation.failed || observation.readyAt === undefined)
35
+ return "unknown";
36
+ if (observation.outputAt !== undefined && this.now() - observation.outputAt < WINDOW_MS)
37
+ return "working";
38
+ return this.now() - observation.readyAt >= WINDOW_MS ? "idle" : "unknown";
39
+ }
40
+ #subscribe(observation) {
41
+ const { binding } = observation;
42
+ const fail = () => {
43
+ observation.failed = true;
44
+ observation.readyAt = undefined;
45
+ observation.outputAt = undefined;
46
+ observation.subscription?.close();
47
+ };
48
+ try {
49
+ observation.subscription = this.listen(binding, (output) => {
50
+ if (!observation.failed && output)
51
+ observation.outputAt = this.now();
52
+ }, fail);
53
+ if (observation.failed)
54
+ observation.subscription.close();
55
+ void observation.subscription.ready.then(() => {
56
+ if (!observation.failed)
57
+ observation.readyAt = this.now();
58
+ }, fail);
59
+ }
60
+ catch {
61
+ fail();
62
+ }
63
+ }
64
+ remove(sessionId) {
65
+ const observation = this.#observations.get(sessionId);
66
+ this.#observations.delete(sessionId);
67
+ observation?.subscription?.close();
68
+ }
69
+ invalidate(owns) {
70
+ for (const [id, observation] of this.#observations)
71
+ if (observation.expiresAt <= this.now() || !owns(id, observation.binding))
72
+ this.remove(id);
73
+ }
74
+ close() {
75
+ this.#closed = true;
76
+ clearInterval(this.#timer);
77
+ for (const id of this.#observations.keys())
78
+ this.remove(id);
79
+ }
80
+ }
81
+ //# sourceMappingURL=terminal-activity-service.js.map
@@ -0,0 +1,71 @@
1
+ /** Bounded Myers search. Trace stores only frontiers, never historical snapshots. */
2
+ export function diffLines(before, after) {
3
+ const a = before.split("\n");
4
+ const b = after.split("\n");
5
+ let work = 0;
6
+ let frontier = new Map([[1, 0]]);
7
+ const trace = [];
8
+ for (let d = 0; d <= 256; d++) {
9
+ trace.push(frontier);
10
+ const next = new Map();
11
+ for (let k = -d; k <= d; k += 2) {
12
+ if (++work > 1_000_000)
13
+ return null;
14
+ let x = k === -d || (k !== d && (frontier.get(k - 1) ?? -1) < (frontier.get(k + 1) ?? -1))
15
+ ? (frontier.get(k + 1) ?? 0)
16
+ : (frontier.get(k - 1) ?? 0) + 1;
17
+ let y = x - k;
18
+ while (x < a.length && y < b.length) {
19
+ if (++work > 1_000_000)
20
+ return null;
21
+ if (a[x] !== b[y])
22
+ break;
23
+ x++;
24
+ y++;
25
+ }
26
+ next.set(k, x);
27
+ if (x >= a.length && y >= b.length) {
28
+ const edits = [];
29
+ for (let depth = d; depth > 0; depth--) {
30
+ const previous = trace[depth];
31
+ const diagonal = x - y;
32
+ const pk = diagonal === -depth ||
33
+ (diagonal !== depth &&
34
+ (previous.get(diagonal - 1) ?? -1) < (previous.get(diagonal + 1) ?? -1))
35
+ ? diagonal + 1
36
+ : diagonal - 1;
37
+ const px = previous.get(pk);
38
+ const py = px - pk;
39
+ while (x > px && y > py) {
40
+ x--;
41
+ y--;
42
+ }
43
+ if (x === px) {
44
+ y--;
45
+ edits.push({ x, line: b[y] });
46
+ }
47
+ else {
48
+ x--;
49
+ edits.push({ x });
50
+ }
51
+ }
52
+ const operations = [];
53
+ for (const edit of edits.reverse()) {
54
+ let op = operations.at(-1);
55
+ if (!op || edit.x !== op.startLine + op.deleteCount) {
56
+ op = { startLine: edit.x, deleteCount: 0, lines: [] };
57
+ operations.push(op);
58
+ }
59
+ if (edit.line === undefined)
60
+ op.deleteCount++;
61
+ else
62
+ op.lines.push(edit.line);
63
+ }
64
+ return operations.reverse();
65
+ }
66
+ }
67
+ frontier = next;
68
+ }
69
+ return null;
70
+ }
71
+ //# sourceMappingURL=terminal-preview-diff.js.map