@opencode-ai/client 0.0.0-dev-18400 → 0.0.0-dev-18407

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.
@@ -2115,6 +2115,15 @@ export type ExperimentalPersistentPtyCreateOutput = {
2115
2115
  export type ExperimentalPersistentPtyCreateOperation<E = never> = (input: ExperimentalPersistentPtyCreateInput) => Effect.Effect<ExperimentalPersistentPtyCreateOutput, E>;
2116
2116
  export type ExperimentalPersistentPtyShutdownOutput = void;
2117
2117
  export type ExperimentalPersistentPtyShutdownOperation<E = never> = () => Effect.Effect<ExperimentalPersistentPtyShutdownOutput, E>;
2118
+ export type ExperimentalPersistentPtyHandoffOutput = {
2119
+ readonly handoff: {
2120
+ readonly directory: string;
2121
+ readonly instanceID: string;
2122
+ readonly ticket: string;
2123
+ readonly expiresAt: number;
2124
+ } | null;
2125
+ };
2126
+ export type ExperimentalPersistentPtyHandoffOperation<E = never> = () => Effect.Effect<ExperimentalPersistentPtyHandoffOutput, E>;
2118
2127
  export type ExperimentalPersistentPtyGetInput = {
2119
2128
  readonly ptyID: Pty.ID;
2120
2129
  };
@@ -2216,6 +2225,7 @@ export interface ExperimentalApi<E = never> {
2216
2225
  readonly list: ExperimentalPersistentPtyListOperation<E>;
2217
2226
  readonly create: ExperimentalPersistentPtyCreateOperation<E>;
2218
2227
  readonly shutdown: ExperimentalPersistentPtyShutdownOperation<E>;
2228
+ readonly handoff: ExperimentalPersistentPtyHandoffOperation<E>;
2219
2229
  readonly get: ExperimentalPersistentPtyGetOperation<E>;
2220
2230
  readonly update: ExperimentalPersistentPtyUpdateOperation<E>;
2221
2231
  readonly snapshot: ExperimentalPersistentPtySnapshotOperation<E>;
@@ -1,7 +1,7 @@
1
1
  import { Effect, Stream, Schema } from "effect";
2
2
  import { Sse } from "effect/unstable/encoding";
3
3
  import { HttpClientError } from "effect/unstable/http";
4
- import type { HealthGetOutput, ServerGetOutput, LocationGetInput, AgentListInput, AgentListOutput, AgentGetInput, AgentGetOutput, PluginListInput, PluginListOutput, SessionListInput, SessionListOutput, SessionStatsInput, SessionStatsOutput, SessionCreateInput, SessionImportInput, SessionExportInput, SessionExportOutput, SessionActiveOutput, SessionGetInput, SessionRemoveInput, SessionForkInput, SessionSwitchAgentInput, SessionSwitchModelInput, SessionRenameInput, SessionMoveInput, SessionPromptInput, SessionCommandInput, SessionSkillInput, SessionSyntheticInput, SessionShellInput, SessionCompactInput, SessionWaitInput, SessionRevertStageInput, SessionRevertClearInput, SessionRevertCommitInput, SessionContextInput, SessionContextOutput, SessionInboxListInput, SessionInboxListOutput, SessionInboxCancelInput, SessionInboxSteerInput, SessionInboxQueueInput, SessionInstructionsEntryListInput, SessionInstructionsEntryListOutput, SessionInstructionsEntryPutInput, SessionInstructionsEntryRemoveInput, SessionGenerateInput, SessionGenerateOutput, SessionLogInput, SessionLogOutput, SessionInterruptInput, SessionInterruptOutput, SessionBackgroundInput, SessionMessageInput, SessionMessageUpdateInput, SessionEnvironmentInput, SessionViewInput, MessageListInput, MessageListOutput, ModelListInput, ModelListOutput, ModelDefaultInput, ModelDefaultOutput, GenerateTextInput, GenerateTextOutput, ProviderListInput, ProviderListOutput, ProviderGetInput, ProviderGetOutput, IntegrationListInput, IntegrationListOutput, IntegrationGetInput, IntegrationGetOutput, IntegrationWellknownAddInput, IntegrationConnectKeyInput, IntegrationOauthConnectInput, IntegrationOauthConnectOutput, IntegrationOauthStatusInput, IntegrationOauthStatusOutput, IntegrationOauthCompleteInput, IntegrationOauthCancelInput, IntegrationCommandConnectInput, IntegrationCommandConnectOutput, IntegrationCommandStatusInput, IntegrationCommandStatusOutput, IntegrationCommandCancelInput, McpListInput, McpListOutput, McpAddInput, McpRemoveInput, McpConnectInput, McpDisconnectInput, McpResourceCatalogInput, McpResourceCatalogOutput, CredentialUpdateInput, CredentialActivateInput, CredentialRemoveInput, ProjectListOutput, ProjectUpdateInput, ProjectCurrentInput, FormRequestListInput, FormRequestListOutput, FormListInput, FormListOutput, FormCreateInput, FormGetInput, FormStateInput, FormReplyInput, FormCancelInput, PermissionRequestListInput, PermissionRequestListOutput, PermissionSavedListInput, PermissionSavedListOutput, PermissionSavedRemoveInput, PermissionCreateInput, PermissionCreateOutput, PermissionListInput, PermissionListOutput, PermissionGetInput, PermissionReplyInput, FileListInput, FileListOutput, FileFindInput, FileFindOutput, CommandListInput, CommandListOutput, SkillListInput, SkillListOutput, PtyListInput, PtyListOutput, PtyCreateInput, PtyCreateOutput, PtyGetInput, PtyGetOutput, PtyUpdateInput, PtyUpdateOutput, PtyRemoveInput, PtyConnectTokenInput, PtyConnectTokenOutput, ExperimentalPersistentPtyListInput, ExperimentalPersistentPtyListOutput, ExperimentalPersistentPtyCreateInput, ExperimentalPersistentPtyCreateOutput, ExperimentalPersistentPtyGetInput, ExperimentalPersistentPtyGetOutput, ExperimentalPersistentPtyUpdateInput, ExperimentalPersistentPtyUpdateOutput, ExperimentalPersistentPtySnapshotInput, ExperimentalPersistentPtySnapshotOutput, ExperimentalPersistentPtyRemoveInput, ExperimentalPersistentPtyConnectTokenInput, ShellListInput, ShellListOutput, ShellCreateInput, ShellCreateOutput, ShellGetInput, ShellGetOutput, ShellTimeoutInput, ShellTimeoutOutput, ShellOutputInput, ShellOutputOutput, ShellRemoveInput, ReferenceListInput, ReferenceListOutput, WorktreeListInput, WorktreeCreateInput, WorktreeRemoveInput, WorktreeRefreshInput, WorkspaceCreateInput, WorkspaceDestroyInput, VcsGetInput, VcsGetOutput, VcsStatusInput, VcsStatusOutput, VcsBranchesInput, VcsBranchesOutput, VcsDiffInput, VcsDiffOutput, DebugLocationListOutput, DebugLocationEvictInput, MigrationV1StatusOutput, WebsearchProvidersInput, WebsearchProvidersOutput, WebsearchQueryInput, WebsearchQueryOutput, ConfigGetInput, ConfigGetOutput } from "../api/api.js";
4
+ import type { HealthGetOutput, ServerGetOutput, LocationGetInput, AgentListInput, AgentListOutput, AgentGetInput, AgentGetOutput, PluginListInput, PluginListOutput, SessionListInput, SessionListOutput, SessionStatsInput, SessionStatsOutput, SessionCreateInput, SessionImportInput, SessionExportInput, SessionExportOutput, SessionActiveOutput, SessionGetInput, SessionRemoveInput, SessionForkInput, SessionSwitchAgentInput, SessionSwitchModelInput, SessionRenameInput, SessionMoveInput, SessionPromptInput, SessionCommandInput, SessionSkillInput, SessionSyntheticInput, SessionShellInput, SessionCompactInput, SessionWaitInput, SessionRevertStageInput, SessionRevertClearInput, SessionRevertCommitInput, SessionContextInput, SessionContextOutput, SessionInboxListInput, SessionInboxListOutput, SessionInboxCancelInput, SessionInboxSteerInput, SessionInboxQueueInput, SessionInstructionsEntryListInput, SessionInstructionsEntryListOutput, SessionInstructionsEntryPutInput, SessionInstructionsEntryRemoveInput, SessionGenerateInput, SessionGenerateOutput, SessionLogInput, SessionLogOutput, SessionInterruptInput, SessionInterruptOutput, SessionBackgroundInput, SessionMessageInput, SessionMessageUpdateInput, SessionEnvironmentInput, SessionViewInput, MessageListInput, MessageListOutput, ModelListInput, ModelListOutput, ModelDefaultInput, ModelDefaultOutput, GenerateTextInput, GenerateTextOutput, ProviderListInput, ProviderListOutput, ProviderGetInput, ProviderGetOutput, IntegrationListInput, IntegrationListOutput, IntegrationGetInput, IntegrationGetOutput, IntegrationWellknownAddInput, IntegrationConnectKeyInput, IntegrationOauthConnectInput, IntegrationOauthConnectOutput, IntegrationOauthStatusInput, IntegrationOauthStatusOutput, IntegrationOauthCompleteInput, IntegrationOauthCancelInput, IntegrationCommandConnectInput, IntegrationCommandConnectOutput, IntegrationCommandStatusInput, IntegrationCommandStatusOutput, IntegrationCommandCancelInput, McpListInput, McpListOutput, McpAddInput, McpRemoveInput, McpConnectInput, McpDisconnectInput, McpResourceCatalogInput, McpResourceCatalogOutput, CredentialUpdateInput, CredentialActivateInput, CredentialRemoveInput, ProjectListOutput, ProjectUpdateInput, ProjectCurrentInput, FormRequestListInput, FormRequestListOutput, FormListInput, FormListOutput, FormCreateInput, FormGetInput, FormStateInput, FormReplyInput, FormCancelInput, PermissionRequestListInput, PermissionRequestListOutput, PermissionSavedListInput, PermissionSavedListOutput, PermissionSavedRemoveInput, PermissionCreateInput, PermissionCreateOutput, PermissionListInput, PermissionListOutput, PermissionGetInput, PermissionReplyInput, FileListInput, FileListOutput, FileFindInput, FileFindOutput, CommandListInput, CommandListOutput, SkillListInput, SkillListOutput, PtyListInput, PtyListOutput, PtyCreateInput, PtyCreateOutput, PtyGetInput, PtyGetOutput, PtyUpdateInput, PtyUpdateOutput, PtyRemoveInput, PtyConnectTokenInput, PtyConnectTokenOutput, ExperimentalPersistentPtyListInput, ExperimentalPersistentPtyListOutput, ExperimentalPersistentPtyCreateInput, ExperimentalPersistentPtyCreateOutput, ExperimentalPersistentPtyHandoffOutput, ExperimentalPersistentPtyGetInput, ExperimentalPersistentPtyGetOutput, ExperimentalPersistentPtyUpdateInput, ExperimentalPersistentPtyUpdateOutput, ExperimentalPersistentPtySnapshotInput, ExperimentalPersistentPtySnapshotOutput, ExperimentalPersistentPtyRemoveInput, ExperimentalPersistentPtyConnectTokenInput, ShellListInput, ShellListOutput, ShellCreateInput, ShellCreateOutput, ShellGetInput, ShellGetOutput, ShellTimeoutInput, ShellTimeoutOutput, ShellOutputInput, ShellOutputOutput, ShellRemoveInput, ReferenceListInput, ReferenceListOutput, WorktreeListInput, WorktreeCreateInput, WorktreeRemoveInput, WorktreeRefreshInput, WorkspaceCreateInput, WorkspaceDestroyInput, VcsGetInput, VcsGetOutput, VcsStatusInput, VcsStatusOutput, VcsBranchesInput, VcsBranchesOutput, VcsDiffInput, VcsDiffOutput, DebugLocationListOutput, DebugLocationEvictInput, MigrationV1StatusOutput, WebsearchProvidersInput, WebsearchProvidersOutput, WebsearchQueryInput, WebsearchQueryOutput, ConfigGetInput, ConfigGetOutput } from "../api/api.js";
5
5
  import { ClientError } from "./client-error.js";
6
6
  export declare const make: (options?: {
7
7
  readonly baseUrl?: URL | string;
@@ -2444,6 +2444,7 @@ export declare const make: (options?: {
2444
2444
  list: (input: ExperimentalPersistentPtyListInput) => Effect.Effect<ExperimentalPersistentPtyListOutput, import("@opencode-ai/protocol/errors").InvalidRequestError | import("@opencode-ai/protocol/errors").UnauthorizedError | import("@opencode-ai/protocol/errors").ServiceUnavailableError | Schema.SchemaError | ClientError | HttpClientError.HttpClientError, never>;
2445
2445
  create: (input: ExperimentalPersistentPtyCreateInput) => Effect.Effect<ExperimentalPersistentPtyCreateOutput, import("@opencode-ai/protocol/errors").InvalidRequestError | import("@opencode-ai/protocol/errors").UnauthorizedError | import("@opencode-ai/protocol/errors").ServiceUnavailableError | Schema.SchemaError | ClientError | HttpClientError.HttpClientError, never>;
2446
2446
  shutdown: () => Effect.Effect<void, import("@opencode-ai/protocol/errors").InvalidRequestError | import("@opencode-ai/protocol/errors").UnauthorizedError | import("@opencode-ai/protocol/errors").ServiceUnavailableError | Schema.SchemaError | ClientError | HttpClientError.HttpClientError, never>;
2447
+ handoff: () => Effect.Effect<ExperimentalPersistentPtyHandoffOutput, import("@opencode-ai/protocol/errors").InvalidRequestError | import("@opencode-ai/protocol/errors").UnauthorizedError | import("@opencode-ai/protocol/errors").ServiceUnavailableError | Schema.SchemaError | ClientError | HttpClientError.HttpClientError, never>;
2447
2448
  get: (input: ExperimentalPersistentPtyGetInput) => Effect.Effect<ExperimentalPersistentPtyGetOutput, import("@opencode-ai/protocol/errors").InvalidRequestError | import("@opencode-ai/protocol/errors").UnauthorizedError | import("@opencode-ai/protocol/errors").ServiceUnavailableError | import("@opencode-ai/protocol/errors").PtyNotFoundError | Schema.SchemaError | ClientError | HttpClientError.HttpClientError, never>;
2448
2449
  update: (input: ExperimentalPersistentPtyUpdateInput) => Effect.Effect<ExperimentalPersistentPtyUpdateOutput, import("@opencode-ai/protocol/errors").InvalidRequestError | import("@opencode-ai/protocol/errors").UnauthorizedError | import("@opencode-ai/protocol/errors").ServiceUnavailableError | import("@opencode-ai/protocol/errors").PtyNotFoundError | Schema.SchemaError | ClientError | HttpClientError.HttpClientError, never>;
2449
2450
  snapshot: (input: ExperimentalPersistentPtySnapshotInput) => Effect.Effect<ExperimentalPersistentPtySnapshotOutput, import("@opencode-ai/protocol/errors").InvalidRequestError | import("@opencode-ai/protocol/errors").UnauthorizedError | import("@opencode-ai/protocol/errors").ServiceUnavailableError | import("@opencode-ai/protocol/errors").PtyNotFoundError | Schema.SchemaError | ClientError | HttpClientError.HttpClientError, never>;
@@ -435,6 +435,7 @@ const EndpointExperimentalPersistentPtyCreate = (raw) => (input) => preserveEffe
435
435
  },
436
436
  }).pipe(Effect.mapError(mapClientError), Effect.map((value) => value.data)));
437
437
  const EndpointExperimentalPersistentPtyShutdown = (raw) => () => preserveEffect()(raw["persistentPty.shutdown"]({}).pipe(Effect.mapError(mapClientError)));
438
+ const EndpointExperimentalPersistentPtyHandoff = (raw) => () => preserveEffect()(raw["persistentPty.handoff"]({}).pipe(Effect.mapError(mapClientError)));
438
439
  const EndpointExperimentalPersistentPtyGet = (raw) => (input) => preserveEffect()(raw["persistentPty.get"]({ params: { ptyID: input["ptyID"] } }).pipe(Effect.mapError(mapClientError), Effect.map((value) => value.data)));
439
440
  const EndpointExperimentalPersistentPtyUpdate = (raw) => (input) => preserveEffect()(raw["persistentPty.update"]({
440
441
  params: { ptyID: input["ptyID"] },
@@ -451,6 +452,7 @@ const adaptGroupExperimental = (raw) => ({
451
452
  list: EndpointExperimentalPersistentPtyList(raw),
452
453
  create: EndpointExperimentalPersistentPtyCreate(raw),
453
454
  shutdown: EndpointExperimentalPersistentPtyShutdown(raw),
455
+ handoff: EndpointExperimentalPersistentPtyHandoff(raw),
454
456
  get: EndpointExperimentalPersistentPtyGet(raw),
455
457
  update: EndpointExperimentalPersistentPtyUpdate(raw),
456
458
  snapshot: EndpointExperimentalPersistentPtySnapshot(raw),
@@ -5,6 +5,7 @@ import { join } from "node:path";
5
5
  import { contenderFailure, contenderFinished, spawnServiceContender, } from "../service-contender.js";
6
6
  import { defaultEnsureTiming, ensureTiming } from "../service-timing.js";
7
7
  import { matchesVersion } from "../service-version.js";
8
+ import { PtyHandoff } from "../pty-handoff.js";
8
9
  export * from "../service.js";
9
10
  // Find, start, and stop the local opencode background service.
10
11
  //
@@ -55,9 +56,10 @@ export const ensure = Effect.fn("service.ensure")(function* (options = {}) {
55
56
  const [command, ...args] = options.command ?? ["opencode", "serve", "--service"];
56
57
  if (command === undefined)
57
58
  return yield* Effect.fail(new Error("Missing service command"));
59
+ const env = yield* Effect.tryPromise(() => PtyHandoff.environment(options.file ?? fallback(), options.env));
58
60
  return yield* Effect.try({
59
61
  try: () => {
60
- return spawnServiceContender(command, args, options.env);
62
+ return spawnServiceContender(command, args, env);
61
63
  },
62
64
  catch: (cause) => new Error("Failed to start server", { cause }),
63
65
  });
@@ -73,6 +75,8 @@ export const ensure = Effect.fn("service.ensure")(function* (options = {}) {
73
75
  };
74
76
  if (timeouts.count >= 3) {
75
77
  yield* announce("missing");
78
+ yield* Effect.logWarning("Background service is unresponsive; recovery cannot preserve persistent terminals");
79
+ yield* Effect.tryPromise(() => PtyHandoff.clear(options.file ?? fallback()));
76
80
  yield* terminate(info, options, timing);
77
81
  timeouts = undefined;
78
82
  lastSpawn = Date.now() - spawnDelay;
@@ -83,13 +87,22 @@ export const ensure = Effect.fn("service.ensure")(function* (options = {}) {
83
87
  if (service !== undefined) {
84
88
  spawnDelay = timing.spawnDelay;
85
89
  const compatible = !service.legacy && matchesVersion(service.version, options);
86
- if (compatible && service.state === "ready")
90
+ if (compatible && service.state === "ready") {
91
+ yield* Effect.tryPromise(() => PtyHandoff.complete(options.file ?? fallback(), service.info));
87
92
  return Option.some(service);
93
+ }
88
94
  if (compatible && service.state === "failed")
89
95
  return yield* Effect.fail(new Error("Background service failed to start"));
90
96
  if (compatible)
91
97
  return Option.none();
92
98
  yield* announce("version-mismatch", service.version);
99
+ if (!service.legacy && service.state === "ready")
100
+ yield* Effect.tryPromise(() => PtyHandoff.prepare(options.file ?? fallback(), service.info, timing.requestTimeout));
101
+ else {
102
+ if (!service.legacy)
103
+ yield* Effect.logWarning("Background service is not ready; replacement cannot preserve persistent terminals");
104
+ yield* Effect.tryPromise(() => PtyHandoff.clear(options.file ?? fallback()));
105
+ }
93
106
  yield* terminate(service.info, options, timing).pipe(Effect.ignore);
94
107
  lastSpawn = 0;
95
108
  return Option.none();
@@ -121,6 +134,7 @@ export const ensure = Effect.fn("service.ensure")(function* (options = {}) {
121
134
  });
122
135
  /** Stop the registered local service. */
123
136
  export const stop = Effect.fn("service.stop")(function* (options = {}) {
137
+ yield* Effect.tryPromise(() => PtyHandoff.clear(options.file ?? fallback()));
124
138
  const info = yield* read(options.file);
125
139
  if (info !== undefined)
126
140
  yield* terminate(info, options, defaultEnsureTiming);
@@ -1,4 +1,4 @@
1
- import type { ServerGetOutput, LocationGetInput, LocationGetOutput, AgentListInput, AgentListOutput, AgentGetInput, AgentGetOutput, PluginListInput, PluginListOutput, SessionListInput, SessionStatsInput, SessionCreateInput, SessionImportInput, SessionExportInput, SessionGetInput, SessionRemoveInput, SessionForkInput, SessionSwitchAgentInput, SessionSwitchModelInput, SessionRenameInput, SessionMoveInput, SessionPromptInput, SessionCommandInput, SessionSkillInput, SessionSyntheticInput, SessionShellInput, SessionCompactInput, SessionWaitInput, SessionRevertStageInput, SessionRevertClearInput, SessionRevertCommitInput, SessionContextInput, SessionInboxListInput, SessionInboxCancelInput, SessionInboxSteerInput, SessionInboxQueueInput, SessionInstructionsEntryListInput, SessionInstructionsEntryPutInput, SessionInstructionsEntryRemoveInput, SessionGenerateInput, SessionLogInput, SessionLogOutput, SessionInterruptInput, SessionBackgroundInput, SessionMessageInput, SessionMessageUpdateInput, SessionEnvironmentInput, SessionViewInput, MessageListInput, ModelListInput, ModelListOutput, ModelDefaultInput, ModelDefaultOutput, GenerateTextInput, ProviderListInput, ProviderListOutput, ProviderGetInput, ProviderGetOutput, IntegrationListInput, IntegrationListOutput, IntegrationGetInput, IntegrationGetOutput, IntegrationWellknownAddInput, IntegrationConnectKeyInput, IntegrationOauthConnectInput, IntegrationOauthConnectOutput, IntegrationOauthStatusInput, IntegrationOauthStatusOutput, IntegrationOauthCompleteInput, IntegrationOauthCancelInput, IntegrationCommandConnectInput, IntegrationCommandConnectOutput, IntegrationCommandStatusInput, IntegrationCommandStatusOutput, IntegrationCommandCancelInput, McpListInput, McpListOutput, McpAddInput, McpRemoveInput, McpConnectInput, McpDisconnectInput, McpResourceCatalogInput, McpResourceCatalogOutput, CredentialUpdateInput, CredentialActivateInput, CredentialRemoveInput, ProjectListOutput, ProjectUpdateInput, ProjectCurrentInput, FormRequestListInput, FormRequestListOutput, FormListInput, FormCreateInput, FormGetInput, FormStateInput, FormReplyInput, FormCancelInput, PermissionRequestListInput, PermissionRequestListOutput, PermissionSavedListInput, PermissionSavedRemoveInput, PermissionCreateInput, PermissionListInput, PermissionGetInput, PermissionReplyInput, FileReadInput, FileReadOutput, FileListInput, FileListOutput, FileFindInput, FileFindOutput, CommandListInput, CommandListOutput, SkillListInput, SkillListOutput, EventSubscribeOutput, PtyListInput, PtyListOutput, PtyCreateInput, PtyCreateOutput, PtyGetInput, PtyGetOutput, PtyUpdateInput, PtyUpdateOutput, PtyRemoveInput, PtyConnectTokenInput, PtyConnectTokenOutput, ExperimentalPersistentPtyListInput, ExperimentalPersistentPtyCreateInput, ExperimentalPersistentPtyGetInput, ExperimentalPersistentPtyUpdateInput, ExperimentalPersistentPtySnapshotInput, ExperimentalPersistentPtyRemoveInput, ExperimentalPersistentPtyConnectTokenInput, ShellListInput, ShellListOutput, ShellCreateInput, ShellCreateOutput, ShellGetInput, ShellGetOutput, ShellTimeoutInput, ShellTimeoutOutput, ShellOutputInput, ShellOutputOutput, ShellRemoveInput, ReferenceListInput, ReferenceListOutput, WorktreeListInput, WorktreeCreateInput, WorktreeRemoveInput, WorktreeRefreshInput, WorkspaceCreateInput, WorkspaceDestroyInput, VcsGetInput, VcsGetOutput, VcsStatusInput, VcsStatusOutput, VcsBranchesInput, VcsBranchesOutput, VcsDiffInput, VcsDiffOutput, DebugLocationListOutput, DebugLocationEvictInput, MigrationV1StatusOutput, WebsearchProvidersInput, WebsearchProvidersOutput, WebsearchQueryInput, WebsearchQueryOutput, ConfigGetInput, ConfigGetOutput } from "./types.js";
1
+ import type { ServerGetOutput, LocationGetInput, LocationGetOutput, AgentListInput, AgentListOutput, AgentGetInput, AgentGetOutput, PluginListInput, PluginListOutput, SessionListInput, SessionStatsInput, SessionCreateInput, SessionImportInput, SessionExportInput, SessionGetInput, SessionRemoveInput, SessionForkInput, SessionSwitchAgentInput, SessionSwitchModelInput, SessionRenameInput, SessionMoveInput, SessionPromptInput, SessionCommandInput, SessionSkillInput, SessionSyntheticInput, SessionShellInput, SessionCompactInput, SessionWaitInput, SessionRevertStageInput, SessionRevertClearInput, SessionRevertCommitInput, SessionContextInput, SessionInboxListInput, SessionInboxCancelInput, SessionInboxSteerInput, SessionInboxQueueInput, SessionInstructionsEntryListInput, SessionInstructionsEntryPutInput, SessionInstructionsEntryRemoveInput, SessionGenerateInput, SessionLogInput, SessionLogOutput, SessionInterruptInput, SessionBackgroundInput, SessionMessageInput, SessionMessageUpdateInput, SessionEnvironmentInput, SessionViewInput, MessageListInput, ModelListInput, ModelListOutput, ModelDefaultInput, ModelDefaultOutput, GenerateTextInput, ProviderListInput, ProviderListOutput, ProviderGetInput, ProviderGetOutput, IntegrationListInput, IntegrationListOutput, IntegrationGetInput, IntegrationGetOutput, IntegrationWellknownAddInput, IntegrationConnectKeyInput, IntegrationOauthConnectInput, IntegrationOauthConnectOutput, IntegrationOauthStatusInput, IntegrationOauthStatusOutput, IntegrationOauthCompleteInput, IntegrationOauthCancelInput, IntegrationCommandConnectInput, IntegrationCommandConnectOutput, IntegrationCommandStatusInput, IntegrationCommandStatusOutput, IntegrationCommandCancelInput, McpListInput, McpListOutput, McpAddInput, McpRemoveInput, McpConnectInput, McpDisconnectInput, McpResourceCatalogInput, McpResourceCatalogOutput, CredentialUpdateInput, CredentialActivateInput, CredentialRemoveInput, ProjectListOutput, ProjectUpdateInput, ProjectCurrentInput, FormRequestListInput, FormRequestListOutput, FormListInput, FormCreateInput, FormGetInput, FormStateInput, FormReplyInput, FormCancelInput, PermissionRequestListInput, PermissionRequestListOutput, PermissionSavedListInput, PermissionSavedRemoveInput, PermissionCreateInput, PermissionListInput, PermissionGetInput, PermissionReplyInput, FileReadInput, FileReadOutput, FileListInput, FileListOutput, FileFindInput, FileFindOutput, CommandListInput, CommandListOutput, SkillListInput, SkillListOutput, EventSubscribeOutput, PtyListInput, PtyListOutput, PtyCreateInput, PtyCreateOutput, PtyGetInput, PtyGetOutput, PtyUpdateInput, PtyUpdateOutput, PtyRemoveInput, PtyConnectTokenInput, PtyConnectTokenOutput, ExperimentalPersistentPtyListInput, ExperimentalPersistentPtyCreateInput, ExperimentalPersistentPtyHandoffOutput, ExperimentalPersistentPtyGetInput, ExperimentalPersistentPtyUpdateInput, ExperimentalPersistentPtySnapshotInput, ExperimentalPersistentPtyRemoveInput, ExperimentalPersistentPtyConnectTokenInput, ShellListInput, ShellListOutput, ShellCreateInput, ShellCreateOutput, ShellGetInput, ShellGetOutput, ShellTimeoutInput, ShellTimeoutOutput, ShellOutputInput, ShellOutputOutput, ShellRemoveInput, ReferenceListInput, ReferenceListOutput, WorktreeListInput, WorktreeCreateInput, WorktreeRemoveInput, WorktreeRefreshInput, WorkspaceCreateInput, WorkspaceDestroyInput, VcsGetInput, VcsGetOutput, VcsStatusInput, VcsStatusOutput, VcsBranchesInput, VcsBranchesOutput, VcsDiffInput, VcsDiffOutput, DebugLocationListOutput, DebugLocationEvictInput, MigrationV1StatusOutput, WebsearchProvidersInput, WebsearchProvidersOutput, WebsearchQueryInput, WebsearchQueryOutput, ConfigGetInput, ConfigGetOutput } from "./types.js";
2
2
  export interface ClientOptions {
3
3
  readonly baseUrl: string;
4
4
  readonly fetch?: typeof globalThis.fetch;
@@ -198,6 +198,7 @@ export declare function make(options: ClientOptions): {
198
198
  list: (input: ExperimentalPersistentPtyListInput, requestOptions?: RequestOptions) => Promise<import("./types.js").PersistentPtyInfo[]>;
199
199
  create: (input: ExperimentalPersistentPtyCreateInput, requestOptions?: RequestOptions) => Promise<import("./types.js").PersistentPtyInfo>;
200
200
  shutdown: (requestOptions?: RequestOptions) => Promise<void>;
201
+ handoff: (requestOptions?: RequestOptions) => Promise<ExperimentalPersistentPtyHandoffOutput>;
201
202
  get: (input: ExperimentalPersistentPtyGetInput, requestOptions?: RequestOptions) => Promise<import("./types.js").PersistentPtyInfo>;
202
203
  update: (input: ExperimentalPersistentPtyUpdateInput, requestOptions?: RequestOptions) => Promise<import("./types.js").PersistentPtyInfo>;
203
204
  snapshot: (input: ExperimentalPersistentPtySnapshotInput, requestOptions?: RequestOptions) => Promise<import("./types.js").PersistentPtySnapshot>;
@@ -1046,6 +1046,13 @@ export function make(options) {
1046
1046
  declaredStatuses: [503, 401, 400],
1047
1047
  empty: true,
1048
1048
  }, requestOptions),
1049
+ handoff: (requestOptions) => request({
1050
+ method: "POST",
1051
+ path: `/api/experimental/persistent-pty/handoff`,
1052
+ successStatus: 200,
1053
+ declaredStatuses: [503, 401, 400],
1054
+ empty: false,
1055
+ }, requestOptions),
1049
1056
  get: (input, requestOptions) => request({
1050
1057
  method: "GET",
1051
1058
  path: `/api/experimental/persistent-pty/${encodeURIComponent(input.ptyID)}`,
@@ -562,6 +562,12 @@ export type PtyTicketConnectToken = {
562
562
  ticket: string;
563
563
  expires_in: number;
564
564
  };
565
+ export type PersistentPtyHandoff = {
566
+ directory: string;
567
+ instanceID: string;
568
+ ticket: string;
569
+ expiresAt: number;
570
+ };
565
571
  export type ShellInfo1 = {
566
572
  id: string;
567
573
  status: "running" | "exited" | "timeout" | "killed";
@@ -7844,6 +7850,9 @@ export type ExperimentalPersistentPtyCreateOutput = {
7844
7850
  data: PersistentPtyInfo;
7845
7851
  }["data"];
7846
7852
  export type ExperimentalPersistentPtyShutdownOutput = void;
7853
+ export type ExperimentalPersistentPtyHandoffOutput = {
7854
+ handoff: PersistentPtyHandoff | null;
7855
+ };
7847
7856
  export type ExperimentalPersistentPtyGetInput = {
7848
7857
  readonly ptyID: {
7849
7858
  readonly ptyID: string;
@@ -4,6 +4,7 @@ import { join } from "node:path";
4
4
  import { contenderFailure, contenderFinished, spawnServiceContender, } from "../service-contender.js";
5
5
  import { defaultEnsureTiming, ensureTiming } from "../service-timing.js";
6
6
  import { matchesVersion } from "../service-version.js";
7
+ import { PtyHandoff } from "../pty-handoff.js";
7
8
  export * from "../service.js";
8
9
  // Find, start, and stop the local opencode background service.
9
10
  //
@@ -34,12 +35,12 @@ export async function ensure(options = {}) {
34
35
  announced = true;
35
36
  options.onStart?.(reason, previousVersion);
36
37
  };
37
- const spawnContender = () => {
38
+ const spawnContender = async () => {
38
39
  const [command, ...args] = options.command ?? ["opencode", "serve", "--service"];
39
40
  if (command === undefined)
40
41
  throw new Error("Missing service command");
41
42
  try {
42
- return spawnServiceContender(command, args, options.env);
43
+ return spawnServiceContender(command, args, await PtyHandoff.environment(options.file ?? fallback(), options.env));
43
44
  }
44
45
  catch (cause) {
45
46
  throw new Error("Failed to start server", { cause });
@@ -57,6 +58,8 @@ export async function ensure(options = {}) {
57
58
  };
58
59
  if (timeouts.count >= 3) {
59
60
  announce("missing");
61
+ console.warn("Background service is unresponsive; recovery cannot preserve persistent terminals");
62
+ await PtyHandoff.clear(options.file ?? fallback());
60
63
  await terminate(registration.info, options, timing);
61
64
  timeouts = undefined;
62
65
  lastSpawn = Date.now() - spawnDelay;
@@ -68,12 +71,21 @@ export async function ensure(options = {}) {
68
71
  spawnDelay = timing.spawnDelay;
69
72
  const service = registration.service;
70
73
  const compatible = !service.legacy && matchesVersion(service.version, options);
71
- if (compatible && service.state === "ready")
74
+ if (compatible && service.state === "ready") {
75
+ await PtyHandoff.complete(options.file ?? fallback(), service.info);
72
76
  return service.endpoint;
77
+ }
73
78
  if (compatible && service.state === "failed")
74
79
  throw new Error("Background service failed to start");
75
80
  if (!compatible) {
76
81
  announce("version-mismatch", service.version);
82
+ if (!service.legacy && service.state === "ready")
83
+ await PtyHandoff.prepare(options.file ?? fallback(), service.info, timing.requestTimeout);
84
+ else {
85
+ if (!service.legacy)
86
+ console.warn("Background service is not ready; replacement cannot preserve persistent terminals");
87
+ await PtyHandoff.clear(options.file ?? fallback());
88
+ }
77
89
  await terminate(service.info, options, timing).catch(() => undefined);
78
90
  lastSpawn = 0;
79
91
  }
@@ -92,7 +104,7 @@ export async function ensure(options = {}) {
92
104
  // Keep one candidate plus one lock probe so a pre-lock stall cannot block recovery.
93
105
  if (contenders.size < 2 && Date.now() - lastSpawn >= spawnDelay) {
94
106
  announce("missing");
95
- contenders.add(spawnContender());
107
+ contenders.add(await spawnContender());
96
108
  lastSpawn = Date.now();
97
109
  }
98
110
  }
@@ -105,6 +117,7 @@ export async function ensure(options = {}) {
105
117
  }
106
118
  /** Stop the registered local service. */
107
119
  export async function stop(options = {}) {
120
+ await PtyHandoff.clear(options.file ?? fallback());
108
121
  const info = await read(options.file);
109
122
  if (info !== undefined)
110
123
  await terminate(info, options, defaultEnsureTiming);
@@ -0,0 +1,9 @@
1
+ export * as PtyHandoff from "./pty-handoff.js";
2
+ import type { Info } from "./service.js";
3
+ /** Publish the ticket before stopping its owner so every replacement contender can adopt it. */
4
+ export declare function prepare(file: string, info: Info, timeout: number): Promise<void>;
5
+ export declare function environment(file: string, env?: Readonly<Record<string, string>>): Promise<{
6
+ OPENCODE_PTY_HANDOFF: string | undefined;
7
+ }>;
8
+ export declare function complete(file: string, info: Info): Promise<void>;
9
+ export declare function clear(file: string): Promise<void>;
@@ -0,0 +1,121 @@
1
+ export * as PtyHandoff from "./pty-handoff.js";
2
+ import { readFile, rename, rm, writeFile } from "node:fs/promises";
3
+ /** Publish the ticket before stopping its owner so every replacement contender can adopt it. */
4
+ export async function prepare(file, info, timeout) {
5
+ const existing = await read(file);
6
+ if (existing !== undefined && existing.expiresAt > Date.now() && same(existing.source, info))
7
+ return;
8
+ const { ClientError, OpenCode } = await import("./promise/index.js");
9
+ const client = OpenCode.make({
10
+ baseUrl: info.url,
11
+ headers: info.password === undefined
12
+ ? undefined
13
+ : { authorization: "Basic " + Buffer.from(`opencode:${info.password}`).toString("base64") },
14
+ });
15
+ const missing = (error) => error instanceof ClientError &&
16
+ error.reason === "UnexpectedStatus" &&
17
+ typeof error.cause === "object" &&
18
+ error.cause !== null &&
19
+ "status" in error.cause &&
20
+ error.cause.status === 404;
21
+ const result = await client.experimental.persistentPty.handoff({ signal: AbortSignal.timeout(timeout) }).then((value) => ({ value }), (cause) => ({ cause }));
22
+ if ("cause" in result) {
23
+ // Another caller may already have prepared and stopped this server.
24
+ const concurrent = await read(file);
25
+ if (concurrent !== undefined && concurrent.expiresAt > Date.now() && same(concurrent.source, info))
26
+ return;
27
+ if (!missing(result.cause))
28
+ throw new Error("Failed to prepare persistent terminals for service replacement", { cause: result.cause });
29
+ console.warn("Background service cannot hand off persistent terminals; shutting them down before replacement");
30
+ await client.experimental.persistentPty
31
+ .shutdown({ signal: AbortSignal.timeout(timeout) })
32
+ .catch((cause) => {
33
+ if (missing(cause))
34
+ return;
35
+ throw new Error("Failed to shut down persistent terminals before service replacement", { cause });
36
+ });
37
+ await publish(file, info, null);
38
+ return;
39
+ }
40
+ const body = result.value;
41
+ if (typeof body !== "object" || body === null || !("handoff" in body))
42
+ throw new Error("Invalid persistent terminal handoff response");
43
+ if (body.handoff === null) {
44
+ await publish(file, info, null);
45
+ return;
46
+ }
47
+ if (!isHandoff(body.handoff) || body.handoff.expiresAt <= Date.now())
48
+ throw new Error("Invalid or expired persistent terminal handoff");
49
+ await publish(file, info, body.handoff);
50
+ }
51
+ async function publish(file, info, handoff) {
52
+ const temporary = `${file}.pty-handoff.${crypto.randomUUID()}.tmp`;
53
+ await writeFile(temporary, JSON.stringify({
54
+ source: { id: info.id, pid: info.pid, url: info.url },
55
+ handoff,
56
+ expiresAt: handoff?.expiresAt ?? Date.now() + 30_000,
57
+ }), { mode: 0o600, flag: "wx" });
58
+ await rename(temporary, file + ".pty-handoff").finally(() => rm(temporary, { force: true }));
59
+ }
60
+ export async function environment(file, env) {
61
+ const record = await read(file);
62
+ const current = await readFile(file, "utf8")
63
+ .then((text) => JSON.parse(text))
64
+ .catch(() => undefined);
65
+ const handoff = record !== undefined && record.expiresAt > Date.now() && (current === undefined || same(record.source, current))
66
+ ? record.handoff
67
+ : undefined;
68
+ return { ...env, OPENCODE_PTY_HANDOFF: handoff == null ? undefined : JSON.stringify(handoff) };
69
+ }
70
+ export async function complete(file, info) {
71
+ const record = await read(file);
72
+ if (record !== undefined && !same(record.source, info))
73
+ await clear(file);
74
+ }
75
+ export async function clear(file) {
76
+ await rm(file + ".pty-handoff", { force: true });
77
+ }
78
+ async function read(file) {
79
+ const value = await readFile(file + ".pty-handoff", "utf8")
80
+ .then((text) => JSON.parse(text))
81
+ .catch(() => undefined);
82
+ if (typeof value !== "object" || value === null || !("source" in value) || !("handoff" in value))
83
+ return;
84
+ if (typeof value.source !== "object" || value.source === null)
85
+ return;
86
+ if (!("pid" in value.source) || typeof value.source.pid !== "number")
87
+ return;
88
+ if (!("url" in value.source) || typeof value.source.url !== "string")
89
+ return;
90
+ if ("id" in value.source && typeof value.source.id !== "string")
91
+ return;
92
+ if (value.handoff !== null && !isHandoff(value.handoff))
93
+ return;
94
+ if (!("expiresAt" in value) || typeof value.expiresAt !== "number" || !Number.isFinite(value.expiresAt))
95
+ return;
96
+ return {
97
+ source: {
98
+ id: "id" in value.source && typeof value.source.id === "string" ? value.source.id : undefined,
99
+ pid: value.source.pid,
100
+ url: value.source.url,
101
+ },
102
+ handoff: value.handoff,
103
+ expiresAt: value.expiresAt,
104
+ };
105
+ }
106
+ function same(left, right) {
107
+ return left.id === right.id && left.pid === right.pid && left.url === right.url;
108
+ }
109
+ function isHandoff(value) {
110
+ return (typeof value === "object" &&
111
+ value !== null &&
112
+ "directory" in value &&
113
+ typeof value.directory === "string" &&
114
+ "instanceID" in value &&
115
+ typeof value.instanceID === "string" &&
116
+ "ticket" in value &&
117
+ typeof value.ticket === "string" &&
118
+ "expiresAt" in value &&
119
+ typeof value.expiresAt === "number" &&
120
+ Number.isFinite(value.expiresAt));
121
+ }
@@ -6,6 +6,6 @@ export type ServiceContender = {
6
6
  readonly stderr: () => string;
7
7
  readonly release: () => void;
8
8
  };
9
- export declare function spawnServiceContender(command: string, args: ReadonlyArray<string>, env?: Readonly<Record<string, string>>): ServiceContender;
9
+ export declare function spawnServiceContender(command: string, args: ReadonlyArray<string>, env?: Readonly<Record<string, string | undefined>>): ServiceContender;
10
10
  export declare function contenderFailure(contender: ServiceContender): Error | undefined;
11
11
  export declare function contenderFinished(contender: ServiceContender): boolean;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "$schema": "https://json.schemastore.org/package.json",
3
3
  "name": "@opencode-ai/client",
4
- "version": "0.0.0-dev-18400",
4
+ "version": "0.0.0-dev-18407",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "repository": {
@@ -57,8 +57,8 @@
57
57
  "typecheck": "tsgo --noEmit"
58
58
  },
59
59
  "dependencies": {
60
- "@opencode-ai/schema": "0.0.0-dev-18400",
61
- "@opencode-ai/protocol": "0.0.0-dev-18400"
60
+ "@opencode-ai/schema": "0.0.0-dev-18407",
61
+ "@opencode-ai/protocol": "0.0.0-dev-18407"
62
62
  },
63
63
  "peerDependencies": {
64
64
  "effect": "4.0.0-rc.111",
@@ -74,7 +74,7 @@
74
74
  },
75
75
  "devDependencies": {
76
76
  "@effect/platform-node": "4.0.0-rc.111",
77
- "@opencode-ai/httpapi-codegen": "0.0.0-dev-18400",
77
+ "@opencode-ai/httpapi-codegen": "0.0.0-dev-18407",
78
78
  "@tsconfig/bun": "1.0.9",
79
79
  "@types/bun": "1.3.13",
80
80
  "@typescript/native-preview": "7.0.0-dev.20251207.1",