pi-maestro-teammate 2.2.0 → 2.3.0
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.
- package/README.md +11 -1
- package/package.json +17 -5
- package/src/backends/acp-cli-catalog.ts +6 -0
- package/src/backends/acp-cli.ts +24 -8
- package/src/cli-tools/cli-tools-config.ts +86 -26
- package/src/cli-tools/local-acp.ts +8 -4
- package/src/completion-outbox/coordinator.ts +7 -12
- package/src/completion-outbox/file-store.ts +195 -123
- package/src/completion-outbox/types.ts +2 -0
- package/src/extension/herdr-window-provider.ts +794 -0
- package/src/extension/index.ts +1387 -286
- package/src/extension/mailbox/file-store.ts +25 -8
- package/src/extension/mailbox/host.ts +2 -0
- package/src/extension/mailbox/rollout.ts +12 -2
- package/src/extension/mailbox/router.ts +77 -33
- package/src/extension/mailbox/service.ts +4 -0
- package/src/extension/mailbox/types.ts +8 -1
- package/src/extension/monitor-window-lifecycle.ts +278 -0
- package/src/extension/monitor-window-state.ts +514 -0
- package/src/extension/monitor.ts +616 -15
- package/src/extension/remote-window-monitor.ts +357 -0
- package/src/extension/remote-window-session.ts +50 -0
- package/src/extension/runtime-actor-host.ts +113 -2
- package/src/extension/schemas.ts +112 -6
- package/src/extension/session-endpoints.ts +168 -20
- package/src/extension/teammate-helpers.ts +6 -0
- package/src/extension/teammate-proxy.ts +37 -0
- package/src/extension/tool-error-signal.ts +42 -0
- package/src/extension/workspace-peers.ts +8 -2848
- package/src/extension/workspace-turns-observation.ts +8 -5
- package/src/models/cli-add.ts +8 -2
- package/src/models/model-routing.ts +92 -0
- package/src/public/v1/index.ts +3 -0
- package/src/public/v1/monitor-window-state.ts +525 -0
- package/src/public/v1/session-history.ts +2 -0
- package/src/public/v1/ssh-hosts.ts +209 -0
- package/src/public/v2/runtime-broker.ts +6 -0
- package/src/public/v2/runtime.ts +3 -0
- package/src/remote/child-security.ts +30 -4
- package/src/remote/cli.ts +3 -1
- package/src/remote/config.ts +131 -17
- package/src/remote/driver.ts +15 -0
- package/src/remote/process-tree.ts +12 -28
- package/src/remote/protocol.ts +164 -1
- package/src/remote/server.ts +93 -1
- package/src/remote/ssh-exec.ts +17 -1
- package/src/remote/ssh.ts +162 -17
- package/src/remote/types.ts +29 -2
- package/src/remote/window-protocol.ts +501 -0
- package/src/remote/window-service.ts +566 -0
- package/src/runs/execution-infra.ts +61 -6
- package/src/runs/execution.ts +2 -0
- package/src/runs/pi-subprocess-attempt.ts +246 -99
- package/src/runtime-broker/client.ts +59 -11
- package/src/runtime-broker/mailbox-commit.ts +4 -3
- package/src/runtime-broker/session-commit.ts +121 -0
- package/src/runtime-v2/session-broker-read-model.ts +101 -0
- package/src/runtime-v2/session-domain.ts +397 -0
- package/src/runtime-v2/session-rollout.ts +90 -0
- package/src/sessions/session-core.ts +152 -19
- package/src/sessions/workspace-peer-core.ts +2993 -0
- package/src/shared/agent-status.ts +4 -1
- package/src/shared/types.ts +6 -3
- package/src/transcript/session-history.ts +758 -0
- package/src/tui/connection-wizards.ts +177 -16
- package/src/tui/locale-catalog-model.ts +36 -0
- package/src/tui/model-mapping-overlay.ts +60 -3
- package/src/tui/remote-config-pane.ts +114 -16
- package/src/tui/render.ts +504 -25
- package/types/cli-tools/cli-tools-config.d.ts +8 -3
- package/types/completion-outbox/file-store.d.ts +3 -5
- package/types/completion-outbox/types.d.ts +2 -0
- package/types/extension/herdr-window-provider.d.ts +165 -0
- package/types/extension/mailbox/host.d.ts +1 -0
- package/types/extension/mailbox/rollout.d.ts +6 -0
- package/types/extension/mailbox/router.d.ts +4 -0
- package/types/extension/mailbox/service.d.ts +4 -0
- package/types/extension/mailbox/types.d.ts +7 -0
- package/types/extension/monitor-window-lifecycle.d.ts +96 -0
- package/types/extension/monitor-window-state.d.ts +45 -0
- package/types/extension/monitor.d.ts +105 -0
- package/types/extension/remote-window-monitor.d.ts +52 -0
- package/types/extension/remote-window-session.d.ts +12 -0
- package/types/extension/runtime-actor-host.d.ts +3 -0
- package/types/extension/schemas.d.ts +19 -8
- package/types/extension/session-endpoints.d.ts +27 -3
- package/types/extension/teammate-helpers.d.ts +2 -0
- package/types/extension/tool-error-signal.d.ts +7 -0
- package/types/extension/workspace-peers.d.ts +4 -558
- package/types/models/model-routing.d.ts +17 -0
- package/types/public/v1/index.d.ts +3 -0
- package/types/public/v1/monitor-window-state.d.ts +183 -0
- package/types/public/v1/session-history.d.ts +2 -0
- package/types/public/v1/ssh-hosts.d.ts +24 -0
- package/types/public/v2/runtime-broker.d.ts +1 -0
- package/types/public/v2/runtime.d.ts +3 -0
- package/types/remote/child-security.d.ts +1 -0
- package/types/remote/config.d.ts +12 -4
- package/types/remote/driver.d.ts +8 -2
- package/types/remote/process-tree.d.ts +4 -4
- package/types/remote/protocol.d.ts +40 -4
- package/types/remote/server.d.ts +9 -1
- package/types/remote/ssh-exec.d.ts +3 -0
- package/types/remote/ssh.d.ts +9 -1
- package/types/remote/types.d.ts +22 -2
- package/types/remote/window-protocol.d.ts +127 -0
- package/types/remote/window-service.d.ts +21 -0
- package/types/runs/execution-infra.d.ts +3 -0
- package/types/runtime-broker/client.d.ts +2 -0
- package/types/runtime-broker/mailbox-commit.d.ts +2 -1
- package/types/runtime-broker/session-commit.d.ts +33 -0
- package/types/runtime-v2/session-broker-read-model.d.ts +16 -0
- package/types/runtime-v2/session-domain.d.ts +84 -0
- package/types/runtime-v2/session-rollout.d.ts +24 -0
- package/types/sessions/session-core.d.ts +47 -7
- package/types/sessions/workspace-peer-core.d.ts +603 -0
- package/types/shared/types.d.ts +4 -2
- package/types/transcript/session-history.d.ts +170 -0
- package/types/tui/connection-wizards.d.ts +13 -2
- package/types/tui/locale-catalog-model.d.ts +36 -0
- package/types/tui/model-mapping-overlay.d.ts +2 -0
- package/types/tui/remote-config-pane.d.ts +20 -0
- package/types/tui/render.d.ts +32 -1
package/README.md
CHANGED
|
@@ -187,7 +187,17 @@ project .pi/agents > project .agents > ~/.agents > legacy user directory > bundl
|
|
|
187
187
|
|
|
188
188
|
## Model Routing Profiles
|
|
189
189
|
|
|
190
|
-
Configure task-type defaults with `Alt+M` or `/teammate-models`. The Control Center's **Profiles** tab manages named routing Profiles shared by every project. Its **Routing** tab edits the active global Profile and combines built-in task types, discovered agent types, and types already present in that Profile.
|
|
190
|
+
Configure task-type defaults with `Alt+M` or `/teammate-models`. The Control Center's **Profiles** tab is the saved-template system: it manages multiple named routing Profiles shared by every project. Its **Routing** tab edits the active global Profile and combines built-in task types, discovered agent types, and types already present in that Profile.
|
|
191
|
+
|
|
192
|
+
Switch the current project's template directly by stable Profile ID or display name. With no argument, the command opens the Control Center on **Profiles**:
|
|
193
|
+
|
|
194
|
+
```text
|
|
195
|
+
/teammate-model fast-lane
|
|
196
|
+
/teammate-model Fast Lane
|
|
197
|
+
/teammate-model
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Command completion lists every saved template and marks the project-active and global-default entries. If display names are duplicated, use the stable ID. Direct switching disables but preserves project overrides, matching the Profiles UI.
|
|
191
201
|
|
|
192
202
|
The **Roles** tab lists discovered roles and their effective durable route. Role-scoped settings can be edited through the unified Settings surface as `role.<role-name>.model`, `role.<role-name>.fallbacks`, and `role.<role-name>.thinking`. These settings are stored in the same global Profile or project override file; package updates never rewrite role definitions or user routing data.
|
|
193
203
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "pi-maestro-teammate",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.3.0",
|
|
4
4
|
"description": "Pi extension — teammate agent dispatch with DAG task graphs, RPC messaging, and compact TUI",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -73,6 +73,10 @@
|
|
|
73
73
|
"types": "./types/public/v1/model-routing.d.ts",
|
|
74
74
|
"default": "./src/public/v1/model-routing.ts"
|
|
75
75
|
},
|
|
76
|
+
"./v1/monitor-window-state": {
|
|
77
|
+
"types": "./types/public/v1/monitor-window-state.d.ts",
|
|
78
|
+
"default": "./src/public/v1/monitor-window-state.ts"
|
|
79
|
+
},
|
|
76
80
|
"./v1/cli-tools": {
|
|
77
81
|
"types": "./types/public/v1/cli-tools.d.ts",
|
|
78
82
|
"default": "./src/public/v1/cli-tools.ts"
|
|
@@ -101,6 +105,10 @@
|
|
|
101
105
|
"types": "./types/public/v1/retry.d.ts",
|
|
102
106
|
"default": "./src/public/v1/retry.ts"
|
|
103
107
|
},
|
|
108
|
+
"./v1/session-history": {
|
|
109
|
+
"types": "./types/public/v1/session-history.d.ts",
|
|
110
|
+
"default": "./src/public/v1/session-history.ts"
|
|
111
|
+
},
|
|
104
112
|
"./v1/scheduler": {
|
|
105
113
|
"types": "./types/public/v1/scheduler.d.ts",
|
|
106
114
|
"default": "./src/public/v1/scheduler.ts"
|
|
@@ -113,6 +121,10 @@
|
|
|
113
121
|
"types": "./types/public/v1/supervision.d.ts",
|
|
114
122
|
"default": "./src/public/v1/supervision.ts"
|
|
115
123
|
},
|
|
124
|
+
"./v1/ssh-hosts": {
|
|
125
|
+
"types": "./types/public/v1/ssh-hosts.d.ts",
|
|
126
|
+
"default": "./src/public/v1/ssh-hosts.ts"
|
|
127
|
+
},
|
|
116
128
|
"./v1/workspace-projections": {
|
|
117
129
|
"types": "./types/public/v1/workspace-projections.d.ts",
|
|
118
130
|
"default": "./src/public/v1/workspace-projections.ts"
|
|
@@ -193,11 +205,11 @@
|
|
|
193
205
|
"pi-maestro-settings-core"
|
|
194
206
|
],
|
|
195
207
|
"devDependencies": {
|
|
196
|
-
"@earendil-works/pi-agent-core": "0.
|
|
208
|
+
"@earendil-works/pi-agent-core": "0.84.4",
|
|
197
209
|
"@types/cross-spawn": "6.0.6",
|
|
198
|
-
"@earendil-works/pi-ai": "0.
|
|
199
|
-
"@earendil-works/pi-coding-agent": "0.
|
|
200
|
-
"@earendil-works/pi-tui": "0.
|
|
210
|
+
"@earendil-works/pi-ai": "0.84.4",
|
|
211
|
+
"@earendil-works/pi-coding-agent": "0.84.4",
|
|
212
|
+
"@earendil-works/pi-tui": "0.84.4",
|
|
201
213
|
"@types/ssh2": "1.15.5",
|
|
202
214
|
"typebox": "1.3.7"
|
|
203
215
|
},
|
|
@@ -43,6 +43,9 @@ export const ACP_CLI_SETTINGS_CATALOGS: TranslationCatalogs = {
|
|
|
43
43
|
"acpCli.mode": "Launch mode",
|
|
44
44
|
"acpCli.mode.local": "Local process",
|
|
45
45
|
"acpCli.mode.ssh": "Remote over SSH",
|
|
46
|
+
"acpCli.sshHostRef": "/ssh host reference",
|
|
47
|
+
"acpCli.sshHostRef.description":
|
|
48
|
+
"Host id from the unlocked encrypted /ssh manager. Use it instead of every embedded SSH connection field; the host is resolved again for each run.",
|
|
46
49
|
"acpCli.host": "SSH host",
|
|
47
50
|
"acpCli.user": "SSH user",
|
|
48
51
|
"acpCli.port": "SSH port",
|
|
@@ -92,6 +95,9 @@ export const ACP_CLI_SETTINGS_CATALOGS: TranslationCatalogs = {
|
|
|
92
95
|
"acpCli.mode": "启动方式",
|
|
93
96
|
"acpCli.mode.local": "本地进程",
|
|
94
97
|
"acpCli.mode.ssh": "经 SSH 远程",
|
|
98
|
+
"acpCli.sshHostRef": "/ssh 主机引用",
|
|
99
|
+
"acpCli.sshHostRef.description":
|
|
100
|
+
"已解锁加密 /ssh 管理器中的主机 id。它必须替代全部内嵌 SSH 连接字段,并在每次运行时重新解析。",
|
|
95
101
|
"acpCli.host": "SSH 主机",
|
|
96
102
|
"acpCli.user": "SSH 用户",
|
|
97
103
|
"acpCli.port": "SSH 端口",
|
package/src/backends/acp-cli.ts
CHANGED
|
@@ -52,6 +52,7 @@ import type {
|
|
|
52
52
|
import {
|
|
53
53
|
cliToolArgv,
|
|
54
54
|
probeCliToolCommand,
|
|
55
|
+
sshHostConfigIssue,
|
|
55
56
|
type CliToolConfig,
|
|
56
57
|
} from "../cli-tools/cli-tools-config.ts";
|
|
57
58
|
import {
|
|
@@ -282,9 +283,18 @@ export const ACP_CLI_CONFIG_FIELDS: readonly BackendConfigField[] = [
|
|
|
282
283
|
labelKey: "acpCli.mode",
|
|
283
284
|
default: "local",
|
|
284
285
|
},
|
|
286
|
+
{
|
|
287
|
+
key: "sshHostRef",
|
|
288
|
+
kind: "text",
|
|
289
|
+
labelKey: "acpCli.sshHostRef",
|
|
290
|
+
descriptionKey: "acpCli.sshHostRef.description",
|
|
291
|
+
},
|
|
285
292
|
{ key: "host", kind: "text", labelKey: "acpCli.host" },
|
|
286
293
|
{ key: "user", kind: "text", labelKey: "acpCli.user" },
|
|
287
|
-
|
|
294
|
+
// Applied only to embedded SSH below. Leaving this declaration without a
|
|
295
|
+
// generic default lets validation distinguish an explicit override from a
|
|
296
|
+
// reference that must take its port exclusively from `/ssh`.
|
|
297
|
+
{ key: "port", kind: "integer", labelKey: "acpCli.port" },
|
|
288
298
|
{ key: "hostKeySha256", kind: "text", labelKey: "acpCli.hostKeySha256" },
|
|
289
299
|
{ key: "identityFile", kind: "path", labelKey: "acpCli.identityFile" },
|
|
290
300
|
{
|
|
@@ -420,6 +430,7 @@ async function installedLaunchConfig(
|
|
|
420
430
|
function launchConfigOf(config: Record<string, ConfigValue>): CliToolConfig {
|
|
421
431
|
const command = text(config, "command");
|
|
422
432
|
const cwd = text(config, "cwd");
|
|
433
|
+
const sshHostRef = text(config, "sshHostRef");
|
|
423
434
|
const host = text(config, "host");
|
|
424
435
|
const user = text(config, "user");
|
|
425
436
|
const port = count(config, "port");
|
|
@@ -432,6 +443,7 @@ function launchConfigOf(config: Record<string, ConfigValue>): CliToolConfig {
|
|
|
432
443
|
args: list(config, "args"),
|
|
433
444
|
...(cwd === undefined ? {} : { cwd }),
|
|
434
445
|
env: list(config, "env"),
|
|
446
|
+
...(sshHostRef === undefined ? {} : { sshHostRef }),
|
|
435
447
|
...(host === undefined ? {} : { host }),
|
|
436
448
|
...(user === undefined ? {} : { user }),
|
|
437
449
|
...(port === undefined ? {} : { port }),
|
|
@@ -605,14 +617,18 @@ export function createAcpCliBackend(
|
|
|
605
617
|
+ "parent process forwards by name; a name=value entry puts the value in the registration document",
|
|
606
618
|
);
|
|
607
619
|
}
|
|
608
|
-
// Checked at load rather than at launch:
|
|
609
|
-
//
|
|
610
|
-
//
|
|
620
|
+
// Checked at load rather than at launch: one SSH source must be complete
|
|
621
|
+
// and authoritative. A reference cannot be paired with even a seemingly
|
|
622
|
+
// harmless port override, because that would recreate two sources of
|
|
623
|
+
// truth for the connection.
|
|
611
624
|
if (mode === "ssh") {
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
625
|
+
const issue = sshHostConfigIssue(launchConfigOf(values));
|
|
626
|
+
if (issue !== undefined) errors.push(issue);
|
|
627
|
+
else if ((text(values, "sshHostRef") ?? "").trim().length === 0 && count(values, "port") === undefined) {
|
|
628
|
+
values.port = 22;
|
|
615
629
|
}
|
|
630
|
+
} else if ((text(values, "sshHostRef") ?? "").trim().length > 0) {
|
|
631
|
+
errors.push('"sshHostRef" requires "mode" to be "ssh"');
|
|
616
632
|
}
|
|
617
633
|
return { values, errors };
|
|
618
634
|
},
|
|
@@ -638,7 +654,7 @@ export function createAcpCliBackend(
|
|
|
638
654
|
// beats returning the local machine's answer for a remote target.
|
|
639
655
|
throw new Error(
|
|
640
656
|
`teammate backend "acp-cli" cannot list "${field}" for an "ssh" registration: `
|
|
641
|
-
+ "
|
|
657
|
+
+ "whether embedded or selected by sshHostRef, the target host's catalogue is not reachable by the local probe",
|
|
642
658
|
);
|
|
643
659
|
}
|
|
644
660
|
const route = routeOf(config);
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* models and executed over the Agent Client Protocol:
|
|
7
7
|
*
|
|
8
8
|
* - mode "local": the CLI is spawned on this machine (which/where reachability);
|
|
9
|
-
* - mode "ssh": the CLI runs on a remote host over a direct ssh2 exec,
|
|
10
|
-
* ssh
|
|
9
|
+
* - mode "ssh": the CLI runs on a remote host over a direct ssh2 exec, using
|
|
10
|
+
* either an `/ssh` manager reference or embedded connection fields.
|
|
11
11
|
*
|
|
12
12
|
* Discovery follows the same convention as teammate-models.json / teammate-remotes.json:
|
|
13
13
|
* the global file is ~/.pi/agent/teammate-cli-tools.json and the project file is
|
|
@@ -22,7 +22,11 @@ import * as os from "node:os";
|
|
|
22
22
|
import * as path from "node:path";
|
|
23
23
|
import type { AvailableModelEntry } from "../models/model-catalog.ts";
|
|
24
24
|
import type { RemoteHostConfig } from "../remote/types.ts";
|
|
25
|
-
import {
|
|
25
|
+
import { resolveSshHostRef } from "../public/v1/ssh-hosts.ts";
|
|
26
|
+
import {
|
|
27
|
+
probeSshCliExecutable,
|
|
28
|
+
remoteHostConfigFromSshProfile,
|
|
29
|
+
} from "../remote/ssh-exec.ts";
|
|
26
30
|
|
|
27
31
|
/** Launch configuration for a teammate-managed CLI tool. */
|
|
28
32
|
export interface CliToolConfig {
|
|
@@ -37,7 +41,8 @@ export interface CliToolConfig {
|
|
|
37
41
|
cwd?: string;
|
|
38
42
|
/** Trusted environment-variable names forwarded from the parent process. */
|
|
39
43
|
env?: readonly string[];
|
|
40
|
-
// SSH
|
|
44
|
+
// SSH source (mode "ssh" only): a manager reference OR embedded fields.
|
|
45
|
+
sshHostRef?: string;
|
|
41
46
|
host?: string;
|
|
42
47
|
user?: string;
|
|
43
48
|
port?: number;
|
|
@@ -235,24 +240,73 @@ export function cliToolArgv(name: string, config: CliToolConfig): [string, ...st
|
|
|
235
240
|
return [cliToolCommand(name, config), ...(config.args ?? [])];
|
|
236
241
|
}
|
|
237
242
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
const SSH_INLINE_FIELDS: readonly (keyof CliToolConfig)[] = [
|
|
244
|
+
"host",
|
|
245
|
+
"user",
|
|
246
|
+
"port",
|
|
247
|
+
"hostKeySha256",
|
|
248
|
+
"identityFile",
|
|
249
|
+
];
|
|
250
|
+
|
|
251
|
+
function hasInlineSshField(config: CliToolConfig): boolean {
|
|
252
|
+
return SSH_INLINE_FIELDS.some((key) => {
|
|
253
|
+
const value = config[key];
|
|
254
|
+
return typeof value === "number" || (typeof value === "string" && value.trim().length > 0);
|
|
255
|
+
});
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Safe configuration diagnostic for one ssh-mode tool, or undefined when complete. */
|
|
259
|
+
export function sshHostConfigIssue(config: CliToolConfig): string | undefined {
|
|
260
|
+
if ((config.mode ?? "local") !== "ssh") return undefined;
|
|
261
|
+
const rawRef = config.sshHostRef;
|
|
262
|
+
const hostRef = rawRef?.trim() ?? "";
|
|
263
|
+
if (rawRef !== undefined && hostRef.length === 0) {
|
|
264
|
+
return "sshHostRef must be a non-empty /ssh host id";
|
|
265
|
+
}
|
|
266
|
+
if (hostRef && hasInlineSshField(config)) {
|
|
267
|
+
return "sshHostRef cannot be combined with host, user, port, hostKeySha256 or identityFile";
|
|
268
|
+
}
|
|
269
|
+
if (hostRef) return undefined;
|
|
270
|
+
|
|
241
271
|
const host = config.host?.trim() ?? "";
|
|
242
272
|
const user = config.user?.trim() ?? "";
|
|
243
|
-
const port = config.port ?? 22;
|
|
244
273
|
const hostKeySha256 = config.hostKeySha256?.trim() ?? "";
|
|
245
|
-
if (!host || !user || !hostKeySha256)
|
|
246
|
-
|
|
274
|
+
if (!host || !user || !hostKeySha256) {
|
|
275
|
+
return "ssh mode requires sshHostRef or host, user and hostKeySha256";
|
|
276
|
+
}
|
|
277
|
+
const port = config.port ?? 22;
|
|
278
|
+
if (!Number.isInteger(port) || port < 1 || port > 65_535) {
|
|
279
|
+
return "ssh port must be an integer between 1 and 65535";
|
|
280
|
+
}
|
|
281
|
+
return undefined;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/** Embedded SSH connection fields lifted from a tool config; null for references or invalid input. */
|
|
285
|
+
export function sshHostConfigOf(config: CliToolConfig): RemoteHostConfig | null {
|
|
286
|
+
if (sshHostConfigIssue(config) !== undefined || (config.sshHostRef?.trim() ?? "")) return null;
|
|
287
|
+
if ((config.mode ?? "local") !== "ssh") return null;
|
|
247
288
|
return {
|
|
248
|
-
host,
|
|
249
|
-
user,
|
|
250
|
-
port,
|
|
251
|
-
hostKeySha256,
|
|
289
|
+
host: config.host!.trim(),
|
|
290
|
+
user: config.user!.trim(),
|
|
291
|
+
port: config.port ?? 22,
|
|
292
|
+
hostKeySha256: config.hostKeySha256!.trim(),
|
|
252
293
|
...(config.identityFile?.trim() ? { identityFile: config.identityFile.trim() } : {}),
|
|
253
294
|
};
|
|
254
295
|
}
|
|
255
296
|
|
|
297
|
+
/** Resolve either host source immediately before a new direct-SSH connection. */
|
|
298
|
+
export async function resolveSshHostConfigOf(config: CliToolConfig): Promise<RemoteHostConfig> {
|
|
299
|
+
const issue = sshHostConfigIssue(config);
|
|
300
|
+
if (issue !== undefined) throw new Error(issue);
|
|
301
|
+
const hostRef = config.sshHostRef?.trim();
|
|
302
|
+
if (hostRef) {
|
|
303
|
+
return remoteHostConfigFromSshProfile(await resolveSshHostRef(hostRef));
|
|
304
|
+
}
|
|
305
|
+
const inline = sshHostConfigOf(config);
|
|
306
|
+
if (!inline) throw new Error("ssh mode requires sshHostRef or complete embedded SSH fields");
|
|
307
|
+
return inline;
|
|
308
|
+
}
|
|
309
|
+
|
|
256
310
|
export interface CliToolProbeResult {
|
|
257
311
|
ok: boolean;
|
|
258
312
|
command: string;
|
|
@@ -278,6 +332,10 @@ const sshProbeInflight = new Map<string, Promise<void>>();
|
|
|
278
332
|
*/
|
|
279
333
|
function probeCacheKey(command: string, config: CliToolConfig): string {
|
|
280
334
|
if ((config.mode ?? "local") !== "ssh") return JSON.stringify(["local", command]);
|
|
335
|
+
const hostRef = config.sshHostRef?.trim();
|
|
336
|
+
if (hostRef && sshHostConfigIssue(config) === undefined) {
|
|
337
|
+
return JSON.stringify(["ssh-ref", hostRef, command]);
|
|
338
|
+
}
|
|
281
339
|
const hostConfig = sshHostConfigOf(config);
|
|
282
340
|
// An incomplete ssh config has no target to name; the verdict is the same
|
|
283
341
|
// refusal for every such registration, so they share one entry.
|
|
@@ -386,22 +444,20 @@ function probeLocalExecutable(key: string, command: string): CliToolProbeResult
|
|
|
386
444
|
}
|
|
387
445
|
|
|
388
446
|
function probeSshTool(key: string, command: string, config: CliToolConfig): CliToolProbeResult {
|
|
389
|
-
const
|
|
390
|
-
if (
|
|
391
|
-
const result: CliToolProbeResult = {
|
|
392
|
-
ok: false,
|
|
393
|
-
command,
|
|
394
|
-
error: "ssh mode requires host, user and hostKeySha256 in teammate-cli-tools.json",
|
|
395
|
-
};
|
|
447
|
+
const issue = sshHostConfigIssue(config);
|
|
448
|
+
if (issue !== undefined) {
|
|
449
|
+
const result: CliToolProbeResult = { ok: false, command, error: issue };
|
|
396
450
|
probeCache.set(key, { result, at: Date.now() });
|
|
397
451
|
return result;
|
|
398
452
|
}
|
|
399
|
-
// Optimistically list a complete
|
|
400
|
-
//
|
|
453
|
+
// Optimistically list a complete source while an actual provider-resolution
|
|
454
|
+
// and remote executable probe warms the cache. This is the same bounded
|
|
455
|
+
// discovery behavior as embedded SSH, not a local catalogue result.
|
|
401
456
|
const result: CliToolProbeResult = { ok: true, command };
|
|
402
457
|
probeCache.set(key, { result, at: Date.now() });
|
|
403
458
|
if (!sshProbeInflight.has(key)) {
|
|
404
|
-
const probe =
|
|
459
|
+
const probe = resolveSshHostConfigOf(config)
|
|
460
|
+
.then((hostConfig) => probeSshCliExecutable(hostConfig, command))
|
|
405
461
|
.then((outcome) => {
|
|
406
462
|
probeCache.set(key, {
|
|
407
463
|
result: outcome.ok
|
|
@@ -410,9 +466,13 @@ function probeSshTool(key: string, command: string, config: CliToolConfig): CliT
|
|
|
410
466
|
at: Date.now(),
|
|
411
467
|
});
|
|
412
468
|
})
|
|
413
|
-
.catch(() => {
|
|
469
|
+
.catch((error: unknown) => {
|
|
414
470
|
probeCache.set(key, {
|
|
415
|
-
result: {
|
|
471
|
+
result: {
|
|
472
|
+
ok: false,
|
|
473
|
+
command,
|
|
474
|
+
error: error instanceof Error ? error.message : `remote executable "${command}" probe failed`,
|
|
475
|
+
},
|
|
416
476
|
at: Date.now(),
|
|
417
477
|
});
|
|
418
478
|
})
|
|
@@ -34,7 +34,7 @@ import type {
|
|
|
34
34
|
import {
|
|
35
35
|
cliToolArgv,
|
|
36
36
|
probeCliToolCommand,
|
|
37
|
-
|
|
37
|
+
resolveSshHostConfigOf,
|
|
38
38
|
type CliToolConfig,
|
|
39
39
|
} from "./cli-tools-config.ts";
|
|
40
40
|
|
|
@@ -174,11 +174,15 @@ export async function runSshCliTool(
|
|
|
174
174
|
params: RunLocalCliToolParams,
|
|
175
175
|
): Promise<CliToolRunResult> {
|
|
176
176
|
const startedAt = Date.now();
|
|
177
|
-
|
|
178
|
-
|
|
177
|
+
let hostConfig: Awaited<ReturnType<typeof resolveSshHostConfigOf>>;
|
|
178
|
+
try {
|
|
179
|
+
// Resolve for every new run: locking, deleting, or editing an `/ssh` host
|
|
180
|
+
// therefore affects new connections without disturbing an active channel.
|
|
181
|
+
hostConfig = await resolveSshHostConfigOf(params.config);
|
|
182
|
+
} catch (error) {
|
|
179
183
|
return failedResult(
|
|
180
184
|
startedAt,
|
|
181
|
-
`CLI tool "${params.tool}"
|
|
185
|
+
`CLI tool "${params.tool}" SSH host could not be resolved: ${error instanceof Error ? error.message : "unknown SSH host error"}`,
|
|
182
186
|
);
|
|
183
187
|
}
|
|
184
188
|
|
|
@@ -151,15 +151,10 @@ export class CompletionDeliveryCoordinator {
|
|
|
151
151
|
#binding: CompletionSessionBinding | undefined;
|
|
152
152
|
#unsubscribe: (() => void) | undefined;
|
|
153
153
|
#disposed = false;
|
|
154
|
-
// Throttle
|
|
155
|
-
//
|
|
156
|
-
//
|
|
157
|
-
//
|
|
158
|
-
// a sweep between two reconciles is safe; the next reconcile past this gap
|
|
159
|
-
// reclaims them. This is the only place store.gc() is called — bindSession()
|
|
160
|
-
// relies on this reconcile path for the initial sweep on a fresh binding.
|
|
161
|
-
// Seeded lazily on the first reconcile so the first sweep always runs (a zero
|
|
162
|
-
// seed under a small mocked clock would otherwise skip the first sweep).
|
|
154
|
+
// Throttle GC attempts inside reconcile(). tryGc() also persists a cross-process
|
|
155
|
+
// page/expiry fence, but this local guard prevents one coordinator from
|
|
156
|
+
// immediately reacquiring the maintenance lock while a backlog remains.
|
|
157
|
+
// Seeded lazily so the first reconcile still attempts one non-blocking page.
|
|
163
158
|
#lastReconcileGcAt: number | undefined;
|
|
164
159
|
|
|
165
160
|
constructor(options: CompletionCoordinatorOptions = {}) {
|
|
@@ -331,12 +326,12 @@ export class CompletionDeliveryCoordinator {
|
|
|
331
326
|
// failed" warning. Canonical and legacy roots remain isolated and are
|
|
332
327
|
// swept independently; no alias root is merged or deleted.
|
|
333
328
|
if (this.#lastReconcileGcAt === undefined || now - this.#lastReconcileGcAt >= RECONCILE_GC_MIN_INTERVAL_MS) {
|
|
334
|
-
let
|
|
329
|
+
let attempted = false;
|
|
335
330
|
for (const target of targets) {
|
|
336
331
|
const result = await this.store.tryGc(target.workspaceId);
|
|
337
|
-
if (!result.busy && !result.skipped
|
|
332
|
+
if (!result.busy && !result.skipped) attempted = true;
|
|
338
333
|
}
|
|
339
|
-
if (
|
|
334
|
+
if (attempted) this.#lastReconcileGcAt = now;
|
|
340
335
|
}
|
|
341
336
|
const providers = new Set<CompletionDurabilityProvider>();
|
|
342
337
|
const currentProvider = this.registry.current();
|