dsh-ntr 0.2.1 → 0.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.en.md +4 -4
- package/README.md +4 -4
- package/cordis.patch.yml +12 -0
- package/lib/bridge.js +640 -62
- package/lib/client/panel.js +130 -2
- package/lib/client/store.js +133 -1
- package/lib/client.js +302 -6
- package/lib/client.js.map +1 -1
- package/lib/impl.js +47 -0
- package/lib/index.js +15 -1
- package/lib/types/bridge.d.ts +205 -3
- package/lib/types/client/store.d.ts +54 -1
- package/lib/types/impl.d.ts +26 -1
- package/lib/types/protocol.d.ts +73 -0
- package/lib/types/update.d.ts +183 -0
- package/lib/update.js +681 -0
- package/package.json +1 -1
- package/src/bridge.ts +684 -61
- package/src/client/panel.tsx +215 -3
- package/src/client/store.ts +146 -2
- package/src/impl.ts +66 -1
- package/src/index.ts +16 -1
- package/src/protocol.ts +76 -0
- package/src/update.ts +699 -0
package/lib/impl.js
CHANGED
|
@@ -514,6 +514,22 @@ export function validateConfig(raw) {
|
|
|
514
514
|
if (typeof staleCancelMaxRetries !== 'number' || !Number.isInteger(staleCancelMaxRetries) || staleCancelMaxRetries < 0 || staleCancelMaxRetries > 5) {
|
|
515
515
|
throw new AcpError('bad_config', 'acp: staleCancelMaxRetries must be an integer in [0, 5]');
|
|
516
516
|
}
|
|
517
|
+
// Stall-recovery windows (inactivity watchdog). Two lengths on purpose: a
|
|
518
|
+
// silent turn with the floor is a stuck agent, but a turn waiting on a TOOL
|
|
519
|
+
// call is a working agent, and the tool half may take as long as the guard's
|
|
520
|
+
// own tool-output wait. `0` means "off" / "fall back", never "expire now".
|
|
521
|
+
const turnStallMs = value.turnStallMs ?? 600_000;
|
|
522
|
+
if (typeof turnStallMs !== 'number' || !Number.isInteger(turnStallMs) || turnStallMs < 0 || turnStallMs > 86_400_000) {
|
|
523
|
+
throw new AcpError('bad_config', 'acp: turnStallMs must be an integer in [0, 86400000] (0 = disable the watchdog)');
|
|
524
|
+
}
|
|
525
|
+
const turnStallToolMs = value.turnStallToolMs ?? 1_800_000;
|
|
526
|
+
if (typeof turnStallToolMs !== 'number' || !Number.isInteger(turnStallToolMs) || turnStallToolMs < 0 || turnStallToolMs > 86_400_000) {
|
|
527
|
+
throw new AcpError('bad_config', 'acp: turnStallToolMs must be an integer in [0, 86400000] (0 = fall back to turnStallMs)');
|
|
528
|
+
}
|
|
529
|
+
const turnStallTickMs = value.turnStallTickMs ?? 30_000;
|
|
530
|
+
if (typeof turnStallTickMs !== 'number' || !Number.isInteger(turnStallTickMs) || turnStallTickMs < 1 || turnStallTickMs > 60_000) {
|
|
531
|
+
throw new AcpError('bad_config', 'acp: turnStallTickMs must be an integer in [1, 60000]');
|
|
532
|
+
}
|
|
517
533
|
return {
|
|
518
534
|
agents,
|
|
519
535
|
transcriptMaxEntries: max,
|
|
@@ -524,6 +540,9 @@ export function validateConfig(raw) {
|
|
|
524
540
|
cancelFallbackMs,
|
|
525
541
|
staleCancelRetryDelayMs,
|
|
526
542
|
staleCancelMaxRetries,
|
|
543
|
+
turnStallMs,
|
|
544
|
+
turnStallToolMs,
|
|
545
|
+
turnStallTickMs,
|
|
527
546
|
};
|
|
528
547
|
}
|
|
529
548
|
// ---- Workspace guard ----
|
|
@@ -1499,6 +1518,19 @@ export class PermissionLedger {
|
|
|
1499
1518
|
sessionIds() {
|
|
1500
1519
|
return [...this.pending.values()].map(e => ({ requestId: e.requestId, sessionId: e.sessionId }));
|
|
1501
1520
|
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Whether ANY outstanding ask belongs to `sessionId`, a HOST session id
|
|
1523
|
+
* (`LiveSession.sessionId`) — NOT the ACP protocol session id. Used by the
|
|
1524
|
+
* stall watchdog: a turn blocked on a human is not a silent turn, so the
|
|
1525
|
+
* session with a pending permission must not be finalized as stalled.
|
|
1526
|
+
*/
|
|
1527
|
+
hasPendingFor(sessionId) {
|
|
1528
|
+
for (const entry of this.pending.values()) {
|
|
1529
|
+
if (entry.sessionId === sessionId)
|
|
1530
|
+
return true;
|
|
1531
|
+
}
|
|
1532
|
+
return false;
|
|
1533
|
+
}
|
|
1502
1534
|
/**
|
|
1503
1535
|
* Outstanding asks WITH the view the browser was shown, for the reconnect
|
|
1504
1536
|
* replay. Entries created without a view are skipped: replaying a frame the
|
|
@@ -1734,6 +1766,21 @@ export async function dispatchRpc(manager, endpoint, payload) {
|
|
|
1734
1766
|
}
|
|
1735
1767
|
case 'preset/status':
|
|
1736
1768
|
return ok({ preset: await manager.presetStatus?.() ?? null });
|
|
1769
|
+
case 'plugin/version':
|
|
1770
|
+
// Absent on a host half from before the badge existed: the browser
|
|
1771
|
+
// hides it instead of naming a version it cannot know.
|
|
1772
|
+
return ok({ version: manager.pluginVersion?.() });
|
|
1773
|
+
case 'plugin/update-check':
|
|
1774
|
+
// An older host half answers `unsupported` rather than failing: the
|
|
1775
|
+
// settings card then hides the update section.
|
|
1776
|
+
return ok(await (manager.pluginUpdateCheck?.() ?? { status: 'unsupported', current: '' }));
|
|
1777
|
+
case 'plugin/update-apply': {
|
|
1778
|
+
const version = str(body, 'version', 64);
|
|
1779
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) {
|
|
1780
|
+
throw new AcpError('bad_payload', 'acp: version must be a stable semver (x.y.z)');
|
|
1781
|
+
}
|
|
1782
|
+
return ok(await (manager.pluginUpdateApply?.(version) ?? { status: 'unsupported', current: '' }));
|
|
1783
|
+
}
|
|
1737
1784
|
case 'agent/stop':
|
|
1738
1785
|
await manager.stopAgent(str(body, 'agentId', 64));
|
|
1739
1786
|
return ok({ done: true });
|
package/lib/index.js
CHANGED
|
@@ -4,8 +4,9 @@
|
|
|
4
4
|
* them down with the plugin fiber. Named exports only (a default export would
|
|
5
5
|
* hide the loader metadata — see harness postmortem 0001).
|
|
6
6
|
*/
|
|
7
|
-
import { ACP_PRESET_ID, AcpLlmAdapter, BridgeManager } from "./bridge.js";
|
|
7
|
+
import { ACP_PRESET_ID, AcpLlmAdapter, BridgeManager, PLUGIN_VERSION } from "./bridge.js";
|
|
8
8
|
import { EventHub, dispatchRpc, permissionReplayFrames, sessionAgentPreset, sessionAgentPresetFromLog, validateConfig } from "./impl.js";
|
|
9
|
+
import { UpdateService } from "./update.js";
|
|
9
10
|
export const name = 'dsh-ntr';
|
|
10
11
|
// Deliberately minimal: every other host service is probed structurally
|
|
11
12
|
// (`ctx.get` + a late `ctx.inject`) so a deployment without one still loads
|
|
@@ -28,8 +29,21 @@ export function apply(ctx, config) {
|
|
|
28
29
|
cancelFallbackMs: resolved.cancelFallbackMs,
|
|
29
30
|
staleCancelRetryDelayMs: resolved.staleCancelRetryDelayMs,
|
|
30
31
|
staleCancelMaxRetries: resolved.staleCancelMaxRetries,
|
|
32
|
+
turnStallMs: resolved.turnStallMs,
|
|
33
|
+
turnStallToolMs: resolved.turnStallToolMs,
|
|
34
|
+
turnStallTickMs: resolved.turnStallTickMs,
|
|
31
35
|
});
|
|
32
36
|
const adapter = new AcpLlmAdapter(manager);
|
|
37
|
+
// Built-in update: the plugin never downloads or swaps files itself. The
|
|
38
|
+
// harness plugin manager owns the install; this only relays its facts.
|
|
39
|
+
// `resolveManager` re-probes on every call so a late-loading service is
|
|
40
|
+
// picked up without an `inject` dependency (a host without it must still
|
|
41
|
+
// load this plugin, and then reports `unsupported`).
|
|
42
|
+
manager.setUpdateService(new UpdateService({
|
|
43
|
+
resolveManager: () => (typeof ctx.get === 'function' ? ctx.get('pluginManager') : undefined),
|
|
44
|
+
currentVersion: PLUGIN_VERSION,
|
|
45
|
+
isBusy: () => manager.hasActiveTurns(),
|
|
46
|
+
}));
|
|
33
47
|
// The RAW patch-file deadlines, before validation filled the defaults in.
|
|
34
48
|
// `validateConfig` returns a number for every knob, so the validated object
|
|
35
49
|
// cannot tell "the deployment asked for 10 minutes" from "the deployment said
|
package/lib/types/bridge.d.ts
CHANGED
|
@@ -9,7 +9,15 @@
|
|
|
9
9
|
import { type InitializeResponse } from '@agentclientprotocol/sdk';
|
|
10
10
|
import { EventHub, PermissionLedger, Transcript } from './impl.ts';
|
|
11
11
|
import type { AcpToolCallSnapshot, PendingPermissionView, RosterPresetRow } from './impl.ts';
|
|
12
|
-
import type { AcpAgentCatalog, AcpAgentDef, AcpApprovalDeadlines, AcpConversationChoice, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult, AgentView, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, GenerateOptions, LlmAdapterLike, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, ApprovalDeadlineKey, ApprovalDeadlinePatch, PermissionTimeoutMode, PreparedAdapterCall, SessionView, StreamChunk } from './protocol.ts';
|
|
12
|
+
import type { AcpAgentCatalog, AcpAgentDef, AcpApprovalDeadlines, AcpConversationChoice, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult, AgentView, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, GenerateOptions, LlmAdapterLike, LlmModelInfo, LlmProviderInfo, LlmResolvedModelInfo, ApprovalDeadlineKey, ApprovalDeadlinePatch, PermissionTimeoutMode, PluginUpdateState, PreparedAdapterCall, SessionView, StreamChunk } from './protocol.ts';
|
|
13
|
+
import { UpdateService } from './update.ts';
|
|
14
|
+
/**
|
|
15
|
+
* Version of this installed package, read from the shipped `package.json` —
|
|
16
|
+
* one level above both `src/` at test time and `lib/` at runtime. The settings
|
|
17
|
+
* badge and the ACP handshake's `clientInfo` both report it; a copied literal
|
|
18
|
+
* would go stale on every release.
|
|
19
|
+
*/
|
|
20
|
+
export declare const PLUGIN_VERSION: string;
|
|
13
21
|
interface LiveSession {
|
|
14
22
|
/** Host-minted id (stable across an agent restart; the ACP id is per-connection). */
|
|
15
23
|
sessionId: string;
|
|
@@ -37,6 +45,20 @@ interface LiveSession {
|
|
|
37
45
|
closed: boolean;
|
|
38
46
|
/** The agent process died; the session awaits a `session/load` resume. */
|
|
39
47
|
released: boolean;
|
|
48
|
+
/**
|
|
49
|
+
* When the agent last showed ANY sign of life on this session: an update, a
|
|
50
|
+
* permission ask (arrival or answer), or a turn start. The stall watchdog
|
|
51
|
+
* measures silence from here rather than from the prompt, which may
|
|
52
|
+
* legitimately be followed by a long tool run.
|
|
53
|
+
*/
|
|
54
|
+
lastAgentActivityAt: number;
|
|
55
|
+
/**
|
|
56
|
+
* Tool calls the agent announced and has not finished, by ACP `toolCallId`.
|
|
57
|
+
* A SET, not a flag: parallel calls finish independently, and one sibling's
|
|
58
|
+
* `completed` must not clear the long tool window while another is still
|
|
59
|
+
* running. Non-empty = the agent is working on its own clock.
|
|
60
|
+
*/
|
|
61
|
+
activeToolCalls: Set<string>;
|
|
40
62
|
/**
|
|
41
63
|
* Owning DSH chat session id, when the session was opened through the LLM
|
|
42
64
|
* adapter (`AcpLlmAdapter`). It is what the conversation's permission card
|
|
@@ -90,6 +112,36 @@ interface LiveSession {
|
|
|
90
112
|
* that answer means the message was silently dropped.
|
|
91
113
|
*/
|
|
92
114
|
cancelRequestedAtTurn?: number;
|
|
115
|
+
/**
|
|
116
|
+
* The {@link turnSeq} of a turn the stall watchdog force-finalized while the
|
|
117
|
+
* agent still held its `session/prompt`. The agent is presumed wedged, so the
|
|
118
|
+
* NEXT prompt reclaims the process (`BridgeManager.prepareStalledSession`)
|
|
119
|
+
* instead of queueing behind a turn that may never answer. Cleared when the
|
|
120
|
+
* late response lands or when the reclaim replaces the process.
|
|
121
|
+
*/
|
|
122
|
+
orphanPromptTurnSeq?: number;
|
|
123
|
+
/**
|
|
124
|
+
* The {@link turnSeq} of a turn a reclaim (`prepareStalledSession`) ended by
|
|
125
|
+
* closing the connection while the agent still held its `session/prompt`.
|
|
126
|
+
* The close rejects that request, and the caller's catch would broadcast a
|
|
127
|
+
* second `turn/end` for a turn already finalized: this marker tells the
|
|
128
|
+
* late `then`/`catch` to drop the answer instead. Set by the reclaim, cleared
|
|
129
|
+
* when that answer lands, at every new turn start, and by a resume.
|
|
130
|
+
*/
|
|
131
|
+
reclaimedTurnSeq?: number;
|
|
132
|
+
/**
|
|
133
|
+
* The controller of the turn currently streaming through this session.
|
|
134
|
+
*
|
|
135
|
+
* Chat turns are ALSO indexed by chat id ({@link BridgeManager}'s
|
|
136
|
+
* `turnStreams`) because their continuation steps arrive as fresh
|
|
137
|
+
* `streamPrompt` calls that look the turn up by chat id. An EPHEMERAL turn
|
|
138
|
+
* (`options.sessionId === undefined`) has no chat id to index under, so this
|
|
139
|
+
* attachment is the only way the stall watchdog can find its controller —
|
|
140
|
+
* without it a silent ephemeral turn falls into the `/acp` branch, which
|
|
141
|
+
* leaves the driver's segment queue open instead of finishing it. Cleared by
|
|
142
|
+
* the controller's own `retire` when it still points at it.
|
|
143
|
+
*/
|
|
144
|
+
turnController?: TurnStreamController;
|
|
93
145
|
transcript: Transcript;
|
|
94
146
|
}
|
|
95
147
|
/** Reliability tuning, threaded from `AcpPluginConfig` through the manager. */
|
|
@@ -155,6 +207,23 @@ export interface BridgeTuning {
|
|
|
155
207
|
* racing a concurrent `session/prompt` into the agent.
|
|
156
208
|
*/
|
|
157
209
|
turnSettleMs?: number;
|
|
210
|
+
/**
|
|
211
|
+
* How long a turn may stay SILENT — no update, no ask, no turn start — before
|
|
212
|
+
* the watchdog force-finalizes it as stalled. The turn is only watched while
|
|
213
|
+
* the agent is the one holding the floor (a segment on air, no pending ask),
|
|
214
|
+
* and the process survives: the next message reclaims it.
|
|
215
|
+
*
|
|
216
|
+
* `0` disables the watchdog.
|
|
217
|
+
*/
|
|
218
|
+
turnStallMs?: number;
|
|
219
|
+
/**
|
|
220
|
+
* The longer silence window used while an ACP tool call is in progress: the
|
|
221
|
+
* agent runs the tool on its own clock, so the plain turn window would call
|
|
222
|
+
* a healthy long-running tool stalled. `0` falls back to `turnStallMs`.
|
|
223
|
+
*/
|
|
224
|
+
turnStallToolMs?: number;
|
|
225
|
+
/** How often the watchdog scans for silent turns. */
|
|
226
|
+
turnStallTickMs?: number;
|
|
158
227
|
/**
|
|
159
228
|
* Default idle-release window for agents without their own
|
|
160
229
|
* `AcpAgentDef.idleTimeoutMs`: a READY process that holds no live session
|
|
@@ -248,8 +317,19 @@ export declare class AgentBridge {
|
|
|
248
317
|
private lastActivityAt;
|
|
249
318
|
/** Throwaway catalog probes in flight — they hold no live session of their own. */
|
|
250
319
|
private pendingProbes;
|
|
251
|
-
/**
|
|
252
|
-
|
|
320
|
+
/**
|
|
321
|
+
* Host→agent session commands in flight (mode / config); not turns. Counted
|
|
322
|
+
* PER SESSION: a command the wedged agent never answers must not pin the
|
|
323
|
+
* bridge against reclaiming THAT session (it is exactly the state the
|
|
324
|
+
* watchdog is recovering from), while another session's command still does.
|
|
325
|
+
*/
|
|
326
|
+
private readonly pendingCommands;
|
|
327
|
+
/**
|
|
328
|
+
* `session/new` requests in flight. The session does not exist in `sessions`
|
|
329
|
+
* yet, so nothing else would tell a teardown (`releaseAll` / idle release)
|
|
330
|
+
* that a caller is waiting on the child it is about to kill.
|
|
331
|
+
*/
|
|
332
|
+
private pendingCreates;
|
|
253
333
|
/**
|
|
254
334
|
* Whether a REAL session has ever been registered on this bridge. Probes
|
|
255
335
|
* bypass session bookkeeping, so a bridge that has never hosted anything was
|
|
@@ -383,6 +463,12 @@ export declare class AgentBridge {
|
|
|
383
463
|
* @returns the settler (idempotent; also used by connection loss).
|
|
384
464
|
*/
|
|
385
465
|
beginTurn(session: LiveSession): () => void;
|
|
466
|
+
/**
|
|
467
|
+
* Open a session's settle gate without owning its turn stream: the turn's
|
|
468
|
+
* stream is gone (a resume, a release) but the next prompt still waits on
|
|
469
|
+
* this promise, and nothing is left that could ever resolve it.
|
|
470
|
+
*/
|
|
471
|
+
private openSettleGate;
|
|
386
472
|
view(): AgentView;
|
|
387
473
|
/**
|
|
388
474
|
* Work a teardown would interrupt: a turn in flight (or one the agent is
|
|
@@ -390,6 +476,18 @@ export declare class AgentBridge {
|
|
|
390
476
|
* probe, a resume, or a start. A registered-but-idle session is NOT work.
|
|
391
477
|
*/
|
|
392
478
|
private hasActiveWork;
|
|
479
|
+
/**
|
|
480
|
+
* Active work on this bridge EXCEPT `session`'s own — what a restart-carrying
|
|
481
|
+
* reclaim (the stall watchdog's follow-up) would destroy. Killing this child
|
|
482
|
+
* restarts every ACP session on the connection, so a live conversation that
|
|
483
|
+
* is mid-turn, mid-resume, or waiting on a human pins the process: its
|
|
484
|
+
* context is the one thing a restart cannot give back.
|
|
485
|
+
*
|
|
486
|
+
* `session`'s OWN mode/config command is deliberately not counted: an agent
|
|
487
|
+
* that ignores `session/set_mode` would otherwise make its own stall
|
|
488
|
+
* unrecoverable — the command's counter is exactly the evidence of the wedge.
|
|
489
|
+
*/
|
|
490
|
+
hasOtherActiveWork(session: LiveSession): boolean;
|
|
393
491
|
/**
|
|
394
492
|
* Nothing depends on this child's identity right now: no live session and no
|
|
395
493
|
* work. This is the WORKSPACE MOVE's predicate. It stays stricter than the
|
|
@@ -398,6 +496,13 @@ export declare class AgentBridge {
|
|
|
398
496
|
* state for a restart on every cross-workspace turn.
|
|
399
497
|
*/
|
|
400
498
|
private isQuiet;
|
|
499
|
+
/**
|
|
500
|
+
* A turn is in flight (or the agent has not settled its response yet) on any
|
|
501
|
+
* of this bridge's sessions. `settled` only stays non-undefined while the
|
|
502
|
+
* agent's answer is outstanding — the settler clears it on completion — so
|
|
503
|
+
* this is real work, not an idle session.
|
|
504
|
+
*/
|
|
505
|
+
hasActiveTurn(): boolean;
|
|
401
506
|
/**
|
|
402
507
|
* An idle teardown is safe when nothing is WORKING. Idle live sessions do not
|
|
403
508
|
* block it: `releaseAll` parks them as `released` (never closed), and the next
|
|
@@ -437,6 +542,8 @@ export declare class AgentBridge {
|
|
|
437
542
|
* @param spawnCwd - where the agent PROCESS should run; see `ensureReady`.
|
|
438
543
|
*/
|
|
439
544
|
createSession(cwd: string | undefined, extraRoots?: readonly string[], spawnCwd?: string): Promise<LiveSession>;
|
|
545
|
+
/** The body of {@link createSession}; the caller keeps `pendingCreates` for it. */
|
|
546
|
+
private openSession;
|
|
440
547
|
/**
|
|
441
548
|
* Spawn a throwaway ACP session purely to read its `configOptions` (the
|
|
442
549
|
* model catalog probe). Deliberately bypasses the live-session bookkeeping
|
|
@@ -482,6 +589,17 @@ export declare class AgentBridge {
|
|
|
482
589
|
* successor — so a moved-on {@link LiveSession.turnSeq} disarms it.
|
|
483
590
|
*/
|
|
484
591
|
private armCancelFallback;
|
|
592
|
+
/**
|
|
593
|
+
* Force the local end of one turn whose agent never acknowledged it: clear
|
|
594
|
+
* `running`, open the settle gate, and broadcast the turn end with `turnId:
|
|
595
|
+
* ''` (there is no agent response to attribute it to). Shared by the cancel
|
|
596
|
+
* fallback and the stall watchdog.
|
|
597
|
+
*
|
|
598
|
+
* Generation-guarded: a turn that already ended — or that a later turn
|
|
599
|
+
* superseded — is not this call's to finalize. Returns whether it acted; a
|
|
600
|
+
* `false` means the caller was late and must leave the session alone.
|
|
601
|
+
*/
|
|
602
|
+
finalizeTurn(session: LiveSession, turnGen: number, message: string): boolean;
|
|
485
603
|
/**
|
|
486
604
|
* Bring a released session back on a fresh agent process via
|
|
487
605
|
* `session/load` ("send to recover"). Concurrent callers share one inflight
|
|
@@ -512,6 +630,18 @@ export declare class AgentBridge {
|
|
|
512
630
|
hasSession(sessionId: string): LiveSession | undefined;
|
|
513
631
|
/** Kill the child and drop every local session bound to this connection. */
|
|
514
632
|
stop(reason?: 'closed' | 'agent-stopped' | 'agent-died'): Promise<void>;
|
|
633
|
+
/**
|
|
634
|
+
* Drop every live session record and kill the child WITHOUT asking the agent
|
|
635
|
+
* to close them. `stop` sends `session/close` per session, and a wedged agent
|
|
636
|
+
* that ignores it would hang the caller (the stall reclaim) — the very agent
|
|
637
|
+
* this teardown exists to get rid of. The local teardown is what frees the
|
|
638
|
+
* sessions, so it runs unconditionally and immediately.
|
|
639
|
+
*
|
|
640
|
+
* Deliberately does NOT clear `released`: those records are constraint 10's
|
|
641
|
+
* — a released session survives a process death and is resumed by its next
|
|
642
|
+
* message. This is a stop, not a discard.
|
|
643
|
+
*/
|
|
644
|
+
forceReset(reason: 'agent-stopped'): void;
|
|
515
645
|
private killChild;
|
|
516
646
|
/**
|
|
517
647
|
* Answer one ACP permission ask.
|
|
@@ -838,6 +968,7 @@ type TurnSettle = {
|
|
|
838
968
|
} | {
|
|
839
969
|
kind: 'error';
|
|
840
970
|
message: string;
|
|
971
|
+
code?: string;
|
|
841
972
|
};
|
|
842
973
|
/**
|
|
843
974
|
* The state of ONE ACP prompt, shared across every driver step of the turn.
|
|
@@ -875,6 +1006,8 @@ declare class TurnStreamController {
|
|
|
875
1006
|
get replayCursor(): number;
|
|
876
1007
|
/** Whether the agent already settled its `session/prompt` for this turn. */
|
|
877
1008
|
get hasSettled(): boolean;
|
|
1009
|
+
/** Whether a segment is on air — the agent is expected to speak into a live driver step. */
|
|
1010
|
+
get hasOpenSegment(): boolean;
|
|
878
1011
|
/** Generation guard: a controller whose turn has been superseded must not touch the session. */
|
|
879
1012
|
get stale(): boolean;
|
|
880
1013
|
/**
|
|
@@ -894,6 +1027,13 @@ declare class TurnStreamController {
|
|
|
894
1027
|
abortActiveSegment(): void;
|
|
895
1028
|
/** Stop observing and release the manager's index. Idempotent. */
|
|
896
1029
|
dispose(): void;
|
|
1030
|
+
/**
|
|
1031
|
+
* Stop observing and release the manager's index WITHOUT touching the
|
|
1032
|
+
* process-wide tool-output waiters. A late settle of a superseded turn must
|
|
1033
|
+
* use this: those waiters now belong to the turn on air, and waking them
|
|
1034
|
+
* early would hand the guard a "no output" answer for a tool still running.
|
|
1035
|
+
*/
|
|
1036
|
+
retire(): void;
|
|
897
1037
|
private finishSegment;
|
|
898
1038
|
/** A segment opened on an already-settled turn: replay, then close it out. */
|
|
899
1039
|
private finishAfterSettle;
|
|
@@ -1016,6 +1156,28 @@ export declare class BridgeManager {
|
|
|
1016
1156
|
forgetTurnStream(controller: TurnStreamController): void;
|
|
1017
1157
|
/** Tear down the turn streams of every session of one bridge (close/release/stop). */
|
|
1018
1158
|
private disposeTurnStreamsOf;
|
|
1159
|
+
/**
|
|
1160
|
+
* The stall watchdog's scan loop. Armed lazily at the first turn start and
|
|
1161
|
+
* DROPPED once nothing anywhere is running: a manager that never runs a turn
|
|
1162
|
+
* never holds a timer, and one that goes quiet does not keep the host awake.
|
|
1163
|
+
*/
|
|
1164
|
+
private stallTimer?;
|
|
1165
|
+
private armStallTimer;
|
|
1166
|
+
/**
|
|
1167
|
+
* Force-finalize turns the agent has gone silent in.
|
|
1168
|
+
*
|
|
1169
|
+
* "Silent" is scoped to the stretch where the AGENT owes the turn a word: a
|
|
1170
|
+
* running session with a segment on air and no pending ask. A turn waiting on
|
|
1171
|
+
* the host (tool cards executing between segments) or on a human (an
|
|
1172
|
+
* unanswered permission) is not stuck and is skipped.
|
|
1173
|
+
*
|
|
1174
|
+
* Firing does NOT kill the process (see `LiveSession.orphanPromptTurnSeq`):
|
|
1175
|
+
* the session, its transcript and its binding survive, and the next message
|
|
1176
|
+
* reclaims the process. The turn's streaming face ends through its own
|
|
1177
|
+
* controller when there is one, so the driver step on air closes with an
|
|
1178
|
+
* error instead of being torn down mid-stream.
|
|
1179
|
+
*/
|
|
1180
|
+
private reapStalledTurns;
|
|
1019
1181
|
constructor(defs: readonly AcpAgentDef[], hub: EventHub, transcriptMax: number, configStoragePath?: string | undefined, catalogStoragePath?: string | undefined,
|
|
1020
1182
|
/** Auto-probe agents for model catalogs (boot + lazy); manual probes always run. */
|
|
1021
1183
|
probeModelsOnBoot?: boolean, tuning?: BridgeTuning);
|
|
@@ -1252,6 +1414,23 @@ export declare class BridgeManager {
|
|
|
1252
1414
|
* the block rather than claiming the preset is gone.
|
|
1253
1415
|
*/
|
|
1254
1416
|
presetStatus(): Promise<AcpPresetStatus | undefined>;
|
|
1417
|
+
/** The installed package's version (the settings page's badge). */
|
|
1418
|
+
pluginVersion(): string;
|
|
1419
|
+
/** Wire the built-in updater (host entry only; absent in pure tests). */
|
|
1420
|
+
setUpdateService(service?: UpdateService): void;
|
|
1421
|
+
/** Registry check for the settings card; `unsupported` when no updater is wired. */
|
|
1422
|
+
pluginUpdateCheck(): Promise<PluginUpdateState>;
|
|
1423
|
+
/** Delegate one install to the host plugin manager; never touches files itself. */
|
|
1424
|
+
pluginUpdateApply(version: string): Promise<PluginUpdateState>;
|
|
1425
|
+
/**
|
|
1426
|
+
* Whether any ACP work is in flight. An update install may restart the host,
|
|
1427
|
+
* so check/apply describe the host as busy while a turn is running.
|
|
1428
|
+
*
|
|
1429
|
+
* Segmented turn streams count even between steps (the host is still
|
|
1430
|
+
* continuing that turn); `running` / `settled` on a session covers `/acp`
|
|
1431
|
+
* turns and the window between a prompt and the agent's acknowledgement.
|
|
1432
|
+
*/
|
|
1433
|
+
hasActiveTurns(): boolean;
|
|
1255
1434
|
private getStoragePath;
|
|
1256
1435
|
/**
|
|
1257
1436
|
* Read the agent registry file.
|
|
@@ -1522,6 +1701,8 @@ export declare class BridgeManager {
|
|
|
1522
1701
|
prompt(sessionId: string, text: string): Promise<{
|
|
1523
1702
|
turnId: string;
|
|
1524
1703
|
}>;
|
|
1704
|
+
/** The late answer landed (or failed): the turn is no longer an orphan. */
|
|
1705
|
+
private clearOrphanPrompt;
|
|
1525
1706
|
cancel(sessionId: string): Promise<void>;
|
|
1526
1707
|
closeSession(sessionId: string): Promise<void>;
|
|
1527
1708
|
setMode(sessionId: string, modeId: string): Promise<void>;
|
|
@@ -1539,6 +1720,11 @@ export declare class BridgeManager {
|
|
|
1539
1720
|
shutdownAll(): Promise<void>;
|
|
1540
1721
|
/** Host-side reader of the harness preset roster (`ctx.agentPresets`), for `presetStatus`. */
|
|
1541
1722
|
private presetRosterReader?;
|
|
1723
|
+
/**
|
|
1724
|
+
* The built-in updater, wired by the host entry. Absent in pure bridge tests —
|
|
1725
|
+
* the update RPCs then answer `unsupported` instead of inventing a source.
|
|
1726
|
+
*/
|
|
1727
|
+
private updateService?;
|
|
1542
1728
|
/**
|
|
1543
1729
|
* The plugin's own default ACP agent (the panel's ★ gesture): which agent an
|
|
1544
1730
|
* ACP-mode conversation starts on. Host-side state in the agent registry
|
|
@@ -1633,6 +1819,22 @@ export declare class BridgeManager {
|
|
|
1633
1819
|
*/
|
|
1634
1820
|
private seedChatChoiceFromCatalog;
|
|
1635
1821
|
getOrCreateDshSession(dshSessionId: string | undefined, agentId: string, cwd?: string): Promise<LiveSession>;
|
|
1822
|
+
/**
|
|
1823
|
+
* Take the agent process back from a turn the stall watchdog abandoned.
|
|
1824
|
+
*
|
|
1825
|
+
* The wedged agent still holds its `session/prompt`, so the next message
|
|
1826
|
+
* cannot simply be sent: it would queue behind a turn that may never answer.
|
|
1827
|
+
* The process is restarted instead — unless another conversation on the same
|
|
1828
|
+
* connection is doing real work, because killing the child destroys THEIR
|
|
1829
|
+
* context too, and "retry after they finish" is the honest answer.
|
|
1830
|
+
*
|
|
1831
|
+
* A restart that can `session/load` costs the conversation nothing: the
|
|
1832
|
+
* session is parked `released` and the normal acquisition path resumes it, so
|
|
1833
|
+
* chat keeps its lock / `applyChatChoice` semantics and `/acp` keeps
|
|
1834
|
+
* `locateOrResume`. An agent without resume support is reset instead, and the
|
|
1835
|
+
* user is told the context is gone.
|
|
1836
|
+
*/
|
|
1837
|
+
private prepareStalledSession;
|
|
1636
1838
|
streamPrompt(options: GenerateOptions): AsyncIterable<StreamChunk>;
|
|
1637
1839
|
}
|
|
1638
1840
|
export declare class AcpLlmAdapter implements LlmAdapterLike {
|
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
*
|
|
6
6
|
* Deliberately framework-free: React binds through `useSyncExternalStore`.
|
|
7
7
|
*/
|
|
8
|
-
import type { AcpApprovalDeadlines, AcpConversationChoice, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentSavePayload, AgentTestResult, AgentView, ChatSessionView, DetectedAgentInfo, PermissionTimeoutMode, SessionView } from '../protocol.ts';
|
|
8
|
+
import type { AcpApprovalDeadlines, AcpConversationChoice, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentSavePayload, AgentTestResult, AgentView, ChatSessionView, DetectedAgentInfo, PermissionTimeoutMode, PluginUpdateState, SessionView } from '../protocol.ts';
|
|
9
9
|
/** One slash command the ACP agent declared, whole row (the menu draws name, description and `input.hint`). */
|
|
10
10
|
export type AgentCommand = NonNullable<ChatSessionView['availableCommands']>[number];
|
|
11
11
|
/** The unary RPC face (narrowed `ctx.connection.rpc.call`). */
|
|
@@ -304,6 +304,18 @@ export interface UiState {
|
|
|
304
304
|
* (`preset/status` answering `null`), in which case the panel hides the block.
|
|
305
305
|
*/
|
|
306
306
|
preset?: AcpPresetStatus;
|
|
307
|
+
/**
|
|
308
|
+
* The installed host package's version (`plugin/version`), rendered as the
|
|
309
|
+
* settings page badge. Absent on a host half without the endpoint, which
|
|
310
|
+
* hides the badge rather than naming a version this browser invented.
|
|
311
|
+
*/
|
|
312
|
+
pluginVersion?: string;
|
|
313
|
+
/**
|
|
314
|
+
* The running version and its update status (`plugin/update-check`). Absent
|
|
315
|
+
* on a host half without the endpoint, which hides the update entry instead
|
|
316
|
+
* of offering an install this browser cannot perform.
|
|
317
|
+
*/
|
|
318
|
+
pluginUpdate?: PluginUpdateState;
|
|
307
319
|
}
|
|
308
320
|
interface Listener {
|
|
309
321
|
(): void;
|
|
@@ -375,6 +387,22 @@ export declare class AcpStore {
|
|
|
375
387
|
private readonly cachedModelReads;
|
|
376
388
|
/** Counts behind the last successful explicit re-detect, so a locale switch can re-render its summary. */
|
|
377
389
|
private detectCounts;
|
|
390
|
+
/** When {@link UiState.pluginUpdate} was last read from the host (epoch ms). */
|
|
391
|
+
private pluginUpdateFetchedAt;
|
|
392
|
+
/**
|
|
393
|
+
* The shared in-flight `plugin/update-check`. The settings section mounts
|
|
394
|
+
* twice (both entries) and the update popover asks too; without this, two
|
|
395
|
+
* mounts in the same tick each fire their own registry read.
|
|
396
|
+
*/
|
|
397
|
+
private pluginUpdateInflight?;
|
|
398
|
+
/** Re-entry guard for `plugin/update-apply` (a double click is one install). */
|
|
399
|
+
private pluginUpdateApplying;
|
|
400
|
+
/**
|
|
401
|
+
* Update read generation. An apply bumps it so a check that was already in
|
|
402
|
+
* flight (started before the install) cannot land afterwards and overwrite
|
|
403
|
+
* the install's verdict with the pre-install `available`.
|
|
404
|
+
*/
|
|
405
|
+
private pluginUpdateGeneration;
|
|
378
406
|
constructor(rpc: RpcCall, locale: (zh: string, en: string) => string);
|
|
379
407
|
subscribe(listener: Listener): () => void;
|
|
380
408
|
getSnapshot(): UiState;
|
|
@@ -398,6 +426,31 @@ export declare class AcpStore {
|
|
|
398
426
|
private call;
|
|
399
427
|
/** Snapshot refresh used on panel open and after a stream outage. */
|
|
400
428
|
resync(): Promise<void>;
|
|
429
|
+
/**
|
|
430
|
+
* Read the plugin's update state (`plugin/update-check`). Throttled to ten
|
|
431
|
+
* minutes unless `force`: the settings panel asks on mount, and every
|
|
432
|
+
* remount must not hit the registry.
|
|
433
|
+
*
|
|
434
|
+
* An older host serves no such endpoint (`unknown endpoint`). That is a
|
|
435
|
+
* normal degradation, not an error worth a notice: the answer is `undefined`
|
|
436
|
+
* and the panel simply keeps its update entry hidden.
|
|
437
|
+
*/
|
|
438
|
+
checkPluginUpdate(force?: boolean): Promise<PluginUpdateState | undefined>;
|
|
439
|
+
private readPluginUpdate;
|
|
440
|
+
/**
|
|
441
|
+
* Install one exact version (`plugin/update-apply`). The status flips to
|
|
442
|
+
* `applying` first so the badge answers the click; the host's answer then
|
|
443
|
+
* becomes the state.
|
|
444
|
+
*
|
|
445
|
+
* A failed call does NOT prove the install did not happen: on an HMR host
|
|
446
|
+
* the update uninstalls the plugin before reinstalling it, which can cut the
|
|
447
|
+
* response. Whatever the error, the disk may already carry the new version,
|
|
448
|
+
* so the state becomes `restart-required` (the one neutral verdict for "the
|
|
449
|
+
* running code is not the disk's") with `installed` dropped (unknown, not a
|
|
450
|
+
* fact to display) and the notice asks for a reload rather than restoring
|
|
451
|
+
* `available` and pretending nothing changed.
|
|
452
|
+
*/
|
|
453
|
+
applyPluginUpdate(version: string): Promise<PluginUpdateState | undefined>;
|
|
401
454
|
detectAgents(): Promise<void>;
|
|
402
455
|
private detectSummaryOf;
|
|
403
456
|
saveAgent(payload: AgentSavePayload): Promise<void>;
|
package/lib/types/impl.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* fakes.
|
|
6
6
|
*/
|
|
7
7
|
import { randomUUID } from './ids.ts';
|
|
8
|
-
import type { AcpAgentCatalog, AcpAgentDef, AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpMcpServerDef, AcpPluginConfig, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult, AgentView, ApprovalDeadlineKey, ApprovalDeadlinePatch, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, PermissionOptionView, PermissionOutcome, PermissionTimeoutMode, RpcResult } from './protocol.ts';
|
|
8
|
+
import type { AcpAgentCatalog, AcpAgentDef, AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpMcpServerDef, AcpPluginConfig, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult, AgentView, ApprovalDeadlineKey, ApprovalDeadlinePatch, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, PermissionOptionView, PermissionOutcome, PermissionTimeoutMode, PluginUpdateState, RpcResult } from './protocol.ts';
|
|
9
9
|
export { randomUUID };
|
|
10
10
|
/** Structured failure the RPC layer converts to `{ok:false, error}` and `apply()` surfaces at boot. */
|
|
11
11
|
export declare class AcpError extends Error {
|
|
@@ -605,6 +605,13 @@ export declare class PermissionLedger {
|
|
|
605
605
|
requestId: string;
|
|
606
606
|
sessionId: string;
|
|
607
607
|
}>;
|
|
608
|
+
/**
|
|
609
|
+
* Whether ANY outstanding ask belongs to `sessionId`, a HOST session id
|
|
610
|
+
* (`LiveSession.sessionId`) — NOT the ACP protocol session id. Used by the
|
|
611
|
+
* stall watchdog: a turn blocked on a human is not a silent turn, so the
|
|
612
|
+
* session with a pending permission must not be finalized as stalled.
|
|
613
|
+
*/
|
|
614
|
+
hasPendingFor(sessionId: string): boolean;
|
|
608
615
|
/**
|
|
609
616
|
* Outstanding asks WITH the view the browser was shown, for the reconnect
|
|
610
617
|
* replay. Entries created without a view are skipped: replaying a frame the
|
|
@@ -808,6 +815,24 @@ export interface AcpManager {
|
|
|
808
815
|
* the registry service), which hides the panel block instead of guessing.
|
|
809
816
|
*/
|
|
810
817
|
presetStatus?(): Promise<AcpPresetStatus | undefined>;
|
|
818
|
+
/**
|
|
819
|
+
* The installed host package's version, reported for the settings page's
|
|
820
|
+
* badge. `undefined` on a host half that predates the endpoint, which hides
|
|
821
|
+
* the badge rather than showing a number the browser invented.
|
|
822
|
+
*/
|
|
823
|
+
pluginVersion?(): string | undefined;
|
|
824
|
+
/**
|
|
825
|
+
* Ask the host's plugin manager what is published for this package. Read-only
|
|
826
|
+
* toward the installation: never downloads, never swaps files, never restarts
|
|
827
|
+
* the host. Absent on a host half from before the updater existed.
|
|
828
|
+
*/
|
|
829
|
+
pluginUpdateCheck?(): Promise<PluginUpdateState>;
|
|
830
|
+
/**
|
|
831
|
+
* Install one exact stable version THROUGH the host's plugin manager. The
|
|
832
|
+
* manager owns download/restart semantics; this plugin only relays. Absent on
|
|
833
|
+
* an older host half.
|
|
834
|
+
*/
|
|
835
|
+
pluginUpdateApply?(version: string): Promise<PluginUpdateState>;
|
|
811
836
|
}
|
|
812
837
|
/** Dispatch one decoded `/acp` channel call. Never rejects; protocol errors come back as `ok:false`. */
|
|
813
838
|
export declare function dispatchRpc(manager: AcpManager, endpoint: string, payload: unknown): Promise<RpcResult<unknown>>;
|
package/lib/types/protocol.d.ts
CHANGED
|
@@ -229,6 +229,50 @@ export interface AcpPluginConfig {
|
|
|
229
229
|
* up and reports the `cancelled` stop as-is. Default 2.
|
|
230
230
|
*/
|
|
231
231
|
staleCancelMaxRetries?: number;
|
|
232
|
+
/**
|
|
233
|
+
* Inactivity watchdog for a turn the agent owes an answer: how long the turn
|
|
234
|
+
* may stay SILENT before the host finalizes it as stalled.
|
|
235
|
+
*
|
|
236
|
+
* The clock is `LiveSession.lastAgentActivityAt`, refreshed by ANY
|
|
237
|
+
* `session/update`, at every turn start, at each segment start (the host
|
|
238
|
+
* handing the turn back after a card), and when a permission ask arrives or
|
|
239
|
+
* is resolved. The watchdog only READS it while the turn is watched: an OPEN
|
|
240
|
+
* agent segment (always true on the `/acp`-RPC path, which has no driver
|
|
241
|
+
* stream) and no pending approval. The host-card execution phase and an
|
|
242
|
+
* unanswered ask are exempt by that gate, not because the clock is frozen —
|
|
243
|
+
* they legitimately move it too.
|
|
244
|
+
*
|
|
245
|
+
* Firing finalizes the TURN only (an `agent/notice` plus the usual turn
|
|
246
|
+
* end); it does NOT kill the agent process. The wedged `session/prompt` is
|
|
247
|
+
* abandoned, and the next message reclaims the session: parked with
|
|
248
|
+
* `releaseAll` and resumed with `session/load`, or reset outright when the
|
|
249
|
+
* agent declares no `loadSession` (the context is gone and the user is
|
|
250
|
+
* told).
|
|
251
|
+
*
|
|
252
|
+
* Default 600000 ms (10 minutes); `0` disables the WHOLE watchdog, the tool
|
|
253
|
+
* window included.
|
|
254
|
+
*/
|
|
255
|
+
turnStallMs?: number;
|
|
256
|
+
/**
|
|
257
|
+
* The longer silence window applied while ANY ACP tool call is still active
|
|
258
|
+
* by `toolCallId` — parallel calls are tracked as a set, so the window lasts
|
|
259
|
+
* until the LAST of them finishes. The agent is working on its own clock in
|
|
260
|
+
* there, so the plain turn window would call a healthy long-running tool
|
|
261
|
+
* stalled.
|
|
262
|
+
*
|
|
263
|
+
* Default 1800000 ms (30 minutes); `0` falls back to
|
|
264
|
+
* {@link AcpPluginConfig.turnStallMs}. It matches
|
|
265
|
+
* `TOOL_OUTPUT_WAIT_MS = 30 * 60 * 1000` in `presets/acp/acp-guard.mjs`, so
|
|
266
|
+
* the two halves of one turn agree on how long a tool may take.
|
|
267
|
+
*/
|
|
268
|
+
turnStallToolMs?: number;
|
|
269
|
+
/**
|
|
270
|
+
* How often the watchdog scans for silent turns, in milliseconds. A tuning /
|
|
271
|
+
* test knob only: NO settings-page control exists for it — and none for the
|
|
272
|
+
* two windows above either; the whole watchdog is configured in the patch
|
|
273
|
+
* file. Default 30000 ms.
|
|
274
|
+
*/
|
|
275
|
+
turnStallTickMs?: number;
|
|
232
276
|
}
|
|
233
277
|
export type AgentProcessState = 'stopped' | 'starting' | 'ready' | 'error';
|
|
234
278
|
/** Agent facts the browser may render (never includes env/argv secrets beyond argv itself, which is config-visible). */
|
|
@@ -669,6 +713,35 @@ export interface DefaultAgentSetPayload {
|
|
|
669
713
|
export type DefaultAgentResult = {
|
|
670
714
|
agentId?: string;
|
|
671
715
|
};
|
|
716
|
+
/** `plugin/version`: the installed host package's version, for the settings badge. */
|
|
717
|
+
export type PluginVersionResult = {
|
|
718
|
+
version?: string;
|
|
719
|
+
};
|
|
720
|
+
/**
|
|
721
|
+
* Where the plugin's own update check stands. The host half only ever reports
|
|
722
|
+
* facts it can read from the harness plugin manager and the npm registry — it
|
|
723
|
+
* never downloads, swaps files, or restarts itself.
|
|
724
|
+
*/
|
|
725
|
+
export type PluginUpdateStatus = 'unsupported' | 'not-installed' | 'source-install' | 'managed' | 'current' | 'available' | 'applying' | 'busy' | 'applied' | 'restart-required' | 'failed';
|
|
726
|
+
/**
|
|
727
|
+
* One update-check answer, shared by `plugin/update-check` and
|
|
728
|
+
* `plugin/update-apply`.
|
|
729
|
+
*/
|
|
730
|
+
export interface PluginUpdateState {
|
|
731
|
+
status: PluginUpdateStatus;
|
|
732
|
+
/** 运行中(内存里)的插件版本 */
|
|
733
|
+
current: string;
|
|
734
|
+
/** 磁盘上装着的版本(listBundles().version),可读时才有 */
|
|
735
|
+
installed?: string;
|
|
736
|
+
/** profile registry 链上最新的稳定版 */
|
|
737
|
+
latest?: string;
|
|
738
|
+
/** latest 的 npm 发布时间 ISO */
|
|
739
|
+
publishedAt?: string;
|
|
740
|
+
/** 实际按顺序问过的 registry */
|
|
741
|
+
registries?: string[];
|
|
742
|
+
/** 失败/额外说明(英文技术信息,UI 自己负责本地化) */
|
|
743
|
+
message?: string;
|
|
744
|
+
}
|
|
672
745
|
/**
|
|
673
746
|
* How the composer-edge agent tag shows liveness (the blue halo). `breath`
|
|
674
747
|
* pulses the halo at the speed below; `steady` shows a constant glow.
|