@sigrun/memory-honcho 0.0.0-stage → 0.1.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/CHANGELOG.md +64 -0
- package/LICENSE +21 -0
- package/README.md +103 -2
- package/dist/capture.d.ts +17 -0
- package/dist/capture.js +61 -0
- package/dist/chains.d.ts +18 -0
- package/dist/chains.js +33 -0
- package/dist/config.d.ts +28 -0
- package/dist/config.js +43 -0
- package/dist/delivery.d.ts +58 -0
- package/dist/delivery.js +264 -0
- package/dist/fork-rules.d.ts +32 -0
- package/dist/fork-rules.js +42 -0
- package/dist/honcho.d.ts +65 -0
- package/dist/honcho.js +127 -0
- package/dist/index.d.ts +42 -0
- package/dist/index.js +345 -0
- package/dist/representations.d.ts +42 -0
- package/dist/representations.js +81 -0
- package/dist/status.d.ts +57 -0
- package/dist/status.js +89 -0
- package/dist/stores.d.ts +83 -0
- package/dist/stores.js +244 -0
- package/dist/sweep.d.ts +60 -0
- package/dist/sweep.js +198 -0
- package/dist/tools.d.ts +41 -0
- package/dist/tools.js +157 -0
- package/dist/types.d.ts +139 -0
- package/dist/types.js +10 -0
- package/package.json +56 -4
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
All notable changes to this project will be documented in this file.
|
|
4
|
+
|
|
5
|
+
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
|
|
6
|
+
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
|
7
|
+
|
|
8
|
+
## [0.1.1] - 2026-10-31
|
|
9
|
+
|
|
10
|
+
### Fixed
|
|
11
|
+
|
|
12
|
+
- `establish` adds session peers via the SDK's tuple form
|
|
13
|
+
(`[[peerId, {observeMe, observeOthers}]]`) instead of the documented
|
|
14
|
+
map form — the SDK's client-side zod union rejects the map form, so
|
|
15
|
+
every first-capture establishment failed against a real server
|
|
16
|
+
(verified live against honcho v3.0.9).
|
|
17
|
+
|
|
18
|
+
## [0.1.0] - 2026-10-31
|
|
19
|
+
### Added
|
|
20
|
+
- Honcho persistent-memory extension for sigrun (pi-durable). Every
|
|
21
|
+
settled run (answered and unanswered) appends one capture batch to a
|
|
22
|
+
durable JSONL outbox under a module-level mutex, then kicks
|
|
23
|
+
fire-and-forget delivery; per-conversation selection gating reads the
|
|
24
|
+
conversation's `AgentDoc.extensions` (array-form exclusion skips
|
|
25
|
+
capture; positive verdicts memoized per conversation).
|
|
26
|
+
- Delivery engine: per-conversation serialization chains; lazy session
|
|
27
|
+
establishment (get-or-create `sigrun-<conversationId>` with map-form
|
|
28
|
+
peer observation config and `summary`/`dream` enabled); batchId
|
|
29
|
+
dedup via the ledger's delivered-batch window; fork anchors
|
|
30
|
+
(sigrun entry id → Honcho message id) folded on delivery; an
|
|
31
|
+
accepted send-then-crash duplicate window documented.
|
|
32
|
+
- Fork continuity: `conversationForked` writes a durable pending-fork
|
|
33
|
+
record with zero network; the child's first delivery clones the
|
|
34
|
+
parent's Honcho session at the newest anchor at-or-before the fork
|
|
35
|
+
point (`pickCloneAnchor`: clone / fresh-session `none-window` /
|
|
36
|
+
`none-empty`), with clone-duplicate mitigation by write-ahead fork
|
|
37
|
+
records and status-route session counts.
|
|
38
|
+
- Cron job `outbox-sweep` (`*/15 * * * *`, task action
|
|
39
|
+
`honcho-outbox-sweep`): fork-first establishment, per-conversation
|
|
40
|
+
redelivery, then compaction — delivered records dropped, over-retention
|
|
41
|
+
pending records dropped with a per-record warning.
|
|
42
|
+
- Representation cache + two prompt sections (`long-term-memory`,
|
|
43
|
+
`honcho-memory`) rendering synchronously from the cache; leading-edge
|
|
44
|
+
refresh at first delivery; wake-only worker `honcho-representations`
|
|
45
|
+
(`conversationIdle`/`conversationArchived`) re-refreshes the woken
|
|
46
|
+
conversation through the memoized engine client.
|
|
47
|
+
- Recall tools `honcho_search` (author-scoped `Peer.search`),
|
|
48
|
+
`honcho_chat` (peer-pair dialectic, no session scope), and
|
|
49
|
+
`honcho_remember` (session-less `conclusionsOf` write — the
|
|
50
|
+
README documents the visibility caveat).
|
|
51
|
+
- Status route `GET /v1/ext/memory-honcho/status` (`auth: "user"`):
|
|
52
|
+
config summary with the key projected to a `configured` boolean,
|
|
53
|
+
`lastDeliveryAt` from the persistent `state/last-delivery.json`
|
|
54
|
+
marker (falling back to retained-record derivation), outbox/ledger/
|
|
55
|
+
fork/session counts, and worker bookkeeping — best-effort, degrading
|
|
56
|
+
to zeroed counts on a corrupt file rather than failing the request.
|
|
57
|
+
- `userDeleted`: departure notice only — no data movement (peers are
|
|
58
|
+
server-side Honcho state; transfer is Honcho-side or deferred).
|
|
59
|
+
- `harnessShutdown` drains delivery chains under a 25s deadline.
|
|
60
|
+
- Settings/secret resolution: `resolveSettings` (eleven keys with
|
|
61
|
+
defaults and numeric validation) and `resolveApiKey` (stored
|
|
62
|
+
`api-key` secret, then env `SIGRUN_EXT_MEMORY_HONCHO_API_KEY`) —
|
|
63
|
+
both read live per use, so a key written after boot applies at the
|
|
64
|
+
next event without a restart.
|
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Michael MacLeod
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -1,3 +1,104 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @sigrun/memory-honcho
|
|
2
2
|
|
|
3
|
-
|
|
3
|
+
Honcho persistent-memory extension for [sigrun](https://github.com/sigrun-ai/sigrun) (pi-durable).
|
|
4
|
+
|
|
5
|
+
Sigrun's memory seam delivers every settled run to this extension, which captures both halves of the turn into a durable on-disk outbox and delivers them to [Honcho](https://github.com/plastic-labs/honcho) — cross-conversation, peer-level memory: Honcho derives a representation of each user from every conversation the agent shared with them, and this extension recalls through tools and renders a per-conversation prompt section from a cached copy of that representation.
|
|
6
|
+
|
|
7
|
+
Why Honcho over a local file store: derivation (dreams, summaries, peer representations) runs **server-side** — the client's only jobs are capture delivery and representation refresh, both scheduled here; nothing else is ported.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```jsonc
|
|
12
|
+
// settings.json (sigrun config dir) — the UNIFIED extensions list:
|
|
13
|
+
// install + load in one place; npm specifiers
|
|
14
|
+
// ("@sigrun/memory-honcho@^0.1.0") and the file: form below are the
|
|
15
|
+
// same list.
|
|
16
|
+
{
|
|
17
|
+
"extensions": ["file:/path/to/sigrun-memory-honcho"]
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## Configuration
|
|
22
|
+
|
|
23
|
+
**API key** — resolution order, fresh read per use (stored wins without restart):
|
|
24
|
+
|
|
25
|
+
1. the stored secret: section `api-key` in `<configDir>/secrets.json`, then
|
|
26
|
+
2. the `SIGRUN_EXT_MEMORY_HONCHO_API_KEY` environment variable.
|
|
27
|
+
|
|
28
|
+
With no key configured, captures still append to the outbox and the sweeper delivers them once a key appears — a key added after boot applies at the next event, no restart.
|
|
29
|
+
|
|
30
|
+
**Settings** — `<configDir>/settings/memory-honcho.json`, all optional (defaults in parentheses), read live per use: `workspaceId` (`"sigrun"`), `agentPeerId` (`"sigrun"`), `baseUrl` (the Honcho SDK default — omit for honcho.dev), `contextTokens` (1200), `maxMessageLength` (8000), `searchLimit` (8), `toolPreviewLength` (500), `representationTtlMs` (600000), `outboxRetentionDays` (30), `deliveredBatchWindow` (200), `anchorWindow` (1000). Operator caution: changing `workspaceId` or `baseUrl` after first capture leaves stale sessions in the old workspace.
|
|
31
|
+
|
|
32
|
+
## On-disk layout
|
|
33
|
+
|
|
34
|
+
The host hands the extension three store roots; writes are atomic (temp + rename):
|
|
35
|
+
|
|
36
|
+
- `data/outbox/capture.jsonl` — the durable outbox, the **only durable copy** of not-yet-delivered conversation content (the backed-up bucket).
|
|
37
|
+
- `state/ledger/<conversationId>.json` — per-conversation bookkeeping: the Honcho session mapping, the delivered-batchId window (dedup), and the fork anchors (sigrun entry id → Honcho message id).
|
|
38
|
+
- `state/forks/pending/<childId>.json` — fork-mapping records, written by the fork handler with zero network, resolved on clone.
|
|
39
|
+
- `state/last-delivery.json` — the persistent newest-delivery marker the status route reads (the sweeper drops delivered records, so the marker is the surviving source in steady state).
|
|
40
|
+
- `cache/representations/<conversationId>.json` — the representation snapshots the prompt section renders.
|
|
41
|
+
|
|
42
|
+
## Capture semantics
|
|
43
|
+
|
|
44
|
+
Every settled run (answered **and** unanswered) appends one outbox record with the user inputs and the answer, then kicks fire-and-forget delivery; a missing key skips only the kick — the sweeper delivers later. Delivery is per-conversation chain-serialized; the delivered-batchId window in the ledger makes redelivery idempotent.
|
|
45
|
+
|
|
46
|
+
**Selection gating:** the conversation's per-conversation extension selection is read per settle. The array form (`{extensions: ["…"]}`) selects exactly its members — an array not naming `@sigrun/memory-honcho` skips capture. The object form (`{add, remove}`) edits the host default and does not exclude this extension — a `remove` of us is a no-op, because we are not host-default. Positive verdicts are memoized per conversation (a `false` is re-read on the next event, so a mid-conversation opt-in captures). **Residual, accepted for v1:** an array-form opt-out issued *after* a positive verdict keeps capturing until the process restarts.
|
|
47
|
+
|
|
48
|
+
**Pending-input visibility:** `runSettled` fires on settle — inputs queued or typed mid-run are invisible to capture until the run settles.
|
|
49
|
+
|
|
50
|
+
## Recall tools
|
|
51
|
+
|
|
52
|
+
- **`honcho_search`** (`query`) — workspace-wide search over the calling user's own messages, across conversations. **Author-scoped, stated honestly:** `Peer.search` returns messages with that peer as *author* — user text only, never assistant answers; assistant recall lives in `honcho_chat` and the section representation.
|
|
53
|
+
- **`honcho_chat`** (`query`, `reasoningLevel?`) — a dialectic query over the agent peer's full representation of the calling user (no session scope).
|
|
54
|
+
- **`honcho_remember`** (`content`) — writes a conclusion about the calling user (`conclusionsOf`), **with no `sessionId`**: a remembered fact is user-level, not conversation-level, matching the cross-conversation recall ruling. **Visibility caveat (not yet live-verified):** whether a session-less conclusion appears in the session-scoped representation the prompt section renders depends on Honcho's default `limitToSession` behavior; if it is scoped out, a remembered fact may not surface in the section — the tool would need to stamp the calling session's id.
|
|
55
|
+
|
|
56
|
+
**Privacy boundary (qualified):** `honcho_search` is author-scoped — one user's search returns only messages captured under their own peer. `honcho_chat` and representations are **peer-pair-scoped**: the agent peer's representation of a user derives from every session where the agent observed that user — *including shared conversations*, where `observeOthers` derives over co-participants' turns too. In a multi-user deployment with shared conversations, a `honcho_chat` answer can therefore reflect another user's turns from a co-observed session (Honcho's own derivation semantics, not a sigrun leak). Single-user deployments are unaffected. Peer ids ARE the sigrun usernames — stable, not anonymizing.
|
|
57
|
+
|
|
58
|
+
## Fork semantics
|
|
59
|
+
|
|
60
|
+
A sigrun fork writes a durable pending-fork record (zero network); the child's first capture establishes its Honcho session as a **clone** of the parent's session, cut at the parent's fork anchor — the child inherits the parent's Honcho history to the fork point.
|
|
61
|
+
|
|
62
|
+
**What clone buys (honest premise):** the SDK documents `clone` as a copy of the session's *messages* ("up to and including that message"); nothing promises conclusions or derived representations are copied. The child's representations are **re-derived server-side** from the cloned messages. The value is transcript continuity for derivation, not pre-warmed conclusions. (Live verification of what clone copies is outstanding; the design does not depend on the stronger reading.)
|
|
63
|
+
|
|
64
|
+
**Granularity limit (accepted):** only assistant answers carry Honcho anchors — a fork at a user input entry clones to the answer *before* it, so the child's Honcho history may miss the trailing user input that sigrun's transcript includes.
|
|
65
|
+
|
|
66
|
+
**Fork precondition:** the pending-fork record must exist when the child first captures — a conversation forked **before** this extension was loaded has no record and starts a fresh session (no history).
|
|
67
|
+
|
|
68
|
+
## Jobs and worker
|
|
69
|
+
|
|
70
|
+
- **Cron job `outbox-sweep`** (`*/15 * * * *`) — the delivery engine of record: resolves pending forks, delivers each conversation's pending records, then compacts (delivered records are dropped; pending records older than `outboxRetentionDays` are dropped with a warning per record). The sweep keeps working when Honcho is down — failures are per-conversation and the next sweep retries.
|
|
71
|
+
- **Wake-only worker `honcho-representations`** (`conversationIdle`, `conversationArchived`) — re-refreshes the woken conversation's representation cache, which the two prompt sections (`long-term-memory`, `honcho-memory`) render synchronously. A delivered conversation with no cache also refreshes once at delivery (the leading edge).
|
|
72
|
+
|
|
73
|
+
## Status route
|
|
74
|
+
|
|
75
|
+
`GET /v1/ext/memory-honcho/status` (`auth: "user"`): config summary (never key material — the key projects to a `configured` boolean), newest-delivery time (`lastDeliveryAt`, from the persistent marker), pending outbox count, ledger/session counts (session counts make the accepted clone-duplicate window operator-visible), pending-fork count, and the extension's own worker bookkeeping (last-run, last-error — the route cannot read the host's `/v1/meta` worker projection). Caveats: the counts are **global** across users — the route answers for the whole deployment to any authenticated user; and a corrupt store file degrades that bucket's counts to zero rather than failing the request.
|
|
76
|
+
|
|
77
|
+
## Troubleshooting
|
|
78
|
+
|
|
79
|
+
- **Honcho down** — captures stay pending in the outbox; the sweeper retries every 15 minutes. Sections render the last-good cache. Tool calls throw a clean error naming the extension.
|
|
80
|
+
- **Key added after boot** — nothing to restart: secrets and settings are re-read per use, so the next settled run delivers (and the sweeper drains the backlog).
|
|
81
|
+
- **A fork child has no history** — check the fork precondition above: the fork must happen while this extension is loaded.
|
|
82
|
+
|
|
83
|
+
## Limitations
|
|
84
|
+
|
|
85
|
+
- `honcho_search` is author-scoped: it returns the user's *own* messages only — assistant answers are reachable through `honcho_chat` and the section representation, not through search.
|
|
86
|
+
- Chat and representations are peer-pair-scoped: in a multi-user deployment with shared conversations, a `honcho_chat` answer can reflect another user's turns from a co-observed session (Honcho's derivation semantics). Operators who need a hard boundary would drop `observeOthers` (a future setting).
|
|
87
|
+
- Fork clone = transcript continuity: the child inherits the parent's messages to the fork point and Honcho re-derives conclusions and representations from them — conclusions are session-scoped and are *not* copied by the clone.
|
|
88
|
+
- `honcho_remember` writes a session-less (global) conclusion; whether it surfaces in the session-scoped representation depends on Honcho's `limitToSession` default — not yet live-verified.
|
|
89
|
+
- Pending-input visibility: capture sees a run's inputs only at settle.
|
|
90
|
+
- Fork precondition: a conversation forked before this extension was loaded starts with a fresh Honcho session.
|
|
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
|
+
- **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
|
+
- **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
|
+
|
|
95
|
+
## Development
|
|
96
|
+
|
|
97
|
+
```sh
|
|
98
|
+
npm install
|
|
99
|
+
npm test # manifest check + vitest
|
|
100
|
+
npm run typecheck # tsc --noEmit
|
|
101
|
+
npm run build # tsc -> dist/ (commit the result)
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
`dist/` is committed by design — git-source installs load `dist/index.js` with no build step. Tests own their literals: synthetic users, a fake Honcho SDK, no real keys, no live Honcho.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capture batch builder: turn one settled run into one outbox record.
|
|
3
|
+
*
|
|
4
|
+
* Pure — no I/O, no Honcho. The filtering rules (spec §Capture):
|
|
5
|
+
* user texts first, in admission order, skipping empty and over-cap texts;
|
|
6
|
+
* then, when the run answered AND the answer text survives the same filter,
|
|
7
|
+
* one agent entry. Unanswered runs tag their user entries `unanswered: true`
|
|
8
|
+
* and carry no agent entry and no `answerEntryId`. `undefined` when nothing
|
|
9
|
+
* survives the filter (an all-empty batch is pure outbox churn).
|
|
10
|
+
*/
|
|
11
|
+
import type { OutboxRecord, RunSettledEventLike } from "./types.js";
|
|
12
|
+
/**
|
|
13
|
+
* Build the outbox record for one settled run, or `undefined` when no
|
|
14
|
+
* non-empty, under-cap text survives. `owner` is pre-resolved by the caller
|
|
15
|
+
* (ownerless conversations arrive as `"user"`).
|
|
16
|
+
*/
|
|
17
|
+
export declare function buildCaptureBatch(event: RunSettledEventLike, owner: string, maxMessageLength: number): OutboxRecord | undefined;
|
package/dist/capture.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The capture batch builder: turn one settled run into one outbox record.
|
|
3
|
+
*
|
|
4
|
+
* Pure — no I/O, no Honcho. The filtering rules (spec §Capture):
|
|
5
|
+
* user texts first, in admission order, skipping empty and over-cap texts;
|
|
6
|
+
* then, when the run answered AND the answer text survives the same filter,
|
|
7
|
+
* one agent entry. Unanswered runs tag their user entries `unanswered: true`
|
|
8
|
+
* and carry no agent entry and no `answerEntryId`. `undefined` when nothing
|
|
9
|
+
* survives the filter (an all-empty batch is pure outbox churn).
|
|
10
|
+
*/
|
|
11
|
+
import { createHash } from "node:crypto";
|
|
12
|
+
/**
|
|
13
|
+
* Build the outbox record for one settled run, or `undefined` when no
|
|
14
|
+
* non-empty, under-cap text survives. `owner` is pre-resolved by the caller
|
|
15
|
+
* (ownerless conversations arrive as `"user"`).
|
|
16
|
+
*/
|
|
17
|
+
export function buildCaptureBatch(event, owner, maxMessageLength) {
|
|
18
|
+
const batchId = createHash("sha256")
|
|
19
|
+
.update(`${event.conversationId}:${event.inputs[0] ?? ""}:${event.inputs.length}`)
|
|
20
|
+
.digest("hex");
|
|
21
|
+
const metadata = {
|
|
22
|
+
conversationId: event.conversationId,
|
|
23
|
+
submissionIds: event.inputs,
|
|
24
|
+
batchId,
|
|
25
|
+
};
|
|
26
|
+
const entries = [];
|
|
27
|
+
for (const text of event.inputTexts) {
|
|
28
|
+
if (text === "" || text.length > maxMessageLength)
|
|
29
|
+
continue;
|
|
30
|
+
entries.push({
|
|
31
|
+
peer: "user",
|
|
32
|
+
text,
|
|
33
|
+
metadata: event.outcome.status === "unanswered"
|
|
34
|
+
? { ...metadata, unanswered: true }
|
|
35
|
+
: metadata,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
let answerEntryId;
|
|
39
|
+
if (event.outcome.status === "answered") {
|
|
40
|
+
const answerText = event.outcome.answer.content
|
|
41
|
+
.filter((block) => block.type === "text")
|
|
42
|
+
.map((block) => (block.type === "text" ? (block.text ?? "") : ""))
|
|
43
|
+
.join("");
|
|
44
|
+
if (answerText !== "" && answerText.length <= maxMessageLength) {
|
|
45
|
+
answerEntryId = event.outcome.answerEntryId;
|
|
46
|
+
entries.push({ peer: "agent", text: answerText, metadata });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
if (entries.length === 0)
|
|
50
|
+
return undefined;
|
|
51
|
+
return {
|
|
52
|
+
type: "capture",
|
|
53
|
+
conversationId: event.conversationId,
|
|
54
|
+
owner,
|
|
55
|
+
...(answerEntryId === undefined ? {} : { answerEntryId }),
|
|
56
|
+
entries,
|
|
57
|
+
batchId,
|
|
58
|
+
status: "pending",
|
|
59
|
+
createdAt: Date.now(),
|
|
60
|
+
};
|
|
61
|
+
}
|
package/dist/chains.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-label serialization chains (the pi port's `enqueue` pattern).
|
|
3
|
+
*
|
|
4
|
+
* Work appended under a label runs strictly after the previously queued
|
|
5
|
+
* work for that label settles, so Honcho saves for one conversation
|
|
6
|
+
* happen in order without racing. The caller's promise settles with the
|
|
7
|
+
* work's own result; the internal TAIL promise never rejects (it
|
|
8
|
+
* self-catches and reports via `console.error` labeled with the chain
|
|
9
|
+
* name), so one failing save cannot poison the conversation's queue.
|
|
10
|
+
*/
|
|
11
|
+
type Work<T> = () => Promise<T>;
|
|
12
|
+
export declare function chain<T>(label: string, work: Work<T>): Promise<T>;
|
|
13
|
+
/**
|
|
14
|
+
* Drain hook for `harnessShutdown`: resolves once every tail live at
|
|
15
|
+
* call time has settled.
|
|
16
|
+
*/
|
|
17
|
+
export declare function chainTails(): Promise<void[]>;
|
|
18
|
+
export {};
|
package/dist/chains.js
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-label serialization chains (the pi port's `enqueue` pattern).
|
|
3
|
+
*
|
|
4
|
+
* Work appended under a label runs strictly after the previously queued
|
|
5
|
+
* work for that label settles, so Honcho saves for one conversation
|
|
6
|
+
* happen in order without racing. The caller's promise settles with the
|
|
7
|
+
* work's own result; the internal TAIL promise never rejects (it
|
|
8
|
+
* self-catches and reports via `console.error` labeled with the chain
|
|
9
|
+
* name), so one failing save cannot poison the conversation's queue.
|
|
10
|
+
*/
|
|
11
|
+
const tails = new Map();
|
|
12
|
+
export function chain(label, work) {
|
|
13
|
+
const run = (tails.get(label) ?? Promise.resolve()).then(work);
|
|
14
|
+
// The tail self-catches: a rejection is reported once here and never
|
|
15
|
+
// propagates into the next queued item (which would re-report it).
|
|
16
|
+
const tail = run.then(() => undefined, (error) => {
|
|
17
|
+
console.error(`memory-honcho: chain "${label}" work failed`, error);
|
|
18
|
+
});
|
|
19
|
+
tails.set(label, tail);
|
|
20
|
+
void tail.then(() => {
|
|
21
|
+
// Drop the tail once settled, unless newer work already extended it.
|
|
22
|
+
if (tails.get(label) === tail)
|
|
23
|
+
tails.delete(label);
|
|
24
|
+
});
|
|
25
|
+
return run;
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Drain hook for `harnessShutdown`: resolves once every tail live at
|
|
29
|
+
* call time has settled.
|
|
30
|
+
*/
|
|
31
|
+
export function chainTails() {
|
|
32
|
+
return Promise.all([...tails.values()]);
|
|
33
|
+
}
|
package/dist/config.d.ts
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings and secret resolution for @sigrun/memory-honcho.
|
|
3
|
+
*
|
|
4
|
+
* Both readers are structural slices of `host.stores`; values are read
|
|
5
|
+
* LIVE per call (never memoized) so setting/key changes apply at next
|
|
6
|
+
* use without a restart.
|
|
7
|
+
*/
|
|
8
|
+
export interface SettingsReader {
|
|
9
|
+
get(): Promise<Record<string, unknown>>;
|
|
10
|
+
}
|
|
11
|
+
export interface SecretsReader {
|
|
12
|
+
get(key: string): Promise<string | undefined>;
|
|
13
|
+
}
|
|
14
|
+
export interface HonchoSettings {
|
|
15
|
+
agentPeerId: string;
|
|
16
|
+
baseUrl: string | undefined;
|
|
17
|
+
workspaceId: string;
|
|
18
|
+
contextTokens: number;
|
|
19
|
+
maxMessageLength: number;
|
|
20
|
+
searchLimit: number;
|
|
21
|
+
toolPreviewLength: number;
|
|
22
|
+
representationTtlMs: number;
|
|
23
|
+
outboxRetentionDays: number;
|
|
24
|
+
deliveredBatchWindow: number;
|
|
25
|
+
anchorWindow: number;
|
|
26
|
+
}
|
|
27
|
+
export declare function resolveSettings(stores: SettingsReader): Promise<HonchoSettings>;
|
|
28
|
+
export declare function resolveApiKey(secrets: SecretsReader): Promise<string | undefined>;
|
package/dist/config.js
ADDED
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settings and secret resolution for @sigrun/memory-honcho.
|
|
3
|
+
*
|
|
4
|
+
* Both readers are structural slices of `host.stores`; values are read
|
|
5
|
+
* LIVE per call (never memoized) so setting/key changes apply at next
|
|
6
|
+
* use without a restart.
|
|
7
|
+
*/
|
|
8
|
+
const API_KEY_ENV = "SIGRUN_EXT_MEMORY_HONCHO_API_KEY";
|
|
9
|
+
function stringSetting(value, fallback) {
|
|
10
|
+
return typeof value === "string" && value !== "" ? value : fallback;
|
|
11
|
+
}
|
|
12
|
+
function integerSetting(value, fallback) {
|
|
13
|
+
return typeof value === "number" && Number.isInteger(value) && value > 0
|
|
14
|
+
? value
|
|
15
|
+
: fallback;
|
|
16
|
+
}
|
|
17
|
+
export async function resolveSettings(stores) {
|
|
18
|
+
const raw = await stores.get();
|
|
19
|
+
return {
|
|
20
|
+
agentPeerId: stringSetting(raw.agentPeerId, "sigrun"),
|
|
21
|
+
baseUrl: typeof raw.baseUrl === "string" && raw.baseUrl !== ""
|
|
22
|
+
? raw.baseUrl
|
|
23
|
+
: undefined,
|
|
24
|
+
workspaceId: stringSetting(raw.workspaceId, "sigrun"),
|
|
25
|
+
contextTokens: integerSetting(raw.contextTokens, 1200),
|
|
26
|
+
maxMessageLength: integerSetting(raw.maxMessageLength, 8000),
|
|
27
|
+
searchLimit: integerSetting(raw.searchLimit, 8),
|
|
28
|
+
toolPreviewLength: integerSetting(raw.toolPreviewLength, 500),
|
|
29
|
+
representationTtlMs: integerSetting(raw.representationTtlMs, 600_000),
|
|
30
|
+
outboxRetentionDays: integerSetting(raw.outboxRetentionDays, 30),
|
|
31
|
+
deliveredBatchWindow: integerSetting(raw.deliveredBatchWindow, 200),
|
|
32
|
+
anchorWindow: integerSetting(raw.anchorWindow, 1000),
|
|
33
|
+
};
|
|
34
|
+
}
|
|
35
|
+
export async function resolveApiKey(secrets) {
|
|
36
|
+
const stored = await secrets.get("api-key");
|
|
37
|
+
if (typeof stored === "string" && stored.trim() !== "")
|
|
38
|
+
return stored;
|
|
39
|
+
const env = process.env[API_KEY_ENV];
|
|
40
|
+
if (typeof env === "string" && env !== "")
|
|
41
|
+
return env;
|
|
42
|
+
return undefined;
|
|
43
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The delivery engine: session establishment (with the fork clone rules)
|
|
3
|
+
* and outbox delivery.
|
|
4
|
+
*
|
|
5
|
+
* Every public method runs inside the conversation's serialization chain
|
|
6
|
+
* (`client.for(id).chain`) — the spec's one-chain-per-conversation rule,
|
|
7
|
+
* so establishment and delivery never race and the double-clone race
|
|
8
|
+
* cannot happen.
|
|
9
|
+
*
|
|
10
|
+
* `deliverPending` acquires the outbox mutex exactly once per flow
|
|
11
|
+
* (`withOutbox` around the read → ledger flip → rewrite cycle); it never
|
|
12
|
+
* nests a second `withOutbox` inside it (the mutex is not re-entrant),
|
|
13
|
+
* and the sweeper (Task 7) calls it per conversation OUTSIDE any outbox
|
|
14
|
+
* lock — lock acquisition lives in one place per flow. A flipped batch
|
|
15
|
+
* also updates the `state/last-delivery.json` marker in the same critical
|
|
16
|
+
* section (the status route's newest-delivery read survives the sweeper's
|
|
17
|
+
* delivered-record drop that way).
|
|
18
|
+
*/
|
|
19
|
+
import type { HonchoSettings } from "./config.js";
|
|
20
|
+
import type { HonchoClient } from "./honcho.js";
|
|
21
|
+
import type { ExtensionStores } from "./stores.js";
|
|
22
|
+
export interface DeliveryHooks {
|
|
23
|
+
/** Leading-edge hook (Task 8 injects the refresh kick); default no-op. */
|
|
24
|
+
onDelivered?(conversationId: number): Promise<void> | void;
|
|
25
|
+
/**
|
|
26
|
+
* Test seam: deliver the batch but skip the ledger write — the accepted
|
|
27
|
+
* crash window between a successful `addMessages` and the ledger flip,
|
|
28
|
+
* whose cost is a re-sent (duplicate) batch on the next pass.
|
|
29
|
+
*/
|
|
30
|
+
failAfterSend?: boolean;
|
|
31
|
+
}
|
|
32
|
+
export declare class DeliveryEngine {
|
|
33
|
+
#private;
|
|
34
|
+
constructor(client: HonchoClient, stores: ExtensionStores, settings: HonchoSettings, hooks?: DeliveryHooks);
|
|
35
|
+
/**
|
|
36
|
+
* Establish the conversation's Honcho session: plain get-or-create, or
|
|
37
|
+
* a clone-with-mapping when a pending-fork record points at a parent
|
|
38
|
+
* ledger with a usable anchor. Idempotent (chain-serialized).
|
|
39
|
+
*/
|
|
40
|
+
establish(conversationId: number, owner: string): Promise<void>;
|
|
41
|
+
/**
|
|
42
|
+
* Deliver this conversation's pending outbox records, oldest first.
|
|
43
|
+
* Delivered batches flip status, fold their batchId into the ledger's
|
|
44
|
+
* delivered window, and fold a fork anchor ONLY when an agent entry was
|
|
45
|
+
* in the batch — keyed by THAT agent entry's returned `Message.id`
|
|
46
|
+
* (unanswered and user-only batches fold no anchor).
|
|
47
|
+
*/
|
|
48
|
+
deliverPending(conversationId: number, owner: string): Promise<void>;
|
|
49
|
+
/**
|
|
50
|
+
* Fire-and-forget delivery (the capture path's posture): self-catching,
|
|
51
|
+
* logs rather than rejecting. Establishment is LAZY (spec §Capture: the
|
|
52
|
+
* capture path runs the establishment path inside the fire-and-forget
|
|
53
|
+
* attempt) — `establish` inside the attempt is chain-serialized ahead of
|
|
54
|
+
* the delivery, is a no-op when the conversation is already established,
|
|
55
|
+
* and re-marks a rebuilt client that lost its in-memory session promise.
|
|
56
|
+
*/
|
|
57
|
+
kick(conversationId: number, owner: string): void;
|
|
58
|
+
}
|