@tt-a1i/openpi 0.5.0 → 0.6.1

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.
Files changed (80) hide show
  1. package/README.md +30 -20
  2. package/SETUP.md +10 -4
  3. package/THIRD_PARTY_NOTICES.md +16 -0
  4. package/bin/openpi.js +25 -15
  5. package/extensions/ai-providers/LICENSE.upstream +23 -0
  6. package/extensions/ai-providers/README.md +65 -0
  7. package/extensions/ai-providers/antigravity/credentials.ts +52 -0
  8. package/extensions/ai-providers/antigravity/discovery.ts +130 -0
  9. package/extensions/ai-providers/antigravity/google-conversion.ts +455 -0
  10. package/extensions/ai-providers/antigravity/models.ts +84 -0
  11. package/extensions/ai-providers/antigravity/oauth.ts +700 -0
  12. package/extensions/ai-providers/antigravity/provider.ts +1116 -0
  13. package/extensions/ai-providers/antigravity/routing.ts +340 -0
  14. package/extensions/ai-providers/antigravity/with-resolvers.d.ts +19 -0
  15. package/extensions/ai-providers/cursor/constants.ts +5 -0
  16. package/extensions/ai-providers/cursor/credentials.ts +14 -0
  17. package/extensions/ai-providers/cursor/discovery.ts +291 -0
  18. package/extensions/ai-providers/cursor/input-images.ts +105 -0
  19. package/extensions/ai-providers/cursor/models.ts +45 -0
  20. package/extensions/ai-providers/cursor/oauth.ts +263 -0
  21. package/extensions/ai-providers/cursor/proto.ts +1271 -0
  22. package/extensions/ai-providers/cursor/protobuf.ts +1181 -0
  23. package/extensions/ai-providers/cursor/provider.ts +1431 -0
  24. package/extensions/ai-providers/cursor/proxy.ts +213 -0
  25. package/extensions/ai-providers/cursor/tool-bridge.ts +68 -0
  26. package/extensions/ai-providers/cursor/with-resolvers.d.ts +12 -0
  27. package/extensions/ai-providers/index.ts +86 -0
  28. package/extensions/ai-providers/oauth-adapter.ts +81 -0
  29. package/extensions/ai-providers/usage.ts +10 -0
  30. package/extensions/background-terminals/index.ts +8 -1
  31. package/extensions/background-terminals/src/manager.ts +3 -5
  32. package/extensions/background-terminals/src/result-delivery.ts +43 -23
  33. package/extensions/cron/index.ts +68 -27
  34. package/extensions/cron/schedule.ts +5 -1
  35. package/extensions/model-info/cache-diagnostics.ts +220 -0
  36. package/extensions/model-info/index.ts +45 -1
  37. package/extensions/plan-mode/index.ts +75 -4
  38. package/extensions/setup/index.ts +15 -3
  39. package/extensions/shared/child-session.ts +39 -5
  40. package/extensions/shared/completion-inbox.ts +193 -0
  41. package/extensions/shared/setup-config.ts +10 -1
  42. package/extensions/shared/structured-output.ts +154 -0
  43. package/extensions/subagents/index.ts +64 -7
  44. package/extensions/subagents/src/agent-types.ts +5 -17
  45. package/extensions/subagents/src/backends/pi.ts +130 -48
  46. package/extensions/subagents/src/backends/tool-preview.ts +29 -0
  47. package/extensions/subagents/src/domain.ts +16 -1
  48. package/extensions/subagents/src/manager.ts +7 -71
  49. package/extensions/subagents/src/prompt.ts +19 -5
  50. package/extensions/subagents/src/result-artifact.ts +32 -0
  51. package/extensions/subagents/src/result-delivery.ts +33 -14
  52. package/extensions/subagents/src/runtime.ts +10 -3
  53. package/extensions/ui-customization/footer.ts +16 -5
  54. package/extensions/user-input-fold/index.ts +42 -6
  55. package/extensions/web/index.ts +25 -2
  56. package/extensions/workflows/acceptance.ts +43 -19
  57. package/extensions/workflows/completion-projection.ts +3 -1
  58. package/extensions/workflows/dashboard.ts +147 -21
  59. package/extensions/workflows/index.ts +75 -20
  60. package/extensions/workflows/model.ts +5 -1
  61. package/extensions/workflows/progress-projection.ts +7 -1
  62. package/extensions/workflows/prompt.ts +4 -10
  63. package/extensions/workflows/result-delivery.ts +96 -22
  64. package/extensions/workflows/retention.ts +6 -0
  65. package/extensions/workflows/runner.ts +11 -233
  66. package/extensions/workflows/sandbox.ts +4 -0
  67. package/package.json +7 -7
  68. package/skills/subagents/REFERENCE.md +9 -9
  69. package/skills/subagents/SKILL.md +2 -1
  70. package/skills/workflows/REFERENCE.md +5 -3
  71. package/skills/workflows/SKILL.md +1 -1
  72. package/web/adapter/pi-adapter.ts +3 -0
  73. package/web/host/pi-coding-agent-entry.ts +162 -0
  74. package/web/host/web-host.ts +330 -50
  75. package/web/protocol/types.ts +5 -0
  76. package/web/runtime/pi-runtime.ts +240 -25
  77. package/web/runtime/types.ts +32 -1
  78. package/web/ui/app.js +343 -41
  79. package/web/ui/index.html +3 -0
  80. package/web/ui/styles.css +119 -37
