remote-codex 0.11.40 → 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;
@@ -11717,7 +11717,40 @@ import { pathToFileURL } from "url";
11717
11717
  import { promisify } from "util";
11718
11718
  var execFileAsync = promisify(execFile);
11719
11719
  var promptPhotoTokenPattern2 = /\[PHOTO\s+([^\]]+)\]/g;
11720
+ var claudeCompactSummaryPrefix = "This session is being continued from a previous conversation that ran out of context.";
11720
11721
  var activeTranscriptMatchWindowMs = 12e4;
11722
+ function isClaudeCompactBoundaryMessage(message) {
11723
+ if (message.type !== "system") {
11724
+ return false;
11725
+ }
11726
+ if (message.subtype === "compact_boundary" || message.compact_metadata) {
11727
+ return true;
11728
+ }
11729
+ return isRecord8(message.message) && (message.message.subtype === "compact_boundary" || isRecord8(message.message.compact_metadata));
11730
+ }
11731
+ function isClaudeCompactSummaryMessage(message) {
11732
+ if (message.type !== "user" || message.parent_tool_use_id) {
11733
+ return false;
11734
+ }
11735
+ const payload = isRecord8(message.message) ? message.message : null;
11736
+ if (message.isCompactSummary || message.is_compact_summary || payload?.isCompactSummary === true || payload?.is_compact_summary === true) {
11737
+ return true;
11738
+ }
11739
+ return messageContentText(message.message).trim().startsWith(claudeCompactSummaryPrefix);
11740
+ }
11741
+ function claudeContextCompactionItem(id, status, error = null) {
11742
+ const completed = status === "completed";
11743
+ const failed = status === "failed";
11744
+ const text2 = failed ? "Context compaction failed" : completed ? "Context compacted" : "Compacting context";
11745
+ return {
11746
+ id,
11747
+ kind: "contextCompaction",
11748
+ text: text2,
11749
+ previewText: text2,
11750
+ detailText: failed ? error : null,
11751
+ status
11752
+ };
11753
+ }
11721
11754
  function normalizePromptForTurnReconciliation(value) {
11722
11755
  return value.replace(/\[PHOTO\s+[^\]]+\]/g, " ").replace(/\s+/g, " ").trim();
11723
11756
  }
@@ -12923,7 +12956,8 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
12923
12956
  suppressedToolUseIds: /* @__PURE__ */ new Set(),
12924
12957
  assistantUsage: null,
12925
12958
  resultUsage: null,
12926
- modelContextWindow: null
12959
+ modelContextWindow: null,
12960
+ currentCompactionItemId: null
12927
12961
  };
12928
12962
  this.knownSessionIds.add(input.providerSessionId);
12929
12963
  let sessionPrompts = this.liveUserPrompts.get(input.providerSessionId);
@@ -13147,6 +13181,53 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
13147
13181
  }
13148
13182
  return;
13149
13183
  }
