@awebai/oats-pi 0.56.0 → 0.57.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/README.md +6 -2
- package/extension/index.ts +50 -7
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -19,8 +19,12 @@ from the same release tag). This bridge registers no operational tools. It only:
|
|
|
19
19
|
make pi refuse it, because pi's interactive mode sends the initial
|
|
20
20
|
message with no streamingBehavior. @awebai/pi has no such handler;
|
|
21
21
|
- journals compaction summaries and sends resume nudges when the active
|
|
22
|
-
knowledge capability created `STATE.md`/`log.md`.
|
|
23
|
-
|
|
22
|
+
knowledge capability created `STATE.md`/`log.md`. A startup gets the nudge
|
|
23
|
+
only when pi kept an earlier session of the home (pi files a session once
|
|
24
|
+
it holds a message), so a fresh home's first turn is its opening task. What
|
|
25
|
+
`STATE.md` says decides nothing, and the bridge writes nothing to tell.
|
|
26
|
+
Knowledge ownership, read/capture instructions, judgment and delivery
|
|
27
|
+
remain capability-owned.
|
|
24
28
|
|
|
25
29
|
Skill resolution itself is owned by the kernel: spawn materializes the exact
|
|
26
30
|
kernel + soul + active-capability set into each instance's `.agents/skills`
|
package/extension/index.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
*/
|
|
9
9
|
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
10
10
|
import { appendLogEntry, PACKAGED_SKILLS_DIR } from "./core-loader.mjs";
|
|
11
|
-
import { existsSync,
|
|
11
|
+
import { closeSync, existsSync, openSync, readdirSync, readSync, statSync } from "node:fs";
|
|
12
12
|
import { join } from "node:path";
|
|
13
13
|
|
|
14
14
|
export default function (pi: ExtensionAPI) {
|
|
@@ -44,13 +44,12 @@ export default function (pi: ExtensionAPI) {
|
|
|
44
44
|
}, { deliverAs: "steer" });
|
|
45
45
|
});
|
|
46
46
|
|
|
47
|
-
pi.on("session_start", async (event) => {
|
|
47
|
+
pi.on("session_start", async (event, ctx) => {
|
|
48
48
|
if (event.reason !== "startup" && event.reason !== "resume" && event.reason !== "new") return;
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
if (event.reason === "startup" && !touched) return;
|
|
49
|
+
if (!existsSync(join(agentHome!, "STATE.md"))) return;
|
|
50
|
+
// A home's first startup gets no resume nudge, whatever a capability
|
|
51
|
+
// seeded into its STATE.md.
|
|
52
|
+
if (event.reason === "startup" && !hasWorkedBefore(agentHome!, ctx)) return;
|
|
54
53
|
pi.sendMessage({
|
|
55
54
|
customType: "oats-memory",
|
|
56
55
|
content: `You are agent instance home ${agentHome}. Read ./STATE.md and the recent entries of ./log.md now, then continue from STATE.md's "Next" section. Keep STATE.md current as you work.`,
|
|
@@ -60,6 +59,50 @@ export default function (pi: ExtensionAPI) {
|
|
|
60
59
|
}
|
|
61
60
|
}
|
|
62
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Whether pi kept an earlier session of this home. pi files a session only
|
|
64
|
+
* once it holds a message (an assistant one in pi 0.80.3,
|
|
65
|
+
* core/session-manager.js:641-665; any in pi 1.x), groups sessions by working
|
|
66
|
+
* directory, and starts each file with a header naming its cwd and start time
|
|
67
|
+
* (docs/session-format.md). A session that started before the home was
|
|
68
|
+
* created belongs to an earlier instance of the same name. Where the birth
|
|
69
|
+
* time is unknown (0, or ctime, which libuv reports without statx) none is
|
|
70
|
+
* told apart. A file last written before the home was created is skipped
|
|
71
|
+
* unread. Unreadable history reads as none: memory automation must never
|
|
72
|
+
* break a session.
|
|
73
|
+
*/
|
|
74
|
+
function hasWorkedBefore(home: string, ctx: ExtensionContext): boolean {
|
|
75
|
+
try {
|
|
76
|
+
const sessions = ctx.sessionManager;
|
|
77
|
+
const dir = sessions.getSessionDir();
|
|
78
|
+
const current = sessions.getSessionFile();
|
|
79
|
+
const cwd = sessions.getCwd();
|
|
80
|
+
const { birthtimeMs, ctimeMs } = statSync(home);
|
|
81
|
+
const born = birthtimeMs !== ctimeMs ? birthtimeMs : 0;
|
|
82
|
+
const head = Buffer.alloc(16 * 1024);
|
|
83
|
+
for (const name of readdirSync(dir)) {
|
|
84
|
+
const file = join(dir, name);
|
|
85
|
+
if (!name.endsWith(".jsonl") || file === current) continue;
|
|
86
|
+
if (statSync(file).mtimeMs < born) continue;
|
|
87
|
+
const header = sessionHeader(file, head);
|
|
88
|
+
if (header?.type === "session" && header.cwd === cwd && typeof header.timestamp === "string" && Date.parse(header.timestamp) >= born) return true;
|
|
89
|
+
}
|
|
90
|
+
} catch { /* no history read */ }
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** A session file's first line, parsed, or undefined. Only its head is read. */
|
|
95
|
+
function sessionHeader(file: string, head: Buffer): { type?: unknown; cwd?: unknown; timestamp?: unknown } | undefined {
|
|
96
|
+
let fd: number | undefined;
|
|
97
|
+
try {
|
|
98
|
+
fd = openSync(file, "r");
|
|
99
|
+
const text = head.subarray(0, readSync(fd, head, 0, head.length, 0)).toString("utf8");
|
|
100
|
+
const end = text.indexOf("\n");
|
|
101
|
+
return end < 0 ? undefined : JSON.parse(text.slice(0, end));
|
|
102
|
+
} catch { return undefined; }
|
|
103
|
+
finally { if (fd !== undefined) closeSync(fd); }
|
|
104
|
+
}
|
|
105
|
+
|
|
63
106
|
/**
|
|
64
107
|
* The opening task runs once, after any turn another extension starts, as
|
|
65
108
|
* pi's input processing made it. pi sends the launch's `@TASK.md` as the
|
package/package.json
CHANGED