@@ -0,0 +1,213 @@
1
+ import * as http2 from "node:http2";
2
+ import * as net from "node:net";
3
+ import * as tls from "node:tls";
4
+
5
+ export interface CursorHttp2ConnectOptions {
6
+ signal?: AbortSignal;
7
+ timeoutMs?: number;
8
+ }
9
+
10
+ function isLocalOrMetadataHost(hostname: string): boolean {
11
+ const host = hostname.toLowerCase().replace(/^\[|\]$/g, "");
12
+ if (
13
+ host === "localhost" ||
14
+ host.endsWith(".localhost") ||
15
+ host === "metadata.google.internal"
16
+ ) {
17
+ return true;
18
+ }
19
+ if (host === "::" || host === "::1" || /^f[cd]/.test(host)) return true;
20
+ const ipv4 = /^(\d{1,3})\.(\d{1,3})\./.exec(host);
21
+ if (!ipv4) return false;
22
+ const first = Number(ipv4[1]);
23
+ const second = Number(ipv4[2]);
24
+ return (
25
+ first === 0 ||
26
+ first === 10 ||
27
+ first === 127 ||
28
+ (first === 169 && second === 254) ||
29
+ (first === 172 && second >= 16 && second <= 31) ||
30
+ (first === 192 && second === 168)
31
+ );
32
+ }
33
+
34
+ function shouldBypassProxy(target: URL): boolean {
35
+ if (isLocalOrMetadataHost(target.hostname)) return true;
36
+ const noProxy = process.env.NO_PROXY || process.env.no_proxy;
37
+ if (!noProxy) return false;
38
+ const targetHost = target.hostname.toLowerCase().replace(/^\[|\]$/g, "");
39
+ const targetPort =
40
+ target.port || (target.protocol === "https:" ? "443" : "80");
41
+ for (const rawRule of noProxy.split(/[,\s]+/)) {
42
+ let rule = rawRule.trim().toLowerCase();
43
+ if (!rule) continue;
44
+ if (rule === "*") return true;
45
+ let rulePort: string | undefined;
46
+ const portMatch = /^(\[[^\]]+\]|[^:]+):(\d+)$/.exec(rule);
47
+ if (portMatch) {
48
+ rule = portMatch[1]!;
49
+ rulePort = portMatch[2];
50
+ }
51
+ if (rulePort && rulePort !== targetPort) continue;
52
+ rule = rule.replace(/^\[|\]$/g, "").replace(/^\./, "");
53
+ if (targetHost === rule || targetHost.endsWith(`.${rule}`)) return true;
54
+ }
55
+ return false;
56
+ }
57
+
58
+ /** Resolve Cursor's provider override first, then the standard proxy variables. */
59
+ export function resolveCursorProxy(target: URL): string | undefined {
60
+ if (shouldBypassProxy(target)) return undefined;
61
+ const protocolProxy =
62
+ target.protocol === "https:"
63
+ ? process.env.HTTPS_PROXY || process.env.https_proxy
64
+ : process.env.HTTP_PROXY || process.env.http_proxy;
65
+ return [
66
+ process.env.PI_PROXY_CURSOR,
67
+ process.env.PI_PROXY,
68
+ protocolProxy,
69
+ process.env.ALL_PROXY || process.env.all_proxy,
70
+ ]
71
+ .map((value) => value?.trim())
72
+ .find((value): value is string => Boolean(value));
73
+ }
74
+
75
+ function connectProxyTunnel(
76
+ proxyUrl: URL,
77
+ targetUrl: URL,
78
+ options: CursorHttp2ConnectOptions,
79
+ ): Promise<net.Socket> {
80
+ if (!["http:", "https:"].includes(proxyUrl.protocol)) {
81
+ return Promise.reject(
82
+ new Error(`Unsupported Cursor proxy protocol: ${proxyUrl.protocol}`),
83
+ );
84
+ }
85
+ if (options.signal?.aborted) {
86
+ return Promise.reject(new Error("Cursor proxy tunnel aborted"));
87
+ }
88
+ const proxyTls = proxyUrl.protocol === "https:";
89
+ const proxyPort = Number(proxyUrl.port || (proxyTls ? 443 : 80));
90
+ const targetPort = Number(
91
+ targetUrl.port || (targetUrl.protocol === "https:" ? 443 : 80),
92
+ );
93
+ const targetAuthority = `${targetUrl.hostname}:${targetPort}`;
94
+ let proxyAuthorization: string | undefined;
95
+ if (proxyUrl.username || proxyUrl.password) {
96
+ try {
97
+ const credentials = `${decodeURIComponent(proxyUrl.username)}:${decodeURIComponent(proxyUrl.password)}`;
98
+ proxyAuthorization = Buffer.from(credentials).toString("base64");
99
+ } catch (cause) {
100
+ return Promise.reject(
101
+ new Error("Cursor proxy credentials contain invalid percent-encoding", {
102
+ cause,
103
+ }),
104
+ );
105
+ }
106
+ }
107
+ const { promise, resolve, reject } = Promise.withResolvers<net.Socket>();
108
+ let rawSocket: net.Socket | undefined;
109
+ let targetSocket: net.Socket | undefined;
110
+ let timer: ReturnType<typeof setTimeout> | undefined;
111
+ let response = Buffer.alloc(0);
112
+ let settled = false;
113
+
114
+ const cleanup = () => {
115
+ if (timer) clearTimeout(timer);
116
+ options.signal?.removeEventListener("abort", onAbort);
117
+ rawSocket?.removeListener("error", onError);
118
+ rawSocket?.removeListener(proxyTls ? "secureConnect" : "connect", onReady);
119
+ rawSocket?.removeListener("data", onData);
120
+ targetSocket?.removeListener("error", onError);
121
+ targetSocket?.removeListener("secureConnect", onTargetReady);
122
+ };
123
+ const fail = (error: Error) => {
124
+ if (settled) return;
125
+ settled = true;
126
+ cleanup();
127
+ targetSocket?.destroy();
128
+ rawSocket?.destroy();
129
+ reject(error);
130
+ };
131
+ const succeed = (socket: net.Socket) => {
132
+ if (settled) return;
133
+ settled = true;
134
+ cleanup();
135
+ resolve(socket);
136
+ };
137
+ const onAbort = () => fail(new Error("Cursor proxy tunnel aborted"));
138
+ const onError = (error: Error) => fail(error);
139
+ const onTargetReady = () => {
140
+ if (targetSocket) succeed(targetSocket);
141
+ };
142
+ const onData = (chunk: Buffer) => {
143
+ if (!rawSocket) return;
144
+ response = Buffer.concat([response, chunk]);
145
+ if (response.length > 64 * 1024) {
146
+ fail(new Error("Cursor proxy response headers exceed 64 KiB"));
147
+ return;
148
+ }
149
+ const headerEnd = response.indexOf("\r\n\r\n");
150
+ if (headerEnd === -1) return;
151
+ const statusLine = response
152
+ .subarray(0, headerEnd)
153
+ .toString("latin1")
154
+ .split("\r\n")[0];
155
+ if (!/^HTTP\/1\.[01] 200\b/.test(statusLine ?? "")) {
156
+ fail(
157
+ new Error(
158
+ `Cursor proxy tunnel failed: ${statusLine || "invalid response"}`,
159
+ ),
160
+ );
161
+ return;
162
+ }
163
+ rawSocket.removeListener("data", onData);
164
+ if (targetUrl.protocol !== "https:") {
165
+ succeed(rawSocket);
166
+ return;
167
+ }
168
+ targetSocket = tls.connect({
169
+ socket: rawSocket,
170
+ servername: targetUrl.hostname,
171
+ ALPNProtocols: ["h2"],
172
+ });
173
+ targetSocket.once("error", onError);
174
+ targetSocket.once("secureConnect", onTargetReady);
175
+ };
176
+ const onReady = () => {
177
+ if (!rawSocket) return;
178
+ let request = `CONNECT ${targetAuthority} HTTP/1.1\r\nHost: ${targetAuthority}\r\n`;
179
+ if (proxyAuthorization) {
180
+ request += `Proxy-Authorization: Basic ${proxyAuthorization}\r\n`;
181
+ }
182
+ rawSocket.on("data", onData);
183
+ rawSocket.write(`${request}\r\n`);
184
+ };
185
+
186
+ options.signal?.addEventListener("abort", onAbort, { once: true });
187
+ if (options.timeoutMs !== undefined && options.timeoutMs > 0) {
188
+ const timeoutMs = Math.floor(options.timeoutMs);
189
+ timer = setTimeout(
190
+ () =>
191
+ fail(new Error(`Cursor proxy tunnel timed out after ${timeoutMs}ms`)),
192
+ timeoutMs,
193
+ );
194
+ }
195
+ rawSocket = proxyTls
196
+ ? tls.connect({ host: proxyUrl.hostname, port: proxyPort })
197
+ : net.connect({ host: proxyUrl.hostname, port: proxyPort });
198
+ rawSocket.once("error", onError);
199
+ rawSocket.once(proxyTls ? "secureConnect" : "connect", onReady);
200
+ return promise;
201
+ }
202
+
203
+ /** Open Cursor's HTTP/2 session directly or through an HTTP CONNECT proxy. */
204
+ export async function connectCursorHttp2(
205
+ baseUrl: string,
206
+ options: CursorHttp2ConnectOptions = {},
207
+ ): Promise<http2.ClientHttp2Session> {
208
+ const target = new URL(baseUrl);
209
+ const proxy = resolveCursorProxy(target);
210
+ if (!proxy) return http2.connect(target);
211
+ const socket = await connectProxyTunnel(new URL(proxy), target, options);
212
+ return http2.connect(target, { createConnection: () => socket });
213
+ }
@@ -0,0 +1,68 @@
1
+ /** Translate Cursor MCP requests into Pi calls; execution stays in Pi's loop. */
2
+ import type { Context, ToolCall } from "@earendil-works/pi-ai/compat";
3
+ import { type McpArgs, McpToolDefinitionSchema } from "./proto.ts";
4
+ import {
5
+ create,
6
+ decodeJsonValue,
7
+ encodeJsonValue,
8
+ type JsonValue,
9
+ } from "./protobuf.ts";
10
+
11
+ export const CURSOR_PI_PROVIDER = "openpi";
12
+ export const CURSOR_PI_TOOLS_SYSTEM_PROMPT =
13
+ "Use only the provided openpi MCP tools. These are the active Pi tools and Pi owns their execution and permissions. Do not use Cursor-native filesystem, shell, editing, web, task, or interaction tools. When tool results appear in conversation history, continue from those results. Do not repeat a completed tool call.";
14
+
15
+ export function buildCursorTools(tools: Context["tools"]) {
16
+ return (tools ?? []).map((tool) => {
17
+ const schema: JsonValue = JSON.parse(JSON.stringify(tool.parameters));
18
+ return create(McpToolDefinitionSchema, {
19
+ name: tool.name,
20
+ providerIdentifier: CURSOR_PI_PROVIDER,
21
+ toolName: tool.name,
22
+ description: tool.description,
23
+ inputSchema: encodeJsonValue(schema),
24
+ inputSchemaJson: JSON.stringify(schema),
25
+ });
26
+ });
27
+ }
28
+
29
+ export function decodeCursorTool(
30
+ args: McpArgs,
31
+ tools: Context["tools"],
32
+ ): ToolCall {
33
+ const name = args.toolName || args.name;
34
+ if (
35
+ args.providerIdentifier !== CURSOR_PI_PROVIDER ||
36
+ (args.serverIdentifier && args.serverIdentifier !== CURSOR_PI_PROVIDER) ||
37
+ !name ||
38
+ (args.name && args.name !== name) ||
39
+ !tools?.some((tool) => tool.name === name)
40
+ ) {
41
+ throw new Error("Cursor requested an unadvertised Pi tool identity");
42
+ }
43
+ if (!args.toolCallId.trim())
44
+ throw new Error("Cursor MCP tool call has no identity");
45
+ const values: Record<string, unknown> = {};
46
+ for (const [key, value] of Object.entries(args.args)) {
47
+ // google.protobuf.Value, not JSON text. Define own properties so keys such
48
+ // as __proto__ cannot alter the decoded argument object's prototype.
49
+ if (!value.length || ![8, 17, 26, 32, 42, 50].includes(value[0]!)) {
50
+ throw new Error("Cursor MCP argument is not a protobuf JSON value");
51
+ }
52
+ const decoded = decodeJsonValue(value);
53
+ const validateJson = (item: JsonValue): void => {
54
+ if (typeof item === "number" && !Number.isFinite(item))
55
+ throw new Error("Cursor MCP argument contains a non-finite number");
56
+ if (item && typeof item === "object")
57
+ for (const child of Object.values(item)) validateJson(child);
58
+ };
59
+ validateJson(decoded);
60
+ Object.defineProperty(values, key, {
61
+ value: decoded,
62
+ enumerable: true,
63
+ configurable: true,
64
+ writable: true,
65
+ });
66
+ }
67
+ return { type: "toolCall", id: args.toolCallId, name, arguments: values };
68
+ }
@@ -0,0 +1,12 @@
1
+ /** Node >=22 provides Promise.withResolvers; the repo's ES2022 lib needs a shim. */
2
+ declare global {
3
+ interface PromiseConstructor {
4
+ withResolvers<T>(): {
5
+ promise: Promise<T>;
6
+ resolve: (value: T | PromiseLike<T>) => void;
7
+ reject: (reason?: unknown) => void;
8
+ };
9
+ }
10
+ }
11
+
12
+ export {};
@@ -0,0 +1,86 @@
1
+ /**
2
+ * ai-providers — OAuth-backed model providers for pi.
3
+ *
4
+ * Adds OAuth-backed Google Antigravity and Cursor model providers. Both are
5
+ * inert until the user logs in and selects one of their models. Cursor uses
6
+ * AgentService/Run with an experimental bridge to normal Pi tool calls.
7
+ * Cursor-native coding tools are not exposed or executed by this extension.
8
+ *
9
+ * Wire protocol: Cloud Code Assist `v1internal:streamGenerateContent` over
10
+ * SSE (see antigravity/provider.ts). Reference implementation: oh-my-pi's
11
+ * google-gemini-cli provider (shared google-gemini-cli/google-antigravity).
12
+ */
13
+
14
+ import { createProvider, type ProviderStreams } from "@earendil-works/pi-ai";
15
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
16
+ import { encodeApiKey } from "./antigravity/credentials.ts";
17
+ import { fetchAntigravityModels } from "./antigravity/discovery.ts";
18
+ import {
19
+ ANTIGRAVITY_API_URL,
20
+ ANTIGRAVITY_MODELS,
21
+ } from "./antigravity/models.ts";
22
+ import {
23
+ loginAntigravity,
24
+ refreshAntigravityToken,
25
+ } from "./antigravity/oauth.ts";
26
+ import { streamAntigravity } from "./antigravity/provider.ts";
27
+ import { getCursorApiKey } from "./cursor/credentials.ts";
28
+ import { fetchCursorModels } from "./cursor/discovery.ts";
29
+ import { transformCursorImageInput } from "./cursor/input-images.ts";
30
+ import { CURSOR_MODELS } from "./cursor/models.ts";
31
+ import { loginCursor, refreshCursorToken } from "./cursor/oauth.ts";
32
+ import { streamCursor } from "./cursor/provider.ts";
33
+ import { createOAuthAuth } from "./oauth-adapter.ts";
34
+
35
+ function providerStreams(
36
+ streamSimple: ProviderStreams["streamSimple"],
37
+ ): ProviderStreams {
38
+ return {
39
+ stream: (model, context, options) => streamSimple(model, context, options),
40
+ streamSimple,
41
+ };
42
+ }
43
+
44
+ export default function authProviders(pi: ExtensionAPI) {
45
+ pi.on("input", transformCursorImageInput);
46
+
47
+ pi.registerProvider(
48
+ createProvider({
49
+ id: "google-antigravity",
50
+ name: "Google Antigravity",
51
+ baseUrl: ANTIGRAVITY_API_URL,
52
+ api: providerStreams(streamAntigravity),
53
+ auth: {
54
+ oauth: createOAuthAuth({
55
+ name: "Google (Antigravity)",
56
+ isSubscription: true,
57
+ login: loginAntigravity,
58
+ refreshToken: refreshAntigravityToken,
59
+ getApiKey: encodeApiKey,
60
+ }),
61
+ },
62
+ models: ANTIGRAVITY_MODELS,
63
+ fetchModels: fetchAntigravityModels,
64
+ }),
65
+ );
66
+
67
+ pi.registerProvider(
68
+ createProvider({
69
+ id: "cursor",
70
+ name: "Cursor",
71
+ baseUrl: "https://api2.cursor.sh",
72
+ api: providerStreams(streamCursor),
73
+ auth: {
74
+ oauth: createOAuthAuth({
75
+ name: "Cursor",
76
+ isSubscription: true,
77
+ login: loginCursor,
78
+ refreshToken: refreshCursorToken,
79
+ getApiKey: getCursorApiKey,
80
+ }),
81
+ },
82
+ models: CURSOR_MODELS,
83
+ fetchModels: fetchCursorModels,
84
+ }),
85
+ );
86
+ }
@@ -0,0 +1,81 @@
1
+ import type {
2
+ ModelAuth,
3
+ OAuthAuth,
4
+ OAuthCredential,
5
+ OAuthCredentials,
6
+ OAuthLoginCallbacks,
7
+ ProviderAuthInteraction,
8
+ } from "@earendil-works/pi-ai";
9
+
10
+ interface LegacyOAuthImplementation {
11
+ name: string;
12
+ isSubscription?: boolean;
13
+ login(callbacks: CancellableOAuthLoginCallbacks): Promise<OAuthCredentials>;
14
+ refreshToken(
15
+ credential: OAuthCredentials,
16
+ signal: AbortSignal,
17
+ ): Promise<OAuthCredentials>;
18
+ getApiKey(credential: OAuthCredentials): string | Promise<string>;
19
+ }
20
+
21
+ export type CancellableOAuthLoginCallbacks = Omit<
22
+ OAuthLoginCallbacks,
23
+ "onManualCodeInput"
24
+ > & {
25
+ onManualCodeInput?(signal?: AbortSignal): Promise<string>;
26
+ };
27
+
28
+ function legacyCallbacks(
29
+ interaction: ProviderAuthInteraction,
30
+ ): CancellableOAuthLoginCallbacks {
31
+ return {
32
+ signal: interaction.signal,
33
+ onAuth: (info) => interaction.notify({ type: "auth_url", ...info }),
34
+ onDeviceCode: (info) =>
35
+ interaction.notify({ type: "device_code", ...info }),
36
+ onProgress: (message) => interaction.notify({ type: "progress", message }),
37
+ onPrompt: (prompt) =>
38
+ interaction.prompt({
39
+ type: "text",
40
+ message: prompt.message,
41
+ placeholder: prompt.placeholder,
42
+ }),
43
+ onManualCodeInput: (signal) =>
44
+ interaction.prompt({
45
+ type: "manual_code",
46
+ message: "Paste the authorization callback URL or code",
47
+ signal,
48
+ }),
49
+ onSelect: (prompt) =>
50
+ interaction.prompt({
51
+ type: "select",
52
+ message: prompt.message,
53
+ options: prompt.options,
54
+ }),
55
+ };
56
+ }
57
+
58
+ function canonicalCredential(credentials: OAuthCredentials): OAuthCredential {
59
+ return { ...credentials, type: "oauth" };
60
+ }
61
+
62
+ /** Adapt pi's retained extension OAuth callbacks to the native Provider API. */
63
+ export function createOAuthAuth(
64
+ implementation: LegacyOAuthImplementation,
65
+ ): OAuthAuth {
66
+ return {
67
+ name: implementation.name,
68
+ isSubscription: implementation.isSubscription,
69
+ login: async (interaction) =>
70
+ canonicalCredential(
71
+ await implementation.login(legacyCallbacks(interaction)),
72
+ ),
73
+ refresh: async (credential, signal) =>
74
+ canonicalCredential(
75
+ await implementation.refreshToken(credential, signal),
76
+ ),
77
+ toAuth: async (credential): Promise<ModelAuth> => ({
78
+ apiKey: await implementation.getApiKey(credential),
79
+ }),
80
+ };
81
+ }
@@ -0,0 +1,10 @@
1
+ export function emptyUsage() {
2
+ return {
3
+ input: 0,
4
+ output: 0,
5
+ cacheRead: 0,
6
+ cacheWrite: 0,
7
+ totalTokens: 0,
8
+ cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 },
9
+ };
10
+ }
@@ -32,6 +32,7 @@ import {
32
32
  OPENPI_TOOL_SURFACE,
33
33
  patchOwnedTools,
34
34
  } from "../shared/tool-surface.ts";
