ace-handbook-integration-pi 0.6.0 → 0.7.0

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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 4e1e1f851408ab94551b1f23f43b258fdd064b8df2790f0bb2a80ab05363983a
4
- data.tar.gz: 7fa298d85b1ce4844320dc980d323b28079c2a05a37dd26ea3dc46d159a41f47
3
+ metadata.gz: 9a4d798b4819221f1fc95c725700fc5911859b62a197ce3973389d18e208f6d4
4
+ data.tar.gz: 66c216a3a9c9ebd4164b1e6ea2e3d096b0e0f4468d3efc9ebb7941e6f652be47
5
5
  SHA512:
6
- metadata.gz: 75c4ed2da5fce4f238a8e8d7b401ee4ac524548f15fb3e5f574a9bd5f526f7b73eecbefc80241e814e6ac71d055253e619dffb2c87443b8c949f94b34ce304d1
7
- data.tar.gz: 53bff4c326bad616aff1ddfcf417d16496cd3177f38c6cb35c95d40b2bd5fdcbe6b923883e55241af306c5f03671b96d1538dd2c244417af5928b96fd769560c
6
+ metadata.gz: 1b2818ac03f7a22254d4247ae0a6374e738dbdad853ebf06d08c92c76619bc746ed0d9365f42590d1057322bdede120459ec6de718b795cd702182f24a1d3047
7
+ data.tar.gz: 8b31425debbb283d7694fbb8e38222271f55762f4eae14c59f84e3719d97d41b96d85e6921605e5c14e76a1564b4593513324fa5d65a3f8518794343d8f79533
data/CHANGELOG.md CHANGED
@@ -4,6 +4,12 @@ All notable changes to ace-handbook-integration-pi will be documented in this fi
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [0.7.0] - 2026-10-23
8
+
9
+ ### Added
10
+
11
+ - Add durable in-process semantic eyes, coalescing, explicit deadlines, bounded budgets and actual Pi notification acceptance; retire the actor’s old loops when enabled.
12
+
7
13
 
8
14
  ## [0.6.0] - 2026-10-20
9
15
 
@@ -18,6 +18,7 @@
18
18
 
19
19
  import { clearInterval, setInterval } from "node:timers";
20
20
 
21
+ import { registerEyes } from "./wake/eyes-runtime.js";
21
22
  import { WakeDispatcher } from "./wake/wake-dispatcher.js";
22
23
  import { parseWakeCommand } from "./wake/wake-command-parser.js";
23
24
  import { createLoopPort } from "./wake/loop-subscription.js";
