dsh-ntr 0.2.0 → 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 +9 -6
- package/README.md +9 -6
- package/cordis.patch.yml +12 -0
- package/lib/bridge.js +654 -85
- package/lib/client/panel.js +224 -78
- package/lib/client/store.js +133 -1
- package/lib/client.js +702 -304
- 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 +208 -6
- package/lib/types/client/panel.d.ts +0 -1
- 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 +5 -1
- package/src/bridge.ts +700 -83
- package/src/client/panel.tsx +406 -205
- 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/src/client/store.ts
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
* Deliberately framework-free: React binds through `useSyncExternalStore`.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import type { AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentSavePayload, AgentTestResult, AgentView, ChatSessionView, ChatTranscriptView, ApprovalDeadlinesSetPayload, ApprovalDeadlinesResult, DetectedAgentInfo, PermissionTimeoutMode, SessionView } from '../protocol.ts'
|
|
9
|
+
import type { AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentSavePayload, AgentTestResult, AgentView, ChatSessionView, ChatTranscriptView, ApprovalDeadlinesSetPayload, ApprovalDeadlinesResult, DetectedAgentInfo, PermissionTimeoutMode, PluginUpdateState, PluginVersionResult, SessionView } from '../protocol.ts'
|
|
10
10
|
import { DEFAULT_TAG_HALO } from '../protocol.ts'
|
|
11
11
|
|
|
12
12
|
/** One slash command the ACP agent declared, whole row (the menu draws name, description and `input.hint`). */
|
|
@@ -298,6 +298,18 @@ export interface UiState {
|
|
|
298
298
|
* (`preset/status` answering `null`), in which case the panel hides the block.
|
|
299
299
|
*/
|
|
300
300
|
preset?: AcpPresetStatus
|
|
301
|
+
/**
|
|
302
|
+
* The installed host package's version (`plugin/version`), rendered as the
|
|
303
|
+
* settings page badge. Absent on a host half without the endpoint, which
|
|
304
|
+
* hides the badge rather than naming a version this browser invented.
|
|
305
|
+
*/
|
|
306
|
+
pluginVersion?: string
|
|
307
|
+
/**
|
|
308
|
+
* The running version and its update status (`plugin/update-check`). Absent
|
|
309
|
+
* on a host half without the endpoint, which hides the update entry instead
|
|
310
|
+
* of offering an install this browser cannot perform.
|
|
311
|
+
*/
|
|
312
|
+
pluginUpdate?: PluginUpdateState
|
|
301
313
|
}
|
|
302
314
|
|
|
303
315
|
interface Listener { (): void }
|
|
@@ -309,6 +321,13 @@ const MAX_WATCHED = 6
|
|
|
309
321
|
/** Raw JSON budget for one event block; the cut is marked. */
|
|
310
322
|
const RAW_MAX = 256_000
|
|
311
323
|
|
|
324
|
+
/**
|
|
325
|
+
* How long a fetched update state stays fresh. The settings panel asks on every
|
|
326
|
+
* mount; without this, every open would hit the registry. A forced check (the
|
|
327
|
+
* card's own button) always goes out.
|
|
328
|
+
*/
|
|
329
|
+
const PLUGIN_UPDATE_TTL_MS = 10 * 60 * 1000
|
|
330
|
+
|
|
312
331
|
/** Pretty-printed JSON of an agent message (never throws, cuts only at {@link RAW_MAX} with a marker). */
|
|
313
332
|
export function jsonText(value: unknown): string {
|
|
314
333
|
let text: string
|
|
@@ -531,6 +550,14 @@ export const DEFAULT_KNOWN_PRESETS: DetectedAgentInfo[] = [
|
|
|
531
550
|
defaultCommand: 'opencode',
|
|
532
551
|
defaultArgs: ['acp'],
|
|
533
552
|
},
|
|
553
|
+
{
|
|
554
|
+
id: 'cursor',
|
|
555
|
+
name: 'Cursor CLI',
|
|
556
|
+
description: 'Cursor CLI — run as `agent acp` over stdio (legacy alias `cursor-agent acp`)',
|
|
557
|
+
detected: false,
|
|
558
|
+
defaultCommand: 'agent',
|
|
559
|
+
defaultArgs: ['acp'],
|
|
560
|
+
},
|
|
534
561
|
{
|
|
535
562
|
id: 'qwen',
|
|
536
563
|
name: 'Qwen Code',
|
|
@@ -646,6 +673,22 @@ export class AcpStore {
|
|
|
646
673
|
private readonly cachedModelReads = new Set<string>()
|
|
647
674
|
/** Counts behind the last successful explicit re-detect, so a locale switch can re-render its summary. */
|
|
648
675
|
private detectCounts: { total: number; installed: number } | undefined
|
|
676
|
+
/** When {@link UiState.pluginUpdate} was last read from the host (epoch ms). */
|
|
677
|
+
private pluginUpdateFetchedAt = 0
|
|
678
|
+
/**
|
|
679
|
+
* The shared in-flight `plugin/update-check`. The settings section mounts
|
|
680
|
+
* twice (both entries) and the update popover asks too; without this, two
|
|
681
|
+
* mounts in the same tick each fire their own registry read.
|
|
682
|
+
*/
|
|
683
|
+
private pluginUpdateInflight?: Promise<PluginUpdateState | undefined>
|
|
684
|
+
/** Re-entry guard for `plugin/update-apply` (a double click is one install). */
|
|
685
|
+
private pluginUpdateApplying = false
|
|
686
|
+
/**
|
|
687
|
+
* Update read generation. An apply bumps it so a check that was already in
|
|
688
|
+
* flight (started before the install) cannot land afterwards and overwrite
|
|
689
|
+
* the install's verdict with the pre-install `available`.
|
|
690
|
+
*/
|
|
691
|
+
private pluginUpdateGeneration = 0
|
|
649
692
|
|
|
650
693
|
constructor(private readonly rpc: RpcCall, private readonly locale: (zh: string, en: string) => string) {}
|
|
651
694
|
|
|
@@ -783,7 +826,7 @@ export class AcpStore {
|
|
|
783
826
|
/** Snapshot refresh used on panel open and after a stream outage. */
|
|
784
827
|
async resync(): Promise<void> {
|
|
785
828
|
try {
|
|
786
|
-
const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult] = await Promise.all([
|
|
829
|
+
const [{ agents }, { sessions }, defaultAgentInfo, detectResult, presetResult, tagHaloResult, deadlinesResult, versionResult] = await Promise.all([
|
|
787
830
|
this.call<{ agents: AgentView[] }>('agents/list'),
|
|
788
831
|
this.call<{ sessions: SessionView[] }>('sessions/list'),
|
|
789
832
|
// The plugin's OWN default ACP agent — a plugin-private id, not a DSH
|
|
@@ -797,6 +840,9 @@ export class AcpStore {
|
|
|
797
840
|
// `undefined` (older half) hides the deadline card instead of showing a
|
|
798
841
|
// number this browser made up.
|
|
799
842
|
this.call<ApprovalDeadlinesResult>('ui/approval-deadlines').catch(() => ({ deadlines: undefined })),
|
|
843
|
+
// The settings badge's data source; older halves answer `unknown
|
|
844
|
+
// endpoint`, which hides the badge.
|
|
845
|
+
this.call<PluginVersionResult>('plugin/version').catch(() => null),
|
|
800
846
|
])
|
|
801
847
|
const detectedAgents = (detectResult && detectResult.agents.length > 0)
|
|
802
848
|
? detectResult.agents
|
|
@@ -811,6 +857,7 @@ export class AcpStore {
|
|
|
811
857
|
preset: presetResult?.preset ?? undefined,
|
|
812
858
|
tagHalo: tagHaloResult?.halo ?? { ...DEFAULT_TAG_HALO },
|
|
813
859
|
approvalDeadlines: deadlinesResult.deadlines,
|
|
860
|
+
pluginVersion: versionResult?.version !== undefined && versionResult.version !== '' ? versionResult.version : undefined,
|
|
814
861
|
})
|
|
815
862
|
const active = this.state.activeSessionId ?? sessions[0]?.sessionId
|
|
816
863
|
if (active !== undefined) await this.loadTranscript(active)
|
|
@@ -819,6 +866,103 @@ export class AcpStore {
|
|
|
819
866
|
}
|
|
820
867
|
}
|
|
821
868
|
|
|
869
|
+
/**
|
|
870
|
+
* Read the plugin's update state (`plugin/update-check`). Throttled to ten
|
|
871
|
+
* minutes unless `force`: the settings panel asks on mount, and every
|
|
872
|
+
* remount must not hit the registry.
|
|
873
|
+
*
|
|
874
|
+
* An older host serves no such endpoint (`unknown endpoint`). That is a
|
|
875
|
+
* normal degradation, not an error worth a notice: the answer is `undefined`
|
|
876
|
+
* and the panel simply keeps its update entry hidden.
|
|
877
|
+
*/
|
|
878
|
+
async checkPluginUpdate(force = false): Promise<PluginUpdateState | undefined> {
|
|
879
|
+
// One read for every caller while it is in flight, force included: the
|
|
880
|
+
// settings page and the popover mounting together must not hit the
|
|
881
|
+
// registry twice.
|
|
882
|
+
if (this.pluginUpdateInflight !== undefined) return this.pluginUpdateInflight
|
|
883
|
+
const current = this.state.pluginUpdate
|
|
884
|
+
if (!force && current !== undefined && Date.now() - this.pluginUpdateFetchedAt < PLUGIN_UPDATE_TTL_MS) {
|
|
885
|
+
return current
|
|
886
|
+
}
|
|
887
|
+
const generation = this.pluginUpdateGeneration
|
|
888
|
+
const inflight = this.readPluginUpdate(generation)
|
|
889
|
+
this.pluginUpdateInflight = inflight
|
|
890
|
+
try {
|
|
891
|
+
return await inflight
|
|
892
|
+
} finally {
|
|
893
|
+
this.pluginUpdateInflight = undefined
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
private async readPluginUpdate(generation: number): Promise<PluginUpdateState | undefined> {
|
|
898
|
+
try {
|
|
899
|
+
const pluginUpdate = await this.call<PluginUpdateState>('plugin/update-check')
|
|
900
|
+
// An install started while this read was in flight: its answer is
|
|
901
|
+
// already stale, and the install's verdict owns the state now.
|
|
902
|
+
if (generation !== this.pluginUpdateGeneration) return undefined
|
|
903
|
+
this.pluginUpdateFetchedAt = Date.now()
|
|
904
|
+
this.mutate({ pluginUpdate })
|
|
905
|
+
return pluginUpdate
|
|
906
|
+
} catch {
|
|
907
|
+
return undefined
|
|
908
|
+
}
|
|
909
|
+
}
|
|
910
|
+
|
|
911
|
+
/**
|
|
912
|
+
* Install one exact version (`plugin/update-apply`). The status flips to
|
|
913
|
+
* `applying` first so the badge answers the click; the host's answer then
|
|
914
|
+
* becomes the state.
|
|
915
|
+
*
|
|
916
|
+
* A failed call does NOT prove the install did not happen: on an HMR host
|
|
917
|
+
* the update uninstalls the plugin before reinstalling it, which can cut the
|
|
918
|
+
* response. Whatever the error, the disk may already carry the new version,
|
|
919
|
+
* so the state becomes `restart-required` (the one neutral verdict for "the
|
|
920
|
+
* running code is not the disk's") with `installed` dropped (unknown, not a
|
|
921
|
+
* fact to display) and the notice asks for a reload rather than restoring
|
|
922
|
+
* `available` and pretending nothing changed.
|
|
923
|
+
*/
|
|
924
|
+
async applyPluginUpdate(version: string): Promise<PluginUpdateState | undefined> {
|
|
925
|
+
if (this.pluginUpdateApplying) return undefined
|
|
926
|
+
this.pluginUpdateApplying = true
|
|
927
|
+
// Any check already in flight predates the install; its answer must not
|
|
928
|
+
// land on top of the install's verdict.
|
|
929
|
+
this.pluginUpdateGeneration += 1
|
|
930
|
+
const previous = this.state.pluginUpdate
|
|
931
|
+
if (previous !== undefined) {
|
|
932
|
+
this.mutate({ pluginUpdate: { ...previous, status: 'applying' } })
|
|
933
|
+
}
|
|
934
|
+
try {
|
|
935
|
+
const pluginUpdate = await this.call<PluginUpdateState>('plugin/update-apply', { version })
|
|
936
|
+
this.pluginUpdateFetchedAt = Date.now()
|
|
937
|
+
this.mutate({ pluginUpdate })
|
|
938
|
+
return pluginUpdate
|
|
939
|
+
} catch (error) {
|
|
940
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
941
|
+
// The disk state is UNKNOWN here: the install may have completed before
|
|
942
|
+
// the response was cut. Keep `latest` for display, drop `installed` so no
|
|
943
|
+
// surface can claim a disk version we never read, and reset the throttle
|
|
944
|
+
// so the next check actually asks the host instead of serving this
|
|
945
|
+
// unknown verdict for ten minutes.
|
|
946
|
+
const interrupted: PluginUpdateState = previous !== undefined
|
|
947
|
+
? { ...previous, status: 'restart-required', installed: undefined, message }
|
|
948
|
+
: { status: 'restart-required', current: this.state.pluginVersion ?? '?', message }
|
|
949
|
+
this.pluginUpdateFetchedAt = 0
|
|
950
|
+
this.mutate({
|
|
951
|
+
pluginUpdate: interrupted,
|
|
952
|
+
notice: this.locale(
|
|
953
|
+
'更新请求已中断;请刷新页面确认当前版本。',
|
|
954
|
+
'The update request was interrupted. Reload the page to confirm the installed version.',
|
|
955
|
+
),
|
|
956
|
+
})
|
|
957
|
+
return undefined
|
|
958
|
+
} finally {
|
|
959
|
+
// Invalidate reads that STARTED during the install: their "applying"
|
|
960
|
+
// answer would otherwise land on top of this call's final verdict.
|
|
961
|
+
this.pluginUpdateGeneration += 1
|
|
962
|
+
this.pluginUpdateApplying = false
|
|
963
|
+
}
|
|
964
|
+
}
|
|
965
|
+
|
|
822
966
|
async detectAgents(): Promise<void> {
|
|
823
967
|
this.mutate({ detecting: true })
|
|
824
968
|
try {
|
package/src/impl.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
import { randomUUID } from './ids.ts'
|
|
9
9
|
import type {
|
|
10
10
|
AcpAgentCatalog, AcpAgentDef, AcpCatalogOption, AcpApprovalDeadlines, AcpConversationChoice, AcpEvent, AcpMcpServerDef, AcpPluginConfig, AcpPresetStatus, AcpTagHalo, AgentCatalogResult, AgentProbeModelsResult, AgentTestResult,
|
|
11
|
-
AgentView, ApprovalDeadlineKey, ApprovalDeadlinePatch, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, PermissionOptionView, PermissionOutcome, PermissionTimeoutMode, RpcResult,
|
|
11
|
+
AgentView, ApprovalDeadlineKey, ApprovalDeadlinePatch, ChatSessionView, ChatTranscriptView, DetectedAgentInfo, PermissionOptionView, PermissionOutcome, PermissionTimeoutMode, PluginUpdateState, RpcResult,
|
|
12
12
|
} from './protocol.ts'
|
|
13
13
|
import { DEFAULT_TAG_HALO } from './protocol.ts'
|
|
14
14
|
|
|
@@ -535,6 +535,22 @@ export function validateConfig(raw: unknown): AcpPluginConfig {
|
|
|
535
535
|
if (typeof staleCancelMaxRetries !== 'number' || !Number.isInteger(staleCancelMaxRetries) || staleCancelMaxRetries < 0 || staleCancelMaxRetries > 5) {
|
|
536
536
|
throw new AcpError('bad_config', 'acp: staleCancelMaxRetries must be an integer in [0, 5]')
|
|
537
537
|
}
|
|
538
|
+
// Stall-recovery windows (inactivity watchdog). Two lengths on purpose: a
|
|
539
|
+
// silent turn with the floor is a stuck agent, but a turn waiting on a TOOL
|
|
540
|
+
// call is a working agent, and the tool half may take as long as the guard's
|
|
541
|
+
// own tool-output wait. `0` means "off" / "fall back", never "expire now".
|
|
542
|
+
const turnStallMs = value.turnStallMs ?? 600_000
|
|
543
|
+
if (typeof turnStallMs !== 'number' || !Number.isInteger(turnStallMs) || turnStallMs < 0 || turnStallMs > 86_400_000) {
|
|
544
|
+
throw new AcpError('bad_config', 'acp: turnStallMs must be an integer in [0, 86400000] (0 = disable the watchdog)')
|
|
545
|
+
}
|
|
546
|
+
const turnStallToolMs = value.turnStallToolMs ?? 1_800_000
|
|
547
|
+
if (typeof turnStallToolMs !== 'number' || !Number.isInteger(turnStallToolMs) || turnStallToolMs < 0 || turnStallToolMs > 86_400_000) {
|
|
548
|
+
throw new AcpError('bad_config', 'acp: turnStallToolMs must be an integer in [0, 86400000] (0 = fall back to turnStallMs)')
|
|
549
|
+
}
|
|
550
|
+
const turnStallTickMs = value.turnStallTickMs ?? 30_000
|
|
551
|
+
if (typeof turnStallTickMs !== 'number' || !Number.isInteger(turnStallTickMs) || turnStallTickMs < 1 || turnStallTickMs > 60_000) {
|
|
552
|
+
throw new AcpError('bad_config', 'acp: turnStallTickMs must be an integer in [1, 60000]')
|
|
553
|
+
}
|
|
538
554
|
return {
|
|
539
555
|
agents,
|
|
540
556
|
transcriptMaxEntries: max,
|
|
@@ -545,6 +561,9 @@ export function validateConfig(raw: unknown): AcpPluginConfig {
|
|
|
545
561
|
cancelFallbackMs,
|
|
546
562
|
staleCancelRetryDelayMs,
|
|
547
563
|
staleCancelMaxRetries,
|
|
564
|
+
turnStallMs,
|
|
565
|
+
turnStallToolMs,
|
|
566
|
+
turnStallTickMs,
|
|
548
567
|
}
|
|
549
568
|
}
|
|
550
569
|
|
|
@@ -1649,6 +1668,19 @@ export class PermissionLedger {
|
|
|
1649
1668
|
return [...this.pending.values()].map(e => ({ requestId: e.requestId, sessionId: e.sessionId }))
|
|
1650
1669
|
}
|
|
1651
1670
|
|
|
1671
|
+
/**
|
|
1672
|
+
* Whether ANY outstanding ask belongs to `sessionId`, a HOST session id
|
|
1673
|
+
* (`LiveSession.sessionId`) — NOT the ACP protocol session id. Used by the
|
|
1674
|
+
* stall watchdog: a turn blocked on a human is not a silent turn, so the
|
|
1675
|
+
* session with a pending permission must not be finalized as stalled.
|
|
1676
|
+
*/
|
|
1677
|
+
hasPendingFor(sessionId: string): boolean {
|
|
1678
|
+
for (const entry of this.pending.values()) {
|
|
1679
|
+
if (entry.sessionId === sessionId) return true
|
|
1680
|
+
}
|
|
1681
|
+
return false
|
|
1682
|
+
}
|
|
1683
|
+
|
|
1652
1684
|
/**
|
|
1653
1685
|
* Outstanding asks WITH the view the browser was shown, for the reconnect
|
|
1654
1686
|
* replay. Entries created without a view are skipped: replaying a frame the
|
|
@@ -1874,6 +1906,24 @@ export interface AcpManager {
|
|
|
1874
1906
|
* the registry service), which hides the panel block instead of guessing.
|
|
1875
1907
|
*/
|
|
1876
1908
|
presetStatus?(): Promise<AcpPresetStatus | undefined>
|
|
1909
|
+
/**
|
|
1910
|
+
* The installed host package's version, reported for the settings page's
|
|
1911
|
+
* badge. `undefined` on a host half that predates the endpoint, which hides
|
|
1912
|
+
* the badge rather than showing a number the browser invented.
|
|
1913
|
+
*/
|
|
1914
|
+
pluginVersion?(): string | undefined
|
|
1915
|
+
/**
|
|
1916
|
+
* Ask the host's plugin manager what is published for this package. Read-only
|
|
1917
|
+
* toward the installation: never downloads, never swaps files, never restarts
|
|
1918
|
+
* the host. Absent on a host half from before the updater existed.
|
|
1919
|
+
*/
|
|
1920
|
+
pluginUpdateCheck?(): Promise<PluginUpdateState>
|
|
1921
|
+
/**
|
|
1922
|
+
* Install one exact stable version THROUGH the host's plugin manager. The
|
|
1923
|
+
* manager owns download/restart semantics; this plugin only relays. Absent on
|
|
1924
|
+
* an older host half.
|
|
1925
|
+
*/
|
|
1926
|
+
pluginUpdateApply?(version: string): Promise<PluginUpdateState>
|
|
1877
1927
|
}
|
|
1878
1928
|
|
|
1879
1929
|
function ok<T>(value: T): RpcResult<T> {
|
|
@@ -2038,6 +2088,21 @@ export async function dispatchRpc(manager: AcpManager, endpoint: string, payload
|
|
|
2038
2088
|
}
|
|
2039
2089
|
case 'preset/status':
|
|
2040
2090
|
return ok({ preset: await manager.presetStatus?.() ?? null })
|
|
2091
|
+
case 'plugin/version':
|
|
2092
|
+
// Absent on a host half from before the badge existed: the browser
|
|
2093
|
+
// hides it instead of naming a version it cannot know.
|
|
2094
|
+
return ok({ version: manager.pluginVersion?.() })
|
|
2095
|
+
case 'plugin/update-check':
|
|
2096
|
+
// An older host half answers `unsupported` rather than failing: the
|
|
2097
|
+
// settings card then hides the update section.
|
|
2098
|
+
return ok(await (manager.pluginUpdateCheck?.() ?? { status: 'unsupported' as const, current: '' }))
|
|
2099
|
+
case 'plugin/update-apply': {
|
|
2100
|
+
const version = str(body, 'version', 64)
|
|
2101
|
+
if (!/^\d+\.\d+\.\d+$/.test(version)) {
|
|
2102
|
+
throw new AcpError('bad_payload', 'acp: version must be a stable semver (x.y.z)')
|
|
2103
|
+
}
|
|
2104
|
+
return ok(await (manager.pluginUpdateApply?.(version) ?? { status: 'unsupported' as const, current: '' }))
|
|
2105
|
+
}
|
|
2041
2106
|
case 'agent/stop':
|
|
2042
2107
|
await manager.stopAgent(str(body, 'agentId', 64)); return ok({ done: true })
|
|
2043
2108
|
case 'default/agent':
|
package/src/index.ts
CHANGED
|
@@ -8,10 +8,12 @@
|
|
|
8
8
|
import type { Context } from '@deepseek-ai/cordis'
|
|
9
9
|
import type {} from '@deepseek-ai/dsh-host-webserver'
|
|
10
10
|
import type { ConnectionRpcHandler, HostConnectionHandle } from '@deepseek-ai/dsh-client-connection'
|
|
11
|
-
import { ACP_PRESET_ID, AcpLlmAdapter, BridgeManager } from './bridge.ts'
|
|
11
|
+
import { ACP_PRESET_ID, AcpLlmAdapter, BridgeManager, PLUGIN_VERSION } from './bridge.ts'
|
|
12
12
|
import { EventHub, dispatchRpc, permissionReplayFrames, sessionAgentPreset, sessionAgentPresetFromLog, validateConfig } from './impl.ts'
|
|
13
13
|
import type { AcpEvent, ApprovalDeadlineKey } from './protocol.ts'
|
|
14
14
|
import type { RosterPresetRow, SessionLogLike } from './impl.ts'
|
|
15
|
+
import { UpdateService } from './update.ts'
|
|
16
|
+
import type { UpdateManagerLike } from './update.ts'
|
|
15
17
|
|
|
16
18
|
export const name = 'dsh-ntr'
|
|
17
19
|
// Deliberately minimal: every other host service is probed structurally
|
|
@@ -86,9 +88,22 @@ export function apply(ctx: Context, config?: unknown): void {
|
|
|
86
88
|
cancelFallbackMs: resolved.cancelFallbackMs,
|
|
87
89
|
staleCancelRetryDelayMs: resolved.staleCancelRetryDelayMs,
|
|
88
90
|
staleCancelMaxRetries: resolved.staleCancelMaxRetries,
|
|
91
|
+
turnStallMs: resolved.turnStallMs,
|
|
92
|
+
turnStallToolMs: resolved.turnStallToolMs,
|
|
93
|
+
turnStallTickMs: resolved.turnStallTickMs,
|
|
89
94
|
},
|
|
90
95
|
)
|
|
91
96
|
const adapter = new AcpLlmAdapter(manager)
|
|
97
|
+
// Built-in update: the plugin never downloads or swaps files itself. The
|
|
98
|
+
// harness plugin manager owns the install; this only relays its facts.
|
|
99
|
+
// `resolveManager` re-probes on every call so a late-loading service is
|
|
100
|
+
// picked up without an `inject` dependency (a host without it must still
|
|
101
|
+
// load this plugin, and then reports `unsupported`).
|
|
102
|
+
manager.setUpdateService(new UpdateService({
|
|
103
|
+
resolveManager: () => (typeof ctx.get === 'function' ? ctx.get('pluginManager') : undefined) as UpdateManagerLike | undefined,
|
|
104
|
+
currentVersion: PLUGIN_VERSION,
|
|
105
|
+
isBusy: () => manager.hasActiveTurns(),
|
|
106
|
+
}))
|
|
92
107
|
// The RAW patch-file deadlines, before validation filled the defaults in.
|
|
93
108
|
// `validateConfig` returns a number for every knob, so the validated object
|
|
94
109
|
// cannot tell "the deployment asked for 10 minutes" from "the deployment said
|
package/src/protocol.ts
CHANGED
|
@@ -238,6 +238,50 @@ export interface AcpPluginConfig {
|
|
|
238
238
|
* up and reports the `cancelled` stop as-is. Default 2.
|
|
239
239
|
*/
|
|
240
240
|
staleCancelMaxRetries?: number
|
|
241
|
+
/**
|
|
242
|
+
* Inactivity watchdog for a turn the agent owes an answer: how long the turn
|
|
243
|
+
* may stay SILENT before the host finalizes it as stalled.
|
|
244
|
+
*
|
|
245
|
+
* The clock is `LiveSession.lastAgentActivityAt`, refreshed by ANY
|
|
246
|
+
* `session/update`, at every turn start, at each segment start (the host
|
|
247
|
+
* handing the turn back after a card), and when a permission ask arrives or
|
|
248
|
+
* is resolved. The watchdog only READS it while the turn is watched: an OPEN
|
|
249
|
+
* agent segment (always true on the `/acp`-RPC path, which has no driver
|
|
250
|
+
* stream) and no pending approval. The host-card execution phase and an
|
|
251
|
+
* unanswered ask are exempt by that gate, not because the clock is frozen —
|
|
252
|
+
* they legitimately move it too.
|
|
253
|
+
*
|
|
254
|
+
* Firing finalizes the TURN only (an `agent/notice` plus the usual turn
|
|
255
|
+
* end); it does NOT kill the agent process. The wedged `session/prompt` is
|
|
256
|
+
* abandoned, and the next message reclaims the session: parked with
|
|
257
|
+
* `releaseAll` and resumed with `session/load`, or reset outright when the
|
|
258
|
+
* agent declares no `loadSession` (the context is gone and the user is
|
|
259
|
+
* told).
|
|
260
|
+
*
|
|
261
|
+
* Default 600000 ms (10 minutes); `0` disables the WHOLE watchdog, the tool
|
|
262
|
+
* window included.
|
|
263
|
+
*/
|
|
264
|
+
turnStallMs?: number
|
|
265
|
+
/**
|
|
266
|
+
* The longer silence window applied while ANY ACP tool call is still active
|
|
267
|
+
* by `toolCallId` — parallel calls are tracked as a set, so the window lasts
|
|
268
|
+
* until the LAST of them finishes. The agent is working on its own clock in
|
|
269
|
+
* there, so the plain turn window would call a healthy long-running tool
|
|
270
|
+
* stalled.
|
|
271
|
+
*
|
|
272
|
+
* Default 1800000 ms (30 minutes); `0` falls back to
|
|
273
|
+
* {@link AcpPluginConfig.turnStallMs}. It matches
|
|
274
|
+
* `TOOL_OUTPUT_WAIT_MS = 30 * 60 * 1000` in `presets/acp/acp-guard.mjs`, so
|
|
275
|
+
* the two halves of one turn agree on how long a tool may take.
|
|
276
|
+
*/
|
|
277
|
+
turnStallToolMs?: number
|
|
278
|
+
/**
|
|
279
|
+
* How often the watchdog scans for silent turns, in milliseconds. A tuning /
|
|
280
|
+
* test knob only: NO settings-page control exists for it — and none for the
|
|
281
|
+
* two windows above either; the whole watchdog is configured in the patch
|
|
282
|
+
* file. Default 30000 ms.
|
|
283
|
+
*/
|
|
284
|
+
turnStallTickMs?: number
|
|
241
285
|
}
|
|
242
286
|
|
|
243
287
|
// ---- Host → browser events (`/api/acp/stream`, SSE) ----
|
|
@@ -564,6 +608,38 @@ export interface AgentIdPayload { agentId: string }
|
|
|
564
608
|
export interface DefaultAgentSetPayload { agentId?: string }
|
|
565
609
|
/** `default/agent`: the plugin's own default agent id, or `undefined` when unset. */
|
|
566
610
|
export type DefaultAgentResult = { agentId?: string }
|
|
611
|
+
/** `plugin/version`: the installed host package's version, for the settings badge. */
|
|
612
|
+
export type PluginVersionResult = { version?: string }
|
|
613
|
+
|
|
614
|
+
/**
|
|
615
|
+
* Where the plugin's own update check stands. The host half only ever reports
|
|
616
|
+
* facts it can read from the harness plugin manager and the npm registry — it
|
|
617
|
+
* never downloads, swaps files, or restarts itself.
|
|
618
|
+
*/
|
|
619
|
+
export type PluginUpdateStatus =
|
|
620
|
+
| 'unsupported' | 'not-installed' | 'source-install' | 'managed'
|
|
621
|
+
| 'current' | 'available' | 'applying' | 'busy'
|
|
622
|
+
| 'applied' | 'restart-required' | 'failed'
|
|
623
|
+
|
|
624
|
+
/**
|
|
625
|
+
* One update-check answer, shared by `plugin/update-check` and
|
|
626
|
+
* `plugin/update-apply`.
|
|
627
|
+
*/
|
|
628
|
+
export interface PluginUpdateState {
|
|
629
|
+
status: PluginUpdateStatus
|
|
630
|
+
/** 运行中(内存里)的插件版本 */
|
|
631
|
+
current: string
|
|
632
|
+
/** 磁盘上装着的版本(listBundles().version),可读时才有 */
|
|
633
|
+
installed?: string
|
|
634
|
+
/** profile registry 链上最新的稳定版 */
|
|
635
|
+
latest?: string
|
|
636
|
+
/** latest 的 npm 发布时间 ISO */
|
|
637
|
+
publishedAt?: string
|
|
638
|
+
/** 实际按顺序问过的 registry */
|
|
639
|
+
registries?: string[]
|
|
640
|
+
/** 失败/额外说明(英文技术信息,UI 自己负责本地化) */
|
|
641
|
+
message?: string
|
|
642
|
+
}
|
|
567
643
|
|
|
568
644
|
/**
|
|
569
645
|
* How the composer-edge agent tag shows liveness (the blue halo). `breath`
|