@matthewfl/pi-contemplator 0.0.1 → 0.0.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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matthewfl/pi-contemplator",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "A Pi extension that keeps long-running agentic sessions on track with background memory, contemplation, and structural review.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -27,7 +27,16 @@ type Intervention =
|
|
|
27
27
|
| { kind: "probe"; question: string }
|
|
28
28
|
| { kind: "review"; request: Omit<StructuralReviewRequest, "createdAt" | "requestedBy"> };
|
|
29
29
|
|
|
30
|
-
type ReviewerSession = {
|
|
30
|
+
type ReviewerSession = {
|
|
31
|
+
scope: StructuralReviewRequest["scope"];
|
|
32
|
+
history: AgentMessage[];
|
|
33
|
+
/** Latest compact checkpoint; older transcript content stays in referenced append-only entries. */
|
|
34
|
+
checkpointEntryId?: string;
|
|
35
|
+
/** Reviewer message entries appended since checkpointEntryId. */
|
|
36
|
+
messageEntryIds: string[];
|
|
37
|
+
/** Message-entry ids already folded into `history` in this live session (restore re-walk guard). */
|
|
38
|
+
foldedEntryIds: Set<string>;
|
|
39
|
+
};
|
|
31
40
|
|
|
32
41
|
type QueueStructuralReviewOptions = {
|
|
33
42
|
ctx: MemoryUpdateCtx;
|
|
@@ -252,17 +261,42 @@ export class Contemplator {
|
|
|
252
261
|
}
|
|
253
262
|
}
|
|
254
263
|
if (entry.customType === OM_REVIEWER_STATE && entry.data && typeof entry.data === "object") {
|
|
255
|
-
const state = entry.data as { reviewRequestId?: unknown; scope?: unknown; history?: unknown };
|
|
256
|
-
if (typeof state.reviewRequestId === "string" && (state.scope === "workflow" || state.scope === "software")
|
|
257
|
-
|
|
264
|
+
const state = entry.data as { version?: unknown; reviewRequestId?: unknown; scope?: unknown; history?: unknown; messageEntryIds?: unknown };
|
|
265
|
+
if (typeof state.reviewRequestId === "string" && (state.scope === "workflow" || state.scope === "software")) {
|
|
266
|
+
if (state.version === 1 && Array.isArray(state.history)) {
|
|
267
|
+
// Backward compatibility for the old, expensive full-transcript snapshots.
|
|
268
|
+
this.reviewerSessions.set(state.reviewRequestId, {
|
|
269
|
+
scope: state.scope,
|
|
270
|
+
history: state.history.filter((message): message is AgentMessage => !!message && typeof message === "object"),
|
|
271
|
+
checkpointEntryId: entry.id,
|
|
272
|
+
messageEntryIds: [],
|
|
273
|
+
foldedEntryIds: new Set(),
|
|
274
|
+
});
|
|
275
|
+
} else if (state.version === 2 && Array.isArray(state.messageEntryIds) && state.messageEntryIds.every((id) => typeof id === "string")) {
|
|
276
|
+
// V2 checkpoints contain references only. The referenced state/messages
|
|
277
|
+
// have already been folded while walking this append-only branch.
|
|
278
|
+
const session = this.reviewerSessions.get(state.reviewRequestId) ?? { scope: state.scope, history: [], messageEntryIds: [], foldedEntryIds: new Set() };
|
|
279
|
+
session.checkpointEntryId = entry.id;
|
|
280
|
+
session.messageEntryIds = [];
|
|
281
|
+
this.reviewerSessions.set(state.reviewRequestId, session);
|
|
282
|
+
}
|
|
258
283
|
}
|
|
259
284
|
}
|
|
260
285
|
if (entry.customType === OM_REVIEWER_MESSAGE && entry.data && typeof entry.data === "object") {
|
|
261
286
|
const data = entry.data as { reviewRequestId?: unknown; scope?: unknown; message?: unknown };
|
|
262
287
|
if (typeof data.reviewRequestId === "string" && (data.scope === "workflow" || data.scope === "software") && data.message && typeof data.message === "object") {
|
|
263
|
-
|
|
264
|
-
session
|
|
265
|
-
|
|
288
|
+
let session = this.reviewerSessions.get(data.reviewRequestId);
|
|
289
|
+
if (!session) {
|
|
290
|
+
session = { scope: data.scope, history: [], messageEntryIds: [], foldedEntryIds: new Set() };
|
|
291
|
+
this.reviewerSessions.set(data.reviewRequestId, session);
|
|
292
|
+
}
|
|
293
|
+
// restore() re-walks the whole branch whenever the tip moves
|
|
294
|
+
// (turn_end), so fold each message entry at most once per live session.
|
|
295
|
+
if (!session.foldedEntryIds.has(entry.id)) {
|
|
296
|
+
session.foldedEntryIds.add(entry.id);
|
|
297
|
+
session.history.push(data.message as AgentMessage);
|
|
298
|
+
session.messageEntryIds.push(entry.id);
|
|
299
|
+
}
|
|
266
300
|
}
|
|
267
301
|
}
|
|
268
302
|
if (entry.customType === CONTEMPLATOR_SUGGESTION && entry.data && typeof entry.data === "object") {
|
|
@@ -572,7 +606,7 @@ export class Contemplator {
|
|
|
572
606
|
this.resumedReviewIds.add(request.id);
|
|
573
607
|
this.inFlightReviewIds.add(request.id);
|
|
574
608
|
if (key) this.inFlightReviewKeys.add(key);
|
|
575
|
-
const session = this.reviewerSessions.get(request.id) ?? { scope: request.scope, history: options.history ?? [] };
|
|
609
|
+
const session = this.reviewerSessions.get(request.id) ?? { scope: request.scope, history: options.history ?? [], messageEntryIds: [], foldedEntryIds: new Set() };
|
|
576
610
|
this.reviewerSessions.set(request.id, session);
|
|
577
611
|
const task = this.runtime.launchReviewTask(ctx, async () => {
|
|
578
612
|
try {
|
|
@@ -586,9 +620,12 @@ export class Contemplator {
|
|
|
586
620
|
if (sessionGeneration !== this.sessionGeneration || !this.reviewIsPending(ctx, request.id)) return;
|
|
587
621
|
for (const message of messages) {
|
|
588
622
|
session.history.push(message);
|
|
589
|
-
this.
|
|
623
|
+
const entryId = this.appendEntryWithId(ctx, OM_REVIEWER_MESSAGE, { version: 1, reviewRequestId: request.id, scope: request.scope, message }, request.id);
|
|
624
|
+
if (entryId) {
|
|
625
|
+
session.foldedEntryIds.add(entryId);
|
|
626
|
+
session.messageEntryIds.push(entryId);
|
|
627
|
+
}
|
|
590
628
|
}
|
|
591
|
-
if (messages.length > 0) this.markTipPersisted(ctx);
|
|
592
629
|
},
|
|
593
630
|
});
|
|
594
631
|
if (sessionGeneration !== this.sessionGeneration) {
|
|
@@ -643,10 +680,17 @@ export class Contemplator {
|
|
|
643
680
|
|
|
644
681
|
private persistReviewerStates(ctx: MemoryUpdateCtx): void {
|
|
645
682
|
for (const [reviewRequestId, session] of this.reviewerSessions) {
|
|
646
|
-
if (session.
|
|
647
|
-
this.
|
|
683
|
+
if (session.messageEntryIds.length === 0) continue;
|
|
684
|
+
const checkpointEntryId = this.appendEntryWithId(ctx, OM_REVIEWER_STATE, {
|
|
685
|
+
version: 2,
|
|
686
|
+
reviewRequestId,
|
|
687
|
+
scope: session.scope,
|
|
688
|
+
previousStateEntryId: session.checkpointEntryId,
|
|
689
|
+
messageEntryIds: session.messageEntryIds,
|
|
690
|
+
}, reviewRequestId);
|
|
691
|
+
if (checkpointEntryId) session.checkpointEntryId = checkpointEntryId;
|
|
692
|
+
session.messageEntryIds = [];
|
|
648
693
|
}
|
|
649
|
-
if (this.reviewerSessions.size > 0) this.markTipPersisted(ctx);
|
|
650
694
|
}
|
|
651
695
|
|
|
652
696
|
private async resumePendingReviews(ctx: MemoryUpdateCtx): Promise<void> {
|
|
@@ -669,8 +713,27 @@ export class Contemplator {
|
|
|
669
713
|
}
|
|
670
714
|
}
|
|
671
715
|
|
|
672
|
-
private markTipPersisted(ctx: MemoryUpdateCtx):
|
|
716
|
+
private markTipPersisted(ctx: MemoryUpdateCtx): string | undefined {
|
|
673
717
|
this.restoredTipId = (ctx.sessionManager.getBranch() as Entry[]).at(-1)?.id;
|
|
718
|
+
return this.restoredTipId;
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
/**
|
|
722
|
+
* Append a custom entry and return the id of the entry just written, or
|
|
723
|
+
* undefined when it cannot be attributed. pi.appendEntry() does not return
|
|
724
|
+
* the entry id, so the branch is diffed around the synchronous append and the
|
|
725
|
+
* candidate is matched by customType + reviewRequestId. This never credits a
|
|
726
|
+
* concurrently-appended foreign entry reached via the branch tail. A failed
|
|
727
|
+
* attribution is harmless: restore rebuilds transcripts from the durable
|
|
728
|
+
* om.reviewer.message entries themselves and only skips a checkpoint.
|
|
729
|
+
*/
|
|
730
|
+
private appendEntryWithId(ctx: MemoryUpdateCtx, customType: string, data: Record<string, unknown>, reviewRequestId: string): string | undefined {
|
|
731
|
+
const branch = ctx.sessionManager.getBranch() as readonly Entry[];
|
|
732
|
+
const before = branch.length;
|
|
733
|
+
this.pi.appendEntry(customType, data);
|
|
734
|
+
const added = (ctx.sessionManager.getBranch() as readonly Entry[]).slice(before);
|
|
735
|
+
const own = added.find((entry) => entry.customType === customType && (entry.data as { reviewRequestId?: unknown } | undefined)?.reviewRequestId === reviewRequestId);
|
|
736
|
+
return own?.id;
|
|
674
737
|
}
|
|
675
738
|
|
|
676
739
|
private async compactHistory(model: Model<any>, apiKey: string, headers: Record<string, string> | undefined, sessionGeneration: number): Promise<void> {
|
|
@@ -5,7 +5,7 @@ export const OM_REVIEW_REQUEST = "om.review.request";
|
|
|
5
5
|
export const OM_REVIEW_RESULT = "om.review.result";
|
|
6
6
|
/** Persisted assistant/tool output from a short-lived structural reviewer. */
|
|
7
7
|
export const OM_REVIEWER_MESSAGE = "om.reviewer.message";
|
|
8
|
-
/**
|
|
8
|
+
/** Compact checkpoint referencing reviewer transcript entries across primary-session compaction. */
|
|
9
9
|
export const OM_REVIEWER_STATE = "om.reviewer.state";
|
|
10
10
|
/** Compact proposal notice queued for the primary agent. */
|
|
11
11
|
export const OM_REVIEWER_NOTICE = "om.reviewer.notice";
|