@halofy/agent-connect 0.13.0 → 0.13.1
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.md +46 -10
- package/bin/halofy-agent.mjs +13 -3
- package/package.json +1 -1
- package/src/health.mjs +11 -4
- package/src/host-hook.mjs +6 -8
- package/src/runtime.mjs +33 -22
- package/src/version.mjs +2 -2
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# Halofy agent lifecycle installer
|
|
2
2
|
|
|
3
|
-
Status: source for `@halofy/agent-connect@0.13.
|
|
3
|
+
Status: source for `@halofy/agent-connect@0.13.1`; publication and deployment
|
|
4
4
|
require separate verification. The production console supplies the command for
|
|
5
5
|
the verified published package pinned by the deployment workflow. Host loading
|
|
6
6
|
requires separate verification.
|
|
@@ -33,7 +33,7 @@ conversation coverage from the presence of `hooks.json`. This source adds an
|
|
|
33
33
|
explicit refresh command for an existing installation:
|
|
34
34
|
|
|
35
35
|
```bash
|
|
36
|
-
node "/absolute/path/to/runtime/0.13.
|
|
36
|
+
node "/absolute/path/to/runtime/0.13.1/bin/halofy-agent.mjs" sync --connection <installation-id>
|
|
37
37
|
```
|
|
38
38
|
|
|
39
39
|
The installer prints `syncCommand` with the exact executable, argument array
|
|
@@ -93,11 +93,11 @@ runtimes or their managed copies.
|
|
|
93
93
|
## Browser sign-in
|
|
94
94
|
|
|
95
95
|
Users assigned to a team can run the installer without a manually copied badge.
|
|
96
|
-
Use the production console’s verified command. After 0.13.
|
|
96
|
+
Use the production console’s verified command. After 0.13.1 is published and
|
|
97
97
|
verified, the command for this source version is:
|
|
98
98
|
|
|
99
99
|
```bash
|
|
100
|
-
npx @halofy/agent-connect@0.13.
|
|
100
|
+
npx @halofy/agent-connect@0.13.1 install claude-code --server https://app.halofy.ai
|
|
101
101
|
```
|
|
102
102
|
|
|
103
103
|
`login <client-kind> --server <origin>` is an alias for the same installation
|
|
@@ -111,11 +111,11 @@ pending key survives retries. Installation still requires the terminal's
|
|
|
111
111
|
explicit CONNECT disclosure before changing host configuration.
|
|
112
112
|
|
|
113
113
|
Browser sign-in requires the matching server enrollment endpoints. The example
|
|
114
|
-
above does not establish that 0.13.
|
|
115
|
-
manual claim path remains supported; after publication, its 0.13.
|
|
114
|
+
above does not establish that 0.13.1 is published or enabled in production. The
|
|
115
|
+
manual claim path remains supported; after publication, its 0.13.1 form is:
|
|
116
116
|
|
|
117
117
|
```bash
|
|
118
|
-
npx --yes @halofy/agent-connect@0.13.
|
|
118
|
+
npx --yes @halofy/agent-connect@0.13.1 install <client-kind> \
|
|
119
119
|
--server https://app.halofy.ai \
|
|
120
120
|
--claim '<one-time-claim>'
|
|
121
121
|
```
|
|
@@ -193,13 +193,49 @@ npm test
|
|
|
193
193
|
npm run check
|
|
194
194
|
```
|
|
195
195
|
|
|
196
|
-
Content-free local queue/version evidence is available without
|
|
197
|
-
|
|
196
|
+
Content-free local queue/version evidence is available without printing any
|
|
197
|
+
pending event body or proof key:
|
|
198
198
|
|
|
199
199
|
```bash
|
|
200
200
|
node kernel/integrations/agent-runtime/bin/halofy-agent.mjs diagnostics
|
|
201
201
|
```
|
|
202
202
|
|
|
203
|
+
Pass `--connection <installation-id>` to inspect a specific installation.
|
|
204
|
+
Diagnostics shows installed and running runtime versions, local hook/pause
|
|
205
|
+
health, frozen capture capabilities, queue depth, oldest pending time and expired
|
|
206
|
+
batch count. It makes no server request, and neither active local hooks nor an
|
|
207
|
+
empty queue proves that a particular conversation reached the archive.
|
|
208
|
+
|
|
209
|
+
## Conversation return reliability (0.13.1 source)
|
|
210
|
+
|
|
211
|
+
Append acknowledgement is now mandatory and bounded to the submitted batch.
|
|
212
|
+
An empty, non-JSON or malformed success response cannot discard queued events.
|
|
213
|
+
Only explicitly accepted or duplicate events advance the cursor; a server
|
|
214
|
+
sequence ahead of an event conflict cannot acknowledge the rejected local body.
|
|
215
|
+
An unchanged transcript retries its already durable queue, including after a
|
|
216
|
+
lost append response, without requiring another user message. Hook messages
|
|
217
|
+
without a native event id receive an id per invocation, so multiple prompts in
|
|
218
|
+
one Cline task and repeated identical prompts remain separate events. Queue
|
|
219
|
+
retries preserve that id; native event ids still deduplicate host retries.
|
|
220
|
+
|
|
221
|
+
Retries run at supported hooks, explicit runtime replay and the existing MCP
|
|
222
|
+
proxy's 60-second health tick. Each background tick attempts at most one pending
|
|
223
|
+
batch, with the proxy's five-second request timeout and cancellation on exit.
|
|
224
|
+
Heartbeat reporting continues when replay fails. Paused or replaced installations
|
|
225
|
+
skip background replay; resume permits queued work to retry. The pause result
|
|
226
|
+
reports an incomplete flush while any unacknowledged batch remains. No new monitoring
|
|
227
|
+
process is installed. Abrupt host shutdown can leave pending data until the next
|
|
228
|
+
proxy start or supported hook, subject to the existing seven-day queue limit.
|
|
229
|
+
This release does not widen host capabilities or add assistant
|
|
230
|
+
responses to the partial adapters listed above. Hosts without native event ids
|
|
231
|
+
cannot distinguish two host invocations from a duplicated host callback.
|
|
232
|
+
|
|
233
|
+
The npm `0.12.1` tarball was downloaded and matched the deployment workflow's
|
|
234
|
+
SHA-256 pin on 2026-09-17. These `0.13.1` fixes require a new reviewed publication,
|
|
235
|
+
server pin promotion and a recipient-confirmed reinstall. Existing runtimes do
|
|
236
|
+
not auto-upgrade. Public health responses and package integrity do not verify
|
|
237
|
+
authenticated production conversation capture.
|
|
238
|
+
|
|
203
239
|
Version 0.8.0 additionally refreshes authorized organization and team policy and
|
|
204
240
|
knowledge-base references at installation and SessionStart for the seven pre-Hermes hosts
|
|
205
241
|
above. Hermes does not support this managed delivery. Each installation gets a private `halofy-context-*` skill folder containing
|
|
@@ -425,7 +461,7 @@ in status receipts. Existing policy/knowledge/skill operations and runtime queue
|
|
|
425
461
|
files are not changed by instruction sync.
|
|
426
462
|
|
|
427
463
|
|
|
428
|
-
## Hermes pinned adapter (0.13.0
|
|
464
|
+
## Hermes pinned adapter (since 0.13.0)
|
|
429
465
|
|
|
430
466
|
This source supports [Hermes Agent v2026.9.14](https://github.com/NousResearch/hermes-agent/releases/tag/v2026.9.14),
|
|
431
467
|
package version `0.21.3`, upstream commit
|
package/bin/halofy-agent.mjs
CHANGED
|
@@ -8,8 +8,9 @@ import { runClaudeLifecycleHook } from "../src/claude-hook.mjs";
|
|
|
8
8
|
import { HOST_HOOK_EVENTS, runHostLifecycleHook } from "../src/host-hook.mjs";
|
|
9
9
|
import { HERMES_HOOK_EVENTS, runHermesLifecycleHook } from "../src/hermes-hook.mjs";
|
|
10
10
|
import { LifecycleRuntime } from "../src/runtime.mjs";
|
|
11
|
-
import { setCapturePaused } from "../src/health.mjs";
|
|
11
|
+
import { inspectCaptureHealth, setCapturePaused } from "../src/health.mjs";
|
|
12
12
|
import { refreshManagedConfiguration } from "../src/delivery-sync.mjs";
|
|
13
|
+
import { RUNTIME_VERSION } from "../src/version.mjs";
|
|
13
14
|
import { join, resolve } from "node:path";
|
|
14
15
|
import { realpath } from "node:fs/promises";
|
|
15
16
|
import { readJson, safeDirectory } from "../src/storage.mjs";
|
|
@@ -113,19 +114,28 @@ if (!["mcp", "hermes-mcp", "diagnostics", "hook", "hermes-hook", "pause", "resum
|
|
|
113
114
|
process.exit(0);
|
|
114
115
|
}
|
|
115
116
|
} else {
|
|
116
|
-
const
|
|
117
|
+
const root = defaultRuntimeDirectory();
|
|
118
|
+
const queue = new BoundedEncryptedQueue(join(root, connection.installationId));
|
|
117
119
|
process.stdout.write(`${JSON.stringify({
|
|
118
120
|
installationId: connection.installationId,
|
|
119
121
|
clientKind: connection.clientKind,
|
|
120
122
|
protocolVersion: connection.protocolVersion,
|
|
121
123
|
pluginVersion: connection.pluginVersion,
|
|
124
|
+
runtimeVersion: RUNTIME_VERSION,
|
|
122
125
|
proofStorage: connection.proofStorage,
|
|
126
|
+
health: await inspectCaptureHealth(connection, root),
|
|
127
|
+
declaredCapabilities: Object.fromEntries([
|
|
128
|
+
"userMessages", "assistantMessages", "toolInputs", "toolOutputs", "toolFailures",
|
|
129
|
+
"sessionEnd", "tokenUsage", "sessionMetadata", "images", "artifactBodies",
|
|
130
|
+
].map((name) => [name, connection.capabilities?.[name] === true])),
|
|
131
|
+
serverCheck: "not_performed",
|
|
123
132
|
queue: await queue.diagnostics(),
|
|
124
133
|
}, null, 2)}\n`);
|
|
125
134
|
}
|
|
126
135
|
} catch (error) {
|
|
127
136
|
if (hookEvent === "PreToolUse") process.stdout.write(`${JSON.stringify(skillInvocationDenial())}\n`);
|
|
128
|
-
process.stderr.write(`${
|
|
137
|
+
process.stderr.write(`${command === "diagnostics" ? "Halofy diagnostics unavailable" :
|
|
138
|
+
error?.message || "Halofy MCP proxy unavailable"}\n`);
|
|
129
139
|
process.exitCode = hookEvent === "PreToolUse" ? 0 : 1;
|
|
130
140
|
}
|
|
131
141
|
}
|
package/package.json
CHANGED
package/src/health.mjs
CHANGED
|
@@ -140,8 +140,8 @@ export async function setCapturePaused(runtime, root, paused, { flushTimeoutMs =
|
|
|
140
140
|
fetchImpl: runtime.transport.fetch, signal: controller.signal, timeoutMs: flushTimeoutMs,
|
|
141
141
|
}),
|
|
142
142
|
});
|
|
143
|
-
await flushRuntime.replay();
|
|
144
|
-
flushed =
|
|
143
|
+
const replay = await flushRuntime.replay();
|
|
144
|
+
flushed = replay.depth === 0;
|
|
145
145
|
} catch { flushed = false; }
|
|
146
146
|
finally { clearTimeout(timer); }
|
|
147
147
|
}
|
|
@@ -160,8 +160,15 @@ export function startHealthHeartbeat(runtime, {
|
|
|
160
160
|
let pending = null;
|
|
161
161
|
const tick = () => {
|
|
162
162
|
if (stopped || pending) return pending;
|
|
163
|
-
pending = Promise.resolve().then(() =>
|
|
164
|
-
|
|
163
|
+
pending = Promise.resolve().then(async () => {
|
|
164
|
+
// Report health independently: a failed append must not hide a running
|
|
165
|
+
// installation. The existing proxy transport bounds each request to 5s
|
|
166
|
+
// and aborts on exit; one batch needs at most one open and one append.
|
|
167
|
+
try { await runtime.heartbeat(runtime.connection.capabilities || {}); } catch { /* next tick retries */ }
|
|
168
|
+
if (!stopped) {
|
|
169
|
+
try { await runtime.replay?.({ maxBatches: 1, onlyWhileActive: true }); } catch { /* keep the durable queue */ }
|
|
170
|
+
}
|
|
171
|
+
}).finally(() => { pending = null; });
|
|
165
172
|
return pending;
|
|
166
173
|
};
|
|
167
174
|
void tick();
|
package/src/host-hook.mjs
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import { captureStateSnapshot } from "./health.mjs";
|
|
2
|
-
import { createHash } from "node:crypto";
|
|
3
2
|
import { basename } from "node:path";
|
|
4
|
-
import { LifecycleRuntime } from "./runtime.mjs";
|
|
3
|
+
import { LifecycleRuntime, newLocalEventId } from "./runtime.mjs";
|
|
5
4
|
import {
|
|
6
5
|
normalizeClaudeHookEvent,
|
|
7
6
|
normalizeHostMessageEvent,
|
|
@@ -29,10 +28,6 @@ export const HOST_HOOK_EVENTS = Object.freeze(new Set([
|
|
|
29
28
|
...SUBAGENT_START_EVENTS, ...SUBAGENT_STOP_EVENTS,
|
|
30
29
|
]));
|
|
31
30
|
|
|
32
|
-
function digest(value) {
|
|
33
|
-
return createHash("sha256").update(String(value)).digest("hex");
|
|
34
|
-
}
|
|
35
|
-
|
|
36
31
|
function hostSession(input) {
|
|
37
32
|
return String(input.session_id || input.conversation_id || input.taskId || input.task_id ||
|
|
38
33
|
input.sessionKey || input.session_key || input.extra?.session_id || "");
|
|
@@ -46,8 +41,11 @@ function childSession(input, parent) {
|
|
|
46
41
|
|
|
47
42
|
function eventEvidenceId(eventName, input) {
|
|
48
43
|
const native = input.event_id || input.generation_id || input.turn_id || input.tool_call_id ||
|
|
49
|
-
input.
|
|
50
|
-
|
|
44
|
+
input.extra?.turn_id || input.extra?.tool_call_id;
|
|
45
|
+
// A task id identifies a session, and identical text can be a new turn.
|
|
46
|
+
// Without native event identity, assign one id per hook invocation. The
|
|
47
|
+
// encrypted queue retains that id across all transport retries.
|
|
48
|
+
return native ? `${eventName}:${String(native)}` : `${eventName}:${newLocalEventId()}`;
|
|
51
49
|
}
|
|
52
50
|
|
|
53
51
|
function promptText(input) {
|
package/src/runtime.mjs
CHANGED
|
@@ -11,6 +11,21 @@ import { readJson, withFileLock, writePrivateFile } from "./storage.mjs";
|
|
|
11
11
|
import { capturePaused, captureStateSnapshot, captureAdmission, inspectCaptureHealth } from "./health.mjs";
|
|
12
12
|
import { syncAgentInstructions } from "./instructions.mjs";
|
|
13
13
|
import { RUNTIME_VERSION } from "./version.mjs";
|
|
14
|
+
import { isActiveInstallation } from "./active.mjs";
|
|
15
|
+
|
|
16
|
+
function acknowledgedPrefix(response, batchLength) {
|
|
17
|
+
const accepted = response?.acceptedCount !== undefined ? response.acceptedCount
|
|
18
|
+
: response?.acceptedPrefix !== undefined ? response.acceptedPrefix : response?.accepted;
|
|
19
|
+
const duplicates = response?.acceptedCount === undefined && response?.acceptedPrefix === undefined
|
|
20
|
+
? (response?.duplicates ?? 0) : 0;
|
|
21
|
+
if (!Number.isSafeInteger(accepted) || accepted < 0 ||
|
|
22
|
+
!Number.isSafeInteger(duplicates) || duplicates < 0 || accepted + duplicates > batchLength) {
|
|
23
|
+
const error = new Error("append response did not acknowledge a valid event prefix");
|
|
24
|
+
error.code = "invalid_append_acknowledgement";
|
|
25
|
+
throw error;
|
|
26
|
+
}
|
|
27
|
+
return accepted + duplicates;
|
|
28
|
+
}
|
|
14
29
|
|
|
15
30
|
export class LifecycleRuntime {
|
|
16
31
|
constructor(connection, {
|
|
@@ -177,7 +192,8 @@ export class LifecycleRuntime {
|
|
|
177
192
|
if (usageGaps > 0) await this.cursors.bumpUsageGaps(usageGaps);
|
|
178
193
|
return result;
|
|
179
194
|
});
|
|
180
|
-
|
|
195
|
+
// A previous hook may have persisted this entire suffix before its append
|
|
196
|
+
// failed. Retry that durable queue even when there are no new host records.
|
|
181
197
|
const replay = await this.replay();
|
|
182
198
|
return { queued: queued.queued, acknowledged: replay.acknowledged };
|
|
183
199
|
}
|
|
@@ -228,13 +244,20 @@ export class LifecycleRuntime {
|
|
|
228
244
|
await this.cursors.rememberEventKeys(sessionHash, unseenEvents.map((event) => event.eventKey));
|
|
229
245
|
}
|
|
230
246
|
|
|
231
|
-
async replay() {
|
|
232
|
-
|
|
247
|
+
async replay({ maxBatches = Number.MAX_SAFE_INTEGER, onlyWhileActive = false } = {}) {
|
|
248
|
+
if (!Number.isSafeInteger(maxBatches) || maxBatches < 1) throw new Error("invalid replay batch limit");
|
|
249
|
+
return withFileLock(this.operationLockPath, async () => {
|
|
250
|
+
if (onlyWhileActive && (await capturePaused(this.root, this.connection.installationId) ||
|
|
251
|
+
!await isActiveInstallation(this.connection, this.root))) {
|
|
252
|
+
return { acknowledged: 0, ...(await this.queue.diagnostics()) };
|
|
253
|
+
}
|
|
254
|
+
return this.#replayUnlocked(maxBatches);
|
|
255
|
+
});
|
|
233
256
|
}
|
|
234
257
|
|
|
235
|
-
async #replayUnlocked() {
|
|
258
|
+
async #replayUnlocked(maxBatches = Number.MAX_SAFE_INTEGER) {
|
|
236
259
|
let acknowledged = 0;
|
|
237
|
-
|
|
260
|
+
for (let batch = 0; batch < maxBatches; batch += 1) {
|
|
238
261
|
const head = await this.queue.peek();
|
|
239
262
|
if (!head) break;
|
|
240
263
|
const sessionId = await this.resolveSessionByHash(head.sessionHash);
|
|
@@ -244,23 +267,11 @@ export class LifecycleRuntime {
|
|
|
244
267
|
payloadSha256,
|
|
245
268
|
...event
|
|
246
269
|
}) => ({ ...event, eventId: eventKey })));
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
)
|
|
252
|
-
if (accepted === 0) {
|
|
253
|
-
if (Number.isSafeInteger(response?.nextExpectedSequence)) {
|
|
254
|
-
const expected = response.nextExpectedSequence;
|
|
255
|
-
const firstExpected = head.events.findIndex((event) => event.sequence >= expected);
|
|
256
|
-
if (firstExpected > 0) {
|
|
257
|
-
await this.#acknowledge(head, firstExpected);
|
|
258
|
-
acknowledged += firstExpected;
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
break;
|
|
263
|
-
}
|
|
270
|
+
// A success HTTP status or an advanced server cursor is insufficient:
|
|
271
|
+
// the prefix must be explicitly accepted or confirmed as duplicates.
|
|
272
|
+
// In particular, event_conflict must retain the rejected local bodies.
|
|
273
|
+
const accepted = acknowledgedPrefix(response, head.events.length);
|
|
274
|
+
if (accepted === 0) break;
|
|
264
275
|
await this.#acknowledge(head, accepted);
|
|
265
276
|
acknowledged += accepted;
|
|
266
277
|
}
|
package/src/version.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export const PACKAGE_NAME = "@halofy/agent-connect";
|
|
2
|
-
export const INSTALLER_VERSION = "0.13.
|
|
3
|
-
export const RUNTIME_VERSION = "0.13.
|
|
2
|
+
export const INSTALLER_VERSION = "0.13.1";
|
|
3
|
+
export const RUNTIME_VERSION = "0.13.1";
|
|
4
4
|
export const DISCLOSURE_VERSION = "halofy-agent-lifecycle-2026-09-17.1";
|