portable-agent-layer 0.79.2 → 0.79.3

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.
@@ -59,7 +59,6 @@
59
59
  "relationship": { "type": "boolean", "description": "Recent relationship notes." },
60
60
  "failurePatterns": { "type": "boolean", "description": "Recurring failure patterns." },
61
61
  "projects": { "type": "boolean", "description": "Active projects list." },
62
- "sessionIntelligence": { "type": "boolean", "description": "Algorithm reflection stats and this project's self-observations." },
63
62
  "handoff": { "type": "boolean", "description": "Handoff note from the previous session." },
64
63
  "selfModel": { "type": "boolean", "description": "The assistant's self-model synthesis." },
65
64
  "contextualSteering": { "type": "boolean", "description": "Prompt-time steering self-checks (see the steering section)." },
@@ -21,7 +21,6 @@
21
21
  "relationship": true,
22
22
  "failurePatterns": true,
23
23
  "projects": true,
24
- "sessionIntelligence": true,
25
24
  "handoff": true,
26
25
  "selfModel": true,
27
26
  "contextualSteering": true,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "portable-agent-layer",
3
- "version": "0.79.2",
3
+ "version": "0.79.3",
4
4
  "description": "PAL — Portable Agent Layer: persistent personal context for AI coding assistants",
5
5
  "type": "module",
6
6
  "bin": {
@@ -71,8 +71,20 @@ function readHandoffs(): Record<string, StoredHandoff> {
71
71
  }
72
72
  }
73
73
 
74
+ function isWithinReadWindow(entry: StoredHandoff): boolean {
75
+ if (!entry.timestamp) return false;
76
+ return Date.now() - new Date(entry.timestamp).getTime() <= HANDOFF_STALE_MS;
77
+ }
78
+
74
79
  function writeHandoffs(handoffs: Record<string, StoredHandoff>): void {
75
- writeFileSync(handoffPath(), JSON.stringify(handoffs, null, 2), "utf-8");
80
+ const readable = Object.entries(handoffs).filter(([, entry]) =>
81
+ isWithinReadWindow(entry)
82
+ );
83
+ writeFileSync(
84
+ handoffPath(),
85
+ JSON.stringify(Object.fromEntries(readable), null, 2),
86
+ "utf-8"
87
+ );
76
88
  }
77
89
 
78
90
  export function structuredHandoff(parts: {
@@ -232,6 +232,7 @@ export async function captureSessionIntelligence(
232
232
  title,
233
233
  summary,
234
234
  insights,
235
+ session: sessionId,
235
236
  });
236
237
  }
237
238
 
@@ -117,8 +117,11 @@ export function loadRelationshipContext(): string {
117
117
  }
118
118
  }
119
119
 
