@sigrun/memory-honcho 0.1.0 → 0.1.2
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/CHANGELOG.md +43 -0
- package/README.md +1 -0
- package/dist/delivery.d.ts +10 -1
- package/dist/delivery.js +27 -15
- package/dist/index.d.ts +16 -0
- package/dist/index.js +53 -28
- package/dist/tools.js +17 -13
- package/package.json +2 -2
package/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,49 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
6
|
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
7
|
|
|
8
|
+
## [0.1.2] - 2026-11-03
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- Worker `endedAt` bookkeeping: the `engineClient === undefined` early
|
|
13
|
+
return inside the refresh gate skipped `status.endedAt`, leaving the
|
|
14
|
+
status route with a started-but-never-ended run. The gate now falls
|
|
15
|
+
through to the shared `endedAt` write.
|
|
16
|
+
- Worker `lastError` writer cap: tested (the 500-char slice in the
|
|
17
|
+
catch block was untested).
|
|
18
|
+
- Dead `#settings` field on `DeliveryEngine` removed (never read; the
|
|
19
|
+
engine resolves settings live per call site).
|
|
20
|
+
- `honchoError` no longer nests the extension prefix on internal guard
|
|
21
|
+
errors (e.g. "client not initialized" already carries it).
|
|
22
|
+
- Duplicate client-undefined guards in `honcho_chat` and
|
|
23
|
+
`honcho_remember` extracted to a shared `clientOrThrow` helper.
|
|
24
|
+
- Missing-API-key warn-once flag is factory-scoped (was module-global,
|
|
25
|
+
which made test order observable).
|
|
26
|
+
- Delivery-kick catch log aligned with the sweeper's per-conversation
|
|
27
|
+
catch shape (same prefix, conversation id, error).
|
|
28
|
+
|
|
29
|
+
### Added
|
|
30
|
+
|
|
31
|
+
- `makeLeadingEdgeHook` exported from `src/index.ts` — the composition
|
|
32
|
+
test uses the factory instead of re-implementing the hook body
|
|
33
|
+
verbatim (drift risk).
|
|
34
|
+
- README limitation: the `honcho-representations` worker can be frozen
|
|
35
|
+
on Windows (observed in homelab deployments; the leading-edge refresh
|
|
36
|
+
and the sweep job keep the cache fresh regardless).
|
|
37
|
+
- Code comment documenting the accepted duplicate-refresh window on
|
|
38
|
+
overlapping deliveries (bounded by the per-conversation chain,
|
|
39
|
+
last-write-wins, one extra refresh per conversation lifetime).
|
|
40
|
+
|
|
41
|
+
## [0.1.1] - 2026-10-31
|
|
42
|
+
|
|
43
|
+
### Fixed
|
|
44
|
+
|
|
45
|
+
- `establish` adds session peers via the SDK's tuple form
|
|
46
|
+
(`[[peerId, {observeMe, observeOthers}]]`) instead of the documented
|
|
47
|
+
map form — the SDK's client-side zod union rejects the map form, so
|
|
48
|
+
every first-capture establishment failed against a real server
|
|
49
|
+
(verified live against honcho v3.0.9).
|
|
50
|
+
|
|
8
51
|
## [0.1.0] - 2026-10-31
|
|
9
52
|
### Added
|
|
10
53
|
- Honcho persistent-memory extension for sigrun (pi-durable). Every
|
package/README.md
CHANGED
|
@@ -91,6 +91,7 @@ A sigrun fork writes a durable pending-fork record (zero network); the child's f
|
|
|
91
91
|
- **Queue-overflow capture loss (accepted, host posture):** under host event-queue overflow the oldest queued event is dropped — a dropped settle is a run the extension never sees, so its capture batch is permanently lost (the outbox cannot recover an event it never received).
|
|
92
92
|
- **Clone duplicate window (accepted):** a clone that succeeds server-side but crashes before the ledger write retries into a duplicate full-history session — rare, bounded to one duplicate per crash, and non-corrupting (the ledger points at exactly one session); the status route's session count makes duplicates operator-visible.
|
|
93
93
|
- **Jobs-posture caveat:** a task-action job whose run dies un-settled (harness close mid-run) leaves the host's job-run record `running` until restart — the host has no staleness sweep; this extension's sweeper is exposed to it like every task-action job.
|
|
94
|
+
- **Windows: the `honcho-representations` worker can be frozen.** On Windows, the host's worker scheduling can leave a wake-only worker idle even when conversations are idle/archived (observed in homelab deployments; mechanism not yet root-caused). The representation cache stays fresh through the leading-edge refresh at delivery and the `outbox-sweep` job's deliveries — the worker's idle-wake refresh is a convenience, not the only lane.
|
|
94
95
|
|
|
95
96
|
## Development
|
|
96
97
|
|
package/dist/delivery.d.ts
CHANGED
|
@@ -15,6 +15,15 @@
|
|
|
15
15
|
* also updates the `state/last-delivery.json` marker in the same critical
|
|
16
16
|
* section (the status route's newest-delivery read survives the sweeper's
|
|
17
17
|
* delivered-record drop that way).
|
|
18
|
+
*
|
|
19
|
+
* **Accepted duplicate-refresh window (stays):** two deliveries in quick
|
|
20
|
+
* succession (e.g. a capture kick + the sweeper) can each trigger a
|
|
21
|
+
* leading-edge representation refresh when the cache is absent — the
|
|
22
|
+
* check-then-refresh in `onDelivered` is not atomic with the delivery.
|
|
23
|
+
* Bounded by the per-conversation chain (refreshes queue behind each
|
|
24
|
+
* other, never overlap); last-write-wins is safe because the refresh
|
|
25
|
+
* is idempotent (same session, same server-side state). The window is
|
|
26
|
+
* one extra refresh per conversation lifetime, at first delivery only.
|
|
18
27
|
*/
|
|
19
28
|
import type { HonchoSettings } from "./config.js";
|
|
20
29
|
import type { HonchoClient } from "./honcho.js";
|
|
@@ -31,7 +40,7 @@ export interface DeliveryHooks {
|
|
|
31
40
|
}
|
|
32
41
|
export declare class DeliveryEngine {
|
|
33
42
|
#private;
|
|
34
|
-
constructor(client: HonchoClient, stores: ExtensionStores,
|
|
43
|
+
constructor(client: HonchoClient, stores: ExtensionStores, _settings: HonchoSettings, hooks?: DeliveryHooks);
|
|
35
44
|
/**
|
|
36
45
|
* Establish the conversation's Honcho session: plain get-or-create, or
|
|
37
46
|
* a clone-with-mapping when a pending-fork record points at a parent
|
package/dist/delivery.js
CHANGED
|
@@ -15,6 +15,15 @@
|
|
|
15
15
|
* also updates the `state/last-delivery.json` marker in the same critical
|
|
16
16
|
* section (the status route's newest-delivery read survives the sweeper's
|
|
17
17
|
* delivered-record drop that way).
|
|
18
|
+
*
|
|
19
|
+
* **Accepted duplicate-refresh window (stays):** two deliveries in quick
|
|
20
|
+
* succession (e.g. a capture kick + the sweeper) can each trigger a
|
|
21
|
+
* leading-edge representation refresh when the cache is absent — the
|
|
22
|
+
* check-then-refresh in `onDelivered` is not atomic with the delivery.
|
|
23
|
+
* Bounded by the per-conversation chain (refreshes queue behind each
|
|
24
|
+
* other, never overlap); last-write-wins is safe because the refresh
|
|
25
|
+
* is idempotent (same session, same server-side state). The window is
|
|
26
|
+
* one extra refresh per conversation lifetime, at first delivery only.
|
|
18
27
|
*/
|
|
19
28
|
import { pickCloneAnchor } from "./fork-rules.js";
|
|
20
29
|
/** The session configuration applied on get-or-create AND on clones. */
|
|
@@ -25,12 +34,14 @@ const SESSION_CONFIGURATION = {
|
|
|
25
34
|
export class DeliveryEngine {
|
|
26
35
|
#client;
|
|
27
36
|
#stores;
|
|
28
|
-
#settings;
|
|
29
37
|
#hooks;
|
|
30
|
-
constructor(client, stores,
|
|
38
|
+
constructor(client, stores,
|
|
39
|
+
// The settings parameter is accepted but not stored: every value the
|
|
40
|
+
// engine needs is resolved live at the call site (the factory's
|
|
41
|
+
// live-settings seam), so a stale snapshot would be a bug source.
|
|
42
|
+
_settings, hooks = {}) {
|
|
31
43
|
this.#client = client;
|
|
32
44
|
this.#stores = stores;
|
|
33
|
-
this.#settings = settings;
|
|
34
45
|
this.#hooks = hooks;
|
|
35
46
|
}
|
|
36
47
|
/**
|
|
@@ -227,24 +238,25 @@ export class DeliveryEngine {
|
|
|
227
238
|
void this.establish(conversationId, owner)
|
|
228
239
|
.then(() => this.deliverPending(conversationId, owner))
|
|
229
240
|
.catch((error) => {
|
|
230
|
-
|
|
241
|
+
// Same log shape as the sweeper's per-conversation catches —
|
|
242
|
+
// one prefix, conversation id, then the error.
|
|
243
|
+
console.error(`memory-honcho: delivery failed for conversation ${conversationId}`, error);
|
|
231
244
|
});
|
|
232
245
|
}
|
|
233
|
-
/** Get-or-create `sigrun-<id>` with the
|
|
246
|
+
/** Get-or-create `sigrun-<id>` with the per-peer observe config. */
|
|
234
247
|
async #getOrCreate(conversationId, owner) {
|
|
235
248
|
const session = await this.#client.session(`sigrun-${conversationId}`);
|
|
236
249
|
const userPeer = await this.#client.userPeerFor(owner);
|
|
237
250
|
const agentPeer = await this.#client.agentPeer();
|
|
238
|
-
// The SDK's
|
|
239
|
-
//
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
await session.addPeers(peers);
|
|
251
|
+
// The SDK's zod union (PeerAdditionSchema) does NOT accept the
|
|
252
|
+
// documented map form — only string | Peer | array | tuple. The
|
|
253
|
+
// tuple form [peerId, config] transforms to the API's per-peer
|
|
254
|
+
// observe config (verified live 2026-10-31 against honcho v3.0.9:
|
|
255
|
+
// tuple-array 200s, map form fails client-side validation).
|
|
256
|
+
await session.addPeers([
|
|
257
|
+
[userPeer.id, { observeMe: true, observeOthers: true }],
|
|
258
|
+
[agentPeer.id, { observeMe: true, observeOthers: true }],
|
|
259
|
+
]);
|
|
248
260
|
await session.setConfiguration(SESSION_CONFIGURATION);
|
|
249
261
|
return session;
|
|
250
262
|
}
|
package/dist/index.d.ts
CHANGED
|
@@ -38,5 +38,21 @@ export interface FactoryDeps {
|
|
|
38
38
|
createEngine?: (client: HonchoClient, stores: ExtensionStores, settings: HonchoSettings, hooks: DeliveryHooks) => DeliveryEngine;
|
|
39
39
|
createClient?: (settings: HonchoSettings, apiKey: string | undefined) => HonchoClient;
|
|
40
40
|
}
|
|
41
|
+
/**
|
|
42
|
+
* The leading-edge hook: a delivered conversation whose representation
|
|
43
|
+
* cache is ABSENT refreshes once (a present cache is fresh enough — the
|
|
44
|
+
* idle worker owns re-refreshes). The delivery engine calls the hook OUT
|
|
45
|
+
* of the conversation chain, so this refresh chains behind the delivery
|
|
46
|
+
* that just settled (never a circular wait). Failures log and are
|
|
47
|
+
* swallowed: a failed refresh must never fault the delivery it rode in on.
|
|
48
|
+
*
|
|
49
|
+
* Extracted for the composition test (T8 parked minor): the test uses the
|
|
50
|
+
* same factory instead of re-implementing the body verbatim.
|
|
51
|
+
*/
|
|
52
|
+
export declare function makeLeadingEdgeHook(opts: {
|
|
53
|
+
readonly stores: () => Promise<ExtensionStores>;
|
|
54
|
+
readonly client: HonchoClient;
|
|
55
|
+
readonly settings: HonchoSettings;
|
|
56
|
+
}): DeliveryHooks;
|
|
41
57
|
export declare function createHonchoMemory(host: SigrunHost, deps?: FactoryDeps): SigrunExtensionResultLike;
|
|
42
58
|
export default createHonchoMemory;
|
package/dist/index.js
CHANGED
|
@@ -37,13 +37,19 @@ import { createTools } from "./tools.js";
|
|
|
37
37
|
const EXTENSION_NAME = "@sigrun/memory-honcho";
|
|
38
38
|
/** The shutdown drain deadline (spec: 25s under the host's shutdown budget). */
|
|
39
39
|
const DRAIN_DEADLINE_MS = 25_000;
|
|
40
|
-
/**
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
40
|
+
/**
|
|
41
|
+
* The missing-API-key notice fires once per FACTORY INSTANCE, not per
|
|
42
|
+
* event. Factory-scoped (not module-global) so tests that create multiple
|
|
43
|
+
* instances in any order each see the warning.
|
|
44
|
+
*/
|
|
45
|
+
function createWarnMissingKeyOnce() {
|
|
46
|
+
let warned = false;
|
|
47
|
+
return () => {
|
|
48
|
+
if (warned)
|
|
49
|
+
return;
|
|
50
|
+
warned = true;
|
|
51
|
+
console.warn("memory-honcho: no API key (secrets 'api-key' or SIGRUN_EXT_MEMORY_HONCHO_API_KEY) — capturing to the outbox without delivery");
|
|
52
|
+
};
|
|
47
53
|
}
|
|
48
54
|
/**
|
|
49
55
|
* Structural equality over the resolved settings (an engine rebuild is
|
|
@@ -62,10 +68,40 @@ function settingsEqual(a, b) {
|
|
|
62
68
|
a.deliveredBatchWindow === b.deliveredBatchWindow &&
|
|
63
69
|
a.anchorWindow === b.anchorWindow);
|
|
64
70
|
}
|
|
71
|
+
/**
|
|
72
|
+
* The leading-edge hook: a delivered conversation whose representation
|
|
73
|
+
* cache is ABSENT refreshes once (a present cache is fresh enough — the
|
|
74
|
+
* idle worker owns re-refreshes). The delivery engine calls the hook OUT
|
|
75
|
+
* of the conversation chain, so this refresh chains behind the delivery
|
|
76
|
+
* that just settled (never a circular wait). Failures log and are
|
|
77
|
+
* swallowed: a failed refresh must never fault the delivery it rode in on.
|
|
78
|
+
*
|
|
79
|
+
* Extracted for the composition test (T8 parked minor): the test uses the
|
|
80
|
+
* same factory instead of re-implementing the body verbatim.
|
|
81
|
+
*/
|
|
82
|
+
export function makeLeadingEdgeHook(opts) {
|
|
83
|
+
return {
|
|
84
|
+
onDelivered: async (conversationId) => {
|
|
85
|
+
const store = await opts.stores();
|
|
86
|
+
if (store.readRepresentationSync(conversationId) !== undefined)
|
|
87
|
+
return;
|
|
88
|
+
const ledger = store.readLedgerSync(conversationId);
|
|
89
|
+
if (ledger === undefined)
|
|
90
|
+
return;
|
|
91
|
+
try {
|
|
92
|
+
await refreshRepresentation(opts.client, store, opts.settings, conversationId, ledger.ownerAtFirstCapture);
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
console.error(`memory-honcho: leading-edge representation refresh failed for conversation ${conversationId}`, error);
|
|
96
|
+
}
|
|
97
|
+
},
|
|
98
|
+
};
|
|
99
|
+
}
|
|
65
100
|
export function createHonchoMemory(host, deps = {}) {
|
|
66
101
|
const settingsStore = host.stores.settings;
|
|
67
102
|
const secretsStore = host.stores.secrets;
|
|
68
103
|
const paths = host.stores.paths;
|
|
104
|
+
const warnMissingKeyOnce = createWarnMissingKeyOnce();
|
|
69
105
|
// Lazily constructed on first use — settings/key are resolved at that
|
|
70
106
|
// moment, so configuration written after boot still applies.
|
|
71
107
|
let storesPromise;
|
|
@@ -127,22 +163,11 @@ export function createHonchoMemory(host, deps = {}) {
|
|
|
127
163
|
// so this refresh chains behind the delivery that just settled
|
|
128
164
|
// (never a circular wait). Failures log and are swallowed: a
|
|
129
165
|
// failed refresh must never fault the delivery it rode in on.
|
|
130
|
-
const hooks = {
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
const ledger = store.readLedgerSync(conversationId);
|
|
136
|
-
if (ledger === undefined)
|
|
137
|
-
return;
|
|
138
|
-
try {
|
|
139
|
-
await refreshRepresentation(client, store, settings, conversationId, ledger.ownerAtFirstCapture);
|
|
140
|
-
}
|
|
141
|
-
catch (error) {
|
|
142
|
-
console.error(`memory-honcho: leading-edge representation refresh failed for conversation ${conversationId}`, error);
|
|
143
|
-
}
|
|
144
|
-
},
|
|
145
|
-
};
|
|
166
|
+
const hooks = makeLeadingEdgeHook({
|
|
167
|
+
stores,
|
|
168
|
+
client,
|
|
169
|
+
settings,
|
|
170
|
+
});
|
|
146
171
|
return (deps.createEngine ?? ((c, st, s, h) => new DeliveryEngine(c, st, s, h)))(client, await stores(), settings, hooks);
|
|
147
172
|
})();
|
|
148
173
|
enginePromise = building;
|
|
@@ -284,11 +309,11 @@ export function createHonchoMemory(host, deps = {}) {
|
|
|
284
309
|
// snapshot; skip quietly. `engineClient` is set
|
|
285
310
|
// alongside the engine the `instanceof` check
|
|
286
311
|
// just confirmed.
|
|
287
|
-
if (engineClient
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
312
|
+
if (engineClient !== undefined) {
|
|
313
|
+
const conversation = engineClient.for(String(conversationId));
|
|
314
|
+
if (conversation.hasSession()) {
|
|
315
|
+
await refreshRepresentation(engineClient, store, engineSettings, conversationId, ledger.ownerAtFirstCapture);
|
|
316
|
+
}
|
|
292
317
|
}
|
|
293
318
|
}
|
|
294
319
|
}
|
package/dist/tools.js
CHANGED
|
@@ -19,6 +19,10 @@ const NO_MEMORY_TEXT = "No relevant memory found.";
|
|
|
19
19
|
/** Wrap a Honcho failure with the extension's error prefix. */
|
|
20
20
|
function honchoError(error) {
|
|
21
21
|
const message = error instanceof Error ? error.message : String(error);
|
|
22
|
+
// Internal guard errors already carry the extension prefix; wrapping
|
|
23
|
+
// them again would nest it.
|
|
24
|
+
if (message.startsWith("memory-honcho: "))
|
|
25
|
+
return new Error(message);
|
|
22
26
|
return new Error(`memory-honcho: Honcho unavailable (${message})`);
|
|
23
27
|
}
|
|
24
28
|
/** Build the three recall tools. */
|
|
@@ -38,11 +42,19 @@ export function createTools(deps) {
|
|
|
38
42
|
async function userPeer(conversationId) {
|
|
39
43
|
const owner = await resolveOwner(conversationId);
|
|
40
44
|
await deps.engine().then((engine) => engine.establish(conversationId, owner));
|
|
45
|
+
return clientOrThrow().userPeerFor(owner);
|
|
46
|
+
}
|
|
47
|
+
/** The agent peer, after the client is guaranteed. */
|
|
48
|
+
async function agentPeer() {
|
|
49
|
+
return clientOrThrow().agentPeer();
|
|
50
|
+
}
|
|
51
|
+
/** The client, or a clean error (the factory wires a live accessor). */
|
|
52
|
+
function clientOrThrow() {
|
|
41
53
|
const client = deps.client();
|
|
42
54
|
if (client === undefined) {
|
|
43
55
|
throw new Error("memory-honcho: client not initialized");
|
|
44
56
|
}
|
|
45
|
-
return client
|
|
57
|
+
return client;
|
|
46
58
|
}
|
|
47
59
|
/** Settings after the engine has resolved (guaranteed initialized). */
|
|
48
60
|
function currentSettings() {
|
|
@@ -105,12 +117,8 @@ export function createTools(deps) {
|
|
|
105
117
|
try {
|
|
106
118
|
const conversationId = Number(api.conversationId);
|
|
107
119
|
const targetPeer = await userPeer(conversationId);
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
throw new Error("memory-honcho: client not initialized");
|
|
111
|
-
}
|
|
112
|
-
const agentPeer = await client.agentPeer();
|
|
113
|
-
const answer = await agentPeer.chat(args.query, {
|
|
120
|
+
const agent = await agentPeer();
|
|
121
|
+
const answer = await agent.chat(args.query, {
|
|
114
122
|
target: targetPeer,
|
|
115
123
|
reasoningLevel: args.reasoningLevel ?? "low",
|
|
116
124
|
});
|
|
@@ -136,12 +144,8 @@ export function createTools(deps) {
|
|
|
136
144
|
try {
|
|
137
145
|
const conversationId = Number(api.conversationId);
|
|
138
146
|
const targetPeer = await userPeer(conversationId);
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
throw new Error("memory-honcho: client not initialized");
|
|
142
|
-
}
|
|
143
|
-
const agentPeer = await client.agentPeer();
|
|
144
|
-
await agentPeer.conclusionsOf(targetPeer).create({
|
|
147
|
+
const agent = await agentPeer();
|
|
148
|
+
await agent.conclusionsOf(targetPeer).create({
|
|
145
149
|
content: args.content,
|
|
146
150
|
});
|
|
147
151
|
return {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@sigrun/memory-honcho",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Honcho persistent-memory extension for sigrun (pi-durable)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"engines": {
|
|
@@ -55,4 +55,4 @@
|
|
|
55
55
|
"access": "public",
|
|
56
56
|
"provenance": true
|
|
57
57
|
}
|
|
58
|
-
}
|
|
58
|
+
}
|