Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions packages/ats-skills-source.json
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,14 @@
"schema_version": "aether.ats.vendor/1",
"repository": "AetherAI3/ATSv2",
"path": "electron-app-v2/packages/aether-ats-skills",
"revision": "2c38586b7fb011163281fb964b2b9585398717e8",
"revision": "ecdbc5c296f28f2331aea4e031c459f1171b99ea",
"files": {
"LICENSE": "c71d239df91726fc519c6eb72d318ec65820627232b2f796219e87dcf35d0ab4",
"README.md": "ccad3cffc19e5f9d451dfce9d708adb1c88c1ac0acaa85c5c809b04b62e64718",
"SETTINGS.md": "aa4622824e2a15798e2bf3a139c55f824db346458fc48d85c5aacf0cb555dd9f",
"bin/aether-ats-skills.js": "cf13b49ce174f5bb13da0daa93e38e3fbb926c44beaa7d7a7b827c8c2cba4c3d",
"package.json": "1a40fd6e767e21f823c251a70ed37cb9e34448a566fe16c2b54e9680086c4f29",
"python/bridge.py": "8b3ad23101f2f36acdf1ff1e1481be2b0116947cdc664a942563ca59baf4f671",
"python/bridge.py": "4df7be96d14456d23292a8f0dbf71ceff2ed73b27f25b5ede74eabe87fd916aa",
"python/memory_lease.py": "d445fc8e59adc749d67ff7dc539d45017a8e56af22420be69ff0bccc9ece1532",
"src/browser.js": "b816ff29bec0d4e38f62a5fcd77d6d16aaa8b06d3a2438a595cdfd20d38f31f0",
"src/browser_recovery.js": "f92324dfd104772e1f32f0b6d6eaf890a45929a1209652b0937b2c62d721ad08",
Expand Down
24 changes: 20 additions & 4 deletions packages/ats-skills/python/bridge.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,24 @@ def sync_directory(path):
os.close(descriptor)


def sync_native_file(path: Path):
"""Flush a native file using a writable descriptor on every platform.

Windows rejects fsync on a read-only descriptor (Errno 9). Native setup
files are owned by this adapter, so opening them read/write is safe and
keeps the durability check meaningful on both POSIX and Windows.
"""
reject_links(path)
flags = os.O_RDWR | getattr(os, "O_BINARY", 0) | getattr(os, "O_NOFOLLOW", 0)
descriptor = os.open(path, flags)
try:
if not stat.S_ISREG(os.fstat(descriptor).st_mode):
raise ValueError("Native memory file must be a regular file")
os.fsync(descriptor)
finally:
os.close(descriptor)


def atomic_json(path: Path, value):
temp = path.with_name(path.name + "." + uuid.uuid4().hex + ".tmp")
try:
Expand Down Expand Up @@ -320,8 +338,7 @@ def initialize_memory(request):
native_config["dir"] = str(path)
(stage / "config.json").write_text(json.dumps(native_config, sort_keys=True, indent=2), encoding="utf-8")
for name in NATIVE_FILES:
with (stage / name).open("rb") as handle:
os.fsync(handle.fileno())
sync_native_file(stage / name)
transaction = {**transaction, "phase": "publishing", "files": {name: digest(stage / name) for name in sorted(NATIVE_FILES)}}
atomic_json(transaction_path, transaction)
if transaction and transaction["phase"] == "publishing":
Expand Down Expand Up @@ -391,8 +408,7 @@ def close_verification_pool():
# The binding is the readiness commit: all native files were reopened.
if not existing:
for name in NATIVE_FILES:
with (path / name).open("rb") as handle:
os.fsync(handle.fileno())
sync_native_file(path / name)
atomic_json(binding_path, receipt)
if transaction:
if stage.exists():
Expand Down
9 changes: 8 additions & 1 deletion src/commands/ats_agent.ts
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,14 @@ async function loadPackage(): Promise<AtsPackage> {
}

function verifyMemory(receipt: Record<string, unknown>, binding: Binding): void {
if (receipt["state"] !== "ready" || receipt["persistence_verified"] !== true
if (receipt["state"] !== "ready") {
const code = typeof receipt["code"] === "string" ? receipt["code"].replace(/\s+/g, " ").slice(0, 120) : "";
const message = typeof receipt["message"] === "string" ? receipt["message"].replace(/\s+/g, " ").slice(0, 300) : "";
const detail = [code, message].filter(Boolean).join(": ");
if (detail) throw new Error(`ATS memory setup unavailable${detail ? ` (${detail})` : ""}.`);
throw new Error("ATS memory did not return a verified ready receipt matching this agent, directory and size.");
}
if (receipt["persistence_verified"] !== true
|| receipt["agent_id"] !== binding.agent_id || receipt["directory"] !== binding.memory_directory
|| receipt["size_gb"] !== binding.memory_gb
|| receipt["schema_version"] !== "aether.ats.memory/1" || receipt["backend"] !== "aether-context"
Expand Down
12 changes: 12 additions & 0 deletions test/ats_agent.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,18 @@ test("setup failure preserves the created Cloud draft and never saves a ready lo
});
});

test("ATS setup surfaces a safe actionable memory-engine failure", async () => {
await fixture(async (dir) => {
let output = "";
const hooks = createAtsHooks({ root: dir, output: (text) => { output += text; }, setup: async () => ({ memoryGb: 5, strategiesDirectory: join(dir, "strategies") }),
load: async () => fakePackage({ initializeMemory: async () => ({ state: "unavailable", code: "CONTEXT_ENGINE_UNAVAILABLE", message: "Install the pinned aether-context engine, or select its Python interpreter." }) }) });
await withCreate(async () => { assert.equal(await hooks.createATS!(context(), "Market Scout"), 1); });
assert.match(output, /CONTEXT_ENGINE_UNAVAILABLE/);
assert.match(output, /Install the pinned aether-context engine/);
assert.doesNotMatch(output, /token|api.?key|aek_/i);
});
});

test("successful setup stores an account-scoped binding only after memory and strategy checks", async () => {
await fixture(async (dir) => {
const calls: string[] = [];
Expand Down
Loading