totemlayer 0.1.21 → 0.1.25

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.
@@ -1,91 +1,132 @@
1
1
  import { setTimeout as sleep } from "node:timers/promises";
2
2
  import { runMissionAgentService } from "./mission-agent-service.mjs";
3
3
  import { runRuntime } from "./network-runtime.mjs";
4
+ import { acquireMissionAgentSession } from "./mission-agent-session.mjs";
4
5
 
5
6
  // Both queues use the same verified identity and private profile. A stopped or
6
7
  // revoked connection cancels both consumers; Missions retains its own leases.
7
8
  export async function runConnectedAgentService(settings, dependencies = {}) {
8
- const controller = new AbortController();
9
- const signal = settings.signal
10
- ? AbortSignal.any([settings.signal, controller.signal])
11
- : controller.signal;
12
- const emit = settings.emit ?? (() => {});
13
- const phases = { messages: "starting", missions: settings.codexThreadId ? "waiting" : "starting" };
14
- const blockers = {};
15
- const report = (queue, event) => {
16
- phases[queue] = event.type === "completed" ? "waiting" : event.type;
17
- if (event.type === "blocked") blockers[queue] = { code: event.code, messageId: event.messageId };
18
- else delete blockers[queue];
19
- const values = Object.values(phases);
20
- const type =
21
- ["reconnecting", "starting", "blocked", "working"].find((phase) => values.includes(phase)) ??
22
- "waiting";
23
- const blocked = type === "blocked" ? blockers.messages ?? blockers.missions : undefined;
24
- emit({ ...event, type, ...(blocked ?? {}) });
25
- };
26
- const messages = async () => {
27
- let attempts = 0;
28
- while (!signal.aborted) {
9
+ // Own the agent across both consumers before starting a second profile's probe.
10
+ const session =
11
+ settings.verifyReadiness && settings.client && settings.expectedAgentId
12
+ ? await acquireMissionAgentSession(
13
+ { baseUrl: settings.baseUrl, agentId: settings.expectedAgentId, signal: settings.signal },
14
+ settings.sessionLockDirectory ? { lockDirectory: settings.sessionLockDirectory } : {},
15
+ )
16
+ : null;
17
+ try {
18
+ const controller = new AbortController();
19
+ const signal = settings.signal
20
+ ? AbortSignal.any([settings.signal, controller.signal])
21
+ : controller.signal;
22
+ const emit = settings.emit ?? (() => {});
23
+ const phases = {
24
+ messages: "starting",
25
+ missions: settings.connectionOnly ? "waiting" : "starting",
26
+ };
27
+ let releaseMissions;
28
+ const messageReady = new Promise((resolve) => {
29
+ releaseMissions = resolve;
30
+ });
31
+ signal.addEventListener("abort", () => releaseMissions(false), { once: true });
32
+ if (signal.aborted) releaseMissions(false);
33
+ const report = (queue, event) => {
34
+ if (
35
+ [
36
+ "briefing_updated",
37
+ "file_overlap",
38
+ "session_visibility_unavailable",
39
+ "briefing_unavailable",
40
+ ].includes(event.type)
41
+ ) {
42
+ emit(event);
43
+ return;
44
+ }
45
+ phases[queue] = event.type === "completed" ? "waiting" : event.type;
46
+ const values = Object.values(phases);
47
+ const type =
48
+ ["reconnecting", "starting", "blocked", "working"].find((phase) =>
49
+ values.includes(phase),
50
+ ) ?? "waiting";
51
+ emit({ ...event, type });
52
+ };
53
+ const messages = async () => {
54
+ let attempts = 0;
55
+ while (!signal.aborted) {
56
+ try {
57
+ await (dependencies.runRuntime ?? runRuntime)({
58
+ ...settings,
59
+ signal,
60
+ stateDir: settings.stateDirectory,
61
+ allowAgents: [],
62
+ modelTimeoutMs: 900000,
63
+ replyTimeoutMs: 90000,
64
+ pollMs: settings.pollMs ?? 5000,
65
+ emit(event) {
66
+ if (event.type === "runtime_ready") emit(event);
67
+ else if (event.type === "runtime_reconnecting")
68
+ report("messages", { type: "reconnecting" });
69
+ else if (event.type === "runtime_resumed") report("messages", { type: "waiting" });
70
+ else if (event.type === "runtime_started") {
71
+ releaseMissions(true);
72
+ attempts = 0;
73
+ report("messages", { type: "waiting" });
74
+ } else if (event.type === "runtime_error")
75
+ report("messages", { type: "blocked", code: event.error?.code });
76
+ else if (["message_handled", "chat_completed"].includes(event.type))
77
+ report("messages", { type: "waiting" });
78
+ },
79
+ });
80
+ if (!signal.aborted) throw new Error("The message runtime stopped unexpectedly.");
81
+ } catch (error) {
82
+ if (signal.aborted) return;
83
+ if (
84
+ error.status === 401 ||
85
+ error.status === 403 ||
86
+ ["READINESS_PROBE_FAILED", "READINESS_UNSUPPORTED", "RUNTIME_SUPERSEDED"].includes(
87
+ error.code,
88
+ ) ||
89
+ error.code === "AGENT_IDENTITY_MISMATCH"
90
+ )
91
+ throw error;
92
+ report("messages", { type: "reconnecting" });
93
+ await sleep(
94
+ error.status === 429 ? 30000 : Math.min(30000, 1000 * 2 ** Math.min(attempts++, 5)),
95
+ undefined,
96
+ { signal },
97
+ );
98
+ }
99
+ }
100
+ };
101
+ const guarded = async (run) => {
29
102
  try {
30
- await (dependencies.runRuntime ?? runRuntime)({
31
- ...settings,
32
- signal,
33
- stateDir: settings.stateDirectory,
34
- allowAgents: [],
35
- adapterSettings: { ...settings.adapterSettings, diagnosticDirectory: settings.diagnosticDirectory },
36
- modelTimeoutMs: 900000,
37
- replyTimeoutMs: 90000,
38
- pollMs: settings.pollMs ?? 5000,
39
- emit(event) {
40
- if (event.type === "runtime_started") {
41
- attempts = 0;
42
- report("messages", { type: "waiting" });
43
- } else if (event.type === "runtime_error")
44
- report("messages", { type: "blocked", code: event.error?.code, messageId: event.messageId });
45
- else if (event.type === "message_started")
46
- report("messages", { type: "working", messageId: event.data?.messageId });
47
- else if (["message_handled", "chat_completed", "chat_queued"].includes(event.type))
48
- report("messages", { type: "waiting" });
49
- },
50
- });
51
- if (!signal.aborted) throw new Error("The message runtime stopped unexpectedly.");
103
+ await run();
52
104
  } catch (error) {
53
- if (signal.aborted) return;
54
- if (
55
- error.status === 401 ||
56
- error.status === 403 ||
57
- error.code === "AGENT_IDENTITY_MISMATCH"
58
- )
105
+ if (!signal.aborted) {
106
+ controller.abort();
59
107
  throw error;
60
- report("messages", { type: "reconnecting" });
61
- await sleep(
62
- error.status === 429 ? 30000 : Math.min(30000, 1000 * 2 ** Math.min(attempts++, 5)),
63
- undefined,
64
- { signal },
65
- );
66
- }
67
- }
68
- };
69
- const guarded = async (run) => {
70
- try {
71
- await run();
72
- } catch (error) {
73
- if (!signal.aborted) {
74
- controller.abort();
75
- throw error;
108
+ }
76
109
  }
77
- }
78
- };
79
- const results = await Promise.allSettled([
80
- guarded(messages),
81
- ...(!settings.codexThreadId ? [guarded(() =>
82
- (dependencies.runMissions ?? runMissionAgentService)({
83
- ...settings,
84
- signal,
85
- emit: (event) => report("missions", event),
110
+ };
111
+ const results = await Promise.allSettled([
112
+ guarded(messages),
113
+ guarded(async () => {
114
+ // Do not dispatch business work while onboarding is still testing its adapter.
115
+ if (settings.connectionOnly) return;
116
+ if (settings.verifyReadiness && !(await messageReady)) return;
117
+ return (dependencies.runMissions ?? runMissionAgentService)(
118
+ {
119
+ ...settings,
120
+ signal,
121
+ emit: (event) => report("missions", event),
122
+ },
123
+ session ? { ownedSession: session } : {},
124
+ );
86
125
  }),
87
- )] : []),
88
- ]);
89
- const failure = results.find((result) => result.status === "rejected");
90
- if (failure) throw failure.reason;
126
+ ]);
127
+ const failure = results.find((result) => result.status === "rejected");
128
+ if (failure) throw failure.reason;
129
+ } finally {
130
+ await session?.release();
131
+ }
91
132
  }
@@ -2,10 +2,11 @@ import { hostname } from "node:os";
2
2
  import { join } from "node:path";
3
3
  import { execFile } from "node:child_process";
4
4
  import { promisify } from "node:util";
5
- import { privateRead, privateWrite } from "./runtime-installation.mjs";
5
+ import { privateRead, privateWrite, privateDirectory } from "./runtime-installation.mjs";
6
+ import { mkdir, rm, rename } from "node:fs/promises";
6
7
  import { detectInstalledProvider, providerArguments } from "./provider-detection.mjs";
7
8
 
8
- function origin(value) {
9
+ export function origin(value) {
9
10
  const url = new URL(value ?? "https://totemlayer.com");
10
11
  if (
11
12
  url.username ||
@@ -42,7 +43,9 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
42
43
  !pending ||
43
44
  pending.baseUrl !== baseUrl ||
44
45
  pending.requestedAgentId !== env.TOTEM_AGENT_ID ||
46
+ pending.connectionId !== env.TOTEM_CONNECTION_ID ||
45
47
  pending.provider !== (env.TOTEM_PROVIDER ?? "auto") ||
48
+ pending.redirectUri !== options.binding?.redirectUri ||
46
49
  new Date(pending.expiresAt) <= new Date()
47
50
  ) {
48
51
  const result = await request(
@@ -55,6 +58,13 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
55
58
  deviceName: hostname().slice(0, 100),
56
59
  provider: env.TOTEM_PROVIDER ?? "auto",
57
60
  ...(env.TOTEM_AGENT_ID ? { agentId: env.TOTEM_AGENT_ID } : {}),
61
+ ...(env.TOTEM_CONNECTION_ID ? { connectionId: env.TOTEM_CONNECTION_ID } : {}),
62
+ ...(options.binding
63
+ ? {
64
+ bindingChallenge: options.binding.challenge,
65
+ redirectUri: options.binding.redirectUri,
66
+ }
67
+ : {}),
58
68
  }),
59
69
  },
60
70
  options.fetch,
@@ -70,11 +80,18 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
70
80
  ...result,
71
81
  baseUrl,
72
82
  requestedAgentId: env.TOTEM_AGENT_ID,
83
+ connectionId: env.TOTEM_CONNECTION_ID,
73
84
  provider: env.TOTEM_PROVIDER ?? "auto",
85
+ replace: Boolean(options.replace),
86
+ ...(options.binding
87
+ ? { redirectUri: options.binding.redirectUri, verifier: options.binding.verifier }
88
+ : {}),
74
89
  };
75
90
  await privateWrite(join(runtime.directory, "connection-request.json"), pending);
76
91
  }
77
- const url = `${baseUrl}/connect?code=${pending.code}`;
92
+ const url = pending.connectionId
93
+ ? `${baseUrl}/connections/${pending.connectionId}/accept?code=${pending.code}`
94
+ : `${baseUrl}/connect?code=${pending.code}`;
78
95
  if (options.openBrowser !== false) {
79
96
  const command =
80
97
  process.platform === "darwin"
@@ -86,10 +103,45 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
86
103
  () => {},
87
104
  );
88
105
  }
