totemlayer 0.1.21 → 0.1.24

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,128 @@
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 = { messages: "starting", missions: "starting" };
24
+ let releaseMissions;
25
+ const messageReady = new Promise((resolve) => {
26
+ releaseMissions = resolve;
27
+ });
28
+ signal.addEventListener("abort", () => releaseMissions(false), { once: true });
29
+ if (signal.aborted) releaseMissions(false);
30
+ const report = (queue, event) => {
31
+ if (
32
+ [
33
+ "briefing_updated",
34
+ "file_overlap",
35
+ "session_visibility_unavailable",
36
+ "briefing_unavailable",
37
+ ].includes(event.type)
38
+ ) {
39
+ emit(event);
40
+ return;
41
+ }
42
+ phases[queue] = event.type === "completed" ? "waiting" : event.type;
43
+ const values = Object.values(phases);
44
+ const type =
45
+ ["reconnecting", "starting", "blocked", "working"].find((phase) =>
46
+ values.includes(phase),
47
+ ) ?? "waiting";
48
+ emit({ ...event, type });
49
+ };
50
+ const messages = async () => {
51
+ let attempts = 0;
52
+ while (!signal.aborted) {
53
+ try {
54
+ await (dependencies.runRuntime ?? runRuntime)({
55
+ ...settings,
56
+ signal,
57
+ stateDir: settings.stateDirectory,
58
+ allowAgents: [],
59
+ modelTimeoutMs: 900000,
60
+ replyTimeoutMs: 90000,
61
+ pollMs: settings.pollMs ?? 5000,
62
+ emit(event) {
63
+ if (event.type === "runtime_ready") emit(event);
64
+ else if (event.type === "runtime_reconnecting")
65
+ report("messages", { type: "reconnecting" });
66
+ else if (event.type === "runtime_resumed") report("messages", { type: "waiting" });
67
+ else if (event.type === "runtime_started") {
68
+ releaseMissions(true);
69
+ attempts = 0;
70
+ report("messages", { type: "waiting" });
71
+ } else if (event.type === "runtime_error")
72
+ report("messages", { type: "blocked", code: event.error?.code });
73
+ else if (["message_handled", "chat_completed"].includes(event.type))
74
+ report("messages", { type: "waiting" });
75
+ },
76
+ });
77
+ if (!signal.aborted) throw new Error("The message runtime stopped unexpectedly.");
78
+ } catch (error) {
79
+ if (signal.aborted) return;
80
+ if (
81
+ error.status === 401 ||
82
+ error.status === 403 ||
83
+ ["READINESS_PROBE_FAILED", "READINESS_UNSUPPORTED", "RUNTIME_SUPERSEDED"].includes(
84
+ error.code,
85
+ ) ||
86
+ error.code === "AGENT_IDENTITY_MISMATCH"
87
+ )
88
+ throw error;
89
+ report("messages", { type: "reconnecting" });
90
+ await sleep(
91
+ error.status === 429 ? 30000 : Math.min(30000, 1000 * 2 ** Math.min(attempts++, 5)),
92
+ undefined,
93
+ { signal },
94
+ );
95
+ }
96
+ }
97
+ };
98
+ const guarded = async (run) => {
29
99
  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.");
100
+ await run();
52
101
  } catch (error) {
53
- if (signal.aborted) return;
54
- if (
55
- error.status === 401 ||
56
- error.status === 403 ||
57
- error.code === "AGENT_IDENTITY_MISMATCH"
58
- )
102
+ if (!signal.aborted) {
103
+ controller.abort();
59
104
  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;
105
+ }
76
106
  }
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),
107
+ };
108
+ const results = await Promise.allSettled([
109
+ guarded(messages),
110
+ guarded(async () => {
111
+ // Do not dispatch business work while onboarding is still testing its adapter.
112
+ if (settings.verifyReadiness && !(await messageReady)) return;
113
+ return (dependencies.runMissions ?? runMissionAgentService)(
114
+ {
115
+ ...settings,
116
+ signal,
117
+ emit: (event) => report("missions", event),
118
+ },
119
+ session ? { ownedSession: session } : {},
120
+ );
86
121
  }),
87
- )] : []),
88
- ]);
89
- const failure = results.find((result) => result.status === "rejected");
90
- if (failure) throw failure.reason;
122
+ ]);
123
+ const failure = results.find((result) => result.status === "rejected");
124
+ if (failure) throw failure.reason;
125
+ } finally {
126
+ await session?.release();
127
+ }
91
128
  }
