| author | |
| committer | |
| log | e1ad595e0f10cc16d66fe54b4ee195435dd3cc0d |
| tree | 55c1f0d12ba1002566eccd953a1587dbe2709c40 |
| parent | 4606f9d6f213952124d93792854b0c4e48658e60 |
| signature | Signed by PGP key B5690EEEBB952194 |
7 files changed, 121 insertions(+), 14 deletions(-)
apps/server/src/orchestration-v2/Adapters/OpenCode2AdapterV2.test.ts+65-1| ... | @@ -79,8 +79,11 @@ const mcpRules = [ | ... | @@ -79,8 +79,11 @@ const mcpRules = [ |
| 79 | { action: "t3-code-thread_opencode2-adapter_*", resource: "*", effect: "allow" }, | 79 | { action: "t3-code-thread_opencode2-adapter_*", resource: "*", effect: "allow" }, |
| 80 | ]; | 80 | ]; |
| 81 | const t3Rules = [{ action: "*", resource: "*", effect: "allow" }, ...mcpRules]; | 81 | const t3Rules = [{ action: "*", resource: "*", effect: "allow" }, ...mcpRules]; |
| 82 | /** The thread's title, which the recorded session already carries. */ | ||
| 83 | const TITLE = "Prime check"; | ||
| 82 | const sessionInfo = (overrides: Record<string, unknown> = {}) => ({ | 84 | const sessionInfo = (overrides: Record<string, unknown> = {}) => ({ |
| 83 | id: SESSION, | 85 | id: SESSION, |
| 86 | title: TITLE, | ||
| 84 | permissions: t3Rules, | 87 | permissions: t3Rules, |
| 85 | projectID: "global", | 88 | projectID: "global", |
| 86 | model: { id: "big-pickle", providerID: "opencode", variant: "default" }, | 89 | model: { id: "big-pickle", providerID: "opencode", variant: "default" }, |
| ... | @@ -226,7 +229,7 @@ const turnInput = ( | ... | @@ -226,7 +229,7 @@ const turnInput = ( |
| 226 | modelSelection: ModelSelection = bigPickle, | 229 | modelSelection: ModelSelection = bigPickle, |
| 227 | runtimeMode: "full-access" | "approval-required" = "full-access", | 230 | runtimeMode: "full-access" | "approval-required" = "full-access", |
| 228 | ) => ({ | 231 | ) => ({ |
| 229 | appThread: {} as OrchestrationV2AppThread, | 232 | appThread: { title: TITLE } as OrchestrationV2AppThread, |
| 230 | threadId, | 233 | threadId, |
| 231 | runId: RunId.make("run:opencode2-adapter"), | 234 | runId: RunId.make("run:opencode2-adapter"), |
| 232 | runOrdinal: 1, | 235 | runOrdinal: 1, |
| ... | @@ -433,6 +436,66 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { | ... | @@ -433,6 +436,66 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { |
| 433 | }).pipe(Effect.scoped), | 436 | }).pipe(Effect.scoped), |
| 434 | ); | 437 | ); |
| 435 | 438 | ||
| 439 | it.effect("creates the session with the thread's title, so OpenCode generates none", () => | ||
| 440 | Effect.gen(function* () { | ||
| 441 | const runtime = yield* openCode2ReplayRuntimeWithInstructions([ | ||
| 442 | ...opening, | ||
| 443 | out("session.create", { | ||
| 444 | location: { directory: WORK }, | ||
| 445 | model: { providerID: "opencode", id: "big-pickle" }, | ||
| 446 | permissions: t3Rules, | ||
| 447 | title: "Check whether 391 is prime", | ||
| 448 | }), | ||
| 449 | replyData("session.create", sessionInfo({ title: "Check whether 391 is prime" })), | ||
| 450 | out("session.prompt", { sessionID: SESSION, text: "<any>" }), | ||
| 451 | promptAccepted, | ||
| 452 | event("session.execution.succeeded", { sessionID: SESSION }), | ||
| 453 | ]); | ||
| 454 | const thread = yield* runtime.ensureThread({ | ||
| 455 | threadId, | ||
| 456 | title: " Check whether 391 is prime ", | ||
| 457 | modelSelection: bigPickle, | ||
| 458 | runtimePolicy: policy(), | ||
| 459 | }); | ||
| 460 | const terminal = yield* terminalOf(runtime).pipe(Effect.forkScoped); | ||
| 461 | yield* runtime.startTurn({ | ||
| 462 | ...turnInput(thread), | ||
| 463 | appThread: { title: "Check whether 391 is prime" } as OrchestrationV2AppThread, | ||
| 464 | }); | ||
| 465 | assert.equal((yield* Fiber.join(terminal))?.status, "completed"); | ||
| 466 | }).pipe(Effect.scoped), | ||
| 467 | ); | ||
| 468 | |||
| 469 | it.effect("renames the session before the first turn after the thread was renamed", () => | ||
| 470 | Effect.gen(function* () { | ||
| 471 | const { runtime, thread } = yield* resumed([ | ||
| 472 | out("session.update", { sessionID: SESSION, title: "Prime factors of 391" }), | ||
| 473 | reply("session.update", null), | ||
| 474 | out("session.prompt", { sessionID: SESSION, text: "<any>" }), | ||
| 475 | promptAccepted, | ||
| 476 | event("session.execution.succeeded", { sessionID: SESSION }), | ||
| 477 | // Renamed once: the next turn under the same title sends no update. | ||
| 478 | out("session.prompt", { sessionID: SESSION, text: "<any>" }), | ||
| 479 | promptAccepted, | ||
| 480 | event("session.execution.succeeded", { sessionID: SESSION }), | ||
| 481 | ]); | ||
| 482 | const renamed = { | ||
| 483 | ...turnInput(thread), | ||
| 484 | appThread: { title: "Prime factors of 391" } as OrchestrationV2AppThread, | ||
| 485 | }; | ||
| 486 | const first = yield* terminalOf(runtime).pipe(Effect.forkScoped); | ||
| 487 | yield* runtime.startTurn(renamed); | ||
| 488 | assert.equal((yield* Fiber.join(first))?.status, "completed"); | ||
| 489 | const second = yield* terminalOf(runtime).pipe(Effect.forkScoped); | ||
| 490 | yield* runtime.startTurn({ | ||
| 491 | ...renamed, | ||
| 492 | runId: RunId.make("run:opencode2-adapter:2"), | ||
| 493 | attemptId: RunAttemptId.make("attempt:opencode2-adapter:2"), | ||
| 494 | }); | ||
| 495 | assert.equal((yield* Fiber.join(second))?.status, "completed"); | ||
| 496 | }).pipe(Effect.scoped), | ||
| 497 | ); | ||
| 498 | |||
| 436 | it.effect("ends a turn on the provider thread it started on", () => | 499 | it.effect("ends a turn on the provider thread it started on", () => |
| 437 | Effect.gen(function* () { | 500 | Effect.gen(function* () { |
| 438 | const { runtime, thread } = yield* resumed([ | 501 | const { runtime, thread } = yield* resumed([ |
| ... | @@ -631,6 +694,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { | ... | @@ -631,6 +694,7 @@ it.layer(McpProviderSessions.layer)("OpenCode2 adapter", (it) => { |
| 631 | ...turnInput(thread), | 694 | ...turnInput(thread), |
| 632 | appThread: { | 695 | appThread: { |
| 633 | id: threadId, | 696 | id: threadId, |
| 697 | title: TITLE, | ||
| 634 | lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId }, | 698 | lineage: { parentThreadId: null, relationshipToParent: null, rootThreadId: threadId }, |
| 635 | } as OrchestrationV2AppThread, | 699 | } as OrchestrationV2AppThread, |
| 636 | }); | 700 | }); |
apps/server/src/orchestration-v2/OpenCode2OrchestratorV2.integration.test.ts+18-12| ... | @@ -151,9 +151,10 @@ const planRules = (name: string) => [ | ... | @@ -151,9 +151,10 @@ const planRules = (name: string) => [ |
| 151 | ...mcpRules(name), | 151 | ...mcpRules(name), |
| 152 | ]; | 152 | ]; |
| 153 | 153 | ||
| 154 | const sessionInfo = (directory: string, permissions: ReadonlyArray<unknown>) => ({ | 154 | const sessionInfo = (directory: string, permissions: ReadonlyArray<unknown>, title: string) => ({ |
| 155 | data: { | 155 | data: { |
| 156 | id: SESSION, | 156 | id: SESSION, |
| 157 | title, | ||
| 157 | projectID: "global", | 158 | projectID: "global", |
| 158 | model: { id: "big-pickle", providerID: "opencode", variant: "default" }, | 159 | model: { id: "big-pickle", providerID: "opencode", variant: "default" }, |
| 159 | cost: 0, | 160 | cost: 0, |
| ... | @@ -235,8 +236,9 @@ const createdSession = ( | ... | @@ -235,8 +236,9 @@ const createdSession = ( |
| 235 | ], | 236 | ], |
| 236 | }), | 237 | }), |
| 237 | ...(narrows ? [out("agent.list", "<any>"), reply("agent.list", agentList(directory))] : []), | 238 | ...(narrows ? [out("agent.list", "<any>"), reply("agent.list", agentList(directory))] : []), |
| 238 | out("session.create", { location: { directory }, model: "<any>", permissions }), | 239 | // The thread's title, which is its name here. |
| 239 | reply("session.create", sessionInfo(directory, permissions)), | 240 | out("session.create", { location: { directory }, model: "<any>", permissions, title: name }), |
| 241 | reply("session.create", sessionInfo(directory, permissions, name)), | ||
| 240 | ]; | 242 | ]; |
| 241 | 243 | ||
| 242 | /** A recording with its scrubbed `<work>` directory replaced by `directory`. */ | 244 | /** A recording with its scrubbed `<work>` directory replaced by `directory`. */ |
| ... | @@ -348,7 +350,7 @@ describe("OpenCode 2 through the orchestrator", () => { | ... | @@ -348,7 +350,7 @@ describe("OpenCode 2 through the orchestrator", () => { |
| 348 | ...answeredPrompt("FIRST"), | 350 | ...answeredPrompt("FIRST"), |
| 349 | // The next turn resumes the session at its new selection. | 351 | // The next turn resumes the session at its new selection. |
| 350 | out("session.get", { sessionID: SESSION }), | 352 | out("session.get", { sessionID: SESSION }), |
| 351 | reply("session.get", sessionInfo(cwd, t3Rules(name))), | 353 | reply("session.get", sessionInfo(cwd, t3Rules(name), name)), |
| 352 | out("session.switchModel", { | 354 | out("session.switchModel", { |
| 353 | sessionID: SESSION, | 355 | sessionID: SESSION, |
| 354 | model: { providerID: "opencode", id: "mimo-v2.6-flash-free" }, | 356 | model: { providerID: "opencode", id: "mimo-v2.6-flash-free" }, |
| ... | @@ -401,7 +403,7 @@ describe("OpenCode 2 through the orchestrator", () => { | ... | @@ -401,7 +403,7 @@ describe("OpenCode 2 through the orchestrator", () => { |
| 401 | ...answeredPrompt("FIRST"), | 403 | ...answeredPrompt("FIRST"), |
| 402 | ...directoryModels(after), | 404 | ...directoryModels(after), |
| 403 | out("session.get", { sessionID: SESSION }), | 405 | out("session.get", { sessionID: SESSION }), |
| 404 | reply("session.get", sessionInfo(before, t3Rules(name))), | 406 | reply("session.get", sessionInfo(before, t3Rules(name), name)), |
| 405 | // The worktree change detached the thread, so its session is loaded afresh. | 407 | // The worktree change detached the thread, so its session is loaded afresh. |
| 406 | ...noOpenRequests, | 408 | ...noOpenRequests, |
| 407 | out("session.move", { sessionID: SESSION, directory: after }), | 409 | out("session.move", { sessionID: SESSION, directory: after }), |
| ... | @@ -450,10 +452,14 @@ describe("OpenCode 2 through the orchestrator", () => { | ... | @@ -450,10 +452,14 @@ describe("OpenCode 2 through the orchestrator", () => { |
| 450 | out("session.get", { sessionID: SESSION }), | 452 | out("session.get", { sessionID: SESSION }), |
| 451 | reply( | 453 | reply( |
| 452 | "session.get", | 454 | "session.get", |
| 453 | sessionInfo(before, [ | 455 | sessionInfo( |
| 454 | { action: "*", resource: "*", effect: "allow" }, | 456 | before, |
| 455 | { action: "subagent", resource: "*", effect: "deny" }, | 457 | [ |
| 456 | ]), | 458 | { action: "*", resource: "*", effect: "allow" }, |
| 459 | { action: "subagent", resource: "*", effect: "deny" }, | ||
| 460 | ], | ||
| 461 | name, | ||
| 462 | ), | ||
| 457 | ), | 463 | ), |
| 458 | ...noOpenRequests, | 464 | ...noOpenRequests, |
| 459 | out("session.update", { sessionID: SESSION, permissions: t3Rules(name) }), | 465 | out("session.update", { sessionID: SESSION, permissions: t3Rules(name) }), |
| ... | @@ -531,7 +537,7 @@ describe("OpenCode 2 through the orchestrator", () => { | ... | @@ -531,7 +537,7 @@ describe("OpenCode 2 through the orchestrator", () => { |
| 531 | ...answeredPrompt("FIRST"), | 537 | ...answeredPrompt("FIRST"), |
| 532 | // A mode change detaches nothing: the same session is resumed with the new rules. | 538 | // A mode change detaches nothing: the same session is resumed with the new rules. |
| 533 | out("session.get", { sessionID: SESSION }), | 539 | out("session.get", { sessionID: SESSION }), |
| 534 | reply("session.get", sessionInfo(cwd, t3Rules(name))), | 540 | reply("session.get", sessionInfo(cwd, t3Rules(name), name)), |
| 535 | out("agent.list", "<any>"), | 541 | out("agent.list", "<any>"), |
| 536 | reply("agent.list", agentList(cwd)), | 542 | reply("agent.list", agentList(cwd)), |
| 537 | out("session.update", { sessionID: SESSION, permissions: autoEditRules(name) }), | 543 | out("session.update", { sessionID: SESSION, permissions: autoEditRules(name) }), |
| ... | @@ -539,7 +545,7 @@ describe("OpenCode 2 through the orchestrator", () => { | ... | @@ -539,7 +545,7 @@ describe("OpenCode 2 through the orchestrator", () => { |
| 539 | ...answeredPrompt("SECOND"), | 545 | ...answeredPrompt("SECOND"), |
| 540 | // Back to Full access: the narrowing rules go. | 546 | // Back to Full access: the narrowing rules go. |
| 541 | out("session.get", { sessionID: SESSION }), | 547 | out("session.get", { sessionID: SESSION }), |
| 542 | reply("session.get", sessionInfo(cwd, autoEditRules(name))), | 548 | reply("session.get", sessionInfo(cwd, autoEditRules(name), name)), |
| 543 | out("session.update", { sessionID: SESSION, permissions: t3Rules(name) }), | 549 | out("session.update", { sessionID: SESSION, permissions: t3Rules(name) }), |
| 544 | reply("session.update", null), | 550 | reply("session.update", null), |
| 545 | ...answeredPrompt("THIRD"), | 551 | ...answeredPrompt("THIRD"), |
| ... | @@ -577,7 +583,7 @@ describe("OpenCode 2 through the orchestrator", () => { | ... | @@ -577,7 +583,7 @@ describe("OpenCode 2 through the orchestrator", () => { |
| 577 | ...instructionsWritten, | 583 | ...instructionsWritten, |
| 578 | ...answeredPrompt("PLANNED"), | 584 | ...answeredPrompt("PLANNED"), |
| 579 | out("session.get", { sessionID: SESSION }), | 585 | out("session.get", { sessionID: SESSION }), |
| 580 | reply("session.get", sessionInfo(cwd, planRules(name))), | 586 | reply("session.get", sessionInfo(cwd, planRules(name), name)), |
| 581 | out("session.update", { sessionID: SESSION, permissions: t3Rules(name) }), | 587 | out("session.update", { sessionID: SESSION, permissions: t3Rules(name) }), |
| 582 | reply("session.update", null), | 588 | reply("session.update", null), |
| 583 | out("session.switchAgent", { sessionID: SESSION, agent: "build" }), | 589 | out("session.switchAgent", { sessionID: SESSION, agent: "build" }), |
apps/server/src/orchestration-v2/ProviderTurnStartService.ts+2| ... | @@ -651,6 +651,7 @@ export const layer: Layer.Layer< | ... | @@ -651,6 +651,7 @@ export const layer: Layer.Layer< |
| 651 | return yield* loadFromProvider( | 651 | return yield* loadFromProvider( |
| 652 | session.ensureThread({ | 652 | session.ensureThread({ |
| 653 | threadId: projection.thread.id, | 653 | threadId: projection.thread.id, |
| 654 | title: projection.thread.title, | ||
| 654 | modelSelection: run.modelSelection, | 655 | modelSelection: run.modelSelection, |
| 655 | runtimePolicy: resolvedRuntimePolicy, | 656 | runtimePolicy: resolvedRuntimePolicy, |
| 656 | providerSessionId, | 657 | providerSessionId, |
| ... | @@ -696,6 +697,7 @@ export const layer: Layer.Layer< | ... | @@ -696,6 +697,7 @@ export const layer: Layer.Layer< |
| 696 | const replacement = yield* loadFromProvider( | 697 | const replacement = yield* loadFromProvider( |
| 697 | session.ensureThread({ | 698 | session.ensureThread({ |
| 698 | threadId: projection.thread.id, | 699 | threadId: projection.thread.id, |
| 700 | title: projection.thread.title, | ||
| 699 | modelSelection: run.modelSelection, | 701 | modelSelection: run.modelSelection, |
| 700 | runtimePolicy: resolvedRuntimePolicy, | 702 | runtimePolicy: resolvedRuntimePolicy, |
| 701 | providerSessionId, | 703 | providerSessionId, |
apps/server/src/orchestration-v2/testkit/fixtures/opencode2_fork/opencode_transcript.ndjson+2| ... | @@ -50,6 +50,8 @@ | ... | @@ -50,6 +50,8 @@ |
| 50 | {"type": "expect_outbound", "label": "session.update", "frame": {"type": "session.update", "input": {"sessionID": "ses_f1484db83ffeLGtrRCFimo1H0e", "permissions": [{"action": "*", "resource": "*", "effect": "allow"}, {"action": "t3-code-*", "resource": "*", "effect": "deny"}, {"action": "t3-code-thread_opencode2-fork_target_*", "resource": "*", "effect": "allow"}]}}} | 50 | {"type": "expect_outbound", "label": "session.update", "frame": {"type": "session.update", "input": {"sessionID": "ses_f1484db83ffeLGtrRCFimo1H0e", "permissions": [{"action": "*", "resource": "*", "effect": "allow"}, {"action": "t3-code-*", "resource": "*", "effect": "deny"}, {"action": "t3-code-thread_opencode2-fork_target_*", "resource": "*", "effect": "allow"}]}}} |
| 51 | {"type": "emit_inbound", "label": "session.update.response", "frame": {"type": "sdk.response", "operation": "session.update", "data": null}} | 51 | {"type": "emit_inbound", "label": "session.update.response", "frame": {"type": "sdk.response", "operation": "session.update", "data": null}} |
| 52 | {"type": "emit_inbound", "label": "session.forked", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7b247d001fXzFroPg5P0bps", "created": 1790657111165, "type": "session.forked", "location": {"directory": "<work>"}, "data": {"sessionID": "ses_f1484db83ffeLGtrRCFimo1H0e", "parentID": "ses_f1484ea8affe5aTo8CcgbGIszj", "boundary": {"type": "before", "messageID": "msg_0eb7b1f9f001g95Bduh3bi8syS"}, "instructions": {"core/environment": "db4a793a05fec32fdfa985a3432125a86d6cb42debc8b9ee8f6a99fbfcf2f93b", "core/date": "bf08b8486572ab3eef677a3c9ee12468e411ae41a8f4d2fb6cf02a6ca73f1986", "core/codemode": "616a7ad8b8e40e8a71eab238dfbbe9eaf0f4a9e6fdad853226d075bfe602e1e6", "core/skill-guidance": "0d001919496102654717308c72635523bf7c3cd99c81e058cc79f2947104f9a5"}, "instructionEntries": []}, "durable": {"aggregateID": "ses_f1484db83ffeLGtrRCFimo1H0e", "seq": 0, "version": 2}}}} | 52 | {"type": "emit_inbound", "label": "session.forked", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7b247d001fXzFroPg5P0bps", "created": 1790657111165, "type": "session.forked", "location": {"directory": "<work>"}, "data": {"sessionID": "ses_f1484db83ffeLGtrRCFimo1H0e", "parentID": "ses_f1484ea8affe5aTo8CcgbGIszj", "boundary": {"type": "before", "messageID": "msg_0eb7b1f9f001g95Bduh3bi8syS"}, "instructions": {"core/environment": "db4a793a05fec32fdfa985a3432125a86d6cb42debc8b9ee8f6a99fbfcf2f93b", "core/date": "bf08b8486572ab3eef677a3c9ee12468e411ae41a8f4d2fb6cf02a6ca73f1986", "core/codemode": "616a7ad8b8e40e8a71eab238dfbbe9eaf0f4a9e6fdad853226d075bfe602e1e6", "core/skill-guidance": "0d001919496102654717308c72635523bf7c3cd99c81e058cc79f2947104f9a5"}, "instructionEntries": []}, "durable": {"aggregateID": "ses_f1484db83ffeLGtrRCFimo1H0e", "seq": 0, "version": 2}}}} |
| 53 | {"type": "expect_outbound", "label": "session.update.title", "frame": {"type": "session.update", "input": {"sessionID": "ses_f1484db83ffeLGtrRCFimo1H0e", "title": "<any>"}}} | ||
| 54 | {"type": "emit_inbound", "label": "session.update.title.response", "frame": {"type": "sdk.response", "operation": "session.update", "data": null}} | ||
| 53 | {"type": "expect_outbound", "label": "session.instructions.entry.put.2", "frame": {"type": "session.instructions.entry.put", "input": {"sessionID": "ses_f1484db83ffeLGtrRCFimo1H0e", "key": "t3-code", "value": "<any>"}}} | 55 | {"type": "expect_outbound", "label": "session.instructions.entry.put.2", "frame": {"type": "session.instructions.entry.put", "input": {"sessionID": "ses_f1484db83ffeLGtrRCFimo1H0e", "key": "t3-code", "value": "<any>"}}} |
| 54 | {"type": "emit_inbound", "label": "session.instructions.entry.put.response.2", "frame": {"type": "sdk.response", "operation": "session.instructions.entry.put", "data": null}} | 56 | {"type": "emit_inbound", "label": "session.instructions.entry.put.response.2", "frame": {"type": "sdk.response", "operation": "session.instructions.entry.put", "data": null}} |
| 55 | {"type": "expect_outbound", "label": "session.prompt.3", "frame": {"type": "session.prompt", "input": {"sessionID": "ses_f1484db83ffeLGtrRCFimo1H0e", "id": "msg_0eb7b2493001nZTYCniIjU5Y65", "text": "<any>"}}} | 57 | {"type": "expect_outbound", "label": "session.prompt.3", "frame": {"type": "session.prompt", "input": {"sessionID": "ses_f1484db83ffeLGtrRCFimo1H0e", "id": "msg_0eb7b2493001nZTYCniIjU5Y65", "text": "<any>"}}} |
apps/server/src/orchestration-v2/testkit/fixtures/opencode2_switch/opencode_transcript.ndjson+2| ... | @@ -59,6 +59,8 @@ | ... | @@ -59,6 +59,8 @@ |
| 59 | {"type": "emit_inbound", "label": "session.switchAgent.response.2", "frame": {"type": "sdk.response", "operation": "session.switchAgent", "data": null}} | 59 | {"type": "emit_inbound", "label": "session.switchAgent.response.2", "frame": {"type": "sdk.response", "operation": "session.switchAgent", "data": null}} |
| 60 | {"type": "emit_inbound", "label": "session.agent.selected.2", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7eef80001lOHUbFIwlg7iap", "created": 1790657359744, "type": "session.agent.selected", "location": {"directory": "<work>"}, "data": {"sessionID": "ses_f14812197ffeCbw641wM2r3ZmP", "agent": "build", "previous": "plan"}, "durable": {"aggregateID": "ses_f14812197ffeCbw641wM2r3ZmP", "seq": 16, "version": 1}}}} | 60 | {"type": "emit_inbound", "label": "session.agent.selected.2", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7eef80001lOHUbFIwlg7iap", "created": 1790657359744, "type": "session.agent.selected", "location": {"directory": "<work>"}, "data": {"sessionID": "ses_f14812197ffeCbw641wM2r3ZmP", "agent": "build", "previous": "plan"}, "durable": {"aggregateID": "ses_f14812197ffeCbw641wM2r3ZmP", "seq": 16, "version": 1}}}} |
| 61 | {"type": "emit_inbound", "label": "session.inbox.enqueued.3", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7eef830020JKd2UmFNPZ8vu", "created": 1790657359747, "type": "session.inbox.enqueued", "location": {"directory": "<work>"}, "data": {"inboxID": "msg_0eb7eef83001M1o8rpSkbKzlf5", "sessionID": "ses_f14812197ffeCbw641wM2r3ZmP", "item": {"type": "synthetic", "payload": {"text": "<system-reminder>\nYou are NO LONGER in Plan mode. The previous Plan restrictions no longer apply. Any Plan mode instructions from earlier in this conversation are no longer active.\n</system-reminder>"}, "delivery": "steer"}}, "durable": {"aggregateID": "ses_f14812197ffeCbw641wM2r3ZmP", "seq": 17, "version": 1}}}} | 61 | {"type": "emit_inbound", "label": "session.inbox.enqueued.3", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7eef830020JKd2UmFNPZ8vu", "created": 1790657359747, "type": "session.inbox.enqueued", "location": {"directory": "<work>"}, "data": {"inboxID": "msg_0eb7eef83001M1o8rpSkbKzlf5", "sessionID": "ses_f14812197ffeCbw641wM2r3ZmP", "item": {"type": "synthetic", "payload": {"text": "<system-reminder>\nYou are NO LONGER in Plan mode. The previous Plan restrictions no longer apply. Any Plan mode instructions from earlier in this conversation are no longer active.\n</system-reminder>"}, "delivery": "steer"}}, "durable": {"aggregateID": "ses_f14812197ffeCbw641wM2r3ZmP", "seq": 17, "version": 1}}}} |
| 62 | {"type": "expect_outbound", "label": "session.update.title", "frame": {"type": "session.update", "input": {"sessionID": "ses_f14812197ffeCbw641wM2r3ZmP", "title": "<any>"}}} | ||
| 63 | {"type": "emit_inbound", "label": "session.update.title.response", "frame": {"type": "sdk.response", "operation": "session.update", "data": null}} | ||
| 62 | {"type": "expect_outbound", "label": "session.switchModel", "frame": {"type": "session.switchModel", "input": {"sessionID": "ses_f14812197ffeCbw641wM2r3ZmP", "model": {"providerID": "opencode", "id": "nemotron-3.5-lightning-free"}}}} | 64 | {"type": "expect_outbound", "label": "session.switchModel", "frame": {"type": "session.switchModel", "input": {"sessionID": "ses_f14812197ffeCbw641wM2r3ZmP", "model": {"providerID": "opencode", "id": "nemotron-3.5-lightning-free"}}}} |
| 63 | {"type": "emit_inbound", "label": "session.switchModel.response", "frame": {"type": "sdk.response", "operation": "session.switchModel", "data": null}} | 65 | {"type": "emit_inbound", "label": "session.switchModel.response", "frame": {"type": "sdk.response", "operation": "session.switchModel", "data": null}} |
| 64 | {"type": "emit_inbound", "label": "session.model.selected", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7eef8d001Yy5fmSDr6nQ8OS", "created": 1790657359757, "type": "session.model.selected", "location": {"directory": "<work>"}, "data": {"sessionID": "ses_f14812197ffeCbw641wM2r3ZmP", "model": {"id": "nemotron-3.5-lightning-free", "providerID": "opencode"}, "previous": {"id": "big-pickle", "providerID": "opencode", "variant": "default"}}, "durable": {"aggregateID": "ses_f14812197ffeCbw641wM2r3ZmP", "seq": 18, "version": 1}}}} | 66 | {"type": "emit_inbound", "label": "session.model.selected", "frame": {"type": "sdk.event", "event": {"id": "evt_0eb7eef8d001Yy5fmSDr6nQ8OS", "created": 1790657359757, "type": "session.model.selected", "location": {"directory": "<work>"}, "data": {"sessionID": "ses_f14812197ffeCbw641wM2r3ZmP", "model": {"id": "nemotron-3.5-lightning-free", "providerID": "opencode"}, "previous": {"id": "big-pickle", "providerID": "opencode", "variant": "default"}}, "durable": {"aggregateID": "ses_f14812197ffeCbw641wM2r3ZmP", "seq": 18, "version": 1}}}} |
packages/provider-core/src/server/ProviderAdapter.ts+2| ... | @@ -394,6 +394,8 @@ export interface ProviderAdapterV2OpenSessionInput { | ... | @@ -394,6 +394,8 @@ export interface ProviderAdapterV2OpenSessionInput { |
| 394 | 394 | ||
| 395 | export interface ProviderAdapterV2EnsureThreadInput { | 395 | export interface ProviderAdapterV2EnsureThreadInput { |
| 396 | readonly threadId: ThreadId; | 396 | readonly threadId: ThreadId; |
| 397 | /** The app thread's title, for providers that name their native sessions. */ | ||
| 398 | readonly title?: string; | ||
| 397 | readonly modelSelection: ModelSelection; | 399 | readonly modelSelection: ModelSelection; |
| 398 | readonly runtimePolicy: ProviderAdapterV2RuntimePolicy; | 400 | readonly runtimePolicy: ProviderAdapterV2RuntimePolicy; |
| 399 | readonly providerSessionId?: ProviderSessionId; | 401 | readonly providerSessionId?: ProviderSessionId; |
packages/provider-opencode/src/server/v2/adapter.ts+30-1| ... | @@ -345,6 +345,8 @@ interface ThreadState { | ... | @@ -345,6 +345,8 @@ interface ThreadState { |
| 345 | * rules stay in force, and a changed mode switches it before prompting. | 345 | * rules stay in force, and a changed mode switches it before prompting. |
| 346 | */ | 346 | */ |
| 347 | agent: string; | 347 | agent: string; |
| 348 | /** The native session's title as T3 last read or wrote it. */ | ||
| 349 | title: string | undefined; | ||
| 348 | /** The native session's rules as T3 last read or wrote them, and the policy they are for. */ | 350 | /** The native session's rules as T3 last read or wrote them, and the policy they are for. */ |
| 349 | rules: ReadonlyArray<Rule> | undefined; | 351 | rules: ReadonlyArray<Rule> | undefined; |
| 350 | policy: RulesPolicy; | 352 | policy: RulesPolicy; |
| ... | @@ -934,6 +936,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr | ... | @@ -934,6 +936,7 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr |
| 934 | unsettled: false, | 936 | unsettled: false, |
| 935 | directory, | 937 | directory, |
| 936 | agent: "build", | 938 | agent: "build", |
| 939 | title: undefined, | ||
| 937 | rules: undefined, | 940 | rules: undefined, |
| 938 | policy: input.runtimePolicy, | 941 | policy: input.runtimePolicy, |
| 939 | grants: [], | 942 | grants: [], |
| ... | @@ -3012,12 +3015,22 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr | ... | @@ -3012,12 +3015,22 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr |
| 3012 | state.policy = policy; | 3015 | state.policy = policy; |
| 3013 | }); | 3016 | }); |
| 3014 | 3017 | ||
| 3018 | /** Names the session after its thread when they differ. */ | ||
| 3019 | const writeTitle = Effect.fnUntraced(function* (state: ThreadState, title: string) { | ||
| 3020 | const next = title.trim(); | ||
| 3021 | // An empty title asks OpenCode to generate one. | ||
| 3022 | if (next === "" || next === state.title) return; | ||
| 3023 | yield* client.session.update({ sessionID: Session.ID.make(state.sessionId), title: next }); | ||
| 3024 | state.title = next; | ||
| 3025 | }); | ||
| 3026 | |||
| 3015 | const register = ( | 3027 | const register = ( |
| 3016 | providerThread: OrchestrationV2ProviderThread, | 3028 | providerThread: OrchestrationV2ProviderThread, |
| 3017 | native: { | 3029 | native: { |
| 3018 | readonly id: string; | 3030 | readonly id: string; |
| 3019 | readonly model?: ModelRef | undefined; | 3031 | readonly model?: ModelRef | undefined; |
| 3020 | readonly agent?: string | undefined; | 3032 | readonly agent?: string | undefined; |
| 3033 | readonly title?: string | undefined; | ||
| 3021 | readonly permissions?: ReadonlyArray<Rule> | undefined; | 3034 | readonly permissions?: ReadonlyArray<Rule> | undefined; |
| 3022 | }, | 3035 | }, |
| 3023 | directory: string, | 3036 | directory: string, |
| ... | @@ -3029,12 +3042,14 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr | ... | @@ -3029,12 +3042,14 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr |
| 3029 | existing.model = native.model; | 3042 | existing.model = native.model; |
| 3030 | existing.directory = directory; | 3043 | existing.directory = directory; |
| 3031 | existing.agent = native.agent ?? existing.agent; | 3044 | existing.agent = native.agent ?? existing.agent; |
| 3045 | existing.title = native.title; | ||
| 3032 | existing.rules = native.permissions; | 3046 | existing.rules = native.permissions; |
| 3033 | return existing; | 3047 | return existing; |
| 3034 | } | 3048 | } |
| 3035 | const state = newThreadState(native.id, providerThread, directory, undefined); | 3049 | const state = newThreadState(native.id, providerThread, directory, undefined); |
| 3036 | state.model = native.model; | 3050 | state.model = native.model; |
| 3037 | state.agent = native.agent ?? state.agent; | 3051 | state.agent = native.agent ?? state.agent; |
| 3052 | state.title = native.title; | ||
| 3038 | state.rules = native.permissions; | 3053 | state.rules = native.permissions; |
| 3039 | threads.set(native.id, state); | 3054 | threads.set(native.id, state); |
| 3040 | return state; | 3055 | return state; |
| ... | @@ -3615,10 +3630,13 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr | ... | @@ -3615,10 +3630,13 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr |
| 3615 | policy, | 3630 | policy, |
| 3616 | threadInput.threadId, | 3631 | threadInput.threadId, |
| 3617 | ); | 3632 | ); |
| 3633 | const title = threadInput.title?.trim() || undefined; | ||
| 3634 | // A session created with a title is never titled by OpenCode's own model. | ||
| 3618 | const created = yield* client.session.create({ | 3635 | const created = yield* client.session.create({ |
| 3619 | location: Location.PublicRef.make({ directory: AbsolutePath.make(directory) }), | 3636 | location: Location.PublicRef.make({ directory: AbsolutePath.make(directory) }), |
| 3620 | model, | 3637 | model, |
| 3621 | permissions, | 3638 | permissions, |
| 3639 | ...(title === undefined ? {} : { title }), | ||
| 3622 | }); | 3640 | }); |
| 3623 | const createdAt = yield* DateTime.now; | 3641 | const createdAt = yield* DateTime.now; |
| 3624 | const providerThread: OrchestrationV2ProviderThread = { | 3642 | const providerThread: OrchestrationV2ProviderThread = { |
| ... | @@ -3642,7 +3660,13 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr | ... | @@ -3642,7 +3660,13 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr |
| 3642 | }; | 3660 | }; |
| 3643 | const state = register( | 3661 | const state = register( |
| 3644 | providerThread, | 3662 | providerThread, |
| 3645 | { id: created.id, model: created.model, agent: created.agent, permissions }, | 3663 | { |
| 3664 | id: created.id, | ||
| 3665 | model: created.model, | ||
| 3666 | agent: created.agent, | ||
| 3667 | title: title ?? created.title, | ||
| 3668 | permissions, | ||
| 3669 | }, | ||
| 3646 | directory, | 3670 | directory, |
| 3647 | ); | 3671 | ); |
| 3648 | state.policy = policy; | 3672 | state.policy = policy; |
| ... | @@ -3801,6 +3825,11 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr | ... | @@ -3801,6 +3825,11 @@ export const make = Effect.fn("OpenCode2Adapter.make")(function* (instanceId: Pr |
| 3801 | // and its subagents still running hold the rules they started with. | 3825 | // and its subagents still running hold the rules they started with. |
| 3802 | // Those run on whether or not this turn starts, so theirs are best effort. | 3826 | // Those run on whether or not this turn starts, so theirs are best effort. |
| 3803 | yield* writeRules(state, turnInput.runtimePolicy); | 3827 | yield* writeRules(state, turnInput.runtimePolicy); |
| 3828 | // A thread renamed since the last turn, or titled after its first prompt. | ||
| 3829 | yield* writeTitle(state, turnInput.appThread.title).pipe( | ||
| 3830 | Effect.timeout(REQUEST_REPLY_TIMEOUT), | ||
| 3831 | Effect.ignore({ log: true }), | ||
| 3832 | ); | ||
| 3804 | for (const call of runningCalls(state)) { | 3833 | for (const call of runningCalls(state)) { |
| 3805 | if (call.child === undefined) continue; | 3834 | if (call.child === undefined) continue; |
| 3806 | yield* writeRules(call.child, turnInput.runtimePolicy).pipe( | 3835 | yield* writeRules(call.child, turnInput.runtimePolicy).pipe( |