@awebai/oats 0.24.10 → 0.24.11

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/bin/oats.mjs CHANGED
@@ -30,7 +30,7 @@ import {
30
30
  approveCapability, approveAvailableCapability, updatePackage, removePackage, migrateLegacyLock, applyLegacyLockMigration,
31
31
  packageIntegrity, capabilityArtifactIntegrity, verifyCapabilityInstallation, installedCapabilityDir, installedCapabilitiesDir, ownedCapabilitiesDir, loadPackageManifestAt,
32
32
  resolveOatsConfig, resolveWorkMode, composeInstanceAgentsMd, planInstanceResources, parseYamlNested, assertSafeConfigValue, assertSafeConfigWriteKey, stripInternalAnnotations, withConfigFile, packagedInject, teamAgentRoots,
33
- findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, findInstanceHomes, listCapabilityAgents, workspaceOf, stopInstanceSession, recomposeInstanceInstructions, refreshRetirementBaselineHome,
33
+ findTeamAgent, findTeamInstance, findCapabilityAgent, findInstanceHome, findInstanceHomes, listCapabilityAgents, workspaceOf, stopInstanceSession, recomposeInstanceInstructions,
34
34
  ensureRoot, findRoot, findAgent, listAgents, listInstances, listAgentDefs, createAgent as coreCreateAgent,
35
35
  spawnInstance, retireInstance, inspectInstanceSession, inputInstanceSession, attachInstanceSession, startInstanceSession, upsertLocalAgent, defaultRepo, RELATIONS, validateLaunchConfig, resolveLaunchSelection, resolveLaunchExecutable, checkLaunchExecutable, missingLaunchEnvRefs, renderLaunchRecipe, describeLaunchCommand, redactLaunchRecipe, LAUNCH_RUNTIMES, LAUNCH_RECIPE_VERSION, parseLaunchCommand, resolveYolo, planLaunch, redactLaunchCommand, restartInstanceSession,
36
36
  } from "../lib/core.mjs";
@@ -4358,10 +4358,10 @@ function spawnCmd() {
4358
4358
  // K6e: record the wake outcome in the home so a same-key replay can report
4359
4359
  // it instead of leaving "saved or not?" to inference.
4360
4360
  if (r.spawnIdempotencyKey) {
4361
- try { const f = join(r.home, "instance.json"); const m = JSON.parse(readFileSync(f, "utf8")); m.wake = { requested: true, saved: !wakeScheduleError, error: wakeScheduleError ?? null }; writeFileSync(f, JSON.stringify(m, null, 2) + "\n"); r.wake = m.wake; refreshRetirementBaselineHome(r.home); } catch { /* the receipt still says it */ }
4361
+ try { const f = join(r.home, "instance.json"); const m = JSON.parse(readFileSync(f, "utf8")); m.wake = { requested: true, saved: !wakeScheduleError, error: wakeScheduleError ?? null }; writeFileSync(f, JSON.stringify(m, null, 2) + "\n"); r.wake = m.wake; } catch { /* the receipt still says it */ }
4362
4362
  }
4363
4363
  } else if (r.spawnIdempotencyKey && r.replayed !== true) {
4364
- try { const f = join(r.home, "instance.json"); const m = JSON.parse(readFileSync(f, "utf8")); m.wake = { requested: false, saved: null, error: null }; writeFileSync(f, JSON.stringify(m, null, 2) + "\n"); r.wake = m.wake; refreshRetirementBaselineHome(r.home); } catch { /* nothing to record */ }
4364
+ try { const f = join(r.home, "instance.json"); const m = JSON.parse(readFileSync(f, "utf8")); m.wake = { requested: false, saved: null, error: null }; writeFileSync(f, JSON.stringify(m, null, 2) + "\n"); r.wake = m.wake; } catch { /* nothing to record */ }
4365
4365
  }
