@someonegg/codex-hub 0.1.2 → 0.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +4 -2
- package/dist/client/apple-touch-icon.png +0 -0
- package/dist/client/assets/{index-Ajj_Djqn.js → index-By9A7vLA.js} +3 -3
- package/dist/client/assets/index-Cai3CWfF.css +1 -0
- package/dist/client/favicon.ico +0 -0
- package/dist/client/favicon.svg +8 -0
- package/dist/client/icons/icon-192.png +0 -0
- package/dist/client/icons/icon-512.png +0 -0
- package/dist/client/icons/icon-maskable-512.png +0 -0
- package/dist/client/index.html +9 -3
- package/dist/client/manifest.webmanifest +21 -0
- package/dist/server/server/adapters/codex/interaction-parser.js +9 -1
- package/dist/server/server/application/session-read-service.js +17 -12
- package/dist/server/server/application/session-response.js +11 -0
- package/dist/server/server/http/create-server.js +3 -0
- package/dist/server/server/http/session-interaction-routes.js +1 -1
- package/dist/server/server/http/session-read-routes.js +3 -17
- package/dist/server/server/http/terminal-preview-websocket.js +1 -1
- package/dist/server/server/interaction/interaction-service.js +53 -47
- package/dist/server/server/interaction/pane-observation.js +31 -0
- package/dist/server/server/interaction/session-runtime.js +17 -12
- package/dist/server/server/interaction/terminal-activity-service.js +82 -0
- package/dist/server/server/interaction/tmux-service.js +9 -4
- package/dist/server/server/management/operation-service.js +42 -292
- package/dist/server/server/management/operation-state.js +3 -0
- package/dist/server/server/management/session-launch-executor.js +174 -0
- package/dist/server/server/management/session-lifecycle-executor.js +151 -0
- package/dist/server/server/updates/session-updates-service.js +3 -14
- package/package.json +1 -1
- package/dist/client/assets/index-Bda69Igs.css +0 -1
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { resolveResumeDirectory, } from "../management/session-manager.js";
|
|
2
2
|
import { paneBindings } from "./pane-binding-manager.js";
|
|
3
|
+
import { isCurrentObservation, observePaneBinding, } from "./pane-observation.js";
|
|
3
4
|
/** Shared, bounded observations. Errors never prove that a pane exited. */
|
|
4
5
|
export class SessionRuntime {
|
|
5
6
|
transport;
|
|
@@ -43,23 +44,28 @@ export class SessionRuntime {
|
|
|
43
44
|
void query.then(() => this.#pending.delete(key), () => this.#pending.delete(key));
|
|
44
45
|
return query;
|
|
45
46
|
}
|
|
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, sessions, detail = true, observation) {
|
|
47
54
|
let status = session.archived ? "archived" : "normal";
|
|
48
55
|
const deny = (reason) => ({ status, resume: { allowed: false, reason } });
|
|
49
56
|
if (session.archived)
|
|
50
57
|
return deny("Archived sessions cannot be resumed");
|
|
51
|
-
const
|
|
52
|
-
if (!
|
|
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 === "unbound")
|
|
53
62
|
return deny("No pane binding has been recorded");
|
|
54
|
-
if (
|
|
63
|
+
if (observed.state === "invalid")
|
|
55
64
|
return deny("The latest pane binding is invalid");
|
|
56
|
-
|
|
65
|
+
if (observed.state === "unknown")
|
|
66
|
+
return deny("Runtime or working directory validation failed");
|
|
57
67
|
try {
|
|
58
|
-
|
|
59
|
-
if (attempt !== this.bindings.latest(session.id) ||
|
|
60
|
-
replacement !== this.bindings.replacement(session.id))
|
|
61
|
-
return deny("Pane binding changed during validation");
|
|
62
|
-
if (target && replacement === null)
|
|
68
|
+
if (observed.state === "alive" && observed.replacement === null)
|
|
63
69
|
status = "live";
|
|
64
70
|
if (!detail)
|
|
65
71
|
return deny("Resume validation required");
|
|
@@ -72,8 +78,7 @@ export class SessionRuntime {
|
|
|
72
78
|
item.sourceId === session.sourceId).length !== 1)
|
|
73
79
|
return deny("Native session identity is ambiguous");
|
|
74
80
|
await resolveResumeDirectory(manager, session);
|
|
75
|
-
if (
|
|
76
|
-
replacement !== this.bindings.replacement(session.id))
|
|
81
|
+
if (!this.isCurrent(observed))
|
|
77
82
|
return deny("Pane binding changed during validation");
|
|
78
83
|
return { status, resume: { allowed: true, reason: null } };
|
|
79
84
|
}
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
const WINDOW_MS = 5000;
|
|
2
|
+
const LEASE_MS = 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.paneId !== binding.paneId ||
|
|
25
|
+
observation.binding.sessionName !== binding.sessionName)) {
|
|
26
|
+
this.remove(sessionId);
|
|
27
|
+
observation = undefined;
|
|
28
|
+
}
|
|
29
|
+
if (!observation) {
|
|
30
|
+
observation = { binding, expiresAt: this.now() + LEASE_MS, failed: false };
|
|
31
|
+
this.#observations.set(sessionId, observation);
|
|
32
|
+
this.#subscribe(observation);
|
|
33
|
+
}
|
|
34
|
+
observation.expiresAt = this.now() + LEASE_MS;
|
|
35
|
+
if (observation.failed || observation.readyAt === undefined)
|
|
36
|
+
return "unknown";
|
|
37
|
+
if (observation.outputAt !== undefined && this.now() - observation.outputAt < WINDOW_MS)
|
|
38
|
+
return "working";
|
|
39
|
+
return this.now() - observation.readyAt >= WINDOW_MS ? "idle" : "unknown";
|
|
40
|
+
}
|
|
41
|
+
#subscribe(observation) {
|
|
42
|
+
const { binding } = observation;
|
|
43
|
+
const fail = () => {
|
|
44
|
+
observation.failed = true;
|
|
45
|
+
observation.readyAt = undefined;
|
|
46
|
+
observation.outputAt = undefined;
|
|
47
|
+
observation.subscription?.close();
|
|
48
|
+
};
|
|
49
|
+
try {
|
|
50
|
+
observation.subscription = this.listen(binding, (pane) => {
|
|
51
|
+
if (!observation.failed && pane === binding.paneId)
|
|
52
|
+
observation.outputAt = this.now();
|
|
53
|
+
}, fail);
|
|
54
|
+
if (observation.failed)
|
|
55
|
+
observation.subscription.close();
|
|
56
|
+
void observation.subscription.ready.then(() => {
|
|
57
|
+
if (!observation.failed)
|
|
58
|
+
observation.readyAt = this.now();
|
|
59
|
+
}, fail);
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
fail();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
remove(sessionId) {
|
|
66
|
+
const observation = this.#observations.get(sessionId);
|
|
67
|
+
this.#observations.delete(sessionId);
|
|
68
|
+
observation?.subscription?.close();
|
|
69
|
+
}
|
|
70
|
+
invalidate(owns) {
|
|
71
|
+
for (const [id, observation] of this.#observations)
|
|
72
|
+
if (observation.expiresAt <= this.now() || !owns(id, observation.binding))
|
|
73
|
+
this.remove(id);
|
|
74
|
+
}
|
|
75
|
+
close() {
|
|
76
|
+
this.#closed = true;
|
|
77
|
+
clearInterval(this.#timer);
|
|
78
|
+
for (const id of this.#observations.keys())
|
|
79
|
+
this.remove(id);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
//# sourceMappingURL=terminal-activity-service.js.map
|
|
@@ -2,6 +2,7 @@ import { spawn } from "node:child_process";
|
|
|
2
2
|
import { randomUUID } from "node:crypto";
|
|
3
3
|
import { lstat } from "node:fs/promises";
|
|
4
4
|
import { createConnection } from "node:net";
|
|
5
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
5
6
|
import { normalizeMessage } from "./message.js";
|
|
6
7
|
import { InteractionTransportError, } from "./transport.js";
|
|
7
8
|
export const MAX_TERMINAL_PREVIEW_BYTES = 256_000;
|
|
@@ -117,11 +118,15 @@ export class TmuxInteractionService {
|
|
|
117
118
|
}
|
|
118
119
|
});
|
|
119
120
|
}
|
|
120
|
-
sendKeys(binding,
|
|
121
|
+
sendKeys(binding, steps, assertAvailable = () => { }) {
|
|
121
122
|
return this.#serialize(binding, async () => {
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
123
|
+
for (const step of steps) {
|
|
124
|
+
if (step.delayMs > 0)
|
|
125
|
+
await delay(step.delayMs);
|
|
126
|
+
await this.#revalidate(binding);
|
|
127
|
+
assertAvailable();
|
|
128
|
+
await this.#run(binding, ["send-keys", "-t", binding.paneId, ...step.keys]);
|
|
129
|
+
}
|
|
125
130
|
});
|
|
126
131
|
}
|
|
127
132
|
captureTerminal(binding, assertAvailable = () => { }) {
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { randomUUID } from "node:crypto";
|
|
2
2
|
import { paneBindings } from "../interaction/pane-binding-manager.js";
|
|
3
3
|
import { attachCommand } from "../interaction/tmux-launcher.js";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
}
|
|
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 { SessionLaunchError } from "./session-manager.js";
|
|
8
8
|
export class ManagementError extends Error {
|
|
9
9
|
status;
|
|
10
10
|
constructor(status, message) {
|
|
@@ -13,34 +13,38 @@ export class ManagementError extends Error {
|
|
|
13
13
|
}
|
|
14
14
|
}
|
|
15
15
|
export class OperationService {
|
|
16
|
-
sessions;
|
|
17
16
|
interaction;
|
|
18
17
|
managers;
|
|
19
18
|
now;
|
|
20
19
|
waitMs;
|
|
21
|
-
bindings;
|
|
22
|
-
runtime;
|
|
23
20
|
#entries = new Map();
|
|
24
21
|
#requests = new Map();
|
|
25
22
|
#active = new Map();
|
|
26
23
|
#results = new Map();
|
|
27
24
|
#timer;
|
|
28
25
|
#closed = false;
|
|
29
|
-
#
|
|
26
|
+
#launch;
|
|
27
|
+
#lifecycle;
|
|
30
28
|
constructor(sessions, interaction, managers, now = Date.now, waitMs = {
|
|
31
29
|
create: 300_000,
|
|
32
30
|
exit: 30_000,
|
|
33
31
|
archive: 30_000,
|
|
34
32
|
delete: 30_000,
|
|
35
33
|
}, bindings = paneBindings, runtime) {
|
|
36
|
-
this.sessions = sessions;
|
|
37
34
|
this.interaction = interaction;
|
|
38
35
|
this.managers = managers;
|
|
39
36
|
this.now = now;
|
|
40
37
|
this.waitMs = waitMs;
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
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);
|
|
44
48
|
this.#timer = setInterval(() => {
|
|
45
49
|
void this.poll();
|
|
46
50
|
}, 1_000);
|
|
@@ -70,34 +74,7 @@ export class OperationService {
|
|
|
70
74
|
entry.metadata.mode = "new";
|
|
71
75
|
entry.sourceInstanceId = sourceInstanceId;
|
|
72
76
|
this.#requests.set(input.requestId, { fingerprint, operationId: entry.metadata.operationId });
|
|
73
|
-
void (
|
|
74
|
-
try {
|
|
75
|
-
const manager = this.managers.get(entry.sourceInstanceId);
|
|
76
|
-
const resolved = await manager.resolveDirectory(input.directory);
|
|
77
|
-
if (!this.#pending(entry))
|
|
78
|
-
return;
|
|
79
|
-
this.#transition(entry, { kind: "create", phase: "launching" });
|
|
80
|
-
const target = await manager.launch({
|
|
81
|
-
operationId: entry.metadata.operationId,
|
|
82
|
-
directory: resolved.directory,
|
|
83
|
-
worktree: input.worktree,
|
|
84
|
-
yolo: input.yolo ?? false,
|
|
85
|
-
projectDirectory: resolved.directory,
|
|
86
|
-
});
|
|
87
|
-
if (!this.#pending(entry))
|
|
88
|
-
return;
|
|
89
|
-
this.#transition(entry, {
|
|
90
|
-
kind: "create",
|
|
91
|
-
phase: "observing",
|
|
92
|
-
project: resolved.directory,
|
|
93
|
-
target,
|
|
94
|
-
startedAt: this.now(),
|
|
95
|
-
}, this.now() + this.waitMs.create);
|
|
96
|
-
}
|
|
97
|
-
catch (error) {
|
|
98
|
-
this.#error(entry, error);
|
|
99
|
-
}
|
|
100
|
-
})();
|
|
77
|
+
void this.#launch.create(entry, input);
|
|
101
78
|
return this.#project(entry);
|
|
102
79
|
}
|
|
103
80
|
#resume(input) {
|
|
@@ -116,77 +93,7 @@ export class OperationService {
|
|
|
116
93
|
this.#active.set(sessionId, entry);
|
|
117
94
|
this.interaction.setBusy(sessionId, true);
|
|
118
95
|
this.#requests.set(input.requestId, { fingerprint, operationId: entry.metadata.operationId });
|
|
119
|
-
void (
|
|
120
|
-
try {
|
|
121
|
-
if (!this.runtime)
|
|
122
|
-
throw new Error("Resume is unavailable");
|
|
123
|
-
let snapshot = await this.sessions.managementSnapshot();
|
|
124
|
-
let session = snapshot.sessions.find((item) => item.id === sessionId);
|
|
125
|
-
if (!session)
|
|
126
|
-
throw new Error("Session not found");
|
|
127
|
-
let capability = await this.runtime.evaluate(session, snapshot.sessions);
|
|
128
|
-
if (!capability.resume.allowed)
|
|
129
|
-
throw new Error(capability.resume.reason);
|
|
130
|
-
entry.sourceInstanceId = session.origin.sourceInstanceId;
|
|
131
|
-
entry.resumeNativeId = session.sourceId ?? undefined;
|
|
132
|
-
const manager = this.managers.get(entry.sourceInstanceId);
|
|
133
|
-
const attempt = this.bindings.latest(session.id);
|
|
134
|
-
const replacement = this.bindings.replacement(session.id);
|
|
135
|
-
await resolveResumeDirectory(manager, session);
|
|
136
|
-
// Refresh the source after asynchronous checks, then revalidate immediately before launch.
|
|
137
|
-
snapshot = await this.sessions.managementSnapshot();
|
|
138
|
-
const latest = snapshot.sessions.find((item) => item.id === sessionId);
|
|
139
|
-
if (!latest ||
|
|
140
|
-
latest.sourceId !== session.sourceId ||
|
|
141
|
-
latest.origin.sourceInstanceId !== entry.sourceInstanceId ||
|
|
142
|
-
latest.workingDirectory !== session.workingDirectory ||
|
|
143
|
-
latest.projectDirectory !== session.projectDirectory ||
|
|
144
|
-
attempt !== this.bindings.latest(session.id) ||
|
|
145
|
-
replacement !== this.bindings.replacement(session.id))
|
|
146
|
-
throw new Error("Session identity or directory changed during validation");
|
|
147
|
-
session = latest;
|
|
148
|
-
capability = await this.runtime.evaluate(session, snapshot.sessions);
|
|
149
|
-
if (!capability.resume.allowed)
|
|
150
|
-
throw new Error(capability.resume.reason);
|
|
151
|
-
const resolved = await resolveResumeDirectory(manager, session);
|
|
152
|
-
const finalSnapshot = await this.sessions.managementSnapshot();
|
|
153
|
-
const finalSession = finalSnapshot.sessions.find((item) => item.id === sessionId);
|
|
154
|
-
if (!finalSession ||
|
|
155
|
-
finalSession.sourceId !== session.sourceId ||
|
|
156
|
-
finalSession.origin.sourceInstanceId !== session.origin.sourceInstanceId ||
|
|
157
|
-
finalSession.archived ||
|
|
158
|
-
finalSession.workingDirectory !== session.workingDirectory ||
|
|
159
|
-
finalSession.projectDirectory !== session.projectDirectory ||
|
|
160
|
-
finalSnapshot.sessions.filter((item) => item.sourceId === session.sourceId &&
|
|
161
|
-
item.origin.sourceInstanceId === session.origin.sourceInstanceId).length !== 1 ||
|
|
162
|
-
attempt !== this.bindings.latest(session.id) ||
|
|
163
|
-
replacement !== this.bindings.replacement(session.id))
|
|
164
|
-
throw new Error("Session identity, binding or directory changed during validation");
|
|
165
|
-
if (!this.#pending(entry))
|
|
166
|
-
return;
|
|
167
|
-
this.#transition(entry, { kind: "create", phase: "launching" });
|
|
168
|
-
const target = await manager.launch({
|
|
169
|
-
operationId: entry.metadata.operationId,
|
|
170
|
-
directory: resolved.directory,
|
|
171
|
-
worktree: false,
|
|
172
|
-
yolo: input.yolo ?? false,
|
|
173
|
-
resumeNativeId: session.sourceId,
|
|
174
|
-
projectDirectory: session.projectDirectory,
|
|
175
|
-
});
|
|
176
|
-
if (!this.#pending(entry))
|
|
177
|
-
return;
|
|
178
|
-
this.#transition(entry, {
|
|
179
|
-
kind: "create",
|
|
180
|
-
phase: "observing",
|
|
181
|
-
target,
|
|
182
|
-
project: session.projectDirectory ?? resolved.directory,
|
|
183
|
-
startedAt: this.now(),
|
|
184
|
-
}, this.now() + this.waitMs.create);
|
|
185
|
-
}
|
|
186
|
-
catch (error) {
|
|
187
|
-
this.#error(entry, error);
|
|
188
|
-
}
|
|
189
|
-
})();
|
|
96
|
+
void this.#launch.resume(entry, input);
|
|
190
97
|
return this.#project(entry);
|
|
191
98
|
}
|
|
192
99
|
manage(type, sessionId) {
|
|
@@ -201,54 +108,7 @@ export class OperationService {
|
|
|
201
108
|
this.interaction.setBusy(sessionId, true);
|
|
202
109
|
this.#transition(entry, { kind: "manage", phase: "resolving" }, this.now() +
|
|
203
110
|
(type === "exit" ? (this.waitMs.exit ?? this.waitMs.archive) : this.waitMs[type]));
|
|
204
|
-
void (
|
|
205
|
-
try {
|
|
206
|
-
const snapshot = await this.sessions.managementSnapshot();
|
|
207
|
-
if (!this.#pending(entry))
|
|
208
|
-
return;
|
|
209
|
-
const session = snapshot.sessions.find((item) => item.id === sessionId);
|
|
210
|
-
if (!session) {
|
|
211
|
-
if (type === "delete") {
|
|
212
|
-
this.#finish(entry, "succeeded", null);
|
|
213
|
-
return;
|
|
214
|
-
}
|
|
215
|
-
throw new Error("Session not found");
|
|
216
|
-
}
|
|
217
|
-
entry.sourceInstanceId = session.origin.sourceInstanceId;
|
|
218
|
-
const manager = this.managers.get(entry.sourceInstanceId);
|
|
219
|
-
if (type === "archive" && session.archived) {
|
|
220
|
-
this.#finish(entry, "succeeded", null);
|
|
221
|
-
return;
|
|
222
|
-
}
|
|
223
|
-
if (type === "exit") {
|
|
224
|
-
await this.#startExit(entry, sessionId, null);
|
|
225
|
-
return;
|
|
226
|
-
}
|
|
227
|
-
const nativeId = session.sourceId;
|
|
228
|
-
if (!nativeId?.trim())
|
|
229
|
-
throw new Error("Session has no valid native identity");
|
|
230
|
-
if (this.#matches(snapshot, entry, nativeId).length !== 1)
|
|
231
|
-
throw new Error("Native session identity is ambiguous");
|
|
232
|
-
if (type === "delete") {
|
|
233
|
-
if (!session.archived)
|
|
234
|
-
throw new Error("Only archived sessions can be deleted");
|
|
235
|
-
this.#transition(entry, { kind: "delete", phase: "deleting", nativeId });
|
|
236
|
-
await manager.delete(nativeId);
|
|
237
|
-
if (!this.#pending(entry))
|
|
238
|
-
return;
|
|
239
|
-
this.#transition(entry, { kind: "delete", phase: "confirming", nativeId });
|
|
240
|
-
}
|
|
241
|
-
else {
|
|
242
|
-
this.#transition(entry, { kind: "archive", phase: "resolving_target", nativeId });
|
|
243
|
-
await this.#startExit(entry, sessionId, nativeId);
|
|
244
|
-
}
|
|
245
|
-
if (this.#pending(entry))
|
|
246
|
-
await this.#observe(entry);
|
|
247
|
-
}
|
|
248
|
-
catch (error) {
|
|
249
|
-
this.#error(entry, error);
|
|
250
|
-
}
|
|
251
|
-
})();
|
|
111
|
+
void this.#lifecycle.manage(entry, type, sessionId);
|
|
252
112
|
return this.#project(entry);
|
|
253
113
|
}
|
|
254
114
|
async get(id) {
|
|
@@ -273,8 +133,8 @@ export class OperationService {
|
|
|
273
133
|
}
|
|
274
134
|
if ((context.kind === "create" && context.phase === "observing") ||
|
|
275
135
|
context.kind === "exiting" ||
|
|
276
|
-
(context.kind === "archive"
|
|
277
|
-
|
|
136
|
+
((context.kind === "archive" || context.kind === "delete") &&
|
|
137
|
+
context.phase === "confirming")) {
|
|
278
138
|
checks.push(this.#observe(entry));
|
|
279
139
|
}
|
|
280
140
|
}
|
|
@@ -298,87 +158,10 @@ export class OperationService {
|
|
|
298
158
|
if (!this.#pending(entry))
|
|
299
159
|
return;
|
|
300
160
|
try {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
await this.sessions.managementSnapshot();
|
|
306
|
-
if (!this.#pending(entry))
|
|
307
|
-
return;
|
|
308
|
-
const phase = context.task.phase;
|
|
309
|
-
const exited = await context.task.advance();
|
|
310
|
-
if (!this.#pending(entry))
|
|
311
|
-
return;
|
|
312
|
-
if (phase !== context.task.phase)
|
|
313
|
-
entry.metadata.updatedAt = new Date(this.now()).toISOString();
|
|
314
|
-
if (!exited)
|
|
315
|
-
return;
|
|
316
|
-
if (context.nativeId === null) {
|
|
317
|
-
this.#finish(entry, "succeeded", null);
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
await this.#archive(entry, context.nativeId);
|
|
321
|
-
if (!this.#pending(entry))
|
|
322
|
-
return;
|
|
323
|
-
}
|
|
324
|
-
catch (error) {
|
|
325
|
-
if (this.#pending(entry))
|
|
326
|
-
this.#finish(entry, "unknown", error instanceof Error ? error.message : String(error));
|
|
327
|
-
return;
|
|
328
|
-
}
|
|
329
|
-
}
|
|
330
|
-
const snapshot = await this.sessions.managementSnapshot();
|
|
331
|
-
if (!this.#pending(entry))
|
|
332
|
-
return;
|
|
333
|
-
context = entry.stage;
|
|
334
|
-
if (context.kind === "create" && context.phase === "observing") {
|
|
335
|
-
const matches = snapshot.sessions.filter((session) => {
|
|
336
|
-
const attempt = this.bindings.latest(session.id);
|
|
337
|
-
return (session.origin.sourceInstanceId === entry.sourceInstanceId &&
|
|
338
|
-
attempt?.valid &&
|
|
339
|
-
attempt.operationId === entry.metadata.operationId);
|
|
340
|
-
});
|
|
341
|
-
if (matches.length > 1)
|
|
342
|
-
throw new AssociationError("Multiple sessions claimed this operation ID");
|
|
343
|
-
const match = matches[0];
|
|
344
|
-
if (match) {
|
|
345
|
-
if (entry.metadata.mode === "resume" &&
|
|
346
|
-
(match.id !== entry.metadata.sessionId ||
|
|
347
|
-
match.archived ||
|
|
348
|
-
match.sourceId !== entry.resumeNativeId ||
|
|
349
|
-
!match.sourceId ||
|
|
350
|
-
this.#matches(snapshot, entry, match.sourceId).length !== 1))
|
|
351
|
-
throw new AssociationError("Resume did not bind the original unique session");
|
|
352
|
-
const attempt = this.bindings.latest(match.id);
|
|
353
|
-
if (!attempt?.valid ||
|
|
354
|
-
attempt.socketPath !== context.target.socketPath ||
|
|
355
|
-
attempt.paneId !== context.target.paneId ||
|
|
356
|
-
match.projectDirectory !== context.project) {
|
|
357
|
-
throw new AssociationError("Binding does not match the launched target/project");
|
|
358
|
-
}
|
|
359
|
-
const binding = await this.interaction.describe(match.id);
|
|
360
|
-
if (!this.#pending(entry))
|
|
361
|
-
return;
|
|
362
|
-
if (binding?.supported && binding.state === "unknown")
|
|
363
|
-
return;
|
|
364
|
-
if (!binding?.supported || binding.state !== "connected")
|
|
365
|
-
throw new AssociationError("The new session binding failed validation");
|
|
366
|
-
const matchesTarget = await this.interaction.matchesTarget(match.id, context.target);
|
|
367
|
-
if (!this.#pending(entry))
|
|
368
|
-
return;
|
|
369
|
-
if (!matchesTarget)
|
|
370
|
-
throw new AssociationError("Binding does not match the launched target");
|
|
371
|
-
entry.metadata.sessionId = match.id;
|
|
372
|
-
this.#finish(entry, "succeeded", null);
|
|
373
|
-
}
|
|
374
|
-
}
|
|
375
|
-
else if ((context.kind === "archive" || context.kind === "delete") &&
|
|
376
|
-
context.phase === "confirming") {
|
|
377
|
-
const matches = this.#matches(snapshot, entry, context.nativeId);
|
|
378
|
-
if ((context.kind === "archive" && matches.length === 1 && matches[0]?.archived) ||
|
|
379
|
-
(context.kind === "delete" && matches.length === 0))
|
|
380
|
-
this.#finish(entry, "succeeded", null);
|
|
381
|
-
}
|
|
161
|
+
if (entry.stage.kind === "create")
|
|
162
|
+
await this.#launch.observe(entry);
|
|
163
|
+
else
|
|
164
|
+
await this.#lifecycle.observe(entry);
|
|
382
165
|
}
|
|
383
166
|
catch (error) {
|
|
384
167
|
// Read failures never prove disappearance or justify another command.
|
|
@@ -387,49 +170,14 @@ export class OperationService {
|
|
|
387
170
|
}
|
|
388
171
|
this.#timeout(entry);
|
|
389
172
|
}
|
|
390
|
-
async #startExit(entry, sessionId, nativeId) {
|
|
391
|
-
const target = await this.interaction.resolveTarget(sessionId);
|
|
392
|
-
if (!this.#pending(entry))
|
|
393
|
-
return;
|
|
394
|
-
if (!target) {
|
|
395
|
-
if (nativeId === null)
|
|
396
|
-
this.#finish(entry, "succeeded", null);
|
|
397
|
-
else
|
|
398
|
-
await this.#archive(entry, nativeId);
|
|
399
|
-
return;
|
|
400
|
-
}
|
|
401
|
-
const task = this.#exit.start(sessionId, target, this.managers.get(entry.sourceInstanceId), () => {
|
|
402
|
-
this.#timeout(entry);
|
|
403
|
-
if (!this.#pending(entry))
|
|
404
|
-
throw new Error("Exit operation is no longer active");
|
|
405
|
-
});
|
|
406
|
-
this.#transition(entry, { kind: "exiting", task, nativeId }, this.now() + (this.waitMs.exit ?? this.waitMs.archive));
|
|
407
|
-
await this.#observe(entry);
|
|
408
|
-
}
|
|
409
|
-
async #archive(entry, nativeId) {
|
|
410
|
-
if (!this.#pending(entry))
|
|
411
|
-
return;
|
|
412
|
-
this.#transition(entry, { kind: "archive", phase: "archiving", nativeId }, this.now() + 25_000);
|
|
413
|
-
try {
|
|
414
|
-
await this.managers.get(entry.sourceInstanceId).archive(nativeId);
|
|
415
|
-
if (!this.#pending(entry))
|
|
416
|
-
return;
|
|
417
|
-
this.#transition(entry, { kind: "archive", phase: "confirming", nativeId }, this.now() + this.waitMs.archive);
|
|
418
|
-
}
|
|
419
|
-
catch (error) {
|
|
420
|
-
if (!this.#pending(entry))
|
|
421
|
-
return;
|
|
422
|
-
this.#error(entry, error);
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
173
|
#transition(entry, context, deadline = entry.stage?.deadline ?? null) {
|
|
426
174
|
if (!this.#pending(entry))
|
|
427
175
|
return;
|
|
428
|
-
const previous =
|
|
176
|
+
const previous = operationPhase(entry.stage);
|
|
429
177
|
entry.stage = { ...context, deadline };
|
|
430
178
|
if (context.kind === "create" && context.phase === "observing")
|
|
431
179
|
entry.metadata.target = context.target;
|
|
432
|
-
if (previous !==
|
|
180
|
+
if (previous !== operationPhase(entry.stage))
|
|
433
181
|
entry.metadata.updatedAt = new Date(this.now()).toISOString();
|
|
434
182
|
}
|
|
435
183
|
#lookup(id) {
|
|
@@ -442,16 +190,7 @@ export class OperationService {
|
|
|
442
190
|
const context = entry.stage;
|
|
443
191
|
if (!context)
|
|
444
192
|
throw new Error("Pending operation has no execution stage");
|
|
445
|
-
|
|
446
|
-
? context.task.phase
|
|
447
|
-
: context.kind === "archive"
|
|
448
|
-
? context.phase === "confirming"
|
|
449
|
-
? "confirming_archive"
|
|
450
|
-
: context.phase === "resolving_target"
|
|
451
|
-
? null
|
|
452
|
-
: context.phase
|
|
453
|
-
: null;
|
|
454
|
-
return { ...entry.metadata, phase, status: "pending", error: null };
|
|
193
|
+
return { ...entry.metadata, phase: operationPhase(context), status: "pending", error: null };
|
|
455
194
|
}
|
|
456
195
|
#attach(entry, command) {
|
|
457
196
|
if (!this.#pending(entry) || entry.metadata.attachCommand === command)
|
|
@@ -459,9 +198,6 @@ export class OperationService {
|
|
|
459
198
|
entry.metadata.attachCommand = command;
|
|
460
199
|
entry.metadata.updatedAt = new Date(this.now()).toISOString();
|
|
461
200
|
}
|
|
462
|
-
#matches(snapshot, entry, nativeId) {
|
|
463
|
-
return snapshot.sessions.filter((session) => session.origin.sourceInstanceId === entry.sourceInstanceId && session.sourceId === nativeId);
|
|
464
|
-
}
|
|
465
201
|
#pending(entry) {
|
|
466
202
|
return !this.#closed && !entry.result && entry.stage !== null;
|
|
467
203
|
}
|
|
@@ -550,4 +286,18 @@ export class OperationService {
|
|
|
550
286
|
}
|
|
551
287
|
}
|
|
552
288
|
}
|
|
289
|
+
function operationPhase(context) {
|
|
290
|
+
if (context.kind === "exiting")
|
|
291
|
+
return context.task.phase;
|
|
292
|
+
if (context.kind !== "archive")
|
|
293
|
+
return null;
|
|
294
|
+
switch (context.phase) {
|
|
295
|
+
case "confirming":
|
|
296
|
+
return "confirming_archive";
|
|
297
|
+
case "resolving_target":
|
|
298
|
+
return null;
|
|
299
|
+
case "archiving":
|
|
300
|
+
return context.phase;
|
|
301
|
+
}
|
|
302
|
+
}
|
|
553
303
|
//# sourceMappingURL=operation-service.js.map
|