Repository navigation
feat(harness): connect assistant lifecycle server actions [SAP-3292] #1026
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| --- | ||
| "@sapiom/harness": patch | ||
| --- | ||
|
|
||
| Connect Assistant history, Resume and Continue to the shared server context owner and hide unfinished continuation children. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -201,15 +201,21 @@ import { AssistantLifecycleCoordinator } from "../core/assistant-lifecycle.js"; | |
| import { AssistantEndCoordinator } from "../core/assistant-end.js"; | ||
| import { createAssistantStateProjection } from "../core/assistant-state-projection.js"; | ||
| import { OpenCodeAssociations } from "../core/opencode-association.js"; | ||
| import { OpenCodeHost, type HostedOpenCode } from "../core/opencode-host.js"; | ||
| import { OpenCodeHost, OpenCodeAccessError, type HostedOpenCode } from "../core/opencode-host.js"; | ||
| import { AssistantRecordStore } from "../core/assistant-record-store.js"; | ||
| import { AssistantRecordCapture } from "../core/assistant-record-capture.js"; | ||
| import { assistantHistoryAccess } from "../core/assistant-history-access.js"; | ||
| import { AssistantHistory } from "../core/assistant-history.js"; | ||
| import { OpenCodeFinalResponse } from "../core/opencode-final-response.js"; | ||
| import { createAssistantRecordsRouter } from "./assistant-records.js"; | ||
| import { OpenCodeObserver } from "../core/opencode-observer.js"; | ||
| import { createAssistantContextResolver } from "./studio-assistant.js"; | ||
| import { createAssistantContextRuntime, type ActivatedAssistantContextRuntime } from "./assistant-context-runtime.js"; | ||
| import { AssistantContinuationStore } from "../core/assistant-continuation-store.js"; | ||
| import { AssistantContinuationNative } from "../core/assistant-continuation-native.js"; | ||
| import { AssistantContinuation } from "../core/assistant-continuation.js"; | ||
| import { AssistantNativeHistory } from "../core/assistant-native-history.js"; | ||
| import { createAssistantLifecycleRouter, projectAssistantContinuation } from "./assistant-lifecycle.js"; | ||
| import type { AssistantContinuationView } from "../shared/assistant-continuation.js"; | ||
| import { OpenCodeBridge } from "./opencode-bridge.js"; | ||
| import { createStaticRouter } from "./static.js"; | ||
| import { createTerminalWebSocketHandler } from "./terminal-ws.js"; | ||
|
|
@@ -302,6 +308,8 @@ export interface HarnessServerOptions { | |
| * bundled DEFAULT_SYSTEM_PROMPT on any failure. Tests and offline hosts inject a | ||
| * plain function instead of reaching the network. */ | ||
| loadSystemPrompt?: () => Promise<string>; | ||
| /** Explicit verified capability supplied by the existing context runtime owner. */ | ||
| assistantContextRuntime?: ActivatedAssistantContextRuntime; | ||
| /** Host-supplied launcher for the local sapiom-dev MCP server, replacing the | ||
| * default `npx @sapiom/mcp@latest`. The Electron host passes its own binary | ||
| * (GUI-subsystem — allocates no console window on Windows, where the npx | ||
|
|
@@ -3546,6 +3554,7 @@ export const startServer = async ( | |
|
|
||
| const assistantSessions = new AssistantSessionStore(statePaths.root); | ||
| const assistantRecords = new AssistantRecordStore(statePaths.root); | ||
| const assistantContinuations = new AssistantContinuationStore(statePaths.root); | ||
| const recordCaptures = new WeakMap<HostedOpenCode, AssistantRecordCapture>(); | ||
| const authorizeAssistantWorkspace = async (id: string) => { | ||
| const session = sessionManager.get(id); | ||
|
|
@@ -3556,10 +3565,24 @@ export const startServer = async ( | |
| }))) return null; | ||
| return { harnessSessionId: id, cwd: session.cwd }; | ||
| }; | ||
| const authorizeAssistantHistory = assistantHistoryAccess({ access: assistantAccess, authorize: authorizeAssistantWorkspace, store: assistantSessions }); | ||
| const continuationFor = async (id: string, conversationId: string): Promise<AssistantContinuationView | null> => { | ||
| const receipt = await assistantContinuations.readChild(id); | ||
| if (!receipt) return null; | ||
| const binding = await authorizeAssistantHistory(id); | ||
| if (!binding || !receipt.childBinding || receipt.childStudioId !== id || | ||
| binding.conversationId !== conversationId || | ||
| binding.conversationId !== receipt.childBinding.conversationId || | ||
| binding.contextAuthorityScope !== receipt.childBinding.contextAuthorityScope || | ||
| binding.cwd !== receipt.childBinding.cwd) | ||
| throw new OpenCodeAccessError("Assistant continuation binding changed"); | ||
| return projectAssistantContinuation(receipt); | ||
| }; | ||
| const openCodeHost = new OpenCodeHost({ | ||
| contextRuntime: options.assistantContextRuntime, | ||
| access: assistantAccess, | ||
| createObserver: (hosted, id, update) => { | ||
| const capture = new AssistantRecordCapture(hosted, id, assistantRecords); | ||
| const capture = new AssistantRecordCapture(hosted, id, assistantRecords, () => continuationFor(hosted.harnessSessionId, id)); | ||
| recordCaptures.set(hosted, capture); | ||
| const checkpoint = () => capture.checkpoint(); | ||
| const observer = new OpenCodeObserver(hosted, id, update, capture.invalidate, () => { void checkpoint(); }); | ||
|
|
@@ -3571,7 +3594,7 @@ export const startServer = async ( | |
| authorize: authorizeAssistantWorkspace, | ||
| }); | ||
| const openCodeAssociations = new OpenCodeAssociations(assistantSessions); | ||
| const assistantLifecycle = new AssistantLifecycleCoordinator({ store: assistantSessions, host: openCodeHost, associations: openCodeAssociations }); | ||
| const assistantLifecycle = new AssistantLifecycleCoordinator({ store: assistantSessions, host: openCodeHost, associations: openCodeAssociations, canAttach: (id) => assistantContinuations.canAttach(id) }); | ||
| const assistantEnd = new AssistantEndCoordinator({ store: assistantSessions, lifecycle: assistantLifecycle, sessionManager }); | ||
| // Hydrate lifecycle headers without launching runtimes. A corrupt entry fails | ||
| // closed when selected rather than preventing unrelated sessions from loading. | ||
|
|
@@ -3580,6 +3603,35 @@ export const startServer = async ( | |
| bus.publish({ type: "assistant.state", snapshot }), | ||
| ); | ||
| const getAssistantState = assistantProjection.get; | ||
| const assistantContext = createAssistantContextRuntime({ | ||
| activated: options.assistantContextRuntime, | ||
| continuations: assistantContinuations, | ||
| assertCurrent: (hosted) => openCodeHost.assertCurrent(hosted), | ||
| contextOptions: { | ||
| getSession: (id) => sessionManager.get(id), | ||
| resolveProject: (id) => studioProjectCatalog.resolveIdentity(id), | ||
| getWorkflows: readPublicWorkflows, | ||
| getEnvironment: () => assistantAccess.get()?.environment ?? null, | ||
| loadSystemPrompt: options.loadSystemPrompt, | ||
| }, | ||
| }); | ||
| const assistantNativeHistory = new AssistantNativeHistory({ | ||
| authorize: authorizeAssistantHistory, | ||
| lifecycle: assistantLifecycle, | ||
| preflight: assistantContext.delivery.recover, | ||
| }); | ||
| const assistantContinue = new AssistantContinuation({ | ||
| store: assistantContinuations, | ||
| records: assistantRecords, | ||
| sessions: sessionManager, | ||
| associations: assistantSessions, | ||
| lifecycle: assistantLifecycle, | ||
| native: new AssistantContinuationNative((hosted) => openCodeHost.assertCurrent(hosted)), | ||
| delivery: assistantContext.delivery, | ||
| authorize: authorizeAssistantHistory, | ||
| resolveCandidate: (hosted, _receipt, signal) => assistantContext.resolveCandidate(hosted, null, signal), | ||
| assertAvailable: assistantContext.assertAvailable, | ||
| }); | ||
| const app: Express = express(); | ||
| app.disable("x-powered-by"); | ||
| app.use("/opencode-runtime", openCodeBridge.router); | ||
|
|
@@ -3588,19 +3640,15 @@ export const startServer = async ( | |
| createOpenCodeRouter( | ||
| openCodeHost, | ||
| options.bootToken, | ||
| createAssistantContextResolver({ | ||
| getSession: (id) => sessionManager.get(id), | ||
| resolveProject: (id) => studioProjectCatalog.resolveIdentity(id), | ||
| getWorkflows: readPublicWorkflows, | ||
| getEnvironment: () => assistantAccess.get()?.environment ?? null, | ||
| loadSystemPrompt: options.loadSystemPrompt, | ||
| }), | ||
| assistantContext.context, | ||
| openCodeAssociations, | ||
| new OpenCodeFinalResponse({ | ||
| recoverPrompt: assistantContext.delivery.recover, | ||
| assertCurrent: (hosted) => assistantLifecycle.assertRuntime(hosted, true), | ||
| onPersisted: async (hosted) => { await recordCaptures.get(hosted)?.checkpoint(); }, | ||
| }), | ||
| assistantLifecycle, | ||
| continuationFor, | ||
| ), | ||
| ); | ||
|
|
||
|
|
@@ -3625,8 +3673,20 @@ export const startServer = async ( | |
| res.setHeader("Cache-Control", "no-store"); | ||
| res.json(assistantAccess.getBrowserState()); | ||
| }); | ||
| const authorizeAssistantHistory = assistantHistoryAccess({ access: assistantAccess, authorize: authorizeAssistantWorkspace, store: assistantSessions }); | ||
| const assistantHistory = new AssistantHistory({ sessions: sessionManager, authorize: authorizeAssistantHistory, records: assistantRecords, lifecycle: assistantLifecycle }); | ||
| const assistantHistory = new AssistantHistory({ sessions: sessionManager, authorize: authorizeAssistantHistory, records: assistantRecords, lifecycle: assistantLifecycle, isVisible: (id) => assistantContinuations.canAttach(id) }); | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🟡 Pending continuation children remain visible After Continue allocates an unfinished child, Learn moreContinuation allocation persists an exited, dormant Example: Continue allocates child Recommended fix: Apply one continuation-visibility projection to every public session source, including Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| app.use("/api", createAssistantLifecycleRouter({ | ||
| bootToken: options.bootToken, | ||
| history: assistantHistory, | ||
| native: assistantNativeHistory, | ||
| getSession: (id) => sessionManager.get(id), | ||
| resume: (id, revision, operationId) => assistantNativeHistory.resume(id, revision, operationId), | ||
| continue: async (id, request) => { | ||
| const prepared = await assistantContinue.continue(id, request); | ||
| // Publish only the verified child. Pending receipt allocation is private. | ||
| bus.publish({ type: "session.status", session: prepared.session }); | ||
| return prepared; | ||
| }, | ||
| })); | ||
|
Comment on lines
3673
to
+3689
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🔍 Server wiring lacks integration coverage Tests exercise the router, not the new (Refers to this code) Was this helpful? React with 👍 or 👎 to provide feedback. |
||
| app.use("/api", createAssistantRecordsRouter({ | ||
| bootToken: options.bootToken, | ||
| store: assistantRecords, | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🔍 No shipping host activates Continue
Neither the CLI nor Electron supplies
assistantContextRuntime, so Continue returnscontext_unavailable. Verify that the stack intentionally defers host activation.Was this helpful? React with 👍 or 👎 to provide feedback.