13184
+ if (message.type === "system" && message.subtype === "status") {
13185
+ if (message.status === "compacting") {
13186
+ const existing = state.currentCompactionItemId ? state.items.get(state.currentCompactionItemId) : null;
13187
+ const itemId = existing?.status === "running" ? existing.id : `claude-compaction-${messageUuid(message, randomUUID2())}`;
13188
+ state.currentCompactionItemId = itemId;
13189
+ const item = withHistoryItemCreatedAt(
13190
+ claudeContextCompactionItem(itemId, "running"),
13191
+ messageCreatedAt
13192
+ );
13193
+ addOrUpdateItem(state, item);
13194
+ this.emitItem(state, item, "item.started", { force: Boolean(existing) });
13195
+ return;
13196
+ }
13197
+ if (message.compact_result) {
13198
+ const itemId = state.currentCompactionItemId ?? `claude-compaction-${messageUuid(message, randomUUID2())}`;
13199
+ const status = message.compact_result === "success" ? "completed" : "failed";
13200
+ const item = withHistoryItemCreatedAt(
13201
+ claudeContextCompactionItem(itemId, status, message.compact_error ?? null),
13202
+ messageCreatedAt
13203
+ );
13204
+ addOrUpdateItem(state, item);
13205
+ this.emitItem(state, item, "item.completed");
13206
+ state.currentCompactionItemId = status === "failed" ? null : itemId;
13207
+ return;
13208
+ }
13209
+ return;
13210
+ }
13211
+ if (isClaudeCompactBoundaryMessage(message)) {
13212
+ const itemId = state.currentCompactionItemId ?? `claude-compaction-${messageUuid(message, randomUUID2())}`;
13213
+ const previous = state.items.get(itemId);
13214
+ const item = withHistoryItemCreatedAt(
13215
+ claudeContextCompactionItem(itemId, "completed"),
13216
+ previous?.createdAt ?? messageCreatedAt
13217
+ );
13218
+ addOrUpdateItem(state, item);
13219
+ if (!previous) {
13220
+ this.emitItem(state, item, "item.started");
13221
+ }
13222
+ if (previous?.status !== "completed") {
13223
+ this.emitItem(state, item, "item.completed");
13224
+ }
13225
+ state.currentCompactionItemId = null;
13226
+ return;
13227
+ }
13228
+ if (isClaudeCompactSummaryMessage(message)) {
13229
+ return;
13230
+ }
13150
13231
  if (message.type === "stream_event") {
13151
13232
  const nextStreamMessageId = streamMessageId(message.event);
13152
13233
  if (nextStreamMessageId) {
@@ -13498,6 +13579,8 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
13498
13579
  const turns = [];
13499
13580
  let current = null;
13500
13581
  let skippingHiddenInit = false;
13582
+ let pendingCompactSummaryItemId = null;
13583
+ let pendingCompactBoundaryItemId = null;
13501
13584
  const suppressedToolUseIds = /* @__PURE__ */ new Set();
13502
13585
  const upsertCurrentItem = (item) => {
13503
13586
  if (!current) {
@@ -13529,6 +13612,18 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
13529
13612
  current.itemsById.set(item.id, item);
13530
13613
  };
13531
13614
  for (const message of messages) {
13615
+ if (isClaudeCompactBoundaryMessage(message)) {
13616
+ const itemId = pendingCompactSummaryItemId ?? `claude-compaction-${messageUuid(message, randomUUID2())}`;
13617
+ pendingCompactSummaryItemId = null;
13618
+ pendingCompactBoundaryItemId = itemId;
13619
+ upsertCurrentItem(
13620
+ withHistoryItemCreatedAt(
13621
+ claudeContextCompactionItem(itemId, "completed"),
13622
+ sessionMessageTimestamp(message) ?? current?.startedAt
13623
+ )
13624
+ );
13625
+ continue;
13626
+ }
13532
13627
  if (message.type === "user") {
13533
13628
  const taskNotification = taskNotificationToolResult(message.message);
13534
13629
  if (taskNotification) {
@@ -13573,6 +13668,20 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
13573
13668
  }
13574
13669
  }
13575
13670
  if (message.type === "user" && !message.parent_tool_use_id) {
13671
+ if (isClaudeCompactSummaryMessage(message)) {
13672
+ const itemId = pendingCompactBoundaryItemId ?? `claude-compaction-${messageUuid(message, randomUUID2())}`;
13673
+ if (!pendingCompactBoundaryItemId) {
13674
+ pendingCompactSummaryItemId = itemId;
13675
+ }
13676
+ pendingCompactBoundaryItemId = null;
13677
+ upsertCurrentItem(
13678
+ withHistoryItemCreatedAt(
13679
+ claudeContextCompactionItem(itemId, "completed"),
13680
+ sessionMessageTimestamp(message) ?? current?.startedAt
13681
+ )
13682
+ );
13683
+ continue;
13684
+ }
13576
13685
  if (isHiddenInitMessage(message.message)) {
13577
13686
  skippingHiddenInit = true;
13578
13687
  current = null;
@@ -13592,19 +13701,19 @@ var ClaudeRuntimeAdapter = class extends EventEmitter5 {
13592
13701
  items: current.items
13593
13702
  }));
13594
13703
  }
13595
- const messageUuid2 = message.uuid ?? randomUUID2();
13596
- const userStartedAt = isoFromUuidV7(messageUuid2);
13704
+ const userMessageUuid = message.uuid ?? randomUUID2();
13705
+ const userStartedAt = isoFromUuidV7(userMessageUuid);
13597
13706
  const userItem = await this.userMessageToHistoryItem(
13598
- messageUuid2,
13707
+ userMessageUuid,
13599
13708
  message.message,
13600
13709
  context
13601
13710
  );
13602
13711
  const stampedUserItem = withHistoryItemCreatedAt(userItem, userStartedAt);
13603
13712
  current = {
13604
- providerTurnId: `claude-turn-${messageUuid2}`,
13713
+ providerTurnId: `claude-turn-${userMessageUuid}`,
13605
13714
  startedAt: userStartedAt,
13606
13715
  items: [stampedUserItem],
13607
- itemsById: /* @__PURE__ */ new Map([[messageUuid2, stampedUserItem]])
13716
+ itemsById: /* @__PURE__ */ new Map([[userMessageUuid, stampedUserItem]])
13608
13717
  };
13609
13718
  continue;
13610
13719
  }
@@ -16350,7 +16459,7 @@ var ThreadAuxiliaryStateStore = class {
16350
16459
  );
16351
16460
  return {
16352
16461
  id: record.id,
16353
- kind: "fastMode",
16462
+ kind: record.kind === "goal" ? "goal" : "fastMode",
16354
16463
  text: record.text,
16355
16464
  createdAt: record.createdAt,
16356
16465
  anchorTurnId: record.anchorTurnId ?? fallbackAnchor?.id ?? null
@@ -16564,13 +16673,10 @@ var ThreadGoalCoordinator = class {
16564
16673
  }
16565
16674
  try {
16566
16675
  await this.ensureGoalsFeatureEnabled(record.provider);
16567
- const activeGoal = this.listThreadGoalHistory(record.id).find(
16676
+ const goalHistoryBeforeUpdate = this.listThreadGoalHistory(record.id);
16677
+ const activeGoal = goalHistoryBeforeUpdate.find(
16568
16678
  (goal2) => ["active", "paused", "budgetLimited"].includes(goal2.status)
16569
16679
  ) ?? null;
16570
- const creatingNewGoal = goalObjectiveChanged(activeGoal, input.objective);
16571
- if (creatingNewGoal) {
16572
- markActiveThreadGoalRecordTerminated(this.db, record.id);
16573
- }
16574
16680
  if (input.status === "terminated") {
16575
16681
  const terminatedGoal = markActiveThreadGoalRecordTerminated(this.db, record.id);
16576
16682
  const goalHistory = this.listThreadGoalHistory(record.id);
@@ -16581,6 +16687,23 @@ var ThreadGoalCoordinator = class {
16581
16687
  });
16582
16688
  return goal2;
16583
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
+ }
16584
16707
  const upstreamStatus = input.status;
16585
16708
  const goal = await runtime.setGoal({
16586
16709
  providerSessionId,
@@ -16592,10 +16715,15 @@ var ThreadGoalCoordinator = class {
16592
16715
  toThreadGoalDtoFromAgentGoal(goal),
16593
16716
  record
16594
16717
  );
16595
- const dto = creatingNewGoal ? resetGoalProgress(upstreamDto) : upstreamDto;
16718
+ const dto = startingNewGoal ? startFreshGoalLifecycle(upstreamDto) : upstreamDto;
16596
16719
  const persistedGoal = toThreadGoalDtoFromRecord(
16597
- this.persistThreadGoalSnapshot(record.id, dto)
16720
+ this.persistThreadGoalSnapshot(record.id, dto, {
16721
+ createNew: startingNewGoal
16722
+ })
16598
16723
  );
16724
+ if (startingNewGoal) {
16725
+ this.callbacks.appendGoalActivityNote(record.id, persistedGoal.objective);
16726
+ }
16599
16727
  this.callbacks.emitThreadEvent("thread.goal.updated", record.id, {
16600
16728
  goal: persistedGoal,
16601
16729
  goalHistory: this.listThreadGoalHistory(record.id)
@@ -16663,12 +16791,13 @@ var ThreadGoalCoordinator = class {
16663
16791
  throw error;
16664
16792
  }
16665
16793
  }
16666
- persistThreadGoalSnapshot(localThreadId, goal) {
16794
+ persistThreadGoalSnapshot(localThreadId, goal, options = {}) {
16667
16795
  const dto = "createdAt" in goal && typeof goal.createdAt === "string" ? goal : toThreadGoalDto(goal);
16668
16796
  return upsertThreadGoalRecord(this.db, {
16669
16797
  threadId: localThreadId,
16670
16798
  providerSessionId: dto.threadId,
16671
16799
  localGoalId: dto.localGoalId ?? null,
16800
+ createNew: options.createNew ?? false,
16672
16801
  objective: dto.objective,
16673
16802
  status: dto.status,
16674
16803
  tokenBudget: dto.tokenBudget,
@@ -16791,15 +16920,19 @@ function mergeGoalHistoryEntry(existing, incoming) {
16791
16920
  function goalHistoryStatusRank(status) {
16792
16921
  return ["active", "paused", "budgetLimited"].includes(status) ? 0 : 1;
16793
16922
  }
16794
- function resetGoalProgress(goal) {
16923
+ function startFreshGoalLifecycle(goal) {
16924
+ const now = (/* @__PURE__ */ new Date()).toISOString();
16795
16925
  return {
16796
16926
  ...goal,
16797
16927
  tokensUsed: 0,
16798
- timeUsedSeconds: 0
16928
+ timeUsedSeconds: 0,
16929
+ createdAt: now,
16930
+ updatedAt: now,
16931
+ completedAt: null
16799
16932
  };
16800
16933
  }
16801
- function goalObjectiveChanged(existing, nextObjective) {
16802
- 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);
16803
16936
  }
16804
16937
  function isLocalGoalStatus(status) {
16805
16938
  return ["active", "paused", "budgetLimited"].includes(status);
@@ -18000,6 +18133,14 @@ var ThreadRuntimeEventProjector = class {
18000
18133
  if (!record) {
18001
18134
  return;
18002
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
+ }
18003
18144
  markActiveThreadGoalRecordTerminated(db, record.id);
18004
18145
  callbacks.emitThreadEvent("thread.goal.cleared", record.id, {
18005
18146
  goalHistory: callbacks.listThreadGoalHistory(record.id)
@@ -22314,7 +22455,11 @@ var ThreadService = class {
22314
22455
  this.goalCoordinator = new ThreadGoalCoordinator(db, providerFeatures, {
22315
22456
  emitThreadEvent: (type, threadId, payload) => this.emitThreadEvent(type, threadId, payload),
22316
22457
  requireProviderSessionId: (record) => this.requireProviderSessionId(record),
22317
- 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
+ })
22318
22463
  });
22319
22464
  this.auxiliaryState = new ThreadAuxiliaryStateStore(db, {
22320
22465
  cachedTurns: (localThreadId) => this.detailAssembler.cachedTurns(localThreadId),
@@ -22412,6 +22557,7 @@ var ThreadService = class {
22412
22557
  resetThreadContextUsage: (localThreadId, emitEvent) => this.resetThreadContextUsage(localThreadId, emitEvent),
22413
22558
  setThreadContextUsage: (localThreadId, usage, emitEvent) => this.setThreadContextUsage(localThreadId, usage, emitEvent),
22414
22559
  getThreadContextUsage: (localThreadId) => this.getThreadContextUsage(localThreadId),
22560
+ getThreadGoalAfterRuntimeClear: (record) => this.goalCoordinator.getThreadGoalForRecord(record),
22415
22561
  toThreadGoalDtoFromAgentGoal: (goal) => this.goalCoordinator.toThreadGoalDtoFromAgentGoal(goal),
22416
22562
  toThreadGoalDtoFromRecord: (record) => this.goalCoordinator.toThreadGoalDtoFromRecord(record)
22417
22563
  }