@hypercarrier/rarebit 0.1.0-alpha.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.
Files changed (50) hide show
  1. package/AGENTS.md +14 -0
  2. package/CHANGELOG.md +10 -0
  3. package/LICENSE +21 -0
  4. package/README.md +142 -0
  5. package/RUNBOOK.md +36 -0
  6. package/SECURITY.md +11 -0
  7. package/VISUAL-LANGUAGE.md +89 -0
  8. package/bin/rarebit.mjs +20 -0
  9. package/package.json +96 -0
  10. package/schemas/rarebit.schema.json +618 -0
  11. package/scripts/e2e-recall-pi083.mjs +199 -0
  12. package/scripts/e2e-summary-cache.mjs +257 -0
  13. package/scripts/verify-package.mjs +81 -0
  14. package/src/automatic-summary-policy.mjs +210 -0
  15. package/src/extension.mjs +516 -0
  16. package/src/herdr-activity.mjs +111 -0
  17. package/src/index.mjs +13 -0
  18. package/src/lifecycle.mjs +322 -0
  19. package/src/rarebit-activity.mjs +48 -0
  20. package/src/rarebit-artifact-state.mjs +839 -0
  21. package/src/rarebit-cli.mjs +305 -0
  22. package/src/rarebit-command.mjs +281 -0
  23. package/src/rarebit-core.mjs +525 -0
  24. package/src/rarebit-model.mjs +226 -0
  25. package/src/rarebit-recall.mjs +182 -0
  26. package/src/rarebit-service.mjs +579 -0
  27. package/src/rarebit-session.mjs +227 -0
  28. package/src/rarebit-settings.mjs +79 -0
  29. package/src/rarebit-store.mjs +771 -0
  30. package/src/rarebit-visual-language.mjs +110 -0
  31. package/src/types.d.ts +601 -0
  32. package/test/automatic-summary-policy.test.mjs +476 -0
  33. package/test/extension-activity.test.mjs +73 -0
  34. package/test/fixtures/branch-mixed.json +117 -0
  35. package/test/fixtures/conformance-corpus.v1.json +306 -0
  36. package/test/fixtures/truncated-mixed.jsonl +2 -0
  37. package/test/herdr-activity.test.mjs +93 -0
  38. package/test/lifecycle-contract-independent.test.mjs +627 -0
  39. package/test/rarebit-activity.test.mjs +82 -0
  40. package/test/rarebit-artifact-state.test.mjs +440 -0
  41. package/test/rarebit-cli.test.mjs +222 -0
  42. package/test/rarebit-command.test.mjs +127 -0
  43. package/test/rarebit-core.test.mjs +461 -0
  44. package/test/rarebit-cross-consumer-conformance.test.mjs +150 -0
  45. package/test/rarebit-independent-contract.test.mjs +317 -0
  46. package/test/rarebit-recall.test.mjs +434 -0
  47. package/test/rarebit-sidecar-protocol.test.mjs +538 -0
  48. package/test/rarebit-visual-language.test.mjs +75 -0
  49. package/test/summary-attention-independent.test.mjs +172 -0
  50. package/test/types-history.ts +13 -0