35
+ import { completionOwnerFor } from "../shared/completion-inbox.ts";
35
36
  import {
36
37
  projectBackgroundTerminalCapability,
37
38
  registerWebCapability,
@@ -105,7 +106,12 @@ export default function (pi: ExtensionAPI) {
105
106
  let ui: ExtensionUIContext | undefined;
106
107
  let unsubStatus: (() => void) | undefined;
107
108
  let startReservations = 0;
108
- const resultDelivery = createDeferredResultDelivery<TerminalSnapshot>();
109
+ const resultDelivery = createDeferredResultDelivery<TerminalSnapshot>({
110
+ owner: () =>
111
+ sessionContext
112
+ ? completionOwnerFor(sessionContext.sessionManager)
113
+ : undefined,
114
+ });
109
115
  const hideLifecycleTools = () =>
110
116
  patchOwnedTools(pi, "background", {
111
117
  disable: OPENPI_TOOL_SURFACE.background.deferred,
@@ -245,6 +251,7 @@ export default function (pi: ExtensionAPI) {
245
251
  const flushResults = (wake: boolean) => {
246
252
  const snaps = resultDelivery.drain(MAX_RUNNING);
247
253
  if (!deliverResults(snaps, wake)) resultDelivery.restore(snaps);
254
+ else resultDelivery.acknowledge(snaps);
248
255
  };
249
256
 
250
257
  const idleResultBatcher = createIdleResultBatcher({
@@ -395,11 +395,9 @@ export async function signalWindowsProcessTree(
395
395
  : attempt.outcome === "timed_out"
396
396
  ? `taskkill timed out after ${attempt.timeoutMs}ms; helper ${attempt.helperClosed ? "closed after SIGKILL" : `did not close within an additional ${attempt.helperCloseTimeoutMs}ms`}`
397
397
  : `taskkill exited ${attempt.exitCode ?? "without a code"}${attempt.signal ? ` (${attempt.signal})` : ""}`;
398
- if (
399
- attempt.outcome === "timed_out" &&
400
- !attempt.helperClosed &&
401
- !targetExited()
402
- ) {
398
+ // Closing the helper or observing the shell exit does not prove that all
399
+ // descendants exited. Preserve uncertainty instead of killing only the shell.
400
+ if (attempt.outcome === "timed_out") {
403
401
  return { outcome: "unresolved", detail };
404
402
  }
405
403
  // A failed graceful taskkill must leave the shell PID alive for the
@@ -1,44 +1,64 @@
1
1
  import type { ConsumableResultDeliveryQueue } from "../../shared/result-delivery.ts";
2
+ import {
3
+ type CompletionOwner,
4
+ createCompletionInbox,
5
+ } from "../../shared/completion-inbox.ts";
2
6
 
3
7
  /**
4
- * Deferred one-shot delivery map (same semantics as subagents'): a settled
5
- * terminal's result is held here until it is either drained into a follow-up
6
- * message or consumed by a tool call (bg_kill / bg_status) that already
7
- * returned the settlement itself. Keyed by id, so double delivery is
8
- * structurally impossible — whoever drains first wins.
8
+ * Deferred one-shot delivery adapter (same semantics as subagents'): a
9
+ * settled terminal's result is held in the shared inbox until it is either
10
+ * drained into a follow-up message or consumed by a tool call (bg_kill /
11
+ * bg_status) that already returned the settlement itself. Stable ids make
12
+ * double delivery structurally impossible — whoever claims first wins.
9
13
  */
10
- export function createDeferredResultDelivery<T extends { id: string }>() {
11
- const pending = new Map<string, T>();
14
+ export function createDeferredResultDelivery<T extends { id: string }>(
15
+ options: { readonly owner?: () => CompletionOwner | undefined } = {},
16
+ ) {
17
+ const inbox = createCompletionInbox<T>();
18
+ const owner = options.owner ?? (() => ({ sessionId: "test", epoch: 0 }));
12
19
 
13
20
  const queue = {
14
21
  defer(result: T) {
15
- pending.set(result.id, result);
16
- return pending.size;
22
+ const currentOwner = owner();
23
+ inbox.defer(
24
+ {
25
+ deliveryId: `background:${result.id}`,
26
+ owner: currentOwner ?? { sessionId: "unowned", epoch: 0 },
27
+ producer: "background",
28
+ producerId: result.id,
29
+ terminalRef: { kind: "terminal-snapshot", id: result.id },
30
+ wake: "producer-policy",
31
+ payload: result,
32
+ },
33
+ currentOwner,
34
+ );
35
+ return inbox.size();
17
36
  },
18
37
  consume(ids: Iterable<string>) {
19
- for (const id of ids) pending.delete(id);
38
+ inbox.consume("background", ids);
20
39
  },
21
40
  drain(maxResults = Number.POSITIVE_INFINITY) {
22
- const results: T[] = [];
23
- for (const [id, result] of pending) {
24
- if (results.length >= maxResults) break;
25
- results.push(result);
26
- pending.delete(id);
27
- }
28
- return results;
41
+ return inbox
42
+ .claim(owner(), maxResults)
43
+ .map((envelope) => envelope.payload);
29
44
  },
30
45
  restore(results: readonly T[]) {
31
- const current = [...pending.values()];
32
- pending.clear();
33
- for (const result of results) pending.set(result.id, result);
34
- for (const result of current) pending.set(result.id, result);
46
+ inbox.retryClaimed(
47
+ "background",
48
+ results.map((result) => result.id),
49
+ owner(),
50
+ );
51
+ },
52
+ acknowledge(results: readonly T[]) {
53
+ inbox.acknowledge(results.map((result) => `background:${result.id}`));
35
54
  },
36
55
  size() {
37
- return pending.size;
56
+ return inbox.size();
38
57
  },
39
58
  clear() {
40
- pending.clear();
59
+ inbox.clear();
41
60
  },
61
+ inspectDeadLetters: inbox.inspectDeadLetters,
42
62
  };
43
63
  return queue satisfies ConsumableResultDeliveryQueue<T>;
44
64
  }