remote-codex 0.11.41 → 0.11.43

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(
@@ -4629,6 +4702,10 @@ function buildRelayServer(config2, options = {}) {
4629
4702
  parsed.clientId
4630
4703
  );
4631
4704
  const eventThreadId = threadIdFromSocketPayload(parsed.payload);
4705
+ const shellEventForClient = clientConnection ? isShellEventForClient(
4706
+ parsed.payload,
4707
+ clientConnection.attachedShellId
4708
+ ) : false;
4632
4709
  if (clientConnection) {
4633
4710
  const freshAccess = store.effectiveAccess(
4634
4711
  clientConnection.user.id,
@@ -4651,7 +4728,7 @@ function buildRelayServer(config2, options = {}) {
4651
4728
  clientConnection.user.id
4652
4729
  );
4653
4730
  const connectionControlEvent = parsed.payload.type === "supervisor.connected" || parsed.payload.type === "supervisor.pong";
4654
- if (isolation?.enabled && !connectionControlEvent && (!eventThreadId || !store.ownsHostedThread(
4731
+ if (isolation?.enabled && !connectionControlEvent && !shellEventForClient && (!eventThreadId || !store.ownsHostedThread(
4655
4732
  isolation.sandboxId,
4656
4733
  clientConnection.user.id,
4657
4734
  eventThreadId
@@ -4661,7 +4738,8 @@ function buildRelayServer(config2, options = {}) {
4661
4738
  }
4662
4739
  if (clientConnection && clientConnection.socket.readyState === WEBSOCKET_OPEN && shouldForwardSocketEvent(
4663
4740
  parsed.payload,
4664
- clientConnection.threadId
4741
+ clientConnection.threadId,
4742
+ clientConnection.attachedShellId
4665
4743
  )) {
4666
4744
  clientConnection.socket.send(JSON.stringify(parsed.payload));
4667
4745
  }
@@ -4681,6 +4759,7 @@ function buildRelayServer(config2, options = {}) {
4681
4759
  clientConnection.socket.close();
4682
4760
  }
4683
4761
  });
4762
+ scheduleHostedUserBootstraps(deviceId);
4684
4763
  }
4685
4764
  });
4686
4765
  realtimeApp.route({
@@ -5073,9 +5152,6 @@ async function forwardSharedThreadList(input) {
5073
5152
  );
5074
5153
  }
5075
5154
  async function ensureHostedUserBootstrap(input) {
5076
- if (input.store.hostedUserWorkspaceIds(input.sandboxId, input.user.id).length) {
5077
- return;
5078
- }
5079
5155
  const key = `${input.sandboxId}:${input.user.id}`;
5080
5156
  const existing = hostedBootstrapPromises.get(key);
5081
5157
  if (existing) return existing;
@@ -5084,55 +5160,69 @@ async function ensureHostedUserBootstrap(input) {
5084
5160
  const directory = `${slug}-${input.user.id.slice(0, 8)}`;
5085
5161
  const absoluteDirectory = `/home/remote-codex/workspaces/${directory}`;
5086
5162
  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
5163
+ let workspaceId = input.store.hostedInitialWorkspaceId(
5164
+ input.sandboxId,
5165
+ input.user.id
5096
5166
  );
5097
- if (!workspace) {
5098
- workspace = await forwardSupervisorCommandJson(
5167
+ if (!workspaceId) {
5168
+ const current = await forwardSupervisorCommandJson(
5099
5169
  input.supervisor,
5100
5170
  input.deviceId,
5101
- "POST",
5102
- "/api/workspaces",
5103
- { absPath: absoluteDirectory, label }
5171
+ "GET",
5172
+ "/api/workspaces"
5104
5173
  );
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"
5174
+ const currentWorkspaces = Array.isArray(current) ? current : [];
5175
+ let workspace = currentWorkspaces.find(
5176
+ (candidate) => isObject(candidate) && typeof candidate.absPath === "string" && candidate.absPath === absoluteDirectory
5177
+ );
5178
+ if (!workspace) {
5179
+ workspace = await forwardSupervisorCommandJson(
5180
+ input.supervisor,
5181
+ input.deviceId,
5182
+ "POST",
5183
+ "/api/workspaces",
5184
+ { absPath: absoluteDirectory, label }
5185
+ );
5120
5186
  }
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(
5187
+ workspaceId = stringField(workspace, "id");
5188
+ if (!workspaceId) {
5189
+ throw new Error("Initial workspace creation returned no id.");
5190
+ }
5191
+ input.store.recordHostedUserWorkspace(
5192
+ input.sandboxId,
5193
+ input.user.id,
5194
+ workspaceId,
5195
+ true
5196
+ );
5197
+ }
5198
+ if (!input.store.hasHostedUserThreadInWorkspace(
5131
5199
  input.sandboxId,
5132
5200
  input.user.id,
5133
- threadId,
5134
5201
  workspaceId
5135
- );
5202
+ )) {
5203
+ const thread = await forwardSupervisorCommandJson(
5204
+ input.supervisor,
5205
+ input.deviceId,
5206
+ "POST",
5207
+ "/api/threads/start",
5208
+ {
5209
+ workspaceId,
5210
+ title: "Getting started",
5211
+ provider: "codex",
5212
+ model: "gpt-5.6-sol",
5213
+ reasoningEffort: "low",
5214
+ approvalMode: "yolo"
5215
+ }
5216
+ );
5217
+ const threadId = stringField(thread, "id");
5218
+ if (!threadId) throw new Error("Initial thread creation returned no id.");
5219
+ input.store.recordHostedUserThread(
5220
+ input.sandboxId,
5221
+ input.user.id,
5222
+ threadId,
5223
+ workspaceId
5224
+ );
5225
+ }
5136
5226
  })().finally(() => hostedBootstrapPromises.delete(key));
5137
5227
  hostedBootstrapPromises.set(key, pending);
5138
5228
  return pending;
@@ -5234,6 +5324,7 @@ function connectRelayWebsocket(supervisor, socket, store, user, deviceId, thread
5234
5324
  supervisor.clientSockets.set(clientId, {
5235
5325
  socket,
5236
5326
  threadId,
5327
+ attachedShellId: null,
5237
5328
  deviceId,
5238
5329
  user,
5239
5330
  access
@@ -5266,6 +5357,10 @@ function connectRelayWebsocket(supervisor, socket, store, user, deviceId, thread
5266
5357
  socket.close(1008, "Shared read-only session cannot control supervisor.");
5267
5358
  return;
5268
5359
  }
5360
+ const attachedShellId = shellAttachIdFromSocketPayload(payload);
5361
+ if (clientConnection && attachedShellId) {
5362
+ clientConnection.attachedShellId = attachedShellId;
5363
+ }
5269
5364
  sendToSupervisor(supervisor, {
5270
5365
  type: "relay.client.message",
5271
5366
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
@@ -5763,7 +5858,9 @@ function isAllowedSharedRuntimeMetadataRequest(method, pathname) {
5763
5858
  function threadIdFromPath(pathValue) {
5764
5859
  const pathname = new URL(pathValue, "http://relay.local").pathname;
5765
5860
  const match = /^\/api\/threads\/([^/?#]+)/.exec(pathname);
5766
- return match ? decodeURIComponent(match[1]) : null;
5861
+ if (!match) return null;
5862
+ const threadId = decodeURIComponent(match[1]);
5863
+ return threadId === "start" || threadId === "import" ? null : threadId;
5767
5864
  }
5768
5865
  function workspaceIdFromPath(pathValue) {
5769
5866
  const pathname = new URL(pathValue, "http://relay.local").pathname;
@@ -5960,14 +6057,25 @@ function isAllowedSharedWorkspacePath(access, methodName, pathname, workspaceId)
5960
6057
  ];
5961
6058
  return ["POST", "PUT", "PATCH", "DELETE"].includes(methodName) && writePatterns.some((pattern) => pattern.test(pathname));
5962
6059
  }
5963
- function shouldForwardSocketEvent(event, threadId) {
6060
+ function shouldForwardSocketEvent(event, threadId, attachedShellId) {
5964
6061
  if (!threadId) {
5965
6062
  return true;
5966
6063
  }
5967
6064
  if (event.type === "supervisor.connected" || event.type === "supervisor.pong") {
5968
6065
  return true;
5969
6066
  }
5970
- return "threadId" in event && event.threadId === threadId;
6067
+ if ("threadId" in event && event.threadId === threadId) {
6068
+ return true;
6069
+ }
6070
+ return isShellEventForClient(event, attachedShellId);
6071
+ }
6072
+ function shellAttachIdFromSocketPayload(payload) {
6073
+ return isObject(payload) && payload.type === "shell.attach" && typeof payload.shellId === "string" ? payload.shellId : null;
6074
+ }
6075
+ function isShellEventForClient(event, attachedShellId) {
6076
+ return Boolean(
6077
+ attachedShellId && event.type.startsWith("shell.") && "shellId" in event && event.shellId === attachedShellId
6078
+ );
5971
6079
  }
5972
6080
  function relayRequestBody(body) {
5973
6081
  if (body === void 0 || body === null) {
@@ -12089,7 +12089,7 @@ function mapModelInfo(model, index) {
12089
12089
  return {
12090
12090
  id: model.value,
12091
12091
  model: model.value,
12092
- displayName: model.displayName,
12092
+ displayName: versionedClaudeModelDisplayName(model),
12093
12093
  description: model.description,
12094
12094
  isDefault: index === 0,
12095
12095
  hidden: false,
@@ -12100,11 +12100,44 @@ function mapModelInfo(model, index) {
12100
12100
  defaultReasoningEffort: model.supportsEffort ? "medium" : null
12101
12101
  };
12102
12102
  }
12103
+ function versionedClaudeModelDisplayName(model) {
12104
+ const resolved = model.resolvedModel?.match(
12105
+ /^claude-(sonnet|opus|haiku|fable)-(\d+)(?:-(\d+))?(?:-|\[|$)/i
12106
+ );
12107
+ if (!resolved) {
12108
+ return model.displayName;
12109
+ }
12110
+ const family = `${resolved[1][0].toUpperCase()}${resolved[1].slice(1).toLowerCase()}`;
12111
+ const version2 = resolved[3] ? `${resolved[2]}.${resolved[3]}` : resolved[2];
12112
+ if (model.value === "default") {
12113
+ return `Default \xB7 ${family} ${version2}`;
12114
+ }
12115
+ const qualifier = model.displayName.match(/\s*(\([^)]+\))\s*$/)?.[1];
12116
+ return `${family} \xB7 ${version2}${qualifier ? ` ${qualifier}` : ""}`;
12117
+ }
12118
+ function versionedAlias(alias, discovered, qualifier) {
12119
+ if (!discovered) {
12120
+ return alias;
12121
+ }
12122
+ return {
12123
+ ...alias,
12124
+ displayName: `${discovered.displayName.replace(/\s+\([^)]+\)\s*$/, "")}${qualifier ?? ""}`,
12125
+ description: discovered.description,
12126
+ supportedReasoningEfforts: discovered.supportedReasoningEfforts,
12127
+ defaultReasoningEffort: discovered.defaultReasoningEffort
12128
+ };
12129
+ }
12103
12130
  function withClaudeCodeModelAliases(models) {
12104
12131
  const output = [...models];
12105
- const defaultSonnet = DEFAULT_CLAUDE_MODELS[0];
12106
- const oneMillionSonnet = DEFAULT_CLAUDE_MODELS[1];
12107
- const fable = DEFAULT_CLAUDE_MODELS[2];
12132
+ const discoveredSonnet = output.find((model) => /^Sonnet · /.test(model.displayName));
12133
+ const discoveredFable = output.find((model) => /^Fable · /.test(model.displayName));
12134
+ const defaultSonnet = versionedAlias(DEFAULT_CLAUDE_MODELS[0], discoveredSonnet);
12135
+ const oneMillionSonnet = versionedAlias(
12136
+ DEFAULT_CLAUDE_MODELS[1],
12137
+ discoveredSonnet,
12138
+ " (1M context)"
12139
+ );
12140
+ const fable = versionedAlias(DEFAULT_CLAUDE_MODELS[2], discoveredFable);
12108
12141
  const hasSonnetAlias = output.some((model) => model.model === "sonnet");
12109
12142
  if (!hasSonnetAlias) {
12110
12143
  output.unshift(defaultSonnet);
@@ -12613,6 +12646,7 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
12613
12646
  subscriptionUsageWindows = /* @__PURE__ */ new Map();
12614
12647
  subscriptionAuthKind = "unknown";
12615
12648
  subscriptionUsageObservedAt = null;
12649
+ modelOptionsCache = null;
12616
12650
  historicalTurnIdAliases = /* @__PURE__ */ new Map();
12617
12651
  clientApp;
12618
12652
  sdkLoadError = null;
@@ -12733,14 +12767,33 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
12733
12767
  }
12734
12768
  async listModels() {
12735
12769
  const active = [...this.activeTurns.values()][0];
12736
- if (!active) {
12737
- return DEFAULT_CLAUDE_MODELS;
12770
+ if (!active && this.modelOptionsCache) {
12771
+ return this.modelOptionsCache;
12738
12772
  }
12773
+ let query = active?.query ?? null;
12739
12774
  try {
12740
- const models = await active.query.supportedModels();
12741
- return withClaudeCodeModelAliases(models.map(mapModelInfo));
12775
+ query ??= this.queryFactory({
12776
+ prompt: "",
12777
+ options: queryOptionsForRuntime({
12778
+ home: this.options.home,
12779
+ command: this.options.command,
12780
+ clientApp: this.clientApp,
12781
+ approvalMode: "guarded",
12782
+ includePartialMessages: false,
12783
+ tools: [],
12784
+ maxTurns: 1
12785
+ })
12786
+ });
12787
+ const models = await query.supportedModels();
12788
+ const mapped = withClaudeCodeModelAliases(models.map(mapModelInfo));
12789
+ this.modelOptionsCache = mapped;
12790
+ return mapped;
12742
12791
  } catch {
12743
- return DEFAULT_CLAUDE_MODELS;
12792
+ return this.modelOptionsCache ?? DEFAULT_CLAUDE_MODELS;
12793
+ } finally {
12794
+ if (!active && query) {
12795
+ query.close();
12796
+ }
12744
12797
  }
12745
12798
  }
12746
12799
  async listSessions() {
@@ -16459,7 +16512,7 @@ var ThreadAuxiliaryStateStore = class {
16459
16512
  );
16460
16513
  return {
16461
16514
  id: record.id,
16462
- kind: "fastMode",
16515
+ kind: record.kind === "goal" ? "goal" : "fastMode",
16463
16516
  text: record.text,
16464
16517
  createdAt: record.createdAt,
16465
16518
  anchorTurnId: record.anchorTurnId ?? fallbackAnchor?.id ?? null
@@ -16673,13 +16726,10 @@ var ThreadGoalCoordinator = class {
16673
16726
  }
16674
16727
  try {
16675
16728
  await this.ensureGoalsFeatureEnabled(record.provider);
16676
- const activeGoal = this.listThreadGoalHistory(record.id).find(
16729
+ const goalHistoryBeforeUpdate = this.listThreadGoalHistory(record.id);
16730
+ const activeGoal = goalHistoryBeforeUpdate.find(
16677
16731
  (goal2) => ["active", "paused", "budgetLimited"].includes(goal2.status)
16678
16732
  ) ?? null;
16679
- const creatingNewGoal = goalObjectiveChanged(activeGoal, input.objective);
16680
- if (creatingNewGoal) {
16681
- markActiveThreadGoalRecordTerminated(this.db, record.id);
16682
- }
16683
16733
  if (input.status === "terminated") {
16684
16734
  const terminatedGoal = markActiveThreadGoalRecordTerminated(this.db, record.id);
16685
16735
  const goalHistory = this.listThreadGoalHistory(record.id);
@@ -16690,6 +16740,23 @@ var ThreadGoalCoordinator = class {
16690
16740
  });
16691
16741
  return goal2;
16692
16742
  }
16743
+ const startingNewGoal = shouldStartNewGoal(activeGoal, input.objective);
16744
+ if (startingNewGoal && (record.providerTurnId || record.status === "running")) {
16745
+ throw new HttpError(409, {
16746
+ code: "conflict",
16747
+ message: "Interrupt the running turn before replacing this goal."
16748
+ });
16749
+ }
16750
+ if (startingNewGoal && goalHistoryBeforeUpdate.length > 0) {
16751
+ if (!runtime.clearGoal) {
16752
+ throw new HttpError(409, {
16753
+ code: "conflict",
16754
+ message: "This backend cannot safely replace an existing goal."
16755
+ });
16756
+ }
16757
+ await runtime.clearGoal(providerSessionId);
16758
+ markActiveThreadGoalRecordTerminated(this.db, record.id);
16759
+ }
16693
16760
  const upstreamStatus = input.status;
16694
16761
  const goal = await runtime.setGoal({
16695
16762
  providerSessionId,
@@ -16701,10 +16768,15 @@ var ThreadGoalCoordinator = class {
16701
16768
  toThreadGoalDtoFromAgentGoal(goal),
16702
16769
  record
16703
16770
  );
16704
- const dto = creatingNewGoal ? resetGoalProgress(upstreamDto) : upstreamDto;
16771
+ const dto = startingNewGoal ? startFreshGoalLifecycle(upstreamDto) : upstreamDto;
16705
16772
  const persistedGoal = toThreadGoalDtoFromRecord(
16706
- this.persistThreadGoalSnapshot(record.id, dto)
16773
+ this.persistThreadGoalSnapshot(record.id, dto, {
16774
+ createNew: startingNewGoal
16775
+ })
16707
16776
  );
16777
+ if (startingNewGoal) {
16778
+ this.callbacks.appendGoalActivityNote(record.id, persistedGoal.objective);
16779
+ }
16708
16780
  this.callbacks.emitThreadEvent("thread.goal.updated", record.id, {
16709
16781
  goal: persistedGoal,
16710
16782
  goalHistory: this.listThreadGoalHistory(record.id)
@@ -16772,12 +16844,13 @@ var ThreadGoalCoordinator = class {
16772
16844
  throw error;
16773
16845
  }
16774
16846
  }
16775
- persistThreadGoalSnapshot(localThreadId, goal) {
16847
+ persistThreadGoalSnapshot(localThreadId, goal, options = {}) {
16776
16848
  const dto = "createdAt" in goal && typeof goal.createdAt === "string" ? goal : toThreadGoalDto(goal);
16777
16849
  return upsertThreadGoalRecord(this.db, {
16778
16850
  threadId: localThreadId,
16779
16851
  providerSessionId: dto.threadId,
16780
16852
  localGoalId: dto.localGoalId ?? null,
16853
+ createNew: options.createNew ?? false,
16781
16854
  objective: dto.objective,
16782
16855
  status: dto.status,
16783
16856
  tokenBudget: dto.tokenBudget,
@@ -16900,15 +16973,19 @@ function mergeGoalHistoryEntry(existing, incoming) {
16900
16973
  function goalHistoryStatusRank(status) {
16901
16974
  return ["active", "paused", "budgetLimited"].includes(status) ? 0 : 1;
16902
16975
  }
16903
- function resetGoalProgress(goal) {
16976
+ function startFreshGoalLifecycle(goal) {
16977
+ const now = (/* @__PURE__ */ new Date()).toISOString();
16904
16978
  return {
16905
16979
  ...goal,
16906
16980
  tokensUsed: 0,
16907
- timeUsedSeconds: 0
16981
+ timeUsedSeconds: 0,
16982
+ createdAt: now,
16983
+ updatedAt: now,
16984
+ completedAt: null
16908
16985
  };
16909
16986
  }
16910
- function goalObjectiveChanged(existing, nextObjective) {
16911
- return existing !== null && typeof nextObjective === "string" && nextObjective.trim().length > 0 && nextObjective !== existing.objective;
16987
+ function shouldStartNewGoal(existing, nextObjective) {
16988
+ return typeof nextObjective === "string" && nextObjective.trim().length > 0 && (existing === null || nextObjective !== existing.objective);
16912
16989
  }
16913
16990
  function isLocalGoalStatus(status) {
16914
16991
  return ["active", "paused", "budgetLimited"].includes(status);
@@ -18109,6 +18186,14 @@ var ThreadRuntimeEventProjector = class {
18109
18186
  if (!record) {
18110
18187
  return;
18111
18188
  }
18189
+ const currentGoal = await callbacks.getThreadGoalAfterRuntimeClear(record);
18190
+ if (currentGoal) {
18191
+ callbacks.emitThreadEvent("thread.goal.updated", record.id, {
18192
+ goal: currentGoal,
18193
+ goalHistory: callbacks.listThreadGoalHistory(record.id)
18194
+ });
18195
+ return;
18196
+ }
18112
18197
  markActiveThreadGoalRecordTerminated(db, record.id);
18113
18198
  callbacks.emitThreadEvent("thread.goal.cleared", record.id, {
18114
18199
  goalHistory: callbacks.listThreadGoalHistory(record.id)
@@ -22423,7 +22508,11 @@ var ThreadService = class {
22423
22508
  this.goalCoordinator = new ThreadGoalCoordinator(db, providerFeatures, {
22424
22509
  emitThreadEvent: (type, threadId, payload) => this.emitThreadEvent(type, threadId, payload),
22425
22510
  requireProviderSessionId: (record) => this.requireProviderSessionId(record),
22426
- runtimeForProvider: (provider2) => this.runtimeForProvider(provider2)
22511
+ runtimeForProvider: (provider2) => this.runtimeForProvider(provider2),
22512
+ appendGoalActivityNote: (threadId, objective) => this.auxiliaryState.appendActivityNote(threadId, {
22513
+ kind: "goal",
22514
+ text: objective
22515
+ })
22427
22516
  });
22428
22517
  this.auxiliaryState = new ThreadAuxiliaryStateStore(db, {
22429
22518
  cachedTurns: (localThreadId) => this.detailAssembler.cachedTurns(localThreadId),
@@ -22521,6 +22610,7 @@ var ThreadService = class {
22521
22610
  resetThreadContextUsage: (localThreadId, emitEvent) => this.resetThreadContextUsage(localThreadId, emitEvent),
22522
22611
  setThreadContextUsage: (localThreadId, usage, emitEvent) => this.setThreadContextUsage(localThreadId, usage, emitEvent),
22523
22612
  getThreadContextUsage: (localThreadId) => this.getThreadContextUsage(localThreadId),
22613
+ getThreadGoalAfterRuntimeClear: (record) => this.goalCoordinator.getThreadGoalForRecord(record),
22524
22614
  toThreadGoalDtoFromAgentGoal: (goal) => this.goalCoordinator.toThreadGoalDtoFromAgentGoal(goal),
22525
22615
  toThreadGoalDtoFromRecord: (record) => this.goalCoordinator.toThreadGoalDtoFromRecord(record)
22526
22616
  }