4366
4366
  if (JSON_MODE) {
4367
4367
  // Desktop CLI API v1 spawn result — a FIXED shape (see docs/desktop-cli-api.md).
@@ -2,7 +2,7 @@
2
2
 
3
3
  **Purpose:** the one accurate view of every work stream in the redesign, what is on main, what is in flight, who owns it, and what blocks it. Lead: `oats-expert` (redesign lead). Updated whenever anything merges, is returned, or reality changes. Older per-lane boards are superseded by this file.
4
4
 
5
- **Last update:** 2026-09-22 19:10Z · **v0.24.9 PUBLISHED** (tag `04a4f709`; both packages on npm, tarball shasum `6333c01c`; bump PR79 → main `a5cc390e`; artifact probe: readiness/preview API 2/bound apply/E_DECISION_STALE/retire retention/no-git verify all pass, caller alive, soul intact) · **6b READ MERGED** (PR78) · K6b ✅ · PR77 ✅ · engineer → **6b APPLY companion proposal** → 7b → 8 · open contracts: K11, attach-knowledge, auto-PR, branch enumeration
5
+ **Last update:** 2026-09-22 21:30Z · **v0.24.10 PUBLISHED** (tag `d47dda94`; tarball shasum `030bcc3e`; bump PR86 → main `27f9f334`; artifact probe: preview API 2 → keyed apply → explicit-branch same-key replay → E_IDEMPOTENCY_CONFLICT → concurrent E_PLACEMENT_TAKEN/winner → keyed home retires clean) · **6b COMPLETE**: READ (PR78) + APPLY companion (PR85) · kernel K6b–K6f (PR76/80/81/82/83/84) · engineer → **7b (K7 instance events) proposal** → 8 · open contracts: K11, attach-knowledge, auto-PR, branch enumeration
6
6
 
7
7
  Legend: ✅ on main/published · 🔄 in flight (PR/branch) · 🟡 preserved, not adopted · ⬜ not started · ⛔ blocked
8
8
 
@@ -0,0 +1,30 @@
1
+ # OATS v0.24.11 — retirement recovery: kernel-owned fields only, nothing re-blessed after launch
2
+
3
+ Kernel/Pi/Desktop **0.24.11**. Tag `v0.24.11` → the commit carrying these
4
+ notes; the version-bump commit lands after the tag. **Upgrade from 0.24.10 —
5
+ it can lose an agent's early home bytes at retire.**
6
+
7
+ ## Fix
8
+
9
+ 0.24.10 (PR84) made a fresh keyed spawn retire clean by re-hashing the **whole
10
+ home** after the kernel's own post-spawn writes (completion marker, wake
11
+ record). That re-stamp ran after the tmux launch, so anything the agent wrote
12
+ in the launch→completion interval — `STATE.md`, notes — was blessed into the
13
+ retirement baseline and **not recovered** at retire. Found by the Desktop
14
+ engineer's exact-source inert analysis of the merged fix.
15
+
16
+ Now the re-stamp is gone. The baseline fingerprint hashes the root
17
+ `instance.json` with exactly the kernel-owned post-spawn fields
18
+ (`spawnCompleted`, `wake`) removed. Kernel writes never read as the agent's
19
+ changes; **nothing else in the home is ever re-blessed**; there is no
20
+ observation of the home after launch. Regression: authored bytes written at
21
+ launch are recovered under `changed instance-home bytes` with the bytes
22
+ intact; a plain keyed spawn still retires with `workRecovery: null`.
23
+
24
+ Rule recorded for the framework: the retirement baseline may account only for
25
+ bytes the kernel can *prove* it wrote — never for what it *observes* after the
26
+ agent may have started.
27
+
28
+ ## Upgrade
29
+
30
+ `npm i -g @awebai/oats@0.24.11`, then `oats doctor`.
package/lib/core.mjs CHANGED
@@ -6898,8 +6898,7 @@ ${task.trim() ? `\n## Task\n\n${task.trim()}\n` : "\nNo task was provided at spa
6898
6898
  if (o.idempotencyKey !== undefined) {
6899
6899
  // Only now is the spawn a finished receipt a same-key retry may replay.
6900
6900
  meta.spawnCompleted = true;
6901
- writeFileSync(join(home, "instance.json"), JSON.stringify(meta, null, 2) + "\n");
6902
- refreshRetirementBaselineHome(home); // a kernel write, not the agent's
6901
+ writeFileSync(join(home, "instance.json"), JSON.stringify(meta, null, 2) + "\n"); // a kernel-owned field: the baseline fingerprint ignores it
6903
6902
  }
6904
6903
  return { ...meta, ...(o.expectDecision !== undefined ? { replayed: false } : {}), launch: redactLaunchRecipe(recipe), command: redactLaunchCommand(cmdline), attach: spawnHerdr ? `HERDR_SOCKET_PATH=${shq(spawnHerdr.socket)} ${shq(spawnHerdr.binary)} terminal attach ${shq(spawnHerdr.terminalId)}` : backend === "herdr" ? "not launched" : `tmux attach -t ${session}`, warnings: spawnWarnings.length ? spawnWarnings : undefined };
6905
6904
  } catch (error) {
@@ -7276,7 +7275,7 @@ function fingerprintTree(root, { excludeRoot = new Set(), excludeGitMetadata = f
7276
7275
  const st = lstatSync(path);
7277
7276
  hash.update(childRel); hash.update("\0"); hash.update(String(st.mode & 0o7777)); hash.update("\0");
7278
7277
  if (st.isSymbolicLink()) { hash.update("link\0"); hash.update(readlinkSync(path)); hash.update("\0"); }
7279
- else if (st.isFile()) { hash.update("file\0"); hash.update(readFileSync(path)); hash.update("\0"); }
7278
+ else if (st.isFile()) { hash.update("file\0"); hash.update(!rel && e.name === "instance.json" ? kernelNeutralInstanceJson(readFileSync(path)) : readFileSync(path)); hash.update("\0"); }
7280
7279
  else if (st.isDirectory()) { hash.update("dir\0"); walk(path, childRel); }
7281
7280
  else throw oatsError("E_WORK_INSPECTION_FAILED", `${path} has an unsupported filesystem type`);
7282
7281
  }
@@ -7335,17 +7334,21 @@ function retirementDisposableRoots(work, workMode, capabilities) {
7335
7334
  return roots;
7336
7335
  }
7337
7336
 
7338
- /** Re-stamp ONLY the home fingerprint of an existing baseline after a KERNEL
7339
- * write to the home (completion marker, wake record). Retirement compares the
7340
- * home against this baseline; kernel-owned metadata written after spawn must
7341
- * not read as the agent's "changed instance-home bytes". Everything else in
7342
- * the baseline (work fingerprint, mode, capabilities) is untouched. */
7343
- export function refreshRetirementBaselineHome(home) {
7344
- const path = retirementBaselinePath(home);
7345
- let baseline; try { baseline = JSON.parse(readFileSync(path, "utf8")); } catch { return false; }
7346
- baseline.homeFingerprint = fingerprintTree(home, { excludeRoot: new Set(["work"]) });
7347
- writeFileSync(path, JSON.stringify(baseline, null, 2) + "\n", { mode: 0o600 });
7348
- return true;
7337
+ /** The fields the KERNEL writes into a home's instance.json after the spawn's
7338
+ * retirement baseline was taken (completion marker, wake record). The
7339
+ * baseline fingerprint hashes instance.json with exactly these removed, so a
7340
+ * kernel write does not read as the agent's change — and NOTHING ELSE in the
7341
+ * home is ever re-blessed: an agent's STATE.md written seconds after launch
7342
+ * is still recovered at retire. (Replaces a whole-home re-stamp that could
7343
+ * bless authored bytes written in the launch→completion interval.) */
7344
+ const KERNEL_POST_SPAWN_FIELDS = ["spawnCompleted", "wake"];
7345
+ function kernelNeutralInstanceJson(bytes) {
7346
+ try {
7347
+ const m = JSON.parse(String(bytes));
7348
+ if (!m || typeof m !== "object" || Array.isArray(m)) return bytes;
7349
+ for (const k of KERNEL_POST_SPAWN_FIELDS) delete m[k];
7350
+ return Buffer.from(JSON.stringify(m));
7351
+ } catch { return bytes; }
7349
7352
  }
7350
7353
  function writeRetirementBaseline(home, work, mode, workMode, capabilities, runtime, { exclusive = false, incarnationId, executionBinding } = {}) {
7351
7354
  if (mode === "directory") assertDirectoryRoots(home);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@awebai/oats",
3
- "version": "0.24.10",
3
+ "version": "0.24.11",
4
4
  "description": "OATS (Open Agent Team Specification) — durable souls, disposable instances, targetable capability packages, and the runtime-neutral oats CLI/kernel.",
5
5
  "keywords": [
6
6
  "agents",