Skip to content

Commit bfcc048

Browse files
author
Garming
committed
fix(studio): synchronize intelligent task state
1 parent a11ee4a commit bfcc048

71 files changed

Lines changed: 551 additions & 413 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

frontend/server/intelligent_development_routes.py

Lines changed: 76 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -264,7 +264,11 @@ def _http_error(error: SandboxError) -> HTTPException:
264264
)
265265

266266

267-
def _public_session(session: SandboxCloudSession) -> dict[str, object]:
267+
def _public_session(
268+
session: SandboxCloudSession,
269+
*,
270+
busy: bool = False,
271+
) -> dict[str, object]:
268272
return {
269273
"sessionId": session.instance_id,
270274
"userSessionId": session.user_session_id,
@@ -276,6 +280,7 @@ def _public_session(session: SandboxCloudSession) -> dict[str, object]:
276280
"displayName": session.display_name,
277281
"persistent": False,
278282
"toolName": INTELLIGENT_DEVELOPMENT_TOOL_NAME,
283+
"busy": busy,
279284
}
280285

281286

@@ -836,6 +841,12 @@ def mount_intelligent_development_routes(
836841
delegated = FastAPI()
837842
task_locks: dict[tuple[str, str], asyncio.Lock] = {}
838843
task_locks_guard = asyncio.Lock()
844+
845+
async def task_active(owner_id: str, session_id: str) -> bool:
846+
async with task_locks_guard:
847+
task_lock = task_locks.get((owner_id, session_id))
848+
return task_lock is not None and task_lock.locked()
849+
839850
mount_sandbox_routes(
840851
delegated,
841852
service,
@@ -898,13 +909,17 @@ async def _list(request: Request) -> dict[str, object]:
898909
sessions = await service.list_sessions(owner, is_admin=False)
899910
except SandboxError as error:
900911
raise _http_error(error) from error
901-
return {
902-
"sessions": [
903-
_public_session(session)
904-
for session in sessions
905-
if session.agent_kind == INTELLIGENT_DEVELOPMENT_AGENT_KIND
906-
]
907-
}
912+
public_sessions = []
913+
for session in sessions:
914+
if session.agent_kind != INTELLIGENT_DEVELOPMENT_AGENT_KIND:
915+
continue
916+
public_sessions.append(
917+
_public_session(
918+
session,
919+
busy=await task_active(owner, session.instance_id),
920+
)
921+
)
922+
return {"sessions": public_sessions}
908923

909924
@app.post(f"{INTELLIGENT_DEVELOPMENT_PREFIX}/sessions")
910925
async def _create(request: Request) -> dict[str, object]:
@@ -1008,46 +1023,70 @@ async def _connect(session_id: str, request: Request) -> dict[str, object]:
10081023
service, session_id, owner
10091024
)
10101025
workspace = _workspace(cloud)
1011-
conversation = await service.connect(session_id, owner, is_admin=False)
1012-
_require_development_session(conversation.cloud)
1013-
if not conversation.codex.workspace_locked:
1014-
await _prepare_workspace(conversation.cloud)
1015-
if project_service is not None:
1016-
try:
1017-
await project_service.restore_base_version(
1018-
owner_id=owner,
1019-
session_id=session_id,
1020-
endpoint=conversation.cloud.endpoint,
1021-
workspace=workspace,
1022-
)
1023-
except IntelligentDevelopmentProjectNotFound:
1024-
# Sessions created before project persistence have no binding.
1025-
pass
1026-
await service.update_workspace(session_id, owner, workspace)
1027-
elif conversation.codex.cwd != workspace:
1028-
raise SandboxSessionUnavailableError("开发会话已在非预期工作空间启动。")
1029-
if conversation.codex.permissions != _BUILDER_PERMISSIONS:
1030-
await service.update_permissions(
1026+
if await task_active(owner, session_id):
1027+
conversation = service._owned(session_id, owner)
1028+
settings = service.settings(session_id, owner)
1029+
busy = True
1030+
restored = None
1031+
else:
1032+
conversation = await service.connect(session_id, owner, is_admin=False)
1033+
_require_development_session(conversation.cloud)
1034+
if not conversation.codex.workspace_locked:
1035+
await _prepare_workspace(conversation.cloud)
1036+
if project_service is not None:
1037+
try:
1038+
await project_service.restore_base_version(
1039+
owner_id=owner,
1040+
session_id=session_id,
1041+
endpoint=conversation.cloud.endpoint,
1042+
workspace=workspace,
1043+
)
1044+
except IntelligentDevelopmentProjectNotFound:
1045+
# Sessions created before project persistence have no binding.
1046+
pass
1047+
await service.update_workspace(session_id, owner, workspace)
1048+
elif conversation.codex.cwd != workspace:
1049+
raise SandboxSessionUnavailableError(
1050+
"开发会话已在非预期工作空间启动。"
1051+
)
1052+
if conversation.codex.permissions != _BUILDER_PERMISSIONS:
1053+
await service.update_permissions(
1054+
session_id,
1055+
owner,
1056+
_BUILDER_PERMISSIONS,
1057+
)
1058+
settings = service.settings(session_id, owner)
1059+
busy = bool(settings.get("busy"))
1060+
restored = await _restore_latest_conversation(
1061+
service,
10311062
session_id,
10321063
owner,
1033-
_BUILDER_PERMISSIONS,
1064+
busy=busy,
10341065
)
1035-
restored = await _restore_latest_conversation(
1036-
service,
1037-
session_id,
1038-
owner,
1039-
busy=conversation.codex.active,
1040-
)
1066+
settings = service.settings(session_id, owner)
10411067
except SandboxError as error:
10421068
raise _http_error(error) from error
10431069
except PROJECT_EXCEPTIONS as error:
10441070
raise project_http_error(error) from error
10451071
return {
10461072
**_public_session(conversation.cloud),
1047-
**service.settings(session_id, owner),
1073+
**settings,
1074+
"busy": busy,
10481075
**({"conversation": restored} if restored is not None else {}),
10491076
}
10501077

1078+
@app.get(f"{INTELLIGENT_DEVELOPMENT_PREFIX}/sessions/{{session_id}}/status")
1079+
async def _status(session_id: str, request: Request) -> dict[str, object]:
1080+
owner = owner_resolver(request)
1081+
try:
1082+
status = service.status(session_id, owner)
1083+
except SandboxError as error:
1084+
raise _http_error(error) from error
1085+
return {
1086+
**status,
1087+
"busy": bool(status.get("busy")) or await task_active(owner, session_id),
1088+
}
1089+
10511090
@app.get(f"{INTELLIGENT_DEVELOPMENT_PREFIX}/releases/current")
10521091
async def _current_release(
10531092
request: Request,
@@ -1316,6 +1355,7 @@ async def cleanup_task_files() -> None:
13161355

13171356
try:
13181357
failure_stage = "task_prepare"
1358+
yield _progress_sse("Codex 正在处理本次请求。")
13191359
transport = SandboxRemoteTransport(cloud.endpoint)
13201360
completion_path = (
13211361
f"{project_root}/{COMPLETION_FILE_PREFIX}{uuid4().hex}.json"
@@ -1325,7 +1365,6 @@ async def cleanup_task_files() -> None:
13251365
lease = await create_credential_lease(
13261366
cloud.endpoint, credential_resolver
13271367
)
1328-
yield _progress_sse("Codex 正在处理本次请求。")
13291368
delivery = None
13301369
failure_stage = "codex_turn"
13311370
async for event in service.stream_message(

frontend/src/App.tsx

Lines changed: 47 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -182,6 +182,7 @@ import {
182182
intelligentDevelopmentErrorMessage,
183183
intelligentDevelopmentClient,
184184
sandboxClient,
185+
SandboxServiceError,
185186
type SandboxApproval,
186187
type SandboxApprovalDecision,
187188
type SandboxAgentResource,
@@ -1782,12 +1783,15 @@ export default function App() {
17821783

17831784
const syncBackgroundTurn = async () => {
17841785
try {
1785-
const status = await sandboxClient.getStatus(activeSession.id, {
1786+
const backgroundClient = activeSession.intelligentDevelopment
1787+
? intelligentDevelopmentClient
1788+
: sandboxClient;
1789+
const status = await backgroundClient.getStatus(activeSession.id, {
17861790
signal: controller.signal,
17871791
});
17881792
if (stopped || sandboxSessionIdRef.current !== activeSession.id) return;
17891793
const snapshot = status.threadId
1790-
? await sandboxClient.readThread(activeSession.id, status.threadId, {
1794+
? await backgroundClient.readThread(activeSession.id, status.threadId, {
17911795
signal: controller.signal,
17921796
})
17931797
: null;
@@ -1822,6 +1826,11 @@ export default function App() {
18221826
}
18231827
} catch (cause) {
18241828
if ((cause as Error)?.name === "AbortError" || stopped) return;
1829+
if (activeSession.intelligentDevelopment) {
1830+
setError(intelligentDevelopmentErrorMessage(cause));
1831+
timer = window.setTimeout(syncBackgroundTurn, 1500);
1832+
return;
1833+
}
18251834
setSandboxBusy(false);
18261835
setSandboxSession((current) =>
18271836
current?.id === activeSession.id ? { ...current, busy: false } : current
@@ -3654,6 +3663,7 @@ export default function App() {
36543663
setSandboxTurns(restoredTurns);
36553664
sandboxSessionIdRef.current = connected.id;
36563665
setSandboxSession(connected);
3666+
setSandboxBusy(connected.busy);
36573667
setCreateView(null);
36583668
setSkillCenter(false);
36593669
setAddAgent(false);
@@ -4266,6 +4276,7 @@ export default function App() {
42664276
const activeClient = activeSession.intelligentDevelopment
42674277
? intelligentDevelopmentClient
42684278
: sandboxClient;
4279+
let remainingBusy = false;
42694280
try {
42704281
const reply = await activeClient.sendMessage(
42714282
{
@@ -4392,25 +4403,33 @@ export default function App() {
43924403
setInput(text);
43934404
setAttachments(messageAttachments);
43944405
sandboxCommands.setSelectedSkills(selectedSkills);
4395-
setError(
4396-
activeSession.intelligentDevelopment
4397-
? intelligentDevelopmentErrorMessage(messageError)
4398-
: `内置智能体发送失败:${
4399-
messageError instanceof Error
4400-
? messageError.message
4401-
: String(messageError)
4402-
}`,
4403-
);
4406+
const taskStillRunning =
4407+
activeSession.intelligentDevelopment &&
4408+
messageError instanceof SandboxServiceError &&
4409+
messageError.code === "INTELLIGENT_DEVELOPMENT_TASK_IN_PROGRESS";
4410+
remainingBusy = activeSession.intelligentDevelopment;
44044411
try {
4405-
const settings = await activeClient.getSettings(activeSession.id);
4412+
const status = await activeClient.getStatus(activeSession.id);
4413+
remainingBusy = status.busy;
44064414
setSandboxSession((current) =>
44074415
current?.id === activeSession.id
4408-
? { ...current, ...settings }
4416+
? { ...current, ...status }
44094417
: current,
44104418
);
44114419
} catch {
44124420
// Keep the optimistic lock when the connection itself is unavailable.
44134421
}
4422+
if (!taskStillRunning) {
4423+
setError(
4424+
activeSession.intelligentDevelopment
4425+
? intelligentDevelopmentErrorMessage(messageError)
4426+
: `内置智能体发送失败:${
4427+
messageError instanceof Error
4428+
? messageError.message
4429+
: String(messageError)
4430+
}`,
4431+
);
4432+
}
44144433
} finally {
44154434
if (sandboxMessageAbortRef.current === controller) {
44164435
const stopWait = sandboxStopWaitRef.current;
@@ -4431,13 +4450,22 @@ export default function App() {
44314450
if (sandboxActiveAssistantTurnIdRef.current === assistantTurnId) {
44324451
sandboxActiveAssistantTurnIdRef.current = "";
44334452
}
4434-
setSandboxBusy(false);
44354453
setSandboxApproval(null);
4436-
setSandboxSession((current) =>
4437-
current?.id === activeSession.id
4438-
? { ...current, busy: false }
4439-
: current,
4440-
);
4454+
if (activeSession.intelligentDevelopment) {
4455+
setSandboxBusy(remainingBusy);
4456+
setSandboxSession((current) =>
4457+
current?.id === activeSession.id
4458+
? { ...current, busy: remainingBusy }
4459+
: current,
4460+
);
4461+
} else {
4462+
setSandboxBusy(false);
4463+
setSandboxSession((current) =>
4464+
current?.id === activeSession.id
4465+
? { ...current, busy: false }
4466+
: current,
4467+
);
4468+
}
44414469
}
44424470
}
44434471
}

frontend/tests/intelligentDevelopment.test.mjs

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -400,6 +400,21 @@ test("intelligent development errors preserve specific recovery guidance", async
400400
);
401401
});
402402

403+
test("intelligent busy state follows the backend across reconnect and recovery", () => {
404+
assert.match(
405+
appSource,
406+
/function activateIntelligentDevelopmentSession[\s\S]*?setSandboxBusy\(connected\.busy\)/,
407+
);
408+
assert.match(
409+
appSource,
410+
/const backgroundClient = activeSession\.intelligentDevelopment[\s\S]*?backgroundClient\.getStatus[\s\S]*?backgroundClient\.readThread/,
411+
);
412+
assert.match(
413+
appSource,
414+
/let remainingBusy = false[\s\S]*?remainingBusy = activeSession\.intelligentDevelopment[\s\S]*?activeClient\.getStatus[\s\S]*?remainingBusy = status\.busy[\s\S]*?setSandboxBusy\(remainingBusy\)/,
415+
);
416+
});
417+
403418
test("source-ready delivery is upgraded in place only by the verified event", async (t) => {
404419
const previousFetch = globalThis.fetch;
405420
const writes = [];

frontend/tests/sandboxCodexControls.test.mjs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,12 @@ test("active Codex Sandbox threads replace normal history in the Sidebar", () =>
8989
test("a running handoff session is opened with live busy-state recovery", () => {
9090
assert.match(appSource, /setSandboxBusy\(connected\.busy\)/);
9191
assert.match(appSource, /sandboxSnapshotTurnsForStatus\(snapshot, connected\.busy\)/);
92-
assert.match(appSource, /sandboxClient\.getStatus\(activeSession\.id/);
93-
assert.match(appSource, /sandboxClient\.readThread\(activeSession\.id, status\.threadId/);
92+
assert.match(
93+
appSource,
94+
/const backgroundClient = activeSession\.intelligentDevelopment[\s\S]*?intelligentDevelopmentClient[\s\S]*?: sandboxClient/,
95+
);
96+
assert.match(appSource, /backgroundClient\.getStatus\(activeSession\.id/);
97+
assert.match(appSource, /backgroundClient\.readThread\(activeSession\.id, status\.threadId/);
9498
assert.match(appSource, /setSandboxBusy\(status\.busy\)/);
9599
assert.match(appSource, /window\.setTimeout\(syncBackgroundTurn, 1500\)/);
96100
});

0 commit comments

Comments
 (0)