remote-codex 0.11.41 → 0.11.42

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.
@@ -675,6 +675,20 @@ var RelayStore = class _RelayStore {
675
675
  ).get(deviceId, userId);
676
676
  return row ? { sandboxId: row.id, enabled: Boolean(row.workspace_isolation_enabled) } : null;
677
677
  }
678
+ hostedWorkspaceBootstrapContext(deviceId) {
679
+ const sandbox = this.hostedWorkspaceIsolation(deviceId);
680
+ if (!sandbox?.enabled) {
681
+ return null;
682
+ }
683
+ const users = this.sqlite.prepare(
684
+ `SELECT u.*
685
+ FROM relay_hosted_sandbox_members m
686
+ JOIN relay_users u ON u.id = m.user_id
687
+ WHERE m.sandbox_id = ? AND u.enabled = 1
688
+ ORDER BY m.position ASC, m.created_at ASC`
689
+ ).all(sandbox.sandboxId).map((row) => this.rowToUser(row)).filter((user) => Boolean(user)).map((user) => this.publicUser(user));
690
+ return { sandboxId: sandbox.sandboxId, users };
691
+ }
678
692
  hostedUserWorkspaceIds(sandboxId, userId) {
679
693
  return this.sqlite.prepare(
680
694
  `SELECT workspace_id FROM relay_hosted_user_workspaces
@@ -683,11 +697,24 @@ var RelayStore = class _RelayStore {
683
697
  }
684
698
  recordHostedUserWorkspace(sandboxId, userId, workspaceId, initial = false) {
685
699
  this.sqlite.prepare(
686
- `INSERT OR IGNORE INTO relay_hosted_user_workspaces
700
+ `INSERT INTO relay_hosted_user_workspaces
687
701
  (sandbox_id, user_id, workspace_id, initial_workspace, created_at)
688
- VALUES (?, ?, ?, ?, ?)`
702
+ VALUES (?, ?, ?, ?, ?)
703
+ ON CONFLICT(sandbox_id, workspace_id) DO UPDATE SET
704
+ initial_workspace = MAX(
705
+ relay_hosted_user_workspaces.initial_workspace,
706
+ excluded.initial_workspace
707
+ )`
689
708
  ).run(sandboxId, userId, workspaceId, initial ? 1 : 0, (/* @__PURE__ */ new Date()).toISOString());
690
709
  }
710
+ hostedInitialWorkspaceId(sandboxId, userId) {
711
+ const row = this.sqlite.prepare(
712
+ `SELECT workspace_id FROM relay_hosted_user_workspaces
713
+ WHERE sandbox_id = ? AND user_id = ? AND initial_workspace = 1
714
+ ORDER BY created_at ASC LIMIT 1`
715
+ ).get(sandboxId, userId);
716
+ return row?.workspace_id ?? null;
717
+ }
691
718
  ownsHostedWorkspace(sandboxId, userId, workspaceId) {
692
719
  return Boolean(
693
720
  this.sqlite.prepare(
@@ -711,6 +738,15 @@ var RelayStore = class _RelayStore {
711
738
  ).get(sandboxId, userId, threadId)
712
739
  );
713
740
  }
741
+ hasHostedUserThreadInWorkspace(sandboxId, userId, workspaceId) {
742
+ return Boolean(
743
+ this.sqlite.prepare(
744
+ `SELECT 1 FROM relay_hosted_user_threads
745
+ WHERE sandbox_id = ? AND user_id = ? AND workspace_id = ?
746
+ LIMIT 1`
747
+ ).get(sandboxId, userId, workspaceId)
748
+ );
749
+ }
714
750
  listHostedProviderRecords() {
715
751
  return this.sqlite.prepare(
716
752
  "SELECT id, credential_ref FROM relay_hosted_sandboxes ORDER BY id"
@@ -3951,6 +3987,37 @@ function buildRelayServer(config2, options = {}) {
3951
3987
  hostedSandboxProvider,
3952
3988
  config2.hostedSandbox
3953
3989
  );
3990
+ const scheduleHostedUserBootstraps = (deviceId) => {
3991
+ const context = store.hostedWorkspaceBootstrapContext(deviceId);
3992
+ const supervisor = state.supervisors.get(deviceId);
3993
+ if (!context || !supervisor || supervisor.socket.readyState !== WEBSOCKET_OPEN) {
3994
+ return;
3995
+ }
3996
+ void Promise.allSettled(
3997
+ context.users.map(
3998
+ (bootstrapUser) => ensureHostedUserBootstrap({
3999
+ store,
4000
+ supervisor,
4001
+ deviceId,
4002
+ sandboxId: context.sandboxId,
4003
+ user: bootstrapUser
4004
+ })
4005
+ )
4006
+ ).then((results) => {
4007
+ results.forEach((result, index) => {
4008
+ if (result.status === "rejected") {
4009
+ app2.log.warn(
4010
+ {
4011
+ err: result.reason,
4012
+ deviceId,
4013
+ userId: context.users[index]?.id
4014
+ },
4015
+ "Hosted VM user bootstrap failed."
4016
+ );
4017
+ }
4018
+ });
4019
+ });
4020
+ };
3954
4021
  const allowedWebViewCorsOrigins = webViewCorsOrigins(
3955
4022
  options.env ?? process.env
3956
4023
  );
@@ -4303,10 +4370,12 @@ function buildRelayServer(config2, options = {}) {
4303
4370
  }
4304
4371
  const { sandboxId } = z.object({ sandboxId: z.string().uuid() }).parse(request.params);
4305
4372
  const body = updateHostedSandboxMembersSchema.parse(request.body ?? {});
4306
- return hostedSandboxService.updateMembers(
4373
+ const sandbox = hostedSandboxService.updateMembers(
4307
4374
  sandboxId,
4308
4375
  body.assignedUserIds
4309
4376
  );
4377
+ scheduleHostedUserBootstraps(sandbox.deviceId);
4378
+ return sandbox;
4310
4379
  }
4311
4380
  );
4312
4381
  app2.patch(
@@ -4316,10 +4385,14 @@ function buildRelayServer(config2, options = {}) {
4316
4385
  if (!user) return;
4317
4386
  const { sandboxId } = z.object({ sandboxId: z.string().uuid() }).parse(request.params);
4318
4387
  const body = updateHostedSandboxSettingsSchema.parse(request.body ?? {});
4319
- return store.setHostedWorkspaceIsolation(
4388
+ const sandbox = store.setHostedWorkspaceIsolation(
4320
4389
  sandboxId,
4321
4390
  body.workspaceIsolationEnabled
4322
4391
  );
4392
+ if (sandbox.workspaceIsolationEnabled) {
4393
+ scheduleHostedUserBootstraps(sandbox.deviceId);
4394
+ }
4395
+ return sandbox;
4323
4396
  }
4324
4397
  );
4325
4398
  app2.post(
@@ -4681,6 +4754,7 @@ function buildRelayServer(config2, options = {}) {
4681
4754
  clientConnection.socket.close();
4682
4755
  }
4683
4756
  });
4757
+ scheduleHostedUserBootstraps(deviceId);
4684
4758
  }
4685
4759
  });
4686
4760
  realtimeApp.route({
@@ -5073,9 +5147,6 @@ async function forwardSharedThreadList(input) {
5073
5147
  );
5074
5148
  }
5075
5149
  async function ensureHostedUserBootstrap(input) {
5076
- if (input.store.hostedUserWorkspaceIds(input.sandboxId, input.user.id).length) {
5077
- return;
5078
- }
5079
5150
  const key = `${input.sandboxId}:${input.user.id}`;
5080
5151
  const existing = hostedBootstrapPromises.get(key);
5081
5152
  if (existing) return existing;
@@ -5084,55 +5155,69 @@ async function ensureHostedUserBootstrap(input) {
5084
5155
  const directory = `${slug}-${input.user.id.slice(0, 8)}`;
5085
5156
  const absoluteDirectory = `/home/remote-codex/workspaces/${directory}`;
5086
5157
  const label = `${input.user.username}'s workspace`;
5087
- const current = await forwardSupervisorCommandJson(
5088
- input.supervisor,
5089
- input.deviceId,
5090
- "GET",
5091
- "/api/workspaces"
5092
- );
5093
- const currentWorkspaces = Array.isArray(current) ? current : [];
5094
- let workspace = currentWorkspaces.find(
5095
- (candidate) => isObject(candidate) && typeof candidate.absPath === "string" && candidate.absPath === absoluteDirectory
5158
+ let workspaceId = input.store.hostedInitialWorkspaceId(
5159
+ input.sandboxId,
5160
+ input.user.id
5096
5161
  );
5097
- if (!workspace) {
5098
- workspace = await forwardSupervisorCommandJson(
5162
+ if (!workspaceId) {
5163
+ const current = await forwardSupervisorCommandJson(
5099
5164
  input.supervisor,
5100
5165
  input.deviceId,
5101
- "POST",
5102
- "/api/workspaces",
5103
- { absPath: absoluteDirectory, label }
5166
+ "GET",
5167
+ "/api/workspaces"
5104
5168
  );
5105
- }
5106
- const workspaceId = stringField(workspace, "id");
5107
- if (!workspaceId) throw new Error("Initial workspace creation returned no id.");
5108
- const thread = await forwardSupervisorCommandJson(
5109
- input.supervisor,
5110
- input.deviceId,
5111
- "POST",
5112
- "/api/threads/start",
5113
- {
5114
- workspaceId,
5115
- title: "Getting started",
5116
- provider: "codex",
5117
- model: "gpt-5.6-sol",
5118
- reasoningEffort: "low",
5119
- approvalMode: "yolo"
5169
+ const currentWorkspaces = Array.isArray(current) ? current : [];
5170
+ let workspace = currentWorkspaces.find(
5171
+ (candidate) => isObject(candidate) && typeof candidate.absPath === "string" && candidate.absPath === absoluteDirectory
5172
+ );
5173
+ if (!workspace) {
5174
+ workspace = await forwardSupervisorCommandJson(
5175
+ input.supervisor,
5176
+ input.deviceId,
5177
+ "POST",
5178
+ "/api/workspaces",
5179
+ { absPath: absoluteDirectory, label }
5180
+ );
5120
5181
  }
5121
- );
5122
- const threadId = stringField(thread, "id");
5123
- if (!threadId) throw new Error("Initial thread creation returned no id.");
5124
- input.store.recordHostedUserWorkspace(
5125
- input.sandboxId,
5126
- input.user.id,
5127
- workspaceId,
5128
- true
5129
- );
5130
- input.store.recordHostedUserThread(
5182
+ workspaceId = stringField(workspace, "id");
5183
+ if (!workspaceId) {
5184
+ throw new Error("Initial workspace creation returned no id.");
5185
+ }
5186
+ input.store.recordHostedUserWorkspace(
5187
+ input.sandboxId,
5188
+ input.user.id,
5189
+ workspaceId,
5190
+ true
5191
+ );
5192
+ }
5193
+ if (!input.store.hasHostedUserThreadInWorkspace(
5131
5194
  input.sandboxId,
5132
5195
  input.user.id,
5133
- threadId,
5134
5196
  workspaceId
5135
- );
5197
+ )) {
5198
+ const thread = await forwardSupervisorCommandJson(
5199
+ input.supervisor,
5200
+ input.deviceId,
5201
+ "POST",
5202
+ "/api/threads/start",
5203
+ {
5204
+ workspaceId,
5205
+ title: "Getting started",
5206
+ provider: "codex",
5207
+ model: "gpt-5.6-sol",
5208
+ reasoningEffort: "low",
5209
+ approvalMode: "yolo"
5210
+ }
5211
+ );
5212
+ const threadId = stringField(thread, "id");
5213
+ if (!threadId) throw new Error("Initial thread creation returned no id.");
5214
+ input.store.recordHostedUserThread(
5215
+ input.sandboxId,
5216
+ input.user.id,
5217
+ threadId,
5218
+ workspaceId
5219
+ );
5220
+ }
5136
5221
  })().finally(() => hostedBootstrapPromises.delete(key));
5137
5222
  hostedBootstrapPromises.set(key, pending);
5138
5223
  return pending;
@@ -5763,7 +5848,9 @@ function isAllowedSharedRuntimeMetadataRequest(method, pathname) {
5763
5848
  function threadIdFromPath(pathValue) {
5764
5849
  const pathname = new URL(pathValue, "http://relay.local").pathname;
5765
5850
  const match = /^\/api\/threads\/([^/?#]+)/.exec(pathname);
5766
- return match ? decodeURIComponent(match[1]) : null;
5851
+ if (!match) return null;
5852
+ const threadId = decodeURIComponent(match[1]);
5853
+ return threadId === "start" || threadId === "import" ? null : threadId;
5767
5854
  }
5768
5855
  function workspaceIdFromPath(pathValue) {
5769
5856
  const pathname = new URL(pathValue, "http://relay.local").pathname;
@@ -16459,7 +16459,7 @@ var ThreadAuxiliaryStateStore = class {
16459
16459
  );
16460
16460
  return {
16461
16461
  id: record.id,
16462
- kind: "fastMode",
16462
+ kind: record.kind === "goal" ? "goal" : "fastMode",
16463
16463
  text: record.text,
16464
16464
  createdAt: record.createdAt,
16465
16465
  anchorTurnId: record.anchorTurnId ?? fallbackAnchor?.id ?? null
@@ -16673,13 +16673,10 @@ var ThreadGoalCoordinator = class {
16673
16673
  }
16674
16674
  try {
16675
16675
  await this.ensureGoalsFeatureEnabled(record.provider);
16676
- const activeGoal = this.listThreadGoalHistory(record.id).find(
16676
+ const goalHistoryBeforeUpdate = this.listThreadGoalHistory(record.id);
16677
+ const activeGoal = goalHistoryBeforeUpdate.find(
16677
16678
  (goal2) => ["active", "paused", "budgetLimited"].includes(goal2.status)
16678
16679
  ) ?? null;
16679
- const creatingNewGoal = goalObjectiveChanged(activeGoal, input.objective);
16680
- if (creatingNewGoal) {
16681
- markActiveThreadGoalRecordTerminated(this.db, record.id);
16682
- }
16683
16680
  if (input.status === "terminated") {
16684
16681
  const terminatedGoal = markActiveThreadGoalRecordTerminated(this.db, record.id);
16685
16682
  const goalHistory = this.listThreadGoalHistory(record.id);
@@ -16690,6 +16687,23 @@ var ThreadGoalCoordinator = class {
16690
16687
  });
16691
16688
  return goal2;
16692
16689
  }
16690
+ const startingNewGoal = shouldStartNewGoal(activeGoal, input.objective);
16691
+ if (startingNewGoal && (record.providerTurnId || record.status === "running")) {
16692
+ throw new HttpError(409, {
16693
+ code: "conflict",
16694
+ message: "Interrupt the running turn before replacing this goal."
16695
+ });
16696
+ }
16697
+ if (startingNewGoal && goalHistoryBeforeUpdate.length > 0) {
16698
+ if (!runtime.clearGoal) {
16699
+ throw new HttpError(409, {
16700
+ code: "conflict",
16701
+ message: "This backend cannot safely replace an existing goal."
16702
+ });
16703
+ }
16704
+ await runtime.clearGoal(providerSessionId);
16705
+ markActiveThreadGoalRecordTerminated(this.db, record.id);
16706
+ }
16693
16707
  const upstreamStatus = input.status;
16694
16708
  const goal = await runtime.setGoal({
16695
16709
  providerSessionId,
@@ -16701,10 +16715,15 @@ var ThreadGoalCoordinator = class {
16701
16715
  toThreadGoalDtoFromAgentGoal(goal),
16702
16716
  record
16703
16717
  );
16704
- const dto = creatingNewGoal ? resetGoalProgress(upstreamDto) : upstreamDto;
16718
+ const dto = startingNewGoal ? startFreshGoalLifecycle(upstreamDto) : upstreamDto;
16705
16719
  const persistedGoal = toThreadGoalDtoFromRecord(
16706
- this.persistThreadGoalSnapshot(record.id, dto)
16720
+ this.persistThreadGoalSnapshot(record.id, dto, {
16721
+ createNew: startingNewGoal
16722
+ })
16707
16723
  );
16724
+ if (startingNewGoal) {
16725
+ this.callbacks.appendGoalActivityNote(record.id, persistedGoal.objective);
16726
+ }
16708
16727
  this.callbacks.emitThreadEvent("thread.goal.updated", record.id, {
16709
16728
  goal: persistedGoal,
16710
16729
  goalHistory: this.listThreadGoalHistory(record.id)
@@ -16772,12 +16791,13 @@ var ThreadGoalCoordinator = class {
16772
16791
  throw error;
16773
16792
  }
16774
16793
  }
16775
- persistThreadGoalSnapshot(localThreadId, goal) {
16794
+ persistThreadGoalSnapshot(localThreadId, goal, options = {}) {
16776
16795
  const dto = "createdAt" in goal && typeof goal.createdAt === "string" ? goal : toThreadGoalDto(goal);
16777
16796
  return upsertThreadGoalRecord(this.db, {
16778
16797
  threadId: localThreadId,
16779
16798
  providerSessionId: dto.threadId,
16780
16799
  localGoalId: dto.localGoalId ?? null,
16800
+ createNew: options.createNew ?? false,
16781
16801
  objective: dto.objective,
16782
16802
  status: dto.status,
16783
16803
  tokenBudget: dto.tokenBudget,
@@ -16900,15 +16920,19 @@ function mergeGoalHistoryEntry(existing, incoming) {
16900
16920
  function goalHistoryStatusRank(status) {
16901
16921
  return ["active", "paused", "budgetLimited"].includes(status) ? 0 : 1;
16902
16922
  }
16903
- function resetGoalProgress(goal) {
16923
+ function startFreshGoalLifecycle(goal) {
16924
+ const now = (/* @__PURE__ */ new Date()).toISOString();
16904
16925
  return {
16905
16926
  ...goal,
16906
16927
  tokensUsed: 0,
16907
- timeUsedSeconds: 0
16928
+ timeUsedSeconds: 0,
16929
+ createdAt: now,
16930
+ updatedAt: now,
16931
+ completedAt: null
16908
16932
  };
16909
16933
  }
16910
- function goalObjectiveChanged(existing, nextObjective) {
16911
- return existing !== null && typeof nextObjective === "string" && nextObjective.trim().length > 0 && nextObjective !== existing.objective;
16934
+ function shouldStartNewGoal(existing, nextObjective) {
16935
+ return typeof nextObjective === "string" && nextObjective.trim().length > 0 && (existing === null || nextObjective !== existing.objective);
16912
16936
  }
16913
16937
  function isLocalGoalStatus(status) {
16914
16938
  return ["active", "paused", "budgetLimited"].includes(status);
@@ -18109,6 +18133,14 @@ var ThreadRuntimeEventProjector = class {
18109
18133
  if (!record) {
18110
18134
  return;
18111
18135
  }
18136
+ const currentGoal = await callbacks.getThreadGoalAfterRuntimeClear(record);
18137
+ if (currentGoal) {
18138
+ callbacks.emitThreadEvent("thread.goal.updated", record.id, {
18139
+ goal: currentGoal,
18140
+ goalHistory: callbacks.listThreadGoalHistory(record.id)
18141
+ });
18142
+ return;
18143
+ }
18112
18144
  markActiveThreadGoalRecordTerminated(db, record.id);
18113
18145
  callbacks.emitThreadEvent("thread.goal.cleared", record.id, {
18114
18146
  goalHistory: callbacks.listThreadGoalHistory(record.id)
@@ -22423,7 +22455,11 @@ var ThreadService = class {
22423
22455
  this.goalCoordinator = new ThreadGoalCoordinator(db, providerFeatures, {
22424
22456
  emitThreadEvent: (type, threadId, payload) => this.emitThreadEvent(type, threadId, payload),
22425
22457
  requireProviderSessionId: (record) => this.requireProviderSessionId(record),
22426
- runtimeForProvider: (provider2) => this.runtimeForProvider(provider2)
22458
+ runtimeForProvider: (provider2) => this.runtimeForProvider(provider2),
22459
+ appendGoalActivityNote: (threadId, objective) => this.auxiliaryState.appendActivityNote(threadId, {
22460
+ kind: "goal",
22461
+ text: objective
22462
+ })
22427
22463
  });
22428
22464
  this.auxiliaryState = new ThreadAuxiliaryStateStore(db, {
22429
22465
  cachedTurns: (localThreadId) => this.detailAssembler.cachedTurns(localThreadId),
@@ -22521,6 +22557,7 @@ var ThreadService = class {
22521
22557
  resetThreadContextUsage: (localThreadId, emitEvent) => this.resetThreadContextUsage(localThreadId, emitEvent),
22522
22558
  setThreadContextUsage: (localThreadId, usage, emitEvent) => this.setThreadContextUsage(localThreadId, usage, emitEvent),
22523
22559
  getThreadContextUsage: (localThreadId) => this.getThreadContextUsage(localThreadId),
22560
+ getThreadGoalAfterRuntimeClear: (record) => this.goalCoordinator.getThreadGoalForRecord(record),
22524
22561
  toThreadGoalDtoFromAgentGoal: (goal) => this.goalCoordinator.toThreadGoalDtoFromAgentGoal(goal),
22525
22562
  toThreadGoalDtoFromRecord: (record) => this.goalCoordinator.toThreadGoalDtoFromRecord(record)
22526
22563
  }