120
- /** Load session intelligence from compact synthesis state */
121
- function loadSessionIntelligence(): string {
120
+ /**
121
+ * Off the startup context until the reflection stats are reliable enough to act on.
122
+ * @lintignore exercised directly by test/context-build.test.ts
123
+ */
124
+ export function loadSessionIntelligence(): string {
122
125
  try {
123
126
  const p = resolve(paths.state(), "synthesis.json");
124
127
  if (!existsSync(p)) return "";
@@ -211,9 +214,6 @@ export function buildSystemReminder(
211
214
  !skipSemiStatic && settings.isEnabled("opinions") ? loadOpinionContext() : "";
212
215
  const selfModel =
213
216
  !skipSemiStatic && settings.isEnabled("selfModel") ? loadSelfModel() : "";
214
- const intelligence = settings.isEnabled("sessionIntelligence")
215
- ? loadSessionIntelligence()
216
- : "";
217
217
  const handoff =
218
218
  !opts.withoutHandoff && settings.isEnabled("handoff")
219
219
  ? loadHandoffContext(sessionDir())
@@ -231,7 +231,6 @@ export function buildSystemReminder(
231
231
  if (selfModel) parts.push(selfModel);
232
232
  if (wisdom) parts.push(wisdom);
233
233
  if (opinions) parts.push(opinions);
234
- if (intelligence) parts.push(intelligence);
235
234
  if (relationship) parts.push(relationship);
236
235
  if (activeProjects) parts.push(activeProjects);
237
236
  if (failures) parts.push(failures);
@@ -22,7 +22,12 @@ import { captureWorkSession } from "../handlers/work-session";
22
22
  import { spawnDetachedInference } from "./detached-inference";
23
23
  import { logDebug, logError } from "./log";
24
24
  import { assets, ensureDir, paths } from "./paths";
25
- import { extractContent, extractLastAssistant, parseMessages } from "./transcript";
25
+ import {
26
+ extractContent,
27
+ extractLastAssistant,
28
+ parseMessages,
29
+ withFinalReply,
30
+ } from "./transcript";
26
31
 
27
32
  interface RunStopHandlersOptions {
28
33
  lastAssistantMessage?: string;
@@ -31,11 +36,13 @@ interface RunStopHandlersOptions {
31
36
 
32
37
  /** Run all stop handlers with a transcript string */
33
38
  export async function runStopHandlers(
34
- transcript: string,
39
+ fileTranscript: string,
35
40
  options: RunStopHandlersOptions = {}
36
41
  ): Promise<void> {
37
- const messages = parseMessages(transcript);
38
- if (messages.length < 2) return;
42
+ const fileMessages = parseMessages(fileTranscript);
43
+ if (fileMessages.length < 2) return;
44
+ const messages = withFinalReply(fileMessages, options.lastAssistantMessage);
45
+ const transcript = JSON.stringify(messages);
39
46
 
40
47
  logDebug("runStopHandlers", `Running handlers (${messages.length} messages)`);
41
48
 
@@ -89,6 +89,15 @@ export function extractLastAssistant(messages: Message[]): Message | undefined {
89
89
  return messages.filter((m) => m.role === "assistant").pop();
90
90
  }
91
91
 
92
+ /** The agent hands its final reply to the Stop hook before that reply reaches the transcript file. */
93
+ export function withFinalReply(messages: Message[], finalReply?: string): Message[] {
94
+ const reply = finalReply?.trim();
95
+ if (!reply) return messages;
96
+ const last = messages.at(-1);
97
+ if (last?.role === "assistant" && extractContent(last).includes(reply)) return messages;
98
+ return [...messages, { role: "assistant", content: reply }];
99
+ }
100
+
92
101
  /** Get the last user message from a messages array */
93
102
  export function extractLastUser(messages: Message[]): Message | undefined {
94
103
  return messages.filter((m) => m.role === "user").pop();
@@ -145,6 +145,7 @@ interface ProjectHistoryEntry {
145
145
  title: string;
146
146
  summary: string;
147
147
  insights: string;
148
+ session?: string;
148
149
  }
149
150
 
150
151
  /**
@@ -160,11 +161,28 @@ function historyFileFor(cwd: string): string {
160
161
  : resolve(paths.unboundHistory(), `${defaultSlug(cwd)}.jsonl`);
161
162
  }
162
163
 
163
- /** Append a learning entry to the history file owning this cwd */
164
+ function withoutSession(lines: string[], session: string): string[] {
165
+ return lines.filter((line) => {
166
+ try {
167
+ return (JSON.parse(line) as ProjectHistoryEntry).session !== session;
168
+ } catch {
169
+ return true;
170
+ }
171
+ });
172
+ }
173
+
174
+ /** A session is summarised again as it grows; its entry is replaced, not repeated. */
164
175
  export function appendProjectHistory(cwd: string, entry: ProjectHistoryEntry): void {
165
176
  const historyPath = historyFileFor(cwd);
166
177
  ensureDir(dirname(historyPath));
167
- appendFileSync(historyPath, `${JSON.stringify(entry)}\n`, "utf-8");
178
+ const line = JSON.stringify(entry);
179
+ if (!entry.session || !existsSync(historyPath)) {
180
+ appendFileSync(historyPath, `${line}\n`, "utf-8");
181
+ return;
182
+ }
183
+ const lines = readFileSync(historyPath, "utf-8").split("\n").filter(Boolean);
184
+ const kept = withoutSession(lines, entry.session);
185
+ writeFileSync(historyPath, `${[...kept, line].join("\n")}\n`, "utf-8");
168
186
  }
169
187
 
170
188
  /** Read the session history for a given cwd */