external-subagent 0.5.3 → 0.5.5
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/cli/commands/update.mjs
CHANGED
|
@@ -69,9 +69,36 @@ export async function updateCommand(paths, args = [], daemon = {}) {
|
|
|
69
69
|
// returns with the daemon still serving spawns. A pure reaffirmal that
|
|
70
70
|
// activates nothing drains only for its claim and needs no candidate.
|
|
71
71
|
const preflight = daemon.preflightUpdate || preflightUpdate;
|
|
72
|
-
|
|
72
|
+
let candidate = null;
|
|
73
|
+
if (!(reconciling && !coordinate)) {
|
|
74
|
+
candidate = preflight({ version: requestedVersion });
|
|
75
|
+
}
|
|
76
|
+
// A JavaScript-only release ships a native payload byte-identical to the
|
|
77
|
+
// one the running daemon already executes. Draining and restarting the
|
|
78
|
+
// service for it would bounce every live subagent task for nothing, and
|
|
79
|
+
// the health gate would then refuse the "new" payload anyway: the daemon
|
|
80
|
+
// self-reports the version baked into the (unchanged) binary, which can
|
|
81
|
+
// never equal the requested JS-only version. So when the running daemon's
|
|
82
|
+
// self-reported artifact digest matches the candidate's verified daemon
|
|
83
|
+
// digest, this run skips the drain/service-restart entirely: the JS state
|
|
84
|
+
// still activates, host bindings still reconcile, and the service keeps
|
|
85
|
+
// executing the very bytes it already runs. The restart path stays
|
|
86
|
+
// untouched for any real binary change (digest mismatch) and for offline
|
|
87
|
+
// daemons (nothing to compare against; the activation boots the service).
|
|
88
|
+
let jsOnlyRestartSkip = false;
|
|
89
|
+
if (candidate?.daemon?.digest) {
|
|
90
|
+
try {
|
|
91
|
+
const live = await rpc(socket, 'status', {});
|
|
92
|
+
const liveArtifact = live?.identity?.daemon?.artifact;
|
|
93
|
+
if (liveArtifact?.sha256 === candidate.daemon.digest) jsOnlyRestartSkip = true;
|
|
94
|
+
} catch { /* offline or unanswerable daemon: take the normal restart path */ }
|
|
95
|
+
}
|
|
73
96
|
let status = null;
|
|
74
97
|
let online = true;
|
|
98
|
+
if (jsOnlyRestartSkip) {
|
|
99
|
+
online = false;
|
|
100
|
+
status = { ready_for_activation: true, activation_claim: null, js_only_restart_skipped: true };
|
|
101
|
+
} else {
|
|
75
102
|
try {
|
|
76
103
|
status = await rpc(socket, 'drain', cancelActive ? { cancel_active: true } : {});
|
|
77
104
|
} catch (error) {
|
|
@@ -99,6 +126,7 @@ export async function updateCommand(paths, args = [], daemon = {}) {
|
|
|
99
126
|
}
|
|
100
127
|
status = await rpc(socket, 'activate-ready', {});
|
|
101
128
|
}
|
|
129
|
+
}
|
|
102
130
|
const claim = online ? status.activation_claim : `offline-${process.pid}-${Date.now()}-activation`;
|
|
103
131
|
if (!claim) return { ...status, activation_claim: null, update: 'not_activated' };
|
|
104
132
|
if (prior?.status === 'success' && prior.claim === claim
|
|
@@ -123,7 +151,11 @@ export async function updateCommand(paths, args = [], daemon = {}) {
|
|
|
123
151
|
let partialHosts = null;
|
|
124
152
|
try {
|
|
125
153
|
const serviceInstalled = typeof daemon.hasInstalledService === 'function' ? daemon.hasInstalledService(paths) : hasInstalledService(paths);
|
|
126
|
-
|
|
154
|
+
// A JS-only release skips the service restart (see jsOnlyRestartSkip):
|
|
155
|
+
// the running daemon already executes the candidate's exact daemon bytes,
|
|
156
|
+
// so a bootout/bootstrap would only bounce live tasks and then fail the
|
|
157
|
+
// version-bearing health gate against the unchanged binary.
|
|
158
|
+
const serviceDue = serviceInstalled && !daemon.skipServiceActivation && !jsOnlyRestartSkip;
|
|
127
159
|
result = (reconciling && !coordinate)
|
|
128
160
|
? { ...reconcileInstallation(paths, { cancelActive, yes }), homes: reconcileCodexHomes(paths), zcode: reconcileZcodeBinding(paths), dsh: reconcileDshBindings(paths) }
|
|
129
161
|
: await (daemon.updateInstallation || updateInstallation)(paths, { version: requestedVersion, deferCodexSync: serviceDue });
|
|
@@ -163,8 +195,8 @@ export async function updateCommand(paths, args = [], daemon = {}) {
|
|
|
163
195
|
partialHosts = partialHostSync(result);
|
|
164
196
|
atomicWrite(receiptPath(paths), jsonBytes(partialHosts
|
|
165
197
|
? { claim, version: receiptVersion, status: 'partial', retryable: true, homes: result.homes, zcode: result.zcode, result }
|
|
166
|
-
: { claim, version: receiptVersion, status: 'success', result }));
|
|
167
|
-
if (partialHosts === null) return result;
|
|
198
|
+
: { claim, version: receiptVersion, status: 'success', ...(jsOnlyRestartSkip ? { js_only_restart_skipped: true } : {}), result }));
|
|
199
|
+
if (partialHosts === null) return jsOnlyRestartSkip ? { ...result, js_only_restart_skipped: true } : result;
|
|
168
200
|
} catch (error) {
|
|
169
201
|
// Snapshot what the updater recorded before rollback replaces it, so the
|
|
170
202
|
// receipt keeps the candidate and active evidence the retry will need.
|
package/cli/constants.mjs
CHANGED
|
@@ -12,7 +12,7 @@ const IS_DEBUG_VARIANT = process.env.EXTERNAL_SUBAGENT_VARIANT === 'debug';
|
|
|
12
12
|
|
|
13
13
|
export const PRODUCT_NAME = IS_DEBUG_VARIANT ? 'external-subagent-debug' : 'external-subagent';
|
|
14
14
|
export const PRODUCT_ID = IS_DEBUG_VARIANT ? 'external_subagent_debug' : 'external_subagent';
|
|
15
|
-
export const VERSION = '0.5.
|
|
15
|
+
export const VERSION = '0.5.5';
|
|
16
16
|
export const LAUNCH_AGENT_LABEL = IS_DEBUG_VARIANT ? 'com.external-subagent-debug.daemon' : 'com.external-subagent.daemon';
|
|
17
17
|
export const DAEMON_BIN_NAME = IS_DEBUG_VARIANT ? 'external-subagent-debugd' : 'external-subagentd';
|
|
18
18
|
export const MCP_BIN_NAME = IS_DEBUG_VARIANT ? 'external-subagent-debug-mcp' : 'external-subagent-mcp';
|
package/cli/install/dsh.mjs
CHANGED
|
@@ -140,7 +140,7 @@ function managedBlock(binding, socket) {
|
|
|
140
140
|
SENTINEL,
|
|
141
141
|
'- insert:',
|
|
142
142
|
` - id: ${MCP_ENTRY_ID}`,
|
|
143
|
-
`
|
|
143
|
+
` name: ${yamlScalar(MCP_CLIENT_PACKAGE)}`,
|
|
144
144
|
' config:',
|
|
145
145
|
` serverName: ${yamlScalar('external_subagent')}`,
|
|
146
146
|
` transport: ${yamlScalar('stdio')}`,
|
|
@@ -394,15 +394,22 @@ function verifyWithDump(dshHome, profileName, options = {}) {
|
|
|
394
394
|
// composition (an older manual row, another profile's leftovers in a
|
|
395
395
|
// copied fixture) could otherwise satisfy a substring check and mask a
|
|
396
396
|
// silently-dropped managed row. The composed dump re-serializes the
|
|
397
|
-
// entry
|
|
398
|
-
//
|
|
399
|
-
//
|
|
400
|
-
//
|
|
401
|
-
|
|
397
|
+
// entry with its id first (dsh emits `id`, `name`, `config` in key
|
|
398
|
+
// order), so anchor on OUR entry id and require both the plugin name and
|
|
399
|
+
// the serverName in the same mapping block. Anchoring on the id is what
|
|
400
|
+
// makes this structural: a sibling `- id:`/`- name:` regression (two list
|
|
401
|
+
// items instead of one) drops the id from the composed entry, and this
|
|
402
|
+
// check then fails even though a bare `name:` line still exists for some
|
|
403
|
+
// other mcp-client row in the dump. The dump may render the package name
|
|
404
|
+
// quoted or bare depending on the dsh version, so accept both spellings.
|
|
405
|
+
const anchor = out.indexOf(`- id: ${MCP_ENTRY_ID}\n`);
|
|
402
406
|
if (anchor === -1) {
|
|
403
|
-
throw new CliError('DSH_BINDING_UNVERIFIABLE',
|
|
407
|
+
throw new CliError('DSH_BINDING_UNVERIFIABLE', `the managed entry id ${MCP_ENTRY_ID} is absent from the composed profile dump`);
|
|
404
408
|
}
|
|
405
409
|
const window = out.slice(anchor, anchor + 1200);
|
|
410
|
+
if (!window.includes(`name: ${MCP_CLIENT_PACKAGE}`) && !window.includes(`name: '${MCP_CLIENT_PACKAGE}'`)) {
|
|
411
|
+
throw new CliError('DSH_BINDING_UNVERIFIABLE', 'the managed entry is present but does not carry the mcp-client plugin name');
|
|
412
|
+
}
|
|
406
413
|
if (!window.includes('serverName: external_subagent')) {
|
|
407
414
|
throw new CliError('DSH_BINDING_UNVERIFIABLE', 'the managed mcp-client entry is present but its serverName is missing from the composed dump');
|
|
408
415
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "external-subagent",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "Manage bounded ZCode, DeepSeek Harness (DSH), Codex, and Google Antigravity (agy) subagent tasks through a local MCP service for any local host (Codex, ZCode, or a custom MCP client)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|