@@ -0,0 +1,322 @@
1
+ function isUserSubmissionOrigin(event) {
2
+ return event?.source === "interactive" || event?.source === "rpc";
3
+ }
4
+
5
+ function isUserMessage(event) {
6
+ return event?.message?.role === "user";
7
+ }
8
+
9
+ function messageText(event) {
10
+ const content = event?.message?.content;
11
+ if (typeof content === "string") return content;
12
+ if (!Array.isArray(content)) return "";
13
+ return content
14
+ .filter((block) => block?.type === "text" && typeof block.text === "string")
15
+ .map((block) => block.text)
16
+ .join("\n");
17
+ }
18
+
19
+ function snapshotMaterializationContext(ctx, mayNotify, lifecycleBoundary) {
20
+ const sessionManager = ctx?.sessionManager;
21
+ const header = sessionManager?.getHeader?.();
22
+ const branch = sessionManager?.getBranch?.();
23
+ const branchSnapshot = Array.isArray(branch) ? branch.slice() : [];
24
+ const sessionFile = sessionManager?.getSessionFile?.();
25
+ const projectTrusted = ctx?.isProjectTrusted?.() === true;
26
+ const liveUi = ctx?.ui;
27
+ const ui =
28
+ liveUi && typeof liveUi.notify === "function"
29
+ ? {
30
+ notify: (...args) => {
31
+ if (mayNotify()) liveUi.notify(...args);
32
+ },
33
+ }
34
+ : liveUi;
35
+ return {
36
+ cwd: ctx?.cwd,
37
+ hasUI: ctx?.hasUI,
38
+ ui,
39
+ modelRegistry: ctx?.modelRegistry,
40
+ sessionId: ctx?.sessionId,
41
+ lifecycleBoundary,
42
+ isProjectTrusted: () => projectTrusted,
43
+ sessionManager: {
44
+ getHeader: () => header,
45
+ getBranch: () => branchSnapshot.slice(),
46
+ getSessionFile: () => sessionFile,
47
+ },
48
+ };
49
+ }
50
+
51
+ /**
52
+ * Run at most one materialization at a time without returning its Promise to
53
+ * Pi's awaited extension hook chain. Triggers that arrive while a run is in
54
+ * flight collapse into one rerun against the latest ExtensionContext.
55
+ */
56
+ export function createDetachedMaterializer(materialize, onError = () => {}) {
57
+ let running = false;
58
+ let queued = false;
59
+ let latestContext;
60
+
61
+ const drain = async () => {
62
+ try {
63
+ while (queued) {
64
+ const ctx = latestContext;
65
+ queued = false;
66
+ latestContext = undefined;
67
+ try {
68
+ await materialize(ctx);
69
+ } catch (error) {
70
+ try {
71
+ onError(error, ctx);
72
+ } catch {
73
+ // Error reporting is best-effort and must never create an
74
+ // unhandled rejection or re-enter Pi's lifecycle chain.
75
+ }
76
+ }
77
+ }
78
+ } finally {
79
+ running = false;
80
+ // Preserve a trigger that arrived after the loop observed `queued` but
81
+ // before this finally block ran.
82
+ if (queued) kick();
83
+ }
84
+ };
85
+
86
+ const kick = () => {
87
+ if (running) return;
88
+ running = true;
89
+ // Yield a full event-loop turn. A microtask would let synchronous branch
90
+ // selection run before Pi resumes its provider/TUI foreground pipeline.
91
+ setImmediate(() => {
92
+ void drain();
93
+ });
94
+ };
95
+
96
+ return (ctx) => {
97
+ latestContext = ctx;
98
+ queued = true;
99
+ kick();
100
+ };
101
+ }
102
+
103
+ /**
104
+ * Register Pi's lifecycle handshake for Rarebit materialization.
105
+ *
106
+ * `input` carries origin but runs before persistence. `message_end(user)`
107
+ * confirms a user record entered the loop, and `before_provider_request`
108
+ * follows Pi's Session append. `agent_end` records whether its terminal
109
+ * assistant stopped normally; `agent_settled` captures the complete branch
110
+ * only after Pi has exhausted retries, compaction, and queued continuations.
111
+ * ESC instead ends in an `aborted` assistant message and never creates a
112
+ * settlement checkpoint.
113
+ * Extension-origin prompts are excluded from the user-submission trigger.
114
+ */
115
+ export function registerRarebitLifecycle(pi, schedule, options = {}) {
116
+ const inputOrigins = {
117
+ direct: [],
118
+ steer: [],
119
+ followUp: [],
120
+ };
121
+ let persistedUserInputAwaitingProvider = false;
122
+ let selectedUserMessageAwaitingProvider = false;
123
+ let pendingLifecycleBoundary = "owner_request";
124
+ let normalStopAwaitingSettlement = false;
125
+ let firstOwnerInputAwaitingProvider;
126
+ let ownerMessageSeen = false;
127
+ let sessionGeneration = 0;
128
+ let sessionLive = false;
129
+ const MAX_PENDING_INPUT_ORIGINS = 256;
130
+
131
+ const clearInputOrigins = () => {
132
+ for (const queue of Object.values(inputOrigins)) queue.length = 0;
133
+ };
134
+
135
+ const pendingOriginCount = () =>
136
+ Object.values(inputOrigins).reduce(
137
+ (total, queue) => total + queue.length,
138
+ 0,
139
+ );
140
+
141
+ const pruneOldestOrigin = () => {
142
+ const candidates = Object.values(inputOrigins)
143
+ .filter((queue) => queue.length > 0)
144
+ .sort((left, right) => left[0].sequence - right[0].sequence);
145
+ candidates[0]?.shift();
146
+ };
147
+
148
+ let inputSequence = 0;
149
+ const consumeInputOrigin = (text) => {
150
+ // Prefer an exact semantic payload match. If skill/template expansion
151
+ // changed the text, fall back to Pi's delivery order: direct prompt first,
152
+ // then every steer before follow-ups.
153
+ for (const queue of [
154
+ inputOrigins.direct,
155
+ inputOrigins.steer,
156
+ inputOrigins.followUp,
157
+ ]) {
158
+ const index = queue.findIndex((entry) => entry.text === text);
159
+ if (index >= 0) return queue.splice(index, 1)[0];
160
+ }
161
+ for (const queue of [
162
+ inputOrigins.direct,
163
+ inputOrigins.steer,
164
+ inputOrigins.followUp,
165
+ ]) {
166
+ if (queue.length > 0) return queue.shift();
167
+ }
168
+ return undefined;
169
+ };
170
+
171
+ pi.on("session_start", (_event, ctx) => {
172
+ options.onSessionStart?.(ctx);
173
+ clearInputOrigins();
174
+ persistedUserInputAwaitingProvider = false;
175
+ selectedUserMessageAwaitingProvider = false;
176
+ normalStopAwaitingSettlement = false;
177
+ firstOwnerInputAwaitingProvider = undefined;
178
+ ownerMessageSeen = (ctx?.sessionManager?.getBranch?.() ?? []).some(
179
+ (entry) => entry?.type === "message" && entry?.message?.role === "user",
180
+ );
181
+ sessionGeneration += 1;
182
+ sessionLive = true;
183
+ // Session attachment initializes exact-session and title bookkeeping only.
184
+ // It must never derive a Summary: resume/reload changes the adapter, not
185
+ // the selected Session evidence.
186
+ });
187
+
188
+ pi.on("agent_start", () => {
189
+ // A retry, compaction pass, or queued continuation starts a new agent run;
190
+ // an earlier stop is not the fully settled branch.
191
+ normalStopAwaitingSettlement = false;
192
+ });
193
+
194
+ pi.on("input", (event) => {
195
+ const bucket =
196
+ event?.streamingBehavior === "steer"
197
+ ? "steer"
198
+ : event?.streamingBehavior === "followUp"
199
+ ? "followUp"
200
+ : "direct";
201
+ inputOrigins[bucket].push({
202
+ source: event?.source,
203
+ bucket,
204
+ text: typeof event?.text === "string" ? event.text : "",
205
+ sequence: inputSequence,
206
+ });
207
+ inputSequence += 1;
208
+ if (pendingOriginCount() > MAX_PENDING_INPUT_ORIGINS) pruneOldestOrigin();
209
+ });
210
+
211
+ pi.on("message_end", (event) => {
212
+ if (!isUserMessage(event)) return;
213
+ // Pi invokes this hook before it appends the entry. The later provider
214
+ // boundary is the first point where an activity consumer may read the
215
+ // new exact active branch without missing this selected user occurrence.
216
+ selectedUserMessageAwaitingProvider = true;
217
+ const origin = consumeInputOrigin(messageText(event));
218
+ if (isUserSubmissionOrigin(origin)) {
219
+ persistedUserInputAwaitingProvider = true;
220
+ pendingLifecycleBoundary = "owner_request";
221
+ if (!ownerMessageSeen && origin.bucket === "direct") {
222
+ ownerMessageSeen = true;
223
+ firstOwnerInputAwaitingProvider = {
224
+ text: messageText(event),
225
+ source: origin.source,
226
+ sourceEntryId:
227
+ typeof event?.message?.id === "string"
228
+ ? event.message.id
229
+ : typeof event?.id === "string"
230
+ ? event.id
231
+ : null,
232
+ };
233
+ }
234
+ }
235
+ });
236
+
237
+ pi.on("session_tree", (event, ctx) => {
238
+ try {
239
+ options.onSessionTree?.(ctx, event);
240
+ } catch {
241
+ // Tree recency is an optional projection, not materialization truth.
242
+ }
243
+ });
244
+
245
+ pi.on("before_provider_request", (_event, ctx) => {
246
+ if (selectedUserMessageAwaitingProvider) {
247
+ selectedUserMessageAwaitingProvider = false;
248
+ try {
249
+ options.onSelectedUserPersisted?.(ctx);
250
+ } catch {
251
+ // Activity/UI projections are optional and must not alter provider I/O.
252
+ }
253
+ }
254
+ if (!persistedUserInputAwaitingProvider) return;
255
+ persistedUserInputAwaitingProvider = false;
256
+ // Pi persists message_end(user) before this provider boundary. Snapshot
257
+ // the evidence coordinates now so a later Session switch cannot retarget
258
+ // an already detached materialization.
259
+ const generation = sessionGeneration;
260
+ const snapshot = snapshotMaterializationContext(
261
+ ctx,
262
+ () => sessionLive && sessionGeneration === generation,
263
+ pendingLifecycleBoundary,
264
+ );
265
+ pendingLifecycleBoundary = "owner_request";
266
+ schedule(snapshot);
267
+ if (firstOwnerInputAwaitingProvider) {
268
+ const ownerMessage = firstOwnerInputAwaitingProvider;
269
+ firstOwnerInputAwaitingProvider = undefined;
270
+ try {
271
+ options.onFirstPersistedOwnerMessage?.(snapshot, ownerMessage);
272
+ } catch {
273
+ // Auto-title is a detached human-facing projection. Failure must not
274
+ // enter Pi's provider path or change summary settlement.
275
+ }
276
+ }
277
+ });
278
+
279
+ pi.on("agent_end", (event) => {
280
+ // Record the latest run outcome, but do not project yet: Pi may still
281
+ // retry, compact, or drain a continuation before `agent_settled`.
282
+ const terminalAssistant = [...(event?.messages ?? [])]
283
+ .reverse()
284
+ .find((message) => message?.role === "assistant");
285
+ normalStopAwaitingSettlement = terminalAssistant?.stopReason === "stop";
286
+ if (terminalAssistant?.stopReason === "aborted") {
287
+ clearInputOrigins();
288
+ persistedUserInputAwaitingProvider = false;
289
+ selectedUserMessageAwaitingProvider = false;
290
+ firstOwnerInputAwaitingProvider = undefined;
291
+ }
292
+ });
293
+
294
+ pi.on("agent_settled", (_event, ctx) => {
295
+ try {
296
+ options.onAgentSettled?.(ctx);
297
+ } catch {
298
+ // Activity/UI projections are optional and must not alter settlement.
299
+ }
300
+ if (!normalStopAwaitingSettlement) return;
301
+ normalStopAwaitingSettlement = false;
302
+
303
+ const generation = sessionGeneration;
304
+ schedule(
305
+ snapshotMaterializationContext(
306
+ ctx,
307
+ () => sessionLive && sessionGeneration === generation,
308
+ "agent_settled",
309
+ ),
310
+ );
311
+ });
312
+
313
+ pi.on("session_shutdown", () => {
314
+ clearInputOrigins();
315
+ persistedUserInputAwaitingProvider = false;
316
+ selectedUserMessageAwaitingProvider = false;
317
+ firstOwnerInputAwaitingProvider = undefined;
318
+ normalStopAwaitingSettlement = false;
319
+ sessionLive = false;
320
+ options.onSessionShutdown?.();
321
+ });
322
+ }
@@ -0,0 +1,48 @@
1
+ // Native Rarebit selection is the source of these recency coordinates. This
2
+ // module deliberately has no clock or renderer: consumers choose how (and
3
+ // whether) to project an age from an exact active-branch snapshot.
4
+
5
+ function latest(occurrences, matches) {
6
+ for (let index = occurrences.length - 1; index >= 0; index -= 1) {
7
+ const occurrence = occurrences[index];
8
+ if (!matches(occurrence)) continue;
9
+ return Object.freeze({
10
+ sourceEntryId: occurrence.sourceEntryId ?? null,
11
+ timestamp:
12
+ typeof occurrence.timestamp === "string" ? occurrence.timestamp : null,
13
+ });
14
+ }
15
+ return null;
16
+ }
17
+
18
+ /**
19
+ * Projects the latest selected user-role and normal assistant-stop evidence.
20
+ * "Latest" is active-branch order, not a clock-sorted guess. A missing or
21
+ * invalid native timestamp stays unavailable to a renderer; callers must not
22
+ * substitute materialization time, mtime, or an earlier occurrence.
23
+ */
24
+ export function projectRarebitSessionActivity(selection) {
25
+ const occurrences = Array.isArray(selection?.occurrences)
26
+ ? selection.occurrences
27
+ : [];
28
+ return Object.freeze({
29
+ schemaVersion: 1,
30
+ selectorVersion:
31
+ typeof selection?.manifest?.selectorVersion === "string"
32
+ ? selection.manifest.selectorVersion
33
+ : null,
34
+ selectionManifestHash:
35
+ typeof selection?.manifestHash === "string"
36
+ ? selection.manifestHash
37
+ : null,
38
+ latestUser: latest(
39
+ occurrences,
40
+ (occurrence) => occurrence?.role === "user",
41
+ ),
42
+ latestAgentStop: latest(
43
+ occurrences,
44
+ (occurrence) =>
45
+ occurrence?.role === "assistant" && occurrence?.outcome === "stop",
46
+ ),
47
+ });
48
+ }