@@ -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 ||
@@ -43,6 +44,7 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
43
44
  pending.baseUrl !== baseUrl ||
44
45
  pending.requestedAgentId !== env.TOTEM_AGENT_ID ||
45
46
  pending.provider !== (env.TOTEM_PROVIDER ?? "auto") ||
47
+ pending.redirectUri !== options.binding?.redirectUri ||
46
48
  new Date(pending.expiresAt) <= new Date()
47
49
  ) {
48
50
  const result = await request(
@@ -55,6 +57,12 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
55
57
  deviceName: hostname().slice(0, 100),
56
58
  provider: env.TOTEM_PROVIDER ?? "auto",
57
59
  ...(env.TOTEM_AGENT_ID ? { agentId: env.TOTEM_AGENT_ID } : {}),
60
+ ...(options.binding
61
+ ? {
62
+ bindingChallenge: options.binding.challenge,
63
+ redirectUri: options.binding.redirectUri,
64
+ }
65
+ : {}),
58
66
  }),
59
67
  },
60
68
  options.fetch,
@@ -71,6 +79,10 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
71
79
  baseUrl,
72
80
  requestedAgentId: env.TOTEM_AGENT_ID,
73
81
  provider: env.TOTEM_PROVIDER ?? "auto",
82
+ replace: Boolean(options.replace),
83
+ ...(options.binding
84
+ ? { redirectUri: options.binding.redirectUri, verifier: options.binding.verifier }
85
+ : {}),
74
86
  };
75
87
  await privateWrite(join(runtime.directory, "connection-request.json"), pending);
76
88
  }
@@ -86,10 +98,45 @@ export async function beginDeviceLogin(runtime, env = process.env, options = {})
86
98
  () => {},
87
99
  );
88
100
  }
89
- return { url, code: pending.code, expiresAt: pending.expiresAt };
101
+ return { id: pending.id, url, code: pending.code, expiresAt: pending.expiresAt };
90
102
  }
91
103
 
92
104
  export async function resumeDeviceLogin(runtime, env = process.env, options = {}) {
105
+ await privateDirectory(runtime.directory);
106
+ const lock = join(runtime.directory, "login.lock");
107
+ try {
108
+ await mkdir(lock, { mode: 0o700 });
109
+ } catch (error) {
110
+ if (error.code !== "EEXIST") throw error;
111
+ const owner = await privateRead(join(lock, "owner.json")).catch(() => null);
112
+ if (owner && Number.isSafeInteger(owner.pid) && owner.pid > 0) {
113
+ try {
114
+ process.kill(owner.pid, 0);
115
+ } catch (failure) {
116
+ if (failure.code === "ESRCH") await rm(lock, { recursive: true });
117
+ }
118
+ }
119
+ return { state: "setup_in_progress" };
120
+ }
121
+ try {
122
+ await privateWrite(join(lock, "owner.json"), { pid: process.pid });
123
+ if (options.reset) {
124
+ for (const name of ["connection-ready.json", "connection-request.json"])
125
+ await rename(
126
+ join(runtime.directory, name),
127
+ join(runtime.directory, `${name}.reauthorize-${Date.now()}`),
128
+ ).catch((error) => {
129
+ if (error.code !== "ENOENT") throw error;
130
+ });
131
+ return { state: "reset" };
132
+ }
133
+ return await resumeLoginLocked(runtime, env, options);
134
+ } finally {
135
+ await rm(lock, { recursive: true });
136
+ }
137
+ }
138
+
139
+ async function resumeLoginLocked(runtime, env, options) {
93
140
  let staged;
94
141
  try {
95
142
  staged = await privateRead(join(runtime.directory, "connection-ready.json"));
@@ -111,6 +158,8 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
111
158
  { headers: { "x-totem-installation-token": pending.token } },
112
159
  options.fetch,
113
160
  );
161
+ if (result.status === "expired")
162
+ throw new Error("This installer authorization expired. Run totem login again.");
114
163
  if (result.status !== "approved") return;
115
164
  if (
116
165
  !/^[a-f0-9-]{36}$/.test(result.agentId) ||
@@ -122,6 +171,8 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
122
171
  key: result.key,
123
172
  baseUrl: pending.baseUrl,
124
173
  requestId: pending.id,
174
+ installationId: pending.id,
175
+ replace: Boolean(pending.replace),
125
176
  token: pending.token,
126
177
  };
127
178
  // Persist privately before acknowledging, so an interrupted setup never loses the key.
@@ -140,8 +191,36 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
140
191
  delete staged.token;
141
192
  await privateWrite(join(runtime.directory, "connection-ready.json"), staged);
142
193
  }
