@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # Halofy agent lifecycle installer
2
2
 
3
- Status: source for `@halofy/agent-connect@0.13.0`; publication and deployment
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.0/bin/halofy-agent.mjs" sync --connection <installation-id>
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.0 is published and
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.0 install claude-code --server https://app.halofy.ai
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.0 is published or enabled in production. The
115
- manual claim path remains supported; after publication, its 0.13.0 form is:
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.0 install <client-kind> \
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 decrypting or
197
- printing any pending event body:
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 source)
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
@@ -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 queue = new BoundedEncryptedQueue(join(defaultRuntimeDirectory(), connection.installationId));
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(`${error?.message || "Halofy MCP proxy unavailable"}\n`);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@halofy/agent-connect",
3
- "version": "0.13.0",
3
+ "version": "0.13.1",
4
4
  "type": "module",
5
5
  "description": "Halofy lifecycle installer and runtime for supported agents; runtime requests are signed with a per-installation Ed25519 key",
6
6
  "bin": {
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 = true;
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(() => runtime.heartbeat(runtime.connection.capabilities || {}))
164
- .catch(() => {}).finally(() => { pending = null; });
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.taskId || input.task_id || input.extra?.turn_id || input.extra?.tool_call_id;
50
- return native ? `${eventName}:${String(native)}` : `${eventName}:${digest(JSON.stringify(input))}`;
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
- if (queued.queued === 0) return { queued: 0, acknowledged: 0 };
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
- return withFileLock(this.operationLockPath, () => this.#replayUnlocked());
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
- while (true) {
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
- const accepted = Math.max(0, Math.min(
248
- head.events.length,
249
- Number(response?.acceptedCount ?? response?.acceptedPrefix ??
250
- (response?.accepted === undefined ? head.events.length : Number(response.accepted) + Number(response.duplicates ?? 0))),
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.0";
3
- export const RUNTIME_VERSION = "0.13.0";
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";