Persist explicit user goal edits in model history (#49598)

## Why

User goal edits and clears need to reach the model as user instructions, including when a thread is unloaded or history is being compacted. Automatic goal lifecycle updates must not supply user authorization.

## What changed

- Add optional `origin` values of `user` and `automatic` to `thread/goal/set` and `thread/goal/clear`, and mark TUI goal requests as user actions. Omitted origins do not supply user authorization.
- Persist explicit user objective, status, and clear instructions before changing goal state. Recording failures leave goal state unchanged.
- Coordinate goal edits with thread resume, reload stored history under writer ownership, and preserve edits accepted after the compaction input snapshot.
- Update generated protocol schemas and TypeScript and Python bindings.

## Testing

Add coverage for user edits and clears on loaded and unloaded threads, concurrent path-based resume, exclusion of automatic goals from user instructions, preservation of edits during compaction, and recording failures that leave goal state unchanged.

GitOrigin-RevId: d779d7c678a0c32a6ef76975dbe5b1400d1df245
This commit is contained in:
felixxia-oai
2026-09-30 11:58:03 +00:00
committed by copyberry
parent 1fc8d54807
commit de02016798
35 changed files with 698 additions and 109 deletions
+29 -16
View File
@@ -5247,13 +5247,6 @@ class ThreadExtra(BaseModel):
)
class ThreadGoalClearParams(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
thread_id: Annotated[str, Field(alias="threadId")]
class ThreadGoalClearResponse(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -5275,6 +5268,11 @@ class ThreadGoalGetParams(BaseModel):
thread_id: Annotated[str, Field(alias="threadId")]
class ThreadGoalMutationOrigin(Enum):
user = "user"
automatic = "automatic"
class ThreadGoalStatus(Enum):
active = "active"
paused = "paused"
@@ -6867,15 +6865,6 @@ class ThreadGoalGetRequest(BaseModel):
params: ThreadGoalGetParams
class ThreadGoalClearRequest(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
id: RequestId
method: Annotated[Literal["thread/goal/clear"], Field(title="Thread/goal/clearRequestMethod")]
params: ThreadGoalClearParams
class ThreadMetadataUpdateRequest(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -9812,6 +9801,17 @@ class ThreadGoal(BaseModel):
updated_at: Annotated[int, Field(alias="updatedAt")]
class ThreadGoalClearParams(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
origin: Annotated[
ThreadGoalMutationOrigin | None,
Field(description="Missing provenance does not supply user authorization."),
] = None
thread_id: Annotated[str, Field(alias="threadId")]
class ThreadGoalGetResponse(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
@@ -9824,6 +9824,10 @@ class ThreadGoalSetParams(BaseModel):
populate_by_name=True,
)
objective: str | None = None
origin: Annotated[
ThreadGoalMutationOrigin | None,
Field(description="Missing provenance does not supply user authorization."),
] = None
status: ThreadGoalStatus | None = None
thread_id: Annotated[str, Field(alias="threadId")]
token_budget: Annotated[int | None, Field(alias="tokenBudget")] = None
@@ -10524,6 +10528,15 @@ class ThreadGoalSetRequest(BaseModel):
params: ThreadGoalSetParams
class ThreadGoalClearRequest(BaseModel):
model_config = ConfigDict(
populate_by_name=True,
)
id: RequestId
method: Annotated[Literal["thread/goal/clear"], Field(title="Thread/goal/clearRequestMethod")]
params: ThreadGoalClearParams
class ThreadListRequest(BaseModel):
model_config = ConfigDict(
populate_by_name=True,