@halofy/agent-connect 0.12.0 → 0.12.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 +23 -10
- package/package.json +1 -1
- package/src/claude-hook.mjs +12 -6
- package/src/host-hook.mjs +4 -7
- package/src/install.mjs +2 -2
- package/src/instructions.mjs +29 -14
- package/src/runtime.mjs +1 -1
- package/src/version.mjs +2 -2
package/README.md
CHANGED
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
# Halofy agent lifecycle installer
|
|
2
2
|
|
|
3
|
-
Status:
|
|
4
|
-
|
|
3
|
+
Status: source for `@halofy/agent-connect@0.12.1`; publication and deployment
|
|
4
|
+
require separate verification. The production console supplies the command for
|
|
5
|
+
the verified published package pinned by the deployment workflow. Host loading
|
|
6
|
+
requires separate verification.
|
|
5
7
|
|
|
6
8
|
This package contains the host-neutral client pieces for installation-bound
|
|
7
9
|
agent connections:
|
|
@@ -22,12 +24,14 @@ conversations and serves the agent-invoked MCP memory tools, but does not push
|
|
|
22
24
|
recalled memory into host sessions on session start or prompt submit. The
|
|
23
25
|
bounded recall block formats stay in place and tested for when it returns.
|
|
24
26
|
|
|
25
|
-
## Browser sign-in
|
|
27
|
+
## Browser sign-in
|
|
26
28
|
|
|
27
|
-
Users assigned to a team can run the installer without a manually copied badge
|
|
29
|
+
Users assigned to a team can run the installer without a manually copied badge.
|
|
30
|
+
Use the production console’s verified command. After 0.12.1 is published and
|
|
31
|
+
verified, the command for this source version is:
|
|
28
32
|
|
|
29
33
|
```bash
|
|
30
|
-
npx @halofy/agent-connect@0.12.
|
|
34
|
+
npx @halofy/agent-connect@0.12.1 install claude-code --server https://app.halofy.ai
|
|
31
35
|
```
|
|
32
36
|
|
|
33
37
|
`login <client-kind> --server <origin>` is an alias for the same installation
|
|
@@ -40,13 +44,12 @@ put in browser URLs. Retry a failed/expired sign-in with the same command; the
|
|
|
40
44
|
pending key survives retries. Installation still requires the terminal's
|
|
41
45
|
explicit CONNECT disclosure before changing host configuration.
|
|
42
46
|
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
manual claim path remains supported:
|
|
46
|
-
|
|
47
|
+
Browser sign-in requires the matching server enrollment endpoints. The example
|
|
48
|
+
above does not establish that 0.12.1 is published or enabled in production. The
|
|
49
|
+
manual claim path remains supported; after publication, its 0.12.1 form is:
|
|
47
50
|
|
|
48
51
|
```bash
|
|
49
|
-
npx --yes @halofy/agent-connect@0.
|
|
52
|
+
npx --yes @halofy/agent-connect@0.12.1 install <client-kind> \
|
|
50
53
|
--server https://app.halofy.ai \
|
|
51
54
|
--claim '<one-time-claim>'
|
|
52
55
|
```
|
|
@@ -220,6 +223,16 @@ memory recall. There is no resident instruction daemon; an offline device or a
|
|
|
220
223
|
host session without an installed startup hook does not fetch updates. Claude's
|
|
221
224
|
existing hook installation is project-scoped even though its rule file is global.
|
|
222
225
|
|
|
226
|
+
Version 0.12.1 gives instruction GET and its best-effort status receipt one shared
|
|
227
|
+
8-second deadline at startup or direct refresh, and 15 seconds during interactive
|
|
228
|
+
installation. A failed GET preserves current instructions; a receipt timeout does
|
|
229
|
+
not undo a completed local update or prove server acknowledgement. Other request
|
|
230
|
+
and host-hook limits are unchanged; this is not a deadline guarantee for the
|
|
231
|
+
whole startup sequence. Startup runs instruction sync alongside skill/reference
|
|
232
|
+
delivery, including while capture is paused, so slow reference downloads or
|
|
233
|
+
receipts do not hold up the instruction fetch. Replay and heartbeat still follow
|
|
234
|
+
both refreshes.
|
|
235
|
+
|
|
223
236
|
| Host | Local target | Boundaries |
|
|
224
237
|
|---|---|---|
|
|
225
238
|
| Codex | `CODEX_HOME` or `~/.codex`, active nonempty `AGENTS.override.md` else `AGENTS.md` | Marked section; changing the active target reports a conflict |
|
package/package.json
CHANGED
package/src/claude-hook.mjs
CHANGED
|
@@ -12,6 +12,15 @@ export async function syncSkillsAtSessionStart(runtime, connection, root, stderr
|
|
|
12
12
|
return result.skills;
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
+
/** Disjoint owned files and locks let instructions refresh without waiting for
|
|
16
|
+
* skill/reference downloads or their receipt. Both settle before capture work. */
|
|
17
|
+
export async function syncConfigurationAtSessionStart(runtime, connection, root, stderr, syncOptions = {}) {
|
|
18
|
+
await Promise.allSettled([
|
|
19
|
+
Promise.resolve().then(() => runtime.syncInstructions?.()),
|
|
20
|
+
syncSkillsAtSessionStart(runtime, connection, root, stderr, syncOptions),
|
|
21
|
+
]);
|
|
22
|
+
}
|
|
23
|
+
|
|
15
24
|
function id(value) {
|
|
16
25
|
return createHash("sha256").update(String(value)).digest("hex");
|
|
17
26
|
}
|
|
@@ -96,9 +105,8 @@ export async function runClaudeLifecycleHook(connection, eventName, {
|
|
|
96
105
|
const runtime = runtimeFactory(connection, { root, captureGeneration: capture.generation });
|
|
97
106
|
if (capture.paused) {
|
|
98
107
|
if (eventName === "SessionStart") {
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
await runtime.syncInstructions?.().catch(() => {});
|
|
108
|
+
// Capture pause does not suspend instruction or reference delivery.
|
|
109
|
+
await syncConfigurationAtSessionStart(runtime, connection, root, stderr, { home, env });
|
|
102
110
|
await runtime.heartbeat(connection.capabilities || {});
|
|
103
111
|
} else if (eventName === "UserPromptSubmit") {
|
|
104
112
|
const delivery = await syncManagedDelivery({ connection, transport: runtime.transport, root, home, env, stderr, phase: "turn" });
|
|
@@ -110,9 +118,7 @@ export async function runClaudeLifecycleHook(connection, eventName, {
|
|
|
110
118
|
|
|
111
119
|
if (eventName === "SessionStart") {
|
|
112
120
|
// Refresh/withdraw local context even when replay or heartbeat fails.
|
|
113
|
-
await
|
|
114
|
-
// Instructions have their own brownout boundary, independent of recall.
|
|
115
|
-
await runtime.syncInstructions?.().catch(() => {});
|
|
121
|
+
await syncConfigurationAtSessionStart(runtime, connection, root, stderr, { home, env });
|
|
116
122
|
await runtime.replay();
|
|
117
123
|
await runtime.heartbeat(connection.capabilities || {});
|
|
118
124
|
if (RECALL_INJECTION_ENABLED) {
|
package/src/host-hook.mjs
CHANGED
|
@@ -9,7 +9,7 @@ import {
|
|
|
9
9
|
rankedRecallBlocks,
|
|
10
10
|
} from "./session.mjs";
|
|
11
11
|
import { defaultRuntimeDirectory } from "./storage.mjs";
|
|
12
|
-
import { readHookInput,
|
|
12
|
+
import { readHookInput, syncConfigurationAtSessionStart } from "./claude-hook.mjs";
|
|
13
13
|
import { syncManagedDelivery } from "./delivery-sync.mjs";
|
|
14
14
|
import { transcriptDriverFor } from "./transcript-drivers/index.mjs";
|
|
15
15
|
|
|
@@ -161,9 +161,8 @@ export async function runHostLifecycleHook(connection, eventName, {
|
|
|
161
161
|
const runtime = runtimeFactory(connection, { root, captureGeneration: capture.generation });
|
|
162
162
|
if (capture.paused) {
|
|
163
163
|
if (START_EVENTS.has(eventName)) {
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
await runtime.syncInstructions?.().catch(() => {});
|
|
164
|
+
// Capture pause does not suspend instruction or reference delivery.
|
|
165
|
+
await syncConfigurationAtSessionStart(runtime, connection, root, stderr, { home, env });
|
|
167
166
|
await runtime.heartbeat(connection.capabilities || {});
|
|
168
167
|
} else if (USER_EVENTS.has(eventName)) {
|
|
169
168
|
const delivery = await syncManagedDelivery({ connection, transport: runtime.transport, root, home, env, stderr, phase: "turn" });
|
|
@@ -175,9 +174,7 @@ export async function runHostLifecycleHook(connection, eventName, {
|
|
|
175
174
|
|
|
176
175
|
if (START_EVENTS.has(eventName)) {
|
|
177
176
|
// Refresh/withdraw local context even when replay or heartbeat fails.
|
|
178
|
-
await
|
|
179
|
-
// Instructions have their own brownout boundary, independent of recall.
|
|
180
|
-
await runtime.syncInstructions?.().catch(() => {});
|
|
177
|
+
await syncConfigurationAtSessionStart(runtime, connection, root, stderr, { home, env });
|
|
181
178
|
await runtime.replay();
|
|
182
179
|
await runtime.heartbeat(connection.capabilities || {});
|
|
183
180
|
if (RECALL_INJECTION_ENABLED) {
|
package/src/install.mjs
CHANGED
|
@@ -4,7 +4,7 @@ import { chmod, cp, rename, rm, unlink } from "node:fs/promises";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { generateInstallationKeyPair } from "./crypto.mjs";
|
|
6
6
|
import { ConnectionStore, defaultRuntimeDirectory, ensurePrivateDirectory, readJson, writePrivateFile } from "./storage.mjs";
|
|
7
|
-
import { syncAgentInstructions } from "./instructions.mjs";
|
|
7
|
+
import { INSTALL_INSTRUCTION_SYNC_TIMEOUT_MS, syncAgentInstructions } from "./instructions.mjs";
|
|
8
8
|
import { SignedRuntimeTransport } from "./transport.mjs";
|
|
9
9
|
import { inspectCaptureHealth } from "./health.mjs";
|
|
10
10
|
import { INSTALLER_VERSION, RUNTIME_VERSION } from "./version.mjs";
|
|
@@ -232,7 +232,7 @@ export async function installLocalConnection({
|
|
|
232
232
|
}
|
|
233
233
|
}
|
|
234
234
|
const instructions = instructionProfile
|
|
235
|
-
? await syncAgentInstructions(connection, { root,
|
|
235
|
+
? await syncAgentInstructions(connection, { root, fetchImpl, timeoutMs: INSTALL_INSTRUCTION_SYNC_TIMEOUT_MS })
|
|
236
236
|
: undefined;
|
|
237
237
|
return {
|
|
238
238
|
installationId,
|
package/src/instructions.mjs
CHANGED
|
@@ -7,6 +7,9 @@ import { isActiveInstallation } from "./active.mjs";
|
|
|
7
7
|
import { ConnectionStore, readJson } from "./storage.mjs";
|
|
8
8
|
import { SignedRuntimeTransport } from "./transport.mjs";
|
|
9
9
|
|
|
10
|
+
export const INSTRUCTION_SYNC_TIMEOUT_MS = 8_000;
|
|
11
|
+
export const INSTALL_INSTRUCTION_SYNC_TIMEOUT_MS = 15_000;
|
|
12
|
+
|
|
10
13
|
const sha = (value) => createHash("sha256").update(value).digest("hex");
|
|
11
14
|
const HEX = /^[a-f0-9]{64}$/;
|
|
12
15
|
const MARKER = "<!-- HALOFY-INSTRUCTIONS:";
|
|
@@ -247,20 +250,32 @@ async function retirePredecessorInstructions(connection, root) {
|
|
|
247
250
|
}
|
|
248
251
|
}
|
|
249
252
|
|
|
250
|
-
/** Separate best-effort operation: a failed GET never means remove instructions.
|
|
251
|
-
|
|
253
|
+
/** Separate best-effort operation: a failed GET never means remove instructions.
|
|
254
|
+
* GET and receipt share one deadline so a slow receipt cannot double startup delay.
|
|
255
|
+
* An explicitly injected transport retains its own cancellation policy. */
|
|
256
|
+
export async function syncAgentInstructions(connection, {
|
|
257
|
+
root, transport, fetchImpl = globalThis.fetch, timeoutMs = INSTRUCTION_SYNC_TIMEOUT_MS,
|
|
258
|
+
} = {}) {
|
|
259
|
+
const controller = new AbortController();
|
|
260
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
261
|
+
const activeTransport = transport ?? new SignedRuntimeTransport(connection, {
|
|
262
|
+
fetchImpl, timeoutMs, signal: controller.signal,
|
|
263
|
+
});
|
|
252
264
|
let receipt = { digest: sha("[]"), status: "unavailable", reason: "fetch_unavailable" };
|
|
253
265
|
try {
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
+
try {
|
|
267
|
+
if (!(await isActiveInstallation(connection, root))) return { ...receipt, status: "unsupported", reason: "inactive_installation" };
|
|
268
|
+
await retirePredecessorInstructions(connection, root);
|
|
269
|
+
const bundle = await activeTransport.instructions();
|
|
270
|
+
if (!transport) controller.signal.throwIfAborted();
|
|
271
|
+
receipt.digest = typeof bundle?.digest === "string" && HEX.test(bundle.digest) ? bundle.digest : receipt.digest;
|
|
272
|
+
receipt = await applyInstructionBundle(connection, bundle, { root, canActivate: () => isActiveInstallation(connection, root) });
|
|
273
|
+
} catch (error) {
|
|
274
|
+
const reason = error.instructionCode;
|
|
275
|
+
const unsupported = ["custom_context_unsupported", "reinstall_required", "inactive_installation"].includes(reason);
|
|
276
|
+
receipt = { ...receipt, status: unsupported ? "unsupported" : reason && reason !== "invalid_bundle" ? "conflict" : "unavailable", reason: reason || "fetch_unavailable" };
|
|
277
|
+
}
|
|
278
|
+
try { await activeTransport.instructionStatus(receipt); } catch { /* A receipt failure cannot disable capture/MCP. */ }
|
|
279
|
+
return receipt;
|
|
280
|
+
} finally { clearTimeout(timer); }
|
|
266
281
|
}
|
package/src/runtime.mjs
CHANGED
|
@@ -16,7 +16,7 @@ export class LifecycleRuntime {
|
|
|
16
16
|
constructor(connection, {
|
|
17
17
|
root,
|
|
18
18
|
transport = new SignedRuntimeTransport(connection),
|
|
19
|
-
instructionTransport
|
|
19
|
+
instructionTransport,
|
|
20
20
|
maxBatchEvents = 100,
|
|
21
21
|
maxBatchBytes = 1024 * 1024,
|
|
22
22
|
commitThreshold = 20,
|
package/src/version.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
export const PACKAGE_NAME = "@halofy/agent-connect";
|
|
2
|
-
export const INSTALLER_VERSION = "0.12.
|
|
3
|
-
export const RUNTIME_VERSION = "0.12.
|
|
2
|
+
export const INSTALLER_VERSION = "0.12.1";
|
|
3
|
+
export const RUNTIME_VERSION = "0.12.1";
|
|
4
4
|
export const DISCLOSURE_VERSION = "halofy-agent-lifecycle-2026-09-10.1";
|