@ouro.bot/cli 0.1.0-alpha.755 → 0.1.0-alpha.756
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/changelog.json +6 -0
- package/deploy/unraid/sanctuary.ouro/bundle-meta.json +1 -1
- package/deploy/unraid/sanctuary.xml +1 -1
- package/dist/mind/session-transaction.js +58 -22
- package/dist/senses/private-runtime-worker.js +6 -0
- package/dist/senses/sanctuary-runtime.js +9 -7
- package/npm-shrinkwrap.json +2 -2
- package/package.json +1 -1
package/changelog.json
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"_note": "This changelog is maintained as part of the PR/version-bump workflow. Agent-curated, not auto-generated. Agents read this file directly via read_file to understand what changed between versions.",
|
|
3
3
|
"versions": [
|
|
4
|
+
{
|
|
5
|
+
"version": "0.1.0-alpha.756",
|
|
6
|
+
"changes": [
|
|
7
|
+
"Recover private-runtime turns safely across container restarts and restore Sanctuary steward-policy context."
|
|
8
|
+
]
|
|
9
|
+
},
|
|
4
10
|
{
|
|
5
11
|
"version": "0.1.0-alpha.755",
|
|
6
12
|
"changes": [
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<?xml version="1.0"?>
|
|
2
2
|
<Container version="2">
|
|
3
3
|
<Name>Mendelow Cloud Butler</Name>
|
|
4
|
-
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.
|
|
4
|
+
<Repository>ghcr.io/ourostack/ouroboros-butler:0.1.0-alpha.756</Repository>
|
|
5
5
|
<Registry>https://github.com/ourostack/ouroboros/pkgs/container/ouroboros-butler</Registry>
|
|
6
6
|
<Network>host</Network>
|
|
7
7
|
<Shell>sh</Shell>
|
|
@@ -51,6 +51,7 @@ const fs = __importStar(require("node:fs"));
|
|
|
51
51
|
const path = __importStar(require("node:path"));
|
|
52
52
|
const better_sqlite3_1 = __importDefault(require("better-sqlite3"));
|
|
53
53
|
const runtime_1 = require("../nerves/runtime");
|
|
54
|
+
const habit_lifecycle_1 = require("../heart/habits/habit-lifecycle");
|
|
54
55
|
class SessionTurnBusyError extends Error {
|
|
55
56
|
retryable = true;
|
|
56
57
|
constructor(message = "session turn is busy") {
|
|
@@ -101,34 +102,71 @@ function openLeaseDatabase(lockPath, busyTimeoutMs = 0) {
|
|
|
101
102
|
owner_token TEXT NOT NULL
|
|
102
103
|
)
|
|
103
104
|
`);
|
|
105
|
+
const columns = new Set(database.prepare(`PRAGMA table_info(session_turn_lease)`).all().map((column) => column.name));
|
|
106
|
+
if (!columns.has("boot_identity"))
|
|
107
|
+
database.exec(`ALTER TABLE session_turn_lease ADD COLUMN boot_identity TEXT`);
|
|
108
|
+
if (!columns.has("process_started_at"))
|
|
109
|
+
database.exec(`ALTER TABLE session_turn_lease ADD COLUMN process_started_at TEXT`);
|
|
104
110
|
return database;
|
|
105
111
|
}
|
|
106
|
-
function
|
|
112
|
+
function acquisitionRecord(options) {
|
|
113
|
+
const pid = options.pid ?? process.pid;
|
|
114
|
+
const bootIdentity = (options.getBootIdentity ?? habit_lifecycle_1.probeHabitBootIdentity)();
|
|
115
|
+
const processStartedAt = (options.getProcessStartedAt ?? habit_lifecycle_1.probeHabitProcessStartedAt)(pid);
|
|
116
|
+
if (!bootIdentity || !processStartedAt)
|
|
117
|
+
throw new SessionTransactionError("current process identity is unavailable");
|
|
118
|
+
return {
|
|
119
|
+
pid,
|
|
120
|
+
ownerId: options.ownerId ?? (0, node_crypto_1.randomUUID)(),
|
|
121
|
+
ownerToken: options.ownerToken ?? (0, node_crypto_1.randomUUID)(),
|
|
122
|
+
bootIdentity,
|
|
123
|
+
processStartedAt,
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
function claimLeaseRecord(lockPath, record, isProcessAlive, getProcessStartedAt) {
|
|
107
127
|
const database = openLeaseDatabase(lockPath);
|
|
108
128
|
try {
|
|
109
129
|
return database.transaction(() => {
|
|
110
130
|
const current = database.prepare(`
|
|
111
|
-
SELECT pid, owner_id, owner_token FROM session_turn_lease WHERE singleton = 1
|
|
131
|
+
SELECT pid, owner_id, owner_token, boot_identity, process_started_at FROM session_turn_lease WHERE singleton = 1
|
|
112
132
|
`).get();
|
|
113
133
|
if (!current) {
|
|
114
134
|
database.prepare(`
|
|
115
|
-
INSERT INTO session_turn_lease (singleton, pid, owner_id, owner_token) VALUES (1, ?, ?, ?)
|
|
116
|
-
`).run(record.pid, record.ownerId, record.ownerToken);
|
|
135
|
+
INSERT INTO session_turn_lease (singleton, pid, owner_id, owner_token, boot_identity, process_started_at) VALUES (1, ?, ?, ?, ?, ?)
|
|
136
|
+
`).run(record.pid, record.ownerId, record.ownerToken, record.bootIdentity, record.processStartedAt);
|
|
117
137
|
return { acquired: true, stale: null };
|
|
118
138
|
}
|
|
119
139
|
if (!Number.isSafeInteger(current.pid) || current.pid <= 0
|
|
120
140
|
|| typeof current.owner_id !== "string" || current.owner_id.length === 0
|
|
121
|
-
|| typeof current.owner_token !== "string" || current.owner_token.length === 0
|
|
141
|
+
|| typeof current.owner_token !== "string" || current.owner_token.length === 0
|
|
142
|
+
|| !((current.boot_identity === null && current.process_started_at === null)
|
|
143
|
+
|| (typeof current.boot_identity === "string" && current.boot_identity.length > 0
|
|
144
|
+
&& typeof current.process_started_at === "string" && current.process_started_at.length > 0))) {
|
|
122
145
|
throw new SessionTransactionError("invalid session lease record");
|
|
123
146
|
}
|
|
124
|
-
const observed = {
|
|
125
|
-
|
|
147
|
+
const observed = {
|
|
148
|
+
pid: current.pid,
|
|
149
|
+
ownerId: current.owner_id,
|
|
150
|
+
ownerToken: current.owner_token,
|
|
151
|
+
bootIdentity: current.boot_identity,
|
|
152
|
+
processStartedAt: current.process_started_at,
|
|
153
|
+
};
|
|
154
|
+
const differentBoot = observed.bootIdentity !== null && observed.bootIdentity !== record.bootIdentity;
|
|
155
|
+
let differentProcess = false;
|
|
156
|
+
if (observed.bootIdentity !== null && !differentBoot) {
|
|
157
|
+
const liveStartedAt = getProcessStartedAt(observed.pid);
|
|
158
|
+
differentProcess = liveStartedAt !== null && liveStartedAt !== observed.processStartedAt;
|
|
159
|
+
if (liveStartedAt === observed.processStartedAt)
|
|
160
|
+
return { acquired: false, stale: null };
|
|
161
|
+
}
|
|
162
|
+
if (!differentBoot && !differentProcess && isProcessAlive(observed.pid))
|
|
126
163
|
return { acquired: false, stale: null };
|
|
127
|
-
database.prepare(`
|
|
128
|
-
UPDATE session_turn_lease SET pid = ?, owner_id = ?, owner_token = ?
|
|
164
|
+
const changed = database.prepare(`
|
|
165
|
+
UPDATE session_turn_lease SET pid = ?, owner_id = ?, owner_token = ?, boot_identity = ?, process_started_at = ?
|
|
129
166
|
WHERE singleton = 1 AND pid = ? AND owner_id = ? AND owner_token = ?
|
|
130
|
-
|
|
131
|
-
|
|
167
|
+
AND boot_identity IS ? AND process_started_at IS ?
|
|
168
|
+
`).run(record.pid, record.ownerId, record.ownerToken, record.bootIdentity, record.processStartedAt, observed.pid, observed.ownerId, observed.ownerToken, observed.bootIdentity, observed.processStartedAt).changes;
|
|
169
|
+
return changed === 1 ? { acquired: true, stale: observed } : { acquired: false, stale: null };
|
|
132
170
|
})();
|
|
133
171
|
}
|
|
134
172
|
finally {
|
|
@@ -140,7 +178,8 @@ function releaseLeaseRecord(lockPath, record) {
|
|
|
140
178
|
try {
|
|
141
179
|
return database.prepare(`
|
|
142
180
|
DELETE FROM session_turn_lease WHERE singleton = 1 AND pid = ? AND owner_id = ? AND owner_token = ?
|
|
143
|
-
|
|
181
|
+
AND boot_identity IS ? AND process_started_at IS ?
|
|
182
|
+
`).run(record.pid, record.ownerId, record.ownerToken, record.bootIdentity, record.processStartedAt).changes === 1;
|
|
144
183
|
}
|
|
145
184
|
finally {
|
|
146
185
|
database.close();
|
|
@@ -180,9 +219,8 @@ async function acquireSessionTurnLease(sessionPath, options = {}) {
|
|
|
180
219
|
return makeLease(existing);
|
|
181
220
|
}
|
|
182
221
|
}
|
|
183
|
-
const
|
|
184
|
-
const
|
|
185
|
-
const pid = options.pid ?? process.pid;
|
|
222
|
+
const record = acquisitionRecord(options);
|
|
223
|
+
const { ownerId } = record;
|
|
186
224
|
const timeoutMs = options.timeoutMs ?? 5_000;
|
|
187
225
|
const pollIntervalMs = options.pollIntervalMs ?? 10;
|
|
188
226
|
const isProcessAlive = options.isProcessAlive ?? processAlive;
|
|
@@ -192,14 +230,14 @@ async function acquireSessionTurnLease(sessionPath, options = {}) {
|
|
|
192
230
|
for (;;) {
|
|
193
231
|
let claim = null;
|
|
194
232
|
try {
|
|
195
|
-
claim = claimLeaseRecord(lockPath,
|
|
233
|
+
claim = claimLeaseRecord(lockPath, record, isProcessAlive, options.getProcessStartedAt ?? habit_lifecycle_1.probeHabitProcessStartedAt);
|
|
196
234
|
}
|
|
197
235
|
catch (error) {
|
|
198
236
|
if (!isSqliteBusy(error))
|
|
199
237
|
throw error;
|
|
200
238
|
}
|
|
201
239
|
if (claim?.acquired) {
|
|
202
|
-
const held = { sessionPath: canonical, lockPath,
|
|
240
|
+
const held = { sessionPath: canonical, lockPath, ...record, depth: 1, released: false };
|
|
203
241
|
heldLeases.set(canonical, held);
|
|
204
242
|
(0, runtime_1.emitNervesEvent)({
|
|
205
243
|
component: "mind",
|
|
@@ -246,15 +284,13 @@ function withImmediateSessionTurnLease(sessionPath, work, options = {}) {
|
|
|
246
284
|
const contextual = currentSessionTurnLease(canonical);
|
|
247
285
|
if (contextual)
|
|
248
286
|
return work(contextual);
|
|
249
|
-
const
|
|
250
|
-
const ownerToken = options.ownerToken ?? (0, node_crypto_1.randomUUID)();
|
|
251
|
-
const pid = options.pid ?? process.pid;
|
|
287
|
+
const record = acquisitionRecord(options);
|
|
252
288
|
const lockPath = `${canonical}.turn.lock`;
|
|
253
289
|
fs.mkdirSync(path.dirname(canonical), { recursive: true });
|
|
254
290
|
const tryAcquire = () => {
|
|
255
291
|
let claim;
|
|
256
292
|
try {
|
|
257
|
-
claim = claimLeaseRecord(lockPath,
|
|
293
|
+
claim = claimLeaseRecord(lockPath, record, options.isProcessAlive ?? processAlive, options.getProcessStartedAt ?? habit_lifecycle_1.probeHabitProcessStartedAt);
|
|
258
294
|
}
|
|
259
295
|
catch (error) {
|
|
260
296
|
if (isSqliteBusy(error))
|
|
@@ -262,7 +298,7 @@ function withImmediateSessionTurnLease(sessionPath, work, options = {}) {
|
|
|
262
298
|
throw error;
|
|
263
299
|
}
|
|
264
300
|
if (claim.acquired) {
|
|
265
|
-
const held = { sessionPath: canonical, lockPath,
|
|
301
|
+
const held = { sessionPath: canonical, lockPath, ...record, depth: 1, released: false };
|
|
266
302
|
heldLeases.set(canonical, held);
|
|
267
303
|
if (claim.stale) {
|
|
268
304
|
options.onStaleLease?.(claim.stale);
|
|
@@ -515,6 +515,7 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
|
|
|
515
515
|
const currentHabitName = nextHabitName;
|
|
516
516
|
const currentTrigger = nextTrigger ?? "overdue";
|
|
517
517
|
const currentNoSend = nextNoSend;
|
|
518
|
+
const currentExternalEvent = nextExternalEvent;
|
|
518
519
|
const currentExternalEventHeartbeat = nextExternalEventHeartbeat;
|
|
519
520
|
nextExternalEventHeartbeat = undefined;
|
|
520
521
|
const currentHabitRun = currentReason === "habit" && currentHabitName
|
|
@@ -655,6 +656,11 @@ function createPrivateRuntimeWorker(runTurn = (options) => (0, private_runtime_1
|
|
|
655
656
|
// would cause hasPendingWork() to be true here, producing a
|
|
656
657
|
// self-sustaining "instinct" loop with no external input. Cap it.
|
|
657
658
|
if (hasPendingWork(currentHabitRun?.paths.pendingDir)) {
|
|
659
|
+
if (currentExternalEvent && (turnErrors.length > 0
|
|
660
|
+
|| (turnResult && typeof turnResult === "object" && "turnOutcome" in turnResult && turnResult.turnOutcome === "errored"))) {
|
|
661
|
+
finalizeCurrentHabitRun();
|
|
662
|
+
break;
|
|
663
|
+
}
|
|
658
664
|
clearHeartbeatRestShield();
|
|
659
665
|
if (consecutiveInstinctTurns >= exports.MAX_CONSECUTIVE_INSTINCT_TURNS) {
|
|
660
666
|
(0, runtime_1.emitNervesEvent)({
|
|
@@ -120,10 +120,10 @@ function persistAttempt(filePath, attempt) {
|
|
|
120
120
|
fs.writeFileSync(temporary, `${JSON.stringify(attempt)}\n`, { mode: 0o600 });
|
|
121
121
|
fs.renameSync(temporary, filePath);
|
|
122
122
|
}
|
|
123
|
-
async function appendAcceptanceAttempt(
|
|
123
|
+
async function appendAcceptanceAttempt(agentRoot, attempt) {
|
|
124
124
|
if (!attempt.scenarioHandleDigest)
|
|
125
125
|
return;
|
|
126
|
-
const filePath = path.join(
|
|
126
|
+
const filePath = path.join(agentRoot, "state", "acceptance", "restart-attempts.ndjson");
|
|
127
127
|
const previous = acceptanceLedgerTails.get(filePath) ?? Promise.resolve();
|
|
128
128
|
const current = previous.catch(() => undefined).then(() => {
|
|
129
129
|
fs.mkdirSync(path.dirname(filePath), { recursive: true, mode: 0o700 });
|
|
@@ -187,6 +187,7 @@ function createSanctuaryToolContext(agentName) {
|
|
|
187
187
|
message: "creating typed Sanctuary tool context",
|
|
188
188
|
meta: { agentName },
|
|
189
189
|
});
|
|
190
|
+
const agentRoot = (0, identity_1.getAgentRoot)(agentName);
|
|
190
191
|
const initial = machineConfig(agentName);
|
|
191
192
|
const endpoint = required(initial, "unraidGraphqlUrl");
|
|
192
193
|
const readClient = new unraid_client_1.UnraidClient({ endpoint, apiKey: required(initial, "unraidReadApiKey") });
|
|
@@ -216,15 +217,16 @@ function createSanctuaryToolContext(agentName) {
|
|
|
216
217
|
listContainers: reads.listContainers,
|
|
217
218
|
loadWriteApiKey: async () => required(machineConfig(agentName), "unraidWriteApiKey"),
|
|
218
219
|
persistAttempt: async (attempt) => {
|
|
219
|
-
persistAttempt(path.join(
|
|
220
|
-
await appendAcceptanceAttempt(
|
|
220
|
+
persistAttempt(path.join(agentRoot, "state", "approvals", "unraid-restart-attempt.json"), attempt);
|
|
221
|
+
await appendAcceptanceAttempt(agentRoot, attempt);
|
|
221
222
|
},
|
|
222
223
|
acceptanceScenarioHandleDigest: () => (0, sanctuary_acceptance_marker_1.readSanctuaryAcceptanceMarker)(agentName)?.scenarioHandleDigest,
|
|
223
224
|
acceptanceApproval: sanctuary_acceptance_marker_1.readSanctuaryAcceptanceApproval,
|
|
224
|
-
reserveRoutineAction: (input) => (0, steward_policy_1.consumeRoutineActionGrant)(
|
|
225
|
-
transitionRoutineAction: (input) => (0, steward_policy_1.transitionRoutineActionReceipt)(
|
|
225
|
+
reserveRoutineAction: (input) => (0, steward_policy_1.consumeRoutineActionGrant)(agentRoot, input),
|
|
226
|
+
transitionRoutineAction: (input) => (0, steward_policy_1.transitionRoutineActionReceipt)(agentRoot, input),
|
|
226
227
|
});
|
|
227
228
|
return {
|
|
229
|
+
agentRoot,
|
|
228
230
|
sanctuary: {
|
|
229
231
|
listContainers: acceptanceRead("unraid_list_containers", reads.listContainers),
|
|
230
232
|
getContainerLogs: acceptanceRead("unraid_get_container_logs", reads.getContainerLogs),
|
|
@@ -255,7 +257,7 @@ function createSanctuaryToolContext(agentName) {
|
|
|
255
257
|
collectToolResult(result);
|
|
256
258
|
return result;
|
|
257
259
|
},
|
|
258
|
-
recoverRoutineActions: () => (0, steward_policy_1.recoverRoutineActionReceipts)(
|
|
260
|
+
recoverRoutineActions: () => (0, steward_policy_1.recoverRoutineActionReceipts)(agentRoot, {
|
|
259
261
|
observeTarget: async (target) => {
|
|
260
262
|
const observed = await reads.listContainers();
|
|
261
263
|
if (!observed.ok)
|
package/npm-shrinkwrap.json
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ouro.bot/cli",
|
|
3
|
-
"version": "0.1.0-alpha.
|
|
3
|
+
"version": "0.1.0-alpha.756",
|
|
4
4
|
"lockfileVersion": 3,
|
|
5
5
|
"requires": true,
|
|
6
6
|
"packages": {
|
|
7
7
|
"": {
|
|
8
8
|
"name": "@ouro.bot/cli",
|
|
9
|
-
"version": "0.1.0-alpha.
|
|
9
|
+
"version": "0.1.0-alpha.756",
|
|
10
10
|
"dependencies": {
|
|
11
11
|
"@anthropic-ai/sdk": "^0.78.0",
|
|
12
12
|
"@azure/identity": "^4.13.0",
|