@awebai/oats-pi 0.57.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 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`. Knowledge ownership,
23
- read/capture instructions, judgment and delivery remain capability-owned.
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`
@@ -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, readFileSync } from "node:fs";
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
- const statePath = join(agentHome!, "STATE.md");
50
- if (!existsSync(statePath)) return;
51
- const state = readFileSync(statePath, "utf8");
52
- const touched = !/_No task assigned yet/.test(state) || !/_\(the single next action/.test(state);
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats-pi",
3
- "version": "0.57.0",
3
+ "version": "0.57.1",
4
4
  "description": "OATS pi runtime bridge — memory session events and pre-workspace bootstrap over the runtime-neutral @awebai/oats kernel",
5
5
  "keywords": [
6
6
  "pi-package",