89
- return { url, code: pending.code, expiresAt: pending.expiresAt };
106
+ return { id: pending.id, url, code: pending.code, expiresAt: pending.expiresAt };
90
107
  }
91
108
 
92
109
  export async function resumeDeviceLogin(runtime, env = process.env, options = {}) {
110
+ await privateDirectory(runtime.directory);
111
+ const lock = join(runtime.directory, "login.lock");
112
+ try {
113
+ await mkdir(lock, { mode: 0o700 });
114
+ } catch (error) {
115
+ if (error.code !== "EEXIST") throw error;
116
+ const owner = await privateRead(join(lock, "owner.json")).catch(() => null);
117
+ if (owner && Number.isSafeInteger(owner.pid) && owner.pid > 0) {
118
+ try {
119
+ process.kill(owner.pid, 0);
120
+ } catch (failure) {
121
+ if (failure.code === "ESRCH") await rm(lock, { recursive: true });
122
+ }
123
+ }
124
+ return { state: "setup_in_progress" };
125
+ }
126
+ try {
127
+ await privateWrite(join(lock, "owner.json"), { pid: process.pid });
128
+ if (options.reset) {
129
+ for (const name of ["connection-ready.json", "connection-request.json"])
130
+ await rename(
131
+ join(runtime.directory, name),
132
+ join(runtime.directory, `${name}.reauthorize-${Date.now()}`),
133
+ ).catch((error) => {
134
+ if (error.code !== "ENOENT") throw error;
135
+ });
136
+ return { state: "reset" };
137
+ }
138
+ return await resumeLoginLocked(runtime, env, options);
139
+ } finally {
140
+ await rm(lock, { recursive: true });
141
+ }
142
+ }
143
+
144
+ async function resumeLoginLocked(runtime, env, options) {
93
145
  let staged;
94
146
  try {
95
147
  staged = await privateRead(join(runtime.directory, "connection-ready.json"));
@@ -111,6 +163,8 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
111
163
  { headers: { "x-totem-installation-token": pending.token } },
112
164
  options.fetch,
113
165
  );
166
+ if (result.status === "expired")
167
+ throw new Error("This installer authorization expired. Run totem login again.");
114
168
  if (result.status !== "approved") return;
115
169
  if (
116
170
  !/^[a-f0-9-]{36}$/.test(result.agentId) ||
@@ -119,9 +173,12 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
119
173
  throw new Error("Totem returned an invalid connection.");
120
174
  staged = {
121
175
  agentId: result.agentId,
176
+ ...(result.connectionId ? { connectionId: result.connectionId } : {}),
122
177
  key: result.key,
123
178
  baseUrl: pending.baseUrl,
124
179
  requestId: pending.id,
180
+ installationId: pending.id,
181
+ replace: Boolean(pending.replace),
125
182
  token: pending.token,
126
183
  };
127
184
  // Persist privately before acknowledging, so an interrupted setup never loses the key.
@@ -140,8 +197,36 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
140
197
  delete staged.token;
141
198
  await privateWrite(join(runtime.directory, "connection-ready.json"), staged);
142
199
  }
143
- const provider = await (options.detectProvider ?? detectInstalledProvider)(env);
144
- if (!provider) return { state: "executor_needed" };
200
+ const reportFailure = async (errorCode) => {
201
+ const pending = await privateRead(join(runtime.directory, "connection-request.json")).catch(
202
+ () => null,
203
+ );
204
+ if (pending && pending.id === staged.installationId)
205
+ await request(
206
+ origin(pending.baseUrl),
207
+ `/api/installations/${pending.id}/runtime`,
208
+ {
209
+ method: "POST",
210
+ headers: {
211
+ "content-type": "application/json",
212
+ "x-totem-installation-token": pending.token,
213
+ },
214
+ body: JSON.stringify({ errorCode }),
215
+ },
216
+ options.fetch,
217
+ ).catch(() => {});
218
+ };
219
+ let provider;
220
+ try {
221
+ provider = await (options.detectProvider ?? detectInstalledProvider)(env);
222
+ } catch (error) {
223
+ await reportFailure("UNSUPPORTED_ADAPTER");
224
+ throw error;
225
+ }
226
+ if (!provider) {
227
+ await reportFailure("PROVIDER_AUTH_REQUIRED");
228
+ return { state: "executor_needed" };
229
+ }
145
230
  const { agentSetupMain } = await import("./agent-setup.mjs");
146
231
  const status = await (options.setup ?? agentSetupMain)(
147
232
  [
@@ -152,6 +237,8 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
152
237
  origin(staged.baseUrl),
153
238
  ...(await providerArguments(provider, env)),
154
239
  "--no-start",
240
+ ...(staged.connectionId ? ["--no-mcp"] : []),
241
+ ...(options.replace || staged.replace ? ["--replace"] : []),
155
242
  "--json",
156
243
  ],
157
244
  {
@@ -160,9 +247,21 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
160
247
  stderr: { write() {} },
161
248
  },
162
249
  );
163
- if (status) throw new Error("The approved connection could not be saved.");
164
- const { rm } = await import("node:fs/promises");
250
+ if (status) {
251
+ await reportFailure("LISTENER_START_FAILED");
252
+ throw new Error("The approved connection could not be saved.");
253
+ }
254
+ await privateWrite(join(runtime.directory, "connection-last.json"), {
255
+ requestId: staged.installationId,
256
+ profile: staged.agentId,
257
+ });
165
258
  await rm(join(runtime.directory, "connection-ready.json"));
166
- await rm(join(runtime.directory, "connection-request.json"), { force: true });
259
+ const current = await privateRead(join(runtime.directory, "connection-request.json")).catch(
260
+ (error) => {
261
+ if (error.code !== "ENOENT") throw error;
262
+ },
263
+ );
264
+ if (current?.id === staged.installationId)
265
+ await rm(join(runtime.directory, "connection-request.json"), { force: true });
167
266
  return { state: "connected", profile: staged.agentId };
168
267
  }