totemlayer 0.1.77 → 0.1.88

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.
@@ -64,15 +64,18 @@ export async function observeSetupDispatch({ directory, identity, sessionId, kin
64
64
  !text.includes(`Totem setup test ${request.nonce}`)
65
65
  )
66
66
  return;
67
- const previous = await read(directory, `${kind}-dispatch`);
68
- if (previous && previous.id !== id) return;
69
- await privateWrite(path(directory, `${kind}-dispatch`), {
67
+ const name = `${kind}-dispatch-${id}`;
68
+ if (await read(directory, name)) return;
69
+ const dispatch = {
70
70
  version: 1,
71
71
  binding: request.binding,
72
72
  id,
73
73
  sessionId,
74
74
  dispatchedAt: Date.now(),
75
- });
75
+ };
76
+ await privateWrite(path(directory, name), dispatch);
77
+ if (!(await read(directory, `${kind}-dispatch`)))
78
+ await privateWrite(path(directory, `${kind}-dispatch`), dispatch);
76
79
  }
77
80
 
78
81
  export async function recordSetupPeerReply({
@@ -97,7 +100,9 @@ export async function recordSetupPeerReply({
97
100
  !message.content?.text?.includes(`Totem setup test ${request.nonce}`)
98
101
  )
99
102
  return;
100
- const dispatch = await read(profile.stateDirectory, "peer-dispatch");
103
+ const dispatch =
104
+ (await read(profile.stateDirectory, `peer-dispatch-${message.id}`)) ??
105
+ (await read(profile.stateDirectory, "peer-dispatch"));
101
106
  if (dispatch?.id !== message.id || dispatch.binding !== request.binding) return;
102
107
  const receivedAt = Date.parse(message.createdAt);
103
108
  if (!Number.isFinite(receivedAt) || receivedAt < request.issuedAt) return;
@@ -117,14 +122,24 @@ export async function recordSetupPeerReply({
117
122
  }
118
123
  if (cursor || replies.length !== 1 || replies[0].id !== reply.id) return;
119
124
  const acknowledged = await client.messages.get(message.id);
120
- if (acknowledged.state !== "acknowledged") return;
125
+ const repliedAt = Date.parse(reply.createdAt);
126
+ const acknowledgedAt = Date.parse(acknowledged.readAt);
127
+ if (
128
+ acknowledged.id !== message.id ||
129
+ !["acknowledged", "responded"].includes(acknowledged.state) ||
130
+ !Number.isFinite(repliedAt) ||
131
+ !Number.isFinite(acknowledgedAt) ||
132
+ acknowledgedAt < repliedAt
133
+ )
134
+ return;
121
135
  await privateWrite(path(profile.stateDirectory, "peer"), {
122
136
  binding: request.binding,
123
137
  nativeSessionId: sessionId,
124
138
  messageId: message.id,
125
139
  replyId: reply.id,
126
140
  receivedAt,
127
- repliedAt: Date.now(),
141
+ repliedAt,
142
+ acknowledgedAt,
128
143
  correlated: true,
129
144
  exactlyOneReply: true,
130
145
  acknowledgedAfterReply: true,
@@ -143,6 +158,7 @@ export async function recordSetupOwnerChat({
143
158
  text,
144
159
  sessionId,
145
160
  leaseRenewed,
161
+ client,
146
162
  }) {
147
163
  const request = await read(profile.stateDirectory, "request");
148
164
  if (!request || request.credentialFingerprint !== (await credentialFingerprint(profile))) return;
@@ -153,12 +169,41 @@ export async function recordSetupOwnerChat({
153
169
  !uuid(saved.chatId) ||
154
170
  !uuid(result?.event?.id) ||
155
171
  result.run?.state !== "completed" ||
156
- result.event.chatId !== saved.chatId ||
172
+ result.run.id !== commandId ||
173
+ result.run.commandEventId !== saved.commandEventId ||
174
+ result.event.runId !== commandId ||
175
+ result.event.actor !== "agent" ||
157
176
  leaseRenewed !== true
158
177
  )
159
178
  return;
160
- const dispatch = await read(profile.stateDirectory, "ownerChat-dispatch");
179
+ const dispatch =
180
+ (await read(profile.stateDirectory, `ownerChat-dispatch-${commandId}`)) ??
181
+ (await read(profile.stateDirectory, "ownerChat-dispatch"));
161
182
  if (dispatch?.id !== commandId || dispatch.binding !== request.binding) return;
183
+ let cursor = "0",
184
+ completedInOriginalChat = false;
185
+ for (let page = 0; page < 100; page++) {
186
+ const detail = await client.chats.get(saved.chatId, {
187
+ limit: 100,
188
+ ...(cursor ? { cursor } : {}),
189
+ });
190
+ if (detail.chat.id !== saved.chatId) return;
191
+ const event = detail.events.find((item) => item.id === result.event.id);
192
+ const run = detail.runs.find((item) => item.id === commandId);
193
+ if (
194
+ event?.actor === "agent" &&
195
+ event.runId === commandId &&
196
+ event.text === result.event.text &&
197
+ run?.state === "completed" &&
198
+ run.commandEventId === saved.commandEventId
199
+ ) {
200
+ completedInOriginalChat = true;
201
+ break;
202
+ }
203
+ if (!detail.hasMore || !detail.nextCursor || detail.nextCursor === cursor) return;
204
+ cursor = detail.nextCursor;
205
+ }
206
+ if (!completedInOriginalChat) return;
162
207
  await privateWrite(path(profile.stateDirectory, "ownerChat"), {
163
208
  binding: request.binding,
164
209
  commandId,
@@ -4,6 +4,20 @@ export const HOST_APPROVAL_RECOVERY =
4
4
  "Stop retrying. Ask the owner to inspect this command in the host app's approval or automatic review controls. If no approval path is available, contact the host's support with the denial. Resume once after the owner resolves it; do not bypass the policy or change agents.";
5
5
 
6
6
  const causes = {
7
+ STARTUP_CONFLICT: [
8
+ "The existing startup task or shortcut uses a different command.",
9
+ "Inspect the current-user Windows startup task with totem repair-startup. Apply only its reviewed task digest; preserve disabled startup and verify receiving separately.",
10
+ false,
11
+ ],
12
+ STARTUP_DISABLED: [
13
+ "The owner disabled automatic startup on this host.",
14
+ "Keep receiving paused until the owner explicitly enables startup through this host's controls.",
15
+ false,
16
+ ],
17
+ STARTUP_REGISTRATION_FAILED: [
18
+ "The host could not verify registration of its startup entry.",
19
+ "Inspect this host's current-user startup permissions and service manager, then retry once after repair.",
20
+ ],
7
21
  LISTENER_STARTUP_UNAVAILABLE: [
8
22
  "The host has no verified enabled persistent listener service.",
9
23
  "Restore the host's supported startup service, then rerun setup. A temporary background process is not a complete installation.",
@@ -0,0 +1,177 @@
1
+ import { execFile } from "node:child_process";
2
+ import { promisify, parseArgs } from "node:util";
3
+ import { createHash } from "node:crypto";
4
+ import { homedir } from "node:os";
5
+ import { join, resolve } from "node:path";
6
+ import {
7
+ runtimeRoot,
8
+ privateDirectory,
9
+ privateRead,
10
+ ensureInstalledRuntime,
11
+ } from "./runtime-installation.mjs";
12
+ import { powershellInvocation } from "./platform-client.mjs";
13
+
14
+ const execute = promisify(execFile);
15
+ const literal = (value) => `'${String(value).replaceAll("'", "''")}'`;
16
+
17
+ /** Explicit repair of one owner-reviewed task. Passive MCP startup never invokes this. */
18
+ export async function repairWindowsStartup(environment = process.env, options = {}) {
19
+ if ((options.platform ?? process.platform) !== "win32")
20
+ throw Object.assign(new Error("Run this repair on the Windows receiver host."), {
21
+ code: "STARTUP_REPAIR_UNSUPPORTED",
22
+ });
23
+ const digest = options.expectedTaskSha256 ?? "";
24
+ if (digest && !/^[a-f0-9]{64}$/.test(digest)) throw new Error("Invalid task digest.");
25
+ const root = runtimeRoot(environment),
26
+ directory = join(root, "runtime");
27
+ const current = await privateRead(join(directory, "current.json"));
28
+ if (!/^\d+\.\d+\.\d+$/.test(current.version)) throw new Error("Invalid installed runtime.");
29
+ const suffix =
30
+ root === resolve(join(homedir(), ".totem"))
31
+ ? ""
32
+ : `.${createHash("sha256").update(root).digest("hex").slice(0, 12)}`;
33
+ const task = `TotemRuntime${suffix}`;
34
+ const backups = join(directory, "startup-backups");
35
+ if (digest) await privateDirectory(backups);
36
+ const script = `
37
+ $ErrorActionPreference='Stop'
38
+ $task=${literal(task)};$expected=${literal(digest)};$node=${literal(process.execPath)}
39
+ $arguments=${literal('"' + join(directory, "supervisor.mjs") + '"')}
40
+ $backupDirectory=${literal(backups)}
41
+ function Hash($text) {
42
+ $sha=[Security.Cryptography.SHA256]::Create()
43
+ try { -join ($sha.ComputeHash([Text.Encoding]::UTF8.GetBytes($text))|ForEach-Object {$_.ToString('x2')}) } finally { $sha.Dispose() }
44
+ }
45
+ function Owned($entry) {
46
+ $user=$entry.Principal.UserId
47
+ try {
48
+ $sid=if($user -like 'S-1-*'){[Security.Principal.SecurityIdentifier]::new($user)}else{[Security.Principal.NTAccount]::new($user).Translate([Security.Principal.SecurityIdentifier])}
49
+ $sid.Value -eq [Security.Principal.WindowsIdentity]::GetCurrent().User.Value -and [string]$entry.Principal.RunLevel -in @('Limited','0') -and [string]$entry.Principal.LogonType -in @('Interactive','InteractiveToken','3')
50
+ } catch { $false }
51
+ }
52
+ try {
53
+ $old=Get-ScheduledTask -TaskName $task -TaskPath '\\'
54
+ if(!(Owned $old)){throw 'STARTUP_OWNER_MISMATCH'}
55
+ if($old.State -eq 'Disabled' -or !$old.Settings.Enabled){throw 'STARTUP_DISABLED'}
56
+ if($old.Actions.Count -ne 1){throw 'STARTUP_CONFLICT'}
57
+ foreach($hive in @('HKCU:','HKLM:')) {
58
+ $key=$hive+'\\Software\\Microsoft\\Windows\\CurrentVersion\\Explorer\\StartupApproved\\StartupFolder'
59
+ if(Test-Path -LiteralPath $key) {
60
+ $approval=(Get-Item -LiteralPath $key).GetValue($task+'.lnk',$null)
61
+ if($null -ne $approval -and ($approval.Length -lt 1 -or $approval[0] -notin @(2,6))){throw 'STARTUP_DISABLED'}
62
+ }
63
+ }
64
+ $xml=Export-ScheduledTask -TaskName $task -TaskPath '\\'
65
+ $hash=Hash $xml
66
+ $same=$old.Actions[0].Execute -eq $node -and $old.Actions[0].Arguments -eq $arguments
67
+ if(!$expected){@{state=if($same){'current'}else{'plan'};task=$task;taskSha256=$hash;version=${literal(current.version)};currentCommandType=[IO.Path]::GetFileName($old.Actions[0].Execute);proposedCommandType='node';changesProfiles=$false;changesPermissions=$false;requiresRestart=$true;receivingVerified=$false}|ConvertTo-Json -Compress;exit 0}
68
+ if($hash -ne $expected){throw 'STARTUP_PLAN_STALE'}
69
+ if($same){@{state='current';task=$task;version=${literal(current.version)};receivingVerified=$false}|ConvertTo-Json -Compress;exit 0}
70
+ $backup=Join-Path $backupDirectory ($hash+'.xml')
71
+ if(Test-Path -LiteralPath $backup) {
72
+ $file=Get-Item -LiteralPath $backup
73
+ if($file.PSIsContainer -or ($file.Attributes -band [IO.FileAttributes]::ReparsePoint) -or (Hash ([IO.File]::ReadAllText($backup))) -ne $hash){throw 'STARTUP_BACKUP_CONFLICT'}
74
+ } else {
75
+ $stream=[IO.File]::Open($backup,[IO.FileMode]::CreateNew,[IO.FileAccess]::Write,[IO.FileShare]::None)
76
+ try {$bytes=[Text.Encoding]::UTF8.GetBytes($xml);$stream.Write($bytes,0,$bytes.Length);$stream.Flush()}finally{$stream.Dispose()}
77
+ }
78
+ if((Hash (Export-ScheduledTask -TaskName $task -TaskPath '\\')) -ne $hash){throw 'STARTUP_PLAN_STALE'}
79
+ $wasRunning=$old.State -eq 'Running'
80
+ $changed=$false
81
+ try {
82
+ Stop-ScheduledTask -TaskName $task -TaskPath '\\'
83
+ $stopped=Get-ScheduledTask -TaskName $task -TaskPath '\\'
84
+ if($stopped.State -eq 'Running' -or $stopped.State -eq 'Disabled'){throw 'STARTUP_STOP_UNVERIFIED'}
85
+ if((Hash (Export-ScheduledTask -TaskName $task -TaskPath '\\')) -ne $hash){throw 'STARTUP_PLAN_STALE'}
86
+ $action=New-ScheduledTaskAction -Execute $node -Argument $arguments
87
+ $changed=$true
88
+ Set-ScheduledTask -TaskName $task -TaskPath '\\' -Action $action|Out-Null
89
+ $saved=Get-ScheduledTask -TaskName $task -TaskPath '\\'
90
+ if(!(Owned $saved) -or $saved.State -eq 'Disabled' -or $saved.Actions.Count -ne 1 -or $saved.Actions[0].Execute -ne $node -or $saved.Actions[0].Arguments -ne $arguments){throw 'STARTUP_REPAIR_UNVERIFIED'}
91
+ Start-ScheduledTask -TaskName $task -TaskPath '\\'
92
+ @{state='repaired';task=$task;version=${literal(current.version)};backupSha256=$hash;receivingVerified=$false}|ConvertTo-Json -Compress
93
+ } catch {
94
+ if(!$changed){
95
+ if($wasRunning -and (Hash (Export-ScheduledTask -TaskName $task -TaskPath '\\')) -eq $hash){Start-ScheduledTask -TaskName $task -TaskPath '\\'}
96
+ throw 'STARTUP_REPAIR_FAILED'
97
+ }
98
+ try {
99
+ Stop-ScheduledTask -TaskName $task -TaskPath '\\' -ErrorAction SilentlyContinue
100
+ Register-ScheduledTask -TaskName $task -TaskPath '\\' -Xml $xml -Force|Out-Null
101
+ if((Hash (Export-ScheduledTask -TaskName $task -TaskPath '\\')) -ne $hash){throw 'Rollback did not match'}
102
+ if($wasRunning){Start-ScheduledTask -TaskName $task -TaskPath '\\'}
103
+ } catch {throw 'STARTUP_ROLLBACK_FAILED'}
104
+ throw 'STARTUP_REPAIR_ROLLED_BACK'
105
+ }
106
+ } catch {
107
+ $code=[string]$_.Exception.Message
108
+ if($code -notmatch '^STARTUP_[A-Z_]+$'){$code='STARTUP_REPAIR_FAILED'}
109
+ @{state='blocked';code=$code;receivingVerified=$false}|ConvertTo-Json -Compress
110
+ }
111
+ `;
112
+ const invocation = powershellInvocation(script, environment);
113
+ const result = await (options.execute ?? execute)(invocation.command, invocation.args, {
114
+ timeout: 30000,
115
+ maxBuffer: 8192,
116
+ windowsHide: true,
117
+ });
118
+ const receipt = JSON.parse(result.stdout.trim());
119
+ if (
120
+ !["plan", "current", "repaired", "blocked"].includes(receipt.state) ||
121
+ (receipt.state === "plan" && !/^[a-f0-9]{64}$/.test(receipt.taskSha256 ?? "")) ||
122
+ (receipt.state === "repaired" && receipt.backupSha256 !== digest)
123
+ )
124
+ throw new Error("Invalid startup repair receipt.");
125
+ return receipt;
126
+ }
127
+
128
+ export async function startupRepairMain(args, options = {}) {
129
+ const stdout = options.stdout ?? process.stdout;
130
+ try {
131
+ const { values } = parseArgs({
132
+ args,
133
+ allowPositionals: false,
134
+ options: {
135
+ help: { type: "boolean", short: "h" },
136
+ "expected-task-sha256": { type: "string" },
137
+ json: { type: "boolean" },
138
+ },
139
+ });
140
+ if (values.help) {
141
+ stdout.write(
142
+ "totem repair-startup [--expected-task-sha256 HEX] [--json]\nInspect the current-user Windows startup task. Supply its inspected SHA-256 to migrate only that task with a local backup and rollback. Profiles and pauses are preserved; receiving must be verified separately.\n",
143
+ );
144
+ return 0;
145
+ }
146
+ if (values["expected-task-sha256"]) {
147
+ if (!/^[a-f0-9]{64}$/.test(values["expected-task-sha256"]))
148
+ throw new Error("Invalid task digest.");
149
+ const plan = await repairWindowsStartup(options.env ?? process.env, options);
150
+ if (
151
+ !["plan", "current"].includes(plan.state) ||
152
+ plan.taskSha256 !== values["expected-task-sha256"]
153
+ ) {
154
+ stdout.write(
155
+ JSON.stringify({
156
+ state: "blocked",
157
+ code: plan.code ?? "STARTUP_PLAN_STALE",
158
+ receivingVerified: false,
159
+ }) + "\n",
160
+ );
161
+ return 1;
162
+ }
163
+ await (options.ensureRuntime ?? ensureInstalledRuntime)(options.env ?? process.env);
164
+ }
165
+ const result = await repairWindowsStartup(options.env ?? process.env, {
166
+ ...options,
167
+ expectedTaskSha256: values["expected-task-sha256"],
168
+ });
169
+ stdout.write(JSON.stringify(result) + "\n");
170
+ return result.state === "blocked" ? 1 : 0;
171
+ } catch (error) {
172
+ stdout.write(
173
+ JSON.stringify({ state: "blocked", code: error.code ?? "STARTUP_REPAIR_FAILED" }) + "\n",
174
+ );
175
+ return 1;
176
+ }
177
+ }
@@ -1,6 +1,8 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { createReadStream } from "node:fs";
2
3
  import { opendir, readFile } from "node:fs/promises";
3
- import { join } from "node:path";
4
+ import { isAbsolute, join } from "node:path";
5
+ import { createInterface } from "node:readline";
4
6
  import { execFile } from "node:child_process";
5
7
  import { promisify } from "node:util";
6
8
  import { setTimeout as sleep } from "node:timers/promises";
@@ -90,28 +92,71 @@ export function createStrictCodexAdapter(flags, options = {}) {
90
92
  rpc = (options.connectHarness ?? connectCodexHarness)(inherited);
91
93
  await rpc.initialize();
92
94
  }
93
- // Resuming with no overrides reads the persisted model and policy. No turn starts.
94
- const current = await rpc.request("thread/resume", { threadId });
95
+ // Reading avoids acquiring the active chat's writer or changing its settings.
96
+ const current = await rpc.request("thread/read", { threadId });
97
+ const thread = current?.thread;
98
+ if (thread?.id !== threadId || thread.ephemeral !== false || thread.archived)
99
+ fail("EXACT_NATIVE_SESSION_REQUIRED");
95
100
  if (
96
- current?.thread?.id !== threadId ||
97
- current.thread.ephemeral !== false ||
98
- current.thread.archived
101
+ !thread.model ||
102
+ thread.model === "default" ||
103
+ (flags.model && thread.model !== flags.model)
99
104
  )
100
- fail("EXACT_NATIVE_SESSION_REQUIRED");
101
- if (!current.model || (flags.model && current.model !== flags.model))
105
+ fail("CURRENT_MODEL_UNVERIFIED");
106
+ // Codex returns the native rollout path; permission settings are not in thread/read.
107
+ if (!thread.path || !isAbsolute(thread.path)) fail("CURRENT_SESSION_SETTINGS_UNVERIFIED");
108
+ let metadata,
109
+ settings,
110
+ partialRecord = false;
111
+ const input = createReadStream(thread.path, { encoding: "utf8" });
112
+ const lines = createInterface({ input, crlfDelay: Infinity });
113
+ try {
114
+ for await (const line of lines) {
115
+ if (partialRecord) fail("CURRENT_SESSION_SETTINGS_UNVERIFIED");
116
+ let record;
117
+ try {
118
+ record = JSON.parse(line);
119
+ } catch {
120
+ // The native writer may still be appending its final JSONL record.
121
+ if (line.endsWith("}")) fail("CURRENT_SESSION_SETTINGS_UNVERIFIED");
122
+ partialRecord = true;
123
+ continue;
124
+ }
125
+ if (record.type === "session_meta") metadata = record.payload;
126
+ if (record.type === "turn_context") settings = record.payload;
127
+ }
128
+ } catch {
129
+ fail("CURRENT_SESSION_SETTINGS_UNVERIFIED");
130
+ } finally {
131
+ lines.close();
132
+ input.destroy();
133
+ }
134
+ if (
135
+ metadata?.id !== threadId ||
136
+ metadata.model_provider !== thread.modelProvider ||
137
+ !settings?.turn_id ||
138
+ settings.cwd !== thread.cwd ||
139
+ !settings.approval_policy ||
140
+ !settings.sandbox_policy?.type
141
+ )
142
+ fail("CURRENT_SESSION_SETTINGS_UNVERIFIED");
143
+ if (settings.model !== thread.model || settings.effort !== thread.reasoningEffort)
102
144
  fail("CURRENT_MODEL_UNVERIFIED");
103
145
  return {
104
146
  name: "codex",
105
- model: current.model,
147
+ model: thread.model,
106
148
  sessionId: threadId,
107
149
  modelVerified: true,
108
150
  policyDigest: sha([
109
- current.modelProvider,
110
- current.approvalPolicy,
111
- current.sandbox,
112
- current.permissionProfile,
113
- current.reasoningEffort,
114
- current.serviceTier,
151
+ thread.modelProvider,
152
+ settings.approval_policy,
153
+ settings.approvals_reviewer,
154
+ settings.sandbox_policy,
155
+ settings.permission_profile,
156
+ settings.active_permission_profile,
157
+ settings.disabled_plugin_ids,
158
+ settings.effort,
159
+ settings.service_tier,
115
160
  ]),
116
161
  };
117
162
  }
@@ -354,11 +399,14 @@ export function createStrictCodexAdapter(flags, options = {}) {
354
399
  },
355
400
  async readReceiver(context) {
356
401
  let current;
357
- for (let attempt = 0; attempt < 16; attempt++) {
358
- current = await readAgentRuntimeStatus(profile);
359
- if (Number.isFinite(current.measuredCheckSeconds) || !current.running) break;
402
+ const deadline = Date.now() + (flags.waitSeconds ?? 1200) * 1000;
403
+ do {
404
+ current = await (options.readStatus ?? readAgentRuntimeStatus)(profile);
405
+ if (Number.isFinite(current.measuredCheckSeconds) && current.listenerReady) break;
406
+ if (!(await automaticEnabled(profile))) fail("RECEIVER_INTENTIONALLY_PAUSED");
407
+ if (Date.now() >= deadline) break;
360
408
  await pause(1000);
361
- }
409
+ } while (true);
362
410
  return {
363
411
  binding: context.binding,
364
412
  count: current.running ? 1 : 0,
@@ -368,7 +416,7 @@ export function createStrictCodexAdapter(flags, options = {}) {
368
416
  measuredCheckSeconds: current.measuredCheckSeconds,
369
417
  independentOfModel: true,
370
418
  silentWhenIdle: true,
371
- bothQueues: true,
419
+ bothQueues: !identity.listenerConnectionId,
372
420
  privateState: true,
373
421
  startupEnabled: activation?.persistent === true,
374
422
  restartOnFailure: activation?.persistent === true,
@@ -396,7 +444,8 @@ export function createStrictCodexAdapter(flags, options = {}) {
396
444
  await privateWrite(join(profile.stateDirectory, "runner", `${current.runId}.stop`), {
397
445
  runId: current.runId,
398
446
  });
399
- for (let attempt = 0; attempt < 120; attempt++) {
447
+ const deadline = Date.now() + (flags.waitSeconds ?? 1200) * 1000;
448
+ do {
400
449
  if (!(await automaticEnabled(profile))) fail("RECEIVER_INTENTIONALLY_PAUSED");
401
450
  const after = await readAgentRuntimeStatus(profile);
402
451
  if (after.runId !== current.runId && after.ready && after.listenerReady) {
@@ -414,8 +463,9 @@ export function createStrictCodexAdapter(flags, options = {}) {
414
463
  await privateWrite(file, receipt);
415
464
  return receipt;
416
465
  }
466
+ if (Date.now() >= deadline) break;
417
467
  await pause(1000);
418
- }
468
+ } while (true);
419
469
  return { binding: context.binding, newRunVerified: false };
420
470
  },
421
471
  async requestTests(context) {
@@ -9,7 +9,7 @@ import {
9
9
  installRuntimeService,
10
10
  runtimeRoot,
11
11
  } from "./runtime-installation.mjs";
12
- import { acquireSupervisorLock, processIdentity } from "./automatic-runtime.mjs";
12
+ import { acquireSupervisorLock, processIdentity } from "./runtime-lock.mjs";
13
13
 
14
14
  const valid = (id) => typeof id === "string" && /^[a-f0-9]{64}$/.test(id);
15
15
  const missing = async (promise) =>
@@ -93,10 +93,18 @@ export async function scheduleStrictSetup(flags, host, env = process.env, option
93
93
  throw Object.assign(new Error(), { code: "SETUP_BINDING_CHANGED" });
94
94
  const job = previous ?? { version: 1, id, flags: selected };
95
95
  if (!validJob(job)) throw Object.assign(new Error(), { code: "SETUP_BINDING_UNVERIFIED" });
96
+ const status = await missing(privateRead(join(path, "status.json")));
97
+ const recheck =
98
+ status?.state === "blocked" &&
99
+ [
100
+ "RECEIVER_INTERVAL_UNVERIFIED",
101
+ "RECEIVER_CAPABILITY_INCOMPLETE",
102
+ "RECOVERY_UNVERIFIED",
103
+ ].includes(status.code);
104
+ if (status?.state === "blocked" && !recheck) return status;
96
105
  const runtime = await (options.ensureRuntime ?? ensureInstalledRuntime)(env);
97
106
  if (!previous) await privateWrite(join(path, "job.json"), job);
98
- const status = await missing(privateRead(join(path, "status.json")));
99
- if (status?.state === "complete")
107
+ if (status?.state === "complete" || recheck)
100
108
  await privateWrite(join(path, "status.json"), {
101
109
  state: "checking",
102
110
  complete: false,
package/bin/totem.mjs CHANGED
@@ -249,6 +249,7 @@ Connect once and receive messages and approved Mission work
249
249
  totem setup
250
250
  totem status
251
251
  totem diagnose [--profile NAME] [--fresh] [--json]
252
+ totem repair-startup [--expected-task-sha256 HEX] [--json]
252
253
  totem start --background
253
254
  totem stop
254
255
 
@@ -1754,6 +1755,11 @@ async function runRfq(options, baseUrl) {
1754
1755
  }
1755
1756
 
1756
1757
  async function main() {
1758
+ if (process.argv[2] === "repair-startup") {
1759
+ const { startupRepairMain } = await import("./startup-repair.mjs");
1760
+ process.exitCode = await startupRepairMain(process.argv.slice(3));
1761
+ return;
1762
+ }
1757
1763
  if (process.argv[2] === "install-worker") {
1758
1764
  const { strictSetupWorkerMain } = await import("./strict-setup-jobs.mjs");
1759
1765
  process.exitCode = await strictSetupWorkerMain(process.argv[3]);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "totemlayer",
3
- "version": "0.1.77",
3
+ "version": "0.1.88",
4
4
  "description": "Connect AI agents to Totem Network for discovery, messaging, and shared work.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -32,6 +32,10 @@
32
32
  "authorization"
33
33
  ],
34
34
  "homepage": "https://totemlayer.com",
35
+ "repository": {
36
+ "type": "git",
37
+ "url": "git+https://github.com/totemlayer/Totem.git"
38
+ },
35
39
  "license": "SEE LICENSE IN LICENSE.md",
36
40
  "publishConfig": {
37
41
  "access": "public",