@staix/agent-hub 0.12.33 → 0.12.35
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/CHANGELOG.md +22 -0
- package/README.md +3 -3
- package/docs/agent-notes/adapters.md +9 -3
- package/docs/agent-notes/budget.md +7 -3
- package/docs/agent-notes/bus.md +2 -1
- package/docs/agent-notes/daemon.md +7 -3
- package/docs/agent-notes/local-worker.md +1 -1
- package/docs/agent-notes/tasks.md +6 -2
- package/docs/agent-notes/tests.md +2 -1
- package/docs/gsd-collaboration-analysis.md +5 -1
- package/docs/operations.md +80 -16
- package/docs/quickstart.md +10 -2
- package/docs/sdlc.md +59 -34
- package/docs/security.md +7 -4
- package/docs/smoke.md +1 -1
- package/docs/specs/2026-09-19-agent-hub-design.md +407 -10
- package/docs/verification/2026-10-18-0.12.34.md +48 -0
- package/docs/verification/2026-10-18-0.12.35.md +49 -0
- package/docs/verification/2026-10-18-493-claude-busy-channel.md +91 -0
- package/docs/verified.json +35 -19
- package/package.json +1 -1
- package/plugins/agent-hub/.claude-plugin/plugin.json +1 -1
- package/plugins/agent-hub/server.js +23567 -16606
- package/src/adapters/acp.ts +3 -1
- package/src/adapters/claude-channel.ts +59 -29
- package/src/adapters/codex-appserver.ts +82 -12
- package/src/adapters/definitions/builtin-metadata.ts +101 -7
- package/src/adapters/definitions/native-cli-claude-launch.ts +90 -0
- package/src/adapters/definitions/native-cli-claude-setup.ts +80 -0
- package/src/adapters/definitions/native-cli-codex-launch.ts +14 -0
- package/src/adapters/definitions/native-cli-doctor.ts +46 -0
- package/src/adapters/definitions/native-cli-entry-claude.ts +52 -0
- package/src/adapters/definitions/native-cli-entry-codex.ts +34 -0
- package/src/adapters/definitions/native-cli-entry-jcode.ts +27 -0
- package/src/adapters/definitions/native-cli-entry-kimi.ts +57 -0
- package/src/adapters/definitions/native-cli-entry-local.ts +15 -0
- package/src/adapters/definitions/native-cli-entry-pi.ts +53 -0
- package/src/adapters/definitions/native-cli-entry-qwen.ts +21 -0
- package/src/adapters/definitions/native-cli-entry-types.ts +41 -0
- package/src/adapters/definitions/native-cli-entry.ts +19 -0
- package/src/adapters/definitions/native-cli-jcode-mcp.ts +58 -0
- package/src/adapters/definitions/native-cli-launch-common.ts +17 -0
- package/src/adapters/definitions/native-cli-pi.ts +33 -0
- package/src/adapters/definitions/native-cli-preview-claude.ts +37 -0
- package/src/adapters/definitions/native-cli-preview-codex.ts +20 -0
- package/src/adapters/definitions/native-cli-preview-common.ts +57 -0
- package/src/adapters/definitions/native-cli-preview-jcode.ts +20 -0
- package/src/adapters/definitions/native-cli-preview-kimi.ts +28 -0
- package/src/adapters/definitions/native-cli-preview-pi.ts +36 -0
- package/src/adapters/definitions/native-cli-preview-qwen.ts +22 -0
- package/src/adapters/definitions/native-cli-preview.ts +16 -0
- package/src/adapters/definitions/native-config-defaults.ts +19 -0
- package/src/adapters/definitions/native-config-policy.ts +35 -0
- package/src/adapters/definitions/native-context.ts +35 -0
- package/src/adapters/definitions/native-daemon-kimi.ts +5 -0
- package/src/adapters/definitions/native-daemon-policy.ts +18 -0
- package/src/adapters/definitions/native-display.ts +18 -0
- package/src/adapters/definitions/native-factories.ts +48 -0
- package/src/adapters/definitions/native-policy-aux.ts +130 -0
- package/src/adapters/definitions/native-quota.ts +29 -0
- package/src/adapters/definitions/native-recovery-continuity.ts +12 -0
- package/src/adapters/definitions/native-recovery.ts +88 -0
- package/src/adapters/definitions/qwen-native.ts +2 -2
- package/src/adapters/definitions/qwen.ts +8 -4
- package/src/adapters/local-worker.ts +3 -2
- package/src/adapters/pi.ts +3 -1
- package/src/adapters/registry-types.ts +100 -10
- package/src/adapters/registry.ts +127 -11
- package/src/cli/console-state.ts +5 -5
- package/src/cli/identity.ts +8 -7
- package/src/cli/jcode-mcp.ts +2 -58
- package/src/cli/launch.ts +41 -102
- package/src/cli/main.ts +126 -290
- package/src/cli/output.ts +3 -2
- package/src/cli/preview.ts +26 -126
- package/src/cli/recovery-guidance.ts +43 -22
- package/src/cli/setup.ts +2 -80
- package/src/cli/shell-quote.ts +3 -0
- package/src/cli/terminal-recovery.ts +81 -22
- package/src/cli/upgrade-interactive.ts +4 -2
- package/src/cli/upgrade-restore-state.ts +17 -7
- package/src/cli/upgrade-runtime.ts +126 -147
- package/src/cli/upgrade.ts +44 -27
- package/src/hub/budget.ts +59 -31
- package/src/hub/bus.ts +1 -1
- package/src/hub/conductor.ts +19 -9
- package/src/hub/config-apply.ts +22 -15
- package/src/hub/config-reload.ts +18 -3
- package/src/hub/config-trust.ts +1 -1
- package/src/hub/context-window.ts +3 -27
- package/src/hub/control-client.ts +1 -1
- package/src/hub/crash.ts +15 -39
- package/src/hub/daemon.ts +506 -341
- package/src/hub/execution-budget.ts +3 -2
- package/src/hub/hub-tools.ts +19 -8
- package/src/hub/instance-budget.ts +73 -0
- package/src/hub/instance-config.ts +47 -0
- package/src/hub/instance-execution.ts +50 -0
- package/src/hub/instances.ts +100 -0
- package/src/hub/lifecycle-schema.ts +7 -3
- package/src/hub/peer-catalog.ts +4 -13
- package/src/hub/permission-mode.ts +14 -17
- package/src/hub/recovery-continuity.ts +6 -5
- package/src/hub/report.ts +9 -8
- package/src/hub/roster.ts +9 -9
- package/src/hub/routing.ts +94 -19
- package/src/hub/sdlc-queue.ts +2 -2
- package/src/hub/sdlc.ts +84 -38
- package/src/hub/settings.ts +39 -10
- package/src/hub/settled-roles.ts +1 -1
- package/src/hub/snapshots.ts +40 -9
- package/src/hub/start-mode.ts +13 -8
- package/src/hub/status-roster.ts +8 -0
- package/src/hub/tasks.ts +60 -27
- package/src/hub/text-projection.ts +7 -3
- package/src/hub/tooling.ts +4 -60
- package/src/hub/ui.ts +3 -1
- package/src/hub/workflow.ts +30 -52
- package/src/memory/recall.ts +4 -5
- package/src/models/route/signals.ts +3 -1
- package/src/pi/launch.ts +5 -3
- package/src/ui/index.html +4 -3
- package/src/ui/office-model.ts +3 -1
- package/src/ui/peer-palette.ts +16 -6
- package/templates/AGENTS.block.md +2 -1
- package/templates/routing.toml +2 -2
package/src/hub/config-reload.ts
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
|
+
import { ADAPTERS } from "../adapters/registry.ts";
|
|
1
2
|
import { createHash } from "node:crypto";
|
|
2
3
|
import { readFileSync } from "node:fs";
|
|
3
4
|
import { join } from "node:path";
|
|
4
5
|
import { validateRawConfig, type ConfigLayer } from "./config-apply.ts";
|
|
5
6
|
import { parseOverlay, parseRouting, type Routing } from "./routing.ts";
|
|
6
7
|
import { CLASSES } from "./board.ts";
|
|
8
|
+
import { validateInstances } from "./instances.ts";
|
|
7
9
|
|
|
8
10
|
export const CONFIG_SOURCE_FILES = ["config.json", "config.local.json", "routing.toml", "routing.local.toml"] as const;
|
|
9
11
|
export type ConfigSourceName = typeof CONFIG_SOURCE_FILES[number];
|
|
@@ -66,15 +68,19 @@ function validateRoutingRaw(value: unknown): void {
|
|
|
66
68
|
const entry = table(item, `${key}.${name}`); text(entry[required], `${key}.${name}.${required}`); openValue(entry, `${key}.${name}`);
|
|
67
69
|
}
|
|
68
70
|
};
|
|
71
|
+
const nativeValidators = { bool, text, strings, argv: strings,
|
|
72
|
+
number: (min = 1, max = Number.MAX_SAFE_INTEGER, _integer = true) => (v: unknown, k: string) => numeric(v, k, min, max),
|
|
73
|
+
words: (values: readonly string[]) => (v: unknown, k: string) => choice(v, k, values),
|
|
74
|
+
block: (fields: Record<string, (v: unknown, k: string) => void>) => (v: unknown, k: string) => closed(v, k, fields),
|
|
75
|
+
};
|
|
69
76
|
closed(value, "routing", {
|
|
70
|
-
|
|
77
|
+
...Object.assign({}, ...ADAPTERS.map(adapter => adapter.configuration?.routingSchema?.(nativeValidators) ?? {})),
|
|
71
78
|
targets: (v, k) => switchyard(v, k, "id"), routes: (v, k) => switchyard(v, k, "type"),
|
|
72
79
|
// parseRouting delegates these tables to the existing parseHubRoutes validator.
|
|
73
80
|
hub_routes: (v, k) => { table(v, k); },
|
|
74
81
|
stay_switch: (v, k) => choice(v, k, ["off", "shadow", "enforce"]), max_switch_prefill_tokens: numeric,
|
|
75
82
|
signals: (v, k) => closed(v, k, { pii_patterns: strings, long_context_tokens: numeric, pii_screen: (x, p) => choice(x, p, ["off", "local"]) }),
|
|
76
83
|
constraints: (v, k) => closed(v, k, { pii: (x, p) => choice(x, p, ["local_only", "off"]), long_context: (x, p) => choice(x, p, ["skip_local", "off"]), budget_paused: (x, p) => choice(x, p, ["skip_peer", "off"]) }),
|
|
77
|
-
pi: (v, k) => closed(v, k, { dgx_max_context_tokens: numeric, mlx_max_context_tokens: numeric, efficient_wait_ms: (x, p) => numeric(x, p, 0, 119999) }),
|
|
78
84
|
classes: (v, k) => { for (const [name, item] of Object.entries(table(v, k))) {
|
|
79
85
|
if (!(CLASSES as readonly string[]).includes(name)) bad(`${k}.${name}`);
|
|
80
86
|
closed(item, `${k}.${name}`, { peers, escalate_to: peers, route: text, fixed_model: text, local_allowed: bool, pi_backend: (x, p) => choice(x, p, ["dgx", "mlx"]) });
|
|
@@ -85,6 +91,15 @@ function validateRoutingRaw(value: unknown): void {
|
|
|
85
91
|
/** Strict reloads never use routing's last-good fallback. Startup can retain its existing fallback explicitly. */
|
|
86
92
|
export function parseConfigSources(texts: ConfigSourceTexts, fallbackRouting?: Routing): ConfigSources {
|
|
87
93
|
const layers: ConfigLayer[] = [], trustLayers: ConfigSources["trustLayers"] = [];
|
|
94
|
+
let localRaw: Record<string, unknown> = {};
|
|
95
|
+
try { localRaw = JSON.parse(texts["config.local.json"] ?? "{}"); }
|
|
96
|
+
catch { if (!fallbackRouting) throw new Error(".agenthub/config.local.json: invalid JSON object"); }
|
|
97
|
+
let declared;
|
|
98
|
+
try { declared = validateInstances(localRaw?.instances); }
|
|
99
|
+
catch (error) {
|
|
100
|
+
if (!fallbackRouting) throw new Error(`.agenthub/config.local.json: ${(error as Error).message}`);
|
|
101
|
+
declared = validateInstances(undefined);
|
|
102
|
+
}
|
|
88
103
|
for (const name of CONFIG_SOURCE_FILES) {
|
|
89
104
|
const text = texts[name];
|
|
90
105
|
if (text === null) continue;
|
|
@@ -99,7 +114,7 @@ export function parseConfigSources(texts: ConfigSourceTexts, fallbackRouting?: R
|
|
|
99
114
|
}
|
|
100
115
|
if (name === "config.json" || name === "config.local.json") {
|
|
101
116
|
if (!fallbackRouting) {
|
|
102
|
-
try { validateRawConfig(raw); }
|
|
117
|
+
try { validateRawConfig(raw, declared); }
|
|
103
118
|
catch (error) { throw new Error(`.agenthub/${name}: ${(error as Error).message}`); }
|
|
104
119
|
}
|
|
105
120
|
layers.push({ name, raw });
|
package/src/hub/config-trust.ts
CHANGED
|
@@ -29,7 +29,7 @@ export const MACHINE_LOCAL = [
|
|
|
29
29
|
"terminal.open", // the command a hub-made TUI start runs to open a terminal (#269)
|
|
30
30
|
] as const;
|
|
31
31
|
/** Legacy exported field list stays compatible; new policies use the complete list below. */
|
|
32
|
-
export const ALL_MACHINE_LOCAL = [...MACHINE_LOCAL, "protect_instruction_files", "qwen_cmd", "jcode_cmd", "jcode_home", "jcode_provider_profile", "text_projection"] as const;
|
|
32
|
+
export const ALL_MACHINE_LOCAL = [...MACHINE_LOCAL, "protect_instruction_files", "qwen_cmd", "jcode_cmd", "jcode_home", "jcode_provider_profile", "text_projection", "instances"] as const;
|
|
33
33
|
|
|
34
34
|
/** A known repository, including an unreadable marker, must never be mistaken for a plain directory. */
|
|
35
35
|
export function configRepositoryPresent(cwd: string): boolean {
|
|
@@ -1,3 +1,6 @@
|
|
|
1
|
+
import { nonnegative, positive } from "../adapters/definitions/native-context.ts";
|
|
2
|
+
export { claudeContext, codexContext, acpContext } from "../adapters/definitions/native-context.ts";
|
|
3
|
+
|
|
1
4
|
/** Native active-context occupancy, separate from accumulated billable usage (#185). */
|
|
2
5
|
export interface ContextReading {
|
|
3
6
|
source: "claude_statusline" | "codex_token_usage" | "acp_usage_update";
|
|
@@ -17,33 +20,6 @@ export interface ContextView {
|
|
|
17
20
|
}
|
|
18
21
|
export interface ContextConfig { gate: number; stale_min: number }
|
|
19
22
|
export const DEFAULT_CONTEXT: ContextConfig = { gate: 0, stale_min: 30 };
|
|
20
|
-
const nonnegative = (n: unknown): n is number => typeof n === "number" && Number.isFinite(n) && n >= 0;
|
|
21
|
-
const positive = (n: unknown): n is number => nonnegative(n) && n > 0;
|
|
22
|
-
const record = (v: unknown): v is Record<string, unknown> => v !== null && typeof v === "object" && !Array.isArray(v);
|
|
23
|
-
const object = (v: unknown): Record<string, unknown> => record(v) ? v : {};
|
|
24
|
-
export function claudeContext(value: unknown, sessionId: string, measuredAt: number): ContextReading {
|
|
25
|
-
const c = object(value);
|
|
26
|
-
const current = object(c.current_usage);
|
|
27
|
-
const parts = [current.input_tokens, current.cache_creation_input_tokens, current.cache_read_input_tokens];
|
|
28
|
-
const window = positive(c.context_window_size) ? c.context_window_size : null;
|
|
29
|
-
const sum = c.current_usage != null && parts.every(nonnegative) ? parts.reduce<number>((sum, n) => sum + (n as number), 0) : null;
|
|
30
|
-
const tokens = nonnegative(sum) ? sum : null;
|
|
31
|
-
const used = record(c.current_usage) && tokens !== null && nonnegative(c.used_percentage) && c.used_percentage <= 100 && window !== null ? c.used_percentage / 100 : null;
|
|
32
|
-
return { source: "claude_statusline", sessionId, measuredAt, tokens, window, used };
|
|
33
|
-
}
|
|
34
|
-
export function codexContext(value: unknown, sessionId: string, measuredAt: number): ContextReading {
|
|
35
|
-
const c = object(value), last = object(c.last);
|
|
36
|
-
const window = positive(c.modelContextWindow) ? c.modelContextWindow : null;
|
|
37
|
-
const tokens = nonnegative(last.totalTokens) ? last.totalTokens : null;
|
|
38
|
-
return { source: "codex_token_usage", sessionId, measuredAt, tokens, window, used: tokens !== null && window !== null ? Math.min(1, tokens / window) : null };
|
|
39
|
-
}
|
|
40
|
-
/** An ACP `usage_update` of the `{ used, size }` shape (Kimi 2.x): context occupancy, not consumption (#167, #285). */
|
|
41
|
-
export function acpContext(usage: { used: number; size: number }, sessionId: string, measuredAt: number): ContextReading {
|
|
42
|
-
const window = positive(usage.size) ? usage.size : null;
|
|
43
|
-
const tokens = nonnegative(usage.used) ? usage.used : null;
|
|
44
|
-
// If a report ever shows used > size, the fraction clamps so a bar renders; tokens keeps the reported number.
|
|
45
|
-
return { source: "acp_usage_update", sessionId, measuredAt, tokens, window, used: tokens !== null && window !== null ? Math.min(1, tokens / window) : null };
|
|
46
|
-
}
|
|
47
23
|
export const unknownContext = (): ContextView => ({ source: null, measuredAt: null, tokens: null, window: null, used: null, freshness: "unknown" });
|
|
48
24
|
/** Invalid/stale readings hide the value but never rearm a threshold already crossed. */
|
|
49
25
|
export class ContextWindows {
|
|
@@ -8,7 +8,7 @@ export function stateDirFor(cwd: string): string {
|
|
|
8
8
|
|
|
9
9
|
/** Control WS wire version. 2 = `deliver` carries `envs` (digests); 3 = `tools` role and task messages; 4 = budget messages and `hub_checkpoint`; 5 = `ask`; 6 = console-only `ui` session bootstrap; 8 = controlled recovery; 9 = Pi bridge metadata; 10 = durable delivery receipts; 11 = queue hold diagnostics; 12 = generation-bound channel settlement and execution budgets; 13 = turn-free facts (`facts` with tool, session and transcript binding, `silenced`) and per-recipient send results (issue #108). 14 = native context-window status and session-bound context checkpoint request ids (issue #185). 15 = conductor tools and permission deadlines/closed events (issues #190, #194). 16 = a Claude session without channel evidence: hello `channel: false` and `inbox` (issue #205). The plugin is installed apart from the daemon, so they can drift. */
|
|
10
10
|
export const PROTOCOL = 16;
|
|
11
|
-
export const DAEMON_FEATURES = ["task_cancel", "recovery_deferred_hub"] as const;
|
|
11
|
+
export const DAEMON_FEATURES = ["task_cancel", "recovery_deferred_hub", "peer_instances"] as const;
|
|
12
12
|
/** Protocols a current coordinator may authenticate while upgrading a running source. */
|
|
13
13
|
export const RECOVERY_SOURCE_PROTOCOLS = [9, 10, 11, 12, 13, 14, 15, PROTOCOL] as const;
|
|
14
14
|
|
package/src/hub/crash.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { definitionOf } from "../adapters/registry.ts";
|
|
1
|
+
import { definitionOf, type KindMap } from "../adapters/registry.ts";
|
|
2
2
|
import { chmodSync, existsSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
|
|
3
3
|
import { join } from "node:path";
|
|
4
4
|
import type { JournalDelivery } from "./delivery-journal.ts";
|
|
@@ -56,50 +56,26 @@ const str = (v: unknown) => (typeof v === "string" && v ? v : undefined);
|
|
|
56
56
|
* What can come back after a crash. `resume` holds the start arguments for peers the hub launches itself (Kimi through
|
|
57
57
|
* ACP `session/load`, Pi through its session file, the local worker afresh); the others say what the user has to do.
|
|
58
58
|
*/
|
|
59
|
-
export function crashPlan(records: SessionRecord[]): { peer: string; resume?: Record<string, string>; how: string; fresh?: Record<string, string>; tui?: true }[] {
|
|
59
|
+
export function crashPlan(records: SessionRecord[], instances?: KindMap): { peer: string; resume?: Record<string, string>; how: string; fresh?: Record<string, string>; tui?: true }[] {
|
|
60
60
|
return records.map(({ peer, meta }) => {
|
|
61
61
|
const launch = (meta.launch && typeof meta.launch === "object" ? meta.launch : {}) as Record<string, unknown>;
|
|
62
62
|
const model = str(launch.model);
|
|
63
|
-
const adapter = definitionOf(peer);
|
|
64
|
-
|
|
63
|
+
const adapter = definitionOf(peer, instances);
|
|
64
|
+
const extra = adapter !== undefined && definitionOf(peer) === undefined;
|
|
65
|
+
const command = extra ? `ahub ${adapter.cli.command} --as ${peer}` : `ahub ${peer}`;
|
|
66
|
+
if (adapter?.session.resumeById) {
|
|
65
67
|
const sessionId = adapter.session.crashIdentity?.(meta).sessionId;
|
|
66
|
-
if (!sessionId) return { peer, how: `${peer}: saved ACP session identity unavailable; start it deliberately with
|
|
68
|
+
if (!sessionId) return { peer, how: `${peer}: saved ACP session identity unavailable; start it deliberately with ${command}` };
|
|
67
69
|
return { peer, resume: { sessionId, ...(model ? { model } : {}) }, how: `${peer}: the recorded conversation can be loaded again (ACP session/load)` };
|
|
68
70
|
}
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const sessionId = str(meta.sessionId);
|
|
78
|
-
if (!sessionId) return { peer, how: "kimi: no session id was recorded; start it again with ahub kimi" };
|
|
79
|
-
return { peer, resume: { sessionId, ...(model ? { model } : {}) }, how: `kimi: session ${sessionId} can be loaded again (ACP session/load)` };
|
|
80
|
-
}
|
|
81
|
-
case "pi": {
|
|
82
|
-
// `fresh`: what a new headless session keeps of the recorded one (pi.auto_start's fallback, #66).
|
|
83
|
-
const fresh: Record<string, string> = {};
|
|
84
|
-
for (const k of ["backend", "model"] as const) if (str(launch[k])) fresh[k] = str(launch[k])!;
|
|
85
|
-
const sessionFile = str(meta.sessionFile) ?? str(launch.sessionFile);
|
|
86
|
-
// #269: a terminal Pi stays marked as one without a session file too, so nothing headless is started in its place.
|
|
87
|
-
if (!sessionFile) return { peer, fresh, ...(str(launch.mode) === "tui" ? { tui: true } : {}), how: "pi: no session file was recorded; start it again with ahub pi" };
|
|
88
|
-
// A terminal Pi is run by the CLI that launched it, not by the hub: nothing here could start it again.
|
|
89
|
-
if (str(launch.mode) === "tui") return { peer, fresh, tui: true, how: `pi: it ran in a terminal; start it again with ahub pi --mode tui --session-file ${sessionFile}` };
|
|
90
|
-
const args: Record<string, string> = { sessionFile };
|
|
91
|
-
for (const k of ["mode", "backend", "model"] as const) if (str(launch[k])) args[k] = str(launch[k])!;
|
|
92
|
-
return { peer, fresh, resume: args, how: `pi: its session file can be resumed (${sessionFile})` };
|
|
93
|
-
}
|
|
94
|
-
case "local": {
|
|
95
|
-
// `model` is also recorded as the route's fallback, and a model given at start pins it: pass one or the other.
|
|
96
|
-
const route = str(launch.route);
|
|
97
|
-
const args: Record<string, string> = route ? { route } : model ? { model } : {};
|
|
98
|
-
return { peer, resume: args, how: "local: starts again without its history (the worker keeps none across a hub stop)" };
|
|
99
|
-
}
|
|
100
|
-
default:
|
|
101
|
-
return { peer, how: `${peer}: reconnects by itself if its client is still running` };
|
|
102
|
-
}
|
|
71
|
+
const native = adapter?.session.crashPlan?.(meta);
|
|
72
|
+
if (!native) return { peer, how: `${peer}: reconnects by itself if its client is still running` };
|
|
73
|
+
if (!extra) return { peer, ...native };
|
|
74
|
+
const prefix = `${adapter.kind}:`;
|
|
75
|
+
let how = native.how.startsWith(prefix) ? `${peer}:${native.how.slice(prefix.length)}` : `${peer}: ${native.how}`;
|
|
76
|
+
// Manual launch guidance selects this instance. Recorded headless resume prose can contain an opaque id, never a command.
|
|
77
|
+
if (!native.resume || native.tui) how = how.replace(`ahub ${adapter.cli.command}`, command);
|
|
78
|
+
return { peer, ...native, how };
|
|
103
79
|
});
|
|
104
80
|
}
|
|
105
81
|
|