@@ -47,6 +48,7 @@ export default function (pi, ports = {}) {
47
48
  /** @type {{registry: WakeRegistry} | undefined} */
48
49
  let runtime;
49
50
 
51
+ let eyes;
50
52
  const getRuntime = () => runtime;
51
53
  const bindRuntime = (ctx) => {
52
54
  runtime?.registry.dispose();
@@ -57,6 +59,9 @@ export default function (pi, ports = {}) {
57
59
  pi.registerCommand("loop", {
58
60
  description: "Manage named timer loops that wake this agent (add | list | remove)",
59
61
  handler: async (args, ctx) => {
62
+ if (eyes.active() && args.trim().startsWith("add")) {
63
+ ctx.ui?.notify?.("Legacy loops are retired while eyes owns observation", "error"); return;
64
+ }
60
65
  await handleCommand(ctx, "loop", args, getRuntime, bindRuntime);
61
66
  },
62
67
  });
@@ -64,6 +69,9 @@ export default function (pi, ports = {}) {
64
69
  pi.registerCommand("watch", {
65
70
  description: "Manage named file watches that wake this agent (add | list | remove)",
66
71
  handler: async (args, ctx) => {
72
+ if (eyes.active() && args.trim().startsWith("add")) {
73
+ ctx.ui?.notify?.("Legacy watches are retired while eyes owns observation", "error"); return;
74
+ }
67
75
  await handleCommand(ctx, "watch", args, getRuntime, bindRuntime);
68
76
  },
69
77
  });
@@ -78,6 +86,16 @@ export default function (pi, ports = {}) {
78
86
  bindRuntime(ctx);
79
87
  });
80
88
 
89
+ eyes = registerEyes(pi, { ...ports.eyes,
90
+ retireLegacy: () => {
91
+ if (!runtime) return;
92
+ const list = runtime.registry.list();
93
+ for (const entry of list) {
94
+ if (entry.kind === "loop") runtime.registry.removeLoop(entry.name);
95
+ else runtime.registry.removeWatch(entry.name);
96
+ }
97
+ } });
98
+
81
99
  // A run has begun: wakes dispatched concurrently during the idle-to-running
82
100
  // transition can now enter the follow-up queue safely, so the registry
83
101
  // drains anything it serialized.
@@ -0,0 +1,145 @@
1
+ import fs from "node:fs";
2
+ import path from "node:path";
3
+ import { execFile } from "node:child_process";
4
+ import { promisify } from "node:util";
5
+ import { Eyes, EYES_ENTRY } from "./eyes.js";
6
+
7
+ const execute = promisify(execFile);
8
+
9
+ /** Existing native sync owns canonical import; HITL owns event projection.
10
+ * No Ruby parsing, private text, shell command strings or model work here. */
11
+ export async function observeHitl(cwd, config) {
12
+ const options = { cwd, timeout: config.timeoutMs, maxBuffer: 1024 * 1024, encoding: "utf8" };
13
+ for (const route of config.syncRoutes) {
14
+ const { stdout } = await execute("ace-hitl-hermes", ["sync", "--config", route], options);
15
+ const result = JSON.parse(stdout);
16
+ if (!Array.isArray(result.errors) || result.errors.length) throw new Error("Native synchronization incomplete");
17
+ }
18
+ const { stdout } = await execute("ace-overseer", ["observe", "--owner", config.recipient, "--scope", config.scope, "--limit", String(config.maxEvents)], options);
19
+ return JSON.parse(stdout);
20
+ }
21
+
22
+ export function registerEyes(pi, { retireLegacy, observe = observeHitl, now = Date.now,
23
+ schedule = (fn, ms) => setInterval(fn, ms), cancel = clearInterval } = {}) {
24
+ let eyes, timer, attempt, compacting = false, running = false;
25
+ const stopTimer = () => { if (timer !== undefined) cancel(timer); timer = undefined; };
26
+ function arm() {
27
+ stopTimer();
28
+ if (eyes?.s.enabled && !eyes.s.retired && !eyes.s.breaker) {
29
+ timer = schedule(() => { void eyes.tick(); }, eyes.s.config.intervalSeconds * 1000);
30
+ timer?.unref?.();
31
+ }
32
+ }
33
+ const bind = (ctx) => {
34
+ stopTimer(); eyes?.dispose(); attempt = undefined; compacting = false; running = false;
35
+ const entries = ctx.sessionManager.getBranch();
36
+ const snapshot = [...entries].reverse().find((e) => e.type === "custom" && e.customType === EYES_ENTRY)?.data;
37
+ if (snapshot?.enabled || snapshot?.retired) retireLegacy?.();
38
+ const ownerCwd = path.resolve(ctx.cwd);
39
+ if (snapshot?.ownerCwd && snapshot.ownerCwd !== ownerCwd) {
40
+ eyes = undefined;
41
+ ctx.ui?.setStatus?.("ace-eyes", "eyes blocked: session belongs to another project cwd");
42
+ throw new Error("Eyes project owner mismatch; preserve this actor and use its original project session");
43
+ }
44
+ eyes = new Eyes({ snapshot, now,
45
+ save: (data) => pi.appendEntry(EYES_ENTRY, { ...data, ownerCwd }),
46
+ status: (state) => {
47
+ ctx.ui?.setStatus?.("ace-eyes", `eyes ${state.retired ? "retired" : state.breaker?.reason || (state.enabled ? "observing" : "off")} pending:${state.pending}`);
48
+ if (state.breaker || state.retired) stopTimer();
49
+ },
50
+ observe: (config) => observe(ctx.cwd, config),
51
+ canDeliver: () => !compacting && (ctx.isIdle() || running),
52
+ deliveryStatus: () => {
53
+ if (attempt?.accepted) return "accepted";
54
+ // sendUserMessage is a void wrapper. Busy/queued is uncertain, not
55
+ // acceptance or rejection. Idle without confirmation is uncertain:
56
+ // async input/auth preflight may still be running, so never blind-retry.
57
+ return compacting || !ctx.isIdle() || ctx.hasPendingMessages() ? "waiting" : "uncertain";
58
+ },
59
+ deliver: async (batch) => {
60
+ if (!ctx.model) return false;
61
+ const text = `[ace-eyes] Pending canonical HITL/deadline revisions: ${JSON.stringify(batch)}. ` +
62
+ "Read original events; adopt reports through ace-overseer process-reports and existing task/assignment gates. " +
63
+ "Reconcile uncertain effects; never repeat external actions on wake alone. After handling each exact revision, " +
64
+ "acknowledge with the ace_eyes tool (action ack, key, revision). Settlement alone is not acknowledgement.";
65
+ attempt = { text, accepted: false };
66
+ const result = pi.sendUserMessage(text, { deliverAs: "followUp" });
67
+ // Some hosts expose a promise, the standard ExtensionAPI exposes void.
68
+ // Neither completion nor void is proof of message consumption.
69
+ if (result?.then) await result;
70
+ return result !== false;
71
+ } });
72
+ if (eyes.s.enabled || eyes.s.retired) retireLegacy?.();
73
+ arm();
74
+ if (eyes.s.enabled) void eyes.tick();
75
+ };
76
+ pi.on("session_start", async (_event, ctx) => bind(ctx));
77
+ pi.on("session_tree", async (_event, ctx) => bind(ctx));
78
+ pi.on("agent_start", async () => { running = true; });
79
+ pi.on("agent_settled", async () => { running = false; });
80
+ pi.on("message_start", async (event) => {
81
+ if (event.message?.role !== "user" || !attempt) return;
82
+ const text = typeof event.message.content === "string" ? event.message.content :
83
+ (event.message.content || []).filter(part => part.type === "text").map(part => part.text).join("");
84
+ if (text === attempt.text) attempt.accepted = true;
85
+ });
86
+ pi.on("session_before_compact", async (event) => { if (event.reason === "manual") compacting = true; });
87
+ pi.on("session_compact", async () => { compacting = false; eyes?.persist(); });
88
+ pi.on("session_compact_failed", async () => { compacting = false; });
89
+ pi.on("session_shutdown", async () => { stopTimer(); eyes?.dispose(); });
90
+ pi.on("message_end", async (event) => {
91
+ const message = event.message;
92
+ if (message?.role === "assistant" && message.stopReason === "error") eyes?.quota(message.errorMessage);
93
+ });
94
+ pi.registerTool?.({
95
+ name: "ace_eyes", label: "Eyes acknowledgement", description: "Read pending semantic events or acknowledge an exact handled revision. Does not accept reports, authorize effects, start actors or reset quota. Use existing report/assignment owners first. Deadlines require an operation identity and revision; cancel when no longer active.",
96
+ parameters: { type: "object", properties: {
97
+ action: { type: "string", enum: ["status", "ack", "deadline", "cancel"] },
98
+ key: { type: "string" }, revision: { type: "string" }, at: { type: "integer" }
99
+ }, required: ["action"], additionalProperties: false },
100
+ execute: async (_id, args, _signal, _onUpdate, ctx) => {
101
+ if (!eyes) {
102
+ if (!ctx) throw new Error("Eyes session not initialized");
103
+ bind(ctx);
104
+ }
105
+ if (args.action === "ack") eyes.ack(args.key, args.revision);
106
+ else if (args.action === "deadline") eyes.deadline(args.key, args.at, args.revision);
107
+ else if (args.action === "cancel") eyes.cancelDeadline(args.key);
108
+ else if (args.action !== "status") throw new Error("Unknown eyes action");
109
+ // Do not send a second wake from inside the running model turn. The next
110
+ // observation delivers remaining coalesced revisions through the same owner.
111
+ const details = { ...eyes.summary(), pending: eyes.s.pending, deadlines: eyes.s.deadlines };
112
+ return { content: [{ type: "text", text: JSON.stringify(details) }], details };
113
+ }
114
+ });
115
+ pi.registerCommand("eyes", { description: "Event-driven HITL observation: start CONFIG | status | ack KEY REV | deadline NAME REVISION EPOCH_MS | cancel NAME | resume quota-verified | retire",
116
+ handler: async (args, ctx) => {
117
+ try {
118
+ if (!eyes) bind(ctx);
119
+ const [command, a, b, ...rest] = args.trim().split(/\s+/);
120
+ if (rest.length && command !== "deadline") throw new Error("Too many arguments");
121
+ switch (command) {
122
+ case "start": {
123
+ if (!a || b) throw new Error("Use eyes start CONFIG_JSON");
124
+ const config = JSON.parse(fs.readFileSync(path.resolve(ctx.cwd, a), "utf8"));
125
+ eyes.start(config); retireLegacy?.(); arm(); await eyes.tick(); break;
126
+ }
127
+ case "status": ctx.ui?.notify?.(JSON.stringify({ ...eyes.summary(), pending: eyes.s.pending, deadlines: eyes.s.deadlines }), "info"); break;
128
+ case "ack": eyes.ack(a, b); await eyes.tick(); break;
129
+ case "deadline":
130
+ if (rest.length !== 1) throw new Error("Deadline requires operation identity, revision and epoch milliseconds");
131
+ eyes.deadline(a, Number(rest[0]), b); await eyes.tick(); break;
132
+ case "cancel": eyes.cancelDeadline(a); break;
133
+ case "resume":
134
+ if (a !== "quota-verified" || b) throw new Error("Explicit owner verification required: eyes resume quota-verified");
135
+ await eyes.resume(); arm(); break;
136
+ case "retire": eyes.retire(); retireLegacy?.(); stopTimer(); break;
137
+ default: throw new Error("Unknown eyes command; use start, status, ack, deadline, cancel, resume or retire");
138
+ }
139
+ } catch (error) {
140
+ // Never echo process stdout/stderr: a failed native sync may carry private data.
141
+ ctx.ui?.notify?.(`Eyes operation refused (${error.name}); check configuration or /eyes status`, "error");
142
+ }
143
+ } });
144
+ return { active: () => Boolean(eyes?.s.enabled || eyes?.s.retired) };
145
+ }
@@ -0,0 +1,150 @@
1
+ import { createHash } from "node:crypto";
2
+
3
+ const hash = (value) => createHash("sha256").update(JSON.stringify(value)).digest("hex");
4
+ export const EYES_ENTRY = "ace-wake-eyes";
5
+ export const defaults = { intervalSeconds: 30, maxEvents: 500, maxObservationsPerHour: 120, maxWakesPerHour: 20,
6
+ timeoutMs: 10000, maxErrors: 3, maxPending: 2000, scope: "current", recipient: "self", syncRoutes: [] };
7
+
8
+ export function validateConfig(input) {
9
+ if (!input || typeof input !== "object" || Array.isArray(input) ||
10
+ Object.keys(input).some(key => !(key in defaults))) throw new Error("Invalid eyes configuration");
11
+ const config = { ...defaults, ...input };
12
+ for (const [key, min, max] of [["intervalSeconds", 1, 3600], ["maxEvents", 1, 2000],
13
+ ["maxObservationsPerHour", 1, 3600], ["maxWakesPerHour", 1, 1000], ["timeoutMs", 100, 30000], ["maxErrors", 1, 10], ["maxPending", 1, 10000]]) {
14
+ if (!Number.isInteger(config[key]) || config[key] < min || config[key] > max) throw new Error(`Invalid ${key}`);
15
+ }
16
+ if (typeof config.recipient !== "string" || !/^[a-zA-Z0-9_-]{1,64}$/.test(config.recipient)) throw new Error("Invalid observer recipient");
17
+ if (!["current", "all"].includes(config.scope) || !Array.isArray(config.syncRoutes) || config.syncRoutes.length > 10 ||
18
+ config.syncRoutes.some((p) => typeof p !== "string" || !p.startsWith("/"))) throw new Error("Invalid eyes sources");
19
+ return config;
20
+ }
21
+
22
+ /** Observation/delivery bookkeeping in the existing Pi session, not a work
23
+ * queue. It never starts tasks or accepts reports on behalf of their owner. */
24
+ export class Eyes {
25
+ constructor({ snapshot, save, observe, deliver, now = Date.now, status = () => {}, deliveryStatus = () => "accepted", canDeliver = () => true }) {
26
+ this.s = structuredClone(snapshot || { enabled: false, retired: false, config: defaults,
27
+ pending: {}, acknowledged: {}, deadlines: {}, breaker: null, errors: 0, window: null });
28
+ this.s.config = validateConfig(this.s.config);
29
+ for (const key of ["pending", "acknowledged", "deadlines"])
30
+ if (!this.s[key] || typeof this.s[key] !== "object" || Array.isArray(this.s[key])) throw new Error("Invalid persisted eyes state");
31
+ if (typeof this.s.enabled !== "boolean" || typeof this.s.retired !== "boolean" ||
32
+ !Number.isInteger(this.s.errors) || this.s.errors < 0) throw new Error("Invalid persisted eyes state");
33
+ this.save = save; this.observe = observe; this.deliver = deliver; this.now = now; this.status = status;
34
+ this.deliveryStatus = deliveryStatus; this.canDeliver = canDeliver;
35
+ this.busy = false; this.disposed = false;
36
+ // A process restart does not imply receipt acknowledgement. Reconcile the
37
+ // same pending revisions; business-effect idempotence remains with .5.4.
38
+ this.inflight = false;
39
+ this.sent = new Map();
40
+ this.generation = 0;
41
+ }
42
+ persist() { this.save(structuredClone(this.s)); this.status(this.summary()); }
43
+ summary() { return { enabled: this.s.enabled, retired: this.s.retired, pending: Object.keys(this.s.pending).length,
44
+ breaker: this.s.breaker, errors: this.s.errors, observations: this.s.window?.count || 0, wakes: this.s.window?.wakes || 0 }; }
45
+ start(config) {
46
+ if (this.s.retired) throw new Error("Retired actor cannot restart its eyes; hand off pending to its owner");
47
+ if (this.s.breaker) throw new Error("Resolve breaker with explicit resume first");
48
+ this.s.config = validateConfig(config); this.s.enabled = true; this.persist();
49
+ }
50
+ block(reason) {
51
+ if (!this.s.breaker) { this.s.breaker = { reason, at: this.now() }; this.persist(); }
52
+ }
53
+ async resume() {
54
+ if (this.s.retired) throw new Error("Retired actor cannot resume");
55
+ // Explicit operator action; check source/transport readiness without a model
56
+ // request. Quota availability must have been verified by the owner.
57
+ const result = await this.observe(this.s.config);
58
+ this.validate(result);
59
+ this.s.breaker = null; this.s.errors = 0; this.s.window = null; this.inflight = false;
60
+ this.s.enabled = true; this.persist();
61
+ await this.tick();
62
+ }
63
+ retire() { this.generation++; this.s.enabled = false; this.s.retired = true; this.persist(); }
64
+ dispose() { this.disposed = true; this.generation++; }
65
+ quota(message) {
66
+ if (/quota|insufficient.?(?:credit|balance)|billing.?limit|budget.?exceed|usage.?limit|rate.?limit|\b429\b/i.test(String(message))) this.block("quota-or-budget");
67
+ }
68
+ deadline(name, at, operationRevision) {
69
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/.test(name) || !Number.isSafeInteger(at) || at < 0 || typeof operationRevision !== "string" || !/^[a-zA-Z0-9._:-]{1,128}$/.test(operationRevision)) throw new Error("Invalid deadline");
70
+ const key = `deadline:${name}`;
71
+ delete this.s.pending[key];
72
+ this.sent.delete(key); this.inflight = this.sent.size > 0;
73
+ this.s.deadlines[name] = { at, operationRevision }; this.persist();
74
+ }
75
+ cancelDeadline(name) {
76
+ delete this.s.deadlines[name]; delete this.s.pending[`deadline:${name}`];
77
+ this.sent.delete(`deadline:${name}`); this.inflight = this.sent.size > 0; this.persist();
78
+ }
79
+ ack(key, revision) {
80
+ if (this.s.pending[key]?.revision !== revision && this.sent.get(key) !== revision)
81
+ throw new Error("Ack does not match an observed pending or delivered revision");
82
+ this.s.acknowledged[key] = revision;
83
+ if (this.s.pending[key]?.revision === revision) delete this.s.pending[key];
84
+ this.sent.delete(key);
85
+ this.inflight = this.sent.size > 0; this.persist();
86
+ }
87
+ validate(result) {
88
+ if (result?.schema !== "ace.hitl.observation/v1" || !Array.isArray(result.events) || result.events.length > this.s.config.maxEvents)
89
+ throw new Error("Invalid or over-budget HITL observation");
90
+ if (result.settled !== undefined && !Array.isArray(result.settled)) throw new Error("Invalid settlement projection");
91
+ const rows = [...result.events, ...(result.settled || [])];
92
+ if (rows.length > this.s.config.maxEvents) throw new Error("Over-budget projection");
93
+ const keys = new Set();
94
+ for (const event of rows) {
95
+ if (!/^[a-f0-9]{64}$/.test(event.key) || !/^[a-f0-9]{64}$/.test(event.revision) ||
96
+ typeof event.id !== "string" || !/^[a-z0-9_-]{1,64}$/i.test(event.id) || keys.has(event.key)) throw new Error("Invalid event identity");
97
+ keys.add(event.key);
98
+ }
99
+ }
100
+ async tick() {
101
+ if (this.busy || this.disposed || !this.s.enabled || this.s.retired || this.s.breaker) return;
102
+ const now = this.now();
103
+ if (!this.s.window || now - this.s.window.start >= 3600000) this.s.window = { start: now, count: 0, wakes: 0 };
104
+ if (this.s.window.count >= this.s.config.maxObservationsPerHour) { this.block("observation-budget"); return; }
105
+ this.s.window.count++; this.persist(); // Budget survives restart too.
106
+ this.busy = true;
107
+ const generation = this.generation;
108
+ try {
109
+ const result = await this.observe(this.s.config);
110
+ if (generation !== this.generation || this.disposed) return;
111
+ this.validate(result);
112
+ for (const e of result.settled || []) {
113
+ this.s.acknowledged[e.key] = e.revision;
114
+ if (this.s.pending[e.key]?.revision === e.revision) delete this.s.pending[e.key];
115
+ this.sent.delete(e.key);
116
+ }
117
+ if (this.inflight && this.sent.size === 0) this.inflight = false;
118
+ for (const e of result.events) {
119
+ if (this.s.acknowledged[e.key] !== e.revision) this.s.pending[e.key] = { id: e.id, revision: e.revision };
120
+ }
121
+ for (const [name, deadline] of Object.entries(this.s.deadlines)) {
122
+ const { at, operationRevision } = deadline;
123
+ const key = `deadline:${name}`, revision = hash([name, at, operationRevision]);
124
+ if (now >= at && this.s.acknowledged[key] !== revision) this.s.pending[key] = { id: name, revision };
125
+ }
126
+ if (Object.keys(this.s.pending).length + Object.keys(this.s.acknowledged).length > this.s.config.maxPending) {
127
+ this.block("retention-budget"); return;
128
+ }
129
+ this.persist();
130
+ if (this.inflight && this.deliveryStatus() === "uncertain") {
131
+ this.block("notification-unconfirmed"); return;
132
+ }
133
+ if (!this.inflight && !this.s.breaker && Object.keys(this.s.pending).length && this.canDeliver()) {
134
+ // Retain identities before attempting delivery. A rejected send leaves
135
+ // pending intact; acceptance by Pi is not application acknowledgement.
136
+ if ((this.s.window.wakes || 0) >= this.s.config.maxWakesPerHour) { this.block("wake-budget"); return; }
137
+ this.s.window.wakes = (this.s.window.wakes || 0) + 1;
138
+ this.persist(); // Includes uncertain notification attempts, never token billing.
139
+ const batch = Object.entries(this.s.pending).slice(0, 20);
140
+ this.inflight = Boolean(await this.deliver(batch));
141
+ if (this.inflight) this.sent = new Map(batch.map(([key, value]) => [key, value.revision]));
142
+ if (!this.inflight) throw new Error("wake delivery refused");
143
+ }
144
+ if (this.s.errors && (!this.inflight || this.deliveryStatus() === "accepted")) { this.s.errors = 0; this.persist(); }
145
+ } catch {
146
+ this.s.errors++; this.persist();
147
+ if (this.s.errors >= this.s.config.maxErrors) this.block("observation-or-delivery-failure");
148
+ } finally { this.busy = false; }
149
+ }
150
+ }
@@ -4,7 +4,7 @@ module Ace
4
4
  module Handbook
5
5
  module Integration
6
6
  module Pi
7
- VERSION = "0.6.0"
7
+ VERSION = "0.7.0"
8
8
  end
9
9
  end
10
10
  end
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ace-handbook-integration-pi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.6.0
4
+ version: 0.7.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Michal Czyz
8
8
  bindir: exe
9
9
  cert_chain: []
10
- date: 2026-10-20 00:00:00.000000000 Z
10
+ date: 2026-10-23 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: ace-handbook
@@ -23,6 +23,34 @@ dependencies:
23
23
  - - "~>"
24
24
  - !ruby/object:Gem::Version
25
25
  version: '0.33'
26
+ - !ruby/object:Gem::Dependency
27
+ name: ace-overseer
28
+ requirement: !ruby/object:Gem::Requirement
29
+ requirements:
30
+ - - "~>"
31
+ - !ruby/object:Gem::Version
32
+ version: '0.26'
33
+ type: :runtime
34
+ prerelease: false
35
+ version_requirements: !ruby/object:Gem::Requirement
36
+ requirements:
37
+ - - "~>"
38
+ - !ruby/object:Gem::Version
39
+ version: '0.26'
40
+ - !ruby/object:Gem::Dependency
41
+ name: ace-hitl-hermes
42
+ requirement: !ruby/object:Gem::Requirement
43
+ requirements:
44
+ - - "~>"
45
+ - !ruby/object:Gem::Version
46
+ version: 0.9.3
47
+ type: :runtime
48
+ prerelease: false
49
+ version_requirements: !ruby/object:Gem::Requirement
50
+ requirements:
51
+ - - "~>"
52
+ - !ruby/object:Gem::Version
53
+ version: 0.9.3
26
54
  description: PI-specific provider integration package that extends ace-handbook with
27
55
  provider manifests.
28
56
  email:
@@ -36,6 +64,8 @@ files:
36
64
  - README.md
37
65
  - handbook/extensions/ace-wake/index.js
38
66
  - handbook/extensions/ace-wake/package.json
67
+ - handbook/extensions/ace-wake/wake/eyes-runtime.js
68
+ - handbook/extensions/ace-wake/wake/eyes.js
39
69
  - handbook/extensions/ace-wake/wake/inbox-discovery.js
40
70
  - handbook/extensions/ace-wake/wake/loop-subscription.js
41
71
  - handbook/extensions/ace-wake/wake/types.js