dsh-device-fleet 0.1.0-dev.5 → 0.1.0-dev.6

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-device-fleet",
3
- "version": "0.1.0-dev.5",
3
+ "version": "0.1.0-dev.6",
4
4
  "description": "DeepSeek Harness device fleet: enroll devices, direct-call remote tools, shared Rooms with lazy-woken agents, and delegated subagents across devices.",
5
5
  "type": "module",
6
6
  "main": "src/plugin/index.js",
package/src/host/store.js CHANGED
@@ -50,6 +50,7 @@ export function loadState({ dshHome, dataDir, name = 'device' } = {}) {
50
50
 
51
51
  const roster = readJson(path.join(dir, 'roster.json'), [])
52
52
  const config = readJson(path.join(dir, 'config.json'), {})
53
+ const peerHints = readJson(path.join(dir, 'peer-hints.json'), [])
53
54
  let enrollment = readJson(path.join(dir, 'enrollment.json'), null)
54
55
  let pendingEnrollment = readJson(path.join(dir, 'enrollment.pending.json'), null)
55
56
 
@@ -75,7 +76,32 @@ export function loadState({ dshHome, dataDir, name = 'device' } = {}) {
75
76
  }
76
77
  }
77
78
 
78
- return { dataDir: dir, identity, fleet, roster: Array.isArray(roster) ? roster : [], config, enrollment, pendingEnrollment, createdFleet }
79
+ return {
80
+ dataDir: dir,
81
+ identity,
82
+ fleet,
83
+ roster: Array.isArray(roster) ? roster : [],
84
+ config,
85
+ enrollment,
86
+ pendingEnrollment,
87
+ peerHints: Array.isArray(peerHints) ? peerHints : [],
88
+ createdFleet,
89
+ }
90
+ }
91
+
92
+ /** Persist one manually supplied peer address (from fleet_connect) for reconnects. */
93
+ export function savePeerHint(state, hint) {
94
+ if (!hint || typeof hint.host !== 'string' || !Number.isInteger(hint.port) || hint.port <= 0) return
95
+ const entry = {
96
+ host: hint.host,
97
+ port: hint.port,
98
+ ...(typeof hint.deviceId === 'string' ? { deviceId: hint.deviceId } : {}),
99
+ }
100
+ const hints = Array.isArray(state.peerHints) ? state.peerHints : []
101
+ const exists = hints.some((item) => item.host === entry.host && item.port === entry.port && item.deviceId === entry.deviceId)
102
+ if (!exists) hints.push(entry)
103
+ state.peerHints = hints
104
+ writeJsonAtomic(path.join(state.dataDir, 'peer-hints.json'), hints)
79
105
  }
80
106
 
81
107
  /** Queue an invite for the next plugin load (the running node keeps its fleet). */
package/src/host/tools.js CHANGED
@@ -22,7 +22,7 @@ function fail(error) {
22
22
  }
23
23
 
24
24
  /** Pure handlers; wrapped into ToolDefinitions by `createFleetToolDefinitions`. */
25
- export function createFleetHandlers({ node, client = new DirectCallClient({ node }), pairing = null }) {
25
+ export function createFleetHandlers({ node, client = new DirectCallClient({ node }), pairing = null, onPeerHint = null }) {
26
26
  return {
27
27
  async fleet_status() {
28
28
  if (typeof node.status !== 'function') return { text: 'Error: [unsupported] status is unavailable' }
@@ -34,6 +34,7 @@ export function createFleetHandlers({ node, client = new DirectCallClient({ node
34
34
  try {
35
35
  node.addPeerHint?.({ host, port })
36
36
  const peer = await node.connectAddress(host, port)
37
+ await onPeerHint?.({ host, port })
37
38
  return { text: `connected to ${peer.deviceId} (${peer.name ?? 'device'}) at ${host}:${port}` }
38
39
  } catch (error) {
39
40
  return fail(error)
@@ -281,6 +281,14 @@ export class FleetNode extends EventEmitter {
281
281
  if (current === channel) this.channels.delete(channel.peer.deviceId)
282
282
  this.rejectPendingForChannel(channel, Object.assign(new Error('peer connection closed'), { code: 'peer-offline' }))
283
283
  this.emit('peer-disconnected', { deviceId: channel.peer?.deviceId, via })
284
+ // Recover automatically: the peer may have restarted or moved; retry the
285
+ // known addresses instead of waiting for the next process start.
286
+ if (!this.closed && this.channels.size === 0 && this.peerHints.length > 0) {
287
+ clearTimeout(this.hintTimer)
288
+ this.hintTimer = null
289
+ this.hintAttempt = 0
290
+ this.scheduleHintConnects()
291
+ }
284
292
  })
285
293
  }
286
294
 
@@ -17,7 +17,7 @@ import { DelegationHost } from '../host/delegation.js'
17
17
  import { ExecutionSlot, DEFAULT_SHELL_TOOL } from '../host/execution-slot.js'
18
18
  import { ToolPolicy } from '../host/tool-policy.js'
19
19
  import { createFleetHandlers, createFleetToolDefinitions } from '../host/tools.js'
20
- import { accountMatches, createInvite, loadState, saveRoster, writePendingEnrollment } from '../host/store.js'
20
+ import { accountMatches, createInvite, loadState, savePeerHint, saveRoster, writePendingEnrollment } from '../host/store.js'
21
21
 
22
22
  export const name = 'device-fleet'
23
23
 
@@ -147,7 +147,7 @@ export function apply(ctx, config) {
147
147
  discovery: config.discovery ? true : false,
148
148
  relay: config.relay ?? state.enrollment?.relay ?? null,
149
149
  roster: state.roster,
150
- peerHints: state.enrollment?.endpoints ?? [],
150
+ peerHints: [...(state.enrollment?.endpoints ?? []), ...(state.peerHints ?? [])],
151
151
  pluginVersion,
152
152
  dshVersion,
153
153
  capabilities: CAPABILITIES,
@@ -268,7 +268,18 @@ export function apply(ctx, config) {
268
268
  writePendingEnrollment(state, code)
269
269
  },
270
270
  }
271
- const handlers = createFleetHandlers({ node, client, pairing })
271
+ const handlers = createFleetHandlers({
272
+ node,
273
+ client,
274
+ pairing,
275
+ onPeerHint: async (hint) => {
276
+ try {
277
+ savePeerHint(state, hint)
278
+ } catch (error) {
279
+ logger.warn?.(`[device-fleet] cannot persist peer hint: ${String(error?.message ?? error)}`)
280
+ }
281
+ },
282
+ })
272
283
  const definitions = createFleetToolDefinitions({ defineTool, handlers })
273
284
  const wrapped = withManual(definitions.map((definition) => ({ ...definition, execute: safe(definition.execute) })))
274
285
  for (const definition of wrapped) ctx.tools.register(definition)