143
- const provider = await (options.detectProvider ?? detectInstalledProvider)(env);
144
- if (!provider) return { state: "executor_needed" };
194
+ const reportFailure = async (errorCode) => {
195
+ const pending = await privateRead(join(runtime.directory, "connection-request.json")).catch(
196
+ () => null,
197
+ );
198
+ if (pending && pending.id === staged.installationId)
199
+ await request(
200
+ origin(pending.baseUrl),
201
+ `/api/installations/${pending.id}/runtime`,
202
+ {
203
+ method: "POST",
204
+ headers: {
205
+ "content-type": "application/json",
206
+ "x-totem-installation-token": pending.token,
207
+ },
208
+ body: JSON.stringify({ errorCode }),
209
+ },
210
+ options.fetch,
211
+ ).catch(() => {});
212
+ };
213
+ let provider;
214
+ try {
215
+ provider = await (options.detectProvider ?? detectInstalledProvider)(env);
216
+ } catch (error) {
217
+ await reportFailure("UNSUPPORTED_ADAPTER");
218
+ throw error;
219
+ }
220
+ if (!provider) {
221
+ await reportFailure("PROVIDER_AUTH_REQUIRED");
222
+ return { state: "executor_needed" };
223
+ }
145
224
  const { agentSetupMain } = await import("./agent-setup.mjs");
146
225
  const status = await (options.setup ?? agentSetupMain)(
147
226
  [
@@ -152,6 +231,7 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
152
231
  origin(staged.baseUrl),
153
232
  ...(await providerArguments(provider, env)),
154
233
  "--no-start",
234
+ ...(options.replace || staged.replace ? ["--replace"] : []),
155
235
  "--json",
156
236
  ],
157
237
  {
@@ -160,9 +240,21 @@ export async function resumeDeviceLogin(runtime, env = process.env, options = {}
160
240
  stderr: { write() {} },
161
241
  },
162
242
  );
163
- if (status) throw new Error("The approved connection could not be saved.");
164
- const { rm } = await import("node:fs/promises");
243
+ if (status) {
244
+ await reportFailure("LISTENER_START_FAILED");
245
+ throw new Error("The approved connection could not be saved.");
246
+ }
247
+ await privateWrite(join(runtime.directory, "connection-last.json"), {
248
+ requestId: staged.installationId,
249
+ profile: staged.agentId,
250
+ });
165
251
  await rm(join(runtime.directory, "connection-ready.json"));
166
- await rm(join(runtime.directory, "connection-request.json"), { force: true });
252
+ const current = await privateRead(join(runtime.directory, "connection-request.json")).catch(
253
+ (error) => {
254
+ if (error.code !== "ENOENT") throw error;
255
+ },
256
+ );
257
+ if (current?.id === staged.installationId)
258
+ await rm(join(runtime.directory, "connection-request.json"), { force: true });
167
259
  return { state: "connected", profile: staged.agentId };
168
260
  }